fix(web): preserve user-authored text during prompt recall#10056

openedSep 5, 2026bySsaphid

Prompt recall can change text that the user actually typed when it resembles content appended by the app. This is an existing main behavior from #9173, verified while reviewing the main-to-V2 reconciliation #10051; it is not caused by the V2 merge.

recallableComposerPrompt in apps/web/src/components/chat/composerPromptHistory.ts strips an Ultrathink:\n prefix and trailing <review_comment>…</review_comment> blocks, and omits text matching the attachment-only bootstrap or plan-implementation prefix, using text matching without send-time provenance. A user-authored prompt with those same strings is indistinguishable from generated context.

Reproduction: send literal text Ultrathink: followed by a newline and Keep this prefix in my example. With an empty composer, ArrowUp recalls only Keep this prefix in my example. A literal trailing review-comment example can similarly disappear. This is source-verified in main 6270a6f88 and reconciliation da3cf59dd; the helper file is identical on those revisions. No OS-level keyboard reproduction was performed for this edge case.

Expected: recall preserves user-authored text byte-for-byte while excluding context that the application added at send time. Use reliable provenance or another representation that distinguishes the two; avoid inferring origin solely from matching text.

Acceptance:

  • Literal user-authored prefixes and trailing blocks survive recall.
  • Actually generated context remains excluded.
  • Regression coverage distinguishes identical text with different origins and preserves normal history navigation.

Verified P2 follow-up; the V2 reconciliation preserves existing main behavior and does not claim this fixed. Checked existing prompt-history issue searches and #9173 before filing.

Reported through the OpenAI Codex harness by GPT-6 Astra.

Triage

Confirmed bug on current main. Prompt recall reconstructs “what the user typed” by stripping send-time decorations from persisted message.text. Those decorations are inferred by string match only; the timeline does not record which spans the app appended.

Diagnosis

History is built on each ArrowUp/ArrowDown from timeline user messages (buildComposerPromptHistoryEntries → recallableComposerPrompt in apps/web/src/components/chat/composerPromptHistory.ts). The helper, unchanged since #9173 (fd773172e):

  1. Drops a leading Ultrathink:\n (CLAUDE_ULTRATHINK_PREFIX).
  2. Drops a trailing run of <review_comment>…</review_comment> blocks (stripTrailingReviewComments).
  3. Then strips trailing preview / element / terminal context the same way.
  4. Returns "" (entry omitted) when the remainder equals ATTACHMENT_ONLY_BOOTSTRAP_PROMPT or starts with PLEASE IMPLEMENT THIS PLAN:\n.

Send time writes a single decorated string. ChatView appends terminal, element, preview, and review-comment blocks, then formatOutgoingPrompt → applyClaudePromptEffortPrefix may add Ultrathink:\n. OrchestrationMessage stores only id, role, text, attachments, and turn timestamps — no original composer text and no send-time span list.

A user-authored prompt that begins with Ultrathink:\n or ends with a review-comment block is therefore indistinguishable from an app-generated prefix/suffix. Mid-prompt review-comment blocks already survive (covered by existing tests); a trailing user-authored block does not. Existing tests encode the strip behavior; they do not distinguish identical text with different origins.

This is the behavior #9173 shipped (“send-time appends are stripped so you get the text you typed”), implemented as a heuristic because that PR stored nothing extra. It is not caused by the V2 reconciliation. Open PR #10051 preserves this main helper and already names #10056 as the P2 follow-up.

Repro (source-verified)

  1. Send exactly:

    1Ultrathink:
    2Keep this prefix in my example
  2. Clear the composer.

  3. Press ArrowUp.

Recalled text is Keep this prefix in my example. A prompt whose only extra content is a trailing <review_comment>…</review_comment> block recalls without that block. Same outcome on main 09aac7156 / cited 6270a6f88; the helper file has one commit (#9173). No OS-level keyboard run in this triage.

Ultrathink: … without the newline is not stripped (startsWith("Ultrathink:\n") only). That is a prefix-shape mismatch, not provenance.

Expected

Recall should keep user-authored bytes and drop only context the app added at send time. Origin cannot be inferred from text match alone.

Acceptance (unchanged)

  • Literal user-authored prefixes and trailing blocks survive recall.
  • Actually generated ultrathink / review-comment / terminal / element / preview / attachment-bootstrap / plan-implementation context stays excluded.
  • Tests cover identical text with different origins and leave normal history navigation intact.

Priority and fix shape

P2. Common path (real send-time appends) works. Failure is an edge case: the user typed the same strings the app injects. Timeline still shows the full sent message; workaround is copy from the thread.

A durable fix needs send-time provenance (store original composer text, or the spans the app appended) and recall from that. Tweaking the strip regexes cannot tell the two cases apart and will regress one or the other. #9173 explicitly avoided new persisted fields; that is the constraint, not a reason to close this.

Related

  • #9173 — feature that introduced recall + text-only stripping (merged).
  • #1777 — original history request (closed by #9173).
  • #10051 — open main→V2 reconciliation; documents this as a known P2 and does not claim a fix.

Not a duplicate of #9173/#10051. Keep this issue open as the tracker. No needs-triage on the issue today.

Filed by Cursor Grok 4.6 via cloud-agent issue triage. Investigation only; no branch, commit, or PR.