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.
- package/CHANGELOG.md +128 -0
- package/Readme.md +19 -6
- package/dist/canvas/global.css +35 -2
- package/dist/canvas/index.js +70 -69
- package/dist/json-render/index.js +109 -109
- package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
- package/dist/types/client/icons.d.ts +2 -0
- package/dist/types/client/state/canvas-store.d.ts +2 -0
- package/dist/types/client/types.d.ts +2 -1
- package/dist/types/json-render/charts/components.d.ts +5 -1
- package/dist/types/json-render/renderer/index.d.ts +1 -0
- package/dist/types/json-render/server.d.ts +1 -0
- package/dist/types/mcp/canvas-access.d.ts +3 -0
- package/dist/types/server/canvas-operations.d.ts +4 -0
- package/dist/types/server/canvas-schema.d.ts +19 -3
- package/dist/types/server/canvas-serialization.d.ts +1 -0
- package/dist/types/server/canvas-state.d.ts +8 -2
- package/dist/types/server/html-primitives.d.ts +34 -0
- package/dist/types/server/index.d.ts +19 -0
- package/docs/RELEASE.md +153 -0
- package/docs/bun-webview-integration.md +296 -0
- package/docs/cli.md +143 -0
- package/docs/evals/e2e-cli-coverage.md +61 -0
- package/docs/http-api.md +201 -0
- package/docs/mcp.md +137 -0
- package/docs/node-types.md +272 -0
- package/docs/plans/.gitkeep +0 -0
- package/docs/plans/plan-001-semantic-watch-mvp.md +335 -0
- package/docs/plans/plan-002-human-attention-layer-design-spec.md +679 -0
- package/docs/plans/plan-003-human-attention-layer-implementation-plan.md +572 -0
- package/docs/reactive-canvas-proposal.md +578 -0
- package/docs/release-review-0.1.0.md +38 -0
- package/docs/screenshot.png +0 -0
- package/docs/screenshots/demo-workbench-dark.png +0 -0
- package/docs/screenshots/demo-workbench-light.png +0 -0
- package/docs/screenshots/welcome-dark.png +0 -0
- package/docs/screenshots/welcome-light.png +0 -0
- package/docs/sdk.md +103 -0
- package/package.json +2 -1
- package/skills/pmx-canvas/SKILL.md +8 -0
- package/src/cli/agent.ts +167 -5
- package/src/client/App.tsx +20 -1
- package/src/client/canvas/AnnotationLayer.tsx +33 -12
- package/src/client/canvas/CanvasViewport.tsx +88 -7
- package/src/client/canvas/CommandPalette.tsx +1 -1
- package/src/client/canvas/ContextMenu.tsx +2 -2
- package/src/client/canvas/ExpandedNodeOverlay.tsx +7 -1
- package/src/client/icons.tsx +13 -0
- package/src/client/nodes/McpAppNode.tsx +12 -4
- package/src/client/state/canvas-store.ts +15 -5
- package/src/client/state/sse-bridge.ts +4 -3
- package/src/client/theme/global.css +35 -2
- package/src/client/types.ts +2 -1
- package/src/json-render/charts/components.tsx +41 -7
- package/src/json-render/charts/extra-components.tsx +13 -12
- package/src/json-render/renderer/index.tsx +1 -0
- package/src/json-render/server.ts +3 -1
- package/src/mcp/canvas-access.ts +25 -0
- package/src/mcp/server.ts +85 -27
- package/src/server/agent-context.ts +17 -0
- package/src/server/canvas-operations.ts +91 -38
- package/src/server/canvas-schema.ts +83 -3
- package/src/server/canvas-serialization.ts +9 -2
- package/src/server/canvas-state.ts +27 -9
- package/src/server/demo-state.json +1143 -0
- package/src/server/demo.ts +25 -777
- package/src/server/html-primitives.ts +990 -0
- package/src/server/index.ts +43 -2
- package/src/server/server.ts +140 -14
- 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.
|
|
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
|
|
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
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
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]) {
|
package/src/client/App.tsx
CHANGED
|
@@ -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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
}
|