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
package/src/server/ax-state.ts
CHANGED
|
@@ -13,7 +13,7 @@ export interface PmxAxFocusState {
|
|
|
13
13
|
// ── New enums ──────────────────────────────────────────────────────
|
|
14
14
|
export type PmxAxEventKind =
|
|
15
15
|
| 'prompt' | 'assistant-message' | 'tool-start' | 'tool-result'
|
|
16
|
-
| 'failure' | 'approval' | 'steering' | 'command';
|
|
16
|
+
| 'failure' | 'approval' | 'steering' | 'command' | 'note';
|
|
17
17
|
export type PmxAxEvidenceKind =
|
|
18
18
|
| 'logs' | 'tool-result' | 'screenshot' | 'file' | 'diff' | 'test-output';
|
|
19
19
|
export type PmxAxWorkItemStatus = 'todo' | 'in-progress' | 'blocked' | 'done' | 'cancelled';
|
|
@@ -157,6 +157,22 @@ export interface PmxAxFocusContext extends PmxAxFocusState {
|
|
|
157
157
|
nodes: AgentContextNode[];
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
// ── Pending agent-actionable digest (open canvas-bound items) ──────
|
|
161
|
+
export interface PendingAxActivityItem {
|
|
162
|
+
kind: 'work-item' | 'approval-gate' | 'elicitation' | 'mode-request';
|
|
163
|
+
id: string;
|
|
164
|
+
title: string;
|
|
165
|
+
status: string;
|
|
166
|
+
nodeIds: string[];
|
|
167
|
+
source: PmxAxSource | null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ── Delivery lead block (compact, un-truncated; for per-turn injection) ──
|
|
171
|
+
export interface PmxAxDeliveryContext {
|
|
172
|
+
pendingSteering: PmxAxSteeringMessage[];
|
|
173
|
+
pendingActivity: PendingAxActivityItem[];
|
|
174
|
+
}
|
|
175
|
+
|
|
160
176
|
export interface PmxAxContext {
|
|
161
177
|
version: 1;
|
|
162
178
|
generatedAt: string;
|
|
@@ -164,6 +180,10 @@ export interface PmxAxContext {
|
|
|
164
180
|
nodeCount: number;
|
|
165
181
|
edgeCount: number;
|
|
166
182
|
};
|
|
183
|
+
// Compact, loop-safe lead block of what's awaiting the agent right now
|
|
184
|
+
// (undelivered steering + open work/approvals/elicitations/mode requests).
|
|
185
|
+
// Sits ABOVE the full dump so a host adapter can inject it un-truncated.
|
|
186
|
+
delivery: PmxAxDeliveryContext;
|
|
167
187
|
pinned: PmxAxPinnedContext;
|
|
168
188
|
focus: PmxAxFocusContext;
|
|
169
189
|
workItems: PmxAxWorkItem[];
|
|
@@ -176,6 +196,44 @@ export interface PmxAxContext {
|
|
|
176
196
|
host: PmxAxHostCapability | null;
|
|
177
197
|
}
|
|
178
198
|
|
|
199
|
+
const OPEN_AX_WORK_STATUSES = new Set<PmxAxWorkItemStatus>(['todo', 'in-progress', 'blocked']);
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Open, agent-actionable canvas-bound AX items (open work items + pending approval
|
|
203
|
+
* gates / elicitations / mode requests). Unlike steering (a directive routed through
|
|
204
|
+
* the claim/ack delivery queue), these are STATE the human curates in the browser —
|
|
205
|
+
* they fire `ax-state-changed` (so resource-subscribers are pushed canvas://ax-work),
|
|
206
|
+
* but an adapterless client that only POLLS the delivery surface never saw them.
|
|
207
|
+
* Optionally excludes items the consumer itself originated (loop prevention),
|
|
208
|
+
* mirroring getPendingSteering. Shared by the MCP delivery surface and the HTTP
|
|
209
|
+
* context lead block so the digest never drifts between the two.
|
|
210
|
+
*/
|
|
211
|
+
export function buildPendingAxActivity(state: PmxAxState, consumer?: string): PendingAxActivityItem[] {
|
|
212
|
+
const notMine = (source: PmxAxSource | null) => !consumer || source !== consumer;
|
|
213
|
+
const out: PendingAxActivityItem[] = [];
|
|
214
|
+
for (const w of state.workItems) {
|
|
215
|
+
if (OPEN_AX_WORK_STATUSES.has(w.status) && notMine(w.source)) {
|
|
216
|
+
out.push({ kind: 'work-item', id: w.id, title: w.title, status: w.status, nodeIds: w.nodeIds, source: w.source });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
for (const g of state.approvalGates) {
|
|
220
|
+
if (g.status === 'pending' && notMine(g.source)) {
|
|
221
|
+
out.push({ kind: 'approval-gate', id: g.id, title: g.title, status: g.status, nodeIds: g.nodeIds, source: g.source });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
for (const e of state.elicitations) {
|
|
225
|
+
if (e.status === 'pending' && notMine(e.source)) {
|
|
226
|
+
out.push({ kind: 'elicitation', id: e.id, title: e.prompt, status: e.status, nodeIds: e.nodeIds, source: e.source });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
for (const m of state.modeRequests) {
|
|
230
|
+
if (m.status === 'pending' && notMine(m.source)) {
|
|
231
|
+
out.push({ kind: 'mode-request', id: m.id, title: m.reason ? `${m.mode}: ${m.reason}` : `mode: ${m.mode}`, status: m.status, nodeIds: m.nodeIds, source: m.source });
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return out;
|
|
235
|
+
}
|
|
236
|
+
|
|
179
237
|
const AX_SOURCES = new Set<PmxAxSource>(['agent', 'api', 'browser', 'cli', 'codex', 'copilot', 'mcp', 'sdk', 'system']);
|
|
180
238
|
|
|
181
239
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
@@ -205,7 +263,34 @@ function normalizeNodeIds(value: unknown, validNodeIds?: Set<string>): string[]
|
|
|
205
263
|
return ids;
|
|
206
264
|
}
|
|
207
265
|
|
|
208
|
-
const AX_EVENT_KINDS = new Set<PmxAxEventKind>(['prompt', 'assistant-message', 'tool-start', 'tool-result', 'failure', 'approval', 'steering', 'command']);
|
|
266
|
+
const AX_EVENT_KINDS = new Set<PmxAxEventKind>(['prompt', 'assistant-message', 'tool-start', 'tool-result', 'failure', 'approval', 'steering', 'command', 'note']);
|
|
267
|
+
|
|
268
|
+
// ── Activity ingestion (harness-forwarded tool/session events) ─────
|
|
269
|
+
// A normalized activity the agent's harness forwards; the board auto-reacts.
|
|
270
|
+
// The precise activity kind is preserved on the recorded event's `data.activityKind`;
|
|
271
|
+
// only the (coarser) timeline event kind is constrained to PmxAxEventKind.
|
|
272
|
+
export type PmxAxActivityKind =
|
|
273
|
+
| 'tool-start' | 'tool-result' | 'failure' | 'error'
|
|
274
|
+
| 'session-start' | 'session-end' | 'command' | 'note';
|
|
275
|
+
const AX_ACTIVITY_KINDS = new Set<PmxAxActivityKind>([
|
|
276
|
+
'tool-start', 'tool-result', 'failure', 'error', 'session-start', 'session-end', 'command', 'note',
|
|
277
|
+
]);
|
|
278
|
+
const ACTIVITY_TO_EVENT_KIND: Record<PmxAxActivityKind, PmxAxEventKind> = {
|
|
279
|
+
'tool-start': 'tool-start',
|
|
280
|
+
'tool-result': 'tool-result',
|
|
281
|
+
'failure': 'failure',
|
|
282
|
+
'error': 'failure',
|
|
283
|
+
'session-start': 'assistant-message',
|
|
284
|
+
'session-end': 'assistant-message',
|
|
285
|
+
'command': 'command',
|
|
286
|
+
'note': 'assistant-message',
|
|
287
|
+
};
|
|
288
|
+
export function isAxActivityKind(value: unknown): value is PmxAxActivityKind {
|
|
289
|
+
return typeof value === 'string' && AX_ACTIVITY_KINDS.has(value as PmxAxActivityKind);
|
|
290
|
+
}
|
|
291
|
+
export function mapAxActivityKindToEventKind(kind: PmxAxActivityKind): PmxAxEventKind {
|
|
292
|
+
return ACTIVITY_TO_EVENT_KIND[kind];
|
|
293
|
+
}
|
|
209
294
|
|
|
210
295
|
// ── Command registry (plan-004 Phase 5) ────────────────────────
|
|
211
296
|
// Named, registry-gated PMX intents — NOT arbitrary execution. Invoking a command
|
|
@@ -725,6 +810,7 @@ export function normalizeAxState(input: unknown, validNodeIds?: Set<string>): Pm
|
|
|
725
810
|
|
|
726
811
|
export function buildAxContext(input: {
|
|
727
812
|
layout: CanvasLayout;
|
|
813
|
+
delivery: PmxAxDeliveryContext;
|
|
728
814
|
pinned: PmxAxPinnedContext;
|
|
729
815
|
focus: PmxAxFocusState;
|
|
730
816
|
focusNodes: AgentContextNode[];
|
|
@@ -744,6 +830,7 @@ export function buildAxContext(input: {
|
|
|
744
830
|
nodeCount: input.layout.nodes.length,
|
|
745
831
|
edgeCount: input.layout.edges.length,
|
|
746
832
|
},
|
|
833
|
+
delivery: input.delivery,
|
|
747
834
|
pinned: input.pinned,
|
|
748
835
|
focus: {
|
|
749
836
|
...input.focus,
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { canvasState } from './canvas-state.js';
|
|
2
|
+
|
|
3
|
+
/** Hard ceiling on a single blocking wait, regardless of the requested timeout. */
|
|
4
|
+
export const AX_WAIT_MAX_MS = 120000;
|
|
5
|
+
|
|
6
|
+
export interface AxWaitResult<T> {
|
|
7
|
+
/** Latest value, or null if the item does not exist / vanished mid-wait. */
|
|
8
|
+
value: T | null;
|
|
9
|
+
/** True only when the item still exists and is still pending after the wait. */
|
|
10
|
+
pending: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Block until a canvas-bound AX item resolves (its status leaves `pending`), the
|
|
15
|
+
* timeout elapses, or the request aborts — the server side of report primitive D
|
|
16
|
+
* ("gates that actually gate"). Resolves immediately when the item is already
|
|
17
|
+
* resolved, missing, or `timeoutMs <= 0` (a plain single read). Subscribes to the
|
|
18
|
+
* `ax` change channel and always disposes the listener + timer.
|
|
19
|
+
*/
|
|
20
|
+
export async function waitForAxResolution<T extends { status: string }>(opts: {
|
|
21
|
+
read: () => T | null;
|
|
22
|
+
isResolved: (value: T) => boolean;
|
|
23
|
+
timeoutMs: number;
|
|
24
|
+
signal?: AbortSignal;
|
|
25
|
+
}): Promise<AxWaitResult<T>> {
|
|
26
|
+
const { read, isResolved } = opts;
|
|
27
|
+
const timeoutMs = Math.max(0, Math.min(opts.timeoutMs, AX_WAIT_MAX_MS));
|
|
28
|
+
const pendingOf = (v: T | null): boolean => (v ? !isResolved(v) : false);
|
|
29
|
+
|
|
30
|
+
const current = read();
|
|
31
|
+
if (!current || isResolved(current) || timeoutMs === 0 || opts.signal?.aborted) {
|
|
32
|
+
return { value: current, pending: pendingOf(current) };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return new Promise<AxWaitResult<T>>((resolve) => {
|
|
36
|
+
let done = false;
|
|
37
|
+
const finish = (): void => {
|
|
38
|
+
if (done) return;
|
|
39
|
+
done = true;
|
|
40
|
+
clearTimeout(timer);
|
|
41
|
+
dispose();
|
|
42
|
+
opts.signal?.removeEventListener('abort', onSettle);
|
|
43
|
+
const v = read();
|
|
44
|
+
resolve({ value: v, pending: pendingOf(v) });
|
|
45
|
+
};
|
|
46
|
+
const onSettle = finish;
|
|
47
|
+
const check = (type: string): void => {
|
|
48
|
+
if (type !== 'ax') return;
|
|
49
|
+
const v = read();
|
|
50
|
+
if (!v || isResolved(v)) finish();
|
|
51
|
+
};
|
|
52
|
+
const timer = setTimeout(finish, timeoutMs);
|
|
53
|
+
const dispose = canvasState.onChange(check);
|
|
54
|
+
opts.signal?.addEventListener('abort', onSettle, { once: true });
|
|
55
|
+
});
|
|
56
|
+
}
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
import { mutationHistory } from './mutation-history.js';
|
|
20
20
|
import { computeGroupBounds, findOpenCanvasPosition } from './placement.js';
|
|
21
21
|
import { searchNodes } from './spatial-analysis.js';
|
|
22
|
-
import { getCanvasNodeTitle
|
|
22
|
+
import { getCanvasNodeTitle } from './canvas-serialization.js';
|
|
23
23
|
import { computeAutoArrange } from '../shared/auto-arrange.js';
|
|
24
24
|
import {
|
|
25
25
|
applyJsonRenderStreamPatches,
|
|
@@ -52,7 +52,7 @@ export function setCanvasLayoutUpdateEmitter(emitter: (() => void) | null): void
|
|
|
52
52
|
canvasLayoutUpdateEmitter = emitter;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
function emitCanvasLayoutUpdate(): void {
|
|
55
|
+
export function emitCanvasLayoutUpdate(): void {
|
|
56
56
|
canvasLayoutUpdateEmitter?.();
|
|
57
57
|
}
|
|
58
58
|
|
|
@@ -125,12 +125,6 @@ interface CanvasCreateGroupInput {
|
|
|
125
125
|
childLayout?: CanvasArrangeMode;
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
export interface CanvasBatchOperation {
|
|
129
|
-
op: string;
|
|
130
|
-
assign?: string;
|
|
131
|
-
args?: Record<string, unknown>;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
128
|
interface CanvasNodeLookupInput {
|
|
135
129
|
id?: string;
|
|
136
130
|
search?: string;
|
|
@@ -1659,323 +1653,3 @@ export function fitCanvasView(options: CanvasFitViewOptions = {}): CanvasFitView
|
|
|
1659
1653
|
canvasState.setViewport(viewport);
|
|
1660
1654
|
return { ok: true, viewport: canvasState.viewport, nodeCount: targetNodes.length, bounds };
|
|
1661
1655
|
}
|
|
1662
|
-
|
|
1663
|
-
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
1664
|
-
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
1665
|
-
}
|
|
1666
|
-
|
|
1667
|
-
function resolveBatchRefs(value: unknown, refs: Record<string, unknown>): unknown {
|
|
1668
|
-
if (typeof value === 'string' && value.startsWith('$')) {
|
|
1669
|
-
const path = value.slice(1).split('.');
|
|
1670
|
-
let current: unknown = refs[path[0] ?? ''];
|
|
1671
|
-
if (path.length === 1 && isPlainRecord(current) && typeof current.id === 'string') return current.id;
|
|
1672
|
-
for (const segment of path.slice(1)) {
|
|
1673
|
-
if (!isPlainRecord(current) && !Array.isArray(current)) return undefined;
|
|
1674
|
-
current = (current as Record<string, unknown>)[segment];
|
|
1675
|
-
}
|
|
1676
|
-
return current;
|
|
1677
|
-
}
|
|
1678
|
-
if (Array.isArray(value)) return value.map((item) => resolveBatchRefs(item, refs));
|
|
1679
|
-
if (isPlainRecord(value)) {
|
|
1680
|
-
const resolved: Record<string, unknown> = {};
|
|
1681
|
-
for (const [key, child] of Object.entries(value)) {
|
|
1682
|
-
resolved[key] = resolveBatchRefs(child, refs);
|
|
1683
|
-
}
|
|
1684
|
-
return resolved;
|
|
1685
|
-
}
|
|
1686
|
-
return value;
|
|
1687
|
-
}
|
|
1688
|
-
|
|
1689
|
-
function serializeCreatedNode(node: CanvasNodeState): SerializedCanvasNode {
|
|
1690
|
-
return serializeCanvasNodeCompact(node);
|
|
1691
|
-
}
|
|
1692
|
-
|
|
1693
|
-
export async function executeCanvasBatch(
|
|
1694
|
-
operations: CanvasBatchOperation[],
|
|
1695
|
-
): Promise<{
|
|
1696
|
-
ok: boolean;
|
|
1697
|
-
results: Array<Record<string, unknown>>;
|
|
1698
|
-
refs: Record<string, unknown>;
|
|
1699
|
-
failedIndex?: number;
|
|
1700
|
-
error?: string;
|
|
1701
|
-
}> {
|
|
1702
|
-
const refs: Record<string, unknown> = {};
|
|
1703
|
-
const results: Array<Record<string, unknown>> = [];
|
|
1704
|
-
|
|
1705
|
-
for (let index = 0; index < operations.length; index++) {
|
|
1706
|
-
const operation = operations[index];
|
|
1707
|
-
const args = isPlainRecord(operation.args) ? resolveBatchRefs(operation.args, refs) : {};
|
|
1708
|
-
if (!isPlainRecord(args)) {
|
|
1709
|
-
return {
|
|
1710
|
-
ok: false,
|
|
1711
|
-
failedIndex: index,
|
|
1712
|
-
error: `Operation ${index} has invalid args.`,
|
|
1713
|
-
results,
|
|
1714
|
-
refs,
|
|
1715
|
-
};
|
|
1716
|
-
}
|
|
1717
|
-
|
|
1718
|
-
try {
|
|
1719
|
-
let result: Record<string, unknown>;
|
|
1720
|
-
switch (operation.op) {
|
|
1721
|
-
case 'node.add': {
|
|
1722
|
-
const type = typeof args.type === 'string' ? args.type : 'markdown';
|
|
1723
|
-
if (type === 'html-primitive') {
|
|
1724
|
-
throw new Error('Batch html-primitive creation is not supported yet. Use node.add with type "html" and generated html, or create the primitive through MCP/HTTP/CLI first.');
|
|
1725
|
-
}
|
|
1726
|
-
if (type === 'webpage') {
|
|
1727
|
-
const content = typeof args.url === 'string' && args.url.trim().length > 0
|
|
1728
|
-
? args.url
|
|
1729
|
-
: typeof args.content === 'string'
|
|
1730
|
-
? args.content
|
|
1731
|
-
: undefined;
|
|
1732
|
-
const created = addCanvasNode({
|
|
1733
|
-
type: 'webpage',
|
|
1734
|
-
...(typeof args.title === 'string' ? { title: args.title } : {}),
|
|
1735
|
-
...(content ? { content } : {}),
|
|
1736
|
-
...(isPlainRecord(args.data) ? { data: args.data } : {}),
|
|
1737
|
-
...(typeof args.x === 'number' ? { x: args.x } : {}),
|
|
1738
|
-
...(typeof args.y === 'number' ? { y: args.y } : {}),
|
|
1739
|
-
...(typeof args.width === 'number' ? { width: args.width } : {}),
|
|
1740
|
-
...(typeof args.height === 'number' ? { height: args.height } : {}),
|
|
1741
|
-
...(args.strictSize === true ? { strictSize: true } : {}),
|
|
1742
|
-
defaultWidth: 520,
|
|
1743
|
-
defaultHeight: 420,
|
|
1744
|
-
});
|
|
1745
|
-
const fetch = await refreshCanvasWebpageNode(created.id);
|
|
1746
|
-
const refreshed = canvasState.getNode(created.id) ?? created.node;
|
|
1747
|
-
result = {
|
|
1748
|
-
ok: true,
|
|
1749
|
-
...serializeCreatedNode(refreshed),
|
|
1750
|
-
fetch: fetch.ok
|
|
1751
|
-
? { ok: true }
|
|
1752
|
-
: { ok: false, error: fetch.error ?? 'Failed to fetch webpage content.' },
|
|
1753
|
-
...(fetch.ok ? {} : { error: fetch.error }),
|
|
1754
|
-
};
|
|
1755
|
-
} else {
|
|
1756
|
-
const data = isPlainRecord(args.data) ? args.data : {};
|
|
1757
|
-
const htmlData = type === 'html'
|
|
1758
|
-
? {
|
|
1759
|
-
...data,
|
|
1760
|
-
...(typeof args.html === 'string' ? { html: args.html } : {}),
|
|
1761
|
-
...(typeof args.summary === 'string' ? { summary: args.summary } : {}),
|
|
1762
|
-
...(typeof args.agentSummary === 'string' ? { agentSummary: args.agentSummary } : {}),
|
|
1763
|
-
...(typeof args.description === 'string' ? { description: args.description } : {}),
|
|
1764
|
-
...(Array.isArray(args.embeddedNodeIds) ? { embeddedNodeIds: args.embeddedNodeIds } : {}),
|
|
1765
|
-
...(Array.isArray(args.embeddedUrls) ? { embeddedUrls: args.embeddedUrls } : {}),
|
|
1766
|
-
}
|
|
1767
|
-
: data;
|
|
1768
|
-
const created = addCanvasNode({
|
|
1769
|
-
type: type as CanvasNodeState['type'],
|
|
1770
|
-
...(typeof args.title === 'string' ? { title: args.title } : {}),
|
|
1771
|
-
...(typeof args.content === 'string' ? { content: args.content } : {}),
|
|
1772
|
-
...(Object.keys(htmlData).length > 0 ? { data: htmlData } : {}),
|
|
1773
|
-
...(typeof args.x === 'number' ? { x: args.x } : {}),
|
|
1774
|
-
...(typeof args.y === 'number' ? { y: args.y } : {}),
|
|
1775
|
-
...(typeof args.width === 'number' ? { width: args.width } : {}),
|
|
1776
|
-
...(typeof args.height === 'number' ? { height: args.height } : {}),
|
|
1777
|
-
...(args.strictSize === true ? { strictSize: true } : {}),
|
|
1778
|
-
defaultWidth: type === 'html'
|
|
1779
|
-
? 720
|
|
1780
|
-
: type === 'markdown'
|
|
1781
|
-
? MARKDOWN_NODE_DEFAULT_SIZE.width
|
|
1782
|
-
: type === 'mcp-app'
|
|
1783
|
-
? MCP_APP_NODE_DEFAULT_SIZE.width
|
|
1784
|
-
: type === 'image'
|
|
1785
|
-
? IMAGE_NODE_DEFAULT_SIZE.width
|
|
1786
|
-
: type === 'ledger'
|
|
1787
|
-
? LEDGER_NODE_DEFAULT_SIZE.width
|
|
1788
|
-
: 360,
|
|
1789
|
-
defaultHeight: type === 'html'
|
|
1790
|
-
? 640
|
|
1791
|
-
: type === 'markdown'
|
|
1792
|
-
? MARKDOWN_NODE_DEFAULT_SIZE.height
|
|
1793
|
-
: type === 'mcp-app'
|
|
1794
|
-
? MCP_APP_NODE_DEFAULT_SIZE.height
|
|
1795
|
-
: type === 'image'
|
|
1796
|
-
? IMAGE_NODE_DEFAULT_SIZE.height
|
|
1797
|
-
: type === 'ledger'
|
|
1798
|
-
? LEDGER_NODE_DEFAULT_SIZE.height
|
|
1799
|
-
: 200,
|
|
1800
|
-
fileMode: 'auto',
|
|
1801
|
-
});
|
|
1802
|
-
result = { ok: true, ...serializeCreatedNode(created.node) };
|
|
1803
|
-
}
|
|
1804
|
-
break;
|
|
1805
|
-
}
|
|
1806
|
-
case 'node.update': {
|
|
1807
|
-
const id = typeof args.id === 'string' ? args.id : '';
|
|
1808
|
-
const node = canvasState.getNode(id);
|
|
1809
|
-
if (!node) throw new Error(`Node "${id}" not found.`);
|
|
1810
|
-
const patch: Partial<CanvasNodeState> = {};
|
|
1811
|
-
if (typeof args.x === 'number' || typeof args.y === 'number') {
|
|
1812
|
-
patch.position = {
|
|
1813
|
-
x: typeof args.x === 'number' ? args.x : node.position.x,
|
|
1814
|
-
y: typeof args.y === 'number' ? args.y : node.position.y,
|
|
1815
|
-
};
|
|
1816
|
-
}
|
|
1817
|
-
if (typeof args.width === 'number' || typeof args.height === 'number') {
|
|
1818
|
-
patch.size = {
|
|
1819
|
-
width: typeof args.width === 'number' ? args.width : node.size.width,
|
|
1820
|
-
height: typeof args.height === 'number' ? args.height : node.size.height,
|
|
1821
|
-
};
|
|
1822
|
-
}
|
|
1823
|
-
if (typeof args.collapsed === 'boolean') patch.collapsed = args.collapsed;
|
|
1824
|
-
if (typeof args.pinned === 'boolean') patch.pinned = args.pinned;
|
|
1825
|
-
if (args.dockPosition === null || args.dockPosition === 'left' || args.dockPosition === 'right') {
|
|
1826
|
-
patch.dockPosition = args.dockPosition;
|
|
1827
|
-
}
|
|
1828
|
-
if (typeof args.title === 'string' || typeof args.content === 'string' || typeof args.arrangeLocked === 'boolean' || typeof args.strictSize === 'boolean' || isPlainRecord(args.data)) {
|
|
1829
|
-
patch.data = {
|
|
1830
|
-
...node.data,
|
|
1831
|
-
...(typeof args.title === 'string' ? { title: args.title } : {}),
|
|
1832
|
-
...(typeof args.content === 'string' ? { content: args.content } : {}),
|
|
1833
|
-
...(typeof args.arrangeLocked === 'boolean' ? { arrangeLocked: args.arrangeLocked } : {}),
|
|
1834
|
-
...(typeof args.strictSize === 'boolean' ? { strictSize: args.strictSize } : {}),
|
|
1835
|
-
...(isPlainRecord(args.data) ? args.data : {}),
|
|
1836
|
-
};
|
|
1837
|
-
}
|
|
1838
|
-
canvasState.updateNode(id, patch);
|
|
1839
|
-
const updated = canvasState.getNode(id);
|
|
1840
|
-
result = { ok: true, ...(updated ? serializeCreatedNode(updated) : { id }) };
|
|
1841
|
-
break;
|
|
1842
|
-
}
|
|
1843
|
-
case 'node.remove': {
|
|
1844
|
-
const id = typeof args.id === 'string' ? args.id : '';
|
|
1845
|
-
const { removed } = removeCanvasNode(id);
|
|
1846
|
-
if (!removed) throw new Error(`Node "${id}" not found.`);
|
|
1847
|
-
result = { ok: true, id, removed: true };
|
|
1848
|
-
break;
|
|
1849
|
-
}
|
|
1850
|
-
case 'graph.add': {
|
|
1851
|
-
const created = createCanvasGraphNode({
|
|
1852
|
-
graphType: String(args.graphType ?? 'line'),
|
|
1853
|
-
data: Array.isArray(args.data) ? args.data.filter((item): item is Record<string, unknown> => isPlainRecord(item)) : [],
|
|
1854
|
-
...(typeof args.title === 'string' ? { title: args.title } : {}),
|
|
1855
|
-
...(typeof args.xKey === 'string' ? { xKey: args.xKey } : {}),
|
|
1856
|
-
...(typeof args.yKey === 'string' ? { yKey: args.yKey } : {}),
|
|
1857
|
-
...(typeof args.zKey === 'string' ? { zKey: args.zKey } : {}),
|
|
1858
|
-
...(typeof args.nameKey === 'string' ? { nameKey: args.nameKey } : {}),
|
|
1859
|
-
...(typeof args.valueKey === 'string' ? { valueKey: args.valueKey } : {}),
|
|
1860
|
-
...(typeof args.axisKey === 'string' ? { axisKey: args.axisKey } : {}),
|
|
1861
|
-
...(Array.isArray(args.metrics) ? { metrics: args.metrics.filter((m): m is string => typeof m === 'string') } : {}),
|
|
1862
|
-
...(Array.isArray(args.series) ? { series: args.series.filter((s): s is string => typeof s === 'string') } : {}),
|
|
1863
|
-
...(typeof args.barKey === 'string' ? { barKey: args.barKey } : {}),
|
|
1864
|
-
...(typeof args.lineKey === 'string' ? { lineKey: args.lineKey } : {}),
|
|
1865
|
-
...(args.aggregate === 'sum' || args.aggregate === 'count' || args.aggregate === 'avg'
|
|
1866
|
-
? { aggregate: args.aggregate }
|
|
1867
|
-
: {}),
|
|
1868
|
-
...(typeof args.color === 'string' ? { color: args.color } : {}),
|
|
1869
|
-
...(typeof args.barColor === 'string' ? { barColor: args.barColor } : {}),
|
|
1870
|
-
...(typeof args.lineColor === 'string' ? { lineColor: args.lineColor } : {}),
|
|
1871
|
-
...(typeof args.height === 'number' ? { height: args.height } : {}),
|
|
1872
|
-
...(typeof args.x === 'number' ? { x: args.x } : {}),
|
|
1873
|
-
...(typeof args.y === 'number' ? { y: args.y } : {}),
|
|
1874
|
-
...(typeof args.width === 'number' ? { width: args.width } : {}),
|
|
1875
|
-
...(typeof args.nodeHeight === 'number' ? { heightPx: args.nodeHeight } : {}),
|
|
1876
|
-
...(args.strictSize === true ? { strictSize: true } : {}),
|
|
1877
|
-
});
|
|
1878
|
-
result = {
|
|
1879
|
-
ok: true,
|
|
1880
|
-
...serializeCreatedNode(created.node),
|
|
1881
|
-
url: created.url,
|
|
1882
|
-
spec: created.spec,
|
|
1883
|
-
};
|
|
1884
|
-
break;
|
|
1885
|
-
}
|
|
1886
|
-
case 'edge.add': {
|
|
1887
|
-
const added = addCanvasEdge({
|
|
1888
|
-
...(typeof args.from === 'string' ? { from: args.from } : {}),
|
|
1889
|
-
...(typeof args.to === 'string' ? { to: args.to } : {}),
|
|
1890
|
-
...(typeof args.fromSearch === 'string' ? { fromSearch: args.fromSearch } : {}),
|
|
1891
|
-
...(typeof args.toSearch === 'string' ? { toSearch: args.toSearch } : {}),
|
|
1892
|
-
type: String(args.type) as CanvasEdge['type'],
|
|
1893
|
-
...(typeof args.label === 'string' ? { label: args.label } : {}),
|
|
1894
|
-
...(typeof args.style === 'string' ? { style: args.style as CanvasEdge['style'] } : {}),
|
|
1895
|
-
...(typeof args.animated === 'boolean' ? { animated: args.animated } : {}),
|
|
1896
|
-
});
|
|
1897
|
-
result = { ok: true, ...added };
|
|
1898
|
-
break;
|
|
1899
|
-
}
|
|
1900
|
-
case 'group.create': {
|
|
1901
|
-
const created = createCanvasGroup({
|
|
1902
|
-
...(typeof args.title === 'string' ? { title: args.title } : {}),
|
|
1903
|
-
...(Array.isArray(args.childIds) ? { childIds: args.childIds.filter((id): id is string => typeof id === 'string') } : {}),
|
|
1904
|
-
...(typeof args.x === 'number' ? { x: args.x } : {}),
|
|
1905
|
-
...(typeof args.y === 'number' ? { y: args.y } : {}),
|
|
1906
|
-
...(typeof args.width === 'number' ? { width: args.width } : {}),
|
|
1907
|
-
...(typeof args.height === 'number' ? { height: args.height } : {}),
|
|
1908
|
-
...(typeof args.color === 'string' ? { color: args.color } : {}),
|
|
1909
|
-
...(args.childLayout === 'grid' || args.childLayout === 'column' || args.childLayout === 'flow'
|
|
1910
|
-
? { childLayout: args.childLayout }
|
|
1911
|
-
: {}),
|
|
1912
|
-
});
|
|
1913
|
-
result = { ok: true, ...serializeCreatedNode(created.node) };
|
|
1914
|
-
break;
|
|
1915
|
-
}
|
|
1916
|
-
case 'group.add': {
|
|
1917
|
-
const groupId = typeof args.groupId === 'string' ? args.groupId : '';
|
|
1918
|
-
const childIds = Array.isArray(args.childIds) ? args.childIds.filter((id): id is string => typeof id === 'string') : [];
|
|
1919
|
-
const ok = canvasState.groupNodes(groupId, childIds, {
|
|
1920
|
-
preservePositions: args.childLayout === undefined,
|
|
1921
|
-
...(args.childLayout === 'grid' || args.childLayout === 'column' || args.childLayout === 'flow'
|
|
1922
|
-
? { layout: args.childLayout }
|
|
1923
|
-
: {}),
|
|
1924
|
-
});
|
|
1925
|
-
if (!ok) throw new Error('Group not found or no valid children.');
|
|
1926
|
-
const group = canvasState.getNode(groupId);
|
|
1927
|
-
result = { ok: true, ...(group ? serializeCreatedNode(group) : { id: groupId }) };
|
|
1928
|
-
break;
|
|
1929
|
-
}
|
|
1930
|
-
case 'group.remove': {
|
|
1931
|
-
const groupId = typeof args.groupId === 'string' ? args.groupId : '';
|
|
1932
|
-
const ok = canvasState.ungroupNodes(groupId);
|
|
1933
|
-
if (!ok) throw new Error('Group not found or empty.');
|
|
1934
|
-
result = { ok: true, groupId };
|
|
1935
|
-
break;
|
|
1936
|
-
}
|
|
1937
|
-
case 'pin.set':
|
|
1938
|
-
case 'pin.add':
|
|
1939
|
-
case 'pin.remove': {
|
|
1940
|
-
const ids = Array.isArray(args.nodeIds) ? args.nodeIds.filter((id): id is string => typeof id === 'string') : [];
|
|
1941
|
-
result = {
|
|
1942
|
-
ok: true,
|
|
1943
|
-
...setCanvasContextPins(ids, operation.op === 'pin.set' ? 'set' : operation.op === 'pin.add' ? 'add' : 'remove'),
|
|
1944
|
-
};
|
|
1945
|
-
break;
|
|
1946
|
-
}
|
|
1947
|
-
case 'snapshot.save': {
|
|
1948
|
-
const snapshot = saveCanvasSnapshot(typeof args.name === 'string' ? args.name : '');
|
|
1949
|
-
if (!snapshot) throw new Error('Failed to save snapshot.');
|
|
1950
|
-
result = { ok: true, snapshot };
|
|
1951
|
-
break;
|
|
1952
|
-
}
|
|
1953
|
-
case 'arrange': {
|
|
1954
|
-
const layout =
|
|
1955
|
-
args.layout === 'column' || args.layout === 'flow' || args.layout === 'grid'
|
|
1956
|
-
? args.layout
|
|
1957
|
-
: 'grid';
|
|
1958
|
-
result = { ok: true, ...arrangeCanvasNodes(layout) };
|
|
1959
|
-
break;
|
|
1960
|
-
}
|
|
1961
|
-
default:
|
|
1962
|
-
throw new Error(`Unsupported batch operation "${operation.op}".`);
|
|
1963
|
-
}
|
|
1964
|
-
|
|
1965
|
-
results.push(result);
|
|
1966
|
-
if (typeof operation.assign === 'string' && operation.assign.trim().length > 0) {
|
|
1967
|
-
refs[operation.assign] = result;
|
|
1968
|
-
}
|
|
1969
|
-
} catch (error) {
|
|
1970
|
-
return {
|
|
1971
|
-
ok: false,
|
|
1972
|
-
failedIndex: index,
|
|
1973
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1974
|
-
results,
|
|
1975
|
-
refs,
|
|
1976
|
-
};
|
|
1977
|
-
}
|
|
1978
|
-
}
|
|
1979
|
-
|
|
1980
|
-
return { ok: true, results, refs };
|
|
1981
|
-
}
|
|
@@ -244,7 +244,7 @@ const CANVAS_CREATE_TYPES: CanvasCreateTypeSchema[] = [
|
|
|
244
244
|
kind: 'node',
|
|
245
245
|
description: 'Sandboxed iframe node rendered from inline HTML.',
|
|
246
246
|
endpoint: '/api/canvas/node',
|
|
247
|
-
mcpTool: '
|
|
247
|
+
mcpTool: 'canvas_node (action:"add", type:"html")',
|
|
248
248
|
fields: [
|
|
249
249
|
{ name: 'html', type: 'string', required: false, description: 'HTML document or fragment rendered in the sandboxed iframe.', aliases: ['content', 'stdin'] },
|
|
250
250
|
{ name: 'summary', type: 'string', required: false, description: 'Explicit agent-readable summary. If omitted, PMX derives one from visible HTML text.' },
|
|
@@ -281,7 +281,7 @@ const CANVAS_CREATE_TYPES: CanvasCreateTypeSchema[] = [
|
|
|
281
281
|
kind: 'virtual-node',
|
|
282
282
|
description: 'Reusable sandboxed HTML communication primitive rendered as an html node.',
|
|
283
283
|
endpoint: '/api/canvas/node',
|
|
284
|
-
mcpTool: '
|
|
284
|
+
mcpTool: 'canvas_node (action:"add", type:"html", primitive:"<kind>")',
|
|
285
285
|
fields: [
|
|
286
286
|
{ name: 'kind', type: 'HtmlPrimitiveKind', required: true, description: 'Primitive kind. See top-level htmlPrimitives for the supported catalog.' },
|
|
287
287
|
{ name: 'data', type: 'record<string, unknown>', required: false, description: 'Primitive-specific JSON object payload.' },
|