pmx-canvas 0.1.24 → 0.1.25
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 +53 -0
- package/dist/canvas/index.js +23 -23
- package/dist/types/client/nodes/McpAppNode.d.ts +1 -0
- package/dist/types/server/canvas-operations.d.ts +4 -0
- package/dist/types/server/canvas-state.d.ts +1 -0
- package/package.json +1 -1
- package/skills/pmx-canvas/references/codex-app-adapter.md +8 -5
- package/skills/pmx-canvas/references/github-copilot-app-adapter.md +14 -0
- package/src/client/nodes/McpAppNode.tsx +13 -1
- package/src/client/state/sse-bridge.ts +1 -1
- package/src/server/canvas-operations.ts +17 -4
- package/src/server/canvas-state.ts +10 -2
- package/src/server/index.ts +11 -2
- package/src/server/mutation-history.ts +11 -0
- package/src/server/server.ts +119 -6
|
@@ -60,6 +60,10 @@ export declare const MARKDOWN_NODE_DEFAULT_SIZE: {
|
|
|
60
60
|
width: number;
|
|
61
61
|
height: number;
|
|
62
62
|
};
|
|
63
|
+
export declare const MCP_APP_NODE_DEFAULT_SIZE: {
|
|
64
|
+
width: number;
|
|
65
|
+
height: number;
|
|
66
|
+
};
|
|
63
67
|
interface CanvasCreateGroupInput {
|
|
64
68
|
title?: string;
|
|
65
69
|
childIds?: string[];
|
|
@@ -163,6 +163,7 @@ declare class CanvasStateManager {
|
|
|
163
163
|
private applyResolvedGroupBounds;
|
|
164
164
|
private getGroupSnapshot;
|
|
165
165
|
private normalizeNode;
|
|
166
|
+
private nodeForRead;
|
|
166
167
|
private reflowAllGroups;
|
|
167
168
|
private translateGroupChildren;
|
|
168
169
|
private recomputeParentGroupBounds;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmx-canvas",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.25",
|
|
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",
|
|
@@ -62,8 +62,10 @@ in the Codex in-app Browser, usually `http://127.0.0.1:4313/workbench` or
|
|
|
62
62
|
|
|
63
63
|
## Codex-Native Workflow
|
|
64
64
|
|
|
65
|
-
1.
|
|
66
|
-
|
|
65
|
+
1. Open `/workbench` in the Codex in-app Browser as the first visible action. If PMX is not running
|
|
66
|
+
yet, start/connect the MCP server first only long enough to get the workbench URL, then open the
|
|
67
|
+
browser before mutating the board.
|
|
68
|
+
2. Start or keep using the PMX MCP server for agent operations.
|
|
67
69
|
3. Use the browser canvas for human spatial curation: pin nodes, move nodes, group nodes, and
|
|
68
70
|
inspect rendered artifacts.
|
|
69
71
|
4. Use MCP tools for agent operations: create/update nodes, pin nodes, read layout, and read AX
|
|
@@ -90,9 +92,10 @@ Codex agents should treat PMX AX context as host-native working context:
|
|
|
90
92
|
|
|
91
93
|
## Live-Test Checklist
|
|
92
94
|
|
|
93
|
-
1.
|
|
94
|
-
|
|
95
|
-
|
|
95
|
+
1. Open `http://127.0.0.1:4313/workbench` in the Codex in-app Browser first so the user can see
|
|
96
|
+
all later canvas mutations.
|
|
97
|
+
2. Confirm the PMX MCP server is configured for the workspace.
|
|
98
|
+
3. Call `canvas_get_ax` and confirm it returns `ok: true`.
|
|
96
99
|
4. Add or reuse a node, then pin it from the browser or with `canvas_pin_nodes`.
|
|
97
100
|
5. Read `canvas://ax-context` and confirm the pinned node appears.
|
|
98
101
|
6. Call `canvas_set_ax_focus` with `source: "codex"` and a real node ID.
|
|
@@ -11,6 +11,20 @@ maps Copilot SDK features onto PMX AX primitives.
|
|
|
11
11
|
- Canvas ID: `pmx-canvas`
|
|
12
12
|
- Display name: `PMX Canvas`
|
|
13
13
|
|
|
14
|
+
## Quick Start
|
|
15
|
+
|
|
16
|
+
1. Install the project adapter by copying the packaged extension into the repository:
|
|
17
|
+
`mkdir -p .github/extensions/pmx-canvas && cp node_modules/pmx-canvas/.github/extensions/pmx-canvas/extension.mjs .github/extensions/pmx-canvas/extension.mjs`
|
|
18
|
+
2. Reload Copilot app extensions with `extensions_reload` so `project:pmx-canvas` is registered.
|
|
19
|
+
3. Start or confirm a PMX Canvas daemon for the workspace: `pmx-canvas serve --daemon`
|
|
20
|
+
and `pmx-canvas serve status`. The adapter can auto-start in many local sessions, but a running
|
|
21
|
+
daemon is the most reliable setup for fresh agents.
|
|
22
|
+
4. Open the canvas with `extensionId: "project:pmx-canvas"`, `canvasId: "pmx-canvas"`, and a stable
|
|
23
|
+
`instanceId`.
|
|
24
|
+
5. If the first `invoke_canvas_action` immediately after `open_canvas` returns
|
|
25
|
+
`Canvas instance not open`, retry the same action once. This is a known Copilot app timing race
|
|
26
|
+
during panel initialization, not a PMX server failure.
|
|
27
|
+
|
|
14
28
|
Open it with:
|
|
15
29
|
|
|
16
30
|
```json
|
|
@@ -14,6 +14,18 @@ function withViewerParams(url: string, expanded: boolean): string {
|
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
export function isSameOriginFrameDocumentUrl(url: string, origin = window.location.origin): boolean {
|
|
18
|
+
if (!url) return false;
|
|
19
|
+
try {
|
|
20
|
+
const baseOrigin = new URL(origin).origin;
|
|
21
|
+
const resolved = new URL(url, baseOrigin);
|
|
22
|
+
return resolved.origin === baseOrigin &&
|
|
23
|
+
resolved.pathname.startsWith('/api/canvas/frame-documents/');
|
|
24
|
+
} catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
17
29
|
export function McpAppNode({ node, expanded = false }: { node: CanvasNodeState; expanded?: boolean }) {
|
|
18
30
|
if (node.data.mode === 'ext-app') {
|
|
19
31
|
return <ExtAppFrame node={node} expanded={expanded} />;
|
|
@@ -23,7 +35,7 @@ export function McpAppNode({ node, expanded = false }: { node: CanvasNodeState;
|
|
|
23
35
|
const sourceServer = (node.data.sourceServer as string) || '';
|
|
24
36
|
const hostMode = (node.data.hostMode as string) || 'hosted';
|
|
25
37
|
const fallbackReason = node.data.fallbackReason as string | undefined;
|
|
26
|
-
const trustedDomain = node.data.trustedDomain
|
|
38
|
+
const trustedDomain = node.data.trustedDomain === true || isSameOriginFrameDocumentUrl(url);
|
|
27
39
|
|
|
28
40
|
if (hostMode === 'fallback') {
|
|
29
41
|
return (
|
|
@@ -75,7 +75,7 @@ const DEFAULT_POSITIONS: Record<
|
|
|
75
75
|
status: { x: 40, y: 80, w: 300, h: 120 },
|
|
76
76
|
markdown: { x: 380, y: 80, w: 720, h: 600 },
|
|
77
77
|
context: { x: 1130, y: 80, w: 320, h: 400 },
|
|
78
|
-
'mcp-app': { x: 380, y: 720, w:
|
|
78
|
+
'mcp-app': { x: 380, y: 720, w: 960, h: 600 },
|
|
79
79
|
webpage: { x: 380, y: 80, w: 520, h: 420 },
|
|
80
80
|
'json-render': { x: 380, y: 720, w: 840, h: 620 },
|
|
81
81
|
graph: { x: 380, y: 720, w: 760, h: 520 },
|
|
@@ -103,7 +103,8 @@ interface CanvasAddNodeInput {
|
|
|
103
103
|
strictSize?: boolean;
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
export const MARKDOWN_NODE_DEFAULT_SIZE = { width:
|
|
106
|
+
export const MARKDOWN_NODE_DEFAULT_SIZE = { width: 640, height: 420 };
|
|
107
|
+
export const MCP_APP_NODE_DEFAULT_SIZE = { width: 960, height: 600 };
|
|
107
108
|
|
|
108
109
|
interface CanvasCreateGroupInput {
|
|
109
110
|
title?: string;
|
|
@@ -1176,7 +1177,7 @@ function shiftGridUpdatesBelowObstacles(
|
|
|
1176
1177
|
}
|
|
1177
1178
|
|
|
1178
1179
|
export function arrangeCanvasNodes(layout: CanvasArrangeMode): { arranged: number; layout: CanvasArrangeMode } {
|
|
1179
|
-
const nodes = canvasState.
|
|
1180
|
+
const nodes = canvasState.getLayoutForPersistence().nodes;
|
|
1180
1181
|
const excludedIds = layout === 'grid'
|
|
1181
1182
|
? collectGridArrangeExcludedNodeIds(nodes)
|
|
1182
1183
|
: collectArrangeExcludedNodeIds(nodes);
|
|
@@ -1657,8 +1658,20 @@ export async function executeCanvasBatch(
|
|
|
1657
1658
|
...(typeof args.width === 'number' ? { width: args.width } : {}),
|
|
1658
1659
|
...(typeof args.height === 'number' ? { height: args.height } : {}),
|
|
1659
1660
|
...(args.strictSize === true ? { strictSize: true } : {}),
|
|
1660
|
-
defaultWidth: type === 'html'
|
|
1661
|
-
|
|
1661
|
+
defaultWidth: type === 'html'
|
|
1662
|
+
? 720
|
|
1663
|
+
: type === 'markdown'
|
|
1664
|
+
? MARKDOWN_NODE_DEFAULT_SIZE.width
|
|
1665
|
+
: type === 'mcp-app'
|
|
1666
|
+
? MCP_APP_NODE_DEFAULT_SIZE.width
|
|
1667
|
+
: 360,
|
|
1668
|
+
defaultHeight: type === 'html'
|
|
1669
|
+
? 640
|
|
1670
|
+
: type === 'markdown'
|
|
1671
|
+
? MARKDOWN_NODE_DEFAULT_SIZE.height
|
|
1672
|
+
: type === 'mcp-app'
|
|
1673
|
+
? MCP_APP_NODE_DEFAULT_SIZE.height
|
|
1674
|
+
: 200,
|
|
1662
1675
|
fileMode: 'auto',
|
|
1663
1676
|
});
|
|
1664
1677
|
result = { ok: true, ...serializeCreatedNode(created.node) };
|
|
@@ -400,6 +400,14 @@ class CanvasStateManager {
|
|
|
400
400
|
return normalized;
|
|
401
401
|
}
|
|
402
402
|
|
|
403
|
+
private nodeForRead(node: CanvasNodeState): CanvasNodeState {
|
|
404
|
+
const resolved = this.resolveNodeDataBlobs(node);
|
|
405
|
+
return {
|
|
406
|
+
...resolved,
|
|
407
|
+
pinned: resolved.pinned || this._contextPinnedNodeIds.has(resolved.id),
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
403
411
|
private reflowAllGroups(): void {
|
|
404
412
|
const groups = Array.from(this.nodes.values())
|
|
405
413
|
.filter((node): node is CanvasNodeState => node.type === 'group')
|
|
@@ -1379,7 +1387,7 @@ class CanvasStateManager {
|
|
|
1379
1387
|
|
|
1380
1388
|
getNode(id: string): CanvasNodeState | undefined {
|
|
1381
1389
|
const node = this.nodes.get(id);
|
|
1382
|
-
return node ? structuredClone(this.
|
|
1390
|
+
return node ? structuredClone(this.nodeForRead(node)) : undefined;
|
|
1383
1391
|
}
|
|
1384
1392
|
|
|
1385
1393
|
getNodeForPersistence(id: string): CanvasNodeState | undefined {
|
|
@@ -1482,7 +1490,7 @@ class CanvasStateManager {
|
|
|
1482
1490
|
return {
|
|
1483
1491
|
viewport: structuredClone(this._viewport),
|
|
1484
1492
|
theme: this._theme,
|
|
1485
|
-
nodes: Array.from(this.nodes.values(), (node) => structuredClone(node)),
|
|
1493
|
+
nodes: Array.from(this.nodes.values(), (node) => structuredClone(this.nodeForRead(node))),
|
|
1486
1494
|
edges: Array.from(this.edges.values(), (edge) => structuredClone(edge)),
|
|
1487
1495
|
annotations: this.getAnnotations(),
|
|
1488
1496
|
};
|
package/src/server/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
addCanvasNode,
|
|
14
14
|
addCanvasEdge,
|
|
15
15
|
MARKDOWN_NODE_DEFAULT_SIZE,
|
|
16
|
+
MCP_APP_NODE_DEFAULT_SIZE,
|
|
16
17
|
applyCanvasNodeUpdates,
|
|
17
18
|
arrangeCanvasNodes,
|
|
18
19
|
clearCanvas,
|
|
@@ -183,8 +184,16 @@ export class PmxCanvas extends EventEmitter {
|
|
|
183
184
|
}
|
|
184
185
|
const { id, needsCodeGraphRecompute } = addCanvasNode({
|
|
185
186
|
...input,
|
|
186
|
-
defaultWidth: input.type === 'markdown'
|
|
187
|
-
|
|
187
|
+
defaultWidth: input.type === 'markdown'
|
|
188
|
+
? MARKDOWN_NODE_DEFAULT_SIZE.width
|
|
189
|
+
: input.type === 'mcp-app'
|
|
190
|
+
? MCP_APP_NODE_DEFAULT_SIZE.width
|
|
191
|
+
: 360,
|
|
192
|
+
defaultHeight: input.type === 'markdown'
|
|
193
|
+
? MARKDOWN_NODE_DEFAULT_SIZE.height
|
|
194
|
+
: input.type === 'mcp-app'
|
|
195
|
+
? MCP_APP_NODE_DEFAULT_SIZE.height
|
|
196
|
+
: 200,
|
|
188
197
|
fileMode: 'path',
|
|
189
198
|
...(input.strictSize ? { strictSize: true } : {}),
|
|
190
199
|
});
|
|
@@ -66,6 +66,13 @@ export interface SnapshotDiffResult {
|
|
|
66
66
|
removedEdges: { id: string; from: string; to: string; type: string }[];
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
function comparableNodeData(data: Record<string, unknown>): Record<string, unknown> {
|
|
70
|
+
const comparable = { ...data };
|
|
71
|
+
delete comparable.title;
|
|
72
|
+
delete comparable.content;
|
|
73
|
+
return comparable;
|
|
74
|
+
}
|
|
75
|
+
|
|
69
76
|
// ── Ring Buffer ──────────────────────────────────────────────────────
|
|
70
77
|
|
|
71
78
|
const MAX_ENTRIES = 200;
|
|
@@ -258,6 +265,10 @@ export function diffLayouts(
|
|
|
258
265
|
changes.push(`content changed (${lenDiff >= 0 ? '+' : ''}${lenDiff} chars)`);
|
|
259
266
|
}
|
|
260
267
|
|
|
268
|
+
if (JSON.stringify(comparableNodeData(snapNode.data)) !== JSON.stringify(comparableNodeData(curNode.data))) {
|
|
269
|
+
changes.push('data changed');
|
|
270
|
+
}
|
|
271
|
+
|
|
261
272
|
if (changes.length > 0) {
|
|
262
273
|
modifiedNodes.push({
|
|
263
274
|
id,
|
package/src/server/server.ts
CHANGED
|
@@ -47,7 +47,7 @@ import type {
|
|
|
47
47
|
ListResourceTemplatesResult,
|
|
48
48
|
ListToolsResult,
|
|
49
49
|
} from '@modelcontextprotocol/sdk/types.js';
|
|
50
|
-
import { type CanvasAnnotation, type CanvasEdge, type CanvasNodeState, IMAGE_MIME_MAP, canvasState } from './canvas-state.js';
|
|
50
|
+
import { type CanvasAnnotation, type CanvasEdge, type CanvasLayout, type CanvasNodeState, IMAGE_MIME_MAP, canvasState } from './canvas-state.js';
|
|
51
51
|
import { findCanvasExtAppNodeId as findCanvasExtAppNodeIdShared } from './ext-app-lookup.js';
|
|
52
52
|
import { normalizeExtAppToolResult } from './ext-app-tool-result.js';
|
|
53
53
|
import { getMcpAppHostSnapshot } from './mcp-app-host.js';
|
|
@@ -84,6 +84,7 @@ import {
|
|
|
84
84
|
addCanvasNode,
|
|
85
85
|
addCanvasEdge,
|
|
86
86
|
MARKDOWN_NODE_DEFAULT_SIZE,
|
|
87
|
+
MCP_APP_NODE_DEFAULT_SIZE,
|
|
87
88
|
applyCanvasNodeUpdates,
|
|
88
89
|
buildStructuredNodeUpdate,
|
|
89
90
|
arrangeCanvasNodes,
|
|
@@ -790,6 +791,64 @@ function parseGraphPayloadData(value: unknown): Array<Record<string, unknown>> |
|
|
|
790
791
|
return value as Array<Record<string, unknown>>;
|
|
791
792
|
}
|
|
792
793
|
|
|
794
|
+
type StringListField = { value?: string[]; error?: string };
|
|
795
|
+
|
|
796
|
+
function parseStringListField(field: string, value: unknown): StringListField {
|
|
797
|
+
if (value === undefined) return {};
|
|
798
|
+
if (!Array.isArray(value)) return { error: `"${field}" must be an array of node IDs.` };
|
|
799
|
+
const invalid = value.find((item) => typeof item !== 'string' || item.trim().length === 0);
|
|
800
|
+
if (invalid !== undefined) return { error: `"${field}" must contain only non-empty node IDs.` };
|
|
801
|
+
return { value };
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
function pickGroupChildIds(body: Record<string, unknown>): StringListField {
|
|
805
|
+
if ('children' in body) return parseStringListField('children', body.children);
|
|
806
|
+
if ('childIds' in body) return parseStringListField('childIds', body.childIds);
|
|
807
|
+
const data = isRecord(body.data) ? body.data : undefined;
|
|
808
|
+
return data && 'children' in data ? parseStringListField('data.children', data.children) : {};
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
function validateGroupChildIds(groupId: string, childIds: string[]): string | null {
|
|
812
|
+
const missingChildIds = childIds.filter((id) => !canvasState.getNode(id));
|
|
813
|
+
if (missingChildIds.length > 0) {
|
|
814
|
+
return `Missing child node ID${missingChildIds.length === 1 ? '' : 's'}: ${missingChildIds.join(', ')}.`;
|
|
815
|
+
}
|
|
816
|
+
const invalidChildIds = childIds.filter((id) => {
|
|
817
|
+
const node = canvasState.getNode(id);
|
|
818
|
+
return id === groupId || node?.type === 'group';
|
|
819
|
+
});
|
|
820
|
+
if (invalidChildIds.length > 0) {
|
|
821
|
+
return `Invalid group child ID${invalidChildIds.length === 1 ? '' : 's'}: ${invalidChildIds.join(', ')}.`;
|
|
822
|
+
}
|
|
823
|
+
return null;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function setGroupChildrenFromApi(groupId: string, childIds: string[]): boolean {
|
|
827
|
+
const group = canvasState.getNode(groupId);
|
|
828
|
+
if (!group || group.type !== 'group') return false;
|
|
829
|
+
|
|
830
|
+
const dataChildIds = Array.isArray(group.data.children)
|
|
831
|
+
? group.data.children.filter((id): id is string => typeof id === 'string')
|
|
832
|
+
: [];
|
|
833
|
+
const parentBackrefIds = canvasState.getLayout().nodes
|
|
834
|
+
.filter((node) => node.id !== groupId && node.data.parentGroup === groupId)
|
|
835
|
+
.map((node) => node.id);
|
|
836
|
+
const currentChildIds = [...new Set([...dataChildIds, ...parentBackrefIds])];
|
|
837
|
+
if (currentChildIds.length > 0) {
|
|
838
|
+
if (currentChildIds.length !== dataChildIds.length || currentChildIds.some((id) => !dataChildIds.includes(id))) {
|
|
839
|
+
canvasState.updateNode(groupId, { data: { ...group.data, children: currentChildIds } });
|
|
840
|
+
}
|
|
841
|
+
canvasState.ungroupNodes(groupId);
|
|
842
|
+
}
|
|
843
|
+
if (childIds.length === 0) return true;
|
|
844
|
+
|
|
845
|
+
const latestGroup = canvasState.getNode(groupId);
|
|
846
|
+
return canvasState.groupNodes(groupId, childIds, {
|
|
847
|
+
preservePositions: true,
|
|
848
|
+
keepGroupFrame: latestGroup?.data.frameMode === 'manual',
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
|
|
793
852
|
function getExtAppNodeCheckpointId(node: CanvasNodeState): string {
|
|
794
853
|
const appCheckpoint = isRecord(node.data.appCheckpoint) ? node.data.appCheckpoint : null;
|
|
795
854
|
const storedCheckpointId = appCheckpoint?.id;
|
|
@@ -1467,6 +1526,20 @@ function buildNodeResponse(node: CanvasNodeState): Record<string, unknown> {
|
|
|
1467
1526
|
};
|
|
1468
1527
|
}
|
|
1469
1528
|
|
|
1529
|
+
function withContextPinReadState(node: CanvasNodeState): CanvasNodeState {
|
|
1530
|
+
return {
|
|
1531
|
+
...node,
|
|
1532
|
+
pinned: node.pinned || canvasState.contextPinnedNodeIds.has(node.id),
|
|
1533
|
+
};
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
function withContextPinLayoutReadState(layout: CanvasLayout): CanvasLayout {
|
|
1537
|
+
return {
|
|
1538
|
+
...layout,
|
|
1539
|
+
nodes: layout.nodes.map(withContextPinReadState),
|
|
1540
|
+
};
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1470
1543
|
async function createCanvasWebpageNode(body: Record<string, unknown>): Promise<Response> {
|
|
1471
1544
|
const rawUrl = typeof body.url === 'string' && body.url.trim().length > 0
|
|
1472
1545
|
? body.url
|
|
@@ -1547,6 +1620,23 @@ async function handleCanvasAddNode(req: Request): Promise<Response> {
|
|
|
1547
1620
|
return createCanvasHtmlPrimitiveNode(body);
|
|
1548
1621
|
}
|
|
1549
1622
|
|
|
1623
|
+
if (type === 'group') {
|
|
1624
|
+
const geometry = resolveCreateGeometry(body);
|
|
1625
|
+
const childList = pickGroupChildIds(body);
|
|
1626
|
+
if (childList.error) return responseJson({ ok: false, error: `Cannot create group: ${childList.error}` }, 400);
|
|
1627
|
+
const childIds = childList.value ?? [];
|
|
1628
|
+
const childError = validateGroupChildIds('', childIds);
|
|
1629
|
+
if (childError) return responseJson({ ok: false, error: `Cannot create group: ${childError}` }, 400);
|
|
1630
|
+
const { node } = createCanvasGroup({
|
|
1631
|
+
...(typeof body.title === 'string' ? { title: body.title } : {}),
|
|
1632
|
+
childIds,
|
|
1633
|
+
...(typeof body.color === 'string' ? { color: body.color } : {}),
|
|
1634
|
+
...geometry,
|
|
1635
|
+
});
|
|
1636
|
+
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
1637
|
+
return responseJson(buildNodeResponse(node));
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1550
1640
|
const extraData = body.data && typeof body.data === 'object' && !Array.isArray(body.data)
|
|
1551
1641
|
? body.data as Record<string, unknown>
|
|
1552
1642
|
: undefined;
|
|
@@ -1592,8 +1682,20 @@ async function handleCanvasAddNode(req: Request): Promise<Response> {
|
|
|
1592
1682
|
...(type === 'trace' && typeof body.error === 'string' ? { error: body.error } : {}),
|
|
1593
1683
|
...(body.strictSize === true ? { strictSize: true } : {}),
|
|
1594
1684
|
...geometry,
|
|
1595
|
-
defaultWidth: type === 'html'
|
|
1596
|
-
|
|
1685
|
+
defaultWidth: type === 'html'
|
|
1686
|
+
? 720
|
|
1687
|
+
: type === 'markdown'
|
|
1688
|
+
? MARKDOWN_NODE_DEFAULT_SIZE.width
|
|
1689
|
+
: type === 'mcp-app'
|
|
1690
|
+
? MCP_APP_NODE_DEFAULT_SIZE.width
|
|
1691
|
+
: 360,
|
|
1692
|
+
defaultHeight: type === 'html'
|
|
1693
|
+
? 640
|
|
1694
|
+
: type === 'markdown'
|
|
1695
|
+
? MARKDOWN_NODE_DEFAULT_SIZE.height
|
|
1696
|
+
: type === 'mcp-app'
|
|
1697
|
+
? MCP_APP_NODE_DEFAULT_SIZE.height
|
|
1698
|
+
: 200,
|
|
1597
1699
|
fileMode: 'auto',
|
|
1598
1700
|
});
|
|
1599
1701
|
} catch (error) {
|
|
@@ -1797,6 +1899,13 @@ async function handleCanvasUpdateNode(nodeId: string, req: Request): Promise<Res
|
|
|
1797
1899
|
if (existing.type === 'webpage' && body.refresh === true) {
|
|
1798
1900
|
return handleCanvasRefreshWebpageNode(nodeId, req);
|
|
1799
1901
|
}
|
|
1902
|
+
const groupChildList = existing.type === 'group' ? pickGroupChildIds(body) : {};
|
|
1903
|
+
if (groupChildList.error) return responseJson({ ok: false, error: `Cannot update group: ${groupChildList.error}` }, 400);
|
|
1904
|
+
const groupChildIds = groupChildList.value;
|
|
1905
|
+
if (groupChildIds !== undefined) {
|
|
1906
|
+
const childError = validateGroupChildIds(nodeId, groupChildIds);
|
|
1907
|
+
if (childError) return responseJson({ ok: false, error: `Cannot update group: ${childError}` }, 400);
|
|
1908
|
+
}
|
|
1800
1909
|
const patch: Record<string, unknown> = resolvePatchGeometry(body, existing);
|
|
1801
1910
|
if (body.collapsed !== undefined) patch.collapsed = body.collapsed;
|
|
1802
1911
|
if (body.pinned !== undefined) patch.pinned = Boolean(body.pinned);
|
|
@@ -1855,6 +1964,9 @@ async function handleCanvasUpdateNode(nodeId: string, req: Request): Promise<Res
|
|
|
1855
1964
|
});
|
|
1856
1965
|
if (error) return responseJson({ ok: false, error }, 400);
|
|
1857
1966
|
canvasState.updateNode(nodeId, patch as Partial<CanvasNodeState>);
|
|
1967
|
+
if (groupChildIds !== undefined && !setGroupChildrenFromApi(nodeId, groupChildIds)) {
|
|
1968
|
+
return responseJson({ ok: false, error: `Group "${nodeId}" not found.` }, 404);
|
|
1969
|
+
}
|
|
1858
1970
|
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
1859
1971
|
const updated = canvasState.getNode(nodeId);
|
|
1860
1972
|
return responseJson(updated ? buildNodeResponse(updated) : { ok: true, id: nodeId });
|
|
@@ -4346,7 +4458,7 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
4346
4458
|
const includeBlobs = url.searchParams.get('includeBlobs') === 'true';
|
|
4347
4459
|
return responseJson(includeBlobs
|
|
4348
4460
|
? serializeCanvasLayout(canvasState.getLayout())
|
|
4349
|
-
: serializeCanvasLayoutWithBlobSummaries(canvasState.getLayoutForPersistence()));
|
|
4461
|
+
: serializeCanvasLayoutWithBlobSummaries(withContextPinLayoutReadState(canvasState.getLayoutForPersistence())));
|
|
4350
4462
|
}
|
|
4351
4463
|
|
|
4352
4464
|
if (url.pathname === '/api/canvas/summary' && req.method === 'GET') {
|
|
@@ -4416,9 +4528,10 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
4416
4528
|
const includeBlobs = url.searchParams.get('includeBlobs') === 'true';
|
|
4417
4529
|
const node = includeBlobs ? canvasState.getNode(nodeId) : canvasState.getNodeForPersistence(nodeId);
|
|
4418
4530
|
if (!node) return responseJson({ ok: false, error: `Node "${nodeId}" not found.` }, 404);
|
|
4531
|
+
const responseNode = withContextPinReadState(node);
|
|
4419
4532
|
return responseJson(includeBlobs
|
|
4420
|
-
? serializeCanvasNode(
|
|
4421
|
-
: serializeCanvasNodeWithBlobSummaries(
|
|
4533
|
+
? serializeCanvasNode(responseNode)
|
|
4534
|
+
: serializeCanvasNodeWithBlobSummaries(responseNode));
|
|
4422
4535
|
}
|
|
4423
4536
|
|
|
4424
4537
|
if (url.pathname.startsWith('/api/canvas/node/') && req.method === 'PATCH') {
|