/** * @file openInPreview.ts — the ONE rule every link in the dashboard follows * (a chat's markdown link or bare URL, a terminal's link): a page served by * THIS machine (`localhost`, `127.0.0.1`, `0.0.0.0`, `[::1]`) opens in the * session view's Preview tab, everything else in a browser tab as before. * The request travels as a window event (like `openInChanges.ts`); the * Workspace page answers it only while it shows a session panel, so with no * listener — another page, or a remote dashboard where "localhost" is the * viewer's own machine — the link falls back to a browser tab. Also holds the * URL checks the Preview tab itself uses. * @author Nguyễn Ngọc Trí Vĩ */ export const OPEN_IN_PREVIEW_EVENT = "agenthub:open-in-preview"; /** The event's detail. `handled` is set by the listener, read by the sender. */ export type OpenInPreview = { url: string; handled: boolean }; const LOOPBACK = new Set(["localhost", "127.0.0.1", "0.0.0.0", "[::1]", "::1"]); /** A URL the Preview may show: http(s) only, `0.0.0.0` read as `localhost` * (a dev server prints it, a browser cannot open it), never the dashboard's * own origin (the frame would then BE the dashboard, cookies and API * included). A bare `localhost:3000` gets `http://`. Null when refused. */ export function normalizePreviewUrl(raw: string, self = window.location.origin): string | null { let text = raw.trim(); if (!text) return null; if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(text)) text = `http://${text}`; let u: URL; try { u = new URL(text); } catch { return null; } if (u.protocol !== "http:" && u.protocol !== "https:") return null; if (u.username || u.password) return null; if (u.hostname === "0.0.0.0") u.hostname = "localhost"; if (u.origin === self) return null; return u.toString(); } /** True for a page served by the machine the BROWSER runs on. */ export function isLoopbackUrl(url: string): boolean { try { const u = new URL(url); return (u.protocol === "http:" || u.protocol === "https:") && LOOPBACK.has(u.hostname); } catch { return false; } } /** The dashboard itself is opened from this machine: only then does a * `localhost` link name the same machine the agents run on. */ export function dashboardIsLocal(hostname = window.location.hostname): boolean { return LOOPBACK.has(hostname); } /** Asks the open session panel to show `url`. False when nothing took it * (not a local page, a remote dashboard, no session panel). */ export function openInPreview(url: string): boolean { if (!isLoopbackUrl(url) || !dashboardIsLocal()) return false; const target = normalizePreviewUrl(url); if (!target) return false; const detail: OpenInPreview = { url: target, handled: false }; window.dispatchEvent(new CustomEvent(OPEN_IN_PREVIEW_EVENT, { detail })); return detail.handled; } /** A link click: Preview when the rule says so, else a browser tab. A * Ctrl/Cmd/Shift/middle click always keeps the browser's own behaviour. */ export function onLinkClick( e: { ctrlKey: boolean; metaKey: boolean; shiftKey: boolean; button: number; preventDefault: () => void; }, url: string ): void { if (e.ctrlKey || e.metaKey || e.shiftKey || e.button !== 0) return; if (openInPreview(url)) e.preventDefault(); }