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,25 +1,138 @@
1
- import type { Signal } from '@preact/signals';
1
+ import { type Signal, signal } from '@preact/signals';
2
+ import { useEffect, useRef } from 'preact/hooks';
2
3
  import {
3
- activeNodeId,
4
4
  draggingEdge,
5
+ edges,
5
6
  searchHighlightIds,
6
7
  selectedEdgeId,
7
8
  viewport,
8
9
  visibleNodeFor,
9
10
  } from '../state/canvas-store';
11
+ import { linkMarks, writerName } from '../state/context-status-store';
12
+ import { HUMAN_STARTED_SESSION_LABEL } from '../../shared/agent-presence';
13
+ import { activeSession } from '../state/presence-store';
14
+ import { activeBoard } from '../state/boards-store';
15
+ import { reportClientViewportSize } from '../state/intent-bridge';
16
+ import { relationDefinition, relationKind, relationText } from '../../shared/relations.js';
10
17
  import type { CanvasEdge, CanvasNodeState } from '../types';
18
+ import { relationFilter, relationFocus, relationHighlight } from '../state/relation-store';
11
19
 
12
- // ── Edge type visual styles ──────────────────────────────────
13
- const EDGE_COLORS: Record<CanvasEdge['type'], string> = {
14
- relation: 'var(--c-muted)',
15
- 'depends-on': 'var(--c-rel-informs)',
16
- flow: 'var(--c-accent)',
17
- // --c-dim is a very low-contrast hairline in dark palettes (forest: #5D7566
18
- // on #0C1712); the dashed style is what distinguishes `references`.
19
- references: 'var(--c-muted)',
20
- };
20
+ /** Filter only the kinds on this board; shared presets are managed in Home. */
21
+ export function RelationLegend() {
22
+ const board = activeBoard();
23
+ const list = [...edges.value.values()];
24
+ const legendRef = useRef<HTMLDivElement>(null);
25
+ useEffect(() => {
26
+ const legend = legendRef.current;
27
+ if (!legend) return;
28
+ const observer = new ResizeObserver(() => void reportClientViewportSize());
29
+ observer.observe(legend);
30
+ return () => observer.disconnect();
31
+ }, [board?.id, list.length > 0]);
32
+ if (!board || list.length === 0) return null;
33
+ const kinds = [...new Set(list.map(relationKind))];
34
+ const selected = relationFilter.value?.boardId === board.id ? relationFilter.value.kind : null;
35
+ return (
36
+ <div
37
+ ref={legendRef}
38
+ class="relation-legend"
39
+ aria-label="Relation legend"
40
+ onPointerDown={(event) => event.stopPropagation()}
41
+ >
42
+ <select
43
+ aria-label="Filter board relations"
44
+ value={selected ?? ''}
45
+ onChange={(event) => {
46
+ const kind = event.currentTarget.value;
47
+ relationFilter.value = kind ? { boardId: board.id, kind } : null;
48
+ }}
49
+ >
50
+ <option value="">All relations</option>
51
+ {kinds.map((kind) => (
52
+ <option key={kind} value={kind}>
53
+ {kind} · {list.filter((edge) => relationKind(edge) === kind).length}
54
+ </option>
55
+ ))}
56
+ </select>
57
+ {kinds.map((kind) => {
58
+ const definition = relationDefinition({ kind }, board.relationVocabulary);
59
+ return (
60
+ <button
61
+ type="button"
62
+ key={kind}
63
+ aria-pressed={selected === kind}
64
+ style={{ '--relation-ink': `var(--c-rel-${definition.ink})` }}
65
+ onClick={() => {
66
+ relationFilter.value = selected === kind ? null : { boardId: board.id, kind };
67
+ }}
68
+ >
69
+ <span class="relation-swatch" aria-hidden="true" />
70
+ {kind} <span class="relation-count">{list.filter((edge) => relationKind(edge) === kind).length}</span>
71
+ </button>
72
+ );
73
+ })}
74
+ </div>
75
+ );
76
+ }
77
+
78
+ /** The attached agent's name; a session you started that no agent has joined yet names no one. */
79
+ function agentName(): string {
80
+ const label = activeSession.value?.label;
81
+ return label && label !== HUMAN_STARTED_SESSION_LABEL ? label : 'the agent';
82
+ }
21
83
 
22
- const DIRECTED_TYPES = new Set<CanvasEdge['type']>(['depends-on', 'flow']);
84
+ /** The hovered link pill's hint, drawn at screen level by `EdgeHint` (the edge layer is SVG in world space). */
85
+ const edgeHint = signal<{ x: number; y: number; text: string; edgeId: string; view: unknown } | null>(null);
86
+
87
+ function clock(iso: string | undefined): string {
88
+ if (!iso) return '';
89
+ const date = new Date(iso);
90
+ return Number.isNaN(date.getTime())
91
+ ? ''
92
+ : ` · ${date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`;
93
+ }
94
+
95
+ function who(actor: NonNullable<CanvasEdge['changedBy']>): string {
96
+ return actor.actor === 'agent' ? writerName(actor) : 'you';
97
+ }
98
+
99
+ /**
100
+ * Who drew the link and when (docs/design/LinkAuthorship.dc.html): "Drawn by
101
+ * Codex · 14:06", or, once someone else changed it, "You relabelled it · 14:20
102
+ * (drawn by Codex · 14:06)". A folded "not seen" state joins it.
103
+ */
104
+ function linkHint(edge: CanvasEdge, notSeen: boolean): string | null {
105
+ // A link from before authorship was recorded names no one: "you" could be false.
106
+ if (edge.changedBy?.actor !== 'agent' && edge.changedBy?.actor !== 'human') return null;
107
+ const by = who(edge.changedBy);
108
+ const takenOver = !!edge.createdBy && who(edge.createdBy) !== by;
109
+ const lead = takenOver
110
+ ? `${by === 'you' ? 'You' : by} relabelled it${clock(edge.changedAt)} (drawn by ${edge.createdBy ? who(edge.createdBy) : ''}${clock(edge.createdAt)})`
111
+ : `${by === 'you' ? 'You drew it' : `Drawn by ${by}`}${clock(edge.createdAt ?? edge.changedAt)}`;
112
+ return notSeen ? `${lead} · not seen by ${agentName()} yet` : lead;
113
+ }
114
+
115
+ /** The link pill's hint, outside the zoomed canvas so it keeps screen size and is never clipped. */
116
+ export function EdgeHint() {
117
+ const hint = edgeHint.value;
118
+ // The pill can vanish or move under a resting pointer (the link removed, the
119
+ // view panned or zoomed) without a pointerleave: the hint goes with it.
120
+ if (!hint || hint.view !== viewport.value || !edges.value.has(hint.edgeId)) return null;
121
+ return (
122
+ <span
123
+ class="toolbar-tooltip edge-hint"
124
+ role="tooltip"
125
+ data-testid="edge-hint"
126
+ style={{ left: `${hint.x}px`, top: `${hint.y}px` }}
127
+ >
128
+ <span class="toolbar-tooltip-label">{hint.text}</span>
129
+ </span>
130
+ );
131
+ }
132
+
133
+ // Connection-change glyphs (docs/design/LinksOptions.dc.html, option C), 24-unit paths.
134
+ const LINK_MARK_EYE_OFF =
135
+ 'M3 3l18 18M10.6 6.1A10 10 0 0 1 12 6c6.5 0 10 6 10 6a17 17 0 0 1-3 3.6M6.6 6.6C3.8 8.3 2 12 2 12s3.5 6 10 6a9.6 9.6 0 0 0 4.4-1';
23
136
 
