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
@@ -5,6 +5,7 @@ import { createServer as createNetServer } from "node:net";
5
5
  import { dirname, resolve } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { CanvasError, createCanvas, joinSession } from "@github/copilot-sdk/extension";
8
+ import { createSteeringDeliveryPump } from "./steering-delivery.mjs";
8
9
 
9
10
  const EXTENSION_DIR = dirname(fileURLToPath(import.meta.url));
10
11
  const PROJECT_ROOT = resolve(EXTENSION_DIR, "../../..");
@@ -12,8 +13,10 @@ const DEFAULT_PORT = 4313;
12
13
  const MAX_AX_CONTEXT_CHARS = 16_000;
13
14
  const MANAGED_START_TIMEOUT_MS = 10_000;
14
15
  const HEALTH_TIMEOUT_MS = 500;
16
+ const STEERING_WAIT_MS = 30_000;
15
17
 
16
18
  let copilotSession;
19
+ let steeringDeliveryPump;
17
20
  let managedProcess = null;
18
21
  let managedBaseUrl = null;
19
22
  let managedWorkspaceRoot = null;
@@ -467,12 +470,97 @@ async function postAxRecord(ctx, path, payload) {
467
470
  }
468
471
  }
469
472
 
473
+ // Agent presence (rail-chrome-v2): the Copilot session attaches itself so the
474
+ // canvas shows its cursor + phase chip, and the server attributes the agent's
475
+ // MCP/HTTP writes to this session. Best-effort and never blocking the host.
476
+ async function postPresence(workspaceRoot, payload, options = {}) {
477
+ try {
478
+ const resolved = await resolvePmxServer(
479
+ { input: {}, session: { workingDirectory: workspaceRoot } },
480
+ { autoStart: false },
481
+ );
482
+ if (!resolved.ok || !resolved.baseUrl) return false;
483
+ await fetchJson(resolved.baseUrl, "/api/canvas/ax/presence", {
484
+ method: "POST",
485
+ headers: { "Content-Type": "application/json" },
486
+ body: JSON.stringify({ source: "copilot", label: "GitHub Copilot", ...payload }),
487
+ timeoutMs: options.timeoutMs ?? 1_500,
488
+ });
489
+ return true;
490
+ } catch {
491
+ return false;
492
+ }
493
+ }
494
+
495
+ async function getCopilotPresence() {
496
+ try {
497
+ const resolved = await resolvePmxServer(
498
+ { input: {}, session: { workingDirectory: PROJECT_ROOT } },
499
+ { autoStart: false },
500
+ );
501
+ if (!resolved.ok || !resolved.baseUrl) return null;
502
+ const snapshot = await fetchJson(resolved.baseUrl, "/api/canvas/ax/presence", { timeoutMs: 1_500 });
503
+ if (!Array.isArray(snapshot.presences)) return null;
504
+ return (
505
+ snapshot.presences.find(
506
+ (presence) =>
507
+ presence?.sessionId === "copilot" ||
508
+ (presence?.source === "copilot" && presence?.agentId == null),
509
+ ) ?? null
510
+ );
511
+ } catch {
512
+ return null;
513
+ }
514
+ }
515
+
470
516
  // Fire-and-forget mirror of a copilot-originated action onto the AX timeline.
471
517
  // Never throws — adapter UX must not depend on the canvas being reachable.
472
518
  function recordCopilotSteering(ctx, message) {
473
519
  void postAxRecord(ctx, "/api/canvas/ax/steer", { message }).catch(() => {});
474
520
  }
475
521
 
