pmx-canvas 0.7.2 → 0.8.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 (251) hide show
  1. package/.github/extensions/pmx-canvas/THIRD_PARTY_LICENSES.txt +331 -0
  2. package/.github/extensions/pmx-canvas/extension.mjs +25 -172
  3. package/.github/extensions/pmx-canvas/mcp-bootstrap.ts +198 -0
  4. package/.github/extensions/pmx-canvas/runtime.d.mts +41 -0
  5. package/.github/extensions/pmx-canvas/runtime.json +3 -0
  6. package/.github/extensions/pmx-canvas/runtime.mjs +194 -0
  7. package/.github/extensions/pmx-canvas/start-mcp.mjs +51 -0
  8. package/.github/extensions/pmx-canvas/tools.json +2560 -0
  9. package/CHANGELOG.md +60 -0
  10. package/Readme.md +44 -16
  11. package/dist/canvas/fonts/OFL.txt +93 -0
  12. package/dist/canvas/fonts/ibm-plex-mono-latin-400-normal.woff2 +0 -0
  13. package/dist/canvas/fonts/ibm-plex-mono-latin-500-normal.woff2 +0 -0
  14. package/dist/canvas/fonts/ibm-plex-mono-latin-ext-400-normal.woff2 +0 -0
  15. package/dist/canvas/fonts/ibm-plex-mono-latin-ext-500-normal.woff2 +0 -0
  16. package/dist/canvas/fonts/ibm-plex-sans-latin-400-normal.woff2 +0 -0
  17. package/dist/canvas/fonts/ibm-plex-sans-latin-500-normal.woff2 +0 -0
  18. package/dist/canvas/fonts/ibm-plex-sans-latin-600-normal.woff2 +0 -0
  19. package/dist/canvas/fonts/ibm-plex-sans-latin-ext-400-normal.woff2 +0 -0
  20. package/dist/canvas/fonts/ibm-plex-sans-latin-ext-500-normal.woff2 +0 -0
  21. package/dist/canvas/fonts/ibm-plex-sans-latin-ext-600-normal.woff2 +0 -0
  22. package/dist/canvas/global.css +844 -710
  23. package/dist/canvas/index.js +92 -90
  24. package/dist/canvas/mcp-app.html +7878 -0
  25. package/dist/canvas/mermaid-entry.js +1 -1
  26. package/dist/canvas/surface-theme.css +151 -46
  27. package/dist/json-render/index.css +1 -1
  28. package/dist/json-render/index.js +55 -55
  29. package/dist/types/cli/commands/record.d.ts +1 -1
  30. package/dist/types/client/canvas/ChoiceMenu.d.ts +13 -0
  31. package/dist/types/client/canvas/NodeContextMark.d.ts +24 -0
  32. package/dist/types/client/canvas/SessionReceipt.d.ts +3 -1
  33. package/dist/types/client/icons.d.ts +1 -2
  34. package/dist/types/client/mcp-app/index.d.ts +1 -0
  35. package/dist/types/client/nodes/McpAppNode.d.ts +2 -1
  36. package/dist/types/client/nodes/ax-node-actions.d.ts +2 -2
  37. package/dist/types/client/nodes/markdown-sanitize.d.ts +2 -0
  38. package/dist/types/client/nodes/use-workbench-asset.d.ts +2 -0
  39. package/dist/types/client/nodes/viewer-runtime-state.d.ts +1 -0
  40. package/dist/types/client/state/attention-store.d.ts +0 -5
  41. package/dist/types/client/state/canvas-store.d.ts +2 -0
  42. package/dist/types/client/state/context-status-store.d.ts +53 -0
  43. package/dist/types/client/state/intent-bridge.d.ts +4 -1
  44. package/dist/types/client/state/near-pin-store.d.ts +16 -0
  45. package/dist/types/client/state/presence-store.d.ts +5 -0
  46. package/dist/types/client/state/session-store.d.ts +33 -3
  47. package/dist/types/client/state/workbench-transport.d.ts +16 -0
  48. package/dist/types/client/theme/tokens.d.ts +1 -3
  49. package/dist/types/client/types.d.ts +9 -0
  50. package/dist/types/json-render/renderer/index.d.ts +2 -1
  51. package/dist/types/json-render/server.d.ts +3 -1
  52. package/dist/types/mcp/app-server.d.ts +3 -0
  53. package/dist/types/mcp/canvas-access.d.ts +1 -1
  54. package/dist/types/mcp/canvas-app.d.ts +4 -0
  55. package/dist/types/mcp/workbench-app.d.ts +4 -0
  56. package/dist/types/server/agent-presence.d.ts +1 -1
  57. package/dist/types/server/ax-state.d.ts +1 -1
  58. package/dist/types/server/canvas-db.d.ts +8 -0
  59. package/dist/types/server/canvas-geometry.d.ts +25 -0
  60. package/dist/types/server/canvas-operations.d.ts +1 -11
  61. package/dist/types/server/canvas-state.d.ts +25 -3
  62. package/dist/types/server/context-brief.d.ts +8 -1
  63. package/dist/types/server/context-reads.d.ts +30 -7
  64. package/dist/types/server/index.d.ts +6 -1
  65. package/dist/types/server/mcp-app-runtime.d.ts +1 -0
  66. package/dist/types/server/operations/http.d.ts +2 -0
  67. package/dist/types/server/operations/invoker.d.ts +4 -2
  68. package/dist/types/server/operations/ops/nodes.d.ts +0 -6
  69. package/dist/types/server/server.d.ts +1 -0
  70. package/dist/types/server/spatial-analysis.d.ts +2 -0
  71. package/dist/types/shared/agent-context.d.ts +22 -0
  72. package/dist/types/shared/agent-presence.d.ts +3 -10
  73. package/dist/types/shared/ax-surface-protocol.d.ts +2 -0
  74. package/dist/types/shared/canvas-app.d.ts +54 -0
  75. package/dist/types/shared/themes.d.ts +1 -1
  76. package/docs/RELEASE.md +7 -3
  77. package/docs/agent-plugin.md +123 -0
  78. package/docs/ax-host-adapter-contract.md +13 -2
  79. package/docs/ax-state-contract.md +40 -0
  80. package/docs/cli.md +7 -2
  81. package/docs/design/AgentContext.dc.html +186 -0
  82. package/docs/design/Before.dc.html +208 -0
  83. package/docs/design/BoardPins.dc.html +151 -0
  84. package/docs/design/Chrome.dc.html +208 -0
  85. package/docs/design/Context.dc.html +239 -0
  86. package/docs/design/Flows.dc.html +181 -0
  87. package/docs/design/Home.dc.html +209 -0
  88. package/docs/design/HomeGraph.dc.html +211 -0
  89. package/docs/design/HomeMap.dc.html +117 -0
  90. package/docs/design/HomeTeam.dc.html +176 -0
  91. package/docs/design/Light.dc.html +28 -0
  92. package/docs/design/Main.dc.html +280 -0
  93. package/docs/design/NearPin.dc.html +260 -0
  94. package/docs/design/Pane600.dc.html +120 -0
  95. package/docs/design/Relations.dc.html +297 -0
  96. package/docs/design/Themes.dc.html +121 -0
  97. package/docs/design/TokenSystem.dc.html +136 -0
  98. package/docs/design/Tokens.dc.html +163 -0
  99. package/docs/design/Types1.dc.html +177 -0
  100. package/docs/design/Types2.dc.html +226 -0
  101. package/docs/design/Types3.dc.html +195 -0
  102. package/docs/design/Uses.dc.html +152 -0
  103. package/docs/design/Vision.dc.html +116 -0
  104. package/docs/design/Zoom.dc.html +82 -0
  105. package/docs/design/canvas.json +274 -0
  106. package/docs/design.md +214 -0
  107. package/docs/environment.md +1 -1
  108. package/docs/http-api.md +80 -15
  109. package/docs/mcp.md +198 -3
  110. package/docs/node-types.md +34 -10
  111. package/docs/plans/plan-009-tech-debt-backlog.md +1 -1
  112. package/docs/plans/plan-015-design-foundation-wave-0.md +177 -0
  113. package/docs/plans/plan-016-context-made-visible-wave-1.md +148 -0
  114. package/docs/product-vision-2026-09.md +47 -7
  115. package/docs/screenshot.png +0 -0
  116. package/docs/sdk.md +1 -0
  117. package/package.json +10 -4
  118. package/plugins/pmx-canvas/LICENSE +21 -0
  119. package/plugins/pmx-canvas/THIRD_PARTY_LICENSES.txt +331 -0
  120. package/plugins/pmx-canvas/assets/preview.png +0 -0
  121. package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/extension.mjs +930 -0
  122. package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/runtime.json +3 -0
  123. package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/runtime.mjs +194 -0
  124. package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/steering-delivery.mjs +79 -0
  125. package/plugins/pmx-canvas/mcp.json +12 -0
  126. package/plugins/pmx-canvas/plugin.json +24 -0
  127. package/plugins/pmx-canvas/runtime.json +3 -0
  128. package/plugins/pmx-canvas/runtime.mjs +194 -0
  129. package/plugins/pmx-canvas/start-mcp.mjs +51 -0
  130. package/plugins/pmx-canvas/tools.json +2560 -0
  131. package/skills/pmx-canvas/SKILL.md +31 -16
  132. package/skills/pmx-canvas/evals/evals.json +69 -0
  133. package/skills/pmx-canvas/references/full-reference.md +14 -10
  134. package/skills/pmx-canvas/references/github-copilot-app-adapter.md +20 -8
  135. package/skills/pmx-canvas/references/installing-pmx-canvas.md +76 -4
  136. package/skills/pmx-canvas-testing/SKILL.md +54 -4
  137. package/src/cli/commands/copilot.ts +5 -0
  138. package/src/cli/commands/view.ts +1 -1
  139. package/src/cli/index.ts +8 -3
  140. package/src/client/App.tsx +6 -1
  141. package/src/client/canvas/AgentPresenceLayer.tsx +3 -3
  142. package/src/client/canvas/AnnotationLayer.tsx +1 -1
  143. package/src/client/canvas/CanvasNode.tsx +208 -220
  144. package/src/client/canvas/CanvasViewport.tsx +6 -2
  145. package/src/client/canvas/ChoiceMenu.tsx +122 -0
  146. package/src/client/canvas/CommandBar.tsx +25 -23
  147. package/src/client/canvas/CommandPalette.tsx +3 -2
  148. package/src/client/canvas/ContextMenu.tsx +11 -0
  149. package/src/client/canvas/EdgeLayer.tsx +1 -1
  150. package/src/client/canvas/ExportDialog.tsx +33 -6
  151. package/src/client/canvas/FeedbackDialog.tsx +10 -8
  152. package/src/client/canvas/IntentLayer.tsx +2 -2
  153. package/src/client/canvas/Minimap.tsx +12 -7
  154. package/src/client/canvas/NodeContextMark.tsx +156 -0
  155. package/src/client/canvas/SessionPanel.tsx +4 -7
  156. package/src/client/canvas/SessionReceipt.tsx +114 -17
  157. package/src/client/canvas/ShortcutOverlay.tsx +6 -1
  158. package/src/client/canvas/SnapshotPanel.tsx +11 -2
  159. package/src/client/canvas/ToolRail.tsx +58 -108
  160. package/src/client/canvas/TopBar.tsx +16 -11
  161. package/src/client/icons.tsx +2 -12
  162. package/src/client/mcp-app/index.tsx +137 -0
  163. package/src/client/mcp-app/style.css +10 -0
  164. package/src/client/nodes/AttachmentNode.tsx +8 -1
  165. package/src/client/nodes/AxStepControls.tsx +1 -1
  166. package/src/client/nodes/ContextNode.tsx +9 -9
  167. package/src/client/nodes/ExtAppFrame.tsx +13 -8
  168. package/src/client/nodes/FileNode.tsx +8 -5
  169. package/src/client/nodes/ImageNode.tsx +4 -2
  170. package/src/client/nodes/InlineMarkdownEditor.tsx +2 -1
  171. package/src/client/nodes/LedgerNode.tsx +2 -2
  172. package/src/client/nodes/MarkdownNode.tsx +44 -9
  173. package/src/client/nodes/McpAppNode.tsx +22 -4
  174. package/src/client/nodes/PromptNode.tsx +5 -17
  175. package/src/client/nodes/ResponseNode.tsx +3 -15
  176. package/src/client/nodes/StatusNode.tsx +2 -2
  177. package/src/client/nodes/StatusSummary.tsx +3 -1
  178. package/src/client/nodes/TraceNode.tsx +6 -4
  179. package/src/client/nodes/WebpageNode.tsx +1 -1
  180. package/src/client/nodes/ax-node-actions.ts +2 -2
  181. package/src/client/nodes/iframe-document-url.ts +3 -2
  182. package/src/client/nodes/markdown-sanitize.ts +10 -0
  183. package/src/client/nodes/surface-url.ts +10 -0
  184. package/src/client/nodes/use-ax-surface-bridge.ts +4 -2
  185. package/src/client/nodes/use-surface-frame.ts +3 -1
  186. package/src/client/nodes/use-workbench-asset.ts +26 -0
  187. package/src/client/nodes/viewer-runtime-state.ts +78 -0
  188. package/src/client/state/attention-bridge.ts +0 -8
  189. package/src/client/state/attention-store.ts +0 -17
  190. package/src/client/state/canvas-store.ts +7 -2
  191. package/src/client/state/context-status-store.ts +66 -0
  192. package/src/client/state/human-store.ts +2 -0
  193. package/src/client/state/iframe-mode.ts +5 -0
  194. package/src/client/state/intent-bridge.ts +24 -6
  195. package/src/client/state/near-pin-store.ts +53 -0
  196. package/src/client/state/presence-store.ts +5 -10
  197. package/src/client/state/session-store.ts +106 -9
  198. package/src/client/state/sse-bridge.ts +26 -7
  199. package/src/client/state/theme-override.ts +1 -1
  200. package/src/client/state/workbench-transport.ts +36 -0
  201. package/src/client/theme/global.css +844 -710
  202. package/src/client/theme/surface-theme.css +151 -46
  203. package/src/client/theme/tokens.ts +7 -11
  204. package/src/client/types.ts +10 -0
  205. package/src/json-render/renderer/index.tsx +58 -38
  206. package/src/json-render/server.ts +79 -15
  207. package/src/mcp/app-server.ts +64 -0
  208. package/src/mcp/canvas-access.ts +14 -11
  209. package/src/mcp/canvas-app.ts +318 -0
  210. package/src/mcp/server.ts +1 -1
  211. package/src/mcp/workbench-app.ts +211 -0
  212. package/src/server/agent-presence.ts +45 -6
  213. package/src/server/ax-context.ts +1 -1
  214. package/src/server/ax-state.ts +1 -1
  215. package/src/server/board-export.ts +5 -2
  216. package/src/server/canvas-db.ts +46 -12
  217. package/src/server/canvas-geometry.ts +64 -0
  218. package/src/server/canvas-operations.ts +6 -24
  219. package/src/server/canvas-state.ts +105 -15
  220. package/src/server/canvas-validation.ts +19 -4
  221. package/src/server/context-brief.ts +39 -2
  222. package/src/server/context-reads.ts +86 -15
  223. package/src/server/demo-state.json +31 -31
  224. package/src/server/html-surface.ts +4 -5
  225. package/src/server/index.ts +19 -3
  226. package/src/server/mcp-app-runtime.ts +11 -0
  227. package/src/server/operations/composites.ts +4 -3
  228. package/src/server/operations/http.ts +4 -4
  229. package/src/server/operations/invoker.ts +13 -4
  230. package/src/server/operations/ops/ax-read.ts +1 -1
  231. package/src/server/operations/ops/ax-timeline.ts +37 -0
  232. package/src/server/operations/ops/boards.ts +1 -1
  233. package/src/server/operations/ops/canvas-wire.ts +3 -6
  234. package/src/server/operations/ops/intent.ts +31 -2
  235. package/src/server/operations/ops/json-render.ts +1 -1
  236. package/src/server/operations/ops/nodes.ts +8 -76
  237. package/src/server/operations/ops/query.ts +8 -2
  238. package/src/server/operations/ops/snapshots.ts +4 -1
  239. package/src/server/operations/ops/viewport.ts +29 -5
  240. package/src/server/operations/registry.ts +38 -4
  241. package/src/server/scope-fence.ts +55 -10
  242. package/src/server/server.ts +86 -6
  243. package/src/server/spatial-analysis.ts +2 -2
  244. package/src/server/workboard.ts +6 -1
  245. package/src/{server → shared}/agent-context.ts +2 -2
  246. package/src/shared/agent-presence.ts +3 -15
  247. package/src/shared/ax-surface-protocol.ts +3 -0
  248. package/src/shared/canvas-app.ts +62 -0
  249. package/src/shared/themes.ts +5 -5
  250. package/src/client/canvas/FocusFieldLayer.tsx +0 -116
  251. package/src/client/canvas/kind-colors.ts +0 -23
