pmx-canvas 0.7.2 → 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 (251) 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 +60 -0
  10. package/Readme.md +44 -16
  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 +844 -710
  23. package/dist/canvas/index.js +92 -90
  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/json-render/index.js +55 -55
  29. package/dist/types/cli/commands/record.d.ts +1 -1
  30. package/dist/types/client/canvas/ChoiceMenu.d.ts +13 -0
  31. package/dist/types/client/canvas/NodeContextMark.d.ts +24 -0
  32. package/dist/types/client/canvas/SessionReceipt.d.ts +3 -1
  33. package/dist/types/client/icons.d.ts +1 -2
  34. package/dist/types/client/mcp-app/index.d.ts +1 -0
  35. package/dist/types/client/nodes/McpAppNode.d.ts +2 -1
  36. package/dist/types/client/nodes/ax-node-actions.d.ts +2 -2
  37. package/dist/types/client/nodes/markdown-sanitize.d.ts +2 -0
  38. package/dist/types/client/nodes/use-workbench-asset.d.ts +2 -0
  39. package/dist/types/client/nodes/viewer-runtime-state.d.ts +1 -0
  40. package/dist/types/client/state/attention-store.d.ts +0 -5
  41. package/dist/types/client/state/canvas-store.d.ts +2 -0
  42. package/dist/types/client/state/context-status-store.d.ts +53 -0
  43. package/dist/types/client/state/intent-bridge.d.ts +4 -1
  44. package/dist/types/client/state/near-pin-store.d.ts +16 -0
  45. package/dist/types/client/state/presence-store.d.ts +5 -0
  46. package/dist/types/client/state/session-store.d.ts +33 -3
  47. package/dist/types/client/state/workbench-transport.d.ts +16 -0
  48. package/dist/types/client/theme/tokens.d.ts +1 -3
  49. package/dist/types/client/types.d.ts +9 -0
  50. package/dist/types/json-render/renderer/index.d.ts +2 -1
  51. package/dist/types/json-render/server.d.ts +3 -1
  52. package/dist/types/mcp/app-server.d.ts +3 -0
  53. package/dist/types/mcp/canvas-access.d.ts +1 -1
  54. package/dist/types/mcp/canvas-app.d.ts +4 -0
  55. package/dist/types/mcp/workbench-app.d.ts +4 -0
  56. package/dist/types/server/agent-presence.d.ts +1 -1
  57. package/dist/types/server/ax-state.d.ts +1 -1
  58. package/dist/types/server/canvas-db.d.ts +8 -0
  59. package/dist/types/server/canvas-geometry.d.ts +25 -0
  60. package/dist/types/server/canvas-operations.d.ts +1 -11
  61. package/dist/types/server/canvas-state.d.ts +25 -3
  62. package/dist/types/server/context-brief.d.ts +8 -1
  63. package/dist/types/server/context-reads.d.ts +30 -7
  64. package/dist/types/server/index.d.ts +6 -1
  65. package/dist/types/server/mcp-app-runtime.d.ts +1 -0
  66. package/dist/types/server/operations/http.d.ts +2 -0
  67. package/dist/types/server/operations/invoker.d.ts +4 -2
  68. package/dist/types/server/operations/ops/nodes.d.ts +0 -6
  69. package/dist/types/server/server.d.ts +1 -0
  70. package/dist/types/server/spatial-analysis.d.ts +2 -0
  71. package/dist/types/shared/agent-context.d.ts +22 -0
  72. package/dist/types/shared/agent-presence.d.ts +3 -10
  73. package/dist/types/shared/ax-surface-protocol.d.ts +2 -0
  74. package/dist/types/shared/canvas-app.d.ts +54 -0
  75. package/dist/types/shared/themes.d.ts +1 -1
  76. package/docs/RELEASE.md +7 -3
  77. package/docs/agent-plugin.md +123 -0
  78. package/docs/ax-host-adapter-contract.md +13 -2
  79. package/docs/ax-state-contract.md +40 -0
  80. package/docs/cli.md +7 -2
  81. package/docs/design/AgentContext.dc.html +186 -0
  82. package/docs/design/Before.dc.html +208 -0
  83. package/docs/design/BoardPins.dc.html +151 -0
  84. package/docs/design/Chrome.dc.html +208 -0
  85. package/docs/design/Context.dc.html +239 -0
  86. package/docs/design/Flows.dc.html +181 -0
  87. package/docs/design/Home.dc.html +209 -0
  88. package/docs/design/HomeGraph.dc.html +211 -0
  89. package/docs/design/HomeMap.dc.html +117 -0
  90. package/docs/design/HomeTeam.dc.html +176 -0
  91. package/docs/design/Light.dc.html +28 -0
  92. package/docs/design/Main.dc.html +280 -0
  93. package/docs/design/NearPin.dc.html +260 -0
  94. package/docs/design/Pane600.dc.html +120 -0
  95. package/docs/design/Relations.dc.html +297 -0
  96. package/docs/design/Themes.dc.html +121 -0
  97. package/docs/design/TokenSystem.dc.html +136 -0
  98. package/docs/design/Tokens.dc.html +163 -0
  99. package/docs/design/Types1.dc.html +177 -0
  100. package/docs/design/Types2.dc.html +226 -0
  101. package/docs/design/Types3.dc.html +195 -0
  102. package/docs/design/Uses.dc.html +152 -0
  103. package/docs/design/Vision.dc.html +116 -0
  104. package/docs/design/Zoom.dc.html +82 -0
  105. package/docs/design/canvas.json +274 -0
  106. package/docs/design.md +214 -0
  107. package/docs/environment.md +1 -1
  108. package/docs/http-api.md +80 -15
  109. package/docs/mcp.md +198 -3
  110. package/docs/node-types.md +34 -10
  111. package/docs/plans/plan-009-tech-debt-backlog.md +1 -1
  112. package/docs/plans/plan-015-design-foundation-wave-0.md +177 -0
  113. package/docs/plans/plan-016-context-made-visible-wave-1.md +148 -0
  114. package/docs/product-vision-2026-09.md +47 -7
  115. package/docs/screenshot.png +0 -0
  116. package/docs/sdk.md +1 -0
  117. package/package.json +10 -4
  118. package/plugins/pmx-canvas/LICENSE +21 -0
  119. package/plugins/pmx-canvas/THIRD_PARTY_LICENSES.txt +331 -0
  120. package/plugins/pmx-canvas/assets/preview.png +0 -0
  121. package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/extension.mjs +930 -0
  122. package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/runtime.json +3 -0
  123. package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/runtime.mjs +194 -0
  124. package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/steering-delivery.mjs +79 -0
  125. package/plugins/pmx-canvas/mcp.json +12 -0
  126. package/plugins/pmx-canvas/plugin.json +24 -0
  127. package/plugins/pmx-canvas/runtime.json +3 -0
  128. package/plugins/pmx-canvas/runtime.mjs +194 -0
  129. package/plugins/pmx-canvas/start-mcp.mjs +51 -0
  130. package/plugins/pmx-canvas/tools.json +2560 -0
  131. package/skills/pmx-canvas/SKILL.md +31 -16
  132. package/skills/pmx-canvas/evals/evals.json +69 -0
  133. package/skills/pmx-canvas/references/full-reference.md +14 -10
  134. package/skills/pmx-canvas/references/github-copilot-app-adapter.md +20 -8
  135. package/skills/pmx-canvas/references/installing-pmx-canvas.md +76 -4
  136. package/skills/pmx-canvas-testing/SKILL.md +54 -4
  137. package/src/cli/commands/copilot.ts +5 -0
  138. package/src/cli/commands/view.ts +1 -1
  139. package/src/cli/index.ts +8 -3
  140. package/src/client/App.tsx +6 -1
  141. package/src/client/canvas/AgentPresenceLayer.tsx +3 -3
  142. package/src/client/canvas/AnnotationLayer.tsx +1 -1
  143. package/src/client/canvas/CanvasNode.tsx +208 -220
  144. package/src/client/canvas/CanvasViewport.tsx +6 -2
  145. package/src/client/canvas/ChoiceMenu.tsx +122 -0
  146. package/src/client/canvas/CommandBar.tsx +25 -23
  147. package/src/client/canvas/CommandPalette.tsx +3 -2
  148. package/src/client/canvas/ContextMenu.tsx +11 -0
  149. package/src/client/canvas/EdgeLayer.tsx +1 -1
  150. package/src/client/canvas/ExportDialog.tsx +33 -6
  151. package/src/client/canvas/FeedbackDialog.tsx +10 -8
  152. package/src/client/canvas/IntentLayer.tsx +2 -2
  153. package/src/client/canvas/Minimap.tsx +12 -7
  154. package/src/client/canvas/NodeContextMark.tsx +156 -0
  155. package/src/client/canvas/SessionPanel.tsx +4 -7
  156. package/src/client/canvas/SessionReceipt.tsx +114 -17
  157. package/src/client/canvas/ShortcutOverlay.tsx +6 -1
  158. package/src/client/canvas/SnapshotPanel.tsx +11 -2
  159. package/src/client/canvas/ToolRail.tsx +58 -108
  160. package/src/client/canvas/TopBar.tsx +16 -11
  161. package/src/client/icons.tsx +2 -12
  162. package/src/client/mcp-app/index.tsx +137 -0
  163. package/src/client/mcp-app/style.css +10 -0
  164. package/src/client/nodes/AttachmentNode.tsx +8 -1
  165. package/src/client/nodes/AxStepControls.tsx +1 -1
  166. package/src/client/nodes/ContextNode.tsx +9 -9
  167. package/src/client/nodes/ExtAppFrame.tsx +13 -8
  168. package/src/client/nodes/FileNode.tsx +8 -5
  169. package/src/client/nodes/ImageNode.tsx +4 -2
  170. package/src/client/nodes/InlineMarkdownEditor.tsx +2 -1
  171. package/src/client/nodes/LedgerNode.tsx +2 -2
  172. package/src/client/nodes/MarkdownNode.tsx +44 -9
  173. package/src/client/nodes/McpAppNode.tsx +22 -4
  174. package/src/client/nodes/PromptNode.tsx +5 -17
  175. package/src/client/nodes/ResponseNode.tsx +3 -15
  176. package/src/client/nodes/StatusNode.tsx +2 -2
  177. package/src/client/nodes/StatusSummary.tsx +3 -1
  178. package/src/client/nodes/TraceNode.tsx +6 -4
  179. package/src/client/nodes/WebpageNode.tsx +1 -1
  180. package/src/client/nodes/ax-node-actions.ts +2 -2
  181. package/src/client/nodes/iframe-document-url.ts +3 -2
  182. package/src/client/nodes/markdown-sanitize.ts +10 -0
  183. package/src/client/nodes/surface-url.ts +10 -0
  184. package/src/client/nodes/use-ax-surface-bridge.ts +4 -2
  185. package/src/client/nodes/use-surface-frame.ts +3 -1
  186. package/src/client/nodes/use-workbench-asset.ts +26 -0
  187. package/src/client/nodes/viewer-runtime-state.ts +78 -0
  188. package/src/client/state/attention-bridge.ts +0 -8
  189. package/src/client/state/attention-store.ts +0 -17
  190. package/src/client/state/canvas-store.ts +7 -2
  191. package/src/client/state/context-status-store.ts +66 -0
  192. package/src/client/state/human-store.ts +2 -0
  193. package/src/client/state/iframe-mode.ts +5 -0
  194. package/src/client/state/intent-bridge.ts +24 -6
  195. package/src/client/state/near-pin-store.ts +53 -0
  196. package/src/client/state/presence-store.ts +5 -10
  197. package/src/client/state/session-store.ts +106 -9
  198. package/src/client/state/sse-bridge.ts +26 -7
  199. package/src/client/state/theme-override.ts +1 -1
  200. package/src/client/state/workbench-transport.ts +36 -0
  201. package/src/client/theme/global.css +844 -710
  202. package/src/client/theme/surface-theme.css +151 -46
  203. package/src/client/theme/tokens.ts +7 -11
  204. package/src/client/types.ts +10 -0
  205. package/src/json-render/renderer/index.tsx +58 -38
  206. package/src/json-render/server.ts +79 -15
  207. package/src/mcp/app-server.ts +64 -0
  208. package/src/mcp/canvas-access.ts +14 -11
  209. package/src/mcp/canvas-app.ts +318 -0
  210. package/src/mcp/server.ts +1 -1
  211. package/src/mcp/workbench-app.ts +211 -0
  212. package/src/server/agent-presence.ts +45 -6
  213. package/src/server/ax-context.ts +1 -1
  214. package/src/server/ax-state.ts +1 -1
  215. package/src/server/board-export.ts +5 -2
  216. package/src/server/canvas-db.ts +46 -12
  217. package/src/server/canvas-geometry.ts +64 -0
  218. package/src/server/canvas-operations.ts +6 -24
  219. package/src/server/canvas-state.ts +105 -15
  220. package/src/server/canvas-validation.ts +19 -4
  221. package/src/server/context-brief.ts +39 -2
  222. package/src/server/context-reads.ts +86 -15
  223. package/src/server/demo-state.json +31 -31
  224. package/src/server/html-surface.ts +4 -5
  225. package/src/server/index.ts +19 -3
  226. package/src/server/mcp-app-runtime.ts +11 -0
  227. package/src/server/operations/composites.ts +4 -3
  228. package/src/server/operations/http.ts +4 -4
  229. package/src/server/operations/invoker.ts +13 -4
  230. package/src/server/operations/ops/ax-read.ts +1 -1
  231. package/src/server/operations/ops/ax-timeline.ts +37 -0
  232. package/src/server/operations/ops/boards.ts +1 -1
  233. package/src/server/operations/ops/canvas-wire.ts +3 -6
  234. package/src/server/operations/ops/intent.ts +31 -2
  235. package/src/server/operations/ops/json-render.ts +1 -1
  236. package/src/server/operations/ops/nodes.ts +8 -76
  237. package/src/server/operations/ops/query.ts +8 -2
  238. package/src/server/operations/ops/snapshots.ts +4 -1
  239. package/src/server/operations/ops/viewport.ts +29 -5
  240. package/src/server/operations/registry.ts +38 -4
  241. package/src/server/scope-fence.ts +55 -10
  242. package/src/server/server.ts +86 -6
  243. package/src/server/spatial-analysis.ts +2 -2
  244. package/src/server/workboard.ts +6 -1
  245. package/src/{server → shared}/agent-context.ts +2 -2
  246. package/src/shared/agent-presence.ts +3 -15
  247. package/src/shared/ax-surface-protocol.ts +3 -0
  248. package/src/shared/canvas-app.ts +62 -0
  249. package/src/shared/themes.ts +5 -5
  250. package/src/client/canvas/FocusFieldLayer.tsx +0 -116
  251. package/src/client/canvas/kind-colors.ts +0 -23
