pmx-canvas 0.2.7 → 0.3.1
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 +148 -1
- package/Readme.md +13 -11
- package/dist/canvas/index.js +2 -2
- package/dist/json-render/index.css +1 -1
- package/dist/types/cli/daemon.d.ts +74 -0
- package/dist/types/cli/watch.d.ts +2 -2
- package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
- package/dist/types/client/canvas/CommandPalette.d.ts +1 -1
- package/dist/types/client/canvas/Minimap.d.ts +1 -1
- package/dist/types/client/nodes/FileNode.d.ts +1 -1
- package/dist/types/client/nodes/ImageNode.d.ts +1 -1
- package/dist/types/client/nodes/InlineFormatBar.d.ts +1 -1
- package/dist/types/client/nodes/MarkdownNode.d.ts +1 -1
- package/dist/types/client/nodes/PromptNode.d.ts +1 -1
- package/dist/types/client/nodes/ResponseNode.d.ts +1 -1
- package/dist/types/mcp/canvas-access.d.ts +0 -7
- package/dist/types/server/ax-state-manager.d.ts +3 -2
- package/dist/types/server/bundled-skills.d.ts +3 -3
- package/dist/types/server/canvas-schema.d.ts +1 -1
- package/dist/types/server/canvas-state.d.ts +2 -0
- package/dist/types/server/ext-app-lookup.d.ts +1 -3
- package/dist/types/server/html-primitives.d.ts +1 -1
- package/dist/types/server/index.d.ts +4 -4
- package/dist/types/server/operations/composites.d.ts +29 -23
- package/dist/types/server/operations/http.d.ts +2 -1
- package/dist/types/server/operations/index.d.ts +1 -1
- package/dist/types/server/operations/ops/ax-read.d.ts +2 -0
- package/dist/types/server/operations/ops/canvas-wire.d.ts +2 -0
- package/dist/types/server/operations/ops/ext-app.d.ts +2 -0
- package/dist/types/shared/ax-intent.d.ts +1 -1
- package/docs/RELEASE.md +23 -8
- package/docs/ax-host-adapter-contract.md +7 -7
- package/docs/ax-state-contract.md +7 -6
- package/docs/bun-webview-integration.md +5 -5
- package/docs/cli.md +1 -1
- package/docs/http-api.md +35 -3
- package/docs/mcp.md +121 -100
- package/docs/node-types.md +45 -35
- package/docs/plans/plan-006-mcp-tool-consolidation.md +3 -1
- package/docs/plans/plan-008-registry-finish.md +2 -0
- package/docs/plans/plan-009-tech-debt-backlog.md +51 -0
- package/docs/screenshot.png +0 -0
- package/docs/tech-debt-assessment-2026-06.md +1 -1
- package/docs/tech-debt-assessment-2026-07.md +135 -0
- package/package.json +5 -2
- package/skills/data-analysis/SKILL.md +3 -3
- package/skills/frontend-design/SKILL.md +3 -2
- package/skills/json-render-mcp/SKILL.md +4 -3
- package/skills/playwright-cli/SKILL.md +1 -1
- package/skills/pmx-canvas/SKILL.md +12 -7
- package/skills/pmx-canvas/evals/evals.json +2 -2
- package/skills/pmx-canvas/references/ax-html-control-surface.md +3 -1
- package/skills/pmx-canvas/references/excalidraw-diagram-authoring.md +2 -2
- package/skills/pmx-canvas/references/full-reference.md +58 -56
- package/skills/pmx-canvas/references/html-primitives.md +3 -3
- package/skills/tufte-viz/SKILL.md +5 -4
- package/skills/web-artifacts-builder/SKILL.md +4 -4
- package/src/cli/agent.ts +1861 -1548
- package/src/cli/daemon.ts +460 -0
- package/src/cli/index.ts +63 -326
- package/src/cli/watch.ts +2 -10
- package/src/client/App.tsx +48 -46
- package/src/client/canvas/AttentionHistory.tsx +11 -1
- package/src/client/canvas/CanvasNode.tsx +41 -29
- package/src/client/canvas/CanvasViewport.tsx +101 -66
- package/src/client/canvas/CommandPalette.tsx +61 -27
- package/src/client/canvas/ContextMenu.tsx +13 -20
- package/src/client/canvas/ContextPinBar.tsx +1 -5
- package/src/client/canvas/ContextPinHud.tsx +1 -6
- package/src/client/canvas/DockedNode.tsx +4 -4
- package/src/client/canvas/EdgeLayer.tsx +37 -36
- package/src/client/canvas/ExpandedNodeOverlay.tsx +69 -45
- package/src/client/canvas/FocusFieldLayer.tsx +20 -22
- package/src/client/canvas/IntentLayer.tsx +31 -14
- package/src/client/canvas/Minimap.tsx +11 -16
- package/src/client/canvas/SelectionBar.tsx +4 -11
- package/src/client/canvas/ShortcutOverlay.tsx +3 -1
- package/src/client/canvas/SnapshotPanel.tsx +77 -95
- package/src/client/canvas/auto-fit.ts +15 -14
- package/src/client/canvas/snap-guides.ts +12 -12
- package/src/client/canvas/use-node-resize.ts +1 -5
- package/src/client/canvas/use-pan-zoom.ts +25 -26
- package/src/client/ext-app/bridge.ts +3 -12
- package/src/client/icons.tsx +63 -20
- package/src/client/nodes/ContextNode.tsx +14 -25
- package/src/client/nodes/ExtAppFrame.tsx +60 -39
- package/src/client/nodes/FileNode.tsx +74 -62
- package/src/client/nodes/GroupNode.tsx +4 -6
- package/src/client/nodes/HtmlNode.tsx +76 -46
- package/src/client/nodes/ImageNode.tsx +18 -27
- package/src/client/nodes/InlineFormatBar.tsx +4 -21
- package/src/client/nodes/InlineMarkdownEditor.tsx +0 -1
- package/src/client/nodes/LedgerNode.tsx +10 -4
- package/src/client/nodes/MarkdownNode.tsx +3 -10
- package/src/client/nodes/McpAppNode.tsx +26 -22
- package/src/client/nodes/MdFormatBar.tsx +10 -7
- package/src/client/nodes/PromptNode.tsx +23 -51
- package/src/client/nodes/ResponseNode.tsx +3 -13
- package/src/client/nodes/StatusNode.tsx +5 -9
- package/src/client/nodes/StatusSummary.tsx +2 -8
- package/src/client/nodes/WebpageNode.tsx +20 -14
- package/src/client/nodes/iframe-document-url.ts +25 -16
- package/src/client/nodes/image-warnings.ts +1 -7
- package/src/client/nodes/md-format.ts +20 -5
- package/src/client/state/attention-bridge.ts +4 -9
- package/src/client/state/attention-store.ts +1 -7
- package/src/client/state/canvas-store.ts +52 -36
- package/src/client/state/intent-bridge.ts +176 -112
- package/src/client/state/intent-store.ts +4 -1
- package/src/client/state/sse-bridge.ts +53 -70
- package/src/json-render/catalog.ts +12 -16
- package/src/json-render/charts/components.tsx +16 -20
- package/src/json-render/charts/extra-components.tsx +8 -16
- package/src/json-render/charts/extra-definitions.ts +1 -2
- package/src/json-render/charts/tufte-components.tsx +37 -20
- package/src/json-render/charts/tufte-definitions.ts +8 -2
- package/src/json-render/renderer/index.tsx +42 -22
- package/src/json-render/schema.ts +6 -3
- package/src/json-render/server.ts +33 -39
- package/src/mcp/canvas-access.ts +40 -121
- package/src/mcp/server.ts +206 -271
- package/src/server/agent-context.ts +63 -36
- package/src/server/ax-context.ts +7 -5
- package/src/server/ax-interaction.ts +176 -43
- package/src/server/ax-state-manager.ts +185 -41
- package/src/server/ax-state.ts +142 -47
- package/src/server/bundled-skills.ts +3 -3
- package/src/server/canvas-db.ts +213 -95
- package/src/server/canvas-operations.ts +177 -120
- package/src/server/canvas-provenance.ts +1 -4
- package/src/server/canvas-schema.ts +500 -102
- package/src/server/canvas-serialization.ts +27 -35
- package/src/server/canvas-state.ts +155 -58
- package/src/server/chart-template.ts +4 -5
- package/src/server/code-graph.ts +20 -7
- package/src/server/diagram-presets.ts +28 -29
- package/src/server/ext-app-lookup.ts +4 -20
- package/src/server/html-node-summary.ts +19 -10
- package/src/server/html-primitives.ts +326 -97
- package/src/server/html-surface.ts +6 -9
- package/src/server/image-source.ts +6 -4
- package/src/server/index.ts +321 -218
- package/src/server/intent-registry.ts +3 -6
- package/src/server/mcp-app-candidate.ts +5 -10
- package/src/server/mcp-app-host.ts +14 -38
- package/src/server/mcp-app-runtime.ts +12 -20
- package/src/server/mutation-history.ts +15 -5
- package/src/server/operations/composites.ts +38 -40
- package/src/server/operations/http.ts +5 -5
- package/src/server/operations/index.ts +7 -1
- package/src/server/operations/invoker.ts +4 -3
- package/src/server/operations/mcp.ts +29 -36
- package/src/server/operations/ops/annotation.ts +122 -10
- package/src/server/operations/ops/app.ts +102 -77
- package/src/server/operations/ops/ax-await.ts +17 -10
- package/src/server/operations/ops/ax-read.ts +347 -0
- package/src/server/operations/ops/ax-shared.ts +2 -7
- package/src/server/operations/ops/ax-state.ts +32 -14
- package/src/server/operations/ops/ax-timeline.ts +32 -19
- package/src/server/operations/ops/ax-work.ts +54 -37
- package/src/server/operations/ops/batch.ts +39 -14
- package/src/server/operations/ops/canvas-wire.ts +91 -0
- package/src/server/operations/ops/edges.ts +37 -25
- package/src/server/operations/ops/ext-app.ts +346 -0
- package/src/server/operations/ops/groups.ts +49 -20
- package/src/server/operations/ops/intent.ts +18 -12
- package/src/server/operations/ops/json-render.ts +239 -98
- package/src/server/operations/ops/nodes.ts +300 -111
- package/src/server/operations/ops/query.ts +62 -33
- package/src/server/operations/ops/snapshots.ts +35 -26
- package/src/server/operations/ops/validate.ts +2 -1
- package/src/server/operations/ops/viewport.ts +60 -16
- package/src/server/operations/ops/webview.ts +44 -18
- package/src/server/operations/registry.ts +2 -3
- package/src/server/operations/types.ts +7 -5
- package/src/server/operations/webview-runner.ts +1 -3
- package/src/server/placement.ts +8 -18
- package/src/server/server.ts +121 -1012
- package/src/server/spatial-analysis.ts +39 -25
- package/src/server/trace-manager.ts +3 -8
- package/src/server/web-artifacts.ts +23 -27
- package/src/server/webpage-node.ts +5 -13
- package/src/shared/auto-arrange.ts +12 -5
- package/src/shared/ax-intent.ts +1 -1
- package/src/shared/content-height-reporter.ts +8 -6
- package/src/shared/ext-app-tool-result.ts +2 -6
- package/src/shared/placement.ts +1 -4
- package/src/shared/semantic-attention.ts +39 -37
- package/src/shared/surface.ts +8 -4
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ext-app operations (plan-009 C1 slice 1): extapp.call-tool /
|
|
3
|
+
* extapp.read-resource / extapp.list-tools / extapp.list-resources /
|
|
4
|
+
* extapp.list-resource-templates / extapp.list-prompts / extapp.model-context
|
|
5
|
+
* — the /api/ext-app/* wire surface the canvas iframe bridge calls. Wire
|
|
6
|
+
* envelopes are byte-identical to the legacy server.ts handlers they replace
|
|
7
|
+
* ({ ok: true, result } / { ok: false, error } with the same status codes).
|
|
8
|
+
* HTTP-only: none of these register MCP tools (the sandboxed iframe is the
|
|
9
|
+
* caller, not an agent), so the frozen 27-tool MCP surface is untouched.
|
|
10
|
+
*
|
|
11
|
+
* This module must never import server.ts or index.ts.
|
|
12
|
+
*/
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
15
|
+
import { canvasState, type CanvasNodeState } from '../../canvas-state.js';
|
|
16
|
+
import {
|
|
17
|
+
buildExcalidrawRestoreCheckpointToolInput,
|
|
18
|
+
buildExcalidrawCheckpointId,
|
|
19
|
+
ensureExcalidrawCheckpointId,
|
|
20
|
+
EXCALIDRAW_READ_CHECKPOINT_TOOL,
|
|
21
|
+
EXCALIDRAW_SAVE_CHECKPOINT_TOOL,
|
|
22
|
+
getExcalidrawCheckpointIdFromToolResult,
|
|
23
|
+
isExcalidrawCreateView,
|
|
24
|
+
} from '../../diagram-presets.js';
|
|
25
|
+
import {
|
|
26
|
+
callMcpAppTool,
|
|
27
|
+
listMcpAppPrompts,
|
|
28
|
+
listMcpAppResources,
|
|
29
|
+
listMcpAppResourceTemplates,
|
|
30
|
+
listMcpAppTools,
|
|
31
|
+
readMcpAppResource,
|
|
32
|
+
} from '../../mcp-app-runtime.js';
|
|
33
|
+
import { defineOperation, OperationError, type Operation, type OperationContext } from '../types.js';
|
|
34
|
+
import { isRecord } from './nodes.js';
|
|
35
|
+
|
|
36
|
+
// ── Excalidraw checkpoint helpers (moved from server.ts) ──────
|
|
37
|
+
|
|
38
|
+
function isCheckpointToolName(toolName: string): boolean {
|
|
39
|
+
return toolName === EXCALIDRAW_SAVE_CHECKPOINT_TOOL || toolName === EXCALIDRAW_READ_CHECKPOINT_TOOL;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Decide whether a fresh `callServerTool` result should *replace* the
|
|
44
|
+
* canvas node's bootstrap-replay `toolResult`.
|
|
45
|
+
*
|
|
46
|
+
* The bootstrap-replay toolResult is what the server re-sends to the
|
|
47
|
+
* widget on reload to restore visible state. We only want to overwrite
|
|
48
|
+
* it when the new result genuinely carries widget state — `isError` or
|
|
49
|
+
* `structuredContent`. A plain-text result (e.g. `read_checkpoint`
|
|
50
|
+
* returning a string status, or any informational message) updates
|
|
51
|
+
* `appModelContext` for the agent's record but should *not* clobber the
|
|
52
|
+
* bootstrap entry, because doing so would replace the widget's restored
|
|
53
|
+
* state with conversational noise on the next reload.
|
|
54
|
+
*
|
|
55
|
+
* This separation is exercised by:
|
|
56
|
+
* - tests/unit/server-api.test.ts "keeps ext-app model context
|
|
57
|
+
* separate from the replayed tool result" (text-only result preserves
|
|
58
|
+
* bootstrap replay)
|
|
59
|
+
* - tests/unit/server-api.test.ts "app-only text tool results update
|
|
60
|
+
* model context without replacing bootstrap replay"
|
|
61
|
+
* - tests/unit/server-api.test.ts "rehydrates Excalidraw checkpoint
|
|
62
|
+
* replay after server restart" (structured-content result becomes
|
|
63
|
+
* the new bootstrap replay)
|
|
64
|
+
*/
|
|
65
|
+
function shouldReplayAppToolResult(toolName: string, result: CallToolResult): boolean {
|
|
66
|
+
void toolName;
|
|
67
|
+
return Boolean(result.isError || result.structuredContent);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function getExtAppNodeCheckpointId(node: CanvasNodeState): string {
|
|
71
|
+
const appCheckpoint = isRecord(node.data.appCheckpoint) ? node.data.appCheckpoint : null;
|
|
72
|
+
const storedCheckpointId = appCheckpoint?.id;
|
|
73
|
+
if (typeof storedCheckpointId === 'string' && storedCheckpointId.trim().length > 0) {
|
|
74
|
+
return storedCheckpointId.trim();
|
|
75
|
+
}
|
|
76
|
+
return getExcalidrawCheckpointIdFromToolResult(node.data.toolResult) ?? buildExcalidrawCheckpointId(node.id);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function getLocalExcalidrawCheckpointData(
|
|
80
|
+
node: CanvasNodeState,
|
|
81
|
+
args: Record<string, unknown> | undefined,
|
|
82
|
+
): string | null {
|
|
83
|
+
if (!isExcalidrawCreateView(node.data.serverName, node.data.toolName)) return null;
|
|
84
|
+
if (!isRecord(args) || typeof args.id !== 'string') return null;
|
|
85
|
+
if (args.id.trim() !== getExtAppNodeCheckpointId(node)) return null;
|
|
86
|
+
const appCheckpoint = isRecord(node.data.appCheckpoint) ? node.data.appCheckpoint : null;
|
|
87
|
+
const data = appCheckpoint?.data;
|
|
88
|
+
return typeof data === 'string' ? data : '';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function persistExcalidrawCheckpointToNode(
|
|
92
|
+
nodeId: string,
|
|
93
|
+
node: CanvasNodeState,
|
|
94
|
+
args: Record<string, unknown> | undefined,
|
|
95
|
+
): boolean {
|
|
96
|
+
if (!isExcalidrawCreateView(node.data.serverName, node.data.toolName)) return false;
|
|
97
|
+
if (!isRecord(args) || typeof args.id !== 'string') return false;
|
|
98
|
+
const checkpointId = getExtAppNodeCheckpointId(node);
|
|
99
|
+
if (args.id.trim() !== checkpointId) return false;
|
|
100
|
+
|
|
101
|
+
const currentToolInput = isRecord(node.data.toolInput) ? node.data.toolInput : {};
|
|
102
|
+
const nextToolInput = {
|
|
103
|
+
...currentToolInput,
|
|
104
|
+
elements: buildExcalidrawRestoreCheckpointToolInput(checkpointId, args.data),
|
|
105
|
+
};
|
|
106
|
+
const currentToolResult = isRecord(node.data.toolResult)
|
|
107
|
+
? ensureExcalidrawCheckpointId(node.data.toolResult as CallToolResult, node.id, checkpointId)
|
|
108
|
+
: undefined;
|
|
109
|
+
|
|
110
|
+
canvasState.updateNode(nodeId, {
|
|
111
|
+
data: {
|
|
112
|
+
...node.data,
|
|
113
|
+
toolInput: nextToolInput,
|
|
114
|
+
...(currentToolResult ? { toolResult: currentToolResult } : {}),
|
|
115
|
+
appCheckpoint: {
|
|
116
|
+
toolName: EXCALIDRAW_SAVE_CHECKPOINT_TOOL,
|
|
117
|
+
id: checkpointId,
|
|
118
|
+
...(typeof args.data === 'string' ? { data: args.data } : {}),
|
|
119
|
+
updatedAt: new Date().toISOString(),
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ── Shared input plumbing ─────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
function requireSessionId(body: Record<string, unknown>): string {
|
|
129
|
+
const sessionId = typeof body.sessionId === 'string' ? body.sessionId.trim() : '';
|
|
130
|
+
if (!sessionId) throw new OperationError('Missing sessionId.');
|
|
131
|
+
return sessionId;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Runtime failures keep the legacy wire shape: 400 { ok:false, error }. */
|
|
135
|
+
function toWireError(error: unknown): OperationError {
|
|
136
|
+
return new OperationError(error instanceof Error ? error.message : String(error));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const sessionShape = {
|
|
140
|
+
sessionId: z.unknown().optional().describe('Ext-app session id'),
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/** The four list-* endpoints differ only in the runtime call they proxy. */
|
|
144
|
+
function defineExtAppListOperation(segment: string, list: (sessionId: string) => Promise<unknown>): Operation {
|
|
145
|
+
const schema = z.looseObject(sessionShape);
|
|
146
|
+
return defineOperation<z.infer<typeof schema>, Record<string, unknown>>({
|
|
147
|
+
name: `extapp.${segment}`,
|
|
148
|
+
mutates: false,
|
|
149
|
+
input: schema,
|
|
150
|
+
inputShape: sessionShape,
|
|
151
|
+
http: {
|
|
152
|
+
method: 'POST',
|
|
153
|
+
path: `/api/ext-app/${segment}`,
|
|
154
|
+
},
|
|
155
|
+
handler: async (input) => {
|
|
156
|
+
const sessionId = requireSessionId(input);
|
|
157
|
+
try {
|
|
158
|
+
return { ok: true, result: await list(sessionId) };
|
|
159
|
+
} catch (error) {
|
|
160
|
+
throw toWireError(error);
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ── extapp.call-tool ──────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
const callToolShape = {
|
|
169
|
+
sessionId: z.unknown().optional().describe('Ext-app session id'),
|
|
170
|
+
toolName: z.unknown().optional().describe('Tool to invoke on the ext-app MCP session'),
|
|
171
|
+
arguments: z.unknown().optional().describe('Tool arguments object'),
|
|
172
|
+
nodeId: z.unknown().optional().describe('Canvas node id to replay/persist the tool result on'),
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const callToolSchema = z.looseObject(callToolShape);
|
|
176
|
+
|
|
177
|
+
const callToolOperation = defineOperation<z.infer<typeof callToolSchema>, Record<string, unknown>>({
|
|
178
|
+
name: 'extapp.call-tool',
|
|
179
|
+
mutates: false,
|
|
180
|
+
input: callToolSchema,
|
|
181
|
+
inputShape: callToolShape,
|
|
182
|
+
http: {
|
|
183
|
+
method: 'POST',
|
|
184
|
+
path: '/api/ext-app/call-tool',
|
|
185
|
+
},
|
|
186
|
+
handler: async (input, ctx: OperationContext) => {
|
|
187
|
+
const body: Record<string, unknown> = input;
|
|
188
|
+
const sessionId = typeof body.sessionId === 'string' ? body.sessionId.trim() : '';
|
|
189
|
+
const toolName = typeof body.toolName === 'string' ? body.toolName.trim() : '';
|
|
190
|
+
if (!sessionId || !toolName) {
|
|
191
|
+
throw new OperationError('Missing sessionId or toolName.');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const args = isRecord(body.arguments) ? body.arguments : undefined;
|
|
195
|
+
const nodeId = typeof body.nodeId === 'string' ? body.nodeId.trim() : '';
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
const requestedNode = nodeId ? canvasState.getNode(nodeId) : undefined;
|
|
199
|
+
const canReadLocalCheckpoint =
|
|
200
|
+
requestedNode?.type === 'mcp-app' &&
|
|
201
|
+
requestedNode.data.mode === 'ext-app' &&
|
|
202
|
+
requestedNode.data.appSessionId === sessionId;
|
|
203
|
+
const localCheckpointData =
|
|
204
|
+
canReadLocalCheckpoint && toolName === EXCALIDRAW_READ_CHECKPOINT_TOOL
|
|
205
|
+
? getLocalExcalidrawCheckpointData(requestedNode, args)
|
|
206
|
+
: null;
|
|
207
|
+
const result =
|
|
208
|
+
localCheckpointData === null
|
|
209
|
+
? await callMcpAppTool(sessionId, toolName, args)
|
|
210
|
+
: ({ content: [{ type: 'text', text: localCheckpointData }] } satisfies CallToolResult);
|
|
211
|
+
if (nodeId) {
|
|
212
|
+
const node = canvasState.getNode(nodeId);
|
|
213
|
+
if (node?.type === 'mcp-app' && node.data.mode === 'ext-app' && node.data.appSessionId === sessionId) {
|
|
214
|
+
let changed = false;
|
|
215
|
+
if (toolName === EXCALIDRAW_SAVE_CHECKPOINT_TOOL && persistExcalidrawCheckpointToNode(nodeId, node, args)) {
|
|
216
|
+
// Checkpoint saves are replayed through toolInput.elements instead of
|
|
217
|
+
// replacing the original create_view result with a generic "ok".
|
|
218
|
+
changed = true;
|
|
219
|
+
} else if (
|
|
220
|
+
!(isExcalidrawCreateView(node.data.serverName, node.data.toolName) && isCheckpointToolName(toolName))
|
|
221
|
+
) {
|
|
222
|
+
const nextData: Record<string, unknown> = { ...node.data };
|
|
223
|
+
if (shouldReplayAppToolResult(toolName, result)) nextData.toolResult = result;
|
|
224
|
+
const nextModelContext: Record<string, unknown> = {};
|
|
225
|
+
if (Array.isArray(result.content)) {
|
|
226
|
+
nextModelContext.content = result.content;
|
|
227
|
+
}
|
|
228
|
+
if (
|
|
229
|
+
result.structuredContent &&
|
|
230
|
+
typeof result.structuredContent === 'object' &&
|
|
231
|
+
!Array.isArray(result.structuredContent)
|
|
232
|
+
) {
|
|
233
|
+
nextModelContext.structuredContent = result.structuredContent;
|
|
234
|
+
}
|
|
235
|
+
if (Object.keys(nextModelContext).length > 0) {
|
|
236
|
+
nextData.appModelContext = {
|
|
237
|
+
...nextModelContext,
|
|
238
|
+
updatedAt: new Date().toISOString(),
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
canvasState.updateNode(nodeId, {
|
|
242
|
+
data: nextData,
|
|
243
|
+
});
|
|
244
|
+
changed = true;
|
|
245
|
+
}
|
|
246
|
+
if (changed) {
|
|
247
|
+
ctx.emit('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return { ok: true, result };
|
|
252
|
+
} catch (error) {
|
|
253
|
+
throw toWireError(error);
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// ── extapp.read-resource ──────────────────────────────────────
|
|
259
|
+
|
|
260
|
+
const readResourceShape = {
|
|
261
|
+
sessionId: z.unknown().optional().describe('Ext-app session id'),
|
|
262
|
+
uri: z.unknown().optional().describe('Resource URI to read from the ext-app MCP session'),
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const readResourceSchema = z.looseObject(readResourceShape);
|
|
266
|
+
|
|
267
|
+
const readResourceOperation = defineOperation<z.infer<typeof readResourceSchema>, Record<string, unknown>>({
|
|
268
|
+
name: 'extapp.read-resource',
|
|
269
|
+
mutates: false,
|
|
270
|
+
input: readResourceSchema,
|
|
271
|
+
inputShape: readResourceShape,
|
|
272
|
+
http: {
|
|
273
|
+
method: 'POST',
|
|
274
|
+
path: '/api/ext-app/read-resource',
|
|
275
|
+
},
|
|
276
|
+
handler: async (input) => {
|
|
277
|
+
const body: Record<string, unknown> = input;
|
|
278
|
+
const sessionId = typeof body.sessionId === 'string' ? body.sessionId.trim() : '';
|
|
279
|
+
const uri = typeof body.uri === 'string' ? body.uri.trim() : '';
|
|
280
|
+
if (!sessionId || !uri) {
|
|
281
|
+
throw new OperationError('Missing sessionId or uri.');
|
|
282
|
+
}
|
|
283
|
+
try {
|
|
284
|
+
return { ok: true, result: await readMcpAppResource(sessionId, uri) };
|
|
285
|
+
} catch (error) {
|
|
286
|
+
throw toWireError(error);
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// ── extapp.model-context ──────────────────────────────────────
|
|
292
|
+
|
|
293
|
+
const modelContextShape = {
|
|
294
|
+
nodeId: z.unknown().optional().describe('Canvas node id to attach the model context to'),
|
|
295
|
+
content: z.unknown().optional().describe('MCP content array'),
|
|
296
|
+
structuredContent: z.unknown().optional().describe('MCP structured content object'),
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const modelContextSchema = z.looseObject(modelContextShape);
|
|
300
|
+
|
|
301
|
+
const modelContextOperation = defineOperation<z.infer<typeof modelContextSchema>, Record<string, unknown>>({
|
|
302
|
+
name: 'extapp.model-context',
|
|
303
|
+
mutates: false,
|
|
304
|
+
input: modelContextSchema,
|
|
305
|
+
inputShape: modelContextShape,
|
|
306
|
+
http: {
|
|
307
|
+
method: 'POST',
|
|
308
|
+
path: '/api/ext-app/model-context',
|
|
309
|
+
},
|
|
310
|
+
handler: (input, ctx: OperationContext) => {
|
|
311
|
+
const body: Record<string, unknown> = input;
|
|
312
|
+
const nodeId = typeof body.nodeId === 'string' ? body.nodeId.trim() : '';
|
|
313
|
+
if (!nodeId) throw new OperationError('Missing nodeId.');
|
|
314
|
+
|
|
315
|
+
const node = canvasState.getNode(nodeId);
|
|
316
|
+
if (!node) throw new OperationError(`Node "${nodeId}" not found.`, 404);
|
|
317
|
+
|
|
318
|
+
canvasState.updateNode(nodeId, {
|
|
319
|
+
data: {
|
|
320
|
+
...node.data,
|
|
321
|
+
appModelContext: {
|
|
322
|
+
...(Array.isArray(body.content) ? { content: body.content } : {}),
|
|
323
|
+
...(body.structuredContent &&
|
|
324
|
+
typeof body.structuredContent === 'object' &&
|
|
325
|
+
!Array.isArray(body.structuredContent)
|
|
326
|
+
? { structuredContent: body.structuredContent }
|
|
327
|
+
: {}),
|
|
328
|
+
updatedAt: new Date().toISOString(),
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
ctx.emit('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
334
|
+
return { ok: true };
|
|
335
|
+
},
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
export const extAppOperations: Operation[] = [
|
|
339
|
+
callToolOperation,
|
|
340
|
+
readResourceOperation,
|
|
341
|
+
defineExtAppListOperation('list-tools', listMcpAppTools),
|
|
342
|
+
defineExtAppListOperation('list-resources', listMcpAppResources),
|
|
343
|
+
defineExtAppListOperation('list-resource-templates', listMcpAppResourceTemplates),
|
|
344
|
+
defineExtAppListOperation('list-prompts', listMcpAppPrompts),
|
|
345
|
+
modelContextOperation,
|
|
346
|
+
];
|
|
@@ -10,11 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { z } from 'zod';
|
|
12
12
|
import { canvasState, type CanvasNodeState } from '../../canvas-state.js';
|
|
13
|
-
import {
|
|
14
|
-
createCanvasGroup,
|
|
15
|
-
groupCanvasNodes,
|
|
16
|
-
ungroupCanvasNodes,
|
|
17
|
-
} from '../../canvas-operations.js';
|
|
13
|
+
import { createCanvasGroup, groupCanvasNodes, ungroupCanvasNodes } from '../../canvas-operations.js';
|
|
18
14
|
import { defineOperation, OperationError, type Operation } from '../types.js';
|
|
19
15
|
import { buildNodeResponse, createdNodePayloadFromNode, isRecord } from './nodes.js';
|
|
20
16
|
|
|
@@ -25,7 +21,11 @@ function pickChildLayout(value: unknown): 'grid' | 'column' | 'flow' | undefined
|
|
|
25
21
|
// ── group.create ──────────────────────────────────────────────
|
|
26
22
|
|
|
27
23
|
const groupCreateShape = {
|
|
28
|
-
intentId: z
|
|
24
|
+
intentId: z
|
|
25
|
+
.string()
|
|
26
|
+
.optional()
|
|
27
|
+
.catch(undefined)
|
|
28
|
+
.describe('Ghost intent id returned by canvas_intent signal. A vetoed or expired intent blocks this mutation.'),
|
|
29
29
|
title: z.string().optional().catch(undefined).describe('Group title (default: "Group")'),
|
|
30
30
|
childIds: z.unknown().optional().describe('Node IDs to include in the group. Group auto-sizes to fit them.'),
|
|
31
31
|
color: z.string().optional().catch(undefined).describe('Group accent color (CSS color string, e.g. "#4a9eff")'),
|
|
@@ -33,7 +33,11 @@ const groupCreateShape = {
|
|
|
33
33
|
y: z.number().optional().catch(undefined).describe('Y position (auto-computed from children if omitted)'),
|
|
34
34
|
width: z.number().optional().catch(undefined).describe('Width (auto-computed from children if omitted)'),
|
|
35
35
|
height: z.number().optional().catch(undefined).describe('Height (auto-computed from children if omitted)'),
|
|
36
|
-
childLayout: z
|
|
36
|
+
childLayout: z
|
|
37
|
+
.enum(['grid', 'column', 'flow'])
|
|
38
|
+
.optional()
|
|
39
|
+
.catch(undefined)
|
|
40
|
+
.describe('Optional child auto-layout. Omit to preserve current child positions.'),
|
|
37
41
|
};
|
|
38
42
|
|
|
39
43
|
const groupCreateSchema = z.looseObject(groupCreateShape);
|
|
@@ -49,10 +53,17 @@ const groupCreateOperation = defineOperation<z.infer<typeof groupCreateSchema>,
|
|
|
49
53
|
},
|
|
50
54
|
mcp: {
|
|
51
55
|
toolName: 'canvas_create_group',
|
|
52
|
-
description:
|
|
56
|
+
description:
|
|
57
|
+
'Create a group (frame) on the canvas that visually contains other nodes. Groups are spatial containers — they communicate "these nodes belong together." If childIds are provided, grouping preserves child positions by default; pass childLayout to auto-pack them. You can also provide an explicit frame (x/y/width/height) and auto-arrange children inside it.',
|
|
53
58
|
extraShape: {
|
|
54
|
-
childIds: z
|
|
55
|
-
|
|
59
|
+
childIds: z
|
|
60
|
+
.array(z.string())
|
|
61
|
+
.optional()
|
|
62
|
+
.describe('Node IDs to include in the group. Group auto-sizes to fit them.'),
|
|
63
|
+
full: z
|
|
64
|
+
.boolean()
|
|
65
|
+
.optional()
|
|
66
|
+
.describe('Return the full created group payload. Default false returns compact metadata.'),
|
|
56
67
|
verbose: z.boolean().optional().describe('Alias for full:true.'),
|
|
57
68
|
},
|
|
58
69
|
formatResult: (result, input) => {
|
|
@@ -82,7 +93,16 @@ const groupCreateOperation = defineOperation<z.infer<typeof groupCreateSchema>,
|
|
|
82
93
|
);
|
|
83
94
|
}
|
|
84
95
|
}
|
|
85
|
-
const { node } = createCanvasGroup({
|
|
96
|
+
const { node } = createCanvasGroup({
|
|
97
|
+
title,
|
|
98
|
+
childIds,
|
|
99
|
+
color,
|
|
100
|
+
x,
|
|
101
|
+
y,
|
|
102
|
+
width,
|
|
103
|
+
height,
|
|
104
|
+
...(childLayout ? { childLayout } : {}),
|
|
105
|
+
});
|
|
86
106
|
return node;
|
|
87
107
|
},
|
|
88
108
|
serialize: (node) => buildNodeResponse(node),
|
|
@@ -91,10 +111,18 @@ const groupCreateOperation = defineOperation<z.infer<typeof groupCreateSchema>,
|
|
|
91
111
|
// ── group.add ─────────────────────────────────────────────────
|
|
92
112
|
|
|
93
113
|
const groupAddShape = {
|
|
94
|
-
intentId: z
|
|
114
|
+
intentId: z
|
|
115
|
+
.string()
|
|
116
|
+
.optional()
|
|
117
|
+
.catch(undefined)
|
|
118
|
+
.describe('Ghost intent id returned by canvas_intent signal. A vetoed or expired intent blocks this mutation.'),
|
|
95
119
|
groupId: z.string().optional().catch(undefined).describe('The group node ID'),
|
|
96
120
|
childIds: z.unknown().optional().describe('Node IDs to add to the group'),
|
|
97
|
-
childLayout: z
|
|
121
|
+
childLayout: z
|
|
122
|
+
.enum(['grid', 'column', 'flow'])
|
|
123
|
+
.optional()
|
|
124
|
+
.catch(undefined)
|
|
125
|
+
.describe('Optional child layout to apply while grouping'),
|
|
98
126
|
};
|
|
99
127
|
|
|
100
128
|
const groupAddSchema = z.looseObject(groupAddShape);
|
|
@@ -138,7 +166,11 @@ const groupAddOperation = defineOperation<z.infer<typeof groupAddSchema>, Record
|
|
|
138
166
|
// ── group.remove (ungroup) ────────────────────────────────────
|
|
139
167
|
|
|
140
168
|
const groupRemoveShape = {
|
|
141
|
-
intentId: z
|
|
169
|
+
intentId: z
|
|
170
|
+
.string()
|
|
171
|
+
.optional()
|
|
172
|
+
.catch(undefined)
|
|
173
|
+
.describe('Ghost intent id returned by canvas_intent signal. A vetoed or expired intent blocks this mutation.'),
|
|
142
174
|
groupId: z.string().optional().catch(undefined).describe('The group node ID to ungroup'),
|
|
143
175
|
};
|
|
144
176
|
|
|
@@ -155,7 +187,8 @@ const groupRemoveOperation = defineOperation<z.infer<typeof groupRemoveSchema>,
|
|
|
155
187
|
},
|
|
156
188
|
mcp: {
|
|
157
189
|
toolName: 'canvas_ungroup',
|
|
158
|
-
description:
|
|
190
|
+
description:
|
|
191
|
+
'Remove all children from a group, releasing them as independent nodes. The group node itself remains (delete it separately with canvas_remove_node if desired).',
|
|
159
192
|
extraShape: {
|
|
160
193
|
groupId: z.string().describe('The group node ID to ungroup'),
|
|
161
194
|
},
|
|
@@ -172,8 +205,4 @@ const groupRemoveOperation = defineOperation<z.infer<typeof groupRemoveSchema>,
|
|
|
172
205
|
},
|
|
173
206
|
});
|
|
174
207
|
|
|
175
|
-
export const groupOperations: Operation[] = [
|
|
176
|
-
groupCreateOperation,
|
|
177
|
-
groupAddOperation,
|
|
178
|
-
groupRemoveOperation,
|
|
179
|
-
];
|
|
208
|
+
export const groupOperations: Operation[] = [groupCreateOperation, groupAddOperation, groupRemoveOperation];
|
|
@@ -24,7 +24,10 @@ const positionShape = z.object({ x: z.number(), y: z.number() });
|
|
|
24
24
|
// ── intent.signal (canvas_intent action "signal") ─────────────
|
|
25
25
|
|
|
26
26
|
const intentSignalShape = {
|
|
27
|
-
kind: z
|
|
27
|
+
kind: z
|
|
28
|
+
.enum(INTENT_KINDS)
|
|
29
|
+
.optional()
|
|
30
|
+
.describe('create | move | connect | remove | edit — the move about to be made.'),
|
|
28
31
|
position: positionShape.optional().describe('World coords: where a create forms, or the destination of a move.'),
|
|
29
32
|
nodeId: z.string().optional().describe('The existing node a move/edit/remove targets.'),
|
|
30
33
|
edge: z
|
|
@@ -66,7 +69,12 @@ const intentUpdateShape = {
|
|
|
66
69
|
confidence: z.number().optional().describe('0..1 → ghost opacity/solidity.'),
|
|
67
70
|
seq: z.number().optional().describe('New ordering hint.'),
|
|
68
71
|
ttlMs: z.number().optional().describe('Reset the TTL to this many ms from now.'),
|
|
69
|
-
vetoed: z
|
|
72
|
+
vetoed: z
|
|
73
|
+
.boolean()
|
|
74
|
+
.optional()
|
|
75
|
+
.describe(
|
|
76
|
+
'Veto the intent: dissolves the ghost AND poisons the id so a later linked settle is rejected (same as clear { vetoed:true }).',
|
|
77
|
+
),
|
|
70
78
|
};
|
|
71
79
|
|
|
72
80
|
const intentUpdateSchema = z.looseObject(intentUpdateShape);
|
|
@@ -104,14 +112,16 @@ const intentClearOperation = defineOperation<z.infer<typeof intentClearSchema>,
|
|
|
104
112
|
method: 'DELETE',
|
|
105
113
|
path: '/api/canvas/ax/intent/:id',
|
|
106
114
|
readInput: async (req, params, url) => {
|
|
107
|
-
const query: Record<string,
|
|
115
|
+
const query: Record<string, unknown> = {};
|
|
108
116
|
url.searchParams.forEach((value, key) => {
|
|
109
|
-
query
|
|
117
|
+
// `vetoed` is a boolean in the op schema; query params arrive as strings.
|
|
118
|
+
// Coerce only the literal "true"/"false" forms — anything else passes
|
|
119
|
+
// through and fails validation loudly instead of silently becoming false.
|
|
120
|
+
query[key] = key === 'vetoed' && (value === 'true' || value === 'false') ? value === 'true' : value;
|
|
110
121
|
});
|
|
111
122
|
const body = await readJsonValue(req);
|
|
112
|
-
const record =
|
|
113
|
-
? body as Record<string, unknown>
|
|
114
|
-
: {};
|
|
123
|
+
const record =
|
|
124
|
+
body !== null && typeof body === 'object' && !Array.isArray(body) ? (body as Record<string, unknown>) : {};
|
|
115
125
|
return { ...query, ...record, ...params };
|
|
116
126
|
},
|
|
117
127
|
},
|
|
@@ -126,8 +136,4 @@ const intentClearOperation = defineOperation<z.infer<typeof intentClearSchema>,
|
|
|
126
136
|
},
|
|
127
137
|
});
|
|
128
138
|
|
|
129
|
-
export const intentOperations: Operation[] = [
|
|
130
|
-
intentSignalOperation,
|
|
131
|
-
intentUpdateOperation,
|
|
132
|
-
intentClearOperation,
|
|
133
|
-
];
|
|
139
|
+
export const intentOperations: Operation[] = [intentSignalOperation, intentUpdateOperation, intentClearOperation];
|