pmx-canvas 0.4.6 → 0.4.7

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 (70) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/Readme.md +56 -33
  3. package/dist/canvas/global.css +101 -3
  4. package/dist/canvas/index.js +87 -83
  5. package/dist/types/cli/shared.d.ts +13 -1
  6. package/dist/types/client/canvas/CanvasNode.d.ts +11 -0
  7. package/dist/types/client/canvas/EdgeLayer.d.ts +8 -0
  8. package/dist/types/client/nodes/AxStepControls.d.ts +4 -0
  9. package/dist/types/client/nodes/FileNode.d.ts +14 -0
  10. package/dist/types/client/state/canvas-store.d.ts +10 -0
  11. package/dist/types/client/state/intent-bridge.d.ts +9 -0
  12. package/dist/types/server/ax-flow-loop.d.ts +5 -0
  13. package/dist/types/server/ax-interaction.d.ts +21 -2
  14. package/dist/types/server/canvas-operations.d.ts +5 -0
  15. package/dist/types/server/canvas-state.d.ts +8 -0
  16. package/dist/types/server/canvas-validation.d.ts +15 -21
  17. package/dist/types/server/file-content.d.ts +46 -0
  18. package/dist/types/server/html-primitives.d.ts +13 -1
  19. package/dist/types/server/operations/ops/ax-work.d.ts +1 -0
  20. package/dist/types/server/server.d.ts +1 -0
  21. package/dist/types/server/web-artifacts.d.ts +14 -0
  22. package/dist/types/shared/ax-flow.d.ts +51 -0
  23. package/dist/types/shared/node-sizes.d.ts +33 -0
  24. package/docs/cli.md +19 -2
  25. package/docs/environment.md +1 -0
  26. package/docs/http-api.md +17 -0
  27. package/docs/mcp.md +12 -0
  28. package/docs/node-types.md +45 -1
  29. package/docs/screenshot.png +0 -0
  30. package/package.json +2 -2
  31. package/skills/pmx-canvas/SKILL.md +68 -7
  32. package/skills/pmx-canvas/evals/evals.json +54 -3
  33. package/skills/pmx-canvas/references/full-reference.md +11 -6
  34. package/src/cli/commands/history.ts +1 -1
  35. package/src/cli/commands/nodes.ts +2 -2
  36. package/src/cli/commands/query.ts +1 -1
  37. package/src/cli/commands/view.ts +1 -1
  38. package/src/cli/index.ts +6 -0
  39. package/src/cli/shared.ts +34 -5
  40. package/src/client/App.tsx +28 -15
  41. package/src/client/canvas/CanvasNode.tsx +54 -5
  42. package/src/client/canvas/EdgeLayer.tsx +44 -24
  43. package/src/client/canvas/ExpandedNodeOverlay.tsx +11 -0
  44. package/src/client/canvas/auto-fit.ts +10 -1
  45. package/src/client/canvas/use-pan-zoom.ts +16 -1
  46. package/src/client/nodes/AxStepControls.tsx +226 -0
  47. package/src/client/nodes/FileNode.tsx +249 -51
  48. package/src/client/state/canvas-store.ts +19 -0
  49. package/src/client/state/intent-bridge.ts +32 -0
  50. package/src/client/state/sse-bridge.ts +4 -1
  51. package/src/client/theme/global.css +101 -3
  52. package/src/server/ax-flow-loop.ts +114 -0
  53. package/src/server/ax-interaction.ts +299 -4
  54. package/src/server/canvas-operations.ts +44 -17
  55. package/src/server/canvas-state.ts +20 -10
  56. package/src/server/canvas-validation.ts +50 -41
  57. package/src/server/demo-state.json +4605 -523
  58. package/src/server/demo.ts +24 -0
  59. package/src/server/file-content.ts +152 -0
  60. package/src/server/file-watcher.ts +7 -10
  61. package/src/server/html-primitives.ts +636 -0
  62. package/src/server/operations/ops/ax-work.ts +5 -0
  63. package/src/server/operations/ops/nodes.ts +15 -1
  64. package/src/server/operations/ops/viewport.ts +7 -1
  65. package/src/server/server.ts +106 -5
  66. package/src/server/web-artifacts.ts +109 -0
  67. package/src/shared/ax-flow.ts +102 -0
  68. package/src/shared/node-sizes.ts +52 -0
  69. package/dist/types/server/chart-template.d.ts +0 -29
  70. package/src/server/chart-template.ts +0 -448
@@ -19,7 +19,19 @@ export declare function extractGlobalTargetFlags(args: string[]): string[];
19
19
  declare function die(message: string, hint?: string): never;
20
20
  declare function output(data: unknown): void;
21
21
  declare function invokeOperation(name: string, input: Record<string, unknown>): Promise<unknown>;
