From c2e64eef048c114a4dfb023095d4b9d1e78f602c Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Fri, 7 Aug 2026 18:59:26 -0700 Subject: [PATCH] Harden Cmd+J incomplete hydration re-rank latch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clear the provisional order latch when the ranked list goes empty so a brief tab wipe cannot freeze an empty Recent section, and assert that a user-moved selection survives the incomplete→complete re-rank. --- .../components/WorktreeJumpPalette.test.tsx | 20 +++++++------------ .../src/components/WorktreeJumpPalette.tsx | 5 ++++- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/renderer/src/components/WorktreeJumpPalette.test.tsx b/src/renderer/src/components/WorktreeJumpPalette.test.tsx index 5b692ee5909..1afc6949b72 100644 --- a/src/renderer/src/components/WorktreeJumpPalette.test.tsx +++ b/src/renderer/src/components/WorktreeJumpPalette.test.tsx @@ -769,7 +769,7 @@ describe('WorktreeJumpPalette recent chats & terminals', () => { it('re-ranks once when terminal entities hydrate after unified tabs', async () => { // Why split hydration: unified tabs can land before tabsByWorktree; without a re-capture every - // row ranks IDLE and the blocked chat stays under focus recency until reopen. + // 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()) @@ -778,23 +778,17 @@ describe('WorktreeJumpPalette recent chats & terminals', () => { }) await renderPalette({ ...hydrated, tabsByWorktree: {} }) expect(getTabRowIds()).toEqual(['tab-beta', 'tab-alpha']) - + const movedTo = `workspace-tab:${getTabRowIds()[1]}` + await act(async () => { + setCommandSelection?.(movedTo) + }) + await flushEffects() await act(async () => { useAppStore.setState({ tabsByWorktree: hydrated.tabsByWorktree } as Partial) }) await flushEffects() expect(getTabRowIds()).toEqual(['tab-alpha', 'tab-beta']) - - // Why: incomplete→complete re-rank is one-shot — later status churn still freezes positions. - await act(async () => { - useAppStore.setState({ - agentStatusByPaneKey: { - [`term-beta:${LEAF_ID}`]: makeAgentEntry('term-beta', 'blocked', Date.now()) - } - } as Partial) - }) - await flushEffects() - expect(getTabRowIds()).toEqual(['tab-alpha', 'tab-beta']) + expect(getCommandValue()).toBe(movedTo) }) it('ranks a blocked agent above a more recently visited idle tab', async () => { diff --git a/src/renderer/src/components/WorktreeJumpPalette.tsx b/src/renderer/src/components/WorktreeJumpPalette.tsx index 4a66f6eb2c7..e7d23aaea68 100644 --- a/src/renderer/src/components/WorktreeJumpPalette.tsx +++ b/src/renderer/src/components/WorktreeJumpPalette.tsx @@ -1098,7 +1098,10 @@ export default function WorktreeJumpPalette(): React.JSX.Element | null { if (order.length === 0) { // Why: tabs can arrive after the palette opens (cold start, session restore, a late tab // mirror). Latching an empty snapshot would leave Recent dead — and digits inert — until - // close+reopen, so stay unlatched; the stable empty ref keeps this a no-op re-render. + // close+reopen. Also clear a provisional latch: incomplete→complete fallthrough can hit empty + // if open tabs briefly vanish, and keeping captured would freeze an empty Recent forever. + recentTabOrderCapturedRef.current = false + recentTabOrderAttentionReadyRef.current = false setRecentTabOrder(EMPTY_RECENT_TAB_ORDER) return }