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,439 @@
1
+ export interface ArrangePosition {
2
+ x: number;
3
+ y: number;
4
+ }
5
+
6
+ export interface ArrangeSize {
7
+ width: number;
8
+ height: number;
9
+ }
10
+
11
+ export interface ArrangeNode {
12
+ id: string;
13
+ type: string;
14
+ position: ArrangePosition;
15
+ size: ArrangeSize;
16
+ pinned: boolean;
17
+ dockPosition: 'left' | 'right' | null;
18
+ data: Record<string, unknown>;
19
+ }
20
+
21
+ export interface ArrangeEdge {
22
+ id: string;
23
+ from: string;
24
+ to: string;
25
+ }
26
+
27
+ export interface AutoArrangeResult {
28
+ nodePositions: Map<string, ArrangePosition>;
29
+ groupBounds: Map<string, ArrangePosition & ArrangeSize>;
30
+ }
31
+
32
+ type ArrangeMode = 'grid' | 'graph';
33
+
34
+ interface ArrangeUnit {
35
+ id: string;
36
+ memberIds: string[];
37
+ origin: ArrangePosition;
38
+ size: ArrangeSize;
39
+ sortKey: { x: number; y: number };
40
+ groupId?: string;
41
+ }
42
+
43
+ interface ComponentLayout {
44
+ positions: Map<string, ArrangePosition>;
45
+ size: ArrangeSize;
46
+ sortKey: { x: number; y: number };
47
+ }
48
+
49
+ const START_X = 40;
50
+ const START_Y = 80;
51
+ const UNIT_GAP_X = 96;
52
+ const UNIT_GAP_Y = 72;
53
+ const COMPONENT_GAP_X = 220;
54
+ const COMPONENT_GAP_Y = 180;
55
+ const MAX_ROW_WIDTH = 3200;
56
+ const GROUP_PAD = 40;
57
+ const GROUP_TITLEBAR_HEIGHT = 32;
58
+
59
+ function computeGroupBounds(rects: Array<{ position: ArrangePosition; size: ArrangeSize }>): (ArrangePosition & ArrangeSize) | null {
60
+ if (rects.length === 0) return null;
61
+
62
+ let minX = Number.POSITIVE_INFINITY;
63
+ let minY = Number.POSITIVE_INFINITY;
64
+ let maxX = Number.NEGATIVE_INFINITY;
65
+ let maxY = Number.NEGATIVE_INFINITY;
66
+
67
+ for (const rect of rects) {
68
+ minX = Math.min(minX, rect.position.x);
69
+ minY = Math.min(minY, rect.position.y);
70
+ maxX = Math.max(maxX, rect.position.x + rect.size.width);
71
+ maxY = Math.max(maxY, rect.position.y + rect.size.height);
72
+ }
73
+
74
+ if (!Number.isFinite(minX) || !Number.isFinite(minY)) return null;
75
+
76
+ return {
77
+ x: minX - GROUP_PAD,
78
+ y: minY - GROUP_PAD - GROUP_TITLEBAR_HEIGHT,
79
+ width: maxX - minX + GROUP_PAD * 2,
80
+ height: maxY - minY + GROUP_PAD * 2 + GROUP_TITLEBAR_HEIGHT,
81
+ };
82
+ }
83
+
84
+ function buildArrangeUnits(allNodes: ArrangeNode[]): {
85
+ units: ArrangeUnit[];
86
+ nodesById: Map<string, ArrangeNode>;
87
+ nodeToUnit: Map<string, string>;
88
+ } {
89
+ const movable = allNodes.filter((node) => !node.pinned && node.dockPosition === null);
90
+ const nodesById = new Map(movable.map((node) => [node.id, node]));
91
+ const nodeToUnit = new Map<string, string>();
92
+ const units: ArrangeUnit[] = [];
93
+
94
+ const groupChildren = new Map<string, ArrangeNode[]>();
95
+ for (const node of movable) {
96
+ if (node.type === 'group') continue;
97
+ const parentGroupId = typeof node.data.parentGroup === 'string' ? node.data.parentGroup : '';
98
+ const parent = parentGroupId ? nodesById.get(parentGroupId) : undefined;
99
+ if (!parent || parent.type !== 'group') continue;
100
+ if (!groupChildren.has(parentGroupId)) groupChildren.set(parentGroupId, []);
101
+ groupChildren.get(parentGroupId)!.push(node);
102
+ }
103
+
104
+ const groups = movable
105
+ .filter((node) => node.type === 'group')
106
+ .sort((a, b) => a.position.y - b.position.y || a.position.x - b.position.x);
107
+
108
+ for (const group of groups) {
109
+ const children = groupChildren.get(group.id) ?? [];
110
+ if (children.length === 0) {
111
+ const unitId = `unit:${group.id}`;
112
+ units.push({
113
+ id: unitId,
114
+ memberIds: [group.id],
115
+ origin: { ...group.position },
116
+ size: { ...group.size },
117
+ sortKey: { x: group.position.x, y: group.position.y },
118
+ });
119
+ nodeToUnit.set(group.id, unitId);
120
+ continue;
121
+ }
122
+
123
+ const childRects = children.map((child) => ({ position: child.position, size: child.size }));
124
+ const bounds = computeGroupBounds(childRects);
125
+ if (!bounds) continue;
126
+
127
+ const unitId = `group:${group.id}`;
128
+ units.push({
129
+ id: unitId,
130
+ memberIds: children.map((child) => child.id),
131
+ origin: { x: bounds.x, y: bounds.y },
132
+ size: { width: bounds.width, height: bounds.height },
133
+ sortKey: { x: bounds.x, y: bounds.y },
134
+ groupId: group.id,
135
+ });
136
+ nodeToUnit.set(group.id, unitId);
137
+ for (const child of children) {
138
+ nodeToUnit.set(child.id, unitId);
139
+ }
140
+ }
141
+
142
+ const remaining = movable
143
+ .filter((node) => !nodeToUnit.has(node.id))
144
+ .sort((a, b) => a.position.y - b.position.y || a.position.x - b.position.x);
145
+
146
+ for (const node of remaining) {
147
+ const unitId = `unit:${node.id}`;
148
+ units.push({
149
+ id: unitId,
150
+ memberIds: [node.id],
151
+ origin: { ...node.position },
152
+ size: { ...node.size },
153
+ sortKey: { x: node.position.x, y: node.position.y },
154
+ });
155
+ nodeToUnit.set(node.id, unitId);
156
+ }
157
+
158
+ return { units, nodesById, nodeToUnit };
159
+ }
160
+
161
+ function buildUnitGraphs(units: ArrangeUnit[], nodeToUnit: Map<string, string>, edges: ArrangeEdge[]): {
162
+ outgoing: Map<string, Set<string>>;
163
+ undirected: Map<string, Set<string>>;
164
+ indegree: Map<string, number>;
165
+ outdegree: Map<string, number>;
166
+ } {
167
+ const outgoing = new Map<string, Set<string>>();
168
+ const undirected = new Map<string, Set<string>>();
169
+ const indegree = new Map<string, number>();
170
+ const outdegree = new Map<string, number>();
171
+
172
+ for (const unit of units) {
173
+ outgoing.set(unit.id, new Set());
174
+ undirected.set(unit.id, new Set());
175
+ indegree.set(unit.id, 0);
176
+ outdegree.set(unit.id, 0);
177
+ }
178
+
179
+ for (const edge of edges) {
180
+ const fromUnit = nodeToUnit.get(edge.from);
181
+ const toUnit = nodeToUnit.get(edge.to);
182
+ if (!fromUnit || !toUnit || fromUnit === toUnit) continue;
183
+
184
+ if (!outgoing.get(fromUnit)!.has(toUnit)) {
185
+ outgoing.get(fromUnit)!.add(toUnit);
186
+ outdegree.set(fromUnit, (outdegree.get(fromUnit) ?? 0) + 1);
187
+ indegree.set(toUnit, (indegree.get(toUnit) ?? 0) + 1);
188
+ }
189
+ undirected.get(fromUnit)!.add(toUnit);
190
+ undirected.get(toUnit)!.add(fromUnit);
191
+ }
192
+
193
+ return { outgoing, undirected, indegree, outdegree };
194
+ }
195
+
196
+ function collectComponents(units: ArrangeUnit[], undirected: Map<string, Set<string>>): ArrangeUnit[][] {
197
+ const unitsById = new Map(units.map((unit) => [unit.id, unit]));
198
+ const seen = new Set<string>();
199
+ const components: ArrangeUnit[][] = [];
200
+
201
+ for (const unit of units) {
202
+ if (seen.has(unit.id)) continue;
203
+ const stack = [unit.id];
204
+ const componentIds: string[] = [];
205
+ seen.add(unit.id);
206
+
207
+ while (stack.length > 0) {
208
+ const current = stack.pop()!;
209
+ componentIds.push(current);
210
+ for (const neighbor of undirected.get(current) ?? []) {
211
+ if (seen.has(neighbor)) continue;
212
+ seen.add(neighbor);
213
+ stack.push(neighbor);
214
+ }
215
+ }
216
+
217
+ const component = componentIds
218
+ .map((id) => unitsById.get(id))
219
+ .filter((entry): entry is ArrangeUnit => entry !== undefined)
220
+ .sort((a, b) => a.sortKey.y - b.sortKey.y || a.sortKey.x - b.sortKey.x);
221
+ components.push(component);
222
+ }
223
+
224
+ return components.sort((a, b) => a[0].sortKey.y - b[0].sortKey.y || a[0].sortKey.x - b[0].sortKey.x);
225
+ }
226
+
227
+ function computeGridComponent(component: ArrangeUnit[]): ComponentLayout {
228
+ const positions = new Map<string, ArrangePosition>();
229
+ let cursorX = 0;
230
+ let cursorY = 0;
231
+ let rowHeight = 0;
232
+ let maxWidth = 0;
233
+ const targetRowWidth = Math.max(
234
+ 900,
235
+ Math.ceil(Math.sqrt(component.reduce((sum, unit) => sum + unit.size.width * unit.size.height, 0))),
236
+ );
237
+
238
+ for (const unit of component) {
239
+ if (cursorX > 0 && cursorX + unit.size.width > targetRowWidth) {
240
+ cursorX = 0;
241
+ cursorY += rowHeight + UNIT_GAP_Y;
242
+ rowHeight = 0;
243
+ }
244
+
245
+ positions.set(unit.id, { x: cursorX, y: cursorY });
246
+ cursorX += unit.size.width + UNIT_GAP_X;
247
+ rowHeight = Math.max(rowHeight, unit.size.height);
248
+ maxWidth = Math.max(maxWidth, cursorX - UNIT_GAP_X);
249
+ }
250
+
251
+ return {
252
+ positions,
253
+ size: {
254
+ width: Math.max(0, maxWidth),
255
+ height: cursorY + rowHeight,
256
+ },
257
+ sortKey: component[0].sortKey,
258
+ };
259
+ }
260
+
261
+ function computeGraphComponent(
262
+ component: ArrangeUnit[],
263
+ outgoing: Map<string, Set<string>>,
264
+ undirected: Map<string, Set<string>>,
265
+ indegree: Map<string, number>,
266
+ outdegree: Map<string, number>,
267
+ ): ComponentLayout {
268
+ if (component.length === 1) {
269
+ return {
270
+ positions: new Map([[component[0].id, { x: 0, y: 0 }]]),
271
+ size: { ...component[0].size },
272
+ sortKey: component[0].sortKey,
273
+ };
274
+ }
275
+
276
+ const componentIds = new Set(component.map((unit) => unit.id));
277
+ const roots = component
278
+ .filter((unit) => (indegree.get(unit.id) ?? 0) === 0 && (outdegree.get(unit.id) ?? 0) > 0)
279
+ .sort((a, b) => a.sortKey.x - b.sortKey.x || a.sortKey.y - b.sortKey.y);
280
+ const seedUnits = roots.length > 0
281
+ ? roots
282
+ : [component.slice().sort((a, b) => a.sortKey.x - b.sortKey.x || a.sortKey.y - b.sortKey.y)[0]];
283
+
284
+ const levels = new Map<string, number>();
285
+ const queue = seedUnits.map((unit) => unit.id);
286
+ for (const unit of seedUnits) levels.set(unit.id, 0);
287
+
288
+ while (queue.length > 0) {
289
+ const current = queue.shift()!;
290
+ const currentLevel = levels.get(current) ?? 0;
291
+ for (const neighbor of undirected.get(current) ?? []) {
292
+ if (!componentIds.has(neighbor) || levels.has(neighbor)) continue;
293
+ levels.set(neighbor, currentLevel + 1);
294
+ queue.push(neighbor);
295
+ }
296
+ }
297
+
298
+ let fallbackLevel = 0;
299
+ for (const unit of component) {
300
+ if (levels.has(unit.id)) continue;
301
+ levels.set(unit.id, fallbackLevel);
302
+ fallbackLevel += 1;
303
+ }
304
+
305
+ const columns = new Map<number, ArrangeUnit[]>();
306
+ for (const unit of component) {
307
+ const level = levels.get(unit.id) ?? 0;
308
+ if (!columns.has(level)) columns.set(level, []);
309
+ columns.get(level)!.push(unit);
310
+ }
311
+
312
+ const orderedLevels = Array.from(columns.keys()).sort((a, b) => a - b);
313
+ for (const level of orderedLevels) {
314
+ columns.get(level)!.sort((a, b) => a.sortKey.y - b.sortKey.y || a.sortKey.x - b.sortKey.x);
315
+ }
316
+
317
+ const columnHeights = new Map<number, number>();
318
+ const columnWidths = new Map<number, number>();
319
+ let maxHeight = 0;
320
+ for (const level of orderedLevels) {
321
+ const levelUnits = columns.get(level)!;
322
+ const height = levelUnits.reduce((sum, unit, index) => sum + unit.size.height + (index > 0 ? UNIT_GAP_Y : 0), 0);
323
+ const width = Math.max(...levelUnits.map((unit) => unit.size.width));
324
+ columnHeights.set(level, height);
325
+ columnWidths.set(level, width);
326
+ maxHeight = Math.max(maxHeight, height);
327
+ }
328
+
329
+ const positions = new Map<string, ArrangePosition>();
330
+ let cursorX = 0;
331
+ for (const level of orderedLevels) {
332
+ const levelUnits = columns.get(level)!;
333
+ const columnHeight = columnHeights.get(level) ?? 0;
334
+ const columnWidth = columnWidths.get(level) ?? 0;
335
+ let cursorY = Math.max(0, (maxHeight - columnHeight) / 2);
336
+
337
+ for (const unit of levelUnits) {
338
+ positions.set(unit.id, {
339
+ x: cursorX,
340
+ y: cursorY,
341
+ });
342
+ cursorY += unit.size.height + UNIT_GAP_Y;
343
+ }
344
+
345
+ cursorX += columnWidth + UNIT_GAP_X;
346
+ }
347
+
348
+ return {
349
+ positions,
350
+ size: {
351
+ width: Math.max(0, cursorX - UNIT_GAP_X),
352
+ height: maxHeight,
353
+ },
354
+ sortKey: component[0].sortKey,
355
+ };
356
+ }
357
+
358
+ function placeComponents(components: ComponentLayout[]): Map<string, ArrangePosition> {
359
+ const absolute = new Map<string, ArrangePosition>();
360
+ let cursorX = START_X;
361
+ let cursorY = START_Y;
362
+ let rowHeight = 0;
363
+
364
+ for (const component of components) {
365
+ if (cursorX > START_X && cursorX + component.size.width > MAX_ROW_WIDTH) {
366
+ cursorX = START_X;
367
+ cursorY += rowHeight + COMPONENT_GAP_Y;
368
+ rowHeight = 0;
369
+ }
370
+
371
+ for (const [unitId, position] of component.positions.entries()) {
372
+ absolute.set(unitId, {
373
+ x: cursorX + position.x,
374
+ y: cursorY + position.y,
375
+ });
376
+ }
377
+
378
+ cursorX += component.size.width + COMPONENT_GAP_X;
379
+ rowHeight = Math.max(rowHeight, component.size.height);
380
+ }
381
+
382
+ return absolute;
383
+ }
384
+
385
+ export function computeAutoArrange(
386
+ allNodes: ArrangeNode[],
387
+ allEdges: ArrangeEdge[],
388
+ mode: ArrangeMode,
389
+ ): AutoArrangeResult {
390
+ const { units, nodesById, nodeToUnit } = buildArrangeUnits(allNodes);
391
+ const nodePositions = new Map<string, ArrangePosition>();
392
+ const groupBounds = new Map<string, ArrangePosition & ArrangeSize>();
393
+
394
+ if (units.length === 0) {
395
+ return { nodePositions, groupBounds };
396
+ }
397
+
398
+ const { outgoing, undirected, indegree, outdegree } = buildUnitGraphs(units, nodeToUnit, allEdges);
399
+ const components = collectComponents(units, undirected).map((component) =>
400
+ mode === 'graph'
401
+ ? computeGraphComponent(component, outgoing, undirected, indegree, outdegree)
402
+ : computeGridComponent(component),
403
+ );
404
+ const absoluteUnitPositions = placeComponents(components);
405
+
406
+ for (const unit of units) {
407
+ const targetOrigin = absoluteUnitPositions.get(unit.id);
408
+ if (!targetOrigin) continue;
409
+ const deltaX = targetOrigin.x - unit.origin.x;
410
+ const deltaY = targetOrigin.y - unit.origin.y;
411
+
412
+ if (unit.groupId) {
413
+ const translatedRects: Array<{ position: ArrangePosition; size: ArrangeSize }> = [];
414
+ for (const memberId of unit.memberIds) {
415
+ const node = nodesById.get(memberId);
416
+ if (!node) continue;
417
+ const position = {
418
+ x: node.position.x + deltaX,
419
+ y: node.position.y + deltaY,
420
+ };
421
+ nodePositions.set(memberId, position);
422
+ translatedRects.push({ position, size: node.size });
423
+ }
424
+ const bounds = computeGroupBounds(translatedRects);
425
+ if (bounds) groupBounds.set(unit.groupId, bounds);
426
+ continue;
427
+ }
428
+
429
+ const nodeId = unit.memberIds[0];
430
+ const node = nodesById.get(nodeId);
431
+ if (!node) continue;
432
+ nodePositions.set(nodeId, {
433
+ x: node.position.x + deltaX,
434
+ y: node.position.y + deltaY,
435
+ });
436
+ }
437
+
438
+ return { nodePositions, groupBounds };
439
+ }
@@ -0,0 +1,76 @@
1
+ import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
2
+
3
+ export interface NormalizeExtAppToolResultInput {
4
+ result: unknown;
5
+ success?: boolean;
6
+ error?: string;
7
+ content?: string;
8
+ detailedContent?: string;
9
+ }
10
+
11
+ function isRecord(value: unknown): value is Record<string, unknown> {
12
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
13
+ }
14
+
15
+ function isCallToolResult(value: unknown): value is CallToolResult {
16
+ return (
17
+ isRecord(value) &&
18
+ Array.isArray(value.content) &&
19
+ value.content.every(
20
+ (item) => isRecord(item) && typeof item.type === 'string',
21
+ )
22
+ );
23
+ }
24
+
25
+ function firstNonEmptyString(...values: Array<unknown>): string | undefined {
26
+ for (const value of values) {
27
+ if (typeof value === 'string') {
28
+ const trimmed = value.trim();
29
+ if (trimmed.length > 0) return value;
30
+ }
31
+ }
32
+ return undefined;
33
+ }
34
+
35
+ function serializeExtAppResultValue(value: unknown): string | undefined {
36
+ if (typeof value === 'string') return value;
37
+ if (value === undefined) return undefined;
38
+ try {
39
+ return JSON.stringify(value);
40
+ } catch (error) {
41
+ console.debug('[ext-app-tool-result] stringify failed', error);
42
+ return String(value);
43
+ }
44
+ }
45
+
46
+ export function normalizeExtAppToolResult(
47
+ input: NormalizeExtAppToolResultInput,
48
+ ): CallToolResult {
49
+ const isError = input.success === false;
50
+
51
+ if (isCallToolResult(input.result)) {
52
+ return {
53
+ ...input.result,
54
+ isError: input.result.isError === true || isError,
55
+ };
56
+ }
57
+
58
+ const resultRecord = isRecord(input.result) ? input.result : null;
59
+ const text =
60
+ firstNonEmptyString(
61
+ input.detailedContent,
62
+ input.content,
63
+ resultRecord?.detailedContent,
64
+ resultRecord?.content,
65
+ resultRecord?.textResultForLlm,
66
+ resultRecord?.text,
67
+ resultRecord?.message,
68
+ typeof input.result === 'string' ? input.result : undefined,
69
+ input.error,
70
+ ) ?? serializeExtAppResultValue(input.result);
71
+
72
+ return {
73
+ content: text ? [{ type: 'text', text }] : [],
74
+ isError,
75
+ };
76
+ }
@@ -0,0 +1,81 @@
1
+ export interface CanvasPlacementRect {
2
+ position: { x: number; y: number };
3
+ size: { width: number; height: number };
4
+ }
5
+
6
+ export function rectsOverlap(
7
+ a: { x: number; y: number },
8
+ aw: number,
9
+ ah: number,
10
+ b: CanvasPlacementRect,
11
+ gap: number,
12
+ ): boolean {
13
+ return (
14
+ a.x < b.position.x + b.size.width + gap &&
15
+ a.x + aw + gap > b.position.x &&
16
+ a.y < b.position.y + b.size.height + gap &&
17
+ a.y + ah + gap > b.position.y
18
+ );
19
+ }
20
+
21
+ export function overlapsAny(
22
+ pos: { x: number; y: number },
23
+ width: number,
24
+ height: number,
25
+ existing: CanvasPlacementRect[],
26
+ gap: number,
27
+ ): boolean {
28
+ return existing.some((rect) => rectsOverlap(pos, width, height, rect, gap));
29
+ }
30
+
31
+ export function findBlocker(
32
+ pos: { x: number; y: number },
33
+ width: number,
34
+ height: number,
35
+ existing: CanvasPlacementRect[],
36
+ gap: number,
37
+ ): CanvasPlacementRect | undefined {
38
+ return existing.find((rect) => rectsOverlap(pos, width, height, rect, gap));
39
+ }
40
+
41
+ export function findOpenCanvasPosition(
42
+ existing: CanvasPlacementRect[],
43
+ width: number,
44
+ height: number,
45
+ gap = 24,
46
+ ): { x: number; y: number } {
47
+ if (existing.length === 0) return { x: 40, y: 80 };
48
+
49
+ const last = existing[existing.length - 1];
50
+ const candidate = {
51
+ x: last.position.x + last.size.width + gap,
52
+ y: last.position.y,
53
+ };
54
+ if (!overlapsAny(candidate, width, height, existing, gap)) return candidate;
55
+
56
+ const startX = 40;
57
+ const startY = 80;
58
+ const maxX = 3000;
59
+ let y = startY;
60
+
61
+ for (let row = 0; row < 20; row++) {
62
+ let x = startX;
63
+ while (x + width < maxX) {
64
+ const blocker = findBlocker({ x, y }, width, height, existing, gap);
65
+ if (!blocker) return { x, y };
66
+ x = blocker.position.x + blocker.size.width + gap;
67
+ }
68
+
69
+ const rowNodes = existing.filter(
70
+ (node) => node.position.y <= y + height + gap && node.position.y + node.size.height + gap > y,
71
+ );
72
+ const maxBottom = rowNodes.reduce(
73
+ (max, node) => Math.max(max, node.position.y + node.size.height),
74
+ y,
75
+ );
76
+ y = maxBottom + gap;
77
+ }
78
+
79
+ const maxY = existing.reduce((max, node) => Math.max(max, node.position.y + node.size.height), 0);
80
+ return { x: startX, y: maxY + gap };
81
+ }