From 3e477eab5f7c37015504a6ada56fa1f90aad7e0b Mon Sep 17 00:00:00 2001 From: David Brailovsky Date: Mon, 12 Oct 2026 18:05:19 +0200 Subject: [PATCH] feat(coding-agent): add PI_CLIPBOARD_OSC52 to force OSC 52 copies Sends copied text to the terminal with OSC 52 after the local clipboard write, as over SSH, for remote clients without SSH variables such as multiplexer attaches and remote IDE daemons. closes #10826, closes #10835, closes #10114 --- packages/coding-agent/CHANGELOG.md | 1 + .../docs/environment-variables.md | 1 + packages/coding-agent/src/utils/clipboard.ts | 7 ++++++- packages/coding-agent/test/clipboard.test.ts | 21 +++++++++++++++++++ 4 files changed, 29 insertions(+), 1 deletion(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 0d2180b16..851944197 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -14,6 +14,7 @@ - Added `reachesTools` to tool definitions, so tools that reach other tools (such as `codemode` and `tool_search`) declare which tools a call may reach; the MCP extension uses it to wait for the servers a call needs and to decide whether a replacement `codemode` or `tool_search` tool can reach MCP tools - Added `aliases` to tool namespaces; codemode's `describeNamespace()` and the `namespace` option of `searchTools()` match them, and MCP namespaces use the server name - Added per-key OpenRouter model lists: with an OpenRouter key, Pi lists only the chat models that key may use, with the key's context size, max output, and prices, including guardrails and regional endpoints set as the base URL in `models.json`. The list is saved and used offline ([#10353](https://github.com/earendil-works/pi/issues/10353)) +- Added `PI_CLIPBOARD_OSC52=1` to send copied text to the terminal with OSC 52 after the local clipboard write, so copies reach a remote client's clipboard when Pi cannot detect the remote session, such as multiplexer attaches from another machine or remote IDE daemons ([#10826](https://github.com/earendil-works/pi/issues/10826), [#10835](https://github.com/earendil-works/pi/issues/10835), [#10114](https://github.com/earendil-works/pi/issues/10114)) - Added elapsed time while a codemode script runs and its total time once finished to the codemode tool row, like bash rows ([#10708](https://github.com/earendil-works/pi/issues/10708)) ### Changed diff --git a/packages/coding-agent/docs/environment-variables.md b/packages/coding-agent/docs/environment-variables.md index 0a25dba61..8a1975043 100644 --- a/packages/coding-agent/docs/environment-variables.md +++ b/packages/coding-agent/docs/environment-variables.md @@ -88,6 +88,7 @@ These variables are read by Pi itself: | `PI_SHARE_VIEWER_URL` | Override the base URL used by `/share` | | `PI_RADIUS_GATEWAY` | Override the Radius gateway origin used by `/bug` uploads | | `PI_HARDWARE_CURSOR` | Set to `1` to show the hardware cursor; see [Terminal setup](terminal-setup.md) | +| `PI_CLIPBOARD_OSC52` | Set to `1` to also send copied text to the terminal with OSC 52 after the local clipboard write, as Pi does over SSH. Use it when a remote client reaches Pi without SSH variables, such as a multiplexer attached from another machine or a remote IDE daemon | | `PI_HYPERLINKS` | Override OSC 8 hyperlink detection with `1`, `0`, or `auto` | | `PI_PROGRAM_STATUS` | Override OSC 7501 program status detection: `1` always reports, `0` never reports; otherwise Pi reports only after the terminal confirms support. See [Terminal setup](terminal-setup.md#program-status) | | `PI_IMAGE_PROTOCOL` | Override inline image detection with `kitty`, `iterm2`, `none`, or `auto` | diff --git a/packages/coding-agent/src/utils/clipboard.ts b/packages/coding-agent/src/utils/clipboard.ts index 540635e64..c1eedc636 100644 --- a/packages/coding-agent/src/utils/clipboard.ts +++ b/packages/coding-agent/src/utils/clipboard.ts @@ -8,8 +8,13 @@ import { isWSL } from "./wsl.ts"; const MAX_OSC52_ENCODED_LENGTH = 100_000; +/** + * Remote sessions also emit OSC 52 so the client terminal receives the copy. PI_CLIPBOARD_OSC52=1 + * covers remote clients that leave no SSH markers, such as multiplexer attaches (#10114, #10835) + * or IDE daemons (#10826). + */ function isRemoteSession(env: NodeJS.ProcessEnv): boolean { - return Boolean(env.SSH_CONNECTION || env.SSH_CLIENT || env.MOSH_CONNECTION); + return Boolean(env.SSH_CONNECTION || env.SSH_CLIENT || env.MOSH_CONNECTION || env.PI_CLIPBOARD_OSC52 === "1"); } function emitOsc52(text: string): boolean { diff --git a/packages/coding-agent/test/clipboard.test.ts b/packages/coding-agent/test/clipboard.test.ts index d428d8077..d283209f3 100644 --- a/packages/coding-agent/test/clipboard.test.ts +++ b/packages/coding-agent/test/clipboard.test.ts @@ -42,6 +42,7 @@ beforeEach(() => { "WT_SESSION", "WSL_DISTRO_NAME", "WSLENV", + "PI_CLIPBOARD_OSC52", ]) vi.stubEnv(name, ""); mocks.platform.mockReturnValue("darwin"); @@ -150,6 +151,26 @@ describe("copyToClipboard", () => { expect(osc52Writes).toHaveLength(1); expect(mocks.command).not.toHaveBeenCalled(); }); + test.each([ + ["darwin", undefined], + ["win32", undefined], + ["linux", ":99"], + ] as const)("PI_CLIPBOARD_OSC52=1 emits OSC 52 after a successful %s copy", async (platform, display) => { + // Regression test for #10826, #10835, #10114: remote clients without SSH markers. + mocks.platform.mockReturnValue(platform); + if (display) vi.stubEnv("DISPLAY", display); + vi.stubEnv("PI_CLIPBOARD_OSC52", "1"); + await copyToClipboard("hello"); + if (platform === "linux") { + expect(mocks.command).toHaveBeenCalledWith("xclip", ["-selection", "clipboard"], { + input: "hello", + timeoutMs: 5000, + }); + } else { + expect(mocks.clipboard.setText).toHaveBeenCalledWith("hello"); + } + expect(osc52Writes).toEqual([`\x1b]52;c;${Buffer.from("hello").toString("base64")}\x07`]); + }); test("a rejected native write falls back to pbcopy", async () => { mocks.clipboard.setText.mockRejectedValue(new Error("native failed")); await copyToClipboard("hello");