diff --git a/packages/opencode/src/cli/cmd/tui/util/clipboard.ts b/packages/opencode/src/cli/cmd/tui/util/clipboard.ts index be3cec14c6a2..12fd11e9bc13 100644 --- a/packages/opencode/src/cli/cmd/tui/util/clipboard.ts +++ b/packages/opencode/src/cli/cmd/tui/util/clipboard.ts @@ -34,13 +34,16 @@ const getClipboardy = lazy(async () => { * This allows clipboard operations to work over SSH by having * the terminal emulator handle the clipboard locally. */ -function writeOsc52(text: string): void { - if (!process.stdout.isTTY) return +export function buildOsc52Sequence(text: string): string { const base64 = Buffer.from(text).toString("base64") const osc52 = `\x1b]52;c;${base64}\x07` const passthrough = process.env["TMUX"] || process.env["STY"] - const sequence = passthrough ? `\x1bPtmux;\x1b${osc52}\x1b\\` : osc52 - process.stdout.write(sequence) + return passthrough ? `\x1bPtmux;${osc52}\x1b\\` : osc52 +} + +function writeOsc52(text: string): void { + if (!process.stdout.isTTY) return + process.stdout.write(buildOsc52Sequence(text)) } export interface Content { diff --git a/packages/opencode/test/cli/cmd/tui/util/clipboard.test.ts b/packages/opencode/test/cli/cmd/tui/util/clipboard.test.ts new file mode 100644 index 000000000000..9901789a62d4 --- /dev/null +++ b/packages/opencode/test/cli/cmd/tui/util/clipboard.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, test } from "bun:test" +import { buildOsc52Sequence } from "../../../../../src/cli/cmd/tui/util/clipboard" + +function withTmux(value: string | undefined, fn: () => void): void { + const before = process.env["TMUX"] + if (value === undefined) delete process.env["TMUX"] + else process.env["TMUX"] = value + try { + fn() + } finally { + if (before === undefined) delete process.env["TMUX"] + else process.env["TMUX"] = before + } +} + +describe("buildOsc52Sequence", () => { + test("returns plain OSC 52 without TMUX", () => { + withTmux(undefined, () => { + const result = buildOsc52Sequence("hello") + expect(result).toBe("\x1b]52;c;aGVsbG8=\x07") + }) + }) + + test("returns DCS-wrapped OSC 52 with TMUX (no double ESC)", () => { + withTmux("ses", () => { + const result = buildOsc52Sequence("x") + + expect(result.startsWith("\x1bPtmux;")).toBe(true) + + const semicolonPos = result.indexOf(";") + expect(result.charCodeAt(semicolonPos + 1)).toBe(0x1b) + expect(result.charCodeAt(semicolonPos + 2)).toBe(0x5d) + + expect(result.endsWith("\x1b\\")).toBe(true) + + const escCount = Array.from(result).filter((c) => c === "\x1b").length + expect(escCount).toBe(3) + }) + }) +})