fix(frontend): interrupt-promotion on deferred prompts + queue UX - #2664
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 onlypendingPrompts. A queue message the backend defers to'failed'— which a long current turn does almost immediately — moves tofailedPrompts, 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 topendingPrompts-only. Extracted a single canonicalqueuedPrompts; 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 firstmessage_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
yarn buildgreen; built & deployed to meta.helix.ml for live retest.🤖 Generated with Claude Code