pmx-canvas 0.3.0 → 0.3.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 (161) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/dist/canvas/index.js +65 -65
  3. package/dist/types/cli/agent.d.ts +9 -1
  4. package/dist/types/cli/daemon.d.ts +74 -0
  5. package/dist/types/cli/watch.d.ts +2 -2
  6. package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
  7. package/dist/types/client/canvas/CommandPalette.d.ts +1 -1
  8. package/dist/types/client/canvas/Minimap.d.ts +1 -1
  9. package/dist/types/client/nodes/ExtAppFrame.d.ts +9 -1
  10. package/dist/types/client/nodes/FileNode.d.ts +1 -1
  11. package/dist/types/client/nodes/ImageNode.d.ts +1 -1
  12. package/dist/types/client/nodes/InlineFormatBar.d.ts +1 -1
  13. package/dist/types/client/nodes/MarkdownNode.d.ts +1 -1
  14. package/dist/types/client/nodes/PromptNode.d.ts +1 -1
  15. package/dist/types/client/nodes/ResponseNode.d.ts +1 -1
  16. package/dist/types/server/canvas-schema.d.ts +1 -1
  17. package/dist/types/server/html-primitives.d.ts +1 -1
  18. package/dist/types/server/index.d.ts +4 -4
  19. package/dist/types/server/operations/index.d.ts +1 -1
  20. package/dist/types/server/operations/ops/ax-read.d.ts +2 -0
  21. package/dist/types/server/operations/ops/canvas-wire.d.ts +2 -0
  22. package/dist/types/server/operations/ops/ext-app.d.ts +2 -0
  23. package/dist/types/server/server.d.ts +8 -0
  24. package/docs/cli.md +10 -1
  25. package/docs/http-api.md +28 -0
  26. package/docs/plans/plan-009-tech-debt-backlog.md +5 -5
  27. package/docs/screenshot.png +0 -0
  28. package/docs/tech-debt-assessment-2026-07.md +2 -2
  29. package/package.json +5 -2
  30. package/skills/pmx-canvas/SKILL.md +15 -10
  31. package/skills/pmx-canvas/references/full-reference.md +17 -3
  32. package/src/cli/agent.ts +1951 -1571
  33. package/src/cli/daemon.ts +460 -0
  34. package/src/cli/index.ts +80 -323
  35. package/src/cli/watch.ts +2 -10
  36. package/src/client/App.tsx +48 -46
  37. package/src/client/canvas/AttentionHistory.tsx +11 -1
  38. package/src/client/canvas/CanvasNode.tsx +41 -29
  39. package/src/client/canvas/CanvasViewport.tsx +101 -66
  40. package/src/client/canvas/CommandPalette.tsx +61 -27
  41. package/src/client/canvas/ContextMenu.tsx +13 -20
  42. package/src/client/canvas/ContextPinBar.tsx +1 -5
  43. package/src/client/canvas/ContextPinHud.tsx +1 -6
  44. package/src/client/canvas/DockedNode.tsx +4 -4
  45. package/src/client/canvas/EdgeLayer.tsx +37 -36
  46. package/src/client/canvas/ExpandedNodeOverlay.tsx +69 -45
  47. package/src/client/canvas/FocusFieldLayer.tsx +20 -22
  48. package/src/client/canvas/IntentLayer.tsx +31 -14
  49. package/src/client/canvas/Minimap.tsx +11 -16
  50. package/src/client/canvas/SelectionBar.tsx +4 -11
  51. package/src/client/canvas/ShortcutOverlay.tsx +3 -1
  52. package/src/client/canvas/SnapshotPanel.tsx +77 -95
  53. package/src/client/canvas/auto-fit.ts +15 -14
  54. package/src/client/canvas/snap-guides.ts +12 -12
  55. package/src/client/canvas/use-node-resize.ts +1 -5
  56. package/src/client/canvas/use-pan-zoom.ts +25 -26
  57. package/src/client/ext-app/bridge.ts +3 -12
  58. package/src/client/icons.tsx +63 -20
  59. package/src/client/nodes/ContextNode.tsx +14 -25
  60. package/src/client/nodes/ExtAppFrame.tsx +194 -80
  61. package/src/client/nodes/FileNode.tsx +74 -62
  62. package/src/client/nodes/GroupNode.tsx +4 -6
  63. package/src/client/nodes/HtmlNode.tsx +76 -46
  64. package/src/client/nodes/ImageNode.tsx +18 -27
  65. package/src/client/nodes/InlineFormatBar.tsx +4 -21
  66. package/src/client/nodes/InlineMarkdownEditor.tsx +0 -1
  67. package/src/client/nodes/LedgerNode.tsx +10 -4
  68. package/src/client/nodes/MarkdownNode.tsx +3 -10
  69. package/src/client/nodes/McpAppNode.tsx +26 -22
  70. package/src/client/nodes/MdFormatBar.tsx +10 -7
  71. package/src/client/nodes/PromptNode.tsx +23 -51
  72. package/src/client/nodes/ResponseNode.tsx +3 -13
  73. package/src/client/nodes/StatusNode.tsx +5 -9
  74. package/src/client/nodes/StatusSummary.tsx +2 -8
  75. package/src/client/nodes/WebpageNode.tsx +20 -14
  76. package/src/client/nodes/iframe-document-url.ts +25 -16
  77. package/src/client/nodes/image-warnings.ts +1 -7
  78. package/src/client/nodes/md-format.ts +20 -5
  79. package/src/client/state/attention-bridge.ts +4 -9
  80. package/src/client/state/attention-store.ts +1 -7
  81. package/src/client/state/canvas-store.ts +52 -36
  82. package/src/client/state/intent-bridge.ts +176 -112
  83. package/src/client/state/intent-store.ts +4 -1
  84. package/src/client/state/sse-bridge.ts +53 -70
  85. package/src/json-render/catalog.ts +12 -16
  86. package/src/json-render/charts/components.tsx +16 -20
  87. package/src/json-render/charts/extra-components.tsx +8 -16
  88. package/src/json-render/charts/extra-definitions.ts +1 -2
  89. package/src/json-render/charts/tufte-components.tsx +37 -20
  90. package/src/json-render/charts/tufte-definitions.ts +8 -2
  91. package/src/json-render/renderer/index.tsx +42 -22
  92. package/src/json-render/schema.ts +6 -3
  93. package/src/json-render/server.ts +33 -39
  94. package/src/mcp/canvas-access.ts +35 -21
  95. package/src/mcp/server.ts +132 -70
  96. package/src/server/agent-context.ts +63 -36
  97. package/src/server/ax-context.ts +7 -5
  98. package/src/server/ax-interaction.ts +176 -43
  99. package/src/server/ax-state-manager.ts +182 -39
  100. package/src/server/ax-state.ts +142 -47
  101. package/src/server/canvas-db.ts +213 -95
  102. package/src/server/canvas-operations.ts +180 -123
  103. package/src/server/canvas-provenance.ts +1 -4
  104. package/src/server/canvas-schema.ts +454 -73
  105. package/src/server/canvas-serialization.ts +27 -35
  106. package/src/server/canvas-state.ts +150 -58
  107. package/src/server/chart-template.ts +4 -5
  108. package/src/server/code-graph.ts +19 -6
  109. package/src/server/diagram-presets.ts +28 -29
  110. package/src/server/ext-app-lookup.ts +3 -12
  111. package/src/server/html-node-summary.ts +19 -10
  112. package/src/server/html-primitives.ts +326 -97
  113. package/src/server/html-surface.ts +6 -9
  114. package/src/server/image-source.ts +6 -4
  115. package/src/server/index.ts +320 -217
  116. package/src/server/intent-registry.ts +2 -5
  117. package/src/server/mcp-app-candidate.ts +5 -10
  118. package/src/server/mcp-app-host.ts +14 -38
  119. package/src/server/mcp-app-runtime.ts +12 -20
  120. package/src/server/mutation-history.ts +15 -5
  121. package/src/server/operations/composites.ts +1 -3
  122. package/src/server/operations/http.ts +2 -3
  123. package/src/server/operations/index.ts +7 -1
  124. package/src/server/operations/invoker.ts +4 -3
  125. package/src/server/operations/mcp.ts +22 -30
  126. package/src/server/operations/ops/annotation.ts +122 -10
  127. package/src/server/operations/ops/app.ts +98 -73
  128. package/src/server/operations/ops/ax-await.ts +17 -10
  129. package/src/server/operations/ops/ax-read.ts +347 -0
  130. package/src/server/operations/ops/ax-shared.ts +2 -7
  131. package/src/server/operations/ops/ax-state.ts +32 -14
  132. package/src/server/operations/ops/ax-timeline.ts +32 -19
  133. package/src/server/operations/ops/ax-work.ts +54 -37
  134. package/src/server/operations/ops/batch.ts +41 -15
  135. package/src/server/operations/ops/canvas-wire.ts +91 -0
  136. package/src/server/operations/ops/edges.ts +37 -25
  137. package/src/server/operations/ops/ext-app.ts +346 -0
  138. package/src/server/operations/ops/groups.ts +49 -20
  139. package/src/server/operations/ops/intent.ts +18 -12
  140. package/src/server/operations/ops/json-render.ts +239 -98
  141. package/src/server/operations/ops/nodes.ts +298 -109
  142. package/src/server/operations/ops/query.ts +46 -28
  143. package/src/server/operations/ops/snapshots.ts +35 -26
  144. package/src/server/operations/ops/validate.ts +2 -1
  145. package/src/server/operations/ops/viewport.ts +60 -16
  146. package/src/server/operations/ops/webview.ts +44 -18
  147. package/src/server/operations/registry.ts +2 -3
  148. package/src/server/operations/types.ts +7 -5
  149. package/src/server/operations/webview-runner.ts +1 -3
  150. package/src/server/placement.ts +8 -18
  151. package/src/server/server.ts +122 -1028
  152. package/src/server/spatial-analysis.ts +39 -25
  153. package/src/server/trace-manager.ts +3 -8
  154. package/src/server/web-artifacts.ts +23 -27
  155. package/src/server/webpage-node.ts +5 -13
  156. package/src/shared/auto-arrange.ts +12 -5
  157. package/src/shared/content-height-reporter.ts +8 -6
  158. package/src/shared/ext-app-tool-result.ts +2 -6
  159. package/src/shared/placement.ts +1 -4
  160. package/src/shared/semantic-attention.ts +39 -37
  161. package/src/shared/surface.ts +8 -4
