Repository navigation
Description
Written-by: Dev Lead
Reproduction (main 7a4fa61e77b0791485b6add4866b0f334be8ef2e)
A Bot↔Bot exchange sheet renders each stored entry by handing the stored string straight to the bubbles:
// core/src/client/exchange.js:55
h(Bubbles, { texts: [entry.text], roster, selfId: bot.id, actions, lead: … })Bubbles passes that same string to MessageTools (core/src/client/cells.js:49), whose Copy and Download use it verbatim (:127). So the sheet is a read path with no read step at all, and a stored exchange line keeps its wire format:
- a fenced line is shown and copied whole:
<untrusted-message from="User">\nfirst\n\nsecond\n</untrusted-message>reaches the bubble and the clipboard as that same string, fence markers,from="User"and all; - a leading delegation trace line is shown and copied:
Chief → 助手\n\nB kick test 4 > hellois rendered asChief → 助手on the first line.
Every other read surface of the same stored line hides both (the transcript uses transcriptOf, the ⌘K row uses previewOf). The exchange sheet shows the arrow and the fence to a person reading it, and Copy hands the wire format to whatever they paste into.
Expected
The sheet reads the line once, before it renders, and both the bubble and Copy/Download/Quote get that one value:
- a complete
<untrusted-message …>…</untrusted-message>wrap (nested wraps included) is unwrapped: markers andfrom="…"gone, the interior text kept, and text before/after the wrap not dropped; - a leading delegation trace line is dropped;
- paragraph breaks inside the message survive byte for byte — this is a multi-paragraph body, not a one-line row, so the read must not flatten
\n\n; - a line that was never fenced or traced renders and copies exactly as today, byte for byte;
- the fence/arrow shown here must not come back through Copy: the clipboard string equals the displayed string.
Accepted trade-off, to be stated in the PR body: a body that really contains <untrusted-message …> / from="…" as prose is treated as a marker (same trade-off as #1347 and #1406), and a residual marker fragment left by an earlier unwrap is dropped too.
Scope
core/src/client/exchange.js— one reader for the entry text; the map handsBubblesthat reader's output and nothing elsecore/test/exchange-read.test.js— new: fenced wrap, nested wrap, residual fragment, multi-paragraph body preserved, plain body untouched, and a rendered bubble whoseMarkdownTexttext andnavigator.clipboard.writeTextstring are equal and free of<untrusted,from=",Chief →- generated:
core/client.js,patches/ds-bot/0003-flybot-api-client.patch(+ thepatches/ds-bot/topics.jsonentry this upstream client file needs),patches/ds-bot/0008-client-bundle.patch,patches/ds-bot/files/test/exchange-read.test.js docs/subsystems/core-client.mdif the module doc lists the client files
Out of scope, must not change: the write side and entry.text, the other tabs, Bubbles and MessageTools themselves, previewOf / readableLine (#1394, #1408), transcriptOf and the details-drawer copy (#1406), palette.js, voice.js. visibleText must not be used here: it flattens paragraphs, which the exchange sheet has to keep.
Acceptance
- red first on base (
node --import ./test/harness-resolve.mjs --test test/exchange-read.test.jsincore/), quoting the leaked fence and the trace line; green on the tip with the same command - knife A: put back only
texts: [entry.text]→ the red is the defect itself (<untrusted-message from="User">\nChief → 助手\n\nfirst para\n\nsecond para\n</untrusted-message>vsfirst para\n\nsecond para), then restore - knife B: swap the reader for
visibleText(stripTrace(body))→ red on flattened paragraphs and on the trace, proving the flattening is excluded, then restore;git diffclean afterwards,core/src/client/exchange.jssha256 back to the tip's TZ=UTC npm run verifyexit 0 with per-package counts,node scripts/ds-bot-patches.mjs checkthreeok,build-client --check0, arch-docs OK
Activity
docs(changelog): note the exchange-sheet copy fix (ThinkFlowLab#1423)