diff --git a/src/workspaces/officerdev/src/apps/Terminal/Terminal.tsx b/src/workspaces/officerdev/src/apps/Terminal/Terminal.tsx index 1a7ae05b..9de2badc 100644 --- a/src/workspaces/officerdev/src/apps/Terminal/Terminal.tsx +++ b/src/workspaces/officerdev/src/apps/Terminal/Terminal.tsx @@ -167,6 +167,34 @@ export const TerminalView = ({ // Clickable URLs in output. term.loadAddon(new WebLinksAddon()); + // ── OSC 52: let a program in the terminal put text on the user's clipboard ── + // + // xterm.js does not handle OSC 52 unless something registers for it, and nothing did. Programs that + // offer "press c to copy" emit the sequence and it vanished — so the confirmation they print was true + // about having SENT it and false about anything arriving. Observed while signing a member into Claude + // Code on 2026-08-12: its first-run login prints a URL too long to read off a wrapped pane and offers + // to copy it, "(Copied!)" appeared, and the clipboard was untouched. Without this, first-run login in + // the web terminal cannot be completed at all, which is every new member's first five minutes. + // + // The payload is `;`. A lone `?` in the data position is a READ request — a program + // asking the terminal to hand it whatever the user has copied — and it is deliberately not answered: + // a shell should not be able to exfiltrate the clipboard of the person watching it. Writes only. + // + // `navigator.clipboard` needs a secure context and generally a user gesture; the keypress that caused + // the sequence is that gesture. It can still be refused, so the failure is swallowed rather than + // thrown into the render — a copy that does not land is the status quo, not a reason to break the pane. + term.parser.registerOscHandler(52, (payload) => { + const data = payload.slice(payload.indexOf(';') + 1); + if (!data || data === '?') return true; + try { + const bytes = Uint8Array.from(atob(data), (c) => c.charCodeAt(0)); + void navigator.clipboard?.writeText(new TextDecoder().decode(bytes)); + } catch { + // Malformed base64, no clipboard API, or permission refused. + } + return true; + }); + const searchAddon = new SearchAddon(); term.loadAddon(searchAddon); searchRef.current = searchAddon;