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
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Many boards (plan 012): board.list / board.get / board.create /
|
|
3
|
+
* board.update / board.open / board.delete.
|
|
4
|
+
*
|
|
5
|
+
* Agents list, read and create boards. Opening, switching and deleting are the
|
|
6
|
+
* human's (HUMAN_ONLY_OPS in registry.ts): an agent that wants another board
|
|
7
|
+
* asks. Every change broadcasts `boards-changed` with the full list.
|
|
8
|
+
*
|
|
9
|
+
* Route-order note: board.open (POST /api/canvas/boards/open) is registered
|
|
10
|
+
* before any `/api/canvas/boards/:id` POST route so ':id' cannot swallow it.
|
|
11
|
+
*
|
|
12
|
+
* This module must never import server.ts or index.ts.
|
|
13
|
+
*/
|
|
14
|
+
import { z } from 'zod';
|
|
15
|
+
import { normalizeBoardCategory } from '../../../shared/boards.js';
|
|
16
|
+
import { canvasState, type CanvasLayout } from '../../canvas-state.js';
|
|
17
|
+
import { openCanvasBoard } from '../../canvas-operations.js';
|
|
18
|
+
import { summarizeNodeForAgentContext } from '../../agent-context.js';
|
|
19
|
+
import { defineOperation, OperationError, type Operation, type OperationContext } from '../types.js';
|
|
20
|
+
|
|
21
|
+
export function boardsPayload(): Record<string, unknown> {
|
|
22
|
+
const boards = canvasState.listBoards();
|
|
23
|
+
const names = new Map(boards.map((board) => [board.id, board.name]));
|
|
24
|
+
const links = new Map<string, Array<{ nodeId: string; boardId: string; title: string | null; missing: boolean }>>();
|
|
25
|
+
for (const board of boards) {
|
|
26
|
+
const nodes = canvasState.readBoard(board.id, false)?.layout.nodes ?? [];
|
|
27
|
+
links.set(
|
|
28
|
+
board.id,
|
|
29
|
+
nodes
|
|
30
|
+
.filter((node) => node.type === 'board')
|
|
31
|
+
.map((node) => {
|
|
32
|
+
const boardId = typeof node.data.boardId === 'string' ? node.data.boardId : '';
|
|
33
|
+
return { nodeId: node.id, boardId, title: names.get(boardId) ?? null, missing: !names.has(boardId) };
|
|
34
|
+
}),
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
activeBoardId: canvasState.activeBoardId,
|
|
39
|
+
boards: boards.map((board) => {
|
|
40
|
+
const read = canvasState.readBoard(board.id, true);
|
|
41
|
+
const readme = read?.layout.nodes.find((node) => node.id === board.readmeNodeId && node.type === 'markdown');
|
|
42
|
+
return {
|
|
43
|
+
...board,
|
|
44
|
+
summary: readme ? summarizeNodeForAgentContext(readme, { defaultTextLength: 1000 }) : null,
|
|
45
|
+
pinnedTitles: (read?.layout.nodes ?? [])
|
|
46
|
+
.filter((node) => read?.state.contextPins.includes(node.id))
|
|
47
|
+
.map((node) => ({ nodeId: node.id, title: typeof node.data.title === 'string' ? node.data.title : node.id })),
|
|
48
|
+
links: links.get(board.id) ?? [],
|
|
49
|
+
backlinks: boards.flatMap((source) =>
|
|
50
|
+
(links.get(source.id) ?? [])
|
|
51
|
+
.filter((link) => link.boardId === board.id)
|
|
52
|
+
.map((link) => ({ boardId: source.id, title: source.name, nodeId: link.nodeId })),
|
|
53
|
+
),
|
|
54
|
+
};
|
|
55
|
+
}),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Read a board without opening it. Omitted board preserves the active-board contract. */
|
|
60
|
+
export function readTargetBoard(
|
|
61
|
+
boardId?: unknown,
|
|
62
|
+
includeBlobs = true,
|
|
63
|
+
): {
|
|
64
|
+
boardId: string | null;
|
|
65
|
+
layout: CanvasLayout;
|
|
66
|
+
pinnedNodeIds: Set<string>;
|
|
67
|
+
} {
|
|
68
|
+
const requested = typeof boardId === 'string' && boardId.trim() ? boardId.trim() : null;
|
|
69
|
+
if (!requested) {
|
|
70
|
+
return {
|
|
71
|
+
boardId: canvasState.activeBoardId,
|
|
72
|
+
layout: canvasState.getLayout(),
|
|
73
|
+
pinnedNodeIds: new Set(canvasState.contextPinnedNodeIds),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const read = canvasState.readBoard(requested, includeBlobs);
|
|
77
|
+
if (!read) throw new OperationError(`Board "${requested}" not found.`, 404);
|
|
78
|
+
return {
|
|
79
|
+
boardId: requested,
|
|
80
|
+
layout: read.layout,
|
|
81
|
+
pinnedNodeIds: new Set(read.state.contextPins),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function emitBoardsChanged(ctx: OperationContext): void {
|
|
86
|
+
ctx.emit('boards-changed', boardsPayload());
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const jsonResult = (result: unknown) => ({
|
|
90
|
+
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
/** '' or null clears the category; undefined leaves it alone. */
|
|
94
|
+
function boardCategory(value: unknown): string | null | undefined {
|
|
95
|
+
if (value === undefined) return undefined;
|
|
96
|
+
try {
|
|
97
|
+
return normalizeBoardCategory(typeof value === 'string' ? value : null);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
throw new OperationError(error instanceof Error ? error.message : String(error));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function boardName(value: unknown): string {
|
|
104
|
+
const name = typeof value === 'string' ? value.trim() : '';
|
|
105
|
+
if (!name) throw new OperationError('A board needs a name.');
|
|
106
|
+
if (name.length > 120) throw new OperationError('Board names are at most 120 characters.');
|
|
107
|
+
return name;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ── board.list ────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
const listShape = {};
|
|
113
|
+
const listSchema = z.looseObject(listShape);
|
|
114
|
+
|
|
115
|
+
const boardListOperation = defineOperation<z.infer<typeof listSchema>, Record<string, unknown>>({
|
|
116
|
+
name: 'board.list',
|
|
117
|
+
mutates: false,
|
|
118
|
+
input: listSchema,
|
|
119
|
+
inputShape: listShape,
|
|
120
|
+
http: { method: 'GET', path: '/api/canvas/boards' },
|
|
121
|
+
mcp: {
|
|
122
|
+
toolName: 'canvas_list_boards',
|
|
123
|
+
description:
|
|
124
|
+
'List the boards in this workspace, most recently opened first, with node counts and which one is open (activeBoardId; null means the human is on Home).',
|
|
125
|
+
formatResult: jsonResult,
|
|
126
|
+
},
|
|
127
|
+
handler: () => ({ ok: true, ...boardsPayload() }),
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// ── board.get ─────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
const getShape = {
|
|
133
|
+
id: z.unknown().optional().describe('Board id (default: the open board)'),
|
|
134
|
+
};
|
|
135
|
+
const getSchema = z.looseObject(getShape);
|
|
136
|
+
|
|
137
|
+
const boardGetOperation = defineOperation<z.infer<typeof getSchema>, Record<string, unknown>>({
|
|
138
|
+
name: 'board.get',
|
|
139
|
+
mutates: false,
|
|
140
|
+
input: getSchema,
|
|
141
|
+
inputShape: getShape,
|
|
142
|
+
http: { method: 'GET', path: '/api/canvas/boards/:id' },
|
|
143
|
+
mcp: {
|
|
144
|
+
toolName: 'canvas_get_board',
|
|
145
|
+
description: 'Read one board by id (name, created, last opened, node count). Omit id for the open board.',
|
|
146
|
+
extraShape: { id: z.string().optional().describe('Board id (default: the open board)') },
|
|
147
|
+
formatResult: jsonResult,
|
|
148
|
+
},
|
|
149
|
+
handler: (input) => {
|
|
150
|
+
const id = typeof input.id === 'string' && input.id.trim() ? input.id.trim() : canvasState.activeBoardId;
|
|
151
|
+
const board = id ? canvasState.listBoards().find((entry) => entry.id === id) : undefined;
|
|
152
|
+
if (!board) throw new OperationError(id ? `Board "${id}" not found.` : 'No board is open.', 404);
|
|
153
|
+
return { ok: true, board, open: board.id === canvasState.activeBoardId };
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// ── board.create ──────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
const createShape = {
|
|
160
|
+
name: z.unknown().optional().describe('Board name'),
|
|
161
|
+
category: z.unknown().optional().describe('Folder path on Home, e.g. Engineering/Canvas'),
|
|
162
|
+
};
|
|
163
|
+
const createSchema = z.looseObject(createShape);
|
|
164
|
+
|
|
165
|
+
const boardCreateOperation = defineOperation<z.infer<typeof createSchema>, Record<string, unknown>>({
|
|
166
|
+
name: 'board.create',
|
|
167
|
+
mutates: false,
|
|
168
|
+
input: createSchema,
|
|
169
|
+
inputShape: createShape,
|
|
170
|
+
http: { method: 'POST', path: '/api/canvas/boards' },
|
|
171
|
+
mcp: {
|
|
172
|
+
toolName: 'canvas_create_board',
|
|
173
|
+
description:
|
|
174
|
+
'Create a new empty board. It is NOT opened: only the human opens boards, so ask them to open it before writing to it.',
|
|
175
|
+
extraShape: {
|
|
176
|
+
name: z.string().describe('Board name'),
|
|
177
|
+
category: z.string().optional().describe('Folder path on Home, e.g. Engineering/Canvas'),
|
|
178
|
+
},
|
|
179
|
+
formatResult: jsonResult,
|
|
180
|
+
},
|
|
181
|
+
handler: (input, ctx) => {
|
|
182
|
+
const board = canvasState.createBoard(boardName(input.name), boardCategory(input.category) ?? null);
|
|
183
|
+
if (!board) throw new OperationError('Boards need a workspace database.', 409);
|
|
184
|
+
emitBoardsChanged(ctx);
|
|
185
|
+
return { ok: true, board };
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// ── board.open (human only) ───────────────────────────────────
|
|
190
|
+
|
|
191
|
+
const openShape = {
|
|
192
|
+
id: z.unknown().optional().describe('Board id to open; null or omitted opens Home'),
|
|
193
|
+
};
|
|
194
|
+
const openSchema = z.looseObject(openShape);
|
|
195
|
+
|
|
196
|
+
const boardOpenOperation = defineOperation<z.infer<typeof openSchema>, Record<string, unknown>>({
|
|
197
|
+
name: 'board.open',
|
|
198
|
+
mutates: false,
|
|
199
|
+
input: openSchema,
|
|
200
|
+
inputShape: openShape,
|
|
201
|
+
http: { method: 'POST', path: '/api/canvas/boards/open' },
|
|
202
|
+
handler: (input, ctx) => {
|
|
203
|
+
const id = typeof input.id === 'string' && input.id.trim() ? input.id.trim() : null;
|
|
204
|
+
if (!openCanvasBoard(id).ok) throw new OperationError(`Board "${id}" not found.`, 404);
|
|
205
|
+
emitBoardsChanged(ctx);
|
|
206
|
+
return { ok: true, ...boardsPayload() };
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// ── board.update ──────────────────────────────────────────────
|
|
211
|
+
|
|
212
|
+
const updateShape = {
|
|
213
|
+
id: z.unknown().optional().describe('Board id'),
|
|
214
|
+
name: z.unknown().optional().describe('New name'),
|
|
215
|
+
category: z.unknown().optional().describe('Folder path on Home; "" or null unfiles it'),
|
|
216
|
+
readmeNodeId: z.unknown().optional().describe('Markdown node id to use as README; null clears it'),
|
|
217
|
+
};
|
|
218
|
+
const updateSchema = z.looseObject(updateShape);
|
|
219
|
+
|
|
220
|
+
const boardUpdateOperation = defineOperation<z.infer<typeof updateSchema>, Record<string, unknown>>({
|
|
221
|
+
name: 'board.update',
|
|
222
|
+
mutates: false,
|
|
223
|
+
input: updateSchema,
|
|
224
|
+
inputShape: updateShape,
|
|
225
|
+
http: { method: 'PATCH', path: '/api/canvas/boards/:id' },
|
|
226
|
+
mcp: {
|
|
227
|
+
toolName: 'canvas_update_board',
|
|
228
|
+
description:
|
|
229
|
+
'Rename a board, move it into a nested folder on Home (e.g. Engineering/Canvas; category "" unfiles it), or designate its markdown introduction with readmeNodeId. During board authoring, set a useful README when none exists; preserve an existing designation unless the human requests a change. Moving preserves the board ID and content.',
|
|
230
|
+
extraShape: {
|
|
231
|
+
id: z.string().describe('Board id'),
|
|
232
|
+
name: z.string().optional().describe('New name'),
|
|
233
|
+
category: z.string().optional().describe('Folder path, up to 8 levels of 1–60 characters; "" unfiles it'),
|
|
234
|
+
readmeNodeId: z.string().nullable().optional().describe('Markdown node id used as the board README'),
|
|
235
|
+
},
|
|
236
|
+
formatResult: jsonResult,
|
|
237
|
+
},
|
|
238
|
+
handler: (input, ctx) => {
|
|
239
|
+
const id = typeof input.id === 'string' ? input.id : '';
|
|
240
|
+
const patch = {
|
|
241
|
+
...(input.name !== undefined ? { name: boardName(input.name) } : {}),
|
|
242
|
+
...(input.category !== undefined ? { category: boardCategory(input.category) ?? null } : {}),
|
|
243
|
+
};
|
|
244
|
+
if (input.readmeNodeId !== undefined) {
|
|
245
|
+
const board = canvasState.readBoard(id, false);
|
|
246
|
+
if (!board) throw new OperationError(`Board "${id}" not found.`, 404);
|
|
247
|
+
if (
|
|
248
|
+
input.readmeNodeId &&
|
|
249
|
+
!board.layout.nodes.some((node) => node.id === input.readmeNodeId && node.type === 'markdown')
|
|
250
|
+
) {
|
|
251
|
+
throw new OperationError('README must identify a markdown node on this board.');
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (!canvasState.updateBoard(id, patch)) {
|
|
255
|
+
throw new OperationError(`Board "${id}" not found.`, 404);
|
|
256
|
+
}
|
|
257
|
+
if (input.readmeNodeId !== undefined) {
|
|
258
|
+
const nodeId = typeof input.readmeNodeId === 'string' && input.readmeNodeId ? input.readmeNodeId : null;
|
|
259
|
+
if (!canvasState.setBoardReadme(id, nodeId)) {
|
|
260
|
+
throw new OperationError('README must identify a markdown node on this board.');
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
emitBoardsChanged(ctx);
|
|
264
|
+
return { ok: true, ...boardsPayload() };
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const fromShape = {
|
|
269
|
+
sourceBoardId: z.string().describe('Source board id'),
|
|
270
|
+
name: z.string().describe('New board name'),
|
|
271
|
+
category: z.string().optional(),
|
|
272
|
+
nodeIds: z.array(z.string()).optional(),
|
|
273
|
+
includeReadme: z.boolean().optional(),
|
|
274
|
+
includeStructure: z.boolean().optional(),
|
|
275
|
+
preview: z.boolean().optional().describe('Return reusable cards and structure without creating'),
|
|
276
|
+
};
|
|
277
|
+
const fromSchema = z.looseObject(fromShape);
|
|
278
|
+
const boardCreateFromOperation = defineOperation<z.infer<typeof fromSchema>, Record<string, unknown>>({
|
|
279
|
+
name: 'board.create-from',
|
|
280
|
+
// Like board.create, this writes only a new inactive board. `mutates` means
|
|
281
|
+
// active-layout mutation: setting it would auto-open a board from Home.
|
|
282
|
+
mutates: false,
|
|
283
|
+
input: fromSchema,
|
|
284
|
+
inputShape: fromShape,
|
|
285
|
+
http: { method: 'POST', path: '/api/canvas/boards/from' },
|
|
286
|
+
mcp: {
|
|
287
|
+
toolName: 'canvas_create_board_from',
|
|
288
|
+
description: 'Preview or create an inactive board from selected reusable cards and structure on another board.',
|
|
289
|
+
extraShape: fromShape,
|
|
290
|
+
formatResult: jsonResult,
|
|
291
|
+
},
|
|
292
|
+
handler: (input, ctx) => {
|
|
293
|
+
const source = canvasState.readBoard(input.sourceBoardId, false);
|
|
294
|
+
if (!source) throw new OperationError(`Board "${input.sourceBoardId}" not found.`, 404);
|
|
295
|
+
if (input.preview) {
|
|
296
|
+
return {
|
|
297
|
+
ok: true,
|
|
298
|
+
sourceBoardId: input.sourceBoardId,
|
|
299
|
+
readmeNodeId: source.board.readmeNodeId,
|
|
300
|
+
cards: source.layout.nodes.map((node) => ({
|
|
301
|
+
id: node.id,
|
|
302
|
+
type: node.type,
|
|
303
|
+
title: typeof node.data.title === 'string' ? node.data.title : null,
|
|
304
|
+
reusable: !['prompt', 'response', 'trace', 'mcp-app'].includes(node.type),
|
|
305
|
+
})),
|
|
306
|
+
edges: source.layout.edges.map((edge) => ({ id: edge.id, from: edge.from, to: edge.to, type: edge.type })),
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
const board = canvasState.createBoardFromBoard({
|
|
310
|
+
...input,
|
|
311
|
+
name: boardName(input.name),
|
|
312
|
+
category: boardCategory(input.category),
|
|
313
|
+
});
|
|
314
|
+
if (!board) throw new OperationError('Could not create board from source.', 409);
|
|
315
|
+
emitBoardsChanged(ctx);
|
|
316
|
+
return { ok: true, board };
|
|
317
|
+
},
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// ── board.delete (human only) ─────────────────────────────────
|
|
321
|
+
|
|
322
|
+
const deleteShape = {
|
|
323
|
+
id: z.unknown().optional().describe('Board id'),
|
|
324
|
+
};
|
|
325
|
+
const deleteSchema = z.looseObject(deleteShape);
|
|
326
|
+
|
|
327
|
+
const boardDeleteOperation = defineOperation<z.infer<typeof deleteSchema>, Record<string, unknown>>({
|
|
328
|
+
name: 'board.delete',
|
|
329
|
+
mutates: false,
|
|
330
|
+
input: deleteSchema,
|
|
331
|
+
inputShape: deleteShape,
|
|
332
|
+
http: { method: 'DELETE', path: '/api/canvas/boards/:id' },
|
|
333
|
+
handler: (input, ctx) => {
|
|
334
|
+
const id = typeof input.id === 'string' ? input.id : '';
|
|
335
|
+
// Deleting the open board returns to Home through the same path as opening it.
|
|
336
|
+
if (id === canvasState.activeBoardId) openCanvasBoard(null);
|
|
337
|
+
if (!canvasState.deleteBoard(id)) throw new OperationError(`Board "${id}" not found.`, 404);
|
|
338
|
+
emitBoardsChanged(ctx);
|
|
339
|
+
return { ok: true, ...boardsPayload() };
|
|
340
|
+
},
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
export const boardOperations: Operation[] = [
|
|
344
|
+
boardListOperation,
|
|
345
|
+
boardOpenOperation,
|
|
346
|
+
boardGetOperation,
|
|
347
|
+
boardCreateOperation,
|
|
348
|
+
boardCreateFromOperation,
|
|
349
|
+
boardUpdateOperation,
|
|
350
|
+
boardDeleteOperation,
|
|
351
|
+
];
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static board export (plan 013): export.preview / export.run. HTTP, CLI and
|
|
3
|
+
* SDK — sharing is the human's act, so there is no MCP surface. The written
|
|
4
|
+
* file is served by GET /api/canvas/exports/:name (server.ts).
|
|
5
|
+
*
|
|
6
|
+
* This module must never import server.ts or index.ts.
|
|
7
|
+
*/
|
|
8
|
+
import { basename } from 'node:path';
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
import { buildBoardExport, previewBoardExport, writeBoardExport } from '../../board-export.js';
|
|
11
|
+
import { canvasState } from '../../canvas-state.js';
|
|
12
|
+
import { defineOperation, OperationError, type Operation } from '../types.js';
|
|
13
|
+
|
|
14
|
+
const shape = {
|
|
15
|
+
board: z.unknown().optional().describe('Board id (default: the open board)'),
|
|
16
|
+
includeFiles: z.unknown().optional().describe('Include the contents of file cards (default: false)'),
|
|
17
|
+
includeDerivedText: z
|
|
18
|
+
.unknown()
|
|
19
|
+
.optional()
|
|
20
|
+
.describe('Include imported document text (default: false; originals remain excluded)'),
|
|
21
|
+
};
|
|
22
|
+
const schema = z.looseObject(shape);
|
|
23
|
+
|
|
24
|
+
function target(input: z.infer<typeof schema>): { boardId: string; includeFiles: boolean } {
|
|
25
|
+
const boardId =
|
|
26
|
+
typeof input.board === 'string' && input.board.trim() ? input.board.trim() : canvasState.activeBoardId;
|
|
27
|
+
if (!boardId) throw new OperationError('No board is open. Open one, or pass board.', 409);
|
|
28
|
+
return { boardId, includeFiles: input.includeFiles === true || input.includeFiles === 'true' };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const exportPreviewOperation = defineOperation<z.infer<typeof schema>, Record<string, unknown>>({
|
|
32
|
+
name: 'export.preview',
|
|
33
|
+
mutates: false,
|
|
34
|
+
input: schema,
|
|
35
|
+
inputShape: shape,
|
|
36
|
+
http: { method: 'GET', path: '/api/canvas/export/preview' },
|
|
37
|
+
handler: async (input) => {
|
|
38
|
+
const { boardId, includeFiles } = target(input);
|
|
39
|
+
const manifest = await previewBoardExport(
|
|
40
|
+
boardId,
|
|
41
|
+
includeFiles,
|
|
42
|
+
input.includeDerivedText === true || input.includeDerivedText === 'true',
|
|
43
|
+
);
|
|
44
|
+
if (!manifest) throw new OperationError(`Board "${boardId}" not found.`, 404);
|
|
45
|
+
return { ok: true, manifest };
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const exportRunOperation = defineOperation<z.infer<typeof schema>, Record<string, unknown>>({
|
|
50
|
+
name: 'export.run',
|
|
51
|
+
mutates: false,
|
|
52
|
+
input: schema,
|
|
53
|
+
inputShape: shape,
|
|
54
|
+
http: { method: 'POST', path: '/api/canvas/export' },
|
|
55
|
+
handler: async (input) => {
|
|
56
|
+
const { boardId, includeFiles } = target(input);
|
|
57
|
+
const built = await buildBoardExport(
|
|
58
|
+
boardId,
|
|
59
|
+
includeFiles,
|
|
60
|
+
input.includeDerivedText === true || input.includeDerivedText === 'true',
|
|
61
|
+
);
|
|
62
|
+
if (!built) throw new OperationError(`Board "${boardId}" not found.`, 404);
|
|
63
|
+
const path = writeBoardExport(built.html, built.manifest.boardName);
|
|
64
|
+
return {
|
|
65
|
+
ok: true,
|
|
66
|
+
path,
|
|
67
|
+
url: `/api/canvas/exports/${encodeURIComponent(basename(path))}`,
|
|
68
|
+
bytes: Buffer.byteLength(built.html),
|
|
69
|
+
manifest: built.manifest,
|
|
70
|
+
};
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
export const exportOperations: Operation[] = [exportPreviewOperation, exportRunOperation];
|
|
@@ -204,10 +204,14 @@ const callToolOperation = defineOperation<z.infer<typeof callToolSchema>, Record
|
|
|
204
204
|
canReadLocalCheckpoint && toolName === EXCALIDRAW_READ_CHECKPOINT_TOOL
|
|
205
205
|
? getLocalExcalidrawCheckpointData(requestedNode, args)
|
|
206
206
|
: null;
|
|
207
|
+
const boardGeneration = canvasState.boardGeneration;
|
|
207
208
|
const result =
|
|
208
209
|
localCheckpointData === null
|
|
209
210
|
? await callMcpAppTool(sessionId, toolName, args)
|
|
210
211
|
: ({ content: [{ type: 'text', text: localCheckpointData }] } satisfies CallToolResult);
|
|
212
|
+
if (canvasState.boardGeneration !== boardGeneration) {
|
|
213
|
+
throw new OperationError('The board changed while the MCP app tool was running.', 409);
|
|
214
|
+
}
|
|
211
215
|
if (nodeId) {
|
|
212
216
|
const node = canvasState.getNode(nodeId);
|
|
213
217
|
if (node?.type === 'mcp-app' && node.data.mode === 'ext-app' && node.data.appSessionId === sessionId) {
|
|
@@ -42,7 +42,7 @@ const groupCreateShape = {
|
|
|
42
42
|
|
|
43
43
|
const groupCreateSchema = z.looseObject(groupCreateShape);
|
|
44
44
|
|
|
45
|
-
const groupCreateOperation = defineOperation<z.infer<typeof groupCreateSchema>,
|
|
45
|
+
const groupCreateOperation = defineOperation<z.infer<typeof groupCreateSchema>, ReturnType<typeof createCanvasGroup>>({
|
|
46
46
|
name: 'group.create',
|
|
47
47
|
mutates: true,
|
|
48
48
|
input: groupCreateSchema,
|
|
@@ -69,7 +69,12 @@ const groupCreateOperation = defineOperation<z.infer<typeof groupCreateSchema>,
|
|
|
69
69
|
formatResult: (result, input) => {
|
|
70
70
|
const body = isRecord(result) ? result : {};
|
|
71
71
|
const node = body.node as CanvasNodeState | undefined;
|
|
72
|
-
const payload = node
|
|
72
|
+
const payload = node
|
|
73
|
+
? {
|
|
74
|
+
...createdNodePayloadFromNode(node, input),
|
|
75
|
+
...(body.sizeAdjustment ? { sizeAdjustment: body.sizeAdjustment } : {}),
|
|
76
|
+
}
|
|
77
|
+
: { ok: true };
|
|
73
78
|
return { content: [{ type: 'text' as const, text: JSON.stringify(payload, null, 2) }] };
|
|
74
79
|
},
|
|
75
80
|
},
|
|
@@ -93,7 +98,7 @@ const groupCreateOperation = defineOperation<z.infer<typeof groupCreateSchema>,
|
|
|
93
98
|
);
|
|
94
99
|
}
|
|
95
100
|
}
|
|
96
|
-
|
|
101
|
+
return createCanvasGroup({
|
|
97
102
|
title,
|
|
98
103
|
childIds,
|
|
99
104
|
color,
|
|
@@ -103,9 +108,11 @@ const groupCreateOperation = defineOperation<z.infer<typeof groupCreateSchema>,
|
|
|
103
108
|
height,
|
|
104
109
|
...(childLayout ? { childLayout } : {}),
|
|
105
110
|
});
|
|
106
|
-
return node;
|
|
107
111
|
},
|
|
108
|
-
serialize: (node) =>
|
|
112
|
+
serialize: ({ node, sizeAdjustment }) => ({
|
|
113
|
+
...buildNodeResponse(node),
|
|
114
|
+
...(sizeAdjustment ? { sizeAdjustment } : {}),
|
|
115
|
+
}),
|
|
109
116
|
});
|
|
110
117
|
|
|
111
118
|
// ── group.add ─────────────────────────────────────────────────
|
|
@@ -157,9 +164,9 @@ const groupAddOperation = defineOperation<z.infer<typeof groupAddSchema>, Record
|
|
|
157
164
|
if (!groupId || childIds.length === 0) {
|
|
158
165
|
throw new OperationError('Missing groupId or childIds.');
|
|
159
166
|
}
|
|
160
|
-
const { ok } = groupCanvasNodes(groupId, childIds, childLayout ? { childLayout } : {});
|
|
167
|
+
const { ok, sizeAdjustment } = groupCanvasNodes(groupId, childIds, childLayout ? { childLayout } : {});
|
|
161
168
|
if (!ok) throw new OperationError('Group not found or no valid children.');
|
|
162
|
-
return { ok: true, groupId };
|
|
169
|
+
return { ok: true, groupId, ...(sizeAdjustment ? { sizeAdjustment } : {}) };
|
|
163
170
|
},
|
|
164
171
|
});
|
|
165
172
|
|