Improve cmd+j search ranking with direct fields and recency

Prioritize results matching direct fields (titles, content) over
container fields (worktree, branch, repo). Use tab focus time to break
ranking ties. Makes search more useful for quick navigation.
This commit is contained in:
Jinjing
2026-08-19 18:22:52 -07:00
parent e0fb5c9bb0
commit a37b3fca15
19 changed files with 267 additions and 49 deletions
@@ -138,7 +138,7 @@ import {
} from '@/components/browser-pane/host-guest/browser-focus'
import { RepoBadgeMark } from '@/components/repo/RepoBadgeLabel'
import { buildSidebarHostOptions } from '@/components/sidebar/sidebar-host-options'
import { getPaletteHostBadge, type PaletteHostBadge } from '@/components/cmd-j/palette-host-badge'
import { getPaletteHostBadge } from '@/components/cmd-j/palette-host-badge'
import {
composeWorktreeHostIdentity,
getWorktreeHostIdentity
@@ -1385,8 +1385,18 @@ function WorktreeJumpPaletteContent({
// An empty query leaves every rank null, so ordering falls through to those scores.
return items.sort((a, b) =>
comparePaletteRankedItems(
{ rank: a.result.rank, order: a.result.score, id: a.id },
{ rank: b.result.rank, order: b.result.score, id: b.id }
{
rank: a.result.rank,
order: a.result.score,
id: a.id,
lastActiveAt: a.result.lastActiveAt ?? undefined
},
{
rank: b.result.rank,
order: b.result.score,
id: b.id,
lastActiveAt: b.result.lastActiveAt ?? undefined
}
)
)
}, [browserItems, simulatorItems, workspaceTabItems])
+13 -2
View File
@@ -55,6 +55,7 @@ export type BrowserPaletteSearchResult = {
score: number
qualityClass: PaletteResultQualityClass | null
rank: PaletteDocumentRank | null
lastActiveAt?: number | null
}
export const BROWSER_PALETTE_QUERY_MAX_BYTES = 2 * 1024
@@ -208,8 +209,18 @@ export function searchBrowserPages(
return results.sort((a, b) =>
a.rank && b.rank
? comparePaletteTabResults(
{ rank: a.rank, positionScore: a.score, id: a.pageId },
{ rank: b.rank, positionScore: b.score, id: b.pageId }
{
rank: a.rank,
positionScore: a.score,
id: a.pageId,
lastActiveAt: a.lastActiveAt ?? undefined
},
{
rank: b.rank,
positionScore: b.score,
id: b.pageId,
lastActiveAt: b.lastActiveAt ?? undefined
}
)
: compareEmptyQueryResults(a, b)
)
@@ -12,6 +12,7 @@ import type { PaletteDocumentRank } from './palette-match/palette-document'
function rank(overrides: Partial<PaletteDocumentRank> = {}): PaletteDocumentRank {
return {
exactIntent: 1,
matchedDirectField: 0,
wholeQuery: 3,
worstQuality: 5,
usesSupportingEvidence: 0,
@@ -114,9 +115,15 @@ describe('ranked item comparison', () => {
expect(comparePaletteRankedItems(strong, weak)).toBeLessThan(0)
})
it('falls back to the section order when match rank ties', () => {
const first = { rank: rank(), order: 1, id: 'z' }
const second = { rank: rank(), order: 2, id: 'a' }
it('prefers recently active item when match rank ties', () => {
const recent = { rank: rank(), order: 10, id: 'z', lastActiveAt: 2000 }
const older = { rank: rank(), order: 0, id: 'a', lastActiveAt: 1000 }
expect(comparePaletteRankedItems(recent, older)).toBeLessThan(0)
})
it('falls back to the section order when match rank and recency tie', () => {
const first = { rank: rank(), order: 1, id: 'z', lastActiveAt: 1000 }
const second = { rank: rank(), order: 2, id: 'a', lastActiveAt: 1000 }
expect(comparePaletteRankedItems(first, second)).toBeLessThan(0)
})
@@ -31,9 +31,11 @@ export type PaletteRankedItem = {
/** Existing smart-recency / list position, used only after match rank ties. */
order: number
id: string
/** Timestamp of most recent activity (focus or agent interaction). */
lastActiveAt?: number
}
/** Match rank first, then the section's own recency order, then a stable id. */
/** Match rank first, then recent activity, then positional order, then stable id. */
export function comparePaletteRankedItems(a: PaletteRankedItem, b: PaletteRankedItem): number {
if (a.rank && b.rank) {
const byRank = comparePaletteDocumentRank(a.rank, b.rank)
@@ -43,6 +45,13 @@ export function comparePaletteRankedItems(a: PaletteRankedItem, b: PaletteRanked
} else if (a.rank !== b.rank) {
return a.rank ? -1 : 1
}
if (a.lastActiveAt !== b.lastActiveAt) {
const aTime = a.lastActiveAt ?? 0
const bTime = b.lastActiveAt ?? 0
if (aTime !== bTime) {
return bTime - aTime
}
}
if (a.order !== b.order) {
return a.order - b.order
}
@@ -30,6 +30,8 @@ export type PaletteFieldSource = {
/** null marks a visible identity field; identity fields combine freely. */
evidenceId?: string | null
identifier?: PaletteIdentifierOptions
/** Container-level fields (e.g. worktree/branch for tabs) demote when matched alone. */
isContainer?: boolean
}
export type PaletteIndexedField = {
@@ -40,6 +42,7 @@ export type PaletteIndexedField = {
words: readonly PaletteWord[]
evidenceId: string | null
identifier: PaletteIdentifierOptions | null
isContainer: boolean
}
const IDENTIFIER_PREFIX_KINDS: ReadonlySet<PaletteIdentifierKind> = new Set<PaletteIdentifierKind>([
@@ -125,7 +128,8 @@ export function indexPaletteField(source: PaletteFieldSource): PaletteIndexedFie
atoms: segments.atoms,
words: segments.words,
evidenceId: source.evidenceId ?? null,
identifier: source.identifier ?? null
identifier: source.identifier ?? null,
isContainer: Boolean(source.isContainer)
}
}
@@ -154,14 +154,16 @@ function buildAssignments(
}
function rankAssignments(args: {
document: PaletteDocument
assignments: readonly PaletteTokenAssignment[]
usesEvidence: boolean
wholeQuery: number
exactIntent: boolean
}): { rank: PaletteDocumentRank; worstQuality: PaletteMatchQuality } {
}): { rank: PaletteDocumentRank; worstQuality: PaletteMatchQuality; isContainerOnly: boolean } {
let worstQuality: PaletteMatchQuality = 'field-exact'
let fuzzyTokenCount = 0
const fields = new Set<string>()
let hasDirectHit = args.usesEvidence
for (const assignment of args.assignments) {
if (paletteMatchQualityRank(assignment.quality) > paletteMatchQualityRank(worstQuality)) {
@@ -171,12 +173,20 @@ function rankAssignments(args: {
fuzzyTokenCount += 1
}
fields.add(assignment.fieldId)
const field = args.document.fieldById.get(assignment.fieldId)
if (field && !field.isContainer) {
hasDirectHit = true
}
}
const isContainerOnly = !hasDirectHit
return {
worstQuality,
isContainerOnly,
rank: {
exactIntent: args.exactIntent ? 0 : 1,
matchedDirectField: isContainerOnly ? 1 : 0,
wholeQuery: args.wholeQuery,
worstQuality: paletteMatchQualityRank(worstQuality),
usesSupportingEvidence: args.usesEvidence ? 1 : 0,
@@ -274,7 +284,8 @@ export function matchPaletteDocument(args: {
continue
}
const usedEvidenceId = built.usesEvidence ? evidenceId : null
const { rank, worstQuality } = rankAssignments({
const { rank, worstQuality, isContainerOnly } = rankAssignments({
document,
assignments: built.assignments,
usesEvidence: built.usesEvidence,
wholeQuery,
@@ -288,7 +299,8 @@ export function matchPaletteDocument(args: {
? 'exact-intent'
: resolvePaletteResultQualityClass({
worstQuality,
usesSupportingEvidence: built.usesEvidence
usesSupportingEvidence: built.usesEvidence,
isContainerOnly
}),
rank,
assignments: built.assignments,
@@ -59,11 +59,15 @@ export function paletteResultQualityClassRank(value: PaletteResultQualityClass):
export function resolvePaletteResultQualityClass(args: {
worstQuality: PaletteMatchQuality
usesSupportingEvidence: boolean
isContainerOnly?: boolean
}): PaletteResultQualityClass {
const { worstQuality, usesSupportingEvidence } = args
const { worstQuality, usesSupportingEvidence, isContainerOnly } = args
if (isFuzzyPaletteMatchQuality(worstQuality)) {
return 'fuzzy-evidence'
}
if (isContainerOnly) {
return isExactPaletteMatchQuality(worstQuality) ? 'exact-evidence' : 'partial-evidence'
}
if (usesSupportingEvidence) {
return isExactPaletteMatchQuality(worstQuality) ? 'exact-evidence' : 'partial-evidence'
}
@@ -40,6 +40,7 @@ export type PaletteDocument = {
/** Visible identity fields, cached because they carry the whole-query check. */
visibleFields: readonly PaletteIndexedField[]
fieldsByEvidenceId: ReadonlyMap<string, readonly PaletteIndexedField[]>
fieldById: ReadonlyMap<string, PaletteIndexedField>
}
export type PaletteDocumentInput = {
@@ -80,7 +81,9 @@ export function buildPaletteDocument(input: PaletteDocumentInput): PaletteDocume
const fields = indexPaletteFields([...input.visibleFields, ...evidenceSources])
const fieldsByEvidenceId = new Map<string, PaletteIndexedField[]>()
const visibleFields: PaletteIndexedField[] = []
const fieldById = new Map<string, PaletteIndexedField>()
for (const field of fields) {
fieldById.set(field.id, field)
if (!field.evidenceId) {
visibleFields.push(field)
continue
@@ -103,7 +106,8 @@ export function buildPaletteDocument(input: PaletteDocumentInput): PaletteDocume
evidenceUnits,
renderOffsetByFieldId,
visibleFields,
fieldsByEvidenceId
fieldsByEvidenceId,
fieldById
}
}
@@ -125,6 +129,8 @@ export type PaletteSupportingEvidence = {
export type PaletteDocumentRank = {
/** 0 when a recognized exact intent (such as a task URL) produced this row. */
exactIntent: number
/** 0 when query matched at least one direct field; 1 when all matched tokens are container-only. */
matchedDirectField: number
/** 0 equality, 1 prefix, 2 word boundary, 3 none — whole query in visible text. */
wholeQuery: number
worstQuality: number
@@ -144,6 +150,7 @@ export type PaletteDocumentMatch = {
const RANK_KEYS: readonly (keyof PaletteDocumentRank)[] = [
'exactIntent',
'matchedDirectField',
'wholeQuery',
'worstQuality',
'usesSupportingEvidence',
@@ -381,3 +381,35 @@ describe('typo distance', () => {
expect(isWithinOnePaletteEdit(a, b)).toBe(expected)
})
})
describe('container field matching', () => {
it('marks matchedDirectField as 1 and demotes quality class when all tokens land on container fields', () => {
const tabDoc: PaletteDocumentInput = {
id: 'tab-1',
visibleFields: [
{ id: 'title', profile: 'structured-label', text: 'README.md' },
{ id: 'worktree', profile: 'structured-label', text: 'STA-4360-feature', isContainer: true }
],
evidence: []
}
const match = run(tabDoc, '4360')
expect(match).not.toBeNull()
expect(match?.rank.matchedDirectField).toBe(1)
expect(match?.qualityClass).toBe('exact-evidence')
})
it('marks matchedDirectField as 0 when at least one token lands on a direct field', () => {
const tabDoc: PaletteDocumentInput = {
id: 'tab-1',
visibleFields: [
{ id: 'title', profile: 'structured-label', text: 'wsl-transcript-4360.ts' },
{ id: 'worktree', profile: 'structured-label', text: 'STA-4360-feature', isContainer: true }
],
evidence: []
}
const match = run(tabDoc, '4360')
expect(match).not.toBeNull()
expect(match?.rank.matchedDirectField).toBe(0)
expect(match?.qualityClass).toBe('exact-visible')
})
})
@@ -72,13 +72,29 @@ function dedupeSecondaryTexts(
export function buildPaletteTabDocument(input: PaletteTabDocumentInput): PaletteDocument {
const fields: PaletteFieldSource[] = [
{ id: PALETTE_TAB_TITLE_FIELD_ID, profile: 'structured-label', text: input.title },
{ id: PALETTE_TAB_WORKTREE_FIELD_ID, profile: 'structured-label', text: input.worktreeName },
{ id: PALETTE_TAB_BRANCH_FIELD_ID, profile: 'structured-label', text: input.branch },
{ id: PALETTE_TAB_REPO_FIELD_ID, profile: 'structured-label', text: input.repoName },
{
id: PALETTE_TAB_WORKTREE_FIELD_ID,
profile: 'structured-label',
text: input.worktreeName,
isContainer: true
},
{
id: PALETTE_TAB_BRANCH_FIELD_ID,
profile: 'structured-label',
text: input.branch,
isContainer: true
},
{
id: PALETTE_TAB_REPO_FIELD_ID,
profile: 'structured-label',
text: input.repoName,
isContainer: true
},
{
id: PALETTE_TAB_WORKSPACE_FIELD_ID,
profile: 'structured-label',
text: input.workspaceLabel ?? ''
text: input.workspaceLabel ?? '',
isContainer: true
}
]
+17 -14
View File
@@ -12,7 +12,11 @@ import {
} from './tab-document'
import type { MatchRange } from './normalized-text'
import type { PaletteResultQualityClass } from './match-quality'
import type { PaletteDocument, PaletteDocumentRank } from './palette-document'
import {
comparePaletteDocumentRank,
type PaletteDocument,
type PaletteDocumentRank
} from './palette-document'
const NO_RANGES: readonly MatchRange[] = []
@@ -93,22 +97,21 @@ export type PaletteTabRankInputs = {
/** Existing positional score: current tab, current worktree, then list order. */
positionScore: number
id: string
/** Timestamp of most recent activity (focus or agent interaction). */
lastActiveAt?: number
}
const RANK_KEYS: readonly (keyof PaletteDocumentRank)[] = [
'exactIntent',
'wholeQuery',
'worstQuality',
'usesSupportingEvidence',
'fuzzyTokenCount',
'fieldHopCount'
]
/** Lexicographic match rank first, then the section's existing recency order. */
/** Lexicographic match rank first, then recent activity, then positional order. */
export function comparePaletteTabResults(a: PaletteTabRankInputs, b: PaletteTabRankInputs): number {
for (const key of RANK_KEYS) {
if (a.rank[key] !== b.rank[key]) {
return a.rank[key] - b.rank[key]
const byRank = comparePaletteDocumentRank(a.rank, b.rank)
if (byRank !== 0) {
return byRank
}
if (a.lastActiveAt !== b.lastActiveAt) {
const aTime = a.lastActiveAt ?? 0
const bTime = b.lastActiveAt ?? 0
if (aTime !== bTime) {
return bTime - aTime
}
}
if (a.positionScore !== b.positionScore) {
@@ -54,6 +54,7 @@ export type SimulatorPaletteSearchResult = {
score: number
qualityClass: PaletteResultQualityClass | null
rank: PaletteDocumentRank | null
lastActiveAt?: number | null
}
type SimulatorPaletteActiveTabType = 'browser' | 'editor' | 'terminal' | 'simulator'
@@ -290,8 +291,18 @@ export function searchSimulatorTabs(
return results.sort((a, b) =>
a.rank && b.rank
? comparePaletteTabResults(
{ rank: a.rank, positionScore: a.score, id: a.tabId },
{ rank: b.rank, positionScore: b.score, id: b.tabId }
{
rank: a.rank,
positionScore: a.score,
id: a.tabId,
lastActiveAt: a.lastActiveAt ?? undefined
},
{
rank: b.rank,
positionScore: b.score,
id: b.tabId,
lastActiveAt: b.lastActiveAt ?? undefined
}
)
: compareEmptyQueryResults(a, b)
)
@@ -21,6 +21,7 @@ import type { AgentMetadata } from './workspace-tab-agent-metadata'
*/
const AGENT_SNIPPET_RANK: PaletteDocumentRank = {
exactIntent: 1,
matchedDirectField: 0,
wholeQuery: 3,
worstQuality: paletteMatchQualityRank(PALETTE_MATCH_QUALITIES.at(-1) as PaletteMatchQuality) + 1,
usesSupportingEvidence: 1,
@@ -92,9 +92,12 @@ function isCurrentWorkspaceTab({
) {
return false
}
if (activeUnifiedTabId) {
return activeUnifiedTabId === tab.id
}
const visibleType = tab.contentType === 'terminal' ? 'terminal' : 'editor'
const storedType = activeTabTypeByWorktree[tab.worktreeId] ?? activeTabType
if (storedType !== visibleType || activeUnifiedTabId !== tab.id) {
if (storedType !== visibleType) {
return false
}
return visibleType === 'terminal'
@@ -68,6 +68,7 @@ function makeEntry({
worktreeSortIndex: 0,
groupSortIndex: 0,
tabSortIndex: 0,
occupantAgent: null,
title,
secondaryText: '',
titleSearchText: title,
@@ -137,4 +138,73 @@ describe('searchWorkspaceTabs lastActiveAt', () => {
const [result] = searchWorkspaceTabs([entry], '')
expect(result.lastActiveAt).toBe(4000)
})
it('uses tab lastFocusedAt when no agent metadata is present', () => {
const entry = makeEntry({
id: 'tab-focused',
createdAt: 1000
})
entry.tab.lastFocusedAt = 6000
const [result] = searchWorkspaceTabs([entry], '')
expect(result.lastActiveAt).toBe(6000)
})
})
describe('searchWorkspaceTabs ranking', () => {
it('ranks direct tab title matches ahead of container-only worktree matches', () => {
const directEntry = makeEntry({ id: 'README-4360' })
const containerEntry = makeEntry({ id: 'unrelated-file' })
// Both are in WORKTREE_NAME 'Aurora Workspace', but suppose worktree has 4360
directEntry.document = buildPaletteTabDocument({
id: 'tab-direct',
title: 'STA-4360-fix.ts',
secondaryTexts: [],
worktreeName: 'STA-4360',
branch: 'main',
repoName: 'repo'
})
containerEntry.document = buildPaletteTabDocument({
id: 'tab-container',
title: 'other-file.ts',
secondaryTexts: [],
worktreeName: 'STA-4360',
branch: 'main',
repoName: 'repo'
})
const results = searchWorkspaceTabs([containerEntry, directEntry], '4360')
expect(results).toHaveLength(2)
expect(results[0].tabId).toBe('README-4360')
expect(results[0].rank?.matchedDirectField).toBe(0)
expect(results[1].tabId).toBe('unrelated-file')
expect(results[1].rank?.matchedDirectField).toBe(1)
})
it('breaks tie between two container-matching tabs using lastActiveAt recency', () => {
const olderTab = makeEntry({ id: 'older-tab' })
const newerTab = makeEntry({ id: 'newer-tab' })
olderTab.document = buildPaletteTabDocument({
id: 'older',
title: 'file-a.ts',
secondaryTexts: [],
worktreeName: 'STA-4360',
branch: 'main',
repoName: 'repo'
})
newerTab.document = buildPaletteTabDocument({
id: 'newer',
title: 'file-b.ts',
secondaryTexts: [],
worktreeName: 'STA-4360',
branch: 'main',
repoName: 'repo'
})
olderTab.tab.lastFocusedAt = 1000
newerTab.tab.lastFocusedAt = 5000
const results = searchWorkspaceTabs([olderTab, newerTab], '4360')
expect(results).toHaveLength(2)
expect(results[0].tabId).toBe('newer-tab')
expect(results[1].tabId).toBe('older-tab')
})
})
@@ -86,10 +86,11 @@ function positionScore(entry: SearchableWorkspaceTab): number {
}
function resolveWorkspaceTabLastActiveAt(entry: SearchableWorkspaceTab): number | null {
// Agent activity wins when present; otherwise fall back to the worktree's PTY
// activity, which covers idle terminals and editor tabs alike.
// Agent activity wins when present; then explicit tab focus; otherwise fall back to worktree.
const candidate =
maxAgentActivityAt(entry.agentMetadata) ?? (entry.worktree.lastActivityAt || null)
maxAgentActivityAt(entry.agentMetadata) ??
entry.tab.lastFocusedAt ??
(entry.worktree.lastActivityAt || null)
if (candidate == null) {
return null
}
@@ -193,8 +194,18 @@ export function searchWorkspaceTabs(
return results.sort((a, b) =>
a.rank && b.rank
? comparePaletteTabResults(
{ rank: a.rank, positionScore: a.score, id: a.tabId },
{ rank: b.rank, positionScore: b.score, id: b.tabId }
{
rank: a.rank,
positionScore: a.score,
id: a.tabId,
lastActiveAt: a.lastActiveAt ?? undefined
},
{
rank: b.rank,
positionScore: b.score,
id: b.tabId,
lastActiveAt: b.lastActiveAt ?? undefined
}
)
: compareEmptyQueryResults(a, b)
)
@@ -38,6 +38,7 @@ export function buildWorktreePaletteTaskUrlResult(args: {
qualityClass: 'exact-intent',
rank: {
exactIntent: 0,
matchedDirectField: 0,
wholeQuery: 0,
worstQuality: 0,
usesSupportingEvidence: 1,
+12 -8
View File
@@ -1059,14 +1059,18 @@ export const createTabsSlice: StateCreator<AppState, [], [], TabsSlice> = (set,
})()
: state.unreadTerminalTabs
return {
unifiedTabsByWorktree: opts?.preservePreview
? state.unifiedTabsByWorktree
: {
...state.unifiedTabsByWorktree,
[worktreeId]: (state.unifiedTabsByWorktree[worktreeId] ?? []).map((item) =>
item.id === tabId ? { ...item, isPreview: false } : item
)
},
unifiedTabsByWorktree: {
...state.unifiedTabsByWorktree,
[worktreeId]: (state.unifiedTabsByWorktree[worktreeId] ?? []).map((item) =>
item.id === tabId
? {
...item,
isPreview: opts?.preservePreview ? item.isPreview : false,
lastFocusedAt: Date.now()
}
: item
)
},
groupsByWorktree: {
...state.groupsByWorktree,
[worktreeId]: (state.groupsByWorktree[worktreeId] ?? []).map((group) =>
+2
View File
@@ -58,6 +58,8 @@ export type Tab = {
* underneath; `'terminal'` (the default for legacy/missing) shows the raw
* xterm. Optional so sessions persisted before this field hydrate cleanly. */
viewMode?: 'terminal' | 'chat'
/** Timestamp when the tab was last focused / activated by the user. */
lastFocusedAt?: number
}
export type TabGroup = {