fix(composer): restore the prompt when a run fails after send#87
Merged
Conversation
PR #78 restored the typed prompt only on *synchronous* send errors (no connection, off-workspace). But the common failures the user hits (429/400/token-exhausted) happen *asynchronously* during run execution, after assistant_send_message already returned success and the composer already cleared. On those, the backend retracts the unanswered user message (discard_unanswered_run_input -> MessageDeleted), so the prompt vanished from both the composer AND the conversation, forcing a retype. The discard's own doc even claimed the text "stays recoverable via the input history" -- a recovery path that never existed. Fix: capture the retracted text where the deletion lands. The store's removeMessage now stashes a removed *user* message's text in recoverablePrompts[sessionId]; the wrapper surfaces it to the composer, which restores it into the empty input box (during-render setState, converges -- mirrors the draft-swap pattern; lint-safe vs set-state-in-effect) and clears the store slot once consumed. Guarded by restoreFailedPrompt so it never clobbers text the user typed since. Image-bearing messages are unaffected: the backend preserves and auto-retries them, so no MessageDeleted fires for them. Tests: 4 store tests (capture trims; ignores assistant/empty; clear). tsc / eslint / vitest (132) / vite build clean.
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.
The bug (still happening despite PR #78)
When a message fails to send because the run errors — tokens exhausted, 400, 429, etc. — the typed prompt is lost: it's gone from the composer input box and from the conversation, forcing a full retype.
PR #78 already restores the prompt, but only on synchronous send errors (no connection, off-workspace route). The failures you actually hit happen asynchronously:
assistant_send_messagepersists the message, starts the run, and returns success immediately — the composer clears. The 429/400/token-limit error surfaces later, during run execution. At that point the backend'sdiscard_unanswered_run_inputretracts the unanswered user message (emitsMessageDeleted), so the prompt disappears from both places and PR #78's path never had a chance to fire.Ironically, the backend's own discard doc-comment claimed "the typed text stays recoverable via the input history" — a recovery path that never existed in the composer.
The fix
Capture the retracted text exactly where the deletion lands, and feed it back to the composer:
sessionStore.ts):removeMessagenow detects when the removed message is a user message and stashes its (trimmed) text inrecoverablePrompts[sessionId]. NewclearRecoverablePromptaction.TerminalEmulatorWrapper.tsx): subscribes torecoverablePrompts[workspaceSessionId], passes it to the composer, and clears the slot once consumed.TerminalEmulator.tsx): restores the prompt into the input box via the during-render setState pattern (converges; mirrors the existing draft-swap; lint-safe vsreact-hooks/set-state-in-effect), guarded byrestoreFailedPromptso it never clobbers text the user has typed since. Clears the store slot in an effect (a zustand action, not a React setter, so exempt from the effect lint).Scope / correctness
MessageDeletedfires and this path is never invoked — correct, since images aren't recoverable from text anyway.Verification
clearRecoverablePromptworks.tsc/eslint --max-warnings 0/vitest(132) /vite build— all clean.Test locally
Trigger a run failure (e.g. exhaust tokens, or a 429) with text in the composer. After the error, the typed prompt should reappear in the input box instead of being lost.