Skip to content

fix(tui): read clipboard images through Windows interop on WSL - #57470

Open
AlexeyShirshov wants to merge 3 commits into
anomalyco:devfrom
AlexeyShirshov:wsl-image-paste
Open

AlexeyShirshov wants to merge 3 commits into
anomalyco:devfrom
AlexeyShirshov:wsl-image-paste

Conversation

@AlexeyShirshov

@AlexeyShirshov AlexeyShirshov commented Oct 20, 2026 •

Copy link
Copy Markdown

Issue for this PR

Closes #49895

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

On WSL the clipboard belongs to Windows: Windows Terminal owns the selection, and OpenTUI's host clipboard can only read it when WSLg forwards Wayland to wl-paste (or another Linux clipboard tool is installed). Without that, read() reports empty/cancelled, or unsupported - which since #61094 only turns into an error. Either way Ctrl+V with an image did nothing: no attachment and no message.

createClipboardAdapter now falls back to the Windows clipboard through powershell.exe when the native read has no representation and the platform is WSL. The image comes back as image/png base64, the same interop path V1 used. If the interop is missing or PowerShell fails, read() throws instead of returning undefined, so the TUI shows an error instead of swallowing the paste.

Scope of the change:

  • Only a read with no representation falls back. A successful native read is returned as before, so a machine with WSLg never spawns PowerShell.
  • On WSL every non-error status falls back (empty, cancelled, unsupported). Without WSLg the host reports the clipboard as unsupported rather than empty (fix(tui): show an error when Ctrl+V can't read the clipboard #61094 added that status), and it is the only status a plain WSL2 + Windows Terminal setup produces, so a narrower condition would make the fallback unreachable.
  • Non-WSL platforms are unchanged: unsupported still throws "Clipboard read is not supported on this system". The same error is kept on WSL when the interop route returns no image, so fix(tui): show an error when Ctrl+V can't read the clipboard #61094's intent survives the fallback.
  • WSL is detected by WSL_DISTRO_NAME/WSL_INTEROP, not by the kernel release, so containers on Docker Desktop or devcontainers (same kernel, no interop) keep their previous behaviour.
  • Text paste is untouched: it still travels through the terminal.
Setup Native read Before After
WSL2 with WSLg and wl-paste present image/png image attaches unchanged, the fallback never runs
WSL2 without WSLg, powershell.exe interop available (the setup in #49895: plain WSL2 + Windows Terminal) empty / unsupported Ctrl+V does nothing (empty), or reports "not supported" (unsupported) image attaches
WSL2 without interop (a container on the WSL kernel) empty / unsupported Ctrl+V does nothing unchanged
macOS, Windows, native Linux image/png / empty works or stays silent unchanged

The fix is only reachable on WSL installations whose clipboard has no native route. It does not help WSL with interop disabled (/etc/wsl.conf, [interop] enabled=false): there the read throws a message naming the missing interop.

How did you verify your code works?

On WSL2 (kernel 6.18.33.2-microsoft-standard-WSL2):

  • With WSLg present, where wl-paste is available: the native read already returns the 29 KB screenshot as image/png and the fallback is not reached. Unpatched dev behaves the same here.
  • With Wayland/X11 unset and wl-paste removed from PATH, but powershell.exe kept on PATH (the tui: image paste fails silently on WSL when no clipboard tool is installed (V2) #49895 setup): unpatched dev returns undefined and Ctrl+V does nothing, while this branch returns the same screenshot as image/png base64 and the TUI attaches it.
  • The status the host reports in that setup, read straight from @opentui/core on dev at eb670f1e98, is unsupported - not empty - so the fallback is wired to every non-error status rather than to empty alone.
  • Empty and image-free clipboards still return undefined, and text paste still goes through the terminal as before.
  • bun test test/clipboard.test.ts passes in both environments: 6 pass / 2 skip where the interop exists, 8 pass with powershell.exe removed from PATH (the Linux CI case). The tests pin WSL_DISTRO_NAME/WSL_INTEROP themselves, so running the suite on a WSL host no longer reads that machine's real clipboard instead of the stubbed read; the unsupported test from fix(tui): show an error when Ctrl+V can't read the clipboard #61094 is pinned the same way, because on a WSL host that status now reaches the interop.
  • bun typecheck and oxlint pass in packages/tui.

The first fallback read can take a few seconds, because PowerShell has to load System.Windows.Forms; later reads take about 0.5 s. The spawn is bounded by a 10 s timeout.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

@github-actions

Copy link
Copy Markdown
Contributor

The following comment was made by an LLM, it may be inaccurate:

@AlexeyShirshov
AlexeyShirshov marked this pull request as ready for review October 20, 2026 18:20

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fallback looks correct, but I couldn't check it on real WSL: this sandbox has no Windows interop or Windows clipboard. Instead I put a fake powershell.exe on PATH and called createClipboardAdapter with a stubbed native read. An image comes back as image/png base64 and an empty Windows clipboard returns nothing. A failing or missing powershell.exe now throws, and the prompt's paste queue catches that and shows it, instead of the paste silently doing nothing. Cancelled reads and non-WSL systems are unchanged. bun test test/clipboard.test.ts and the packages/tui typecheck pass.

Public API: unchanged.

One thing worth fixing: WSL detection also matches Docker Desktop containers and devcontainers on Windows, where the error message would be misleading (see the inline comment).

Related: #55639

Comment thread packages/tui/src/clipboard.ts Outdated
].join("; ")

function isWSL() {
return process.platform === "linux" && (Boolean(process.env.WSL_DISTRO_NAME) || release().includes("WSL"))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

release().includes("WSL") also matches containers on Docker Desktop and devcontainers on Windows: they run the microsoft-standard-WSL2 kernel but have no Windows interop. There, pressing Ctrl+V with nothing readable on the clipboard now throws "needs powershell.exe interop" where it used to be a silent no-op, and the message is misleading. Checking only what WSL itself sets (WSL_DISTRO_NAME / WSL_INTEROP) covers WSL1 and WSL2 and avoids this, since those variables don't reach containers.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 1052174: detection now only checks the variables WSL sets (WSL_DISTRO_NAME / WSL_INTEROP), so containers on the WSL kernel keep the previous silent no-op instead of throwing a misleading interop error. Verified both paths here: with the variables set the Windows clipboard image comes back as image/png; with them unset the read returns undefined.

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The WSL detection point is addressed. isWSL() now checks only WSL_DISTRO_NAME/WSL_INTEROP, so containers that share the WSL kernel no longer take the PowerShell path. I checked it the same way as before (stand-in powershell.exe, stubbed native read). Either variable enables the fallback. With neither set, the read returns nothing and throws no interop error. bun test test/clipboard.test.ts and tsc -b pass. I still couldn't test on real WSL.

Public API: unchanged.

Nothing new to raise.

@AlexeyShirshov

AlexeyShirshov commented Nov 4, 2026 •

Copy link
Copy Markdown
Author

Branch rebuilt on today's dev (5a750e64a2); the PR contains only the two files it should. Commits: c0de91d8a3 (fallback), e2e0da7076 (test for the read without a representation), c0bb780cdd (test fix).

The previous update left the branch 50 commits behind dev. Refreshing it by merging the fork's dev branch (which has not been synced with upstream since Oct 28) pulled in fork-only CI commits instead, so the history was rewritten on top of upstream dev rather than merged.

The clipboard code is unchanged since the last review; only the tests changed.

The test fix matters on WSL: returns undefined when the host clipboard has no representation used to inherit the variables from the machine it ran on, so on a WSL host the fallback ran, spawned powershell.exe and read the real Windows clipboard - it timed out at 5 s on a cold Add-Type and then failed the assertion. Both WSL tests now pin WSL_DISTRO_NAME/WSL_INTEROP via a withWSL helper and restore them afterwards, and the missing-interop assertion is awaited.

Verified on WSL2 (6.18.33.2-microsoft-standard-WSL2):

  • WSLg present (wl-paste available): the native read returns the image and the fallback is never reached, before and after this branch.
  • Wayland/X11 unset, wl-paste off PATH, powershell.exe on PATH: dev returns undefined, this branch returns image/png base64.
  • bun test test/clipboard.test.ts: 5 pass / 1 skip with interop, 6 pass without powershell.exe on PATH; bun typecheck and oxlint clean in packages/tui.

The native host clipboard cannot read the Windows selection from WSL, so
pasting a screenshot did nothing. Fall back to powershell.exe and report
interop failures instead of returning undefined.
Assert the read stays silent outside WSL and that a WSL read without
powershell.exe interop reports the failure instead of returning undefined.
@AlexeyShirshov

Copy link
Copy Markdown
Author

Rebased onto dev at eb670f1e98 (596 commits since the previous base 5a750e64a2).

The rebase needed one real change: #61094 (merged 2026-11-05) added a throw for result.status === "unsupported" in createClipboardAdapter, ahead of the WSL fallback. Measured on WSL2 with DISPLAY/WAYLAND_DISPLAY unset and no wl-paste on PATH, OpenTUI now reports exactly that status:

$ WSL_DISTRO_NAME=Ubuntu PATH=<powershell.exe only> env -u DISPLAY -u WAYLAND_DISPLAY bun run probe.ts
{"display":null,"wayland":null,"status":"unsupported"}

With the throw first, the interop path became unreachable in the only configuration this PR targets. So packages/tui/src/clipboard.ts now routes every non-error status (empty, cancelled, unsupported) through readWindowsClipboard() on WSL, and keeps #61094 behaviour intact everywhere else: the "Clipboard read is not supported on this system" error is still thrown when the interop route returns no image, and for every non-WSL host.

Tests: the new upstream test that asserts the unsupported error now pins the non-WSL variables with the existing withWSL helper (it would otherwise spawn powershell.exe on a WSL developer machine and assert the wrong thing), plus one added case covering unsupported inside WSL.

  • packages/tui: bun test test/clipboard.test.ts — 8 pass with powershell.exe off PATH (CI shape), 6 pass / 2 skip on this WSL2 box.
  • bun typecheck — clean.

@github-actions github-actions Bot added needs:compliance This means the issue will auto-close after 2 hours. and removed needs:compliance This means the issue will auto-close after 2 hours. labels Nov 11, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Thanks for updating your PR! It now meets our contributing guidelines. 👍

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

tui: image paste fails silently on WSL when no clipboard tool is installed (V2)

1 participant