pmx-canvas 0.7.1 → 0.7.2

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 (55) hide show
  1. package/.github/extensions/pmx-canvas/extension.mjs +79 -61
  2. package/CHANGELOG.md +16 -1
  3. package/Readme.md +31 -6
  4. package/dist/canvas/global.css +21 -5
  5. package/dist/canvas/index.js +73 -73
  6. package/dist/types/client/canvas/ExternalWriters.d.ts +3 -3
  7. package/dist/types/client/icons.d.ts +15 -0
  8. package/dist/types/client/nodes/ExtAppFrame.d.ts +1 -1
  9. package/dist/types/client/nodes/FileNode.d.ts +0 -12
  10. package/dist/types/shared/delimited-text.d.ts +14 -0
  11. package/docs/cli.md +9 -2
  12. package/docs/http-api.md +12 -8
  13. package/docs/mcp.md +8 -1
  14. package/docs/sdk.md +2 -2
  15. package/package.json +1 -1
  16. package/skills/pmx-canvas/SKILL.md +19 -6
  17. package/skills/pmx-canvas/evals/evals.json +43 -2
  18. package/skills/pmx-canvas/references/full-reference.md +6 -1
  19. package/skills/pmx-canvas/references/github-copilot-app-adapter.md +17 -3
  20. package/skills/pmx-canvas/references/installing-pmx-canvas.md +4 -4
  21. package/src/cli/commands/backup.ts +15 -4
  22. package/src/cli/commands/boards.ts +16 -3
  23. package/src/client/canvas/AttentionHistory.tsx +2 -1
  24. package/src/client/canvas/BoardSwitcher.tsx +3 -3
  25. package/src/client/canvas/CanvasNode.tsx +17 -8
  26. package/src/client/canvas/CommandBar.tsx +2 -2
  27. package/src/client/canvas/ContextPinBar.tsx +3 -1
  28. package/src/client/canvas/ExpandedNodeOverlay.tsx +3 -3
  29. package/src/client/canvas/ExternalWriters.tsx +4 -3
  30. package/src/client/canvas/FeedbackDialog.tsx +2 -1
  31. package/src/client/canvas/IntentLayer.tsx +2 -2
  32. package/src/client/canvas/SelectionBar.tsx +2 -2
  33. package/src/client/canvas/SessionPanel.tsx +6 -4
  34. package/src/client/canvas/SessionReceipt.tsx +2 -1
  35. package/src/client/canvas/SnapshotPanel.tsx +3 -2
  36. package/src/client/canvas/ToolRail.tsx +2 -1
  37. package/src/client/canvas/TopBar.tsx +2 -2
  38. package/src/client/icons.tsx +118 -1
  39. package/src/client/nodes/BoardNode.tsx +2 -1
  40. package/src/client/nodes/ExtAppFrame.tsx +3 -1
  41. package/src/client/nodes/FileNode.tsx +4 -78
  42. package/src/client/nodes/ImageNode.tsx +2 -1
  43. package/src/client/nodes/MarkdownNode.tsx +2 -1
  44. package/src/client/nodes/PromptNode.tsx +3 -1
  45. package/src/client/theme/global.css +21 -5
  46. package/src/mcp/server.ts +2 -2
  47. package/src/server/agent-presence.ts +17 -11
  48. package/src/server/board-export.ts +97 -32
  49. package/src/server/index.ts +9 -7
  50. package/src/server/operations/composites.ts +3 -2
  51. package/src/server/operations/ops/ax-presence.ts +7 -3
  52. package/src/server/operations/ops/boards.ts +12 -5
  53. package/src/server/operations/registry.ts +4 -13
  54. package/src/server/server.ts +10 -3
  55. package/src/shared/delimited-text.ts +75 -0
@@ -1,5 +1,5 @@
1
1
  /** Top-bar indicator: avatar cluster, "3 writers", op count, pulsing dot. */
2
- export declare function ExternalWriterIndicator(): import("preact/jsx-runtime").JSX.Element | null;
3
- export declare function ActivityFeed(): import("preact/jsx-runtime").JSX.Element | null;
2
+ export declare function ExternalWriterIndicator(): import("preact/src").JSX.Element | null;
3
+ export declare function ActivityFeed(): import("preact/src").JSX.Element | null;
4
4
  /** Connected writers — pure visibility, no permissions. */
5
- export declare function WritersSheet(): import("preact/jsx-runtime").JSX.Element | null;
5
+ export declare function WritersSheet(): import("preact/src").JSX.Element | null;
@@ -3,6 +3,21 @@ interface IconProps {
3
3
  size?: number;
4
4
  class?: string;
5
5
  }
