From 5ac46d5eef9aa615c96fdc6ce9851188b4b99d09 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Fri, 7 Aug 2026 19:23:19 -0700 Subject: [PATCH] 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. --- .../components/WorktreeJumpPalette.test.tsx | 9 +++++---- .../cmd-j/palette-live-status.test.tsx | 5 +++-- .../src/lib/cmd-j-match-relevance.test.ts | 2 +- .../src/lib/recent-workspace-tab-rows.test.ts | 20 +++++++++++++++++++ .../src/lib/recent-workspace-tab-rows.ts | 7 +++++++ 5 files changed, 36 insertions(+), 7 deletions(-) diff --git a/src/renderer/src/components/WorktreeJumpPalette.test.tsx b/src/renderer/src/components/WorktreeJumpPalette.test.tsx index 1afc6949b72..8378690f580 100644 --- a/src/renderer/src/components/WorktreeJumpPalette.test.tsx +++ b/src/renderer/src/components/WorktreeJumpPalette.test.tsx @@ -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) }) diff --git a/src/renderer/src/components/cmd-j/palette-live-status.test.tsx b/src/renderer/src/components/cmd-j/palette-live-status.test.tsx index 1a809c02501..9e6daf7f7a9 100644 --- a/src/renderer/src/components/cmd-j/palette-live-status.test.tsx +++ b/src/renderer/src/components/cmd-j/palette-live-status.test.tsx @@ -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 })) } diff --git a/src/renderer/src/lib/cmd-j-match-relevance.test.ts b/src/renderer/src/lib/cmd-j-match-relevance.test.ts index 397225083ec..36da3f66d0d 100644 --- a/src/renderer/src/lib/cmd-j-match-relevance.test.ts +++ b/src/renderer/src/lib/cmd-j-match-relevance.test.ts @@ -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 }])) diff --git a/src/renderer/src/lib/recent-workspace-tab-rows.test.ts b/src/renderer/src/lib/recent-workspace-tab-rows.test.ts index 963d9935d9c..1e71bb74f63 100644 --- a/src/renderer/src/lib/recent-workspace-tab-rows.test.ts +++ b/src/renderer/src/lib/recent-workspace-tab-rows.test.ts @@ -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' })] diff --git a/src/renderer/src/lib/recent-workspace-tab-rows.ts b/src/renderer/src/lib/recent-workspace-tab-rows.ts index b4cc0c5111c..b990ef1bdd2 100644 --- a/src/renderer/src/lib/recent-workspace-tab-rows.ts +++ b/src/renderer/src/lib/recent-workspace-tab-rows.ts @@ -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