pmx-canvas 0.1.22 → 0.1.24

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 (53) hide show
  1. package/.github/extensions/pmx-canvas/extension.mjs +591 -0
  2. package/CHANGELOG.md +140 -0
  3. package/Readme.md +40 -8
  4. package/dist/canvas/global.css +36 -3
  5. package/dist/canvas/index.js +54 -54
  6. package/dist/types/client/nodes/ExtAppFrame.d.ts +1 -0
  7. package/dist/types/client/nodes/iframe-document-url.d.ts +8 -0
  8. package/dist/types/client/state/intent-bridge.d.ts +4 -0
  9. package/dist/types/client/types.d.ts +1 -0
  10. package/dist/types/json-render/catalog.d.ts +1 -1
  11. package/dist/types/mcp/canvas-access.d.ts +9 -0
  12. package/dist/types/server/ax-context.d.ts +3 -0
  13. package/dist/types/server/ax-state.d.ts +43 -0
  14. package/dist/types/server/canvas-db.d.ts +38 -0
  15. package/dist/types/server/canvas-state.d.ts +36 -16
  16. package/dist/types/server/index.d.ts +6 -0
  17. package/dist/types/server/mutation-history.d.ts +1 -1
  18. package/docs/cli.md +13 -0
  19. package/docs/http-api.md +24 -0
  20. package/docs/mcp.md +20 -2
  21. package/docs/plans/plan-004-pmx-ax-primitives.md +463 -0
  22. package/docs/screenshot.png +0 -0
  23. package/docs/sdk.md +5 -0
  24. package/package.json +3 -2
  25. package/skills/pmx-canvas/SKILL.md +22 -4
  26. package/skills/pmx-canvas/references/codex-app-adapter.md +107 -0
  27. package/skills/pmx-canvas/references/github-copilot-app-adapter.md +111 -0
  28. package/src/cli/agent.ts +34 -0
  29. package/src/cli/index.ts +2 -1
  30. package/src/client/App.tsx +2 -0
  31. package/src/client/canvas/CanvasNode.tsx +7 -0
  32. package/src/client/canvas/CommandPalette.tsx +2 -1
  33. package/src/client/canvas/use-node-drag.ts +29 -7
  34. package/src/client/canvas/use-node-resize.ts +27 -7
  35. package/src/client/nodes/ExtAppFrame.tsx +51 -10
  36. package/src/client/nodes/HtmlNode.tsx +5 -2
  37. package/src/client/nodes/iframe-document-url.ts +58 -0
  38. package/src/client/state/intent-bridge.ts +8 -0
  39. package/src/client/state/sse-bridge.ts +2 -2
  40. package/src/client/theme/global.css +36 -3
  41. package/src/client/types.ts +1 -0
  42. package/src/mcp/canvas-access.ts +38 -0
  43. package/src/mcp/server.ts +113 -4
  44. package/src/server/ax-context.ts +38 -0
  45. package/src/server/ax-state.ts +130 -0
  46. package/src/server/canvas-db.ts +745 -0
  47. package/src/server/canvas-operations.ts +80 -1
  48. package/src/server/canvas-schema.ts +3 -3
  49. package/src/server/canvas-state.ts +390 -50
  50. package/src/server/canvas-validation.ts +6 -0
  51. package/src/server/index.ts +18 -0
  52. package/src/server/mutation-history.ts +1 -0
  53. package/src/server/server.ts +197 -11
@@ -40,6 +40,10 @@ function overlaps(a: CanvasNodeState, b: CanvasNodeState): boolean {
40
40
  );
41
41
  }
42
42
 
