From 8cb83aa0e198ac68d7305225759c3d75a172522f Mon Sep 17 00:00:00 2001 From: s-zx Date: Thu, 23 Apr 2026 20:51:46 +0200 Subject: [PATCH] fix(term): provide linkHandler so OSC 8 hyperlinks open on Cmd/Ctrl-click MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit xterm.js falls back to window.confirm() + window.open() for OSC 8 hyperlinks when no linkHandler is set. In Electron's renderer, window.open() navigation is intercepted by shNavHandler / shFrameNavHandler — so the dialog appears but the browser never opens. Route OSC 8 clicks through the same Cmd/Ctrl-click path that WebLinksAddon uses for regex-detected URLs. --- frontend/app/view/term/termwrap.ts | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/frontend/app/view/term/termwrap.ts b/frontend/app/view/term/termwrap.ts index d10b600459..f796b0a451 100644 --- a/frontend/app/view/term/termwrap.ts +++ b/frontend/app/view/term/termwrap.ts @@ -143,6 +143,36 @@ export class TermWrap { this.lastCommandAtom = jotai.atom(null) as jotai.PrimitiveAtom; this.claudeCodeActiveAtom = jotai.atom(false); this.webglEnabledAtom = jotai.atom(false) as jotai.PrimitiveAtom; + // Custom linkHandler for OSC 8 hyperlinks. Without this, xterm.js falls + // back to window.confirm() + window.open(), which Electron's renderer + // blocks — so the confirmation dialog appears but clicking OK silently + // fails. Route OSC 8 clicks through the same Cmd/Ctrl-click path that + // WebLinksAddon uses for regex-detected URLs. + options.linkHandler = { + activate: (event: MouseEvent, uri: string) => { + event.preventDefault(); + switch (PLATFORM) { + case PlatformMacOS: + if (event.metaKey) { + fireAndForget(() => openLink(uri)); + } + break; + default: + if (event.ctrlKey) { + fireAndForget(() => openLink(uri)); + } + break; + } + }, + hover: (event: MouseEvent, uri: string) => { + this.hoveredLinkUri = uri; + this.onLinkHover?.(uri, event.clientX, event.clientY); + }, + leave: () => { + this.hoveredLinkUri = null; + this.onLinkHover?.(null, 0, 0); + }, + }; this.terminal = new Terminal(options); this.fitAddon = new FitAddon(); this.serializeAddon = new SerializeAddon();