Skip to content

fix(web): restore a submitted message to the composer when its turn fails - #7415

Open
sideeffffect wants to merge 4 commits into
pingdotgg:mainfrom
sideeffffect:fix/restore-composer-on-turn-failure
Open

fix(web): restore a submitted message to the composer when its turn fails#7415
sideeffffect wants to merge 4 commits into
pingdotgg:mainfrom
sideeffffect:fix/restore-composer-on-turn-failure

Conversation

@sideeffffect

@sideeffffect sideeffffect commented Aug 18, 2026

Copy link
Copy Markdown

Problem

When you submit a message in a conversation, the composer is cleared immediately. If the server accepts the turn and it then fails asynchronously — e.g. Claude runtime stream failed., Stale pending user-input request: … Provider callback state does not survive app restarts or recovered sessions., or a bare Runtime error — the failure surfaces only later via session.lastError, long after onSend has returned. The existing send-failure restore path only runs for a synchronous start-turn RPC rejection, so in the async case the typed text stays cleared and is lost for good, with no way to retrieve it.

Fix

Keep a snapshot of the just-submitted text (plus images and terminal/element/preview/review contexts) alive until the turn is confirmed accepted-and-clean, and restore it into an empty composer when the turn's session enters an error state.

  • The restore decision is a pure, unit-tested helper deriveTurnFailureRecoveryAction in ChatView.logic.ts.
  • It keys on session.status === "error" (a fresh, current-state signal), not on lastError — the server carries lastError forward across a new turn until the session next reaches ready, so keying on it would restore a stale prior-turn error. A session.updatedAt guard ensures a session that was already in error at send time can't trigger a spurious restore before the new turn begins.
  • It never clobbers text the user typed after sending (the failed attempt also remains in the transcript), and it drops the snapshot once the turn completes cleanly.
  • The synchronous and asynchronous failure paths now share one restore routine.

Scope: the primary onSend path in an existing/started conversation (the reported case). Plan-mode follow-up and "implement in new thread" are intentionally out of scope for this focused change.

Verification

apps/web typecheck, lint, and the touched unit tests pass, including six new cases for deriveTurnFailureRecoveryAction (async-failure restore, retyped-composer drop, stale-error no-op, clean-completion drop, still-running wait).

This is a state/logic fix rather than a visual one, so there is no meaningful before/after screenshot; the difference is whether the composer is repopulated after an induced runtime failure. Happy to attach a short screen recording of a forced runtime error if useful.

Claude Opus 4.8 via Claude Code.


Note

Medium Risk
Touches core send/composer state in ChatView with nuanced session/turn timing; logic is unit-tested but wrong edge cases could restore stale text or drop recovery incorrectly.

Overview
Fixes lost composer text when the server accepts a message but the turn fails later (runtime stream errors, stale callbacks, etc.)—cases the synchronous start-turn failure path never handled.

Adds deriveTurnFailureRecoveryAction in ChatView.logic.ts to decide restore, drop, or wait from pre-send session/turn markers and current thread state. Recovery keys on session.status === "error" with proof the session advanced (updated timestamp or new turn id), not on lastError, and skips restore if the user already retyped.

ChatView.tsx keeps a post-accept snapshot in pendingSendRecoveryRef, runs a recovery useEffect (with recoveryReevaluateTick for in-flight failures), and restoreComposerContentFromSnapshot shared with the existing synchronous send-failure path.

Reviewed by Cursor Bugbot for commit d9aa8ff. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Restore submitted message to composer when an accepted turn later fails

  • When a turn is accepted by the server but the session later transitions to an error state, the composer is automatically restored with the original message content, attachments, and contexts.
  • A new deriveTurnFailureRecoveryAction function in ChatView.logic.ts determines whether to restore, drop, or wait based on pre-send session/turn markers versus current state.
  • On each send, ChatViewContent captures a recovery snapshot (prompt, attachments, contexts, annotations) and arms it after a successful turn start; a useEffect monitors thread state changes and triggers restoration when appropriate.
  • If the user has already started typing a new message when the failure is detected, the snapshot is dropped to avoid overwriting their input.
  • Sync rejection (turn never starts) now also uses the shared restoreComposerContentFromSnapshot callback instead of inline restoration logic.

Macroscope summarized d9aa8ff.

…ails

Sending a message clears the composer immediately. If the server accepted the
turn and it then failed asynchronously — a runtime stream error, a stale
pending provider callback, a runtime.error — the failure only surfaced later
via session.lastError, long after onSend returned. The inline send-failure
path never ran, so the typed text was cleared and lost for good.

Keep a snapshot of the submitted text (plus images and contexts) alive until
the turn is confirmed accepted-and-clean, and restore it into an empty
composer when the turn's session enters an error state. The restore decision
is a pure helper (deriveTurnFailureRecoveryAction) keyed on session.status
rather than the carried-forward lastError, so a stale prior-turn error cannot
trigger a spurious restore. The synchronous and asynchronous failure paths now
share one restore routine.

Claude Opus 4.8 via Claude Code.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 18, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 430585d6-06e4-4fe1-a8d0-0ce284381e69

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Aug 18, 2026
Comment thread apps/web/src/components/ChatView.tsx
Comment thread apps/web/src/components/ChatView.logic.ts Outdated
Comment thread apps/web/src/components/ChatView.tsx Outdated
@macroscopeapp

macroscopeapp Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This PR introduces new runtime behavior for automatic message recovery on async turn failures, with complex state machine logic handling multiple edge cases. The scope exceeds a simple bug fix and warrants human review given the behavioral implications.

You can add or adjust custom eligibility rules. Learn more.

- Arm the pending-send snapshot before the async send RPCs instead of after
  startThreadTurn resolves. The recovery effect keys off activeServerThread,
  not the ref, so a failure that lands during the awaits would otherwise run
  the effect with no snapshot and never restore the composer. A synchronous
  send failure clears the snapshot again.
- Treat a moved latestTurnCompletedAt as clean completion, not just a new
  turn id. A steered follow-up folds into the running turn and keeps the same
  id, so the snapshot would otherwise linger and could restore an
  already-sent message on a later, unrelated session error.

Claude Opus 4.8 via Claude Code.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread apps/web/src/components/ChatView.logic.ts
Comment thread apps/web/src/components/ChatView.tsx Outdated
Follow-up to review of the recovery snapshot:

- Arm the snapshot only after the turn is accepted (not before the send RPCs),
  and bump a state tick so the recovery effect re-evaluates even if the failing
  session state already landed during the awaits. Arming early let the effect
  restore mid-onSend, after which a synchronous failure saw a non-empty
  composer and left the optimistic user message orphaned in the transcript.
- Treat a changed latestTurnId as session advancement in addition to a changed
  sessionUpdatedAt, so an accept-then-fail that shares the pre-send millisecond
  timestamp still restores.

Claude Opus 4.8 via Claude Code.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread apps/web/src/components/ChatView.tsx
A prior turn's recovery snapshot stayed live while a follow-up or steer send
cleared the composer and awaited its RPCs. If the session hit error in that
window the effect restored the old snapshot into the now-empty composer, and a
later synchronous failure for the new send then skipped restore (composer no
longer empty), losing the newly typed text.

Drop any pending snapshot at each point onSend takes over the composer (main
send, plan follow-up, standalone slash command); the main send re-arms its own
snapshot once its turn is accepted. Only the most recent send is recoverable,
which matches the single-snapshot model.

Claude Opus 4.8 via Claude Code.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@sideeffffect

Copy link
Copy Markdown
Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit d9aa8ff. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant