mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-20 09:09:35 +02:00
Merge pull request #611: feat(terminal): copy OSC 52 clipboard writes from the PTY into the browser clipboard
This commit is contained in:
commit
f3d7a86c16
12 files changed
+259
-3
No files matched your search
@@ -1564,6 +1564,14 @@ function scheduleBackground(fn) {
|
||||
// but server-sent terminal buffers may still contain markers from Claude CLI.
|
||||
const DEC_SYNC_STRIP_RE = /\x1b\[\?2026[hl]/g;
|
||||
|
||||
// OSC 52 clipboard writes stripping — a replay re-feeds the raw byte history, and
|
||||
// xterm parses it after chunkedTerminalWrite has already released the load
|
||||
// gate, so an OSC 52 left in would re-copy old text on every tab switch.
|
||||
const OSC52_STRIP_RE = /\x1b\]52;[^\x07\x1b]*(?:\x07|\x1b\\)/g;
|
||||
// The same OSC 52 text arriving again within this window is one copy delivered
|
||||
// several ways (direct, tmux load-buffer -w, tmux set-clipboard forwarding).
|
||||
const OSC52_DEDUPE_MS = 1000;
|
||||
|
||||
// Built-in respawn configuration presets
|
||||
const BUILTIN_RESPAWN_PRESETS = [
|
||||
{
|
||||
|
||||
@@ -246,6 +246,9 @@
|
||||
|
||||
this.fitAddon = new FitAddon.FitAddon();
|
||||
this.terminal.loadAddon(this.fitAddon);
|
||||
// OSC 52 clipboard writes from this pane's PTY, gated on THIS pane's
|
||||
// buffer load so a replay never re-copies (terminal-ui.js).
|
||||
global.app?._installOsc52Clipboard?.(this.terminal, () => this._bufferLoading);
|
||||
this.terminal.open(this.mountEl);
|
||||
this.fitAddon.fit();
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*
|
||||
* @mixin Extends CodemanApp.prototype via Object.assign
|
||||
* @dependency app.js (CodemanApp class, this.terminal, this.fitAddon, this.sessions)
|
||||
* @dependency constants.js (DEC_SYNC_STRIP_RE, TIMING constants)
|
||||
* @dependency constants.js (DEC_SYNC_STRIP_RE, OSC52_STRIP_RE, OSC52_DEDUPE_MS, TIMING constants)
|
||||
* @dependency mobile-handlers.js (MobileDetection)
|
||||
* @dependency vendor/xterm.js, vendor/xterm-addon-fit.js, vendor/xterm-addon-webgl.js
|
||||
* @dependency vendor/xterm-zerolag-input.js (LocalEchoOverlay)
|
||||
@@ -706,6 +706,8 @@ Object.assign(CodemanApp.prototype, {
|
||||
}
|
||||
}
|
||||
|
||||
this._installOsc52Clipboard(this.terminal);
|
||||
|
||||
const container = document.getElementById('terminalContainer');
|
||||
this.terminal.open(container);
|
||||
this._initMobileImePreview();
|
||||
@@ -4605,8 +4607,9 @@ Object.assign(CodemanApp.prototype, {
|
||||
}
|
||||
|
||||
// Strip any DEC 2026 markers that might be in the buffer
|
||||
// (from historical SSE data that was stored with markers)
|
||||
const cleanBuffer = buffer.replace(DEC_SYNC_STRIP_RE, '');
|
||||
// (from historical SSE data that was stored with markers), and every
|
||||
// OSC 52 clipboard write: a replay must never copy (see OSC52_STRIP_RE).
|
||||
const cleanBuffer = buffer.replace(DEC_SYNC_STRIP_RE, '').replace(OSC52_STRIP_RE, '');
|
||||
|
||||
// For small buffers, write directly — single-frame render is fast enough
|
||||
if (cleanBuffer.length <= chunkSize) {
|
||||
@@ -5072,6 +5075,43 @@ Object.assign(CodemanApp.prototype, {
|
||||
return Number.isInteger(columns) && columns > 0 ? columns : 0;
|
||||
},
|
||||
|
||||
// OSC 52 (`ESC ] 52 ; <sel> ; <base64> BEL`) is how programs inside the PTY —
|
||||
// Claude Code's /copy, tmux yanks, vim — set the clipboard. The server has no
|
||||
// display, so this is the only route to the browser's clipboard. Writes only:
|
||||
// a `?` query (clipboard read) is ignored. A replay must never copy: the main
|
||||
// terminal's replays strip OSC 52 in chunkedTerminalWrite (xterm parses after
|
||||
// _isLoadingBuffer is already cleared, so that flag alone cannot hold), and a
|
||||
// TerminalTile passes `isReplaying`, its own load flag, which does hold there.
|
||||
// Every tab watching the session gets the sequence, so only the focused one
|
||||
// opens the manual-copy dialog when the browser refuses the write.
|
||||
// One copy usually arrives several times: Claude Code writes OSC 52 itself and
|
||||
// through `tmux load-buffer -w`, and tmux forwards it with set-clipboard on,
|
||||
// so the same text lands up to three times within a few ms. Identical text
|
||||
// inside OSC52_DEDUPE_MS is that one copy, not a new one.
|
||||
_installOsc52Clipboard(terminal, isReplaying = () => this._isLoadingBuffer) {
|
||||
terminal.parser?.registerOscHandler?.(52, (data) => {
|
||||
const sep = data.indexOf(';');
|
||||
const payload = sep >= 0 ? data.slice(sep + 1) : data;
|
||||
if (!payload || payload === '?' || isReplaying()) return true;
|
||||
let text;
|
||||
try {
|
||||
const bytes = Uint8Array.from(atob(payload), (c) => c.charCodeAt(0));
|
||||
text = new TextDecoder().decode(bytes);
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
if (!text) return true;
|
||||
const now = Date.now();
|
||||
if (this._lastOsc52?.text === text && now - this._lastOsc52.at < OSC52_DEDUPE_MS) return true;
|
||||
this._lastOsc52 = { text, at: now };
|
||||
this._copyText(text).then((ok) => {
|
||||
if (ok) this.showToast(`Copied to clipboard (${text.length} chars)`, 'success');
|
||||
else if (document.hasFocus?.()) this._showClipboardFallback?.(text);
|
||||
});
|
||||
return true;
|
||||
});
|
||||
},
|
||||
|
||||
// Copy the current terminal selection. Goes through _copyText (Clipboard API,
|
||||
// then a hidden-textarea + execCommand fallback) because install.sh's LAN
|
||||
// option serves plain HTTP, where navigator.clipboard is undefined.
|
||||
|
||||
Reference in new issue
Block a user