From 96bddf81258090690bfcf4e6a6414aa0dcf1910a Mon Sep 17 00:00:00 2001 From: Antony <97451137+TonybynMp4@users.noreply.github.com> Date: Tue, 15 Sep 2026 23:26:36 +0200 Subject: [PATCH] fix(desktop): paste as text no longer doubles the pasted text (#11958) Co-authored-by: Antony Co-authored-by: Claude Opus 5 --- .../src/window/DesktopApplicationMenu.test.ts | 31 +++++++++++++++++++ .../src/window/DesktopApplicationMenu.ts | 12 ++++++- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/window/DesktopApplicationMenu.test.ts b/apps/desktop/src/window/DesktopApplicationMenu.test.ts index bf0c4c3eff6ecf08e9100bd56438e2a6b79b73d1..4b06f5ee510e58b8a53b3ae8c9c1e70f956a809c 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 a90b9ca632317a86654eaa429cc2ef417fa25a76..d3b8db89535274dd6d8973e49e17600bf228754c 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) => () => { -- 2.54.0