@@ -10,4 +10,12 @@
10
10
  * - Idempotent operations where possible
11
11
  * - --yes for destructive actions, --dry-run for preview
12
12
  */
13
- export declare function runAgentCli(args: string[]): Promise<void>;
13
+ /**
14
+ * Extract the global `--port <n>` / `--server-url <url>` flags (any position,
15
+ * `=` or space-separated value) and set the invocation's target override.
16
+ * Returns the remaining args for command dispatch. Invalid values are a loud
17
+ * `die` — never a silent fallback to the default port. `--server-url` wins
18
+ * over `--port` when both are given.
19
+ */
20
+ export declare function extractGlobalTargetFlags(args: string[]): string[];
21
+ export declare function runAgentCli(rawArgs: string[]): Promise<void>;
@@ -0,0 +1,74 @@
1
+ export declare function outputJson(data: unknown): void;
2
+ export declare function readPidFile(path: string): number | null;
3
+ export declare function isProcessRunning(pid: number): boolean;
4
+ /**
5
+ * Whether pid's command line contains `needle`. Returns null when `ps` cannot
6
+ * answer (no such process, unsupported platform) — callers fall back to the
7
+ * plain liveness signal.
8
+ */
9
+ export declare function processCommandMatches(pid: number, needle: string): boolean | null;
10
+ /**
11
+ * Liveness with a PID-recycling guard: the pid must be alive AND its command
12
+ * line must still look like our daemon (unless `ps` is unavailable, in which
13
+ * case the plain liveness signal wins).
14
+ */
15
+ export declare function isOwnDaemonProcess(pid: number, entryNeedle: string): boolean;
16
+ export declare function removePidFile(path: string): void;
17
+ export interface HealthStatus {
18
+ responsive: boolean;
19
+ workspace: string | null;
20
+ }
21
+ export declare function readHealthStatus(url: string): Promise<HealthStatus>;
22
+ export declare function isHealthy(url: string): Promise<boolean>;
23
+ export declare function readLogTail(path: string, maxLines?: number): string | null;
24
+ export declare function waitForHealth(healthUrl: string, timeoutMs: number, getExitMessage: () => string | null): Promise<{
25
+ ok: true;
26
+ } | {
27
+ ok: false;
28
+ reason: string;
29
+ }>;
30
+ export declare function waitForShutdown(healthUrl: string, timeoutMs: number, isAlive: () => boolean): Promise<boolean>;
31
+ /** Workspace the daemon child will bind — mirrors startCanvasServer's resolution. */
32
+ export declare function resolveExpectedWorkspace(): string;
33
+ export type PrecheckVerdict = 'not-running' | 'already-running' | 'foreign-port-owner';
34
+ /** Pure decision for the pre-spawn health probe. */
35
+ export declare function classifyPrecheck(health: HealthStatus, expectedWorkspace: string): PrecheckVerdict;
36
+ export type LockResult = {
37
+ ok: true;
38
+ } | {
39
+ ok: false;
40
+ holderPid: number | null;
41
+ reason: 'running' | 'starting';
42
+ };
43
+ /**
44
+ * Exclusive pid-file creation IS the spawn lock. A held file with a live
45
+ * daemon pid means 'running'; a fresh empty file means another starter is
46
+ * mid-spawn ('starting'); anything stale is reclaimed once.
47
+ */
48
+ export declare function acquireDaemonLock(pidFile: string, entryNeedle: string): LockResult;
49
+ /**
50
+ * True when the pid file is a concurrent starter's spawn lock: an EMPTY file
51
+ * (acquireDaemonLock creates it before writing the child pid) younger than
52
+ * LOCK_FRESH_MS. `serve status` must NOT delete such a file — clearing it
53
+ * mid-spawn would defeat the spawn lock and let a racing starter double-spawn.
54
+ * Mirrors the fresh-lock protection acquireDaemonLock applies. A stat failure
55
+ * biases toward preserving the lock.
56
+ */
57
+ export declare function isFreshEmptyLock(pidFile: string): boolean;
58
+ export interface DaemonPaths {
59
+ port: number;
60
+ logFile: string;
61
+ pidFile: string;
62
+ }
63
+ export declare function startDaemonMode(options: DaemonPaths & {
64
+ baseArgs: string[];
65
+ waitMs: number;
66
+ entry: string;
67
+ }): Promise<void>;
68
+ export declare function showServeStatus(options: DaemonPaths & {
69
+ entry: string;
70
+ }): Promise<void>;
71
+ export declare function stopServeDaemon(options: DaemonPaths & {
72
+ waitMs: number;
73
+ entry: string;
74
+ }): Promise<void>;
@@ -1,5 +1,5 @@
1
1
  import { ALL_SEMANTIC_WATCH_EVENT_TYPES, formatCompactWatchEvent, SemanticWatchReducer, type SemanticWatchEvent, type SemanticWatchEventType, type SseMessage } from '../shared/semantic-attention.js';
