diff --git a/src/renderer/src/components/WorktreeJumpPalette.tsx b/src/renderer/src/components/WorktreeJumpPalette.tsx index fb0487a2704..6304e1fa099 100644 --- a/src/renderer/src/components/WorktreeJumpPalette.tsx +++ b/src/renderer/src/components/WorktreeJumpPalette.tsx @@ -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]) diff --git a/src/renderer/src/lib/browser-palette-search.ts b/src/renderer/src/lib/browser-palette-search.ts index ec1adeda66e..5072d0ee87d 100644 --- a/src/renderer/src/lib/browser-palette-search.ts +++ b/src/renderer/src/lib/browser-palette-search.ts @@ -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) ) diff --git a/src/renderer/src/lib/cmd-j-section-leadership.test.ts b/src/renderer/src/lib/cmd-j-section-leadership.test.ts index bf0d082a501..620f32869a5 100644 --- a/src/renderer/src/lib/cmd-j-section-leadership.test.ts +++ b/src/renderer/src/lib/cmd-j-section-leadership.test.ts @@ -12,6 +12,7 @@ import type { PaletteDocumentRank } from './palette-match/palette-document' function rank(overrides: Partial = {}): 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) }) diff --git a/src/renderer/src/lib/cmd-j-section-leadership.ts b/src/renderer/src/lib/cmd-j-section-leadership.ts index 372eb2aebf6..093cacce567 100644 --- a/src/renderer/src/lib/cmd-j-section-leadership.ts +++ b/src/renderer/src/lib/cmd-j-section-leadership.ts @@ -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 } diff --git a/src/renderer/src/lib/palette-match/indexed-field.ts b/src/renderer/src/lib/palette-match/indexed-field.ts index 5d87776bf47..0e87ae37895 100644 --- a/src/renderer/src/lib/palette-match/indexed-field.ts +++ b/src/renderer/src/lib/palette-match/indexed-field.ts @@ -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 = new Set([ @@ -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) } } diff --git a/src/renderer/src/lib/palette-match/match-document.ts b/src/renderer/src/lib/palette-match/match-document.ts index b5f7647ef28..a17116ad96f 100644 --- a/src/renderer/src/lib/palette-match/match-document.ts +++ b/src/renderer/src/lib/palette-match/match-document.ts @@ -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() + 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, diff --git a/src/renderer/src/lib/palette-match/match-quality.ts b/src/renderer/src/lib/palette-match/match-quality.ts index 77f3c7aa8df..d400489b6be 100644 --- a/src/renderer/src/lib/palette-match/match-quality.ts +++ b/src/renderer/src/lib/palette-match/match-quality.ts @@ -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' } diff --git a/src/renderer/src/lib/palette-match/palette-document.ts b/src/renderer/src/lib/palette-match/palette-document.ts index 0e92875d493..2400921f6be 100644 --- a/src/renderer/src/lib/palette-match/palette-document.ts +++ b/src/renderer/src/lib/palette-match/palette-document.ts @@ -40,6 +40,7 @@ export type PaletteDocument = { /** Visible identity fields, cached because they carry the whole-query check. */ visibleFields: readonly PaletteIndexedField[] fieldsByEvidenceId: ReadonlyMap + fieldById: ReadonlyMap } export type PaletteDocumentInput = { @@ -80,7 +81,9 @@ export function buildPaletteDocument(input: PaletteDocumentInput): PaletteDocume const fields = indexPaletteFields([...input.visibleFields, ...evidenceSources]) const fieldsByEvidenceId = new Map() const visibleFields: PaletteIndexedField[] = [] + const fieldById = new Map() 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', diff --git a/src/renderer/src/lib/palette-match/palette-match-core.test.ts b/src/renderer/src/lib/palette-match/palette-match-core.test.ts index 6fe7878513f..8862165bc79 100644 --- a/src/renderer/src/lib/palette-match/palette-match-core.test.ts +++ b/src/renderer/src/lib/palette-match/palette-match-core.test.ts @@ -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') + }) +}) diff --git a/src/renderer/src/lib/palette-match/tab-document.ts b/src/renderer/src/lib/palette-match/tab-document.ts index d03c09a7706..8c289931afc 100644 --- a/src/renderer/src/lib/palette-match/tab-document.ts +++ b/src/renderer/src/lib/palette-match/tab-document.ts @@ -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 } ] diff --git a/src/renderer/src/lib/palette-match/tab-match.ts b/src/renderer/src/lib/palette-match/tab-match.ts index d9c65000fa0..f40dd6de03d 100644 --- a/src/renderer/src/lib/palette-match/tab-match.ts +++ b/src/renderer/src/lib/palette-match/tab-match.ts @@ -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) { diff --git a/src/renderer/src/lib/simulator-palette-search.ts b/src/renderer/src/lib/simulator-palette-search.ts index f5d1faecde1..43196fd72ab 100644 --- a/src/renderer/src/lib/simulator-palette-search.ts +++ b/src/renderer/src/lib/simulator-palette-search.ts @@ -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) ) diff --git a/src/renderer/src/lib/workspace-tab-agent-snippet-match.ts b/src/renderer/src/lib/workspace-tab-agent-snippet-match.ts index f5b47aa1871..3a67ed6a11b 100644 --- a/src/renderer/src/lib/workspace-tab-agent-snippet-match.ts +++ b/src/renderer/src/lib/workspace-tab-agent-snippet-match.ts @@ -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, diff --git a/src/renderer/src/lib/workspace-tab-palette-entry-builder.ts b/src/renderer/src/lib/workspace-tab-palette-entry-builder.ts index 2b70345904d..4eea1c3b9d6 100644 --- a/src/renderer/src/lib/workspace-tab-palette-entry-builder.ts +++ b/src/renderer/src/lib/workspace-tab-palette-entry-builder.ts @@ -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' diff --git a/src/renderer/src/lib/workspace-tab-palette-results.test.ts b/src/renderer/src/lib/workspace-tab-palette-results.test.ts index f0d52346ae4..a62c6870ae8 100644 --- a/src/renderer/src/lib/workspace-tab-palette-results.test.ts +++ b/src/renderer/src/lib/workspace-tab-palette-results.test.ts @@ -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') + }) }) diff --git a/src/renderer/src/lib/workspace-tab-palette-results.ts b/src/renderer/src/lib/workspace-tab-palette-results.ts index 56b7db32cad..8e6989b7754 100644 --- a/src/renderer/src/lib/workspace-tab-palette-results.ts +++ b/src/renderer/src/lib/workspace-tab-palette-results.ts @@ -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) ) diff --git a/src/renderer/src/lib/worktree-palette-task-url-result.ts b/src/renderer/src/lib/worktree-palette-task-url-result.ts index 54c6e265c2d..303083dff92 100644 --- a/src/renderer/src/lib/worktree-palette-task-url-result.ts +++ b/src/renderer/src/lib/worktree-palette-task-url-result.ts @@ -38,6 +38,7 @@ export function buildWorktreePaletteTaskUrlResult(args: { qualityClass: 'exact-intent', rank: { exactIntent: 0, + matchedDirectField: 0, wholeQuery: 0, worstQuality: 0, usesSupportingEvidence: 1, diff --git a/src/renderer/src/store/slices/tabs.ts b/src/renderer/src/store/slices/tabs.ts index b1f1392d82f..54e2248556b 100644 --- a/src/renderer/src/store/slices/tabs.ts +++ b/src/renderer/src/store/slices/tabs.ts @@ -1059,14 +1059,18 @@ export const createTabsSlice: StateCreator = (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) => diff --git a/src/shared/tab-types.ts b/src/shared/tab-types.ts index e456c1df412..2211668e615 100644 --- a/src/shared/tab-types.ts +++ b/src/shared/tab-types.ts @@ -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 = {