pmx-canvas 0.1.36 → 0.2.0
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 +409 -0
- package/Readme.md +2 -2
- package/dist/json-render/index.js +89 -334
- package/dist/types/mcp/canvas-access.d.ts +5 -171
- package/dist/types/server/ax-state-manager.d.ts +256 -0
- package/dist/types/server/ax-state.d.ts +1 -1
- package/dist/types/server/canvas-operations.d.ts +1 -12
- package/dist/types/server/canvas-state.d.ts +3 -23
- package/dist/types/server/index.d.ts +6 -24
- package/dist/types/server/operations/composites.d.ts +121 -0
- package/dist/types/server/operations/http.d.ts +7 -0
- package/dist/types/server/operations/index.d.ts +8 -0
- package/dist/types/server/operations/invoker.d.ts +13 -0
- package/dist/types/server/operations/mcp.d.ts +15 -0
- package/dist/types/server/operations/ops/annotation.d.ts +2 -0
- package/dist/types/server/operations/ops/app.d.ts +33 -0
- package/dist/types/server/operations/ops/ax-await.d.ts +2 -0
- package/dist/types/server/operations/ops/ax-shared.d.ts +31 -0
- package/dist/types/server/operations/ops/ax-state.d.ts +2 -0
- package/dist/types/server/operations/ops/ax-timeline.d.ts +2 -0
- package/dist/types/server/operations/ops/ax-work.d.ts +2 -0
- package/dist/types/server/operations/ops/batch.d.ts +19 -0
- package/dist/types/server/operations/ops/edges.d.ts +2 -0
- package/dist/types/server/operations/ops/groups.d.ts +2 -0
- package/dist/types/server/operations/ops/json-render.d.ts +31 -0
- package/dist/types/server/operations/ops/nodes.d.ts +62 -0
- package/dist/types/server/operations/ops/query.d.ts +2 -0
- package/dist/types/server/operations/ops/snapshots.d.ts +2 -0
- package/dist/types/server/operations/ops/validate.d.ts +2 -0
- package/dist/types/server/operations/ops/viewport.d.ts +2 -0
- package/dist/types/server/operations/ops/webview.d.ts +2 -0
- package/dist/types/server/operations/registry.d.ts +15 -0
- package/dist/types/server/operations/types.d.ts +116 -0
- package/dist/types/server/operations/webview-runner.d.ts +69 -0
- package/docs/RELEASE.md +5 -0
- package/docs/adr-001-bun-only-runtime.md +46 -0
- package/docs/api-stability.md +57 -0
- package/docs/ax-state-contract.md +72 -0
- package/docs/mcp.md +60 -11
- package/docs/plans/plan-005-operation-registry.md +84 -0
- package/docs/plans/plan-006-mcp-tool-consolidation.md +109 -0
- package/docs/plans/plan-007-ax-domain.md +99 -0
- package/docs/plans/plan-008-registry-finish.md +91 -0
- package/docs/tech-debt-assessment-2026-06.md +90 -0
- package/package.json +3 -3
- package/skills/pmx-canvas/SKILL.md +192 -186
- package/skills/pmx-canvas/evals/evals.json +3 -3
- package/skills/pmx-canvas/references/codex-app-adapter.md +13 -14
- package/skills/pmx-canvas/references/github-copilot-app-adapter.md +4 -5
- package/src/cli/agent.ts +52 -31
- package/src/mcp/canvas-access.ts +30 -830
- package/src/mcp/server.ts +162 -2014
- package/src/server/ax-state-manager.ts +808 -0
- package/src/server/ax-state.ts +2 -2
- package/src/server/canvas-operations.ts +2 -328
- package/src/server/canvas-schema.ts +2 -2
- package/src/server/canvas-state.ts +95 -465
- package/src/server/index.ts +54 -190
- package/src/server/operations/composites.ts +355 -0
- package/src/server/operations/http.ts +103 -0
- package/src/server/operations/index.ts +65 -0
- package/src/server/operations/invoker.ts +87 -0
- package/src/server/operations/mcp.ts +221 -0
- package/src/server/operations/ops/annotation.ts +60 -0
- package/src/server/operations/ops/app.ts +447 -0
- package/src/server/operations/ops/ax-await.ts +216 -0
- package/src/server/operations/ops/ax-shared.ts +38 -0
- package/src/server/operations/ops/ax-state.ts +249 -0
- package/src/server/operations/ops/ax-timeline.ts +381 -0
- package/src/server/operations/ops/ax-work.ts +635 -0
- package/src/server/operations/ops/batch.ts +365 -0
- package/src/server/operations/ops/edges.ts +166 -0
- package/src/server/operations/ops/groups.ts +176 -0
- package/src/server/operations/ops/json-render.ts +691 -0
- package/src/server/operations/ops/nodes.ts +1047 -0
- package/src/server/operations/ops/query.ts +281 -0
- package/src/server/operations/ops/snapshots.ts +366 -0
- package/src/server/operations/ops/validate.ts +37 -0
- package/src/server/operations/ops/viewport.ts +219 -0
- package/src/server/operations/ops/webview.ts +339 -0
- package/src/server/operations/registry.ts +79 -0
- package/src/server/operations/types.ts +150 -0
- package/src/server/operations/webview-runner.ts +77 -0
- package/src/server/server.ts +158 -2255
- package/src/server/web-artifacts.ts +6 -2
package/src/server/server.ts
CHANGED
|
@@ -47,7 +47,7 @@ import type {
|
|
|
47
47
|
ListResourceTemplatesResult,
|
|
48
48
|
ListToolsResult,
|
|
49
49
|
} from '@modelcontextprotocol/sdk/types.js';
|
|
50
|
-
import { type CanvasAnnotation, type
|
|
50
|
+
import { type CanvasAnnotation, type CanvasLayout, type CanvasNodeState, IMAGE_MIME_MAP, canvasState } from './canvas-state.js';
|
|
51
51
|
import { buildAxBridge, buildAxStateBridge, buildContentHeightReporter, buildHtmlSurfaceDocument, HTML_SURFACE_SANDBOX, normalizeSurfaceTheme } from './html-surface.js';
|
|
52
52
|
import { findCanvasExtAppNodeId as findCanvasExtAppNodeIdShared } from './ext-app-lookup.js';
|
|
53
53
|
import { normalizeExtAppToolResult } from './ext-app-tool-result.js';
|
|
@@ -60,99 +60,54 @@ import {
|
|
|
60
60
|
listMcpAppResources,
|
|
61
61
|
listMcpAppResourceTemplates,
|
|
62
62
|
listMcpAppTools,
|
|
63
|
-
openMcpApp,
|
|
64
63
|
readMcpAppResource,
|
|
65
|
-
type ExternalMcpTransportConfig,
|
|
66
64
|
} from './mcp-app-runtime.js';
|
|
67
65
|
import { findOpenCanvasPosition, computeGroupBounds } from './placement.js';
|
|
68
|
-
import {
|
|
69
|
-
import {
|
|
70
|
-
import {
|
|
71
|
-
buildCanvasSummary,
|
|
72
|
-
serializeCanvasLayout,
|
|
73
|
-
serializeCanvasLayoutWithBlobSummaries,
|
|
74
|
-
serializeCanvasNode,
|
|
75
|
-
serializeCanvasNodeWithBlobSummaries,
|
|
76
|
-
summarizeCanvasAnnotation,
|
|
77
|
-
} from './canvas-serialization.js';
|
|
66
|
+
import { mutationHistory } from './mutation-history.js';
|
|
67
|
+
import { summarizeCanvasAnnotation } from './canvas-serialization.js';
|
|
78
68
|
import { buildCodeGraphSummary, formatCodeGraph } from './code-graph.js';
|
|
79
69
|
import { buildAgentContextPreamble, serializeNodeForAgentContext } from './agent-context.js';
|
|
80
70
|
import { buildCanvasAxContext, buildCanvasAxSurfaceSnapshot } from './ax-context.js';
|
|
81
|
-
import { applyAxInteraction, resolveNodeAxCapabilities
|
|
82
|
-
import {
|
|
83
|
-
import { waitForAxResolution, AX_WAIT_MAX_MS } from './ax-wait.js';
|
|
71
|
+
import { applyAxInteraction, resolveNodeAxCapabilities } from './ax-interaction.js';
|
|
72
|
+
import { isAxEvidenceKind, isAxActivityKind } from './ax-state.js';
|
|
84
73
|
import type {
|
|
85
74
|
PmxAxEvidenceKind,
|
|
86
75
|
PmxAxPolicy,
|
|
87
76
|
PmxAxReviewAnchorType,
|
|
88
77
|
PmxAxReviewKind,
|
|
89
|
-
PmxAxReviewRegion,
|
|
90
78
|
PmxAxReviewSeverity,
|
|
91
|
-
PmxAxReviewStatus,
|
|
92
79
|
PmxAxSource,
|
|
93
80
|
PmxAxWorkItemStatus,
|
|
94
81
|
} from './ax-state.js';
|
|
95
82
|
import { normalizeCanvasTheme, type CanvasTheme } from './canvas-db.js';
|
|
96
83
|
import { validateLocalImageFile } from './image-source.js';
|
|
97
84
|
import {
|
|
98
|
-
addCanvasNode,
|
|
99
|
-
addCanvasEdge,
|
|
100
|
-
MARKDOWN_NODE_DEFAULT_SIZE,
|
|
101
|
-
MCP_APP_NODE_DEFAULT_SIZE,
|
|
102
|
-
IMAGE_NODE_DEFAULT_SIZE,
|
|
103
|
-
LEDGER_NODE_DEFAULT_SIZE,
|
|
104
85
|
applyCanvasNodeUpdates,
|
|
105
|
-
appendCanvasJsonRenderStream,
|
|
106
|
-
buildStructuredNodeUpdate,
|
|
107
|
-
arrangeCanvasNodes,
|
|
108
|
-
clearCanvas,
|
|
109
|
-
createCanvasGraphNode,
|
|
110
|
-
createCanvasGroup,
|
|
111
|
-
createCanvasJsonRenderNode,
|
|
112
|
-
createCanvasStreamingJsonRenderNode,
|
|
113
|
-
deleteCanvasSnapshot,
|
|
114
|
-
executeCanvasBatch,
|
|
115
|
-
fitCanvasView,
|
|
116
|
-
gcCanvasSnapshots,
|
|
117
|
-
groupCanvasNodes,
|
|
118
|
-
listCanvasSnapshots,
|
|
119
86
|
refreshCanvasWebpageNode,
|
|
120
|
-
removeCanvasNode,
|
|
121
|
-
removeCanvasEdge,
|
|
122
|
-
resolveHtmlContent,
|
|
123
|
-
restoreCanvasSnapshot,
|
|
124
|
-
saveCanvasSnapshot,
|
|
125
|
-
scheduleCodeGraphRecompute,
|
|
126
87
|
primeCanvasRuntimeBackends,
|
|
127
88
|
setCanvasLayoutUpdateEmitter,
|
|
128
89
|
syncCanvasRuntimeBackends,
|
|
129
|
-
setCanvasContextPins,
|
|
130
|
-
ungroupCanvasNodes,
|
|
131
|
-
validateCanvasNodePatch,
|
|
132
|
-
hasStructuredNodeUpdateFields,
|
|
133
|
-
hasTraceNodeDataFields,
|
|
134
|
-
mergeTraceNodeDataFields,
|
|
135
90
|
} from './canvas-operations.js';
|
|
136
|
-
import {
|
|
137
|
-
import {
|
|
138
|
-
import {
|
|
91
|
+
import { dispatchOperationRoute, setOperationEventEmitter } from './operations/index.js';
|
|
92
|
+
import { setWebviewRunner } from './operations/webview-runner.js';
|
|
93
|
+
import {
|
|
94
|
+
closeNodeAppSession,
|
|
95
|
+
nodeAppSessionId,
|
|
96
|
+
} from './operations/ops/nodes.js';
|
|
139
97
|
import {
|
|
140
98
|
EXCALIDRAW_READ_CHECKPOINT_TOOL,
|
|
141
99
|
EXCALIDRAW_SAVE_CHECKPOINT_TOOL,
|
|
142
100
|
buildExcalidrawCheckpointId,
|
|
143
|
-
buildExcalidrawOpenMcpAppInput,
|
|
144
101
|
buildExcalidrawRestoreCheckpointToolInput,
|
|
145
102
|
ensureExcalidrawCheckpointId,
|
|
146
103
|
getExcalidrawCheckpointIdFromToolResult,
|
|
147
104
|
isExcalidrawCreateView,
|
|
148
105
|
} from './diagram-presets.js';
|
|
149
106
|
import { traceManager } from './trace-manager.js';
|
|
150
|
-
import { buildWebArtifactOnCanvas, resolveWorkspacePath } from './web-artifacts.js';
|
|
151
107
|
import {
|
|
152
108
|
buildJsonRenderViewerHtml,
|
|
153
109
|
} from '../json-render/server.js';
|
|
154
110
|
import {
|
|
155
|
-
WEBPAGE_NODE_DEFAULT_SIZE,
|
|
156
111
|
normalizeWebpageUrl,
|
|
157
112
|
} from './webpage-node.js';
|
|
158
113
|
import type { JsonRenderSpec } from '../json-render/server.js';
|
|
@@ -172,6 +127,52 @@ let primaryWorkbenchAutoOpenEnabled = true;
|
|
|
172
127
|
const initialCanvasThemeSetting = normalizeCanvasTheme(process.env.PMX_CANVAS_THEME);
|
|
173
128
|
let lastWorkbenchContextCardsEnvelope: Record<string, unknown> | null = null;
|
|
174
129
|
|
|
130
|
+
// Operation-registry SSE wiring (plan-005): the registry never imports this
|
|
131
|
+
// module — the workbench event emitter is injected here, mirroring the
|
|
132
|
+
// setCanvasLayoutUpdateEmitter pattern. Wired at module top level so local
|
|
133
|
+
// (in-process) MCP/SDK invocations emit even without startCanvasServer().
|
|
134
|
+
setOperationEventEmitter((event, payload) => {
|
|
135
|
+
emitPrimaryWorkbenchEvent(event, payload);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Webview-runner wiring (plan-008 Wave 3): the webview ops never import this
|
|
139
|
+
// module — the Bun.WebView automation runner is injected here, mirroring the
|
|
140
|
+
// setOperationEventEmitter pattern. The closures call the real automation
|
|
141
|
+
// functions (declared below, hoisted) so a webview op resolves to the same
|
|
142
|
+
// machinery the legacy hand-written tools/routes used. `screenshot` stays out —
|
|
143
|
+
// it returns binary and remains the standalone canvas_screenshot tool.
|
|
144
|
+
setWebviewRunner({
|
|
145
|
+
status: () => getCanvasAutomationWebViewStatus(),
|
|
146
|
+
start: async (options) => {
|
|
147
|
+
const url = currentWorkbenchUrl();
|
|
148
|
+
if (!url) {
|
|
149
|
+
// Mirrors the legacy 503 "server not running" branch: no URL → not a
|
|
150
|
+
// start failure but a precondition error. Surface it through the
|
|
151
|
+
// error-shaped result so the op can map it to the same 503 wire body
|
|
152
|
+
// (no webview field, matching the legacy handler).
|
|
153
|
+
return {
|
|
154
|
+
ok: false as const,
|
|
155
|
+
serverNotRunning: true as const,
|
|
156
|
+
error: 'Canvas server is not running.',
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
const webview = await startCanvasAutomationWebView(url, options);
|
|
161
|
+
return { ok: true as const, webview };
|
|
162
|
+
} catch (error) {
|
|
163
|
+
return {
|
|
164
|
+
ok: false as const,
|
|
165
|
+
error: error instanceof Error ? error.message : String(error),
|
|
166
|
+
// 500-vs-501 is read off webview.supported (the status), so no separate field.
|
|
167
|
+
webview: getCanvasAutomationWebViewStatus(),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
stop: () => stopCanvasAutomationWebView(),
|
|
172
|
+
resize: (width, height) => resizeCanvasAutomationWebView(width, height),
|
|
173
|
+
evaluate: (expression) => evaluateCanvasAutomationWebView(expression),
|
|
174
|
+
});
|
|
175
|
+
|
|
175
176
|
function normalizeGraphViewerSpec(
|
|
176
177
|
node: { type: string; data: Record<string, unknown> },
|
|
177
178
|
spec: JsonRenderSpec,
|
|
@@ -719,155 +720,6 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
719
720
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
720
721
|
}
|
|
721
722
|
|
|
722
|
-
function pickFiniteNumber(record: Record<string, unknown>, key: string): number | undefined {
|
|
723
|
-
const value = record[key];
|
|
724
|
-
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
function getRecord(value: unknown): Record<string, unknown> | undefined {
|
|
728
|
-
return isRecord(value) ? value : undefined;
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
function pickPositiveNumber(record: Record<string, unknown>, key: string): number | undefined {
|
|
732
|
-
const value = pickFiniteNumber(record, key);
|
|
733
|
-
return value !== undefined && value > 0 ? value : undefined;
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
function normalizeGeometryInput(body: Record<string, unknown>): {
|
|
737
|
-
x?: number;
|
|
738
|
-
y?: number;
|
|
739
|
-
width?: number;
|
|
740
|
-
height?: number;
|
|
741
|
-
position?: { x?: number; y?: number };
|
|
742
|
-
size?: { width?: number; height?: number };
|
|
743
|
-
} {
|
|
744
|
-
const position = getRecord(body.position);
|
|
745
|
-
const size = getRecord(body.size);
|
|
746
|
-
return {
|
|
747
|
-
...(pickFiniteNumber(body, 'x') !== undefined ? { x: pickFiniteNumber(body, 'x') } : {}),
|
|
748
|
-
...(pickFiniteNumber(body, 'y') !== undefined ? { y: pickFiniteNumber(body, 'y') } : {}),
|
|
749
|
-
...(pickFiniteNumber(body, 'width') !== undefined ? { width: pickFiniteNumber(body, 'width') } : {}),
|
|
750
|
-
...(pickFiniteNumber(body, 'height') !== undefined ? { height: pickFiniteNumber(body, 'height') } : {}),
|
|
751
|
-
...(position ? {
|
|
752
|
-
position: {
|
|
753
|
-
...(pickFiniteNumber(position, 'x') !== undefined ? { x: pickFiniteNumber(position, 'x') } : {}),
|
|
754
|
-
...(pickFiniteNumber(position, 'y') !== undefined ? { y: pickFiniteNumber(position, 'y') } : {}),
|
|
755
|
-
},
|
|
756
|
-
} : {}),
|
|
757
|
-
...(size ? {
|
|
758
|
-
size: {
|
|
759
|
-
...(pickFiniteNumber(size, 'width') !== undefined ? { width: pickFiniteNumber(size, 'width') } : {}),
|
|
760
|
-
...(pickFiniteNumber(size, 'height') !== undefined ? { height: pickFiniteNumber(size, 'height') } : {}),
|
|
761
|
-
},
|
|
762
|
-
} : {}),
|
|
763
|
-
};
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
function resolveCreateGeometry(body: Record<string, unknown>): {
|
|
767
|
-
x?: number;
|
|
768
|
-
y?: number;
|
|
769
|
-
width?: number;
|
|
770
|
-
height?: number;
|
|
771
|
-
} {
|
|
772
|
-
const geometry = normalizeGeometryInput(body);
|
|
773
|
-
const x = geometry.x ?? geometry.position?.x;
|
|
774
|
-
const y = geometry.y ?? geometry.position?.y;
|
|
775
|
-
const width = geometry.width ?? geometry.size?.width;
|
|
776
|
-
const height = geometry.height ?? geometry.size?.height;
|
|
777
|
-
return {
|
|
778
|
-
...(x !== undefined ? { x } : {}),
|
|
779
|
-
...(y !== undefined ? { y } : {}),
|
|
780
|
-
...(width !== undefined ? { width } : {}),
|
|
781
|
-
...(height !== undefined ? { height } : {}),
|
|
782
|
-
};
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
function resolvePatchGeometry(
|
|
786
|
-
body: Record<string, unknown>,
|
|
787
|
-
existing: CanvasNodeState,
|
|
788
|
-
): {
|
|
789
|
-
position?: { x: number; y: number };
|
|
790
|
-
size?: { width: number; height: number };
|
|
791
|
-
} {
|
|
792
|
-
const geometry = normalizeGeometryInput(body);
|
|
793
|
-
const x = geometry.x ?? geometry.position?.x;
|
|
794
|
-
const y = geometry.y ?? geometry.position?.y;
|
|
795
|
-
const width = geometry.width ?? geometry.size?.width;
|
|
796
|
-
const height = geometry.height ?? geometry.size?.height;
|
|
797
|
-
return {
|
|
798
|
-
...(x !== undefined || y !== undefined
|
|
799
|
-
? { position: { x: x ?? existing.position.x, y: y ?? existing.position.y } }
|
|
800
|
-
: {}),
|
|
801
|
-
...(width !== undefined || height !== undefined
|
|
802
|
-
? { size: { width: width ?? existing.size.width, height: height ?? existing.size.height } }
|
|
803
|
-
: {}),
|
|
804
|
-
};
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
function parseGraphPayloadData(value: unknown): Array<Record<string, unknown>> | null {
|
|
808
|
-
if (!Array.isArray(value)) return null;
|
|
809
|
-
if (value.some((item) => !isRecord(item))) return null;
|
|
810
|
-
return value as Array<Record<string, unknown>>;
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
type StringListField = { value?: string[]; error?: string };
|
|
814
|
-
|
|
815
|
-
function parseStringListField(field: string, value: unknown): StringListField {
|
|
816
|
-
if (value === undefined) return {};
|
|
817
|
-
if (!Array.isArray(value)) return { error: `"${field}" must be an array of node IDs.` };
|
|
818
|
-
const invalid = value.find((item) => typeof item !== 'string' || item.trim().length === 0);
|
|
819
|
-
if (invalid !== undefined) return { error: `"${field}" must contain only non-empty node IDs.` };
|
|
820
|
-
return { value };
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
function pickGroupChildIds(body: Record<string, unknown>): StringListField {
|
|
824
|
-
if ('children' in body) return parseStringListField('children', body.children);
|
|
825
|
-
if ('childIds' in body) return parseStringListField('childIds', body.childIds);
|
|
826
|
-
const data = isRecord(body.data) ? body.data : undefined;
|
|
827
|
-
return data && 'children' in data ? parseStringListField('data.children', data.children) : {};
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
function validateGroupChildIds(groupId: string, childIds: string[]): string | null {
|
|
831
|
-
const missingChildIds = childIds.filter((id) => !canvasState.getNode(id));
|
|
832
|
-
if (missingChildIds.length > 0) {
|
|
833
|
-
return `Missing child node ID${missingChildIds.length === 1 ? '' : 's'}: ${missingChildIds.join(', ')}.`;
|
|
834
|
-
}
|
|
835
|
-
const invalidChildIds = childIds.filter((id) => {
|
|
836
|
-
const node = canvasState.getNode(id);
|
|
837
|
-
return id === groupId || node?.type === 'group';
|
|
838
|
-
});
|
|
839
|
-
if (invalidChildIds.length > 0) {
|
|
840
|
-
return `Invalid group child ID${invalidChildIds.length === 1 ? '' : 's'}: ${invalidChildIds.join(', ')}.`;
|
|
841
|
-
}
|
|
842
|
-
return null;
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
function setGroupChildrenFromApi(groupId: string, childIds: string[]): boolean {
|
|
846
|
-
const group = canvasState.getNode(groupId);
|
|
847
|
-
if (!group || group.type !== 'group') return false;
|
|
848
|
-
|
|
849
|
-
const dataChildIds = Array.isArray(group.data.children)
|
|
850
|
-
? group.data.children.filter((id): id is string => typeof id === 'string')
|
|
851
|
-
: [];
|
|
852
|
-
const parentBackrefIds = canvasState.getLayout().nodes
|
|
853
|
-
.filter((node) => node.id !== groupId && node.data.parentGroup === groupId)
|
|
854
|
-
.map((node) => node.id);
|
|
855
|
-
const currentChildIds = [...new Set([...dataChildIds, ...parentBackrefIds])];
|
|
856
|
-
if (currentChildIds.length > 0) {
|
|
857
|
-
if (currentChildIds.length !== dataChildIds.length || currentChildIds.some((id) => !dataChildIds.includes(id))) {
|
|
858
|
-
canvasState.updateNode(groupId, { data: { ...group.data, children: currentChildIds } });
|
|
859
|
-
}
|
|
860
|
-
canvasState.ungroupNodes(groupId);
|
|
861
|
-
}
|
|
862
|
-
if (childIds.length === 0) return true;
|
|
863
|
-
|
|
864
|
-
const latestGroup = canvasState.getNode(groupId);
|
|
865
|
-
return canvasState.groupNodes(groupId, childIds, {
|
|
866
|
-
preservePositions: true,
|
|
867
|
-
keepGroupFrame: latestGroup?.data.frameMode === 'manual',
|
|
868
|
-
});
|
|
869
|
-
}
|
|
870
|
-
|
|
871
723
|
function getExtAppNodeCheckpointId(node: CanvasNodeState): string {
|
|
872
724
|
const appCheckpoint = isRecord(node.data.appCheckpoint) ? node.data.appCheckpoint : null;
|
|
873
725
|
const storedCheckpointId = appCheckpoint?.id;
|
|
@@ -1075,34 +927,6 @@ async function readJson(req: Request): Promise<Record<string, unknown>> {
|
|
|
1075
927
|
}
|
|
1076
928
|
}
|
|
1077
929
|
|
|
1078
|
-
/**
|
|
1079
|
-
* Like {@link readJson}, but PRESERVES a top-level JSON array. For endpoints that
|
|
1080
|
-
* accept either an object or a bare array (e.g. `/api/canvas/batch`, whose CLI
|
|
1081
|
-
* help and handler both document a bare `[...]` form). readJson coerces arrays to
|
|
1082
|
-
* `{}` so object-shaped handlers never crash on `body.field`; this variant keeps
|
|
1083
|
-
* the array so the handler's array branch can run. Empty/whitespace/malformed
|
|
1084
|
-
* bodies still resolve to `{}`.
|
|
1085
|
-
*/
|
|
1086
|
-
async function readJsonObjectOrArray(req: Request): Promise<Record<string, unknown> | unknown[]> {
|
|
1087
|
-
let text = '';
|
|
1088
|
-
try {
|
|
1089
|
-
text = await req.text();
|
|
1090
|
-
} catch (error) {
|
|
1091
|
-
logWorkbenchWarning('readJson', error);
|
|
1092
|
-
return {};
|
|
1093
|
-
}
|
|
1094
|
-
if (!text.trim()) return {};
|
|
1095
|
-
try {
|
|
1096
|
-
const value = JSON.parse(text) as unknown;
|
|
1097
|
-
if (Array.isArray(value)) return value;
|
|
1098
|
-
if (!value || typeof value !== 'object') return {};
|
|
1099
|
-
return value as Record<string, unknown>;
|
|
1100
|
-
} catch (error) {
|
|
1101
|
-
logWorkbenchWarning('readJson', error);
|
|
1102
|
-
return {};
|
|
1103
|
-
}
|
|
1104
|
-
}
|
|
1105
|
-
|
|
1106
930
|
function htmlEscape(value: string): string {
|
|
1107
931
|
return value
|
|
1108
932
|
.replaceAll('&', '&')
|
|
@@ -1643,14 +1467,6 @@ async function handleCanvasAddAnnotation(req: Request): Promise<Response> {
|
|
|
1643
1467
|
return responseJson({ ok: true, annotation: summarizeCanvasAnnotation(annotation) });
|
|
1644
1468
|
}
|
|
1645
1469
|
|
|
1646
|
-
function handleCanvasRemoveAnnotation(id: string): Response {
|
|
1647
|
-
const decodedId = decodeURIComponent(id);
|
|
1648
|
-
const removed = canvasState.removeAnnotation(decodedId);
|
|
1649
|
-
if (!removed) return responseJson({ ok: false, error: `Annotation "${decodedId}" not found.` }, 404);
|
|
1650
|
-
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
1651
|
-
return responseJson({ ok: true, removed: decodedId });
|
|
1652
|
-
}
|
|
1653
|
-
|
|
1654
1470
|
// ── Serve image file for image nodes ─────────────────────────
|
|
1655
1471
|
async function handleCanvasImage(pathname: string): Promise<Response> {
|
|
1656
1472
|
const nodeId = pathname.replace('/api/canvas/image/', '');
|
|
@@ -1687,384 +1503,6 @@ async function handleCanvasImage(pathname: string): Promise<Response> {
|
|
|
1687
1503
|
});
|
|
1688
1504
|
}
|
|
1689
1505
|
|
|
1690
|
-
// ── Add node from client ─────────────────────────────────────
|
|
1691
|
-
const VALID_NODE_TYPES = new Set(['markdown', 'status', 'context', 'ledger', 'trace', 'file', 'image', 'mcp-app', 'webpage', 'html', 'group']);
|
|
1692
|
-
|
|
1693
|
-
function buildNodeResponse(node: CanvasNodeState): Record<string, unknown> {
|
|
1694
|
-
const serialized = serializeCanvasNode(node);
|
|
1695
|
-
return {
|
|
1696
|
-
ok: true,
|
|
1697
|
-
node: serialized,
|
|
1698
|
-
...serialized,
|
|
1699
|
-
// `nodeId` aliases `id` so HTTP/CLI node-create responses match the MCP
|
|
1700
|
-
// createdNodePayload — agents using either key (or a cached schema) work.
|
|
1701
|
-
nodeId: node.id,
|
|
1702
|
-
};
|
|
1703
|
-
}
|
|
1704
|
-
|
|
1705
|
-
function withContextPinReadState(node: CanvasNodeState): CanvasNodeState {
|
|
1706
|
-
return {
|
|
1707
|
-
...node,
|
|
1708
|
-
pinned: node.pinned || canvasState.contextPinnedNodeIds.has(node.id),
|
|
1709
|
-
};
|
|
1710
|
-
}
|
|
1711
|
-
|
|
1712
|
-
function withContextPinLayoutReadState(layout: CanvasLayout): CanvasLayout {
|
|
1713
|
-
return {
|
|
1714
|
-
...layout,
|
|
1715
|
-
nodes: layout.nodes.map(withContextPinReadState),
|
|
1716
|
-
};
|
|
1717
|
-
}
|
|
1718
|
-
|
|
1719
|
-
async function createCanvasWebpageNode(body: Record<string, unknown>): Promise<Response> {
|
|
1720
|
-
const rawUrl = typeof body.url === 'string' && body.url.trim().length > 0
|
|
1721
|
-
? body.url
|
|
1722
|
-
: typeof body.content === 'string'
|
|
1723
|
-
? body.content
|
|
1724
|
-
: '';
|
|
1725
|
-
|
|
1726
|
-
let normalizedUrl: string;
|
|
1727
|
-
try {
|
|
1728
|
-
normalizedUrl = normalizeWebpageUrl(rawUrl);
|
|
1729
|
-
} catch (error) {
|
|
1730
|
-
return responseJson({ ok: false, error: error instanceof Error ? error.message : 'Invalid webpage URL.' }, 400);
|
|
1731
|
-
}
|
|
1732
|
-
|
|
1733
|
-
const extraData = body.data && typeof body.data === 'object' && !Array.isArray(body.data)
|
|
1734
|
-
? body.data as Record<string, unknown>
|
|
1735
|
-
: undefined;
|
|
1736
|
-
const geometry = resolveCreateGeometry(body);
|
|
1737
|
-
const { id, node } = addCanvasNode({
|
|
1738
|
-
type: 'webpage',
|
|
1739
|
-
...(typeof body.title === 'string' ? { title: body.title } : {}),
|
|
1740
|
-
content: normalizedUrl,
|
|
1741
|
-
...(extraData ? { data: extraData } : {}),
|
|
1742
|
-
...(body.strictSize === true ? { strictSize: true } : {}),
|
|
1743
|
-
...geometry,
|
|
1744
|
-
...(geometry.width === undefined ? { width: WEBPAGE_NODE_DEFAULT_SIZE.width } : {}),
|
|
1745
|
-
...(geometry.height === undefined ? { height: WEBPAGE_NODE_DEFAULT_SIZE.height } : {}),
|
|
1746
|
-
});
|
|
1747
|
-
|
|
1748
|
-
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
1749
|
-
const refreshed = await refreshCanvasWebpageNode(id);
|
|
1750
|
-
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
1751
|
-
const created = canvasState.getNode(id) ?? node;
|
|
1752
|
-
return responseJson({
|
|
1753
|
-
...buildNodeResponse(created),
|
|
1754
|
-
fetch: refreshed.ok
|
|
1755
|
-
? { ok: true }
|
|
1756
|
-
: { ok: false, error: refreshed.error ?? 'Failed to fetch webpage content.' },
|
|
1757
|
-
...(refreshed.ok ? {} : { error: refreshed.error }),
|
|
1758
|
-
});
|
|
1759
|
-
}
|
|
1760
|
-
|
|
1761
|
-
async function handleCanvasAddNode(req: Request): Promise<Response> {
|
|
1762
|
-
const body = await readJson(req);
|
|
1763
|
-
const queryType = new URL(req.url).searchParams.get('type');
|
|
1764
|
-
// Report #50: require a resolvable type rather than silently defaulting to a
|
|
1765
|
-
// markdown node — an empty / type-less body created a phantom node before.
|
|
1766
|
-
const type = typeof body.type === 'string' ? body.type : (queryType || '');
|
|
1767
|
-
if (!type) {
|
|
1768
|
-
return responseJson({
|
|
1769
|
-
ok: false,
|
|
1770
|
-
error: `node creation requires a 'type' — pass it in the JSON body ({ "type": "markdown", ... }) or as a ?type= query param. Valid types: ${[...VALID_NODE_TYPES].join(', ')} (json-render / graph / web-artifact have dedicated endpoints).`,
|
|
1771
|
-
}, 400);
|
|
1772
|
-
}
|
|
1773
|
-
|
|
1774
|
-
if (!VALID_NODE_TYPES.has(type)) {
|
|
1775
|
-
if (type === 'json-render') {
|
|
1776
|
-
return responseJson({
|
|
1777
|
-
ok: false,
|
|
1778
|
-
error: 'Node type "json-render" is created via POST /api/canvas/json-render. See /api/canvas/schema for the required spec shape.',
|
|
1779
|
-
}, 400);
|
|
1780
|
-
}
|
|
1781
|
-
if (type === 'graph') {
|
|
1782
|
-
return responseJson({
|
|
1783
|
-
ok: false,
|
|
1784
|
-
error: 'Node type "graph" is created via POST /api/canvas/graph. See /api/canvas/schema for graphType + data fields.',
|
|
1785
|
-
}, 400);
|
|
1786
|
-
}
|
|
1787
|
-
if (type === 'web-artifact') {
|
|
1788
|
-
return responseJson({
|
|
1789
|
-
ok: false,
|
|
1790
|
-
error: 'Node type "web-artifact" is created via POST /api/canvas/web-artifact with appTsx + title.',
|
|
1791
|
-
}, 400);
|
|
1792
|
-
}
|
|
1793
|
-
if (type === 'html-primitive') {
|
|
1794
|
-
return createCanvasHtmlPrimitiveNode(body);
|
|
1795
|
-
}
|
|
1796
|
-
return responseJson({ ok: false, error: `Invalid node type: "${type}".` }, 400);
|
|
1797
|
-
}
|
|
1798
|
-
|
|
1799
|
-
if (type === 'webpage') {
|
|
1800
|
-
return createCanvasWebpageNode(body);
|
|
1801
|
-
}
|
|
1802
|
-
|
|
1803
|
-
if (type === 'html' && (typeof body.primitive === 'string' || typeof body.kind === 'string')) {
|
|
1804
|
-
return createCanvasHtmlPrimitiveNode(body);
|
|
1805
|
-
}
|
|
1806
|
-
|
|
1807
|
-
if (type === 'group') {
|
|
1808
|
-
const geometry = resolveCreateGeometry(body);
|
|
1809
|
-
const childList = pickGroupChildIds(body);
|
|
1810
|
-
if (childList.error) return responseJson({ ok: false, error: `Cannot create group: ${childList.error}` }, 400);
|
|
1811
|
-
const childIds = childList.value ?? [];
|
|
1812
|
-
const childError = validateGroupChildIds('', childIds);
|
|
1813
|
-
if (childError) return responseJson({ ok: false, error: `Cannot create group: ${childError}` }, 400);
|
|
1814
|
-
const { node } = createCanvasGroup({
|
|
1815
|
-
...(typeof body.title === 'string' ? { title: body.title } : {}),
|
|
1816
|
-
childIds,
|
|
1817
|
-
...(typeof body.color === 'string' ? { color: body.color } : {}),
|
|
1818
|
-
...geometry,
|
|
1819
|
-
});
|
|
1820
|
-
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
1821
|
-
return responseJson(buildNodeResponse(node));
|
|
1822
|
-
}
|
|
1823
|
-
|
|
1824
|
-
const extraData = body.data && typeof body.data === 'object' && !Array.isArray(body.data)
|
|
1825
|
-
? body.data as Record<string, unknown>
|
|
1826
|
-
: undefined;
|
|
1827
|
-
if (type === 'html') {
|
|
1828
|
-
if ('html' in body && typeof body.html !== 'string') {
|
|
1829
|
-
return responseJson({ ok: false, error: 'HTML node field "html" must be a string.' }, 400);
|
|
1830
|
-
}
|
|
1831
|
-
if (extraData && 'html' in extraData && typeof extraData.html !== 'string') {
|
|
1832
|
-
return responseJson({ ok: false, error: 'HTML node field "data.html" must be a string.' }, 400);
|
|
1833
|
-
}
|
|
1834
|
-
}
|
|
1835
|
-
const content = type === 'image' && typeof body.path === 'string' && typeof body.content !== 'string'
|
|
1836
|
-
? body.path
|
|
1837
|
-
: body.content;
|
|
1838
|
-
// For html nodes, accept top-level `html` AND `axCapabilities` and merge into data
|
|
1839
|
-
// so callers can POST { type: 'html', title, html, axCapabilities } without nesting
|
|
1840
|
-
// under `data` (report #53 — transport parity with MCP canvas_add_html_node). A
|
|
1841
|
-
// top-level value overrides the same key under `data` (mirrors the `html` precedence).
|
|
1842
|
-
const topAxCapabilities = type === 'html' ? normalizeNodeAxCapabilities(body.axCapabilities) : null;
|
|
1843
|
-
const htmlMergedData = type === 'html'
|
|
1844
|
-
? {
|
|
1845
|
-
...(extraData ?? {}),
|
|
1846
|
-
...(typeof body.html === 'string' ? { html: resolveHtmlContent(body.html) } : {}),
|
|
1847
|
-
...(typeof body.summary === 'string' ? { summary: body.summary } : {}),
|
|
1848
|
-
...(typeof body.agentSummary === 'string' ? { agentSummary: body.agentSummary } : {}),
|
|
1849
|
-
...(typeof body.description === 'string' ? { description: body.description } : {}),
|
|
1850
|
-
...(body.presentation === true ? { presentation: true } : {}),
|
|
1851
|
-
...(Array.isArray(body.slideTitles) ? { slideTitles: body.slideTitles } : {}),
|
|
1852
|
-
...(Array.isArray(body.embeddedNodeIds) ? { embeddedNodeIds: body.embeddedNodeIds } : {}),
|
|
1853
|
-
...(Array.isArray(body.embeddedUrls) ? { embeddedUrls: body.embeddedUrls } : {}),
|
|
1854
|
-
...(topAxCapabilities ? { axCapabilities: topAxCapabilities } : {}),
|
|
1855
|
-
}
|
|
1856
|
-
: extraData;
|
|
1857
|
-
let added: ReturnType<typeof addCanvasNode>;
|
|
1858
|
-
const geometry = resolveCreateGeometry(body);
|
|
1859
|
-
try {
|
|
1860
|
-
added = addCanvasNode({
|
|
1861
|
-
type: type as CanvasNodeState['type'],
|
|
1862
|
-
...(typeof body.title === 'string' ? { title: body.title } : {}),
|
|
1863
|
-
...(typeof content === 'string' ? { content } : {}),
|
|
1864
|
-
...(htmlMergedData && Object.keys(htmlMergedData).length > 0 ? { data: htmlMergedData } : {}),
|
|
1865
|
-
...(type === 'trace' && typeof body.toolName === 'string' ? { toolName: body.toolName } : {}),
|
|
1866
|
-
...(type === 'trace' && typeof body.category === 'string' ? { category: body.category } : {}),
|
|
1867
|
-
...(type === 'trace' && typeof body.status === 'string' ? { status: body.status } : {}),
|
|
1868
|
-
...(type === 'trace' && typeof body.duration === 'string' ? { duration: body.duration } : {}),
|
|
1869
|
-
...(type === 'trace' && typeof body.resultSummary === 'string' ? { resultSummary: body.resultSummary } : {}),
|
|
1870
|
-
...(type === 'trace' && typeof body.error === 'string' ? { error: body.error } : {}),
|
|
1871
|
-
...(body.strictSize === true ? { strictSize: true } : {}),
|
|
1872
|
-
...geometry,
|
|
1873
|
-
defaultWidth: type === 'html'
|
|
1874
|
-
? 720
|
|
1875
|
-
: type === 'markdown'
|
|
1876
|
-
? MARKDOWN_NODE_DEFAULT_SIZE.width
|
|
1877
|
-
: type === 'mcp-app'
|
|
1878
|
-
? MCP_APP_NODE_DEFAULT_SIZE.width
|
|
1879
|
-
: type === 'image'
|
|
1880
|
-
? IMAGE_NODE_DEFAULT_SIZE.width
|
|
1881
|
-
: type === 'ledger'
|
|
1882
|
-
? LEDGER_NODE_DEFAULT_SIZE.width
|
|
1883
|
-
: 360,
|
|
1884
|
-
defaultHeight: type === 'html'
|
|
1885
|
-
? 640
|
|
1886
|
-
: type === 'markdown'
|
|
1887
|
-
? MARKDOWN_NODE_DEFAULT_SIZE.height
|
|
1888
|
-
: type === 'mcp-app'
|
|
1889
|
-
? MCP_APP_NODE_DEFAULT_SIZE.height
|
|
1890
|
-
: type === 'image'
|
|
1891
|
-
? IMAGE_NODE_DEFAULT_SIZE.height
|
|
1892
|
-
: type === 'ledger'
|
|
1893
|
-
? LEDGER_NODE_DEFAULT_SIZE.height
|
|
1894
|
-
: 200,
|
|
1895
|
-
fileMode: 'auto',
|
|
1896
|
-
});
|
|
1897
|
-
} catch (error) {
|
|
1898
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1899
|
-
return responseJson({ ok: false, error: message }, 400);
|
|
1900
|
-
}
|
|
1901
|
-
const { node, needsCodeGraphRecompute } = added;
|
|
1902
|
-
|
|
1903
|
-
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
1904
|
-
if (needsCodeGraphRecompute) {
|
|
1905
|
-
scheduleCodeGraphRecompute(() => {
|
|
1906
|
-
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
1907
|
-
});
|
|
1908
|
-
}
|
|
1909
|
-
return responseJson(buildNodeResponse(node));
|
|
1910
|
-
}
|
|
1911
|
-
|
|
1912
|
-
function createCanvasHtmlPrimitiveNode(body: Record<string, unknown>): Response {
|
|
1913
|
-
const rawKind = typeof body.primitive === 'string' ? body.primitive : body.kind;
|
|
1914
|
-
if (typeof rawKind !== 'string' || !isHtmlPrimitiveKind(rawKind)) {
|
|
1915
|
-
return responseJson({ ok: false, error: `Unknown HTML primitive: ${String(rawKind)}.` }, 400);
|
|
1916
|
-
}
|
|
1917
|
-
const data = isRecord(body.data) ? body.data : {};
|
|
1918
|
-
let built: ReturnType<typeof buildHtmlPrimitive>;
|
|
1919
|
-
try {
|
|
1920
|
-
built = buildHtmlPrimitive({
|
|
1921
|
-
kind: rawKind,
|
|
1922
|
-
...(typeof body.title === 'string' ? { title: body.title } : {}),
|
|
1923
|
-
data,
|
|
1924
|
-
});
|
|
1925
|
-
} catch (error) {
|
|
1926
|
-
return responseJson({ ok: false, error: error instanceof Error ? error.message : String(error) }, 400);
|
|
1927
|
-
}
|
|
1928
|
-
const geometry = resolveCreateGeometry(body);
|
|
1929
|
-
const { node } = addCanvasNode({
|
|
1930
|
-
type: 'html',
|
|
1931
|
-
title: built.title,
|
|
1932
|
-
data: {
|
|
1933
|
-
html: built.html,
|
|
1934
|
-
htmlPrimitive: built.kind,
|
|
1935
|
-
primitiveData: built.data,
|
|
1936
|
-
description: built.summary,
|
|
1937
|
-
agentSummary: typeof data.agentSummary === 'string' ? data.agentSummary : built.summary,
|
|
1938
|
-
...(typeof data.summary === 'string' ? { summary: data.summary } : {}),
|
|
1939
|
-
...getHtmlPrimitiveSemanticMetadata(built.data),
|
|
1940
|
-
},
|
|
1941
|
-
...(body.strictSize === true ? { strictSize: true } : {}),
|
|
1942
|
-
...geometry,
|
|
1943
|
-
defaultWidth: built.defaultSize.width,
|
|
1944
|
-
defaultHeight: built.defaultSize.height,
|
|
1945
|
-
});
|
|
1946
|
-
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
1947
|
-
return responseJson({
|
|
1948
|
-
...buildNodeResponse(node),
|
|
1949
|
-
primitive: {
|
|
1950
|
-
kind: built.kind,
|
|
1951
|
-
title: built.title,
|
|
1952
|
-
htmlBytes: Buffer.byteLength(built.html, 'utf-8'),
|
|
1953
|
-
defaultSize: built.defaultSize,
|
|
1954
|
-
},
|
|
1955
|
-
});
|
|
1956
|
-
}
|
|
1957
|
-
|
|
1958
|
-
// ── Group operations ─────────────────────────────────────────
|
|
1959
|
-
async function handleCanvasCreateGroup(req: Request): Promise<Response> {
|
|
1960
|
-
const body = await readJson(req);
|
|
1961
|
-
const title = typeof body.title === 'string' ? body.title : 'Group';
|
|
1962
|
-
const childIds = Array.isArray(body.childIds) ? body.childIds.filter((id: unknown) => typeof id === 'string') : [];
|
|
1963
|
-
const color = typeof body.color === 'string' ? body.color : undefined;
|
|
1964
|
-
const x = typeof body.x === 'number' ? body.x : undefined;
|
|
1965
|
-
const y = typeof body.y === 'number' ? body.y : undefined;
|
|
1966
|
-
const width = typeof body.width === 'number' ? body.width : undefined;
|
|
1967
|
-
const height = typeof body.height === 'number' ? body.height : undefined;
|
|
1968
|
-
const childLayout =
|
|
1969
|
-
body.childLayout === 'grid' || body.childLayout === 'column' || body.childLayout === 'flow'
|
|
1970
|
-
? body.childLayout
|
|
1971
|
-
: undefined;
|
|
1972
|
-
if (childIds.length > 0) {
|
|
1973
|
-
const missingChildIds = childIds.filter((id) => !canvasState.getNode(id));
|
|
1974
|
-
if (missingChildIds.length > 0) {
|
|
1975
|
-
return responseJson({
|
|
1976
|
-
ok: false,
|
|
1977
|
-
error: `Cannot create group: missing child node ID${missingChildIds.length === 1 ? '' : 's'}: ${missingChildIds.join(', ')}.`,
|
|
1978
|
-
}, 400);
|
|
1979
|
-
}
|
|
1980
|
-
}
|
|
1981
|
-
|
|
1982
|
-
const { node } = createCanvasGroup({ title, childIds, color, x, y, width, height, ...(childLayout ? { childLayout } : {}) });
|
|
1983
|
-
|
|
1984
|
-
broadcastWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
1985
|
-
return responseJson(buildNodeResponse(node));
|
|
1986
|
-
}
|
|
1987
|
-
|
|
1988
|
-
async function handleCanvasGroupNodes(req: Request): Promise<Response> {
|
|
1989
|
-
const body = await readJson(req);
|
|
1990
|
-
const groupId = body.groupId as string;
|
|
1991
|
-
const childIds = Array.isArray(body.childIds) ? body.childIds.filter((id: unknown) => typeof id === 'string') : [];
|
|
1992
|
-
const childLayout =
|
|
1993
|
-
body.childLayout === 'grid' || body.childLayout === 'column' || body.childLayout === 'flow'
|
|
1994
|
-
? body.childLayout
|
|
1995
|
-
: undefined;
|
|
1996
|
-
if (!groupId || childIds.length === 0) {
|
|
1997
|
-
return responseJson({ ok: false, error: 'Missing groupId or childIds.' }, 400);
|
|
1998
|
-
}
|
|
1999
|
-
const { ok } = groupCanvasNodes(groupId, childIds, childLayout ? { childLayout } : {});
|
|
2000
|
-
if (!ok) return responseJson({ ok: false, error: 'Group not found or no valid children.' }, 400);
|
|
2001
|
-
broadcastWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
2002
|
-
return responseJson({ ok: true, groupId });
|
|
2003
|
-
}
|
|
2004
|
-
|
|
2005
|
-
async function handleCanvasUngroupNodes(req: Request): Promise<Response> {
|
|
2006
|
-
const body = await readJson(req);
|
|
2007
|
-
const groupId = body.groupId as string;
|
|
2008
|
-
if (!groupId) return responseJson({ ok: false, error: 'Missing groupId.' }, 400);
|
|
2009
|
-
const { ok } = ungroupCanvasNodes(groupId);
|
|
2010
|
-
if (!ok) return responseJson({ ok: false, error: 'Group not found or empty.' }, 400);
|
|
2011
|
-
broadcastWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
2012
|
-
return responseJson({ ok: true, groupId });
|
|
2013
|
-
}
|
|
2014
|
-
|
|
2015
|
-
const VALID_EDGE_TYPES = new Set(['relation', 'depends-on', 'flow', 'references']);
|
|
2016
|
-
const VALID_EDGE_STYLES = new Set(['solid', 'dashed', 'dotted']);
|
|
2017
|
-
|
|
2018
|
-
async function handleCanvasAddEdge(req: Request): Promise<Response> {
|
|
2019
|
-
const body = await readJson(req);
|
|
2020
|
-
const type = body.type as string;
|
|
2021
|
-
const style = typeof body.style === 'string' ? body.style : undefined;
|
|
2022
|
-
|
|
2023
|
-
if (
|
|
2024
|
-
!type ||
|
|
2025
|
-
(!body.from && !body.fromSearch) ||
|
|
2026
|
-
(!body.to && !body.toSearch)
|
|
2027
|
-
) {
|
|
2028
|
-
return responseJson({ ok: false, error: 'Missing required fields: type plus from/fromSearch and to/toSearch.' }, 400);
|
|
2029
|
-
}
|
|
2030
|
-
if (!VALID_EDGE_TYPES.has(type)) {
|
|
2031
|
-
return responseJson({ ok: false, error: `Invalid edge type: "${type}".` }, 400);
|
|
2032
|
-
}
|
|
2033
|
-
if (style && !VALID_EDGE_STYLES.has(style)) {
|
|
2034
|
-
return responseJson({ ok: false, error: `Invalid edge style: "${style}". Use solid, dashed, or dotted.` }, 400);
|
|
2035
|
-
}
|
|
2036
|
-
try {
|
|
2037
|
-
const result = addCanvasEdge({
|
|
2038
|
-
...(typeof body.from === 'string' ? { from: body.from } : {}),
|
|
2039
|
-
...(typeof body.to === 'string' ? { to: body.to } : {}),
|
|
2040
|
-
...(typeof body.fromSearch === 'string' ? { fromSearch: body.fromSearch } : {}),
|
|
2041
|
-
...(typeof body.toSearch === 'string' ? { toSearch: body.toSearch } : {}),
|
|
2042
|
-
type: type as CanvasEdge['type'],
|
|
2043
|
-
...(body.label ? { label: String(body.label) } : {}),
|
|
2044
|
-
...(style ? { style: style as CanvasEdge['style'] } : {}),
|
|
2045
|
-
...(body.animated !== undefined ? { animated: Boolean(body.animated) } : {}),
|
|
2046
|
-
});
|
|
2047
|
-
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
2048
|
-
return responseJson({ ok: true, ...result });
|
|
2049
|
-
} catch (error) {
|
|
2050
|
-
return responseJson({ ok: false, error: error instanceof Error ? error.message : 'Duplicate or self-edge.' }, 400);
|
|
2051
|
-
}
|
|
2052
|
-
}
|
|
2053
|
-
|
|
2054
|
-
async function handleCanvasRemoveEdge(req: Request): Promise<Response> {
|
|
2055
|
-
const body = await readJson(req);
|
|
2056
|
-
const edgeId = body.edge_id as string;
|
|
2057
|
-
if (!edgeId) {
|
|
2058
|
-
return responseJson({ ok: false, error: 'Missing edge_id.' }, 400);
|
|
2059
|
-
}
|
|
2060
|
-
const { removed } = removeCanvasEdge(edgeId);
|
|
2061
|
-
if (!removed) {
|
|
2062
|
-
return responseJson({ ok: false, error: `Edge "${edgeId}" not found.` }, 404);
|
|
2063
|
-
}
|
|
2064
|
-
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
2065
|
-
return responseJson({ ok: true, removed: edgeId });
|
|
2066
|
-
}
|
|
2067
|
-
|
|
2068
1506
|
async function handleCanvasRefreshWebpageNode(nodeId: string, req: Request): Promise<Response> {
|
|
2069
1507
|
const existing = canvasState.getNode(nodeId);
|
|
2070
1508
|
if (!existing || existing.type !== 'webpage') {
|
|
@@ -2087,513 +1525,28 @@ async function handleCanvasRefreshWebpageNode(nodeId: string, req: Request): Pro
|
|
|
2087
1525
|
return responseJson(result, result.ok ? 200 : 400);
|
|
2088
1526
|
}
|
|
2089
1527
|
|
|
2090
|
-
//
|
|
2091
|
-
|
|
2092
|
-
const existing = canvasState.getNode(nodeId);
|
|
2093
|
-
if (!existing) return responseJson({ ok: false, error: `Node "${nodeId}" not found.` }, 404);
|
|
2094
|
-
const body = await readJson(req);
|
|
2095
|
-
if (existing.type === 'webpage' && body.refresh === true) {
|
|
2096
|
-
return handleCanvasRefreshWebpageNode(nodeId, req);
|
|
2097
|
-
}
|
|
2098
|
-
const groupChildList = existing.type === 'group' ? pickGroupChildIds(body) : {};
|
|
2099
|
-
if (groupChildList.error) return responseJson({ ok: false, error: `Cannot update group: ${groupChildList.error}` }, 400);
|
|
2100
|
-
const groupChildIds = groupChildList.value;
|
|
2101
|
-
if (groupChildIds !== undefined) {
|
|
2102
|
-
const childError = validateGroupChildIds(nodeId, groupChildIds);
|
|
2103
|
-
if (childError) return responseJson({ ok: false, error: `Cannot update group: ${childError}` }, 400);
|
|
2104
|
-
}
|
|
2105
|
-
const patch: Record<string, unknown> = resolvePatchGeometry(body, existing);
|
|
2106
|
-
if (body.collapsed !== undefined) patch.collapsed = body.collapsed;
|
|
2107
|
-
if (body.pinned !== undefined) patch.pinned = Boolean(body.pinned);
|
|
2108
|
-
if (body.dockPosition === null || body.dockPosition === 'left' || body.dockPosition === 'right') {
|
|
2109
|
-
patch.dockPosition = body.dockPosition;
|
|
2110
|
-
}
|
|
2111
|
-
if (hasStructuredNodeUpdateFields(body)) {
|
|
2112
|
-
try {
|
|
2113
|
-
patch.data = buildStructuredNodeUpdate(existing, body).data;
|
|
2114
|
-
} catch (error) {
|
|
2115
|
-
return responseJson({ ok: false, error: error instanceof Error ? error.message : String(error) }, 400);
|
|
2116
|
-
}
|
|
2117
|
-
} else if (
|
|
2118
|
-
body.title !== undefined ||
|
|
2119
|
-
body.content !== undefined ||
|
|
2120
|
-
body.data ||
|
|
2121
|
-
typeof body.arrangeLocked === 'boolean' ||
|
|
2122
|
-
typeof body.strictSize === 'boolean' ||
|
|
2123
|
-
(existing.type === 'trace' && hasTraceNodeDataFields(body)) ||
|
|
2124
|
-
(existing.type === 'html' && (body.html !== undefined || body.axCapabilities !== undefined))
|
|
2125
|
-
) {
|
|
2126
|
-
const data = { ...existing.data };
|
|
2127
|
-
if (body.title !== undefined) {
|
|
2128
|
-
data.title = String(body.title);
|
|
2129
|
-
if (existing.type === 'webpage') {
|
|
2130
|
-
data.titleSource = 'user';
|
|
2131
|
-
}
|
|
2132
|
-
}
|
|
2133
|
-
if (body.content !== undefined) data.content = String(body.content);
|
|
2134
|
-
if (typeof body.arrangeLocked === 'boolean') data.arrangeLocked = body.arrangeLocked;
|
|
2135
|
-
if (typeof body.strictSize === 'boolean') data.strictSize = body.strictSize;
|
|
2136
|
-
// Merge extra data fields (for status, context, ledger, trace nodes)
|
|
2137
|
-
if (body.data && typeof body.data === 'object' && !Array.isArray(body.data)) {
|
|
2138
|
-
Object.assign(data, body.data as Record<string, unknown>);
|
|
2139
|
-
}
|
|
2140
|
-
// Report #53: for html nodes, accept top-level `html` / `axCapabilities` on PATCH
|
|
2141
|
-
// too (top-level overrides the `data.*` merge above — matches POST + MCP parity).
|
|
2142
|
-
if (existing.type === 'html') {
|
|
2143
|
-
if (body.html !== undefined) {
|
|
2144
|
-
if (typeof body.html !== 'string') {
|
|
2145
|
-
return responseJson({ ok: false, error: 'HTML node field "html" must be a string.' }, 400);
|
|
2146
|
-
}
|
|
2147
|
-
data.html = resolveHtmlContent(body.html);
|
|
2148
|
-
}
|
|
2149
|
-
const patchAxCapabilities = normalizeNodeAxCapabilities(body.axCapabilities);
|
|
2150
|
-
if (patchAxCapabilities) data.axCapabilities = patchAxCapabilities;
|
|
2151
|
-
}
|
|
2152
|
-
if (existing.type === 'webpage') {
|
|
2153
|
-
const nextUrl = typeof body.url === 'string'
|
|
2154
|
-
? body.url
|
|
2155
|
-
: typeof (body.data as Record<string, unknown> | undefined)?.url === 'string'
|
|
2156
|
-
? (body.data as Record<string, unknown>).url as string
|
|
2157
|
-
: undefined;
|
|
2158
|
-
if (typeof nextUrl === 'string' && nextUrl.trim().length > 0) {
|
|
2159
|
-
try {
|
|
2160
|
-
data.url = normalizeWebpageUrl(nextUrl);
|
|
2161
|
-
} catch (error) {
|
|
2162
|
-
return responseJson({ ok: false, error: error instanceof Error ? error.message : 'Invalid webpage URL.' }, 400);
|
|
2163
|
-
}
|
|
2164
|
-
}
|
|
2165
|
-
}
|
|
2166
|
-
patch.data = existing.type === 'trace'
|
|
2167
|
-
? mergeTraceNodeDataFields(data, body)
|
|
2168
|
-
: data;
|
|
2169
|
-
}
|
|
2170
|
-
const error = validateCanvasNodePatch({
|
|
2171
|
-
...(patch.position ? { position: patch.position as { x: number; y: number } } : {}),
|
|
2172
|
-
...(patch.size ? { size: patch.size as { width: number; height: number } } : {}),
|
|
2173
|
-
});
|
|
2174
|
-
if (error) return responseJson({ ok: false, error }, 400);
|
|
2175
|
-
canvasState.updateNode(nodeId, patch as Partial<CanvasNodeState>);
|
|
2176
|
-
if (groupChildIds !== undefined && !setGroupChildrenFromApi(nodeId, groupChildIds)) {
|
|
2177
|
-
return responseJson({ ok: false, error: `Group "${nodeId}" not found.` }, 404);
|
|
2178
|
-
}
|
|
2179
|
-
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
2180
|
-
const updated = canvasState.getNode(nodeId);
|
|
2181
|
-
return responseJson(updated ? buildNodeResponse(updated) : { ok: true, id: nodeId });
|
|
2182
|
-
}
|
|
2183
|
-
|
|
2184
|
-
// ── Arrange nodes ───────────────────────────────────────────
|
|
2185
|
-
async function handleCanvasArrange(req: Request): Promise<Response> {
|
|
2186
|
-
const body = await readJson(req);
|
|
2187
|
-
const layout = typeof body.layout === 'string' ? body.layout : 'grid';
|
|
2188
|
-
if (!['grid', 'column', 'flow'].includes(layout)) {
|
|
2189
|
-
return responseJson({ ok: false, error: `Invalid layout: "${layout}". Use: grid, column, flow` }, 400);
|
|
2190
|
-
}
|
|
2191
|
-
const result = arrangeCanvasNodes(layout as 'grid' | 'column' | 'flow');
|
|
2192
|
-
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
2193
|
-
const validation = validateCanvasLayout(canvasState.getLayout());
|
|
2194
|
-
return responseJson({
|
|
2195
|
-
ok: validation.ok,
|
|
2196
|
-
arranged: result.arranged,
|
|
2197
|
-
layout: result.layout,
|
|
2198
|
-
...(validation.ok ? {} : { validation, collisions: validation.summary.collisions }),
|
|
2199
|
-
});
|
|
2200
|
-
}
|
|
1528
|
+
// handleCanvasBuildWebArtifact migrated to the operation registry
|
|
1529
|
+
// (plan-008 Wave 4): src/server/operations/ops/app.ts (webartifact.build).
|
|
2201
1530
|
|
|
2202
|
-
|
|
2203
|
-
async function handleCanvasFocus(req: Request): Promise<Response> {
|
|
1531
|
+
async function handleCanvasThemeUpdate(req: Request): Promise<Response> {
|
|
2204
1532
|
const body = await readJson(req);
|
|
2205
|
-
const
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
const noPan = body.noPan === true;
|
|
2210
|
-
if (!noPan) {
|
|
2211
|
-
canvasState.setViewport({ x: node.position.x - 100, y: node.position.y - 100 });
|
|
2212
|
-
} else {
|
|
2213
|
-
const maxZ = canvasState.getLayout().nodes.reduce((max, layoutNode) => Math.max(max, layoutNode.zIndex), 0);
|
|
2214
|
-
canvasState.updateNode(nodeId, { zIndex: maxZ + 1 });
|
|
2215
|
-
}
|
|
2216
|
-
const focus = canvasState.setAxFocus([nodeId], { source: 'api', recordHistory: false });
|
|
2217
|
-
broadcastWorkbenchEvent('ax-state-changed', {
|
|
2218
|
-
focus,
|
|
1533
|
+
const theme = normalizeCanvasTheme(body.theme, canvasState.theme);
|
|
1534
|
+
const next = canvasState.setTheme(theme);
|
|
1535
|
+
broadcastWorkbenchEvent('theme-changed', {
|
|
1536
|
+
theme: next,
|
|
2219
1537
|
sessionId: primaryWorkbenchSessionId,
|
|
2220
1538
|
timestamp: new Date().toISOString(),
|
|
2221
1539
|
});
|
|
2222
|
-
|
|
2223
|
-
if (!noPan) emitPrimaryWorkbenchEvent('canvas-viewport-update', { viewport: canvasState.viewport });
|
|
2224
|
-
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
2225
|
-
return responseJson({ ok: true, focused: nodeId, panned: !noPan, axFocus: focus });
|
|
1540
|
+
return responseJson({ ok: true, theme: next });
|
|
2226
1541
|
}
|
|
2227
1542
|
|
|
2228
|
-
async function
|
|
2229
|
-
const
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
...(typeof body.height === 'number' ? { height: body.height } : {}),
|
|
2236
|
-
...(typeof body.padding === 'number' ? { padding: body.padding } : {}),
|
|
2237
|
-
...(typeof body.maxScale === 'number' ? { maxScale: body.maxScale } : {}),
|
|
2238
|
-
...(nodeIds ? { nodeIds } : {}),
|
|
2239
|
-
});
|
|
2240
|
-
emitPrimaryWorkbenchEvent('canvas-viewport-update', { viewport: result.viewport });
|
|
2241
|
-
return responseJson(result);
|
|
2242
|
-
}
|
|
2243
|
-
|
|
2244
|
-
async function handleCanvasBuildWebArtifact(req: Request): Promise<Response> {
|
|
2245
|
-
const body = await readJson(req);
|
|
2246
|
-
const title = typeof body.title === 'string' ? body.title.trim() : '';
|
|
2247
|
-
const appTsx = typeof body.appTsx === 'string' ? body.appTsx : '';
|
|
2248
|
-
if (!title || !appTsx) {
|
|
2249
|
-
return responseJson({ ok: false, error: 'Missing required fields: title, appTsx.' }, 400);
|
|
2250
|
-
}
|
|
2251
|
-
|
|
2252
|
-
const files: Record<string, string> = {};
|
|
2253
|
-
if (body.files && typeof body.files === 'object' && !Array.isArray(body.files)) {
|
|
2254
|
-
for (const [pathKey, value] of Object.entries(body.files as Record<string, unknown>)) {
|
|
2255
|
-
if (typeof value === 'string') files[pathKey] = value;
|
|
2256
|
-
}
|
|
2257
|
-
}
|
|
2258
|
-
|
|
2259
|
-
try {
|
|
2260
|
-
const result = await buildWebArtifactOnCanvas({
|
|
2261
|
-
title,
|
|
2262
|
-
appTsx,
|
|
2263
|
-
...(typeof body.indexCss === 'string' ? { indexCss: body.indexCss } : {}),
|
|
2264
|
-
...(typeof body.mainTsx === 'string' ? { mainTsx: body.mainTsx } : {}),
|
|
2265
|
-
...(typeof body.indexHtml === 'string' ? { indexHtml: body.indexHtml } : {}),
|
|
2266
|
-
...(Object.keys(files).length > 0 ? { files } : {}),
|
|
2267
|
-
...(typeof body.projectPath === 'string'
|
|
2268
|
-
? { projectPath: resolveWorkspacePath(body.projectPath, activeWorkspaceRoot) }
|
|
2269
|
-
: {}),
|
|
2270
|
-
...(typeof body.outputPath === 'string'
|
|
2271
|
-
? { outputPath: resolveWorkspacePath(body.outputPath, activeWorkspaceRoot) }
|
|
2272
|
-
: {}),
|
|
2273
|
-
// Script-path overrides are honored only when contained inside the
|
|
2274
|
-
// workspace (enforced by resolveTrustedScriptPath in
|
|
2275
|
-
// executeWebArtifactBuild), so they cannot point at an arbitrary host
|
|
2276
|
-
// script for bash execution.
|
|
2277
|
-
...(typeof body.initScriptPath === 'string'
|
|
2278
|
-
? { initScriptPath: body.initScriptPath }
|
|
2279
|
-
: {}),
|
|
2280
|
-
...(typeof body.bundleScriptPath === 'string'
|
|
2281
|
-
? { bundleScriptPath: body.bundleScriptPath }
|
|
2282
|
-
: {}),
|
|
2283
|
-
...(Array.isArray(body.deps)
|
|
2284
|
-
? { deps: body.deps.filter((dep): dep is string => typeof dep === 'string') }
|
|
2285
|
-
: {}),
|
|
2286
|
-
...(typeof body.timeoutMs === 'number' ? { timeoutMs: body.timeoutMs } : {}),
|
|
2287
|
-
...(typeof body.openInCanvas === 'boolean' ? { openInCanvas: body.openInCanvas } : {}),
|
|
2288
|
-
});
|
|
2289
|
-
|
|
2290
|
-
return responseJson({
|
|
2291
|
-
ok: true,
|
|
2292
|
-
path: result.filePath,
|
|
2293
|
-
bytes: result.fileSize,
|
|
2294
|
-
projectPath: result.projectPath,
|
|
2295
|
-
openedInCanvas: result.openedInCanvas,
|
|
2296
|
-
startedAt: result.startedAt,
|
|
2297
|
-
completedAt: result.completedAt,
|
|
2298
|
-
durationMs: result.durationMs,
|
|
2299
|
-
timeoutMs: result.timeoutMs,
|
|
2300
|
-
// `id` is the canvas node id alias used by every other add-style
|
|
2301
|
-
// response. It is only present when a canvas node was actually
|
|
2302
|
-
// created (i.e. openInCanvas was not explicitly disabled). When
|
|
2303
|
-
// there is no canvas node, the alias is intentionally omitted so
|
|
2304
|
-
// consumers can `'id' in response` to detect the build-only case.
|
|
2305
|
-
...(typeof result.nodeId === 'string' ? { id: result.nodeId } : {}),
|
|
2306
|
-
nodeId: result.nodeId,
|
|
2307
|
-
url: result.url,
|
|
2308
|
-
metadata: result.metadata,
|
|
2309
|
-
logs: result.logs,
|
|
2310
|
-
...(body.includeLogs === true ? {
|
|
2311
|
-
stdout: result.stdout,
|
|
2312
|
-
stderr: result.stderr,
|
|
2313
|
-
} : {}),
|
|
2314
|
-
});
|
|
2315
|
-
} catch (error) {
|
|
2316
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2317
|
-
return responseJson({ ok: false, error: message }, 400);
|
|
2318
|
-
}
|
|
2319
|
-
}
|
|
2320
|
-
|
|
2321
|
-
function handleCanvasDescribeSchema(): Response {
|
|
2322
|
-
return responseJson(describeCanvasSchema());
|
|
2323
|
-
}
|
|
2324
|
-
|
|
2325
|
-
async function handleCanvasValidateSpec(req: Request): Promise<Response> {
|
|
2326
|
-
const body = await readJson(req);
|
|
2327
|
-
const rawType = typeof body.type === 'string' ? body.type.trim() : '';
|
|
2328
|
-
if (rawType !== 'json-render' && rawType !== 'graph' && rawType !== 'html-primitive') {
|
|
2329
|
-
return responseJson({ ok: false, error: 'Validation type must be "json-render", "graph", or "html-primitive".' }, 400);
|
|
2330
|
-
}
|
|
2331
|
-
|
|
2332
|
-
try {
|
|
2333
|
-
if (rawType === 'json-render') {
|
|
2334
|
-
const rawSpec =
|
|
2335
|
-
body.spec && typeof body.spec === 'object' && !Array.isArray(body.spec)
|
|
2336
|
-
? body.spec
|
|
2337
|
-
: body;
|
|
2338
|
-
return responseJson(validateStructuredCanvasPayload({
|
|
2339
|
-
type: 'json-render',
|
|
2340
|
-
spec: rawSpec,
|
|
2341
|
-
}));
|
|
2342
|
-
}
|
|
2343
|
-
|
|
2344
|
-
if (rawType === 'html-primitive') {
|
|
2345
|
-
const kind = typeof body.kind === 'string'
|
|
2346
|
-
? body.kind
|
|
2347
|
-
: typeof body.primitive === 'string'
|
|
2348
|
-
? body.primitive
|
|
2349
|
-
: '';
|
|
2350
|
-
const data = isRecord(body.data) ? body.data : {};
|
|
2351
|
-
return responseJson(validateStructuredCanvasPayload({
|
|
2352
|
-
type: 'html-primitive',
|
|
2353
|
-
primitive: {
|
|
2354
|
-
kind,
|
|
2355
|
-
...(typeof body.title === 'string' ? { title: body.title } : {}),
|
|
2356
|
-
data,
|
|
2357
|
-
},
|
|
2358
|
-
}));
|
|
2359
|
-
}
|
|
2360
|
-
|
|
2361
|
-
const data = parseGraphPayloadData(body.data);
|
|
2362
|
-
if (!data) {
|
|
2363
|
-
return responseJson({ ok: false, error: 'Graph validation requires a data array.' }, 400);
|
|
2364
|
-
}
|
|
2365
|
-
|
|
2366
|
-
const aggregate =
|
|
2367
|
-
body.aggregate === 'sum' || body.aggregate === 'count' || body.aggregate === 'avg'
|
|
2368
|
-
? body.aggregate
|
|
2369
|
-
: undefined;
|
|
2370
|
-
|
|
2371
|
-
return responseJson(validateStructuredCanvasPayload({
|
|
2372
|
-
type: 'graph',
|
|
2373
|
-
graph: {
|
|
2374
|
-
title: typeof body.title === 'string' && body.title.trim() ? body.title.trim() : 'Graph',
|
|
2375
|
-
graphType: typeof body.graphType === 'string'
|
|
2376
|
-
? body.graphType
|
|
2377
|
-
: typeof body.typeName === 'string'
|
|
2378
|
-
? body.typeName
|
|
2379
|
-
: 'line',
|
|
2380
|
-
data,
|
|
2381
|
-
...(typeof body.xKey === 'string' ? { xKey: body.xKey } : {}),
|
|
2382
|
-
...(typeof body.yKey === 'string' ? { yKey: body.yKey } : {}),
|
|
2383
|
-
...(typeof body.zKey === 'string' ? { zKey: body.zKey } : {}),
|
|
2384
|
-
...(typeof body.nameKey === 'string' ? { nameKey: body.nameKey } : {}),
|
|
2385
|
-
...(typeof body.valueKey === 'string' ? { valueKey: body.valueKey } : {}),
|
|
2386
|
-
...(typeof body.axisKey === 'string' ? { axisKey: body.axisKey } : {}),
|
|
2387
|
-
...(Array.isArray(body.metrics)
|
|
2388
|
-
? { metrics: body.metrics.filter((m: unknown): m is string => typeof m === 'string') }
|
|
2389
|
-
: {}),
|
|
2390
|
-
...(Array.isArray(body.series)
|
|
2391
|
-
? { series: body.series.filter((s: unknown): s is string => typeof s === 'string') }
|
|
2392
|
-
: {}),
|
|
2393
|
-
...(typeof body.barKey === 'string' ? { barKey: body.barKey } : {}),
|
|
2394
|
-
...(typeof body.lineKey === 'string' ? { lineKey: body.lineKey } : {}),
|
|
2395
|
-
...(aggregate ? { aggregate } : {}),
|
|
2396
|
-
...(typeof body.color === 'string' ? { color: body.color } : {}),
|
|
2397
|
-
...(typeof body.barColor === 'string' ? { barColor: body.barColor } : {}),
|
|
2398
|
-
...(typeof body.lineColor === 'string' ? { lineColor: body.lineColor } : {}),
|
|
2399
|
-
...(typeof body.height === 'number' ? { height: body.height } : {}),
|
|
2400
|
-
},
|
|
2401
|
-
}));
|
|
2402
|
-
} catch (error) {
|
|
2403
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2404
|
-
return responseJson({ ok: false, error: message, type: rawType }, 400);
|
|
2405
|
-
}
|
|
2406
|
-
}
|
|
2407
|
-
|
|
2408
|
-
async function handleCanvasAddJsonRender(req: Request): Promise<Response> {
|
|
2409
|
-
const body = await readJson(req);
|
|
2410
|
-
const title = typeof body.title === 'string' ? body.title.trim() : '';
|
|
2411
|
-
const rawSpec =
|
|
2412
|
-
body.spec && typeof body.spec === 'object' && !Array.isArray(body.spec) ? body.spec : body;
|
|
2413
|
-
const geometry = resolveCreateGeometry(body);
|
|
2414
|
-
|
|
2415
|
-
try {
|
|
2416
|
-
const result = createCanvasJsonRenderNode({
|
|
2417
|
-
...(title ? { title } : {}),
|
|
2418
|
-
spec: rawSpec,
|
|
2419
|
-
...(body.strictSize === true ? { strictSize: true } : {}),
|
|
2420
|
-
...geometry,
|
|
2421
|
-
});
|
|
2422
|
-
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
2423
|
-
return responseJson({ ...buildNodeResponse(result.node), url: result.url, spec: result.spec });
|
|
2424
|
-
} catch (error) {
|
|
2425
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2426
|
-
return responseJson({ ok: false, error: message }, 400);
|
|
2427
|
-
}
|
|
2428
|
-
}
|
|
2429
|
-
|
|
2430
|
-
async function handleJsonRenderStream(req: Request): Promise<Response> {
|
|
2431
|
-
const body = await readJson(req);
|
|
2432
|
-
const patches = Array.isArray(body.patches) ? body.patches : [];
|
|
2433
|
-
const done = body.done === true;
|
|
2434
|
-
const geometry = resolveCreateGeometry(body);
|
|
2435
|
-
try {
|
|
2436
|
-
let nodeId = typeof body.nodeId === 'string' && body.nodeId ? body.nodeId : undefined;
|
|
2437
|
-
let url = '';
|
|
2438
|
-
if (!nodeId) {
|
|
2439
|
-
const created = createCanvasStreamingJsonRenderNode({
|
|
2440
|
-
...(typeof body.title === 'string' ? { title: body.title } : {}),
|
|
2441
|
-
...(body.strictSize === true ? { strictSize: true } : {}),
|
|
2442
|
-
...geometry,
|
|
2443
|
-
});
|
|
2444
|
-
nodeId = created.id;
|
|
2445
|
-
url = created.url;
|
|
2446
|
-
}
|
|
2447
|
-
const result = appendCanvasJsonRenderStream(nodeId, patches, done);
|
|
2448
|
-
if (!result.ok) return responseJson({ ok: false, error: result.error }, 400);
|
|
2449
|
-
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
2450
|
-
const node = canvasState.getNode(nodeId);
|
|
2451
|
-
return responseJson({ id: nodeId, url: url || String(node?.data.url ?? ''), ...result });
|
|
2452
|
-
} catch (error) {
|
|
2453
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2454
|
-
return responseJson({ ok: false, error: message }, 400);
|
|
2455
|
-
}
|
|
2456
|
-
}
|
|
2457
|
-
|
|
2458
|
-
async function handleCanvasAddGraph(req: Request): Promise<Response> {
|
|
2459
|
-
const body = await readJson(req);
|
|
2460
|
-
const title = typeof body.title === 'string' && body.title.trim() ? body.title.trim() : 'Graph';
|
|
2461
|
-
const graphType = typeof body.graphType === 'string' ? body.graphType : typeof body.type === 'string' ? body.type : 'line';
|
|
2462
|
-
const data = parseGraphPayloadData(body.data);
|
|
2463
|
-
if (!data) {
|
|
2464
|
-
return responseJson({ ok: false, error: 'Missing required field: data.' }, 400);
|
|
2465
|
-
}
|
|
2466
|
-
|
|
2467
|
-
try {
|
|
2468
|
-
const aggregate =
|
|
2469
|
-
body.aggregate === 'sum' || body.aggregate === 'count' || body.aggregate === 'avg'
|
|
2470
|
-
? body.aggregate
|
|
2471
|
-
: undefined;
|
|
2472
|
-
const metrics = Array.isArray(body.metrics)
|
|
2473
|
-
? body.metrics.filter((m: unknown): m is string => typeof m === 'string')
|
|
2474
|
-
: null;
|
|
2475
|
-
const series = Array.isArray(body.series)
|
|
2476
|
-
? body.series.filter((s: unknown): s is string => typeof s === 'string')
|
|
2477
|
-
: null;
|
|
2478
|
-
const position = getRecord(body.position);
|
|
2479
|
-
const size = getRecord(body.size);
|
|
2480
|
-
const x = pickFiniteNumber(body, 'x') ?? (position ? pickFiniteNumber(position, 'x') : undefined);
|
|
2481
|
-
const y = pickFiniteNumber(body, 'y') ?? (position ? pickFiniteNumber(position, 'y') : undefined);
|
|
2482
|
-
const width = pickPositiveNumber(body, 'width') ?? (size ? pickPositiveNumber(size, 'width') : undefined);
|
|
2483
|
-
// Node FRAME height. `body.height` is the CHART plot height (passed through as
|
|
2484
|
-
// `input.height` below), so the node frame accepts `nodeHeight` / `heightPx` /
|
|
2485
|
-
// `size.height` as aliases — `heightPx` matches createCanvasGraphNode's own input
|
|
2486
|
-
// field, the natural thing a caller reaches for. (With content-fit the node grows
|
|
2487
|
-
// to the chart anyway; this just removes the silent "height ignored" surprise.)
|
|
2488
|
-
const nodeHeight = pickPositiveNumber(body, 'nodeHeight')
|
|
2489
|
-
?? pickPositiveNumber(body, 'heightPx')
|
|
2490
|
-
?? (size ? pickPositiveNumber(size, 'height') : undefined);
|
|
2491
|
-
const showLegend = typeof body.showLegend === 'boolean' ? body.showLegend : undefined;
|
|
2492
|
-
const showLabels = typeof body.showLabels === 'boolean' ? body.showLabels : undefined;
|
|
2493
|
-
const colorBy =
|
|
2494
|
-
body.colorBy === 'series' || body.colorBy === 'category' || body.colorBy === 'value' || body.colorBy === 'none'
|
|
2495
|
-
? body.colorBy
|
|
2496
|
-
: undefined;
|
|
2497
|
-
const highlight =
|
|
2498
|
-
typeof body.highlight === 'number' || body.highlight === 'max' || body.highlight === 'min' || body.highlight === null
|
|
2499
|
-
? body.highlight
|
|
2500
|
-
: undefined;
|
|
2501
|
-
const sort =
|
|
2502
|
-
body.sort === 'asc' || body.sort === 'desc' || body.sort === 'none' ? body.sort : undefined;
|
|
2503
|
-
const result = createCanvasGraphNode({
|
|
2504
|
-
title,
|
|
2505
|
-
graphType,
|
|
2506
|
-
data,
|
|
2507
|
-
...(typeof body.xKey === 'string' ? { xKey: body.xKey } : {}),
|
|
2508
|
-
...(typeof body.yKey === 'string' ? { yKey: body.yKey } : {}),
|
|
2509
|
-
...(typeof body.zKey === 'string' ? { zKey: body.zKey } : {}),
|
|
2510
|
-
...(typeof body.nameKey === 'string' ? { nameKey: body.nameKey } : {}),
|
|
2511
|
-
...(typeof body.valueKey === 'string' ? { valueKey: body.valueKey } : {}),
|
|
2512
|
-
...(typeof body.axisKey === 'string' ? { axisKey: body.axisKey } : {}),
|
|
2513
|
-
...(metrics ? { metrics } : {}),
|
|
2514
|
-
...(series ? { series } : {}),
|
|
2515
|
-
...(typeof body.barKey === 'string' ? { barKey: body.barKey } : {}),
|
|
2516
|
-
...(typeof body.lineKey === 'string' ? { lineKey: body.lineKey } : {}),
|
|
2517
|
-
...(aggregate ? { aggregate } : {}),
|
|
2518
|
-
...(typeof body.color === 'string' ? { color: body.color } : {}),
|
|
2519
|
-
...(colorBy ? { colorBy } : {}),
|
|
2520
|
-
...(highlight !== undefined ? { highlight } : {}),
|
|
2521
|
-
...(typeof body.barColor === 'string' ? { barColor: body.barColor } : {}),
|
|
2522
|
-
...(typeof body.lineColor === 'string' ? { lineColor: body.lineColor } : {}),
|
|
2523
|
-
...(typeof body.labelKey === 'string' ? { labelKey: body.labelKey } : {}),
|
|
2524
|
-
...(typeof body.targetKey === 'string' ? { targetKey: body.targetKey } : {}),
|
|
2525
|
-
...(typeof body.rangesKey === 'string' ? { rangesKey: body.rangesKey } : {}),
|
|
2526
|
-
...(typeof body.beforeKey === 'string' ? { beforeKey: body.beforeKey } : {}),
|
|
2527
|
-
...(typeof body.afterKey === 'string' ? { afterKey: body.afterKey } : {}),
|
|
2528
|
-
...(typeof body.beforeLabel === 'string' ? { beforeLabel: body.beforeLabel } : {}),
|
|
2529
|
-
...(typeof body.afterLabel === 'string' ? { afterLabel: body.afterLabel } : {}),
|
|
2530
|
-
...(sort ? { sort } : {}),
|
|
2531
|
-
...(typeof body.fill === 'boolean' ? { fill: body.fill } : {}),
|
|
2532
|
-
...(typeof body.showEndDot === 'boolean' ? { showEndDot: body.showEndDot } : {}),
|
|
2533
|
-
...(typeof body.showMinMax === 'boolean' ? { showMinMax: body.showMinMax } : {}),
|
|
2534
|
-
...(typeof body.showValue === 'boolean' ? { showValue: body.showValue } : {}),
|
|
2535
|
-
...(typeof body.colorByDirection === 'boolean' ? { colorByDirection: body.colorByDirection } : {}),
|
|
2536
|
-
...(typeof body.height === 'number' ? { height: body.height } : {}),
|
|
2537
|
-
...(showLegend !== undefined ? { showLegend } : {}),
|
|
2538
|
-
...(showLabels !== undefined ? { showLabels } : {}),
|
|
2539
|
-
...(body.strictSize === true ? { strictSize: true } : {}),
|
|
2540
|
-
...(x !== undefined ? { x } : {}),
|
|
2541
|
-
...(y !== undefined ? { y } : {}),
|
|
2542
|
-
...(width !== undefined ? { width } : {}),
|
|
2543
|
-
...(nodeHeight !== undefined ? { heightPx: nodeHeight } : {}),
|
|
2544
|
-
});
|
|
2545
|
-
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
2546
|
-
return responseJson({ ...buildNodeResponse(result.node), url: result.url, spec: result.spec });
|
|
2547
|
-
} catch (error) {
|
|
2548
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2549
|
-
return responseJson({ ok: false, error: message }, 400);
|
|
2550
|
-
}
|
|
2551
|
-
}
|
|
2552
|
-
|
|
2553
|
-
async function handleCanvasBatch(req: Request): Promise<Response> {
|
|
2554
|
-
// Accept both documented shapes: { operations: [...] } and a bare [...] array.
|
|
2555
|
-
// Uses the array-preserving reader so the bare-array form isn't coerced to {}.
|
|
2556
|
-
const body = await readJsonObjectOrArray(req);
|
|
2557
|
-
const operations = Array.isArray(body)
|
|
2558
|
-
? body
|
|
2559
|
-
: Array.isArray(body.operations) ? body.operations : [];
|
|
2560
|
-
const normalized = operations
|
|
2561
|
-
.filter((operation): operation is Record<string, unknown> => operation && typeof operation === 'object' && !Array.isArray(operation))
|
|
2562
|
-
.map((operation) => ({
|
|
2563
|
-
op: String(operation.op ?? ''),
|
|
2564
|
-
...(typeof operation.assign === 'string' ? { assign: operation.assign } : {}),
|
|
2565
|
-
args: operation.args && typeof operation.args === 'object' && !Array.isArray(operation.args)
|
|
2566
|
-
? operation.args as Record<string, unknown>
|
|
2567
|
-
: {},
|
|
2568
|
-
}));
|
|
2569
|
-
const result = await executeCanvasBatch(normalized);
|
|
2570
|
-
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
2571
|
-
return responseJson(result, result.ok ? 200 : 400);
|
|
2572
|
-
}
|
|
2573
|
-
|
|
2574
|
-
function handleCanvasValidate(): Response {
|
|
2575
|
-
return responseJson(validateCanvasLayout(canvasState.getLayout()));
|
|
2576
|
-
}
|
|
2577
|
-
|
|
2578
|
-
async function handleCanvasThemeUpdate(req: Request): Promise<Response> {
|
|
2579
|
-
const body = await readJson(req);
|
|
2580
|
-
const theme = normalizeCanvasTheme(body.theme, canvasState.theme);
|
|
2581
|
-
const next = canvasState.setTheme(theme);
|
|
2582
|
-
broadcastWorkbenchEvent('theme-changed', {
|
|
2583
|
-
theme: next,
|
|
2584
|
-
sessionId: primaryWorkbenchSessionId,
|
|
2585
|
-
timestamp: new Date().toISOString(),
|
|
2586
|
-
});
|
|
2587
|
-
return responseJson({ ok: true, theme: next });
|
|
2588
|
-
}
|
|
2589
|
-
|
|
2590
|
-
async function handleJsonRenderView(url: URL): Promise<Response> {
|
|
2591
|
-
const nodeId = url.searchParams.get('nodeId') ?? '';
|
|
2592
|
-
if (!nodeId) return responseText('Missing nodeId', 400);
|
|
2593
|
-
const node = canvasState.getNode(nodeId);
|
|
2594
|
-
if (!node || (node.type !== 'json-render' && node.type !== 'graph')) {
|
|
2595
|
-
return responseText('json-render node not found', 404);
|
|
2596
|
-
}
|
|
1543
|
+
async function handleJsonRenderView(url: URL): Promise<Response> {
|
|
1544
|
+
const nodeId = url.searchParams.get('nodeId') ?? '';
|
|
1545
|
+
if (!nodeId) return responseText('Missing nodeId', 400);
|
|
1546
|
+
const node = canvasState.getNode(nodeId);
|
|
1547
|
+
if (!node || (node.type !== 'json-render' && node.type !== 'graph')) {
|
|
1548
|
+
return responseText('json-render node not found', 404);
|
|
1549
|
+
}
|
|
2597
1550
|
|
|
2598
1551
|
const rawSpec = node.data.spec;
|
|
2599
1552
|
if (!rawSpec || typeof rawSpec !== 'object') {
|
|
@@ -2660,13 +1613,6 @@ function responseText(text: string, status = 400): Response {
|
|
|
2660
1613
|
});
|
|
2661
1614
|
}
|
|
2662
1615
|
|
|
2663
|
-
function parsePositiveIntegerParam(value: string | null): number | undefined {
|
|
2664
|
-
if (!value) return undefined;
|
|
2665
|
-
const parsed = Number(value);
|
|
2666
|
-
if (!Number.isFinite(parsed) || parsed <= 0) return undefined;
|
|
2667
|
-
return Math.floor(parsed);
|
|
2668
|
-
}
|
|
2669
|
-
|
|
2670
1616
|
function handleArtifactView(url: URL): Response {
|
|
2671
1617
|
const pathLike = url.searchParams.get('path') ?? '';
|
|
2672
1618
|
const safePath = resolveWorkspaceArtifactPath(pathLike);
|
|
@@ -2855,229 +1801,6 @@ function handleRead(pathLike: string): Response {
|
|
|
2855
1801
|
});
|
|
2856
1802
|
}
|
|
2857
1803
|
|
|
2858
|
-
function randomExtAppToolCallId(): string {
|
|
2859
|
-
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
2860
|
-
}
|
|
2861
|
-
|
|
2862
|
-
function nodeAppSessionId(node: CanvasNodeState | undefined): string | null {
|
|
2863
|
-
if (!node || node.type !== 'mcp-app') return null;
|
|
2864
|
-
const sessionId = node.data.appSessionId;
|
|
2865
|
-
return typeof sessionId === 'string' && sessionId.trim().length > 0 ? sessionId : null;
|
|
2866
|
-
}
|
|
2867
|
-
|
|
2868
|
-
function closeNodeAppSession(node: CanvasNodeState | undefined): void {
|
|
2869
|
-
const sessionId = nodeAppSessionId(node);
|
|
2870
|
-
if (sessionId) closeMcpAppSession(sessionId);
|
|
2871
|
-
}
|
|
2872
|
-
|
|
2873
|
-
function normalizeStringRecord(value: unknown): Record<string, string> | undefined {
|
|
2874
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
|
|
2875
|
-
const entries = Object.entries(value)
|
|
2876
|
-
.filter((entry): entry is [string, string] => typeof entry[1] === 'string')
|
|
2877
|
-
.map(([key, text]) => [key, text.trim()] as const)
|
|
2878
|
-
.filter(([, text]) => text.length > 0);
|
|
2879
|
-
return entries.length > 0 ? Object.fromEntries(entries) : undefined;
|
|
2880
|
-
}
|
|
2881
|
-
|
|
2882
|
-
function parseExternalMcpTransportConfig(body: Record<string, unknown>): ExternalMcpTransportConfig | null {
|
|
2883
|
-
const transport = body.transport;
|
|
2884
|
-
if (!transport || typeof transport !== 'object' || Array.isArray(transport)) return null;
|
|
2885
|
-
const transportRecord = transport as Record<string, unknown>;
|
|
2886
|
-
|
|
2887
|
-
const type = typeof transportRecord.type === 'string' ? transportRecord.type : '';
|
|
2888
|
-
if (type === 'http') {
|
|
2889
|
-
const url = typeof transportRecord.url === 'string' ? transportRecord.url.trim() : '';
|
|
2890
|
-
if (!url) return null;
|
|
2891
|
-
const headers = normalizeStringRecord(transportRecord.headers);
|
|
2892
|
-
return {
|
|
2893
|
-
type: 'http',
|
|
2894
|
-
url,
|
|
2895
|
-
...(headers ? { headers } : {}),
|
|
2896
|
-
};
|
|
2897
|
-
}
|
|
2898
|
-
|
|
2899
|
-
if (type === 'stdio') {
|
|
2900
|
-
const command = typeof transportRecord.command === 'string' ? transportRecord.command.trim() : '';
|
|
2901
|
-
if (!command) return null;
|
|
2902
|
-
const env = normalizeStringRecord(transportRecord.env);
|
|
2903
|
-
return {
|
|
2904
|
-
type: 'stdio',
|
|
2905
|
-
command,
|
|
2906
|
-
...(Array.isArray(transportRecord.args)
|
|
2907
|
-
? { args: transportRecord.args.filter((value: unknown): value is string => typeof value === 'string') }
|
|
2908
|
-
: {}),
|
|
2909
|
-
...(typeof transportRecord.cwd === 'string' && transportRecord.cwd.trim().length > 0 ? { cwd: transportRecord.cwd } : {}),
|
|
2910
|
-
...(env ? { env } : {}),
|
|
2911
|
-
};
|
|
2912
|
-
}
|
|
2913
|
-
|
|
2914
|
-
return null;
|
|
2915
|
-
}
|
|
2916
|
-
|
|
2917
|
-
interface RunAndEmitOpenMcpAppParams {
|
|
2918
|
-
transport: ExternalMcpTransportConfig;
|
|
2919
|
-
toolName: string;
|
|
2920
|
-
toolArguments?: Record<string, unknown>;
|
|
2921
|
-
nodeId?: string;
|
|
2922
|
-
serverName?: string;
|
|
2923
|
-
title?: string;
|
|
2924
|
-
x?: number;
|
|
2925
|
-
y?: number;
|
|
2926
|
-
width?: number;
|
|
2927
|
-
height?: number;
|
|
2928
|
-
timeoutMs?: number;
|
|
2929
|
-
}
|
|
2930
|
-
|
|
2931
|
-
async function runAndEmitOpenMcpApp(params: RunAndEmitOpenMcpAppParams): Promise<Response> {
|
|
2932
|
-
try {
|
|
2933
|
-
const targetNode = params.nodeId ? canvasState.getNode(params.nodeId) : undefined;
|
|
2934
|
-
if (params.nodeId && !targetNode) {
|
|
2935
|
-
return responseJson({ ok: false, error: `Node "${params.nodeId}" not found.` }, 404);
|
|
2936
|
-
}
|
|
2937
|
-
if (targetNode && (targetNode.type !== 'mcp-app' || targetNode.data.mode !== 'ext-app')) {
|
|
2938
|
-
return responseJson({ ok: false, error: `Node "${params.nodeId}" is not an external app node.` }, 400);
|
|
2939
|
-
}
|
|
2940
|
-
|
|
2941
|
-
const opened = await openMcpApp({
|
|
2942
|
-
transport: params.transport,
|
|
2943
|
-
toolName: params.toolName,
|
|
2944
|
-
...(params.toolArguments ? { toolArguments: params.toolArguments } : {}),
|
|
2945
|
-
...(params.serverName ? { serverName: params.serverName } : {}),
|
|
2946
|
-
...(typeof params.timeoutMs === 'number' ? { timeoutMs: params.timeoutMs } : {}),
|
|
2947
|
-
});
|
|
2948
|
-
|
|
2949
|
-
const toolCallId = randomExtAppToolCallId();
|
|
2950
|
-
if (params.nodeId) closeNodeAppSession(targetNode);
|
|
2951
|
-
const nodeIdSeed = params.nodeId ?? (toolCallId.startsWith('ext-app-') ? toolCallId : `ext-app-${toolCallId}`);
|
|
2952
|
-
const toolResult = isExcalidrawCreateView(opened.serverName, opened.toolName)
|
|
2953
|
-
? ensureExcalidrawCheckpointId(opened.toolResult, nodeIdSeed)
|
|
2954
|
-
: opened.toolResult;
|
|
2955
|
-
const nodeTitle = params.title
|
|
2956
|
-
?? (typeof targetNode?.data.title === 'string' ? targetNode.data.title : undefined)
|
|
2957
|
-
?? opened.tool.title
|
|
2958
|
-
?? opened.tool.name;
|
|
2959
|
-
|
|
2960
|
-
emitPrimaryWorkbenchEvent('ext-app-open', {
|
|
2961
|
-
toolCallId,
|
|
2962
|
-
nodeId: nodeIdSeed,
|
|
2963
|
-
title: nodeTitle,
|
|
2964
|
-
html: opened.html,
|
|
2965
|
-
toolInput: opened.toolInput,
|
|
2966
|
-
serverName: opened.serverName,
|
|
2967
|
-
toolName: opened.toolName,
|
|
2968
|
-
appSessionId: opened.sessionId,
|
|
2969
|
-
transportConfig: params.transport,
|
|
2970
|
-
resourceUri: opened.resourceUri,
|
|
2971
|
-
toolDefinition: opened.tool,
|
|
2972
|
-
sessionStatus: 'ready',
|
|
2973
|
-
sessionError: null,
|
|
2974
|
-
...(opened.resourceMeta ? { resourceMeta: opened.resourceMeta } : {}),
|
|
2975
|
-
...(typeof params.x === 'number' ? { x: params.x } : {}),
|
|
2976
|
-
...(typeof params.y === 'number' ? { y: params.y } : {}),
|
|
2977
|
-
...(typeof params.width === 'number' ? { width: params.width } : {}),
|
|
2978
|
-
...(typeof params.height === 'number' ? { height: params.height } : {}),
|
|
2979
|
-
});
|
|
2980
|
-
emitPrimaryWorkbenchEvent('ext-app-result', {
|
|
2981
|
-
toolCallId,
|
|
2982
|
-
nodeId: nodeIdSeed,
|
|
2983
|
-
serverName: opened.serverName,
|
|
2984
|
-
toolName: opened.toolName,
|
|
2985
|
-
success: toolResult.isError !== true,
|
|
2986
|
-
result: toolResult,
|
|
2987
|
-
});
|
|
2988
|
-
const nodeId = params.nodeId ?? findCanvasExtAppNodeId(toolCallId);
|
|
2989
|
-
|
|
2990
|
-
return responseJson({
|
|
2991
|
-
ok: true,
|
|
2992
|
-
...(nodeId ? { id: nodeId } : {}),
|
|
2993
|
-
nodeId,
|
|
2994
|
-
toolCallId,
|
|
2995
|
-
sessionId: opened.sessionId,
|
|
2996
|
-
resourceUri: opened.resourceUri,
|
|
2997
|
-
serverName: opened.serverName,
|
|
2998
|
-
toolName: opened.toolName,
|
|
2999
|
-
});
|
|
3000
|
-
} catch (error) {
|
|
3001
|
-
return responseJson({
|
|
3002
|
-
ok: false,
|
|
3003
|
-
error: error instanceof Error ? error.message : String(error),
|
|
3004
|
-
}, 400);
|
|
3005
|
-
}
|
|
3006
|
-
}
|
|
3007
|
-
|
|
3008
|
-
async function handleCanvasOpenMcpApp(req: Request): Promise<Response> {
|
|
3009
|
-
const body = await readJson(req);
|
|
3010
|
-
const transport = parseExternalMcpTransportConfig(body);
|
|
3011
|
-
const toolName = typeof body.toolName === 'string' ? body.toolName.trim() : '';
|
|
3012
|
-
if (!transport || !toolName) {
|
|
3013
|
-
return responseJson({ ok: false, error: 'Missing valid transport or toolName.' }, 400);
|
|
3014
|
-
}
|
|
3015
|
-
|
|
3016
|
-
const toolArguments =
|
|
3017
|
-
body.toolArguments && typeof body.toolArguments === 'object' && !Array.isArray(body.toolArguments)
|
|
3018
|
-
? body.toolArguments as Record<string, unknown>
|
|
3019
|
-
: undefined;
|
|
3020
|
-
|
|
3021
|
-
const requestedTitle = typeof body.title === 'string' && body.title.trim().length > 0
|
|
3022
|
-
? body.title.trim()
|
|
3023
|
-
: undefined;
|
|
3024
|
-
const requestedServerName = typeof body.serverName === 'string' && body.serverName.trim().length > 0
|
|
3025
|
-
? body.serverName.trim()
|
|
3026
|
-
: undefined;
|
|
3027
|
-
const requestedNodeId = typeof body.nodeId === 'string' && body.nodeId.trim().length > 0
|
|
3028
|
-
? body.nodeId.trim()
|
|
3029
|
-
: undefined;
|
|
3030
|
-
|
|
3031
|
-
return runAndEmitOpenMcpApp({
|
|
3032
|
-
transport,
|
|
3033
|
-
toolName,
|
|
3034
|
-
...(toolArguments ? { toolArguments } : {}),
|
|
3035
|
-
...(requestedNodeId ? { nodeId: requestedNodeId } : {}),
|
|
3036
|
-
...(requestedServerName ? { serverName: requestedServerName } : {}),
|
|
3037
|
-
...(requestedTitle ? { title: requestedTitle } : {}),
|
|
3038
|
-
...(typeof body.x === 'number' ? { x: body.x } : {}),
|
|
3039
|
-
...(typeof body.y === 'number' ? { y: body.y } : {}),
|
|
3040
|
-
...(typeof body.width === 'number' ? { width: body.width } : {}),
|
|
3041
|
-
...(typeof body.height === 'number' ? { height: body.height } : {}),
|
|
3042
|
-
...(typeof body.timeoutMs === 'number' ? { timeoutMs: body.timeoutMs } : {}),
|
|
3043
|
-
});
|
|
3044
|
-
}
|
|
3045
|
-
|
|
3046
|
-
async function handleCanvasAddDiagram(req: Request): Promise<Response> {
|
|
3047
|
-
const body = await readJson(req);
|
|
3048
|
-
let built;
|
|
3049
|
-
try {
|
|
3050
|
-
built = buildExcalidrawOpenMcpAppInput({
|
|
3051
|
-
elements: body.elements,
|
|
3052
|
-
...(typeof body.nodeId === 'string' ? { nodeId: body.nodeId } : {}),
|
|
3053
|
-
...(typeof body.title === 'string' ? { title: body.title } : {}),
|
|
3054
|
-
...(typeof body.x === 'number' ? { x: body.x } : {}),
|
|
3055
|
-
...(typeof body.y === 'number' ? { y: body.y } : {}),
|
|
3056
|
-
...(typeof body.width === 'number' ? { width: body.width } : {}),
|
|
3057
|
-
...(typeof body.height === 'number' ? { height: body.height } : {}),
|
|
3058
|
-
...(typeof body.timeoutMs === 'number' ? { timeoutMs: body.timeoutMs } : {}),
|
|
3059
|
-
});
|
|
3060
|
-
} catch (error) {
|
|
3061
|
-
return responseJson({
|
|
3062
|
-
ok: false,
|
|
3063
|
-
error: error instanceof Error ? error.message : String(error),
|
|
3064
|
-
}, 400);
|
|
3065
|
-
}
|
|
3066
|
-
return runAndEmitOpenMcpApp({
|
|
3067
|
-
transport: built.transport,
|
|
3068
|
-
toolName: built.toolName,
|
|
3069
|
-
toolArguments: built.toolArguments,
|
|
3070
|
-
serverName: built.serverName,
|
|
3071
|
-
...(built.nodeId ? { nodeId: built.nodeId } : {}),
|
|
3072
|
-
...(built.title ? { title: built.title } : {}),
|
|
3073
|
-
...(typeof built.x === 'number' ? { x: built.x } : {}),
|
|
3074
|
-
...(typeof built.y === 'number' ? { y: built.y } : {}),
|
|
3075
|
-
...(typeof built.width === 'number' ? { width: built.width } : {}),
|
|
3076
|
-
...(typeof built.height === 'number' ? { height: built.height } : {}),
|
|
3077
|
-
...(typeof built.timeoutMs === 'number' ? { timeoutMs: built.timeoutMs } : {}),
|
|
3078
|
-
});
|
|
3079
|
-
}
|
|
3080
|
-
|
|
3081
1804
|
async function handleExtAppCallTool(req: Request): Promise<Response> {
|
|
3082
1805
|
const body = await readJson(req);
|
|
3083
1806
|
const sessionId = typeof body.sessionId === 'string' ? body.sessionId.trim() : '';
|
|
@@ -3279,120 +2002,15 @@ function handleWorkbenchState(): Response {
|
|
|
3279
2002
|
});
|
|
3280
2003
|
}
|
|
3281
2004
|
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
backendValue === 'chrome' || backendValue === 'webkit'
|
|
3288
|
-
? backendValue
|
|
3289
|
-
: undefined;
|
|
3290
|
-
|
|
3291
|
-
const width = typeof body.width === 'number' ? body.width : undefined;
|
|
3292
|
-
const height = typeof body.height === 'number' ? body.height : undefined;
|
|
3293
|
-
const chromePath = typeof body.chromePath === 'string' ? body.chromePath : undefined;
|
|
3294
|
-
const dataStoreDir = typeof body.dataStoreDir === 'string' ? body.dataStoreDir : undefined;
|
|
3295
|
-
const chromeArgv = Array.isArray(body.chromeArgv)
|
|
3296
|
-
? body.chromeArgv.filter((value): value is string => typeof value === 'string')
|
|
3297
|
-
: undefined;
|
|
3298
|
-
|
|
3299
|
-
return {
|
|
3300
|
-
...(backend ? { backend } : {}),
|
|
3301
|
-
...(width !== undefined ? { width } : {}),
|
|
3302
|
-
...(height !== undefined ? { height } : {}),
|
|
3303
|
-
...(chromePath ? { chromePath } : {}),
|
|
3304
|
-
...(chromeArgv ? { chromeArgv } : {}),
|
|
3305
|
-
...(dataStoreDir ? { dataStoreDir } : {}),
|
|
3306
|
-
};
|
|
3307
|
-
}
|
|
3308
|
-
|
|
2005
|
+
// Webview status / start / stop / evaluate / resize HTTP routes migrated to the
|
|
2006
|
+
// operation registry (plan-008 Wave 3): src/server/operations/ops/webview.ts,
|
|
2007
|
+
// dispatched in the fetch handler. The screenshot route + handler below stay
|
|
2008
|
+
// hand-written (binary response). `currentWorkbenchUrl` is still used by the
|
|
2009
|
+
// injected webview runner's start closure (see setWebviewRunner above).
|
|
3309
2010
|
function currentWorkbenchUrl(): string | null {
|
|
3310
2011
|
return server && typeof server.port === 'number' ? `${loopbackBaseUrl(server.port)}/workbench` : null;
|
|
3311
2012
|
}
|
|
3312
2013
|
|
|
3313
|
-
function handleWorkbenchWebViewStatus(): Response {
|
|
3314
|
-
return responseJson(getCanvasAutomationWebViewStatus());
|
|
3315
|
-
}
|
|
3316
|
-
|
|
3317
|
-
async function handleWorkbenchWebViewStart(req: Request): Promise<Response> {
|
|
3318
|
-
const url = currentWorkbenchUrl();
|
|
3319
|
-
if (!url) {
|
|
3320
|
-
return responseJson({ ok: false, error: 'Canvas server is not running.' }, 503);
|
|
3321
|
-
}
|
|
3322
|
-
|
|
3323
|
-
const body = await readJson(req);
|
|
3324
|
-
const options = parseCanvasAutomationWebViewRequestBody(body);
|
|
3325
|
-
|
|
3326
|
-
try {
|
|
3327
|
-
const webview = await startCanvasAutomationWebView(url, options);
|
|
3328
|
-
return responseJson({ ok: true, webview });
|
|
3329
|
-
} catch (error) {
|
|
3330
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
3331
|
-
const status = hasCanvasAutomationWebViewSupport() ? 500 : 501;
|
|
3332
|
-
return responseJson({
|
|
3333
|
-
ok: false,
|
|
3334
|
-
error: message,
|
|
3335
|
-
webview: getCanvasAutomationWebViewStatus(),
|
|
3336
|
-
}, status);
|
|
3337
|
-
}
|
|
3338
|
-
}
|
|
3339
|
-
|
|
3340
|
-
async function handleWorkbenchWebViewStop(): Promise<Response> {
|
|
3341
|
-
try {
|
|
3342
|
-
const stopped = await stopCanvasAutomationWebView();
|
|
3343
|
-
return responseJson({
|
|
3344
|
-
ok: true,
|
|
3345
|
-
stopped,
|
|
3346
|
-
webview: getCanvasAutomationWebViewStatus(),
|
|
3347
|
-
});
|
|
3348
|
-
} catch (error) {
|
|
3349
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
3350
|
-
return responseJson({
|
|
3351
|
-
ok: false,
|
|
3352
|
-
error: message,
|
|
3353
|
-
webview: getCanvasAutomationWebViewStatus(),
|
|
3354
|
-
}, 500);
|
|
3355
|
-
}
|
|
3356
|
-
}
|
|
3357
|
-
|
|
3358
|
-
async function handleWorkbenchWebViewEvaluate(req: Request): Promise<Response> {
|
|
3359
|
-
const body = await readJson(req);
|
|
3360
|
-
const expression = typeof body.expression === 'string' ? body.expression.trim() : '';
|
|
3361
|
-
const script = typeof body.script === 'string' ? body.script.trim() : '';
|
|
3362
|
-
if ((expression ? 1 : 0) + (script ? 1 : 0) !== 1) {
|
|
3363
|
-
return responseJson({
|
|
3364
|
-
ok: false,
|
|
3365
|
-
error: 'Pass exactly one of "expression" (single JS expression) or "script" (multi-statement body, wrapped in an async IIFE).',
|
|
3366
|
-
}, 400);
|
|
3367
|
-
}
|
|
3368
|
-
const source = script ? wrapCanvasAutomationScript(script) : expression;
|
|
3369
|
-
|
|
3370
|
-
try {
|
|
3371
|
-
const value = await evaluateCanvasAutomationWebView(source);
|
|
3372
|
-
return responseJson({ ok: true, value });
|
|
3373
|
-
} catch (error) {
|
|
3374
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
3375
|
-
return responseJson({ ok: false, error: message, webview: getCanvasAutomationWebViewStatus() }, 400);
|
|
3376
|
-
}
|
|
3377
|
-
}
|
|
3378
|
-
|
|
3379
|
-
async function handleWorkbenchWebViewResize(req: Request): Promise<Response> {
|
|
3380
|
-
const body = await readJson(req);
|
|
3381
|
-
const width = typeof body.width === 'number' ? body.width : NaN;
|
|
3382
|
-
const height = typeof body.height === 'number' ? body.height : NaN;
|
|
3383
|
-
if (!Number.isFinite(width) || width <= 0 || !Number.isFinite(height) || height <= 0) {
|
|
3384
|
-
return responseJson({ ok: false, error: 'Missing required positive numeric fields: width, height.' }, 400);
|
|
3385
|
-
}
|
|
3386
|
-
|
|
3387
|
-
try {
|
|
3388
|
-
const webview = await resizeCanvasAutomationWebView(width, height);
|
|
3389
|
-
return responseJson({ ok: true, webview });
|
|
3390
|
-
} catch (error) {
|
|
3391
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
3392
|
-
return responseJson({ ok: false, error: message, webview: getCanvasAutomationWebViewStatus() }, 400);
|
|
3393
|
-
}
|
|
3394
|
-
}
|
|
3395
|
-
|
|
3396
2014
|
async function handleWorkbenchWebViewScreenshot(req: Request): Promise<Response> {
|
|
3397
2015
|
const body = await readJson(req);
|
|
3398
2016
|
const format = body.format === 'jpeg' || body.format === 'webp' || body.format === 'png'
|
|
@@ -3866,43 +2484,6 @@ async function handleCanvasPrompt(req: Request): Promise<Response> {
|
|
|
3866
2484
|
return responseJson({ ok: true, nodeId });
|
|
3867
2485
|
}
|
|
3868
2486
|
|
|
3869
|
-
async function handleSnapshotSave(req: Request): Promise<Response> {
|
|
3870
|
-
const body = await readJson(req);
|
|
3871
|
-
const name = typeof body.name === 'string' ? body.name.trim() : '';
|
|
3872
|
-
if (!name) return responseText('Missing snapshot name', 400);
|
|
3873
|
-
const snapshot = saveCanvasSnapshot(name);
|
|
3874
|
-
if (!snapshot) return responseText('Failed to save snapshot', 500);
|
|
3875
|
-
return responseJson({ ok: true, id: snapshot.id, snapshot });
|
|
3876
|
-
}
|
|
3877
|
-
|
|
3878
|
-
async function handleSnapshotGc(req: Request): Promise<Response> {
|
|
3879
|
-
const body = await readJson(req);
|
|
3880
|
-
const keepValue = body.keep;
|
|
3881
|
-
const keep = typeof keepValue === 'number'
|
|
3882
|
-
? keepValue
|
|
3883
|
-
: typeof keepValue === 'string'
|
|
3884
|
-
? Number(keepValue)
|
|
3885
|
-
: undefined;
|
|
3886
|
-
const dryRun = body.dryRun === true || body['dry-run'] === true;
|
|
3887
|
-
return responseJson(gcCanvasSnapshots({ keep, dryRun }));
|
|
3888
|
-
}
|
|
3889
|
-
|
|
3890
|
-
async function handleContextPinsUpdate(req: Request): Promise<Response> {
|
|
3891
|
-
const body = await readJson(req);
|
|
3892
|
-
const MAX_PINS = 20;
|
|
3893
|
-
const nodeIds = Array.isArray(body.nodeIds)
|
|
3894
|
-
? (body.nodeIds.filter((id: unknown) => typeof id === 'string') as string[]).slice(0, MAX_PINS)
|
|
3895
|
-
: [];
|
|
3896
|
-
const result = setCanvasContextPins(nodeIds, 'set');
|
|
3897
|
-
broadcastWorkbenchEvent('context-pins-changed', {
|
|
3898
|
-
count: result.count,
|
|
3899
|
-
nodeIds: result.nodeIds,
|
|
3900
|
-
sessionId: primaryWorkbenchSessionId,
|
|
3901
|
-
timestamp: new Date().toISOString(),
|
|
3902
|
-
});
|
|
3903
|
-
return responseJson({ ok: true, count: result.count });
|
|
3904
|
-
}
|
|
3905
|
-
|
|
3906
2487
|
function handleGetPinnedContext(): Response {
|
|
3907
2488
|
const pinnedIds = Array.from(canvasState.contextPinnedNodeIds);
|
|
3908
2489
|
const preamble = pinnedIds.length > 0 ? buildSelectionContextPreamble(pinnedIds) : '';
|
|
@@ -3936,10 +2517,6 @@ function normalizeAxSource(value: unknown, fallback: PmxAxSource): PmxAxSource {
|
|
|
3936
2517
|
: fallback;
|
|
3937
2518
|
}
|
|
3938
2519
|
|
|
3939
|
-
function handleGetAxState(): Response {
|
|
3940
|
-
return responseJson({ ok: true, state: canvasState.getAxState() });
|
|
3941
|
-
}
|
|
3942
|
-
|
|
3943
2520
|
function handleGetAxContext(url: URL): Response {
|
|
3944
2521
|
// Optional ?consumer= filters the compact `delivery` lead block (loop-safe — a
|
|
3945
2522
|
// consumer never sees steering/activity it originated), so a host adapter can
|
|
@@ -3948,12 +2525,6 @@ function handleGetAxContext(url: URL): Response {
|
|
|
3948
2525
|
return responseJson(buildCanvasAxContext(consumer));
|
|
3949
2526
|
}
|
|
3950
2527
|
|
|
3951
|
-
// Clamp ?waitMs= to [0, AX_WAIT_MAX_MS]. 0 (or absent/NaN) = a plain single read.
|
|
3952
|
-
function parseAxWaitMs(url: URL): number {
|
|
3953
|
-
const raw = Number(url.searchParams.get('waitMs') ?? '');
|
|
3954
|
-
return Number.isFinite(raw) && raw > 0 ? Math.min(raw, AX_WAIT_MAX_MS) : 0;
|
|
3955
|
-
}
|
|
3956
|
-
|
|
3957
2528
|
function isReviewSeverity(v: unknown): v is PmxAxReviewSeverity {
|
|
3958
2529
|
return v === 'info' || v === 'warning' || v === 'error';
|
|
3959
2530
|
}
|
|
@@ -4030,43 +2601,10 @@ async function handleAxActivityIngest(req: Request): Promise<Response> {
|
|
|
4030
2601
|
return responseJson({ ok: true, ...result });
|
|
4031
2602
|
}
|
|
4032
2603
|
|
|
4033
|
-
// Report primitive D
|
|
4034
|
-
//
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
const { value, pending } = await waitForAxResolution({
|
|
4038
|
-
read: () => canvasState.getApproval(id),
|
|
4039
|
-
isResolved: (g) => g.status !== 'pending',
|
|
4040
|
-
timeoutMs: waitMs,
|
|
4041
|
-
signal: req.signal,
|
|
4042
|
-
});
|
|
4043
|
-
if (!value) return responseJson({ ok: false, error: 'approval gate not found.' }, 404);
|
|
4044
|
-
return responseJson({ ok: true, approvalGate: value, pending });
|
|
4045
|
-
}
|
|
4046
|
-
|
|
4047
|
-
async function handleAxElicitationGet(url: URL, id: string, req: Request): Promise<Response> {
|
|
4048
|
-
const waitMs = parseAxWaitMs(url);
|
|
4049
|
-
const { value, pending } = await waitForAxResolution({
|
|
4050
|
-
read: () => canvasState.getElicitation(id),
|
|
4051
|
-
isResolved: (e) => e.status !== 'pending',
|
|
4052
|
-
timeoutMs: waitMs,
|
|
4053
|
-
signal: req.signal,
|
|
4054
|
-
});
|
|
4055
|
-
if (!value) return responseJson({ ok: false, error: 'elicitation not found.' }, 404);
|
|
4056
|
-
return responseJson({ ok: true, elicitation: value, pending });
|
|
4057
|
-
}
|
|
4058
|
-
|
|
4059
|
-
async function handleAxModeGet(url: URL, id: string, req: Request): Promise<Response> {
|
|
4060
|
-
const waitMs = parseAxWaitMs(url);
|
|
4061
|
-
const { value, pending } = await waitForAxResolution({
|
|
4062
|
-
read: () => canvasState.getModeRequest(id),
|
|
4063
|
-
isResolved: (m) => m.status !== 'pending',
|
|
4064
|
-
timeoutMs: waitMs,
|
|
4065
|
-
signal: req.signal,
|
|
4066
|
-
});
|
|
4067
|
-
if (!value) return responseJson({ ok: false, error: 'mode request not found.' }, 404);
|
|
4068
|
-
return responseJson({ ok: true, modeRequest: value, pending });
|
|
4069
|
-
}
|
|
2604
|
+
// Report primitive D single-item gate reads (GET /api/canvas/ax/{approval,
|
|
2605
|
+
// elicitation,mode}/:id) with the optional ?waitMs= long-poll migrated to the
|
|
2606
|
+
// operation registry (plan-007 Slice B wave 4):
|
|
2607
|
+
// src/server/operations/ops/ax-await.ts.
|
|
4070
2608
|
|
|
4071
2609
|
// Compact AX state for surfaces (the same shape seeded into AX-enabled iframes).
|
|
4072
2610
|
// The client fetches this and pushes it to surfaces over the ax-update channel.
|
|
@@ -4113,136 +2651,34 @@ async function handleAxInteraction(req: Request): Promise<Response> {
|
|
|
4113
2651
|
return responseJson(result, result.ok ? 200 : result.status);
|
|
4114
2652
|
}
|
|
4115
2653
|
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
const limitRaw = Number(url.searchParams.get('limit') ?? '');
|
|
4119
|
-
const limit = Number.isFinite(limitRaw) && limitRaw > 0 ? limitRaw : undefined;
|
|
4120
|
-
const pending = canvasState.getPendingSteering({
|
|
4121
|
-
...(consumer ? { consumer } : {}),
|
|
4122
|
-
...(limit ? { limit } : {}),
|
|
4123
|
-
});
|
|
4124
|
-
return responseJson({ ok: true, pending });
|
|
4125
|
-
}
|
|
4126
|
-
|
|
4127
|
-
function handleAxDeliveryMark(id: string): Response {
|
|
4128
|
-
const delivered = canvasState.markSteeringDelivered(id);
|
|
4129
|
-
if (delivered) {
|
|
4130
|
-
broadcastWorkbenchEvent('ax-event-created', {
|
|
4131
|
-
steeringDelivered: id,
|
|
4132
|
-
sessionId: primaryWorkbenchSessionId,
|
|
4133
|
-
timestamp: new Date().toISOString(),
|
|
4134
|
-
});
|
|
4135
|
-
}
|
|
4136
|
-
return responseJson({ ok: true, delivered });
|
|
4137
|
-
}
|
|
2654
|
+
// handleAxDeliveryPending / handleAxDeliveryMark migrated to the operation
|
|
2655
|
+
// registry (plan-007 Slice B wave 3): src/server/operations/ops/ax-timeline.ts.
|
|
4138
2656
|
|
|
4139
2657
|
function handleAxElicitationList(): Response {
|
|
4140
2658
|
return responseJson({ ok: true, elicitations: canvasState.getElicitations() });
|
|
4141
2659
|
}
|
|
4142
2660
|
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
if (typeof body.prompt !== 'string' || !body.prompt.trim()) {
|
|
4146
|
-
return responseJson({ ok: false, error: 'elicitation requires a prompt.' }, 400);
|
|
4147
|
-
}
|
|
4148
|
-
const elicitation = canvasState.requestElicitation(
|
|
4149
|
-
{
|
|
4150
|
-
prompt: body.prompt,
|
|
4151
|
-
...(Array.isArray(body.fields) ? { fields: body.fields.filter((f: unknown): f is string => typeof f === 'string') } : {}),
|
|
4152
|
-
...(Array.isArray(body.nodeIds) ? { nodeIds: normalizeAxNodeIds(body.nodeIds) } : {}),
|
|
4153
|
-
},
|
|
4154
|
-
{ source: normalizeAxSource(body.source, 'api') },
|
|
4155
|
-
);
|
|
4156
|
-
broadcastWorkbenchEvent('ax-state-changed', { elicitation, sessionId: primaryWorkbenchSessionId, timestamp: new Date().toISOString() });
|
|
4157
|
-
return responseJson({ ok: true, elicitation });
|
|
4158
|
-
}
|
|
4159
|
-
|
|
4160
|
-
async function handleAxElicitationRespond(req: Request, id: string): Promise<Response> {
|
|
4161
|
-
const body = await readJson(req);
|
|
4162
|
-
const response = isRecord(body.response) ? body.response : {};
|
|
4163
|
-
const elicitation = canvasState.respondElicitation(id, response, { source: normalizeAxSource(body.source, 'api') });
|
|
4164
|
-
if (!elicitation) return responseJson({ ok: false, error: 'elicitation not found or already answered.' }, 404);
|
|
4165
|
-
broadcastWorkbenchEvent('ax-state-changed', { elicitation, sessionId: primaryWorkbenchSessionId, timestamp: new Date().toISOString() });
|
|
4166
|
-
return responseJson({ ok: true, elicitation });
|
|
4167
|
-
}
|
|
2661
|
+
// handleAxElicitationRequest / handleAxElicitationRespond migrated to the
|
|
2662
|
+
// operation registry (plan-007 Slice B wave 2): src/server/operations/ops/ax-work.ts.
|
|
4168
2663
|
|
|
4169
2664
|
function handleAxModeList(): Response {
|
|
4170
2665
|
return responseJson({ ok: true, modeRequests: canvasState.getModeRequests() });
|
|
4171
2666
|
}
|
|
4172
2667
|
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
if (body.mode !== 'plan' && body.mode !== 'execute' && body.mode !== 'autonomous') {
|
|
4176
|
-
return responseJson({ ok: false, error: 'mode request requires mode plan|execute|autonomous.' }, 400);
|
|
4177
|
-
}
|
|
4178
|
-
const modeRequest = canvasState.requestMode(
|
|
4179
|
-
{
|
|
4180
|
-
mode: body.mode,
|
|
4181
|
-
...(typeof body.reason === 'string' ? { reason: body.reason } : {}),
|
|
4182
|
-
...(Array.isArray(body.nodeIds) ? { nodeIds: normalizeAxNodeIds(body.nodeIds) } : {}),
|
|
4183
|
-
},
|
|
4184
|
-
{ source: normalizeAxSource(body.source, 'api') },
|
|
4185
|
-
);
|
|
4186
|
-
broadcastWorkbenchEvent('ax-state-changed', { modeRequest, sessionId: primaryWorkbenchSessionId, timestamp: new Date().toISOString() });
|
|
4187
|
-
return responseJson({ ok: true, modeRequest });
|
|
4188
|
-
}
|
|
4189
|
-
|
|
4190
|
-
async function handleAxModeResolve(req: Request, id: string): Promise<Response> {
|
|
4191
|
-
const body = await readJson(req);
|
|
4192
|
-
if (body.decision !== 'approved' && body.decision !== 'rejected') {
|
|
4193
|
-
return responseJson({ ok: false, error: 'resolve requires decision approved or rejected.' }, 400);
|
|
4194
|
-
}
|
|
4195
|
-
const modeRequest = canvasState.resolveModeRequest(id, body.decision, {
|
|
4196
|
-
...(typeof body.resolution === 'string' ? { resolution: body.resolution } : {}),
|
|
4197
|
-
source: normalizeAxSource(body.source, 'api'),
|
|
4198
|
-
});
|
|
4199
|
-
if (!modeRequest) return responseJson({ ok: false, error: 'mode request not found or already resolved.' }, 404);
|
|
4200
|
-
broadcastWorkbenchEvent('ax-state-changed', { modeRequest, sessionId: primaryWorkbenchSessionId, timestamp: new Date().toISOString() });
|
|
4201
|
-
return responseJson({ ok: true, modeRequest });
|
|
4202
|
-
}
|
|
2668
|
+
// handleAxModeRequest / handleAxModeResolve migrated to the operation registry
|
|
2669
|
+
// (plan-007 Slice B wave 2): src/server/operations/ops/ax-work.ts.
|
|
4203
2670
|
|
|
4204
2671
|
function handleAxCommandList(): Response {
|
|
4205
2672
|
return responseJson({ ok: true, commands: canvasState.getCommandRegistry() });
|
|
4206
2673
|
}
|
|
4207
2674
|
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
if (typeof body.name !== 'string') {
|
|
4211
|
-
return responseJson({ ok: false, error: 'command requires a name.' }, 400);
|
|
4212
|
-
}
|
|
4213
|
-
const event = canvasState.invokeCommand(body.name, isRecord(body.args) ? body.args : null, { source: normalizeAxSource(body.source, 'api') });
|
|
4214
|
-
if (!event) return responseJson({ ok: false, error: `Unknown command "${body.name}".` }, 400);
|
|
4215
|
-
broadcastWorkbenchEvent('ax-event-created', { event, sessionId: primaryWorkbenchSessionId, timestamp: new Date().toISOString() });
|
|
4216
|
-
return responseJson({ ok: true, event });
|
|
4217
|
-
}
|
|
2675
|
+
// handleAxCommandInvoke migrated to the operation registry (plan-007 Slice B
|
|
2676
|
+
// wave 3): src/server/operations/ops/ax-timeline.ts.
|
|
4218
2677
|
|
|
4219
2678
|
function handleAxPolicyGet(): Response {
|
|
4220
2679
|
return responseJson({ ok: true, policy: canvasState.getPolicy() });
|
|
4221
2680
|
}
|
|
4222
2681
|
|
|
4223
|
-
async function handleAxPolicySet(req: Request): Promise<Response> {
|
|
4224
|
-
const body = await readJson(req);
|
|
4225
|
-
const patch: { tools?: Partial<PmxAxPolicy['tools']>; prompt?: Partial<PmxAxPolicy['prompt']> } = {};
|
|
4226
|
-
if (isRecord(body.tools)) patch.tools = body.tools as Partial<PmxAxPolicy['tools']>;
|
|
4227
|
-
if (isRecord(body.prompt)) patch.prompt = body.prompt as Partial<PmxAxPolicy['prompt']>;
|
|
4228
|
-
const policy = canvasState.setPolicy(patch, { source: normalizeAxSource(body.source, 'api') });
|
|
4229
|
-
broadcastWorkbenchEvent('ax-state-changed', { policy, sessionId: primaryWorkbenchSessionId, timestamp: new Date().toISOString() });
|
|
4230
|
-
return responseJson({ ok: true, policy });
|
|
4231
|
-
}
|
|
4232
|
-
|
|
4233
|
-
async function handleAxFocusUpdate(req: Request): Promise<Response> {
|
|
4234
|
-
const body = await readJson(req);
|
|
4235
|
-
const nodeIds = normalizeAxNodeIds(body.nodeIds);
|
|
4236
|
-
const source = normalizeAxSource(body.source, 'api');
|
|
4237
|
-
const focus = canvasState.setAxFocus(nodeIds, { source });
|
|
4238
|
-
broadcastWorkbenchEvent('ax-state-changed', {
|
|
4239
|
-
focus,
|
|
4240
|
-
sessionId: primaryWorkbenchSessionId,
|
|
4241
|
-
timestamp: new Date().toISOString(),
|
|
4242
|
-
});
|
|
4243
|
-
return responseJson({ ok: true, focus });
|
|
4244
|
-
}
|
|
4245
|
-
|
|
4246
2682
|
async function handleAxStatePatch(req: Request): Promise<Response> {
|
|
4247
2683
|
const body = await readJson(req);
|
|
4248
2684
|
if (!body.focus || typeof body.focus !== 'object' || Array.isArray(body.focus)) {
|
|
@@ -4260,52 +2696,8 @@ async function handleAxStatePatch(req: Request): Promise<Response> {
|
|
|
4260
2696
|
return responseJson({ ok: true, state: canvasState.getAxState() });
|
|
4261
2697
|
}
|
|
4262
2698
|
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
if (!isAxEventKind(body.kind) || typeof body.summary !== 'string') {
|
|
4266
|
-
return responseJson({ ok: false, error: 'event requires kind and summary.' }, 400);
|
|
4267
|
-
}
|
|
4268
|
-
const event = canvasState.recordAxEvent(
|
|
4269
|
-
{
|
|
4270
|
-
kind: body.kind,
|
|
4271
|
-
summary: body.summary,
|
|
4272
|
-
detail: typeof body.detail === 'string' ? body.detail : null,
|
|
4273
|
-
nodeIds: normalizeAxNodeIds(body.nodeIds),
|
|
4274
|
-
data: isRecord(body.data) ? body.data : null,
|
|
4275
|
-
},
|
|
4276
|
-
{ source: normalizeAxSource(body.source, 'api') },
|
|
4277
|
-
);
|
|
4278
|
-
broadcastWorkbenchEvent('ax-event-created', {
|
|
4279
|
-
event,
|
|
4280
|
-
sessionId: primaryWorkbenchSessionId,
|
|
4281
|
-
timestamp: new Date().toISOString(),
|
|
4282
|
-
});
|
|
4283
|
-
return responseJson({ ok: true, event });
|
|
4284
|
-
}
|
|
4285
|
-
|
|
4286
|
-
async function handleAxSteer(req: Request): Promise<Response> {
|
|
4287
|
-
const body = await readJson(req);
|
|
4288
|
-
if (typeof body.message !== 'string' || !body.message.trim()) {
|
|
4289
|
-
return responseJson({ ok: false, error: 'steer requires a non-empty message.' }, 400);
|
|
4290
|
-
}
|
|
4291
|
-
const steering = canvasState.recordSteeringMessage(body.message, {
|
|
4292
|
-
source: normalizeAxSource(body.source, 'api'),
|
|
4293
|
-
});
|
|
4294
|
-
broadcastWorkbenchEvent('ax-event-created', {
|
|
4295
|
-
steering,
|
|
4296
|
-
sessionId: primaryWorkbenchSessionId,
|
|
4297
|
-
timestamp: new Date().toISOString(),
|
|
4298
|
-
});
|
|
4299
|
-
return responseJson({ ok: true, steering });
|
|
4300
|
-
}
|
|
4301
|
-
|
|
4302
|
-
function handleAxTimelineGet(url: URL): Response {
|
|
4303
|
-
const limit = Number(url.searchParams.get('limit') ?? '');
|
|
4304
|
-
return responseJson({
|
|
4305
|
-
ok: true,
|
|
4306
|
-
...canvasState.getAxTimeline(Number.isFinite(limit) && limit > 0 ? { limit } : {}),
|
|
4307
|
-
});
|
|
4308
|
-
}
|
|
2699
|
+
// handleAxEventAdd / handleAxSteer / handleAxTimelineGet migrated to the
|
|
2700
|
+
// operation registry (plan-007 Slice B wave 3): src/server/operations/ops/ax-timeline.ts.
|
|
4309
2701
|
|
|
4310
2702
|
const AX_WORK_STATUSES = new Set(['todo', 'in-progress', 'blocked', 'done', 'cancelled']);
|
|
4311
2703
|
|
|
@@ -4319,234 +2711,33 @@ function handleAxWorkList(): Response {
|
|
|
4319
2711
|
return responseJson({ ok: true, workItems: canvasState.getWorkItems() });
|
|
4320
2712
|
}
|
|
4321
2713
|
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
if (typeof body.title !== 'string' || !body.title.trim()) {
|
|
4325
|
-
return responseJson({ ok: false, error: 'work item requires a title.' }, 400);
|
|
4326
|
-
}
|
|
4327
|
-
// Report #56: reject an unknown status (e.g. "in_progress") instead of silently
|
|
4328
|
-
// dropping it — the accepted tokens use hyphens.
|
|
4329
|
-
if (body.status !== undefined && !normalizeAxWorkItemStatus(body.status)) {
|
|
4330
|
-
return responseJson({ ok: false, error: `invalid work item status "${String(body.status)}"; expected one of: todo, in-progress, blocked, done, cancelled.` }, 400);
|
|
4331
|
-
}
|
|
4332
|
-
const status = normalizeAxWorkItemStatus(body.status);
|
|
4333
|
-
const workItem = canvasState.addWorkItem(
|
|
4334
|
-
{
|
|
4335
|
-
title: body.title,
|
|
4336
|
-
...(status ? { status } : {}),
|
|
4337
|
-
...(typeof body.detail === 'string' ? { detail: body.detail } : {}),
|
|
4338
|
-
...(Array.isArray(body.nodeIds) ? { nodeIds: normalizeAxNodeIds(body.nodeIds) } : {}),
|
|
4339
|
-
},
|
|
4340
|
-
{ source: normalizeAxSource(body.source, 'api') },
|
|
4341
|
-
);
|
|
4342
|
-
broadcastWorkbenchEvent('ax-state-changed', {
|
|
4343
|
-
workItem,
|
|
4344
|
-
sessionId: primaryWorkbenchSessionId,
|
|
4345
|
-
timestamp: new Date().toISOString(),
|
|
4346
|
-
});
|
|
4347
|
-
return responseJson({ ok: true, workItem });
|
|
4348
|
-
}
|
|
4349
|
-
|
|
4350
|
-
async function handleAxWorkUpdate(req: Request, id: string): Promise<Response> {
|
|
4351
|
-
const body = await readJson(req);
|
|
4352
|
-
// Report #56: reject an unknown status instead of returning ok:true + no-op.
|
|
4353
|
-
if (body.status !== undefined && !normalizeAxWorkItemStatus(body.status)) {
|
|
4354
|
-
return responseJson({ ok: false, error: `invalid work item status "${String(body.status)}"; expected one of: todo, in-progress, blocked, done, cancelled.` }, 400);
|
|
4355
|
-
}
|
|
4356
|
-
const status = normalizeAxWorkItemStatus(body.status);
|
|
4357
|
-
const workItem = canvasState.updateWorkItem(
|
|
4358
|
-
id,
|
|
4359
|
-
{
|
|
4360
|
-
...(typeof body.title === 'string' ? { title: body.title } : {}),
|
|
4361
|
-
...(status ? { status } : {}),
|
|
4362
|
-
...(typeof body.detail === 'string' || body.detail === null ? { detail: body.detail as string | null } : {}),
|
|
4363
|
-
...(Array.isArray(body.nodeIds) ? { nodeIds: normalizeAxNodeIds(body.nodeIds) } : {}),
|
|
4364
|
-
},
|
|
4365
|
-
{ source: normalizeAxSource(body.source, 'api') },
|
|
4366
|
-
);
|
|
4367
|
-
if (!workItem) return responseJson({ ok: false, error: 'work item not found.' }, 404);
|
|
4368
|
-
broadcastWorkbenchEvent('ax-state-changed', {
|
|
4369
|
-
workItem,
|
|
4370
|
-
sessionId: primaryWorkbenchSessionId,
|
|
4371
|
-
timestamp: new Date().toISOString(),
|
|
4372
|
-
});
|
|
4373
|
-
return responseJson({ ok: true, workItem });
|
|
4374
|
-
}
|
|
2714
|
+
// handleAxWorkAdd / handleAxWorkUpdate migrated to the operation registry
|
|
2715
|
+
// (plan-007 Slice B wave 2): src/server/operations/ops/ax-work.ts.
|
|
4375
2716
|
|
|
4376
2717
|
function handleAxApprovalList(): Response {
|
|
4377
2718
|
return responseJson({ ok: true, approvalGates: canvasState.getApprovalGates() });
|
|
4378
2719
|
}
|
|
4379
2720
|
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
if (typeof body.title !== 'string' || !body.title.trim()) {
|
|
4383
|
-
return responseJson({ ok: false, error: 'approval request requires a title.' }, 400);
|
|
4384
|
-
}
|
|
4385
|
-
const approvalGate = canvasState.requestApproval(
|
|
4386
|
-
{
|
|
4387
|
-
title: body.title,
|
|
4388
|
-
...(typeof body.detail === 'string' ? { detail: body.detail } : {}),
|
|
4389
|
-
...(typeof body.action === 'string' ? { action: body.action } : {}),
|
|
4390
|
-
...(Array.isArray(body.nodeIds) ? { nodeIds: normalizeAxNodeIds(body.nodeIds) } : {}),
|
|
4391
|
-
},
|
|
4392
|
-
{ source: normalizeAxSource(body.source, 'api') },
|
|
4393
|
-
);
|
|
4394
|
-
broadcastWorkbenchEvent('ax-state-changed', {
|
|
4395
|
-
approvalGate,
|
|
4396
|
-
sessionId: primaryWorkbenchSessionId,
|
|
4397
|
-
timestamp: new Date().toISOString(),
|
|
4398
|
-
});
|
|
4399
|
-
return responseJson({ ok: true, approvalGate });
|
|
4400
|
-
}
|
|
4401
|
-
|
|
4402
|
-
async function handleAxApprovalResolve(req: Request, id: string): Promise<Response> {
|
|
4403
|
-
const body = await readJson(req);
|
|
4404
|
-
if (body.decision !== 'approved' && body.decision !== 'rejected') {
|
|
4405
|
-
return responseJson({ ok: false, error: 'resolve requires decision approved or rejected.' }, 400);
|
|
4406
|
-
}
|
|
4407
|
-
const approvalGate = canvasState.resolveApproval(
|
|
4408
|
-
id,
|
|
4409
|
-
body.decision,
|
|
4410
|
-
{
|
|
4411
|
-
...(typeof body.resolution === 'string' ? { resolution: body.resolution } : {}),
|
|
4412
|
-
source: normalizeAxSource(body.source, 'api'),
|
|
4413
|
-
},
|
|
4414
|
-
);
|
|
4415
|
-
if (!approvalGate) return responseJson({ ok: false, error: 'approval gate not found or already resolved.' }, 404);
|
|
4416
|
-
broadcastWorkbenchEvent('ax-state-changed', {
|
|
4417
|
-
approvalGate,
|
|
4418
|
-
sessionId: primaryWorkbenchSessionId,
|
|
4419
|
-
timestamp: new Date().toISOString(),
|
|
4420
|
-
});
|
|
4421
|
-
return responseJson({ ok: true, approvalGate });
|
|
4422
|
-
}
|
|
2721
|
+
// handleAxApprovalRequest / handleAxApprovalResolve migrated to the operation
|
|
2722
|
+
// registry (plan-007 Slice B wave 2): src/server/operations/ops/ax-work.ts.
|
|
4423
2723
|
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
if (!isAxEvidenceKind(body.kind) || typeof body.title !== 'string' || !body.title.trim()) {
|
|
4427
|
-
return responseJson({ ok: false, error: 'evidence requires kind and title.' }, 400);
|
|
4428
|
-
}
|
|
4429
|
-
const evidence = canvasState.addEvidence(
|
|
4430
|
-
{
|
|
4431
|
-
kind: body.kind,
|
|
4432
|
-
title: body.title,
|
|
4433
|
-
body: typeof body.body === 'string' ? body.body : null,
|
|
4434
|
-
ref: typeof body.ref === 'string' ? body.ref : null,
|
|
4435
|
-
nodeIds: normalizeAxNodeIds(body.nodeIds),
|
|
4436
|
-
data: isRecord(body.data) ? body.data : null,
|
|
4437
|
-
},
|
|
4438
|
-
{ source: normalizeAxSource(body.source, 'api') },
|
|
4439
|
-
);
|
|
4440
|
-
broadcastWorkbenchEvent('ax-event-created', {
|
|
4441
|
-
evidence,
|
|
4442
|
-
sessionId: primaryWorkbenchSessionId,
|
|
4443
|
-
timestamp: new Date().toISOString(),
|
|
4444
|
-
});
|
|
4445
|
-
return responseJson({ ok: true, evidence });
|
|
4446
|
-
}
|
|
2724
|
+
// handleAxEvidenceAdd migrated to the operation registry (plan-007 Slice B
|
|
2725
|
+
// wave 3): src/server/operations/ops/ax-timeline.ts.
|
|
4447
2726
|
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
const AX_REVIEW_STATUSES = new Set(['open', 'resolved', 'dismissed']);
|
|
4451
|
-
const AX_REVIEW_ANCHORS = new Set(['node', 'file', 'region']);
|
|
4452
|
-
|
|
4453
|
-
function normalizeAxReviewKind(value: unknown): PmxAxReviewKind | undefined {
|
|
4454
|
-
return typeof value === 'string' && AX_REVIEW_KINDS.has(value) ? value as PmxAxReviewKind : undefined;
|
|
4455
|
-
}
|
|
4456
|
-
function normalizeAxReviewSeverity(value: unknown): PmxAxReviewSeverity | undefined {
|
|
4457
|
-
return typeof value === 'string' && AX_REVIEW_SEVERITIES.has(value) ? value as PmxAxReviewSeverity : undefined;
|
|
4458
|
-
}
|
|
4459
|
-
function normalizeAxReviewStatus(value: unknown): PmxAxReviewStatus | undefined {
|
|
4460
|
-
return typeof value === 'string' && AX_REVIEW_STATUSES.has(value) ? value as PmxAxReviewStatus : undefined;
|
|
4461
|
-
}
|
|
4462
|
-
function normalizeAxReviewAnchor(value: unknown): PmxAxReviewAnchorType | undefined {
|
|
4463
|
-
return typeof value === 'string' && AX_REVIEW_ANCHORS.has(value) ? value as PmxAxReviewAnchorType : undefined;
|
|
4464
|
-
}
|
|
4465
|
-
function normalizeAxReviewRegion(value: unknown): PmxAxReviewRegion | undefined {
|
|
4466
|
-
if (!isRecord(value)) return undefined;
|
|
4467
|
-
return {
|
|
4468
|
-
...(typeof value.line === 'number' ? { line: value.line } : {}),
|
|
4469
|
-
...(typeof value.endLine === 'number' ? { endLine: value.endLine } : {}),
|
|
4470
|
-
...(typeof value.label === 'string' ? { label: value.label } : {}),
|
|
4471
|
-
};
|
|
4472
|
-
}
|
|
2727
|
+
// The AX review normalize helpers + their constant sets moved with the
|
|
2728
|
+
// migrated handlers (plan-007 Slice B wave 2): src/server/operations/ops/ax-work.ts.
|
|
4473
2729
|
|
|
4474
2730
|
function handleAxReviewList(): Response {
|
|
4475
2731
|
return responseJson({ ok: true, reviewAnnotations: canvasState.getReviewAnnotations() });
|
|
4476
2732
|
}
|
|
4477
2733
|
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
if (typeof body.body !== 'string' || !body.body.trim()) {
|
|
4481
|
-
return responseJson({ ok: false, error: 'review annotation requires a body.' }, 400);
|
|
4482
|
-
}
|
|
4483
|
-
const kind = normalizeAxReviewKind(body.kind);
|
|
4484
|
-
const severity = normalizeAxReviewSeverity(body.severity);
|
|
4485
|
-
const anchorType = normalizeAxReviewAnchor(body.anchorType);
|
|
4486
|
-
const region = normalizeAxReviewRegion(body.region);
|
|
4487
|
-
const reviewAnnotation = canvasState.addReviewAnnotation(
|
|
4488
|
-
{
|
|
4489
|
-
body: body.body,
|
|
4490
|
-
...(kind ? { kind } : {}),
|
|
4491
|
-
...(severity ? { severity } : {}),
|
|
4492
|
-
...(anchorType ? { anchorType } : {}),
|
|
4493
|
-
...(typeof body.nodeId === 'string' ? { nodeId: body.nodeId } : {}),
|
|
4494
|
-
...(typeof body.file === 'string' ? { file: body.file } : {}),
|
|
4495
|
-
...(region ? { region } : {}),
|
|
4496
|
-
...(typeof body.author === 'string' ? { author: body.author } : {}),
|
|
4497
|
-
},
|
|
4498
|
-
{ source: normalizeAxSource(body.source, 'api') },
|
|
4499
|
-
);
|
|
4500
|
-
if (!reviewAnnotation) {
|
|
4501
|
-
return responseJson({ ok: false, error: 'node-anchored review annotation requires a nodeId that exists on the canvas.' }, 400);
|
|
4502
|
-
}
|
|
4503
|
-
broadcastWorkbenchEvent('ax-state-changed', {
|
|
4504
|
-
reviewAnnotation,
|
|
4505
|
-
sessionId: primaryWorkbenchSessionId,
|
|
4506
|
-
timestamp: new Date().toISOString(),
|
|
4507
|
-
});
|
|
4508
|
-
return responseJson({ ok: true, reviewAnnotation });
|
|
4509
|
-
}
|
|
4510
|
-
|
|
4511
|
-
async function handleAxReviewUpdate(req: Request, id: string): Promise<Response> {
|
|
4512
|
-
const body = await readJson(req);
|
|
4513
|
-
const status = normalizeAxReviewStatus(body.status);
|
|
4514
|
-
const severity = normalizeAxReviewSeverity(body.severity);
|
|
4515
|
-
const kind = normalizeAxReviewKind(body.kind);
|
|
4516
|
-
const reviewAnnotation = canvasState.updateReviewAnnotation(
|
|
4517
|
-
id,
|
|
4518
|
-
{
|
|
4519
|
-
...(typeof body.body === 'string' ? { body: body.body } : {}),
|
|
4520
|
-
...(status ? { status } : {}),
|
|
4521
|
-
...(severity ? { severity } : {}),
|
|
4522
|
-
...(kind ? { kind } : {}),
|
|
4523
|
-
},
|
|
4524
|
-
{ source: normalizeAxSource(body.source, 'api') },
|
|
4525
|
-
);
|
|
4526
|
-
if (!reviewAnnotation) return responseJson({ ok: false, error: 'review annotation not found.' }, 404);
|
|
4527
|
-
broadcastWorkbenchEvent('ax-state-changed', {
|
|
4528
|
-
reviewAnnotation,
|
|
4529
|
-
sessionId: primaryWorkbenchSessionId,
|
|
4530
|
-
timestamp: new Date().toISOString(),
|
|
4531
|
-
});
|
|
4532
|
-
return responseJson({ ok: true, reviewAnnotation });
|
|
4533
|
-
}
|
|
2734
|
+
// handleAxReviewAdd / handleAxReviewUpdate migrated to the operation registry
|
|
2735
|
+
// (plan-007 Slice B wave 2): src/server/operations/ops/ax-work.ts.
|
|
4534
2736
|
|
|
4535
2737
|
function handleAxHostCapabilityGet(): Response {
|
|
4536
2738
|
return responseJson({ ok: true, host: canvasState.getHostCapability() });
|
|
4537
2739
|
}
|
|
4538
2740
|
|
|
4539
|
-
async function handleAxHostCapabilityReport(req: Request): Promise<Response> {
|
|
4540
|
-
const body = await readJson(req);
|
|
4541
|
-
const host = canvasState.setHostCapability(body, { source: normalizeAxSource(body.source, 'api') });
|
|
4542
|
-
broadcastWorkbenchEvent('ax-state-changed', {
|
|
4543
|
-
host,
|
|
4544
|
-
sessionId: primaryWorkbenchSessionId,
|
|
4545
|
-
timestamp: new Date().toISOString(),
|
|
4546
|
-
});
|
|
4547
|
-
return responseJson({ ok: true, host });
|
|
4548
|
-
}
|
|
4549
|
-
|
|
4550
2741
|
// ── Port resolution ───────────────────────────────────────────
|
|
4551
2742
|
|
|
4552
2743
|
function buildPortCandidates(preferredPort: number): number[] {
|
|
@@ -5310,6 +3501,15 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
5310
3501
|
hostname: DEFAULT_HOST,
|
|
5311
3502
|
port: portCandidate,
|
|
5312
3503
|
idleTimeout: 0,
|
|
3504
|
+
// Last-resort boundary: any throw that escapes the fetch handler must NOT
|
|
3505
|
+
// render Bun's default dev error overlay (HTTP 500 text/html disclosing the
|
|
3506
|
+
// absolute server source path + stack). Return a clean JSON 500 and log the
|
|
3507
|
+
// real error server-side only. Operation dispatch has its own catch
|
|
3508
|
+
// (operations/http.ts); this covers the hand-written routes too.
|
|
3509
|
+
error(error) {
|
|
3510
|
+
logWorkbenchWarning('serverFetch', error);
|
|
3511
|
+
return responseJson({ ok: false, error: 'Internal server error.' }, 500);
|
|
3512
|
+
},
|
|
5313
3513
|
async fetch(req) {
|
|
5314
3514
|
const url = new URL(req.url);
|
|
5315
3515
|
|
|
@@ -5370,30 +3570,20 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
5370
3570
|
return handleWorkbenchIntent(req);
|
|
5371
3571
|
}
|
|
5372
3572
|
|
|
5373
|
-
|
|
5374
|
-
|
|
5375
|
-
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
|
|
5379
|
-
|
|
5380
|
-
|
|
5381
|
-
if (url.pathname === '/api/workbench/webview/evaluate' && req.method === 'POST') {
|
|
5382
|
-
return handleWorkbenchWebViewEvaluate(req);
|
|
5383
|
-
}
|
|
5384
|
-
|
|
5385
|
-
if (url.pathname === '/api/workbench/webview/resize' && req.method === 'POST') {
|
|
5386
|
-
return handleWorkbenchWebViewResize(req);
|
|
3573
|
+
// Webview automation routes (plan-008 Wave 3): status / start /
|
|
3574
|
+
// evaluate / resize / stop are now registered operations served by the
|
|
3575
|
+
// registry. A null return falls through (e.g. the screenshot route
|
|
3576
|
+
// below, which stays hand-written — it returns a binary image, not a
|
|
3577
|
+
// JSON wire body).
|
|
3578
|
+
if (url.pathname.startsWith('/api/workbench/webview')) {
|
|
3579
|
+
const webviewResponse = await dispatchOperationRoute(req, url);
|
|
3580
|
+
if (webviewResponse) return webviewResponse;
|
|
5387
3581
|
}
|
|
5388
3582
|
|
|
5389
3583
|
if (url.pathname === '/api/workbench/webview/screenshot' && req.method === 'POST') {
|
|
5390
3584
|
return handleWorkbenchWebViewScreenshot(req);
|
|
5391
3585
|
}
|
|
5392
3586
|
|
|
5393
|
-
if (url.pathname === '/api/workbench/webview' && req.method === 'DELETE') {
|
|
5394
|
-
return handleWorkbenchWebViewStop();
|
|
5395
|
-
}
|
|
5396
|
-
|
|
5397
3587
|
if (url.pathname === '/api/file/save' && req.method === 'POST') {
|
|
5398
3588
|
return handleSave(req);
|
|
5399
3589
|
}
|
|
@@ -5402,16 +3592,11 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
5402
3592
|
return handleRender(req);
|
|
5403
3593
|
}
|
|
5404
3594
|
|
|
5405
|
-
//
|
|
5406
|
-
|
|
5407
|
-
|
|
5408
|
-
|
|
5409
|
-
|
|
5410
|
-
: serializeCanvasLayoutWithBlobSummaries(withContextPinLayoutReadState(canvasState.getLayoutForPersistence())));
|
|
5411
|
-
}
|
|
5412
|
-
|
|
5413
|
-
if (url.pathname === '/api/canvas/summary' && req.method === 'GET') {
|
|
5414
|
-
return responseJson(buildCanvasSummary());
|
|
3595
|
+
// Operation registry routes (plan-005): registered operations are
|
|
3596
|
+
// dispatched here; a null return falls through to the legacy routes.
|
|
3597
|
+
if (url.pathname.startsWith('/api/canvas/')) {
|
|
3598
|
+
const operationResponse = await dispatchOperationRoute(req, url);
|
|
3599
|
+
if (operationResponse) return operationResponse;
|
|
5415
3600
|
}
|
|
5416
3601
|
|
|
5417
3602
|
if (url.pathname === '/api/canvas/theme' && req.method === 'GET') {
|
|
@@ -5426,17 +3611,8 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
5426
3611
|
return handleCanvasUpdate(req);
|
|
5427
3612
|
}
|
|
5428
3613
|
|
|
5429
|
-
|
|
5430
|
-
|
|
5431
|
-
}
|
|
5432
|
-
|
|
5433
|
-
if (url.pathname === '/api/canvas/schema/validate' && req.method === 'POST') {
|
|
5434
|
-
return handleCanvasValidateSpec(req);
|
|
5435
|
-
}
|
|
5436
|
-
|
|
5437
|
-
if (url.pathname === '/api/canvas/batch' && req.method === 'POST') {
|
|
5438
|
-
return handleCanvasBatch(req);
|
|
5439
|
-
}
|
|
3614
|
+
// POST /api/canvas/batch migrated to the operation registry
|
|
3615
|
+
// (plan-008 Wave 2): src/server/operations/ops/batch.ts.
|
|
5440
3616
|
|
|
5441
3617
|
if (url.pathname === '/api/canvas/viewport' && req.method === 'POST') {
|
|
5442
3618
|
return handleCanvasViewport(req);
|
|
@@ -5446,25 +3622,9 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
5446
3622
|
return handleCanvasAddAnnotation(req);
|
|
5447
3623
|
}
|
|
5448
3624
|
|
|
5449
|
-
|
|
5450
|
-
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
if (url.pathname === '/api/canvas/node' && req.method === 'POST') {
|
|
5454
|
-
return handleCanvasAddNode(req);
|
|
5455
|
-
}
|
|
5456
|
-
|
|
5457
|
-
if (url.pathname === '/api/canvas/mcp-app/open' && req.method === 'POST') {
|
|
5458
|
-
return handleCanvasOpenMcpApp(req);
|
|
5459
|
-
}
|
|
5460
|
-
|
|
5461
|
-
if (url.pathname === '/api/canvas/diagram' && req.method === 'POST') {
|
|
5462
|
-
return handleCanvasAddDiagram(req);
|
|
5463
|
-
}
|
|
5464
|
-
|
|
5465
|
-
if (url.pathname === '/api/canvas/web-artifact' && req.method === 'POST') {
|
|
5466
|
-
return handleCanvasBuildWebArtifact(req);
|
|
5467
|
-
}
|
|
3625
|
+
// POST /api/canvas/mcp-app/open, /api/canvas/diagram, and
|
|
3626
|
+
// /api/canvas/web-artifact migrated to the operation registry
|
|
3627
|
+
// (plan-008 Wave 4): src/server/operations/ops/app.ts.
|
|
5468
3628
|
|
|
5469
3629
|
// Individual node GET/PATCH/DELETE
|
|
5470
3630
|
if (url.pathname.startsWith('/api/canvas/node/') && url.pathname.endsWith('/refresh') && req.method === 'POST') {
|
|
@@ -5472,111 +3632,15 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
5472
3632
|
return handleCanvasRefreshWebpageNode(nodeId, req);
|
|
5473
3633
|
}
|
|
5474
3634
|
|
|
5475
|
-
if (url.pathname.startsWith('/api/canvas/node/') && req.method === 'GET') {
|
|
5476
|
-
const nodeId = decodeURIComponent(url.pathname.slice('/api/canvas/node/'.length));
|
|
5477
|
-
const includeBlobs = url.searchParams.get('includeBlobs') === 'true';
|
|
5478
|
-
const node = includeBlobs ? canvasState.getNode(nodeId) : canvasState.getNodeForPersistence(nodeId);
|
|
5479
|
-
if (!node) return responseJson({ ok: false, error: `Node "${nodeId}" not found.` }, 404);
|
|
5480
|
-
const responseNode = withContextPinReadState(node);
|
|
5481
|
-
return responseJson(includeBlobs
|
|
5482
|
-
? serializeCanvasNode(responseNode)
|
|
5483
|
-
: serializeCanvasNodeWithBlobSummaries(responseNode));
|
|
5484
|
-
}
|
|
5485
|
-
|
|
5486
|
-
if (url.pathname.startsWith('/api/canvas/node/') && req.method === 'PATCH') {
|
|
5487
|
-
const nodeId = url.pathname.slice('/api/canvas/node/'.length);
|
|
5488
|
-
return handleCanvasUpdateNode(nodeId, req);
|
|
5489
|
-
}
|
|
5490
|
-
|
|
5491
|
-
if (url.pathname.startsWith('/api/canvas/node/') && req.method === 'DELETE') {
|
|
5492
|
-
const nodeId = url.pathname.slice('/api/canvas/node/'.length);
|
|
5493
|
-
closeNodeAppSession(canvasState.getNode(nodeId));
|
|
5494
|
-
const result = removeCanvasNode(nodeId);
|
|
5495
|
-
if (!result.removed) return responseJson({ ok: false, error: `Node "${nodeId}" not found.` }, 404);
|
|
5496
|
-
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
5497
|
-
if (result.needsCodeGraphRecompute) {
|
|
5498
|
-
scheduleCodeGraphRecompute(() => {
|
|
5499
|
-
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
5500
|
-
});
|
|
5501
|
-
}
|
|
5502
|
-
return responseJson({ ok: true, removed: nodeId });
|
|
5503
|
-
}
|
|
5504
|
-
|
|
5505
3635
|
if (url.pathname.startsWith('/api/canvas/image/') && req.method === 'GET') {
|
|
5506
3636
|
return await handleCanvasImage(url.pathname);
|
|
5507
3637
|
}
|
|
5508
3638
|
|
|
5509
|
-
if (url.pathname === '/api/canvas/edge' && req.method === 'POST') {
|
|
5510
|
-
return handleCanvasAddEdge(req);
|
|
5511
|
-
}
|
|
5512
|
-
|
|
5513
|
-
if (url.pathname === '/api/canvas/edge' && req.method === 'DELETE') {
|
|
5514
|
-
return handleCanvasRemoveEdge(req);
|
|
5515
|
-
}
|
|
5516
|
-
|
|
5517
|
-
// Snapshot API
|
|
5518
|
-
if (url.pathname === '/api/canvas/snapshots' && req.method === 'GET') {
|
|
5519
|
-
return responseJson(listCanvasSnapshots({
|
|
5520
|
-
limit: parsePositiveIntegerParam(url.searchParams.get('limit')),
|
|
5521
|
-
query: url.searchParams.get('q') ?? url.searchParams.get('query') ?? undefined,
|
|
5522
|
-
before: url.searchParams.get('before') ?? undefined,
|
|
5523
|
-
after: url.searchParams.get('after') ?? undefined,
|
|
5524
|
-
all: url.searchParams.get('all') === 'true',
|
|
5525
|
-
}));
|
|
5526
|
-
}
|
|
5527
|
-
|
|
5528
|
-
if (url.pathname === '/api/canvas/snapshots' && req.method === 'POST') {
|
|
5529
|
-
return handleSnapshotSave(req);
|
|
5530
|
-
}
|
|
5531
|
-
|
|
5532
|
-
if (url.pathname === '/api/canvas/snapshots/gc' && req.method === 'POST') {
|
|
5533
|
-
return handleSnapshotGc(req);
|
|
5534
|
-
}
|
|
5535
|
-
|
|
5536
|
-
if (url.pathname === '/api/canvas/snapshots/diff' && req.method === 'GET') {
|
|
5537
|
-
const name = url.searchParams.get('name') ?? url.searchParams.get('id') ?? '';
|
|
5538
|
-
if (!name.trim()) return responseJson({ ok: false, error: 'Missing snapshot name or id.' }, 400);
|
|
5539
|
-
const snapshot = canvasState.getSnapshotData(name);
|
|
5540
|
-
if (!snapshot) return responseJson({ ok: false, error: `Snapshot "${name}" not found.` }, 404);
|
|
5541
|
-
const diff = diffLayouts(snapshot.name, snapshot, canvasState.getLayout());
|
|
5542
|
-
return responseJson({ ok: true, text: formatDiff(diff), diff });
|
|
5543
|
-
}
|
|
5544
|
-
|
|
5545
|
-
if (url.pathname.startsWith('/api/canvas/snapshots/') && url.pathname.endsWith('/diff') && req.method === 'GET') {
|
|
5546
|
-
const id = decodeURIComponent(url.pathname.slice('/api/canvas/snapshots/'.length, -'/diff'.length));
|
|
5547
|
-
const snapshot = canvasState.getSnapshotData(id);
|
|
5548
|
-
if (!snapshot) return responseJson({ ok: false, error: `Snapshot "${id}" not found.` }, 404);
|
|
5549
|
-
const diff = diffLayouts(snapshot.name, snapshot, canvasState.getLayout());
|
|
5550
|
-
return responseJson({ ok: true, text: formatDiff(diff), diff });
|
|
5551
|
-
}
|
|
5552
|
-
|
|
5553
|
-
if (url.pathname.startsWith('/api/canvas/snapshots/') && req.method === 'POST') {
|
|
5554
|
-
const id = decodeURIComponent(url.pathname.slice('/api/canvas/snapshots/'.length));
|
|
5555
|
-
const result = await restoreCanvasSnapshot(id);
|
|
5556
|
-
if (!result.ok) return responseText('Snapshot not found', 404);
|
|
5557
|
-
broadcastWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
5558
|
-
return responseJson({ ok: true });
|
|
5559
|
-
}
|
|
5560
|
-
|
|
5561
|
-
if (url.pathname.startsWith('/api/canvas/snapshots/') && req.method === 'DELETE') {
|
|
5562
|
-
const id = url.pathname.split('/').pop() ?? '';
|
|
5563
|
-
const result = deleteCanvasSnapshot(id);
|
|
5564
|
-
if (!result.ok) return responseText('Snapshot not found', 404);
|
|
5565
|
-
return responseJson({ ok: true });
|
|
5566
|
-
}
|
|
5567
|
-
|
|
5568
|
-
// Context pins API
|
|
5569
|
-
if (url.pathname === '/api/canvas/context-pins' && req.method === 'POST') {
|
|
5570
|
-
return handleContextPinsUpdate(req);
|
|
5571
|
-
}
|
|
5572
|
-
|
|
5573
3639
|
if (url.pathname === '/api/canvas/pinned-context' && req.method === 'GET') {
|
|
5574
3640
|
return handleGetPinnedContext();
|
|
5575
3641
|
}
|
|
5576
3642
|
|
|
5577
|
-
|
|
5578
|
-
return handleGetAxState();
|
|
5579
|
-
}
|
|
3643
|
+
// GET /api/canvas/ax migrated to the operation registry (plan-007 Slice B.1).
|
|
5580
3644
|
|
|
5581
3645
|
if (url.pathname === '/api/canvas/ax' && req.method === 'PATCH') {
|
|
5582
3646
|
return handleAxStatePatch(req);
|
|
@@ -5598,202 +3662,85 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
5598
3662
|
return handleOpenExternalSurface(req);
|
|
5599
3663
|
}
|
|
5600
3664
|
|
|
5601
|
-
|
|
5602
|
-
return handleAxFocusUpdate(req);
|
|
5603
|
-
}
|
|
3665
|
+
// POST /api/canvas/ax/focus migrated to the operation registry (plan-007 Slice B.1).
|
|
5604
3666
|
|
|
5605
|
-
|
|
5606
|
-
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
if (url.pathname === '/api/canvas/ax/steer' && req.method === 'POST') {
|
|
5610
|
-
return handleAxSteer(req);
|
|
5611
|
-
}
|
|
5612
|
-
|
|
5613
|
-
if (url.pathname === '/api/canvas/ax/timeline' && req.method === 'GET') {
|
|
5614
|
-
return handleAxTimelineGet(url);
|
|
5615
|
-
}
|
|
3667
|
+
// POST /api/canvas/ax/event + POST /api/canvas/ax/steer + GET
|
|
3668
|
+
// /api/canvas/ax/timeline migrated to the operation registry
|
|
3669
|
+
// (plan-007 Slice B wave 3): src/server/operations/ops/ax-timeline.ts.
|
|
5616
3670
|
|
|
5617
3671
|
if (url.pathname === '/api/canvas/ax/work' && req.method === 'GET') {
|
|
5618
3672
|
return handleAxWorkList();
|
|
5619
3673
|
}
|
|
5620
3674
|
|
|
5621
|
-
|
|
5622
|
-
|
|
5623
|
-
}
|
|
5624
|
-
|
|
5625
|
-
if (url.pathname.startsWith('/api/canvas/ax/work/') && req.method === 'PATCH') {
|
|
5626
|
-
const workItemId = decodeURIComponent(url.pathname.slice('/api/canvas/ax/work/'.length));
|
|
5627
|
-
return handleAxWorkUpdate(req, workItemId);
|
|
5628
|
-
}
|
|
3675
|
+
// POST /api/canvas/ax/work + PATCH /api/canvas/ax/work/:id migrated to
|
|
3676
|
+
// the operation registry (plan-007 Slice B wave 2).
|
|
5629
3677
|
|
|
5630
3678
|
if (url.pathname === '/api/canvas/ax/approval' && req.method === 'GET') {
|
|
5631
3679
|
return handleAxApprovalList();
|
|
5632
3680
|
}
|
|
5633
3681
|
|
|
5634
|
-
|
|
5635
|
-
|
|
5636
|
-
}
|
|
3682
|
+
// POST /api/canvas/ax/approval + POST /api/canvas/ax/approval/:id/resolve
|
|
3683
|
+
// migrated to the operation registry (plan-007 Slice B wave 2).
|
|
5637
3684
|
|
|
5638
|
-
|
|
5639
|
-
|
|
5640
|
-
url.pathname.slice('/api/canvas/ax/approval/'.length, -'/resolve'.length),
|
|
5641
|
-
);
|
|
5642
|
-
return handleAxApprovalResolve(req, approvalId);
|
|
5643
|
-
}
|
|
3685
|
+
// GET /api/canvas/ax/approval/:id (single-item read + ?waitMs long-poll)
|
|
3686
|
+
// migrated to the operation registry (plan-007 Slice B wave 4).
|
|
5644
3687
|
|
|
5645
|
-
|
|
5646
|
-
|
|
5647
|
-
return handleAxApprovalGet(url, approvalId, req);
|
|
5648
|
-
}
|
|
5649
|
-
|
|
5650
|
-
if (url.pathname === '/api/canvas/ax/evidence' && req.method === 'POST') {
|
|
5651
|
-
return handleAxEvidenceAdd(req);
|
|
5652
|
-
}
|
|
3688
|
+
// POST /api/canvas/ax/evidence migrated to the operation registry
|
|
3689
|
+
// (plan-007 Slice B wave 3): src/server/operations/ops/ax-timeline.ts.
|
|
5653
3690
|
|
|
5654
3691
|
if (url.pathname === '/api/canvas/ax/review' && req.method === 'GET') {
|
|
5655
3692
|
return handleAxReviewList();
|
|
5656
3693
|
}
|
|
5657
3694
|
|
|
5658
|
-
|
|
5659
|
-
|
|
5660
|
-
}
|
|
5661
|
-
|
|
5662
|
-
if (url.pathname.startsWith('/api/canvas/ax/review/') && req.method === 'PATCH') {
|
|
5663
|
-
const reviewId = decodeURIComponent(url.pathname.slice('/api/canvas/ax/review/'.length));
|
|
5664
|
-
return handleAxReviewUpdate(req, reviewId);
|
|
5665
|
-
}
|
|
3695
|
+
// POST /api/canvas/ax/review + PATCH /api/canvas/ax/review/:id migrated
|
|
3696
|
+
// to the operation registry (plan-007 Slice B wave 2).
|
|
5666
3697
|
|
|
5667
3698
|
if (url.pathname === '/api/canvas/ax/host-capability' && req.method === 'GET') {
|
|
5668
3699
|
return handleAxHostCapabilityGet();
|
|
5669
3700
|
}
|
|
5670
3701
|
|
|
5671
|
-
|
|
5672
|
-
return handleAxHostCapabilityReport(req);
|
|
5673
|
-
}
|
|
3702
|
+
// PUT /api/canvas/ax/host-capability migrated to the operation registry (plan-007 Slice B.1).
|
|
5674
3703
|
|
|
5675
3704
|
if (url.pathname === '/api/canvas/ax/interaction' && req.method === 'POST') {
|
|
5676
3705
|
return handleAxInteraction(req);
|
|
5677
3706
|
}
|
|
5678
3707
|
|
|
5679
|
-
|
|
5680
|
-
|
|
5681
|
-
|
|
5682
|
-
|
|
5683
|
-
if (url.pathname.startsWith('/api/canvas/ax/delivery/') && url.pathname.endsWith('/mark') && req.method === 'POST') {
|
|
5684
|
-
const deliveryId = decodeURIComponent(
|
|
5685
|
-
url.pathname.slice('/api/canvas/ax/delivery/'.length, -'/mark'.length),
|
|
5686
|
-
);
|
|
5687
|
-
return handleAxDeliveryMark(deliveryId);
|
|
5688
|
-
}
|
|
3708
|
+
// GET /api/canvas/ax/delivery/pending + POST /api/canvas/ax/delivery/:id/mark
|
|
3709
|
+
// migrated to the operation registry (plan-007 Slice B wave 3):
|
|
3710
|
+
// src/server/operations/ops/ax-timeline.ts.
|
|
5689
3711
|
|
|
5690
3712
|
if (url.pathname === '/api/canvas/ax/elicitation' && req.method === 'GET') {
|
|
5691
3713
|
return handleAxElicitationList();
|
|
5692
3714
|
}
|
|
5693
3715
|
|
|
5694
|
-
|
|
5695
|
-
|
|
5696
|
-
}
|
|
3716
|
+
// POST /api/canvas/ax/elicitation + POST /api/canvas/ax/elicitation/:id/respond
|
|
3717
|
+
// migrated to the operation registry (plan-007 Slice B wave 2).
|
|
5697
3718
|
|
|
5698
|
-
|
|
5699
|
-
|
|
5700
|
-
url.pathname.slice('/api/canvas/ax/elicitation/'.length, -'/respond'.length),
|
|
5701
|
-
);
|
|
5702
|
-
return handleAxElicitationRespond(req, elicitationId);
|
|
5703
|
-
}
|
|
5704
|
-
|
|
5705
|
-
if (url.pathname.startsWith('/api/canvas/ax/elicitation/') && !url.pathname.endsWith('/respond') && req.method === 'GET') {
|
|
5706
|
-
const elicitationId = decodeURIComponent(url.pathname.slice('/api/canvas/ax/elicitation/'.length));
|
|
5707
|
-
return handleAxElicitationGet(url, elicitationId, req);
|
|
5708
|
-
}
|
|
3719
|
+
// GET /api/canvas/ax/elicitation/:id (single-item read + ?waitMs long-poll)
|
|
3720
|
+
// migrated to the operation registry (plan-007 Slice B wave 4).
|
|
5709
3721
|
|
|
5710
3722
|
if (url.pathname === '/api/canvas/ax/mode' && req.method === 'GET') {
|
|
5711
3723
|
return handleAxModeList();
|
|
5712
3724
|
}
|
|
5713
3725
|
|
|
5714
|
-
|
|
5715
|
-
|
|
5716
|
-
}
|
|
3726
|
+
// POST /api/canvas/ax/mode + POST /api/canvas/ax/mode/:id/resolve migrated
|
|
3727
|
+
// to the operation registry (plan-007 Slice B wave 2).
|
|
5717
3728
|
|
|
5718
|
-
|
|
5719
|
-
|
|
5720
|
-
url.pathname.slice('/api/canvas/ax/mode/'.length, -'/resolve'.length),
|
|
5721
|
-
);
|
|
5722
|
-
return handleAxModeResolve(req, modeId);
|
|
5723
|
-
}
|
|
5724
|
-
|
|
5725
|
-
if (url.pathname.startsWith('/api/canvas/ax/mode/') && !url.pathname.endsWith('/resolve') && req.method === 'GET') {
|
|
5726
|
-
const modeId = decodeURIComponent(url.pathname.slice('/api/canvas/ax/mode/'.length));
|
|
5727
|
-
return handleAxModeGet(url, modeId, req);
|
|
5728
|
-
}
|
|
3729
|
+
// GET /api/canvas/ax/mode/:id (single-item read + ?waitMs long-poll)
|
|
3730
|
+
// migrated to the operation registry (plan-007 Slice B wave 4).
|
|
5729
3731
|
|
|
5730
3732
|
if (url.pathname === '/api/canvas/ax/command' && req.method === 'GET') {
|
|
5731
3733
|
return handleAxCommandList();
|
|
5732
3734
|
}
|
|
5733
3735
|
|
|
5734
|
-
|
|
5735
|
-
|
|
5736
|
-
}
|
|
3736
|
+
// POST /api/canvas/ax/command migrated to the operation registry
|
|
3737
|
+
// (plan-007 Slice B wave 3): src/server/operations/ops/ax-timeline.ts.
|
|
5737
3738
|
|
|
5738
3739
|
if (url.pathname === '/api/canvas/ax/policy' && req.method === 'GET') {
|
|
5739
3740
|
return handleAxPolicyGet();
|
|
5740
3741
|
}
|
|
5741
3742
|
|
|
5742
|
-
|
|
5743
|
-
return handleAxPolicySet(req);
|
|
5744
|
-
}
|
|
5745
|
-
|
|
5746
|
-
// Spatial context API
|
|
5747
|
-
if (url.pathname === '/api/canvas/spatial-context' && req.method === 'GET') {
|
|
5748
|
-
const layout = canvasState.getLayout();
|
|
5749
|
-
const spatial = buildSpatialContext(layout.nodes, layout.edges, canvasState.contextPinnedNodeIds, layout.annotations);
|
|
5750
|
-
return responseJson(spatial);
|
|
5751
|
-
}
|
|
5752
|
-
|
|
5753
|
-
// Search API
|
|
5754
|
-
if (url.pathname === '/api/canvas/search' && req.method === 'GET') {
|
|
5755
|
-
const q = url.searchParams.get('q') ?? '';
|
|
5756
|
-
if (!q.trim()) {
|
|
5757
|
-
return responseJson({ results: [], query: q });
|
|
5758
|
-
}
|
|
5759
|
-
const results = searchNodes(canvasState.getLayout().nodes, q);
|
|
5760
|
-
return responseJson({ results, query: q });
|
|
5761
|
-
}
|
|
5762
|
-
|
|
5763
|
-
// Group API
|
|
5764
|
-
if (url.pathname === '/api/canvas/group' && req.method === 'POST') {
|
|
5765
|
-
return handleCanvasCreateGroup(req);
|
|
5766
|
-
}
|
|
5767
|
-
|
|
5768
|
-
if (url.pathname === '/api/canvas/group/add' && req.method === 'POST') {
|
|
5769
|
-
return handleCanvasGroupNodes(req);
|
|
5770
|
-
}
|
|
5771
|
-
|
|
5772
|
-
if (url.pathname === '/api/canvas/group/ungroup' && req.method === 'POST') {
|
|
5773
|
-
return handleCanvasUngroupNodes(req);
|
|
5774
|
-
}
|
|
5775
|
-
|
|
5776
|
-
// Arrange / Focus / Clear API (for agent CLI)
|
|
5777
|
-
if (url.pathname === '/api/canvas/arrange' && req.method === 'POST') {
|
|
5778
|
-
return handleCanvasArrange(req);
|
|
5779
|
-
}
|
|
5780
|
-
|
|
5781
|
-
if (url.pathname === '/api/canvas/focus' && req.method === 'POST') {
|
|
5782
|
-
return handleCanvasFocus(req);
|
|
5783
|
-
}
|
|
5784
|
-
|
|
5785
|
-
if (url.pathname === '/api/canvas/fit' && req.method === 'POST') {
|
|
5786
|
-
return handleCanvasFit(req);
|
|
5787
|
-
}
|
|
5788
|
-
|
|
5789
|
-
if (url.pathname === '/api/canvas/clear' && req.method === 'POST') {
|
|
5790
|
-
for (const node of canvasState.getLayout().nodes) {
|
|
5791
|
-
closeNodeAppSession(node);
|
|
5792
|
-
}
|
|
5793
|
-
clearCanvas();
|
|
5794
|
-
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
5795
|
-
return responseJson({ ok: true });
|
|
5796
|
-
}
|
|
3743
|
+
// POST /api/canvas/ax/policy migrated to the operation registry (plan-007 Slice B.1).
|
|
5797
3744
|
|
|
5798
3745
|
// Code graph API
|
|
5799
3746
|
if (url.pathname === '/api/canvas/code-graph' && req.method === 'GET') {
|
|
@@ -5801,54 +3748,10 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
5801
3748
|
return responseJson(summary);
|
|
5802
3749
|
}
|
|
5803
3750
|
|
|
5804
|
-
if (url.pathname === '/api/canvas/json-render' && req.method === 'POST') {
|
|
5805
|
-
return handleCanvasAddJsonRender(req);
|
|
5806
|
-
}
|
|
5807
|
-
|
|
5808
|
-
if (url.pathname === '/api/canvas/json-render/stream' && req.method === 'POST') {
|
|
5809
|
-
return handleJsonRenderStream(req);
|
|
5810
|
-
}
|
|
5811
|
-
|
|
5812
|
-
if (url.pathname === '/api/canvas/graph' && req.method === 'POST') {
|
|
5813
|
-
return handleCanvasAddGraph(req);
|
|
5814
|
-
}
|
|
5815
|
-
|
|
5816
3751
|
if (url.pathname === '/api/canvas/prompt' && req.method === 'POST') {
|
|
5817
3752
|
return handleCanvasPrompt(req);
|
|
5818
3753
|
}
|
|
5819
3754
|
|
|
5820
|
-
// Undo/Redo/History API
|
|
5821
|
-
if (url.pathname === '/api/canvas/undo' && req.method === 'POST') {
|
|
5822
|
-
const entry = mutationHistory.undo();
|
|
5823
|
-
if (!entry) return responseJson({ ok: false, description: 'Nothing to undo' });
|
|
5824
|
-
await syncCanvasRuntimeBackends();
|
|
5825
|
-
emitPrimaryWorkbenchEvent('canvas-viewport-update', { viewport: canvasState.viewport });
|
|
5826
|
-
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
5827
|
-
return responseJson({ ok: true, description: `Undid: ${entry.description}` });
|
|
5828
|
-
}
|
|
5829
|
-
|
|
5830
|
-
if (url.pathname === '/api/canvas/redo' && req.method === 'POST') {
|
|
5831
|
-
const entry = mutationHistory.redo();
|
|
5832
|
-
if (!entry) return responseJson({ ok: false, description: 'Nothing to redo' });
|
|
5833
|
-
await syncCanvasRuntimeBackends();
|
|
5834
|
-
emitPrimaryWorkbenchEvent('canvas-viewport-update', { viewport: canvasState.viewport });
|
|
5835
|
-
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
5836
|
-
return responseJson({ ok: true, description: `Redid: ${entry.description}` });
|
|
5837
|
-
}
|
|
5838
|
-
|
|
5839
|
-
if (url.pathname === '/api/canvas/history' && req.method === 'GET') {
|
|
5840
|
-
return responseJson({
|
|
5841
|
-
text: mutationHistory.toHumanReadable(),
|
|
5842
|
-
entries: mutationHistory.getSummaries(),
|
|
5843
|
-
canUndo: mutationHistory.canUndo(),
|
|
5844
|
-
canRedo: mutationHistory.canRedo(),
|
|
5845
|
-
});
|
|
5846
|
-
}
|
|
5847
|
-
|
|
5848
|
-
if (url.pathname === '/api/canvas/validate' && req.method === 'GET') {
|
|
5849
|
-
return handleCanvasValidate();
|
|
5850
|
-
}
|
|
5851
|
-
|
|
5852
3755
|
if (url.pathname === '/api/ext-app/call-tool' && req.method === 'POST') {
|
|
5853
3756
|
return handleExtAppCallTool(req);
|
|
5854
3757
|
}
|