pmx-canvas 0.1.12 → 0.1.13
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.
- package/CHANGELOG.md +82 -0
- package/dist/canvas/index.js +34 -34
- package/dist/types/client/nodes/trace-model.d.ts +9 -0
- package/dist/types/client/state/canvas-store.d.ts +2 -0
- package/dist/types/mcp/canvas-access.d.ts +1 -0
- package/dist/types/server/web-artifacts.d.ts +1 -0
- package/package.json +1 -1
- package/skills/web-artifacts-builder/scripts/init-artifact.sh +9 -8
- package/src/client/nodes/TraceNode.tsx +2 -6
- package/src/client/nodes/trace-model.ts +19 -0
- package/src/client/state/canvas-store.ts +5 -2
- package/src/client/state/sse-bridge.ts +2 -1
- package/src/mcp/canvas-access.ts +28 -3
- package/src/mcp/server.ts +30 -8
- package/src/server/canvas-operations.ts +1 -0
- package/src/server/canvas-schema.ts +11 -0
- package/src/server/diagram-presets.ts +6 -28
- package/src/server/server.ts +1 -0
- package/src/server/web-artifacts/scripts/init-artifact.sh +9 -8
- package/src/server/web-artifacts.ts +14 -1
|
@@ -54,6 +54,8 @@ export declare function replaceViewport(next: ViewportState): void;
|
|
|
54
54
|
export declare function commitViewport(next: ViewportState): void;
|
|
55
55
|
export declare function applyServerCanvasLayout(layout: Pick<CanvasLayout, 'nodes' | 'edges'> & {
|
|
56
56
|
viewport?: ViewportState;
|
|
57
|
+
}, options?: {
|
|
58
|
+
applyViewport?: boolean;
|
|
57
59
|
}): void;
|
|
58
60
|
/**
|
|
59
61
|
* Smoothly animate the viewport to a target state.
|
|
@@ -83,5 +83,6 @@ export interface CanvasAccess {
|
|
|
83
83
|
resizeAutomationWebView(width: number, height: number): Promise<AutomationWebViewStatus>;
|
|
84
84
|
screenshotAutomationWebView(options?: AutomationScreenshotOptions): Promise<Uint8Array>;
|
|
85
85
|
}
|
|
86
|
+
export declare function refreshCanvasAccess(access: CanvasAccess): Promise<CanvasAccess>;
|
|
86
87
|
export declare function createCanvasAccess(): Promise<CanvasAccess>;
|
|
87
88
|
export {};
|
|
@@ -38,6 +38,7 @@ export interface WebArtifactCanvasBuildResult extends WebArtifactBuildOutput {
|
|
|
38
38
|
openedInCanvas: boolean;
|
|
39
39
|
nodeId?: string;
|
|
40
40
|
url?: string;
|
|
41
|
+
completedAt: string;
|
|
41
42
|
}
|
|
42
43
|
export declare function resolveWorkspacePath(pathLike: string, cwd?: string): string;
|
|
43
44
|
export declare function resolveWebArtifactScriptPath(kind: 'init' | 'bundle'): string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmx-canvas",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"description": "Spatial canvas workbench for coding agents — infinite 2D canvas with agent-native CLI, MCP integration, nodes, edges, file watching, and snapshots",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/server/index.ts",
|
|
@@ -115,12 +115,13 @@ else
|
|
|
115
115
|
echo "✅ Using Vite $VITE_VERSION (Node 18 compatible)"
|
|
116
116
|
fi
|
|
117
117
|
|
|
118
|
-
|
|
119
|
-
if [[ "$OSTYPE" == "darwin"* ]]; then
|
|
120
|
-
|
|
121
|
-
else
|
|
122
|
-
|
|
123
|
-
fi
|
|
118
|
+
function sed_in_place() {
|
|
119
|
+
if [[ "$OSTYPE" == "darwin"* ]]; then
|
|
120
|
+
sed -i '' "$@"
|
|
121
|
+
else
|
|
122
|
+
sed -i "$@"
|
|
123
|
+
fi
|
|
124
|
+
}
|
|
124
125
|
|
|
125
126
|
declare -a PNPM_CMD
|
|
126
127
|
configure_pnpm
|
|
@@ -158,8 +159,8 @@ fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');
|
|
|
158
159
|
"
|
|
159
160
|
|
|
160
161
|
echo "🧹 Cleaning up Vite template..."
|
|
161
|
-
|
|
162
|
-
|
|
162
|
+
sed_in_place '/<link rel="icon".*/d' index.html
|
|
163
|
+
sed_in_place 's/<title>.*<\/title>/<title>'"$PROJECT_NAME"'<\/title>/' index.html
|
|
163
164
|
|
|
164
165
|
echo "📦 Installing base dependencies..."
|
|
165
166
|
run_pnpm_quiet install
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CanvasNodeState } from '../types';
|
|
2
|
+
import { buildTraceDisplayModel } from './trace-model';
|
|
2
3
|
|
|
3
4
|
const CATEGORY_COLORS: Record<string, string> = {
|
|
4
5
|
mcp: 'var(--c-accent)',
|
|
@@ -20,12 +21,7 @@ const STATUS_COLORS: Record<string, string> = {
|
|
|
20
21
|
};
|
|
21
22
|
|
|
22
23
|
export function TraceNode({ node }: { node: CanvasNodeState }) {
|
|
23
|
-
const toolName = (node.data
|
|
24
|
-
const category = (node.data.category as string) || 'other';
|
|
25
|
-
const status = (node.data.status as string) || 'running';
|
|
26
|
-
const duration = (node.data.duration as string) || '';
|
|
27
|
-
const resultSummary = (node.data.resultSummary as string) || '';
|
|
28
|
-
const error = (node.data.error as string) || '';
|
|
24
|
+
const { toolName, category, status, duration, resultSummary, error } = buildTraceDisplayModel(node.data);
|
|
29
25
|
|
|
30
26
|
const catColor = CATEGORY_COLORS[category] ?? CATEGORY_COLORS.other;
|
|
31
27
|
const statusIcon = STATUS_ICONS[status] ?? '◌';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface TraceDisplayModel {
|
|
2
|
+
toolName: string;
|
|
3
|
+
category: string;
|
|
4
|
+
status: string;
|
|
5
|
+
duration: string;
|
|
6
|
+
resultSummary: string;
|
|
7
|
+
error: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function buildTraceDisplayModel(data: Record<string, unknown>): TraceDisplayModel {
|
|
11
|
+
return {
|
|
12
|
+
toolName: (data.toolName as string) || (data.title as string) || 'unknown',
|
|
13
|
+
category: (data.category as string) || 'other',
|
|
14
|
+
status: (data.status as string) || 'running',
|
|
15
|
+
duration: (data.duration as string) || '',
|
|
16
|
+
resultSummary: (data.resultSummary as string) || (data.content as string) || '',
|
|
17
|
+
error: (data.error as string) || '',
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -313,7 +313,10 @@ export function commitViewport(next: ViewportState): void {
|
|
|
313
313
|
void updateViewportFromClient(next);
|
|
314
314
|
}
|
|
315
315
|
|
|
316
|
-
export function applyServerCanvasLayout(
|
|
316
|
+
export function applyServerCanvasLayout(
|
|
317
|
+
layout: Pick<CanvasLayout, 'nodes' | 'edges'> & { viewport?: ViewportState },
|
|
318
|
+
options: { applyViewport?: boolean } = {},
|
|
319
|
+
): void {
|
|
317
320
|
const nextNodes = new Map<string, CanvasNodeState>();
|
|
318
321
|
let nextMaxZ = 1;
|
|
319
322
|
for (const node of layout.nodes) {
|
|
@@ -337,7 +340,7 @@ export function applyServerCanvasLayout(layout: Pick<CanvasLayout, 'nodes' | 'ed
|
|
|
337
340
|
const nextContextPinnedNodeIds = filterNodeIdSet(contextPinnedNodeIds.value, nextNodes);
|
|
338
341
|
|
|
339
342
|
batch(() => {
|
|
340
|
-
if (layout.viewport) {
|
|
343
|
+
if (options.applyViewport === true && layout.viewport) {
|
|
341
344
|
viewport.value = layout.viewport;
|
|
342
345
|
}
|
|
343
346
|
maxZ = nextMaxZ;
|
|
@@ -803,6 +803,7 @@ function handleCanvasLayoutUpdate(data: Record<string, unknown>): void {
|
|
|
803
803
|
}
|
|
804
804
|
| undefined;
|
|
805
805
|
if (!layout?.nodes) return;
|
|
806
|
+
const shouldApplyViewport = !hasInitialServerLayout.value;
|
|
806
807
|
hasInitialServerLayout.value = true;
|
|
807
808
|
|
|
808
809
|
const serverNodes = layout.nodes
|
|
@@ -824,7 +825,7 @@ function handleCanvasLayoutUpdate(data: Record<string, unknown>): void {
|
|
|
824
825
|
...(nextViewport ? { viewport: nextViewport } : {}),
|
|
825
826
|
nodes: serverNodes,
|
|
826
827
|
edges: serverEdges,
|
|
827
|
-
});
|
|
828
|
+
}, { applyViewport: shouldApplyViewport });
|
|
828
829
|
|
|
829
830
|
syncAttentionFromSse({ event: 'canvas-layout-update', data });
|
|
830
831
|
}
|
package/src/mcp/canvas-access.ts
CHANGED
|
@@ -135,7 +135,11 @@ export interface CanvasAccess {
|
|
|
135
135
|
class LocalCanvasAccess implements CanvasAccess {
|
|
136
136
|
readonly remoteBaseUrl = null;
|
|
137
137
|
|
|
138
|
-
constructor(
|
|
138
|
+
constructor(
|
|
139
|
+
private readonly canvas: PmxCanvas,
|
|
140
|
+
readonly workspaceRoot: string,
|
|
141
|
+
readonly targetPort: number,
|
|
142
|
+
) {}
|
|
139
143
|
|
|
140
144
|
get port(): number {
|
|
141
145
|
return this.canvas.port;
|
|
@@ -335,6 +339,9 @@ class RemoteCanvasAccess implements CanvasAccess {
|
|
|
335
339
|
const error = parsed && typeof parsed === 'object' && 'error' in parsed
|
|
336
340
|
? String((parsed as { error?: unknown }).error)
|
|
337
341
|
: `HTTP ${response.status}`;
|
|
342
|
+
if (path === '/api/canvas/batch' && parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
343
|
+
return parsed as T;
|
|
344
|
+
}
|
|
338
345
|
throw new Error(error);
|
|
339
346
|
}
|
|
340
347
|
return parsed as T;
|
|
@@ -617,6 +624,10 @@ function candidateBaseUrls(port: number): string[] {
|
|
|
617
624
|
return urls;
|
|
618
625
|
}
|
|
619
626
|
|
|
627
|
+
function localBaseUrls(port: number): string[] {
|
|
628
|
+
return [`http://127.0.0.1:${port}`, `http://localhost:${port}`];
|
|
629
|
+
}
|
|
630
|
+
|
|
620
631
|
async function readHealth(baseUrl: string): Promise<HealthResponse | null> {
|
|
621
632
|
try {
|
|
622
633
|
const response = await fetch(`${baseUrl}/health`, { signal: AbortSignal.timeout(400) });
|
|
@@ -627,9 +638,15 @@ async function readHealth(baseUrl: string): Promise<HealthResponse | null> {
|
|
|
627
638
|
}
|
|
628
639
|
}
|
|
629
640
|
|
|
630
|
-
async function findExistingCanvasServer(
|
|
641
|
+
async function findExistingCanvasServer(
|
|
642
|
+
workspaceRoot: string,
|
|
643
|
+
port: number,
|
|
644
|
+
options: { excludeBaseUrls?: string[] } = {},
|
|
645
|
+
): Promise<string | null> {
|
|
631
646
|
const canonicalWorkspaceRoot = canonicalWorkspacePath(workspaceRoot);
|
|
647
|
+
const excluded = new Set((options.excludeBaseUrls ?? []).map((baseUrl) => baseUrl.replace(/\/$/, '')));
|
|
632
648
|
for (const baseUrl of candidateBaseUrls(port)) {
|
|
649
|
+
if (excluded.has(baseUrl)) continue;
|
|
633
650
|
const health = await readHealth(baseUrl);
|
|
634
651
|
if (health?.ok !== true) continue;
|
|
635
652
|
const healthWorkspace = typeof health.workspace === 'string' ? canonicalWorkspacePath(health.workspace) : '';
|
|
@@ -639,6 +656,14 @@ async function findExistingCanvasServer(workspaceRoot: string, port: number): Pr
|
|
|
639
656
|
return null;
|
|
640
657
|
}
|
|
641
658
|
|
|
659
|
+
export async function refreshCanvasAccess(access: CanvasAccess): Promise<CanvasAccess> {
|
|
660
|
+
if (!(access instanceof LocalCanvasAccess)) return access;
|
|
661
|
+
const remoteBaseUrl = await findExistingCanvasServer(access.workspaceRoot, access.targetPort, {
|
|
662
|
+
excludeBaseUrls: localBaseUrls(access.port),
|
|
663
|
+
});
|
|
664
|
+
return remoteBaseUrl ? new RemoteCanvasAccess(remoteBaseUrl) : access;
|
|
665
|
+
}
|
|
666
|
+
|
|
642
667
|
export async function createCanvasAccess(): Promise<CanvasAccess> {
|
|
643
668
|
const workspaceRoot = resolve(process.cwd());
|
|
644
669
|
const port = targetPort();
|
|
@@ -647,5 +672,5 @@ export async function createCanvasAccess(): Promise<CanvasAccess> {
|
|
|
647
672
|
|
|
648
673
|
const canvas = createCanvas({ port });
|
|
649
674
|
await canvas.start({ open: true });
|
|
650
|
-
return new LocalCanvasAccess(canvas);
|
|
675
|
+
return new LocalCanvasAccess(canvas, workspaceRoot, port);
|
|
651
676
|
}
|
package/src/mcp/server.ts
CHANGED
|
@@ -25,7 +25,7 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
|
25
25
|
import { isAbsolute, relative, resolve } from 'node:path';
|
|
26
26
|
import { z } from 'zod';
|
|
27
27
|
import { canvasState, describeCanvasSchema, validateStructuredCanvasPayload } from '../server/index.js';
|
|
28
|
-
import { createCanvasAccess, type CanvasAccess } from './canvas-access.js';
|
|
28
|
+
import { createCanvasAccess, refreshCanvasAccess, type CanvasAccess } from './canvas-access.js';
|
|
29
29
|
import { serializeNodeForAgentContext } from '../server/agent-context.js';
|
|
30
30
|
import { wrapCanvasAutomationScript } from '../server/server.js';
|
|
31
31
|
import { buildSpatialContext, findNeighborhoods } from '../server/spatial-analysis.js';
|
|
@@ -34,7 +34,8 @@ import { listBundledSkills, readBundledSkill } from '../server/bundled-skills.js
|
|
|
34
34
|
|
|
35
35
|
let canvas: CanvasAccess | null = null;
|
|
36
36
|
let resourceNotificationServer: McpServer | null = null;
|
|
37
|
-
let
|
|
37
|
+
let localResourceNotificationsStarted = false;
|
|
38
|
+
let remoteResourceNotificationsBaseUrl: string | null = null;
|
|
38
39
|
|
|
39
40
|
const jsonRenderSpecSchema = z.union([
|
|
40
41
|
z.object({
|
|
@@ -78,6 +79,8 @@ function safeWorkspacePath(pathLike: string): string {
|
|
|
78
79
|
async function ensureCanvas(): Promise<CanvasAccess> {
|
|
79
80
|
if (!canvas) {
|
|
80
81
|
canvas = await createCanvasAccess();
|
|
82
|
+
} else {
|
|
83
|
+
canvas = await refreshCanvasAccess(canvas);
|
|
81
84
|
}
|
|
82
85
|
startResourceNotifications(canvas);
|
|
83
86
|
return canvas;
|
|
@@ -139,16 +142,20 @@ async function watchRemoteCanvasEvents(baseUrl: string): Promise<void> {
|
|
|
139
142
|
}
|
|
140
143
|
|
|
141
144
|
function startResourceNotifications(c: CanvasAccess): void {
|
|
142
|
-
if (resourceNotificationsStarted) return;
|
|
143
145
|
const server = resourceNotificationServer;
|
|
144
146
|
if (!server) return;
|
|
145
|
-
resourceNotificationsStarted = true;
|
|
146
147
|
|
|
147
148
|
if (c.remoteBaseUrl) {
|
|
148
|
-
|
|
149
|
+
if (remoteResourceNotificationsBaseUrl !== c.remoteBaseUrl) {
|
|
150
|
+
remoteResourceNotificationsBaseUrl = c.remoteBaseUrl;
|
|
151
|
+
void watchRemoteCanvasEvents(c.remoteBaseUrl);
|
|
152
|
+
}
|
|
149
153
|
return;
|
|
150
154
|
}
|
|
151
155
|
|
|
156
|
+
if (localResourceNotificationsStarted) return;
|
|
157
|
+
localResourceNotificationsStarted = true;
|
|
158
|
+
|
|
152
159
|
canvasState.onChange((type) => {
|
|
153
160
|
sendCanvasResourceNotifications(type);
|
|
154
161
|
});
|
|
@@ -184,6 +191,19 @@ function buildSummaryFromLayout(layout: Awaited<ReturnType<CanvasAccess['getLayo
|
|
|
184
191
|
};
|
|
185
192
|
}
|
|
186
193
|
|
|
194
|
+
function buildSnapshotRestoreSummary(layout: Awaited<ReturnType<CanvasAccess['getLayout']>>): Record<string, unknown> {
|
|
195
|
+
const nodesByType: Record<string, number> = {};
|
|
196
|
+
for (const node of layout.nodes) {
|
|
197
|
+
nodesByType[node.type] = (nodesByType[node.type] ?? 0) + 1;
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
nodeCount: layout.nodes.length,
|
|
201
|
+
edgeCount: layout.edges.length,
|
|
202
|
+
nodesByType,
|
|
203
|
+
viewport: layout.viewport,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
187
207
|
export async function startMcpServer(): Promise<void> {
|
|
188
208
|
const server = new McpServer({
|
|
189
209
|
name: 'pmx-canvas',
|
|
@@ -463,7 +483,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
463
483
|
// ── canvas_build_web_artifact ───────────────────────────────
|
|
464
484
|
server.tool(
|
|
465
485
|
'canvas_build_web_artifact',
|
|
466
|
-
'Build a bundled single-file HTML web artifact from React/Tailwind source files using the bundled web-artifacts-builder skill scripts. MCP callers pass source content in appTsx (the CLI app-file flag reads a file before calling this path). Builds
|
|
486
|
+
'Build a bundled single-file HTML web artifact from React/Tailwind source files using the bundled web-artifacts-builder skill scripts. MCP callers pass source content in appTsx (the CLI app-file flag reads a file before calling this path). Builds can exceed default 60s MCP client timeouts on cold workspaces; set a long client timeout or retry with the same projectPath/outputPath if the client times out. Optionally opens the generated artifact as an embedded node on the canvas. Read canvas://skills/web-artifacts-builder for the full workflow, stack, and anti-slop design guidelines before calling.',
|
|
467
487
|
{
|
|
468
488
|
title: z.string().describe('Artifact title used for default project and output paths'),
|
|
469
489
|
appTsx: z.string().describe('Contents for src/App.tsx'),
|
|
@@ -514,6 +534,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
514
534
|
bytes: result.fileSize,
|
|
515
535
|
projectPath: result.projectPath,
|
|
516
536
|
openedInCanvas: result.openedInCanvas,
|
|
537
|
+
completedAt: result.completedAt,
|
|
517
538
|
// `id` only present when a canvas node was actually created.
|
|
518
539
|
// See the matching block in src/server/server.ts handleCanvasBuildWebArtifact.
|
|
519
540
|
...(typeof result.nodeId === 'string' ? { id: result.nodeId } : {}),
|
|
@@ -1453,7 +1474,7 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1453
1474
|
|
|
1454
1475
|
server.tool(
|
|
1455
1476
|
'canvas_batch',
|
|
1456
|
-
'Run a batch of canvas operations with optional assigned references. Supports node.add, node.update, graph.add, edge.add, group.create, group.add, group.remove, pin.set/add/remove, snapshot.save, and arrange.',
|
|
1477
|
+
'Run a non-atomic batch of canvas operations with optional assigned references. Use assign to name a result, then reference it later as "$name" for the created node id or "$name.id" for a specific result field. On failure, earlier successful operations remain applied and the response includes ok:false, failedIndex, error, results, and refs. Supports node.add, node.update, graph.add, edge.add, group.create, group.add, group.remove, pin.set/add/remove, snapshot.save, and arrange.',
|
|
1457
1478
|
{
|
|
1458
1479
|
operations: z.array(z.object({
|
|
1459
1480
|
op: z.string().describe('Operation name, e.g. "node.add" or "edge.add"'),
|
|
@@ -1568,8 +1589,9 @@ export async function startMcpServer(): Promise<void> {
|
|
|
1568
1589
|
if (!result.ok) {
|
|
1569
1590
|
return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: 'Snapshot not found' }) }] };
|
|
1570
1591
|
}
|
|
1592
|
+
const layout = await c.getLayout();
|
|
1571
1593
|
return {
|
|
1572
|
-
content: [{ type: 'text', text: JSON.stringify({ ok: true,
|
|
1594
|
+
content: [{ type: 'text', text: JSON.stringify({ ok: true, restored: input.id, summary: buildSnapshotRestoreSummary(layout) }, null, 2) }],
|
|
1573
1595
|
};
|
|
1574
1596
|
},
|
|
1575
1597
|
);
|
|
@@ -1373,6 +1373,7 @@ function resolveBatchRefs(value: unknown, refs: Record<string, unknown>): unknow
|
|
|
1373
1373
|
if (typeof value === 'string' && value.startsWith('$')) {
|
|
1374
1374
|
const path = value.slice(1).split('.');
|
|
1375
1375
|
let current: unknown = refs[path[0] ?? ''];
|
|
1376
|
+
if (path.length === 1 && isPlainRecord(current) && typeof current.id === 'string') return current.id;
|
|
1376
1377
|
for (const segment of path.slice(1)) {
|
|
1377
1378
|
if (!isPlainRecord(current) && !Array.isArray(current)) return undefined;
|
|
1378
1379
|
current = (current as Record<string, unknown>)[segment];
|
|
@@ -140,11 +140,18 @@ const CANVAS_CREATE_TYPES: CanvasCreateTypeSchema[] = [
|
|
|
140
140
|
fields: [
|
|
141
141
|
{ name: 'title', type: 'string', required: false, description: 'Optional title.' },
|
|
142
142
|
{ name: 'content', type: 'string', required: false, description: 'Trace summary.' },
|
|
143
|
+
{ name: 'toolName', type: 'string', required: false, description: 'Tool or operation label shown in the trace pill; defaults to title.' },
|
|
144
|
+
{ name: 'category', type: 'string', required: false, description: 'Trace category color key: mcp, file, subagent, or other.' },
|
|
145
|
+
{ name: 'status', type: 'string', required: false, description: 'Trace status: running, success, or failed.' },
|
|
146
|
+
{ name: 'duration', type: 'string', required: false, description: 'Optional duration badge text.' },
|
|
147
|
+
{ name: 'resultSummary', type: 'string', required: false, description: 'Short trace result summary; defaults to content.' },
|
|
148
|
+
{ name: 'error', type: 'string', required: false, description: 'Short error message shown in failed traces.' },
|
|
143
149
|
],
|
|
144
150
|
example: {
|
|
145
151
|
type: 'trace',
|
|
146
152
|
title: 'Execution Trace',
|
|
147
153
|
content: 'Canvas actions and tool events.',
|
|
154
|
+
status: 'success',
|
|
148
155
|
},
|
|
149
156
|
},
|
|
150
157
|
{
|
|
@@ -378,12 +385,16 @@ const CANVAS_CREATE_TYPES: CanvasCreateTypeSchema[] = [
|
|
|
378
385
|
{ name: 'openInCanvas', type: 'boolean', required: false, description: 'Open the built artifact on the canvas (default true).' },
|
|
379
386
|
{ name: 'includeLogs', type: 'boolean', required: false, description: 'Include raw build stdout/stderr in the response (default false).' },
|
|
380
387
|
{ name: 'deps', type: 'string[]', required: false, description: 'Optional npm dependencies to add before bundling, e.g. recharts.', aliases: ['deps'] },
|
|
388
|
+
{ name: 'timeoutMs', type: 'number', required: false, description: 'Build command timeout in milliseconds. This controls subprocess timeout, not the MCP client request timeout.' },
|
|
381
389
|
],
|
|
382
390
|
example: {
|
|
383
391
|
title: 'Dashboard Artifact',
|
|
384
392
|
appTsx: 'export default function App() { return <main>Artifact</main>; }',
|
|
385
393
|
indexCss: 'body { background: #123456; color: white; }',
|
|
386
394
|
},
|
|
395
|
+
notes: [
|
|
396
|
+
'Cold builds can exceed default 60s MCP client timeouts; configure a longer MCP call timeout or retry with the same projectPath/outputPath if the first call times out.',
|
|
397
|
+
],
|
|
387
398
|
},
|
|
388
399
|
];
|
|
389
400
|
|
|
@@ -135,8 +135,6 @@ function normalizeExcalidrawBoundText(elements: Array<Record<string, unknown>>):
|
|
|
135
135
|
|
|
136
136
|
let changed = false;
|
|
137
137
|
const boundElementIdsByContainer = new Map<string, Set<string>>();
|
|
138
|
-
const labelByContainer = new Map<string, Record<string, unknown>>();
|
|
139
|
-
const textIdsConvertedToLabels = new Set<string>();
|
|
140
138
|
|
|
141
139
|
for (const element of elements) {
|
|
142
140
|
if (element.type !== 'text' || typeof element.id !== 'string' || typeof element.containerId !== 'string') continue;
|
|
@@ -145,48 +143,28 @@ function normalizeExcalidrawBoundText(elements: Array<Record<string, unknown>>):
|
|
|
145
143
|
const ids = boundElementIdsByContainer.get(element.containerId) ?? new Set<string>();
|
|
146
144
|
ids.add(element.id);
|
|
147
145
|
boundElementIdsByContainer.set(element.containerId, ids);
|
|
148
|
-
const text = typeof element.text === 'string' ? element.text.trim() : '';
|
|
149
|
-
if (!isRecord(container.label) && text.length > 0) {
|
|
150
|
-
labelByContainer.set(element.containerId, {
|
|
151
|
-
text,
|
|
152
|
-
...(typeof element.fontSize === 'number' && Number.isFinite(element.fontSize) ? { fontSize: element.fontSize } : {}),
|
|
153
|
-
});
|
|
154
|
-
textIdsConvertedToLabels.add(element.id);
|
|
155
|
-
}
|
|
156
146
|
}
|
|
157
147
|
|
|
158
|
-
const normalized = elements.
|
|
159
|
-
if (typeof element.id === 'string' && textIdsConvertedToLabels.has(element.id)) {
|
|
160
|
-
changed = true;
|
|
161
|
-
return [];
|
|
162
|
-
}
|
|
148
|
+
const normalized = elements.map((element) => {
|
|
163
149
|
if (typeof element.id !== 'string') return element;
|
|
164
150
|
const boundTextIds = boundElementIdsByContainer.get(element.id);
|
|
165
|
-
|
|
166
|
-
if ((!boundTextIds || boundTextIds.size === 0) && !label) return element;
|
|
151
|
+
if (!boundTextIds || boundTextIds.size === 0) return element;
|
|
167
152
|
|
|
168
153
|
const existing = Array.isArray(element.boundElements)
|
|
169
154
|
? element.boundElements.filter(isRecord)
|
|
170
155
|
: [];
|
|
171
|
-
const remainingExisting = existing.filter((boundElement) => {
|
|
172
|
-
return !(boundElement.type === 'text' && typeof boundElement.id === 'string' && textIdsConvertedToLabels.has(boundElement.id));
|
|
173
|
-
});
|
|
174
156
|
const existingTextIds = new Set(
|
|
175
|
-
|
|
157
|
+
existing
|
|
176
158
|
.filter((boundElement) => boundElement.type === 'text' && typeof boundElement.id === 'string')
|
|
177
159
|
.map((boundElement) => boundElement.id as string),
|
|
178
160
|
);
|
|
179
|
-
const missing = [...(
|
|
180
|
-
|
|
181
|
-
if (missing.length === 0 && !label && remainingExisting.length === existing.length) return element;
|
|
161
|
+
const missing = [...boundTextIds].filter((id) => !existingTextIds.has(id));
|
|
162
|
+
if (missing.length === 0) return element;
|
|
182
163
|
|
|
183
164
|
changed = true;
|
|
184
165
|
return {
|
|
185
166
|
...element,
|
|
186
|
-
...(
|
|
187
|
-
...(remainingExisting.length > 0 || missing.length > 0
|
|
188
|
-
? { boundElements: [...remainingExisting, ...missing.map((id) => ({ type: 'text', id }))] }
|
|
189
|
-
: {}),
|
|
167
|
+
boundElements: [...existing, ...missing.map((id) => ({ type: 'text', id }))],
|
|
190
168
|
};
|
|
191
169
|
});
|
|
192
170
|
|
package/src/server/server.ts
CHANGED
|
@@ -1639,6 +1639,7 @@ async function handleCanvasBuildWebArtifact(req: Request): Promise<Response> {
|
|
|
1639
1639
|
bytes: result.fileSize,
|
|
1640
1640
|
projectPath: result.projectPath,
|
|
1641
1641
|
openedInCanvas: result.openedInCanvas,
|
|
1642
|
+
completedAt: result.completedAt,
|
|
1642
1643
|
// `id` is the canvas node id alias used by every other add-style
|
|
1643
1644
|
// response. It is only present when a canvas node was actually
|
|
1644
1645
|
// created (i.e. openInCanvas was not explicitly disabled). When
|
|
@@ -116,12 +116,13 @@ else
|
|
|
116
116
|
echo "✅ Using Vite $VITE_VERSION (Node 18 compatible)"
|
|
117
117
|
fi
|
|
118
118
|
|
|
119
|
-
|
|
120
|
-
if [[ "$OSTYPE" == "darwin"* ]]; then
|
|
121
|
-
|
|
122
|
-
else
|
|
123
|
-
|
|
124
|
-
fi
|
|
119
|
+
function sed_in_place() {
|
|
120
|
+
if [[ "$OSTYPE" == "darwin"* ]]; then
|
|
121
|
+
sed -i '' "$@"
|
|
122
|
+
else
|
|
123
|
+
sed -i "$@"
|
|
124
|
+
fi
|
|
125
|
+
}
|
|
125
126
|
|
|
126
127
|
declare -a PNPM_CMD
|
|
127
128
|
configure_pnpm
|
|
@@ -159,8 +160,8 @@ fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');
|
|
|
159
160
|
"
|
|
160
161
|
|
|
161
162
|
echo "🧹 Cleaning up Vite template..."
|
|
162
|
-
|
|
163
|
-
|
|
163
|
+
sed_in_place '/<link rel="icon".*/d' index.html
|
|
164
|
+
sed_in_place 's/<title>.*<\/title>/<title>'"$PROJECT_NAME"'<\/title>/' index.html
|
|
164
165
|
|
|
165
166
|
echo "📦 Installing base dependencies..."
|
|
166
167
|
run_pnpm_quiet install
|
|
@@ -2,9 +2,11 @@ import { spawn } from 'node:child_process';
|
|
|
2
2
|
import {
|
|
3
3
|
copyFileSync,
|
|
4
4
|
existsSync,
|
|
5
|
+
readdirSync,
|
|
5
6
|
mkdirSync,
|
|
6
7
|
readFileSync,
|
|
7
8
|
statSync,
|
|
9
|
+
unlinkSync,
|
|
8
10
|
writeFileSync,
|
|
9
11
|
} from 'node:fs';
|
|
10
12
|
import { basename, delimiter, dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
@@ -71,6 +73,7 @@ export interface WebArtifactCanvasBuildResult extends WebArtifactBuildOutput {
|
|
|
71
73
|
openedInCanvas: boolean;
|
|
72
74
|
nodeId?: string;
|
|
73
75
|
url?: string;
|
|
76
|
+
completedAt: string;
|
|
74
77
|
}
|
|
75
78
|
|
|
76
79
|
function currentWorkspaceRoot(): string {
|
|
@@ -300,6 +303,14 @@ function writeProjectFiles(
|
|
|
300
303
|
}
|
|
301
304
|
}
|
|
302
305
|
|
|
306
|
+
function removeLiteralSedBackupFiles(projectPath: string): void {
|
|
307
|
+
for (const entry of readdirSync(projectPath, { withFileTypes: true })) {
|
|
308
|
+
if (entry.isFile() && entry.name.endsWith("''")) {
|
|
309
|
+
unlinkSync(join(projectPath, entry.name));
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
303
314
|
function ensurePackageManagerBoundary(dirPath: string): void {
|
|
304
315
|
const packageJsonPath = join(dirPath, 'package.json');
|
|
305
316
|
mkdirSync(dirPath, { recursive: true });
|
|
@@ -401,6 +412,7 @@ export async function executeWebArtifactBuild(
|
|
|
401
412
|
});
|
|
402
413
|
stdout = [stdout, initResult.stdout].filter(Boolean).join('\n');
|
|
403
414
|
stderr = [stderr, initResult.stderr].filter(Boolean).join('\n');
|
|
415
|
+
removeLiteralSedBackupFiles(projectPath);
|
|
404
416
|
}
|
|
405
417
|
|
|
406
418
|
writeProjectFiles(projectPath, input);
|
|
@@ -508,7 +520,7 @@ export async function buildWebArtifactOnCanvas(input: WebArtifactBuildInput & {
|
|
|
508
520
|
}): Promise<WebArtifactCanvasBuildResult> {
|
|
509
521
|
const build = await executeWebArtifactBuild(input);
|
|
510
522
|
if (input.openInCanvas === false) {
|
|
511
|
-
return { ...build, openedInCanvas: false };
|
|
523
|
+
return { ...build, openedInCanvas: false, completedAt: new Date().toISOString() };
|
|
512
524
|
}
|
|
513
525
|
const opened = openWebArtifactInCanvas({
|
|
514
526
|
title: input.title,
|
|
@@ -519,5 +531,6 @@ export async function buildWebArtifactOnCanvas(input: WebArtifactBuildInput & {
|
|
|
519
531
|
openedInCanvas: true,
|
|
520
532
|
nodeId: opened.nodeId,
|
|
521
533
|
url: opened.url,
|
|
534
|
+
completedAt: new Date().toISOString(),
|
|
522
535
|
};
|
|
523
536
|
}
|