5d62003fe5
The session panel's header becomes two tabs, Changes and Preview. Preview shows a web page by address (Reload, Open in a browser tab, the workspace's ports as chips, last address per workspace) in an iframe on the page's own origin: http(s) only, no userinfo, never the dashboard's origin, no top navigation. One rule for every link (lib/openInPreview.ts): a chat or terminal link to localhost/127.0.0.1/0.0.0.0/[::1] opens in Preview while a session panel answers and the dashboard is on loopback; anything else, or a Ctrl/Cmd click, opens a browser tab as before.
84 lines
3.3 KiB
TypeScript
84 lines
3.3 KiB
TypeScript
/**
|
|
* @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ĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
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();
|
|
}
|