From 4726aaa08cf1000f04978e504986116c450f28ea Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 4 Jun 2026 08:07:38 -0400 Subject: [PATCH] docs: document canvas plugin --- extensions/canvas/cli-metadata.ts | 3 +++ extensions/canvas/index.ts | 4 ++++ extensions/canvas/runtime-api.ts | 4 ++++ extensions/canvas/scripts/bundle-a2ui.mjs | 8 ++++++++ extensions/canvas/scripts/copy-a2ui.mjs | 4 ++++ extensions/canvas/scripts/pnpm-runner.mjs | 4 ++++ extensions/canvas/setup-api.ts | 3 +++ extensions/canvas/src/a2ui-jsonl.ts | 6 ++++++ extensions/canvas/src/capability.ts | 9 +++++++++ extensions/canvas/src/cli-helpers.ts | 6 ++++++ extensions/canvas/src/cli.ts | 8 ++++++++ extensions/canvas/src/config-migration.ts | 4 ++++ extensions/canvas/src/config.ts | 10 ++++++++++ extensions/canvas/src/documents.ts | 9 +++++++++ extensions/canvas/src/host-url.ts | 4 ++++ extensions/canvas/src/host/a2ui-app/bootstrap.js | 4 ++++ .../canvas/src/host/a2ui-app/rolldown.config.mjs | 3 +++ extensions/canvas/src/host/a2ui-shared.ts | 8 ++++++++ extensions/canvas/src/host/a2ui.ts | 4 ++++ extensions/canvas/src/host/file-resolver.ts | 5 +++++ extensions/canvas/src/host/server.ts | 10 ++++++++++ extensions/canvas/src/http-route.ts | 6 ++++++ extensions/canvas/src/tool-schema.ts | 6 ++++++ extensions/canvas/src/tool.ts | 5 +++++ 24 files changed, 137 insertions(+) diff --git a/extensions/canvas/cli-metadata.ts b/extensions/canvas/cli-metadata.ts index 0af33a762db8..fc51683f5f79 100644 --- a/extensions/canvas/cli-metadata.ts +++ b/extensions/canvas/cli-metadata.ts @@ -1,3 +1,6 @@ +/** + * Canvas CLI metadata entrypoint used for lightweight command discovery. + */ import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; export default definePluginEntry({ diff --git a/extensions/canvas/index.ts b/extensions/canvas/index.ts index 5c75283efeb0..19b4932b6565 100644 --- a/extensions/canvas/index.ts +++ b/extensions/canvas/index.ts @@ -1,3 +1,7 @@ +/** + * Canvas plugin entrypoint for node canvas control, hosted A2UI routes, and + * node CLI registration. + */ import type { IncomingMessage, ServerResponse } from "node:http"; import type { Duplex } from "node:stream"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; diff --git a/extensions/canvas/runtime-api.ts b/extensions/canvas/runtime-api.ts index 9962ec980b54..607251f64b89 100644 --- a/extensions/canvas/runtime-api.ts +++ b/extensions/canvas/runtime-api.ts @@ -1,3 +1,7 @@ +/** + * Runtime API exports for Canvas plugin host, document, CLI, and capability + * helpers. + */ export { canvasConfigSchema, isCanvasHostEnabled, diff --git a/extensions/canvas/scripts/bundle-a2ui.mjs b/extensions/canvas/scripts/bundle-a2ui.mjs index dba655595369..ca82f07c93e8 100644 --- a/extensions/canvas/scripts/bundle-a2ui.mjs +++ b/extensions/canvas/scripts/bundle-a2ui.mjs @@ -1,4 +1,7 @@ #!/usr/bin/env node +/** + * Bundles the Canvas A2UI web app and writes a hash for tracked inputs. + */ import { spawnSync } from "node:child_process"; import { createHash } from "node:crypto"; @@ -42,10 +45,12 @@ function normalizePath(filePath) { return filePath.split(path.sep).join("/"); } +/** Returns whether a path should participate in the A2UI bundle input hash. */ export function isBundleHashInputPath(filePath, repoRoot = rootDir) { return Boolean(filePath && repoRoot); } +/** Returns local Rolldown CLI candidates for the current install layout. */ export function getLocalRolldownCliCandidates(repoRoot = rootDir) { return [ path.join(repoRoot, "node_modules", "rolldown", "bin", "cli.mjs"), @@ -63,6 +68,7 @@ export function getLocalRolldownCliCandidates(repoRoot = rootDir) { ]; } +/** Returns repository paths that define the A2UI bundle hash inputs. */ export function getBundleHashRepoInputPaths(repoRoot = rootDir) { return [ path.join(repoRoot, "package.json"), @@ -71,10 +77,12 @@ export function getBundleHashRepoInputPaths(repoRoot = rootDir) { ]; } +/** Returns A2UI bundle hash input paths. */ export function getBundleHashInputPaths(repoRoot = rootDir) { return getBundleHashRepoInputPaths(repoRoot); } +/** Compares paths after normalizing separators to POSIX slashes. */ export function compareNormalizedPaths(left, right) { const normalizedLeft = normalizePath(left); const normalizedRight = normalizePath(right); diff --git a/extensions/canvas/scripts/copy-a2ui.mjs b/extensions/canvas/scripts/copy-a2ui.mjs index 0c4576191a8a..18176cb64d2d 100644 --- a/extensions/canvas/scripts/copy-a2ui.mjs +++ b/extensions/canvas/scripts/copy-a2ui.mjs @@ -1,4 +1,7 @@ #!/usr/bin/env node +/** + * Copies bundled Canvas A2UI assets into the dist host asset directory. + */ import fs from "node:fs/promises"; import path from "node:path"; @@ -17,6 +20,7 @@ function shouldSkipMissingA2uiAssets(env = process.env) { return env.OPENCLAW_A2UI_SKIP_MISSING === "1" || Boolean(env.OPENCLAW_SPARSE_PROFILE); } +/** Copies A2UI assets, optionally tolerating missing bundles in sparse builds. */ export async function copyA2uiAssets({ srcDir, outDir }) { const skipMissing = shouldSkipMissingA2uiAssets(process.env); try { diff --git a/extensions/canvas/scripts/pnpm-runner.mjs b/extensions/canvas/scripts/pnpm-runner.mjs index edd69f461d6d..651e64bb7c46 100644 --- a/extensions/canvas/scripts/pnpm-runner.mjs +++ b/extensions/canvas/scripts/pnpm-runner.mjs @@ -1,3 +1,6 @@ +/** + * Cross-platform pnpm command resolver used by Canvas build scripts. + */ import { accessSync, closeSync, constants, openSync, readSync, statSync } from "node:fs"; const WINDOWS_UNSAFE_CMD_CHARS_RE = /[&|<>%\r\n]/; @@ -117,6 +120,7 @@ function resolveConfiguredPnpmExec(params) { return undefined; } +/** Resolves a safe pnpm command spec for Unix, Windows, and npm_execpath launches. */ export function resolvePnpmRunner(params = {}) { const configured = resolveConfiguredPnpmExec(params); if (configured) { diff --git a/extensions/canvas/setup-api.ts b/extensions/canvas/setup-api.ts index 56abb3b0b030..60f44dd11c11 100644 --- a/extensions/canvas/setup-api.ts +++ b/extensions/canvas/setup-api.ts @@ -1,3 +1,6 @@ +/** + * Canvas setup entrypoint that exposes config migrations. + */ import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; import { migrateLegacyCanvasHostConfig } from "./src/config-migration.js"; diff --git a/extensions/canvas/src/a2ui-jsonl.ts b/extensions/canvas/src/a2ui-jsonl.ts index 80c73e567d8d..4054bfc184d4 100644 --- a/extensions/canvas/src/a2ui-jsonl.ts +++ b/extensions/canvas/src/a2ui-jsonl.ts @@ -1,3 +1,6 @@ +/** + * A2UI JSONL helpers for Canvas text rendering and validation. + */ const A2UI_ACTION_KEYS = [ "beginRendering", "surfaceUpdate", @@ -6,8 +9,10 @@ const A2UI_ACTION_KEYS = [ "createSurface", ] as const; +/** Supported A2UI message dialects accepted by the Canvas host. */ export type A2UIVersion = "v0.8" | "v0.9"; +/** Builds a minimal A2UI JSONL payload that renders text in a single surface. */ export function buildA2UITextJsonl(text: string) { const surfaceId = "main"; const rootId = "root"; @@ -35,6 +40,7 @@ export function buildA2UITextJsonl(text: string) { return payloads.map((payload) => JSON.stringify(payload)).join("\n"); } +/** Validates A2UI JSONL and returns the detected dialect/version metadata. */ export function validateA2UIJsonl(jsonl: string) { const lines = jsonl.split(/\r?\n/); const errors: string[] = []; diff --git a/extensions/canvas/src/capability.ts b/extensions/canvas/src/capability.ts index bcf2472d9b0f..c8dbb62c8434 100644 --- a/extensions/canvas/src/capability.ts +++ b/extensions/canvas/src/capability.ts @@ -1,3 +1,6 @@ +/** + * Canvas capability-token helpers for scoped hosted node URLs. + */ import { buildPluginNodeCapabilityScopedHostUrl, DEFAULT_PLUGIN_NODE_CAPABILITY_TTL_MS, @@ -7,19 +10,25 @@ import { type NormalizedPluginNodeCapabilityUrl, } from "openclaw/plugin-sdk/gateway-runtime"; +/** Path prefix used for Canvas capability-scoped gateway routes. */ export const CANVAS_CAPABILITY_PATH_PREFIX = PLUGIN_NODE_CAPABILITY_PATH_PREFIX; +/** Default Canvas capability token TTL in milliseconds. */ export const CANVAS_CAPABILITY_TTL_MS = DEFAULT_PLUGIN_NODE_CAPABILITY_TTL_MS; +/** Normalized Canvas capability-scoped URL shape. */ export type NormalizedCanvasScopedUrl = NormalizedPluginNodeCapabilityUrl; +/** Creates a new opaque Canvas capability token. */ export function mintCanvasCapabilityToken(): string { return mintPluginNodeCapabilityToken(); } +/** Builds a Canvas host URL scoped by the supplied capability token. */ export function buildCanvasScopedHostUrl(baseUrl: string, capability: string): string | undefined { return buildPluginNodeCapabilityScopedHostUrl(baseUrl, capability); } +/** Normalizes and validates a Canvas capability-scoped URL. */ export function normalizeCanvasScopedUrl(rawUrl: string): NormalizedCanvasScopedUrl { return normalizePluginNodeCapabilityScopedUrl(rawUrl); } diff --git a/extensions/canvas/src/cli-helpers.ts b/extensions/canvas/src/cli-helpers.ts index 649db0e6f5d1..ba751b27f2e7 100644 --- a/extensions/canvas/src/cli-helpers.ts +++ b/extensions/canvas/src/cli-helpers.ts @@ -1,3 +1,6 @@ +/** + * Shared Canvas CLI helpers for snapshot payload parsing and temp paths. + */ import { randomUUID } from "node:crypto"; import fs from "node:fs"; import * as path from "node:path"; @@ -20,6 +23,7 @@ function normalizeCanvasSnapshotFormat(value: string | undefined): CanvasSnapsho return null; } +/** Normalizes Canvas snapshot output extensions, mapping jpeg to jpg. */ export function normalizeCanvasSnapshotFileExtension(value: string): CanvasSnapshotFileExtension { const format = normalizeCanvasSnapshotFormat(value.startsWith(".") ? value.slice(1) : value); if (!format) { @@ -28,6 +32,7 @@ export function normalizeCanvasSnapshotFileExtension(value: string): CanvasSnaps return format === "jpeg" ? "jpg" : format; } +/** Parses the node.invoke canvas.snapshot payload shape. */ export function parseCanvasSnapshotPayload(value: unknown): CanvasSnapshotPayload { const obj = asRecord(value); const format = normalizeCanvasSnapshotFormat(readStringValue(obj.format)); @@ -61,6 +66,7 @@ function resolveTempPathParts(opts: { ext: string; tmpDir?: string; id?: string }; } +/** Builds a safe temp path for a Canvas snapshot output file. */ export function canvasSnapshotTempPath(opts: { ext: string; tmpDir?: string; id?: string }) { const { tmpDir, id, ext } = resolveTempPathParts(opts); const cliName = resolveCliName(); diff --git a/extensions/canvas/src/cli.ts b/extensions/canvas/src/cli.ts index a4fac02f7737..8ee212964353 100644 --- a/extensions/canvas/src/cli.ts +++ b/extensions/canvas/src/cli.ts @@ -1,3 +1,6 @@ +/** + * Canvas node CLI command registration and runtime dependency wiring. + */ import { randomUUID } from "node:crypto"; import fs from "node:fs/promises"; import type { Command } from "commander"; @@ -21,6 +24,7 @@ import { shortenHomePath } from "openclaw/plugin-sdk/text-utility-runtime"; import { buildA2UITextJsonl, validateA2UIJsonl } from "./a2ui-jsonl.js"; import { canvasSnapshotTempPath, parseCanvasSnapshotPayload } from "./cli-helpers.js"; +/** Runtime output surface used by Canvas CLI commands. */ export type CanvasCliRuntime = { log: (message: string) => void; error: (message: string) => void; @@ -28,6 +32,7 @@ export type CanvasCliRuntime = { writeJson: (value: unknown) => void; }; +/** Parent node/gateway options consumed by Canvas CLI commands. */ export type CanvasNodesRpcOpts = { url?: string; token?: string; @@ -48,6 +53,7 @@ export type CanvasNodesRpcOpts = { quality?: string; }; +/** Dependency bundle used to keep Canvas CLI commands testable. */ export type CanvasCliDependencies = { defaultRuntime: CanvasCliRuntime; nodesCallOpts: (cmd: Command, defaults?: { timeoutMs?: number }) => Command; @@ -173,6 +179,7 @@ function unauthorizedHintForMessage(message: string): string | null { return null; } +/** Creates the default Canvas CLI dependency bundle backed by the OpenClaw gateway CLI. */ export function createDefaultCanvasCliDependencies(): CanvasCliDependencies { const nodesCallOpts = (cmd: Command, defaults?: { timeoutMs?: number }) => cmd @@ -252,6 +259,7 @@ async function invokeCanvas( ); } +/** Registers Canvas subcommands under the nodes CLI command group. */ export function registerNodesCanvasCommands(nodes: Command, deps: CanvasCliDependencies) { const canvas = nodes .command("canvas") diff --git a/extensions/canvas/src/config-migration.ts b/extensions/canvas/src/config-migration.ts index 564fa84862fb..bcf31a380a0e 100644 --- a/extensions/canvas/src/config-migration.ts +++ b/extensions/canvas/src/config-migration.ts @@ -1,3 +1,6 @@ +/** + * Canvas config migration from legacy root canvasHost config to plugin config. + */ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; import { asOptionalRecord as readRecord } from "openclaw/plugin-sdk/string-coerce-runtime"; @@ -10,6 +13,7 @@ function mergeHostConfig(params: { return Object.assign({}, params.legacyHost, params.existingHost); } +/** Migrates root canvasHost config into plugins.entries.canvas.config.host. */ export function migrateLegacyCanvasHostConfig(config: OpenClawConfig): { config: OpenClawConfig; changes: string[]; diff --git a/extensions/canvas/src/config.ts b/extensions/canvas/src/config.ts index f1ac5bd1e57f..726248e9c26c 100644 --- a/extensions/canvas/src/config.ts +++ b/extensions/canvas/src/config.ts @@ -1,3 +1,6 @@ +/** + * Canvas plugin config parsing, enablement, and schema metadata. + */ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; import { normalizePluginsConfig, @@ -11,6 +14,7 @@ import { readStringValue as readString, } from "openclaw/plugin-sdk/string-coerce-runtime"; +/** Host-server configuration for Canvas and A2UI assets. */ export type CanvasHostConfig = { enabled?: boolean; root?: string; @@ -18,6 +22,7 @@ export type CanvasHostConfig = { liveReload?: boolean; }; +/** Canvas plugin configuration shape. */ export type CanvasPluginConfig = { host?: CanvasHostConfig; }; @@ -47,6 +52,7 @@ function parseCanvasHostConfig(value: unknown): CanvasHostConfig | undefined { }; } +/** Parses raw Canvas plugin config into a typed, normalized shape. */ export function parseCanvasPluginConfig(value: unknown): CanvasPluginConfig { if (!isRecord(value)) { return {}; @@ -55,6 +61,7 @@ export function parseCanvasPluginConfig(value: unknown): CanvasPluginConfig { return host ? { host } : {}; } +/** Returns whether the bundled Canvas plugin is effectively enabled. */ export function isCanvasPluginEnabled(config?: OpenClawConfig): boolean { if (!config) { return true; @@ -68,6 +75,7 @@ export function isCanvasPluginEnabled(config?: OpenClawConfig): boolean { }).enabled; } +/** Resolves Canvas host config from plugin config or root config. */ export function resolveCanvasHostConfig(params: { config?: OpenClawConfig; pluginConfig?: Record; @@ -78,6 +86,7 @@ export function resolveCanvasHostConfig(params: { return parsedPluginConfig.host ?? {}; } +/** Returns whether the Canvas hosted route/server surface should be active. */ export function isCanvasHostEnabled(config?: OpenClawConfig): boolean { if (isTruthyEnvValue(process.env.OPENCLAW_SKIP_CANVAS_HOST)) { return false; @@ -88,6 +97,7 @@ export function isCanvasHostEnabled(config?: OpenClawConfig): boolean { return resolveCanvasHostConfig({ config }).enabled !== false; } +/** Config schema metadata for Canvas plugin settings. */ export const canvasConfigSchema: CanvasPluginConfigSchema = { parse: parseCanvasPluginConfig, uiHints: { diff --git a/extensions/canvas/src/documents.ts b/extensions/canvas/src/documents.ts index 32dcfc5af0d8..ef396e616f53 100644 --- a/extensions/canvas/src/documents.ts +++ b/extensions/canvas/src/documents.ts @@ -1,3 +1,7 @@ +/** + * Canvas document materialization helpers for hosted HTML, media, documents, + * and asset manifests. + */ import { randomUUID } from "node:crypto"; import fs from "node:fs/promises"; import path from "node:path"; @@ -122,6 +126,7 @@ function resolveCanvasDocumentsDir(rootDir?: string, stateDir = resolveStateDir( return path.join(resolveCanvasRootDir(rootDir, stateDir), CANVAS_DOCUMENTS_DIR_NAME); } +/** Resolves the on-disk directory for one Canvas document id. */ export function resolveCanvasDocumentDir( documentId: string, options?: { rootDir?: string; stateDir?: string }, @@ -129,6 +134,7 @@ export function resolveCanvasDocumentDir( return path.join(resolveCanvasDocumentsDir(options?.rootDir, options?.stateDir), documentId); } +/** Builds the hosted URL path for a Canvas document entrypoint. */ export function buildCanvasDocumentEntryUrl(documentId: string, entrypoint: string): string { const normalizedEntrypoint = normalizeLogicalPath(entrypoint); const encodedEntrypoint = normalizedEntrypoint @@ -142,6 +148,7 @@ function buildCanvasDocumentAssetUrl(documentId: string, logicalPath: string): s return buildCanvasDocumentEntryUrl(documentId, logicalPath); } +/** Maps a Canvas hosted document URL path back to a local file path. */ export function resolveCanvasHttpPathToLocalPath( requestPath: string, options?: { rootDir?: string; stateDir?: string }, @@ -289,6 +296,7 @@ async function materializeEntrypoint( }; } +/** Creates a Canvas document directory, copies assets, and writes its manifest. */ export async function createCanvasDocument( input: CanvasDocumentCreateInput, options?: { stateDir?: string; workspaceDir?: string; canvasRootDir?: string }, @@ -322,6 +330,7 @@ export async function createCanvasDocument( return manifest; } +/** Resolves manifest assets to local paths and hosted URLs. */ export function resolveCanvasDocumentAssets( manifest: CanvasDocumentManifest, options?: { baseUrl?: string; stateDir?: string; canvasRootDir?: string }, diff --git a/extensions/canvas/src/host-url.ts b/extensions/canvas/src/host-url.ts index 028a798b0aa9..4d24762311dd 100644 --- a/extensions/canvas/src/host-url.ts +++ b/extensions/canvas/src/host-url.ts @@ -1,3 +1,6 @@ +/** + * Canvas hosted-surface URL resolver. + */ import { resolveHostedPluginSurfaceUrl, type HostedPluginSurfaceUrlParams, @@ -7,6 +10,7 @@ type CanvasHostUrlParams = Omit & { canvasPort?: number; }; +/** Resolves the externally visible Canvas host URL for a gateway/plugin surface. */ export function resolveCanvasHostUrl(params: CanvasHostUrlParams) { return resolveHostedPluginSurfaceUrl({ ...params, diff --git a/extensions/canvas/src/host/a2ui-app/bootstrap.js b/extensions/canvas/src/host/a2ui-app/bootstrap.js index f1aba7394dcd..a8c2df8b5ffc 100644 --- a/extensions/canvas/src/host/a2ui-app/bootstrap.js +++ b/extensions/canvas/src/host/a2ui-app/bootstrap.js @@ -1,3 +1,7 @@ +/** + * Canvas A2UI browser bootstrap that installs theme overrides and native bridge + * helpers. + */ import { v0_8 } from "@a2ui/lit"; import { ContextProvider } from "@lit/context"; import { themeContext } from "@openclaw/a2ui-theme-context"; diff --git a/extensions/canvas/src/host/a2ui-app/rolldown.config.mjs b/extensions/canvas/src/host/a2ui-app/rolldown.config.mjs index ab9cbfa64cc9..38a0b2d73a96 100644 --- a/extensions/canvas/src/host/a2ui-app/rolldown.config.mjs +++ b/extensions/canvas/src/host/a2ui-app/rolldown.config.mjs @@ -1,3 +1,6 @@ +/** + * Rolldown config for bundling the Canvas A2UI app into a single browser asset. + */ import { existsSync } from "node:fs"; import { createRequire } from "node:module"; import path from "node:path"; diff --git a/extensions/canvas/src/host/a2ui-shared.ts b/extensions/canvas/src/host/a2ui-shared.ts index d3aee7564d3a..4e92128c209b 100644 --- a/extensions/canvas/src/host/a2ui-shared.ts +++ b/extensions/canvas/src/host/a2ui-shared.ts @@ -1,15 +1,23 @@ +/** + * Shared A2UI/Canvas host paths and live-reload injection helpers. + */ import { lowercasePreservingWhitespace } from "openclaw/plugin-sdk/string-coerce-runtime"; +/** Hosted path prefix for bundled A2UI assets. */ export const A2UI_PATH = "/__openclaw__/a2ui"; +/** Hosted path prefix for Canvas document/static assets. */ export const CANVAS_HOST_PATH = "/__openclaw__/canvas"; +/** Hosted WebSocket path for Canvas live reload. */ export const CANVAS_WS_PATH = "/__openclaw__/ws"; +/** Returns whether a URL path targets the hosted A2UI asset surface. */ export function isA2uiPath(pathname: string): boolean { return pathname === A2UI_PATH || pathname.startsWith(`${A2UI_PATH}/`); } +/** Injects Canvas bridge helpers and live-reload WebSocket code into HTML. */ export function injectCanvasLiveReload(html: string): string { const snippet = `