Skip to content

fix: safely copy text when Clipboard API is unavailable - #503

Open
Moviw wants to merge 1 commit into
openagents-org:developfrom
Moviw:fix/issue-444-safe-clipboard
Open

fix: safely copy text when Clipboard API is unavailable#503
Moviw wants to merge 1 commit into
openagents-org:developfrom
Moviw:fix/issue-444-safe-clipboard

Conversation

@Moviw

@Moviw Moviw commented Jun 18, 2026

Copy link
Copy Markdown

Summary

Fix clipboard operations when navigator.clipboard is unavailable or rejects the write, such as on non-secure HTTP origins.

  • use the Clipboard API when available
  • fall back to a temporary textarea and document.execCommand("copy")
  • remove the temporary element after every fallback attempt
  • keep existing copied-state and error behavior unchanged
  • replace unsafe direct clipboard calls across Workspace, Go Web, Studio, and Launcher

Testing

  • clipboard regression tests: 5 passed
  • Go Web production build passed
  • Workspace production build passed
  • Studio production build passed
  • Launcher production build passed
  • manually verified fallback copying in Chrome on a non-secure HTTP origin

Fixes #444

@vercel

vercel Bot commented Jun 18, 2026

Copy link
Copy Markdown

@Moviw is attempting to deploy a commit to the Raphael's projects Team on Vercel.

A member of the Team first needs to authorize it.

@QuanCheng-QC

Copy link
Copy Markdown
Collaborator

Thanks for this — the direction is right and the implementation is clean. Extracting a single copyTextToClipboard() helper, trying the Clipboard API first and falling back to textarea + execCommand('copy'), with finally-guaranteed cleanup, is exactly what #444 needs. Two things I verified locally and liked:

No bare navigator.clipboard calls are left anywhere on the branch — the sweep across Workspace, Go Web, Studio and Launcher is genuinely complete as of the base commit.
packages/go/web/lib/clipboard.test.ts passes (5/5 under vitest 4). The "fallback throws → element still removed" case is a nice touch.
There's one blocking item and a few smaller ones.

Blocking: the branch is stale and no longer merges
The base is de30412 (mid-June); develop has moved a long way since. Merging today produces:

CONFLICT (modify/delete): packages/launcher/src/renderer/components/agent-detail/AgentDetail.tsx — deleted in develop
CONFLICT (modify/delete): packages/launcher/src/renderer/components/agent-detail/AgentQuickStart.tsx — deleted in develop
CONFLICT (modify/delete): workspace/frontend/components/layout/sidebar-content.tsx — deleted in develop
CONFLICT (content): packages/launcher/src/renderer/pages/logs/index.tsx
CONFLICT (content): packages/launcher/src/renderer/pages/workspaces/index.tsx
CONFLICT (content): workspace/frontend/components/chat/chat-message.tsx
Could you rebase onto current develop? Note that the Launcher's components/agent-detail/* files were replaced by pages/install/detail/*, so those two edits move rather than disappear.

While rebasing, there are 6 new call sites on develop that didn't exist at your base and still call navigator.clipboard directly. They'd need the same treatment for the fix to stay complete:

workspace/frontend/components/layout/user-menu.tsx (2 occurrences)
workspace/frontend/components/knowledge/knowledge-view.tsx
packages/go/web/components/agents/connect-agents-empty.tsx (2 occurrences)
packages/launcher/src/renderer/pages/settings/sections/runtime-section.tsx
packages/launcher/src/renderer/pages/install/detail/use-agent-detail.ts
packages/launcher/src/renderer/pages/install/detail/detail-unmanaged-notice.tsx
packages/launcher/src/renderer/pages/install/detail/detail-quick-start.tsx
The fallback won't actually work on iOS Safari
In all four copies of clipboard.ts, the fallback does textArea.select() and nothing else. On iOS Safari select() alone does not produce a selection that execCommand('copy') will act on — you need the readonly attribute plus an explicit range:

const textArea = document.createElement('textarea');
textArea.value = text;
textArea.setAttribute('readonly', '');
textArea.style.position = 'fixed';
textArea.style.left = '-9999px';
document.body.appendChild(textArea);

try {
textArea.select();
textArea.setSelectionRange(0, text.length);
if (!document.execCommand('copy')) {
throw new Error('Failed to copy text to clipboard');
}
} finally {
textArea.remove();
}
readonly also stops the on-screen keyboard from popping up when the textarea is focused.

This matters more than it might seem: a non-secure HTTP origin on iOS is precisely a case where navigator.clipboard is undefined, so it's squarely in this PR's target scenario. Incidentally, the test helper already mocks setAttribute and setSelectionRange even though the implementation never calls them — I suspect an earlier draft had this and it got trimmed.

Smaller points (non-blocking)
Silent catch {} discards the new error signal. The PR is inconsistent about failures: sidebar-content.tsx, workspace-switcher-menu.tsx and connect-agent-view.tsx surface toast.error(...), while sdk/studio/src/components/chat/CodeBlock.tsx, sdk/studio/src/pages/admin/TransportConfig.tsx, workspace/frontend/components/chat/empty-state.tsx and packages/launcher/src/renderer/components/chat/Markdown.tsx swallow everything. Now that the helper can reject on a genuine failure, an empty catch throws away information users would want. TransportConfig.tsx is the easiest fix — toast is already imported two lines up.

The removed SSR guard. The three use-copy-to-clipboard.ts hooks dropped if (typeof window === 'undefined' || ...) return;, and the helper's first statement is navigator.clipboard?.writeText. Under Node 20 (what Next 16 targets) navigator is not merely undefined — the bare identifier throws ReferenceError. In practice this only runs from click handlers so the risk is low, but the zero-cost version is:

if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) {
Test coverage and CI. Only 1 of the 4 copies is tested. sdk/studio/src/utils/ already has a tests/ directory, so adding the same suite there is cheap. More importantly: no workflow under .github/workflows/ runs packages/go/web's vitest, so clipboard.test.ts will never execute in CI as things stand. Worth either wiring it up or flagging it in a follow-up issue — otherwise the test silently rots.

On the four duplicated files — I looked into whether these should be a shared module, and I don't think they should. There's no root package.json or pnpm-workspace.yaml; these are four independent npm projects, so introducing a shared package would be a much larger change. Copying is the right call here.

Overall: rebase, extend to the new call sites, and fix the iOS selection, and this is good to go. Nice, well-scoped contribution — thanks for tackling it.

@Moviw
Moviw force-pushed the fix/issue-444-safe-clipboard branch from 7b70084 to a884aad Compare August 5, 2026 04:11
@Moviw
Moviw force-pushed the fix/issue-444-safe-clipboard branch from a884aad to 0fb7a11 Compare August 5, 2026 04:13
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.

Copy buttons crash when navigator.clipboard is unavailable

2 participants