pmx-canvas 0.2.2 → 0.2.4
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 +63 -0
- package/dist/canvas/index.js +1 -1
- package/package.json +1 -1
- package/skills/pmx-canvas/SKILL.md +10 -3
- package/skills/pmx-canvas/evals/evals.json +56 -1
- package/skills/pmx-canvas/evals/fixtures/code-exploration/src/auth/jwt.ts +17 -0
- package/skills/pmx-canvas/evals/fixtures/code-exploration/src/auth/login.ts +12 -0
- package/skills/pmx-canvas/evals/fixtures/code-exploration/src/auth/middleware.ts +13 -0
- package/skills/pmx-canvas/evals/fixtures/code-exploration/src/routes/auth.ts +13 -0
- package/skills/pmx-canvas/evals/fixtures/investigation-board/src/handlers/users.ts +27 -0
- package/skills/pmx-canvas/references/full-reference.md +27 -6
- package/src/cli/agent.ts +33 -17
- package/src/client/nodes/ExtAppFrame.tsx +1 -1
- package/src/server/operations/ops/ax-timeline.ts +12 -5
- package/src/server/operations/ops/webview.ts +15 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmx-canvas",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
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",
|
|
@@ -27,8 +27,12 @@ Humans curate agent context by pinning nodes; agents read that curation through
|
|
|
27
27
|
nodes. Read the full layout only when necessary.
|
|
28
28
|
4. **Snapshot before destructive changes.** Use `canvas_snapshot` before clear, restore, or a major
|
|
29
29
|
reorganization.
|
|
30
|
-
5. **
|
|
31
|
-
|
|
30
|
+
5. **Show intent with the Ghost Cursor — by default.** Signal with
|
|
31
|
+
`canvas_intent { action: "signal", ... }` before every meaningful create, move, connect, remove,
|
|
32
|
+
or edit, then pass the returned `intent.id` as `intentId` on the mutation so the ghost settles
|
|
33
|
+
into the result. Use it as much as possible to make your next move and your work visible: the
|
|
34
|
+
human watches intent form and can veto mid-thought. Skip it only for trivial in-place tweaks or
|
|
35
|
+
high-frequency batch churn.
|
|
32
36
|
6. **Mutate through current composites.** Prefer the 15 composite MCP tools below.
|
|
33
37
|
7. **Arrange and validate.** After batch changes, use `canvas_view { action: "arrange" }` when
|
|
34
38
|
appropriate and always finish with `canvas_query { action: "validate" }`.
|
|
@@ -191,7 +195,10 @@ Prefer `canvas_query { action: "search" }` over parsing the full layout.
|
|
|
191
195
|
- Hosted MCP-app/ext-app nodes such as Excalidraw require the in-canvas host bridge and are not
|
|
192
196
|
standalone **Open as site** targets. URL-backed viewers and bundled web artifacts remain
|
|
193
197
|
openable.
|
|
194
|
-
- Graph and json-render standalone surfaces use `display=site` and fill the browser viewport
|
|
198
|
+
- Graph and json-render standalone surfaces use `display=site` and fill the browser viewport, and
|
|
199
|
+
reflow on a live window resize in a normal browser. Some single-tab host browsers (e.g. the
|
|
200
|
+
Codex in-app browser) don't deliver live-resize events, so a resized standalone chart can look
|
|
201
|
+
stale until reload — use a system browser for separate full-page viewing.
|
|
195
202
|
- Some hosts cannot automate inside sandboxed workbench iframes. Verify those interactions in a
|
|
196
203
|
system browser or through server-side AX state.
|
|
197
204
|
- `pmx-canvas screenshot` requires an active WebView. Start it with
|
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
"name": "investigation-board",
|
|
7
7
|
"prompt": "I'm debugging a memory leak in our Node.js API. The /api/users endpoint is leaking memory on every request. I found a suspicious closure in src/handlers/users.ts that captures the entire request object, and the heap snapshot shows growing EventEmitter listeners. Can you set up an investigation board on the canvas so I can see the full picture?",
|
|
8
8
|
"expected_output": "Creates multiple nodes (bug description, code file, heap findings, hypothesis) connected with edges, arranged in a tree layout. Uses appropriate node types (markdown for findings, file for source, status for investigation progress).",
|
|
9
|
+
"files": [
|
|
10
|
+
"evals/fixtures/investigation-board/src/handlers/users.ts"
|
|
11
|
+
],
|
|
9
12
|
"assertions": [
|
|
10
13
|
{
|
|
11
14
|
"name": "creates-multiple-nodes",
|
|
@@ -93,7 +96,7 @@
|
|
|
93
96
|
"assertions": [
|
|
94
97
|
{
|
|
95
98
|
"name": "reads-pinned-context",
|
|
96
|
-
"description": "Reads the canvas://pinned-context
|
|
99
|
+
"description": "Reads the curated pin set via canvas://pinned-context — or the host-equivalent (canvas_ax_state / the HTTP pinned-context endpoint) where a direct resource tool is unavailable — not just canvas_query action:layout",
|
|
97
100
|
"type": "output_check"
|
|
98
101
|
},
|
|
99
102
|
{
|
|
@@ -113,6 +116,12 @@
|
|
|
113
116
|
"name": "code-exploration-files",
|
|
114
117
|
"prompt": "I'm trying to understand how the authentication flow works in this project. Can you put the relevant auth files on the canvas so I can see how they connect? The main files are src/auth/login.ts, src/auth/middleware.ts, src/auth/jwt.ts, and src/routes/auth.ts.",
|
|
115
118
|
"expected_output": "Creates file nodes for each mentioned file (content auto-loads), relies on code graph to auto-detect import dependencies, groups auth files together, and reads canvas://code-graph for dependency analysis.",
|
|
119
|
+
"files": [
|
|
120
|
+
"evals/fixtures/code-exploration/src/auth/jwt.ts",
|
|
121
|
+
"evals/fixtures/code-exploration/src/auth/login.ts",
|
|
122
|
+
"evals/fixtures/code-exploration/src/auth/middleware.ts",
|
|
123
|
+
"evals/fixtures/code-exploration/src/routes/auth.ts"
|
|
124
|
+
],
|
|
116
125
|
"assertions": [
|
|
117
126
|
{
|
|
118
127
|
"name": "uses-file-nodes",
|
|
@@ -380,6 +389,52 @@
|
|
|
380
389
|
"type": "output_check"
|
|
381
390
|
}
|
|
382
391
|
]
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
"id": 16,
|
|
395
|
+
"name": "ghost-cursor-intent",
|
|
396
|
+
"prompt": "Add an 'Auth design' status node to the review area. Before adding it, signal what you're about to do so I can veto it if I disagree, then make the change.",
|
|
397
|
+
"expected_output": "Signals a create intent first with canvas_intent {action:signal, kind:\"create\", position, nodeType:\"status\", label, reason, confidence}, then creates the 'Auth design' node at that position with canvas_node {action:\"add\", type:\"status\", x, y, intentId}. Understands that a vetoed intent rejects its linked mutation. Treats signalling intent as the default before visible mutations rather than an optional step, and does not silently mutate.",
|
|
398
|
+
"assertions": [
|
|
399
|
+
{
|
|
400
|
+
"name": "signals-intent-first",
|
|
401
|
+
"description": "Calls canvas_intent {action:signal} with label/reason and a confidence in [0,1] before the visible mutation",
|
|
402
|
+
"type": "output_check"
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
"name": "links-mutation-to-intent",
|
|
406
|
+
"description": "Passes the returned intentId on canvas_node {action:add, type:\"status\", x, y, intentId} so the ghost settles into the real node",
|
|
407
|
+
"type": "output_check"
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
"name": "respects-veto",
|
|
411
|
+
"description": "Understands a vetoed intent rejects its linked mutation (\"Intent <id> was vetoed\") rather than treating signal as cosmetic, and clears/updates intent appropriately",
|
|
412
|
+
"type": "output_check"
|
|
413
|
+
}
|
|
414
|
+
]
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
"id": 17,
|
|
418
|
+
"name": "standalone-surface-open-as-site",
|
|
419
|
+
"prompt": "Open the bar-chart graph node as a standalone full-page site for a screenshot, and also try to open the Excalidraw diagram node the same way. Tell me the correct surface URL for each.",
|
|
420
|
+
"expected_output": "For the graph/json-render node, uses the stable surface URL /api/canvas/surface/<id> which redirects to the full-viewport display=site viewer (reads surfaceUrl from the node, does not fabricate a path). Recognizes the hosted Excalidraw/ext-app node is NOT an open-as-site target (its surface route returns a clean 404 — it renders only with the in-canvas host bridge) and does not claim a standalone tab will work. Notes that some host browsers (e.g. the Codex in-app single-tab browser) don't deliver live-resize events, so a system browser is recommended for separate full-page viewing.",
|
|
421
|
+
"assertions": [
|
|
422
|
+
{
|
|
423
|
+
"name": "graph-site-surface-url",
|
|
424
|
+
"description": "Gives the graph node's /api/canvas/surface/<id> URL (which redirects to display=site, full viewport) from the node payload rather than fabricating one",
|
|
425
|
+
"type": "output_check"
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
"name": "extapp-not-open-as-site",
|
|
429
|
+
"description": "States the hosted Excalidraw/ext-app node is not an open-as-site target (clean 404; renders in-canvas only) instead of claiming a standalone tab works",
|
|
430
|
+
"type": "output_check"
|
|
431
|
+
},
|
|
432
|
+
{
|
|
433
|
+
"name": "host-browser-caveat",
|
|
434
|
+
"description": "Recommends a system browser for separate full-page viewing and/or notes single-tab host browsers may not reflow on live resize",
|
|
435
|
+
"type": "output_check"
|
|
436
|
+
}
|
|
437
|
+
]
|
|
383
438
|
}
|
|
384
439
|
]
|
|
385
440
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const SECRET = process.env.JWT_SECRET ?? 'dev-secret';
|
|
2
|
+
|
|
3
|
+
export interface JwtClaims {
|
|
4
|
+
sub: string;
|
|
5
|
+
exp: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function signJwt(claims: JwtClaims): string {
|
|
9
|
+
const payload = Buffer.from(JSON.stringify(claims)).toString('base64url');
|
|
10
|
+
return `${payload}.${SECRET}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function verifyJwt(token: string): JwtClaims | null {
|
|
14
|
+
const [payload, signature] = token.split('.');
|
|
15
|
+
if (signature !== SECRET || !payload) return null;
|
|
16
|
+
return JSON.parse(Buffer.from(payload, 'base64url').toString()) as JwtClaims;
|
|
17
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { signJwt, type JwtClaims } from './jwt';
|
|
2
|
+
|
|
3
|
+
export async function login(username: string, password: string): Promise<string | null> {
|
|
4
|
+
const ok = await checkCredentials(username, password);
|
|
5
|
+
if (!ok) return null;
|
|
6
|
+
const claims: JwtClaims = { sub: username, exp: Date.now() + 3_600_000 };
|
|
7
|
+
return signJwt(claims);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async function checkCredentials(username: string, password: string): Promise<boolean> {
|
|
11
|
+
return Boolean(username) && password.length >= 8;
|
|
12
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { verifyJwt } from './jwt';
|
|
2
|
+
|
|
3
|
+
export interface AuthedRequest {
|
|
4
|
+
headers: Record<string, string>;
|
|
5
|
+
userId?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function authMiddleware(req: AuthedRequest, next: () => void): void {
|
|
9
|
+
const token = (req.headers.authorization ?? '').replace(/^Bearer /, '');
|
|
10
|
+
const claims = verifyJwt(token);
|
|
11
|
+
if (claims) req.userId = claims.sub;
|
|
12
|
+
next();
|
|
13
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { login } from '../auth/login';
|
|
2
|
+
import { authMiddleware, type AuthedRequest } from '../auth/middleware';
|
|
3
|
+
|
|
4
|
+
export function registerAuthRoutes(router: {
|
|
5
|
+
post(path: string, handler: (req: AuthedRequest) => Promise<unknown>): void;
|
|
6
|
+
use(handler: (req: AuthedRequest, next: () => void) => void): void;
|
|
7
|
+
}): void {
|
|
8
|
+
router.use(authMiddleware);
|
|
9
|
+
router.post('/login', async (req) => {
|
|
10
|
+
const token = await login(req.headers.username ?? '', req.headers.password ?? '');
|
|
11
|
+
return token ? { token } : { error: 'invalid credentials' };
|
|
12
|
+
});
|
|
13
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
|
|
3
|
+
const refreshBus = new EventEmitter();
|
|
4
|
+
|
|
5
|
+
interface UserRequest {
|
|
6
|
+
query: Record<string, string>;
|
|
7
|
+
headers: Record<string, string>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface UserResponse {
|
|
11
|
+
json(body: unknown): void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* GET /api/users
|
|
16
|
+
*
|
|
17
|
+
* Memory leak: every request registers a `refresh` listener whose closure
|
|
18
|
+
* captures the entire `req` object and is never removed. The heap retains one
|
|
19
|
+
* request per call and the EventEmitter listener count grows without bound.
|
|
20
|
+
*/
|
|
21
|
+
export function getUsers(req: UserRequest, res: UserResponse): void {
|
|
22
|
+
refreshBus.on('refresh', () => {
|
|
23
|
+
// Captures `req` for the lifetime of the process — the leak.
|
|
24
|
+
void req.headers;
|
|
25
|
+
});
|
|
26
|
+
res.json({ users: [] });
|
|
27
|
+
}
|
|
@@ -75,8 +75,8 @@ section below.
|
|
|
75
75
|
you need the full board.
|
|
76
76
|
4. **Snapshot before destructive work** — `canvas_snapshot { name }` before clear/major
|
|
77
77
|
reorg; restore if needed.
|
|
78
|
-
5. **Signal then mutate** —
|
|
79
|
-
move, then create with the right composite: `canvas_node` (markdown/status/file/webpage/html
|
|
78
|
+
5. **Signal then mutate (default behavior)** — signal with `canvas_intent { action: "signal", … }`
|
|
79
|
+
to telegraph the move before nearly every mutation, then create with the right composite: `canvas_node` (markdown/status/file/webpage/html
|
|
80
80
|
incl. primitives), `canvas_render` (json-render/graph), `canvas_app` (mcp-app/diagram/
|
|
81
81
|
web-artifact). Prefer composites — the legacy single-purpose tools are deprecated (removed in
|
|
82
82
|
v0.3).
|
|
@@ -441,6 +441,10 @@ identifier is passed as `approvalAction`, since `action` is the lifecycle discri
|
|
|
441
441
|
|
|
442
442
|
#### Narrate your next move with `canvas_intent` (Ghost Cursor of Intent)
|
|
443
443
|
|
|
444
|
+
**Use the Ghost Cursor as much as possible** — it is the primary way to make your intent and your
|
|
445
|
+
work visible on a shared board. Default to signaling before you act; skip it only for trivial
|
|
446
|
+
in-place tweaks or high-frequency batch churn.
|
|
447
|
+
|
|
444
448
|
Before you create/move/connect/edit/remove on the canvas, **signal the move** so a
|
|
445
449
|
faint placeholder forms where you're about to act — the human sees the next move
|
|
446
450
|
coming and can veto it mid-thought. Intents are ephemeral presence: never
|
|
@@ -455,12 +459,25 @@ Narrate → linked mutation → automatic settle:
|
|
|
455
459
|
Use `canvas_intent { action: "clear", id }` only when abandoning a plan without
|
|
456
460
|
performing the linked mutation.
|
|
457
461
|
|
|
462
|
+
**Linked settle is scoped to node, edge, and group mutations** (`canvas_node`,
|
|
463
|
+
`canvas_edge`, `canvas_group` and their ops). `canvas_app` opens (diagram /
|
|
464
|
+
mcp-app) and `canvas_webview` do **not** accept an `intentId` and reject it with a
|
|
465
|
+
400 — to telegraph one of those, signal a ghost, then `clear` it (or let it
|
|
466
|
+
expire) and run the open *without* an `intentId`.
|
|
467
|
+
|
|
458
468
|
Per kind, pass the anchor it renders against: `position` for `create`/`move`,
|
|
459
469
|
`nodeId` for `move`/`edit`/`remove`, `edge: { from, to, type }` for `connect`. The
|
|
460
|
-
payoff is **legibility** — `reason` is shown beneath the ghost.
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
470
|
+
payoff is **legibility** — `reason` is shown beneath the ghost.
|
|
471
|
+
|
|
472
|
+
**When to use vs skip.** Signal for adds, removes, and moves of visible nodes;
|
|
473
|
+
connecting nodes; creating groups; layout reorganizations; meaningful title/content
|
|
474
|
+
edits; destructive actions (clear/restore/remove); and creating
|
|
475
|
+
artifact/report/dashboard nodes. Skip for tiny metadata fixes, API-only pin/unpin
|
|
476
|
+
verification, deterministic report-node refreshes the human just asked for,
|
|
477
|
+
post-restore cleanup, and bulk fixture churn. **For batch work, signal one ghost
|
|
478
|
+
per human-meaningful move, not one per low-level op** — e.g. one "lay out the
|
|
479
|
+
investigation board" intent, then run the batch with that linked `intentId` (use
|
|
480
|
+
`seq` to order staged previews) so the human watches the wireframe before it fills.
|
|
464
481
|
|
|
465
482
|
### Standalones (first-class — not deprecated)
|
|
466
483
|
|
|
@@ -901,6 +918,10 @@ the host's embedded browser (e.g. Codex) opens `_blank` tabs in-place.
|
|
|
901
918
|
site"); view/edit them in the canvas, or open them externally through their own app
|
|
902
919
|
(report #61). Only bundled `web-artifact` apps (redirect to `/artifact`) and URL-backed
|
|
903
920
|
`mcp-app` / `webpage` viewers redirect to their external site.
|
|
921
|
+
- `graph` / `json-render` nodes redirect to the full-viewport `display=site` viewer; the chart
|
|
922
|
+
fills the window and reflows on a live resize in a normal browser. Single-tab host browsers
|
|
923
|
+
that don't deliver live-resize events (e.g. the Codex in-app browser) can leave a resized chart
|
|
924
|
+
stale until reload — recommend a system browser for separate full-page viewing (report #67).
|
|
904
925
|
- This is additive — opening a site never evicts or replaces canvas nodes.
|
|
905
926
|
|
|
906
927
|
### Choosing the Right Visual Tier
|
package/src/cli/agent.ts
CHANGED
|
@@ -249,6 +249,16 @@ function optionalNumberFlag(flags: Record<string, string | true>, name: string,
|
|
|
249
249
|
return Math.floor(parsed);
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
+
/**
|
|
253
|
+
* AX `source` for a CLI-originated action. Defaults to `cli`, but honors an
|
|
254
|
+
* explicit `--source <label>` so an adapterless agent using the CLI as a fallback
|
|
255
|
+
* transport (e.g. `--source codex`) attributes its actions correctly — keeping
|
|
256
|
+
* loop-safety (a consumer never gets back its own steering) accurate (report #69).
|
|
257
|
+
*/
|
|
258
|
+
function resolveAxSource(flags: Record<string, string | true>): string {
|
|
259
|
+
return getStringFlag(flags, 'source') ?? 'cli';
|
|
260
|
+
}
|
|
261
|
+
|
|
252
262
|
function optionalFiniteFlag(flags: Record<string, string | true>, name: string, hint: string): number | undefined {
|
|
253
263
|
const val = flags[name];
|
|
254
264
|
if (!val || val === true) return undefined;
|
|
@@ -1884,7 +1894,7 @@ cmd('ax focus', 'Set or clear PMX AX focus without moving the viewport', [
|
|
|
1884
1894
|
die('Missing node ID', 'pmx-canvas ax focus <node-id> [more-node-ids]');
|
|
1885
1895
|
}
|
|
1886
1896
|
|
|
1887
|
-
output(await api('POST', '/api/canvas/ax/focus', { nodeIds, source:
|
|
1897
|
+
output(await api('POST', '/api/canvas/ax/focus', { nodeIds, source: resolveAxSource(flags) }));
|
|
1888
1898
|
});
|
|
1889
1899
|
|
|
1890
1900
|
cmd('ax event add', 'Record a normalized AX timeline event', [
|
|
@@ -1903,7 +1913,7 @@ cmd('ax event add', 'Record a normalized AX timeline event', [
|
|
|
1903
1913
|
summary,
|
|
1904
1914
|
...(detail ? { detail } : {}),
|
|
1905
1915
|
...(positional.length > 0 ? { nodeIds: positional } : {}),
|
|
1906
|
-
source:
|
|
1916
|
+
source: resolveAxSource(flags),
|
|
1907
1917
|
}));
|
|
1908
1918
|
});
|
|
1909
1919
|
|
|
@@ -1919,7 +1929,7 @@ cmd('ax steer', 'Send a steering message to the active agent session', [
|
|
|
1919
1929
|
die('Missing steering message', 'pmx-canvas ax steer <message>');
|
|
1920
1930
|
}
|
|
1921
1931
|
|
|
1922
|
-
output(await api('POST', '/api/canvas/ax/steer', { message, source:
|
|
1932
|
+
output(await api('POST', '/api/canvas/ax/steer', { message, source: resolveAxSource(flags) }));
|
|
1923
1933
|
});
|
|
1924
1934
|
|
|
1925
1935
|
cmd('ax interaction', 'Submit a node-originated AX interaction (capability-gated)', [
|
|
@@ -1948,21 +1958,27 @@ cmd('ax interaction', 'Submit a node-originated AX interaction (capability-gated
|
|
|
1948
1958
|
type,
|
|
1949
1959
|
sourceNodeId,
|
|
1950
1960
|
...(payload !== undefined ? { payload } : {}),
|
|
1951
|
-
source:
|
|
1961
|
+
source: resolveAxSource(flags),
|
|
1952
1962
|
}));
|
|
1953
1963
|
});
|
|
1954
1964
|
|
|
1955
1965
|
cmd('ax delivery list', 'List pending AX steering for a consumer (loop-safe)', [
|
|
1956
1966
|
'pmx-canvas ax delivery list',
|
|
1957
1967
|
'pmx-canvas ax delivery list --consumer copilot --limit 20',
|
|
1968
|
+
'pmx-canvas ax delivery list --order newest # latest browser steering first (#68)',
|
|
1958
1969
|
], async (args) => {
|
|
1959
1970
|
const { flags } = parseFlags(args);
|
|
1960
1971
|
if (flags.help || flags.h) return showCommandHelp('ax delivery list');
|
|
1961
1972
|
const consumer = getStringFlag(flags, 'consumer');
|
|
1962
1973
|
const limit = optionalNumberFlag(flags, 'limit', 'pmx-canvas ax delivery list --limit <n>');
|
|
1974
|
+
const order = getStringFlag(flags, 'order');
|
|
1975
|
+
if (order !== undefined && order !== 'newest' && order !== 'oldest') {
|
|
1976
|
+
die('Invalid --order', 'pmx-canvas ax delivery list --order newest|oldest');
|
|
1977
|
+
}
|
|
1963
1978
|
const params = new URLSearchParams();
|
|
1964
1979
|
if (consumer) params.set('consumer', consumer);
|
|
1965
1980
|
if (limit) params.set('limit', String(limit));
|
|
1981
|
+
if (order) params.set('order', order);
|
|
1966
1982
|
const qs = params.toString();
|
|
1967
1983
|
output(await api('GET', `/api/canvas/ax/delivery/pending${qs ? `?${qs}` : ''}`));
|
|
1968
1984
|
});
|
|
@@ -1988,7 +2004,7 @@ cmd('ax elicitation request', 'Request structured human input', [
|
|
|
1988
2004
|
output(await api('POST', '/api/canvas/ax/elicitation', {
|
|
1989
2005
|
prompt,
|
|
1990
2006
|
...(fields ? { fields: fields.split(',').map((f) => f.trim()).filter(Boolean) } : {}),
|
|
1991
|
-
source:
|
|
2007
|
+
source: resolveAxSource(flags),
|
|
1992
2008
|
}));
|
|
1993
2009
|
});
|
|
1994
2010
|
|
|
@@ -2004,7 +2020,7 @@ cmd('ax elicitation respond', 'Answer a pending elicitation', [
|
|
|
2004
2020
|
if (raw) {
|
|
2005
2021
|
try { response = JSON.parse(raw); } catch { die('Invalid --response JSON', '--response \'{"k":"v"}\''); }
|
|
2006
2022
|
}
|
|
2007
|
-
output(await api('POST', `/api/canvas/ax/elicitation/${encodeURIComponent(id)}/respond`, { response, source:
|
|
2023
|
+
output(await api('POST', `/api/canvas/ax/elicitation/${encodeURIComponent(id)}/respond`, { response, source: resolveAxSource(flags) }));
|
|
2008
2024
|
});
|
|
2009
2025
|
|
|
2010
2026
|
cmd('ax elicitation list', 'List elicitations', ['pmx-canvas ax elicitation list'], async (args) => {
|
|
@@ -2020,7 +2036,7 @@ cmd('ax mode request', 'Request a workflow mode transition (plan/execute/autonom
|
|
|
2020
2036
|
if (flags.help || flags.h) return showCommandHelp('ax mode request');
|
|
2021
2037
|
const mode = requireFlag(flags, 'mode', 'pmx-canvas ax mode request --mode plan|execute|autonomous');
|
|
2022
2038
|
const reason = getStringFlag(flags, 'reason');
|
|
2023
|
-
output(await api('POST', '/api/canvas/ax/mode', { mode, ...(reason ? { reason } : {}), source:
|
|
2039
|
+
output(await api('POST', '/api/canvas/ax/mode', { mode, ...(reason ? { reason } : {}), source: resolveAxSource(flags) }));
|
|
2024
2040
|
});
|
|
2025
2041
|
|
|
2026
2042
|
cmd('ax mode resolve', 'Resolve a pending mode request', [
|
|
@@ -2036,7 +2052,7 @@ cmd('ax mode resolve', 'Resolve a pending mode request', [
|
|
|
2036
2052
|
output(await api('POST', `/api/canvas/ax/mode/${encodeURIComponent(id)}/resolve`, {
|
|
2037
2053
|
decision,
|
|
2038
2054
|
...(resolution ? { resolution } : {}),
|
|
2039
|
-
source:
|
|
2055
|
+
source: resolveAxSource(flags),
|
|
2040
2056
|
}));
|
|
2041
2057
|
});
|
|
2042
2058
|
|
|
@@ -2065,7 +2081,7 @@ cmd('ax command invoke', 'Invoke a registry-gated PMX command intent', [
|
|
|
2065
2081
|
if (raw) {
|
|
2066
2082
|
try { cmdArgs = JSON.parse(raw); } catch { die('Invalid --args JSON', '--args \'{"k":"v"}\''); }
|
|
2067
2083
|
}
|
|
2068
|
-
output(await api('POST', '/api/canvas/ax/command', { name, ...(cmdArgs !== undefined ? { args: cmdArgs } : {}), source:
|
|
2084
|
+
output(await api('POST', '/api/canvas/ax/command', { name, ...(cmdArgs !== undefined ? { args: cmdArgs } : {}), source: resolveAxSource(flags) }));
|
|
2069
2085
|
});
|
|
2070
2086
|
|
|
2071
2087
|
cmd('ax policy get', 'Show the current declarative AX policy', ['pmx-canvas ax policy get'], async (args) => {
|
|
@@ -2091,7 +2107,7 @@ cmd('ax policy set', 'Set the declarative AX policy (stored by PMX, enforced by
|
|
|
2091
2107
|
const prompt = (mode || systemAppend)
|
|
2092
2108
|
? { ...(mode ? { mode } : {}), ...(systemAppend ? { systemAppend } : {}) }
|
|
2093
2109
|
: undefined;
|
|
2094
|
-
output(await api('POST', '/api/canvas/ax/policy', { ...(tools ? { tools } : {}), ...(prompt ? { prompt } : {}), source:
|
|
2110
|
+
output(await api('POST', '/api/canvas/ax/policy', { ...(tools ? { tools } : {}), ...(prompt ? { prompt } : {}), source: resolveAxSource(flags) }));
|
|
2095
2111
|
});
|
|
2096
2112
|
|
|
2097
2113
|
cmd('ax timeline', 'Read the bounded AX timeline (events, evidence, steering)', [
|
|
@@ -2121,7 +2137,7 @@ cmd('ax work add', 'Add a canvas-bound AX work item', [
|
|
|
2121
2137
|
...(status ? { status } : {}),
|
|
2122
2138
|
...(detail ? { detail } : {}),
|
|
2123
2139
|
...(positional.length > 0 ? { nodeIds: positional } : {}),
|
|
2124
|
-
source:
|
|
2140
|
+
source: resolveAxSource(flags),
|
|
2125
2141
|
}));
|
|
2126
2142
|
});
|
|
2127
2143
|
|
|
@@ -2143,7 +2159,7 @@ cmd('ax work update', 'Update a canvas-bound AX work item by ID', [
|
|
|
2143
2159
|
...(status ? { status } : {}),
|
|
2144
2160
|
...(detail ? { detail } : {}),
|
|
2145
2161
|
...(positional.length > 1 ? { nodeIds: positional.slice(1) } : {}),
|
|
2146
|
-
source:
|
|
2162
|
+
source: resolveAxSource(flags),
|
|
2147
2163
|
}));
|
|
2148
2164
|
});
|
|
2149
2165
|
|
|
@@ -2172,7 +2188,7 @@ cmd('ax approval request', 'Request a canvas-bound AX approval gate', [
|
|
|
2172
2188
|
...(detail ? { detail } : {}),
|
|
2173
2189
|
...(action ? { action } : {}),
|
|
2174
2190
|
...(positional.length > 0 ? { nodeIds: positional } : {}),
|
|
2175
|
-
source:
|
|
2191
|
+
source: resolveAxSource(flags),
|
|
2176
2192
|
}));
|
|
2177
2193
|
});
|
|
2178
2194
|
|
|
@@ -2194,7 +2210,7 @@ cmd('ax approval resolve', 'Resolve a pending AX approval gate by ID', [
|
|
|
2194
2210
|
output(await api('POST', `/api/canvas/ax/approval/${encodeURIComponent(id)}/resolve`, {
|
|
2195
2211
|
decision,
|
|
2196
2212
|
...(resolution ? { resolution } : {}),
|
|
2197
|
-
source:
|
|
2213
|
+
source: resolveAxSource(flags),
|
|
2198
2214
|
}));
|
|
2199
2215
|
});
|
|
2200
2216
|
|
|
@@ -2225,7 +2241,7 @@ cmd('ax evidence add', 'Record an AX evidence item on the timeline', [
|
|
|
2225
2241
|
...(body ? { body } : {}),
|
|
2226
2242
|
...(ref ? { ref } : {}),
|
|
2227
2243
|
...(positional.length > 0 ? { nodeIds: positional } : {}),
|
|
2228
|
-
source:
|
|
2244
|
+
source: resolveAxSource(flags),
|
|
2229
2245
|
}));
|
|
2230
2246
|
});
|
|
2231
2247
|
|
|
@@ -2252,7 +2268,7 @@ cmd('ax review add', 'Add a canvas-bound AX review annotation', [
|
|
|
2252
2268
|
...(nodeId ? { nodeId } : {}),
|
|
2253
2269
|
...(file ? { file } : {}),
|
|
2254
2270
|
...(author ? { author } : {}),
|
|
2255
|
-
source:
|
|
2271
|
+
source: resolveAxSource(flags),
|
|
2256
2272
|
}));
|
|
2257
2273
|
});
|
|
2258
2274
|
|
|
@@ -2283,7 +2299,7 @@ cmd('ax host report', 'Report host/session capability to the canvas', [
|
|
|
2283
2299
|
permissions: flags.permissions === true,
|
|
2284
2300
|
files: flags.files === true,
|
|
2285
2301
|
uiPrompts: flags['ui-prompts'] === true,
|
|
2286
|
-
source:
|
|
2302
|
+
source: resolveAxSource(flags),
|
|
2287
2303
|
}));
|
|
2288
2304
|
});
|
|
2289
2305
|
|
|
@@ -799,7 +799,7 @@ export function ExtAppFrame({ node, expanded = false }: { node: CanvasNodeState;
|
|
|
799
799
|
minHeight: 0,
|
|
800
800
|
border: 'none',
|
|
801
801
|
background: 'var(--c-panel)',
|
|
802
|
-
pointerEvents: isExpanded ? 'auto' : 'none',
|
|
802
|
+
pointerEvents: isExpanded && status !== 'loading' ? 'auto' : 'none',
|
|
803
803
|
}}
|
|
804
804
|
title={`Ext App: ${toolName}`}
|
|
805
805
|
/>
|
|
@@ -250,6 +250,7 @@ const axTimelineGetOperation = defineOperation<z.infer<typeof axTimelineGetSchem
|
|
|
250
250
|
const axDeliveryPendingShape = {
|
|
251
251
|
consumer: z.unknown().optional().describe('Consumer/source label to exclude from results (e.g. copilot, mcp).'),
|
|
252
252
|
limit: z.unknown().optional().describe('Max steering messages to return.'),
|
|
253
|
+
order: z.unknown().optional().describe('"oldest" (FIFO, default) or "newest" first.'),
|
|
253
254
|
};
|
|
254
255
|
|
|
255
256
|
const axDeliveryPendingSchema = z.looseObject(axDeliveryPendingShape);
|
|
@@ -265,10 +266,11 @@ const axDeliveryPendingOperation = defineOperation<z.infer<typeof axDeliveryPend
|
|
|
265
266
|
},
|
|
266
267
|
mcp: {
|
|
267
268
|
toolName: 'canvas_claim_ax_delivery',
|
|
268
|
-
description: 'Claim pending PMX AX deliveries for a consumer (adapterless delivery). Returns `pending` undelivered steering (mark each with canvas_mark_ax_delivery after acting) AND `pendingActivity`: open canvas-bound AX items awaiting the agent (open work items, pending approval gates / elicitations / mode requests) — typically created by the human in the browser. Both exclude items the consumer itself originated (loop prevention). pendingActivity is read-only here: resolve each via its own tool (canvas_resolve_approval / canvas_respond_elicitation / canvas_resolve_mode / canvas_update_work_item), not canvas_mark_ax_delivery.',
|
|
269
|
+
description: 'Claim pending PMX AX deliveries for a consumer (adapterless delivery). Returns `pending` undelivered steering (mark each with canvas_mark_ax_delivery after acting) AND `pendingActivity`: open canvas-bound AX items awaiting the agent (open work items, pending approval gates / elicitations / mode requests) — typically created by the human in the browser. Both exclude items the consumer itself originated (loop prevention). `pending` defaults to oldest-first (FIFO, for ordered processing); pass `order:"newest"` to surface the human\'s LATEST in-canvas steering first when a small `limit` would otherwise bury it behind a stale backlog (report #68). pendingActivity is read-only here: resolve each via its own tool (canvas_resolve_approval / canvas_respond_elicitation / canvas_resolve_mode / canvas_update_work_item), not canvas_mark_ax_delivery.',
|
|
269
270
|
extraShape: {
|
|
270
271
|
consumer: z.string().optional().describe('Consumer/source label to exclude from results (e.g. copilot, mcp).'),
|
|
271
272
|
limit: z.number().optional().describe('Max steering messages to return.'),
|
|
273
|
+
order: z.enum(['newest', 'oldest']).optional().describe('Order of returned steering: "oldest" (FIFO, default) for ordered processing, or "newest" first to see the latest browser action when limited.'),
|
|
272
274
|
},
|
|
273
275
|
// `consumer` is a loop-safety scope, not a source label — never defaulted.
|
|
274
276
|
formatResult: axJsonResult,
|
|
@@ -277,10 +279,15 @@ const axDeliveryPendingOperation = defineOperation<z.infer<typeof axDeliveryPend
|
|
|
277
279
|
const consumer = typeof input.consumer === 'string' ? input.consumer : undefined;
|
|
278
280
|
const limitRaw = Number(input.limit ?? '');
|
|
279
281
|
const limit = Number.isFinite(limitRaw) && limitRaw > 0 ? limitRaw : undefined;
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
282
|
+
// #68: default FIFO (oldest-first) for ordered processing; `order:"newest"`
|
|
283
|
+
// surfaces the latest browser-originated steering first so a small `limit`
|
|
284
|
+
// can't bury the human's current action behind stale undelivered rows. Both
|
|
285
|
+
// queries apply the same loop-safe consumer filter before the limit.
|
|
286
|
+
const newest = input.order === 'newest';
|
|
287
|
+
const scope = { ...(consumer ? { consumer } : {}), ...(limit ? { limit } : {}) };
|
|
288
|
+
const pending = newest
|
|
289
|
+
? canvasState.getPendingSteeringForContext(scope)
|
|
290
|
+
: canvasState.getPendingSteering(scope);
|
|
284
291
|
// The MCP tool aggregated pendingActivity; one wire body now serves it over
|
|
285
292
|
// HTTP too (documented broadening). Loop-safe: consumer scopes both queries.
|
|
286
293
|
const pendingActivity = buildPendingAxActivity(canvasState.getAxState(), consumer);
|
|
@@ -147,6 +147,7 @@ const startOperation = defineOperation<z.infer<typeof startSchema>, WebviewStart
|
|
|
147
147
|
http: {
|
|
148
148
|
method: 'POST',
|
|
149
149
|
path: '/api/workbench/webview/start',
|
|
150
|
+
errorBodyAsResult: true,
|
|
150
151
|
// Mirror the legacy handler status codes from the SERIALIZED wire body
|
|
151
152
|
// (`status` receives the serialized result): 200 ok; 503 server-not-running
|
|
152
153
|
// ({ ok:false, error } — no webview); else 501 when the runtime is
|
|
@@ -173,14 +174,24 @@ const startOperation = defineOperation<z.infer<typeof startSchema>, WebviewStart
|
|
|
173
174
|
},
|
|
174
175
|
// dataStoreDir is sandboxed to the workspace in buildStartOptions (both the
|
|
175
176
|
// MCP and HTTP surfaces), so no MCP-only buildInput is needed.
|
|
176
|
-
// formatResult receives the SERIALIZED wire body.
|
|
177
|
-
//
|
|
178
|
-
//
|
|
177
|
+
// formatResult receives the SERIALIZED wire body. On success JSON-stringifies the
|
|
178
|
+
// webview status. On failure return parseable JSON ({ ok:false, error, webview })
|
|
179
|
+
// — NOT a bare message string — so MCP clients can reliably tell a failure/timeout
|
|
180
|
+
// apart from valid tool content instead of choking on non-JSON text (report #66).
|
|
181
|
+
// isError still flags the tool-call failure. (The legacy tool returned a bare
|
|
182
|
+
// message here; the composite + standalone now share this structured shape.)
|
|
179
183
|
formatResult: (result) => {
|
|
180
184
|
const body = result as { ok?: boolean; webview?: WebviewStatus; error?: string };
|
|
181
185
|
if (body.ok && body.webview) return statusText(body.webview);
|
|
182
186
|
return {
|
|
183
|
-
content: [{
|
|
187
|
+
content: [{
|
|
188
|
+
type: 'text' as const,
|
|
189
|
+
text: JSON.stringify({
|
|
190
|
+
ok: false,
|
|
191
|
+
error: body.error ?? 'WebView start failed.',
|
|
192
|
+
...(body.webview ? { webview: body.webview } : {}),
|
|
193
|
+
}, null, 2),
|
|
194
|
+
}],
|
|
184
195
|
isError: true,
|
|
185
196
|
};
|
|
186
197
|
},
|