pmx-canvas 0.7.3 → 0.8.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 (235) hide show
  1. package/.github/extensions/pmx-canvas/THIRD_PARTY_LICENSES.txt +331 -0
  2. package/.github/extensions/pmx-canvas/extension.mjs +25 -172
  3. package/.github/extensions/pmx-canvas/mcp-bootstrap.ts +198 -0
  4. package/.github/extensions/pmx-canvas/runtime.d.mts +41 -0
  5. package/.github/extensions/pmx-canvas/runtime.json +3 -0
  6. package/.github/extensions/pmx-canvas/runtime.mjs +194 -0
  7. package/.github/extensions/pmx-canvas/start-mcp.mjs +51 -0
  8. package/.github/extensions/pmx-canvas/tools.json +2560 -0
  9. package/CHANGELOG.md +47 -0
  10. package/Readme.md +21 -7
  11. package/dist/canvas/fonts/OFL.txt +93 -0
  12. package/dist/canvas/fonts/ibm-plex-mono-latin-400-normal.woff2 +0 -0
  13. package/dist/canvas/fonts/ibm-plex-mono-latin-500-normal.woff2 +0 -0
  14. package/dist/canvas/fonts/ibm-plex-mono-latin-ext-400-normal.woff2 +0 -0
  15. package/dist/canvas/fonts/ibm-plex-mono-latin-ext-500-normal.woff2 +0 -0
  16. package/dist/canvas/fonts/ibm-plex-sans-latin-400-normal.woff2 +0 -0
  17. package/dist/canvas/fonts/ibm-plex-sans-latin-500-normal.woff2 +0 -0
  18. package/dist/canvas/fonts/ibm-plex-sans-latin-600-normal.woff2 +0 -0
  19. package/dist/canvas/fonts/ibm-plex-sans-latin-ext-400-normal.woff2 +0 -0
  20. package/dist/canvas/fonts/ibm-plex-sans-latin-ext-500-normal.woff2 +0 -0
  21. package/dist/canvas/fonts/ibm-plex-sans-latin-ext-600-normal.woff2 +0 -0
  22. package/dist/canvas/global.css +723 -672
  23. package/dist/canvas/index.js +90 -88
  24. package/dist/canvas/mcp-app.html +7878 -0
  25. package/dist/canvas/mermaid-entry.js +1 -1
  26. package/dist/canvas/surface-theme.css +151 -46
  27. package/dist/json-render/index.css +1 -1
  28. package/dist/types/cli/commands/record.d.ts +1 -1
  29. package/dist/types/client/canvas/NodeContextMark.d.ts +24 -0
  30. package/dist/types/client/canvas/SessionReceipt.d.ts +3 -1
  31. package/dist/types/client/icons.d.ts +1 -2
  32. package/dist/types/client/mcp-app/index.d.ts +1 -0
  33. package/dist/types/client/nodes/ax-node-actions.d.ts +2 -2
  34. package/dist/types/client/nodes/markdown-sanitize.d.ts +2 -0
  35. package/dist/types/client/nodes/use-workbench-asset.d.ts +2 -0
  36. package/dist/types/client/state/attention-store.d.ts +0 -5
  37. package/dist/types/client/state/context-status-store.d.ts +53 -0
  38. package/dist/types/client/state/intent-bridge.d.ts +3 -0
  39. package/dist/types/client/state/near-pin-store.d.ts +16 -0
  40. package/dist/types/client/state/presence-store.d.ts +5 -0
  41. package/dist/types/client/state/session-store.d.ts +33 -3
  42. package/dist/types/client/state/workbench-transport.d.ts +16 -0
  43. package/dist/types/client/theme/tokens.d.ts +1 -3
  44. package/dist/types/client/types.d.ts +9 -0
  45. package/dist/types/json-render/server.d.ts +1 -1
  46. package/dist/types/mcp/app-server.d.ts +3 -0
  47. package/dist/types/mcp/canvas-access.d.ts +1 -1
  48. package/dist/types/mcp/canvas-app.d.ts +4 -0
  49. package/dist/types/mcp/workbench-app.d.ts +4 -0
  50. package/dist/types/server/agent-presence.d.ts +1 -1
  51. package/dist/types/server/ax-state.d.ts +1 -1
  52. package/dist/types/server/canvas-db.d.ts +8 -0
  53. package/dist/types/server/canvas-geometry.d.ts +25 -0
  54. package/dist/types/server/canvas-operations.d.ts +1 -11
  55. package/dist/types/server/canvas-state.d.ts +25 -3
  56. package/dist/types/server/context-brief.d.ts +8 -1
  57. package/dist/types/server/context-reads.d.ts +30 -7
  58. package/dist/types/server/index.d.ts +6 -1
  59. package/dist/types/server/mcp-app-runtime.d.ts +1 -0
  60. package/dist/types/server/operations/http.d.ts +2 -0
  61. package/dist/types/server/operations/invoker.d.ts +4 -2
  62. package/dist/types/server/operations/ops/nodes.d.ts +0 -6
  63. package/dist/types/server/server.d.ts +1 -0
  64. package/dist/types/server/spatial-analysis.d.ts +2 -0
  65. package/dist/types/shared/agent-context.d.ts +22 -0
  66. package/dist/types/shared/agent-presence.d.ts +3 -10
  67. package/dist/types/shared/canvas-app.d.ts +54 -0
  68. package/dist/types/shared/themes.d.ts +1 -1
  69. package/docs/RELEASE.md +7 -3
  70. package/docs/agent-plugin.md +123 -0
  71. package/docs/ax-state-contract.md +40 -0
  72. package/docs/cli.md +7 -2
  73. package/docs/design/AgentContext.dc.html +186 -0
  74. package/docs/design/Before.dc.html +208 -0
  75. package/docs/design/BoardPins.dc.html +151 -0
  76. package/docs/design/Chrome.dc.html +208 -0
  77. package/docs/design/Context.dc.html +239 -0
  78. package/docs/design/Flows.dc.html +181 -0
  79. package/docs/design/Home.dc.html +209 -0
  80. package/docs/design/HomeGraph.dc.html +211 -0
  81. package/docs/design/HomeMap.dc.html +117 -0
  82. package/docs/design/HomeTeam.dc.html +176 -0
  83. package/docs/design/Light.dc.html +28 -0
  84. package/docs/design/Main.dc.html +280 -0
  85. package/docs/design/NearPin.dc.html +260 -0
  86. package/docs/design/Pane600.dc.html +120 -0
  87. package/docs/design/Relations.dc.html +297 -0
  88. package/docs/design/Themes.dc.html +121 -0
  89. package/docs/design/TokenSystem.dc.html +136 -0
  90. package/docs/design/Tokens.dc.html +163 -0
  91. package/docs/design/Types1.dc.html +177 -0
  92. package/docs/design/Types2.dc.html +226 -0
  93. package/docs/design/Types3.dc.html +195 -0
  94. package/docs/design/Uses.dc.html +152 -0
  95. package/docs/design/Vision.dc.html +116 -0
  96. package/docs/design/Zoom.dc.html +82 -0
  97. package/docs/design/canvas.json +274 -0
  98. package/docs/design.md +214 -0
  99. package/docs/environment.md +1 -1
  100. package/docs/http-api.md +80 -15
  101. package/docs/mcp.md +198 -3
  102. package/docs/node-types.md +34 -10
  103. package/docs/plans/plan-009-tech-debt-backlog.md +1 -1
  104. package/docs/plans/plan-015-design-foundation-wave-0.md +177 -0
  105. package/docs/plans/plan-016-context-made-visible-wave-1.md +148 -0
  106. package/docs/product-vision-2026-09.md +47 -7
  107. package/docs/screenshot.png +0 -0
  108. package/docs/sdk.md +1 -0
  109. package/package.json +10 -4
  110. package/plugins/pmx-canvas/LICENSE +21 -0
  111. package/plugins/pmx-canvas/THIRD_PARTY_LICENSES.txt +331 -0
  112. package/plugins/pmx-canvas/assets/preview.png +0 -0
  113. package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/extension.mjs +930 -0
  114. package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/runtime.json +3 -0
  115. package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/runtime.mjs +194 -0
  116. package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/steering-delivery.mjs +79 -0
  117. package/plugins/pmx-canvas/mcp.json +12 -0
  118. package/plugins/pmx-canvas/plugin.json +24 -0
  119. package/plugins/pmx-canvas/runtime.json +3 -0
  120. package/plugins/pmx-canvas/runtime.mjs +194 -0
  121. package/plugins/pmx-canvas/start-mcp.mjs +51 -0
  122. package/plugins/pmx-canvas/tools.json +2560 -0
  123. package/skills/pmx-canvas/SKILL.md +10 -7
  124. package/skills/pmx-canvas/references/full-reference.md +11 -7
  125. package/skills/pmx-canvas/references/github-copilot-app-adapter.md +20 -8
  126. package/skills/pmx-canvas/references/installing-pmx-canvas.md +6 -6
  127. package/skills/pmx-canvas-testing/SKILL.md +5 -4
  128. package/src/cli/commands/copilot.ts +5 -0
  129. package/src/cli/commands/view.ts +1 -1
  130. package/src/cli/index.ts +8 -3
  131. package/src/client/App.tsx +6 -1
  132. package/src/client/canvas/AgentPresenceLayer.tsx +3 -3
  133. package/src/client/canvas/AnnotationLayer.tsx +1 -1
  134. package/src/client/canvas/CanvasNode.tsx +32 -49
  135. package/src/client/canvas/CanvasViewport.tsx +0 -2
  136. package/src/client/canvas/CommandPalette.tsx +3 -2
  137. package/src/client/canvas/ContextMenu.tsx +11 -0
  138. package/src/client/canvas/EdgeLayer.tsx +1 -1
  139. package/src/client/canvas/ExportDialog.tsx +33 -6
  140. package/src/client/canvas/IntentLayer.tsx +2 -2
  141. package/src/client/canvas/Minimap.tsx +12 -7
  142. package/src/client/canvas/NodeContextMark.tsx +156 -0
  143. package/src/client/canvas/SessionPanel.tsx +4 -7
  144. package/src/client/canvas/SessionReceipt.tsx +114 -17
  145. package/src/client/canvas/ShortcutOverlay.tsx +6 -1
  146. package/src/client/canvas/SnapshotPanel.tsx +11 -2
  147. package/src/client/canvas/ToolRail.tsx +12 -9
  148. package/src/client/canvas/TopBar.tsx +16 -11
  149. package/src/client/icons.tsx +2 -12
  150. package/src/client/mcp-app/index.tsx +137 -0
  151. package/src/client/mcp-app/style.css +10 -0
  152. package/src/client/nodes/AttachmentNode.tsx +8 -1
  153. package/src/client/nodes/AxStepControls.tsx +1 -1
  154. package/src/client/nodes/ContextNode.tsx +9 -9
  155. package/src/client/nodes/ExtAppFrame.tsx +13 -8
  156. package/src/client/nodes/FileNode.tsx +8 -5
  157. package/src/client/nodes/ImageNode.tsx +4 -2
  158. package/src/client/nodes/InlineMarkdownEditor.tsx +2 -1
  159. package/src/client/nodes/LedgerNode.tsx +2 -2
  160. package/src/client/nodes/MarkdownNode.tsx +44 -9
  161. package/src/client/nodes/McpAppNode.tsx +6 -4
  162. package/src/client/nodes/PromptNode.tsx +5 -17
  163. package/src/client/nodes/ResponseNode.tsx +3 -15
  164. package/src/client/nodes/StatusNode.tsx +2 -2
  165. package/src/client/nodes/StatusSummary.tsx +3 -1
  166. package/src/client/nodes/TraceNode.tsx +6 -4
  167. package/src/client/nodes/WebpageNode.tsx +1 -1
  168. package/src/client/nodes/ax-node-actions.ts +2 -2
  169. package/src/client/nodes/iframe-document-url.ts +3 -2
  170. package/src/client/nodes/markdown-sanitize.ts +10 -0
  171. package/src/client/nodes/surface-url.ts +10 -0
  172. package/src/client/nodes/use-surface-frame.ts +3 -1
  173. package/src/client/nodes/use-workbench-asset.ts +26 -0
  174. package/src/client/state/attention-bridge.ts +0 -8
  175. package/src/client/state/attention-store.ts +0 -17
  176. package/src/client/state/canvas-store.ts +4 -2
  177. package/src/client/state/context-status-store.ts +66 -0
  178. package/src/client/state/human-store.ts +2 -0
  179. package/src/client/state/iframe-mode.ts +5 -0
  180. package/src/client/state/intent-bridge.ts +10 -4
  181. package/src/client/state/near-pin-store.ts +53 -0
  182. package/src/client/state/presence-store.ts +5 -10
  183. package/src/client/state/session-store.ts +106 -9
  184. package/src/client/state/sse-bridge.ts +26 -7
  185. package/src/client/state/theme-override.ts +1 -1
  186. package/src/client/state/workbench-transport.ts +36 -0
  187. package/src/client/theme/global.css +723 -672
  188. package/src/client/theme/surface-theme.css +151 -46
  189. package/src/client/theme/tokens.ts +7 -11
  190. package/src/client/types.ts +10 -0
  191. package/src/json-render/server.ts +71 -14
  192. package/src/mcp/app-server.ts +64 -0
  193. package/src/mcp/canvas-access.ts +14 -11
  194. package/src/mcp/canvas-app.ts +318 -0
  195. package/src/mcp/server.ts +1 -1
  196. package/src/mcp/workbench-app.ts +211 -0
  197. package/src/server/agent-presence.ts +45 -6
  198. package/src/server/ax-context.ts +1 -1
  199. package/src/server/ax-state.ts +1 -1
  200. package/src/server/board-export.ts +5 -2
  201. package/src/server/canvas-db.ts +46 -12
  202. package/src/server/canvas-geometry.ts +64 -0
  203. package/src/server/canvas-operations.ts +6 -24
  204. package/src/server/canvas-state.ts +105 -15
  205. package/src/server/canvas-validation.ts +19 -4
  206. package/src/server/context-brief.ts +39 -2
  207. package/src/server/context-reads.ts +86 -15
  208. package/src/server/demo-state.json +31 -31
  209. package/src/server/html-surface.ts +4 -5
  210. package/src/server/index.ts +19 -3
  211. package/src/server/mcp-app-runtime.ts +11 -0
  212. package/src/server/operations/composites.ts +4 -3
  213. package/src/server/operations/http.ts +4 -4
  214. package/src/server/operations/invoker.ts +13 -4
  215. package/src/server/operations/ops/ax-read.ts +1 -1
  216. package/src/server/operations/ops/ax-timeline.ts +37 -0
  217. package/src/server/operations/ops/boards.ts +1 -1
  218. package/src/server/operations/ops/canvas-wire.ts +3 -6
  219. package/src/server/operations/ops/intent.ts +31 -2
  220. package/src/server/operations/ops/json-render.ts +1 -1
  221. package/src/server/operations/ops/nodes.ts +8 -76
  222. package/src/server/operations/ops/query.ts +8 -2
  223. package/src/server/operations/ops/snapshots.ts +4 -1
  224. package/src/server/operations/ops/viewport.ts +29 -5
  225. package/src/server/operations/registry.ts +38 -4
  226. package/src/server/scope-fence.ts +55 -10
  227. package/src/server/server.ts +83 -5
  228. package/src/server/spatial-analysis.ts +2 -2
  229. package/src/server/workboard.ts +6 -1
  230. package/src/{server → shared}/agent-context.ts +2 -2
  231. package/src/shared/agent-presence.ts +3 -15
  232. package/src/shared/canvas-app.ts +62 -0
  233. package/src/shared/themes.ts +5 -5
  234. package/src/client/canvas/FocusFieldLayer.tsx +0 -116
  235. package/src/client/canvas/kind-colors.ts +0 -23
