fix(coding-agent): handle WSL clipboard image fallback\n\nCloses #1722
This commit is contained in:
@@ -1,4 +1,8 @@
|
||||
import { spawnSync } from "child_process";
|
||||
import { randomUUID } from "crypto";
|
||||
import { readFileSync, unlinkSync } from "fs";
|
||||
import { tmpdir } from "os";
|
||||
import { join } from "path";
|
||||
|
||||
import { clipboard } from "./clipboard-native.js";
|
||||
import { loadPhoton } from "./photon.js";
|
||||
@@ -12,6 +16,7 @@ const SUPPORTED_IMAGE_MIME_TYPES = ["image/png", "image/jpeg", "image/webp", "im
|
||||
|
||||
const DEFAULT_LIST_TIMEOUT_MS = 1000;
|
||||
const DEFAULT_READ_TIMEOUT_MS = 3000;
|
||||
const DEFAULT_POWERSHELL_TIMEOUT_MS = 5000;
|
||||
const DEFAULT_MAX_BUFFER_BYTES = 50 * 1024 * 1024;
|
||||
|
||||
export function isWaylandSession(env: NodeJS.ProcessEnv = process.env): boolean {
|
||||
@@ -84,7 +89,7 @@ async function convertToPng(bytes: Uint8Array): Promise<Uint8Array | null> {
|
||||
function runCommand(
|
||||
command: string,
|
||||
args: string[],
|
||||
options?: { timeoutMs?: number; maxBufferBytes?: number },
|
||||
options?: { timeoutMs?: number; maxBufferBytes?: number; env?: NodeJS.ProcessEnv },
|
||||
): { stdout: Buffer; ok: boolean } {
|
||||
const timeoutMs = options?.timeoutMs ?? DEFAULT_READ_TIMEOUT_MS;
|
||||
const maxBufferBytes = options?.maxBufferBytes ?? DEFAULT_MAX_BUFFER_BYTES;
|
||||
@@ -92,6 +97,7 @@ function runCommand(
|
||||
const result = spawnSync(command, args, {
|
||||
timeout: timeoutMs,
|
||||
maxBuffer: maxBufferBytes,
|
||||
env: options?.env,
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
@@ -134,6 +140,76 @@ function readClipboardImageViaWlPaste(): ClipboardImage | null {
|
||||
return { bytes: data.stdout, mimeType: baseMimeType(selectedType) };
|
||||
}
|
||||
|
||||
function isWSL(env: NodeJS.ProcessEnv = process.env): boolean {
|
||||
if (env.WSL_DISTRO_NAME || env.WSLENV) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
const release = readFileSync("/proc/version", "utf-8");
|
||||
return /microsoft|wsl/i.test(release);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* On WSL, the Linux clipboard (Wayland/X11) does not receive image data from
|
||||
* Windows screenshots (Win+Shift+S). PowerShell can access the Windows clipboard
|
||||
* directly, so we use it as a fallback.
|
||||
*/
|
||||
function readClipboardImageViaPowerShell(): ClipboardImage | null {
|
||||
const tmpFile = join(tmpdir(), `pi-wsl-clip-${randomUUID()}.png`);
|
||||
|
||||
try {
|
||||
const winPathResult = runCommand("wslpath", ["-w", tmpFile], { timeoutMs: DEFAULT_LIST_TIMEOUT_MS });
|
||||
if (!winPathResult.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const winPath = winPathResult.stdout.toString("utf-8").trim();
|
||||
if (!winPath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const psScript = [
|
||||
"Add-Type -AssemblyName System.Windows.Forms",
|
||||
"Add-Type -AssemblyName System.Drawing",
|
||||
"$path = $env:PI_WSL_CLIPBOARD_IMAGE_PATH",
|
||||
"$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;
|
||||
}
|
||||
|
||||
const output = result.stdout.toString("utf-8").trim();
|
||||
if (output !== "ok") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const bytes = readFileSync(tmpFile);
|
||||
if (bytes.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { bytes: new Uint8Array(bytes), mimeType: "image/png" };
|
||||
} catch {
|
||||
return null;
|
||||
} finally {
|
||||
try {
|
||||
unlinkSync(tmpFile);
|
||||
} catch {
|
||||
// Ignore cleanup errors.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function readClipboardImageViaXclip(): ClipboardImage | null {
|
||||
const targets = runCommand("xclip", ["-selection", "clipboard", "-t", "TARGETS", "-o"], {
|
||||
timeoutMs: DEFAULT_LIST_TIMEOUT_MS,
|
||||
@@ -161,6 +237,20 @@ function readClipboardImageViaXclip(): ClipboardImage | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
async function readClipboardImageViaNativeClipboard(): Promise<ClipboardImage | null> {
|
||||
if (!clipboard || !clipboard.hasImage()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const imageData = await clipboard.getImageBinary();
|
||||
if (!imageData || imageData.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const bytes = imageData instanceof Uint8Array ? imageData : Uint8Array.from(imageData);
|
||||
return { bytes, mimeType: "image/png" };
|
||||
}
|
||||
|
||||
export async function readClipboardImage(options?: {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
platform?: NodeJS.Platform;
|
||||
@@ -174,20 +264,23 @@ export async function readClipboardImage(options?: {
|
||||
|
||||
let image: ClipboardImage | null = null;
|
||||
|
||||
if (platform === "linux" && isWaylandSession(env)) {
|
||||
image = readClipboardImageViaWlPaste() ?? readClipboardImageViaXclip();
|
||||
if (platform === "linux") {
|
||||
const wsl = isWSL(env);
|
||||
const wayland = isWaylandSession(env);
|
||||
|
||||
if (wayland || wsl) {
|
||||
image = readClipboardImageViaWlPaste() ?? readClipboardImageViaXclip();
|
||||
}
|
||||
|
||||
if (!image && wsl) {
|
||||
image = readClipboardImageViaPowerShell();
|
||||
}
|
||||
|
||||
if (!image && !wayland) {
|
||||
image = await readClipboardImageViaNativeClipboard();
|
||||
}
|
||||
} else {
|
||||
if (!clipboard || !clipboard.hasImage()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const imageData = await clipboard.getImageBinary();
|
||||
if (!imageData || imageData.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const bytes = imageData instanceof Uint8Array ? imageData : Uint8Array.from(imageData);
|
||||
image = { bytes, mimeType: "image/png" };
|
||||
image = await readClipboardImageViaNativeClipboard();
|
||||
}
|
||||
|
||||
if (!image) {
|
||||
|
||||
Reference in New Issue
Block a user