Make workspace search global (#2748)

This commit is contained in:
Neil 2026-05-24 13:06:21 -07:00 committed by GitHub
parent 05af0e92ef
commit d6013ee404
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 64 additions and 18 deletions

View File

@ -25,6 +25,7 @@ import { getWorktreeStatus, getWorktreeStatusLabel } from '@/lib/worktree-status
import { activateAndRevealWorktree } from '@/lib/worktree-activation'
import { findWorktreeById } from '@/store/slices/worktree-helpers'
import {
getWorktreePaletteSearchScope,
searchWorktrees,
type MatchRange,
type PaletteSearchResult
@ -200,16 +201,10 @@ export default function WorktreeJumpPalette(): React.JSX.Element | null {
const hasQuery = deferredQuery.trim().length > 0
// Why: keep the jump palette aligned with the sidebar. Surfacing hidden
// default-branch or sleeping workspaces here would reintroduce entries the
// user asked the workspace navigation surfaces to omit.
// Drift warning: this check must stay in lockstep with the sidebar's
// filter in computeVisibleWorktreeIds (visible-worktrees.ts). Both
// surfaces share isDefaultBranchWorkspace so the predicate can't drift,
// but adding a new filter axis (e.g. a second toggle) here would need
// the matching change in the sidebar pipeline — otherwise Cmd+J and
// the sidebar will show different lists.
const visibleWorktrees = useMemo(
// Why: the empty-query palette mirrors sidebar filters so opening Search
// starts from the same quiet list. Typed search switches to the global
// non-archived scope below.
const emptyQueryVisibleWorktrees = useMemo(
() =>
allWorktrees.filter((worktree) => {
if (worktree.isArchived) {
@ -246,11 +241,21 @@ export default function WorktreeJumpPalette(): React.JSX.Element | null {
const { visibleWorktreesForState, switchableWorktreesForRows } = useMemo(
() =>
orderEmptyQueryWorktrees({
visibleWorktrees,
visibleWorktrees: emptyQueryVisibleWorktrees,
activeWorktreeId,
lastVisitedAtByWorktreeId
}),
[visibleWorktrees, activeWorktreeId, lastVisitedAtByWorktreeId]
[emptyQueryVisibleWorktrees, activeWorktreeId, lastVisitedAtByWorktreeId]
)
const searchScopeWorktrees = useMemo(
() =>
getWorktreePaletteSearchScope({
hasQuery,
allWorktrees,
emptyQueryWorktrees: switchableWorktreesForRows
}),
[allWorktrees, hasQuery, switchableWorktreesForRows]
)
// Why: typed queries still route through sortWorktreesSmart — switcher
@ -259,7 +264,7 @@ export default function WorktreeJumpPalette(): React.JSX.Element | null {
() =>
hasQuery
? sortWorktreesSmart(
visibleWorktrees,
searchScopeWorktrees,
tabsByWorktree,
repoMap,
agentStatusByPaneKey,
@ -268,11 +273,10 @@ export default function WorktreeJumpPalette(): React.JSX.Element | null {
migrationUnsupportedByPtyId,
terminalLayoutsByTabId
)
: switchableWorktreesForRows,
: searchScopeWorktrees,
[
hasQuery,
visibleWorktrees,
switchableWorktreesForRows,
searchScopeWorktrees,
tabsByWorktree,
repoMap,
agentStatusByPaneKey,
@ -500,6 +504,7 @@ export default function WorktreeJumpPalette(): React.JSX.Element | null {
// because the only visible worktree is the currently active one.
// See docs/cmd-j-empty-query-ordering.md.
const hasAnyWorktrees = visibleWorktreesForState.length > 0
const hasAnySearchableWorktrees = hasQuery ? searchScopeWorktrees.length > 0 : hasAnyWorktrees
const hasAnyBrowserPages = browserPageEntries.length > 0
useEffect(() => {
@ -857,7 +862,7 @@ export default function WorktreeJumpPalette(): React.JSX.Element | null {
const resultCount = selectableItems.length
const emptyState = (() => {
if ((hasAnyWorktrees || hasAnyBrowserPages) && hasQuery) {
if ((hasAnySearchableWorktrees || hasAnyBrowserPages) && hasQuery) {
return {
title: 'No results match your search',
subtitle: 'Try a name, branch, repo, port, comment, PR, page title, or URL.'

View File

@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { searchWorktrees } from './worktree-palette-search'
import { getWorktreePaletteSearchScope, searchWorktrees } from './worktree-palette-search'
import type { Repo, Worktree } from '../../../shared/types'
function makeWorktree(overrides: Partial<Worktree> = {}): Worktree {
@ -39,6 +39,33 @@ const repoMap = new Map<string, Repo>([
])
describe('worktree-palette-search', () => {
it('uses the filtered recent list for empty queries', () => {
const visible = makeWorktree({ id: 'visible' })
const hidden = makeWorktree({ id: 'hidden-by-filter' })
const scope = getWorktreePaletteSearchScope({
hasQuery: false,
allWorktrees: [visible, hidden],
emptyQueryWorktrees: [visible]
})
expect(scope.map((worktree) => worktree.id)).toEqual(['visible'])
})
it('uses all non-archived worktrees for typed queries', () => {
const visible = makeWorktree({ id: 'visible' })
const hiddenByFilter = makeWorktree({ id: 'hidden-by-filter' })
const archived = makeWorktree({ id: 'archived', isArchived: true })
const scope = getWorktreePaletteSearchScope({
hasQuery: true,
allWorktrees: [visible, hiddenByFilter, archived],
emptyQueryWorktrees: [visible]
})
expect(scope.map((worktree) => worktree.id)).toEqual(['visible', 'hidden-by-filter'])
})
it('returns every worktree with no match metadata for an empty query', () => {
const results = searchWorktrees([makeWorktree()], '', repoMap, null, null)

View File

@ -27,6 +27,20 @@ export type PaletteSearchResult = {
supportingText: PaletteSupportingText | null
}
export function getWorktreePaletteSearchScope(args: {
hasQuery: boolean
allWorktrees: readonly Worktree[]
emptyQueryWorktrees: readonly Worktree[]
}): Worktree[] {
if (!args.hasQuery) {
return [...args.emptyQueryWorktrees]
}
// Why: sidebar filters keep the default list quiet, but explicit search is
// a recovery path for sleeping/default-branch workspaces hidden by filters.
return args.allWorktrees.filter((worktree) => !worktree.isArchived)
}
type PRCacheEntry = { data?: { number: number; title: string } | null } | undefined
type IssueCacheEntry = { data?: { number: number; title: string } | null } | undefined