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.
- package/.github/extensions/pmx-canvas/extension.mjs +79 -61
- package/CHANGELOG.md +16 -1
- package/Readme.md +31 -6
- package/dist/canvas/global.css +21 -5
- package/dist/canvas/index.js +73 -73
- package/dist/types/client/canvas/ExternalWriters.d.ts +3 -3
- package/dist/types/client/icons.d.ts +15 -0
- package/dist/types/client/nodes/ExtAppFrame.d.ts +1 -1
- package/dist/types/client/nodes/FileNode.d.ts +0 -12
- package/dist/types/shared/delimited-text.d.ts +14 -0
- package/docs/cli.md +9 -2
- package/docs/http-api.md +12 -8
- package/docs/mcp.md +8 -1
- package/docs/sdk.md +2 -2
- package/package.json +1 -1
- package/skills/pmx-canvas/SKILL.md +19 -6
- package/skills/pmx-canvas/evals/evals.json +43 -2
- package/skills/pmx-canvas/references/full-reference.md +6 -1
- package/skills/pmx-canvas/references/github-copilot-app-adapter.md +17 -3
- package/skills/pmx-canvas/references/installing-pmx-canvas.md +4 -4
- package/src/cli/commands/backup.ts +15 -4
- package/src/cli/commands/boards.ts +16 -3
- package/src/client/canvas/AttentionHistory.tsx +2 -1
- package/src/client/canvas/BoardSwitcher.tsx +3 -3
- package/src/client/canvas/CanvasNode.tsx +17 -8
- package/src/client/canvas/CommandBar.tsx +2 -2
- package/src/client/canvas/ContextPinBar.tsx +3 -1
- package/src/client/canvas/ExpandedNodeOverlay.tsx +3 -3
- package/src/client/canvas/ExternalWriters.tsx +4 -3
- package/src/client/canvas/FeedbackDialog.tsx +2 -1
- package/src/client/canvas/IntentLayer.tsx +2 -2
- package/src/client/canvas/SelectionBar.tsx +2 -2
- package/src/client/canvas/SessionPanel.tsx +6 -4
- package/src/client/canvas/SessionReceipt.tsx +2 -1
- package/src/client/canvas/SnapshotPanel.tsx +3 -2
- package/src/client/canvas/ToolRail.tsx +2 -1
- package/src/client/canvas/TopBar.tsx +2 -2
- package/src/client/icons.tsx +118 -1
- package/src/client/nodes/BoardNode.tsx +2 -1
- package/src/client/nodes/ExtAppFrame.tsx +3 -1
- package/src/client/nodes/FileNode.tsx +4 -78
- package/src/client/nodes/ImageNode.tsx +2 -1
- package/src/client/nodes/MarkdownNode.tsx +2 -1
- package/src/client/nodes/PromptNode.tsx +3 -1
- package/src/client/theme/global.css +21 -5
- package/src/mcp/server.ts +2 -2
- package/src/server/agent-presence.ts +17 -11
- package/src/server/board-export.ts +97 -32
- package/src/server/index.ts +9 -7
- package/src/server/operations/composites.ts +3 -2
- package/src/server/operations/ops/ax-presence.ts +7 -3
- package/src/server/operations/ops/boards.ts +12 -5
- package/src/server/operations/registry.ts +4 -13
- package/src/server/server.ts +10 -3
- 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/
|
|
3
|
-
export declare function ActivityFeed(): import("preact/
|
|
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/
|
|
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/
|
|
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.
|
|
291
|
-
|
|
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.
|
|
52
|
-
|
|
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
|
|
55
|
-
|
|
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} —
|
|
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" -
|
|
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.
|
|
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 —
|
|
95
|
-
//
|
|
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.
|
|
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.
|
|
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. **
|
|
99
|
-
`canvas_ax_state { action: "
|
|
100
|
-
|
|
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).
|
|
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.
|
|
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
|
|
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": "
|
|
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 `
|
|
142
|
-
`
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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 } =
|
|
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 } =
|
|
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 } =
|
|
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 } =
|
|
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
|
|
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 (
|
|
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 {
|
|
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
|
-
{
|
|
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>
|