From 8eed2acd1b7f8fdb054c61c4e4dbbe05f327bab4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 1 Jun 2026 13:01:58 +0000 Subject: [PATCH] fix(queue-store): clear phase hints on auto-dismiss Auto-dismiss timer removed processes but left phaseHintsMap entries, so reusing the same processId could show stale Undo/Retry. Centralize removal in removeProcess() and call it from scheduleDismiss. --- src/lib/queue-store.test.ts | 30 +++++++++++++++++++++++++++++- src/lib/queue-store.ts | 25 ++++++++++++++----------- 2 files changed, 43 insertions(+), 12 deletions(-) diff --git a/src/lib/queue-store.test.ts b/src/lib/queue-store.test.ts index 39b9a12..aa8d988 100644 --- a/src/lib/queue-store.test.ts +++ b/src/lib/queue-store.test.ts @@ -51,7 +51,8 @@ beforeEach(() => { vi.useFakeTimers() toastShow.mockReset() // clear any leftover processes between tests - for (const pid of ['p1', 'p2', 'bulk']) queueStore.dismiss(pid) + for (const pid of ['p1', 'p2', 'bulk', 'hints-auto-1', 'hints-1', 'hints-2', 'hints-bg-1', 'hints-bg-2']) + queueStore.dismiss(pid) }) afterEach(() => { @@ -386,6 +387,33 @@ describe('queueStore.attachPhaseHints', () => { expect(queueStore.getPhaseHints(pid)).toBeUndefined() }) + it('clears hints after auto-dismiss when undo window elapses', async () => { + const pid = 'hints-auto-1' + dispatch({ total: 1, completed: 0, paused: false, processId: pid, label: 'A' }) + queueStore.attachPhaseHints(pid, { + reverse: { + messageType: 'bulkUpdate', + data: { reopen: true }, + affectedItemIds: ['i1'], + }, + }) + dispatch({ total: 0, completed: 0, paused: false, status: 'Done!', processId: pid }) + + await vi.advanceTimersByTimeAsync(10_500) + + expect(queueStore.getPhaseHints(pid)).toBeUndefined() + + dispatch({ total: 2, completed: 0, paused: false, processId: pid, label: 'Second run' }) + + const snapshots: any[][] = [] + const unsub = queueStore.subscribe((e) => snapshots.push([...e])) + const entry = snapshots[snapshots.length - 1].find((p: any) => p.processId === pid) + expect(entry.phase.kind).toBe('in-flight') + expect(entry.phase.reverse).toBeUndefined() + expect(entry.phase.undoableUntil).toBeUndefined() + unsub() + }) + it('§4.9 — consumes reverse hint piggy-backed on Done broadcast and exposes Undo', () => { const pid = 'hints-bg-1' const snapshots: any[][] = [] diff --git a/src/lib/queue-store.ts b/src/lib/queue-store.ts index 3ce032f..9c55d3a 100644 --- a/src/lib/queue-store.ts +++ b/src/lib/queue-store.ts @@ -158,6 +158,16 @@ function clearDismissTimer(id: string): void { } } +/** Remove process entry and attached phase hints (manual dismiss + auto-dismiss). */ +function removeProcess(processId: string, opts?: { skipClearTimer?: boolean }): void { + if (!opts?.skipClearTimer) clearDismissTimer(processId) + phaseHintsMap.delete(processId) + if (!processes.has(processId)) return + const next = new Map(processes) + next.delete(processId) + setState(next) +} + function scheduleDismiss(processId: string, delay: Duration.Duration = DISMISS_DELAY) { clearDismissTimer(processId) const fiber = Effect.runFork( @@ -166,10 +176,7 @@ function scheduleDismiss(processId: string, delay: Duration.Duration = DISMISS_D Effect.sync(() => { if (dismissTimers.get(processId) !== fiber) return dismissTimers.delete(processId) - if (!processes.has(processId)) return - const next = new Map(processes) - next.delete(processId) - setState(next) + removeProcess(processId, { skipClearTimer: true }) }), ), ), @@ -194,16 +201,12 @@ export const queueStore = { return queueStore.getActiveCount() > 0 }, dismiss(processId: string) { - clearDismissTimer(processId) - phaseHintsMap.delete(processId) - if (!processes.has(processId)) { + const existed = processes.has(processId) + removeProcess(processId) + if (!existed) { // still notify so callers observing 'after dismiss' state get a tick. setState(new Map(processes)) - return } - const next = new Map(processes) - next.delete(processId) - setState(next) }, /** * Attach reverse / retry hints to a process. Verb handlers call this before