pmx-canvas 0.3.4 → 0.4.0
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 +53 -0
- package/Readme.md +11 -3
- package/dist/canvas/index.js +67 -67
- package/dist/types/client/index.d.ts +1 -1
- package/dist/types/client/nodes/ExtAppFrame.d.ts +10 -0
- package/dist/types/client/state/node-factory.d.ts +29 -0
- package/dist/types/client/uuid-polyfill.d.ts +0 -0
- package/dist/types/server/canvas-db.d.ts +0 -1
- package/dist/types/server/canvas-state.d.ts +14 -15
- package/dist/types/server/operations/composites.d.ts +6 -21
- package/dist/types/server/server.d.ts +1 -0
- package/docs/cli.md +5 -0
- package/docs/environment.md +2 -2
- package/docs/http-api.md +25 -1
- package/docs/mcp.md +21 -30
- package/docs/plans/plan-009-tech-debt-backlog.md +13 -12
- package/docs/screenshot.png +0 -0
- package/docs/tech-debt-assessment-2026-06.md +5 -5
- package/package.json +7 -7
- package/skills/pmx-canvas/SKILL.md +16 -6
- package/skills/pmx-canvas/evals/evals.json +2 -2
- package/skills/pmx-canvas/references/full-reference.md +17 -15
- package/src/cli/commands/history.ts +1 -1
- package/src/cli/commands/nodes.ts +31 -1
- package/src/cli/index.ts +8 -2
- package/src/cli/shared.ts +1 -0
- package/src/client/index.tsx +2 -0
- package/src/client/nodes/ExtAppFrame.tsx +17 -2
- package/src/client/state/node-factory.ts +72 -0
- package/src/client/state/sse-bridge.ts +209 -86
- package/src/client/uuid-polyfill.ts +14 -0
- package/src/server/canvas-db.ts +0 -8
- package/src/server/canvas-schema.ts +2 -8
- package/src/server/canvas-state.ts +51 -151
- package/src/server/index.ts +3 -9
- package/src/server/operations/composites.ts +24 -21
- package/src/server/operations/ops/snapshots.ts +6 -7
- package/src/server/operations/registry.ts +4 -1
- package/src/server/server.ts +170 -74
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
import './uuid-polyfill';
|
|
@@ -51,6 +51,16 @@ export declare function buildExtAppAxBridgeScript(axToken: string, nodeId: strin
|
|
|
51
51
|
* the host also checks event.source against the live iframe.
|
|
52
52
|
*/
|
|
53
53
|
export declare function buildExtAppBootBeaconScript(frameToken: string, nodeId: string): string;
|
|
54
|
+
/**
|
|
55
|
+
* Base styles injected into every ext-app document (Finding Q, 0.3.4 report):
|
|
56
|
+
* apps aspect-fit their drawn surface to the frame width, and on narrow/tall
|
|
57
|
+
* tiles the leftover region shows the app document's own body background —
|
|
58
|
+
* pure black for the hosted Excalidraw bundle, in every engine. Making the
|
|
59
|
+
* app document transparent lets the host iframe's theme background
|
|
60
|
+
* (var(--c-panel)) show through the letterbox instead. Background only — app
|
|
61
|
+
* content, its drawn scene, and its own foreground styling are untouched.
|
|
62
|
+
*/
|
|
63
|
+
export declare function buildExtAppSurfaceBaseStyles(): string;
|
|
54
64
|
export declare function injectExtAppAxBridgeScript(html: string, axBridgeScript: string): string;
|
|
55
65
|
export declare function resolveExtAppContainerDimensions(target: ExtAppHostDimensionsTarget | null | undefined, fallback: {
|
|
56
66
|
width: number;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { CanvasNodeState } from '../types';
|
|
2
|
+
export declare const DEFAULT_POSITIONS: Record<CanvasNodeState['type'], {
|
|
3
|
+
x: number;
|
|
4
|
+
y: number;
|
|
5
|
+
w: number;
|
|
6
|
+
h: number;
|
|
7
|
+
}> & Record<'prompt' | 'response', {
|
|
8
|
+
x: number;
|
|
9
|
+
y: number;
|
|
10
|
+
w: number;
|
|
11
|
+
h: number;
|
|
12
|
+
}>;
|
|
13
|
+
/**
|
|
14
|
+
* Build a canvas node with the shared client defaults. `position`/`size`
|
|
15
|
+
* default to the type's DEFAULT_POSITIONS entry; callers that compute
|
|
16
|
+
* placement (auto-placement, event-supplied geometry) pass explicit values.
|
|
17
|
+
* Status nodes sit at zIndex 0 (background chrome), everything else at 1.
|
|
18
|
+
*/
|
|
19
|
+
export declare function makeNodeState(id: string, type: CanvasNodeState['type'], data: Record<string, unknown>, options?: {
|
|
20
|
+
position?: {
|
|
21
|
+
x: number;
|
|
22
|
+
y: number;
|
|
23
|
+
};
|
|
24
|
+
size?: {
|
|
25
|
+
width: number;
|
|
26
|
+
height: number;
|
|
27
|
+
};
|
|
28
|
+
dockPosition?: 'left' | 'right' | null;
|
|
29
|
+
}): CanvasNodeState;
|
|
File without changes
|
|
@@ -35,7 +35,6 @@ export declare function listSnapshotsFromDB(db: Database, options?: CanvasSnapsh
|
|
|
35
35
|
export declare function deleteSnapshotFromDB(db: Database, id: string): boolean;
|
|
36
36
|
export declare function writeBlobToDB(db: Database, sha256: string, jsonValue: string): number;
|
|
37
37
|
export declare function readBlobFromDB(db: Database, sha256: string): string | null;
|
|
38
|
-
export declare function hasBlobInDB(db: Database, sha256: string): boolean;
|
|
39
38
|
export interface AxTimelineQuery {
|
|
40
39
|
limit?: number;
|
|
41
40
|
sessionId?: string;
|
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Persistence: canvas state auto-saves to `.pmx-canvas/canvas.db` (SQLite WAL mode)
|
|
9
9
|
* in the workspace root on every mutation (debounced). Auto-loads on `loadFromDisk()`.
|
|
10
|
-
* Legacy `.pmx-canvas/state.json
|
|
10
|
+
* Legacy pre-0.2 formats (`.pmx-canvas/state.json`, `.pmx-canvas.json`,
|
|
11
|
+
* `.pmx-canvas-snapshots/`, loose blob files) are no longer imported as of 0.4.0 —
|
|
12
|
+
* the one-shot boot migration into SQLite was retired.
|
|
11
13
|
*/
|
|
12
14
|
import { type PersistedCanvasState, type CanvasTheme, type AxTimelineQuery } from './canvas-db.js';
|
|
13
15
|
import { type PmxAxActivityKind, type PmxAxElicitation, type PmxAxModeRequest, type PmxAxMode, type PmxAxCommandDescriptor, type PmxAxPolicy, type PmxAxFocusState, type PmxAxSource, type PmxAxState, type PmxAxWorkItem, type PmxAxWorkItemStatus, type PmxAxApprovalGate, type PmxAxReviewAnnotation, type PmxAxReviewKind, type PmxAxReviewSeverity, type PmxAxReviewStatus, type PmxAxReviewAnchorType, type PmxAxReviewRegion, type PmxAxEvent, type PmxAxEventKind, type PmxAxEvidence, type PmxAxEvidenceKind, type PmxAxSteeringMessage, type PmxAxHostCapability, type PmxAxTimelineSummary } from './ax-state.js';
|
|
@@ -155,7 +157,7 @@ declare class CanvasStateManager {
|
|
|
155
157
|
private notifyChange;
|
|
156
158
|
private _mutationRecorder;
|
|
157
159
|
private _suppressRecordingDepth;
|
|
158
|
-
/** Register
|
|
160
|
+
/** Register THE mutation recorder (single slot, last-write-wins — see note above). */
|
|
159
161
|
onMutation(cb: (info: MutationRecordInfo) => void): void;
|
|
160
162
|
/** Run a function with mutation recording suppressed (for undo/redo replay and computed edges). */
|
|
161
163
|
withSuppressedRecording(fn: () => void): void;
|
|
@@ -171,7 +173,6 @@ declare class CanvasStateManager {
|
|
|
171
173
|
private translateGroupChildren;
|
|
172
174
|
private recomputeParentGroupBounds;
|
|
173
175
|
private compactGroupChildren;
|
|
174
|
-
private _stateFilePath;
|
|
175
176
|
private _db;
|
|
176
177
|
private _saveTimer;
|
|
177
178
|
/** Workspace root backing persistence and workspace-relative path resolution. */
|
|
@@ -188,20 +189,9 @@ declare class CanvasStateManager {
|
|
|
188
189
|
isBlobReference(value: unknown): value is PersistedBlobRef;
|
|
189
190
|
resolveBlobReference(value: unknown): unknown;
|
|
190
191
|
private externalizePersistedStateBlobs;
|
|
191
|
-
/**
|
|
192
|
-
* One-time migration: rename files from the pre-consolidation layout
|
|
193
|
-
* (`.pmx-canvas.json` + `.pmx-canvas-snapshots/`) into `.pmx-canvas/`.
|
|
194
|
-
* No-op when the new layout already exists.
|
|
195
|
-
*/
|
|
196
|
-
private migrateLegacyLayout;
|
|
197
|
-
/**
|
|
198
|
-
* One-time migration: import state.json + snapshot JSON files + blob files
|
|
199
|
-
* into the SQLite database. Renames originals to `.bak`.
|
|
200
|
-
*/
|
|
201
|
-
private migrateJsonToSqlite;
|
|
202
192
|
getWorkspaceRoot(): string;
|
|
203
193
|
private emptyPersistedState;
|
|
204
|
-
/** Load canvas state from SQLite
|
|
194
|
+
/** Load canvas state from SQLite. Call once on server startup. */
|
|
205
195
|
loadFromDisk(options?: LoadFromDiskOptions): boolean;
|
|
206
196
|
/**
|
|
207
197
|
* Whether this workspace's canvas DB already holds saved state. Used to gate
|
|
@@ -215,6 +205,15 @@ declare class CanvasStateManager {
|
|
|
215
205
|
flushToDisk(): void;
|
|
216
206
|
/** Write current state to SQLite immediately. */
|
|
217
207
|
private saveToDisk;
|
|
208
|
+
private _lastPersistenceError;
|
|
209
|
+
/** Health view of state persistence: ok until a save fails, ok again after the next success. */
|
|
210
|
+
get persistenceHealth(): {
|
|
211
|
+
ok: boolean;
|
|
212
|
+
lastError: {
|
|
213
|
+
message: string;
|
|
214
|
+
at: string;
|
|
215
|
+
} | null;
|
|
216
|
+
};
|
|
218
217
|
/** Close the SQLite database cleanly. Call on server shutdown. */
|
|
219
218
|
close(): void;
|
|
220
219
|
private get snapshotsDir();
|
|
@@ -16,27 +16,12 @@
|
|
|
16
16
|
* operation (plan-005 slices 1–7 + plan-008 Wave 1) that is now reachable ONLY
|
|
17
17
|
* through its composite (or `canvas_batch`).
|
|
18
18
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* Wave 3 via runner injection; `canvas_app` shipped in Wave 4 — open-mcp-app /
|
|
26
|
-
* diagram / build-artifact. Wave 5 folded the last three legacy tools
|
|
27
|
-
* deprecate-only — NO per-action input-injection mechanism was needed:
|
|
28
|
-
* `canvas_add_html_node` / `canvas_add_html_primitive` → `canvas_node` action
|
|
29
|
-
* "add" (type:"html" [+ primitive]); `canvas_refresh_webpage_node` → `canvas_node`
|
|
30
|
-
* action "update" (refresh:true). `canvas_screenshot` stays standalone — it
|
|
31
|
-
* returns a binary image payload the composite/registry JSON wire shape does not
|
|
32
|
-
* model.)
|
|
33
|
-
*
|
|
34
|
-
* Not shipped here: the `canvas_snapshot` composite (plan-006 #7). Its target
|
|
35
|
-
* name is ALREADY a legacy standalone tool (the save-snapshot tool, op
|
|
36
|
-
* `snapshot.save`), so it cannot be added additively without a name clash, and
|
|
37
|
-
* repurposing `canvas_snapshot` to be action-discriminated now would break
|
|
38
|
-
* existing callers. It lands in v0.4, in the same change that removes the 6
|
|
39
|
-
* kept-but-deprecated legacy snapshot tools and frees the name.
|
|
19
|
+
* v0.4.0 completed the fold: the `canvas_snapshot` composite shipped (its name
|
|
20
|
+
* was held by the legacy save-snapshot standalone through v0.3.x, which — with
|
|
21
|
+
* the other 5 snapshot standalones — was deprecated in 0.3.0 and removed here
|
|
22
|
+
* per docs/api-stability.md's deprecate-one-minor-before-removal rule).
|
|
23
|
+
* `canvas_screenshot` stays standalone — it returns a binary image payload the
|
|
24
|
+
* composite/registry JSON wire shape does not model.
|
|
40
25
|
*
|
|
41
26
|
* This module must never import server.ts or index.ts.
|
|
42
27
|
*/
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
* - GET /api/canvas/json-render/view?nodeId=... -> local json-render viewer
|
|
25
25
|
* - POST /api/canvas/web-artifact -> build bundled HTML artifact + optional canvas node
|
|
26
26
|
* - GET /api/workbench/events -> SSE event stream
|
|
27
|
+
* - GET /api/workbench/poll -> proxy-safe polling transport (same events, JSON)
|
|
27
28
|
* - GET /api/workbench/state -> workbench state snapshot
|
|
28
29
|
* - POST /api/workbench/intent -> workbench intents
|
|
29
30
|
* - GET /api/workbench/webview -> Bun.WebView automation status
|
package/docs/cli.md
CHANGED
|
@@ -51,6 +51,11 @@ pmx-canvas node add --help --type webpage --json
|
|
|
51
51
|
pmx-canvas external-app add --kind excalidraw --title "Diagram"
|
|
52
52
|
|
|
53
53
|
pmx-canvas edge add --from-search "DVT O3 — GitOps" --to-search "deep work trend" --type relation
|
|
54
|
+
|
|
55
|
+
# Updates return a compact { ok, id, position, size, updatedFields } envelope;
|
|
56
|
+
# pass --full for the complete node payload (ext-app payloads can be huge).
|
|
57
|
+
pmx-canvas node update <node-id> --width 840 --height 620
|
|
58
|
+
pmx-canvas node update <node-id> --title "Renamed" --full
|
|
54
59
|
```
|
|
55
60
|
|
|
56
61
|
`--from-search` / `--to-search` must each resolve to exactly one node — broad
|
package/docs/environment.md
CHANGED
|
@@ -8,7 +8,7 @@ optional; defaults are listed.
|
|
|
8
8
|
| Variable | Read by | Effect |
|
|
9
9
|
|---|---|---|
|
|
10
10
|
| `PMX_WEB_CANVAS_PORT` | server, CLI `serve` | Preferred server port (default `4313`). CLI `--port` wins over it. |
|
|
11
|
-
| `PMX_CANVAS_PORT` |
|
|
11
|
+
| `PMX_CANVAS_PORT` | all entry points | Client-side target port for CLI commands and the `--mcp` lookup; as of 0.4.0 the server also falls back to it when `--port`/`PMX_WEB_CANVAS_PORT` are unset, so one var can drive the whole stack. |
|
|
12
12
|
| `PMX_CANVAS_URL` | agent CLI, MCP transport | Full base URL target (wins over `PMX_CANVAS_PORT`). |
|
|
13
13
|
| `PMX_CANVAS_WORKSPACE_ROOT` | server, MCP transport | Pins the workspace root for daemon binds and the MCP same-workspace lookup, overriding the launch cwd. Set it when a host spawns `--mcp` from an incidental dir. |
|
|
14
14
|
| `PMX_CANVAS_ALLOW_WORKSPACE_SPLIT` | MCP transport | `1` forces a separate canvas instead of attaching to a different-workspace daemon on the preferred port. |
|
|
@@ -18,7 +18,7 @@ optional; defaults are listed.
|
|
|
18
18
|
| Variable | Read by | Effect |
|
|
19
19
|
|---|---|---|
|
|
20
20
|
| `PMX_CANVAS_DB_PATH` | server | Overrides the SQLite path (default `.pmx-canvas/canvas.db`). |
|
|
21
|
-
| `PMX_CANVAS_STATE_FILE` | server |
|
|
21
|
+
| `PMX_CANVAS_STATE_FILE` | server | `.db`-path alias only as of 0.4.0 (non-`.db` values are ignored with a boot warning; the pre-0.2 JSON import was removed). |
|
|
22
22
|
| `PMX_CANVAS_BLOB_THRESHOLD_BYTES` | server | Node payloads above this many JSON bytes are stored as blobs (default `2048`). |
|
|
23
23
|
|
|
24
24
|
## Server behavior
|
package/docs/http-api.md
CHANGED
|
@@ -5,7 +5,9 @@ any language. Default base URL: `http://localhost:4313`.
|
|
|
5
5
|
|
|
6
6
|
A non-empty request body that is not valid JSON returns
|
|
7
7
|
`400 { "ok": false, "error": "Malformed JSON body." }` on every route; empty
|
|
8
|
-
bodies are treated as an empty request.
|
|
8
|
+
bodies are treated as an empty request. As of 0.4.0 every error response is a
|
|
9
|
+
JSON envelope of that same shape (`{ ok: false, error }`, status unchanged) —
|
|
10
|
+
no plain-text errors remain.
|
|
9
11
|
|
|
10
12
|
## Canvas state
|
|
11
13
|
|
|
@@ -321,6 +323,28 @@ curl -N http://localhost:4313/api/workbench/events
|
|
|
321
323
|
The browser, the CLI `watch` command, and the MCP resource notifications
|
|
322
324
|
all consume this stream. Auto-reconnect with exponential backoff.
|
|
323
325
|
|
|
326
|
+
## Polling transport (proxy-safe)
|
|
327
|
+
|
|
328
|
+
Some proxies (e.g. the Amp orb portal) buffer streaming responses and only
|
|
329
|
+
flush them on close, so the SSE stream above delivers nothing. The polling
|
|
330
|
+
transport returns the same events as short-lived JSON responses that pass
|
|
331
|
+
through any proxy:
|
|
332
|
+
|
|
333
|
+
```bash
|
|
334
|
+
# No `since`: a full connect snapshot (same events an SSE connect sends) + the
|
|
335
|
+
# current cursor. With `since=<seq>`: only events after that cursor.
|
|
336
|
+
curl "http://localhost:4313/api/workbench/poll"
|
|
337
|
+
curl "http://localhost:4313/api/workbench/poll?since=42"
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
Responses are `{ ok, transport: "poll", snapshot, seq, events }` — poll again
|
|
341
|
+
with `since=<seq>` every couple of seconds. A `since` from a previous server
|
|
342
|
+
run or one that fell off the bounded event ring recovers with a fresh
|
|
343
|
+
snapshot. The browser client uses this automatically: if the SSE stream
|
|
344
|
+
produces no event within 3 seconds of connecting, it switches to polling.
|
|
345
|
+
Force a transport with `/workbench?transport=poll` (or `transport=sse` to
|
|
346
|
+
disable the fallback).
|
|
347
|
+
|
|
324
348
|
## Time travel
|
|
325
349
|
|
|
326
350
|
```bash
|
package/docs/mcp.md
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
# MCP reference
|
|
2
2
|
|
|
3
|
-
PMX Canvas ships an MCP stdio server with **
|
|
3
|
+
PMX Canvas ships an MCP stdio server with **22 tools** + **14 core resources**,
|
|
4
4
|
plus per-skill resources at `canvas://skills/<name>`. The server emits
|
|
5
5
|
`notifications/resources/updated` when canvas state changes — humans pin
|
|
6
6
|
nodes in the browser, agents are notified immediately.
|
|
7
7
|
|
|
8
|
-
> **Consolidation
|
|
9
|
-
>
|
|
10
|
-
>
|
|
11
|
-
>
|
|
12
|
-
>
|
|
13
|
-
>
|
|
14
|
-
>
|
|
15
|
-
> `canvas_snapshot` composite in v0.4 (see [Standalone tools](#standalone-tools)
|
|
16
|
-
> below). **Prefer the composites.**
|
|
8
|
+
> **Consolidation complete (plan-006/008).** The MCP surface shrank from 84
|
|
9
|
+
> tools to 22: 16 action-discriminated **composites** (recommended — see
|
|
10
|
+
> below) plus 6 standalone tools. The 57 legacy single-purpose tools the
|
|
11
|
+
> composites replaced were removed in v0.3.0, and v0.4.0 finished the fold by
|
|
12
|
+
> shipping the `canvas_snapshot` composite and removing the 6 deprecated
|
|
13
|
+
> snapshot standalones — each step per [`api-stability.md`](api-stability.md)'s
|
|
14
|
+
> deprecate-one-minor-before-removal rule. **Prefer the composites.**
|
|
17
15
|
|
|
18
16
|
## Connect
|
|
19
17
|
|
|
@@ -54,6 +52,7 @@ its `action` to the same operation the legacy tool used, so results are identica
|
|
|
54
52
|
| `canvas_ax_timeline` | `read` · `record-event` · `add-evidence` · `send-steering` | `canvas_get_ax_timeline`, `canvas_record_ax_event`, `canvas_add_evidence`, `canvas_send_steering` |
|
|
55
53
|
| `canvas_ax_delivery` | `claim` · `mark` | `canvas_claim_ax_delivery`, `canvas_mark_ax_delivery` |
|
|
56
54
|
| `canvas_intent` | `signal` · `update` · `clear` | _(new — Ghost Cursor of Intent; no legacy standalone tool)_ |
|
|
55
|
+
| `canvas_snapshot` | `save` · `list` · `restore` · `delete` · `gc` · `diff` | `canvas_snapshot` (legacy save tool), `canvas_list_snapshots`, `canvas_restore`, `canvas_delete_snapshot`, `canvas_gc_snapshots`, `canvas_diff` — removed in v0.4.0 after one deprecated minor |
|
|
57
56
|
|
|
58
57
|
### `canvas_intent` — Ghost Cursor of Intent
|
|
59
58
|
|
|
@@ -89,20 +88,16 @@ tools). `canvas_screenshot` also stays standalone — it returns a binary image
|
|
|
89
88
|
the composite/registry JSON wire shape does not model. (Wave 5 folded
|
|
90
89
|
`canvas_refresh_webpage_node` → `canvas_node { action: "update", refresh: true }` after
|
|
91
90
|
fixing `node.update`'s `formatResult` to surface a FAILED refresh as `isError` +
|
|
92
|
-
`{ ok:false, error }` instead of masking it as a false `{ ok:true }`.) The
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
tool, and [`api-stability.md`](api-stability.md) requires deprecating a tool
|
|
96
|
-
for one minor before removing it) — they stay registered as deprecated
|
|
97
|
-
standalones until then.
|
|
91
|
+
`{ ok:false, error }` instead of masking it as a false `{ ok:true }`.) The snapshot
|
|
92
|
+
fold completed in v0.4.0: `canvas_snapshot { action: "save" | "list" | "restore" |
|
|
93
|
+
"delete" | "gc" | "diff" }` replaced the 6 deprecated snapshot standalones.
|
|
98
94
|
|
|
99
95
|
## Standalone tools
|
|
100
96
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
57 tools these composites replaced.
|
|
97
|
+
6 tools that intentionally stay outside the composites — folding them would
|
|
98
|
+
hurt (distinct callers, binary payloads, trust-boundary or execution-intent
|
|
99
|
+
semantics). See [Migration reference](#migration-reference) below for the
|
|
100
|
+
legacy tools the composites replaced.
|
|
106
101
|
|
|
107
102
|
| Tool | Description |
|
|
108
103
|
|------|-------------|
|
|
@@ -112,12 +107,6 @@ v0.4 composite. See [Migration reference](#migration-reference) below for the
|
|
|
112
107
|
| `canvas_ax_interaction` | Submit one capability-gated AX interaction envelope (`{ type, sourceNodeId, payload }`) that maps onto an AX operation; the server re-validates and clamps sandboxed surfaces to their own node |
|
|
113
108
|
| `canvas_ingest_activity` | Ingest a harness-forwarded agent activity (tool/session event); the board auto-reacts with kind-driven, overridable defaults (failure → work item + review + evidence; `tool-result`+success → evidence). Makes AX bidirectional |
|
|
114
109
|
| `canvas_screenshot` | Capture a screenshot from the active workbench automation session |
|
|
115
|
-
| `canvas_snapshot` | **Deprecated** → folds into the `canvas_snapshot` composite in v0.4 (actions `save\|list\|restore\|delete\|gc\|diff`). Save current canvas as a named snapshot |
|
|
116
|
-
| `canvas_list_snapshots` | **Deprecated** → folds into the `canvas_snapshot` composite in v0.4 (actions `save\|list\|restore\|delete\|gc\|diff`). List saved snapshots, bounded to the newest 20 by default |
|
|
117
|
-
| `canvas_restore` | **Deprecated** → folds into the `canvas_snapshot` composite in v0.4 (actions `save\|list\|restore\|delete\|gc\|diff`). Restore canvas from a saved snapshot |
|
|
118
|
-
| `canvas_delete_snapshot` | **Deprecated** → folds into the `canvas_snapshot` composite in v0.4 (actions `save\|list\|restore\|delete\|gc\|diff`). Delete a saved snapshot |
|
|
119
|
-
| `canvas_gc_snapshots` | **Deprecated** → folds into the `canvas_snapshot` composite in v0.4 (actions `save\|list\|restore\|delete\|gc\|diff`). Delete old snapshots while keeping the newest N |
|
|
120
|
-
| `canvas_diff` | **Deprecated** → folds into the `canvas_snapshot` composite in v0.4 (actions `save\|list\|restore\|delete\|gc\|diff`). Compare current canvas vs a saved snapshot |
|
|
121
110
|
|
|
122
111
|
`canvas_node { action: "add", type: "html" }` accepts optional `summary`, `agentSummary`,
|
|
123
112
|
`embeddedNodeIds`, and `embeddedUrls`. PMX also derives a bounded text summary from visible
|
|
@@ -125,7 +114,9 @@ HTML, so rich HTML nodes stay searchable and readable in pinned/spatial context.
|
|
|
125
114
|
|
|
126
115
|
## Migration reference
|
|
127
116
|
|
|
128
|
-
The 57 legacy single-purpose tools below were removed in v0.3.0
|
|
117
|
+
The 57 legacy single-purpose tools below were removed in v0.3.0, and v0.4.0
|
|
118
|
+
additionally removed the 6 deprecated snapshot standalones (see the
|
|
119
|
+
`canvas_snapshot` composite row above for their replacements). Each row is
|
|
129
120
|
the composite call that replaces it — kept as a lookup table for anyone
|
|
130
121
|
migrating an older integration.
|
|
131
122
|
|
|
@@ -304,5 +295,5 @@ CLI and MCP are kept aligned for the main canvas operations: node and edge
|
|
|
304
295
|
creation, graph/json-render/html/html-primitive nodes, web artifacts, external apps, groups,
|
|
305
296
|
batch builds, layout validation, snapshots, search, focus, pins, undo/redo,
|
|
306
297
|
semantic watch streams, WebView automation, and daemon/server control where
|
|
307
|
-
it applies. A few agent-native capabilities —
|
|
308
|
-
|
|
298
|
+
it applies. A few agent-native capabilities — such as resource
|
|
299
|
+
subscriptions — remain MCP-only.
|
|
@@ -11,7 +11,8 @@ Each item is tagged by shape: **project** (multi-day, own plan/PR), **decision**
|
|
|
11
11
|
The remaining items were re-analyzed with pros/cons and tiered; the maintainer approved this ordering:
|
|
12
12
|
|
|
13
13
|
- **Tier 1 (do):** CLI raw fetches → operation invoker **together with** the H3 parser/module split (same file, one project); H7 test coverage **as a prerequisite** for any client refactor; M2 AX-bridge dedup (first client task after H7); the M8 flake subset (mock the Excalidraw network tests, share `getAvailablePort`, Playwright `retries`/`forbidOnly`, `e2e-cli` into CI). *C1b done (2026-07-08):* all 60 raw `api()` calls migrated onto the invoker (9-agent verified mapping; `webartifact.build` gained `errorBodyAsResult`; batch failure prints the envelope + exits 1; the `api()` helper is deleted; two deliberate raw `fetch` sites remain — binary screenshot, SSE watch). *H3 module split done (2026-07-09):* `agent.ts` is a 227-line entry over `src/cli/shared.ts` (cycle-free core: registry, parser, transport) + 11 `src/cli/commands/*` domain modules, moved byte-identically; ax-parity reads the whole cli tree. **Parser sub-item reshaped:** `parseFlags`/`BOOL_FLAGS` is now single-sited in shared.ts; `index.ts` keeps its intentionally tiny fixed-flag launcher reader (different domain — server launch, not agent commands). The spec-driven per-command flag parser (`defineCommand`) remains the ideal but is a redesign of all 83 command surfaces — deferred to a future CLI UX change, not worth manufactured churn now.
|
|
14
|
-
- **Tier 2
|
|
14
|
+
- **Tier 2 — async-SDK question DECIDED NO (2026-08-03, v0.4.0):** the sync `PmxCanvas` facade is the contract. Post-registry the 57 sync methods are thin presentation over the shared operation cores (single logic site — the tier's own analysis), so forcing them async would break every SDK consumer to remove zero duplication. The already-async/void methods dispatch through `executeOperation`; the rest stay sync by design. No further collapse is planned.
|
|
15
|
+
- **Tier 2 (original analysis):** the PmxCanvas→executeOperation collapse is **not** mechanical — `executeOperation` is async while 57/76 SDK methods are sync with typed returns, so a full collapse is a breaking SDK change. Verdict: migrate only the already-async/void methods now; decide the full async-SDK question as a **v0.4 breaking item** with the `canvas_snapshot` composite. Post-registry the sync methods are thin presentation over the shared cores (single logic site) — not logic duplication. H5 (sse-bridge factory) proceeds **after H7**. M6 gets the middle path: runtime per-action required-field errors, no `oneOf` schema break (many MCP hosts handle `oneOf` poorly). M1 gets the minimum four (minify, devtools→devDeps, lazy-load, replace the python mtime gate); the React-stack decision stays open.
|
|
15
16
|
- **Tier 3 (cheap batch):** CI check that CLAUDE.md/AGENTS.md stay identical (instead of doc generation); document `PMX_DATA` + the `PMX_CANVAS_JSON_RENDER_*`/`PMX_MCP_APP_HOST_*` env families; publish.yml verifies the tag SHA passed test.yml; `@types/turndown` → devDependencies; L1 dead `ContextPinHud`; L4 one-shot migration marker; L6 timer-leak fixes.
|
|
16
17
|
- **Tier 4 (closed as by-design / parked):** the six server-coupled routes (nothing to dedup — injection machinery for single-copy handlers isn't worth it); M3 ExtAppFrame refactor (the timing hacks are load-bearing WebKit fixes — document invariants, enter only when a feature forces it); M7 notification fan-out, L2, L3, `noUncheckedIndexedAccess` (parked); M10 packaging closed after the `@types` one-liner (`bin`→`src` under Bun and shipped `dist/` are both load-bearing by design).
|
|
17
18
|
|
|
@@ -19,10 +20,10 @@ The remaining items were re-analyzed with pros/cons and tiered; the maintainer a
|
|
|
19
20
|
|
|
20
21
|
| Item | Source | What's open | Shape |
|
|
21
22
|
|---|---|---|---|
|
|
22
|
-
| 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.
|
|
23
|
-
| One HTTP envelope | M4 residual |
|
|
23
|
+
| 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. **Status (2026-08-03):** the CLI fetches are done (C1b) and the `PmxCanvas` collapse is closed by the async-SDK decision (sync facade stays). The 4 server-coupled routes (`prompt`, workbench `state`/`open`/`intent`, `render`, `open-external`) remain the only open C1 work — internal state-injection refactor, zero wire impact, deliberately post-0.4. | tail open (post-0.4) |
|
|
24
|
+
| One HTTP envelope | M4 residual | **Done (2026-08-03, v0.4.0):** every hand-written error response is now a JSON `{ ok:false, error }` envelope with unchanged status codes (matching the registry dispatcher); `/health` gained a `persistence` block that reports degraded durability after a failed save (ok again on the next success). The ext-app `{ok:true,result}` wrapper is KEPT deliberately — it is the app-bridge proxy contract our own panel parses; changing it bought no consumer value. | done |
|
|
24
25
|
| `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 |
|
|
25
|
-
| Skill-mirror validator | C2 residual |
|
|
26
|
+
| Skill-mirror validator | C2 residual | **Decided + done (2026-08-03):** the script prints a labeled SKIPPED notice when trees are absent (local behavior unchanged), and the permanently-no-op CI step was removed with a comment. | done |
|
|
26
27
|
| Generate CLAUDE.md/AGENTS.md from one source | M9 residual | **Done (tier-3 batch):** CI fails on any CLAUDE.md/AGENTS.md byte drift (`test.yml` identity gate) — the cheap alternative to generation the decision analysis chose. Generation/symlinking stays unnecessary while the gate holds. | done |
|
|
27
28
|
|
|
28
29
|
## Phase 2 — make the safety net real
|
|
@@ -30,19 +31,19 @@ The remaining items were re-analyzed with pros/cons and tiered; the maintainer a
|
|
|
30
31
|
| Item | Source | What's open | Shape |
|
|
31
32
|
|---|---|---|---|
|
|
32
33
|
| 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 list — warnings don't fail the gate. | done |
|
|
33
|
-
| Client + server unit coverage | H7 | **Done post-0.3.3 (first tranche):** happy-dom + @testing-library/preact harness under `tests/client/` (own preload so DOM globals never leak into server suites); 20 client render tests incl. the SelectionBar conditional-hooks regression; `semantic-attention` (25 tests) and `canvas-schema` (21 tests incl. an M12 stale-tool-drift invariant) covered; tests are TYPECHECKED (`tsconfig.tests.json`, 270 latent errors fixed, wired into `typecheck` + CI); coverage now GATES via `scripts/check-coverage.ts` total floors (bun's bunfig threshold is per-file and its table form is silently ignored — documented). **
|
|
34
|
+
| Client + server unit coverage | H7 | **Done post-0.3.3 (first tranche):** happy-dom + @testing-library/preact harness under `tests/client/` (own preload so DOM globals never leak into server suites); 20 client render tests incl. the SelectionBar conditional-hooks regression; `semantic-attention` (25 tests) and `canvas-schema` (21 tests incl. an M12 stale-tool-drift invariant) covered; tests are TYPECHECKED (`tsconfig.tests.json`, 270 latent errors fixed, wired into `typecheck` + CI); coverage now GATES via `scripts/check-coverage.ts` total floors (bun's bunfig threshold is per-file and its table form is silently ignored — documented). **Done (2026-08-03):** dedicated suites for `ax-state-manager.ts` (39 tests, injected-deps harness + in-memory SQLite) and `mcp-app-host.ts` (15 tests; stateful paths in hermetic subprocesses). Unit-test scripts now pin `PMX_MCP_APP_HOST_STATE_FILE` to a scratch path so suites never touch the real home-dir state. Client suite grows alongside H5. | done |
|
|
34
35
|
| 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 |
|
|
35
|
-
| 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. **M8 subset done (2026-07-08):** those tests now run against the local stdio fixture via `PMX_CANVAS_EXCALIDRAW_MCP_COMMAND`; `getAvailablePort` is one shared helper; Playwright has `retries`/`forbidOnly` on CI; `e2e-cli` is a CI job. **Remaining (
|
|
36
|
+
| 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. **M8 subset done (2026-07-08):** those tests now run against the local stdio fixture via `PMX_CANVAS_EXCALIDRAW_MCP_COMMAND`; `getAvailablePort` is one shared helper; Playwright has `retries`/`forbidOnly` on CI; `e2e-cli` is a CI job. **Sleep sweep done (2026-08-03):** fixed persistence sleeps replaced by condition polling via a shared helper. **Remaining (post-0.4, test organization only):** the `tests/unit/` monolith split. | sleep sweep done |
|
|
36
37
|
| `noUncheckedIndexedAccess` | L7 residual | Enable in tsconfig; will surface many index-access errors — treat as its own sweep. | small (noisy) |
|
|
37
38
|
|
|
38
39
|
## Phase 3 — client consolidation
|
|
39
40
|
|
|
40
41
|
| Item | Source | What's open | Shape |
|
|
41
42
|
|---|---|---|---|
|
|
42
|
-
| One node factory | H5 |
|
|
43
|
+
| One node factory | H5 | **Client half done (2026-08-03):** the `responseToThreadMap` reconnect leak is fixed (cleared in the shared per-connection reset path; mutation-verified test) and all four sse-bridge construction sites route through a shared `node-factory.ts`. **Premise correction:** `canvas-store` holds no node-creation defaults — the real divergences are client-vs-SERVER (server `defaultNodeSize` vs the client table; `ensureDefaultDockedNodes` seeding collapsed/docked vs the bridge's expanded/undocked; a same-class `serverResponseToThreadMap` leak at server.ts:2469) — all catalogued in the factory for the deferred server half (table-drive `syncEventToCanvasState`, single source of truth). | client half done |
|
|
43
44
|
| Shared AX iframe bridge | M2 | **Done (2026-08-03):** `useAxSurfaceBridge` (`src/client/nodes/use-ax-surface-bridge.ts`) single-sites the sandboxed-surface trust boundary previously copy-pasted in `HtmlNode.tsx`, `McpAppNode.tsx`, `ExtAppFrame.tsx`; `src/shared/ax-surface-protocol.ts` owns the wire tags (`pmx-canvas-ax`, `-ack`, `pmx-canvas-html-node`, the ext-app boot beacon) — client parsers import them, generated surface scripts (html-surface.ts, ext-app builders, json-render viewer) interpolate them byte-identically. Client render tests pin the validation gates + submit→ack round-trip (`tests/client/use-ax-surface-bridge.test.tsx`). | done |
|
|
44
45
|
| `ExtAppFrame.tsx` timing hacks | M3 | 915 lines, 12 refs, 7 `setTimeout`s, 5 rAFs, WebKit remount slot counter, 1200 ms fallback bootstrap, two near-duplicate `buildHostContext` blocks. Extract `useExtAppBridge`; single `buildHostContext`. | project |
|
|
45
|
-
| json-render stack decision | M1 |
|
|
46
|
+
| json-render stack decision | M1 | **Minimum-four done (2026-08-03):** both bundles verified minified (the audit's "unminified" was stale — React+recharts dominate the 1.31 MiB minified size); the 2 `@json-render/devtools*` packages moved to devDependencies; the viewer is already a separate page bundle (nothing to lazy-load — verified, no churn manufactured); the python mtime gate is now Bun with parity-tested exit codes. **Still open (decision):** the React-stack question — a separate optional package remains the only real size lever. | decision |
|
|
46
47
|
| Client dead code / duplication | L1, L2, L3 | `ContextPinHud.tsx` unreferenced; three pure re-export shims; two parallel markdown-format stacks; `global.css` 3,746 lines with 11 `!important` vs 227 inline styles and unscaled z-indexes. | small (batchable) |
|
|
47
48
|
|
|
48
49
|
## Smaller items (no phase, pick up opportunistically)
|
|
@@ -51,13 +52,13 @@ The remaining items were re-analyzed with pros/cons and tiered; the maintainer a
|
|
|
51
52
|
|---|---|---|
|
|
52
53
|
| Per-action composite schemas | M6 | **Done (2026-08-03, middle path per the decision analysis):** composite dispatch re-enforces each member op's required MCP fields at runtime and names the missing field (`requiredMcpFields` in `operations/mcp.ts`) — no `oneOf` schema break (many MCP hosts handle `oneOf` poorly). Advertised schemas stay all-optional by design. |
|
|
53
54
|
| Packaging | M10 | `files` ships both `src/` and `dist/` (~double tarball, two import paths); `bin` points at raw `.ts` (undocumented Bun-only constraint); `@types/turndown` in runtime deps. |
|
|
54
|
-
| Env var hygiene | M11 residual |
|
|
55
|
+
| Env var hygiene | M11 residual | **Done (2026-08-03):** server startup now falls back `--port` → `PMX_WEB_CANVAS_PORT` → `PMX_CANVAS_PORT` → 4313, so setting only the client var no longer splits client and server ports; documented in `docs/environment.md`. |
|
|
55
56
|
| `publish.yml` e2e gap | M11 residual | **Done (tier-3 batch):** publish.yml verifies the tagged SHA has a successful `test.yml` run before publishing. |
|
|
56
57
|
| MCP notification fan-out | M7 residual | Five `canvas://` resource URIs notify on every mutation regardless of type (`mcp/server.ts:95-99`). |
|
|
57
|
-
| Legacy migration code on every boot | L4 | **
|
|
58
|
-
| Suppression/mutation plumbing | L5 |
|
|
58
|
+
| Legacy migration code on every boot | L4 | **Done (2026-08-03, v0.4.0 breaking window):** the pre-0.2 import path is deleted (~150 lines); fresh-boot bootstrapping untouched; `PMX_CANVAS_STATE_FILE` survives only as a `.db`-path alias with a boot warning otherwise. Verified with a legacy-artifacts smoke (files untouched, no `.bak`). |
|
|
59
|
+
| Suppression/mutation plumbing | L5 | **Done (2026-08-03):** the "two counters" are semantically distinct by design (emit-suppression for batch vs recording-suppression for undo/redo — merging breaks one or the other; documented at both declarations). The redundant second `onMutation` wire in `PmxCanvas.start` was deleted; the slot contract is documented. |
|
|
59
60
|
| Module-global lifecycle leaks | L6 | ~20 module-level `let`s in `server.ts`; code-graph debounce timer survives `stopCanvasServer`; remote SSE watcher loop has no cancellation. |
|
|
60
|
-
| June doc citations | L7 residual |
|
|
61
|
+
| June doc citations | L7 residual | **Done (2026-08-03):** each cited entry's substance is inlined so the doc stands alone; one miscitation corrected. |
|
|
61
62
|
|
|
62
63
|
## Added post-0.3.3 (2026-08-03 sweep)
|
|
63
64
|
|
package/docs/screenshot.png
CHANGED
|
Binary file
|
|
@@ -14,11 +14,11 @@ Code quality is better than the release velocity would suggest: zero `as any` ac
|
|
|
14
14
|
|
|
15
15
|
Every operation is hand-written four times — `CanvasStateManager` → `PmxCanvas` → HTTP handler in `server.ts` → MCP tool in `src/mcp/server.ts` — each with its own validation and error shapes. The CLI (`src/cli/agent.ts`, 3,300 lines) duplicates it a fifth time with raw fetch calls instead of using the SDK.
|
|
16
16
|
|
|
17
|
-
Evidence from `.learnings/` that this is actively producing bugs:
|
|
17
|
+
Evidence from `.learnings/` that this is actively producing bugs (the `.learnings/` tree is gitignored, so the substance of each cited entry is inlined here):
|
|
18
18
|
|
|
19
|
-
- [LRN-20260606-006] Fix #32 applied to only one of two duplicated mutation paths (`updateNode()` vs `applyUpdates()`).
|
|
20
|
-
- [LRN-20260607-005] New `json-render` sourceSurface enum member silently took the permissive default because the `scoped` guard in `applyAxInteraction` was not updated. Security near-miss.
|
|
21
|
-
- [LRN-20260608-002] `readJson` hardening silently killed the documented bare-array shape of `POST /api/canvas/batch` (#49).
|
|
19
|
+
- [LRN-20260606-006] Fix #32 applied to only one of two duplicated mutation paths (`updateNode()` vs `applyUpdates()`). Both paths carried an identical copy of the group repack-on-resize logic, so fixing only the batch path left single-node `PATCH` updates still repacking siblings. The same entry records the parallel serializer pair drifting: the `nodeId` response alias was added to the MCP `createdNodePayload` but not the HTTP `buildNodeResponse`, so CLI/HTTP `node add --json` lacked it (#31).
|
|
20
|
+
- [LRN-20260607-005] New `json-render` sourceSurface enum member silently took the permissive default because the `scoped` guard in `applyAxInteraction` was not updated. Security near-miss: the sandboxed viewer forwards author-controlled spec action params verbatim, so a prompt-injected spec could have anchored AX state onto arbitrary nodes via caller-supplied `nodeIds` (`on.press → { action: 'ax.work.create', params: { nodeIds: ['victim'] } }`). The fix added `json-render` to the clamp; the durable rule is that any guard switching on an enum-like string is a mandatory co-change site for every new member, or the new member takes the permissive default.
|
|
21
|
+
- [LRN-20260608-002] `readJson` hardening silently killed the documented bare-array shape of `POST /api/canvas/batch` (#49). The #47 hardening normalized every non-object body — including top-level arrays — to `{}`, which turned the handler's `Array.isArray(body)` branch into dead code: bare-array batches returned `{ ok: true, results: [] }` and created nothing. Fixed with an array-preserving reader (`readJsonObjectOrArray`) used only by the batch endpoint.
|
|
22
22
|
|
|
23
23
|
These are not three bugs. They are one architecture failing three times.
|
|
24
24
|
|
|
@@ -36,7 +36,7 @@ A project whose thesis is agent experience ships a tool surface that consumes a
|
|
|
36
36
|
|
|
37
37
|
### 4. E2E is not a CI gate (high)
|
|
38
38
|
|
|
39
|
-
The Playwright/Bun ESM loader blocker ([ERR-20260508-001]) has been open for weeks. E2E was removed from the publish workflow after the apt-mirror hang ([LRN-20260603-
|
|
39
|
+
The Playwright/Bun ESM loader blocker ([ERR-20260508-001]: `bun x playwright test` dies before test discovery because Playwright's transform layer tries to import a nonexistent `playwright.config.ts.esm.preflight` module under Bun; browser tests only run when Playwright is launched through Node with Bun on `PATH`) has been open for weeks. E2E was removed from the publish workflow after the apt-mirror hang ([LRN-20260603-001]: the `playwright install --with-deps chromium` step began reliably hanging ~15 minutes on GitHub runners with zero code change — a runner-image/apt regression — so publish dropped its redundant browser steps and kept build + typecheck + unit + smoke) and does not gate PRs. The bugs that matter (iframe blank flicker, literal `\n` in ledger, SVG calc()) were all caught only by browser tests. Green CI can currently ship a broken canvas.
|
|
40
40
|
|
|
41
41
|
**Fix:** run Playwright via Node's runner in CI permanently, and make headless e2e a hard PR gate.
|
|
42
42
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmx-canvas",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Spatial canvas workbench for coding agents
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Spatial canvas workbench for coding agents \u2014 infinite 2D canvas with agent-native CLI, MCP integration, nodes, edges, file watching, and snapshots",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/server/index.ts",
|
|
7
7
|
"module": "./src/server/index.ts",
|
|
@@ -43,10 +43,10 @@
|
|
|
43
43
|
"typecheck": "tsc --noEmit && tsc -p tsconfig.tests.json",
|
|
44
44
|
"lint": "biome check .",
|
|
45
45
|
"lint:fix": "biome check --write .",
|
|
46
|
-
"test": "PMX_CANVAS_DISABLE_BROWSER_OPEN=1 bun test tests/unit",
|
|
47
|
-
"test:unit": "PMX_CANVAS_DISABLE_BROWSER_OPEN=1 bun test tests/unit",
|
|
46
|
+
"test": "PMX_MCP_APP_HOST_STATE_FILE=/tmp/pmx-canvas-test-app-host-state.json PMX_CANVAS_DISABLE_BROWSER_OPEN=1 bun test tests/unit",
|
|
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": "bun test --preload ./tests/client/setup.ts tests/client",
|
|
49
|
-
"test:coverage": "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 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",
|
|
@@ -60,8 +60,6 @@
|
|
|
60
60
|
"dependencies": {
|
|
61
61
|
"@joplin/turndown-plugin-gfm": "^1.0.64",
|
|
62
62
|
"@json-render/core": "0.19.0",
|
|
63
|
-
"@json-render/devtools": "0.19.0",
|
|
64
|
-
"@json-render/devtools-react": "0.19.0",
|
|
65
63
|
"@json-render/directives": "0.19.0",
|
|
66
64
|
"@json-render/mcp": "0.19.0",
|
|
67
65
|
"@json-render/react": "0.19.0",
|
|
@@ -80,6 +78,8 @@
|
|
|
80
78
|
"devDependencies": {
|
|
81
79
|
"@biomejs/biome": "^2.5.2",
|
|
82
80
|
"@happy-dom/global-registrator": "^20.10.6",
|
|
81
|
+
"@json-render/devtools": "0.19.0",
|
|
82
|
+
"@json-render/devtools-react": "0.19.0",
|
|
83
83
|
"@playwright/test": "^1.58.2",
|
|
84
84
|
"@tailwindcss/cli": "^4.1.18",
|
|
85
85
|
"@testing-library/preact": "^3.2.4",
|
|
@@ -26,8 +26,8 @@ Humans curate agent context by pinning nodes; agents read that curation through
|
|
|
26
26
|
3. **Read before write.** Search with `canvas_query { action: "search", query }` before creating
|
|
27
27
|
nodes. Read the full layout only when necessary. The MCP parameter is `query` — passing the
|
|
28
28
|
HTTP API's `q` is silently ignored and returns zero results.
|
|
29
|
-
4. **Snapshot before destructive changes.** Use `canvas_snapshot
|
|
30
|
-
|
|
29
|
+
4. **Snapshot before destructive changes.** Use `canvas_snapshot { action: "save", name }` before
|
|
30
|
+
clear, restore, or a major
|
|
31
31
|
reorganization.
|
|
32
32
|
5. **Show intent with the Ghost Cursor — by default.** Signal with
|
|
33
33
|
`canvas_intent { action: "signal", ... }` before every meaningful create, move, connect, remove,
|
|
@@ -36,7 +36,7 @@ Humans curate agent context by pinning nodes; agents read that curation through
|
|
|
36
36
|
human watches intent form and can veto mid-thought. Skip it only for trivial in-place tweaks or
|
|
37
37
|
high-frequency batch churn. The default TTL (~8s) expires between agent turns: signal with
|
|
38
38
|
`ttlMs: 30000` and settle by passing `intentId` on the mutation in the same or next call.
|
|
39
|
-
6. **Mutate through current composites.** Prefer the
|
|
39
|
+
6. **Mutate through current composites.** Prefer the 16 composite MCP tools below.
|
|
40
40
|
7. **Arrange and validate.** After batch changes, use `canvas_view { action: "arrange" }` when
|
|
41
41
|
appropriate and always finish with `canvas_query { action: "validate" }`.
|
|
42
42
|
8. **Verify context pins.** Pin with `canvas_pin_nodes` or the browser's **Pin as context**, then
|
|
@@ -115,6 +115,7 @@ json-render, a graph, or an HTML primitive is sufficient.
|
|
|
115
115
|
| `canvas_ax_gate` | `request`, `resolve`, `await` with `approval`, `elicitation`, or `mode` |
|
|
116
116
|
| `canvas_ax_timeline` | `read`, `record-event`, `add-evidence`, `send-steering` |
|
|
117
117
|
| `canvas_ax_delivery` | `claim`, `mark` |
|
|
118
|
+
| `canvas_snapshot` | `save`, `list`, `restore`, `delete`, `gc`, `diff` |
|
|
118
119
|
| `canvas_intent` | `signal`, `update`, `clear` |
|
|
119
120
|
|
|
120
121
|
Important routing:
|
|
@@ -131,9 +132,9 @@ Important routing:
|
|
|
131
132
|
As of v0.3.0, the 57 legacy single-purpose tools from the v0.2 compatibility window are removed.
|
|
132
133
|
The composites above plus the retained standalones are now the whole MCP surface: `canvas_batch`,
|
|
133
134
|
`canvas_pin_nodes`, `canvas_screenshot`, `canvas_ax_interaction`, `canvas_ingest_activity`, and
|
|
134
|
-
`canvas_invoke_command`.
|
|
135
|
-
`
|
|
136
|
-
|
|
135
|
+
`canvas_invoke_command`. Snapshots are the `canvas_snapshot` composite (actions
|
|
136
|
+
`save | list | restore | delete | gc | diff`); the 6 legacy snapshot standalones were removed in
|
|
137
|
+
v0.4.0 after their deprecated 0.3.x window.
|
|
137
138
|
|
|
138
139
|
## Spatial Rules
|
|
139
140
|
|
|
@@ -228,6 +229,15 @@ Prefer `canvas_query { action: "search" }` over parsing the full layout.
|
|
|
228
229
|
(Chrome). Do not diagnose a healthy app session as a broken node. When reporting a black tile,
|
|
229
230
|
fetch `GET /api/canvas/debug/ext-app-recovery` from the daemon and attach the trace — it shows
|
|
230
231
|
what the recovery queue actually did in that panel.
|
|
232
|
+
- A hosted ext-app (Excalidraw) resized NARROW/TALL (e.g. 360x529) can show its diagram in the
|
|
233
|
+
upper region with the app's own dark fill below it — in every engine (0.3.4 report Finding Q).
|
|
234
|
+
This letterboxing lives inside the hosted app bundle's root container, not in PMX (a body-level
|
|
235
|
+
background override ships but cannot reach the app's inner root). Keep ext-app tiles near the
|
|
236
|
+
app's aspect (the 960x738 default, or the same ratio), or expand the node — the fullscreen
|
|
237
|
+
overlay renders full-bleed. The durable fix is upstream in the excalidraw-mcp app.
|
|
238
|
+
- Behind proxies that buffer streaming responses (e.g. portal hosts), the workbench auto-falls
|
|
239
|
+
back from SSE to a polling transport within ~3s, so the board still boots and stays live.
|
|
240
|
+
Force a mode with `/workbench?transport=poll` (or `transport=sse`) when diagnosing.
|
|
231
241
|
- Graph and json-render standalone surfaces use `display=site` and fill the browser viewport, and
|
|
232
242
|
reflow on a live window resize in a normal browser. Some single-tab host browsers (e.g. the
|
|
233
243
|
Codex in-app browser) don't deliver live-resize events, so a resized standalone chart can look
|