pmx-canvas 0.1.31 → 0.1.33
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 +80 -0
- package/dist/canvas/global.css +18 -3
- package/dist/canvas/index.js +57 -57
- package/dist/json-render/index.js +97 -97
- package/dist/types/client/nodes/surface-url.d.ts +7 -0
- package/dist/types/client/state/canvas-store.d.ts +1 -0
- package/dist/types/client/state/intent-bridge.d.ts +7 -0
- package/dist/types/json-render/renderer/index.d.ts +1 -0
- package/dist/types/json-render/server.d.ts +1 -0
- package/dist/types/server/ax-context.d.ts +24 -1
- package/dist/types/server/canvas-operations.d.ts +1 -5
- package/dist/types/server/html-surface.d.ts +23 -0
- package/dist/types/server/index.d.ts +6 -0
- package/package.json +1 -1
- package/skills/pmx-canvas/SKILL.md +96 -1
- package/src/cli/agent.ts +18 -1
- package/src/client/App.tsx +3 -3
- package/src/client/canvas/CanvasNode.tsx +16 -1
- package/src/client/canvas/ExpandedNodeOverlay.tsx +12 -1
- package/src/client/nodes/ContextNode.tsx +1 -1
- package/src/client/nodes/HtmlNode.tsx +26 -1
- package/src/client/nodes/McpAppNode.tsx +35 -8
- package/src/client/nodes/StatusNode.tsx +0 -20
- package/src/client/nodes/surface-url.ts +12 -0
- package/src/client/state/canvas-store.ts +4 -0
- package/src/client/state/intent-bridge.ts +19 -0
- package/src/client/state/sse-bridge.ts +17 -0
- package/src/client/theme/global.css +18 -3
- package/src/json-render/renderer/index.tsx +31 -2
- package/src/json-render/server.ts +3 -0
- package/src/mcp/canvas-access.ts +3 -0
- package/src/mcp/server.ts +23 -1
- package/src/server/ax-context.ts +49 -1
- package/src/server/ax-interaction.ts +3 -0
- package/src/server/ax-state.ts +3 -1
- package/src/server/canvas-operations.ts +2 -2
- package/src/server/canvas-state.ts +6 -1
- package/src/server/html-surface.ts +48 -11
- package/src/server/index.ts +8 -0
- package/src/server/server.ts +81 -14
package/src/server/server.ts
CHANGED
|
@@ -48,7 +48,7 @@ import type {
|
|
|
48
48
|
ListToolsResult,
|
|
49
49
|
} from '@modelcontextprotocol/sdk/types.js';
|
|
50
50
|
import { type CanvasAnnotation, type CanvasEdge, type CanvasLayout, type CanvasNodeState, IMAGE_MIME_MAP, canvasState } from './canvas-state.js';
|
|
51
|
-
import { buildHtmlSurfaceDocument, HTML_SURFACE_SANDBOX, normalizeSurfaceTheme } from './html-surface.js';
|
|
51
|
+
import { buildAxBridge, buildAxStateBridge, buildHtmlSurfaceDocument, HTML_SURFACE_SANDBOX, normalizeSurfaceTheme } from './html-surface.js';
|
|
52
52
|
import { findCanvasExtAppNodeId as findCanvasExtAppNodeIdShared } from './ext-app-lookup.js';
|
|
53
53
|
import { normalizeExtAppToolResult } from './ext-app-tool-result.js';
|
|
54
54
|
import { getMcpAppHostSnapshot } from './mcp-app-host.js';
|
|
@@ -77,7 +77,7 @@ import {
|
|
|
77
77
|
} from './canvas-serialization.js';
|
|
78
78
|
import { buildCodeGraphSummary, formatCodeGraph } from './code-graph.js';
|
|
79
79
|
import { buildAgentContextPreamble, serializeNodeForAgentContext } from './agent-context.js';
|
|
80
|
-
import { buildCanvasAxContext } from './ax-context.js';
|
|
80
|
+
import { buildCanvasAxContext, buildCanvasAxSurfaceSnapshot } from './ax-context.js';
|
|
81
81
|
import { applyAxInteraction, resolveNodeAxCapabilities } from './ax-interaction.js';
|
|
82
82
|
import { isAxEventKind, isAxEvidenceKind } from './ax-state.js';
|
|
83
83
|
import type {
|
|
@@ -1051,16 +1051,25 @@ function rotatePrimaryWorkbenchSessionIfNeeded(): void {
|
|
|
1051
1051
|
primaryWorkbenchSessionId = `pmx-${Date.now().toString(36)}`;
|
|
1052
1052
|
}
|
|
1053
1053
|
|
|
1054
|
-
function readJson(req: Request): Promise<Record<string, unknown>> {
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1054
|
+
async function readJson(req: Request): Promise<Record<string, unknown>> {
|
|
1055
|
+
let text = '';
|
|
1056
|
+
try {
|
|
1057
|
+
text = await req.text();
|
|
1058
|
+
} catch (error) {
|
|
1059
|
+
logWorkbenchWarning('readJson', error);
|
|
1060
|
+
return {};
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
if (!text.trim()) return {};
|
|
1064
|
+
|
|
1065
|
+
try {
|
|
1066
|
+
const value = JSON.parse(text) as unknown;
|
|
1067
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
|
|
1068
|
+
return value as Record<string, unknown>;
|
|
1069
|
+
} catch (error) {
|
|
1070
|
+
logWorkbenchWarning('readJson', error);
|
|
1071
|
+
return {};
|
|
1072
|
+
}
|
|
1064
1073
|
}
|
|
1065
1074
|
|
|
1066
1075
|
function htmlEscape(value: string): string {
|
|
@@ -1432,6 +1441,7 @@ function handleNodeSurface(pathname: string, url: URL): Response {
|
|
|
1432
1441
|
if (!html) return responseText('HTML node has no content', 404);
|
|
1433
1442
|
const present = url.searchParams.get('present') === '1';
|
|
1434
1443
|
const axCaps = resolveNodeAxCapabilities(node);
|
|
1444
|
+
const axEnabled = axCaps.enabled && axCaps.allowed.length > 0;
|
|
1435
1445
|
const surfaceTitle = typeof node.data.title === 'string' && node.data.title.trim()
|
|
1436
1446
|
? node.data.title
|
|
1437
1447
|
: node.id;
|
|
@@ -1441,9 +1451,11 @@ function handleNodeSurface(pathname: string, url: URL): Response {
|
|
|
1441
1451
|
themeToken: url.searchParams.get('themeToken') ?? undefined,
|
|
1442
1452
|
presentation: present,
|
|
1443
1453
|
presentationExitToken: url.searchParams.get('presentToken') ?? undefined,
|
|
1444
|
-
axBridge:
|
|
1454
|
+
axBridge: axEnabled,
|
|
1445
1455
|
axToken: url.searchParams.get('axToken') ?? undefined,
|
|
1446
1456
|
nodeId: node.id,
|
|
1457
|
+
// Seed the read-side bridge with the current AX state (only for AX surfaces).
|
|
1458
|
+
...(axEnabled ? { axState: buildCanvasAxSurfaceSnapshot() } : {}),
|
|
1447
1459
|
});
|
|
1448
1460
|
return surfaceHtmlResponse(doc, HTML_SURFACE_SANDBOX);
|
|
1449
1461
|
}
|
|
@@ -2536,6 +2548,7 @@ async function handleJsonRenderView(url: URL): Promise<Response> {
|
|
|
2536
2548
|
process.env.PMX_CANVAS_JSON_RENDER_DEVTOOLS === '1' &&
|
|
2537
2549
|
url.searchParams.get('devtools') === '1';
|
|
2538
2550
|
const axToken = url.searchParams.get('axToken');
|
|
2551
|
+
const axEnabled = resolveNodeAxCapabilities(node).enabled;
|
|
2539
2552
|
const html = await buildJsonRenderViewerHtml({
|
|
2540
2553
|
title,
|
|
2541
2554
|
spec,
|
|
@@ -2543,6 +2556,8 @@ async function handleJsonRenderView(url: URL): Promise<Response> {
|
|
|
2543
2556
|
...(url.searchParams.get('display') === 'expanded' ? { display: 'expanded' as const } : {}),
|
|
2544
2557
|
...(devtoolsEnabled ? { devtools: true } : {}),
|
|
2545
2558
|
...(axToken ? { nodeId, axToken } : {}),
|
|
2559
|
+
// Seed the read-side AX state (only for AX-enabled nodes) so specs can bind /ax.
|
|
2560
|
+
...(axToken && axEnabled ? { axState: buildCanvasAxSurfaceSnapshot() } : {}),
|
|
2546
2561
|
});
|
|
2547
2562
|
return new Response(html, {
|
|
2548
2563
|
headers: {
|
|
@@ -2600,7 +2615,27 @@ function handleArtifactView(url: URL): Response {
|
|
|
2600
2615
|
}
|
|
2601
2616
|
|
|
2602
2617
|
if (ext === '.html' || ext === '.htm') {
|
|
2603
|
-
|
|
2618
|
+
let content = readFileSync(safePath, 'utf-8');
|
|
2619
|
+
// AX bridge for web-artifacts (same opaque-origin postMessage bridge as html
|
|
2620
|
+
// surfaces — a sandboxed artifact can't fetch the API directly). The viewer
|
|
2621
|
+
// appends axToken + axNodeId only for AX-enabled artifacts; the server still
|
|
2622
|
+
// re-validates every interaction.
|
|
2623
|
+
const axToken = url.searchParams.get('axToken');
|
|
2624
|
+
const axNodeId = url.searchParams.get('axNodeId');
|
|
2625
|
+
if (axToken && axNodeId) {
|
|
2626
|
+
const node = canvasState.getNode(axNodeId);
|
|
2627
|
+
if (node && resolveNodeAxCapabilities(node).enabled) {
|
|
2628
|
+
const safeToken = axToken.replace(/[^A-Za-z0-9_-]/g, '').slice(0, 80);
|
|
2629
|
+
// Use the canonical node.id (server-generated [a-z0-9-]) rather than the raw
|
|
2630
|
+
// query param so nothing untrusted reaches the inline bridge script.
|
|
2631
|
+
const safeNodeId = node.id.replace(/[^A-Za-z0-9_-]/g, '').slice(0, 80);
|
|
2632
|
+
const stateJson = JSON.stringify(buildCanvasAxSurfaceSnapshot()).replace(/</g, '\\u003c');
|
|
2633
|
+
const bridge = `${buildAxBridge(safeToken, safeNodeId)}${buildAxStateBridge(safeToken, stateJson)}`;
|
|
2634
|
+
content = content.includes('</head>')
|
|
2635
|
+
? content.replace('</head>', `${bridge}</head>`)
|
|
2636
|
+
: `${bridge}${content}`;
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2604
2639
|
return new Response(content, {
|
|
2605
2640
|
headers: {
|
|
2606
2641
|
'Content-Type': 'text/html; charset=utf-8',
|
|
@@ -3827,6 +3862,30 @@ function handleGetAxContext(): Response {
|
|
|
3827
3862
|
return responseJson(buildCanvasAxContext());
|
|
3828
3863
|
}
|
|
3829
3864
|
|
|
3865
|
+
// Compact AX state for surfaces (the same shape seeded into AX-enabled iframes).
|
|
3866
|
+
// The client fetches this and pushes it to surfaces over the ax-update channel.
|
|
3867
|
+
function handleGetAxSurfaceSnapshot(): Response {
|
|
3868
|
+
return responseJson(buildCanvasAxSurfaceSnapshot());
|
|
3869
|
+
}
|
|
3870
|
+
|
|
3871
|
+
// Open a node's surface in the user's real system browser (for hosts whose
|
|
3872
|
+
// embedded browser makes window.open('_blank') feel in-place, e.g. Codex).
|
|
3873
|
+
// Accepts ONLY { nodeId } and opens this server's own surface URL — never an
|
|
3874
|
+
// arbitrary URL — so it can't be used to launch external sites (no SSRF). Honors
|
|
3875
|
+
// the PMX_CANVAS_DISABLE_BROWSER_OPEN kill switch via openUrlInExternalBrowser.
|
|
3876
|
+
async function handleOpenExternalSurface(req: Request): Promise<Response> {
|
|
3877
|
+
const body = await readJson(req);
|
|
3878
|
+
const nodeId = typeof body.nodeId === 'string' ? body.nodeId : '';
|
|
3879
|
+
if (!nodeId) return responseJson({ ok: false, error: 'nodeId is required.' }, 400);
|
|
3880
|
+
const node = canvasState.getNode(nodeId);
|
|
3881
|
+
if (!node) return responseJson({ ok: false, error: `Node "${nodeId}" not found.` }, 404);
|
|
3882
|
+
const port = getCanvasServerPort();
|
|
3883
|
+
if (!port) return responseJson({ ok: false, opened: false, error: 'Server port unavailable.' }, 503);
|
|
3884
|
+
const surfacePath = `/api/canvas/surface/${encodeURIComponent(nodeId)}`;
|
|
3885
|
+
const opened = openUrlInExternalBrowser(`http://localhost:${port}${surfacePath}`);
|
|
3886
|
+
return responseJson({ ok: true, opened, url: surfacePath });
|
|
3887
|
+
}
|
|
3888
|
+
|
|
3830
3889
|
async function handleAxInteraction(req: Request): Promise<Response> {
|
|
3831
3890
|
const body = await readJson(req);
|
|
3832
3891
|
const { result, events } = applyAxInteraction(canvasState, body, normalizeAxSource(body.source, 'api'));
|
|
@@ -5304,6 +5363,14 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
5304
5363
|
return handleGetAxContext();
|
|
5305
5364
|
}
|
|
5306
5365
|
|
|
5366
|
+
if (url.pathname === '/api/canvas/ax/surface-snapshot' && req.method === 'GET') {
|
|
5367
|
+
return handleGetAxSurfaceSnapshot();
|
|
5368
|
+
}
|
|
5369
|
+
|
|
5370
|
+
if (url.pathname === '/api/canvas/open-external' && req.method === 'POST') {
|
|
5371
|
+
return handleOpenExternalSurface(req);
|
|
5372
|
+
}
|
|
5373
|
+
|
|
5307
5374
|
if (url.pathname === '/api/canvas/ax/focus' && req.method === 'POST') {
|
|
5308
5375
|
return handleAxFocusUpdate(req);
|
|
5309
5376
|
}
|