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,601 @@
1
+ import type { ComponentChildren } from 'preact';
2
+ import { useCallback, useEffect, useState } from 'preact/hooks';
3
+ import {
4
+ contextPinnedNodeIds,
5
+ dockNode,
6
+ edges,
7
+ expandNode,
8
+ focusNode,
9
+ nodes,
10
+ pendingConnection,
11
+ removeNode,
12
+ toggleCollapsed,
13
+ toggleContextPin,
14
+ undockNode,
15
+ updateNode,
16
+ } from '../state/canvas-store';
17
+ import {
18
+ createEdgeFromClient,
19
+ createGroupFromClient,
20
+ createNodeFromClient,
21
+ refreshWebpageNodeFromClient,
22
+ removeNodeFromClient,
23
+ sendIntent,
24
+ ungroupFromClient,
25
+ updateNodeFromClient,
26
+ } from '../state/intent-bridge';
27
+ import { EXPANDABLE_TYPES } from '../types';
28
+ import type { CanvasNodeState } from '../types';
29
+
30
+ export type MenuState =
31
+ | {
32
+ kind: 'node';
33
+ x: number;
34
+ y: number;
35
+ nodeId: string;
36
+ }
37
+ | {
38
+ kind: 'canvas';
39
+ x: number;
40
+ y: number;
41
+ canvasX: number;
42
+ canvasY: number;
43
+ };
44
+
45
+ export function useContextMenu() {
46
+ const [menu, setMenu] = useState<MenuState | null>(null);
47
+
48
+ const openNodeMenu = useCallback((e: MouseEvent, nodeId: string) => {
49
+ e.preventDefault();
50
+ e.stopPropagation();
51
+ setMenu({ kind: 'node', x: e.clientX, y: e.clientY, nodeId });
52
+ }, []);
53
+
54
+ const openCanvasMenu = useCallback((e: MouseEvent, canvasX: number, canvasY: number) => {
55
+ e.preventDefault();
56
+ e.stopPropagation();
57
+ setMenu({ kind: 'canvas', x: e.clientX, y: e.clientY, canvasX, canvasY });
58
+ }, []);
59
+
60
+ const closeMenu = useCallback(() => setMenu(null), []);
61
+
62
+ useEffect(() => {
63
+ if (!menu) return;
64
+ const handleClick = () => setMenu(null);
65
+ const handleKey = (e: KeyboardEvent) => {
66
+ if (e.key === 'Escape') setMenu(null);
67
+ };
68
+ document.addEventListener('click', handleClick);
69
+ document.addEventListener('keydown', handleKey);
70
+ return () => {
71
+ document.removeEventListener('click', handleClick);
72
+ document.removeEventListener('keydown', handleKey);
73
+ };
74
+ }, [menu]);
75
+
76
+ return { menu, openNodeMenu, openCanvasMenu, closeMenu };
77
+ }
78
+
79
+ interface ContextMenuProps {
80
+ menu: MenuState;
81
+ onClose: () => void;
82
+ }
83
+
84
+ const DEFAULT_GROUP_COLOR = '#4bbcFF';
85
+
86
+ const GROUP_COLOR_PRESETS = [
87
+ { label: 'Blue', value: '#3b82f6' },
88
+ { label: 'Green', value: '#22c55e' },
89
+ { label: 'Yellow', value: '#eab308' },
90
+ { label: 'Red', value: '#ef4444' },
91
+ { label: 'Gray', value: '#6b7280' },
92
+ { label: 'Purple', value: '#a855f7' },
93
+ ] as const;
94
+
95
+ export function ContextMenu({ menu, onClose }: ContextMenuProps) {
96
+ let items: MenuItem[];
97
+ if (menu.kind === 'node') {
98
+ const node = nodes.value.get(menu.nodeId);
99
+ if (!node) return null;
100
+ items = buildNodeMenuItems(node);
101
+ } else {
102
+ items = buildCanvasMenuItems(menu.canvasX, menu.canvasY);
103
+ }
104
+ const keyCounts = new Map<string, number>();
105
+ const estimatedHeight = items.some((item) => item.render)
106
+ ? items.length * 32 + 168
107
+ : items.length * 32 + 8;
108
+
109
+ // Keep menu on screen
110
+ const adjustedX = Math.min(menu.x, Math.max(12, window.innerWidth - 240));
111
+ const adjustedY = Math.min(menu.y, Math.max(12, window.innerHeight - estimatedHeight));
112
+
113
+ return (
114
+ <div
115
+ class="context-menu"
116
+ style={{
117
+ position: 'fixed',
118
+ left: `${adjustedX}px`,
119
+ top: `${adjustedY}px`,
120
+ zIndex: 10000,
121
+ }}
122
+ onPointerDown={(e) => e.stopPropagation()}
123
+ onClick={(e) => e.stopPropagation()}
124
+ >
125
+ {items.map((item) => {
126
+ const baseKey = item.separator
127
+ ? 'separator'
128
+ : item.render
129
+ ? 'custom'
130
+ : `${item.label ?? 'item'}:${item.shortcut ?? ''}`;
131
+ const nextCount = (keyCounts.get(baseKey) ?? 0) + 1;
132
+ keyCounts.set(baseKey, nextCount);
133
+ const itemKey = `${baseKey}:${nextCount}`;
134
+
135
+ if (item.separator) {
136
+ return <div key={itemKey} class="context-menu-separator" />;
137
+ }
138
+
139
+ if (item.render) {
140
+ return (
141
+ <div key={itemKey} class="context-menu-custom">
142
+ {item.render(onClose)}
143
+ </div>
144
+ );
145
+ }
146
+
147
+ return (
148
+ <button
149
+ key={itemKey}
150
+ type="button"
151
+ class="context-menu-item"
152
+ onClick={() => {
153
+ item.action?.();
154
+ onClose();
155
+ }}
156
+ >
157
+ <span class="context-menu-label">{item.label}</span>
158
+ {item.shortcut && <span class="context-menu-shortcut">{item.shortcut}</span>}
159
+ </button>
160
+ );
161
+ })}
162
+ </div>
163
+ );
164
+ }
165
+
166
+ interface MenuItem {
167
+ label?: string;
168
+ shortcut?: string;
169
+ action?: () => void;
170
+ separator?: boolean;
171
+ render?: (onClose: () => void) => ComponentChildren;
172
+ }
173
+
174
+ function getNodeLocalPath(node: CanvasNodeState): string | null {
175
+ const path = typeof node.data.path === 'string' ? node.data.path.trim() : '';
176
+ return path || null;
177
+ }
178
+
179
+ function normalizeHexColor(color: string): string {
180
+ const trimmed = color.trim().toLowerCase();
181
+ const shortMatch = trimmed.match(/^#([0-9a-f]{3})$/);
182
+ if (shortMatch) {
183
+ const [r, g, b] = shortMatch[1].split('');
184
+ return `#${r}${r}${g}${g}${b}${b}`;
185
+ }
186
+ return trimmed;
187
+ }
188
+
189
+ function currentGroupColor(node: CanvasNodeState): string | null {
190
+ if (typeof node.data.color !== 'string' || !node.data.color.trim()) return null;
191
+ return normalizeHexColor(node.data.color);
192
+ }
193
+
194
+ function groupColorInputValue(node: CanvasNodeState): string {
195
+ const color = currentGroupColor(node);
196
+ return color && /^#[0-9a-f]{6}$/.test(color) ? color : normalizeHexColor(DEFAULT_GROUP_COLOR);
197
+ }
198
+
199
+ function applyGroupColor(node: CanvasNodeState, color: string | null): void {
200
+ const nextColor = color ? normalizeHexColor(color) : null;
201
+ updateNode(node.id, { data: { ...node.data, color: nextColor } });
202
+ void updateNodeFromClient(node.id, { data: { color: nextColor } });
203
+ }
204
+
205
+ function renderGroupColorSection(node: CanvasNodeState, onClose: () => void): ComponentChildren {
206
+ const activeColor = currentGroupColor(node);
207
+
208
+ return (
209
+ <div class="context-menu-section">
210
+ <div class="context-menu-section-header">
211
+ <span class="context-menu-section-label">Group color</span>
212
+ <button
213
+ type="button"
214
+ class="context-menu-reset"
215
+ onClick={() => {
216
+ applyGroupColor(node, null);
217
+ onClose();
218
+ }}
219
+ >
220
+ Theme default
221
+ </button>
222
+ </div>
223
+
224
+ <div class="context-menu-color-grid">
225
+ {GROUP_COLOR_PRESETS.map((preset) => {
226
+ const normalizedPreset = normalizeHexColor(preset.value);
227
+ const active = activeColor === normalizedPreset;
228
+ return (
229
+ <button
230
+ key={preset.value}
231
+ type="button"
232
+ class={`context-menu-color-swatch${active ? ' active' : ''}`}
233
+ aria-label={`Set group color to ${preset.label}`}
234
+ title={preset.label}
235
+ style={{ '--swatch-color': preset.value }}
236
+ onClick={() => {
237
+ applyGroupColor(node, preset.value);
238
+ onClose();
239
+ }}
240
+ >
241
+ <span
242
+ class="context-menu-color-dot"
243
+ style={{ '--swatch-color': preset.value }}
244
+ />
245
+ <span>{preset.label}</span>
246
+ </button>
247
+ );
248
+ })}
249
+ </div>
250
+
251
+ <label class="context-menu-color-custom">
252
+ <span>Custom</span>
253
+ <input
254
+ type="color"
255
+ class="context-menu-color-input"
256
+ aria-label="Custom group color"
257
+ value={groupColorInputValue(node)}
258
+ onClick={(e) => e.stopPropagation()}
259
+ onInput={(e) => {
260
+ applyGroupColor(node, (e.currentTarget as HTMLInputElement).value);
261
+ onClose();
262
+ }}
263
+ />
264
+ </label>
265
+ </div>
266
+ );
267
+ }
268
+
269
+ function centeredPosition(canvasX: number, canvasY: number, width: number, height: number): {
270
+ x: number;
271
+ y: number;
272
+ } {
273
+ return {
274
+ x: canvasX - width / 2,
275
+ y: canvasY - height / 2,
276
+ };
277
+ }
278
+
279
+ function titleFromResource(raw: string, fallback: string): string {
280
+ const trimmed = raw.trim().replace(/[?#].*$/, '');
281
+ const parts = trimmed.split('/').filter(Boolean);
282
+ return parts.at(-1) || fallback;
283
+ }
284
+
285
+ async function createPromptedNode(opts: {
286
+ promptLabel: string;
287
+ type: 'webpage' | 'file' | 'image';
288
+ canvasX: number;
289
+ canvasY: number;
290
+ width: number;
291
+ height: number;
292
+ placeholder?: string;
293
+ titleFallback: string;
294
+ errorMessage: string;
295
+ }): Promise<void> {
296
+ const value = window.prompt(opts.promptLabel, opts.placeholder ?? '');
297
+ if (!value) return;
298
+ const trimmed = value.trim();
299
+ if (!trimmed) return;
300
+
301
+ const position = centeredPosition(opts.canvasX, opts.canvasY, opts.width, opts.height);
302
+ const result = await createNodeFromClient({
303
+ type: opts.type,
304
+ ...(opts.type !== 'webpage' ? { title: titleFromResource(trimmed, opts.titleFallback) } : {}),
305
+ content: trimmed,
306
+ x: position.x,
307
+ y: position.y,
308
+ width: opts.width,
309
+ height: opts.height,
310
+ });
311
+
312
+ if (!result.ok) {
313
+ window.alert(opts.errorMessage);
314
+ }
315
+ }
316
+
317
+ function buildCanvasMenuItems(canvasX: number, canvasY: number): MenuItem[] {
318
+ return [
319
+ {
320
+ label: 'New note',
321
+ action: () => {
322
+ const width = 360;
323
+ const height = 200;
324
+ const position = centeredPosition(canvasX, canvasY, width, height);
325
+ void createNodeFromClient({
326
+ type: 'markdown',
327
+ title: 'New note',
328
+ x: position.x,
329
+ y: position.y,
330
+ width,
331
+ height,
332
+ });
333
+ },
334
+ },
335
+ {
336
+ label: 'Open webpage...',
337
+ action: () => {
338
+ void createPromptedNode({
339
+ promptLabel: 'Webpage URL:',
340
+ type: 'webpage',
341
+ canvasX,
342
+ canvasY,
343
+ width: 520,
344
+ height: 420,
345
+ placeholder: 'https://example.com',
346
+ titleFallback: 'Webpage',
347
+ errorMessage: 'Could not create webpage node. Enter a valid http(s) URL.',
348
+ });
349
+ },
350
+ },
351
+ {
352
+ label: 'Open file...',
353
+ action: () => {
354
+ void createPromptedNode({
355
+ promptLabel: 'Workspace path or absolute file path:',
356
+ type: 'file',
357
+ canvasX,
358
+ canvasY,
359
+ width: 720,
360
+ height: 500,
361
+ placeholder: 'src/server/server.ts',
362
+ titleFallback: 'File',
363
+ errorMessage: 'Could not create file node. Check the path and try again.',
364
+ });
365
+ },
366
+ },
367
+ {
368
+ label: 'Open image...',
369
+ action: () => {
370
+ void createPromptedNode({
371
+ promptLabel: 'Image path, URL, or data URI:',
372
+ type: 'image',
373
+ canvasX,
374
+ canvasY,
375
+ width: 480,
376
+ height: 360,
377
+ placeholder: 'artifacts/architecture.png',
378
+ titleFallback: 'Image',
379
+ errorMessage: 'Could not create image node. Check the image source and try again.',
380
+ });
381
+ },
382
+ },
383
+ { separator: true },
384
+ {
385
+ label: 'New group',
386
+ action: () => {
387
+ const width = 600;
388
+ const height = 400;
389
+ const position = centeredPosition(canvasX, canvasY, width, height);
390
+ void createGroupFromClient({
391
+ title: 'Group',
392
+ x: position.x,
393
+ y: position.y,
394
+ width,
395
+ height,
396
+ });
397
+ },
398
+ },
399
+ ];
400
+ }
401
+
402
+ function buildNodeMenuItems(node: CanvasNodeState): MenuItem[] {
403
+ const items: MenuItem[] = [];
404
+ const localPath = getNodeLocalPath(node);
405
+
406
+ // S2: Delegate to focusNode() which centers, brings to front, and persists
407
+ items.push({
408
+ label: 'Focus',
409
+ action: () => focusNode(node.id),
410
+ });
411
+
412
+ // Expand into full-viewport overlay for focused work
413
+ if (EXPANDABLE_TYPES.has(node.type)) {
414
+ items.push({
415
+ label: 'Expand',
416
+ shortcut: '⤢',
417
+ action: () => expandNode(node.id),
418
+ });
419
+ }
420
+
421
+ // Collapse/Expand
422
+ items.push({
423
+ label: node.collapsed ? 'Expand' : 'Collapse',
424
+ action: () => toggleCollapsed(node.id),
425
+ });
426
+
427
+ // Pin/Unpin
428
+ items.push({
429
+ label: node.pinned ? 'Unpin' : 'Pin (exclude from auto-arrange)',
430
+ action: () => {
431
+ const pinned = !node.pinned;
432
+ updateNode(node.id, { pinned });
433
+ void updateNodeFromClient(node.id, { pinned });
434
+ },
435
+ });
436
+
437
+ // Context pin — add/remove from persistent agent context
438
+ const isCtxPinned = contextPinnedNodeIds.value.has(node.id);
439
+ items.push({
440
+ label: isCtxPinned ? 'Remove from context' : 'Add to context',
441
+ action: () => toggleContextPin(node.id),
442
+ });
443
+
444
+ // ── Edge connection ──
445
+ const pending = pendingConnection.value;
446
+ if (pending && pending.from !== node.id) {
447
+ const sourceNode = nodes.value.get(pending.from);
448
+ const sourceTitle = sourceNode
449
+ ? ((sourceNode.data.title as string) || sourceNode.id).slice(0, 20)
450
+ : pending.from;
451
+ items.push({
452
+ label: `Connect from "${sourceTitle}"`,
453
+ action: () => {
454
+ createEdgeFromClient(pending.from, node.id, 'relation');
455
+ pendingConnection.value = null;
456
+ },
457
+ });
458
+ }
459
+
460
+ items.push({
461
+ label: pending ? 'Connect from here (replace)' : 'Connect from here',
462
+ action: () => {
463
+ pendingConnection.value = { from: node.id };
464
+ },
465
+ });
466
+
467
+ // Show edge count
468
+ const edgeCount = Array.from(edges.value.values()).filter(
469
+ (e) => e.from === node.id || e.to === node.id,
470
+ ).length;
471
+ if (edgeCount > 0) {
472
+ items.push({
473
+ label: `${edgeCount} edge${edgeCount !== 1 ? 's' : ''} connected`,
474
+ });
475
+ }
476
+
477
+ items.push({ separator: true });
478
+
479
+ // Type-specific
480
+ if ((node.type === 'markdown' || node.type === 'file' || node.type === 'image') && localPath) {
481
+ items.push({
482
+ label: 'Open in browser',
483
+ action: () => {
484
+ window.open(`/artifact?path=${encodeURIComponent(localPath)}`, '_blank', 'noopener');
485
+ },
486
+ });
487
+ items.push({
488
+ label: 'Copy path',
489
+ action: () => {
490
+ navigator.clipboard.writeText(localPath);
491
+ },
492
+ });
493
+ }
494
+
495
+ if (node.type === 'mcp-app' || node.type === 'json-render' || node.type === 'graph') {
496
+ if (node.data.chartConfig) {
497
+ // Chart ext-app node — chart-specific actions
498
+ const chartTitle =
499
+ ((node.data.chartConfig as Record<string, unknown>).title as string) || 'chart';
500
+ items.push({
501
+ label: 'Copy chart data',
502
+ action: () => {
503
+ navigator.clipboard.writeText(JSON.stringify(node.data.chartConfig, null, 2));
504
+ },
505
+ });
506
+ } else {
507
+ // Regular MCP app node
508
+ const url = node.data.url as string;
509
+ items.push({
510
+ label: 'Open in browser',
511
+ action: () => {
512
+ if (url) window.open(url, '_blank');
513
+ },
514
+ });
515
+ items.push({
516
+ label: 'Focus in TUI',
517
+ action: () => sendIntent('mcp-app-focus', { url }),
518
+ });
519
+ if (node.type === 'json-render' || node.type === 'graph') {
520
+ items.push({
521
+ label: 'Copy spec',
522
+ action: () => {
523
+ navigator.clipboard.writeText(
524
+ JSON.stringify(node.data.spec ?? node.data.graphConfig ?? {}, null, 2),
525
+ );
526
+ },
527
+ });
528
+ }
529
+ }
530
+ }
531
+
532
+ if (node.type === 'webpage') {
533
+ const url = typeof node.data.url === 'string' ? node.data.url : '';
534
+ items.push({
535
+ label: 'Refresh webpage',
536
+ action: () => {
537
+ void refreshWebpageNodeFromClient(node.id);
538
+ },
539
+ });
540
+ items.push({
541
+ label: 'Open in browser',
542
+ action: () => {
543
+ if (url) window.open(url, '_blank', 'noopener');
544
+ },
545
+ });
546
+ items.push({
547
+ label: 'Copy URL',
548
+ action: () => {
549
+ if (url) navigator.clipboard.writeText(url);
550
+ },
551
+ });
552
+ }
553
+
554
+ // Group-specific actions
555
+ if (node.type === 'group') {
556
+ const childIds = (node.data.children as string[]) ?? [];
557
+ items.push({ separator: true });
558
+ items.push({
559
+ render: (onClose) => renderGroupColorSection(node, onClose),
560
+ });
561
+ if (childIds.length > 0) {
562
+ items.push({
563
+ label: `Ungroup (${childIds.length} node${childIds.length !== 1 ? 's' : ''})`,
564
+ action: () => ungroupFromClient(node.id),
565
+ });
566
+ }
567
+ }
568
+
569
+ // Dock/undock for status and ledger nodes
570
+ if (node.type === 'status' || node.type === 'ledger') {
571
+ items.push({ separator: true });
572
+ if (node.dockPosition !== null) {
573
+ items.push({
574
+ label: 'Undock to canvas',
575
+ action: () => undockNode(node.id),
576
+ });
577
+ } else {
578
+ items.push({
579
+ label: 'Dock left of toolbar',
580
+ action: () => dockNode(node.id, 'left'),
581
+ });
582
+ items.push({
583
+ label: 'Dock right of toolbar',
584
+ action: () => dockNode(node.id, 'right'),
585
+ });
586
+ }
587
+ }
588
+
589
+ if (node.type !== 'status') {
590
+ items.push({ separator: true });
591
+ items.push({
592
+ label: 'Close',
593
+ action: () => {
594
+ removeNode(node.id);
595
+ void removeNodeFromClient(node.id);
596
+ },
597
+ });
598
+ }
599
+
600
+ return items;
601
+ }
@@ -0,0 +1,25 @@
1
+ import {
2
+ clearContextPins,
3
+ contextPinnedNodeIds,
4
+ } from '../state/canvas-store';
5
+
6
+ export function ContextPinBar() {
7
+ const count = contextPinnedNodeIds.value.size;
8
+ if (count === 0) return null;
9
+
10
+ return (
11
+ <div class="context-pin-bar">
12
+ <span class="context-pin-bar-count">
13
+ {'\u2726'} {count} node{count !== 1 ? 's' : ''} in context
14
+ </span>
15
+ <button
16
+ type="button"
17
+ class="context-pin-bar-btn context-pin-bar-clear"
18
+ onClick={clearContextPins}
19
+ title="Clear all context pins"
20
+ >
21
+ {'\u00d7'}
22
+ </button>
23
+ </div>
24
+ );
25
+ }
@@ -0,0 +1,22 @@
1
+ import { clearContextPins, contextPinnedNodeIds } from '../state/canvas-store';
2
+
3
+ export function ContextPinHud() {
4
+ const count = contextPinnedNodeIds.value.size;
5
+ if (count === 0) return null;
6
+
7
+ return (
8
+ <div class="context-pin-hud">
9
+ <span class="context-pin-hud-label">
10
+ {'\u2726'} {count} in context
11
+ </span>
12
+ <button
13
+ type="button"
14
+ class="context-pin-hud-clear"
15
+ onClick={clearContextPins}
16
+ title="Clear all context pins"
17
+ >
18
+ ×
19
+ </button>
20
+ </div>
21
+ );
22
+ }
@@ -0,0 +1,66 @@
1
+ import { ContextNode } from '../nodes/ContextNode';
2
+ import { LedgerNode } from '../nodes/LedgerNode';
3
+ import { StatusNode } from '../nodes/StatusNode';
4
+ import { StatusSummary } from '../nodes/StatusSummary';
5
+ import { toggleCollapsed, undockNode } from '../state/canvas-store';
6
+ import { TYPE_LABELS } from '../types';
7
+ import type { CanvasNodeState } from '../types';
8
+
9
+ function renderDockedContent(node: CanvasNodeState) {
10
+ switch (node.type) {
11
+ case 'status':
12
+ return <StatusNode node={node} />;
13
+ case 'ledger':
14
+ return <LedgerNode node={node} />;
15
+ case 'context':
16
+ return <ContextNode node={node} />;
17
+ default:
18
+ return null;
19
+ }
20
+ }
21
+
22
+ export function DockedNode({ node }: { node: CanvasNodeState }) {
23
+ return (
24
+ <div class="docked-node">
25
+ <div class="docked-node-header">
26
+ <span class="node-type-badge">{TYPE_LABELS[node.type] ?? node.type}</span>
27
+ {node.type === 'status' && node.collapsed && <StatusSummary node={node} />}
28
+ {node.type === 'context' && node.collapsed && (
29
+ <span style={{ fontSize: '11px', color: 'var(--c-muted)' }}>
30
+ Active Agent Context
31
+ {typeof node.data.utilization === 'number' && (
32
+ <> · {Math.round(Number(node.data.utilization) * 100)}%</>
33
+ )}
34
+ </span>
35
+ )}
36
+ <div class="docked-node-controls">
37
+ <button
38
+ type="button"
39
+ onClick={(e) => {
40
+ e.stopPropagation();
41
+ toggleCollapsed(node.id);
42
+ }}
43
+ title={node.collapsed ? 'Expand' : 'Collapse'}
44
+ >
45
+ {node.collapsed ? '\u25B8' : '\u25BE'}
46
+ </button>
47
+ <button
48
+ type="button"
49
+ onClick={(e) => {
50
+ e.stopPropagation();
51
+ undockNode(node.id);
52
+ }}
53
+ title="Undock to canvas"
54
+ >
55
+ {'\u2299'}
56
+ </button>
57
+ </div>
58
+ </div>
59
+ {!node.collapsed && (
60
+ <div class={`docked-node-body${node.type === 'context' ? ' context-body' : ''}`}>
61
+ {renderDockedContent(node)}
62
+ </div>
63
+ )}
64
+ </div>
65
+ );
66
+ }