From ddb3913052bdb52e7649f89101b209e32cd8ed00 Mon Sep 17 00:00:00 2001
From: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
Date: Thu, 13 Aug 2026 11:28:24 -0700
Subject: [PATCH] Move worktree labels to right badge rail with truncation
tooltips (#14313)
* refactor(palette): move worktree labels to right badge rail with tooltip
Move worktree and branch names from the tab title line to a dedicated right-side badge rail, preventing long titles from being truncated. Add smart tooltips showing full names when truncated, and distinguish between workspace names and branch names based on whether they're auto-generated labels.
* fix(palette): disconnect worktree rail ResizeObserver on unmount
Move truncation observation into a layout effect so React Doctor sees a cleanup path and the subscription cannot leak after unmount.
(cherry picked from commit d957e1c0f5f95532ee3d5e9d59f6285a1badc2d8)
---
.../src/components/WorktreeJumpPalette.tsx | 166 +++++++++++++-----
...jump-palette-interleaved-sections.test.tsx | 61 ++++++-
src/renderer/src/i18n/locales/en.json | 4 +-
3 files changed, 183 insertions(+), 48 deletions(-)
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": {