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
JavaScript
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