pmx-canvas 0.1.35 → 0.2.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 +461 -0
- package/Readme.md +14 -2
- package/dist/canvas/index.js +82 -41
- package/dist/json-render/index.js +89 -334
- package/dist/types/client/nodes/ExtAppFrame.d.ts +2 -0
- package/dist/types/mcp/canvas-access.d.ts +12 -159
- package/dist/types/server/ax-context.d.ts +1 -1
- package/dist/types/server/ax-state-manager.d.ts +256 -0
- package/dist/types/server/ax-state.d.ts +29 -1
- package/dist/types/server/ax-wait.d.ts +23 -0
- package/dist/types/server/canvas-operations.d.ts +1 -12
- package/dist/types/server/canvas-state.d.ts +46 -14
- package/dist/types/server/html-surface.d.ts +7 -0
- package/dist/types/server/index.d.ts +66 -26
- package/dist/types/server/operations/composites.d.ts +121 -0
- package/dist/types/server/operations/http.d.ts +7 -0
- package/dist/types/server/operations/index.d.ts +8 -0
- package/dist/types/server/operations/invoker.d.ts +13 -0
- package/dist/types/server/operations/mcp.d.ts +15 -0
- package/dist/types/server/operations/ops/annotation.d.ts +2 -0
- package/dist/types/server/operations/ops/app.d.ts +33 -0
- package/dist/types/server/operations/ops/ax-await.d.ts +2 -0
- package/dist/types/server/operations/ops/ax-shared.d.ts +31 -0
- package/dist/types/server/operations/ops/ax-state.d.ts +2 -0
- package/dist/types/server/operations/ops/ax-timeline.d.ts +2 -0
- package/dist/types/server/operations/ops/ax-work.d.ts +2 -0
- package/dist/types/server/operations/ops/batch.d.ts +19 -0
- package/dist/types/server/operations/ops/edges.d.ts +2 -0
- package/dist/types/server/operations/ops/groups.d.ts +2 -0
- package/dist/types/server/operations/ops/json-render.d.ts +31 -0
- package/dist/types/server/operations/ops/nodes.d.ts +62 -0
- package/dist/types/server/operations/ops/query.d.ts +2 -0
- package/dist/types/server/operations/ops/snapshots.d.ts +2 -0
- package/dist/types/server/operations/ops/validate.d.ts +2 -0
- package/dist/types/server/operations/ops/viewport.d.ts +2 -0
- package/dist/types/server/operations/ops/webview.d.ts +2 -0
- package/dist/types/server/operations/registry.d.ts +15 -0
- package/dist/types/server/operations/types.d.ts +116 -0
- package/dist/types/server/operations/webview-runner.d.ts +69 -0
- package/docs/RELEASE.md +5 -0
- package/docs/adr-001-bun-only-runtime.md +46 -0
- package/docs/api-stability.md +57 -0
- package/docs/ax-host-adapter-contract.md +65 -0
- package/docs/ax-state-contract.md +72 -0
- package/docs/http-api.md +34 -2
- package/docs/mcp.md +64 -11
- package/docs/plans/plan-005-operation-registry.md +84 -0
- package/docs/plans/plan-006-mcp-tool-consolidation.md +109 -0
- package/docs/plans/plan-007-ax-domain.md +99 -0
- package/docs/plans/plan-008-registry-finish.md +91 -0
- package/docs/screenshot.png +0 -0
- package/docs/tech-debt-assessment-2026-06.md +90 -0
- package/package.json +3 -3
- package/skills/pmx-canvas/SKILL.md +233 -185
- package/skills/pmx-canvas/evals/evals.json +3 -3
- package/skills/pmx-canvas/references/codex-app-adapter.md +24 -11
- package/skills/pmx-canvas/references/github-copilot-app-adapter.md +31 -1
- package/src/cli/agent.ts +52 -31
- package/src/client/nodes/ExtAppFrame.tsx +73 -5
- package/src/client/nodes/HtmlNode.tsx +12 -3
- package/src/client/nodes/McpAppNode.tsx +12 -3
- package/src/json-render/renderer/index.tsx +3 -0
- package/src/mcp/canvas-access.ts +43 -774
- package/src/mcp/server.ts +190 -2001
- package/src/server/ax-context.ts +7 -1
- package/src/server/ax-state-manager.ts +808 -0
- package/src/server/ax-state.ts +89 -2
- package/src/server/ax-wait.ts +56 -0
- package/src/server/canvas-operations.ts +2 -328
- package/src/server/canvas-schema.ts +2 -2
- package/src/server/canvas-state.ts +140 -382
- package/src/server/html-surface.ts +49 -11
- package/src/server/index.ts +136 -192
- package/src/server/operations/composites.ts +355 -0
- package/src/server/operations/http.ts +103 -0
- package/src/server/operations/index.ts +65 -0
- package/src/server/operations/invoker.ts +87 -0
- package/src/server/operations/mcp.ts +221 -0
- package/src/server/operations/ops/annotation.ts +60 -0
- package/src/server/operations/ops/app.ts +447 -0
- package/src/server/operations/ops/ax-await.ts +216 -0
- package/src/server/operations/ops/ax-shared.ts +38 -0
- package/src/server/operations/ops/ax-state.ts +249 -0
- package/src/server/operations/ops/ax-timeline.ts +381 -0
- package/src/server/operations/ops/ax-work.ts +635 -0
- package/src/server/operations/ops/batch.ts +365 -0
- package/src/server/operations/ops/edges.ts +166 -0
- package/src/server/operations/ops/groups.ts +176 -0
- package/src/server/operations/ops/json-render.ts +691 -0
- package/src/server/operations/ops/nodes.ts +1047 -0
- package/src/server/operations/ops/query.ts +281 -0
- package/src/server/operations/ops/snapshots.ts +366 -0
- package/src/server/operations/ops/validate.ts +37 -0
- package/src/server/operations/ops/viewport.ts +219 -0
- package/src/server/operations/ops/webview.ts +339 -0
- package/src/server/operations/registry.ts +79 -0
- package/src/server/operations/types.ts +150 -0
- package/src/server/operations/webview-runner.ts +77 -0
- package/src/server/server.ts +253 -2170
- package/src/server/web-artifacts.ts +6 -2
|
@@ -93,7 +93,7 @@
|
|
|
93
93
|
"assertions": [
|
|
94
94
|
{
|
|
95
95
|
"name": "reads-pinned-context",
|
|
96
|
-
"description": "Reads the canvas://pinned-context MCP resource (not just
|
|
96
|
+
"description": "Reads the canvas://pinned-context MCP resource (not just canvas_query action:layout)",
|
|
97
97
|
"type": "output_check"
|
|
98
98
|
},
|
|
99
99
|
{
|
|
@@ -135,7 +135,7 @@
|
|
|
135
135
|
"id": 6,
|
|
136
136
|
"name": "status-dashboard-updates",
|
|
137
137
|
"prompt": "I'm running a deployment pipeline. Create a dashboard on the canvas showing: Build (passing), Unit Tests (running), Integration Tests (queued), Deploy to Staging (queued). Then update it - unit tests just passed and integration tests are now running.",
|
|
138
|
-
"expected_output": "Creates status nodes with semantic colors, arranges as grid dashboard, then updates nodes in place using
|
|
138
|
+
"expected_output": "Creates status nodes with semantic colors, arranges as grid dashboard, then updates nodes in place using canvas_node action:update (not delete+recreate) to reflect new state.",
|
|
139
139
|
"assertions": [
|
|
140
140
|
{
|
|
141
141
|
"name": "initial-dashboard",
|
|
@@ -144,7 +144,7 @@
|
|
|
144
144
|
},
|
|
145
145
|
{
|
|
146
146
|
"name": "updates-in-place",
|
|
147
|
-
"description": "Uses
|
|
147
|
+
"description": "Uses canvas_node action:update to change status (not remove+add)",
|
|
148
148
|
"type": "output_check"
|
|
149
149
|
},
|
|
150
150
|
{
|
|
@@ -21,9 +21,9 @@ tools, resources, and `canvas://ax-context`; use the CLI only as a fallback or f
|
|
|
21
21
|
|
|
22
22
|
- Opens the live PMX workbench in the Codex in-app Browser.
|
|
23
23
|
- Uses MCP tools/resources for all agent-side operations.
|
|
24
|
-
- Reads `canvas://ax-context` or `
|
|
25
|
-
- Sets AX focus through `
|
|
26
|
-
from Codex-hosted steering.
|
|
24
|
+
- Reads `canvas://ax-context` or `canvas_ax_state { action: "get" }` for pinned and focused context.
|
|
25
|
+
- Sets AX focus through `canvas_ax_state { action: "set-focus" }` with `source: "codex"` when the
|
|
26
|
+
focus change comes from Codex-hosted steering.
|
|
27
27
|
- Keeps all persistent PMX state in `.pmx-canvas/canvas.db`; Codex does not own canvas state.
|
|
28
28
|
|
|
29
29
|
## Setup
|
|
@@ -70,7 +70,8 @@ in the Codex in-app Browser, usually `http://127.0.0.1:4313/workbench` or
|
|
|
70
70
|
inspect rendered artifacts.
|
|
71
71
|
4. Use MCP tools for agent operations: create/update nodes, pin nodes, read layout, and read AX
|
|
72
72
|
context.
|
|
73
|
-
5. When Codex wants to mark the current attention target, call
|
|
73
|
+
5. When Codex wants to mark the current attention target, call
|
|
74
|
+
`canvas_ax_state { action: "set-focus", … }` with:
|
|
74
75
|
|
|
75
76
|
```json
|
|
76
77
|
{
|
|
@@ -79,27 +80,39 @@ in the Codex in-app Browser, usually `http://127.0.0.1:4313/workbench` or
|
|
|
79
80
|
}
|
|
80
81
|
```
|
|
81
82
|
|
|
82
|
-
against `canvas_set_ax_focus`.
|
|
83
|
-
|
|
84
83
|
## Context Contract
|
|
85
84
|
|
|
86
85
|
Codex agents should treat PMX AX context as host-native working context:
|
|
87
86
|
|
|
88
87
|
- `canvas://pinned-context` is the explicit human-curated node set.
|
|
89
|
-
- `canvas://ax-context` combines pins, focus, and surface metadata
|
|
90
|
-
- `
|
|
88
|
+
- `canvas://ax-context` combines pins, focus, and surface metadata, plus a compact
|
|
89
|
+
loop-safe `delivery: { pendingSteering, pendingActivity }` lead block
|
|
90
|
+
(`GET /api/canvas/ax/context?consumer=codex` filters out Codex-originated items).
|
|
91
|
+
- `canvas_ax_state { action: "get" }` returns both persisted AX state and agent-ready context.
|
|
91
92
|
- Focus is a current attention target, not a command to ignore the rest of the repository.
|
|
92
93
|
|
|
94
|
+
The adapterless MCP+Browser path is poll-based: there is no automatic prompt injection,
|
|
95
|
+
so a board click does not wake the current turn. Codex agents poll
|
|
96
|
+
`canvas_ax_delivery { action: "claim" }` (steering + `pendingActivity`) and act/ack explicitly. The
|
|
97
|
+
loop-closing surfaces work over MCP today even without a dedicated extension:
|
|
98
|
+
|
|
99
|
+
- **Self-report work** with `canvas_ingest_activity` (the board auto-reacts: a failed
|
|
100
|
+
tool → a blocked work item + review + evidence). Automatic forwarding of Codex's own
|
|
101
|
+
tool hooks would need a Codex adapter; manual ingestion works now.
|
|
102
|
+
- **Block on a decision** with `canvas_ax_gate { kind, action: "await", id }` (it long-polls PMX
|
|
103
|
+
until the human resolves the gate in the Browser or the timeout elapses) instead of looping on
|
|
104
|
+
`canvas_ax_state { action: "get" }`.
|
|
105
|
+
|
|
93
106
|
## Live-Test Checklist
|
|
94
107
|
|
|
95
108
|
1. Open `http://127.0.0.1:4313/workbench` in the Codex in-app Browser first so the user can see
|
|
96
109
|
all later canvas mutations.
|
|
97
110
|
2. Confirm the PMX MCP server is configured for the workspace.
|
|
98
|
-
3. Call `
|
|
111
|
+
3. Call `canvas_ax_state { action: "get" }` and confirm it returns `ok: true`.
|
|
99
112
|
4. Add or reuse a node, then pin it from the browser or with `canvas_pin_nodes`.
|
|
100
113
|
5. Read `canvas://ax-context` and confirm the pinned node appears.
|
|
101
|
-
6. Call `
|
|
102
|
-
7. Read `
|
|
114
|
+
6. Call `canvas_ax_state { action: "set-focus", source: "codex" }` with a real node ID.
|
|
115
|
+
7. Read `canvas_ax_state { action: "get" }` again and confirm `state.focus.source` is `codex`.
|
|
103
116
|
8. Refresh the browser and confirm the workbench still shows the same state.
|
|
104
117
|
|
|
105
118
|
## Adapter Boundary
|
|
@@ -48,6 +48,36 @@ panel.
|
|
|
48
48
|
- Reads `/api/canvas/ax/context` and injects pinned/focused context from
|
|
49
49
|
`onUserPromptSubmitted`.
|
|
50
50
|
- Exposes adapter actions for status, AX context refresh, AX focus, and explicit session steering.
|
|
51
|
+
|
|
52
|
+
### Agent behavior — steering is gated, not pushed
|
|
53
|
+
|
|
54
|
+
`onUserPromptSubmitted` injects the whole `/api/canvas/ax/context` (pins, focus, work
|
|
55
|
+
items, approval gates, and `timeline.pendingSteering`) as hidden context — but only
|
|
56
|
+
when the **pin/focus gate is open** (`pinned.count > 0 || focus.nodeIds.length > 0`),
|
|
57
|
+
and it is clipped to a char budget. Three consequences the adapter/agent must honor:
|
|
58
|
+
|
|
59
|
+
1. A steering board must **stay pinned** (or its button must also emit `ax.focus.set`
|
|
60
|
+
on the board node) to hold the gate open.
|
|
61
|
+
2. A sandbox button click does **not** wake a turn — a human message does. The click
|
|
62
|
+
only enqueues the steer.
|
|
63
|
+
3. The agent must **act on injected `pendingSteering` / `pendingActivity` and then ack**
|
|
64
|
+
(`canvas_ax_delivery { action: "mark" }`), or it re-injects every gated turn.
|
|
65
|
+
|
|
66
|
+
To be robust to the char clip, prefer injecting the compact loop-safe lead block from
|
|
67
|
+
`GET /api/canvas/ax/context?consumer=copilot` (`delivery.pendingSteering` +
|
|
68
|
+
`delivery.pendingActivity`) **above** the full dump.
|
|
69
|
+
|
|
70
|
+
### Closing the loop (optional, recommended)
|
|
71
|
+
|
|
72
|
+
- **Forward tool/session hooks** (`onPreToolUse` / `onPostToolUse` /
|
|
73
|
+
`onPostToolUseFailure` / `onSessionStart` / `onSessionEnd` / `onErrorOccurred`) to
|
|
74
|
+
`POST /api/canvas/ax/activity` (`canvas_ingest_activity`) so the board reflects the
|
|
75
|
+
agent's real work automatically (a failed tool → a blocked work item + review +
|
|
76
|
+
evidence).
|
|
77
|
+
- **Await gates** with `canvas_ax_gate { kind, action: "await", id }` (or surface a native modal
|
|
78
|
+
and await the PMX result) so an approval gate actually blocks the agent until the human resolves it.
|
|
79
|
+
|
|
80
|
+
See [`docs/ax-host-adapter-contract.md`](../../../docs/ax-host-adapter-contract.md).
|
|
51
81
|
- Keeps all persistent PMX state in `.pmx-canvas/canvas.db`; the extension does not own canvas
|
|
52
82
|
state.
|
|
53
83
|
|
|
@@ -127,5 +157,5 @@ global instruction to ignore the rest of the repository.
|
|
|
127
157
|
For non-Copilot agents, use the same core primitives directly:
|
|
128
158
|
|
|
129
159
|
- HTTP: `/api/canvas/ax`, `/api/canvas/ax/context`, `/api/canvas/ax/focus`
|
|
130
|
-
- MCP: `canvas://ax`, `canvas://ax-context`, `
|
|
160
|
+
- MCP: `canvas://ax`, `canvas://ax-context`, `canvas_ax_state { action: "get" }`, `canvas_ax_state { action: "set-focus" }`
|
|
131
161
|
- CLI: `pmx-canvas ax status|context|focus`
|
package/src/cli/agent.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from
|
|
|
15
15
|
import { dirname, join } from 'node:path';
|
|
16
16
|
import { fileURLToPath } from 'node:url';
|
|
17
17
|
import { openUrlInExternalBrowser, wrapCanvasAutomationScript } from '../server/server.js';
|
|
18
|
+
import { HttpOperationInvoker, OperationError } from '../server/operations/index.js';
|
|
18
19
|
import { DEFAULT_EXCALIDRAW_ELEMENTS } from '../server/diagram-presets.js';
|
|
19
20
|
import {
|
|
20
21
|
ALL_SEMANTIC_WATCH_EVENT_TYPES,
|
|
@@ -158,6 +159,26 @@ async function api(
|
|
|
158
159
|
return json;
|
|
159
160
|
}
|
|
160
161
|
|
|
162
|
+
// Operation-registry invoker (plan-005): node CRUD, layout reads, edge,
|
|
163
|
+
// pin/search/history/undo/redo, and snapshot commands build their HTTP request
|
|
164
|
+
// from the shared route table instead of hand-written paths.
|
|
165
|
+
// Error handling mirrors api(): operation failures and connection failures
|
|
166
|
+
// die with the same JSON error shape.
|
|
167
|
+
async function invokeOperation(name: string, input: Record<string, unknown>): Promise<unknown> {
|
|
168
|
+
const base = getBaseUrl();
|
|
169
|
+
try {
|
|
170
|
+
return await new HttpOperationInvoker(base).invoke(name, input);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
if (error instanceof OperationError) {
|
|
173
|
+
die(error.message);
|
|
174
|
+
}
|
|
175
|
+
die(
|
|
176
|
+
`Cannot connect to pmx-canvas at ${base}: ${error instanceof Error ? error.message : String(error)}`,
|
|
177
|
+
`Start the server first: pmx-canvas --no-open`,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
161
182
|
// ── Flag parsing ─────────────────────────────────────────────
|
|
162
183
|
|
|
163
184
|
function parseFlags(args: string[]): { positional: string[]; flags: Record<string, string | true> } {
|
|
@@ -1162,13 +1183,13 @@ cmd('node add', 'Add a node to the canvas', [
|
|
|
1162
1183
|
const type = (flags.type as string) || 'markdown';
|
|
1163
1184
|
|
|
1164
1185
|
if (type === 'json-render') {
|
|
1165
|
-
const result = await
|
|
1186
|
+
const result = await invokeOperation('jsonrender.add', await buildJsonRenderRequestBody(flags));
|
|
1166
1187
|
output(result);
|
|
1167
1188
|
return;
|
|
1168
1189
|
}
|
|
1169
1190
|
|
|
1170
1191
|
if (type === 'graph') {
|
|
1171
|
-
const result = await
|
|
1192
|
+
const result = await invokeOperation('graph.add', await buildGraphRequestBody(flags));
|
|
1172
1193
|
output(result);
|
|
1173
1194
|
return;
|
|
1174
1195
|
}
|
|
@@ -1179,13 +1200,13 @@ cmd('node add', 'Add a node to the canvas', [
|
|
|
1179
1200
|
}
|
|
1180
1201
|
|
|
1181
1202
|
if (type === 'html-primitive') {
|
|
1182
|
-
const result = await
|
|
1203
|
+
const result = await invokeOperation('node.add', await buildHtmlPrimitiveRequestBody(flags));
|
|
1183
1204
|
output(result);
|
|
1184
1205
|
return;
|
|
1185
1206
|
}
|
|
1186
1207
|
|
|
1187
1208
|
if (type === 'html' && getStringFlag(flags, 'primitive', 'kind')) {
|
|
1188
|
-
const result = await
|
|
1209
|
+
const result = await invokeOperation('node.add', await buildHtmlPrimitiveRequestBody(flags));
|
|
1189
1210
|
output(result);
|
|
1190
1211
|
return;
|
|
1191
1212
|
}
|
|
@@ -1245,7 +1266,7 @@ cmd('node add', 'Add a node to the canvas', [
|
|
|
1245
1266
|
}
|
|
1246
1267
|
}
|
|
1247
1268
|
|
|
1248
|
-
const result = await
|
|
1269
|
+
const result = await invokeOperation('node.add', body);
|
|
1249
1270
|
output(result);
|
|
1250
1271
|
});
|
|
1251
1272
|
|
|
@@ -1310,7 +1331,7 @@ cmd('graph add', 'Add a graph node to the canvas', [
|
|
|
1310
1331
|
const { flags } = parseFlags(args);
|
|
1311
1332
|
if (flags.help || flags.h) return showCommandHelp('graph add');
|
|
1312
1333
|
|
|
1313
|
-
const result = await
|
|
1334
|
+
const result = await invokeOperation('graph.add', await buildGraphRequestBody(flags));
|
|
1314
1335
|
output(result);
|
|
1315
1336
|
});
|
|
1316
1337
|
|
|
@@ -1383,7 +1404,7 @@ cmd('node list', 'List all nodes on the canvas', [
|
|
|
1383
1404
|
const { flags } = parseFlags(args);
|
|
1384
1405
|
if (flags.help || flags.h) return showCommandHelp('node list');
|
|
1385
1406
|
|
|
1386
|
-
const layout = (await
|
|
1407
|
+
const layout = (await invokeOperation('layout.get', {})) as { nodes: Array<Record<string, unknown>> };
|
|
1387
1408
|
let nodes = layout.nodes;
|
|
1388
1409
|
|
|
1389
1410
|
if (flags.type && flags.type !== true) {
|
|
@@ -1414,7 +1435,7 @@ cmd('node get', 'Get a node by ID', [
|
|
|
1414
1435
|
const id = positional[0];
|
|
1415
1436
|
if (!id) die('Missing node ID', 'pmx-canvas node get <node-id>');
|
|
1416
1437
|
|
|
1417
|
-
const result = await
|
|
1438
|
+
const result = await invokeOperation('node.get', { id }) as Record<string, unknown>;
|
|
1418
1439
|
const requestedFields = collectRequestedFields(args, flags);
|
|
1419
1440
|
if (requestedFields.length > 0) {
|
|
1420
1441
|
const picked = Object.fromEntries(requestedFields.map((field) => [field, resolveNodeFieldValue(result, field)]));
|
|
@@ -1496,7 +1517,7 @@ cmd('node update', 'Update a node by ID', [
|
|
|
1496
1517
|
}
|
|
1497
1518
|
|
|
1498
1519
|
if (x !== undefined || y !== undefined || width !== undefined || frameHeight !== undefined || arrangeLocked !== undefined) {
|
|
1499
|
-
const existing = await
|
|
1520
|
+
const existing = await invokeOperation('node.get', { id }) as {
|
|
1500
1521
|
position: { x: number; y: number };
|
|
1501
1522
|
size: { width: number; height: number };
|
|
1502
1523
|
data: Record<string, unknown>;
|
|
@@ -1545,7 +1566,7 @@ cmd('node update', 'Update a node by ID', [
|
|
|
1545
1566
|
);
|
|
1546
1567
|
}
|
|
1547
1568
|
|
|
1548
|
-
const result = await
|
|
1569
|
+
const result = await invokeOperation('node.update', { id, ...body });
|
|
1549
1570
|
output(result);
|
|
1550
1571
|
});
|
|
1551
1572
|
|
|
@@ -1560,7 +1581,7 @@ cmd('node remove', 'Remove a node from the canvas', [
|
|
|
1560
1581
|
const id = positional[0];
|
|
1561
1582
|
if (!id) die('Missing node ID', 'pmx-canvas node remove <node-id>');
|
|
1562
1583
|
|
|
1563
|
-
const result = await
|
|
1584
|
+
const result = await invokeOperation('node.remove', { id });
|
|
1564
1585
|
output(result);
|
|
1565
1586
|
});
|
|
1566
1587
|
|
|
@@ -1604,7 +1625,7 @@ cmd('edge add', 'Add an edge between two nodes', [
|
|
|
1604
1625
|
if (typeof flags.style === 'string') body.style = flags.style;
|
|
1605
1626
|
if (flags.animated) body.animated = true;
|
|
1606
1627
|
|
|
1607
|
-
const result = await
|
|
1628
|
+
const result = await invokeOperation('edge.add', body);
|
|
1608
1629
|
output(result);
|
|
1609
1630
|
});
|
|
1610
1631
|
|
|
@@ -1629,7 +1650,7 @@ cmd('edge remove', 'Remove an edge by ID', [
|
|
|
1629
1650
|
const id = positional[0];
|
|
1630
1651
|
if (!id) die('Missing edge ID', 'pmx-canvas edge remove <edge-id>');
|
|
1631
1652
|
|
|
1632
|
-
const result = await
|
|
1653
|
+
const result = await invokeOperation('edge.remove', { edge_id: id });
|
|
1633
1654
|
output(result);
|
|
1634
1655
|
});
|
|
1635
1656
|
|
|
@@ -1644,7 +1665,7 @@ cmd('search', 'Search nodes by title or content', [
|
|
|
1644
1665
|
const query = positional[0] || (typeof flags.query === 'string' ? flags.query : '');
|
|
1645
1666
|
if (!query) die('Missing search query', 'pmx-canvas search "query"');
|
|
1646
1667
|
|
|
1647
|
-
const result = await
|
|
1668
|
+
const result = await invokeOperation('search', { q: query });
|
|
1648
1669
|
output(result);
|
|
1649
1670
|
});
|
|
1650
1671
|
|
|
@@ -1815,14 +1836,14 @@ cmd('pin', 'Manage context pins', [
|
|
|
1815
1836
|
}
|
|
1816
1837
|
|
|
1817
1838
|
if (flags.clear) {
|
|
1818
|
-
const result = await
|
|
1839
|
+
const result = await invokeOperation('pin.set', { nodeIds: [] });
|
|
1819
1840
|
output(result);
|
|
1820
1841
|
return;
|
|
1821
1842
|
}
|
|
1822
1843
|
|
|
1823
1844
|
// --set: positional args are node IDs
|
|
1824
1845
|
if (positional.length > 0 || flags.set) {
|
|
1825
|
-
const result = await
|
|
1846
|
+
const result = await invokeOperation('pin.set', { nodeIds: positional });
|
|
1826
1847
|
output(result);
|
|
1827
1848
|
return;
|
|
1828
1849
|
}
|
|
@@ -2314,7 +2335,7 @@ cmd('undo', 'Undo the last canvas mutation', [
|
|
|
2314
2335
|
const { flags } = parseFlags(args);
|
|
2315
2336
|
if (flags.help || flags.h) return showCommandHelp('undo');
|
|
2316
2337
|
|
|
2317
|
-
const result = await
|
|
2338
|
+
const result = await invokeOperation('canvas.undo', {});
|
|
2318
2339
|
output(result);
|
|
2319
2340
|
});
|
|
2320
2341
|
|
|
@@ -2325,7 +2346,7 @@ cmd('redo', 'Redo the last undone mutation', [
|
|
|
2325
2346
|
const { flags } = parseFlags(args);
|
|
2326
2347
|
if (flags.help || flags.h) return showCommandHelp('redo');
|
|
2327
2348
|
|
|
2328
|
-
const result = await
|
|
2349
|
+
const result = await invokeOperation('canvas.redo', {});
|
|
2329
2350
|
output(result);
|
|
2330
2351
|
});
|
|
2331
2352
|
|
|
@@ -2338,7 +2359,7 @@ cmd('history', 'Show canvas mutation history', [
|
|
|
2338
2359
|
const { flags } = parseFlags(args);
|
|
2339
2360
|
if (flags.help || flags.h) return showCommandHelp('history');
|
|
2340
2361
|
|
|
2341
|
-
const result = await
|
|
2362
|
+
const result = await invokeOperation('history.get', {}) as Record<string, unknown>;
|
|
2342
2363
|
if (flags.summary) {
|
|
2343
2364
|
output(summarizeHistoryResult(result));
|
|
2344
2365
|
return;
|
|
@@ -2359,7 +2380,7 @@ cmd('snapshot save', 'Save a named snapshot of the current canvas', [
|
|
|
2359
2380
|
if (flags.help || flags.h) return showCommandHelp('snapshot save');
|
|
2360
2381
|
|
|
2361
2382
|
const name = requireFlag(flags, 'name', 'pmx-canvas snapshot save --name "my-snapshot"');
|
|
2362
|
-
const result = await
|
|
2383
|
+
const result = await invokeOperation('snapshot.save', { name });
|
|
2363
2384
|
output(result);
|
|
2364
2385
|
});
|
|
2365
2386
|
|
|
@@ -2373,17 +2394,17 @@ cmd('snapshot list', 'List saved snapshots', [
|
|
|
2373
2394
|
const { flags } = parseFlags(args);
|
|
2374
2395
|
if (flags.help || flags.h) return showCommandHelp('snapshot list');
|
|
2375
2396
|
|
|
2376
|
-
const params = new URLSearchParams();
|
|
2377
2397
|
const limit = optionalNumberFlag(flags, 'limit', 'Use a positive integer, e.g. --limit 50');
|
|
2378
2398
|
const query = getStringFlag(flags, 'query', 'q');
|
|
2379
2399
|
const before = getStringFlag(flags, 'before');
|
|
2380
2400
|
const after = getStringFlag(flags, 'after');
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2401
|
+
const result = await invokeOperation('snapshot.list', {
|
|
2402
|
+
...(limit !== undefined ? { limit } : {}),
|
|
2403
|
+
...(query ? { q: query } : {}),
|
|
2404
|
+
...(before ? { before } : {}),
|
|
2405
|
+
...(after ? { after } : {}),
|
|
2406
|
+
...(flags.all ? { all: true } : {}),
|
|
2407
|
+
});
|
|
2387
2408
|
output(result);
|
|
2388
2409
|
});
|
|
2389
2410
|
|
|
@@ -2400,7 +2421,7 @@ cmd('snapshot gc', 'Delete old snapshots, keeping the newest N', [
|
|
|
2400
2421
|
if (!dryRun && !flags.yes) {
|
|
2401
2422
|
die('Destructive operation requires --yes flag', 'Preview with: pmx-canvas snapshot gc --keep 20 --dry-run');
|
|
2402
2423
|
}
|
|
2403
|
-
const result = await
|
|
2424
|
+
const result = await invokeOperation('snapshot.gc', {
|
|
2404
2425
|
...(keep !== undefined ? { keep } : {}),
|
|
2405
2426
|
dryRun,
|
|
2406
2427
|
});
|
|
@@ -2417,7 +2438,7 @@ cmd('snapshot restore', 'Restore canvas from a snapshot', [
|
|
|
2417
2438
|
const id = positional[0];
|
|
2418
2439
|
if (!id) die('Missing snapshot ID or name', 'pmx-canvas snapshot restore <snapshot-id-or-name>');
|
|
2419
2440
|
|
|
2420
|
-
const result = await
|
|
2441
|
+
const result = await invokeOperation('snapshot.restore', { id });
|
|
2421
2442
|
output(result);
|
|
2422
2443
|
});
|
|
2423
2444
|
|
|
@@ -2431,7 +2452,7 @@ cmd('snapshot delete', 'Delete a saved snapshot', [
|
|
|
2431
2452
|
const id = positional[0];
|
|
2432
2453
|
if (!id) die('Missing snapshot ID', 'pmx-canvas snapshot delete <snapshot-id>');
|
|
2433
2454
|
|
|
2434
|
-
const result = await
|
|
2455
|
+
const result = await invokeOperation('snapshot.delete', { id });
|
|
2435
2456
|
output(result);
|
|
2436
2457
|
});
|
|
2437
2458
|
|
|
@@ -2439,7 +2460,7 @@ async function runSnapshotDiff(args: string[]): Promise<void> {
|
|
|
2439
2460
|
const { positional, flags } = parseFlags(args);
|
|
2440
2461
|
const snapshot = positional[0];
|
|
2441
2462
|
if (!snapshot) die('Missing snapshot ID or name', 'pmx-canvas snapshot diff <snapshot-id-or-name>');
|
|
2442
|
-
const result = await
|
|
2463
|
+
const result = await invokeOperation('snapshot.diff', { id: snapshot });
|
|
2443
2464
|
output(result);
|
|
2444
2465
|
}
|
|
2445
2466
|
|
|
@@ -97,6 +97,66 @@ export function resolveExtAppSandbox(value: unknown): string {
|
|
|
97
97
|
: DEFAULT_EXT_APP_SANDBOX;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
export function buildExtAppAxBridgeScript(axToken: string, nodeId: string): string {
|
|
101
|
+
return `<script data-pmx-canvas-ax-bridge>
|
|
102
|
+
(function () {
|
|
103
|
+
const PMX_AX_TOKEN = ${JSON.stringify(axToken)};
|
|
104
|
+
const PMX_AX_NODE_ID = ${JSON.stringify(nodeId)};
|
|
105
|
+
window.PMX_AX = window.PMX_AX || {};
|
|
106
|
+
const pending = new Map();
|
|
107
|
+
const ackListeners = [];
|
|
108
|
+
let seq = 0;
|
|
109
|
+
window.PMX_AX.emit = function (type, payload) {
|
|
110
|
+
seq += 1;
|
|
111
|
+
const correlationId = PMX_AX_NODE_ID + '-' + seq + '-' + (Date.now ? Date.now() : 0);
|
|
112
|
+
return new Promise(function (resolve) {
|
|
113
|
+
const timer = setTimeout(function () {
|
|
114
|
+
pending.delete(correlationId);
|
|
115
|
+
resolve({ ok: false, status: 504, code: 'ax-ack-timeout', error: 'ax-ack-timeout' });
|
|
116
|
+
}, 10000);
|
|
117
|
+
pending.set(correlationId, function (result) { clearTimeout(timer); resolve(result); });
|
|
118
|
+
window.parent.postMessage({
|
|
119
|
+
source: 'pmx-canvas-ax',
|
|
120
|
+
token: PMX_AX_TOKEN,
|
|
121
|
+
nodeId: PMX_AX_NODE_ID,
|
|
122
|
+
correlationId: correlationId,
|
|
123
|
+
interaction: { type: String(type), payload: payload && typeof payload === 'object' ? payload : {} },
|
|
124
|
+
}, '*');
|
|
125
|
+
});
|
|
126
|
+
};
|
|
127
|
+
window.PMX_AX.on = function (eventType, cb) {
|
|
128
|
+
if (eventType === 'ack' && typeof cb === 'function') ackListeners.push(cb);
|
|
129
|
+
};
|
|
130
|
+
window.addEventListener('message', function (event) {
|
|
131
|
+
const m = event.data;
|
|
132
|
+
if (!m || m.source !== 'pmx-canvas-ax-ack' || m.token !== PMX_AX_TOKEN) return;
|
|
133
|
+
const result = m.result || { ok: false };
|
|
134
|
+
const resolver = m.correlationId ? pending.get(m.correlationId) : undefined;
|
|
135
|
+
if (resolver) { pending.delete(m.correlationId); resolver(result); }
|
|
136
|
+
for (let i = 0; i < ackListeners.length; i += 1) {
|
|
137
|
+
try { ackListeners[i](result, m.interaction || null); } catch (e) {}
|
|
138
|
+
}
|
|
139
|
+
try { window.dispatchEvent(new CustomEvent('pmx-ax-ack', { detail: { result: result, interaction: m.interaction || null } })); } catch (e) {}
|
|
140
|
+
});
|
|
141
|
+
})();
|
|
142
|
+
</script>`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function injectExtAppAxBridgeScript(html: string, axBridgeScript: string): string {
|
|
146
|
+
if (!axBridgeScript) return html;
|
|
147
|
+
const headMatch = /<head\b[^>]*>/i.exec(html);
|
|
148
|
+
if (headMatch?.index !== undefined) {
|
|
149
|
+
const insertAt = headMatch.index + headMatch[0].length;
|
|
150
|
+
return `${html.slice(0, insertAt)}${axBridgeScript}${html.slice(insertAt)}`;
|
|
151
|
+
}
|
|
152
|
+
const bodyMatch = /<body\b[^>]*>/i.exec(html);
|
|
153
|
+
if (bodyMatch?.index !== undefined) {
|
|
154
|
+
const insertAt = bodyMatch.index + bodyMatch[0].length;
|
|
155
|
+
return `${html.slice(0, insertAt)}${axBridgeScript}${html.slice(insertAt)}`;
|
|
156
|
+
}
|
|
157
|
+
return `${axBridgeScript}${html}`;
|
|
158
|
+
}
|
|
159
|
+
|
|
100
160
|
function positiveDimension(value: number, fallback: number): number {
|
|
101
161
|
if (Number.isFinite(value) && value > 0) return Math.round(value);
|
|
102
162
|
if (Number.isFinite(fallback) && fallback > 0) return Math.round(fallback);
|
|
@@ -164,31 +224,39 @@ export function ExtAppFrame({ node, expanded = false }: { node: CanvasNodeState;
|
|
|
164
224
|
const axEnabled = axCaps?.enabled === true && typeof html === 'string' && html.length > 0;
|
|
165
225
|
const axToken = useMemo(() => `ax-${crypto.randomUUID()}`, []);
|
|
166
226
|
const axBridgeScript = axEnabled
|
|
167
|
-
?
|
|
227
|
+
? buildExtAppAxBridgeScript(axToken, nodeId)
|
|
168
228
|
: '';
|
|
169
|
-
const iframeDocument = useIframeDocument((html ?? ''
|
|
229
|
+
const iframeDocument = useIframeDocument(injectExtAppAxBridgeScript(html ?? '', axBridgeScript), iframeSandbox);
|
|
170
230
|
|
|
171
231
|
useEffect(() => {
|
|
172
232
|
if (!axEnabled) return;
|
|
173
233
|
function onAxMessage(event: MessageEvent) {
|
|
174
234
|
if (event.source !== iframeRef.current?.contentWindow) return;
|
|
175
235
|
const data = event.data as {
|
|
176
|
-
source?: string; token?: string; nodeId?: string;
|
|
236
|
+
source?: string; token?: string; nodeId?: string; correlationId?: string;
|
|
177
237
|
interaction?: { type?: unknown; payload?: unknown };
|
|
178
238
|
} | null;
|
|
179
239
|
if (!data || data.source !== 'pmx-canvas-ax' || data.token !== axToken || data.nodeId !== nodeId) return;
|
|
180
240
|
const interaction = data.interaction;
|
|
181
241
|
if (!interaction || typeof interaction.type !== 'string') return;
|
|
242
|
+
const interactionType = interaction.type;
|
|
182
243
|
void submitAxInteractionFromClient({
|
|
183
|
-
type:
|
|
244
|
+
type: interactionType,
|
|
184
245
|
sourceNodeId: nodeId,
|
|
185
246
|
sourceSurface: 'mcp-app',
|
|
186
247
|
...(interaction.payload && typeof interaction.payload === 'object'
|
|
187
248
|
? { payload: interaction.payload as Record<string, unknown> }
|
|
188
249
|
: {}),
|
|
189
250
|
}).then((res) => {
|
|
190
|
-
if (res.ok) showToast('context', 'AX interaction',
|
|
251
|
+
if (res.ok) showToast('context', 'AX interaction', interactionType, [nodeId]);
|
|
191
252
|
else showToast('remove', 'AX interaction rejected', res.error ?? res.code ?? '', [nodeId]);
|
|
253
|
+
iframeRef.current?.contentWindow?.postMessage({
|
|
254
|
+
source: 'pmx-canvas-ax-ack',
|
|
255
|
+
token: axToken,
|
|
256
|
+
...(data.correlationId ? { correlationId: data.correlationId } : {}),
|
|
257
|
+
interaction: { type: interactionType },
|
|
258
|
+
result: res,
|
|
259
|
+
}, '*');
|
|
192
260
|
});
|
|
193
261
|
}
|
|
194
262
|
window.addEventListener('message', onAxMessage);
|
|
@@ -57,22 +57,31 @@ export function HtmlNode({
|
|
|
57
57
|
// nodeId are a second gate, not the only one.
|
|
58
58
|
if (event.source !== iframeRef.current?.contentWindow) return;
|
|
59
59
|
const data = event.data as {
|
|
60
|
-
source?: string; token?: string; nodeId?: string;
|
|
60
|
+
source?: string; token?: string; nodeId?: string; correlationId?: string;
|
|
61
61
|
interaction?: { type?: unknown; payload?: unknown };
|
|
62
62
|
} | null;
|
|
63
63
|
if (!data || data.source !== 'pmx-canvas-ax' || data.token !== axToken || data.nodeId !== node.id) return;
|
|
64
64
|
const interaction = data.interaction;
|
|
65
65
|
if (!interaction || typeof interaction.type !== 'string') return;
|
|
66
|
+
const interactionType = interaction.type;
|
|
66
67
|
void submitAxInteractionFromClient({
|
|
67
|
-
type:
|
|
68
|
+
type: interactionType,
|
|
68
69
|
sourceNodeId: node.id,
|
|
69
70
|
sourceSurface: 'html-node',
|
|
70
71
|
...(interaction.payload && typeof interaction.payload === 'object'
|
|
71
72
|
? { payload: interaction.payload as Record<string, unknown> }
|
|
72
73
|
: {}),
|
|
73
74
|
}).then((res) => {
|
|
74
|
-
if (res.ok) showToast('context', 'AX interaction',
|
|
75
|
+
if (res.ok) showToast('context', 'AX interaction', interactionType, [node.id]);
|
|
75
76
|
else showToast('remove', 'AX interaction rejected', res.error ?? res.code ?? '', [node.id]);
|
|
77
|
+
// Report #55: ack back to the surface so it can self-confirm (e.g. "queued ✓").
|
|
78
|
+
iframeRef.current?.contentWindow?.postMessage({
|
|
79
|
+
source: 'pmx-canvas-ax-ack',
|
|
80
|
+
token: axToken,
|
|
81
|
+
...(data.correlationId ? { correlationId: data.correlationId } : {}),
|
|
82
|
+
interaction: { type: interactionType },
|
|
83
|
+
result: res,
|
|
84
|
+
}, '*');
|
|
76
85
|
});
|
|
77
86
|
}
|
|
78
87
|
window.addEventListener('message', onAxMessage);
|
|
@@ -89,22 +89,31 @@ function McpAppViewer({ node, expanded }: { node: CanvasNodeState; expanded: boo
|
|
|
89
89
|
function onAxMessage(event: MessageEvent) {
|
|
90
90
|
if (event.source !== iframeRef.current?.contentWindow) return;
|
|
91
91
|
const data = event.data as {
|
|
92
|
-
source?: string; token?: string; nodeId?: string;
|
|
92
|
+
source?: string; token?: string; nodeId?: string; correlationId?: string;
|
|
93
93
|
interaction?: { type?: unknown; payload?: unknown };
|
|
94
94
|
} | null;
|
|
95
95
|
if (!data || data.source !== 'pmx-canvas-ax' || data.token !== axToken || data.nodeId !== node.id) return;
|
|
96
96
|
const interaction = data.interaction;
|
|
97
97
|
if (!interaction || typeof interaction.type !== 'string') return;
|
|
98
|
+
const interactionType = interaction.type;
|
|
98
99
|
void submitAxInteractionFromClient({
|
|
99
|
-
type:
|
|
100
|
+
type: interactionType,
|
|
100
101
|
sourceNodeId: node.id,
|
|
101
102
|
sourceSurface: axSurface,
|
|
102
103
|
...(interaction.payload && typeof interaction.payload === 'object'
|
|
103
104
|
? { payload: interaction.payload as Record<string, unknown> }
|
|
104
105
|
: {}),
|
|
105
106
|
}).then((res) => {
|
|
106
|
-
if (res.ok) showToast('context', 'AX interaction',
|
|
107
|
+
if (res.ok) showToast('context', 'AX interaction', interactionType, [node.id]);
|
|
107
108
|
else showToast('remove', 'AX interaction rejected', res.error ?? res.code ?? '', [node.id]);
|
|
109
|
+
// Report #55: ack back to the viewer so the surface can self-confirm.
|
|
110
|
+
iframeRef.current?.contentWindow?.postMessage({
|
|
111
|
+
source: 'pmx-canvas-ax-ack',
|
|
112
|
+
token: axToken,
|
|
113
|
+
...(data.correlationId ? { correlationId: data.correlationId } : {}),
|
|
114
|
+
interaction: { type: interactionType },
|
|
115
|
+
result: res,
|
|
116
|
+
}, '*');
|
|
108
117
|
});
|
|
109
118
|
}
|
|
110
119
|
window.addEventListener('message', onAxMessage);
|
|
@@ -121,6 +121,9 @@ function buildAxHandlers(): Record<string, (params: Record<string, unknown>) =>
|
|
|
121
121
|
const token = window.__PMX_CANVAS_AX_TOKEN__;
|
|
122
122
|
const handlers: Record<string, (params: Record<string, unknown>) => void> = {};
|
|
123
123
|
if (!nodeId || !token) return handlers;
|
|
124
|
+
// Declarative json-render boards are reflect-only: a spec action is fire-and-forget
|
|
125
|
+
// and confirmation arrives as a live `pmx-ax-update` (the work item appears). There
|
|
126
|
+
// is no JS surface for a Promise-style ack here, so we don't stamp a correlationId.
|
|
124
127
|
for (const type of AX_INTERACTION_HANDLER_NAMES) {
|
|
125
128
|
handlers[type] = (params: Record<string, unknown>) => {
|
|
126
129
|
window.parent.postMessage({
|