pmx-canvas 0.3.0 → 0.3.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.
Files changed (156) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/dist/canvas/index.js +2 -2
  3. package/dist/types/cli/daemon.d.ts +74 -0
  4. package/dist/types/cli/watch.d.ts +2 -2
  5. package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
  6. package/dist/types/client/canvas/CommandPalette.d.ts +1 -1
  7. package/dist/types/client/canvas/Minimap.d.ts +1 -1
  8. package/dist/types/client/nodes/FileNode.d.ts +1 -1
  9. package/dist/types/client/nodes/ImageNode.d.ts +1 -1
  10. package/dist/types/client/nodes/InlineFormatBar.d.ts +1 -1
  11. package/dist/types/client/nodes/MarkdownNode.d.ts +1 -1
  12. package/dist/types/client/nodes/PromptNode.d.ts +1 -1
  13. package/dist/types/client/nodes/ResponseNode.d.ts +1 -1
  14. package/dist/types/server/canvas-schema.d.ts +1 -1
  15. package/dist/types/server/html-primitives.d.ts +1 -1
  16. package/dist/types/server/index.d.ts +4 -4
  17. package/dist/types/server/operations/index.d.ts +1 -1
  18. package/dist/types/server/operations/ops/ax-read.d.ts +2 -0
  19. package/dist/types/server/operations/ops/canvas-wire.d.ts +2 -0
  20. package/dist/types/server/operations/ops/ext-app.d.ts +2 -0
  21. package/docs/http-api.md +28 -0
  22. package/docs/plans/plan-009-tech-debt-backlog.md +5 -5
  23. package/docs/tech-debt-assessment-2026-07.md +2 -2
  24. package/package.json +5 -2
  25. package/skills/pmx-canvas/SKILL.md +3 -1
  26. package/skills/pmx-canvas/references/full-reference.md +10 -3
  27. package/src/cli/agent.ts +1861 -1548
  28. package/src/cli/daemon.ts +460 -0
  29. package/src/cli/index.ts +63 -326
  30. package/src/cli/watch.ts +2 -10
  31. package/src/client/App.tsx +48 -46
  32. package/src/client/canvas/AttentionHistory.tsx +11 -1
  33. package/src/client/canvas/CanvasNode.tsx +41 -29
  34. package/src/client/canvas/CanvasViewport.tsx +101 -66
  35. package/src/client/canvas/CommandPalette.tsx +61 -27
  36. package/src/client/canvas/ContextMenu.tsx +13 -20
  37. package/src/client/canvas/ContextPinBar.tsx +1 -5
  38. package/src/client/canvas/ContextPinHud.tsx +1 -6
  39. package/src/client/canvas/DockedNode.tsx +4 -4
  40. package/src/client/canvas/EdgeLayer.tsx +37 -36
  41. package/src/client/canvas/ExpandedNodeOverlay.tsx +69 -45
  42. package/src/client/canvas/FocusFieldLayer.tsx +20 -22
  43. package/src/client/canvas/IntentLayer.tsx +31 -14
  44. package/src/client/canvas/Minimap.tsx +11 -16
  45. package/src/client/canvas/SelectionBar.tsx +4 -11
  46. package/src/client/canvas/ShortcutOverlay.tsx +3 -1
  47. package/src/client/canvas/SnapshotPanel.tsx +77 -95
  48. package/src/client/canvas/auto-fit.ts +15 -14
  49. package/src/client/canvas/snap-guides.ts +12 -12
  50. package/src/client/canvas/use-node-resize.ts +1 -5
  51. package/src/client/canvas/use-pan-zoom.ts +25 -26
  52. package/src/client/ext-app/bridge.ts +3 -12
  53. package/src/client/icons.tsx +63 -20
  54. package/src/client/nodes/ContextNode.tsx +14 -25
  55. package/src/client/nodes/ExtAppFrame.tsx +60 -39
  56. package/src/client/nodes/FileNode.tsx +74 -62
  57. package/src/client/nodes/GroupNode.tsx +4 -6
  58. package/src/client/nodes/HtmlNode.tsx +76 -46
  59. package/src/client/nodes/ImageNode.tsx +18 -27
  60. package/src/client/nodes/InlineFormatBar.tsx +4 -21
  61. package/src/client/nodes/InlineMarkdownEditor.tsx +0 -1
  62. package/src/client/nodes/LedgerNode.tsx +10 -4
  63. package/src/client/nodes/MarkdownNode.tsx +3 -10
  64. package/src/client/nodes/McpAppNode.tsx +26 -22
  65. package/src/client/nodes/MdFormatBar.tsx +10 -7
  66. package/src/client/nodes/PromptNode.tsx +23 -51
  67. package/src/client/nodes/ResponseNode.tsx +3 -13
  68. package/src/client/nodes/StatusNode.tsx +5 -9
  69. package/src/client/nodes/StatusSummary.tsx +2 -8
  70. package/src/client/nodes/WebpageNode.tsx +20 -14
  71. package/src/client/nodes/iframe-document-url.ts +25 -16
  72. package/src/client/nodes/image-warnings.ts +1 -7
  73. package/src/client/nodes/md-format.ts +20 -5
  74. package/src/client/state/attention-bridge.ts +4 -9
  75. package/src/client/state/attention-store.ts +1 -7
  76. package/src/client/state/canvas-store.ts +52 -36
  77. package/src/client/state/intent-bridge.ts +176 -112
  78. package/src/client/state/intent-store.ts +4 -1
  79. package/src/client/state/sse-bridge.ts +53 -70
  80. package/src/json-render/catalog.ts +12 -16
  81. package/src/json-render/charts/components.tsx +16 -20
  82. package/src/json-render/charts/extra-components.tsx +8 -16
  83. package/src/json-render/charts/extra-definitions.ts +1 -2
  84. package/src/json-render/charts/tufte-components.tsx +37 -20
  85. package/src/json-render/charts/tufte-definitions.ts +8 -2
  86. package/src/json-render/renderer/index.tsx +42 -22
  87. package/src/json-render/schema.ts +6 -3
  88. package/src/json-render/server.ts +33 -39
  89. package/src/mcp/canvas-access.ts +35 -21
  90. package/src/mcp/server.ts +132 -70
  91. package/src/server/agent-context.ts +63 -36
  92. package/src/server/ax-context.ts +7 -5
  93. package/src/server/ax-interaction.ts +176 -43
  94. package/src/server/ax-state-manager.ts +182 -39
  95. package/src/server/ax-state.ts +142 -47
  96. package/src/server/canvas-db.ts +213 -95
  97. package/src/server/canvas-operations.ts +177 -120
  98. package/src/server/canvas-provenance.ts +1 -4
  99. package/src/server/canvas-schema.ts +454 -73
  100. package/src/server/canvas-serialization.ts +27 -35
  101. package/src/server/canvas-state.ts +150 -58
  102. package/src/server/chart-template.ts +4 -5
  103. package/src/server/code-graph.ts +19 -6
  104. package/src/server/diagram-presets.ts +28 -29
  105. package/src/server/ext-app-lookup.ts +3 -12
  106. package/src/server/html-node-summary.ts +19 -10
  107. package/src/server/html-primitives.ts +326 -97
  108. package/src/server/html-surface.ts +6 -9
  109. package/src/server/image-source.ts +6 -4
  110. package/src/server/index.ts +320 -217
  111. package/src/server/intent-registry.ts +2 -5
  112. package/src/server/mcp-app-candidate.ts +5 -10
  113. package/src/server/mcp-app-host.ts +14 -38
  114. package/src/server/mcp-app-runtime.ts +12 -20
  115. package/src/server/mutation-history.ts +15 -5
  116. package/src/server/operations/composites.ts +1 -3
  117. package/src/server/operations/http.ts +2 -3
  118. package/src/server/operations/index.ts +7 -1
  119. package/src/server/operations/invoker.ts +4 -3
  120. package/src/server/operations/mcp.ts +22 -30
  121. package/src/server/operations/ops/annotation.ts +122 -10
  122. package/src/server/operations/ops/app.ts +98 -73
  123. package/src/server/operations/ops/ax-await.ts +17 -10
  124. package/src/server/operations/ops/ax-read.ts +347 -0
  125. package/src/server/operations/ops/ax-shared.ts +2 -7
  126. package/src/server/operations/ops/ax-state.ts +32 -14
  127. package/src/server/operations/ops/ax-timeline.ts +32 -19
  128. package/src/server/operations/ops/ax-work.ts +54 -37
  129. package/src/server/operations/ops/batch.ts +39 -14
  130. package/src/server/operations/ops/canvas-wire.ts +91 -0
  131. package/src/server/operations/ops/edges.ts +37 -25
  132. package/src/server/operations/ops/ext-app.ts +346 -0
  133. package/src/server/operations/ops/groups.ts +49 -20
  134. package/src/server/operations/ops/intent.ts +18 -12
  135. package/src/server/operations/ops/json-render.ts +239 -98
  136. package/src/server/operations/ops/nodes.ts +298 -109
  137. package/src/server/operations/ops/query.ts +46 -28
  138. package/src/server/operations/ops/snapshots.ts +35 -26
  139. package/src/server/operations/ops/validate.ts +2 -1
  140. package/src/server/operations/ops/viewport.ts +60 -16
  141. package/src/server/operations/ops/webview.ts +44 -18
  142. package/src/server/operations/registry.ts +2 -3
  143. package/src/server/operations/types.ts +7 -5
  144. package/src/server/operations/webview-runner.ts +1 -3
  145. package/src/server/placement.ts +8 -18
  146. package/src/server/server.ts +108 -1027
  147. package/src/server/spatial-analysis.ts +39 -25
  148. package/src/server/trace-manager.ts +3 -8
  149. package/src/server/web-artifacts.ts +23 -27
  150. package/src/server/webpage-node.ts +5 -13
  151. package/src/shared/auto-arrange.ts +12 -5
  152. package/src/shared/content-height-reporter.ts +8 -6
  153. package/src/shared/ext-app-tool-result.ts +2 -6
  154. package/src/shared/placement.ts +1 -4
  155. package/src/shared/semantic-attention.ts +39 -37
  156. package/src/shared/surface.ts +8 -4
@@ -40,50 +40,27 @@ import { existsSync, readFileSync, statSync, writeFileSync, appendFileSync } fro
40
40
  import { readFile } from 'node:fs/promises';
41
41
  import { basename, extname, join, relative, resolve } from 'node:path';
42
42
  import * as marked from 'marked';
