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,442 @@
1
+ import { spawn } from 'node:child_process';
2
+ import {
3
+ copyFileSync,
4
+ existsSync,
5
+ mkdirSync,
6
+ readFileSync,
7
+ statSync,
8
+ writeFileSync,
9
+ } from 'node:fs';
10
+ import { basename, delimiter, dirname, isAbsolute, join, relative, resolve } from 'node:path';
11
+ import { ensureArtifactsDir, getWorkspaceRoot } from './artifact-paths.js';
12
+ import { canvasState, type CanvasNodeState } from './canvas-state.js';
13
+ import { findOpenCanvasPosition } from './placement.js';
14
+ import { emitPrimaryWorkbenchEvent } from './server.js';
15
+
16
+ const BUNDLED_WEB_ARTIFACT_SCRIPTS_DIR = join(import.meta.dir, 'web-artifacts', 'scripts');
17
+ const LEGACY_SKILL_WEB_ARTIFACT_SCRIPTS_DIR = join(
18
+ import.meta.dir,
19
+ '..',
20
+ '..',
21
+ 'skills',
22
+ 'web-artifacts-builder',
23
+ 'scripts',
24
+ );
25
+ const DEFAULT_TIMEOUT_MS = 10 * 60_000;
26
+ const DEFAULT_PACKAGE_MANAGER = 'pnpm@10.33.0';
27
+ const DEFAULT_WEB_ARTIFACT_NODE_SIZE = { width: 960, height: 720 };
28
+ const FALLBACK_PATH_DIRS = ['/opt/homebrew/bin', '/usr/local/bin', '/usr/bin', '/bin'];
29
+
30
+ export interface WebArtifactBuildInput {
31
+ title: string;
32
+ appTsx: string;
33
+ indexCss?: string;
34
+ mainTsx?: string;
35
+ indexHtml?: string;
36
+ files?: Record<string, string>;
37
+ projectPath?: string;
38
+ outputPath?: string;
39
+ initScriptPath?: string;
40
+ bundleScriptPath?: string;
41
+ timeoutMs?: number;
42
+ }
43
+
44
+ export interface WebArtifactBuildOutput {
45
+ filePath: string;
46
+ fileSize: number;
47
+ projectPath: string;
48
+ metadata: Record<string, unknown>;
49
+ logs?: {
50
+ stdout?: WebArtifactLogSummary;
51
+ stderr?: WebArtifactLogSummary;
52
+ };
53
+ stdout?: string;
54
+ stderr?: string;
55
+ }
56
+
57
+ export interface WebArtifactLogSummary {
58
+ lineCount: number;
59
+ excerpt: string[];
60
+ truncated: boolean;
61
+ suppressedNoiseCount: number;
62
+ }
63
+
64
+ export interface WebArtifactCanvasOpenResult {
65
+ nodeId: string;
66
+ url: string;
67
+ }
68
+
69
+ export interface WebArtifactCanvasBuildResult extends WebArtifactBuildOutput {
70
+ openedInCanvas: boolean;
71
+ nodeId?: string;
72
+ url?: string;
73
+ }
74
+
75
+ function currentWorkspaceRoot(): string {
76
+ return canvasState.getWorkspaceRoot();
77
+ }
78
+
79
+ function escapeHtml(value: string): string {
80
+ return value
81
+ .replaceAll('&', '&amp;')
82
+ .replaceAll('<', '&lt;')
83
+ .replaceAll('>', '&gt;')
84
+ .replaceAll('"', '&quot;')
85
+ .replaceAll("'", '&#39;');
86
+ }
87
+
88
+ function defaultIndexHtml(title: string): string {
89
+ return `<!DOCTYPE html>
90
+ <html lang="en">
91
+ <head>
92
+ <meta charset="UTF-8" />
93
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
94
+ <title>${escapeHtml(title)}</title>
95
+ </head>
96
+ <body>
97
+ <div id="root"></div>
98
+ <script type="module" src="/src/main.tsx"></script>
99
+ </body>
100
+ </html>
101
+ `;
102
+ }
103
+
104
+ function slugify(value: string): string {
105
+ const slug = value
106
+ .toLowerCase()
107
+ .replace(/[^a-z0-9]+/g, '-')
108
+ .replace(/^-+|-+$/g, '')
109
+ .slice(0, 48);
110
+ return slug.length > 0 ? slug : 'web-artifact';
111
+ }
112
+
113
+ function isPathInside(base: string, candidate: string): boolean {
114
+ const rel = relative(base, candidate);
115
+ if (rel === '') return true;
116
+ return !rel.startsWith('..') && rel !== '..' && !isAbsolute(rel);
117
+ }
118
+
119
+ export function resolveWorkspacePath(pathLike: string, cwd?: string): string {
120
+ const workspace = getWorkspaceRoot(cwd ?? currentWorkspaceRoot());
121
+ const resolved = resolve(workspace, pathLike);
122
+ if (!isPathInside(workspace, resolved)) {
123
+ throw new Error(`Path "${pathLike}" resolves outside workspace.`);
124
+ }
125
+ return resolved;
126
+ }
127
+
128
+ async function runProcess(
129
+ command: string,
130
+ args: string[],
131
+ options: { cwd: string; timeoutMs: number },
132
+ ): Promise<{ stdout: string; stderr: string }> {
133
+ const pathEntries = new Set(
134
+ String(process.env.PATH ?? '')
135
+ .split(delimiter)
136
+ .filter(Boolean),
137
+ );
138
+ for (const entry of FALLBACK_PATH_DIRS) {
139
+ pathEntries.add(entry);
140
+ }
141
+ const env = Object.fromEntries(
142
+ Object.entries(process.env).filter(([key, value]) => {
143
+ if (typeof value !== 'string' || value.length === 0) return false;
144
+ return new Set([
145
+ 'PATH',
146
+ 'HOME',
147
+ 'SHELL',
148
+ 'LANG',
149
+ 'LC_ALL',
150
+ 'TERM',
151
+ 'USER',
152
+ 'TMPDIR',
153
+ 'TMP',
154
+ 'TEMP',
155
+ 'http_proxy',
156
+ 'https_proxy',
157
+ 'HTTP_PROXY',
158
+ 'HTTPS_PROXY',
159
+ 'NO_PROXY',
160
+ 'no_proxy',
161
+ 'SSL_CERT_FILE',
162
+ 'NODE_EXTRA_CA_CERTS',
163
+ ]).has(key);
164
+ }),
165
+ );
166
+ const child = spawn(command, args, {
167
+ cwd: options.cwd,
168
+ env: {
169
+ ...env,
170
+ PATH: [...pathEntries].join(delimiter),
171
+ CI: '1',
172
+ npm_config_yes: 'true',
173
+ pnpm_config_yes: 'true',
174
+ },
175
+ stdio: ['ignore', 'pipe', 'pipe'],
176
+ });
177
+
178
+ let stdout = '';
179
+ let stderr = '';
180
+ let timedOut = false;
181
+
182
+ child.stdout.on('data', (chunk) => {
183
+ stdout += chunk.toString();
184
+ });
185
+ child.stderr.on('data', (chunk) => {
186
+ stderr += chunk.toString();
187
+ });
188
+
189
+ await new Promise<void>((resolvePromise, rejectPromise) => {
190
+ const timer = setTimeout(() => {
191
+ timedOut = true;
192
+ child.kill('SIGKILL');
193
+ rejectPromise(new Error(`Command timed out after ${options.timeoutMs}ms: ${command}`));
194
+ }, options.timeoutMs);
195
+
196
+ child.on('error', (error) => {
197
+ clearTimeout(timer);
198
+ rejectPromise(error);
199
+ });
200
+
201
+ child.on('close', (code) => {
202
+ clearTimeout(timer);
203
+ if (timedOut) return;
204
+ if (code !== 0) {
205
+ rejectPromise(
206
+ new Error(
207
+ [`Command failed (${code}): ${command} ${args.join(' ')}`, stderr.trim()]
208
+ .filter(Boolean)
209
+ .join('\n'),
210
+ ),
211
+ );
212
+ return;
213
+ }
214
+ resolvePromise();
215
+ });
216
+ });
217
+
218
+ return { stdout: stdout.trim(), stderr: stderr.trim() };
219
+ }
220
+
221
+ export function resolveWebArtifactScriptPath(kind: 'init' | 'bundle'): string {
222
+ const scriptFile = kind === 'init' ? 'init-artifact.sh' : 'bundle-artifact.sh';
223
+ const candidates = [
224
+ join(currentWorkspaceRoot(), 'skills', 'web-artifacts-builder', 'scripts', scriptFile),
225
+ join(BUNDLED_WEB_ARTIFACT_SCRIPTS_DIR, scriptFile),
226
+ join(LEGACY_SKILL_WEB_ARTIFACT_SCRIPTS_DIR, scriptFile),
227
+ ];
228
+
229
+ for (const candidate of candidates) {
230
+ if (existsSync(candidate)) return resolve(candidate);
231
+ }
232
+
233
+ throw new Error(
234
+ `No web-artifact ${kind} script found. Expected one of: ${candidates.join(', ')}`,
235
+ );
236
+ }
237
+
238
+ function writeProjectFiles(
239
+ projectPath: string,
240
+ input: Pick<
241
+ WebArtifactBuildInput,
242
+ 'title' | 'appTsx' | 'indexCss' | 'mainTsx' | 'indexHtml' | 'files'
243
+ >,
244
+ ): void {
245
+ const writes = new Map<string, string>();
246
+ writes.set(join(projectPath, 'src', 'App.tsx'), input.appTsx);
247
+ writes.set(join(projectPath, 'index.html'), input.indexHtml ?? defaultIndexHtml(input.title));
248
+ if (typeof input.indexCss === 'string') {
249
+ writes.set(join(projectPath, 'src', 'index.css'), input.indexCss);
250
+ }
251
+ if (typeof input.mainTsx === 'string') {
252
+ writes.set(join(projectPath, 'src', 'main.tsx'), input.mainTsx);
253
+ }
254
+ for (const [relativePath, content] of Object.entries(input.files ?? {})) {
255
+ writes.set(join(projectPath, relativePath), content);
256
+ }
257
+
258
+ for (const [filePath, content] of writes.entries()) {
259
+ mkdirSync(dirname(filePath), { recursive: true });
260
+ writeFileSync(filePath, content, 'utf-8');
261
+ }
262
+ }
263
+
264
+ function ensurePackageManagerBoundary(dirPath: string): void {
265
+ const packageJsonPath = join(dirPath, 'package.json');
266
+ mkdirSync(dirPath, { recursive: true });
267
+ let nextPackageJson: Record<string, unknown> = {};
268
+ if (existsSync(packageJsonPath)) {
269
+ try {
270
+ const parsed = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
271
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
272
+ nextPackageJson = parsed as Record<string, unknown>;
273
+ }
274
+ } catch (error) {
275
+ console.warn('[web-artifacts] failed to parse existing package.json boundary', {
276
+ error,
277
+ packageJsonPath,
278
+ });
279
+ nextPackageJson = {};
280
+ }
281
+ }
282
+ nextPackageJson.private = true;
283
+ nextPackageJson.packageManager = DEFAULT_PACKAGE_MANAGER;
284
+ writeFileSync(packageJsonPath, JSON.stringify(nextPackageJson, null, 2), 'utf-8');
285
+ }
286
+
287
+ function summarizeArtifactLog(text: string): WebArtifactLogSummary | undefined {
288
+ if (!text.trim()) return undefined;
289
+
290
+ const lines = text
291
+ .split('\n')
292
+ .map((line) => line.trimEnd())
293
+ .filter((line) => line.trim().length > 0);
294
+ if (lines.length === 0) return undefined;
295
+
296
+ const noisyPatterns = [
297
+ /\/dev\/tty/i,
298
+ /no such device or address/i,
299
+ ];
300
+ const filteredLines = lines.filter((line) => !noisyPatterns.some((pattern) => pattern.test(line)));
301
+ const suppressedNoiseCount = lines.length - filteredLines.length;
302
+ const visibleLines = filteredLines.length > 0 ? filteredLines : lines;
303
+ const excerpt = visibleLines.slice(-6);
304
+
305
+ return {
306
+ lineCount: visibleLines.length,
307
+ excerpt,
308
+ truncated: visibleLines.length > excerpt.length,
309
+ suppressedNoiseCount,
310
+ };
311
+ }
312
+
313
+ export async function executeWebArtifactBuild(
314
+ input: WebArtifactBuildInput,
315
+ ): Promise<WebArtifactBuildOutput> {
316
+ const workspaceRoot = currentWorkspaceRoot();
317
+ const artifactsDir = ensureArtifactsDir(workspaceRoot);
318
+ const slug = slugify(input.title);
319
+ const projectPath = resolve(input.projectPath ?? join(artifactsDir, '.web-artifacts', slug));
320
+ const outputPath = resolve(input.outputPath ?? join(artifactsDir, `${slug}.html`));
321
+ const initScriptPath = resolve(input.initScriptPath ?? resolveWebArtifactScriptPath('init'));
322
+ const bundleScriptPath = resolve(
323
+ input.bundleScriptPath ?? resolveWebArtifactScriptPath('bundle'),
324
+ );
325
+ const timeoutMs = input.timeoutMs ?? DEFAULT_TIMEOUT_MS;
326
+
327
+ if (!existsSync(initScriptPath)) {
328
+ throw new Error(`Web-artifact init script not found: ${initScriptPath}`);
329
+ }
330
+ if (!existsSync(bundleScriptPath)) {
331
+ throw new Error(`Web-artifact bundle script not found: ${bundleScriptPath}`);
332
+ }
333
+
334
+ const parentDir = dirname(projectPath);
335
+ mkdirSync(parentDir, { recursive: true });
336
+ ensurePackageManagerBoundary(parentDir);
337
+
338
+ let stdout = '';
339
+ let stderr = '';
340
+ const needsInit =
341
+ !existsSync(projectPath) ||
342
+ !existsSync(join(projectPath, 'package.json')) ||
343
+ !existsSync(join(projectPath, 'src'));
344
+
345
+ if (needsInit) {
346
+ const initResult = await runProcess('bash', [initScriptPath, basename(projectPath)], {
347
+ cwd: parentDir,
348
+ timeoutMs,
349
+ });
350
+ stdout = [stdout, initResult.stdout].filter(Boolean).join('\n');
351
+ stderr = [stderr, initResult.stderr].filter(Boolean).join('\n');
352
+ }
353
+
354
+ writeProjectFiles(projectPath, input);
355
+
356
+ const bundleResult = await runProcess('bash', [bundleScriptPath], {
357
+ cwd: projectPath,
358
+ timeoutMs,
359
+ });
360
+ stdout = [stdout, bundleResult.stdout].filter(Boolean).join('\n');
361
+ stderr = [stderr, bundleResult.stderr].filter(Boolean).join('\n');
362
+
363
+ const bundlePath = join(projectPath, 'bundle.html');
364
+ if (!existsSync(bundlePath)) {
365
+ throw new Error(`Expected bundled artifact at ${bundlePath}`);
366
+ }
367
+
368
+ mkdirSync(dirname(outputPath), { recursive: true });
369
+ copyFileSync(bundlePath, outputPath);
370
+
371
+ return {
372
+ filePath: outputPath,
373
+ fileSize: statSync(outputPath).size,
374
+ projectPath,
375
+ metadata: {
376
+ title: input.title,
377
+ bundlePath,
378
+ projectPath,
379
+ hasIndexCss: typeof input.indexCss === 'string',
380
+ extraFileCount: Object.keys(input.files ?? {}).length,
381
+ outputPreview: readFileSync(outputPath, 'utf-8').slice(0, 200),
382
+ },
383
+ logs: {
384
+ ...(summarizeArtifactLog(stdout) ? { stdout: summarizeArtifactLog(stdout) } : {}),
385
+ ...(summarizeArtifactLog(stderr) ? { stderr: summarizeArtifactLog(stderr) } : {}),
386
+ },
387
+ stdout: stdout || undefined,
388
+ stderr: stderr || undefined,
389
+ };
390
+ }
391
+
392
+ export function openWebArtifactInCanvas(input: {
393
+ title: string;
394
+ filePath: string;
395
+ }): WebArtifactCanvasOpenResult {
396
+ const width = DEFAULT_WEB_ARTIFACT_NODE_SIZE.width;
397
+ const height = DEFAULT_WEB_ARTIFACT_NODE_SIZE.height;
398
+ const pos = findOpenCanvasPosition(canvasState.getLayout().nodes, width, height);
399
+ const id = `artifact-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
400
+ const url = `/artifact?path=${encodeURIComponent(input.filePath)}`;
401
+ const node: CanvasNodeState = {
402
+ id,
403
+ type: 'mcp-app',
404
+ position: pos,
405
+ size: { width, height },
406
+ zIndex: 1,
407
+ collapsed: false,
408
+ pinned: false,
409
+ dockPosition: null,
410
+ data: {
411
+ title: input.title,
412
+ url,
413
+ path: input.filePath,
414
+ trustedDomain: true,
415
+ sourceServer: 'pmx-canvas',
416
+ hostMode: 'hosted',
417
+ },
418
+ };
419
+
420
+ canvasState.addNode(node);
421
+ emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
422
+ return { nodeId: id, url };
423
+ }
424
+
425
+ export async function buildWebArtifactOnCanvas(input: WebArtifactBuildInput & {
426
+ openInCanvas?: boolean;
427
+ }): Promise<WebArtifactCanvasBuildResult> {
428
+ const build = await executeWebArtifactBuild(input);
429
+ if (input.openInCanvas === false) {
430
+ return { ...build, openedInCanvas: false };
431
+ }
432
+ const opened = openWebArtifactInCanvas({
433
+ title: input.title,
434
+ filePath: build.filePath,
435
+ });
436
+ return {
437
+ ...build,
438
+ openedInCanvas: true,
439
+ nodeId: opened.nodeId,
440
+ url: opened.url,
441
+ };
442
+ }