fix(coding-agent): read Termux clipboard on Android platform

Termux reports process.platform as android, so the Linux-only branch skipped
termux-clipboard-get and Ctrl+V pasted nothing. Also show the Termux:API hint
when a Termux copy fails.

fixes #10391
This commit is contained in:
David Brailovsky 2026-10-07 14:35:48 +02:00
parent adae824645
commit 592fb57b70
3 changed files with 41 additions and 10 deletions

View file

@ -24,6 +24,7 @@
- Fixed codemode output items running together, so models could not tell where one `text()` or `console.log()` output ended and the next began. With several text items, each now starts with a `==> text N/M <==` line, and `console` calls follow the other output in one `<console_output>` block with one line per call
- Fixed `/mcp` waiting for all servers to connect before opening; the manager now updates live and remains usable while enabling, reconnecting, or disabling servers ([#10562](https://github.com/earendil-works/pi/issues/10562))
- Fixed images being dropped as "could not be resized" when running under `node --watch` on Node 24.19+ and 26.x, where Node posts its own messages on the image resize worker channel ([#10527](https://github.com/earendil-works/pi/issues/10527))
- Fixed clipboard paste doing nothing in Termux, and failed copies there omitting the Termux:API install hint ([#10391](https://github.com/earendil-works/pi/issues/10391))
## [1.0.4] - 2026-10-05

View file

@ -52,17 +52,18 @@ async function copyViaWindowsClipboard(text: string): Promise<boolean> {
/** Read plain text from the system clipboard. */
export async function readClipboardText(): Promise<string | null> {
const commands: [string, string[]][] = [];
// Termux reports platform "android", not "linux" (#10391).
if (process.env.TERMUX_VERSION) commands.push(["termux-clipboard-get", []]);
if (platform() === "linux") {
const commands: [string, string[]][] = [];
if (process.env.TERMUX_VERSION) commands.push(["termux-clipboard-get", []]);
if (process.env.WAYLAND_DISPLAY) commands.push(["wl-paste", ["--no-newline", "--type", "text"]]);
if (process.env.DISPLAY) {
commands.push(["xclip", ["-selection", "clipboard", "-out"]], ["xsel", ["--clipboard", "--output"]]);
}
for (const [command, args] of commands) {
const bytes = await runClipboardCommand(command, args, { timeoutMs: 5000 });
if (bytes !== undefined) return bytes.toString("utf8") || null;
}
}
for (const [command, args] of commands) {
const bytes = await runClipboardCommand(command, args, { timeoutMs: 5000 });
if (bytes !== undefined) return bytes.toString("utf8") || null;
}
try {
return (await getNativeClipboard()?.getText()) || null;
@ -129,10 +130,10 @@ export async function copyToClipboard(text: string): Promise<void> {
}
if (copied) return;
if (oversized) throw new Error("Clipboard unavailable: text exceeds the OSC 52 size limit");
if (env.TERMUX_VERSION) {
throw new Error("Clipboard unavailable: install the Termux:API app and `termux-api` package");
}
if (p === "linux") {
if (env.TERMUX_VERSION) {
throw new Error("Clipboard unavailable: install the Termux:API app and `termux-api` package");
}
if (env.WAYLAND_DISPLAY) {
throw new Error("Clipboard unavailable: install `wl-clipboard` (`wl-copy`) or check Wayland access");
}

View file

@ -76,7 +76,6 @@ describe("readClipboardText", () => {
["WAYLAND_DISPLAY", "wl-paste", ["--no-newline", "--type", "text"], ["wl-paste"]],
["DISPLAY", "xclip", ["-selection", "clipboard", "-out"], ["xclip"]],
["DISPLAY", "xsel", ["--clipboard", "--output"], ["xclip", "xsel"]],
["TERMUX_VERSION", "termux-clipboard-get", [], ["termux-clipboard-get"]],
] as const) {
test.each(["clipboard text", ""])(`${command} result %j stops fallback`, async (text) => {
// Regression test for #7248: empty Wayland content must not fall through to stale X11.
@ -100,6 +99,15 @@ describe("readClipboardText", () => {
expect(mocks.getNativeClipboard).toHaveBeenCalledExactlyOnceWith();
expect(mocks.command.mock.calls.map(([name]) => name)).toEqual(["wl-paste", "xclip", "xsel"]);
});
test.each(["clipboard text", ""])("Termux reads termux-clipboard-get result %j on Android", async (text) => {
// Regression test for #10391: Termux reports platform "android".
mocks.platform.mockReturnValue("android");
vi.stubEnv("TERMUX_VERSION", "0.119");
mocks.command.mockResolvedValue(Buffer.from(text));
await expect(readClipboardText()).resolves.toBe(text || null);
expect(mocks.command).toHaveBeenCalledExactlyOnceWith("termux-clipboard-get", [], { timeoutMs: 5000 });
expect(mocks.getNativeClipboard).not.toHaveBeenCalled();
});
test("falls back to X11 tools when wl-paste is unavailable", async () => {
mocks.platform.mockReturnValue("linux");
vi.stubEnv("WAYLAND_DISPLAY", "wayland-0");
@ -257,6 +265,27 @@ describe("copyToClipboard", () => {
);
expect(mocks.command.mock.calls.map(([name]) => name)).toEqual(["wl-copy", "xclip", "xsel"]);
});
test("Termux on Android writes through termux-clipboard-set", async () => {
mocks.platform.mockReturnValue("android");
vi.stubEnv("TERMUX_VERSION", "0.119");
mocks.getNativeClipboard.mockReturnValue(undefined);
await copyToClipboard("hello");
expect(mocks.command).toHaveBeenCalledExactlyOnceWith("termux-clipboard-set", [], {
input: "hello",
timeoutMs: 5000,
});
expect(osc52Writes).toHaveLength(0);
});
test("reports the Termux:API requirement on Android", async () => {
// Regression test for #10391: Termux reports platform "android".
mocks.platform.mockReturnValue("android");
vi.stubEnv("TERMUX_VERSION", "0.119");
mocks.getNativeClipboard.mockReturnValue(undefined);
mocks.command.mockResolvedValue(undefined);
await expect(copyToClipboard("hello")).rejects.toThrow(
"Clipboard unavailable: install the Termux:API app and `termux-api` package",
);
});
test("uses OSC 52 when native and command writes fail in a remote session", async () => {
vi.stubEnv("SSH_CONNECTION", "client server");
mocks.clipboard.setText.mockRejectedValue(new Error("native failed"));