22
- declare function parseFlags(args: string[]): {
22
+ /**
23
+ * `options.boolFlags` marks flags that are boolean FOR THIS COMMAND. Value
24
+ * flags consume the next token verbatim (so a unified diff beginning with
25
+ * `--- a/file` survives), which makes a flag that is boolean here and
26
+ * value-taking elsewhere ambiguous — `--summary` is boolean in `history` /
27
+ * `layout` / `node get` / `validate spec` but carries text in `node add`.
28
+ * Without this per-command override, `history --summary --limit 5` would
29
+ * swallow `--limit` as the summary's value. The global set below holds flags
30
+ * that are boolean everywhere.
31
+ */
32
+ declare function parseFlags(args: string[], options?: {
33
+ boolFlags?: readonly string[];
34
+ }): {
23
35
  positional: string[];
24
36
  flags: Record<string, string | true>;
25
37
  };
@@ -1,4 +1,15 @@
1
1
  import type { CanvasNodeState } from '../types';
2
+ /**
3
+ * How much to enlarge node chrome (title bar, badges, icons) when zoomed out so
4
+ * it stays legible. Full inverse compensation is capped at 2.2x — but the cap
5
+ * alone ignored the node it is drawn on, so on SHORT nodes the growing bar ate
6
+ * the body: a 116px-tall section label at 46% zoom gave the title bar 72% of the
7
+ * node and left 14px for text that needed 41, so the markdown was simply cut off.
8
+ * The scale is therefore also bounded by the node's own height, keeping the bar
9
+ * under MAX_TITLEBAR_HEIGHT_RATIO of it. Tall nodes are unaffected (their height
10
+ * never binds); pass height 0 for collapsed/auto-height nodes to skip the bound.
11
+ */
12
+ export declare function nodeChromeScale(viewportScale: number, nodeHeight: number, nodeWidth?: number): number;
2
13
  interface CanvasNodeProps {
3
14
  node: CanvasNodeState;
4
15
  children: preact.ComponentChildren;
@@ -1,5 +1,13 @@
1
1
  import type { Signal } from '@preact/signals';
2
2
  import type { CanvasEdge, CanvasNodeState } from '../types';
3
+ /**
4
+ * Edges are drawn in world space, so a 1.5px stroke renders as 0.4 screen px at
5
+ * 26% zoom. Full inverse compensation keeps edge chrome at a constant SCREEN
6
+ * size while zoomed out (standard graph-editor behaviour). Deliberately
7
+ * uncapped — the 2.2 cap used for node chrome still leaves hairlines invisible
8
+ * at overview zoom.
9
+ */
10
+ export declare function edgeChromeScale(scale: number): number;
3
11
  interface EdgeLayerProps {
4
12
  nodes: Signal<Map<string, CanvasNodeState>>;
5
13
  edges: Signal<Map<string, CanvasEdge>>;
@@ -0,0 +1,4 @@
1
+ import type { CanvasNodeState } from '../types';
2
+ export declare function AxStepControls({ node }: {
3
+ node: CanvasNodeState;
4
+ }): import("preact/jsx-runtime").JSX.Element | null;
@@ -1,4 +1,18 @@
1
1
  import type { CanvasNodeState } from '../types';
2
+ export interface DelimitedTable {
3
+ header: string[];
4
+ /** Rectangular: every row padded (short) or truncated (long) to the header width. */
5
+ rows: string[][];
6
+ }
7
+ /**
8
+ * RFC4180-ish delimited-text parser. Quoted fields may contain the delimiter,
9
+ * newlines, and `""` escapes; CRLF, LF and lone CR all end a record; a trailing
10
+ * newline produces no phantom empty row. Ragged records are squared off against
11
+ * the header width instead of throwing.
12
+ */
13
+ export declare function parseDelimitedText(text: string, delimiter: string): DelimitedTable;
14
+ /** Human-readable byte size, e.g. `4.2 MB`. */
15
+ export declare function formatBytes(bytes: number): string;
2
16
  export declare function FileNode({ node, expanded }: {
3
17
  node: CanvasNodeState;
4
18
  expanded?: boolean;
@@ -83,6 +83,16 @@ export declare function applyServerCanvasLayout(layout: Pick<CanvasLayout, 'node
83
83
  * Cancels any in-flight animation. Direct manipulation (pan/zoom gestures)
84
84
  * should use setViewport() instead for instant response.
85
85
  */
86
+ /**
87
+ * Zoom by a factor about the CENTRE of the viewport.
88
+ *
89
+ * The toolbar's +/- used to change `scale` alone and keep `x`/`y`, which anchors
90
+ * the zoom at the world origin — so zooming in visibly slid the board up-left and
91
+ * zooming out pushed it down-right, instead of magnifying what you were looking
92
+ * at. Same correction the pointer-anchored wheel zoom applies, with the viewport
93
+ * centre as the anchor.
94
+ */
95
+ export declare function zoomByFactor(factor: number, duration?: number): void;
86
96
  export declare function animateViewport(target: ViewportState, duration?: number, options?: {
87
97
  recordHistory?: boolean;
88
98
  }): void;
@@ -155,6 +155,15 @@ export declare function updateViewportFromClient(viewport: {
155
155
  }): Promise<{
156
156
  ok: boolean;
157
157
  }>;
158
+ /**
159
+ * Report this window's size WITHOUT touching the stored viewport. The
160
+ * viewport op defaults every omitted coordinate to the server's CURRENT
161
+ * viewport, so a body carrying only the size records the size and leaves the
162
+ * viewport exactly as it was — never echoing a stale client viewport back over
163
+ * the server's. Called on connect and on resize so an agent `fit` has the real
164
+ * window size before the human ever pans or zooms.
165
+ */
166
+ export declare function reportClientViewportSize(): Promise<void>;
158
167
  /** Create a group containing the given child node IDs. */
159
168
  export declare function createGroupFromClient(opts: {
160
169
  title?: string;
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Advance every running flow loop on the canvas. Safe to call on any work-item
3
+ * change: flows that are not running, not ready, or already advanced are no-ops.
4
+ */
5
+ export declare function advanceAxFlowLoops(): void;
@@ -15,10 +15,10 @@
15
15
  * import cycle (canvas-state → canvas-provenance must not pull this in).
16
16
  */
17
17
  import { z } from 'zod';
18
- import type { CanvasNodeState } from './canvas-state.js';
18
+ import type { CanvasEdge, CanvasLayout, CanvasNodeState } from './canvas-state.js';
19
19
  import type { CanvasNodeType } from './canvas-provenance.js';
20
20
  import type { PmxAxApprovalGate, PmxAxElicitation, PmxAxEvent, PmxAxEventKind, PmxAxEvidence, PmxAxEvidenceKind, PmxAxFocusState, PmxAxMode, PmxAxModeRequest, PmxAxReviewAnchorType, PmxAxReviewAnnotation, PmxAxReviewKind, PmxAxReviewRegion, PmxAxReviewSeverity, PmxAxSource, PmxAxSteeringMessage, PmxAxWorkItem, PmxAxWorkItemStatus } from './ax-state.js';
21
- export declare const AX_INTERACTION_TYPES: readonly ["ax.event.record", "ax.steer", "ax.work.create", "ax.work.update", "ax.evidence.add", "ax.approval.request", "ax.approval.resolve", "ax.review.add", "ax.focus.set", "ax.command.invoke", "ax.elicitation.request", "ax.mode.request"];
21
+ export declare const AX_INTERACTION_TYPES: readonly ["ax.event.record", "ax.steer", "ax.work.create", "ax.work.update", "ax.evidence.add", "ax.approval.request", "ax.approval.resolve", "ax.review.add", "ax.focus.set", "ax.command.invoke", "ax.elicitation.request", "ax.mode.request", "ax.flow.materialize"];
22
22
  export type AxInteractionType = (typeof AX_INTERACTION_TYPES)[number];
23
23
  export type AxDeliveryMode = 'record-only' | 'notify-agent' | 'send-to-agent';
24
24
  export interface NodeAxCapabilities {
@@ -59,6 +59,7 @@ declare const InteractionEnvelopeSchema: z.ZodObject<{
59
59
  "ax.command.invoke": "ax.command.invoke";
60
60
  "ax.elicitation.request": "ax.elicitation.request";
61
61
  "ax.mode.request": "ax.mode.request";
62
+ "ax.flow.materialize": "ax.flow.materialize";
62
63
  }>;
63
64
  sourceNodeId: z.ZodString;
64
65
  sourceSurface: z.ZodOptional<z.ZodEnum<{
@@ -98,6 +99,13 @@ export interface AxInteractionInput {
98
99
  */
99
100
  export interface AxInteractionManager {
100
101
  getNode(id: string): CanvasNodeState | undefined;
102
+ /** Canvas read for `ax.flow.materialize` (placement obstacles + stale flow sweep). */
103
+ getLayout(): CanvasLayout;
104
+ addNode(node: CanvasNodeState): void;
105
+ updateNode(id: string, patch: Partial<CanvasNodeState>): void;
106
+ /** Also drops the node's edges — the flow's replace path relies on that. */
107
+ removeNode(id: string): void;
108
+ addEdge(edge: CanvasEdge): boolean;
101
109
  recordAxEvent(input: {
102
110
  kind: PmxAxEventKind;
103
111
  summary: string;
@@ -200,6 +208,17 @@ export interface AxInteractionResult {
200
208
  result: AxInteractionPublicResult;
201
209
  events: AxInteractionEvent[];
202
210
  }
211
+ export interface AxFlowMaterializeInput {
212
+ title?: string;
213
+ steps: Array<{
214
+ title: string;
215
+ detail?: string;
216
+ }>;
217
+ loop?: {
218
+ enabled?: boolean;
219
+ maxRuns?: number;
220
+ };
221
+ }
203
222
  /**
204
223
  * Validate + execute a node-originated AX interaction. Returns the public result
205
224
  * plus the SSE events the caller should emit (accepted/rejected outcome + the
@@ -259,5 +259,10 @@ export declare function createCanvasGraphNode(input: GraphNodeInput): {
259
259
  spec: JsonRenderSpec;
260
260
  node: CanvasNodeState;
261
261
  };
262
+ export declare function setClientViewportSize(width: unknown, height: unknown): void;
263
+ export declare function getClientViewportSize(): {
264
+ width: number;
265
+ height: number;
266
+ } | null;
262
267
  export declare function fitCanvasView(options?: CanvasFitViewOptions): CanvasFitViewResult;
263
268
  export {};
@@ -277,6 +277,14 @@ declare class CanvasStateManager {
277
277
  setTheme(theme: CanvasTheme): CanvasTheme;
278
278
  get contextPinnedNodeIds(): Set<string>;
279
279
  getAxState(): PmxAxState;
280
+ /**
281
+ * Replace the canvas-bound AX partition from a persisted blob, normalized against
282
+ * the current node set — the same operation snapshot restore performs internally
283
+ * (`applyPersistedState`), exposed for the demo seed. Both restore nodes whose
284
+ * `data.axStep.workItemId` only resolves if the work items come back with their
285
+ * original ids, so this preserves them verbatim rather than re-creating them.
286
+ */
287
+ applyPersistedAxState(ax: unknown): void;
280
288
  getAxFocus(): PmxAxFocusState;
281
289
  setAxFocus(nodeIds: string[], options?: {
282
290
  source?: PmxAxSource;
@@ -1,25 +1,5 @@
1
1
  import type { CanvasLayout, CanvasNodeState } from './canvas-state.js';
2
- /**
3
- * Per-type minimum node size (0.4.5 report follow-up): agents frequently
4
- * create nodes with explicit frames far too small for their content — clipped
5
- * markdown, charts squeezed behind inner scrollbars. Explicit sizes below the
6
- * floor are clamped UP at creation (canvas-operations.ts creators);
7
- * `strictSize: true` is the escape hatch for a genuinely small fixed frame.
8
- * Floors sit below every type default and above the point of unreadability.
9
- * Creation-only: later updates are untouched (the browser's drag-resize
10
- * enforces its own 200×100 floor client-side), but `validate` reports any
11
- * node below its floor as an advisory sizeWarning. Absent types (trace,
12
- * group, prompt/response) are intentionally unclamped — trace is small by
13
- * design, groups size to their children.
14
- */
15
- export declare const NODE_MIN_CREATE_SIZES: Partial<Record<CanvasNodeState['type'], {
16
- width: number;
17
- height: number;
18
- }>>;
19
- export declare function clampCreateNodeSize(type: CanvasNodeState['type'], width: number, height: number, strictSize?: boolean): {
20
- width: number;
21
- height: number;
22
- };
2
+ export { NODE_MIN_SIZES, clampCreateNodeSize, nodeMinSize } from '../shared/node-sizes.js';
23
3
  export interface CanvasValidationPair {
24
4
  aId: string;
25
5
  aTitle: string | null;
@@ -41,6 +21,12 @@ export interface CanvasSizeWarning {
41
21
  minWidth: number;
42
22
  minHeight: number;
43
23
  }
24
+ export interface CanvasHiddenEdgeEndpoint {
25
+ edgeId: string;
26
+ nodeId: string;
27
+ nodeTitle: string | null;
28
+ dockPosition: 'left' | 'right';
29
+ }
44
30
  export interface CanvasValidationResult {
45
31
  ok: boolean;
46
32
  collisions: CanvasValidationPair[];
@@ -51,6 +37,13 @@ export interface CanvasValidationResult {
51
37
  from: string;
52
38
  to: string;
53
39
  }>;
40
+ /**
41
+ * Edges whose endpoint node is DOCKED — it renders in the HUD column, not on
42
+ * the canvas, so the edge visually terminates in empty space even though both
43
+ * endpoint IDs resolve (0.4.6 orb feedback #1). Advisory: reported for
44
+ * diagnosis, but does NOT fail `ok` (see the note at the return site).
45
+ */
46
+ hiddenEdgeEndpoints: CanvasHiddenEdgeEndpoint[];
54
47
  /** Nodes below their type's readable minimum (advisory — does not fail `ok`). */
55
48
  sizeWarnings: CanvasSizeWarning[];
56
49
  summary: {
@@ -60,6 +53,7 @@ export interface CanvasValidationResult {
60
53
  containments: number;
61
54
  containmentViolations: number;
62
55
  missingEdgeEndpoints: number;
56
+ hiddenEdgeEndpoints: number;
63
57
  sizeWarnings: number;
64
58
  };
65
59
  }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Shared disk reader for `file` canvas nodes.
3
+ *
4
+ * Both entry points that put a file on the canvas — `buildFileNodeData`
5
+ * (creation) and the file watcher (live updates) — go through here so their
6
+ * notion of "what this file is" can never drift. Reading a file blind with
7
+ * `readFileSync(path, 'utf-8')` turned binaries into mojibake and had no size
8
+ * ceiling at all, so a multi-GB file was pulled into memory as a string.
9
+ */
10
+ /** Text past this many bytes is stored truncated instead of whole. */
11
+ export declare const MAX_FILE_TEXT_BYTES: number;
12
+ export type FileNodeContent =
13
+ /** Missing, unreadable, or not a regular file — the node stays path-backed. */
14
+ {
15
+ kind: 'unavailable';
16
+ } | {
17
+ kind: 'binary';
18
+ byteSize: number;
19
+ mtimeMs: number;
20
+ mimeType?: string;
21
+ } | {
22
+ kind: 'text';
23
+ byteSize: number;
24
+ mtimeMs: number;
25
+ text: string;
26
+ lineCount: number;
27
+ truncated: boolean;
28
+ };
29
+ /**
30
+ * PDF check straight from a path, for the byte route's server-derived
31
+ * Content-Type. Reads only the magic-byte prefix. Any read failure is "not a
32
+ * PDF" — the caller then serves an opaque download rather than an inline type.
33
+ */
34
+ export declare function isPdfFilePath(filePath: string): boolean;
35
+ /**
36
+ * Classify a file and read at most `MAX_FILE_TEXT_BYTES` of it.
37
+ *
38
+ * Never throws: an unreadable path is reported as `unavailable`, matching the
39
+ * pre-existing behavior where a bad path still rendered as a file node.
40
+ */
41
+ export declare function readFileNodeContent(filePath: string): FileNodeContent;
42
+ /**
43
+ * Map a read result onto `file` node data in place, clearing the fields the new
44
+ * state contradicts (a file that turns binary must not keep stale decoded text).
45
+ */
46
+ export declare function applyFileContentToNodeData(data: Record<string, unknown>, content: FileNodeContent): Record<string, unknown>;
@@ -1,4 +1,5 @@
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"];
1
+ import type { AxInteractionType } from './ax-interaction.js';
2
+ 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", "ax-board", "ax-flow"];
2
3
  export type HtmlPrimitiveKind = (typeof HTML_PRIMITIVE_KINDS)[number];
3
4
  export interface HtmlPrimitiveDescriptor {
4
5
  kind: HtmlPrimitiveKind;
@@ -11,6 +12,17 @@ export interface HtmlPrimitiveDescriptor {
11
12
  };
12
13
  dataShape: string;
13
14
  example: Record<string, unknown>;
15
+ /**
16
+ * AX capabilities this kind REQUIRES to function. `html` is AX-opt-in, so a
17
+ * primitive that emits interactions is inert without them — node creation
18
+ * applies this to `data.axCapabilities` (an explicit caller value still wins,
19
+ * and the server clamps both to the `html` type ceiling). Only set it on kinds
20
+ * that actually emit; every other kind stays AX-free.
21
+ */
22
+ axCapabilities?: {
23
+ enabled: true;
24
+ allowed: AxInteractionType[];
25
+ };
14
26
  }
15
27
  export interface HtmlPrimitiveInput {
16
28
  kind: HtmlPrimitiveKind;
@@ -1,2 +1,3 @@
1
+ import '../../ax-flow-loop.js';
1
2
  import { type Operation } from '../types.js';
2
3
  export declare const axWorkOperations: Operation[];
@@ -22,6 +22,7 @@
22
22
  * - POST /api/canvas/json-render -> create a native json-render node
23
23
  * - POST /api/canvas/graph -> create a native graph node
24
24
  * - GET /api/canvas/json-render/view?nodeId=... -> local json-render viewer
25
+ * - GET /api/canvas/file-bytes?nodeId=... -> raw bytes for a file node
25
26
  * - POST /api/canvas/web-artifact -> build bundled HTML artifact + optional canvas node
26
27
  * - GET /api/workbench/events -> SSE event stream
27
28
  * - GET /api/workbench/poll -> proxy-safe polling transport (same events, JSON)
@@ -53,6 +53,20 @@ export interface WebArtifactCanvasBuildResult extends WebArtifactBuildOutput {
53
53
  }
54
54
  export declare function resolveWorkspacePath(pathLike: string, cwd?: string): string;
55
55
  export declare function resolveWebArtifactScriptPath(kind: 'init' | 'bundle'): string;
56
+ export type NodeVersionCheck = {
57
+ ok: true;
58
+ } | {
59
+ ok: false;
60
+ message: string;
61
+ };
62
+ /**
63
+ * Pure preflight comparison used before any package-manager invocation.
64
+ * `detected` is raw `node --version` output ("v20.11.1"), or null when no node
65
+ * executable answered; `required` is a `major.minor` floor; `packageManager` is
66
+ * the pin that imposes it (e.g. "pnpm@11.1.2"). Returns an actionable message
67
+ * instead of throwing so every caller surfaces it on its own error channel.
68
+ */
69
+ export declare function checkWebArtifactNodeVersion(detected: string | null, required: string, packageManager: string): NodeVersionCheck;
56
70
  export declare function executeWebArtifactBuild(input: WebArtifactBuildInput): Promise<WebArtifactBuildOutput>;
57
71
  export declare function openWebArtifactInCanvas(input: {
58
72
  title: string;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * The node-data contract for a materialized AX flow.
3
+ *
4
+ * `ax.flow.materialize` stamps every step node with `data.axStep` and the anchor
5
+ * (first) step with `data.axFlow`. Three consumers read those stamps and none of
6
+ * them owns the shape, so it lives here:
7
+ * - the server loop (`ax-flow-loop.ts`) advances the flow from work-item changes
8
+ * - the browser renders native Start/Done/Blocked + Run loop/Stop controls
9
+ * - snapshots/restore carry the stamps as ordinary node data
10
+ *
11
+ * Node data is `Record<string, unknown>` on both sides, so the readers below are
12
+ * the ONLY way to narrow it: they validate rather than cast, because a stamp can
13
+ * also arrive from a restored snapshot or a hand-edited node.
14
+ */
15
+ /** Hard ceiling on loop passes, whatever a node's stamped `maxRuns` claims. */
16
+ export declare const AX_FLOW_LOOP_HARD_CAP = 20;
17
+ /** One step of a flow, as recorded on the anchor node. */
18
+ export interface AxFlowStepRef {
19
+ /** 1-based position in the flow. */
20
+ index: number;
21
+ nodeId: string;
22
+ workItemId: string;
23
+ title: string;
24
+ }
25
+ export interface AxFlowLoopState {
26
+ /** The loop only advances while this is true; Stop persists `false`. */
27
+ running: boolean;
28
+ /** Completed passes. `run >= maxRuns` ends the loop. */
29
+ run: number;
30
+ maxRuns: number;
31
+ }
32
+ /** `data.axStep` — stamped on EVERY step node, including the anchor. */
33
+ export interface AxFlowStepStamp {
34
+ flowId: string;
35
+ index: number;
36
+ total: number;
37
+ workItemId: string;
38
+ }
39
+ /** `data.axFlow` — stamped on the anchor (first) step node only. */
40
+ export interface AxFlowStamp {
41
+ flowId: string;
42
+ title: string;
43
+ steps: AxFlowStepRef[];
44
+ loop: AxFlowLoopState;
45
+ }
46
+ /** Clamp a stamped `maxRuns` into [1, AX_FLOW_LOOP_HARD_CAP]. */
47
+ export declare function clampAxFlowMaxRuns(value: unknown): number;
48
+ /** Read `data.axStep`, or null when the node carries no valid step stamp. */
49
+ export declare function readAxStep(data: Record<string, unknown>): AxFlowStepStamp | null;
50
+ /** Read `data.axFlow`, or null when the node is not a flow anchor. */
51
+ export declare function readAxFlow(data: Record<string, unknown>): AxFlowStamp | null;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Per-type minimum node size — the readability floor.
3
+ *
4
+ * Agents frequently create nodes with explicit frames far too small for their
5
+ * content (clipped markdown, charts squeezed behind inner scrollbars), so the
6
+ * server clamps undersized explicit sizes UP at creation and `validate` reports
7
+ * anything below the floor.
8
+ *
9
+ * This lives in `shared/` because BOTH sides must honor it. The 0.4.6 report's
10
+ * Finding AA: the server clamped a 200x100 markdown node up to 360x180, and
11
+ * then the browser's DOM auto-fit measured the (short) content and persisted
12
+ * 360x132 — undoing the guarantee on the most common path, a connected
13
+ * workbench. A floor only one side knows about is not a floor.
14
+ *
15
+ * `strictSize: true` is the escape hatch for a deliberately small fixed frame,
16
+ * and it exempts a node from both the creation clamp and the auto-fit floor.
17
+ * Types absent from the table are intentionally unclamped — `trace` is small by
18
+ * design, `group` sizes to its children.
19
+ */
20
+ export declare const NODE_MIN_SIZES: Record<string, {
21
+ width: number;
22
+ height: number;
23
+ }>;
24
+ /** The readability floor for a node type, or undefined when it has none. */
25
+ export declare function nodeMinSize(type: string): {
26
+ width: number;
27
+ height: number;
28
+ } | undefined;
29
+ /** Creation-time clamp: an explicit size below the floor is raised to it. */
30
+ export declare function clampCreateNodeSize(type: string, width: number, height: number, strictSize?: boolean): {
31
+ width: number;
32
+ height: number;
33
+ };
package/docs/cli.md CHANGED
@@ -17,7 +17,7 @@ pmx-canvas --server-url http://127.0.0.1:4750 status # same, by URL
17
17
 
18
18
  ```bash
19
19
  pmx-canvas # Start canvas, open browser
20
- pmx-canvas --demo # Start with the saved dashboard demo board
20
+ pmx-canvas --demo # Start with the showcase demo board
21
21
  pmx-canvas --port=8080 # Custom port
22
22
  pmx-canvas --no-open # Headless (for agents/CI)
23
23
  pmx-canvas --theme=light # dark | light | high-contrast | midnight | sepia | arctic | ember | forest | volt
@@ -119,11 +119,28 @@ pmx-canvas watch --events context-pin,move-end
119
119
  pmx-canvas watch --json --events context-pin --max-events 1
120
120
  ```
121
121
 
122
- ## Focus
122
+ ## Focus and fit
123
123
 
124
124
  ```bash
125
125
  pmx-canvas focus <node-id> # Pan viewport to a node
126
126
  pmx-canvas focus <node-id> --no-pan # Select/raise without panning
127
+ pmx-canvas fit # Fit the viewport to every node
128
+ pmx-canvas fit <id-a> <id-b> # Fit to just these nodes
129
+ ```
130
+
131
+ `fit` sizes itself to the connected browser window, so you rarely need to pass
132
+ `--width` / `--height`. After creating output a human should see, focus a single
133
+ node or fit the new ids — auto-placement is board-relative, not camera-relative.
134
+
135
+ ## Snapshots
136
+
137
+ ```bash
138
+ pmx-canvas snapshot save --name "before-refactor"
139
+ pmx-canvas snapshot list
140
+ pmx-canvas snapshot restore <snapshot-id>
141
+ pmx-canvas snapshot delete <snapshot-id>
142
+ pmx-canvas snapshot gc --keep 20 # Delete all but the newest N
143
+ pmx-canvas diff <snapshot-id> # Compare the canvas against a snapshot
127
144
  ```
128
145
 
129
146
  ## AX context
@@ -29,6 +29,7 @@ optional; defaults are listed.
29
29
  | `AMP_ORB` | server | Set by Amp orb services. When present, the served canvas page tells the browser it is running in an orb, which skips the iframe embed probe, forces `srcdoc` surface rendering when embedded (the orb portal blocks `src`-URL child iframes and the probe is unreliable there), and defaults the event transport to polling instead of SSE (the portal proxy buffers streams; `?transport=sse` overrides for diagnosis). |
30
30
  | `PORT` | server | Portal-assigned service port, honored ONLY when `AMP_ORB` is also set (Amp orbs) — after `--port`, `PMX_WEB_CANVAS_PORT`, and `PMX_CANVAS_PORT`, before the 4313 default. A stray `PORT` in normal shells never changes the port. |
31
31
  | `PMX_CANVAS_DISABLE_BROWSER_OPEN` | server | `1` suppresses auto-opening the browser (used by tests/CI). |
32
+ | `PMX_CANVAS_AUTO_INTENT` | server | `0` turns off the auto-ghost: agent mutations that arrive without an explicit `canvas_intent` signal stop synthesizing a short ghost cursor. Any other value (or unset) leaves it on. |
32
33
  | `PMX_CANVAS_DIST` | server | Explicit client bundle directory to serve instead of the packaged `dist/canvas`. |
33
34
  | `PMX_CANVAS_WEBVIEW_TIMEOUT_MS` | server | Startup timeout for the Bun.WebView automation session. |
34
35
 
package/docs/http-api.md CHANGED
@@ -64,6 +64,23 @@ A node creation request must resolve a `type` — pass it in the body (`{ "type"
64
64
  ... }`) or as a `?type=` query param. An empty / type-less body returns `400`
65
65
  rather than silently creating a markdown node.
66
66
 
67
+ ### File node content and bytes
68
+
69
+ A `file` node reads its file server-side. Text is stored on the node as
70
+ `data.fileContent` (with `data.lineCount`); `data.byteSize` is always set.
71
+ Non-text files are never decoded into mojibake — they carry `data.binary: true`
72
+ with no `fileContent`, plus `data.mimeType` when the type is known
73
+ (`application/pdf` for PDFs). A text file larger than 2 MB is stored truncated
74
+ with `data.truncated: true`.
75
+
76
+ ```bash
77
+ # Raw bytes for a binary file node (PDF viewer, downloads)
78
+ curl http://localhost:4313/api/canvas/file-bytes?nodeId=node-abc123
79
+ ```
80
+
81
+ The byte route is workspace-scoped: a file node whose path resolves outside the
82
+ workspace root returns `403`, and an unknown node id returns `404`.
83
+
67
84
  ## Edges
68
85
 
69
86
  ```bash
package/docs/mcp.md CHANGED
@@ -227,6 +227,18 @@ interaction against the node's effective capabilities.
227
227
  - **Commands:** `canvas_invoke_command` runs a registry command (`pmx.plan`,
228
228
  `pmx.execute`, `pmx.promote-context`, `pmx.summarize`, `pmx.review`); unknown
229
229
  names are rejected and a successful call records a `command` agent-event.
230
+ Commands *record* a signal for the agent — they do not execute anything
231
+ server-side.
232
+ - **`ax.flow.materialize`:** the one interaction that creates canvas nodes, and
233
+ it is deliberately narrow. The caller supplies text only — up to 12 steps with
234
+ a title (<= 120 chars) and optional detail — and the SERVER owns the resulting
235
+ shape: one `markdown` node per step, `flow` edges between them, a dashed
236
+ `references` loop edge when looping, and one work item per step linked to its
237
+ node (so the existing status-chip mirroring shows progress). The surface never
238
+ names a node type, geometry, or node data. Re-materializing replaces the
239
+ previous flow: the source node keeps a manifest of the ids it created, and
240
+ only nodes on that manifest that still carry the matching flow id are removed,
241
+ so a surface can never delete anything it did not create.
230
242
 
231
243
  ## Change notifications
232
244
 
@@ -14,7 +14,7 @@ see [MCP tools](mcp.md), [HTTP API](http-api.md), and [SDK](sdk.md).
14
14
  | `context` | Context cards, token usage, workspace grounding |
15
15
  | `ledger` | Execution ledger summary |
16
16
  | `trace` | Agent trace pills (tool calls, subagent activity) |
17
- | `file` | Live file viewer with auto-update on disk changes |
17
+ | `file` | Live file viewer with auto-update on disk changes — also renders CSV/TSV as tables and PDFs inline |
18
18
  | `diff` | Unified diff viewer: per-file headers, hunk rows, +/− colored lines |
19
19
  | `mermaid` | Mermaid diagram rendered client-side from text (flowchart, sequence, state, …) |
20
20
  | `image` | Image viewer (file paths, data URIs, URLs) |
@@ -50,6 +50,20 @@ automatically via `fs.watch()`.
50
50
  canvas_node({ action: 'add', type: 'file', content: 'src/server/index.ts' })
51
51
  ```
52
52
 
53
+ The same node type handles non-source files, so you never have to pick a
54
+ different type by extension:
55
+
56
+ - **`.csv` / `.tsv`** render as a real table (quoted fields, embedded newlines,
57
+ and escaped quotes are parsed properly) instead of raw delimited text. Very
58
+ long files render a bounded number of rows and say how many were omitted.
59
+ - **`.pdf`** renders in an embedded viewer, served from
60
+ `GET /api/canvas/file-bytes?nodeId=…`. In hosts that block src-URL iframes
61
+ (Amp orb portals) the node offers an "Open PDF" link instead of a blank frame.
62
+ - **Other binary files** show a placeholder with the name and size rather than
63
+ decoding bytes into mojibake.
64
+ - **Text files over 2 MB** render truncated with a banner saying so, so a huge
65
+ log can never exhaust memory.
66
+
53
67
  ## Image nodes
54
68
 
55
69
  Image nodes display local paths, remote URLs, and data URIs. File-backed and
@@ -221,6 +235,36 @@ component gallery, interaction prototype, flowchart, SVG illustration set,
221
235
  presentation, explainer, status report, incident report, triage board, config
222
236
  editor, or prompt tuner.
223
237
 
238
+ Two kinds are **live AX control surfaces** rather than static documents, and are
239
+ the only primitives created with AX enabled:
240
+
241
+ - **`ax-board`** — give the agent tasks, watch their status change as it works,
242
+ send steering, and run a bounded task loop. Task rows read from live AX state,
243
+ so a status the agent sets anywhere shows up here.
244
+ - **`ax-flow`** — the same controls drawn as a task flow with an explicit
245
+ loop-back rail, plus **Materialize to board**: the flow is laid out as real
246
+ canvas nodes joined by `flow` edges (and a dashed `references` loop edge),
247
+ each linked to a work item so the node's status chip tracks progress.
248
+ Re-materializing replaces the previous flow instead of duplicating it.
249
+
250
+ A steer emitted from either surface is *queued* on the timeline — it reaches the
251
+ agent on its next turn rather than waking it. Both stop their loop if the panel
252
+ reloads; the work items are the durable record.
253
+
254
+ ### The flow on ordinary nodes
255
+
256
+ You do not need the panel to drive a flow. A materialized step node carries
257
+ `data.axStep`, and the canvas renders native controls on it — **Start / Done /
258
+ Blocked** on every step, plus **Run loop / Stop** and a steer box on the first
259
+ one. These are canvas chrome, not sandboxed content, so they work on any node
260
+ type carrying the stamp.
261
+
262
+ The native loop runs **server-side**, which is the one real difference from the
263
+ panel: it advances when a step's work item is completed, survives a browser
264
+ reload, and keeps going while the tab is closed. Same bounds either way — it only
265
+ advances while running, stops at its run cap (hard limit 20), and Stop is
266
+ persisted immediately. `blocked` or `cancelled` halts it without disarming.
267
+
224
268
  ```ts
225
269
  canvas_node({
226
270
  action: 'add',
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmx-canvas",
3
- "version": "0.4.6",
3
+ "version": "0.4.7",
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",
@@ -46,7 +46,7 @@
46
46
  "test": "if [ -d tests/unit ]; then PMX_MCP_APP_HOST_STATE_FILE=/tmp/pmx-canvas-test-app-host-state.json PMX_CANVAS_DISABLE_BROWSER_OPEN=1 bun test tests/unit; else echo 'tests/ is not shipped in the npm package — clone the repo to run the suite'; fi",
47
47
  "test:unit": "PMX_MCP_APP_HOST_STATE_FILE=/tmp/pmx-canvas-test-app-host-state.json PMX_CANVAS_DISABLE_BROWSER_OPEN=1 bun test tests/unit",
48
48
  "test:client": "if [ -d tests/client ]; then bun test --preload ./tests/client/setup.ts tests/client; else echo 'tests/ is not shipped in the npm package — clone the repo to run the suite'; fi",
49
- "test:coverage": "PMX_MCP_APP_HOST_STATE_FILE=/tmp/pmx-canvas-test-app-host-state.json bun test tests/unit --coverage --coverage-reporter=text --coverage-reporter=lcov --coverage-dir coverage && bun scripts/check-coverage.ts",
49
+ "test:coverage": "PMX_MCP_APP_HOST_STATE_FILE=/tmp/pmx-canvas-test-app-host-state.json PMX_CANVAS_DISABLE_BROWSER_OPEN=1 bun test tests/unit --coverage --coverage-reporter=text --coverage-reporter=lcov --coverage-dir coverage && bun scripts/check-coverage.ts",
50
50
  "test:web-canvas": "PMX_CANVAS_DISABLE_BROWSER_OPEN=1 bun run build && PMX_CANVAS_DISABLE_BROWSER_OPEN=1 bash scripts/run-playwright.sh",
51
51
  "test:e2e": "bun run test:web-canvas",
52
52
  "test:e2e-cli": "bash scripts/e2e-cli-coverage.sh",