pmx-canvas 0.3.3 → 0.3.4
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 +46 -0
- package/Readme.md +2 -1
- package/dist/canvas/index.js +83 -83
- package/dist/json-render/index.js +47 -47
- package/dist/types/cli/agent.d.ts +13 -8
- package/dist/types/cli/commands/apps.d.ts +1 -0
- package/dist/types/cli/commands/ax.d.ts +1 -0
- package/dist/types/cli/commands/copilot.d.ts +1 -0
- package/dist/types/cli/commands/edges.d.ts +1 -0
- package/dist/types/cli/commands/groups.d.ts +1 -0
- package/dist/types/cli/commands/history.d.ts +1 -0
- package/dist/types/cli/commands/nodes.d.ts +1 -0
- package/dist/types/cli/commands/pins.d.ts +1 -0
- package/dist/types/cli/commands/query.d.ts +1 -0
- package/dist/types/cli/commands/view.d.ts +1 -0
- package/dist/types/cli/commands/webview.d.ts +1 -0
- package/dist/types/cli/shared.d.ts +73 -0
- package/dist/types/client/canvas/ExpandedNodeOverlay.d.ts +1 -1
- package/dist/types/client/nodes/HtmlNode.d.ts +1 -1
- package/dist/types/client/nodes/McpAppNode.d.ts +1 -1
- package/dist/types/client/nodes/use-ax-surface-bridge.d.ts +20 -0
- package/dist/types/server/canvas-operations.d.ts +2 -0
- package/dist/types/server/diagram-presets.d.ts +8 -0
- package/dist/types/server/operations/composites.d.ts +0 -9
- package/dist/types/shared/ax-surface-protocol.d.ts +18 -0
- package/docs/bun-webview-integration.md +5 -0
- package/docs/environment.md +48 -0
- package/docs/node-types.md +5 -0
- package/docs/plans/plan-009-tech-debt-backlog.md +27 -9
- package/docs/screenshot.png +0 -0
- package/package.json +8 -5
- package/skills/pmx-canvas/evals/evals.json +5 -5
- package/skills/pmx-canvas/references/full-reference.md +8 -1
- package/src/cli/agent.ts +22 -3512
- package/src/cli/commands/apps.ts +165 -0
- package/src/cli/commands/ax.ts +604 -0
- package/src/cli/commands/copilot.ts +43 -0
- package/src/cli/commands/edges.ts +73 -0
- package/src/cli/commands/groups.ts +81 -0
- package/src/cli/commands/history.ts +220 -0
- package/src/cli/commands/nodes.ts +954 -0
- package/src/cli/commands/pins.ts +42 -0
- package/src/cli/commands/query.ts +223 -0
- package/src/cli/commands/view.ts +186 -0
- package/src/cli/commands/webview.ts +227 -0
- package/src/cli/shared.ts +860 -0
- package/src/client/canvas/ExpandedNodeOverlay.tsx +3 -2
- package/src/client/nodes/ExtAppFrame.tsx +53 -49
- package/src/client/nodes/HtmlNode.tsx +8 -51
- package/src/client/nodes/McpAppNode.tsx +12 -47
- package/src/client/nodes/use-ax-surface-bridge.ts +71 -0
- package/src/json-render/renderer/index.tsx +3 -2
- package/src/mcp/server.ts +0 -47
- package/src/server/canvas-operations.ts +8 -0
- package/src/server/diagram-presets.ts +15 -1
- package/src/server/html-surface.ts +11 -6
- package/src/server/operations/composites.ts +1 -13
- package/src/server/operations/invoker.ts +10 -3
- package/src/server/operations/mcp.ts +51 -1
- package/src/server/operations/ops/app.ts +5 -0
- package/src/server/server.ts +2 -0
- package/src/shared/ax-surface-protocol.ts +22 -0
- package/dist/types/client/canvas/ContextPinHud.d.ts +0 -1
- package/src/client/canvas/ContextPinHud.tsx +0 -17
|
@@ -94,12 +94,6 @@ export interface CompositeToolDefinition {
|
|
|
94
94
|
* raise a loud error instead of silently no-op'ing.
|
|
95
95
|
*/
|
|
96
96
|
resolveOp?: (input: { kind: string; action: string }) => string | undefined;
|
|
97
|
-
/**
|
|
98
|
-
* Two-discriminator extension: human-readable `(kind, action)` for a member op
|
|
99
|
-
* (the inverse of `resolveOp`), used to build that op's deprecation note. The
|
|
100
|
-
* `kind` field-collision is resolved here (see `gateFieldRemap`).
|
|
101
|
-
*/
|
|
102
|
-
describeOp?: (opName: string) => { kind: string; action: string } | undefined;
|
|
103
97
|
/**
|
|
104
98
|
* Field-name remap applied to the composite's advertised schema and undone at
|
|
105
99
|
* dispatch. Resolves a collision between a discriminator name and a member-op
|
|
@@ -131,7 +125,7 @@ function resolveGateOp(input: { kind: string; action: string }): string | undefi
|
|
|
131
125
|
return undefined;
|
|
132
126
|
}
|
|
133
127
|
|
|
134
|
-
/** The 9 gate ops, in (kind, action) order — single source for memberOps
|
|
128
|
+
/** The 9 gate ops, in (kind, action) order — single source for memberOps. */
|
|
135
129
|
const GATE_OPS: Array<{ op: string; kind: string; action: string }> = GATE_KINDS.flatMap((kind) =>
|
|
136
130
|
GATE_ACTIONS.map((action) => {
|
|
137
131
|
const op = resolveGateOp({ kind, action });
|
|
@@ -143,11 +137,6 @@ const GATE_OPS: Array<{ op: string; kind: string; action: string }> = GATE_KINDS
|
|
|
143
137
|
}),
|
|
144
138
|
);
|
|
145
139
|
|
|
146
|
-
function describeGateOp(opName: string): { kind: string; action: string } | undefined {
|
|
147
|
-
const match = GATE_OPS.find((entry) => entry.op === opName);
|
|
148
|
-
return match ? { kind: match.kind, action: match.action } : undefined;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
140
|
export const compositeToolDefinitions: CompositeToolDefinition[] = [
|
|
152
141
|
{
|
|
153
142
|
toolName: 'canvas_node',
|
|
@@ -289,7 +278,6 @@ export const compositeToolDefinitions: CompositeToolDefinition[] = [
|
|
|
289
278
|
},
|
|
290
279
|
memberOps: GATE_OPS.map((entry) => entry.op),
|
|
291
280
|
resolveOp: resolveGateOp,
|
|
292
|
-
describeOp: describeGateOp,
|
|
293
281
|
// ax.approval.request defines its own optional `action` field (a
|
|
294
282
|
// machine-readable identifier the gate guards) — it collides with the gate
|
|
295
283
|
// lifecycle `action` discriminator. Namespace it to `approvalAction` in the
|
|
@@ -68,19 +68,26 @@ export class HttpOperationInvoker implements OperationInvoker {
|
|
|
68
68
|
const response = await fetch(url, init);
|
|
69
69
|
const text = await response.text();
|
|
70
70
|
let parsed: unknown = null;
|
|
71
|
+
let parsedAsJson = false;
|
|
71
72
|
if (text.length > 0) {
|
|
72
73
|
try {
|
|
73
74
|
parsed = JSON.parse(text) as unknown;
|
|
75
|
+
parsedAsJson = true;
|
|
74
76
|
} catch {
|
|
75
77
|
parsed = { error: text };
|
|
76
78
|
}
|
|
77
79
|
}
|
|
78
80
|
if (!response.ok) {
|
|
79
|
-
|
|
81
|
+
// errorBodyAsResult carries the route's STRUCTURED partial-failure
|
|
82
|
+
// envelope (e.g. canvas_batch { ok:false, failedIndex, ... }) through as
|
|
83
|
+
// a result. A non-JSON error body (an HTML 502 from a proxy, a foreign
|
|
84
|
+
// service) is not that envelope — fall through to the throw path so the
|
|
85
|
+
// caller still fails loudly with a nonzero exit.
|
|
86
|
+
if (route.errorBodyAsResult && parsedAsJson) return parsed;
|
|
80
87
|
const message =
|
|
81
|
-
parsed !== null && typeof parsed === 'object' && 'error' in parsed
|
|
88
|
+
parsedAsJson && parsed !== null && typeof parsed === 'object' && 'error' in parsed
|
|
82
89
|
? String((parsed as { error?: unknown }).error)
|
|
83
|
-
: `HTTP ${response.status}`;
|
|
90
|
+
: `HTTP ${response.status}${text.length > 0 ? `: ${text.slice(0, 200)}` : ''}`;
|
|
84
91
|
throw new OperationError(message, toOperationErrorStatus(response.status));
|
|
85
92
|
}
|
|
86
93
|
return parsed;
|
|
@@ -11,7 +11,7 @@ import { z, type ZodRawShape, type ZodTypeAny } from 'zod';
|
|
|
11
11
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
12
12
|
import { getOperation, listOperations } from './registry.js';
|
|
13
13
|
import type { OperationInvoker } from './invoker.js';
|
|
14
|
-
import { OperationError, type OperationMcpToolHost } from './types.js';
|
|
14
|
+
import { OperationError, type Operation, type OperationMcpToolHost } from './types.js';
|
|
15
15
|
import { compositeFoldedOpNames, compositeToolDefinitions, type CompositeToolDefinition } from './composites.js';
|
|
16
16
|
|
|
17
17
|
export interface OperationToolHost extends OperationMcpToolHost {
|
|
@@ -176,6 +176,43 @@ function stripCompositeDiscriminators(
|
|
|
176
176
|
return out;
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
+
/**
|
|
180
|
+
* Per-op cache of the MCP arg names its extraShape marks REQUIRED (a schema
|
|
181
|
+
* that rejects `undefined`). These are the standalone tools' old contracts,
|
|
182
|
+
* which buildCompositeShape flattens to all-optional — the composite handler
|
|
183
|
+
* re-enforces them at dispatch so a missing required field fails loudly and
|
|
184
|
+
* names the field, instead of failing deep in the handler or silently
|
|
185
|
+
* no-oping (the `q`-vs-`query` zero-results trap from the 0.3.1 test report).
|
|
186
|
+
*/
|
|
187
|
+
const requiredFieldsCache = new WeakMap<object, string[]>();
|
|
188
|
+
|
|
189
|
+
// Advertised input aliases that satisfy a required extraShape field: the op's
|
|
190
|
+
// handler accepts either name, so the pre-dispatch check must too. (The only
|
|
191
|
+
// such collision across all 15 composites — edges.ts advertises `edge_id` as a
|
|
192
|
+
// legacy alias for `id` and its handler reads `edge_id ?? id`.)
|
|
193
|
+
const REQUIRED_FIELD_ALIASES: Record<string, Record<string, readonly string[]>> = {
|
|
194
|
+
'edge.remove': { id: ['edge_id'] },
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
function requiredFieldSatisfied(opName: string, field: string, rest: Record<string, unknown>): boolean {
|
|
198
|
+
if (rest[field] !== undefined) return true;
|
|
199
|
+
const aliases = REQUIRED_FIELD_ALIASES[opName]?.[field] ?? [];
|
|
200
|
+
return aliases.some((alias) => rest[alias] !== undefined);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function requiredMcpFields(op: Operation): string[] {
|
|
204
|
+
const tool = op.mcp;
|
|
205
|
+
if (!tool?.extraShape) return [];
|
|
206
|
+
let keys = requiredFieldsCache.get(tool);
|
|
207
|
+
if (!keys) {
|
|
208
|
+
keys = Object.entries(tool.extraShape)
|
|
209
|
+
.filter(([, schema]) => !(schema as ZodTypeAny).safeParse(undefined).success)
|
|
210
|
+
.map(([key]) => key);
|
|
211
|
+
requiredFieldsCache.set(tool, keys);
|
|
212
|
+
}
|
|
213
|
+
return keys;
|
|
214
|
+
}
|
|
215
|
+
|
|
179
216
|
/**
|
|
180
217
|
* Register composite (action-discriminated) MCP tools (plan-006). Each action
|
|
181
218
|
* dispatches to a registered operation, reusing that op's `mcp.buildInput` and
|
|
@@ -188,6 +225,12 @@ export function registerCompositeTools(
|
|
|
188
225
|
definitions: CompositeToolDefinition[] = compositeToolDefinitions,
|
|
189
226
|
): void {
|
|
190
227
|
for (const def of definitions) {
|
|
228
|
+
// Public composite name for each op field (inverse of fieldRemap), so
|
|
229
|
+
// required-field errors name the argument the agent actually sends.
|
|
230
|
+
const publicFieldName = new Map<string, string>();
|
|
231
|
+
for (const [publicKey, opField] of Object.entries(def.fieldRemap ?? {})) {
|
|
232
|
+
publicFieldName.set(opField, publicKey);
|
|
233
|
+
}
|
|
191
234
|
server.tool(def.toolName, def.description, buildCompositeShape(def), async (input: Record<string, unknown>) => {
|
|
192
235
|
try {
|
|
193
236
|
const host = await getHost();
|
|
@@ -197,6 +240,13 @@ export function registerCompositeTools(
|
|
|
197
240
|
// and undo any field remap; the rest is the op's raw MCP args — the same
|
|
198
241
|
// value the standalone tool would receive.
|
|
199
242
|
const rest = stripCompositeDiscriminators(def, input);
|
|
243
|
+
const missing = requiredMcpFields(op).filter((field) => !requiredFieldSatisfied(opName, field, rest));
|
|
244
|
+
if (missing.length > 0) {
|
|
245
|
+
const named = missing.map((field) => `"${publicFieldName.get(field) ?? field}"`).join(', ');
|
|
246
|
+
throw new OperationError(
|
|
247
|
+
`${def.toolName} action "${String(input.action)}" requires ${named}. Unknown parameters are ignored — check the argument names.`,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
200
250
|
const opInput = op.mcp?.buildInput ? op.mcp.buildInput(rest) : rest;
|
|
201
251
|
const result = await host.invoker().invoke(opName, opInput);
|
|
202
252
|
if (op.mcp?.formatResult) {
|
|
@@ -405,6 +405,11 @@ const webArtifactOperation = defineOperation<z.infer<typeof webArtifactSchema>,
|
|
|
405
405
|
http: {
|
|
406
406
|
method: 'POST',
|
|
407
407
|
path: '/api/canvas/web-artifact',
|
|
408
|
+
// Build failures throw OperationError, served as { ok:false, error }. The
|
|
409
|
+
// CLI prints that failure envelope on STDOUT and exits 1
|
|
410
|
+
// (cli-node.test.ts:2502-2548 pins it), so the HTTP invoker must return
|
|
411
|
+
// the non-2xx body instead of throwing — mirrors canvas.batch/webview.start.
|
|
412
|
+
errorBodyAsResult: true,
|
|
408
413
|
},
|
|
409
414
|
mcp: {
|
|
410
415
|
toolName: 'canvas_build_web_artifact',
|
package/src/server/server.ts
CHANGED
|
@@ -61,6 +61,7 @@ import { resolveNodeAxCapabilities } from './ax-interaction.js';
|
|
|
61
61
|
import { normalizeCanvasTheme, type CanvasTheme } from './canvas-db.js';
|
|
62
62
|
import { validateLocalImageFile } from './image-source.js';
|
|
63
63
|
import {
|
|
64
|
+
cancelCodeGraphRecompute,
|
|
64
65
|
primeCanvasRuntimeBackends,
|
|
65
66
|
setCanvasLayoutUpdateEmitter,
|
|
66
67
|
syncCanvasRuntimeBackends,
|
|
@@ -3015,6 +3016,7 @@ export function stopCanvasServer(): void {
|
|
|
3015
3016
|
intentRegistry.reset();
|
|
3016
3017
|
canvasState.close();
|
|
3017
3018
|
closeAllMcpAppSessions();
|
|
3019
|
+
cancelCodeGraphRecompute();
|
|
3018
3020
|
setCanvasLayoutUpdateEmitter(null);
|
|
3019
3021
|
void closeCanvasAutomationWebViewInternal().catch((error) => {
|
|
3020
3022
|
logWorkbenchWarning('stopCanvasServer closeCanvasAutomationWebViewInternal', error);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire-protocol source tags for sandboxed-surface ↔ canvas postMessage traffic
|
|
3
|
+
* (plan-009 M2): one module owns the strings that used to be spread across the
|
|
4
|
+
* client node components, the server-side surface script generators, the
|
|
5
|
+
* ext-app script builders, and the json-render viewer. These are the trust
|
|
6
|
+
* boundary between opaque-origin iframes and the canvas — a typo'd tag fails
|
|
7
|
+
* SILENTLY (the listener just ignores the message), so single-siting them is a
|
|
8
|
+
* correctness measure, not cosmetics. The values are a persisted wire contract
|
|
9
|
+
* with already-generated surface documents: never change them.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** A sandboxed surface emitting an AX interaction to the parent canvas. */
|
|
13
|
+
export const AX_SURFACE_EMIT_SOURCE = 'pmx-canvas-ax';
|
|
14
|
+
|
|
15
|
+
/** The parent canvas acking an AX interaction back to the emitting surface (#55). */
|
|
16
|
+
export const AX_SURFACE_ACK_SOURCE = 'pmx-canvas-ax-ack';
|
|
17
|
+
|
|
18
|
+
/** Parent→surface pushes (theme updates, live AX state) into html/viewer iframes. */
|
|
19
|
+
export const HTML_SURFACE_PUSH_SOURCE = 'pmx-canvas-html-node';
|
|
20
|
+
|
|
21
|
+
/** Ext-app boot beacon: the iframe's scripts executed (WebKit watchdog liveness). */
|
|
22
|
+
export const EXT_APP_BOOT_BEACON_SOURCE = 'pmx-canvas-ext-app-alive';
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function ContextPinHud(): import("preact/src").JSX.Element | null;
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { clearContextPins, contextPinnedNodeIds } from '../state/canvas-store';
|
|
2
|
-
|
|
3
|
-
export function ContextPinHud() {
|
|
4
|
-
const count = contextPinnedNodeIds.value.size;
|
|
5
|
-
if (count === 0) return null;
|
|
6
|
-
|
|
7
|
-
return (
|
|
8
|
-
<div class="context-pin-hud">
|
|
9
|
-
<span class="context-pin-hud-label">
|
|
10
|
-
{'\u2726'} {count} in context
|
|
11
|
-
</span>
|
|
12
|
-
<button type="button" class="context-pin-hud-clear" onClick={clearContextPins} title="Clear all context pins">
|
|
13
|
-
×
|
|
14
|
-
</button>
|
|
15
|
-
</div>
|
|
16
|
-
);
|
|
17
|
-
}
|