Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions apps/web/src/components/BranchToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import {
} from "./ui/menu";
import { Separator } from "./ui/separator";
import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip";
import { MiddleTruncate } from "./ui/middle-truncate";
import { ComposerSurface } from "./chat/ComposerSurface";
import { useComposerMenuProps } from "./chat/composerEventScope";
import { measureRestingComposerControls } from "./chat/restingComposerControlsMeasurement";
Expand Down Expand Up @@ -283,9 +284,7 @@ const MobileRunContextSelector = memo(function MobileRunContextSelector({
) : (
<FolderIcon className="size-3" />
)}
<span className="min-w-0 truncate">
{resolveCurrentWorkspaceLabel(activeWorktreePath)}
</span>
<MiddleTruncate value={resolveCurrentWorkspaceLabel(activeWorktreePath)} />
</span>
</MenuRadioItem>
<MenuRadioItem disabled={envModeLocked} value="worktree" closeOnClick>
Expand All @@ -298,7 +297,7 @@ const MobileRunContextSelector = memo(function MobileRunContextSelector({
<MenuRadioItem disabled={envModeLocked} value="previous-worktree" closeOnClick>
<span className="flex min-w-0 items-center gap-1.5">
<HistoryIcon className="size-3" />
<span className="min-w-0 truncate">{previousWorktreeLabel}</span>
<MiddleTruncate value={previousWorktreeLabel} />
</span>
</MenuRadioItem>
) : null}
Expand Down
12 changes: 6 additions & 6 deletions apps/web/src/components/BranchToolbarBranchSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ import {
} from "./ui/combobox";
import { stackedThreadToast, toastManager } from "./ui/toast";
import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip";
import { MiddleTruncate } from "./ui/middle-truncate";

export interface BranchToolbarBranchSelectorHandle {
open: () => void;
Expand Down Expand Up @@ -757,7 +758,7 @@ export function BranchToolbarBranchSelector({
onContextMenu={(event) => handleBranchContextMenu(event, itemValue)}
>
<div className="flex w-full min-w-0 items-center justify-between gap-2">
<span className="min-w-0 flex-1 truncate">{itemValue}</span>
<MiddleTruncate value={itemValue} className="flex-1" />
{badge && <span className="shrink-0 text-[10px] text-muted-foreground/45">{badge}</span>}
</div>
</ComboboxItem>
Expand Down Expand Up @@ -818,12 +819,11 @@ export function BranchToolbarBranchSelector({
data-composer-label
className="min-w-0 max-w-[240px] group-data-[compact]/composer-context:max-w-0"
>
<span
<MiddleTruncate
value={triggerLabel}
data-composer-label-motion
className="block w-full min-w-0 max-w-[240px] truncate transition-opacity duration-180 ease-[cubic-bezier(0.32,0.72,0,1)] group-data-[compact]/composer-context:opacity-0 motion-reduce:transition-none"
>
{triggerLabel}
</span>
className="flex w-full max-w-[240px] transition-opacity duration-180 ease-[cubic-bezier(0.32,0.72,0,1)] group-data-[compact]/composer-context:opacity-0 motion-reduce:transition-none"
/>
</span>
<ChevronDownIcon className="size-3 shrink-0 opacity-50" />
</ComboboxTrigger>
Expand Down
11 changes: 7 additions & 4 deletions apps/web/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ import { SidebarChromeFooter, SidebarChromeHeader } from "./sidebar/SidebarChrom
import { SidebarHeaderIconButton, SidebarThreadHeader } from "./sidebar/SidebarThreadHeader";
import { Popover, PopoverPopup, PopoverTrigger } from "./ui/popover";
import { Tooltip, TooltipPopup, TooltipProvider, TooltipTrigger } from "./ui/tooltip";
import { MiddleTruncate } from "./ui/middle-truncate";
import {
composerDraftHasUserContent,
DraftId,
Expand Down Expand Up @@ -379,7 +380,7 @@ function SidebarThreadTooltip({
{thread.branch ? (
<div className="flex min-w-0 items-center gap-2">
<GitBranchIcon className="size-3 shrink-0 stroke-muted-foreground" />
<div className="min-w-0 truncate text-foreground/75">{thread.branch}</div>
<MiddleTruncate value={thread.branch} className="flex text-foreground/75" />
</div>
) : null}
{branchMismatch ? (
Expand Down Expand Up @@ -1932,9 +1933,11 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
{thread.branch ? (
<>
<ThreadWorktreeIndicator thread={thread} />
<span className="min-w-0 flex-1 truncate whitespace-nowrap text-muted-foreground/40">
{thread.branch}
</span>
<MiddleTruncate
value={thread.branch}
showTitle={false}
className="flex-1 text-muted-foreground/40"
/>
</>
) : (
<span className="flex-1" />
Expand Down
4 changes: 3 additions & 1 deletion apps/web/src/components/ThreadCommandSubtitle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { ProjectFavicon, type ProjectFaviconProject } from "./ProjectFavicon";
import { ProviderInstanceIcon } from "./chat/ProviderInstanceIcon";
import { cn } from "~/lib/utils";

import { MiddleTruncate } from "./ui/middle-truncate";

/**
* Flip this while reviewing command-palette thread subtitles.
* - favicon-workspace-harness: favicon + Folder/FolderGit2 + branch + harness (default)
Expand Down Expand Up @@ -84,7 +86,7 @@ export function ThreadCommandSubtitle(props: {
{projectLabel ? <CommandPaletteMetaDot /> : null}
<span className="inline-flex min-w-0 items-center gap-1">
<WorkspaceIcon variant={variant} isWorktree={isWorktree} />
<span className="min-w-0 truncate">{branchLabel}</span>
<MiddleTruncate value={branchLabel} />
</span>
</>
) : null}
Expand Down
8 changes: 5 additions & 3 deletions apps/web/src/components/chat/ChangedFilesTree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { DiffStatLabel, hasNonZeroStat } from "./DiffStatLabel";
import { PierreEntryIcon } from "./PierreEntryIcon";
import { Button } from "../ui/button";
import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
import { MiddleTruncate } from "../ui/middle-truncate";

const EMPTY_DIRECTORY_OVERRIDES: Record<string, boolean> = {};

Expand Down Expand Up @@ -246,9 +247,10 @@ export const ChangedFilesTree = memo(function ChangedFilesTree(props: {
theme={resolvedTheme}
className="size-3.5 text-muted-foreground/70"
/>
<span className="truncate font-mono text-xs text-foreground/85 group-hover:text-foreground">
{node.name}
</span>
<MiddleTruncate
value={node.name}
className="font-mono text-xs text-foreground/85 group-hover:text-foreground"
/>
{node.stat && (
<span className="ml-auto shrink-0 font-mono text-[10px] tabular-nums">
<DiffStatLabel additions={node.stat.additions} deletions={node.stat.deletions} />
Expand Down
13 changes: 10 additions & 3 deletions apps/web/src/components/chat/WorktreeSetupCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { useEffect, useState, type ReactNode } from "react";

import { Button } from "~/components/ui/button";
import { Spinner } from "~/components/ui/spinner";
import { MiddleTruncate } from "../ui/middle-truncate";
import { observeVisibleAnimation } from "~/lib/visibleAnimation";
import { cn } from "~/lib/utils";

Expand Down Expand Up @@ -260,19 +261,25 @@ function SetupDetails({ snapshot }: { snapshot: WorktreeSetupSnapshot }) {
{snapshot.branch ? (
<>
<dt className="text-foreground/80">Branch</dt>
<dd className="truncate font-mono">{snapshot.branch}</dd>
<dd className="min-w-0 font-mono">
<MiddleTruncate value={snapshot.branch} className="flex" />
</dd>
</>
) : null}
{snapshot.baseRef ? (
<>
<dt className="text-foreground/80">Base</dt>
<dd className="truncate font-mono">{snapshot.baseRef}</dd>
<dd className="min-w-0 font-mono">
<MiddleTruncate value={snapshot.baseRef} className="flex" />
</dd>
</>
) : null}
{snapshot.worktreePath ? (
<>
<dt className="text-foreground/80">Path</dt>
<dd className="truncate font-mono">{snapshot.worktreePath}</dd>
<dd className="min-w-0 font-mono">
<MiddleTruncate value={snapshot.worktreePath} className="flex" />
</dd>
</>
) : null}
{snapshot.setupScript ? (
Expand Down
21 changes: 16 additions & 5 deletions apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ import {
import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover";
import { toastManager } from "../ui/toast";
import { Tooltip, TooltipPopup, TooltipProvider, TooltipTrigger } from "../ui/tooltip";
import { MiddleTruncate } from "../ui/middle-truncate";
import { PullRequestDetailGhost, PullRequestTimelineGhost } from "./PullRequestGhosts";
import { PullRequestCopyableCode } from "./PullRequestCopyableCode";
import { PullRequestActivityUnavailableState } from "./PullRequestActivityUnavailableState";
Expand Down Expand Up @@ -2267,7 +2268,9 @@ export function PullRequestDetailPanel({
className="size-3 shrink-0"
/>
) : null}
<code className="min-w-0 truncate">{detail.baseBranch}</code>
<code className="flex min-w-0">
<MiddleTruncate value={detail.baseBranch} showTitle={false} />
</code>
</PullRequestBaseFreshnessWarning>
) : (
<Tooltip>
Expand All @@ -2280,7 +2283,9 @@ export function PullRequestDetailPanel({
className="size-3 shrink-0"
/>
) : null}
<code className="min-w-0 truncate">{detail.baseBranch}</code>
<code className="flex min-w-0">
<MiddleTruncate value={detail.baseBranch} showTitle={false} />
</code>
</span>
}
/>
Expand All @@ -2298,7 +2303,9 @@ export function PullRequestDetailPanel({
<Tooltip>
<TooltipTrigger
render={
<code className="min-w-0 flex-1 truncate">{detail.headBranch}</code>
<code className="flex min-w-0 flex-1">
<MiddleTruncate value={detail.headBranch} showTitle={false} />
</code>
}
/>
<TooltipPopup side="top">{detail.headBranch}</TooltipPopup>
Expand Down Expand Up @@ -2452,7 +2459,9 @@ export function PullRequestDetailPanel({
className="size-3 shrink-0"
/>
) : null}
<code className="min-w-0 truncate">{detail.baseBranch}</code>
<code className="flex min-w-0">
<MiddleTruncate value={detail.baseBranch} showTitle={false} />
</code>
</PullRequestBaseFreshnessWarning>
) : (
<Tooltip>
Expand All @@ -2465,7 +2474,9 @@ export function PullRequestDetailPanel({
className="size-3 shrink-0"
/>
) : null}
<code className="min-w-0 truncate">{detail.baseBranch}</code>
<code className="flex min-w-0">
<MiddleTruncate value={detail.baseBranch} showTitle={false} />
</code>
</span>
}
/>
Expand Down
5 changes: 4 additions & 1 deletion apps/web/src/components/pullRequest/PullRequestGhosts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { formatRelativeTimeLabel } from "~/timestampFormat";

import { Button, InlineButton } from "../ui/button";
import { Toggle, ToggleGroup } from "../ui/toggle-group";
import { MiddleTruncate } from "../ui/middle-truncate";
import { PullRequestCopyableCode } from "./PullRequestCopyableCode";
import {
PullRequestActorLabel,
Expand Down Expand Up @@ -252,7 +253,9 @@ export function PullRequestDetailGhost({
<span className="flex min-w-0 flex-1 items-center gap-1.5 font-mono text-xs text-muted-foreground/70">
{seed ? (
<span className="inline-flex min-w-0 max-w-[40%] shrink-0 items-center gap-1">
<code className="min-w-0 truncate">{seed.baseBranch}</code>
<code className="flex min-w-0">
<MiddleTruncate value={seed.baseBranch} />
</code>
</span>
) : (
<span className="inline-flex min-w-0 max-w-[40%] shrink-0 items-center gap-1">
Expand Down
13 changes: 10 additions & 3 deletions apps/web/src/components/pullRequest/PullRequestListRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { ReactNode } from "react";
import { cn } from "~/lib/utils";
import { formatRelativeTimeLabel } from "~/timestampFormat";

import { MiddleTruncate } from "../ui/middle-truncate";
import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
import {
PullRequestActorAvatar,
Expand Down Expand Up @@ -148,11 +149,17 @@ export function PullRequestRowAuthor({
);
}

/** `head → base`, in the mono the branches are typed in. */
/**
* `head → base`, in the mono the branches are typed in, each cut in the middle when the row is
* short of room. The base keeps its width up to a share of the line, so a long head cannot
* squeeze a short `main` out; the arrow stays readable so the two are not read as one name.
*/
export function PullRequestRowBranches({ head, base }: { head: string; base: string }) {
return (
<span className="truncate font-mono">
{head} → {base}
<span className="flex min-w-0 items-center gap-1 font-mono">
<MiddleTruncate value={head} />
<span className="shrink-0">→</span>
<MiddleTruncate value={base} className="max-w-[45%] shrink-0" />
</span>
);
}
73 changes: 73 additions & 0 deletions apps/web/src/components/ui/middle-truncate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import type { ComponentProps } from "react";

import { cn } from "~/lib/utils";

/**
* Truncates in the middle, the way Finder does, for strings that carry meaning at both ends:
* branch names, paths, worktree names, shas. A tail-cut `fix/cache-main-20260918-180825` loses
* the date that tells two branches apart; a path loses its file name.
*
* CSS has no middle ellipsis, so the string is split into a head that truncates and a tail
* that does not. No measuring and no observers, so it costs the same as `truncate` in a long
* list and works inside `content-visibility` skipping. Both halves are real text: selection
* and copy give the whole string, and a screen reader reads it through.
*/
export function MiddleTruncate({
value,
tail,
showTitle = true,
className,
...props
}: Omit<ComponentProps<"span">, "children"> & {
value: string;
/** Characters kept at the end. Defaults to the last path segment, capped, or 10. */
tail?: number;
/** The full value on hover. Off where a tooltip already carries it. */
showTitle?: boolean;
}) {
const split = splitForMiddleTruncate(value, tail);
return (
<span
{...(showTitle ? { title: value } : {})}
className={cn("inline-flex min-w-0 max-w-full overflow-hidden whitespace-nowrap", className)}
{...props}
>
{split ? (
<>
<span className="min-w-0 truncate">{split.head}</span>
<span className="shrink-0">{split.tail}</span>
</>
) : (
<span className="min-w-0 truncate">{value}</span>
)}
</span>
);
}

const DEFAULT_TAIL = 10;
const MAX_SEGMENT_TAIL = 16;

/**
* Where to cut. A path keeps its last segment when that is short enough to be the useful
* part; anything else keeps a fixed count. Nothing is split when the tail would be most of
* the string, since then the head could never show enough to be worth an ellipsis.
*/
export function splitForMiddleTruncate(
value: string,
tail?: number,
): { head: string; tail: string } | null {
// Code points, not UTF-16 units: a cut inside a surrogate pair would render two broken
// glyphs where an emoji or a CJK extension character used to be.
const chars = Array.from(value);
let keep = tail ?? DEFAULT_TAIL;
if (tail === undefined) {
const slash = chars.lastIndexOf("/");
if (slash > 0 && slash < chars.length - 1) {
const segment = chars.length - slash - 1;
keep = segment <= MAX_SEGMENT_TAIL ? segment : DEFAULT_TAIL;
}
}
if (keep <= 0 || chars.length <= keep + 4) return null;
const cut = chars.length - keep;
return { head: chars.slice(0, cut).join(""), tail: chars.slice(cut).join("") };
}
Loading