pmx-canvas 0.2.7 → 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.
- package/CHANGELOG.md +148 -1
- package/Readme.md +13 -11
- package/dist/canvas/index.js +2 -2
- package/dist/json-render/index.css +1 -1
- package/dist/types/cli/daemon.d.ts +74 -0
- package/dist/types/cli/watch.d.ts +2 -2
- package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
- package/dist/types/client/canvas/CommandPalette.d.ts +1 -1
- package/dist/types/client/canvas/Minimap.d.ts +1 -1
- package/dist/types/client/nodes/FileNode.d.ts +1 -1
- package/dist/types/client/nodes/ImageNode.d.ts +1 -1
- package/dist/types/client/nodes/InlineFormatBar.d.ts +1 -1
- package/dist/types/client/nodes/MarkdownNode.d.ts +1 -1
- package/dist/types/client/nodes/PromptNode.d.ts +1 -1
- package/dist/types/client/nodes/ResponseNode.d.ts +1 -1
- package/dist/types/mcp/canvas-access.d.ts +0 -7
- package/dist/types/server/ax-state-manager.d.ts +3 -2
- package/dist/types/server/bundled-skills.d.ts +3 -3
- package/dist/types/server/canvas-schema.d.ts +1 -1
- package/dist/types/server/canvas-state.d.ts +2 -0
- package/dist/types/server/ext-app-lookup.d.ts +1 -3
- package/dist/types/server/html-primitives.d.ts +1 -1
- package/dist/types/server/index.d.ts +4 -4
- package/dist/types/server/operations/composites.d.ts +29 -23
- package/dist/types/server/operations/http.d.ts +2 -1
- package/dist/types/server/operations/index.d.ts +1 -1
- package/dist/types/server/operations/ops/ax-read.d.ts +2 -0
- package/dist/types/server/operations/ops/canvas-wire.d.ts +2 -0
- package/dist/types/server/operations/ops/ext-app.d.ts +2 -0
- package/dist/types/shared/ax-intent.d.ts +1 -1
- package/docs/RELEASE.md +23 -8
- package/docs/ax-host-adapter-contract.md +7 -7
- package/docs/ax-state-contract.md +7 -6
- package/docs/bun-webview-integration.md +5 -5
- package/docs/cli.md +1 -1
- package/docs/http-api.md +35 -3
- package/docs/mcp.md +121 -100
- package/docs/node-types.md +45 -35
- package/docs/plans/plan-006-mcp-tool-consolidation.md +3 -1
- package/docs/plans/plan-008-registry-finish.md +2 -0
- package/docs/plans/plan-009-tech-debt-backlog.md +51 -0
- package/docs/screenshot.png +0 -0
- package/docs/tech-debt-assessment-2026-06.md +1 -1
- package/docs/tech-debt-assessment-2026-07.md +135 -0
- package/package.json +5 -2
- package/skills/data-analysis/SKILL.md +3 -3
- package/skills/frontend-design/SKILL.md +3 -2
- package/skills/json-render-mcp/SKILL.md +4 -3
- package/skills/playwright-cli/SKILL.md +1 -1
- package/skills/pmx-canvas/SKILL.md +12 -7
- package/skills/pmx-canvas/evals/evals.json +2 -2
- package/skills/pmx-canvas/references/ax-html-control-surface.md +3 -1
- package/skills/pmx-canvas/references/excalidraw-diagram-authoring.md +2 -2
- package/skills/pmx-canvas/references/full-reference.md +58 -56
- package/skills/pmx-canvas/references/html-primitives.md +3 -3
- package/skills/tufte-viz/SKILL.md +5 -4
- package/skills/web-artifacts-builder/SKILL.md +4 -4
- package/src/cli/agent.ts +1861 -1548
- package/src/cli/daemon.ts +460 -0
- package/src/cli/index.ts +63 -326
- package/src/cli/watch.ts +2 -10
- package/src/client/App.tsx +48 -46
- package/src/client/canvas/AttentionHistory.tsx +11 -1
- package/src/client/canvas/CanvasNode.tsx +41 -29
- package/src/client/canvas/CanvasViewport.tsx +101 -66
- package/src/client/canvas/CommandPalette.tsx +61 -27
- package/src/client/canvas/ContextMenu.tsx +13 -20
- package/src/client/canvas/ContextPinBar.tsx +1 -5
- package/src/client/canvas/ContextPinHud.tsx +1 -6
- package/src/client/canvas/DockedNode.tsx +4 -4
- package/src/client/canvas/EdgeLayer.tsx +37 -36
- package/src/client/canvas/ExpandedNodeOverlay.tsx +69 -45
- package/src/client/canvas/FocusFieldLayer.tsx +20 -22
- package/src/client/canvas/IntentLayer.tsx +31 -14
- package/src/client/canvas/Minimap.tsx +11 -16
- package/src/client/canvas/SelectionBar.tsx +4 -11
- package/src/client/canvas/ShortcutOverlay.tsx +3 -1
- package/src/client/canvas/SnapshotPanel.tsx +77 -95
- package/src/client/canvas/auto-fit.ts +15 -14
- package/src/client/canvas/snap-guides.ts +12 -12
- package/src/client/canvas/use-node-resize.ts +1 -5
- package/src/client/canvas/use-pan-zoom.ts +25 -26
- package/src/client/ext-app/bridge.ts +3 -12
- package/src/client/icons.tsx +63 -20
- package/src/client/nodes/ContextNode.tsx +14 -25
- package/src/client/nodes/ExtAppFrame.tsx +60 -39
- package/src/client/nodes/FileNode.tsx +74 -62
- package/src/client/nodes/GroupNode.tsx +4 -6
- package/src/client/nodes/HtmlNode.tsx +76 -46
- package/src/client/nodes/ImageNode.tsx +18 -27
- package/src/client/nodes/InlineFormatBar.tsx +4 -21
- package/src/client/nodes/InlineMarkdownEditor.tsx +0 -1
- package/src/client/nodes/LedgerNode.tsx +10 -4
- package/src/client/nodes/MarkdownNode.tsx +3 -10
- package/src/client/nodes/McpAppNode.tsx +26 -22
- package/src/client/nodes/MdFormatBar.tsx +10 -7
- package/src/client/nodes/PromptNode.tsx +23 -51
- package/src/client/nodes/ResponseNode.tsx +3 -13
- package/src/client/nodes/StatusNode.tsx +5 -9
- package/src/client/nodes/StatusSummary.tsx +2 -8
- package/src/client/nodes/WebpageNode.tsx +20 -14
- package/src/client/nodes/iframe-document-url.ts +25 -16
- package/src/client/nodes/image-warnings.ts +1 -7
- package/src/client/nodes/md-format.ts +20 -5
- package/src/client/state/attention-bridge.ts +4 -9
- package/src/client/state/attention-store.ts +1 -7
- package/src/client/state/canvas-store.ts +52 -36
- package/src/client/state/intent-bridge.ts +176 -112
- package/src/client/state/intent-store.ts +4 -1
- package/src/client/state/sse-bridge.ts +53 -70
- package/src/json-render/catalog.ts +12 -16
- package/src/json-render/charts/components.tsx +16 -20
- package/src/json-render/charts/extra-components.tsx +8 -16
- package/src/json-render/charts/extra-definitions.ts +1 -2
- package/src/json-render/charts/tufte-components.tsx +37 -20
- package/src/json-render/charts/tufte-definitions.ts +8 -2
- package/src/json-render/renderer/index.tsx +42 -22
- package/src/json-render/schema.ts +6 -3
- package/src/json-render/server.ts +33 -39
- package/src/mcp/canvas-access.ts +40 -121
- package/src/mcp/server.ts +206 -271
- package/src/server/agent-context.ts +63 -36
- package/src/server/ax-context.ts +7 -5
- package/src/server/ax-interaction.ts +176 -43
- package/src/server/ax-state-manager.ts +185 -41
- package/src/server/ax-state.ts +142 -47
- package/src/server/bundled-skills.ts +3 -3
- package/src/server/canvas-db.ts +213 -95
- package/src/server/canvas-operations.ts +177 -120
- package/src/server/canvas-provenance.ts +1 -4
- package/src/server/canvas-schema.ts +500 -102
- package/src/server/canvas-serialization.ts +27 -35
- package/src/server/canvas-state.ts +155 -58
- package/src/server/chart-template.ts +4 -5
- package/src/server/code-graph.ts +20 -7
- package/src/server/diagram-presets.ts +28 -29
- package/src/server/ext-app-lookup.ts +4 -20
- package/src/server/html-node-summary.ts +19 -10
- package/src/server/html-primitives.ts +326 -97
- package/src/server/html-surface.ts +6 -9
- package/src/server/image-source.ts +6 -4
- package/src/server/index.ts +321 -218
- package/src/server/intent-registry.ts +3 -6
- package/src/server/mcp-app-candidate.ts +5 -10
- package/src/server/mcp-app-host.ts +14 -38
- package/src/server/mcp-app-runtime.ts +12 -20
- package/src/server/mutation-history.ts +15 -5
- package/src/server/operations/composites.ts +38 -40
- package/src/server/operations/http.ts +5 -5
- package/src/server/operations/index.ts +7 -1
- package/src/server/operations/invoker.ts +4 -3
- package/src/server/operations/mcp.ts +29 -36
- package/src/server/operations/ops/annotation.ts +122 -10
- package/src/server/operations/ops/app.ts +102 -77
- package/src/server/operations/ops/ax-await.ts +17 -10
- package/src/server/operations/ops/ax-read.ts +347 -0
- package/src/server/operations/ops/ax-shared.ts +2 -7
- package/src/server/operations/ops/ax-state.ts +32 -14
- package/src/server/operations/ops/ax-timeline.ts +32 -19
- package/src/server/operations/ops/ax-work.ts +54 -37
- package/src/server/operations/ops/batch.ts +39 -14
- package/src/server/operations/ops/canvas-wire.ts +91 -0
- package/src/server/operations/ops/edges.ts +37 -25
- package/src/server/operations/ops/ext-app.ts +346 -0
- package/src/server/operations/ops/groups.ts +49 -20
- package/src/server/operations/ops/intent.ts +18 -12
- package/src/server/operations/ops/json-render.ts +239 -98
- package/src/server/operations/ops/nodes.ts +300 -111
- package/src/server/operations/ops/query.ts +62 -33
- package/src/server/operations/ops/snapshots.ts +35 -26
- package/src/server/operations/ops/validate.ts +2 -1
- package/src/server/operations/ops/viewport.ts +60 -16
- package/src/server/operations/ops/webview.ts +44 -18
- package/src/server/operations/registry.ts +2 -3
- package/src/server/operations/types.ts +7 -5
- package/src/server/operations/webview-runner.ts +1 -3
- package/src/server/placement.ts +8 -18
- package/src/server/server.ts +121 -1012
- package/src/server/spatial-analysis.ts +39 -25
- package/src/server/trace-manager.ts +3 -8
- package/src/server/web-artifacts.ts +23 -27
- package/src/server/webpage-node.ts +5 -13
- package/src/shared/auto-arrange.ts +12 -5
- package/src/shared/ax-intent.ts +1 -1
- package/src/shared/content-height-reporter.ts +8 -6
- package/src/shared/ext-app-tool-result.ts +2 -6
- package/src/shared/placement.ts +1 -4
- package/src/shared/semantic-attention.ts +39 -37
- package/src/shared/surface.ts +8 -4
package/src/server/server.ts
CHANGED
|
@@ -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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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 {
|
|
68
|
-
import {
|
|
69
|
-
import {
|
|
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
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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(
|
|
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;
|
|
@@ -920,7 +778,8 @@ function rotatePrimaryWorkbenchSessionIfNeeded(): void {
|
|
|
920
778
|
primaryWorkbenchSessionId = `pmx-${Date.now().toString(36)}`;
|
|
921
779
|
}
|
|
922
780
|
|
|
923
|
-
|
|
781
|
+
/** Null means a non-empty body failed to parse — handlers must 400, not silently no-op. */
|
|
782
|
+
async function readJson(req: Request): Promise<Record<string, unknown> | null> {
|
|
924
783
|
let text = '';
|
|
925
784
|
try {
|
|
926
785
|
text = await req.text();
|
|
@@ -937,17 +796,12 @@ async function readJson(req: Request): Promise<Record<string, unknown>> {
|
|
|
937
796
|
return value as Record<string, unknown>;
|
|
938
797
|
} catch (error) {
|
|
939
798
|
logWorkbenchWarning('readJson', error);
|
|
940
|
-
return
|
|
799
|
+
return null;
|
|
941
800
|
}
|
|
942
801
|
}
|
|
943
802
|
|
|
944
|
-
function
|
|
945
|
-
return
|
|
946
|
-
.replaceAll('&', '&')
|
|
947
|
-
.replaceAll('<', '<')
|
|
948
|
-
.replaceAll('>', '>')
|
|
949
|
-
.replaceAll('"', '"')
|
|
950
|
-
.replaceAll("'", ''');
|
|
803
|
+
function malformedJsonResponse(): Response {
|
|
804
|
+
return responseJson({ ok: false, error: 'Malformed JSON body.' }, 400);
|
|
951
805
|
}
|
|
952
806
|
|
|
953
807
|
function toPreferredExcalidrawUrl(raw: string): string {
|
|
@@ -977,18 +831,13 @@ function isExcalidrawUrl(url: string): boolean {
|
|
|
977
831
|
}
|
|
978
832
|
|
|
979
833
|
function normalizeMarkdownExternalUrls(markdown: string): string {
|
|
980
|
-
const normalizedLinks = markdown.replace(/https?:\/\/[^\s<>"'`)\]]+/gi, (url) =>
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
if (!isExcalidrawUrl(url)) return full;
|
|
988
|
-
const label = (altRaw || 'Open Excalidraw diagram').trim() || 'Open Excalidraw diagram';
|
|
989
|
-
return `> Excalidraw diagram: [${label}](${url})`;
|
|
990
|
-
},
|
|
991
|
-
);
|
|
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
|
+
});
|
|
992
841
|
}
|
|
993
842
|
|
|
994
843
|
// ── Canvas SPA HTML ────────────────────────────────────────────
|
|
@@ -1228,6 +1077,7 @@ function addFrameDocument(html: string, sandbox: string): string {
|
|
|
1228
1077
|
|
|
1229
1078
|
async function handleCreateFrameDocument(req: Request): Promise<Response> {
|
|
1230
1079
|
const body = await readJson(req);
|
|
1080
|
+
if (body === null) return malformedJsonResponse();
|
|
1231
1081
|
const html = body.html;
|
|
1232
1082
|
if (typeof html !== 'string' || !html) {
|
|
1233
1083
|
return responseJson({ ok: false, error: 'Frame document requires non-empty html.' }, 400);
|
|
@@ -1302,18 +1152,17 @@ function handleNodeSurface(pathname: string, url: URL): Response {
|
|
|
1302
1152
|
const theme = normalizeSurfaceTheme(url.searchParams.get('theme'));
|
|
1303
1153
|
|
|
1304
1154
|
if (node.type === 'html') {
|
|
1305
|
-
const html =
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1155
|
+
const html =
|
|
1156
|
+
typeof node.data.html === 'string'
|
|
1157
|
+
? node.data.html
|
|
1158
|
+
: typeof node.data.content === 'string'
|
|
1159
|
+
? node.data.content
|
|
1160
|
+
: '';
|
|
1310
1161
|
if (!html) return responseText('HTML node has no content', 404);
|
|
1311
1162
|
const present = url.searchParams.get('present') === '1';
|
|
1312
1163
|
const axCaps = resolveNodeAxCapabilities(node);
|
|
1313
1164
|
const axEnabled = axCaps.enabled && axCaps.allowed.length > 0;
|
|
1314
|
-
const surfaceTitle = typeof node.data.title === 'string' && node.data.title.trim()
|
|
1315
|
-
? node.data.title
|
|
1316
|
-
: node.id;
|
|
1165
|
+
const surfaceTitle = typeof node.data.title === 'string' && node.data.title.trim() ? node.data.title : node.id;
|
|
1317
1166
|
const doc = buildHtmlSurfaceDocument(html, {
|
|
1318
1167
|
theme,
|
|
1319
1168
|
title: surfaceTitle,
|
|
@@ -1326,7 +1175,9 @@ function handleNodeSurface(pathname: string, url: URL): Response {
|
|
|
1326
1175
|
// Seed the read-side bridge with the current AX state (only for AX surfaces).
|
|
1327
1176
|
...(axEnabled ? { axState: buildCanvasAxSurfaceSnapshot() } : {}),
|
|
1328
1177
|
// Content-height reporter nonce (lets an html node grow to fit its content).
|
|
1329
|
-
...(url.searchParams.get('frameToken')
|
|
1178
|
+
...(url.searchParams.get('frameToken')
|
|
1179
|
+
? { contentHeightToken: url.searchParams.get('frameToken') as string }
|
|
1180
|
+
: {}),
|
|
1330
1181
|
});
|
|
1331
1182
|
return surfaceHtmlResponse(doc, HTML_SURFACE_SANDBOX);
|
|
1332
1183
|
}
|
|
@@ -1368,124 +1219,11 @@ function handleNodeSurface(pathname: string, url: URL): Response {
|
|
|
1368
1219
|
return responseText('Node type cannot be opened as a site', 404);
|
|
1369
1220
|
}
|
|
1370
1221
|
|
|
1371
|
-
async function handleCanvasUpdate(req: Request): Promise<Response> {
|
|
1372
|
-
const body = await readJson(req);
|
|
1373
|
-
const updates = Array.isArray(body.updates) ? body.updates : [];
|
|
1374
|
-
const result = body.recordHistory === false
|
|
1375
|
-
? (() => {
|
|
1376
|
-
let suppressedResult: ReturnType<typeof applyCanvasNodeUpdates> = { applied: 0, skipped: updates.length };
|
|
1377
|
-
canvasState.withSuppressedRecording(() => {
|
|
1378
|
-
suppressedResult = applyCanvasNodeUpdates(updates);
|
|
1379
|
-
});
|
|
1380
|
-
return suppressedResult;
|
|
1381
|
-
})()
|
|
1382
|
-
: applyCanvasNodeUpdates(updates);
|
|
1383
|
-
if (result.applied > 0) {
|
|
1384
|
-
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
1385
|
-
}
|
|
1386
|
-
return responseJson({ ok: true, ...result });
|
|
1387
|
-
}
|
|
1388
|
-
|
|
1389
|
-
async function handleCanvasViewport(req: Request): Promise<Response> {
|
|
1390
|
-
const body = await readJson(req);
|
|
1391
|
-
const next = {
|
|
1392
|
-
x: typeof body.x === 'number' ? body.x : canvasState.viewport.x,
|
|
1393
|
-
y: typeof body.y === 'number' ? body.y : canvasState.viewport.y,
|
|
1394
|
-
scale: typeof body.scale === 'number' ? body.scale : canvasState.viewport.scale,
|
|
1395
|
-
};
|
|
1396
|
-
if (body.recordHistory === false) {
|
|
1397
|
-
canvasState.withSuppressedRecording(() => {
|
|
1398
|
-
canvasState.setViewport(next);
|
|
1399
|
-
});
|
|
1400
|
-
} else {
|
|
1401
|
-
canvasState.setViewport(next);
|
|
1402
|
-
}
|
|
1403
|
-
emitPrimaryWorkbenchEvent('canvas-viewport-update', { viewport: canvasState.viewport });
|
|
1404
|
-
return responseJson({ ok: true });
|
|
1405
|
-
}
|
|
1406
|
-
|
|
1407
|
-
function annotationBounds(points: CanvasAnnotation['points']): CanvasAnnotation['bounds'] {
|
|
1408
|
-
const xs = points.map((point) => point.x);
|
|
1409
|
-
const ys = points.map((point) => point.y);
|
|
1410
|
-
const minX = Math.min(...xs);
|
|
1411
|
-
const minY = Math.min(...ys);
|
|
1412
|
-
const maxX = Math.max(...xs);
|
|
1413
|
-
const maxY = Math.max(...ys);
|
|
1414
|
-
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
|
1415
|
-
}
|
|
1416
|
-
|
|
1417
|
-
function textAnnotationBounds(point: CanvasAnnotation['points'][number], text: string, width: number): CanvasAnnotation['bounds'] {
|
|
1418
|
-
return {
|
|
1419
|
-
x: point.x,
|
|
1420
|
-
y: point.y - width,
|
|
1421
|
-
width: Math.max(width, text.length * width * 0.62),
|
|
1422
|
-
height: width * 1.2,
|
|
1423
|
-
};
|
|
1424
|
-
}
|
|
1425
|
-
|
|
1426
|
-
function parseAnnotationPoints(value: unknown): CanvasAnnotation['points'] {
|
|
1427
|
-
if (!Array.isArray(value)) return [];
|
|
1428
|
-
return value
|
|
1429
|
-
.map((point) => {
|
|
1430
|
-
if (!point || typeof point !== 'object' || Array.isArray(point)) return null;
|
|
1431
|
-
const record = point as Record<string, unknown>;
|
|
1432
|
-
if (typeof record.x !== 'number' || typeof record.y !== 'number') return null;
|
|
1433
|
-
if (!Number.isFinite(record.x) || !Number.isFinite(record.y)) return null;
|
|
1434
|
-
return { x: record.x, y: record.y };
|
|
1435
|
-
})
|
|
1436
|
-
.filter((point): point is CanvasAnnotation['points'][number] => point !== null);
|
|
1437
|
-
}
|
|
1438
|
-
|
|
1439
|
-
async function handleCanvasAddAnnotation(req: Request): Promise<Response> {
|
|
1440
|
-
const body = await readJson(req);
|
|
1441
|
-
const type = body.type === 'text' ? 'text' : 'freehand';
|
|
1442
|
-
const points = parseAnnotationPoints(body.points);
|
|
1443
|
-
if (points.length < (type === 'text' ? 1 : 2)) {
|
|
1444
|
-
return responseJson({ ok: false, error: type === 'text' ? 'Text annotation requires a valid point.' : 'Annotation requires at least two valid points.' }, 400);
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
|
-
const defaultWidth = type === 'text' ? 24 : 4;
|
|
1448
|
-
const maxWidth = type === 'text' ? 96 : 24;
|
|
1449
|
-
const width = typeof body.width === 'number' && Number.isFinite(body.width)
|
|
1450
|
-
? Math.min(maxWidth, Math.max(1, body.width))
|
|
1451
|
-
: defaultWidth;
|
|
1452
|
-
const color = typeof body.color === 'string' && (body.color === 'currentColor' || /^#[0-9a-fA-F]{6}$/.test(body.color))
|
|
1453
|
-
? body.color
|
|
1454
|
-
: 'currentColor';
|
|
1455
|
-
const label = typeof body.label === 'string' && body.label.trim().length > 0
|
|
1456
|
-
? body.label.trim().slice(0, 160)
|
|
1457
|
-
: undefined;
|
|
1458
|
-
const text = type === 'text' && typeof body.text === 'string' && body.text.trim().length > 0
|
|
1459
|
-
? body.text.trim().slice(0, 240)
|
|
1460
|
-
: undefined;
|
|
1461
|
-
if (type === 'text' && !text) {
|
|
1462
|
-
return responseJson({ ok: false, error: 'Text annotation requires text.' }, 400);
|
|
1463
|
-
}
|
|
1464
|
-
const id = typeof body.id === 'string' && body.id.trim().length > 0
|
|
1465
|
-
? body.id.trim().slice(0, 120)
|
|
1466
|
-
: `ann-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1467
|
-
const annotation: CanvasAnnotation = {
|
|
1468
|
-
id,
|
|
1469
|
-
type,
|
|
1470
|
-
points,
|
|
1471
|
-
bounds: type === 'text' ? textAnnotationBounds(points[0]!, text!, width) : annotationBounds(points),
|
|
1472
|
-
color,
|
|
1473
|
-
width,
|
|
1474
|
-
...(text ? { text } : {}),
|
|
1475
|
-
...(label ?? text ? { label: label ?? text } : {}),
|
|
1476
|
-
createdAt: new Date().toISOString(),
|
|
1477
|
-
};
|
|
1478
|
-
|
|
1479
|
-
canvasState.addAnnotation(annotation);
|
|
1480
|
-
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
1481
|
-
return responseJson({ ok: true, annotation: summarizeCanvasAnnotation(annotation) });
|
|
1482
|
-
}
|
|
1483
|
-
|
|
1484
1222
|
// ── Serve image file for image nodes ─────────────────────────
|
|
1485
1223
|
async function handleCanvasImage(pathname: string): Promise<Response> {
|
|
1486
1224
|
const nodeId = pathname.replace('/api/canvas/image/', '');
|
|
1487
1225
|
const node = canvasState.getNode(nodeId);
|
|
1488
|
-
if (
|
|
1226
|
+
if (node?.type !== 'image') {
|
|
1489
1227
|
return responseText('Image node not found', 404);
|
|
1490
1228
|
}
|
|
1491
1229
|
const src = (node.data.path as string) || (node.data.src as string) || '';
|
|
@@ -1517,43 +1255,6 @@ async function handleCanvasImage(pathname: string): Promise<Response> {
|
|
|
1517
1255
|
});
|
|
1518
1256
|
}
|
|
1519
1257
|
|
|
1520
|
-
async function handleCanvasRefreshWebpageNode(nodeId: string, req: Request): Promise<Response> {
|
|
1521
|
-
const existing = canvasState.getNode(nodeId);
|
|
1522
|
-
if (!existing || existing.type !== 'webpage') {
|
|
1523
|
-
return responseJson({ ok: false, error: `Webpage node "${nodeId}" not found.` }, 404);
|
|
1524
|
-
}
|
|
1525
|
-
|
|
1526
|
-
const body = await readJson(req);
|
|
1527
|
-
const rawUrl = typeof body.url === 'string' ? body.url : undefined;
|
|
1528
|
-
let url: string | undefined;
|
|
1529
|
-
if (rawUrl && rawUrl.trim().length > 0) {
|
|
1530
|
-
try {
|
|
1531
|
-
url = normalizeWebpageUrl(rawUrl);
|
|
1532
|
-
} catch (error) {
|
|
1533
|
-
return responseJson({ ok: false, error: error instanceof Error ? error.message : 'Invalid webpage URL.' }, 400);
|
|
1534
|
-
}
|
|
1535
|
-
}
|
|
1536
|
-
|
|
1537
|
-
const result = await refreshCanvasWebpageNode(nodeId, { ...(url ? { url } : {}) });
|
|
1538
|
-
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
1539
|
-
return responseJson(result, result.ok ? 200 : 400);
|
|
1540
|
-
}
|
|
1541
|
-
|
|
1542
|
-
// handleCanvasBuildWebArtifact migrated to the operation registry
|
|
1543
|
-
// (plan-008 Wave 4): src/server/operations/ops/app.ts (webartifact.build).
|
|
1544
|
-
|
|
1545
|
-
async function handleCanvasThemeUpdate(req: Request): Promise<Response> {
|
|
1546
|
-
const body = await readJson(req);
|
|
1547
|
-
const theme = normalizeCanvasTheme(body.theme, canvasState.theme);
|
|
1548
|
-
const next = canvasState.setTheme(theme);
|
|
1549
|
-
broadcastWorkbenchEvent('theme-changed', {
|
|
1550
|
-
theme: next,
|
|
1551
|
-
sessionId: primaryWorkbenchSessionId,
|
|
1552
|
-
timestamp: new Date().toISOString(),
|
|
1553
|
-
});
|
|
1554
|
-
return responseJson({ ok: true, theme: next });
|
|
1555
|
-
}
|
|
1556
|
-
|
|
1557
1258
|
async function handleJsonRenderView(url: URL): Promise<Response> {
|
|
1558
1259
|
const nodeId = url.searchParams.get('nodeId') ?? '';
|
|
1559
1260
|
if (!nodeId) return responseText('Missing nodeId', 400);
|
|
@@ -1574,22 +1275,18 @@ async function handleJsonRenderView(url: URL): Promise<Response> {
|
|
|
1574
1275
|
|
|
1575
1276
|
const themeValue = url.searchParams.get('theme');
|
|
1576
1277
|
const theme =
|
|
1577
|
-
themeValue === 'dark' || themeValue === 'light' || themeValue === 'high-contrast'
|
|
1578
|
-
? themeValue
|
|
1579
|
-
: undefined;
|
|
1278
|
+
themeValue === 'dark' || themeValue === 'light' || themeValue === 'high-contrast' ? themeValue : undefined;
|
|
1580
1279
|
const title = (node.data.title as string) || node.id;
|
|
1581
1280
|
// Devtools panel is double-gated: the operator must opt in via the env flag
|
|
1582
1281
|
// AND the request must carry ?devtools=1. Off by default in all normal runs.
|
|
1583
1282
|
const devtoolsEnabled =
|
|
1584
|
-
process.env.PMX_CANVAS_JSON_RENDER_DEVTOOLS === '1' &&
|
|
1585
|
-
url.searchParams.get('devtools') === '1';
|
|
1283
|
+
process.env.PMX_CANVAS_JSON_RENDER_DEVTOOLS === '1' && url.searchParams.get('devtools') === '1';
|
|
1586
1284
|
const axToken = url.searchParams.get('axToken');
|
|
1587
1285
|
const axEnabled = resolveNodeAxCapabilities(node).enabled;
|
|
1588
1286
|
const frameToken = url.searchParams.get('frameToken');
|
|
1589
1287
|
const displayParam = url.searchParams.get('display');
|
|
1590
|
-
const display =
|
|
1591
|
-
: displayParam === 'site' ? 'site' as const
|
|
1592
|
-
: undefined;
|
|
1288
|
+
const display =
|
|
1289
|
+
displayParam === 'expanded' ? ('expanded' as const) : displayParam === 'site' ? ('site' as const) : undefined;
|
|
1593
1290
|
// A standalone "site" tab fills the viewport; content-fit (grow-to-content for the
|
|
1594
1291
|
// in-canvas iframe) would fight that, so it's ignored in site mode (#65).
|
|
1595
1292
|
const fitContent = url.searchParams.get('fit') === 'content' && display !== 'site';
|
|
@@ -1671,9 +1368,7 @@ function handleArtifactView(url: URL): Response {
|
|
|
1671
1368
|
const safeNodeId = node.id.replace(/[^A-Za-z0-9_-]/g, '').slice(0, 80);
|
|
1672
1369
|
const stateJson = JSON.stringify(buildCanvasAxSurfaceSnapshot()).replace(/</g, '\\u003c');
|
|
1673
1370
|
const bridge = `${buildAxBridge(safeToken, safeNodeId)}${buildAxStateBridge(safeToken, stateJson)}`;
|
|
1674
|
-
content = content.includes('</head>')
|
|
1675
|
-
? content.replace('</head>', `${bridge}</head>`)
|
|
1676
|
-
: `${bridge}${content}`;
|
|
1371
|
+
content = content.includes('</head>') ? content.replace('</head>', `${bridge}</head>`) : `${bridge}${content}`;
|
|
1677
1372
|
}
|
|
1678
1373
|
}
|
|
1679
1374
|
// Content-height reporter so a web-artifact node grows to fit its app (#48).
|
|
@@ -1700,7 +1395,8 @@ function handleArtifactView(url: URL): Response {
|
|
|
1700
1395
|
? `<article class="markdown-body">${marked.parse(content) as string}</article>`
|
|
1701
1396
|
: `<pre class="artifact-code"><code>${escapeHtml(content)}</code></pre>`;
|
|
1702
1397
|
|
|
1703
|
-
return new Response(
|
|
1398
|
+
return new Response(
|
|
1399
|
+
`<!doctype html>
|
|
1704
1400
|
<html lang="en">
|
|
1705
1401
|
<head>
|
|
1706
1402
|
<meta charset="utf-8" />
|
|
@@ -1798,12 +1494,14 @@ function handleArtifactView(url: URL): Response {
|
|
|
1798
1494
|
<section class="panel">${body}</section>
|
|
1799
1495
|
</main>
|
|
1800
1496
|
</body>
|
|
1801
|
-
</html>`,
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1497
|
+
</html>`,
|
|
1498
|
+
{
|
|
1499
|
+
headers: {
|
|
1500
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
1501
|
+
'Cache-Control': 'no-store',
|
|
1502
|
+
},
|
|
1805
1503
|
},
|
|
1806
|
-
|
|
1504
|
+
);
|
|
1807
1505
|
}
|
|
1808
1506
|
|
|
1809
1507
|
function handleRead(pathLike: string): Response {
|
|
@@ -1821,173 +1519,6 @@ function handleRead(pathLike: string): Response {
|
|
|
1821
1519
|
});
|
|
1822
1520
|
}
|
|
1823
1521
|
|
|
1824
|
-
async function handleExtAppCallTool(req: Request): Promise<Response> {
|
|
1825
|
-
const body = await readJson(req);
|
|
1826
|
-
const sessionId = typeof body.sessionId === 'string' ? body.sessionId.trim() : '';
|
|
1827
|
-
const toolName = typeof body.toolName === 'string' ? body.toolName.trim() : '';
|
|
1828
|
-
if (!sessionId || !toolName) {
|
|
1829
|
-
return responseJson({ ok: false, error: 'Missing sessionId or toolName.' }, 400);
|
|
1830
|
-
}
|
|
1831
|
-
|
|
1832
|
-
const args =
|
|
1833
|
-
body.arguments && typeof body.arguments === 'object' && !Array.isArray(body.arguments)
|
|
1834
|
-
? body.arguments as Record<string, unknown>
|
|
1835
|
-
: undefined;
|
|
1836
|
-
const nodeId = typeof body.nodeId === 'string' ? body.nodeId.trim() : '';
|
|
1837
|
-
|
|
1838
|
-
try {
|
|
1839
|
-
const requestedNode = nodeId ? canvasState.getNode(nodeId) : undefined;
|
|
1840
|
-
const canReadLocalCheckpoint =
|
|
1841
|
-
requestedNode?.type === 'mcp-app' &&
|
|
1842
|
-
requestedNode.data.mode === 'ext-app' &&
|
|
1843
|
-
requestedNode.data.appSessionId === sessionId;
|
|
1844
|
-
const localCheckpointData = canReadLocalCheckpoint && toolName === EXCALIDRAW_READ_CHECKPOINT_TOOL
|
|
1845
|
-
? getLocalExcalidrawCheckpointData(requestedNode, args)
|
|
1846
|
-
: null;
|
|
1847
|
-
const result = localCheckpointData === null
|
|
1848
|
-
? await callMcpAppTool(sessionId, toolName, args)
|
|
1849
|
-
: { content: [{ type: 'text', text: localCheckpointData }] } satisfies CallToolResult;
|
|
1850
|
-
if (nodeId) {
|
|
1851
|
-
const node = canvasState.getNode(nodeId);
|
|
1852
|
-
if (node?.type === 'mcp-app' && node.data.mode === 'ext-app' && node.data.appSessionId === sessionId) {
|
|
1853
|
-
let changed = false;
|
|
1854
|
-
if (toolName === EXCALIDRAW_SAVE_CHECKPOINT_TOOL && persistExcalidrawCheckpointToNode(nodeId, node, args)) {
|
|
1855
|
-
// Checkpoint saves are replayed through toolInput.elements instead of
|
|
1856
|
-
// replacing the original create_view result with a generic "ok".
|
|
1857
|
-
changed = true;
|
|
1858
|
-
} else if (!(isExcalidrawCreateView(node.data.serverName, node.data.toolName) && isCheckpointToolName(toolName))) {
|
|
1859
|
-
const nextData: Record<string, unknown> = { ...node.data };
|
|
1860
|
-
if (shouldReplayAppToolResult(toolName, result)) nextData.toolResult = result;
|
|
1861
|
-
const nextModelContext: Record<string, unknown> = {};
|
|
1862
|
-
if (Array.isArray(result.content)) {
|
|
1863
|
-
nextModelContext.content = result.content;
|
|
1864
|
-
}
|
|
1865
|
-
if (result.structuredContent && typeof result.structuredContent === 'object' && !Array.isArray(result.structuredContent)) {
|
|
1866
|
-
nextModelContext.structuredContent = result.structuredContent;
|
|
1867
|
-
}
|
|
1868
|
-
if (Object.keys(nextModelContext).length > 0) {
|
|
1869
|
-
nextData.appModelContext = {
|
|
1870
|
-
...nextModelContext,
|
|
1871
|
-
updatedAt: new Date().toISOString(),
|
|
1872
|
-
};
|
|
1873
|
-
}
|
|
1874
|
-
canvasState.updateNode(nodeId, {
|
|
1875
|
-
data: nextData,
|
|
1876
|
-
});
|
|
1877
|
-
changed = true;
|
|
1878
|
-
}
|
|
1879
|
-
if (changed) {
|
|
1880
|
-
broadcastWorkbenchEvent('canvas-layout-update', {
|
|
1881
|
-
layout: canvasState.getLayout(),
|
|
1882
|
-
sessionId: primaryWorkbenchSessionId,
|
|
1883
|
-
timestamp: new Date().toISOString(),
|
|
1884
|
-
});
|
|
1885
|
-
}
|
|
1886
|
-
}
|
|
1887
|
-
}
|
|
1888
|
-
return responseJson({ ok: true, result });
|
|
1889
|
-
} catch (error) {
|
|
1890
|
-
return responseJson({ ok: false, error: error instanceof Error ? error.message : String(error) }, 400);
|
|
1891
|
-
}
|
|
1892
|
-
}
|
|
1893
|
-
|
|
1894
|
-
async function handleExtAppReadResource(req: Request): Promise<Response> {
|
|
1895
|
-
const body = await readJson(req);
|
|
1896
|
-
const sessionId = typeof body.sessionId === 'string' ? body.sessionId.trim() : '';
|
|
1897
|
-
const uri = typeof body.uri === 'string' ? body.uri.trim() : '';
|
|
1898
|
-
if (!sessionId || !uri) {
|
|
1899
|
-
return responseJson({ ok: false, error: 'Missing sessionId or uri.' }, 400);
|
|
1900
|
-
}
|
|
1901
|
-
|
|
1902
|
-
try {
|
|
1903
|
-
const result = await readMcpAppResource(sessionId, uri);
|
|
1904
|
-
return responseJson({ ok: true, result });
|
|
1905
|
-
} catch (error) {
|
|
1906
|
-
return responseJson({ ok: false, error: error instanceof Error ? error.message : String(error) }, 400);
|
|
1907
|
-
}
|
|
1908
|
-
}
|
|
1909
|
-
|
|
1910
|
-
async function handleExtAppListTools(req: Request): Promise<Response> {
|
|
1911
|
-
const body = await readJson(req);
|
|
1912
|
-
const sessionId = typeof body.sessionId === 'string' ? body.sessionId.trim() : '';
|
|
1913
|
-
if (!sessionId) return responseJson({ ok: false, error: 'Missing sessionId.' }, 400);
|
|
1914
|
-
|
|
1915
|
-
try {
|
|
1916
|
-
const result: ListToolsResult = await listMcpAppTools(sessionId);
|
|
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 handleExtAppListResources(req: Request): Promise<Response> {
|
|
1924
|
-
const body = await readJson(req);
|
|
1925
|
-
const sessionId = typeof body.sessionId === 'string' ? body.sessionId.trim() : '';
|
|
1926
|
-
if (!sessionId) return responseJson({ ok: false, error: 'Missing sessionId.' }, 400);
|
|
1927
|
-
|
|
1928
|
-
try {
|
|
1929
|
-
const result: ListResourcesResult = await listMcpAppResources(sessionId);
|
|
1930
|
-
return responseJson({ ok: true, result });
|
|
1931
|
-
} catch (error) {
|
|
1932
|
-
return responseJson({ ok: false, error: error instanceof Error ? error.message : String(error) }, 400);
|
|
1933
|
-
}
|
|
1934
|
-
}
|
|
1935
|
-
|
|
1936
|
-
async function handleExtAppListResourceTemplates(req: Request): Promise<Response> {
|
|
1937
|
-
const body = await readJson(req);
|
|
1938
|
-
const sessionId = typeof body.sessionId === 'string' ? body.sessionId.trim() : '';
|
|
1939
|
-
if (!sessionId) return responseJson({ ok: false, error: 'Missing sessionId.' }, 400);
|
|
1940
|
-
|
|
1941
|
-
try {
|
|
1942
|
-
const result: ListResourceTemplatesResult = await listMcpAppResourceTemplates(sessionId);
|
|
1943
|
-
return responseJson({ ok: true, result });
|
|
1944
|
-
} catch (error) {
|
|
1945
|
-
return responseJson({ ok: false, error: error instanceof Error ? error.message : String(error) }, 400);
|
|
1946
|
-
}
|
|
1947
|
-
}
|
|
1948
|
-
|
|
1949
|
-
async function handleExtAppListPrompts(req: Request): Promise<Response> {
|
|
1950
|
-
const body = await readJson(req);
|
|
1951
|
-
const sessionId = typeof body.sessionId === 'string' ? body.sessionId.trim() : '';
|
|
1952
|
-
if (!sessionId) return responseJson({ ok: false, error: 'Missing sessionId.' }, 400);
|
|
1953
|
-
|
|
1954
|
-
try {
|
|
1955
|
-
const result: ListPromptsResult = await listMcpAppPrompts(sessionId);
|
|
1956
|
-
return responseJson({ ok: true, result });
|
|
1957
|
-
} catch (error) {
|
|
1958
|
-
return responseJson({ ok: false, error: error instanceof Error ? error.message : String(error) }, 400);
|
|
1959
|
-
}
|
|
1960
|
-
}
|
|
1961
|
-
|
|
1962
|
-
async function handleExtAppModelContext(req: Request): Promise<Response> {
|
|
1963
|
-
const body = await readJson(req);
|
|
1964
|
-
const nodeId = typeof body.nodeId === 'string' ? body.nodeId.trim() : '';
|
|
1965
|
-
if (!nodeId) return responseJson({ ok: false, error: 'Missing nodeId.' }, 400);
|
|
1966
|
-
|
|
1967
|
-
const node = canvasState.getNode(nodeId);
|
|
1968
|
-
if (!node) return responseJson({ ok: false, error: `Node "${nodeId}" not found.` }, 404);
|
|
1969
|
-
|
|
1970
|
-
canvasState.updateNode(nodeId, {
|
|
1971
|
-
data: {
|
|
1972
|
-
...node.data,
|
|
1973
|
-
appModelContext: {
|
|
1974
|
-
...(Array.isArray(body.content) ? { content: body.content } : {}),
|
|
1975
|
-
...(body.structuredContent && typeof body.structuredContent === 'object' && !Array.isArray(body.structuredContent)
|
|
1976
|
-
? { structuredContent: body.structuredContent }
|
|
1977
|
-
: {}),
|
|
1978
|
-
updatedAt: new Date().toISOString(),
|
|
1979
|
-
},
|
|
1980
|
-
},
|
|
1981
|
-
});
|
|
1982
|
-
|
|
1983
|
-
broadcastWorkbenchEvent('canvas-layout-update', {
|
|
1984
|
-
layout: canvasState.getLayout(),
|
|
1985
|
-
sessionId: primaryWorkbenchSessionId,
|
|
1986
|
-
timestamp: new Date().toISOString(),
|
|
1987
|
-
});
|
|
1988
|
-
return responseJson({ ok: true });
|
|
1989
|
-
}
|
|
1990
|
-
|
|
1991
1522
|
function handleWorkbenchState(): Response {
|
|
1992
1523
|
const mcpAppHost = getMcpAppHostSnapshot();
|
|
1993
1524
|
if (!primaryWorkbenchPath) {
|
|
@@ -2033,9 +1564,8 @@ function currentWorkbenchUrl(): string | null {
|
|
|
2033
1564
|
|
|
2034
1565
|
async function handleWorkbenchWebViewScreenshot(req: Request): Promise<Response> {
|
|
2035
1566
|
const body = await readJson(req);
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
: 'png';
|
|
1567
|
+
if (body === null) return malformedJsonResponse();
|
|
1568
|
+
const format = body.format === 'jpeg' || body.format === 'webp' || body.format === 'png' ? body.format : 'png';
|
|
2039
1569
|
const quality = typeof body.quality === 'number' ? body.quality : undefined;
|
|
2040
1570
|
|
|
2041
1571
|
try {
|
|
@@ -2044,11 +1574,7 @@ async function handleWorkbenchWebViewScreenshot(req: Request): Promise<Response>
|
|
|
2044
1574
|
...(quality !== undefined ? { quality } : {}),
|
|
2045
1575
|
});
|
|
2046
1576
|
const responseBytes = Uint8Array.from(bytes);
|
|
2047
|
-
const mimeType = format === 'jpeg'
|
|
2048
|
-
? 'image/jpeg'
|
|
2049
|
-
: format === 'webp'
|
|
2050
|
-
? 'image/webp'
|
|
2051
|
-
: 'image/png';
|
|
1577
|
+
const mimeType = format === 'jpeg' ? 'image/jpeg' : format === 'webp' ? 'image/webp' : 'image/png';
|
|
2052
1578
|
return new Response(responseBytes.buffer, {
|
|
2053
1579
|
headers: {
|
|
2054
1580
|
'Content-Type': mimeType,
|
|
@@ -2063,6 +1589,7 @@ async function handleWorkbenchWebViewScreenshot(req: Request): Promise<Response>
|
|
|
2063
1589
|
|
|
2064
1590
|
async function handleWorkbenchOpen(req: Request): Promise<Response> {
|
|
2065
1591
|
const body = await readJson(req);
|
|
1592
|
+
if (body === null) return malformedJsonResponse();
|
|
2066
1593
|
const pathLike = typeof body.path === 'string' ? body.path : '';
|
|
2067
1594
|
const safePath = resolveWorkspaceMarkdownPath(pathLike);
|
|
2068
1595
|
if (!safePath) return responseText('Invalid path', 400);
|
|
@@ -2074,6 +1601,7 @@ async function handleWorkbenchOpen(req: Request): Promise<Response> {
|
|
|
2074
1601
|
|
|
2075
1602
|
async function handleWorkbenchIntent(req: Request): Promise<Response> {
|
|
2076
1603
|
const body = await readJson(req);
|
|
1604
|
+
if (body === null) return malformedJsonResponse();
|
|
2077
1605
|
const rawType = typeof body.type === 'string' ? body.type.trim() : '';
|
|
2078
1606
|
if (!rawType) return responseText('Missing intent type', 400);
|
|
2079
1607
|
if (!ALLOWED_WORKBENCH_INTENTS.has(rawType as PrimaryWorkbenchIntent['type'])) {
|
|
@@ -2081,10 +1609,7 @@ async function handleWorkbenchIntent(req: Request): Promise<Response> {
|
|
|
2081
1609
|
}
|
|
2082
1610
|
|
|
2083
1611
|
const rawPayload = body.payload;
|
|
2084
|
-
const payload =
|
|
2085
|
-
rawPayload && typeof rawPayload === 'object'
|
|
2086
|
-
? (rawPayload as PrimaryWorkbenchEventPayload)
|
|
2087
|
-
: {};
|
|
1612
|
+
const payload = rawPayload && typeof rawPayload === 'object' ? (rawPayload as PrimaryWorkbenchEventPayload) : {};
|
|
2088
1613
|
|
|
2089
1614
|
// Handle trace intents directly on the server
|
|
2090
1615
|
if (rawType === 'trace-toggle') {
|
|
@@ -2234,6 +1759,7 @@ function handleWorkbenchEvents(req: Request): Response {
|
|
|
2234
1759
|
|
|
2235
1760
|
async function handleSave(req: Request): Promise<Response> {
|
|
2236
1761
|
const body = await readJson(req);
|
|
1762
|
+
if (body === null) return malformedJsonResponse();
|
|
2237
1763
|
const pathLike = typeof body.path === 'string' ? body.path : '';
|
|
2238
1764
|
const safePath = resolveWorkspaceMarkdownPath(pathLike);
|
|
2239
1765
|
if (!safePath) return responseText('Invalid path', 400);
|
|
@@ -2250,6 +1776,7 @@ async function handleSave(req: Request): Promise<Response> {
|
|
|
2250
1776
|
|
|
2251
1777
|
async function handleRender(req: Request): Promise<Response> {
|
|
2252
1778
|
const body = await readJson(req);
|
|
1779
|
+
if (body === null) return malformedJsonResponse();
|
|
2253
1780
|
const markdown = typeof body.markdown === 'string' ? body.markdown : '';
|
|
2254
1781
|
const html =
|
|
2255
1782
|
(marked.parse(normalizeMarkdownExternalUrls(markdown), {
|
|
@@ -2271,6 +1798,7 @@ function buildSelectionContextPreamble(contextNodeIds: string[]): string {
|
|
|
2271
1798
|
|
|
2272
1799
|
async function handleCanvasPrompt(req: Request): Promise<Response> {
|
|
2273
1800
|
const body = await readJson(req);
|
|
1801
|
+
if (body === null) return malformedJsonResponse();
|
|
2274
1802
|
const text = typeof body.text === 'string' ? body.text.trim() : '';
|
|
2275
1803
|
if (!text) return responseText('Missing prompt text', 400);
|
|
2276
1804
|
|
|
@@ -2279,10 +1807,7 @@ async function handleCanvasPrompt(req: Request): Promise<Response> {
|
|
|
2279
1807
|
const parentNodeId = typeof body.parentNodeId === 'string' ? body.parentNodeId : undefined;
|
|
2280
1808
|
const MAX_CONTEXT_NODES = 20;
|
|
2281
1809
|
let contextNodeIds = Array.isArray(body.contextNodeIds)
|
|
2282
|
-
? (body.contextNodeIds.filter((id: unknown) => typeof id === 'string') as string[]).slice(
|
|
2283
|
-
0,
|
|
2284
|
-
MAX_CONTEXT_NODES,
|
|
2285
|
-
)
|
|
1810
|
+
? (body.contextNodeIds.filter((id: unknown) => typeof id === 'string') as string[]).slice(0, MAX_CONTEXT_NODES)
|
|
2286
1811
|
: [];
|
|
2287
1812
|
|
|
2288
1813
|
if (contextNodeIds.length === 0 && canvasState.contextPinnedNodeIds.size > 0) {
|
|
@@ -2341,9 +1866,7 @@ async function handleCanvasPrompt(req: Request): Promise<Response> {
|
|
|
2341
1866
|
}
|
|
2342
1867
|
|
|
2343
1868
|
const MAX_THREAD_TURNS = 100;
|
|
2344
|
-
const existingTurnCount = Array.isArray(threadNode.data.turns)
|
|
2345
|
-
? (threadNode.data.turns as unknown[]).length
|
|
2346
|
-
: 0;
|
|
1869
|
+
const existingTurnCount = Array.isArray(threadNode.data.turns) ? (threadNode.data.turns as unknown[]).length : 0;
|
|
2347
1870
|
if (existingTurnCount >= MAX_THREAD_TURNS) {
|
|
2348
1871
|
return responseText('Thread has reached the maximum number of turns', 400);
|
|
2349
1872
|
}
|
|
@@ -2418,9 +1941,7 @@ async function handleCanvasPrompt(req: Request): Promise<Response> {
|
|
|
2418
1941
|
const suffix = Math.random().toString(36).slice(2, 8);
|
|
2419
1942
|
const nodeId = `prompt-${Date.now()}-${suffix}`;
|
|
2420
1943
|
|
|
2421
|
-
const promptCount = canvasState
|
|
2422
|
-
.getLayout()
|
|
2423
|
-
.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;
|
|
2424
1945
|
const pos = position ?? { x: 380 + promptCount * 30, y: 1260 + promptCount * 30 };
|
|
2425
1946
|
|
|
2426
1947
|
let enrichedText = text;
|
|
@@ -2514,134 +2035,6 @@ async function handleCanvasPrompt(req: Request): Promise<Response> {
|
|
|
2514
2035
|
return responseJson({ ok: true, nodeId });
|
|
2515
2036
|
}
|
|
2516
2037
|
|
|
2517
|
-
function handleGetPinnedContext(): Response {
|
|
2518
|
-
const pinnedIds = Array.from(canvasState.contextPinnedNodeIds);
|
|
2519
|
-
const preamble = pinnedIds.length > 0 ? buildSelectionContextPreamble(pinnedIds) : '';
|
|
2520
|
-
const nodes = pinnedIds
|
|
2521
|
-
.map((id) => canvasState.getNode(id))
|
|
2522
|
-
.filter((node): node is CanvasNodeState => node !== undefined)
|
|
2523
|
-
.map((node) => serializeNodeForAgentContext(node, {
|
|
2524
|
-
defaultTextLength: 700,
|
|
2525
|
-
webpageTextLength: 1600,
|
|
2526
|
-
includePosition: true,
|
|
2527
|
-
}));
|
|
2528
|
-
return responseJson({ preamble, nodeIds: pinnedIds, count: pinnedIds.length, nodes });
|
|
2529
|
-
}
|
|
2530
|
-
|
|
2531
|
-
function normalizeAxNodeIds(value: unknown): string[] {
|
|
2532
|
-
if (!Array.isArray(value)) return [];
|
|
2533
|
-
return value.filter((id): id is string => typeof id === 'string');
|
|
2534
|
-
}
|
|
2535
|
-
|
|
2536
|
-
function normalizeAxSource(value: unknown, fallback: PmxAxSource): PmxAxSource {
|
|
2537
|
-
return value === 'agent' ||
|
|
2538
|
-
value === 'api' ||
|
|
2539
|
-
value === 'browser' ||
|
|
2540
|
-
value === 'cli' ||
|
|
2541
|
-
value === 'codex' ||
|
|
2542
|
-
value === 'copilot' ||
|
|
2543
|
-
value === 'mcp' ||
|
|
2544
|
-
value === 'sdk' ||
|
|
2545
|
-
value === 'system'
|
|
2546
|
-
? value
|
|
2547
|
-
: fallback;
|
|
2548
|
-
}
|
|
2549
|
-
|
|
2550
|
-
function handleGetAxContext(url: URL): Response {
|
|
2551
|
-
// Optional ?consumer= filters the compact `delivery` lead block (loop-safe — a
|
|
2552
|
-
// consumer never sees steering/activity it originated), so a host adapter can
|
|
2553
|
-
// inject its own un-truncated pending block per turn (report #54 hardening).
|
|
2554
|
-
const consumer = url.searchParams.get('consumer') ?? undefined;
|
|
2555
|
-
return responseJson(buildCanvasAxContext(consumer));
|
|
2556
|
-
}
|
|
2557
|
-
|
|
2558
|
-
function isReviewSeverity(v: unknown): v is PmxAxReviewSeverity {
|
|
2559
|
-
return v === 'info' || v === 'warning' || v === 'error';
|
|
2560
|
-
}
|
|
2561
|
-
function isReviewKind(v: unknown): v is PmxAxReviewKind {
|
|
2562
|
-
return v === 'comment' || v === 'finding';
|
|
2563
|
-
}
|
|
2564
|
-
function isReviewAnchor(v: unknown): v is PmxAxReviewAnchorType {
|
|
2565
|
-
return v === 'node' || v === 'file' || v === 'region';
|
|
2566
|
-
}
|
|
2567
|
-
|
|
2568
|
-
// Validate untrusted activity `reactions` from an HTTP body into the typed override
|
|
2569
|
-
// shape ingestActivity expects. `false` suppresses a default reaction; an object
|
|
2570
|
-
// overrides its fields (invalid fields are dropped, not stored raw).
|
|
2571
|
-
function normalizeActivityReactions(input: Record<string, unknown>): {
|
|
2572
|
-
workItem?: false | { status?: PmxAxWorkItemStatus; detail?: string | null };
|
|
2573
|
-
evidence?: false | { kind?: PmxAxEvidenceKind; body?: string | null };
|
|
2574
|
-
review?: false | { severity?: PmxAxReviewSeverity; kind?: PmxAxReviewKind; anchorType?: PmxAxReviewAnchorType; nodeId?: string | null };
|
|
2575
|
-
} {
|
|
2576
|
-
const out: ReturnType<typeof normalizeActivityReactions> = {};
|
|
2577
|
-
if (input.workItem === false) out.workItem = false;
|
|
2578
|
-
else if (isRecord(input.workItem)) {
|
|
2579
|
-
const status = normalizeAxWorkItemStatus(input.workItem.status);
|
|
2580
|
-
out.workItem = {
|
|
2581
|
-
...(status ? { status } : {}),
|
|
2582
|
-
...(typeof input.workItem.detail === 'string' ? { detail: input.workItem.detail } : {}),
|
|
2583
|
-
};
|
|
2584
|
-
}
|
|
2585
|
-
if (input.evidence === false) out.evidence = false;
|
|
2586
|
-
else if (isRecord(input.evidence)) {
|
|
2587
|
-
out.evidence = {
|
|
2588
|
-
...(isAxEvidenceKind(input.evidence.kind) ? { kind: input.evidence.kind } : {}),
|
|
2589
|
-
...(typeof input.evidence.body === 'string' ? { body: input.evidence.body } : {}),
|
|
2590
|
-
};
|
|
2591
|
-
}
|
|
2592
|
-
if (input.review === false) out.review = false;
|
|
2593
|
-
else if (isRecord(input.review)) {
|
|
2594
|
-
out.review = {
|
|
2595
|
-
...(isReviewSeverity(input.review.severity) ? { severity: input.review.severity } : {}),
|
|
2596
|
-
...(isReviewKind(input.review.kind) ? { kind: input.review.kind } : {}),
|
|
2597
|
-
...(isReviewAnchor(input.review.anchorType) ? { anchorType: input.review.anchorType } : {}),
|
|
2598
|
-
...(typeof input.review.nodeId === 'string' ? { nodeId: input.review.nodeId } : {}),
|
|
2599
|
-
};
|
|
2600
|
-
}
|
|
2601
|
-
return out;
|
|
2602
|
-
}
|
|
2603
|
-
|
|
2604
|
-
// Report primitive A: ingest a harness-forwarded agent activity; the board auto-reacts.
|
|
2605
|
-
async function handleAxActivityIngest(req: Request): Promise<Response> {
|
|
2606
|
-
const body = await readJson(req);
|
|
2607
|
-
if (!isAxActivityKind(body.kind)) {
|
|
2608
|
-
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);
|
|
2609
|
-
}
|
|
2610
|
-
if (typeof body.title !== 'string' || !body.title.trim()) {
|
|
2611
|
-
return responseJson({ ok: false, error: 'activity requires a title.' }, 400);
|
|
2612
|
-
}
|
|
2613
|
-
const result = canvasState.ingestActivity(
|
|
2614
|
-
{
|
|
2615
|
-
kind: body.kind,
|
|
2616
|
-
title: body.title,
|
|
2617
|
-
...(typeof body.summary === 'string' ? { summary: body.summary } : {}),
|
|
2618
|
-
...(body.outcome === 'success' || body.outcome === 'failure' ? { outcome: body.outcome } : {}),
|
|
2619
|
-
...(typeof body.ref === 'string' ? { ref: body.ref } : {}),
|
|
2620
|
-
...(Array.isArray(body.nodeIds) ? { nodeIds: normalizeAxNodeIds(body.nodeIds) } : {}),
|
|
2621
|
-
...(isRecord(body.data) ? { data: body.data } : {}),
|
|
2622
|
-
...(isRecord(body.reactions) ? { reactions: normalizeActivityReactions(body.reactions) } : {}),
|
|
2623
|
-
},
|
|
2624
|
-
{ source: normalizeAxSource(body.source, 'api') },
|
|
2625
|
-
);
|
|
2626
|
-
const meta = { sessionId: primaryWorkbenchSessionId, timestamp: new Date().toISOString() };
|
|
2627
|
-
broadcastWorkbenchEvent('ax-event-created', { event: result.event, ...meta });
|
|
2628
|
-
if (result.workItem) broadcastWorkbenchEvent('ax-state-changed', { workItem: result.workItem, ...meta });
|
|
2629
|
-
if (result.evidence) broadcastWorkbenchEvent('ax-event-created', { evidence: result.evidence, ...meta });
|
|
2630
|
-
if (result.review) broadcastWorkbenchEvent('ax-state-changed', { reviewAnnotation: result.review, ...meta });
|
|
2631
|
-
return responseJson({ ok: true, ...result });
|
|
2632
|
-
}
|
|
2633
|
-
|
|
2634
|
-
// Report primitive D single-item gate reads (GET /api/canvas/ax/{approval,
|
|
2635
|
-
// elicitation,mode}/:id) with the optional ?waitMs= long-poll migrated to the
|
|
2636
|
-
// operation registry (plan-007 Slice B wave 4):
|
|
2637
|
-
// src/server/operations/ops/ax-await.ts.
|
|
2638
|
-
|
|
2639
|
-
// Compact AX state for surfaces (the same shape seeded into AX-enabled iframes).
|
|
2640
|
-
// The client fetches this and pushes it to surfaces over the ax-update channel.
|
|
2641
|
-
function handleGetAxSurfaceSnapshot(): Response {
|
|
2642
|
-
return responseJson(buildCanvasAxSurfaceSnapshot());
|
|
2643
|
-
}
|
|
2644
|
-
|
|
2645
2038
|
// Open a node's surface in the user's real system browser (for hosts whose
|
|
2646
2039
|
// embedded browser makes window.open('_blank') feel in-place, e.g. Codex).
|
|
2647
2040
|
// Accepts ONLY { nodeId, url? } and opens this server's own surface URL — never
|
|
@@ -2650,6 +2043,7 @@ function handleGetAxSurfaceSnapshot(): Response {
|
|
|
2650
2043
|
// safe presentation query params like the current theme.
|
|
2651
2044
|
async function handleOpenExternalSurface(req: Request): Promise<Response> {
|
|
2652
2045
|
const body = await readJson(req);
|
|
2046
|
+
if (body === null) return malformedJsonResponse();
|
|
2653
2047
|
const nodeId = typeof body.nodeId === 'string' ? body.nodeId : '';
|
|
2654
2048
|
if (!nodeId) return responseJson({ ok: false, error: 'nodeId is required.' }, 400);
|
|
2655
2049
|
const node = canvasState.getNode(nodeId);
|
|
@@ -2668,106 +2062,6 @@ async function handleOpenExternalSurface(req: Request): Promise<Response> {
|
|
|
2668
2062
|
return responseJson({ ok: true, opened, url: surfacePath });
|
|
2669
2063
|
}
|
|
2670
2064
|
|
|
2671
|
-
async function handleAxInteraction(req: Request): Promise<Response> {
|
|
2672
|
-
const body = await readJson(req);
|
|
2673
|
-
const { result, events } = applyAxInteraction(canvasState, body, normalizeAxSource(body.source, 'api'));
|
|
2674
|
-
for (const e of events) {
|
|
2675
|
-
broadcastWorkbenchEvent(e.event, {
|
|
2676
|
-
...e.payload,
|
|
2677
|
-
sessionId: primaryWorkbenchSessionId,
|
|
2678
|
-
timestamp: new Date().toISOString(),
|
|
2679
|
-
});
|
|
2680
|
-
}
|
|
2681
|
-
return responseJson(result, result.ok ? 200 : result.status);
|
|
2682
|
-
}
|
|
2683
|
-
|
|
2684
|
-
// handleAxDeliveryPending / handleAxDeliveryMark migrated to the operation
|
|
2685
|
-
// registry (plan-007 Slice B wave 3): src/server/operations/ops/ax-timeline.ts.
|
|
2686
|
-
|
|
2687
|
-
function handleAxElicitationList(): Response {
|
|
2688
|
-
return responseJson({ ok: true, elicitations: canvasState.getElicitations() });
|
|
2689
|
-
}
|
|
2690
|
-
|
|
2691
|
-
// handleAxElicitationRequest / handleAxElicitationRespond migrated to the
|
|
2692
|
-
// operation registry (plan-007 Slice B wave 2): src/server/operations/ops/ax-work.ts.
|
|
2693
|
-
|
|
2694
|
-
function handleAxModeList(): Response {
|
|
2695
|
-
return responseJson({ ok: true, modeRequests: canvasState.getModeRequests() });
|
|
2696
|
-
}
|
|
2697
|
-
|
|
2698
|
-
// handleAxModeRequest / handleAxModeResolve migrated to the operation registry
|
|
2699
|
-
// (plan-007 Slice B wave 2): src/server/operations/ops/ax-work.ts.
|
|
2700
|
-
|
|
2701
|
-
function handleAxCommandList(): Response {
|
|
2702
|
-
return responseJson({ ok: true, commands: canvasState.getCommandRegistry() });
|
|
2703
|
-
}
|
|
2704
|
-
|
|
2705
|
-
// handleAxCommandInvoke migrated to the operation registry (plan-007 Slice B
|
|
2706
|
-
// wave 3): src/server/operations/ops/ax-timeline.ts.
|
|
2707
|
-
|
|
2708
|
-
function handleAxPolicyGet(): Response {
|
|
2709
|
-
return responseJson({ ok: true, policy: canvasState.getPolicy() });
|
|
2710
|
-
}
|
|
2711
|
-
|
|
2712
|
-
async function handleAxStatePatch(req: Request): Promise<Response> {
|
|
2713
|
-
const body = await readJson(req);
|
|
2714
|
-
if (!body.focus || typeof body.focus !== 'object' || Array.isArray(body.focus)) {
|
|
2715
|
-
return responseJson({ ok: false, error: 'PATCH /api/canvas/ax currently requires a focus object.' }, 400);
|
|
2716
|
-
}
|
|
2717
|
-
const focusInput = body.focus as Record<string, unknown>;
|
|
2718
|
-
const focus = canvasState.setAxFocus(normalizeAxNodeIds(focusInput.nodeIds), {
|
|
2719
|
-
source: normalizeAxSource(focusInput.source, 'api'),
|
|
2720
|
-
});
|
|
2721
|
-
broadcastWorkbenchEvent('ax-state-changed', {
|
|
2722
|
-
focus,
|
|
2723
|
-
sessionId: primaryWorkbenchSessionId,
|
|
2724
|
-
timestamp: new Date().toISOString(),
|
|
2725
|
-
});
|
|
2726
|
-
return responseJson({ ok: true, state: canvasState.getAxState() });
|
|
2727
|
-
}
|
|
2728
|
-
|
|
2729
|
-
// handleAxEventAdd / handleAxSteer / handleAxTimelineGet migrated to the
|
|
2730
|
-
// operation registry (plan-007 Slice B wave 3): src/server/operations/ops/ax-timeline.ts.
|
|
2731
|
-
|
|
2732
|
-
const AX_WORK_STATUSES = new Set(['todo', 'in-progress', 'blocked', 'done', 'cancelled']);
|
|
2733
|
-
|
|
2734
|
-
function normalizeAxWorkItemStatus(value: unknown): 'todo' | 'in-progress' | 'blocked' | 'done' | 'cancelled' | undefined {
|
|
2735
|
-
return typeof value === 'string' && AX_WORK_STATUSES.has(value)
|
|
2736
|
-
? value as 'todo' | 'in-progress' | 'blocked' | 'done' | 'cancelled'
|
|
2737
|
-
: undefined;
|
|
2738
|
-
}
|
|
2739
|
-
|
|
2740
|
-
function handleAxWorkList(): Response {
|
|
2741
|
-
return responseJson({ ok: true, workItems: canvasState.getWorkItems() });
|
|
2742
|
-
}
|
|
2743
|
-
|
|
2744
|
-
// handleAxWorkAdd / handleAxWorkUpdate migrated to the operation registry
|
|
2745
|
-
// (plan-007 Slice B wave 2): src/server/operations/ops/ax-work.ts.
|
|
2746
|
-
|
|
2747
|
-
function handleAxApprovalList(): Response {
|
|
2748
|
-
return responseJson({ ok: true, approvalGates: canvasState.getApprovalGates() });
|
|
2749
|
-
}
|
|
2750
|
-
|
|
2751
|
-
// handleAxApprovalRequest / handleAxApprovalResolve migrated to the operation
|
|
2752
|
-
// registry (plan-007 Slice B wave 2): src/server/operations/ops/ax-work.ts.
|
|
2753
|
-
|
|
2754
|
-
// handleAxEvidenceAdd migrated to the operation registry (plan-007 Slice B
|
|
2755
|
-
// wave 3): src/server/operations/ops/ax-timeline.ts.
|
|
2756
|
-
|
|
2757
|
-
// The AX review normalize helpers + their constant sets moved with the
|
|
2758
|
-
// migrated handlers (plan-007 Slice B wave 2): src/server/operations/ops/ax-work.ts.
|
|
2759
|
-
|
|
2760
|
-
function handleAxReviewList(): Response {
|
|
2761
|
-
return responseJson({ ok: true, reviewAnnotations: canvasState.getReviewAnnotations() });
|
|
2762
|
-
}
|
|
2763
|
-
|
|
2764
|
-
// handleAxReviewAdd / handleAxReviewUpdate migrated to the operation registry
|
|
2765
|
-
// (plan-007 Slice B wave 2): src/server/operations/ops/ax-work.ts.
|
|
2766
|
-
|
|
2767
|
-
function handleAxHostCapabilityGet(): Response {
|
|
2768
|
-
return responseJson({ ok: true, host: canvasState.getHostCapability() });
|
|
2769
|
-
}
|
|
2770
|
-
|
|
2771
2065
|
// ── Port resolution ───────────────────────────────────────────
|
|
2772
2066
|
|
|
2773
2067
|
function buildPortCandidates(preferredPort: number): number[] {
|
|
@@ -2902,11 +2196,9 @@ export function buildMacBrowserOpenScript(appName: string, url: string): string
|
|
|
2902
2196
|
}
|
|
2903
2197
|
|
|
2904
2198
|
function resolveWindowsBrowserForCanvas(): { name: string; exe: string } | null {
|
|
2905
|
-
const envDirs = [
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
process.env.LOCALAPPDATA,
|
|
2909
|
-
].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
|
+
);
|
|
2910
2202
|
|
|
2911
2203
|
const browsers = [
|
|
2912
2204
|
{ name: 'Edge', subpath: join('Microsoft', 'Edge', 'Application', 'msedge.exe') },
|
|
@@ -2967,8 +2259,7 @@ function syncContextNodeToCanvasState(
|
|
|
2967
2259
|
mergedData.messagesLength !== undefined ||
|
|
2968
2260
|
mergedData.utilization !== undefined ||
|
|
2969
2261
|
mergedData.nearLimit !== undefined;
|
|
2970
|
-
const shouldCreate =
|
|
2971
|
-
options.forceCreate === true || cards.length > 0 || auxTabs.length > 0 || hasUsage;
|
|
2262
|
+
const shouldCreate = options.forceCreate === true || cards.length > 0 || auxTabs.length > 0 || hasUsage;
|
|
2972
2263
|
|
|
2973
2264
|
if (!existing) {
|
|
2974
2265
|
if (!shouldCreate) return;
|
|
@@ -3077,16 +2368,17 @@ function syncEventToCanvasState(event: string, payload: PrimaryWorkbenchEventPay
|
|
|
3077
2368
|
} else if (event === 'ext-app-open') {
|
|
3078
2369
|
const toolCallId = payload.toolCallId as string;
|
|
3079
2370
|
if (!toolCallId) return;
|
|
3080
|
-
const id =
|
|
3081
|
-
|
|
3082
|
-
|
|
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}`;
|
|
3083
2377
|
const dataPatch = {
|
|
3084
2378
|
mode: 'ext-app',
|
|
3085
2379
|
toolCallId,
|
|
3086
2380
|
nodeId: id,
|
|
3087
|
-
...(typeof payload.title === 'string' && payload.title.trim().length > 0
|
|
3088
|
-
? { title: payload.title.trim() }
|
|
3089
|
-
: {}),
|
|
2381
|
+
...(typeof payload.title === 'string' && payload.title.trim().length > 0 ? { title: payload.title.trim() } : {}),
|
|
3090
2382
|
html: payload.html,
|
|
3091
2383
|
toolInput: payload.toolInput,
|
|
3092
2384
|
serverName: payload.serverName,
|
|
@@ -3196,18 +2488,14 @@ function syncEventToCanvasState(event: string, payload: PrimaryWorkbenchEventPay
|
|
|
3196
2488
|
success: typeof payload.success === 'boolean' ? payload.success : undefined,
|
|
3197
2489
|
error: typeof payload.error === 'string' ? payload.error : undefined,
|
|
3198
2490
|
content: typeof payload.content === 'string' ? payload.content : undefined,
|
|
3199
|
-
detailedContent:
|
|
3200
|
-
typeof payload.detailedContent === 'string' ? payload.detailedContent : undefined,
|
|
2491
|
+
detailedContent: typeof payload.detailedContent === 'string' ? payload.detailedContent : undefined,
|
|
3201
2492
|
}),
|
|
3202
2493
|
},
|
|
3203
2494
|
});
|
|
3204
2495
|
}
|
|
3205
2496
|
});
|
|
3206
2497
|
} else if (event === 'context-cards') {
|
|
3207
|
-
syncContextNodeToCanvasState(
|
|
3208
|
-
{ cards: Array.isArray(payload.cards) ? payload.cards : [] },
|
|
3209
|
-
{ forceCreate: true },
|
|
3210
|
-
);
|
|
2498
|
+
syncContextNodeToCanvasState({ cards: Array.isArray(payload.cards) ? payload.cards : [] }, { forceCreate: true });
|
|
3211
2499
|
} else if (event === 'context-usage') {
|
|
3212
2500
|
syncContextNodeToCanvasState({
|
|
3213
2501
|
currentTokens: payload.currentTokens,
|
|
@@ -3230,9 +2518,7 @@ function syncEventToCanvasState(event: string, payload: PrimaryWorkbenchEventPay
|
|
|
3230
2518
|
return;
|
|
3231
2519
|
}
|
|
3232
2520
|
const auxTabs = Array.isArray(existing.data.auxTabs)
|
|
3233
|
-
? (existing.data.auxTabs as Array<Record<string, unknown>>).filter(
|
|
3234
|
-
(tab) => tab.id !== payload.id,
|
|
3235
|
-
)
|
|
2521
|
+
? (existing.data.auxTabs as Array<Record<string, unknown>>).filter((tab) => tab.id !== payload.id)
|
|
3236
2522
|
: [];
|
|
3237
2523
|
syncContextNodeToCanvasState({ auxTabs });
|
|
3238
2524
|
} else if (event === 'canvas-status' || event === 'execution-phase') {
|
|
@@ -3377,10 +2663,7 @@ function syncEventToCanvasState(event: string, payload: PrimaryWorkbenchEventPay
|
|
|
3377
2663
|
}
|
|
3378
2664
|
}
|
|
3379
2665
|
|
|
3380
|
-
export function emitPrimaryWorkbenchEvent(
|
|
3381
|
-
event: string,
|
|
3382
|
-
payload: PrimaryWorkbenchEventPayload = {},
|
|
3383
|
-
): void {
|
|
2666
|
+
export function emitPrimaryWorkbenchEvent(event: string, payload: PrimaryWorkbenchEventPayload = {}): void {
|
|
3384
2667
|
rotatePrimaryWorkbenchSessionIfNeeded();
|
|
3385
2668
|
const envelope = {
|
|
3386
2669
|
...payload,
|
|
@@ -3457,10 +2740,7 @@ function ensureCanvasBrowserOpen(): void {
|
|
|
3457
2740
|
canvasBrowserOpening = false;
|
|
3458
2741
|
}
|
|
3459
2742
|
|
|
3460
|
-
export function openPrimaryWorkbenchPath(
|
|
3461
|
-
pathLike: string,
|
|
3462
|
-
workspaceRoot = process.cwd(),
|
|
3463
|
-
): string | null {
|
|
2743
|
+
export function openPrimaryWorkbenchPath(pathLike: string, workspaceRoot = process.cwd()): string | null {
|
|
3464
2744
|
const safePath = resolve(pathLike);
|
|
3465
2745
|
if (!isMarkdownFile(safePath)) return null;
|
|
3466
2746
|
if (!existsSync(safePath)) return null;
|
|
@@ -3524,11 +2804,12 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
3524
2804
|
rotatePrimaryWorkbenchSessionIfNeeded();
|
|
3525
2805
|
|
|
3526
2806
|
const preferredPort = options.port ?? Number(process.env.PMX_WEB_CANVAS_PORT ?? DEFAULT_PORT);
|
|
3527
|
-
const portCandidates =
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
2807
|
+
const portCandidates =
|
|
2808
|
+
options.port === 0
|
|
2809
|
+
? [0]
|
|
2810
|
+
: options.allowPortFallback === false
|
|
2811
|
+
? [preferredPort > 0 ? Math.floor(preferredPort) : DEFAULT_PORT]
|
|
2812
|
+
: buildPortCandidates(preferredPort);
|
|
3532
2813
|
|
|
3533
2814
|
for (const portCandidate of portCandidates) {
|
|
3534
2815
|
try {
|
|
@@ -3605,14 +2886,14 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
3605
2886
|
return handleWorkbenchIntent(req);
|
|
3606
2887
|
}
|
|
3607
2888
|
|
|
3608
|
-
//
|
|
3609
|
-
//
|
|
3610
|
-
//
|
|
3611
|
-
//
|
|
3612
|
-
//
|
|
3613
|
-
if (url.pathname.startsWith('/api/
|
|
3614
|
-
const
|
|
3615
|
-
if (
|
|
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;
|
|
3616
2897
|
}
|
|
3617
2898
|
|
|
3618
2899
|
if (url.pathname === '/api/workbench/webview/screenshot' && req.method === 'POST') {
|
|
@@ -3627,194 +2908,22 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
3627
2908
|
return handleRender(req);
|
|
3628
2909
|
}
|
|
3629
2910
|
|
|
3630
|
-
// Operation registry routes (plan-005): registered operations are
|
|
3631
|
-
// dispatched here; a null return falls through to the legacy routes.
|
|
3632
|
-
if (url.pathname.startsWith('/api/canvas/')) {
|
|
3633
|
-
const operationResponse = await dispatchOperationRoute(req, url);
|
|
3634
|
-
if (operationResponse) return operationResponse;
|
|
3635
|
-
}
|
|
3636
|
-
|
|
3637
|
-
if (url.pathname === '/api/canvas/theme' && req.method === 'GET') {
|
|
3638
|
-
return responseJson({ ok: true, theme: canvasState.theme });
|
|
3639
|
-
}
|
|
3640
|
-
|
|
3641
|
-
if (url.pathname === '/api/canvas/theme' && req.method === 'POST') {
|
|
3642
|
-
return handleCanvasThemeUpdate(req);
|
|
3643
|
-
}
|
|
3644
|
-
|
|
3645
|
-
if (url.pathname === '/api/canvas/update' && req.method === 'POST') {
|
|
3646
|
-
return handleCanvasUpdate(req);
|
|
3647
|
-
}
|
|
3648
|
-
|
|
3649
|
-
// POST /api/canvas/batch migrated to the operation registry
|
|
3650
|
-
// (plan-008 Wave 2): src/server/operations/ops/batch.ts.
|
|
3651
|
-
|
|
3652
|
-
if (url.pathname === '/api/canvas/viewport' && req.method === 'POST') {
|
|
3653
|
-
return handleCanvasViewport(req);
|
|
3654
|
-
}
|
|
3655
|
-
|
|
3656
|
-
if (url.pathname === '/api/canvas/annotation' && req.method === 'POST') {
|
|
3657
|
-
return handleCanvasAddAnnotation(req);
|
|
3658
|
-
}
|
|
3659
|
-
|
|
3660
|
-
// POST /api/canvas/mcp-app/open, /api/canvas/diagram, and
|
|
3661
|
-
// /api/canvas/web-artifact migrated to the operation registry
|
|
3662
|
-
// (plan-008 Wave 4): src/server/operations/ops/app.ts.
|
|
3663
|
-
|
|
3664
|
-
// Individual node GET/PATCH/DELETE
|
|
3665
|
-
if (url.pathname.startsWith('/api/canvas/node/') && url.pathname.endsWith('/refresh') && req.method === 'POST') {
|
|
3666
|
-
const nodeId = url.pathname.slice('/api/canvas/node/'.length, -'/refresh'.length);
|
|
3667
|
-
return handleCanvasRefreshWebpageNode(nodeId, req);
|
|
3668
|
-
}
|
|
3669
|
-
|
|
3670
2911
|
if (url.pathname.startsWith('/api/canvas/image/') && req.method === 'GET') {
|
|
3671
2912
|
return await handleCanvasImage(url.pathname);
|
|
3672
2913
|
}
|
|
3673
2914
|
|
|
3674
|
-
if (url.pathname === '/api/canvas/pinned-context' && req.method === 'GET') {
|
|
3675
|
-
return handleGetPinnedContext();
|
|
3676
|
-
}
|
|
3677
|
-
|
|
3678
|
-
// GET /api/canvas/ax migrated to the operation registry (plan-007 Slice B.1).
|
|
3679
|
-
|
|
3680
|
-
if (url.pathname === '/api/canvas/ax' && req.method === 'PATCH') {
|
|
3681
|
-
return handleAxStatePatch(req);
|
|
3682
|
-
}
|
|
3683
|
-
|
|
3684
|
-
if (url.pathname === '/api/canvas/ax/context' && req.method === 'GET') {
|
|
3685
|
-
return handleGetAxContext(url);
|
|
3686
|
-
}
|
|
3687
|
-
|
|
3688
|
-
if (url.pathname === '/api/canvas/ax/activity' && req.method === 'POST') {
|
|
3689
|
-
return handleAxActivityIngest(req);
|
|
3690
|
-
}
|
|
3691
|
-
|
|
3692
|
-
if (url.pathname === '/api/canvas/ax/surface-snapshot' && req.method === 'GET') {
|
|
3693
|
-
return handleGetAxSurfaceSnapshot();
|
|
3694
|
-
}
|
|
3695
|
-
|
|
3696
2915
|
if (url.pathname === '/api/canvas/open-external' && req.method === 'POST') {
|
|
3697
2916
|
return handleOpenExternalSurface(req);
|
|
3698
2917
|
}
|
|
3699
2918
|
|
|
3700
|
-
//
|
|
3701
|
-
|
|
3702
|
-
//
|
|
3703
|
-
// /api/canvas/ax/timeline migrated to the operation registry
|
|
3704
|
-
// (plan-007 Slice B wave 3): src/server/operations/ops/ax-timeline.ts.
|
|
3705
|
-
|
|
3706
|
-
if (url.pathname === '/api/canvas/ax/work' && req.method === 'GET') {
|
|
3707
|
-
return handleAxWorkList();
|
|
3708
|
-
}
|
|
3709
|
-
|
|
3710
|
-
// POST /api/canvas/ax/work + PATCH /api/canvas/ax/work/:id migrated to
|
|
3711
|
-
// the operation registry (plan-007 Slice B wave 2).
|
|
3712
|
-
|
|
3713
|
-
if (url.pathname === '/api/canvas/ax/approval' && req.method === 'GET') {
|
|
3714
|
-
return handleAxApprovalList();
|
|
3715
|
-
}
|
|
3716
|
-
|
|
3717
|
-
// POST /api/canvas/ax/approval + POST /api/canvas/ax/approval/:id/resolve
|
|
3718
|
-
// migrated to the operation registry (plan-007 Slice B wave 2).
|
|
3719
|
-
|
|
3720
|
-
// GET /api/canvas/ax/approval/:id (single-item read + ?waitMs long-poll)
|
|
3721
|
-
// migrated to the operation registry (plan-007 Slice B wave 4).
|
|
3722
|
-
|
|
3723
|
-
// POST /api/canvas/ax/evidence migrated to the operation registry
|
|
3724
|
-
// (plan-007 Slice B wave 3): src/server/operations/ops/ax-timeline.ts.
|
|
3725
|
-
|
|
3726
|
-
if (url.pathname === '/api/canvas/ax/review' && req.method === 'GET') {
|
|
3727
|
-
return handleAxReviewList();
|
|
3728
|
-
}
|
|
3729
|
-
|
|
3730
|
-
// POST /api/canvas/ax/review + PATCH /api/canvas/ax/review/:id migrated
|
|
3731
|
-
// to the operation registry (plan-007 Slice B wave 2).
|
|
3732
|
-
|
|
3733
|
-
if (url.pathname === '/api/canvas/ax/host-capability' && req.method === 'GET') {
|
|
3734
|
-
return handleAxHostCapabilityGet();
|
|
3735
|
-
}
|
|
3736
|
-
|
|
3737
|
-
// PUT /api/canvas/ax/host-capability migrated to the operation registry (plan-007 Slice B.1).
|
|
3738
|
-
|
|
3739
|
-
if (url.pathname === '/api/canvas/ax/interaction' && req.method === 'POST') {
|
|
3740
|
-
return handleAxInteraction(req);
|
|
3741
|
-
}
|
|
3742
|
-
|
|
3743
|
-
// GET /api/canvas/ax/delivery/pending + POST /api/canvas/ax/delivery/:id/mark
|
|
3744
|
-
// migrated to the operation registry (plan-007 Slice B wave 3):
|
|
3745
|
-
// src/server/operations/ops/ax-timeline.ts.
|
|
3746
|
-
|
|
3747
|
-
if (url.pathname === '/api/canvas/ax/elicitation' && req.method === 'GET') {
|
|
3748
|
-
return handleAxElicitationList();
|
|
3749
|
-
}
|
|
3750
|
-
|
|
3751
|
-
// POST /api/canvas/ax/elicitation + POST /api/canvas/ax/elicitation/:id/respond
|
|
3752
|
-
// migrated to the operation registry (plan-007 Slice B wave 2).
|
|
3753
|
-
|
|
3754
|
-
// GET /api/canvas/ax/elicitation/:id (single-item read + ?waitMs long-poll)
|
|
3755
|
-
// migrated to the operation registry (plan-007 Slice B wave 4).
|
|
3756
|
-
|
|
3757
|
-
if (url.pathname === '/api/canvas/ax/mode' && req.method === 'GET') {
|
|
3758
|
-
return handleAxModeList();
|
|
3759
|
-
}
|
|
3760
|
-
|
|
3761
|
-
// POST /api/canvas/ax/mode + POST /api/canvas/ax/mode/:id/resolve migrated
|
|
3762
|
-
// to the operation registry (plan-007 Slice B wave 2).
|
|
3763
|
-
|
|
3764
|
-
// GET /api/canvas/ax/mode/:id (single-item read + ?waitMs long-poll)
|
|
3765
|
-
// migrated to the operation registry (plan-007 Slice B wave 4).
|
|
3766
|
-
|
|
3767
|
-
if (url.pathname === '/api/canvas/ax/command' && req.method === 'GET') {
|
|
3768
|
-
return handleAxCommandList();
|
|
3769
|
-
}
|
|
3770
|
-
|
|
3771
|
-
// POST /api/canvas/ax/command migrated to the operation registry
|
|
3772
|
-
// (plan-007 Slice B wave 3): src/server/operations/ops/ax-timeline.ts.
|
|
3773
|
-
|
|
3774
|
-
if (url.pathname === '/api/canvas/ax/policy' && req.method === 'GET') {
|
|
3775
|
-
return handleAxPolicyGet();
|
|
3776
|
-
}
|
|
3777
|
-
|
|
3778
|
-
// POST /api/canvas/ax/policy migrated to the operation registry (plan-007 Slice B.1).
|
|
3779
|
-
|
|
3780
|
-
// Code graph API
|
|
3781
|
-
if (url.pathname === '/api/canvas/code-graph' && req.method === 'GET') {
|
|
3782
|
-
const summary = buildCodeGraphSummary();
|
|
3783
|
-
return responseJson(summary);
|
|
3784
|
-
}
|
|
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.
|
|
3785
2922
|
|
|
3786
2923
|
if (url.pathname === '/api/canvas/prompt' && req.method === 'POST') {
|
|
3787
2924
|
return handleCanvasPrompt(req);
|
|
3788
2925
|
}
|
|
3789
2926
|
|
|
3790
|
-
if (url.pathname === '/api/ext-app/call-tool' && req.method === 'POST') {
|
|
3791
|
-
return handleExtAppCallTool(req);
|
|
3792
|
-
}
|
|
3793
|
-
|
|
3794
|
-
if (url.pathname === '/api/ext-app/read-resource' && req.method === 'POST') {
|
|
3795
|
-
return handleExtAppReadResource(req);
|
|
3796
|
-
}
|
|
3797
|
-
|
|
3798
|
-
if (url.pathname === '/api/ext-app/list-tools' && req.method === 'POST') {
|
|
3799
|
-
return handleExtAppListTools(req);
|
|
3800
|
-
}
|
|
3801
|
-
|
|
3802
|
-
if (url.pathname === '/api/ext-app/list-resources' && req.method === 'POST') {
|
|
3803
|
-
return handleExtAppListResources(req);
|
|
3804
|
-
}
|
|
3805
|
-
|
|
3806
|
-
if (url.pathname === '/api/ext-app/list-resource-templates' && req.method === 'POST') {
|
|
3807
|
-
return handleExtAppListResourceTemplates(req);
|
|
3808
|
-
}
|
|
3809
|
-
|
|
3810
|
-
if (url.pathname === '/api/ext-app/list-prompts' && req.method === 'POST') {
|
|
3811
|
-
return handleExtAppListPrompts(req);
|
|
3812
|
-
}
|
|
3813
|
-
|
|
3814
|
-
if (url.pathname === '/api/ext-app/model-context' && req.method === 'POST') {
|
|
3815
|
-
return handleExtAppModelContext(req);
|
|
3816
|
-
}
|
|
3817
|
-
|
|
3818
2927
|
// Static files for canvas SPA bundle
|
|
3819
2928
|
if (url.pathname.startsWith('/canvas/')) {
|
|
3820
2929
|
const staticResponse = serveCanvasStatic(url.pathname);
|