pmx-canvas 0.1.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 (226) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/LICENSE +21 -0
  3. package/Readme.md +865 -0
  4. package/dist/canvas/global.css +3173 -0
  5. package/dist/canvas/index.js +183 -0
  6. package/dist/json-render/index.css +2 -0
  7. package/dist/json-render/index.js +389 -0
  8. package/dist/types/cli/agent.d.ts +13 -0
  9. package/dist/types/cli/index.d.ts +2 -0
  10. package/dist/types/cli/watch.d.ts +5 -0
  11. package/dist/types/client/App.d.ts +1 -0
  12. package/dist/types/client/canvas/AttentionHistory.d.ts +1 -0
  13. package/dist/types/client/canvas/AttentionToast.d.ts +1 -0
  14. package/dist/types/client/canvas/CanvasNode.d.ts +8 -0
  15. package/dist/types/client/canvas/CanvasViewport.d.ts +8 -0
  16. package/dist/types/client/canvas/CommandPalette.d.ts +4 -0
  17. package/dist/types/client/canvas/ContextMenu.d.ts +24 -0
  18. package/dist/types/client/canvas/ContextPinBar.d.ts +1 -0
  19. package/dist/types/client/canvas/ContextPinHud.d.ts +1 -0
  20. package/dist/types/client/canvas/DockedNode.d.ts +4 -0
  21. package/dist/types/client/canvas/EdgeLayer.d.ts +8 -0
  22. package/dist/types/client/canvas/ExpandedNodeOverlay.d.ts +1 -0
  23. package/dist/types/client/canvas/FocusFieldLayer.d.ts +1 -0
  24. package/dist/types/client/canvas/Minimap.d.ts +23 -0
  25. package/dist/types/client/canvas/SelectionBar.d.ts +1 -0
  26. package/dist/types/client/canvas/ShortcutOverlay.d.ts +3 -0
  27. package/dist/types/client/canvas/SnapshotPanel.d.ts +7 -0
  28. package/dist/types/client/canvas/snap-guides.d.ts +23 -0
  29. package/dist/types/client/canvas/use-node-drag.d.ts +15 -0
  30. package/dist/types/client/canvas/use-node-resize.d.ts +15 -0
  31. package/dist/types/client/canvas/use-pan-zoom.d.ts +16 -0
  32. package/dist/types/client/ext-app/bridge.d.ts +161 -0
  33. package/dist/types/client/icons.d.ts +70 -0
  34. package/dist/types/client/index.d.ts +1 -0
  35. package/dist/types/client/nodes/ContextNode.d.ts +34 -0
  36. package/dist/types/client/nodes/ExtAppFrame.d.ts +18 -0
  37. package/dist/types/client/nodes/FileNode.d.ts +5 -0
  38. package/dist/types/client/nodes/GroupNode.d.ts +6 -0
  39. package/dist/types/client/nodes/ImageNode.d.ts +10 -0
  40. package/dist/types/client/nodes/InlineFormatBar.d.ts +7 -0
  41. package/dist/types/client/nodes/InlineMarkdownEditor.d.ts +14 -0
  42. package/dist/types/client/nodes/LedgerNode.d.ts +4 -0
  43. package/dist/types/client/nodes/MarkdownNode.d.ts +6 -0
  44. package/dist/types/client/nodes/McpAppNode.d.ts +4 -0
  45. package/dist/types/client/nodes/MdFormatBar.d.ts +8 -0
  46. package/dist/types/client/nodes/PromptNode.d.ts +5 -0
  47. package/dist/types/client/nodes/ResponseNode.d.ts +5 -0
  48. package/dist/types/client/nodes/StatusNode.d.ts +4 -0
  49. package/dist/types/client/nodes/StatusSummary.d.ts +4 -0
  50. package/dist/types/client/nodes/TraceNode.d.ts +4 -0
  51. package/dist/types/client/nodes/WebpageNode.d.ts +5 -0
  52. package/dist/types/client/nodes/image-warnings.d.ts +6 -0
  53. package/dist/types/client/nodes/inline-editor-commands.d.ts +11 -0
  54. package/dist/types/client/nodes/md-format.d.ts +25 -0
  55. package/dist/types/client/state/attention-bridge.d.ts +3 -0
  56. package/dist/types/client/state/attention-store.d.ts +25 -0
  57. package/dist/types/client/state/canvas-store.d.ts +74 -0
  58. package/dist/types/client/state/intent-bridge.d.ts +158 -0
  59. package/dist/types/client/state/sse-bridge.d.ts +5 -0
  60. package/dist/types/client/theme/tokens.d.ts +27 -0
  61. package/dist/types/client/types.d.ts +40 -0
  62. package/dist/types/client/utils/ext-app-tool-result.d.ts +1 -0
  63. package/dist/types/client/utils/placement.d.ts +1 -0
  64. package/dist/types/client/utils/platform.d.ts +2 -0
  65. package/dist/types/json-render/catalog.d.ts +815 -0
  66. package/dist/types/json-render/charts/components.d.ts +54 -0
  67. package/dist/types/json-render/charts/definitions.d.ts +103 -0
  68. package/dist/types/json-render/charts/extra-components.d.ts +58 -0
  69. package/dist/types/json-render/charts/extra-definitions.d.ts +181 -0
  70. package/dist/types/json-render/renderer/index.d.ts +16 -0
  71. package/dist/types/json-render/schema.d.ts +46 -0
  72. package/dist/types/json-render/server.d.ts +55 -0
  73. package/dist/types/mcp/server.d.ts +22 -0
  74. package/dist/types/server/agent-context.d.ts +21 -0
  75. package/dist/types/server/artifact-paths.d.ts +3 -0
  76. package/dist/types/server/canvas-operations.d.ts +154 -0
  77. package/dist/types/server/canvas-provenance.d.ts +13 -0
  78. package/dist/types/server/canvas-schema.d.ts +49 -0
  79. package/dist/types/server/canvas-serialization.d.ts +25 -0
  80. package/dist/types/server/canvas-state.d.ts +174 -0
  81. package/dist/types/server/canvas-validation.d.ts +33 -0
  82. package/dist/types/server/chart-template.d.ts +29 -0
  83. package/dist/types/server/code-graph.d.ts +67 -0
  84. package/dist/types/server/context-cards.d.ts +24 -0
  85. package/dist/types/server/diagram-presets.d.ts +28 -0
  86. package/dist/types/server/ext-app-call-registry.d.ts +16 -0
  87. package/dist/types/server/ext-app-tool-result.d.ts +1 -0
  88. package/dist/types/server/file-watcher.d.ts +16 -0
  89. package/dist/types/server/index.d.ts +243 -0
  90. package/dist/types/server/mcp-app-candidate.d.ts +25 -0
  91. package/dist/types/server/mcp-app-host.d.ts +65 -0
  92. package/dist/types/server/mcp-app-runtime.d.ts +47 -0
  93. package/dist/types/server/mutation-history.d.ts +105 -0
  94. package/dist/types/server/placement.d.ts +37 -0
  95. package/dist/types/server/server.d.ts +103 -0
  96. package/dist/types/server/spatial-analysis.d.ts +87 -0
  97. package/dist/types/server/trace-manager.d.ts +48 -0
  98. package/dist/types/server/web-artifacts.d.ts +50 -0
  99. package/dist/types/server/webpage-node.d.ts +25 -0
  100. package/dist/types/shared/auto-arrange.d.ts +29 -0
  101. package/dist/types/shared/ext-app-tool-result.d.ts +9 -0
  102. package/dist/types/shared/placement.d.ts +26 -0
  103. package/dist/types/shared/semantic-attention.d.ts +97 -0
  104. package/package.json +109 -0
  105. package/skills/data-analysis/SKILL.md +324 -0
  106. package/skills/doc-coauthoring/SKILL.md +375 -0
  107. package/skills/frontend-design/SKILL.md +45 -0
  108. package/skills/json-render-codegen/SKILL.md +112 -0
  109. package/skills/json-render-core/SKILL.md +265 -0
  110. package/skills/json-render-ink/SKILL.md +273 -0
  111. package/skills/json-render-mcp/SKILL.md +132 -0
  112. package/skills/json-render-react/SKILL.md +264 -0
  113. package/skills/json-render-shadcn/SKILL.md +159 -0
  114. package/skills/playwright-cli/SKILL.md +67 -0
  115. package/skills/pmx-canvas/SKILL.md +668 -0
  116. package/skills/pmx-canvas/evals/evals.json +186 -0
  117. package/skills/pmx-canvas-testing/SKILL.md +78 -0
  118. package/skills/published-consumer-e2e/SKILL.md +43 -0
  119. package/skills/published-consumer-e2e/scripts/run-published-consumer-e2e.sh +241 -0
  120. package/skills/web-artifacts-builder/SKILL.md +80 -0
  121. package/skills/web-artifacts-builder/scripts/bundle-artifact.sh +167 -0
  122. package/skills/web-artifacts-builder/scripts/init-artifact.sh +425 -0
  123. package/skills/web-artifacts-builder/scripts/shadcn-components.tar.gz +0 -0
  124. package/skills/web-design-guidelines/SKILL.md +39 -0
  125. package/src/cli/agent.ts +2144 -0
  126. package/src/cli/index.ts +622 -0
  127. package/src/cli/watch.ts +88 -0
  128. package/src/client/App.tsx +507 -0
  129. package/src/client/canvas/AttentionHistory.tsx +81 -0
  130. package/src/client/canvas/AttentionToast.tsx +19 -0
  131. package/src/client/canvas/CanvasNode.tsx +363 -0
  132. package/src/client/canvas/CanvasViewport.tsx +590 -0
  133. package/src/client/canvas/CommandPalette.tsx +302 -0
  134. package/src/client/canvas/ContextMenu.tsx +601 -0
  135. package/src/client/canvas/ContextPinBar.tsx +25 -0
  136. package/src/client/canvas/ContextPinHud.tsx +22 -0
  137. package/src/client/canvas/DockedNode.tsx +66 -0
  138. package/src/client/canvas/EdgeLayer.tsx +280 -0
  139. package/src/client/canvas/ExpandedNodeOverlay.tsx +260 -0
  140. package/src/client/canvas/FocusFieldLayer.tsx +107 -0
  141. package/src/client/canvas/Minimap.tsx +301 -0
  142. package/src/client/canvas/SelectionBar.tsx +69 -0
  143. package/src/client/canvas/ShortcutOverlay.tsx +69 -0
  144. package/src/client/canvas/SnapshotPanel.tsx +236 -0
  145. package/src/client/canvas/snap-guides.ts +170 -0
  146. package/src/client/canvas/use-node-drag.ts +51 -0
  147. package/src/client/canvas/use-node-resize.ts +59 -0
  148. package/src/client/canvas/use-pan-zoom.ts +191 -0
  149. package/src/client/ext-app/bridge.ts +542 -0
  150. package/src/client/icons.tsx +424 -0
  151. package/src/client/index.tsx +7 -0
  152. package/src/client/nodes/ContextNode.tsx +412 -0
  153. package/src/client/nodes/ExtAppFrame.tsx +509 -0
  154. package/src/client/nodes/FileNode.tsx +256 -0
  155. package/src/client/nodes/GroupNode.tsx +39 -0
  156. package/src/client/nodes/ImageNode.tsx +160 -0
  157. package/src/client/nodes/InlineFormatBar.tsx +169 -0
  158. package/src/client/nodes/InlineMarkdownEditor.tsx +123 -0
  159. package/src/client/nodes/LedgerNode.tsx +37 -0
  160. package/src/client/nodes/MarkdownNode.tsx +359 -0
  161. package/src/client/nodes/McpAppNode.tsx +85 -0
  162. package/src/client/nodes/MdFormatBar.tsx +109 -0
  163. package/src/client/nodes/PromptNode.tsx +597 -0
  164. package/src/client/nodes/ResponseNode.tsx +153 -0
  165. package/src/client/nodes/StatusNode.tsx +84 -0
  166. package/src/client/nodes/StatusSummary.tsx +38 -0
  167. package/src/client/nodes/TraceNode.tsx +120 -0
  168. package/src/client/nodes/WebpageNode.tsx +288 -0
  169. package/src/client/nodes/image-warnings.ts +95 -0
  170. package/src/client/nodes/inline-editor-commands.ts +37 -0
  171. package/src/client/nodes/md-format.ts +206 -0
  172. package/src/client/state/attention-bridge.ts +328 -0
  173. package/src/client/state/attention-store.ts +73 -0
  174. package/src/client/state/canvas-store.ts +631 -0
  175. package/src/client/state/intent-bridge.ts +315 -0
  176. package/src/client/state/sse-bridge.ts +965 -0
  177. package/src/client/theme/global.css +3173 -0
  178. package/src/client/theme/tokens.ts +72 -0
  179. package/src/client/types-shims.d.ts +5 -0
  180. package/src/client/types.ts +81 -0
  181. package/src/client/utils/ext-app-tool-result.ts +4 -0
  182. package/src/client/utils/placement.ts +4 -0
  183. package/src/client/utils/platform.ts +2 -0
  184. package/src/json-render/catalog.ts +256 -0
  185. package/src/json-render/charts/components.tsx +198 -0
  186. package/src/json-render/charts/definitions.ts +81 -0
  187. package/src/json-render/charts/extra-components.tsx +267 -0
  188. package/src/json-render/charts/extra-definitions.ts +145 -0
  189. package/src/json-render/renderer/index.css +174 -0
  190. package/src/json-render/renderer/index.tsx +86 -0
  191. package/src/json-render/schema.ts +62 -0
  192. package/src/json-render/server.ts +597 -0
  193. package/src/mcp/server.ts +1377 -0
  194. package/src/server/agent-context.ts +242 -0
  195. package/src/server/artifact-paths.ts +17 -0
  196. package/src/server/canvas-operations.ts +1279 -0
  197. package/src/server/canvas-provenance.ts +243 -0
  198. package/src/server/canvas-schema.ts +432 -0
  199. package/src/server/canvas-serialization.ts +95 -0
  200. package/src/server/canvas-state.ts +1134 -0
  201. package/src/server/canvas-validation.ts +114 -0
  202. package/src/server/chart-template.ts +449 -0
  203. package/src/server/code-graph.ts +370 -0
  204. package/src/server/context-cards.ts +31 -0
  205. package/src/server/diagram-presets.ts +71 -0
  206. package/src/server/ext-app-call-registry.ts +77 -0
  207. package/src/server/ext-app-tool-result.ts +4 -0
  208. package/src/server/file-watcher.ts +121 -0
  209. package/src/server/index.ts +647 -0
  210. package/src/server/mcp-app-candidate.ts +174 -0
  211. package/src/server/mcp-app-host.ts +814 -0
  212. package/src/server/mcp-app-runtime.ts +459 -0
  213. package/src/server/mutation-history.ts +350 -0
  214. package/src/server/placement.ts +125 -0
  215. package/src/server/server.ts +3846 -0
  216. package/src/server/spatial-analysis.ts +356 -0
  217. package/src/server/trace-manager.ts +333 -0
  218. package/src/server/web-artifacts/scripts/bundle-artifact.sh +167 -0
  219. package/src/server/web-artifacts/scripts/init-artifact.sh +426 -0
  220. package/src/server/web-artifacts/scripts/shadcn-components.tar.gz +0 -0
  221. package/src/server/web-artifacts.ts +442 -0
  222. package/src/server/webpage-node.ts +328 -0
  223. package/src/shared/auto-arrange.ts +439 -0
  224. package/src/shared/ext-app-tool-result.ts +76 -0
  225. package/src/shared/placement.ts +81 -0
  226. package/src/shared/semantic-attention.ts +598 -0
