pmx-canvas 0.1.36 → 0.2.1
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 +447 -0
- package/Readme.md +2 -2
- package/dist/json-render/index.js +89 -334
- package/dist/types/mcp/canvas-access.d.ts +5 -171
- package/dist/types/server/ax-state-manager.d.ts +267 -0
- package/dist/types/server/ax-state.d.ts +3 -1
- package/dist/types/server/canvas-db.d.ts +13 -0
- package/dist/types/server/canvas-operations.d.ts +1 -12
- package/dist/types/server/canvas-state.d.ts +8 -23
- package/dist/types/server/index.d.ts +6 -24
- 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 +19 -1
- package/docs/ax-state-contract.md +72 -0
- package/docs/http-api.md +4 -0
- package/docs/mcp.md +61 -12
- 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/tech-debt-assessment-2026-06.md +90 -0
- package/package.json +3 -3
- package/skills/pmx-canvas/SKILL.md +221 -193
- package/skills/pmx-canvas/evals/evals.json +3 -3
- package/skills/pmx-canvas/references/ax-html-control-surface.md +93 -0
- package/skills/pmx-canvas/references/codex-app-adapter.md +13 -14
- package/skills/pmx-canvas/references/github-copilot-app-adapter.md +26 -11
- package/src/cli/agent.ts +52 -31
- package/src/mcp/canvas-access.ts +30 -830
- package/src/mcp/server.ts +162 -2014
- package/src/server/ax-context.ts +8 -1
- package/src/server/ax-state-manager.ts +826 -0
- package/src/server/ax-state.ts +10 -2
- package/src/server/canvas-db.ts +35 -0
- package/src/server/canvas-operations.ts +2 -328
- package/src/server/canvas-schema.ts +2 -2
- package/src/server/canvas-state.ts +103 -465
- package/src/server/index.ts +54 -190
- 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 +158 -2255
- 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';
|
|
@@ -168,8 +168,16 @@ export interface PendingAxActivityItem {
|
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
// ── Delivery lead block (compact, un-truncated; for per-turn injection) ──
|
|
171
|
+
// `pendingSteering` here is NEWEST-first (most recent at index 0), capped at
|
|
172
|
+
// AX_CONTEXT_STEERING_LIMIT, so a fresh steer is always visible even behind a long
|
|
173
|
+
// backlog (report #57). This is "what's new?" awareness — distinct from the FIFO
|
|
174
|
+
// claim/ack delivery queue (`/api/canvas/ax/delivery/pending`, getPendingSteering),
|
|
175
|
+
// which stays OLDEST-first for ordered processing. The counts let an agent detect a
|
|
176
|
+
// backlog the compact block omits.
|
|
171
177
|
export interface PmxAxDeliveryContext {
|
|
172
178
|
pendingSteering: PmxAxSteeringMessage[];
|
|
179
|
+
totalPending: number;
|
|
180
|
+
omittedPending: number;
|
|
173
181
|
pendingActivity: PendingAxActivityItem[];
|
|
174
182
|
}
|
|
175
183
|
|
|
@@ -263,7 +271,7 @@ function normalizeNodeIds(value: unknown, validNodeIds?: Set<string>): string[]
|
|
|
263
271
|
return ids;
|
|
264
272
|
}
|
|
265
273
|
|
|
266
|
-
const AX_EVENT_KINDS = new Set<PmxAxEventKind>(['prompt', 'assistant-message', 'tool-start', 'tool-result', 'failure', 'approval', 'steering', 'command']);
|
|
274
|
+
const AX_EVENT_KINDS = new Set<PmxAxEventKind>(['prompt', 'assistant-message', 'tool-start', 'tool-result', 'failure', 'approval', 'steering', 'command', 'note']);
|
|
267
275
|
|
|
268
276
|
// ── Activity ingestion (harness-forwarded tool/session events) ─────
|
|
269
277
|
// A normalized activity the agent's harness forwards; the board auto-reacts.
|
package/src/server/canvas-db.ts
CHANGED
|
@@ -921,6 +921,41 @@ export function loadPendingAxSteeringFromDB(
|
|
|
921
921
|
.filter((s): s is PmxAxSteeringMessage => s !== null);
|
|
922
922
|
}
|
|
923
923
|
|
|
924
|
+
/**
|
|
925
|
+
* NEWEST undelivered steering first (report #57) for the compact AX context lead
|
|
926
|
+
* block — so a fresh steer is visible even behind a long backlog. Loop-safe: excludes
|
|
927
|
+
* the consumer's own steering in SQL so the LIMIT applies after loop-prevention.
|
|
928
|
+
* Distinct from loadPendingAxSteeringFromDB (FIFO oldest-first) which the claim/ack
|
|
929
|
+
* delivery queue uses for ordered processing.
|
|
930
|
+
*/
|
|
931
|
+
export function loadNewestPendingAxSteeringFromDB(
|
|
932
|
+
db: Database,
|
|
933
|
+
options: { consumer?: string; limit?: number } = {},
|
|
934
|
+
): PmxAxSteeringMessage[] {
|
|
935
|
+
interface Row { seq: number; id: string; message: string; delivered: number; created_at: string; source: string | null }
|
|
936
|
+
const limit = clampTimelineLimit(options.limit);
|
|
937
|
+
const rows = options.consumer
|
|
938
|
+
? db.query<Row, [string, number]>(
|
|
939
|
+
'SELECT * FROM ax_steering WHERE delivered = 0 AND (source IS NULL OR source != ?) ORDER BY seq DESC LIMIT ?',
|
|
940
|
+
).all(options.consumer, limit)
|
|
941
|
+
: db.query<Row, [number]>(
|
|
942
|
+
'SELECT * FROM ax_steering WHERE delivered = 0 ORDER BY seq DESC LIMIT ?',
|
|
943
|
+
).all(limit);
|
|
944
|
+
return rows
|
|
945
|
+
.map((r) => normalizeAxSteeringMessage({ ...r, createdAt: r.created_at, delivered: r.delivered === 1 }))
|
|
946
|
+
.filter((s): s is PmxAxSteeringMessage => s !== null);
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
/** Total undelivered steering for a consumer (loop-safe — excludes the consumer's own). */
|
|
950
|
+
export function countPendingAxSteeringFromDB(db: Database, consumer?: string): number {
|
|
951
|
+
const n = consumer
|
|
952
|
+
? db.query<{ n: number }, [string]>(
|
|
953
|
+
'SELECT COUNT(*) AS n FROM ax_steering WHERE delivered = 0 AND (source IS NULL OR source != ?)',
|
|
954
|
+
).get(consumer)?.n
|
|
955
|
+
: db.query<{ n: number }, []>('SELECT COUNT(*) AS n FROM ax_steering WHERE delivered = 0').get()?.n;
|
|
956
|
+
return Number(n ?? 0);
|
|
957
|
+
}
|
|
958
|
+
|
|
924
959
|
function countRows(db: Database, table: 'ax_events' | 'ax_evidence' | 'ax_steering'): number {
|
|
925
960
|
return Number(db.query<{ n: number }, []>(`SELECT COUNT(*) AS n FROM ${table}`).get()?.n ?? 0);
|
|
926
961
|
}
|
|
@@ -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.' },
|