diff --git a/packages/tui/src/clipboard.ts b/packages/tui/src/clipboard.ts index 9374fc84417a..36cfa7954f4d 100644 --- a/packages/tui/src/clipboard.ts +++ b/packages/tui/src/clipboard.ts @@ -32,7 +32,11 @@ export function createClipboardAdapter(clipboard: CoreClipboardService): OwnedCl if (result.status === "limit-exceeded") { throw new RangeError("Clipboard content exceeded configured read or image conversion limits") } - return undefined + // WSL keeps the clipboard on the Windows side: Windows Terminal owns the selection and + // the native host clipboard has no bridge to it, so this read fails unless WSLg forwards + // Wayland. Read the Windows clipboard through interop instead. + if (result.status === "cancelled" || !isWSL()) return undefined + return readWindowsClipboard() } if (result.representation.mimeType === "image/png") { @@ -65,3 +69,39 @@ export function createClipboardAdapter(clipboard: CoreClipboardService): OwnedCl }, } } + +const WINDOWS_CLIPBOARD_IMAGE = [ + "Add-Type -AssemblyName System.Windows.Forms", + "$image = [System.Windows.Forms.Clipboard]::GetImage()", + "if (-not $image) { exit 0 }", + "$stream = New-Object System.IO.MemoryStream", + "$image.Save($stream, [System.Drawing.Imaging.ImageFormat]::Png)", + "[System.Convert]::ToBase64String($stream.ToArray())", +].join("; ") + +// Containers on Docker Desktop or devcontainers on Windows also run the WSL kernel but have no +// interop, so detect WSL by the variables it sets instead of the kernel release. +function isWSL() { + return process.platform === "linux" && Boolean(process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP) +} + +async function readWindowsClipboard(): Promise { + if (!Bun.which("powershell.exe")) { + throw new Error("Clipboard image paste under WSL needs powershell.exe interop, which is not available") + } + const child = Bun.spawn(["powershell.exe", "-NonInteractive", "-NoProfile", "-Command", WINDOWS_CLIPBOARD_IMAGE], { + stdin: "ignore", + stdout: "pipe", + stderr: "ignore", + }) + const timeout = setTimeout(() => child.kill(), 10_000) + try { + const output = await new Response(child.stdout).text() + if ((await child.exited) !== 0) throw new Error("Reading the Windows clipboard image failed") + const data = output.trim() + if (!data) return undefined + return { data, mime: "image/png" } + } finally { + clearTimeout(timeout) + } +}