522
+ async function claimCopilotSteering(consumer) {
523
+ const workspaceRoot = PROJECT_ROOT;
524
+ const resolved = await resolvePmxServer(
525
+ { input: {}, session: { workingDirectory: workspaceRoot } },
526
+ { autoStart: false },
527
+ );
528
+ if (!resolved.ok || !resolved.baseUrl) {
529
+ throw new Error(resolved.error ?? "PMX Canvas server is unavailable.");
530
+ }
531
+ const delivery = await fetchJson(
532
+ resolved.baseUrl,
533
+ `/api/canvas/ax/delivery/pending?consumer=${encodeURIComponent(consumer)}&limit=1&order=oldest&waitMs=${STEERING_WAIT_MS}`,
534
+ { timeoutMs: STEERING_WAIT_MS + 2_000 },
535
+ );
536
+ return Array.isArray(delivery.pending) ? delivery.pending : [];
537
+ }
538
+
539
+ async function markCopilotSteering(id, consumer) {
540
+ const workspaceRoot = PROJECT_ROOT;
541
+ const resolved = await resolvePmxServer(
542
+ { input: {}, session: { workingDirectory: workspaceRoot } },
543
+ { autoStart: false },
544
+ );
545
+ if (!resolved.ok || !resolved.baseUrl) {
546
+ throw new Error(resolved.error ?? "PMX Canvas server is unavailable.");
547
+ }
548
+ await fetchJson(resolved.baseUrl, `/api/canvas/ax/delivery/${encodeURIComponent(id)}/mark`, {
549
+ method: "POST",
550
+ headers: { "Content-Type": "application/json" },
551
+ body: JSON.stringify({ consumer }),
552
+ timeoutMs: 2_000,
553
+ });
554
+ }
555
+
556
+ let lastSteeringDeliveryError = null;
557
+ function reportSteeringDeliveryError(error) {
558
+ const message = error instanceof Error ? error.message : String(error);
559
+ if (message === lastSteeringDeliveryError) return;
560
+ lastSteeringDeliveryError = message;
561
+ process.stderr.write(`[pmx-canvas] Copilot steering delivery retrying: ${message}\n`);
562
+ }
563
+
476
564
  async function getAxTimeline(ctx, limit) {
477
565
  const resolved = await resolvePmxServer(ctx, { autoStart: false });
478
566
  if (!resolved.ok || !resolved.baseUrl) return { ok: false, error: resolved.error };
@@ -766,6 +854,10 @@ copilotSession = await joinSession({
766
854
  hooks: {
767
855
  onUserPromptSubmitted: async (input) => {
768
856
  const workspaceRoot = resolve(input?.workingDirectory ?? copilotSession?.workspacePath ?? PROJECT_ROOT);
857
+ // A submitted prompt means the agent is about to think: attach (idempotent)
858
+ // and flip the phase. The agent's subsequent canvas writes reach the server
859
+ // as MCP/HTTP tool calls and are attributed to this session.
860
+ void postPresence(workspaceRoot, { attached: true, phase: "thinking", detail: null });
769
861
  const resolved = await resolvePmxServer({ input: {}, session: { workingDirectory: workspaceRoot } }, { autoStart: false });
770
862
  if (!resolved.ok || !resolved.baseUrl) return undefined;
771
863
  const context = await getAxContext(resolved.baseUrl, workspaceRoot, {});
@@ -775,11 +867,68 @@ copilotSession = await joinSession({
775
867
  },
776
868
  });
777
869
 
778
- process.once("SIGTERM", () => {
779
- stopManagedServer();
780
- process.exit(0);
870
+ steeringDeliveryPump = createSteeringDeliveryPump({
871
+ consumer: "copilot",
872
+ claim: claimCopilotSteering,
873
+ send: async (message) => await copilotSession.send({ prompt: message }),
874
+ mark: markCopilotSteering,
875
+ getPresence: getCopilotPresence,
876
+ setPresence: async (patch) => await postPresence(PROJECT_ROOT, patch),
877
+ shouldSend: (() => {
878
+ const startedAt = Date.now();
879
+ return (steering) => Date.parse(steering.createdAt) >= startedAt;
880
+ })(),
881
+ pause: delay,
882
+ onError: reportSteeringDeliveryError,
781
883
  });
782
- process.once("SIGINT", () => {
884
+ void steeringDeliveryPump.start();
885
+
886
+ // Attach the session as soon as the host joins (the canvas may not be running
887
+ // yet — the prompt hook above re-attaches lazily in that case).
888
+ void postPresence(resolve(copilotSession?.workspacePath ?? PROJECT_ROOT), { attached: true });
889
+
890
+ // Real context window (rail-chrome-v2): Copilot emits `session.usage_info`
891
+ // with the live token count and the model's window after each turn. Report it
892
+ // on the presence so the board's top-bar meter shows the agent's ACTUAL window
893
+ // instead of the pinned-context estimate. Root agent only — sub-agent events
894
+ // carry an `agentId` and have their own windows. Coalesced to one POST per
895
+ // 500ms: a turn can emit several usage events back to back.
896
+ let usageFlush = null;
897
+ let pendingUsage = null;
898
+ function reportContextUsage(event) {
899
+ const data = event?.data;
900
+ if (event?.agentId || !data || typeof data.currentTokens !== "number" || typeof data.tokenLimit !== "number" || data.tokenLimit <= 0) {
901
+ return;
902
+ }
903
+ pendingUsage = { used: Math.max(0, Math.round(data.currentTokens)), total: Math.round(data.tokenLimit) };
904
+ if (usageFlush) return;
905
+ usageFlush = setTimeout(() => {
906
+ usageFlush = null;
907
+ const usage = pendingUsage;
908
+ pendingUsage = null;
909
+ if (usage) void postPresence(resolve(copilotSession?.workspacePath ?? PROJECT_ROOT), { contextUsage: usage });
910
+ }, 500);
911
+ }
912
+ if (typeof copilotSession?.on === "function") {
913
+ try {
914
+ copilotSession.on("session.usage_info", reportContextUsage);
915
+ } catch {
916
+ // An SDK without the event keeps the estimate — the meter says so.
917
+ }
918
+ }
919
+
920
+ async function shutdown() {
921
+ steeringDeliveryPump?.stop();
922
+ // Detach the presence first so the canvas returns to the quiet board the
923
+ // moment the host goes away, then stop any managed server. The whole
924
+ // detach is capped at 800ms (server probing + the POST) so a host with a
925
+ // short SIGTERM grace window can never be held past it.
926
+ await Promise.race([
927
+ postPresence(resolve(copilotSession?.workspacePath ?? PROJECT_ROOT), { attached: false }, { timeoutMs: 500 }),
928
+ delay(800),
929
+ ]);
783
930
  stopManagedServer();
784
931
  process.exit(0);
785
- });
932
+ }
933
+ process.once("SIGTERM", () => void shutdown());
934
+ process.once("SIGINT", () => void shutdown());
@@ -0,0 +1,79 @@
1
+ export function createSteeringDeliveryPump({
2
+ consumer,
3
+ claim,
4
+ send,
5
+ mark,
6
+ shouldSend = () => true,
7
+ getPresence,
8
+ setPresence,
9
+ pause,
10
+ onError,
11
+ retryMs = 1_000,
12
+ }) {
13
+ let stopped = false;
14
+ let loopPromise = null;
15
+ const awaitingMark = [];
16
+
17
+ async function flushMarks() {
18
+ while (awaitingMark.length > 0) {
19
+ const id = awaitingMark[0];
20
+ await mark(id, consumer);
21
+ awaitingMark.shift();
22
+ }
23
+ }
24
+
25
+ async function sendWithOwnedPhase(message) {
26
+ const detail = `steer: ${message.slice(0, 60)}`;
27
+ const presence = await getPresence?.();
28
+ const ownsPhase =
29
+ presence?.phase === "idle" &&
30
+ typeof setPresence === "function" &&
31
+ (await setPresence({ phase: "tooling", detail })) !== false;
32
+
33
+ try {
34
+ await send(message);
35
+ } finally {
36
+ if (ownsPhase && typeof getPresence === "function" && typeof setPresence === "function") {
37
+ const current = await getPresence();
38
+ if (current?.phase === "tooling" && current.detail === detail) {
39
+ await setPresence({ phase: "idle", detail: null });
40
+ }
41
+ }
42
+ }
43
+ }
44
+
45
+ async function runOnce() {
46
+ await flushMarks();
47
+ const pending = await claim(consumer);
48
+ for (const steering of pending) {
49
+ if (stopped) break;
50
+ if (shouldSend(steering)) await sendWithOwnedPhase(steering.message);
51
+ awaitingMark.push(steering.id);
52
+ await flushMarks();
53
+ }
54
+ return pending.length;
55
+ }
56
+
57
+ async function run() {
58
+ while (!stopped) {
59
+ try {
60
+ const claimed = await runOnce();
61
+ if (claimed === 0 && !stopped) await pause(retryMs);
62
+ } catch (error) {
63
+ onError(error);
64
+ if (!stopped) await pause(retryMs);
65
+ }
66
+ }
67
+ }
68
+
69
+ return {
70
+ runOnce,
71
+ start() {
72
+ if (!loopPromise) loopPromise = run();
73
+ return loopPromise;
74
+ },
75
+ stop() {
76
+ stopped = true;
77
+ },
78
+ };
79
+ }
package/CHANGELOG.md CHANGED
@@ -3,6 +3,160 @@
3
3
  All notable changes to `pmx-canvas` are documented here. This project follows
4
4
  [Semantic Versioning](https://semver.org/).
5
5
 
6
+ ## [0.5.0] - 2026-08-28
7
+
8
+ The multi-agent workbench release. The canvas becomes a live, shared surface
9
+ where several agents and a human can work at once — everyone can see who is
10
+ writing where, steer any connected agent, and take the board back when they
11
+ need it.
12
+
13
+
14
+ ### Breaking
15
+
16
+ - Node docking is gone. `dockPosition` is removed from node payloads, from
17
+ `canvas_update_node`'s input, and from the SDK's `CanvasNodeState` and node
18
+ updates; the docked HUD pills and the first-run docked status/context widgets
19
+ are retired. Nodes always live on the canvas now — use `position` and
20
+ `pinned`. Old boards load fine and previously docked nodes appear as normal
21
+ nodes. The validator's advisory `hiddenEdgeEndpoints` field went with them.
22
+ - Ungrouping now dissolves the group in one undo step: children are released
23
+ (into the enclosing group when nested) and the frame is removed — it no
24
+ longer survives to be deleted separately. A missing group now returns 404,
25
+ and an empty group dissolves instead of erroring. To release children while
26
+ keeping the frame, patch the group with `childIds: []`.
27
+ - Approval gates no longer wait forever: an unanswered gate auto-resolves to
28
+ the new `held` status (a non-approval — the action does not proceed) after
29
+ its TTL, default 5 minutes via `PMX_CANVAS_GATE_TTL_MS`; reopen it with
30
+ `POST /api/canvas/ax/approval/:id/reopen`. Consumers switching on gate status
31
+ must handle `held`.
32
+ - Broadcast steering is delivered per consumer: marking a broadcast with your
33
+ `consumer` clears it from your queue only, and every other consumer still
34
+ receives it (previously the first mark removed it for everyone — fleets must
35
+ no longer rely on that for dedupe). Unclaimed broadcasts leave the pending
36
+ queue after 15 minutes; addressed steers still wait for their consumer
37
+ indefinitely.
38
+ ### Highlights
39
+
40
+ - See who is on the board in real time — every writing agent paints a named,
41
+ colored cursor and a phase, and a top-bar indicator shows external steering
42
+ even for agents that aren't attached.
43
+ - Steer any connected agent from the composer: address one by name or broadcast
44
+ to the whole fleet, and `pmx-canvas pump` turns any CLI agent into a steerable
45
+ board citizen.
46
+ - Human-wins editing: grab a node and agents are locked out of it until you let
47
+ go, and you can undo an agent's last edit straight from the session panel.
48
+ - A redesigned board — tool rail, top bar, session panel, command bar, minimap,
49
+ History drawer, groups, and an expanded node view.
50
+ - Unattended approval gates now resolve themselves safely instead of hanging
51
+ forever, and an orchestrator can fence each agent to its own lane.
52
+
53
+ ### Added
54
+
55
+ - Agent presence: a live view of every agent writing to the board — identity
56
+ color, phase (thinking / working / waiting), a cursor that tracks where it is
57
+ working, and whether a session is attached. Read it at
58
+ `GET /api/canvas/ax/presence` or set it via `canvas_ax_state`
59
+ (`presence` / `set-presence`).
60
+ - Human presence and a user-wins edit lock: your cursor is shared with agents,
61
+ and while you are holding a node an agent write to it is refused until you
62
+ release it.
63
+ - Addressed steering: the composer picks which connected agent to steer, and a
64
+ broadcast reaches every consumer in the fleet; the steering timeline names the
65
+ agent behind each message.
66
+ - `pmx-canvas pump` — a new CLI command that makes any CLI agent (Codex, Amp, a
67
+ script) steerable: it long-polls for steers and runs your command once per
68
+ message. Supports `--consumer`, `--exec`, `--parent`, `--once`, and
69
+ `--backlog`. On Windows the command runs through `cmd.exe` and receives the
70
+ steer on stdin; the `{message}` / `{id}` placeholders are refused there,
71
+ because `cmd.exe` re-parses expanded variables and a steer could inject
72
+ commands.
73
+ - Edges can be edited in place — update an edge's label, type, style, or
74
+ animation without deleting and redrawing it (`PATCH /api/canvas/edge/:id`,
75
+ `canvas_edge { action: "update" }`).
76
+ - File nodes can be repointed at another file with a patch — content is re-read
77
+ and the watcher rewired, keeping the node's edges, pins, and position
78
+ (`PATCH /api/canvas/node/:id` with `path`).
79
+ - Shared undo of agent edits: when an agent's change tops the history stack, a
80
+ fixed "Undo" row appears in the session panel and taking it back posts a
81
+ steering note.
82
+ - Unattended-approval policy: an approval gate nobody answers within its TTL
83
+ resolves to `held` (the action does not proceed) instead of hanging; reopen it
84
+ with a fresh clock from the session panel or
85
+ `POST /api/canvas/ax/approval/:id/reopen`.
86
+ - Scope fence: an orchestrator can fence each agent to a set of nodes, and
87
+ agent writes outside the fence are refused (403) — the human is never fenced.
88
+ - Session receipts that say why a session ended (by you, by the agent, or idle
89
+ timeout), and a History drawer that stops hoarding duplicate snapshots.
90
+ - A redesigned board: tool rail with hover labels and shortcuts, top bar,
91
+ command bar, session panel, minimap v2, groups v2, an empty state, a palette,
92
+ a selection bar, a connection banner, and an expanded node view.
93
+ - An honest context meter: it shows the agent's real context window when the
94
+ host reports usage, and otherwise a "Pins" estimate of what your pinned
95
+ context would cost.
96
+ - In-canvas prompt entry (Shift+F / W / I everywhere), right-click Delete and
97
+ "Remove from ‹frame›" on member cards, and a work-item Assistant filter.
98
+ - New environment variables: `PMX_CANVAS_AGENT_SOURCE` (name the agent behind a
99
+ transport), `PMX_CANVAS_CONTEXT_BUDGET_TOKENS` (the Pins-meter ceiling,
100
+ default 32000), and `PMX_CANVAS_GATE_TTL_MS` (approval-gate TTL, default 5 min).
101
+
102
+ ### Changed
103
+
104
+ - Create and patch now reject the dead `path` (on non-image nodes) and `src`
105
+ (anywhere) fields with a clear 400 instead of silently dropping them and
106
+ leaving an empty node — the canonical field is `content`. The image-node
107
+ `path` alias still works.
108
+ - Approval gates report a new `held` outcome (from the unattended-approval
109
+ policy); consumers reading gate status should tolerate it alongside
110
+ `approved` / `rejected`.
111
+ - Steering delivery claims can long-poll (`timeoutMs`) so a host that can't be
112
+ woken parks on the claim, and delivery is per-consumer so a broadcast reaches
113
+ the whole fleet without double-delivering.
114
+ - Auto-arrange is cluster-aware — each on-board neighborhood packs in place
115
+ instead of being interleaved with the others.
116
+ - Saving a snapshot on an unchanged board now returns the existing snapshot (marked `reused`) instead of creating a duplicate — a checkpoint-by-name on an unchanged board keeps the earlier snapshot's name.
117
+ - Cancelled work items no longer read as vetoed backlog in the timeline.
118
+ - Keyboard shortcuts are platform-aware on Windows keyboards (Ctrl+Y redo,
119
+ AltGr guard, platform-correct labels).
120
+ - Humans now draw annotations from the rail's Annotate popover (pen / text /
121
+ eraser) rather than separate toolbar buttons.
122
+
123
+ ### Fixed
124
+
125
+ - Node headlines stay crisp at high zoom instead of blurring.
126
+ - HTML and mermaid surfaces render in embedded hosts that block sub-frame
127
+ requests or use `srcdoc` (Copilot, Codex, Claude panels).
128
+ - Open webpage / file / image work on every surface — no flow depends on
129
+ `window.prompt`, which is a silent no-op in embedded browser panes.
130
+ - Edges no longer draw as degenerate straight lines — side-tangent bezier
131
+ routing gives them a real bend, and the draw preview no longer jumps.
132
+ - A focus glow no longer wraps the whole board when an attention region spans
133
+ everything.
134
+ - Card checkboxes survive a human-speed press, expanded-view checkboxes tick,
135
+ and closing a document with Esc no longer wipes it.
136
+ - Stale browser tabs self-heal on a dev rebuild instead of faithfully
137
+ reproducing already-fixed bugs.
138
+ - Timeline filter hints and selection-bar tooltips render in-flow as styled
139
+ hints (native title tooltips never show in embedded panes), and the filter
140
+ chips fit one row.
141
+ - Presence overlay expiry always emits, held gates no longer pin the session
142
+ panel open, and Copilot presence is preserved during steering.
143
+
144
+ ## [0.4.8] - 2026-08-20
145
+
146
+ ### Fixed
147
+
148
+ - Saving now writes only what actually changed instead of rewriting the whole
149
+ board on every save, so the write cost of dragging one node no longer grows
150
+ with board size ([#22](https://github.com/pskoett/pmx-canvas/issues/22)).
151
+ - Saves are now fsynced (`synchronous=FULL`), so an abrupt host crash no longer
152
+ loses the most recent saves — affordable now that saves are incremental.
153
+ - Orphaned large-content blobs are removed on shutdown — only when nothing
154
+ else has the canvas open, so a second live process can never lose content —
155
+ and `canvas.db` no longer grows without bound as node content is edited.
156
+ - MCP-app tiles now recover from black tiles in embedded hosts that never
157
+ report the page as visible (e.g. the GitHub Copilot panel), and recovery
158
+ logs no longer claim an unverified paint as a success there.
159
+
6
160
  ## [0.4.7] - 2026-08-09
7
161
 
8
162
  ### Added
@@ -3105,6 +3259,8 @@ otherwise have to discover by trial and error.
3105
3259
  - Regression coverage for snapshot flat-`id` aliases on both MCP and
3106
3260
  HTTP surfaces, plus async / top-level-`await` WebView script bodies.
3107
3261
 
3262
+ [0.5.0]: https://github.com/pskoett/pmx-canvas/compare/v0.4.8...v0.5.0
3263
+ [0.4.8]: https://github.com/pskoett/pmx-canvas/compare/v0.4.7...v0.4.8
3108
3264
  [0.4.7]: https://github.com/pskoett/pmx-canvas/releases/tag/v0.4.7
3109
3265
  [0.4.6]: https://github.com/pskoett/pmx-canvas/releases/tag/v0.4.6
3110
3266
  [0.4.5]: https://github.com/pskoett/pmx-canvas/releases/tag/v0.4.5
package/Readme.md CHANGED
@@ -9,9 +9,10 @@ project. Pin what matters and the agent reads your spatial curation as
9
9
  structured context.
10
10
 
11
11
  <p align="center">
12
- <img src="docs/screenshots/welcome-dark.png" alt="Empty canvas — dark theme" width="49%" />
13
- <img src="docs/screenshots/welcome-light.png" alt="Empty canvas — light theme" width="49%" />
12
+ <img src="docs/screenshots/workbench-live-dark.png" alt="Live multi-agent workbench — dark theme: two agents on the board, review cards, an architecture diagram, and the writers pill" width="49%" />
13
+ <img src="docs/screenshots/workbench-live-light.png" alt="Live multi-agent workbench — light theme" width="49%" />
14
14
  </p>
15
+ <p align="center"><sub>A real working board: two agents attached (the writers pill, top right), review cards they co-wrote, and a mermaid architecture diagram — dark and light themes.</sub></p>
15
16
 
16
17
  <p align="center">
17
18
  <img src="docs/screenshots/demo-workbench-dark.png" alt="Structured workbench demo — dark theme" width="49%" />
@@ -28,7 +29,11 @@ communication — proximity means relatedness, pinning means *focus here*.
28
29
 
29
30
  ### 01 / Curate
30
31
 
31
- Drag, group, arrange, and **pin** nodes spatially. Curation is the channel
32
+ Drag, group, arrange, and **pin** nodes spatially. Connect nodes with typed
33
+ edges — flow, depends-on, relation, references — drawn as smooth curves that
34
+ leave each card perpendicular to its border and bend toward their target.
35
+ Edges stay editable in place: right-click one to relabel, retype, restyle, or
36
+ delete it, or click it and press Delete. Curation is the channel
32
37
  from human intent to agent context — the agent reads `canvas://pinned-context`
33
38
  and `canvas://spatial-context` (proximity clusters, reading order, pinned
34
39
  neighborhoods) and uses your layout to ground its next action.
@@ -38,7 +43,8 @@ neighborhoods) and uses your layout to ground its next action.
38
43
  Files, web pages, screenshots, structured panels, charts, diagrams, embedded
39
44
  MCP Apps, and bundled web artifacts all live on the same surface. A file node
40
45
  follows the file on disk as the agent edits it — source with line numbers, a
41
- CSV or TSV as a real table, a PDF inline. Anything else binary shows its name
46
+ CSV or TSV as a real table, a PDF inline — and can be re-pointed at another
47
+ file (patch its `path`) without losing its edges, pins, or position. Anything else binary shows its name
42
48
  and size rather than decoded gibberish, and a very large text file renders
43
49
  truncated with a note saying so. Code review has its own node: a unified
44
50
  diff with per-file headers and colored added and removed lines. Diagrams can
@@ -91,11 +97,16 @@ canvas into a shared workspace between you and the agent:
91
97
  commands (`pmx.plan`, `pmx.review`, …), and read a tool/prompt policy.
92
98
  - **Host capability** — adapters report what the host can do, for diagnostics.
93
99
 
94
- Recent additions in this layer: agent-native nodes can now complete the full
95
- AX loop; agent tool/session activity can flow back into the board as work,
96
- evidence, and findings; approval, input, and mode gates can wait for a human
97
- decision instead of polling; and AX-enabled HTML/web-artifact or MCP app
98
- controls can show an in-surface confirmation after an action is accepted.
100
+ Recent additions in this layer: a **composer** steers directly from the board
101
+ (with a target picker when several agents are connected); the **session panel**
102
+ shows work items, approval gates, and a filterable timeline where every row
103
+ names its writer; a shared **undo stack** knows whether the human or the agent
104
+ made each edit, and undoing an agent's edit tells the agent as steering; a
105
+ **scope fence** confines an agent's writes to selected nodes; and grabbing a
106
+ node in the browser locks it against agent edits while you hold it. Dialogs,
107
+ prompts, and tooltips are all in-canvas — every flow works identically in
108
+ embedded browser panes (Copilot, Codex, Claude Code desktop) where native
109
+ browser UI doesn't exist.
99
110
 
100
111
  Canvas-bound state (focus, work items, approvals, review annotations,
101
112
  elicitations, mode requests, policy) rides canvas snapshots and restore; the
@@ -105,7 +116,40 @@ snapshot. Every primitive is reachable from MCP, the HTTP API, the SDK, and
105
116
  GitHub Copilot app) map onto the same neutral surfaces without making PMX Canvas
106
117
  vendor-specific.
107
118
 
108
- ### 05 / Save
119
+ ### 05 / Run several agents on one board
120
+
121
+ The canvas is built for more than one writer. Every connected agent gets a
122
+ stable **identity color** carried by its cursor, its top-bar session chip, and
123
+ its minimap dot — you always see who is where. Workers roll up under their
124
+ orchestrator's chip (`+2 workers`), each session chip has its own End button,
125
+ and an idle board with no writers stays byte-clean.
126
+
127
+ <p align="center">
128
+ <img src="docs/screenshots/multi-agent-live.png" alt="Two agent sessions and three rolled-up workers live on one board — identity-colored cursors on the cards, per-chip End buttons, the attributed session timeline, and the all-agents composer" width="100%" />
129
+ </p>
130
+ <p align="center"><sub>Two agent sessions (Claude Code +3 workers, GitHub Copilot) and their cursors live on one board, with the writer-attributed timeline and the all-agents composer.</sub></p>
131
+
132
+ Steering is **addressed and reliable**: the composer's picker lists every
133
+ connected writer with live pump health (`· polling`, `· 2 queued`), an
134
+ addressed steer waits in that agent's delivery queue until the agent claims
135
+ and marks it, and broadcasts age out instead of greeting every future
136
+ consumer. Each host wakes its own way — the GitHub Copilot extension pumps
137
+ steers into real Copilot turns, Codex picks them up through its app-native
138
+ heartbeat, and any CLI agent becomes reactive with one command:
139
+
140
+ ```bash
141
+ pmx-canvas pump --consumer amp --exec 'amp -x {message}'
142
+ ```
143
+
144
+ A failed hand-off is never silently swallowed: the steer stays pending and the
145
+ pump exits non-zero. Presence is honest too — "thinking" settles to idle after
146
+ silence, tool calls never clobber an explicit phase, and an attached writer
147
+ that has never made a spatial write still parks its cursor at its last
148
+ activity so it cannot be invisible. Agents coordinate with each other over the
149
+ same steering queue (agent-to-agent hand-offs, reviews, and territory splits),
150
+ and the `pmx-canvas-orchestration` skill documents the choreography.
151
+
152
+ ### 06 / Save
109
153
 
110
154
  Spatial state auto-saves to `.pmx-canvas/canvas.db` (debounced ~500 ms) —
111
155
  git-committable, shareable across machines, and survives both browser
@@ -114,7 +158,7 @@ undo/redo, and an auto-detected code graph (JS/TS, Python, Go, Rust) make
114
158
  the canvas durable rather than throwaway. Stop the server before committing
115
159
  the DB so SQLite WAL data is checkpointed into the file.
116
160
 
117
- ### 06 / Any agent
161
+ ### 07 / Any agent
118
162
 
119
163
  Harness-agnostic. Drive the canvas from [MCP](docs/mcp.md) (22 tools,
120
164
  14 resources, change notifications), the [CLI](docs/cli.md), the
@@ -124,7 +168,7 @@ Claude Code, the GitHub Copilot app and CLI, Codex, Amp, Cursor, Windsurf, or
124
168
  any agent that can spawn an MCP stdio server, call a CLI, or hit an HTTP
125
169
  endpoint.
126
170
 
127
- ### 07 / Native app adapters
171
+ ### 08 / Native app adapters
128
172
 
129
173
  PMX Canvas doesn't just run in a browser tab — it embeds **natively inside the
130
174
  agent apps you already use**, as a thin adapter layer over the same neutral AX
@@ -153,11 +197,11 @@ surfaces (the core never imports a host SDK).
153
197
  - **GitHub Copilot app** — a committed project canvas extension
154
198
  (`.github/extensions/pmx-canvas/`) opens the live PMX workbench in a native
155
199
  Copilot panel (light-themed by default to match the app, via the `?theme=`
156
- session override), injects pinned/focused AX context on prompt submission, and
157
- exposes actions for focus, session steering, work items, approval gates,
158
- review annotations, the AX timeline, and host-capability reporting. Install it
159
- into any repo with `pmx-canvas copilot install-extension` (`--dry-run` to
160
- preview).
200
+ session override), wakes Copilot when the board sends a steer, injects
201
+ pinned/focused AX context on prompt submission, and exposes actions for focus,
202
+ work items, approval gates, review annotations, the AX timeline, and
203
+ host-capability reporting. Install it into any repo with
204
+ `pmx-canvas copilot install-extension` (`--dry-run` to preview).
161
205
  - **Codex app** — native through the Codex in-app Browser (opened to
162
206
  `/workbench`) plus the PMX MCP server: agents read `canvas://ax-context` /
163
207
  `canvas_ax_state { action: "get" }` and label Codex-originated focus with
@@ -222,9 +266,13 @@ bunx pmx-canvas --mcp # Run as MCP server (stdio)
222
266
  bunx pmx-canvas --help # All commands
223
267
  ```
224
268
 
225
- Themes are a toolbar picker away too, and any one panel can override the
269
+ Themes are a tool-rail picker away too, and any one panel can override the
226
270
  shared theme for itself with `?theme=<name|auto>`.
227
271
 
272
+ Press `?` on the board for the full keyboard map. Shortcuts follow the
273
+ platform — `⌘` on a Mac, `Ctrl` on Windows and Linux, where `Ctrl+Y` also
274
+ redoes and `Backspace`/`Delete` both delete the selection.
275
+
228
276
  The canvas opens at `http://localhost:4313`. Try `--demo` first — it seeds a
229
277
  showcase board with every node type on it: notes and status, files, a diff, a
230
278
  CSV table, charts of every kind, mermaid diagrams, the HTML primitives,
@@ -307,7 +355,7 @@ services:
307
355
  ```bash
308
356
  # .agents/setup — install the CLI (pin the exact version: a fresh orb running
309
357
  # @latest can silently pick up a newer release than the one you validated)
310
- npm install -g pmx-canvas@0.4.7
358
+ npm install -g pmx-canvas@0.5.0
311
359
  ```
312
360
 
313
361
  The server binds the portal-assigned `$PORT` automatically (gated on the
@@ -371,10 +419,11 @@ the agent can read `canvas://skills` and pull in companion skills
371
419
  ## Scope
372
420
 
373
421
  - **Single-machine, today.** One canvas per `bunx pmx-canvas` instance, on
374
- one machine. No built-in multi-user auth or presence — collaboration means
375
- human ↔ agent on the same machine, plus any other browser tab/agent
376
- pointed at the same `localhost:4313`. To share across machines, commit
377
- `.pmx-canvas/canvas.db`.
422
+ one machine. No built-in multi-user auth — collaboration means humans and
423
+ agents on the same machine: every browser tab and every connected agent
424
+ pointed at the same `localhost:4313` shares one live board, with per-writer
425
+ presence (cursors, chips, minimap dots) for all of them. To share across
426
+ machines, commit `.pmx-canvas/canvas.db`.
378
427
  - **What leaves your machine.** The core canvas runs entirely on
379
428
  `localhost`. Network egress only happens for explicit, opt-in flows:
380
429
  `webpage` nodes and remote-URL `image` nodes fetch the address you give