2
- export { ALL_SEMANTIC_WATCH_EVENT_TYPES, formatCompactWatchEvent, SemanticWatchReducer, };
3
- export type { SemanticWatchEvent, SemanticWatchEventType, SseMessage, };
2
+ export { ALL_SEMANTIC_WATCH_EVENT_TYPES, formatCompactWatchEvent, SemanticWatchReducer };
3
+ export type { SemanticWatchEvent, SemanticWatchEventType, SseMessage };
4
4
  export declare function parseSemanticEventFilter(raw: string | undefined): Set<SemanticWatchEventType>;
5
5
  export declare function parseSseStream(stream: ReadableStream<Uint8Array>): AsyncGenerator<SseMessage>;
@@ -7,5 +7,5 @@ interface CanvasViewportProps {
7
7
  annotationTool?: AnnotationTool;
8
8
  }
9
9
  export declare function getRenderableWorldNodes(allNodes: Iterable<CanvasNodeState>, focusedNodeId: string | null): CanvasNodeState[];
10
- export declare function CanvasViewport({ onNodeContextMenu, onCanvasContextMenu, annotationMode, annotationTool }: CanvasViewportProps): import("preact/src").JSX.Element;
10
+ export declare function CanvasViewport({ onNodeContextMenu, onCanvasContextMenu, annotationMode, annotationTool, }: CanvasViewportProps): import("preact/src").JSX.Element;
11
11
  export {};
