feat: add copy-to-clipboard toolbar for message bubbles - #783
Conversation
…icks Body (if you want more detail): - Catch navigator.clipboard.writeText() rejections and show an error state instead of an unhandled promise rejection - Reset via .finally() using a closure timer instead of storing state on the DOM element - Disable pointer-events on the copy toolbar while hidden so hovering near a message can't trigger an accidental copy
There was a problem hiding this comment.
Pull request overview
Adds a hover-revealed copy-to-clipboard action on message bubbles in the VS Code extension chat panel, enabling quick copying of raw markdown for both user prompts and agent responses (including in-progress streamed output).
Changes:
- Adds a reusable
createCopyToolbar()and wraps message bubbles to show a copy icon on hover. - Ensures streamed agent responses copy the latest text by keeping
wrapper.dataset.rawTextsynced per chunk. - Updates the generated Tailwind CSS output and adds a changeset entry for the main package changelog.
Reviewed changes
Copilot reviewed 2 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| plugins/vscode/media/chat-panel.js | Adds wrapper + hover toolbar and hooks copy behavior into both static and streamed message rendering paths. |
| plugins/vscode/media/chat-panel.css | Updates generated Tailwind output to include new utility classes used by the toolbar (e.g., pointer-events, sizing, variants). |
| .changeset/add-vscode-copy-button.md | Records the feature in the monorepo changelog as a patch-level change. |
Files not reviewed (1)
- plugins/vscode/media/chat-panel.css: Generated file
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| btn.addEventListener('click', () => { | ||
| const text = getText(); | ||
| if (!text) return; | ||
| navigator.clipboard.writeText(text).then(() => { | ||
| btn.innerHTML = ICONS.success; | ||
| btn.title = 'Copied!'; | ||
| }).catch(() => { | ||
| btn.innerHTML = ICONS.error; | ||
| btn.title = 'Copy failed'; | ||
| }).finally(() => { | ||
| clearTimeout(resetTimer); | ||
| resetTimer = setTimeout(() => { | ||
| btn.innerHTML = ICONS.clipboard; | ||
| btn.title = 'Copy'; | ||
| }, 1500); | ||
| }); | ||
| }); |
| const btn = document.createElement('button'); | ||
| btn.className = 'flex items-center justify-center bg-transparent border-none cursor-pointer text-vscode-fg opacity-60 hover:opacity-100 p-1 rounded hover:bg-vscode-toolbarHover [&_svg]:mr-0'; | ||
| btn.title = 'Copy'; | ||
| btn.innerHTML = ICONS.clipboard; |
|
@akramcodez Check out this pr and tell me if any changes are needed , thanks |
|
@arpan7sarkar Thanks for the contribution! I really like the overall implementation, especially the use of I went through the issue, the implementation, and the user flow, and I have a few comments before I'd be comfortable approving this. 1. Missing Clipboard API GuardWhat is wrong: The copy button click handler assumes Why is it wrong: In some VS Code webview environments (or older runtimes), Where is it wrong:
What should be done: Guard against 2. Accessibility & Button SemanticsWhat is wrong: The icon-only button doesn't specify Why is it wrong: Without an explicit button type, it could behave as a submit button if this UI is ever placed inside a form. Since the button only contains an SVG icon, screen readers also have no way of announcing its purpose. Where is it wrong:
What should be done: Set:
to improve accessibility and future-proof the component. 3. Missing Test CoverageWhat is wrong: The new functionality currently relies entirely on manual verification. Why is it wrong: This PR introduces two important behaviors:
Without automated coverage, future refactors could accidentally break the streamed-copy logic without anyone noticing. What should be done: I'd recommend adding tests covering:
4. UI Suggestion (Non-blocking)This isn't a blocker, but I think the current placement of the copy button looks a little disconnected from the message bubble. It feels more like a floating action than metadata associated with that specific message. Since Nanocoder already persists conversations, I think we could make better use of the bottom area of each message by displaying both the copy action and the local timestamp. For example: User message Assistant message This keeps the layout balanced:
I think this has a few advantages:
Overall, I think this is a really nice feature and the streaming solution is well thought out. Once the clipboard guard, accessibility improvements, and some automated coverage are added, I'd be happy to approve it. The timestamp suggestion is non-blocking, but I think it would make the overall chat experience feel much more polished. |
… accessibility attributes
|
Hey @akramcodez I looked into the best way to add these tests and wanted to flag something before just doing it. chat-panel.js is a single top-level IIFE with no exports and no module system, and there's currently no test setup for this file at all (no DOM environment, nothing wired up). To actually test the copy-to-clipboard behavior (normal copy, streamed copy reading the live dataset.rawText, and the clipboard-failure state) against the real code, I need some way to run it in a fake DOM from Node. I looked into jsdom for this — it's the standard tool for testing browser DOM code from Node, dev-only (never ships in the extension bundle or CLI), and it's what most projects reach for instead of hand-rolling a fake DOM. The alternative is writing our own minimal DOM stub with Node's built-in vm module, which avoids a new dependency but means we maintain that fake DOM ourselves and it'll quietly drift out of sync as chat-panel.js grows. Do I have the go-ahead to add jsdom as a devDependency for this, or is there a different testing approach you'd prefer for this package? |
…nd timestamp display
|
@arpan7sarkar Great work! Just a couple of small suggestions:
|
|
Hey @akramcodez , and removed unnecessary commits now you can merge it thanks. |
|
Thanks for this PR - feel free to add yourself as a contributor to our website via a PR which I will approve :) https://nanocollective.org/contributors |
|
Hi , @will-lamerton I have added the pr to add me as contributor here Nano-Collective/organisation#89 (comment) , |


Description
Adds a "Copy" button to both user prompt bubbles and AI response bubbles in the VS Code extension's chat panel (closes #746). Hovering over a message bubble reveals a subtle clipboard icon (bottom-right for user messages, bottom-left for agent messages); clicking it copies the raw markdown text via navigator.clipboard.writeText() and briefly swaps the icon for a checkmark to confirm the copy.
Screencast_20260803_234750.webm
For streamed agent responses, the copy button always reads the latest in-progress text (via a dataset.rawText kept in sync on each chunk) rather than a stale snapshot, so copying mid-stream works correctly.
Type of Change
Changeset
nanocoder-vscode is listed in .changeset/config.json's ignore array, so no changeset is needed for this package.
Testing
Automated Tests
chat-panel.js is a plain DOM script with no existing test harness/spec coverage in this extension, so no automated test was added for the UI behavior. Verified pnpm run test:format and pnpm run
test:lint both pass.
Manual Testing
Built and installed the extension locally (pnpm run build:vscode) and confirmed it activates
correctly and the copy button rendechat panel. Full end-to-endmessage-send testing against a live provider is still pending on my end.
Checklist