authorgravatar for 97451137+TonybynMp4@users.noreply.github.comAntony <97451137+TonybynMp4@users.noreply.github.com> 2026-09-15 23:26:36+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-15 21:26:36+00:00
log96bddf81258090690bfcf4e6a6414aa0dcf1910a
tree9127086350a3c79d6f379d7fa38020ae7e40e0cb
parentb18a560bbac70c7166677d7f3bb51ecaf42acf08
signature Signed by PGP key B5690EEEBB952194

fix(desktop): paste as text no longer doubles the pasted text (#11958)

Co-authored-by: Antony <tnybyn@gmail.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

2 files changed, 42 insertions(+), 1 deletions(-)

apps/desktop/src/window/DesktopApplicationMenu.test.ts+31
...@@ -183,6 +183,37 @@ describe("DesktopApplicationMenu", () => {...@@ -183,6 +183,37 @@ describe("DesktopApplicationMenu", () => {
183 }),183 }),
184 );184 );
185185
186 // Chromium pastes as plain text for the accelerator on its own. Dispatching
187 // the action as well injects a second paste, which doubles the pasted text.
188 it.effect("leaves the accelerator to Chromium instead of injecting a paste", () =>
189 Effect.gen(function* () {
190 const selectedAction = yield* Deferred.make<string>();
191 const applicationMenuTemplate =
192 yield* Deferred.make<readonly Electron.MenuItemConstructorOptions[]>();
193
194 yield* configureMenu(selectedAction, applicationMenuTemplate);
195
196 const template = yield* Deferred.await(applicationMenuTemplate);
197 const editMenu = template.find((item) => item.label === "Edit");
198 if (!Array.isArray(editMenu?.submenu)) {
199 throw new Error("Expected Edit menu submenu to be an array.");
200 }
201 const pasteAsTextItem = editMenu.submenu.find((item) => item.label === "Paste as Text");
202 if (typeof pasteAsTextItem?.click !== "function") {
203 throw new Error("Expected Paste as Text menu item to have a click handler.");
204 }
205
206 pasteAsTextItem.click(
207 {} as Electron.MenuItem,
208 {} as Electron.BrowserWindow,
209 {
210 triggeredByAccelerator: true,
211 } as unknown as KeyboardEvent,
212 );
213 assert.isFalse(yield* Deferred.isDone(selectedAction));
214 }),
215 );
216
186 // Zoom must route through DesktopWindow.zoomMain instead of the Electron217 // Zoom must route through DesktopWindow.zoomMain instead of the Electron
187 // zoom roles: the roles zoom whichever webContents has focus, which breaks218 // zoom roles: the roles zoom whichever webContents has focus, which breaks
188 // app zoom while an embedded preview WebContentsView holds focus.219 // app zoom while an embedded preview WebContentsView holds focus.
apps/desktop/src/window/DesktopApplicationMenu.ts+11-1
...@@ -137,7 +137,17 @@ export const make = Effect.gen(function* () {...@@ -137,7 +137,17 @@ export const make = Effect.gen(function* () {
137 const settingsClick = () => {137 const settingsClick = () => {
138 runMenuEffect("open-settings", dispatchMenuAction("open-settings"));138 runMenuEffect("open-settings", dispatchMenuAction("open-settings"));
139 };139 };
140 const pasteAsTextClick = () => {140 // Chromium already pastes as plain text for this chord, so the accelerator
141 // needs nothing from the menu: the composer and the terminal each arm
142 // themselves from the same keydown. Routing it through the renderer anyway
143 // lands a second, injected paste and doubles the text. Only a menu click,
144 // which produces no keystroke for them to see, needs that round trip.
145 const pasteAsTextClick = (
146 _item: Electron.MenuItem,
147 _window: Electron.BaseWindow | undefined,
148 event: Electron.KeyboardEvent,
149 ) => {
150 if (event.triggeredByAccelerator === true) return;
141 runMenuEffect("paste-as-text", dispatchMenuAction("paste-as-text"));151 runMenuEffect("paste-as-text", dispatchMenuAction("paste-as-text"));
142 };152 };
143 const zoomClick = (direction: DesktopWindow.MainWindowZoomDirection) => () => {153 const zoomClick = (direction: DesktopWindow.MainWindowZoomDirection) => () => {