package/docs/mcp.md CHANGED
@@ -42,6 +42,196 @@ Add to your agent's MCP config:
42
42
 
43
43
  The canvas auto-starts on first tool call.
44
44
 
45
+ ## Private MCP App preview
46
+
47
+ `pmx-canvas --mcp-app` runs a **separate, experimental stdio profile**; the
48
+ 24-tool `--mcp` profile above is unchanged. Build from this checkout first:
49
+
50
+ ```bash
51
+ bun run build
52
+ PMX_CANVAS_WORKSPACE_ROOT=/absolute/project bun run src/cli/index.ts --mcp-app
53
+ ```
54
+
55
+ Configure a compatible MCP Apps host to launch that command. Pin the workspace
56
+ root explicitly; `PMX_CANVAS_URL` can attach it to an existing same-workspace
57
+ daemon. This profile shares the daemon's **one active board** across all app
58
+ instances and conversations. Choose or create a board in the app toolbar.
59
+
60
+ Attached daemons must advertise `pmx-embedded-workbench-v4` in `/health` on every
61
+ access. Matching package versions alone do not prove support for board/revision
62
+ guards and embedded execution restrictions. An incompatible daemon is refused
63
+ before any operation; restart it with this preview build and retry.
64
+
65
+ The standard `ui://pmx/canvas` resource is self-contained HTML with MIME type
66
+ `text/html;profile=mcp-app`. Only the launcher descriptor has ChatGPT-specific
67
+ `openai/ui` metadata, advertising both `global` (sidebar/fullscreen) and `thread`
68
+ (conversation panel) entrypoints. Actual availability depends on the host.
69
+
70
+ The embedded view sends selected, pinned and AX-focused nodes to the host,
71
+ including summaries of structured cards. Its context badge sits at the bottom
72
+ left to avoid the host composer. Export downloads use the host's `downloadFile`
73
+ capability; hosts without it show a local-retrieval message. Local-only trace
74
+ capture and shared undo/redo are unavailable in the embedded view. Save a
75
+ snapshot before restoring another one. Local surfaces can be expanded or
76
+ exported, but cannot be opened as public sites without a public URL.
77
+
78
+ | Tool | Input | Effect |
79
+ |------|-------|--------|
80
+ | `pmx_open_canvas` | `{}` | Open the real PMX workbench; standard UI resource plus ChatGPT entrypoints |
81
+ | `pmx_read_canvas` | `{}` | Read board list, identity, projection revision, node IDs/titles/types/content revisions, counts and pin IDs |
82
+ | `pmx_read_node` | `boardId`, `id` | Read one standalone note's full editable content, revision, position, size and incident edges in model-visible results |
83
+ | `pmx_add_note` | `boardId`, `title`, `content`, `x`, `y` | Add Markdown through `node.add`; rejects a stale board ID |
84
+ | `pmx_connect_nodes` | `boardId`, `from`, `to`, `type`, optional `label` | Add a `flow`, `depends-on`, `relation` or `references` edge through `edge.add` |
85
+ | `pmx_create_board` | `name` | Create an empty board without opening it |
86
+ | `pmx_open_board` | `id` (or null for Home), `expectedBoardId` | Switch the shared active board, refusing if it changed since the last read |
87
+ | `pmx_update_note` | `boardId`, `id`, `expectedContentRevision`, `title`, `content` | Edit standalone Markdown; reject stale revisions, file-linked notes and oversized originals |
88
+ | `pmx_move_node` | `boardId`, `id`, `x`, `y` | Move through `node.update`; group children follow |
89
+ | `pmx_pin_nodes` | `boardId`, `nodeIds`, `mode` (`add` or `remove`) | Curate pins without replacing unrelated pins |
90
+
91
+ Read tools declare read-only, non-destructive, closed-world annotations; the
92
+ three additive tools declare non-read-only, non-destructive, closed-world
93
+ annotations. Editing, moving, pin changes and board switching conservatively
94
+ declare destructive writes. These assistant tools are closed-world. Writes retain the
95
+ operation registry's scope fences, locks and attribution. **App clicks are
96
+ agent-attributed**: MCP does not prove a human clicked, so the app cannot bypass
97
+ these safeguards or acquire workbench authority.
98
+
99
+ An eleventh tool, **`pmx_workbench_request`**, is app-only (`ui.visibility: ["app"]`),
100
+ not an assistant tool. It accepts a relative PMX path, method, optional JSON body,
101
+ and captured `expectedBoardId`. An explicit allowlist routes workbench operations
102
+ through the same canonical registry; raw reads hydrate local assets. Replies stay
103
+ in `_meta.response`. It is not an arbitrary HTTP proxy, shell executor, external
104
+ MCP tool dispatcher, or human-authority bypass. This app-only tool declares
105
+ open-world behavior because webpage/image actions may fetch external URLs.
106
+
107
+ Embedded writes cannot supply external MCP launch metadata. App startup, board
108
+ switches and snapshot restore cannot launch saved external backends; open them
109
+ explicitly in local PMX. Undo/redo, flow materialization and approval resolve/reopen
110
+ (including node interactions) stay local. Embedded source attribution is fixed
111
+ to `mcp`. Child updates require permission for auto-sizing parent frames too;
112
+ the fence and human-lock checks share that target calculation. Manual frames
113
+ do not inherit child-update restrictions because their bounds remain fixed.
114
+ Child geometry changes re-fit only affected automatic ancestors, not unrelated
115
+ frames. PATCH aliases and partial coordinates resolve before destination checks;
116
+ bulk guards and execution filter invalid patches identically before deciding
117
+ which child positions override group translation.
118
+ Rendered Markdown is sanitized before entering the workbench document; active
119
+ HTML remains confined to sandboxed node frames.
120
+
121
+ The app bundles the **existing PMX workbench**, not a second editor: Home, board
122
+ navigation, tool rail, spatial nodes, edges, dragging, grouping, pins, themes,
123
+ and the full Markdown editor use their normal components. A transport adapter
124
+ replaces HTTP with host `tools/call`; the existing workbench poll cursor supplies
125
+ live updates every two seconds. Local assets travel over MCP, not localhost URLs
126
+ in the user's browser. Standalone Markdown edits carry captured board/content
127
+ revisions; a refused save keeps the current editor draft. Copy that draft before
128
+ closing the editor or switching boards; unsaved drafts do not survive remounts.
129
+
130
+ **Host constraints still apply.** Native PMX controls work without child frames.
131
+ HTML, Mermaid, JSON/graph and nested app renderers need the host to permit local
132
+ `srcdoc` frames. The portable MCP Apps default with empty `frameDomains` is
133
+ `frame-src 'none'`; there is no portable local-frame permission, so full rich
134
+ rendering in ChatGPT is an outstanding native-host check. The local browser test
135
+ explicitly enables local frames for that case. External assets remain subject
136
+ to host CSP. File uploads, file-linked editing, shell/webview automation, external
137
+ MCP tool calls, approval resolution, and trusted-human operations such as board
138
+ deletion require the local workbench. Unsupported writes are refused, never
139
+ silently granted human authority.
140
+
141
+ Selecting nodes shares up to 20 selections plus 20 pins, with 700 characters
142
+ per node, through `ui/update-model-context` when supported. This does **not**
143
+ start inference. Use **ChatGPT's own composer** to ask about the canvas; the
144
+ embedded workbench has no second composer and sends no `ui/message` requests.
145
+ The normal local PMX composer remains unchanged. Multiple conversations may
146
+ connect, but share one active board and the `mcp` writer label; distinct
147
+ ChatGPT conversation identities are not yet represented in PMX presence.
148
+ Board switches clear selection.
149
+ Context synchronization runs behind the scenes, with no hosted footer. Failed
150
+ updates retry automatically, including unchanged context, and only a host
151
+ acknowledgement marks a delivery successful. Launcher results do not hydrate
152
+ the workbench, so a delayed result cannot roll back the polling stream.
153
+
154
+ Bulk preview and image hydration stay in `_meta.canvas`, not model-visible
155
+ `structuredContent`; the canvas revision is a projection digest, not a content
156
+ cursor. Before editing or appending a note, the assistant must use `pmx_read_node`
157
+ for its complete source and content revision. Never replace content from a
158
+ 700-character selection excerpt. File-linked, oversized and non-Markdown nodes
159
+ are not editable through the assistant's focused note tools.
160
+
161
+ This is not a published or hosted ChatGPT plugin. Private ChatGPT testing needs
162
+ an account-supported local MCP connection or Secure MCP Tunnel; no HTTP MCP
163
+ endpoint is added here. Public deployment, authentication, tenant isolation,
164
+ directory submission and real ChatGPT validation remain separate work. Do not
165
+ expose the local workbench HTTP API as a public service.
166
+
167
+ ### Test privately in ChatGPT
168
+
169
+ This is the MCP connection test, not a directory publication or packaged marketplace
170
+ plugin. No plugin manifest or invented plugin ID is needed for this step. Follow
171
+ OpenAI's [connection guide](https://developers.openai.com/plugins/deploy/connect-chatgpt)
172
+ and [Secure MCP Tunnel guide](https://developers.openai.com/api/docs/guides/secure-mcp-tunnels):
173
+
174
+ 1. Enable **Settings → Security and login → Developer mode** in ChatGPT (subject
175
+ to your account/workspace policy).
176
+ 2. Create a tunnel in [Platform tunnel settings](https://platform.openai.com/settings/organization/tunnels)
177
+ and associate it with your target **ChatGPT workspace**. Tunnel creation needs
178
+ Tunnels Read + Manage; running/selecting it needs Read + Use.
179
+ 3. Install `tunnel-client` from the download link in those settings or its
180
+ [latest release](https://github.com/openai/tunnel-client/releases/latest).
181
+ Supply its runtime key privately as `CONTROL_PLANE_API_KEY` in your shell.
182
+ Do not paste the key into chat, commit it or put it in a command argument.
183
+ 4. Run the following from the root of **this modified checkout**, with Bun installed:
184
+
185
+ ```bash
186
+ bun install --frozen-lockfile
187
+ bun run build
188
+ export PMX_CANVAS_WORKSPACE_ROOT="$HOME/pmx-chatgpt-test"
189
+ mkdir -p "$PMX_CANVAS_WORKSPACE_ROOT"
190
+ export PMX_CANVAS_ALLOW_WORKSPACE_SPLIT=1
191
+ export PMX_CANVAS_DISABLE_BROWSER_OPEN=1
192
+ unset PMX_CANVAS_URL PMX_CANVAS_DB_PATH PMX_CANVAS_STATE_FILE
193
+ tunnel-client init \
194
+ --sample sample_mcp_stdio_local \
195
+ --profile pmx-canvas \
196
+ --tunnel-id "<your actual tunnel ID>" \
197
+ --mcp-command "bun run '$PWD/src/cli/index.ts' --mcp-app"
198
+ tunnel-client doctor --profile pmx-canvas --explain
199
+ tunnel-client run --profile pmx-canvas
200
+ ```
201
+
202
+ Keep the process running in this shell with the workspace variables set. Run
203
+ only one client for this stdio tunnel. This uses a disposable test workspace;
204
+ its data persists under `~/pmx-chatgpt-test/.pmx-canvas/`. To test an existing
205
+ board instead, explicitly set that project's workspace root.
206
+
207
+ For an **unpublished preview tarball**, install it into a clean directory with
208
+ `bun add /absolute/path/pmx-canvas-chatgpt-preview.tgz`; skip the checkout build
209
+ and use `bun run '$PWD/node_modules/pmx-canvas/src/cli/index.ts' --mcp-app` as
210
+ the MCP command above. The tarball includes the built app HTML. Do not use
211
+ `bunx pmx-canvas` for this preview: that fetches the published version.
212
+ 5. Open [ChatGPT Plugins](https://chatgpt.com/plugins), select **+**, name it
213
+ **PMX Canvas**, choose **Connection → Tunnel**, select your tunnel and create
214
+ the connection. Confirm discovery of **ten assistant tools**, including
215
+ `pmx_open_canvas`, plus the app-only `pmx_workbench_request` transport (eleven
216
+ descriptors in `tools/list`), then start a new conversation with it enabled.
217
+
218
+ Test both the conversation entrypoint and navigation sidebar launcher. Create a
219
+ board, add/edit/move a note, pin it, select it and send a question using
220
+ **ChatGPT's composer**. Selection alone must not start an answer. Ask ChatGPT to add a second
221
+ note using `pmx_add_note`; it should appear within a few seconds without replacing
222
+ the first. Reopen the app and verify persistence. With a second app/workbench
223
+ instance, verify that board switches clear selection and stale saves refuse
224
+ instead of overwriting another edit. Test HTML, Mermaid, and JSON/graph cards
225
+ separately and record any CSP frame denial; native controls passing does not
226
+ prove rich-frame support.
227
+
228
+ If a launcher is missing, record the ChatGPT account/workspace and discovered
229
+ `pmx_open_canvas` metadata; host entrypoint support is not proven by local tests.
230
+ After changing the server, restart the tunnel client, use **Refresh** on the
231
+ ChatGPT connection and start a new conversation. If the tunnel is missing, check
232
+ its workspace association and permissions first. Do not substitute the workbench
233
+ HTTP URL for an MCP endpoint.
234
+
45
235
  ## Composite tools (recommended)
46
236
 
47
237
  Action-discriminated tools that consolidate the single-purpose tools. Each maps
@@ -62,7 +252,7 @@ its `action` to the same operation the legacy tool used, so results are identica
62
252
  | `canvas_ax_state` | `get` · `set-focus` · `set-policy` · `report-capability` · `presence` · `set-presence` | `canvas_get_ax`, `canvas_set_ax_focus`, `canvas_set_ax_policy`, `canvas_report_host_capability` |
63
253
  | `canvas_ax_work` | `add` · `update` · `annotate` | `canvas_add_work_item`, `canvas_update_work_item`, `canvas_add_review_annotation` |
64
254
  | `canvas_ax_gate` | `request` · `resolve` · `await` × kind `approval` \| `elicitation` \| `mode` | `canvas_request_approval`, `canvas_resolve_approval`, `canvas_await_approval`, `canvas_request_elicitation`, `canvas_respond_elicitation`, `canvas_await_elicitation`, `canvas_request_mode`, `canvas_resolve_mode`, `canvas_await_mode` (9 → 1) |
65
- | `canvas_ax_timeline` | `read` · `record-event` · `add-evidence` · `send-steering` · `reads` | `canvas_get_ax_timeline`, `canvas_record_ax_event`, `canvas_add_evidence`, `canvas_send_steering` (`reads` is new — the context read log) |
255
+ | `canvas_ax_timeline` | `read` · `record-event` · `add-evidence` · `send-steering` · `reads` · `read-status` | `canvas_get_ax_timeline`, `canvas_record_ax_event`, `canvas_add_evidence`, `canvas_send_steering` (`reads` is the context read log; `read-status` is per node: last read, by whom, at which content revision) |
66
256
  | `canvas_ax_delivery` | `claim` (long-polls with `timeoutMs`) · `mark` | `canvas_claim_ax_delivery`, `canvas_mark_ax_delivery` |
67
257
  | `canvas_intent` | `signal` · `update` · `clear` | _(new — Ghost Cursor of Intent; no legacy standalone tool)_ |
68
258
  | `canvas_board` | `list` · `get` · `create` · `create-from` · `open` · `update` | Boards, explicit shared-workbench switching, README designation, and transactional inactive copies |
@@ -133,7 +323,7 @@ Announce the spatial move you are **about** to make so the canvas paints a faint
133
323
  pre-commit placeholder (a "ghost"). The human sees the next move forming — and can
134
324
  veto it — before the mutation lands.
135
325
 
136
- - `signal` — register an intent: `kind` (`create` \| `move` \| `connect` \| `remove` \| `edit`) plus the anchor it renders against (`position` for create/move, `nodeId` for move/edit/remove, `edge` for connect). Optional `label`, `reason`, `confidence` (0..1 → ghost opacity), `seq` (staged-batch ordering), `ttlMs` (default ~8s), and a stable `id` to update/clear later.
326
+ - `signal` — register an intent: `kind` (`create` \| `move` \| `connect` \| `remove` \| `edit`) plus the anchor it renders against (`position` for move, optional for create; `nodeId` for move/edit/remove; `edge` for connect). A create without `position` forms where the canvas will auto-place the node, and the linked create lands there. Optional `label`, `reason`, `confidence` (0..1 → ghost opacity), `seq` (staged-batch ordering), `ttlMs` (default ~8s), and a stable `id` to update/clear later.
137
327
  - `update` — patch a live intent by `id` (position/label/reason/confidence/ttlMs).
138
328
  - `clear` — abandon/dissolve it explicitly. Normal linked mutations settle automatically.
139
329
 
@@ -172,6 +362,11 @@ as a writer operation or refreshing agent presence. Viewport translation uses
172
362
  screen-space canvas-area coordinates (`screen = world * scale + viewport`). Fit
173
363
  dimensions and padding likewise refer to the canvas area after excluding the
174
364
  tool rail, top bar, and surrounding page chrome—not the full browser window.
365
+ The canvas area starts right of the 52 px tool rail and below the 44 px top bar
366
+ in the default layout, so world `(wx, wy)` lands at browser window point
367
+ `(52 + wx * scale + x, 44 + wy * scale + y)`. To set the camera directly, use
368
+ HTTP `POST /api/canvas/viewport` with `{x, y, scale}` or with `{center: {x, y},
369
+ scale?}` to centre a world point in the canvas area (see `docs/http-api.md`).
175
370
  Use `canvas_ax_state { action: "set-presence", cursor, focusNodeId }` when the
176
371
  agent intends to publish its own cursor or attention target.
177
372
 
@@ -185,7 +380,7 @@ legacy tools the composites replaced.
185
380
  | Tool | Description |
186
381
  |------|-------------|
187
382
  | `canvas_batch` | Run a batch of canvas operations with `$ref` support |
188
- | `canvas_pin_nodes` | Pin nodes to include in agent context |
383
+ | `canvas_pin_nodes` | Pin nodes to include in agent context (`nodeIds`, `mode` set/add/remove, optional `reason` shown to the human) |
189
384
  | `canvas_invoke_command` | Invoke a registry command (`pmx.plan`, `pmx.execute`, `pmx.promote-context`, `pmx.summarize`, `pmx.review`); records a `command` agent-event, unknown names rejected |
190
385
  | `canvas_ax_interaction` | Submit one capability-gated AX interaction envelope (`{ type, sourceNodeId, payload }`) that maps onto an AX operation; the server re-validates and clamps sandboxed surfaces to their own node |
191
386
  | `canvas_ingest_activity` | Ingest a harness-forwarded agent activity (tool/session event); the board auto-reacts with kind-driven, overridable defaults (failure → work item + review + evidence; `tool-result`+success → evidence). Makes AX bidirectional |
@@ -228,15 +228,39 @@ Specs preserve top-level `state` and element-level `repeat`, `watch`, and
228
228
  `$state` only reads a value. Nested repeats can use `statePath: { "$item": "tasks" }`
229
229
  to iterate the current item's tasks. Watchers run after changes, not on mount.
230
230
 
231
- **Seed form values through bindings, not literal `value` props.** In shadcn
232
- 0.21, `Input`, `Textarea`, `Select`, `Radio`, `Slider`, `ToggleGroup`, and
233
- `Tabs` ignore an unbound `value`; `ButtonGroup` ignores unbound `selected`.
234
- This includes read-only `$state` expressions. PMX returns a `warnings` array
235
- from `POST /api/canvas/json-render`, `POST /api/canvas/schema/validate`, and
236
- `canvas_render` actions `add-json-render` and `validate`. Warnings name the
237
- element/property; they do not reject or rewrite the spec. PMX does not invent
238
- state paths because repeated controls and existing actions may need distinct
239
- or deliberately shared state.
231
+ **Literal form values are bound for you.** In shadcn 0.21, `Input`,
232
+ `Textarea`, `Select`, `Radio`, `Slider`, `ToggleGroup`, and `Tabs` ignore an
233
+ unbound `value`, and `ButtonGroup` ignores an unbound `selected`, so the control
234
+ would render empty. During validation PMX rewrites a literal value on these
235
+ props into `{ "$bindState": "/form/<elementKey>" }` and seeds
236
+ `state.form.<elementKey>` with it (adding `-2`, `-3`, … if that field already
237
+ exists). This input:
238
+
239
+ ```json
240
+ { "type": "Input", "props": { "label": "Postcode", "value": "nw1 6xe" } }
241
+ ```
242
+
243
+ is stored as (bare elements are wrapped under the key `root`):
244
+
245
+ ```json
246
+ {
247
+ "root": "root",
248
+ "state": { "form": { "root": "nw1 6xe" } },
249
+ "elements": {
250
+ "root": {
251
+ "type": "Input",
252
+ "props": { "label": "Postcode", "name": "root", "value": { "$bindState": "/form/root" } }
253
+ }
254
+ }
255
+ }
256
+ ```
257
+
258
+ PMX does not rewrite `$state` or other expressions, controls inside a `repeat`
259
+ (one path would be shared by every item), or specs whose `state.form` is not an
260
+ object. For those it returns a `warnings` array naming the element/property from
261
+ `POST /api/canvas/json-render`, `POST /api/canvas/schema/validate`, and
262
+ `canvas_render` actions `add-json-render` and `validate`. Bind them yourself
263
+ when the field needs a known path for actions, watchers, or `$state` reads:
240
264
 
241
265
  ```json
242
266
  {
@@ -259,7 +283,7 @@ Inside repeats, use `$bindItem` with seeded item data. `Checkbox`/`Switch`
259
283
  honor literal `checked`, `Toggle` honors literal `pressed`, and `Tabs` honors
260
284
  `defaultValue` for an uncontrolled initial tab. `Pagination` reads `page`
261
285
  directly; `DropdownMenu.value` is an output binding for selection, not a
262
- displayed selection. These do not require the same ignored-value warning.
286
+ displayed selection. These are neither rewritten nor warned about.
263
287
  Use two-way bindings whenever edits or form checks must participate in state.
264
288
 
265
289
  Cards keep their body in `children` and accept
@@ -82,7 +82,7 @@ The remaining items were re-analyzed with pros/cons and tiered; the maintainer a
82
82
 
83
83
  ## Added during rail-chrome-v2 (2026-08-23, phases 1–5)
84
84
 
85
- Recorded while the redesign's phases land so nothing rides in silently. Plan: `design/rail-chrome-v2/PLAN.md`.
85
+ Recorded while the redesign's phases land so nothing rides in silently. The redesign's plan was retired into `docs/design.md` and `docs/ax-state-contract.md` (2026-10-04).
86
86
 
87
87
  | Item | What's open |
88
88
  |---|---|
@@ -0,0 +1,177 @@
1
+ # Plan 015 — Design foundation (wave 0)
2
+
3
+ **Status:** Wave 0 done (2026-10-05): steps 1–4 are on main.
4
+ **Date:** 2026-10-05
5
+ **Source:** [design.md](../design.md#build-plan) wave 0 ("ships alone, first"); vision moves 5, 9, 10.
6
+ Drawings: [`TokenSystem`](../design/TokenSystem.dc.html), [`Tokens`](../design/Tokens.dc.html),
7
+ [`Themes`](../design/Themes.dc.html), [`Chrome`](../design/Chrome.dc.html),
8
+ [`Context`](../design/Context.dc.html), [`Main`](../design/Main.dc.html) / [`Light`](../design/Light.dc.html).
9
+
10
+ ## Why first
11
+
12
+ Every later wave paints with the meaning colours, the scale and the font. Today none of them
13
+ exist: pins and agent attention borrow `--c-warn`, the light theme hard-codes `#4BBCFF` for pins,
14
+ Plex is named in `--font` but not bundled (controls fall back to Arial or the system font), and
15
+ there is no type, spacing, radius or elevation scale and no check that keeps any of it honest.
16
+
17
+ ## Wave done when (from design.md)
18
+
19
+ No control renders in a fallback font; the token test passes for all nine themes; the reference
20
+ cases pass at 600, 1024 and 1920 px; the demo board is regenerated.
21
+
22
+ ## Steps
23
+
24
+ Each step is its own commit and leaves the suite green.
25
+
26
+ ### 1. Meaning and scale tokens, with the token test (no component changes)
27
+
28
+ - Add the meaning layer to every theme block in `global.css` and `surface-theme.css`:
29
+ `--c-pin`, `--c-agent`, `--c-rel-supports`, `--c-rel-contradicts`, `--c-rel-cites`,
30
+ `--c-rel-derived`, `--c-rel-informs`, `--c-rel-related`. `--c-warn` already exists; it takes the
31
+ drawn value (changes Light `#c89b2a` → `#94600A` and Sepia `#A97B1E` → `#8A5A00`, with their
32
+ fixed `--c-warn-*` copies; every other theme already matches).
33
+ - Values come from `Themes.dc.html` `renderVals()`. Where the drawing is silent (cites, derived,
34
+ informs, related outside Harbor) they are tuned here — see the table below.
35
+ - Add the scale to `:root` in both files: `--fs-meta|ui|body|title|heading` (11/12/13/15/20),
36
+ `--space-1|2|3|4|6|8` (4/8/12/16/24/32), `--r-control|node|overlay|pill` (6/10/14/999),
37
+ `--e-1`, `--e-2` (shadows as drawn on `Tokens.dc.html`).
38
+ - Token test (`tests/unit/theme-meaning-tokens.test.ts`), all nine themes:
39
+ - every meaning token is present, in both files, with the same value;
40
+ - each has at least 3:1 contrast on that theme's `--c-panel`;
41
+ - meaning colours are apart: OKLab distance ≥ 0.06 (≈3× a just-noticeable difference)
42
+ between `pin`/`agent`/`warn`, between each of those and every relation ink, and between
43
+ relation inks.
44
+ - Nothing consumes the new tokens yet, so nothing on screen changes except the warning amber in
45
+ Light and Sepia.
46
+
47
+ **Verify:** `bun run test:unit` (token + surface sync tests), `bun run build`, `bun run lint`.
48
+
49
+ ### 2. Bundle IBM Plex; every control inherits the font
50
+
51
+ - Ship Plex Sans (400/500/600) and Plex Mono (400/500) woff2 with the client bundle, `@font-face`
52
+ in `global.css`, no CDN. Confirm the npm `files` list carries them.
53
+ *Done:* `@fontsource` packages are dev dependencies; `scripts/copy-fonts.ts` copies the latin
54
+ and latin-ext subsets (192 KB) and the OFL licence to `dist/canvas/fonts/`, which ships. The
55
+ single-file MCP app inlines them as data URIs. HTML nodes, json-render and exports still use the
56
+ `--font-sans`/`--font-mono` aliases without the faces until the frame host (wave 4).
57
+ - Rename `--font`/`--mono` to `--font-ui`/`--font-code` (no aliases); `button, input, select,
58
+ textarea { font: inherit }`; remove Arial fallbacks.
59
+ - Check: a browser test that every visible control computes `--font-ui` and that the Plex faces
60
+ are loaded (`document.fonts`).
61
+ *Done:* in `reference-pane.pw.ts`. Before: 365 of 400 controls on the demo board rendered in
62
+ Arial and no Plex face loaded; after: 0 of 389, all five faces loaded.
63
+
64
+ ### 3. Pins and agent marks move to their meaning tokens
65
+
66
+ Split into three slices after reading the code (2026-10-05):
67
+
68
+ **3a. Node states and the context pin (done).** Built to `Context.dc.html` and `Chrome.dc.html`:
69
+ - In context: 1.5 px `--c-pin` border, header tinted 12% pin, pin-blue kind icon, and the header
70
+ pin control always visible — outline pin in a circle when out of context, the filled `--c-pin`
71
+ badge (`--c-on-pin` glyph, new token) when in. The light theme's hard-coded `#4BBCFF` overrides
72
+ are gone.
73
+ - The amber "attention" halo was not agent activity: it was semantic attention derived from pins
74
+ (primary = the pinned nodes, secondary = their unpinned neighbours, plus a glow around pinned
75
+ clusters). Decided with the maintainer: pin style only — the halos, neighbour halos and the
76
+ focus field (`FocusFieldLayer`) are removed. The change pulse stays, in the neutral accent.
77
+ - The violet agent bar (2.5 px `--c-agent` across the header) marks live agent edits
78
+ (`agent-mutating`), replacing the accent-blue shimmer; wave 1 extends it to read, created and
79
+ edited. The design session corrected the Context board's "Today" row to match.
80
+ - Selection is a ring with a gap (3 px background, 2 px accent) outside the border, so pin styling
81
+ survives it; base nodes take `--r-node` and `--e-1`.
82
+
83
+ **3b. Node header overflow (done).** Per `Chrome.dc.html`: expand and the context pin always show;
84
+ ⋯ and × appear on hover or keyboard focus. ⋯ opens the existing node menu (the same one as
85
+ right-click), which already holds Collapse, Rename and Delete; "Open in new tab" (open as site)
86
+ moves into it from the header. The drawn "Ask agent" entry has no function yet and waits for one;
87
+ "Set as README" stays in the markdown card footer for now.
88
+
89
+ **3c. The rest of the old colours (done).**
90
+ - Every remaining `--c-warn` use was sorted by meaning. Pin blue: the command-bar chips, the
91
+ context pin bar, the expanded overlay's pinned state and toggle, the pin HUD and the "Context
92
+ updated" toast. Agent violet: the "agent yielded" pill and a status node's active tool.
93
+ `--c-rel-informs`: `depends-on` edges. Neutral (accent or muted): generic toast and history
94
+ borders, cluster/neighbourhood toasts, group drop feedback, the budget bar at rest, "saving",
95
+ webpage loading, the "required" source tag. Real warnings stay amber: approval gates and
96
+ waiting phases, blocked status, reconnecting, budget warnings, image and app warnings, pending
97
+ asks.
98
+ - Tints are `color-mix(in srgb, var(--c-…) N%, transparent)`; the fixed alpha copies,
99
+ `--c-warn-alt`, `--c-glow-accent` and `--c-thinking` are gone (`--c-agent` replaces it).
100
+ - `tests/unit/raw-hex-guard.test.ts`: no hex colour in client code or outside `global.css`'s theme
101
+ blocks. Allowlisted as content, not theme: group frame colours, the annotation default, the
102
+ white behind webpage previews, and the embedded Mermaid viewer's fallback (goes in wave 4).
103
+
104
+ **3d. Kind colours and the writer palette (done).** Per the design session's rule in `design.md`
105
+ (2b74c7b4): kinds carry no colour. Type icons, group kind dots and minimap rectangles are
106
+ `--c-muted`; the minimap marks only meaning — pinned nodes in `--c-pin`, nodes an agent is
107
+ editing in `--c-agent`. `KIND_COLOR`, the `--kind-accent` rules and `--c-purple` are gone (agent
108
+ chrome that borrowed purple now uses `--c-agent`). Writers are told apart by name and initial: the
109
+ writer palette and the per-agent `agentIdentityHue` are gone; an agent's cursor, chip, minimap dot
110
+ and activity rows take `--c-agent`, a subagent's `--c-subagent`. Trace categories go neutral except
111
+ subagent rows.
112
+
113
+ ### 4. Theme renames and the wave's definition of done (done)
114
+
115
+ - `dark` → `harbor` (label Harbor, still the default) and `light` → `daylight` (label Daylight) in
116
+ `src/shared/themes.ts`, the CSS selectors, the defaults (state, persistence, client), the command
117
+ palette toggle (now by scheme), the CLI help, the bundled Copilot extension (`?theme=daylight`),
118
+ docs, skills, CLAUDE.md and AGENTS.md. No aliases: a stored or passed `dark`/`light` falls back
119
+ to Harbor, so anyone who had chosen the light theme picks Daylight once.
120
+ - Viewer scheme parameters stay `dark`/`light` (the json-render viewer and embedded MCP apps speak
121
+ schemes, not theme ids); the static export maps its scheme to Harbor or Daylight for surface
122
+ documents.
123
+ - Daylight takes the drawn neutral surfaces from `Main.dc.html` (`#F6F7F9` / `#FFFFFF` /
124
+ `#F2F4F7` / `#DDE2E9`, text `#0B1726`, ok `#1E7F51`, danger `#B8344A`) instead of beige.
125
+ - Definition of done: reference cases at 600, 1024 and 1920 px in the e2e gate; the token test
126
+ covers all nine themes; the demo board regenerated (deterministic: two runs, same file).
127
+
128
+ ## Step 1 colour table
129
+
130
+ Drawn values are from `Themes.dc.html`; *tuned* values are chosen in this plan.
131
+
132
+ | Theme | pin | agent | warn | supports | contradicts | cites | derived | informs | related |
133
+ |---|---|---|---|---|---|---|---|---|---|
134
+ | Harbor (`dark`) | `#4BBCFF` | `#B388FF` | `#f4c542` | `#5FCFC0` | `#E59B6B` | `#7C9CFF` | `#D5DEEA` | `#E58FB0` | `#8ea3bd` |
135
+ | Daylight (`light`) | `#1A7ABF` | `#7C4DDB` | `#94600A` | `#1E8C80` | `#B8572A` | *`#4257C9`* | *`#4A5566`* | *`#B03F72`* | *`#5c6b80`* |
136
+ | High contrast | `#00ffff` | `#e040fb` | `#ffff00` | `#00e5c0` | `#ff8a00` | *`#8c9eff`* | *`#ffffff`* | *`#ff80ab`* | *`#aaaaaa`* |
137
+ | Midnight | Harbor | Harbor | `#F5C452` | Harbor | Harbor | Harbor | Harbor | Harbor | *`#8D96BC`* |
138
+ | Sepia | `#1F6FA8` | `#7046A6` | `#8A5A00` | `#1E8C80` | `#A3461C` | *`#3D50B0`* | *`#4E4433`* | *`#9C3A66`* | *`#6B6A66`* |
139
+ | Arctic | `#6EC1F5` | Harbor | `#E8C476` | Harbor | Harbor | Harbor | Harbor | Harbor | *`#939DB1`* |
140
+ | Ember | Harbor | Harbor | `#F5B944` | Harbor | Harbor | Harbor | Harbor | Harbor | *`#96948D`* |
141
+ | Forest | Harbor | Harbor | `#E0B84F` | `#4FC9D9` | Harbor | Harbor | Harbor | Harbor | *`#8BA695`* |
142
+ | Volt | Harbor | Harbor | `#E4C465` | Harbor | Harbor | Harbor | Harbor | Harbor | *`#9CA49C`* |
143
+
144
+ "Related" is the theme's own `--c-muted` except in Sepia, where muted brown sits too close to the
145
+ warning amber.
146
+
147
+ ## Decided 2026-10-05
148
+
149
+ - **Tight pairs stay as drawn.** The closest drawn pairs (OKLab distance) pass the 0.06 floor:
150
+ Forest supports/pin 0.075, Daylight contradicts/warning 0.075, Sepia contradicts/warning 0.070.
151
+ The alternatives were compared side by side and rejected: Forest teal `#5FCFC0` lands on the
152
+ done green (0.062), and a redder contradicts in the light themes lands on `--c-danger`. Sepia
153
+ contradicts is already close to its error red (0.038); red is not a meaning colour, so the
154
+ test allows it.
155
+ - **Light-theme elevation.** Daylight and Sepia override `--e-1`/`--e-2` with ink-tinted shadows
156
+ (10/8% and 18%) instead of the drawn black ones; recorded as the one per-theme scale exception
157
+ in `design.md`.
158
+
159
+ ## Decided 2026-10-06
160
+
161
+ - **Folders, not levels.** Folders are the one browsing hierarchy; portals and links stay a graph
162
+ the agent navigates (links/backlinks on `canvas://boards`) and Map/Graph draw (vision move 0,
163
+ `design.md`). Unblocks wave 2.
164
+ - **The theme rename ships as a breaking change** (no aliases): the release changelog lists it
165
+ under Breaking, with `--theme=light`, `PMX_CANVAS_THEME=light`, a saved Light choice and
166
+ `?theme=light` all falling back to Harbor.
167
+
168
+ ## Open
169
+
170
+ - **Near a pin (decided 2026-10-05).** Nodes near a pin (up to 5 within 600 px,
171
+ `findNeighborhoods`) are treated as related, but the agent gets them only as titles, in
172
+ `canvas://pinned-context` and `canvas://spatial-context`; the main brief `canvas://context` does
173
+ not include them. Decided with the maintainer: no near-a-pin mark on the board until the brief
174
+ carries neighbours (vision move 1's ranker). The mark and that brief change ship together, in
175
+ wave 1; the design session draws the state ahead of it. Not part of wave 0. **Built** in plan 016
176
+ slice 4a (brief + chip); the hover lines, drag preview and Updates entry follow in 4b.
177
+
@@ -0,0 +1,148 @@
1
+ # Plan 016 — Context made visible (wave 1)
2
+
3
+ **Status:** Slices 1–3 and 4a done (2026-10-05); 4b (tethers, drag preview, Updates entry) next.
4
+ **Date:** 2026-10-05
5
+ **Source:** [design.md](../design.md#build-plan) wave 1; vision Part 1 bet (item 4), moves 0a, 2, 7.
6
+ Drawings: [`AgentContext`](../design/AgentContext.dc.html), [`Context`](../design/Context.dc.html),
7
+ [`NearPin`](../design/NearPin.dc.html), [`BoardPins`](../design/BoardPins.dc.html),
8
+ [`Home`](../design/Home.dc.html) (Folders).
9
+
10
+ ## Done when (from design.md)
11
+
12
+ From the board alone, a person can answer "did the agent read what I pinned, and is its copy
13
+ current?" The evaluation in `docs/evals/` reads the same data.
14
+
15
+ ## What exists and what is missing
16
+
17
+ | Need | Today | Gap |
18
+ |---|---|---|
19
+ | Who wrote a node | `createdBy` / `lastEditedBy` per node, per-board `contentRevision` (plan 014) | none |
20
+ | What an agent read | `context_reads`: per read, which **pinned** ids were in the payload (plan 011) | every delivered node, and the content revision it had when delivered |
21
+ | Who pinned a node | `context_pins (board_id, node_id)` | who, when, and an optional why |
22
+ | Board pins | none | `board_pins`, brief tiers, `canvas_board pin/unpin` in all four layers |
23
+ | Neighbours in the brief | titles only, in `canvas://pinned-context` / `spatial-context` | the brief carries them (title + summary, reason "near <pin>") |
24
+
25
+ ## Slices
26
+
27
+ Each slice is its own commit and leaves the suite green. Design ships with its function: no mark
28
+ appears before the data that backs it.
29
+
30
+ ### 1. Data: what was read, at which revision, and who pinned (no UI)
31
+
32
+ Split in two: 1a (reads) is enough for read / not read / changed since read; 1b (pin attribution)
33
+ adds "pinned by the agent".
34
+
35
+ - **1a, done.** `context_reads` records every node whose serialized form was delivered (not just
36
+ pins) and its `contentRevision` at read time, in a `read_nodes` column. Same "delivered" rule as
37
+ plan 011 (a serialized node carrying the id; a bare id list or a title does not count). The reader
38
+ side reports every node-shaped id it delivered (`readNodeIds`, so an MCP server attached to a
39
+ daemon only posts the record); the server keeps the ids that are nodes on the read board and
40
+ stamps their revisions. `ax.reads.status` (`GET /api/canvas/ax/context-status`,
41
+ `canvas_ax_timeline { action: "read-status" }`, `PmxCanvas.getNodeReadStatus`) returns, per node,
42
+ the latest agent read: when, by whom, at which revision.
43
+ - **1b, done.** Pins record who pinned them (`pinnedBy`, the same actor attribution as node
44
+ authorship: a person only with the workbench token), when, and an optional `reason`
45
+ (`canvas_pin_nodes` / `POST /api/canvas/context-pins` / `setContextPins(..., reason)`). Pins that
46
+ stay keep their attribution; undo restores it; it persists with the board and its snapshots
47
+ (`meta` on `context_pins` and `snapshot_pins`). Pins from before have an unknown pinner. The read
48
+ status returns `pins` alongside `nodes`.
49
+ - `context_pins` gains `pinned_by` (actor attribution, same shape as `createdBy`), `pinned_at` and an
50
+ optional `reason`. Pins made before this have an unknown pinner, not a guessed human.
51
+ - One server read model, `nodeContextStatus(boardId)`: per node, last read (when, by whom, revision
52
+ read), current revision, pinned (by whom, when, why). Served over HTTP and SDK and MCP alongside
53
+ the existing context-reads surface (four layers in step); the workbench reads it through SSE.
54
+ - **Check:** unit tests for delivered-all-nodes, revision capture, pin attribution through every
55
+ write path (workbench → human, agent writes → agent id/source), and the status model.
56
+
57
+ ### 2. Node marks: read, not read yet, changed since read, pinned by the agent, created, edited
58
+
59
+ Built to `AgentContext.dc.html`: eye + "read" (hover: who, when); eye-off + "not read" on a pin the
60
+ agent has not loaded since it was pinned; amber warn + "changed since read" when the current revision
61
+ is newer than the revision read; violet dot on the pin badge when an agent pinned it (hover: why);
62
+ "by <agent>" byline on agent-created nodes until a person edits them; violet bar + "edited" for the
63
+ session, with "see change" opening the diff and undo.
64
+ - **Check:** client tests per state; e2e at 600/1024/1920 that a pinned-then-changed node shows
65
+ "changed since read" after an agent read, and clears after the next read.
66
+ - **Done.** Decided with the maintainer (2026-10-05): the read / not read / changed since read marks
67
+ show on **pinned** nodes only (a full-layout read delivers every node, so marking all of them
68
+ would cover the board); unpinned reads go to the lens (slice 3). "by <agent>" and "edited" stay
69
+ until a person edits the node. One chip per node (`NodeContextMark`), glyph + word, explained
70
+ on hover; agent pins get the violet dot. The workbench refetches the read status on connect, on
71
+ pin changes and when a read lands (`context-status-changed`, sent to the browser only), and
72
+ ignores an answer for a board it has left. Not built: "see change" (diff and undo of an agent
73
+ edit) needs stored previous content, which revisions do not keep yet.
74
+ - **Note.** The demo board is generated through the API as agent writes, so its nodes show
75
+ "by sdk" until edited.
76
+
77
+ ### 3. After a session: the agent-activity lens and receipt
78
+
79
+ The lens from `AgentContext.dc.html`: counts (read, created, edited, pinned) and the receipt rows
80
+ with their actions (Unpin, Undo). Data from presence activity, revisions and slice 1.
81
+ - **Done.** The existing session receipt (`agent-session-ended`) gains `context`: nodes read since
82
+ the session attached (presence now records `startedAt`), pins the agent made with their reason,
83
+ nodes it created and edited (the pre-session snapshot diff, filtered by authorship), and pinned
84
+ nodes changed since their latest read. A session that only read or pinned now gets a receipt.
85
+ The receipt card shows an amber line for out-of-date pins, Read / Pinned / Created / Edited rows
86
+ (Unpin on agent pins) and the lens switch "Dim untouched nodes". The read mark's hover says how
87
+ many times a node was read. Reads are counted per board in the session window, not per reader:
88
+ on a board with two agents at once, each receipt includes the other's reads.
89
+ The receipt also reads the board open when the session ends; a session whose board the person
90
+ switched away from mid-session reports the newly open board (known limit, 0.8.0 review).
91
+ - **Not built:** per-row Undo of an edit (needs stored previous content); the pre-session snapshot
92
+ restore still undoes the whole session. The drawn bottom-bar placement of the lens is folded into
93
+ the receipt card, which already sits where session results appear.
94
+
95
+ ### 4. Near a pin: neighbours in the brief, and the near mark
96
+
97
+ - The brief (`canvas://context`) carries each pin's neighbours (up to 5 within 600 px, nearest
98
+ first) as title + short summary, reason "near <pin title>", never full content; they share the
99
+ budget after pinned cards.
100
+ - The `NearPin.dc.html` mark: dotted "near" chip, tethers on hovering a pin, the drag preview and
101
+ the single Updates entry that replaces "Neighborhood changed". "read" from slice 2 then also
102
+ covers neighbours honestly (title + summary delivered).
103
+ - **4a, done.** The brief carries `near` entries right after the pinned cards (each whole or
104
+ absent, title + summary capped at 280 characters, `near: { pinNodeId, pinTitle }`); a neighbour
105
+ that changed since the cursor arrives in full as `changed` instead; a first read delivers every
106
+ node in full, so it has no near entries. The client computes the same `findNeighborhoods` and
107
+ shows the dotted "near" / "near N" chip on unpinned nodes, with the pins and distances on hover.
108
+ - **4b, next.** Tethers and the 600 px radius when hovering a pin, the drag preview, the single
109
+ Updates entry that replaces "Neighborhood changed", the context chip's "N cards · M near" count,
110
+ and "Pin to send content" in the chip's hover. The maintainer agreed the design (2026-10-06) on
111
+ the condition that performance stays good:
112
+ - **Budget.** No added work per drag frame beyond the neighbour calculation for the board
113
+ (measured 0.8 ms at 1,000 nodes / 40 pins; guarded at < 8 ms by
114
+ `tests/unit/near-pin-performance.test.ts`) and the dragged card against the pins (O(pins)).
115
+ - **No fan-out.** The near map keeps its identity while no card's set of pins changes, so the
116
+ chips re-render only when a neighbourhood does, not on every frame (done ahead of 4b, guarded
117
+ by a client test). Distances, which change every frame, are computed only for an open hover.
118
+ - **Lines and radius** are drawn only while a pin is hovered or selected, for that pin alone, in
119
+ one SVG layer; nothing renders for pins at rest.
120
+ - **Check.** A headed drag on a 300-node board with 20 pins records no long task (> 50 ms)
121
+ attributable to near-a-pin.
122
+
123
+ ### 5. Board pins
124
+
125
+ Per vision move 0a and `BoardPins.dc.html`: `board_pins` table (board, who, when); `canvas_board`
126
+ `pin`/`unpin` across PmxCanvas, HTTP, MCP and CLI; the tiered brief (active board's pins, then each
127
+ pinned board's README + pinned cards, then discovery); the context chip counts "N cards · M boards";
128
+ Home's pinned-boards section per `Home.dc.html`. Read status per board from slice 1.
129
+
130
+ ### 6. The curation evaluation
131
+
132
+ `docs/evals/curation-effect.md`: one fixed task, run with the brief delivered on a curated and an
133
+ uncurated board, scored on criteria written before the first run (vision Part 1, item 4). It reads
134
+ the same `context_reads` data as slice 1 to confirm delivery before scoring effect. Running it
135
+ needs the maintainer's real agent sessions; the decision rule is the vision's.
136
+
137
+ ## Out of wave 1
138
+
139
+ - "Suggested by the agent" (dashed node, accept/reject): needs agent suggestions as a function
140
+ (vision move 3, wave 5). The drawing exists; it ships with that function.
141
+ - Home Map and Graph (wave 2).
142
+
143
+ ## Defaults (change if the maintainer disagrees)
144
+
145
+ - **Read by whom.** Any agent reader on that board counts; the mark shows the most recent reader
146
+ and time on hover. The workbench's own reads never count.
147
+ - **Changed since read** compares against the most recent read of that node by any agent.
148
+ - **Unknown pinner.** Existing pins show no agent dot; only pins attributed to an agent get it.