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
@@ -0,0 +1 @@
1
+ export declare function IntentLayer(): import("preact/src").JSX.Element | null;
@@ -2,6 +2,16 @@
2
2
  export declare function sendIntent(type: string, payload?: Record<string, unknown>): Promise<{
3
3
  ok: boolean;
4
4
  }>;
5
+ /**
6
+ * Veto a forming ghost intent at the mutation gate, then queue steering for the
7
+ * active agent session only when the server accepted the veto.
8
+ */
9
+ export declare function vetoGhostIntent(intent: {
10
+ id: string;
11
+ kind: string;
12
+ label?: string;
13
+ reason?: string;
14
+ }): Promise<boolean>;
5
15
  /** Fetch rendered markdown HTML from the server. */
6
16
  export declare function renderMarkdown(markdown: string): Promise<string>;
7
17
  /** Fetch file content from the server. */
@@ -0,0 +1,25 @@
1
+ import type { PmxAxIntent } from '../../shared/ax-intent.js';
2
+ /**
3
+ * Client-side store for the Ghost Cursor of Intent. Ghosts are ephemeral
4
+ * presence pushed over SSE (`ax-intent` / `ax-intent-clear`); this store mirrors
5
+ * them into a signal the IntentLayer renders, tracks a short exit phase so
6
+ * settle/dissolve can animate, and prunes anything the server's TTL frame did
7
+ * not reach (SSE backstop). Nothing here is ever persisted.
8
+ */
9
+ export type IntentPhase = 'forming' | 'settling' | 'dissolving';
10
+ export interface ClientIntent extends PmxAxIntent {
11
+ phase: IntentPhase;
12
+ /** The real node a settled intent became — seeds the settle morph. */
13
+ settledNodeId?: string;
14
+ }
15
+ export declare const intents: import("@preact/signals-core").Signal<Map<string, ClientIntent>>;
16
+ /** The ghost currently hovered — drives Esc-to-veto. */
17
+ export declare const hoveredIntentId: import("@preact/signals-core").Signal<string | null>;
18
+ /** A live `ax-intent` frame: (re)place the ghost in its forming state. */
19
+ export declare function upsertIntent(intent: PmxAxIntent): void;
20
+ export declare function removeIntent(id: string): void;
21
+ /** Resolve a ghost into a real node — the settle morph, then removal. */
22
+ export declare function settleIntent(id: string, settledNodeId?: string): void;
23
+ /** Dissolve a ghost (expired / vetoed / evicted / abandoned), then remove it. */
24
+ export declare function dissolveIntent(id: string): void;
25
+ export declare function resetIntents(): void;
@@ -88,7 +88,7 @@ export declare function buildJsonRenderViewerHtml(options: {
88
88
  title: string;
89
89
  spec: JsonRenderSpec;
90
90
  theme?: 'dark' | 'light' | 'high-contrast';
91
- display?: 'expanded';
91
+ display?: 'expanded' | 'site';
92
92
  devtools?: boolean;
93
93
  nodeId?: string;
94
94
  axToken?: string;
@@ -2,6 +2,7 @@ import { EventEmitter } from 'node:events';
2
2
  import { canvasState } from './canvas-state.js';
3
3
  import type { CanvasAnnotation, CanvasNodeState, CanvasEdge, CanvasLayout } from './canvas-state.js';
4
4
  import { type AxInteractionInput, type AxInteractionPublicResult } from './ax-interaction.js';
5
+ import type { PmxAxIntent } from '../shared/ax-intent.js';
5
6
  import type { PmxAxActivityKind, PmxAxApprovalGate, PmxAxCommandDescriptor, PmxAxContext, PmxAxElicitation, PmxAxEvent, PmxAxEvidence, PmxAxEvidenceKind, PmxAxFocusState, PmxAxHostCapability, PmxAxMode, PmxAxModeRequest, PmxAxPolicy, PmxAxReviewAnchorType, PmxAxReviewAnnotation, PmxAxReviewKind, PmxAxReviewRegion, PmxAxReviewSeverity, PmxAxReviewStatus, PmxAxSource, PmxAxState, PmxAxSteeringMessage, PmxAxWorkItem, PmxAxWorkItemStatus } from './ax-state.js';
6
7
  import type { AxTimelineQuery } from './canvas-db.js';
7
8
  import { searchNodes } from './spatial-analysis.js';
@@ -30,6 +31,7 @@ export declare class PmxCanvas extends EventEmitter {
30
31
  constructor(options?: {
31
32
  port?: number;
32
33
  });
34
+ private runIntentCommit;
33
35
  start(options?: {
34
36
  open?: boolean;
35
37
  automationWebView?: boolean | CanvasAutomationWebViewOptions;
@@ -47,6 +49,7 @@ export declare class PmxCanvas extends EventEmitter {
47
49
  * or keep the whole node — both work. (Previously returned a bare id string.)
48
50
  */
49
51
  addNode(input: {
52
+ intentId?: string;
50
53
  type: CanvasNodeState['type'];
51
54
  title?: string;
52
55
  content?: string;
@@ -67,6 +70,7 @@ export declare class PmxCanvas extends EventEmitter {
67
70
  strictSize?: boolean;
68
71
  }): SdkCanvasNode;
69
72
  addWebpageNode(input: {
73
+ intentId?: string;
70
74
  title?: string;
71
75
  url: string;
72
76
  x?: number;
@@ -90,8 +94,11 @@ export declare class PmxCanvas extends EventEmitter {
90
94
  }>;
91
95
  updateNode(id: string, patch: Partial<CanvasNodeState> & Record<string, unknown>): void;
92
96
  /** Remove a node. Missing id throws (plan-005 unifies this across surfaces). */
93
- removeNode(id: string): void;
97
+ removeNode(id: string, options?: {
98
+ intentId?: string;
99
+ }): void;
94
100
  addEdge(input: {
101
+ intentId?: string;
95
102
  from?: string;
96
103
  to?: string;
97
104
  fromSearch?: string;
@@ -112,6 +119,7 @@ export declare class PmxCanvas extends EventEmitter {
112
119
  * If childIds are provided, the group auto-sizes to contain them with padding.
113
120
  */
114
121
  createGroup(input: {
122
+ intentId?: string;
115
123
  title?: string;
116
124
  childIds?: string[];
117
125
  x?: number;
@@ -124,9 +132,12 @@ export declare class PmxCanvas extends EventEmitter {
124
132
  /** Add nodes to an existing group. */
125
133
  groupNodes(groupId: string, childIds: string[], options?: {
126
134
  childLayout?: 'grid' | 'column' | 'flow';
135
+ intentId?: string;
127
136
  }): boolean;
128
137
  /** Remove all children from a group (the group node remains). */
129
- ungroupNodes(groupId: string): boolean;
138
+ ungroupNodes(groupId: string, options?: {
139
+ intentId?: string;
140
+ }): boolean;
130
141
  clear(): void;
131
142
  arrange(layout?: 'grid' | 'column' | 'flow'): void;
132
143
  focusNode(id: string, options?: {
@@ -155,6 +166,18 @@ export declare class PmxCanvas extends EventEmitter {
155
166
  source?: PmxAxSource;
156
167
  }): PmxAxSteeringMessage;
157
168
  markSteeringDelivered(id: string): boolean;
169
+ /**
170
+ * Ghost Cursor of Intent — announce a spatial move before making it. The ghost
171
+ * is ephemeral presence (auto-expiring, never snapshotted); the registry emits
172
+ * the `ax-intent` SSE frame so the browser paints a pre-commit placeholder.
173
+ */
174
+ signalIntent(input: Record<string, unknown>): PmxAxIntent;
175
+ updateIntent(id: string, patch: Record<string, unknown>): PmxAxIntent;
176
+ /** Dissolve a ghost; pass `settledNodeId` once the real node has landed. */
177
+ clearIntent(id: string, options?: {
178
+ settledNodeId?: string;
179
+ vetoed?: boolean;
180
+ }): boolean;
158
181
  /** Undelivered steering for a consumer (loop-safe; excludes consumer-originated). */
159
182
  getPendingSteering(options?: {
160
183
  consumer?: string;
@@ -440,7 +463,9 @@ export declare class PmxCanvas extends EventEmitter {
440
463
  timeoutMs?: number;
441
464
  }): Promise<OpenMcpAppCoreResult>;
442
465
  addDiagram(input: DiagramPresetOpenInput): Promise<OpenMcpAppCoreResult>;
443
- addJsonRenderNode(input: JsonRenderNodeInput): {
466
+ addJsonRenderNode(input: JsonRenderNodeInput & {
467
+ intentId?: string;
468
+ }): {
444
469
  id: string;
445
470
  url: string;
446
471
  spec: JsonRenderSpec;
@@ -452,6 +477,7 @@ export declare class PmxCanvas extends EventEmitter {
452
477
  * reloads the viewer as the specVersion bumps.
453
478
  */
454
479
  streamJsonRenderNode(input: {
480
+ intentId?: string;
455
481
  nodeId?: string;
456
482
  title?: string;
457
483
  patches?: unknown[];
@@ -471,6 +497,7 @@ export declare class PmxCanvas extends EventEmitter {
471
497
  streamStatus: 'open' | 'closed';
472
498
  };
473
499
  addHtmlNode(input: {
500
+ intentId?: string;
474
501
  html: string;
475
502
  title?: string;
476
503
  summary?: string;
@@ -493,6 +520,7 @@ export declare class PmxCanvas extends EventEmitter {
493
520
  };
494
521
  }): SdkCanvasNode;
495
522
  addHtmlPrimitive(input: {
523
+ intentId?: string;
496
524
  kind: HtmlPrimitiveKind;
497
525
  title?: string;
498
526
  data?: Record<string, unknown>;
@@ -507,7 +535,9 @@ export declare class PmxCanvas extends EventEmitter {
507
535
  title: string;
508
536
  htmlBytes: number;
509
537
  };
510
- addGraphNode(input: GraphNodeInput): {
538
+ addGraphNode(input: GraphNodeInput & {
539
+ intentId?: string;
540
+ }): {
511
541
  id: string;
512
542
  url: string;
513
543
  spec: JsonRenderSpec;
@@ -0,0 +1,45 @@
1
+ import { type PmxAxIntent, type PmxAxIntentKind } from '../shared/ax-intent.js';
2
+ type IntentEmitter = (event: string, payload: Record<string, unknown>) => void;
3
+ export declare class IntentRegistry {
4
+ private readonly intents;
5
+ private readonly vetoedIntentIds;
6
+ private readonly committingIntentIds;
7
+ private emit;
8
+ private sweepTimer;
9
+ /** Inject the workbench SSE emitter (server.ts wires this at module load). */
10
+ setEmitter(emitter: IntentEmitter | null): void;
11
+ list(): PmxAxIntent[];
12
+ /** Signal a new (or replace an existing) intent. Returns the stored envelope. */
13
+ signal(raw: unknown): PmxAxIntent;
14
+ /** Patch a live intent (position/label/reason/confidence/seq) and bump its TTL. */
15
+ update(id: string, raw: unknown): PmxAxIntent;
16
+ /**
17
+ * Clear an intent. `settledNodeId` resolves it INTO a real node (the settle
18
+ * morph); `vetoed` marks a human pre-emptive veto. Either way the ghost
19
+ * dissolves. Returns true when an intent was actually removed.
20
+ */
21
+ clear(id: string, opts?: {
22
+ settledNodeId?: string;
23
+ vetoed?: boolean;
24
+ }): boolean;
25
+ /**
26
+ * Gate one real mutation behind a live, non-vetoed intent. The claim is
27
+ * synchronous: once this method has accepted the intent, a later veto cannot
28
+ * race in between the check and the mutation.
29
+ */
30
+ beginCommit(id: string, allowedKinds: readonly PmxAxIntentKind[]): PmxAxIntent;
31
+ completeCommit(id: string, settledNodeId?: string): void;
32
+ abortCommit(id: string): void;
33
+ runCommit<T>(id: string, allowedKinds: readonly PmxAxIntentKind[], mutate: () => T | Promise<T>, settledNodeId: (result: T, intent: PmxAxIntent) => string | undefined): Promise<T>;
34
+ /** Drop every live intent without per-id SSE (used on hard resets). */
35
+ reset(): void;
36
+ private rememberVeto;
37
+ private pruneVetoTombstones;
38
+ private evictOverflow;
39
+ private sweep;
40
+ private ensureSweeper;
41
+ private maybeStopSweeper;
42
+ }
43
+ /** Process-wide singleton, shared across HTTP handlers, MCP ops, and the SDK. */
44
+ export declare const intentRegistry: IntentRegistry;
45
+ export {};
@@ -0,0 +1,2 @@
1
+ import { type Operation } from '../types.js';
2
+ export declare const intentOperations: Operation[];
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Ghost Cursor of Intent — the shared pre-commit "intent" envelope.
3
+ *
4
+ * An intent is EPHEMERAL PRESENCE, not canvas state: it describes a move the
5
+ * agent is ABOUT to make (create / move / connect / remove / edit) so the canvas
6
+ * can paint a faint placeholder before the real mutation lands. Like a
7
+ * multiplayer cursor, it auto-expires, is count-capped, and never enters
8
+ * `canvas_get_layout`, `state.json`, or snapshots.
9
+ *
10
+ * This module is import-shared by the server (IntentRegistry + the intent ops)
11
+ * and the client (intent-store + IntentLayer); it must stay free of any
12
+ * server-only or client-only imports.
13
+ */
14
+ export type PmxAxIntentKind = 'create' | 'move' | 'connect' | 'remove' | 'edit';
15
+ export type PmxAxIntentEdgeType = 'relation' | 'depends-on' | 'flow' | 'references';
16
+ export interface PmxAxIntentEdge {
17
+ from: string;
18
+ to: string;
19
+ type: PmxAxIntentEdgeType;
20
+ }
21
+ export interface PmxAxIntent {
22
+ /** Stable id → update / clear / veto. Auto-generated when a signal omits it. */
23
+ id: string;
24
+ kind: PmxAxIntentKind;
25
+ /** create: where the new node forms. move: the destination. */
26
+ position?: {
27
+ x: number;
28
+ y: number;
29
+ };
30
+ /** move / edit / remove: the existing node the intent targets. */
31
+ nodeId?: string;
32
+ /** connect: the edge about to be drawn. */
33
+ edge?: PmxAxIntentEdge;
34
+ /** Node type the ghost renders (icon + type badge). Defaults to a neutral box. */
35
+ nodeType?: string;
36
+ /** Short action label shown on the ghost chip ("Add evidence"). */
37
+ label?: string;
38
+ /** WHY — shown beneath the ghost. The legibility payoff. */
39
+ reason?: string;
40
+ /** 0..1 → ghost opacity/solidity. */
41
+ confidence?: number;
42
+ /** Ordering hint for staged-batch ghosts (the numbered previsualization rail). */
43
+ seq?: number;
44
+ /** Source label of the surface that signalled the intent (mcp/api/sdk/...). */
45
+ source?: string;
46
+ /** Epoch ms when the intent was first signalled. */
47
+ createdAt: number;
48
+ /** Epoch ms when the intent auto-dissolves if not settled/cleared first. */
49
+ expiresAt: number;
50
+ }
51
+ export declare const INTENT_KINDS: PmxAxIntentKind[];
52
+ export declare const INTENT_EDGE_TYPES: PmxAxIntentEdgeType[];
53
+ /** Default lifetime of an unsettled ghost. */
54
+ export declare const DEFAULT_INTENT_TTL_MS = 8000;
55
+ /** Hard ceiling on TTL so a stuck ghost can never linger. */
56
+ export declare const MAX_INTENT_TTL_MS = 60000;
57
+ /** Live-intent cap — oldest is evicted past this (presence, not a queue). */
58
+ export declare const MAX_LIVE_INTENTS = 12;
package/docs/mcp.md CHANGED
@@ -1,11 +1,11 @@
1
1
  # MCP reference
2
2
 
3
- PMX Canvas ships an MCP stdio server with **83 tools** + **14 core resources**,
3
+ PMX Canvas ships an MCP stdio server with **84 tools** + **14 core resources**,
4
4
  plus per-skill resources at `canvas://skills/<name>`. The server emits
5
5
  `notifications/resources/updated` when canvas state changes — humans pin
6
6
  nodes in the browser, agents are notified immediately.
7
7
 
8
- > **Consolidation in progress (plan-006/008).** The 83 tools are 14 action-discriminated
8
+ > **Consolidation in progress (plan-006/008).** The 84 tools are 15 action-discriminated
9
9
  > **composites** (recommended — see below) plus 69 legacy single-purpose tools.
10
10
  > The composites fold the legacy tools behind an `action` (and, for `canvas_ax_gate`,
11
11
  > a `kind`) enum; each action dispatches to the same operation, so behavior is
@@ -51,6 +51,25 @@ its `action` to the same operation the legacy tool used, so results are identica
51
51
  | `canvas_ax_gate` | `request` · `resolve` · `await` × kind `approval` \| `elicitation` \| `mode` | `canvas_request_approval`, `canvas_resolve_approval`, `canvas_await_approval`, `canvas_request_elicitation`, `canvas_respond_elicitation`, `canvas_await_elicitation`, `canvas_request_mode`, `canvas_resolve_mode`, `canvas_await_mode` (9 → 1) |
52
52
  | `canvas_ax_timeline` | `read` · `record-event` · `add-evidence` · `send-steering` | `canvas_get_ax_timeline`, `canvas_record_ax_event`, `canvas_add_evidence`, `canvas_send_steering` |
53
53
  | `canvas_ax_delivery` | `claim` · `mark` | `canvas_claim_ax_delivery`, `canvas_mark_ax_delivery` |
54
+ | `canvas_intent` | `signal` · `update` · `clear` | _(new — Ghost Cursor of Intent; no legacy standalone tool)_ |
55
+
56
+ ### `canvas_intent` — Ghost Cursor of Intent
57
+
58
+ Announce the spatial move you are **about** to make so the canvas paints a faint
59
+ pre-commit placeholder (a "ghost"). The human sees the next move forming — and can
60
+ veto it — before the mutation lands.
61
+
62
+ - `signal` — register an intent: `kind` (`create` \| `move` \| `connect` \| `remove` \| `edit`) plus the anchor it renders against (`position` for create/move, `nodeId` for move/edit/remove, `edge` for connect). Optional `label`, `reason`, `confidence` (0..1 → ghost opacity), `seq` (staged-batch ordering), `ttlMs` (default ~8s), and a stable `id` to update/clear later.
63
+ - `update` — patch a live intent by `id` (position/label/reason/confidence/ttlMs).
64
+ - `clear` — abandon/dissolve it explicitly. Normal linked mutations settle automatically.
65
+
66
+ Intents are **ephemeral presence**: never persisted, never snapshotted, never in
67
+ `canvas_get_layout`, and auto-expiring. They ride their own SSE channel
68
+ (`ax-intent` / `ax-intent-clear`) and replay to reconnecting browsers while still
69
+ live. Best practice — narrate your next move: `signal` → mutate with the returned
70
+ `intent.id` as `intentId`. A vetoed or expired linked mutation is rejected, and a
71
+ successful mutation settles the ghost automatically. Also reachable over HTTP:
72
+ `POST/PATCH/DELETE /api/canvas/ax/intent[/:id]`.
54
73
 
55
74
  Field names match the underlying operation (e.g. `canvas_view { action: "focus", id }`,
56
75
  `canvas_group { action: "create", childIds }`). `canvas_ax_gate` has two discriminators:
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmx-canvas",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
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",