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
@@ -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;
@@ -245,6 +245,17 @@ export declare class AxStateManager {
245
245
  consumer?: string;
246
246
  limit?: number;
247
247
  }): PmxAxSteeringMessage[];
248
+ /**
249
+ * NEWEST undelivered steering first, for the compact AX context lead block (report
250
+ * #57) — so a fresh steer is visible even behind a long backlog. Loop-safe like
251
+ * getPendingSteering, but ordered DESC instead of the FIFO ASC delivery queue.
252
+ */
253
+ getPendingSteeringForContext(options?: {
254
+ consumer?: string;
255
+ limit?: number;
256
+ }): PmxAxSteeringMessage[];
257
+ /** Total undelivered steering for a consumer (loop-safe), for the context backlog counts. */
258
+ getPendingSteeringCount(consumer?: string): number;
248
259
  getAxTimelineSummary(): PmxAxTimelineSummary;
249
260
  getAxTimeline(q?: AxTimelineQuery): {
250
261
  events: PmxAxEvent[];
@@ -145,6 +145,8 @@ export interface PendingAxActivityItem {
145
145
  }
146
146
  export interface PmxAxDeliveryContext {
147
147
  pendingSteering: PmxAxSteeringMessage[];
148
+ totalPending: number;
149
+ omittedPending: number;
148
150
  pendingActivity: PendingAxActivityItem[];
149
151
  }
150
152
  export interface PmxAxContext {
@@ -53,6 +53,19 @@ export declare function loadPendingAxSteeringFromDB(db: Database, options?: {
53
53
  consumer?: string;
54
54
  limit?: number;
55
55
  }): PmxAxSteeringMessage[];
56
+ /**
57
+ * NEWEST undelivered steering first (report #57) for the compact AX context lead
58
+ * block — so a fresh steer is visible even behind a long backlog. Loop-safe: excludes
59
+ * the consumer's own steering in SQL so the LIMIT applies after loop-prevention.
60
+ * Distinct from loadPendingAxSteeringFromDB (FIFO oldest-first) which the claim/ack
61
+ * delivery queue uses for ordered processing.
62
+ */
63
+ export declare function loadNewestPendingAxSteeringFromDB(db: Database, options?: {
64
+ consumer?: string;
65
+ limit?: number;
66
+ }): PmxAxSteeringMessage[];
67
+ /** Total undelivered steering for a consumer (loop-safe — excludes the consumer's own). */
68
+ export declare function countPendingAxSteeringFromDB(db: Database, consumer?: string): number;
56
69
  export declare function loadAxTimelineSummaryFromDB(db: Database): PmxAxTimelineSummary;
57
70
  export declare function upsertAxHostCapabilityToDB(db: Database, cap: PmxAxHostCapability): void;
58
71
  export declare function loadAxHostCapabilityFromDB(db: Database): PmxAxHostCapability | null;
@@ -431,6 +431,11 @@ declare class CanvasStateManager {
431
431
  consumer?: string;
432
432
  limit?: number;
433
433
  }): PmxAxSteeringMessage[];
434
+ getPendingSteeringForContext(options?: {
435
+ consumer?: string;
436
+ limit?: number;
437
+ }): PmxAxSteeringMessage[];
438
+ getPendingSteeringCount(consumer?: string): number;
434
439
  getAxTimelineSummary(): PmxAxTimelineSummary;
435
440
  getAxTimeline(q?: AxTimelineQuery): {
436
441
  events: PmxAxEvent[];
@@ -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;
@@ -42,7 +42,25 @@ message; it does **not** wake the agent. It reaches the next turn only when:
42
42
 
43
43
  The `delivery` lead block (`GET /api/canvas/ax/context?consumer=<id>`) is the
44
44
  robustness hedge: it's compact and sits above the full dump, so an adapter can inject
45
- it un-truncated even on a busy board where the full context is clipped.
45
+ it un-truncated even on a busy board where the full context is clipped. Its
46
+ `pendingSteering` is **newest-first** (most recent at index 0), capped at 10, so a
47
+ *fresh* steer is always visible even behind a long backlog of old unacked steers
48
+ (report #57); `delivery.totalPending` / `delivery.omittedPending` tell the agent how
49
+ many more are queued so it can drain the FIFO `…/delivery/pending` endpoint when the
50
+ count is non-zero. **Adapters should read `delivery.pendingSteering`** (this compact,
51
+ count-bearing block), not `timeline.pendingSteering`.
52
+
53
+ ### Canvas-origin steering does not wake the agent by itself (#59)
54
+
55
+ Recording a browser-origin `ax.steer` (and the `ok:true` ack a surface button gets —
56
+ report #55) means the steer is **queued on the timeline**, not delivered into a live
57
+ agent turn. PMX deliberately does not import a host SDK, so the *wake* — turning a
58
+ queued steer into a visible turn — is **adapter-owned**: a cooperating host adapter
59
+ must drain `…/delivery/pending?consumer=<id>` and call its native send (e.g.
60
+ `copilotSession.send`), then `…/delivery/<id>/mark` it. Until an adapter wires that,
61
+ canvas-origin steering is delivered on the next human turn, not pushed. A steering
62
+ surface should therefore label its button honestly ("queued for the agent's next
63
+ turn"), never imply it interrupts the agent now.
46
64
 
47
65
  ## The two primitives that close the loop
48
66
 
package/docs/http-api.md CHANGED
@@ -246,6 +246,10 @@ curl "http://localhost:4313/api/canvas/ax/mode/<id>?waitMs=30000"
246
246
 
247
247
  # Context — optional ?consumer= filters the compact, loop-safe `delivery` lead block
248
248
  # (undelivered steering + open work/approvals it can act on) for per-turn injection.
249
+ # `delivery.pendingSteering` is NEWEST-first (most recent first), capped at 10, so a
250
+ # fresh steer is visible even behind a backlog; `delivery.totalPending` /
251
+ # `delivery.omittedPending` report how many more are queued. Drain the full FIFO
252
+ # (oldest-first) backlog via /api/canvas/ax/delivery/pending when omittedPending > 0.
249
253
  curl "http://localhost:4313/api/canvas/ax/context?consumer=copilot"
250
254
 
251
255
  # Commands — list the registry, invoke a command (records a `command` agent-event)
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:
@@ -157,7 +176,7 @@ Individual bundled skills are also readable at `canvas://skills/<name>`.
157
176
  |----------|-------------|
158
177
  | `canvas://pinned-context` | Content of pinned nodes + nearby unpinned neighbors |
159
178
  | `canvas://ax` | PMX AX state: focus, work items, approval gates, review annotations |
160
- | `canvas://ax-context` | Agent-readable pinned and focused AX context, plus timeline summary and host capability |
179
+ | `canvas://ax-context` | Agent-readable pinned and focused AX context, plus a compact `delivery` lead block (`pendingSteering` newest-first + `totalPending`/`omittedPending` counts), timeline summary, and host capability |
161
180
  | `canvas://ax-work` | Canvas-bound AX work: work items, approval gates, review annotations, elicitations, mode requests, and tool/prompt policy |
162
181
  | `canvas://ax-timeline` | Bounded AX timeline: recent agent-events, evidence, and steering messages |
163
182
  | `canvas://ax-pending-steering` | Undelivered steering an adapterless MCP client can claim, act on, and mark delivered |
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmx-canvas",
3
- "version": "0.2.0",
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",