Skip to content

fix(claude): show the user's words, not Claude's paste envelope - #1060

Merged
Juliusolsson05 merged 3 commits into
mainfrom
fix/pasted-envelope-display
Sep 20, 2026
Merged

Juliusolsson05 merged 3 commits into
mainfrom
fix/pasted-envelope-display

Conversation

@Juliusolsson05

Copy link
Copy Markdown
Owner

Closes #1059.

What the owner saw

❯ <pasted_content id="cade"> should we make a new release? are we ready for
  that, we have resolved most of the worst stuff? i think you should kick off
  a new release for me please </pasted_content id="cade">

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

  • ⌘↑ history replayed the envelope into the composer. Sending it again made Claude wrap the already-wrapped text, so the stored prompt grew one envelope per round trip and drifted further from what the user wrote each time.
  • Pane titles for any pasted prompt began with <pasted_content id="… — exactly the prompt long enough to need a title.
  • Two pastes of the same prompt carry different envelope ids, so the consecutive-duplicate guard compared unequal strings and ⌘↑ offered it twice.

The shape of the fix

typedUserPromptText joins isTypedUserPrompt on 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.

isClaudeTypedUserPrompt already 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:

Edge Reaches
latestUserPrompts.ts pane titles, ⌘↑ history, View Prompts, the Rewind picker
UserPromptProse (new), used by ConversationRow + Block the feed

Both feed paths, because Claude commits a user prompt as a string message.content for 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.json is the owner's own pasted prompt, lifted verbatim from the live transcript (uuid 9981e169-…). The existing envelope fixture records a goal-loop continuation, which has no permissionMode and 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.tsx drives the real exported extractors and the real ConversationRow, 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):

Mutation Result
Claude's typedUserPromptText returns the raw text 4 failed
feed string row back to raw TextProse 1 failed
feed block row back to raw TextProse 1 failed

Also 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 -b clean; src/renderer/src/features/feed/ and features/workspace/lib/ green (21 tests).

🤖 Generated with Claude Code

Juliusolsson05 and others added 3 commits September 19, 2026 23:44
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>
@Juliusolsson05
Juliusolsson05 merged commit 1688bee into main Sep 20, 2026
2 checks passed
@Juliusolsson05
Juliusolsson05 deleted the fix/pasted-envelope-display branch September 20, 2026 07:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Claude's pasted_content envelope is shown raw as the user's prompt, and re-sending it double-wraps

1 participant