Skip to content

fix(clipboard): stop toasting a copy that never happened (BB-215) - #4947

Merged
nachocossio merged 2 commits into
mainfrom
fix/bb-215-clipboard-fallback-false-positive
Sep 11, 2026
Merged

nachocossio merged 2 commits into
mainfrom
fix/bb-215-clipboard-fallback-false-positive

Conversation

@nachocossio

@nachocossio nachocossio commented Sep 11, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes BB-215.

What broke

Open a User Testing study, click Share, click Copy link. A green "Link copied" toast appears and nothing is written to the clipboard.

The chain, all inside client/src/lib/clipboard.ts:

  1. navigator.clipboard.writeText() gets the correct URL and rejects with NotAllowedError: Write permission denied.
  2. The catch runs the execCommand fallback, which appends its scratch <textarea> to document.body — outside the Radix dialog.
  3. The dialog's FocusScope sees focus leave the scope the moment select() runs and pulls it back synchronously, collapsing the selection.
  4. document.execCommand("copy") returns true anyway, so copyToClipboard forwards a success and ShareSection.handleCopyLink toasts "Link copied".

The file already guarded against the neighbouring mistake — its comment says that swallowing the result would make every caller show a success toast for a copy that never happened, and the code forwards the result faithfully. The problem was one layer deeper: the source itself lies.

Not specific to the share modal. Every caller of copyToClipboard that runs inside a dialog had the same false positive — ScenarioShareEmptyPanel, UrlElicitationConsent, error-card, the json-editor copy buttons.

The fix

  1. Append the textarea to the open dialog (document.activeElement?.closest('[role="dialog"],[role="alertdialog"]')) instead of <body>, so the focus trap leaves the selection alone and the copy actually works. alertdialog is in the selector because ShareSection mounts an AlertDialog for link rotation.
  2. Verify the selection before trusting execCommand. If it does not span the textarea, return false and don't run the copy. This makes the failure honest even where the fallback still can't work.

One deviation from the fix suggested on the ticket: the check measures against textarea.value.length, not text.length. A textarea normalizes CRLF to LF, so comparing against the input string would return false for every multi-line copy containing \r\n — trading a false positive for a false negative in the callers that copy JSON, commands and traces. Verified in jsdom: "a\r\nb" is 4 characters, and the textarea's value is 3.

Tests

Three added to clipboard.test.ts. The first two fail against the previous implementation:

  • reports FAILURE when the fallback selection does not take — stubs select() to a no-op while execCommand returns true, which is exactly the shape of the bug.
  • puts the scratch textarea inside the open dialog.
  • still copies multi-line text with CRLF newlines — covers the textarea.value measurement.

Out of scope

NetworkAccessError.tsx has its own inline fallback with the same if (document.execCommand("copy")) shape. It doesn't route through copyToClipboard, so this change doesn't reach it. It renders as a full-page error rather than inside a dialog, so the selection does take there and the BB-215 symptom doesn't apply — left alone rather than widened into this PR.

Verification

Run on the merged tree (branch is up to date with main at ab2a7b730), exit codes captured to a file rather than read through a pipe:

Check Result
npm run docs:check-tokens exit 0
npm run typecheck exit 0
npm run typecheck:client -w @mcpjam/inspector exit 0 (incl. the tier-b and browser-viewer import guards)
vitest run --project client 1304 passed | 3 skipped, 0 assertion failures
npm run build:inspector exit 0
client/src/lib/__tests__/clipboard.test.ts 7/7

Both new regression tests were confirmed to fail against the pre-fix clipboard.ts and pass after it, so they are testing the bug rather than the implementation.

