Fix Cmd+J ordering to not compare focus ordinals across worktrees

focusOrdinal is a per-worktree sequence, so comparing rows from different worktrees corrupts their relative order. Preserve input (positional) order instead.

Also: refactor test helpers to use makePaneKey() utility for pane-key construction, and clarify a test description about CJK character handling in relevance scoring.
This commit is contained in:
Jinjing
2026-08-07 19:23:19 -07:00
parent c2e64eef04
commit 5ac46d5eef
5 changed files with 36 additions and 7 deletions
@@ -6,6 +6,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type * as ReactI18Next from 'react-i18next'
import type { Repo, Tab, TabGroup, TerminalTab, Worktree } from '../../../shared/types'
import type { AgentStatusEntry, AgentStatusState } from '../../../shared/agent-status-types'
import { makePaneKey } from '../../../shared/stable-pane-id'
import { useAppStore } from '@/store'
import type { AppState } from '@/store/types'
import { emitCmdJRowIndexJump } from '@/lib/cmd-j-row-index-jump'
@@ -431,7 +432,7 @@ function makeAgentEntry(
prompt: '',
updatedAt: stateStartedAt,
stateStartedAt,
paneKey: `${tabId}:${LEAF_ID}`,
paneKey: makePaneKey(tabId, LEAF_ID),
stateHistory: []
}
}
@@ -772,7 +773,7 @@ describe('WorktreeJumpPalette recent chats & terminals', () => {
// row ranks IDLE. A deliberate second-row highlight must survive that one re-rank.
const hydrated = makeRecentTabState({
agentStatusByPaneKey: {
[`term-alpha:${LEAF_ID}`]: makeAgentEntry('term-alpha', 'blocked', Date.now())
[makePaneKey('term-alpha', LEAF_ID)]: makeAgentEntry('term-alpha', 'blocked', Date.now())
},
lastVisitedAtByWorktreeId: { 'wt-beta': Date.now() }
})
@@ -795,7 +796,7 @@ describe('WorktreeJumpPalette recent chats & terminals', () => {
await renderPalette(
makeRecentTabState({
agentStatusByPaneKey: {
[`term-alpha:${LEAF_ID}`]: makeAgentEntry('term-alpha', 'blocked', Date.now())
[makePaneKey('term-alpha', LEAF_ID)]: makeAgentEntry('term-alpha', 'blocked', Date.now())
},
lastVisitedAtByWorktreeId: { 'wt-beta': Date.now() }
})
@@ -816,7 +817,7 @@ describe('WorktreeJumpPalette recent chats & terminals', () => {
await act(async () => {
useAppStore.setState({
agentStatusByPaneKey: {
[`term-alpha:${LEAF_ID}`]: makeAgentEntry('term-alpha', 'blocked', Date.now())
[makePaneKey('term-alpha', LEAF_ID)]: makeAgentEntry('term-alpha', 'blocked', Date.now())
}
} as Partial<AppState>)
})
@@ -6,6 +6,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { useAppStore } from '@/store'
import type { AppState } from '@/store/types'
import type { AgentStatusEntry, AgentStatusState } from '../../../../shared/agent-status-types'
import { makePaneKey } from '../../../../shared/stable-pane-id'
import type { TerminalTab } from '../../../../shared/types'
import {
PaletteLiveStatusProvider,
@@ -42,14 +43,14 @@ function makeAgentEntry(tabId: string, state: AgentStatusState): AgentStatusEntr
prompt: '',
updatedAt: Date.now(),
stateStartedAt: Date.now(),
paneKey: `${tabId}:${LEAF}`,
paneKey: makePaneKey(tabId, LEAF),
stateHistory: []
}
}
function setAgentState(state: AgentStatusState): void {
useAppStore.setState((s) => ({
agentStatusByPaneKey: { [`term-a:${LEAF}`]: makeAgentEntry('term-a', state) },
agentStatusByPaneKey: { [makePaneKey('term-a', LEAF)]: makeAgentEntry('term-a', state) },
agentStatusEpoch: s.agentStatusEpoch + 1
}))
}
@@ -79,7 +79,7 @@ describe('scorePaletteRelevance', () => {
expect(worstPrimary).toBeLessThan(bestSecondary)
})
it('treats a CJK character before the match as a word boundary', () => {
it('treats a CJK letter before the match as mid-word, like a Latin letter', () => {
expect(
scorePaletteRelevance([{ text: '工作树perf', range: { start: 3, end: 7 }, tier: 0 }])
).toBe(scorePaletteRelevance([{ text: 'aperf', range: { start: 1, end: 5 }, tier: 0 }]))
@@ -159,6 +159,26 @@ describe('orderRecentWorkspaceTabs', () => {
).toEqual(['second', 'third', 'first'])
})
it('keeps input order across worktrees instead of comparing their unrelated MRU ordinals', () => {
// Callers pass worktree-grouped positional order; both worktrees are never-visited, so only
// the per-worktree focus ordinals differ — beta's larger ordinal must not hoist it over alpha.
const rows = [
row('alpha-1', { worktreeId: 'wt-alpha', unifiedTabId: 'unified-alpha-1' }),
row('alpha-2', { worktreeId: 'wt-alpha', unifiedTabId: 'unified-alpha-2' }),
row('beta-1', { worktreeId: 'wt-beta', unifiedTabId: 'unified-beta-1' })
]
expect(
order(rows, sources([]), {
focusedGroupTabRecency: new Map([
['unified-alpha-1', 0],
['unified-alpha-2', 1],
['unified-beta-1', 5]
])
})
).toEqual(['alpha-2', 'alpha-1', 'beta-1'])
})
it('keeps input (positional) order when nothing else separates two rows', () => {
const rows = [row('a', { worktreeId: 'wt-1' }), row('b', { worktreeId: 'wt-1' })]
@@ -43,6 +43,7 @@ type RankedRow = {
attentionTimestamp: number
visitedAt: number | undefined
focusOrdinal: number
worktreeId: string
}
/** Classes 1 (blocked/waiting) and 2 (freshly done) are the rows that want the user. */
@@ -125,6 +126,11 @@ 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
}
return b.focusOrdinal - a.focusOrdinal
}
@@ -145,6 +151,7 @@ export function orderRecentWorkspaceTabs(inputs: RecentWorkspaceTabOrderInputs):
attentionClass: attention.cls,
attentionTimestamp: attention.attentionTimestamp,
visitedAt: lastVisitedAtByWorktreeId[row.worktreeId],
worktreeId: row.worktreeId,
focusOrdinal:
row.unifiedTabId === null
? NO_FOCUS_ORDINAL