Skip to content

feat(ui): add one-click copy button for code blocks in WebChat#30430

Open
AceOld wants to merge 4 commits intoopenclaw:mainfrom
AceOld:feature/code-block-copy-button
Open

feat(ui): add one-click copy button for code blocks in WebChat#30430
AceOld wants to merge 4 commits intoopenclaw:mainfrom
AceOld:feature/code-block-copy-button

Conversation

@AceOld
Copy link

@AceOld AceOld commented Mar 1, 2026

Summary

Adds a one-click copy button to all code blocks in WebChat messages, making it easier to copy commands and code snippets.

Changes

  • markdown.ts: Custom renderer wraps code blocks in a container with a copy button
  • app-lifecycle.ts: Global event delegation for efficient click handling
  • text.css: Styling for copy button with hover states and feedback animations

Features

  • 🖱️ Hover to reveal: Button appears on hover (always visible on touch devices)
  • Visual feedback: Checkmark icon shows after successful copy
  • 🎨 Theme support: Proper styling for both light and dark themes
  • Event delegation: Single global listener for all code blocks

Screenshot

Hover over any code block to see the copy button in the top-right corner.

Testing

  • Updated markdown test to verify new code block structure
  • Manual testing confirmed copy functionality works

Closes #30390

- 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
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

// 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}` : "";

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

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>`;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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-apps
Copy link
Contributor

greptile-apps bot commented Mar 1, 2026

Greptile Summary

Added 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:

  • SVG icons in copy button will be stripped by DOMPurify - need to add SVG tags (svg, rect, path) and attributes (viewBox, stroke, etc.) to allowlists in markdown.ts:23-55

Changes:

  • Custom markdown renderer wraps code blocks with copy button
  • Global click handler uses event delegation for all copy buttons
  • CSS provides hover states, focus states, and visual feedback
  • Touch device support (button always visible)

Minor Issues:

  • Duplicate CSS rules at text.css:210-218 (already defined earlier)

Confidence Score: 1/5

  • Critical bug will prevent feature from working - SVG icons will be stripped
  • The SVG sanitization issue is a blocker - copy buttons will render without icons, making the feature confusing and broken. This must be fixed before merge.
  • ui/src/ui/markdown.ts requires immediate attention to fix SVG allowlisting

Last reviewed commit: 6cc8533

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 1, 2026

Additional Comments (2)

ui/src/ui/markdown.ts
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:

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"];
Prompt To Fix With AI
This 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.

ui/src/styles/chat/text.css
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.

Prompt To Fix With AI
This 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.

@AceOld
Copy link
Author

AceOld commented Mar 1, 2026

感谢审核!两个问题已修复:

  1. SVG allowlist - 已添加 svg, path, rect, circle, line, polyline, polygon 标签和相关属性到 DOMPurify allowlist
  2. 重复 CSS - 已移除 text.css 中的重复规则

Commit: 905e8ba

@AceOld
Copy link
Author

AceOld commented Mar 1, 2026

感谢 Codex Review!两个问题已修复:

P1 - Escape fence language:

  • 现在只允许 a-zA-Z0-9_+- 字符作为语言标识符
  • 其他字符会被过滤,防止 HTML 注入

P2 - Handle malformed UTF-16:

  • 添加 try-catch 处理 encodeURIComponent 异常
  • 无效代理对会被替换为 U+FFFD

Commit: 457d5cf

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

encodedCode = encodeURIComponent(text);
} catch {
// Fallback: replace unpaired surrogates and retry
encodedCode = encodeURIComponent(text.replace(/[\uD800-\uDFFF]/g, "\uFFFD"));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

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>`;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@AceOld
Copy link
Author

AceOld commented Mar 1, 2026

感谢 Codex Review!两个 P2 问题已修复:

P2 - Preserve valid surrogate pairs:

  • 新增 sanitizeForUriEncoding 函数,只替换无效的代理对
  • 有效的 emoji(代理对)会被保留

P2 - Remove max-height clamp:

  • code-block 改名为 fenced-code-block
  • 新增样式定义,不包含 max-height,避免长代码被裁剪

Commit: 4645067

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +66 to +67
.chat-text :where(.code-block-wrapper) {
position: relative;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Add one-click copy button for code blocks in WebChat

1 participant