From 6978e6a4aa8cc502f39f251d0d6ca63bfb74005d Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Mon, 27 Apr 2026 11:32:40 -0700 Subject: [PATCH] feat(cmd-j): show Recent Worktrees section + repo/branch composite search (#1180) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Surface top 3 worktrees under a RECENT WORKTREES header on empty query when there are ≥4 worktrees, with a WORKTREES header for the rest - Support "repo/branch" composite queries in the palette search, with highlight ranges on both segments - Add placeholder hint for the new composite query Co-authored-by: Orca --- .../src/components/WorktreeJumpPalette.tsx | 42 ++++++++++++++++--- .../src/lib/worktree-palette-search.test.ts | 37 ++++++++++++++++ .../src/lib/worktree-palette-search.ts | 28 +++++++++++++ 3 files changed, 102 insertions(+), 5 deletions(-) diff --git a/src/renderer/src/components/WorktreeJumpPalette.tsx b/src/renderer/src/components/WorktreeJumpPalette.tsx index 7f9104bbce6..cba2264ab2e 100644 --- a/src/renderer/src/components/WorktreeJumpPalette.tsx +++ b/src/renderer/src/components/WorktreeJumpPalette.tsx @@ -282,18 +282,50 @@ export default function WorktreeJumpPalette(): React.JSX.Element | null { const bothSectionsPopulated = worktreeItems.length > 0 && browserItems.length > 0 const hasQuery = deferredQuery.trim().length > 0 const EMPTY_QUERY_BROWSER_PREVIEW = 3 + const RECENT_COUNT = 3 + const RECENT_MIN_WORKTREES = 4 + + // Rule A: Recent section renders only when the user hasn't typed and there + // are enough worktrees that "the rest" is non-empty. With ≤3 worktrees, a + // RECENT label would sit atop the entire list with nothing below it — + // chrome with no decision value. In that case we fall through to the + // existing showHeaders logic below. + const showRecentSection = !hasQuery && worktreeItems.length >= RECENT_MIN_WORKTREES - const visibleWorktreeItems = worktreeItems const visibleBrowserItems = !hasQuery && bothSectionsPopulated ? browserItems.slice(0, EMPTY_QUERY_BROWSER_PREVIEW) : browserItems const showHeaders = bothSectionsPopulated - if (visibleWorktreeItems.length > 0) { + + if (showRecentSection) { + // Why: symmetric RECENT WORKTREES / WORKTREES headers make the two + // groups unambiguous. Users asked for an explicit "Worktrees" title on + // the lower group rather than a bare divider. + entries.push({ + id: '__header_recent__', + type: 'section-header', + label: 'Recent Worktrees' + }) + entries.push(...worktreeItems.slice(0, RECENT_COUNT)) + entries.push({ id: '__header_all_worktrees__', type: 'section-header', label: 'Worktrees' }) + entries.push(...worktreeItems.slice(RECENT_COUNT)) + if (visibleBrowserItems.length > 0) { + entries.push({ + id: '__header_browser__', + type: 'section-header', + label: 'Browser Tabs' + }) + entries.push(...visibleBrowserItems) + } + return entries + } + + if (worktreeItems.length > 0) { if (showHeaders) { entries.push({ id: '__header_worktrees__', type: 'section-header', label: 'Worktrees' }) } - entries.push(...visibleWorktreeItems) + entries.push(...worktreeItems) } if (visibleBrowserItems.length > 0) { if (showHeaders) { @@ -669,7 +701,7 @@ export default function WorktreeJumpPalette(): React.JSX.Element | null { }} > {entry.label} diff --git a/src/renderer/src/lib/worktree-palette-search.test.ts b/src/renderer/src/lib/worktree-palette-search.test.ts index 55f1dcb5853..83f8468385a 100644 --- a/src/renderer/src/lib/worktree-palette-search.test.ts +++ b/src/renderer/src/lib/worktree-palette-search.test.ts @@ -134,6 +134,43 @@ describe('worktree-palette-search', () => { expect(results[2].worktreeId).toBe('wt-main') }) + it('supports "repo/branch" composite queries and highlights both segments', () => { + const worktrees = [ + makeWorktree({ + id: 'wt-main', + branch: 'refs/heads/main', + displayName: 'main' + }), + makeWorktree({ + id: 'wt-feature', + branch: 'refs/heads/feature/foo', + displayName: 'feature foo' + }) + ] + + const results = searchWorktrees(worktrees, 'orca/main', repoMap, null, null) + + expect(results).toHaveLength(1) + expect(results[0].worktreeId).toBe('wt-main') + expect(results[0].matchedField).toBe('branch') + expect(results[0].repoRange).toEqual({ start: 9, end: 13 }) + expect(results[0].branchRange).toEqual({ start: 0, end: 4 }) + }) + + it('falls back to single-token matching when a composite query has no composite hits', () => { + const results = searchWorktrees( + [makeWorktree({ branch: 'refs/heads/feature/palette-refresh' })], + 'feature/palette', + repoMap, + null, + null + ) + + expect(results).toHaveLength(1) + expect(results[0].matchedField).toBe('branch') + expect(results[0].branchRange).toEqual({ start: 0, end: 'feature/palette'.length }) + }) + it('matches issue numbers with a leading hash and returns issue render context', () => { const results = searchWorktrees( [makeWorktree({ linkedIssue: 304 })], diff --git a/src/renderer/src/lib/worktree-palette-search.ts b/src/renderer/src/lib/worktree-palette-search.ts index 82d33fee5e2..90bca8843e1 100644 --- a/src/renderer/src/lib/worktree-palette-search.ts +++ b/src/renderer/src/lib/worktree-palette-search.ts @@ -86,7 +86,35 @@ export function searchWorktrees( const numericQuery = q.startsWith('#') ? q.slice(1) : q const results: PaletteSearchResult[] = [] + // Support "repo/branch" composite queries (e.g. "orca/main") so users can + // narrow by repo and branch in a single token. We split on the FIRST slash + // only — branch names themselves contain slashes (e.g. "feature/foo"), and + // we still want the right-hand side to match those in full. + const slashIndex = q.indexOf('/') + const composite = + slashIndex > 0 && slashIndex < q.length - 1 + ? { repoPart: q.slice(0, slashIndex), branchPart: q.slice(slashIndex + 1) } + : null + for (const worktree of worktrees) { + if (composite) { + const repoName = repoMap.get(worktree.repoId)?.displayName ?? '' + const branch = branchName(worktree.branch) + const repoIdx = repoName.toLowerCase().indexOf(composite.repoPart) + const branchIdx = branch.toLowerCase().indexOf(composite.branchPart) + if (repoIdx !== -1 && branchIdx !== -1) { + results.push( + makeResult(worktree.id, 'branch', { + repoRange: { start: repoIdx, end: repoIdx + composite.repoPart.length }, + branchRange: { start: branchIdx, end: branchIdx + composite.branchPart.length } + }) + ) + continue + } + // Fall through to single-token matching so users who type a branch name + // that happens to contain a slash (e.g. "feature/foo") still get hits. + } + const nameIndex = worktree.displayName.toLowerCase().indexOf(q) if (nameIndex !== -1) { results.push(