Skip to content

feat(viewer): artifact copy button in toolbar - #13

Merged
baanish merged 6 commits into
mainfrom
cursor/artifacts-copy-button-62d6
Mar 20, 2026
Merged

baanish merged 6 commits into
mainfrom
cursor/artifacts-copy-button-62d6

Conversation

@baanish

@baanish baanish commented Mar 20, 2026

Copy link
Copy Markdown
Owner

Summary

  • Adds a Copy control next to Download (and Print / PDF when the active artifact is markdown) in the artifact stage header.
  • Copies the same raw payload as download: getArtifactBody() (markdown/code/csv/json content; diff uses patch or oldContent/newContent join).
  • Introduces src/lib/copy-text.ts (copyTextToClipboard) — Async Clipboard API with execCommand('copy') fallback — and reuses it from the link creator to avoid duplication.
  • Copy completion is ignored if the user switches artifacts while the async copy runs; navigator.clipboard.writeText failures fall through to the sync fallback.
  • UI feedback: Copied (brief highlight, auto-resets after 2s) / Copy failed on error; state resets when switching artifacts.
  • Playwright: e2e for copy on the Viewer bootstrap fixture (localStorage probe cleared before stub).

Test plan

  • npm run lint
  • npm run test
  • npx playwright test tests/e2e/viewer.spec.ts -g "copy action"
Open in Web Open in Cursor 

Summary by CodeRabbit

  • New Features

    • Viewer toolbar adds a Copy action with visual feedback (idle → copied → failed), automatic reset, and clipboard support alongside existing download and markdown print-to-PDF.
  • Documentation

    • Updated README and docs to describe viewer toolbar copy/download/print flows and expanded payload descriptions for markdown and code artifacts.
  • Tests

    • Added end-to-end tests covering successful and failed Copy scenarios.

- Toolbar Copy uses getArtifactBody (same raw text as Download) with feedback states
- Extract copyTextToClipboard for link creator and shell (clipboard API + fallback)
- Document shell actions; add Playwright coverage for copy

Co-authored-by: Aanish Bhirud <baanish@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented Mar 20, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

A clipboard-copy utility was added and used by ViewerShell and Link Creator to enable copying artifact bodies; the viewer toolbar UI and Playwright tests were updated. Documentation files were revised to document copy, download, and markdown print-to-PDF capabilities.

Changes

Cohort / File(s) Summary
Documentation Updates
AGENTS.md, README.md, docs/architecture.md, docs/payload-format.md, docs/testing.md
Text changes to state that the viewer/shell supports copying artifact bodies to the clipboard, downloading artifacts as files, and browser print-to-PDF for markdown; adjusted renderer and testing descriptions.
Clipboard Utility
src/lib/copy-text.ts
Added export async function copyTextToClipboard(value: string): Promise<void> which prefers navigator.clipboard.writeText and falls back to creating a hidden readonly <textarea> + document.execCommand("copy"), with DOM cleanup and error propagation.
Viewer Shell Integration
src/components/viewer-shell.tsx
Added Copy toolbar action and icons, integrated copyTextToClipboard, introduced artifactCopyState (idle → copied/failed), activeArtifactRef and artifactCopyTokenRef to avoid stale updates, handleArtifactCopy with guards and a 2s auto-reset for the copied state; UI updated to reflect copy states.
Link Creator Refactor
src/components/home/link-creator.tsx
Removed local copyText helper and imported copyTextToClipboard; handleCopy now calls the shared utility while preserving try/catch and copy state transitions.
E2E Test Coverage
tests/e2e/viewer.spec.ts
Added two Playwright tests for the viewer Copy control: one that stubs navigator.clipboard.writeText and verifies copied content, and one that simulates clipboard failure (rejecting writeText and execCommand returning false) and asserts the "Copy failed" UI state.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant ViewerShell
  participant ClipboardAPI
  participant FallbackDOM

  User->>ViewerShell: Click "Copy" button
  ViewerShell->>ViewerShell: read artifact body (getArtifactBody)
  ViewerShell->>ClipboardAPI: call navigator.clipboard.writeText(body)
  alt Clipboard API available & succeeds
    ClipboardAPI-->>ViewerShell: resolves
    ViewerShell-->>User: show "Copied" state (auto-reset after 2s)
  else Clipboard API unavailable or rejects
    ViewerShell->>FallbackDOM: create hidden textarea, select text, execCommand("copy")
    FallbackDOM-->>ViewerShell: execCommand result (success/fail)
    ViewerShell-->>User: show "Copied" or "Copy failed" state
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I tapped the Copy button with a happy hop,
The artifact hopped in—no clipboard flop,
With async fetch or textarea sweep,
The viewer copies clean, then naps for a peep. ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(viewer): artifact copy button in toolbar' directly and concisely describes the main change: adding a copy button feature to the viewer artifact toolbar.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/artifacts-copy-button-62d6
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Mar 20, 2026

