pmx-canvas 0.8.2 → 0.9.1

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 (338) 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 +59 -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/human-store.d.ts +2 -1
  53. package/dist/types/client/state/intent-bridge.d.ts +11 -8
  54. package/dist/types/client/state/knowledge-store.d.ts +22 -0
  55. package/dist/types/client/state/near-pin-store.d.ts +44 -1
  56. package/dist/types/client/state/presence-store.d.ts +4 -4
  57. package/dist/types/client/state/presentation.d.ts +28 -0
  58. package/dist/types/client/state/relation-store.d.ts +10 -0
  59. package/dist/types/client/state/session-store.d.ts +41 -2
  60. package/dist/types/client/state/workspace-settings.d.ts +7 -0
  61. package/dist/types/client/types.d.ts +13 -1
  62. package/dist/types/client/utils/keyboard.d.ts +2 -0
  63. package/dist/types/client/utils/time-ago.d.ts +4 -0
  64. package/dist/types/mcp/context-uri-template.d.ts +3 -1
  65. package/dist/types/server/agent-presence.d.ts +10 -3
  66. package/dist/types/server/ax-context.d.ts +1 -1
  67. package/dist/types/server/canvas-db.d.ts +30 -7
  68. package/dist/types/server/canvas-operations.d.ts +24 -2
  69. package/dist/types/server/canvas-provenance.d.ts +22 -2
  70. package/dist/types/server/canvas-serialization.d.ts +2 -0
  71. package/dist/types/server/canvas-state.d.ts +93 -15
  72. package/dist/types/server/canvas-validation.d.ts +1 -1
  73. package/dist/types/server/context-brief.d.ts +94 -6
  74. package/dist/types/server/context-reads.d.ts +33 -7
  75. package/dist/types/server/edit-summary.d.ts +20 -0
  76. package/dist/types/server/index.d.ts +71 -10
  77. package/dist/types/server/knowledge-lifecycle.d.ts +221 -0
  78. package/dist/types/server/lifecycle-status.d.ts +10 -0
  79. package/dist/types/server/memory-graph.d.ts +9 -0
  80. package/dist/types/server/mutation-history.d.ts +2 -0
  81. package/dist/types/server/operations/ops/ax-shared.d.ts +1 -1
  82. package/dist/types/server/operations/ops/boards.d.ts +2 -1
  83. package/dist/types/server/operations/ops/lifecycle.d.ts +2 -0
  84. package/dist/types/server/operations/ops/memory-graph.d.ts +2 -0
  85. package/dist/types/server/operations/ops/nodes.d.ts +5 -0
  86. package/dist/types/server/operations/ops/query.d.ts +23 -0
  87. package/dist/types/server/operations/registry.d.ts +1 -0
  88. package/dist/types/server/placement.d.ts +7 -0
  89. package/dist/types/server/scope-fence.d.ts +2 -1
  90. package/dist/types/server/spatial-analysis.d.ts +6 -3
  91. package/dist/types/server/tour-control.d.ts +32 -0
  92. package/dist/types/server/wiki-links.d.ts +14 -0
  93. package/dist/types/server/workbench-auth.d.ts +2 -0
  94. package/dist/types/shared/agent-context.d.ts +2 -0
  95. package/dist/types/shared/agent-presence.d.ts +15 -12
  96. package/dist/types/shared/auto-arrange.d.ts +1 -0
  97. package/dist/types/shared/boards.d.ts +4 -4
  98. package/dist/types/shared/knowledge-lifecycle.d.ts +41 -0
  99. package/dist/types/shared/knowledge-presets.d.ts +6 -0
  100. package/dist/types/shared/lifecycle-status.d.ts +24 -0
  101. package/dist/types/shared/memory-graph.d.ts +53 -0
  102. package/dist/types/shared/relations.d.ts +68 -0
  103. package/dist/types/shared/semantic-attention.d.ts +2 -0
  104. package/dist/types/shared/tour.d.ts +38 -0
  105. package/dist/types/shared/workspace-settings.d.ts +11 -0
  106. package/docs/RELEASE.md +21 -3
  107. package/docs/adr-001-bun-only-runtime.md +7 -1
  108. package/docs/adr-002-shared-product-separate-runtimes.md +93 -0
  109. package/docs/api-reference.md +109 -0
  110. package/docs/ax-host-adapter-contract.md +1 -1
  111. package/docs/ax-state-contract.md +5 -4
  112. package/docs/cli.md +153 -15
  113. package/docs/demos/pmx-canvas-demo.mp4 +0 -0
  114. package/docs/design/AgentContext.dc.html +3 -3
  115. package/docs/design/BoardPins.dc.html +37 -22
  116. package/docs/design/Categories.dc.html +208 -0
  117. package/docs/design/CategoryEditor.dc.html +33 -0
  118. package/docs/design/Chrome.dc.html +52 -9
  119. package/docs/design/ContextChip.dc.html +224 -0
  120. package/docs/design/ContextChip600.dc.html +136 -0
  121. package/docs/design/EmptyState.dc.html +45 -0
  122. package/docs/design/Flows.dc.html +23 -1
  123. package/docs/design/HeaderMarks.dc.html +183 -0
  124. package/docs/design/Home.dc.html +19 -1
  125. package/docs/design/HomeGraph.dc.html +86 -202
  126. package/docs/design/HomeMap.dc.html +23 -1
  127. package/docs/design/HomeTeam.dc.html +32 -14
  128. package/docs/design/Lifecycle.dc.html +22 -0
  129. package/docs/design/Lifecycle600.dc.html +18 -0
  130. package/docs/design/LifecycleVariants.dc.html +157 -0
  131. package/docs/design/LinkAuthorship.dc.html +139 -0
  132. package/docs/design/LinksChanged.dc.html +203 -0
  133. package/docs/design/LinksOptions.dc.html +137 -0
  134. package/docs/design/LinksOptions600.dc.html +134 -0
  135. package/docs/design/NearPin.dc.html +9 -10
  136. package/docs/design/Pane600.dc.html +4 -3
  137. package/docs/design/Relations.dc.html +3 -2
  138. package/docs/design/Restore.dc.html +152 -0
  139. package/docs/design/SeeChange.dc.html +267 -0
  140. package/docs/design/Uses.dc.html +12 -6
  141. package/docs/design/canvas.json +146 -19
  142. package/docs/design.md +449 -16
  143. package/docs/environment.md +0 -1
  144. package/docs/evals/curation-effect.md +128 -0
  145. package/docs/http-api.md +459 -37
  146. package/docs/mcp.md +146 -18
  147. package/docs/node-types.md +58 -3
  148. package/docs/plans/design-gaps-2026-10-08.md +53 -0
  149. package/docs/plans/plan-009-tech-debt-backlog.md +379 -0
  150. package/docs/plans/plan-016-context-made-visible-wave-1.md +150 -5
  151. package/docs/plans/plan-017-knowledge-categories-and-lifecycle.md +175 -0
  152. package/docs/plans/plan-018-connected-memory-wave-2.md +594 -0
  153. package/docs/plans/plan-019-post-0.9-roadmap.md +198 -0
  154. package/docs/product-context-vision-2026-09.md +17 -1
  155. package/docs/product-vision-2026-09.md +1082 -43
  156. package/docs/product-vision-review-2026-09.md +13 -1
  157. package/docs/screenshots/workbench-live-dark.png +0 -0
  158. package/docs/screenshots/workbench-live-light.png +0 -0
  159. package/docs/sdk.md +62 -2
  160. package/package.json +1 -1
  161. package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/runtime.json +1 -1
  162. package/plugins/pmx-canvas/plugin.json +1 -1
  163. package/plugins/pmx-canvas/runtime.json +1 -1
  164. package/plugins/pmx-canvas/tools.json +601 -22
  165. package/skills/pmx-canvas/SKILL.md +216 -42
  166. package/skills/pmx-canvas/evals/evals.json +224 -0
  167. package/skills/pmx-canvas/references/full-reference.md +120 -21
  168. package/skills/pmx-canvas/references/github-copilot-app-adapter.md +2 -2
  169. package/skills/pmx-canvas/references/importing-context.md +132 -0
  170. package/skills/pmx-canvas/references/installing-pmx-canvas.md +6 -6
  171. package/skills/pmx-canvas/references/tours-and-recording.md +16 -1
  172. package/skills/pmx-canvas-testing/SKILL.md +20 -0
  173. package/src/cli/agent.ts +2 -0
  174. package/src/cli/commands/ax.ts +12 -0
  175. package/src/cli/commands/boards.ts +88 -11
  176. package/src/cli/commands/edges.ts +28 -0
  177. package/src/cli/commands/groups.ts +7 -0
  178. package/src/cli/commands/nodes.ts +1 -0
  179. package/src/cli/commands/query.ts +34 -2
  180. package/src/cli/commands/record.ts +199 -48
  181. package/src/cli/index.ts +6 -1
  182. package/src/client/App.tsx +177 -70
  183. package/src/client/canvas/AttentionHistory.tsx +0 -2
  184. package/src/client/canvas/BarHint.tsx +40 -2
  185. package/src/client/canvas/BoardCopyDialog.tsx +1 -1
  186. package/src/client/canvas/BoardFolderDialog.tsx +7 -7
  187. package/src/client/canvas/BoardLinkDialog.tsx +138 -33
  188. package/src/client/canvas/BoardPinButton.tsx +33 -0
  189. package/src/client/canvas/BoardSwitcher.tsx +26 -40
  190. package/src/client/canvas/CanvasNode.tsx +360 -237
  191. package/src/client/canvas/CanvasViewport.tsx +45 -7
  192. package/src/client/canvas/CommandBar.tsx +29 -23
  193. package/src/client/canvas/CommandPalette.tsx +5 -12
  194. package/src/client/canvas/ConnectionBanner.tsx +3 -3
  195. package/src/client/canvas/ContextChip.tsx +296 -0
  196. package/src/client/canvas/ContextMenu.tsx +63 -5
  197. package/src/client/canvas/CrossBoardReferences.tsx +60 -0
  198. package/src/client/canvas/EdgeLayer.tsx +339 -76
  199. package/src/client/canvas/EmptyState.tsx +33 -18
  200. package/src/client/canvas/ExpandedNodeOverlay.tsx +72 -40
  201. package/src/client/canvas/ExternalWriters.tsx +57 -42
  202. package/src/client/canvas/HomeGraphView.tsx +942 -0
  203. package/src/client/canvas/HomeMapView.tsx +641 -0
  204. package/src/client/canvas/HomeReviewView.tsx +167 -0
  205. package/src/client/canvas/HomeView.tsx +687 -160
  206. package/src/client/canvas/IntentLayer.tsx +19 -12
  207. package/src/client/canvas/KnowledgeDialog.tsx +488 -0
  208. package/src/client/canvas/LifecycleChrome.tsx +133 -0
  209. package/src/client/canvas/LifecycleFields.tsx +91 -0
  210. package/src/client/canvas/LifecyclePanel.tsx +543 -0
  211. package/src/client/canvas/MemoryInspector.tsx +104 -0
  212. package/src/client/canvas/Minimap.tsx +3 -3
  213. package/src/client/canvas/NearPinLayer.tsx +187 -0
  214. package/src/client/canvas/NodeContextMark.tsx +175 -97
  215. package/src/client/canvas/Presentation.tsx +23 -57
  216. package/src/client/canvas/RelationPresetsDialog.tsx +413 -0
  217. package/src/client/canvas/RelationsPanel.tsx +82 -0
  218. package/src/client/canvas/SelectionBar.tsx +3 -6
  219. package/src/client/canvas/SessionPanel.tsx +90 -50
  220. package/src/client/canvas/SessionReceipt.tsx +311 -39
  221. package/src/client/canvas/ShortcutOverlay.tsx +1 -1
  222. package/src/client/canvas/SnapshotPanel.tsx +27 -36
  223. package/src/client/canvas/ToolRail.tsx +367 -192
  224. package/src/client/canvas/TopBar.tsx +354 -124
  225. package/src/client/canvas/canvas-area.ts +10 -3
  226. package/src/client/canvas/graph-geometry.ts +195 -0
  227. package/src/client/canvas/graph-model.ts +254 -0
  228. package/src/client/canvas/memory-layout.ts +106 -0
  229. package/src/client/canvas/use-chrome-height.ts +20 -0
  230. package/src/client/canvas/use-focus-trap.ts +31 -5
  231. package/src/client/canvas/use-memory-graph.ts +58 -0
  232. package/src/client/canvas/use-node-drag.ts +2 -1
  233. package/src/client/canvas/use-pan-zoom.ts +64 -7
  234. package/src/client/icons.tsx +0 -63
  235. package/src/client/mcp-app/style.css +0 -6
  236. package/src/client/nodes/AxStepControls.tsx +74 -50
  237. package/src/client/nodes/BoardNode.tsx +5 -1
  238. package/src/client/nodes/ContextNode.tsx +18 -11
  239. package/src/client/nodes/ExtAppFrame.tsx +6 -3
  240. package/src/client/nodes/FileNode.tsx +46 -42
  241. package/src/client/nodes/HtmlNode.tsx +32 -0
  242. package/src/client/nodes/ImageNode.tsx +1 -1
  243. package/src/client/nodes/InlineFormatBar.tsx +6 -9
  244. package/src/client/nodes/InlineMarkdownEditor.tsx +56 -17
  245. package/src/client/nodes/MarkdownNode.tsx +377 -109
  246. package/src/client/nodes/McpAppNode.tsx +50 -20
  247. package/src/client/nodes/MdFormatBar.tsx +21 -18
  248. package/src/client/nodes/PromptNode.tsx +1 -1
  249. package/src/client/nodes/ResponseNode.tsx +1 -1
  250. package/src/client/nodes/viewer-runtime-state.ts +54 -22
  251. package/src/client/state/attention-bridge.ts +64 -15
  252. package/src/client/state/boards-store.ts +193 -39
  253. package/src/client/state/canvas-store.ts +99 -12
  254. package/src/client/state/context-chip-store.ts +70 -0
  255. package/src/client/state/context-status-store.ts +53 -0
  256. package/src/client/state/human-store.ts +13 -7
  257. package/src/client/state/intent-bridge.ts +51 -30
  258. package/src/client/state/knowledge-store.ts +43 -0
  259. package/src/client/state/near-pin-store.ts +115 -6
  260. package/src/client/state/presence-store.ts +7 -10
  261. package/src/client/state/presentation.ts +91 -0
  262. package/src/client/state/relation-store.ts +81 -0
  263. package/src/client/state/session-store.ts +144 -18
  264. package/src/client/state/sse-bridge.ts +235 -32
  265. package/src/client/state/workspace-settings.ts +11 -0
  266. package/src/client/theme/global.css +2724 -476
  267. package/src/client/types.ts +12 -0
  268. package/src/client/utils/keyboard.ts +10 -0
  269. package/src/client/utils/time-ago.ts +14 -0
  270. package/src/json-render/renderer/index.tsx +31 -0
  271. package/src/mcp/canvas-app.ts +12 -2
  272. package/src/mcp/context-uri-template.ts +7 -4
  273. package/src/mcp/server.ts +63 -41
  274. package/src/mcp/workbench-app.ts +39 -6
  275. package/src/server/agent-presence.ts +128 -28
  276. package/src/server/ax-context.ts +27 -13
  277. package/src/server/board-export.ts +12 -1
  278. package/src/server/canvas-db.ts +331 -42
  279. package/src/server/canvas-operations.ts +195 -83
  280. package/src/server/canvas-provenance.ts +53 -22
  281. package/src/server/canvas-serialization.ts +2 -0
  282. package/src/server/canvas-state.ts +950 -122
  283. package/src/server/canvas-validation.ts +9 -2
  284. package/src/server/context-brief.ts +345 -38
  285. package/src/server/context-reads.ts +178 -30
  286. package/src/server/demo-state.json +355 -24
  287. package/src/server/edit-summary.ts +74 -0
  288. package/src/server/html-surface.ts +8 -1
  289. package/src/server/index.ts +181 -11
  290. package/src/server/knowledge-lifecycle.ts +304 -0
  291. package/src/server/lifecycle-status.ts +232 -0
  292. package/src/server/memory-graph.ts +166 -0
  293. package/src/server/mutation-history.ts +18 -6
  294. package/src/server/operations/composites.ts +32 -11
  295. package/src/server/operations/http.ts +7 -2
  296. package/src/server/operations/index.ts +4 -0
  297. package/src/server/operations/mcp.ts +26 -1
  298. package/src/server/operations/ops/ax-presence.ts +1 -1
  299. package/src/server/operations/ops/ax-read.ts +57 -6
  300. package/src/server/operations/ops/ax-timeline.ts +28 -2
  301. package/src/server/operations/ops/backup.ts +5 -3
  302. package/src/server/operations/ops/batch.ts +35 -4
  303. package/src/server/operations/ops/boards.ts +188 -47
  304. package/src/server/operations/ops/canvas-wire.ts +31 -1
  305. package/src/server/operations/ops/edges.ts +36 -2
  306. package/src/server/operations/ops/groups.ts +6 -1
  307. package/src/server/operations/ops/lifecycle.ts +262 -0
  308. package/src/server/operations/ops/memory-graph.ts +45 -0
  309. package/src/server/operations/ops/nodes.ts +107 -24
  310. package/src/server/operations/ops/query.ts +281 -68
  311. package/src/server/operations/ops/snapshots.ts +73 -1
  312. package/src/server/operations/ops/validate.ts +5 -1
  313. package/src/server/operations/ops/viewport.ts +97 -3
  314. package/src/server/operations/registry.ts +74 -25
  315. package/src/server/placement.ts +29 -0
  316. package/src/server/scope-fence.ts +78 -6
  317. package/src/server/server.ts +115 -16
  318. package/src/server/spatial-analysis.ts +48 -23
  319. package/src/server/tour-control.ts +82 -0
  320. package/src/server/wiki-links.ts +137 -0
  321. package/src/server/workbench-auth.ts +11 -0
  322. package/src/shared/agent-context.ts +3 -0
  323. package/src/shared/agent-presence.ts +15 -15
  324. package/src/shared/auto-arrange.ts +2 -0
  325. package/src/shared/boards.ts +4 -4
  326. package/src/shared/knowledge-lifecycle.ts +22 -0
  327. package/src/shared/knowledge-presets.ts +80 -0
  328. package/src/shared/lifecycle-status.ts +26 -0
  329. package/src/shared/memory-graph.ts +74 -0
  330. package/src/shared/relations.ts +98 -0
  331. package/src/shared/semantic-attention.ts +15 -1
  332. package/src/shared/tour.ts +23 -5
  333. package/src/shared/workspace-settings.ts +13 -0
  334. package/dist/types/client/canvas/ContextPinBar.d.ts +0 -1
  335. package/dist/types/client/canvas/FocusFieldLayer.d.ts +0 -1
  336. package/dist/types/client/canvas/kind-colors.d.ts +0 -3
  337. package/dist/types/server/agent-context.d.ts +0 -22
  338. package/src/client/canvas/ContextPinBar.tsx +0 -25
