Custom sidebars: gesture-gated, write-only clipboard.write

Merged
TE
1mo ago
+344-9

Summary

Custom .js sidebars couldn't copy anything, so they couldn't offer the Copy Workspace ID / Copy Workspace Link items the built-in sidebar has. They can now call clipboard.write(text):

Text(() => w().title).contextMenu([
  Button("Copy Workspace ID", () => clipboard.write(w().id)),
])

It only works during a user gesture, so a sidebar can't swap the clipboard in the background right before you paste into a shell (pastejacking):

  • SidebarJSRuntime.dispatchEvent opens a user-activation scope for tap (which includes context-menu items) and doubletap only, and closes it when that synchronous JS dispatch returns. clipboard.write is honored only inside it. Text-field submit doesn't count: it also fires on focus loss, and an autofocused field mounted by a data update can receive a Return the user meant for the terminal.
  • Calls from load, data effects, drag/move callbacks, text-field submit/edit, promise continuations queued at load, or a callback saved during a click and run later return false, write nothing, and emit clipboard.write rejected: <reason> on the log channel (plus stderr in DEBUG builds).
  • Plain text only, capped at 64 KB (UTF-8). There is no read API.
  • The app writes through WorkspaceSurfaceIdentifierClipboardText.copy, the same pasteboard helper the built-in Copy Workspace ID / Link commands use. It reaches the runtime as a new optional SidebarActionDispatch.writeClipboard; hosts that don't pass one (the out-of-process renderer) reject every write.

Documented in docs/custom-sidebars.md. Request 2 of the issue (tab icons) isn't in this PR.

Refs #18988

Testing

  • New SidebarJSRuntimeClipboardTests (CmuxSwiftRenderUI): writes from a click handler and from a context-menu item; rejected from load (top level and the sidebar body), from a data-update effect, from a promise queued at load, from a callback captured during a click but run from a later data update, from move/dragChange; 64 KB cap at the boundary; host without a writer rejects. The JS context has no timers, so the deferred-callback cases stand in for the timer case.
  • Text fields: focusLossSubmitIsRejected and returnKeySubmitIsRejected (committed failing first in f2d46688fbed); doubleTapWritesClipboard covers double-tap.
  • The tests are committed first (46bf21feb5e6) and the implementation second. Neither was run locally: per our build rules this Mac doesn't build Swift packages, so the swift-package-tests CI lane is the execution evidence.
  • python3 scripts/verify-local.py (swift-syntax, localization-defaults, package-groups, feature-flags) and scripts/lint-ios-package-conventions.sh pass.
  • Localization audit: no new UI strings. The rejection reasons are developer-facing log diagnostics, like the runtime's existing English error strings.

Changelog

Added: Custom JS sidebars can copy text with clipboard.write(text) from a click or menu item, e.g. for Copy Workspace ID

Proof

CI only; no visual change.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Custom sidebar actions can now copy plain text to the clipboard during eligible click, double-click, and menu-item handlers. Writes are limited to 64 KB and return false when unavailable or called outside an eligible handler.
  • Documentation
    • Updated the custom sidebar guide with clipboard access rules, size limits, and cases where clipboard writes are rejected.