Skip to content

fix(client): the conversation copy carries a stored reply's words, not its untrusted fence or its delegation trace #1406

Closed

Description

@HugoXK

Written-by: Dev Lead

Reproduction (main 06bed53bb596913db0ff4583e5f0539401f945aa)

In a conversation's details drawer, "Copy conversation as Markdown" copies transcriptOf(...) (core/src/client/details-drawer.js:254 gives it to the drawer, the share handler at :266 hands the string to copiedToClipboard). transcriptOf (:21) builds every reply line as

lines.push(`**${name}:** ${stripHeader(entry.response).trim()}`)

(:28), so the clipboard gets the stored line raw apart from the delivery header:

  • <untrusted-message from="User">\nB踢后测4\n</untrusted-message> is copied as **Name:** <untrusted-message from="User">\nB踢后测4\n</untrusted-message>
  • the delegation trace is copied too: Chief → Product\nB踢后测4 becomes **Name:** Chief → Product\nB踢后测4

So the pasted Markdown shows the fence, from="User" and the routing arrow that the on-screen preview already hides. Copy is a read path like any other and must not show the wire format.

Expected

Every reply line in the copied Markdown carries the words of the stored line, read through the shared read-side chain stripTrace(stripHeader(text)) → visibleText(...) (the chain previewOf uses at core/src/client/text.js:289), with the **Name:** prefix kept. For both strings above the copied body must be B踢后测4. A line that was never fenced is copied exactly as today (line breaks preserved, only the delivery header removed). The owner's own prompt lines and the **You:** prefix are unchanged, and no other tab or copy action changes.

There must be one transcript reader, not two: the copy path and any other reader share the exported helper in core/src/client/text.js; details-drawer.js keeps no local copy of the chain.

Accepted trade-off, to be stated in the PR body: a fenced body is flattened to one line in the clipboard, exactly as the line-reading surface shows it; and a body that really contains <untrusted-message …> / from="…" text is treated as a marker (same trade-off as #1347).

Scope

  • core/src/client/text.js — the shared transcript/copy reader next to previewOf
  • core/src/client/details-drawer.js — import it instead of the local transcriptOf; only the read path
  • core/test/details-copy.test.js — new tests: a fenced reply and a trace reply are copied as their words, a never-fenced reply is unchanged, and the drawer no longer holds its own stripHeader(entry.response) chain
  • generated: core/client.js, patches/ds-bot/0001-core-fixes.patch, patches/ds-bot/0008-client-bundle.patch, patches/ds-bot/files/test/details-copy.test.js
  • docs/subsystems/core-client.md if the module doc names the reader (the module owns both source files), and one changelog fragment

Out of scope, must not change: entry.prompt and its t('You') prefix, the other drawer tabs, the write side, previewOf itself, palette.js, voice.js.

Acceptance

  • red first on base (both shapes fail, quoting the leaked <untrusted-message from="…"> and the trace), green on the tip with the same command
  • knife: put only the production read path back to stripHeader(body).trim(), keep the tests → red again, then restore; generated core/client.js sha256 unchanged across the knife, git diff on it empty
  • TZ=UTC npm run verify exit 0 with per-package counts, node scripts/ds-bot-patches.mjs check three ok, build-client --check 0, arch-docs OK

Activity

  1. Birfy commented on Oct 27, 2026

    @Birfy
    Collaborator

    Triaged on main 5123a8726 (read 2026-10-27T09:56Z). Confirmed; same helper as #1405, different call site.

    Dup check — none. transcriptOf appears in no other issue; #1405 is the voice surface (sibling, not duplicate).

    Verified: core/src/client/details-drawer.js:21 transcriptOf; :28 pushes **Name:** <stripHeader(entry.response).trim()> — header only, fence and trace copied verbatim; :254 builds it for the drawer; :266 hands the string to copiedToClipboard. So the clipboard gets <untrusted-message from="User"> and Chief → Product today.

    Ordering: behind #1395 and #1408 (both edit core/src/client/text.js). Then this and #1405 are one pull request: one reader in text.js, two call sites, two test files.

    Acceptance:

    1. Copied body for the fenced reply and for Chief → Product\nB踢后测4 is exactly B踢后测4, with the **Name:** prefix kept.
    2. A never-fenced reply keeps its line breaks and differs from today only by the delivery header — assert the exact string.
    3. entry.prompt / **You:** lines, the other drawer tabs and the on-screen transcript are untouched.
    4. One reader: transcriptOf keeps no local stripHeader(entry.response) chain, and a test asserts the drawer imports the helper.
    5. core/client.js + patches/ds-bot/** regenerated; docs/subsystems/core-client.md updated if it names the reader; one changelog fragment.

    Labels bug + area:ui. No Owner; off agent-ready until the text.js queue clears, then it rides with #1405.

  2. added
    area:uiWeb/desktop UI and copy
    bugSomething isn't working
    agent-prPull request opened by an agent
    on Oct 27, 2026
  3. Birfy commented on Oct 28, 2026

    @Birfy
    Collaborator

    Written-by: Triage

    Label repair at 2026-10-28T11:01:59Z: agent-pr added — this issue's open pull request is #1410 (this repository), head 4235606eb4edf66367ee4edd8711b856e8ae8685. Implementation stays on the pull request line; nothing new starts from this issue.

  4. added a commit that references this issue on Oct 28, 2026
    6cee395
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:uiWeb/desktop UI and copybugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions