pmx-canvas 0.7.3 → 0.8.2

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 (244) 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 +64 -0
  10. package/Readme.md +21 -7
  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 +726 -674
  23. package/dist/canvas/index.js +90 -88
  24. package/dist/canvas/mcp-app.html +7879 -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 +81 -1
  28. package/dist/json-render/index.js +1 -1
  29. package/dist/types/cli/commands/record.d.ts +1 -1
  30. package/dist/types/client/canvas/NodeContextMark.d.ts +24 -0
  31. package/dist/types/client/canvas/SessionReceipt.d.ts +3 -1
  32. package/dist/types/client/icons.d.ts +1 -2
  33. package/dist/types/client/mcp-app/index.d.ts +1 -0
  34. package/dist/types/client/nodes/HtmlNode.d.ts +1 -0
  35. package/dist/types/client/nodes/ax-node-actions.d.ts +2 -2
  36. package/dist/types/client/nodes/markdown-sanitize.d.ts +2 -0
  37. package/dist/types/client/nodes/use-workbench-asset.d.ts +2 -0
  38. package/dist/types/client/state/attention-store.d.ts +0 -5
  39. package/dist/types/client/state/context-status-store.d.ts +53 -0
  40. package/dist/types/client/state/intent-bridge.d.ts +3 -0
  41. package/dist/types/client/state/near-pin-store.d.ts +16 -0
  42. package/dist/types/client/state/presence-store.d.ts +5 -0
  43. package/dist/types/client/state/session-store.d.ts +33 -3
  44. package/dist/types/client/state/workbench-transport.d.ts +16 -0
  45. package/dist/types/client/theme/tokens.d.ts +1 -3
  46. package/dist/types/client/types.d.ts +9 -0
  47. package/dist/types/json-render/server.d.ts +1 -1
  48. package/dist/types/mcp/app-server.d.ts +3 -0
  49. package/dist/types/mcp/canvas-access.d.ts +1 -1
  50. package/dist/types/mcp/canvas-app.d.ts +4 -0
  51. package/dist/types/mcp/workbench-app.d.ts +4 -0
  52. package/dist/types/server/agent-presence.d.ts +1 -1
  53. package/dist/types/server/ax-state.d.ts +1 -1
  54. package/dist/types/server/canvas-db.d.ts +8 -0
  55. package/dist/types/server/canvas-geometry.d.ts +25 -0
  56. package/dist/types/server/canvas-operations.d.ts +1 -11
  57. package/dist/types/server/canvas-state.d.ts +25 -3
  58. package/dist/types/server/context-brief.d.ts +8 -1
  59. package/dist/types/server/context-reads.d.ts +30 -7
  60. package/dist/types/server/index.d.ts +6 -1
  61. package/dist/types/server/mcp-app-runtime.d.ts +1 -0
  62. package/dist/types/server/operations/http.d.ts +2 -0
  63. package/dist/types/server/operations/invoker.d.ts +4 -2
  64. package/dist/types/server/operations/ops/nodes.d.ts +0 -6
  65. package/dist/types/server/server.d.ts +1 -0
  66. package/dist/types/server/spatial-analysis.d.ts +2 -0
  67. package/dist/types/shared/agent-context.d.ts +22 -0
  68. package/dist/types/shared/agent-presence.d.ts +3 -10
  69. package/dist/types/shared/canvas-app.d.ts +54 -0
  70. package/dist/types/shared/themes.d.ts +1 -1
  71. package/docs/RELEASE.md +7 -3
  72. package/docs/agent-plugin.md +123 -0
  73. package/docs/ax-state-contract.md +40 -0
  74. package/docs/cli.md +7 -2
  75. package/docs/design/AgentContext.dc.html +186 -0
  76. package/docs/design/Before.dc.html +208 -0
  77. package/docs/design/BoardPins.dc.html +151 -0
  78. package/docs/design/Chrome.dc.html +211 -0
  79. package/docs/design/Context.dc.html +239 -0
  80. package/docs/design/Flows.dc.html +181 -0
  81. package/docs/design/Home.dc.html +209 -0
  82. package/docs/design/HomeGraph.dc.html +211 -0
  83. package/docs/design/HomeMap.dc.html +117 -0
  84. package/docs/design/HomeTeam.dc.html +176 -0
  85. package/docs/design/Light.dc.html +28 -0
  86. package/docs/design/Main.dc.html +280 -0
  87. package/docs/design/NearPin.dc.html +260 -0
  88. package/docs/design/Pane600.dc.html +120 -0
  89. package/docs/design/Relations.dc.html +297 -0
  90. package/docs/design/Themes.dc.html +121 -0
  91. package/docs/design/TokenSystem.dc.html +136 -0
  92. package/docs/design/Tokens.dc.html +163 -0
  93. package/docs/design/Types1.dc.html +177 -0
  94. package/docs/design/Types2.dc.html +226 -0
  95. package/docs/design/Types3.dc.html +195 -0
  96. package/docs/design/Uses.dc.html +152 -0
  97. package/docs/design/Vision.dc.html +116 -0
  98. package/docs/design/Zoom.dc.html +82 -0
  99. package/docs/design/canvas.json +274 -0
  100. package/docs/design.md +217 -0
  101. package/docs/environment.md +1 -1
  102. package/docs/http-api.md +86 -16
  103. package/docs/mcp.md +202 -4
  104. package/docs/node-types.md +34 -10
  105. package/docs/plans/plan-009-tech-debt-backlog.md +1 -1
  106. package/docs/plans/plan-015-design-foundation-wave-0.md +177 -0
  107. package/docs/plans/plan-016-context-made-visible-wave-1.md +148 -0
  108. package/docs/product-vision-2026-09.md +47 -7
  109. package/docs/screenshot.png +0 -0
  110. package/docs/sdk.md +1 -0
  111. package/package.json +10 -4
  112. package/plugins/pmx-canvas/LICENSE +21 -0
  113. package/plugins/pmx-canvas/THIRD_PARTY_LICENSES.txt +331 -0
  114. package/plugins/pmx-canvas/assets/preview.png +0 -0
  115. package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/extension.mjs +930 -0
  116. package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/runtime.json +3 -0
  117. package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/runtime.mjs +194 -0
  118. package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/steering-delivery.mjs +79 -0
  119. package/plugins/pmx-canvas/mcp.json +12 -0
  120. package/plugins/pmx-canvas/plugin.json +24 -0
  121. package/plugins/pmx-canvas/runtime.json +3 -0
  122. package/plugins/pmx-canvas/runtime.mjs +194 -0
  123. package/plugins/pmx-canvas/start-mcp.mjs +51 -0
  124. package/plugins/pmx-canvas/tools.json +2560 -0
  125. package/skills/pmx-canvas/SKILL.md +29 -9
  126. package/skills/pmx-canvas/evals/evals.json +62 -0
  127. package/skills/pmx-canvas/references/ax-html-control-surface.md +6 -0
  128. package/skills/pmx-canvas/references/full-reference.md +11 -7
  129. package/skills/pmx-canvas/references/github-copilot-app-adapter.md +20 -8
  130. package/skills/pmx-canvas/references/installing-pmx-canvas.md +6 -6
  131. package/skills/pmx-canvas-testing/SKILL.md +5 -4
  132. package/src/cli/commands/copilot.ts +5 -0
  133. package/src/cli/commands/nodes.ts +3 -2
  134. package/src/cli/commands/view.ts +1 -1
  135. package/src/cli/index.ts +10 -4
  136. package/src/client/App.tsx +6 -1
  137. package/src/client/canvas/AgentPresenceLayer.tsx +3 -3
  138. package/src/client/canvas/AnnotationLayer.tsx +1 -1
  139. package/src/client/canvas/CanvasNode.tsx +32 -49
  140. package/src/client/canvas/CanvasViewport.tsx +0 -2
  141. package/src/client/canvas/CommandPalette.tsx +3 -2
  142. package/src/client/canvas/ContextMenu.tsx +11 -0
  143. package/src/client/canvas/EdgeLayer.tsx +1 -1
  144. package/src/client/canvas/ExportDialog.tsx +33 -6
  145. package/src/client/canvas/IntentLayer.tsx +2 -2
  146. package/src/client/canvas/Minimap.tsx +12 -7
  147. package/src/client/canvas/NodeContextMark.tsx +156 -0
  148. package/src/client/canvas/SessionPanel.tsx +4 -7
  149. package/src/client/canvas/SessionReceipt.tsx +114 -17
  150. package/src/client/canvas/ShortcutOverlay.tsx +6 -1
  151. package/src/client/canvas/SnapshotPanel.tsx +11 -2
  152. package/src/client/canvas/ToolRail.tsx +12 -9
  153. package/src/client/canvas/TopBar.tsx +16 -11
  154. package/src/client/icons.tsx +2 -12
  155. package/src/client/mcp-app/index.tsx +137 -0
  156. package/src/client/mcp-app/style.css +10 -0
  157. package/src/client/nodes/AttachmentNode.tsx +8 -1
  158. package/src/client/nodes/AxStepControls.tsx +1 -1
  159. package/src/client/nodes/ContextNode.tsx +9 -9
  160. package/src/client/nodes/ExtAppFrame.tsx +13 -8
  161. package/src/client/nodes/FileNode.tsx +8 -5
  162. package/src/client/nodes/HtmlNode.tsx +12 -2
  163. package/src/client/nodes/ImageNode.tsx +4 -2
  164. package/src/client/nodes/InlineMarkdownEditor.tsx +2 -1
  165. package/src/client/nodes/LedgerNode.tsx +2 -2
  166. package/src/client/nodes/MarkdownNode.tsx +44 -9
  167. package/src/client/nodes/McpAppNode.tsx +6 -4
  168. package/src/client/nodes/PromptNode.tsx +5 -17
  169. package/src/client/nodes/ResponseNode.tsx +3 -15
  170. package/src/client/nodes/StatusNode.tsx +2 -2
  171. package/src/client/nodes/StatusSummary.tsx +3 -1
  172. package/src/client/nodes/TraceNode.tsx +6 -4
  173. package/src/client/nodes/WebpageNode.tsx +1 -1
  174. package/src/client/nodes/ax-node-actions.ts +2 -2
  175. package/src/client/nodes/iframe-document-url.ts +3 -2
  176. package/src/client/nodes/markdown-sanitize.ts +10 -0
  177. package/src/client/nodes/surface-url.ts +10 -0
  178. package/src/client/nodes/use-surface-frame.ts +3 -1
  179. package/src/client/nodes/use-workbench-asset.ts +26 -0
  180. package/src/client/state/attention-bridge.ts +0 -8
  181. package/src/client/state/attention-store.ts +0 -17
  182. package/src/client/state/canvas-store.ts +4 -2
  183. package/src/client/state/context-status-store.ts +66 -0
  184. package/src/client/state/human-store.ts +2 -0
  185. package/src/client/state/iframe-mode.ts +5 -0
  186. package/src/client/state/intent-bridge.ts +10 -4
  187. package/src/client/state/near-pin-store.ts +53 -0
  188. package/src/client/state/presence-store.ts +5 -10
  189. package/src/client/state/session-store.ts +106 -9
  190. package/src/client/state/sse-bridge.ts +26 -7
  191. package/src/client/state/theme-override.ts +1 -1
  192. package/src/client/state/workbench-transport.ts +36 -0
  193. package/src/client/theme/global.css +726 -674
  194. package/src/client/theme/surface-theme.css +151 -46
  195. package/src/client/theme/tokens.ts +7 -11
  196. package/src/client/types.ts +10 -0
  197. package/src/json-render/renderer/index.css +6 -1
  198. package/src/json-render/renderer/index.tsx +0 -1
  199. package/src/json-render/server.ts +71 -14
  200. package/src/mcp/app-server.ts +64 -0
  201. package/src/mcp/canvas-access.ts +14 -11
  202. package/src/mcp/canvas-app.ts +318 -0
  203. package/src/mcp/server.ts +1 -1
  204. package/src/mcp/workbench-app.ts +211 -0
  205. package/src/server/agent-presence.ts +45 -6
  206. package/src/server/ax-context.ts +1 -1
  207. package/src/server/ax-state.ts +1 -1
  208. package/src/server/board-export.ts +5 -2
  209. package/src/server/canvas-db.ts +46 -12
  210. package/src/server/canvas-geometry.ts +64 -0
  211. package/src/server/canvas-operations.ts +7 -25
  212. package/src/server/canvas-state.ts +105 -15
  213. package/src/server/canvas-validation.ts +19 -4
  214. package/src/server/context-brief.ts +39 -2
  215. package/src/server/context-reads.ts +87 -15
  216. package/src/server/demo-state.json +31 -31
  217. package/src/server/html-surface.ts +4 -5
  218. package/src/server/index.ts +41 -5
  219. package/src/server/mcp-app-runtime.ts +11 -0
  220. package/src/server/operations/composites.ts +4 -3
  221. package/src/server/operations/http.ts +4 -4
  222. package/src/server/operations/invoker.ts +13 -4
  223. package/src/server/operations/mcp.ts +6 -0
  224. package/src/server/operations/ops/ax-read.ts +1 -1
  225. package/src/server/operations/ops/ax-timeline.ts +37 -0
  226. package/src/server/operations/ops/boards.ts +1 -1
  227. package/src/server/operations/ops/canvas-wire.ts +3 -6
  228. package/src/server/operations/ops/intent.ts +31 -2
  229. package/src/server/operations/ops/json-render.ts +6 -2
  230. package/src/server/operations/ops/nodes.ts +8 -76
  231. package/src/server/operations/ops/query.ts +8 -2
  232. package/src/server/operations/ops/snapshots.ts +4 -1
  233. package/src/server/operations/ops/viewport.ts +29 -5
  234. package/src/server/operations/registry.ts +41 -5
  235. package/src/server/scope-fence.ts +55 -10
  236. package/src/server/server.ts +83 -5
  237. package/src/server/spatial-analysis.ts +2 -2
  238. package/src/server/workboard.ts +6 -1
  239. package/src/{server → shared}/agent-context.ts +2 -2
  240. package/src/shared/agent-presence.ts +3 -15
  241. package/src/shared/canvas-app.ts +62 -0
  242. package/src/shared/themes.ts +5 -5
  243. package/src/client/canvas/FocusFieldLayer.tsx +0 -116
  244. package/src/client/canvas/kind-colors.ts +0 -23
@@ -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;
@@ -428,8 +417,6 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
428
417
  isSearchDimmed ? 'search-dimmed' : '',
429
418
  isSelected ? 'selected' : '',
430
419
  isContextPinned ? 'context-pinned' : '',
431
- isAttentionPrimary ? 'attention-focus-primary' : '',
432
- isAttentionSecondary ? 'attention-focus-secondary' : '',
433
420
  isAttentionPulse ? 'attention-pulse' : '',
434
421
  isPinned ? 'pinned' : '',
435
422
  isTrace ? 'trace-node' : '',
@@ -437,6 +424,7 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
437
424
  isGroup ? 'group-node' : '',
438
425
  isStrictSize ? 'strict-size' : '',
439
426
  isAgentMutating ? 'agent-mutating' : '',
427
+ activityLensNodeIds.value && !activityLensNodeIds.value.has(node.id) ? 'lens-dimmed' : '',
440
428
  ]
441
429
  .filter(Boolean)
442
430
  .join(' ');
@@ -488,7 +476,7 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
488
476
  <div class="group-chip-name">{title}</div>
489
477
  <div class="group-chip-meta">
490
478
  {groupKindDots.map((kind) => (
491
- <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" />
492
480
  ))}
493
481
  <span class="group-chip-count">
494
482
  {groupChildren.length} node{groupChildren.length === 1 ? '' : 's'}
