pmx-canvas 0.4.7 → 0.5.0

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 (212) hide show
  1. package/.github/extensions/pmx-canvas/extension.mjs +154 -5
  2. package/.github/extensions/pmx-canvas/steering-delivery.mjs +79 -0
  3. package/CHANGELOG.md +156 -0
  4. package/Readme.md +72 -23
  5. package/dist/canvas/global.css +3069 -796
  6. package/dist/canvas/index.js +104 -104
  7. package/dist/json-render/index.css +1 -1
  8. package/dist/types/cli/agent.d.ts +1 -0
  9. package/dist/types/cli/commands/pump.d.ts +9 -0
  10. package/dist/types/client/App.d.ts +1 -1
  11. package/dist/types/client/canvas/AgentPresenceLayer.d.ts +11 -0
  12. package/dist/types/client/canvas/BarHint.d.ts +22 -0
  13. package/dist/types/client/canvas/CanvasNode.d.ts +5 -0
  14. package/dist/types/client/canvas/CanvasViewport.d.ts +4 -4
  15. package/dist/types/client/canvas/CommandBar.d.ts +10 -0
  16. package/dist/types/client/canvas/ConnectionBanner.d.ts +13 -0
  17. package/dist/types/client/canvas/ContextMenu.d.ts +6 -0
  18. package/dist/types/client/canvas/EdgeLayer.d.ts +2 -1
  19. package/dist/types/client/canvas/EmptyState.d.ts +11 -0
  20. package/dist/types/client/canvas/ExternalWriters.d.ts +5 -0
  21. package/dist/types/client/canvas/HumanPresenceLayer.d.ts +8 -0
  22. package/dist/types/client/canvas/Minimap.d.ts +12 -1
  23. package/dist/types/client/canvas/ScopeFenceLayer.d.ts +8 -0
  24. package/dist/types/client/canvas/SelectionBar.d.ts +6 -0
  25. package/dist/types/client/canvas/SessionPanel.d.ts +1 -0
  26. package/dist/types/client/canvas/SessionReceipt.d.ts +17 -0
  27. package/dist/types/client/canvas/TextPrompt.d.ts +25 -0
  28. package/dist/types/client/canvas/ToolRail.d.ts +21 -0
  29. package/dist/types/client/canvas/TopBar.d.ts +7 -0
  30. package/dist/types/client/canvas/canvas-area.d.ts +25 -0
  31. package/dist/types/client/canvas/create-in-view.d.ts +18 -0
  32. package/dist/types/client/canvas/import-files.d.ts +8 -0
  33. package/dist/types/client/canvas/kind-colors.d.ts +3 -0
  34. package/dist/types/client/canvas/use-focus-trap.d.ts +14 -0
  35. package/dist/types/client/canvas/use-now.d.ts +6 -0
  36. package/dist/types/client/icons.d.ts +11 -4
  37. package/dist/types/client/nodes/ExtAppFrame.d.ts +32 -0
  38. package/dist/types/client/nodes/GroupNode.d.ts +6 -1
  39. package/dist/types/client/nodes/MarkdownNode.d.ts +2 -0
  40. package/dist/types/client/nodes/inline-editor-commands.d.ts +3 -4
  41. package/dist/types/client/state/canvas-store.d.ts +49 -5
  42. package/dist/types/client/state/human-store.d.ts +26 -0
  43. package/dist/types/client/state/iframe-mode.d.ts +1 -0
  44. package/dist/types/client/state/intent-bridge.d.ts +37 -5
  45. package/dist/types/client/state/node-factory.d.ts +0 -1
  46. package/dist/types/client/state/presence-store.d.ts +78 -0
  47. package/dist/types/client/state/session-store.d.ts +176 -0
  48. package/dist/types/client/types.d.ts +2 -1
  49. package/dist/types/client/utils/platform.d.ts +2 -0
  50. package/dist/types/server/agent-presence.d.ts +192 -0
  51. package/dist/types/server/ax-gate-ttl.d.ts +6 -0
  52. package/dist/types/server/ax-interaction.d.ts +4 -10
  53. package/dist/types/server/ax-state-manager.d.ts +21 -2
  54. package/dist/types/server/ax-state.d.ts +49 -3
  55. package/dist/types/server/ax-wait.d.ts +13 -0
  56. package/dist/types/server/canvas-db.d.ts +23 -1
  57. package/dist/types/server/canvas-operations.d.ts +1 -0
  58. package/dist/types/server/canvas-state.d.ts +27 -13
  59. package/dist/types/server/canvas-validation.d.ts +0 -14
  60. package/dist/types/server/html-surface.d.ts +1 -1
  61. package/dist/types/server/human-presence.d.ts +40 -0
  62. package/dist/types/server/index.d.ts +29 -3
  63. package/dist/types/server/intent-registry.d.ts +7 -0
  64. package/dist/types/server/mutation-history.d.ts +15 -2
  65. package/dist/types/server/operations/invoker.d.ts +13 -2
  66. package/dist/types/server/operations/ops/ax-presence.d.ts +2 -0
  67. package/dist/types/server/operations/ops/human-presence.d.ts +3 -0
  68. package/dist/types/server/operations/registry.d.ts +11 -0
  69. package/dist/types/server/operations/types.d.ts +1 -1
  70. package/dist/types/server/scope-fence.d.ts +62 -0
  71. package/dist/types/server/server.d.ts +0 -12
  72. package/dist/types/shared/agent-presence.d.ts +130 -0
  73. package/dist/types/shared/approval-gates.d.ts +19 -0
  74. package/dist/types/shared/auto-arrange.d.ts +0 -1
  75. package/dist/types/shared/ax-kinds.d.ts +11 -0
  76. package/dist/types/shared/human-presence.d.ts +31 -0
  77. package/dist/types/shared/iframe-probe.d.ts +8 -0
  78. package/dist/types/shared/scope-fence.d.ts +21 -0
  79. package/docs/RELEASE.md +54 -0
  80. package/docs/ax-host-adapter-contract.md +30 -14
  81. package/docs/cli.md +37 -1
  82. package/docs/environment.md +3 -0
  83. package/docs/http-api.md +222 -5
  84. package/docs/mcp.md +6 -5
  85. package/docs/node-types.md +3 -1
  86. package/docs/plans/plan-009-tech-debt-backlog.md +13 -0
  87. package/docs/screenshot.png +0 -0
  88. package/docs/screenshots/multi-agent-live.png +0 -0
  89. package/docs/screenshots/workbench-live-dark.png +0 -0
  90. package/docs/screenshots/workbench-live-light.png +0 -0
  91. package/docs/sdk.md +1 -1
  92. package/package.json +1 -1
  93. package/skills/pmx-canvas/SKILL.md +109 -18
  94. package/skills/pmx-canvas/references/codex-app-adapter.md +28 -0
  95. package/skills/pmx-canvas/references/full-reference.md +55 -16
  96. package/skills/pmx-canvas/references/github-copilot-app-adapter.md +41 -24
  97. package/skills/pmx-canvas-orchestration/SKILL.md +56 -2
  98. package/skills/pmx-canvas-testing/SKILL.md +57 -0
  99. package/src/cli/agent.ts +1 -0
  100. package/src/cli/commands/copilot.ts +15 -8
  101. package/src/cli/commands/nodes.ts +1 -19
  102. package/src/cli/commands/pump.ts +252 -0
  103. package/src/cli/index.ts +1 -5
  104. package/src/cli/shared.ts +1 -1
  105. package/src/client/App.tsx +209 -585
  106. package/src/client/canvas/AgentPresenceLayer.tsx +74 -0
  107. package/src/client/canvas/AttentionHistory.tsx +0 -10
  108. package/src/client/canvas/BarHint.tsx +49 -0
  109. package/src/client/canvas/CanvasNode.tsx +488 -136
  110. package/src/client/canvas/CanvasViewport.tsx +190 -94
  111. package/src/client/canvas/CommandBar.tsx +140 -0
  112. package/src/client/canvas/CommandPalette.tsx +133 -58
  113. package/src/client/canvas/ConnectionBanner.tsx +65 -0
  114. package/src/client/canvas/ContextMenu.tsx +88 -27
  115. package/src/client/canvas/ContextPinBar.tsx +2 -2
  116. package/src/client/canvas/EdgeLayer.tsx +121 -38
  117. package/src/client/canvas/EmptyState.tsx +93 -0
  118. package/src/client/canvas/ExpandedNodeOverlay.tsx +64 -118
  119. package/src/client/canvas/ExternalWriters.tsx +308 -0
  120. package/src/client/canvas/FocusFieldLayer.tsx +12 -1
  121. package/src/client/canvas/HumanPresenceLayer.tsx +45 -0
  122. package/src/client/canvas/IntentLayer.tsx +1 -1
  123. package/src/client/canvas/Minimap.tsx +117 -190
  124. package/src/client/canvas/ScopeFenceLayer.tsx +38 -0
  125. package/src/client/canvas/SelectionBar.tsx +175 -23
  126. package/src/client/canvas/SessionPanel.tsx +481 -0
  127. package/src/client/canvas/SessionReceipt.tsx +141 -0
  128. package/src/client/canvas/ShortcutOverlay.tsx +21 -6
  129. package/src/client/canvas/SnapshotPanel.tsx +166 -80
  130. package/src/client/canvas/TextPrompt.tsx +128 -0
  131. package/src/client/canvas/ToolRail.tsx +447 -0
  132. package/src/client/canvas/TopBar.tsx +308 -0
  133. package/src/client/canvas/auto-fit.ts +1 -5
  134. package/src/client/canvas/canvas-area.ts +41 -0
  135. package/src/client/canvas/create-in-view.ts +48 -0
  136. package/src/client/canvas/import-files.ts +64 -0
  137. package/src/client/canvas/kind-colors.ts +22 -0
  138. package/src/client/canvas/snap-guides.ts +1 -1
  139. package/src/client/canvas/use-focus-trap.ts +54 -0
  140. package/src/client/canvas/use-now.ts +17 -0
  141. package/src/client/canvas/use-pan-zoom.ts +13 -2
  142. package/src/client/icons.tsx +57 -18
  143. package/src/client/nodes/ExtAppFrame.tsx +79 -2
  144. package/src/client/nodes/GroupNode.tsx +9 -28
  145. package/src/client/nodes/InlineFormatBar.tsx +4 -3
  146. package/src/client/nodes/InlineMarkdownEditor.tsx +27 -0
  147. package/src/client/nodes/MarkdownNode.tsx +59 -24
  148. package/src/client/nodes/MdFormatBar.tsx +2 -4
  149. package/src/client/nodes/inline-editor-commands.ts +17 -6
  150. package/src/client/nodes/use-surface-frame.ts +3 -1
  151. package/src/client/state/attention-bridge.ts +3 -0
  152. package/src/client/state/canvas-store.ts +273 -84
  153. package/src/client/state/human-store.ts +175 -0
  154. package/src/client/state/iframe-mode.ts +33 -9
  155. package/src/client/state/intent-bridge.ts +75 -13
  156. package/src/client/state/node-factory.ts +2 -8
  157. package/src/client/state/presence-store.ts +168 -0
  158. package/src/client/state/session-store.ts +419 -0
  159. package/src/client/state/sse-bridge.ts +93 -30
  160. package/src/client/theme/global.css +3069 -796
  161. package/src/client/types.ts +3 -1
  162. package/src/client/utils/platform.ts +6 -1
  163. package/src/mcp/canvas-access.ts +2 -2
  164. package/src/server/agent-presence.ts +755 -0
  165. package/src/server/ax-context.ts +45 -0
  166. package/src/server/ax-gate-ttl.ts +61 -0
  167. package/src/server/ax-interaction.ts +29 -1
  168. package/src/server/ax-state-manager.ts +78 -6
  169. package/src/server/ax-state.ts +82 -15
  170. package/src/server/ax-wait.ts +33 -0
  171. package/src/server/canvas-db.ts +237 -61
  172. package/src/server/canvas-operations.ts +17 -8
  173. package/src/server/canvas-state.ts +131 -58
  174. package/src/server/canvas-validation.ts +3 -53
  175. package/src/server/demo-state.json +2 -70
  176. package/src/server/html-surface.ts +11 -2
  177. package/src/server/human-presence.ts +160 -0
  178. package/src/server/index.ts +107 -4
  179. package/src/server/intent-registry.ts +13 -0
  180. package/src/server/mutation-history.ts +47 -2
  181. package/src/server/operations/composites.ts +6 -3
  182. package/src/server/operations/http.ts +10 -1
  183. package/src/server/operations/index.ts +4 -0
  184. package/src/server/operations/invoker.ts +33 -6
  185. package/src/server/operations/ops/ax-presence.ts +62 -0
  186. package/src/server/operations/ops/ax-read.ts +8 -0
  187. package/src/server/operations/ops/ax-state.ts +106 -1
  188. package/src/server/operations/ops/ax-timeline.ts +54 -5
  189. package/src/server/operations/ops/ax-work.ts +46 -0
  190. package/src/server/operations/ops/batch.ts +3 -1
  191. package/src/server/operations/ops/edges.ts +88 -2
  192. package/src/server/operations/ops/groups.ts +4 -3
  193. package/src/server/operations/ops/human-presence.ts +35 -0
  194. package/src/server/operations/ops/json-render.ts +11 -1
  195. package/src/server/operations/ops/nodes.ts +65 -16
  196. package/src/server/operations/ops/query.ts +1 -0
  197. package/src/server/operations/ops/snapshots.ts +16 -0
  198. package/src/server/operations/registry.ts +196 -1
  199. package/src/server/operations/types.ts +1 -1
  200. package/src/server/scope-fence.ts +207 -0
  201. package/src/server/server.ts +177 -79
  202. package/src/server/trace-manager.ts +0 -2
  203. package/src/server/web-artifacts.ts +0 -1
  204. package/src/shared/agent-presence.ts +162 -0
  205. package/src/shared/approval-gates.ts +35 -0
  206. package/src/shared/auto-arrange.ts +121 -13
  207. package/src/shared/ax-kinds.ts +23 -0
  208. package/src/shared/human-presence.ts +30 -0
  209. package/src/shared/iframe-probe.ts +8 -0
  210. package/src/shared/scope-fence.ts +31 -0
  211. package/dist/types/client/canvas/DockedNode.d.ts +0 -4
  212. package/src/client/canvas/DockedNode.tsx +0 -165
