pmx-canvas 0.2.0 → 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 (58) hide show
  1. package/CHANGELOG.md +124 -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/ax-state-manager.d.ts +11 -0
  11. package/dist/types/server/ax-state.d.ts +2 -0
  12. package/dist/types/server/canvas-db.d.ts +13 -0
  13. package/dist/types/server/canvas-state.d.ts +5 -0
  14. package/dist/types/server/index.d.ts +34 -4
  15. package/dist/types/server/intent-registry.d.ts +45 -0
  16. package/dist/types/server/operations/ops/intent.d.ts +2 -0
  17. package/dist/types/shared/ax-intent.d.ts +58 -0
  18. package/docs/ax-host-adapter-contract.md +19 -1
  19. package/docs/http-api.md +4 -0
  20. package/docs/mcp.md +22 -3
  21. package/docs/screenshot.png +0 -0
  22. package/package.json +1 -1
  23. package/skills/pmx-canvas/SKILL.md +197 -1283
  24. package/skills/pmx-canvas/evals/evals.json +199 -0
  25. package/skills/pmx-canvas/references/ax-html-control-surface.md +93 -0
  26. package/skills/pmx-canvas/references/full-reference.md +1441 -0
  27. package/skills/pmx-canvas/references/github-copilot-app-adapter.md +23 -7
  28. package/src/cli/index.ts +21 -4
  29. package/src/client/canvas/CanvasNode.tsx +13 -13
  30. package/src/client/canvas/CanvasViewport.tsx +2 -0
  31. package/src/client/canvas/ContextMenu.tsx +25 -19
  32. package/src/client/canvas/IntentLayer.tsx +278 -0
  33. package/src/client/nodes/ExtAppFrame.tsx +31 -22
  34. package/src/client/state/intent-bridge.ts +31 -0
  35. package/src/client/state/intent-store.ts +107 -0
  36. package/src/client/state/sse-bridge.ts +31 -0
  37. package/src/client/theme/global.css +260 -0
  38. package/src/json-render/charts/components.tsx +18 -4
  39. package/src/json-render/renderer/index.tsx +11 -2
  40. package/src/json-render/server.ts +1 -1
  41. package/src/server/ax-context.ts +8 -1
  42. package/src/server/ax-state-manager.ts +18 -0
  43. package/src/server/ax-state.ts +8 -0
  44. package/src/server/canvas-db.ts +35 -0
  45. package/src/server/canvas-state.ts +8 -0
  46. package/src/server/index.ts +240 -158
  47. package/src/server/intent-registry.ts +324 -0
  48. package/src/server/operations/composites.ts +11 -0
  49. package/src/server/operations/index.ts +2 -0
  50. package/src/server/operations/ops/edges.ts +1 -0
  51. package/src/server/operations/ops/groups.ts +3 -0
  52. package/src/server/operations/ops/intent.ts +132 -0
  53. package/src/server/operations/ops/json-render.ts +3 -0
  54. package/src/server/operations/ops/nodes.ts +3 -0
  55. package/src/server/operations/registry.ts +68 -3
  56. package/src/server/server.ts +40 -12
  57. package/src/shared/ax-intent.ts +64 -0
  58. package/src/shared/surface.ts +5 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,128 @@ 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
+
93
+ ## [0.2.1] - 2026-06-17
94
+
95
+ ### Fixed
96
+
97
+ - **Compact AX context surfaces the NEWEST steering first (report #57).** The
98
+ `delivery.pendingSteering` lead block on `GET /api/canvas/ax/context` /
99
+ `canvas://ax-context` returned the OLDEST 10 undelivered steers, so on a long-lived
100
+ board a fresh steer was buried behind old unacked ones and never appeared in the
101
+ compact block. It now returns the **newest** undelivered steering first (capped at
102
+ 10), so a fresh steer is always visible. The FIFO claim/ack delivery queue
103
+ (`/api/canvas/ax/delivery/pending`, `canvas_ax_delivery { action: "claim" }`) is
104
+ unchanged — still oldest-first for ordered processing.
105
+
106
+ ### Added
107
+
108
+ - **AX context delivery backlog counts (report #57).** `delivery` now includes
109
+ `totalPending` (undelivered steering for the consumer, loop-safe) and `omittedPending`
110
+ (`= totalPending − pendingSteering.length`), so an agent can tell the compact block
111
+ omitted a backlog and drain the full FIFO queue when `omittedPending > 0`. Additive,
112
+ non-breaking.
113
+ - **Blessed AX HTML Control Surface recipe (report #60).** New
114
+ `skills/pmx-canvas/references/ax-html-control-surface.md` (linked from `SKILL.md`): a
115
+ copy-paste-safe template (awaited `emit` + live ack display + `pmx-ax-update`
116
+ reflection) plus the three footguns that make a hand-rolled AX node look inert — the
117
+ sandboxed opaque-origin iframe throws on `localStorage`/`sessionStorage`/cookies,
118
+ `emit` must be awaited, and `ax.steer` is recorded (queued), not delivered.
119
+
120
+ ### Docs
121
+
122
+ - **Canvas-origin steering does not wake the active agent by itself (report #59).** The
123
+ host-adapter contract, `SKILL.md`, and the GitHub Copilot adapter reference now state
124
+ plainly that a browser-origin `ax.steer` (and its `ok:true` emit ack) is *queued*, not
125
+ pushed into the live session; the wake is host-adapter-owned (drain
126
+ `canvas_ax_delivery { action: "claim" }` → native send → `mark`), and a steering button
127
+ must be labeled "queued for the agent's next turn." Adapters should read steering from
128
+ the compact `delivery.pendingSteering` block, not `timeline.pendingSteering`.
129
+
8
130
  ## [0.2.0] - 2026-06-16
9
131
 
10
132
  ### Breaking
@@ -2234,6 +2356,8 @@ otherwise have to discover by trial and error.
2234
2356
  - Regression coverage for snapshot flat-`id` aliases on both MCP and
2235
2357
  HTTP surfaces, plus async / top-level-`await` WebView script bodies.
2236
2358
 
2359
+ [0.2.2]: https://github.com/pskoett/pmx-canvas/releases/tag/v0.2.2
2360
+ [0.2.1]: https://github.com/pskoett/pmx-canvas/releases/tag/v0.2.1
2237
2361
  [0.2.0]: https://github.com/pskoett/pmx-canvas/releases/tag/v0.2.0
2238
2362
  [0.1.36]: https://github.com/pskoett/pmx-canvas/releases/tag/v0.1.36
2239
2363
  [0.1.35]: https://github.com/pskoett/pmx-canvas/releases/tag/v0.1.35
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
+ }