mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
feat(cmd-j): show Recent Worktrees section + repo/branch composite search (#1180)
- 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 <help@stably.ai>
This commit is contained in:
@@ -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 {
|
||||
}}
|
||||
>
|
||||
<CommandInput
|
||||
placeholder="Jump to worktree or browser tab..."
|
||||
placeholder={'Jump to worktree or browser tab\u2026 try "repo/branch"'}
|
||||
value={query}
|
||||
onValueChange={setQuery}
|
||||
wrapperClassName="mx-3 mt-3 rounded-lg border border-border/55 bg-muted/28 px-3.5 shadow-[inset_0_1px_0_rgba(255,255,255,0.04)]"
|
||||
@@ -693,7 +725,7 @@ export default function WorktreeJumpPalette(): React.JSX.Element | null {
|
||||
return (
|
||||
<div
|
||||
key={entry.id}
|
||||
className="mx-0.5 mt-2 mb-0.5 px-3 text-[11px] font-medium uppercase tracking-wider text-muted-foreground/70 first:mt-0"
|
||||
className="mx-0.5 mt-3 mb-1 px-3 text-[11px] font-medium uppercase tracking-wider text-muted-foreground/70"
|
||||
>
|
||||
{entry.label}
|
||||
</div>
|
||||
|
||||
@@ -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 })],
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user