pmx-canvas 0.1.18 → 0.1.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/CHANGELOG.md +128 -0
  2. package/Readme.md +19 -6
  3. package/dist/canvas/global.css +35 -2
  4. package/dist/canvas/index.js +70 -69
  5. package/dist/json-render/index.js +109 -109
  6. package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
  7. package/dist/types/client/icons.d.ts +2 -0
  8. package/dist/types/client/state/canvas-store.d.ts +2 -0
  9. package/dist/types/client/types.d.ts +2 -1
  10. package/dist/types/json-render/charts/components.d.ts +5 -1
  11. package/dist/types/json-render/renderer/index.d.ts +1 -0
  12. package/dist/types/json-render/server.d.ts +1 -0
  13. package/dist/types/mcp/canvas-access.d.ts +3 -0
  14. package/dist/types/server/canvas-operations.d.ts +4 -0
  15. package/dist/types/server/canvas-schema.d.ts +19 -3
  16. package/dist/types/server/canvas-serialization.d.ts +1 -0
  17. package/dist/types/server/canvas-state.d.ts +8 -2
  18. package/dist/types/server/html-primitives.d.ts +34 -0
  19. package/dist/types/server/index.d.ts +19 -0
  20. package/docs/RELEASE.md +153 -0
  21. package/docs/bun-webview-integration.md +296 -0
  22. package/docs/cli.md +143 -0
  23. package/docs/evals/e2e-cli-coverage.md +61 -0
  24. package/docs/http-api.md +201 -0
  25. package/docs/mcp.md +137 -0
  26. package/docs/node-types.md +272 -0
  27. package/docs/plans/.gitkeep +0 -0
  28. package/docs/plans/plan-001-semantic-watch-mvp.md +335 -0
  29. package/docs/plans/plan-002-human-attention-layer-design-spec.md +679 -0
  30. package/docs/plans/plan-003-human-attention-layer-implementation-plan.md +572 -0
  31. package/docs/reactive-canvas-proposal.md +578 -0
  32. package/docs/release-review-0.1.0.md +38 -0
  33. package/docs/screenshot.png +0 -0
  34. package/docs/screenshots/demo-workbench-dark.png +0 -0
  35. package/docs/screenshots/demo-workbench-light.png +0 -0
  36. package/docs/screenshots/welcome-dark.png +0 -0
  37. package/docs/screenshots/welcome-light.png +0 -0
  38. package/docs/sdk.md +103 -0
  39. package/package.json +2 -1
  40. package/skills/pmx-canvas/SKILL.md +8 -0
  41. package/src/cli/agent.ts +167 -5
  42. package/src/client/App.tsx +20 -1
  43. package/src/client/canvas/AnnotationLayer.tsx +33 -12
  44. package/src/client/canvas/CanvasViewport.tsx +88 -7
  45. package/src/client/canvas/CommandPalette.tsx +1 -1
  46. package/src/client/canvas/ContextMenu.tsx +2 -2
  47. package/src/client/canvas/ExpandedNodeOverlay.tsx +7 -1
  48. package/src/client/icons.tsx +13 -0
  49. package/src/client/nodes/McpAppNode.tsx +12 -4
  50. package/src/client/state/canvas-store.ts +15 -5
  51. package/src/client/state/sse-bridge.ts +4 -3
  52. package/src/client/theme/global.css +35 -2
  53. package/src/client/types.ts +2 -1
  54. package/src/json-render/charts/components.tsx +41 -7
  55. package/src/json-render/charts/extra-components.tsx +13 -12
  56. package/src/json-render/renderer/index.tsx +1 -0
  57. package/src/json-render/server.ts +3 -1
  58. package/src/mcp/canvas-access.ts +25 -0
  59. package/src/mcp/server.ts +85 -27
  60. package/src/server/agent-context.ts +17 -0
  61. package/src/server/canvas-operations.ts +91 -38
  62. package/src/server/canvas-schema.ts +83 -3
  63. package/src/server/canvas-serialization.ts +9 -2
  64. package/src/server/canvas-state.ts +27 -9
  65. package/src/server/demo-state.json +1143 -0
  66. package/src/server/demo.ts +25 -777
  67. package/src/server/html-primitives.ts +990 -0
  68. package/src/server/index.ts +43 -2
  69. package/src/server/server.ts +140 -14
  70. package/src/server/spatial-analysis.ts +3 -3
