pmx-canvas 0.8.2 → 0.9.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 (335) hide show
  1. package/.github/extensions/pmx-canvas/runtime.json +1 -1
  2. package/.github/extensions/pmx-canvas/tools.json +601 -22
  3. package/CHANGELOG.md +49 -0
  4. package/Readme.md +131 -532
  5. package/dist/canvas/global.css +2724 -476
  6. package/dist/canvas/index.js +168 -107
  7. package/dist/canvas/mcp-app.html +2899 -596
  8. package/dist/json-render/index.js +20 -20
  9. package/dist/types/client/canvas/BarHint.d.ts +12 -2
  10. package/dist/types/client/canvas/BoardLinkDialog.d.ts +3 -1
  11. package/dist/types/client/canvas/BoardPinButton.d.ts +10 -0
  12. package/dist/types/client/canvas/BoardSwitcher.d.ts +1 -1
  13. package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
  14. package/dist/types/client/canvas/CommandBar.d.ts +3 -4
  15. package/dist/types/client/canvas/ConnectionBanner.d.ts +2 -1
  16. package/dist/types/client/canvas/ContextChip.d.ts +3 -0
  17. package/dist/types/client/canvas/CrossBoardReferences.d.ts +5 -0
  18. package/dist/types/client/canvas/EdgeLayer.d.ts +14 -3
  19. package/dist/types/client/canvas/EmptyState.d.ts +4 -4
  20. package/dist/types/client/canvas/HomeGraphView.d.ts +2 -0
  21. package/dist/types/client/canvas/HomeMapView.d.ts +1 -0
  22. package/dist/types/client/canvas/HomeReviewView.d.ts +6 -0
  23. package/dist/types/client/canvas/HomeView.d.ts +9 -7
  24. package/dist/types/client/canvas/KnowledgeDialog.d.ts +4 -0
  25. package/dist/types/client/canvas/LifecycleChrome.d.ts +5 -0
  26. package/dist/types/client/canvas/LifecycleFields.d.ts +7 -0
  27. package/dist/types/client/canvas/LifecyclePanel.d.ts +1 -0
  28. package/dist/types/client/canvas/MemoryInspector.d.ts +7 -0
  29. package/dist/types/client/canvas/NearPinLayer.d.ts +14 -0
  30. package/dist/types/client/canvas/NodeContextMark.d.ts +40 -7
  31. package/dist/types/client/canvas/RelationPresetsDialog.d.ts +4 -0
  32. package/dist/types/client/canvas/RelationsPanel.d.ts +4 -0
  33. package/dist/types/client/canvas/SessionPanel.d.ts +2 -0
  34. package/dist/types/client/canvas/canvas-area.d.ts +1 -1
  35. package/dist/types/client/canvas/graph-geometry.d.ts +27 -0
  36. package/dist/types/client/canvas/graph-model.d.ts +37 -0
  37. package/dist/types/client/canvas/memory-layout.d.ts +23 -0
  38. package/dist/types/client/canvas/use-chrome-height.d.ts +2 -0
  39. package/dist/types/client/canvas/use-focus-trap.d.ts +1 -0
  40. package/dist/types/client/canvas/use-memory-graph.d.ts +8 -0
  41. package/dist/types/client/canvas/use-pan-zoom.d.ts +4 -1
  42. package/dist/types/client/icons.d.ts +0 -10
  43. package/dist/types/client/nodes/AxStepControls.d.ts +1 -1
  44. package/dist/types/client/nodes/InlineMarkdownEditor.d.ts +4 -4
  45. package/dist/types/client/nodes/MarkdownNode.d.ts +5 -1
  46. package/dist/types/client/nodes/McpAppNode.d.ts +4 -4
  47. package/dist/types/client/nodes/viewer-runtime-state.d.ts +1 -1
  48. package/dist/types/client/state/boards-store.d.ts +81 -9
  49. package/dist/types/client/state/canvas-store.d.ts +14 -1
  50. package/dist/types/client/state/context-chip-store.d.ts +22 -0
  51. package/dist/types/client/state/context-status-store.d.ts +35 -0
  52. package/dist/types/client/state/intent-bridge.d.ts +11 -8
  53. package/dist/types/client/state/knowledge-store.d.ts +22 -0
  54. package/dist/types/client/state/near-pin-store.d.ts +44 -1
  55. package/dist/types/client/state/presence-store.d.ts +4 -4
  56. package/dist/types/client/state/presentation.d.ts +28 -0
  57. package/dist/types/client/state/relation-store.d.ts +10 -0
  58. package/dist/types/client/state/session-store.d.ts +41 -2
  59. package/dist/types/client/state/workspace-settings.d.ts +7 -0
  60. package/dist/types/client/types.d.ts +13 -1
  61. package/dist/types/client/utils/keyboard.d.ts +2 -0
  62. package/dist/types/client/utils/time-ago.d.ts +4 -0
  63. package/dist/types/mcp/context-uri-template.d.ts +3 -1
  64. package/dist/types/server/agent-presence.d.ts +10 -3
  65. package/dist/types/server/ax-context.d.ts +1 -1
  66. package/dist/types/server/canvas-db.d.ts +30 -7
  67. package/dist/types/server/canvas-operations.d.ts +24 -2
  68. package/dist/types/server/canvas-provenance.d.ts +22 -2
  69. package/dist/types/server/canvas-serialization.d.ts +2 -0
  70. package/dist/types/server/canvas-state.d.ts +93 -15
  71. package/dist/types/server/canvas-validation.d.ts +1 -1
  72. package/dist/types/server/context-brief.d.ts +94 -6
  73. package/dist/types/server/context-reads.d.ts +33 -7
  74. package/dist/types/server/edit-summary.d.ts +20 -0
  75. package/dist/types/server/index.d.ts +71 -10
  76. package/dist/types/server/knowledge-lifecycle.d.ts +221 -0
  77. package/dist/types/server/lifecycle-status.d.ts +10 -0
  78. package/dist/types/server/memory-graph.d.ts +9 -0
  79. package/dist/types/server/mutation-history.d.ts +2 -0
  80. package/dist/types/server/operations/ops/ax-shared.d.ts +1 -1
  81. package/dist/types/server/operations/ops/boards.d.ts +2 -1
  82. package/dist/types/server/operations/ops/lifecycle.d.ts +2 -0
  83. package/dist/types/server/operations/ops/memory-graph.d.ts +2 -0
  84. package/dist/types/server/operations/ops/nodes.d.ts +5 -0
  85. package/dist/types/server/operations/ops/query.d.ts +23 -0
  86. package/dist/types/server/operations/registry.d.ts +1 -0
  87. package/dist/types/server/placement.d.ts +7 -0
  88. package/dist/types/server/scope-fence.d.ts +2 -1
  89. package/dist/types/server/spatial-analysis.d.ts +6 -3
  90. package/dist/types/server/tour-control.d.ts +32 -0
  91. package/dist/types/server/wiki-links.d.ts +14 -0
  92. package/dist/types/server/workbench-auth.d.ts +2 -0
  93. package/dist/types/shared/agent-context.d.ts +2 -0
  94. package/dist/types/shared/agent-presence.d.ts +15 -12
  95. package/dist/types/shared/auto-arrange.d.ts +1 -0
  96. package/dist/types/shared/boards.d.ts +4 -4
  97. package/dist/types/shared/knowledge-lifecycle.d.ts +41 -0
  98. package/dist/types/shared/knowledge-presets.d.ts +6 -0
  99. package/dist/types/shared/lifecycle-status.d.ts +24 -0
  100. package/dist/types/shared/memory-graph.d.ts +53 -0
  101. package/dist/types/shared/relations.d.ts +68 -0
  102. package/dist/types/shared/semantic-attention.d.ts +2 -0
  103. package/dist/types/shared/tour.d.ts +38 -0
  104. package/dist/types/shared/workspace-settings.d.ts +11 -0
  105. package/docs/RELEASE.md +21 -3
  106. package/docs/adr-001-bun-only-runtime.md +7 -1
  107. package/docs/adr-002-shared-product-separate-runtimes.md +93 -0
  108. package/docs/api-reference.md +109 -0
  109. package/docs/ax-host-adapter-contract.md +1 -1
  110. package/docs/ax-state-contract.md +5 -4
  111. package/docs/cli.md +153 -15
  112. package/docs/demos/pmx-canvas-demo.mp4 +0 -0
  113. package/docs/design/AgentContext.dc.html +3 -3
  114. package/docs/design/BoardPins.dc.html +37 -22
  115. package/docs/design/Categories.dc.html +208 -0
  116. package/docs/design/CategoryEditor.dc.html +33 -0
  117. package/docs/design/Chrome.dc.html +52 -9
  118. package/docs/design/ContextChip.dc.html +224 -0
  119. package/docs/design/ContextChip600.dc.html +136 -0
  120. package/docs/design/EmptyState.dc.html +45 -0
  121. package/docs/design/Flows.dc.html +23 -1
  122. package/docs/design/HeaderMarks.dc.html +183 -0
  123. package/docs/design/Home.dc.html +19 -1
  124. package/docs/design/HomeGraph.dc.html +86 -202
  125. package/docs/design/HomeMap.dc.html +23 -1
  126. package/docs/design/HomeTeam.dc.html +32 -14
  127. package/docs/design/Lifecycle.dc.html +22 -0
  128. package/docs/design/Lifecycle600.dc.html +18 -0
  129. package/docs/design/LifecycleVariants.dc.html +157 -0
  130. package/docs/design/LinkAuthorship.dc.html +139 -0
  131. package/docs/design/LinksChanged.dc.html +203 -0
  132. package/docs/design/LinksOptions.dc.html +137 -0
  133. package/docs/design/LinksOptions600.dc.html +134 -0
  134. package/docs/design/NearPin.dc.html +9 -10
  135. package/docs/design/Pane600.dc.html +4 -3
  136. package/docs/design/Relations.dc.html +3 -2
  137. package/docs/design/Restore.dc.html +152 -0
  138. package/docs/design/SeeChange.dc.html +267 -0
  139. package/docs/design/Uses.dc.html +12 -6
  140. package/docs/design/canvas.json +146 -19
  141. package/docs/design.md +449 -16
  142. package/docs/environment.md +0 -1
  143. package/docs/evals/curation-effect.md +128 -0
  144. package/docs/http-api.md +459 -37
  145. package/docs/mcp.md +146 -18
  146. package/docs/node-types.md +58 -3
  147. package/docs/plans/design-gaps-2026-10-08.md +53 -0
  148. package/docs/plans/plan-009-tech-debt-backlog.md +379 -0
  149. package/docs/plans/plan-016-context-made-visible-wave-1.md +150 -5
  150. package/docs/plans/plan-017-knowledge-categories-and-lifecycle.md +175 -0
  151. package/docs/plans/plan-018-connected-memory-wave-2.md +594 -0
  152. package/docs/plans/plan-019-post-0.9-roadmap.md +198 -0
  153. package/docs/product-context-vision-2026-09.md +17 -1
  154. package/docs/product-vision-2026-09.md +1082 -43
  155. package/docs/product-vision-review-2026-09.md +13 -1
  156. package/docs/screenshots/workbench-live-dark.png +0 -0
  157. package/docs/screenshots/workbench-live-light.png +0 -0
  158. package/docs/sdk.md +62 -2
  159. package/package.json +1 -1
  160. package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/runtime.json +1 -1
  161. package/plugins/pmx-canvas/plugin.json +1 -1
  162. package/plugins/pmx-canvas/runtime.json +1 -1
  163. package/plugins/pmx-canvas/tools.json +601 -22
  164. package/skills/pmx-canvas/SKILL.md +216 -42
  165. package/skills/pmx-canvas/references/full-reference.md +120 -21
  166. package/skills/pmx-canvas/references/github-copilot-app-adapter.md +2 -2
  167. package/skills/pmx-canvas/references/importing-context.md +132 -0
  168. package/skills/pmx-canvas/references/installing-pmx-canvas.md +6 -6
  169. package/skills/pmx-canvas/references/tours-and-recording.md +16 -1
  170. package/skills/pmx-canvas-testing/SKILL.md +20 -0
  171. package/src/cli/agent.ts +2 -0
  172. package/src/cli/commands/ax.ts +12 -0
  173. package/src/cli/commands/boards.ts +87 -11
  174. package/src/cli/commands/edges.ts +28 -0
  175. package/src/cli/commands/groups.ts +7 -0
  176. package/src/cli/commands/nodes.ts +1 -0
  177. package/src/cli/commands/query.ts +34 -2
  178. package/src/cli/commands/record.ts +199 -48
  179. package/src/cli/index.ts +6 -1
  180. package/src/client/App.tsx +177 -70
  181. package/src/client/canvas/AttentionHistory.tsx +0 -2
  182. package/src/client/canvas/BarHint.tsx +40 -2
  183. package/src/client/canvas/BoardCopyDialog.tsx +1 -1
  184. package/src/client/canvas/BoardFolderDialog.tsx +7 -7
  185. package/src/client/canvas/BoardLinkDialog.tsx +138 -33
  186. package/src/client/canvas/BoardPinButton.tsx +33 -0
  187. package/src/client/canvas/BoardSwitcher.tsx +26 -40
  188. package/src/client/canvas/CanvasNode.tsx +360 -237
  189. package/src/client/canvas/CanvasViewport.tsx +45 -7
  190. package/src/client/canvas/CommandBar.tsx +29 -23
  191. package/src/client/canvas/CommandPalette.tsx +5 -12
  192. package/src/client/canvas/ConnectionBanner.tsx +3 -3
  193. package/src/client/canvas/ContextChip.tsx +296 -0
  194. package/src/client/canvas/ContextMenu.tsx +63 -5
  195. package/src/client/canvas/CrossBoardReferences.tsx +60 -0
  196. package/src/client/canvas/EdgeLayer.tsx +339 -76
  197. package/src/client/canvas/EmptyState.tsx +33 -18
  198. package/src/client/canvas/ExpandedNodeOverlay.tsx +72 -40
  199. package/src/client/canvas/ExternalWriters.tsx +57 -42
  200. package/src/client/canvas/HomeGraphView.tsx +942 -0
  201. package/src/client/canvas/HomeMapView.tsx +641 -0
  202. package/src/client/canvas/HomeReviewView.tsx +167 -0
  203. package/src/client/canvas/HomeView.tsx +687 -160
  204. package/src/client/canvas/IntentLayer.tsx +19 -12
  205. package/src/client/canvas/KnowledgeDialog.tsx +488 -0
  206. package/src/client/canvas/LifecycleChrome.tsx +133 -0
  207. package/src/client/canvas/LifecycleFields.tsx +91 -0
  208. package/src/client/canvas/LifecyclePanel.tsx +533 -0
  209. package/src/client/canvas/MemoryInspector.tsx +104 -0
  210. package/src/client/canvas/Minimap.tsx +3 -3
  211. package/src/client/canvas/NearPinLayer.tsx +187 -0
  212. package/src/client/canvas/NodeContextMark.tsx +175 -97
  213. package/src/client/canvas/Presentation.tsx +23 -57
  214. package/src/client/canvas/RelationPresetsDialog.tsx +413 -0
  215. package/src/client/canvas/RelationsPanel.tsx +82 -0
  216. package/src/client/canvas/SelectionBar.tsx +3 -6
  217. package/src/client/canvas/SessionPanel.tsx +90 -50
  218. package/src/client/canvas/SessionReceipt.tsx +311 -39
  219. package/src/client/canvas/ShortcutOverlay.tsx +1 -1
  220. package/src/client/canvas/SnapshotPanel.tsx +27 -36
  221. package/src/client/canvas/ToolRail.tsx +367 -192
  222. package/src/client/canvas/TopBar.tsx +354 -124
  223. package/src/client/canvas/canvas-area.ts +10 -3
  224. package/src/client/canvas/graph-geometry.ts +195 -0
  225. package/src/client/canvas/graph-model.ts +254 -0
  226. package/src/client/canvas/memory-layout.ts +106 -0
  227. package/src/client/canvas/use-chrome-height.ts +20 -0
  228. package/src/client/canvas/use-focus-trap.ts +31 -5
  229. package/src/client/canvas/use-memory-graph.ts +58 -0
  230. package/src/client/canvas/use-node-drag.ts +2 -1
  231. package/src/client/canvas/use-pan-zoom.ts +64 -7
  232. package/src/client/icons.tsx +0 -63
  233. package/src/client/mcp-app/style.css +0 -6
  234. package/src/client/nodes/AxStepControls.tsx +74 -50
  235. package/src/client/nodes/BoardNode.tsx +5 -1
  236. package/src/client/nodes/ContextNode.tsx +18 -11
  237. package/src/client/nodes/ExtAppFrame.tsx +0 -1
  238. package/src/client/nodes/FileNode.tsx +46 -42
  239. package/src/client/nodes/HtmlNode.tsx +32 -0
  240. package/src/client/nodes/ImageNode.tsx +1 -1
  241. package/src/client/nodes/InlineFormatBar.tsx +6 -9
  242. package/src/client/nodes/InlineMarkdownEditor.tsx +56 -17
  243. package/src/client/nodes/MarkdownNode.tsx +377 -109
  244. package/src/client/nodes/McpAppNode.tsx +50 -20
  245. package/src/client/nodes/MdFormatBar.tsx +21 -18
  246. package/src/client/nodes/PromptNode.tsx +1 -1
  247. package/src/client/nodes/ResponseNode.tsx +1 -1
  248. package/src/client/nodes/viewer-runtime-state.ts +54 -22
  249. package/src/client/state/attention-bridge.ts +64 -15
  250. package/src/client/state/boards-store.ts +193 -39
  251. package/src/client/state/canvas-store.ts +99 -12
  252. package/src/client/state/context-chip-store.ts +70 -0
  253. package/src/client/state/context-status-store.ts +53 -0
  254. package/src/client/state/intent-bridge.ts +51 -30
  255. package/src/client/state/knowledge-store.ts +43 -0
  256. package/src/client/state/near-pin-store.ts +115 -6
  257. package/src/client/state/presence-store.ts +7 -10
  258. package/src/client/state/presentation.ts +91 -0
  259. package/src/client/state/relation-store.ts +81 -0
  260. package/src/client/state/session-store.ts +144 -18
  261. package/src/client/state/sse-bridge.ts +206 -29
  262. package/src/client/state/workspace-settings.ts +11 -0
  263. package/src/client/theme/global.css +2724 -476
  264. package/src/client/types.ts +12 -0
  265. package/src/client/utils/keyboard.ts +10 -0
  266. package/src/client/utils/time-ago.ts +14 -0
  267. package/src/json-render/renderer/index.tsx +31 -0
  268. package/src/mcp/canvas-app.ts +12 -2
  269. package/src/mcp/context-uri-template.ts +7 -4
  270. package/src/mcp/server.ts +63 -41
  271. package/src/mcp/workbench-app.ts +39 -6
  272. package/src/server/agent-presence.ts +128 -28
  273. package/src/server/ax-context.ts +27 -13
  274. package/src/server/board-export.ts +12 -1
  275. package/src/server/canvas-db.ts +331 -42
  276. package/src/server/canvas-operations.ts +195 -83
  277. package/src/server/canvas-provenance.ts +53 -22
  278. package/src/server/canvas-serialization.ts +2 -0
  279. package/src/server/canvas-state.ts +950 -122
  280. package/src/server/canvas-validation.ts +9 -2
  281. package/src/server/context-brief.ts +345 -38
  282. package/src/server/context-reads.ts +178 -30
  283. package/src/server/demo-state.json +355 -24
  284. package/src/server/edit-summary.ts +74 -0
  285. package/src/server/html-surface.ts +8 -1
  286. package/src/server/index.ts +181 -11
  287. package/src/server/knowledge-lifecycle.ts +304 -0
  288. package/src/server/lifecycle-status.ts +232 -0
  289. package/src/server/memory-graph.ts +166 -0
  290. package/src/server/mutation-history.ts +18 -6
  291. package/src/server/operations/composites.ts +32 -11
  292. package/src/server/operations/http.ts +7 -2
  293. package/src/server/operations/index.ts +4 -0
  294. package/src/server/operations/mcp.ts +26 -1
  295. package/src/server/operations/ops/ax-presence.ts +1 -1
  296. package/src/server/operations/ops/ax-read.ts +57 -6
  297. package/src/server/operations/ops/ax-timeline.ts +28 -2
  298. package/src/server/operations/ops/backup.ts +5 -3
  299. package/src/server/operations/ops/batch.ts +35 -4
  300. package/src/server/operations/ops/boards.ts +188 -47
  301. package/src/server/operations/ops/canvas-wire.ts +31 -1
  302. package/src/server/operations/ops/edges.ts +36 -2
  303. package/src/server/operations/ops/groups.ts +6 -1
  304. package/src/server/operations/ops/lifecycle.ts +262 -0
  305. package/src/server/operations/ops/memory-graph.ts +45 -0
  306. package/src/server/operations/ops/nodes.ts +107 -24
  307. package/src/server/operations/ops/query.ts +281 -68
  308. package/src/server/operations/ops/snapshots.ts +73 -1
  309. package/src/server/operations/ops/validate.ts +5 -1
  310. package/src/server/operations/ops/viewport.ts +97 -3
  311. package/src/server/operations/registry.ts +74 -25
  312. package/src/server/placement.ts +29 -0
  313. package/src/server/scope-fence.ts +78 -6
  314. package/src/server/server.ts +115 -16
  315. package/src/server/spatial-analysis.ts +48 -23
  316. package/src/server/tour-control.ts +82 -0
  317. package/src/server/wiki-links.ts +137 -0
  318. package/src/server/workbench-auth.ts +11 -0
  319. package/src/shared/agent-context.ts +3 -0
  320. package/src/shared/agent-presence.ts +15 -15
  321. package/src/shared/auto-arrange.ts +2 -0
  322. package/src/shared/boards.ts +4 -4
  323. package/src/shared/knowledge-lifecycle.ts +22 -0
  324. package/src/shared/knowledge-presets.ts +80 -0
  325. package/src/shared/lifecycle-status.ts +26 -0
  326. package/src/shared/memory-graph.ts +74 -0
  327. package/src/shared/relations.ts +98 -0
  328. package/src/shared/semantic-attention.ts +15 -1
  329. package/src/shared/tour.ts +23 -5
  330. package/src/shared/workspace-settings.ts +13 -0
  331. package/dist/types/client/canvas/ContextPinBar.d.ts +0 -1
  332. package/dist/types/client/canvas/FocusFieldLayer.d.ts +0 -1
  333. package/dist/types/client/canvas/kind-colors.d.ts +0 -3
  334. package/dist/types/server/agent-context.d.ts +0 -22
  335. package/src/client/canvas/ContextPinBar.tsx +0 -25