@@ -1,4 +1,4 @@
1
- export declare function CommandPalette({ onClose, onToggleMinimap, }: {
1
+ export declare function CommandPalette({ onClose, onToggleMinimap }: {
2
2
  onClose: () => void;
3
3
  onToggleMinimap: () => void;
4
4
  }): import("preact/src").JSX.Element;
@@ -19,5 +19,5 @@ interface MinimapProps {
19
19
  containerWidth: number;
20
20
  containerHeight: number;
21
21
  }
22
- export declare function Minimap({ viewport, nodes, edges, onNavigate, containerWidth, containerHeight, }: MinimapProps): import("preact/src").JSX.Element;
22
+ export declare function Minimap({ viewport, nodes, edges, onNavigate, containerWidth, containerHeight }: MinimapProps): import("preact/src").JSX.Element;
23
23
  export {};
@@ -19,7 +19,15 @@ interface ExtAppHostDimensionsTarget {
19
19
  * everywhere we can test (Chrome / Codex / Playwright).
20
20
  */
21
21
  export declare function isWebKitOnlyHost(userAgent: string): boolean;
22
- export declare function nextWebkitRepaintSlot(): number;
22
+ export declare const WEBKIT_REMOUNT_SETTLE_MS = 1000;
23
+ export interface WebkitRemountTask {
24
+ /** Perform the remount. Return false if the node no longer needs it (skips the boot wait). */
25
+ remount: () => boolean;
26
+ /** Resolves when the remounted app genuinely boots AND finishes its bootstrap replay, or after a bounded timeout. */
27
+ awaitBoot: () => Promise<void>;
28
+ }
29
+ export declare function extAppRecoveryLog(nodeId: string, event: string): void;
30
+ export declare function enqueueWebkitRemount(task: WebkitRemountTask): void;
23
31
  export declare function shouldScheduleWebKitRepaint(status: ExtAppFrameStatus, hasReplayToolResult: boolean): boolean;
24
32
  export declare function getExtAppBridgeInitKey(node: CanvasNodeState, retryKey: number): string;
25
33
  export declare function resolveExtAppDisplayModeRequest(requestedMode: DisplayMode, isExpanded: boolean): {
@@ -1,5 +1,5 @@
1
1
  import type { CanvasNodeState } from '../types';
2
- export declare function FileNode({ node, expanded, }: {
2
+ export declare function FileNode({ node, expanded }: {
3
3
  node: CanvasNodeState;
4
4
  expanded?: boolean;
5
5
  }): import("preact/src").JSX.Element;
@@ -4,7 +4,7 @@ import type { CanvasNodeState } from '../types';
4
4
  * Supports: file paths (served via /api/canvas/image/:nodeId), data URIs, and URLs.
5
5
  * Features: fit-to-container, zoom in/out within node, pan when zoomed.
6
6
  */
7
- export declare function ImageNode({ node, expanded, }: {
7
+ export declare function ImageNode({ node, expanded }: {
8
8
  node: CanvasNodeState;
9
9
  expanded?: boolean;
10
10
  }): import("preact/src").JSX.Element;
@@ -1,7 +1,7 @@
1
1
  import type { RefObject } from 'preact';
2
2
  /** Floating selection toolbar for a contentEditable host. Mounts once; while
3
3
  * visible, positions itself above the current selection's viewport rect. */
4
- export declare function InlineFormatBar({ hostRef, onChange, }: {
4
+ export declare function InlineFormatBar({ hostRef, onChange }: {
5
5
  hostRef: RefObject<HTMLElement>;
6
6
  onChange: () => void;
7
7
  }): import("preact").JSX.Element | null;
@@ -1,6 +1,6 @@
1
1
  import type { JSX } from 'preact';
2
2
  import type { CanvasNodeState } from '../types';
3
- export declare function MarkdownNode({ node, expanded, }: {
3
+ export declare function MarkdownNode({ node, expanded }: {
4
4
  node: CanvasNodeState;
5
5
  expanded?: boolean;
6
6
  }): JSX.Element;
@@ -1,5 +1,5 @@
1
1
  import type { CanvasNodeState } from '../types';
2
- export declare function PromptNode({ node, expanded, }: {
2
+ export declare function PromptNode({ node, expanded }: {
3
3
  node: CanvasNodeState;
4
4
  expanded?: boolean;
5
5
  }): import("preact/src").JSX.Element;
@@ -1,5 +1,5 @@
1
1
  import type { CanvasNodeState } from '../types';
2
- export declare function ResponseNode({ node, expanded, }: {
2
+ export declare function ResponseNode({ node, expanded }: {
3
3
  node: CanvasNodeState;
4
4
  expanded?: boolean;
5
5
  }): import("preact/src").JSX.Element;
@@ -34,7 +34,7 @@ export interface StructuredValidationResult {
34
34
  summary: Record<string, unknown>;
35
35
  }
36
36
  declare const CANONICAL_GRAPH_TYPES: readonly ["line", "bar", "pie", "area", "scatter", "radar", "stacked-bar", "composed", "sparkline", "dot-plot", "bullet", "slopegraph"];
37
- type CanvasGraphType = typeof CANONICAL_GRAPH_TYPES[number];
37
+ type CanvasGraphType = (typeof CANONICAL_GRAPH_TYPES)[number];
38
38
  export declare function describeCanvasSchema(): {
39
39
  ok: true;
40
40
  source: 'running-server';
@@ -1,5 +1,5 @@
1
1
  export declare const HTML_PRIMITIVE_KINDS: readonly ["choice-grid", "plan-timeline", "review-sheet", "pr-writeup", "system-map", "code-walkthrough", "design-sheet", "component-gallery", "interaction-prototype", "flowchart", "deck", "presentation", "illustration-set", "explainer", "status-report", "incident-report", "triage-board", "config-editor", "prompt-tuner"];
2
- export type HtmlPrimitiveKind = typeof HTML_PRIMITIVE_KINDS[number];
2
+ export type HtmlPrimitiveKind = (typeof HTML_PRIMITIVE_KINDS)[number];
3
3
  export interface HtmlPrimitiveDescriptor {
4
4
  kind: HtmlPrimitiveKind;
5
5
  title: string;
@@ -13,7 +13,7 @@ import { type SerializedCanvasNode } from './canvas-serialization.js';
13
13
  import type { HtmlPrimitiveKind } from './html-primitives.js';
14
14
  import { type WebArtifactBuildInput, type WebArtifactCanvasBuildResult } from './web-artifacts.js';
15
15
  import { type ExternalMcpTransportConfig } from './mcp-app-runtime.js';
16
- import { type DiagramPresetOpenInput } from './diagram-presets.js';
16
+ import type { DiagramPresetOpenInput } from './diagram-presets.js';
17
17
  import { type GraphNodeInput, type JsonRenderNodeInput, type JsonRenderSpec } from '../json-render/server.js';
18
18
  import type { CanvasAutomationWebViewOptions, CanvasAutomationWebViewStatus } from './server.js';
19
19
  /**
@@ -557,21 +557,21 @@ export type { CanvasNodeState, CanvasEdge, CanvasLayout, ViewportState } from '.
557
557
  export type { CanvasAutomationWebViewOptions, CanvasAutomationWebViewStatus, PrimaryWorkbenchCanvasPromptRequest, PrimaryWorkbenchIntent, } from './server.js';
558
558
  export { emitPrimaryWorkbenchEvent, consumePrimaryWorkbenchIntents, setPrimaryWorkbenchAutoOpenEnabled, setPrimaryWorkbenchCanvasPromptHandler, startCanvasServer, stopCanvasServer, getCanvasServerPort, openUrlInExternalBrowser, getCanvasAutomationWebViewStatus, startCanvasAutomationWebView, stopCanvasAutomationWebView, evaluateCanvasAutomationWebView, resizeCanvasAutomationWebView, screenshotCanvasAutomationWebView, } from './server.js';
559
559
  export { canvasState } from './canvas-state.js';
560
- export type { CanvasAnnotation, CanvasSnapshot, CanvasSnapshotGcResult, CanvasSnapshotListOptions } from './canvas-state.js';
560
+ export type { CanvasAnnotation, CanvasSnapshot, CanvasSnapshotGcResult, CanvasSnapshotListOptions, } from './canvas-state.js';
561
561
  export { findOpenCanvasPosition } from './placement.js';
562
562
  export { searchNodes, buildSpatialContext, detectClusters, findNeighborhoods } from './spatial-analysis.js';
563
563
  export type { SpatialCluster, SpatialContext, SpatialNeighbor, NodeSpatialInfo } from './spatial-analysis.js';
564
564
  export { mutationHistory, diffLayouts, formatDiff } from './mutation-history.js';
565
565
  export { recomputeCodeGraph, buildCodeGraphSummary, formatCodeGraph } from './code-graph.js';
566
566
  export { describeCanvasSchema, validateStructuredCanvasPayload } from './canvas-schema.js';
567
- export { buildHtmlPrimitive, getHtmlPrimitiveSemanticMetadata, isHtmlPrimitiveKind, listHtmlPrimitiveDescriptors } from './html-primitives.js';
567
+ export { buildHtmlPrimitive, getHtmlPrimitiveSemanticMetadata, isHtmlPrimitiveKind, listHtmlPrimitiveDescriptors, } from './html-primitives.js';
568
568
  export { buildWebArtifactOnCanvas, executeWebArtifactBuild, openWebArtifactInCanvas, resolveWebArtifactScriptPath, resolveWorkspacePath, } from './web-artifacts.js';
569
569
  export { buildGraphSpec, buildJsonRenderViewerHtml, createJsonRenderNodeData, GRAPH_NODE_SIZE, JSON_RENDER_NODE_SIZE, normalizeAndValidateJsonRenderSpec, } from '../json-render/server.js';
570
570
  export type { CodeGraphSummary, CodeGraphEdge } from './code-graph.js';
571
571
  export type { MutationEntry, MutationSummary, SnapshotDiffResult } from './mutation-history.js';
572
572
  export type { WebArtifactBuildInput, WebArtifactBuildOutput, WebArtifactCanvasBuildResult, WebArtifactCanvasOpenResult, } from './web-artifacts.js';
573
573
  export type { GraphNodeInput, JsonRenderNodeInput, JsonRenderSpec } from '../json-render/server.js';
574
- export type { HtmlPrimitiveKind, HtmlPrimitiveDescriptor, HtmlPrimitiveInput, HtmlPrimitiveBuildResult } from './html-primitives.js';
574
+ export type { HtmlPrimitiveKind, HtmlPrimitiveDescriptor, HtmlPrimitiveInput, HtmlPrimitiveBuildResult, } from './html-primitives.js';
575
575
  export { traceManager } from './trace-manager.js';
576
576
  export type { PmxAxApprovalGate, PmxAxApprovalStatus, PmxAxCommandDescriptor, PmxAxContext, PmxAxEvent, PmxAxElicitation, PmxAxElicitationStatus, PmxAxEventKind, PmxAxEvidence, PmxAxEvidenceKind, PmxAxFocusState, PmxAxHostCapability, PmxAxMode, PmxAxModeRequest, PmxAxModeRequestStatus, PmxAxPolicy, PmxAxReviewAnchorType, PmxAxReviewAnnotation, PmxAxReviewKind, PmxAxReviewRegion, PmxAxReviewSeverity, PmxAxReviewStatus, PmxAxSource, PmxAxState, PmxAxSteeringMessage, PmxAxTimelineSummary, PmxAxWorkItem, PmxAxWorkItemStatus, } from './ax-state.js';
577
577
  export type { AxTimelineQuery } from './canvas-db.js';
@@ -1,7 +1,7 @@
1
1
  export { executeOperation, getOperation, listOperations, registerOperation, setOperationEventEmitter, } from './registry.js';
2
2
  export { dispatchOperationRoute } from './http.js';
3
3
  export { runCanvasBatchOperation, type BatchEnvelope } from './ops/batch.js';
4
- export { type OpenMcpAppCoreResult } from './ops/app.js';
4
+ export type { OpenMcpAppCoreResult } from './ops/app.js';
5
5
  export { LocalOperationInvoker, HttpOperationInvoker, type OperationInvoker } from './invoker.js';
6
6
  export { registerOperationTools, registerCompositeTools, type OperationToolHost } from './mcp.js';
7
7
  export { compositeToolDefinitions, type CompositeToolDefinition } from './composites.js';
@@ -0,0 +1,2 @@
1
+ import { type Operation } from '../types.js';
2
+ export declare const axReadOperations: Operation[];
@@ -0,0 +1,2 @@
1
+ import { type Operation } from '../types.js';
2
+ export declare const canvasWireOperations: Operation[];
@@ -0,0 +1,2 @@
1
+ import { type Operation } from '../types.js';
2
+ export declare const extAppOperations: Operation[];
@@ -81,6 +81,14 @@ export declare function setPrimaryWorkbenchAutoOpenEnabled(enabled: boolean): vo
81
81
  export declare function isPrimaryWorkbenchAutoOpenEnabled(): boolean;
82
82
  export declare function hasWorkbenchSubscribers(): boolean;
83
83
  export declare function setPrimaryWorkbenchCanvasPromptHandler(handler: PrimaryWorkbenchCanvasPromptHandler | null): void;
84
+ /**
85
+ * Containment check for bundle-asset serving. Separator-agnostic: on Windows,
86
+ * `resolve()` returns backslash paths, so comparing against a `${dir}/` template
87
+ * rejected every asset — /canvas/index.js 404'd and the SPA never booted (the
88
+ * 0.3.1 Windows report). Normalizing both sides keeps the check exact on POSIX
89
+ * and correct on win32, and testable with win32-shaped fixtures on any host.
90
+ */
91
+ export declare function isCanvasBundlePath(distPath: string, bundleDir: string): boolean;
84
92
  export declare function buildMacBrowserOpenScript(appName: string, url: string): string;
85
93
  export declare function openUrlInExternalBrowser(url: string): boolean;
86
94
  /**
package/docs/cli.md CHANGED
@@ -2,7 +2,16 @@
2
2
 
3
3
  The CLI is the shell-native way to run and control PMX Canvas. It targets
4
4
  `http://localhost:4313` by default — override with `PMX_CANVAS_URL` or
5
- `PMX_CANVAS_PORT` when the server runs elsewhere.
5
+ `PMX_CANVAS_PORT` when the server runs elsewhere, or per invocation with the
6
+ global `--port <n>` / `--server-url <url>` flags (any position; `--server-url`
7
+ wins over `--port`, and both win over the environment variables). An invalid
8
+ value for either flag is a hard error — the CLI never falls back silently to
9
+ the default port.
10
+
11
+ ```bash
12
+ pmx-canvas node list --port 4750 # target a non-default daemon
13
+ pmx-canvas --server-url http://127.0.0.1:4750 status # same, by URL
14
+ ```
6
15
 
7
16
  ## Server lifecycle
8
17
 
package/docs/http-api.md CHANGED
@@ -276,6 +276,34 @@ and `/ax/mode/:id/resolve` return 404 for unknown IDs; `/ax/activity` requires a
276
276
  valid `kind` + `title` (400 otherwise); the single-item gate GETs return 404 for
277
277
  unknown IDs and clamp `?waitMs` to ≤120000.
278
278
 
279
+ ## Ghost Cursor intents (ephemeral)
280
+
281
+ Intents are verb-routed over HTTP — there is **no `action` field**. That
282
+ discriminator exists only on the MCP `canvas_intent` composite; `POST` always
283
+ signals a *new* intent, so a body like `{"action":"clear"}` is rejected by the
284
+ registry's kind validation. Pick the operation by method + path:
285
+
286
+ ```bash
287
+ # Signal a ghost — returns { ok, intent } with the id. Auto-expires after ttlMs
288
+ # (default 8000, max 60000), so cleanup is optional.
289
+ curl -X POST http://localhost:4313/api/canvas/ax/intent \
290
+ -H "Content-Type: application/json" \
291
+ -d '{"kind":"create","position":{"x":400,"y":300},"nodeType":"markdown","label":"Add evidence","reason":"collecting run logs"}'
292
+
293
+ # Update the ghost (position/label/reason/confidence/ttlMs; vetoed:true dissolves
294
+ # it AND poisons the id so a later linked settle is rejected)
295
+ curl -X PATCH http://localhost:4313/api/canvas/ax/intent/<id> \
296
+ -H "Content-Type: application/json" \
297
+ -d '{"position":{"x":520,"y":300},"reason":"moved next to the trace"}'
298
+
299
+ # Clear the ghost: plain DELETE dissolves it; settle it into the real node it
300
+ # became via settledNodeId, or veto it. Fields are accepted as JSON body or
301
+ # query params (?vetoed=true/false is coerced; other values are rejected 400).
302
+ curl -X DELETE http://localhost:4313/api/canvas/ax/intent/<id>
303
+ curl -X DELETE "http://localhost:4313/api/canvas/ax/intent/<id>?settledNodeId=node-42"
304
+ curl -X DELETE "http://localhost:4313/api/canvas/ax/intent/<id>?vetoed=true"
305
+ ```
306
+
279
307
  ## Diagrams (Excalidraw preset)
280
308
 
281
309
  ```bash
@@ -10,9 +10,9 @@ Each item is tagged by shape: **project** (multi-day, own plan/PR), **decision**
10
10
 
11
11
  | Item | Source | What's open | Shape |
12
12
  |---|---|---|---|
13
- | Complete the registry migration | C1 | Fold the ~50 remaining hand-written routes (44 tombstone comments) in `server.ts` into operations; collapse the 76-method `PmxCanvas` onto `executeOperation`; migrate the CLI's 60 raw `fetch` calls onto the operation invoker. Deletes the M5 handler clones and most M4 envelope inconsistency as side effects. | project |
13
+ | Complete the registry migration | C1 | Fold the remaining hand-written routes in `server.ts` into operations; collapse the 76-method `PmxCanvas` onto `executeOperation`; migrate the CLI's 60 raw `fetch` calls onto the operation invoker. **Slice 1 done post-0.3.0:** the 7 `/api/ext-app/*` routes moved to `ops/ext-app.ts` (~290 lines out of `server.ts`, the 5 clone handlers — old M5 collapsed to one parametrized definition; wire envelopes byte-identical, no new MCP tools). **Slice 2 done:** the 15 ax wire routes (8 GET lists + `ax/context` + `ax/surface-snapshot` + `pinned-context` + `code-graph` + `ax/activity` + `ax/interaction` + legacy `PATCH /api/canvas/ax`) moved to `ops/ax-read.ts` (~370 more lines out of `server.ts`); `open-external` stays hand-written (coupled to the live server port + system browser opener). **Slice 3 done:** theme GET/POST, canvas update, viewport, annotation POST, and node `:id/refresh` moved to the registry (`ops/canvas-wire.ts`, plus additions to `ops/viewport.ts`, `ops/annotation.ts`, `ops/nodes.ts`); the two dispatch gates unified into one `/api/` gate; `server.ts` is at ~2,980 lines (from 3,873 at the audit). **Remaining routes (~20)** are the deliberate poor fits (SSE, binary, HTML/static, file bodies) plus four server-coupled handlers: `prompt` + workbench `state`/`open`/`intent` (module-local `primaryWorkbench*` session state), `render` (Excalidraw URL-normalizer chain), `open-external` (live port + system browser). Those need state injection like the webview runner — a deliberate follow-up, not mechanical. After routes, the bigger halves remain: `PmxCanvas` (76 methods) onto `executeOperation`, and the CLI's raw fetches onto the invoker. | project (in progress) |
14
14
  | One HTTP envelope | M4 residual | Registry ops return bare bodies, legacy ext-app handlers `{ok:true,result}`, 404s plaintext. Unify (naturally falls out of C1). Also: surface persistence save failures via a `/health` degraded flag — `canvas-state.ts` has 63 catch→warn→continue sites. | project (with C1) |
15
- | `dist/` commit policy | C3 residual | Decide whether `dist/canvas` + `dist/types` stay committed; if yes, add a CI staleness check (every source change currently hand-carries `.d.ts` diffs). | decision |
15
+ | `dist/` commit policy | C3 residual | **Decided + done post-0.3.0:** `dist/` stays committed; `test.yml` now has a `dist/types` staleness gate (rebuild + fail on drift). The `dist/canvas`/`dist/json-render` bundles are deliberately not byte-checked bundler output varies across bun versions; their staleness story folds into M1. | done |
16
16
  | Skill-mirror validator | C2 residual | `validate:agent-skills` exits 0 when the (local-only, gitignored) trees are absent. Docs are honest about this now; decide whether to repoint, gate, or delete the check. | decision |
17
17
  | Generate CLAUDE.md/AGENTS.md from one source | M9 residual | The twins are byte-identical again but still hand-maintained; generate or symlink to prevent the next drift. | small |
18
18
 
@@ -20,10 +20,10 @@ Each item is tagged by shape: **project** (multi-day, own plan/PR), **decision**
20
20
 
21
21
  | Item | Source | What's open | Shape |
22
22
  |---|---|---|---|
23
- | Lint/format tooling | H6 | No ESLint/Biome/Prettier anywhere; `docs/RELEASE.md` claims `release:check` lints (it doesn't). Adopt Biome, wire into CI and `release:check`. First run will be a large mechanical diff land it standalone. | decision + project |
23
+ | Lint/format tooling | H6 | **Done post-0.3.0:** Biome 2.5 adopted — `bun run lint` / `lint:fix`, wired into `release:check` (making RELEASE.md's "lints" claim true) and CI. First-run format sweep landed as its own mechanical commit; 7 lint errors hand-fixed (incl. a real conditional-hooks bug in `SelectionBar.tsx`). Deliberately disabled rules to revisit: a11y group, `useExhaustiveDependencies` (misfires on Preact signals), `noNonNullAssertion` (143 deliberate uses), `noArrayIndexKey` (13 sites). ~77 warnings (unused imports/vars, optional-chain) left as a burn-down listwarnings don't fail the gate. | done |
24
24
  | Client + server unit coverage | H7 | Zero component render tests (no DOM test lib in repo); untested big modules: `ax-state-manager.ts` (826 lines), `mcp-app-host.ts` (814), `canvas-schema.ts` (644), `shared/semantic-attention.ts` (600, in zero test files). Coverage is collected but gates nothing; tests aren't typechecked. Add `happy-dom` + `@testing-library/preact`, a tests tsconfig, and a coverage threshold. | project |
25
- | Daemon port/lock lifecycle | H4 | The server does port fallback but `cli/index.ts:202,268,302` polls the *requested* port: a fallen-back daemon reads as dead and `serve stop` can't kill it. TOCTOU double-spawn between health check and spawn; `isProcessRunning` treats `EPERM` as alive with no PID-recycling guard. Fix: child reports its actual bound port; parent locks before spawn and records the pid immediately. Root cause of the intermittently flaky `pmx-canvas-sdk.test.ts` port collision. | project (small-ish, high value) |
26
- | Test-suite structure | M8 | Integration monoliths under `tests/unit/` (`server-api` 5,213 lines, `cli-node` 2,903); `getAvailablePort()` copy-pasted in 5 files; `waitForPersistence(650ms)` fixed sleeps in 9 files + ~19 magic `Bun.sleep`s; Playwright lacks `retries`/`forbidOnly`; `test:e2e-cli` (517-line script) runs in no CI workflow. | project |
25
+ | Daemon port/lock lifecycle | H4 | **Fixed post-0.3.0** (`src/cli/daemon.ts` + `tests/unit/cli-daemon.test.ts`). Note the original finding was partly wrong: the daemon child was already strict-port (`PmxCanvas.start` pins `allowPortFallback: false`), so fallback-blind polling could not occur. The real defects fixed were: the pre-check accepted any responsive `/health` as "already running" (even a foreign workspace's daemon), a TOCTOU double-spawn window, the pid recorded only after health passed (slow start → unkillable orphan), and no PID-recycling guard. The flaky `pmx-canvas-sdk.test.ts` port collision is NOT this — it's M8's copy-pasted `getAvailablePort()` racing. | done |
26
+ | Test-suite structure | M8 | Integration monoliths under `tests/unit/` (`server-api` 5,213 lines, `cli-node` 2,903); `getAvailablePort()` copy-pasted in 5 files; `waitForPersistence(650ms)` fixed sleeps in 9 files + ~19 magic `Bun.sleep`s; Playwright lacks `retries`/`forbidOnly`; `test:e2e-cli` (517-line script) runs in no CI workflow. *Post-0.3.0:* `operation-parity` now binds port 0 and derives the port from the base URL (removes one reserve-then-rebind race); the shared-helper dedup and the rest remain. Also noted: `ax-parity.test.ts` is a string-matching source-analysis test whose needles broke when Biome re-wrapped `cmd(` calls — its CLI needles are now layout-independent, but any future needle should avoid depending on line layout. **Also noted (2026-07-07):** the `server-api` Excalidraw diagram tests do real network round-trips to the hosted `mcp.excalidraw.com` MCP server and intermittently time out (30s) under full parallel runs — they should be mocked or moved to a network-tagged tier. | project |
27
27
  | `noUncheckedIndexedAccess` | L7 residual | Enable in tsconfig; will surface many index-access errors — treat as its own sweep. | small (noisy) |
28
28
 
29
29
  ## Phase 3 — client consolidation
Binary file
@@ -67,8 +67,8 @@ The 3 fully parallel implementations (`canvas_add_html_node`, `canvas_add_html_p
67
67
  #### H3. `src/cli/agent.ts` is a 3,337-line monolith with 83 hand-registered commands
68
68
  83 `cmd(...)` registrations in one file, routed by a manual three-then-two-then-one word lookup (`agent.ts:3287-3301`); 81 copy-pasted `if (flags.help || flags.h) return showCommandHelp(...)` preambles; a hand-rolled flag parser with a ~40-entry `BOOL_FLAGS` allowlist (`agent.ts:184-193`) that must be updated for every new boolean flag — while `src/cli/index.ts:43-87` independently implements a *second* parser with different rules. **Remediation:** per-domain command modules + a `defineCommand` wrapper + one shared arg parser (and see C1 for the transport).
69
69
 
70
- #### H4. Daemon lifecycle management contradicts the server's own port behavior
71
- The server does port fallback (`buildPortCandidates()`, `server.ts:2773`, bind loop at `:3533`), but the CLI daemon manager hardcodes `http://localhost:${options.port}/health` (`cli/index.ts:202,268,302`). If the child falls back, the parent polls the wrong port and reports a dead daemon that is actually running orphaned and unkillable via `serve stop` since the pid file is only written after health succeeds (`:249`). Additional fragility: TOCTOU between the health check (`:206`) and spawn (`:225`) lets two concurrent `serve` calls double-spawn, and `isProcessRunning` treats `EPERM` as alive with no PID-recycling guard (`:105-115`). This port-collision class caused the one flaky unit-test failure in the first verification run (`pmx-canvas-sdk.test.ts`, `EADDRINUSE` on candidate 4799; passed in the 2026-07-06 re-run the flake is intermittent, the root cause is unfixed). **Remediation:** child reports its actual bound port (pidfile/stdout); parent locks before spawning and records pid immediately.
70
+ #### H4. Daemon lifecycle: races, orphans, and a dishonest pre-check — FIXED post-0.3.0 (finding partly corrected)
71
+ *Correction (2026-07-07):* the original claim that a fallen-back child leaves the parent polling the wrong port was wrong — the daemon child was already strict-port (`PmxCanvas.start` pins `allowPortFallback: false`, `index.ts:176`; only the MCP auto-start path opts into fallback). The real defects were: the pre-check treated **any** responsive `/health` on the port as "already running" (including a different workspace's daemon the same wrong-workspace class as v0.2.7 Finding I); a TOCTOU window between health check and spawn; the pid file written only after health passed, so a slow-starting daemon outlived its `--wait-ms` budget as an unkillable orphan; and `isProcessRunning` treating `EPERM` as alive with no PID-recycling guard. *Fix (post-0.3.0):* daemon lifecycle extracted to `src/cli/daemon.ts` — the pid file is created exclusively as the spawn lock and records the pid immediately, failed startups kill the child, the pre-check is workspace-aware, and liveness is cross-checked against the process command line (`tests/unit/cli-daemon.test.ts`, 19 tests, plus a live start/double-start/foreign-workspace/status/stop smoke). The intermittent `pmx-canvas-sdk.test.ts` port flake is unrelated it is M8's copy-pasted `getAvailablePort()` racing.
72
72
 
73
73
  #### H5. sse-bridge maintains a second, divergent copy of node construction and thread state
74
74
  `src/client/state/sse-bridge.ts` (1,066 lines, 34 handlers) has its own `DEFAULT_POSITIONS`/`makeNode` node factory (`:75-114`) fully separate from `canvas-store`'s `addNode`/`parseCanvasNode` — two factories with divergent defaults. Thread/turn reconciliation hand-mutates a `turns[]` array plus a module-level `responseToThreadMap` whose own comment admits it is *"Not cleaned on SSE reconnect"* (`:43`), with heuristic dedup (`:650-654`). Server-side, `syncEventToCanvasState` (`server.ts:3046`, ~330 lines) mirrors this as a giant if/else chain, with node geometry defaults duplicated against `ensureDefaultDockedNodes` under a comment saying *"keep geometry/dock defaults in sync if you change them"* (`:3006`). This is the exact drift class that Canvas Architecture Rule 2 exists to prevent. **Remediation:** one shared node factory; treat `canvas-layout-update` as the single source of truth for thread nodes; table-drive the event sync.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmx-canvas",
3
- "version": "0.3.0",
3
+ "version": "0.3.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",
@@ -38,9 +38,11 @@
38
38
  "build": "bun run build:client && bun run build:json-render && bun run build:types",
39
39
  "start": "bun run src/cli/index.ts --no-open",
40
40
  "pack:dry-run": "bun pm pack --dry-run",
41
- "release:check": "bun run build && bun run typecheck && bun run test:all",
41
+ "release:check": "bun run build && bun run typecheck && bun run lint && bun run test:all",
42
42
  "release:smoke": "bash scripts/release-smoke.sh",
43
43
  "typecheck": "tsc --noEmit",
44
+ "lint": "biome check .",
45
+ "lint:fix": "biome check --write .",
44
46
  "test": "PMX_CANVAS_DISABLE_BROWSER_OPEN=1 bun test tests/unit",
45
47
  "test:unit": "PMX_CANVAS_DISABLE_BROWSER_OPEN=1 bun test tests/unit",
46
48
  "test:coverage": "bun test tests/unit --coverage --coverage-reporter=text --coverage-reporter=lcov --coverage-dir coverage",
@@ -76,6 +78,7 @@
76
78
  "zod": "^4.3.6"
77
79
  },
78
80
  "devDependencies": {
81
+ "@biomejs/biome": "^2.5.2",
79
82
  "@playwright/test": "^1.58.2",
80
83
  "@tailwindcss/cli": "^4.1.18",
81
84
  "@types/bun": "latest",
@@ -24,7 +24,8 @@ Humans curate agent context by pinning nodes; agents read that curation through
24
24
  `workspace` must equal the intended absolute workspace root. A healthy listener on port 4313
25
25
  may belong to another project.
26
26
  3. **Read before write.** Search with `canvas_query { action: "search", query }` before creating
27
- nodes. Read the full layout only when necessary.
27
+ nodes. Read the full layout only when necessary. The MCP parameter is `query` — passing the
28
+ HTTP API's `q` is silently ignored and returns zero results.
28
29
  4. **Snapshot before destructive changes.** Use `canvas_snapshot` (deprecated standalone; folds into
29
30
  the `canvas_snapshot` composite's `save` action in v0.4) before clear, restore, or a major
30
31
  reorganization.
@@ -33,7 +34,8 @@ Humans curate agent context by pinning nodes; agents read that curation through
33
34
  or edit, then pass the returned `intent.id` as `intentId` on the mutation so the ghost settles
34
35
  into the result. Use it as much as possible to make your next move and your work visible: the
35
36
  human watches intent form and can veto mid-thought. Skip it only for trivial in-place tweaks or
36
- high-frequency batch churn.
37
+ high-frequency batch churn. The default TTL (~8s) expires between agent turns: signal with
38
+ `ttlMs: 30000` and settle by passing `intentId` on the mutation in the same or next call.
37
39
  6. **Mutate through current composites.** Prefer the 15 composite MCP tools below.
38
40
  7. **Arrange and validate.** After batch changes, use `canvas_view { action: "arrange" }` when
39
41
  appropriate and always finish with `canvas_query { action: "validate" }`.
@@ -55,7 +57,9 @@ Both surfaces report `workspace`. It must match the intended workspace root.
55
57
 
56
58
  - If `responsive: true` but `pidRunning: false`, treat the listener as potentially stale.
57
59
  - On mismatch, do not mutate. Start the intended workspace on an explicit free port:
58
- `pmx-canvas serve --daemon --no-open --port=<free-port>`.
60
+ `pmx-canvas serve --daemon --no-open --port=<free-port>`. (`serve --daemon` enforces this
61
+ itself: pointed at a port owned by another workspace, it refuses with the owner named instead
62
+ of reporting "already running".)
59
63
  - Target that port and re-check `/health`.
60
64
  - `PMX_CANVAS_PORT` is the agent CLI target; the server's startup port is controlled by `--port`
61
65
  or `PMX_WEB_CANVAS_PORT`.
@@ -213,13 +217,14 @@ Prefer `canvas_query { action: "search" }` over parsing the full layout.
213
217
  loads (e.g. the GitHub Copilot app's embedded WKWebView) can render as a black tile — a host
214
218
  compositor paint race on the nested iframe, **not** a broken node (the session is healthy and
215
219
  `sessionStatus` is `ready`; it renders fine in Chrome, the Codex browser, and for nodes created
216
- live after the panel hydrates). The canvas forces a one-time post-boot repaint remount under
217
- WebKit, which reliably repaints a **single** present-at-load
218
- ext-app but a board with **several** ext-apps present at WebKit panel-load can still black out
219
- (the simultaneous cold-hydration burst overwhelms the WebKit compositor). Recovery is
220
- deterministic: **expand-then-close** any black tile (forces a fresh mount in the fullscreen
221
- overlay, which always paints), or open the workbench in a normal browser (Chrome). Do not
222
- diagnose a healthy app session as a broken node; the durable fix is upstream in the host panel.
220
+ live after the panel hydrates). Under WebKit the canvas auto-recovers: after boot, each
221
+ present-at-load ext-app is remounted through a **serialized, boot-aware queue** (one app at a
222
+ time, each waiting for the previous app's handshake + scene replay), with a watchdog retry for
223
+ iframes that never boot — so multi-app boards converge to painted tiles within a few seconds of
224
+ panel load. In rare cases a tile can still end up black (compositing success is not observable
225
+ from page JS, so retries are bounded); recovery is deterministic: **expand-then-close** the
226
+ black tile (forces a fresh mount in the fullscreen overlay, which always paints), or open the
227
+ workbench in a normal browser (Chrome). Do not diagnose a healthy app session as a broken node.
223
228
  - Graph and json-render standalone surfaces use `display=site` and fill the browser viewport, and
224
229
  reflow on a live window resize in a normal browser. Some single-tab host browsers (e.g. the
225
230
  Codex in-app browser) don't deliver live-resize events, so a resized standalone chart can look
@@ -151,8 +151,13 @@ pmx-canvas serve stop # Stop the daemon for thi
151
151
 
152
152
  `serve --daemon` writes a pid file (`.pmx-canvas/daemon-<port>.pid`) and a log file
153
153
  (`.pmx-canvas/daemon-<port>.log`); the wait flag blocks until `/health` returns OK so a script
154
- can rely on the server being responsive when the command returns. `serve stop` reads the pid
155
- file, sends SIGTERM, and cleans up on exit.
154
+ can rely on the server being responsive when the command returns. The pid file doubles as the
155
+ spawn lock and records the pid immediately, so a starting-but-not-yet-healthy daemon is already
156
+ addressable by `serve stop`, and a failed startup kills the child — no orphans. If the requested
157
+ port already serves a *different* workspace (or a non-canvas app), `serve --daemon` refuses with
158
+ the owner named and a hint to pick another port; it never reports a foreign daemon as "already
159
+ running". `serve stop` reads the pid file, verifies the pid still looks like a canvas daemon
160
+ (recycled pids read as stale), sends SIGTERM, and cleans up on exit.
156
161
 
157
162
  ### Verify workspace identity BEFORE mutating (required)
158
163
 
@@ -170,7 +175,9 @@ file, sends SIGTERM, and cleans up on exit.
170
175
  3. **On mismatch, isolate**: start the intended workspace on an explicit free port —
171
176
  `pmx-canvas serve --daemon --no-open --port=<free-port>` — and target that port. (`PMX_CANVAS_PORT`
172
177
  alone may still attach to an existing `4313` listener; prefer an explicit `--port`.) Then
173
- **re-read `/health`** to confirm the workspace now matches.
178
+ **re-read `/health`** to confirm the workspace now matches. `serve --daemon` also enforces
179
+ this itself: pointed at a port owned by another workspace, it exits with an error naming the
180
+ owner instead of attaching to it.
174
181
  4. **MCP transport exception:** `pmx-canvas --mcp` launched from an incidental host dir may attach to
175
182
  the healthy daemon already on the preferred port when no explicit workspace root is set, so writes
176
183
  land in the visible workbench instead of a hidden fallback workspace. Host adapters should set
@@ -503,6 +510,13 @@ surfaces): `canvas_batch`, `canvas_pin_nodes`, `canvas_screenshot`, `canvas_ax_i
503
510
  `canvas_diff` — deprecated pending a `canvas_snapshot` composite in v0.4; the name collides with
504
511
  the current save-snapshot tool, so the composite cannot land additively).
505
512
 
513
+ `canvas_batch` supports exactly these ops: `node.add`, `node.update`, `node.remove`, `graph.add`,
514
+ `edge.add`, `edge.remove`, `group.create`, `group.add`, `group.remove`, `pin.set`/`pin.add`/
515
+ `pin.remove`, `snapshot.save`, and `arrange`. Anything else fails with "Unsupported canvas_batch
516
+ operation" — batch is non-atomic, so earlier ops stay applied and the response carries
517
+ `failedIndex`. `canvas_screenshot` requires an active automation WebView: call
518
+ `canvas_webview { action: "start" }` first.
519
+
506
520
  > **Removed in v0.3.0 → use the composite instead**: `canvas_open_mcp_app` / `canvas_add_diagram` /
507
521
  > `canvas_build_web_artifact` → **`canvas_app`**; `canvas_webview_start` / `canvas_webview_status` /
508
522
  > `canvas_webview_stop` / `canvas_resize` / `canvas_evaluate` → **`canvas_webview`**;