6
+ export declare function IconClose(p: IconProps): JSX.Element;
7
+ export declare function IconPlus(p: IconProps): JSX.Element;
8
+ export declare function IconPlusCircle(p: IconProps): JSX.Element;
9
+ export declare function IconChevronRight(p: IconProps): JSX.Element;
10
+ export declare function IconMore(p: IconProps): JSX.Element;
11
+ export declare function IconExternalLink(p: IconProps): JSX.Element;
12
+ export declare function IconExpand(p: IconProps): JSX.Element;
13
+ export declare function IconArrowLeft(p: IconProps): JSX.Element;
14
+ export declare function IconRefresh(p: IconProps): JSX.Element;
15
+ export declare function IconUndo(p: IconProps): JSX.Element;
16
+ export declare function IconSparkle(p: IconProps): JSX.Element;
17
+ /** Upload into a tray — attach a document. */
18
+ export declare function IconUpload(p: IconProps): JSX.Element;
19
+ /** Dropdown disclosure, sized like the other toolbar icons. */
20
+ export declare function IconChevronDown(p: IconProps): JSX.Element;
6
21
  /** Expand-arrows — fit all nodes */
7
22
  export declare function IconFitAll(p: IconProps): JSX.Element;
8
23
  /** Magnifier with + */
@@ -106,5 +106,5 @@ export declare function resolveExtAppInlineFrameHeight(appHeight: number, hostHe
106
106
  export declare function ExtAppFrame({ node, expanded }: {
107
107
  node: CanvasNodeState;
108
108
  expanded?: boolean;
109
- }): import("preact/jsx-runtime").JSX.Element;
109
+ }): import("preact/src").JSX.Element;
110
110
  export {};
@@ -1,16 +1,4 @@
1
1
  import type { CanvasNodeState } from '../types';
2
- export interface DelimitedTable {
3
- header: string[];
4
- /** Rectangular: every row padded (short) or truncated (long) to the header width. */
5
- rows: string[][];
6
- }
7
- /**
8
- * RFC4180-ish delimited-text parser. Quoted fields may contain the delimiter,
9
- * newlines, and `""` escapes; CRLF, LF and lone CR all end a record; a trailing
10
- * newline produces no phantom empty row. Ragged records are squared off against
11
- * the header width instead of throwing.
12
- */
13
- export declare function parseDelimitedText(text: string, delimiter: string): DelimitedTable;
14
2
  /** Human-readable byte size, e.g. `4.2 MB`. */
15
3
  export declare function formatBytes(bytes: number): string;
