From 8a1ce2ea655c1177e8725469e6428af371fa8d15 Mon Sep 17 00:00:00 2001 From: Son0fSun Date: Fri, 16 Jan 2026 18:04:11 -0800 Subject: [PATCH] fix(clipboard): improve OSC 52 escape sequence for SSH compatibility Fix clipboard operations not working over SSH by improving the OSC 52 escape sequence implementation with proper terminal multiplexer support. ## Problems Fixed 1. **BEL vs ST terminator**: Changed from BEL (\x07) to ST (\x1b\\) for better compatibility. Some terminals don't handle BEL correctly inside DCS passthrough sequences. 2. **GNU Screen passthrough**: Fixed incorrect format - Screen uses DCS without the `tmux;` prefix that was being applied to both. - Before: `\x1bPtmux;\x1b\x1b\\` (wrong for Screen) - After: `\x1bP\x1b\\` (correct for Screen) 3. **Nested tmux sessions**: Added detection of tmux nesting level via comma count in TMUX env var, doubling escapes for each level. ## Technical Details OSC 52 format: ESC ] 52 ; c ; ST Passthrough wrapping: - tmux: `ESC P tmux; ESC ESC ESC \` - screen: `ESC P ESC ESC ESC \` The escape characters must be doubled inside DCS sequences so they pass through to the outer terminal emulator. ## Supported Terminals - iTerm2, Kitty, Alacritty, Windows Terminal - tmux (including nested sessions) - GNU Screen - Most modern terminal emulators Fixes: https://github.com/sst/opencode/issues/6111 Fixes: https://github.com/anomalyco/opencode/issues/2773 Co-Authored-By: Claude Opus 4.5 --- .../src/cli/cmd/tui/util/clipboard.ts | 41 +++++++++++++++++-- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/util/clipboard.ts b/packages/opencode/src/cli/cmd/tui/util/clipboard.ts index 2526f41714c7..4fea9df792ef 100644 --- a/packages/opencode/src/cli/cmd/tui/util/clipboard.ts +++ b/packages/opencode/src/cli/cmd/tui/util/clipboard.ts @@ -9,14 +9,47 @@ import path from "path" * Writes text to clipboard via OSC 52 escape sequence. * This allows clipboard operations to work over SSH by having * the terminal emulator handle the clipboard locally. + * + * Supports: + * - Direct terminal output + * - tmux passthrough (with nested tmux support) + * - GNU Screen passthrough + * - Kitty, iTerm2, and other modern terminals */ function writeOsc52(text: string): void { if (!process.stdout.isTTY) return + const base64 = Buffer.from(text).toString("base64") - const osc52 = `\x1b]52;c;${base64}\x07` - // tmux and screen require DCS passthrough wrapping - const passthrough = process.env["TMUX"] || process.env["STY"] - const sequence = passthrough ? `\x1bPtmux;\x1b${osc52}\x1b\\` : osc52 + + // Use ST (String Terminator) instead of BEL for better compatibility + // Some terminals don't handle BEL correctly inside DCS sequences + const osc52 = `\x1b]52;c;${base64}\x1b\\` + + let sequence: string + + if (process.env["TMUX"]) { + // tmux requires DCS passthrough with tmux; prefix + // The escape character must be doubled inside the passthrough + // Format: DCS tmux; ESC ST + // For nested tmux, we need to double the escapes for each level + const tmuxLevel = (process.env["TMUX"]?.match(/,/g) || []).length + 1 + let wrapped = osc52 + for (let i = 0; i < tmuxLevel; i++) { + // Double all escape characters and wrap in DCS passthrough + wrapped = wrapped.replace(/\x1b/g, "\x1b\x1b") + wrapped = `\x1bPtmux;${wrapped}\x1b\\` + } + sequence = wrapped + } else if (process.env["STY"]) { + // GNU Screen requires DCS passthrough without the tmux; prefix + // Format: DCS ESC ST + const wrapped = osc52.replace(/\x1b/g, "\x1b\x1b") + sequence = `\x1bP${wrapped}\x1b\\` + } else { + // Direct output for terminals that support OSC52 natively + sequence = osc52 + } + process.stdout.write(sequence) }