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,4 +1,5 @@
1
1
  import { effect } from '@preact/signals';
2
+ import { Component } from 'preact';
2
3
  import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
3
4
  import { askText } from './TextPrompt';
4
5
  import { ContextNode } from '../nodes/ContextNode';
@@ -42,6 +43,7 @@ import { importFiles } from './import-files';
42
43
  import { IntentLayer } from './IntentLayer';
43
44
  import { AgentPresenceLayer } from './AgentPresenceLayer';
44
45
  import { HumanPresenceLayer } from './HumanPresenceLayer';
46
+ import { NearPinCard, NearPinLayer } from './NearPinLayer';
45
47
  import { reportHumanCursor } from '../state/human-store';
46
48
  import { ScopeFenceLayer } from './ScopeFenceLayer';
47
49
  import { CanvasNode } from './CanvasNode';
@@ -94,6 +96,29 @@ function renderNodeContent(node: CanvasNodeState) {
94
96
  }
95
97
  }
96
98
 
99
+ /**
100
+ * One card in the world. A drag frame replaces only the moved node's object in
101
+ * the nodes map, so every other card keeps its props and skips the re-render
102
+ * (and its header re-measure); its own signals still update it.
103
+ */
104
+ class WorldNode extends Component<{
105
+ node: CanvasNodeState;
106
+ onContextMenu?: (e: MouseEvent, nodeId: string) => void;
107
+ }> {
108
+ shouldComponentUpdate(next: { node: CanvasNodeState; onContextMenu?: (e: MouseEvent, nodeId: string) => void }) {
109
+ return next.node !== this.props.node || next.onContextMenu !== this.props.onContextMenu;
110
+ }
111
+
112
+ render() {
113
+ const { node, onContextMenu } = this.props;
114
+ return (
115
+ <CanvasNode node={node} onContextMenu={onContextMenu}>
116
+ {renderNodeContent(node)}
117
+ </CanvasNode>
118
+ );
119
+ }
120
+ }
121
+
97
122
  function distanceToSegment(
98
123
  point: { x: number; y: number },
99
124
  start: { x: number; y: number },
@@ -295,6 +320,8 @@ export function CanvasViewport({
295
320
  const containerRef = usePanZoom({
296
321
  viewport,
297
322
  disabled: annotationMode,
323
+ onBackgroundContextMenu: onCanvasContextMenu,
324
+ onEdgeContextMenu,
298
325
  onViewportChange: (next) => {
299
326
  // Don't pan while lassoing — usePanZoom's pointerdown still fires
300
327
  // (native listener) before our Preact handler can stopPropagation.
@@ -683,8 +710,8 @@ export function CanvasViewport({
683
710
  );
684
711
 
685
712
  const handleContextMenu = useCallback((e: MouseEvent) => {
686
- // Right-click is a pan gesture, never a menu trigger. Suppress the
687
- // browser menu whether it lands on the board or bubbles from a node.
713
+ // usePanZoom distinguishes a background right-click from a right-drag
714
+ // on release. Suppress the native menu on both press and release.
688
715
  e.preventDefault();
689
716
  }, []);
690
717
 
@@ -783,6 +810,10 @@ export function CanvasViewport({
783
810
  expandedNodeId.value,
784
811
  new Set(hiddenByCollapsedGroup.value.keys()),
785
812
  );
813
+ const lowestZ = worldNodes.reduce((lowest, node) => Math.min(lowest, Math.floor(node.zIndex)), 0);
814
+ const highestZ = worldNodes.reduce((highest, node) => Math.max(highest, Math.ceil(node.zIndex)), 0);
815
+ // Keep group-body hit order without changing the frames' visual stacking.
816
+ const hitOffset = highestZ - lowestZ + 3;
786
817
  const dropTarget = dragDropTarget.value;
787
818
  const dropTargetGroup = dropTarget ? nodes.value.get(dropTarget.groupId) : undefined;
788
819
  const draggedNode = dropTarget ? nodes.value.get(dropTarget.nodeId) : undefined;
@@ -827,7 +858,9 @@ export function CanvasViewport({
827
858
  width: '100%',
828
859
  height: '100%',
829
860
  position: 'relative',
830
- overflow: 'hidden',
861
+ // The camera owns scrolling. Hidden permits native focus/resize scroll,
862
+ // adding an untracked offset underneath our viewport transform.
863
+ overflow: 'clip',
831
864
  cursor:
832
865
  annotationTool === 'eraser'
833
866
  ? 'cell'
@@ -849,6 +882,9 @@ export function CanvasViewport({
849
882
  style={{
850
883
  transform: `matrix(${v.scale}, 0, 0, ${v.scale}, ${v.x}, ${v.y})`,
851
884
  transformOrigin: '0 0',
885
+ '--node-port-z': lowestZ - 1,
886
+ '--node-hit-offset': hitOffset,
887
+ '--edge-line-z': lowestZ - hitOffset - 1,
852
888
  position: 'absolute',
853
889
  top: 0,
854
890
  left: 0,
@@ -858,7 +894,9 @@ export function CanvasViewport({
858
894
  <IntentLayer />
859
895
  <AgentPresenceLayer />
860
896
  <HumanPresenceLayer />
861
- <EdgeLayer nodes={nodes} edges={edges} onEdgeContextMenu={onEdgeContextMenu} />
897
+ <EdgeLayer nodes={nodes} edges={edges} part="lines" />
898
+ <EdgeLayer nodes={nodes} edges={edges} part="pills" />
899
+ <EdgeLayer nodes={nodes} edges={edges} part="pill-hits" />
862
900
  {draggingEdge.value && (
863
901
  <div
864
902
  class="edge-hint-pill"
@@ -893,9 +931,7 @@ export function CanvasViewport({
893
931
  <AnnotationLayer annotations={Array.from(annotations.value.values())} />
894
932
  {draftAnnotation && draftAnnotation.points.length >= 2 && <AnnotationLayer annotations={[draftAnnotation]} />}
895
933
  {worldNodes.map((node) => (
896
- <CanvasNode key={node.id} node={node} onContextMenu={onNodeContextMenu}>
897
- {renderNodeContent(node)}
898
- </CanvasNode>
934
+ <WorldNode key={node.id} node={node} onContextMenu={onNodeContextMenu} />
899
935
  ))}
900
936
  {/* Snap alignment guide lines */}
901
937
  {activeGuides.value && (
@@ -952,6 +988,8 @@ export function CanvasViewport({
952
988
  }}
953
989
  />
954
990
  )}
991
+ <NearPinLayer />
992
+ <NearPinCard />
955
993
  {lassoStyle && <div class="lasso-rect" style={lassoStyle} />}
956
994
  {dropActive && (
957
995
  <div class="drop-zone-overlay">
@@ -1,22 +1,26 @@
1
1
  import { useState } from 'preact/hooks';
2
+ import { BarHint } from './BarHint';
2
3
  import { IconSteer, IconClose } from '../icons';
3
- import { contextPinnedNodeIds, nodes, toggleContextPin } from '../state/canvas-store';
4
+ import { nodes, toggleContextPin } from '../state/canvas-store';
5
+ import { activeContextPinnedNodeIds } from '../state/knowledge-store';
4
6
  import { steerableAgents } from '../state/presence-store';
5
7
  import { sendSteering } from '../state/session-store';
6
8
  import { modChord } from '../utils/platform';
7
9
  import { ChoiceMenu } from './ChoiceMenu';
10
+ import { agentSessionsEnabled } from '../state/workspace-settings';
11
+ import { useChromeHeight } from './use-chrome-height';
8
12
 
9
13
  /**
10
14
  * Command bar (rail-chrome-v2 phase 5): the human's steering surface while a
11
15
  * session is attached — the design's floating composer, centered at the
12
- * bottom of the canvas region. Above it, the pinned context as gold ✦ chips
16
+ * bottom of the canvas region. Above it, the pinned context as blue chips
13
17
  * (× unpins — the same pin the node's own control toggles); the composer row
14
- * posts an AX steering message to the session. The context-budget meter lives
15
- * in the top bar (`ContextBudget`), where the mockup puts it. Replaces the
16
- * quiet board's pin bar only while attached.
18
+ * posts an AX steering message to the session. What is in the agent's
19
+ * context is summarised in the top bar's context chip (`ContextChip`).
17
20
  */
18
21
  export function CommandBar() {
19
- const pinnedIds = [...contextPinnedNodeIds.value];
22
+ const barRef = useChromeHeight('--command-bar-height');
23
+ const pinnedIds = [...activeContextPinnedNodeIds.value];
20
24
  const nodeMap = nodes.value;
21
25
  const [draft, setDraft] = useState('');
22
26
  const [sending, setSending] = useState(false);
@@ -43,7 +47,7 @@ export function CommandBar() {
43
47
 
44
48
  const submit = async () => {
45
49
  const message = draft.trim();
46
- if (!message || sending) return;
50
+ if (!agentSessionsEnabled.value || !message || sending) return;
47
51
  setSending(true);
48
52
  const ok = await sendSteering(message, effectiveTarget);
49
53
  setSending(false);
@@ -51,7 +55,13 @@ export function CommandBar() {
51
55
  };
52
56
 
53
57
  return (
54
- <div class="command-bar" data-testid="command-bar">
58
+ <div
59
+ ref={barRef}
60
+ class="command-bar"
61
+ data-testid="command-bar"
62
+ hidden={!agentSessionsEnabled.value}
63
+ style={!agentSessionsEnabled.value ? { display: 'none' } : undefined}
64
+ >
55
65
  {pinnedIds.length > 0 && (
56
66
  <div class="command-bar-chips" aria-label="Pinned context">
57
67
  {pinnedIds.map((id) => {
@@ -60,21 +70,17 @@ export function CommandBar() {
60
70
  typeof node?.data.title === 'string' && node.data.title.trim() ? node.data.title : (node?.type ?? id);
61
71
  return (
62
72
  <span key={id} class="command-bar-chip" data-node-id={id}>
63
- <span class="command-bar-chip-glyph" aria-hidden="true">
64
- ✦
65
- </span>
66
- <span class="command-bar-chip-label" title={title}>
67
- {title}
68
- </span>
69
- <button
70
- type="button"
71
- class="command-bar-chip-unpin"
72
- aria-label={`Unpin ${title}`}
73
- title="Remove from context"
74
- onClick={() => toggleContextPin(id)}
75
- >
76
- <IconClose size={14} />
77
- </button>
73
+ <span class="command-bar-chip-label">{title}</span>
74
+ <BarHint label="Remove from context" side="up">
75
+ <button
76
+ type="button"
77
+ class="command-bar-chip-unpin"
78
+ aria-label={`Unpin ${title}`}
79
+ onClick={() => toggleContextPin(id)}
80
+ >
81
+ <IconClose size={14} />
82
+ </button>
83
+ </BarHint>
78
84
  </span>
79
85
  );
80
86
  })}
@@ -13,12 +13,13 @@ import { saveCanvasTheme } from '../state/intent-bridge';
13
13
  import { createNodeInView } from './create-in-view';
14
14
  import { sessionActive } from '../state/presence-store';
15
15
  import { startSession } from '../state/session-store';
16
+ import { agentSessionsEnabled } from '../state/workspace-settings';
16
17
  import { TYPE_LABELS, type CanvasNodeState } from '../types';
17
18
  import { invalidateTokenCache } from '../theme/tokens';
18
19
  import { clearThemeOverride } from '../state/theme-override';
19
20
  import { canvasThemeScheme } from '../../shared/themes.js';
20
21
  import { getNodeIcon, IconArrange, IconFitAll, IconMinimap, IconMoon, IconNodeMarkdown, IconSteer } from '../icons';
21
- import { activeBoardId, openBoard } from '../state/boards-store';
22
+ import { openMemoryTarget } from '../state/boards-store';
22
23
  import { requestJson } from '../state/intent-bridge';
23
24
 
24
25
  import { modChord } from '../utils/platform';
@@ -219,7 +220,7 @@ export function CommandPalette({ onClose, onToggleMinimap }: { onClose: () => vo
219
220
  onClose();
220
221
  },
221
222
  },
222
- ...(sessionActive.value
223
+ ...(!agentSessionsEnabled.value || sessionActive.value
223
224
  ? []
224
225
  : [
225
226
  {
@@ -341,16 +342,8 @@ export function CommandPalette({ onClose, onToggleMinimap }: { onClose: () => vo
341
342
  indices: [],
342
343
  action: () => {
343
344
  void (async () => {
344
- await openBoard(hit.boardId);
345
- if (activeBoardId.value !== hit.boardId) return;
346
- for (let attempt = 0; attempt < 40 && activeBoardId.value === hit.boardId; attempt++) {
347
- if (nodes.value.has(hit.cardId)) {
348
- focusNode(hit.cardId);
349
- onClose();
350
- return;
351
- }
352
- await new Promise((resolve) => window.setTimeout(resolve, 25));
353
- }
345
+ await openMemoryTarget(hit);
346
+ onClose();
354
347
  })();
355
348
  },
356
349
  });
@@ -11,7 +11,8 @@ import {
11
11
  * Degraded connection states (rail-chrome-v2 phase 7, design item 14), mapped
12
12
  * onto the transport the bridge already runs:
13
13
  * - reconnecting: the stream (SSE or the polling fallback) is down and the
14
- * bridge is backing off. Edits still go over HTTP; the board may be stale.
14
+ * bridge is backing off. HTTP saves may need renewed capability after a
15
+ * daemon restart; the board may be stale.
15
16
  * - resyncing: a transport came back after a drop and the full `connected`
16
17
  * snapshot is being applied — the seq cursor was dropped, the board reloads
17
18
  * from the server's snapshot without touching local edits.
@@ -39,8 +40,7 @@ export function ConnectionBanner() {
39
40
  ↻
40
41
  </span>
41
42
  <span class="connection-banner-text">
42
- <b>Reconnecting…</b> live updates are paused; edits still save, the board may be stale until the stream
43
- returns.
43
+ <b>Reconnecting…</b> live updates are paused; retry refused saves after reconnect. The board may be stale.
44
44
  </span>
45
45
  <span class="connection-banner-meta">
46
46
  {attempt > 0 ? `retry ${attempt}` : 'retrying'}
@@ -0,0 +1,296 @@
1
+ import { age } from '../utils/time-ago';
2
+ import { useEffect, useRef, useState } from 'preact/hooks';
3
+ import { IconClose, IconPin } from '../icons';
4
+ import { attentionHistoryOpen } from '../state/attention-store';
5
+ import { activeBoardId, boardList, browsingMemory, setBoardPinned } from '../state/boards-store';
6
+ import { clearContextPins, nodes, toggleContextPin } from '../state/canvas-store';
7
+ import { activeContextPinnedNodeIds } from '../state/knowledge-store';
8
+ import {
9
+ approxTokens,
10
+ briefSize,
11
+ formatTokens,
12
+ pinnedBoardReads,
13
+ refreshContextChip,
14
+ } from '../state/context-chip-store';
15
+ import { contextPinMeta, linksChanged, pinnedReadState, writerName } from '../state/context-status-store';
16
+ import { nearPins } from '../state/near-pin-store';
17
+ import { activeSession } from '../state/presence-store';
18
+ import { BarHint } from './BarHint';
19
+ import { GLYPHS } from './NodeContextMark';
20
+
21
+ /** The quiet footer when Agent sessions is off; pins still curate agent context. */
22
+ export function ContextPinBar() {
23
+ const count = activeContextPinnedNodeIds.value.size;
24
+ if (count === 0 || attentionHistoryOpen.value) return null;
25
+
26
+ return (
27
+ <div class="context-pin-bar" role="region" aria-label="Pinned context">
28
+ <IconPin size={14} />
29
+ <span>{plural(count, 'node')} in context</span>
30
+ <BarHint label="Clear all context pins" side="up" align="end">
31
+ <button type="button" aria-label="Clear all context pins" onClick={clearContextPins}>
32
+ <IconClose size={12} />
33
+ </button>
34
+ </BarHint>
35
+ </div>
36
+ );
37
+ }
38
+
39
+ /**
40
+ * One context chip (docs/design/ContextChip.dc.html): everything in the agent's
41
+ * context, in brief order — cards pinned on this board, near (derived, muted),
42
+ * pinned boards (sent as maps) — with what the brief costs in tokens. Opened,
43
+ * one list with read state and unpin.
44
+ */
45
+
46
+ function plural(count: number, word: string): string {
47
+ return `${count} ${word}${count === 1 ? '' : 's'}`;
48
+ }
49
+
50
+ function nodeTitle(id: string): string {
51
+ const node = nodes.value.get(id);
52
+ return typeof node?.data.title === 'string' && node.data.title ? node.data.title : id;
53
+ }
54
+
55
+ export function ContextChip() {
56
+ const [open, setOpen] = useState(false);
57
+ const root = useRef<HTMLDivElement>(null);
58
+ const chip = useRef<HTMLButtonElement>(null);
59
+ const boardId = activeBoardId.value;
60
+
61
+ useEffect(() => {
62
+ if (!open) return;
63
+ refreshContextChip();
64
+ const onKey = (event: KeyboardEvent) => {
65
+ if (event.key !== 'Escape') return;
66
+ setOpen(false);
67
+ chip.current?.focus();
68
+ };
69
+ const onDown = (event: PointerEvent) => {
70
+ if (root.current && !root.current.contains(event.target as Node)) setOpen(false);
71
+ };
72
+ window.addEventListener('keydown', onKey);
73
+ window.addEventListener('pointerdown', onDown);
74
+ return () => {
75
+ window.removeEventListener('keydown', onKey);
76
+ window.removeEventListener('pointerdown', onDown);
77
+ };
78
+ }, [open]);
79
+
80
+ const library = browsingMemory.value;
81
+ if (!boardId && !library) return null;
82
+
83
+ // Library curation must not expose controls for the agent's hidden active cards.
84
+ const cardIds = library ? [] : [...activeContextPinnedNodeIds.value];
85
+ const near = library ? [] : [...nearPins.value.entries()];
86
+ const boards = boardList.value.filter((board) => board.pin && (library || board.id !== boardId));
87
+ const empty = cardIds.length === 0 && boards.length === 0;
88
+ const pinners = [
89
+ ...cardIds.map((id) => contextPinMeta.value[id]?.pinnedBy),
90
+ ...boards.map((board) => board.pin?.pinnedBy),
91
+ ];
92
+ const agentOnly = !empty && pinners.every((by) => by?.actor === 'agent');
93
+
94
+ const size = briefSize.value;
95
+ const cost = size ? `≈ ${approxTokens(size.chars)} tokens` : '';
96
+ const host = activeSession.value;
97
+
98
+ const segments: Array<{ text: string; kind?: 'near' | 'agent' }> = [];
99
+ if (cardIds.length) segments.push({ text: plural(cardIds.length, 'card') });
100
+ if (near.length) segments.push({ text: `${near.length} near`, kind: 'near' });
101
+ if (boards.length) segments.push({ text: plural(boards.length, 'board') });
102
+ if (agentOnly) {
103
+ const by = pinners.find((pinner) => pinner?.actor === 'agent');
104
+ segments.push({ text: `by ${by ? writerName(by) : 'agent'}`, kind: 'agent' });
105
+ }
106
+
107
+ return (
108
+ <div class="context-chip-wrap" ref={root}>
109
+ <button
110
+ ref={chip}
111
+ type="button"
112
+ class={`context-chip${empty ? ' is-empty' : ''}${open ? ' is-open' : ''}`}
113
+ aria-expanded={open}
114
+ aria-haspopup="dialog"
115
+ data-testid="context-chip"
116
+ onClick={() => setOpen(!open)}
117
+ >
118
+ <span class="context-chip-pin">
119
+ <IconPin size={13} />
120
+ {agentOnly && <span class="context-chip-agent-dot" aria-hidden="true" />}
121
+ </span>
122
+ {empty ? (
123
+ <span>Nothing in context</span>
124
+ ) : (
125
+ <>
126
+ <span class="context-chip-wide">
127
+ {segments.map((segment, index) => (
128
+ <span key={segment.text} class={segment.kind ? `is-${segment.kind}` : undefined}>
129
+ {index > 0 ? ' · ' : ''}
130
+ {segment.text}
131
+ </span>
132
+ ))}
133
+ </span>
134
+ <span class="context-chip-narrow">{cardIds.length + boards.length} in context</span>
135
+ {cost && (
136
+ <span class="context-chip-cost" data-testid="token-cost">
137
+ {cost}
138
+ </span>
139
+ )}
140
+ </>
141
+ )}
142
+ </button>
143
+ {open && (
144
+ <div class="context-panel" role="dialog" aria-label="In the agent's context">
145
+ <div class="context-panel-head">
146
+ <span class="context-panel-title">In the agent’s context</span>
147
+ {cost && <span class="context-panel-mono">{cost}</span>}
148
+ </div>
149
+ {empty ? (
150
+ <p class="context-panel-cap">
151
+ Nothing is pinned. Pin a card or a board and the agent gets it first; it can still find and pull anything
152
+ else on the board.
153
+ </p>
154
+ ) : (
155
+ <>
156
+ <p class="context-panel-cap">
157
+ What the canvas sends the agent costs {cost || 'a few tokens'}.
158
+ {host?.contextUsage
159
+ ? ` ${host.label}’s whole context holds ${formatTokens(host.contextUsage.used)} tokens.`
160
+ : ''}
161
+ </p>
162
+
163
+ <div class="context-panel-section">
164
+ <span>Cards on this board · {cardIds.length}</span>
165
+ {cardIds.length > 0 && (
166
+ <button type="button" class="context-panel-link" onClick={() => clearContextPins()}>
167
+ Clear all
168
+ </button>
169
+ )}
170
+ </div>
171
+ <ul class="context-panel-list">
172
+ {cardIds.map((id) => {
173
+ const node = nodes.value.get(id);
174
+ const state = pinnedReadState(id, node?.contentRevision ?? 0);
175
+ const links = state.kind === 'read' && linksChanged(id, node?.linksRevision ?? 0);
176
+ const byAgent = contextPinMeta.value[id]?.pinnedBy.actor === 'agent';
177
+ return (
178
+ <li key={id} class="context-panel-row">
179
+ <span class="context-panel-pin">
180
+ <IconPin size={9} />
181
+ {byAgent && <span class="context-chip-agent-dot" aria-hidden="true" />}
182
+ </span>
183
+ <span class="context-panel-name">{nodeTitle(id)}</span>
184
+ {links ? (
185
+ <span class="context-tag is-links">
186
+ <svg viewBox="0 0 24 24" width="10" height="10" aria-hidden="true">
187
+ <path d="M10 14a4 4 0 0 0 5.7 0l3-3a4 4 0 0 0-5.7-5.7l-1 1M14 10a4 4 0 0 0-5.7 0l-3 3a4 4 0 0 0 5.7 5.7l1-1" />
188
+ </svg>
189
+ links changed
190
+ </span>
191
+ ) : (
192
+ <ReadTag state={state.kind} at={state.kind === 'not-read' ? null : state.at} />
193
+ )}
194
+ <button
195
+ type="button"
196
+ class="context-panel-x"
197
+ aria-label={`Unpin ${nodeTitle(id)}`}
198
+ onClick={() => toggleContextPin(id)}
199
+ >
200
+ ×
201
+ </button>
202
+ </li>
203
+ );
204
+ })}
205
+ </ul>
206
+
207
+ {near.length > 0 && (
208
+ <>
209
+ <div class="context-panel-section">
210
+ <span>Near · {near.length}</span>
211
+ <span class="context-panel-cap-inline">
212
+ derived · <span class="context-panel-wide">sent as </span>title + summary
213
+ </span>
214
+ </div>
215
+ <ul class="context-panel-list">
216
+ {near.map(([id, pins]) => (
217
+ <li key={id} class="context-panel-row is-near">
218
+ <span class="context-panel-near-ring" aria-hidden="true" />
219
+ <span class="context-panel-name is-soft">{nodeTitle(id)}</span>
220
+ <span class="context-panel-cap-inline">near {pins[0]?.pinTitle}</span>
221
+ <button type="button" class="context-panel-promote" onClick={() => toggleContextPin(id)}>
222
+ Pin
223
+ </button>
224
+ </li>
225
+ ))}
226
+ </ul>
227
+ </>
228
+ )}
229
+
230
+ {boards.length > 0 && (
231
+ <>
232
+ <div class="context-panel-section">
233
+ <span>Boards · {boards.length}</span>
234
+ <span class="context-panel-cap-inline">sent as a map</span>
235
+ </div>
236
+ <ul class="context-panel-list">
237
+ {boards.map((board) => {
238
+ const read = pinnedBoardReads.value.get(board.id);
239
+ return (
240
+ <li key={board.id} class="context-panel-row">
241
+ <svg
242
+ class="context-panel-board"
243
+ viewBox="0 0 24 24"
244
+ width="14"
245
+ height="14"
246
+ aria-hidden="true"
247
+ >
248
+ <path d="M4 4h16v16H4zM9 9h6v6H9z" />
249
+ </svg>
250
+ <span class="context-panel-name">{board.name}</span>
251
+ <ReadTag state={read ? 'read' : 'not-read'} at={read?.lastReadAt ?? null} />
252
+ <span class="context-panel-cap-inline">map</span>
253
+ <button
254
+ type="button"
255
+ class="context-panel-x"
256
+ aria-label={`Unpin ${board.name}`}
257
+ onClick={() => void setBoardPinned(board.id, false)}
258
+ >
259
+ ×
260
+ </button>
261
+ </li>
262
+ );
263
+ })}
264
+ </ul>
265
+ <p class="context-panel-cap">
266
+ A pinned board travels as a map: its folder, README summary, each pinned card’s title and summary,
267
+ and its links and backlinks. The agent pulls a card or the whole board in full when it decides it
268
+ needs to.
269
+ </p>
270
+ </>
271
+ )}
272
+ </>
273
+ )}
274
+ </div>
275
+ )}
276
+ </div>
277
+ );
278
+ }
279
+
280
+ /** A read-state tag with its glyph (ContextChip.dc.html): eye read, eye-off not read, warn changed. */
281
+ function ReadTag({ state, at }: { state: 'read' | 'not-read' | 'changed'; at: string | null }) {
282
+ const [tone, glyph, word] =
283
+ state === 'changed'
284
+ ? (['is-changed', GLYPHS.warn, 'changed'] as const)
285
+ : state === 'read' && at
286
+ ? (['is-read', GLYPHS.eye, `read ${age(at)}`] as const)
287
+ : (['is-not-read', GLYPHS.eyeOff, 'not read yet'] as const);
288
+ return (
289
+ <span class={`context-tag ${tone}`}>
290
+ <svg viewBox="0 0 24 24" width="11" height="11" aria-hidden="true">
291
+ <path d={glyph} />
292
+ </svg>
293
+ {word}
294
+ </span>
295
+ );
296
+ }