refactor: node tools and canvas host url

This commit is contained in:
Peter Steinberger
2025-12-27 01:36:24 +01:00
parent 52ca5c4aa2
commit c54e4d0900
19 changed files with 448 additions and 128 deletions

View File

@@ -22,6 +22,7 @@ type CanvasOpts = {
height?: string;
js?: string;
jsonl?: string;
text?: string;
format?: string;
maxWidth?: string;
quality?: string;
@@ -54,6 +55,16 @@ type PairingList = {
paired: PairedNode[];
};
const A2UI_ACTION_KEYS = [
"beginRendering",
"surfaceUpdate",
"dataModelUpdate",
"deleteSurface",
"createSurface",
] as const;
type A2UIVersion = "v0.8" | "v0.9";
const canvasCallOpts = (cmd: Command) =>
cmd
.option("--url <url>", "Gateway WebSocket URL", "ws://127.0.0.1:18789")
@@ -104,6 +115,86 @@ function normalizeNodeKey(value: string) {
.replace(/-+$/, "");
}
function buildA2UITextJsonl(text: string) {
const surfaceId = "main";
const rootId = "root";
const textId = "text";
const payloads = [
{
surfaceUpdate: {
surfaceId,
components: [
{
id: rootId,
component: { Column: { children: { explicitList: [textId] } } },
},
{
id: textId,
component: {
Text: { text: { literalString: text }, usageHint: "body" },
},
},
],
},
},
{ beginRendering: { surfaceId, root: rootId } },
];
return payloads.map((payload) => JSON.stringify(payload)).join("\n");
}
function validateA2UIJsonl(jsonl: string) {
const lines = jsonl.split(/\r?\n/);
const errors: string[] = [];
let sawV08 = false;
let sawV09 = false;
let messageCount = 0;
lines.forEach((line, idx) => {
const trimmed = line.trim();
if (!trimmed) return;
messageCount += 1;
let obj: unknown;
try {
obj = JSON.parse(trimmed) as unknown;
} catch (err) {
errors.push(`line ${idx + 1}: ${String(err)}`);
return;
}
if (!obj || typeof obj !== "object" || Array.isArray(obj)) {
errors.push(`line ${idx + 1}: expected JSON object`);
return;
}
const record = obj as Record<string, unknown>;
const actionKeys = A2UI_ACTION_KEYS.filter((key) => key in record);
if (actionKeys.length !== 1) {
errors.push(
`line ${idx + 1}: expected exactly one action key (${A2UI_ACTION_KEYS.join(
", ",
)})`,
);
return;
}
if (actionKeys[0] === "createSurface") {
sawV09 = true;
} else {
sawV08 = true;
}
});
if (messageCount === 0) {
errors.push("no JSONL messages found");
}
if (sawV08 && sawV09) {
errors.push("mixed A2UI v0.8 and v0.9 messages in one file");
}
if (errors.length > 0) {
throw new Error(`Invalid A2UI JSONL:\n- ${errors.join("\n- ")}`);
}
const version: A2UIVersion = sawV09 ? "v0.9" : "v0.8";
return { version, messageCount };
}
async function loadNodes(opts: CanvasOpts): Promise<NodeListNode[]> {
try {
const res = (await callGatewayCli("node.list", opts, {})) as unknown;
@@ -389,14 +480,30 @@ export function registerCanvasCli(program: Command) {
.command("push")
.description("Push A2UI JSONL to the canvas")
.option("--jsonl <path>", "Path to JSONL payload")
.option("--text <text>", "Render a quick A2UI text payload")
.option("--node <idOrNameOrIp>", "Node id, name, or IP")
.action(async (opts: CanvasOpts) => {
try {
if (!opts.jsonl) throw new Error("missing --jsonl");
const jsonl = await fs.readFile(String(opts.jsonl), "utf8");
const hasJsonl = Boolean(opts.jsonl);
const hasText = typeof opts.text === "string";
if (hasJsonl === hasText) {
throw new Error("provide exactly one of --jsonl or --text");
}
const jsonl = hasText
? buildA2UITextJsonl(String(opts.text ?? ""))
: await fs.readFile(String(opts.jsonl), "utf8");
const { version, messageCount } = validateA2UIJsonl(jsonl);
if (version === "v0.9") {
throw new Error(
"Detected A2UI v0.9 JSONL (createSurface). Clawdis currently supports v0.8 only.",
);
}
await invokeCanvas(opts, "canvas.a2ui.pushJSONL", { jsonl });
if (!opts.json) {
defaultRuntime.log("canvas a2ui push ok");
defaultRuntime.log(
`canvas a2ui push ok (v0.8, ${messageCount} message${messageCount === 1 ? "" : "s"})`,
);
}
} catch (err) {
defaultRuntime.error(`canvas a2ui push failed: ${String(err)}`);