fix(desktop): make Edit menu Cut/Copy work off macOS - #1336
Conversation
winc fires a menu accelerator from WM_KEYDOWN without consuming the key, so Paste's explicit callback ran on top of the webview's own native paste and the clipboard landed twice. macOS still needs the accelerator, where it does consume the event and is the only path that reaches Monaco. Fixes skyhook-io#1276
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want higher recall? High effort reviews run extra passes and find more bugs. A team admin can switch effort levels in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 1c62dc5. Configure here.
| editMenu.AddText("Copy", keys.CmdOrCtrl("c"), nil) | ||
| editMenu.AddText("Paste", keys.CmdOrCtrl("v"), func(_ *menu.CallbackData) { | ||
| editMenu.AddText("Cut", keys.CmdOrCtrl("x"), clipboardDelegate(goruntime.GOOS, desktopApp, "cut")) | ||
| editMenu.AddText("Copy", keys.CmdOrCtrl("c"), clipboardDelegate(goruntime.GOOS, desktopApp, "copy")) |
There was a problem hiding this comment.
Cut/Copy steal Linux Ctrl+C
High Severity
Giving Cut/Copy non-nil callbacks while keeping the Ctrl+C/Ctrl+X accelerators newly binds those chords in the native menu. On Linux, GTK accelerators consume the key before it reaches the webview, so Ctrl+C in a pod or local terminal no longer delivers SIGINT. The same hijack risk already drove the Reload accelerator change, and terminalClipboard intentionally leaves Ctrl+C alone for that reason.
Reviewed by Cursor Bugbot for commit 1c62dc5. Configure here.
There was a problem hiding this comment.
I don't think this one is right. The chord is not newly bound by this change.
In wails/v2@v2.12.0/internal/frontend/desktop/linux/menu.go, processMenuItem sets the callback and the accelerator in two separate blocks:
if menuItem.Click != nil {
handler := C.connectClick(result)
...
}
if menuItem.Accelerator != nil {
key, mods := acceleratorToGTK(menuItem.Accelerator)
C.addAccelerator(result, group, key, mods)
}Cut/Copy already passed keys.CmdOrCtrl("x") and ("c") on main, so the GTK accel entries were already there before this PR. This change only adds the Click handler. It does not touch Accelerator.
The real question is whether connecting an "activate" handler changes key consumption. That would make your point work. It does not. In GTK 3.24 the closure behind gtk_widget_add_accelerator is:
static void
closure_accel_activate (GClosure *closure, GValue *return_value, ...)
{
gboolean can_activate = gtk_widget_can_activate_accel (closure->data, aclosure->signal_id);
if (can_activate)
g_signal_emit (closure->data, aclosure->signal_id, 0);
g_value_set_boolean (return_value, can_activate);
}The "handled" result comes from gtk_widget_can_activate_accel, so sensitivity and visibility, never from whether a handler is connected. Emitting "activate" with no handler still counts as handled, and there is no fall through. gtk_widget_add_accelerator also rejects signals that have a return type. On top of that, gtkwindow.c runs gtk_window_activate_key before gtk_window_propagate_key_event, so an accel entry beats the focused webview anyway.
So on Linux main, Ctrl+C and Ctrl+X were already swallowed by the menubar and never reached the webview. They just did nothing, because no handler was connected. After this change the same keypress actually copies or cuts. That is a fix, not a new hijack.
About the terminalClipboard reference: the "Ctrl+C stays SIGINT" comment sits inside the macOS only branch. That attachCustomKeyEventHandler is gated on mac and matches metaKey && !ctrlKey. It is about JS on macOS and says nothing about Linux.
The double insert is specific to winc on Windows, which fires the menu action from WM_KEYDOWN and still forwards the key to the webview. AppKit and GTK consume the accelerator, so there it is the only paste path — dropping it would hand Monaco to native webview behavior that hasn't been verified on either.
A nil menu callback only delegates to a responder chain on macOS; on Windows and Linux Wails binds no handler at all, so clicking Edit -> Cut or Copy did nothing. Route those two through execCommand, which main.tsx already intercepts for Monaco's virtual selection.
1c62dc5 to
17da660
Compare


Description
On Windows and Linux, Edit → Cut and Edit → Copy do nothing when clicked.
Both items are registered with a
nilcallback:That is correct on macOS, where
nildelegates to the native responder chain. Off macOS there is nothing to delegate to — Wails only binds a handler whenClick != nil(windows/menu.go), and firing an unbound event is a no-op (winc/eventmanager.go), so the entries are wired to nothing.Ctrl+C/Ctrl+Xwere never broken:web/src/main.tsxhas its ownkeydownlistener that checkse.ctrlKey, so the keyboard path already worked. Only the menu items were dead.Fix: off macOS, give the two items a callback that calls
document.execCommand('cut'|'copy').main.tsxalready monkey-patchesexecCommandand routes those tohandleCopyOrCut, so Monaco's virtual selection is handled the same way as the existing right-click path. macOS keeps itsnildelegation untouched.The
Ctrl+X/Ctrl+Caccelerators are kept, so the menu still shows the hints. Adding a callback does mean the accelerator now fires it as well, so the keyboard path reacheshandleCopyOrCuttwice. That is safe: JS is single-threaded, so the two runs are sequential rather than interleaved, and each one re-reads the selection. Copy writes the same text twice. The first Cut collapses the selection, so the second returns atif (!text) returnbefore deleting anything — there is no state in which a second delete has something to remove.This is why the accelerator is kept here but dropped for Paste. Paste doesn't read the document, it inserts the clipboard, so a second run always has something to do and the duplicate is visible.
Type of change
How has this been tested?
Environment: Windows 11 (26200), Go 1.26.1, Node 24.13.1, Wails v2.12.0.
Tested locally with minikube/kind
Tested against a remote cluster
Added/updated unit tests
Added
TestClipboardDelegateSkipsOnlyMacandTestCreateMenuCutCopyAreClickableOffMac. The wiring test fails against the unfixed code withCut has no callback on windows — the menu entry would be inert(and the same for Copy), and passes with the fix. It skips ondarwin, wherenilis correct.go build ./...— clean.go test ./cmd/desktop/— pass.go test ./...—internal/serverandinternal/timelinefail identically on unmodifiedmain, so they are pre-existing and unrelated.cd web && npm run tsc— pass (no frontend files touched).gofmt/go vet ./cmd/desktop/— clean.Manual check on a
wails build -platform windows/amd64binary from this branch: Edit → Copy and Edit → Cut now work (they did nothing before), andCtrl+C/Ctrl+Xstill behave correctly.Not tested on macOS or Linux — I don't have access to either. The change is scoped to non-
darwin, so macOS behavior is unchanged by construction.Checklist
Related issues
Fixes #1334
Stacked on #1335 (the Paste double-insert fix) — both touch the Edit menu clipboard items, so this branch includes that commit. Please merge that one first, or say the word and I'll rebase this onto
mainas a standalone change.Note
Low Risk
Desktop menu wiring only; behavior is scoped by OS with tests and no auth or data-path changes.
Overview
Edit → Cut/Copy on Windows and Linux now run
document.execCommand('cut'|'copy')via a newclipboardDelegatehelper; macOS still usesnilcallbacks for the native responder chain.The same change set wires Paste through
pasteAccelerator, which omits the Ctrl+V menu accelerator on Windows only so Wails does not paste twice (menu callback + webview). Comments inmenu.godocument the platform split.Unit tests lock in
pasteAccelerator,clipboardDelegate, and non-mac Cut/Copy menu callbacks.Reviewed by Cursor Bugbot for commit 17da660. Bugbot is set up for automated code reviews on this repo. Configure here.