Skip to content

Commit 8a1ce2e

Browse files
Son0fSunclaude
andcommitted
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<seq>\x1b\\` (wrong for Screen) - After: `\x1bP<seq>\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 ; <base64> ST Passthrough wrapping: - tmux: `ESC P tmux; ESC ESC <seq> ESC \` - screen: `ESC P ESC ESC <seq> 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: anomalyco#6111 Fixes: anomalyco#2773 Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 759ce8f commit 8a1ce2e

1 file changed

Lines changed: 37 additions & 4 deletions

File tree

‎packages/opencode/src/cli/cmd/tui/util/clipboard.ts‎

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,47 @@ import path from "path"
99
* Writes text to clipboard via OSC 52 escape sequence.
1010
* This allows clipboard operations to work over SSH by having
1111
* the terminal emulator handle the clipboard locally.
12+
*
13+
* Supports:
14+
* - Direct terminal output
15+
* - tmux passthrough (with nested tmux support)
16+
* - GNU Screen passthrough
17+
* - Kitty, iTerm2, and other modern terminals
1218
*/
1319
function writeOsc52(text: string): void {
1420
if (!process.stdout.isTTY) return
21+
1522
const base64 = Buffer.from(text).toString("base64")
16-
const osc52 = `\x1b]52;c;${base64}\x07`
17-
// tmux and screen require DCS passthrough wrapping
18-
const passthrough = process.env["TMUX"] || process.env["STY"]
19-
const sequence = passthrough ? `\x1bPtmux;\x1b${osc52}\x1b\\` : osc52
23+
24+
// Use ST (String Terminator) instead of BEL for better compatibility
25+
// Some terminals don't handle BEL correctly inside DCS sequences
26+
const osc52 = `\x1b]52;c;${base64}\x1b\\`
27+
28+
let sequence: string
29+
30+
if (process.env["TMUX"]) {
31+
// tmux requires DCS passthrough with tmux; prefix
32+
// The escape character must be doubled inside the passthrough
33+
// Format: DCS tmux; ESC <sequence> ST
34+
// For nested tmux, we need to double the escapes for each level
35+
const tmuxLevel = (process.env["TMUX"]?.match(/,/g) || []).length + 1
36+
let wrapped = osc52
37+
for (let i = 0; i < tmuxLevel; i++) {
38+
// Double all escape characters and wrap in DCS passthrough
39+
wrapped = wrapped.replace(/\x1b/g, "\x1b\x1b")
40+
wrapped = `\x1bPtmux;${wrapped}\x1b\\`
41+
}
42+
sequence = wrapped
43+
} else if (process.env["STY"]) {
44+
// GNU Screen requires DCS passthrough without the tmux; prefix
45+
// Format: DCS ESC <sequence> ST
46+
const wrapped = osc52.replace(/\x1b/g, "\x1b\x1b")
47+
sequence = `\x1bP${wrapped}\x1b\\`
48+
} else {
49+
// Direct output for terminals that support OSC52 natively
50+
sequence = osc52
51+
}
52+
2053
process.stdout.write(sequence)
2154
}
2255

0 commit comments

Comments
 (0)