pmx-canvas 0.7.3 → 0.8.2

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 (244) 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 +64 -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 +726 -674
  23. package/dist/canvas/index.js +90 -88
  24. package/dist/canvas/mcp-app.html +7879 -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 +81 -1
  28. package/dist/json-render/index.js +1 -1
  29. package/dist/types/cli/commands/record.d.ts +1 -1
  30. package/dist/types/client/canvas/NodeContextMark.d.ts +24 -0
  31. package/dist/types/client/canvas/SessionReceipt.d.ts +3 -1
  32. package/dist/types/client/icons.d.ts +1 -2
  33. package/dist/types/client/mcp-app/index.d.ts +1 -0
  34. package/dist/types/client/nodes/HtmlNode.d.ts +1 -0
  35. package/dist/types/client/nodes/ax-node-actions.d.ts +2 -2
  36. package/dist/types/client/nodes/markdown-sanitize.d.ts +2 -0
  37. package/dist/types/client/nodes/use-workbench-asset.d.ts +2 -0
  38. package/dist/types/client/state/attention-store.d.ts +0 -5
  39. package/dist/types/client/state/context-status-store.d.ts +53 -0
  40. package/dist/types/client/state/intent-bridge.d.ts +3 -0
  41. package/dist/types/client/state/near-pin-store.d.ts +16 -0
  42. package/dist/types/client/state/presence-store.d.ts +5 -0
  43. package/dist/types/client/state/session-store.d.ts +33 -3
  44. package/dist/types/client/state/workbench-transport.d.ts +16 -0
  45. package/dist/types/client/theme/tokens.d.ts +1 -3
  46. package/dist/types/client/types.d.ts +9 -0
  47. package/dist/types/json-render/server.d.ts +1 -1
  48. package/dist/types/mcp/app-server.d.ts +3 -0
  49. package/dist/types/mcp/canvas-access.d.ts +1 -1
  50. package/dist/types/mcp/canvas-app.d.ts +4 -0
  51. package/dist/types/mcp/workbench-app.d.ts +4 -0
  52. package/dist/types/server/agent-presence.d.ts +1 -1
  53. package/dist/types/server/ax-state.d.ts +1 -1
  54. package/dist/types/server/canvas-db.d.ts +8 -0
  55. package/dist/types/server/canvas-geometry.d.ts +25 -0
  56. package/dist/types/server/canvas-operations.d.ts +1 -11
  57. package/dist/types/server/canvas-state.d.ts +25 -3
  58. package/dist/types/server/context-brief.d.ts +8 -1
  59. package/dist/types/server/context-reads.d.ts +30 -7
  60. package/dist/types/server/index.d.ts +6 -1
  61. package/dist/types/server/mcp-app-runtime.d.ts +1 -0
  62. package/dist/types/server/operations/http.d.ts +2 -0
  63. package/dist/types/server/operations/invoker.d.ts +4 -2
  64. package/dist/types/server/operations/ops/nodes.d.ts +0 -6
  65. package/dist/types/server/server.d.ts +1 -0
  66. package/dist/types/server/spatial-analysis.d.ts +2 -0
  67. package/dist/types/shared/agent-context.d.ts +22 -0
  68. package/dist/types/shared/agent-presence.d.ts +3 -10
  69. package/dist/types/shared/canvas-app.d.ts +54 -0
  70. package/dist/types/shared/themes.d.ts +1 -1
  71. package/docs/RELEASE.md +7 -3
  72. package/docs/agent-plugin.md +123 -0
  73. package/docs/ax-state-contract.md +40 -0
  74. package/docs/cli.md +7 -2
  75. package/docs/design/AgentContext.dc.html +186 -0
  76. package/docs/design/Before.dc.html +208 -0
  77. package/docs/design/BoardPins.dc.html +151 -0
  78. package/docs/design/Chrome.dc.html +211 -0
  79. package/docs/design/Context.dc.html +239 -0
  80. package/docs/design/Flows.dc.html +181 -0
  81. package/docs/design/Home.dc.html +209 -0
  82. package/docs/design/HomeGraph.dc.html +211 -0
  83. package/docs/design/HomeMap.dc.html +117 -0
  84. package/docs/design/HomeTeam.dc.html +176 -0
  85. package/docs/design/Light.dc.html +28 -0
  86. package/docs/design/Main.dc.html +280 -0
  87. package/docs/design/NearPin.dc.html +260 -0
  88. package/docs/design/Pane600.dc.html +120 -0
  89. package/docs/design/Relations.dc.html +297 -0
  90. package/docs/design/Themes.dc.html +121 -0
  91. package/docs/design/TokenSystem.dc.html +136 -0
  92. package/docs/design/Tokens.dc.html +163 -0
  93. package/docs/design/Types1.dc.html +177 -0
  94. package/docs/design/Types2.dc.html +226 -0
  95. package/docs/design/Types3.dc.html +195 -0
  96. package/docs/design/Uses.dc.html +152 -0
  97. package/docs/design/Vision.dc.html +116 -0
  98. package/docs/design/Zoom.dc.html +82 -0
  99. package/docs/design/canvas.json +274 -0
  100. package/docs/design.md +217 -0
  101. package/docs/environment.md +1 -1
  102. package/docs/http-api.md +86 -16
  103. package/docs/mcp.md +202 -4
  104. package/docs/node-types.md +34 -10
  105. package/docs/plans/plan-009-tech-debt-backlog.md +1 -1
  106. package/docs/plans/plan-015-design-foundation-wave-0.md +177 -0
  107. package/docs/plans/plan-016-context-made-visible-wave-1.md +148 -0
  108. package/docs/product-vision-2026-09.md +47 -7
  109. package/docs/screenshot.png +0 -0
  110. package/docs/sdk.md +1 -0
  111. package/package.json +10 -4
  112. package/plugins/pmx-canvas/LICENSE +21 -0
  113. package/plugins/pmx-canvas/THIRD_PARTY_LICENSES.txt +331 -0
  114. package/plugins/pmx-canvas/assets/preview.png +0 -0
  115. package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/extension.mjs +930 -0
  116. package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/runtime.json +3 -0
  117. package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/runtime.mjs +194 -0
  118. package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/steering-delivery.mjs +79 -0
  119. package/plugins/pmx-canvas/mcp.json +12 -0
  120. package/plugins/pmx-canvas/plugin.json +24 -0
  121. package/plugins/pmx-canvas/runtime.json +3 -0
  122. package/plugins/pmx-canvas/runtime.mjs +194 -0
  123. package/plugins/pmx-canvas/start-mcp.mjs +51 -0
  124. package/plugins/pmx-canvas/tools.json +2560 -0
  125. package/skills/pmx-canvas/SKILL.md +29 -9
  126. package/skills/pmx-canvas/evals/evals.json +62 -0
  127. package/skills/pmx-canvas/references/ax-html-control-surface.md +6 -0
  128. package/skills/pmx-canvas/references/full-reference.md +11 -7
  129. package/skills/pmx-canvas/references/github-copilot-app-adapter.md +20 -8
  130. package/skills/pmx-canvas/references/installing-pmx-canvas.md +6 -6
  131. package/skills/pmx-canvas-testing/SKILL.md +5 -4
  132. package/src/cli/commands/copilot.ts +5 -0
  133. package/src/cli/commands/nodes.ts +3 -2
  134. package/src/cli/commands/view.ts +1 -1
  135. package/src/cli/index.ts +10 -4
  136. package/src/client/App.tsx +6 -1
  137. package/src/client/canvas/AgentPresenceLayer.tsx +3 -3
  138. package/src/client/canvas/AnnotationLayer.tsx +1 -1
  139. package/src/client/canvas/CanvasNode.tsx +32 -49
  140. package/src/client/canvas/CanvasViewport.tsx +0 -2
  141. package/src/client/canvas/CommandPalette.tsx +3 -2
  142. package/src/client/canvas/ContextMenu.tsx +11 -0
  143. package/src/client/canvas/EdgeLayer.tsx +1 -1
  144. package/src/client/canvas/ExportDialog.tsx +33 -6
  145. package/src/client/canvas/IntentLayer.tsx +2 -2
  146. package/src/client/canvas/Minimap.tsx +12 -7
  147. package/src/client/canvas/NodeContextMark.tsx +156 -0
  148. package/src/client/canvas/SessionPanel.tsx +4 -7
  149. package/src/client/canvas/SessionReceipt.tsx +114 -17
  150. package/src/client/canvas/ShortcutOverlay.tsx +6 -1
  151. package/src/client/canvas/SnapshotPanel.tsx +11 -2
  152. package/src/client/canvas/ToolRail.tsx +12 -9
  153. package/src/client/canvas/TopBar.tsx +16 -11
  154. package/src/client/icons.tsx +2 -12
  155. package/src/client/mcp-app/index.tsx +137 -0
  156. package/src/client/mcp-app/style.css +10 -0
  157. package/src/client/nodes/AttachmentNode.tsx +8 -1
  158. package/src/client/nodes/AxStepControls.tsx +1 -1
  159. package/src/client/nodes/ContextNode.tsx +9 -9
  160. package/src/client/nodes/ExtAppFrame.tsx +13 -8
  161. package/src/client/nodes/FileNode.tsx +8 -5
  162. package/src/client/nodes/HtmlNode.tsx +12 -2
  163. package/src/client/nodes/ImageNode.tsx +4 -2
  164. package/src/client/nodes/InlineMarkdownEditor.tsx +2 -1
  165. package/src/client/nodes/LedgerNode.tsx +2 -2
  166. package/src/client/nodes/MarkdownNode.tsx +44 -9
  167. package/src/client/nodes/McpAppNode.tsx +6 -4
  168. package/src/client/nodes/PromptNode.tsx +5 -17
  169. package/src/client/nodes/ResponseNode.tsx +3 -15
  170. package/src/client/nodes/StatusNode.tsx +2 -2
  171. package/src/client/nodes/StatusSummary.tsx +3 -1
  172. package/src/client/nodes/TraceNode.tsx +6 -4
  173. package/src/client/nodes/WebpageNode.tsx +1 -1
  174. package/src/client/nodes/ax-node-actions.ts +2 -2
  175. package/src/client/nodes/iframe-document-url.ts +3 -2
  176. package/src/client/nodes/markdown-sanitize.ts +10 -0
  177. package/src/client/nodes/surface-url.ts +10 -0
  178. package/src/client/nodes/use-surface-frame.ts +3 -1
  179. package/src/client/nodes/use-workbench-asset.ts +26 -0
  180. package/src/client/state/attention-bridge.ts +0 -8
  181. package/src/client/state/attention-store.ts +0 -17
  182. package/src/client/state/canvas-store.ts +4 -2
  183. package/src/client/state/context-status-store.ts +66 -0
  184. package/src/client/state/human-store.ts +2 -0
  185. package/src/client/state/iframe-mode.ts +5 -0
  186. package/src/client/state/intent-bridge.ts +10 -4
  187. package/src/client/state/near-pin-store.ts +53 -0
  188. package/src/client/state/presence-store.ts +5 -10
  189. package/src/client/state/session-store.ts +106 -9
  190. package/src/client/state/sse-bridge.ts +26 -7
  191. package/src/client/state/theme-override.ts +1 -1
  192. package/src/client/state/workbench-transport.ts +36 -0
  193. package/src/client/theme/global.css +726 -674
  194. package/src/client/theme/surface-theme.css +151 -46
  195. package/src/client/theme/tokens.ts +7 -11
  196. package/src/client/types.ts +10 -0
  197. package/src/json-render/renderer/index.css +6 -1
  198. package/src/json-render/renderer/index.tsx +0 -1
  199. package/src/json-render/server.ts +71 -14
  200. package/src/mcp/app-server.ts +64 -0
  201. package/src/mcp/canvas-access.ts +14 -11
  202. package/src/mcp/canvas-app.ts +318 -0
  203. package/src/mcp/server.ts +1 -1
  204. package/src/mcp/workbench-app.ts +211 -0
  205. package/src/server/agent-presence.ts +45 -6
  206. package/src/server/ax-context.ts +1 -1
  207. package/src/server/ax-state.ts +1 -1
  208. package/src/server/board-export.ts +5 -2
  209. package/src/server/canvas-db.ts +46 -12
  210. package/src/server/canvas-geometry.ts +64 -0
  211. package/src/server/canvas-operations.ts +7 -25
  212. package/src/server/canvas-state.ts +105 -15
  213. package/src/server/canvas-validation.ts +19 -4
  214. package/src/server/context-brief.ts +39 -2
  215. package/src/server/context-reads.ts +87 -15
  216. package/src/server/demo-state.json +31 -31
  217. package/src/server/html-surface.ts +4 -5
  218. package/src/server/index.ts +41 -5
  219. package/src/server/mcp-app-runtime.ts +11 -0
  220. package/src/server/operations/composites.ts +4 -3
  221. package/src/server/operations/http.ts +4 -4
  222. package/src/server/operations/invoker.ts +13 -4
  223. package/src/server/operations/mcp.ts +6 -0
  224. package/src/server/operations/ops/ax-read.ts +1 -1
  225. package/src/server/operations/ops/ax-timeline.ts +37 -0
  226. package/src/server/operations/ops/boards.ts +1 -1
  227. package/src/server/operations/ops/canvas-wire.ts +3 -6
  228. package/src/server/operations/ops/intent.ts +31 -2
  229. package/src/server/operations/ops/json-render.ts +6 -2
  230. package/src/server/operations/ops/nodes.ts +8 -76
  231. package/src/server/operations/ops/query.ts +8 -2
  232. package/src/server/operations/ops/snapshots.ts +4 -1
  233. package/src/server/operations/ops/viewport.ts +29 -5
  234. package/src/server/operations/registry.ts +41 -5
  235. package/src/server/scope-fence.ts +55 -10
  236. package/src/server/server.ts +83 -5
  237. package/src/server/spatial-analysis.ts +2 -2
  238. package/src/server/workboard.ts +6 -1
  239. package/src/{server → shared}/agent-context.ts +2 -2
  240. package/src/shared/agent-presence.ts +3 -15
  241. package/src/shared/canvas-app.ts +62 -0
  242. package/src/shared/themes.ts +5 -5
  243. package/src/client/canvas/FocusFieldLayer.tsx +0 -116
  244. package/src/client/canvas/kind-colors.ts +0 -23