@@ -679,40 +667,16 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
679
667
  {title}
680
668
  </span>
681
669
  )}
670
+ <NearPinMark node={node} pinned={isContextPinned} />
671
+ <NodeContextMark node={node} pinned={isContextPinned} />
682
672
  <div class="node-controls">
683
673
  {isPinned && (
684
674
  <span class="pin-indicator" title="Pinned">
685
675
  ⊙
686
676
  </span>
687
677
  )}
688
- <button
689
- type="button"
690
- class={`ctx-pin-btn${isContextPinned ? ' ctx-pin-active' : ''}`}
691
- onClick={(e) => {
692
- e.stopPropagation();
693
- toggleContextPin(node.id);
694
- }}
695
- title={isContextPinned ? 'Remove from context' : 'Add to context'}
696
- >
697
- <IconSparkle size={14} />
698
- </button>
699
- {/* Open as site — full-page standalone view of this node's surface,
700
- served from /api/canvas/surface/:id (same document as the canvas
701
- iframe). Opens via the system browser so embedded hosts do not trap
702
- it in their own webview. */}
703
- {canOpenAsSite(node) && (
704
- <button
705
- type="button"
706
- onClick={(e) => {
707
- e.stopPropagation();
708
- void openNodeAsSite(node);
709
- }}
710
- title="Open as site"
711
- >
712
- <IconExternalLink size={14} />
713
- </button>
714
- )}
715
- {/* Expand — opens node as full-viewport overlay for focused work */}
678
+ {/* Expand and the context pin always show; the rest sit behind ⋯ and ×
679
+ on hover (docs/design/Chrome.dc.html). */}
716
680
  {EXPANDABLE_TYPES.has(node.type) && (
717
681
  <button
718
682
  type="button"
@@ -727,18 +691,37 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
727
691
  )}
