pmx-canvas 0.6.5 → 0.7.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 (169) hide show
  1. package/.github/extensions/pmx-canvas/extension.mjs +67 -3
  2. package/CHANGELOG.md +54 -0
  3. package/Readme.md +29 -5
  4. package/dist/canvas/global.css +440 -24
  5. package/dist/canvas/index.js +90 -75
  6. package/dist/types/cli/agent.d.ts +3 -0
  7. package/dist/types/cli/commands/backup.d.ts +1 -0
  8. package/dist/types/cli/commands/boards.d.ts +1 -0
  9. package/dist/types/cli/commands/export.d.ts +1 -0
  10. package/dist/types/client/canvas/BoardCopyDialog.d.ts +5 -0
  11. package/dist/types/client/canvas/BoardFolderDialog.d.ts +7 -0
  12. package/dist/types/client/canvas/BoardLinkDialog.d.ts +3 -0
  13. package/dist/types/client/canvas/BoardSwitcher.d.ts +8 -0
  14. package/dist/types/client/canvas/ExportDialog.d.ts +7 -0
  15. package/dist/types/client/canvas/HomeView.d.ts +9 -0
  16. package/dist/types/client/nodes/AttachmentNode.d.ts +6 -0
  17. package/dist/types/client/nodes/BoardNode.d.ts +5 -0
  18. package/dist/types/client/state/boards-store.d.ts +79 -0
  19. package/dist/types/client/state/canvas-store.d.ts +2 -0
  20. package/dist/types/client/state/intent-bridge.d.ts +1 -0
  21. package/dist/types/client/state/session-store.d.ts +7 -0
  22. package/dist/types/client/types.d.ts +2 -2
  23. package/dist/types/json-render/catalog.d.ts +1 -1
  24. package/dist/types/json-render/charts/definitions.d.ts +1 -1
  25. package/dist/types/json-render/server.d.ts +6 -0
  26. package/dist/types/mcp/canvas-access.d.ts +10 -3
  27. package/dist/types/mcp/context-uri-template.d.ts +6 -0
  28. package/dist/types/server/agent-presence.d.ts +1 -1
  29. package/dist/types/server/attribution.d.ts +11 -0
  30. package/dist/types/server/ax-context.d.ts +3 -1
  31. package/dist/types/server/ax-interaction.d.ts +2 -2
  32. package/dist/types/server/ax-state.d.ts +13 -3
  33. package/dist/types/server/backup.d.ts +36 -0
  34. package/dist/types/server/board-export.d.ts +45 -0
  35. package/dist/types/server/canvas-db.d.ts +63 -9
  36. package/dist/types/server/canvas-operations.d.ts +23 -0
  37. package/dist/types/server/canvas-provenance.d.ts +1 -1
  38. package/dist/types/server/canvas-state.d.ts +140 -3
  39. package/dist/types/server/context-brief.d.ts +111 -0
  40. package/dist/types/server/context-reads.d.ts +55 -0
  41. package/dist/types/server/document-import.d.ts +66 -0
  42. package/dist/types/server/index.d.ts +103 -4
  43. package/dist/types/server/mutation-history.d.ts +1 -0
  44. package/dist/types/server/operations/invoker.d.ts +2 -0
  45. package/dist/types/server/operations/mcp.d.ts +7 -1
  46. package/dist/types/server/operations/ops/ax-shared.d.ts +2 -2
  47. package/dist/types/server/operations/ops/backup.d.ts +2 -0
  48. package/dist/types/server/operations/ops/boards.d.ts +10 -0
  49. package/dist/types/server/operations/ops/export.d.ts +2 -0
  50. package/dist/types/server/operations/ops/imports.d.ts +2 -0
  51. package/dist/types/server/operations/ops/nodes.d.ts +1 -1
  52. package/dist/types/server/operations/ops/query.d.ts +2 -0
  53. package/dist/types/server/operations/registry.d.ts +9 -1
  54. package/dist/types/server/operations/types.d.ts +1 -1
  55. package/dist/types/server/server.d.ts +1 -0
  56. package/dist/types/server/trace-manager.d.ts +7 -0
  57. package/dist/types/server/webpage-node.d.ts +22 -1
  58. package/dist/types/server/workbench-auth.d.ts +3 -0
  59. package/dist/types/shared/boards.d.ts +6 -0
  60. package/docs/RELEASE.md +5 -0
  61. package/docs/cli.md +68 -0
  62. package/docs/http-api.md +198 -2
  63. package/docs/mcp.md +71 -6
  64. package/docs/node-types.md +26 -0
  65. package/docs/plans/plan-010-attributed-ask-answers.md +39 -0
  66. package/docs/plans/plan-011-context-read-instrumentation.md +54 -0
  67. package/docs/plans/plan-012-many-boards-stage-1.md +98 -0
  68. package/docs/plans/plan-013-static-board-export.md +47 -0
  69. package/docs/plans/plan-014-connected-board-memory.md +140 -0
  70. package/docs/product-vision-2026-09.md +118 -18
  71. package/docs/sdk.md +50 -0
  72. package/package.json +1 -1
  73. package/skills/pmx-canvas/SKILL.md +53 -4
  74. package/skills/pmx-canvas/references/full-reference.md +43 -7
  75. package/skills/pmx-canvas/references/github-copilot-app-adapter.md +3 -0
  76. package/skills/pmx-canvas/references/installing-pmx-canvas.md +4 -4
  77. package/skills/pmx-canvas-orchestration/SKILL.md +5 -0
  78. package/skills/pmx-canvas-testing/SKILL.md +4 -0
  79. package/src/cli/agent.ts +4 -0
  80. package/src/cli/commands/ax.ts +13 -0
  81. package/src/cli/commands/backup.ts +66 -0
  82. package/src/cli/commands/boards.ts +71 -0
  83. package/src/cli/commands/export.ts +30 -0
  84. package/src/cli/commands/nodes.ts +4 -1
  85. package/src/cli/commands/pins.ts +6 -2
  86. package/src/cli/commands/pump.ts +3 -0
  87. package/src/cli/commands/query.ts +33 -7
  88. package/src/cli/commands/view.ts +7 -5
  89. package/src/cli/index.ts +19 -9
  90. package/src/client/App.tsx +21 -3
  91. package/src/client/canvas/BoardCopyDialog.tsx +153 -0
  92. package/src/client/canvas/BoardFolderDialog.tsx +120 -0
  93. package/src/client/canvas/BoardLinkDialog.tsx +93 -0
  94. package/src/client/canvas/BoardSwitcher.tsx +149 -0
  95. package/src/client/canvas/CanvasViewport.tsx +3 -0
  96. package/src/client/canvas/CommandPalette.tsx +107 -4
  97. package/src/client/canvas/ContextMenu.tsx +4 -0
  98. package/src/client/canvas/ExpandedNodeOverlay.tsx +3 -0
  99. package/src/client/canvas/ExportDialog.tsx +197 -0
  100. package/src/client/canvas/HomeView.tsx +277 -0
  101. package/src/client/canvas/SessionPanel.tsx +28 -1
  102. package/src/client/canvas/ToolRail.tsx +38 -3
  103. package/src/client/canvas/TopBar.tsx +34 -18
  104. package/src/client/canvas/import-files.ts +19 -1
  105. package/src/client/canvas/kind-colors.ts +1 -0
  106. package/src/client/icons.tsx +2 -0
  107. package/src/client/nodes/AttachmentNode.tsx +148 -0
  108. package/src/client/nodes/BoardNode.tsx +35 -0
  109. package/src/client/nodes/FileNode.tsx +6 -0
  110. package/src/client/nodes/MarkdownNode.tsx +47 -19
  111. package/src/client/nodes/ax-node-actions.ts +2 -5
  112. package/src/client/nodes/use-ax-surface-bridge.ts +1 -1
  113. package/src/client/state/boards-store.ts +167 -0
  114. package/src/client/state/canvas-store.ts +22 -0
  115. package/src/client/state/intent-bridge.ts +30 -2
  116. package/src/client/state/node-factory.ts +1 -0
  117. package/src/client/state/session-store.ts +5 -0
  118. package/src/client/state/sse-bridge.ts +49 -5
  119. package/src/client/theme/global.css +440 -24
  120. package/src/client/types.ts +2 -0
  121. package/src/json-render/server.ts +16 -9
  122. package/src/mcp/canvas-access.ts +59 -22
  123. package/src/mcp/context-uri-template.ts +28 -0
  124. package/src/mcp/server.ts +122 -15
  125. package/src/server/agent-context.ts +116 -29
  126. package/src/server/attribution.ts +29 -0
  127. package/src/server/ax-context.ts +4 -2
  128. package/src/server/ax-interaction.ts +1 -0
  129. package/src/server/ax-state-manager.ts +72 -3
  130. package/src/server/ax-state.ts +41 -0
  131. package/src/server/backup.ts +178 -0
  132. package/src/server/board-export.ts +686 -0
  133. package/src/server/canvas-db.ts +599 -107
  134. package/src/server/canvas-operations.ts +68 -9
  135. package/src/server/canvas-provenance.ts +1 -0
  136. package/src/server/canvas-schema.ts +12 -0
  137. package/src/server/canvas-state.ts +774 -45
  138. package/src/server/context-brief.ts +372 -0
  139. package/src/server/context-reads.ts +238 -0
  140. package/src/server/demo-state.json +659 -2
  141. package/src/server/document-import.ts +223 -0
  142. package/src/server/index.ts +231 -5
  143. package/src/server/mutation-history.ts +10 -13
  144. package/src/server/operations/composites.ts +26 -4
  145. package/src/server/operations/http.ts +9 -3
  146. package/src/server/operations/index.ts +8 -0
  147. package/src/server/operations/invoker.ts +14 -3
  148. package/src/server/operations/mcp.ts +50 -11
  149. package/src/server/operations/ops/app.ts +5 -0
  150. package/src/server/operations/ops/ax-read.ts +10 -6
  151. package/src/server/operations/ops/ax-timeline.ts +75 -0
  152. package/src/server/operations/ops/backup.ts +134 -0
  153. package/src/server/operations/ops/batch.ts +8 -1
  154. package/src/server/operations/ops/boards.ts +351 -0
  155. package/src/server/operations/ops/export.ts +74 -0
  156. package/src/server/operations/ops/ext-app.ts +4 -0
  157. package/src/server/operations/ops/imports.ts +220 -0
  158. package/src/server/operations/ops/nodes.ts +57 -5
  159. package/src/server/operations/ops/query.ts +206 -9
  160. package/src/server/operations/registry.ts +123 -31
  161. package/src/server/operations/types.ts +1 -1
  162. package/src/server/server.ts +146 -2
  163. package/src/server/spatial-analysis.ts +9 -17
  164. package/src/server/trace-manager.ts +19 -0
  165. package/src/server/web-artifacts.ts +2 -1
  166. package/src/server/webpage-node.ts +173 -13
  167. package/src/server/workbench-auth.ts +11 -0
  168. package/src/shared/boards.ts +18 -0
  169. package/src/shared/node-sizes.ts +1 -0
