pmx-canvas 0.3.0 → 0.3.2
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 +73 -0
- package/dist/canvas/index.js +65 -65
- package/dist/types/cli/agent.d.ts +9 -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/ExtAppFrame.d.ts +9 -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/server/canvas-schema.d.ts +1 -1
- package/dist/types/server/html-primitives.d.ts +1 -1
- package/dist/types/server/index.d.ts +4 -4
- 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/server/server.d.ts +8 -0
- package/docs/cli.md +10 -1
- package/docs/http-api.md +28 -0
- package/docs/plans/plan-009-tech-debt-backlog.md +5 -5
- package/docs/screenshot.png +0 -0
- package/docs/tech-debt-assessment-2026-07.md +2 -2
- package/package.json +5 -2
- package/skills/pmx-canvas/SKILL.md +15 -10
- package/skills/pmx-canvas/references/full-reference.md +17 -3
- package/src/cli/agent.ts +1951 -1571
- package/src/cli/daemon.ts +460 -0
- package/src/cli/index.ts +80 -323
- 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 +194 -80
- 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 +35 -21
- package/src/mcp/server.ts +132 -70
- 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 +182 -39
- package/src/server/ax-state.ts +142 -47
- package/src/server/canvas-db.ts +213 -95
- package/src/server/canvas-operations.ts +180 -123
- package/src/server/canvas-provenance.ts +1 -4
- package/src/server/canvas-schema.ts +454 -73
- package/src/server/canvas-serialization.ts +27 -35
- package/src/server/canvas-state.ts +150 -58
- package/src/server/chart-template.ts +4 -5
- package/src/server/code-graph.ts +19 -6
- package/src/server/diagram-presets.ts +28 -29
- package/src/server/ext-app-lookup.ts +3 -12
- 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 +320 -217
- package/src/server/intent-registry.ts +2 -5
- 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 +1 -3
- package/src/server/operations/http.ts +2 -3
- package/src/server/operations/index.ts +7 -1
- package/src/server/operations/invoker.ts +4 -3
- package/src/server/operations/mcp.ts +22 -30
- package/src/server/operations/ops/annotation.ts +122 -10
- package/src/server/operations/ops/app.ts +98 -73
- 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 +41 -15
- 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 +298 -109
- package/src/server/operations/ops/query.ts +46 -28
- 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 +122 -1028
- 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/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;
|
|
@@ -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('&', '&')
|
|
952
|
-
.replaceAll('<', '<')
|
|
953
|
-
.replaceAll('>', '>')
|
|
954
|
-
.replaceAll('"', '"')
|
|
955
|
-
.replaceAll("'", ''');
|
|
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
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
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 ────────────────────────────────────────────
|
|
@@ -1177,13 +1021,26 @@ function resolveCanvasBundleDir(): string {
|
|
|
1177
1021
|
return fallback;
|
|
1178
1022
|
}
|
|
1179
1023
|
|
|
1024
|
+
/**
|
|
1025
|
+
* Containment check for bundle-asset serving. Separator-agnostic: on Windows,
|
|
1026
|
+
* `resolve()` returns backslash paths, so comparing against a `${dir}/` template
|
|
1027
|
+
* rejected every asset — /canvas/index.js 404'd and the SPA never booted (the
|
|
1028
|
+
* 0.3.1 Windows report). Normalizing both sides keeps the check exact on POSIX
|
|
1029
|
+
* and correct on win32, and testable with win32-shaped fixtures on any host.
|
|
1030
|
+
*/
|
|
1031
|
+
export function isCanvasBundlePath(distPath: string, bundleDir: string): boolean {
|
|
1032
|
+
const normalize = (value: string) => value.replaceAll('\\', '/');
|
|
1033
|
+
const dir = normalize(bundleDir).replace(/\/+$/, '');
|
|
1034
|
+
return normalize(distPath).startsWith(`${dir}/`);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1180
1037
|
function serveCanvasStatic(pathname: string): Response | null {
|
|
1181
1038
|
const fileName = pathname.slice('/canvas/'.length);
|
|
1182
1039
|
if (!fileName || fileName.includes('..') || fileName.startsWith('/')) return null;
|
|
1183
1040
|
|
|
1184
1041
|
const bundleDir = resolveCanvasBundleDir();
|
|
1185
1042
|
const distPath = resolve(bundleDir, fileName);
|
|
1186
|
-
if (!distPath
|
|
1043
|
+
if (!isCanvasBundlePath(distPath, bundleDir)) return null;
|
|
1187
1044
|
if (existsSync(distPath)) {
|
|
1188
1045
|
const ext = extname(fileName);
|
|
1189
1046
|
return new Response(readFileSync(distPath), {
|
|
@@ -1308,18 +1165,17 @@ function handleNodeSurface(pathname: string, url: URL): Response {
|
|
|
1308
1165
|
const theme = normalizeSurfaceTheme(url.searchParams.get('theme'));
|
|
1309
1166
|
|
|
1310
1167
|
if (node.type === 'html') {
|
|
1311
|
-
const html =
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1168
|
+
const html =
|
|
1169
|
+
typeof node.data.html === 'string'
|
|
1170
|
+
? node.data.html
|
|
1171
|
+
: typeof node.data.content === 'string'
|
|
1172
|
+
? node.data.content
|
|
1173
|
+
: '';
|
|
1316
1174
|
if (!html) return responseText('HTML node has no content', 404);
|
|
1317
1175
|
const present = url.searchParams.get('present') === '1';
|
|
1318
1176
|
const axCaps = resolveNodeAxCapabilities(node);
|
|
1319
1177
|
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;
|
|
1178
|
+
const surfaceTitle = typeof node.data.title === 'string' && node.data.title.trim() ? node.data.title : node.id;
|
|
1323
1179
|
const doc = buildHtmlSurfaceDocument(html, {
|
|
1324
1180
|
theme,
|
|
1325
1181
|
title: surfaceTitle,
|
|
@@ -1332,7 +1188,9 @@ function handleNodeSurface(pathname: string, url: URL): Response {
|
|
|
1332
1188
|
// Seed the read-side bridge with the current AX state (only for AX surfaces).
|
|
1333
1189
|
...(axEnabled ? { axState: buildCanvasAxSurfaceSnapshot() } : {}),
|
|
1334
1190
|
// Content-height reporter nonce (lets an html node grow to fit its content).
|
|
1335
|
-
...(url.searchParams.get('frameToken')
|
|
1191
|
+
...(url.searchParams.get('frameToken')
|
|
1192
|
+
? { contentHeightToken: url.searchParams.get('frameToken') as string }
|
|
1193
|
+
: {}),
|
|
1336
1194
|
});
|
|
1337
1195
|
return surfaceHtmlResponse(doc, HTML_SURFACE_SANDBOX);
|
|
1338
1196
|
}
|
|
@@ -1374,127 +1232,11 @@ function handleNodeSurface(pathname: string, url: URL): Response {
|
|
|
1374
1232
|
return responseText('Node type cannot be opened as a site', 404);
|
|
1375
1233
|
}
|
|
1376
1234
|
|
|
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
1235
|
// ── Serve image file for image nodes ─────────────────────────
|
|
1494
1236
|
async function handleCanvasImage(pathname: string): Promise<Response> {
|
|
1495
1237
|
const nodeId = pathname.replace('/api/canvas/image/', '');
|
|
1496
1238
|
const node = canvasState.getNode(nodeId);
|
|
1497
|
-
if (
|
|
1239
|
+
if (node?.type !== 'image') {
|
|
1498
1240
|
return responseText('Image node not found', 404);
|
|
1499
1241
|
}
|
|
1500
1242
|
const src = (node.data.path as string) || (node.data.src as string) || '';
|
|
@@ -1526,45 +1268,6 @@ async function handleCanvasImage(pathname: string): Promise<Response> {
|
|
|
1526
1268
|
});
|
|
1527
1269
|
}
|
|
1528
1270
|
|
|
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
1271
|
async function handleJsonRenderView(url: URL): Promise<Response> {
|
|
1569
1272
|
const nodeId = url.searchParams.get('nodeId') ?? '';
|
|
1570
1273
|
if (!nodeId) return responseText('Missing nodeId', 400);
|
|
@@ -1585,22 +1288,18 @@ async function handleJsonRenderView(url: URL): Promise<Response> {
|
|
|
1585
1288
|
|
|
1586
1289
|
const themeValue = url.searchParams.get('theme');
|
|
1587
1290
|
const theme =
|
|
1588
|
-
themeValue === 'dark' || themeValue === 'light' || themeValue === 'high-contrast'
|
|
1589
|
-
? themeValue
|
|
1590
|
-
: undefined;
|
|
1291
|
+
themeValue === 'dark' || themeValue === 'light' || themeValue === 'high-contrast' ? themeValue : undefined;
|
|
1591
1292
|
const title = (node.data.title as string) || node.id;
|
|
1592
1293
|
// Devtools panel is double-gated: the operator must opt in via the env flag
|
|
1593
1294
|
// AND the request must carry ?devtools=1. Off by default in all normal runs.
|
|
1594
1295
|
const devtoolsEnabled =
|
|
1595
|
-
process.env.PMX_CANVAS_JSON_RENDER_DEVTOOLS === '1' &&
|
|
1596
|
-
url.searchParams.get('devtools') === '1';
|
|
1296
|
+
process.env.PMX_CANVAS_JSON_RENDER_DEVTOOLS === '1' && url.searchParams.get('devtools') === '1';
|
|
1597
1297
|
const axToken = url.searchParams.get('axToken');
|
|
1598
1298
|
const axEnabled = resolveNodeAxCapabilities(node).enabled;
|
|
1599
1299
|
const frameToken = url.searchParams.get('frameToken');
|
|
1600
1300
|
const displayParam = url.searchParams.get('display');
|
|
1601
|
-
const display =
|
|
1602
|
-
: displayParam === 'site' ? 'site' as const
|
|
1603
|
-
: undefined;
|
|
1301
|
+
const display =
|
|
1302
|
+
displayParam === 'expanded' ? ('expanded' as const) : displayParam === 'site' ? ('site' as const) : undefined;
|
|
1604
1303
|
// A standalone "site" tab fills the viewport; content-fit (grow-to-content for the
|
|
1605
1304
|
// in-canvas iframe) would fight that, so it's ignored in site mode (#65).
|
|
1606
1305
|
const fitContent = url.searchParams.get('fit') === 'content' && display !== 'site';
|
|
@@ -1682,9 +1381,7 @@ function handleArtifactView(url: URL): Response {
|
|
|
1682
1381
|
const safeNodeId = node.id.replace(/[^A-Za-z0-9_-]/g, '').slice(0, 80);
|
|
1683
1382
|
const stateJson = JSON.stringify(buildCanvasAxSurfaceSnapshot()).replace(/</g, '\\u003c');
|
|
1684
1383
|
const bridge = `${buildAxBridge(safeToken, safeNodeId)}${buildAxStateBridge(safeToken, stateJson)}`;
|
|
1685
|
-
content = content.includes('</head>')
|
|
1686
|
-
? content.replace('</head>', `${bridge}</head>`)
|
|
1687
|
-
: `${bridge}${content}`;
|
|
1384
|
+
content = content.includes('</head>') ? content.replace('</head>', `${bridge}</head>`) : `${bridge}${content}`;
|
|
1688
1385
|
}
|
|
1689
1386
|
}
|
|
1690
1387
|
// Content-height reporter so a web-artifact node grows to fit its app (#48).
|
|
@@ -1711,7 +1408,8 @@ function handleArtifactView(url: URL): Response {
|
|
|
1711
1408
|
? `<article class="markdown-body">${marked.parse(content) as string}</article>`
|
|
1712
1409
|
: `<pre class="artifact-code"><code>${escapeHtml(content)}</code></pre>`;
|
|
1713
1410
|
|
|
1714
|
-
return new Response(
|
|
1411
|
+
return new Response(
|
|
1412
|
+
`<!doctype html>
|
|
1715
1413
|
<html lang="en">
|
|
1716
1414
|
<head>
|
|
1717
1415
|
<meta charset="utf-8" />
|
|
@@ -1809,12 +1507,14 @@ function handleArtifactView(url: URL): Response {
|
|
|
1809
1507
|
<section class="panel">${body}</section>
|
|
1810
1508
|
</main>
|
|
1811
1509
|
</body>
|
|
1812
|
-
</html>`,
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1510
|
+
</html>`,
|
|
1511
|
+
{
|
|
1512
|
+
headers: {
|
|
1513
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
1514
|
+
'Cache-Control': 'no-store',
|
|
1515
|
+
},
|
|
1816
1516
|
},
|
|
1817
|
-
|
|
1517
|
+
);
|
|
1818
1518
|
}
|
|
1819
1519
|
|
|
1820
1520
|
function handleRead(pathLike: string): Response {
|
|
@@ -1832,180 +1532,6 @@ function handleRead(pathLike: string): Response {
|
|
|
1832
1532
|
});
|
|
1833
1533
|
}
|
|
1834
1534
|
|
|
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
1535
|
function handleWorkbenchState(): Response {
|
|
2010
1536
|
const mcpAppHost = getMcpAppHostSnapshot();
|
|
2011
1537
|
if (!primaryWorkbenchPath) {
|
|
@@ -2052,9 +1578,7 @@ function currentWorkbenchUrl(): string | null {
|
|
|
2052
1578
|
async function handleWorkbenchWebViewScreenshot(req: Request): Promise<Response> {
|
|
2053
1579
|
const body = await readJson(req);
|
|
2054
1580
|
if (body === null) return malformedJsonResponse();
|
|
2055
|
-
const format = body.format === 'jpeg' || body.format === 'webp' || body.format === 'png'
|
|
2056
|
-
? body.format
|
|
2057
|
-
: 'png';
|
|
1581
|
+
const format = body.format === 'jpeg' || body.format === 'webp' || body.format === 'png' ? body.format : 'png';
|
|
2058
1582
|
const quality = typeof body.quality === 'number' ? body.quality : undefined;
|
|
2059
1583
|
|
|
2060
1584
|
try {
|
|
@@ -2063,11 +1587,7 @@ async function handleWorkbenchWebViewScreenshot(req: Request): Promise<Response>
|
|
|
2063
1587
|
...(quality !== undefined ? { quality } : {}),
|
|
2064
1588
|
});
|
|
2065
1589
|
const responseBytes = Uint8Array.from(bytes);
|
|
2066
|
-
const mimeType = format === 'jpeg'
|
|
2067
|
-
? 'image/jpeg'
|
|
2068
|
-
: format === 'webp'
|
|
2069
|
-
? 'image/webp'
|
|
2070
|
-
: 'image/png';
|
|
1590
|
+
const mimeType = format === 'jpeg' ? 'image/jpeg' : format === 'webp' ? 'image/webp' : 'image/png';
|
|
2071
1591
|
return new Response(responseBytes.buffer, {
|
|
2072
1592
|
headers: {
|
|
2073
1593
|
'Content-Type': mimeType,
|
|
@@ -2102,10 +1622,7 @@ async function handleWorkbenchIntent(req: Request): Promise<Response> {
|
|
|
2102
1622
|
}
|
|
2103
1623
|
|
|
2104
1624
|
const rawPayload = body.payload;
|
|
2105
|
-
const payload =
|
|
2106
|
-
rawPayload && typeof rawPayload === 'object'
|
|
2107
|
-
? (rawPayload as PrimaryWorkbenchEventPayload)
|
|
2108
|
-
: {};
|
|
1625
|
+
const payload = rawPayload && typeof rawPayload === 'object' ? (rawPayload as PrimaryWorkbenchEventPayload) : {};
|
|
2109
1626
|
|
|
2110
1627
|
// Handle trace intents directly on the server
|
|
2111
1628
|
if (rawType === 'trace-toggle') {
|
|
@@ -2303,10 +1820,7 @@ async function handleCanvasPrompt(req: Request): Promise<Response> {
|
|
|
2303
1820
|
const parentNodeId = typeof body.parentNodeId === 'string' ? body.parentNodeId : undefined;
|
|
2304
1821
|
const MAX_CONTEXT_NODES = 20;
|
|
2305
1822
|
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
|
-
)
|
|
1823
|
+
? (body.contextNodeIds.filter((id: unknown) => typeof id === 'string') as string[]).slice(0, MAX_CONTEXT_NODES)
|
|
2310
1824
|
: [];
|
|
2311
1825
|
|
|
2312
1826
|
if (contextNodeIds.length === 0 && canvasState.contextPinnedNodeIds.size > 0) {
|
|
@@ -2365,9 +1879,7 @@ async function handleCanvasPrompt(req: Request): Promise<Response> {
|
|
|
2365
1879
|
}
|
|
2366
1880
|
|
|
2367
1881
|
const MAX_THREAD_TURNS = 100;
|
|
2368
|
-
const existingTurnCount = Array.isArray(threadNode.data.turns)
|
|
2369
|
-
? (threadNode.data.turns as unknown[]).length
|
|
2370
|
-
: 0;
|
|
1882
|
+
const existingTurnCount = Array.isArray(threadNode.data.turns) ? (threadNode.data.turns as unknown[]).length : 0;
|
|
2371
1883
|
if (existingTurnCount >= MAX_THREAD_TURNS) {
|
|
2372
1884
|
return responseText('Thread has reached the maximum number of turns', 400);
|
|
2373
1885
|
}
|
|
@@ -2442,9 +1954,7 @@ async function handleCanvasPrompt(req: Request): Promise<Response> {
|
|
|
2442
1954
|
const suffix = Math.random().toString(36).slice(2, 8);
|
|
2443
1955
|
const nodeId = `prompt-${Date.now()}-${suffix}`;
|
|
2444
1956
|
|
|
2445
|
-
const promptCount = canvasState
|
|
2446
|
-
.getLayout()
|
|
2447
|
-
.nodes.filter((n) => n.type === 'prompt' || n.type === 'response').length;
|
|
1957
|
+
const promptCount = canvasState.getLayout().nodes.filter((n) => n.type === 'prompt' || n.type === 'response').length;
|
|
2448
1958
|
const pos = position ?? { x: 380 + promptCount * 30, y: 1260 + promptCount * 30 };
|
|
2449
1959
|
|
|
2450
1960
|
let enrichedText = text;
|
|
@@ -2538,135 +2048,6 @@ async function handleCanvasPrompt(req: Request): Promise<Response> {
|
|
|
2538
2048
|
return responseJson({ ok: true, nodeId });
|
|
2539
2049
|
}
|
|
2540
2050
|
|
|
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
2051
|
// Open a node's surface in the user's real system browser (for hosts whose
|
|
2671
2052
|
// embedded browser makes window.open('_blank') feel in-place, e.g. Codex).
|
|
2672
2053
|
// Accepts ONLY { nodeId, url? } and opens this server's own surface URL — never
|
|
@@ -2694,108 +2075,6 @@ async function handleOpenExternalSurface(req: Request): Promise<Response> {
|
|
|
2694
2075
|
return responseJson({ ok: true, opened, url: surfacePath });
|
|
2695
2076
|
}
|
|
2696
2077
|
|
|
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
2078
|
// ── Port resolution ───────────────────────────────────────────
|
|
2800
2079
|
|
|
2801
2080
|
function buildPortCandidates(preferredPort: number): number[] {
|
|
@@ -2930,11 +2209,9 @@ export function buildMacBrowserOpenScript(appName: string, url: string): string
|
|
|
2930
2209
|
}
|
|
2931
2210
|
|
|
2932
2211
|
function resolveWindowsBrowserForCanvas(): { name: string; exe: string } | null {
|
|
2933
|
-
const envDirs = [
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
process.env.LOCALAPPDATA,
|
|
2937
|
-
].filter((d): d is string => Boolean(d));
|
|
2212
|
+
const envDirs = [process.env.PROGRAMFILES, process.env['PROGRAMFILES(X86)'], process.env.LOCALAPPDATA].filter(
|
|
2213
|
+
(d): d is string => Boolean(d),
|
|
2214
|
+
);
|
|
2938
2215
|
|
|
2939
2216
|
const browsers = [
|
|
2940
2217
|
{ name: 'Edge', subpath: join('Microsoft', 'Edge', 'Application', 'msedge.exe') },
|
|
@@ -2995,8 +2272,7 @@ function syncContextNodeToCanvasState(
|
|
|
2995
2272
|
mergedData.messagesLength !== undefined ||
|
|
2996
2273
|
mergedData.utilization !== undefined ||
|
|
2997
2274
|
mergedData.nearLimit !== undefined;
|
|
2998
|
-
const shouldCreate =
|
|
2999
|
-
options.forceCreate === true || cards.length > 0 || auxTabs.length > 0 || hasUsage;
|
|
2275
|
+
const shouldCreate = options.forceCreate === true || cards.length > 0 || auxTabs.length > 0 || hasUsage;
|
|
3000
2276
|
|
|
3001
2277
|
if (!existing) {
|
|
3002
2278
|
if (!shouldCreate) return;
|
|
@@ -3105,16 +2381,17 @@ function syncEventToCanvasState(event: string, payload: PrimaryWorkbenchEventPay
|
|
|
3105
2381
|
} else if (event === 'ext-app-open') {
|
|
3106
2382
|
const toolCallId = payload.toolCallId as string;
|
|
3107
2383
|
if (!toolCallId) return;
|
|
3108
|
-
const id =
|
|
3109
|
-
|
|
3110
|
-
|
|
2384
|
+
const id =
|
|
2385
|
+
typeof payload.nodeId === 'string' && payload.nodeId.length > 0
|
|
2386
|
+
? payload.nodeId
|
|
2387
|
+
: toolCallId.startsWith('ext-app-')
|
|
2388
|
+
? toolCallId
|
|
2389
|
+
: `ext-app-${toolCallId}`;
|
|
3111
2390
|
const dataPatch = {
|
|
3112
2391
|
mode: 'ext-app',
|
|
3113
2392
|
toolCallId,
|
|
3114
2393
|
nodeId: id,
|
|
3115
|
-
...(typeof payload.title === 'string' && payload.title.trim().length > 0
|
|
3116
|
-
? { title: payload.title.trim() }
|
|
3117
|
-
: {}),
|
|
2394
|
+
...(typeof payload.title === 'string' && payload.title.trim().length > 0 ? { title: payload.title.trim() } : {}),
|
|
3118
2395
|
html: payload.html,
|
|
3119
2396
|
toolInput: payload.toolInput,
|
|
3120
2397
|
serverName: payload.serverName,
|
|
@@ -3224,18 +2501,14 @@ function syncEventToCanvasState(event: string, payload: PrimaryWorkbenchEventPay
|
|
|
3224
2501
|
success: typeof payload.success === 'boolean' ? payload.success : undefined,
|
|
3225
2502
|
error: typeof payload.error === 'string' ? payload.error : undefined,
|
|
3226
2503
|
content: typeof payload.content === 'string' ? payload.content : undefined,
|
|
3227
|
-
detailedContent:
|
|
3228
|
-
typeof payload.detailedContent === 'string' ? payload.detailedContent : undefined,
|
|
2504
|
+
detailedContent: typeof payload.detailedContent === 'string' ? payload.detailedContent : undefined,
|
|
3229
2505
|
}),
|
|
3230
2506
|
},
|
|
3231
2507
|
});
|
|
3232
2508
|
}
|
|
3233
2509
|
});
|
|
3234
2510
|
} else if (event === 'context-cards') {
|
|
3235
|
-
syncContextNodeToCanvasState(
|
|
3236
|
-
{ cards: Array.isArray(payload.cards) ? payload.cards : [] },
|
|
3237
|
-
{ forceCreate: true },
|
|
3238
|
-
);
|
|
2511
|
+
syncContextNodeToCanvasState({ cards: Array.isArray(payload.cards) ? payload.cards : [] }, { forceCreate: true });
|
|
3239
2512
|
} else if (event === 'context-usage') {
|
|
3240
2513
|
syncContextNodeToCanvasState({
|
|
3241
2514
|
currentTokens: payload.currentTokens,
|
|
@@ -3258,9 +2531,7 @@ function syncEventToCanvasState(event: string, payload: PrimaryWorkbenchEventPay
|
|
|
3258
2531
|
return;
|
|
3259
2532
|
}
|
|
3260
2533
|
const auxTabs = Array.isArray(existing.data.auxTabs)
|
|
3261
|
-
? (existing.data.auxTabs as Array<Record<string, unknown>>).filter(
|
|
3262
|
-
(tab) => tab.id !== payload.id,
|
|
3263
|
-
)
|
|
2534
|
+
? (existing.data.auxTabs as Array<Record<string, unknown>>).filter((tab) => tab.id !== payload.id)
|
|
3264
2535
|
: [];
|
|
3265
2536
|
syncContextNodeToCanvasState({ auxTabs });
|
|
3266
2537
|
} else if (event === 'canvas-status' || event === 'execution-phase') {
|
|
@@ -3405,10 +2676,7 @@ function syncEventToCanvasState(event: string, payload: PrimaryWorkbenchEventPay
|
|
|
3405
2676
|
}
|
|
3406
2677
|
}
|
|
3407
2678
|
|
|
3408
|
-
export function emitPrimaryWorkbenchEvent(
|
|
3409
|
-
event: string,
|
|
3410
|
-
payload: PrimaryWorkbenchEventPayload = {},
|
|
3411
|
-
): void {
|
|
2679
|
+
export function emitPrimaryWorkbenchEvent(event: string, payload: PrimaryWorkbenchEventPayload = {}): void {
|
|
3412
2680
|
rotatePrimaryWorkbenchSessionIfNeeded();
|
|
3413
2681
|
const envelope = {
|
|
3414
2682
|
...payload,
|
|
@@ -3485,10 +2753,7 @@ function ensureCanvasBrowserOpen(): void {
|
|
|
3485
2753
|
canvasBrowserOpening = false;
|
|
3486
2754
|
}
|
|
3487
2755
|
|
|
3488
|
-
export function openPrimaryWorkbenchPath(
|
|
3489
|
-
pathLike: string,
|
|
3490
|
-
workspaceRoot = process.cwd(),
|
|
3491
|
-
): string | null {
|
|
2756
|
+
export function openPrimaryWorkbenchPath(pathLike: string, workspaceRoot = process.cwd()): string | null {
|
|
3492
2757
|
const safePath = resolve(pathLike);
|
|
3493
2758
|
if (!isMarkdownFile(safePath)) return null;
|
|
3494
2759
|
if (!existsSync(safePath)) return null;
|
|
@@ -3552,11 +2817,12 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
3552
2817
|
rotatePrimaryWorkbenchSessionIfNeeded();
|
|
3553
2818
|
|
|
3554
2819
|
const preferredPort = options.port ?? Number(process.env.PMX_WEB_CANVAS_PORT ?? DEFAULT_PORT);
|
|
3555
|
-
const portCandidates =
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
2820
|
+
const portCandidates =
|
|
2821
|
+
options.port === 0
|
|
2822
|
+
? [0]
|
|
2823
|
+
: options.allowPortFallback === false
|
|
2824
|
+
? [preferredPort > 0 ? Math.floor(preferredPort) : DEFAULT_PORT]
|
|
2825
|
+
: buildPortCandidates(preferredPort);
|
|
3560
2826
|
|
|
3561
2827
|
for (const portCandidate of portCandidates) {
|
|
3562
2828
|
try {
|
|
@@ -3633,14 +2899,14 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
3633
2899
|
return handleWorkbenchIntent(req);
|
|
3634
2900
|
}
|
|
3635
2901
|
|
|
3636
|
-
//
|
|
3637
|
-
//
|
|
3638
|
-
//
|
|
3639
|
-
//
|
|
3640
|
-
//
|
|
3641
|
-
if (url.pathname.startsWith('/api/
|
|
3642
|
-
const
|
|
3643
|
-
if (
|
|
2902
|
+
// Operation registry routes (plan-005): every registered operation is
|
|
2903
|
+
// dispatched here — webview automation, /api/canvas/*, /api/ext-app/*,
|
|
2904
|
+
// theme/update/viewport/annotation/refresh (plan-009 C1 slices 1-3). A
|
|
2905
|
+
// null return falls through to the remaining hand-written routes
|
|
2906
|
+
// (SSE, binary, HTML/static serving, and the server-coupled handlers).
|
|
2907
|
+
if (url.pathname.startsWith('/api/')) {
|
|
2908
|
+
const operationResponse = await dispatchOperationRoute(req, url);
|
|
2909
|
+
if (operationResponse) return operationResponse;
|
|
3644
2910
|
}
|
|
3645
2911
|
|
|
3646
2912
|
if (url.pathname === '/api/workbench/webview/screenshot' && req.method === 'POST') {
|
|
@@ -3655,194 +2921,22 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
3655
2921
|
return handleRender(req);
|
|
3656
2922
|
}
|
|
3657
2923
|
|
|
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
2924
|
if (url.pathname.startsWith('/api/canvas/image/') && req.method === 'GET') {
|
|
3699
2925
|
return await handleCanvasImage(url.pathname);
|
|
3700
2926
|
}
|
|
3701
2927
|
|
|
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
2928
|
if (url.pathname === '/api/canvas/open-external' && req.method === 'POST') {
|
|
3725
2929
|
return handleOpenExternalSurface(req);
|
|
3726
2930
|
}
|
|
3727
2931
|
|
|
3728
|
-
//
|
|
3729
|
-
|
|
3730
|
-
//
|
|
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
|
-
}
|
|
2932
|
+
// Every other /api/canvas/ax* route, pinned-context, and code-graph is
|
|
2933
|
+
// registry-served (plan-007 slices; plan-009 C1 slice 2 folded the GET
|
|
2934
|
+
// reads + activity/interaction/PATCH): src/server/operations/ops/ax-*.ts.
|
|
3813
2935
|
|
|
3814
2936
|
if (url.pathname === '/api/canvas/prompt' && req.method === 'POST') {
|
|
3815
2937
|
return handleCanvasPrompt(req);
|
|
3816
2938
|
}
|
|
3817
2939
|
|
|
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
2940
|
// Static files for canvas SPA bundle
|
|
3847
2941
|
if (url.pathname.startsWith('/canvas/')) {
|
|
3848
2942
|
const staticResponse = serveCanvasStatic(url.pathname);
|