From 21c2934a369ec5cdee00cbb5cc5e26201cfedd90 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Tue, 27 Oct 2026 22:22:44 +0800 Subject: [PATCH] fix(terminal): Open OSC 8 hyperlinks in the system browser --- src/features/terminal/embedded.ts | 3 ++- src/features/terminal/session.ts | 15 ++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/features/terminal/embedded.ts b/src/features/terminal/embedded.ts index ae64676a..4fa7cb0e 100644 --- a/src/features/terminal/embedded.ts +++ b/src/features/terminal/embedded.ts @@ -11,7 +11,7 @@ import type { EmbeddedTerminal, EmbeddedTerminalOptions } from "@/workbench/cont import { useProject } from "@/platform/project"; import { workflowSettings } from "@/platform/workflowSettings"; import { findTerminalLinks } from "./links"; -import { fontFor, openExternal, openFileLink } from "./session"; +import { LINK_HANDLER, fontFor, openExternal, openFileLink } from "./session"; import { readTerminalTheme, remeasureWhenFontsLoad } from "./theme"; const live = new Set(); @@ -26,6 +26,7 @@ export function createEmbeddedTerminal(host: HTMLElement, opts: EmbeddedTerminal const settings = workflowSettings().terminal; const font = fontFor(settings); const term = new Terminal({ + linkHandler: LINK_HANDLER, fontFamily: font.family, fontSize: font.size, lineHeight: font.lineHeight, diff --git a/src/features/terminal/session.ts b/src/features/terminal/session.ts index 01aca4e1..cfccf603 100644 --- a/src/features/terminal/session.ts +++ b/src/features/terminal/session.ts @@ -3,7 +3,7 @@ * lives in a detached host element owned by the session (not by React), so a * session survives its tool window being hidden or its pane being re-mounted. */ -import { Terminal, type IDisposable, type ILink } from "@xterm/xterm"; +import { Terminal, type IDisposable, type ILink, type ILinkHandler } from "@xterm/xterm"; import { FitAddon } from "@xterm/addon-fit"; import { SearchAddon, type ISearchOptions } from "@xterm/addon-search"; import "@xterm/xterm/css/xterm.css"; @@ -105,6 +105,18 @@ export async function openExternal(url: string) { } } +/** + * OSC 8 hyperlinks (`claude` / `gh` login links…): open in the system browser. + * xterm's default confirms with `window.confirm` and then calls `window.open`, + * which does nothing in the app's webview. + */ +export const LINK_HANDLER: ILinkHandler = { + activate: (event, uri) => { + event.preventDefault(); + if (/^https?:\/\//i.test(uri)) void openExternal(uri); + }, +}; + export async function openFileLink(link: Extract, cwd: string) { const selection = link.line ? { line: link.line - 1, col: Math.max(0, (link.col ?? 1) - 1) } : undefined; const path = resolvePath(link.path, cwd); @@ -188,6 +200,7 @@ export class TerminalSession { // Same width as the IDE's scrollbars (global.css); the slider is styled there too (Terminal.module.css). overviewRuler: { width: 10 }, theme: readTerminalTheme(), + linkHandler: LINK_HANDLER, }); this.term.loadAddon(this.fit); this.term.loadAddon(this.searchAddon);