mirror of
https://github.com/stablyai/orca.git
synced 2026-10-23 16:03:49 +00:00
Track container-only tokens and tab focus for cmd+j ranking
Previously ranked by whether any container-only matches existed (boolean); now counts tokens matching only containers for finer-grained ranking. Tab focus recency is now tracked explicitly so recent refocuses rank above stale worktree activity. Preserves worktree grouping by input order while applying focused-group MRU within each block.
This commit is contained in:
@@ -12,7 +12,7 @@ import type { PaletteDocumentRank } from './palette-match/palette-document'
|
||||
function rank(overrides: Partial<PaletteDocumentRank> = {}): PaletteDocumentRank {
|
||||
return {
|
||||
exactIntent: 1,
|
||||
matchedDirectField: 0,
|
||||
containerOnlyTokenCount: 0,
|
||||
wholeQuery: 3,
|
||||
worstQuality: 5,
|
||||
usesSupportingEvidence: 0,
|
||||
|
||||
@@ -163,8 +163,10 @@ function rankAssignments(args: {
|
||||
let worstQuality: PaletteMatchQuality = 'field-exact'
|
||||
let fuzzyTokenCount = 0
|
||||
const fields = new Set<string>()
|
||||
// Derive from field metadata only: an evidence field can itself be a container.
|
||||
let hasDirectHit = false
|
||||
let containerOnlyTokenCount = 0
|
||||
let tokenIndex = -1
|
||||
let tokenHasDirectField = false
|
||||
let matchedTokenCount = 0
|
||||
|
||||
for (const assignment of args.assignments) {
|
||||
if (paletteMatchQualityRank(assignment.quality) > paletteMatchQualityRank(worstQuality)) {
|
||||
@@ -174,20 +176,32 @@ function rankAssignments(args: {
|
||||
fuzzyTokenCount += 1
|
||||
}
|
||||
fields.add(assignment.fieldId)
|
||||
if (assignment.tokenIndex !== tokenIndex) {
|
||||
if (tokenIndex !== -1 && !tokenHasDirectField) {
|
||||
containerOnlyTokenCount += 1
|
||||
}
|
||||
tokenIndex = assignment.tokenIndex
|
||||
tokenHasDirectField = false
|
||||
matchedTokenCount += 1
|
||||
}
|
||||
const field = args.document.fieldById.get(assignment.fieldId)
|
||||
if (field && !field.isContainer) {
|
||||
hasDirectHit = true
|
||||
tokenHasDirectField = true
|
||||
}
|
||||
}
|
||||
|
||||
const isContainerOnly = !hasDirectHit
|
||||
if (tokenIndex !== -1 && !tokenHasDirectField) {
|
||||
containerOnlyTokenCount += 1
|
||||
}
|
||||
const isContainerOnly =
|
||||
containerOnlyTokenCount > 0 && containerOnlyTokenCount === matchedTokenCount
|
||||
|
||||
return {
|
||||
worstQuality,
|
||||
isContainerOnly,
|
||||
rank: {
|
||||
exactIntent: args.exactIntent ? 0 : 1,
|
||||
matchedDirectField: isContainerOnly ? 1 : 0,
|
||||
containerOnlyTokenCount,
|
||||
wholeQuery: args.wholeQuery,
|
||||
worstQuality: paletteMatchQualityRank(worstQuality),
|
||||
usesSupportingEvidence: args.usesEvidence ? 1 : 0,
|
||||
|
||||
@@ -129,8 +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
|
||||
/** Tokens whose chosen assignment only matched container fields. */
|
||||
containerOnlyTokenCount: number
|
||||
/** 0 equality, 1 prefix, 2 word boundary, 3 none — whole query in visible text. */
|
||||
wholeQuery: number
|
||||
worstQuality: number
|
||||
@@ -150,7 +150,7 @@ export type PaletteDocumentMatch = {
|
||||
|
||||
const RANK_KEYS: readonly (keyof PaletteDocumentRank)[] = [
|
||||
'exactIntent',
|
||||
'matchedDirectField',
|
||||
'containerOnlyTokenCount',
|
||||
'wholeQuery',
|
||||
'worstQuality',
|
||||
'usesSupportingEvidence',
|
||||
|
||||
@@ -2,7 +2,11 @@ import { describe, expect, it } from 'vitest'
|
||||
import { matchPaletteDocument } from './match-document'
|
||||
import { mapNormalizedRange, normalizePaletteText } from './normalized-text'
|
||||
import { preparePaletteQuery, PALETTE_QUERY_MAX_TOKENS } from './palette-query'
|
||||
import { buildPaletteDocument, type PaletteDocumentInput } from './palette-document'
|
||||
import {
|
||||
buildPaletteDocument,
|
||||
comparePaletteDocumentRank,
|
||||
type PaletteDocumentInput
|
||||
} from './palette-document'
|
||||
import { segmentPaletteText } from './text-segments'
|
||||
import { isWithinOnePaletteEdit } from './typo-distance'
|
||||
|
||||
@@ -383,7 +387,7 @@ describe('typo distance', () => {
|
||||
})
|
||||
|
||||
describe('container field matching', () => {
|
||||
it('marks matchedDirectField as 1 and demotes quality class when all tokens land on container fields', () => {
|
||||
it('counts a container-only token and demotes quality class when every token lands on containers', () => {
|
||||
const tabDoc: PaletteDocumentInput = {
|
||||
id: 'tab-1',
|
||||
visibleFields: [
|
||||
@@ -394,11 +398,11 @@ describe('container field matching', () => {
|
||||
}
|
||||
const match = run(tabDoc, '4360')
|
||||
expect(match).not.toBeNull()
|
||||
expect(match?.rank.matchedDirectField).toBe(1)
|
||||
expect(match?.rank.containerOnlyTokenCount).toBe(1)
|
||||
expect(match?.qualityClass).toBe('exact-evidence')
|
||||
})
|
||||
|
||||
it('marks matchedDirectField as 0 when at least one token lands on a direct field', () => {
|
||||
it('does not count a token that lands on a direct field', () => {
|
||||
const tabDoc: PaletteDocumentInput = {
|
||||
id: 'tab-1',
|
||||
visibleFields: [
|
||||
@@ -409,7 +413,40 @@ describe('container field matching', () => {
|
||||
}
|
||||
const match = run(tabDoc, '4360')
|
||||
expect(match).not.toBeNull()
|
||||
expect(match?.rank.matchedDirectField).toBe(0)
|
||||
expect(match?.rank.containerOnlyTokenCount).toBe(0)
|
||||
expect(match?.qualityClass).toBe('exact-visible')
|
||||
})
|
||||
|
||||
it('ranks an all-direct multi-token match ahead of a mixed direct and container match', () => {
|
||||
const direct = run(
|
||||
{
|
||||
id: 'direct',
|
||||
visibleFields: [
|
||||
{ id: 'title', profile: 'structured-label', text: 'alpha' },
|
||||
{ id: 'path', profile: 'structured-label', text: 'beta' }
|
||||
],
|
||||
evidence: []
|
||||
},
|
||||
'alpha beta'
|
||||
)
|
||||
const mixed = run(
|
||||
{
|
||||
id: 'mixed',
|
||||
visibleFields: [
|
||||
{ id: 'title', profile: 'structured-label', text: 'alpha' },
|
||||
{ id: 'worktree', profile: 'structured-label', text: 'beta', isContainer: true }
|
||||
],
|
||||
evidence: []
|
||||
},
|
||||
'alpha beta'
|
||||
)
|
||||
|
||||
expect(direct).not.toBeNull()
|
||||
expect(mixed).not.toBeNull()
|
||||
expect(direct?.rank.containerOnlyTokenCount).toBe(0)
|
||||
expect(mixed?.rank.containerOnlyTokenCount).toBe(1)
|
||||
if (direct && mixed) {
|
||||
expect(comparePaletteDocumentRank(direct.rank, mixed.rank)).toBeLessThan(0)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -179,6 +179,24 @@ describe('orderRecentWorkspaceTabs', () => {
|
||||
).toEqual(['alpha-2', 'alpha-1', 'beta-1'])
|
||||
})
|
||||
|
||||
it('keeps an interleaved worktree block together before applying its focused-group MRU', () => {
|
||||
const rows = [
|
||||
row('alpha-old', { worktreeId: 'wt-alpha', unifiedTabId: 'unified-alpha-old' }),
|
||||
row('beta', { worktreeId: 'wt-beta', unifiedTabId: 'unified-beta' }),
|
||||
row('alpha-new', { worktreeId: 'wt-alpha', unifiedTabId: 'unified-alpha-new' })
|
||||
]
|
||||
|
||||
expect(
|
||||
order(rows, sources([]), {
|
||||
focusedGroupTabRecency: new Map([
|
||||
['unified-alpha-old', 0],
|
||||
['unified-alpha-new', 1],
|
||||
['unified-beta', 0]
|
||||
])
|
||||
})
|
||||
).toEqual(['alpha-new', 'alpha-old', 'beta'])
|
||||
})
|
||||
|
||||
it('keeps input (positional) order when nothing else separates two rows', () => {
|
||||
const rows = [row('a', { worktreeId: 'wt-1' }), row('b', { worktreeId: 'wt-1' })]
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ type RankedRow = {
|
||||
visitedAt: number | undefined
|
||||
focusOrdinal: number
|
||||
worktreeId: string
|
||||
worktreeOrder: number
|
||||
}
|
||||
|
||||
/** Classes 1 (blocked/waiting) and 2 (freshly done) are the rows that want the user. */
|
||||
@@ -127,10 +128,8 @@ function compareRankedRows(a: RankedRow, b: RankedRow): number {
|
||||
}
|
||||
return b.visitedAt - a.visitedAt
|
||||
}
|
||||
// Why: focusOrdinal is a per-worktree sequence, so comparing it across worktrees interleaves
|
||||
// them arbitrarily. Keep input (positional) order instead.
|
||||
if (a.worktreeId !== b.worktreeId) {
|
||||
return 0
|
||||
if (a.worktreeOrder !== b.worktreeOrder) {
|
||||
return a.worktreeOrder - b.worktreeOrder
|
||||
}
|
||||
return b.focusOrdinal - a.focusOrdinal
|
||||
}
|
||||
@@ -139,10 +138,16 @@ function compareRankedRows(a: RankedRow, b: RankedRow): number {
|
||||
* Rank rows into ids, most-wanted first:
|
||||
* tier 1 — needs attention: class 1 (blocked/waiting) then 2 (fresh done), newest first
|
||||
* tier 2 — everything else: worktree focus recency, then focused-group MRU
|
||||
* Full ties keep input order, which callers pass in positional (worktree/group/tab) order.
|
||||
* Equal worktree tiers preserve first-seen worktree order, then use that worktree's MRU.
|
||||
*/
|
||||
export function orderRecentWorkspaceTabs(inputs: RecentWorkspaceTabOrderInputs): string[] {
|
||||
const { rows, paneSources, now, lastVisitedAtByWorktreeId, focusedGroupTabRecency } = inputs
|
||||
const worktreeOrder = new Map<string, number>()
|
||||
for (const row of rows) {
|
||||
if (!worktreeOrder.has(row.worktreeId)) {
|
||||
worktreeOrder.set(row.worktreeId, worktreeOrder.size)
|
||||
}
|
||||
}
|
||||
return rows
|
||||
.map((row): RankedRow => {
|
||||
const attention = resolveRecentWorkspaceTabAttention(row, paneSources, now)
|
||||
@@ -153,6 +158,7 @@ export function orderRecentWorkspaceTabs(inputs: RecentWorkspaceTabOrderInputs):
|
||||
attentionTimestamp: attention.attentionTimestamp,
|
||||
visitedAt: lastVisitedAtByWorktreeId[row.worktreeId],
|
||||
worktreeId: row.worktreeId,
|
||||
worktreeOrder: worktreeOrder.get(row.worktreeId) ?? Number.MAX_SAFE_INTEGER,
|
||||
focusOrdinal:
|
||||
row.unifiedTabId === null
|
||||
? NO_FOCUS_ORDINAL
|
||||
|
||||
@@ -21,7 +21,7 @@ import type { AgentMetadata } from './workspace-tab-agent-metadata'
|
||||
*/
|
||||
const AGENT_SNIPPET_RANK: PaletteDocumentRank = {
|
||||
exactIntent: 1,
|
||||
matchedDirectField: 0,
|
||||
containerOnlyTokenCount: Number.MAX_SAFE_INTEGER,
|
||||
wholeQuery: 3,
|
||||
worstQuality: paletteMatchQualityRank(PALETTE_MATCH_QUALITIES.at(-1) as PaletteMatchQuality) + 1,
|
||||
usesSupportingEvidence: 1,
|
||||
|
||||
@@ -99,13 +99,14 @@ function makeEntry({
|
||||
|
||||
describe('searchWorkspaceTabs lastActiveAt', () => {
|
||||
it('is null when neither agent activity nor worktree activity is known', () => {
|
||||
const [result] = searchWorkspaceTabs([makeEntry()], '')
|
||||
const [result] = searchWorkspaceTabs([makeEntry({ createdAt: 4000 })], '')
|
||||
expect(result.lastActiveAt).toBeNull()
|
||||
})
|
||||
|
||||
it('falls back to worktree PTY activity for editor tabs with no agent metadata', () => {
|
||||
const entry = makeEntry({
|
||||
contentType: 'editor',
|
||||
createdAt: 1000,
|
||||
worktree: makeWorktree({ lastActivityAt: 5000 })
|
||||
})
|
||||
const [result] = searchWorkspaceTabs([entry], '')
|
||||
@@ -148,9 +149,61 @@ describe('searchWorkspaceTabs lastActiveAt', () => {
|
||||
const [result] = searchWorkspaceTabs([entry], '')
|
||||
expect(result.lastActiveAt).toBe(6000)
|
||||
})
|
||||
|
||||
it('uses a newer tab focus instead of stale agent activity', () => {
|
||||
const entry = makeEntry({
|
||||
id: 'tab-refocused',
|
||||
createdAt: 1000,
|
||||
agentLastActivityAt: 3000
|
||||
})
|
||||
entry.tab.lastFocusedAt = 8000
|
||||
|
||||
const [result] = searchWorkspaceTabs([entry], '')
|
||||
|
||||
expect(result.lastActiveAt).toBe(8000)
|
||||
})
|
||||
|
||||
it('uses newer agent activity instead of an older tab focus', () => {
|
||||
const entry = makeEntry({
|
||||
id: 'tab-agent-active',
|
||||
createdAt: 1000,
|
||||
agentLastActivityAt: 8000
|
||||
})
|
||||
entry.tab.lastFocusedAt = 3000
|
||||
|
||||
const [result] = searchWorkspaceTabs([entry], '')
|
||||
|
||||
expect(result.lastActiveAt).toBe(8000)
|
||||
})
|
||||
})
|
||||
|
||||
describe('searchWorkspaceTabs ranking', () => {
|
||||
it('ranks a multi-token direct-plus-container hit above a container-only whole-query hit', () => {
|
||||
const directEntry = makeEntry({ id: 'direct-tab' })
|
||||
const containerEntry = makeEntry({ id: 'container-tab' })
|
||||
directEntry.document = buildPaletteTabDocument({
|
||||
id: 'direct',
|
||||
title: 'Auth notes',
|
||||
secondaryTexts: [],
|
||||
worktreeName: 'Aurora migration',
|
||||
branch: 'main',
|
||||
repoName: 'repo'
|
||||
})
|
||||
containerEntry.document = buildPaletteTabDocument({
|
||||
id: 'container',
|
||||
title: 'Unrelated notes',
|
||||
secondaryTexts: [],
|
||||
worktreeName: 'Auth Aurora',
|
||||
branch: 'main',
|
||||
repoName: 'repo'
|
||||
})
|
||||
|
||||
const results = searchWorkspaceTabs([containerEntry, directEntry], 'auth aurora')
|
||||
|
||||
expect(results.map((result) => result.tabId)).toEqual(['direct-tab', 'container-tab'])
|
||||
expect(results.map((result) => result.rank?.containerOnlyTokenCount)).toEqual([1, 2])
|
||||
})
|
||||
|
||||
it('ranks direct tab title matches ahead of container-only worktree matches', () => {
|
||||
const directEntry = makeEntry({ id: 'README-4360' })
|
||||
const containerEntry = makeEntry({ id: 'unrelated-file' })
|
||||
@@ -175,9 +228,9 @@ describe('searchWorkspaceTabs ranking', () => {
|
||||
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[0].rank?.containerOnlyTokenCount).toBe(0)
|
||||
expect(results[1].tabId).toBe('unrelated-file')
|
||||
expect(results[1].rank?.matchedDirectField).toBe(1)
|
||||
expect(results[1].rank?.containerOnlyTokenCount).toBe(1)
|
||||
})
|
||||
|
||||
it('breaks tie between two container-matching tabs using lastActiveAt recency', () => {
|
||||
|
||||
@@ -87,15 +87,13 @@ function positionScore(entry: SearchableWorkspaceTab): number {
|
||||
}
|
||||
|
||||
function resolveWorkspaceTabLastActiveAt(entry: SearchableWorkspaceTab): number | null {
|
||||
// Agent activity wins when present; then explicit tab focus; otherwise fall back to worktree.
|
||||
const candidate =
|
||||
maxAgentActivityAt(entry.agentMetadata) ??
|
||||
entry.tab.lastFocusedAt ??
|
||||
(entry.worktree.lastActivityAt || null)
|
||||
// Why: explicit tab activity outranks the worktree fallback; creation only clamps stale signals.
|
||||
const tabLocalActivity =
|
||||
Math.max(maxAgentActivityAt(entry.agentMetadata) ?? 0, entry.tab.lastFocusedAt ?? 0) || null
|
||||
const candidate = tabLocalActivity || entry.worktree.lastActivityAt || null
|
||||
if (candidate == null) {
|
||||
return null
|
||||
}
|
||||
// Never report activity older than the tab itself (e.g. a stale worktree signal).
|
||||
return Math.max(candidate, entry.tab.createdAt)
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ export function buildWorktreePaletteTaskUrlResult(args: {
|
||||
qualityClass: 'exact-intent',
|
||||
rank: {
|
||||
exactIntent: 0,
|
||||
matchedDirectField: 0,
|
||||
containerOnlyTokenCount: 0,
|
||||
wholeQuery: 0,
|
||||
worstQuality: 0,
|
||||
usesSupportingEvidence: 1,
|
||||
|
||||
@@ -147,6 +147,8 @@ const tabSchema = z.object({
|
||||
color: z.string().nullable(),
|
||||
sortOrder: z.number(),
|
||||
createdAt: z.number(),
|
||||
// Why: corrupt optional recency must not discard the whole persisted tab.
|
||||
lastFocusedAt: z.number().finite().nonnegative().optional().catch(undefined),
|
||||
isPreview: z.boolean().optional(),
|
||||
isPinned: z.boolean().optional(),
|
||||
// Why: persist the per-tab native-chat view mode so 'chat' survives reload /
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { parseWorkspaceSession } from './workspace-session-schema'
|
||||
|
||||
function sessionWithLastFocusedAt(lastFocusedAt: unknown): Record<string, unknown> {
|
||||
return {
|
||||
activeRepoId: null,
|
||||
activeWorktreeId: null,
|
||||
activeTabId: null,
|
||||
tabsByWorktree: {},
|
||||
terminalLayoutsByTabId: {},
|
||||
unifiedTabs: {
|
||||
wt: [
|
||||
{
|
||||
id: 'tab-1',
|
||||
entityId: 'terminal-1',
|
||||
groupId: 'group-1',
|
||||
worktreeId: 'wt',
|
||||
contentType: 'terminal',
|
||||
label: 'Terminal',
|
||||
customLabel: null,
|
||||
color: null,
|
||||
sortOrder: 0,
|
||||
createdAt: 1,
|
||||
lastFocusedAt
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('workspace session unified-tab focus timestamp', () => {
|
||||
it('preserves a finite nonnegative lastFocusedAt', () => {
|
||||
const result = parseWorkspaceSession(sessionWithLastFocusedAt(1_700_000_000_123))
|
||||
|
||||
expect(result.ok).toBe(true)
|
||||
if (result.ok) {
|
||||
expect(result.value.unifiedTabs?.wt[0]?.lastFocusedAt).toBe(1_700_000_000_123)
|
||||
}
|
||||
})
|
||||
|
||||
it.each([-1, Number.NaN, Number.POSITIVE_INFINITY])(
|
||||
'drops an invalid lastFocusedAt without dropping its tab (%s)',
|
||||
(lastFocusedAt) => {
|
||||
const result = parseWorkspaceSession(sessionWithLastFocusedAt(lastFocusedAt))
|
||||
|
||||
expect(result.ok).toBe(true)
|
||||
if (result.ok) {
|
||||
expect(result.value.unifiedTabs?.wt).toHaveLength(1)
|
||||
expect(result.value.unifiedTabs?.wt[0]?.lastFocusedAt).toBeUndefined()
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
Reference in New Issue
Block a user