diff --git a/src/renderer/src/components/WorktreeJumpPalette.tsx b/src/renderer/src/components/WorktreeJumpPalette.tsx index 45a31847834..a89fdb8ca53 100644 --- a/src/renderer/src/components/WorktreeJumpPalette.tsx +++ b/src/renderer/src/components/WorktreeJumpPalette.tsx @@ -43,6 +43,7 @@ import { CommandEmpty, CommandItem } from '@/components/ui/command' +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' import { parseGitHubIssueOrPRNumber, parseGitHubIssueOrPRLink } from '@/lib/github-links' import { getLinkedWorkItemSuggestedName, getLinkedWorkItemWorkspaceName } from '@/lib/new-workspace' import type { LinkedWorkItemSummary } from '@/lib/new-workspace' @@ -419,50 +420,113 @@ function PaletteOpenTabPrimaryLine({ titleRange, secondaryText, secondaryRange, - worktreeName, - worktreeRange, leadingBadges }: { title: string titleRange: MatchRange | null secondaryText: string secondaryRange: MatchRange | null - worktreeName: string - worktreeRange: MatchRange | null leadingBadges?: React.ReactNode }): React.JSX.Element { // Why gate on non-empty: empty secondaries (terminals/simulators) used to still - // render two "·" separators, which read as a double mark and stole width from - // the worktree name until it collided with host/repo badges. + // render a leftover "·" after the title. const showSecondary = secondaryText.trim().length > 0 - const showWorktree = worktreeName.trim().length > 0 return (
- + {leadingBadges} {showSecondary ? ( <> · - + ) : null} - {showWorktree ? ( - <> - · - - - - - ) : null}
) } +function resolveOpenTabWorktreeRailTooltip({ + isBranch, + truncated, + name +}: { + isBranch: boolean + truncated: boolean + name: string +}): string { + if (truncated) { + return name + } + return isBranch + ? translate('auto.components.WorktreeJumpPalette.paletteOpenTabBranch', 'Branch name') + : translate('auto.components.WorktreeJumpPalette.paletteOpenTabWorkspace', 'Workspace name') +} + +function PaletteOpenTabWorktreeRailLabel({ + name, + matchRange, + worktree, + className, + slot = 'palette-open-tab-worktree' +}: { + name: string + matchRange: MatchRange | null + worktree?: Pick | null + className?: string + slot?: string +}): React.JSX.Element | null { + const [truncated, setTruncated] = useState(false) + const labelRef = useRef(null) + // Why: observe in an effect so unmount disconnects the ResizeObserver instead of + // leaking the callback-ref subscription (react-doctor effect-needs-cleanup). + useLayoutEffect(() => { + const node = labelRef.current + if (!node) { + setTruncated(false) + return + } + const updateTruncated = (): void => { + const next = node.scrollWidth > node.clientWidth + setTruncated((current) => (current === next ? current : next)) + } + updateTruncated() + if (typeof ResizeObserver === 'undefined') { + return + } + const observer = new ResizeObserver(updateTruncated) + observer.observe(node) + return () => observer.disconnect() + }, [name]) + + if (name.trim().length === 0) { + return null + } + // Why tag the visible value: a custom display name or folder path is a workspace + // label, not a branch, even when the workspace sits on one. + const isBranch = worktree != null && name === resolveWorktreeBranchLabel(worktree) + const tooltip = resolveOpenTabWorktreeRailTooltip({ isBranch, truncated, name }) + return ( + + + + + + + + {tooltip} + + + ) +} + function PaletteState({ title, subtitle }: { title: string; subtitle: string }): React.JSX.Element { return (
@@ -2686,16 +2750,13 @@ function WorktreeJumpPaletteContent({ )} )} - - {entry.match.displayNameRange ? ( - - ) : ( - worktreeLabel - )} - + {isCurrentWorktree && ( {translate( @@ -2712,17 +2773,18 @@ function WorktreeJumpPaletteContent({ )} )} - · - - {entry.match.branchRange ? ( - 0 ? ( + <> + · + - ) : ( - branch - )} - + + ) : null}
{entry.match.supportingText && (
@@ -2883,8 +2945,6 @@ function WorktreeJumpPaletteContent({ titleRange={result.titleRange} secondaryText={result.secondaryText} secondaryRange={result.secondaryRange} - worktreeName={result.worktreeName} - worktreeRange={result.worktreeRange} leadingBadges={ <> {result.isCurrentTab && ( @@ -2908,6 +2968,12 @@ function WorktreeJumpPaletteContent({ />
+ {workspaceTabRepoName && ( @@ -2962,8 +3028,6 @@ function WorktreeJumpPaletteContent({ titleRange={result.titleRange} secondaryText={result.secondaryText} secondaryRange={result.secondaryRange} - worktreeName={result.worktreeName} - worktreeRange={result.worktreeRange} leadingBadges={ <> {result.isCurrentTab && ( @@ -2987,6 +3051,12 @@ function WorktreeJumpPaletteContent({ />
+ {simulatorRepoName && ( @@ -3038,8 +3108,6 @@ function WorktreeJumpPaletteContent({ titleRange={result.titleRange} secondaryText={result.secondaryText} secondaryRange={result.secondaryRange} - worktreeName={result.worktreeName} - worktreeRange={result.worktreeRange} leadingBadges={ <> {result.isCurrentPage && ( @@ -3063,6 +3131,12 @@ function WorktreeJumpPaletteContent({ />
+ {browserRepoName && ( @@ -3132,9 +3206,11 @@ function WorktreeJumpPaletteContent({ ) return ( - - {paletteDialog} - + + + {paletteDialog} + + ) } diff --git a/src/renderer/src/components/worktree-jump-palette-interleaved-sections.test.tsx b/src/renderer/src/components/worktree-jump-palette-interleaved-sections.test.tsx index aad685c5666..425522acbb3 100644 --- a/src/renderer/src/components/worktree-jump-palette-interleaved-sections.test.tsx +++ b/src/renderer/src/components/worktree-jump-palette-interleaved-sections.test.tsx @@ -102,7 +102,11 @@ function makeRepo(): Repo { } } -function makeWorktree(id: string, displayName: string): Worktree { +function makeWorktree( + id: string, + displayName: string, + overrides: Partial = {} +): Worktree { return { id, repoId: 'repo-1', @@ -120,7 +124,8 @@ function makeWorktree(id: string, displayName: string): Worktree { isUnread: false, isPinned: false, sortOrder: 0, - lastActivityAt: 0 + lastActivityAt: 0, + ...overrides } } @@ -335,4 +340,56 @@ describe('WorktreeJumpPalette interleaved primary sections', () => { expect(testContainer.textContent).toContain('Open Tabs') expect(testContainer.textContent).not.toContain('Worktrees') }) + + it('puts the tab title on the left and the worktree name in the badge rail', async () => { + const longTitle = 'macOS Orca App Permission Update Delivery' + await renderPalette({ + worktreesByRepo: { + 'repo-1': [makeWorktree('wt-tabs', 'user-support')] + }, + showSleepingWorkspaces: true, + ptyIdsByTabId: { 'term-0': ['pty-0'] }, + tabsByWorktree: { + 'wt-tabs': [makeTerminalTab('term-0', 'wt-tabs', longTitle)] + }, + unifiedTabsByWorktree: { + 'wt-tabs': [makeUnifiedTab('tab-0', 'wt-tabs', 'term-0', longTitle)] + }, + groupsByWorktree: { 'wt-tabs': [makeGroup('wt-tabs', ['tab-0'])] }, + activeGroupIdByWorktree: { 'wt-tabs': 'group-wt-tabs' } + }) + + const row = testContainer.querySelector('[data-command-item="workspace-tab:tab-0"]') + expect(row).not.toBeNull() + const title = row?.querySelector('[data-slot="palette-open-tab-title"]') + const worktree = row?.querySelector('[data-slot="palette-open-tab-worktree"]') + expect(title?.textContent).toBe(longTitle) + expect(worktree?.textContent).toBe('user-support') + expect(worktree?.compareDocumentPosition(title ?? document.createElement('span'))).toBe( + Node.DOCUMENT_POSITION_PRECEDING + ) + }) + + it('tags the worktree rail label as a branch when the visible name is the branch', async () => { + await renderPalette({ + worktreesByRepo: { + 'repo-1': [makeWorktree('wt-tabs', '', { displayName: '' })] + }, + showSleepingWorkspaces: true, + ptyIdsByTabId: { 'term-0': ['pty-0'] }, + tabsByWorktree: { + 'wt-tabs': [makeTerminalTab('term-0', 'wt-tabs', 'Query the API')] + }, + unifiedTabsByWorktree: { + 'wt-tabs': [makeUnifiedTab('tab-0', 'wt-tabs', 'term-0', 'Query the API')] + }, + groupsByWorktree: { 'wt-tabs': [makeGroup('wt-tabs', ['tab-0'])] }, + activeGroupIdByWorktree: { 'wt-tabs': 'group-wt-tabs' } + }) + + const row = testContainer.querySelector('[data-command-item="workspace-tab:tab-0"]') + expect(row).not.toBeNull() + const worktree = row?.querySelector('[data-slot="palette-open-tab-worktree"]') + expect(worktree?.textContent).toBe('main') + }) }) diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 9263a72bc89..74af211dac5 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -2127,7 +2127,9 @@ "pluginCommandFailed": "Could not run the plugin command.", "recentChatsTerminalsHeader": "Recent Chats & Terminals", "27f10cca63": "Search chats, terminals, worktrees, settings, and actions...", - "2770f02910": "Search chats, terminals, worktrees, settings, and actions" + "2770f02910": "Search chats, terminals, worktrees, settings, and actions", + "paletteOpenTabBranch": "Branch name", + "paletteOpenTabWorkspace": "Workspace name" }, "github": { "pr": {