728
692
  <button
729
693
  type="button"
694
+ class={`ctx-pin-btn${isContextPinned ? ' ctx-pin-active' : ''}`}
730
695
  onClick={(e) => {
731
696
  e.stopPropagation();
732
- toggleCollapsed(node.id);
697
+ toggleContextPin(node.id);
698
+ }}
699
+ title={isContextPinned ? 'Remove from context' : 'Add to context'}
700
+ aria-label={isContextPinned ? 'Remove from context' : 'Add to context'}
701
+ aria-pressed={isContextPinned}
702
+ >
703
+ <span class="ctx-pin-mark">
704
+ <IconPin />
705
+ {isContextPinned && isAgentPin(node.id) && <span class="ctx-pin-agent-dot" aria-hidden="true" />}
706
+ </span>
707
+ </button>
708
+ <button
709
+ type="button"
710
+ class="node-hover-control"
711
+ onClick={(e) => {
712
+ e.stopPropagation();
713
+ onContextMenu?.(e, node.id);
733
714
  }}
734
- title={node.collapsed ? 'Expand' : 'Collapse'}
715
+ title="More actions"
716
+ aria-label="More actions"
735
717
  >
736
- {node.collapsed ? <IconChevronRight size={14} /> : <IconChevronDown size={14} />}
718
+ <IconMore size={14} />
737
719
  </button>
738
720
  {/* Report #64: status nodes get the same remove control as every other
739
721
  node type (backend removal + undo/history handle status uniformly). */}
740
722
  <button
741
723
  type="button"