@@ -25,6 +25,7 @@
25
25
  */
26
26
  import { type FenceRect, fenceRectFromNodes } from '../shared/scope-fence.js';
27
27
  import { canvasState } from './canvas-state.js';
28
+ import { resolvePatchGeometry, validCanvasNodeUpdates } from './canvas-geometry.js';
28
29
  import type { Operation } from './operations/types.js';
29
30
 
30
31
  /** Bounding box of the fenced nodes plus padding; null when none of them exist. */
@@ -103,6 +104,48 @@ function groupChildren(input: Record<string, unknown>): string[] {
103
104
  return 'children' in data ? stringList(data.children) : [];
104
105
  }
105
106
 
107
+ /** Automatic frames are write targets when their descendants can cause a re-fit.
108
+ * Conservatively include them even if the resulting bounds happen to be equal.
109
+ * A manual frame does not re-fit, so propagation stops there. */
110
+ function includeAutoFrameParents(nodeIds: string[]): string[] {
111
+ const targets = new Set(nodeIds);
112
+ for (const id of targets) {
113
+ const parentId = canvasState.getNode(id)?.data.parentGroup;
114
+ if (typeof parentId !== 'string') continue;
115
+ const parent = canvasState.getNode(parentId);
116
+ if (parent?.type === 'group' && parent.data.frameMode !== 'manual') targets.add(parentId);
117
+ }
118
+ return [...targets];
119
+ }
120
+
121
+ /** `canvasState.applyUpdates`: group moves translate children unless that child has its own position update. */
122
+ function applyUpdatesTarget(input: Record<string, unknown>): FenceTarget {
123
+ const updates = validCanvasNodeUpdates(input.updates);
124
+ const explicitPositionIds = new Set(
125
+ updates.filter((update) => update.position !== undefined).map((update) => update.id),
126
+ );
127
+ const nodeIds: string[] = [];
128
+ const points: Array<{ x: number; y: number }> = [];
129
+ for (const update of updates) {
130
+ const id = update.id;
131
+ nodeIds.push(id);
132
+ if (!update.position) continue;
133
+ const { x, y } = update.position;
134
+ points.push({ x, y });
135
+ const node = canvasState.getNode(id);
136
+ if (node?.type !== 'group') continue;
137
+ const deltaX = x - node.position.x;
138
+ const deltaY = y - node.position.y;
139
+ for (const childId of stringList(node.data.children)) {
140
+ if (explicitPositionIds.has(childId)) continue;
141
+ nodeIds.push(childId);
142
+ const child = canvasState.getNode(childId);
143
+ if (child) points.push({ x: child.position.x + deltaX, y: child.position.y + deltaY });
144
+ }
145
+ }
146
+ return { nodeIds: includeAutoFrameParents(nodeIds), points };
147
+ }
148
+
106
149
  function createTarget(input: Record<string, unknown>, extraNodeIds: string[] = []): FenceTarget {
107
150
  const x = Number(input.x);
108
151
  const y = Number(input.y);
@@ -118,17 +161,19 @@ export function describeOpTarget(op: Operation, rawInput: unknown): FenceTarget
118
161
  const input = asRecord(rawInput);
119
162
  if (BOARD_WIDE_OPS.has(op.name)) return { boardWide: true };
120
163
  switch (op.name) {
164
+ case 'canvas.apply-updates':
165
+ return applyUpdatesTarget(input);
121
166
  case 'node.update': {
122
- // A group's membership change reparents (and may reposition) its children.
123
- const nodeIds = [str(input.id), ...groupChildren(input)];
124
- // A move's DESTINATION must also land inside the fence — otherwise an
125
- // agent relocates a fenced node outward, and since the fence rect is
126
- // derived from live node positions, that grows its own writable region
127
- // past the human-seeded region.
128
- const x = Number(input.x);
129
- const y = Number(input.y);
130
- if (Number.isFinite(x) && Number.isFinite(y)) return { points: [{ x, y }], nodeIds };
131
- return { nodeIds };
167
+ const id = str(input.id);
168
+ const existing = canvasState.getNode(id);
169
+ const geometry = existing ? resolvePatchGeometry(input, existing) : {};
170
+ // Share the bulk translation rules after resolving PATCH aliases and
171
+ // omitted axes. Check children's destinations as well as the group origin.
172
+ const target = applyUpdatesTarget({ updates: [{ id, ...geometry }] });
173
+ return {
174
+ ...target,
175
+ nodeIds: includeAutoFrameParents([id, ...(target.nodeIds ?? []), ...groupChildren(input)]),
176
+ };
132
177
  }
133
178
  case 'node.remove':
134
179
  return { nodeIds: [str(input.id)] };
@@ -62,8 +62,10 @@ import { getMcpAppHostSnapshot } from './mcp-app-host.js';
62
62
  import { closeMcpAppSession, closeAllMcpAppSessions } from './mcp-app-runtime.js';
63
63
  import { findOpenCanvasPosition } from './placement.js';
64
64
  import { overlapsAny } from '../shared/placement.js';
65
+ import { CANVAS_APP_WRITE_CONTRACT } from '../shared/canvas-app.js';
66
+ import { withoutExternalMcp } from './mcp-app-runtime.js';
65
67
  import { mutationHistory } from './mutation-history.js';
66
- import { buildAgentContextPreamble } from './agent-context.js';
68
+ import { buildAgentContextPreamble } from '../shared/agent-context.js';
67
69
  import { buildCanvasAxSurfaceSnapshot } from './ax-context.js';
68
70
  import { resolveNodeAxCapabilities } from './ax-interaction.js';
69
71
  import { normalizeCanvasTheme, type CanvasTheme } from './canvas-db.js';
@@ -145,20 +147,27 @@ agentPresence.setSessionStartListener((presence) => {
145
147
  const startedAt = new Date().toISOString();
146
148
  return canvasState.saveSnapshot(`Before session · ${presence.label} · ${startedAt.slice(11, 16)}`)?.id ?? null;
147
149
  });
148
- agentPresence.setSessionEndListener((presence, startSnapshotId, endedBy) => {
150
+ agentPresence.setSessionEndListener((presence, startSnapshotId, endedBy, startedAt) => {
149
151
  const ax = canvasState.getAxState();
150
152
  const endedAt = new Date().toISOString();
153
+ const layout = canvasState.getLayout();
151
154
  let snapshot = startSnapshotId
152
155
  ? (canvasState.listSnapshots({ all: true }).find((entry) => entry.id === startSnapshotId) ?? null)
153
156
  : null;
154
- let unchanged = !startSnapshotId && canvasState.getLayout().nodes.length === 0;
157
+ let unchanged = !startSnapshotId && layout.nodes.length === 0;
158
+ // Nodes the session added and changed: the diff against the start snapshot,
159
+ // or every node when the board was empty at attach (no snapshot was taken).
160
+ let addedIds = startSnapshotId ? new Set<string>() : new Set(layout.nodes.map((node) => node.id));
161
+ let modifiedIds = new Set<string>();
155
162
  // A session that changed NOTHING on the board leaves no snapshot behind —
156
163
  // session churn (idle timeouts, receipt tests, attach/detach cycles) was
157
164
  // flooding the History drawer with identical boards.
158
165
  if (snapshot) {
159
166
  const snapData = canvasState.getSnapshotData(snapshot.id);
160
167
  if (snapData) {
161
- const diff = diffLayouts(snapData.name, snapData, canvasState.getLayout());
168
+ const diff = diffLayouts(snapData.name, snapData, layout);
169
+ addedIds = new Set(diff.addedNodes.map((node) => node.id));
170
+ modifiedIds = new Set(diff.modifiedNodes.map((node) => node.id));
162
171
  if (
163
172
  diff.addedNodes.length === 0 &&
164
173
  diff.removedNodes.length === 0 &&
@@ -188,6 +197,10 @@ agentPresence.setSessionEndListener((presence, startSnapshotId, endedBy) => {
188
197
  rejected: ax.approvalGates.filter((gate) => gate.status === 'rejected').length,
189
198
  held: ax.approvalGates.filter((gate) => gate.status === 'held').length,
190
199
  };
200
+ const context = sessionContextActivity(layout.nodes, startedAt, addedIds, modifiedIds);
201
+ // Reading or pinning is something the human wants to see even when the
202
+ // board itself did not change ("did the agent read what I pinned?").
203
+ if (context.read.length > 0 || context.pinned.length > 0) unchanged = false;
191
204
  // No agent `sessionId` here: the SSE envelope reserves that key for the
192
205
  // workbench session and would overwrite it. The label is what the receipt shows.
193
206
  emitPrimaryWorkbenchEvent('agent-session-ended', {
@@ -198,9 +211,63 @@ agentPresence.setSessionEndListener((presence, startSnapshotId, endedBy) => {
198
211
  counts,
199
212
  unchanged,
200
213
  snapshot: snapshot ? { id: snapshot.id, name: snapshot.name } : null,
214
+ context,
201
215
  });
202
216
  });
203
217
 
218
+ interface ReceiptNode {
219
+ id: string;
220
+ title: string;
221
+ reason?: string;
222
+ }
223
+
224
+ /**
225
+ * What an agent session did with the board's context (docs/design/AgentContext.dc.html
226
+ * lens and receipt): nodes read since it attached, pins it made, nodes it created and
227
+ * edited, and pinned nodes whose content changed after the latest read.
228
+ */
229
+ function sessionContextActivity(
230
+ nodes: CanvasNodeState[],
231
+ startedAt: string | null,
232
+ addedIds: Set<string>,
233
+ modifiedIds: Set<string>,
234
+ ): {
235
+ read: ReceiptNode[];
236
+ pinned: ReceiptNode[];
237
+ created: ReceiptNode[];
238
+ edited: ReceiptNode[];
239
+ changedSinceRead: ReceiptNode[];
240
+ } {
241
+ const byId = new Map(nodes.map((node) => [node.id, node]));
242
+ const entry = (node: CanvasNodeState): ReceiptNode => ({
243
+ id: node.id,
244
+ title: typeof node.data.title === 'string' && node.data.title.trim() ? node.data.title : node.id,
245
+ });
246
+ const boardId = canvasState.activeBoardId;
247
+ const read = startedAt
248
+ ? canvasState.getReadNodeIdsSince(boardId, startedAt).flatMap((id) => {
249
+ const node = byId.get(id);
250
+ return node ? [entry(node)] : [];
251
+ })
252
+ : [];
253
+ const pinned = Object.entries(canvasState.getContextPinMeta()).flatMap(([id, meta]) => {
254
+ const node = byId.get(id);
255
+ if (!node || meta.pinnedBy.actor !== 'agent' || !startedAt || meta.pinnedAt < startedAt) return [];
256
+ return [{ ...entry(node), ...(meta.reason ? { reason: meta.reason } : {}) }];
257
+ });
258
+ const created = nodes.filter((node) => addedIds.has(node.id) && node.createdBy?.actor === 'agent').map(entry);
259
+ const edited = nodes.filter((node) => modifiedIds.has(node.id) && node.lastEditedBy?.actor === 'agent').map(entry);
260
+ const readStatus = new Map(canvasState.getNodeReadStatus(boardId).map((status) => [status.nodeId, status]));
261
+ const changedSinceRead = [...canvasState.contextPinnedNodeIds].flatMap((id) => {
262
+ const node = byId.get(id);
263
+ const status = readStatus.get(id);
264
+ // Only pins this session read: the receipt says "changed after <agent> read it".
265
+ const readThisSession = status && startedAt !== null && status.lastReadAt >= startedAt;
266
+ return node && status && readThisSession && (node.contentRevision ?? 0) > status.readRevision ? [entry(node)] : [];
267
+ });
268
+ return { read, pinned, created, edited, changedSinceRead };
269
+ }
270
+
204
271
  // Webview-runner wiring (plan-008 Wave 3): the webview ops never import this
205
272
  // module — the Bun.WebView automation runner is injected here, mirroring the
206
273
  // setOperationEventEmitter pattern. The closures call the real automation
@@ -1181,6 +1248,7 @@ const CANVAS_STATIC_MIME: Record<string, string> = {
1181
1248
  '.json': 'application/json',
1182
1249
  '.map': 'application/json',
1183
1250
  '.wasm': 'application/wasm',
1251
+ '.woff2': 'font/woff2',
1184
1252
  };
1185
1253
 
1186
1254
  const CANVAS_FAVICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
@@ -3263,6 +3331,7 @@ export interface CanvasServerOptions {
3263
3331
  workspaceRoot?: string;
3264
3332
  autoOpenBrowser?: boolean;
3265
3333
  allowPortFallback?: boolean;
3334
+ allowExternalMcp?: boolean;
3266
3335
  }
3267
3336
 
3268
3337
  export function startCanvasServer(options: CanvasServerOptions = {}): string | null {
@@ -3281,6 +3350,12 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
3281
3350
  primaryWorkbenchAutoOpenEnabled = options.autoOpenBrowser;
3282
3351
  }
3283
3352
 
3353
+ // A recorded agent read changes what the workbench marks as read; tell the
3354
+ // browser only (an MCP notification would make a read cause more reads).
3355
+ canvasState.setContextReadListener((boardId) => {
3356
+ emitPrimaryWorkbenchEvent('context-status-changed', { boardId });
3357
+ });
3358
+
3284
3359
  // Ensure direct HTTP server usage records undo/redo history, not just PmxCanvas.start().
3285
3360
  canvasState.onMutation((info) => {
3286
3361
  mutationHistory.record({
@@ -3304,7 +3379,9 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
3304
3379
  if (loaded) {
3305
3380
  console.log(' Canvas state restored from .pmx-canvas/canvas.db');
3306
3381
  primeCanvasRuntimeBackends({ forceRehydrateExtApps: true });
3307
- void syncCanvasRuntimeBackends({ forceRehydrateExtApps: true, alreadyPrimed: true }).finally(() => {
3382
+ const rehydrate = () => syncCanvasRuntimeBackends({ forceRehydrateExtApps: true, alreadyPrimed: true });
3383
+ const pending = options.allowExternalMcp === false ? withoutExternalMcp(rehydrate) : rehydrate();
3384
+ void pending.finally(() => {
3308
3385
  emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
3309
3386
  });
3310
3387
  }
@@ -3361,6 +3438,7 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
3361
3438
  workspace: activeWorkspaceRoot,
3362
3439
  pid: process.pid,
3363
3440
  version: serverPackageVersion(),
3441
+ contracts: [CANVAS_APP_WRITE_CONTRACT],
3364
3442
  persistence: canvasState.persistenceHealth,
3365
3443
  });
3366
3444
  }
@@ -11,7 +11,7 @@
11
11
  */
12
12
 
13
13
  import type { CanvasAnnotation, CanvasNodeState, CanvasEdge } from './canvas-state.js';
14
- import { summarizeNodeForAgentContext } from './agent-context.js';
14
+ import { summarizeNodeForAgentContext } from '../shared/agent-context.js';
15
15
 
16
16
  // ── Types ────────────────────────────────────────────────────────────
17
17
 
@@ -73,7 +73,7 @@ export interface SpatialContext {
73
73
  // ── Helpers ──────────────────────────────────────────────────────────
74
74
 
75
75
  /** Euclidean distance between two node centers */
76
- function centerDistance(a: CanvasNodeState, b: CanvasNodeState): number {
76
+ export function centerDistance(a: CanvasNodeState, b: CanvasNodeState): number {
77
77
  const ax = a.position.x + a.size.width / 2;
78
78
  const ay = a.position.y + a.size.height / 2;
79
79
  const bx = b.position.x + b.size.width / 2;
@@ -77,7 +77,12 @@ export function buildWorkboardSpec(workItems: PmxAxWorkItem[]): JsonRenderSpec {
77
77
  };
78
78
  cardChildren.push(`${cardId}-detail`);
79
79
  }
80
- elements[cardId] = { type: 'Card', props: { title: item.title }, children: cardChildren };
80
+ // wrap-anywhere: an unbreakable token in a title must wrap, not spill into the next column.
81
+ elements[cardId] = {
82
+ type: 'Card',
83
+ props: { title: item.title, className: 'wrap-anywhere' },
84
+ children: cardChildren,
85
+ };
81
86
  columnChildren.push(cardId);
82
87
  }
83
88
  elements[columnId] = {
@@ -1,5 +1,5 @@
1
- import type { CanvasNodeState } from './canvas-state.js';
2
- import { getCanvasNodeKind } from '../shared/canvas-node-kind.js';
1
+ import type { CanvasNodeState } from '../server/canvas-state.js';
2
+ import { getCanvasNodeKind } from './canvas-node-kind.js';
3
3
 
4
4
  const DEFAULT_CONTEXT_TEXT_LENGTH = 700;
5
5
  const DEFAULT_WEBPAGE_CONTEXT_TEXT_LENGTH = 1600;
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Agent presence — the contract behind every agent surface in the rail chrome
3
- * (rail-chrome-v2 phase 2). Shared by server and client so the two cannot
4
- * drift. See design/rail-chrome-v2/PLAN.md, "Phase 2 contract".
2
+ * Agent presence — the contract behind every agent surface in the rail chrome.
3
+ * Shared by server and client so the two cannot drift. See
4
+ * docs/ax-state-contract.md, "Agent presence (contract)".
5
5
  *
6
6
  * Presence is DERIVED from feeds that already exist (AX activity ingest,
7
7
  * agent-originated mutations) plus an explicit set for adapters with richer
@@ -148,15 +148,3 @@ export function agentPhaseLabel(presence: Pick<AgentPresence, 'phase' | 'detail'
148
148
  return 'Idle';
149
149
  }
150
150
  }
151
-
152
- /**
153
- * Stable per-agent identity hue (0-359) from the writer key. Phase colors
154
- * kept telling the human WHAT an agent is doing while erasing WHO — two
155
- * thinking agents rendered identically. Identity rides the glyph/border;
156
- * phase stays on dots and labels.
157
- */
158
- export function agentIdentityHue(key: string): number {
159
- let hash = 5381;
160
- for (let i = 0; i < key.length; i++) hash = ((hash << 5) + hash + key.charCodeAt(i)) | 0;
161
- return ((hash % 360) + 360) % 360;
162
- }
@@ -0,0 +1,62 @@
1
+ import { z } from 'zod';
2
+
3
+ export const CANVAS_APP_URI = 'ui://pmx/canvas';
4
+ // Independent of package version: older builds can share the same version.
5
+ export const CANVAS_APP_WRITE_CONTRACT = 'pmx-embedded-workbench-v4';
6
+
7
+ const point = z.object({ x: z.number(), y: z.number() });
8
+ export const canvasAppSnapshotSchema = z.object({
9
+ boardId: z.string().nullable(),
10
+ boardName: z.string(),
11
+ boards: z.array(z.object({ id: z.string(), name: z.string() })),
12
+ // A digest of this projection, not the board's content-delta cursor.
13
+ revision: z.string(),
14
+ nodes: z.array(
15
+ z.object({
16
+ id: z.string(),
17
+ type: z.string(),
18
+ title: z.string(),
19
+ text: z.string(),
20
+ contentRevision: z.number().int().nonnegative(),
21
+ // Only standalone Markdown within the editing limit; never a truncated draft.
22
+ editableContent: z.string().optional(),
23
+ position: point,
24
+ size: z.object({ width: z.number(), height: z.number() }),
25
+ image: z.string().optional(),
26
+ }),
27
+ ),
28
+ edges: z.array(z.object({ id: z.string(), from: z.string(), to: z.string(), label: z.string() })),
29
+ pinnedNodeIds: z.array(z.string()),
30
+ });
31
+
32
+ export type CanvasAppSnapshot = z.infer<typeof canvasAppSnapshotSchema>;
33
+
34
+ /** Share only curated text, never image bytes or the whole board's contents. */
35
+ export function canvasAppContext(
36
+ snapshot: Pick<CanvasAppSnapshot, 'boardId' | 'boardName' | 'pinnedNodeIds'> & {
37
+ nodes: Pick<CanvasAppSnapshot['nodes'][number], 'id' | 'type' | 'title' | 'text' | 'contentRevision'>[];
38
+ },
39
+ selectedIds: string[],
40
+ focusedIds: string[] = [],
41
+ ) {
42
+ const existing = new Set(snapshot.nodes.map((node) => node.id));
43
+ const selected = new Set(selectedIds.filter((id) => existing.has(id)).slice(0, 20));
44
+ const pinned = new Set(snapshot.pinnedNodeIds.filter((id) => existing.has(id)).slice(0, 20));
45
+ const focused = new Set(focusedIds.filter((id) => existing.has(id)).slice(0, 20));
46
+ return {
47
+ boardId: snapshot.boardId,
48
+ boardName: snapshot.boardName,
49
+ selectedNodeIds: [...selected],
50
+ focusedNodeIds: [...focused],
51
+ pinnedNodeIds: [...pinned],
52
+ nodes: snapshot.nodes
53
+ .filter((node) => selected.has(node.id) || pinned.has(node.id) || focused.has(node.id))
54
+ .map((node) => ({
55
+ id: node.id,
56
+ type: node.type,
57
+ title: node.title,
58
+ contentRevision: node.contentRevision,
59
+ text: node.text.slice(0, 700),
60
+ })),
61
+ };
62
+ }
@@ -11,8 +11,8 @@
11
11
  */
12
12
 
13
13
  export const CANVAS_THEMES = [
14
- 'dark',
15
- 'light',
14
+ 'harbor',
15
+ 'daylight',
16
16
  'high-contrast',
17
17
  'midnight',
18
18
  'sepia',
@@ -28,7 +28,7 @@ export function isCanvasTheme(value: unknown): value is CanvasThemeName {
28
28
  return typeof value === 'string' && (CANVAS_THEMES as readonly string[]).includes(value);
29
29
  }
30
30
 
31
- export function normalizeCanvasThemeName(value: unknown, fallback: CanvasThemeName = 'dark'): CanvasThemeName {
31
+ export function normalizeCanvasThemeName(value: unknown, fallback: CanvasThemeName = 'harbor'): CanvasThemeName {
32
32
  return isCanvasTheme(value) ? value : fallback;
33
33
  }
34
34
 
@@ -43,8 +43,8 @@ export interface CanvasThemeMeta {
43
43
  }
44
44
 
45
45
  export const CANVAS_THEME_META: Record<CanvasThemeName, CanvasThemeMeta> = {
46
- dark: { label: 'Dark', scheme: 'dark', swatchBg: '#081524', swatchAccent: '#4BBCFF' },
47
- light: { label: 'Light', scheme: 'light', swatchBg: '#F4EFE6', swatchAccent: '#1A7ABF' },
46
+ harbor: { label: 'Harbor', scheme: 'dark', swatchBg: '#081524', swatchAccent: '#4BBCFF' },
47
+ daylight: { label: 'Daylight', scheme: 'light', swatchBg: '#F6F7F9', swatchAccent: '#1A7ABF' },
48
48
  'high-contrast': { label: 'High contrast', scheme: 'dark', swatchBg: '#000000', swatchAccent: '#00ffff' },
49
49
  midnight: { label: 'Midnight', scheme: 'dark', swatchBg: '#0A0D1C', swatchAccent: '#8B96FF' },
50
50
  sepia: { label: 'Sepia', scheme: 'light', swatchBg: '#F2E7D5', swatchAccent: '#B4632C' },
@@ -1,116 +0,0 @@
1
- import { attentionPrimaryNodeIds, attentionRegions, attentionSecondaryNodeIds } from '../state/attention-store';
2
- import { nodes } from '../state/canvas-store';
3
-
4
- interface Rect {
5
- left: number;
6
- top: number;
7
- width: number;
8
- height: number;
9
- }
10
-
11
- function getNodeRect(nodeId: string): Rect | null {
12
- const node = nodes.value.get(nodeId);
13
- if (!node) return null;
14
- return {
15
- left: node.position.x,
16
- top: node.position.y,
17
- width: node.size.width,
18
- height: node.size.height,
19
- };
20
- }
21
-
22
- function getRegionRect(nodeIds: string[]): Rect | null {
23
- const rects = nodeIds.map((nodeId) => getNodeRect(nodeId)).filter((rect): rect is Rect => rect !== null);
24
- if (rects.length === 0) return null;
25
-
26
- const minLeft = Math.min(...rects.map((rect) => rect.left));
27
- const minTop = Math.min(...rects.map((rect) => rect.top));
28
- const maxRight = Math.max(...rects.map((rect) => rect.left + rect.width));
29
- const maxBottom = Math.max(...rects.map((rect) => rect.top + rect.height));
30
- const padX = 54;
31
- const padY = 46;
32
-
33
- return {
34
- left: minLeft - padX,
35
- top: minTop - padY,
36
- width: maxRight - minLeft + padX * 2,
37
- height: maxBottom - minTop + padY * 2,
38
- };
39
- }
40
-
41
- function rectStyle(rect: Rect, radius: number): Record<string, string> {
42
- return {
43
- left: `${rect.left}px`,
44
- top: `${rect.top}px`,
45
- width: `${rect.width}px`,
46
- height: `${rect.height}px`,
47
- borderRadius: `${radius}px`,
48
- };
49
- }
50
-
51
- export function FocusFieldLayer() {
52
- const primaryNodeIds = Array.from(attentionPrimaryNodeIds.value);
53
- const secondaryNodeIds = Array.from(attentionSecondaryNodeIds.value);
54
- const regions = attentionRegions.value;
55
-
56
- if (primaryNodeIds.length === 0 && secondaryNodeIds.length === 0) return null;
57
-
58
- // A region that spans (nearly) the whole populated board distinguishes
59
- // nothing — pins far apart degrade the glow into a border around
60
- // everything. Skip those; the per-node halos still mark the pins.
61
- const all = [...nodes.value.values()];
62
- const boardArea =
63
- all.length > 0
64
- ? (Math.max(...all.map((n) => n.position.x + n.size.width)) - Math.min(...all.map((n) => n.position.x))) *
65
- (Math.max(...all.map((n) => n.position.y + n.size.height)) - Math.min(...all.map((n) => n.position.y)))
66
- : 0;
67
-
68
- return (
69
- <div class="attention-field-layer" aria-hidden="true">
70
- {regions.map((region) => {
71
- const rect = getRegionRect(region.nodeIds);
72
- if (!rect) return null;
73
- if (boardArea > 0 && rect.width * rect.height >= boardArea * 0.75) return null;
74
- return <div key={region.id} class="attention-field-region" style={rectStyle(rect, 42)} />;
75
- })}
76
- {secondaryNodeIds.map((nodeId) => {
77
- const rect = getNodeRect(nodeId);
78
- if (!rect) return null;
79
- return (
80
- <div
81
- key={`secondary-${nodeId}`}
82
- class="attention-field-node attention-field-secondary"
83
- style={rectStyle(
84
- {
85
- left: rect.left - 18,
86
- top: rect.top - 18,
87
- width: rect.width + 36,
88
- height: rect.height + 36,
89
- },
90
- 28,
91
- )}
92
- />
93
- );
94
- })}
95
- {primaryNodeIds.map((nodeId) => {
96
- const rect = getNodeRect(nodeId);
97
- if (!rect) return null;
98
- return (
99
- <div
100
- key={`primary-${nodeId}`}
101
- class="attention-field-node attention-field-primary"
102
- style={rectStyle(
103
- {
104
- left: rect.left - 24,
105
- top: rect.top - 24,
106
- width: rect.width + 48,
107
- height: rect.height + 48,
108
- },
109
- 30,
110
- )}
111
- />
112
- );
113
- })}
114
- </div>
115
- );
116
- }
@@ -1,23 +0,0 @@
1
- import type { CanvasNodeState } from '../types';
2
-
3
- /** Kind color per node type, as CSS tokens so every theme keeps its palette. */
4
- export const KIND_COLOR: Record<CanvasNodeState['type'], string> = {
5
- markdown: 'var(--c-accent)',
6
- 'mcp-app': 'var(--c-ok)',
7
- webpage: 'var(--c-warn)',
8
- 'json-render': 'var(--c-ok)',
9
- graph: 'var(--c-purple)',
10
- board: 'var(--c-accent)',
11
- prompt: 'var(--c-accent)',
12
- response: 'var(--c-ok)',
13
- status: 'var(--c-warn)',
14
- context: 'var(--c-muted)',
15
- ledger: 'var(--c-dim)',
16
- trace: 'var(--c-purple)',
17
- file: 'var(--c-accent)',
18
- diff: 'var(--c-ok)',
19
- mermaid: 'var(--c-purple)',
20
- image: 'var(--c-ok)',
21
- html: 'var(--c-warn)',
22
- group: 'var(--c-accent)',
23
- };