Two failures showed up locally that this branch does not cause:

  • The root npm test exits 1 on Windows in server-side suites (routes/web/*, the utils/harness/local/* sandbox and supervisor family, ws-native-fallback) plus the verify scripts of discord-app, surface-core and slack-app on CRLF format checks. Nothing under server/ or cli/ imports lib/clipboard — this diff is two client files — and the merge-base ab2a7b730 is green on test.yml in CI. Left untouched rather than reformatting packages this change has no business in.
  • ScenarioChatPage.test.tsx > prompts the recipient when the server does require authorization timed out in the full client run and passes in isolation (exit 0). It asserts on an "Authorize again" button, and ScenarioChatPage calls navigator.clipboard.writeText directly without going through copyToClipboard.

E2E not run locally: the diff touches neither routing, app boot, nor the OAuth debugger.


Summary by cubic

Fixes BB-215: the share modal's "Copy link" toasted success while leaving the clipboard untouched. Inside a Radix dialog, the execCommand fallback parked its scratch textarea on <body>, the focus trap collapsed the selection, and execCommand still returned true.

The fallback now appends the textarea to the open dialog so the selection takes, and verifies the selection spans the textarea before trusting execCommand's return value. Callers now get false instead of a false positive.

Bug Fixes

  • The fallback reported success when nothing was copied; it now returns false and logs a warning.
  • The selection check measures against textarea.value (normalizes CRLF to LF), so multi-line copies with Windows newlines still pass.
  • Added regression tests for selection failure, dialog hosting, and CRLF multi-line text; the first two fail against the pre-fix code.

Written for commit ab0c639. Summary will update on new commits.

Review in cubic

The Share modal's "Copy link" showed "Link copied" over an untouched
clipboard. `navigator.clipboard.writeText` rejects with NotAllowedError, the
fallback appends its scratch textarea to `<body>` — outside the Radix dialog —
and the dialog's focus trap pulls focus back off it the moment `select()` runs,
collapsing the selection. `execCommand("copy")` returns true anyway, so
`copyToClipboard` forwarded a success for a copy that took nothing.

Not specific to the share modal: every caller inside a dialog had the same
false positive, including ScenarioShareEmptyPanel, UrlElicitationConsent,
error-card and the json-editor copy buttons.

Append the textarea to the open dialog so the selection can take, and check
that the selection spans the textarea before trusting execCommand's return
value. The check measures against `textarea.value`, not the input string: a
textarea normalizes CRLF to LF, so comparing against `text.length` would
reject every multi-line copy made on Windows.

The two regression tests — "reports FAILURE when the fallback selection does
not take" and "puts the scratch textarea inside the open dialog" — both fail
against the previous implementation.
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@chelojimenez

chelojimenez commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

✅ Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
✅ Open Source Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@coderabbitai

coderabbitai Bot commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 6ff24b5e-07cf-4fbe-8018-c508197aff4b

📥 Commits

Reviewing files that changed from the base of the PR and between d77965e and 1b7ef82.

📒 Files selected for processing (2)
  • mcpjam-inspector/client/src/lib/__tests__/clipboard.test.ts
  • mcpjam-inspector/client/src/lib/clipboard.ts

Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review.


Walkthrough

The clipboard fallback now appends its scratch textarea inside the nearest open dialog when available. It verifies that selection succeeded before running execCommand("copy") and returns false when selection failed. Tests cover collapsed selection, CRLF text normalization, and dialog placement.

Priority: ⬇️ Low

Merge Risk: ⚪ Minimal · up to ab0c6

The clipboard fallback now reports failure when it cannot select text and works within dialog focus traps. No actionable merge-blocking risk remains.


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.

@github-actions

github-actions Bot commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

Internal preview

Preview URL: https://mcp-inspector-pr-4947.up.railway.app
Deployed commit: 5bc76a1
PR head commit: ab0c639
Backend target: staging fallback.
Health: ✅ Convex reachable
Access is employee-only in non-production environments.

@prathmeshpatel prathmeshpatel left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

lgtm

@nachocossio
nachocossio enabled auto-merge (squash) September 11, 2026 02:06
@nachocossio
nachocossio merged commit 874a79f into main Sep 11, 2026
25 of 26 checks passed
@nachocossio
nachocossio deleted the fix/bb-215-clipboard-fallback-false-positive branch September 11, 2026 02:13

This branch was successfully deployed

1 active deployment
preview-pr-4947 — ab0c6391 Deployed Sep 11, 2026 by nachocossio via upsert-preview #19252
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.

3 participants