@@ -1,7 +1,7 @@
1
1
  import { useRef } from 'preact/hooks';
2
- import { agentIdentityHue, agentPhaseLabel, type AgentPresence } from '../../shared/agent-presence.js';
2
+ import { agentPhaseLabel, type AgentPresence } from '../../shared/agent-presence.js';
3
3
  import { nodes, viewport } from '../state/canvas-store';
4
- import { agentPresences, presenceWorldPosition } from '../state/presence-store';
4
+ import { agentPresences, presenceWorldPosition, writerColor } from '../state/presence-store';
5
5
 
6
6
  /**
7
7
  * Agent presence layer (rail-chrome-v2 phase 3): one cursor + phase chip per
@@ -49,7 +49,7 @@ export function AgentPresenceLayer() {
49
49
  data-session-id={presence.sessionId}
50
50
  style={{
51
51
  transform: `translate(${position.x}px, ${position.y}px)`,
52
- '--identity-color': `hsl(${agentIdentityHue(presence.sessionId)} 65% 62%)`,
52
+ '--identity-color': writerColor(presence.sessionId),
53
53
  }}
54
54
  >
55
55
  <div class="agent-cursor-inner" style={{ transform: `scale(${counterScale})` }}>
@@ -23,7 +23,7 @@ export function AnnotationLayer({ annotations }: { annotations: CanvasAnnotation
23
23
  y={point.y}
24
24
  fill={color}
25
25
  font-size={annotation.width}
26
- font-family="var(--font)"
26
+ font-family="var(--font-ui)"
27
27
  font-weight="700"
28
28
  opacity="0.95"
29
29
  >
@@ -1,6 +1,8 @@
1
1
  import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
2
2
  import { mutatingNodeIds, sessionActive } from '../state/presence-store';
3
- import { attentionPrimaryNodeIds, attentionPulseNodeIds, attentionSecondaryNodeIds } from '../state/attention-store';
3
+ import { attentionPulseNodeIds } from '../state/attention-store';
4
+ import { isAgentPin, NearPinMark, NodeContextMark } from './NodeContextMark';
5
+ import { activityLensNodeIds } from '../state/session-store';
4
6
  import {
5
7
  activeNodeId,
6
8
  activeNeighborNodeIds,
@@ -34,20 +36,9 @@ import {
34
36
  ungroupFromClient,
35
37
  updateNodeFromClient,
36
38
  } from '../state/intent-bridge';
37
- import { KIND_COLOR } from './kind-colors';
38
39
  import { reportHumanGrab, takeOverNode, yieldedNodes } from '../state/human-store';
39
40
  import { AxStepControls } from '../nodes/AxStepControls';
40
- import { canOpenAsSite, openNodeAsSite } from '../nodes/surface-url';
41
- import {
42
- getNodeIcon,
43
- IconChevronRight,
44
- IconChevronDown,
45
- IconClose,
46
- IconMore,
47
- IconExternalLink,
48
- IconExpand,
49
- IconSparkle,
50
- } from '../icons';
41
+ import { getNodeIcon, IconChevronRight, IconClose, IconMore, IconExpand, IconPin } from '../icons';
51
42
  import { EXPANDABLE_TYPES, TYPE_LABELS } from '../types';
52
43
  import type { CanvasNodeState } from '../types';
53
44
  import { AUTO_FIT_TITLEBAR_HEIGHT, computeAutoFitHeight, shouldAutoFitNode } from './auto-fit';
@@ -137,8 +128,6 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
137
128
  const isActive = activeNodeId.value === node.id;
138
129
  const isSelected = selectedNodeIds.value.has(node.id);
139
130
  const isContextPinned = contextPinnedNodeIds.value.has(node.id);
140
- const isAttentionPrimary = attentionPrimaryNodeIds.value.has(node.id);
141
- const isAttentionSecondary = !isAttentionPrimary && attentionSecondaryNodeIds.value.has(node.id);
142
131
  const isAttentionPulse = attentionPulseNodeIds.value.has(node.id);
143
132
  const isNeighbor = !isActive && activeNeighborNodeIds.value.has(node.id);
144
133
  const searchSet = searchHighlightIds.value;
@@ -414,6 +403,7 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
414
403
  height: node.collapsed ? 'auto' : `${node.size.height}px`,
415
404
  zIndex: node.zIndex,
416
405
  '--node-chrome-scale': chromeScale.toFixed(3),
406
+ '--node-port-scale': (1 / viewportScale).toFixed(3),
417
407
  ...(groupColor ? { '--group-color': groupColor } : {}),
418
408
  };
419
409
  // Shimmer while an attached agent is mutating this node (rail-chrome-v2
@@ -427,8 +417,6 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
427
417
  isSearchDimmed ? 'search-dimmed' : '',
428
418
  isSelected ? 'selected' : '',
429
419
  isContextPinned ? 'context-pinned' : '',
430
- isAttentionPrimary ? 'attention-focus-primary' : '',
431
- isAttentionSecondary ? 'attention-focus-secondary' : '',
432
420
  isAttentionPulse ? 'attention-pulse' : '',
433
421
  isPinned ? 'pinned' : '',
434
422
  isTrace ? 'trace-node' : '',
@@ -436,6 +424,7 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
436
424
  isGroup ? 'group-node' : '',
437
425
  isStrictSize ? 'strict-size' : '',
438
426
  isAgentMutating ? 'agent-mutating' : '',
427
+ activityLensNodeIds.value && !activityLensNodeIds.value.has(node.id) ? 'lens-dimmed' : '',
439
428
  ]
440
429
  .filter(Boolean)
441
430
  .join(' ');
@@ -487,7 +476,7 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
487
476
  <div class="group-chip-name">{title}</div>
488
477
  <div class="group-chip-meta">
489
478
  {groupKindDots.map((kind) => (
490
- <span key={kind} class="group-kind-dot" style={{ background: KIND_COLOR[kind] }} aria-hidden="true" />
479
+ <span key={kind} class="group-kind-dot" aria-hidden="true" />
491
480
  ))}
492
481
  <span class="group-chip-count">
493
482
  {groupChildren.length} node{groupChildren.length === 1 ? '' : 's'}
@@ -515,13 +504,156 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
515
504
  }}
516
505
  onDblClick={handleDblClick}
517
506
  >
518
- {isGroup && (
519
- <div class="node-titlebar group-edge-row" onPointerDown={handleTitlePointerDown}>
520
- <span class="group-name-pill">
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());
605
+ }}
606
+ >
607
+ Rename
608
+ </button>
609
+ <button
610
+ type="button"
611
+ role="menuitem"
612
+ onClick={(e) => {
613
+ e.stopPropagation();
614
+ setGroupMenuOpen(false);
615
+ // Dissolve — the same op an agent's ungroup runs.
616
+ removeNode(node.id);
617
+ void ungroupFromClient(node.id);
618
+ }}
619
+ >
620
+ Ungroup
621
+ </button>
622
+ <button
623
+ type="button"
624
+ role="menuitem"
625
+ disabled={groupChildren.length === 0}
626
+ onClick={(e) => {
627
+ e.stopPropagation();
628
+ setGroupMenuOpen(false);
629
+ addContextPins(groupChildren);
630
+ }}
631
+ >
632
+ Pin all to context
633
+ </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
648
+ shell: icon · title · controls, no type badge). Only the AX status
649
+ 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
+ )}
521
653
  {renaming ? (
522
654
  <input
523
655
  ref={renameRef}
524
- class="node-title-input group-title-input"
656
+ class="node-title-input"
525
657
  value={title}
526
658
  onBlur={(e) => commitRename((e.target as HTMLInputElement).value)}
527
659
  onKeyDown={(e) => {
@@ -531,234 +663,88 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
531
663
  onClick={(e) => e.stopPropagation()}
532
664
  />
533
665
  ) : (
534
- <span class="group-name" title={title}>
666
+ <span class="node-title" title={title}>
535
667
  {title}
536
668
  </span>
537
669
  )}
538
- <span class="group-count">{groupChildren.length}</span>
539
- </span>
540
- <span class="group-edge-spacer" />
541
- <span class="group-actions" role="toolbar" aria-label={`${title} actions`}>
542
- <button
543
- type="button"
544
- class="group-action"
545
- title="Auto-arrange children"
546
- aria-label="Auto-arrange children"
547
- disabled={groupChildren.length === 0}
548
- onClick={(e) => {
549
- e.stopPropagation();
550
- void addToGroupFromClient(node.id, groupChildren, 'grid');
551
- }}
552
- >
553
- <svg
554
- width="11"
555
- height="11"
556
- viewBox="0 0 16 16"
557
- fill="none"
558
- stroke="currentColor"
559
- stroke-width="1.5"
560
- aria-hidden="true"
561
- >
562
- <rect x="1.5" y="1.5" width="5" height="5" rx="1" />
563
- <rect x="9.5" y="1.5" width="5" height="5" rx="1" />
564
- <rect x="1.5" y="9.5" width="5" height="5" rx="1" />
565
- <rect x="9.5" y="9.5" width="5" height="5" rx="1" />
566
- </svg>
567
- </button>
568
- <button
569
- type="button"
570
- class="group-action"
571
- title="Collapse group"
572
- aria-label="Collapse group"
573
- onClick={(e) => {
574
- e.stopPropagation();
575
- toggleCollapsed(node.id);
576
- }}
577
- >
578
- <svg
579
- width="11"
580
- height="11"
581
- viewBox="0 0 16 16"
582
- fill="none"
583
- stroke="currentColor"
584
- stroke-width="1.8"
585
- stroke-linecap="round"
586
- stroke-linejoin="round"
587
- aria-hidden="true"
588
- >
589
- <path d="M3 10 L8 5 L13 10" />
590
- </svg>
591
- </button>
592
- <button
593
- type="button"
594
- class="group-action"
595
- title="Rename · Ungroup · Pin all to context"
596
- aria-label="Group menu"
597
- aria-expanded={groupMenuOpen}
598
- onClick={(e) => {
599
- e.stopPropagation();
600
- setGroupMenuOpen((open) => !open);
601
- }}
602
- >
603
- <IconMore size={14} />
604
- </button>
605
- {groupMenuOpen && (
606
- <div class="group-menu" role="menu" onPointerDown={(e) => e.stopPropagation()}>
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
+ ⊙
676
+ </span>
677
+ )}
678
+ {/* Expand and the context pin always show; the rest sit behind ⋯ and ×
679
+ on hover (docs/design/Chrome.dc.html). */}
680
+ {EXPANDABLE_TYPES.has(node.type) && (
607
681
  <button
608
682
  type="button"
609
- role="menuitem"
610
683
  onClick={(e) => {
611
684
  e.stopPropagation();
612
- setGroupMenuOpen(false);
613
- setRenaming(true);
614
- requestAnimationFrame(() => renameRef.current?.focus());
685
+ expandNode(node.id);
615
686
  }}
687
+ title="Expand (focus mode)"
616
688
  >
617
- Rename
689
+ <IconExpand size={14} />
618
690
  </button>
619
- <button
620
- type="button"
621
- role="menuitem"
622
- onClick={(e) => {
623
- e.stopPropagation();
624
- setGroupMenuOpen(false);
625
- // Dissolve — the same op an agent's ungroup runs.
626
- removeNode(node.id);
627
- void ungroupFromClient(node.id);
628
- }}
629
- >
630
- Ungroup
631
- </button>
632
- <button
633
- type="button"
634
- role="menuitem"
635
- disabled={groupChildren.length === 0}
636
- onClick={(e) => {
637
- e.stopPropagation();
638
- setGroupMenuOpen(false);
639
- addContextPins(groupChildren);
640
- }}
641
- >
642
- Pin all to context
643
- </button>
644
- </div>
645
- )}
646
- </span>
647
- </div>
648
- )}
649
- {!isGroup && (
650
- <div class="node-titlebar" onPointerDown={handleTitlePointerDown}>
651
- <span class="node-type-icon" aria-hidden="true">
652
- {(() => {
653
- const NodeIcon = getNodeIcon(node.type);
654
- return <NodeIcon size={Math.round(14 * chromeScale)} />;
655
- })()}
656
- </span>
657
- {/* The kind-colored icon says what type this is (rail-chrome-v2 card
658
- shell: icon · title · controls, no type badge). Only the AX status
659
- chip joins it, and only when there is a status to show. */}
660
- {typeof node.data.axWorkStatus === 'string' && (
661
- <span class={`node-ax-status node-ax-status-${node.data.axWorkStatus}`}>{node.data.axWorkStatus}</span>
662
- )}
663
- {renaming ? (
664
- <input
665
- ref={renameRef}
666
- class="node-title-input"
667
- value={title}
668
- onBlur={(e) => commitRename((e.target as HTMLInputElement).value)}
669
- onKeyDown={(e) => {
670
- if (e.key === 'Enter') commitRename((e.target as HTMLInputElement).value);
671
- if (e.key === 'Escape') setRenaming(false);
672
- }}
673
- onClick={(e) => e.stopPropagation()}
674
- />
675
- ) : (
676
- <span class="node-title" title={title}>
677
- {title}
678
- </span>
679
- )}
680
- <div class="node-controls">
681
- {isPinned && (
682
- <span class="pin-indicator" title="Pinned">
683
- ⊙
684
- </span>
685
- )}
686
- <button
687
- type="button"
688
- class={`ctx-pin-btn${isContextPinned ? ' ctx-pin-active' : ''}`}
689
- onClick={(e) => {
690
- e.stopPropagation();
691
- toggleContextPin(node.id);
692
- }}
693
- title={isContextPinned ? 'Remove from context' : 'Add to context'}
694
- >
695
- <IconSparkle size={14} />
696
- </button>
697
- {/* Open as site — full-page standalone view of this node's surface,
698
- served from /api/canvas/surface/:id (same document as the canvas
699
- iframe). Opens via the system browser so embedded hosts do not trap
700
- it in their own webview. */}
701
- {canOpenAsSite(node) && (
691
+ )}
702
692
  <button
703
693
  type="button"
694
+ class={`ctx-pin-btn${isContextPinned ? ' ctx-pin-active' : ''}`}
704
695
  onClick={(e) => {
705
696
  e.stopPropagation();
706
- void openNodeAsSite(node);
697
+ toggleContextPin(node.id);
707
698
  }}
708
- title="Open as site"
699
+ title={isContextPinned ? 'Remove from context' : 'Add to context'}
700
+ aria-label={isContextPinned ? 'Remove from context' : 'Add to context'}
701
+ aria-pressed={isContextPinned}
709
702
  >
710
- <IconExternalLink size={14} />
703
+ <span class="ctx-pin-mark">
704
+ <IconPin />
705
+ {isContextPinned && isAgentPin(node.id) && <span class="ctx-pin-agent-dot" aria-hidden="true" />}
706
+ </span>
711
707
  </button>
712
- )}
713
- {/* Expand — opens node as full-viewport overlay for focused work */}
714
- {EXPANDABLE_TYPES.has(node.type) && (
715
708
  <button
716
709
  type="button"
710
+ class="node-hover-control"
717
711
  onClick={(e) => {
718
712
  e.stopPropagation();
719
- expandNode(node.id);
713
+ onContextMenu?.(e, node.id);
720
714
  }}
721
- title="Expand (focus mode)"
715
+ title="More actions"
716
+ aria-label="More actions"
722
717
  >
723
- <IconExpand size={14} />
718
+ <IconMore size={14} />
724
719
  </button>
725
- )}
726
- <button
727
- type="button"
728
- onClick={(e) => {
729
- e.stopPropagation();
730
- toggleCollapsed(node.id);
731
- }}
732
- title={node.collapsed ? 'Expand' : 'Collapse'}
733
- >
734
- {node.collapsed ? <IconChevronRight size={14} /> : <IconChevronDown size={14} />}
735
- </button>
736
- {/* Report #64: status nodes get the same remove control as every other
720
+ {/* Report #64: status nodes get the same remove control as every other
737
721
  node type (backend removal + undo/history handle status uniformly). */}