@@ -1,13 +1,25 @@
1
- import { useState } from 'preact/hooks';
2
- import { dismissSessionReceipt, sessionReceipt } from '../state/session-store';
3
- import { IconClose } from '../icons';
1
+ import { useEffect, useState } from 'preact/hooks';
2
+ import { toggleContextPin } from '../state/canvas-store';
3
+ import {
4
+ activityLensNodeIds,
5
+ dismissSessionReceipt,
6
+ type ReceiptNode,
7
+ type SessionContextActivity,
8
+ sessionReceipt,
9
+ setActivityLens,
10
+ } from '../state/session-store';
11
+ import { IconClose, IconPin } from '../icons';
12
+ import { GLYPHS } from './NodeContextMark';
13
+ import { workbenchFetch } from '../state/workbench-transport';
4
14
 
5
15
  /**
6
16
  * Session receipt (rail-chrome-v2 phase 5, design item 2): a dismissible card
7
17
  * at the canvas region's top-right after a session ends — what the session did
8
18
  * (items / done / vetoed), the pre-session snapshot (taken at attach, so View
9
19
  * diff shows the session's changes and a restore undoes them), and History
10
- * (the snapshots panel). Client-side state, cleared on dismiss.
20
+ * (the snapshots panel). Client-side state, cleared on dismiss (see
21
+ * applySessionReceipt for which endings show at all and how a burst merges
22
+ * into one card).
11
23
  */
