feat(ui): add one-click copy button for code blocks in WebChat#30430
feat(ui): add one-click copy button for code blocks in WebChat#30430AceOld wants to merge 4 commits intoopenclaw:mainfrom
Conversation
- Add copy button to all fenced code blocks in chat messages - Button appears on hover (always visible on touch devices) - Shows checkmark feedback after successful copy - Uses event delegation for efficient click handling - Includes proper styling for both light and dark themes Closes openclaw#30390
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6cc85337a8
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
ui/src/ui/markdown.ts
Outdated
| // Custom renderer for code blocks with copy button | ||
| const codeBlockRenderer = new marked.Renderer(); | ||
| codeBlockRenderer.code = ({ text, lang }: { text: string; lang?: string }): string => { | ||
| const langClass = lang ? ` language-${lang}` : ""; |
There was a problem hiding this comment.
Escape fence language before building the
<pre> class
The custom code renderer injects lang directly into the class attribute, so a fenced info string containing quotes or > can break attribute context and inject extra HTML nodes. Because div/span/button and related attributes are now allowed by the sanitizer, this re-opens structural HTML injection through code fences even though raw HTML rendering is intended to be escaped. Restrict lang to a safe pattern (or escape it) before concatenating it into markup.
Useful? React with 👍 / 👎.
ui/src/ui/markdown.ts
Outdated
| const langClass = lang ? ` language-${lang}` : ""; | ||
| const escapedCode = escapeHtml(text); | ||
| // Use a wrapper div with relative positioning for the copy button | ||
| return `<div class="code-block-wrapper"><pre class="code-block${langClass}"><code>${escapedCode}</code></pre><button class="code-block-copy-btn" type="button" data-code="${encodeURIComponent(text)}" title="Copy code">${COPY_ICON_SVG}<span class="code-block-copy-btn__check">${CHECK_ICON_SVG}</span></button></div>`; |
There was a problem hiding this comment.
Handle malformed UTF-16 before URI-encoding code text
encodeURIComponent(text) throws URIError for unpaired surrogate code units (for example payloads containing JSON escapes like \uD800), and this call runs during markdown rendering. A single malformed code block will therefore throw out of toSanitizedMarkdownHtml and prevent the message from rendering. Normalize or safely encode invalid surrogate input before writing data-code.
Useful? React with 👍 / 👎.
Greptile SummaryAdded one-click copy functionality to code blocks with hover-activated button, visual feedback, and theme support. The implementation uses event delegation for efficiency and URL-encoding for data attributes. Critical Issue:
Changes:
Minor Issues:
Confidence Score: 1/5
Last reviewed commit: 6cc8533 |
Additional Comments (2)
The copy button SVG icons (defined at lines 6-7) will be stripped by DOMPurify because:
The button will render but without any visual icons. Add the necessary SVG tags and attributes to both arrays: Prompt To Fix With AIThis is a comment left during a code review.
Path: ui/src/ui/markdown.ts
Line: 23-53
Comment:
SVG tags and attributes missing from DOMPurify allowlists - icons won't render.
The copy button SVG icons (defined at lines 6-7) will be stripped by DOMPurify because:
- Tags `svg`, `rect`, `path` are not in `allowedTags`
- SVG attributes like `viewBox`, `fill`, `stroke`, `stroke-width`, `stroke-linecap`, `stroke-linejoin`, `width`, `height`, `x`, `y`, `rx`, `ry`, `d` are not in `allowedAttrs`
The button will render but without any visual icons. Add the necessary SVG tags and attributes to both arrays:
```suggestion
const allowedTags = [
"a",
"b",
"blockquote",
"br",
"button",
"code",
"del",
"div",
"em",
"h1",
"h2",
"h3",
"h4",
"hr",
"i",
"li",
"ol",
"p",
"pre",
"span",
"strong",
"svg",
"path",
"rect",
"circle",
"line",
"polyline",
"polygon",
"table",
"tbody",
"td",
"th",
"thead",
"tr",
"ul",
"img",
];
const allowedAttrs = ["class", "href", "rel", "target", "title", "start", "src", "alt", "data-code", "type", "viewBox", "fill", "stroke", "stroke-width", "stroke-linecap", "stroke-linejoin", "width", "height", "x", "y", "x1", "x2", "y1", "y2", "rx", "ry", "d", "cx", "cy", "r", "points"];
```
How can I resolve this? If you propose a fix, please make it concise.
These light mode styles are duplicates of rules defined earlier in the file. Lines 154-157 already define inline code styling, and lines 159-162 define code block wrapper styling. These duplicate rules at the end can be removed. Prompt To Fix With AIThis is a comment left during a code review.
Path: ui/src/styles/chat/text.css
Line: 210-218
Comment:
Duplicate CSS rules - already defined at lines 154-162.
These light mode styles are duplicates of rules defined earlier in the file. Lines 154-157 already define inline code styling, and lines 159-162 define code block wrapper styling. These duplicate rules at the end can be removed.
How can I resolve this? If you propose a fix, please make it concise. |
|
感谢审核!两个问题已修复:
Commit: 905e8ba |
|
感谢 Codex Review!两个问题已修复: P1 - Escape fence language:
P2 - Handle malformed UTF-16:
Commit: 457d5cf |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 457d5cfbfc
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
ui/src/ui/markdown.ts
Outdated
| encodedCode = encodeURIComponent(text); | ||
| } catch { | ||
| // Fallback: replace unpaired surrogates and retry | ||
| encodedCode = encodeURIComponent(text.replace(/[\uD800-\uDFFF]/g, "\uFFFD")); |
There was a problem hiding this comment.
Preserve valid surrogate pairs in copy fallback
When encodeURIComponent(text) throws, the fallback replaces every UTF-16 surrogate code unit with \uFFFD, not just malformed ones, so any valid astral characters in the same block (for example emoji) are corrupted in data-code and the copied text no longer matches what is rendered. This regression is triggered by inputs that contain at least one unpaired surrogate plus any valid surrogate pair, and it can silently alter copied commands/snippets.
Useful? React with 👍 / 👎.
ui/src/ui/markdown.ts
Outdated
| encodedCode = encodeURIComponent(text.replace(/[\uD800-\uDFFF]/g, "\uFFFD")); | ||
| } | ||
| // Use a wrapper div with relative positioning for the copy button | ||
| return `<div class="code-block-wrapper"><pre class="code-block${langClass}"><code>${escapedCode}</code></pre><button class="code-block-copy-btn" type="button" data-code="${encodedCode}" title="Copy code">${COPY_ICON_SVG}<span class="code-block-copy-btn__check">${CHECK_ICON_SVG}</span></button></div>`; |
There was a problem hiding this comment.
Remove unintended max-height clamp from markdown fences
Adding class="code-block" to every fenced block causes chat markdown code fences to inherit the global .code-block rule (max-height: 360px; overflow: auto; in ui/src/styles/components.css), so long snippets are now vertically clipped with an internal scrollbar. This is a functional UX regression introduced by the new renderer and affects any multi-hundred-line snippet in chat messages.
Useful? React with 👍 / 👎.
|
感谢 Codex Review!两个 P2 问题已修复: P2 - Preserve valid surrogate pairs:
P2 - Remove max-height clamp:
Commit: 4645067 |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4645067e38
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| .chat-text :where(.code-block-wrapper) { | ||
| position: relative; |
There was a problem hiding this comment.
Scope code block wrapper positioning for non-chat markdown
toSanitizedMarkdownHtml now emits .code-block-wrapper for every fenced block and is also used by the sidebar renderer (ui/src/ui/views/markdown-sidebar.ts), but position: relative is only applied under .chat-text. In sidebar/tool-output markdown, .code-block-copy-btn stays position: absolute without a positioned wrapper, so the button is anchored to an outer container/viewport instead of its code block, causing misplaced controls and overlap with unrelated UI whenever sidebar content includes fences.
Useful? React with 👍 / 👎.
| font-size: 13px; | ||
| line-height: 1.5; | ||
| background: var(--secondary); | ||
| padding: 12px; |
There was a problem hiding this comment.
Keep copy-button padding when styling fenced code blocks
The wrapper rule reserves padding-right: 40px for the copy button, but the later .fenced-code-block { padding: 12px; } shorthand overrides it at equal specificity, removing the reserved space. This causes the button to sit on top of code text (especially on touch devices where the button is always visible), which can obscure the first line and interfere with selection/copying.
Useful? React with 👍 / 👎.
Summary
Adds a one-click copy button to all code blocks in WebChat messages, making it easier to copy commands and code snippets.
Changes
Features
Screenshot
Hover over any code block to see the copy button in the top-right corner.
Testing
Closes #30390