@@ -0,0 +1,507 @@
1
+ import type { ComponentChildren } from 'preact';
2
+ import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
3
+ import { AttentionHistory } from './canvas/AttentionHistory';
4
+ import { AttentionToast } from './canvas/AttentionToast';
5
+ import { CanvasViewport } from './canvas/CanvasViewport';
6
+ import { CommandPalette } from './canvas/CommandPalette';
7
+ import { ContextMenu, useContextMenu } from './canvas/ContextMenu';
8
+ import { ContextPinBar } from './canvas/ContextPinBar';
9
+ import { DockedNode } from './canvas/DockedNode';
10
+ import { ExpandedNodeOverlay } from './canvas/ExpandedNodeOverlay';
11
+ import { Minimap } from './canvas/Minimap';
12
+ import { SelectionBar } from './canvas/SelectionBar';
13
+ import { ShortcutOverlay } from './canvas/ShortcutOverlay';
14
+ import { SnapshotPanel } from './canvas/SnapshotPanel';
15
+ import {
16
+ activeNodeId,
17
+ animateViewport,
18
+ autoArrange,
19
+ canvasTheme,
20
+ clearSelection,
21
+ collapseExpandedNode,
22
+ connectionStatus,
23
+ contextPinnedNodeIds,
24
+ cycleActiveNode,
25
+ edges,
26
+ expandedNodeId,
27
+ fitAll,
28
+ forceDirectedArrange,
29
+ hasInitialServerLayout,
30
+ nodes,
31
+ persistLayout,
32
+ selectedNodeIds,
33
+ sessionId,
34
+ setViewport,
35
+ traceEnabled,
36
+ viewport,
37
+ walkGraph,
38
+ } from './state/canvas-store';
39
+ import { connectSSE } from './state/sse-bridge';
40
+ import {
41
+ IconArrange,
42
+ IconClearTrace,
43
+ IconFitAll,
44
+ IconLogo,
45
+ IconMinimap,
46
+ IconMoon,
47
+ IconResetView,
48
+ IconSearch,
49
+ IconShortcuts,
50
+ IconSnapshot,
51
+ IconSun,
52
+ IconTrace,
53
+ IconZoomIn,
54
+ IconZoomOut,
55
+ } from './icons';
56
+ import { invalidateTokenCache } from './theme/tokens';
57
+ import { MOD_KEY } from './utils/platform';
58
+
59
+ function logAppError(action: string, error: unknown): void {
60
+ console.error(`[app] ${action} failed`, error);
61
+ }
62
+
63
+ function sendIntent(type: string, payload: Record<string, unknown> = {}): void {
64
+ fetch(`/api/workbench/intent?_ts=${Date.now()}`, {
65
+ method: 'POST',
66
+ headers: { 'Content-Type': 'application/json' },
67
+ body: JSON.stringify({ type, payload }),
68
+ }).catch((error) => {
69
+ logAppError('sendIntent', error);
70
+ });
71
+ }
72
+
73
+ function ToolbarHint({
74
+ label,
75
+ detail,
76
+ shortcut,
77
+ align = 'center',
78
+ children,
79
+ }: {
80
+ label: string;
81
+ detail?: string;
82
+ shortcut?: string;
83
+ align?: 'start' | 'center' | 'end';
84
+ children: ComponentChildren;
85
+ }) {
86
+ return (
87
+ <span class={`toolbar-tooltip-anchor toolbar-tooltip-anchor-${align}`}>
88
+ {children}
89
+ <span class="toolbar-tooltip" role="tooltip">
90
+ <span class="toolbar-tooltip-label">{label}</span>
91
+ {(detail || shortcut) && (
92
+ <span class="toolbar-tooltip-meta">
93
+ {detail && <span>{detail}</span>}
94
+ {shortcut && <kbd class="toolbar-tooltip-shortcut">{shortcut}</kbd>}
95
+ </span>
96
+ )}
97
+ </span>
98
+ </span>
99
+ );
100
+ }
101
+
102
+ function Toolbar({
103
+ minimapVisible,
104
+ onToggleMinimap,
105
+ snapshotOpen,
106
+ onToggleSnapshot,
107
+ snapshotBtnRef,
108
+ onOpenPalette,
109
+ onOpenShortcuts,
110
+ }: {
111
+ minimapVisible: boolean;
112
+ onToggleMinimap: () => void;
113
+ snapshotOpen: boolean;
114
+ onToggleSnapshot: () => void;
115
+ snapshotBtnRef: { current: HTMLButtonElement | null };
116
+ onOpenPalette: () => void;
117
+ onOpenShortcuts: () => void;
118
+ }) {
119
+ const status = connectionStatus.value;
120
+ const hasSynced = hasInitialServerLayout.value;
121
+ const v = viewport.value;
122
+ const nodeCount = nodes.value.size;
123
+ const edgeCount = edges.value.size;
124
+ const isTraceOn = traceEnabled.value;
125
+ const traceNodeCount = Array.from(nodes.value.values()).filter((n) => n.type === 'trace').length;
126
+ const statusTitle = status === 'connected' && !hasSynced ? 'syncing' : status;
127
+ const statusLabel = statusTitle.charAt(0).toUpperCase() + statusTitle.slice(1);
128
+ const countsLabel = hasSynced
129
+ ? [
130
+ `${nodeCount} node${nodeCount !== 1 ? 's' : ''}`,
131
+ ...(edgeCount > 0 ? [`${edgeCount} edge${edgeCount !== 1 ? 's' : ''}`] : []),
132
+ ...(traceNodeCount > 0
133
+ ? [`${traceNodeCount} trace${traceNodeCount !== 1 ? 's' : ''}`]
134
+ : isTraceOn
135
+ ? ['trace armed']
136
+ : []),
137
+ ].join(' · ')
138
+ : 'Syncing canvas…';
139
+
140
+ return (
141
+ <div class="toolbar-group">
142
+ {/* ── Navigation Bar ──────────────────────────────────── */}
143
+ <div class="canvas-toolbar">
144
+ <ToolbarHint label="PMX Canvas" detail="Focus Field · spatial workbench for coding agents" align="start">
145
+ <span class="canvas-brand" aria-label="PMX Canvas">
146
+ <IconLogo size={22} />
147
+ </span>
148
+ </ToolbarHint>
149
+
150
+ <div class="separator" />
151
+
152
+ <ToolbarHint label="Canvas status" detail={hasSynced ? statusLabel : 'Syncing canvas from server'} align="start">
153
+ <span class={`connection-dot ${status}`} aria-label={`Canvas status: ${statusTitle}`} />
154
+ </ToolbarHint>
155
+ <span style={{ fontSize: '11px', color: 'var(--c-muted)' }}>
156
+ {sessionId.value ? sessionId.value.slice(0, 12) : '…'}
157
+ </span>
158
+
159
+ <div class="separator" />
160
+
161
+ <ToolbarHint label="Fit canvas" detail="Frame every node on screen">
162
+ <button
163
+ type="button"
164
+ onClick={() => fitAll(window.innerWidth, window.innerHeight)}
165
+ aria-label="Fit canvas"
166
+ >
167
+ <IconFitAll />
168
+ </button>
169
+ </ToolbarHint>
170
+ <ToolbarHint label="Reset view" shortcut={`${MOD_KEY}+0`}>
171
+ <button
172
+ type="button"
173
+ onClick={() => animateViewport({ x: 0, y: 0, scale: 1 }, 250)}
174
+ aria-label="Reset view"
175
+ >
176
+ <IconResetView />
177
+ </button>
178
+ </ToolbarHint>
179
+ <ToolbarHint label="Zoom in" shortcut={`${MOD_KEY}++`}>
180
+ <button
181
+ type="button"
182
+ onClick={() => animateViewport({ ...v, scale: Math.min(4, v.scale * 1.25) }, 150)}
183
+ aria-label="Zoom in"
184
+ >
185
+ <IconZoomIn />
186
+ </button>
187
+ </ToolbarHint>
188
+ <ToolbarHint label="Zoom out" shortcut={`${MOD_KEY}+-`}>
189
+ <button
190
+ type="button"
191
+ onClick={() => animateViewport({ ...v, scale: Math.max(0.1, v.scale / 1.25) }, 150)}
192
+ aria-label="Zoom out"
193
+ >
194
+ <IconZoomOut />
195
+ </button>
196
+ </ToolbarHint>
197
+ <span style={{ fontSize: '10px', color: 'var(--c-dim)', minWidth: '36px', textAlign: 'center' }}>
198
+ {Math.round(v.scale * 100)}%
199
+ </span>
200
+
201
+ <div class="separator" />
202
+
203
+ <ToolbarHint
204
+ label="Arrange layout"
205
+ detail={edgeCount > 0 ? 'Graph-aware layout for connected nodes' : 'Grid layout for loose nodes'}
206
+ >
207
+ <button type="button" onClick={() => edgeCount > 0 ? forceDirectedArrange() : autoArrange()} aria-label="Arrange layout">
208
+ <IconArrange />
209
+ </button>
210
+ </ToolbarHint>
211
+ <ToolbarHint label={minimapVisible ? 'Hide minimap' : 'Show minimap'} detail="Quickly navigate large canvases">
212
+ <button
213
+ type="button"
214
+ onClick={onToggleMinimap}
215
+ aria-label={minimapVisible ? 'Hide minimap' : 'Show minimap'}
216
+ style={{ color: minimapVisible ? 'var(--c-accent)' : undefined }}
217
+ >
218
+ <IconMinimap />
219
+ </button>
220
+ </ToolbarHint>
221
+ <ToolbarHint label={`Switch to ${canvasTheme.value === 'dark' ? 'light' : 'dark'} theme`} detail={`Current theme: ${canvasTheme.value}`}>
222
+ <button
223
+ type="button"
224
+ onClick={() => {
225
+ const next = canvasTheme.value === 'dark' ? 'light' : 'dark';
226
+ canvasTheme.value = next;
227
+ document.documentElement.setAttribute('data-theme', next);
228
+ invalidateTokenCache();
229
+ }}
230
+ aria-label={`Switch to ${canvasTheme.value === 'dark' ? 'light' : 'dark'} theme`}
231
+ >
232
+ {canvasTheme.value === 'dark' ? <IconSun /> : <IconMoon />}
233
+ </button>
234
+ </ToolbarHint>
235
+ <ToolbarHint label="Snapshots" detail="Capture and restore canvas states" align="end">
236
+ <button
237
+ ref={snapshotBtnRef}
238
+ type="button"
239
+ onClick={onToggleSnapshot}
240
+ aria-label="Snapshots"
241
+ style={{ color: snapshotOpen ? 'var(--c-accent)' : undefined }}
242
+ >
243
+ <IconSnapshot />
244
+ </button>
245
+ </ToolbarHint>
246
+ </div>
247
+
248
+ {/* ── Action Bar ──────────────────────────────────────── */}
249
+ <div class="canvas-toolbar">
250
+ <ToolbarHint
251
+ label={isTraceOn ? 'Disable trace' : 'Enable trace'}
252
+ detail={isTraceOn ? 'Stop collecting new trace nodes' : 'Capture agent execution on the canvas'}
253
+ >
254
+ <button
255
+ type="button"
256
+ onClick={() => sendIntent('trace-toggle', { enabled: !isTraceOn })}
257
+ aria-label={isTraceOn ? 'Disable trace' : 'Enable trace'}
258
+ style={{ color: isTraceOn ? 'var(--c-purple)' : undefined }}
259
+ >
260
+ <IconTrace />
261
+ </button>
262
+ </ToolbarHint>
263
+ {(isTraceOn || traceNodeCount > 0) && (
264
+ <ToolbarHint
265
+ label="Clear trace"
266
+ detail={traceNodeCount > 0 ? `Remove ${traceNodeCount} trace node${traceNodeCount === 1 ? '' : 's'}` : 'Trace is enabled but still empty'}
267
+ >
268
+ <button
269
+ type="button"
270
+ onClick={() => sendIntent('trace-clear')}
271
+ aria-label="Clear trace"
272
+ >
273
+ <IconClearTrace />
274
+ </button>
275
+ </ToolbarHint>
276
+ )}
277
+
278
+ <div class="separator" />
279
+
280
+ <ToolbarHint label="Search nodes and actions" shortcut={`${MOD_KEY}+K`}>
281
+ <button
282
+ type="button"
283
+ onClick={onOpenPalette}
284
+ aria-label="Search nodes and actions"
285
+ >
286
+ <IconSearch />
287
+ </button>
288
+ </ToolbarHint>
289
+ <ToolbarHint label="Keyboard shortcuts" shortcut="?" align="end">
290
+ <button
291
+ type="button"
292
+ onClick={onOpenShortcuts}
293
+ aria-label="Keyboard shortcuts"
294
+ >
295
+ <IconShortcuts />
296
+ </button>
297
+ </ToolbarHint>
298
+
299
+ <span style={{ fontSize: '10px', color: 'var(--c-dim)' }}>{countsLabel}</span>
300
+ </div>
301
+ </div>
302
+ );
303
+ }
304
+
305
+ function WelcomeCard({ onOpenPalette }: { onOpenPalette: () => void }) {
306
+ return (
307
+ <div class="welcome-card">
308
+ <div class="welcome-icon">◇</div>
309
+ <div class="welcome-title">Shape What The Agent Sees</div>
310
+ <div class="welcome-subtitle">
311
+ Lay out notes, files, and evidence. Bring related nodes together. Pin what matters. The board will reflect the active focus.
312
+ </div>
313
+ <div class="welcome-hints">
314
+ <button type="button" class="welcome-hint" onClick={onOpenPalette}>
315
+ <kbd>{MOD_KEY}+K</kbd>
316
+ <span>Create a note</span>
317
+ </button>
318
+ <div class="welcome-hint">
319
+ <kbd>Drop files</kbd>
320
+ <span>Add evidence to the board</span>
321
+ </div>
322
+ <div class="welcome-hint">
323
+ <kbd>{'\u2726'}</kbd>
324
+ <span>Pin important nodes</span>
325
+ </div>
326
+ <div class="welcome-hint">
327
+ <kbd>Move nearby</kbd>
328
+ <span>Shape the focus field</span>
329
+ </div>
330
+ </div>
331
+ <div class="welcome-footer">
332
+ The canvas is a shared attention surface, not just an editor.
333
+ </div>
334
+ </div>
335
+ );
336
+ }
337
+
338
+ export function App() {
339
+ const [minimapVisible, setMinimapVisible] = useState(true);
340
+ const [snapshotOpen, setSnapshotOpen] = useState(false);
341
+ const [paletteOpen, setPaletteOpen] = useState(false);
342
+ const [shortcutsOpen, setShortcutsOpen] = useState(false);
343
+ const snapshotBtnRef = useRef<HTMLButtonElement>(null);
344
+ const { menu, openNodeMenu, openCanvasMenu, closeMenu } = useContextMenu();
345
+ const hasInitialLayout = hasInitialServerLayout.value;
346
+
347
+ const handleToggleMinimap = useCallback(() => setMinimapVisible((v) => !v), []);
348
+ const handleToggleSnapshot = useCallback(() => setSnapshotOpen((v) => !v), []);
349
+ const handleCloseSnapshot = useCallback(() => setSnapshotOpen(false), []);
350
+
351
+ const handleMinimapNavigate = useCallback((x: number, y: number) => {
352
+ animateViewport({ x, y, scale: viewport.value.scale }, 200);
353
+ }, []);
354
+
355
+ useEffect(() => {
356
+ const disconnect = connectSSE();
357
+
358
+ const handleKeyDown = (e: KeyboardEvent) => {
359
+ const mod = e.metaKey || e.ctrlKey;
360
+
361
+ // Cmd/Ctrl+K toggles command palette (works from anywhere, including inputs)
362
+ if (mod && e.key === 'k') {
363
+ e.preventDefault();
364
+ setPaletteOpen((v) => !v);
365
+ return;
366
+ }
367
+
368
+ // Esc always collapses expanded node first (even from inside inputs)
369
+ if (e.key === 'Escape' && expandedNodeId.value) {
370
+ e.preventDefault();
371
+ collapseExpandedNode();
372
+ return;
373
+ }
374
+
375
+ // Esc closes command palette
376
+ if (e.key === 'Escape' && paletteOpen) {
377
+ e.preventDefault();
378
+ setPaletteOpen(false);
379
+ return;
380
+ }
381
+
382
+ // Esc closes shortcut overlay
383
+ if (e.key === 'Escape' && shortcutsOpen) {
384
+ e.preventDefault();
385
+ setShortcutsOpen(false);
386
+ return;
387
+ }
388
+
389
+ // Ignore other shortcuts when inside inputs
390
+ const tag = (e.target as HTMLElement)?.tagName;
391
+ if (tag === 'INPUT' || tag === 'TEXTAREA') return;
392
+
393
+ // ? toggles shortcut overlay
394
+ if (e.key === '?' || (e.key === '/' && e.shiftKey)) {
395
+ e.preventDefault();
396
+ setShortcutsOpen((v) => !v);
397
+ return;
398
+ }
399
+
400
+ if (mod && e.key === '0') {
401
+ e.preventDefault();
402
+ animateViewport({ x: 0, y: 0, scale: 1 }, 250);
403
+ } else if (mod && (e.key === '=' || e.key === '+')) {
404
+ e.preventDefault();
405
+ const cur = viewport.value;
406
+ animateViewport({ ...cur, scale: Math.min(4, cur.scale * 1.25) }, 150);
407
+ } else if (mod && e.key === '-') {
408
+ e.preventDefault();
409
+ const cur = viewport.value;
410
+ animateViewport({ ...cur, scale: Math.max(0.1, cur.scale / 1.25) }, 150);
411
+ } else if (e.key === 'Escape') {
412
+ if (selectedNodeIds.value.size > 0) {
413
+ clearSelection();
414
+ return;
415
+ }
416
+ activeNodeId.value = null;
417
+ closeMenu();
418
+ } else if (e.key === 'Tab') {
419
+ e.preventDefault();
420
+ cycleActiveNode(e.shiftKey ? -1 : 1);
421
+ } else if (activeNodeId.value && ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
422
+ e.preventDefault();
423
+ const dir = e.key.replace('Arrow', '').toLowerCase() as 'up' | 'down' | 'left' | 'right';
424
+ walkGraph(dir);
425
+ }
426
+ };
427
+
428
+ document.addEventListener('keydown', handleKeyDown);
429
+ return () => {
430
+ disconnect();
431
+ document.removeEventListener('keydown', handleKeyDown);
432
+ };
433
+ }, [closeMenu, paletteOpen, shortcutsOpen]);
434
+
435
+ useEffect(() => {
436
+ if (!hasInitialLayout) return;
437
+ const ready = (window as Window & { __pmxCanvasBootstrapReady?: () => void })
438
+ .__pmxCanvasBootstrapReady;
439
+ if (typeof ready === 'function') ready();
440
+ }, [hasInitialLayout]);
441
+
442
+ const allNodes = Array.from(nodes.value.values());
443
+ const dockedLeft = allNodes.filter((n) => n.dockPosition === 'left');
444
+ const dockedRight = allNodes
445
+ .filter((n) => n.dockPosition === 'right')
446
+ .sort((a, b) => {
447
+ const order: Record<string, number> = { context: 0, ledger: 1 };
448
+ return (order[a.type] ?? 2) - (order[b.type] ?? 2);
449
+ });
450
+
451
+ return (
452
+ <div style={{ width: '100%', height: '100%', position: 'relative' }}>
453
+ <div class="hud-layer">
454
+ <div class="hud-left">
455
+ {dockedLeft.map((n) => (
456
+ <DockedNode key={n.id} node={n} />
457
+ ))}
458
+ </div>
459
+ <Toolbar
460
+ minimapVisible={minimapVisible}
461
+ onToggleMinimap={handleToggleMinimap}
462
+ snapshotOpen={snapshotOpen}
463
+ onToggleSnapshot={handleToggleSnapshot}
464
+ snapshotBtnRef={snapshotBtnRef}
465
+ onOpenPalette={() => setPaletteOpen(true)}
466
+ onOpenShortcuts={() => setShortcutsOpen((v) => !v)}
467
+ />
468
+ <div class="hud-right">
469
+ {dockedRight.map((n) => (
470
+ <DockedNode key={n.id} node={n} />
471
+ ))}
472
+ </div>
473
+ </div>
474
+ <AttentionToast />
475
+ <AttentionHistory />
476
+ <CanvasViewport
477
+ onNodeContextMenu={openNodeMenu}
478
+ onCanvasContextMenu={openCanvasMenu}
479
+ />
480
+ {hasInitialLayout && allNodes.filter((n) => !n.dockPosition).length === 0 && (
481
+ <WelcomeCard onOpenPalette={() => setPaletteOpen(true)} />
482
+ )}
483
+ {selectedNodeIds.value.size > 0 && <SelectionBar />}
484
+ {contextPinnedNodeIds.value.size > 0 && <ContextPinBar />}
485
+ {expandedNodeId.value && <ExpandedNodeOverlay />}
486
+ <SnapshotPanel open={snapshotOpen} onClose={handleCloseSnapshot} anchorRef={snapshotBtnRef} />
487
+ {minimapVisible && (
488
+ <Minimap
489
+ viewport={viewport}
490
+ nodes={nodes}
491
+ edges={edges}
492
+ onNavigate={handleMinimapNavigate}
493
+ containerWidth={window.innerWidth}
494
+ containerHeight={window.innerHeight}
495
+ />
496
+ )}
497
+ {menu && <ContextMenu menu={menu} onClose={closeMenu} />}
498
+ {paletteOpen && (
499
+ <CommandPalette
500
+ onClose={() => setPaletteOpen(false)}
501
+ onToggleMinimap={handleToggleMinimap}
502
+ />
503
+ )}
504
+ {shortcutsOpen && <ShortcutOverlay onClose={() => setShortcutsOpen(false)} />}
505
+ </div>
506
+ );
507
+ }
@@ -0,0 +1,81 @@
1
+ import {
2
+ attentionHistory,
3
+ attentionHistoryOpen,
4
+ attentionHistoryUnread,
5
+ closeAttentionHistory,
6
+ openAttentionHistory,
7
+ } from '../state/attention-store';
8
+
9
+ function formatTimestamp(timestamp: number): string {
10
+ return new Date(timestamp).toLocaleTimeString([], {
11
+ hour: 'numeric',
12
+ minute: '2-digit',
13
+ });
14
+ }
15
+
16
+ export function AttentionHistory() {
17
+ const entries = attentionHistory.value;
18
+ if (entries.length === 0) return null;
19
+
20
+ const isOpen = attentionHistoryOpen.value;
21
+ const unread = attentionHistoryUnread.value;
22
+
23
+ if (!isOpen) {
24
+ return (
25
+ <button
26
+ type="button"
27
+ class="attention-history-tab"
28
+ onClick={openAttentionHistory}
29
+ aria-label={unread > 0 ? `Recent updates — ${unread} new` : 'Recent updates'}
30
+ title={unread > 0 ? `${unread} new updates since last viewed` : 'Recent updates'}
31
+ >
32
+ <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
33
+ <rect x="1.5" y="2.5" width="13" height="11" rx="1.5" />
34
+ <circle cx="4.5" cy="8" r="1.1" fill="currentColor" stroke="none" />
35
+ <line x1="6.5" y1="6.5" x2="12.5" y2="6.5" />
36
+ <line x1="6.5" y1="8" x2="11" y2="8" />
37
+ <line x1="6.5" y1="9.5" x2="12" y2="9.5" />
38
+ </svg>
39
+ <span class="attention-history-tab-label">Updates</span>
40
+ {unread > 0 && (
41
+ <span class="attention-history-tab-badge" aria-hidden="true">
42
+ {unread > 9 ? '9+' : unread}
43
+ </span>
44
+ )}
45
+ </button>
46
+ );
47
+ }
48
+
49
+ return (
50
+ <aside class="attention-history" aria-label="Recent semantic changes">
51
+ <div class="attention-history-header">
52
+ <div class="attention-history-header-text">
53
+ <span class="attention-history-title">Recent Updates</span>
54
+ <span class="attention-history-subtitle">Focus and meaning shifts</span>
55
+ </div>
56
+ <button
57
+ type="button"
58
+ class="attention-history-close"
59
+ onClick={closeAttentionHistory}
60
+ aria-label="Collapse changes panel"
61
+ title="Collapse"
62
+ >
63
+ ×
64
+ </button>
65
+ </div>
66
+ <div class="attention-history-list">
67
+ {entries.map((entry) => (
68
+ <article key={entry.id} class={`attention-history-entry attention-tone-${entry.tone}`}>
69
+ <div class="attention-history-meta">
70
+ <span class="attention-history-kind">{entry.title}</span>
71
+ <time class="attention-history-time" dateTime={new Date(entry.createdAt).toISOString()}>
72
+ {formatTimestamp(entry.createdAt)}
73
+ </time>
74
+ </div>
75
+ <p class="attention-history-detail">{entry.detail}</p>
76
+ </article>
77
+ ))}
78
+ </div>
79
+ </aside>
80
+ );
81
+ }
@@ -0,0 +1,19 @@
1
+ import { attentionToast, openAttentionHistory } from '../state/attention-store';
2
+
3
+ export function AttentionToast() {
4
+ const toast = attentionToast.value;
5
+ if (!toast) return null;
6
+
7
+ return (
8
+ <button
9
+ type="button"
10
+ class={`attention-toast attention-tone-${toast.tone}`}
11
+ onClick={openAttentionHistory}
12
+ aria-label={`${toast.title} — open change history`}
13
+ title={toast.detail || toast.title}
14
+ >
15
+ <span class="attention-toast-dot" aria-hidden="true" />
16
+ <span class="attention-toast-title">{toast.title}</span>
17
+ </button>
18
+ );
19
+ }