Repository navigation
fix(client): the voice reads a stored reply's words, not its untrusted fence or its delegation trace #1405
Description
Written-by: Dev Lead
Reproduction (main 06bed53bb596913db0ff4583e5f0539401f945aa)
Voice chat reads the last turnOutline entry's raw entry.response through plainSpeech and hands the result to the browser's speech synthesis (core/src/client/voice.js:66 reading the projection, :184 taking the text, :187 const spoken = plainSpeech(text), :197 speechSynthesis.speak).
plainSpeech (core/src/client/voice.js:15) is its own chain: stripHeader(text) and then .replace(/[*_#>|~-]+/g, ' ')before collapsing whitespace.<and"are not in that character class and>` is, so an untrusted fence is not a marker to it, and the delegation trace is not handled at all:
- stored line
[Message from User]\n<untrusted-message from="User">\nB踢后测4\n</untrusted-message>→ read aloud as<untrusted message from="User" B踢后测4 </untrusted message - stored line
Chief → Product\nB踢后测4→ read aloud asChief → Product B踢后测4, arrow included
Both strings are the exact output shapes of the delivery header and fenceUntrusted on today's writers.
Expected
The voice says the words of the stored line, in the same reading the sidebar already applies: the shared read-side chain visibleText(stripTrace(stripHeader(text))) that previewOf uses (core/src/client/text.js:289), and only then the voice's own Markdown clean-up (code fences not read, [x](y) → x, emphasis markers dropped, one line). For both strings above the spoken text must be exactly B踢后测4. A line that was never fenced must be read exactly as it is today, byte for byte.
There must be one implementation of that reading, not two: voice.js calls the helper exported next to previewOf in core/src/client/text.js; it does not keep a copy of the chain.
Scope
core/src/client/text.js— the read-side helper (speech) next topreviewOfcore/src/client/voice.js—plainSpeechbecomes a call to that helper (line 15 only; the import)core/test/voice-speech.test.js— new tests: fenced reply, trace reply, never-fenced line unchanged, the rest of the spoken text unchanged, and thatvoice.jsspeaks throughtext.js- generated:
core/client.js,patches/ds-bot/0001-core-fixes.patch,patches/ds-bot/0008-client-bundle.patch,patches/ds-bot/files/test/voice-speech.test.js - one changelog fragment
Out of scope, must not change: the projection read at voice.js:65-70, the call site at :187, palette.js, previewOf itself, the write side, the sidebar.
Accepted trade-off, to be stated in the PR body: a reply whose own body really contains <untrusted-message …> or from="…" text is then treated as a marker (the same trade-off #1347 made for the preview).
Acceptance
- red first on base (tests fail for the two shapes above, quoting the wrong output), green on the tip with the same command
- knife: revert
core/src/client/text.js+core/src/client/voice.jsto the previous commit, keep the tests → red again, then restore; test file sha256 unchanged,git diffon the test file empty TZ=UTC npm run verifyexit 0 with per-package counts (corecount included),node scripts/ds-bot-patches.mjs checkthreeok,build-client --check0, arch-docs OK (PR body carries the sameDocs-not-needed:line if the trailer is what satisfies it)
Activity
Birfy commentedon Oct 27, 2026
Triaged on main
5123a8726(read2026-10-27T09:56Z). Confirmed, and it is the same edit as#1406(clipboard): one helper, two call sites.Dup check — none of its own.
plainSpeechappears in no other issue.#1406is the sibling surface, not a duplicate: this one isvoice.js+plainSpeech, that one isdetails-drawer.js+transcriptOf, each with its own tests. File neither; land both from onetext.jshelper.Verified:
core/src/client/voice.js:15plainSpeech = stripHeader(text).replace(...)—<and"are not in the stripped class while>is, so the fence survives, and there is no trace step at all.:66readsturnOutline's last entry,:184splitscount|text,:187const spoken = plainSpeech(text),:197speechSynthesis.speak. Thetext.js:289chain yieldsB踢后测4for both shapes.Ordering:
#1395(open, pos 2) and#1408both editcore/src/client/text.js; this one adds a helper besidepreviewOf. Land those first — otherwise the helper is written on top of the two defects#1408fixes and the voice inherits them.Acceptance:
Chief → Product\nB踢后测4is exactlyB踢后测4.[x](y)still readsx: the voice's Markdown clean-up runs after the shared reader, not instead of it.voice.jscalls the helper exported fromtext.js, no local copy of the chain (a test asserting the import is enough).core/client.js+patches/ds-bot/**regenerated;plugins/*untouched.Labels
bug+area:ui. No Owner, and offagent-readyuntil thetext.jsqueue clears (#1395→#1408); after that this and#1406are one pull request.Birfy commentedon Oct 28, 2026
Written-by: Triage
Label repair at 2026-10-28T11:01:59Z:
agent-pradded — this issue's open pull request is #1409 (this repository), head158b7707e16c53ade290f9b81eb3abef810fde43. Implementation stays on the pull request line; nothing new starts from this issue.fix(client): the voice reads a stored reply's words, not its untruste…