Repository navigation
fix(claude): show the user's words, not Claude's paste envelope - #1060
Merged
Merged
Conversation
Closes #1059. Claude Code 2.1.278 commits a PASTED prompt wrapped in its own tag, with the id repeated on the closing side: <pasted_content id="cade"> …what the user actually wrote… </pasted_content id="cade"> #1053 taught prompt ACCEPTANCE about that shape. Nothing taught the surfaces that show or replay the prompt, so the owner's own transcript read `❯ <pasted_content id="cade"> should we make a new release? …`. It is more than cosmetic. Prompt history (⌘↑) replayed the envelope into the composer, and sending that again made Claude wrap the already-wrapped text — one envelope deeper per round trip, the stored prompt drifting further from what the user wrote each time. A pane title for any pasted prompt began with `<pasted_content id="…`, which is exactly the prompt long enough to need a title. And two pastes of the SAME prompt carry different envelope ids, so the consecutive-duplicate guard called them distinct and ⌘↑ offered it twice. The unwrapping rule belongs to the provider, not to the feed or to a shared helper: `typedUserPromptText` joins `isTypedUserPrompt` on the renderer capability, defaulting to identity, so a fifth provider with its own envelope has one place to answer. `isTypedUserPrompt` already had to understand the envelope to decide whether a row was the user's — it just never passed the unwrapped text on. Applied at both display edges: `latestUserPrompts` (pane titles, ⌘↑ history, View Prompts, the Rewind picker) and the feed's two user-text paths — a string `message.content` and a `[{type:'text'}]` block list, because Claude commits both and fixing only the one in the screenshot would leave the other painting the envelope. Acceptance matching in main is untouched: it compares against the row Claude committed, and Claude committed the envelope. Tests drive the real exported extractors and the real ConversationRow against a NEW recording — the owner's own pasted prompt, verbatim, `permissionMode` and repeated closing id included. Each half is mutation-checked: making the capability identity fails 4, reverting either feed path fails 1. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
That test's subject is that a PASTED prompt is kept in history rather than rejected by the `<` scaffolding guard, and that is unchanged. Its expected text spelled out the raw envelope because that is what #1052 left behind; #1059 changes what the extractor returns, deliberately, so the expectation moves with it. The assertion stays exact and still fails if the row is dropped or if scaffolding sneaks in. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ew found Review of #1060 accepted the mechanism and then proved the coverage claim wrong: two surfaces the PR listed as fixed were not, and a third was never considered. All three are the SAME failure, one layer down. 1. View Prompts and the conversations picker read prompts from the catalog, not from the feed — `conversations/catalog/unwrap.ts`, whose closed wrapper list never learned about `<pasted_content>`. It therefore fell to the generic `<`-prefix rule and the prompt was DROPPED, not shown wrapped. A session whose prompts are all pasted rendered "No visible user prompts found for this session" while the feed directly above it showed them, and `firstUnwrappedPrompt` skipped the opening prompt, so the picker labelled the conversation from a later, shorter one. This is the #1053 symptom reproduced on a different surface, which is what a closed list is FOR: the envelope is now an entry beside `<stt …>`, the other wrapper around something the user really did mean. 2. Rewind. `claudeDraft` feeds the picker's row text AND the composer prefill of the rewound session, so the envelope was both shown and RE-SENT — and a re-send is what makes Claude wrap the already-wrapped text. Unwrapped beside the `<bash-input>` and `<command-name>` extraction that was already there for exactly this reason. 3. The queue strip did the right thing through a direct call to Claude's unwrapper. It goes through the provider capability now, like every other display surface; harmless today because the pattern is exact, but it is the copy that drifts. Acceptance matching, the queued-prompt dialog's exact source, redaction, recordings and the ledger stay untouched: those compare against or preserve the row Claude committed. Tests drive the real exported entry points — `unwrapUserText` / `firstUnwrappedPrompt`, and `getHostTranscriptAdapter('claude').draft` — on the recorded envelope. Mutation-checked: the catalog dropping it again fails 2, the rewind draft keeping it fails 1. Both ambiguous-envelope cases are pinned so the shared unwrapper's refusal to guess cannot be loosened here. One comment corrected in passing: the first draft claimed the unwrap had to precede the tag extraction or a pasted `/command` would never match. `extractTagBody` is unanchored, so that was false. The order is kept for a real reason — one value through every branch — and the comment now says that. Co-Authored-By: Claude Opus 5 (1M context) <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.
Closes #1059.
What the owner saw
Claude Code 2.1.278 commits a pasted prompt wrapped in its own tag, with the id repeated on the closing side. #1053 taught prompt acceptance about that shape; nothing taught the surfaces that show or replay the prompt.
Why it is more than cosmetic
<pasted_content id="…— exactly the prompt long enough to need a title.The shape of the fix
typedUserPromptTextjoinsisTypedUserPrompton the renderer provider capability, defaulting to identity. The envelope is Claude's, so Claude answers; the feed and the shared extractor keep knowing nothing about any provider's scaffolding, and a fifth provider with its own wrapper has one place to answer.isClaudeTypedUserPromptalready had to understand the envelope to decide whether a row was the user's (#1053) — it just unwrapped to decide and handed the raw text on.Applied at both display edges:
latestUserPrompts.tsUserPromptProse(new), used byConversationRow+BlockBoth feed paths, because Claude commits a user prompt as a string
message.contentfor some prompts and a[{type:'text'}]block list for others (an attachment, a queued send). Fixing only the one the screenshot showed would leave the other painting the envelope.Acceptance matching in main is untouched — it compares against the row Claude committed, and Claude committed the envelope.
Integration tests, on a new real recording
testing/fixtures/prompt-acceptance/pasted-typed-prompt-2026-09-20.jsonis the owner's own pasted prompt, lifted verbatim from the live transcript (uuid9981e169-…). The existing envelope fixture records a goal-loop continuation, which has nopermissionModeand is correctly not treated as typed — so it could not drive this path.Two details make it a recording rather than a literal: the closing tag repeats the id, and it carries
permissionMode: "bypassPermissions".pastedPromptDisplay.renderer.test.tsxdrives the real exported extractors and the realConversationRow, and starts by asserting the recording really is a wrapped user-submitted prompt, so nothing below can pass vacuously.Fail-first, by mutation (each applied and recorded):
typedUserPromptTextreturns the raw textTextProseTextProseAlso pinned: the prompt is still found (the #1053 failure must not come back), two ids collapse to one history entry, a non-Claude provider's text is untouched, and an assistant message that quotes the envelope still renders it verbatim — that is the model's output, not the user's prompt.
npx tsc -bclean;src/renderer/src/features/feed/andfeatures/workspace/lib/green (21 tests).🤖 Generated with Claude Code