24
137
  /**
25
138
  * Edges are drawn in world space, so a 1.5px stroke renders as 0.4 screen px at
@@ -37,6 +150,8 @@ function dashArray(edge: CanvasEdge, scale: number): string | undefined {
37
150
  const dashed = `${8 * scale} ${4 * scale}`;
38
151
  if (edge.style === 'dashed') return dashed;
39
152
  if (edge.style === 'dotted') return `${3 * scale} ${3 * scale}`;
153
+ if (!edge.style && relationDefinition(edge, activeBoard()?.relationVocabulary).kind === 'related')
154
+ return `${2 * scale} ${5 * scale}`;
40
155
  if (edge.type === 'references' && !edge.style) return dashed;
41
156
  return undefined;
42
157
  }
@@ -128,17 +243,24 @@ interface EdgePathProps {
128
243
  edge: CanvasEdge;
129
244
  fromNode: CanvasNodeState;
130
245
  toNode: CanvasNodeState;
246
+ start: Anchor;
247
+ end: Anchor;
131
248
  focused: boolean; // connected to the active node
132
249
  dimmed: boolean; // active node exists but this edge is NOT connected
133
250
  selected: boolean; // click-selected — Delete removes it
134
251
  scale: number; // inverse-viewport compensation, see edgeChromeScale()
135
- onContextMenu?: (e: MouseEvent, edgeId: string) => void;
252
+ part: EdgeLayerPart;
136
253
  }
137
254
 
138
- function EdgePath({ edge, fromNode, toNode, focused, dimmed, selected, scale, onContextMenu }: EdgePathProps) {
139
- const start = computeAnchor(fromNode, toNode);
140
- const end = computeAnchor(toNode, fromNode);
255
+ /**
256
+ * Lines draw below group frames, so a link never covers a group's header; pills
257
+ * draw in their own layer above the frames, so a link inside a group keeps a
258
+ * label (LinkAuthorship.dc.html). Transparent pill hits stay below ports without
259
+ * moving the painted label below the frame's wash.
260
+ */
261
+ export type EdgeLayerPart = 'lines' | 'pills' | 'pill-hits';
141
262
 
