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.
Files changed (40) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/dist/canvas/global.css +18 -3
  3. package/dist/canvas/index.js +57 -57
  4. package/dist/json-render/index.js +97 -97
  5. package/dist/types/client/nodes/surface-url.d.ts +7 -0
  6. package/dist/types/client/state/canvas-store.d.ts +1 -0
  7. package/dist/types/client/state/intent-bridge.d.ts +7 -0
  8. package/dist/types/json-render/renderer/index.d.ts +1 -0
  9. package/dist/types/json-render/server.d.ts +1 -0
  10. package/dist/types/server/ax-context.d.ts +24 -1
  11. package/dist/types/server/canvas-operations.d.ts +1 -5
  12. package/dist/types/server/html-surface.d.ts +23 -0
  13. package/dist/types/server/index.d.ts +6 -0
  14. package/package.json +1 -1
  15. package/skills/pmx-canvas/SKILL.md +96 -1
  16. package/src/cli/agent.ts +18 -1
  17. package/src/client/App.tsx +3 -3
  18. package/src/client/canvas/CanvasNode.tsx +16 -1
  19. package/src/client/canvas/ExpandedNodeOverlay.tsx +12 -1
  20. package/src/client/nodes/ContextNode.tsx +1 -1
  21. package/src/client/nodes/HtmlNode.tsx +26 -1
  22. package/src/client/nodes/McpAppNode.tsx +35 -8
  23. package/src/client/nodes/StatusNode.tsx +0 -20
  24. package/src/client/nodes/surface-url.ts +12 -0
  25. package/src/client/state/canvas-store.ts +4 -0
  26. package/src/client/state/intent-bridge.ts +19 -0
  27. package/src/client/state/sse-bridge.ts +17 -0
  28. package/src/client/theme/global.css +18 -3
  29. package/src/json-render/renderer/index.tsx +31 -2
  30. package/src/json-render/server.ts +3 -0
  31. package/src/mcp/canvas-access.ts +3 -0
  32. package/src/mcp/server.ts +23 -1
  33. package/src/server/ax-context.ts +49 -1
  34. package/src/server/ax-interaction.ts +3 -0
  35. package/src/server/ax-state.ts +3 -1
  36. package/src/server/canvas-operations.ts +2 -2
  37. package/src/server/canvas-state.ts +6 -1
  38. package/src/server/html-surface.ts +48 -11
  39. package/src/server/index.ts +8 -0
  40. package/src/server/server.ts +81 -14
@@ -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
- return req.json()
1056
- .then((value) => {
1057
- if (!value || typeof value !== 'object') return {};
1058
- return value as Record<string, unknown>;
1059
- })
1060
- .catch((error) => {
1061
- logWorkbenchWarning('readJson', error);
1062
- return {};
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: axCaps.enabled && axCaps.allowed.length > 0,
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
- const content = readFileSync(safePath, 'utf-8');
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
  }