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.
Files changed (64) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/Readme.md +2 -1
  3. package/dist/canvas/index.js +83 -83
  4. package/dist/json-render/index.js +47 -47
  5. package/dist/types/cli/agent.d.ts +13 -8
  6. package/dist/types/cli/commands/apps.d.ts +1 -0
  7. package/dist/types/cli/commands/ax.d.ts +1 -0
  8. package/dist/types/cli/commands/copilot.d.ts +1 -0
  9. package/dist/types/cli/commands/edges.d.ts +1 -0
  10. package/dist/types/cli/commands/groups.d.ts +1 -0
  11. package/dist/types/cli/commands/history.d.ts +1 -0
  12. package/dist/types/cli/commands/nodes.d.ts +1 -0
  13. package/dist/types/cli/commands/pins.d.ts +1 -0
  14. package/dist/types/cli/commands/query.d.ts +1 -0
  15. package/dist/types/cli/commands/view.d.ts +1 -0
  16. package/dist/types/cli/commands/webview.d.ts +1 -0
  17. package/dist/types/cli/shared.d.ts +73 -0
  18. package/dist/types/client/canvas/ExpandedNodeOverlay.d.ts +1 -1
  19. package/dist/types/client/nodes/HtmlNode.d.ts +1 -1
  20. package/dist/types/client/nodes/McpAppNode.d.ts +1 -1
  21. package/dist/types/client/nodes/use-ax-surface-bridge.d.ts +20 -0
  22. package/dist/types/server/canvas-operations.d.ts +2 -0
  23. package/dist/types/server/diagram-presets.d.ts +8 -0
  24. package/dist/types/server/operations/composites.d.ts +0 -9
  25. package/dist/types/shared/ax-surface-protocol.d.ts +18 -0
  26. package/docs/bun-webview-integration.md +5 -0
  27. package/docs/environment.md +48 -0
  28. package/docs/node-types.md +5 -0
  29. package/docs/plans/plan-009-tech-debt-backlog.md +27 -9
  30. package/docs/screenshot.png +0 -0
  31. package/package.json +8 -5
  32. package/skills/pmx-canvas/evals/evals.json +5 -5
  33. package/skills/pmx-canvas/references/full-reference.md +8 -1
  34. package/src/cli/agent.ts +22 -3512
  35. package/src/cli/commands/apps.ts +165 -0
  36. package/src/cli/commands/ax.ts +604 -0
  37. package/src/cli/commands/copilot.ts +43 -0
  38. package/src/cli/commands/edges.ts +73 -0
  39. package/src/cli/commands/groups.ts +81 -0
  40. package/src/cli/commands/history.ts +220 -0
  41. package/src/cli/commands/nodes.ts +954 -0
  42. package/src/cli/commands/pins.ts +42 -0
  43. package/src/cli/commands/query.ts +223 -0
  44. package/src/cli/commands/view.ts +186 -0
  45. package/src/cli/commands/webview.ts +227 -0
  46. package/src/cli/shared.ts +860 -0
  47. package/src/client/canvas/ExpandedNodeOverlay.tsx +3 -2
  48. package/src/client/nodes/ExtAppFrame.tsx +53 -49
  49. package/src/client/nodes/HtmlNode.tsx +8 -51
  50. package/src/client/nodes/McpAppNode.tsx +12 -47
  51. package/src/client/nodes/use-ax-surface-bridge.ts +71 -0
  52. package/src/json-render/renderer/index.tsx +3 -2
  53. package/src/mcp/server.ts +0 -47
  54. package/src/server/canvas-operations.ts +8 -0
  55. package/src/server/diagram-presets.ts +15 -1
  56. package/src/server/html-surface.ts +11 -6
  57. package/src/server/operations/composites.ts +1 -13
  58. package/src/server/operations/invoker.ts +10 -3
  59. package/src/server/operations/mcp.ts +51 -1
  60. package/src/server/operations/ops/app.ts +5 -0
  61. package/src/server/server.ts +2 -0
  62. package/src/shared/ax-surface-protocol.ts +22 -0
  63. package/dist/types/client/canvas/ContextPinHud.d.ts +0 -1
  64. 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 + describeOp. */
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
- if (route.errorBodyAsResult) return parsed;
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',
@@ -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
- }