@@ -1,5 +1,8 @@
1
1
  import { findOpenCanvasPosition } from '../utils/placement.js';
2
- import { ownsCamera } from './presentation';
2
+ import { refreshKnowledge } from './knowledge-store';
3
+ import { applyWorkspaceSettings } from './workspace-settings';
4
+ import { cameraSchema, tourStopSchema } from '../../shared/tour.js';
5
+ import { ownsCamera, playStop, presentationError, presenting, presentStop, recordingCamera } from './presentation';
3
6
  import { isHostedWorkbench, workbenchFetch } from './workbench-transport';
4
7
  import { normalizeExtAppToolResult } from '../utils/ext-app-tool-result.js';
5
8
  import type { CanvasAnnotation, CanvasEdge, CanvasNodeState, NodeActor } from '../types';
@@ -11,8 +14,12 @@ import {
11
14
  axSurfaceState,
12
15
  bringToFront,
13
16
  cancelViewportAnimation,
17
+ commitViewport,
14
18
  canvasTheme,
15
19
  connectionStatus,
20
+ contextPinsRevision,
21
+ contextPinsPendingWrites,
22
+ refreshContextPins,
16
23
  replaceContextPinsFromServer,
17
24
  edges,
18
25
  focusNode,
@@ -30,8 +37,18 @@ import {
30
37
  reconnectDelay,
31
38
  resetCanvasInteractionState,
32
39
  } from './canvas-store';
33
- import { applyBoards, loadBoards, setBoardSwitchHandler } from './boards-store';
40
+ import {
41
+ activeBoardId,
42
+ applyBoards,
43
+ boardGeneration,
44
+ catalogEpoch,
45
+ connectBoardCatalog,
46
+ loadBoards,
47
+ memoryGraphRevision,
48
+ setBoardSwitchHandler,
49
+ } from './boards-store';
34
50
  import { applyContextStatus, refreshContextStatus } from './context-status-store';
51
+ import { refreshContextChip } from './context-chip-store';
35
52
  import {
36
53
  fetchAgentPresence,
37
54
  fetchAxSurfaceState,
@@ -41,9 +58,9 @@ import {
41
58
  tabAgentKey,
42
59
  } from './intent-bridge';
43
60
  import { agentActivity, applyPresenceSnapshot, sessionActive } from './presence-store';
44
- import { applyHumanSnapshot, startHumanPresence } from './human-store';
61
+ import { applyHumanSnapshot, humanClientId, startHumanPresence, stopHumanPresence } from './human-store';
45
62
  import type { HumanPresenceSnapshot } from '../../shared/human-presence.js';
46
- import { applySessionReceipt, dismissSessionReceipt, refreshTimeline } from './session-store';
63
+ import { activityLens, applySessionReceipt, dismissSessionReceipt, refreshTimeline } from './session-store';
47
64
  import { initSessionThemeOverride, themeOverrideActive } from './theme-override';
48
65
  import { DEFAULT_POSITIONS, makeNodeState } from './node-factory';
49
66
  import { invalidateTokenCache } from '../theme/tokens';
@@ -64,14 +81,19 @@ let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
64
81
  // "opens" (headers pass through) but no event ever arrives. Fallback: if the
65
82
  // stream produces no event at all within the watchdog window, switch to
66
83
  // polling GET /api/workbench/poll, which returns buffered events (or a full
67
- // connect snapshot) as a normal response every couple of seconds. The same
84
+ // connect snapshot) as a completed response as soon as an event arrives. The same
68
85
  // EVENT_HANDLERS map serves both transports. `?transport=poll` forces polling
69
86
  // from the start; `?transport=sse` disables the fallback.
70
87
  const SSE_FIRST_EVENT_WATCHDOG_MS = 3000;
71
88
  const POLL_INTERVAL_MS = 2000;
89
+ const POLL_WAIT_MS = 1000;
90
+ const POLL_LIVE_INTERVAL_MS = 80;
72
91
  const POLL_ERROR_INTERVAL_MS = 5000;
92
+ // Bound headers AND body, including host transports that ignore AbortSignal.
93
+ const POLL_REQUEST_TIMEOUT_MS = 10000;
73
94
  let pollingMode = false;
74
95
  let pollTimer: ReturnType<typeof setTimeout> | null = null;
96
+ let pollAbort: AbortController | null = null;
75
97
  let pollSeq: number | null = null;
76
98
  let pollGeneration = 0;
77
99
  let sseFirstEventWatchdog: ReturnType<typeof setTimeout> | null = null;
@@ -369,6 +391,7 @@ function parseCanvasNode(raw: Record<string, unknown>): CanvasNodeState | null {
369
391
  collapsed: raw.collapsed === true,
370
392
  pinned: raw.pinned === true,
371
393
  contentRevision: typeof raw.contentRevision === 'number' ? raw.contentRevision : 0,
394
+ linksRevision: typeof raw.linksRevision === 'number' ? raw.linksRevision : 0,
372
395
  ...(isActor(raw.createdBy) ? { createdBy: raw.createdBy } : {}),
373
396
  ...(isActor(raw.lastEditedBy) ? { lastEditedBy: raw.lastEditedBy } : {}),
374
397
  data,
@@ -391,10 +414,18 @@ function parseCanvasEdge(raw: Record<string, unknown>): CanvasEdge | null {
391
414
  id: raw.id,
392
415
  from: raw.from,
393
416
  to: raw.to,
417
+ ...(typeof raw.toBoardId === 'string' ? { toBoardId: raw.toBoardId } : {}),
394
418
  type: raw.type,
419
+ ...(typeof raw.kind === 'string' ? { kind: raw.kind } : {}),
420
+ ...(typeof raw.reason === 'string' ? { reason: raw.reason } : {}),
395
421
  ...(typeof raw.label === 'string' ? { label: raw.label } : {}),
396
422
  ...(raw.style === 'solid' || raw.style === 'dashed' || raw.style === 'dotted' ? { style: raw.style } : {}),
397
423
  ...(raw.animated === true ? { animated: true } : {}),
424
+ ...(typeof raw.revision === 'number' ? { revision: raw.revision } : {}),
425
+ ...(isActor(raw.changedBy) ? { changedBy: raw.changedBy } : {}),
426
+ ...(typeof raw.changedAt === 'string' ? { changedAt: raw.changedAt } : {}),
427
+ ...(isActor(raw.createdBy) ? { createdBy: raw.createdBy } : {}),
428
+ ...(typeof raw.createdAt === 'string' ? { createdAt: raw.createdAt } : {}),
398
429
  };
399
430
  }
400
431
 
@@ -471,10 +502,9 @@ function handleAgentSessionEnded(data: Record<string, unknown>): void {
471
502
  applySessionReceipt(data);
472
503
  }
473
504
 
474
- let stopHumanPresence: (() => void) | null = null;
475
505
  let boardResyncGeneration = 0;
476
506
  let layoutRevision = 0;
477
- let pinsRevision = 0;
507
+ let pendingBoardLayout: Record<string, unknown> | null = null;
478
508
 
479
509
  function handleHumanPresence(data: Record<string, unknown>): void {
480
510
  applyHumanSnapshot(data as Partial<HumanPresenceSnapshot>);
@@ -485,32 +515,61 @@ function handleHumanPresence(data: Record<string, unknown>): void {
485
515
  * a different board now, so take its layout like a fresh connection would —
486
516
  * camera included — and drop what belonged to the old board.
487
517
  */
488
- function resyncForBoardSwitch(): void {
518
+ function resyncForBoardSwitch(initial: boolean): void {
519
+ const pending = pendingBoardLayout;
520
+ pendingBoardLayout = null;
521
+ if (initial) {
522
+ // Untagged initial fetches may already have supplied the layout. Tagged
523
+ // SSE waits here for the first catalog to identify its owner.
524
+ if (pending?.boardId === activeBoardId.value) handleCanvasLayoutUpdate(pending);
525
+ return;
526
+ }
527
+ const boardId = activeBoardId.value;
489
528
  const generation = ++boardResyncGeneration;
490
529
  savedLayout = null;
491
530
  hasInitialServerLayout.value = false;
492
531
  resetIntents();
493
532
  resetCanvasInteractionState();
494
533
  resetAttentionBridge();
495
- dismissSessionReceipt();
534
+ applyServerCanvasLayout({ nodes: [], edges: [] });
535
+ if (pending?.boardId === activeBoardId.value) handleCanvasLayoutUpdate(pending);
536
+ // Keep the change available when visiting another board, so its Open board
537
+ // action can bring the person back to Undo. Home still clears board chrome.
538
+ if (activeBoardId.value === null) dismissSessionReceipt();
539
+ if (activityLens.value?.kind === 'receipt') activityLens.value = null;
496
540
  applyContextStatus({});
497
541
  axSurfaceState.value = null;
498
542
  void refreshAxSurface();
499
543
  const layoutAtRequest = layoutRevision;
500
- const pinsAtRequest = pinsRevision;
501
- void fetchCanvasState().then((layout) => {
544
+ const pinsAtRequest = contextPinsRevision;
545
+ void fetchCanvasState(boardId).then((layout) => {
502
546
  if (generation === boardResyncGeneration && layoutAtRequest === layoutRevision)
503
- handleCanvasLayoutUpdate({ layout });
547
+ handleCanvasLayoutUpdate({ layout, boardId });
504
548
  });
505
549
  void requestJson<{ nodeIds?: unknown }>('fetchPinnedContext', '/api/canvas/pinned-context', {}).then((data) => {
506
- if (generation === boardResyncGeneration && pinsAtRequest === pinsRevision)
550
+ if (generation === boardResyncGeneration && pinsAtRequest === contextPinsRevision)
507
551
  handleContextPinsChanged({ nodeIds: data.nodeIds });
508
552
  });
509
553
  }
510
554
  setBoardSwitchHandler(resyncForBoardSwitch);
511
555
 
512
556
  function handleConnected(data: Record<string, unknown>): void {
557
+ memoryGraphRevision.value++;
558
+ // A current connection authorizes catalogs from this server run. Old HTTP
559
+ // continuations and buffered layouts cannot cross the snapshot boundary.
560
+ boardResyncGeneration++;
561
+ pendingBoardLayout = null;
562
+ // The following snapshot is a fresh baseline, including lower revisions after
563
+ // a restart/restore. Keep same-board editors and their text selections mounted.
564
+ hasInitialServerLayout.value = false;
565
+ if (typeof data.catalogEpoch === 'string') connectBoardCatalog(data.catalogEpoch);
566
+ // A same-bundle daemon restart keeps drafts alive but rotates its human capability.
567
+ // Install it before connect-time requests or presence heartbeats can write.
568
+ if (!isHostedWorkbench() && typeof data.workbenchToken === 'string') {
569
+ (window as Window & { __PMX_WORKBENCH_TOKEN?: string }).__PMX_WORKBENCH_TOKEN = data.workbenchToken;
570
+ }
513
571
  sessionId.value = (data.sessionId as string) || '';
572
+ applyWorkspaceSettings(data.workspaceSettings);
514
573
  connectionStatus.value = 'connected';
515
574
  // Hosted writes capture the active board. Load its identity before reporting
516
575
  // the viewport, rather than sending a spurious Home precondition on boot.
@@ -522,11 +581,12 @@ function handleConnected(data: Record<string, unknown>): void {
522
581
  // keep it live from here on.
523
582
  void fetchAgentPresence().then(applyPresenceSnapshot);
524
583
  void refreshContextStatus();
584
+ refreshContextChip();
525
585
  // Human collaborators (phase 8): announce this tab and read who else is here.
526
586
  // An `?agent=<key>` tab is an AGENT's view — it must not heartbeat a human
527
587
  // guest cursor (its writes already book as the agent).
528
- stopHumanPresence?.();
529
- if (!tabAgentKey && !isHostedWorkbench()) stopHumanPresence = startHumanPresence();
588
+ stopHumanPresence();
589
+ if (!tabAgentKey && !isHostedWorkbench()) startHumanPresence();
530
590
  void requestJson<Partial<HumanPresenceSnapshot> | null>(
531
591
  'fetchHumanPresence',
532
592
  '/api/canvas/human-presence',
@@ -942,6 +1002,9 @@ function handleCanvasResponseComplete(data: Record<string, unknown>): void {
942
1002
  // ── Agent canvas tool events ──────────────────────────────────
943
1003
 
944
1004
  function handleCanvasLayoutUpdate(data: Record<string, unknown>): void {
1005
+ // Even an inactive board can affect library links. This invalidates a read;
1006
+ // the ownership guards below still control applying the canvas snapshot.
1007
+ memoryGraphRevision.value++;
945
1008
  const layout = data.layout as
946
1009
  | {
947
1010
  nodes?: Array<Record<string, unknown>>;
@@ -951,6 +1014,22 @@ function handleCanvasLayoutUpdate(data: Record<string, unknown>): void {
951
1014
  }
952
1015
  | undefined;
953
1016
  if (!layout?.nodes) return;
1017
+ // Board IDs repeat on A → B → A and after restores. Only the accepted
1018
+ // activation may establish its camera/content baseline.
1019
+ if (typeof data.boardGeneration === 'number') {
1020
+ if (data.catalogEpoch !== catalogEpoch || (boardGeneration !== null && data.boardGeneration < boardGeneration))
1021
+ return;
1022
+ if (boardGeneration === null || data.boardGeneration > boardGeneration) {
1023
+ pendingBoardLayout = data;
1024
+ return;
1025
+ }
1026
+ }
1027
+ // The server sends a switched board's layout before boards-changed. Do not
1028
+ // mount it under the previous owner, even when card IDs happen to match.
1029
+ if (data.boardId !== undefined && data.boardId !== activeBoardId.value) {
1030
+ pendingBoardLayout = data;
1031
+ return;
1032
+ }
954
1033
  layoutRevision += 1;
955
1034
  const shouldApplyViewport = !hasInitialServerLayout.value;
956
1035
  hasInitialServerLayout.value = true;
@@ -986,6 +1065,8 @@ function handleCanvasLayoutUpdate(data: Record<string, unknown>): void {
986
1065
  );
987
1066
 
988
1067
  syncAttentionFromSse({ event: 'canvas-layout-update', data });
1068
+ // Content changes move the brief's size and can clip it (debounced, so a burst fetches once).
1069
+ refreshContextChip(1_000);
989
1070
  }
990
1071
 
991
1072
  function reconnectDelayMs(attempt: number): number {
@@ -1007,7 +1088,8 @@ function handleCanvasFocusNode(data: Record<string, unknown>): void {
1007
1088
  }
1008
1089
 
1009
1090
  function handleCanvasViewportUpdate(data: Record<string, unknown>): void {
1010
- if (ownsCamera()) return;
1091
+ // A delayed echo of this tab's earlier commit must not cancel its newer gesture.
1092
+ if (ownsCamera() || data.clientId === humanClientId) return;
1011
1093
  const viewport = data.viewport as Record<string, unknown> | undefined;
1012
1094
  if (!viewport) return;
1013
1095
  const x = typeof viewport.x === 'number' ? viewport.x : 0;
@@ -1017,6 +1099,40 @@ function handleCanvasViewportUpdate(data: Record<string, unknown>): void {
1017
1099
  replaceViewport({ x, y, scale });
1018
1100
  }
1019
1101
 
1102
+ // Tour steps and camera moves animate locally; a deterministic capture owns
1103
+ // its camera outright and ignores both.
1104
+ function handleCanvasTourStep(data: Record<string, unknown>): void {
1105
+ if (recordingCamera.value) return;
1106
+ // A human stepping in one tab moves tabs already presenting; only a
1107
+ // `present` step (the agent's default) starts presenting elsewhere.
1108
+ if (!presenting.value && data.present !== true) return;
1109
+ const stop = tourStopSchema.safeParse(data.stop);
1110
+ if (stop.success) presentStop(stop.data);
1111
+ }
1112
+
1113
+ function handleCanvasTourExit(): void {
1114
+ if (recordingCamera.value) return;
1115
+ presenting.value = false;
1116
+ presentationError.value = '';
1117
+ }
1118
+
1119
+ function handleCanvasCameraMove(data: Record<string, unknown>): void {
1120
+ if (recordingCamera.value) return;
1121
+ const stop = tourStopSchema.safeParse(data.stop);
1122
+ if (!stop.success) return;
1123
+ try {
1124
+ // The presentation camera is local; otherwise the end camera becomes the
1125
+ // board viewport — unrecorded, so a camera move is never an undo step.
1126
+ playStop(stop.data, presenting.value ? undefined : (end) => commitViewport(end, { recordHistory: false }));
1127
+ } catch {
1128
+ // Target not in this tab's layout yet: take the server's resolved camera.
1129
+ const fallback = cameraSchema.safeParse(data.viewport);
1130
+ if (!fallback.success) return;
1131
+ cancelViewportAnimation();
1132
+ replaceViewport(fallback.data);
1133
+ }
1134
+ }
1135
+
1020
1136
  function handleContextUsage(data: Record<string, unknown>): void {
1021
1137
  const id = 'context-main';
1022
1138
  const existing = nodes.value.get(id);
@@ -1042,15 +1158,17 @@ function handleThemeChanged(data: Record<string, unknown>): void {
1042
1158
  }
1043
1159
 
1044
1160
  function handleContextPinsChanged(data: Record<string, unknown>): void {
1045
- pinsRevision += 1;
1161
+ memoryGraphRevision.value++;
1046
1162
  const nodeIds = Array.isArray(data.nodeIds) ? data.nodeIds.filter((id): id is string => typeof id === 'string') : [];
1047
- replaceContextPinsFromServer(nodeIds);
1163
+ if (contextPinsPendingWrites === 0) replaceContextPinsFromServer(nodeIds);
1048
1164
  syncAttentionFromSse({ event: 'context-pins-changed', data });
1049
1165
  void refreshContextStatus();
1166
+ refreshContextChip();
1050
1167
  }
1051
1168
 
1052
1169
  function handleContextStatusChanged(): void {
1053
1170
  void refreshContextStatus();
1171
+ refreshContextChip();
1054
1172
  }
1055
1173
 
1056
1174
  // AX state changes arrive as per-primitive deltas; rather than reduce them, treat
@@ -1128,13 +1246,22 @@ export const EVENT_HANDLERS: Record<string, (data: Record<string, unknown>) => v
1128
1246
  'ext-app-result': handleExtAppResult,
1129
1247
  'context-pins-changed': handleContextPinsChanged,
1130
1248
  'context-status-changed': handleContextStatusChanged,
1131
- 'boards-changed': applyBoards,
1249
+ 'knowledge-changed': () => void refreshKnowledge(),
1250
+ 'lifecycle-changed': () => void refreshKnowledge(),
1251
+ 'boards-changed': (data) => {
1252
+ applyBoards(data);
1253
+ refreshContextChip();
1254
+ },
1132
1255
  'canvas-layout-update': handleCanvasLayoutUpdate,
1133
1256
  'canvas-focus-node': handleCanvasFocusNode,
1134
1257
  'canvas-viewport-update': handleCanvasViewportUpdate,
1258
+ 'canvas-tour-step': handleCanvasTourStep,
1259
+ 'canvas-tour-exit': handleCanvasTourExit,
1260
+ 'canvas-camera-move': handleCanvasCameraMove,
1135
1261
  'context-usage': handleContextUsage,
1136
1262
  'trace-state': handleTraceState,
1137
1263
  'theme-changed': handleThemeChanged,
1264
+ 'settings-changed': (data) => applyWorkspaceSettings(data.workspaceSettings),
1138
1265
  'canvas-prompt-created': handleCanvasPromptCreated,
1139
1266
  'canvas-prompt-status': handleCanvasPromptStatus,
1140
1267
  'canvas-response-start': handleCanvasResponseStart,
@@ -1174,6 +1301,12 @@ function startPollingTransport(): () => void {
1174
1301
  pollingMode = true;
1175
1302
  pollGeneration += 1;
1176
1303
  const generation = pollGeneration;
1304
+ pollAbort?.abort();
1305
+ const controller = new AbortController();
1306
+ pollAbort = controller;
1307
+ // MCP-app polls are host tool calls, not direct HTTP requests. Keep their
1308
+ // existing cadence; HTTP clients can park one request until an event arrives.
1309
+ const longPolling = !isHostedWorkbench();
1177
1310
  connectionStatus.value = 'connecting';
1178
1311
  // Server-run identity from the last poll response. The event seq counter
1179
1312
  // restarts at 0 on every server boot, so a cursor from a previous run can
@@ -1183,22 +1316,39 @@ function startPollingTransport(): () => void {
1183
1316
 
1184
1317
  const pollOnce = async (): Promise<void> => {
1185
1318
  if (generation !== pollGeneration) return;
1319
+ const requestController = new AbortController();
1320
+ let deadline: ReturnType<typeof setTimeout> | undefined;
1186
1321
  try {
1187
1322
  const params = new URLSearchParams();
1188
- if (pollSeq !== null) params.set('since', String(pollSeq));
1323
+ if (pollSeq !== null) {
1324
+ params.set('since', String(pollSeq));
1325
+ if (longPolling) params.set('waitMs', String(POLL_WAIT_MS));
1326
+ }
1189
1327
  const sid = sessionId.value;
1190
1328
  if (sid) params.set('session', sid);
1191
1329
  const query = params.toString();
1192
- const response = await workbenchFetch(`/api/workbench/poll${query ? `?${query}` : ''}`);
1193
- if (generation !== pollGeneration) return;
1194
- if (!response.ok) throw new Error(`poll failed with HTTP ${response.status}`);
1195
- const body = (await response.json()) as {
1196
- ok: boolean;
1197
- seq: number;
1198
- snapshot: boolean;
1199
- sessionId?: string;
1200
- events: Array<{ event: string; payload: unknown }>;
1201
- };
1330
+ const pinsAtRequest = contextPinsRevision;
1331
+ const body = await Promise.race([
1332
+ (async () => {
1333
+ const response = await workbenchFetch(`/api/workbench/poll${query ? `?${query}` : ''}`, {
1334
+ signal: AbortSignal.any([controller.signal, requestController.signal]),
1335
+ });
1336
+ if (!response.ok) throw new Error(`poll failed with HTTP ${response.status}`);
1337
+ return (await response.json()) as {
1338
+ ok: boolean;
1339
+ seq: number;
1340
+ snapshot: boolean;
1341
+ sessionId?: string;
1342
+ events: Array<{ event: string; payload: unknown }>;
1343
+ };
1344
+ })(),
1345
+ new Promise<never>((_, reject) => {
1346
+ deadline = setTimeout(() => {
1347
+ requestController.abort();
1348
+ reject(new Error('poll request timed out'));
1349
+ }, POLL_REQUEST_TIMEOUT_MS);
1350
+ }),
1351
+ ]);
1202
1352
  if (generation !== pollGeneration) return;
1203
1353
  if (!body.ok) throw new Error('poll returned ok: false');
1204
1354
  const priorSession = lastPollSessionId;
@@ -1222,13 +1372,55 @@ function startPollingTransport(): () => void {
1222
1372
  resetIntents();
1223
1373
  responseToThreadMap.clear();
1224
1374
  }
1225
- for (const entry of body.events) dispatchWorkbenchEvent(entry.event, entry.payload);
1375
+ // A poll may have captured pins before a newer human click or write acknowledgement.
1376
+ // Skip only those pin frames, then read the canonical set so peer changes and server
1377
+ // normalization still arrive even though the event cursor advances past the old batch.
1378
+ const acceptPins = pinsAtRequest === contextPinsRevision && contextPinsPendingWrites === 0;
1379
+ let skippedPins = false;
1380
+ // Full layouts in a buffered poll can predate an HTTP refresh. Replaying
1381
+ // an obsolete empty layout would detach a live editor before its card
1382
+ // reappears. Keep the last snapshot across bookkeeping frames only:
1383
+ // commands (focus, tours, deltas, etc.) still need their preceding layout.
1384
+ const supersededLayouts = new Set<(typeof body.events)[number]>();
1385
+ if (!body.snapshot && hasInitialServerLayout.value) {
1386
+ let previousLayout: (typeof body.events)[number] | undefined;
1387
+ for (const entry of body.events) {
1388
+ const data = entry.payload as Record<string, unknown>;
1389
+ const sameActivation =
1390
+ data.catalogEpoch === catalogEpoch &&
1391
+ data.boardGeneration === boardGeneration &&
1392
+ (entry.event === 'boards-changed' ? data.activeBoardId : data.boardId) === activeBoardId.value;
1393
+ if (entry.event === 'canvas-layout-update' && sameActivation) {
1394
+ if (previousLayout) supersededLayouts.add(previousLayout);
1395
+ previousLayout = entry;
1396
+ } else if (
1397
+ !(entry.event === 'boards-changed' && sameActivation) &&
1398
+ !['agent-presence', 'human-presence', 'ax-intent', 'ax-intent-clear'].includes(entry.event)
1399
+ ) {
1400
+ previousLayout = undefined;
1401
+ }
1402
+ }
1403
+ }
1404
+ for (const entry of body.events) {
1405
+ if (supersededLayouts.has(entry)) continue;
1406
+ if (entry.event === 'context-pins-changed' && !acceptPins) {
1407
+ skippedPins = true;
1408
+ continue;
1409
+ }
1410
+ dispatchWorkbenchEvent(entry.event, entry.payload);
1411
+ }
1412
+ if (skippedPins) {
1413
+ void refreshContextPins();
1414
+ void refreshContextStatus();
1415
+ refreshContextChip();
1416
+ }
1226
1417
  dispatchedOnce = true;
1227
1418
  pollSeq = body.seq;
1228
1419
  reconnectAttempt.value = 0;
1229
1420
  reconnectDelay.value = 0;
1230
1421
  connectionStatus.value = 'connected';
1231
- pollTimer = setTimeout(() => void pollOnce(), POLL_INTERVAL_MS);
1422
+ // Bound busy-stream request rate without imposing a two-second cursor gap.
1423
+ pollTimer = setTimeout(() => void pollOnce(), longPolling ? POLL_LIVE_INTERVAL_MS : POLL_INTERVAL_MS);
1232
1424
  } catch (err) {
1233
1425
  if (generation !== pollGeneration) return;
1234
1426
  console.warn('[sse-bridge] poll transport error:', err);
@@ -1236,14 +1428,19 @@ function startPollingTransport(): () => void {
1236
1428
  reconnectAttempt.value += 1;
1237
1429
  reconnectDelay.value = POLL_ERROR_INTERVAL_MS;
1238
1430
  pollTimer = setTimeout(() => void pollOnce(), POLL_ERROR_INTERVAL_MS);
1431
+ } finally {
1432
+ clearTimeout(deadline);
1239
1433
  }
1240
1434
  };
1241
1435
  void pollOnce();
1242
1436
 
1243
1437
  return () => {
1438
+ stopHumanPresence();
1244
1439
  pollGeneration += 1;
1245
1440
  pollingMode = false;
1246
1441
  pollSeq = null;
1442
+ controller.abort();
1443
+ if (pollAbort === controller) pollAbort = null;
1247
1444
  if (pollTimer) {
1248
1445
  clearTimeout(pollTimer);
1249
1446
  pollTimer = null;
@@ -1252,6 +1449,8 @@ function startPollingTransport(): () => void {
1252
1449
  }
1253
1450
 
1254
1451
  export function connectSSE(): () => void {
1452
+ boardResyncGeneration++;
1453
+ pendingBoardLayout = null;
1255
1454
  // Host-default theming: a ?theme= param applies before the first server
1256
1455
  // frame so an embedding host panel never flashes the server-global theme.
1257
1456
  initSessionThemeOverride(applyCanvasTheme);
@@ -1302,6 +1501,7 @@ export function connectSSE(): () => void {
1302
1501
 
1303
1502
  for (const [event] of Object.entries(EVENT_HANDLERS)) {
1304
1503
  source.addEventListener(event, (e) => {
1504
+ if (eventSource !== source) return;
1305
1505
  gotFirstEvent = true;
1306
1506
  try {
1307
1507
  dispatchWorkbenchEvent(event, JSON.parse((e as MessageEvent).data));
@@ -1336,6 +1536,7 @@ export function connectSSE(): () => void {
1336
1536
  };
1337
1537
 
1338
1538
  return () => {
1539
+ stopHumanPresence();
1339
1540
  if (reconnectTimer) {
1340
1541
  clearTimeout(reconnectTimer);
1341
1542
  reconnectTimer = null;
@@ -1348,6 +1549,8 @@ export function connectSSE(): () => void {
1348
1549
  pollGeneration += 1;
1349
1550
  pollingMode = false;
1350
1551
  pollSeq = null;
1552
+ pollAbort?.abort();
1553
+ pollAbort = null;
1351
1554
  if (pollTimer) {
1352
1555
  clearTimeout(pollTimer);
1353
1556
  pollTimer = null;
@@ -0,0 +1,11 @@
1
+ import { computed, signal } from '@preact/signals';
2
+ import { workspaceSettingsSchema, type WorkspaceSettings } from '../../shared/workspace-settings.js';
3
+
4
+ export const workspaceSettings = signal<WorkspaceSettings | null>(null);
5
+ export const agentSessionsEnabled = computed(() => workspaceSettings.value?.agentSessions ?? false);
6
+ export const knowledgeLifecycleEnabled = computed(() => workspaceSettings.value?.knowledgeLifecycle ?? false);
7
+
8
+ export function applyWorkspaceSettings(value: unknown): void {
9
+ const parsed = workspaceSettingsSchema.safeParse(value);
10
+ if (parsed.success) workspaceSettings.value = parsed.data;
11
+ }