diff --git a/src/renderer/src/components/WorktreeJumpPalette.tsx b/src/renderer/src/components/WorktreeJumpPalette.tsx
index a8adbec77a9..255d24b12aa 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'
@@ -410,50 +411,119 @@ 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 resizeObserverRef = useRef(null)
+ const handleRef = useCallback((node: HTMLSpanElement | null) => {
+ resizeObserverRef.current?.disconnect()
+ resizeObserverRef.current = null
+ 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)
+ resizeObserverRef.current = observer
+ }, [])
+
+ 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 (
@@ -2627,16 +2697,13 @@ export default function WorktreeJumpPalette(): React.JSX.Element | null {
)}
)}
-
- {entry.match.displayNameRange ? (
-
- ) : (
- worktreeLabel
- )}
-
+
{isCurrentWorktree && (
{translate(
@@ -2653,17 +2720,18 @@ export default function WorktreeJumpPalette(): React.JSX.Element | null {
)}
)}
-
·
-
- {entry.match.branchRange ? (
- 0 ? (
+ <>
+ ·
+
- ) : (
- branch
- )}
-
+ >
+ ) : null}
{entry.match.supportingText && (
@@ -2833,8 +2901,6 @@ export default function WorktreeJumpPalette(): React.JSX.Element | null {
titleRange={result.titleRange}
secondaryText={result.secondaryText}
secondaryRange={result.secondaryRange}
- worktreeName={result.worktreeName}
- worktreeRange={result.worktreeRange}
leadingBadges={
<>
{result.isCurrentTab && (
@@ -2858,6 +2924,12 @@ export default function WorktreeJumpPalette(): React.JSX.Element | null {
/>
+
{workspaceTabRepoName && (
@@ -2915,8 +2987,6 @@ export default function WorktreeJumpPalette(): React.JSX.Element | null {
titleRange={result.titleRange}
secondaryText={result.secondaryText}
secondaryRange={result.secondaryRange}
- worktreeName={result.worktreeName}
- worktreeRange={result.worktreeRange}
leadingBadges={
<>
{result.isCurrentTab && (
@@ -2940,6 +3010,12 @@ export default function WorktreeJumpPalette(): React.JSX.Element | null {
/>
+
{simulatorRepoName && (
@@ -2994,8 +3070,6 @@ export default function WorktreeJumpPalette(): React.JSX.Element | null {
titleRange={result.titleRange}
secondaryText={result.secondaryText}
secondaryRange={result.secondaryRange}
- worktreeName={result.worktreeName}
- worktreeRange={result.worktreeRange}
leadingBadges={
<>
{result.isCurrentPage && (
@@ -3019,6 +3093,12 @@ export default function WorktreeJumpPalette(): React.JSX.Element | null {
/>
+
{browserRepoName && (
@@ -3088,9 +3168,11 @@ export default function WorktreeJumpPalette(): React.JSX.Element | null {
)
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 3184344d835..07d511f4f56 100644
--- a/src/renderer/src/i18n/locales/en.json
+++ b/src/renderer/src/i18n/locales/en.json
@@ -2103,7 +2103,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": {