/** * Where a plugin route opens when the conversation is the page. * * On HQ the copilot is the page: the thread, the composer and the setup strip * all live there, and a route change unmounts every one of them. So a link the * agent writes, a chip it attaches and a directive it emits mid-stream all * resolve through here first, and a route with a canvas opens *beside* the * conversation rather than instead of it. */ import type { CanvasView } from './use-home-canvas'; /** Where a path resolves to on HQ. */ export type InPageTarget = | { kind: 'canvas'; view: CanvasView } | { kind: 'none' }; /** Routes that have a canvas of their own, longest prefix first so * ``/ai-visibility/contents`` never matches ``/ai-visibility``. */ const CANVAS_ROUTES: ReadonlyArray = [ ['/ai-visibility/page-templates', 'templates'], ['/ai-visibility/contents', 'content'], ['/ai-visibility', 'week'], ['/catalog-quality', 'catalog'], ]; /** * What HQ should open for a path. * * An unknown path answers ``none`` rather than guessing: the caller then * decides what to do with it, and a route added elsewhere in the plugin never * silently lands in the wrong canvas. * * @param path - The plugin route, with or without a query string. * @return What to open beside the conversation. */ export function resolveInPageTarget(path: string): InPageTarget { if (!path.startsWith('/')) return { kind: 'none' }; // The pathname alone decides; a query string is the page's own business and // the canvas has no way to honour it. const pathname: string = path.split(/[?#]/)[0].replace(/\/+$/, '') || '/'; for (const [prefix, view] of CANVAS_ROUTES) { if (pathname === prefix || pathname.startsWith(`${prefix}/`)) { return { kind: 'canvas', view }; } } return { kind: 'none' }; }