738
- <button
739
- type="button"
740
- onClick={(e) => {
741
- e.stopPropagation();
742
- void removeNodeFromClient(node.id);
743
- }}
744
- title="Close"
745
- >
746
- <IconClose size={14} />
747
- </button>
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>
733
+ </div>
748
734
  </div>
749
- </div>
750
- )}
751
- {!node.collapsed && (
752
- <div ref={bodyRef} class="node-body">
753
- {children}
754
- {/* AX flow step controls — rendered here rather than in a node renderer
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
755
740
  because a materialized step can be ANY node type, and because the
756
741
  body is what auto-fit measures (a footer outside it would be clipped
757
742
  on a content-fitted node). Renders nothing unless the node carries a
758
743
  `data.axStep` stamp. */}
759
- <AxStepControls node={node} />
760
- </div>
761
- )}
744
+ <AxStepControls node={node} />
745
+ </div>
746
+ )}
747
+ </div>
762
748
  {!node.collapsed && (
763
749
  <div class="node-resize-handle" onPointerDown={(e) => startResize(e, node.size.width, node.size.height)} />
764
750
  )}
@@ -781,8 +767,10 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
781
767
  key={side}
782
768
  class={`node-port node-port-${side}`}
783
769
  onPointerDown={(e) => {
770
+ if (e.button !== 0) return;
784
771
  e.stopPropagation();
785
772
  e.preventDefault();
773
+ e.currentTarget.setPointerCapture(e.pointerId);
786
774
  const cx = node.position.x + node.size.width / 2;
787
775
  const cy = node.position.y + node.size.height / 2;
788
776
  const hw = node.size.width / 2;
@@ -38,7 +38,6 @@ import {
38
38
  } from '../state/canvas-store';
39
39
  import { createEdgeFromClient, createNodeFromClient } from '../state/intent-bridge';
40
40
  import type { AnnotationTool, CanvasAnnotation, CanvasNodeState } from '../types';
41
- import { FocusFieldLayer } from './FocusFieldLayer';
42
41
  import { importFiles } from './import-files';
43
42
  import { IntentLayer } from './IntentLayer';
44
43
  import { AgentPresenceLayer } from './AgentPresenceLayer';
@@ -652,12 +651,18 @@ export function CanvasViewport({
652
651
  }
653
652
  }
654
653
 
654
+ function handleCancel() {
655
+ draggingEdge.value = null;
656
+ }
657
+
655
658
  document.addEventListener('pointermove', handleMove);
656
659
  document.addEventListener('pointerup', handleUp);
660
+ document.addEventListener('pointercancel', handleCancel);
657
661
  document.addEventListener('keydown', handleKey);
658
662
  return () => {
659
663
  document.removeEventListener('pointermove', handleMove);
660
664
  document.removeEventListener('pointerup', handleUp);
665
+ document.removeEventListener('pointercancel', handleCancel);
661
666
  document.removeEventListener('keydown', handleKey);
662
667
  };
663
668
  }, [containerRef]);
@@ -849,7 +854,6 @@ export function CanvasViewport({
849
854
  left: 0,
850
855
  }}
851
856
  >
852
- <FocusFieldLayer />
853
857
  <ScopeFenceLayer />
854
858
  <IntentLayer />
855
859
  <AgentPresenceLayer />