package/docs/sdk.md ADDED
@@ -0,0 +1,103 @@
1
+ # JavaScript/TypeScript SDK (Bun runtime)
2
+
3
+ The published SDK entrypoint is Bun-first. Node.js consumers should use the
4
+ [CLI](cli.md), [MCP server](mcp.md), or [HTTP API](http-api.md) instead.
5
+
6
+ ```bash
7
+ bun add pmx-canvas
8
+ ```
9
+
10
+ ## Quick example
11
+
12
+ ```ts
13
+ import { createCanvas } from 'pmx-canvas';
14
+
15
+ const canvas = createCanvas({ port: 4313 });
16
+ await canvas.start({ open: true });
17
+
18
+ // Add nodes
19
+ const n1 = canvas.addNode({ type: 'markdown', title: 'Plan', content: '# Step 1\nDo the thing.' });
20
+ const n2 = canvas.addNode({ type: 'status', title: 'Build', content: 'passing' });
21
+ const n3 = canvas.addNode({ type: 'file', content: 'src/index.ts' });
22
+
23
+ // Connect them
24
+ canvas.addEdge({ from: n1, to: n2, type: 'flow' });
25
+
26
+ // Group related nodes
27
+ canvas.createGroup({ title: 'Build Pipeline', childIds: [n1, n2] });
28
+
29
+ // Self-contained HTML in a sandboxed iframe
30
+ canvas.addHtmlNode({
31
+ title: 'Cost projection',
32
+ html: '<canvas id="c"></canvas><script src="https://cdn.jsdelivr.net/npm/chart.js"></script><script>/* ... */</script>',
33
+ });
34
+
35
+ // Generated HTML communication primitive, stored as a sandboxed html node
36
+ canvas.addHtmlPrimitive({
37
+ kind: 'choice-grid',
38
+ title: 'Implementation options',
39
+ data: {
40
+ items: [
41
+ { title: 'Small patch', summary: 'Least disruption.', pros: ['Fast'], cons: ['Less flexible'] },
42
+ ],
43
+ },
44
+ });
45
+
46
+ // Hand-drawn diagram via the Excalidraw MCP-app preset
47
+ await canvas.addDiagram({
48
+ elements: [
49
+ { type: 'rectangle', id: 'r1', x: 80, y: 80, width: 160, height: 60,
50
+ roundness: { type: 3 }, backgroundColor: '#a5d8ff', fillStyle: 'solid',
51
+ label: { text: 'Agent' } },
52
+ ],
53
+ title: 'Quick sketch',
54
+ });
55
+
56
+ // Batch-build a graph and group around it
57
+ await canvas.runBatch([
58
+ {
59
+ op: 'graph.add',
60
+ assign: 'graph',
61
+ args: {
62
+ title: 'Major wins',
63
+ graphType: 'bar',
64
+ data: [
65
+ { label: 'Docs', value: 5 },
66
+ { label: 'Tests', value: 8 },
67
+ ],
68
+ xKey: 'label',
69
+ yKey: 'value',
70
+ },
71
+ },
72
+ {
73
+ op: 'group.create',
74
+ args: {
75
+ title: 'Quarterly graphs',
76
+ childIds: ['$graph.id'],
77
+ },
78
+ },
79
+ ]);
80
+
81
+ // Arrange and inspect
82
+ canvas.arrange('grid');
83
+ console.log(canvas.validate());
84
+ console.log(canvas.getLayout());
85
+ ```
86
+
87
+ ## WebView automation
88
+
89
+ ```ts
90
+ const webview = await canvas.startAutomationWebView({ backend: 'chrome', width: 1280, height: 800 });
91
+ console.log(webview.active);
92
+ console.log(await canvas.evaluateAutomationWebView('document.title'));
93
+ await canvas.resizeAutomationWebView(1440, 900);
94
+ const screenshot = await canvas.screenshotAutomationWebView({ format: 'png' });
95
+ console.log(screenshot.byteLength);
96
+ await canvas.stopAutomationWebView();
97
+ ```
98
+
99
+ ## See also
100
+
101
+ - [Node types](node-types.md) — what each node type is for
102
+ - [HTTP API](http-api.md) — the same operations from any language
103
+ - [MCP reference](mcp.md) — the agent-facing surface
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmx-canvas",
3
- "version": "0.1.18",
3
+ "version": "0.1.20",
4
4
  "description": "Spatial canvas workbench for coding agents — infinite 2D canvas with agent-native CLI, MCP integration, nodes, edges, file watching, and snapshots",
5
5
  "type": "module",
6
6
  "main": "./src/server/index.ts",
