pmx-canvas 0.4.4 → 0.4.6

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 (98) hide show
  1. package/CHANGELOG.md +108 -0
  2. package/Readme.md +52 -4
  3. package/dist/canvas/global.css +115 -11
  4. package/dist/canvas/index.js +93 -72
  5. package/dist/canvas/mermaid-entry.js +3563 -0
  6. package/dist/canvas/surface-theme.css +2 -2
  7. package/dist/json-render/index.css +1 -1
  8. package/dist/types/cli/agent.d.ts +1 -0
  9. package/dist/types/cli/commands/skills.d.ts +1 -1
  10. package/dist/types/cli/commands/smoke.d.ts +1 -0
  11. package/dist/types/client/mermaid-entry.d.ts +11 -0
  12. package/dist/types/client/nodes/DiffNode.d.ts +20 -0
  13. package/dist/types/client/nodes/ExtAppFrame.d.ts +0 -2
  14. package/dist/types/client/nodes/MermaidNode.d.ts +12 -0
  15. package/dist/types/client/state/intent-store.d.ts +6 -0
  16. package/dist/types/client/types.d.ts +2 -2
  17. package/dist/types/server/ax-state-manager.d.ts +5 -0
  18. package/dist/types/server/ax-state.d.ts +9 -3
  19. package/dist/types/server/canvas-db.d.ts +3 -2
  20. package/dist/types/server/canvas-provenance.d.ts +1 -1
  21. package/dist/types/server/canvas-state.d.ts +22 -1
  22. package/dist/types/server/canvas-validation.d.ts +34 -1
  23. package/dist/types/server/html-surface.d.ts +8 -0
  24. package/dist/types/server/index.d.ts +5 -0
  25. package/dist/types/server/operations/ops/ax-shared.d.ts +4 -1
  26. package/dist/types/server/operations/ops/nodes.d.ts +1 -1
  27. package/dist/types/server/operations/registry.d.ts +9 -1
  28. package/dist/types/server/workboard.d.ts +46 -0
  29. package/dist/types/shared/ax-intent.d.ts +6 -0
  30. package/dist/types/shared/ax-surface-protocol.d.ts +7 -0
  31. package/docs/cli.md +12 -0
  32. package/docs/environment.md +1 -0
  33. package/docs/http-api.md +12 -0
  34. package/docs/mcp.md +1 -1
  35. package/docs/node-types.md +2 -0
  36. package/docs/screenshot.png +0 -0
  37. package/docs/screenshots/amp-orb-portal.png +0 -0
  38. package/docs/screenshots/claude-code-desktop.png +0 -0
  39. package/docs/screenshots/codex-app.png +0 -0
  40. package/docs/screenshots/github-copilot-app.png +0 -0
  41. package/package.json +4 -3
  42. package/skills/pmx-canvas/SKILL.md +71 -24
  43. package/skills/pmx-canvas-orchestration/SKILL.md +82 -0
  44. package/skills/pmx-canvas-testing/SKILL.md +3 -0
  45. package/src/cli/agent.ts +2 -0
  46. package/src/cli/commands/ax.ts +28 -3
  47. package/src/cli/commands/skills.ts +1 -1
  48. package/src/cli/commands/smoke.ts +212 -0
  49. package/src/cli/index.ts +15 -2
  50. package/src/cli/shared.ts +1 -0
  51. package/src/client/canvas/CanvasNode.tsx +12 -1
  52. package/src/client/canvas/CanvasViewport.tsx +6 -0
  53. package/src/client/canvas/ExpandedNodeOverlay.tsx +10 -0
  54. package/src/client/canvas/IntentLayer.tsx +8 -6
  55. package/src/client/canvas/Minimap.tsx +2 -0
  56. package/src/client/canvas/auto-fit.ts +2 -0
  57. package/src/client/icons.tsx +4 -0
  58. package/src/client/mermaid-entry.ts +76 -0
  59. package/src/client/nodes/DiffNode.tsx +114 -0
  60. package/src/client/nodes/ExtAppFrame.tsx +259 -84
  61. package/src/client/nodes/MermaidNode.tsx +81 -0
  62. package/src/client/state/intent-bridge.ts +12 -3
  63. package/src/client/state/intent-store.ts +47 -2
  64. package/src/client/state/node-factory.ts +2 -0
  65. package/src/client/state/sse-bridge.ts +28 -0
  66. package/src/client/theme/global.css +115 -11
  67. package/src/client/theme/surface-theme.css +2 -2
  68. package/src/client/types.ts +6 -0
  69. package/src/mcp/server.ts +22 -6
  70. package/src/server/ax-interaction.ts +6 -0
  71. package/src/server/ax-state-manager.ts +27 -6
  72. package/src/server/ax-state.ts +36 -3
  73. package/src/server/canvas-db.ts +83 -60
  74. package/src/server/canvas-operations.ts +39 -9
  75. package/src/server/canvas-provenance.ts +2 -0
  76. package/src/server/canvas-schema.ts +33 -0
  77. package/src/server/canvas-state.ts +78 -6
  78. package/src/server/canvas-validation.ts +79 -0
  79. package/src/server/html-surface.ts +11 -0
  80. package/src/server/index.ts +25 -6
  81. package/src/server/intent-registry.ts +2 -0
  82. package/src/server/operations/composites.ts +3 -2
  83. package/src/server/operations/http.ts +5 -1
  84. package/src/server/operations/ops/ax-shared.ts +20 -1
  85. package/src/server/operations/ops/ax-state.ts +4 -7
  86. package/src/server/operations/ops/ax-timeline.ts +26 -3
  87. package/src/server/operations/ops/ax-work.ts +27 -1
  88. package/src/server/operations/ops/batch.ts +11 -1
  89. package/src/server/operations/ops/json-render.ts +44 -0
  90. package/src/server/operations/ops/nodes.ts +24 -2
  91. package/src/server/operations/ops/viewport.ts +8 -1
  92. package/src/server/operations/registry.ts +122 -1
  93. package/src/server/server.ts +58 -3
  94. package/src/server/workboard.ts +159 -0
  95. package/src/shared/ax-intent.ts +6 -0
  96. package/src/shared/ax-surface-protocol.ts +8 -0
  97. package/src/shared/surface.ts +2 -0
  98. package/src/shared/themes.ts +1 -1
