Skip to content

fix(frontend): interrupt-promotion on deferred prompts + queue UX - #2664

Merged
lukemarsden merged 2 commits into
mainfrom
fix/interrupt-queue-ux
Jun 19, 2026
Merged

fix(frontend): interrupt-promotion on deferred prompts + queue UX#2664
lukemarsden merged 2 commits into
mainfrom
fix/interrupt-queue-ux

Conversation

@lukemarsden

Copy link
Copy Markdown
Collaborator

Three related fixes to the spec-task prompt queue (RobustPromptInput.tsx), all surfaced while validating the boot-race interrupt work (#2660/#2661).

1. Interrupt promotion silently failed on deferred prompts

"Enter twice" (empty-Enter) promotes the most-recent queued message interrupt:false → true. But it scanned only pendingPrompts. A queue message the backend defers to 'failed' — which a long current turn does almost immediately — moves to failedPrompts, so the promotion found zero candidates and silently no-op'd. The message stayed queue-mode and waited for the (long) current turn instead of interrupting.

This is the bug behind a live repro where a user's mid-stream interrupt was recorded interrupt=false: the action was correct, the frontend dropped the promotion. Fixed — promotion now considers deferred prompts.

2. Refactor (root cause of #1)

[...failedPrompts, ...pendingPrompts] was recomputed ad-hoc in five places, and the promotion path had diverged to pendingPrompts-only. Extracted a single canonical queuedPrompts; display, toggle, promotion, client-pump, and count all route through it. The divergence is now structurally impossible.

3. Optimistic-hide of dispatched prompts

The visible queue now excludes 'sending' (backend has dispatched to Zed, awaiting first message_added). A just-sent prompt disappears the moment dispatch is confirmed, instead of lingering in the queue until the next sync flips it to 'sent'. If it later bounces, it returns via 'failed'. (Reverses the deliberate "'sending' stays visible" grouping at the request of the UX owner.)

Notes

🤖 Generated with Claude Code

lukemarsden and others added 2 commits June 19, 2026 14:24
Three related fixes to the spec-task prompt queue (RobustPromptInput), all
surfaced while validating the boot-race interrupt work:

1. Empty-Enter interrupt promotion ("Enter twice") scanned only pendingPrompts,
   so a queue message the backend had deferred to 'failed' (which a long current
   turn does almost immediately) was not a promotion candidate — the promotion
   silently no-op'd and the message stayed queue-mode. It now scans the deferred
   (failed) prompts too.

2. Refactor: the [...failedPrompts, ...pendingPrompts] combination was recomputed
   ad-hoc in five places and one (the promotion) had diverged — the root cause of
   #1. Extracted a single canonical `queuedPrompts`; every site routes through it,
   so the divergence is structurally impossible.

3. Optimistic-hide: the visible queue now excludes 'sending' (backend dispatched
   to Zed, awaiting first message_added). A just-sent prompt disappears the moment
   dispatch is confirmed instead of lingering until the next sync flips it to
   'sent'; if it bounces it returns via 'failed'.

See design/2026-06-19-incident-interrupt-during-boot-context-loss.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The interrupt promotion showed the lightning icon in the UI but the backend
kept interrupt=false, so the message queued instead of interrupting.

Root cause: mergeWithBackend (and the status-poll path) re-marked any
backend-present entry syncedToBackend=true based purely on id presence. When a
backend poll landed between updateInterrupt (which sets the entry dirty,
syncedToBackend=false) and the debounced syncToBackend push, it clobbered the
dirty flag — so syncToBackend (which only pushes !syncedToBackend) skipped the
entry and the interrupt=true change never reached the backend.

Fix: a pull/merge may confirm-sync only entries with no pending local change.
mergeWithBackend keeps syncedToBackend=false when it's already false; the
status-poll path preserves the dirty flag while still reflecting backend status.
Only push-ack paths clear the dirty flag.

Follow-up (noted, not in this PR): the "only push clears dirty; pulls never do"
invariant is still applied ad-hoc across the sync sites — worth centralizing
behind one reconcile helper with hook tests.

See design/2026-06-19-incident-interrupt-during-boot-context-loss.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@lukemarsden
lukemarsden merged commit 3a0255d into main Jun 19, 2026
5 checks passed
@lukemarsden
lukemarsden deleted the fix/interrupt-queue-ux branch June 19, 2026 13:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant