pmx-canvas 0.8.0 → 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 (341) 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 +66 -0
  4. package/Readme.md +131 -532
  5. package/dist/canvas/global.css +2759 -510
  6. package/dist/canvas/index.js +168 -107
  7. package/dist/canvas/mcp-app.html +2936 -632
  8. package/dist/json-render/index.css +81 -1
  9. package/dist/json-render/index.js +20 -20
  10. package/dist/types/client/canvas/BarHint.d.ts +12 -2
  11. package/dist/types/client/canvas/BoardLinkDialog.d.ts +3 -1
  12. package/dist/types/client/canvas/BoardPinButton.d.ts +10 -0
  13. package/dist/types/client/canvas/BoardSwitcher.d.ts +1 -1
  14. package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
  15. package/dist/types/client/canvas/CommandBar.d.ts +3 -4
  16. package/dist/types/client/canvas/ConnectionBanner.d.ts +2 -1
  17. package/dist/types/client/canvas/ContextChip.d.ts +3 -0
  18. package/dist/types/client/canvas/CrossBoardReferences.d.ts +5 -0
  19. package/dist/types/client/canvas/EdgeLayer.d.ts +14 -3
  20. package/dist/types/client/canvas/EmptyState.d.ts +4 -4
  21. package/dist/types/client/canvas/HomeGraphView.d.ts +2 -0
  22. package/dist/types/client/canvas/HomeMapView.d.ts +1 -0
  23. package/dist/types/client/canvas/HomeReviewView.d.ts +6 -0
  24. package/dist/types/client/canvas/HomeView.d.ts +9 -7
  25. package/dist/types/client/canvas/KnowledgeDialog.d.ts +4 -0
  26. package/dist/types/client/canvas/LifecycleChrome.d.ts +5 -0
  27. package/dist/types/client/canvas/LifecycleFields.d.ts +7 -0
  28. package/dist/types/client/canvas/LifecyclePanel.d.ts +1 -0
  29. package/dist/types/client/canvas/MemoryInspector.d.ts +7 -0
  30. package/dist/types/client/canvas/NearPinLayer.d.ts +14 -0
  31. package/dist/types/client/canvas/NodeContextMark.d.ts +40 -7
  32. package/dist/types/client/canvas/RelationPresetsDialog.d.ts +4 -0
  33. package/dist/types/client/canvas/RelationsPanel.d.ts +4 -0
  34. package/dist/types/client/canvas/SessionPanel.d.ts +2 -0
  35. package/dist/types/client/canvas/canvas-area.d.ts +1 -1
  36. package/dist/types/client/canvas/graph-geometry.d.ts +27 -0
  37. package/dist/types/client/canvas/graph-model.d.ts +37 -0
  38. package/dist/types/client/canvas/memory-layout.d.ts +23 -0
  39. package/dist/types/client/canvas/use-chrome-height.d.ts +2 -0
  40. package/dist/types/client/canvas/use-focus-trap.d.ts +1 -0
  41. package/dist/types/client/canvas/use-memory-graph.d.ts +8 -0
  42. package/dist/types/client/canvas/use-pan-zoom.d.ts +4 -1
  43. package/dist/types/client/icons.d.ts +0 -10
  44. package/dist/types/client/nodes/AxStepControls.d.ts +1 -1
  45. package/dist/types/client/nodes/HtmlNode.d.ts +1 -0
  46. package/dist/types/client/nodes/InlineMarkdownEditor.d.ts +4 -4
  47. package/dist/types/client/nodes/MarkdownNode.d.ts +5 -1
  48. package/dist/types/client/nodes/McpAppNode.d.ts +4 -4
  49. package/dist/types/client/nodes/viewer-runtime-state.d.ts +1 -1
  50. package/dist/types/client/state/boards-store.d.ts +81 -9
  51. package/dist/types/client/state/canvas-store.d.ts +14 -1
  52. package/dist/types/client/state/context-chip-store.d.ts +22 -0
  53. package/dist/types/client/state/context-status-store.d.ts +35 -0
  54. package/dist/types/client/state/intent-bridge.d.ts +11 -8
  55. package/dist/types/client/state/knowledge-store.d.ts +22 -0
  56. package/dist/types/client/state/near-pin-store.d.ts +44 -1
  57. package/dist/types/client/state/presence-store.d.ts +4 -4
  58. package/dist/types/client/state/presentation.d.ts +28 -0
  59. package/dist/types/client/state/relation-store.d.ts +10 -0
  60. package/dist/types/client/state/session-store.d.ts +41 -2
  61. package/dist/types/client/state/workspace-settings.d.ts +7 -0
  62. package/dist/types/client/types.d.ts +13 -1
  63. package/dist/types/client/utils/keyboard.d.ts +2 -0
  64. package/dist/types/client/utils/time-ago.d.ts +4 -0
  65. package/dist/types/mcp/context-uri-template.d.ts +3 -1
  66. package/dist/types/server/agent-presence.d.ts +10 -3
  67. package/dist/types/server/ax-context.d.ts +1 -1
  68. package/dist/types/server/canvas-db.d.ts +30 -7
  69. package/dist/types/server/canvas-operations.d.ts +24 -2
  70. package/dist/types/server/canvas-provenance.d.ts +22 -2
  71. package/dist/types/server/canvas-serialization.d.ts +2 -0
  72. package/dist/types/server/canvas-state.d.ts +93 -15
  73. package/dist/types/server/canvas-validation.d.ts +1 -1
  74. package/dist/types/server/context-brief.d.ts +94 -6
  75. package/dist/types/server/context-reads.d.ts +33 -7
  76. package/dist/types/server/edit-summary.d.ts +20 -0
  77. package/dist/types/server/index.d.ts +71 -10
  78. package/dist/types/server/knowledge-lifecycle.d.ts +221 -0
  79. package/dist/types/server/lifecycle-status.d.ts +10 -0
  80. package/dist/types/server/memory-graph.d.ts +9 -0
  81. package/dist/types/server/mutation-history.d.ts +2 -0
  82. package/dist/types/server/operations/ops/ax-shared.d.ts +1 -1
  83. package/dist/types/server/operations/ops/boards.d.ts +2 -1
  84. package/dist/types/server/operations/ops/lifecycle.d.ts +2 -0
  85. package/dist/types/server/operations/ops/memory-graph.d.ts +2 -0
  86. package/dist/types/server/operations/ops/nodes.d.ts +5 -0
  87. package/dist/types/server/operations/ops/query.d.ts +23 -0
  88. package/dist/types/server/operations/registry.d.ts +1 -0
  89. package/dist/types/server/placement.d.ts +7 -0
  90. package/dist/types/server/scope-fence.d.ts +2 -1
  91. package/dist/types/server/spatial-analysis.d.ts +6 -3
  92. package/dist/types/server/tour-control.d.ts +32 -0
  93. package/dist/types/server/wiki-links.d.ts +14 -0
  94. package/dist/types/server/workbench-auth.d.ts +2 -0
  95. package/dist/types/shared/agent-context.d.ts +2 -0
  96. package/dist/types/shared/agent-presence.d.ts +15 -12
  97. package/dist/types/shared/auto-arrange.d.ts +1 -0
  98. package/dist/types/shared/boards.d.ts +4 -4
  99. package/dist/types/shared/knowledge-lifecycle.d.ts +41 -0
  100. package/dist/types/shared/knowledge-presets.d.ts +6 -0
  101. package/dist/types/shared/lifecycle-status.d.ts +24 -0
  102. package/dist/types/shared/memory-graph.d.ts +53 -0
  103. package/dist/types/shared/relations.d.ts +68 -0
  104. package/dist/types/shared/semantic-attention.d.ts +2 -0
  105. package/dist/types/shared/tour.d.ts +38 -0
  106. package/dist/types/shared/workspace-settings.d.ts +11 -0
  107. package/docs/RELEASE.md +21 -3
  108. package/docs/adr-001-bun-only-runtime.md +7 -1
  109. package/docs/adr-002-shared-product-separate-runtimes.md +93 -0
  110. package/docs/api-reference.md +109 -0
  111. package/docs/ax-host-adapter-contract.md +1 -1
  112. package/docs/ax-state-contract.md +5 -4
  113. package/docs/cli.md +153 -15
  114. package/docs/demos/pmx-canvas-demo.mp4 +0 -0
  115. package/docs/design/AgentContext.dc.html +3 -3
  116. package/docs/design/BoardPins.dc.html +37 -22
  117. package/docs/design/Categories.dc.html +208 -0
  118. package/docs/design/CategoryEditor.dc.html +33 -0
  119. package/docs/design/Chrome.dc.html +55 -9
  120. package/docs/design/ContextChip.dc.html +224 -0
  121. package/docs/design/ContextChip600.dc.html +136 -0
  122. package/docs/design/EmptyState.dc.html +45 -0
  123. package/docs/design/Flows.dc.html +23 -1
  124. package/docs/design/HeaderMarks.dc.html +183 -0
  125. package/docs/design/Home.dc.html +19 -1
  126. package/docs/design/HomeGraph.dc.html +86 -202
  127. package/docs/design/HomeMap.dc.html +23 -1
  128. package/docs/design/HomeTeam.dc.html +32 -14
  129. package/docs/design/Lifecycle.dc.html +22 -0
  130. package/docs/design/Lifecycle600.dc.html +18 -0
  131. package/docs/design/LifecycleVariants.dc.html +157 -0
  132. package/docs/design/LinkAuthorship.dc.html +139 -0
  133. package/docs/design/LinksChanged.dc.html +203 -0
  134. package/docs/design/LinksOptions.dc.html +137 -0
  135. package/docs/design/LinksOptions600.dc.html +134 -0
  136. package/docs/design/NearPin.dc.html +9 -10
  137. package/docs/design/Pane600.dc.html +4 -3
  138. package/docs/design/Relations.dc.html +3 -2
  139. package/docs/design/Restore.dc.html +152 -0
  140. package/docs/design/SeeChange.dc.html +267 -0
  141. package/docs/design/Uses.dc.html +12 -6
  142. package/docs/design/canvas.json +146 -19
  143. package/docs/design.md +454 -18
  144. package/docs/environment.md +0 -1
  145. package/docs/evals/curation-effect.md +128 -0
  146. package/docs/http-api.md +465 -38
  147. package/docs/mcp.md +150 -19
  148. package/docs/node-types.md +58 -3
  149. package/docs/plans/design-gaps-2026-10-08.md +53 -0
  150. package/docs/plans/plan-009-tech-debt-backlog.md +379 -0
  151. package/docs/plans/plan-016-context-made-visible-wave-1.md +150 -5
  152. package/docs/plans/plan-017-knowledge-categories-and-lifecycle.md +175 -0
  153. package/docs/plans/plan-018-connected-memory-wave-2.md +594 -0
  154. package/docs/plans/plan-019-post-0.9-roadmap.md +198 -0
  155. package/docs/product-context-vision-2026-09.md +17 -1
  156. package/docs/product-vision-2026-09.md +1082 -43
  157. package/docs/product-vision-review-2026-09.md +13 -1
  158. package/docs/screenshots/workbench-live-dark.png +0 -0
  159. package/docs/screenshots/workbench-live-light.png +0 -0
  160. package/docs/sdk.md +62 -2
  161. package/package.json +1 -1
  162. package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/runtime.json +1 -1
  163. package/plugins/pmx-canvas/plugin.json +1 -1
  164. package/plugins/pmx-canvas/runtime.json +1 -1
  165. package/plugins/pmx-canvas/tools.json +601 -22
  166. package/skills/pmx-canvas/SKILL.md +234 -43
  167. package/skills/pmx-canvas/evals/evals.json +62 -0
  168. package/skills/pmx-canvas/references/ax-html-control-surface.md +6 -0
  169. package/skills/pmx-canvas/references/full-reference.md +120 -21
  170. package/skills/pmx-canvas/references/github-copilot-app-adapter.md +2 -2
  171. package/skills/pmx-canvas/references/importing-context.md +132 -0
  172. package/skills/pmx-canvas/references/installing-pmx-canvas.md +6 -6
  173. package/skills/pmx-canvas/references/tours-and-recording.md +16 -1
  174. package/skills/pmx-canvas-testing/SKILL.md +20 -0
  175. package/src/cli/agent.ts +2 -0
  176. package/src/cli/commands/ax.ts +12 -0
  177. package/src/cli/commands/boards.ts +87 -11
  178. package/src/cli/commands/edges.ts +28 -0
  179. package/src/cli/commands/groups.ts +7 -0
  180. package/src/cli/commands/nodes.ts +4 -2
  181. package/src/cli/commands/query.ts +34 -2
  182. package/src/cli/commands/record.ts +199 -48
  183. package/src/cli/index.ts +7 -1
  184. package/src/client/App.tsx +177 -70
  185. package/src/client/canvas/AttentionHistory.tsx +0 -2
  186. package/src/client/canvas/BarHint.tsx +40 -2
  187. package/src/client/canvas/BoardCopyDialog.tsx +1 -1
  188. package/src/client/canvas/BoardFolderDialog.tsx +7 -7
  189. package/src/client/canvas/BoardLinkDialog.tsx +138 -33
  190. package/src/client/canvas/BoardPinButton.tsx +33 -0
  191. package/src/client/canvas/BoardSwitcher.tsx +26 -40
  192. package/src/client/canvas/CanvasNode.tsx +360 -237
  193. package/src/client/canvas/CanvasViewport.tsx +45 -7
  194. package/src/client/canvas/CommandBar.tsx +29 -23
  195. package/src/client/canvas/CommandPalette.tsx +5 -12
  196. package/src/client/canvas/ConnectionBanner.tsx +3 -3
  197. package/src/client/canvas/ContextChip.tsx +296 -0
  198. package/src/client/canvas/ContextMenu.tsx +63 -5
  199. package/src/client/canvas/CrossBoardReferences.tsx +60 -0
  200. package/src/client/canvas/EdgeLayer.tsx +339 -76
  201. package/src/client/canvas/EmptyState.tsx +33 -18
  202. package/src/client/canvas/ExpandedNodeOverlay.tsx +72 -40
  203. package/src/client/canvas/ExternalWriters.tsx +57 -42
  204. package/src/client/canvas/HomeGraphView.tsx +942 -0
  205. package/src/client/canvas/HomeMapView.tsx +641 -0
  206. package/src/client/canvas/HomeReviewView.tsx +167 -0
  207. package/src/client/canvas/HomeView.tsx +687 -160
  208. package/src/client/canvas/IntentLayer.tsx +19 -12
  209. package/src/client/canvas/KnowledgeDialog.tsx +488 -0
  210. package/src/client/canvas/LifecycleChrome.tsx +133 -0
  211. package/src/client/canvas/LifecycleFields.tsx +91 -0
  212. package/src/client/canvas/LifecyclePanel.tsx +533 -0
  213. package/src/client/canvas/MemoryInspector.tsx +104 -0
  214. package/src/client/canvas/Minimap.tsx +3 -3
  215. package/src/client/canvas/NearPinLayer.tsx +187 -0
  216. package/src/client/canvas/NodeContextMark.tsx +175 -97
  217. package/src/client/canvas/Presentation.tsx +23 -57
  218. package/src/client/canvas/RelationPresetsDialog.tsx +413 -0
  219. package/src/client/canvas/RelationsPanel.tsx +82 -0
  220. package/src/client/canvas/SelectionBar.tsx +3 -6
  221. package/src/client/canvas/SessionPanel.tsx +90 -50
  222. package/src/client/canvas/SessionReceipt.tsx +311 -39
  223. package/src/client/canvas/ShortcutOverlay.tsx +1 -1
  224. package/src/client/canvas/SnapshotPanel.tsx +27 -36
  225. package/src/client/canvas/ToolRail.tsx +367 -192
  226. package/src/client/canvas/TopBar.tsx +354 -124
  227. package/src/client/canvas/canvas-area.ts +10 -3
  228. package/src/client/canvas/graph-geometry.ts +195 -0
  229. package/src/client/canvas/graph-model.ts +254 -0
  230. package/src/client/canvas/memory-layout.ts +106 -0
  231. package/src/client/canvas/use-chrome-height.ts +20 -0
  232. package/src/client/canvas/use-focus-trap.ts +31 -5
  233. package/src/client/canvas/use-memory-graph.ts +58 -0
  234. package/src/client/canvas/use-node-drag.ts +2 -1
  235. package/src/client/canvas/use-pan-zoom.ts +64 -7
  236. package/src/client/icons.tsx +0 -63
  237. package/src/client/mcp-app/style.css +0 -6
  238. package/src/client/nodes/AxStepControls.tsx +74 -50
  239. package/src/client/nodes/BoardNode.tsx +5 -1
  240. package/src/client/nodes/ContextNode.tsx +18 -11
  241. package/src/client/nodes/ExtAppFrame.tsx +0 -1
  242. package/src/client/nodes/FileNode.tsx +46 -42
  243. package/src/client/nodes/HtmlNode.tsx +44 -2
  244. package/src/client/nodes/ImageNode.tsx +1 -1
  245. package/src/client/nodes/InlineFormatBar.tsx +6 -9
  246. package/src/client/nodes/InlineMarkdownEditor.tsx +56 -17
  247. package/src/client/nodes/MarkdownNode.tsx +377 -109
  248. package/src/client/nodes/McpAppNode.tsx +50 -20
  249. package/src/client/nodes/MdFormatBar.tsx +21 -18
  250. package/src/client/nodes/PromptNode.tsx +1 -1
  251. package/src/client/nodes/ResponseNode.tsx +1 -1
  252. package/src/client/nodes/viewer-runtime-state.ts +54 -22
  253. package/src/client/state/attention-bridge.ts +64 -15
  254. package/src/client/state/boards-store.ts +193 -39
  255. package/src/client/state/canvas-store.ts +99 -12
  256. package/src/client/state/context-chip-store.ts +70 -0
  257. package/src/client/state/context-status-store.ts +53 -0
  258. package/src/client/state/intent-bridge.ts +51 -30
  259. package/src/client/state/knowledge-store.ts +43 -0
  260. package/src/client/state/near-pin-store.ts +115 -6
  261. package/src/client/state/presence-store.ts +7 -10
  262. package/src/client/state/presentation.ts +91 -0
  263. package/src/client/state/relation-store.ts +81 -0
  264. package/src/client/state/session-store.ts +144 -18
  265. package/src/client/state/sse-bridge.ts +206 -29
  266. package/src/client/state/workspace-settings.ts +11 -0
  267. package/src/client/theme/global.css +2759 -510
  268. package/src/client/types.ts +12 -0
  269. package/src/client/utils/keyboard.ts +10 -0
  270. package/src/client/utils/time-ago.ts +14 -0
  271. package/src/json-render/renderer/index.css +6 -1
  272. package/src/json-render/renderer/index.tsx +31 -1
  273. package/src/mcp/canvas-app.ts +12 -2
  274. package/src/mcp/context-uri-template.ts +7 -4
  275. package/src/mcp/server.ts +63 -41
  276. package/src/mcp/workbench-app.ts +39 -6
  277. package/src/server/agent-presence.ts +128 -28
  278. package/src/server/ax-context.ts +27 -13
  279. package/src/server/board-export.ts +12 -1
  280. package/src/server/canvas-db.ts +331 -42
  281. package/src/server/canvas-operations.ts +196 -84
  282. package/src/server/canvas-provenance.ts +53 -22
  283. package/src/server/canvas-serialization.ts +2 -0
  284. package/src/server/canvas-state.ts +950 -122
  285. package/src/server/canvas-validation.ts +9 -2
  286. package/src/server/context-brief.ts +345 -38
  287. package/src/server/context-reads.ts +179 -30
  288. package/src/server/demo-state.json +355 -24
  289. package/src/server/edit-summary.ts +74 -0
  290. package/src/server/html-surface.ts +8 -1
  291. package/src/server/index.ts +203 -13
  292. package/src/server/knowledge-lifecycle.ts +304 -0
  293. package/src/server/lifecycle-status.ts +232 -0
  294. package/src/server/memory-graph.ts +166 -0
  295. package/src/server/mutation-history.ts +18 -6
  296. package/src/server/operations/composites.ts +32 -11
  297. package/src/server/operations/http.ts +7 -2
  298. package/src/server/operations/index.ts +4 -0
  299. package/src/server/operations/mcp.ts +32 -1
  300. package/src/server/operations/ops/ax-presence.ts +1 -1
  301. package/src/server/operations/ops/ax-read.ts +57 -6
  302. package/src/server/operations/ops/ax-timeline.ts +28 -2
  303. package/src/server/operations/ops/backup.ts +5 -3
  304. package/src/server/operations/ops/batch.ts +35 -4
  305. package/src/server/operations/ops/boards.ts +188 -47
  306. package/src/server/operations/ops/canvas-wire.ts +31 -1
  307. package/src/server/operations/ops/edges.ts +36 -2
  308. package/src/server/operations/ops/groups.ts +6 -1
  309. package/src/server/operations/ops/json-render.ts +5 -1
  310. package/src/server/operations/ops/lifecycle.ts +262 -0
  311. package/src/server/operations/ops/memory-graph.ts +45 -0
  312. package/src/server/operations/ops/nodes.ts +107 -24
  313. package/src/server/operations/ops/query.ts +281 -68
  314. package/src/server/operations/ops/snapshots.ts +73 -1
  315. package/src/server/operations/ops/validate.ts +5 -1
  316. package/src/server/operations/ops/viewport.ts +97 -3
  317. package/src/server/operations/registry.ts +77 -26
  318. package/src/server/placement.ts +29 -0
  319. package/src/server/scope-fence.ts +78 -6
  320. package/src/server/server.ts +115 -16
  321. package/src/server/spatial-analysis.ts +48 -23
  322. package/src/server/tour-control.ts +82 -0
  323. package/src/server/wiki-links.ts +137 -0
  324. package/src/server/workbench-auth.ts +11 -0
  325. package/src/shared/agent-context.ts +3 -0
  326. package/src/shared/agent-presence.ts +15 -15
  327. package/src/shared/auto-arrange.ts +2 -0
  328. package/src/shared/boards.ts +4 -4
  329. package/src/shared/knowledge-lifecycle.ts +22 -0
  330. package/src/shared/knowledge-presets.ts +80 -0
  331. package/src/shared/lifecycle-status.ts +26 -0
  332. package/src/shared/memory-graph.ts +74 -0
  333. package/src/shared/relations.ts +98 -0
  334. package/src/shared/semantic-attention.ts +15 -1
  335. package/src/shared/tour.ts +23 -5
  336. package/src/shared/workspace-settings.ts +13 -0
  337. package/dist/types/client/canvas/ContextPinBar.d.ts +0 -1
  338. package/dist/types/client/canvas/FocusFieldLayer.d.ts +0 -1
  339. package/dist/types/client/canvas/kind-colors.d.ts +0 -3
  340. package/dist/types/server/agent-context.d.ts +0 -22
  341. package/src/client/canvas/ContextPinBar.tsx +0 -25
@@ -1,8 +1,16 @@
1
- import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
2
- import { mutatingNodeIds, sessionActive } from '../state/presence-store';
1
+ import { boardList } from '../state/boards-store';
2
+ import { cardQualification, effectiveLifecycleFilter as lifecycleFilter } from '../state/knowledge-store';
3
+ import { knowledgeLifecycleEnabled } from '../state/workspace-settings';
4
+ import { relationHighlight } from '../state/relation-store';
5
+ import { LifecycleFooter } from './LifecycleChrome';
6
+ import { BarHint } from './BarHint';
7
+ import { CrossBoardReferences } from './CrossBoardReferences';
8
+ import { BoardPinButton } from './BoardPinButton';
9
+ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'preact/hooks';
10
+ import { mutatingNodeIds, sessionChromeActive } from '../state/presence-store';
3
11
  import { attentionPulseNodeIds } from '../state/attention-store';
4
- import { isAgentPin, NearPinMark, NodeContextMark } from './NodeContextMark';
5
- import { activityLensNodeIds } from '../state/session-store';
12
+ import { type ChipFold, headerChip, isAgentPin, NearPinMark, NodeContextMark, NodeTypeIcon } from './NodeContextMark';
13
+ import { activityLensNodeIds, sessionEditedIds } from '../state/session-store';
6
14
  import {
7
15
  activeNodeId,
8
16
  activeNeighborNodeIds,
@@ -37,6 +45,7 @@ import {
37
45
  updateNodeFromClient,
38
46
  } from '../state/intent-bridge';
39
47
  import { reportHumanGrab, takeOverNode, yieldedNodes } from '../state/human-store';
48
+ import { hoveredNodeId, noteNearAtGrab, settleNearAfterDrop } from '../state/near-pin-store';
40
49
  import { AxStepControls } from '../nodes/AxStepControls';
41
50
  import { getNodeIcon, IconChevronRight, IconClose, IconMore, IconExpand, IconPin } from '../icons';
42
51
  import { EXPANDABLE_TYPES, TYPE_LABELS } from '../types';
@@ -118,6 +127,43 @@ export function nodeChromeScale(viewportScale: number, nodeHeight: number, nodeW
118
127
  return Math.max(1, bounded);
119
128
  }
120
129
 
130
+ /** A header title keeps at least this much room, or all of it when shorter (HeaderMarks.dc.html). */
131
+ const TITLE_MIN_WIDTH = 140;
132
+
133
+ /**
134
+ * Whether the header's one state chip shows its word, only its glyph, or folds
135
+ * into the type icon's hint, so the title keeps TITLE_MIN_WIDTH (scaled with
136
+ * the chrome). Measured after each render, one step at a time; a different
137
+ * title, chip, width or zoom starts from the word again. Amber never folds.
138
+ */
139
+ function useHeaderFold(
140
+ titleRef: { current: HTMLSpanElement | null },
141
+ chip: { amber: boolean; folded: string } | null,
142
+ title: string,
143
+ width: number,
144
+ scale: number,
145
+ ): ChipFold {
146
+ const [fold, setFold] = useState<ChipFold>('word');
147
+ const key = `${title}\0${chip ? (chip.amber ? 'amber' : chip.folded) : ''}\0${width}\0${scale.toFixed(3)}`;
148
+ const keyRef = useRef(key);
149
+ useLayoutEffect(() => {
150
+ if (keyRef.current !== key) {
151
+ keyRef.current = key;
152
+ if (fold !== 'word') {
153
+ setFold('word');
154
+ return;
155
+ }
156
+ }
157
+ const el = titleRef.current;
158
+ if (!el || !chip || chip.amber || fold === 'hidden') return;
159
+ const room = Math.min(el.scrollWidth, TITLE_MIN_WIDTH * scale);
160
+ if (el.clientWidth + 0.5 < room) setFold(fold === 'word' ? 'glyph' : 'hidden');
161
+ // Measures only when what fits can change: a moved card keeps its key, so a
162
+ // drag frame does not force a layout read.
163
+ }, [key, fold]);
164
+ return chip ? fold : 'word';
165
+ }
166
+
121
167
  interface CanvasNodeProps {
122
168
  node: CanvasNodeState;
123
169
  children: preact.ComponentChildren;
@@ -128,6 +174,12 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
128
174
  const isActive = activeNodeId.value === node.id;
129
175
  const isSelected = selectedNodeIds.value.has(node.id);
130
176
  const isContextPinned = contextPinnedNodeIds.value.has(node.id);
177
+ const titleRef = useRef<HTMLSpanElement>(null);
178
+ const shellRef = useRef<HTMLDivElement>(null);
179
+ const portalTarget =
180
+ node.type === 'board' && typeof node.data.boardId === 'string'
181
+ ? (boardList.value.find((board) => board.id === node.data.boardId) ?? null)
182
+ : null;
131
183
  const isAttentionPulse = attentionPulseNodeIds.value.has(node.id);
132
184
  const isNeighbor = !isActive && activeNeighborNodeIds.value.has(node.id);
133
185
  const searchSet = searchHighlightIds.value;
@@ -160,6 +212,7 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
160
212
  clearSnapCache();
161
213
  activeGuides.value = null;
162
214
  reportHumanGrab(null);
215
+ settleNearAfterDrop(node.id);
163
216
  if (escListener.current) {
164
217
  document.removeEventListener('keydown', escListener.current);
165
218
  escListener.current = null;
@@ -258,6 +311,7 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
258
311
  bringToFront(node.id);
259
312
  // User wins (phase 8): hold the node for the drag; an agent mid-edit yields.
260
313
  reportHumanGrab(node.id);
314
+ noteNearAtGrab(node.id);
261
315
  takeOverNode(node.id, typeof node.data.title === 'string' && node.data.title ? node.data.title : node.type);
262
316
  buildSnapCache(node.id, nodes.value.values());
263
317
  if (node.type !== 'group') {
@@ -394,6 +448,7 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
394
448
  const isStrictSize = node.data.strictSize === true;
395
449
  const viewportScale = Math.max(viewport.value.scale, 0.01);
396
450
  const chromeScale = nodeChromeScale(viewportScale, node.collapsed ? 0 : node.size.height, node.size.width);
451
+ const chipFold = useHeaderFold(titleRef, headerChip(node, isContextPinned), title, node.size.width, chromeScale);
397
452
 
398
453
  const groupColor = isGroup && node.data.color ? (node.data.color as string) : undefined;
399
454
  const nodeStyle: Record<string, string | number> = {
@@ -408,7 +463,7 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
408
463
  };
409
464
  // Shimmer while an attached agent is mutating this node (rail-chrome-v2
410
465
  // phase 3). Gated on sessionActive: the quiet board never shimmers.
411
- const isAgentMutating = sessionActive.value && mutatingNodeIds.value.has(node.id);
466
+ const isAgentMutating = sessionChromeActive.value && mutatingNodeIds.value.has(node.id);
412
467
  const nodeClass = [
413
468
  'canvas-node',
414
469
  isActive ? 'active' : '',
@@ -424,7 +479,16 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
424
479
  isGroup ? 'group-node' : '',
425
480
  isStrictSize ? 'strict-size' : '',
426
481
  isAgentMutating ? 'agent-mutating' : '',
482
+ sessionChromeActive.value && sessionEditedIds.value.has(node.id) ? 'session-edited' : '',
427
483
  activityLensNodeIds.value && !activityLensNodeIds.value.has(node.id) ? 'lens-dimmed' : '',
484
+ relationHighlight.value && !relationHighlight.value.cards.has(node.id) ? 'relation-dimmed' : '',
485
+ knowledgeLifecycleEnabled.value && cardQualification(node.id)?.retired ? 'lifecycle-retired' : '',
486
+ lifecycleFilter.value === 'To review' &&
487
+ !cardQualification(node.id)?.due &&
488
+ !cardQualification(node.id)?.sources.length
489
+ ? 'lifecycle-filter-dimmed'
490
+ : '',
491
+ lifecycleFilter.value === 'Retired' && !cardQualification(node.id)?.retired ? 'lifecycle-filter-dimmed' : '',
428
492
  ]
429
493
  .filter(Boolean)
430
494
  .join(' ');
@@ -457,7 +521,6 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
457
521
  style={{ ...nodeStyle, width: 'auto', height: 'auto' }}
458
522
  onPointerDown={handlePointerDown}
459
523
  onDblClick={handleDblClick}
460
- title="Expand group"
461
524
  data-testid="group-chip"
462
525
  >
463
526
  <div class="node-titlebar group-chip-inner" onPointerDown={handleTitlePointerDown}>
@@ -484,280 +547,340 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
484
547
  </div>
485
548
  </div>
486
549
  </div>
550
+ <LifecycleFooter nodeId={node.id} />
487
551
  </div>
488
552
  );
489
553
  }
490
554
 
491
555
  return (
556
+ // Position/size without a stacking context: sibling ports participate in
557
+ // the world's lower hit plane, while the card keeps its own z-order.
492
558
  <div
493
- class={`${nodeClass}${isDropTarget ? ' is-drop-target' : ''}${isDropSource ? ' is-drop-source' : ''}${isEdgeTarget ? ' is-edge-target' : ''}${isGroup && groupMenuOpen ? ' group-menu-open' : ''}`}
494
- data-node-type={node.type}
495
- data-node-id={node.id}
496
- style={nodeStyle}
497
- tabIndex={isActive ? 0 : -1}
498
- onKeyDown={handleNodeKeyDown}
499
- onPointerDown={handlePointerDown}
500
- // Shift+click toggles multi-selection; the browser would otherwise extend
501
- // a text selection from the previous card across this one.
502
- onMouseDown={(e) => {
503
- if (e.shiftKey) e.preventDefault();
559
+ class="canvas-node-owner"
560
+ style={{ ...nodeStyle, zIndex: 'auto' }}
561
+ onPointerEnter={() => {
562
+ hoveredNodeId.value = node.id;
563
+ }}
564
+ onPointerLeave={() => {
565
+ if (hoveredNodeId.value === node.id) hoveredNodeId.value = null;
504
566
  }}
505
- onDblClick={handleDblClick}
506
567
  >
507
- <div class="node-content">
508
- {isGroup && (
509
- <div class="node-titlebar group-edge-row" onPointerDown={handleTitlePointerDown}>
510
- <span class="group-name-pill">
511
- {renaming ? (
512
- <input
513
- ref={renameRef}
514
- class="node-title-input group-title-input"
515
- value={title}
516
- onBlur={(e) => commitRename((e.target as HTMLInputElement).value)}
517
- onKeyDown={(e) => {
518
- if (e.key === 'Enter') commitRename((e.target as HTMLInputElement).value);
519
- if (e.key === 'Escape') setRenaming(false);
520
- }}
521
- onClick={(e) => e.stopPropagation()}
522
- />
523
- ) : (
524
- <span class="group-name" title={title}>
525
- {title}
526
- </span>
527
- )}
528
- <span class="group-count">{groupChildren.length}</span>
529
- </span>
530
- <span class="group-edge-spacer" />
531
- <span class="group-actions" role="toolbar" aria-label={`${title} actions`}>
532
- <button
533
- type="button"
534
- class="group-action"
535
- title="Auto-arrange children"
536
- aria-label="Auto-arrange children"
537
- disabled={groupChildren.length === 0}
538
- onClick={(e) => {
539
- e.stopPropagation();
540
- void addToGroupFromClient(node.id, groupChildren, 'grid');
541
- }}
542
- >
543
- <svg
544
- width="11"
545
- height="11"
546
- viewBox="0 0 16 16"
547
- fill="none"
548
- stroke="currentColor"
549
- stroke-width="1.5"
550
- aria-hidden="true"
551
- >
552
- <rect x="1.5" y="1.5" width="5" height="5" rx="1" />
553
- <rect x="9.5" y="1.5" width="5" height="5" rx="1" />
554
- <rect x="1.5" y="9.5" width="5" height="5" rx="1" />
555
- <rect x="9.5" y="9.5" width="5" height="5" rx="1" />
556
- </svg>
557
- </button>
558
- <button
559
- type="button"
560
- class="group-action"
561
- title="Collapse group"
562
- aria-label="Collapse group"
563
- onClick={(e) => {
564
- e.stopPropagation();
565
- toggleCollapsed(node.id);
566
- }}
567
- >
568
- <svg
569
- width="11"
570
- height="11"
571
- viewBox="0 0 16 16"
572
- fill="none"
573
- stroke="currentColor"
574
- stroke-width="1.8"
575
- stroke-linecap="round"
576
- stroke-linejoin="round"
577
- aria-hidden="true"
578
- >
579
- <path d="M3 10 L8 5 L13 10" />
580
- </svg>
581
- </button>
582
- <button
583
- type="button"
584
- class="group-action"
585
- title="Rename · Ungroup · Pin all to context"
586
- aria-label="Group menu"
587
- aria-expanded={groupMenuOpen}
588
- onClick={(e) => {
589
- e.stopPropagation();
590
- setGroupMenuOpen((open) => !open);
591
- }}
592
- >
593
- <IconMore size={14} />
594
- </button>
595
- {groupMenuOpen && (
596
- <div class="group-menu" role="menu" onPointerDown={(e) => e.stopPropagation()}>
597
- <button
598
- type="button"
599
- role="menuitem"
600
- onClick={(e) => {
601
- e.stopPropagation();
602
- setGroupMenuOpen(false);
603
- setRenaming(true);
604
- requestAnimationFrame(() => renameRef.current?.focus());
568
+ {isGroup && (
569
+ <div
570
+ class="group-frame-hit"
571
+ aria-hidden="true"
572
+ style={{ zIndex: `calc(${node.zIndex} - var(--node-hit-offset, 3))` }}
573
+ onPointerDown={(e) => {
574
+ handlePointerDown(e);
575
+ if (e.button === 0 && !e.shiftKey) shellRef.current?.focus({ preventScroll: true });
576
+ }}
577
+ onMouseDown={(e) => {
578
+ // Keep the shell's roving focus instead of the hit target's ancestor.
579
+ if (e.button === 0 || e.shiftKey) e.preventDefault();
580
+ }}
581
+ onDblClick={handleDblClick}
582
+ />
583
+ )}
584
+ <div
585
+ ref={shellRef}
586
+ class={`${nodeClass}${isDropTarget ? ' is-drop-target' : ''}${isDropSource ? ' is-drop-source' : ''}${isEdgeTarget ? ' is-edge-target' : ''}${isGroup && groupMenuOpen ? ' group-menu-open' : ''}`}
587
+ data-node-type={node.type}
588
+ data-node-id={node.id}
589
+ style={{
590
+ position: 'relative',
591
+ left: 0,
592
+ top: 0,
593
+ width: '100%',
594
+ height: node.collapsed ? 'auto' : '100%',
595
+ zIndex: node.zIndex,
596
+ }}
597
+ tabIndex={isActive ? 0 : -1}
598
+ onKeyDown={handleNodeKeyDown}
599
+ onPointerDown={handlePointerDown}
600
+ // Shift+click toggles multi-selection; the browser would otherwise extend
601
+ // a text selection from the previous card across this one.
602
+ onMouseDown={(e) => {
603
+ if (e.shiftKey) e.preventDefault();
604
+ }}
605
+ onDblClick={handleDblClick}
606
+ >
607
+ <div class="node-content">
608
+ {isGroup && (
609
+ <div class="node-titlebar group-edge-row" onPointerDown={handleTitlePointerDown}>
610
+ <span class="group-name-pill">
611
+ {renaming ? (
612
+ <input
613
+ ref={renameRef}
614
+ class="node-title-input group-title-input"
615
+ value={title}
616
+ onBlur={(e) => commitRename((e.target as HTMLInputElement).value)}
617
+ onKeyDown={(e) => {
618
+ if (e.key === 'Enter') commitRename((e.target as HTMLInputElement).value);
619
+ if (e.key === 'Escape') setRenaming(false);
605
620
  }}
606
- >
607
- Rename
608
- </button>
621
+ onClick={(e) => e.stopPropagation()}
622
+ />
623
+ ) : (
624
+ <span class="group-name">{title}</span>
625
+ )}
626
+ <span class="group-count">{groupChildren.length}</span>
627
+ </span>
628
+ <span class="group-edge-spacer" />
629
+ <span class="group-actions" role="toolbar" aria-label={`${title} actions`}>
630
+ <BarHint label="Auto-arrange children" align="end">
609
631
  <button
610
632
  type="button"
611
- role="menuitem"
633
+ class="group-action"
634
+ aria-label="Auto-arrange children"
635
+ disabled={groupChildren.length === 0}
612
636
  onClick={(e) => {
613
637
  e.stopPropagation();
614
- setGroupMenuOpen(false);
615
- // Dissolve — the same op an agent's ungroup runs.
616
- removeNode(node.id);
617
- void ungroupFromClient(node.id);
638
+ void addToGroupFromClient(node.id, groupChildren, 'grid');
618
639
  }}
619
640
  >
620
- Ungroup
641
+ <svg
642
+ width="11"
643
+ height="11"
644
+ viewBox="0 0 16 16"
645
+ fill="none"
646
+ stroke="currentColor"
647
+ stroke-width="1.5"
648
+ aria-hidden="true"
649
+ >
650
+ <rect x="1.5" y="1.5" width="5" height="5" rx="1" />
651
+ <rect x="9.5" y="1.5" width="5" height="5" rx="1" />
652
+ <rect x="1.5" y="9.5" width="5" height="5" rx="1" />
653
+ <rect x="9.5" y="9.5" width="5" height="5" rx="1" />
654
+ </svg>
621
655
  </button>
656
+ </BarHint>
657
+ <BarHint label="Collapse group" align="end">
622
658
  <button
623
659
  type="button"
624
- role="menuitem"
625
- disabled={groupChildren.length === 0}
660
+ class="group-action"
661
+ aria-label="Collapse group"
626
662
  onClick={(e) => {
627
663
  e.stopPropagation();
628
- setGroupMenuOpen(false);
629
- addContextPins(groupChildren);
664
+ toggleCollapsed(node.id);
630
665
  }}
631
666
  >
632
- Pin all to context
667
+ <svg
668
+ width="11"
669
+ height="11"
670
+ viewBox="0 0 16 16"
671
+ fill="none"
672
+ stroke="currentColor"
673
+ stroke-width="1.8"
674
+ stroke-linecap="round"
675
+ stroke-linejoin="round"
676
+ aria-hidden="true"
677
+ >
678
+ <path d="M3 10 L8 5 L13 10" />
679
+ </svg>
633
680
  </button>
634
- </div>
635
- )}
636
- </span>
637
- </div>
638
- )}
639
- {!isGroup && (
640
- <div class="node-titlebar" onPointerDown={handleTitlePointerDown}>
641
- <span class="node-type-icon" aria-hidden="true">
642
- {(() => {
643
- const NodeIcon = getNodeIcon(node.type);
644
- return <NodeIcon size={Math.round(14 * chromeScale)} />;
645
- })()}
646
- </span>
647
- {/* The kind-colored icon says what type this is (rail-chrome-v2 card
681
+ </BarHint>
682
+ <button
683
+ type="button"
684
+ class="group-action"
685
+ aria-label="Group menu"
686
+ aria-expanded={groupMenuOpen}
687
+ onClick={(e) => {
688
+ e.stopPropagation();
689
+ setGroupMenuOpen((open) => !open);
690
+ }}
691
+ >
692
+ <IconMore size={14} />
693
+ </button>
694
+ {groupMenuOpen && (
695
+ <div class="group-menu" role="menu" onPointerDown={(e) => e.stopPropagation()}>
696
+ <button
697
+ type="button"
698
+ role="menuitem"
699
+ onClick={(e) => {
700
+ e.stopPropagation();
701
+ setGroupMenuOpen(false);
702
+ setRenaming(true);
703
+ requestAnimationFrame(() => renameRef.current?.focus());
704
+ }}
705
+ >
706
+ Rename
707
+ </button>
708
+ <button
709
+ type="button"
710
+ role="menuitem"
711
+ onClick={(e) => {
712
+ e.stopPropagation();
713
+ setGroupMenuOpen(false);
714
+ // Dissolve — the same op an agent's ungroup runs.
715
+ removeNode(node.id);
716
+ void ungroupFromClient(node.id);
717
+ }}
718
+ >
719
+ Ungroup
720
+ </button>
721
+ <button
722
+ type="button"
723
+ role="menuitem"
724
+ disabled={groupChildren.length === 0}
725
+ onClick={(e) => {
726
+ e.stopPropagation();
727
+ setGroupMenuOpen(false);
728
+ addContextPins(groupChildren);
729
+ }}
730
+ >
731
+ Pin all to context
732
+ </button>
733
+ </div>
734
+ )}
735
+ </span>
736
+ </div>
737
+ )}
738
+ {!isGroup && (
739
+ <div class="node-titlebar" onPointerDown={handleTitlePointerDown}>
740
+ <NodeTypeIcon node={node} pinned={isContextPinned} title={title} fold={chipFold}>
741
+ {(() => {
742
+ const NodeIcon = getNodeIcon(node.type);
743
+ return <NodeIcon size={Math.round(14 * chromeScale)} />;
744
+ })()}
745
+ </NodeTypeIcon>
746
+ {/* The kind-colored icon says what type this is (rail-chrome-v2 card
648
747
  shell: icon · title · controls, no type badge). Only the AX status
649
748
  chip joins it, and only when there is a status to show. */}
650
- {typeof node.data.axWorkStatus === 'string' && (
651
- <span class={`node-ax-status node-ax-status-${node.data.axWorkStatus}`}>{node.data.axWorkStatus}</span>
652
- )}
653
- {renaming ? (
654
- <input
655
- ref={renameRef}
656
- class="node-title-input"
657
- value={title}
658
- onBlur={(e) => commitRename((e.target as HTMLInputElement).value)}
659
- onKeyDown={(e) => {
660
- if (e.key === 'Enter') commitRename((e.target as HTMLInputElement).value);
661
- if (e.key === 'Escape') setRenaming(false);
662
- }}
663
- onClick={(e) => e.stopPropagation()}
664
- />
665
- ) : (
666
- <span class="node-title" title={title}>
667
- {title}
668
- </span>
669
- )}
670
- <NearPinMark node={node} pinned={isContextPinned} />
671
- <NodeContextMark node={node} pinned={isContextPinned} />
672
- <div class="node-controls">
673
- {isPinned && (
674
- <span class="pin-indicator" title="Pinned">
675
- ⊙
749
+ {typeof node.data.axWorkStatus === 'string' && (
750
+ <span class={`node-ax-status node-ax-status-${node.data.axWorkStatus}`}>{node.data.axWorkStatus}</span>
751
+ )}
752
+ {renaming ? (
753
+ <input
754
+ ref={renameRef}
755
+ class="node-title-input"
756
+ value={title}
757
+ onBlur={(e) => commitRename((e.target as HTMLInputElement).value)}
758
+ onKeyDown={(e) => {
759
+ if (e.key === 'Enter') commitRename((e.target as HTMLInputElement).value);
760
+ if (e.key === 'Escape') setRenaming(false);
761
+ }}
762
+ onClick={(e) => e.stopPropagation()}
763
+ />
764
+ ) : (
765
+ <span ref={titleRef} class="node-title">
766
+ {title}
676
767
  </span>
677
768
  )}
678
- {/* Expand and the context pin always show; the rest sit behind ⋯ and ×
769
+ <NearPinMark node={node} pinned={isContextPinned} fold={chipFold} />
770
+ <NodeContextMark node={node} pinned={isContextPinned} fold={chipFold} />
771
+ <div class="node-controls">
772
+ {isPinned && (
773
+ <BarHint label="Pinned in place" align="end" fitWithin=".node-content" tapToOpen>
774
+ <span class="pin-indicator">⊙</span>
775
+ </BarHint>
776
+ )}
777
+ {/* Expand and the context pin always show; the rest sit behind ⋯ and ×
679
778
  on hover (docs/design/Chrome.dc.html). */}
680
- {EXPANDABLE_TYPES.has(node.type) && (
779
+ {EXPANDABLE_TYPES.has(node.type) && (
780
+ <BarHint label="Expand (focus mode)" align="end" fitWithin=".node-content">
781
+ <button
782
+ type="button"
783
+ onClick={(e) => {
784
+ e.stopPropagation();
785
+ expandNode(node.id);
786
+ }}
787
+ aria-label="Expand (focus mode)"
788
+ >
789
+ <IconExpand size={14} />
790
+ </button>
791
+ </BarHint>
792
+ )}
793
+ {node.type === 'board' ? (
794
+ // A portal's one pin is its board's pin: the linked board joins the
795
+ // agent's working set (BoardPins.dc.html), not this card.
796
+ portalTarget && <BoardPinButton board={portalTarget} inNodeHeader />
797
+ ) : (
798
+ <BarHint
799
+ label={isContextPinned ? 'Remove from context' : 'Add to context'}
800
+ align="end"
801
+ fitWithin=".node-content"
802
+ >
803
+ <button
804
+ type="button"
805
+ class={`ctx-pin-btn${isContextPinned ? ' ctx-pin-active' : ''}`}
806
+ onClick={(e) => {
807
+ e.stopPropagation();
808
+ toggleContextPin(node.id);
809
+ }}
810
+ aria-label={isContextPinned ? 'Remove from context' : 'Add to context'}
811
+ aria-pressed={isContextPinned}
812
+ >
813
+ <span class="ctx-pin-mark">
814
+ <IconPin />
815
+ {isContextPinned && isAgentPin(node.id) && (
816
+ <span class="ctx-pin-agent-dot" aria-hidden="true" />
817
+ )}
818
+ </span>
819
+ </button>
820
+ </BarHint>
821
+ )}
681
822
  <button
682
823
  type="button"
824
+ class="node-hover-control"
683
825
  onClick={(e) => {
684
826
  e.stopPropagation();
685
- expandNode(node.id);
827
+ onContextMenu?.(e, node.id);
686
828
  }}
687
- title="Expand (focus mode)"
829
+ aria-label="More actions"
688
830
  >
689
- <IconExpand size={14} />
831
+ <IconMore size={14} />
690
832
  </button>
691
- )}
692
- <button
693
- type="button"
694
- class={`ctx-pin-btn${isContextPinned ? ' ctx-pin-active' : ''}`}
695
- onClick={(e) => {
696
- e.stopPropagation();
697
- toggleContextPin(node.id);
698
- }}
699
- title={isContextPinned ? 'Remove from context' : 'Add to context'}
700
- aria-label={isContextPinned ? 'Remove from context' : 'Add to context'}
701
- aria-pressed={isContextPinned}
702
- >
703
- <span class="ctx-pin-mark">
704
- <IconPin />
705
- {isContextPinned && isAgentPin(node.id) && <span class="ctx-pin-agent-dot" aria-hidden="true" />}
706
- </span>
707
- </button>
708
- <button
709
- type="button"
710
- class="node-hover-control"
711
- onClick={(e) => {
712
- e.stopPropagation();
713
- onContextMenu?.(e, node.id);
714
- }}
715
- title="More actions"
716
- aria-label="More actions"
717
- >
718
- <IconMore size={14} />
719
- </button>
720
- {/* Report #64: status nodes get the same remove control as every other
833
+ {/* Report #64: status nodes get the same remove control as every other
721
834
  node type (backend removal + undo/history handle status uniformly). */}
722
- <button
723
- type="button"
724
- class="node-hover-control"
725
- onClick={(e) => {
726
- e.stopPropagation();
727
- void removeNodeFromClient(node.id);
728
- }}
729
- title="Close"
730
- >
731
- <IconClose size={14} />
732
- </button>
835
+ <button
836
+ type="button"
837
+ class="node-hover-control"
838
+ onClick={(e) => {
839
+ e.stopPropagation();
840
+ void removeNodeFromClient(node.id);
841
+ }}
842
+ aria-label="Close"
843
+ >
844
+ <IconClose size={14} />
845
+ </button>
846
+ </div>
733
847
  </div>
734
- </div>
735
- )}
736
- {!node.collapsed && (
737
- <div ref={bodyRef} class="node-body">
738
- {children}
739
- {/* AX flow step controls — rendered here rather than in a node renderer
848
+ )}
849
+ {!node.collapsed && (
850
+ <div ref={bodyRef} class="node-body">
851
+ {children}
852
+ {/* AX flow step controls — rendered here rather than in a node renderer
740
853
  because a materialized step can be ANY node type, and because the
741
854
  body is what auto-fit measures (a footer outside it would be clipped
742
855
  on a content-fitted node). Renders nothing unless the node carries a
743
856
  `data.axStep` stamp. */}
744
- <AxStepControls node={node} />
745
- </div>
857
+ <AxStepControls node={node} />
858
+ {!isGroup && <LifecycleFooter nodeId={node.id} />}
859
+ <CrossBoardReferences node={node} />
860
+ </div>
861
+ )}
862
+ {/* Group children live in the world layer, not this body. Keep their
863
+ warning in the frame's bottom chrome, out of the content flex rule. */}
864
+ {(node.collapsed || isGroup) && <LifecycleFooter nodeId={node.id} />}
865
+ </div>
866
+ {!node.collapsed && (
867
+ <div class="node-resize-handle" onPointerDown={(e) => startResize(e, node.size.width, node.size.height)} />
868
+ )}
869
+ {isSelected &&
870
+ (['tl', 'tr', 'bl', 'br'] as const).map((corner) => (
871
+ <span key={corner} class={`node-selection-handle is-${corner}`} aria-hidden="true" />
872
+ ))}
873
+ {yieldedNodes.value.has(node.id) && (
874
+ <span class="node-yield-pill" data-testid="yield-pill">
875
+ <span class="node-yield-dot" aria-hidden="true" />
876
+ {yieldedNodes.value.get(node.id)} took over — agent yielded
877
+ </span>
746
878
  )}
879
+ {!isGroup &&
880
+ (['top', 'right', 'bottom', 'left'] as const).map((side) => (
881
+ <span key={side} class={`node-port-dot node-port-dot-${side}`} aria-hidden="true" />
882
+ ))}
747
883
  </div>
748
- {!node.collapsed && (
749
- <div class="node-resize-handle" onPointerDown={(e) => startResize(e, node.size.width, node.size.height)} />
750
- )}
751
- {isSelected &&
752
- (['tl', 'tr', 'bl', 'br'] as const).map((corner) => (
753
- <span key={corner} class={`node-selection-handle is-${corner}`} aria-hidden="true" />
754
- ))}
755
- {yieldedNodes.value.has(node.id) && (
756
- <span class="node-yield-pill" data-testid="yield-pill">
757
- <span class="node-yield-dot" aria-hidden="true" />
758
- {yieldedNodes.value.get(node.id)} took over — agent yielded
759
- </span>
760
- )}
761
884
  {/* Connection port handles — visible on hover, drag to connect. Groups
762
885
  are frames, not endpoints: no ports (and the edge row sits on the top
763
886
  edge where the port would be). */}