refactor(canvas): host A2UI via gateway

This commit is contained in:
Peter Steinberger
2025-12-20 12:17:27 +00:00
parent 13ebbd1a2b
commit ed001a5f55
28 changed files with 385 additions and 354 deletions

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,23 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Clawdis Canvas</title>
<style>
:root { color-scheme: light dark; }
html, body { height: 100%; margin: 0; }
body {
font: 14px system-ui, -apple-system, BlinkMacSystemFont, "Roboto", sans-serif;
background: #0b1020;
color: #e5e7eb;
overflow: hidden;
}
clawdis-a2ui-host { display: block; height: 100%; }
</style>
</head>
<body>
<clawdis-a2ui-host></clawdis-a2ui-host>
<script src="a2ui.bundle.js"></script>
</body>
</html>

View File

@@ -94,4 +94,36 @@ describe("canvas host", () => {
await fs.rm(dir, { recursive: true, force: true });
}
});
it("serves the gateway-hosted A2UI scaffold", async () => {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdis-canvas-"));
const server = await startCanvasHost({
runtime: defaultRuntime,
rootDir: dir,
port: 0,
listenHost: "127.0.0.1",
allowInTests: true,
});
try {
const res = await fetch(
`http://127.0.0.1:${server.port}/__clawdis__/a2ui/`,
);
const html = await res.text();
expect(res.status).toBe(200);
expect(html).toContain("clawdis-a2ui-host");
expect(html).toContain("clawdisCanvasA2UIAction");
const bundleRes = await fetch(
`http://127.0.0.1:${server.port}/__clawdis__/a2ui/a2ui.bundle.js`,
);
const js = await bundleRes.text();
expect(bundleRes.status).toBe(200);
expect(js).toContain("clawdisA2UI");
} finally {
await server.close();
await fs.rm(dir, { recursive: true, force: true });
}
});
});

View File

@@ -108,6 +108,7 @@ export const HelloOkSchema = Type.Object(
{ additionalProperties: false },
),
snapshot: SnapshotSchema,
canvasHostUrl: Type.Optional(NonEmptyString),
policy: Type.Object(
{
maxPayload: Type.Integer({ minimum: 1 }),

View File

@@ -79,7 +79,11 @@ type BridgeInvokeResponseFrame = {
error?: { code: string; message: string } | null;
};
type BridgeHelloOkFrame = { type: "hello-ok"; serverName: string };
type BridgeHelloOkFrame = {
type: "hello-ok";
serverName: string;
canvasHostUrl?: string;
};
type BridgePairOkFrame = { type: "pair-ok"; token: string };
type BridgeErrorFrame = { type: "error"; code: string; message: string };
@@ -132,6 +136,7 @@ export type NodeBridgeServerOpts = {
host: string;
port: number; // 0 = ephemeral
pairingBaseDir?: string;
canvasHostPort?: number;
onEvent?: (nodeId: string, evt: BridgeEventFrame) => Promise<void> | void;
onRequest?: (
nodeId: string,
@@ -180,6 +185,15 @@ export async function startNodeBridgeServer(
? opts.serverName.trim()
: os.hostname();
const buildCanvasHostUrl = (socket: net.Socket) => {
const port = opts.canvasHostPort;
if (!port) return undefined;
const host = socket.localAddress?.trim();
if (!host) return undefined;
const formatted = host.includes(":") ? `[${host}]` : host;
return `http://${formatted}:${port}`;
};
type ConnectionState = {
socket: net.Socket;
nodeInfo: NodeBridgeClientInfo;
@@ -356,7 +370,11 @@ export async function startNodeBridgeServer(
opts.pairingBaseDir,
);
connections.set(nodeId, { socket, nodeInfo, invokeWaiters });
send({ type: "hello-ok", serverName } satisfies BridgeHelloOkFrame);
send({
type: "hello-ok",
serverName,
canvasHostUrl: buildCanvasHostUrl(socket),
} satisfies BridgeHelloOkFrame);
await opts.onAuthenticated?.(nodeInfo);
};
@@ -466,7 +484,11 @@ export async function startNodeBridgeServer(
};
connections.set(nodeId, { socket, nodeInfo, invokeWaiters });
send({ type: "pair-ok", token: wait.token } satisfies BridgePairOkFrame);
send({ type: "hello-ok", serverName } satisfies BridgeHelloOkFrame);
send({
type: "hello-ok",
serverName,
canvasHostUrl: buildCanvasHostUrl(socket),
} satisfies BridgeHelloOkFrame);
await opts.onAuthenticated?.(nodeInfo);
};