From 0b8452c32f1aed2c74aa447ae7028de4d926a033 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Thu, 30 Apr 2026 23:08:51 +0200 Subject: [PATCH] fix(coding-agent): fix WSL clipboard image paste closes #2469 --- packages/coding-agent/CHANGELOG.md | 1 + .../coding-agent/src/utils/clipboard-image.ts | 4 +- .../coding-agent/test/clipboard-image.test.ts | 38 +++++++++++++++++++ 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 96b9bad7..2a24a735 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -22,6 +22,7 @@ ### Fixed +- Fixed WSL clipboard image paste by passing the PowerShell save path directly instead of through a custom environment variable ([#2469](https://github.com/badlogic/pi-mono/issues/2469)). - Fixed Google Vertex Gemini 3 tool call replay for unsigned tool calls ([#4032](https://github.com/badlogic/pi-mono/issues/4032)). - Fixed blocked `edit` tool results rendering the rejection reason twice after interactive extension confirmation ([#3830](https://github.com/badlogic/pi-mono/issues/3830)). - Fixed extension-triggered thinking level changes refreshing the interactive editor border immediately ([#3888](https://github.com/badlogic/pi-mono/issues/3888)). diff --git a/packages/coding-agent/src/utils/clipboard-image.ts b/packages/coding-agent/src/utils/clipboard-image.ts index 6bddeb30..4cf44908 100644 --- a/packages/coding-agent/src/utils/clipboard-image.ts +++ b/packages/coding-agent/src/utils/clipboard-image.ts @@ -172,17 +172,17 @@ function readClipboardImageViaPowerShell(): ClipboardImage | null { return null; } + const psQuotedWinPath = winPath.replaceAll("'", "''"); const psScript = [ "Add-Type -AssemblyName System.Windows.Forms", "Add-Type -AssemblyName System.Drawing", - "$path = $env:PI_WSL_CLIPBOARD_IMAGE_PATH", + `$path = '${psQuotedWinPath}'`, "$img = [System.Windows.Forms.Clipboard]::GetImage()", "if ($img) { $img.Save($path, [System.Drawing.Imaging.ImageFormat]::Png); Write-Output 'ok' } else { Write-Output 'empty' }", ].join("; "); const result = runCommand("powershell.exe", ["-NoProfile", "-Command", psScript], { timeoutMs: DEFAULT_POWERSHELL_TIMEOUT_MS, - env: { ...process.env, PI_WSL_CLIPBOARD_IMAGE_PATH: winPath }, }); if (!result.ok) { return null; diff --git a/packages/coding-agent/test/clipboard-image.test.ts b/packages/coding-agent/test/clipboard-image.test.ts index ad2ba5de..eda61a29 100644 --- a/packages/coding-agent/test/clipboard-image.test.ts +++ b/packages/coding-agent/test/clipboard-image.test.ts @@ -1,4 +1,5 @@ import type { SpawnSyncReturns } from "child_process"; +import { writeFileSync } from "fs"; import { beforeEach, describe, expect, test, vi } from "vitest"; const mocks = vi.hoisted(() => { @@ -107,6 +108,43 @@ describe("readClipboardImage", () => { expect(Array.from(result?.bytes ?? [])).toEqual([9, 8]); }); + test("WSL: passes PowerShell path directly instead of through a custom env var", async () => { + mocks.clipboard.hasImage.mockImplementation(() => { + throw new Error("clipboard.hasImage should not be called before PowerShell on WSL"); + }); + + let tmpFile: string | undefined; + mocks.spawnSync.mockImplementation((command, args, options) => { + if (command === "wl-paste" || command === "xclip") { + return spawnOk(Buffer.alloc(0)); + } + + if (command === "wslpath") { + tmpFile = args[1]; + return spawnOk(Buffer.from("C:\\Users\\O'Hare\\clip.png\n", "utf-8")); + } + + if (command === "powershell.exe") { + const spawnOptions = options as { env?: NodeJS.ProcessEnv }; + expect(spawnOptions.env?.PI_WSL_CLIPBOARD_IMAGE_PATH).toBeUndefined(); + expect(args[2]).toContain("$path = 'C:\\Users\\O''Hare\\clip.png'"); + if (!tmpFile) { + throw new Error("wslpath should be called before powershell.exe"); + } + writeFileSync(tmpFile, Buffer.from([4, 5, 6])); + return spawnOk(Buffer.from("ok\n", "utf-8")); + } + + throw new Error(`Unexpected spawnSync call: ${command} ${args.join(" ")}`); + }); + + const { readClipboardImage } = await import("../src/utils/clipboard-image.js"); + const result = await readClipboardImage({ platform: "linux", env: { WSL_DISTRO_NAME: "Ubuntu" } }); + expect(result).not.toBeNull(); + expect(result?.mimeType).toBe("image/png"); + expect(Array.from(result?.bytes ?? [])).toEqual([4, 5, 6]); + }); + test("Non-Wayland: uses clipboard", async () => { mocks.spawnSync.mockImplementation(() => { throw new Error("spawnSync should not be called for non-Wayland sessions");