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
@@ -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,
@@ -8,10 +9,7 @@ import {
8
9
  IconLogo,
9
10
  IconMinimap,
10
11
  IconMoon,
11
- IconNodeFile,
12
12
  IconNodeGroup,
13
- IconNodeHtml,
14
- IconNodeImage,
15
13
  IconNodeMarkdown,
16
14
  IconNodeWebpage,
17
15
  IconPen,
@@ -177,37 +175,33 @@ export function ToolRail({
177
175
  const traceNodeCount = Array.from(nodes.value.values()).filter((n) => n.type === 'trace').length;
178
176
  const edgeCount = edges.value.size;
179
177
  const [feedbackOpen, setFeedbackOpen] = useState(false);
180
- const [openMenu, setOpenMenu] = useState<null | 'theme' | 'annotate'>(null);
181
- const [menuAnchor, setMenuAnchor] = useState<{ top: number; bottom: number; right: number } | null>(null);
178
+ const [openMenu, setOpenMenu] = useState<null | 'theme'>(null);
179
+ const [menuAnchor, setMenuAnchor] = useState<{ bottom: number; right: number } | null>(null);
182
180
  const railRef = useRef<HTMLDivElement>(null);
183
- const documentInput = useRef<HTMLInputElement>(null);
181
+ const fileInput = useRef<HTMLInputElement>(null);
184
182
 
185
- const toggleMenu = (menu: 'theme' | 'annotate') => (e: MouseEvent) => {
183
+ const toggleMenu = (menu: 'theme') => (e: MouseEvent) => {
186
184
  if (openMenu === menu) {
187
185
  setOpenMenu(null);
188
186
  return;
189
187
  }
190
188
  const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
191
- setMenuAnchor({ top: rect.top, bottom: rect.bottom, right: rect.right });
189
+ setMenuAnchor({ bottom: rect.bottom, right: rect.right });
192
190
  setOpenMenu(menu);
193
191
  };
194
192
 
195
193
  // The rail scrolls (overflow-y:auto), so an absolutely-positioned popover
196
194
  // would be clipped by it. Fixed positioning from the trigger's rect escapes
197
- // the scroll container; the annotate menu top-aligns with its trigger, the
198
- // theme menu (bottom cluster) bottom-aligns so it grows upward.
199
- const sideMenuStyle = (alignBottom: boolean) =>
200
- menuAnchor
201
- ? alignBottom
202
- ? {
203
- position: 'fixed' as const,
204
- left: `${menuAnchor.right + 8}px`,
205
- bottom: `${window.innerHeight - menuAnchor.bottom}px`,
206
- top: 'auto',
207
- right: 'auto',
208
- }
209
- : { position: 'fixed' as const, left: `${menuAnchor.right + 8}px`, top: `${menuAnchor.top}px`, right: 'auto' }
210
- : undefined;
195
+ // the scroll container; the theme menu bottom-aligns so it grows upward.
196
+ const sideMenuStyle = menuAnchor
197
+ ? {
198
+ position: 'fixed' as const,
199
+ left: `${menuAnchor.right + 8}px`,
200
+ bottom: `${window.innerHeight - menuAnchor.bottom}px`,
201
+ top: 'auto',
202
+ right: 'auto',
203
+ }
204
+ : undefined;
211
205
 
212
206
  useEffect(() => {
213
207
  if (!openMenu) return;
@@ -236,7 +230,7 @@ export function ToolRail({
236
230
  const activeTheme = normalizeCanvasThemeName(canvasTheme.value);
237
231
 
238
232
  const sendIntent = (type: string, payload: Record<string, unknown> = {}) => {
239
- fetch(`/api/workbench/intent?_ts=${Date.now()}`, {
233
+ workbenchFetch(`/api/workbench/intent?_ts=${Date.now()}`, {
240
234
  method: 'POST',
241
235
  headers: { 'Content-Type': 'application/json' },
242
236
  body: JSON.stringify({ type, payload }),
@@ -331,22 +325,20 @@ export function ToolRail({
331
325
  >
332
326
  <IconNodeMarkdown size={15} />
333
327
  </RailButton>
334
- <RailButton label="Image" shortcut="I" onClick={() => promptedCreate('image')}>
335
- <IconNodeImage size={15} />
336
- </RailButton>
337
- <RailButton label="File" shortcut="Shift+F" onClick={() => promptedCreate('file')}>
338
- <IconNodeFile size={15} />
339
- </RailButton>
340
- <RailButton label="Attach document" onClick={() => documentInput.current?.click()}>
328
+ <RailButton
329
+ label="Attach files"
330
+ ariaLabel="Attach files"
331
+ detail="Images (PNG, JPEG, SVG…), Markdown and text/code files. PDF, Word, Excel, PowerPoint and OpenDocument use agent import with review."
332
+ onClick={() => fileInput.current?.click()}
333
+ >
341
334
  <IconUpload size={15} />
342
335
  </RailButton>
343
336
  <input
344
- ref={documentInput}
337
+ ref={fileInput}
345
338
  type="file"
346
339
  hidden
347
340
  multiple
348
- accept=".pdf,.ppt,.pptx,.xls,.xlsx,.doc,.docx,.odt,.ods,.odp"
349
- aria-label="Attach documents"
341
+ aria-label="Attach files"
350
342
  onChange={(event) => {
351
343
  const area = canvasArea();
352
344
  const view = viewport.value;
@@ -361,17 +353,6 @@ export function ToolRail({
361
353
  <RailButton label="Webpage" shortcut="W" onClick={() => promptedCreate('webpage')}>
362
354
  <IconNodeWebpage size={15} />
363
355
  </RailButton>
364
- <RailButton
365
- label="HTML surface"
366
- shortcut="H"
367
- onClick={() =>
368
- void createNodeInView({ type: 'html', title: 'HTML surface' }).catch((error) =>
369
- logRailError('create html', error),
370
- )
371
- }
372
- >
373
- <IconNodeHtml />
374
- </RailButton>
375
356
  <RailButton
376
357
  label="Group"
377
358
  shortcut="G"
@@ -383,61 +364,28 @@ export function ToolRail({
383
364
  >
384
365
  <IconNodeGroup size={15} />
385
366
  </RailButton>
386
- <span class="toolbar-menu-anchor">
387
- <RailButton
388
- label="Annotate"
389
- shortcut="A"
390
- detail="Draw · Text note · Eraser"
391
- active={annotationTool !== null}
392
- menuOpen={openMenu === 'annotate'}
393
- onClick={toggleMenu('annotate')}
394
- >
395
- {annotationTool === 'eraser' ? (
396
- <IconEraser />
397
- ) : annotationTool === 'text' ? (
398
- <IconTextAnnotation />
399
- ) : (
400
- <IconPen />
401
- )}
402
- </RailButton>
403
- {openMenu === 'annotate' && (
404
- <div class="toolbar-menu" style={sideMenuStyle(false)} role="menu" aria-label="Annotate">
405
- <button
406
- type="button"
407
- class={`toolbar-menu-item${annotationTool === 'pen' ? ' active' : ''}`}
408
- onClick={() => {
409
- onSetAnnotationTool(annotationTool === 'pen' ? null : 'pen');
410
- setOpenMenu(null);
411
- }}
412
- >
413
- <IconPen />
414
- <span>{annotationTool === 'pen' ? 'Stop annotating' : 'Draw (A)'}</span>
415
- </button>
416
- <button
417
- type="button"
418
- class={`toolbar-menu-item${annotationTool === 'text' ? ' active' : ''}`}
419
- onClick={() => {
420
- onSetAnnotationTool(annotationTool === 'text' ? null : 'text');
421
- setOpenMenu(null);
422
- }}
423
- >
424
- <IconTextAnnotation />
425
- <span>{annotationTool === 'text' ? 'Stop text notes' : 'Text note'}</span>
426
- </button>
427
- <button
428
- type="button"
429
- class={`toolbar-menu-item${annotationTool === 'eraser' ? ' active' : ''}`}
430
- onClick={() => {
431
- onSetAnnotationTool(annotationTool === 'eraser' ? null : 'eraser');
432
- setOpenMenu(null);
433
- }}
434
- >
435
- <IconEraser />
436
- <span>{annotationTool === 'eraser' ? 'Stop erasing' : 'Eraser'}</span>
437
- </button>
438
- </div>
439
- )}
440
- </span>
367
+ <RailButton
368
+ label="Draw"
369
+ shortcut="A"
370
+ active={annotationTool === 'pen'}
371
+ onClick={() => onSetAnnotationTool(annotationTool === 'pen' ? null : 'pen')}
372
+ >
373
+ <IconPen />
374
+ </RailButton>
375
+ <RailButton
376
+ label="Text note"
377
+ active={annotationTool === 'text'}
378
+ onClick={() => onSetAnnotationTool(annotationTool === 'text' ? null : 'text')}
379
+ >
380
+ <IconTextAnnotation />
381
+ </RailButton>
382
+ <RailButton
383
+ label="Eraser"
384
+ active={annotationTool === 'eraser'}
385
+ onClick={() => onSetAnnotationTool(annotationTool === 'eraser' ? null : 'eraser')}
386
+ >
387
+ <IconEraser />
388
+ </RailButton>
441
389
 
442
390
  <div class="rail-spacer" />
443
391
 
@@ -451,14 +399,16 @@ export function ToolRail({
451
399
  >
452
400
  <IconArrange />
453
401
  </RailButton>
454
- <RailButton
455
- label={isTraceOn ? 'Disable trace' : 'Enable trace'}
456
- active={isTraceOn}
457
- onClick={() => sendIntent('trace-toggle', { enabled: !isTraceOn })}
458
- >
459
- <IconTrace />
460
- </RailButton>
461
- {(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) && (
462
412
  <RailButton label="Clear trace" onClick={() => sendIntent('trace-clear')}>
463
413
  <IconClearTrace />
464
414
  </RailButton>
@@ -485,7 +435,7 @@ export function ToolRail({
485
435
  {canvasThemeScheme(activeTheme) === 'dark' ? <IconSun /> : <IconMoon />}
486
436
  </RailButton>
487
437
  {openMenu === 'theme' && (
488
- <div class="toolbar-menu" style={sideMenuStyle(true)} role="menu" aria-label="Theme">
438
+ <div class="toolbar-menu" style={sideMenuStyle} role="menu" aria-label="Theme">
489
439
  {CANVAS_THEMES.map((name) => (
490
440
  <button
491
441
  key={name}
@@ -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
  }