pmx-canvas 0.6.4 → 0.6.5

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 (78) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/Readme.md +7 -1
  3. package/dist/canvas/global.css +58 -4
  4. package/dist/canvas/index.js +92 -92
  5. package/dist/canvas/mermaid-entry.js +258 -258
  6. package/dist/types/cli/agent.d.ts +1 -0
  7. package/dist/types/cli/commands/record.d.ts +12 -0
  8. package/dist/types/client/canvas/Presentation.d.ts +13 -0
  9. package/dist/types/client/canvas/use-node-drag.d.ts +2 -1
  10. package/dist/types/client/nodes/McpAppNode.d.ts +24 -1
  11. package/dist/types/client/state/presentation.d.ts +3 -0
  12. package/dist/types/client/types.d.ts +1 -1
  13. package/dist/types/json-render/catalog.d.ts +2 -2
  14. package/dist/types/json-render/server.d.ts +2 -0
  15. package/dist/types/server/canvas-db.d.ts +2 -0
  16. package/dist/types/server/canvas-operations.d.ts +22 -0
  17. package/dist/types/server/canvas-schema.d.ts +1 -0
  18. package/dist/types/server/canvas-state.d.ts +6 -0
  19. package/dist/types/server/canvas-validation.d.ts +3 -0
  20. package/dist/types/server/html-surface.d.ts +1 -1
  21. package/dist/types/server/index.d.ts +4 -0
  22. package/dist/types/server/operations/ops/app.d.ts +2 -0
  23. package/dist/types/server/operations/ops/json-render.d.ts +2 -0
  24. package/dist/types/server/operations/ops/nodes.d.ts +3 -1
  25. package/dist/types/server/web-artifacts.d.ts +3 -0
  26. package/dist/types/shared/tour.d.ts +72 -0
  27. package/docs/cli.md +89 -0
  28. package/docs/http-api.md +79 -4
  29. package/docs/mcp.md +25 -1
  30. package/docs/node-types.md +80 -0
  31. package/package.json +1 -1
  32. package/skills/pmx-canvas/SKILL.md +10 -2
  33. package/skills/pmx-canvas/references/full-reference.md +1 -1
  34. package/skills/pmx-canvas/references/installing-pmx-canvas.md +4 -4
  35. package/src/cli/agent.ts +5 -0
  36. package/src/cli/commands/nodes.ts +10 -0
  37. package/src/cli/commands/record.ts +218 -0
  38. package/src/cli/index.ts +2 -0
  39. package/src/client/App.tsx +7 -1
  40. package/src/client/canvas/AgentPresenceLayer.tsx +1 -1
  41. package/src/client/canvas/CanvasNode.tsx +8 -0
  42. package/src/client/canvas/IntentLayer.tsx +9 -2
  43. package/src/client/canvas/Presentation.tsx +108 -0
  44. package/src/client/canvas/TopBar.tsx +28 -2
  45. package/src/client/canvas/use-node-drag.ts +8 -3
  46. package/src/client/mermaid-entry.ts +39 -0
  47. package/src/client/nodes/HtmlNode.tsx +12 -13
  48. package/src/client/nodes/McpAppNode.tsx +105 -13
  49. package/src/client/nodes/MermaidNode.tsx +3 -2
  50. package/src/client/state/intent-store.ts +5 -1
  51. package/src/client/state/presentation.ts +7 -0
  52. package/src/client/state/session-store.ts +3 -0
  53. package/src/client/state/sse-bridge.ts +3 -0
  54. package/src/client/theme/global.css +58 -4
  55. package/src/json-render/server.ts +29 -0
  56. package/src/server/agent-presence.ts +15 -9
  57. package/src/server/canvas-db.ts +15 -0
  58. package/src/server/canvas-operations.ts +115 -22
  59. package/src/server/canvas-schema.ts +10 -0
  60. package/src/server/canvas-state.ts +39 -4
  61. package/src/server/canvas-validation.ts +9 -1
  62. package/src/server/demo-state.json +11 -7
  63. package/src/server/html-primitives.ts +199 -91
  64. package/src/server/html-surface.ts +8 -3
  65. package/src/server/index.ts +12 -0
  66. package/src/server/operations/composites.ts +3 -1
  67. package/src/server/operations/ops/app.ts +10 -0
  68. package/src/server/operations/ops/batch.ts +12 -3
  69. package/src/server/operations/ops/groups.ts +14 -7
  70. package/src/server/operations/ops/json-render.ts +37 -5
  71. package/src/server/operations/ops/nodes.ts +42 -16
  72. package/src/server/operations/ops/viewport.ts +32 -0
  73. package/src/server/operations/registry.ts +6 -0
  74. package/src/server/scope-fence.ts +1 -1
  75. package/src/server/server.ts +9 -6
  76. package/src/server/web-artifacts.ts +9 -2
  77. package/src/server/workboard.ts +3 -3
  78. package/src/shared/tour.ts +87 -0
package/docs/http-api.md CHANGED
@@ -9,6 +9,40 @@ bodies are treated as an empty request. As of 0.4.0 every error response is a
9
9
  JSON envelope of that same shape (`{ ok: false, error }`, status unchanged) —
10
10
  no plain-text errors remain.
11
11
 