@@ -56,7 +56,8 @@ export declare function loadPendingAxSteeringFromDB(db: Database, options?: {
56
56
  /**
57
57
  * NEWEST undelivered steering first (report #57) for the compact AX context lead
58
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.
59
+ * the consumer's own steering and steering targeted at a different consumer in SQL
60
+ * so the LIMIT applies after both filters.
60
61
  * Distinct from loadPendingAxSteeringFromDB (FIFO oldest-first) which the claim/ack
61
62
  * delivery queue uses for ordered processing.
62
63
  */
@@ -64,7 +65,7 @@ export declare function loadNewestPendingAxSteeringFromDB(db: Database, options?
64
65
  consumer?: string;
65
66
  limit?: number;
66
67
  }): PmxAxSteeringMessage[];
67
- /** Total undelivered steering for a consumer (loop-safe — excludes the consumer's own). */
68
+ /** Total undelivered steering for a consumer (loop-safe — excludes the consumer's own; target-scoped). */
68
69
  export declare function countPendingAxSteeringFromDB(db: Database, consumer?: string): number;
69
70
  export declare function loadAxTimelineSummaryFromDB(db: Database): PmxAxTimelineSummary;
70
71
  export declare function upsertAxHostCapabilityToDB(db: Database, cap: PmxAxHostCapability): void;
@@ -1,4 +1,4 @@
1
- export type CanvasNodeType = 'markdown' | 'mcp-app' | 'webpage' | 'json-render' | 'graph' | 'prompt' | 'response' | 'status' | 'context' | 'ledger' | 'trace' | 'file' | 'image' | 'html' | 'group';
1
+ export type CanvasNodeType = 'markdown' | 'mcp-app' | 'webpage' | 'json-render' | 'graph' | 'prompt' | 'response' | 'status' | 'context' | 'ledger' | 'trace' | 'file' | 'diff' | 'mermaid' | 'image' | 'html' | 'group';
2
2
  export type CanvasNodeProvenanceSourceKind = 'workspace-file' | 'webpage-url' | 'mcp-tool' | 'artifact-file' | 'image-url';
3
3
  export type CanvasNodeRefreshStrategy = 'file-watch' | 'file-read-write' | 'image-reload' | 'webpage-refresh' | 'mcp-app-rehydrate' | 'artifact-reopen';
4
4
  export interface CanvasNodeProvenance {
@@ -53,7 +53,7 @@ export interface CanvasSnapshotGcResult {
53
53
  }
54
54
  export interface CanvasNodeState {
55
55
  id: string;
56
- type: 'markdown' | 'mcp-app' | 'webpage' | 'json-render' | 'graph' | 'prompt' | 'response' | 'status' | 'context' | 'ledger' | 'trace' | 'file' | 'image' | 'html' | 'group';
56
+ type: 'markdown' | 'mcp-app' | 'webpage' | 'json-render' | 'graph' | 'prompt' | 'response' | 'status' | 'context' | 'ledger' | 'trace' | 'file' | 'diff' | 'mermaid' | 'image' | 'html' | 'group';
57
57
  position: {
58
58
  x: number;
59
59
  y: number;
@@ -154,6 +154,15 @@ declare class CanvasStateManager {
154
154
  * — e.g. the long-lived MCP subscription — may ignore the return value).
155
155
  */
156
156
  onChange(cb: (type: CanvasChangeType) => void): () => void;
157
+ private _workItemsChangedListener;
158
+ /**
159
+ * Register THE work-item change listener (single slot, last-write-wins).
160
+ * Fired after addWorkItem/updateWorkItem completes (including the node
161
+ * status mirror), so live views like the workboard can rebuild from the
162
+ * fresh work-item list.
163
+ */
164
+ setWorkItemsChangedListener(listener: (() => void) | null): void;
165
+ private notifyWorkItemsChanged;
157
166
  private notifyChange;
158
167
  private _mutationRecorder;
159
168
  private _suppressRecordingDepth;
@@ -280,6 +289,7 @@ declare class CanvasStateManager {
280
289
  status?: PmxAxWorkItemStatus;
281
290
  detail?: string | null;
282
291
  nodeIds?: string[];
292
+ agentId?: string | null;
283
293
  }, options?: {
284
294
  source?: PmxAxSource;
285
295
  }): PmxAxWorkItem;
@@ -288,9 +298,17 @@ declare class CanvasStateManager {
288
298
  status?: PmxAxWorkItemStatus;
289
299
  detail?: string | null;
290
300
  nodeIds?: string[];
301
+ agentId?: string | null;
291
302
  }, options?: {
292
303
  source?: PmxAxSource;
293
304
  }): PmxAxWorkItem | null;
305
+ /**
306
+ * Mirror a work item's status onto every linked canvas node's `data.axWorkStatus`,
307
+ * through the standard node-update path (persistence + undo/redo, same as any other
308
+ * node data change). Nodes unlinked by an updateWorkItem nodeIds change have the
309
+ * mirrored status cleared.
310
+ */
311
+ private mirrorAxWorkStatusToNodes;
294
312
  getApprovalGates(): PmxAxApprovalGate[];
295
313
  requestApproval(input: {
296
314
  title: string;
@@ -376,6 +394,7 @@ declare class CanvasStateManager {
376
394
  data?: Record<string, unknown> | null;
377
395
  }, options?: {
378
396
  source?: PmxAxSource;
397
+ agentId?: string | null;
379
398
  }): PmxAxEvent;
380
399
  addEvidence(input: {
381
400
  kind: PmxAxEvidenceKind;
@@ -389,6 +408,8 @@ declare class CanvasStateManager {
389
408
  }): PmxAxEvidence;
390
409
  recordSteeringMessage(message: string, options?: {
391
410
  source?: PmxAxSource;
411
+ agentId?: string | null;
412
+ target?: string | null;
392
413
  }): PmxAxSteeringMessage;
393
414
  markSteeringDelivered(id: string): boolean;
394
415
  ingestActivity(input: {
@@ -1,4 +1,25 @@
1
- import type { CanvasLayout } from './canvas-state.js';
1
+ import type { CanvasLayout, CanvasNodeState } from './canvas-state.js';
2
+ /**
3
+ * Per-type minimum node size (0.4.5 report follow-up): agents frequently
4
+ * create nodes with explicit frames far too small for their content — clipped
5
+ * markdown, charts squeezed behind inner scrollbars. Explicit sizes below the
6
+ * floor are clamped UP at creation (canvas-operations.ts creators);
7
+ * `strictSize: true` is the escape hatch for a genuinely small fixed frame.
8
+ * Floors sit below every type default and above the point of unreadability.
9
+ * Creation-only: later updates are untouched (the browser's drag-resize
10
+ * enforces its own 200×100 floor client-side), but `validate` reports any
11
+ * node below its floor as an advisory sizeWarning. Absent types (trace,
12
+ * group, prompt/response) are intentionally unclamped — trace is small by
13
+ * design, groups size to their children.
14
+ */
15
+ export declare const NODE_MIN_CREATE_SIZES: Partial<Record<CanvasNodeState['type'], {
16
+ width: number;
17
+ height: number;
18
+ }>>;
19
+ export declare function clampCreateNodeSize(type: CanvasNodeState['type'], width: number, height: number, strictSize?: boolean): {
20
+ width: number;
21
+ height: number;
22
+ };
2
23
  export interface CanvasValidationPair {
3
24
  aId: string;
4
25
  aTitle: string | null;
@@ -11,6 +32,15 @@ export interface CanvasContainmentIssue {
11
32
  childId: string;
12
33
  childTitle: string | null;
13
34
  }
35
+ export interface CanvasSizeWarning {
36
+ id: string;
37
+ title: string | null;
38
+ type: CanvasNodeState['type'];
39
+ width: number;
40
+ height: number;
41
+ minWidth: number;
42
+ minHeight: number;
43
+ }
14
44
  export interface CanvasValidationResult {
15
45
  ok: boolean;
16
46
  collisions: CanvasValidationPair[];
@@ -21,6 +51,8 @@ export interface CanvasValidationResult {
21
51
  from: string;
22
52
  to: string;
23
53
  }>;
54
+ /** Nodes below their type's readable minimum (advisory — does not fail `ok`). */
55
+ sizeWarnings: CanvasSizeWarning[];
24
56
  summary: {
25
57
  nodes: number;
26
58
  edges: number;
@@ -28,6 +60,7 @@ export interface CanvasValidationResult {
28
60
  containments: number;
29
61
  containmentViolations: number;
30
62
  missingEdgeEndpoints: number;
63
+ sizeWarnings: number;
31
64
  };
32
65
  }
33
66
  export declare function validateCanvasLayout(layout: CanvasLayout): CanvasValidationResult;
@@ -53,6 +53,14 @@ export declare function buildAxStateBridge(axToken: string, snapshotJson: string
53
53
  * and the json-render injection site stay byte-identical (no drift).
54
54
  */
55
55
  export declare function buildContentHeightReporter(frameToken: string): string;
56
+ /**
57
+ * Author-HTML fragment for a `mermaid` node surface: the HTML-escaped diagram
58
+ * source in a hidden `<pre>` the entry script reads back via textContent, plus
59
+ * the separately-built renderer bundle (dist/canvas/mermaid-entry.js — kept out
60
+ * of the main SPA bundle). Escaping is mandatory: the source is arbitrary node
61
+ * data and must not be parsed as markup.
62
+ */
63
+ export declare function buildMermaidSurfaceHtml(source: string): string;
56
64
  export interface HtmlSurfaceOptions {
57
65
  theme: SurfaceTheme;
58
66
  /**
@@ -161,9 +161,12 @@ export declare class PmxCanvas extends EventEmitter {
161
161
  data?: Record<string, unknown> | null;
162
162
  }, options?: {
163
163
  source?: PmxAxSource;
164
+ agentId?: string | null;
164
165
  }): PmxAxEvent;
165
166
  sendSteering(message: string, options?: {
166
167
  source?: PmxAxSource;
168
+ agentId?: string | null;
169
+ target?: string | null;
167
170
  }): PmxAxSteeringMessage;
168
171
  markSteeringDelivered(id: string): boolean;
169
172
  /**
@@ -198,6 +201,7 @@ export declare class PmxCanvas extends EventEmitter {
198
201
  status?: PmxAxWorkItemStatus;
199
202
  detail?: string | null;
200
203
  nodeIds?: string[];
204
+ agentId?: string | null;
201
205
  }, options?: {
202
206
  source?: PmxAxSource;
203
207
  }): PmxAxWorkItem;
@@ -206,6 +210,7 @@ export declare class PmxCanvas extends EventEmitter {
206
210
  status?: PmxAxWorkItemStatus;
207
211
  detail?: string | null;
208
212
  nodeIds?: string[];
213
+ agentId?: string | null;
209
214
  }, options?: {
210
215
  source?: PmxAxSource;
211
216
  }): PmxAxWorkItem | null;
@@ -11,9 +11,10 @@
11
11
  import { z } from 'zod';
12
12
  import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
13
13
  import type { PmxAxSource } from '../../ax-state.js';
14
- export declare const AX_SOURCES: readonly ["agent", "api", "browser", "cli", "codex", "copilot", "mcp", "sdk", "system"];
14
+ export declare const AX_SOURCES: readonly ["agent", "amp", "api", "browser", "cli", "codex", "copilot", "mcp", "sdk", "system"];
15
15
  /** Zod schema for the optional `source` field shared by AX MCP tool shapes. */
16
16
  export declare const AX_SOURCE_SHAPE: z.ZodOptional<z.ZodEnum<{
17
+ amp: "amp";
17
18
  agent: "agent";
18
19
  api: "api";
19
20
  browser: "browser";
@@ -24,6 +25,8 @@ export declare const AX_SOURCE_SHAPE: z.ZodOptional<z.ZodEnum<{
24
25
  sdk: "sdk";
25
26
  system: "system";
26
27
  }>>;
28
+ /** Zod schema for the optional `agentId` field shared by AX MCP tool shapes. */
29
+ export declare const AX_AGENT_ID_SHAPE: z.ZodOptional<z.ZodString>;
27
30
  /** An absent or unrecognized source falls back to the per-surface default. */
28
31
  export declare function normalizeAxSource(value: unknown, fallback: PmxAxSource): PmxAxSource;
29
32
  export declare function normalizeAxNodeIds(value: unknown): string[];
@@ -1,6 +1,6 @@
1
1
  import { type CanvasLayout, type CanvasNodeState } from '../../canvas-state.js';
2
2
  import { type Operation } from '../types.js';
3
- export declare const NODE_TYPES: readonly ["markdown", "status", "context", "ledger", "trace", "file", "image", "mcp-app", "webpage", "html", "group"];
3
+ export declare const NODE_TYPES: readonly ["markdown", "status", "context", "ledger", "trace", "file", "diff", "mermaid", "image", "mcp-app", "webpage", "html", "group"];
4
4
  /** Per-type default node frame size (formerly copy-pasted ladders). */
5
5
  export declare function defaultNodeSize(type: string): {
6
6
  width: number;
@@ -11,5 +11,13 @@ export declare function setOperationEventEmitter(emitter: OperationEventEmitter
11
11
  export declare function isEmitSuppressed(): boolean;
12
12
  /** Run `fn` with all operation SSE emits suppressed; restores depth on finally. */
13
13
  export declare function runWithSuppressedEmits<T>(fn: () => Promise<T>): Promise<T>;
14
- export declare function executeOperation(name: string, rawInput: unknown): Promise<unknown>;
14
+ export interface ExecuteOperationMeta {
15
+ /**
16
+ * Skip the synthesized auto-ghost: set by the workbench's own HTTP calls
17
+ * (a human dragging a node is not agent activity) and by canvas.batch for
18
+ * its inner dispatches (batch churn is exempt, matching the skill contract).
19
+ */
20
+ suppressAutoGhost?: boolean;
21
+ }
22
+ export declare function executeOperation(name: string, rawInput: unknown, meta?: ExecuteOperationMeta): Promise<unknown>;
15
23
  export {};
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Live AX work-item board, materialized as a json-render node.
3
+ *
4
+ * `buildWorkboardSpec` is the pure spec builder (status columns of work-item
5
+ * cards); `renderWorkboard` is the operation core (create-or-rebuild the
6
+ * single `data.workboard === true` json-render node); `refreshWorkboardNodes`
7
+ * is the live-refresh hook this module registers on `canvasState` so every
8
+ * addWorkItem/updateWorkItem rebuilds the board from the fresh work-item list
9
+ * through the SAME spec/update path the operation uses.
10
+ *
11
+ * The refresh runs inside `withSuppressedRecording` so live board rebuilds do
12
+ * not spam undo/redo history (persistence and SSE emits are unaffected).
13
+ */
14
+ import type { JsonRenderSpec } from '../json-render/server.js';
15
+ import type { PmxAxWorkItem } from './ax-state.js';
16
+ export declare const WORKBOARD_NODE_TITLE = "Work Board";
17
+ export declare const WORKBOARD_NODE_SIZE: {
18
+ width: number;
19
+ height: number;
20
+ };
21
+ /**
22
+ * Build the workboard json-render spec from a work-item list: one column per
23
+ * status (todo → in-progress → blocked → done → cancelled; empty statuses are
24
+ * omitted), each item a Card with an agentId Badge and detail Text when
25
+ * present. An empty list renders a single muted "No work items" text block.
26
+ */
27
+ export declare function buildWorkboardSpec(workItems: PmxAxWorkItem[]): JsonRenderSpec;
28
+ /**
29
+ * Operation core for `render.workboard`: rebuild the existing workboard node
30
+ * in place, or create one (tagged `data.workboard: true`) when none exists.
31
+ */
32
+ export declare function renderWorkboard(input?: {
33
+ x?: number;
34
+ y?: number;
35
+ }): {
36
+ ok: true;
37
+ id: string;
38
+ created: boolean;
39
+ itemCount: number;
40
+ };
41
+ /**
42
+ * Live refresh: rebuild every workboard node's spec from the current work-item
43
+ * list. Registered below as the canvasState work-item change listener. Node
44
+ * updates never touch work items, so this cannot recurse.
45
+ */
46
+ export declare function refreshWorkboardNodes(): void;
@@ -43,6 +43,12 @@ export interface PmxAxIntent {
43
43
  seq?: number;
44
44
  /** Source label of the surface that signalled the intent (mcp/api/sdk/...). */
45
45
  source?: string;
46
+ /**
47
+ * Server-synthesized ghost for an agent mutation that arrived WITHOUT an
48
+ * explicit canvas_intent signal (auto-ghost). Rendered visually lighter than
49
+ * a deliberate signal, and not vetoable — it settles immediately.
50
+ */
51
+ auto?: boolean;
46
52
  /** Epoch ms when the intent was first signalled. */
47
53
  createdAt: number;
48
54
  /** Epoch ms when the intent auto-dissolves if not settled/cleared first. */
@@ -16,3 +16,10 @@ export declare const AX_SURFACE_ACK_SOURCE = "pmx-canvas-ax-ack";
16
16
  export declare const HTML_SURFACE_PUSH_SOURCE = "pmx-canvas-html-node";
17
17
  /** Ext-app boot beacon: the iframe's scripts executed (WebKit watchdog liveness). */
18
18
  export declare const EXT_APP_BOOT_BEACON_SOURCE = "pmx-canvas-ext-app-alive";
19
+ /**
20
+ * Parent→ext-app paint probe (Finding N): the parent asks the app document to
21
+ * answer with a double-rAF `paint-tick` beacon. A frame whose rendering
22
+ * pipeline is live answers within a frame or two; silence past the timeout is
23
+ * the paint-fail signal that drives the recovery ladder.
24
+ */
25
+ export declare const EXT_APP_PAINT_PROBE_SOURCE = "pmx-canvas-ext-app-paint-probe";
package/docs/cli.md CHANGED
@@ -201,6 +201,18 @@ pmx-canvas ax policy get
201
201
  pmx-canvas ax policy set --excluded-tools shell,write --mode concise
202
202
  ```
203
203
 
204
+ ## Environment smoke check
205
+
206
+ ```bash
207
+ pmx-canvas smoke # one-command check of a running canvas
208
+ pmx-canvas smoke --port 4750 # target a specific daemon
209
+ ```
210
+
211
+ Verifies server health + workspace, CLI/server version skew, the MCP
212
+ initialize handshake, a temporary node create/search/remove round-trip (the
213
+ temp node is always removed, including on failure), and board validation.
214
+ Prints a JSON report and exits 1 if any check fails.
215
+
204
216
  ## Copilot adapter
205
217
 
206
218
  Install the bundled GitHub Copilot extension adapter into a repo. The adapter
@@ -27,6 +27,7 @@ optional; defaults are listed.
27
27
  |---|---|---|
28
28
  | `PMX_CANVAS_THEME` | server | Startup theme: `dark` (default), `light`, `high-contrast`, `midnight`, `sepia`, `arctic`, `ember`, `forest`, `volt`. |
29
29
  | `AMP_ORB` | server | Set by Amp orb services. When present, the served canvas page tells the browser it is running in an orb, which skips the iframe embed probe, forces `srcdoc` surface rendering when embedded (the orb portal blocks `src`-URL child iframes and the probe is unreliable there), and defaults the event transport to polling instead of SSE (the portal proxy buffers streams; `?transport=sse` overrides for diagnosis). |
30
+ | `PORT` | server | Portal-assigned service port, honored ONLY when `AMP_ORB` is also set (Amp orbs) — after `--port`, `PMX_WEB_CANVAS_PORT`, and `PMX_CANVAS_PORT`, before the 4313 default. A stray `PORT` in normal shells never changes the port. |
30
31
  | `PMX_CANVAS_DISABLE_BROWSER_OPEN` | server | `1` suppresses auto-opening the browser (used by tests/CI). |
31
32
  | `PMX_CANVAS_DIST` | server | Explicit client bundle directory to serve instead of the packaged `dist/canvas`. |
32
33
  | `PMX_CANVAS_WEBVIEW_TIMEOUT_MS` | server | Startup timeout for the Bun.WebView automation session. |
package/docs/http-api.md CHANGED
@@ -306,6 +306,18 @@ curl -X DELETE "http://localhost:4313/api/canvas/ax/intent/<id>?settledNodeId=no
306
306
  curl -X DELETE "http://localhost:4313/api/canvas/ax/intent/<id>?vetoed=true"
307
307
  ```
308
308
 
309
+ ## Live work board
310
+
311
+ ```bash
312
+ # Materialize (or refresh) the one board node showing all AX work items by status.
313
+ curl -X POST http://localhost:4313/api/canvas/workboard \
314
+ -H 'Content-Type: application/json' -d '{}'
315
+ ```
316
+
317
+ The board is a `json-render` node tagged `data.workboard: true`. Repeat calls
318
+ are idempotent (`created: false`) and the board refreshes itself whenever a
319
+ work item changes — no polling needed.
320
+
309
321
  ## Diagrams (Excalidraw preset)
310
322
 
311
323
  ```bash
package/docs/mcp.md CHANGED
@@ -38,7 +38,7 @@ its `action` to the same operation the legacy tool used, so results are identica
38
38
  | Composite | `action` values | Replaced (removed in v0.3.0) |
39
39
  |-----------|-----------------|----------|
40
40
  | `canvas_node` | `add` · `get` · `update` · `remove` | `canvas_add_node`, `canvas_get_node`, `canvas_update_node`, `canvas_remove_node`, `canvas_add_html_node` (`add` + `type:"html"`), `canvas_add_html_primitive` (`add` + `type:"html"`, `primitive:"<kind>"`), `canvas_refresh_webpage_node` (`update` + `refresh:true`) |
41
- | `canvas_render` | `describe-schema` · `validate` · `add-json-render` · `stream-json-render` · `add-graph` | `canvas_describe_schema`, `canvas_validate_spec`, `canvas_add_json_render_node`, `canvas_stream_json_render_node`, `canvas_add_graph_node` |
41
+ | `canvas_render` | `describe-schema` · `validate` · `add-json-render` · `stream-json-render` · `add-graph` · `workboard` | `canvas_describe_schema`, `canvas_validate_spec`, `canvas_add_json_render_node`, `canvas_stream_json_render_node`, `canvas_add_graph_node` |
42
42
  | `canvas_edge` | `add` · `remove` | `canvas_add_edge`, `canvas_remove_edge` |
43
43
  | `canvas_group` | `create` · `add` · `ungroup` | `canvas_create_group`, `canvas_group_nodes`, `canvas_ungroup` |
44
44
  | `canvas_history` | `undo` · `redo` | `canvas_undo`, `canvas_redo` |
@@ -15,6 +15,8 @@ see [MCP tools](mcp.md), [HTTP API](http-api.md), and [SDK](sdk.md).
15
15
  | `ledger` | Execution ledger summary |
16
16
  | `trace` | Agent trace pills (tool calls, subagent activity) |
17
17
  | `file` | Live file viewer with auto-update on disk changes |
18
+ | `diff` | Unified diff viewer: per-file headers, hunk rows, +/− colored lines |
19
+ | `mermaid` | Mermaid diagram rendered client-side from text (flowchart, sequence, state, …) |
18
20
  | `image` | Image viewer (file paths, data URIs, URLs) |
19
21
  | `webpage` | Persisted webpage snapshot with stored URL, extracted text, refresh |
20
22
  | `mcp-app` | Tool-backed hosted MCP App iframes (Excalidraw, etc.) |
Binary file
Binary file
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pmx-canvas",
3
- "version": "0.4.4",
4
- "description": "Spatial canvas workbench for coding agents \u2014 infinite 2D canvas with agent-native CLI, MCP integration, nodes, edges, file watching, and snapshots",
3
+ "version": "0.4.6",
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",
7
7
  "module": "./src/server/index.ts",
@@ -32,7 +32,7 @@
32
32
  "dev:demo": "bun run src/cli/index.ts --demo",
33
33
  "dev:portless": "portless run --name pmx --app-port 4313 bun run src/cli/index.ts --no-open --port=4313",
34
34
  "dev:portless:demo": "portless run --name pmx --app-port 4313 bun run src/cli/index.ts --no-open --port=4313 --demo",
35
- "build:client": "bun build src/client/index.tsx --outdir dist/canvas --minify && cp src/client/theme/global.css dist/canvas/global.css && cp src/client/theme/surface-theme.css dist/canvas/surface-theme.css",
35
+ "build:client": "bun build src/client/index.tsx --outdir dist/canvas --minify && bun build src/client/mermaid-entry.ts --outdir dist/canvas --minify && cp src/client/theme/global.css dist/canvas/global.css && cp src/client/theme/surface-theme.css dist/canvas/surface-theme.css",
36
36
  "build:json-render": "bash scripts/build-json-render.sh",
37
37
  "build:types": "tsc -p tsconfig.types.json",
38
38
  "build": "bun run build:client && bun run build:json-render && bun run build:types",
@@ -68,6 +68,7 @@
68
68
  "@modelcontextprotocol/sdk": "^1.0.0",
69
69
  "@preact/signals": "^2.0.0",
70
70
  "marked": "^15.0.0",
71
+ "mermaid": "^11.16.1",
71
72
  "preact": "^10.25.0",
72
73
  "react": "^19.2.3",
73
74
  "react-dom": "^19.2.3",
@@ -44,9 +44,18 @@ Humans curate agent context by pinning nodes; agents read that curation through
44
44
  human watches intent form and can veto mid-thought. Skip it only for trivial in-place tweaks or
45
45
  high-frequency batch churn. The default TTL (~8s) expires between agent turns: signal with
46
46
  `ttlMs: 30000` and settle by passing `intentId` on the mutation in the same or next call.
47
+ Since 0.4.5, an agent mutation WITHOUT a signal still shows a server-synthesized **auto-ghost**
48
+ (rendered lighter — dimmer, dotted, no veto — and settled instantly). That is a visibility
49
+ floor, not a replacement: only an explicit signal gives the human a real pre-mutation veto
50
+ window, your reasoning (`reason`), and staged multi-step previews. Batch and browser-human
51
+ actions never auto-ghost.
47
52
  6. **Mutate through current composites.** Prefer the 16 composite MCP tools below.
48
53
  7. **Arrange and validate.** After batch changes, use `canvas_view { action: "arrange" }` when
49
54
  appropriate and always finish with `canvas_query { action: "validate" }`.
55
+ 7b. **Show the human.** After creating user-facing output, bring the camera to it: a single node
56
+ gets `canvas_view { action: "focus", id }` (pans by default), a small cluster gets
57
+ `canvas_view { action: "fit", nodeIds: [...] }` with exactly the new ids. See
58
+ **In-View Placement & Sizing** below — auto-placement is board-relative, not camera-relative.
50
59
  8. **Verify context pins.** Pin with `canvas_pin_nodes` or the browser's **Pin as context**, then
51
60
  read `canvas://pinned-context`.
52
61
  9. **Clean up temporary nodes.** Remove retry/test fixtures and restore the baseline snapshot when
@@ -62,6 +71,10 @@ pmx-canvas serve status
62
71
  ```
63
72
 
64
73
  Both surfaces report `workspace`. It must match the intended workspace root.
74
+ For a full environment check in one command (health + workspace, CLI/server
75
+ version skew, MCP initialize handshake, temp-node create/search/remove
76
+ round-trip, board validation), run `pmx-canvas smoke` (0.4.6+) — JSON report,
77
+ exit 1 on failure.
65
78
 
66
79
  - If `responsive: true` but `pidRunning: false`, treat the listener as potentially stale.
67
80
  - On mismatch, do not mutate. Start the intended workspace on an explicit free port:
@@ -92,14 +105,17 @@ Both surfaces report `workspace`. It must match the intended workspace root.
92
105
  | Persistent context cards | `context` via `canvas_node` |
93
106
  | Event/check stream | `ledger` or `trace` via `canvas_node` |
94
107
  | Local source with live updates | `file` via `canvas_node` |
108
+ | Code review / unified diff (0.4.6+) | `diff` via `canvas_node` (content = diff text; link to its file node with a `references` edge) |
109
+ | Flowchart / sequence / state diagram (0.4.6+) | `mermaid` via `canvas_node` (content = mermaid source; renders client-side, no hosted app) |
95
110
  | Image | `image` via `canvas_node` |
96
111
  | Cached URL content | `webpage` via `canvas_node` |
97
112
  | Structured UI | `json-render` via `canvas_render` |
98
113
  | Chart | `graph` via `canvas_render` |
114
+ | Live work-item board (0.4.6+) | `canvas_render { action: "workboard" }` — one board node, auto-refreshes on work-item changes |
99
115
  | Generated communication surface | HTML primitive via `canvas_node` |
100
116
  | Self-contained HTML/JS | `html` via `canvas_node` |
101
117
  | Hosted interactive MCP app | `canvas_app { action: "open-mcp-app" }` |
102
- | Excalidraw diagram | `canvas_app { action: "diagram" }` |
118
+ | Excalidraw diagram (interactive/human drawing) | `canvas_app { action: "diagram" }` — prefer `mermaid` for agent-authored diagrams |
103
119
  | Bundled React artifact | `canvas_app { action: "build-artifact" }` |
104
120
 
105
121
  Use the lightest tier that communicates the result. Do not build a web artifact when markdown,
@@ -159,6 +175,35 @@ v0.4.0 after their deprecated 0.3.x window.
159
175
  - Edge types: `flow`, `depends-on`, `relation`, `references`.
160
176
  - After manual or batch layout changes, run `canvas_query { action: "validate" }`.
161
177
 
178
+ ## In-View Placement & Sizing (required for user-facing nodes)
179
+
180
+ Auto-placement (omitting `x`/`y`) is **board-relative, not camera-relative**: it places right of
181
+ the last node or scans rows from the origin, ignoring where the human is looking. On a board with
182
+ distant nodes, an auto-placed node lands off-camera.
183
+
184
+ 1. Omit `x`/`y` only on an empty or locally dense board. Otherwise place near the human's
185
+ attention: the pinned/focused neighborhood, or explicit coordinates beside the last
186
+ user-facing output (gap ≥ 24–48 px).
187
+ 2. After creating nodes the human should see, pan the camera (operating-sequence step 7b):
188
+ `focus` for one node, `fit` with exactly the new `nodeIds` for a cluster. Use `noPan`
189
+ (`focus --no-pan`) only when you must not steal the camera.
190
+ 3. Never fit the whole board to "show" new work — on a board with outliers that miniaturizes
191
+ everything. Always pass explicit `nodeIds` to `fit`.
192
+ 4. Never leave user-facing output at far coordinates without a focus/fit.
193
+
194
+ **Size for content.** Omitting `width`/`height` gives readable per-type defaults — prefer them:
195
+ markdown 640×420, status 360×200, file 520×360, diff 640×420, mermaid 640×460, html 720×640,
196
+ graph 760×520, mcp-app 960×600, web-artifact 960×720. Since 0.4.6 the server clamps explicit creation sizes UP to per-type
197
+ readability floors (e.g. markdown 360×180, graph/json-render/html 420×280, mcp-app 480×320) —
198
+ a tiny probe size silently becomes the floor. `strictSize: true` is the only opt-out (a fixed
199
+ scrolling frame you genuinely want small). `canvas_query { action: "validate" }` additionally
200
+ reports any node below its floor as an advisory `sizeWarnings` entry — treat a non-empty list
201
+ as layout work left to do.
202
+
203
+ **Token hygiene.** For routine state checks use `canvas_ax_state { action: "get" }` WITHOUT
204
+ `includeContext` — the full AX context payload is ~10× larger; request it only when you are
205
+ actually consuming context.
206
+
162
207
  ## Context Pins
163
208
 
164
209
  Context pins are the primary human-to-agent bridge:
@@ -236,24 +281,25 @@ Prefer `canvas_query { action: "search" }` over parsing the full layout.
236
281
  - Hosted MCP-app/ext-app nodes such as Excalidraw require the in-canvas host bridge and are not
237
282
  standalone **Open as site** targets. URL-backed viewers and bundled web artifacts remain
238
283
  openable.
239
- - A hosted ext-app (Excalidraw) node that is already on the board when a **WebKit** host panel
240
- loads (e.g. the GitHub Copilot app's embedded WKWebView) can render as a black tile — a host
241
- compositor paint race on the nested iframe, **not** a broken node (the session is healthy and
242
- `sessionStatus` is `ready`; it renders fine in Chrome, the Codex browser, and for nodes created
243
- live after the panel hydrates). Under WebKit the canvas ATTEMPTS auto-recovery: after boot,
244
- each present-at-load ext-app is remounted through a serialized, boot-aware queue (one app at a
245
- time), with a watchdog retry for iframes that never boot and a re-arm when a panel that loaded
246
- hidden becomes visible. Treat the auto-recovery as best-effort, NOT guaranteed — the 0.3.2
247
- Copilot-host report shows tiles can still come up black in real WKWebView panels (compositing
248
- success is not observable from page JS, so retries are bounded). Since 0.4.1 the canvas also
249
- fires a post-boot **repaint nudge** under WebKit (a 1px layout bounce that forces the
250
- compositor to re-rasterize the frame — the cheap half of the proven enlarge-close unstick;
251
- shows as `repaint-nudge` in the recovery trail). Keep the cautionary behavior anyway:
252
- recovery is deterministic via **expand-then-close** of the black tile (forces a fresh mount in
253
- the fullscreen overlay, which always paints), or open the workbench in a normal browser
254
- (Chrome). Do not diagnose a healthy app session as a broken node. When reporting a black tile,
255
- fetch `GET /api/canvas/debug/ext-app-recovery` from the daemon and attach the trace — it shows
256
- what the recovery queue actually did in that panel.
284
+ - A standalone html surface (`/api/canvas/surface/:id` opened as a site) is a VISUAL view: it
285
+ renders the same content and theme, but `window.PMX_AX` is not injected without the canvas
286
+ iframe's per-mount nonce, so AX buttons only work inside the in-canvas node (0.4.4 Codex note).
287
+ Do not tell a user a standalone tab's controls will steer the agent.
288
+ - A hosted ext-app (Excalidraw) node in a **WebKit** host panel (e.g. the GitHub Copilot app's
289
+ embedded WKWebView) historically could render as a black tile — a host compositor paint race
290
+ on the nested iframe, **not** a broken node (the session is healthy, `sessionStatus` is
291
+ `ready`, and it renders fine in Chrome/Codex). Since 0.4.6 the canvas runs a **paint oracle +
292
+ recovery ladder** under WebKit: present-at-load ext-apps mount strictly one at a time (the
293
+ cold burst was the trigger), each frame answers a double-rAF **paint probe** after settle, and
294
+ on silence the ladder escalates — soft-expand cycle (the automatic analogue of the proven
295
+ enlarge+close) → serialized remount → an explicit "App surface failed to paint / Retry"
296
+ affordance. The connecting overlay stays up until paint is confirmed, so a black layer is
297
+ never presented as ready. The recovery trail
298
+ (`GET /api/canvas/debug/ext-app-recovery` / `window.__PMX_EXTAPP_LOG`) now records
299
+ `mount-slot`, `paint-ok`, `paint-fail`, `soft-expand-cycle`, and `recovery-exhausted` — when
300
+ diagnosing, trust `paint-ok`/`paint-fail`, and never assert health from `settled` alone. If a
301
+ tile still shows the Retry affordance, click Retry (fresh recovery budget) or expand-then-close;
302
+ attach the recovery trail when reporting.
257
303
  - Ext-app frame documents live in server memory. Through 0.4.0, killing/restarting the daemon
258
304
  while a panel stays open leaves ext-app tiles on dead frame URLs (`Frame document not found`,
259
305
  0.4.0 report Finding S) until a full workbench reload. Since 0.4.1 the browser revalidates its
@@ -272,11 +318,12 @@ Prefer `canvas_query { action: "search" }` over parsing the full layout.
272
318
  iframe) can block child iframes from loading ANY `src` URL, breaking every iframe-backed node
273
319
  with a gray placeholder. The canvas probes this at boot and auto-falls back to fetching
274
320
  same-origin surfaces and rendering them inline via `srcdoc` (HTML, graph, json-render, frame
275
- documents). In Amp orbs specifically, the server sees `AMP_ORB` in its environment and stamps
276
- the page so the embedded client skips the (there-unreliable) probe, goes straight to `srcdoc`,
277
- AND defaults to the polling transport (the portal proxy buffers SSE — waiting out the watchdog
278
- could trip the boot modal); HTML surfaces also inline their theme stylesheet so they render
279
- styled. External app
321
+ documents). In Amp orbs specifically, the server sees `AMP_ORB` in its environment and adapts
322
+ end to end: it binds the portal-assigned `$PORT` automatically (no port flag in the service
323
+ command), stamps the page so the embedded client skips the (there-unreliable) probe, goes
324
+ straight to `srcdoc`, AND defaults to the polling transport (the portal proxy buffers SSE —
325
+ waiting out the watchdog could trip the boot modal); HTML surfaces also inline their theme
326
+ stylesheet so they render styled. External app
280
327
  URLs cannot be inlined (cross-origin) and may stay blocked in such hosts. Force a mode with
281
328
  `/workbench?iframe-mode=srcdoc` (or `iframe-mode=src`) when diagnosing.
282
329
  - Graph and json-render standalone surfaces use `display=site` and fill the browser viewport, and