Custom sidebars: gesture-gated, write-only clipboard.write
Merged
TE
1mo ago+344+344+012345678901234567890123456789-9-9-0123456789
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.dispatchEventopens a user-activation scope fortap(which includes context-menu items) anddoubletaponly, and closes it when that synchronous JS dispatch returns.clipboard.writeis honored only inside it. Text-fieldsubmitdoesn'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 returnfalse, write nothing, and emitclipboard.write rejected: <reason>on thelogchannel (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 optionalSidebarActionDispatch.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, frommove/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:
focusLossSubmitIsRejectedandreturnKeySubmitIsRejected(committed failing first in f2d46688fbed);doubleTapWritesClipboardcovers 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-testsCI lane is the execution evidence. python3 scripts/verify-local.py(swift-syntax, localization-defaults, package-groups, feature-flags) andscripts/lint-ios-package-conventions.shpass.- 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
falsewhen unavailable or called outside an eligible handler.
- 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
- Documentation
- Updated the custom sidebar guide with clipboard access rules, size limits, and cases where clipboard writes are rejected.