diff --git a/apps/desktop/src/window/DesktopApplicationMenu.test.ts b/apps/desktop/src/window/DesktopApplicationMenu.test.ts index bf0c4c3eff6e..4b06f5ee510e 100644 --- a/apps/desktop/src/window/DesktopApplicationMenu.test.ts +++ b/apps/desktop/src/window/DesktopApplicationMenu.test.ts @@ -183,6 +183,37 @@ describe("DesktopApplicationMenu", () => { }), ); + // Chromium pastes as plain text for the accelerator on its own. Dispatching + // the action as well injects a second paste, which doubles the pasted text. + it.effect("leaves the accelerator to Chromium instead of injecting a paste", () => + Effect.gen(function* () { + const selectedAction = yield* Deferred.make(); + const applicationMenuTemplate = + yield* Deferred.make(); + + yield* configureMenu(selectedAction, applicationMenuTemplate); + + const template = yield* Deferred.await(applicationMenuTemplate); + const editMenu = template.find((item) => item.label === "Edit"); + if (!Array.isArray(editMenu?.submenu)) { + throw new Error("Expected Edit menu submenu to be an array."); + } + const pasteAsTextItem = editMenu.submenu.find((item) => item.label === "Paste as Text"); + if (typeof pasteAsTextItem?.click !== "function") { + throw new Error("Expected Paste as Text menu item to have a click handler."); + } + + pasteAsTextItem.click( + {} as Electron.MenuItem, + {} as Electron.BrowserWindow, + { + triggeredByAccelerator: true, + } as unknown as KeyboardEvent, + ); + assert.isFalse(yield* Deferred.isDone(selectedAction)); + }), + ); + // Zoom must route through DesktopWindow.zoomMain instead of the Electron // zoom roles: the roles zoom whichever webContents has focus, which breaks // app zoom while an embedded preview WebContentsView holds focus. diff --git a/apps/desktop/src/window/DesktopApplicationMenu.ts b/apps/desktop/src/window/DesktopApplicationMenu.ts index a90b9ca63231..d3b8db895352 100644 --- a/apps/desktop/src/window/DesktopApplicationMenu.ts +++ b/apps/desktop/src/window/DesktopApplicationMenu.ts @@ -137,7 +137,17 @@ export const make = Effect.gen(function* () { const settingsClick = () => { runMenuEffect("open-settings", dispatchMenuAction("open-settings")); }; - const pasteAsTextClick = () => { + // Chromium already pastes as plain text for this chord, so the accelerator + // needs nothing from the menu: the composer and the terminal each arm + // themselves from the same keydown. Routing it through the renderer anyway + // lands a second, injected paste and doubles the text. Only a menu click, + // which produces no keystroke for them to see, needs that round trip. + const pasteAsTextClick = ( + _item: Electron.MenuItem, + _window: Electron.BaseWindow | undefined, + event: Electron.KeyboardEvent, + ) => { + if (event.triggeredByAccelerator === true) return; runMenuEffect("paste-as-text", dispatchMenuAction("paste-as-text")); }; const zoomClick = (direction: DesktopWindow.MainWindowZoomDirection) => () => {