pmx-canvas 0.2.1 → 0.2.3

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 (51) hide show
  1. package/CHANGELOG.md +119 -0
  2. package/Readme.md +2 -2
  3. package/dist/canvas/global.css +260 -0
  4. package/dist/canvas/index.js +76 -76
  5. package/dist/json-render/index.js +2 -2
  6. package/dist/types/client/canvas/IntentLayer.d.ts +1 -0
  7. package/dist/types/client/state/intent-bridge.d.ts +10 -0
  8. package/dist/types/client/state/intent-store.d.ts +25 -0
  9. package/dist/types/json-render/server.d.ts +1 -1
  10. package/dist/types/server/index.d.ts +34 -4
  11. package/dist/types/server/intent-registry.d.ts +45 -0
  12. package/dist/types/server/operations/ops/intent.d.ts +2 -0
  13. package/dist/types/shared/ax-intent.d.ts +58 -0
  14. package/docs/mcp.md +21 -2
  15. package/docs/screenshot.png +0 -0
  16. package/package.json +1 -1
  17. package/skills/pmx-canvas/SKILL.md +200 -1305
  18. package/skills/pmx-canvas/evals/evals.json +255 -1
  19. package/skills/pmx-canvas/evals/fixtures/code-exploration/src/auth/jwt.ts +17 -0
  20. package/skills/pmx-canvas/evals/fixtures/code-exploration/src/auth/login.ts +12 -0
  21. package/skills/pmx-canvas/evals/fixtures/code-exploration/src/auth/middleware.ts +13 -0
  22. package/skills/pmx-canvas/evals/fixtures/code-exploration/src/routes/auth.ts +13 -0
  23. package/skills/pmx-canvas/evals/fixtures/investigation-board/src/handlers/users.ts +27 -0
  24. package/skills/pmx-canvas/references/full-reference.md +1445 -0
  25. package/src/cli/index.ts +21 -4
  26. package/src/client/canvas/CanvasNode.tsx +13 -13
  27. package/src/client/canvas/CanvasViewport.tsx +2 -0
  28. package/src/client/canvas/ContextMenu.tsx +25 -19
  29. package/src/client/canvas/IntentLayer.tsx +278 -0
  30. package/src/client/nodes/ExtAppFrame.tsx +32 -23
  31. package/src/client/state/intent-bridge.ts +31 -0
  32. package/src/client/state/intent-store.ts +107 -0
  33. package/src/client/state/sse-bridge.ts +31 -0
  34. package/src/client/theme/global.css +260 -0
  35. package/src/json-render/charts/components.tsx +18 -4
  36. package/src/json-render/renderer/index.tsx +11 -2
  37. package/src/json-render/server.ts +1 -1
  38. package/src/server/index.ts +240 -158
  39. package/src/server/intent-registry.ts +324 -0
  40. package/src/server/operations/composites.ts +11 -0
  41. package/src/server/operations/index.ts +2 -0
  42. package/src/server/operations/ops/edges.ts +1 -0
  43. package/src/server/operations/ops/groups.ts +3 -0
  44. package/src/server/operations/ops/intent.ts +132 -0
  45. package/src/server/operations/ops/json-render.ts +3 -0
  46. package/src/server/operations/ops/nodes.ts +3 -0
  47. package/src/server/operations/ops/webview.ts +15 -4
  48. package/src/server/operations/registry.ts +68 -3
  49. package/src/server/server.ts +40 -12
  50. package/src/shared/ax-intent.ts +64 -0
  51. package/src/shared/surface.ts +5 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,123 @@ All notable changes to `pmx-canvas` are documented here. This project follows
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.2.3] - 2026-06-23
9
+
10
+ ### Fixed
11
+
12
+ - **`canvas_webview` `start` now returns parseable JSON on failure (report #66).** A start
13
+ failure (e.g. the Bun.WebView startup timeout in a headless host) surfaced the bare error
14
+ message as the MCP tool text, so a client expecting JSON choked with
15
+ `Unexpected token 'T', "Timed out "... is not valid JSON`. The failure path now returns
16
+ `{ ok:false, error, webview }` (still flagged `isError:true`), so callers can reliably
17
+ distinguish a failure/timeout from valid content.
18
+ The composite and the legacy `canvas_webview_start` share the fix.
19
+ - **Expanded ext-app clicks now wait for bridge readiness.** The iframe no longer accepts input
20
+ while reconnecting, preventing the first interaction from being lost during a frame remount.
21
+
22
+ ### Docs
23
+
24
+ - **Documented #67 (standalone graph/json-render live-resize) as a host-browser limitation.** The
25
+ `display=site` chart reflows correctly on a live window resize in a normal browser (added an
26
+ e2e regression that resizes a loaded standalone surface in both directions and asserts the SVG
27
+ tracks the viewport with no horizontal overflow). The report's stale-until-reload behavior was
28
+ observed only in the Codex single-tab in-app browser, which does not deliver live-resize events
29
+ to the page (both width *and* height froze) — no canvas code can supply a resize signal the host
30
+ never sends. Skill **Known Limitations** + the Open-as-Site reference now recommend a system
31
+ browser for separate full-page viewing.
32
+ - **Skill eval suite 15 → 17, plus fixtures and a portability fix (report skill-audit addendum).**
33
+ Added a Ghost Cursor eval (`canvas_intent` signal → linked-settle → veto rejection) and a
34
+ standalone-surface eval (`display=site` URL, ext-app-not-open-as-site, host-resize caveat).
35
+ Bundled real fixture source files under `evals/fixtures/` for the investigation-board and
36
+ code-exploration evals (which referenced files that did not exist, so a real run failed on
37
+ missing fixtures rather than skill quality), and relaxed the pinned-context eval to accept the
38
+ host-equivalent read path (`canvas_ax_state` / HTTP) like the context-pin eval.
39
+
40
+ ## [0.2.2] - 2026-06-22
41
+
42
+ ### Added
43
+
44
+ - **Ghost Cursor of Intent — a pre-commit presence layer.** An agent can announce the
45
+ spatial move it is *about* to make, so the canvas paints a faint, dashed "ghost"
46
+ placeholder *before* the real mutation lands — modeled on a multiplayer cursor
47
+ (ephemeral presence, never canvas state). An intent describes one of five moves
48
+ (`create` / `move` / `connect` / `remove` / `edit`) at a spatial anchor, with an
49
+ optional `label`, `reason` (why the agent wants it), `confidence` (0–1, drives ghost
50
+ opacity), and `seq` (staged-batch ordering). The human watching the browser can
51
+ **veto** a forming ghost (✕ on the ghost or Esc while hovered) before it happens — a
52
+ veto dissolves the ghost, queues a steering message back to the agent, and *poisons
53
+ that intent id* so the linked mutation is rejected. When the agent runs the real
54
+ mutation **linked** to the intent (by passing the returned `intent.id` as
55
+ `intentId`), the ghost **settles** into the real node automatically. Ghosts are never
56
+ persisted, never snapshotted, never in `canvas_get_layout`; they auto-expire (~8 s
57
+ default, 60 s max) and are count-capped (12 live, oldest evicted).
58
+ - **New MCP tool `canvas_intent`** (composite-only): `signal` (register/replace an
59
+ intent → `{ ok, intent }`), `update` (patch a live intent + reset its TTL), `clear`
60
+ (dissolve a ghost; `vetoed:true` for a human veto, `settledNodeId` to morph it into a
61
+ real node). The public MCP surface grows from 83 to 84 tools (15 composites).
62
+ - **New optional `intentId`** on the mutation tools/ops (`canvas_node`, `canvas_edge`,
63
+ `canvas_group`, `canvas_render`) and the matching SDK methods — passing it links the
64
+ mutation to a live ghost so it settles on success and is rejected if vetoed/expired.
65
+ Omitting it (every prior caller) is byte-identical to before.
66
+ - **New HTTP routes** `POST /api/canvas/ax/intent`, `PATCH`/`DELETE
67
+ /api/canvas/ax/intent/:id`; **new SSE frames** `ax-intent` / `ax-intent-clear`
68
+ (replayed to reconnecting browsers); **new SDK methods** `signalIntent` /
69
+ `updateIntent` / `clearIntent`. Purely additive — no existing behavior changes.
70
+
71
+ ### Fixed
72
+
73
+ - **Daemon status reports its workspace identity.** `pmx-canvas serve status` now returns the
74
+ canonical `workspace` from the live `/health` response, so agents can safely verify that a
75
+ responsive listener belongs to the intended project before mutating its board.
76
+ - **Status nodes can be removed (report #64).** Status nodes now show the standard ×
77
+ remove control in their title bar (and a "Close" context-menu item) like every other
78
+ node type, with the same undo/history behavior — they were previously the only node
79
+ type with no removal control, so temporary/test status nodes accumulated with no way
80
+ to dismiss them from the node UI.
81
+ - **Right-click "Pin" now pins to the agent context set (report #63).** The node
82
+ context menu offered the niche arrange-lock as the obvious "Pin"/"Unpin" item while
83
+ the primary human→agent context pin was buried under "Add to context", so clicking
84
+ "Pin" appeared to do nothing. The menu now lists **"Pin as context" / "Unpin from
85
+ context"** first (matching the SelectionBar wording; drives the context count +
86
+ indicator), and the arrange-lock item is renamed **"Lock position (no auto-arrange)"**
87
+ and now persists like other layout mutations.
88
+ - **Hosted MCP-app nodes no longer offer a broken "Open as site" (report #61).** A
89
+ hosted ext-app (e.g. Excalidraw) is a live MCP-app shell that only renders with the
90
+ in-canvas AppBridge host; opening it as a standalone browser tab produced
91
+ `MCP error -32601: Method not found`. Such apps are no longer openable as a PMX site
92
+ (the control is hidden and the surface route returns a clean 404) — open them
93
+ externally through their own app, or view them in the canvas. Bundled web-artifacts
94
+ and url-backed viewers still open as a site.
95
+ - **Expanded MCP-app nodes no longer clip text (report #62).** The expand/collapse
96
+ host-context update measured the iframe synchronously, before the expanded overlay had
97
+ laid out, so an app like Excalidraw reflowed bound text against the stale inline size
98
+ and clipped the start of labels. The measurement + host-context send now wait for
99
+ layout (double rAF, mirroring the post-ready nudge) so the app gets the real expanded
100
+ dimensions.
101
+ - **Graph / json-render "Open as site" fills the browser viewport (report #65).** The
102
+ standalone viewer reused the in-canvas card height, so a graph opened as a site sat in
103
+ a shallow band at the top of a large window. The "Open as site" surface now serves the
104
+ viewer in a `display=site` mode that fills the viewport (flex `100dvh`, chart height
105
+ measured from the full window, resize-responsive) — distinct from the in-canvas
106
+ content-fit grow and the expanded-overlay fill, neither of which changes.
107
+
108
+ ### Docs
109
+
110
+ - **Bundled skill audit fixes (skill-audit 2026-06-22).** `skills/pmx-canvas/SKILL.md`:
111
+ added a mandatory **workspace-identity preflight** (read `/health`, match
112
+ `workspace`, treat `responsive:true`+`pidRunning:false` as a stale listener,
113
+ isolate with an explicit `--port`) so an agent never mutates another project's canvas
114
+ via a leftover port-4313 daemon; a short **Quick Operating Path** front-door; corrected
115
+ the **MCP composite map to the current 15** (added `canvas_app` + `canvas_webview`,
116
+ `canvas_query validate`, `canvas_node`-creates-html/primitive) and reclassified the
117
+ folded standalones as deprecated→composite (removed in v0.3); and a **Known limitations
118
+ & host differences** section reflecting the #61–#65 fixes plus the remaining Codex
119
+ sandboxed-iframe automation limit. The eval suite grew from 7 to 15 cases (workspace
120
+ mismatch, current composites, board extension, verified pinning, status cleanup, AX
121
+ control surface, AX delivery, final cleanup/validate). The top-level skill is now a
122
+ 224-line operational guide; the complete long-form manual lives in
123
+ `references/full-reference.md`.
124
+
8
125
  ## [0.2.1] - 2026-06-17
9
126
 
10
127
  ### Fixed
@@ -2271,6 +2388,8 @@ otherwise have to discover by trial and error.
2271
2388
  - Regression coverage for snapshot flat-`id` aliases on both MCP and
2272
2389
  HTTP surfaces, plus async / top-level-`await` WebView script bodies.
2273
2390
 
2391
+ [0.2.3]: https://github.com/pskoett/pmx-canvas/releases/tag/v0.2.3
2392
+ [0.2.2]: https://github.com/pskoett/pmx-canvas/releases/tag/v0.2.2
2274
2393
  [0.2.1]: https://github.com/pskoett/pmx-canvas/releases/tag/v0.2.1
2275
2394
  [0.2.0]: https://github.com/pskoett/pmx-canvas/releases/tag/v0.2.0
2276
2395
  [0.1.36]: https://github.com/pskoett/pmx-canvas/releases/tag/v0.1.36
package/Readme.md CHANGED
@@ -105,7 +105,7 @@ the DB so SQLite WAL data is checkpointed into the file.
105
105
 
106
106
  ### 06 / Any agent
107
107
 
108
- Harness-agnostic. Drive the canvas from [MCP](docs/mcp.md) (83 tools,
108
+ Harness-agnostic. Drive the canvas from [MCP](docs/mcp.md) (84 tools,
109
109
  14 resources, change notifications), the [CLI](docs/cli.md), the
110
110
  [HTTP API](docs/http-api.md), or the [Bun SDK](docs/sdk.md). Works with
111
111
  Claude Code, GitHub Copilot CLI, Codex, Cursor, Windsurf, or any agent
@@ -255,7 +255,7 @@ the agent can read `canvas://skills` and pull in companion skills
255
255
  the three-tier visual matrix (json-render → html → web-artifact)
256
256
  - **[CLI reference](docs/cli.md)** — full command surface, daemon mode,
257
257
  watch streams, WebView automation
258
- - **[MCP reference](docs/mcp.md)** — 83 tools, 14 resources, change
258
+ - **[MCP reference](docs/mcp.md)** — 84 tools, 14 resources, change
259
259
  notifications, node-type routing
260
260
  - **[HTTP API](docs/http-api.md)** — REST endpoints, SSE, batch operations
261
261
  - **[AX host-adapter contract](docs/ax-host-adapter-contract.md)** — how native
@@ -3484,3 +3484,263 @@ button.welcome-hint:hover {
3484
3484
  .image-node-zoom-reset:hover {
3485
3485
  background: var(--c-surface-hover);
3486
3486
  }
3487
+
3488
+ /* ── Ghost Cursor of Intent ─────────────────────────────────────
3489
+ Pre-commit presence: faint placeholders for the move the agent is about to
3490
+ make. Lives inside the canvas world transform (positions are world coords).
3491
+ The layer sits above nodes so remove/edit overlays read on top of their
3492
+ target, but is pointer-transparent except the info card + veto control. */
3493
+ .intent-layer {
3494
+ display: contents;
3495
+ }
3496
+
3497
+ .intent-ghost {
3498
+ position: absolute;
3499
+ pointer-events: none;
3500
+ z-index: 100000;
3501
+ transition:
3502
+ left 480ms ease,
3503
+ top 480ms ease,
3504
+ width 480ms ease,
3505
+ height 480ms ease,
3506
+ opacity 180ms ease;
3507
+ }
3508
+
3509
+ /* create / move destination — a dashed ghost node */
3510
+ .intent-ghost-box {
3511
+ border: 1.5px dashed var(--c-accent);
3512
+ border-radius: 10px;
3513
+ background: var(--c-accent-10);
3514
+ box-shadow: 0 0 0 1px var(--c-accent-10), 0 6px 22px var(--c-accent-10);
3515
+ animation: intent-breathe 2.1s ease-in-out infinite;
3516
+ overflow: visible;
3517
+ }
3518
+
3519
+ .intent-ghost-titlebar {
3520
+ display: flex;
3521
+ align-items: center;
3522
+ gap: 6px;
3523
+ padding: 8px 10px;
3524
+ color: var(--c-accent);
3525
+ }
3526
+
3527
+ .intent-ghost-icon {
3528
+ display: inline-flex;
3529
+ align-items: center;
3530
+ color: var(--c-accent);
3531
+ }
3532
+
3533
+ .intent-ghost-badge {
3534
+ font-size: 11px;
3535
+ font-weight: 600;
3536
+ letter-spacing: 0.02em;
3537
+ text-transform: uppercase;
3538
+ color: var(--c-accent);
3539
+ opacity: 0.85;
3540
+ }
3541
+
3542
+ @keyframes intent-breathe {
3543
+ 0%, 100% { box-shadow: 0 0 0 1px var(--c-accent-10), 0 6px 22px var(--c-accent-10); }
3544
+ 50% { box-shadow: 0 0 0 1px var(--c-accent-25), 0 8px 30px var(--c-accent-25); }
3545
+ }
3546
+
3547
+ /* remove — a red crosshatch tombstone over the target */
3548
+ .intent-ghost-remove {
3549
+ border: 1.5px dashed var(--c-danger);
3550
+ border-radius: 10px;
3551
+ background-color: color-mix(in srgb, var(--c-danger) 8%, transparent);
3552
+ background-image: repeating-linear-gradient(
3553
+ 45deg,
3554
+ color-mix(in srgb, var(--c-danger) 22%, transparent) 0,
3555
+ color-mix(in srgb, var(--c-danger) 22%, transparent) 2px,
3556
+ transparent 2px,
3557
+ transparent 9px
3558
+ );
3559
+ }
3560
+
3561
+ /* edit — a shimmer bar over the target */
3562
+ .intent-ghost-edit {
3563
+ border: 1.5px dashed var(--c-accent);
3564
+ border-radius: 10px;
3565
+ background: var(--c-accent-10);
3566
+ overflow: visible;
3567
+ }
3568
+
3569
+ .intent-edit-bar {
3570
+ position: absolute;
3571
+ top: 0;
3572
+ left: 0;
3573
+ height: 3px;
3574
+ width: 100%;
3575
+ background: linear-gradient(90deg, transparent, var(--c-accent), transparent);
3576
+ animation: response-stream-pulse 1.5s ease-in-out infinite;
3577
+ }
3578
+
3579
+ /* connect — info card anchored at the bezier midpoint */
3580
+ .intent-ghost-connect {
3581
+ display: flex;
3582
+ justify-content: center;
3583
+ }
3584
+
3585
+ /* the info treatment: label + confidence chip, reason, seq, veto */
3586
+ .intent-info {
3587
+ position: absolute;
3588
+ top: 100%;
3589
+ left: 0;
3590
+ margin-top: 6px;
3591
+ display: flex;
3592
+ flex-direction: column;
3593
+ gap: 4px;
3594
+ pointer-events: auto;
3595
+ max-width: 280px;
3596
+ }
3597
+
3598
+ .intent-ghost-connect .intent-info {
3599
+ position: static;
3600
+ margin-top: 0;
3601
+ align-items: center;
3602
+ }
3603
+
3604
+ .intent-chip {
3605
+ display: inline-flex;
3606
+ align-items: center;
3607
+ gap: 6px;
3608
+ align-self: flex-start;
3609
+ padding: 3px 6px 3px 8px;
3610
+ border-radius: 999px;
3611
+ background: color-mix(in srgb, var(--c-panel-glass) 96%, transparent);
3612
+ backdrop-filter: blur(10px);
3613
+ border: 1px solid var(--c-accent-25);
3614
+ box-shadow: 0 4px 14px var(--c-shadow);
3615
+ color: var(--c-text);
3616
+ font-size: 12px;
3617
+ line-height: 1.2;
3618
+ }
3619
+
3620
+ .intent-seq {
3621
+ display: inline-flex;
3622
+ align-items: center;
3623
+ justify-content: center;
3624
+ min-width: 16px;
3625
+ height: 16px;
3626
+ padding: 0 4px;
3627
+ border-radius: 999px;
3628
+ background: var(--c-accent);
3629
+ color: var(--c-bg);
3630
+ font-size: 10px;
3631
+ font-weight: 700;
3632
+ }
3633
+
3634
+ .intent-chip-icon {
3635
+ display: inline-flex;
3636
+ align-items: center;
3637
+ color: var(--c-accent);
3638
+ }
3639
+
3640
+ .intent-chip-label {
3641
+ font-weight: 600;
3642
+ white-space: nowrap;
3643
+ overflow: hidden;
3644
+ text-overflow: ellipsis;
3645
+ max-width: 180px;
3646
+ }
3647
+
3648
+ .intent-confidence {
3649
+ font-size: 10px;
3650
+ font-variant-numeric: tabular-nums;
3651
+ color: var(--c-muted);
3652
+ }
3653
+
3654
+ .intent-veto {
3655
+ display: inline-flex;
3656
+ align-items: center;
3657
+ justify-content: center;
3658
+ width: 16px;
3659
+ height: 16px;
3660
+ margin-left: 2px;
3661
+ padding: 0;
3662
+ border: none;
3663
+ border-radius: 999px;
3664
+ background: transparent;
3665
+ color: var(--c-muted);
3666
+ font-size: 11px;
3667
+ cursor: pointer;
3668
+ transition: background 120ms ease, color 120ms ease;
3669
+ }
3670
+
3671
+ .intent-veto:hover {
3672
+ background: color-mix(in srgb, var(--c-danger) 18%, transparent);
3673
+ color: var(--c-danger);
3674
+ }
3675
+
3676
+ .intent-reason {
3677
+ align-self: flex-start;
3678
+ padding: 3px 8px;
3679
+ border-radius: 7px;
3680
+ background: color-mix(in srgb, var(--c-panel-glass) 92%, transparent);
3681
+ border: 1px solid var(--c-line);
3682
+ color: var(--c-muted);
3683
+ font-size: 11px;
3684
+ line-height: 1.35;
3685
+ }
3686
+
3687
+ /* connect bezier + move trail (SVG) */
3688
+ .intent-line-layer path {
3689
+ fill: none;
3690
+ }
3691
+
3692
+ .intent-line-layer {
3693
+ z-index: 99999;
3694
+ }
3695
+
3696
+ .intent-edge {
3697
+ stroke-width: 2;
3698
+ stroke-dasharray: 6 5;
3699
+ animation: intent-dash 0.9s linear infinite;
3700
+ }
3701
+
3702
+ .intent-edge.type-flow { stroke: var(--c-accent); }
3703
+ .intent-edge.type-depends-on { stroke: var(--c-warn); }
3704
+ .intent-edge.type-relation { stroke: var(--c-muted); }
3705
+ .intent-edge.type-references { stroke: var(--c-dim); }
3706
+
3707
+ .intent-trail {
3708
+ stroke: var(--c-accent);
3709
+ stroke-width: 2;
3710
+ stroke-dasharray: 5 5;
3711
+ animation: intent-dash 0.9s linear infinite;
3712
+ }
3713
+
3714
+ .intent-arrow-head {
3715
+ fill: var(--c-accent);
3716
+ }
3717
+
3718
+ @keyframes intent-dash {
3719
+ to { stroke-dashoffset: -22; }
3720
+ }
3721
+
3722
+ /* settle — the ghost becomes real, then clears */
3723
+ .intent-ghost.is-settling {
3724
+ animation: intent-settle 480ms ease forwards;
3725
+ }
3726
+
3727
+ @keyframes intent-settle {
3728
+ 0% { transform: scale(1); }
3729
+ 45% { transform: scale(1.04); border-style: solid; opacity: 1; }
3730
+ 100% { transform: scale(1); opacity: 0; }
3731
+ }
3732
+
3733
+ /* dissolve — abandoned / vetoed / expired */
3734
+ .intent-ghost.is-dissolving {
3735
+ animation: intent-dissolve 320ms ease forwards;
3736
+ }
3737
+
3738
+ @keyframes intent-dissolve {
3739
+ to { transform: scale(0.96); opacity: 0; filter: blur(2px); }
3740
+ }
3741
+
3742
+ /* keep ghosts calm while a node is being dragged */
3743
+ html.is-node-dragging .intent-ghost,
3744
+ html.is-node-dragging .intent-line-layer {
3745
+ opacity: 0.5;
3746
+ }