pmx-canvas 0.1.6 → 0.1.7

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 CHANGED
@@ -3,6 +3,55 @@
3
3
  All notable changes to `pmx-canvas` are documented here. This project follows
4
4
  [Semantic Versioning](https://semver.org/).
5
5
 
6
+ ## [0.1.7] - 2026-04-26
7
+
8
+ Small retest-driven follow-up to 0.1.6. Three agent-facing ergonomics:
9
+ `canvas_evaluate` now accepts top-level `await`, snapshot responses gain
10
+ a flat `id` alias for add-style consistency, and the PMX Canvas skill
11
+ documents real DOM selectors plus several quirks an agent would
12
+ otherwise have to discover by trial and error.
13
+
14
+ ### Added
15
+
16
+ - **Snapshot save responses include a flat `id` alias.** Both
17
+ `canvas_snapshot` and `POST /api/canvas/snapshots` still return the
18
+ nested `snapshot` object, and now also include `id: snapshot.id` at
19
+ the top level — same shape as every other add-style response in the
20
+ canvas API. HTTP and MCP surfaces are aligned.
21
+
22
+ ### Changed
23
+
24
+ - **`canvas_evaluate` script mode supports top-level `await`.** Both
25
+ MCP and HTTP WebView script mode wrap multi-statement bodies in an
26
+ async IIFE and serialize the resolved return value, so an agent can
27
+ write `const r = await fetch(...); return r.json();` directly without
28
+ scaffolding the wrapper itself. WebView script documentation now
29
+ describes the async behavior explicitly.
30
+ - **PMX Canvas skill docs now ship a defensive ID extractor pattern.**
31
+ The skill recommends `r.id ?? r.nodeId ?? r.snapshot?.id` so agents
32
+ pull the right id field across add-style, web-artifact, and snapshot
33
+ responses without branching per tool.
34
+ - **PMX Canvas skill docs name the real WebView CSS selectors.** The
35
+ bundled skill calls out `.canvas-node`, `.hud-layer`,
36
+ `.canvas-toolbar`, `.connection-dot`, and related classes, and is
37
+ explicit that nodes do **not** expose stable `data-node-id`
38
+ attributes — agents driving the canvas via `canvas_evaluate` no
39
+ longer have to discover selectors by trial and error.
40
+ - **PMX Canvas skill edge docs list the valid edge types.** `flow`,
41
+ `depends-on`, `relation`, `references` — same as the rest of the
42
+ surface but now explicit in the skill so the agent doesn't guess.
43
+ - **PMX Canvas skill diagram docs clarify
44
+ `canvas_add_diagram.elements`.** The field expects Excalidraw element
45
+ objects (rectangles, ellipses, arrows with bindings, labels), not
46
+ Mermaid / DOT / Graphviz source text or any other diagram DSL.
47
+
48
+ ### Internal
49
+
50
+ - Regression coverage for snapshot flat-`id` aliases on both MCP and
51
+ HTTP surfaces, plus async / top-level-`await` WebView script bodies.
52
+
53
+ [0.1.7]: https://github.com/pskoett/pmx-canvas/releases/tag/v0.1.7
54
+
6
55
  ## [0.1.6] - 2026-04-26
7
56
 
8
57
  CLI/MCP regression cleanup after the 0.1.5 coverage pass. This release tightens
@@ -50,7 +99,6 @@ drift without guessing.
50
99
  omitted from the response instead of being `undefined`. Consumers can now
51
100
  reliably use `'id' in response` to detect the build-only case. `nodeId` is
52
101
  always present and remains the canonical identifier.
53
-
54
102
  ### Fixed
55
103
 
56
104
  - **`pmx-canvas graph add` no longer starts a rogue server.** The top-level
@@ -60,6 +60,7 @@ export declare function getCanvasAutomationWebViewStatus(): CanvasAutomationWebV
60
60
  export declare function stopCanvasAutomationWebView(): Promise<boolean>;
61
61
  export declare function startCanvasAutomationWebView(url: string, options?: CanvasAutomationWebViewOptions): Promise<CanvasAutomationWebViewStatus>;
62
62
  export declare function evaluateCanvasAutomationWebView(expression: string): Promise<unknown>;
63
+ export declare function wrapCanvasAutomationScript(script: string): string;
63
64
  export declare function resizeCanvasAutomationWebView(width: number, height: number): Promise<CanvasAutomationWebViewStatus>;
64
65
  export declare function screenshotCanvasAutomationWebView(options?: Record<string, unknown>): Promise<Uint8Array>;
65
66
  export interface PrimaryWorkbenchIntent {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmx-canvas",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "Spatial canvas workbench for coding agents — infinite 2D canvas with agent-native CLI, MCP integration, nodes, edges, file watching, and snapshots",
5
5
  "type": "module",
6
6
  "main": "./src/server/index.ts",
@@ -332,6 +332,10 @@ If a node type is rejected by `canvas_add_node`, call `canvas_describe_schema` a
332
332
  - Cold builds commonly take 45-60 seconds; use a long client timeout such as 300000 ms or more
333
333
  - Returns both `id` and `nodeId` for the created artifact node when `openInCanvas` is true
334
334
 
335
+ ID extraction for mixed tool responses:
336
+ - Most add-style tools return a flat `id`; web artifacts return `id` plus `nodeId`; snapshots return `id` plus nested `snapshot.id`.
337
+ - Defensive extractor: `const getId = (r) => r.id ?? r.nodeId ?? r.snapshot?.id;`
338
+
335
339
  **`canvas_open_mcp_app`** — Open a tool-backed external MCP app node
336
340
  - Required: `toolName`, `transport`
337
341
  - `transport` is either `{ type: "stdio", command, args?, cwd?, env? }` or `{ type: "http", url, headers? }`
@@ -366,7 +370,7 @@ If a node type is rejected by `canvas_add_node`, call `canvas_describe_schema` a
366
370
  - `from`, `to` (required): source and target node IDs
367
371
  - `fromSearch`, `toSearch`: optional search-based selectors when you do not have IDs. Each search
368
372
  query must resolve to exactly one node or the edge creation fails with an ambiguity error.
369
- - `type`: edge type (default: `relation`)
373
+ - `type`: `flow`, `depends-on`, `relation`, or `references` (default: `relation`)
370
374
  - `label`: descriptive relationship label
371
375
  - `style`: `solid`, `dashed`, or `dotted`
372
376
  - `animated`: boolean for visual emphasis
@@ -465,6 +469,7 @@ Current product caveats for grouped comparison boards:
465
469
  **`canvas_redo`** — Redo the last undone mutation
466
470
  **`canvas_snapshot`** — Save a named snapshot to disk
467
471
  - `name` (required): descriptive snapshot name (e.g., "before-refactor")
472
+ - Returns `{ ok, id, snapshot }`; the flat `id` is an alias for `snapshot.id`
468
473
  **`canvas_restore`** — Restore canvas from a saved snapshot
469
474
  - `id`: snapshot to restore
470
475
  **`canvas_diff`** — Compare current canvas against a saved snapshot
@@ -502,7 +507,8 @@ tools below operate on the live canvas state.
502
507
  **`canvas_webview_stop`** — Tear down the automation session
503
508
 
504
509
  **`canvas_evaluate`** — Run JavaScript inside the workbench page and return the result
505
- - Required: `expression` (a JS expression or a function body)
510
+ - Required: exactly one of `expression` (single JS expression) or `script` (multi-statement body)
511
+ - `script` is wrapped in an async IIFE, so top-level `await` works inside script bodies
506
512
  - Useful for asserting DOM state after a sequence of canvas mutations
507
513
  - Example: read the count of rendered `.canvas-node` elements:
508
514
 
@@ -510,6 +516,20 @@ tools below operate on the live canvas state.
510
516
  canvas_evaluate({ expression: 'document.querySelectorAll(".canvas-node").length' })
511
517
  ```
512
518
 
519
+ Useful workbench selectors:
520
+ - Nodes: `.canvas-node`, `.canvas-node.active`, `.canvas-node.context-pinned`, `.canvas-node.group-node`
521
+ - Node internals: `.node-title`, `.node-titlebar`, `.node-body`, `.node-type-badge`, `.node-controls`
522
+ - Canvas chrome: `.hud-layer`, `.canvas-toolbar`, `.connection-dot`, `.canvas-bootstrap-card`
523
+ - Nodes do not expose stable `data-node-id` attributes. Use `canvas_get_layout`, `canvas_search`, or MCP resource data for exact node IDs.
524
+
525
+ Async script example:
526
+
527
+ ```typescript
528
+ canvas_evaluate({
529
+ script: 'const title = await Promise.resolve(document.title); return title;',
530
+ })
531
+ ```
532
+
513
533
  **`canvas_resize`** — Change the WebView viewport
514
534
  - Required: `width`, `height`
515
535
  - Use before `canvas_screenshot` when the human needs a specific aspect ratio
@@ -535,6 +555,7 @@ canvas_webview_stop();
535
555
  [Excalidraw MCP app](https://github.com/excalidraw/excalidraw-mcp)
536
556
  - Required: `elements` — an array of Excalidraw elements (rectangles, ellipses, diamonds, arrows,
537
557
  text). Can also be a JSON-array string.
558
+ - `elements` must be Excalidraw element objects, not Mermaid/DOT/source-text diagrams. Convert source diagrams to Excalidraw elements first or use a markdown/web-artifact node.
538
559
  - Optional: `title`, `x`, `y`, `width`, `height`
539
560
  - The diagram opens inside an `mcp-app` node with fullscreen editing and draw-on animations
540
561
  - CLI equivalent: `pmx-canvas external-app add --kind excalidraw --title "Diagram"`
package/src/cli/agent.ts CHANGED
@@ -12,7 +12,7 @@
12
12
  */
13
13
 
14
14
  import { readFileSync, writeFileSync } from 'node:fs';
15
- import { openUrlInExternalBrowser } from '../server/server.js';
15
+ import { openUrlInExternalBrowser, wrapCanvasAutomationScript } from '../server/server.js';
16
16
  import { DEFAULT_EXCALIDRAW_ELEMENTS } from '../server/diagram-presets.js';
17
17
  import {
18
18
  ALL_SEMANTIC_WATCH_EVENT_TYPES,
@@ -1856,9 +1856,9 @@ cmd('webview evaluate', 'Evaluate JavaScript in the active Bun.WebView automatio
1856
1856
  'pmx-canvas webview evaluate --file ./probe.js',
1857
1857
  );
1858
1858
  }
1859
- expression = `(() => {\n${script}\n})()`;
1859
+ expression = wrapCanvasAutomationScript(script);
1860
1860
  } else if (typeof flags.script === 'string') {
1861
- expression = `(() => {\n${flags.script}\n})()`;
1861
+ expression = wrapCanvasAutomationScript(flags.script);
1862
1862
  } else {
1863
1863
  expression = requireFlag(
1864
1864
  flags,
package/src/mcp/server.ts CHANGED
@@ -32,7 +32,7 @@ import {
32
32
  type PmxCanvas,
33
33
  } from '../server/index.js';
34
34
  import { serializeNodeForAgentContext } from '../server/agent-context.js';
35
- import { emitPrimaryWorkbenchEvent } from '../server/server.js';
35
+ import { emitPrimaryWorkbenchEvent, wrapCanvasAutomationScript } from '../server/server.js';
36
36
  import { searchNodes, buildSpatialContext, findNeighborhoods } from '../server/spatial-analysis.js';
37
37
  import { mutationHistory, diffLayouts, formatDiff } from '../server/mutation-history.js';
38
38
  import { buildCodeGraphSummary, formatCodeGraph } from '../server/code-graph.js';
@@ -899,7 +899,7 @@ export async function startMcpServer(): Promise<void> {
899
899
  'Evaluate JavaScript in the active Bun.WebView automation session for the workbench page. Use this to inspect rendered browser state. Requires an active automation session started via canvas_webview_start.',
900
900
  {
901
901
  expression: z.string().optional().describe('JavaScript expression to evaluate in the page context'),
902
- script: z.string().optional().describe('Multi-statement JavaScript body. The MCP server wraps it in an IIFE and evaluates the return value.'),
902
+ script: z.string().optional().describe('Multi-statement JavaScript body. The MCP server wraps it in an async IIFE and evaluates the resolved return value.'),
903
903
  },
904
904
  async ({ expression, script }) => {
905
905
  const c = await ensureCanvas();
@@ -910,7 +910,7 @@ export async function startMcpServer(): Promise<void> {
910
910
  };
911
911
  }
912
912
 
913
- const source = script ? `(() => {\n${script}\n})()` : expression!;
913
+ const source = script ? wrapCanvasAutomationScript(script) : expression!;
914
914
  try {
915
915
  const value = await c.evaluateAutomationWebView(source);
916
916
  return {
@@ -1401,7 +1401,7 @@ export async function startMcpServer(): Promise<void> {
1401
1401
  if (!snapshot) {
1402
1402
  return { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: 'Failed to save snapshot' }) }] };
1403
1403
  }
1404
- return { content: [{ type: 'text', text: JSON.stringify({ ok: true, snapshot }) }] };
1404
+ return { content: [{ type: 'text', text: JSON.stringify({ ok: true, id: snapshot.id, snapshot }) }] };
1405
1405
  },
1406
1406
  );
1407
1407
 
@@ -473,6 +473,10 @@ export async function evaluateCanvasAutomationWebView(expression: string): Promi
473
473
  ));
474
474
  }
475
475
 
476
+ export function wrapCanvasAutomationScript(script: string): string {
477
+ return `(async () => {\n${script}\n})()`;
478
+ }
479
+
476
480
  export async function resizeCanvasAutomationWebView(
477
481
  width: number,
478
482
  height: number,
@@ -2389,10 +2393,10 @@ async function handleWorkbenchWebViewEvaluate(req: Request): Promise<Response> {
2389
2393
  if ((expression ? 1 : 0) + (script ? 1 : 0) !== 1) {
2390
2394
  return responseJson({
2391
2395
  ok: false,
2392
- error: 'Pass exactly one of "expression" (single JS expression) or "script" (multi-statement body, wrapped in an IIFE).',
2396
+ error: 'Pass exactly one of "expression" (single JS expression) or "script" (multi-statement body, wrapped in an async IIFE).',
2393
2397
  }, 400);
2394
2398
  }
2395
- const source = script ? `(() => {\n${script}\n})()` : expression;
2399
+ const source = script ? wrapCanvasAutomationScript(script) : expression;
2396
2400
 
2397
2401
  try {
2398
2402
  const value = await evaluateCanvasAutomationWebView(source);
@@ -2899,7 +2903,7 @@ async function handleSnapshotSave(req: Request): Promise<Response> {
2899
2903
  if (!name) return responseText('Missing snapshot name', 400);
2900
2904
  const snapshot = saveCanvasSnapshot(name);
2901
2905
  if (!snapshot) return responseText('Failed to save snapshot', 500);
2902
- return responseJson({ ok: true, snapshot });
2906
+ return responseJson({ ok: true, id: snapshot.id, snapshot });
2903
2907
  }
2904
2908
 
2905
2909
  async function handleContextPinsUpdate(req: Request): Promise<Response> {