copy button on every code block

the bubble's copy button copies the whole reply, which is the wrong unit when
the reply is prose ending in one command to run. fenced blocks get their own
button; inline code doesn't. text read from textContent at click time rather
than the markdown ast, trailing newline stripped so a pasted command doesn't
run itself. the positioned wrapper takes the vertical margin, or the pre's own
margin collapses through it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-07 01:22:57 +00:00
co-authored by Claude Opus 5
parent 29fc9722c1
commit 6a682fae98
4 changed files with 101 additions and 1 deletions
+28
View File
@@ -491,6 +491,34 @@ reasoned, not observed. Typecheck and the sidecar tests are clean.
---
## 18. Every code block has its own copy button
**Where:** any reply containing a fenced block — a command to run, a snippet to paste.
The bubble's copy button copies the _whole reply_. When the reply is prose ending in one command you're
meant to run, that's the wrong unit, and you end up selecting the line by hand — the one chore a command
in a chat exists to save you. Fenced blocks now carry a button in their top-right corner. Hover-revealed
on a pointer device, always visible on touch, because there is no hover there to reveal it with.
Inline `` `code` `` deliberately gets nothing: it's short enough to select, and a button per backticked
word would be noise.
Two details. The text is read from the rendered DOM (`textContent`) at click time rather than
reconstructed from the markdown AST — react-markdown hands the `pre` override a `<code>` element whose
children are strings, elements or nested arrays depending on which plugins ran, and reassembling that is
guesswork; `textContent` is exactly what's on screen. And the trailing newline is stripped, because it
belongs to the fence, not the command — pasted into a shell it would _run_ the thing rather than leave it
on the prompt for you to look at.
The block is wrapped in a positioned div, so `prose.css` moved the vertical margin onto the wrapper;
otherwise the `pre`'s own margin collapses through it and the `:first-child`/`:last-child` reset stops
working. The streaming bubble gets the same treatment, so a block doesn't gain a button when the turn
ends.
**Not verified:** the browser.
---
## Things noticed and deliberately left alone
- **`useChatWebSocket` silently ignores unparseable frames.** That one is intentional and the comment