From 18e3e9e1bb76491a0f21e1f3301fb412501e7e8b Mon Sep 17 00:00:00 2001 From: Nino Walker Date: Thu, 18 Jun 2026 09:27:35 +0200 Subject: [PATCH] Fix OSC 8 hyperlinks not opening in the terminal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OSC 8 escape-sequence hyperlinks are handled by xterm's internal OSC link provider, which uses the Terminal `linkHandler` option. We never set one, so it fell back to xterm's built-in default handler that calls `window.open()` with no URL and then assigns `location.href`. Under Electron the resulting `about:blank` open is denied by the window-open handler (it only forwards http/https/file URLs to shell.openExternal), so the link silently never opened — the confirm dialog appeared but nothing happened. Provide a `linkHandler` that routes OSC 8 links through the same `openLink` path already used for URLs detected by the WebLinksAddon. The activate/hover/ leave behavior is factored into shared closures so both link types behave identically (including hover, which enables the "Open in External Browser" context menu on OSC 8 links). Co-Authored-By: Claude Opus 4.8 --- frontend/app/view/term/termwrap.ts | 54 +++++++++++++++--------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/frontend/app/view/term/termwrap.ts b/frontend/app/view/term/termwrap.ts index d10b600459..5242485e99 100644 --- a/frontend/app/view/term/termwrap.ts +++ b/frontend/app/view/term/termwrap.ts @@ -143,6 +143,29 @@ export class TermWrap { this.lastCommandAtom = jotai.atom(null) as jotai.PrimitiveAtom; this.claudeCodeActiveAtom = jotai.atom(false); this.webglEnabledAtom = jotai.atom(false) as jotai.PrimitiveAtom; + const activateLink = (e: MouseEvent, uri: string) => { + e.preventDefault(); + const modifierPressed = PLATFORM === PlatformMacOS ? e.metaKey : e.ctrlKey; + if (!modifierPressed) { + return; + } + fireAndForget(() => openLink(uri)); + }; + const linkHover = (e: MouseEvent, uri: string) => { + this.hoveredLinkUri = uri; + this.onLinkHover?.(uri, e.clientX, e.clientY); + }; + const linkLeave = () => { + this.hoveredLinkUri = null; + this.onLinkHover?.(null, 0, 0); + }; + // OSC 8 hyperlinks don't go through the WebLinksAddon; without a linkHandler they fall back to + // xterm's default window.open() handler, which Electron's window-open handler silently blocks. + options.linkHandler = { + activate: (e, uri) => activateLink(e, uri), + hover: (e, uri) => linkHover(e, uri), + leave: () => linkLeave(), + }; this.terminal = new Terminal(options); this.fitAddon = new FitAddon(); this.serializeAddon = new SerializeAddon(); @@ -151,33 +174,10 @@ export class TermWrap { this.terminal.loadAddon(this.fitAddon); this.terminal.loadAddon(this.serializeAddon); this.terminal.loadAddon( - new WebLinksAddon( - (e, uri) => { - e.preventDefault(); - switch (PLATFORM) { - case PlatformMacOS: - if (e.metaKey) { - fireAndForget(() => openLink(uri)); - } - break; - default: - if (e.ctrlKey) { - fireAndForget(() => openLink(uri)); - } - break; - } - }, - { - hover: (e, uri) => { - this.hoveredLinkUri = uri; - this.onLinkHover?.(uri, e.clientX, e.clientY); - }, - leave: () => { - this.hoveredLinkUri = null; - this.onLinkHover?.(null, 0, 0); - }, - } - ) + new WebLinksAddon(activateLink, { + hover: linkHover, + leave: linkLeave, + }) ); this.setTermRenderer(WebGLSupported && waveOptions.useWebGl ? "webgl" : "dom"); // Register OSC handlers