43
- import type {
44
- CallToolResult,
45
- ListPromptsResult,
46
- ListResourcesResult,
47
- ListResourceTemplatesResult,
48
- ListToolsResult,
49
- } from '@modelcontextprotocol/sdk/types.js';
50
- import { type CanvasAnnotation, type CanvasLayout, type CanvasNodeState, IMAGE_MIME_MAP, canvasState } from './canvas-state.js';
51
- import { buildAxBridge, buildAxStateBridge, buildContentHeightReporter, buildHtmlSurfaceDocument, HTML_SURFACE_SANDBOX, normalizeSurfaceTheme } from './html-surface.js';
43
+ import { type CanvasNodeState, IMAGE_MIME_MAP, canvasState } from './canvas-state.js';
44
+ import {
45
+ buildAxBridge,
46
+ buildAxStateBridge,
47
+ buildContentHeightReporter,
48
+ buildHtmlSurfaceDocument,
49
+ HTML_SURFACE_SANDBOX,
50
+ normalizeSurfaceTheme,
51
+ } 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';
55
- import {
56
- callMcpAppTool,
57
- closeMcpAppSession,
58
- closeAllMcpAppSessions,
59
- listMcpAppPrompts,
60
- listMcpAppResources,
61
- listMcpAppResourceTemplates,
62
- listMcpAppTools,
63
- readMcpAppResource,
64
- } from './mcp-app-runtime.js';
65
- import { findOpenCanvasPosition, computeGroupBounds } from './placement.js';
55
+ import { closeMcpAppSession, closeAllMcpAppSessions } from './mcp-app-runtime.js';
56
+ import { findOpenCanvasPosition } from './placement.js';
66
57
  import { mutationHistory } from './mutation-history.js';
67
- import { summarizeCanvasAnnotation } from './canvas-serialization.js';
68
- import { buildCodeGraphSummary, formatCodeGraph } from './code-graph.js';
69
- import { buildAgentContextPreamble, serializeNodeForAgentContext } from './agent-context.js';
70
- import { buildCanvasAxContext, buildCanvasAxSurfaceSnapshot } from './ax-context.js';
71
- import { applyAxInteraction, resolveNodeAxCapabilities } from './ax-interaction.js';
72
- import { isAxEvidenceKind, isAxActivityKind } from './ax-state.js';
73
- import type {
74
- PmxAxEvidenceKind,
75
- PmxAxPolicy,
76
- PmxAxReviewAnchorType,
77
- PmxAxReviewKind,
78
- PmxAxReviewSeverity,
79
- PmxAxSource,
80
- PmxAxWorkItemStatus,
81
- } from './ax-state.js';
58
+ import { buildAgentContextPreamble } from './agent-context.js';
59
+ import { buildCanvasAxSurfaceSnapshot } from './ax-context.js';
60
+ import { resolveNodeAxCapabilities } from './ax-interaction.js';
82
61
  import { normalizeCanvasTheme, type CanvasTheme } from './canvas-db.js';
83
62
  import { validateLocalImageFile } from './image-source.js';
84
63
  import {
85
- applyCanvasNodeUpdates,
86
- refreshCanvasWebpageNode,
87
64
  primeCanvasRuntimeBackends,
88
65
  setCanvasLayoutUpdateEmitter,
89
66
  syncCanvasRuntimeBackends,
@@ -91,26 +68,9 @@ import {
91
68
  import { dispatchOperationRoute, setOperationEventEmitter } from './operations/index.js';
92
69
  import { intentRegistry } from './intent-registry.js';
93
70
  import { setWebviewRunner } from './operations/webview-runner.js';
94
- import {
95
- closeNodeAppSession,
96
- nodeAppSessionId,
97
- } from './operations/ops/nodes.js';
98
- import {
99
- EXCALIDRAW_READ_CHECKPOINT_TOOL,
100
- EXCALIDRAW_SAVE_CHECKPOINT_TOOL,
101
- buildExcalidrawCheckpointId,
102
- buildExcalidrawRestoreCheckpointToolInput,
103
- ensureExcalidrawCheckpointId,
104
- getExcalidrawCheckpointIdFromToolResult,
105
- isExcalidrawCreateView,
106
- } from './diagram-presets.js';
71
+ import { closeNodeAppSession, nodeAppSessionId } from './operations/ops/nodes.js';
107
72
  import { traceManager } from './trace-manager.js';
108
- import {
109
- buildJsonRenderViewerHtml,
110
- } from '../json-render/server.js';
111
- import {
112
- normalizeWebpageUrl,
113
- } from './webpage-node.js';
73
+ import { buildJsonRenderViewerHtml } from '../json-render/server.js';
114
74
  import type { JsonRenderSpec } from '../json-render/server.js';
115
75
 
116
76
  const DEFAULT_HOST = '127.0.0.1';
@@ -294,16 +254,15 @@ export interface CanvasAutomationWebViewStatus {
294
254
  }
295
255
 
296
256
  let canvasAutomationWebView: CanvasWebViewLike | null = null;
297
- let canvasAutomationWebViewStatus: Omit<CanvasAutomationWebViewStatus, 'supported' | 'active' | 'headlessOnly'> =
298
- {
299
- url: null,
300
- backend: null,
301
- width: null,
302
- height: null,
303
- dataStoreDir: null,
304
- startedAt: null,
305
- lastError: null,
306
- };
257
+ let canvasAutomationWebViewStatus: Omit<CanvasAutomationWebViewStatus, 'supported' | 'active' | 'headlessOnly'> = {
258
+ url: null,
259
+ backend: null,
260
+ width: null,
261
+ height: null,
262
+ dataStoreDir: null,
263
+ startedAt: null,
264
+ lastError: null,
265
+ };
307
266
  let canvasAutomationWebViewQueue: Promise<void> = Promise.resolve();
308
267
 
309
268
  function sessionDiagLog(tag: string, payload: Record<string, unknown>): void {
@@ -407,9 +366,7 @@ function detectCanvasAutomationWebViewBackendKind(backend: CanvasWebViewBackend)
407
366
 
408
367
  function getCanvasAutomationWebViewTimeoutMs(): number {
409
368
  const raw = Number.parseInt(process.env.PMX_CANVAS_WEBVIEW_TIMEOUT_MS ?? '', 10);
410
- return Number.isFinite(raw) && raw > 0
411
- ? raw
412
- : DEFAULT_CANVAS_AUTOMATION_WEBVIEW_TIMEOUT_MS;
369
+ return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_CANVAS_AUTOMATION_WEBVIEW_TIMEOUT_MS;
413
370
  }
414
371
 
415
372
  async function withCanvasAutomationWebViewTimeout<T>(task: Promise<T>, action: string): Promise<T> {
@@ -457,7 +414,10 @@ async function closeCanvasAutomationWebViewInternal(): Promise<boolean> {
457
414
 
458
415
  function runCanvasAutomationWebViewTask<T>(task: () => Promise<T>): Promise<T> {
459
416
  const result = canvasAutomationWebViewQueue.then(task);
460
- canvasAutomationWebViewQueue = result.then(() => undefined, () => undefined);
417
+ canvasAutomationWebViewQueue = result.then(
418
+ () => undefined,
419
+ () => undefined,
420
+ );
461
421
  return result;
462
422
  }
463
423
 
@@ -553,7 +513,8 @@ export async function evaluateCanvasAutomationWebView(expression: string): Promi
553
513
  withCanvasAutomationWebViewTimeout(
554
514
  requireActiveCanvasAutomationWebView().evaluate(expression),
555
515
  'evaluating JavaScript in the workbench automation WebView',
556
- ));
516
+ ),
517
+ );
557
518
  }
558
519
 
559
520
  export function wrapCanvasAutomationScript(script: string): string {
@@ -580,9 +541,7 @@ export async function resizeCanvasAutomationWebView(
580
541
  });
581
542
  }
582
543
 