724
+ class="node-hover-control"
742
725
  onClick={(e) => {
743
726
  e.stopPropagation();
744
727
  void removeNodeFromClient(node.id);
@@ -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';
@@ -855,7 +854,6 @@ export function CanvasViewport({
855
854
  left: 0,
856
855
  }}
857
856
  >
858
- <FocusFieldLayer />
859
857
  <ScopeFenceLayer />
860
858
  <IntentLayer />
861
859
  <AgentPresenceLayer />
@@ -16,6 +16,7 @@ import { startSession } from '../state/session-store';
16
16
  import { TYPE_LABELS, type CanvasNodeState } from '../types';
17
17
  import { invalidateTokenCache } from '../theme/tokens';
18
18
  import { clearThemeOverride } from '../state/theme-override';
19
+ import { canvasThemeScheme } from '../../shared/themes.js';
19
20
  import { getNodeIcon, IconArrange, IconFitAll, IconMinimap, IconMoon, IconNodeMarkdown, IconSteer } from '../icons';
20
21
  import { activeBoardId, openBoard } from '../state/boards-store';
21
22
  import { requestJson } from '../state/intent-bridge';
@@ -269,11 +270,11 @@ export function CommandPalette({ onClose, onToggleMinimap }: { onClose: () => vo
269
270
  },
270
271
  },
271
272
  {
272
- label: 'Toggle theme (dark/light)',
273
+ label: 'Toggle theme (Harbor/Daylight)',
273
274
  icon: IconMoon,
274
275
  iconTone: 'muted',
275
276
  action: () => {
276
- const next = canvasTheme.value === 'dark' ? 'light' : 'dark';
277
+ const next = canvasThemeScheme(canvasTheme.value) === 'dark' ? 'daylight' : 'harbor';
277
278
  // An explicit pick ends any ?theme= session override, same as the
278
279
  // toolbar picker — otherwise the session stays sticky-overridden.
279
280
  clearThemeOverride();
@@ -26,6 +26,7 @@ import {
26
26
  updateEdgeFromClient,
27
27
  updateNodeFromClient,
28
28
  } from '../state/intent-bridge';
29
+ import { canOpenAsSite, openNodeAsSite } from '../nodes/surface-url';
29
30
  import { askText } from './TextPrompt';
30
31
  import { EXPANDABLE_TYPES } from '../types';
31
32
  import type { CanvasNodeState } from '../types';
@@ -498,6 +499,16 @@ function buildNodeMenuItems(node: CanvasNodeState): MenuItem[] {
498
499
  action: () => toggleCollapsed(node.id),
499
500
  });
500
501
 
502
+ // Open as site — full-page standalone view of this node's surface, served
503
+ // from /api/canvas/surface/:id (same document as the canvas iframe). Opens
504
+ // via the system browser so embedded hosts do not trap it in their webview.
505
+ if (canOpenAsSite(node)) {
506
+ items.push({
507
+ label: 'Open in new tab',
508
+ action: () => void openNodeAsSite(node),
509
+ });
510
+ }
511
+
501
512
  // Context pin — add/remove from the human-curated agent context (report #63).
502
513
  // This is the PRIMARY "pin" in PMX's model ("pin nodes to curate context"); it
503
514
  // matches the SelectionBar's "Pin as context" and updates the context count + the
@@ -12,7 +12,7 @@ import type { CanvasEdge, CanvasNodeState } from '../types';
12
12
  // ── Edge type visual styles ──────────────────────────────────
13
13
  const EDGE_COLORS: Record<CanvasEdge['type'], string> = {
14
14
  relation: 'var(--c-muted)',
15
- 'depends-on': 'var(--c-warn)',
15
+ 'depends-on': 'var(--c-rel-informs)',
16
16
  flow: 'var(--c-accent)',
17
17
  // --c-dim is a very low-contrast hairline in dark palettes (forest: #5D7566
18
18
  // on #0C1712); the dashed style is what distinguishes `references`.
@@ -2,6 +2,7 @@ import { signal } from '@preact/signals';
2
2
  import { useEffect, useState } from 'preact/hooks';
3
3
  import { requestJson } from '../state/intent-bridge';
4
4
  import { activeBoardId } from '../state/boards-store';
5
+ import { downloadWorkbenchExport, isHostedWorkbench } from '../state/workbench-transport';
5
6
 
6
7
  interface ExportManifest {
7
8
  boardId: string;
@@ -43,6 +44,7 @@ export function ExportDialog() {
43
44
  const [manifest, setManifest] = useState<ExportManifest | null>(null);
44
45
  const [result, setResult] = useState<ExportResult | null>(null);
45
46
  const [busy, setBusy] = useState(false);
47
+ const [downloadError, setDownloadError] = useState('');
46
48
  const open = exportDialogOpen.value;
47
49
  const boardId = activeBoardId.value;
48
50
 
@@ -50,6 +52,7 @@ export function ExportDialog() {
50
52
  if (!open || !boardId) return;
51
53
  let current = true;
52
54
  setResult(null);
55
+ setDownloadError('');
53
56
  setManifest(null);
54
57
  void requestJson<{ manifest?: ExportManifest } | null>(
55
58
  'exportPreview',
@@ -174,13 +177,37 @@ export function ExportDialog() {
174
177
  Saved ({sizeLabel(result.bytes)}): <code>{result.path}</code>
175
178
  </div>
176
179
  <div class="export-dialog-links">
177
- <a href={result.url} target="_blank" rel="noopener noreferrer">
178
- Open
179
- </a>
180
- <a class="is-primary" href={`${result.url}?download=1`}>
181
- Download
182
- </a>
180
+ {isHostedWorkbench() ? (
181
+ <button
182
+ type="button"
183
+ disabled={busy}
184
+ onClick={async () => {
185
+ setBusy(true);
186
+ setDownloadError('');
187
+ try {
188
+ await downloadWorkbenchExport(result.url);
189
+ } catch (error) {
190
+ setDownloadError(error instanceof Error ? error.message : 'Download failed. Try again.');
191
+ } finally {
192
+ setBusy(false);
193
+ }
194
+ }}
195
+ >
196
+ {busy ? 'Downloading…' : 'Download'}
197
+ </button>
198
+ ) : (
199
+ <>
200
+ <a href={result.url} target="_blank" rel="noopener noreferrer">
201
+ Open
202
+ </a>
203
+ <a class="is-primary" href={`${result.url}?download=1`}>
204
+ Download
205
+ </a>
206
+ </>
207
+ )}
183
208
  </div>
209
+ {isHostedWorkbench() && <p>Download the HTML file to open it outside ChatGPT.</p>}
210
+ {downloadError && <p role="alert">{downloadError}</p>}
184
211
  </div>
185
212
  )}
186
213
  <div class="text-prompt-actions">
@@ -8,8 +8,8 @@ import type { CanvasNodeState } from '../types';
8
8
 
9
9
  /**
10
10
  * Ghost Cursor of Intent overlay. Renders the agent's pre-commit moves as faint
11
- * placeholders in world space (it lives inside the canvas transform, like
12
- * FocusFieldLayer, so positions are world coords). Five kinds:
11
+ * placeholders in world space (it lives inside the canvas transform, so positions
12
+ * are world coords). Five kinds:
13
13
  * create → dashed ghost node with icon + type badge
14
14
  * move → ghost at the destination + a dashed trail from the current node
15
15
  * connect → dashed bezier in the edge-type color
@@ -1,12 +1,16 @@
1
- import { agentIdentityHue } from '../../shared/agent-presence.js';
2
1
  import type { Signal } from '@preact/signals';
3
2
  import { useCallback, useRef } from 'preact/hooks';
4
3
  import { fenceRectFromNodes } from '../../shared/scope-fence.js';
5
- import { selectedNodeIds } from '../state/canvas-store';
6
- import { agentPresences, presenceWorldPosition } from '../state/presence-store';
4
+ import { contextPinnedNodeIds, selectedNodeIds } from '../state/canvas-store';
5
+ import {
6
+ agentPresences,
7
+ mutatingNodeIds,
8
+ presenceWorldPosition,
9
+ sessionActive,
10
+ writerColor,
11
+ } from '../state/presence-store';
7
12
  import { scopeFence } from '../state/session-store';
8
13
  import type { CanvasEdge, CanvasNodeState, ViewportState } from '../types';
9
- import { KIND_COLOR } from './kind-colors';
10
14
 
11
15
  /**
12
16
  * Minimap v2 (rail-chrome-v2 phase 7, design item 19): a true-scale node map
@@ -141,6 +145,8 @@ export function Minimap({ viewport, nodes, onNavigate, containerWidth, container
141
145
  );
142
146
 
143
147
  const selected = selectedNodeIds.value;
148
+ const pinned = contextPinnedNodeIds.value;
149
+ const agentAtWork = sessionActive.value ? mutatingNodeIds.value : new Set<string>();
144
150
  const fence = scopeFence.value;
145
151
  const fenceRect = fence
146
152
  ? fenceRectFromNodes(
@@ -177,14 +183,13 @@ export function Minimap({ viewport, nodes, onNavigate, containerWidth, container
177
183
  return (
178
184
  <span
179
185
  key={node.id}
180
- class={`minimap-node${isGroup ? ' is-group' : ''}${selected.has(node.id) ? ' is-selected' : ''}`}
186
+ class={`minimap-node${isGroup ? ' is-group' : ''}${selected.has(node.id) ? ' is-selected' : ''}${pinned.has(node.id) ? ' is-pinned' : ''}${agentAtWork.has(node.id) ? ' is-agent' : ''}`}
181
187
  data-kind={node.type}
182
188
  style={{
183
189
  left: `${toX(node.position.x)}px`,
184
190
  top: `${toY(node.position.y)}px`,
185
191
  width: `${Math.max(isGroup ? 6 : 4, node.size.width * scale)}px`,
186
192
  height: `${Math.max(isGroup ? 5 : 3, node.size.height * scale)}px`,
187
- '--kind': KIND_COLOR[node.type],
188
193
  }}
189
194
  />
190
195
  );
@@ -207,7 +212,7 @@ export function Minimap({ viewport, nodes, onNavigate, containerWidth, container
207
212
  style={{
208
213
  left: `${toX(dot.at.x) - 4}px`,
209
214
  top: `${toY(dot.at.y) - 4}px`,
210
- '--identity-color': `hsl(${agentIdentityHue(dot.id)} 65% 62%)`,
215
+ '--identity-color': writerColor(dot.id),
211
216
  }}
212
217
  />
213
218
  ))}
@@ -0,0 +1,156 @@
1
+ import { useState } from 'preact/hooks';
2
+ import { contextPinMeta, pinnedReadState, writerName } from '../state/context-status-store';
3
+ import { centerDistance } from '../../server/spatial-analysis.js';
4
+ import { nodes } from '../state/canvas-store';
5
+ import { nearPins } from '../state/near-pin-store';
6
+ import type { CanvasNodeState } from '../types';
7
+ import { BarHint } from './BarHint';
8
+
9
+ // Glyphs from docs/design/AgentContext.dc.html (24-unit paths).
10
+ export const GLYPHS = {
11
+ eye: 'M2 12s3.5-6 10-6 10 6 10 6-3.5 6-10 6S2 12 2 12zM12 9.5a2.5 2.5 0 1 0 0 5a2.5 2.5 0 1 0 0-5',
12
+ eyeOff:
13
+ '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',
14
+ spark: 'M12 3l1.8 5.2L19 10l-5.2 1.8L12 17l-1.8-5.2L5 10l5.2-1.8z',
15
+ pen: 'M4 20l4.2-1L19 8.2 15.8 5 5 15.8z',
16
+ warn: 'M12 4l9 16H3zM12 10v4M12 17v.5',
17
+ } as const;
18
+
19
+ interface Mark {
20
+ tone: 'agent' | 'muted' | 'warn';
21
+ glyph: keyof typeof GLYPHS;
22
+ word: string;
23
+ label: string;
24
+ body: string;
25
+ }
26
+
27
+ function clock(iso: string): string {
28
+ const date = new Date(iso);
29
+ return Number.isNaN(date.getTime()) ? iso : date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
30
+ }
31
+
32
+ function markFor(node: CanvasNodeState, pinned: boolean): Mark | null {
33
+ if (pinned) {
34
+ const pin = contextPinMeta.value[node.id];
35
+ const pinnedBy =
36
+ pin?.pinnedBy.actor === 'agent'
37
+ ? ` Pinned by ${writerName(pin.pinnedBy)}${pin.reason ? `: ${pin.reason}` : ''}.`
38
+ : '';
39
+ const state = pinnedReadState(node.id, node.contentRevision ?? 0);
40
+ if (state.kind === 'not-read') {
41
+ return {
42
+ tone: 'muted',
43
+ glyph: 'eyeOff',
44
+ word: 'not read',
45
+ label: 'Not read yet',
46
+ body: `In context, but no agent has loaded it since it was pinned.${pinnedBy}`,
47
+ };
48
+ }
49
+ if (state.kind === 'changed') {
50
+ return {
51
+ tone: 'warn',
52
+ glyph: 'warn',
53
+ word: 'changed since read',
54
+ label: "The agent's copy is out of date",
55
+ body: `Changed after ${state.by} read it at ${clock(state.at)}.${pinnedBy}`,
56
+ };
57
+ }
58
+ return {
59
+ tone: 'agent',
60
+ glyph: 'eye',
61
+ word: 'read',
62
+ label: 'Read by the agent',
63
+ body: `${state.count > 1 ? `Read ${state.count} times · last` : 'Read'} ${clock(state.at)} by ${state.by}.${pinnedBy}`,
64
+ };
65
+ }
66
+ // A person's edit ends the agent's byline (decided 2026-10-05).
67
+ if (node.lastEditedBy?.actor === 'human') return null;
68
+ if (node.createdBy?.actor === 'agent') {
69
+ const name = writerName(node.createdBy);
70
+ return {
71
+ tone: 'agent',
72
+ glyph: 'spark',
73
+ word: `by ${name}`,
74
+ label: `Created by ${name}`,
75
+ body: 'The byline stays until you edit the node.',
76
+ };
77
+ }
78
+ if (node.lastEditedBy?.actor === 'agent') {
79
+ const name = writerName(node.lastEditedBy);
80
+ return {
81
+ tone: 'agent',
82
+ glyph: 'pen',
83
+ word: 'edited',
84
+ label: `Edited by ${name}`,
85
+ body: 'The mark stays until you edit the node.',
86
+ };
87
+ }
88
+ return null;
89
+ }
90
+
91
+ /** The header chip saying what agents did with this node (one per node; glyph + word). */
92
+ export function NodeContextMark({ node, pinned }: { node: CanvasNodeState; pinned: boolean }) {
93
+ const mark = markFor(node, pinned);
94
+ if (!mark) return null;
95
+ return (
96
+ <BarHint label={mark.label} body={mark.body} align="end" tapToOpen>
97
+ <span class={`node-context-mark is-${mark.tone}`} data-mark={mark.word}>
98
+ <svg viewBox="0 0 24 24" aria-hidden="true">
99
+ <path d={GLYPHS[mark.glyph]} />
100
+ </svg>
101
+ {mark.word}
102
+ </span>
103
+ </BarHint>
104
+ );
105
+ }
106
+
107
+ /** Pinned by an agent: the violet dot on the pin badge. */
108
+ export function isAgentPin(nodeId: string): boolean {
109
+ return contextPinMeta.value[nodeId]?.pinnedBy.actor === 'agent';
110
+ }
111
+
112
+ /**
113
+ * Near a pin (docs/design/NearPin.dc.html): a dotted pin-blue chip on an
114
+ * unpinned node the brief carries as title + short summary. Weaker than "in
115
+ * context" on purpose: the agent never gets this node's full content.
116
+ */
117
+ export function NearPinMark({ node, pinned }: { node: CanvasNodeState; pinned: boolean }) {
118
+ // The pins' titles join this node's DOM only while the chip is hovered or
119
+ // focused: at rest the node must not contain other nodes' titles.
120
+ const [open, setOpen] = useState(false);
121
+ const near = pinned ? undefined : nearPins.value.get(node.id);
122
+ if (!near || near.length === 0) return null;
123
+ const word = near.length > 1 ? `near ${near.length}` : 'near';
124
+ // Distances change every drag frame; read them only while the hint is open.
125
+ const where = open
126
+ ? near
127
+ .map((pin) => {
128
+ const pinNode = nodes.value.get(pin.pinNodeId);
129
+ return pinNode ? `“${pin.pinTitle}” (${Math.round(centerDistance(node, pinNode))} px)` : `“${pin.pinTitle}”`;
130
+ })
131
+ .join(', ')
132
+ : '';
133
+ return (
134
+ <BarHint
135
+ label={open ? `Near ${where}` : 'Near a pin'}
136
+ body="The agent gets its title and a short summary, not its full content. Pin it to send its content."
137
+ align="end"
138
+ tapToOpen
139
+ >
140
+ <span
141
+ class="node-near-mark"
142
+ data-near={near.map((pin) => pin.pinNodeId).join(' ')}
143
+ onMouseEnter={() => setOpen(true)}
144
+ onMouseLeave={() => setOpen(false)}
145
+ onFocusIn={() => setOpen(true)}
146
+ onFocusOut={() => setOpen(false)}
147
+ >
148
+ <svg viewBox="0 0 24 24" aria-hidden="true">
149
+ <circle cx="12" cy="12" r="8" class="node-near-ring" />
150
+ <circle cx="12" cy="12" r="2.2" class="node-near-dot" />
151
+ </svg>
152
+ {word}
153
+ </span>
154
+ </BarHint>
155
+ );
156
+ }
@@ -1,10 +1,10 @@
1
1
  import { useEffect, useState } from 'preact/hooks';
2
2
  import { IconChevronRight, IconUndo } from '../icons';
3
3
  import { useNow } from './use-now';
4
- import { agentIdentityHue, agentPhaseLabel } from '../../shared/agent-presence.js';
4
+ import { agentPhaseLabel } from '../../shared/agent-presence.js';
5
5
  import { formatCountdown, gateRemainingMs } from '../../shared/approval-gates.js';
6
6
  import { focusNode, selectedNodeIds } from '../state/canvas-store';
7
- import { activeSession, agentPresences } from '../state/presence-store';
7
+ import { activeSession, agentPresences, writerColor } from '../state/presence-store';
8
8
  import {
9
9
  type ApprovalGateView,
10
10
  endSession,
@@ -281,12 +281,9 @@ function TimelineRow({ entry }: { entry: TimelineEntry }) {
281
281
  {entry.label}
282
282
  </span>
283
283
  {/* With several assistants on one board, "Assistant · 23:11" answers
284
- nothing — every row names its writer in that writer's identity hue. */}
284
+ nothing — every row names its writer. */}
285
285
  {entry.who && (
286
- <span
287
- class="session-timeline-who"
288
- style={{ '--identity-color': `hsl(${agentIdentityHue(entry.who)} 65% 62%)` }}
289
- >
286
+ <span class="session-timeline-who" style={{ '--identity-color': writerColor(entry.who) }}>
290
287
  {writerDisplay(entry.who)}
291
288
  </span>
292
289
  )}