pmx-canvas 0.2.1 → 0.2.2

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 (45) hide show
  1. package/CHANGELOG.md +86 -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 +197 -1305
  18. package/skills/pmx-canvas/evals/evals.json +199 -0
  19. package/skills/pmx-canvas/references/full-reference.md +1441 -0
  20. package/src/cli/index.ts +21 -4
  21. package/src/client/canvas/CanvasNode.tsx +13 -13
  22. package/src/client/canvas/CanvasViewport.tsx +2 -0
  23. package/src/client/canvas/ContextMenu.tsx +25 -19
  24. package/src/client/canvas/IntentLayer.tsx +278 -0
  25. package/src/client/nodes/ExtAppFrame.tsx +31 -22
  26. package/src/client/state/intent-bridge.ts +31 -0
  27. package/src/client/state/intent-store.ts +107 -0
  28. package/src/client/state/sse-bridge.ts +31 -0
  29. package/src/client/theme/global.css +260 -0
  30. package/src/json-render/charts/components.tsx +18 -4
  31. package/src/json-render/renderer/index.tsx +11 -2
  32. package/src/json-render/server.ts +1 -1
  33. package/src/server/index.ts +240 -158
  34. package/src/server/intent-registry.ts +324 -0
  35. package/src/server/operations/composites.ts +11 -0
  36. package/src/server/operations/index.ts +2 -0
  37. package/src/server/operations/ops/edges.ts +1 -0
  38. package/src/server/operations/ops/groups.ts +3 -0
  39. package/src/server/operations/ops/intent.ts +132 -0
  40. package/src/server/operations/ops/json-render.ts +3 -0
  41. package/src/server/operations/ops/nodes.ts +3 -0
  42. package/src/server/operations/registry.ts +68 -3
  43. package/src/server/server.ts +40 -12
  44. package/src/shared/ax-intent.ts +64 -0
  45. package/src/shared/surface.ts +5 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,91 @@ All notable changes to `pmx-canvas` are documented here. This project follows
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.2.2] - 2026-06-22
9
+
10
+ ### Added
11
+
12
+ - **Ghost Cursor of Intent — a pre-commit presence layer.** An agent can announce the
13
+ spatial move it is *about* to make, so the canvas paints a faint, dashed "ghost"
14
+ placeholder *before* the real mutation lands — modeled on a multiplayer cursor
15
+ (ephemeral presence, never canvas state). An intent describes one of five moves
16
+ (`create` / `move` / `connect` / `remove` / `edit`) at a spatial anchor, with an
17
+ optional `label`, `reason` (why the agent wants it), `confidence` (0–1, drives ghost
18
+ opacity), and `seq` (staged-batch ordering). The human watching the browser can
19
+ **veto** a forming ghost (✕ on the ghost or Esc while hovered) before it happens — a
20
+ veto dissolves the ghost, queues a steering message back to the agent, and *poisons
21
+ that intent id* so the linked mutation is rejected. When the agent runs the real
22
+ mutation **linked** to the intent (by passing the returned `intent.id` as
23
+ `intentId`), the ghost **settles** into the real node automatically. Ghosts are never
24
+ persisted, never snapshotted, never in `canvas_get_layout`; they auto-expire (~8 s
25
+ default, 60 s max) and are count-capped (12 live, oldest evicted).
26
+ - **New MCP tool `canvas_intent`** (composite-only): `signal` (register/replace an
27
+ intent → `{ ok, intent }`), `update` (patch a live intent + reset its TTL), `clear`
28
+ (dissolve a ghost; `vetoed:true` for a human veto, `settledNodeId` to morph it into a
29
+ real node). The public MCP surface grows from 83 to 84 tools (15 composites).
30
+ - **New optional `intentId`** on the mutation tools/ops (`canvas_node`, `canvas_edge`,
31
+ `canvas_group`, `canvas_render`) and the matching SDK methods — passing it links the
32
+ mutation to a live ghost so it settles on success and is rejected if vetoed/expired.
33
+ Omitting it (every prior caller) is byte-identical to before.
34
+ - **New HTTP routes** `POST /api/canvas/ax/intent`, `PATCH`/`DELETE
35
+ /api/canvas/ax/intent/:id`; **new SSE frames** `ax-intent` / `ax-intent-clear`
36
+ (replayed to reconnecting browsers); **new SDK methods** `signalIntent` /
37
+ `updateIntent` / `clearIntent`. Purely additive — no existing behavior changes.
38
+
39
+ ### Fixed
40
+
41
+ - **Daemon status reports its workspace identity.** `pmx-canvas serve status` now returns the
42
+ canonical `workspace` from the live `/health` response, so agents can safely verify that a
43
+ responsive listener belongs to the intended project before mutating its board.
44
+ - **Status nodes can be removed (report #64).** Status nodes now show the standard ×
45
+ remove control in their title bar (and a "Close" context-menu item) like every other
46
+ node type, with the same undo/history behavior — they were previously the only node
47
+ type with no removal control, so temporary/test status nodes accumulated with no way
48
+ to dismiss them from the node UI.
49
+ - **Right-click "Pin" now pins to the agent context set (report #63).** The node
50
+ context menu offered the niche arrange-lock as the obvious "Pin"/"Unpin" item while
51
+ the primary human→agent context pin was buried under "Add to context", so clicking
52
+ "Pin" appeared to do nothing. The menu now lists **"Pin as context" / "Unpin from
53
+ context"** first (matching the SelectionBar wording; drives the context count +
54
+ indicator), and the arrange-lock item is renamed **"Lock position (no auto-arrange)"**
55
+ and now persists like other layout mutations.
56
+ - **Hosted MCP-app nodes no longer offer a broken "Open as site" (report #61).** A
57
+ hosted ext-app (e.g. Excalidraw) is a live MCP-app shell that only renders with the
58
+ in-canvas AppBridge host; opening it as a standalone browser tab produced
59
+ `MCP error -32601: Method not found`. Such apps are no longer openable as a PMX site
60
+ (the control is hidden and the surface route returns a clean 404) — open them
61
+ externally through their own app, or view them in the canvas. Bundled web-artifacts
62
+ and url-backed viewers still open as a site.
63
+ - **Expanded MCP-app nodes no longer clip text (report #62).** The expand/collapse
64
+ host-context update measured the iframe synchronously, before the expanded overlay had
65
+ laid out, so an app like Excalidraw reflowed bound text against the stale inline size
66
+ and clipped the start of labels. The measurement + host-context send now wait for
67
+ layout (double rAF, mirroring the post-ready nudge) so the app gets the real expanded
68
+ dimensions.
69
+ - **Graph / json-render "Open as site" fills the browser viewport (report #65).** The
70
+ standalone viewer reused the in-canvas card height, so a graph opened as a site sat in
71
+ a shallow band at the top of a large window. The "Open as site" surface now serves the
72
+ viewer in a `display=site` mode that fills the viewport (flex `100dvh`, chart height
73
+ measured from the full window, resize-responsive) — distinct from the in-canvas
74
+ content-fit grow and the expanded-overlay fill, neither of which changes.
75
+
76
+ ### Docs
77
+
78
+ - **Bundled skill audit fixes (skill-audit 2026-06-22).** `skills/pmx-canvas/SKILL.md`:
79
+ added a mandatory **workspace-identity preflight** (read `/health`, match
80
+ `workspace`, treat `responsive:true`+`pidRunning:false` as a stale listener,
81
+ isolate with an explicit `--port`) so an agent never mutates another project's canvas
82
+ via a leftover port-4313 daemon; a short **Quick Operating Path** front-door; corrected
83
+ the **MCP composite map to the current 15** (added `canvas_app` + `canvas_webview`,
84
+ `canvas_query validate`, `canvas_node`-creates-html/primitive) and reclassified the
85
+ folded standalones as deprecated→composite (removed in v0.3); and a **Known limitations
86
+ & host differences** section reflecting the #61–#65 fixes plus the remaining Codex
87
+ sandboxed-iframe automation limit. The eval suite grew from 7 to 15 cases (workspace
88
+ mismatch, current composites, board extension, verified pinning, status cleanup, AX
89
+ control surface, AX delivery, final cleanup/validate). The top-level skill is now a
90
+ 224-line operational guide; the complete long-form manual lives in
91
+ `references/full-reference.md`.
92
+
8
93
  ## [0.2.1] - 2026-06-17
9
94
 
10
95
  ### Fixed
@@ -2271,6 +2356,7 @@ otherwise have to discover by trial and error.
2271
2356
  - Regression coverage for snapshot flat-`id` aliases on both MCP and
2272
2357
  HTTP surfaces, plus async / top-level-`await` WebView script bodies.
2273
2358
 
2359
+ [0.2.2]: https://github.com/pskoett/pmx-canvas/releases/tag/v0.2.2
2274
2360
  [0.2.1]: https://github.com/pskoett/pmx-canvas/releases/tag/v0.2.1
2275
2361
  [0.2.0]: https://github.com/pskoett/pmx-canvas/releases/tag/v0.2.0
2276
2362
  [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
+ }