pmx-canvas 0.3.0 → 0.3.1

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 (156) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/dist/canvas/index.js +2 -2
  3. package/dist/types/cli/daemon.d.ts +74 -0
  4. package/dist/types/cli/watch.d.ts +2 -2
  5. package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
  6. package/dist/types/client/canvas/CommandPalette.d.ts +1 -1
  7. package/dist/types/client/canvas/Minimap.d.ts +1 -1
  8. package/dist/types/client/nodes/FileNode.d.ts +1 -1
  9. package/dist/types/client/nodes/ImageNode.d.ts +1 -1
  10. package/dist/types/client/nodes/InlineFormatBar.d.ts +1 -1
  11. package/dist/types/client/nodes/MarkdownNode.d.ts +1 -1
  12. package/dist/types/client/nodes/PromptNode.d.ts +1 -1
  13. package/dist/types/client/nodes/ResponseNode.d.ts +1 -1
  14. package/dist/types/server/canvas-schema.d.ts +1 -1
  15. package/dist/types/server/html-primitives.d.ts +1 -1
  16. package/dist/types/server/index.d.ts +4 -4
  17. package/dist/types/server/operations/index.d.ts +1 -1
  18. package/dist/types/server/operations/ops/ax-read.d.ts +2 -0
  19. package/dist/types/server/operations/ops/canvas-wire.d.ts +2 -0
  20. package/dist/types/server/operations/ops/ext-app.d.ts +2 -0
  21. package/docs/http-api.md +28 -0
  22. package/docs/plans/plan-009-tech-debt-backlog.md +5 -5
  23. package/docs/tech-debt-assessment-2026-07.md +2 -2
  24. package/package.json +5 -2
  25. package/skills/pmx-canvas/SKILL.md +3 -1
  26. package/skills/pmx-canvas/references/full-reference.md +10 -3
  27. package/src/cli/agent.ts +1861 -1548
  28. package/src/cli/daemon.ts +460 -0
  29. package/src/cli/index.ts +63 -326
  30. package/src/cli/watch.ts +2 -10
  31. package/src/client/App.tsx +48 -46
  32. package/src/client/canvas/AttentionHistory.tsx +11 -1
  33. package/src/client/canvas/CanvasNode.tsx +41 -29
  34. package/src/client/canvas/CanvasViewport.tsx +101 -66
  35. package/src/client/canvas/CommandPalette.tsx +61 -27
  36. package/src/client/canvas/ContextMenu.tsx +13 -20
  37. package/src/client/canvas/ContextPinBar.tsx +1 -5
  38. package/src/client/canvas/ContextPinHud.tsx +1 -6
  39. package/src/client/canvas/DockedNode.tsx +4 -4
  40. package/src/client/canvas/EdgeLayer.tsx +37 -36
  41. package/src/client/canvas/ExpandedNodeOverlay.tsx +69 -45
  42. package/src/client/canvas/FocusFieldLayer.tsx +20 -22
  43. package/src/client/canvas/IntentLayer.tsx +31 -14
  44. package/src/client/canvas/Minimap.tsx +11 -16
  45. package/src/client/canvas/SelectionBar.tsx +4 -11
  46. package/src/client/canvas/ShortcutOverlay.tsx +3 -1
  47. package/src/client/canvas/SnapshotPanel.tsx +77 -95
  48. package/src/client/canvas/auto-fit.ts +15 -14
  49. package/src/client/canvas/snap-guides.ts +12 -12
  50. package/src/client/canvas/use-node-resize.ts +1 -5
  51. package/src/client/canvas/use-pan-zoom.ts +25 -26
  52. package/src/client/ext-app/bridge.ts +3 -12
  53. package/src/client/icons.tsx +63 -20
  54. package/src/client/nodes/ContextNode.tsx +14 -25
  55. package/src/client/nodes/ExtAppFrame.tsx +60 -39
  56. package/src/client/nodes/FileNode.tsx +74 -62
  57. package/src/client/nodes/GroupNode.tsx +4 -6
  58. package/src/client/nodes/HtmlNode.tsx +76 -46
  59. package/src/client/nodes/ImageNode.tsx +18 -27
  60. package/src/client/nodes/InlineFormatBar.tsx +4 -21
  61. package/src/client/nodes/InlineMarkdownEditor.tsx +0 -1
  62. package/src/client/nodes/LedgerNode.tsx +10 -4
  63. package/src/client/nodes/MarkdownNode.tsx +3 -10
  64. package/src/client/nodes/McpAppNode.tsx +26 -22
  65. package/src/client/nodes/MdFormatBar.tsx +10 -7
  66. package/src/client/nodes/PromptNode.tsx +23 -51
  67. package/src/client/nodes/ResponseNode.tsx +3 -13
  68. package/src/client/nodes/StatusNode.tsx +5 -9
  69. package/src/client/nodes/StatusSummary.tsx +2 -8
  70. package/src/client/nodes/WebpageNode.tsx +20 -14
  71. package/src/client/nodes/iframe-document-url.ts +25 -16
  72. package/src/client/nodes/image-warnings.ts +1 -7
  73. package/src/client/nodes/md-format.ts +20 -5
  74. package/src/client/state/attention-bridge.ts +4 -9
  75. package/src/client/state/attention-store.ts +1 -7
  76. package/src/client/state/canvas-store.ts +52 -36
  77. package/src/client/state/intent-bridge.ts +176 -112
  78. package/src/client/state/intent-store.ts +4 -1
  79. package/src/client/state/sse-bridge.ts +53 -70
  80. package/src/json-render/catalog.ts +12 -16
  81. package/src/json-render/charts/components.tsx +16 -20
  82. package/src/json-render/charts/extra-components.tsx +8 -16
  83. package/src/json-render/charts/extra-definitions.ts +1 -2
  84. package/src/json-render/charts/tufte-components.tsx +37 -20
  85. package/src/json-render/charts/tufte-definitions.ts +8 -2
  86. package/src/json-render/renderer/index.tsx +42 -22
  87. package/src/json-render/schema.ts +6 -3
  88. package/src/json-render/server.ts +33 -39
  89. package/src/mcp/canvas-access.ts +35 -21
  90. package/src/mcp/server.ts +132 -70
  91. package/src/server/agent-context.ts +63 -36
  92. package/src/server/ax-context.ts +7 -5
  93. package/src/server/ax-interaction.ts +176 -43
  94. package/src/server/ax-state-manager.ts +182 -39
  95. package/src/server/ax-state.ts +142 -47
  96. package/src/server/canvas-db.ts +213 -95
  97. package/src/server/canvas-operations.ts +177 -120
  98. package/src/server/canvas-provenance.ts +1 -4
  99. package/src/server/canvas-schema.ts +454 -73
  100. package/src/server/canvas-serialization.ts +27 -35
  101. package/src/server/canvas-state.ts +150 -58
  102. package/src/server/chart-template.ts +4 -5
  103. package/src/server/code-graph.ts +19 -6
  104. package/src/server/diagram-presets.ts +28 -29
  105. package/src/server/ext-app-lookup.ts +3 -12
  106. package/src/server/html-node-summary.ts +19 -10
  107. package/src/server/html-primitives.ts +326 -97
  108. package/src/server/html-surface.ts +6 -9
  109. package/src/server/image-source.ts +6 -4
  110. package/src/server/index.ts +320 -217
  111. package/src/server/intent-registry.ts +2 -5
  112. package/src/server/mcp-app-candidate.ts +5 -10
  113. package/src/server/mcp-app-host.ts +14 -38
  114. package/src/server/mcp-app-runtime.ts +12 -20
  115. package/src/server/mutation-history.ts +15 -5
  116. package/src/server/operations/composites.ts +1 -3
  117. package/src/server/operations/http.ts +2 -3
  118. package/src/server/operations/index.ts +7 -1
  119. package/src/server/operations/invoker.ts +4 -3
  120. package/src/server/operations/mcp.ts +22 -30
  121. package/src/server/operations/ops/annotation.ts +122 -10
  122. package/src/server/operations/ops/app.ts +98 -73
  123. package/src/server/operations/ops/ax-await.ts +17 -10
  124. package/src/server/operations/ops/ax-read.ts +347 -0
  125. package/src/server/operations/ops/ax-shared.ts +2 -7
  126. package/src/server/operations/ops/ax-state.ts +32 -14
  127. package/src/server/operations/ops/ax-timeline.ts +32 -19
  128. package/src/server/operations/ops/ax-work.ts +54 -37
  129. package/src/server/operations/ops/batch.ts +39 -14
  130. package/src/server/operations/ops/canvas-wire.ts +91 -0
  131. package/src/server/operations/ops/edges.ts +37 -25
  132. package/src/server/operations/ops/ext-app.ts +346 -0
  133. package/src/server/operations/ops/groups.ts +49 -20
  134. package/src/server/operations/ops/intent.ts +18 -12
  135. package/src/server/operations/ops/json-render.ts +239 -98
  136. package/src/server/operations/ops/nodes.ts +298 -109
  137. package/src/server/operations/ops/query.ts +46 -28
  138. package/src/server/operations/ops/snapshots.ts +35 -26
  139. package/src/server/operations/ops/validate.ts +2 -1
  140. package/src/server/operations/ops/viewport.ts +60 -16
  141. package/src/server/operations/ops/webview.ts +44 -18
  142. package/src/server/operations/registry.ts +2 -3
  143. package/src/server/operations/types.ts +7 -5
  144. package/src/server/operations/webview-runner.ts +1 -3
  145. package/src/server/placement.ts +8 -18
  146. package/src/server/server.ts +108 -1027
  147. package/src/server/spatial-analysis.ts +39 -25
  148. package/src/server/trace-manager.ts +3 -8
  149. package/src/server/web-artifacts.ts +23 -27
  150. package/src/server/webpage-node.ts +5 -13
  151. package/src/shared/auto-arrange.ts +12 -5
  152. package/src/shared/content-height-reporter.ts +8 -6
  153. package/src/shared/ext-app-tool-result.ts +2 -6
  154. package/src/shared/placement.ts +1 -4
  155. package/src/shared/semantic-attention.ts +39 -37
  156. package/src/shared/surface.ts +8 -4
@@ -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 {};
@@ -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[];
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
@@ -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.1",
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",
@@ -55,7 +55,9 @@ Both surfaces report `workspace`. It must match the intended workspace root.
55
55
 
56
56
  - If `responsive: true` but `pidRunning: false`, treat the listener as potentially stale.
57
57
  - On mismatch, do not mutate. Start the intended workspace on an explicit free port:
58
- `pmx-canvas serve --daemon --no-open --port=<free-port>`.
58
+ `pmx-canvas serve --daemon --no-open --port=<free-port>`. (`serve --daemon` enforces this
59
+ itself: pointed at a port owned by another workspace, it refuses with the owner named instead
60
+ of reporting "already running".)
59
61
  - Target that port and re-check `/health`.
60
62
  - `PMX_CANVAS_PORT` is the agent CLI target; the server's startup port is controlled by `--port`
61
63
  or `PMX_WEB_CANVAS_PORT`.
@@ -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