16
4
  export declare function FileNode({ node, expanded }: {
@@ -0,0 +1,14 @@
1
+ export interface DelimitedTable {
2
+ header: string[];
3
+ /** Rectangular: every row padded (short) or truncated (long) to the header width. */
4
+ rows: string[][];
5
+ }
6
+ /** Column delimiter for a delimited-data path, or null when the file is not one. */
7
+ export declare function delimiterForPath(path: string): string | null;
8
+ /**
9
+ * RFC4180-ish delimited-text parser. Quoted fields may contain the delimiter,
10
+ * newlines, and `""` escapes; CRLF, LF and lone CR all end a record; a trailing
11
+ * newline produces no phantom empty row. Ragged records are squared off against
12
+ * the header width instead of throwing.
13
+ */
14
+ export declare function parseDelimitedText(text: string, delimiter: string): DelimitedTable;
package/docs/cli.md CHANGED
@@ -280,6 +280,8 @@ node or fit the new ids — auto-placement is board-relative, not camera-relativ
280
280
  ```bash
281
281
  pmx-canvas board list # most recently opened first; activeBoardId null = Home
282
282
  pmx-canvas board create "Q4 planning" # created, not opened
283
+ pmx-canvas board open <board-id> # open the returned ID before writing
284
+ pmx-canvas board open --home # return the shared workbench to Home
283
285
  pmx-canvas board rename <board-id> "New name"
284
286
  pmx-canvas board create "Q4 planning" --category Planning
285
287
  pmx-canvas board category <board-id> "Planning/Quarterly" # nested folders; --clear unfiles it
@@ -287,8 +289,10 @@ pmx-canvas board from <source-id> "Follow-up" --preview
287
289
  pmx-canvas board from <source-id> "Follow-up" --nodes id1,id2 --readme --structure
288
290
  ```
289
291
 
290
- Commands write to the board open in the workbench. Opening, switching and
291
- deleting boards happens in the workbench (the human's call). `board from`
292
+ Commands write to the board open in the workbench. `board open` switches the
293
+ shared visible board, not a private CLI target. Verify its returned
294
+ `activeBoardId` before writing; another participant can switch it later.
295
+ Deletion remains human-only. `board from`
292
296
  excludes `prompt`, `response`, `trace`, and `mcp-app` cards; selected reusable
293
297
  cards get fresh IDs. `--structure` includes group frames; edges between copied
294
298
  cards are retained automatically. `--readme` carries the designated README. The new board remains inactive and
@@ -300,6 +304,9 @@ Boards save automatically. Backups are separate recovery copies of the whole lib
300
304
  configured through the CLI or HTTP API rather than the Home screen. Scheduled backups
301
305
  run while the server is running; scheduling is off until configured.
302
306
 
307
+ Use `--to` for the destination folder. Backup and restore commands reject unknown
308
+ flags (including `--folder`) rather than silently using the configured default.
309
+
303
310
  ```bash
304
311
  pmx-canvas backup # every board, now; into .pmx-canvas/backups
305
312
  pmx-canvas backup --to ~/Backups/pmx --keep 14
package/docs/http-api.md CHANGED
@@ -48,11 +48,13 @@ the CLI, not an HTTP job endpoint.
48
48
  A workspace holds many named boards; one is open at a time. Agent writes go to
49
49
  the open board. While no board is open (Home), the first write opens the most
50
50
  recently opened board, or creates one when the workspace has none;
51
- `canvas.clear` on Home opens nothing. Only the trusted workbench opens or
52
- deletes a board — HTTP calls require both `x-pmx-workbench: 1` and the
51
+ `canvas.clear` on Home opens nothing. Agents can explicitly open a board using
52
+ `POST /api/canvas/boards/open`; verify the returned `activeBoardId` before writing.
53
+ This changes the shared workbench, not a private per-agent target. Only the
54
+ trusted workbench deletes a board — deletion requires both `x-pmx-workbench: 1` and the
53
55
  per-boot `x-pmx-workbench-token` embedded in the served `/workbench` HTML;
54
- the marker alone is not authority. Public HTTP integrations must not open or
55
- delete boards. In-process SDK embedding hosts retain authority to do so.
56
+ the marker alone is not authority. Public HTTP integrations must not delete
57
+ boards. In-process SDK embedding hosts retain deletion authority.
56
58
  Every change emits a
57
59
  `boards-changed` SSE frame with `{ activeBoardId, boards }`.
58
60
 
@@ -87,10 +89,9 @@ curl -X PATCH http://localhost:4313/api/canvas/boards/<board-id> \
87
89
  curl -X PATCH http://localhost:4313/api/canvas/boards/<board-id> \
88
90
  -H "Content-Type: application/json" -d '{"readmeNodeId":"<markdown-node-id>"}'
89
91
 
90
- # Open a board, or Home with {"id":null} — workbench only
92
+ # Open a board, or Home with {"id":null} — also available to agents
91
93
  curl -X POST http://localhost:4313/api/canvas/boards/open \
92
- -H "Content-Type: application/json" -H "x-pmx-workbench: 1" \
93
- -H "x-pmx-workbench-token: <token-from-served-workbench>" -d '{"id":"<board-id>"}'
94
+ -H "Content-Type: application/json" -d '{"id":"<board-id>"}'
94
95
 
95
96
  # Delete a board and its snapshots — workbench only; deleting the open board returns to Home
96
97
  curl -X DELETE http://localhost:4313/api/canvas/boards/<board-id> \
@@ -158,7 +159,10 @@ curl "http://localhost:4313/api/canvas/exports/<file>.html?download=1" -o board.
158
159
 
159
160
  PDF and Office uploads are raw bytes, not JSON. Upload requires the trusted
160
161
  workbench token, an active captured board, a non-empty body of at most 20 MiB,
161
- and creates both an original file card and an `attached` import. The original
162
+ and creates both an original file card and an `attached` import. Requested x/y
163
+ coordinates are preserved when free; otherwise the original card is placed in
164
+ open space with a 48-unit gap, without moving existing cards. The import's
165
+ initial anchor follows the original card's actual position. The original
162
166
  remains board-owned until board deletion; snapshot restore does not remove it,
163
167
  and whole-database backups include it.
164
168
 
package/docs/mcp.md CHANGED
@@ -65,7 +65,7 @@ its `action` to the same operation the legacy tool used, so results are identica
65
65
  | `canvas_ax_timeline` | `read` · `record-event` · `add-evidence` · `send-steering` · `reads` | `canvas_get_ax_timeline`, `canvas_record_ax_event`, `canvas_add_evidence`, `canvas_send_steering` (`reads` is new — the context read log) |
66
66
  | `canvas_ax_delivery` | `claim` (long-polls with `timeoutMs`) · `mark` | `canvas_claim_ax_delivery`, `canvas_mark_ax_delivery` |
67
67
  | `canvas_intent` | `signal` · `update` · `clear` | _(new — Ghost Cursor of Intent; no legacy standalone tool)_ |
68
- | `canvas_board` | `list` · `get` · `create` · `create-from` · `update` | Boards, README designation, and transactional inactive copies |
68
+ | `canvas_board` | `list` · `get` · `create` · `create-from` · `open` · `update` | Boards, explicit shared-workbench switching, README designation, and transactional inactive copies |
69
69
  | `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 |
70
70
 
71
71
  Board categories are nested folder paths: `canvas_board { action: "update", id:
@@ -91,6 +91,13 @@ state, or history. Groups and the designated README are opt-in.
91
91
  Library search is `canvas_query { action: "search", query: "auth", scope:
92
92
  "library" }`. It returns board/card IDs and never switches the active board.
93
93
 
94
+ To author a new board, create it, then call `canvas_board { action: "open",
95
+ id: "<returned-board-id>" }` and verify `activeBoardId` in the response before
96
+ writing. Opening changes the shared workbench the human sees and the target of
97
+ subsequent writes; it is not a private agent selection. Another participant can
98
+ switch it later, so recheck after interruptions. `id: null` opens Home. Deletion
99
+ remains human-only.
100
+
94
101
  ### `canvas_import` — agent-assisted document extraction
95
102
 
96
103
  A PDF/Office drop retains the original attachment (maximum 20 MiB), but does
package/docs/sdk.md CHANGED
@@ -91,8 +91,8 @@ canvas.setAxFocus([n1], { source: 'sdk' });
91
91
  console.log(canvas.getAxState());
92
92
  console.log(canvas.getAxContext());
93
93
 
94
- // Boards — the SDK is the embedding host, so it may open and delete boards.
95
- // Writes made on Home (no board open) land on a new board; open one first.
94
+ // Boards — agents may open boards; deletion is reserved for trusted embedding hosts.
95
+ // Open the intended board before writing: this also switches the shared workbench.
96
96
  const board = await canvas.createBoard('Q4 planning', 'Planning'); // optional category
97
97
  await canvas.updateBoard(board.id, { category: 'Planning/Quarterly' }); // nested folders; null unfiles it
98
98
  await canvas.openBoard(board.id);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmx-canvas",
3
- "version": "0.7.1",
3
+ "version": "0.7.2",
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",
@@ -43,7 +43,7 @@ for the authoritative stop, viewport, persistence, reset, group-order, and captu
43
43
 
44
44
  ## Runtime prerequisites
45
45
 
46
- PMX Canvas 0.7.1 requires **Bun >=1.4.2**, including when installed through npm.
46
+ PMX Canvas 0.7.2 requires **Bun >=1.4.2**, including when installed through npm.
47
47
  Check `bun --version` and ensure Bun is on the MCP host's PATH, not just your shell's;
48
48
  use an absolute executable path if needed. Install/configure only when requested.
49
49
  For pinned installation, MCP configuration, managed services, and disposable verification,
@@ -95,9 +95,13 @@ the served catalog is a startup snapshot. Local mirrors still use `skills sync`
95
95
  floor, not a replacement: only an explicit signal gives the human a real pre-mutation veto
96
96
  window, your reasoning (`reason`), and staged multi-step previews. Batch and browser-human
97
97
  actions never auto-ghost.
98
- 6. **Attach a session so the human can see you (0.4.8+).** Start board work with
99
- `canvas_ax_state { action: "set-presence", attached: true, label: "<who you are>" }` and end
100
- it with `{ attached: false }` — detaching hands the human a receipt. Everything the session
98
+ 6. **Reuse your host session, or attach one if needed.** First read
99
+ `canvas_ax_state { action: "presence" }`. Copilot's extension already attaches as
100
+ `source: "copilot"` with consumer/session ID `copilot`: reuse that identity, do not create a
101
+ task-named agent. Put task descriptions in `detail`, not a new `agentId` or source. Only if
102
+ your host has no session, attach with `canvas_ax_state { action: "set-presence",
103
+ attached: true, label: "<who you are>" }` and detach that self-owned session when done.
104
+ Do not detach a host-owned session; its adapter manages its lifetime. Everything the session
101
105
  gives you and asks of you (cursor + phase chip, the session panel, steering, the scope fence's
102
106
  403s, the human edit lock's 409s, unattended approvals) is in **Sessions & the human** below.
103
107
  7. **Mutate through current composites.** Prefer the 16 composite MCP tools below.
@@ -360,7 +364,10 @@ The board has three modes, all gated on one fact — whether a session is attach
360
364
 
361
365
  What the session asks of you:
362
366
 
363
- - **Detach explicitly** (`attached: false` or a `session-end` activity). Attaching over a
367
+ - **Detach self-owned sessions explicitly** (`attached: false` or a `session-end` activity).
368
+ Reuse an adapter-owned session without attaching/detaching another; for Copilot, update
369
+ `{ source: "copilot", detail: "Team overview" }` without inventing an `agentId`. Separate
370
+ `agentId` values are for actual independent subagents, not steps in one chat. Attaching over a
364
371
  non-empty board saved a `Before session · …` snapshot; detaching emits the receipt (items
365
372
  done / vetoed, a diff against that snapshot, one-click restore) — an idle timeout delays it.
366
373
  - **403 = outside the scope fence.** The human may fence you to a region (`policy.scope`): writes
@@ -525,7 +532,13 @@ are text-only links through `data.boardId`; they are not direct cross-board card
525
532
  edges. `create-from` should be previewed first. It excludes prompt, response,
526
533
  trace, and MCP-app cards; groups and README are optional; creation assigns fresh
527
534
  IDs, copies no pins/AX/history, preserves the original, and leaves the new board
528
- inactive. Only the human opens/switches or deletes boards.
535
+ inactive. Agents can open/switch boards; only the human deletes them. To create
536
+ and populate a board, call `canvas_board { action: "create", name: "Planning" }`,
537
+ then `canvas_board { action: "open", id: "<returned-board-id>" }` (CLI:
538
+ `pmx-canvas board open <board-id>`). Verify the returned `activeBoardId` before
539
+ writing. Opening changes the shared visible workbench and subsequent write
540
+ target, not a private agent selection. Recheck after interruptions or another
541
+ participant's navigation. Use `id: null` or CLI `board open --home` for Home.
529
542
 
530
543
  Attribution is descriptive, not authority. `requestedBy`, `resolvedBy`, and
531
544
  `selfAnswer` tell you who wrote an ask or answer; an agent answer is not human
@@ -565,11 +565,11 @@
565
565
  "id": 23,
566
566
  "name": "multi-board-targeting",
567
567
  "prompt": "I have Roadmap and Incident Review boards. Add a decision note only to Incident Review and set its tour to the Summary group. Do not disturb Roadmap.",
568
- "expected_output": "Lists/gets boards, resolves the exact Incident Review board and opens or explicitly targets it before mutation, verifies the active target, searches for the Summary group there, then adds the note and sets that board's tour. It does not infer IDs from titles, mutate Roadmap, or assume that a board switch succeeded. It validates Incident Review and reports board/node IDs.",
568
+ "expected_output": "Lists/gets boards, resolves the exact Incident Review board and calls canvas_board action open or CLI board open with its ID, verifies the returned activeBoardId, searches for the Summary group there, then adds the note and sets that board's tour. It explains that opening switches the shared view but preserves Roadmap content. It does not infer IDs from titles, mutate Roadmap, invent inactive-board write targeting, or assume that a board switch succeeded. It validates Incident Review and reports board/node IDs.",
569
569
  "assertions": [
570
570
  {
571
571
  "name": "resolves-and-verifies-target-board",
572
- "description": "Uses board list/get/open or explicit board targeting to resolve and verify Incident Review before any write",
572
+ "description": "Resolves Incident Review with board list/get, explicitly opens its ID and verifies activeBoardId before writing; does not confuse read-only board targeting with write targeting",
573
573
  "type": "output_check"
574
574
  },
575
575
  {
@@ -662,6 +662,47 @@
662
662
  "type": "output_check"
663
663
  }
664
664
  ]
665
+ },
666
+ {
667
+ "id": 27,
668
+ "name": "create-open-and-populate-board",
669
+ "prompt": "Create a new Release checklist board, open it for me, and add a checklist. Keep the content on my current board intact. Do this yourself without asking me to navigate.",
670
+ "expected_output": "Creates Release checklist, captures its returned ID, explicitly opens it with canvas_board action open or CLI board open, verifies activeBoardId, and only then adds the checklist. Creation alone does not switch boards. It verifies the checklist on the new board and leaves the original board content intact. It explains that the shared workbench now shows the new board, without asking the human to open it or claiming private per-agent targeting. Deletion authority remains human-only.",
671
+ "assertions": [
672
+ {
673
+ "name": "opens-created-id-before-authoring",
674
+ "description": "Uses the returned creation ID in an explicit open call and verifies activeBoardId before adding content",
675
+ "type": "output_check"
676
+ },
677
+ {
678
+ "name": "preserves-original-board",
679
+ "description": "Does not clear, delete, or add the checklist to the previously open board; verifies the new board's content",
680
+ "type": "output_check"
681
+ },
682
+ {
683
+ "name": "no-human-navigation-handoff",
684
+ "description": "Performs opening itself and describes the shared visible switch rather than requesting a human click or claiming a private target",
685
+ "type": "output_check"
686
+ }
687
+ ]
688
+ },
689
+ {
690
+ "id": 28,
691
+ "name": "reuse-copilot-adapter-presence",
692
+ "prompt": "You are the main Copilot agent. The canvas presence list already has source copilot, sessionId copilot, label GitHub Copilot, attached true, with recent polling. Create a team overview on this board and show that you are working. You are not launching a subagent.",
693
+ "expected_output": "Reuses the existing adapter-owned copilot session. Reports task detail and phase on source copilot without creating a task-specific agentId or a second attached MCP session. Keeps steering addressed to consumer copilot. Does not detach the adapter-owned session after finishing; separate IDs are only for actual independent agents.",
694
+ "assertions": [
695
+ {
696
+ "name": "reuses-host-identity",
697
+ "description": "Updates source copilot with task detail instead of creating a second task-named identity or attaching a new session",
698
+ "type": "output_check"
699
+ },
700
+ {
701
+ "name": "preserves-delivery-and-lifetime",
702
+ "description": "Uses consumer copilot for addressed steering and leaves attach/detach lifecycle to the host adapter",
703
+ "type": "output_check"
704
+ }
705
+ ]
665
706
  }
666
707
  ]
667
708
  }
@@ -443,7 +443,7 @@ single-purpose tools behind an `action` (and, for `canvas_ax_gate`, a `kind`) di
443
443
  | `canvas_edge` | `add` · `update` · `remove` | Connect / edit / disconnect nodes |
444
444
  | `canvas_group` | `create` · `add` · `ungroup` | Manage spatial group containers |
445
445
  | `canvas_history` | `undo` · `redo` | Time travel through the mutation ring buffer |
446
- | `canvas_board` | `list` · `get` · `create` · `create-from` · `update` | Boards, README designation, links/backlinks, and inactive copies |
446
+ | `canvas_board` | `list` · `get` · `create` · `create-from` · `open` · `update` | Boards, shared-workbench switching, README designation, links/backlinks, and inactive copies |
447
447
  | `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`) |
448
448
  | `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) |
449
449
  | `canvas_query` | `context` · `search` · `layout` · `validate` | Read the budgeted cross-board brief, find nodes by keyword, read full layout, or **`validate`** the board for node collisions / group-containment / dangling edges |
@@ -485,6 +485,11 @@ creation: `prompt`, `response`, `trace`, and `mcp-app` are excluded; README and
485
485
  group structure are optional; fresh IDs are allocated; pins, AX state, and
486
486
  history are not copied; the source remains unchanged and the new board inactive.
487
487
 
488
+ Use `canvas_board { action: "open", id: "<board-id>" }` to open a created or
489
+ existing board before authoring. Verify the returned `activeBoardId`; opening
490
+ switches the shared visible workbench and all subsequent write targets, not
491
+ just this agent's context. `id: null` opens Home. Deletion stays human-only.
492
+
488
493
  `canvas_ax_gate` takes **two** discriminators, `{ kind, action }` — e.g.
489
494
  `{ kind: "approval", action: "request", title }`,
490
495
  `{ kind: "elicitation", action: "resolve", id, response }`,
@@ -53,6 +53,12 @@ canvas writes — which reach the server as MCP or HTTP tool calls, not as `copi
53
53
  to that session by the server. Nothing to configure; `GET /api/canvas/ax/presence` shows the live
54
54
  state.
55
55
 
56
+ Read presence before starting board work. Reuse the extension's `copilot` session and
57
+ steering consumer; do not attach a second session such as “Copilot – team overview” with
58
+ a new source or `agentId`. Send task names as `detail` on `source: "copilot"`, preserving
59
+ the host's identity and polling route. The adapter owns attach/detach; the agent must not
60
+ end it after each task. Explicit agent IDs are reserved for genuinely independent subagents.
61
+
56
62
  ## What the Adapter Does
57
63
 
58
64
  - Opens the live PMX workbench directly in a native Copilot canvas panel.
@@ -138,14 +144,22 @@ Default discovery order:
138
144
  3. `PMX_CANVAS_PORT` / `PMX_WEB_CANVAS_PORT` / `4313` on loopback.
139
145
  4. Managed server startup for the current workspace when `autoStart` is not `false`.
140
146
 
141
- The adapter rejects an unrelated running PMX server unless `serverUrl` is explicit or
142
- `allowWorkspaceMismatch` is true.
147
+ The adapter rejects an unrelated running PMX server unless `allowWorkspaceMismatch` is true,
148
+ including when `serverUrl` is explicit.
149
+
150
+ If discovery fails, the panel shows a connection page, not a partial workbench. It checks the
151
+ original target automatically; **Check connection** retries immediately and **Start server**
152
+ explicitly allows local startup even if `autoStart` was disabled. After recovery it navigates
153
+ to the server's real `/workbench`, with Home and recent boards. Adapter actions keep using that
154
+ server and refuse writes if its workspace changes. Reopen the canvas explicitly to select a
155
+ different target. The fallback shell's own `/health` or `/api/canvas/boards` returning 404 is
156
+ expected: the shell is not a PMX API proxy.
143
157
 
144
158
  ## Actions
145
159
 
146
160
  | Action | Purpose |
147
161
  |---|---|
148
- | `status` | Return PMX server health and persisted AX state. |
162
+ | `status` | Return PMX server health and persisted AX state, excluding serialized node context (`includeContext=false`). |
149
163
  | `get_ax_context` | Return current pinned + focused AX context. |
150
164
  | `focus_nodes` | Set AX focus with `source: "copilot"`. |
151
165
  | `send_instruction` | Send an explicit prompt into the active Copilot session. |
@@ -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.7.1**, which requires **Bun >=1.4.2** even when
7
+ The examples pin **PMX Canvas 0.7.2**, 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.7.1
20
+ npm install -g pmx-canvas@0.7.2
21
21
  pmx-canvas --version
22
22
  ```
23
23
 
24
- Without a global install, use `bunx pmx-canvas@0.7.1` in place of `pmx-canvas` in
24
+ Without a global install, use `bunx pmx-canvas@0.7.2` 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.7.1", "--mcp"],
94
+ "args": ["pmx-canvas@0.7.2", "--mcp"],
95
95
  "env": {
96
96
  "PMX_CANVAS_WORKSPACE_ROOT": "/absolute/path/to/project",
97
97
  "PMX_CANVAS_PORT": "14313",
@@ -12,12 +12,23 @@ import {
12
12
  showCommandHelp,
13
13
  } from '../shared.js';
14
14
 
15
+ function backupArgs(command: string, args: string[], supported: string[]) {
16
+ const parsed = parseFlags(args, { boolFlags: ['off'] });
17
+ const allowed = new Set(['help', 'h', ...supported]);
18
+ const unknown = Object.keys(parsed.flags).find((flag) => !allowed.has(flag));
19
+ if (unknown) die(`Unknown ${command} flag: --${unknown}`, `Run: pmx-canvas ${command} --help`);
20
+ for (const name of ['to', 'keep', 'every']) {
21
+ if (parsed.flags[name] === true) die(`Missing value for --${name}`, `Run: pmx-canvas ${command} --help`);
22
+ }
23
+ return parsed;
24
+ }
25
+
15
26
  cmd(
16
27
  'backup',
17
28
  'Back up every board to a folder (safe while the server runs)',
18
29
  ['pmx-canvas backup', 'pmx-canvas backup --to ~/Backups/pmx --keep 14'],
19
30
  async (args) => {
20
- const { flags } = parseFlags(args);
31
+ const { flags } = backupArgs('backup', args, ['to', 'keep']);
21
32
  if (flags.help || flags.h) return showCommandHelp('backup');
22
33
  const to = getStringFlag(flags, 'to');
23
34
  const keep = optionalNumberFlag(flags, 'keep', 'pmx-canvas backup --keep <n>');
@@ -26,7 +37,7 @@ cmd(
26
37
  );
27
38
 
28
39
  cmd('backup status', 'Show the backup folder, schedule and backups', ['pmx-canvas backup status'], async (args) => {
29
- const { flags } = parseFlags(args);
40
+ const { flags } = backupArgs('backup status', args, []);
30
41
  if (flags.help || flags.h) return showCommandHelp('backup status');
31
42
  output(await invokeOperation('backup.status', {}));
32
43
  });
@@ -36,7 +47,7 @@ cmd(
36
47
  'Back up on a schedule, run by the server',
37
48
  ['pmx-canvas backup schedule --every 24h --keep 14 --to ~/Backups/pmx', 'pmx-canvas backup schedule --off'],
38
49
  async (args) => {
39
- const { flags } = parseFlags(args);
50
+ const { flags } = backupArgs('backup schedule', args, ['every', 'off', 'to', 'keep']);
40
51
  if (flags.help || flags.h) return showCommandHelp('backup schedule');
41
52
  const every = flags.off ? 'off' : getStringFlag(flags, 'every');
42
53
  if (!every) die('Usage: pmx-canvas backup schedule --every <30m|24h|7d> [--keep <n>] [--to <folder>] | --off');
@@ -57,7 +68,7 @@ cmd(
57
68
  'Replace every board with a backup (the current file is kept as canvas.db.before-restore)',
58
69
  ['pmx-canvas restore ~/Backups/pmx/canvas-20260927T090000000Z.db'],
59
70
  async (args) => {
60
- const { positional, flags } = parseFlags(args);
71
+ const { positional, flags } = backupArgs('restore', args, []);
61
72
  if (flags.help || flags.h) return showCommandHelp('restore');
62
73
  const file = positional[0];
63
74
  if (!file) die('Usage: pmx-canvas restore <backup-file>');
@@ -1,5 +1,4 @@
1
- // Board commands (plan 012): board list, board create, board rename, board category.
2
- // Opening, switching and deleting boards is the human's, in the workbench.
1
+ // Board commands. Opening changes the shared workbench; deletion remains human-only.
3
2
 
4
3
  import { cmd, die, getStringFlag, invokeOperation, output, parseFlags, showCommandHelp } from '../shared.js';
5
4
 
@@ -9,6 +8,20 @@ cmd('board list', 'List boards, most recently opened first', ['pmx-canvas board
9
8
  output(await invokeOperation('board.list', {}));
10
9
  });
11
10
 
11
+ cmd(
12
+ 'board open',
13
+ 'Open a board in the shared workbench, or return Home with --home',
14
+ ['pmx-canvas board open <board-id>', 'pmx-canvas board open --home'],
15
+ async (args) => {
16
+ const { positional, flags } = parseFlags(args, { boolFlags: ['home'] });
17
+ if (flags.help || flags.h) return showCommandHelp('board open');
18
+ if (positional.length > 1 || (flags.home ? positional.length !== 0 : !positional[0])) {
19
+ die('Usage: pmx-canvas board open <board-id> | --home');
20
+ }
21
+ output(await invokeOperation('board.open', { id: flags.home ? null : positional[0] }));
22
+ },
23
+ );
24
+
12
25
  cmd(
13
26
  'board from',
14
27
  'Preview or create a board from selected cards on another board',
@@ -35,7 +48,7 @@ cmd(
35
48
 
36
49
  cmd(
37
50
  'board create',
38
- 'Create an empty board (the human opens it in the workbench)',
51
+ 'Create an empty board (use board open with its returned id before writing)',
39
52
  ['pmx-canvas board create "Q4 planning"', 'pmx-canvas board create "Q4 planning" --category Planning'],
40
53
  async (args) => {
41
54
  const { positional, flags } = parseFlags(args);
@@ -5,6 +5,7 @@ import {
5
5
  closeAttentionHistory,
6
6
  openAttentionHistory,
7
7
  } from '../state/attention-store';
8
+ import { IconClose } from '../icons';
8
9
 
9
10
  function formatTimestamp(timestamp: number): string {
10
11
  return new Date(timestamp).toLocaleTimeString([], {
@@ -73,7 +74,7 @@ export function AttentionHistory() {
73
74
  aria-label="Collapse changes panel"
74
75
  title="Collapse"
75
76
  >
76
- ×
77
+ <IconClose />
77
78
  </button>
78
79
  </div>
79
80
  <div class="attention-history-list">
@@ -1,7 +1,7 @@
1
1
  import { render, type VNode } from 'preact';
2
2
  import { useLayoutEffect, useMemo, useRef, useState } from 'preact/hooks';
3
3
  import { activeBoard, activeBoardId, boardList, boardsLoaded, openBoard } from '../state/boards-store';
4
- import { IconLogo, IconArrange } from '../icons';
4
+ import { IconLogo, IconArrange, IconChevronDown, IconPlus } from '../icons';
5
5
  import { promptNewBoard } from './HomeView';
6
6
 
7
7
  const RECENT_LIMIT = 8;
@@ -76,7 +76,7 @@ export function BoardSwitcher({ fallbackName }: { fallbackName: string }) {
76
76
  >
77
77
  <span class="board-switcher-name">{label}</span>
78
78
  <span class="board-switcher-caret" aria-hidden="true">
79
- ▾
79
+ <IconChevronDown />
80
80
  </span>
81
81
  </button>
82
82
  {open && anchor && (
@@ -130,7 +130,7 @@ export function BoardSwitcher({ fallbackName }: { fallbackName: string }) {
130
130
  }}
131
131
  >
132
132
  <span class="board-switcher-icon" aria-hidden="true">
133
- +
133
+ <IconPlus size={18} />
134
134
  </span>
135
135
  New board…
136
136
  </button>
@@ -38,7 +38,16 @@ import { KIND_COLOR } from './kind-colors';
38
38
  import { reportHumanGrab, takeOverNode, yieldedNodes } from '../state/human-store';
39
39
  import { AxStepControls } from '../nodes/AxStepControls';
40
40
  import { canOpenAsSite, openNodeAsSite } from '../nodes/surface-url';
41
- import { getNodeIcon } from '../icons';
41
+ import {
42
+ getNodeIcon,
43
+ IconChevronRight,
44
+ IconChevronDown,
45
+ IconClose,
46
+ IconMore,
47
+ IconExternalLink,
48
+ IconExpand,
49
+ IconSparkle,
50
+ } from '../icons';
42
51
  import { EXPANDABLE_TYPES, TYPE_LABELS } from '../types';
43
52
  import type { CanvasNodeState } from '../types';
44
53
  import { AUTO_FIT_TITLEBAR_HEIGHT, computeAutoFitHeight, shouldAutoFitNode } from './auto-fit';
@@ -472,7 +481,7 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
472
481
  toggleCollapsed(node.id);
473
482
  }}
474
483
  >
475
- ▸
484
+ <IconChevronRight size={14} />
476
485
  </button>
477
486
  <div class="group-chip-text">
478
487
  <div class="group-chip-name">{title}</div>
@@ -591,7 +600,7 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
591
600
  setGroupMenuOpen((open) => !open);
592
601
  }}
593
602
  >
594
- ⋯
603
+ <IconMore size={14} />
595
604
  </button>
596
605
  {groupMenuOpen && (
597
606
  <div class="group-menu" role="menu" onPointerDown={(e) => e.stopPropagation()}>
@@ -683,7 +692,7 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
683
692
  }}
684
693
  title={isContextPinned ? 'Remove from context' : 'Add to context'}
685
694
  >
686
- {'\u2726'}
695
+ <IconSparkle size={14} />
687
696
  </button>
688
697
  {/* Open as site — full-page standalone view of this node's surface,
689
698
  served from /api/canvas/surface/:id (same document as the canvas
@@ -698,7 +707,7 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
698
707
  }}
699
708
  title="Open as site"
700
709
  >
701
- ↗
710
+ <IconExternalLink size={14} />
702
711
  </button>
703
712
  )}
704
713
  {/* Expand — opens node as full-viewport overlay for focused work */}
@@ -711,7 +720,7 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
711
720
  }}
712
721
  title="Expand (focus mode)"
713
722
  >
714
- ⤢
723
+ <IconExpand size={14} />
715
724
  </button>
716
725
  )}
717
726
  <button
@@ -722,7 +731,7 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
722
731
  }}
723
732
  title={node.collapsed ? 'Expand' : 'Collapse'}
724
733
  >
725
- {node.collapsed ? '▸' : '▾'}
734
+ {node.collapsed ? <IconChevronRight size={14} /> : <IconChevronDown size={14} />}
726
735
  </button>
727
736
  {/* Report #64: status nodes get the same remove control as every other
728
737
  node type (backend removal + undo/history handle status uniformly). */}
@@ -734,7 +743,7 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
734
743
  }}
735
744
  title="Close"
736
745
  >
737
- ×
746
+ <IconClose size={14} />
738
747
  </button>
739
748
  </div>
740
749
  </div>