fix(coding-agent): fix WSL clipboard image paste

closes #2469
This commit is contained in:
Mario Zechner
2026-04-30 23:08:51 +02:00
parent 04527355c5
commit 0b8452c32f
3 changed files with 41 additions and 2 deletions

View File

@@ -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)).

View File

@@ -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;

View File

@@ -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");