mirror of
https://github.com/stablyai/orca.git
synced 2026-10-08 00:02:38 +00:00
perf(cmd-j): defer palette indexing until query exists
Lazy-compute search documents only when needed. Skip expensive index building, hosted-review checks, and file indexing when search query is empty, improving dialog responsiveness on open and during navigation.
This commit is contained in:
@@ -4,18 +4,83 @@ import { act } from 'react'
|
||||
import { createRoot, type Root } from 'react-dom/client'
|
||||
import { fireEvent } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type * as ReactModule from 'react'
|
||||
import type * as ReactI18Next from 'react-i18next'
|
||||
import type * as TabDocumentModule from '@/lib/palette-match/tab-document'
|
||||
import type * as ChecksReviewIndexModule from '@/components/cmd-j/worktree-checks-review-index'
|
||||
import { useAppStore } from '@/store'
|
||||
import type { AppState } from '@/store/types'
|
||||
import { encodePaletteIdentity } from '@/lib/palette-match/palette-ranking'
|
||||
import WorktreeJumpPalette from './WorktreeJumpPalette'
|
||||
import { makeRepo, makeWorktree } from './worktree-jump-palette-test-fixtures'
|
||||
import type * as WorktreeDocumentIndexModule from './worktree-jump-palette-document-index'
|
||||
import {
|
||||
makeMixedRecentTabState,
|
||||
makeRepo,
|
||||
makeWorktree
|
||||
} from './worktree-jump-palette-test-fixtures'
|
||||
|
||||
const { activateAndRevealWorktree } = vi.hoisted(() => ({
|
||||
activateAndRevealWorktree: vi.fn(() => false)
|
||||
const {
|
||||
activateAndRevealWorktree,
|
||||
buildChecksReviewIndex,
|
||||
buildWorktreeDocumentIndex,
|
||||
buildTabDocument,
|
||||
deferQuery
|
||||
} = vi.hoisted(() => ({
|
||||
activateAndRevealWorktree: vi.fn(() => false),
|
||||
buildChecksReviewIndex: vi.fn(),
|
||||
buildWorktreeDocumentIndex: vi.fn(),
|
||||
buildTabDocument: vi.fn((_input: unknown) => undefined),
|
||||
deferQuery: vi.fn((_query: string, useActual: () => string) => useActual())
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/worktree-activation', () => ({ activateAndRevealWorktree }))
|
||||
vi.mock('@/components/cmd-j/worktree-checks-review-index', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof ChecksReviewIndexModule>()
|
||||
return {
|
||||
...actual,
|
||||
buildWorktreeChecksReviewIndex: (
|
||||
...args: Parameters<typeof actual.buildWorktreeChecksReviewIndex>
|
||||
) => {
|
||||
buildChecksReviewIndex()
|
||||
return actual.buildWorktreeChecksReviewIndex(...args)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('react', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof ReactModule>()
|
||||
return {
|
||||
...actual,
|
||||
useDeferredValue: (query: string) => deferQuery(query, () => actual.useDeferredValue(query))
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('./worktree-jump-palette-document-index', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof WorktreeDocumentIndexModule>()
|
||||
return {
|
||||
...actual,
|
||||
buildWorktreeJumpPaletteDocumentIndex: (
|
||||
...args: Parameters<typeof actual.buildWorktreeJumpPaletteDocumentIndex>
|
||||
) => {
|
||||
buildWorktreeDocumentIndex()
|
||||
return actual.buildWorktreeJumpPaletteDocumentIndex(...args)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/lib/palette-match/tab-document', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof TabDocumentModule>()
|
||||
return {
|
||||
...actual,
|
||||
buildPaletteTabDocument: (...args: Parameters<typeof actual.buildPaletteTabDocument>) => {
|
||||
buildTabDocument(args[0])
|
||||
return actual.buildPaletteTabDocument(...args)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/lib/worktree-activation', () => ({
|
||||
activateAndRevealWorktree
|
||||
}))
|
||||
|
||||
vi.mock('react-i18next', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof ReactI18Next>()
|
||||
@@ -194,12 +259,85 @@ describe('WorktreeJumpPalette', () => {
|
||||
globalThis.IS_REACT_ACT_ENVIRONMENT = true
|
||||
setCommandQuery = null
|
||||
activateAndRevealWorktree.mockClear()
|
||||
buildChecksReviewIndex.mockClear()
|
||||
buildWorktreeDocumentIndex.mockClear()
|
||||
buildTabDocument.mockClear()
|
||||
deferQuery.mockReset()
|
||||
deferQuery.mockImplementation((_query: string, useActual: () => string) => useActual())
|
||||
useAppStore.setState(initialAppState, true)
|
||||
testContainer = document.createElement('div')
|
||||
document.body.appendChild(testContainer)
|
||||
testRoot = createRoot(testContainer)
|
||||
})
|
||||
|
||||
it('builds documents only for a live query and restores recents before deferral catches up', async () => {
|
||||
let deferredQuery = ''
|
||||
deferQuery.mockImplementation(() => deferredQuery)
|
||||
await renderPalette(makeMixedRecentTabState())
|
||||
|
||||
expect(buildWorktreeDocumentIndex).not.toHaveBeenCalled()
|
||||
expect(buildChecksReviewIndex).not.toHaveBeenCalled()
|
||||
expect(buildTabDocument).not.toHaveBeenCalled()
|
||||
|
||||
deferredQuery = ' '
|
||||
await act(async () => {
|
||||
setCommandQuery?.(' ')
|
||||
})
|
||||
expect(buildWorktreeDocumentIndex).not.toHaveBeenCalled()
|
||||
expect(buildChecksReviewIndex).not.toHaveBeenCalled()
|
||||
expect(buildTabDocument).not.toHaveBeenCalled()
|
||||
|
||||
deferredQuery = 'alpha'
|
||||
await act(async () => {
|
||||
setCommandQuery?.('alpha')
|
||||
})
|
||||
await flushEffects()
|
||||
|
||||
expect(buildWorktreeDocumentIndex).toHaveBeenCalledTimes(1)
|
||||
expect(buildChecksReviewIndex).toHaveBeenCalledTimes(1)
|
||||
expect(buildTabDocument).toHaveBeenCalled()
|
||||
expect(
|
||||
buildTabDocument.mock.calls.map(([input]) => (input as { title: string }).title)
|
||||
).toEqual(
|
||||
expect.arrayContaining([
|
||||
'Alpha chat',
|
||||
'Beta chat',
|
||||
'notes.ts',
|
||||
'Needle simulator',
|
||||
'Needle browser'
|
||||
])
|
||||
)
|
||||
expect(getWorktreeRows().some((row) => row.includes('Beta workspace'))).toBe(false)
|
||||
expect(testContainer.textContent).not.toContain('Beta chat')
|
||||
|
||||
const initialTabBuildCount = buildTabDocument.mock.calls.length
|
||||
deferredQuery = 'beta'
|
||||
await act(async () => {
|
||||
setCommandQuery?.('beta')
|
||||
})
|
||||
expect(buildWorktreeDocumentIndex).toHaveBeenCalledTimes(1)
|
||||
expect(buildChecksReviewIndex).toHaveBeenCalledTimes(1)
|
||||
expect(buildTabDocument).toHaveBeenCalledTimes(initialTabBuildCount)
|
||||
expect(getWorktreeRows().some((row) => row.includes('Beta workspace'))).toBe(true)
|
||||
expect(testContainer.textContent).toContain('Beta chat')
|
||||
|
||||
await act(async () => {
|
||||
useAppStore.setState({ agentStatusByPaneKey: {} })
|
||||
})
|
||||
expect(buildWorktreeDocumentIndex).toHaveBeenCalledTimes(1)
|
||||
|
||||
const tabBuildCount = buildTabDocument.mock.calls.length
|
||||
await act(async () => {
|
||||
setCommandQuery?.('')
|
||||
})
|
||||
|
||||
expect(buildWorktreeDocumentIndex).toHaveBeenCalledTimes(1)
|
||||
expect(buildChecksReviewIndex).toHaveBeenCalledTimes(1)
|
||||
expect(buildTabDocument).toHaveBeenCalledTimes(tabBuildCount)
|
||||
expect(getWorktreeRows().some((row) => row.includes('Beta workspace'))).toBe(true)
|
||||
expect(testContainer.textContent).toContain('Beta chat')
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await act(async () => {
|
||||
testRoot.unmount()
|
||||
|
||||
@@ -53,7 +53,7 @@ it('keeps same-id browser results on their owner with recency, and follows owner
|
||||
worktreeOrder: new Map(),
|
||||
worktreeMatches: [],
|
||||
hasQuery: true,
|
||||
deferredQuery: 'Browser proof',
|
||||
paletteSearchQuery: 'Browser proof',
|
||||
browserTabsByWorktree: { 'same-id': [workspace] },
|
||||
browserPagesByWorkspace: { browser: [page] },
|
||||
unifiedTabsByWorktree: { 'same-id': [tab] }
|
||||
|
||||
@@ -33,7 +33,9 @@ export function useWorktreeJumpPaletteLocalState({
|
||||
// react-doctor-disable-next-line react-doctor/no-ref-current-in-render
|
||||
liveQueryRef.current = query
|
||||
const taskSourceUrl = useMemo(() => parseCmdJTaskSourceUrl(query), [query])
|
||||
const paletteSearchQuery = taskSourceUrl ? query.trim() : deferredQuery.trim()
|
||||
const rawSearchQuery = query.trim()
|
||||
const paletteSearchQuery =
|
||||
rawSearchQuery && !taskSourceUrl ? deferredQuery.trim() : rawSearchQuery
|
||||
const deferredCreateAction = useMemo(
|
||||
() => getWorktreePaletteCreateActionState({ query: deferredQuery }),
|
||||
[deferredQuery]
|
||||
|
||||
@@ -1,16 +1,4 @@
|
||||
import { useMemo } from 'react'
|
||||
import { buildSearchableBrowserPages } from '@/lib/browser-palette-page-entries'
|
||||
import { searchBrowserPages, type SearchableBrowserPage } from '@/lib/browser-palette-search'
|
||||
import {
|
||||
buildSearchableSimulatorTabs,
|
||||
searchSimulatorTabs,
|
||||
type SearchableSimulatorTab
|
||||
} from '@/lib/simulator-palette-search'
|
||||
import {
|
||||
buildSearchableWorkspaceTabs,
|
||||
searchWorkspaceTabs,
|
||||
type SearchableWorkspaceTab
|
||||
} from '@/lib/workspace-tab-palette-search'
|
||||
import { comparePaletteRankedItems } from '@/lib/cmd-j-section-leadership'
|
||||
import { getPaletteWorktreeIdentity } from '@/lib/palette-repo-resolution'
|
||||
import type {
|
||||
@@ -20,209 +8,28 @@ import type {
|
||||
WorkspaceTabPaletteItem,
|
||||
WorktreePaletteItem
|
||||
} from './worktree-jump-palette-model'
|
||||
import type { WorktreeJumpPaletteFilter } from './use-worktree-jump-palette-filter'
|
||||
import type { WorktreeJumpPaletteLocalState } from './use-worktree-jump-palette-local-state'
|
||||
import type { WorktreeJumpPaletteStoreState } from './use-worktree-jump-palette-store-state'
|
||||
import type { WorktreeJumpPaletteWorktrees } from './use-worktree-jump-palette-worktrees'
|
||||
import {
|
||||
encodePaletteIdentity,
|
||||
type PaletteSearchContext
|
||||
} from '@/lib/palette-match/palette-ranking'
|
||||
import { encodePaletteIdentity } from '@/lib/palette-match/palette-ranking'
|
||||
import {
|
||||
buildBrowserPaletteItems,
|
||||
buildOpenTabPaletteItems,
|
||||
buildSimulatorPaletteItems,
|
||||
buildWorkspaceTabPaletteItems
|
||||
} from './worktree-jump-palette-open-tab-items'
|
||||
import {
|
||||
useWorktreeJumpPaletteTabSearch,
|
||||
type WorktreeJumpPaletteTabSearchInput
|
||||
} from './use-worktree-jump-palette-tab-search'
|
||||
|
||||
const EMPTY_BROWSER_PAGE_ENTRIES: SearchableBrowserPage[] = []
|
||||
const EMPTY_SIMULATOR_TAB_ENTRIES: SearchableSimulatorTab[] = []
|
||||
const EMPTY_WORKSPACE_TAB_ENTRIES: SearchableWorkspaceTab[] = []
|
||||
|
||||
type WorktreeJumpPaletteOpenTabsInput = WorktreeJumpPaletteStoreState &
|
||||
WorktreeJumpPaletteWorktrees &
|
||||
Pick<WorktreeJumpPaletteFilter, 'repoMap' | 'repoByHostIdentity'> &
|
||||
Pick<WorktreeJumpPaletteLocalState, 'deferredQuery'> & {
|
||||
paletteSearchContext: PaletteSearchContext
|
||||
}
|
||||
|
||||
export function useWorktreeJumpPaletteOpenTabs({
|
||||
paletteStatusInputsActive,
|
||||
browserSortedWorktrees,
|
||||
allWorktrees,
|
||||
repoMap,
|
||||
repoByHostIdentity,
|
||||
worktreeOrder,
|
||||
browserTabsByWorktree,
|
||||
browserPagesByWorkspace,
|
||||
activeBrowserTabId,
|
||||
activeWorktreeId,
|
||||
activeWorkspaceExecutionHostId,
|
||||
activeTabType,
|
||||
unifiedTabsByWorktree,
|
||||
activeGroupIdByWorktree,
|
||||
groupsByWorktree,
|
||||
tabsByWorktree,
|
||||
openFiles,
|
||||
agentStatusByPaneKey,
|
||||
retainedAgentsByPaneKey,
|
||||
sleepingAgentSessionsByPaneKey,
|
||||
activeTabId,
|
||||
activeTabIdByWorktree,
|
||||
activeFileId,
|
||||
activeFileIdByWorktree,
|
||||
activeTabTypeByWorktree,
|
||||
settings,
|
||||
terminalLayoutsByTabId,
|
||||
paneForegroundAgentByPaneKey,
|
||||
deferredQuery,
|
||||
paletteSearchContext,
|
||||
hasQuery,
|
||||
worktreeMatches,
|
||||
resolveWorktree
|
||||
}: WorktreeJumpPaletteOpenTabsInput) {
|
||||
const browserPageEntries = useMemo<SearchableBrowserPage[]>(() => {
|
||||
if (!paletteStatusInputsActive) {
|
||||
return EMPTY_BROWSER_PAGE_ENTRIES
|
||||
}
|
||||
return buildSearchableBrowserPages({
|
||||
worktrees: browserSortedWorktrees,
|
||||
ownershipWorktrees: allWorktrees,
|
||||
repoMap,
|
||||
repoMapByHostIdentity: repoByHostIdentity,
|
||||
worktreeOrder,
|
||||
browserTabsByWorktree,
|
||||
browserPagesByWorkspace,
|
||||
activeBrowserTabId,
|
||||
activeWorktreeId,
|
||||
activeWorkspaceExecutionHostId,
|
||||
activeTabType,
|
||||
unifiedTabsByWorktree
|
||||
})
|
||||
}, [
|
||||
paletteStatusInputsActive,
|
||||
activeBrowserTabId,
|
||||
activeTabType,
|
||||
activeWorktreeId,
|
||||
activeWorkspaceExecutionHostId,
|
||||
allWorktrees,
|
||||
browserPagesByWorkspace,
|
||||
browserTabsByWorktree,
|
||||
browserSortedWorktrees,
|
||||
repoByHostIdentity,
|
||||
repoMap,
|
||||
unifiedTabsByWorktree,
|
||||
worktreeOrder
|
||||
])
|
||||
const browserMatches = useMemo(
|
||||
() =>
|
||||
searchBrowserPages(browserPageEntries, deferredQuery.trim(), {
|
||||
context: paletteSearchContext
|
||||
}),
|
||||
[browserPageEntries, deferredQuery, paletteSearchContext]
|
||||
)
|
||||
const simulatorTabEntries = useMemo<SearchableSimulatorTab[]>(() => {
|
||||
if (!paletteStatusInputsActive) {
|
||||
return EMPTY_SIMULATOR_TAB_ENTRIES
|
||||
}
|
||||
return buildSearchableSimulatorTabs({
|
||||
worktrees: browserSortedWorktrees,
|
||||
ownershipWorktrees: allWorktrees,
|
||||
repoMap,
|
||||
repoMapByHostIdentity: repoByHostIdentity,
|
||||
worktreeOrder,
|
||||
unifiedTabsByWorktree,
|
||||
activeGroupIdByWorktree,
|
||||
groupsByWorktree,
|
||||
activeWorktreeId,
|
||||
activeWorkspaceExecutionHostId,
|
||||
activeTabType
|
||||
})
|
||||
}, [
|
||||
paletteStatusInputsActive,
|
||||
activeGroupIdByWorktree,
|
||||
activeTabType,
|
||||
activeWorktreeId,
|
||||
activeWorkspaceExecutionHostId,
|
||||
allWorktrees,
|
||||
browserSortedWorktrees,
|
||||
groupsByWorktree,
|
||||
repoByHostIdentity,
|
||||
repoMap,
|
||||
unifiedTabsByWorktree,
|
||||
worktreeOrder
|
||||
])
|
||||
const simulatorMatches = useMemo(
|
||||
() =>
|
||||
searchSimulatorTabs(simulatorTabEntries, deferredQuery.trim(), {
|
||||
context: paletteSearchContext
|
||||
}),
|
||||
[simulatorTabEntries, deferredQuery, paletteSearchContext]
|
||||
)
|
||||
const workspaceTabEntries = useMemo<SearchableWorkspaceTab[]>(() => {
|
||||
if (!paletteStatusInputsActive) {
|
||||
return EMPTY_WORKSPACE_TAB_ENTRIES
|
||||
}
|
||||
return buildSearchableWorkspaceTabs({
|
||||
worktrees: browserSortedWorktrees,
|
||||
ownershipWorktrees: allWorktrees,
|
||||
repoMap,
|
||||
repoMapByHostIdentity: repoByHostIdentity,
|
||||
worktreeOrder,
|
||||
unifiedTabsByWorktree,
|
||||
tabsByWorktree,
|
||||
openFiles,
|
||||
agentStatusByPaneKey,
|
||||
retainedAgentsByPaneKey,
|
||||
sleepingAgentSessionsByPaneKey,
|
||||
activeGroupIdByWorktree,
|
||||
groupsByWorktree,
|
||||
activeWorktreeId,
|
||||
activeWorkspaceExecutionHostId,
|
||||
activeTabType,
|
||||
activeTabId,
|
||||
activeTabIdByWorktree,
|
||||
activeFileId,
|
||||
activeFileIdByWorktree,
|
||||
activeTabTypeByWorktree,
|
||||
generatedTitlesEnabled: settings?.tabAutoGenerateTitle === true,
|
||||
terminalLayoutsByTabId,
|
||||
paneForegroundAgentByPaneKey
|
||||
})
|
||||
}, [
|
||||
paletteStatusInputsActive,
|
||||
activeFileId,
|
||||
activeFileIdByWorktree,
|
||||
activeGroupIdByWorktree,
|
||||
activeTabId,
|
||||
activeTabIdByWorktree,
|
||||
activeTabType,
|
||||
activeTabTypeByWorktree,
|
||||
activeWorktreeId,
|
||||
activeWorkspaceExecutionHostId,
|
||||
allWorktrees,
|
||||
agentStatusByPaneKey,
|
||||
browserSortedWorktrees,
|
||||
groupsByWorktree,
|
||||
openFiles,
|
||||
repoMap,
|
||||
repoByHostIdentity,
|
||||
retainedAgentsByPaneKey,
|
||||
settings?.tabAutoGenerateTitle,
|
||||
sleepingAgentSessionsByPaneKey,
|
||||
paneForegroundAgentByPaneKey,
|
||||
tabsByWorktree,
|
||||
terminalLayoutsByTabId,
|
||||
unifiedTabsByWorktree,
|
||||
worktreeOrder
|
||||
])
|
||||
const workspaceTabMatches = useMemo(
|
||||
() =>
|
||||
searchWorkspaceTabs(workspaceTabEntries, deferredQuery.trim(), {
|
||||
context: paletteSearchContext
|
||||
}),
|
||||
[workspaceTabEntries, deferredQuery, paletteSearchContext]
|
||||
)
|
||||
export function useWorktreeJumpPaletteOpenTabs(input: WorktreeJumpPaletteTabSearchInput) {
|
||||
const { hasQuery, resolveWorktree, worktreeMatches } = input
|
||||
const {
|
||||
browserMatches,
|
||||
browserPageEntries,
|
||||
simulatorMatches,
|
||||
simulatorTabEntries,
|
||||
workspaceTabEntries,
|
||||
workspaceTabMatches
|
||||
} = useWorktreeJumpPaletteTabSearch(input)
|
||||
const worktreeItems = useMemo<WorktreePaletteItem[]>(() => {
|
||||
const items = worktreeMatches
|
||||
.map((match) => {
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
import { useMemo } from 'react'
|
||||
import {
|
||||
buildBrowserPalettePageEntries,
|
||||
prepareSearchableBrowserPages
|
||||
} from '@/lib/browser-palette-page-entries'
|
||||
import {
|
||||
listBrowserPages,
|
||||
searchBrowserPages,
|
||||
type BrowserPalettePageEntry
|
||||
} from '@/lib/browser-palette-search'
|
||||
import {
|
||||
buildSimulatorPaletteTabEntries,
|
||||
listSimulatorTabs,
|
||||
prepareSearchableSimulatorTabs,
|
||||
searchSimulatorTabs,
|
||||
type SimulatorPaletteTabEntry
|
||||
} from '@/lib/simulator-palette-search'
|
||||
import {
|
||||
buildWorkspaceTabPaletteEntries,
|
||||
listWorkspaceTabs,
|
||||
prepareSearchableWorkspaceTabs,
|
||||
searchWorkspaceTabs,
|
||||
type WorkspaceTabPaletteEntry
|
||||
} from '@/lib/workspace-tab-palette-search'
|
||||
import type { PaletteSearchContext } from '@/lib/palette-match/palette-ranking'
|
||||
import type { WorktreeJumpPaletteFilter } from './use-worktree-jump-palette-filter'
|
||||
import type { WorktreeJumpPaletteLocalState } from './use-worktree-jump-palette-local-state'
|
||||
import type { WorktreeJumpPaletteStoreState } from './use-worktree-jump-palette-store-state'
|
||||
import type { WorktreeJumpPaletteWorktrees } from './use-worktree-jump-palette-worktrees'
|
||||
|
||||
const EMPTY_BROWSER_PAGE_ENTRIES: BrowserPalettePageEntry[] = []
|
||||
const EMPTY_SIMULATOR_TAB_ENTRIES: SimulatorPaletteTabEntry[] = []
|
||||
const EMPTY_WORKSPACE_TAB_ENTRIES: WorkspaceTabPaletteEntry[] = []
|
||||
|
||||
type PaletteSearchOptions = { context: PaletteSearchContext }
|
||||
|
||||
function usePaletteEntrySearch<Entry, SearchableEntry, Result>({
|
||||
entries,
|
||||
list,
|
||||
prepare,
|
||||
query,
|
||||
search,
|
||||
searchContext
|
||||
}: {
|
||||
entries: readonly Entry[]
|
||||
list: (entries: readonly Entry[], options: PaletteSearchOptions) => Result[]
|
||||
prepare: (entries: readonly Entry[]) => SearchableEntry[]
|
||||
query: string
|
||||
search: (
|
||||
entries: readonly SearchableEntry[],
|
||||
query: string,
|
||||
options: PaletteSearchOptions
|
||||
) => Result[]
|
||||
searchContext: PaletteSearchContext
|
||||
}): Result[] {
|
||||
const hasQuery = query.length > 0
|
||||
const searchableEntries = useMemo(
|
||||
() => (hasQuery ? prepare(entries) : []),
|
||||
[entries, hasQuery, prepare]
|
||||
)
|
||||
return useMemo(
|
||||
() =>
|
||||
hasQuery
|
||||
? search(searchableEntries, query, { context: searchContext })
|
||||
: list(entries, { context: searchContext }),
|
||||
[entries, hasQuery, list, query, search, searchableEntries, searchContext]
|
||||
)
|
||||
}
|
||||
|
||||
export type WorktreeJumpPaletteTabSearchInput = WorktreeJumpPaletteStoreState &
|
||||
WorktreeJumpPaletteWorktrees &
|
||||
Pick<WorktreeJumpPaletteFilter, 'repoMap' | 'repoByHostIdentity'> &
|
||||
Pick<WorktreeJumpPaletteLocalState, 'paletteSearchQuery'> & {
|
||||
paletteSearchContext: PaletteSearchContext
|
||||
}
|
||||
|
||||
export function useWorktreeJumpPaletteTabSearch({
|
||||
paletteStatusInputsActive,
|
||||
browserSortedWorktrees,
|
||||
allWorktrees,
|
||||
repoMap,
|
||||
repoByHostIdentity,
|
||||
worktreeOrder,
|
||||
browserTabsByWorktree,
|
||||
browserPagesByWorkspace,
|
||||
activeBrowserTabId,
|
||||
activeWorktreeId,
|
||||
activeWorkspaceExecutionHostId,
|
||||
activeTabType,
|
||||
unifiedTabsByWorktree,
|
||||
activeGroupIdByWorktree,
|
||||
groupsByWorktree,
|
||||
tabsByWorktree,
|
||||
openFiles,
|
||||
agentStatusByPaneKey,
|
||||
retainedAgentsByPaneKey,
|
||||
sleepingAgentSessionsByPaneKey,
|
||||
activeTabId,
|
||||
activeTabIdByWorktree,
|
||||
activeFileId,
|
||||
activeFileIdByWorktree,
|
||||
activeTabTypeByWorktree,
|
||||
settings,
|
||||
terminalLayoutsByTabId,
|
||||
paneForegroundAgentByPaneKey,
|
||||
paletteSearchQuery,
|
||||
paletteSearchContext
|
||||
}: WorktreeJumpPaletteTabSearchInput) {
|
||||
const browserPageEntries = useMemo<BrowserPalettePageEntry[]>(() => {
|
||||
if (!paletteStatusInputsActive) {
|
||||
return EMPTY_BROWSER_PAGE_ENTRIES
|
||||
}
|
||||
return buildBrowserPalettePageEntries({
|
||||
worktrees: browserSortedWorktrees,
|
||||
ownershipWorktrees: allWorktrees,
|
||||
repoMap,
|
||||
repoMapByHostIdentity: repoByHostIdentity,
|
||||
worktreeOrder,
|
||||
browserTabsByWorktree,
|
||||
browserPagesByWorkspace,
|
||||
activeBrowserTabId,
|
||||
activeWorktreeId,
|
||||
activeWorkspaceExecutionHostId,
|
||||
activeTabType,
|
||||
unifiedTabsByWorktree
|
||||
})
|
||||
}, [
|
||||
activeBrowserTabId,
|
||||
activeTabType,
|
||||
activeWorktreeId,
|
||||
activeWorkspaceExecutionHostId,
|
||||
allWorktrees,
|
||||
browserPagesByWorkspace,
|
||||
browserSortedWorktrees,
|
||||
browserTabsByWorktree,
|
||||
paletteStatusInputsActive,
|
||||
repoByHostIdentity,
|
||||
repoMap,
|
||||
unifiedTabsByWorktree,
|
||||
worktreeOrder
|
||||
])
|
||||
const browserMatches = usePaletteEntrySearch({
|
||||
entries: browserPageEntries,
|
||||
list: listBrowserPages,
|
||||
prepare: prepareSearchableBrowserPages,
|
||||
query: paletteSearchQuery,
|
||||
search: searchBrowserPages,
|
||||
searchContext: paletteSearchContext
|
||||
})
|
||||
const simulatorTabEntries = useMemo<SimulatorPaletteTabEntry[]>(() => {
|
||||
if (!paletteStatusInputsActive) {
|
||||
return EMPTY_SIMULATOR_TAB_ENTRIES
|
||||
}
|
||||
return buildSimulatorPaletteTabEntries({
|
||||
worktrees: browserSortedWorktrees,
|
||||
ownershipWorktrees: allWorktrees,
|
||||
repoMap,
|
||||
repoMapByHostIdentity: repoByHostIdentity,
|
||||
worktreeOrder,
|
||||
unifiedTabsByWorktree,
|
||||
activeGroupIdByWorktree,
|
||||
groupsByWorktree,
|
||||
activeWorktreeId,
|
||||
activeWorkspaceExecutionHostId,
|
||||
activeTabType
|
||||
})
|
||||
}, [
|
||||
activeGroupIdByWorktree,
|
||||
activeTabType,
|
||||
activeWorktreeId,
|
||||
activeWorkspaceExecutionHostId,
|
||||
allWorktrees,
|
||||
browserSortedWorktrees,
|
||||
groupsByWorktree,
|
||||
paletteStatusInputsActive,
|
||||
repoByHostIdentity,
|
||||
repoMap,
|
||||
unifiedTabsByWorktree,
|
||||
worktreeOrder
|
||||
])
|
||||
const simulatorMatches = usePaletteEntrySearch({
|
||||
entries: simulatorTabEntries,
|
||||
list: listSimulatorTabs,
|
||||
prepare: prepareSearchableSimulatorTabs,
|
||||
query: paletteSearchQuery,
|
||||
search: searchSimulatorTabs,
|
||||
searchContext: paletteSearchContext
|
||||
})
|
||||
const workspaceTabEntries = useMemo<WorkspaceTabPaletteEntry[]>(() => {
|
||||
if (!paletteStatusInputsActive) {
|
||||
return EMPTY_WORKSPACE_TAB_ENTRIES
|
||||
}
|
||||
return buildWorkspaceTabPaletteEntries({
|
||||
worktrees: browserSortedWorktrees,
|
||||
ownershipWorktrees: allWorktrees,
|
||||
repoMap,
|
||||
repoMapByHostIdentity: repoByHostIdentity,
|
||||
worktreeOrder,
|
||||
unifiedTabsByWorktree,
|
||||
tabsByWorktree,
|
||||
openFiles,
|
||||
agentStatusByPaneKey,
|
||||
retainedAgentsByPaneKey,
|
||||
sleepingAgentSessionsByPaneKey,
|
||||
activeGroupIdByWorktree,
|
||||
groupsByWorktree,
|
||||
activeWorktreeId,
|
||||
activeWorkspaceExecutionHostId,
|
||||
activeTabType,
|
||||
activeTabId,
|
||||
activeTabIdByWorktree,
|
||||
activeFileId,
|
||||
activeFileIdByWorktree,
|
||||
activeTabTypeByWorktree,
|
||||
generatedTitlesEnabled: settings?.tabAutoGenerateTitle === true,
|
||||
terminalLayoutsByTabId,
|
||||
paneForegroundAgentByPaneKey
|
||||
})
|
||||
}, [
|
||||
activeFileId,
|
||||
activeFileIdByWorktree,
|
||||
activeGroupIdByWorktree,
|
||||
activeTabId,
|
||||
activeTabIdByWorktree,
|
||||
activeTabType,
|
||||
activeTabTypeByWorktree,
|
||||
activeWorktreeId,
|
||||
activeWorkspaceExecutionHostId,
|
||||
agentStatusByPaneKey,
|
||||
allWorktrees,
|
||||
browserSortedWorktrees,
|
||||
groupsByWorktree,
|
||||
openFiles,
|
||||
paletteStatusInputsActive,
|
||||
paneForegroundAgentByPaneKey,
|
||||
repoByHostIdentity,
|
||||
repoMap,
|
||||
retainedAgentsByPaneKey,
|
||||
settings?.tabAutoGenerateTitle,
|
||||
sleepingAgentSessionsByPaneKey,
|
||||
tabsByWorktree,
|
||||
terminalLayoutsByTabId,
|
||||
unifiedTabsByWorktree,
|
||||
worktreeOrder
|
||||
])
|
||||
const workspaceTabMatches = usePaletteEntrySearch({
|
||||
entries: workspaceTabEntries,
|
||||
list: listWorkspaceTabs,
|
||||
prepare: prepareSearchableWorkspaceTabs,
|
||||
query: paletteSearchQuery,
|
||||
search: searchWorkspaceTabs,
|
||||
searchContext: paletteSearchContext
|
||||
})
|
||||
|
||||
return {
|
||||
browserPageEntries,
|
||||
browserMatches,
|
||||
simulatorTabEntries,
|
||||
simulatorMatches,
|
||||
workspaceTabEntries,
|
||||
workspaceTabMatches
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,11 @@ import type { WorktreeJumpPaletteStoreState } from './use-worktree-jump-palette-
|
||||
import { buildWorktreeJumpPaletteDocumentIndex } from './worktree-jump-palette-document-index'
|
||||
import { buildWorktreeJumpPaletteWorktreeMaps } from './worktree-jump-palette-worktree-maps'
|
||||
import type { PaletteSearchContext } from '@/lib/palette-match/palette-ranking'
|
||||
import type { PaletteDocument } from '@/lib/palette-match/palette-document'
|
||||
import type { HostedReviewInfo } from '../../../shared/hosted-review'
|
||||
|
||||
const EMPTY_WORKTREE_DOCUMENTS: ReadonlyMap<string, PaletteDocument> = new Map()
|
||||
const EMPTY_CHECKS_REVIEW_INDEX: ReadonlyMap<Worktree, HostedReviewInfo | null> = new Map()
|
||||
|
||||
type WorktreeJumpPaletteWorktreesInput = WorktreeJumpPaletteStoreState &
|
||||
Pick<
|
||||
@@ -228,31 +233,36 @@ export function useWorktreeJumpPaletteWorktrees({
|
||||
)
|
||||
const checksReviewByWorktree = useMemo(
|
||||
() =>
|
||||
buildWorktreeChecksReviewIndex({
|
||||
worktrees: allWorktrees,
|
||||
repoByHostIdentity,
|
||||
prCache,
|
||||
hostedReviewCache,
|
||||
settings
|
||||
}),
|
||||
[allWorktrees, hostedReviewCache, prCache, repoByHostIdentity, settings]
|
||||
hasQuery
|
||||
? buildWorktreeChecksReviewIndex({
|
||||
worktrees: allWorktrees,
|
||||
repoByHostIdentity,
|
||||
prCache,
|
||||
hostedReviewCache,
|
||||
settings
|
||||
})
|
||||
: EMPTY_CHECKS_REVIEW_INDEX,
|
||||
[allWorktrees, hasQuery, hostedReviewCache, prCache, repoByHostIdentity, settings]
|
||||
)
|
||||
const worktreeDocuments = useMemo(
|
||||
() =>
|
||||
buildWorktreeJumpPaletteDocumentIndex({
|
||||
worktrees: allWorktrees,
|
||||
repoMap,
|
||||
repoByHostIdentity,
|
||||
hostOptions,
|
||||
hostFilterActive,
|
||||
prCache,
|
||||
issueCache,
|
||||
workspacePortScan,
|
||||
checksReviewByWorktree
|
||||
}),
|
||||
hasQuery
|
||||
? buildWorktreeJumpPaletteDocumentIndex({
|
||||
worktrees: allWorktrees,
|
||||
repoMap,
|
||||
repoByHostIdentity,
|
||||
hostOptions,
|
||||
hostFilterActive,
|
||||
prCache,
|
||||
issueCache,
|
||||
workspacePortScan,
|
||||
checksReviewByWorktree
|
||||
})
|
||||
: EMPTY_WORKTREE_DOCUMENTS,
|
||||
[
|
||||
allWorktrees,
|
||||
checksReviewByWorktree,
|
||||
hasQuery,
|
||||
hostFilterActive,
|
||||
hostOptions,
|
||||
issueCache,
|
||||
|
||||
@@ -138,6 +138,85 @@ export function makeRecentTabState(overrides: Partial<AppState> = {}): Partial<A
|
||||
}
|
||||
}
|
||||
|
||||
/** Recent terminal, editor, simulator and browser rows across two worktrees. */
|
||||
export function makeMixedRecentTabState(): Partial<AppState> {
|
||||
const state = makeRecentTabState()
|
||||
const editorFileId = '/tmp/wt-alpha/notes.ts'
|
||||
return {
|
||||
...state,
|
||||
openFiles: [
|
||||
{
|
||||
id: editorFileId,
|
||||
filePath: editorFileId,
|
||||
relativePath: 'notes.ts',
|
||||
worktreeId: 'wt-alpha',
|
||||
language: 'typescript',
|
||||
isDirty: false,
|
||||
mode: 'edit'
|
||||
}
|
||||
],
|
||||
browserTabsByWorktree: {
|
||||
'wt-alpha': [
|
||||
{
|
||||
id: 'browser-workspace',
|
||||
worktreeId: 'wt-alpha',
|
||||
activePageId: 'browser-page',
|
||||
pageIds: ['browser-page'],
|
||||
url: 'https://example.com/needle',
|
||||
title: 'Needle browser',
|
||||
loading: false,
|
||||
faviconUrl: null,
|
||||
canGoBack: false,
|
||||
canGoForward: false,
|
||||
loadError: null,
|
||||
createdAt: 0
|
||||
}
|
||||
]
|
||||
},
|
||||
browserPagesByWorkspace: {
|
||||
'browser-workspace': [
|
||||
{
|
||||
id: 'browser-page',
|
||||
workspaceId: 'browser-workspace',
|
||||
worktreeId: 'wt-alpha',
|
||||
url: 'https://example.com/needle',
|
||||
title: 'Needle browser',
|
||||
loading: false,
|
||||
faviconUrl: null,
|
||||
canGoBack: false,
|
||||
canGoForward: false,
|
||||
loadError: null,
|
||||
createdAt: 0
|
||||
}
|
||||
]
|
||||
},
|
||||
unifiedTabsByWorktree: {
|
||||
...state.unifiedTabsByWorktree,
|
||||
'wt-alpha': [
|
||||
...(state.unifiedTabsByWorktree?.['wt-alpha'] ?? []),
|
||||
{
|
||||
...makeUnifiedTab('tab-editor', 'wt-alpha', editorFileId, 'notes.ts'),
|
||||
contentType: 'editor'
|
||||
},
|
||||
{
|
||||
...makeUnifiedTab('tab-simulator', 'wt-alpha', 'simulator', 'Needle simulator'),
|
||||
contentType: 'simulator'
|
||||
},
|
||||
{
|
||||
...makeUnifiedTab('tab-browser', 'wt-alpha', 'browser-workspace', 'Needle browser'),
|
||||
contentType: 'browser'
|
||||
}
|
||||
]
|
||||
},
|
||||
groupsByWorktree: {
|
||||
...state.groupsByWorktree,
|
||||
'wt-alpha': [
|
||||
makeGroup('wt-alpha', ['tab-alpha', 'tab-editor', 'tab-simulator', 'tab-browser'])
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Two host-qualified worktrees intentionally publish the same unified tab id. */
|
||||
export function makeDuplicateRecentTabState(): Partial<AppState> {
|
||||
const alpha = makeWorktree('wt-alpha', 'Alpha workspace', { hostId: 'ssh:alpha' })
|
||||
|
||||
@@ -3,7 +3,11 @@ import type { BrowserPage, BrowserWorkspace } from '../../../shared/browser-work
|
||||
import type { Tab } from '../../../shared/tab-types'
|
||||
import type { Worktree } from '../../../shared/worktree/types'
|
||||
import { getWorktreeHostIdentity } from '../../../shared/worktree/host-qualified-identity'
|
||||
import { buildSearchableBrowserPages } from './browser-palette-page-entries'
|
||||
import {
|
||||
buildBrowserPalettePageEntries,
|
||||
buildSearchableBrowserPages,
|
||||
prepareSearchableBrowserPages
|
||||
} from './browser-palette-page-entries'
|
||||
import { searchBrowserPages } from './browser-palette-search'
|
||||
|
||||
function makeWorktree(overrides: Partial<Worktree> = {}): Worktree {
|
||||
@@ -79,10 +83,10 @@ const worktreeOrder = new Map([
|
||||
['wt-2', 1]
|
||||
])
|
||||
|
||||
function buildFixture(
|
||||
function buildFixtureOptions(
|
||||
overrides: Partial<Parameters<typeof buildSearchableBrowserPages>[0]> = {}
|
||||
): ReturnType<typeof buildSearchableBrowserPages> {
|
||||
return buildSearchableBrowserPages({
|
||||
): Parameters<typeof buildSearchableBrowserPages>[0] {
|
||||
return {
|
||||
worktrees: [worktreeA, worktreeB],
|
||||
repoMap,
|
||||
worktreeOrder,
|
||||
@@ -126,10 +130,23 @@ function buildFixture(
|
||||
activeWorktreeId: 'wt-1',
|
||||
activeTabType: 'browser',
|
||||
...overrides
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function buildFixture(
|
||||
overrides: Partial<Parameters<typeof buildSearchableBrowserPages>[0]> = {}
|
||||
): ReturnType<typeof buildSearchableBrowserPages> {
|
||||
return buildSearchableBrowserPages(buildFixtureOptions(overrides))
|
||||
}
|
||||
|
||||
describe('buildSearchableBrowserPages', () => {
|
||||
it('keeps navigation metadata separate from prepared search documents', () => {
|
||||
const entries = buildBrowserPalettePageEntries(buildFixtureOptions())
|
||||
|
||||
expect(entries[0]).not.toHaveProperty('document')
|
||||
expect(prepareSearchableBrowserPages(entries)[0]?.document).toBeDefined()
|
||||
})
|
||||
|
||||
it('treats an unstamped legacy worktree as local when its tab is stamped', () => {
|
||||
expect(
|
||||
buildFixture({
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from './palette-repo-resolution'
|
||||
import {
|
||||
buildSearchableBrowserPageDocument,
|
||||
type BrowserPalettePageEntry,
|
||||
type SearchableBrowserPage
|
||||
} from './browser-palette-search'
|
||||
import {
|
||||
@@ -37,7 +38,7 @@ export type BuildSearchableBrowserPagesOptions = {
|
||||
activeTabType: BrowserPaletteActiveTabType
|
||||
}
|
||||
|
||||
export function buildSearchableBrowserPages({
|
||||
export function buildBrowserPalettePageEntries({
|
||||
worktrees,
|
||||
ownershipWorktrees,
|
||||
repoMap,
|
||||
@@ -50,8 +51,8 @@ export function buildSearchableBrowserPages({
|
||||
activeWorktreeId,
|
||||
activeWorkspaceExecutionHostId,
|
||||
activeTabType
|
||||
}: BuildSearchableBrowserPagesOptions): SearchableBrowserPage[] {
|
||||
const entries: SearchableBrowserPage[] = []
|
||||
}: BuildSearchableBrowserPagesOptions): BrowserPalettePageEntry[] {
|
||||
const entries: BrowserPalettePageEntry[] = []
|
||||
const ambiguousWorktreeIds = findAmbiguousWorktreeIds(ownershipWorktrees ?? worktrees)
|
||||
const allUnifiedTabs = Object.values(unifiedTabsByWorktree ?? {}).flatMap((tabs) => tabs ?? [])
|
||||
const duplicateTabIds = findDuplicateIds(allUnifiedTabs)
|
||||
@@ -133,11 +134,25 @@ export function buildSearchableBrowserPages({
|
||||
lastActiveAt:
|
||||
workspace.activePageId === page.id && workspaceFocusedAt
|
||||
? maxValidPaletteActivityTimestamp([workspaceFocusedAt, page.createdAt])
|
||||
: maxValidPaletteActivityTimestamp([page.createdAt]),
|
||||
document: buildSearchableBrowserPageDocument({ page, workspace, worktree, repoName })
|
||||
: maxValidPaletteActivityTimestamp([page.createdAt])
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
export function prepareSearchableBrowserPages(
|
||||
entries: readonly BrowserPalettePageEntry[]
|
||||
): SearchableBrowserPage[] {
|
||||
return entries.map((entry) => ({
|
||||
...entry,
|
||||
document: buildSearchableBrowserPageDocument(entry)
|
||||
}))
|
||||
}
|
||||
|
||||
export function buildSearchableBrowserPages(
|
||||
options: BuildSearchableBrowserPagesOptions
|
||||
): SearchableBrowserPage[] {
|
||||
return prepareSearchableBrowserPages(buildBrowserPalettePageEntries(options))
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
|
||||
const NO_RANGES: readonly MatchRange[] = []
|
||||
|
||||
export type SearchableBrowserPage = {
|
||||
export type BrowserPalettePageEntry = {
|
||||
page: BrowserPage
|
||||
workspace: BrowserWorkspace
|
||||
worktree: Worktree
|
||||
@@ -40,10 +40,10 @@ export type SearchableBrowserPage = {
|
||||
/** Last time the owning browser workspace was focused; null when never focused. */
|
||||
lastActiveAt?: number | null
|
||||
lastFocusedAt?: number
|
||||
/** Normalized field index, built once per entry rather than per keystroke. */
|
||||
document: PaletteDocument
|
||||
}
|
||||
|
||||
export type SearchableBrowserPage = BrowserPalettePageEntry & { document: PaletteDocument }
|
||||
|
||||
export type BrowserPaletteSearchResult = {
|
||||
/** Worktree ids collide across hosts; activation must not resolve by id alone. */
|
||||
executionHostId?: ExecutionHostId
|
||||
@@ -151,7 +151,7 @@ function compareEmptyQueryResults(
|
||||
|
||||
// Why: empty-query browser ordering is intentionally deterministic and context-first;
|
||||
// lastActiveAt only breaks ties between equally-ranked query matches.
|
||||
function positionScore(entry: SearchableBrowserPage): number {
|
||||
function positionScore(entry: BrowserPalettePageEntry): number {
|
||||
if (entry.isCurrentPage) {
|
||||
return entry.worktreeSortIndex * 100 - 4000
|
||||
}
|
||||
@@ -159,7 +159,7 @@ function positionScore(entry: SearchableBrowserPage): number {
|
||||
}
|
||||
|
||||
function baseResult(
|
||||
entry: SearchableBrowserPage,
|
||||
entry: BrowserPalettePageEntry,
|
||||
context: PaletteSearchContext
|
||||
): BrowserPaletteSearchResult {
|
||||
const formattedUrl = formatBrowserPaletteUrl(entry.page.url)
|
||||
@@ -203,6 +203,14 @@ function baseResult(
|
||||
}
|
||||
}
|
||||
|
||||
export function listBrowserPages(
|
||||
entries: readonly BrowserPalettePageEntry[],
|
||||
options: { context?: PaletteSearchContext } = {}
|
||||
): BrowserPaletteSearchResult[] {
|
||||
const context = options.context ?? createPaletteSearchContext(Date.now())
|
||||
return entries.map((entry) => baseResult(entry, context)).sort(compareEmptyQueryResults)
|
||||
}
|
||||
|
||||
export function searchBrowserPages(
|
||||
entries: readonly SearchableBrowserPage[],
|
||||
query: string,
|
||||
@@ -216,9 +224,7 @@ export function searchBrowserPages(
|
||||
if (!prepared) {
|
||||
// Why not [] on an over-token query: the empty branch also serves the no-query
|
||||
// listing, so the invalid case is filtered out by the token guard below.
|
||||
return query.trim()
|
||||
? []
|
||||
: entries.map((entry) => baseResult(entry, context)).sort(compareEmptyQueryResults)
|
||||
return query.trim() ? [] : listBrowserPages(entries, { context })
|
||||
}
|
||||
|
||||
const results: BrowserPaletteSearchResult[] = []
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import type { ExecutionHostId } from '../../../shared/execution-host'
|
||||
import type { Tab, TabGroup, WorkspaceVisibleTabType } from '../../../shared/tab-types'
|
||||
import type { Worktree } from '../../../shared/worktree/types'
|
||||
import {
|
||||
getPaletteWorktreeIdentity,
|
||||
isPaletteCurrentWorktree,
|
||||
resolvePaletteRepoForWorktree
|
||||
} from './palette-repo-resolution'
|
||||
import { getActiveSimulatorTabId } from './simulator-palette-active-tab'
|
||||
import { buildPaletteTabDocument } from './palette-match/tab-document'
|
||||
import type { PaletteDocument } from './palette-match/palette-document'
|
||||
import {
|
||||
resolveWorktreeBranchLabel,
|
||||
resolveWorktreeDisplayName
|
||||
} from './worktree-default-display-name'
|
||||
import {
|
||||
findAmbiguousWorktreeIds,
|
||||
findDuplicateIds,
|
||||
isUnifiedTabOwnedByWorktree
|
||||
} from './unified-tab-host-ownership'
|
||||
|
||||
export type SimulatorPaletteTabEntry = {
|
||||
tab: Tab
|
||||
worktree: Worktree
|
||||
repoName: string
|
||||
worktreeSortIndex: number
|
||||
isCurrentTab: boolean
|
||||
isCurrentWorktree: boolean
|
||||
}
|
||||
|
||||
export type SearchableSimulatorTab = SimulatorPaletteTabEntry & { document: PaletteDocument }
|
||||
|
||||
export type BuildSearchableSimulatorTabsOptions = {
|
||||
worktrees: readonly Worktree[]
|
||||
ownershipWorktrees?: readonly Pick<Worktree, 'id'>[]
|
||||
repoMap: ReadonlyMap<string, { displayName?: string | null }>
|
||||
repoMapByHostIdentity?: ReadonlyMap<string, { displayName?: string | null }>
|
||||
worktreeOrder: ReadonlyMap<string, number>
|
||||
unifiedTabsByWorktree: Record<string, readonly Tab[] | undefined>
|
||||
activeGroupIdByWorktree: Record<string, string | undefined>
|
||||
groupsByWorktree: Record<string, readonly TabGroup[] | undefined>
|
||||
activeWorktreeId: string | null
|
||||
activeWorkspaceExecutionHostId?: ExecutionHostId | null
|
||||
activeTabType: WorkspaceVisibleTabType
|
||||
}
|
||||
|
||||
// Search-only aliases keep the icon-expressed tab type out of the row text.
|
||||
export const SIMULATOR_TYPE_SEARCH_ALIASES = [
|
||||
'mobile emulator tab',
|
||||
'mobile emulator',
|
||||
'ios simulator',
|
||||
'emulator'
|
||||
] as const
|
||||
|
||||
export function simulatorPaletteTabTitle(tab: Tab): string {
|
||||
return tab.label || 'Mobile Emulator'
|
||||
}
|
||||
|
||||
export function buildSimulatorPaletteTabEntries({
|
||||
worktrees,
|
||||
ownershipWorktrees,
|
||||
repoMap,
|
||||
repoMapByHostIdentity,
|
||||
worktreeOrder,
|
||||
unifiedTabsByWorktree,
|
||||
activeGroupIdByWorktree,
|
||||
groupsByWorktree,
|
||||
activeWorktreeId,
|
||||
activeWorkspaceExecutionHostId,
|
||||
activeTabType
|
||||
}: BuildSearchableSimulatorTabsOptions): SimulatorPaletteTabEntry[] {
|
||||
const entries: SimulatorPaletteTabEntry[] = []
|
||||
const ambiguousWorktreeIds = findAmbiguousWorktreeIds(ownershipWorktrees ?? worktrees)
|
||||
for (const worktree of worktrees) {
|
||||
const repoName =
|
||||
resolvePaletteRepoForWorktree(worktree, repoMap, repoMapByHostIdentity)?.displayName ?? ''
|
||||
const worktreeSortIndex =
|
||||
worktreeOrder.get(getPaletteWorktreeIdentity(worktree)) ??
|
||||
worktreeOrder.get(worktree.id) ??
|
||||
Number.MAX_SAFE_INTEGER
|
||||
const activeUnifiedTabId = getActiveSimulatorTabId({
|
||||
worktreeId: worktree.id,
|
||||
worktreeHostId: worktree.hostId,
|
||||
worktreeRuntimeOwnerEnvironmentId: worktree.runtimeOwnerEnvironmentId,
|
||||
activeWorktreeId,
|
||||
activeWorkspaceExecutionHostId,
|
||||
activeTabType,
|
||||
activeGroupId: activeGroupIdByWorktree[worktree.id],
|
||||
groups: groupsByWorktree[worktree.id]
|
||||
})
|
||||
const tabs = unifiedTabsByWorktree[worktree.id] ?? []
|
||||
const duplicateTabIds = findDuplicateIds(tabs)
|
||||
for (const tab of tabs) {
|
||||
if (
|
||||
duplicateTabIds.has(tab.id) ||
|
||||
tab.contentType !== 'simulator' ||
|
||||
!isUnifiedTabOwnedByWorktree(tab, worktree, ambiguousWorktreeIds)
|
||||
) {
|
||||
continue
|
||||
}
|
||||
entries.push({
|
||||
tab,
|
||||
worktree,
|
||||
repoName,
|
||||
worktreeSortIndex,
|
||||
// Simulator tabs are unified tabs; terminal activeTabId misses split-group activation.
|
||||
isCurrentTab: activeUnifiedTabId === tab.id,
|
||||
isCurrentWorktree: isPaletteCurrentWorktree(
|
||||
worktree,
|
||||
activeWorktreeId,
|
||||
activeWorkspaceExecutionHostId
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
export function prepareSearchableSimulatorTabs(
|
||||
entries: readonly SimulatorPaletteTabEntry[]
|
||||
): SearchableSimulatorTab[] {
|
||||
return entries.map((entry) => ({
|
||||
...entry,
|
||||
document: buildPaletteTabDocument({
|
||||
id: entry.tab.id,
|
||||
title: simulatorPaletteTabTitle(entry.tab),
|
||||
secondaryTexts: [],
|
||||
worktreeName: resolveWorktreeDisplayName(entry.worktree),
|
||||
branch: resolveWorktreeBranchLabel(entry.worktree),
|
||||
repoName: entry.repoName,
|
||||
typeAliases: SIMULATOR_TYPE_SEARCH_ALIASES
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
export function buildSearchableSimulatorTabs(
|
||||
options: BuildSearchableSimulatorTabsOptions
|
||||
): SearchableSimulatorTab[] {
|
||||
return prepareSearchableSimulatorTabs(buildSimulatorPaletteTabEntries(options))
|
||||
}
|
||||
@@ -12,7 +12,9 @@ import {
|
||||
SIMULATOR_PALETTE_QUERY_MAX_BYTES,
|
||||
SIMULATOR_TYPE_SEARCH_ALIASES,
|
||||
buildSearchableSimulatorTabs,
|
||||
buildSimulatorPaletteTabEntries,
|
||||
isSimulatorPaletteQueryTooLarge,
|
||||
prepareSearchableSimulatorTabs,
|
||||
searchSimulatorTabs,
|
||||
simulatorPaletteTabTitle,
|
||||
type SearchableSimulatorTab
|
||||
@@ -120,6 +122,23 @@ describe('simulator-palette-search lastActiveAt', () => {
|
||||
})
|
||||
|
||||
describe('simulator-palette-search', () => {
|
||||
it('keeps navigation metadata separate from prepared search documents', () => {
|
||||
const worktree = makeWorktree()
|
||||
const entries = buildSimulatorPaletteTabEntries({
|
||||
worktrees: [worktree],
|
||||
repoMap: new Map([[worktree.repoId, { displayName: 'repo/mobile' }]]),
|
||||
worktreeOrder: new Map([[worktree.id, 0]]),
|
||||
unifiedTabsByWorktree: { [worktree.id]: [makeTab()] },
|
||||
activeGroupIdByWorktree: { [worktree.id]: 'group-1' },
|
||||
groupsByWorktree: { [worktree.id]: [makeGroup()] },
|
||||
activeWorktreeId: null,
|
||||
activeTabType: 'terminal'
|
||||
})
|
||||
|
||||
expect(entries[0]).not.toHaveProperty('document')
|
||||
expect(prepareSearchableSimulatorTabs(entries)[0]?.document).toBeDefined()
|
||||
})
|
||||
|
||||
it('keeps same-id simulator tabs isolated by execution host', () => {
|
||||
const sharedId = 'repo-shared::/workspace'
|
||||
const local = makeWorktree({ id: sharedId, hostId: 'local', displayName: 'Local workspace' })
|
||||
|
||||
@@ -1,12 +1,4 @@
|
||||
import type { ExecutionHostId } from '../../../shared/execution-host'
|
||||
import type { Tab, TabGroup, WorkspaceVisibleTabType } from '../../../shared/tab-types'
|
||||
import type { Worktree } from '../../../shared/worktree/types'
|
||||
import {
|
||||
getPaletteWorktreeIdentity,
|
||||
isPaletteCurrentWorktree,
|
||||
resolvePaletteRepoForWorktree
|
||||
} from './palette-repo-resolution'
|
||||
import { getActiveSimulatorTabId } from './simulator-palette-active-tab'
|
||||
import { isClipboardTextByteLengthOverLimit } from '../../../shared/clipboard-text'
|
||||
import { compareBaseSensitivityLocaleText } from './locale-text-collators'
|
||||
import {
|
||||
@@ -15,13 +7,12 @@ import {
|
||||
matchPaletteTabDocument,
|
||||
preparePaletteTabQuery
|
||||
} from './palette-match/tab-match'
|
||||
import { buildPaletteTabDocument } from './palette-match/tab-document'
|
||||
import {
|
||||
resolveWorktreeBranchLabel,
|
||||
resolveWorktreeDisplayName
|
||||
} from './worktree-default-display-name'
|
||||
import type { MatchRange } from './palette-match/normalized-text'
|
||||
import type { PaletteDocument, PaletteDocumentRank } from './palette-match/palette-document'
|
||||
import type { PaletteDocumentRank } from './palette-match/palette-document'
|
||||
import type { PaletteResultQualityClass } from './palette-match/match-quality'
|
||||
import {
|
||||
createPaletteSearchContext,
|
||||
@@ -31,26 +22,26 @@ import {
|
||||
type PaletteActivityRank,
|
||||
type PaletteSearchContext
|
||||
} from './palette-match/palette-ranking'
|
||||
import { getUnifiedTabPaletteExecutionHostId } from './unified-tab-host-ownership'
|
||||
import {
|
||||
findAmbiguousWorktreeIds,
|
||||
findDuplicateIds,
|
||||
getUnifiedTabPaletteExecutionHostId,
|
||||
isUnifiedTabOwnedByWorktree
|
||||
} from './unified-tab-host-ownership'
|
||||
SIMULATOR_TYPE_SEARCH_ALIASES,
|
||||
simulatorPaletteTabTitle,
|
||||
type SearchableSimulatorTab,
|
||||
type SimulatorPaletteTabEntry
|
||||
} from './simulator-palette-entries'
|
||||
export {
|
||||
buildSearchableSimulatorTabs,
|
||||
buildSimulatorPaletteTabEntries,
|
||||
prepareSearchableSimulatorTabs,
|
||||
SIMULATOR_TYPE_SEARCH_ALIASES,
|
||||
simulatorPaletteTabTitle,
|
||||
type BuildSearchableSimulatorTabsOptions,
|
||||
type SearchableSimulatorTab,
|
||||
type SimulatorPaletteTabEntry
|
||||
} from './simulator-palette-entries'
|
||||
|
||||
const NO_RANGES: readonly MatchRange[] = []
|
||||
|
||||
export type SearchableSimulatorTab = {
|
||||
tab: Tab
|
||||
worktree: Worktree
|
||||
repoName: string
|
||||
worktreeSortIndex: number
|
||||
isCurrentTab: boolean
|
||||
isCurrentWorktree: boolean
|
||||
/** Normalized field index, built once per entry rather than per keystroke. */
|
||||
document: PaletteDocument
|
||||
}
|
||||
|
||||
export type SimulatorPaletteSearchResult = {
|
||||
/** Worktree ids collide across hosts; activation must not resolve by id alone. */
|
||||
executionHostId?: ExecutionHostId
|
||||
@@ -82,16 +73,6 @@ export type SimulatorPaletteSearchResult = {
|
||||
|
||||
export const SIMULATOR_PALETTE_QUERY_MAX_BYTES = 2 * 1024
|
||||
|
||||
// Why search-only: the row icon already says "emulator"; a fixed secondary label
|
||||
// crowds Cmd+J the same way "Terminal tab" did. Keep these strings matchable so
|
||||
// typing "mobile" / "simulator" still finds emulator tabs.
|
||||
export const SIMULATOR_TYPE_SEARCH_ALIASES = [
|
||||
'mobile emulator tab',
|
||||
'mobile emulator',
|
||||
'ios simulator',
|
||||
'emulator'
|
||||
] as const
|
||||
|
||||
export function isSimulatorPaletteQueryTooLarge(
|
||||
query: string,
|
||||
maxBytes = SIMULATOR_PALETTE_QUERY_MAX_BYTES
|
||||
@@ -99,20 +80,6 @@ export function isSimulatorPaletteQueryTooLarge(
|
||||
return isClipboardTextByteLengthOverLimit(query, maxBytes)
|
||||
}
|
||||
|
||||
export type BuildSearchableSimulatorTabsOptions = {
|
||||
worktrees: readonly Worktree[]
|
||||
ownershipWorktrees?: readonly Pick<Worktree, 'id'>[]
|
||||
repoMap: ReadonlyMap<string, { displayName?: string | null }>
|
||||
repoMapByHostIdentity?: ReadonlyMap<string, { displayName?: string | null }>
|
||||
worktreeOrder: ReadonlyMap<string, number>
|
||||
unifiedTabsByWorktree: Record<string, readonly Tab[] | undefined>
|
||||
activeGroupIdByWorktree: Record<string, string | undefined>
|
||||
groupsByWorktree: Record<string, readonly TabGroup[] | undefined>
|
||||
activeWorktreeId: string | null
|
||||
activeWorkspaceExecutionHostId?: ExecutionHostId | null
|
||||
activeTabType: WorkspaceVisibleTabType
|
||||
}
|
||||
|
||||
function compareText(a: string, b: string): number {
|
||||
return compareBaseSensitivityLocaleText(a, b)
|
||||
}
|
||||
@@ -139,19 +106,15 @@ function compareEmptyQueryResults(
|
||||
|
||||
// Why: empty-query simulator ordering stays deterministic and context-first;
|
||||
// lastActiveAt only breaks ties between equally-ranked query matches.
|
||||
function positionScore(entry: SearchableSimulatorTab): number {
|
||||
function positionScore(entry: SimulatorPaletteTabEntry): number {
|
||||
if (entry.isCurrentTab) {
|
||||
return entry.worktreeSortIndex * 100 - 4000
|
||||
}
|
||||
return entry.worktreeSortIndex * 100 - (entry.isCurrentWorktree ? 1000 : 0)
|
||||
}
|
||||
|
||||
export function simulatorPaletteTabTitle(tab: Tab): string {
|
||||
return tab.label || 'Mobile Emulator'
|
||||
}
|
||||
|
||||
function baseResult(
|
||||
entry: SearchableSimulatorTab,
|
||||
entry: SimulatorPaletteTabEntry,
|
||||
context: PaletteSearchContext
|
||||
): SimulatorPaletteSearchResult {
|
||||
const executionHostId = getUnifiedTabPaletteExecutionHostId(entry.tab, entry.worktree)
|
||||
@@ -194,74 +157,12 @@ function baseResult(
|
||||
}
|
||||
}
|
||||
|
||||
export function buildSearchableSimulatorTabs({
|
||||
worktrees,
|
||||
ownershipWorktrees,
|
||||
repoMap,
|
||||
repoMapByHostIdentity,
|
||||
worktreeOrder,
|
||||
unifiedTabsByWorktree,
|
||||
activeGroupIdByWorktree,
|
||||
groupsByWorktree,
|
||||
activeWorktreeId,
|
||||
activeWorkspaceExecutionHostId,
|
||||
activeTabType
|
||||
}: BuildSearchableSimulatorTabsOptions): SearchableSimulatorTab[] {
|
||||
const entries: SearchableSimulatorTab[] = []
|
||||
const ambiguousWorktreeIds = findAmbiguousWorktreeIds(ownershipWorktrees ?? worktrees)
|
||||
for (const worktree of worktrees) {
|
||||
const repoName =
|
||||
resolvePaletteRepoForWorktree(worktree, repoMap, repoMapByHostIdentity)?.displayName ?? ''
|
||||
const worktreeSortIndex =
|
||||
worktreeOrder.get(getPaletteWorktreeIdentity(worktree)) ??
|
||||
worktreeOrder.get(worktree.id) ??
|
||||
Number.MAX_SAFE_INTEGER
|
||||
const activeUnifiedTabId = getActiveSimulatorTabId({
|
||||
worktreeId: worktree.id,
|
||||
worktreeHostId: worktree.hostId,
|
||||
worktreeRuntimeOwnerEnvironmentId: worktree.runtimeOwnerEnvironmentId,
|
||||
activeWorktreeId,
|
||||
activeWorkspaceExecutionHostId,
|
||||
activeTabType,
|
||||
activeGroupId: activeGroupIdByWorktree[worktree.id],
|
||||
groups: groupsByWorktree[worktree.id]
|
||||
})
|
||||
const tabs = unifiedTabsByWorktree[worktree.id] ?? []
|
||||
const duplicateTabIds = findDuplicateIds(tabs)
|
||||
for (const tab of tabs) {
|
||||
if (
|
||||
duplicateTabIds.has(tab.id) ||
|
||||
tab.contentType !== 'simulator' ||
|
||||
!isUnifiedTabOwnedByWorktree(tab, worktree, ambiguousWorktreeIds)
|
||||
) {
|
||||
continue
|
||||
}
|
||||
entries.push({
|
||||
tab,
|
||||
worktree,
|
||||
repoName,
|
||||
worktreeSortIndex,
|
||||
// Why: simulator tabs are unified tabs; terminal activeTabId does not
|
||||
// identify the visible emulator tab after split-group activation.
|
||||
isCurrentTab: activeUnifiedTabId === tab.id,
|
||||
isCurrentWorktree: isPaletteCurrentWorktree(
|
||||
worktree,
|
||||
activeWorktreeId,
|
||||
activeWorkspaceExecutionHostId
|
||||
),
|
||||
document: buildPaletteTabDocument({
|
||||
id: tab.id,
|
||||
title: simulatorPaletteTabTitle(tab),
|
||||
secondaryTexts: [],
|
||||
worktreeName: resolveWorktreeDisplayName(worktree),
|
||||
branch: resolveWorktreeBranchLabel(worktree),
|
||||
repoName,
|
||||
typeAliases: SIMULATOR_TYPE_SEARCH_ALIASES
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
return entries
|
||||
export function listSimulatorTabs(
|
||||
entries: readonly SimulatorPaletteTabEntry[],
|
||||
options: { context?: PaletteSearchContext } = {}
|
||||
): SimulatorPaletteSearchResult[] {
|
||||
const context = options.context ?? createPaletteSearchContext(Date.now())
|
||||
return entries.map((entry) => baseResult(entry, context)).sort(compareEmptyQueryResults)
|
||||
}
|
||||
|
||||
export function searchSimulatorTabs(
|
||||
@@ -275,9 +176,7 @@ export function searchSimulatorTabs(
|
||||
}
|
||||
const prepared = preparePaletteTabQuery(query)
|
||||
if (!prepared) {
|
||||
return query.trim()
|
||||
? []
|
||||
: entries.map((entry) => baseResult(entry, context)).sort(compareEmptyQueryResults)
|
||||
return query.trim() ? [] : listSimulatorTabs(entries, { context })
|
||||
}
|
||||
|
||||
const results: SimulatorPaletteSearchResult[] = []
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { buildPaletteTabDocument } from './palette-match/tab-document'
|
||||
import {
|
||||
resolveWorktreeBranchLabel,
|
||||
resolveWorktreeDisplayName
|
||||
} from './worktree-default-display-name'
|
||||
import { buildWorkspaceTabPaletteEntries } from './workspace-tab-palette-entry-builder'
|
||||
import type {
|
||||
BuildSearchableWorkspaceTabsOptions,
|
||||
SearchableWorkspaceTab,
|
||||
WorkspaceTabPaletteEntry
|
||||
} from './workspace-tab-palette-search'
|
||||
|
||||
export function prepareSearchableWorkspaceTabs(
|
||||
entries: readonly WorkspaceTabPaletteEntry[]
|
||||
): SearchableWorkspaceTab[] {
|
||||
return entries.map((entry) => ({
|
||||
...entry,
|
||||
document: buildPaletteTabDocument({
|
||||
id: entry.tab.id,
|
||||
title: entry.titleSearchText,
|
||||
secondaryTexts: entry.secondarySearchTexts,
|
||||
worktreeName: resolveWorktreeDisplayName(entry.worktree),
|
||||
branch: resolveWorktreeBranchLabel(entry.worktree),
|
||||
repoName: entry.repoName,
|
||||
typeAliases: entry.typeSearchAliases
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
export function buildSearchableWorkspaceTabs(
|
||||
options: BuildSearchableWorkspaceTabsOptions
|
||||
): SearchableWorkspaceTab[] {
|
||||
return prepareSearchableWorkspaceTabs(buildWorkspaceTabPaletteEntries(options))
|
||||
}
|
||||
@@ -4,24 +4,19 @@ import {
|
||||
} from '../../../shared/tab-title-resolution'
|
||||
import type { Tab } from '../../../shared/tab-types'
|
||||
import { getEditorDisplayLabel } from '@/components/editor/editor-labels'
|
||||
import { buildPaletteTabDocument } from './palette-match/tab-document'
|
||||
import {
|
||||
getPaletteWorktreeIdentity,
|
||||
isPaletteCurrentWorktree,
|
||||
resolvePaletteRepoForWorktree
|
||||
} from './palette-repo-resolution'
|
||||
import { resolveOpenTabOccupantAgent } from './open-tab-occupant-agent'
|
||||
import {
|
||||
resolveWorktreeBranchLabel,
|
||||
resolveWorktreeDisplayName
|
||||
} from './worktree-default-display-name'
|
||||
import {
|
||||
buildAgentMetadataTabIndex,
|
||||
collectAgentMetadataFromIndex
|
||||
} from './workspace-tab-agent-metadata'
|
||||
import type {
|
||||
BuildSearchableWorkspaceTabsOptions,
|
||||
SearchableWorkspaceTab,
|
||||
WorkspaceTabPaletteEntry,
|
||||
WorkspaceTabContentType
|
||||
} from './workspace-tab-palette-search'
|
||||
import {
|
||||
@@ -96,7 +91,7 @@ function isCurrentWorkspaceTab({
|
||||
: (activeFileIdByWorktree[tab.worktreeId] ?? activeFileId) === tab.entityId
|
||||
}
|
||||
|
||||
export function buildSearchableWorkspaceTabEntries({
|
||||
export function buildWorkspaceTabPaletteEntries({
|
||||
worktrees,
|
||||
ownershipWorktrees,
|
||||
repoMap,
|
||||
@@ -121,8 +116,8 @@ export function buildSearchableWorkspaceTabEntries({
|
||||
generatedTitlesEnabled,
|
||||
terminalLayoutsByTabId,
|
||||
paneForegroundAgentByPaneKey
|
||||
}: BuildSearchableWorkspaceTabsOptions): SearchableWorkspaceTab[] {
|
||||
const entries: SearchableWorkspaceTab[] = []
|
||||
}: BuildSearchableWorkspaceTabsOptions): WorkspaceTabPaletteEntry[] {
|
||||
const entries: WorkspaceTabPaletteEntry[] = []
|
||||
const seenTabIdentities = new Set<string>()
|
||||
const openFilesById = new Map<string, OpenFile[]>()
|
||||
for (const file of openFiles) {
|
||||
@@ -143,8 +138,6 @@ export function buildSearchableWorkspaceTabEntries({
|
||||
for (const worktree of worktrees) {
|
||||
const repoName =
|
||||
resolvePaletteRepoForWorktree(worktree, repoMap, repoMapByHostIdentity)?.displayName ?? ''
|
||||
const worktreeName = resolveWorktreeDisplayName(worktree)
|
||||
const branch = resolveWorktreeBranchLabel(worktree)
|
||||
const worktreeSortIndex =
|
||||
worktreeOrder.get(getPaletteWorktreeIdentity(worktree)) ??
|
||||
worktreeOrder.get(worktree.id) ??
|
||||
@@ -236,15 +229,6 @@ export function buildSearchableWorkspaceTabEntries({
|
||||
titleSearchText: title,
|
||||
secondarySearchTexts: [],
|
||||
typeSearchAliases: ['terminal tab', 'terminal'],
|
||||
document: buildPaletteTabDocument({
|
||||
id: tab.id,
|
||||
title,
|
||||
secondaryTexts: [],
|
||||
worktreeName,
|
||||
branch,
|
||||
repoName,
|
||||
typeAliases: ['terminal tab', 'terminal']
|
||||
}),
|
||||
agentMetadata: collectAgentMetadataFromIndex(
|
||||
agentIndex,
|
||||
tab.entityId,
|
||||
@@ -288,14 +272,6 @@ export function buildSearchableWorkspaceTabEntries({
|
||||
secondaryText: file.relativePath,
|
||||
titleSearchText: title,
|
||||
secondarySearchTexts: [file.relativePath, file.filePath],
|
||||
document: buildPaletteTabDocument({
|
||||
id: tab.id,
|
||||
title,
|
||||
secondaryTexts: [file.relativePath, file.filePath],
|
||||
worktreeName,
|
||||
branch,
|
||||
repoName
|
||||
}),
|
||||
agentMetadata: [],
|
||||
occupantAgent: null
|
||||
})
|
||||
|
||||
@@ -28,6 +28,7 @@ import type { TuiAgent } from '../../../shared/tui-agent'
|
||||
import { getUnifiedTabPaletteExecutionHostId } from './unified-tab-host-ownership'
|
||||
import type {
|
||||
SearchableWorkspaceTab,
|
||||
WorkspaceTabPaletteEntry,
|
||||
WorkspaceTabContentType
|
||||
} from './workspace-tab-palette-search'
|
||||
|
||||
@@ -90,7 +91,7 @@ function compareEmptyQueryResults(
|
||||
return compareText(a.title, b.title)
|
||||
}
|
||||
|
||||
function positionScore(entry: SearchableWorkspaceTab): number {
|
||||
function positionScore(entry: WorkspaceTabPaletteEntry): number {
|
||||
// Why: current tab, then current worktree, then rendered tab order.
|
||||
const base = entry.worktreeSortIndex * 100 + entry.groupSortIndex * 10 + entry.tabSortIndex
|
||||
if (entry.isCurrentTab) {
|
||||
@@ -99,7 +100,7 @@ function positionScore(entry: SearchableWorkspaceTab): number {
|
||||
return entry.isCurrentWorktree ? base - 1000 : base
|
||||
}
|
||||
|
||||
function resolveWorkspaceTabLastActiveAt(entry: SearchableWorkspaceTab): number | null {
|
||||
function resolveWorkspaceTabLastActiveAt(entry: WorkspaceTabPaletteEntry): number | null {
|
||||
return maxValidPaletteActivityTimestamp([
|
||||
maxAgentActivityAt(entry.agentMetadata),
|
||||
entry.tab.lastFocusedAt,
|
||||
@@ -108,7 +109,7 @@ function resolveWorkspaceTabLastActiveAt(entry: SearchableWorkspaceTab): number
|
||||
}
|
||||
|
||||
function baseResult(
|
||||
entry: SearchableWorkspaceTab,
|
||||
entry: WorkspaceTabPaletteEntry,
|
||||
context: PaletteSearchContext
|
||||
): WorkspaceTabPaletteSearchResult {
|
||||
const executionHostId = getUnifiedTabPaletteExecutionHostId(entry.tab, entry.worktree)
|
||||
@@ -215,6 +216,14 @@ function matchEntry(
|
||||
}
|
||||
}
|
||||
|
||||
export function listWorkspaceTabs(
|
||||
entries: readonly WorkspaceTabPaletteEntry[],
|
||||
options: { context?: PaletteSearchContext } = {}
|
||||
): WorkspaceTabPaletteSearchResult[] {
|
||||
const context = options.context ?? createPaletteSearchContext(Date.now())
|
||||
return entries.map((entry) => baseResult(entry, context)).sort(compareEmptyQueryResults)
|
||||
}
|
||||
|
||||
export function searchWorkspaceTabs(
|
||||
entries: readonly SearchableWorkspaceTab[],
|
||||
query: string,
|
||||
@@ -229,7 +238,7 @@ export function searchWorkspaceTabs(
|
||||
}
|
||||
const prepared = preparePaletteTabQuery(query)
|
||||
if (!prepared) {
|
||||
return entries.map((entry) => baseResult(entry, context)).sort(compareEmptyQueryResults)
|
||||
return listWorkspaceTabs(entries, { context })
|
||||
}
|
||||
|
||||
const results: WorkspaceTabPaletteSearchResult[] = []
|
||||
|
||||
@@ -8,7 +8,12 @@ import type { Tab, TabGroup } from '../../../shared/tab-types'
|
||||
import type { TerminalTab } from '../../../shared/terminal-tab-types'
|
||||
import type { Worktree } from '../../../shared/worktree/types'
|
||||
import { PALETTE_QUERY_MAX_TOKENS } from './palette-match/palette-query'
|
||||
import { buildSearchableWorkspaceTabs, searchWorkspaceTabs } from './workspace-tab-palette-search'
|
||||
import {
|
||||
buildSearchableWorkspaceTabs,
|
||||
buildWorkspaceTabPaletteEntries,
|
||||
prepareSearchableWorkspaceTabs,
|
||||
searchWorkspaceTabs
|
||||
} from './workspace-tab-palette-search'
|
||||
|
||||
const WT_ROOT = path.join('tmp', 'wt-1')
|
||||
const SRC_APP_RELATIVE_PATH = path.join('src', 'app.ts')
|
||||
@@ -106,10 +111,12 @@ function makeAgentEntry(overrides: Partial<AgentStatusEntry> = {}): AgentStatusE
|
||||
}
|
||||
}
|
||||
|
||||
function buildEntries(overrides: Partial<Parameters<typeof buildSearchableWorkspaceTabs>[0]> = {}) {
|
||||
function buildEntryOptions(
|
||||
overrides: Partial<Parameters<typeof buildSearchableWorkspaceTabs>[0]> = {}
|
||||
): Parameters<typeof buildSearchableWorkspaceTabs>[0] {
|
||||
const worktree = makeWorktree()
|
||||
const tab = makeUnifiedTab()
|
||||
return buildSearchableWorkspaceTabs({
|
||||
return {
|
||||
worktrees: [worktree],
|
||||
repoMap: new Map([[worktree.repoId, { displayName: 'repo/orca' }]]),
|
||||
worktreeOrder: new Map([[worktree.id, 0]]),
|
||||
@@ -130,10 +137,21 @@ function buildEntries(overrides: Partial<Parameters<typeof buildSearchableWorksp
|
||||
activeTabTypeByWorktree: { [worktree.id]: 'terminal' },
|
||||
generatedTitlesEnabled: true,
|
||||
...overrides
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function buildEntries(overrides: Partial<Parameters<typeof buildSearchableWorkspaceTabs>[0]> = {}) {
|
||||
return buildSearchableWorkspaceTabs(buildEntryOptions(overrides))
|
||||
}
|
||||
|
||||
describe('workspace-tab-palette-search', () => {
|
||||
it('keeps navigation metadata separate from prepared search documents', () => {
|
||||
const entries = buildWorkspaceTabPaletteEntries(buildEntryOptions())
|
||||
|
||||
expect(entries[0]).not.toHaveProperty('document')
|
||||
expect(prepareSearchableWorkspaceTabs(entries)[0]?.document).toBeDefined()
|
||||
})
|
||||
|
||||
it('stamps the row execution host so activation never resolves by id alone', () => {
|
||||
// Why: worktree ids repeat across hosts, so a host-blind activation opened the other
|
||||
// host's workspace behind a row labelled with this one's name and branch.
|
||||
|
||||
@@ -7,8 +7,13 @@ import type { TuiAgent } from '../../../shared/tui-agent'
|
||||
import type { Worktree } from '../../../shared/worktree/types'
|
||||
import type { ExecutionHostId } from '../../../shared/execution-host'
|
||||
import type { AgentMetadata, WorkspaceTabAgentMetadataState } from './workspace-tab-agent-metadata'
|
||||
import { buildSearchableWorkspaceTabEntries } from './workspace-tab-palette-entry-builder'
|
||||
import { buildWorkspaceTabPaletteEntries } from './workspace-tab-palette-entry-builder'
|
||||
import {
|
||||
buildSearchableWorkspaceTabs,
|
||||
prepareSearchableWorkspaceTabs
|
||||
} from './workspace-tab-palette-documents'
|
||||
export {
|
||||
listWorkspaceTabs,
|
||||
searchWorkspaceTabs,
|
||||
type WorkspaceTabPaletteSearchResult
|
||||
} from './workspace-tab-palette-results'
|
||||
@@ -20,7 +25,7 @@ export type WorkspaceTabContentType =
|
||||
| 'conflict-review'
|
||||
| 'check-details'
|
||||
|
||||
export type SearchableWorkspaceTab = {
|
||||
export type WorkspaceTabPaletteEntry = {
|
||||
tab: Tab & { contentType: WorkspaceTabContentType }
|
||||
worktree: Worktree
|
||||
repoName: string
|
||||
@@ -36,8 +41,6 @@ export type SearchableWorkspaceTab = {
|
||||
* the row secondary — the content icon already conveys type.
|
||||
*/
|
||||
typeSearchAliases?: readonly string[]
|
||||
/** Normalized field index, built once per entry rather than per keystroke. */
|
||||
document: PaletteDocument
|
||||
agentMetadata: AgentMetadata[]
|
||||
/** Confident occupant for the row icon; null when the pane is a plain shell. */
|
||||
occupantAgent: TuiAgent | null
|
||||
@@ -45,6 +48,8 @@ export type SearchableWorkspaceTab = {
|
||||
isCurrentWorktree: boolean
|
||||
}
|
||||
|
||||
export type SearchableWorkspaceTab = WorkspaceTabPaletteEntry & { document: PaletteDocument }
|
||||
|
||||
// Why search-only: the status/content icon already says "terminal"; a fixed
|
||||
// secondary crowds the row. Keep these matchable so typing "terminal" still finds them.
|
||||
export const TERMINAL_TYPE_SEARCH_ALIASES = ['terminal tab', 'terminal'] as const
|
||||
@@ -75,4 +80,8 @@ export type BuildSearchableWorkspaceTabsOptions = WorkspaceTabAgentMetadataState
|
||||
paneForegroundAgentByPaneKey?: Record<string, PaneForegroundAgentEntry>
|
||||
}
|
||||
|
||||
export const buildSearchableWorkspaceTabs = buildSearchableWorkspaceTabEntries
|
||||
export {
|
||||
buildSearchableWorkspaceTabs,
|
||||
buildWorkspaceTabPaletteEntries,
|
||||
prepareSearchableWorkspaceTabs
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@ const SURFACE_ROWS: readonly SurfaceRow[] = [
|
||||
{
|
||||
row: 52,
|
||||
path: 'src/renderer/src/lib/workspace-tab-palette-entry-builder.ts',
|
||||
marker: 'buildSearchableWorkspaceTabEntries'
|
||||
marker: 'buildWorkspaceTabPaletteEntries'
|
||||
},
|
||||
{
|
||||
row: 53,
|
||||
|
||||
Reference in New Issue
Block a user