Copy link
Copy Markdown

Deploying agent-render with  Cloudflare Pages  Cloudflare Pages

Latest commit: 5c519b4
Status: ✅  Deploy successful!
Preview URL: https://0fafabaa.agent-render.pages.dev
Branch Preview URL: https://cursor-artifacts-copy-button.agent-render.pages.dev

View logs

@kilo-code-bot

kilo-code-bot Bot commented Mar 20, 2026

Copy link
Copy Markdown

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Overview

The PR adds clipboard copy functionality to the artifact viewer. The implementation is well-structured:

  • Centralized utility: New copy-text.ts provides a reusable clipboard function with Async Clipboard API + fallback
  • Race condition handling: Uses token-based pattern in viewer-shell.tsx to prevent stale state overwrites
  • Comprehensive tests: Tests both success path and failure scenarios
  • Documentation updates: Updated relevant docs to reflect the new copy feature

Changes Reviewed

File Change
src/lib/copy-text.ts New clipboard utility with proper error handling
src/components/viewer-shell.tsx Added copy button with token-based state management
src/components/home/link-creator.tsx Refactored to use shared copy utility
docs/*, README.md, AGENTS.md Documentation updates
tests/e2e/viewer.spec.ts Added copy action tests
Visual snapshots Updated for new toolbar UI
Files Reviewed (7 files)
  • src/lib/copy-text.ts
  • src/components/viewer-shell.tsx
  • src/components/home/link-creator.tsx
  • AGENTS.md
  • README.md
  • docs/architecture.md
  • docs/payload-format.md
  • docs/testing.md
  • tests/e2e/viewer.spec.ts

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

Copy link
Copy Markdown

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

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/lib/copy-text.ts Outdated
Comment on lines +5 to +8
export async function copyTextToClipboard(value: string): Promise<void> {
if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(value);
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Fall back when navigator.clipboard.writeText rejects

In browsers/webviews that expose navigator.clipboard but reject writeText at runtime (for example because clipboard-write is blocked or the page is not in an allowed context), this returns immediately and never tries the execCommand("copy") fallback below. That means the new artifact-copy button can show Copy failed even though the legacy fallback would have worked. Catching writeText failures here before falling back would make the new viewer action behave reliably across those environments.

Useful? React with 👍 / 👎.

Comment on lines +359 to +361
try {
await copyTextToClipboard(getArtifactBody(activeArtifact));
setArtifactCopyState("copied");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Ignore stale copy completions after switching artifacts

Because clipboard writes are async, a user can click Copy on artifact A and switch to artifact B before the promise resolves. When A's copy eventually succeeds, this unconditionally sets the shared toolbar state to copied, so B now advertises Copied even though its contents were never copied. The success/failure update needs to be tied to the artifact that initiated the request (or discarded once activeArtifact changes).

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
tests/e2e/viewer.spec.ts (1)

195-211: Pre-clear the probe key to prevent stale-storage false positives.

Consider removing copied-artifact-body before stubbing/clicking so the poll can only pass from the current action.

Suggested tweak
   await page.evaluate(() => {
+    window.localStorage.removeItem("copied-artifact-body");
     Object.defineProperty(navigator, "clipboard", {
       configurable: true,
       value: {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/e2e/viewer.spec.ts` around lines 195 - 211, Pre-clear the localStorage
probe key before stubbing the clipboard and performing the click: call
page.evaluate(() => window.localStorage.removeItem("copied-artifact-body"))
immediately before the Object.defineProperty(navigator, "clipboard", ...) block
(or before the click) so the subsequent poll that reads
window.localStorage.getItem("copied-artifact-body") cannot return a stale value;
keep references to the existing symbols (navigator.clipboard stub,
"copied-artifact-body", page.evaluate, and the poll/assertion) and only add the
single removeItem call.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/viewer-shell.tsx`:
- Around line 354-365: The async copy handler handleArtifactCopy can update
artifactCopyState after activeArtifact changes; guard updates by capturing the
current artifact id (e.g., activeArtifact.id) or a request token before awaiting
copyTextToClipboard(getArtifactBody(activeArtifact)) and verify it still matches
the current activeArtifact (or token) before calling
setArtifactCopyState("copied") or setArtifactCopyState("failed"); implement this
check both in the try and catch paths so stale completions cannot overwrite
state for a newly selected artifact.

In `@src/lib/copy-text.ts`:
- Around line 6-9: The current early return when navigator.clipboard.writeText
exists causes runtime errors to skip the sync fallback; update the clipboard
branch in the copy function so you call navigator.clipboard.writeText(value)
inside a try/catch (catching any error like NotAllowedError) and only return on
success—if it throws, swallow/handle the error and allow the function to
continue to the existing document.execCommand("copy") fallback; reference
navigator.clipboard.writeText and document.execCommand("copy") in the function
(e.g., the copyText handler) so the async failure falls through to the
synchronous fallback.

---

Nitpick comments:
In `@tests/e2e/viewer.spec.ts`:
- Around line 195-211: Pre-clear the localStorage probe key before stubbing the
clipboard and performing the click: call page.evaluate(() =>
window.localStorage.removeItem("copied-artifact-body")) immediately before the
Object.defineProperty(navigator, "clipboard", ...) block (or before the click)
so the subsequent poll that reads
window.localStorage.getItem("copied-artifact-body") cannot return a stale value;
keep references to the existing symbols (navigator.clipboard stub,
"copied-artifact-body", page.evaluate, and the poll/assertion) and only add the
single removeItem call.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fcbacec8-a3d4-4c3e-938d-b0c72256ead5

📥 Commits

Reviewing files that changed from the base of the PR and between 5c1e484 and af14386.

📒 Files selected for processing (9)
  • AGENTS.md
  • README.md
  • docs/architecture.md
  • docs/payload-format.md
  • docs/testing.md
  • src/components/home/link-creator.tsx
  • src/components/viewer-shell.tsx
  • src/lib/copy-text.ts
  • tests/e2e/viewer.spec.ts

Comment thread src/components/viewer-shell.tsx Outdated
Comment thread src/lib/copy-text.ts
…obe reset

- Ignore copy completion when activeArtifact.id changed during await
- Try navigator.clipboard.writeText then fall back to execCommand on failure
- Clear copied-artifact-body in clipboard stub evaluate before assert

Co-authored-by: Aanish Bhirud <baanish@users.noreply.github.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/viewer-shell.tsx`:
- Around line 245-247: The artifact copy status can be overwritten by stale
async completions; modify the copy flow to use a per-request token and guard
updates by checking both token and artifact id before calling
setArtifactCopyState: generate a new unique token when a copy starts, store it
alongside activeArtifactIdRef (e.g., activeArtifactIdRef.current and
activeCopyTokenRef), attach the token to the async copy promise, and on
resolution/rejection verify that activeArtifactIdRef matches the artifact id and
the token matches the stored token before setting artifactCopyState to "copied"
or "failed" (also apply same guard in any other copy handlers referenced around
lines 356-376).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 29dec23d-a078-449c-808c-f45d6724aeb2

📥 Commits

Reviewing files that changed from the base of the PR and between af14386 and 2da2f91.

📒 Files selected for processing (3)
  • src/components/viewer-shell.tsx
  • src/lib/copy-text.ts
  • tests/e2e/viewer.spec.ts
✅ Files skipped from review due to trivial changes (1)
  • tests/e2e/viewer.spec.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/lib/copy-text.ts

Comment thread src/components/viewer-shell.tsx
cursoragent and others added 4 commits March 20, 2026 06:45
- Place Copy before Download; keep Download as primary
- Auto-clear copied and failed states after 2s
- handleArtifactCopy: use activeArtifactRef + useCallback([]) for honest memoization
- E2E: assert Copy failed when clipboard rejects and execCommand copy returns false

Co-authored-by: Aanish Bhirud <baanish@users.noreply.github.com>
Prevents overlapping Copy clicks on the same artifact from applying stale
success/failure to the latest UI state.

Co-authored-by: Aanish Bhirud <baanish@users.noreply.github.com>
Baseline PNGs drifted after Copy was added to the artifact stage header;
regenerated via playwright test visual.spec.ts --update-snapshots.

Co-authored-by: Aanish Bhirud <baanish@users.noreply.github.com>
origExec expects string | undefined; coerce null from the wrapper to undefined.

Co-authored-by: Aanish Bhirud <baanish@users.noreply.github.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/components/viewer-shell.tsx (1)

508-515: Consider adding aria-live for accessibility.

The copy button correctly reflects all three states. For improved screen reader support, consider adding aria-live="polite" to announce state transitions ("Copied" / "Copy failed") without requiring focus.

♿ Optional accessibility enhancement
 <button
   type="button"
   className={cn("artifact-action", artifactCopyState === "copied" && "is-primary")}
   onClick={handleArtifactCopy}
+  aria-live="polite"
 >
   {artifactCopyState === "copied" ? <Check className="h-3.5 w-3.5" /> : <Copy className="h-3.5 w-3.5" />}
   {artifactCopyState === "copied" ? "Copied" : artifactCopyState === "failed" ? "Copy failed" : "Copy"}
 </button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/viewer-shell.tsx` around lines 508 - 515, Add an accessible
live region to the copy button in the ViewerShell component so screen readers
announce the artifactCopyState changes; specifically update the button element
(the one using artifactCopyState and handleArtifactCopy) to include
aria-live="polite" (or wrap the text node in a visually-hidden element with
aria-live="polite") so transitions "Copied" / "Copy failed" are announced
without requiring focus.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/components/viewer-shell.tsx`:
- Around line 508-515: Add an accessible live region to the copy button in the
ViewerShell component so screen readers announce the artifactCopyState changes;
specifically update the button element (the one using artifactCopyState and
handleArtifactCopy) to include aria-live="polite" (or wrap the text node in a
visually-hidden element with aria-live="polite") so transitions "Copied" / "Copy
failed" are announced without requiring focus.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f57ad9b9-ae74-4b5b-8e51-d48f0a12f18e

📥 Commits

Reviewing files that changed from the base of the PR and between 2da2f91 and 5c519b4.

⛔ Files ignored due to path filters (8)
  • tests/e2e/visual.spec.ts-snapshots/bundle-switcher-light-chromium.png is excluded by !**/*.png
  • tests/e2e/visual.spec.ts-snapshots/code-light-chromium.png is excluded by !**/*.png
  • tests/e2e/visual.spec.ts-snapshots/csv-compact-light-chromium.png is excluded by !**/*.png
  • tests/e2e/visual.spec.ts-snapshots/diff-light-chromium.png is excluded by !**/*.png
  • tests/e2e/visual.spec.ts-snapshots/empty-state-light-chromium.png is excluded by !**/*.png
  • tests/e2e/visual.spec.ts-snapshots/json-light-chromium.png is excluded by !**/*.png
  • tests/e2e/visual.spec.ts-snapshots/markdown-dark-chromium.png is excluded by !**/*.png
  • tests/e2e/visual.spec.ts-snapshots/markdown-light-chromium.png is excluded by !**/*.png
📒 Files selected for processing (2)
  • src/components/viewer-shell.tsx
  • tests/e2e/viewer.spec.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/e2e/viewer.spec.ts

@baanish
baanish merged commit ae0548b into main Mar 20, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants