pmx-canvas 0.1.29 → 0.1.31
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 +219 -0
- package/Readme.md +20 -10
- package/dist/canvas/global.css +51 -56
- package/dist/canvas/index.js +80 -163
- package/dist/canvas/surface-theme.css +142 -0
- package/dist/json-render/index.js +103 -103
- package/dist/types/client/nodes/HtmlNode.d.ts +0 -7
- package/dist/types/client/nodes/ax-node-actions.d.ts +18 -0
- package/dist/types/client/nodes/surface-url.d.ts +22 -0
- package/dist/types/client/state/attention-bridge.d.ts +3 -0
- package/dist/types/client/state/intent-bridge.d.ts +17 -0
- package/dist/types/json-render/renderer/index.d.ts +2 -0
- package/dist/types/json-render/schema.d.ts +2 -0
- package/dist/types/json-render/server.d.ts +2 -0
- package/dist/types/mcp/canvas-access.d.ts +47 -0
- package/dist/types/server/ax-interaction.d.ts +210 -0
- package/dist/types/server/ax-state.d.ts +67 -1
- package/dist/types/server/canvas-db.d.ts +4 -0
- package/dist/types/server/canvas-serialization.d.ts +2 -0
- package/dist/types/server/canvas-state.d.ts +54 -2
- package/dist/types/server/html-surface.d.ts +46 -0
- package/dist/types/server/index.d.ts +63 -5
- package/dist/types/server/mutation-history.d.ts +1 -1
- package/dist/types/server/placement.d.ts +1 -1
- package/dist/types/server/server.d.ts +12 -0
- package/dist/types/shared/surface.d.ts +19 -0
- package/docs/cli.md +30 -0
- package/docs/http-api.md +55 -0
- package/docs/mcp.md +40 -2
- package/docs/node-types.md +26 -0
- package/docs/plans/plan-004-pmx-ax-primitives.md +623 -394
- package/docs/sdk.md +23 -1
- package/package.json +2 -2
- package/skills/pmx-canvas/SKILL.md +107 -9
- package/src/cli/agent.ts +177 -0
- package/src/cli/index.ts +8 -1
- package/src/client/canvas/CanvasNode.tsx +8 -4
- package/src/client/canvas/DockedNode.tsx +38 -38
- package/src/client/canvas/ExpandedNodeOverlay.tsx +12 -0
- package/src/client/nodes/ContextNode.tsx +17 -0
- package/src/client/nodes/ExtAppFrame.tsx +40 -3
- package/src/client/nodes/FileNode.tsx +26 -0
- package/src/client/nodes/HtmlNode.tsx +60 -188
- package/src/client/nodes/McpAppNode.tsx +47 -2
- package/src/client/nodes/StatusNode.tsx +20 -0
- package/src/client/nodes/ax-node-actions.ts +39 -0
- package/src/client/nodes/surface-url.ts +48 -0
- package/src/client/state/attention-bridge.ts +5 -0
- package/src/client/state/intent-bridge.ts +33 -0
- package/src/client/theme/global.css +51 -56
- package/src/client/theme/surface-theme.css +142 -0
- package/src/json-render/renderer/index.tsx +31 -0
- package/src/json-render/schema.ts +4 -0
- package/src/json-render/server.ts +13 -0
- package/src/mcp/canvas-access.ts +198 -1
- package/src/mcp/server.ts +232 -2
- package/src/server/ax-context.ts +3 -0
- package/src/server/ax-interaction.ts +549 -0
- package/src/server/ax-state.ts +188 -2
- package/src/server/canvas-db.ts +20 -0
- package/src/server/canvas-operations.ts +11 -0
- package/src/server/canvas-serialization.ts +9 -0
- package/src/server/canvas-state.ts +201 -26
- package/src/server/html-surface.ts +190 -0
- package/src/server/index.ts +122 -7
- package/src/server/mutation-history.ts +5 -0
- package/src/server/placement.ts +5 -1
- package/src/server/server.ts +360 -0
- package/src/shared/surface.ts +38 -0
package/docs/sdk.md
CHANGED
|
@@ -15,7 +15,9 @@ import { createCanvas } from 'pmx-canvas';
|
|
|
15
15
|
const canvas = createCanvas({ port: 4313 });
|
|
16
16
|
await canvas.start({ open: true });
|
|
17
17
|
|
|
18
|
-
// Add nodes — addNode
|
|
18
|
+
// Add nodes — addNode/getNode/addHtmlNode return the created node: `.id`
|
|
19
|
+
// (plus a `.nodeId` alias), geometry, `.data`, and `.surfaceUrl` for
|
|
20
|
+
// surface-eligible types (html, json-render, graph, …).
|
|
19
21
|
const n1 = canvas.addNode({ type: 'markdown', title: 'Plan', content: '# Step 1\nDo the thing.' });
|
|
20
22
|
const n2 = canvas.addNode({ type: 'status', title: 'Build', content: 'passing' });
|
|
21
23
|
const n3 = canvas.addNode({ type: 'file', content: 'src/index.ts' });
|
|
@@ -106,6 +108,26 @@ canvas.addReviewAnnotation({ body: 'off-by-one', kind: 'finding', severity: 'err
|
|
|
106
108
|
// Host/session capability (own table, survives clear)
|
|
107
109
|
canvas.reportHostCapability({ host: 'copilot', canvas: true, sessionMessaging: true }, { source: 'sdk' });
|
|
108
110
|
console.log(canvas.getHostCapability());
|
|
111
|
+
|
|
112
|
+
// Node interactions — one capability-gated envelope; the server re-validates and
|
|
113
|
+
// clamps sandboxed surfaces (html-node/mcp-app/json-render) to their own node.
|
|
114
|
+
canvas.submitAxInteraction({ type: 'ax.work.create', sourceNodeId: n1, payload: { title: 'Wire auth' } });
|
|
115
|
+
|
|
116
|
+
// Delivery — claim pending steering for a consumer (loop-safe), then acknowledge
|
|
117
|
+
const pending = canvas.getPendingSteering({ consumer: 'copilot', limit: 20 });
|
|
118
|
+
if (pending[0]) canvas.markSteeringDelivered(pending[0].id, { consumer: 'copilot' });
|
|
119
|
+
|
|
120
|
+
// Elicitation + mode requests (canvas-bound, snapshotted)
|
|
121
|
+
const elic = canvas.requestElicitation({ prompt: 'Who owns this?', fields: ['owner'] }, { source: 'sdk' });
|
|
122
|
+
canvas.respondElicitation(elic.id, { owner: 'alice' });
|
|
123
|
+
const mode = canvas.requestMode({ mode: 'execute', reason: 'plan approved' }, { source: 'sdk' });
|
|
124
|
+
canvas.resolveModeRequest(mode.id, 'approved');
|
|
125
|
+
|
|
126
|
+
// Command registry + tool/prompt policy
|
|
127
|
+
console.log(canvas.getCommandRegistry());
|
|
128
|
+
canvas.invokeCommand('pmx.plan', { note: 'draft a plan' }, { source: 'sdk' });
|
|
129
|
+
canvas.setPolicy({ tools: { excluded: ['shell'] }, prompt: { mode: 'concise' } }, { source: 'sdk' });
|
|
130
|
+
console.log(canvas.getPolicy());
|
|
109
131
|
```
|
|
110
132
|
|
|
111
133
|
## WebView automation
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmx-canvas",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.31",
|
|
4
4
|
"description": "Spatial canvas workbench for coding agents — infinite 2D canvas with agent-native CLI, MCP integration, nodes, edges, file watching, and snapshots",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/server/index.ts",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"dev:demo": "bun run src/cli/index.ts --demo",
|
|
33
33
|
"dev:portless": "portless run --name pmx --app-port 4313 bun run src/cli/index.ts --no-open --port=4313",
|
|
34
34
|
"dev:portless:demo": "portless run --name pmx --app-port 4313 bun run src/cli/index.ts --no-open --port=4313 --demo",
|
|
35
|
-
"build:client": "bun build src/client/index.tsx --outdir dist/canvas --minify && cp src/client/theme/global.css dist/canvas/global.css",
|
|
35
|
+
"build:client": "bun build src/client/index.tsx --outdir dist/canvas --minify && cp src/client/theme/global.css dist/canvas/global.css && cp src/client/theme/surface-theme.css dist/canvas/surface-theme.css",
|
|
36
36
|
"build:json-render": "bash scripts/build-json-render.sh",
|
|
37
37
|
"build:types": "tsc -p tsconfig.types.json",
|
|
38
38
|
"build": "bun run build:client && bun run build:json-render && bun run build:types",
|
|
@@ -75,14 +75,19 @@ reference before using adapter-native features:
|
|
|
75
75
|
- `references/codex-app-adapter.md` — Codex app native Browser + MCP adapter, AX context reading,
|
|
76
76
|
focus labeling, and live-test checklist.
|
|
77
77
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
-
|
|
84
|
-
|
|
85
|
-
|
|
78
|
+
Open the canvas first — always:
|
|
79
|
+
The canvas is the shared human↔agent surface. **Before you create or mutate any nodes, make the
|
|
80
|
+
workbench visible.** Do **not** assume the host opened it for you — some hosts (e.g. the Codex app)
|
|
81
|
+
do not open it on their own. Take the action to open it yourself, whatever the host:
|
|
82
|
+
- **Native adapter/panel available** (the GitHub Copilot app `pmx-canvas` canvas extension, or the
|
|
83
|
+
Codex in-app Browser): open/focus that panel to the server's `/workbench` route.
|
|
84
|
+
- **Any other browser** (Chrome, Safari, Arc, Edge, …) **or a generic/CLI agent** with no native
|
|
85
|
+
panel: open the server's `/workbench` URL in a browser.
|
|
86
|
+
|
|
87
|
+
Then reuse that **single** surface for the rest of the session — do **not** open a second panel to
|
|
88
|
+
the same workbench (it wastes space and confuses which surface is authoritative). If you genuinely
|
|
89
|
+
cannot open any browser (headless/CI), say so and proceed, but still print the `/workbench` URL so a
|
|
90
|
+
human can open it.
|
|
86
91
|
- External URLs in `mcp-app` nodes show the "Unverified domain" interstitial by design. Only
|
|
87
92
|
same-origin `/api/canvas/frame-documents/<id>` URLs are auto-trusted. For external tools, use a
|
|
88
93
|
bundled `web-artifact`, same-origin frame document, or set `data.trustedDomain: true` only when the
|
|
@@ -213,6 +218,9 @@ pmx-canvas spatial
|
|
|
213
218
|
`ax review add|list` — canvas-bound AX state (work items, approval gates,
|
|
214
219
|
review annotations) that rides snapshots and restore and is cleared by `clear`.
|
|
215
220
|
- `ax host report|status` — report/read the host/session capability (own partition).
|
|
221
|
+
- `ax command list|invoke`, `ax policy get|set` — list/invoke registry commands
|
|
222
|
+
(`pmx.plan`, `pmx.execute`, `pmx.promote-context`, `pmx.summarize`, `pmx.review`)
|
|
223
|
+
and read/patch the canvas-bound tool/prompt policy.
|
|
216
224
|
- `copilot install-extension [--dry-run] [--yes]` — install the bundled GitHub
|
|
217
225
|
Copilot adapter into a repo; the core stays host-agnostic.
|
|
218
226
|
- `fit [id ...]` — set the server viewport to fit the whole canvas or selected nodes before screenshots or whole-board review
|
|
@@ -290,6 +298,24 @@ points from `from` to `to`, indicating sequence or data flow direction.
|
|
|
290
298
|
pending dependencies, and `dotted` for weak/optional relationships. Use `animated: true` to
|
|
291
299
|
draw visual attention to critical paths.
|
|
292
300
|
|
|
301
|
+
### Layout: spacing and groups
|
|
302
|
+
|
|
303
|
+
Agents tend to pack boards too tightly. Give nodes room to breathe — readability beats density.
|
|
304
|
+
|
|
305
|
+
- **Default spacing:** leave a clear gap between neighbors — roughly half a node's width
|
|
306
|
+
horizontally and half its height vertically. A 280×180 node reads well with ~360px between
|
|
307
|
+
column origins and ~260px between row origins.
|
|
308
|
+
- **When nodes are connected by edges, space them further apart** so the edge line, its arrowhead,
|
|
309
|
+
and its label are clearly visible in the gap between nodes. Crowded nodes hide the flow — this is
|
|
310
|
+
the most common cause of an unreadable board.
|
|
311
|
+
- **Inside a group, keep the same breathing room** (groups no longer auto-pack children, so the
|
|
312
|
+
spacing you set is what the human sees). Edges between grouped children especially need the gap.
|
|
313
|
+
- **Size a group frame larger than its children's bounding box** so the group header — including the
|
|
314
|
+
node-count badge — stays visible and isn't hidden under the top-left child. For an explicit
|
|
315
|
+
(manual) group frame, add margin on every side (≈56px) plus room at the top for the header rather
|
|
316
|
+
than hugging the children. Auto-fit groups (created without an explicit width/height) already
|
|
317
|
+
reserve this margin.
|
|
318
|
+
|
|
293
319
|
### Colors (Semantic)
|
|
294
320
|
|
|
295
321
|
Use color consistently to convey meaning:
|
|
@@ -740,6 +766,28 @@ server's `ui://` resource as an iframe node on the canvas
|
|
|
740
766
|
- Read `htmlPrimitives` from `canvas_describe_schema` for the data shape and examples before constructing a payload
|
|
741
767
|
- For payload patterns, export loops, and the primitive catalog, read `references/html-primitives.md` before creating dense or editable artifacts
|
|
742
768
|
|
|
769
|
+
### Open as Site (standalone surfaces)
|
|
770
|
+
|
|
771
|
+
Any renderable surface node can be opened full-page in its own browser tab — the same
|
|
772
|
+
document it shows in the canvas, just without the node chrome. In the workbench, use the
|
|
773
|
+
↗ **Open as site** button in the node title bar (or the expanded overlay).
|
|
774
|
+
|
|
775
|
+
- Works for `html` / `html-primitive`, bundled `web-artifact`, `json-render` / `graph`,
|
|
776
|
+
`webpage`, and hosted ext-app `mcp-app` nodes.
|
|
777
|
+
- The tab loads the node's stable surface URL, `/api/canvas/surface/<nodeId>`. The
|
|
778
|
+
in-canvas iframe loads the **exact same URL**, so there is one render path and no
|
|
779
|
+
separate "preview" version — what you see in the canvas is what opens. The URL reflects
|
|
780
|
+
current node state and survives a refresh.
|
|
781
|
+
- Agents can read this URL from any node payload (`canvas_get_node` / `canvas_get_layout`)
|
|
782
|
+
as `surfaceUrl` — a reliable way to tell a human "open the artifact" without disturbing
|
|
783
|
+
the canvas.
|
|
784
|
+
- Served HTML stays sandboxed (opaque origin via a `Content-Security-Policy: sandbox`
|
|
785
|
+
response header), so opening author code top-level cannot reach the canvas origin.
|
|
786
|
+
- ext-app `mcp-app` nodes open their UI, but interactive tool-calls only work inside the
|
|
787
|
+
canvas (the host bridge has no peer in a bare tab). `webpage` and URL-backed `mcp-app`
|
|
788
|
+
nodes redirect to their external site.
|
|
789
|
+
- This is additive — opening a site never evicts or replaces canvas nodes.
|
|
790
|
+
|
|
743
791
|
### Choosing the Right Visual Tier
|
|
744
792
|
|
|
745
793
|
When the output is more than markdown, pick the lightest tier that fits:
|
|
@@ -761,6 +809,8 @@ Use native `json-render` and `graph` nodes when the output should stay fully ins
|
|
|
761
809
|
3. Use the repo-local `json-render-*` skills when you need help authoring or refining the spec itself
|
|
762
810
|
4. Use `canvas_build_web_artifact` instead when the result needs a full custom React app rather than a schema-driven UI
|
|
763
811
|
|
|
812
|
+
Spec elements support an `on` map (`on.press`, `on.change`, …) binding events to actions (`{ action, params }`) — built-in actions (`setState`, `pushState`, …) or, when named after an AX interaction type, a capability-gated AX emit. e.g. a Button with `on: { press: { action: 'ax.work.create', params: { title: '…' } } }` lets a human turn a panel control into a tracked work item; the viewer forwards it to the canvas, which validates and submits it server-side (clamped to the node's own id). See **Node AX Interactions** above.
|
|
813
|
+
|
|
764
814
|
## MCP Resources
|
|
765
815
|
|
|
766
816
|
These resources give you read access to canvas intelligence. Read them to understand
|
|
@@ -777,10 +827,58 @@ what the human has set up and what they're focusing on.
|
|
|
777
827
|
| `canvas://code-graph` | Auto-detected file import dependencies (JS/TS, Python, Go, Rust) |
|
|
778
828
|
| `canvas://ax` | Host-agnostic AX state: focus, work items, approval gates, review annotations, host capability |
|
|
779
829
|
| `canvas://ax-context` | Agent-ready AX context: pinned context + current focus |
|
|
780
|
-
| `canvas://ax-work` | Canvas-bound AX work: work items, approval gates, review annotations |
|
|
830
|
+
| `canvas://ax-work` | Canvas-bound AX work: work items, approval gates, review annotations, elicitations, mode requests, and tool/prompt policy |
|
|
781
831
|
| `canvas://ax-timeline` | Bounded AX timeline: recent agent events, evidence, and steering messages |
|
|
832
|
+
| `canvas://ax-pending-steering` | Undelivered steering an MCP client can claim, act on, and mark delivered (adapterless delivery) |
|
|
833
|
+
| `canvas://ax-delivery` | Steering delivery state (delivered flag) for diagnostics |
|
|
782
834
|
| `canvas://skills` | Index of bundled agent skills shipped with the install. Each skill is also addressable as `canvas://skills/<name>` (e.g. `canvas://skills/web-artifacts-builder`) and returns the full SKILL.md. Read this resource first to discover companion workflows the canvas is built to support. |
|
|
783
835
|
|
|
836
|
+
### Node AX Interactions (capability-gated)
|
|
837
|
+
|
|
838
|
+
Eligible nodes can emit one normalized, validated AX interaction that maps onto an
|
|
839
|
+
AX operation — work item, evidence, approval, review, focus, steering, event,
|
|
840
|
+
elicitation, or mode request. One envelope, many transports:
|
|
841
|
+
|
|
842
|
+
- **Endpoint:** `POST /api/canvas/ax/interaction` with
|
|
843
|
+
`{ type, sourceNodeId, payload }` (MCP: `canvas_ax_interaction`; CLI:
|
|
844
|
+
`pmx-canvas ax interaction`). Returns `{ ok, primitive }` or
|
|
845
|
+
`{ ok: false, code }` if the node type/metadata disallows the type.
|
|
846
|
+
- **Capabilities:** each node type has a default capability set (a ceiling). A
|
|
847
|
+
node may opt in or narrow via `data.axCapabilities` (`{ enabled, allowed }`),
|
|
848
|
+
clamped to the ceiling — a node can never escalate beyond its type's ceiling.
|
|
849
|
+
`html` / `html-primitive`, `mcp-app`, and internal `prompt` / `response` nodes
|
|
850
|
+
are **disabled by default** (opt-in).
|
|
851
|
+
- **Transports:** native node controls call the endpoint directly. Sandboxed
|
|
852
|
+
surfaces emit via a nonce-tagged `postMessage` the parent canvas validates
|
|
853
|
+
before submitting: `html` / `html-primitive` nodes (when opted in) call
|
|
854
|
+
`window.PMX_AX.emit(type, payload)`; **json-render / graph** viewers forward a
|
|
855
|
+
spec action named after an AX type (e.g. `on.press → { action:
|
|
856
|
+
"ax.work.create", params }`, `sourceSurface: 'json-render'`); opted-in ext-app
|
|
857
|
+
**`mcp-app`** nodes get the same `window.PMX_AX.emit` injected
|
|
858
|
+
(`sourceSurface: 'mcp-app'`). The server re-validates capabilities regardless
|
|
859
|
+
of transport — bridges are convenience, not a trust boundary.
|
|
860
|
+
- **Delivery:** steering can be claimed by adapterless MCP clients via
|
|
861
|
+
`canvas://ax-pending-steering` / `canvas_claim_ax_delivery` and acknowledged
|
|
862
|
+
with `canvas_mark_ax_delivery` (loop-safe — a consumer never receives steering
|
|
863
|
+
it originated).
|
|
864
|
+
- **Elicitation / mode:** request structured human input
|
|
865
|
+
(`canvas_request_elicitation` → `canvas_respond_elicitation`) or a workflow
|
|
866
|
+
mode transition (`canvas_request_mode` → `canvas_resolve_mode`); both are
|
|
867
|
+
canvas-bound and snapshotted.
|
|
868
|
+
- **Commands:** invoke a registry command — `pmx.plan`, `pmx.execute`,
|
|
869
|
+
`pmx.promote-context`, `pmx.summarize`, `pmx.review` — via
|
|
870
|
+
`canvas_invoke_command` (HTTP `POST /api/canvas/ax/command`; CLI
|
|
871
|
+
`pmx-canvas ax command invoke`; envelope `ax.command.invoke`). Unknown names
|
|
872
|
+
are rejected; an invocation records an `agent-event` of kind `command`.
|
|
873
|
+
- **Policy:** a canvas-bound, snapshotted tool/prompt policy
|
|
874
|
+
(`tools.allowed|excluded|approvalRequired`, `prompt.systemAppend|mode`) read
|
|
875
|
+
into `canvas://ax-context`. Patch it with `canvas_set_ax_policy` (HTTP
|
|
876
|
+
`GET|POST /api/canvas/ax/policy`; CLI `pmx-canvas ax policy get|set`); patches
|
|
877
|
+
merge and are normalized server-side.
|
|
878
|
+
|
|
879
|
+
Interactions request PMX-AX primitives only — never arbitrary shell, tool, MCP,
|
|
880
|
+
or host execution.
|
|
881
|
+
|
|
784
882
|
### Reading Spatial Intent
|
|
785
883
|
|
|
786
884
|
The `canvas://spatial-context` resource reveals how the human has organized information:
|
package/src/cli/agent.ts
CHANGED
|
@@ -1099,6 +1099,11 @@ const RESOURCE_SUBCOMMAND_HINTS: Record<string, Record<string, string>> = {
|
|
|
1099
1099
|
work: 'Pick an action: pmx-canvas ax work add | update | list',
|
|
1100
1100
|
approval: 'Pick an action: pmx-canvas ax approval request | resolve | list',
|
|
1101
1101
|
review: 'Pick an action: pmx-canvas ax review add | list',
|
|
1102
|
+
delivery: 'Pick an action: pmx-canvas ax delivery list | mark',
|
|
1103
|
+
elicitation: 'Pick an action: pmx-canvas ax elicitation request | respond | list',
|
|
1104
|
+
mode: 'Pick an action: pmx-canvas ax mode request | resolve | list',
|
|
1105
|
+
command: 'Pick an action: pmx-canvas ax command list | invoke',
|
|
1106
|
+
policy: 'Pick an action: pmx-canvas ax policy get | set',
|
|
1102
1107
|
},
|
|
1103
1108
|
};
|
|
1104
1109
|
|
|
@@ -1879,6 +1884,178 @@ cmd('ax steer', 'Send a steering message to the active agent session', [
|
|
|
1879
1884
|
output(await api('POST', '/api/canvas/ax/steer', { message, source: 'cli' }));
|
|
1880
1885
|
});
|
|
1881
1886
|
|
|
1887
|
+
cmd('ax interaction', 'Submit a node-originated AX interaction (capability-gated)', [
|
|
1888
|
+
'pmx-canvas ax interaction --type ax.work.create --node node-1 --payload \'{"title":"Wire auth"}\'',
|
|
1889
|
+
'pmx-canvas ax interaction --type ax.focus.set --node node-2',
|
|
1890
|
+
], async (args) => {
|
|
1891
|
+
const { flags } = parseFlags(args);
|
|
1892
|
+
if (flags.help || flags.h) return showCommandHelp('ax interaction');
|
|
1893
|
+
|
|
1894
|
+
const type = getStringFlag(flags, 'type');
|
|
1895
|
+
if (!type) die('Missing --type', 'pmx-canvas ax interaction --type <ax.*> --node <id> [--payload <json>]');
|
|
1896
|
+
const sourceNodeId = getStringFlag(flags, 'node');
|
|
1897
|
+
if (!sourceNodeId) die('Missing --node', 'pmx-canvas ax interaction --type <ax.*> --node <id>');
|
|
1898
|
+
|
|
1899
|
+
let payload: unknown;
|
|
1900
|
+
const payloadRaw = getStringFlag(flags, 'payload');
|
|
1901
|
+
if (payloadRaw) {
|
|
1902
|
+
try {
|
|
1903
|
+
payload = JSON.parse(payloadRaw);
|
|
1904
|
+
} catch {
|
|
1905
|
+
die('Invalid --payload JSON', 'pmx-canvas ax interaction --payload \'{"title":"..."}\'');
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
output(await api('POST', '/api/canvas/ax/interaction', {
|
|
1910
|
+
type,
|
|
1911
|
+
sourceNodeId,
|
|
1912
|
+
...(payload !== undefined ? { payload } : {}),
|
|
1913
|
+
source: 'cli',
|
|
1914
|
+
}));
|
|
1915
|
+
});
|
|
1916
|
+
|
|
1917
|
+
cmd('ax delivery list', 'List pending AX steering for a consumer (loop-safe)', [
|
|
1918
|
+
'pmx-canvas ax delivery list',
|
|
1919
|
+
'pmx-canvas ax delivery list --consumer copilot --limit 20',
|
|
1920
|
+
], async (args) => {
|
|
1921
|
+
const { flags } = parseFlags(args);
|
|
1922
|
+
if (flags.help || flags.h) return showCommandHelp('ax delivery list');
|
|
1923
|
+
const consumer = getStringFlag(flags, 'consumer');
|
|
1924
|
+
const limit = optionalNumberFlag(flags, 'limit', 'pmx-canvas ax delivery list --limit <n>');
|
|
1925
|
+
const params = new URLSearchParams();
|
|
1926
|
+
if (consumer) params.set('consumer', consumer);
|
|
1927
|
+
if (limit) params.set('limit', String(limit));
|
|
1928
|
+
const qs = params.toString();
|
|
1929
|
+
output(await api('GET', `/api/canvas/ax/delivery/pending${qs ? `?${qs}` : ''}`));
|
|
1930
|
+
});
|
|
1931
|
+
|
|
1932
|
+
cmd('ax delivery mark', 'Mark an AX steering message as delivered', [
|
|
1933
|
+
'pmx-canvas ax delivery mark <steering-id>',
|
|
1934
|
+
], async (args) => {
|
|
1935
|
+
const { positional, flags } = parseFlags(args);
|
|
1936
|
+
if (flags.help || flags.h) return showCommandHelp('ax delivery mark');
|
|
1937
|
+
const id = getStringFlag(flags, 'id') ?? positional[0];
|
|
1938
|
+
if (!id) die('Missing steering id', 'pmx-canvas ax delivery mark <steering-id>');
|
|
1939
|
+
output(await api('POST', `/api/canvas/ax/delivery/${encodeURIComponent(id)}/mark`, {}));
|
|
1940
|
+
});
|
|
1941
|
+
|
|
1942
|
+
cmd('ax elicitation request', 'Request structured human input', [
|
|
1943
|
+
'pmx-canvas ax elicitation request --prompt "Who owns this migration?"',
|
|
1944
|
+
'pmx-canvas ax elicitation request --prompt "Pick a region" --fields region,owner',
|
|
1945
|
+
], async (args) => {
|
|
1946
|
+
const { flags } = parseFlags(args);
|
|
1947
|
+
if (flags.help || flags.h) return showCommandHelp('ax elicitation request');
|
|
1948
|
+
const prompt = requireFlag(flags, 'prompt', 'pmx-canvas ax elicitation request --prompt <text>');
|
|
1949
|
+
const fields = getStringFlag(flags, 'fields');
|
|
1950
|
+
output(await api('POST', '/api/canvas/ax/elicitation', {
|
|
1951
|
+
prompt,
|
|
1952
|
+
...(fields ? { fields: fields.split(',').map((f) => f.trim()).filter(Boolean) } : {}),
|
|
1953
|
+
source: 'cli',
|
|
1954
|
+
}));
|
|
1955
|
+
});
|
|
1956
|
+
|
|
1957
|
+
cmd('ax elicitation respond', 'Answer a pending elicitation', [
|
|
1958
|
+
'pmx-canvas ax elicitation respond <id> --response \'{"owner":"alice"}\'',
|
|
1959
|
+
], async (args) => {
|
|
1960
|
+
const { positional, flags } = parseFlags(args);
|
|
1961
|
+
if (flags.help || flags.h) return showCommandHelp('ax elicitation respond');
|
|
1962
|
+
const id = getStringFlag(flags, 'id') ?? positional[0];
|
|
1963
|
+
if (!id) die('Missing elicitation id', 'pmx-canvas ax elicitation respond <id> --response <json>');
|
|
1964
|
+
let response: unknown = {};
|
|
1965
|
+
const raw = getStringFlag(flags, 'response');
|
|
1966
|
+
if (raw) {
|
|
1967
|
+
try { response = JSON.parse(raw); } catch { die('Invalid --response JSON', '--response \'{"k":"v"}\''); }
|
|
1968
|
+
}
|
|
1969
|
+
output(await api('POST', `/api/canvas/ax/elicitation/${encodeURIComponent(id)}/respond`, { response, source: 'cli' }));
|
|
1970
|
+
});
|
|
1971
|
+
|
|
1972
|
+
cmd('ax elicitation list', 'List elicitations', ['pmx-canvas ax elicitation list'], async (args) => {
|
|
1973
|
+
const { flags } = parseFlags(args);
|
|
1974
|
+
if (flags.help || flags.h) return showCommandHelp('ax elicitation list');
|
|
1975
|
+
output(await api('GET', '/api/canvas/ax/elicitation'));
|
|
1976
|
+
});
|
|
1977
|
+
|
|
1978
|
+
cmd('ax mode request', 'Request a workflow mode transition (plan/execute/autonomous)', [
|
|
1979
|
+
'pmx-canvas ax mode request --mode execute --reason "plan approved"',
|
|
1980
|
+
], async (args) => {
|
|
1981
|
+
const { flags } = parseFlags(args);
|
|
1982
|
+
if (flags.help || flags.h) return showCommandHelp('ax mode request');
|
|
1983
|
+
const mode = requireFlag(flags, 'mode', 'pmx-canvas ax mode request --mode plan|execute|autonomous');
|
|
1984
|
+
const reason = getStringFlag(flags, 'reason');
|
|
1985
|
+
output(await api('POST', '/api/canvas/ax/mode', { mode, ...(reason ? { reason } : {}), source: 'cli' }));
|
|
1986
|
+
});
|
|
1987
|
+
|
|
1988
|
+
cmd('ax mode resolve', 'Resolve a pending mode request', [
|
|
1989
|
+
'pmx-canvas ax mode resolve <id> --decision approved',
|
|
1990
|
+
], async (args) => {
|
|
1991
|
+
const { positional, flags } = parseFlags(args);
|
|
1992
|
+
if (flags.help || flags.h) return showCommandHelp('ax mode resolve');
|
|
1993
|
+
const id = getStringFlag(flags, 'id') ?? positional[0];
|
|
1994
|
+
if (!id) die('Missing mode request id', 'pmx-canvas ax mode resolve <id> --decision approved|rejected');
|
|
1995
|
+
const decision = getStringFlag(flags, 'decision');
|
|
1996
|
+
if (decision !== 'approved' && decision !== 'rejected') die('Invalid --decision', '--decision approved|rejected');
|
|
1997
|
+
const resolution = getStringFlag(flags, 'resolution');
|
|
1998
|
+
output(await api('POST', `/api/canvas/ax/mode/${encodeURIComponent(id)}/resolve`, {
|
|
1999
|
+
decision,
|
|
2000
|
+
...(resolution ? { resolution } : {}),
|
|
2001
|
+
source: 'cli',
|
|
2002
|
+
}));
|
|
2003
|
+
});
|
|
2004
|
+
|
|
2005
|
+
cmd('ax mode list', 'List mode requests', ['pmx-canvas ax mode list'], async (args) => {
|
|
2006
|
+
const { flags } = parseFlags(args);
|
|
2007
|
+
if (flags.help || flags.h) return showCommandHelp('ax mode list');
|
|
2008
|
+
output(await api('GET', '/api/canvas/ax/mode'));
|
|
2009
|
+
});
|
|
2010
|
+
|
|
2011
|
+
cmd('ax command list', 'List the PMX command registry', ['pmx-canvas ax command list'], async (args) => {
|
|
2012
|
+
const { flags } = parseFlags(args);
|
|
2013
|
+
if (flags.help || flags.h) return showCommandHelp('ax command list');
|
|
2014
|
+
output(await api('GET', '/api/canvas/ax/command'));
|
|
2015
|
+
});
|
|
2016
|
+
|
|
2017
|
+
cmd('ax command invoke', 'Invoke a registry-gated PMX command intent', [
|
|
2018
|
+
'pmx-canvas ax command invoke pmx.plan',
|
|
2019
|
+
'pmx-canvas ax command invoke pmx.promote-context --args \'{"nodeIds":["n1"]}\'',
|
|
2020
|
+
], async (args) => {
|
|
2021
|
+
const { positional, flags } = parseFlags(args);
|
|
2022
|
+
if (flags.help || flags.h) return showCommandHelp('ax command invoke');
|
|
2023
|
+
const name = getStringFlag(flags, 'name') ?? positional[0];
|
|
2024
|
+
if (!name) die('Missing command name', 'pmx-canvas ax command invoke <name>');
|
|
2025
|
+
let cmdArgs: unknown;
|
|
2026
|
+
const raw = getStringFlag(flags, 'args');
|
|
2027
|
+
if (raw) {
|
|
2028
|
+
try { cmdArgs = JSON.parse(raw); } catch { die('Invalid --args JSON', '--args \'{"k":"v"}\''); }
|
|
2029
|
+
}
|
|
2030
|
+
output(await api('POST', '/api/canvas/ax/command', { name, ...(cmdArgs !== undefined ? { args: cmdArgs } : {}), source: 'cli' }));
|
|
2031
|
+
});
|
|
2032
|
+
|
|
2033
|
+
cmd('ax policy get', 'Show the current declarative AX policy', ['pmx-canvas ax policy get'], async (args) => {
|
|
2034
|
+
const { flags } = parseFlags(args);
|
|
2035
|
+
if (flags.help || flags.h) return showCommandHelp('ax policy get');
|
|
2036
|
+
output(await api('GET', '/api/canvas/ax/policy'));
|
|
2037
|
+
});
|
|
2038
|
+
|
|
2039
|
+
cmd('ax policy set', 'Set the declarative AX policy (stored by PMX, enforced by adapters)', [
|
|
2040
|
+
'pmx-canvas ax policy set --excluded-tools shell,write --mode concise',
|
|
2041
|
+
], async (args) => {
|
|
2042
|
+
const { flags } = parseFlags(args);
|
|
2043
|
+
if (flags.help || flags.h) return showCommandHelp('ax policy set');
|
|
2044
|
+
const csv = (v?: string) => (v ? v.split(',').map((s) => s.trim()).filter(Boolean) : undefined);
|
|
2045
|
+
const allowed = csv(getStringFlag(flags, 'allowed-tools'));
|
|
2046
|
+
const excluded = csv(getStringFlag(flags, 'excluded-tools'));
|
|
2047
|
+
const approvalRequired = csv(getStringFlag(flags, 'approval-tools'));
|
|
2048
|
+
const mode = getStringFlag(flags, 'mode');
|
|
2049
|
+
const systemAppend = getStringFlag(flags, 'system-append');
|
|
2050
|
+
const tools = (allowed || excluded || approvalRequired)
|
|
2051
|
+
? { ...(allowed ? { allowed } : {}), ...(excluded ? { excluded } : {}), ...(approvalRequired ? { approvalRequired } : {}) }
|
|
2052
|
+
: undefined;
|
|
2053
|
+
const prompt = (mode || systemAppend)
|
|
2054
|
+
? { ...(mode ? { mode } : {}), ...(systemAppend ? { systemAppend } : {}) }
|
|
2055
|
+
: undefined;
|
|
2056
|
+
output(await api('POST', '/api/canvas/ax/policy', { ...(tools ? { tools } : {}), ...(prompt ? { prompt } : {}), source: 'cli' }));
|
|
2057
|
+
});
|
|
2058
|
+
|
|
1882
2059
|
cmd('ax timeline', 'Read the bounded AX timeline (events, evidence, steering)', [
|
|
1883
2060
|
'pmx-canvas ax timeline',
|
|
1884
2061
|
'pmx-canvas ax timeline --limit 100',
|
package/src/cli/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
6
6
|
import { runAgentCli } from './agent.js';
|
|
7
7
|
import { createCanvas } from '../server/index.js';
|
|
8
8
|
import { seedDemoCanvas } from '../server/demo.js';
|
|
9
|
+
import { ensureDefaultDockedNodes } from '../server/server.js';
|
|
9
10
|
|
|
10
11
|
const args = process.argv.slice(2);
|
|
11
12
|
|
|
@@ -599,7 +600,13 @@ Examples:
|
|
|
599
600
|
process.exit(1);
|
|
600
601
|
}
|
|
601
602
|
|
|
602
|
-
if (demo && canvas.getLayout().nodes.length === 0)
|
|
603
|
+
if (demo && canvas.getLayout().nodes.length === 0) {
|
|
604
|
+
seedDemoCanvas();
|
|
605
|
+
} else if (!demo) {
|
|
606
|
+
// First-run only: dock a status (left) + context (right) widget by default so
|
|
607
|
+
// a fresh canvas isn't empty. No-op once the workspace has saved state.
|
|
608
|
+
ensureDefaultDockedNodes();
|
|
609
|
+
}
|
|
603
610
|
|
|
604
611
|
console.log(`\n PMX Canvas running at http://localhost:${canvas.port}`);
|
|
605
612
|
console.log(` Health: http://localhost:${canvas.port}/health\n`);
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
viewport,
|
|
26
26
|
} from '../state/canvas-store';
|
|
27
27
|
import { removeNodeFromClient, updateNodeFromClient } from '../state/intent-bridge';
|
|
28
|
+
import { canOpenAsSite, openNodeAsSite } from '../nodes/surface-url';
|
|
28
29
|
import { getNodeIcon } from '../icons';
|
|
29
30
|
import { EXPANDABLE_TYPES, TYPE_LABELS } from '../types';
|
|
30
31
|
import type { CanvasNodeState } from '../types';
|
|
@@ -275,15 +276,18 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
|
|
|
275
276
|
>
|
|
276
277
|
{'\u2726'}
|
|
277
278
|
</button>
|
|
278
|
-
{/* Open
|
|
279
|
-
|
|
279
|
+
{/* Open as site — full-page standalone view of this node's surface,
|
|
280
|
+
served from /api/canvas/surface/:id (same document as the canvas
|
|
281
|
+
iframe). Covers html, web-artifact/ext-app/url mcp-apps,
|
|
282
|
+
json-render/graph, and webpage nodes. */}
|
|
283
|
+
{canOpenAsSite(node) && (
|
|
280
284
|
<button
|
|
281
285
|
type="button"
|
|
282
286
|
onClick={(e) => {
|
|
283
287
|
e.stopPropagation();
|
|
284
|
-
|
|
288
|
+
openNodeAsSite(node);
|
|
285
289
|
}}
|
|
286
|
-
title="Open
|
|
290
|
+
title="Open as site (new tab)"
|
|
287
291
|
>
|
|
288
292
|
↗
|
|
289
293
|
</button>
|
|
@@ -2,7 +2,7 @@ import { ContextNode } from '../nodes/ContextNode';
|
|
|
2
2
|
import { LedgerNode } from '../nodes/LedgerNode';
|
|
3
3
|
import { StatusNode } from '../nodes/StatusNode';
|
|
4
4
|
import { StatusSummary } from '../nodes/StatusSummary';
|
|
5
|
-
import {
|
|
5
|
+
import { closeAttentionHistory } from '../state/attention-store';
|
|
6
6
|
import { getContextPinnedNodes, toggleCollapsed, undockNode } from '../state/canvas-store';
|
|
7
7
|
import { TYPE_LABELS } from '../types';
|
|
8
8
|
import type { CanvasNodeState } from '../types';
|
|
@@ -40,44 +40,44 @@ function ContextDockedNode({ node }: { node: CanvasNodeState }) {
|
|
|
40
40
|
toggleCollapsed(node.id);
|
|
41
41
|
};
|
|
42
42
|
|
|
43
|
-
// Hide the collapsed Context pill while the Updates side panel is open.
|
|
44
|
-
// Mutual exclusion guarantees both panels can't be expanded simultaneously,
|
|
45
|
-
// but the pill itself would otherwise sit beneath/beside the Updates panel
|
|
46
|
-
// at the same right edge — better to hide until Updates is closed.
|
|
47
|
-
if (collapsed && attentionHistoryOpen.value) return null;
|
|
48
|
-
|
|
49
43
|
if (collapsed) {
|
|
44
|
+
// Collapsed = a menu-height pill in the right of the top HUD row, mirroring
|
|
45
|
+
// the docked status widget on the left so the bar reads as one continuous menu.
|
|
50
46
|
return (
|
|
51
|
-
<
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
47
|
+
<div class="docked-node docked-node--collapsed" data-docked-node="true">
|
|
48
|
+
<div class="docked-node-header">
|
|
49
|
+
<span class="node-type-badge">Context</span>
|
|
50
|
+
{hasItems && (
|
|
51
|
+
<span class="docked-node-count" aria-hidden="true">
|
|
52
|
+
{count > 99 ? '99+' : count}
|
|
53
|
+
</span>
|
|
54
|
+
)}
|
|
55
|
+
<div class="docked-node-controls">
|
|
56
|
+
<button
|
|
57
|
+
type="button"
|
|
58
|
+
onClick={(e) => {
|
|
59
|
+
e.stopPropagation();
|
|
60
|
+
expand();
|
|
61
|
+
}}
|
|
62
|
+
title={hasItems ? `${count} item${count === 1 ? '' : 's'} in agent context — expand` : 'Expand agent context'}
|
|
63
|
+
aria-label={hasItems ? `Context — ${count} item${count === 1 ? '' : 's'}` : 'Expand agent context'}
|
|
64
|
+
>
|
|
65
|
+
{'▸'}
|
|
66
|
+
</button>
|
|
67
|
+
<button
|
|
68
|
+
type="button"
|
|
69
|
+
onClick={(e) => {
|
|
70
|
+
e.stopPropagation();
|
|
71
|
+
undockNode(node.id);
|
|
72
|
+
}}
|
|
73
|
+
title="Undock to canvas"
|
|
74
|
+
aria-label="Undock to canvas"
|
|
75
|
+
>
|
|
76
|
+
{'⊙'}
|
|
77
|
+
</button>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
81
|
);
|
|
82
82
|
}
|
|
83
83
|
|
|
@@ -130,7 +130,7 @@ export function DockedNode({ node }: { node: CanvasNodeState }) {
|
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
return (
|
|
133
|
-
<div class=
|
|
133
|
+
<div class={`docked-node${node.collapsed ? ' docked-node--collapsed' : ''}`} data-docked-node="true">
|
|
134
134
|
<div class="docked-node-header">
|
|
135
135
|
<span class="node-type-badge">{TYPE_LABELS[node.type] ?? node.type}</span>
|
|
136
136
|
{node.type === 'status' && node.collapsed && <StatusSummary node={node} />}
|
|
@@ -8,6 +8,7 @@ import { StatusNode } from '../nodes/StatusNode';
|
|
|
8
8
|
import { ImageNode } from '../nodes/ImageNode';
|
|
9
9
|
import { WebpageNode } from '../nodes/WebpageNode';
|
|
10
10
|
import { HtmlNode, shouldShowPresentationControls } from '../nodes/HtmlNode';
|
|
11
|
+
import { canOpenAsSite, openNodeAsSite } from '../nodes/surface-url';
|
|
11
12
|
import { PromptNode } from '../nodes/PromptNode';
|
|
12
13
|
import { ResponseNode } from '../nodes/ResponseNode';
|
|
13
14
|
import { TraceNode } from '../nodes/TraceNode';
|
|
@@ -303,6 +304,17 @@ export function ExpandedNodeOverlay() {
|
|
|
303
304
|
</button>
|
|
304
305
|
)}
|
|
305
306
|
|
|
307
|
+
{canOpenAsSite(node) && (
|
|
308
|
+
<button
|
|
309
|
+
type="button"
|
|
310
|
+
class="expanded-action-btn"
|
|
311
|
+
onClick={() => openNodeAsSite(node)}
|
|
312
|
+
title="Open as a full-page site in a new tab"
|
|
313
|
+
>
|
|
314
|
+
Open as site
|
|
315
|
+
</button>
|
|
316
|
+
)}
|
|
317
|
+
|
|
306
318
|
{canPresent && (
|
|
307
319
|
<button
|
|
308
320
|
type="button"
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { openWorkbenchFile } from '../state/intent-bridge';
|
|
2
2
|
import { TYPE_LABELS, type CanvasNodeState } from '../types';
|
|
3
|
+
import { axNodeActionButtonStyle, runNodeAxInteraction } from './ax-node-actions';
|
|
3
4
|
|
|
4
5
|
interface ContextCard {
|
|
5
6
|
key?: string;
|
|
@@ -172,6 +173,22 @@ export function ContextNode({
|
|
|
172
173
|
padding: expanded ? '8px 0' : undefined,
|
|
173
174
|
}}
|
|
174
175
|
>
|
|
176
|
+
{/* AX: focus the agent on this context node */}
|
|
177
|
+
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
|
178
|
+
<button
|
|
179
|
+
type="button"
|
|
180
|
+
class="ax-node-action"
|
|
181
|
+
title="Set AX focus to this node"
|
|
182
|
+
style={axNodeActionButtonStyle}
|
|
183
|
+
onClick={(e) => {
|
|
184
|
+
e.stopPropagation();
|
|
185
|
+
void runNodeAxInteraction(node, 'ax.focus.set', undefined, 'Focus set');
|
|
186
|
+
}}
|
|
187
|
+
>
|
|
188
|
+
Set focus
|
|
189
|
+
</button>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
175
192
|
{tokenLimit !== null && tokenLimit > 0 && (
|
|
176
193
|
<div style={{ marginBottom: '8px' }}>
|
|
177
194
|
<div
|