UNPKG

framework

Version:

The (AI) Framework: turnkey, zero-config AI orchestration that wraps a coding-agent CLI (Claude Code) as a black box and takes you from an idea to a running app. Vite for AI.

96 lines 4.27 kB
import { request } from 'node:http'; import { readLiveMetas } from '../store/index.js'; import { defaultProjectsProvider } from './projects.js'; /** The prefix the dashboard client posts to. */ export const BROWSER_PROXY_PREFIX = '/browser'; /** * Parse `/browser/<projectId>/<agentId>/stream|input`, or undefined for anything else — an * unrecognized shape must fall through to the bundle rather than be guessed at. Ids are taken * verbatim and only ever used to look an agent up, never as a path. */ export function parseBrowserRoute(url) { if (!url) return undefined; // Parse and decode defensively: a malformed target or escape (`/browser/p/%zz/stream`) names // no run, and a throw here would escape the void-dispatched proxy handler as an unhandled // rejection that kills the daemon (#938). Unparseable falls through to the bundle like any // other unrecognized shape. try { const { pathname } = new URL(url, 'http://localhost'); if (!pathname.startsWith(`${BROWSER_PROXY_PREFIX}/`)) return undefined; const parts = pathname.slice(BROWSER_PROXY_PREFIX.length + 1).split('/'); if (parts.length !== 3) return undefined; const [projectId, agentId, leg] = parts; if (!projectId || !agentId) return undefined; if (leg !== 'stream' && leg !== 'input') return undefined; return { projectId, agentId: decodeURIComponent(agentId), leg }; } catch { return undefined; } } /** * The real lookup: the port the agent recorded on its own meta. An agent with no browser, a finished * run, or an unknown id all read as undefined, which the caller turns into a 404. */ const defaultBrowserPortLookup = async (projectId, agentId) => { const cwd = await defaultProjectsProvider().resolvePath(projectId); if (!cwd) return undefined; const live = await readLiveMetas(cwd).catch(() => []); const agent = live.find(meta => meta.id === agentId); // Only a live agent: the bridge is torn down with the agent, so a port off a finished one would // reach whatever the OS handed that number next. return agent?.status === 'running' ? agent.browserStreamPort : undefined; }; /** * Proxy one request to the agent's bridge. Returns false when the URL is not a browser route, so * the dashboard server can carry on to the client bundle. * * Streams both ways rather than buffering: `/stream` is an endless `multipart/x-mixed-replace` * body, so anything that waits for it to finish never answers. */ export async function handleBrowserProxy(req, res, lookup = defaultBrowserPortLookup) { const route = parseBrowserRoute(req.url); if (!route) return false; const port = await lookup(route.projectId, route.agentId).catch(() => undefined); if (!port) { // The pane polls this while an agent is starting, and an agent may never have a browser at all, // so a miss is ordinary rather than an error worth logging. res.writeHead(404, { 'content-type': 'text/plain' }).end('no browser preview for this run'); return true; } const upstream = request({ host: '127.0.0.1', port, path: route.leg === 'stream' ? '/stream' : '/input', method: route.leg === 'stream' ? 'GET' : 'POST', headers: route.leg === 'input' ? { 'content-type': 'application/json' } : {}, }, proxied => { res.writeHead(proxied.statusCode ?? 502, { ...proxied.headers, // The frames are a live view of whatever the human is typing. Nothing caches this. 'cache-control': 'no-store', }); proxied.pipe(res); }); // The agent can die mid-stream, which lands here rather than as a response. upstream.on('error', () => { if (!res.headersSent) res.writeHead(502, { 'content-type': 'text/plain' }); res.end(); }); // Stop pulling frames the moment the pane goes away, or the agent keeps serving a dead viewer. res.on('close', () => upstream.destroy()); if (route.leg === 'input') req.pipe(upstream); else upstream.end(); return true; } //# sourceMappingURL=browser-proxy.js.map