fix(coding-agent): restored windows image paste fallback
Fell back to the PowerShell clipboard bridge when the native Windows clipboard reader reports no image, preserving Ctrl+V image paste in PowerShell terminals.\n\nFixes #2429
This commit is contained in:
@@ -2,6 +2,10 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed Windows PowerShell image paste so Ctrl+V can fall back to the PowerShell clipboard bridge when the native clipboard reader reports no image ([#2429](https://github.com/can1357/oh-my-pi/issues/2429)).
|
||||
|
||||
## [15.12.3] - 2026-06-12
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -66,9 +66,10 @@ export async function copyToClipboard(text: string): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
// PowerShell one-liner that emits the clipboard image as base64-encoded PNG on
|
||||
// stdout, or nothing when the clipboard does not hold image data. Used as the
|
||||
// WSL bridge — arboard cannot read the Windows clipboard through WSLg.
|
||||
// PowerShell one-liner that emits the Windows clipboard image as base64-encoded
|
||||
// PNG on stdout, or nothing when the clipboard does not hold image data. Used
|
||||
// for native Windows fallback and WSL interop because arboard can miss host
|
||||
// clipboard image payloads in those terminal paths.
|
||||
const POWERSHELL_IMAGE_SCRIPT = `
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Add-Type -AssemblyName System.Windows.Forms
|
||||
@@ -84,31 +85,36 @@ if ($img -ne $null) {
|
||||
const POWERSHELL_TIMEOUT_MS = 8000;
|
||||
|
||||
/**
|
||||
* Read a clipboard image through the Windows host's PowerShell.
|
||||
* Read an image through the Windows host's PowerShell.
|
||||
*
|
||||
* WSLg exposes a Wayland socket but no native clipboard image transport, so
|
||||
* `arboard` returns `ContentNotAvailable`. PowerShell, reached via WSL interop,
|
||||
* can read the Windows clipboard directly and round-trip the bitmap as PNG.
|
||||
* Native Windows uses this as a fallback when arboard reports no image or
|
||||
* cannot access the clipboard. WSLg exposes a Wayland socket but no native
|
||||
* clipboard image transport, so arboard returns `ContentNotAvailable` there;
|
||||
* PowerShell, reached via WSL interop, can read the Windows clipboard directly
|
||||
* and round-trip the bitmap as PNG.
|
||||
*
|
||||
* Returns null when no image is on the clipboard, the host PowerShell is
|
||||
* missing, or the bridge times out.
|
||||
*/
|
||||
async function readImageViaPowerShell(): Promise<ClipboardImage | null> {
|
||||
try {
|
||||
const proc = Bun.spawn(["powershell.exe", "-NoProfile", "-NonInteractive", "-Command", POWERSHELL_IMAGE_SCRIPT], {
|
||||
stdout: "pipe",
|
||||
stderr: "ignore",
|
||||
stdin: "ignore",
|
||||
});
|
||||
const proc = Bun.spawn(
|
||||
["powershell.exe", "-NoProfile", "-NonInteractive", "-Sta", "-Command", POWERSHELL_IMAGE_SCRIPT],
|
||||
{
|
||||
stdout: "pipe",
|
||||
stderr: "ignore",
|
||||
stdin: "ignore",
|
||||
},
|
||||
);
|
||||
const timer = setTimeout(() => proc.kill(), POWERSHELL_TIMEOUT_MS);
|
||||
let stdout = "";
|
||||
try {
|
||||
stdout = await new Response(proc.stdout).text();
|
||||
await proc.exited;
|
||||
} catch (err) {
|
||||
// powershell.exe is a Windows process reached over WSL interop; if it
|
||||
// doesn't reap cleanly, swallow the error so the dispatcher can fall
|
||||
// through to the native bridge instead of throwing.
|
||||
// powershell.exe can be a Windows process reached either natively or
|
||||
// over WSL interop; if it doesn't reap cleanly, report no image instead
|
||||
// of surfacing an opaque bridge failure to the prompt.
|
||||
logger.warn("clipboard: powershell read failed", { error: String(err) });
|
||||
return null;
|
||||
} finally {
|
||||
@@ -179,9 +185,10 @@ async function readTextViaPowerShell(): Promise<string | null> {
|
||||
* Read an image from the system clipboard.
|
||||
*
|
||||
* Returns null on Termux (no image clipboard support) or when no display
|
||||
* server is available (headless/SSH without forwarding). Under WSL the
|
||||
* Windows clipboard is reached through `powershell.exe`, since WSLg's
|
||||
* Wayland clipboard does not carry image payloads through to `arboard`.
|
||||
* server is available (headless/SSH without forwarding). Under native Windows
|
||||
* and WSL, the Windows clipboard is also reached through `powershell.exe`
|
||||
* because terminal clipboard paths can leave image payloads invisible to the
|
||||
* native bridge.
|
||||
*
|
||||
* @returns PNG payload or null when no image is available.
|
||||
*/
|
||||
@@ -198,6 +205,16 @@ export async function readImageFromClipboard(): Promise<ClipboardImage | null> {
|
||||
// no display, so arboard would reject anyway.
|
||||
}
|
||||
|
||||
if (process.platform === "win32") {
|
||||
try {
|
||||
const image = await native.readImageFromClipboard();
|
||||
if (image) return image;
|
||||
} catch (err) {
|
||||
logger.warn("clipboard: native Windows image read failed", { error: String(err) });
|
||||
}
|
||||
return await readImageViaPowerShell();
|
||||
}
|
||||
|
||||
if (!hasDisplay()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -147,6 +147,21 @@ describe("readImageFromClipboard dispatch", () => {
|
||||
expect(nativeSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses the PowerShell bridge on native Windows when arboard has no image payload", async () => {
|
||||
setPlatform("win32");
|
||||
const calls: SpawnCall[] = [];
|
||||
spyPowershell(calls, RED_1X1_PNG_BASE64);
|
||||
vi.spyOn(native, "readImageFromClipboard").mockResolvedValue(null);
|
||||
|
||||
const image = await readImageFromClipboard();
|
||||
|
||||
expect(calls).toHaveLength(1);
|
||||
expect(calls[0]?.cmd[0]).toBe("powershell.exe");
|
||||
expect(image?.mimeType).toBe("image/png");
|
||||
expect(Array.from(image!.data.subarray(0, 8))).toEqual([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
||||
expect(calls[0]?.cmd).toContain("-Sta");
|
||||
});
|
||||
|
||||
it("delegates straight to the native bridge on non-WSL linux with a display", async () => {
|
||||
setPlatform("linux");
|
||||
process.env.DISPLAY = ":0";
|
||||
|
||||
Reference in New Issue
Block a user