Skip to content

Commit 6c2d08b

Browse files
fix(queue-store): clear phase hints on auto-dismiss (#42)
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
1 parent 7012203 commit 6c2d08b

2 files changed

Lines changed: 43 additions & 12 deletions

File tree

src/lib/queue-store.test.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ beforeEach(() => {
5151
vi.useFakeTimers()
5252
toastShow.mockReset()
5353
// clear any leftover processes between tests
54-
for (const pid of ['p1', 'p2', 'bulk']) queueStore.dismiss(pid)
54+
for (const pid of ['p1', 'p2', 'bulk', 'hints-auto-1', 'hints-1', 'hints-2', 'hints-bg-1', 'hints-bg-2'])
55+
queueStore.dismiss(pid)
5556
})
5657

5758
afterEach(() => {
@@ -386,6 +387,33 @@ describe('queueStore.attachPhaseHints', () => {
386387
expect(queueStore.getPhaseHints(pid)).toBeUndefined()
387388
})
388389

390+
it('clears hints after auto-dismiss when undo window elapses', async () => {
391+
const pid = 'hints-auto-1'
392+
dispatch({ total: 1, completed: 0, paused: false, processId: pid, label: 'A' })
393+
queueStore.attachPhaseHints(pid, {
394+
reverse: {
395+
messageType: 'bulkUpdate',
396+
data: { reopen: true },
397+
affectedItemIds: ['i1'],
398+
},
399+
})
400+
dispatch({ total: 0, completed: 0, paused: false, status: 'Done!', processId: pid })
401+
402+
await vi.advanceTimersByTimeAsync(10_500)
403+
404+
expect(queueStore.getPhaseHints(pid)).toBeUndefined()
405+
406+
dispatch({ total: 2, completed: 0, paused: false, processId: pid, label: 'Second run' })
407+
408+
const snapshots: any[][] = []
409+
const unsub = queueStore.subscribe((e) => snapshots.push([...e]))
410+
const entry = snapshots[snapshots.length - 1].find((p: any) => p.processId === pid)
411+
expect(entry.phase.kind).toBe('in-flight')
412+
expect(entry.phase.reverse).toBeUndefined()
413+
expect(entry.phase.undoableUntil).toBeUndefined()
414+
unsub()
415+
})
416+
389417
it('§4.9 — consumes reverse hint piggy-backed on Done broadcast and exposes Undo', () => {
390418
const pid = 'hints-bg-1'
391419
const snapshots: any[][] = []

src/lib/queue-store.ts

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,16 @@ function clearDismissTimer(id: string): void {
158158
}
159159
}
160160

161+
/** Remove process entry and attached phase hints (manual dismiss + auto-dismiss). */
162+
function removeProcess(processId: string, opts?: { skipClearTimer?: boolean }): void {
163+
if (!opts?.skipClearTimer) clearDismissTimer(processId)
164+
phaseHintsMap.delete(processId)
165+
if (!processes.has(processId)) return
166+
const next = new Map(processes)
167+
next.delete(processId)
168+
setState(next)
169+
}
170+
161171
function scheduleDismiss(processId: string, delay: Duration.Duration = DISMISS_DELAY) {
162172
clearDismissTimer(processId)
163173
const fiber = Effect.runFork(
@@ -166,10 +176,7 @@ function scheduleDismiss(processId: string, delay: Duration.Duration = DISMISS_D
166176
Effect.sync(() => {
167177
if (dismissTimers.get(processId) !== fiber) return
168178
dismissTimers.delete(processId)
169-
if (!processes.has(processId)) return
170-
const next = new Map(processes)
171-
next.delete(processId)
172-
setState(next)
179+
removeProcess(processId, { skipClearTimer: true })
173180
}),
174181
),
175182
),
@@ -194,16 +201,12 @@ export const queueStore = {
194201
return queueStore.getActiveCount() > 0
195202
},
196203
dismiss(processId: string) {
197-
clearDismissTimer(processId)
198-
phaseHintsMap.delete(processId)
199-
if (!processes.has(processId)) {
204+
const existed = processes.has(processId)
205+
removeProcess(processId)
206+
if (!existed) {
200207
// still notify so callers observing 'after dismiss' state get a tick.
201208
setState(new Map(processes))
202-
return
203209
}
204-
const next = new Map(processes)
205-
next.delete(processId)
206-
setState(next)
207210
},
208211
/**
209212
* Attach reverse / retry hints to a process. Verb handlers call this before

0 commit comments

Comments
 (0)