583
- export async function screenshotCanvasAutomationWebView(
584
- options: Record<string, unknown> = {},
585
- ): Promise<Uint8Array> {
544
+ export async function screenshotCanvasAutomationWebView(options: Record<string, unknown> = {}): Promise<Uint8Array> {
586
545
  return runCanvasAutomationWebViewTask(async () => {
587
546
  const result = await withCanvasAutomationWebViewTimeout(
588
547
  requireActiveCanvasAutomationWebView().screenshot(options),
@@ -621,9 +580,7 @@ export interface PrimaryWorkbenchCanvasPromptRequest {
621
580
  contextNodeIds: string[];
622
581
  }
623
582
 
624
- type PrimaryWorkbenchCanvasPromptHandler = (
625
- request: PrimaryWorkbenchCanvasPromptRequest,
626
- ) => Promise<void>;
583
+ type PrimaryWorkbenchCanvasPromptHandler = (request: PrimaryWorkbenchCanvasPromptRequest) => Promise<void>;
627
584
 
628
585
  let primaryWorkbenchCanvasPromptHandler: PrimaryWorkbenchCanvasPromptHandler | null = null;
629
586
 
@@ -698,98 +655,6 @@ function findCanvasExtAppNodeId(toolCallId: string): string | null {
698
655
  });
699
656
  }
700
657
 
701
- function isCheckpointToolName(toolName: string): boolean {
702
- return toolName === EXCALIDRAW_SAVE_CHECKPOINT_TOOL || toolName === EXCALIDRAW_READ_CHECKPOINT_TOOL;
703
- }
704
-
705
- /**
706
- * Decide whether a fresh `callServerTool` result should *replace* the
707
- * canvas node's bootstrap-replay `toolResult`.
708
- *
709
- * The bootstrap-replay toolResult is what the server re-sends to the
710
- * widget on reload to restore visible state. We only want to overwrite
711
- * it when the new result genuinely carries widget state — `isError` or
712
- * `structuredContent`. A plain-text result (e.g. `read_checkpoint`
713
- * returning a string status, or any informational message) updates
714
- * `appModelContext` for the agent's record but should *not* clobber the
715
- * bootstrap entry, because doing so would replace the widget's restored
716
- * state with conversational noise on the next reload.
717
- *
718
- * This separation is exercised by:
719
- * - tests/unit/server-api.test.ts "keeps ext-app model context
720
- * separate from the replayed tool result" (text-only result preserves
721
- * bootstrap replay)
722
- * - tests/unit/server-api.test.ts "app-only text tool results update
723
- * model context without replacing bootstrap replay"
724
- * - tests/unit/server-api.test.ts "rehydrates Excalidraw checkpoint
725
- * replay after server restart" (structured-content result becomes
726
- * the new bootstrap replay)
727
- */
728
- function shouldReplayAppToolResult(toolName: string, result: CallToolResult): boolean {
729
- void toolName;
730
- return Boolean(result.isError || result.structuredContent);
731
- }
732
-
733
- function isRecord(value: unknown): value is Record<string, unknown> {
734
- return value !== null && typeof value === 'object' && !Array.isArray(value);
735
- }
736
-
737
- function getExtAppNodeCheckpointId(node: CanvasNodeState): string {
738
- const appCheckpoint = isRecord(node.data.appCheckpoint) ? node.data.appCheckpoint : null;
739
- const storedCheckpointId = appCheckpoint?.id;
740
- if (typeof storedCheckpointId === 'string' && storedCheckpointId.trim().length > 0) {
741
- return storedCheckpointId.trim();
742
- }
743
- return getExcalidrawCheckpointIdFromToolResult(node.data.toolResult) ?? buildExcalidrawCheckpointId(node.id);
744
- }
745
-
746
- function getLocalExcalidrawCheckpointData(
747
- node: CanvasNodeState,
748
- args: Record<string, unknown> | undefined,
749
- ): string | null {
750
- if (!isExcalidrawCreateView(node.data.serverName, node.data.toolName)) return null;
751
- if (!isRecord(args) || typeof args.id !== 'string') return null;
752
- if (args.id.trim() !== getExtAppNodeCheckpointId(node)) return null;
753
- const appCheckpoint = isRecord(node.data.appCheckpoint) ? node.data.appCheckpoint : null;
754
- const data = appCheckpoint?.data;
755
- return typeof data === 'string' ? data : '';
756
- }
757
-
758
- function persistExcalidrawCheckpointToNode(
759
- nodeId: string,
760
- node: CanvasNodeState,
761
- args: Record<string, unknown> | undefined,
762
- ): boolean {
763
- if (!isExcalidrawCreateView(node.data.serverName, node.data.toolName)) return false;
764
- if (!isRecord(args) || typeof args.id !== 'string') return false;
765
- const checkpointId = getExtAppNodeCheckpointId(node);
766
- if (args.id.trim() !== checkpointId) return false;
767
-
768
- const currentToolInput = isRecord(node.data.toolInput) ? node.data.toolInput : {};
769
- const nextToolInput = {
770
- ...currentToolInput,
771
- elements: buildExcalidrawRestoreCheckpointToolInput(checkpointId, args.data),
772
- };
773
- const currentToolResult = isRecord(node.data.toolResult)
774
- ? ensureExcalidrawCheckpointId(node.data.toolResult as CallToolResult, node.id, checkpointId)
775
- : undefined;
776
-
777
- canvasState.updateNode(nodeId, {
778
- data: {
779
- ...node.data,
780
- toolInput: nextToolInput,
781
- ...(currentToolResult ? { toolResult: currentToolResult } : {}),
782
- appCheckpoint: {
783
- toolName: EXCALIDRAW_SAVE_CHECKPOINT_TOOL,
784
- id: checkpointId,
785
- ...(typeof args.data === 'string' ? { data: args.data } : {}),
786
- updatedAt: new Date().toISOString(),
787
- },
788
- },
789
- });
790
- return true;
791
- }
792
-
793
658
  function findReusableCanvasExtAppNodeId(serverName: string, toolName: string): string | null {
794
659
  for (const node of canvasState.getLayout().nodes) {
795
660
  if (
@@ -831,9 +696,7 @@ function extAppEventGeometryPatch(
831
696
  const width = typeof payload.width === 'number' ? payload.width : undefined;
832
697
  const height = typeof payload.height === 'number' ? payload.height : undefined;
833
698
  return {
834
- ...(x !== undefined || y !== undefined
835
- ? { position: { x: x ?? node.position.x, y: y ?? node.position.y } }
836
- : {}),
699
+ ...(x !== undefined || y !== undefined ? { position: { x: x ?? node.position.x, y: y ?? node.position.y } } : {}),
837
700
  ...(width !== undefined || height !== undefined
838
701
  ? { size: { width: width ?? node.size.width, height: height ?? node.size.height } }
839
702
  : {}),
@@ -888,9 +751,7 @@ export function hasWorkbenchSubscribers(): boolean {
888
751
  return workbenchSubscribers.size > 0;
889
752
  }
890
753
 
891
- export function setPrimaryWorkbenchCanvasPromptHandler(
892
- handler: PrimaryWorkbenchCanvasPromptHandler | null,
893
- ): void {
754
+ export function setPrimaryWorkbenchCanvasPromptHandler(handler: PrimaryWorkbenchCanvasPromptHandler | null): void {
894
755
  primaryWorkbenchCanvasPromptHandler = handler;
895
756
  }
896
757
 
@@ -906,10 +767,7 @@ function enqueuePrimaryWorkbenchIntent(
906
767
  };
907
768
  pendingWorkbenchIntents.push(intent);
908
769
  if (pendingWorkbenchIntents.length > MAX_PENDING_WORKBENCH_INTENTS) {
909
- pendingWorkbenchIntents.splice(
910
- 0,
911
- pendingWorkbenchIntents.length - MAX_PENDING_WORKBENCH_INTENTS,
912
- );
770
+ pendingWorkbenchIntents.splice(0, pendingWorkbenchIntents.length - MAX_PENDING_WORKBENCH_INTENTS);
913
771
  }
914
772
  broadcastWorkbenchEvent('workbench-intent', { ...intent });
915
773
  return intent;
@@ -946,15 +804,6 @@ function malformedJsonResponse(): Response {
946
804
  return responseJson({ ok: false, error: 'Malformed JSON body.' }, 400);
947
805
  }
948
806
 
949
- function htmlEscape(value: string): string {
950
- return value
951
- .replaceAll('&', '&amp;')
952
- .replaceAll('<', '&lt;')
953
- .replaceAll('>', '&gt;')
954
- .replaceAll('"', '&quot;')
955
- .replaceAll("'", '&#39;');
956
- }
957
-
958
807
  function toPreferredExcalidrawUrl(raw: string): string {
959
808
  const parsed = tryParseUrl(raw);
960
809
  if (!parsed) return raw;
@@ -982,18 +831,13 @@ function isExcalidrawUrl(url: string): boolean {
982
831
  }
983
832
 
984
833
  function normalizeMarkdownExternalUrls(markdown: string): string {
985
- const normalizedLinks = markdown.replace(/https?:\/\/[^\s<>"'`)\]]+/gi, (url) =>
986
- toPreferredExcalidrawUrl(url),
987
- );
988
- return normalizedLinks.replace(
989
- /!\[([^\]]*)\]\((https?:\/\/[^)\s]+)\)/gi,
990
- (full, altRaw: string, urlRaw: string) => {
991
- const url = toPreferredExcalidrawUrl(urlRaw);
992
- if (!isExcalidrawUrl(url)) return full;
993
- const label = (altRaw || 'Open Excalidraw diagram').trim() || 'Open Excalidraw diagram';
994
- return `> Excalidraw diagram: [${label}](${url})`;
995
- },
996
- );
834
+ const normalizedLinks = markdown.replace(/https?:\/\/[^\s<>"'`)\]]+/gi, (url) => toPreferredExcalidrawUrl(url));
835
+ return normalizedLinks.replace(/!\[([^\]]*)\]\((https?:\/\/[^)\s]+)\)/gi, (full, altRaw: string, urlRaw: string) => {
836
+ const url = toPreferredExcalidrawUrl(urlRaw);
837
+ if (!isExcalidrawUrl(url)) return full;
838
+ const label = (altRaw || 'Open Excalidraw diagram').trim() || 'Open Excalidraw diagram';
839
+ return `> Excalidraw diagram: [${label}](${url})`;
840
+ });
997
841
  }
998
842
 
999
843
  // ── Canvas SPA HTML ────────────────────────────────────────────
@@ -1308,18 +1152,17 @@ function handleNodeSurface(pathname: string, url: URL): Response {
1308
1152
  const theme = normalizeSurfaceTheme(url.searchParams.get('theme'));
1309
1153
 
1310
1154
  if (node.type === 'html') {
1311
- const html = typeof node.data.html === 'string'
1312
- ? node.data.html
1313
- : typeof node.data.content === 'string'
1314
- ? node.data.content
1315
- : '';
1155
+ const html =
1156
+ typeof node.data.html === 'string'
1157
+ ? node.data.html
1158
+ : typeof node.data.content === 'string'
1159
+ ? node.data.content
1160
+ : '';
1316
1161
  if (!html) return responseText('HTML node has no content', 404);
1317
1162
  const present = url.searchParams.get('present') === '1';
1318
1163
  const axCaps = resolveNodeAxCapabilities(node);
1319
1164
  const axEnabled = axCaps.enabled && axCaps.allowed.length > 0;
1320
- const surfaceTitle = typeof node.data.title === 'string' && node.data.title.trim()
1321
- ? node.data.title
1322
- : node.id;
1165
+ const surfaceTitle = typeof node.data.title === 'string' && node.data.title.trim() ? node.data.title : node.id;
1323
1166
  const doc = buildHtmlSurfaceDocument(html, {
1324
1167
  theme,
1325
1168
  title: surfaceTitle,
@@ -1332,7 +1175,9 @@ function handleNodeSurface(pathname: string, url: URL): Response {
1332
1175
  // Seed the read-side bridge with the current AX state (only for AX surfaces).
1333
1176
  ...(axEnabled ? { axState: buildCanvasAxSurfaceSnapshot() } : {}),
1334
1177
  // Content-height reporter nonce (lets an html node grow to fit its content).
1335
- ...(url.searchParams.get('frameToken') ? { contentHeightToken: url.searchParams.get('frameToken') as string } : {}),
1178
+ ...(url.searchParams.get('frameToken')
1179
+ ? { contentHeightToken: url.searchParams.get('frameToken') as string }
1180
+ : {}),
1336
1181
  });
1337
1182
  return surfaceHtmlResponse(doc, HTML_SURFACE_SANDBOX);
1338
1183
  }
@@ -1374,127 +1219,11 @@ function handleNodeSurface(pathname: string, url: URL): Response {
1374
1219
  return responseText('Node type cannot be opened as a site', 404);
1375
1220
  }
1376
1221
 
1377
- async function handleCanvasUpdate(req: Request): Promise<Response> {
1378
- const body = await readJson(req);
1379
- if (body === null) return malformedJsonResponse();
1380
- const updates = Array.isArray(body.updates) ? body.updates : [];
1381
- const result = body.recordHistory === false
1382
- ? (() => {
1383
- let suppressedResult: ReturnType<typeof applyCanvasNodeUpdates> = { applied: 0, skipped: updates.length };
1384
- canvasState.withSuppressedRecording(() => {
1385
- suppressedResult = applyCanvasNodeUpdates(updates);
1386
- });
1387
- return suppressedResult;
1388
- })()
1389
- : applyCanvasNodeUpdates(updates);
1390
- if (result.applied > 0) {
1391
- emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
1392
- }
1393
- return responseJson({ ok: true, ...result });
1394
- }
1395
-
1396
- async function handleCanvasViewport(req: Request): Promise<Response> {
1397
- const body = await readJson(req);
1398
- if (body === null) return malformedJsonResponse();
1399
- const next = {
1400
- x: typeof body.x === 'number' ? body.x : canvasState.viewport.x,
1401
- y: typeof body.y === 'number' ? body.y : canvasState.viewport.y,
1402
- scale: typeof body.scale === 'number' ? body.scale : canvasState.viewport.scale,
1403
- };
1404
- if (body.recordHistory === false) {
1405
- canvasState.withSuppressedRecording(() => {
1406
- canvasState.setViewport(next);
1407
- });
1408
- } else {
1409
- canvasState.setViewport(next);
1410
- }
1411
- emitPrimaryWorkbenchEvent('canvas-viewport-update', { viewport: canvasState.viewport });
1412
- return responseJson({ ok: true });
1413
- }
1414
-
1415
- function annotationBounds(points: CanvasAnnotation['points']): CanvasAnnotation['bounds'] {
1416
- const xs = points.map((point) => point.x);
1417
- const ys = points.map((point) => point.y);
1418
- const minX = Math.min(...xs);
1419
- const minY = Math.min(...ys);
1420
- const maxX = Math.max(...xs);
1421
- const maxY = Math.max(...ys);
1422
- return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
1423
- }
1424
-
1425
- function textAnnotationBounds(point: CanvasAnnotation['points'][number], text: string, width: number): CanvasAnnotation['bounds'] {
1426
- return {
1427
- x: point.x,
1428
- y: point.y - width,
1429
- width: Math.max(width, text.length * width * 0.62),
1430
- height: width * 1.2,
1431
- };
1432
- }
1433
-
1434
- function parseAnnotationPoints(value: unknown): CanvasAnnotation['points'] {
1435
- if (!Array.isArray(value)) return [];
1436
- return value
1437
- .map((point) => {
1438
- if (!point || typeof point !== 'object' || Array.isArray(point)) return null;
1439
- const record = point as Record<string, unknown>;
1440
- if (typeof record.x !== 'number' || typeof record.y !== 'number') return null;
1441
- if (!Number.isFinite(record.x) || !Number.isFinite(record.y)) return null;
1442
- return { x: record.x, y: record.y };
1443
- })
1444
- .filter((point): point is CanvasAnnotation['points'][number] => point !== null);
1445
- }
1446
-
1447
- async function handleCanvasAddAnnotation(req: Request): Promise<Response> {
1448
- const body = await readJson(req);
1449
- if (body === null) return malformedJsonResponse();
1450
- const type = body.type === 'text' ? 'text' : 'freehand';
1451
- const points = parseAnnotationPoints(body.points);
1452
- if (points.length < (type === 'text' ? 1 : 2)) {
1453
- return responseJson({ ok: false, error: type === 'text' ? 'Text annotation requires a valid point.' : 'Annotation requires at least two valid points.' }, 400);
1454
- }
1455
-
1456
- const defaultWidth = type === 'text' ? 24 : 4;
1457
- const maxWidth = type === 'text' ? 96 : 24;
1458
- const width = typeof body.width === 'number' && Number.isFinite(body.width)
1459
- ? Math.min(maxWidth, Math.max(1, body.width))
1460
- : defaultWidth;
1461
- const color = typeof body.color === 'string' && (body.color === 'currentColor' || /^#[0-9a-fA-F]{6}$/.test(body.color))
1462
- ? body.color
1463
- : 'currentColor';
1464
- const label = typeof body.label === 'string' && body.label.trim().length > 0
1465
- ? body.label.trim().slice(0, 160)
1466
- : undefined;
1467
- const text = type === 'text' && typeof body.text === 'string' && body.text.trim().length > 0
1468
- ? body.text.trim().slice(0, 240)
1469
- : undefined;
1470
- if (type === 'text' && !text) {
1471
- return responseJson({ ok: false, error: 'Text annotation requires text.' }, 400);
1472
- }
1473
- const id = typeof body.id === 'string' && body.id.trim().length > 0
1474
- ? body.id.trim().slice(0, 120)
1475
- : `ann-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
1476
- const annotation: CanvasAnnotation = {
1477
- id,
1478
- type,
1479
- points,
1480
- bounds: type === 'text' ? textAnnotationBounds(points[0]!, text!, width) : annotationBounds(points),
1481
- color,
1482
- width,
1483
- ...(text ? { text } : {}),
1484
- ...(label ?? text ? { label: label ?? text } : {}),
1485
- createdAt: new Date().toISOString(),
1486
- };
1487
-
1488
- canvasState.addAnnotation(annotation);
1489
- emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
1490
- return responseJson({ ok: true, annotation: summarizeCanvasAnnotation(annotation) });
1491
- }
1492
-
1493
1222
  // ── Serve image file for image nodes ─────────────────────────
1494
1223
  async function handleCanvasImage(pathname: string): Promise<Response> {
1495
1224
  const nodeId = pathname.replace('/api/canvas/image/', '');
1496
1225
  const node = canvasState.getNode(nodeId);
1497
- if (!node || node.type !== 'image') {
1226
+ if (node?.type !== 'image') {
1498
1227
  return responseText('Image node not found', 404);
1499
1228
  }
1500
1229
  const src = (node.data.path as string) || (node.data.src as string) || '';
@@ -1526,45 +1255,6 @@ async function handleCanvasImage(pathname: string): Promise<Response> {
1526
1255
  });
1527
1256
  }
1528
1257
 
1529
- async function handleCanvasRefreshWebpageNode(nodeId: string, req: Request): Promise<Response> {
1530
- const existing = canvasState.getNode(nodeId);
1531
- if (!existing || existing.type !== 'webpage') {
1532
- return responseJson({ ok: false, error: `Webpage node "${nodeId}" not found.` }, 404);
1533
- }
1534
-
1535
- const body = await readJson(req);
1536
- if (body === null) return malformedJsonResponse();
1537
- const rawUrl = typeof body.url === 'string' ? body.url : undefined;
1538
- let url: string | undefined;
1539
- if (rawUrl && rawUrl.trim().length > 0) {
1540
- try {
1541
- url = normalizeWebpageUrl(rawUrl);
1542
- } catch (error) {
1543
- return responseJson({ ok: false, error: error instanceof Error ? error.message : 'Invalid webpage URL.' }, 400);
1544
- }
1545
- }
1546
-
1547
- const result = await refreshCanvasWebpageNode(nodeId, { ...(url ? { url } : {}) });
1548
- emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
1549
- return responseJson(result, result.ok ? 200 : 400);
1550
- }
1551
-
1552
- // handleCanvasBuildWebArtifact migrated to the operation registry
1553
- // (plan-008 Wave 4): src/server/operations/ops/app.ts (webartifact.build).
1554
-
1555
- async function handleCanvasThemeUpdate(req: Request): Promise<Response> {
1556
- const body = await readJson(req);
1557
- if (body === null) return malformedJsonResponse();
1558
- const theme = normalizeCanvasTheme(body.theme, canvasState.theme);
1559
- const next = canvasState.setTheme(theme);
1560
- broadcastWorkbenchEvent('theme-changed', {
1561
- theme: next,
1562
- sessionId: primaryWorkbenchSessionId,
1563
- timestamp: new Date().toISOString(),
1564
- });
1565
- return responseJson({ ok: true, theme: next });
1566
- }
1567
-
1568
1258
  async function handleJsonRenderView(url: URL): Promise<Response> {
1569
1259
  const nodeId = url.searchParams.get('nodeId') ?? '';
1570
1260
  if (!nodeId) return responseText('Missing nodeId', 400);
@@ -1585,22 +1275,18 @@ async function handleJsonRenderView(url: URL): Promise<Response> {
1585
1275
 
1586
1276
  const themeValue = url.searchParams.get('theme');
1587
1277
  const theme =
1588
- themeValue === 'dark' || themeValue === 'light' || themeValue === 'high-contrast'
1589
- ? themeValue
1590
- : undefined;
1278
+ themeValue === 'dark' || themeValue === 'light' || themeValue === 'high-contrast' ? themeValue : undefined;
1591
1279
  const title = (node.data.title as string) || node.id;
1592
1280
  // Devtools panel is double-gated: the operator must opt in via the env flag
1593
1281
  // AND the request must carry ?devtools=1. Off by default in all normal runs.
1594
1282
  const devtoolsEnabled =
1595
- process.env.PMX_CANVAS_JSON_RENDER_DEVTOOLS === '1' &&
1596
- url.searchParams.get('devtools') === '1';
1283
+ process.env.PMX_CANVAS_JSON_RENDER_DEVTOOLS === '1' && url.searchParams.get('devtools') === '1';
1597
1284
  const axToken = url.searchParams.get('axToken');
1598
1285
  const axEnabled = resolveNodeAxCapabilities(node).enabled;
1599
1286
  const frameToken = url.searchParams.get('frameToken');
1600
1287
  const displayParam = url.searchParams.get('display');
1601
- const display = displayParam === 'expanded' ? 'expanded' as const
1602
- : displayParam === 'site' ? 'site' as const
1603
- : undefined;
1288
+ const display =
1289
+ displayParam === 'expanded' ? ('expanded' as const) : displayParam === 'site' ? ('site' as const) : undefined;
1604
1290
  // A standalone "site" tab fills the viewport; content-fit (grow-to-content for the
1605
1291
  // in-canvas iframe) would fight that, so it's ignored in site mode (#65).
1606
1292
  const fitContent = url.searchParams.get('fit') === 'content' && display !== 'site';
@@ -1682,9 +1368,7 @@ function handleArtifactView(url: URL): Response {
1682
1368
  const safeNodeId = node.id.replace(/[^A-Za-z0-9_-]/g, '').slice(0, 80);
1683
1369
  const stateJson = JSON.stringify(buildCanvasAxSurfaceSnapshot()).replace(/</g, '\\u003c');
1684
1370
  const bridge = `${buildAxBridge(safeToken, safeNodeId)}${buildAxStateBridge(safeToken, stateJson)}`;
1685
- content = content.includes('</head>')
1686
- ? content.replace('</head>', `${bridge}</head>`)
1687
- : `${bridge}${content}`;
1371
+ content = content.includes('</head>') ? content.replace('</head>', `${bridge}</head>`) : `${bridge}${content}`;
1688
1372
  }
1689
1373
  }
1690
1374
  // Content-height reporter so a web-artifact node grows to fit its app (#48).
@@ -1711,7 +1395,8 @@ function handleArtifactView(url: URL): Response {
1711
1395
  ? `<article class="markdown-body">${marked.parse(content) as string}</article>`
1712
1396
  : `<pre class="artifact-code"><code>${escapeHtml(content)}</code></pre>`;
1713
1397
 
1714
- return new Response(`<!doctype html>
1398
+ return new Response(
1399
+ `<!doctype html>
1715
1400
  <html lang="en">
1716
1401
  <head>
1717
1402
  <meta charset="utf-8" />
@@ -1809,12 +1494,14 @@ function handleArtifactView(url: URL): Response {
1809
1494
  <section class="panel">${body}</section>
1810
1495
  </main>
1811
1496
  </body>
1812
- </html>`, {
1813
- headers: {
1814
- 'Content-Type': 'text/html; charset=utf-8',
1815
- 'Cache-Control': 'no-store',
1497
+ </html>`,
1498
+ {
1499
+ headers: {
1500
+ 'Content-Type': 'text/html; charset=utf-8',
1501
+ 'Cache-Control': 'no-store',
1502
+ },
1816
1503
  },
1817
- });
1504
+ );
1818
1505
  }
1819
1506
 
1820
1507
  function handleRead(pathLike: string): Response {
@@ -1832,180 +1519,6 @@ function handleRead(pathLike: string): Response {
1832
1519
  });
1833
1520
  }
1834
1521
 
1835
- async function handleExtAppCallTool(req: Request): Promise<Response> {
1836
- const body = await readJson(req);
1837
- if (body === null) return malformedJsonResponse();
1838
- const sessionId = typeof body.sessionId === 'string' ? body.sessionId.trim() : '';
1839
- const toolName = typeof body.toolName === 'string' ? body.toolName.trim() : '';
1840
- if (!sessionId || !toolName) {
1841
- return responseJson({ ok: false, error: 'Missing sessionId or toolName.' }, 400);
1842
- }
1843
-
1844
- const args =
1845
- body.arguments && typeof body.arguments === 'object' && !Array.isArray(body.arguments)
1846
- ? body.arguments as Record<string, unknown>
1847
- : undefined;
1848
- const nodeId = typeof body.nodeId === 'string' ? body.nodeId.trim() : '';
1849
-
1850
- try {
1851
- const requestedNode = nodeId ? canvasState.getNode(nodeId) : undefined;
1852
- const canReadLocalCheckpoint =
1853
- requestedNode?.type === 'mcp-app' &&
1854
- requestedNode.data.mode === 'ext-app' &&
1855
- requestedNode.data.appSessionId === sessionId;
1856
- const localCheckpointData = canReadLocalCheckpoint && toolName === EXCALIDRAW_READ_CHECKPOINT_TOOL
1857
- ? getLocalExcalidrawCheckpointData(requestedNode, args)
1858
- : null;
1859
- const result = localCheckpointData === null
1860
- ? await callMcpAppTool(sessionId, toolName, args)
1861
- : { content: [{ type: 'text', text: localCheckpointData }] } satisfies CallToolResult;
1862
- if (nodeId) {
1863
- const node = canvasState.getNode(nodeId);
1864
- if (node?.type === 'mcp-app' && node.data.mode === 'ext-app' && node.data.appSessionId === sessionId) {
1865
- let changed = false;
1866
- if (toolName === EXCALIDRAW_SAVE_CHECKPOINT_TOOL && persistExcalidrawCheckpointToNode(nodeId, node, args)) {
1867
- // Checkpoint saves are replayed through toolInput.elements instead of
1868
- // replacing the original create_view result with a generic "ok".
1869
- changed = true;
1870
- } else if (!(isExcalidrawCreateView(node.data.serverName, node.data.toolName) && isCheckpointToolName(toolName))) {
1871
- const nextData: Record<string, unknown> = { ...node.data };
1872
- if (shouldReplayAppToolResult(toolName, result)) nextData.toolResult = result;
1873
- const nextModelContext: Record<string, unknown> = {};
1874
- if (Array.isArray(result.content)) {
1875
- nextModelContext.content = result.content;
1876
- }
1877
- if (result.structuredContent && typeof result.structuredContent === 'object' && !Array.isArray(result.structuredContent)) {
1878
- nextModelContext.structuredContent = result.structuredContent;
1879
- }
1880
- if (Object.keys(nextModelContext).length > 0) {
1881
- nextData.appModelContext = {
1882
- ...nextModelContext,
1883
- updatedAt: new Date().toISOString(),
1884
- };
1885
- }
1886
- canvasState.updateNode(nodeId, {
1887
- data: nextData,
1888
- });
1889
- changed = true;
1890
- }
1891
- if (changed) {
1892
- broadcastWorkbenchEvent('canvas-layout-update', {
1893
- layout: canvasState.getLayout(),
1894
- sessionId: primaryWorkbenchSessionId,
1895
- timestamp: new Date().toISOString(),
1896
- });
1897
- }
1898
- }
1899
- }
1900
- return responseJson({ ok: true, result });
1901
- } catch (error) {
1902
- return responseJson({ ok: false, error: error instanceof Error ? error.message : String(error) }, 400);
1903
- }
1904
- }
1905
-
1906
- async function handleExtAppReadResource(req: Request): Promise<Response> {
1907
- const body = await readJson(req);
1908
- if (body === null) return malformedJsonResponse();
1909
- const sessionId = typeof body.sessionId === 'string' ? body.sessionId.trim() : '';
1910
- const uri = typeof body.uri === 'string' ? body.uri.trim() : '';
1911
- if (!sessionId || !uri) {
1912
- return responseJson({ ok: false, error: 'Missing sessionId or uri.' }, 400);
1913
- }
1914
-
1915
- try {
1916
- const result = await readMcpAppResource(sessionId, uri);
1917
- return responseJson({ ok: true, result });
1918
- } catch (error) {
1919
- return responseJson({ ok: false, error: error instanceof Error ? error.message : String(error) }, 400);
1920
- }
1921
- }
1922
-
1923
- async function handleExtAppListTools(req: Request): Promise<Response> {
1924
- const body = await readJson(req);
1925
- if (body === null) return malformedJsonResponse();
1926
- const sessionId = typeof body.sessionId === 'string' ? body.sessionId.trim() : '';
1927
- if (!sessionId) return responseJson({ ok: false, error: 'Missing sessionId.' }, 400);
1928
-
1929
- try {
1930
- const result: ListToolsResult = await listMcpAppTools(sessionId);
1931
- return responseJson({ ok: true, result });
1932
- } catch (error) {
1933
- return responseJson({ ok: false, error: error instanceof Error ? error.message : String(error) }, 400);
1934
- }
1935
- }
1936
-
1937
- async function handleExtAppListResources(req: Request): Promise<Response> {
1938
- const body = await readJson(req);
1939
- if (body === null) return malformedJsonResponse();
1940
- const sessionId = typeof body.sessionId === 'string' ? body.sessionId.trim() : '';
1941
- if (!sessionId) return responseJson({ ok: false, error: 'Missing sessionId.' }, 400);
1942
-
1943
- try {
1944
- const result: ListResourcesResult = await listMcpAppResources(sessionId);
1945
- return responseJson({ ok: true, result });
1946
- } catch (error) {
1947
- return responseJson({ ok: false, error: error instanceof Error ? error.message : String(error) }, 400);
1948
- }
1949
- }
1950
-
1951
- async function handleExtAppListResourceTemplates(req: Request): Promise<Response> {
1952
- const body = await readJson(req);
1953
- if (body === null) return malformedJsonResponse();
1954
- const sessionId = typeof body.sessionId === 'string' ? body.sessionId.trim() : '';
1955
- if (!sessionId) return responseJson({ ok: false, error: 'Missing sessionId.' }, 400);
1956
-
1957
- try {
1958
- const result: ListResourceTemplatesResult = await listMcpAppResourceTemplates(sessionId);
1959
- return responseJson({ ok: true, result });
1960
- } catch (error) {
1961
- return responseJson({ ok: false, error: error instanceof Error ? error.message : String(error) }, 400);
1962
- }
1963
- }
1964
-
1965
- async function handleExtAppListPrompts(req: Request): Promise<Response> {
1966
- const body = await readJson(req);
1967
- if (body === null) return malformedJsonResponse();
1968
- const sessionId = typeof body.sessionId === 'string' ? body.sessionId.trim() : '';
1969
- if (!sessionId) return responseJson({ ok: false, error: 'Missing sessionId.' }, 400);
1970
-
1971
- try {
1972
- const result: ListPromptsResult = await listMcpAppPrompts(sessionId);
1973
- return responseJson({ ok: true, result });
1974
- } catch (error) {
1975
- return responseJson({ ok: false, error: error instanceof Error ? error.message : String(error) }, 400);
1976
- }
1977
- }
1978
-
1979
- async function handleExtAppModelContext(req: Request): Promise<Response> {
1980
- const body = await readJson(req);
1981
- if (body === null) return malformedJsonResponse();
1982
- const nodeId = typeof body.nodeId === 'string' ? body.nodeId.trim() : '';
1983
- if (!nodeId) return responseJson({ ok: false, error: 'Missing nodeId.' }, 400);
1984
-
1985
- const node = canvasState.getNode(nodeId);
1986
- if (!node) return responseJson({ ok: false, error: `Node "${nodeId}" not found.` }, 404);
1987
-
1988
- canvasState.updateNode(nodeId, {
1989
- data: {
1990
- ...node.data,
1991
- appModelContext: {
1992
- ...(Array.isArray(body.content) ? { content: body.content } : {}),
1993
- ...(body.structuredContent && typeof body.structuredContent === 'object' && !Array.isArray(body.structuredContent)
1994
- ? { structuredContent: body.structuredContent }
1995
- : {}),
1996
- updatedAt: new Date().toISOString(),
1997
- },
1998
- },
1999
- });
2000
-
2001
- broadcastWorkbenchEvent('canvas-layout-update', {
2002
- layout: canvasState.getLayout(),
2003
- sessionId: primaryWorkbenchSessionId,
2004
- timestamp: new Date().toISOString(),
2005
- });
2006
- return responseJson({ ok: true });
2007
- }
2008
-
2009
1522
  function handleWorkbenchState(): Response {
2010
1523
  const mcpAppHost = getMcpAppHostSnapshot();
2011
1524
  if (!primaryWorkbenchPath) {
@@ -2052,9 +1565,7 @@ function currentWorkbenchUrl(): string | null {
2052
1565
  async function handleWorkbenchWebViewScreenshot(req: Request): Promise<Response> {
2053
1566
  const body = await readJson(req);
2054
1567
  if (body === null) return malformedJsonResponse();
2055
- const format = body.format === 'jpeg' || body.format === 'webp' || body.format === 'png'
2056
- ? body.format
2057
- : 'png';
1568
+ const format = body.format === 'jpeg' || body.format === 'webp' || body.format === 'png' ? body.format : 'png';
2058
1569
  const quality = typeof body.quality === 'number' ? body.quality : undefined;
2059
1570
 
2060
1571
  try {
@@ -2063,11 +1574,7 @@ async function handleWorkbenchWebViewScreenshot(req: Request): Promise<Response>
2063
1574
  ...(quality !== undefined ? { quality } : {}),
2064
1575
  });
2065
1576
  const responseBytes = Uint8Array.from(bytes);
2066
- const mimeType = format === 'jpeg'
2067
- ? 'image/jpeg'
2068
- : format === 'webp'
2069
- ? 'image/webp'
2070
- : 'image/png';
1577
+ const mimeType = format === 'jpeg' ? 'image/jpeg' : format === 'webp' ? 'image/webp' : 'image/png';
2071
1578
  return new Response(responseBytes.buffer, {
2072
1579
  headers: {
2073
1580
  'Content-Type': mimeType,
@@ -2102,10 +1609,7 @@ async function handleWorkbenchIntent(req: Request): Promise<Response> {
2102
1609
  }
2103
1610
 
2104
1611
  const rawPayload = body.payload;
2105
- const payload =
2106
- rawPayload && typeof rawPayload === 'object'
2107
- ? (rawPayload as PrimaryWorkbenchEventPayload)
2108
- : {};
1612
+ const payload = rawPayload && typeof rawPayload === 'object' ? (rawPayload as PrimaryWorkbenchEventPayload) : {};
2109
1613
 
2110
1614
  // Handle trace intents directly on the server
2111
1615
  if (rawType === 'trace-toggle') {
@@ -2303,10 +1807,7 @@ async function handleCanvasPrompt(req: Request): Promise<Response> {
2303
1807
  const parentNodeId = typeof body.parentNodeId === 'string' ? body.parentNodeId : undefined;
2304
1808
  const MAX_CONTEXT_NODES = 20;
2305
1809
  let contextNodeIds = Array.isArray(body.contextNodeIds)
2306
- ? (body.contextNodeIds.filter((id: unknown) => typeof id === 'string') as string[]).slice(
2307
- 0,
2308
- MAX_CONTEXT_NODES,
2309
- )
1810
+ ? (body.contextNodeIds.filter((id: unknown) => typeof id === 'string') as string[]).slice(0, MAX_CONTEXT_NODES)
2310
1811
  : [];
2311
1812
 
2312
1813
  if (contextNodeIds.length === 0 && canvasState.contextPinnedNodeIds.size > 0) {
@@ -2365,9 +1866,7 @@ async function handleCanvasPrompt(req: Request): Promise<Response> {
2365
1866
  }
2366
1867
 
2367
1868
  const MAX_THREAD_TURNS = 100;
2368
- const existingTurnCount = Array.isArray(threadNode.data.turns)
2369
- ? (threadNode.data.turns as unknown[]).length
2370
- : 0;
1869
+ const existingTurnCount = Array.isArray(threadNode.data.turns) ? (threadNode.data.turns as unknown[]).length : 0;
2371
1870
  if (existingTurnCount >= MAX_THREAD_TURNS) {
2372
1871
  return responseText('Thread has reached the maximum number of turns', 400);
2373
1872
  }
@@ -2442,9 +1941,7 @@ async function handleCanvasPrompt(req: Request): Promise<Response> {
2442
1941
  const suffix = Math.random().toString(36).slice(2, 8);
2443
1942
  const nodeId = `prompt-${Date.now()}-${suffix}`;
2444
1943
 
2445
- const promptCount = canvasState
2446
- .getLayout()
2447
- .nodes.filter((n) => n.type === 'prompt' || n.type === 'response').length;
1944
+ const promptCount = canvasState.getLayout().nodes.filter((n) => n.type === 'prompt' || n.type === 'response').length;
2448
1945
  const pos = position ?? { x: 380 + promptCount * 30, y: 1260 + promptCount * 30 };
2449
1946
 
2450
1947
  let enrichedText = text;
@@ -2538,135 +2035,6 @@ async function handleCanvasPrompt(req: Request): Promise<Response> {
2538
2035
  return responseJson({ ok: true, nodeId });
2539
2036
  }
2540
2037
 
2541
- function handleGetPinnedContext(): Response {
2542
- const pinnedIds = Array.from(canvasState.contextPinnedNodeIds);
2543
- const preamble = pinnedIds.length > 0 ? buildSelectionContextPreamble(pinnedIds) : '';
2544
- const nodes = pinnedIds
2545
- .map((id) => canvasState.getNode(id))
2546
- .filter((node): node is CanvasNodeState => node !== undefined)
2547
- .map((node) => serializeNodeForAgentContext(node, {
2548
- defaultTextLength: 700,
2549
- webpageTextLength: 1600,
2550
- includePosition: true,
2551
- }));
2552
- return responseJson({ preamble, nodeIds: pinnedIds, count: pinnedIds.length, nodes });
2553
- }
2554
-
2555
- function normalizeAxNodeIds(value: unknown): string[] {
2556
- if (!Array.isArray(value)) return [];
2557
- return value.filter((id): id is string => typeof id === 'string');
2558
- }
2559
-
2560
- function normalizeAxSource(value: unknown, fallback: PmxAxSource): PmxAxSource {
2561
- return value === 'agent' ||
2562
- value === 'api' ||
2563
- value === 'browser' ||
2564
- value === 'cli' ||
2565
- value === 'codex' ||
2566
- value === 'copilot' ||
2567
- value === 'mcp' ||
2568
- value === 'sdk' ||
2569
- value === 'system'
2570
- ? value
2571
- : fallback;
2572
- }
2573
-
2574
- function handleGetAxContext(url: URL): Response {
2575
- // Optional ?consumer= filters the compact `delivery` lead block (loop-safe — a
2576
- // consumer never sees steering/activity it originated), so a host adapter can
2577
- // inject its own un-truncated pending block per turn (report #54 hardening).
2578
- const consumer = url.searchParams.get('consumer') ?? undefined;
2579
- return responseJson(buildCanvasAxContext(consumer));
2580
- }
2581
-
2582
- function isReviewSeverity(v: unknown): v is PmxAxReviewSeverity {
2583
- return v === 'info' || v === 'warning' || v === 'error';
2584
- }
2585
- function isReviewKind(v: unknown): v is PmxAxReviewKind {
2586
- return v === 'comment' || v === 'finding';
2587
- }
2588
- function isReviewAnchor(v: unknown): v is PmxAxReviewAnchorType {
2589
- return v === 'node' || v === 'file' || v === 'region';
2590
- }
2591
-
2592
- // Validate untrusted activity `reactions` from an HTTP body into the typed override
2593
- // shape ingestActivity expects. `false` suppresses a default reaction; an object
2594
- // overrides its fields (invalid fields are dropped, not stored raw).
2595
- function normalizeActivityReactions(input: Record<string, unknown>): {
2596
- workItem?: false | { status?: PmxAxWorkItemStatus; detail?: string | null };
2597
- evidence?: false | { kind?: PmxAxEvidenceKind; body?: string | null };
2598
- review?: false | { severity?: PmxAxReviewSeverity; kind?: PmxAxReviewKind; anchorType?: PmxAxReviewAnchorType; nodeId?: string | null };
2599
- } {
2600
- const out: ReturnType<typeof normalizeActivityReactions> = {};
2601
- if (input.workItem === false) out.workItem = false;
2602
- else if (isRecord(input.workItem)) {
2603
- const status = normalizeAxWorkItemStatus(input.workItem.status);
2604
- out.workItem = {
2605
- ...(status ? { status } : {}),
2606
- ...(typeof input.workItem.detail === 'string' ? { detail: input.workItem.detail } : {}),
2607
- };
2608
- }
2609
- if (input.evidence === false) out.evidence = false;
2610
- else if (isRecord(input.evidence)) {
2611
- out.evidence = {
2612
- ...(isAxEvidenceKind(input.evidence.kind) ? { kind: input.evidence.kind } : {}),
2613
- ...(typeof input.evidence.body === 'string' ? { body: input.evidence.body } : {}),
2614
- };
2615
- }
2616
- if (input.review === false) out.review = false;
2617
- else if (isRecord(input.review)) {
2618
- out.review = {
2619
- ...(isReviewSeverity(input.review.severity) ? { severity: input.review.severity } : {}),
2620
- ...(isReviewKind(input.review.kind) ? { kind: input.review.kind } : {}),
2621
- ...(isReviewAnchor(input.review.anchorType) ? { anchorType: input.review.anchorType } : {}),
2622
- ...(typeof input.review.nodeId === 'string' ? { nodeId: input.review.nodeId } : {}),
2623
- };
2624
- }
2625
- return out;
2626
- }
2627
-
2628
- // Report primitive A: ingest a harness-forwarded agent activity; the board auto-reacts.
2629
- async function handleAxActivityIngest(req: Request): Promise<Response> {
2630
- const body = await readJson(req);
2631
- if (body === null) return malformedJsonResponse();
2632
- if (!isAxActivityKind(body.kind)) {
2633
- return responseJson({ ok: false, error: "activity requires a valid 'kind': one of tool-start, tool-result, failure, error, session-start, session-end, command, note." }, 400);
2634
- }
2635
- if (typeof body.title !== 'string' || !body.title.trim()) {
2636
- return responseJson({ ok: false, error: 'activity requires a title.' }, 400);
2637
- }
2638
- const result = canvasState.ingestActivity(
2639
- {
2640
- kind: body.kind,
2641
- title: body.title,
2642
- ...(typeof body.summary === 'string' ? { summary: body.summary } : {}),
2643
- ...(body.outcome === 'success' || body.outcome === 'failure' ? { outcome: body.outcome } : {}),
2644
- ...(typeof body.ref === 'string' ? { ref: body.ref } : {}),
2645
- ...(Array.isArray(body.nodeIds) ? { nodeIds: normalizeAxNodeIds(body.nodeIds) } : {}),
2646
- ...(isRecord(body.data) ? { data: body.data } : {}),
2647
- ...(isRecord(body.reactions) ? { reactions: normalizeActivityReactions(body.reactions) } : {}),
2648
- },
2649
- { source: normalizeAxSource(body.source, 'api') },
2650
- );
2651
- const meta = { sessionId: primaryWorkbenchSessionId, timestamp: new Date().toISOString() };
2652
- broadcastWorkbenchEvent('ax-event-created', { event: result.event, ...meta });
2653
- if (result.workItem) broadcastWorkbenchEvent('ax-state-changed', { workItem: result.workItem, ...meta });
2654
- if (result.evidence) broadcastWorkbenchEvent('ax-event-created', { evidence: result.evidence, ...meta });
2655
- if (result.review) broadcastWorkbenchEvent('ax-state-changed', { reviewAnnotation: result.review, ...meta });
2656
- return responseJson({ ok: true, ...result });
2657
- }
2658
-
2659
- // Report primitive D single-item gate reads (GET /api/canvas/ax/{approval,
2660
- // elicitation,mode}/:id) with the optional ?waitMs= long-poll migrated to the
2661
- // operation registry (plan-007 Slice B wave 4):
2662
- // src/server/operations/ops/ax-await.ts.
2663
-
2664
- // Compact AX state for surfaces (the same shape seeded into AX-enabled iframes).
2665
- // The client fetches this and pushes it to surfaces over the ax-update channel.
2666
- function handleGetAxSurfaceSnapshot(): Response {
2667
- return responseJson(buildCanvasAxSurfaceSnapshot());
2668
- }
2669
-
2670
2038
  // Open a node's surface in the user's real system browser (for hosts whose
2671
2039
  // embedded browser makes window.open('_blank') feel in-place, e.g. Codex).
2672
2040
  // Accepts ONLY { nodeId, url? } and opens this server's own surface URL — never
@@ -2694,108 +2062,6 @@ async function handleOpenExternalSurface(req: Request): Promise<Response> {
2694
2062
  return responseJson({ ok: true, opened, url: surfacePath });
2695
2063
  }
2696
2064
 
2697
- async function handleAxInteraction(req: Request): Promise<Response> {
2698
- const body = await readJson(req);
2699
- if (body === null) return malformedJsonResponse();
2700
- const { result, events } = applyAxInteraction(canvasState, body, normalizeAxSource(body.source, 'api'));
2701
- for (const e of events) {
2702
- broadcastWorkbenchEvent(e.event, {
2703
- ...e.payload,
2704
- sessionId: primaryWorkbenchSessionId,
2705
- timestamp: new Date().toISOString(),
2706
- });
2707
- }
2708
- return responseJson(result, result.ok ? 200 : result.status);
2709
- }
2710
-
2711
- // handleAxDeliveryPending / handleAxDeliveryMark migrated to the operation
2712
- // registry (plan-007 Slice B wave 3): src/server/operations/ops/ax-timeline.ts.
2713
-
2714
- function handleAxElicitationList(): Response {
2715
- return responseJson({ ok: true, elicitations: canvasState.getElicitations() });
2716
- }
2717
-
2718
- // handleAxElicitationRequest / handleAxElicitationRespond migrated to the
2719
- // operation registry (plan-007 Slice B wave 2): src/server/operations/ops/ax-work.ts.
2720
-
2721
- function handleAxModeList(): Response {
2722
- return responseJson({ ok: true, modeRequests: canvasState.getModeRequests() });
2723
- }
2724
-
2725
- // handleAxModeRequest / handleAxModeResolve migrated to the operation registry
2726
- // (plan-007 Slice B wave 2): src/server/operations/ops/ax-work.ts.
2727
-
2728
- function handleAxCommandList(): Response {
2729
- return responseJson({ ok: true, commands: canvasState.getCommandRegistry() });
2730
- }
2731
-
2732
- // handleAxCommandInvoke migrated to the operation registry (plan-007 Slice B
2733
- // wave 3): src/server/operations/ops/ax-timeline.ts.
2734
-
2735
- function handleAxPolicyGet(): Response {
2736
- return responseJson({ ok: true, policy: canvasState.getPolicy() });
2737
- }
2738
-
2739
- async function handleAxStatePatch(req: Request): Promise<Response> {
2740
- const body = await readJson(req);
2741
- if (body === null) return malformedJsonResponse();
2742
- if (!body.focus || typeof body.focus !== 'object' || Array.isArray(body.focus)) {
2743
- return responseJson({ ok: false, error: 'PATCH /api/canvas/ax currently requires a focus object.' }, 400);
2744
- }
2745
- const focusInput = body.focus as Record<string, unknown>;
2746
- const focus = canvasState.setAxFocus(normalizeAxNodeIds(focusInput.nodeIds), {
2747
- source: normalizeAxSource(focusInput.source, 'api'),
2748
- });
2749
- broadcastWorkbenchEvent('ax-state-changed', {
2750
- focus,
2751
- sessionId: primaryWorkbenchSessionId,
2752
- timestamp: new Date().toISOString(),
2753
- });
2754
- return responseJson({ ok: true, state: canvasState.getAxState() });
2755
- }
2756
-
2757
- // handleAxEventAdd / handleAxSteer / handleAxTimelineGet migrated to the
2758
- // operation registry (plan-007 Slice B wave 3): src/server/operations/ops/ax-timeline.ts.
2759
-
2760
- const AX_WORK_STATUSES = new Set(['todo', 'in-progress', 'blocked', 'done', 'cancelled']);
2761
-
2762
- function normalizeAxWorkItemStatus(value: unknown): 'todo' | 'in-progress' | 'blocked' | 'done' | 'cancelled' | undefined {
2763
- return typeof value === 'string' && AX_WORK_STATUSES.has(value)
2764
- ? value as 'todo' | 'in-progress' | 'blocked' | 'done' | 'cancelled'
2765
- : undefined;
2766
- }
2767
-
2768
- function handleAxWorkList(): Response {
2769
- return responseJson({ ok: true, workItems: canvasState.getWorkItems() });
2770
- }
2771
-
2772
- // handleAxWorkAdd / handleAxWorkUpdate migrated to the operation registry
2773
- // (plan-007 Slice B wave 2): src/server/operations/ops/ax-work.ts.
2774
-
2775
- function handleAxApprovalList(): Response {
2776
- return responseJson({ ok: true, approvalGates: canvasState.getApprovalGates() });
2777
- }
2778
-
2779
- // handleAxApprovalRequest / handleAxApprovalResolve migrated to the operation
2780
- // registry (plan-007 Slice B wave 2): src/server/operations/ops/ax-work.ts.
2781
-
2782
- // handleAxEvidenceAdd migrated to the operation registry (plan-007 Slice B
2783
- // wave 3): src/server/operations/ops/ax-timeline.ts.
2784
-
2785
- // The AX review normalize helpers + their constant sets moved with the
2786
- // migrated handlers (plan-007 Slice B wave 2): src/server/operations/ops/ax-work.ts.
2787
-
2788
- function handleAxReviewList(): Response {
2789
- return responseJson({ ok: true, reviewAnnotations: canvasState.getReviewAnnotations() });
2790
- }
2791
-
2792
- // handleAxReviewAdd / handleAxReviewUpdate migrated to the operation registry
2793
- // (plan-007 Slice B wave 2): src/server/operations/ops/ax-work.ts.
2794
-
2795
- function handleAxHostCapabilityGet(): Response {
2796
- return responseJson({ ok: true, host: canvasState.getHostCapability() });
2797
- }
2798
-
2799
2065
  // ── Port resolution ───────────────────────────────────────────
2800
2066
 
2801
2067
  function buildPortCandidates(preferredPort: number): number[] {
@@ -2930,11 +2196,9 @@ export function buildMacBrowserOpenScript(appName: string, url: string): string
2930
2196
  }
2931
2197
 
2932
2198
  function resolveWindowsBrowserForCanvas(): { name: string; exe: string } | null {
2933
- const envDirs = [
2934
- process.env.PROGRAMFILES,
2935
- process.env['PROGRAMFILES(X86)'],
2936
- process.env.LOCALAPPDATA,
2937
- ].filter((d): d is string => Boolean(d));
2199
+ const envDirs = [process.env.PROGRAMFILES, process.env['PROGRAMFILES(X86)'], process.env.LOCALAPPDATA].filter(
2200
+ (d): d is string => Boolean(d),
2201
+ );
2938
2202
 
2939
2203
  const browsers = [
2940
2204
  { name: 'Edge', subpath: join('Microsoft', 'Edge', 'Application', 'msedge.exe') },
@@ -2995,8 +2259,7 @@ function syncContextNodeToCanvasState(
2995
2259
  mergedData.messagesLength !== undefined ||
2996
2260
  mergedData.utilization !== undefined ||
2997
2261
  mergedData.nearLimit !== undefined;
2998
- const shouldCreate =
2999
- options.forceCreate === true || cards.length > 0 || auxTabs.length > 0 || hasUsage;
2262
+ const shouldCreate = options.forceCreate === true || cards.length > 0 || auxTabs.length > 0 || hasUsage;
3000
2263
 
3001
2264
  if (!existing) {
3002
2265
  if (!shouldCreate) return;
@@ -3105,16 +2368,17 @@ function syncEventToCanvasState(event: string, payload: PrimaryWorkbenchEventPay
3105
2368
  } else if (event === 'ext-app-open') {
3106
2369
  const toolCallId = payload.toolCallId as string;
3107
2370
  if (!toolCallId) return;
3108
- const id = typeof payload.nodeId === 'string' && payload.nodeId.length > 0
3109
- ? payload.nodeId
3110
- : toolCallId.startsWith('ext-app-') ? toolCallId : `ext-app-${toolCallId}`;
2371
+ const id =
2372
+ typeof payload.nodeId === 'string' && payload.nodeId.length > 0
2373
+ ? payload.nodeId
2374
+ : toolCallId.startsWith('ext-app-')
2375
+ ? toolCallId
2376
+ : `ext-app-${toolCallId}`;
3111
2377
  const dataPatch = {
3112
2378
  mode: 'ext-app',
3113
2379
  toolCallId,
3114
2380
  nodeId: id,
3115
- ...(typeof payload.title === 'string' && payload.title.trim().length > 0
3116
- ? { title: payload.title.trim() }
3117
- : {}),
2381
+ ...(typeof payload.title === 'string' && payload.title.trim().length > 0 ? { title: payload.title.trim() } : {}),
3118
2382
  html: payload.html,
3119
2383
  toolInput: payload.toolInput,
3120
2384
  serverName: payload.serverName,
@@ -3224,18 +2488,14 @@ function syncEventToCanvasState(event: string, payload: PrimaryWorkbenchEventPay
3224
2488
  success: typeof payload.success === 'boolean' ? payload.success : undefined,
3225
2489
  error: typeof payload.error === 'string' ? payload.error : undefined,
3226
2490
  content: typeof payload.content === 'string' ? payload.content : undefined,
3227
- detailedContent:
3228
- typeof payload.detailedContent === 'string' ? payload.detailedContent : undefined,
2491
+ detailedContent: typeof payload.detailedContent === 'string' ? payload.detailedContent : undefined,
3229
2492
  }),
3230
2493
  },
3231
2494
  });
3232
2495
  }
3233
2496
  });
3234
2497
  } else if (event === 'context-cards') {
3235
- syncContextNodeToCanvasState(
3236
- { cards: Array.isArray(payload.cards) ? payload.cards : [] },
3237
- { forceCreate: true },
3238
- );
2498
+ syncContextNodeToCanvasState({ cards: Array.isArray(payload.cards) ? payload.cards : [] }, { forceCreate: true });
3239
2499
  } else if (event === 'context-usage') {
3240
2500
  syncContextNodeToCanvasState({
3241
2501
  currentTokens: payload.currentTokens,
@@ -3258,9 +2518,7 @@ function syncEventToCanvasState(event: string, payload: PrimaryWorkbenchEventPay
3258
2518
  return;
3259
2519
  }
3260
2520
  const auxTabs = Array.isArray(existing.data.auxTabs)
3261
- ? (existing.data.auxTabs as Array<Record<string, unknown>>).filter(
3262
- (tab) => tab.id !== payload.id,
3263
- )
2521
+ ? (existing.data.auxTabs as Array<Record<string, unknown>>).filter((tab) => tab.id !== payload.id)
3264
2522
  : [];
3265
2523
  syncContextNodeToCanvasState({ auxTabs });
3266
2524
  } else if (event === 'canvas-status' || event === 'execution-phase') {
@@ -3405,10 +2663,7 @@ function syncEventToCanvasState(event: string, payload: PrimaryWorkbenchEventPay
3405
2663
  }
3406
2664
  }
3407
2665
 
3408
- export function emitPrimaryWorkbenchEvent(
3409
- event: string,
3410
- payload: PrimaryWorkbenchEventPayload = {},
3411
- ): void {
2666
+ export function emitPrimaryWorkbenchEvent(event: string, payload: PrimaryWorkbenchEventPayload = {}): void {
3412
2667
  rotatePrimaryWorkbenchSessionIfNeeded();
3413
2668
  const envelope = {
3414
2669
  ...payload,
@@ -3485,10 +2740,7 @@ function ensureCanvasBrowserOpen(): void {
3485
2740
  canvasBrowserOpening = false;
3486
2741
  }
3487
2742
 
3488
- export function openPrimaryWorkbenchPath(
3489
- pathLike: string,
3490
- workspaceRoot = process.cwd(),
3491
- ): string | null {
2743
+ export function openPrimaryWorkbenchPath(pathLike: string, workspaceRoot = process.cwd()): string | null {
3492
2744
  const safePath = resolve(pathLike);
3493
2745
  if (!isMarkdownFile(safePath)) return null;
3494
2746
  if (!existsSync(safePath)) return null;
@@ -3552,11 +2804,12 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
3552
2804
  rotatePrimaryWorkbenchSessionIfNeeded();
3553
2805
 
3554
2806
  const preferredPort = options.port ?? Number(process.env.PMX_WEB_CANVAS_PORT ?? DEFAULT_PORT);
3555
- const portCandidates = options.port === 0
3556
- ? [0]
3557
- : options.allowPortFallback === false
3558
- ? [preferredPort > 0 ? Math.floor(preferredPort) : DEFAULT_PORT]
3559
- : buildPortCandidates(preferredPort);
2807
+ const portCandidates =
2808
+ options.port === 0
2809
+ ? [0]
2810
+ : options.allowPortFallback === false
2811
+ ? [preferredPort > 0 ? Math.floor(preferredPort) : DEFAULT_PORT]
2812
+ : buildPortCandidates(preferredPort);
3560
2813
 
3561
2814
  for (const portCandidate of portCandidates) {
3562
2815
  try {
@@ -3633,14 +2886,14 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
3633
2886
  return handleWorkbenchIntent(req);
3634
2887
  }
3635
2888
 
3636
- // Webview automation routes (plan-008 Wave 3): status / start /
3637
- // evaluate / resize / stop are now registered operations served by the
3638
- // registry. A null return falls through (e.g. the screenshot route
3639
- // below, which stays hand-written it returns a binary image, not a
3640
- // JSON wire body).
3641
- if (url.pathname.startsWith('/api/workbench/webview')) {
3642
- const webviewResponse = await dispatchOperationRoute(req, url);
3643
- if (webviewResponse) return webviewResponse;
2889
+ // Operation registry routes (plan-005): every registered operation is
2890
+ // dispatched here webview automation, /api/canvas/*, /api/ext-app/*,
2891
+ // theme/update/viewport/annotation/refresh (plan-009 C1 slices 1-3). A
2892
+ // null return falls through to the remaining hand-written routes
2893
+ // (SSE, binary, HTML/static serving, and the server-coupled handlers).
2894
+ if (url.pathname.startsWith('/api/')) {
2895
+ const operationResponse = await dispatchOperationRoute(req, url);
2896
+ if (operationResponse) return operationResponse;
3644
2897
  }
3645
2898
 
3646
2899
  if (url.pathname === '/api/workbench/webview/screenshot' && req.method === 'POST') {
@@ -3655,194 +2908,22 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
3655
2908
  return handleRender(req);
3656
2909
  }
3657
2910
 
3658
- // Operation registry routes (plan-005): registered operations are
3659
- // dispatched here; a null return falls through to the legacy routes.
3660
- if (url.pathname.startsWith('/api/canvas/')) {
3661
- const operationResponse = await dispatchOperationRoute(req, url);
3662
- if (operationResponse) return operationResponse;
3663
- }
3664
-
3665
- if (url.pathname === '/api/canvas/theme' && req.method === 'GET') {
3666
- return responseJson({ ok: true, theme: canvasState.theme });
3667
- }
3668
-
3669
- if (url.pathname === '/api/canvas/theme' && req.method === 'POST') {
3670
- return handleCanvasThemeUpdate(req);
3671
- }
3672
-
3673
- if (url.pathname === '/api/canvas/update' && req.method === 'POST') {
3674
- return handleCanvasUpdate(req);
3675
- }
3676
-
3677
- // POST /api/canvas/batch migrated to the operation registry
3678
- // (plan-008 Wave 2): src/server/operations/ops/batch.ts.
3679
-
3680
- if (url.pathname === '/api/canvas/viewport' && req.method === 'POST') {
3681
- return handleCanvasViewport(req);
3682
- }
3683
-
3684
- if (url.pathname === '/api/canvas/annotation' && req.method === 'POST') {
3685
- return handleCanvasAddAnnotation(req);
3686
- }
3687
-
3688
- // POST /api/canvas/mcp-app/open, /api/canvas/diagram, and
3689
- // /api/canvas/web-artifact migrated to the operation registry
3690
- // (plan-008 Wave 4): src/server/operations/ops/app.ts.
3691
-
3692
- // Individual node GET/PATCH/DELETE
3693
- if (url.pathname.startsWith('/api/canvas/node/') && url.pathname.endsWith('/refresh') && req.method === 'POST') {
3694
- const nodeId = url.pathname.slice('/api/canvas/node/'.length, -'/refresh'.length);
3695
- return handleCanvasRefreshWebpageNode(nodeId, req);
3696
- }
3697
-
3698
2911
  if (url.pathname.startsWith('/api/canvas/image/') && req.method === 'GET') {
3699
2912
  return await handleCanvasImage(url.pathname);
3700
2913
  }
3701
2914
 
3702
- if (url.pathname === '/api/canvas/pinned-context' && req.method === 'GET') {
3703
- return handleGetPinnedContext();
3704
- }
3705
-
3706
- // GET /api/canvas/ax migrated to the operation registry (plan-007 Slice B.1).
3707
-
3708
- if (url.pathname === '/api/canvas/ax' && req.method === 'PATCH') {
3709
- return handleAxStatePatch(req);
3710
- }
3711
-
3712
- if (url.pathname === '/api/canvas/ax/context' && req.method === 'GET') {
3713
- return handleGetAxContext(url);
3714
- }
3715
-
3716
- if (url.pathname === '/api/canvas/ax/activity' && req.method === 'POST') {
3717
- return handleAxActivityIngest(req);
3718
- }
3719
-
3720
- if (url.pathname === '/api/canvas/ax/surface-snapshot' && req.method === 'GET') {
3721
- return handleGetAxSurfaceSnapshot();
3722
- }
3723
-
3724
2915
  if (url.pathname === '/api/canvas/open-external' && req.method === 'POST') {
3725
2916
  return handleOpenExternalSurface(req);
3726
2917
  }
3727
2918
 
3728
- // POST /api/canvas/ax/focus migrated to the operation registry (plan-007 Slice B.1).
3729
-
3730
- // POST /api/canvas/ax/event + POST /api/canvas/ax/steer + GET
3731
- // /api/canvas/ax/timeline migrated to the operation registry
3732
- // (plan-007 Slice B wave 3): src/server/operations/ops/ax-timeline.ts.
3733
-
3734
- if (url.pathname === '/api/canvas/ax/work' && req.method === 'GET') {
3735
- return handleAxWorkList();
3736
- }
3737
-
3738
- // POST /api/canvas/ax/work + PATCH /api/canvas/ax/work/:id migrated to
3739
- // the operation registry (plan-007 Slice B wave 2).
3740
-
3741
- if (url.pathname === '/api/canvas/ax/approval' && req.method === 'GET') {
3742
- return handleAxApprovalList();
3743
- }
3744
-
3745
- // POST /api/canvas/ax/approval + POST /api/canvas/ax/approval/:id/resolve
3746
- // migrated to the operation registry (plan-007 Slice B wave 2).
3747
-
3748
- // GET /api/canvas/ax/approval/:id (single-item read + ?waitMs long-poll)
3749
- // migrated to the operation registry (plan-007 Slice B wave 4).
3750
-
3751
- // POST /api/canvas/ax/evidence migrated to the operation registry
3752
- // (plan-007 Slice B wave 3): src/server/operations/ops/ax-timeline.ts.
3753
-
3754
- if (url.pathname === '/api/canvas/ax/review' && req.method === 'GET') {
3755
- return handleAxReviewList();
3756
- }
3757
-
3758
- // POST /api/canvas/ax/review + PATCH /api/canvas/ax/review/:id migrated
3759
- // to the operation registry (plan-007 Slice B wave 2).
3760
-
3761
- if (url.pathname === '/api/canvas/ax/host-capability' && req.method === 'GET') {
3762
- return handleAxHostCapabilityGet();
3763
- }
3764
-
3765
- // PUT /api/canvas/ax/host-capability migrated to the operation registry (plan-007 Slice B.1).
3766
-
3767
- if (url.pathname === '/api/canvas/ax/interaction' && req.method === 'POST') {
3768
- return handleAxInteraction(req);
3769
- }
3770
-
3771
- // GET /api/canvas/ax/delivery/pending + POST /api/canvas/ax/delivery/:id/mark
3772
- // migrated to the operation registry (plan-007 Slice B wave 3):
3773
- // src/server/operations/ops/ax-timeline.ts.
3774
-
3775
- if (url.pathname === '/api/canvas/ax/elicitation' && req.method === 'GET') {
3776
- return handleAxElicitationList();
3777
- }
3778
-
3779
- // POST /api/canvas/ax/elicitation + POST /api/canvas/ax/elicitation/:id/respond
3780
- // migrated to the operation registry (plan-007 Slice B wave 2).
3781
-
3782
- // GET /api/canvas/ax/elicitation/:id (single-item read + ?waitMs long-poll)
3783
- // migrated to the operation registry (plan-007 Slice B wave 4).
3784
-
3785
- if (url.pathname === '/api/canvas/ax/mode' && req.method === 'GET') {
3786
- return handleAxModeList();
3787
- }
3788
-
3789
- // POST /api/canvas/ax/mode + POST /api/canvas/ax/mode/:id/resolve migrated
3790
- // to the operation registry (plan-007 Slice B wave 2).
3791
-
3792
- // GET /api/canvas/ax/mode/:id (single-item read + ?waitMs long-poll)
3793
- // migrated to the operation registry (plan-007 Slice B wave 4).
3794
-
3795
- if (url.pathname === '/api/canvas/ax/command' && req.method === 'GET') {
3796
- return handleAxCommandList();
3797
- }
3798
-
3799
- // POST /api/canvas/ax/command migrated to the operation registry
3800
- // (plan-007 Slice B wave 3): src/server/operations/ops/ax-timeline.ts.
3801
-
3802
- if (url.pathname === '/api/canvas/ax/policy' && req.method === 'GET') {
3803
- return handleAxPolicyGet();
3804
- }
3805
-
3806
- // POST /api/canvas/ax/policy migrated to the operation registry (plan-007 Slice B.1).
3807
-
3808
- // Code graph API
3809
- if (url.pathname === '/api/canvas/code-graph' && req.method === 'GET') {
3810
- const summary = buildCodeGraphSummary();
3811
- return responseJson(summary);
3812
- }
2919
+ // Every other /api/canvas/ax* route, pinned-context, and code-graph is
2920
+ // registry-served (plan-007 slices; plan-009 C1 slice 2 folded the GET
2921
+ // reads + activity/interaction/PATCH): src/server/operations/ops/ax-*.ts.
3813
2922
 
3814
2923
  if (url.pathname === '/api/canvas/prompt' && req.method === 'POST') {
3815
2924
  return handleCanvasPrompt(req);
3816
2925
  }
3817
2926
 
3818
- if (url.pathname === '/api/ext-app/call-tool' && req.method === 'POST') {
3819
- return handleExtAppCallTool(req);
3820
- }
3821
-
3822
- if (url.pathname === '/api/ext-app/read-resource' && req.method === 'POST') {
3823
- return handleExtAppReadResource(req);
3824
- }
3825
-
3826
- if (url.pathname === '/api/ext-app/list-tools' && req.method === 'POST') {
3827
- return handleExtAppListTools(req);
3828
- }
3829
-
3830
- if (url.pathname === '/api/ext-app/list-resources' && req.method === 'POST') {
3831
- return handleExtAppListResources(req);
3832
- }
3833
-
3834
- if (url.pathname === '/api/ext-app/list-resource-templates' && req.method === 'POST') {
3835
- return handleExtAppListResourceTemplates(req);
3836
- }
3837
-
3838
- if (url.pathname === '/api/ext-app/list-prompts' && req.method === 'POST') {
3839
- return handleExtAppListPrompts(req);
3840
- }
3841
-
3842
- if (url.pathname === '/api/ext-app/model-context' && req.method === 'POST') {
3843
- return handleExtAppModelContext(req);
3844
- }
3845
-
3846
2927
  // Static files for canvas SPA bundle
3847
2928
  if (url.pathname.startsWith('/canvas/')) {
3848
2929
  const staticResponse = serveCanvasStatic(url.pathname);