pmx-canvas 0.4.6 → 0.4.8
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.
- package/CHANGELOG.md +79 -0
- package/Readme.md +56 -33
- package/dist/canvas/global.css +101 -3
- package/dist/canvas/index.js +93 -89
- package/dist/types/cli/shared.d.ts +13 -1
- package/dist/types/client/canvas/CanvasNode.d.ts +11 -0
- package/dist/types/client/canvas/EdgeLayer.d.ts +8 -0
- package/dist/types/client/nodes/AxStepControls.d.ts +4 -0
- package/dist/types/client/nodes/ExtAppFrame.d.ts +32 -0
- package/dist/types/client/nodes/FileNode.d.ts +14 -0
- package/dist/types/client/state/canvas-store.d.ts +10 -0
- package/dist/types/client/state/intent-bridge.d.ts +9 -0
- package/dist/types/server/ax-flow-loop.d.ts +5 -0
- package/dist/types/server/ax-interaction.d.ts +21 -2
- package/dist/types/server/canvas-db.d.ts +14 -0
- package/dist/types/server/canvas-operations.d.ts +5 -0
- package/dist/types/server/canvas-state.d.ts +8 -0
- package/dist/types/server/canvas-validation.d.ts +15 -21
- package/dist/types/server/file-content.d.ts +46 -0
- package/dist/types/server/html-primitives.d.ts +13 -1
- package/dist/types/server/operations/ops/ax-work.d.ts +1 -0
- package/dist/types/server/server.d.ts +1 -0
- package/dist/types/server/web-artifacts.d.ts +14 -0
- package/dist/types/shared/ax-flow.d.ts +51 -0
- package/dist/types/shared/node-sizes.d.ts +33 -0
- package/docs/RELEASE.md +48 -0
- package/docs/cli.md +19 -2
- package/docs/environment.md +1 -0
- package/docs/http-api.md +17 -0
- package/docs/mcp.md +12 -0
- package/docs/node-types.md +45 -1
- package/docs/screenshot.png +0 -0
- package/package.json +2 -2
- package/skills/pmx-canvas/SKILL.md +74 -9
- package/skills/pmx-canvas/evals/evals.json +54 -3
- package/skills/pmx-canvas/references/full-reference.md +11 -6
- package/skills/pmx-canvas-testing/SKILL.md +40 -0
- package/src/cli/commands/history.ts +1 -1
- package/src/cli/commands/nodes.ts +2 -2
- package/src/cli/commands/query.ts +1 -1
- package/src/cli/commands/view.ts +1 -1
- package/src/cli/index.ts +6 -0
- package/src/cli/shared.ts +34 -5
- package/src/client/App.tsx +28 -15
- package/src/client/canvas/CanvasNode.tsx +54 -5
- package/src/client/canvas/EdgeLayer.tsx +44 -24
- package/src/client/canvas/ExpandedNodeOverlay.tsx +11 -0
- package/src/client/canvas/auto-fit.ts +10 -1
- package/src/client/canvas/use-pan-zoom.ts +16 -1
- package/src/client/nodes/AxStepControls.tsx +226 -0
- package/src/client/nodes/ExtAppFrame.tsx +79 -2
- package/src/client/nodes/FileNode.tsx +249 -51
- package/src/client/state/canvas-store.ts +19 -0
- package/src/client/state/intent-bridge.ts +32 -0
- package/src/client/state/sse-bridge.ts +4 -1
- package/src/client/theme/global.css +101 -3
- package/src/server/ax-flow-loop.ts +114 -0
- package/src/server/ax-interaction.ts +299 -4
- package/src/server/canvas-db.ts +138 -32
- package/src/server/canvas-operations.ts +44 -17
- package/src/server/canvas-state.ts +20 -10
- package/src/server/canvas-validation.ts +50 -41
- package/src/server/demo-state.json +4605 -523
- package/src/server/demo.ts +24 -0
- package/src/server/file-content.ts +152 -0
- package/src/server/file-watcher.ts +7 -10
- package/src/server/html-primitives.ts +636 -0
- package/src/server/operations/ops/ax-work.ts +5 -0
- package/src/server/operations/ops/nodes.ts +15 -1
- package/src/server/operations/ops/viewport.ts +7 -1
- package/src/server/server.ts +106 -5
- package/src/server/web-artifacts.ts +109 -0
- package/src/shared/ax-flow.ts +102 -0
- package/src/shared/node-sizes.ts +52 -0
- package/dist/types/server/chart-template.d.ts +0 -29
- 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
|
-
|
|
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>>;
|
|
@@ -18,6 +18,38 @@ interface ExtAppHostDimensionsTarget {
|
|
|
18
18
|
* everywhere we can test (Chrome / Codex / Playwright).
|
|
19
19
|
*/
|
|
20
20
|
export declare function isWebKitOnlyHost(userAgent: string): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Finding N (0.4.7 report): the GitHub Copilot panel's WKWebView reports
|
|
23
|
+
* `document.visibilityState === 'hidden'` CONTINUOUSLY — the workbench document
|
|
24
|
+
* never transitions to 'visible' even with the panel open and on-screen. Two
|
|
25
|
+
* things break as a result: the `visibilitychange` re-arm below can never fire
|
|
26
|
+
* (there is no transition to listen for), and `paint-ok` becomes a false green —
|
|
27
|
+
* the app document happily answers the double-rAF paint probe while the
|
|
28
|
+
* compositor still shows black. The instrumented trail from that session carries
|
|
29
|
+
* `visibility: "hidden"` on essentially every recovery event.
|
|
30
|
+
*
|
|
31
|
+
* So: when the document CLAIMS hidden but the frame is demonstrably on-screen,
|
|
32
|
+
* do not trust the claim — spend one more recovery round as if visible. A host
|
|
33
|
+
* that ever reports 'visible' has a working signal and is left to the
|
|
34
|
+
* visibilitychange path, which keeps a genuinely backgrounded panel quiet.
|
|
35
|
+
*/
|
|
36
|
+
export declare function shouldAssumeVisibleRearm(options: {
|
|
37
|
+
userAgent: string;
|
|
38
|
+
visibilityState: string;
|
|
39
|
+
rect: {
|
|
40
|
+
top: number;
|
|
41
|
+
left: number;
|
|
42
|
+
bottom: number;
|
|
43
|
+
right: number;
|
|
44
|
+
width: number;
|
|
45
|
+
height: number;
|
|
46
|
+
};
|
|
47
|
+
viewport: {
|
|
48
|
+
width: number;
|
|
49
|
+
height: number;
|
|
50
|
+
};
|
|
51
|
+
alreadyRearmed: boolean;
|
|
52
|
+
}): boolean;
|
|
21
53
|
export declare const WEBKIT_REMOUNT_SETTLE_MS = 1000;
|
|
22
54
|
export interface WebkitRemountTask {
|
|
23
55
|
/** Perform the remount. Return false if the node no longer needs it (skips the boot wait). */
|
|
@@ -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;
|
|
@@ -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
|
|
@@ -22,6 +22,20 @@ export interface PersistedCanvasState {
|
|
|
22
22
|
}
|
|
23
23
|
export declare function openCanvasDb(dbPath: string): Database;
|
|
24
24
|
export declare function checkpointCanvasDb(db: Database): void;
|
|
25
|
+
/**
|
|
26
|
+
* Delete blob rows nothing references any more. Blobs are content-addressed and
|
|
27
|
+
* written with INSERT OR IGNORE, so editing a node's html supersedes its old blob
|
|
28
|
+
* and leaves it orphaned — without this, `canvas.db` (which is git-committable)
|
|
29
|
+
* would grow without bound once externalization applies to ordinary html nodes.
|
|
30
|
+
*
|
|
31
|
+
* References are collected by scanning the raw persisted JSON for sha256 fields
|
|
32
|
+
* rather than parsing every node. That deliberately OVER-approximates: a stray
|
|
33
|
+
* 64-hex string keeps a blob alive that could have been dropped, which wastes a
|
|
34
|
+
* little space. The opposite error would delete live content, so the scan is
|
|
35
|
+
* biased to the safe side on purpose. Snapshot rows are scanned too — a snapshot
|
|
36
|
+
* restore must still find its blobs.
|
|
37
|
+
*/
|
|
38
|
+
export declare function gcBlobsInDB(db: Database): number;
|
|
25
39
|
export declare function finalizeCanvasDbForClose(db: Database): void;
|
|
26
40
|
export declare function saveStateToDB(db: Database, state: PersistedCanvasState): void;
|
|
27
41
|
/** Check if the DB has been populated with canvas state at least once. */
|
|
@@ -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
|
-
|
|
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;
|
|
@@ -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/RELEASE.md
CHANGED
|
@@ -44,6 +44,25 @@ discovery with a `.esm.preflight` loader error (ERR-20260508-001).
|
|
|
44
44
|
and exercises the CLI flows from
|
|
45
45
|
[`docs/evals/e2e-cli-coverage.md`](evals/e2e-cli-coverage.md).
|
|
46
46
|
|
|
47
|
+
**Bump the pinned version examples.** `Readme.md` pins an exact version in
|
|
48
|
+
its install/orb-setup snippets *because* it tells readers to pin. Three
|
|
49
|
+
releases in a row shipped pointing at the previous version — twice the
|
|
50
|
+
surrounding prose described features that pin does not have, so a reader
|
|
51
|
+
following it copy-pasted a broken setup.
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
grep -rn "pmx-canvas@0\." Readme.md docs/ # every hit must be the version you are about to ship
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**If this release touches CI or the release workflows, review that as
|
|
58
|
+
release content.** A changed gate gets its first real run *on this
|
|
59
|
+
release*: `e2e-cli` once launched Playwright without installing the
|
|
60
|
+
browser, the publish gate once queried `GITHUB_SHA` (the tag object, not
|
|
61
|
+
the commit) so it would have blocked every annotated tag, and
|
|
62
|
+
`test:coverage` once lacked `PMX_CANVAS_DISABLE_BROWSER_OPEN=1` so it
|
|
63
|
+
failed *and* opened a real browser. Ask what happens on a fresh runner
|
|
64
|
+
and on a tag event, not just on your machine.
|
|
65
|
+
|
|
47
66
|
## Versioning
|
|
48
67
|
|
|
49
68
|
Semantic versioning, in 0.x semver honestly (see `docs/api-stability.md`).
|
|
@@ -87,6 +106,23 @@ Don't ship a release without a CHANGELOG entry. The GitHub release
|
|
|
87
106
|
notes file (`/tmp/pmx-canvas-vX.Y.Z-release-notes.md` by convention)
|
|
88
107
|
expands on the CHANGELOG with examples and migration notes.
|
|
89
108
|
|
|
109
|
+
**Check the entries are under a NEW heading.** Feature commits land bullets
|
|
110
|
+
at the top of the file, and three times in the 0.3.x–0.4.x cycles they were
|
|
111
|
+
appended under the *already-published* section instead — so the release
|
|
112
|
+
notes shipped empty while the previous release's published history claimed
|
|
113
|
+
features it never had. Before tagging:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
# every bullet added since the last tag must sit above the last released heading
|
|
117
|
+
git diff v<PREVIOUS>..HEAD -- CHANGELOG.md | grep '^+' | head -40
|
|
118
|
+
grep -n '^## \[' CHANGELOG.md | head -3
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Same rule in reverse: never retro-edit a published section to describe new
|
|
122
|
+
behavior (a theme's colour was changed in place once, so the released notes
|
|
123
|
+
described a build that never shipped it). Correct it forward under the new
|
|
124
|
+
version instead.
|
|
125
|
+
|
|
90
126
|
**Release-blocker gate:** a breaking change without a `### Breaking`
|
|
91
127
|
CHANGELOG entry blocks the release (`docs/api-stability.md`). Check the
|
|
92
128
|
diff against the previous tag for removed/renamed public surface — MCP
|
|
@@ -95,6 +131,18 @@ anything broke and isn't named under `### Breaking`, add the entry first.
|
|
|
95
131
|
|
|
96
132
|
## Tag → publish
|
|
97
133
|
|
|
134
|
+
**Immediately before tagging, re-read the Test CI conclusion — never assert it
|
|
135
|
+
from memory.** v0.3.2 was tagged and published while its Test run had concluded
|
|
136
|
+
*failure*: an interruption landed between the watch finishing and the tag step,
|
|
137
|
+
and "CI was green" was recalled rather than checked. A `gh run watch` exiting 0
|
|
138
|
+
is only evidence if you actually read its output.
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
gh run view <run-id> --json status,conclusion -q '.status + " / " + .conclusion'
|
|
142
|
+
gh run view <run-id> --json jobs -q '.jobs[] | .name + ": " + .conclusion'
|
|
143
|
+
# every job must read `success` — a matrix job added in THIS release has never run green before
|
|
144
|
+
```
|
|
145
|
+
|
|
98
146
|
The publish workflow ([`/.github/workflows/publish.yml`](../.github/workflows/publish.yml))
|
|
99
147
|
triggers on tags matching `v*`:
|
|
100
148
|
|
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
|
|
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
|
package/docs/environment.md
CHANGED
|
@@ -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
|
|