@@ -1,9 +1,10 @@
1
1
  import type { RefObject } from 'preact';
2
- import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
2
+ import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'preact/hooks';
3
3
  import { HTML_SURFACE_PUSH_SOURCE } from '../../shared/ax-surface-protocol.js';
4
4
  import { canvasThemeScheme } from '../../shared/themes.js';
5
5
  import type { CanvasNodeState } from '../types';
6
6
  import { axSurfaceState, canvasTheme } from '../state/canvas-store';
7
+ import { showToast } from '../state/attention-bridge';
7
8
  import { iframeMode } from '../state/iframe-mode';
8
9
  import { shouldContentFitIframeNode } from '../canvas/auto-fit';
9
10
  import { ExtAppFrame } from './ExtAppFrame';
@@ -65,9 +66,9 @@ type ViewerFrameSource = { src?: string; srcdoc?: string };
65
66
  /**
66
67
  * Keep the last painted viewer in front while its replacement navigates. Viewer
67
68
  * specs are immutable documents, so updates normally change `?v=` and reload the
68
- * iframe; putting the new document in a hidden sibling avoids exposing Chromium's
69
- * white navigation paint. Two animation frames after load gives the new document
70
- * a composited paint before the old one is removed.
69
+ * iframe; putting the new document behind the old one conceals Chromium's white
70
+ * navigation paint without suppressing rendering. Wait for restore and render commit,
71
+ * then two animation frames before the old viewer is removed.
71
72
  */