12
+ Node-create responses include `sizeAdjustment` when a frame dimension was
13
+ defaulted or raised to its node type's readability floor. It contains the
14
+ `requested` dimensions (`null` when omitted), the final `applied` dimensions,
15
+ and a `reason` (`defaulted`, `clamped-to-minimum`, or
16
+ `defaulted-and-clamped-to-minimum`; groups also use `fit-to-children`). `strictSize: true`
17
+ still bypasses the readability clamp, but omitted dimensions are reported as
18
+ defaulted. MCP create actions return the same size report.
19
+ Updates preserve explicit dimensions without applying creation floors; their
20
+ returned node `size` is the applied size. Omitted update dimensions retain the
21
+ existing size and are not reported as defaulted adjustments.
22
+
23
+ Both group-create routes report defaults or final child-derived dimensions, including
24
+ when children override supplied width/height because x/y were omitted. Explicit manual
25
+ frames retain their existing semantics. Group child replacement reports `fit-to-children`
26
+ when it changes the frame; `group.add` reports the previous frame as `requested` and
27
+ the final frame as `applied`. Reports survive compact/full MCP and batch responses.
28
+ External MCP app creation and web-artifact builds that open a node report their default
29
+ frames too; reusing an existing app node or building without opening does not report a
30
+ creation default. These reports describe the synchronous operation, not later browser
31
+ content auto-height or indirect parent-group reflow after editing a child.
32
+
33
+ ## Board tours
34
+
35
+ - `GET /api/canvas/tour` → `{ tour: { stops: [...] }, derived: boolean }`.
36
+ - `POST /api/canvas/tour` with `{ "tour": { "stops": [...] } }` persists the tour;
37
+ `{ "tour": null }` resets to derived group reading order. Returns `{ "ok": true }`.
38
+ - `GET /api/canvas/state` includes optional `tour`. Tour writes broadcast a layout
39
+ update and are included in snapshots/undo. They are board-wide writes under a scope fence.
40
+
41
+ The [tour model and recording walkthrough](cli.md#tour-and-presentation) describes
42
+ targets, duration, easing, padding, and pullback. Open `/workbench?present=1` to hide
43
+ chrome; camera movement is local to that viewer. Recording writes local files via
44
+ the CLI, not an HTTP job endpoint.
45
+
12
46
  ## Canvas state
13
47
 
14
48
  ```bash
@@ -35,6 +69,10 @@ curl -X POST http://localhost:4313/api/canvas/json-render \
35
69
  -H "Content-Type: application/json" \
36
70
  -d '{"title":"Panel","nodeHeight":280,"spec":{"root":"card","elements":{"card":{"type":"Card","props":{"title":"Live"},"children":[]}}}}'
37
71
 
72
+ # For graph nodes, `height` is the chart's plot height, not the node frame.
73
+ # Set the frame with `nodeHeight` (`heightPx` in the SDK). A graph request that
74
+ # supplies only `height` gets the default frame height and a response warning.
75
+
38
76
  # Validate an HTML primitive without creating a node
39
77
  curl -X POST http://localhost:4313/api/canvas/schema/validate \
40
78
  -H "Content-Type: application/json" \
@@ -70,6 +108,11 @@ A node creation request must resolve a `type` — pass it in the body (`{ "type"
70
108
  ... }`) or as a `?type=` query param. An empty / type-less body returns `400`
71
109
  rather than silently creating a markdown node.
72
110
 
111
+ Mermaid nodes accept `data: { "fit": "contain" | "none" }` on both
112
+ `POST /api/canvas/node` and `PATCH /api/canvas/node/:id`. The default, `contain`,
113
+ shrinks overflowing diagrams without changing aspect ratio; `none` keeps 100%
114
+ with scrolling. [Mermaid sizing and readability](node-types.md#mermaid-nodes).
115
+
73
116
  ### File node content and bytes
74
117
 
75
118
  A `file` node reads its file server-side. Text is stored on the node as
@@ -145,6 +188,31 @@ curl -X DELETE http://localhost:4313/api/canvas/annotation/ann-123
145
188
  Agent-readable context reports annotation IDs, targets, and bounds. Use WebView
146
189
  inspection or screenshots when the drawn shape matters.
147
190
 
191
+ ## Viewport
192
+
193
+ ```bash
194
+ # Set the camera transform (screen = world * scale + {x,y})
195
+ curl -X POST http://localhost:4313/api/canvas/viewport \
196
+ -H "Content-Type: application/json" \
197
+ -d '{"x":64,"y":96,"scale":0.8}'
198
+
199
+ # Fit selected nodes (omit nodeIds to fit the whole canvas)
200
+ curl -X POST http://localhost:4313/api/canvas/fit \
201
+ -H "Content-Type: application/json" \
202
+ -d '{"nodeIds":["node-1"],"width":1200,"height":700}'
203
+
204
+ # Focus without moving the camera
205
+ curl -X POST http://localhost:4313/api/canvas/focus \
206
+ -H "Content-Type: application/json" \
207
+ -d '{"id":"node-1","noPan":true}'
208
+ ```
209
+
210
+ Viewport `x`/`y`, fit `width`/`height`, and fit padding are interpreted against
211
+ the **canvas area**, excluding the tool rail, top bar, and other surrounding
212
+ page chrome—not the full browser window. Camera, fit, and focus-only requests
213
+ do not create or refresh agent writer presence. Use the explicit presence API
214
+ when an agent should publish a cursor or `focusNodeId`.
215
+
148
216
  ## Pins
149
217
 
150
218
  ```bash
@@ -341,13 +409,20 @@ When that session ends (`attached: false`, `session-end`, or the idle expiry)
341
409
  the stream carries one `agent-session-ended` frame:
342
410
 
343
411
  ```json
344
- { "label": "Copilot", "endedAt": "…", "counts": { "items": 4, "done": 3, "vetoed": 1 },
412
+ { "label": "Copilot", "parentAgentId": null, "endedAt": "…", "endedBy": "agent",
413
+ "unchanged": false, "counts": { "items": 4, "done": 3, "cancelled": 1, "rejected": 0, "held": 0 },
345
414
  "snapshot": { "id": "…", "name": "Before session · Copilot · 14:00" } }
346
415
  ```
347
416
 
348
- `items`/`done` count the work items on the board; `vetoed` counts cancelled
349
- items plus rejected or held gates. `snapshot` is null when the board was
350
- empty at attach. The browser renders this as the session receipt; its *View
417
+ `items`/`done` count the work items on the board; cancelled items, rejected
418
+ gates, and held gates have separate counts. `snapshot` is null when the board was
419
+ empty at attach or unchanged at end (the redundant snapshot is removed).
420
+ The browser shows a receipt only for a changed top-level session: `unchanged: true`
421
+ and worker endings (`parentAgentId` set) never open or replace a popup. An empty
422
+ board that stays empty is unchanged too. This only filters the popup: session-end
423
+ events, timeline records, and changed-session snapshots remain available.
424
+ There is at most one receipt, replaced by the next qualifying ending and kept until
425
+ dismissed so restore/diff actions do not disappear on a timer. Its *View
351
426
  diff* is `GET /api/canvas/snapshots/<id>/diff` against that snapshot, and
352
427
  restoring the snapshot undoes the session. Adapters should end their session
353
428
  explicitly so the human gets the receipt promptly rather than after the idle
package/docs/mcp.md CHANGED
@@ -13,6 +13,15 @@ nodes in the browser, agents are notified immediately.
13
13
  > snapshot standalones — each step per [`api-stability.md`](api-stability.md)'s
14
14
  > deprecate-one-minor-before-removal rule. **Prefer the composites.**
15
15
 
16
+ ## Board tours
17
+
18
+ `canvas_view { action: "get-tour" }` returns `{ tour, derived }`, using group
19
+ reading order when there is no saved tour. `canvas_view { action: "set-tour",
20
+ tour: { stops: [...] } }` persists an ordered tour; `tour: null` resets it.
21
+ See the [tour model and CLI walkthrough](cli.md#tour-and-presentation) for stop
22
+ fields. Recording is a local CLI operation; no remote recording/file-writing
23
+ tool is added.
24
+
16
25
  ## Connect
17
26
 
18
27
  Add to your agent's MCP config:
@@ -42,7 +51,7 @@ its `action` to the same operation the legacy tool used, so results are identica
42
51
  | `canvas_edge` | `add` · `update` · `remove` | `canvas_add_edge`, `canvas_update_edge`, `canvas_remove_edge` |
43
52
  | `canvas_group` | `create` · `add` · `ungroup` | `canvas_create_group`, `canvas_group_nodes`, `canvas_ungroup` |
44
53
  | `canvas_history` | `undo` · `redo` | `canvas_undo`, `canvas_redo` |
45
- | `canvas_view` | `arrange` · `focus` · `fit` · `clear` · `remove-annotation` | `canvas_arrange`, `canvas_focus_node`, `canvas_fit_view`, `canvas_clear`, `canvas_remove_annotation` |
54
+ | `canvas_view` | `arrange` · `focus` · `fit` · `clear` · `remove-annotation` · `get-tour` · `set-tour` | `canvas_arrange`, `canvas_focus_node`, `canvas_fit_view`, `canvas_clear`, `canvas_remove_annotation` |
46
55
  | `canvas_query` | `search` · `layout` · `validate` | `canvas_search`, `canvas_get_layout`, `canvas_validate` |
47
56
  | `canvas_webview` | `status` · `start` · `stop` · `resize` · `evaluate` | `canvas_webview_status`, `canvas_webview_start`, `canvas_webview_stop`, `canvas_resize`, `canvas_evaluate` |
48
57
  | `canvas_app` | `open-mcp-app` · `diagram` · `build-artifact` | `canvas_open_mcp_app`, `canvas_add_diagram`, `canvas_build_web_artifact` |
@@ -54,6 +63,11 @@ its `action` to the same operation the legacy tool used, so results are identica
54
63
  | `canvas_intent` | `signal` · `update` · `clear` | _(new — Ghost Cursor of Intent; no legacy standalone tool)_ |
55
64
  | `canvas_snapshot` | `save` · `list` · `restore` · `delete` · `gc` · `diff` | `canvas_snapshot` (legacy save tool), `canvas_list_snapshots`, `canvas_restore`, `canvas_delete_snapshot`, `canvas_gc_snapshots`, `canvas_diff` — removed in v0.4.0 after one deprecated minor |
56
65
 
66
+ For Mermaid nodes, `canvas_node` actions `add` and `update` accept
67
+ `data: { fit: "contain" | "none" }`. Contain is the default, shrinking overflow
68
+ uniformly; none keeps 100% with scrolling. Use Open as site for 100% without
69
+ changing the node setting. [Sizing and readability](node-types.md#mermaid-nodes).
70
+
57
71
  ### `canvas_intent` — Ghost Cursor of Intent
58
72
 
59
73
  Announce the spatial move you are **about** to make so the canvas paints a faint
@@ -92,6 +106,16 @@ fixing `node.update`'s `formatResult` to surface a FAILED refresh as `isError` +
92
106
  fold completed in v0.4.0: `canvas_snapshot { action: "save" | "list" | "restore" |
93
107
  "delete" | "gc" | "diff" }` replaced the 6 deprecated snapshot standalones.
94
108
 
109
+ ### Viewport coordinates
110
+
111
+ `canvas_view { action: "focus" | "fit" }` controls the camera without counting
112
+ as a writer operation or refreshing agent presence. Viewport translation uses
113
+ screen-space canvas-area coordinates (`screen = world * scale + viewport`). Fit
114
+ dimensions and padding likewise refer to the canvas area after excluding the
115
+ tool rail, top bar, and surrounding page chrome—not the full browser window.
116
+ Use `canvas_ax_state { action: "set-presence", cursor, focusNodeId }` when the
117
+ agent intends to publish its own cursor or attention target.
118
+
95
119
  ## Standalone tools
96
120
 
97
121
  6 tools that intentionally stay outside the composites — folding them would
@@ -26,6 +26,17 @@ see [MCP tools](mcp.md), [HTTP API](http-api.md), and [SDK](sdk.md).
26
26
  | `web-artifact` | Bundled React/Tailwind artifact (full single-file app) |
27
27
  | `group` | Spatial container/frame around other nodes |
28
28
 
29
+ Creation applies per-type frame defaults and readability minimums. When a
30
+ dimension is defaulted or clamped, HTTP and MCP create responses include a
31
+ `sizeAdjustment` with requested and applied dimensions plus the reason.
32
+ `strictSize: true` bypasses minimum-size clamping; it does not change defaults
33
+ for omitted dimensions.
34
+
35
+ For `graph` nodes, `height` controls the chart plot inside the viewer.
36
+ `nodeHeight` controls the canvas node frame (`heightPx` is the SDK alias). If
37
+ only `height` is supplied, the frame keeps its default height and the create
38
+ response warns about the distinction.
39
+
29
40
  Thread node types `prompt` and `response` exist internally for agent
30
41
  conversation rendering and are not created through public APIs.
31
42
 
@@ -64,6 +75,34 @@ different type by extension:
64
75
  - **Text files over 2 MB** render truncated with a banner saying so, so a huge
65
76
  log can never exhaust memory.
66
77
 
78
+ ## Mermaid nodes
79
+
80
+ Mermaid diagrams default to `data.fit: "contain"`: when the diagram overflows
81
+ the available body, it shrinks uniformly until the entire diagram fits. Small
82
+ diagrams stay at 100%; aspect ratio, labels, and edges are preserved, in both
83
+ dark and light themes. Resizing a card or expanding it recalculates the fit.
84
+ `strictSize` keeps the requested node geometry while the diagram fits inside it;
85
+ ordinary cards retain their grow-only automatic sizing.
86
+
87
+ Fitting a large diagram makes its text smaller. Use **Expand** for more space,
88
+ or **Open as site** for a 100% natural-size diagram with scrolling. Set
89
+ `data.fit: "none"` to use that natural-size, scrollable view inside the card too.
90
+ This setting is independent of the canvas camera's **Fit view** action.
91
+
92
+ ```ts
93
+ canvas_node({ action: 'add', type: 'mermaid', content: 'flowchart TD; A-->B-->C-->D',
94
+ width: 920, height: 870, strictSize: true, data: { fit: 'contain' } })
95
+ canvas_node({ action: 'update', id: 'node-id', data: { fit: 'none' } })
96
+ ```
97
+
98
+ HTTP uses the same `data` object in `POST /api/canvas/node` and
99
+ `PATCH /api/canvas/node/:id`. CLI creation and updates expose `--fit`:
100
+
101
+ ```bash
102
+ pmx-canvas node add --type mermaid --content 'flowchart TD; A-->B-->C-->D' --fit contain --width 920 --height 870 --strict-size
103
+ pmx-canvas node update node-id --fit none
104
+ ```
105
+
67
106
  ## Image nodes
68
107
 
69
108
  Image nodes display local paths, remote URLs, and data URIs. File-backed and
@@ -163,6 +202,40 @@ Specs preserve top-level `state` and element-level `repeat`, `watch`, and
163
202
  `$state` only reads a value. Nested repeats can use `statePath: { "$item": "tasks" }`
164
203
  to iterate the current item's tasks. Watchers run after changes, not on mount.
165
204
 
205
+ **Seed form values through bindings, not literal `value` props.** In shadcn
206
+ 0.21, `Input`, `Textarea`, `Select`, `Radio`, `Slider`, `ToggleGroup`, and
207
+ `Tabs` ignore an unbound `value`; `ButtonGroup` ignores unbound `selected`.
208
+ This includes read-only `$state` expressions. PMX returns a `warnings` array
209
+ from `POST /api/canvas/json-render`, `POST /api/canvas/schema/validate`, and
210
+ `canvas_render` actions `add-json-render` and `validate`. Warnings name the
211
+ element/property; they do not reject or rewrite the spec. PMX does not invent
212
+ state paths because repeated controls and existing actions may need distinct
213
+ or deliberately shared state.
214
+
215
+ ```json
216
+ {
217
+ "root": "postcode",
218
+ "state": { "form": { "postcode": "nw1 6xe" } },
219
+ "elements": {
220
+ "postcode": {
221
+ "type": "Input",
222
+ "props": {
223
+ "label": "Postcode",
224
+ "name": "postcode",
225
+ "value": { "$bindState": "/form/postcode" }
226
+ }
227
+ }
228
+ }
229
+ }
230
+ ```
231
+
232
+ Inside repeats, use `$bindItem` with seeded item data. `Checkbox`/`Switch`
233
+ honor literal `checked`, `Toggle` honors literal `pressed`, and `Tabs` honors
234
+ `defaultValue` for an uncontrolled initial tab. `Pagination` reads `page`
235
+ directly; `DropdownMenu.value` is an output binding for selection, not a
236
+ displayed selection. These do not require the same ignored-value warning.
237
+ Use two-way bindings whenever edits or form checks must participate in state.
238
+
166
239
  Cards keep their body in `children` and accept
167
240
  `slots: { header: ['headingId'], footer: ['actionsId'] }`. Slot IDs reference
168
241
  entries in `elements`; a custom header replaces the title and description props.
@@ -277,6 +350,13 @@ component gallery, interaction prototype, flowchart, SVG illustration set,
277
350
  presentation, explainer, status report, incident report, triage board, config
278
351
  editor, or prompt tuner.
279
352
 
353
+ Omitted fields may use the template's examples. Explicit empty arrays, empty
354
+ strings, or `null` mean “show nothing”: the field's section, heading, and
355
+ dependent controls are omitted rather than replaced with sample content.
356
+ Other populated or omitted fields keep their normal behavior. Empty `slides`
357
+ or flow `steps` also omit navigation/run controls; an AX board's live task
358
+ controls remain available when only its optional note is empty.
359
+
280
360
  Two kinds are **live AX control surfaces** rather than static documents, and are
281
361
  the only primitives created with AX enabled:
282
362
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmx-canvas",
3
- "version": "0.6.4",
3
+ "version": "0.6.5",
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",
@@ -16,9 +16,17 @@ PMX Canvas is a server-authoritative spatial workbench controlled through MCP, H
16
16
  Humans curate agent context by pinning nodes; agents read that curation through
17
17
  `canvas://pinned-context`. State survives browser refresh.
18
18
 
19
+ For presentations, `canvas_view { action: "set-tour", tour: { stops: [...] } }`
20
+ persists ordered camera stops; `get-tour` reads them (or derived group order).
21
+ Each stop targets `{ nodeId }` (including groups) or `{ viewport: { x, y, scale } }`,
22
+ with optional `duration` seconds, `easing`, screen-pixel `padding`, and `pullback`.
23
+ Open `/workbench?present=1` for chrome-free viewing. Optionally capture that same
24
+ tour with `pmx-canvas record --mode deterministic --present --output tour.mp4`.
25
+ There is no separate recording model. See `docs/cli.md` for the full tour model.
26
+
19
27
  ## Runtime prerequisites
20
28
 
21
- PMX Canvas 0.6.4 requires **Bun >=1.4.2**, including when installed through npm.
29
+ PMX Canvas 0.6.5 requires **Bun >=1.4.2**, including when installed through npm.
22
30
  Check `bun --version` and ensure Bun is on the MCP host's PATH, not just your shell's;
23
31
  use an absolute executable path if needed. Install/configure only when requested.
24
32
  For pinned installation, MCP configuration, managed services, and disposable verification,
@@ -178,7 +186,7 @@ always authoritative, independent of the installed release version.
178
186
  | `canvas_edge` | `add`, `update`, `remove` |
179
187
  | `canvas_group` | `create`, `add`, `ungroup` |
180
188
  | `canvas_history` | `undo`, `redo` |
181
- | `canvas_view` | `arrange`, `focus`, `fit`, `clear`, `remove-annotation` |
189
+ | `canvas_view` | `arrange`, `focus`, `fit`, `clear`, `remove-annotation`, `get-tour`, `set-tour` |
182
190
  | `canvas_query` | `search`, `layout`, `validate` |
183
191
  | `canvas_webview` | `status`, `start`, `stop`, `resize`, `evaluate` |
184
192
  | `canvas_app` | `open-mcp-app`, `diagram`, `build-artifact` |
@@ -441,7 +441,7 @@ single-purpose tools behind an `action` (and, for `canvas_ax_gate`, a `kind`) di
441
441
  | `canvas_group` | `create` · `add` · `ungroup` | Manage spatial group containers |
442
442
  | `canvas_history` | `undo` · `redo` | Time travel through the mutation ring buffer |
443
443
  | `canvas_snapshot` | `save` · `list` · `restore` · `delete` · `gc` · `diff` | Named snapshots: save/list/restore/delete, garbage-collect old ones, diff current canvas vs a snapshot (`diff` takes `snapshot`, not `id`) |
444
- | `canvas_view` | `arrange` · `focus` · `fit` · `clear` · `remove-annotation` | Auto-arrange, pan-to-node, fit viewport, clear the board, delete a human-drawn annotation by id |
444
+ | `canvas_view` | `arrange` · `focus` · `fit` · `clear` · `remove-annotation` · `get-tour` · `set-tour` | Auto-arrange, pan-to-node, fit viewport, clear the board, delete annotations, read/persist ordered presentation stops (`tour: null` derives group order) |
445
445
  | `canvas_query` | `search` · `layout` · `validate` | Find nodes by keyword, read full layout, or **`validate`** the board for node collisions / group-containment / dangling edges |
446
446
  | `canvas_app` | `open-mcp-app` · `diagram` · `build-artifact` | Hosted MCP apps, the Excalidraw diagram preset, and bundled web artifacts (folds `canvas_open_mcp_app` / `canvas_add_diagram` / `canvas_build_web_artifact`) |
447
447
  | `canvas_webview` | `status` · `start` · `stop` · `resize` · `evaluate` | Headless Bun.WebView automation for the workbench (folds the `canvas_webview_*` / `canvas_resize` / `canvas_evaluate` tools) |
@@ -4,7 +4,7 @@ Use this reference when the `pmx-canvas` skill is installed but the `pmx-canvas`
4
4
 
5
5
  ## Prerequisites and version
6
6
 
7
- The examples pin **PMX Canvas 0.6.4**, which requires **Bun >=1.4.2** even when
7
+ The examples pin **PMX Canvas 0.6.5**, which requires **Bun >=1.4.2** even when
8
8
  installed through npm: the CLI runs through Bun, not Node. Check `bun --version`
9
9
  first. If missing or older, follow the [Bun installation instructions](https://bun.sh/docs/installation)
10
10
  with the user's authorization. Ensure Bun is on the MCP host's PATH as well as the
@@ -17,11 +17,11 @@ in `SKILL.md`. Do not pair newer checkout guidance with an older runtime unknowi
17
17
  ## Install from npm
18
18
 
19
19
  ```bash
20
- npm install -g pmx-canvas@0.6.4
20
+ npm install -g pmx-canvas@0.6.5
21
21
  pmx-canvas --version
22
22
  ```
23
23
 
24
- Without a global install, use `bunx pmx-canvas@0.6.4` in place of `pmx-canvas` in
24
+ Without a global install, use `bunx pmx-canvas@0.6.5` in place of `pmx-canvas` in
25
25
  the commands below. Do not use unpinned `bunx pmx-canvas` for a version-specific check.
26
26
 
27
27
  ## Choose the workspace and target
@@ -91,7 +91,7 @@ discards empty env values, remove `PMX_CANVAS_URL` from its launch environment i
91
91
  "mcpServers": {
92
92
  "canvas": {
93
93
  "command": "bunx",
94
- "args": ["pmx-canvas@0.6.4", "--mcp"],
94
+ "args": ["pmx-canvas@0.6.5", "--mcp"],
95
95
  "env": {
96
96
  "PMX_CANVAS_WORKSPACE_ROOT": "/absolute/path/to/project",
97
97
  "PMX_CANVAS_PORT": "14313",
package/src/cli/agent.ts CHANGED
@@ -30,6 +30,7 @@ import './commands/history.js';
30
30
  import './commands/ax.js';
31
31
  import './commands/pump.js';
32
32
  import './commands/webview.js';
33
+ import './commands/record.js';
33
34
  import './commands/apps.js';
34
35
  import './commands/copilot.js';
35
36
  import './commands/skills.js';
@@ -46,6 +47,10 @@ Usage:
46
47
  pmx-canvas <command> [options]
47
48
  pmx-canvas [server-options]
48
49
 
50
+ Presentation and capture:
51
+ pmx-canvas tour get|set Read or persist ordered camera stops
52
+ pmx-canvas record --help Optionally capture the same board tour or live presentation
53
+
49
54
  Server:
50
55
  pmx-canvas Start server + open browser
51
56
  pmx-canvas --no-open --demo Start server headless with sample data
@@ -27,6 +27,12 @@ import {
27
27
 
28
28
  const TRACE_NODE_FIELDS = ['toolName', 'category', 'status', 'duration', 'resultSummary', 'error'] as const;
29
29
 
30
+ function applyMermaidFitFlag(body: Record<string, unknown>, flags: Record<string, string | boolean>): void {
31
+ if (flags.fit === undefined) return;
32
+ if (flags.fit !== 'contain' && flags.fit !== 'none') die('Use --fit contain or --fit none (Mermaid nodes).');
33
+ body.data = { ...(isRecord(body.data) ? body.data : {}), fit: flags.fit };
34
+ }
35
+
30
36
  interface CanvasSchemaField {
31
37
  name: string;
32
38
  type: string;
@@ -483,6 +489,7 @@ cmd(
483
489
  'pmx-canvas node add --type html --title "Showcase" --content ./report.html (a .html path is read from disk; otherwise --content is raw HTML)',
484
490
  'pmx-canvas node add --type html --primitive choice-grid --data-file ./options.json --title "Options"',
485
491
  'pmx-canvas node add --type markdown --title "Note" --x 100 --y 200',
492
+ 'pmx-canvas node add --type mermaid --content "graph TD; A-->B" --fit contain',
486
493
  'pmx-canvas node add --type json-render --title "Ops Dashboard" --spec-file ./dashboard.json',
487
494
  'pmx-canvas node add --type graph --graph-type bar --data-file ./metrics.json --x-key label --y-key value',
488
495
  'pmx-canvas node add --type web-artifact --title "Dashboard" --app-file ./App.tsx',
@@ -530,6 +537,7 @@ cmd(
530
537
  }
531
538
 
532
539
  const body: Record<string, unknown> = { type };
540
+ applyMermaidFitFlag(body, flags);
533
541
  if (flags.title) body.title = flags.title;
534
542
  const webpageUrl = getStringFlag(flags, 'url');
535
543
  const imagePath = getStringFlag(flags, 'path');
@@ -821,6 +829,7 @@ cmd(
821
829
  [
822
830
  'pmx-canvas node update <node-id> --title "New Title"',
823
831
  'pmx-canvas node update <node-id> --content "Updated content"',
832
+ 'pmx-canvas node update <mermaid-id> --fit none',
824
833
  'pmx-canvas node update <node-id> --title "Moved" --x 500 --y 300',
825
834
  'pmx-canvas node update <node-id> --width 840 --height 620',
826
835
  'pmx-canvas node update <node-id> --spec-file ./dashboard.json',
@@ -838,6 +847,7 @@ cmd(
838
847
 
839
848
  const body: Record<string, unknown> = {};
840
849
  await applyStructuredNodeUpdateFlags(body, flags);
850
+ applyMermaidFitFlag(body, flags);
841
851
  if (flags.title && flags.title !== true) body.title = flags.title;
842
852
  if (flags.content && flags.content !== true) body.content = flags.content;
843
853
  if (flags.stdin) body.content = await readStdin();
@@ -0,0 +1,218 @@
1
+ import { mkdirSync, existsSync } from 'node:fs';
2
+ import { resolve, join } from 'node:path';
3
+ import { z } from 'zod';
4
+ import { isCanvasTheme } from '../../shared/themes.js';
5
+ import { derivedTour, tourSchema, tourFrames, type Camera, type TourNode } from '../../shared/tour.js';
6
+ import {
7
+ startCanvasAutomationWebView,
8
+ evaluateCanvasAutomationWebView,
9
+ screenshotCanvasAutomationWebView,
10
+ resizeCanvasAutomationWebView,
11
+ stopCanvasAutomationWebView,
12
+ } from '../../server/server.js';
13
+ import { cmd, getBaseUrl, invokeOperation, output, parseFlags, showCommandHelp } from '../shared.js';
14
+
15
+ export function parseRecordOptions(flags: Record<string, string | boolean>) {
16
+ const number = (key: string, fallback: number) => {
17
+ if (flags[key] === true || flags[key] === false) throw new Error(`--${key} needs a number`);
18
+ return flags[key] === undefined ? fallback : Number(flags[key]);
19
+ };
20
+ for (const key of ['tour-file', 'chrome-path']) {
21
+ if (flags[key] !== undefined && typeof flags[key] !== 'string') throw new Error(`--${key} needs a path`);
22
+ }
23
+ const resolution = String(flags.resolution ?? '1280x720').match(/^(\d+)x(\d+)$/);
24
+ if (!resolution) throw new Error('Use --resolution WIDTHxHEIGHT');
25
+ const options = z
26
+ .object({
27
+ width: z.number().int().min(64).max(7680),
28
+ height: z.number().int().min(64).max(4320),
29
+ fps: z.number().int().min(1).max(120),
30
+ duration: z.number().positive().max(86400).optional(),
31
+ mode: z.enum(['realtime', 'deterministic']),
32
+ output: z.string().min(1),
33
+ })
34
+ .parse({
35
+ width: Number(resolution[1]),
36
+ height: Number(resolution[2]),
37
+ fps: number('fps', 30),
38
+ duration: flags.duration === undefined ? undefined : number('duration', 10),
39
+ mode: flags.mode ?? 'realtime',
40
+ output: typeof flags.output === 'string' ? flags.output : '',
41
+ });
42
+ if (flags.theme !== undefined && !isCanvasTheme(flags.theme)) throw new Error('Unknown --theme');
43
+ if (options.mode === 'realtime' && !options.duration && flags['stop-on-signal'] !== true) {
44
+ throw new Error('Realtime recording needs --duration SECONDS or --stop-on-signal');
45
+ }
46
+ if (options.mode === 'deterministic' && (flags.duration !== undefined || flags['stop-on-signal'])) {
47
+ throw new Error('Deterministic duration comes from tour stops; omit --duration and --stop-on-signal');
48
+ }
49
+ if (flags['tour-file'] && options.mode !== 'deterministic')
50
+ throw new Error('--tour-file requires --mode deterministic');
51
+ return {
52
+ ...options,
53
+ theme: typeof flags.theme === 'string' ? flags.theme : undefined,
54
+ present: flags.present === true,
55
+ tourFile: typeof flags['tour-file'] === 'string' ? flags['tour-file'] : undefined,
56
+ chromePath: typeof flags['chrome-path'] === 'string' ? flags['chrome-path'] : undefined,
57
+ };
58
+ }
59
+
60
+ cmd('tour get', 'Read the persisted tour or derived group order', ['pmx-canvas tour get'], async () => {
61
+ output(await invokeOperation('tour.get', {}));
62
+ });
63
+ cmd(
64
+ 'tour set',
65
+ 'Persist camera stops from JSON (null resets to group order)',
66
+ ['pmx-canvas tour set --file tour.json'],
67
+ async (args) => {
68
+ const { flags } = parseFlags(args);
69
+ if (flags.help) return showCommandHelp('tour set');
70
+ if (typeof flags.file !== 'string') throw new Error('Missing --file');
71
+ const tour = tourSchema.nullable().parse(await Bun.file(flags.file).json());
72
+ output(await invokeOperation('tour.set', { tour }));
73
+ },
74
+ );
75
+
76
+ cmd(
77
+ 'record',
78
+ 'Optionally capture the board tour or live presentation using Bun.WebView Chrome',
79
+ [
80
+ 'pmx-canvas record --duration 10 --present --output demo.mp4',
81
+ 'pmx-canvas record --mode deterministic --tour-file tour.json --present --fps 30 --output frames',
82
+ 'pmx-canvas record --stop-on-signal --resolution 1920x1080 --output live.mp4',
83
+ ],
84
+ async (args) => {
85
+ const { flags } = parseFlags(args, { boolFlags: ['present', 'stop-on-signal'] });
86
+ if (flags.help || flags.h) return showCommandHelp('record');
87
+ const options = parseRecordOptions(flags);
88
+ const outputPath = resolve(options.output);
89
+ const video = /\.mp4$/i.test(outputPath);
90
+ const framesPath = video ? `${outputPath}.frames` : outputPath;
91
+ if (existsSync(outputPath) || existsSync(framesPath)) throw new Error('Output already exists; choose a new path');
92
+ const response = await fetch(`${getBaseUrl()}/api/canvas/state`);
93
+ if (!response.ok) throw new Error(`Cannot read board: HTTP ${response.status}`);
94
+ const board = (await response.json()) as { nodes: TourNode[]; viewport: Camera; tour?: unknown };
95
+ const tour =
96
+ options.mode === 'deterministic'
97
+ ? tourSchema.parse(
98
+ options.tourFile ? await Bun.file(options.tourFile).json() : (board.tour ?? derivedTour(board.nodes)),
99
+ )
100
+ : undefined;
101
+ if (tour && !tour.stops.length) throw new Error('No tour stops: supply --tour-file or create groups/a tour');
102
+ const url = new URL('/workbench', getBaseUrl());
103
+ if (options.present) url.searchParams.set('present', '1');
104
+ if (tour) url.searchParams.set('capture', '1');
105
+ if (options.theme) url.searchParams.set('theme', options.theme);
106
+ let stopping = false;
107
+ const stop = () => {
108
+ stopping = true;
109
+ };
110
+ process.on('SIGINT', stop);
111
+ process.on('SIGTERM', stop);
112
+ let count = 0;
113
+ let duplicated = 0;
114
+ try {
115
+ await startCanvasAutomationWebView(url.href, {
116
+ backend: 'chrome',
117
+ width: options.width,
118
+ height: options.height,
119
+ chromePath: options.chromePath,
120
+ });
121
+ const deadline = Date.now() + 30_000;
122
+ while (!(await evaluateCanvasAutomationWebView('!!window.pmxCapture?.ready()'))) {
123
+ if (Date.now() > deadline) throw new Error('Workbench did not become capture-ready in 30 seconds');
124
+ await Bun.sleep(50);
125
+ }
126
+ // Chrome launch dimensions describe the outer window. WebView.resize
127
+ // applies viewport emulation, giving screenshots the requested pixels.
128
+ await resizeCanvasAutomationWebView(options.width, options.height);
129
+ await evaluateCanvasAutomationWebView(
130
+ 'new Promise(r => requestAnimationFrame(() => requestAnimationFrame(() => r(true))))',
131
+ );
132
+ await evaluateCanvasAutomationWebView('document.fonts.ready.then(() => true)');
133
+ const area = (await evaluateCanvasAutomationWebView('window.pmxCapture.area()')) as {
134
+ width: number;
135
+ height: number;
136
+ };
137
+ const initial = (await evaluateCanvasAutomationWebView('window.pmxCapture.camera()')) as Camera;
138
+ mkdirSync(framesPath, { recursive: true });
139
+ const write = async (bytes: Uint8Array) => {
140
+ await Bun.write(join(framesPath, `frame-${String(count++).padStart(6, '0')}.png`), bytes);
141
+ };
142
+ const started = performance.now();
143
+ if (tour) {
144
+ for (const camera of tourFrames(tour, initial, board.nodes, area.width, area.height, options.fps)) {
145
+ if (stopping) break;
146
+ const applied = (await evaluateCanvasAutomationWebView(
147
+ `window.pmxCapture.frame(${JSON.stringify(camera)})`,
148
+ )) as Camera;
149
+ if (applied.x !== camera.x || applied.y !== camera.y || applied.scale !== camera.scale) {
150
+ throw new Error('Capture camera was changed before paint');
151
+ }
152
+ await write(await screenshotCanvasAutomationWebView({ format: 'png' }));
153
+ }
154
+ } else {
155
+ const limit = options.duration ? Math.ceil(options.duration * options.fps) : Infinity;
156
+ while (!stopping && count < limit) {
157
+ await Bun.sleep(Math.max(0, started + (count / options.fps) * 1000 - performance.now()));
158
+ const bytes = await screenshotCanvasAutomationWebView({ format: 'png' });
159
+ await write(bytes);
160
+ // Preserve wall-clock speed if capture is slower than requested fps.
161
+ const elapsedFrames = Math.min(limit, Math.floor(((performance.now() - started) / 1000) * options.fps));
162
+ while (count < elapsedFrames) {
163
+ await write(bytes);
164
+ duplicated++;
165
+ }
166
+ }
167
+ }
168
+ await Bun.write(
169
+ join(framesPath, 'recording.json'),
170
+ JSON.stringify(
171
+ {
172
+ mode: options.mode,
173
+ fps: options.fps,
174
+ width: options.width,
175
+ height: options.height,
176
+ frames: count,
177
+ duplicated,
178
+ seconds: count / options.fps,
179
+ },
180
+ null,
181
+ 2,
182
+ ),
183
+ );
184
+ } finally {
185
+ process.off('SIGINT', stop);
186
+ process.off('SIGTERM', stop);
187
+ await stopCanvasAutomationWebView();
188
+ }
189
+ let encoded = false;
190
+ if (video && count > 0) {
191
+ const ffmpeg = Bun.which('ffmpeg');
192
+ if (!ffmpeg) console.error(`ffmpeg not found; PNG sequence retained at ${framesPath}`);
193
+ else {
194
+ const process = Bun.spawn(
195
+ [
196
+ ffmpeg,
197
+ '-n',
198
+ '-framerate',
199
+ String(options.fps),
200
+ '-i',
201
+ join(framesPath, 'frame-%06d.png'),
202
+ '-c:v',
203
+ 'libx264',
204
+ '-pix_fmt',
205
+ 'yuv420p',
206
+ '-vf',
207
+ 'pad=ceil(iw/2)*2:ceil(ih/2)*2',
208
+ outputPath,
209
+ ],
210
+ { stdout: 'ignore', stderr: 'inherit' },
211
+ );
212
+ encoded = (await process.exited) === 0;
213
+ if (!encoded) throw new Error(`ffmpeg failed; PNG sequence retained at ${framesPath}`);
214
+ }
215
+ }
216
+ output({ ok: true, output: encoded ? outputPath : framesPath, frames: count, fps: options.fps, duplicated });
217
+ },
218
+ );
package/src/cli/index.ts CHANGED
@@ -32,6 +32,8 @@ if (args.includes('--version') || args.includes('-v')) {
32
32
  // ── Agent CLI subcommands ────────────────────────────────────
33
33
  // If first arg is a known subcommand (not a --flag), route to the agent CLI.
34
34
  const AGENT_COMMANDS = new Set([
35
+ 'tour',
36
+ 'record',
35
37
  'node',
36
38
  'edge',
37
39
  'json-render',