Improve cmd-j palette ranking with token-count tiebreakers and identity

Add containerOnlyTokenCount and recoveryTokenCount fields to distinguish
entities when match quality is equal, enabling better ranking of results
that rely on container fields or recovery mechanisms. Cache paletteIdentity
in search results to avoid repeated encoding during sorting. Extract omnibox
field filtering and open-tab capping into reusable functions. Optimize
evidence-unit iteration to only process matched units. Strengthen worktree
ambiguity checks to reject hostless tabs when IDs collide across hosts.
This commit is contained in:
Jinjing
2026-09-06 15:40:48 -07:00
parent bb0e2fd31f
commit 80df9aabf5
25 changed files with 344 additions and 136 deletions
@@ -35,7 +35,15 @@ export function capPaletteSection<T>(
return { visible: items, overflowCount: 0 }
}
const visible = items.slice(0, cap)
const retained = retain ? items.slice(cap).find(retain) : undefined
let retained: T | undefined
if (retain) {
for (let index = cap; index < items.length; index += 1) {
if (retain(items[index])) {
retained = items[index]
break
}
}
}
if (retained !== undefined && cap > 0) {
visible.splice(cap - 1, 1, retained)
}
@@ -85,10 +85,9 @@ export type OpenTabSearchInput = {
type RankedResult = {
result: OpenTabSearchResult
sourceRank: number
matchRank: PaletteDocumentRank
activity: PaletteActivityRank
score: number
position: readonly [number, number]
identity: string
}
@@ -161,10 +160,9 @@ function rank<TEngine extends EngineResult>(
const converted = toResult(result)
return [
{
sourceRank: SOURCE_RANK[source],
matchRank: result.rank,
activity: result.activity,
score: result.score,
position: [SOURCE_RANK[source], result.score],
result: converted,
identity: converted.id
}
@@ -172,7 +170,7 @@ function rank<TEngine extends EngineResult>(
})
}
function searchOpenTabCandidates({
export function searchOpenTabCandidates({
workspaceTabs,
browserPages,
simulatorTabs,
@@ -267,13 +265,13 @@ function searchOpenTabCandidates({
{
rank: a.matchRank,
activity: a.activity,
position: [a.sourceRank, a.score],
position: a.position,
identity: a.identity
},
{
rank: b.matchRank,
activity: b.activity,
position: [b.sourceRank, b.score],
position: b.position,
identity: b.identity
}
)
@@ -282,10 +280,17 @@ function searchOpenTabCandidates({
}
export function searchOpenTabs(input: OpenTabSearchInput): OpenTabSearchResult[] {
return capOpenTabSearchCandidates(searchOpenTabCandidates(input), input.retainedResultId)
}
export function capOpenTabSearchCandidates(
candidates: readonly OpenTabSearchResult[],
retainedResultId?: string | null
): OpenTabSearchResult[] {
const capped = capPaletteSection(
searchOpenTabCandidates(input),
candidates,
OPEN_TAB_SEARCH_RESULT_LIMIT,
(result) => result.id === input.retainedResultId
(result) => result.id === retainedResultId
)
return [...capped.visible]
}
@@ -9,7 +9,11 @@ import {
selectOpenTabSearchEntryState,
type OpenTabSearchEntries
} from './open-tab-search-entries'
import { searchOpenTabs, type OpenTabSearchResult } from './open-tab-search'
import {
capOpenTabSearchCandidates,
searchOpenTabCandidates,
type OpenTabSearchResult
} from './open-tab-search'
import { usePaletteSearchEvaluationContext } from '@/hooks/use-palette-search-evaluation-context'
const EMPTY_RESULTS: OpenTabSearchResult[] = []
@@ -51,25 +55,26 @@ export function useOpenTabSearch({
[agentState, state]
)
const deferredQuery = useDeferredValue(query)
const evaluationSnapshot = useMemo(
() => ({ deferredQuery, enabled, entries }),
[deferredQuery, enabled, entries]
)
const evaluationSnapshot = useMemo(() => ({ deferredQuery, enabled }), [deferredQuery, enabled])
const context = usePaletteSearchEvaluationContext(evaluationSnapshot)
const candidates = useMemo(
() =>
entries
? searchOpenTabCandidates({
...entries,
query: deferredQuery,
context
})
: EMPTY_RESULTS,
[context, deferredQuery, entries]
)
return useMemo(
() => ({
query: deferredQuery,
entries,
results: entries
? searchOpenTabs({
...entries,
query: deferredQuery,
context,
retainedResultId
})
: EMPTY_RESULTS
results: capOpenTabSearchCandidates(candidates, retainedResultId)
}),
[context, deferredQuery, entries, retainedResultId]
[candidates, deferredQuery, entries, retainedResultId]
)
}
@@ -31,28 +31,9 @@ export function useWorktreeJumpPaletteController({
const paletteEvaluationSnapshot = useMemo(
() => ({
query: localState.paletteSearchQuery,
agentStatus: storeState.agentStatusByPaneKey,
worktrees: storeState.allWorktrees,
browserPages: storeState.browserPagesByWorkspace,
browserWorkspaces: storeState.browserTabsByWorktree,
openFiles: storeState.openFiles,
retainedAgents: storeState.retainedAgentsByPaneKey,
sleepingAgents: storeState.sleepingAgentSessionsByPaneKey,
unifiedTabs: storeState.unifiedTabsByWorktree,
visible
}),
[
localState.paletteSearchQuery,
storeState.agentStatusByPaneKey,
storeState.allWorktrees,
storeState.browserPagesByWorkspace,
storeState.browserTabsByWorktree,
storeState.openFiles,
storeState.retainedAgentsByPaneKey,
storeState.sleepingAgentSessionsByPaneKey,
storeState.unifiedTabsByWorktree,
visible
]
[localState.paletteSearchQuery, visible]
)
const paletteSearchContext = usePaletteSearchEvaluationContext(paletteEvaluationSnapshot)
const evaluation = { paletteSearchContext }
@@ -103,16 +103,35 @@ export function useWorktreeJumpPaletteSections({
[setExpandedSectionCaps]
)
const openTabsCap = PALETTE_SECTION_RENDER_CAP + (expandedSectionCaps['open-tabs'] ?? 0)
const typedWorktreeCap = PALETTE_SECTION_RENDER_CAP + (expandedSectionCaps.worktrees ?? 0)
const openTabIndexById = useMemo(
() => new Map(openTabItems.map((item, index) => [item.id, index])),
[openTabItems]
)
const worktreeIndexById = useMemo(
() => new Map(worktreeItems.map((item, index) => [item.id, index])),
[worktreeItems]
)
const retainedOpenTabId =
hasQuery && (openTabIndexById.get(selectedItemId ?? '') ?? -1) >= openTabsCap
? selectedItemId
: null
const retainedWorktreeId =
hasQuery && (worktreeIndexById.get(selectedItemId ?? '') ?? -1) >= typedWorktreeCap
? selectedItemId
: null
const paletteSections = useMemo(() => {
const openTabsCap = PALETTE_SECTION_RENDER_CAP + (expandedSectionCaps['open-tabs'] ?? 0)
const retainSelected = (item: { id: string }): boolean => item.id === selectedItemId
const retainOpenTab = (item: { id: string }): boolean => item.id === retainedOpenTabId
const retainWorktree = (item: { id: string }): boolean => item.id === retainedWorktreeId
// Why: "See more" drops the above-the-fold trim outright instead of stepping 20 at a time, so one
// click reveals the whole recent history the shared render cap allows.
const recentTabsCap = expandedSectionCaps['open-tabs']
? openTabsCap
: EMPTY_QUERY_RECENT_TAB_CAP
const openTabs = hasQuery
? capPaletteSection(openTabItems, openTabsCap, retainSelected)
? capPaletteSection(openTabItems, openTabsCap, retainOpenTab)
: capPaletteSection(recentTabItems, recentTabsCap)
const baseWorktreeCap = hasQuery
? Infinity
@@ -121,10 +140,10 @@ export function useWorktreeJumpPaletteSections({
Math.max(1, EMPTY_QUERY_ROW_BUDGET - openTabs.visible.length)
)
const worktreeCap = hasQuery
? PALETTE_SECTION_RENDER_CAP + (expandedSectionCaps.worktrees ?? 0)
? typedWorktreeCap
: baseWorktreeCap + (expandedSectionCaps.worktrees ?? 0)
const worktrees = hasQuery
? capPaletteSection(worktreeItems, worktreeCap, retainSelected)
? capPaletteSection(worktreeItems, worktreeCap, retainWorktree)
: {
visible: worktreeItems.slice(0, worktreeCap),
overflowCount: Math.max(0, worktreeItems.length - worktreeCap)
@@ -148,8 +167,8 @@ export function useWorktreeJumpPaletteSections({
(expandedSectionCaps[openTabsLeadSections ? 'open-tabs' : 'worktrees'] ?? 0),
leadingHardCap: openTabsLeadSections ? openTabsCap : worktreeCap,
trailingHardCap: openTabsLeadSections ? worktreeCap : openTabsCap,
leadingRetain: retainSelected,
trailingRetain: retainSelected
leadingRetain: openTabsLeadSections ? retainOpenTab : retainWorktree,
trailingRetain: openTabsLeadSections ? retainWorktree : retainOpenTab
})
: null
return {
@@ -170,9 +189,12 @@ export function useWorktreeJumpPaletteSections({
middleItems,
openTabItems,
openTabsLeadSections,
openTabsCap,
projectTargetItems,
recentTabItems,
selectedItemId,
retainedOpenTabId,
retainedWorktreeId,
typedWorktreeCap,
worktreeItems
])
+12 -18
View File
@@ -5,6 +5,7 @@ import { isClipboardTextByteLengthOverLimit } from '../../../shared/clipboard-te
import { compareBaseSensitivityLocaleText } from './locale-text-collators'
import {
comparePaletteTabResults,
isOmniboxPaletteTabFieldAllowed,
matchPaletteTabDocument,
preparePaletteTabQuery
} from './palette-match/tab-match'
@@ -45,6 +46,7 @@ export type SearchableBrowserPage = {
export type BrowserPaletteSearchResult = {
/** Worktree ids collide across hosts; activation must not resolve by id alone. */
executionHostId?: ExecutionHostId
paletteIdentity: string
pageId: string
workspaceId: string
worktreeId: string
@@ -163,6 +165,13 @@ function baseResult(
const activity = preparePaletteActivity(entry.lastActiveAt, context)
return {
...(executionHostId ? { executionHostId } : {}),
paletteIdentity: encodePaletteIdentity([
'browser',
executionHostId ?? '',
entry.worktree.id,
entry.workspace.id,
entry.page.id
]),
pageId: entry.page.id,
workspaceId: entry.workspace.id,
worktreeId: entry.worktree.id,
@@ -215,10 +224,7 @@ export function searchBrowserPages(
const base = baseResult(entry, context)
const secondaryTexts = browserPaletteSecondaryTexts(entry.page)
const match = matchPaletteTabDocument(entry.document, prepared, {
isFieldAllowed:
options.fieldMode === 'omnibox'
? (field) => field.id !== 'worktree' && field.id !== 'repo'
: undefined
isFieldAllowed: options.fieldMode === 'omnibox' ? isOmniboxPaletteTabFieldAllowed : undefined
})
if (!match) {
continue
@@ -248,25 +254,13 @@ export function searchBrowserPages(
{
rank: a.rank,
positionScore: a.score,
identity: encodePaletteIdentity([
'browser',
a.executionHostId ?? '',
a.worktreeId,
a.workspaceId,
a.pageId
]),
identity: a.paletteIdentity,
activity: a.activity
},
{
rank: b.rank,
positionScore: b.score,
identity: encodePaletteIdentity([
'browser',
b.executionHostId ?? '',
b.worktreeId,
b.workspaceId,
b.pageId
]),
identity: b.paletteIdentity,
activity: b.activity
}
)
@@ -69,6 +69,20 @@ it('refuses a hostless browser tab for a remote worktree whose ID also exists lo
).toBeNull()
})
it('refuses hostless activation when the caller omits a host for an ambiguous worktree id', () => {
seedState(
{
local: [makeWorktree({ id: 'wt' })],
remote: [makeWorktree({ id: 'wt', repoId: 'repo-remote', hostId: 'ssh:remote' })]
},
{ ...browserTab, executionHostId: 'ssh:remote' }
)
expect(
getActivatableBrowserWorkspaceTab({ worktreeId: 'wt', workspaceId: 'workspace' })
).toBeNull()
})
it('accepts a hostless browser tab when the worktree ID is unambiguous', () => {
seedState({ remote: [makeWorktree({ id: 'wt', hostId: 'ssh:remote' })] }, browserTab)
@@ -13,16 +13,17 @@ type BrowserWorkspaceTabTarget = {
export function getActivatableBrowserWorkspaceTab(params: BrowserWorkspaceTabTarget): Tab | null {
const state = useAppStore.getState()
const worktree = params.executionHostId
? state.getKnownWorktreeById(params.worktreeId, params.executionHostId)
: undefined
if (params.executionHostId && !worktree) {
return null
}
// A hostless tab cannot be attributed when the same worktree ID exists on several hosts.
const ambiguousWorktreeIds = findAmbiguousWorktreeIds(
getIndexedAllWorktrees(state.worktreesByRepo)
)
if (!params.executionHostId && ambiguousWorktreeIds.has(params.worktreeId)) {
return null
}
const worktree = state.getKnownWorktreeById(params.worktreeId, params.executionHostId)
if (!worktree) {
return null
}
// setActiveBrowserTab resolves its backing tab globally by workspace ID.
const tabs = Object.values(state.unifiedTabsByWorktree).flat()
const browserTabs = tabs.filter(
@@ -15,6 +15,8 @@ function rank(overrides: Partial<PaletteDocumentRank> = {}): PaletteDocumentRank
recovery: 0,
wordMatch: 0,
coverage: 0,
containerOnlyTokenCount: 0,
recoveryTokenCount: 0,
strength: 0,
placement: 2,
...overrides
@@ -2,7 +2,7 @@ import { matchPaletteField, type PaletteFieldMatch } from './match-field'
import { resolvePaletteResultQualityClass, type PaletteMatchQuality } from './match-quality'
import { createPaletteQueryToken, type PaletteQueryToken } from './palette-query'
import {
comparePaletteDocumentRank,
comparePaletteProofRank,
type PaletteDocument,
type PaletteDocumentMatch,
type PaletteTokenAssignment
@@ -103,7 +103,7 @@ function toCandidate(hits: readonly FieldHit[]): TokenCandidate {
function matchCompositePairs(
document: PaletteDocument,
token: PaletteQueryToken,
isFieldAllowed: (field: PaletteIndexedField) => boolean
isFieldAllowed?: (field: PaletteIndexedField) => boolean
): TokenCandidate[] {
if (!token.repoBranch || !document.compositePairs.length) {
return []
@@ -114,7 +114,11 @@ function matchCompositePairs(
for (const pair of document.compositePairs) {
const leftField = document.fieldById.get(pair.leftFieldId)
const rightField = document.fieldById.get(pair.rightFieldId)
if (!leftField || !rightField || !isFieldAllowed(leftField) || !isFieldAllowed(rightField)) {
if (
!leftField ||
!rightField ||
(isFieldAllowed && (!isFieldAllowed(leftField) || !isFieldAllowed(rightField)))
) {
continue
}
const leftMatch = matchPaletteField(leftField, left)
@@ -134,7 +138,7 @@ function matchCompositePairs(
function collectTokenCandidates(
document: PaletteDocument,
token: PaletteQueryToken,
isFieldAllowed: (field: PaletteIndexedField) => boolean
isFieldAllowed?: (field: PaletteIndexedField) => boolean
): TokenCandidates | null {
const candidates: TokenCandidates = {
visible: matchCompositePairs(document, token, isFieldAllowed),
@@ -142,7 +146,7 @@ function collectTokenCandidates(
}
let found = candidates.visible.length > 0
for (const field of document.fields) {
if (!isFieldAllowed(field)) {
if (isFieldAllowed && !isFieldAllowed(field)) {
continue
}
const match = matchPaletteField(field, token)
@@ -207,10 +211,9 @@ export function matchPaletteDocument(args: {
isFieldAllowed?: (field: PaletteIndexedField) => boolean
diagnostics?: PaletteMatchDiagnostics
}): PaletteDocumentMatch | null {
const isFieldAllowed = args.isFieldAllowed ?? (() => true)
const candidates: TokenCandidates[] = []
for (const token of args.tokens) {
const collected = collectTokenCandidates(args.document, token, isFieldAllowed)
const collected = collectTokenCandidates(args.document, token, args.isFieldAllowed)
if (!collected) {
return null
}
@@ -244,7 +247,13 @@ export function matchPaletteDocument(args: {
args.normalizedQuery,
null
)
for (const evidenceId of args.document.evidenceUnits.keys()) {
const matchedEvidenceIds = new Set<string>()
for (const candidate of candidates) {
for (const evidenceId of candidate.byEvidenceId.keys()) {
matchedEvidenceIds.add(evidenceId)
}
}
for (const evidenceId of matchedEvidenceIds) {
ranked.push(
...collectScopeAssignments({
document: args.document,
@@ -270,7 +279,7 @@ export function matchPaletteDocument(args: {
return null
}
ranked.sort((a, b) => {
const rank = comparePaletteDocumentRank(a.rank, b.rank)
const rank = comparePaletteProofRank(a.rank, b.rank)
if (rank !== 0) {
return rank
}
@@ -108,6 +108,10 @@ function rankSelected(
recovery: Math.max(...selected.map((candidate) => candidate.recovery)),
wordMatch: Math.max(...selected.map((candidate) => candidate.wordMatch)),
coverage: Math.max(...selected.map((candidate) => candidate.coverage)),
containerOnlyTokenCount: selected.filter((candidate) =>
candidate.hits.every((hit) => hit.field.role === 'container')
).length,
recoveryTokenCount: selected.filter((candidate) => candidate.recovery > 0).length,
strength: Math.max(...selected.map((candidate) => candidate.strength)),
placement
}
@@ -132,6 +132,10 @@ export type PaletteDocumentRank = {
recovery: number
wordMatch: number
coverage: number
/** Tokens proved only by container fields; fewer preserves direct-match relevance. */
containerOnlyTokenCount: number
/** Tokens that required compact or typo recovery; fewer breaks equal-severity ties. */
recoveryTokenCount: number
strength: number
/** 0 prefix, 1 later word boundary, 2 distributed/other. */
placement: number
@@ -146,6 +150,18 @@ export type PaletteDocumentMatch = {
}
const RANK_KEYS: readonly (keyof PaletteDocumentRank)[] = [
'destination',
'recovery',
'wordMatch',
'coverage',
'containerOnlyTokenCount',
'recoveryTokenCount',
'strength',
'placement'
]
// Aggregate token counts distinguish entities without replacing the best rendered proof.
const PROOF_RANK_KEYS: readonly (keyof PaletteDocumentRank)[] = [
'destination',
'recovery',
'wordMatch',
@@ -159,6 +175,8 @@ const SEMANTIC_RANK_KEYS: readonly (keyof PaletteDocumentRank)[] = [
'recovery',
'wordMatch',
'coverage',
'containerOnlyTokenCount',
'recoveryTokenCount',
'strength'
]
@@ -184,10 +202,32 @@ export function comparePaletteDocumentRank(a: PaletteDocumentRank, b: PaletteDoc
return compareRankKeys(a, b, RANK_KEYS)
}
export function comparePaletteProofRank(a: PaletteDocumentRank, b: PaletteDocumentRank): number {
return compareRankKeys(a, b, PROOF_RANK_KEYS)
}
export function createRecognizedPaletteRank(): PaletteDocumentRank {
return { destination: 0, recovery: 0, wordMatch: 0, coverage: 0, strength: 0, placement: 0 }
return {
destination: 0,
recovery: 0,
wordMatch: 0,
coverage: 0,
containerOnlyTokenCount: 0,
recoveryTokenCount: 0,
strength: 0,
placement: 0
}
}
export function createPaletteFallbackRank(): PaletteDocumentRank {
return { destination: 3, recovery: 0, wordMatch: 0, coverage: 0, strength: 0, placement: 0 }
return {
destination: 3,
recovery: 0,
wordMatch: 0,
coverage: 0,
containerOnlyTokenCount: 0,
recoveryTokenCount: 0,
strength: 0,
placement: 0
}
}
@@ -6,7 +6,7 @@ import { preparePaletteQuery } from './palette-query'
import { buildWorktreePaletteDocuments } from '../worktree-palette-document'
import { searchWorktreeDocuments } from '../worktree-palette-search'
import { comparePaletteEntityRanks, createPaletteSearchContext } from './palette-ranking'
import type { PaletteDocument } from './palette-document'
import { buildPaletteDocument, type PaletteDocument } from './palette-document'
import type { PaletteQueryToken } from './palette-query'
import type { Repo } from '../../../../shared/repo-types'
import type { Worktree } from '../../../../shared/worktree/types'
@@ -203,6 +203,55 @@ describe('palette matcher performance budget', () => {
)
})
it('does not revisit an all-visible assignment for unmatched evidence units', () => {
const buildDocument = (evidenceCount: number): PaletteDocument =>
buildPaletteDocument({
id: `visible-${evidenceCount}`,
visibleFields: [
{
id: 'title',
profile: 'structured-label',
text: 'atlas',
role: 'primary',
destinationEligible: true
}
],
evidence: Array.from({ length: evidenceCount }, (_, index) => ({
unit: {
id: `evidence-${index}`,
kind: 'comment',
text: `unrelated ${index}`,
accessibilityLabel: 'Comment'
},
fields: [
{
id: `evidence-field-${index}`,
profile: 'prose' as const,
text: `unrelated ${index}`,
evidenceId: `evidence-${index}`,
renderOffset: 0
}
]
}))
})
const query = preparePaletteQuery('atlas')
if (query.state !== 'ready') {
throw new Error('Expected ready query')
}
const selectionVisits = (document: PaletteDocument): number => {
const diagnostics: PaletteMatchDiagnostics = { selectionCandidateVisits: 0 }
matchPaletteDocument({
document,
tokens: query.tokens,
normalizedQuery: query.normalized,
diagnostics
})
return diagnostics.selectionCandidateVisits
}
expect(selectionVisits(buildDocument(100))).toBe(selectionVisits(buildDocument(0)))
})
it('keeps retained match and range payload within budget', () => {
const documents = buildWorktreePaletteDocuments(worktrees, sources)
const matches = matchEveryDocument(documents)
@@ -19,6 +19,8 @@ function rank(overrides: Partial<PaletteDocumentRank> = {}): PaletteDocumentRank
recovery: 0,
wordMatch: 0,
coverage: 0,
containerOnlyTokenCount: 0,
recoveryTokenCount: 0,
strength: 0,
placement: 2,
...overrides
@@ -18,6 +18,10 @@ import { comparePaletteEntityRanks, type PaletteActivityRank } from './palette-r
const NO_RANGES: readonly MatchRange[] = []
export function isOmniboxPaletteTabFieldAllowed(field: Pick<PaletteIndexedField, 'id'>): boolean {
return field.id !== PALETTE_TAB_WORKTREE_FIELD_ID && field.id !== PALETTE_TAB_REPO_FIELD_ID
}
export type PaletteTabIndexedMatch = { index: number; ranges: readonly MatchRange[] }
export type PaletteTabMatch = {
@@ -8,6 +8,7 @@ import { isClipboardTextByteLengthOverLimit } from '../../../shared/clipboard-te
import { compareBaseSensitivityLocaleText } from './locale-text-collators'
import {
comparePaletteTabResults,
isOmniboxPaletteTabFieldAllowed,
matchPaletteTabDocument,
preparePaletteTabQuery
} from './palette-match/tab-match'
@@ -49,6 +50,7 @@ export type SearchableSimulatorTab = {
export type SimulatorPaletteSearchResult = {
/** Worktree ids collide across hosts; activation must not resolve by id alone. */
executionHostId?: ExecutionHostId
paletteIdentity: string
tabId: string
worktreeId: string
groupId: string
@@ -150,12 +152,19 @@ function baseResult(
entry: SearchableSimulatorTab,
context: PaletteSearchContext
): SimulatorPaletteSearchResult {
const executionHostId = getUnifiedTabPaletteExecutionHostId(entry.tab, entry.worktree)
const activity = preparePaletteActivity(
maxValidPaletteActivityTimestamp([entry.tab.lastFocusedAt, entry.tab.createdAt]),
context
)
return {
executionHostId: getUnifiedTabPaletteExecutionHostId(entry.tab, entry.worktree),
...(executionHostId ? { executionHostId } : {}),
paletteIdentity: encodePaletteIdentity([
'simulator',
executionHostId ?? '',
entry.worktree.id,
entry.tab.id
]),
tabId: entry.tab.id,
worktreeId: entry.worktree.id,
groupId: entry.tab.groupId,
@@ -270,10 +279,7 @@ export function searchSimulatorTabs(
const results: SimulatorPaletteSearchResult[] = []
for (const entry of entries) {
const match = matchPaletteTabDocument(entry.document, prepared, {
isFieldAllowed:
options.fieldMode === 'omnibox'
? (field) => field.id !== 'worktree' && field.id !== 'repo'
: undefined
isFieldAllowed: options.fieldMode === 'omnibox' ? isOmniboxPaletteTabFieldAllowed : undefined
})
if (!match) {
continue
@@ -304,23 +310,13 @@ export function searchSimulatorTabs(
{
rank: a.rank,
positionScore: a.score,
identity: encodePaletteIdentity([
'simulator',
a.executionHostId ?? '',
a.worktreeId,
a.tabId
]),
identity: a.paletteIdentity,
activity: a.activity
},
{
rank: b.rank,
positionScore: b.score,
identity: encodePaletteIdentity([
'simulator',
b.executionHostId ?? '',
b.worktreeId,
b.tabId
]),
identity: b.paletteIdentity,
activity: b.activity
}
)
@@ -157,6 +157,20 @@ describe('activateSimulatorTabPaletteResult', () => {
expect(mocks.activateAndRevealWorktree).not.toHaveBeenCalled()
})
it('rejects a hostless tab when its worktree id exists on multiple hosts', () => {
seedStore({
worktreesByRepo: {
local: [makeWorktree()],
remote: [makeWorktree({ repoId: 'repo-2', hostId: 'ssh:remote', path: '/tmp/remote' })]
}
})
expect(activateSimulatorTabPaletteResult({ ...target, executionHostId: 'ssh:remote' })).toEqual(
{ status: 'failed', reason: 'missing-tab' }
)
expect(mocks.activateAndRevealWorktree).not.toHaveBeenCalled()
})
it('reports an unknown worktree without activating', () => {
seedStore({ worktreesByRepo: {} })
@@ -1,7 +1,8 @@
import { useAppStore } from '@/store'
import type { ExecutionHostId } from '../../../shared/execution-host'
import { activateAndRevealWorktree } from './worktree-activation'
import { isUnifiedTabOwnedByWorktree } from './unified-tab-host-ownership'
import { getIndexedAllWorktrees } from '@/store/worktree-repo-index'
import { findAmbiguousWorktreeIds, isUnifiedTabOwnedByWorktree } from './unified-tab-host-ownership'
export type SimulatorTabPaletteActivationFailure = 'missing-tab' | 'missing-worktree'
@@ -29,10 +30,13 @@ export function activateSimulatorTabPaletteResult({
(candidate) => candidate.id === tabId
)
const tab = tabs[0]
const ambiguousWorktreeIds = findAmbiguousWorktreeIds(
getIndexedAllWorktrees(initialState.worktreesByRepo)
)
if (
tabs.length !== 1 ||
tab.contentType !== 'simulator' ||
!isUnifiedTabOwnedByWorktree(tab, worktree, new Set())
!isUnifiedTabOwnedByWorktree(tab, worktree, ambiguousWorktreeIds)
) {
return { status: 'failed', reason: 'missing-tab' }
}
@@ -75,6 +75,12 @@ export function isOpenFileOwnedByWorktree(
return isExecutionHostAliasForWorktree(LOCAL_EXECUTION_HOST_ID, worktree)
}
export function hasOpenFileExecutionHostEvidence(
file: Pick<OpenFile, 'externalSshTargetId' | 'operationProvenance' | 'runtimeEnvironmentId'>
): boolean {
return Boolean(file.operationProvenance || file.externalSshTargetId || file.runtimeEnvironmentId)
}
export function getUnifiedTabPaletteExecutionHostId(
tab: Pick<Tab, 'executionHostId'> | undefined,
worktree: Pick<Worktree, 'hostId' | 'runtimeOwnerEnvironmentId'>
@@ -120,6 +120,7 @@ function makeResult(
score: 0,
qualityClass: null,
rank: null,
paletteIdentity: 'terminal\u0000wt-1\u0000group-1\u0000unified-terminal-1',
lastActiveAt: null,
activity: { ageBucket: null, timestamp: 0 },
...overrides
@@ -11,6 +11,7 @@ import { activateAndRevealWorktree } from './worktree-activation'
import { getIndexedAllWorktrees } from '@/store/worktree-repo-index'
import {
findAmbiguousWorktreeIds,
hasOpenFileExecutionHostEvidence,
isOpenFileOwnedByWorktree,
isUnifiedTabOwnedByWorktree
} from './unified-tab-host-ownership'
@@ -84,11 +85,9 @@ function validateTarget(
return 'missing-file'
}
const file = files[0]
const hasExplicitHost = Boolean(
file.operationProvenance || file.externalSshTargetId || file.runtimeEnvironmentId
)
// A hostless file falls back to local ownership, which only decides the match when IDs collide.
const requiresOwnershipCheck = hasExplicitHost || ambiguousWorktreeIds.has(worktree.id)
const requiresOwnershipCheck =
hasOpenFileExecutionHostEvidence(file) || ambiguousWorktreeIds.has(worktree.id)
if (requiresOwnershipCheck && !isOpenFileOwnedByWorktree(file, worktree)) {
return 'missing-file'
}
@@ -24,6 +24,7 @@ import type {
import {
findAmbiguousWorktreeIds,
getUnifiedTabPaletteExecutionHostId,
hasOpenFileExecutionHostEvidence,
isOpenFileOwnedByWorktree,
isUnifiedTabOwnedByWorktree
} from './unified-tab-host-ownership'
@@ -264,7 +265,9 @@ export function buildSearchableWorkspaceTabEntries({
?.find(
(candidate) =>
candidate.worktreeId === worktree.id &&
(!ambiguousWorktreeIds.has(worktree.id) ||
(!(
hasOpenFileExecutionHostEvidence(candidate) || ambiguousWorktreeIds.has(worktree.id)
) ||
isOpenFileOwnedByWorktree(candidate, worktree))
)
if (!file) {
@@ -286,8 +286,35 @@ describe('searchWorkspaceTabs ranking', () => {
const results = searchWorkspaceTabs([containerEntry, directEntry], 'auth aurora')
expect(results.map((result) => result.tabId)).toEqual(['container-tab', 'direct-tab'])
expect(results.map((result) => result.tabId)).toEqual(['direct-tab', 'container-tab'])
expect(results.map((result) => result.rank?.coverage)).toEqual([2, 2])
expect(results.map((result) => result.rank?.containerOnlyTokenCount)).toEqual([1, 2])
})
it('ranks one recovered token above an otherwise-equal all-recovered match', () => {
const oneRecovery = makeEntry({ id: 'one-recovery' })
const twoRecoveries = makeEntry({ id: 'two-recoveries' })
oneRecovery.document = buildPaletteTabDocument({
id: 'one-recovery',
title: 'alphx bravo',
secondaryTexts: [],
worktreeName: 'workspace',
branch: 'main',
repoName: 'repo'
})
twoRecoveries.document = buildPaletteTabDocument({
id: 'two-recoveries',
title: 'alphx bravx',
secondaryTexts: [],
worktreeName: 'workspace',
branch: 'main',
repoName: 'repo'
})
const results = searchWorkspaceTabs([twoRecoveries, oneRecovery], 'alpha bravo')
expect(results.map((result) => result.tabId)).toEqual(['one-recovery', 'two-recoveries'])
expect(results.map((result) => result.rank?.recoveryTokenCount)).toEqual([1, 2])
})
it('ranks direct tab title matches ahead of container-only worktree matches', () => {
@@ -1,6 +1,7 @@
import { compareBaseSensitivityLocaleText } from './locale-text-collators'
import {
comparePaletteTabResults,
isOmniboxPaletteTabFieldAllowed,
matchPaletteTabDocument,
preparePaletteTabQuery,
isPaletteTabQueryRejected
@@ -35,6 +36,7 @@ const NO_RANGES: readonly MatchRange[] = []
export type WorkspaceTabPaletteSearchResult = {
/** Worktree ids collide across hosts; activation must not resolve by id alone. */
executionHostId?: ExecutionHostId
paletteIdentity: string
tabId: string
entityId: string
worktreeId: string
@@ -113,6 +115,12 @@ function baseResult(
const activity = preparePaletteActivity(resolveWorkspaceTabLastActiveAt(entry), context)
return {
...(executionHostId ? { executionHostId } : {}),
paletteIdentity: encodePaletteIdentity([
'workspace',
executionHostId ?? '',
entry.worktree.id,
entry.tab.id
]),
tabId: entry.tab.id,
entityId: entry.tab.entityId,
worktreeId: entry.worktree.id,
@@ -148,19 +156,11 @@ function matchEntry(
context: PaletteSearchContext,
fieldMode: 'all' | 'omnibox'
): WorkspaceTabPaletteSearchResult | null {
const isOmniboxFieldAllowed = (field: { id: string }): boolean =>
field.id !== 'worktree' && field.id !== 'repo'
const match = matchPaletteTabDocument(entry.document, query, {
isFieldAllowed: fieldMode === 'omnibox' ? isOmniboxFieldAllowed : undefined
})
if (!match) {
const unrestrictedMatch = matchPaletteTabDocument(entry.document, query)
if (!unrestrictedMatch) {
// Why kept separate: agent text is not part of the structured field set, so it
// never contributes to token coverage — it only recovers a row nothing else found.
const unrestrictedStructuredMatch =
fieldMode === 'omnibox' ? matchPaletteTabDocument(entry.document, query) : null
const snippet = unrestrictedStructuredMatch
? null
: matchWorkspaceTabAgentSnippet(entry.agentMetadata, query)
const snippet = matchWorkspaceTabAgentSnippet(entry.agentMetadata, query)
if (!snippet) {
return null
}
@@ -173,6 +173,17 @@ function matchEntry(
}
}
const match =
fieldMode !== 'omnibox' ||
(unrestrictedMatch.worktreeRanges.length === 0 && unrestrictedMatch.repoRanges.length === 0)
? unrestrictedMatch
: matchPaletteTabDocument(entry.document, query, {
isFieldAllowed: isOmniboxPaletteTabFieldAllowed
})
if (!match) {
return null
}
const secondaryText =
match.secondary !== null
? (entry.secondarySearchTexts[match.secondary.index] ?? entry.secondaryText)
@@ -235,23 +246,13 @@ export function searchWorkspaceTabs(
{
rank: a.rank,
positionScore: a.score,
identity: encodePaletteIdentity([
'workspace',
a.executionHostId ?? '',
a.worktreeId,
a.tabId
]),
identity: a.paletteIdentity,
activity: a.activity
},
{
rank: b.rank,
positionScore: b.score,
identity: encodePaletteIdentity([
'workspace',
b.executionHostId ?? '',
b.worktreeId,
b.tabId
]),
identity: b.paletteIdentity,
activity: b.activity
}
)
@@ -164,6 +164,23 @@ describe('workspace-tab-palette-search', () => {
expect(entries.map((entry) => entry.tab.id)).toEqual(['unified-editor-dup'])
expect(entries[0]?.secondaryText).toBe(SRC_APP_RELATIVE_PATH)
})
it('omits an editor row whose explicit file host disagrees with its unique worktree', () => {
const remote = makeWorktree({ hostId: 'ssh:remote' })
const editor = makeUnifiedTab({
id: 'remote-editor',
entityId: SRC_APP_PATH,
contentType: 'editor',
executionHostId: 'ssh:remote'
})
const entries = buildEntries({
worktrees: [remote],
unifiedTabsByWorktree: { 'wt-1': [editor] },
openFiles: [makeOpenFile({ externalSshTargetId: 'other-host' })]
})
expect(entries).toEqual([])
})
it('emits one entry per tab id when a session persisted the same id twice', () => {
// Why: the palette keys rows by tab id, and duplicated persisted records used
// to render the row twice under one React key, stranding a ghost row.