12
24
 
13
25
  export interface DiffSummary {
@@ -28,17 +40,96 @@ export function summarizeDiff(diff: unknown): DiffSummary | null {
28
40
  };
29
41
  }
30
42
 
43
+ const titles = (nodes: ReceiptNode[]) => nodes.map((node) => node.title).join(', ');
44
+
45
+ /**
46
+ * What the session did with context (docs/design/AgentContext.dc.html): an
47
+ * amber line for pins it read that changed since, one row per kind of touch,
48
+ * and the lens switch that dims every node the session did not touch.
49
+ */
50
+ function ReceiptContext({ context, label }: { context: SessionContextActivity; label: string }) {
51
+ const rows: Array<{ key: keyof SessionContextActivity; name: string; glyph: keyof typeof GLYPHS | 'pin' }> = [
52
+ { key: 'read', name: 'Read', glyph: 'eye' },
53
+ { key: 'pinned', name: 'Pinned', glyph: 'pin' },
54
+ { key: 'created', name: 'Created', glyph: 'spark' },
55
+ { key: 'edited', name: 'Edited', glyph: 'pen' },
56
+ ];
57
+ const present = rows.filter((row) => context[row.key].length > 0);
58
+ if (present.length === 0 && context.changedSinceRead.length === 0) return null;
59
+ const lensOn = activityLensNodeIds.value !== null;
60
+ return (
61
+ <div class="session-receipt-context" data-testid="session-receipt-context">
62
+ {context.changedSinceRead.length > 0 && (
63
+ <div class="session-receipt-warn">
64
+ <svg viewBox="0 0 24 24" aria-hidden="true">
65
+ <path d={GLYPHS.warn} />
66
+ </svg>
67
+ <span>
68
+ {titles(context.changedSinceRead)} changed after {label} read{' '}
69
+ {context.changedSinceRead.length === 1 ? 'it' : 'them'}.
70
+ </span>
71
+ </div>
72
+ )}
73
+ {present.map((row) => (
74
+ <div key={row.key} class="session-receipt-context-row" data-row={row.key}>
75
+ <div class="session-receipt-context-head">
76
+ {row.glyph === 'pin' ? (
77
+ <IconPin />
78
+ ) : (
79
+ <svg viewBox="0 0 24 24" aria-hidden="true">
80
+ <path d={GLYPHS[row.glyph]} />
81
+ </svg>
82
+ )}
83
+ <span class="session-receipt-context-name">{row.name}</span>
84
+ <span class="session-receipt-context-count">{context[row.key].length}</span>
85
+ {row.key === 'pinned' && (
86
+ <button
87
+ type="button"
88
+ class="session-receipt-mini"
89
+ onClick={() => {
90
+ for (const node of context.pinned) toggleContextPin(node.id);
91
+ }}
92
+ >
93
+ Unpin
94
+ </button>
95
+ )}
96
+ </div>
97
+ <div class="session-receipt-context-items">
98
+ {row.key === 'pinned'
99
+ ? context.pinned.map((node) => (node.reason ? `${node.title} — “${node.reason}”` : node.title)).join('; ')
100
+ : titles(context[row.key])}
101
+ </div>
102
+ </div>
103
+ ))}
104
+ <button
105
+ type="button"
106
+ role="switch"
107
+ aria-checked={lensOn}
108
+ class={`session-receipt-lens${lensOn ? ' is-on' : ''}`}
109
+ onClick={() => setActivityLens(!lensOn)}
110
+ >
111
+ <span class="session-receipt-lens-track" aria-hidden="true">
112
+ <span class="session-receipt-lens-knob" />
113
+ </span>
114
+ Dim untouched nodes
115
+ </button>
116
+ </div>
117
+ );
118
+ }
119
+
31
120
  export function SessionReceipt({ onOpenSnapshots }: { onOpenSnapshots: () => void }) {
32
121
  const receipt = sessionReceipt.value;
33
122
  const [diff, setDiff] = useState<DiffSummary | null>(null);
34
123
  const [loadingDiff, setLoadingDiff] = useState(false);
124
+ // A new or merged receipt: the old diff no longer describes it.
125
+ useEffect(() => setDiff(null), [receipt]);
35
126
  if (!receipt) return null;
36
127
 
37
128
  const viewDiff = async () => {
38
129
  if (!receipt.snapshot) return;
39
130
  setLoadingDiff(true);
40
131
  try {
41
- const response = await fetch(`/api/canvas/snapshots/${encodeURIComponent(receipt.snapshot.id)}/diff`, {
132
+ const response = await workbenchFetch(`/api/canvas/snapshots/${encodeURIComponent(receipt.snapshot.id)}/diff`, {
42
133
  headers: { 'x-pmx-workbench': '1' },
43
134
  });
44
135
  if (response.ok) {
@@ -60,13 +151,15 @@ export function SessionReceipt({ onOpenSnapshots }: { onOpenSnapshots: () => voi
60
151
  <div class="session-receipt-head">
61
152
  <span class="session-receipt-dot" aria-hidden="true" />
62
153
  <span class="session-receipt-title">
63
- {receipt.endedBy === 'human'
64
- ? 'Session ended by you'
65
- : receipt.endedBy === 'idle-timeout'
66
- ? 'Session ended — idle timeout'
67
- : receipt.endedBy === 'agent'
68
- ? 'Session ended by the agent'
69
- : 'Session ended'}
154
+ {receipt.sessions > 1
155
+ ? `${receipt.sessions} sessions ended`
156
+ : receipt.endedBy === 'human'
157
+ ? 'Session ended by you'
158
+ : receipt.endedBy === 'idle-timeout'
159
+ ? 'Session ended — idle timeout'
160
+ : receipt.endedBy === 'agent'
161
+ ? 'Session ended by the agent'
162
+ : 'Session ended'}
70
163
  {endedLabel ? ` · ${endedLabel}` : ''}
71
164
  </span>
72
165
  <button
@@ -78,6 +171,7 @@ export function SessionReceipt({ onOpenSnapshots }: { onOpenSnapshots: () => voi
78
171
  <IconClose />
79
172
  </button>
80
173
  </div>
174
+ {receipt.sessions > 1 && <div class="session-receipt-who">{receipt.labels.join(', ')}</div>}
81
175
  <div class="session-receipt-tiles">
82
176
  <div class="session-receipt-tile">
83
177
  <span class="session-receipt-tile-label">Items</span>
@@ -108,12 +202,15 @@ export function SessionReceipt({ onOpenSnapshots }: { onOpenSnapshots: () => voi
108
202
  </div>
109
203
  )}
110
204
  </div>
205
+ <ReceiptContext context={receipt.context} label={receipt.labels.join(', ')} />
111
206
  <div class="session-receipt-note">
112
- {receipt.snapshot
113
- ? 'A snapshot of the board from before this session is saved — restore it to undo the session.'
114
- : receipt.unchanged
115
- ? 'This session changed nothing on the board — no snapshot kept.'
116
- : 'The board was empty when the session started — nothing to restore.'}
207
+ {receipt.sessions > 1
208
+ ? 'History has each session’s diff and the snapshot from before it.'
209
+ : receipt.snapshot
210
+ ? 'A snapshot of the board from before this session is saved — restore it to undo the session.'
211
+ : receipt.context.created.length === 0 && receipt.context.edited.length === 0
212
+ ? 'The board itself did not change — nothing to restore.'
213
+ : 'The board was empty when the session started — nothing to restore.'}
117
214
  </div>
118
215
  {diff && (
119
216
  <div class="session-receipt-diff" data-testid="session-receipt-diff">
@@ -1,3 +1,4 @@
1
+ import { isHostedWorkbench } from '../state/workbench-transport';
1
2
  import { isMac, modChord } from '../utils/platform';
2
3
 
3
4
  interface ShortcutGroup {
@@ -74,7 +75,11 @@ export function ShortcutOverlay({ onClose }: { onClose: () => void }) {
74
75
  {group.shortcuts.map((s) => (
75
76
  <div key={s.keys} class="shortcut-row">
76
77
  <kbd class="shortcut-keys">{s.keys}</kbd>
77
- <span class="shortcut-desc">{s.desc}</span>
78
+ <span class="shortcut-desc">
79
+ {isHostedWorkbench() && s.desc.startsWith('Undo / redo')
80
+ ? 'Undo / redo — available in the local workbench only'
81
+ : s.desc}
82
+ </span>
78
83
  </div>
79
84
  ))}
80
85
  </div>
@@ -1,3 +1,4 @@
1
+ import { isHostedWorkbench } from '../state/workbench-transport';
1
2
  import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
2
3
  import { IconClose } from '../icons';
3
4
  import {
@@ -171,7 +172,11 @@ export function SnapshotPanel({
171
172
  type="button"
172
173
  class="snapshot-action-btn snapshot-action-restore"
173
174
  onClick={() => setConfirming({ id: snap.id, action: 'restore' })}
174
- title="Restore this snapshot — replaces the current canvas (undoable)"
175
+ title={
176
+ isHostedWorkbench()
177
+ ? 'Restore this snapshot — replaces the current canvas'
178
+ : 'Restore this snapshot — replaces the current canvas (undoable)'
179
+ }
175
180
  disabled={restoringId !== null}
176
181
  >
177
182
  {restoringId === snap.id ? 'Restoring…' : restoreLabel}
@@ -219,7 +224,11 @@ export function SnapshotPanel({
219
224
  {saving ? '…' : 'Save'}
220
225
  </button>
221
226
  </div>
222
- <div class="snapshot-restore-note">Restoring replaces the current canvas. You can undo it if needed.</div>
227
+ <div class="snapshot-restore-note">
228
+ {isHostedWorkbench()
229
+ ? 'Restoring replaces the current canvas. Save a snapshot first to keep your current state.'
230
+ : 'Restoring replaces the current canvas. You can undo it if needed.'}
231
+ </div>
223
232
 
224
233
  <div class="snapshot-list">
225
234
  {loading && <div class="snapshot-empty">Loading…</div>}
@@ -1,4 +1,5 @@
1
1
  import { useEffect, useRef, useState } from 'preact/hooks';
2
+ import { isHostedWorkbench, workbenchFetch } from '../state/workbench-transport';
2
3
  import {
3
4
  IconArrange,
4
5
  IconClearTrace,
@@ -229,7 +230,7 @@ export function ToolRail({
229
230
  const activeTheme = normalizeCanvasThemeName(canvasTheme.value);
230
231
 
231
232
  const sendIntent = (type: string, payload: Record<string, unknown> = {}) => {
232
- fetch(`/api/workbench/intent?_ts=${Date.now()}`, {
233
+ workbenchFetch(`/api/workbench/intent?_ts=${Date.now()}`, {
233
234
  method: 'POST',
234
235
  headers: { 'Content-Type': 'application/json' },
235
236
  body: JSON.stringify({ type, payload }),
@@ -398,14 +399,16 @@ export function ToolRail({
398
399
  >
399
400
  <IconArrange />
400
401
  </RailButton>
401
- <RailButton
402
- label={isTraceOn ? 'Disable trace' : 'Enable trace'}
403
- active={isTraceOn}
404
- onClick={() => sendIntent('trace-toggle', { enabled: !isTraceOn })}
405
- >
406
- <IconTrace />
407
- </RailButton>
408
- {(isTraceOn || traceNodeCount > 0) && (
402
+ {!isHostedWorkbench() && (
403
+ <RailButton
404
+ label={isTraceOn ? 'Disable trace' : 'Enable trace'}
405
+ active={isTraceOn}
406
+ onClick={() => sendIntent('trace-toggle', { enabled: !isTraceOn })}
407
+ >
408
+ <IconTrace />
409
+ </RailButton>
410
+ )}
411
+ {!isHostedWorkbench() && (isTraceOn || traceNodeCount > 0) && (
409
412
  <RailButton label="Clear trace" onClick={() => sendIntent('trace-clear')}>
410
413
  <IconClearTrace />
411
414
  </RailButton>
@@ -1,6 +1,7 @@
1
1
  import { exportDialogOpen } from './ExportDialog';
2
2
  import { activeBoardId } from '../state/boards-store';
3
3
  import { BoardSwitcher } from './BoardSwitcher';
4
+ import { isHostedWorkbench, workbenchFetch } from '../state/workbench-transport';
4
5
  import type { ComponentChildren } from 'preact';
5
6
  import { presenting } from '../state/presentation';
6
7
  import { useEffect, useState } from 'preact/hooks';
@@ -24,8 +25,7 @@ import { degradedState } from './ConnectionBanner';
24
25
  import { ExternalWriterIndicator } from './ExternalWriters';
25
26
  import { useNow } from './use-now';
26
27
  import { agentPhaseLabel } from '../../shared/agent-presence.js';
27
- import { agentIdentityHue } from '../../shared/agent-presence.js';
28
- import { activeSession, agentPresences, attachedSessions, contextBudget } from '../state/presence-store';
28
+ import { activeSession, agentPresences, attachedSessions, contextBudget, writerColor } from '../state/presence-store';
29
29
  import { endSession, pendingGates, startSession } from '../state/session-store';
30
30
  import { formatCountdown, gateRemainingMs } from '../../shared/approval-gates.js';
31
31
 
@@ -56,6 +56,7 @@ function AgentChip() {
56
56
  !allSessions.some(
57
57
  (candidate) =>
58
58
  candidate.sessionId !== session.sessionId &&
59
+ !candidate.parentAgentId &&
59
60
  (candidate.sessionId === session.parentAgentId || candidate.source === session.parentAgentId),
60
61
  ),
61
62
  );
@@ -83,12 +84,16 @@ function AgentChip() {
83
84
  <span class="agent-chips">
84
85
  {visible.map((session) => {
85
86
  // Fleet roll-up: workers declaring this session as their parent count
86
- // into its chip instead of growing the bar one chip per worker.
87
- const workers = agentPresences.value.filter(
88
- (presence) =>
89
- presence.parentAgentId != null &&
90
- (presence.parentAgentId === session.sessionId || presence.parentAgentId === session.source),
91
- ).length;
87
+ // into its chip instead of growing the bar one chip per worker. A
88
+ // worker's own chip carries no roll-up: same-source siblings would
89
+ // otherwise match it through `session.source`.
90
+ const workers = session.parentAgentId
91
+ ? 0
92
+ : agentPresences.value.filter(
93
+ (presence) =>
94
+ presence.parentAgentId != null &&
95
+ (presence.parentAgentId === session.sessionId || presence.parentAgentId === session.source),
96
+ ).length;
92
97
  return (
93
98
  <BarHint
94
99
  key={session.sessionId}
@@ -99,7 +104,7 @@ function AgentChip() {
99
104
  <span
100
105
  class={`agent-chip phase-${session.phase}`}
101
106
  data-phase={session.phase}
102
- style={{ '--identity-color': `hsl(${agentIdentityHue(session.sessionId)} 65% 62%)` }}
107
+ style={{ '--identity-color': writerColor(session.sessionId) }}
103
108
  >
104
109
  <span class="agent-chip-dot" aria-hidden="true" />
105
110
  <span class="agent-chip-label">{agentPhaseLabel(session)}</span>
@@ -272,7 +277,7 @@ export function TopBar() {
272
277
  const [workspaceName, setWorkspaceName] = useState<string>('');
273
278
  useEffect(() => {
274
279
  let cancelled = false;
275
- fetch('/health')
280
+ workbenchFetch('/health')
276
281
  .then((r) => (r.ok ? r.json() : null))
277
282
  .then((data: { workspace?: string } | null) => {
278
283
  if (cancelled || typeof data?.workspace !== 'string' || !data.workspace) return;
@@ -327,7 +332,7 @@ export function TopBar() {
327
332
  <GateBadge />
328
333
  <ContextBudget />
329
334
  <ExternalWriterIndicator />
330
- <StartSessionButton />
335
+ {!isHostedWorkbench() && <StartSessionButton />}
331
336
 
332
337
  <div class="top-bar-sep" />
333
338
 
@@ -112,14 +112,6 @@ export function IconUndo(p: IconProps): JSX.Element {
112
112
  );
113
113
  }
114
114
 
115
- export function IconSparkle(p: IconProps): JSX.Element {
116
- return (
117
- <Icon {...p}>
118
- <path d="m8 1 2 5 5 2-5 2-2 5-2-5-5-2 5-2Z" />
119
- </Icon>
120
- );
121
- }
122
-
123
115
  /** Upload into a tray — attach a document. */
124
116
  export function IconUpload(p: IconProps): JSX.Element {
125
117
  return (
@@ -326,13 +318,11 @@ export function IconSteer(p: IconProps): JSX.Element {
326
318
  );
327
319
  }
328
320
 
329
- /** Framed pin — context pinning. */
321
+ /** Push pin — context pinning (docs/design/Chrome.dc.html). */
330
322
  export function IconPin(p: IconProps): JSX.Element {
331
323
  return (
332
324
  <Icon {...p}>
333
- <rect x="2" y="5" width="12" height="9" rx="1.5" />
334
- <line x1="11" y1="1.5" x2="11" y2="5" />
335
- <circle cx="11" cy="4.5" r="1.6" fill="currentColor" stroke="none" />
325
+ <path d="M6 2.67h4l-.53 3.66 1.86 2H4.67l1.86-2zM8 8.33v5" />
336
326
  </Icon>
337
327
  );
338
328
  }
@@ -0,0 +1,137 @@
1
+ import '../uuid-polyfill';
2
+ import { App as McpApp } from '@modelcontextprotocol/ext-apps';
3
+ import { render } from 'preact';
4
+ import { useEffect, useRef, useState } from 'preact/hooks';
5
+ import { z } from 'zod';
6
+ import { App as Workbench } from '../App';
7
+ import { activeBoardId, boardList, boardsLoaded } from '../state/boards-store';
8
+ import { axSurfaceState, contextPinnedNodeIds, nodes, selectedNodeIds } from '../state/canvas-store';
9
+ import { installWorkbenchDownload, installWorkbenchTransport } from '../state/workbench-transport';
10
+ import { resolveIframeMode } from '../state/iframe-mode';
11
+ import { summarizeNodeForAgentContext } from '../../shared/agent-context';
12
+ import { canvasAppContext } from '../../shared/canvas-app';
13
+
14
+ const bridge = new McpApp(
15
+ { name: 'PMX Canvas', version: '1.0.0' },
16
+ { availableDisplayModes: ['inline', 'fullscreen'] },
17
+ );
18
+ const responseSchema = z.object({
19
+ status: z.number(),
20
+ body: z.string(),
21
+ contentType: z.string(),
22
+ encoding: z.enum(['text', 'base64']),
23
+ });
24
+
25
+ // Preserve the workbench's fetch/Response contracts, without passing its human
26
+ // headers or exposing the daemon URL to the embedded browser.
27
+ installWorkbenchTransport(async (input, init) => {
28
+ const path = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url;
29
+ const method = (init?.method ?? (input instanceof Request ? input.method : 'GET')).toUpperCase();
30
+ if (init?.body !== undefined && typeof init.body !== 'string')
31
+ return Response.json({ ok: false, error: 'Document uploads require the local PMX workbench.' }, { status: 403 });
32
+ init?.signal?.throwIfAborted();
33
+ const result = await bridge.callServerTool({
34
+ name: 'pmx_workbench_request',
35
+ arguments: {
36
+ path,
37
+ method,
38
+ ...(init?.body === undefined ? {} : { body: init.body }),
39
+ expectedBoardId: activeBoardId.value,
40
+ },
41
+ });
42
+ init?.signal?.throwIfAborted();
43
+ const response = responseSchema.parse(result._meta?.response);
44
+ const body =
45
+ response.encoding === 'base64' ? Uint8Array.from(atob(response.body), (char) => char.charCodeAt(0)) : response.body;
46
+ return new Response(method === 'HEAD' || response.status === 204 ? null : body, {
47
+ status: response.status,
48
+ headers: { 'Content-Type': response.contentType },
49
+ });
50
+ });
51
+
52
+ const focusSchema = z.object({ focus: z.array(z.string()) });
53
+
54
+ function selectionContext() {
55
+ const focus = focusSchema.safeParse(axSurfaceState.value);
56
+ return canvasAppContext(
57
+ {
58
+ boardId: activeBoardId.value,
59
+ boardName: boardList.value.find((board) => board.id === activeBoardId.value)?.name ?? 'Home',
60
+ pinnedNodeIds: [...contextPinnedNodeIds.value],
61
+ nodes: [...nodes.value.values()].map((node) => ({
62
+ id: node.id,
63
+ type: node.type,
64
+ title: typeof node.data.title === 'string' ? node.data.title : node.type,
65
+ contentRevision: node.contentRevision ?? 0,
66
+ text: summarizeNodeForAgentContext(node),
67
+ })),
68
+ },
69
+ [...selectedNodeIds.value],
70
+ focus.success ? focus.data.focus : [],
71
+ );
72
+ }
73
+
74
+ function HostedWorkbench() {
75
+ const [acknowledged, setAcknowledged] = useState('');
76
+ const [retry, setRetry] = useState(0);
77
+ const queue = useRef(Promise.resolve());
78
+ const context = selectionContext();
79
+ const key = JSON.stringify(context);
80
+ const desired = useRef(key);
81
+ desired.current = key;
82
+ const caps = bridge.getHostCapabilities();
83
+ const ready = boardsLoaded.value;
84
+ useEffect(() => {
85
+ if (!ready || !caps?.updateModelContext || acknowledged === key) return;
86
+ let cancelled = false;
87
+ let timer: ReturnType<typeof setTimeout> | undefined;
88
+ queue.current = queue.current
89
+ .then(async () => {
90
+ if (cancelled || desired.current !== key) return;
91
+ setAcknowledged('');
92
+ await bridge.updateModelContext({ structuredContent: context });
93
+ if (cancelled || desired.current !== key) return;
94
+ setAcknowledged(key);
95
+ })
96
+ .catch(() => {
97
+ // Context is background synchronization, not extra workbench chrome.
98
+ // Retry unchanged context too, and only consider a host ACK successful.
99
+ if (!cancelled && desired.current === key) timer = setTimeout(() => setRetry((value) => value + 1), 2000);
100
+ });
101
+ return () => {
102
+ cancelled = true;
103
+ clearTimeout(timer);
104
+ };
105
+ }, [key, ready, retry]);
106
+ return (
107
+ <div class="pmx-host-app">
108
+ <Workbench />
109
+ </div>
110
+ );
111
+ }
112
+
113
+ // Polling is the sole state owner. Launcher tool results never rehydrate the
114
+ // workbench, so delayed initial notifications cannot roll back its live state.
115
+ bridge.ontoolresult = () => {};
116
+ void bridge
117
+ .connect()
118
+ .then(async () => {
119
+ if (!bridge.getHostCapabilities()?.serverTools) throw new Error('This host does not support app tool calls.');
120
+ if (bridge.getHostCapabilities()?.downloadFile) {
121
+ installWorkbenchDownload((file) =>
122
+ bridge.downloadFile({
123
+ contents: [
124
+ {
125
+ type: 'resource',
126
+ resource: { uri: `file:///${encodeURIComponent(file.name)}`, mimeType: file.mimeType, text: file.text },
127
+ },
128
+ ],
129
+ }),
130
+ );
131
+ }
132
+ await resolveIframeMode();
133
+ render(<HostedWorkbench />, document.getElementById('app')!);
134
+ })
135
+ .catch((error: unknown) => {
136
+ document.getElementById('app')!.textContent = `Could not connect PMX: ${String(error)}`;
137
+ });
@@ -0,0 +1,10 @@
1
+ .pmx-host-app { height: 100%; display: flex; flex-direction: column; }
2
+ .pmx-host-app > .app-shell { flex: 1; min-height: 0; height: auto; }
3
+ /* Keep context controls clear of the host's floating chat composer. */
4
+ .pmx-host-app .context-pin-bar {
5
+ bottom: 16px;
6
+ left: 16px;
7
+ transform: none;
8
+ }
9
+ /* Guard refusals must remain readable while the full source editor is open. */
10
+ .pmx-host-app .attention-toast.attention-tone-remove { z-index: 100100; }
@@ -4,10 +4,12 @@ import type { CanvasNodeState } from '../types';
4
4
  import { activeBoardId } from '../state/boards-store';
5
5
  import { requestJson } from '../state/intent-bridge';
6
6
  import { expandNode, collapseExpandedNode } from '../state/canvas-store';
7
+ import { useWorkbenchAsset } from './use-workbench-asset';
7
8
 
8
9
  /** Originals stay useful even when the connected host cannot extract them. */
9
10
  export function AttachmentNode({ node, expanded = false }: { node: CanvasNodeState; expanded?: boolean }) {
10
11
  const attachmentId = String(node.data.attachmentId);
12
+ const downloadUrl = useWorkbenchAsset(`/api/canvas/attachments/${encodeURIComponent(attachmentId)}/bytes`);
11
13
  const boardId = activeBoardId.value;
12
14
  const [job, setJob] = useState<DocumentImport | null>(null);
13
15
  const [review, setReview] = useState(expanded);
@@ -68,7 +70,12 @@ export function AttachmentNode({ node, expanded = false }: { node: CanvasNodeSta
68
70
  ? 'Markdown added'
69
71
  : 'Original attached'}
70
72
  </div>
71
- <a href={`/api/canvas/attachments/${attachmentId}/bytes`} target="_blank" rel="noopener noreferrer">
73
+ <a
74
+ href={downloadUrl}
75
+ download={String(node.data.title ?? 'attachment')}
76
+ target="_blank"
77
+ rel="noopener noreferrer"
78
+ >
72
79
  Download original · {Math.ceil(Number(node.data.size ?? 0) / 1024)} KB
73
80
  </a>
74
81
  {job?.reason && <p>{job.reason}</p>}
@@ -65,7 +65,7 @@ const labelStyle = {
65
65
 
66
66
  const activeButtonStyle = {
67
67
  ...axNodeActionButtonStyle,
68
- background: 'var(--c-accent-25)',
68
+ background: 'color-mix(in srgb, var(--c-accent) 25%, transparent)',
69
69
  color: 'var(--c-text)',
70
70
  } as const;
71
71
 
@@ -298,8 +298,8 @@ export function ContextNode({
298
298
  style={{
299
299
  padding: '4px 8px',
300
300
  fontSize: '10px',
301
- background: 'var(--c-accent-12)',
302
- border: '1px solid var(--c-accent-25)',
301
+ background: 'color-mix(in srgb, var(--c-accent) 12%, transparent)',
302
+ border: '1px solid color-mix(in srgb, var(--c-accent) 25%, transparent)',
303
303
  borderRadius: '4px',
304
304
  color: 'var(--c-text-soft)',
305
305
  cursor: 'pointer',
@@ -363,7 +363,7 @@ export function ContextNode({
363
363
  style={{
364
364
  fontSize: '9px',
365
365
  padding: '1px 4px',
366
- background: 'var(--c-accent-10)',
366
+ background: 'color-mix(in srgb, var(--c-accent) 10%, transparent)',
367
367
  color: 'var(--c-accent)',
368
368
  borderRadius: '3px',
369
369
  display: 'inline-block',
@@ -402,8 +402,8 @@ export function ContextNode({
402
402
  style={{
403
403
  fontSize: '9px',
404
404
  padding: '1px 4px',
405
- background: 'var(--c-warn-12)',
406
- color: 'var(--c-warn-alt)',
405
+ background: 'color-mix(in srgb, var(--c-accent) 12%, transparent)',
406
+ color: 'var(--c-accent)',
407
407
  borderRadius: '3px',
408
408
  display: 'inline-block',
409
409
  }}
@@ -420,8 +420,8 @@ export function ContextNode({
420
420
  style={{
421
421
  padding: '4px 8px',
422
422
  fontSize: '10px',
423
- background: 'var(--c-accent-12)',
424
- border: '1px solid var(--c-accent-25)',
423
+ background: 'color-mix(in srgb, var(--c-accent) 12%, transparent)',
424
+ border: '1px solid color-mix(in srgb, var(--c-accent) 25%, transparent)',
425
425
  borderRadius: '4px',
426
426
  color: 'var(--c-text-soft)',
427
427
  cursor: 'pointer',
@@ -518,8 +518,8 @@ export function ContextNode({
518
518
  style={{
519
519
  padding: '4px 8px',
520
520
  fontSize: '10px',
521
- background: 'var(--c-accent-12)',
522
- border: '1px solid var(--c-accent-25)',
521
+ background: 'color-mix(in srgb, var(--c-accent) 12%, transparent)',
522
+ border: '1px solid color-mix(in srgb, var(--c-accent) 25%, transparent)',
523
523
  borderRadius: '4px',
524
524
  color: 'var(--c-text-soft)',
525
525
  cursor: 'pointer',