From fee70ee08021706ccfadf1d10c78d5dd679af26b Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Thu, 10 Sep 2026 15:21:14 -0700 Subject: [PATCH] feat(cmd-j): compact palette location layout --- ...ee-jump-palette-browser-simulator-rows.tsx | 41 ++-- ...jump-palette-interleaved-sections.test.tsx | 7 +- .../worktree-jump-palette-primitives.test.tsx | 93 ++++++++- .../worktree-jump-palette-primitives.tsx | 187 ++++++++++-------- ...orktree-jump-palette-workspace-tab-row.tsx | 22 +-- 5 files changed, 226 insertions(+), 124 deletions(-) diff --git a/src/renderer/src/components/worktree-jump-palette-browser-simulator-rows.tsx b/src/renderer/src/components/worktree-jump-palette-browser-simulator-rows.tsx index 95220a25f2e..7ede7b29e03 100644 --- a/src/renderer/src/components/worktree-jump-palette-browser-simulator-rows.tsx +++ b/src/renderer/src/components/worktree-jump-palette-browser-simulator-rows.tsx @@ -1,15 +1,14 @@ import type React from 'react' import { Smartphone } from 'lucide-react' import { CommandItem } from '@/components/ui/command' -import { RepoBadgeMark } from '@/components/repo/RepoBadgeLabel' import { getPaletteHostBadge } from '@/components/cmd-j/palette-host-badge' import { cn } from '@/lib/utils' import { translate } from '@/i18n/i18n' import type { BrowserPaletteItem, SimulatorPaletteItem } from './worktree-jump-palette-model' import type { WorktreeJumpPaletteController } from './use-worktree-jump-palette-controller' import { - HighlightedText, PaletteHostBadgeChip, + PaletteLocationChip, PaletteOpenTabPrimaryLine, PaletteRowShortcutBadge } from './worktree-jump-palette-primitives' @@ -67,8 +66,6 @@ export function WorktreeJumpPaletteSimulatorRow({ secondaryText={result.secondaryText} secondaryRanges={result.secondaryRanges} secondaryMatches={result.secondaryMatches} - worktreeName={result.worktreeName} - worktreeRanges={result.worktreeRanges} sessionAge={simulatorSessionAge} leadingBadges={ <> @@ -94,16 +91,15 @@ export function WorktreeJumpPaletteSimulatorRow({ ) : null} -
+
- {simulatorRepoName && ( - - - - - - - )} + @@ -187,16 +181,15 @@ export function WorktreeJumpPaletteBrowserRow({ } />
-
+
- {browserRepoName && ( - - - - - - - )} + { 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(title?.classList.contains('flex-auto')).toBe(true) + expect(title?.classList.contains('min-w-0')).toBe(true) + expect(title?.classList.contains('shrink-0')).toBe(false) expect(worktree?.textContent).toBe('user-support') + const locationChip = worktree?.closest('[data-slot="palette-location-chip"]') + expect(locationChip).not.toBeNull() + expect(locationChip?.parentElement?.classList.contains('max-w-[40%]')).toBe(true) + expect(locationChip?.parentElement?.classList.contains('min-w-0')).toBe(true) expect(worktree?.compareDocumentPosition(title ?? document.createElement('span'))).toBe( Node.DOCUMENT_POSITION_PRECEDING ) diff --git a/src/renderer/src/components/worktree-jump-palette-primitives.test.tsx b/src/renderer/src/components/worktree-jump-palette-primitives.test.tsx index a54a48329be..39a5f6be3af 100644 --- a/src/renderer/src/components/worktree-jump-palette-primitives.test.tsx +++ b/src/renderer/src/components/worktree-jump-palette-primitives.test.tsx @@ -3,7 +3,7 @@ import { cleanup, render, type RenderResult, screen } from '@testing-library/react' import { afterEach, expect, it } from 'vitest' import { TooltipProvider } from '@/components/ui/tooltip' -import { PaletteOpenTabPrimaryLine } from './worktree-jump-palette-primitives' +import { PaletteLocationChip, PaletteOpenTabPrimaryLine } from './worktree-jump-palette-primitives' afterEach(() => cleanup()) @@ -18,8 +18,6 @@ function renderPrimaryLine( secondaryText="src/app.ts" secondaryRanges={[]} secondaryMatches={secondaryMatches} - worktreeName="Workspace" - worktreeRanges={[]} /> ) @@ -45,3 +43,92 @@ it('renders no badge when every secondary match is already shown', () => { expect(screen.queryByText(/^\+\d+$/)).toBeNull() }) + +it('elides a deep path from the head so the matched tail stays visible', () => { + const path = '/Users/me/projects/orca/new-create-button-design/proposals/create-button.html' + const start = path.indexOf('create-butt') + const { container } = render( + + + + ) + + const secondary = container.querySelector('[data-slot="palette-open-tab-secondary"]') + expect(secondary?.textContent).toBe(path) + const [head, tail] = Array.from(secondary?.children ?? []) + expect(head?.textContent).toBe('/Users/me/projects/orca') + expect(tail?.textContent).toBe('/new-create-button-design/proposals/create-button.html') + expect(tail?.querySelector('.font-semibold')?.textContent).toBe('create-butt') +}) + +it('folds the worktree into the repo chip and drops it when it repeats the repo name', () => { + const { container, rerender } = render( + + + + ) + expect(container.querySelector('[data-slot="palette-location-chip"]')?.textContent).toBe( + 'orca·new-create-button-design' + ) + const chip = container.querySelector('[data-slot="palette-location-chip"]') + const repo = container.querySelector('[data-slot="palette-location-repo"]') + const worktree = container.querySelector('[data-slot="palette-open-tab-worktree"]') + expect(chip?.className).toContain('overflow-hidden') + expect(chip?.className).toContain('min-w-0') + expect(repo?.className).toContain('max-w-[55%]') + expect(repo?.className).toContain('min-w-[3ch]') + expect(worktree?.className).toContain('min-w-[3ch]') + expect(worktree?.getAttribute('data-state')).toBeNull() + expect(worktree?.getAttribute('tabindex')).toBeNull() + + rerender( + + + + ) + expect(container.querySelector('[data-slot="palette-location-chip"]')?.textContent).toBe('orca') + expect( + container.querySelector('[data-slot="palette-location-repo"] .font-semibold')?.textContent + ).toBe('orca') +}) + +it('keeps a short title at its natural width so the session age stays beside it', () => { + const { container } = render( + + + + ) + + const title = container.querySelector('[data-slot="palette-open-tab-title"]') + expect(title?.className).toContain('min-w-0') + expect(title?.className).not.toContain('shrink-0') +}) + +it('caps the title only when it shares the line with secondary text', () => { + const { container } = renderPrimaryLine([]) + + const title = container.querySelector('[data-slot="palette-open-tab-title"]') + expect(title?.className).toContain('max-w-[62%]') + expect(title?.className).toContain('shrink-0') +}) diff --git a/src/renderer/src/components/worktree-jump-palette-primitives.tsx b/src/renderer/src/components/worktree-jump-palette-primitives.tsx index 4162a56cb6e..7c7c01585fc 100644 --- a/src/renderer/src/components/worktree-jump-palette-primitives.tsx +++ b/src/renderer/src/components/worktree-jump-palette-primitives.tsx @@ -1,11 +1,12 @@ -import React, { useLayoutEffect, useRef, useState } from 'react' +import React from 'react' import { ShortcutKeyCombo } from '@/components/ShortcutKeyCombo' import { translate } from '@/i18n/i18n' import type { PaletteHostBadge } from '@/components/cmd-j/palette-host-badge' import type { MatchRange, PaletteSearchResult } from '@/lib/worktree-palette-search' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' -import type { Worktree } from '../../../shared/worktree/types' -import { resolveWorktreeBranchLabel } from '@/lib/worktree-default-display-name' +import { splitPathHeadForElision } from '@/lib/path-head-elision' +import { RepoBadgeMark } from '@/components/repo/RepoBadgeLabel' +import { cn } from '@/lib/utils' const NO_SECONDARY_MATCHES: readonly { text: string; ranges: readonly MatchRange[] }[] = [] @@ -67,14 +68,48 @@ export function HighlightedText({ return <>{parts} } +function PaletteOpenTabSecondaryText({ + text, + ranges +}: { + text: string + ranges: readonly MatchRange[] +}): React.JSX.Element { + const split = splitPathHeadForElision(text, ranges) + const content = split ? ( + + {split.head} + + + + + ) : ( + + + + ) + return ( + + {content} + + {text} + + + ) +} + export function PaletteOpenTabPrimaryLine({ title, titleRanges, secondaryText, secondaryRanges, secondaryMatches = NO_SECONDARY_MATCHES, - worktreeName, - worktreeRanges, sessionAge, leadingBadges }: { @@ -83,13 +118,10 @@ export function PaletteOpenTabPrimaryLine({ secondaryText: string secondaryRanges: readonly MatchRange[] secondaryMatches?: readonly { text: string; ranges: readonly MatchRange[] }[] - worktreeName: string - worktreeRanges: readonly MatchRange[] sessionAge?: string leadingBadges?: React.ReactNode }): React.JSX.Element { const showSecondary = secondaryText.trim().length > 0 - const showWorktree = worktreeName.trim().length > 0 const additionalSecondaryMatches = secondaryMatches.filter( (match) => match.text && match.text !== secondaryText ) @@ -98,7 +130,10 @@ export function PaletteOpenTabPrimaryLine({
@@ -116,12 +151,7 @@ export function PaletteOpenTabPrimaryLine({ ) : null} {leadingBadges} {showSecondary ? ( - <> - · - - - - + ) : null} {additionalSecondaryMatches.length ? ( <> @@ -154,85 +184,76 @@ export function PaletteOpenTabPrimaryLine({ ) : null} - {showWorktree ? ( - <> - · - - - - - ) : null}
) } -function resolveOpenTabWorktreeRailTooltip({ - isBranch, - truncated, - name +export function PaletteLocationChip({ + repoName, + repoRanges, + repoColor, + worktreeName, + worktreeRanges, + className }: { - 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') -} - -export function PaletteOpenTabWorktreeRailLabel({ - name, - matchRanges, - worktree, - className, - slot = 'palette-open-tab-worktree' -}: { - name: string - matchRanges: readonly MatchRange[] - worktree?: Pick | null + repoName: string + repoRanges: readonly MatchRange[] + repoColor?: string + worktreeName: string + worktreeRanges: readonly MatchRange[] className?: string - slot?: string }): React.JSX.Element | null { - const [truncated, setTruncated] = useState(false) - const labelRef = useRef(null) - 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) { + const showRepo = repoName.trim().length > 0 + const repeatedName = showRepo && worktreeName === repoName + const showWorktree = worktreeName.trim().length > 0 && !repeatedName + if (!showRepo && !showWorktree) { return null } - const isBranch = worktree != null && name === resolveWorktreeBranchLabel(worktree) - const tooltip = resolveOpenTabWorktreeRailTooltip({ isBranch, truncated, name }) + const repoMatchRanges = repeatedName + ? [...repoRanges, ...worktreeRanges].sort((left, right) => left.start - right.start) + : repoRanges + const label = [showRepo ? repoName : '', showWorktree ? worktreeName : ''] + .filter(Boolean) + .join(' · ') + const chip = ( + + {showRepo ? ( + <> + + + + + + ) : null} + {showRepo && showWorktree ? ( + + · + + ) : null} + {showWorktree ? ( + + + + ) : null} + + ) return ( - - - - - - - {tooltip} + {chip} + + {label} ) diff --git a/src/renderer/src/components/worktree-jump-palette-workspace-tab-row.tsx b/src/renderer/src/components/worktree-jump-palette-workspace-tab-row.tsx index 787cbff449c..f60131191ef 100644 --- a/src/renderer/src/components/worktree-jump-palette-workspace-tab-row.tsx +++ b/src/renderer/src/components/worktree-jump-palette-workspace-tab-row.tsx @@ -3,15 +3,14 @@ import { FileText, SquareTerminal } from 'lucide-react' import { AgentIcon } from '@/lib/agent-catalog' import { CommandItem } from '@/components/ui/command' import { PaletteRecentTabStatusDot } from '@/components/cmd-j/palette-live-status' -import { RepoBadgeMark } from '@/components/repo/RepoBadgeLabel' import { getPaletteHostBadge } from '@/components/cmd-j/palette-host-badge' import { cn } from '@/lib/utils' import { translate } from '@/i18n/i18n' import type { WorkspaceTabPaletteItem } from './worktree-jump-palette-model' import type { WorktreeJumpPaletteController } from './use-worktree-jump-palette-controller' import { - HighlightedText, PaletteHostBadgeChip, + PaletteLocationChip, PaletteOpenTabPrimaryLine, PaletteRowShortcutBadge } from './worktree-jump-palette-primitives' @@ -76,8 +75,6 @@ export function WorktreeJumpPaletteWorkspaceTabRow({ secondaryText={result.secondaryText} secondaryRanges={result.secondaryRanges} secondaryMatches={result.secondaryMatches} - worktreeName={result.worktreeName} - worktreeRanges={result.worktreeRanges} sessionAge={sessionAge} leadingBadges={ <> @@ -103,16 +100,15 @@ export function WorktreeJumpPaletteWorkspaceTabRow({ ) : null}
-
+
- {workspaceTabRepoName && ( - - - - - - - )} +