@@ -12,7 +12,6 @@ export interface ArrangeNode {
12
12
  position: ArrangePosition;
13
13
  size: ArrangeSize;
14
14
  pinned: boolean;
15
- dockPosition: 'left' | 'right' | null;
16
15
  data: Record<string, unknown>;
17
16
  }
18
17
  export interface ArrangeEdge {
@@ -0,0 +1,11 @@
1
+ /**
2
+ * AX status and kind unions shared by the server's state model and the
3
+ * client's session panel, so a new status cannot be added on one side and
4
+ * silently render unrecognized on the other.
5
+ */
6
+ export type AxWorkItemStatus = 'todo' | 'in-progress' | 'blocked' | 'done' | 'cancelled';
7
+ /** `held` = auto-held by the unattended-approval policy: the action did NOT proceed. */
8
+ export type AxApprovalStatus = 'pending' | 'approved' | 'rejected' | 'held';
9
+ export type AxEventKind = 'prompt' | 'assistant-message' | 'tool-start' | 'tool-result' | 'failure' | 'approval' | 'steering' | 'command' | 'note' | 'policy'
10
+ /** A human took over a node the agent was editing; the agent yielded (rail-chrome-v2 item 6). */
11
+ | 'yield';
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Human collaborator presence (rail-chrome-v2 phase 8, design items 5 and 6).
3
+ * Every open workbench tab is a client; the server fans the set out over SSE
4
+ * so tabs see each other's cursors, and an agent write to a node a human is
5
+ * holding is refused until the grab ends (user wins). In-memory, TTL-swept,
6
+ * never persisted — like agent presence.
7
+ */
8
+ export interface HumanPresence {
9
+ /** Per-tab id (sessionStorage), never an identity. */
10
+ clientId: string;
11
+ /** Display name shown on the cursor tag. */
12
+ name: string;
13
+ /** World coordinates of the pointer, null when it left the canvas. */
14
+ cursor: {
15
+ x: number;
16
+ y: number;
17
+ } | null;
18
+ /** The node this human is dragging or editing right now. */
19
+ grabbingNodeId: string | null;
20
+ lastSeenAt: string;
21
+ }
22
+ export interface HumanPresenceSnapshot {
23
+ humans: HumanPresence[];
24
+ }
25
+ /** A tab that stops reporting is gone after this. */
26
+ export declare const HUMAN_PRESENCE_TTL_MS = 30000;
27
+ /** A grab the tab stops renewing (crash, tab closed mid-drag) releases after this. */
28
+ export declare const HUMAN_GRAB_TTL_MS = 8000;
29
+ /** Cursor updates are throttled to this on the client. */
30
+ export declare const HUMAN_CURSOR_THROTTLE_MS = 80;
31
+ export declare const MAX_HUMANS = 32;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Handshake contract between /api/canvas/iframe-probe (server document) and
3
+ * probeSrcIframes (client). The probe document posts this marker to its parent;
4
+ * receiving it is the ONLY proof that src-URL iframes truly load — hosts that
5
+ * block sub-frame requests (the Claude Code desktop browser) still fire `load`
6
+ * on the error page they commit instead.
7
+ */
8
+ export declare const IFRAME_PROBE_MESSAGE_SOURCE = "pmx-canvas-iframe-probe";
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Scope fence geometry shared by the server's enforcement and the client's
3
+ * rendering so both draw the same box: the bounding box of the fenced nodes
4
+ * plus `padding` px. Null when none of the fenced nodes exist.
5
+ */
6
+ export interface FenceRect {
7
+ x: number;
8
+ y: number;
9
+ width: number;
10
+ height: number;
11
+ }
12
+ export declare function fenceRectFromNodes(nodes: Iterable<{
13
+ position: {
14
+ x: number;
15
+ y: number;
16
+ };
17
+ size: {
18
+ width: number;
19
+ height: number;
20
+ };
21
+ }>, padding: number): FenceRect | null;
package/docs/RELEASE.md CHANGED
@@ -44,6 +44,25 @@ discovery with a `.esm.preflight` loader error (ERR-20260508-001).
44
44
  and exercises the CLI flows from
45
45
  [`docs/evals/e2e-cli-coverage.md`](evals/e2e-cli-coverage.md).
46
46
 
47
+ **Bump the pinned version examples.** `Readme.md` pins an exact version in
48
+ its install/orb-setup snippets *because* it tells readers to pin. Three
49
+ releases in a row shipped pointing at the previous version — twice the
50
+ surrounding prose described features that pin does not have, so a reader
51
+ following it copy-pasted a broken setup.
52
+
53
+ ```bash
54
+ grep -rn "pmx-canvas@0\." Readme.md docs/ # every hit must be the version you are about to ship
55
+ ```
56
+
57
+ **If this release touches CI or the release workflows, review that as
58
+ release content.** A changed gate gets its first real run *on this
59
+ release*: `e2e-cli` once launched Playwright without installing the
60
+ browser, the publish gate once queried `GITHUB_SHA` (the tag object, not
61
+ the commit) so it would have blocked every annotated tag, and
62
+ `test:coverage` once lacked `PMX_CANVAS_DISABLE_BROWSER_OPEN=1` so it
63
+ failed *and* opened a real browser. Ask what happens on a fresh runner
64
+ and on a tag event, not just on your machine.
65
+
47
66
  ## Versioning
48
67
 
49
68
  Semantic versioning, in 0.x semver honestly (see `docs/api-stability.md`).
@@ -87,6 +106,23 @@ Don't ship a release without a CHANGELOG entry. The GitHub release
87
106
  notes file (`/tmp/pmx-canvas-vX.Y.Z-release-notes.md` by convention)
88
107
  expands on the CHANGELOG with examples and migration notes.
89
108
 
109
+ **Check the entries are under a NEW heading.** Feature commits land bullets
110
+ at the top of the file, and three times in the 0.3.x–0.4.x cycles they were
111
+ appended under the *already-published* section instead — so the release
112
+ notes shipped empty while the previous release's published history claimed
113
+ features it never had. Before tagging:
114
+
115
+ ```bash
116
+ # every bullet added since the last tag must sit above the last released heading
117
+ git diff v<PREVIOUS>..HEAD -- CHANGELOG.md | grep '^+' | head -40
118
+ grep -n '^## \[' CHANGELOG.md | head -3
119
+ ```
120
+
121
+ Same rule in reverse: never retro-edit a published section to describe new
122
+ behavior (a theme's colour was changed in place once, so the released notes
123
+ described a build that never shipped it). Correct it forward under the new
124
+ version instead.
125
+
90
126
  **Release-blocker gate:** a breaking change without a `### Breaking`
91
127
  CHANGELOG entry blocks the release (`docs/api-stability.md`). Check the
92
128
  diff against the previous tag for removed/renamed public surface — MCP
@@ -95,6 +131,18 @@ anything broke and isn't named under `### Breaking`, add the entry first.
95
131
 
96
132
  ## Tag → publish
97
133
 
134
+ **Immediately before tagging, re-read the Test CI conclusion — never assert it
135
+ from memory.** v0.3.2 was tagged and published while its Test run had concluded
136
+ *failure*: an interruption landed between the watch finishing and the tag step,
137
+ and "CI was green" was recalled rather than checked. A `gh run watch` exiting 0
138
+ is only evidence if you actually read its output.
139
+
140
+ ```bash
141
+ gh run view <run-id> --json status,conclusion -q '.status + " / " + .conclusion'
142
+ gh run view <run-id> --json jobs -q '.jobs[] | .name + ": " + .conclusion'
143
+ # every job must read `success` — a matrix job added in THIS release has never run green before
144
+ ```
145
+
98
146
  The publish workflow ([`/.github/workflows/publish.yml`](../.github/workflows/publish.yml))
99
147
  triggers on tags matching `v*`:
100
148
 
@@ -193,6 +241,12 @@ with `ok: true`, the published tarball is intact end-to-end.
193
241
  in `Readme.md` actually needs the refresh.
194
242
  - **CHANGELOG dates**: use the actual publish date in the
195
243
  `## [version] - YYYY-MM-DD` header, not the day you wrote the entry.
244
+ - **npm publish is asynchronous now**: a successful publish prints "Your
245
+ package is being processed and may take a few minutes to become
246
+ available" — `npm view` 404s on the new version and `latest` stays on
247
+ the old one for several minutes (~4 min observed for 0.4.8). Poll
248
+ before diagnosing a failed publish; the Publish CI step log's
249
+ `+ pmx-canvas@X.Y.Z` line is the proof it landed.
196
250
  - **Node 24 deprecation timeline**: GitHub Actions is migrating
197
251
  `actions/checkout`, `setup-node`, `upload-artifact` to Node 24 by
198
252
  June 2, 2026. The publish workflow already pins `@v5` of all three.
@@ -24,11 +24,28 @@ surface each one binds to.
24
24
  | `ingestActivity(event)` | `POST /api/canvas/ax/activity` · `canvas_ingest_activity` — board auto-reacts | Forwarding the host's tool/session hooks |
25
25
  | `awaitGate(id)` | `GET /api/canvas/ax/{approval\|elicitation\|mode}/<id>?waitMs=` · `canvas_ax_gate { kind: "approval"\|"elicitation"\|"mode", action: "await" }` | Optionally surfacing a native modal; the agent must await PMX |
26
26
  | `mirrorLog(event)` *(optional)* | `GET /api/canvas/ax/timeline` · `canvas://ax-timeline` | Writing AX events into the host's own chat/session log |
27
+ | `presence()` | `POST /api/canvas/ax/presence` · `canvas_ax_state { action: "set-presence" }` — or simply `ingestActivity` with `session-start` / `session-end` / `tool-start` / `tool-result` | Calling it from the host's session and tool hooks. Attach saves a pre-session snapshot; detach emits the `agent-session-ended` receipt — detach on shutdown (bounded, best-effort) so the human gets it promptly. If the host knows the model's token usage, report it as `contextUsage: { used, total }` — the top-bar meter then shows the real window instead of the pinned-context estimate |
27
28
 
28
- ## Steering is gated, not pushed (#54)
29
+ ### Presence: the cursor works for every adapter
29
30
 
30
- A board action (e.g. an `ax.steer` emit from a surface button) enqueues a steering
31
- message; it does **not** wake the agent. It reaches the next turn only when:
31
+ An adapter does not need per-call labelling for the canvas to show its agent's
32
+ cursor. Attach once (`attached: true`, or a `session-start` activity) and detach
33
+ on exit; from then on every canvas write the agent makes — through MCP, HTTP,
34
+ the CLI or the SDK, under any transport label — is attributed to that session
35
+ while it is the only one attached. The cursor moves to the node each write
36
+ touched and the phase reads `tooling` during work, `waiting-approval` while a
37
+ gate is pending. Report `thinking` from a prompt/turn hook when the host has one
38
+ (the Copilot extension does); name the agent with `PMX_CANVAS_AGENT_SOURCE` on
39
+ the MCP server when the host runs several agents side by side, and pass
40
+ `agentId` for sub-agents that should keep their own cursor.
41
+
42
+ ## Steering is queued first, then pushed by capable adapters (#54)
43
+
44
+ A board action (e.g. an `ax.steer` emit from a surface button) always enqueues a steering message
45
+ first. A capable adapter can immediately claim it, call the host's native send, and mark it after
46
+ the host accepts the prompt. The GitHub Copilot reference adapter implements this delivery loop.
47
+
48
+ When a host cannot wake a turn, queued steering reaches the next turn when:
32
49
 
33
50
  1. **The pin/focus gate is open.** A typical adapter injects `/api/canvas/ax/context`
34
51
  only when something is pinned or focused (`pinned.count > 0 || focus.nodeIds.length > 0`).
@@ -50,17 +67,16 @@ many more are queued so it can drain the FIFO `…/delivery/pending` endpoint wh
50
67
  count is non-zero. **Adapters should read `delivery.pendingSteering`** (this compact,
51
68
  count-bearing block), not `timeline.pendingSteering`.
52
69
 
53
- ### Canvas-origin steering does not wake the agent by itself (#59)
54
-
55
- Recording a browser-origin `ax.steer` (and the `ok:true` ack a surface button gets —
56
- report #55) means the steer is **queued on the timeline**, not delivered into a live
57
- agent turn. PMX deliberately does not import a host SDK, so the *wake* — turning a
58
- queued steer into a visible turn — is **adapter-owned**: a cooperating host adapter
59
- must drain `…/delivery/pending?consumer=<id>` and call its native send (e.g.
60
- `copilotSession.send`), then `…/delivery/<id>/mark` it. Until an adapter wires that,
61
- canvas-origin steering is delivered on the next human turn, not pushed. A steering
62
- surface should therefore label its button honestly ("queued for the agent's next
63
- turn"), never imply it interrupts the agent now.
70
+ ### Canvas-origin steering requires an adapter-owned wake (#59)
71
+
72
+ Recording a browser-origin `ax.steer` (and the `ok:true` ack a surface button gets — report #55)
73
+ means the steer is durably queued on the timeline. PMX deliberately does not import a host SDK, so
74
+ the *wake* is adapter-owned: a cooperating host adapter drains
75
+ `…/delivery/pending?consumer=<id>`, calls its native send, then marks the delivery. The GitHub
76
+ Copilot adapter does this with a long-polling pump and `copilotSession.send`. It establishes a
77
+ baseline at adapter startup, marking older backlog without replaying it so stale instructions from
78
+ prior sessions cannot flood a newly opened chat. Adapters without a native send retain
79
+ next-human-turn delivery.
64
80
 
65
81
  ## The two primitives that close the loop
66
82
 
package/docs/cli.md CHANGED
@@ -13,6 +13,42 @@ pmx-canvas node list --port 4750 # target a non-default dae
13
13
  pmx-canvas --server-url http://127.0.0.1:4750 status # same, by URL
14
14
  ```
15
15
 
16
+ ## pump — reactive steering for CLI agents
17
+
18
+ `pmx-canvas pump` turns any CLI agent (Codex, Amp, a script) into a steerable
19
+ board citizen: it long-polls the per-consumer delivery claim and runs your
20
+ command once per steer, marking per-consumer on success.
21
+
22
+ On Windows the exec runs through `cmd.exe` and receives the steer message on
23
+ stdin. The `{message}` and `{id}` placeholders are refused there: `cmd.exe`
24
+ expands `%VAR%` and re-parses the result, so a steer containing shell
25
+ metacharacters could inject commands. Read stdin, or reference
26
+ `PMX_STEER_MESSAGE` / `PMX_STEER_ID` inside your own script.
27
+
28
+ ```bash
29
+ pmx-canvas pump --consumer codex-cli --exec 'codex exec --full-auto {message}'
30
+ pmx-canvas pump --consumer amp --exec 'amp -x {message}' --parent claude-code
31
+ ```
32
+
33
+ The Codex example starts a non-interactive CLI task. It does not steer an open
34
+ Codex desktop task: desktop owns that thread's writer and requires a host
35
+ adapter that uses the app's native follow-up channel.
36
+
37
+ - The steer text reaches the command as `$PMX_STEER_MESSAGE` (env) and stdin;
38
+ `{message}` in the template expands to the quoted env reference — steer
39
+ content is never spliced into the shell line.
40
+ - The delivery envelope rides along as env: `$PMX_STEER_ID`, `$PMX_STEER_SOURCE`
41
+ (who sent it), `$PMX_STEER_TARGET` (your consumer key, or `ALL` for a
42
+ broadcast), and `$PMX_STEER_CREATED_AT` — so the host turn can say who
43
+ steered, whether it was addressed, and how old it is.
44
+ - Startup backlog is marked silently (pass `--backlog deliver` to process it).
45
+ - A failing command is retried twice, then the pump exits non-zero and leaves
46
+ the steer pending so a failed host injection cannot look delivered.
47
+ - `--parent <key>` rolls the pumped agent up under an orchestrator's chip;
48
+ `--once` processes a single steer (useful in scripts and tests).
49
+ - Survives daemon restarts; backs off against pre-0.4.9 servers that ignore
50
+ `waitMs`.
51
+
16
52
  ## Server lifecycle
17
53
 
18
54
  ```bash
@@ -281,7 +317,7 @@ reports annotation targets and bounds; it does not describe whether the human
281
317
  drew an arrow, line, circle, or other shape. Inspect `.annotation-layer path` or
282
318
  take a screenshot when the drawn form matters.
283
319
 
284
- Humans draw with the pen toolbar button and remove marks with the eraser button.
320
+ Humans draw with the rail's Annotate popover (pen / text) and remove marks with its eraser.
285
321
  If an agent already knows the annotation ID from context, it can remove it through
286
322
  MCP with `canvas_view { action: "remove-annotation", id }`.
287
323
 
@@ -20,6 +20,9 @@ optional; defaults are listed.
20
20
  | `PMX_CANVAS_DB_PATH` | server | Overrides the SQLite path (default `.pmx-canvas/canvas.db`). |
21
21
  | `PMX_CANVAS_STATE_FILE` | server | `.db`-path alias only as of 0.4.0 (non-`.db` values are ignored with a boot warning; the pre-0.2 JSON import was removed). |
22
22
  | `PMX_CANVAS_BLOB_THRESHOLD_BYTES` | server | Node payloads above this many JSON bytes are stored as blobs (default `2048`). |
23
+ | `PMX_CANVAS_AGENT_SOURCE` | MCP transport, CLI | Names the agent behind this transport for presence (`codex`, `claude-code`, …) so its writes, attach and cursor share one identity; default is the transport label (`mcp` / `cli`). |
24
+ | `PMX_CANVAS_GATE_TTL_MS` | server | Default TTL for approval gates before the unattended-approval policy auto-holds them (default `300000` = 5 min, clamped to 1 s – 24 h). |
25
+ | `PMX_CANVAS_CONTEXT_BUDGET_TOKENS` | server | `budget.total` in agent presence — the ceiling the top bar's **Pins** meter measures the pinned-context token estimate against (default `32000`). It is not the agent's context window; a host reports that on the presence as `contextUsage`, and the meter then reads **Context**. |
23
26
 
24
27
  ## Server behavior
25
28
 
package/docs/http-api.md CHANGED
@@ -29,6 +29,12 @@ curl -X POST http://localhost:4313/api/canvas/schema/validate \
29
29
  -H "Content-Type: application/json" \
30
30
  -d '{"type":"json-render","spec":{"root":"card","elements":{"card":{"type":"Card","props":{"title":"Preview"},"children":[]}}}}'
31
31
 
32
+ # Create a json-render node from a spec. `height` and `nodeHeight` are both
33
+ # accepted for the node frame (nodeHeight matches the graph/stream actions).
34
+ curl -X POST http://localhost:4313/api/canvas/json-render \
35
+ -H "Content-Type: application/json" \
36
+ -d '{"title":"Panel","nodeHeight":280,"spec":{"root":"card","elements":{"card":{"type":"Card","props":{"title":"Live"},"children":[]}}}}'
37
+
32
38
  # Validate an HTML primitive without creating a node
33
39
  curl -X POST http://localhost:4313/api/canvas/schema/validate \
34
40
  -H "Content-Type: application/json" \
@@ -73,6 +79,17 @@ with no `fileContent`, plus `data.mimeType` when the type is known
73
79
  (`application/pdf` for PDFs). A text file larger than 2 MB is stored truncated
74
80
  with `data.truncated: true`.
75
81
 
82
+ ```bash
83
+ # Repoint a file node at another file — content re-read, watcher rewired,
84
+ # edges/pins/position kept (recreating the node loses all three)
85
+ curl -X PATCH http://localhost:4313/api/canvas/node/node-abc123 \
86
+ -H "Content-Type: application/json" \
87
+ -d '{"path":"src/server/canvas-state.ts"}'
88
+ ```
89
+
90
+ The title follows the new filename unless the node was renamed away from the
91
+ old one (or the patch sets `title` explicitly).
92
+
76
93
  ```bash
77
94
  # Raw bytes for a binary file node (PDF viewer, downloads)
78
95
  curl http://localhost:4313/api/canvas/file-bytes?nodeId=node-abc123
@@ -99,6 +116,16 @@ Search-based edge creation is intentionally strict: `fromSearch` and
99
116
  `toSearch` must each resolve to exactly one node. Broad queries that match
100
117
  multiple nodes fail; use the full visible title.
101
118
 
119
+ ```bash
120
+ # Update an edge in place (label, type, style, animated) — empty label clears it
121
+ curl -X PATCH http://localhost:4313/api/canvas/edge/edge-123 \
122
+ -H "Content-Type: application/json" \
123
+ -d '{"label":"retries","type":"depends-on","style":"dashed"}'
124
+ ```
125
+
126
+ Retyping an edge onto a from/to/type triple that already exists is refused
127
+ (400) — the same duplicate rule edge creation enforces.
128
+
102
129
  ## Annotations
103
130
 
104
131
  ```bash
@@ -164,10 +191,10 @@ curl -X POST http://localhost:4313/api/canvas/ax/event \
164
191
  -H "Content-Type: application/json" \
165
192
  -d '{"kind":"tool-start","summary":"ran tests","source":"api"}'
166
193
 
167
- # Timeline — send a steering message to the active agent session
194
+ # Timeline — send a steering message; `target` addresses ONE consumer, omit to broadcast
168
195
  curl -X POST http://localhost:4313/api/canvas/ax/steer \
169
196
  -H "Content-Type: application/json" \
170
- -d '{"message":"focus on the failing test first","source":"api"}'
197
+ -d '{"message":"focus on the failing test first","source":"api","target":"copilot"}'
171
198
 
172
199
  # Timeline — record an evidence item (logs/tool-result/screenshot/file/diff/test-output)
173
200
  curl -X POST http://localhost:4313/api/canvas/ax/evidence \
@@ -186,14 +213,21 @@ curl -X PATCH http://localhost:4313/api/canvas/ax/work/<id> \
186
213
  -d '{"status":"done"}'
187
214
  curl http://localhost:4313/api/canvas/ax/work
188
215
 
189
- # Canvas-bound — request / resolve an approval gate (pending → approved/rejected)
216
+ # Canvas-bound — request / resolve an approval gate (pending → approved/rejected/held)
190
217
  curl -X POST http://localhost:4313/api/canvas/ax/approval \
191
218
  -H "Content-Type: application/json" \
192
- -d '{"title":"Deploy to prod","action":"deploy.prod","source":"api"}'
219
+ -d '{"title":"Deploy to prod","action":"deploy.prod","ttlMs":300000,"source":"api"}'
193
220
  curl -X POST http://localhost:4313/api/canvas/ax/approval/<id>/resolve \
194
221
  -H "Content-Type: application/json" \
195
222
  -d '{"decision":"approved","source":"api"}'
196
223
  curl http://localhost:4313/api/canvas/ax/approval
224
+ # Unattended-approval policy: a gate nobody answers within its TTL (default
225
+ # PMX_CANVAS_GATE_TTL_MS = 5 min, max 24 h) resolves to `held` — the action does
226
+ # NOT proceed, an awaiting agent is released with a non-approval, and a
227
+ # `policy` agent-event explains why. Reopen it (fresh TTL) from the session
228
+ # panel or here; this is the human's path, not an MCP tool.
229
+ curl -X POST http://localhost:4313/api/canvas/ax/approval/<id>/reopen \
230
+ -H "Content-Type: application/json" -d '{"source":"browser"}'
197
231
 
198
232
  # Canvas-bound — add a review annotation (comment/finding) anchored to node/file/region
199
233
  curl -X POST http://localhost:4313/api/canvas/ax/review \
@@ -216,6 +250,152 @@ reject an unknown `status` with 400 (the tokens are `todo`, `in-progress`,
216
250
  and `PATCH /ax/review/:id` return 404 for unknown IDs; approval resolve returns
217
251
  404 if the gate is missing or already resolved.
218
252
 
253
+ ## Agent presence
254
+
255
+ Who is writing to the board right now, in what phase, and whether a session
256
+ is attached. Presence is *derived*: every agent-originated mutation (anything
257
+ without the workbench's own `x-pmx-workbench: 1` marker) registers its caller
258
+ as a `tooling` writer, the activity feed drives attach/detach and phase
259
+ (`session-start`, `session-end`, `tool-start`, `tool-result`), and adapters
260
+ with richer hooks can set a phase, cursor, or focus explicitly. Writers fade
261
+ 90 s after their last write; attached sessions expire after 30 min of quiet
262
+ without a `session-end`.
263
+
264
+ ```bash
265
+ # Read the snapshot (the browser does this on connect)
266
+ curl http://localhost:4313/api/canvas/ax/presence
267
+ # → { ok, presences: [{ sessionId, source, agentId, label, phase, detail,
268
+ # focusNodeId, cursor, attached, opCount, contextUsage, lastSeenAt }],
269
+ # budget: { used, total }, sessionActive,
270
+ # activity: [{ id, at, sessionId, label, op, summary, nodeId }] } # newest first, last 50
271
+
272
+ # Attach a session and report a phase (idle | thinking | tooling | waiting-approval)
273
+ curl -X POST http://localhost:4313/api/canvas/ax/presence \
274
+ -H "Content-Type: application/json" \
275
+ -d '{"source":"copilot","attached":true,"phase":"thinking","detail":"planning"}'
276
+
277
+ # Point the agent cursor at a node, then detach
278
+ curl -X POST http://localhost:4313/api/canvas/ax/presence \
279
+ -H "Content-Type: application/json" \
280
+ -d '{"source":"copilot","focusNodeId":"<node-id>","cursor":{"x":320,"y":140}}'
281
+ curl -X POST http://localhost:4313/api/canvas/ax/presence \
282
+ -H "Content-Type: application/json" -d '{"source":"copilot","attached":false}'
283
+ ```
284
+
285
+ `sessionActive` is true when any presence is attached — it is the single gate
286
+ for the agent chrome (session panel, command bar, presence layer). Live but
287
+ unattached writers are the *external steering* case: the browser shows a
288
+ passive top-bar indicator (writers + op count) with an activity feed and a
289
+ connected-writers sheet — visibility only, never permissions. `activity` is
290
+ that feed: one entry per agent write with a human summary ("Created markdown
291
+ “Release plan”"), attributed to the writer (re-attributed to the session when
292
+ a transport writer folds into it), bounded to the last 50 and kept after a
293
+ writer fades. Detaching (`attached: false`, `session-end`) removes the
294
+ presence outright — an ended session never lingers as an external writer.
295
+
296
+ **The context meter is an estimate unless the host says otherwise.** `budget`
297
+ is a token estimate (chars ÷ 4) of the `pinned-context` payload against
298
+ `PMX_CANVAS_CONTEXT_BUDGET_TOKENS` (default 32000) — what the human's pins
299
+ would cost the agent, not the agent's live window; the top bar labels it
300
+ **Pins**. A host that knows the agent's real usage reports it on the presence
301
+ (`contextUsage: { used, total }` on `POST /api/canvas/ax/presence` /
302
+ `set-presence`); the top bar then shows **Context** with those numbers. The
303
+ bundled Copilot extension reports it from the SDK's `session.usage_info`
304
+ event (root agent, coalesced to one report per 500 ms); the legacy
305
+ `context-usage` workbench event feeds the single attached session the same
306
+ way.
307
+
308
+ ```bash
309
+ curl -X POST http://localhost:4313/api/canvas/ax/presence \
310
+ -H "Content-Type: application/json" \
311
+ -d '{"source":"copilot","contextUsage":{"used":42800,"total":128000}}'
312
+ ```
313
+
314
+ **Writer identity and attribution.** Plain HTTP callers read as source `api`,
315
+ the CLI sends `x-pmx-source: cli`, the MCP server sends `mcp` (or
316
+ `PMX_CANVAS_AGENT_SOURCE` when set), the SDK is `sdk`. Those are *transport*
317
+ labels, not agents — so while exactly one session is attached, a transport
318
+ write with no `agentId` is attributed to that session: its cursor moves to
319
+ the touched node, its phase reads `tooling`, and no second writer appears.
320
+ This is what lets a Copilot/Codex/Claude Code session that writes through MCP
321
+ keep one cursor. Pass `agentId` (sub-agents) or a host label to keep a writer
322
+ separate; with several sessions attached, transport writes stay on their own
323
+ label. A session the human started (*Start agent session*, `source: "browser"`)
324
+ is a placeholder for whichever agent comes next: it absorbs agent-less writes
325
+ under *any* label — a host label included — and takes that agent's name. If
326
+ exactly one such writer is already on the board when the human starts the
327
+ session, it is adopted on the spot. One agent arriving over two channels —
328
+ an adapter presence plus its own MCP server, both announcing the same label —
329
+ merges into one session at attach, and the merged channel's writes and detach
330
+ keep landing on it.
331
+
332
+ **Session lifecycle: pre-session snapshot and receipt.** When a session
333
+ attaches (`attached: true`, or `session-start` on the activity feed) over a
334
+ non-empty board, the server saves a snapshot named
335
+ `Before session · <label> · HH:MM` — the board before the agent touched it.
336
+ When that session ends (`attached: false`, `session-end`, or the idle expiry)
337
+ the stream carries one `agent-session-ended` frame:
338
+
339
+ ```json
340
+ { "label": "Copilot", "endedAt": "…", "counts": { "items": 4, "done": 3, "vetoed": 1 },
341
+ "snapshot": { "id": "…", "name": "Before session · Copilot · 14:00" } }
342
+ ```
343
+
344
+ `items`/`done` count the work items on the board; `vetoed` counts cancelled
345
+ items plus rejected or held gates. `snapshot` is null when the board was
346
+ empty at attach. The browser renders this as the session receipt; its *View
347
+ diff* is `GET /api/canvas/snapshots/<id>/diff` against that snapshot, and
348
+ restoring the snapshot undoes the session. Adapters should end their session
349
+ explicitly so the human gets the receipt promptly rather than after the idle
350
+ expiry. The browser's *Start agent session* button is this same endpoint
351
+ (`source: "browser"`, `attached: true`) — subsequent agent-less writes are
352
+ attributed to that session, and the first named agent renames it.
353
+
354
+ ## Human presence (collaborators)
355
+
356
+ Every open workbench tab reports itself; the server fans the set out as
357
+ `human-presence` SSE frames so tabs see each other's cursors. A node a human
358
+ is holding (dragging) is an edit lock: an agent write targeting it is refused
359
+ with **409** ("being edited by <name> — requeue") until the grab is released
360
+ or goes stale (8 s without renewal). Membership changes count as targeting the
361
+ member: dissolving the group a held node belongs to (`group.remove`, or
362
+ `node.remove` on the group) or pulling it into another group is refused the
363
+ same way. Human heartbeats never count as agent activity.
364
+
365
+ ```bash
366
+ curl http://localhost:4313/api/canvas/human-presence
367
+ # → { ok, humans: [{ clientId, name, cursor, grabbingNodeId, lastSeenAt }] }
368
+ curl -X POST http://localhost:4313/api/canvas/human-presence \
369
+ -H "Content-Type: application/json" -H "x-pmx-workbench: 1" \
370
+ -d '{"clientId":"tab-1","name":"mia","cursor":{"x":320,"y":140},"grabbingNodeId":"<node-id>"}'
371
+ ```
372
+
373
+ ## Scope fence (agent writes)
374
+
375
+ The policy's `scope` limits what an attached agent may WRITE: existing-node
376
+ writes must target fenced nodes, new nodes must land inside the fenced
377
+ nodes' bounding box plus `padding` px (default 40), and board-wide writes
378
+ (arrange, clear, restore) are refused. Every refusal is HTTP 403 with a reason
379
+ naming the node or position. Reads are never fenced, nor are the human's own
380
+ workbench writes. A fence is a granted region: a group id in `nodeIds` expands
381
+ to the frame plus every member (nested groups included) when the scope is set.
382
+ The fence is visible to the agent in `canvas://ax-context` (`policy.scope`) and
383
+ drawn on the canvas while a session is attached.
384
+
385
+ The fence belongs to the human: only workbench calls (`x-pmx-workbench: 1`) may
386
+ set, replace, or clear `scope`. An agent call to `POST /api/canvas/ax/policy`
387
+ that includes `scope` is refused with 403, and the MCP `set-policy` action does
388
+ not take it.
389
+
390
+ ```bash
391
+ # Grant (replace semantics) and clear — workbench calls
392
+ curl -X POST http://localhost:4313/api/canvas/ax/policy \
393
+ -H "Content-Type: application/json" -H "x-pmx-workbench: 1" \
394
+ -d '{"scope":{"nodeIds":["<node-a>","<node-b>"],"padding":40},"source":"browser"}'
395
+ curl -X POST http://localhost:4313/api/canvas/ax/policy \
396
+ -H "Content-Type: application/json" -H "x-pmx-workbench: 1" -d '{"scope":null}'
397
+ ```
398
+
219
399
  ## AX interactions, delivery, elicitation, mode, commands & policy
220
400
 
221
401
  Node interactions are one normalized, capability-gated envelope that maps onto an
@@ -229,12 +409,32 @@ curl -X POST http://localhost:4313/api/canvas/ax/interaction \
229
409
  -H "Content-Type: application/json" \
230
410
  -d '{"type":"ax.work.create","sourceNodeId":"node-1","payload":{"title":"Wire auth"}}'
231
411
 
232
- # Delivery — claim pending steering for a consumer (loop-safe), then mark delivered
412
+ # Delivery — claim pending steering for a consumer (loop-safe), then mark delivered.
413
+ # The claim returns broadcasts plus steering addressed to THIS consumer; a claim
414
+ # with no `consumer` sees broadcasts only. With several agents connected, the
415
+ # browser composer offers a target picker fed by live presence — always claim
416
+ # with your own label or addressed steering never reaches you.
233
417
  curl "http://localhost:4313/api/canvas/ax/delivery/pending?consumer=copilot&limit=20"
418
+
419
+ # Long-poll: `waitMs` parks the request until steering for this consumer
420
+ # arrives or the timeout elapses (capped at 120000ms) — the reactive loop for
421
+ # hosts whose model only runs while the host gives it a turn. Loop on this
422
+ # instead of tight polling.
423
+ curl "http://localhost:4313/api/canvas/ax/delivery/pending?consumer=copilot&waitMs=120000"
424
+ # Marks are PER CONSUMER for broadcasts: your mark removes the message from
425
+ # YOUR queue only — every other consumer still receives it ("all workers:
426
+ # stop" reaches the whole fleet). Addressed steers stay single-recipient
427
+ # compare-and-set; a consumer-less mark is the anonymous global ack.
234
428
  curl -X POST http://localhost:4313/api/canvas/ax/delivery/<steering-id>/mark \
235
429
  -H "Content-Type: application/json" \
236
430
  -d '{"consumer":"copilot"}'
237
431
 
432
+ # Per-agent territories (fleet orchestration): fence a worker to its lane.
433
+ # An agent may fence OTHER writers, never its own key; the human may set any.
434
+ curl -X POST http://localhost:4313/api/canvas/ax/policy \
435
+ -H "Content-Type: application/json" \
436
+ -d '{"agentScopes":{"run1:impl":{"nodeIds":["node-a"],"padding":80}}}'
437
+
238
438
  # Elicitation — request structured human input, then respond
239
439
  curl -X POST http://localhost:4313/api/canvas/ax/elicitation \
240
440
  -H "Content-Type: application/json" \
@@ -352,6 +552,15 @@ curl -N http://localhost:4313/api/workbench/events
352
552
  The browser, the CLI `watch` command, and the MCP resource notifications
353
553
  all consume this stream. Auto-reconnect with exponential backoff.
354
554
 
555
+ `agent-presence` frames carry the full presence snapshot (the same body as
556
+ `GET /api/canvas/ax/presence`) on every change, including TTL expiry — a
557
+ client never needs its own expiry timer. `agent-session-ended` carries the
558
+ session receipt (see [Agent presence](#agent-presence)).
559
+
560
+ Every frame's payload is wrapped in an envelope that sets `sessionId` (the
561
+ workbench session, used for reconnect/reload detection) and `timestamp` —
562
+ payload keys of those names are overwritten, so event bodies never use them.
563
+
355
564
  ## Polling transport (proxy-safe)
356
565
 
357
566
  Some proxies (e.g. the Amp orb portal) buffer streaming responses and only
@@ -420,8 +629,16 @@ re-mint them automatically (Finding S).
420
629
  curl -X POST http://localhost:4313/api/canvas/undo
421
630
  curl -X POST http://localhost:4313/api/canvas/redo
422
631
  curl http://localhost:4313/api/canvas/history
632
+ # → { text, entries: [{ id, timestamp, description, actor, operationType, isCurrent, isUndone }],
633
+ # top: { …the entry undo would revert next… } | null, canUndo, canRedo }
423
634
  ```
424
635
 
636
+ One shared undo stack, agent and human alike. Each entry's `actor` is `human`
637
+ for workbench writes (`x-pmx-workbench: 1`) and `agent` for everything else;
638
+ `top` is what the next undo reverts. The session panel uses this to offer
639
+ "undo this edit" on the agent's latest board write and tells the agent through
640
+ steering when the human takes it.
641
+
425
642
  ## WebView automation
426
643
 
427
644
  ```bash
package/docs/mcp.md CHANGED
@@ -39,18 +39,18 @@ its `action` to the same operation the legacy tool used, so results are identica
39
39
  |-----------|-----------------|----------|
40
40
  | `canvas_node` | `add` · `get` · `update` · `remove` | `canvas_add_node`, `canvas_get_node`, `canvas_update_node`, `canvas_remove_node`, `canvas_add_html_node` (`add` + `type:"html"`), `canvas_add_html_primitive` (`add` + `type:"html"`, `primitive:"<kind>"`), `canvas_refresh_webpage_node` (`update` + `refresh:true`) |
41
41
  | `canvas_render` | `describe-schema` · `validate` · `add-json-render` · `stream-json-render` · `add-graph` · `workboard` | `canvas_describe_schema`, `canvas_validate_spec`, `canvas_add_json_render_node`, `canvas_stream_json_render_node`, `canvas_add_graph_node` |
42
- | `canvas_edge` | `add` · `remove` | `canvas_add_edge`, `canvas_remove_edge` |
42
+ | `canvas_edge` | `add` · `update` · `remove` | `canvas_add_edge`, `canvas_update_edge`, `canvas_remove_edge` |
43
43
  | `canvas_group` | `create` · `add` · `ungroup` | `canvas_create_group`, `canvas_group_nodes`, `canvas_ungroup` |
44
44
  | `canvas_history` | `undo` · `redo` | `canvas_undo`, `canvas_redo` |
45
45
  | `canvas_view` | `arrange` · `focus` · `fit` · `clear` · `remove-annotation` | `canvas_arrange`, `canvas_focus_node`, `canvas_fit_view`, `canvas_clear`, `canvas_remove_annotation` |
46
46
  | `canvas_query` | `search` · `layout` · `validate` | `canvas_search`, `canvas_get_layout`, `canvas_validate` |
47
47
  | `canvas_webview` | `status` · `start` · `stop` · `resize` · `evaluate` | `canvas_webview_status`, `canvas_webview_start`, `canvas_webview_stop`, `canvas_resize`, `canvas_evaluate` |
48
48
  | `canvas_app` | `open-mcp-app` · `diagram` · `build-artifact` | `canvas_open_mcp_app`, `canvas_add_diagram`, `canvas_build_web_artifact` |
49
- | `canvas_ax_state` | `get` · `set-focus` · `set-policy` · `report-capability` | `canvas_get_ax`, `canvas_set_ax_focus`, `canvas_set_ax_policy`, `canvas_report_host_capability` |
49
+ | `canvas_ax_state` | `get` · `set-focus` · `set-policy` · `report-capability` · `presence` · `set-presence` | `canvas_get_ax`, `canvas_set_ax_focus`, `canvas_set_ax_policy`, `canvas_report_host_capability` |
50
50
  | `canvas_ax_work` | `add` · `update` · `annotate` | `canvas_add_work_item`, `canvas_update_work_item`, `canvas_add_review_annotation` |
51
51
  | `canvas_ax_gate` | `request` · `resolve` · `await` × kind `approval` \| `elicitation` \| `mode` | `canvas_request_approval`, `canvas_resolve_approval`, `canvas_await_approval`, `canvas_request_elicitation`, `canvas_respond_elicitation`, `canvas_await_elicitation`, `canvas_request_mode`, `canvas_resolve_mode`, `canvas_await_mode` (9 → 1) |
52
52
  | `canvas_ax_timeline` | `read` · `record-event` · `add-evidence` · `send-steering` | `canvas_get_ax_timeline`, `canvas_record_ax_event`, `canvas_add_evidence`, `canvas_send_steering` |
53
- | `canvas_ax_delivery` | `claim` · `mark` | `canvas_claim_ax_delivery`, `canvas_mark_ax_delivery` |
53
+ | `canvas_ax_delivery` | `claim` (long-polls with `timeoutMs`) · `mark` | `canvas_claim_ax_delivery`, `canvas_mark_ax_delivery` |
54
54
  | `canvas_intent` | `signal` · `update` · `clear` | _(new — Ghost Cursor of Intent; no legacy standalone tool)_ |
55
55
  | `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
56
 
@@ -135,6 +135,7 @@ migrating an older integration.
135
135
  | `canvas_stream_json_render_node` | `canvas_render { action: "stream-json-render" }` |
136
136
  | `canvas_add_graph_node` | `canvas_render { action: "add-graph" }` |
137
137
  | `canvas_add_edge` | `canvas_edge { action: "add" }` |
138
+ | `canvas_update_edge` | `canvas_edge { action: "update" }` |
138
139
  | `canvas_remove_edge` | `canvas_edge { action: "remove" }` |
139
140
  | `canvas_create_group` | `canvas_group { action: "create" }` |
140
141
  | `canvas_group_nodes` | `canvas_group { action: "add" }` |
@@ -277,8 +278,8 @@ keep annotation context compact: agents see annotation counts, bounds, and targe
277
278
  summaries such as the node or empty canvas region that was marked, but not the
278
279
  raw stroke geometry or visual shape.
279
280
 
280
- Annotations are a browser-visible markup layer. Use the pen toolbar button to
281
- draw and the eraser toolbar button to remove an annotation again; agents can also
281
+ Annotations are a browser-visible markup layer. Humans draw with the rail's
282
+ Annotate popover (pen / text / eraser — the eraser removes a mark); agents can also
282
283
  remove a known annotation ID with `canvas_view { action: "remove-annotation", id }`.
283
284
 
284
285
  Use WebView automation when an agent needs to actually see annotations as drawn.