pmx-canvas 0.6.4 → 0.7.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.
- package/.github/extensions/pmx-canvas/extension.mjs +67 -3
- package/CHANGELOG.md +73 -0
- package/Readme.md +35 -5
- package/dist/canvas/global.css +498 -28
- package/dist/canvas/index.js +110 -95
- package/dist/canvas/mermaid-entry.js +258 -258
- package/dist/types/cli/agent.d.ts +4 -0
- package/dist/types/cli/commands/backup.d.ts +1 -0
- package/dist/types/cli/commands/boards.d.ts +1 -0
- package/dist/types/cli/commands/export.d.ts +1 -0
- package/dist/types/cli/commands/record.d.ts +12 -0
- package/dist/types/client/canvas/BoardCopyDialog.d.ts +5 -0
- package/dist/types/client/canvas/BoardFolderDialog.d.ts +7 -0
- package/dist/types/client/canvas/BoardLinkDialog.d.ts +3 -0
- package/dist/types/client/canvas/BoardSwitcher.d.ts +8 -0
- package/dist/types/client/canvas/ExportDialog.d.ts +7 -0
- package/dist/types/client/canvas/HomeView.d.ts +9 -0
- package/dist/types/client/canvas/Presentation.d.ts +13 -0
- package/dist/types/client/canvas/use-node-drag.d.ts +2 -1
- package/dist/types/client/nodes/AttachmentNode.d.ts +6 -0
- package/dist/types/client/nodes/BoardNode.d.ts +5 -0
- package/dist/types/client/nodes/McpAppNode.d.ts +24 -1
- package/dist/types/client/state/boards-store.d.ts +79 -0
- package/dist/types/client/state/canvas-store.d.ts +2 -0
- package/dist/types/client/state/intent-bridge.d.ts +1 -0
- package/dist/types/client/state/presentation.d.ts +3 -0
- package/dist/types/client/state/session-store.d.ts +7 -0
- package/dist/types/client/types.d.ts +2 -2
- package/dist/types/json-render/catalog.d.ts +3 -3
- package/dist/types/json-render/charts/definitions.d.ts +1 -1
- package/dist/types/json-render/server.d.ts +8 -0
- package/dist/types/mcp/canvas-access.d.ts +10 -3
- package/dist/types/mcp/context-uri-template.d.ts +6 -0
- package/dist/types/server/agent-presence.d.ts +1 -1
- package/dist/types/server/attribution.d.ts +11 -0
- package/dist/types/server/ax-context.d.ts +3 -1
- package/dist/types/server/ax-interaction.d.ts +2 -2
- package/dist/types/server/ax-state.d.ts +13 -3
- package/dist/types/server/backup.d.ts +36 -0
- package/dist/types/server/board-export.d.ts +45 -0
- package/dist/types/server/canvas-db.d.ts +65 -9
- package/dist/types/server/canvas-operations.d.ts +45 -0
- package/dist/types/server/canvas-provenance.d.ts +1 -1
- package/dist/types/server/canvas-schema.d.ts +1 -0
- package/dist/types/server/canvas-state.d.ts +146 -3
- package/dist/types/server/canvas-validation.d.ts +3 -0
- package/dist/types/server/context-brief.d.ts +111 -0
- package/dist/types/server/context-reads.d.ts +55 -0
- package/dist/types/server/document-import.d.ts +66 -0
- package/dist/types/server/html-surface.d.ts +1 -1
- package/dist/types/server/index.d.ts +107 -4
- package/dist/types/server/mutation-history.d.ts +1 -0
- package/dist/types/server/operations/invoker.d.ts +2 -0
- package/dist/types/server/operations/mcp.d.ts +7 -1
- package/dist/types/server/operations/ops/app.d.ts +2 -0
- package/dist/types/server/operations/ops/ax-shared.d.ts +2 -2
- package/dist/types/server/operations/ops/backup.d.ts +2 -0
- package/dist/types/server/operations/ops/boards.d.ts +10 -0
- package/dist/types/server/operations/ops/export.d.ts +2 -0
- package/dist/types/server/operations/ops/imports.d.ts +2 -0
- package/dist/types/server/operations/ops/json-render.d.ts +2 -0
- package/dist/types/server/operations/ops/nodes.d.ts +4 -2
- package/dist/types/server/operations/ops/query.d.ts +2 -0
- package/dist/types/server/operations/registry.d.ts +9 -1
- package/dist/types/server/operations/types.d.ts +1 -1
- package/dist/types/server/server.d.ts +1 -0
- package/dist/types/server/trace-manager.d.ts +7 -0
- package/dist/types/server/web-artifacts.d.ts +3 -0
- package/dist/types/server/webpage-node.d.ts +22 -1
- package/dist/types/server/workbench-auth.d.ts +3 -0
- package/dist/types/shared/boards.d.ts +6 -0
- package/dist/types/shared/tour.d.ts +72 -0
- package/docs/RELEASE.md +5 -0
- package/docs/cli.md +157 -0
- package/docs/http-api.md +277 -6
- package/docs/mcp.md +96 -7
- package/docs/node-types.md +106 -0
- package/docs/plans/plan-010-attributed-ask-answers.md +39 -0
- package/docs/plans/plan-011-context-read-instrumentation.md +54 -0
- package/docs/plans/plan-012-many-boards-stage-1.md +98 -0
- package/docs/plans/plan-013-static-board-export.md +47 -0
- package/docs/plans/plan-014-connected-board-memory.md +140 -0
- package/docs/product-vision-2026-09.md +118 -18
- package/docs/sdk.md +50 -0
- package/package.json +1 -1
- package/skills/pmx-canvas/SKILL.md +62 -5
- package/skills/pmx-canvas/references/full-reference.md +44 -8
- package/skills/pmx-canvas/references/github-copilot-app-adapter.md +3 -0
- package/skills/pmx-canvas/references/installing-pmx-canvas.md +4 -4
- package/skills/pmx-canvas-orchestration/SKILL.md +5 -0
- package/skills/pmx-canvas-testing/SKILL.md +4 -0
- package/src/cli/agent.ts +9 -0
- package/src/cli/commands/ax.ts +13 -0
- package/src/cli/commands/backup.ts +66 -0
- package/src/cli/commands/boards.ts +71 -0
- package/src/cli/commands/export.ts +30 -0
- package/src/cli/commands/nodes.ts +14 -1
- package/src/cli/commands/pins.ts +6 -2
- package/src/cli/commands/pump.ts +3 -0
- package/src/cli/commands/query.ts +33 -7
- package/src/cli/commands/record.ts +218 -0
- package/src/cli/commands/view.ts +7 -5
- package/src/cli/index.ts +21 -9
- package/src/client/App.tsx +28 -4
- package/src/client/canvas/AgentPresenceLayer.tsx +1 -1
- package/src/client/canvas/BoardCopyDialog.tsx +153 -0
- package/src/client/canvas/BoardFolderDialog.tsx +120 -0
- package/src/client/canvas/BoardLinkDialog.tsx +93 -0
- package/src/client/canvas/BoardSwitcher.tsx +149 -0
- package/src/client/canvas/CanvasNode.tsx +8 -0
- package/src/client/canvas/CanvasViewport.tsx +3 -0
- package/src/client/canvas/CommandPalette.tsx +107 -4
- package/src/client/canvas/ContextMenu.tsx +4 -0
- package/src/client/canvas/ExpandedNodeOverlay.tsx +3 -0
- package/src/client/canvas/ExportDialog.tsx +197 -0
- package/src/client/canvas/HomeView.tsx +277 -0
- package/src/client/canvas/IntentLayer.tsx +9 -2
- package/src/client/canvas/Presentation.tsx +108 -0
- package/src/client/canvas/SessionPanel.tsx +28 -1
- package/src/client/canvas/ToolRail.tsx +38 -3
- package/src/client/canvas/TopBar.tsx +49 -7
- package/src/client/canvas/import-files.ts +19 -1
- package/src/client/canvas/kind-colors.ts +1 -0
- package/src/client/canvas/use-node-drag.ts +8 -3
- package/src/client/icons.tsx +2 -0
- package/src/client/mermaid-entry.ts +39 -0
- package/src/client/nodes/AttachmentNode.tsx +148 -0
- package/src/client/nodes/BoardNode.tsx +35 -0
- package/src/client/nodes/FileNode.tsx +6 -0
- package/src/client/nodes/HtmlNode.tsx +12 -13
- package/src/client/nodes/MarkdownNode.tsx +47 -19
- package/src/client/nodes/McpAppNode.tsx +105 -13
- package/src/client/nodes/MermaidNode.tsx +3 -2
- package/src/client/nodes/ax-node-actions.ts +2 -5
- package/src/client/nodes/use-ax-surface-bridge.ts +1 -1
- package/src/client/state/boards-store.ts +167 -0
- package/src/client/state/canvas-store.ts +22 -0
- package/src/client/state/intent-bridge.ts +30 -2
- package/src/client/state/intent-store.ts +5 -1
- package/src/client/state/node-factory.ts +1 -0
- package/src/client/state/presentation.ts +7 -0
- package/src/client/state/session-store.ts +8 -0
- package/src/client/state/sse-bridge.ts +52 -5
- package/src/client/theme/global.css +498 -28
- package/src/client/types.ts +2 -0
- package/src/json-render/server.ts +45 -9
- package/src/mcp/canvas-access.ts +59 -22
- package/src/mcp/context-uri-template.ts +28 -0
- package/src/mcp/server.ts +122 -15
- package/src/server/agent-context.ts +116 -29
- package/src/server/agent-presence.ts +15 -9
- package/src/server/attribution.ts +29 -0
- package/src/server/ax-context.ts +4 -2
- package/src/server/ax-interaction.ts +1 -0
- package/src/server/ax-state-manager.ts +72 -3
- package/src/server/ax-state.ts +41 -0
- package/src/server/backup.ts +178 -0
- package/src/server/board-export.ts +686 -0
- package/src/server/canvas-db.ts +612 -105
- package/src/server/canvas-operations.ts +183 -31
- package/src/server/canvas-provenance.ts +1 -0
- package/src/server/canvas-schema.ts +22 -0
- package/src/server/canvas-state.ts +812 -48
- package/src/server/canvas-validation.ts +9 -1
- package/src/server/context-brief.ts +372 -0
- package/src/server/context-reads.ts +238 -0
- package/src/server/demo-state.json +670 -9
- package/src/server/document-import.ts +223 -0
- package/src/server/html-primitives.ts +199 -91
- package/src/server/html-surface.ts +8 -3
- package/src/server/index.ts +243 -5
- package/src/server/mutation-history.ts +10 -13
- package/src/server/operations/composites.ts +29 -5
- package/src/server/operations/http.ts +9 -3
- package/src/server/operations/index.ts +8 -0
- package/src/server/operations/invoker.ts +14 -3
- package/src/server/operations/mcp.ts +50 -11
- package/src/server/operations/ops/app.ts +15 -0
- package/src/server/operations/ops/ax-read.ts +10 -6
- package/src/server/operations/ops/ax-timeline.ts +75 -0
- package/src/server/operations/ops/backup.ts +134 -0
- package/src/server/operations/ops/batch.ts +20 -4
- package/src/server/operations/ops/boards.ts +351 -0
- package/src/server/operations/ops/export.ts +74 -0
- package/src/server/operations/ops/ext-app.ts +4 -0
- package/src/server/operations/ops/groups.ts +14 -7
- package/src/server/operations/ops/imports.ts +220 -0
- package/src/server/operations/ops/json-render.ts +37 -5
- package/src/server/operations/ops/nodes.ts +99 -21
- package/src/server/operations/ops/query.ts +206 -9
- package/src/server/operations/ops/viewport.ts +32 -0
- package/src/server/operations/registry.ts +129 -31
- package/src/server/operations/types.ts +1 -1
- package/src/server/scope-fence.ts +1 -1
- package/src/server/server.ts +155 -8
- package/src/server/spatial-analysis.ts +9 -17
- package/src/server/trace-manager.ts +19 -0
- package/src/server/web-artifacts.ts +11 -3
- package/src/server/webpage-node.ts +173 -13
- package/src/server/workbench-auth.ts +11 -0
- package/src/server/workboard.ts +3 -3
- package/src/shared/boards.ts +18 -0
- package/src/shared/node-sizes.ts +1 -0
- package/src/shared/tour.ts +87 -0
|
@@ -5,6 +5,7 @@ import { recomputeCodeGraph } from './code-graph.js';
|
|
|
5
5
|
import { diffLayouts } from './mutation-history.js';
|
|
6
6
|
import {
|
|
7
7
|
canvasState,
|
|
8
|
+
defaultBoardName,
|
|
8
9
|
type CanvasEdge,
|
|
9
10
|
type CanvasNodeState,
|
|
10
11
|
type CanvasNodeUpdate,
|
|
@@ -20,6 +21,7 @@ import {
|
|
|
20
21
|
type ExternalMcpTransportConfig,
|
|
21
22
|
} from './mcp-app-runtime.js';
|
|
22
23
|
import { mutationHistory } from './mutation-history.js';
|
|
24
|
+
import { intentRegistry } from './intent-registry.js';
|
|
23
25
|
import { clampCreateNodeSize } from './canvas-validation.js';
|
|
24
26
|
import { computeGroupBounds, findOpenCanvasPosition } from './placement.js';
|
|
25
27
|
import { searchNodes } from './spatial-analysis.js';
|
|
@@ -33,6 +35,7 @@ import {
|
|
|
33
35
|
emptyStreamingSpec,
|
|
34
36
|
GRAPH_NODE_SIZE,
|
|
35
37
|
inferJsonRenderNodeTitle,
|
|
38
|
+
jsonRenderFormWarnings,
|
|
36
39
|
JSON_RENDER_NODE_SIZE,
|
|
37
40
|
normalizeAndValidateJsonRenderSpec,
|
|
38
41
|
type GraphNodeInput,
|
|
@@ -118,6 +121,29 @@ export const MCP_APP_NODE_DEFAULT_SIZE = { width: 960, height: 600 };
|
|
|
118
121
|
export const IMAGE_NODE_DEFAULT_SIZE = { width: 480, height: 360 };
|
|
119
122
|
export const LEDGER_NODE_DEFAULT_SIZE = { width: 420, height: 280 };
|
|
120
123
|
|
|
124
|
+
export interface CanvasSizeAdjustment {
|
|
125
|
+
requested: { width: number | null; height: number | null };
|
|
126
|
+
applied: { width: number; height: number };
|
|
127
|
+
reason: 'defaulted' | 'clamped-to-minimum' | 'defaulted-and-clamped-to-minimum' | 'fit-to-children';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function describeCreateSize(
|
|
131
|
+
requestedWidth: number | undefined,
|
|
132
|
+
requestedHeight: number | undefined,
|
|
133
|
+
resolvedWidth: number,
|
|
134
|
+
resolvedHeight: number,
|
|
135
|
+
applied: { width: number; height: number },
|
|
136
|
+
): CanvasSizeAdjustment | undefined {
|
|
137
|
+
const defaulted = requestedWidth === undefined || requestedHeight === undefined;
|
|
138
|
+
const clamped = applied.width !== resolvedWidth || applied.height !== resolvedHeight;
|
|
139
|
+
if (!defaulted && !clamped) return undefined;
|
|
140
|
+
return {
|
|
141
|
+
requested: { width: requestedWidth ?? null, height: requestedHeight ?? null },
|
|
142
|
+
applied,
|
|
143
|
+
reason: defaulted && clamped ? 'defaulted-and-clamped-to-minimum' : clamped ? 'clamped-to-minimum' : 'defaulted',
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
121
147
|
// Per-type minimum creation sizes + clamp live in canvas-validation.ts
|
|
122
148
|
// (NODE_MIN_CREATE_SIZES / clampCreateNodeSize) — shared with the validate
|
|
123
149
|
// surface's sizeWarnings.
|
|
@@ -621,6 +647,7 @@ export function primeCanvasRuntimeBackends(options: { forceRehydrateExtApps?: bo
|
|
|
621
647
|
export async function syncCanvasRuntimeBackends(
|
|
622
648
|
options: { forceRehydrateExtApps?: boolean; alreadyPrimed?: boolean } = {},
|
|
623
649
|
): Promise<{ rehydrated: number; failed: number }> {
|
|
650
|
+
const boardGeneration = canvasState.boardGeneration;
|
|
624
651
|
const targetIds =
|
|
625
652
|
options.alreadyPrimed === true
|
|
626
653
|
? canvasState
|
|
@@ -632,6 +659,7 @@ export async function syncCanvasRuntimeBackends(
|
|
|
632
659
|
let failed = 0;
|
|
633
660
|
|
|
634
661
|
for (const nodeId of targetIds) {
|
|
662
|
+
if (canvasState.boardGeneration !== boardGeneration) break;
|
|
635
663
|
const current = canvasState.getNode(nodeId);
|
|
636
664
|
if (!isExtAppNode(current)) continue;
|
|
637
665
|
|
|
@@ -659,6 +687,10 @@ export async function syncCanvasRuntimeBackends(
|
|
|
659
687
|
? { serverName: current.data.serverName.trim() }
|
|
660
688
|
: {}),
|
|
661
689
|
});
|
|
690
|
+
if (canvasState.boardGeneration !== boardGeneration) {
|
|
691
|
+
closeMcpAppSession(opened.sessionId);
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
662
694
|
const toolInput = resolveExtAppRehydratedToolInput(current, opened.toolInput);
|
|
663
695
|
const storedCheckpointId = getStoredExcalidrawCheckpointId(current);
|
|
664
696
|
const toolResult = isExcalidrawCreateView(opened.serverName, opened.toolName)
|
|
@@ -683,6 +715,7 @@ export async function syncCanvasRuntimeBackends(
|
|
|
683
715
|
});
|
|
684
716
|
rehydrated++;
|
|
685
717
|
} catch (error) {
|
|
718
|
+
if (canvasState.boardGeneration !== boardGeneration) continue;
|
|
686
719
|
canvasState.withSuppressedRecording(() => {
|
|
687
720
|
setExtAppRuntimeState(nodeId, {
|
|
688
721
|
appSessionId: null,
|
|
@@ -923,17 +956,15 @@ export function addCanvasNode(input: CanvasAddNodeInput): {
|
|
|
923
956
|
id: string;
|
|
924
957
|
node: CanvasNodeState;
|
|
925
958
|
needsCodeGraphRecompute: boolean;
|
|
959
|
+
sizeAdjustment?: CanvasSizeAdjustment;
|
|
926
960
|
} {
|
|
927
961
|
if (input.type === 'json-render' || input.type === 'graph') {
|
|
928
962
|
throw new Error(`Use the dedicated ${input.type} node APIs for structured viewer nodes.`);
|
|
929
963
|
}
|
|
930
964
|
|
|
931
|
-
const
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
input.height ?? input.defaultHeight ?? 600,
|
|
935
|
-
input.strictSize,
|
|
936
|
-
);
|
|
965
|
+
const resolvedWidth = input.width ?? input.defaultWidth ?? 720;
|
|
966
|
+
const resolvedHeight = input.height ?? input.defaultHeight ?? 600;
|
|
967
|
+
const { width, height } = clampCreateNodeSize(input.type, resolvedWidth, resolvedHeight, input.strictSize);
|
|
937
968
|
const position =
|
|
938
969
|
input.x !== undefined && input.y !== undefined
|
|
939
970
|
? { x: input.x, y: input.y }
|
|
@@ -959,7 +990,16 @@ export function addCanvasNode(input: CanvasAddNodeInput): {
|
|
|
959
990
|
watchFileForNode(id, filePath);
|
|
960
991
|
}
|
|
961
992
|
|
|
962
|
-
|
|
993
|
+
const sizeAdjustment = describeCreateSize(input.width, input.height, resolvedWidth, resolvedHeight, {
|
|
994
|
+
width,
|
|
995
|
+
height,
|
|
996
|
+
});
|
|
997
|
+
return {
|
|
998
|
+
id,
|
|
999
|
+
node: storedNode,
|
|
1000
|
+
needsCodeGraphRecompute: input.type === 'file',
|
|
1001
|
+
...(sizeAdjustment ? { sizeAdjustment } : {}),
|
|
1002
|
+
};
|
|
963
1003
|
}
|
|
964
1004
|
|
|
965
1005
|
export function resolveCanvasNode(nodeRef: CanvasNodeLookupInput):
|
|
@@ -1029,6 +1069,7 @@ export async function refreshCanvasWebpageNode(
|
|
|
1029
1069
|
}
|
|
1030
1070
|
|
|
1031
1071
|
const currentData = existing.data;
|
|
1072
|
+
const boardGeneration = canvasState.boardGeneration;
|
|
1032
1073
|
const configuredUrl =
|
|
1033
1074
|
typeof options.url === 'string' && options.url.trim().length > 0
|
|
1034
1075
|
? options.url
|
|
@@ -1054,16 +1095,11 @@ export async function refreshCanvasWebpageNode(
|
|
|
1054
1095
|
};
|
|
1055
1096
|
}
|
|
1056
1097
|
|
|
1057
|
-
const fetchingData: Record<string, unknown> = {
|
|
1058
|
-
...currentData,
|
|
1059
|
-
url: normalizedUrl,
|
|
1060
|
-
status: 'fetching',
|
|
1061
|
-
};
|
|
1062
|
-
delete fetchingData.error;
|
|
1063
|
-
canvasState.updateNode(id, { data: fetchingData });
|
|
1064
|
-
|
|
1065
1098
|
try {
|
|
1066
1099
|
const snapshot = await fetchWebpageSnapshot(normalizedUrl);
|
|
1100
|
+
if (canvasState.boardGeneration !== boardGeneration) {
|
|
1101
|
+
return { ok: false, id, error: 'The board changed during webpage refresh.' };
|
|
1102
|
+
}
|
|
1067
1103
|
const latest = canvasState.getNode(id);
|
|
1068
1104
|
if (!latest || latest.type !== 'webpage') {
|
|
1069
1105
|
return { ok: false, id, error: `Webpage node "${id}" disappeared during refresh.` };
|
|
@@ -1097,6 +1133,9 @@ export async function refreshCanvasWebpageNode(
|
|
|
1097
1133
|
canvasState.updateNode(id, { data: nextData });
|
|
1098
1134
|
return { ok: true, id };
|
|
1099
1135
|
} catch (error) {
|
|
1136
|
+
if (canvasState.boardGeneration !== boardGeneration) {
|
|
1137
|
+
return { ok: false, id, error: 'The board changed during webpage refresh.' };
|
|
1138
|
+
}
|
|
1100
1139
|
const details = getWebpageFetchErrorDetails(error);
|
|
1101
1140
|
const latest = canvasState.getNode(id);
|
|
1102
1141
|
if (latest?.type === 'webpage') {
|
|
@@ -1415,7 +1454,8 @@ export function saveCanvasSnapshotWithReuse(name: string): { snapshot: CanvasSna
|
|
|
1415
1454
|
diff.removedNodes.length === 0 &&
|
|
1416
1455
|
diff.modifiedNodes.length === 0 &&
|
|
1417
1456
|
diff.addedEdges.length === 0 &&
|
|
1418
|
-
diff.removedEdges.length === 0
|
|
1457
|
+
diff.removedEdges.length === 0 &&
|
|
1458
|
+
JSON.stringify(newestData.tour) === JSON.stringify(canvasState.getTour());
|
|
1419
1459
|
if (identical) return { snapshot: newest, reused: true };
|
|
1420
1460
|
}
|
|
1421
1461
|
}
|
|
@@ -1430,14 +1470,65 @@ export async function restoreCanvasSnapshot(idOrName: string): Promise<{ ok: boo
|
|
|
1430
1470
|
const ok = canvasState.restoreSnapshot(idOrName);
|
|
1431
1471
|
if (ok) {
|
|
1432
1472
|
primeCanvasRuntimeBackends({ forceRehydrateExtApps: true });
|
|
1473
|
+
const boardGeneration = canvasState.boardGeneration;
|
|
1433
1474
|
void syncCanvasRuntimeBackends({ forceRehydrateExtApps: true, alreadyPrimed: true }).finally(() => {
|
|
1434
|
-
emitCanvasLayoutUpdate();
|
|
1475
|
+
if (canvasState.boardGeneration === boardGeneration) emitCanvasLayoutUpdate();
|
|
1435
1476
|
});
|
|
1436
1477
|
canvasState.flushToDisk();
|
|
1437
1478
|
}
|
|
1438
1479
|
return { ok };
|
|
1439
1480
|
}
|
|
1440
1481
|
|
|
1482
|
+
/**
|
|
1483
|
+
* Open a board (or Home for null): the state layer saves the current board and
|
|
1484
|
+
* loads the next, and everything else that holds the old board in memory is
|
|
1485
|
+
* reset — undo closures, pending intents, file watchers, app sessions and the
|
|
1486
|
+
* code graph (the trace chain restarts itself when its last node is gone).
|
|
1487
|
+
* Agent and human presence are kept: they belong to the people and agents,
|
|
1488
|
+
* not to a board.
|
|
1489
|
+
*/
|
|
1490
|
+
export function openCanvasBoard(id: string | null): { ok: boolean } {
|
|
1491
|
+
if (!canvasState.switchBoard(id)) return { ok: false };
|
|
1492
|
+
mutationHistory.reset();
|
|
1493
|
+
intentRegistry.reset();
|
|
1494
|
+
startBoardRuntime();
|
|
1495
|
+
return { ok: true };
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
function startBoardRuntime(): void {
|
|
1499
|
+
primeCanvasRuntimeBackends({ forceRehydrateExtApps: true });
|
|
1500
|
+
scheduleCodeGraphRecompute();
|
|
1501
|
+
emitCanvasLayoutUpdate();
|
|
1502
|
+
// App rehydration can take a network round trip; it finishes in the background.
|
|
1503
|
+
const boardGeneration = canvasState.boardGeneration;
|
|
1504
|
+
void syncCanvasRuntimeBackends({ forceRehydrateExtApps: true, alreadyPrimed: true }).finally(() => {
|
|
1505
|
+
if (canvasState.boardGeneration === boardGeneration) emitCanvasLayoutUpdate();
|
|
1506
|
+
});
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
/**
|
|
1510
|
+
* Restore the whole library from a backup file (the current file is kept as
|
|
1511
|
+
* `<db>.before-restore`); the restored library opens on its last open board.
|
|
1512
|
+
*/
|
|
1513
|
+
export function restoreCanvasLibrary(file: string): void {
|
|
1514
|
+
canvasState.replaceDatabase(file);
|
|
1515
|
+
mutationHistory.reset();
|
|
1516
|
+
intentRegistry.reset();
|
|
1517
|
+
startBoardRuntime();
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
/**
|
|
1521
|
+
* A write that arrives while no board is open (Home) lands on the most recently
|
|
1522
|
+
* opened board, or on a new board when the workspace has none — nothing on
|
|
1523
|
+
* Home is displaced, because Home holds nothing. Intents and undo history are
|
|
1524
|
+
* kept: whatever was signalled on Home was aimed at the board this opens.
|
|
1525
|
+
*/
|
|
1526
|
+
export function openBoardForWrite(): void {
|
|
1527
|
+
if (canvasState.activeBoardId) return;
|
|
1528
|
+
const recent = canvasState.listBoards()[0] ?? canvasState.createBoard(defaultBoardName());
|
|
1529
|
+
if (recent && canvasState.switchBoard(recent.id)) startBoardRuntime();
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1441
1532
|
export function deleteCanvasSnapshot(id: string): { ok: boolean } {
|
|
1442
1533
|
return { ok: canvasState.deleteSnapshot(id) };
|
|
1443
1534
|
}
|
|
@@ -1508,7 +1599,11 @@ export function removeCanvasEdge(id: string): { removed: boolean } {
|
|
|
1508
1599
|
return { removed: canvasState.removeEdge(id) };
|
|
1509
1600
|
}
|
|
1510
1601
|
|
|
1511
|
-
export function createCanvasGroup(input: CanvasCreateGroupInput): {
|
|
1602
|
+
export function createCanvasGroup(input: CanvasCreateGroupInput): {
|
|
1603
|
+
id: string;
|
|
1604
|
+
node: CanvasNodeState;
|
|
1605
|
+
sizeAdjustment?: CanvasSizeAdjustment;
|
|
1606
|
+
} {
|
|
1512
1607
|
let x = input.x;
|
|
1513
1608
|
let y = input.y;
|
|
1514
1609
|
let width = input.width ?? 600;
|
|
@@ -1566,22 +1661,42 @@ export function createCanvasGroup(input: CanvasCreateGroupInput): { id: string;
|
|
|
1566
1661
|
if (!node) {
|
|
1567
1662
|
throw new Error(`Group "${id}" was not created.`);
|
|
1568
1663
|
}
|
|
1569
|
-
|
|
1664
|
+
const adjusted = input.width !== node.size.width || input.height !== node.size.height;
|
|
1665
|
+
const fitted = childIds.length > 0 && ((input.x === undefined && input.y === undefined) || !explicitFrame);
|
|
1666
|
+
return {
|
|
1667
|
+
id,
|
|
1668
|
+
node,
|
|
1669
|
+
...(adjusted
|
|
1670
|
+
? {
|
|
1671
|
+
sizeAdjustment: {
|
|
1672
|
+
requested: { width: input.width ?? null, height: input.height ?? null },
|
|
1673
|
+
applied: node.size,
|
|
1674
|
+
reason: fitted ? ('fit-to-children' as const) : ('defaulted' as const),
|
|
1675
|
+
},
|
|
1676
|
+
}
|
|
1677
|
+
: {}),
|
|
1678
|
+
};
|
|
1570
1679
|
}
|
|
1571
1680
|
|
|
1572
1681
|
export function groupCanvasNodes(
|
|
1573
1682
|
groupId: string,
|
|
1574
1683
|
childIds: string[],
|
|
1575
1684
|
options: { childLayout?: CanvasArrangeMode } = {},
|
|
1576
|
-
): { ok: boolean } {
|
|
1685
|
+
): { ok: boolean; sizeAdjustment?: CanvasSizeAdjustment } {
|
|
1686
|
+
const before = canvasState.getNode(groupId)?.size;
|
|
1687
|
+
const ok = canvasState.groupNodes(groupId, childIds, {
|
|
1688
|
+
// Preserve existing child positions unless an explicit layout is asked
|
|
1689
|
+
// for — matching createCanvasGroup and the batch group.add path. Without
|
|
1690
|
+
// this, grouping silently auto-packs the nodes into a grid.
|
|
1691
|
+
preservePositions: options.childLayout === undefined,
|
|
1692
|
+
...(options.childLayout ? { layout: options.childLayout } : {}),
|
|
1693
|
+
});
|
|
1694
|
+
const applied = canvasState.getNode(groupId)?.size;
|
|
1577
1695
|
return {
|
|
1578
|
-
ok
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
preservePositions: options.childLayout === undefined,
|
|
1583
|
-
...(options.childLayout ? { layout: options.childLayout } : {}),
|
|
1584
|
-
}),
|
|
1696
|
+
ok,
|
|
1697
|
+
...(before && applied && (before.width !== applied.width || before.height !== applied.height)
|
|
1698
|
+
? { sizeAdjustment: { requested: before, applied, reason: 'fit-to-children' as const } }
|
|
1699
|
+
: {}),
|
|
1585
1700
|
};
|
|
1586
1701
|
}
|
|
1587
1702
|
|
|
@@ -1600,6 +1715,8 @@ export function createCanvasJsonRenderNode(input: JsonRenderNodeInput): {
|
|
|
1600
1715
|
url: string;
|
|
1601
1716
|
spec: JsonRenderSpec;
|
|
1602
1717
|
node: CanvasNodeState;
|
|
1718
|
+
warnings: string[];
|
|
1719
|
+
sizeAdjustment?: CanvasSizeAdjustment;
|
|
1603
1720
|
} {
|
|
1604
1721
|
const spec = normalizeAndValidateJsonRenderSpec(input.spec);
|
|
1605
1722
|
const { width, height } = clampCreateNodeSize(
|
|
@@ -1628,7 +1745,24 @@ export function createCanvasJsonRenderNode(input: JsonRenderNodeInput): {
|
|
|
1628
1745
|
};
|
|
1629
1746
|
|
|
1630
1747
|
canvasState.addJsonRenderNode(node);
|
|
1631
|
-
|
|
1748
|
+
const sizeAdjustment = describeCreateSize(
|
|
1749
|
+
input.width,
|
|
1750
|
+
input.height,
|
|
1751
|
+
input.width ?? JSON_RENDER_NODE_SIZE.width,
|
|
1752
|
+
input.height ?? JSON_RENDER_NODE_SIZE.height,
|
|
1753
|
+
{
|
|
1754
|
+
width,
|
|
1755
|
+
height,
|
|
1756
|
+
},
|
|
1757
|
+
);
|
|
1758
|
+
return {
|
|
1759
|
+
id,
|
|
1760
|
+
url: String(node.data.url),
|
|
1761
|
+
spec,
|
|
1762
|
+
node,
|
|
1763
|
+
warnings: jsonRenderFormWarnings(spec),
|
|
1764
|
+
...(sizeAdjustment ? { sizeAdjustment } : {}),
|
|
1765
|
+
};
|
|
1632
1766
|
}
|
|
1633
1767
|
|
|
1634
1768
|
/**
|
|
@@ -1643,7 +1777,7 @@ export function createCanvasStreamingJsonRenderNode(input: {
|
|
|
1643
1777
|
width?: number;
|
|
1644
1778
|
height?: number;
|
|
1645
1779
|
strictSize?: boolean;
|
|
1646
|
-
}): { id: string; url: string; spec: JsonRenderSpec; node: CanvasNodeState } {
|
|
1780
|
+
}): { id: string; url: string; spec: JsonRenderSpec; node: CanvasNodeState; sizeAdjustment?: CanvasSizeAdjustment } {
|
|
1647
1781
|
const spec = emptyStreamingSpec();
|
|
1648
1782
|
const { width, height } = clampCreateNodeSize(
|
|
1649
1783
|
'json-render',
|
|
@@ -1673,7 +1807,17 @@ export function createCanvasStreamingJsonRenderNode(input: {
|
|
|
1673
1807
|
};
|
|
1674
1808
|
|
|
1675
1809
|
canvasState.addJsonRenderNode(node);
|
|
1676
|
-
|
|
1810
|
+
const sizeAdjustment = describeCreateSize(
|
|
1811
|
+
input.width,
|
|
1812
|
+
input.height,
|
|
1813
|
+
input.width ?? JSON_RENDER_NODE_SIZE.width,
|
|
1814
|
+
input.height ?? JSON_RENDER_NODE_SIZE.height,
|
|
1815
|
+
{
|
|
1816
|
+
width,
|
|
1817
|
+
height,
|
|
1818
|
+
},
|
|
1819
|
+
);
|
|
1820
|
+
return { id, url: String(node.data.url), spec, node, ...(sizeAdjustment ? { sizeAdjustment } : {}) };
|
|
1677
1821
|
}
|
|
1678
1822
|
|
|
1679
1823
|
/**
|
|
@@ -1717,6 +1861,7 @@ export function createCanvasGraphNode(input: GraphNodeInput): {
|
|
|
1717
1861
|
url: string;
|
|
1718
1862
|
spec: JsonRenderSpec;
|
|
1719
1863
|
node: CanvasNodeState;
|
|
1864
|
+
sizeAdjustment?: CanvasSizeAdjustment;
|
|
1720
1865
|
} {
|
|
1721
1866
|
const title = input.title?.trim() || 'Graph';
|
|
1722
1867
|
const spec = buildGraphSpec(input);
|
|
@@ -1747,7 +1892,14 @@ export function createCanvasGraphNode(input: GraphNodeInput): {
|
|
|
1747
1892
|
};
|
|
1748
1893
|
|
|
1749
1894
|
canvasState.addGraphNode(node);
|
|
1750
|
-
|
|
1895
|
+
const sizeAdjustment = describeCreateSize(
|
|
1896
|
+
input.width,
|
|
1897
|
+
input.heightPx,
|
|
1898
|
+
input.width ?? GRAPH_NODE_SIZE.width,
|
|
1899
|
+
input.heightPx ?? GRAPH_NODE_SIZE.height,
|
|
1900
|
+
{ width, height },
|
|
1901
|
+
);
|
|
1902
|
+
return { id, url: String(node.data.url), spec, node, ...(sizeAdjustment ? { sizeAdjustment } : {}) };
|
|
1751
1903
|
}
|
|
1752
1904
|
|
|
1753
1905
|
/**
|
|
@@ -3,6 +3,7 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { describeJsonRenderCatalog, type JsonRenderComponentDescriptor } from '../json-render/catalog.js';
|
|
4
4
|
import {
|
|
5
5
|
buildGraphSpec,
|
|
6
|
+
jsonRenderFormWarnings,
|
|
6
7
|
normalizeAndValidateJsonRenderSpec,
|
|
7
8
|
normalizeGraphType,
|
|
8
9
|
type GraphNodeInput,
|
|
@@ -38,6 +39,7 @@ export interface StructuredValidationResult {
|
|
|
38
39
|
ok: true;
|
|
39
40
|
type: 'json-render' | 'graph' | 'html-primitive';
|
|
40
41
|
normalizedSpec?: JsonRenderSpec;
|
|
42
|
+
warnings?: string[];
|
|
41
43
|
normalizedPrimitive?: {
|
|
42
44
|
kind: string;
|
|
43
45
|
title: string;
|
|
@@ -105,6 +107,18 @@ const CANVAS_CREATE_TYPES: CanvasCreateTypeSchema[] = [
|
|
|
105
107
|
'When a markdown node is path-backed (`data.path`), the server persists `data.provenance` so snapshots keep a refreshable file source.',
|
|
106
108
|
],
|
|
107
109
|
},
|
|
110
|
+
{
|
|
111
|
+
type: 'board',
|
|
112
|
+
kind: 'node',
|
|
113
|
+
description: 'Text-only link to another board by durable id.',
|
|
114
|
+
endpoint: '/api/canvas/node',
|
|
115
|
+
mcpTool: 'canvas_node (action:"add")',
|
|
116
|
+
fields: [
|
|
117
|
+
{ name: 'title', type: 'string', required: false, description: 'Pinned fallback title.' },
|
|
118
|
+
{ name: 'data', type: 'object', required: true, description: 'Contains boardId, the durable target board id.' },
|
|
119
|
+
],
|
|
120
|
+
example: { type: 'board', title: 'Research', data: { boardId: 'board-abc123' } },
|
|
121
|
+
},
|
|
108
122
|
{
|
|
109
123
|
type: 'status',
|
|
110
124
|
kind: 'node',
|
|
@@ -235,6 +249,13 @@ const CANVAS_CREATE_TYPES: CanvasCreateTypeSchema[] = [
|
|
|
235
249
|
fields: [
|
|
236
250
|
{ name: 'title', type: 'string', required: false, description: 'Optional node title.' },
|
|
237
251
|
{ name: 'content', type: 'string', required: false, description: 'Mermaid diagram source text.' },
|
|
252
|
+
{
|
|
253
|
+
name: 'data.fit',
|
|
254
|
+
type: '"contain" | "none"',
|
|
255
|
+
required: false,
|
|
256
|
+
description:
|
|
257
|
+
'Default contain: shrink overflowing diagrams proportionally, without enlarging. none: 100% with scrolling. Open as site always uses 100%. CLI: --fit contain|none.',
|
|
258
|
+
},
|
|
238
259
|
],
|
|
239
260
|
example: {
|
|
240
261
|
type: 'mermaid',
|
|
@@ -1013,6 +1034,7 @@ export function validateStructuredCanvasPayload(input: {
|
|
|
1013
1034
|
ok: true,
|
|
1014
1035
|
type: 'json-render',
|
|
1015
1036
|
normalizedSpec,
|
|
1037
|
+
warnings: jsonRenderFormWarnings(normalizedSpec),
|
|
1016
1038
|
summary: {
|
|
1017
1039
|
root: normalizedSpec.root,
|
|
1018
1040
|
elementCount: Object.keys(normalizedSpec.elements).length,
|