@@ -18,6 +18,7 @@
18
18
  "files": [
19
19
  "src/",
20
20
  "skills/",
21
+ "docs/",
21
22
  "dist/canvas/",
22
23
  "dist/json-render/",
23
24
  "dist/types/",
@@ -270,6 +270,7 @@ MCP node-type routing:
270
270
  | Basic nodes (`markdown`, `status`, `context`, `ledger`, `trace`, `file`, `image`, `webpage`) | `canvas_add_node` |
271
271
  | `json-render` | `canvas_add_json_render_node` |
272
272
  | `graph` | `canvas_add_graph_node` |
273
+ | `html-primitive` | `canvas_add_html_primitive` |
273
274
  | `html` | `canvas_add_html_node` |
274
275
  | `web-artifact` | `canvas_build_web_artifact` |
275
276
  | `external-app` / tool-backed `mcp-app` | `canvas_open_mcp_app` |
@@ -659,6 +660,12 @@ server's `ui://` resource as an iframe node on the canvas
659
660
  - Canvas theme tokens are auto-injected as CSS custom properties (both `--c-*` and common `--color-*` aliases such as `--color-text-primary`, `--color-bg`, `--color-accent`) so authored HTML inherits the active theme
660
661
  - Use for moderate-complexity visualizations and interactive widgets that need real JS but do not warrant a full React build (Chart.js demos, D3 sketches, custom HTML report views)
661
662
 
663
+ **`canvas_add_html_primitive`** — Generate a reusable HTML communication primitive as a sandboxed `html` node
664
+ - Required: `kind`; run `canvas_describe_schema` and read `htmlPrimitives` for the current catalog
665
+ - Optional: `title`, `data`, `x`, `y`, `width`, `height`, `strictSize`
666
+ - Use when markdown would be too dense and a structured visual artifact is clearer: tradeoff grids, implementation plans, PR reviews, module maps, design sheets, explainers, reports, and lightweight human-editable boards/editors
667
+ - Read `htmlPrimitives` from `canvas_describe_schema` for the data shape and examples before constructing a payload
668
+
662
669
  ### Choosing the Right Visual Tier
663
670
 
664
671
  When the output is more than markdown, pick the lightest tier that fits:
@@ -666,6 +673,7 @@ When the output is more than markdown, pick the lightest tier that fits:
666
673
  | Tier | Tool | Build cost | When to pick it |
667
674
  |------|------|------------|-----------------|
668
675
  | Declarative UI | `canvas_add_json_render_node` / `canvas_add_graph_node` | None | Schema-driven dashboards, forms, charts; agent-friendly to read back via `canvas_get_node` |
676
+ | Generated HTML primitive | `canvas_add_html_primitive` | None | Reusable communication artifacts such as choices, plans, reviews, maps, reports, decks, and lightweight editors |
669
677
  | Sandboxed HTML+JS | `canvas_add_html_node` | None | Self-contained HTML with inline JS or CDN scripts; one-off visualizations or report views |
670
678
  | Hosted MCP app | `canvas_open_mcp_app` / `canvas_add_diagram` | None | Interactive editors backed by an external MCP server (e.g. Excalidraw) |
671
679
  | Bundled React app | `canvas_build_web_artifact` | Heavy (npm install + bundle) | Multi-component UIs needing React state, routing, shadcn/ui, or Tailwind class composition |
package/src/cli/agent.ts CHANGED
@@ -73,6 +73,15 @@ interface CanvasSchemaResponse {
73
73
  'line' | 'bar' | 'pie' | 'area' | 'scatter' | 'radar' | 'stacked-bar' | 'composed'
74
74
  >;
75
75
  };
76
+ htmlPrimitives?: Array<{
77
+ kind: string;
78
+ title: string;
79
+ description: string;
80
+ useWhen: string;
81
+ defaultSize: { width: number; height: number };
82
+ dataShape: string;
83
+ example: Record<string, unknown>;
84
+ }>;
76
85
  mcp: {
77
86
  tools: string[];
78
87
  resources: string[];
@@ -480,6 +489,14 @@ function parseJsonValue(raw: string, label: string, hint: string): unknown {
480
489
  }
481
490
  }
482
491
 
492
+ function parseJsonRecord(raw: string, label: string, hint: string): Record<string, unknown> {
493
+ const parsed = parseJsonValue(raw, label, hint);
494
+ if (!isRecord(parsed)) {
495
+ die(`${label} must be a JSON object.`, hint);
496
+ }
497
+ return parsed;
498
+ }
499
+
483
500
  async function readTextInput(
484
501
  flags: Record<string, string | true>,
485
502
  options: {
@@ -615,6 +632,34 @@ async function buildJsonRenderRequestBody(
615
632
  return body;
616
633
  }
617
634
 
635
+ async function buildHtmlPrimitiveRequestBody(
636
+ flags: Record<string, string | true>,
637
+ ): Promise<Record<string, unknown>> {
638
+ const hint = 'Use: pmx-canvas html primitive add --kind choice-grid --data-file ./primitive.json --title "Options"';
639
+ const kind = getStringFlag(flags, 'kind', 'primitive');
640
+ if (!kind) die('HTML primitives require --kind.', hint);
641
+ const body: Record<string, unknown> = { type: 'html', primitive: kind };
642
+ if (typeof flags.title === 'string') body.title = flags.title;
643
+ const rawData = await readOptionalTextInput(flags, {
644
+ fileFlags: ['data-file'],
645
+ valueFlags: ['data-json', 'data'],
646
+ allowStdin: true,
647
+ label: 'HTML primitive data',
648
+ hint,
649
+ });
650
+ if (rawData !== undefined) {
651
+ body.data = parseJsonRecord(rawData, 'HTML primitive data', hint);
652
+ }
653
+ applyCommonGeometryFlags(body, flags, {
654
+ x: 'Use a finite number, e.g. --x 500',
655
+ y: 'Use a finite number, e.g. --y 300',
656
+ width: 'Use a positive number, e.g. --width 980',
657
+ height: 'Use a positive number, e.g. --height 720',
658
+ });
659
+ applyStrictSizeFlags(body, flags);
660
+ return body;
661
+ }
662
+
618
663
  async function buildGraphRequestBody(
619
664
  flags: Record<string, string | true>,
620
665
  options: { requireData?: boolean; allowStdin?: boolean } = {},
@@ -967,6 +1012,35 @@ function filterJsonRenderSchemaView(
967
1012
  return flags.summary ? summarizeJsonRenderComponent(component) : component;
968
1013
  }
969
1014
 
1015
+ function summarizeHtmlPrimitive(primitive: NonNullable<CanvasSchemaResponse['htmlPrimitives']>[number]): Record<string, unknown> {
1016
+ return {
1017
+ kind: primitive.kind,
1018
+ title: primitive.title,
1019
+ description: primitive.description,
1020
+ useWhen: primitive.useWhen,
1021
+ defaultSize: primitive.defaultSize,
1022
+ dataShape: primitive.dataShape,
1023
+ };
1024
+ }
1025
+
1026
+ function filterHtmlPrimitiveSchemaView(
1027
+ schema: CanvasSchemaResponse,
1028
+ flags: Record<string, string | true>,
1029
+ ): Record<string, unknown> {
1030
+ const primitives = schema.htmlPrimitives ?? [];
1031
+ const kind = getStringFlag(flags, 'kind', 'primitive');
1032
+ if (!kind) {
1033
+ return {
1034
+ primitives: flags.summary ? primitives.map((entry) => summarizeHtmlPrimitive(entry)) : primitives,
1035
+ };
1036
+ }
1037
+ const primitive = primitives.find((entry) => entry.kind === kind);
1038
+ if (!primitive) {
1039
+ die(`Unknown HTML primitive: ${kind}`, 'Run: pmx-canvas html primitive schema --summary');
1040
+ }
1041
+ return flags.summary ? summarizeHtmlPrimitive(primitive) : primitive;
1042
+ }
1043
+
970
1044
  // ── Commands ─────────────────────────────────────────────────
971
1045
 
972
1046
  const COMMANDS: Record<string, { run: (args: string[]) => Promise<void>; help: string; examples: string[] }> = {};
@@ -1028,6 +1102,7 @@ cmd('node add', 'Add a node to the canvas', [
1028
1102
  'pmx-canvas node add --type file --content "src/index.ts"',
1029
1103
  'pmx-canvas node add --type webpage --url "https://example.com/docs"',
1030
1104
  'pmx-canvas node add --type html --title "Widget" --content "<main>Hello</main>"',
1105
+ 'pmx-canvas node add --type html --primitive choice-grid --data-file ./options.json --title "Options"',
1031
1106
  'pmx-canvas node add --type markdown --title "Note" --x 100 --y 200',
1032
1107
  'pmx-canvas node add --type json-render --title "Ops Dashboard" --spec-file ./dashboard.json',
1033
1108
  'pmx-canvas node add --type graph --graph-type bar --data-file ./metrics.json --x-key label --y-key value',
@@ -1055,6 +1130,18 @@ cmd('node add', 'Add a node to the canvas', [
1055
1130
  return;
1056
1131
  }
1057
1132
 
1133
+ if (type === 'html-primitive') {
1134
+ const result = await api('POST', '/api/canvas/node', await buildHtmlPrimitiveRequestBody(flags));
1135
+ output(result);
1136
+ return;
1137
+ }
1138
+
1139
+ if (type === 'html' && getStringFlag(flags, 'primitive', 'kind')) {
1140
+ const result = await api('POST', '/api/canvas/node', await buildHtmlPrimitiveRequestBody(flags));
1141
+ output(result);
1142
+ return;
1143
+ }
1144
+
1058
1145
  if (type === 'mcp-app') {
1059
1146
  die(
1060
1147
  'mcp-app nodes require tool-backed app metadata and cannot be created with generic node add.',
@@ -1136,6 +1223,28 @@ cmd('json-render', 'Show json-render schema and canonical examples', [
1136
1223
  output(filterJsonRenderSchemaView(schema.jsonRender, flags));
1137
1224
  });
1138
1225
 
1226
+ cmd('html primitive add', 'Create a reusable sandboxed HTML communication primitive', [
1227
+ 'pmx-canvas html primitive add --kind choice-grid --data-file ./options.json --title "Options"',
1228
+ 'pmx-canvas html primitive add --kind plan-timeline --data-json \'{"milestones":[{"title":"Ship","detail":"Implement and verify","status":"next"}]}\'',
1229
+ 'pmx-canvas html primitive add --kind triage-board --data-file ./tickets.json --strict-size',
1230
+ ], async (args) => {
1231
+ const { flags } = parseFlags(args);
1232
+ if (flags.help || flags.h) return showCommandHelp('html primitive add');
1233
+ const result = await api('POST', '/api/canvas/node', await buildHtmlPrimitiveRequestBody(flags));
1234
+ output(result);
1235
+ });
1236
+
1237
+ cmd('html primitive schema', 'Describe reusable HTML communication primitives', [
1238
+ 'pmx-canvas html primitive schema --summary',
1239
+ 'pmx-canvas html primitive schema --kind choice-grid',
1240
+ 'pmx-canvas html primitive schema --kind triage-board --summary',
1241
+ ], async (args) => {
1242
+ const { flags } = parseFlags(args);
1243
+ if (flags.help || flags.h) return showCommandHelp('html primitive schema');
1244
+ const schema = await loadCanvasSchema();
1245
+ output(filterHtmlPrimitiveSchemaView(schema, flags));
1246
+ });
1247
+
1139
1248
  cmd('graph add', 'Add a graph node to the canvas', [
1140
1249
  'pmx-canvas graph add --graph-type bar --data-file ./metrics.json --x-key label --y-key value',
1141
1250
  'pmx-canvas graph add --graphType composed --data \'[{"day":"Mon","visits":10,"conversion":0.4}]\' --xKey day --barKey visits --lineKey conversion',
@@ -1176,6 +1285,7 @@ cmd('node schema', 'Describe server-supported node create schemas and canonical
1176
1285
  rootShape: result.jsonRender.rootShape,
1177
1286
  },
1178
1287
  graph: result.graph,
1288
+ htmlPrimitives: result.htmlPrimitives?.map((entry) => summarizeHtmlPrimitive(entry)) ?? [],
1179
1289
  mcp: result.mcp,
1180
1290
  });
1181
1291
  return;
@@ -1698,6 +1808,7 @@ cmd('snapshot save', 'Save a named snapshot of the current canvas', [
1698
1808
  cmd('snapshot list', 'List saved snapshots', [
1699
1809
  'pmx-canvas snapshot list',
1700
1810
  'pmx-canvas snapshot list --limit 50 --query baseline',
1811
+ 'pmx-canvas snapshot list --after 2026-05-01T00:00:00Z --before 2026-05-05T00:00:00Z',
1701
1812
  'pmx-canvas snapshot list --all',
1702
1813
  ], async (args) => {
1703
1814
  const { flags } = parseFlags(args);
@@ -1706,8 +1817,12 @@ cmd('snapshot list', 'List saved snapshots', [
1706
1817
  const params = new URLSearchParams();
1707
1818
  const limit = optionalNumberFlag(flags, 'limit', 'Use a positive integer, e.g. --limit 50');
1708
1819
  const query = getStringFlag(flags, 'query', 'q');
1820
+ const before = getStringFlag(flags, 'before');
1821
+ const after = getStringFlag(flags, 'after');
1709
1822
  if (limit !== undefined) params.set('limit', String(limit));
1710
1823
  if (query) params.set('q', query);
1824
+ if (before) params.set('before', before);
1825
+ if (after) params.set('after', after);
1711
1826
  if (flags.all) params.set('all', 'true');
1712
1827
  const result = await api('GET', `/api/canvas/snapshots${params.size > 0 ? `?${params.toString()}` : ''}`);
1713
1828
  output(result);
@@ -1893,19 +2008,31 @@ cmd('validate', 'Validate the current layout for collisions and missing edge end
1893
2008
  cmd('validate spec', 'Validate a json-render spec or graph payload without creating a node', [
1894
2009
  'pmx-canvas validate spec --type json-render --spec-file ./dashboard.json',
1895
2010
  'pmx-canvas validate spec --type graph --graph-type bar --data-file ./metrics.json --x-key label --y-key value',
2011
+ 'pmx-canvas validate spec --type html-primitive --kind choice-grid --data-file ./options.json',
1896
2012
  'pmx-canvas validate spec --type json-render --spec-file ./dashboard.json --summary',
1897
2013
  ], async (args) => {
1898
2014
  const { flags } = parseFlags(args);
1899
2015
  if (flags.help || flags.h) return showCommandHelp('validate spec');
1900
2016
 
1901
2017
  const type = getStringFlag(flags, 'type');
1902
- if (type !== 'json-render' && type !== 'graph') {
1903
- die('validate spec requires --type json-render or --type graph.');
2018
+ if (type !== 'json-render' && type !== 'graph' && type !== 'html-primitive') {
2019
+ die('validate spec requires --type json-render, --type graph, or --type html-primitive.');
1904
2020
  }
1905
2021
 
1906
- const body = type === 'json-render'
1907
- ? { type, spec: (await buildJsonRenderRequestBody({ ...flags, title: String(flags.title ?? 'Validation') })).spec }
1908
- : { type, ...(await buildGraphRequestBody(flags)) };
2022
+ let body: Record<string, unknown>;
2023
+ if (type === 'json-render') {
2024
+ body = { type, spec: (await buildJsonRenderRequestBody({ ...flags, title: String(flags.title ?? 'Validation') })).spec };
2025
+ } else if (type === 'html-primitive') {
2026
+ const primitiveBody = await buildHtmlPrimitiveRequestBody(flags);
2027
+ body = {
2028
+ type,
2029
+ kind: primitiveBody.primitive,
2030
+ ...(typeof primitiveBody.title === 'string' ? { title: primitiveBody.title } : {}),
2031
+ ...(isRecord(primitiveBody.data) ? { data: primitiveBody.data } : {}),
2032
+ };
2033
+ } else {
2034
+ body = { type, ...(await buildGraphRequestBody(flags)) };
2035
+ }
1909
2036
 
1910
2037
  const result = await api('POST', '/api/canvas/schema/validate', body) as Record<string, unknown>;
1911
2038
  if (flags.summary) {
@@ -2297,9 +2424,17 @@ function showCommandHelp(name: string): void {
2297
2424
  console.log(' pmx-canvas node add --help --type webpage');
2298
2425
  console.log(' pmx-canvas node add --help --type json-render --component Table');
2299
2426
  console.log(' pmx-canvas node add --help --type graph');
2427
+ console.log(' pmx-canvas html primitive schema --summary');
2300
2428
  console.log(' pmx-canvas node add --help --type webpage --json');
2301
2429
  console.log(' Use --strict-size to keep explicit width/height fixed and scroll overflowing content.');
2302
2430
  }
2431
+ if (name === 'html primitive add' || name === 'html primitive schema') {
2432
+ console.log('\nPrimitive flags:');
2433
+ console.log(' --kind <name> Run `pmx-canvas html primitive schema --summary` for the full catalog');
2434
+ console.log(' --data-file <path> JSON object payload for the primitive');
2435
+ console.log(' --data-json, --data <json> Inline JSON object payload');
2436
+ console.log(' --stdin Read JSON object payload from stdin');
2437
+ }
2303
2438
  if (name === 'json-render') {
2304
2439
  console.log('\nOptions:');
2305
2440
  console.log(' --schema Show json-render catalog schema (default)');
@@ -2314,6 +2449,10 @@ function showCommandHelp(name: string): void {
2314
2449
  console.log(' Use --node-height/--nodeHeight for canvas frame height; use --chart-height for chart content height. --height is kept as a frame-height alias for compatibility.');
2315
2450
  console.log(' Pass --show-legend false to hide legends in compact node layouts.');
2316
2451
  }
2452
+ if (name === 'validate spec') {
2453
+ console.log('\nHTML primitive flags:');
2454
+ console.log(' --type html-primitive --kind <name> --data-file ./payload.json');
2455
+ }
2317
2456
  if (name === 'node schema') {
2318
2457
  console.log('\nFilters:');
2319
2458
  console.log(' --summary Show compact schema summaries');
@@ -2323,13 +2462,26 @@ function showCommandHelp(name: string): void {
2323
2462
  if (name === 'validate spec') {
2324
2463
  console.log('\nOutput control:');
2325
2464
  console.log(' --summary Return only validation summary metadata');
2465
+ console.log(' For --type html-primitive, pass --kind plus optional --data-file/--data-json.');
2326
2466
  }
2327
2467
  if (name === 'snapshot list') {
2328
2468
  console.log('\nOptions:');
2329
2469
  console.log(' --limit <number> Maximum snapshots to return (default 20)');
2330
2470
  console.log(' --query <text> Case-insensitive ID/name filter');
2471
+ console.log(' --before <timestamp> Only return snapshots created at or before this ISO timestamp');
2472
+ console.log(' --after <timestamp> Only return snapshots created at or after this ISO timestamp');
2331
2473
  console.log(' --all Return all snapshots');
2332
2474
  }
2475
+ if (name === 'node update') {
2476
+ console.log('\nTrace fields:');
2477
+ console.log(' --tool-name, --toolName Trace tool or operation label');
2478
+ console.log(' --category <name> Trace category, e.g. mcp, file, subagent, other');
2479
+ console.log(' --status <status> Trace status, e.g. running, success, failed');
2480
+ console.log(' --duration <text> Trace duration badge text');
2481
+ console.log(' --result-summary, --resultSummary <text>');
2482
+ console.log(' Trace result summary');
2483
+ console.log(' --error <text> Trace error message');
2484
+ }
2333
2485
  if (name === 'snapshot gc') {
2334
2486
  console.log('\nOptions:');
2335
2487
  console.log(' --keep <number> Number of newest snapshots to keep (default 20)');
@@ -2398,6 +2550,8 @@ Node commands:
2398
2550
  pmx-canvas node remove <id> Remove a node
2399
2551
  pmx-canvas json-render Show json-render schema/examples
2400
2552
  pmx-canvas graph add [options] Add a graph node
2553
+ pmx-canvas html primitive add Add an HTML communication primitive
2554
+ pmx-canvas html primitive schema List HTML primitive kinds and shapes
2401
2555
 
2402
2556
  Edge commands:
2403
2557
  pmx-canvas edge add [options] Add an edge between nodes
@@ -2471,6 +2625,8 @@ Examples:
2471
2625
  pmx-canvas node add --type web-artifact --title "Dashboard" --app-file ./App.tsx
2472
2626
  pmx-canvas node add --type graph --graph-type bar --data-file ./metrics.json --x-key label --y-key value
2473
2627
  pmx-canvas graph add --graph-type bar --data-file ./metrics.json --x-key label --y-key value
2628
+ pmx-canvas html primitive add --kind choice-grid --data-file ./options.json --title "Options"
2629
+ pmx-canvas html primitive schema --summary
2474
2630
  pmx-canvas node add --help --type webpage
2475
2631
  pmx-canvas node schema --type json-render
2476
2632
  pmx-canvas node schema --type json-render --component Table --summary
@@ -2517,6 +2673,12 @@ export async function runAgentCli(args: string[]): Promise<void> {
2517
2673
  return;
2518
2674
  }
2519
2675
 
2676
+ const threeWord = `${args[0]} ${args[1] ?? ''} ${args[2] ?? ''}`.trim();
2677
+ if (COMMANDS[threeWord]) {
2678
+ await COMMANDS[threeWord].run(args.slice(3));
2679
+ return;
2680
+ }
2681
+
2520
2682
  // Try two-word command first (e.g., "node add"), then one-word (e.g., "search")
2521
2683
  const twoWord = `${args[0]} ${args[1] ?? ''}`.trim();
2522
2684
  if (COMMANDS[twoWord]) {
@@ -52,6 +52,7 @@ import {
52
52
  IconShortcuts,
53
53
  IconSnapshot,
54
54
  IconSun,
55
+ IconTextAnnotation,
55
56
  IconTrace,
56
57
  IconZoomIn,
57
58
  IconZoomOut,
@@ -73,7 +74,7 @@ function sendIntent(type: string, payload: Record<string, unknown> = {}): void {
73
74
  });
74
75
  }
75
76
 
76
- type AnnotationTool = 'pen' | 'eraser' | null;
77
+ type AnnotationTool = 'pen' | 'eraser' | 'text' | null;
77
78
 
78
79
  function ToolbarHint({
79
80
  label,
@@ -115,6 +116,7 @@ function Toolbar({
115
116
  annotationTool,
116
117
  onToggleAnnotationMode,
117
118
  onToggleAnnotationEraser,
119
+ onToggleTextAnnotation,
118
120
  }: {
119
121
  minimapVisible: boolean;
120
122
  onToggleMinimap: () => void;
@@ -126,6 +128,7 @@ function Toolbar({
126
128
  annotationTool: AnnotationTool;
127
129
  onToggleAnnotationMode: () => void;
128
130
  onToggleAnnotationEraser: () => void;
131
+ onToggleTextAnnotation: () => void;
129
132
  }) {
130
133
  const status = connectionStatus.value;
131
134
  const hasSynced = hasInitialServerLayout.value;
@@ -316,6 +319,20 @@ function Toolbar({
316
319
  <IconEraser />
317
320
  </button>
318
321
  </ToolbarHint>
322
+ <ToolbarHint
323
+ label={annotationTool === 'text' ? 'Stop text annotations' : 'Text annotations'}
324
+ detail="Click anywhere to type an intent note"
325
+ >
326
+ <button
327
+ type="button"
328
+ onClick={onToggleTextAnnotation}
329
+ aria-label={annotationTool === 'text' ? 'Stop text annotations' : 'Text annotations'}
330
+ aria-pressed={annotationTool === 'text'}
331
+ style={{ color: annotationTool === 'text' ? 'var(--c-accent)' : undefined }}
332
+ >
333
+ <IconTextAnnotation />
334
+ </button>
335
+ </ToolbarHint>
319
336
 
320
337
  <div class="separator" />
321
338
 
@@ -392,6 +409,7 @@ export function App() {
392
409
  const handleCloseSnapshot = useCallback(() => setSnapshotOpen(false), []);
393
410
  const handleToggleAnnotationMode = useCallback(() => setAnnotationTool((tool) => tool === 'pen' ? null : 'pen'), []);
394
411
  const handleToggleAnnotationEraser = useCallback(() => setAnnotationTool((tool) => tool === 'eraser' ? null : 'eraser'), []);
412
+ const handleToggleTextAnnotation = useCallback(() => setAnnotationTool((tool) => tool === 'text' ? null : 'text'), []);
395
413
 
396
414
  const handleMinimapNavigate = useCallback((x: number, y: number) => {
397
415
  animateViewport({ x, y, scale: viewport.value.scale }, 200);
@@ -520,6 +538,7 @@ export function App() {
520
538
  annotationTool={annotationTool}
521
539
  onToggleAnnotationMode={handleToggleAnnotationMode}
522
540
  onToggleAnnotationEraser={handleToggleAnnotationEraser}
541
+ onToggleTextAnnotation={handleToggleTextAnnotation}
523
542
  />
524
543
  <div class="hud-right">
525
544
  {dockedRight.map((n) => (
@@ -11,18 +11,39 @@ export function AnnotationLayer({ annotations }: { annotations: CanvasAnnotation
11
11
 
12
12
  return (
13
13
  <svg class="annotation-layer" aria-hidden="true">
14
- {annotations.map((annotation) => (
15
- <path
16
- key={annotation.id}
17
- d={pointsToPath(annotation.points)}
18
- fill="none"
19
- stroke={annotation.color === 'currentColor' ? 'var(--c-annotation)' : annotation.color}
20
- stroke-width={annotation.width}
21
- stroke-linecap="round"
22
- stroke-linejoin="round"
23
- opacity="0.9"
24
- />
25
- ))}
14
+ {annotations.map((annotation) => {
15
+ const color = annotation.color === 'currentColor' ? 'var(--c-annotation)' : annotation.color;
16
+ if (annotation.type === 'text') {
17
+ const point = annotation.points[0];
18
+ if (!point || !annotation.text) return null;
19
+ return (
20
+ <text
21
+ key={annotation.id}
22
+ x={point.x}
23
+ y={point.y}
24
+ fill={color}
25
+ font-size={annotation.width}
26
+ font-family="var(--font)"
27
+ font-weight="700"
28
+ opacity="0.95"
29
+ >
30
+ {annotation.text}
31
+ </text>
32
+ );
33
+ }
34
+ return (
35
+ <path
36
+ key={annotation.id}
37
+ d={pointsToPath(annotation.points)}
38
+ fill="none"
39
+ stroke={color}
40
+ stroke-width={annotation.width}
41
+ stroke-linecap="round"
42
+ stroke-linejoin="round"
43
+ opacity="0.9"
44
+ />
45
+ );
46
+ })}
26
47
  </svg>
27
48
  );
28
49
  }