72
73
  export function RefreshingViewerFrame({
73
74
  source,
@@ -83,7 +84,7 @@ export function RefreshingViewerFrame({
83
84
  source: ViewerFrameSource;
84
85
  iframeRef: RefObject<HTMLIFrameElement>;
85
86
  onLoad: () => void;
86
- onDocumentLoad?: (frame: HTMLIFrameElement) => void;
87
+ onDocumentLoad?: (frame: HTMLIFrameElement, signal: AbortSignal) => void | Promise<boolean | 'cancelled' | void>;
87
88
  title: string;
88
89
  className?: string;
89
90
  sandbox?: string;
@@ -91,50 +92,77 @@ export function RefreshingViewerFrame({
91
92
  tabIndex?: number;
92
93
  }) {
93
94
  const sourceKey = source.src ?? source.srcdoc ?? '';
94
- const [painted, setPainted] = useState({ key: sourceKey, source });
95
- const [pending, setPending] = useState<{ key: string; source: ViewerFrameSource } | null>(null);
95
+ const [painted, setPainted] = useState({ key: sourceKey, source, attempt: 0 });
96
+ const [pending, setPending] = useState<{ key: string; source: ViewerFrameSource; attempt: number } | null>(null);
96
97
  const promotionRef = useRef(0);
98
+ const readiness = useRef(new Set<AbortController>());
97
99
 
98
- useEffect(() => {
100
+ useLayoutEffect(() => {
99
101
  promotionRef.current += 1;
102
+ for (const controller of readiness.current) controller.abort();
103
+ readiness.current.clear();
100
104
  if (!sourceKey || sourceKey === painted.key) {
101
105
  setPending(null);
102
106
  return;
103
107
  }
104
108
  if (!painted.key) {
105
- setPainted({ key: sourceKey, source });
109
+ setPainted({ key: sourceKey, source, attempt: 0 });
106
110
  return;
107
111
  }
108
- setPending({ key: sourceKey, source });
112
+ setPending({ key: sourceKey, source, attempt: 0 });
109
113
  }, [sourceKey, painted.key]);
110
114
 
111
115
  useEffect(
112
116
  () => () => {
113
117
  promotionRef.current += 1;
118
+ for (const controller of readiness.current) controller.abort();
119
+ readiness.current.clear();
114
120
  },
115
121
  [],
116
122
  );
117
123
 
118
- const frame = (entry: { key: string; source: ViewerFrameSource }, isPending: boolean) => (
124
+ const frame = (entry: { key: string; source: ViewerFrameSource; attempt: number }, isPending: boolean) => (
119
125
  <iframe
120
- key={entry.key}
126
+ key={`${entry.key}:${entry.attempt}`}
121
127
  ref={isPending ? undefined : iframeRef}
122
128
  {...entry.source}
123
129
  class={className}
124
130
  sandbox={sandbox}
125
131
  allow={allow}
126
132
  tabIndex={tabIndex}
133
+ inert={isPending}
134
+ aria-hidden={isPending ? true : undefined}
127
135
  loading={iframeMode.value === 'srcdoc' ? undefined : 'lazy'}
128
- onLoad={(event) => {
136
+ onLoad={async (event) => {
129
137
  // Seed runtime state while a replacement is still behind the painted
130
- // frame, rather than mounting an empty viewer after promotion.
131
- onDocumentLoad?.(event.currentTarget);
138
+ // frame, and wait until that document has committed its rendered UI.
139
+ const token = isPending ? ++promotionRef.current : promotionRef.current;
140
+ const loadedFrame = event.currentTarget;
141
+ const controller = new AbortController();
142
+ readiness.current.add(controller);
143
+ const ready = onDocumentLoad?.(loadedFrame, controller.signal);
144
+ const loaded = ready ? await ready : undefined;
145
+ readiness.current.delete(controller);
146
+ if (promotionRef.current !== token || loaded === 'cancelled') return;
147
+ if (loaded === false) {
148
+ if (entry.attempt === 0) {
149
+ const retry = { ...entry, attempt: 1 };
150
+ if (isPending) setPending(retry);
151
+ else setPainted(retry);
152
+ } else {
153
+ if (isPending) setPending(null);
154
+ showToast(
155
+ 'remove',
156
+ 'Viewer could not load',
157
+ `${isPending ? 'Keeping the last view. ' : ''}Reload to retry.`,
158
+ );
159
+ }
160
+ return;
161
+ }
132
162
  if (!isPending) {
133
163
  onLoad();
134
164
  return;
135
165
  }
136
- const token = ++promotionRef.current;
137
- const loadedFrame = event.currentTarget;
138
166
  requestAnimationFrame(() =>
139
167
  requestAnimationFrame(() => {
140
168
  if (promotionRef.current !== token) return;
@@ -150,7 +178,8 @@ export function RefreshingViewerFrame({
150
178
  inset: 0,
151
179
  width: '100%',
152
180
  height: '100%',
153
- visibility: isPending ? 'hidden' : 'visible',
181
+ visibility: 'visible',
182
+ zIndex: isPending ? 0 : 1,
154
183
  background: 'var(--c-panel-soft)',
155
184
  }}
156
185
  title={title}
@@ -299,8 +328,9 @@ function McpAppViewer({ node, expanded }: { node: CanvasNodeState; expanded: boo
299
328
  <RefreshingViewerFrame
300
329
  source={surfaceFrame}
301
330
  iframeRef={iframeRef}
302
- onDocumentLoad={(frame) => {
303
- if (stateToken && frame.contentWindow) restoreViewerRuntimeState(node.id, stateToken, frame.contentWindow);
331
+ onDocumentLoad={(frame, signal) => {
332
+ if (stateToken && frame.contentWindow)
333
+ return restoreViewerRuntimeState(node.id, stateToken, frame.contentWindow, signal);
304
334
  }}
305
335
  onLoad={pushAxState}
306
336
  title={`MCP App: ${sourceServer}`}
@@ -1,4 +1,5 @@
1
1
  import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
2
+ import { BarHint } from '../canvas/BarHint';
2
3
  import { modChord } from '../utils/platform';
3
4
  import { type FormatAction, FORMAT_ACTIONS, getSelectionRect } from './md-format';
4
5
 
@@ -83,27 +84,29 @@ export function MdFormatBar({ textareaRef }: { textareaRef: { current: HTMLTextA
83
84
  onMouseDown={(e) => e.preventDefault()}
84
85
  >
85
86
  {PRIMARY_ACTIONS.map((a) => (
86
- <button
87
- key={a.key}
88
- type="button"
89
- class={`md-format-btn md-format-btn-${a.key}`}
90
- title={`${a.label} (${modChord(a.shortcut!.toUpperCase())})`}
91
- onClick={() => runAction(a)}
92
- >
93
- {a.icon}
94
- </button>
87
+ <BarHint key={a.key} label={a.label} shortcut={modChord(a.shortcut!.toUpperCase())}>
88
+ <button
89
+ type="button"
90
+ class={`md-format-btn md-format-btn-${a.key}`}
91
+ aria-label={a.label}
92
+ onClick={() => runAction(a)}
93
+ >
94
+ {a.icon}
95
+ </button>
96
+ </BarHint>
95
97
  ))}
96
98
  <div class="md-format-divider" />
97
99
  {SECONDARY_ACTIONS.map((a) => (
98
- <button
99
- key={a.key}
100
- type="button"
101
- class={`md-format-btn md-format-btn-${a.key}`}
102
- title={a.label}
103
- onClick={() => runAction(a)}
104
- >
105
- {a.icon}
106
- </button>
100
+ <BarHint key={a.key} label={a.label}>
101
+ <button
102
+ type="button"
103
+ class={`md-format-btn md-format-btn-${a.key}`}
104
+ aria-label={a.label}
105
+ onClick={() => runAction(a)}
106
+ >
107
+ {a.icon}
108
+ </button>
109
+ </BarHint>
107
110
  ))}
108
111
  </div>
109
112
  );
@@ -169,7 +169,7 @@ export function PromptNode({ node, expanded = false }: { node: CanvasNodeState;
169
169
  Promise.all(
170
170
  assistantTurns.map(async ({ turn, index }) => {
171
171
  const html = await renderMarkdown(turn.text);
172
- return { index, html: sanitizeMarkdownHtml(html) };
172
+ return { index, html: sanitizeMarkdownHtml(html ?? '') };
173
173
  }),
174
174
  ).then((results) => {
175
175
  if (cancelled) return;
@@ -36,7 +36,7 @@ export function ResponseNode({ node, expanded = false }: { node: CanvasNodeState
36
36
  }
37
37
  let cancelled = false;
38
38
  renderMarkdown(content).then((html) => {
39
- if (!cancelled) setRendered(sanitizeMarkdownHtml(html));
39
+ if (!cancelled) setRendered(sanitizeMarkdownHtml(html ?? ''));
40
40
  });
41
41
  return () => {
42
42
  cancelled = true;
@@ -7,7 +7,7 @@ type Snapshot = {
7
7
  version: unknown;
8
8
  documentUrl: unknown;
9
9
  state: Record<string, unknown> | null;
10
- owners: Array<{ token: string; source: Window }>;
10
+ owners: Array<{ token: string; source: Window; mounted: () => void; ready: () => void; cancel: () => void }>;
11
11
  };
12
12
 
13
13
  // Tab-local UI state only: never persist form drafts in canvas data or AX. Keep
@@ -17,7 +17,12 @@ type Snapshot = {
17
17
  const snapshots = new Map<string, Snapshot>();
18
18
  let listening = false;
19
19
 
20
- export function restoreViewerRuntimeState(nodeId: string, token: string, source: Window): void {
20
+ export function restoreViewerRuntimeState(
21
+ nodeId: string,
22
+ token: string,
23
+ source: Window,
24
+ signal?: AbortSignal,
25
+ ): Promise<boolean | 'cancelled'> {
21
26
  if (!listening) {
22
27
  listening = true;
23
28
  effect(() => {
@@ -30,8 +35,10 @@ export function restoreViewerRuntimeState(nodeId: string, token: string, source:
30
35
  snapshot.epoch !== epoch ||
31
36
  snapshot.version !== node.data.specVersion ||
32
37
  snapshot.documentUrl !== node.data.url
33
- )
38
+ ) {
39
+ for (const owner of snapshot.owners) owner.cancel();
34
40
  snapshots.delete(id);
41
+ }
35
42
  }
36
43
  });
37
44
  window.addEventListener('message', (event: MessageEvent) => {
@@ -42,37 +49,62 @@ export function restoreViewerRuntimeState(nodeId: string, token: string, source:
42
49
  nodeId?: string;
43
50
  state?: unknown;
44
51
  } | null;
45
- if (
46
- !message ||
47
- message.source !== JSON_VIEWER_STATE_SOURCE ||
48
- message.type !== 'snapshot' ||
49
- typeof message.nodeId !== 'string'
50
- )
51
- return;
52
+ if (!message || message.source !== JSON_VIEWER_STATE_SOURCE || typeof message.nodeId !== 'string') return;
52
53
  const snapshot = snapshots.get(message.nodeId);
53
54
  const ownerIndex =
54
55
  snapshot?.owners.findIndex((owner) => owner.token === message.token && owner.source === event.source) ?? -1;
55
56
  if (!snapshot || ownerIndex < 0) return;
57
+ if (message.type === 'mounted') {
58
+ snapshot.owners[ownerIndex]!.mounted();
59
+ return;
60
+ }
61
+ if (message.type === 'ready') {
62
+ snapshot.owners[ownerIndex]!.ready();
63
+ return;
64
+ }
65
+ if (message.type !== 'snapshot') return;
56
66
  if (!message.state || typeof message.state !== 'object' || Array.isArray(message.state)) return;
57
67
  const { ax: _ax, ...state } = message.state as Record<string, unknown>;
58
68
  snapshot.state = state;
59
69
  // Once the replacement emits, the retired document cannot overwrite it.
70
+ const retired = snapshot.owners.slice(0, ownerIndex);
60
71
  snapshot.owners = snapshot.owners.slice(ownerIndex);
72
+ for (const owner of retired) owner.cancel();
61
73
  });
62
74
  }
63
75
  const node = nodes.value.get(nodeId);
64
- if (!node) return;
65
- let snapshot = snapshots.get(nodeId);
66
- if (!snapshot) {
67
- snapshot = {
68
- epoch: canvasInteractionEpoch.value,
69
- version: node.data.specVersion,
70
- documentUrl: node.data.url,
71
- state: null,
72
- owners: [],
76
+ if (!node || signal?.aborted) return Promise.resolve('cancelled');
77
+ const snapshot: Snapshot = snapshots.get(nodeId) ?? {
78
+ epoch: canvasInteractionEpoch.value,
79
+ version: node.data.specVersion,
80
+ documentUrl: node.data.url,
81
+ state: null,
82
+ owners: [],
83
+ };
84
+ snapshots.set(nodeId, snapshot);
85
+ const ready = new Promise<boolean | 'cancelled'>((resolve) => {
86
+ const finish = (loaded: boolean | 'cancelled') => {
87
+ clearTimeout(timer);
88
+ signal?.removeEventListener('abort', owner.cancel);
89
+ if (loaded !== true) snapshot.owners = snapshot.owners.filter((entry) => entry !== owner);
90
+ resolve(loaded);
73
91
  };
74
- snapshots.set(nodeId, snapshot);
75
- }
76
- snapshot.owners = [...snapshot.owners.filter((owner) => owner.source !== source).slice(-1), { token, source }];
92
+ const owner = {
93
+ token,
94
+ source,
95
+ mounted: () => clearTimeout(timer),
96
+ ready: () => finish(true),
97
+ cancel: () => finish('cancelled'),
98
+ };
99
+ // A missing runtime fails, but a healthy offscreen document can wait for
100
+ // paint indefinitely. Its synchronous mount acknowledgement ends the deadline;
101
+ // only the later frame-gated ready message may promote it.
102
+ const timer = setTimeout(() => finish(false), 10_000);
103
+ for (const previous of snapshot.owners.filter((entry) => entry.source === source)) previous.cancel();
104
+ while (snapshot.owners.length > 1) snapshot.owners[0]!.cancel();
105
+ snapshot.owners.push(owner);
106
+ signal?.addEventListener('abort', owner.cancel, { once: true });
107
+ });
77
108
  source.postMessage({ source: JSON_VIEWER_STATE_SOURCE, type: 'restore', token, state: snapshot.state }, '*');
109
+ return ready;
78
110
  }
@@ -16,6 +16,8 @@ import {
16
16
  type AttentionEntry,
17
17
  type AttentionTone,
18
18
  } from './attention-store';
19
+ import { nearPins } from './near-pin-store';
20
+ import { activeBoardId } from './boards-store';
19
21
 
20
22
  let reducer = new SemanticWatchReducer();
21
23
  let toastQueue: AttentionEntry[] = [];
@@ -77,13 +79,39 @@ function formatReason(reason: string): string {
77
79
  if (reason === 'left cluster') return 'moved away from its cluster';
78
80
  if (reason === 'cluster changed') return 'shifted into a different cluster';
79
81
  if (reason === 'pinned neighborhood changed') return 'changed the local focus field';
80
- const enteredMatch = /^entered pinned neighborhood of "(.+)"$/.exec(reason);
81
- if (enteredMatch) return `moved into focus around ${enteredMatch[1]}`;
82
- const leftMatch = /^left pinned neighborhood of "(.+)"$/.exec(reason);
83
- if (leftMatch) return `moved out of focus around ${leftMatch[1]}`;
84
82
  return reason;
85
83
  }
86
84
 
85
+ function pinTitlesFrom(reasons: string[], verb: 'entered' | 'left'): string[] {
86
+ const pattern = new RegExp(`^${verb} pinned neighborhood of "(.+)"$`);
87
+ return reasons.flatMap((reason) => pattern.exec(reason)?.[1] ?? []);
88
+ }
89
+
90
+ /**
91
+ * NearPin.dc.html §3: one entry naming the node and the pin, saying what the
92
+ * agent gets. "Out of the brief" only once the node is near no pin at all.
93
+ */
94
+ function nearPinEntry(name: string, nodeId: string, reasons: string[]): { title: string; body: string } | null {
95
+ const quoted = (titles: string[]) => titles.map((title) => `“${title}”`).join(' and ');
96
+ const entered = pinTitlesFrom(reasons, 'entered');
97
+ if (entered.length > 0) {
98
+ return {
99
+ title: 'Near a pin',
100
+ body: `“${name}” is now near ${quoted(entered)} — the agent gets its title and summary.`,
101
+ };
102
+ }
103
+ const left = pinTitlesFrom(reasons, 'left');
104
+ if (left.length === 0) return null;
105
+ const still = nearPins.value.get(nodeId) ?? [];
106
+ return {
107
+ title: 'Left a pin',
108
+ body:
109
+ still.length > 0
110
+ ? `“${name}” left ${quoted(left)} — still near ${quoted(still.map((pin) => pin.pinTitle))}.`
111
+ : `“${name}” left ${quoted(left)} — it is out of the agent’s brief.`,
112
+ };
113
+ }
114
+
87
115
  function entryFromContextPin(event: ContextPinWatchEvent): AttentionEntry | null {
88
116
  const addedNames = event.added.map((node) => quoteLabel(node.title, node.id));
89
117
  const removedNames = event.removed.map((node) => quoteLabel(node.title, node.id));
@@ -118,7 +146,7 @@ function entryFromConnect(event: ConnectWatchEvent): AttentionEntry | null {
118
146
  'relationship',
119
147
  'Relationship added',
120
148
  `${quoteLabel(edge.fromTitle, edge.fromId)} linked to ${quoteLabel(edge.toTitle, edge.toId)}`,
121
- [edge.fromId, edge.toId],
149
+ [edge.fromId, ...(!edge.toBoardId || edge.toBoardId === activeBoardId.value ? [edge.toId] : [])],
122
150
  event.timestamp ? Date.parse(event.timestamp) || Date.now() : Date.now(),
123
151
  );
124
152
  }
@@ -127,13 +155,33 @@ function entryFromConnect(event: ConnectWatchEvent): AttentionEntry | null {
127
155
  'relationship',
128
156
  'Relationships added',
129
157
  `${event.edges.length} connections changed the board structure`,
130
- event.edges.flatMap((edge) => [edge.fromId, edge.toId]),
158
+ event.edges.flatMap((edge) => [
159
+ edge.fromId,
160
+ ...(!edge.toBoardId || edge.toBoardId === activeBoardId.value ? [edge.toId] : []),
161
+ ]),
131
162
  event.timestamp ? Date.parse(event.timestamp) || Date.now() : Date.now(),
132
163
  );
133
164
  }
134
165
 
135
166
  function entryFromRemove(event: RemoveWatchEvent): AttentionEntry | null {
136
167
  if (event.nodes.length === 0 && event.edges.length === 0) return null;
168
+ // LinksChanged.dc.html: a removed link leaves no line to mark, so Updates names it.
169
+ if (event.nodes.length === 0) {
170
+ const links = event.edges.map(
171
+ (edge) =>
172
+ `“${quoteLabel(edge.fromTitle, edge.fromId)}” – “${quoteLabel(edge.toTitle, edge.toId)}” (${edge.edgeType})`,
173
+ );
174
+ return makeEntry(
175
+ 'remove',
176
+ links.length === 1 ? 'Link removed' : 'Links removed',
177
+ summarizeNames(links),
178
+ event.edges.flatMap((edge) => [
179
+ edge.fromId,
180
+ ...(!edge.toBoardId || edge.toBoardId === activeBoardId.value ? [edge.toId] : []),
181
+ ]),
182
+ event.timestamp ? Date.parse(event.timestamp) || Date.now() : Date.now(),
183
+ );
184
+ }
137
185
  const nodeNames = event.nodes.map((node) => quoteLabel(node.title, node.id));
138
186
  const parts: string[] = [];
139
187
  if (nodeNames.length > 0) parts.push(summarizeNames(nodeNames));
@@ -144,7 +192,13 @@ function entryFromRemove(event: RemoveWatchEvent): AttentionEntry | null {
144
192
  'remove',
145
193
  'Items removed',
146
194
  parts.join(' · '),
147
- [...event.nodes.map((node) => node.id), ...event.edges.flatMap((edge) => [edge.fromId, edge.toId])],
195
+ [
196
+ ...event.nodes.map((node) => node.id),
197
+ ...event.edges.flatMap((edge) => [
198
+ edge.fromId,
199
+ ...(!edge.toBoardId || edge.toBoardId === activeBoardId.value ? [edge.toId] : []),
200
+ ]),
201
+ ],
148
202
  event.timestamp ? Date.parse(event.timestamp) || Date.now() : Date.now(),
149
203
  );
150
204
  }
@@ -184,14 +238,9 @@ function entryFromMoveEnd(event: MoveEndWatchEvent): AttentionEntry | null {
184
238
  const createdAt = event.timestamp ? Date.parse(event.timestamp) || Date.now() : Date.now();
185
239
 
186
240
  if (reasons.some((reason) => reason.includes('pinned neighborhood'))) {
187
- if (event.nodes.length === 1) {
188
- return makeEntry(
189
- 'neighborhood',
190
- 'Neighborhood changed',
191
- `${names[0]} ${formatReason(event.nodes[0].reasons[0])}`,
192
- event.nodes.map((node) => node.id),
193
- createdAt,
194
- );
241
+ const near = event.nodes.length === 1 ? nearPinEntry(names[0], event.nodes[0].id, event.nodes[0].reasons) : null;
242
+ if (near) {
243
+ return makeEntry('neighborhood', near.title, near.body, [event.nodes[0].id], createdAt);
195
244
  }
196
245
  return makeEntry(
197
246
  'neighborhood',