263
+ function EdgePath({ edge, fromNode, toNode, start, end, focused, dimmed, selected, scale, part }: EdgePathProps) {
142
264
  const dx = end.x - start.x;
143
265
  const dy = end.y - start.y;
144
266
  const dist = Math.sqrt(dx * dx + dy * dy);
@@ -148,66 +270,180 @@ function EdgePath({ edge, fromNode, toNode, focused, dimmed, selected, scale, on
148
270
  const cp2 = sideControlPoint(end, curvature);
149
271
 
150
272
  const d = `M ${start.x} ${start.y} C ${cp1.x} ${cp1.y}, ${cp2.x} ${cp2.y}, ${end.x} ${end.y}`;
151
- const color = EDGE_COLORS[edge.type];
152
- const directed = DIRECTED_TYPES.has(edge.type);
273
+ const definition = relationDefinition(edge, activeBoard()?.relationVocabulary);
274
+ const color = `var(--c-rel-${definition.ink})`;
275
+ const directed = definition.directed;
153
276
  const dash = dashArray(edge, scale);
154
277
 
155
- // Labels keep a constant screen size only while nodes are still readable.
156
- // The uncapped chrome compensation made "brief" dwarf its own nodes at
157
- // overview zoom — cap label chrome like node chrome (2.2), and drop labels
158
- // entirely once the board is slivers (chrome scale > 3.4 ≈ zoom < 30%).
278
+ // Labels keep a constant screen size only while nodes are still readable —
279
+ // label chrome caps like node chrome (2.2). Below 35% zoom the label goes and
280
+ // only the link's marks stay (LinkAuthorship.dc.html).
159
281
  const labelScale = Math.min(scale, 2.2);
160
- const mid =
161
- edge.label && scale <= 3.4 ? bezierMidpoint(start.x, start.y, cp1.x, cp1.y, cp2.x, cp2.y, end.x, end.y) : null;
282
+ const zoom = viewport.value.scale;
283
+ const marks = linkMarks(edge);
284
+ // Reasons belong in the hint/inspector, not a long pill covering both endpoints.
285
+ const label = edge.label ?? edge.kind;
286
+ const showLabel = !!label && zoom >= 0.35;
287
+ // Pill layout in label units (× labelScale): the label, then a hairline and
288
+ // "not seen by …", which keeps only its glyph on a short line or below 60% zoom.
289
+ const pad = showLabel ? 9 : 5;
290
+ const labelWidth = showLabel ? (label?.length ?? 0) * 7 : 0;
291
+ const notSeenText = `not seen by ${agentName()}`;
292
+ const wordedNotSeenWidth = 12 + notSeenText.length * 6.2 + 4;
293
+ const fullWidth = pad * 2 + labelWidth + (marks.notSeen ? (labelWidth ? 12 : 0) + wordedNotSeenWidth : 0);
294
+ // A short line: the full pill plus some line on either side would not fit.
295
+ const notSeenWords = marks.notSeen && zoom >= 0.6 && dist >= (fullWidth + 60) * labelScale;
296
+ const notSeenWidth = marks.notSeen ? (labelWidth ? 12 : 0) + (notSeenWords ? wordedNotSeenWidth : 12) : 0;
297
+ const pillWidth = pad * 2 + labelWidth + notSeenWidth;
298
+ const labelX = -pillWidth / 2 + pad;
299
+ const notSeenX = labelX + labelWidth + (labelWidth ? 12 : 0);
300
+ const hasPill = showLabel || marks.notSeen;
301
+ const mid = hasPill ? bezierMidpoint(start.x, start.y, cp1.x, cp1.y, cp2.x, cp2.y, end.x, end.y) : null;
162
302
 
163
303
  const pathId = `edge-path-${edge.id}`;
304
+ const select = (e: MouseEvent) => {
305
+ // The viewport's background click clears the selection — a click
306
+ // that LANDED on an edge must not immediately undo itself.
307
+ e.stopPropagation();
308
+ selectedEdgeId.value = edge.id;
309
+ };
310
+ const showHint = (anchor: Element, point?: { x: number; y: number }) => {
311
+ const text = [
312
+ `${String(fromNode.data.title ?? fromNode.id)} → ${String(toNode.data.title ?? toNode.id)}`,
313
+ relationText(edge),
314
+ linkHint(edge, marks.notSeen),
315
+ ]
316
+ .filter(Boolean)
317
+ .join('\n');
318
+ const rect = anchor.getBoundingClientRect();
319
+ edgeHint.value = {
320
+ x: point?.x ?? rect.left + rect.width / 2,
321
+ y: (point?.y ?? rect.bottom) + 8,
322
+ text,
323
+ edgeId: edge.id,
324
+ view: viewport.value,
325
+ };
326
+ };
327
+ const hideHint = () => {
328
+ edgeHint.value = null;
329
+ };
330
+
331
+ if (part === 'pill-hits') {
332
+ if (!mid) return null;
333
+ return (
334
+ <rect
335
+ class="edge-pill-hit"
336
+ data-testid="edge-pill-hit"
337
+ data-edge-id={edge.id}
338
+ x={mid.x - (pillWidth / 2) * labelScale}
339
+ y={mid.y - 10 * labelScale}
340
+ width={pillWidth * labelScale}
341
+ height={20 * labelScale}
342
+ rx={10 * labelScale}
343
+ fill="transparent"
344
+ onClick={select}
345
+ onPointerEnter={(e) => showHint(e.currentTarget)}
346
+ onPointerLeave={hideHint}
347
+ />
348
+ );
349
+ }
350
+
351
+ if (part === 'pills') {
352
+ if (!mid) return null;
353
+ return (
354
+ <g
355
+ transform={`translate(${mid.x}, ${mid.y})`}
356
+ class="edge-pill"
357
+ data-testid="edge-pill"
358
+ opacity={dimmed && !selected ? 0.35 : 1}
359
+ >
360
+ <rect
361
+ class="edge-label-bg"
362
+ x={(-pillWidth / 2) * labelScale}
363
+ y={-10 * labelScale}
364
+ width={pillWidth * labelScale}
365
+ height={20 * labelScale}
366
+ rx={10 * labelScale}
367
+ />
368
+ {showLabel && (
369
+ <text
370
+ class="edge-label"
371
+ x={(labelX + labelWidth / 2) * labelScale}
372
+ text-anchor="middle"
373
+ dominant-baseline="central"
374
+ fill={color}
375
+ >
376
+ {label}
377
+ </text>
378
+ )}
379
+ {marks.notSeen && (
380
+ <g class="edge-link-mark is-not-seen" data-testid="edge-link-mark">
381
+ {labelWidth > 0 && (
382
+ <line
383
+ x1={(notSeenX - 6) * labelScale}
384
+ x2={(notSeenX - 6) * labelScale}
385
+ y1={-6 * labelScale}
386
+ y2={6 * labelScale}
387
+ stroke="var(--c-line)"
388
+ stroke-width={labelScale}
389
+ />
390
+ )}
391
+ <path
392
+ transform={`translate(${notSeenX * labelScale}, ${-5 * labelScale}) scale(${(10 / 24) * labelScale})`}
393
+ d={LINK_MARK_EYE_OFF}
394
+ fill="none"
395
+ stroke="currentColor"
396
+ stroke-width="2"
397
+ stroke-linecap="round"
398
+ stroke-linejoin="round"
399
+ />
400
+ {notSeenWords && (
401
+ <text
402
+ class="edge-label"
403
+ x={(notSeenX + 14) * labelScale}
404
+ text-anchor="start"
405
+ dominant-baseline="central"
406
+ fill="currentColor"
407
+ >
408
+ {notSeenText}
409
+ </text>
410
+ )}
411
+ </g>
412
+ )}
413
+ </g>
414
+ );
415
+ }
164
416
 
165
417
  return (
166
418
  <g>
167
419
  {/* Invisible wide hitbox — the ONE interactive part of the edge layer
168
420
  (the svg itself is pointer-events:none): click selects the edge
169
- (Delete then removes it), double-click opens the edge menu. */}
421
+ (Delete then removes it); usePanZoom owns right-click versus drag. */}
170
422
  <path
423
+ data-edge-id={edge.id}
171
424
  d={d}
172
425
  fill="none"
173
426
  stroke="transparent"
174
427
  stroke-width={12 * scale}
175
428
  style={{ cursor: 'pointer', pointerEvents: 'stroke' }}
176
- onClick={(e) => {
177
- // The viewport's background click clears the selection — a click
178
- // that LANDED on an edge must not immediately undo itself.
179
- e.stopPropagation();
180
- selectedEdgeId.value = edge.id;
181
- }}
182
- onDblClick={onContextMenu ? (e) => onContextMenu(e as unknown as MouseEvent, edge.id) : undefined}
429
+ onClick={select}
430
+ onPointerEnter={(e) => showHint(e.currentTarget, { x: e.clientX, y: e.clientY })}
431
+ onPointerLeave={hideHint}
183
432
  />
184
433
 
185
- {/* Glow layer for focused or selected edges */}
186
- {(focused || selected) && (
187
- <path
188
- d={d}
189
- fill="none"
190
- stroke={color}
191
- stroke-width={6 * scale}
192
- stroke-dasharray={dash}
193
- opacity={selected ? 0.3 : 0.15}
194
- style={{ filter: 'blur(3px)' }}
195
- />
196
- )}
197
-
198
434
  {/* Visible edge — the `edge-arrow` marker uses the default
199
435
  markerUnits="strokeWidth", so the arrowhead scales with this. */}
200
436
  <path
201
437
  id={pathId}
202
438
  d={d}
203
- class={selected ? 'edge-selected' : undefined}
439
+ class={selected ? 'edge-line edge-selected' : 'edge-line'}
204
440
  fill="none"
205
441
  stroke={color}
206
442
  stroke-width={(selected ? 3 : focused ? 2.5 : 1.5) * scale}
207
443
  stroke-dasharray={dash}
208
444
  marker-end={directed ? 'url(#edge-arrow)' : undefined}
209
445
  opacity={selected ? 1 : dimmed ? 0.2 : focused ? 1 : 0.85}
210
- style={{ transition: 'opacity 0.2s, stroke-width 0.2s' }}
446
+ style={{ color }}
211
447
  />
212
448
 
213
449
  {/* Animated pulse dot */}
@@ -218,23 +454,6 @@ function EdgePath({ edge, fromNode, toNode, focused, dimmed, selected, scale, on
218
454
  </animateMotion>
219
455
  </circle>
220
456
  )}
221
-
222
- {/* Label at midpoint */}
223
- {mid && edge.label && (
224
- <g transform={`translate(${mid.x}, ${mid.y})`}>
225
- <rect
226
- class="edge-label-bg"
227
- x={-(edge.label.length * 3.5 + 8) * labelScale}
228
- y={-10 * labelScale}
229
- width={(edge.label.length * 7 + 16) * labelScale}
230
- height={20 * labelScale}
231
- rx={4 * labelScale}
232
- />
233
- <text class="edge-label" text-anchor="middle" dominant-baseline="central" fill="var(--c-text)">
234
- {edge.label}
235
- </text>
236
- </g>
237
- )}
238
457
  </g>
239
458
  );
240
459
  }
@@ -243,20 +462,52 @@ function EdgePath({ edge, fromNode, toNode, focused, dimmed, selected, scale, on
243
462
  interface EdgeLayerProps {
244
463
  nodes: Signal<Map<string, CanvasNodeState>>;
245
464
  edges: Signal<Map<string, CanvasEdge>>;
246
- onEdgeContextMenu?: (e: MouseEvent, edgeId: string) => void;
465
+ part: EdgeLayerPart;
247
466
  }
248
467
 
249
- export function EdgeLayer({ nodes, edges, onEdgeContextMenu }: EdgeLayerProps) {
468
+ export function EdgeLayer({ nodes, edges, part }: EdgeLayerProps) {
250
469
  const nodeMap = nodes.value;
251
470
  const edgeList = Array.from(edges.value.values());
252
- const focusId = activeNodeId.value;
471
+ const focusId = relationFocus.value;
253
472
  const hasFocus = focusId !== null;
254
473
  const searchSet = searchHighlightIds.value;
255
474
  const hasSearch = searchSet !== null;
256
475
  const scale = edgeChromeScale(viewport.value.scale);
257
476
 
477
+ // Allocate a port to every incident link, including parallel and reverse links.
478
+ const ports = new Map<string, { start: Anchor; end: Anchor }>();
479
+ const sides = new Map<string, Array<{ anchor: Anchor; node: CanvasNodeState; order: number; id: string }>>();
480
+ for (const edge of edgeList) {
481
+ if (edge.toBoardId) continue;
482
+ const from = visibleNodeFor(edge.from);
483
+ const to = visibleNodeFor(edge.to);
484
+ if (!from || !to || from.id === to.id) continue;
485
+ const start = computeAnchor(from, to);
486
+ const end = computeAnchor(to, from);
487
+ ports.set(edge.id, { start, end });
488
+ for (const [anchor, node, other] of [
489
+ [start, from, to],
490
+ [end, to, from],
491
+ ] as const) {
492
+ const key = `${node.id}:${anchor.side}`;
493
+ const vertical = anchor.side === 'left' || anchor.side === 'right';
494
+ const group = sides.get(key) ?? [];
495
+ group.push({ anchor, node, order: vertical ? other.position.y : other.position.x, id: edge.id });
496
+ sides.set(key, group);
497
+ }
498
+ }
499
+ for (const group of sides.values()) {
500
+ if (group.length < 2) continue;
501
+ group.sort((a, b) => a.order - b.order || a.id.localeCompare(b.id));
502
+ group.forEach(({ anchor, node }, index) => {
503
+ const fraction = (index + 1) / (group.length + 1);
504
+ if (anchor.side === 'left' || anchor.side === 'right') anchor.y = node.position.y + node.size.height * fraction;
505
+ else anchor.x = node.position.x + node.size.width * fraction;
506
+ });
507
+ }
508
+
258
509
  // A drag-to-connect preview must draw on a board with no edges yet.
259
- if (edgeList.length === 0 && !draggingEdge.value) return null;
510
+ if (edgeList.length === 0 && (part !== 'lines' || !draggingEdge.value)) return null;
260
511
 
261
512
  const PAD = 96;
262
513
  const worldNodes = Array.from(nodeMap.values());
@@ -270,8 +521,9 @@ export function EdgeLayer({ nodes, edges, onEdgeContextMenu }: EdgeLayerProps) {
270
521
 
271
522
  return (
272
523
  <svg
273
- aria-label="Canvas connections"
274
- role="img"
524
+ aria-label={part === 'lines' ? 'Canvas connections' : undefined}
525
+ aria-hidden={part !== 'lines' ? 'true' : undefined}
526
+ role={part === 'lines' ? 'img' : undefined}
275
527
  viewBox={`${minX} ${minY} ${width} ${height}`}
276
528
  width={width}
277
529
  height={height}
@@ -279,13 +531,17 @@ export function EdgeLayer({ nodes, edges, onEdgeContextMenu }: EdgeLayerProps) {
279
531
  position: 'absolute',
280
532
  top: `${minY}px`,
281
533
  left: `${minX}px`,
534
+ // Painting stays unchanged. Hit priority is card > port > pill > group
535
+ // interior > line, including raised and nested group frames.
536
+ zIndex:
537
+ part === 'pills' ? 1 : part === 'pill-hits' ? 'calc(var(--node-port-z, -1) - 1)' : 'var(--edge-line-z, -5)',
282
538
  pointerEvents: 'none',
283
539
  overflow: 'visible',
284
540
  '--edge-chrome-scale': scale.toFixed(3),
285
541
  '--edge-label-scale': Math.min(scale, 2.2).toFixed(3),
286
542
  }}
287
543
  >
288
- <title>Canvas connections</title>
544
+ {part === 'lines' && <title>Canvas connections</title>}
289
545
  <defs>
290
546
  {/* userSpaceOnUse decouples the arrowhead from the UNCAPPED stroke
291
547
  compensation: the line stays a visible hairline at any zoom, but
@@ -301,32 +557,39 @@ export function EdgeLayer({ nodes, edges, onEdgeContextMenu }: EdgeLayerProps) {
301
557
  markerHeight={8 * Math.min(scale, 2.2)}
302
558
  orient="auto-start-reverse"
303
559
  >
304
- <path d="M 0 1 L 10 5 L 0 9 z" fill="currentColor" opacity="0.75" />
560
+ <path d="M 0 1 L 10 5 L 0 9 z" fill="context-stroke" opacity="0.75" />
305
561
  </marker>
306
562
  </defs>
307
563
  {edgeList.map((edge) => {
564
+ // Remote endpoints are navigable links, never a same-ID card on this board.
565
+ if (edge.toBoardId) return null;
308
566
  // An endpoint hidden behind a collapsed group's chip is drawn to the chip.
309
567
  const fromNode = visibleNodeFor(edge.from);
310
568
  const toNode = visibleNodeFor(edge.to);
311
- if (!fromNode || !toNode || fromNode.id === toNode.id) return null;
569
+ const anchors = ports.get(edge.id);
570
+ if (!fromNode || !toNode || !anchors) return null;
312
571
  const isConnected = hasFocus && (edge.from === focusId || edge.to === focusId);
313
572
  const searchDimmed = hasSearch && !(searchSet.has(edge.from) || searchSet.has(edge.to));
573
+ const highlight = relationHighlight.value;
314
574
  return (
315
575
  <EdgePath
316
576
  key={edge.id}
317
577
  edge={edge}
318
578
  fromNode={fromNode}
319
579
  toNode={toNode}
580
+ start={anchors.start}
581
+ end={anchors.end}
320
582
  focused={isConnected}
321
- dimmed={(hasFocus && !isConnected) || searchDimmed}
583
+ dimmed={(highlight !== null && !highlight.links.has(edge.id)) || searchDimmed}
322
584
  selected={selectedEdgeId.value === edge.id}
323
585
  scale={scale}
324
- onContextMenu={onEdgeContextMenu}
586
+ part={part}
325
587
  />
326
588
  );
327
589
  })}
328
590
  {/* Live preview edge while drag-connecting */}
329
- {draggingEdge.value &&
591
+ {part === 'lines' &&
592
+ draggingEdge.value &&
330
593
  (() => {
331
594
  const de = draggingEdge.value;
332
595
  // Route the preview EXACTLY like the committed edge will route
@@ -7,14 +7,15 @@ import { modChord } from '../utils/platform';
7
7
  import { canvasAreaCenter } from './canvas-area';
8
8
  import { importFiles } from './import-files';
9
9
  import { promptedCreate } from './ToolRail';
10
+ import { agentSessionsEnabled } from '../state/workspace-settings';
10
11
 
11
12
  /**
12
13
  * Empty board (rail-chrome-v2 phase 7, design item 11): centered onboarding
13
- * with the ghost mark, a 2×2 grid of starter actions, and the shortcut hint.
14
- * Every action is real: New note creates the same blank note as M, Drop files
14
+ * with direct starter actions and a separate way to start with your agent.
15
+ * Every action is real: Start a note creates the same blank note as M, Drop files
15
16
  * opens a picker onto the viewport's import path, Paste a link asks for a URL,
16
- * Start agent session attaches a browser-keyed session. Tokens only, so it
17
- * reads the same in dark and light.
17
+ * and the optional Start agent session attaches a browser-keyed session.
18
+ * Agent guidance stays visible independently of the session beta.
18
19
  */
19
20
  export function EmptyState({ onOpenPalette }: { onOpenPalette: () => void }) {
20
21
  const fileInput = useRef<HTMLInputElement>(null);
@@ -33,16 +34,8 @@ export function EmptyState({ onOpenPalette }: { onOpenPalette: () => void }) {
33
34
  <rect x="8" y="8" width="48" height="48" rx="7" />
34
35
  <rect x="20" y="20" width="24" height="24" rx="4" stroke-dasharray="4 4" />
35
36
  </svg>
36
- <div>
37
- <div class="empty-state-title">Nothing on this board yet</div>
38
- <div class="empty-state-sub">Drop files anywhere, paste a link, or start from a note.</div>
39
- </div>
37
+ <div class="empty-state-title">What would you like to work on?</div>
40
38
  <div class="empty-state-grid">
41
- <button type="button" class="empty-state-action" onClick={newNote}>
42
- <IconNodeMarkdown size={16} class="tone-accent" />
43
- <span class="empty-state-action-label">New markdown note</span>
44
- <kbd>M</kbd>
45
- </button>
46
39
  <button type="button" class="empty-state-action" onClick={() => fileInput.current?.click()}>
47
40
  <svg
48
41
  width="16"
@@ -58,19 +51,41 @@ export function EmptyState({ onOpenPalette }: { onOpenPalette: () => void }) {
58
51
  <path d="M8 10 V2 M8 2 L5 5 M8 2 L11 5" />
59
52
  <path d="M2 10 V13 H14 V10" />
60
53
  </svg>
61
- <span class="empty-state-action-label">Drop files</span>
54
+ <span class="empty-state-action-label">
55
+ Drop files here
56
+ <span class="empty-state-action-description">Bring in the material you want to work with.</span>
57
+ </span>
62
58
  <span class="empty-state-action-hint">or pick</span>
63
59
  </button>
64
60
  <button type="button" class="empty-state-action" onClick={() => promptedCreate('webpage')}>
65
61
  <IconNodeWebpage size={16} class="tone-ok" />
66
- <span class="empty-state-action-label">Paste a link</span>
62
+ <span class="empty-state-action-label">
63
+ Paste a link
64
+ <span class="empty-state-action-description">Add a source, reference, or inspiration.</span>
65
+ </span>
67
66
  <kbd>{modChord('V')}</kbd>
68
67
  </button>
69
- <button type="button" class="empty-state-action is-agent" onClick={() => void startSession()}>
70
- <IconSteer size={16} class="tone-purple" />
71
- <span class="empty-state-action-label">Start agent session</span>
68
+ <button type="button" class="empty-state-action" onClick={newNote}>
69
+ <IconNodeMarkdown size={16} class="tone-accent" />
70
+ <span class="empty-state-action-label">
71
+ Start a note
72
+ <span class="empty-state-action-description">
73
+ Write down an idea, then ask your agent to help you develop it.
74
+ </span>
75
+ </span>
76
+ <kbd>M</kbd>
72
77
  </button>
73
78
  </div>
79
+ <div class="empty-state-agent">
80
+ <div class="empty-state-agent-title">Or start with your agent</div>
81
+ <div class="empty-state-sub">Give your agent a task and ask it to work on this board.</div>
82
+ {agentSessionsEnabled.value && (
83
+ <button type="button" class="empty-state-action is-agent" onClick={() => void startSession()}>
84
+ <IconSteer size={16} class="tone-purple" />
85
+ <span class="empty-state-action-label">Start agent session</span>
86
+ </button>
87
+ )}
88
+ </div>
74
89
  <button type="button" class="empty-state-hint" onClick={onOpenPalette}>
75
90
  ? shortcuts · {modChord('K')} palette
76
91
  </button>