43
+ function participatesInCanvasCollisionValidation(node: CanvasNodeState): boolean {
44
+ return node.dockPosition === null;
45
+ }
46
+
43
47
  function fullyContains(group: CanvasNodeState, child: CanvasNodeState): boolean {
44
48
  return (
45
49
  child.position.x >= group.position.x &&
@@ -81,8 +85,10 @@ export function validateCanvasLayout(layout: CanvasLayout): CanvasValidationResu
81
85
 
82
86
  for (let i = 0; i < layout.nodes.length; i++) {
83
87
  const a = layout.nodes[i]!;
88
+ if (!participatesInCanvasCollisionValidation(a)) continue;
84
89
  for (let j = i + 1; j < layout.nodes.length; j++) {
85
90
  const b = layout.nodes[j]!;
91
+ if (!participatesInCanvasCollisionValidation(b)) continue;
86
92
  if (!overlaps(a, b)) continue;
87
93
 
88
94
  if (isGroupChildPair(a, b)) {
@@ -1,6 +1,8 @@
1
1
  import { EventEmitter } from 'node:events';
2
2
  import { canvasState, IMAGE_MIME_MAP } from './canvas-state.js';
3
3
  import type { CanvasAnnotation, CanvasNodeState, CanvasEdge, CanvasLayout, ViewportState } from './canvas-state.js';
4
+ import { buildCanvasAxContext } from './ax-context.js';
5
+ import type { PmxAxContext, PmxAxFocusState, PmxAxSource, PmxAxState } from './ax-state.js';
4
6
  import { findCanvasExtAppNodeId } from './ext-app-lookup.js';
5
7
  import { onFileNodeChanged } from './file-watcher.js';
6
8
  import { findOpenCanvasPosition, computeGroupBounds } from './placement.js';
@@ -402,7 +404,9 @@ export class PmxCanvas extends EventEmitter {
402
404
  y: node.position.y - 100,
403
405
  });
404
406
  }
407
+ const focus = canvasState.setAxFocus([id], { source: 'sdk', recordHistory: false });
405
408
  emitPrimaryWorkbenchEvent('canvas-focus-node', { nodeId: id, noPan });
409
+ emitPrimaryWorkbenchEvent('ax-state-changed', { focus });
406
410
  if (!noPan) {
407
411
  emitPrimaryWorkbenchEvent('canvas-viewport-update', { viewport: canvasState.viewport });
408
412
  }
@@ -410,6 +414,20 @@ export class PmxCanvas extends EventEmitter {
410
414
  return { focused: id, panned: !noPan };
411
415
  }
412
416
 
417
+ getAxState(): PmxAxState {
418
+ return canvasState.getAxState();
419
+ }
420
+
421
+ getAxContext(): PmxAxContext {
422
+ return buildCanvasAxContext();
423
+ }
424
+
425
+ setAxFocus(nodeIds: string[], options?: { source?: PmxAxSource }): PmxAxFocusState {
426
+ const focus = canvasState.setAxFocus(nodeIds, { source: options?.source ?? 'sdk' });
427
+ emitPrimaryWorkbenchEvent('ax-state-changed', { focus });
428
+ return focus;
429
+ }
430
+
413
431
  fitView(options?: {
414
432
  width?: number;
415
433
  height?: number;
@@ -28,6 +28,7 @@ export type MutationOp =
28
28
  | 'arrange'
29
29
  | 'restoreSnapshot'
30
30
  | 'setPins'
31
+ | 'setAxFocus'
31
32
  | 'batch'
32
33
  | 'viewport'
33
34
  | 'groupNodes'
@@ -35,6 +35,7 @@
35
35
  */
36
36
 
37
37
  import { spawnSync } from 'node:child_process';
38
+ import { randomUUID } from 'node:crypto';
38
39
  import { existsSync, readFileSync, statSync, writeFileSync, appendFileSync } from 'node:fs';
39
40
  import { readFile } from 'node:fs/promises';
40
41
  import { basename, extname, join, relative, resolve } from 'node:path';
@@ -75,6 +76,9 @@ import {
75
76
  } from './canvas-serialization.js';
76
77
  import { buildCodeGraphSummary, formatCodeGraph } from './code-graph.js';
77
78
  import { buildAgentContextPreamble, serializeNodeForAgentContext } from './agent-context.js';
79
+ import { buildCanvasAxContext } from './ax-context.js';
80
+ import type { PmxAxSource } from './ax-state.js';
81
+ import { normalizeCanvasTheme, type CanvasTheme } from './canvas-db.js';
78
82
  import { validateLocalImageFile } from './image-source.js';
79
83
  import {
80
84
  addCanvasNode,
@@ -145,9 +149,7 @@ let nextWorkbenchSubscriberId = 1;
145
149
  const workbenchSubscribers = new Map<number, ReadableStreamDefaultController<Uint8Array>>();
146
150
  const textEncoder = new TextEncoder();
147
151
  let primaryWorkbenchAutoOpenEnabled = true;
148
- const canvasThemeSetting = (['dark', 'light', 'high-contrast'].includes(process.env.PMX_CANVAS_THEME ?? '')
149
- ? process.env.PMX_CANVAS_THEME!
150
- : 'dark');
152
+ const initialCanvasThemeSetting = normalizeCanvasTheme(process.env.PMX_CANVAS_THEME);
151
153
  let lastWorkbenchContextCardsEnvelope: Record<string, unknown> | null = null;
152
154
 
153
155
  function normalizeGraphViewerSpec(
@@ -1038,6 +1040,24 @@ function normalizeMarkdownExternalUrls(markdown: string): string {
1038
1040
 
1039
1041
  // ── Canvas SPA HTML ────────────────────────────────────────────
1040
1042
 
1043
+ const CANVAS_ASSET_VERSION = Date.now().toString(36);
1044
+ const MAX_FRAME_DOCUMENTS = 128;
1045
+ const MAX_FRAME_DOCUMENT_BYTES = 5 * 1024 * 1024;
1046
+ const DEFAULT_FRAME_DOCUMENT_SANDBOX = 'allow-scripts';
1047
+ const SAFE_FRAME_DOCUMENT_SANDBOX_TOKENS = new Set([
1048
+ 'allow-downloads',
1049
+ 'allow-forms',
1050
+ 'allow-modals',
1051
+ 'allow-orientation-lock',
1052
+ 'allow-pointer-lock',
1053
+ 'allow-popups',
1054
+ 'allow-popups-to-escape-sandbox',
1055
+ 'allow-presentation',
1056
+ 'allow-scripts',
1057
+ 'allow-storage-access-by-user-activation',
1058
+ ]);
1059
+ const frameDocuments = new Map<string, { html: string; sandbox: string }>();
1060
+
1041
1061
  function canvasSpaHtml(): string {
1042
1062
  return `<!doctype html>
1043
1063
  <html lang="en">
@@ -1106,7 +1126,7 @@ function canvasSpaHtml(): string {
1106
1126
  color: #eef4ff;
1107
1127
  }
1108
1128
  </style>
1109
- <link rel="stylesheet" href="/canvas/global.css" />
1129
+ <link rel="stylesheet" href="/canvas/global.css?v=${CANVAS_ASSET_VERSION}" />
1110
1130
  </head>
1111
1131
  <body>
1112
1132
  <div id="canvasBootstrap">
@@ -1143,7 +1163,7 @@ function canvasSpaHtml(): string {
1143
1163
  }, 4000);
1144
1164
  })();
1145
1165
  </script>
1146
- <script type="module" src="/canvas/index.js"></script>
1166
+ <script type="module" src="/canvas/index.js?v=${CANVAS_ASSET_VERSION}"></script>
1147
1167
  </body>
1148
1168
  </html>`;
1149
1169
  }
@@ -1229,6 +1249,61 @@ function serveCanvasFavicon(): Response {
1229
1249
 
1230
1250
  // ── Canvas REST handlers ──────────────────────────────────────
1231
1251
 
1252
+ function normalizeFrameDocumentSandbox(value: unknown): string | null {
1253
+ if (value === undefined || value === null) return DEFAULT_FRAME_DOCUMENT_SANDBOX;
1254
+ if (typeof value !== 'string') return null;
1255
+ const tokens = value.trim().split(/\s+/).filter(Boolean);
1256
+ if (tokens.length === 0) return DEFAULT_FRAME_DOCUMENT_SANDBOX;
1257
+ const uniqueTokens: string[] = [];
1258
+ for (const token of tokens) {
1259
+ if (!SAFE_FRAME_DOCUMENT_SANDBOX_TOKENS.has(token)) return null;
1260
+ if (!uniqueTokens.includes(token)) uniqueTokens.push(token);
1261
+ }
1262
+ return uniqueTokens.join(' ');
1263
+ }
1264
+
1265
+ function addFrameDocument(html: string, sandbox: string): string {
1266
+ const id = randomUUID();
1267
+ frameDocuments.set(id, { html, sandbox });
1268
+ while (frameDocuments.size > MAX_FRAME_DOCUMENTS) {
1269
+ const firstKey = frameDocuments.keys().next().value;
1270
+ if (typeof firstKey !== 'string') break;
1271
+ frameDocuments.delete(firstKey);
1272
+ }
1273
+ return `/api/canvas/frame-documents/${id}`;
1274
+ }
1275
+
1276
+ async function handleCreateFrameDocument(req: Request): Promise<Response> {
1277
+ const body = await readJson(req);
1278
+ const html = body.html;
1279
+ if (typeof html !== 'string' || !html) {
1280
+ return responseJson({ ok: false, error: 'Frame document requires non-empty html.' }, 400);
1281
+ }
1282
+ if (new TextEncoder().encode(html).byteLength > MAX_FRAME_DOCUMENT_BYTES) {
1283
+ return responseJson({ ok: false, error: 'Frame document is too large.' }, 413);
1284
+ }
1285
+ const sandbox = normalizeFrameDocumentSandbox(body.sandbox);
1286
+ if (!sandbox) {
1287
+ return responseJson({ ok: false, error: 'Frame document sandbox contains unsupported tokens.' }, 400);
1288
+ }
1289
+ return responseJson({ ok: true, url: addFrameDocument(html, sandbox) });
1290
+ }
1291
+
1292
+ function handleFrameDocument(pathname: string): Response {
1293
+ const id = decodeURIComponent(pathname.slice('/api/canvas/frame-documents/'.length));
1294
+ const document = frameDocuments.get(id);
1295
+ if (!document) return responseText('Frame document not found.', 404);
1296
+ return new Response(document.html, {
1297
+ headers: {
1298
+ 'Content-Type': 'text/html; charset=utf-8',
1299
+ 'Cache-Control': 'no-store',
1300
+ 'Content-Security-Policy': `sandbox ${document.sandbox}`,
1301
+ 'Referrer-Policy': 'no-referrer',
1302
+ 'X-Content-Type-Options': 'nosniff',
1303
+ },
1304
+ });
1305
+ }
1306
+
1232
1307
  async function handleCanvasUpdate(req: Request): Promise<Response> {
1233
1308
  const body = await readJson(req);
1234
1309
  const updates = Array.isArray(body.updates) ? body.updates : [];
@@ -1436,7 +1511,8 @@ async function createCanvasWebpageNode(body: Record<string, unknown>): Promise<R
1436
1511
 
1437
1512
  async function handleCanvasAddNode(req: Request): Promise<Response> {
1438
1513
  const body = await readJson(req);
1439
- const type = (body.type as string) || 'markdown';
1514
+ const queryType = new URL(req.url).searchParams.get('type');
1515
+ const type = typeof body.type === 'string' ? body.type : queryType || 'markdown';
1440
1516
 
1441
1517
  if (!VALID_NODE_TYPES.has(type)) {
1442
1518
  if (type === 'json-render') {
@@ -1816,10 +1892,16 @@ async function handleCanvasFocus(req: Request): Promise<Response> {
1816
1892
  const maxZ = canvasState.getLayout().nodes.reduce((max, layoutNode) => Math.max(max, layoutNode.zIndex), 0);
1817
1893
  canvasState.updateNode(nodeId, { zIndex: maxZ + 1 });
1818
1894
  }
1895
+ const focus = canvasState.setAxFocus([nodeId], { source: 'api', recordHistory: false });
1896
+ broadcastWorkbenchEvent('ax-state-changed', {
1897
+ focus,
1898
+ sessionId: primaryWorkbenchSessionId,
1899
+ timestamp: new Date().toISOString(),
1900
+ });
1819
1901
  emitPrimaryWorkbenchEvent('canvas-focus-node', { nodeId, noPan });
1820
1902
  if (!noPan) emitPrimaryWorkbenchEvent('canvas-viewport-update', { viewport: canvasState.viewport });
1821
1903
  emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
1822
- return responseJson({ ok: true, focused: nodeId, panned: !noPan });
1904
+ return responseJson({ ok: true, focused: nodeId, panned: !noPan, axFocus: focus });
1823
1905
  }
1824
1906
 
1825
1907
  async function handleCanvasFit(req: Request): Promise<Response> {
@@ -2104,6 +2186,18 @@ function handleCanvasValidate(): Response {
2104
2186
  return responseJson(validateCanvasLayout(canvasState.getLayout()));
2105
2187
  }
2106
2188
 
2189
+ async function handleCanvasThemeUpdate(req: Request): Promise<Response> {
2190
+ const body = await readJson(req);
2191
+ const theme = normalizeCanvasTheme(body.theme, canvasState.theme);
2192
+ const next = canvasState.setTheme(theme);
2193
+ broadcastWorkbenchEvent('theme-changed', {
2194
+ theme: next,
2195
+ sessionId: primaryWorkbenchSessionId,
2196
+ timestamp: new Date().toISOString(),
2197
+ });
2198
+ return responseJson({ ok: true, theme: next });
2199
+ }
2200
+
2107
2201
  async function handleJsonRenderView(url: URL): Promise<Response> {
2108
2202
  const nodeId = url.searchParams.get('nodeId') ?? '';
2109
2203
  if (!nodeId) return responseText('Missing nodeId', 400);
@@ -2972,7 +3066,7 @@ function handleWorkbenchEvents(req: Request): Response {
2972
3066
  requestedSessionId: requestedSessionId || null,
2973
3067
  continuity,
2974
3068
  path: primaryWorkbenchPath,
2975
- theme: canvasThemeSetting,
3069
+ theme: canvasState.theme,
2976
3070
  timestamp: new Date().toISOString(),
2977
3071
  }),
2978
3072
  );
@@ -3390,6 +3484,63 @@ function handleGetPinnedContext(): Response {
3390
3484
  return responseJson({ preamble, nodeIds: pinnedIds, count: pinnedIds.length, nodes });
3391
3485
  }
3392
3486
 
3487
+ function normalizeAxNodeIds(value: unknown): string[] {
3488
+ if (!Array.isArray(value)) return [];
3489
+ return value.filter((id): id is string => typeof id === 'string');
3490
+ }
3491
+
3492
+ function normalizeAxSource(value: unknown, fallback: PmxAxSource): PmxAxSource {
3493
+ return value === 'agent' ||
3494
+ value === 'api' ||
3495
+ value === 'browser' ||
3496
+ value === 'cli' ||
3497
+ value === 'codex' ||
3498
+ value === 'copilot' ||
3499
+ value === 'mcp' ||
3500
+ value === 'sdk' ||
3501
+ value === 'system'
3502
+ ? value
3503
+ : fallback;
3504
+ }
3505
+
3506
+ function handleGetAxState(): Response {
3507
+ return responseJson({ ok: true, state: canvasState.getAxState() });
3508
+ }
3509
+
3510
+ function handleGetAxContext(): Response {
3511
+ return responseJson(buildCanvasAxContext());
3512
+ }
3513
+
3514
+ async function handleAxFocusUpdate(req: Request): Promise<Response> {
3515
+ const body = await readJson(req);
3516
+ const nodeIds = normalizeAxNodeIds(body.nodeIds);
3517
+ const source = normalizeAxSource(body.source, 'api');
3518
+ const focus = canvasState.setAxFocus(nodeIds, { source });
3519
+ broadcastWorkbenchEvent('ax-state-changed', {
3520
+ focus,
3521
+ sessionId: primaryWorkbenchSessionId,
3522
+ timestamp: new Date().toISOString(),
3523
+ });
3524
+ return responseJson({ ok: true, focus });
3525
+ }
3526
+
3527
+ async function handleAxStatePatch(req: Request): Promise<Response> {
3528
+ const body = await readJson(req);
3529
+ if (!body.focus || typeof body.focus !== 'object' || Array.isArray(body.focus)) {
3530
+ return responseJson({ ok: false, error: 'PATCH /api/canvas/ax currently requires a focus object.' }, 400);
3531
+ }
3532
+ const focusInput = body.focus as Record<string, unknown>;
3533
+ const focus = canvasState.setAxFocus(normalizeAxNodeIds(focusInput.nodeIds), {
3534
+ source: normalizeAxSource(focusInput.source, 'api'),
3535
+ });
3536
+ broadcastWorkbenchEvent('ax-state-changed', {
3537
+ focus,
3538
+ sessionId: primaryWorkbenchSessionId,
3539
+ timestamp: new Date().toISOString(),
3540
+ });
3541
+ return responseJson({ ok: true, state: canvasState.getAxState() });
3542
+ }
3543
+
3393
3544
  // ── Port resolution ───────────────────────────────────────────
3394
3545
 
3395
3546
  function buildPortCandidates(preferredPort: number): number[] {
@@ -4074,12 +4225,13 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
4074
4225
 
4075
4226
  // ── Canvas persistence: set workspace root and load saved state ──
4076
4227
  canvasState.setWorkspaceRoot(activeWorkspaceRoot);
4228
+ canvasState.setTheme(initialCanvasThemeSetting as CanvasTheme);
4077
4229
  const loaded = canvasState.loadFromDisk({ clearExisting: true });
4078
4230
  setCanvasLayoutUpdateEmitter(() => {
4079
4231
  emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
4080
4232
  });
4081
4233
  if (loaded) {
4082
- console.log(' Canvas state restored from .pmx-canvas/state.json');
4234
+ console.log(' Canvas state restored from .pmx-canvas/canvas.db');
4083
4235
  primeCanvasRuntimeBackends({ forceRehydrateExtApps: true });
4084
4236
  void syncCanvasRuntimeBackends({ forceRehydrateExtApps: true, alreadyPrimed: true }).finally(() => {
4085
4237
  emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
@@ -4089,7 +4241,9 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
4089
4241
  rotatePrimaryWorkbenchSessionIfNeeded();
4090
4242
 
4091
4243
  const preferredPort = options.port ?? Number(process.env.PMX_WEB_CANVAS_PORT ?? DEFAULT_PORT);
4092
- const portCandidates = options.allowPortFallback === false
4244
+ const portCandidates = options.port === 0
4245
+ ? [0]
4246
+ : options.allowPortFallback === false
4093
4247
  ? [preferredPort > 0 ? Math.floor(preferredPort) : DEFAULT_PORT]
4094
4248
  : buildPortCandidates(preferredPort);
4095
4249
 
@@ -4118,6 +4272,14 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
4118
4272
  return handleJsonRenderView(url);
4119
4273
  }
4120
4274
 
4275
+ if (url.pathname === '/api/canvas/frame-documents' && req.method === 'POST') {
4276
+ return handleCreateFrameDocument(req);
4277
+ }
4278
+
4279
+ if (url.pathname.startsWith('/api/canvas/frame-documents/') && req.method === 'GET') {
4280
+ return handleFrameDocument(url.pathname);
4281
+ }
4282
+
4121
4283
  if (url.pathname === '/' || url.pathname === '/workbench' || url.pathname === '/artifact') {
4122
4284
  return new Response(canvasSpaHtml(), {
4123
4285
  headers: {
@@ -4191,6 +4353,14 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
4191
4353
  return responseJson(buildCanvasSummary());
4192
4354
  }
4193
4355
 
4356
+ if (url.pathname === '/api/canvas/theme' && req.method === 'GET') {
4357
+ return responseJson({ ok: true, theme: canvasState.theme });
4358
+ }
4359
+
4360
+ if (url.pathname === '/api/canvas/theme' && req.method === 'POST') {
4361
+ return handleCanvasThemeUpdate(req);
4362
+ }
4363
+
4194
4364
  if (url.pathname === '/api/canvas/update' && req.method === 'POST') {
4195
4365
  return handleCanvasUpdate(req);
4196
4366
  }
@@ -4333,6 +4503,22 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
4333
4503
  return handleGetPinnedContext();
4334
4504
  }
4335
4505
 
4506
+ if (url.pathname === '/api/canvas/ax' && req.method === 'GET') {
4507
+ return handleGetAxState();
4508
+ }
4509
+
4510
+ if (url.pathname === '/api/canvas/ax' && req.method === 'PATCH') {
4511
+ return handleAxStatePatch(req);
4512
+ }
4513
+
4514
+ if (url.pathname === '/api/canvas/ax/context' && req.method === 'GET') {
4515
+ return handleGetAxContext();
4516
+ }
4517
+
4518
+ if (url.pathname === '/api/canvas/ax/focus' && req.method === 'POST') {
4519
+ return handleAxFocusUpdate(req);
4520
+ }
4521
+
4336
4522
  // Spatial context API
4337
4523
  if (url.pathname === '/api/canvas/spatial-context' && req.method === 'GET') {
4338
4524
  const layout = canvasState.getLayout();
@@ -4482,7 +4668,7 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
4482
4668
  }
4483
4669
 
4484
4670
  export function stopCanvasServer(): void {
4485
- canvasState.flushToDisk();
4671
+ canvasState.close();
4486
4672
  closeAllMcpAppSessions();
4487
4673
  setCanvasLayoutUpdateEmitter(null);
4488
4674
  void closeCanvasAutomationWebViewInternal().catch((error) => {