@@ -144,6 +144,28 @@ export function clearSelection(): void {
144
144
  selectedNodeIds.value = new Set();
145
145
  }
146
146
 
147
+ /** Clear transient UI state that must never follow a node id onto another board. */
148
+ export function resetCanvasInteractionState(): void {
149
+ if (expandedCloseTimer !== null) {
150
+ clearTimeout(expandedCloseTimer);
151
+ expandedCloseTimer = null;
152
+ }
153
+ batch(() => {
154
+ activeNodeId.value = null;
155
+ expandedNodeId.value = null;
156
+ pendingExpandedNodeCloseId.value = null;
157
+ pendingCloseInitialCheckpointAt = undefined;
158
+ selectedNodeIds.value = new Set();
159
+ selectedEdgeId.value = null;
160
+ pendingConnection.value = null;
161
+ draggingEdge.value = null;
162
+ searchHighlightIds.value = null;
163
+ dragDropTarget.value = null;
164
+ contextPinnedNodeIds.value = new Set();
165
+ });
166
+ dropSuppressedForDrag = false;
167
+ }
168
+
147
169
  // ── Groups v2 (rail-chrome-v2 phase 7, item 20) ──────────────────────
148
170
 
149
171
  /**
@@ -1,4 +1,5 @@
1
1
  import { canvasArea } from '../canvas/canvas-area';
2
+ import { showToast } from './attention-bridge';
2
3
  import type { AgentPresenceSnapshot } from '../../shared/agent-presence.js';
3
4
  function logRequestError(action: string, error: unknown): void {
4
5
  console.error(`[intent-bridge] ${action} failed`, error);
@@ -31,14 +32,38 @@ function withWorkbenchMarker(init?: RequestInit): RequestInit {
31
32
  headers['x-pmx-source'] = tabAgentKey;
32
33
  } else {
33
34
  headers['X-PMX-Workbench'] = '1';
35
+ const token = (window as Window & { __PMX_WORKBENCH_TOKEN?: unknown }).__PMX_WORKBENCH_TOKEN;
36
+ if (typeof token === 'string') headers['X-PMX-Workbench-Token'] = token;
34
37
  }
35
38
  return { ...init, headers };
36
39
  }
37
40
 
41
+ /**
42
+ * A write the server refused (fence, edit lock, validation) must not fail
43
+ * silently: the human sees the server's reason. Reads stay quiet.
44
+ */
45
+ function reportRefusedWrite(res: Response, init: RequestInit | undefined, body: unknown): void {
46
+ if (res.ok || (init?.method ?? 'GET').toUpperCase() === 'GET') return;
47
+ const error =
48
+ body !== null && typeof body === 'object' && 'error' in body && typeof body.error === 'string' ? body.error : '';
49
+ showToast('remove', 'Change refused', error || `The server answered ${res.status}.`);
50
+ }
51
+
52
+ async function readJsonBody(res: Response): Promise<unknown> {
53
+ try {
54
+ return await res.json();
55
+ } catch {
56
+ return null;
57
+ }
58
+ }
59
+
38
60
  export async function requestJson<T>(action: string, url: string, fallback: T, init?: RequestInit): Promise<T> {
39
61
  try {
40
62
  const res = await fetch(url, withWorkbenchMarker(init));
41
- return (await res.json()) as T;
63
+ const body = await readJsonBody(res);
64
+ reportRefusedWrite(res, init, body);
65
+ if (body === null) throw new Error(`${res.status} response had no JSON body`);
66
+ return body as T;
42
67
  } catch (error) {
43
68
  logRequestError(action, error);
44
69
  return fallback;
@@ -48,6 +73,7 @@ export async function requestJson<T>(action: string, url: string, fallback: T, i
48
73
  export async function requestOk(action: string, url: string, init?: RequestInit): Promise<{ ok: boolean }> {
49
74
  try {
50
75
  const res = await fetch(url, withWorkbenchMarker(init));
76
+ if (!res.ok) reportRefusedWrite(res, init, await readJsonBody(res));
51
77
  return { ok: res.ok };
52
78
  } catch (error) {
53
79
  logRequestError(action, error);
@@ -57,7 +83,8 @@ export async function requestOk(action: string, url: string, init?: RequestInit)
57
83
 
58
84
  export async function requestBestEffort(action: string, url: string, init?: RequestInit): Promise<void> {
59
85
  try {
60
- await fetch(url, withWorkbenchMarker(init));
86
+ const res = await fetch(url, withWorkbenchMarker(init));
87
+ if (!res.ok) reportRefusedWrite(res, init, await readJsonBody(res));
61
88
  } catch (error) {
62
89
  logRequestError(action, error);
63
90
  }
@@ -262,6 +289,7 @@ export async function createNodeFromClient(opts: {
262
289
  y?: number;
263
290
  width?: number;
264
291
  height?: number;
292
+ boardId?: string;
265
293
  }): Promise<{ ok: boolean; id?: string }> {
266
294
  return requestJson(
267
295
  'createNodeFromClient',
@@ -27,6 +27,7 @@ export const DEFAULT_POSITIONS: Record<CanvasNodeState['type'], { x: number; y:
27
27
  webpage: { x: 380, y: 80, w: 520, h: 420 },
28
28
  'json-render': { x: 380, y: 720, w: 840, h: 620 },
29
29
  graph: { x: 380, y: 720, w: 760, h: 520 },
30
+ board: { x: 380, y: 80, w: 360, h: 160 },
30
31
  ledger: { x: 1130, y: 520, w: 320, h: 280 },
31
32
  trace: { x: 40, y: 900, w: 200, h: 56 },
32
33
  file: { x: 380, y: 80, w: 720, h: 600 },
@@ -26,6 +26,8 @@ export interface ApprovalGateView {
26
26
  createdAt: string;
27
27
  /** Unattended-approval TTL: when a pending gate auto-holds. */
28
28
  expiresAt: string | null;
29
+ resolvedBy?: { actor: 'human' | 'agent' | 'system' | 'unknown'; source: string; agentId?: string } | null;
30
+ selfAnswer?: boolean;
29
31
  }
30
32
  export interface AxEventView {
31
33
  id: string;
@@ -106,6 +108,9 @@ export async function setScopeFence(nodeIds: string[] | null): Promise<boolean>
106
108
 
107
109
  /** Auto-held gates: the policy said no on the human's behalf; they can be reopened. */
108
110
  export const heldGates = computed(() => sessionGates.value.filter((gate) => gate.status === 'held'));
111
+ export const resolvedGates = computed(() =>
112
+ sessionGates.value.filter((gate) => gate.status === 'approved' || gate.status === 'rejected'),
113
+ );
109
114
 
110
115
  export interface AxTimelineView {
111
116
  events: AxEventView[];
@@ -27,10 +27,13 @@ import {
27
27
  workbenchConnectionEpoch,
28
28
  reconnectAttempt,
29
29
  reconnectDelay,
30
+ resetCanvasInteractionState,
30
31
  } from './canvas-store';
32
+ import { applyBoards, loadBoards, setBoardSwitchHandler } from './boards-store';
31
33
  import {
32
34
  fetchAgentPresence,
33
35
  fetchAxSurfaceState,
36
+ fetchCanvasState,
34
37
  reportClientViewportSize,
35
38
  requestJson,
36
39
  tabAgentKey,
@@ -304,6 +307,7 @@ function isCanvasNodeType(value: unknown): value is CanvasNodeState['type'] {
304
307
  value === 'webpage' ||
305
308
  value === 'json-render' ||
306
309
  value === 'graph' ||
310
+ value === 'board' ||
307
311
  value === 'prompt' ||
308
312
  value === 'response' ||
309
313
  value === 'status' ||
@@ -456,14 +460,44 @@ function handleAgentSessionEnded(data: Record<string, unknown>): void {
456
460
  }
457
461
 
458
462
  let stopHumanPresence: (() => void) | null = null;
463
+ let boardResyncGeneration = 0;
464
+ let layoutRevision = 0;
465
+ let pinsRevision = 0;
459
466
 
460
467
  function handleHumanPresence(data: Record<string, unknown>): void {
461
468
  applyHumanSnapshot(data as Partial<HumanPresenceSnapshot>);
462
469
  }
463
470
 
471
+ /**
472
+ * The open board changed (this tab, another tab, or a restart): the canvas is
473
+ * a different board now, so take its layout like a fresh connection would —
474
+ * camera included — and drop what belonged to the old board.
475
+ */
476
+ function resyncForBoardSwitch(): void {
477
+ const generation = ++boardResyncGeneration;
478
+ savedLayout = null;
479
+ hasInitialServerLayout.value = false;
480
+ resetIntents();
481
+ resetCanvasInteractionState();
482
+ axSurfaceState.value = null;
483
+ void refreshAxSurface();
484
+ const layoutAtRequest = layoutRevision;
485
+ const pinsAtRequest = pinsRevision;
486
+ void fetchCanvasState().then((layout) => {
487
+ if (generation === boardResyncGeneration && layoutAtRequest === layoutRevision)
488
+ handleCanvasLayoutUpdate({ layout });
489
+ });
490
+ void requestJson<{ nodeIds?: unknown }>('fetchPinnedContext', '/api/canvas/pinned-context', {}).then((data) => {
491
+ if (generation === boardResyncGeneration && pinsAtRequest === pinsRevision)
492
+ handleContextPinsChanged({ nodeIds: data.nodeIds });
493
+ });
494
+ }
495
+ setBoardSwitchHandler(resyncForBoardSwitch);
496
+
464
497
  function handleConnected(data: Record<string, unknown>): void {
465
498
  sessionId.value = (data.sessionId as string) || '';
466
499
  connectionStatus.value = 'connected';
500
+ void loadBoards();
467
501
  // Reconnect marker for holders of server-minted URLs (Finding S).
468
502
  workbenchConnectionEpoch.value += 1;
469
503
  reloadIfServerUpgraded(data.version, data.bundleStamp);
@@ -486,8 +520,7 @@ function handleConnected(data: Record<string, unknown>): void {
486
520
  // The AX surface snapshot (work items, gates) likewise — it used to arrive
487
521
  // only on the first ax-state-changed, so a fresh load showed an empty
488
522
  // session panel while persisted work items existed.
489
- void fetchAxSurfaceState().then((state) => {
490
- axSurfaceState.value = state;
523
+ void refreshAxSurface().then(() => {
491
524
  if (sessionActive.value) void refreshTimeline();
492
525
  });
493
526
  // A ?theme= session override (host-default theming) wins over the
@@ -903,6 +936,7 @@ function handleCanvasLayoutUpdate(data: Record<string, unknown>): void {
903
936
  }
904
937
  | undefined;
905
938
  if (!layout?.nodes) return;
939
+ layoutRevision += 1;
906
940
  const shouldApplyViewport = !hasInitialServerLayout.value;
907
941
  hasInitialServerLayout.value = true;
908
942
 
@@ -993,6 +1027,7 @@ function handleThemeChanged(data: Record<string, unknown>): void {
993
1027
  }
994
1028
 
995
1029
  function handleContextPinsChanged(data: Record<string, unknown>): void {
1030
+ pinsRevision += 1;
996
1031
  const nodeIds = Array.isArray(data.nodeIds) ? data.nodeIds.filter((id): id is string => typeof id === 'string') : [];
997
1032
  replaceContextPinsFromServer(nodeIds);
998
1033
  syncAttentionFromSse({ event: 'context-pins-changed', data });
@@ -1003,13 +1038,21 @@ function handleContextPinsChanged(data: Record<string, unknown>): void {
1003
1038
  // (debounced). The snapshot feeds AX-enabled surfaces (HtmlNode/McpAppNode push it
1004
1039
  // into their iframes), so authored boards reflect the live work queue / focus.
1005
1040
  let axRefreshTimer: ReturnType<typeof setTimeout> | null = null;
1041
+ let axRefreshSequence = 0;
1042
+ async function refreshAxSurface(): Promise<void> {
1043
+ const sequence = ++axRefreshSequence;
1044
+ const generation = boardResyncGeneration;
1045
+ const state = await fetchAxSurfaceState();
1046
+ if (sequence === axRefreshSequence && generation === boardResyncGeneration) axSurfaceState.value = state;
1047
+ }
1048
+
1006
1049
  function handleAxStateChanged(): void {
1007
1050
  if (axRefreshTimer) clearTimeout(axRefreshTimer);
1051
+ // Invalidate any in-flight read before the debounced replacement starts.
1052
+ axRefreshSequence += 1;
1008
1053
  axRefreshTimer = setTimeout(() => {
1009
1054
  axRefreshTimer = null;
1010
- void fetchAxSurfaceState().then((state) => {
1011
- axSurfaceState.value = state;
1012
- });
1055
+ void refreshAxSurface();
1013
1056
  // The session panel's timeline — only worth fetching while it is mounted.
1014
1057
  if (sessionActive.value) void refreshTimeline();
1015
1058
  }, 150);
@@ -1064,6 +1107,7 @@ export const EVENT_HANDLERS: Record<string, (data: Record<string, unknown>) => v
1064
1107
  'ext-app-update': handleExtAppUpdate,
1065
1108
  'ext-app-result': handleExtAppResult,
1066
1109
  'context-pins-changed': handleContextPinsChanged,
1110
+ 'boards-changed': applyBoards,
1067
1111
  'canvas-layout-update': handleCanvasLayoutUpdate,
1068
1112
  'canvas-focus-node': handleCanvasFocusNode,
1069
1113
  'canvas-viewport-update': handleCanvasViewportUpdate,