pmx-canvas 0.1.35 → 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 +461 -0
- package/Readme.md +14 -2
- package/dist/canvas/index.js +82 -41
- package/dist/json-render/index.js +89 -334
- package/dist/types/client/nodes/ExtAppFrame.d.ts +2 -0
- package/dist/types/mcp/canvas-access.d.ts +12 -159
- package/dist/types/server/ax-context.d.ts +1 -1
- package/dist/types/server/ax-state-manager.d.ts +256 -0
- package/dist/types/server/ax-state.d.ts +29 -1
- package/dist/types/server/ax-wait.d.ts +23 -0
- package/dist/types/server/canvas-operations.d.ts +1 -12
- package/dist/types/server/canvas-state.d.ts +46 -14
- package/dist/types/server/html-surface.d.ts +7 -0
- package/dist/types/server/index.d.ts +66 -26
- 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-host-adapter-contract.md +65 -0
- package/docs/ax-state-contract.md +72 -0
- package/docs/http-api.md +34 -2
- package/docs/mcp.md +64 -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/screenshot.png +0 -0
- package/docs/tech-debt-assessment-2026-06.md +90 -0
- package/package.json +3 -3
- package/skills/pmx-canvas/SKILL.md +233 -185
- package/skills/pmx-canvas/evals/evals.json +3 -3
- package/skills/pmx-canvas/references/codex-app-adapter.md +24 -11
- package/skills/pmx-canvas/references/github-copilot-app-adapter.md +31 -1
- package/src/cli/agent.ts +52 -31
- package/src/client/nodes/ExtAppFrame.tsx +73 -5
- package/src/client/nodes/HtmlNode.tsx +12 -3
- package/src/client/nodes/McpAppNode.tsx +12 -3
- package/src/json-render/renderer/index.tsx +3 -0
- package/src/mcp/canvas-access.ts +43 -774
- package/src/mcp/server.ts +190 -2001
- package/src/server/ax-context.ts +7 -1
- package/src/server/ax-state-manager.ts +808 -0
- package/src/server/ax-state.ts +89 -2
- package/src/server/ax-wait.ts +56 -0
- package/src/server/canvas-operations.ts +2 -328
- package/src/server/canvas-schema.ts +2 -2
- package/src/server/canvas-state.ts +140 -382
- package/src/server/html-surface.ts +49 -11
- package/src/server/index.ts +136 -192
- 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 +253 -2170
- 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,96 +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
71
|
import { applyAxInteraction, resolveNodeAxCapabilities } from './ax-interaction.js';
|
|
82
|
-
import {
|
|
72
|
+
import { isAxEvidenceKind, isAxActivityKind } from './ax-state.js';
|
|
83
73
|
import type {
|
|
74
|
+
PmxAxEvidenceKind,
|
|
84
75
|
PmxAxPolicy,
|
|
85
76
|
PmxAxReviewAnchorType,
|
|
86
77
|
PmxAxReviewKind,
|
|
87
|
-
PmxAxReviewRegion,
|
|
88
78
|
PmxAxReviewSeverity,
|
|
89
|
-
PmxAxReviewStatus,
|
|
90
79
|
PmxAxSource,
|
|
80
|
+
PmxAxWorkItemStatus,
|
|
91
81
|
} from './ax-state.js';
|
|
92
82
|
import { normalizeCanvasTheme, type CanvasTheme } from './canvas-db.js';
|
|
93
83
|
import { validateLocalImageFile } from './image-source.js';
|
|
94
84
|
import {
|
|
95
|
-
addCanvasNode,
|
|
96
|
-
addCanvasEdge,
|
|
97
|
-
MARKDOWN_NODE_DEFAULT_SIZE,
|
|
98
|
-
MCP_APP_NODE_DEFAULT_SIZE,
|
|
99
|
-
IMAGE_NODE_DEFAULT_SIZE,
|
|
100
|
-
LEDGER_NODE_DEFAULT_SIZE,
|
|
101
85
|
applyCanvasNodeUpdates,
|
|
102
|
-
appendCanvasJsonRenderStream,
|
|
103
|
-
buildStructuredNodeUpdate,
|
|
104
|
-
arrangeCanvasNodes,
|
|
105
|
-
clearCanvas,
|
|
106
|
-
createCanvasGraphNode,
|
|
107
|
-
createCanvasGroup,
|
|
108
|
-
createCanvasJsonRenderNode,
|
|
109
|
-
createCanvasStreamingJsonRenderNode,
|
|
110
|
-
deleteCanvasSnapshot,
|
|
111
|
-
executeCanvasBatch,
|
|
112
|
-
fitCanvasView,
|
|
113
|
-
gcCanvasSnapshots,
|
|
114
|
-
groupCanvasNodes,
|
|
115
|
-
listCanvasSnapshots,
|
|
116
86
|
refreshCanvasWebpageNode,
|
|
117
|
-
removeCanvasNode,
|
|
118
|
-
removeCanvasEdge,
|
|
119
|
-
resolveHtmlContent,
|
|
120
|
-
restoreCanvasSnapshot,
|
|
121
|
-
saveCanvasSnapshot,
|
|
122
|
-
scheduleCodeGraphRecompute,
|
|
123
87
|
primeCanvasRuntimeBackends,
|
|
124
88
|
setCanvasLayoutUpdateEmitter,
|
|
125
89
|
syncCanvasRuntimeBackends,
|
|
126
|
-
setCanvasContextPins,
|
|
127
|
-
ungroupCanvasNodes,
|
|
128
|
-
validateCanvasNodePatch,
|
|
129
|
-
hasStructuredNodeUpdateFields,
|
|
130
|
-
hasTraceNodeDataFields,
|
|
131
|
-
mergeTraceNodeDataFields,
|
|
132
90
|
} from './canvas-operations.js';
|
|
133
|
-
import {
|
|
134
|
-
import {
|
|
135
|
-
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';
|
|
136
97
|
import {
|
|
137
98
|
EXCALIDRAW_READ_CHECKPOINT_TOOL,
|
|
138
99
|
EXCALIDRAW_SAVE_CHECKPOINT_TOOL,
|
|
139
100
|
buildExcalidrawCheckpointId,
|
|
140
|
-
buildExcalidrawOpenMcpAppInput,
|
|
141
101
|
buildExcalidrawRestoreCheckpointToolInput,
|
|
142
102
|
ensureExcalidrawCheckpointId,
|
|
143
103
|
getExcalidrawCheckpointIdFromToolResult,
|
|
144
104
|
isExcalidrawCreateView,
|
|
145
105
|
} from './diagram-presets.js';
|
|
146
106
|
import { traceManager } from './trace-manager.js';
|
|
147
|
-
import { buildWebArtifactOnCanvas, resolveWorkspacePath } from './web-artifacts.js';
|
|
148
107
|
import {
|
|
149
108
|
buildJsonRenderViewerHtml,
|
|
150
109
|
} from '../json-render/server.js';
|
|
151
110
|
import {
|
|
152
|
-
WEBPAGE_NODE_DEFAULT_SIZE,
|
|
153
111
|
normalizeWebpageUrl,
|
|
154
112
|
} from './webpage-node.js';
|
|
155
113
|
import type { JsonRenderSpec } from '../json-render/server.js';
|
|
@@ -169,6 +127,52 @@ let primaryWorkbenchAutoOpenEnabled = true;
|
|
|
169
127
|
const initialCanvasThemeSetting = normalizeCanvasTheme(process.env.PMX_CANVAS_THEME);
|
|
170
128
|
let lastWorkbenchContextCardsEnvelope: Record<string, unknown> | null = null;
|
|
171
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
|
+
|
|
172
176
|
function normalizeGraphViewerSpec(
|
|
173
177
|
node: { type: string; data: Record<string, unknown> },
|
|
174
178
|
spec: JsonRenderSpec,
|
|
@@ -716,155 +720,6 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
716
720
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
717
721
|
}
|
|
718
722
|
|
|
719
|
-
function pickFiniteNumber(record: Record<string, unknown>, key: string): number | undefined {
|
|
720
|
-
const value = record[key];
|
|
721
|
-
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
function getRecord(value: unknown): Record<string, unknown> | undefined {
|
|
725
|
-
return isRecord(value) ? value : undefined;
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
function pickPositiveNumber(record: Record<string, unknown>, key: string): number | undefined {
|
|
729
|
-
const value = pickFiniteNumber(record, key);
|
|
730
|
-
return value !== undefined && value > 0 ? value : undefined;
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
function normalizeGeometryInput(body: Record<string, unknown>): {
|
|
734
|
-
x?: number;
|
|
735
|
-
y?: number;
|
|
736
|
-
width?: number;
|
|
737
|
-
height?: number;
|
|
738
|
-
position?: { x?: number; y?: number };
|
|
739
|
-
size?: { width?: number; height?: number };
|
|
740
|
-
} {
|
|
741
|
-
const position = getRecord(body.position);
|
|
742
|
-
const size = getRecord(body.size);
|
|
743
|
-
return {
|
|
744
|
-
...(pickFiniteNumber(body, 'x') !== undefined ? { x: pickFiniteNumber(body, 'x') } : {}),
|
|
745
|
-
...(pickFiniteNumber(body, 'y') !== undefined ? { y: pickFiniteNumber(body, 'y') } : {}),
|
|
746
|
-
...(pickFiniteNumber(body, 'width') !== undefined ? { width: pickFiniteNumber(body, 'width') } : {}),
|
|
747
|
-
...(pickFiniteNumber(body, 'height') !== undefined ? { height: pickFiniteNumber(body, 'height') } : {}),
|
|
748
|
-
...(position ? {
|
|
749
|
-
position: {
|
|
750
|
-
...(pickFiniteNumber(position, 'x') !== undefined ? { x: pickFiniteNumber(position, 'x') } : {}),
|
|
751
|
-
...(pickFiniteNumber(position, 'y') !== undefined ? { y: pickFiniteNumber(position, 'y') } : {}),
|
|
752
|
-
},
|
|
753
|
-
} : {}),
|
|
754
|
-
...(size ? {
|
|
755
|
-
size: {
|
|
756
|
-
...(pickFiniteNumber(size, 'width') !== undefined ? { width: pickFiniteNumber(size, 'width') } : {}),
|
|
757
|
-
...(pickFiniteNumber(size, 'height') !== undefined ? { height: pickFiniteNumber(size, 'height') } : {}),
|
|
758
|
-
},
|
|
759
|
-
} : {}),
|
|
760
|
-
};
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
function resolveCreateGeometry(body: Record<string, unknown>): {
|
|
764
|
-
x?: number;
|
|
765
|
-
y?: number;
|
|
766
|
-
width?: number;
|
|
767
|
-
height?: number;
|
|
768
|
-
} {
|
|
769
|
-
const geometry = normalizeGeometryInput(body);
|
|
770
|
-
const x = geometry.x ?? geometry.position?.x;
|
|
771
|
-
const y = geometry.y ?? geometry.position?.y;
|
|
772
|
-
const width = geometry.width ?? geometry.size?.width;
|
|
773
|
-
const height = geometry.height ?? geometry.size?.height;
|
|
774
|
-
return {
|
|
775
|
-
...(x !== undefined ? { x } : {}),
|
|
776
|
-
...(y !== undefined ? { y } : {}),
|
|
777
|
-
...(width !== undefined ? { width } : {}),
|
|
778
|
-
...(height !== undefined ? { height } : {}),
|
|
779
|
-
};
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
function resolvePatchGeometry(
|
|
783
|
-
body: Record<string, unknown>,
|
|
784
|
-
existing: CanvasNodeState,
|
|
785
|
-
): {
|
|
786
|
-
position?: { x: number; y: number };
|
|
787
|
-
size?: { width: number; height: number };
|
|
788
|
-
} {
|
|
789
|
-
const geometry = normalizeGeometryInput(body);
|
|
790
|
-
const x = geometry.x ?? geometry.position?.x;
|
|
791
|
-
const y = geometry.y ?? geometry.position?.y;
|
|
792
|
-
const width = geometry.width ?? geometry.size?.width;
|
|
793
|
-
const height = geometry.height ?? geometry.size?.height;
|
|
794
|
-
return {
|
|
795
|
-
...(x !== undefined || y !== undefined
|
|
796
|
-
? { position: { x: x ?? existing.position.x, y: y ?? existing.position.y } }
|
|
797
|
-
: {}),
|
|
798
|
-
...(width !== undefined || height !== undefined
|
|
799
|
-
? { size: { width: width ?? existing.size.width, height: height ?? existing.size.height } }
|
|
800
|
-
: {}),
|
|
801
|
-
};
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
function parseGraphPayloadData(value: unknown): Array<Record<string, unknown>> | null {
|
|
805
|
-
if (!Array.isArray(value)) return null;
|
|
806
|
-
if (value.some((item) => !isRecord(item))) return null;
|
|
807
|
-
return value as Array<Record<string, unknown>>;
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
type StringListField = { value?: string[]; error?: string };
|
|
811
|
-
|
|
812
|
-
function parseStringListField(field: string, value: unknown): StringListField {
|
|
813
|
-
if (value === undefined) return {};
|
|
814
|
-
if (!Array.isArray(value)) return { error: `"${field}" must be an array of node IDs.` };
|
|
815
|
-
const invalid = value.find((item) => typeof item !== 'string' || item.trim().length === 0);
|
|
816
|
-
if (invalid !== undefined) return { error: `"${field}" must contain only non-empty node IDs.` };
|
|
817
|
-
return { value };
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
function pickGroupChildIds(body: Record<string, unknown>): StringListField {
|
|
821
|
-
if ('children' in body) return parseStringListField('children', body.children);
|
|
822
|
-
if ('childIds' in body) return parseStringListField('childIds', body.childIds);
|
|
823
|
-
const data = isRecord(body.data) ? body.data : undefined;
|
|
824
|
-
return data && 'children' in data ? parseStringListField('data.children', data.children) : {};
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
function validateGroupChildIds(groupId: string, childIds: string[]): string | null {
|
|
828
|
-
const missingChildIds = childIds.filter((id) => !canvasState.getNode(id));
|
|
829
|
-
if (missingChildIds.length > 0) {
|
|
830
|
-
return `Missing child node ID${missingChildIds.length === 1 ? '' : 's'}: ${missingChildIds.join(', ')}.`;
|
|
831
|
-
}
|
|
832
|
-
const invalidChildIds = childIds.filter((id) => {
|
|
833
|
-
const node = canvasState.getNode(id);
|
|
834
|
-
return id === groupId || node?.type === 'group';
|
|
835
|
-
});
|
|
836
|
-
if (invalidChildIds.length > 0) {
|
|
837
|
-
return `Invalid group child ID${invalidChildIds.length === 1 ? '' : 's'}: ${invalidChildIds.join(', ')}.`;
|
|
838
|
-
}
|
|
839
|
-
return null;
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
function setGroupChildrenFromApi(groupId: string, childIds: string[]): boolean {
|
|
843
|
-
const group = canvasState.getNode(groupId);
|
|
844
|
-
if (!group || group.type !== 'group') return false;
|
|
845
|
-
|
|
846
|
-
const dataChildIds = Array.isArray(group.data.children)
|
|
847
|
-
? group.data.children.filter((id): id is string => typeof id === 'string')
|
|
848
|
-
: [];
|
|
849
|
-
const parentBackrefIds = canvasState.getLayout().nodes
|
|
850
|
-
.filter((node) => node.id !== groupId && node.data.parentGroup === groupId)
|
|
851
|
-
.map((node) => node.id);
|
|
852
|
-
const currentChildIds = [...new Set([...dataChildIds, ...parentBackrefIds])];
|
|
853
|
-
if (currentChildIds.length > 0) {
|
|
854
|
-
if (currentChildIds.length !== dataChildIds.length || currentChildIds.some((id) => !dataChildIds.includes(id))) {
|
|
855
|
-
canvasState.updateNode(groupId, { data: { ...group.data, children: currentChildIds } });
|
|
856
|
-
}
|
|
857
|
-
canvasState.ungroupNodes(groupId);
|
|
858
|
-
}
|
|
859
|
-
if (childIds.length === 0) return true;
|
|
860
|
-
|
|
861
|
-
const latestGroup = canvasState.getNode(groupId);
|
|
862
|
-
return canvasState.groupNodes(groupId, childIds, {
|
|
863
|
-
preservePositions: true,
|
|
864
|
-
keepGroupFrame: latestGroup?.data.frameMode === 'manual',
|
|
865
|
-
});
|
|
866
|
-
}
|
|
867
|
-
|
|
868
723
|
function getExtAppNodeCheckpointId(node: CanvasNodeState): string {
|
|
869
724
|
const appCheckpoint = isRecord(node.data.appCheckpoint) ? node.data.appCheckpoint : null;
|
|
870
725
|
const storedCheckpointId = appCheckpoint?.id;
|
|
@@ -1072,34 +927,6 @@ async function readJson(req: Request): Promise<Record<string, unknown>> {
|
|
|
1072
927
|
}
|
|
1073
928
|
}
|
|
1074
929
|
|
|
1075
|
-
/**
|
|
1076
|
-
* Like {@link readJson}, but PRESERVES a top-level JSON array. For endpoints that
|
|
1077
|
-
* accept either an object or a bare array (e.g. `/api/canvas/batch`, whose CLI
|
|
1078
|
-
* help and handler both document a bare `[...]` form). readJson coerces arrays to
|
|
1079
|
-
* `{}` so object-shaped handlers never crash on `body.field`; this variant keeps
|
|
1080
|
-
* the array so the handler's array branch can run. Empty/whitespace/malformed
|
|
1081
|
-
* bodies still resolve to `{}`.
|
|
1082
|
-
*/
|
|
1083
|
-
async function readJsonObjectOrArray(req: Request): Promise<Record<string, unknown> | unknown[]> {
|
|
1084
|
-
let text = '';
|
|
1085
|
-
try {
|
|
1086
|
-
text = await req.text();
|
|
1087
|
-
} catch (error) {
|
|
1088
|
-
logWorkbenchWarning('readJson', error);
|
|
1089
|
-
return {};
|
|
1090
|
-
}
|
|
1091
|
-
if (!text.trim()) return {};
|
|
1092
|
-
try {
|
|
1093
|
-
const value = JSON.parse(text) as unknown;
|
|
1094
|
-
if (Array.isArray(value)) return value;
|
|
1095
|
-
if (!value || typeof value !== 'object') return {};
|
|
1096
|
-
return value as Record<string, unknown>;
|
|
1097
|
-
} catch (error) {
|
|
1098
|
-
logWorkbenchWarning('readJson', error);
|
|
1099
|
-
return {};
|
|
1100
|
-
}
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
930
|
function htmlEscape(value: string): string {
|
|
1104
931
|
return value
|
|
1105
932
|
.replaceAll('&', '&')
|
|
@@ -1640,14 +1467,6 @@ async function handleCanvasAddAnnotation(req: Request): Promise<Response> {
|
|
|
1640
1467
|
return responseJson({ ok: true, annotation: summarizeCanvasAnnotation(annotation) });
|
|
1641
1468
|
}
|
|
1642
1469
|
|
|
1643
|
-
function handleCanvasRemoveAnnotation(id: string): Response {
|
|
1644
|
-
const decodedId = decodeURIComponent(id);
|
|
1645
|
-
const removed = canvasState.removeAnnotation(decodedId);
|
|
1646
|
-
if (!removed) return responseJson({ ok: false, error: `Annotation "${decodedId}" not found.` }, 404);
|
|
1647
|
-
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
1648
|
-
return responseJson({ ok: true, removed: decodedId });
|
|
1649
|
-
}
|
|
1650
|
-
|
|
1651
1470
|
// ── Serve image file for image nodes ─────────────────────────
|
|
1652
1471
|
async function handleCanvasImage(pathname: string): Promise<Response> {
|
|
1653
1472
|
const nodeId = pathname.replace('/api/canvas/image/', '');
|
|
@@ -1684,372 +1503,6 @@ async function handleCanvasImage(pathname: string): Promise<Response> {
|
|
|
1684
1503
|
});
|
|
1685
1504
|
}
|
|
1686
1505
|
|
|
1687
|
-
// ── Add node from client ─────────────────────────────────────
|
|
1688
|
-
const VALID_NODE_TYPES = new Set(['markdown', 'status', 'context', 'ledger', 'trace', 'file', 'image', 'mcp-app', 'webpage', 'html', 'group']);
|
|
1689
|
-
|
|
1690
|
-
function buildNodeResponse(node: CanvasNodeState): Record<string, unknown> {
|
|
1691
|
-
const serialized = serializeCanvasNode(node);
|
|
1692
|
-
return {
|
|
1693
|
-
ok: true,
|
|
1694
|
-
node: serialized,
|
|
1695
|
-
...serialized,
|
|
1696
|
-
// `nodeId` aliases `id` so HTTP/CLI node-create responses match the MCP
|
|
1697
|
-
// createdNodePayload — agents using either key (or a cached schema) work.
|
|
1698
|
-
nodeId: node.id,
|
|
1699
|
-
};
|
|
1700
|
-
}
|
|
1701
|
-
|
|
1702
|
-
function withContextPinReadState(node: CanvasNodeState): CanvasNodeState {
|
|
1703
|
-
return {
|
|
1704
|
-
...node,
|
|
1705
|
-
pinned: node.pinned || canvasState.contextPinnedNodeIds.has(node.id),
|
|
1706
|
-
};
|
|
1707
|
-
}
|
|
1708
|
-
|
|
1709
|
-
function withContextPinLayoutReadState(layout: CanvasLayout): CanvasLayout {
|
|
1710
|
-
return {
|
|
1711
|
-
...layout,
|
|
1712
|
-
nodes: layout.nodes.map(withContextPinReadState),
|
|
1713
|
-
};
|
|
1714
|
-
}
|
|
1715
|
-
|
|
1716
|
-
async function createCanvasWebpageNode(body: Record<string, unknown>): Promise<Response> {
|
|
1717
|
-
const rawUrl = typeof body.url === 'string' && body.url.trim().length > 0
|
|
1718
|
-
? body.url
|
|
1719
|
-
: typeof body.content === 'string'
|
|
1720
|
-
? body.content
|
|
1721
|
-
: '';
|
|
1722
|
-
|
|
1723
|
-
let normalizedUrl: string;
|
|
1724
|
-
try {
|
|
1725
|
-
normalizedUrl = normalizeWebpageUrl(rawUrl);
|
|
1726
|
-
} catch (error) {
|
|
1727
|
-
return responseJson({ ok: false, error: error instanceof Error ? error.message : 'Invalid webpage URL.' }, 400);
|
|
1728
|
-
}
|
|
1729
|
-
|
|
1730
|
-
const extraData = body.data && typeof body.data === 'object' && !Array.isArray(body.data)
|
|
1731
|
-
? body.data as Record<string, unknown>
|
|
1732
|
-
: undefined;
|
|
1733
|
-
const geometry = resolveCreateGeometry(body);
|
|
1734
|
-
const { id, node } = addCanvasNode({
|
|
1735
|
-
type: 'webpage',
|
|
1736
|
-
...(typeof body.title === 'string' ? { title: body.title } : {}),
|
|
1737
|
-
content: normalizedUrl,
|
|
1738
|
-
...(extraData ? { data: extraData } : {}),
|
|
1739
|
-
...(body.strictSize === true ? { strictSize: true } : {}),
|
|
1740
|
-
...geometry,
|
|
1741
|
-
...(geometry.width === undefined ? { width: WEBPAGE_NODE_DEFAULT_SIZE.width } : {}),
|
|
1742
|
-
...(geometry.height === undefined ? { height: WEBPAGE_NODE_DEFAULT_SIZE.height } : {}),
|
|
1743
|
-
});
|
|
1744
|
-
|
|
1745
|
-
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
1746
|
-
const refreshed = await refreshCanvasWebpageNode(id);
|
|
1747
|
-
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
1748
|
-
const created = canvasState.getNode(id) ?? node;
|
|
1749
|
-
return responseJson({
|
|
1750
|
-
...buildNodeResponse(created),
|
|
1751
|
-
fetch: refreshed.ok
|
|
1752
|
-
? { ok: true }
|
|
1753
|
-
: { ok: false, error: refreshed.error ?? 'Failed to fetch webpage content.' },
|
|
1754
|
-
...(refreshed.ok ? {} : { error: refreshed.error }),
|
|
1755
|
-
});
|
|
1756
|
-
}
|
|
1757
|
-
|
|
1758
|
-
async function handleCanvasAddNode(req: Request): Promise<Response> {
|
|
1759
|
-
const body = await readJson(req);
|
|
1760
|
-
const queryType = new URL(req.url).searchParams.get('type');
|
|
1761
|
-
const type = typeof body.type === 'string' ? body.type : queryType || 'markdown';
|
|
1762
|
-
|
|
1763
|
-
if (!VALID_NODE_TYPES.has(type)) {
|
|
1764
|
-
if (type === 'json-render') {
|
|
1765
|
-
return responseJson({
|
|
1766
|
-
ok: false,
|
|
1767
|
-
error: 'Node type "json-render" is created via POST /api/canvas/json-render. See /api/canvas/schema for the required spec shape.',
|
|
1768
|
-
}, 400);
|
|
1769
|
-
}
|
|
1770
|
-
if (type === 'graph') {
|
|
1771
|
-
return responseJson({
|
|
1772
|
-
ok: false,
|
|
1773
|
-
error: 'Node type "graph" is created via POST /api/canvas/graph. See /api/canvas/schema for graphType + data fields.',
|
|
1774
|
-
}, 400);
|
|
1775
|
-
}
|
|
1776
|
-
if (type === 'web-artifact') {
|
|
1777
|
-
return responseJson({
|
|
1778
|
-
ok: false,
|
|
1779
|
-
error: 'Node type "web-artifact" is created via POST /api/canvas/web-artifact with appTsx + title.',
|
|
1780
|
-
}, 400);
|
|
1781
|
-
}
|
|
1782
|
-
if (type === 'html-primitive') {
|
|
1783
|
-
return createCanvasHtmlPrimitiveNode(body);
|
|
1784
|
-
}
|
|
1785
|
-
return responseJson({ ok: false, error: `Invalid node type: "${type}".` }, 400);
|
|
1786
|
-
}
|
|
1787
|
-
|
|
1788
|
-
if (type === 'webpage') {
|
|
1789
|
-
return createCanvasWebpageNode(body);
|
|
1790
|
-
}
|
|
1791
|
-
|
|
1792
|
-
if (type === 'html' && (typeof body.primitive === 'string' || typeof body.kind === 'string')) {
|
|
1793
|
-
return createCanvasHtmlPrimitiveNode(body);
|
|
1794
|
-
}
|
|
1795
|
-
|
|
1796
|
-
if (type === 'group') {
|
|
1797
|
-
const geometry = resolveCreateGeometry(body);
|
|
1798
|
-
const childList = pickGroupChildIds(body);
|
|
1799
|
-
if (childList.error) return responseJson({ ok: false, error: `Cannot create group: ${childList.error}` }, 400);
|
|
1800
|
-
const childIds = childList.value ?? [];
|
|
1801
|
-
const childError = validateGroupChildIds('', childIds);
|
|
1802
|
-
if (childError) return responseJson({ ok: false, error: `Cannot create group: ${childError}` }, 400);
|
|
1803
|
-
const { node } = createCanvasGroup({
|
|
1804
|
-
...(typeof body.title === 'string' ? { title: body.title } : {}),
|
|
1805
|
-
childIds,
|
|
1806
|
-
...(typeof body.color === 'string' ? { color: body.color } : {}),
|
|
1807
|
-
...geometry,
|
|
1808
|
-
});
|
|
1809
|
-
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
1810
|
-
return responseJson(buildNodeResponse(node));
|
|
1811
|
-
}
|
|
1812
|
-
|
|
1813
|
-
const extraData = body.data && typeof body.data === 'object' && !Array.isArray(body.data)
|
|
1814
|
-
? body.data as Record<string, unknown>
|
|
1815
|
-
: undefined;
|
|
1816
|
-
if (type === 'html') {
|
|
1817
|
-
if ('html' in body && typeof body.html !== 'string') {
|
|
1818
|
-
return responseJson({ ok: false, error: 'HTML node field "html" must be a string.' }, 400);
|
|
1819
|
-
}
|
|
1820
|
-
if (extraData && 'html' in extraData && typeof extraData.html !== 'string') {
|
|
1821
|
-
return responseJson({ ok: false, error: 'HTML node field "data.html" must be a string.' }, 400);
|
|
1822
|
-
}
|
|
1823
|
-
}
|
|
1824
|
-
const content = type === 'image' && typeof body.path === 'string' && typeof body.content !== 'string'
|
|
1825
|
-
? body.path
|
|
1826
|
-
: body.content;
|
|
1827
|
-
// For html nodes, accept top-level `html` field and merge into data so callers
|
|
1828
|
-
// can POST { type: 'html', title, html } without nesting under `data`.
|
|
1829
|
-
const htmlMergedData = type === 'html'
|
|
1830
|
-
? {
|
|
1831
|
-
...(extraData ?? {}),
|
|
1832
|
-
...(typeof body.html === 'string' ? { html: resolveHtmlContent(body.html) } : {}),
|
|
1833
|
-
...(typeof body.summary === 'string' ? { summary: body.summary } : {}),
|
|
1834
|
-
...(typeof body.agentSummary === 'string' ? { agentSummary: body.agentSummary } : {}),
|
|
1835
|
-
...(typeof body.description === 'string' ? { description: body.description } : {}),
|
|
1836
|
-
...(body.presentation === true ? { presentation: true } : {}),
|
|
1837
|
-
...(Array.isArray(body.slideTitles) ? { slideTitles: body.slideTitles } : {}),
|
|
1838
|
-
...(Array.isArray(body.embeddedNodeIds) ? { embeddedNodeIds: body.embeddedNodeIds } : {}),
|
|
1839
|
-
...(Array.isArray(body.embeddedUrls) ? { embeddedUrls: body.embeddedUrls } : {}),
|
|
1840
|
-
}
|
|
1841
|
-
: extraData;
|
|
1842
|
-
let added: ReturnType<typeof addCanvasNode>;
|
|
1843
|
-
const geometry = resolveCreateGeometry(body);
|
|
1844
|
-
try {
|
|
1845
|
-
added = addCanvasNode({
|
|
1846
|
-
type: type as CanvasNodeState['type'],
|
|
1847
|
-
...(typeof body.title === 'string' ? { title: body.title } : {}),
|
|
1848
|
-
...(typeof content === 'string' ? { content } : {}),
|
|
1849
|
-
...(htmlMergedData && Object.keys(htmlMergedData).length > 0 ? { data: htmlMergedData } : {}),
|
|
1850
|
-
...(type === 'trace' && typeof body.toolName === 'string' ? { toolName: body.toolName } : {}),
|
|
1851
|
-
...(type === 'trace' && typeof body.category === 'string' ? { category: body.category } : {}),
|
|
1852
|
-
...(type === 'trace' && typeof body.status === 'string' ? { status: body.status } : {}),
|
|
1853
|
-
...(type === 'trace' && typeof body.duration === 'string' ? { duration: body.duration } : {}),
|
|
1854
|
-
...(type === 'trace' && typeof body.resultSummary === 'string' ? { resultSummary: body.resultSummary } : {}),
|
|
1855
|
-
...(type === 'trace' && typeof body.error === 'string' ? { error: body.error } : {}),
|
|
1856
|
-
...(body.strictSize === true ? { strictSize: true } : {}),
|
|
1857
|
-
...geometry,
|
|
1858
|
-
defaultWidth: type === 'html'
|
|
1859
|
-
? 720
|
|
1860
|
-
: type === 'markdown'
|
|
1861
|
-
? MARKDOWN_NODE_DEFAULT_SIZE.width
|
|
1862
|
-
: type === 'mcp-app'
|
|
1863
|
-
? MCP_APP_NODE_DEFAULT_SIZE.width
|
|
1864
|
-
: type === 'image'
|
|
1865
|
-
? IMAGE_NODE_DEFAULT_SIZE.width
|
|
1866
|
-
: type === 'ledger'
|
|
1867
|
-
? LEDGER_NODE_DEFAULT_SIZE.width
|
|
1868
|
-
: 360,
|
|
1869
|
-
defaultHeight: type === 'html'
|
|
1870
|
-
? 640
|
|
1871
|
-
: type === 'markdown'
|
|
1872
|
-
? MARKDOWN_NODE_DEFAULT_SIZE.height
|
|
1873
|
-
: type === 'mcp-app'
|
|
1874
|
-
? MCP_APP_NODE_DEFAULT_SIZE.height
|
|
1875
|
-
: type === 'image'
|
|
1876
|
-
? IMAGE_NODE_DEFAULT_SIZE.height
|
|
1877
|
-
: type === 'ledger'
|
|
1878
|
-
? LEDGER_NODE_DEFAULT_SIZE.height
|
|
1879
|
-
: 200,
|
|
1880
|
-
fileMode: 'auto',
|
|
1881
|
-
});
|
|
1882
|
-
} catch (error) {
|
|
1883
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1884
|
-
return responseJson({ ok: false, error: message }, 400);
|
|
1885
|
-
}
|
|
1886
|
-
const { node, needsCodeGraphRecompute } = added;
|
|
1887
|
-
|
|
1888
|
-
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
1889
|
-
if (needsCodeGraphRecompute) {
|
|
1890
|
-
scheduleCodeGraphRecompute(() => {
|
|
1891
|
-
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
1892
|
-
});
|
|
1893
|
-
}
|
|
1894
|
-
return responseJson(buildNodeResponse(node));
|
|
1895
|
-
}
|
|
1896
|
-
|
|
1897
|
-
function createCanvasHtmlPrimitiveNode(body: Record<string, unknown>): Response {
|
|
1898
|
-
const rawKind = typeof body.primitive === 'string' ? body.primitive : body.kind;
|
|
1899
|
-
if (typeof rawKind !== 'string' || !isHtmlPrimitiveKind(rawKind)) {
|
|
1900
|
-
return responseJson({ ok: false, error: `Unknown HTML primitive: ${String(rawKind)}.` }, 400);
|
|
1901
|
-
}
|
|
1902
|
-
const data = isRecord(body.data) ? body.data : {};
|
|
1903
|
-
let built: ReturnType<typeof buildHtmlPrimitive>;
|
|
1904
|
-
try {
|
|
1905
|
-
built = buildHtmlPrimitive({
|
|
1906
|
-
kind: rawKind,
|
|
1907
|
-
...(typeof body.title === 'string' ? { title: body.title } : {}),
|
|
1908
|
-
data,
|
|
1909
|
-
});
|
|
1910
|
-
} catch (error) {
|
|
1911
|
-
return responseJson({ ok: false, error: error instanceof Error ? error.message : String(error) }, 400);
|
|
1912
|
-
}
|
|
1913
|
-
const geometry = resolveCreateGeometry(body);
|
|
1914
|
-
const { node } = addCanvasNode({
|
|
1915
|
-
type: 'html',
|
|
1916
|
-
title: built.title,
|
|
1917
|
-
data: {
|
|
1918
|
-
html: built.html,
|
|
1919
|
-
htmlPrimitive: built.kind,
|
|
1920
|
-
primitiveData: built.data,
|
|
1921
|
-
description: built.summary,
|
|
1922
|
-
agentSummary: typeof data.agentSummary === 'string' ? data.agentSummary : built.summary,
|
|
1923
|
-
...(typeof data.summary === 'string' ? { summary: data.summary } : {}),
|
|
1924
|
-
...getHtmlPrimitiveSemanticMetadata(built.data),
|
|
1925
|
-
},
|
|
1926
|
-
...(body.strictSize === true ? { strictSize: true } : {}),
|
|
1927
|
-
...geometry,
|
|
1928
|
-
defaultWidth: built.defaultSize.width,
|
|
1929
|
-
defaultHeight: built.defaultSize.height,
|
|
1930
|
-
});
|
|
1931
|
-
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
1932
|
-
return responseJson({
|
|
1933
|
-
...buildNodeResponse(node),
|
|
1934
|
-
primitive: {
|
|
1935
|
-
kind: built.kind,
|
|
1936
|
-
title: built.title,
|
|
1937
|
-
htmlBytes: Buffer.byteLength(built.html, 'utf-8'),
|
|
1938
|
-
defaultSize: built.defaultSize,
|
|
1939
|
-
},
|
|
1940
|
-
});
|
|
1941
|
-
}
|
|
1942
|
-
|
|
1943
|
-
// ── Group operations ─────────────────────────────────────────
|
|
1944
|
-
async function handleCanvasCreateGroup(req: Request): Promise<Response> {
|
|
1945
|
-
const body = await readJson(req);
|
|
1946
|
-
const title = typeof body.title === 'string' ? body.title : 'Group';
|
|
1947
|
-
const childIds = Array.isArray(body.childIds) ? body.childIds.filter((id: unknown) => typeof id === 'string') : [];
|
|
1948
|
-
const color = typeof body.color === 'string' ? body.color : undefined;
|
|
1949
|
-
const x = typeof body.x === 'number' ? body.x : undefined;
|
|
1950
|
-
const y = typeof body.y === 'number' ? body.y : undefined;
|
|
1951
|
-
const width = typeof body.width === 'number' ? body.width : undefined;
|
|
1952
|
-
const height = typeof body.height === 'number' ? body.height : undefined;
|
|
1953
|
-
const childLayout =
|
|
1954
|
-
body.childLayout === 'grid' || body.childLayout === 'column' || body.childLayout === 'flow'
|
|
1955
|
-
? body.childLayout
|
|
1956
|
-
: undefined;
|
|
1957
|
-
if (childIds.length > 0) {
|
|
1958
|
-
const missingChildIds = childIds.filter((id) => !canvasState.getNode(id));
|
|
1959
|
-
if (missingChildIds.length > 0) {
|
|
1960
|
-
return responseJson({
|
|
1961
|
-
ok: false,
|
|
1962
|
-
error: `Cannot create group: missing child node ID${missingChildIds.length === 1 ? '' : 's'}: ${missingChildIds.join(', ')}.`,
|
|
1963
|
-
}, 400);
|
|
1964
|
-
}
|
|
1965
|
-
}
|
|
1966
|
-
|
|
1967
|
-
const { node } = createCanvasGroup({ title, childIds, color, x, y, width, height, ...(childLayout ? { childLayout } : {}) });
|
|
1968
|
-
|
|
1969
|
-
broadcastWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
1970
|
-
return responseJson(buildNodeResponse(node));
|
|
1971
|
-
}
|
|
1972
|
-
|
|
1973
|
-
async function handleCanvasGroupNodes(req: Request): Promise<Response> {
|
|
1974
|
-
const body = await readJson(req);
|
|
1975
|
-
const groupId = body.groupId as string;
|
|
1976
|
-
const childIds = Array.isArray(body.childIds) ? body.childIds.filter((id: unknown) => typeof id === 'string') : [];
|
|
1977
|
-
const childLayout =
|
|
1978
|
-
body.childLayout === 'grid' || body.childLayout === 'column' || body.childLayout === 'flow'
|
|
1979
|
-
? body.childLayout
|
|
1980
|
-
: undefined;
|
|
1981
|
-
if (!groupId || childIds.length === 0) {
|
|
1982
|
-
return responseJson({ ok: false, error: 'Missing groupId or childIds.' }, 400);
|
|
1983
|
-
}
|
|
1984
|
-
const { ok } = groupCanvasNodes(groupId, childIds, childLayout ? { childLayout } : {});
|
|
1985
|
-
if (!ok) return responseJson({ ok: false, error: 'Group not found or no valid children.' }, 400);
|
|
1986
|
-
broadcastWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
1987
|
-
return responseJson({ ok: true, groupId });
|
|
1988
|
-
}
|
|
1989
|
-
|
|
1990
|
-
async function handleCanvasUngroupNodes(req: Request): Promise<Response> {
|
|
1991
|
-
const body = await readJson(req);
|
|
1992
|
-
const groupId = body.groupId as string;
|
|
1993
|
-
if (!groupId) return responseJson({ ok: false, error: 'Missing groupId.' }, 400);
|
|
1994
|
-
const { ok } = ungroupCanvasNodes(groupId);
|
|
1995
|
-
if (!ok) return responseJson({ ok: false, error: 'Group not found or empty.' }, 400);
|
|
1996
|
-
broadcastWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
1997
|
-
return responseJson({ ok: true, groupId });
|
|
1998
|
-
}
|
|
1999
|
-
|
|
2000
|
-
const VALID_EDGE_TYPES = new Set(['relation', 'depends-on', 'flow', 'references']);
|
|
2001
|
-
const VALID_EDGE_STYLES = new Set(['solid', 'dashed', 'dotted']);
|
|
2002
|
-
|
|
2003
|
-
async function handleCanvasAddEdge(req: Request): Promise<Response> {
|
|
2004
|
-
const body = await readJson(req);
|
|
2005
|
-
const type = body.type as string;
|
|
2006
|
-
const style = typeof body.style === 'string' ? body.style : undefined;
|
|
2007
|
-
|
|
2008
|
-
if (
|
|
2009
|
-
!type ||
|
|
2010
|
-
(!body.from && !body.fromSearch) ||
|
|
2011
|
-
(!body.to && !body.toSearch)
|
|
2012
|
-
) {
|
|
2013
|
-
return responseJson({ ok: false, error: 'Missing required fields: type plus from/fromSearch and to/toSearch.' }, 400);
|
|
2014
|
-
}
|
|
2015
|
-
if (!VALID_EDGE_TYPES.has(type)) {
|
|
2016
|
-
return responseJson({ ok: false, error: `Invalid edge type: "${type}".` }, 400);
|
|
2017
|
-
}
|
|
2018
|
-
if (style && !VALID_EDGE_STYLES.has(style)) {
|
|
2019
|
-
return responseJson({ ok: false, error: `Invalid edge style: "${style}". Use solid, dashed, or dotted.` }, 400);
|
|
2020
|
-
}
|
|
2021
|
-
try {
|
|
2022
|
-
const result = addCanvasEdge({
|
|
2023
|
-
...(typeof body.from === 'string' ? { from: body.from } : {}),
|
|
2024
|
-
...(typeof body.to === 'string' ? { to: body.to } : {}),
|
|
2025
|
-
...(typeof body.fromSearch === 'string' ? { fromSearch: body.fromSearch } : {}),
|
|
2026
|
-
...(typeof body.toSearch === 'string' ? { toSearch: body.toSearch } : {}),
|
|
2027
|
-
type: type as CanvasEdge['type'],
|
|
2028
|
-
...(body.label ? { label: String(body.label) } : {}),
|
|
2029
|
-
...(style ? { style: style as CanvasEdge['style'] } : {}),
|
|
2030
|
-
...(body.animated !== undefined ? { animated: Boolean(body.animated) } : {}),
|
|
2031
|
-
});
|
|
2032
|
-
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
2033
|
-
return responseJson({ ok: true, ...result });
|
|
2034
|
-
} catch (error) {
|
|
2035
|
-
return responseJson({ ok: false, error: error instanceof Error ? error.message : 'Duplicate or self-edge.' }, 400);
|
|
2036
|
-
}
|
|
2037
|
-
}
|
|
2038
|
-
|
|
2039
|
-
async function handleCanvasRemoveEdge(req: Request): Promise<Response> {
|
|
2040
|
-
const body = await readJson(req);
|
|
2041
|
-
const edgeId = body.edge_id as string;
|
|
2042
|
-
if (!edgeId) {
|
|
2043
|
-
return responseJson({ ok: false, error: 'Missing edge_id.' }, 400);
|
|
2044
|
-
}
|
|
2045
|
-
const { removed } = removeCanvasEdge(edgeId);
|
|
2046
|
-
if (!removed) {
|
|
2047
|
-
return responseJson({ ok: false, error: `Edge "${edgeId}" not found.` }, 404);
|
|
2048
|
-
}
|
|
2049
|
-
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
2050
|
-
return responseJson({ ok: true, removed: edgeId });
|
|
2051
|
-
}
|
|
2052
|
-
|
|
2053
1506
|
async function handleCanvasRefreshWebpageNode(nodeId: string, req: Request): Promise<Response> {
|
|
2054
1507
|
const existing = canvasState.getNode(nodeId);
|
|
2055
1508
|
if (!existing || existing.type !== 'webpage') {
|
|
@@ -2072,504 +1525,32 @@ async function handleCanvasRefreshWebpageNode(nodeId: string, req: Request): Pro
|
|
|
2072
1525
|
return responseJson(result, result.ok ? 200 : 400);
|
|
2073
1526
|
}
|
|
2074
1527
|
|
|
2075
|
-
//
|
|
2076
|
-
|
|
2077
|
-
const existing = canvasState.getNode(nodeId);
|
|
2078
|
-
if (!existing) return responseJson({ ok: false, error: `Node "${nodeId}" not found.` }, 404);
|
|
2079
|
-
const body = await readJson(req);
|
|
2080
|
-
if (existing.type === 'webpage' && body.refresh === true) {
|
|
2081
|
-
return handleCanvasRefreshWebpageNode(nodeId, req);
|
|
2082
|
-
}
|
|
2083
|
-
const groupChildList = existing.type === 'group' ? pickGroupChildIds(body) : {};
|
|
2084
|
-
if (groupChildList.error) return responseJson({ ok: false, error: `Cannot update group: ${groupChildList.error}` }, 400);
|
|
2085
|
-
const groupChildIds = groupChildList.value;
|
|
2086
|
-
if (groupChildIds !== undefined) {
|
|
2087
|
-
const childError = validateGroupChildIds(nodeId, groupChildIds);
|
|
2088
|
-
if (childError) return responseJson({ ok: false, error: `Cannot update group: ${childError}` }, 400);
|
|
2089
|
-
}
|
|
2090
|
-
const patch: Record<string, unknown> = resolvePatchGeometry(body, existing);
|
|
2091
|
-
if (body.collapsed !== undefined) patch.collapsed = body.collapsed;
|
|
2092
|
-
if (body.pinned !== undefined) patch.pinned = Boolean(body.pinned);
|
|
2093
|
-
if (body.dockPosition === null || body.dockPosition === 'left' || body.dockPosition === 'right') {
|
|
2094
|
-
patch.dockPosition = body.dockPosition;
|
|
2095
|
-
}
|
|
2096
|
-
if (hasStructuredNodeUpdateFields(body)) {
|
|
2097
|
-
try {
|
|
2098
|
-
patch.data = buildStructuredNodeUpdate(existing, body).data;
|
|
2099
|
-
} catch (error) {
|
|
2100
|
-
return responseJson({ ok: false, error: error instanceof Error ? error.message : String(error) }, 400);
|
|
2101
|
-
}
|
|
2102
|
-
} else if (
|
|
2103
|
-
body.title !== undefined ||
|
|
2104
|
-
body.content !== undefined ||
|
|
2105
|
-
body.data ||
|
|
2106
|
-
typeof body.arrangeLocked === 'boolean' ||
|
|
2107
|
-
typeof body.strictSize === 'boolean' ||
|
|
2108
|
-
(existing.type === 'trace' && hasTraceNodeDataFields(body))
|
|
2109
|
-
) {
|
|
2110
|
-
const data = { ...existing.data };
|
|
2111
|
-
if (body.title !== undefined) {
|
|
2112
|
-
data.title = String(body.title);
|
|
2113
|
-
if (existing.type === 'webpage') {
|
|
2114
|
-
data.titleSource = 'user';
|
|
2115
|
-
}
|
|
2116
|
-
}
|
|
2117
|
-
if (body.content !== undefined) data.content = String(body.content);
|
|
2118
|
-
if (typeof body.arrangeLocked === 'boolean') data.arrangeLocked = body.arrangeLocked;
|
|
2119
|
-
if (typeof body.strictSize === 'boolean') data.strictSize = body.strictSize;
|
|
2120
|
-
// Merge extra data fields (for status, context, ledger, trace nodes)
|
|
2121
|
-
if (body.data && typeof body.data === 'object' && !Array.isArray(body.data)) {
|
|
2122
|
-
Object.assign(data, body.data as Record<string, unknown>);
|
|
2123
|
-
}
|
|
2124
|
-
if (existing.type === 'webpage') {
|
|
2125
|
-
const nextUrl = typeof body.url === 'string'
|
|
2126
|
-
? body.url
|
|
2127
|
-
: typeof (body.data as Record<string, unknown> | undefined)?.url === 'string'
|
|
2128
|
-
? (body.data as Record<string, unknown>).url as string
|
|
2129
|
-
: undefined;
|
|
2130
|
-
if (typeof nextUrl === 'string' && nextUrl.trim().length > 0) {
|
|
2131
|
-
try {
|
|
2132
|
-
data.url = normalizeWebpageUrl(nextUrl);
|
|
2133
|
-
} catch (error) {
|
|
2134
|
-
return responseJson({ ok: false, error: error instanceof Error ? error.message : 'Invalid webpage URL.' }, 400);
|
|
2135
|
-
}
|
|
2136
|
-
}
|
|
2137
|
-
}
|
|
2138
|
-
patch.data = existing.type === 'trace'
|
|
2139
|
-
? mergeTraceNodeDataFields(data, body)
|
|
2140
|
-
: data;
|
|
2141
|
-
}
|
|
2142
|
-
const error = validateCanvasNodePatch({
|
|
2143
|
-
...(patch.position ? { position: patch.position as { x: number; y: number } } : {}),
|
|
2144
|
-
...(patch.size ? { size: patch.size as { width: number; height: number } } : {}),
|
|
2145
|
-
});
|
|
2146
|
-
if (error) return responseJson({ ok: false, error }, 400);
|
|
2147
|
-
canvasState.updateNode(nodeId, patch as Partial<CanvasNodeState>);
|
|
2148
|
-
if (groupChildIds !== undefined && !setGroupChildrenFromApi(nodeId, groupChildIds)) {
|
|
2149
|
-
return responseJson({ ok: false, error: `Group "${nodeId}" not found.` }, 404);
|
|
2150
|
-
}
|
|
2151
|
-
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
2152
|
-
const updated = canvasState.getNode(nodeId);
|
|
2153
|
-
return responseJson(updated ? buildNodeResponse(updated) : { ok: true, id: nodeId });
|
|
2154
|
-
}
|
|
2155
|
-
|
|
2156
|
-
// ── Arrange nodes ───────────────────────────────────────────
|
|
2157
|
-
async function handleCanvasArrange(req: Request): Promise<Response> {
|
|
2158
|
-
const body = await readJson(req);
|
|
2159
|
-
const layout = typeof body.layout === 'string' ? body.layout : 'grid';
|
|
2160
|
-
if (!['grid', 'column', 'flow'].includes(layout)) {
|
|
2161
|
-
return responseJson({ ok: false, error: `Invalid layout: "${layout}". Use: grid, column, flow` }, 400);
|
|
2162
|
-
}
|
|
2163
|
-
const result = arrangeCanvasNodes(layout as 'grid' | 'column' | 'flow');
|
|
2164
|
-
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
2165
|
-
const validation = validateCanvasLayout(canvasState.getLayout());
|
|
2166
|
-
return responseJson({
|
|
2167
|
-
ok: validation.ok,
|
|
2168
|
-
arranged: result.arranged,
|
|
2169
|
-
layout: result.layout,
|
|
2170
|
-
...(validation.ok ? {} : { validation, collisions: validation.summary.collisions }),
|
|
2171
|
-
});
|
|
2172
|
-
}
|
|
1528
|
+
// handleCanvasBuildWebArtifact migrated to the operation registry
|
|
1529
|
+
// (plan-008 Wave 4): src/server/operations/ops/app.ts (webartifact.build).
|
|
2173
1530
|
|
|
2174
|
-
|
|
2175
|
-
async function handleCanvasFocus(req: Request): Promise<Response> {
|
|
1531
|
+
async function handleCanvasThemeUpdate(req: Request): Promise<Response> {
|
|
2176
1532
|
const body = await readJson(req);
|
|
2177
|
-
const
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
const noPan = body.noPan === true;
|
|
2182
|
-
if (!noPan) {
|
|
2183
|
-
canvasState.setViewport({ x: node.position.x - 100, y: node.position.y - 100 });
|
|
2184
|
-
} else {
|
|
2185
|
-
const maxZ = canvasState.getLayout().nodes.reduce((max, layoutNode) => Math.max(max, layoutNode.zIndex), 0);
|
|
2186
|
-
canvasState.updateNode(nodeId, { zIndex: maxZ + 1 });
|
|
2187
|
-
}
|
|
2188
|
-
const focus = canvasState.setAxFocus([nodeId], { source: 'api', recordHistory: false });
|
|
2189
|
-
broadcastWorkbenchEvent('ax-state-changed', {
|
|
2190
|
-
focus,
|
|
1533
|
+
const theme = normalizeCanvasTheme(body.theme, canvasState.theme);
|
|
1534
|
+
const next = canvasState.setTheme(theme);
|
|
1535
|
+
broadcastWorkbenchEvent('theme-changed', {
|
|
1536
|
+
theme: next,
|
|
2191
1537
|
sessionId: primaryWorkbenchSessionId,
|
|
2192
1538
|
timestamp: new Date().toISOString(),
|
|
2193
1539
|
});
|
|
2194
|
-
|
|
2195
|
-
if (!noPan) emitPrimaryWorkbenchEvent('canvas-viewport-update', { viewport: canvasState.viewport });
|
|
2196
|
-
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
2197
|
-
return responseJson({ ok: true, focused: nodeId, panned: !noPan, axFocus: focus });
|
|
2198
|
-
}
|
|
2199
|
-
|
|
2200
|
-
async function handleCanvasFit(req: Request): Promise<Response> {
|
|
2201
|
-
const body = await readJson(req);
|
|
2202
|
-
const nodeIds = Array.isArray(body.nodeIds)
|
|
2203
|
-
? body.nodeIds.filter((id): id is string => typeof id === 'string')
|
|
2204
|
-
: undefined;
|
|
2205
|
-
const result = fitCanvasView({
|
|
2206
|
-
...(typeof body.width === 'number' ? { width: body.width } : {}),
|
|
2207
|
-
...(typeof body.height === 'number' ? { height: body.height } : {}),
|
|
2208
|
-
...(typeof body.padding === 'number' ? { padding: body.padding } : {}),
|
|
2209
|
-
...(typeof body.maxScale === 'number' ? { maxScale: body.maxScale } : {}),
|
|
2210
|
-
...(nodeIds ? { nodeIds } : {}),
|
|
2211
|
-
});
|
|
2212
|
-
emitPrimaryWorkbenchEvent('canvas-viewport-update', { viewport: result.viewport });
|
|
2213
|
-
return responseJson(result);
|
|
1540
|
+
return responseJson({ ok: true, theme: next });
|
|
2214
1541
|
}
|
|
2215
1542
|
|
|
2216
|
-
async function
|
|
2217
|
-
const
|
|
2218
|
-
|
|
2219
|
-
const
|
|
2220
|
-
if (!
|
|
2221
|
-
return
|
|
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);
|
|
2222
1549
|
}
|
|
2223
1550
|
|
|
2224
|
-
const
|
|
2225
|
-
if (
|
|
2226
|
-
|
|
2227
|
-
if (typeof value === 'string') files[pathKey] = value;
|
|
2228
|
-
}
|
|
2229
|
-
}
|
|
2230
|
-
|
|
2231
|
-
try {
|
|
2232
|
-
const result = await buildWebArtifactOnCanvas({
|
|
2233
|
-
title,
|
|
2234
|
-
appTsx,
|
|
2235
|
-
...(typeof body.indexCss === 'string' ? { indexCss: body.indexCss } : {}),
|
|
2236
|
-
...(typeof body.mainTsx === 'string' ? { mainTsx: body.mainTsx } : {}),
|
|
2237
|
-
...(typeof body.indexHtml === 'string' ? { indexHtml: body.indexHtml } : {}),
|
|
2238
|
-
...(Object.keys(files).length > 0 ? { files } : {}),
|
|
2239
|
-
...(typeof body.projectPath === 'string'
|
|
2240
|
-
? { projectPath: resolveWorkspacePath(body.projectPath, activeWorkspaceRoot) }
|
|
2241
|
-
: {}),
|
|
2242
|
-
...(typeof body.outputPath === 'string'
|
|
2243
|
-
? { outputPath: resolveWorkspacePath(body.outputPath, activeWorkspaceRoot) }
|
|
2244
|
-
: {}),
|
|
2245
|
-
// Script-path overrides are honored only when contained inside the
|
|
2246
|
-
// workspace (enforced by resolveTrustedScriptPath in
|
|
2247
|
-
// executeWebArtifactBuild), so they cannot point at an arbitrary host
|
|
2248
|
-
// script for bash execution.
|
|
2249
|
-
...(typeof body.initScriptPath === 'string'
|
|
2250
|
-
? { initScriptPath: body.initScriptPath }
|
|
2251
|
-
: {}),
|
|
2252
|
-
...(typeof body.bundleScriptPath === 'string'
|
|
2253
|
-
? { bundleScriptPath: body.bundleScriptPath }
|
|
2254
|
-
: {}),
|
|
2255
|
-
...(Array.isArray(body.deps)
|
|
2256
|
-
? { deps: body.deps.filter((dep): dep is string => typeof dep === 'string') }
|
|
2257
|
-
: {}),
|
|
2258
|
-
...(typeof body.timeoutMs === 'number' ? { timeoutMs: body.timeoutMs } : {}),
|
|
2259
|
-
...(typeof body.openInCanvas === 'boolean' ? { openInCanvas: body.openInCanvas } : {}),
|
|
2260
|
-
});
|
|
2261
|
-
|
|
2262
|
-
return responseJson({
|
|
2263
|
-
ok: true,
|
|
2264
|
-
path: result.filePath,
|
|
2265
|
-
bytes: result.fileSize,
|
|
2266
|
-
projectPath: result.projectPath,
|
|
2267
|
-
openedInCanvas: result.openedInCanvas,
|
|
2268
|
-
startedAt: result.startedAt,
|
|
2269
|
-
completedAt: result.completedAt,
|
|
2270
|
-
durationMs: result.durationMs,
|
|
2271
|
-
timeoutMs: result.timeoutMs,
|
|
2272
|
-
// `id` is the canvas node id alias used by every other add-style
|
|
2273
|
-
// response. It is only present when a canvas node was actually
|
|
2274
|
-
// created (i.e. openInCanvas was not explicitly disabled). When
|
|
2275
|
-
// there is no canvas node, the alias is intentionally omitted so
|
|
2276
|
-
// consumers can `'id' in response` to detect the build-only case.
|
|
2277
|
-
...(typeof result.nodeId === 'string' ? { id: result.nodeId } : {}),
|
|
2278
|
-
nodeId: result.nodeId,
|
|
2279
|
-
url: result.url,
|
|
2280
|
-
metadata: result.metadata,
|
|
2281
|
-
logs: result.logs,
|
|
2282
|
-
...(body.includeLogs === true ? {
|
|
2283
|
-
stdout: result.stdout,
|
|
2284
|
-
stderr: result.stderr,
|
|
2285
|
-
} : {}),
|
|
2286
|
-
});
|
|
2287
|
-
} catch (error) {
|
|
2288
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2289
|
-
return responseJson({ ok: false, error: message }, 400);
|
|
2290
|
-
}
|
|
2291
|
-
}
|
|
2292
|
-
|
|
2293
|
-
function handleCanvasDescribeSchema(): Response {
|
|
2294
|
-
return responseJson(describeCanvasSchema());
|
|
2295
|
-
}
|
|
2296
|
-
|
|
2297
|
-
async function handleCanvasValidateSpec(req: Request): Promise<Response> {
|
|
2298
|
-
const body = await readJson(req);
|
|
2299
|
-
const rawType = typeof body.type === 'string' ? body.type.trim() : '';
|
|
2300
|
-
if (rawType !== 'json-render' && rawType !== 'graph' && rawType !== 'html-primitive') {
|
|
2301
|
-
return responseJson({ ok: false, error: 'Validation type must be "json-render", "graph", or "html-primitive".' }, 400);
|
|
2302
|
-
}
|
|
2303
|
-
|
|
2304
|
-
try {
|
|
2305
|
-
if (rawType === 'json-render') {
|
|
2306
|
-
const rawSpec =
|
|
2307
|
-
body.spec && typeof body.spec === 'object' && !Array.isArray(body.spec)
|
|
2308
|
-
? body.spec
|
|
2309
|
-
: body;
|
|
2310
|
-
return responseJson(validateStructuredCanvasPayload({
|
|
2311
|
-
type: 'json-render',
|
|
2312
|
-
spec: rawSpec,
|
|
2313
|
-
}));
|
|
2314
|
-
}
|
|
2315
|
-
|
|
2316
|
-
if (rawType === 'html-primitive') {
|
|
2317
|
-
const kind = typeof body.kind === 'string'
|
|
2318
|
-
? body.kind
|
|
2319
|
-
: typeof body.primitive === 'string'
|
|
2320
|
-
? body.primitive
|
|
2321
|
-
: '';
|
|
2322
|
-
const data = isRecord(body.data) ? body.data : {};
|
|
2323
|
-
return responseJson(validateStructuredCanvasPayload({
|
|
2324
|
-
type: 'html-primitive',
|
|
2325
|
-
primitive: {
|
|
2326
|
-
kind,
|
|
2327
|
-
...(typeof body.title === 'string' ? { title: body.title } : {}),
|
|
2328
|
-
data,
|
|
2329
|
-
},
|
|
2330
|
-
}));
|
|
2331
|
-
}
|
|
2332
|
-
|
|
2333
|
-
const data = parseGraphPayloadData(body.data);
|
|
2334
|
-
if (!data) {
|
|
2335
|
-
return responseJson({ ok: false, error: 'Graph validation requires a data array.' }, 400);
|
|
2336
|
-
}
|
|
2337
|
-
|
|
2338
|
-
const aggregate =
|
|
2339
|
-
body.aggregate === 'sum' || body.aggregate === 'count' || body.aggregate === 'avg'
|
|
2340
|
-
? body.aggregate
|
|
2341
|
-
: undefined;
|
|
2342
|
-
|
|
2343
|
-
return responseJson(validateStructuredCanvasPayload({
|
|
2344
|
-
type: 'graph',
|
|
2345
|
-
graph: {
|
|
2346
|
-
title: typeof body.title === 'string' && body.title.trim() ? body.title.trim() : 'Graph',
|
|
2347
|
-
graphType: typeof body.graphType === 'string'
|
|
2348
|
-
? body.graphType
|
|
2349
|
-
: typeof body.typeName === 'string'
|
|
2350
|
-
? body.typeName
|
|
2351
|
-
: 'line',
|
|
2352
|
-
data,
|
|
2353
|
-
...(typeof body.xKey === 'string' ? { xKey: body.xKey } : {}),
|
|
2354
|
-
...(typeof body.yKey === 'string' ? { yKey: body.yKey } : {}),
|
|
2355
|
-
...(typeof body.zKey === 'string' ? { zKey: body.zKey } : {}),
|
|
2356
|
-
...(typeof body.nameKey === 'string' ? { nameKey: body.nameKey } : {}),
|
|
2357
|
-
...(typeof body.valueKey === 'string' ? { valueKey: body.valueKey } : {}),
|
|
2358
|
-
...(typeof body.axisKey === 'string' ? { axisKey: body.axisKey } : {}),
|
|
2359
|
-
...(Array.isArray(body.metrics)
|
|
2360
|
-
? { metrics: body.metrics.filter((m: unknown): m is string => typeof m === 'string') }
|
|
2361
|
-
: {}),
|
|
2362
|
-
...(Array.isArray(body.series)
|
|
2363
|
-
? { series: body.series.filter((s: unknown): s is string => typeof s === 'string') }
|
|
2364
|
-
: {}),
|
|
2365
|
-
...(typeof body.barKey === 'string' ? { barKey: body.barKey } : {}),
|
|
2366
|
-
...(typeof body.lineKey === 'string' ? { lineKey: body.lineKey } : {}),
|
|
2367
|
-
...(aggregate ? { aggregate } : {}),
|
|
2368
|
-
...(typeof body.color === 'string' ? { color: body.color } : {}),
|
|
2369
|
-
...(typeof body.barColor === 'string' ? { barColor: body.barColor } : {}),
|
|
2370
|
-
...(typeof body.lineColor === 'string' ? { lineColor: body.lineColor } : {}),
|
|
2371
|
-
...(typeof body.height === 'number' ? { height: body.height } : {}),
|
|
2372
|
-
},
|
|
2373
|
-
}));
|
|
2374
|
-
} catch (error) {
|
|
2375
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2376
|
-
return responseJson({ ok: false, error: message, type: rawType }, 400);
|
|
2377
|
-
}
|
|
2378
|
-
}
|
|
2379
|
-
|
|
2380
|
-
async function handleCanvasAddJsonRender(req: Request): Promise<Response> {
|
|
2381
|
-
const body = await readJson(req);
|
|
2382
|
-
const title = typeof body.title === 'string' ? body.title.trim() : '';
|
|
2383
|
-
const rawSpec =
|
|
2384
|
-
body.spec && typeof body.spec === 'object' && !Array.isArray(body.spec) ? body.spec : body;
|
|
2385
|
-
const geometry = resolveCreateGeometry(body);
|
|
2386
|
-
|
|
2387
|
-
try {
|
|
2388
|
-
const result = createCanvasJsonRenderNode({
|
|
2389
|
-
...(title ? { title } : {}),
|
|
2390
|
-
spec: rawSpec,
|
|
2391
|
-
...(body.strictSize === true ? { strictSize: true } : {}),
|
|
2392
|
-
...geometry,
|
|
2393
|
-
});
|
|
2394
|
-
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
2395
|
-
return responseJson({ ...buildNodeResponse(result.node), url: result.url, spec: result.spec });
|
|
2396
|
-
} catch (error) {
|
|
2397
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2398
|
-
return responseJson({ ok: false, error: message }, 400);
|
|
2399
|
-
}
|
|
2400
|
-
}
|
|
2401
|
-
|
|
2402
|
-
async function handleJsonRenderStream(req: Request): Promise<Response> {
|
|
2403
|
-
const body = await readJson(req);
|
|
2404
|
-
const patches = Array.isArray(body.patches) ? body.patches : [];
|
|
2405
|
-
const done = body.done === true;
|
|
2406
|
-
const geometry = resolveCreateGeometry(body);
|
|
2407
|
-
try {
|
|
2408
|
-
let nodeId = typeof body.nodeId === 'string' && body.nodeId ? body.nodeId : undefined;
|
|
2409
|
-
let url = '';
|
|
2410
|
-
if (!nodeId) {
|
|
2411
|
-
const created = createCanvasStreamingJsonRenderNode({
|
|
2412
|
-
...(typeof body.title === 'string' ? { title: body.title } : {}),
|
|
2413
|
-
...(body.strictSize === true ? { strictSize: true } : {}),
|
|
2414
|
-
...geometry,
|
|
2415
|
-
});
|
|
2416
|
-
nodeId = created.id;
|
|
2417
|
-
url = created.url;
|
|
2418
|
-
}
|
|
2419
|
-
const result = appendCanvasJsonRenderStream(nodeId, patches, done);
|
|
2420
|
-
if (!result.ok) return responseJson({ ok: false, error: result.error }, 400);
|
|
2421
|
-
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
2422
|
-
const node = canvasState.getNode(nodeId);
|
|
2423
|
-
return responseJson({ id: nodeId, url: url || String(node?.data.url ?? ''), ...result });
|
|
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 handleCanvasAddGraph(req: Request): Promise<Response> {
|
|
2431
|
-
const body = await readJson(req);
|
|
2432
|
-
const title = typeof body.title === 'string' && body.title.trim() ? body.title.trim() : 'Graph';
|
|
2433
|
-
const graphType = typeof body.graphType === 'string' ? body.graphType : typeof body.type === 'string' ? body.type : 'line';
|
|
2434
|
-
const data = parseGraphPayloadData(body.data);
|
|
2435
|
-
if (!data) {
|
|
2436
|
-
return responseJson({ ok: false, error: 'Missing required field: data.' }, 400);
|
|
2437
|
-
}
|
|
2438
|
-
|
|
2439
|
-
try {
|
|
2440
|
-
const aggregate =
|
|
2441
|
-
body.aggregate === 'sum' || body.aggregate === 'count' || body.aggregate === 'avg'
|
|
2442
|
-
? body.aggregate
|
|
2443
|
-
: undefined;
|
|
2444
|
-
const metrics = Array.isArray(body.metrics)
|
|
2445
|
-
? body.metrics.filter((m: unknown): m is string => typeof m === 'string')
|
|
2446
|
-
: null;
|
|
2447
|
-
const series = Array.isArray(body.series)
|
|
2448
|
-
? body.series.filter((s: unknown): s is string => typeof s === 'string')
|
|
2449
|
-
: null;
|
|
2450
|
-
const position = getRecord(body.position);
|
|
2451
|
-
const size = getRecord(body.size);
|
|
2452
|
-
const x = pickFiniteNumber(body, 'x') ?? (position ? pickFiniteNumber(position, 'x') : undefined);
|
|
2453
|
-
const y = pickFiniteNumber(body, 'y') ?? (position ? pickFiniteNumber(position, 'y') : undefined);
|
|
2454
|
-
const width = pickPositiveNumber(body, 'width') ?? (size ? pickPositiveNumber(size, 'width') : undefined);
|
|
2455
|
-
// Node FRAME height. `body.height` is the CHART plot height (passed through as
|
|
2456
|
-
// `input.height` below), so the node frame accepts `nodeHeight` / `heightPx` /
|
|
2457
|
-
// `size.height` as aliases — `heightPx` matches createCanvasGraphNode's own input
|
|
2458
|
-
// field, the natural thing a caller reaches for. (With content-fit the node grows
|
|
2459
|
-
// to the chart anyway; this just removes the silent "height ignored" surprise.)
|
|
2460
|
-
const nodeHeight = pickPositiveNumber(body, 'nodeHeight')
|
|
2461
|
-
?? pickPositiveNumber(body, 'heightPx')
|
|
2462
|
-
?? (size ? pickPositiveNumber(size, 'height') : undefined);
|
|
2463
|
-
const showLegend = typeof body.showLegend === 'boolean' ? body.showLegend : undefined;
|
|
2464
|
-
const showLabels = typeof body.showLabels === 'boolean' ? body.showLabels : undefined;
|
|
2465
|
-
const colorBy =
|
|
2466
|
-
body.colorBy === 'series' || body.colorBy === 'category' || body.colorBy === 'value' || body.colorBy === 'none'
|
|
2467
|
-
? body.colorBy
|
|
2468
|
-
: undefined;
|
|
2469
|
-
const highlight =
|
|
2470
|
-
typeof body.highlight === 'number' || body.highlight === 'max' || body.highlight === 'min' || body.highlight === null
|
|
2471
|
-
? body.highlight
|
|
2472
|
-
: undefined;
|
|
2473
|
-
const sort =
|
|
2474
|
-
body.sort === 'asc' || body.sort === 'desc' || body.sort === 'none' ? body.sort : undefined;
|
|
2475
|
-
const result = createCanvasGraphNode({
|
|
2476
|
-
title,
|
|
2477
|
-
graphType,
|
|
2478
|
-
data,
|
|
2479
|
-
...(typeof body.xKey === 'string' ? { xKey: body.xKey } : {}),
|
|
2480
|
-
...(typeof body.yKey === 'string' ? { yKey: body.yKey } : {}),
|
|
2481
|
-
...(typeof body.zKey === 'string' ? { zKey: body.zKey } : {}),
|
|
2482
|
-
...(typeof body.nameKey === 'string' ? { nameKey: body.nameKey } : {}),
|
|
2483
|
-
...(typeof body.valueKey === 'string' ? { valueKey: body.valueKey } : {}),
|
|
2484
|
-
...(typeof body.axisKey === 'string' ? { axisKey: body.axisKey } : {}),
|
|
2485
|
-
...(metrics ? { metrics } : {}),
|
|
2486
|
-
...(series ? { series } : {}),
|
|
2487
|
-
...(typeof body.barKey === 'string' ? { barKey: body.barKey } : {}),
|
|
2488
|
-
...(typeof body.lineKey === 'string' ? { lineKey: body.lineKey } : {}),
|
|
2489
|
-
...(aggregate ? { aggregate } : {}),
|
|
2490
|
-
...(typeof body.color === 'string' ? { color: body.color } : {}),
|
|
2491
|
-
...(colorBy ? { colorBy } : {}),
|
|
2492
|
-
...(highlight !== undefined ? { highlight } : {}),
|
|
2493
|
-
...(typeof body.barColor === 'string' ? { barColor: body.barColor } : {}),
|
|
2494
|
-
...(typeof body.lineColor === 'string' ? { lineColor: body.lineColor } : {}),
|
|
2495
|
-
...(typeof body.labelKey === 'string' ? { labelKey: body.labelKey } : {}),
|
|
2496
|
-
...(typeof body.targetKey === 'string' ? { targetKey: body.targetKey } : {}),
|
|
2497
|
-
...(typeof body.rangesKey === 'string' ? { rangesKey: body.rangesKey } : {}),
|
|
2498
|
-
...(typeof body.beforeKey === 'string' ? { beforeKey: body.beforeKey } : {}),
|
|
2499
|
-
...(typeof body.afterKey === 'string' ? { afterKey: body.afterKey } : {}),
|
|
2500
|
-
...(typeof body.beforeLabel === 'string' ? { beforeLabel: body.beforeLabel } : {}),
|
|
2501
|
-
...(typeof body.afterLabel === 'string' ? { afterLabel: body.afterLabel } : {}),
|
|
2502
|
-
...(sort ? { sort } : {}),
|
|
2503
|
-
...(typeof body.fill === 'boolean' ? { fill: body.fill } : {}),
|
|
2504
|
-
...(typeof body.showEndDot === 'boolean' ? { showEndDot: body.showEndDot } : {}),
|
|
2505
|
-
...(typeof body.showMinMax === 'boolean' ? { showMinMax: body.showMinMax } : {}),
|
|
2506
|
-
...(typeof body.showValue === 'boolean' ? { showValue: body.showValue } : {}),
|
|
2507
|
-
...(typeof body.colorByDirection === 'boolean' ? { colorByDirection: body.colorByDirection } : {}),
|
|
2508
|
-
...(typeof body.height === 'number' ? { height: body.height } : {}),
|
|
2509
|
-
...(showLegend !== undefined ? { showLegend } : {}),
|
|
2510
|
-
...(showLabels !== undefined ? { showLabels } : {}),
|
|
2511
|
-
...(body.strictSize === true ? { strictSize: true } : {}),
|
|
2512
|
-
...(x !== undefined ? { x } : {}),
|
|
2513
|
-
...(y !== undefined ? { y } : {}),
|
|
2514
|
-
...(width !== undefined ? { width } : {}),
|
|
2515
|
-
...(nodeHeight !== undefined ? { heightPx: nodeHeight } : {}),
|
|
2516
|
-
});
|
|
2517
|
-
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
2518
|
-
return responseJson({ ...buildNodeResponse(result.node), url: result.url, spec: result.spec });
|
|
2519
|
-
} catch (error) {
|
|
2520
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2521
|
-
return responseJson({ ok: false, error: message }, 400);
|
|
2522
|
-
}
|
|
2523
|
-
}
|
|
2524
|
-
|
|
2525
|
-
async function handleCanvasBatch(req: Request): Promise<Response> {
|
|
2526
|
-
// Accept both documented shapes: { operations: [...] } and a bare [...] array.
|
|
2527
|
-
// Uses the array-preserving reader so the bare-array form isn't coerced to {}.
|
|
2528
|
-
const body = await readJsonObjectOrArray(req);
|
|
2529
|
-
const operations = Array.isArray(body)
|
|
2530
|
-
? body
|
|
2531
|
-
: Array.isArray(body.operations) ? body.operations : [];
|
|
2532
|
-
const normalized = operations
|
|
2533
|
-
.filter((operation): operation is Record<string, unknown> => operation && typeof operation === 'object' && !Array.isArray(operation))
|
|
2534
|
-
.map((operation) => ({
|
|
2535
|
-
op: String(operation.op ?? ''),
|
|
2536
|
-
...(typeof operation.assign === 'string' ? { assign: operation.assign } : {}),
|
|
2537
|
-
args: operation.args && typeof operation.args === 'object' && !Array.isArray(operation.args)
|
|
2538
|
-
? operation.args as Record<string, unknown>
|
|
2539
|
-
: {},
|
|
2540
|
-
}));
|
|
2541
|
-
const result = await executeCanvasBatch(normalized);
|
|
2542
|
-
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
2543
|
-
return responseJson(result, result.ok ? 200 : 400);
|
|
2544
|
-
}
|
|
2545
|
-
|
|
2546
|
-
function handleCanvasValidate(): Response {
|
|
2547
|
-
return responseJson(validateCanvasLayout(canvasState.getLayout()));
|
|
2548
|
-
}
|
|
2549
|
-
|
|
2550
|
-
async function handleCanvasThemeUpdate(req: Request): Promise<Response> {
|
|
2551
|
-
const body = await readJson(req);
|
|
2552
|
-
const theme = normalizeCanvasTheme(body.theme, canvasState.theme);
|
|
2553
|
-
const next = canvasState.setTheme(theme);
|
|
2554
|
-
broadcastWorkbenchEvent('theme-changed', {
|
|
2555
|
-
theme: next,
|
|
2556
|
-
sessionId: primaryWorkbenchSessionId,
|
|
2557
|
-
timestamp: new Date().toISOString(),
|
|
2558
|
-
});
|
|
2559
|
-
return responseJson({ ok: true, theme: next });
|
|
2560
|
-
}
|
|
2561
|
-
|
|
2562
|
-
async function handleJsonRenderView(url: URL): Promise<Response> {
|
|
2563
|
-
const nodeId = url.searchParams.get('nodeId') ?? '';
|
|
2564
|
-
if (!nodeId) return responseText('Missing nodeId', 400);
|
|
2565
|
-
const node = canvasState.getNode(nodeId);
|
|
2566
|
-
if (!node || (node.type !== 'json-render' && node.type !== 'graph')) {
|
|
2567
|
-
return responseText('json-render node not found', 404);
|
|
2568
|
-
}
|
|
2569
|
-
|
|
2570
|
-
const rawSpec = node.data.spec;
|
|
2571
|
-
if (!rawSpec || typeof rawSpec !== 'object') {
|
|
2572
|
-
return responseText('json-render spec missing', 404);
|
|
1551
|
+
const rawSpec = node.data.spec;
|
|
1552
|
+
if (!rawSpec || typeof rawSpec !== 'object') {
|
|
1553
|
+
return responseText('json-render spec missing', 404);
|
|
2573
1554
|
}
|
|
2574
1555
|
const spec = normalizeGraphViewerSpec(
|
|
2575
1556
|
{ type: node.type, data: node.data },
|
|
@@ -2632,13 +1613,6 @@ function responseText(text: string, status = 400): Response {
|
|
|
2632
1613
|
});
|
|
2633
1614
|
}
|
|
2634
1615
|
|
|
2635
|
-
function parsePositiveIntegerParam(value: string | null): number | undefined {
|
|
2636
|
-
if (!value) return undefined;
|
|
2637
|
-
const parsed = Number(value);
|
|
2638
|
-
if (!Number.isFinite(parsed) || parsed <= 0) return undefined;
|
|
2639
|
-
return Math.floor(parsed);
|
|
2640
|
-
}
|
|
2641
|
-
|
|
2642
1616
|
function handleArtifactView(url: URL): Response {
|
|
2643
1617
|
const pathLike = url.searchParams.get('path') ?? '';
|
|
2644
1618
|
const safePath = resolveWorkspaceArtifactPath(pathLike);
|
|
@@ -2827,229 +1801,6 @@ function handleRead(pathLike: string): Response {
|
|
|
2827
1801
|
});
|
|
2828
1802
|
}
|
|
2829
1803
|
|
|
2830
|
-
function randomExtAppToolCallId(): string {
|
|
2831
|
-
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
2832
|
-
}
|
|
2833
|
-
|
|
2834
|
-
function nodeAppSessionId(node: CanvasNodeState | undefined): string | null {
|
|
2835
|
-
if (!node || node.type !== 'mcp-app') return null;
|
|
2836
|
-
const sessionId = node.data.appSessionId;
|
|
2837
|
-
return typeof sessionId === 'string' && sessionId.trim().length > 0 ? sessionId : null;
|
|
2838
|
-
}
|
|
2839
|
-
|
|
2840
|
-
function closeNodeAppSession(node: CanvasNodeState | undefined): void {
|
|
2841
|
-
const sessionId = nodeAppSessionId(node);
|
|
2842
|
-
if (sessionId) closeMcpAppSession(sessionId);
|
|
2843
|
-
}
|
|
2844
|
-
|
|
2845
|
-
function normalizeStringRecord(value: unknown): Record<string, string> | undefined {
|
|
2846
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
|
|
2847
|
-
const entries = Object.entries(value)
|
|
2848
|
-
.filter((entry): entry is [string, string] => typeof entry[1] === 'string')
|
|
2849
|
-
.map(([key, text]) => [key, text.trim()] as const)
|
|
2850
|
-
.filter(([, text]) => text.length > 0);
|
|
2851
|
-
return entries.length > 0 ? Object.fromEntries(entries) : undefined;
|
|
2852
|
-
}
|
|
2853
|
-
|
|
2854
|
-
function parseExternalMcpTransportConfig(body: Record<string, unknown>): ExternalMcpTransportConfig | null {
|
|
2855
|
-
const transport = body.transport;
|
|
2856
|
-
if (!transport || typeof transport !== 'object' || Array.isArray(transport)) return null;
|
|
2857
|
-
const transportRecord = transport as Record<string, unknown>;
|
|
2858
|
-
|
|
2859
|
-
const type = typeof transportRecord.type === 'string' ? transportRecord.type : '';
|
|
2860
|
-
if (type === 'http') {
|
|
2861
|
-
const url = typeof transportRecord.url === 'string' ? transportRecord.url.trim() : '';
|
|
2862
|
-
if (!url) return null;
|
|
2863
|
-
const headers = normalizeStringRecord(transportRecord.headers);
|
|
2864
|
-
return {
|
|
2865
|
-
type: 'http',
|
|
2866
|
-
url,
|
|
2867
|
-
...(headers ? { headers } : {}),
|
|
2868
|
-
};
|
|
2869
|
-
}
|
|
2870
|
-
|
|
2871
|
-
if (type === 'stdio') {
|
|
2872
|
-
const command = typeof transportRecord.command === 'string' ? transportRecord.command.trim() : '';
|
|
2873
|
-
if (!command) return null;
|
|
2874
|
-
const env = normalizeStringRecord(transportRecord.env);
|
|
2875
|
-
return {
|
|
2876
|
-
type: 'stdio',
|
|
2877
|
-
command,
|
|
2878
|
-
...(Array.isArray(transportRecord.args)
|
|
2879
|
-
? { args: transportRecord.args.filter((value: unknown): value is string => typeof value === 'string') }
|
|
2880
|
-
: {}),
|
|
2881
|
-
...(typeof transportRecord.cwd === 'string' && transportRecord.cwd.trim().length > 0 ? { cwd: transportRecord.cwd } : {}),
|
|
2882
|
-
...(env ? { env } : {}),
|
|
2883
|
-
};
|
|
2884
|
-
}
|
|
2885
|
-
|
|
2886
|
-
return null;
|
|
2887
|
-
}
|
|
2888
|
-
|
|
2889
|
-
interface RunAndEmitOpenMcpAppParams {
|
|
2890
|
-
transport: ExternalMcpTransportConfig;
|
|
2891
|
-
toolName: string;
|
|
2892
|
-
toolArguments?: Record<string, unknown>;
|
|
2893
|
-
nodeId?: string;
|
|
2894
|
-
serverName?: string;
|
|
2895
|
-
title?: string;
|
|
2896
|
-
x?: number;
|
|
2897
|
-
y?: number;
|
|
2898
|
-
width?: number;
|
|
2899
|
-
height?: number;
|
|
2900
|
-
timeoutMs?: number;
|
|
2901
|
-
}
|
|
2902
|
-
|
|
2903
|
-
async function runAndEmitOpenMcpApp(params: RunAndEmitOpenMcpAppParams): Promise<Response> {
|
|
2904
|
-
try {
|
|
2905
|
-
const targetNode = params.nodeId ? canvasState.getNode(params.nodeId) : undefined;
|
|
2906
|
-
if (params.nodeId && !targetNode) {
|
|
2907
|
-
return responseJson({ ok: false, error: `Node "${params.nodeId}" not found.` }, 404);
|
|
2908
|
-
}
|
|
2909
|
-
if (targetNode && (targetNode.type !== 'mcp-app' || targetNode.data.mode !== 'ext-app')) {
|
|
2910
|
-
return responseJson({ ok: false, error: `Node "${params.nodeId}" is not an external app node.` }, 400);
|
|
2911
|
-
}
|
|
2912
|
-
|
|
2913
|
-
const opened = await openMcpApp({
|
|
2914
|
-
transport: params.transport,
|
|
2915
|
-
toolName: params.toolName,
|
|
2916
|
-
...(params.toolArguments ? { toolArguments: params.toolArguments } : {}),
|
|
2917
|
-
...(params.serverName ? { serverName: params.serverName } : {}),
|
|
2918
|
-
...(typeof params.timeoutMs === 'number' ? { timeoutMs: params.timeoutMs } : {}),
|
|
2919
|
-
});
|
|
2920
|
-
|
|
2921
|
-
const toolCallId = randomExtAppToolCallId();
|
|
2922
|
-
if (params.nodeId) closeNodeAppSession(targetNode);
|
|
2923
|
-
const nodeIdSeed = params.nodeId ?? (toolCallId.startsWith('ext-app-') ? toolCallId : `ext-app-${toolCallId}`);
|
|
2924
|
-
const toolResult = isExcalidrawCreateView(opened.serverName, opened.toolName)
|
|
2925
|
-
? ensureExcalidrawCheckpointId(opened.toolResult, nodeIdSeed)
|
|
2926
|
-
: opened.toolResult;
|
|
2927
|
-
const nodeTitle = params.title
|
|
2928
|
-
?? (typeof targetNode?.data.title === 'string' ? targetNode.data.title : undefined)
|
|
2929
|
-
?? opened.tool.title
|
|
2930
|
-
?? opened.tool.name;
|
|
2931
|
-
|
|
2932
|
-
emitPrimaryWorkbenchEvent('ext-app-open', {
|
|
2933
|
-
toolCallId,
|
|
2934
|
-
nodeId: nodeIdSeed,
|
|
2935
|
-
title: nodeTitle,
|
|
2936
|
-
html: opened.html,
|
|
2937
|
-
toolInput: opened.toolInput,
|
|
2938
|
-
serverName: opened.serverName,
|
|
2939
|
-
toolName: opened.toolName,
|
|
2940
|
-
appSessionId: opened.sessionId,
|
|
2941
|
-
transportConfig: params.transport,
|
|
2942
|
-
resourceUri: opened.resourceUri,
|
|
2943
|
-
toolDefinition: opened.tool,
|
|
2944
|
-
sessionStatus: 'ready',
|
|
2945
|
-
sessionError: null,
|
|
2946
|
-
...(opened.resourceMeta ? { resourceMeta: opened.resourceMeta } : {}),
|
|
2947
|
-
...(typeof params.x === 'number' ? { x: params.x } : {}),
|
|
2948
|
-
...(typeof params.y === 'number' ? { y: params.y } : {}),
|
|
2949
|
-
...(typeof params.width === 'number' ? { width: params.width } : {}),
|
|
2950
|
-
...(typeof params.height === 'number' ? { height: params.height } : {}),
|
|
2951
|
-
});
|
|
2952
|
-
emitPrimaryWorkbenchEvent('ext-app-result', {
|
|
2953
|
-
toolCallId,
|
|
2954
|
-
nodeId: nodeIdSeed,
|
|
2955
|
-
serverName: opened.serverName,
|
|
2956
|
-
toolName: opened.toolName,
|
|
2957
|
-
success: toolResult.isError !== true,
|
|
2958
|
-
result: toolResult,
|
|
2959
|
-
});
|
|
2960
|
-
const nodeId = params.nodeId ?? findCanvasExtAppNodeId(toolCallId);
|
|
2961
|
-
|
|
2962
|
-
return responseJson({
|
|
2963
|
-
ok: true,
|
|
2964
|
-
...(nodeId ? { id: nodeId } : {}),
|
|
2965
|
-
nodeId,
|
|
2966
|
-
toolCallId,
|
|
2967
|
-
sessionId: opened.sessionId,
|
|
2968
|
-
resourceUri: opened.resourceUri,
|
|
2969
|
-
serverName: opened.serverName,
|
|
2970
|
-
toolName: opened.toolName,
|
|
2971
|
-
});
|
|
2972
|
-
} catch (error) {
|
|
2973
|
-
return responseJson({
|
|
2974
|
-
ok: false,
|
|
2975
|
-
error: error instanceof Error ? error.message : String(error),
|
|
2976
|
-
}, 400);
|
|
2977
|
-
}
|
|
2978
|
-
}
|
|
2979
|
-
|
|
2980
|
-
async function handleCanvasOpenMcpApp(req: Request): Promise<Response> {
|
|
2981
|
-
const body = await readJson(req);
|
|
2982
|
-
const transport = parseExternalMcpTransportConfig(body);
|
|
2983
|
-
const toolName = typeof body.toolName === 'string' ? body.toolName.trim() : '';
|
|
2984
|
-
if (!transport || !toolName) {
|
|
2985
|
-
return responseJson({ ok: false, error: 'Missing valid transport or toolName.' }, 400);
|
|
2986
|
-
}
|
|
2987
|
-
|
|
2988
|
-
const toolArguments =
|
|
2989
|
-
body.toolArguments && typeof body.toolArguments === 'object' && !Array.isArray(body.toolArguments)
|
|
2990
|
-
? body.toolArguments as Record<string, unknown>
|
|
2991
|
-
: undefined;
|
|
2992
|
-
|
|
2993
|
-
const requestedTitle = typeof body.title === 'string' && body.title.trim().length > 0
|
|
2994
|
-
? body.title.trim()
|
|
2995
|
-
: undefined;
|
|
2996
|
-
const requestedServerName = typeof body.serverName === 'string' && body.serverName.trim().length > 0
|
|
2997
|
-
? body.serverName.trim()
|
|
2998
|
-
: undefined;
|
|
2999
|
-
const requestedNodeId = typeof body.nodeId === 'string' && body.nodeId.trim().length > 0
|
|
3000
|
-
? body.nodeId.trim()
|
|
3001
|
-
: undefined;
|
|
3002
|
-
|
|
3003
|
-
return runAndEmitOpenMcpApp({
|
|
3004
|
-
transport,
|
|
3005
|
-
toolName,
|
|
3006
|
-
...(toolArguments ? { toolArguments } : {}),
|
|
3007
|
-
...(requestedNodeId ? { nodeId: requestedNodeId } : {}),
|
|
3008
|
-
...(requestedServerName ? { serverName: requestedServerName } : {}),
|
|
3009
|
-
...(requestedTitle ? { title: requestedTitle } : {}),
|
|
3010
|
-
...(typeof body.x === 'number' ? { x: body.x } : {}),
|
|
3011
|
-
...(typeof body.y === 'number' ? { y: body.y } : {}),
|
|
3012
|
-
...(typeof body.width === 'number' ? { width: body.width } : {}),
|
|
3013
|
-
...(typeof body.height === 'number' ? { height: body.height } : {}),
|
|
3014
|
-
...(typeof body.timeoutMs === 'number' ? { timeoutMs: body.timeoutMs } : {}),
|
|
3015
|
-
});
|
|
3016
|
-
}
|
|
3017
|
-
|
|
3018
|
-
async function handleCanvasAddDiagram(req: Request): Promise<Response> {
|
|
3019
|
-
const body = await readJson(req);
|
|
3020
|
-
let built;
|
|
3021
|
-
try {
|
|
3022
|
-
built = buildExcalidrawOpenMcpAppInput({
|
|
3023
|
-
elements: body.elements,
|
|
3024
|
-
...(typeof body.nodeId === 'string' ? { nodeId: body.nodeId } : {}),
|
|
3025
|
-
...(typeof body.title === 'string' ? { title: body.title } : {}),
|
|
3026
|
-
...(typeof body.x === 'number' ? { x: body.x } : {}),
|
|
3027
|
-
...(typeof body.y === 'number' ? { y: body.y } : {}),
|
|
3028
|
-
...(typeof body.width === 'number' ? { width: body.width } : {}),
|
|
3029
|
-
...(typeof body.height === 'number' ? { height: body.height } : {}),
|
|
3030
|
-
...(typeof body.timeoutMs === 'number' ? { timeoutMs: body.timeoutMs } : {}),
|
|
3031
|
-
});
|
|
3032
|
-
} catch (error) {
|
|
3033
|
-
return responseJson({
|
|
3034
|
-
ok: false,
|
|
3035
|
-
error: error instanceof Error ? error.message : String(error),
|
|
3036
|
-
}, 400);
|
|
3037
|
-
}
|
|
3038
|
-
return runAndEmitOpenMcpApp({
|
|
3039
|
-
transport: built.transport,
|
|
3040
|
-
toolName: built.toolName,
|
|
3041
|
-
toolArguments: built.toolArguments,
|
|
3042
|
-
serverName: built.serverName,
|
|
3043
|
-
...(built.nodeId ? { nodeId: built.nodeId } : {}),
|
|
3044
|
-
...(built.title ? { title: built.title } : {}),
|
|
3045
|
-
...(typeof built.x === 'number' ? { x: built.x } : {}),
|
|
3046
|
-
...(typeof built.y === 'number' ? { y: built.y } : {}),
|
|
3047
|
-
...(typeof built.width === 'number' ? { width: built.width } : {}),
|
|
3048
|
-
...(typeof built.height === 'number' ? { height: built.height } : {}),
|
|
3049
|
-
...(typeof built.timeoutMs === 'number' ? { timeoutMs: built.timeoutMs } : {}),
|
|
3050
|
-
});
|
|
3051
|
-
}
|
|
3052
|
-
|
|
3053
1804
|
async function handleExtAppCallTool(req: Request): Promise<Response> {
|
|
3054
1805
|
const body = await readJson(req);
|
|
3055
1806
|
const sessionId = typeof body.sessionId === 'string' ? body.sessionId.trim() : '';
|
|
@@ -3251,120 +2002,15 @@ function handleWorkbenchState(): Response {
|
|
|
3251
2002
|
});
|
|
3252
2003
|
}
|
|
3253
2004
|
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
backendValue === 'chrome' || backendValue === 'webkit'
|
|
3260
|
-
? backendValue
|
|
3261
|
-
: undefined;
|
|
3262
|
-
|
|
3263
|
-
const width = typeof body.width === 'number' ? body.width : undefined;
|
|
3264
|
-
const height = typeof body.height === 'number' ? body.height : undefined;
|
|
3265
|
-
const chromePath = typeof body.chromePath === 'string' ? body.chromePath : undefined;
|
|
3266
|
-
const dataStoreDir = typeof body.dataStoreDir === 'string' ? body.dataStoreDir : undefined;
|
|
3267
|
-
const chromeArgv = Array.isArray(body.chromeArgv)
|
|
3268
|
-
? body.chromeArgv.filter((value): value is string => typeof value === 'string')
|
|
3269
|
-
: undefined;
|
|
3270
|
-
|
|
3271
|
-
return {
|
|
3272
|
-
...(backend ? { backend } : {}),
|
|
3273
|
-
...(width !== undefined ? { width } : {}),
|
|
3274
|
-
...(height !== undefined ? { height } : {}),
|
|
3275
|
-
...(chromePath ? { chromePath } : {}),
|
|
3276
|
-
...(chromeArgv ? { chromeArgv } : {}),
|
|
3277
|
-
...(dataStoreDir ? { dataStoreDir } : {}),
|
|
3278
|
-
};
|
|
3279
|
-
}
|
|
3280
|
-
|
|
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).
|
|
3281
2010
|
function currentWorkbenchUrl(): string | null {
|
|
3282
2011
|
return server && typeof server.port === 'number' ? `${loopbackBaseUrl(server.port)}/workbench` : null;
|
|
3283
2012
|
}
|
|
3284
2013
|
|
|
3285
|
-
function handleWorkbenchWebViewStatus(): Response {
|
|
3286
|
-
return responseJson(getCanvasAutomationWebViewStatus());
|
|
3287
|
-
}
|
|
3288
|
-
|
|
3289
|
-
async function handleWorkbenchWebViewStart(req: Request): Promise<Response> {
|
|
3290
|
-
const url = currentWorkbenchUrl();
|
|
3291
|
-
if (!url) {
|
|
3292
|
-
return responseJson({ ok: false, error: 'Canvas server is not running.' }, 503);
|
|
3293
|
-
}
|
|
3294
|
-
|
|
3295
|
-
const body = await readJson(req);
|
|
3296
|
-
const options = parseCanvasAutomationWebViewRequestBody(body);
|
|
3297
|
-
|
|
3298
|
-
try {
|
|
3299
|
-
const webview = await startCanvasAutomationWebView(url, options);
|
|
3300
|
-
return responseJson({ ok: true, webview });
|
|
3301
|
-
} catch (error) {
|
|
3302
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
3303
|
-
const status = hasCanvasAutomationWebViewSupport() ? 500 : 501;
|
|
3304
|
-
return responseJson({
|
|
3305
|
-
ok: false,
|
|
3306
|
-
error: message,
|
|
3307
|
-
webview: getCanvasAutomationWebViewStatus(),
|
|
3308
|
-
}, status);
|
|
3309
|
-
}
|
|
3310
|
-
}
|
|
3311
|
-
|
|
3312
|
-
async function handleWorkbenchWebViewStop(): Promise<Response> {
|
|
3313
|
-
try {
|
|
3314
|
-
const stopped = await stopCanvasAutomationWebView();
|
|
3315
|
-
return responseJson({
|
|
3316
|
-
ok: true,
|
|
3317
|
-
stopped,
|
|
3318
|
-
webview: getCanvasAutomationWebViewStatus(),
|
|
3319
|
-
});
|
|
3320
|
-
} catch (error) {
|
|
3321
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
3322
|
-
return responseJson({
|
|
3323
|
-
ok: false,
|
|
3324
|
-
error: message,
|
|
3325
|
-
webview: getCanvasAutomationWebViewStatus(),
|
|
3326
|
-
}, 500);
|
|
3327
|
-
}
|
|
3328
|
-
}
|
|
3329
|
-
|
|
3330
|
-
async function handleWorkbenchWebViewEvaluate(req: Request): Promise<Response> {
|
|
3331
|
-
const body = await readJson(req);
|
|
3332
|
-
const expression = typeof body.expression === 'string' ? body.expression.trim() : '';
|
|
3333
|
-
const script = typeof body.script === 'string' ? body.script.trim() : '';
|
|
3334
|
-
if ((expression ? 1 : 0) + (script ? 1 : 0) !== 1) {
|
|
3335
|
-
return responseJson({
|
|
3336
|
-
ok: false,
|
|
3337
|
-
error: 'Pass exactly one of "expression" (single JS expression) or "script" (multi-statement body, wrapped in an async IIFE).',
|
|
3338
|
-
}, 400);
|
|
3339
|
-
}
|
|
3340
|
-
const source = script ? wrapCanvasAutomationScript(script) : expression;
|
|
3341
|
-
|
|
3342
|
-
try {
|
|
3343
|
-
const value = await evaluateCanvasAutomationWebView(source);
|
|
3344
|
-
return responseJson({ ok: true, value });
|
|
3345
|
-
} catch (error) {
|
|
3346
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
3347
|
-
return responseJson({ ok: false, error: message, webview: getCanvasAutomationWebViewStatus() }, 400);
|
|
3348
|
-
}
|
|
3349
|
-
}
|
|
3350
|
-
|
|
3351
|
-
async function handleWorkbenchWebViewResize(req: Request): Promise<Response> {
|
|
3352
|
-
const body = await readJson(req);
|
|
3353
|
-
const width = typeof body.width === 'number' ? body.width : NaN;
|
|
3354
|
-
const height = typeof body.height === 'number' ? body.height : NaN;
|
|
3355
|
-
if (!Number.isFinite(width) || width <= 0 || !Number.isFinite(height) || height <= 0) {
|
|
3356
|
-
return responseJson({ ok: false, error: 'Missing required positive numeric fields: width, height.' }, 400);
|
|
3357
|
-
}
|
|
3358
|
-
|
|
3359
|
-
try {
|
|
3360
|
-
const webview = await resizeCanvasAutomationWebView(width, height);
|
|
3361
|
-
return responseJson({ ok: true, webview });
|
|
3362
|
-
} catch (error) {
|
|
3363
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
3364
|
-
return responseJson({ ok: false, error: message, webview: getCanvasAutomationWebViewStatus() }, 400);
|
|
3365
|
-
}
|
|
3366
|
-
}
|
|
3367
|
-
|
|
3368
2014
|
async function handleWorkbenchWebViewScreenshot(req: Request): Promise<Response> {
|
|
3369
2015
|
const body = await readJson(req);
|
|
3370
2016
|
const format = body.format === 'jpeg' || body.format === 'webp' || body.format === 'png'
|
|
@@ -3838,43 +2484,6 @@ async function handleCanvasPrompt(req: Request): Promise<Response> {
|
|
|
3838
2484
|
return responseJson({ ok: true, nodeId });
|
|
3839
2485
|
}
|
|
3840
2486
|
|
|
3841
|
-
async function handleSnapshotSave(req: Request): Promise<Response> {
|
|
3842
|
-
const body = await readJson(req);
|
|
3843
|
-
const name = typeof body.name === 'string' ? body.name.trim() : '';
|
|
3844
|
-
if (!name) return responseText('Missing snapshot name', 400);
|
|
3845
|
-
const snapshot = saveCanvasSnapshot(name);
|
|
3846
|
-
if (!snapshot) return responseText('Failed to save snapshot', 500);
|
|
3847
|
-
return responseJson({ ok: true, id: snapshot.id, snapshot });
|
|
3848
|
-
}
|
|
3849
|
-
|
|
3850
|
-
async function handleSnapshotGc(req: Request): Promise<Response> {
|
|
3851
|
-
const body = await readJson(req);
|
|
3852
|
-
const keepValue = body.keep;
|
|
3853
|
-
const keep = typeof keepValue === 'number'
|
|
3854
|
-
? keepValue
|
|
3855
|
-
: typeof keepValue === 'string'
|
|
3856
|
-
? Number(keepValue)
|
|
3857
|
-
: undefined;
|
|
3858
|
-
const dryRun = body.dryRun === true || body['dry-run'] === true;
|
|
3859
|
-
return responseJson(gcCanvasSnapshots({ keep, dryRun }));
|
|
3860
|
-
}
|
|
3861
|
-
|
|
3862
|
-
async function handleContextPinsUpdate(req: Request): Promise<Response> {
|
|
3863
|
-
const body = await readJson(req);
|
|
3864
|
-
const MAX_PINS = 20;
|
|
3865
|
-
const nodeIds = Array.isArray(body.nodeIds)
|
|
3866
|
-
? (body.nodeIds.filter((id: unknown) => typeof id === 'string') as string[]).slice(0, MAX_PINS)
|
|
3867
|
-
: [];
|
|
3868
|
-
const result = setCanvasContextPins(nodeIds, 'set');
|
|
3869
|
-
broadcastWorkbenchEvent('context-pins-changed', {
|
|
3870
|
-
count: result.count,
|
|
3871
|
-
nodeIds: result.nodeIds,
|
|
3872
|
-
sessionId: primaryWorkbenchSessionId,
|
|
3873
|
-
timestamp: new Date().toISOString(),
|
|
3874
|
-
});
|
|
3875
|
-
return responseJson({ ok: true, count: result.count });
|
|
3876
|
-
}
|
|
3877
|
-
|
|
3878
2487
|
function handleGetPinnedContext(): Response {
|
|
3879
2488
|
const pinnedIds = Array.from(canvasState.contextPinnedNodeIds);
|
|
3880
2489
|
const preamble = pinnedIds.length > 0 ? buildSelectionContextPreamble(pinnedIds) : '';
|
|
@@ -3908,14 +2517,95 @@ function normalizeAxSource(value: unknown, fallback: PmxAxSource): PmxAxSource {
|
|
|
3908
2517
|
: fallback;
|
|
3909
2518
|
}
|
|
3910
2519
|
|
|
3911
|
-
function
|
|
3912
|
-
|
|
2520
|
+
function handleGetAxContext(url: URL): Response {
|
|
2521
|
+
// Optional ?consumer= filters the compact `delivery` lead block (loop-safe — a
|
|
2522
|
+
// consumer never sees steering/activity it originated), so a host adapter can
|
|
2523
|
+
// inject its own un-truncated pending block per turn (report #54 hardening).
|
|
2524
|
+
const consumer = url.searchParams.get('consumer') ?? undefined;
|
|
2525
|
+
return responseJson(buildCanvasAxContext(consumer));
|
|
3913
2526
|
}
|
|
3914
2527
|
|
|
3915
|
-
function
|
|
3916
|
-
return
|
|
2528
|
+
function isReviewSeverity(v: unknown): v is PmxAxReviewSeverity {
|
|
2529
|
+
return v === 'info' || v === 'warning' || v === 'error';
|
|
2530
|
+
}
|
|
2531
|
+
function isReviewKind(v: unknown): v is PmxAxReviewKind {
|
|
2532
|
+
return v === 'comment' || v === 'finding';
|
|
2533
|
+
}
|
|
2534
|
+
function isReviewAnchor(v: unknown): v is PmxAxReviewAnchorType {
|
|
2535
|
+
return v === 'node' || v === 'file' || v === 'region';
|
|
2536
|
+
}
|
|
2537
|
+
|
|
2538
|
+
// Validate untrusted activity `reactions` from an HTTP body into the typed override
|
|
2539
|
+
// shape ingestActivity expects. `false` suppresses a default reaction; an object
|
|
2540
|
+
// overrides its fields (invalid fields are dropped, not stored raw).
|
|
2541
|
+
function normalizeActivityReactions(input: Record<string, unknown>): {
|
|
2542
|
+
workItem?: false | { status?: PmxAxWorkItemStatus; detail?: string | null };
|
|
2543
|
+
evidence?: false | { kind?: PmxAxEvidenceKind; body?: string | null };
|
|
2544
|
+
review?: false | { severity?: PmxAxReviewSeverity; kind?: PmxAxReviewKind; anchorType?: PmxAxReviewAnchorType; nodeId?: string | null };
|
|
2545
|
+
} {
|
|
2546
|
+
const out: ReturnType<typeof normalizeActivityReactions> = {};
|
|
2547
|
+
if (input.workItem === false) out.workItem = false;
|
|
2548
|
+
else if (isRecord(input.workItem)) {
|
|
2549
|
+
const status = normalizeAxWorkItemStatus(input.workItem.status);
|
|
2550
|
+
out.workItem = {
|
|
2551
|
+
...(status ? { status } : {}),
|
|
2552
|
+
...(typeof input.workItem.detail === 'string' ? { detail: input.workItem.detail } : {}),
|
|
2553
|
+
};
|
|
2554
|
+
}
|
|
2555
|
+
if (input.evidence === false) out.evidence = false;
|
|
2556
|
+
else if (isRecord(input.evidence)) {
|
|
2557
|
+
out.evidence = {
|
|
2558
|
+
...(isAxEvidenceKind(input.evidence.kind) ? { kind: input.evidence.kind } : {}),
|
|
2559
|
+
...(typeof input.evidence.body === 'string' ? { body: input.evidence.body } : {}),
|
|
2560
|
+
};
|
|
2561
|
+
}
|
|
2562
|
+
if (input.review === false) out.review = false;
|
|
2563
|
+
else if (isRecord(input.review)) {
|
|
2564
|
+
out.review = {
|
|
2565
|
+
...(isReviewSeverity(input.review.severity) ? { severity: input.review.severity } : {}),
|
|
2566
|
+
...(isReviewKind(input.review.kind) ? { kind: input.review.kind } : {}),
|
|
2567
|
+
...(isReviewAnchor(input.review.anchorType) ? { anchorType: input.review.anchorType } : {}),
|
|
2568
|
+
...(typeof input.review.nodeId === 'string' ? { nodeId: input.review.nodeId } : {}),
|
|
2569
|
+
};
|
|
2570
|
+
}
|
|
2571
|
+
return out;
|
|
3917
2572
|
}
|
|
3918
2573
|
|
|
2574
|
+
// Report primitive A: ingest a harness-forwarded agent activity; the board auto-reacts.
|
|
2575
|
+
async function handleAxActivityIngest(req: Request): Promise<Response> {
|
|
2576
|
+
const body = await readJson(req);
|
|
2577
|
+
if (!isAxActivityKind(body.kind)) {
|
|
2578
|
+
return responseJson({ ok: false, error: "activity requires a valid 'kind': one of tool-start, tool-result, failure, error, session-start, session-end, command, note." }, 400);
|
|
2579
|
+
}
|
|
2580
|
+
if (typeof body.title !== 'string' || !body.title.trim()) {
|
|
2581
|
+
return responseJson({ ok: false, error: 'activity requires a title.' }, 400);
|
|
2582
|
+
}
|
|
2583
|
+
const result = canvasState.ingestActivity(
|
|
2584
|
+
{
|
|
2585
|
+
kind: body.kind,
|
|
2586
|
+
title: body.title,
|
|
2587
|
+
...(typeof body.summary === 'string' ? { summary: body.summary } : {}),
|
|
2588
|
+
...(body.outcome === 'success' || body.outcome === 'failure' ? { outcome: body.outcome } : {}),
|
|
2589
|
+
...(typeof body.ref === 'string' ? { ref: body.ref } : {}),
|
|
2590
|
+
...(Array.isArray(body.nodeIds) ? { nodeIds: normalizeAxNodeIds(body.nodeIds) } : {}),
|
|
2591
|
+
...(isRecord(body.data) ? { data: body.data } : {}),
|
|
2592
|
+
...(isRecord(body.reactions) ? { reactions: normalizeActivityReactions(body.reactions) } : {}),
|
|
2593
|
+
},
|
|
2594
|
+
{ source: normalizeAxSource(body.source, 'api') },
|
|
2595
|
+
);
|
|
2596
|
+
const meta = { sessionId: primaryWorkbenchSessionId, timestamp: new Date().toISOString() };
|
|
2597
|
+
broadcastWorkbenchEvent('ax-event-created', { event: result.event, ...meta });
|
|
2598
|
+
if (result.workItem) broadcastWorkbenchEvent('ax-state-changed', { workItem: result.workItem, ...meta });
|
|
2599
|
+
if (result.evidence) broadcastWorkbenchEvent('ax-event-created', { evidence: result.evidence, ...meta });
|
|
2600
|
+
if (result.review) broadcastWorkbenchEvent('ax-state-changed', { reviewAnnotation: result.review, ...meta });
|
|
2601
|
+
return responseJson({ ok: true, ...result });
|
|
2602
|
+
}
|
|
2603
|
+
|
|
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.
|
|
2608
|
+
|
|
3919
2609
|
// Compact AX state for surfaces (the same shape seeded into AX-enabled iframes).
|
|
3920
2610
|
// The client fetches this and pushes it to surfaces over the ax-update channel.
|
|
3921
2611
|
function handleGetAxSurfaceSnapshot(): Response {
|
|
@@ -3961,134 +2651,32 @@ async function handleAxInteraction(req: Request): Promise<Response> {
|
|
|
3961
2651
|
return responseJson(result, result.ok ? 200 : result.status);
|
|
3962
2652
|
}
|
|
3963
2653
|
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
const limitRaw = Number(url.searchParams.get('limit') ?? '');
|
|
3967
|
-
const limit = Number.isFinite(limitRaw) && limitRaw > 0 ? limitRaw : undefined;
|
|
3968
|
-
const pending = canvasState.getPendingSteering({
|
|
3969
|
-
...(consumer ? { consumer } : {}),
|
|
3970
|
-
...(limit ? { limit } : {}),
|
|
3971
|
-
});
|
|
3972
|
-
return responseJson({ ok: true, pending });
|
|
3973
|
-
}
|
|
3974
|
-
|
|
3975
|
-
function handleAxDeliveryMark(id: string): Response {
|
|
3976
|
-
const delivered = canvasState.markSteeringDelivered(id);
|
|
3977
|
-
if (delivered) {
|
|
3978
|
-
broadcastWorkbenchEvent('ax-event-created', {
|
|
3979
|
-
steeringDelivered: id,
|
|
3980
|
-
sessionId: primaryWorkbenchSessionId,
|
|
3981
|
-
timestamp: new Date().toISOString(),
|
|
3982
|
-
});
|
|
3983
|
-
}
|
|
3984
|
-
return responseJson({ ok: true, delivered });
|
|
3985
|
-
}
|
|
2654
|
+
// handleAxDeliveryPending / handleAxDeliveryMark migrated to the operation
|
|
2655
|
+
// registry (plan-007 Slice B wave 3): src/server/operations/ops/ax-timeline.ts.
|
|
3986
2656
|
|
|
3987
2657
|
function handleAxElicitationList(): Response {
|
|
3988
2658
|
return responseJson({ ok: true, elicitations: canvasState.getElicitations() });
|
|
3989
2659
|
}
|
|
3990
2660
|
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
if (typeof body.prompt !== 'string' || !body.prompt.trim()) {
|
|
3994
|
-
return responseJson({ ok: false, error: 'elicitation requires a prompt.' }, 400);
|
|
3995
|
-
}
|
|
3996
|
-
const elicitation = canvasState.requestElicitation(
|
|
3997
|
-
{
|
|
3998
|
-
prompt: body.prompt,
|
|
3999
|
-
...(Array.isArray(body.fields) ? { fields: body.fields.filter((f: unknown): f is string => typeof f === 'string') } : {}),
|
|
4000
|
-
...(Array.isArray(body.nodeIds) ? { nodeIds: normalizeAxNodeIds(body.nodeIds) } : {}),
|
|
4001
|
-
},
|
|
4002
|
-
{ source: normalizeAxSource(body.source, 'api') },
|
|
4003
|
-
);
|
|
4004
|
-
broadcastWorkbenchEvent('ax-state-changed', { elicitation, sessionId: primaryWorkbenchSessionId, timestamp: new Date().toISOString() });
|
|
4005
|
-
return responseJson({ ok: true, elicitation });
|
|
4006
|
-
}
|
|
4007
|
-
|
|
4008
|
-
async function handleAxElicitationRespond(req: Request, id: string): Promise<Response> {
|
|
4009
|
-
const body = await readJson(req);
|
|
4010
|
-
const response = isRecord(body.response) ? body.response : {};
|
|
4011
|
-
const elicitation = canvasState.respondElicitation(id, response, { source: normalizeAxSource(body.source, 'api') });
|
|
4012
|
-
if (!elicitation) return responseJson({ ok: false, error: 'elicitation not found or already answered.' }, 404);
|
|
4013
|
-
broadcastWorkbenchEvent('ax-state-changed', { elicitation, sessionId: primaryWorkbenchSessionId, timestamp: new Date().toISOString() });
|
|
4014
|
-
return responseJson({ ok: true, elicitation });
|
|
4015
|
-
}
|
|
2661
|
+
// handleAxElicitationRequest / handleAxElicitationRespond migrated to the
|
|
2662
|
+
// operation registry (plan-007 Slice B wave 2): src/server/operations/ops/ax-work.ts.
|
|
4016
2663
|
|
|
4017
2664
|
function handleAxModeList(): Response {
|
|
4018
2665
|
return responseJson({ ok: true, modeRequests: canvasState.getModeRequests() });
|
|
4019
2666
|
}
|
|
4020
2667
|
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
}
|
|
4026
|
-
const modeRequest = canvasState.requestMode(
|
|
4027
|
-
{
|
|
4028
|
-
mode: body.mode,
|
|
4029
|
-
...(typeof body.reason === 'string' ? { reason: body.reason } : {}),
|
|
4030
|
-
...(Array.isArray(body.nodeIds) ? { nodeIds: normalizeAxNodeIds(body.nodeIds) } : {}),
|
|
4031
|
-
},
|
|
4032
|
-
{ source: normalizeAxSource(body.source, 'api') },
|
|
4033
|
-
);
|
|
4034
|
-
broadcastWorkbenchEvent('ax-state-changed', { modeRequest, sessionId: primaryWorkbenchSessionId, timestamp: new Date().toISOString() });
|
|
4035
|
-
return responseJson({ ok: true, modeRequest });
|
|
4036
|
-
}
|
|
4037
|
-
|
|
4038
|
-
async function handleAxModeResolve(req: Request, id: string): Promise<Response> {
|
|
4039
|
-
const body = await readJson(req);
|
|
4040
|
-
if (body.decision !== 'approved' && body.decision !== 'rejected') {
|
|
4041
|
-
return responseJson({ ok: false, error: 'resolve requires decision approved or rejected.' }, 400);
|
|
4042
|
-
}
|
|
4043
|
-
const modeRequest = canvasState.resolveModeRequest(id, body.decision, {
|
|
4044
|
-
...(typeof body.resolution === 'string' ? { resolution: body.resolution } : {}),
|
|
4045
|
-
source: normalizeAxSource(body.source, 'api'),
|
|
4046
|
-
});
|
|
4047
|
-
if (!modeRequest) return responseJson({ ok: false, error: 'mode request not found or already resolved.' }, 404);
|
|
4048
|
-
broadcastWorkbenchEvent('ax-state-changed', { modeRequest, sessionId: primaryWorkbenchSessionId, timestamp: new Date().toISOString() });
|
|
4049
|
-
return responseJson({ ok: true, modeRequest });
|
|
4050
|
-
}
|
|
4051
|
-
|
|
4052
|
-
function handleAxCommandList(): Response {
|
|
4053
|
-
return responseJson({ ok: true, commands: canvasState.getCommandRegistry() });
|
|
4054
|
-
}
|
|
4055
|
-
|
|
4056
|
-
async function handleAxCommandInvoke(req: Request): Promise<Response> {
|
|
4057
|
-
const body = await readJson(req);
|
|
4058
|
-
if (typeof body.name !== 'string') {
|
|
4059
|
-
return responseJson({ ok: false, error: 'command requires a name.' }, 400);
|
|
4060
|
-
}
|
|
4061
|
-
const event = canvasState.invokeCommand(body.name, isRecord(body.args) ? body.args : null, { source: normalizeAxSource(body.source, 'api') });
|
|
4062
|
-
if (!event) return responseJson({ ok: false, error: `Unknown command "${body.name}".` }, 400);
|
|
4063
|
-
broadcastWorkbenchEvent('ax-event-created', { event, sessionId: primaryWorkbenchSessionId, timestamp: new Date().toISOString() });
|
|
4064
|
-
return responseJson({ ok: true, event });
|
|
4065
|
-
}
|
|
4066
|
-
|
|
4067
|
-
function handleAxPolicyGet(): Response {
|
|
4068
|
-
return responseJson({ ok: true, policy: canvasState.getPolicy() });
|
|
4069
|
-
}
|
|
4070
|
-
|
|
4071
|
-
async function handleAxPolicySet(req: Request): Promise<Response> {
|
|
4072
|
-
const body = await readJson(req);
|
|
4073
|
-
const patch: { tools?: Partial<PmxAxPolicy['tools']>; prompt?: Partial<PmxAxPolicy['prompt']> } = {};
|
|
4074
|
-
if (isRecord(body.tools)) patch.tools = body.tools as Partial<PmxAxPolicy['tools']>;
|
|
4075
|
-
if (isRecord(body.prompt)) patch.prompt = body.prompt as Partial<PmxAxPolicy['prompt']>;
|
|
4076
|
-
const policy = canvasState.setPolicy(patch, { source: normalizeAxSource(body.source, 'api') });
|
|
4077
|
-
broadcastWorkbenchEvent('ax-state-changed', { policy, sessionId: primaryWorkbenchSessionId, timestamp: new Date().toISOString() });
|
|
4078
|
-
return responseJson({ ok: true, policy });
|
|
2668
|
+
// handleAxModeRequest / handleAxModeResolve migrated to the operation registry
|
|
2669
|
+
// (plan-007 Slice B wave 2): src/server/operations/ops/ax-work.ts.
|
|
2670
|
+
|
|
2671
|
+
function handleAxCommandList(): Response {
|
|
2672
|
+
return responseJson({ ok: true, commands: canvasState.getCommandRegistry() });
|
|
4079
2673
|
}
|
|
4080
2674
|
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
broadcastWorkbenchEvent('ax-state-changed', {
|
|
4087
|
-
focus,
|
|
4088
|
-
sessionId: primaryWorkbenchSessionId,
|
|
4089
|
-
timestamp: new Date().toISOString(),
|
|
4090
|
-
});
|
|
4091
|
-
return responseJson({ ok: true, focus });
|
|
2675
|
+
// handleAxCommandInvoke migrated to the operation registry (plan-007 Slice B
|
|
2676
|
+
// wave 3): src/server/operations/ops/ax-timeline.ts.
|
|
2677
|
+
|
|
2678
|
+
function handleAxPolicyGet(): Response {
|
|
2679
|
+
return responseJson({ ok: true, policy: canvasState.getPolicy() });
|
|
4092
2680
|
}
|
|
4093
2681
|
|
|
4094
2682
|
async function handleAxStatePatch(req: Request): Promise<Response> {
|
|
@@ -4108,52 +2696,8 @@ async function handleAxStatePatch(req: Request): Promise<Response> {
|
|
|
4108
2696
|
return responseJson({ ok: true, state: canvasState.getAxState() });
|
|
4109
2697
|
}
|
|
4110
2698
|
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
if (!isAxEventKind(body.kind) || typeof body.summary !== 'string') {
|
|
4114
|
-
return responseJson({ ok: false, error: 'event requires kind and summary.' }, 400);
|
|
4115
|
-
}
|
|
4116
|
-
const event = canvasState.recordAxEvent(
|
|
4117
|
-
{
|
|
4118
|
-
kind: body.kind,
|
|
4119
|
-
summary: body.summary,
|
|
4120
|
-
detail: typeof body.detail === 'string' ? body.detail : null,
|
|
4121
|
-
nodeIds: normalizeAxNodeIds(body.nodeIds),
|
|
4122
|
-
data: isRecord(body.data) ? body.data : null,
|
|
4123
|
-
},
|
|
4124
|
-
{ source: normalizeAxSource(body.source, 'api') },
|
|
4125
|
-
);
|
|
4126
|
-
broadcastWorkbenchEvent('ax-event-created', {
|
|
4127
|
-
event,
|
|
4128
|
-
sessionId: primaryWorkbenchSessionId,
|
|
4129
|
-
timestamp: new Date().toISOString(),
|
|
4130
|
-
});
|
|
4131
|
-
return responseJson({ ok: true, event });
|
|
4132
|
-
}
|
|
4133
|
-
|
|
4134
|
-
async function handleAxSteer(req: Request): Promise<Response> {
|
|
4135
|
-
const body = await readJson(req);
|
|
4136
|
-
if (typeof body.message !== 'string' || !body.message.trim()) {
|
|
4137
|
-
return responseJson({ ok: false, error: 'steer requires a non-empty message.' }, 400);
|
|
4138
|
-
}
|
|
4139
|
-
const steering = canvasState.recordSteeringMessage(body.message, {
|
|
4140
|
-
source: normalizeAxSource(body.source, 'api'),
|
|
4141
|
-
});
|
|
4142
|
-
broadcastWorkbenchEvent('ax-event-created', {
|
|
4143
|
-
steering,
|
|
4144
|
-
sessionId: primaryWorkbenchSessionId,
|
|
4145
|
-
timestamp: new Date().toISOString(),
|
|
4146
|
-
});
|
|
4147
|
-
return responseJson({ ok: true, steering });
|
|
4148
|
-
}
|
|
4149
|
-
|
|
4150
|
-
function handleAxTimelineGet(url: URL): Response {
|
|
4151
|
-
const limit = Number(url.searchParams.get('limit') ?? '');
|
|
4152
|
-
return responseJson({
|
|
4153
|
-
ok: true,
|
|
4154
|
-
...canvasState.getAxTimeline(Number.isFinite(limit) && limit > 0 ? { limit } : {}),
|
|
4155
|
-
});
|
|
4156
|
-
}
|
|
2699
|
+
// handleAxEventAdd / handleAxSteer / handleAxTimelineGet migrated to the
|
|
2700
|
+
// operation registry (plan-007 Slice B wave 3): src/server/operations/ops/ax-timeline.ts.
|
|
4157
2701
|
|
|
4158
2702
|
const AX_WORK_STATUSES = new Set(['todo', 'in-progress', 'blocked', 'done', 'cancelled']);
|
|
4159
2703
|
|
|
@@ -4167,225 +2711,33 @@ function handleAxWorkList(): Response {
|
|
|
4167
2711
|
return responseJson({ ok: true, workItems: canvasState.getWorkItems() });
|
|
4168
2712
|
}
|
|
4169
2713
|
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
if (typeof body.title !== 'string' || !body.title.trim()) {
|
|
4173
|
-
return responseJson({ ok: false, error: 'work item requires a title.' }, 400);
|
|
4174
|
-
}
|
|
4175
|
-
const status = normalizeAxWorkItemStatus(body.status);
|
|
4176
|
-
const workItem = canvasState.addWorkItem(
|
|
4177
|
-
{
|
|
4178
|
-
title: body.title,
|
|
4179
|
-
...(status ? { status } : {}),
|
|
4180
|
-
...(typeof body.detail === 'string' ? { detail: body.detail } : {}),
|
|
4181
|
-
...(Array.isArray(body.nodeIds) ? { nodeIds: normalizeAxNodeIds(body.nodeIds) } : {}),
|
|
4182
|
-
},
|
|
4183
|
-
{ source: normalizeAxSource(body.source, 'api') },
|
|
4184
|
-
);
|
|
4185
|
-
broadcastWorkbenchEvent('ax-state-changed', {
|
|
4186
|
-
workItem,
|
|
4187
|
-
sessionId: primaryWorkbenchSessionId,
|
|
4188
|
-
timestamp: new Date().toISOString(),
|
|
4189
|
-
});
|
|
4190
|
-
return responseJson({ ok: true, workItem });
|
|
4191
|
-
}
|
|
4192
|
-
|
|
4193
|
-
async function handleAxWorkUpdate(req: Request, id: string): Promise<Response> {
|
|
4194
|
-
const body = await readJson(req);
|
|
4195
|
-
const status = normalizeAxWorkItemStatus(body.status);
|
|
4196
|
-
const workItem = canvasState.updateWorkItem(
|
|
4197
|
-
id,
|
|
4198
|
-
{
|
|
4199
|
-
...(typeof body.title === 'string' ? { title: body.title } : {}),
|
|
4200
|
-
...(status ? { status } : {}),
|
|
4201
|
-
...(typeof body.detail === 'string' || body.detail === null ? { detail: body.detail as string | null } : {}),
|
|
4202
|
-
...(Array.isArray(body.nodeIds) ? { nodeIds: normalizeAxNodeIds(body.nodeIds) } : {}),
|
|
4203
|
-
},
|
|
4204
|
-
{ source: normalizeAxSource(body.source, 'api') },
|
|
4205
|
-
);
|
|
4206
|
-
if (!workItem) return responseJson({ ok: false, error: 'work item not found.' }, 404);
|
|
4207
|
-
broadcastWorkbenchEvent('ax-state-changed', {
|
|
4208
|
-
workItem,
|
|
4209
|
-
sessionId: primaryWorkbenchSessionId,
|
|
4210
|
-
timestamp: new Date().toISOString(),
|
|
4211
|
-
});
|
|
4212
|
-
return responseJson({ ok: true, workItem });
|
|
4213
|
-
}
|
|
2714
|
+
// handleAxWorkAdd / handleAxWorkUpdate migrated to the operation registry
|
|
2715
|
+
// (plan-007 Slice B wave 2): src/server/operations/ops/ax-work.ts.
|
|
4214
2716
|
|
|
4215
2717
|
function handleAxApprovalList(): Response {
|
|
4216
2718
|
return responseJson({ ok: true, approvalGates: canvasState.getApprovalGates() });
|
|
4217
2719
|
}
|
|
4218
2720
|
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
if (typeof body.title !== 'string' || !body.title.trim()) {
|
|
4222
|
-
return responseJson({ ok: false, error: 'approval request requires a title.' }, 400);
|
|
4223
|
-
}
|
|
4224
|
-
const approvalGate = canvasState.requestApproval(
|
|
4225
|
-
{
|
|
4226
|
-
title: body.title,
|
|
4227
|
-
...(typeof body.detail === 'string' ? { detail: body.detail } : {}),
|
|
4228
|
-
...(typeof body.action === 'string' ? { action: body.action } : {}),
|
|
4229
|
-
...(Array.isArray(body.nodeIds) ? { nodeIds: normalizeAxNodeIds(body.nodeIds) } : {}),
|
|
4230
|
-
},
|
|
4231
|
-
{ source: normalizeAxSource(body.source, 'api') },
|
|
4232
|
-
);
|
|
4233
|
-
broadcastWorkbenchEvent('ax-state-changed', {
|
|
4234
|
-
approvalGate,
|
|
4235
|
-
sessionId: primaryWorkbenchSessionId,
|
|
4236
|
-
timestamp: new Date().toISOString(),
|
|
4237
|
-
});
|
|
4238
|
-
return responseJson({ ok: true, approvalGate });
|
|
4239
|
-
}
|
|
4240
|
-
|
|
4241
|
-
async function handleAxApprovalResolve(req: Request, id: string): Promise<Response> {
|
|
4242
|
-
const body = await readJson(req);
|
|
4243
|
-
if (body.decision !== 'approved' && body.decision !== 'rejected') {
|
|
4244
|
-
return responseJson({ ok: false, error: 'resolve requires decision approved or rejected.' }, 400);
|
|
4245
|
-
}
|
|
4246
|
-
const approvalGate = canvasState.resolveApproval(
|
|
4247
|
-
id,
|
|
4248
|
-
body.decision,
|
|
4249
|
-
{
|
|
4250
|
-
...(typeof body.resolution === 'string' ? { resolution: body.resolution } : {}),
|
|
4251
|
-
source: normalizeAxSource(body.source, 'api'),
|
|
4252
|
-
},
|
|
4253
|
-
);
|
|
4254
|
-
if (!approvalGate) return responseJson({ ok: false, error: 'approval gate not found or already resolved.' }, 404);
|
|
4255
|
-
broadcastWorkbenchEvent('ax-state-changed', {
|
|
4256
|
-
approvalGate,
|
|
4257
|
-
sessionId: primaryWorkbenchSessionId,
|
|
4258
|
-
timestamp: new Date().toISOString(),
|
|
4259
|
-
});
|
|
4260
|
-
return responseJson({ ok: true, approvalGate });
|
|
4261
|
-
}
|
|
4262
|
-
|
|
4263
|
-
async function handleAxEvidenceAdd(req: Request): Promise<Response> {
|
|
4264
|
-
const body = await readJson(req);
|
|
4265
|
-
if (!isAxEvidenceKind(body.kind) || typeof body.title !== 'string' || !body.title.trim()) {
|
|
4266
|
-
return responseJson({ ok: false, error: 'evidence requires kind and title.' }, 400);
|
|
4267
|
-
}
|
|
4268
|
-
const evidence = canvasState.addEvidence(
|
|
4269
|
-
{
|
|
4270
|
-
kind: body.kind,
|
|
4271
|
-
title: body.title,
|
|
4272
|
-
body: typeof body.body === 'string' ? body.body : null,
|
|
4273
|
-
ref: typeof body.ref === 'string' ? body.ref : null,
|
|
4274
|
-
nodeIds: normalizeAxNodeIds(body.nodeIds),
|
|
4275
|
-
data: isRecord(body.data) ? body.data : null,
|
|
4276
|
-
},
|
|
4277
|
-
{ source: normalizeAxSource(body.source, 'api') },
|
|
4278
|
-
);
|
|
4279
|
-
broadcastWorkbenchEvent('ax-event-created', {
|
|
4280
|
-
evidence,
|
|
4281
|
-
sessionId: primaryWorkbenchSessionId,
|
|
4282
|
-
timestamp: new Date().toISOString(),
|
|
4283
|
-
});
|
|
4284
|
-
return responseJson({ ok: true, evidence });
|
|
4285
|
-
}
|
|
2721
|
+
// handleAxApprovalRequest / handleAxApprovalResolve migrated to the operation
|
|
2722
|
+
// registry (plan-007 Slice B wave 2): src/server/operations/ops/ax-work.ts.
|
|
4286
2723
|
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
const AX_REVIEW_STATUSES = new Set(['open', 'resolved', 'dismissed']);
|
|
4290
|
-
const AX_REVIEW_ANCHORS = new Set(['node', 'file', 'region']);
|
|
2724
|
+
// handleAxEvidenceAdd migrated to the operation registry (plan-007 Slice B
|
|
2725
|
+
// wave 3): src/server/operations/ops/ax-timeline.ts.
|
|
4291
2726
|
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
}
|
|
4295
|
-
function normalizeAxReviewSeverity(value: unknown): PmxAxReviewSeverity | undefined {
|
|
4296
|
-
return typeof value === 'string' && AX_REVIEW_SEVERITIES.has(value) ? value as PmxAxReviewSeverity : undefined;
|
|
4297
|
-
}
|
|
4298
|
-
function normalizeAxReviewStatus(value: unknown): PmxAxReviewStatus | undefined {
|
|
4299
|
-
return typeof value === 'string' && AX_REVIEW_STATUSES.has(value) ? value as PmxAxReviewStatus : undefined;
|
|
4300
|
-
}
|
|
4301
|
-
function normalizeAxReviewAnchor(value: unknown): PmxAxReviewAnchorType | undefined {
|
|
4302
|
-
return typeof value === 'string' && AX_REVIEW_ANCHORS.has(value) ? value as PmxAxReviewAnchorType : undefined;
|
|
4303
|
-
}
|
|
4304
|
-
function normalizeAxReviewRegion(value: unknown): PmxAxReviewRegion | undefined {
|
|
4305
|
-
if (!isRecord(value)) return undefined;
|
|
4306
|
-
return {
|
|
4307
|
-
...(typeof value.line === 'number' ? { line: value.line } : {}),
|
|
4308
|
-
...(typeof value.endLine === 'number' ? { endLine: value.endLine } : {}),
|
|
4309
|
-
...(typeof value.label === 'string' ? { label: value.label } : {}),
|
|
4310
|
-
};
|
|
4311
|
-
}
|
|
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.
|
|
4312
2729
|
|
|
4313
2730
|
function handleAxReviewList(): Response {
|
|
4314
2731
|
return responseJson({ ok: true, reviewAnnotations: canvasState.getReviewAnnotations() });
|
|
4315
2732
|
}
|
|
4316
2733
|
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
if (typeof body.body !== 'string' || !body.body.trim()) {
|
|
4320
|
-
return responseJson({ ok: false, error: 'review annotation requires a body.' }, 400);
|
|
4321
|
-
}
|
|
4322
|
-
const kind = normalizeAxReviewKind(body.kind);
|
|
4323
|
-
const severity = normalizeAxReviewSeverity(body.severity);
|
|
4324
|
-
const anchorType = normalizeAxReviewAnchor(body.anchorType);
|
|
4325
|
-
const region = normalizeAxReviewRegion(body.region);
|
|
4326
|
-
const reviewAnnotation = canvasState.addReviewAnnotation(
|
|
4327
|
-
{
|
|
4328
|
-
body: body.body,
|
|
4329
|
-
...(kind ? { kind } : {}),
|
|
4330
|
-
...(severity ? { severity } : {}),
|
|
4331
|
-
...(anchorType ? { anchorType } : {}),
|
|
4332
|
-
...(typeof body.nodeId === 'string' ? { nodeId: body.nodeId } : {}),
|
|
4333
|
-
...(typeof body.file === 'string' ? { file: body.file } : {}),
|
|
4334
|
-
...(region ? { region } : {}),
|
|
4335
|
-
...(typeof body.author === 'string' ? { author: body.author } : {}),
|
|
4336
|
-
},
|
|
4337
|
-
{ source: normalizeAxSource(body.source, 'api') },
|
|
4338
|
-
);
|
|
4339
|
-
if (!reviewAnnotation) {
|
|
4340
|
-
return responseJson({ ok: false, error: 'node-anchored review annotation requires a nodeId that exists on the canvas.' }, 400);
|
|
4341
|
-
}
|
|
4342
|
-
broadcastWorkbenchEvent('ax-state-changed', {
|
|
4343
|
-
reviewAnnotation,
|
|
4344
|
-
sessionId: primaryWorkbenchSessionId,
|
|
4345
|
-
timestamp: new Date().toISOString(),
|
|
4346
|
-
});
|
|
4347
|
-
return responseJson({ ok: true, reviewAnnotation });
|
|
4348
|
-
}
|
|
4349
|
-
|
|
4350
|
-
async function handleAxReviewUpdate(req: Request, id: string): Promise<Response> {
|
|
4351
|
-
const body = await readJson(req);
|
|
4352
|
-
const status = normalizeAxReviewStatus(body.status);
|
|
4353
|
-
const severity = normalizeAxReviewSeverity(body.severity);
|
|
4354
|
-
const kind = normalizeAxReviewKind(body.kind);
|
|
4355
|
-
const reviewAnnotation = canvasState.updateReviewAnnotation(
|
|
4356
|
-
id,
|
|
4357
|
-
{
|
|
4358
|
-
...(typeof body.body === 'string' ? { body: body.body } : {}),
|
|
4359
|
-
...(status ? { status } : {}),
|
|
4360
|
-
...(severity ? { severity } : {}),
|
|
4361
|
-
...(kind ? { kind } : {}),
|
|
4362
|
-
},
|
|
4363
|
-
{ source: normalizeAxSource(body.source, 'api') },
|
|
4364
|
-
);
|
|
4365
|
-
if (!reviewAnnotation) return responseJson({ ok: false, error: 'review annotation not found.' }, 404);
|
|
4366
|
-
broadcastWorkbenchEvent('ax-state-changed', {
|
|
4367
|
-
reviewAnnotation,
|
|
4368
|
-
sessionId: primaryWorkbenchSessionId,
|
|
4369
|
-
timestamp: new Date().toISOString(),
|
|
4370
|
-
});
|
|
4371
|
-
return responseJson({ ok: true, reviewAnnotation });
|
|
4372
|
-
}
|
|
2734
|
+
// handleAxReviewAdd / handleAxReviewUpdate migrated to the operation registry
|
|
2735
|
+
// (plan-007 Slice B wave 2): src/server/operations/ops/ax-work.ts.
|
|
4373
2736
|
|
|
4374
2737
|
function handleAxHostCapabilityGet(): Response {
|
|
4375
2738
|
return responseJson({ ok: true, host: canvasState.getHostCapability() });
|
|
4376
2739
|
}
|
|
4377
2740
|
|
|
4378
|
-
async function handleAxHostCapabilityReport(req: Request): Promise<Response> {
|
|
4379
|
-
const body = await readJson(req);
|
|
4380
|
-
const host = canvasState.setHostCapability(body, { source: normalizeAxSource(body.source, 'api') });
|
|
4381
|
-
broadcastWorkbenchEvent('ax-state-changed', {
|
|
4382
|
-
host,
|
|
4383
|
-
sessionId: primaryWorkbenchSessionId,
|
|
4384
|
-
timestamp: new Date().toISOString(),
|
|
4385
|
-
});
|
|
4386
|
-
return responseJson({ ok: true, host });
|
|
4387
|
-
}
|
|
4388
|
-
|
|
4389
2741
|
// ── Port resolution ───────────────────────────────────────────
|
|
4390
2742
|
|
|
4391
2743
|
function buildPortCandidates(preferredPort: number): number[] {
|
|
@@ -5149,6 +3501,15 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
5149
3501
|
hostname: DEFAULT_HOST,
|
|
5150
3502
|
port: portCandidate,
|
|
5151
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
|
+
},
|
|
5152
3513
|
async fetch(req) {
|
|
5153
3514
|
const url = new URL(req.url);
|
|
5154
3515
|
|
|
@@ -5209,30 +3570,20 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
5209
3570
|
return handleWorkbenchIntent(req);
|
|
5210
3571
|
}
|
|
5211
3572
|
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
if (url.pathname === '/api/workbench/webview/evaluate' && req.method === 'POST') {
|
|
5221
|
-
return handleWorkbenchWebViewEvaluate(req);
|
|
5222
|
-
}
|
|
5223
|
-
|
|
5224
|
-
if (url.pathname === '/api/workbench/webview/resize' && req.method === 'POST') {
|
|
5225
|
-
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;
|
|
5226
3581
|
}
|
|
5227
3582
|
|
|
5228
3583
|
if (url.pathname === '/api/workbench/webview/screenshot' && req.method === 'POST') {
|
|
5229
3584
|
return handleWorkbenchWebViewScreenshot(req);
|
|
5230
3585
|
}
|
|
5231
3586
|
|
|
5232
|
-
if (url.pathname === '/api/workbench/webview' && req.method === 'DELETE') {
|
|
5233
|
-
return handleWorkbenchWebViewStop();
|
|
5234
|
-
}
|
|
5235
|
-
|
|
5236
3587
|
if (url.pathname === '/api/file/save' && req.method === 'POST') {
|
|
5237
3588
|
return handleSave(req);
|
|
5238
3589
|
}
|
|
@@ -5241,16 +3592,11 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
5241
3592
|
return handleRender(req);
|
|
5242
3593
|
}
|
|
5243
3594
|
|
|
5244
|
-
//
|
|
5245
|
-
|
|
5246
|
-
|
|
5247
|
-
|
|
5248
|
-
|
|
5249
|
-
: serializeCanvasLayoutWithBlobSummaries(withContextPinLayoutReadState(canvasState.getLayoutForPersistence())));
|
|
5250
|
-
}
|
|
5251
|
-
|
|
5252
|
-
if (url.pathname === '/api/canvas/summary' && req.method === 'GET') {
|
|
5253
|
-
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;
|
|
5254
3600
|
}
|
|
5255
3601
|
|
|
5256
3602
|
if (url.pathname === '/api/canvas/theme' && req.method === 'GET') {
|
|
@@ -5265,17 +3611,8 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
5265
3611
|
return handleCanvasUpdate(req);
|
|
5266
3612
|
}
|
|
5267
3613
|
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
}
|
|
5271
|
-
|
|
5272
|
-
if (url.pathname === '/api/canvas/schema/validate' && req.method === 'POST') {
|
|
5273
|
-
return handleCanvasValidateSpec(req);
|
|
5274
|
-
}
|
|
5275
|
-
|
|
5276
|
-
if (url.pathname === '/api/canvas/batch' && req.method === 'POST') {
|
|
5277
|
-
return handleCanvasBatch(req);
|
|
5278
|
-
}
|
|
3614
|
+
// POST /api/canvas/batch migrated to the operation registry
|
|
3615
|
+
// (plan-008 Wave 2): src/server/operations/ops/batch.ts.
|
|
5279
3616
|
|
|
5280
3617
|
if (url.pathname === '/api/canvas/viewport' && req.method === 'POST') {
|
|
5281
3618
|
return handleCanvasViewport(req);
|
|
@@ -5285,25 +3622,9 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
5285
3622
|
return handleCanvasAddAnnotation(req);
|
|
5286
3623
|
}
|
|
5287
3624
|
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
if (url.pathname === '/api/canvas/node' && req.method === 'POST') {
|
|
5293
|
-
return handleCanvasAddNode(req);
|
|
5294
|
-
}
|
|
5295
|
-
|
|
5296
|
-
if (url.pathname === '/api/canvas/mcp-app/open' && req.method === 'POST') {
|
|
5297
|
-
return handleCanvasOpenMcpApp(req);
|
|
5298
|
-
}
|
|
5299
|
-
|
|
5300
|
-
if (url.pathname === '/api/canvas/diagram' && req.method === 'POST') {
|
|
5301
|
-
return handleCanvasAddDiagram(req);
|
|
5302
|
-
}
|
|
5303
|
-
|
|
5304
|
-
if (url.pathname === '/api/canvas/web-artifact' && req.method === 'POST') {
|
|
5305
|
-
return handleCanvasBuildWebArtifact(req);
|
|
5306
|
-
}
|
|
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.
|
|
5307
3628
|
|
|
5308
3629
|
// Individual node GET/PATCH/DELETE
|
|
5309
3630
|
if (url.pathname.startsWith('/api/canvas/node/') && url.pathname.endsWith('/refresh') && req.method === 'POST') {
|
|
@@ -5311,118 +3632,26 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
5311
3632
|
return handleCanvasRefreshWebpageNode(nodeId, req);
|
|
5312
3633
|
}
|
|
5313
3634
|
|
|
5314
|
-
if (url.pathname.startsWith('/api/canvas/node/') && req.method === 'GET') {
|
|
5315
|
-
const nodeId = decodeURIComponent(url.pathname.slice('/api/canvas/node/'.length));
|
|
5316
|
-
const includeBlobs = url.searchParams.get('includeBlobs') === 'true';
|
|
5317
|
-
const node = includeBlobs ? canvasState.getNode(nodeId) : canvasState.getNodeForPersistence(nodeId);
|
|
5318
|
-
if (!node) return responseJson({ ok: false, error: `Node "${nodeId}" not found.` }, 404);
|
|
5319
|
-
const responseNode = withContextPinReadState(node);
|
|
5320
|
-
return responseJson(includeBlobs
|
|
5321
|
-
? serializeCanvasNode(responseNode)
|
|
5322
|
-
: serializeCanvasNodeWithBlobSummaries(responseNode));
|
|
5323
|
-
}
|
|
5324
|
-
|
|
5325
|
-
if (url.pathname.startsWith('/api/canvas/node/') && req.method === 'PATCH') {
|
|
5326
|
-
const nodeId = url.pathname.slice('/api/canvas/node/'.length);
|
|
5327
|
-
return handleCanvasUpdateNode(nodeId, req);
|
|
5328
|
-
}
|
|
5329
|
-
|
|
5330
|
-
if (url.pathname.startsWith('/api/canvas/node/') && req.method === 'DELETE') {
|
|
5331
|
-
const nodeId = url.pathname.slice('/api/canvas/node/'.length);
|
|
5332
|
-
closeNodeAppSession(canvasState.getNode(nodeId));
|
|
5333
|
-
const result = removeCanvasNode(nodeId);
|
|
5334
|
-
if (!result.removed) return responseJson({ ok: false, error: `Node "${nodeId}" not found.` }, 404);
|
|
5335
|
-
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
5336
|
-
if (result.needsCodeGraphRecompute) {
|
|
5337
|
-
scheduleCodeGraphRecompute(() => {
|
|
5338
|
-
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
5339
|
-
});
|
|
5340
|
-
}
|
|
5341
|
-
return responseJson({ ok: true, removed: nodeId });
|
|
5342
|
-
}
|
|
5343
|
-
|
|
5344
3635
|
if (url.pathname.startsWith('/api/canvas/image/') && req.method === 'GET') {
|
|
5345
3636
|
return await handleCanvasImage(url.pathname);
|
|
5346
3637
|
}
|
|
5347
3638
|
|
|
5348
|
-
if (url.pathname === '/api/canvas/edge' && req.method === 'POST') {
|
|
5349
|
-
return handleCanvasAddEdge(req);
|
|
5350
|
-
}
|
|
5351
|
-
|
|
5352
|
-
if (url.pathname === '/api/canvas/edge' && req.method === 'DELETE') {
|
|
5353
|
-
return handleCanvasRemoveEdge(req);
|
|
5354
|
-
}
|
|
5355
|
-
|
|
5356
|
-
// Snapshot API
|
|
5357
|
-
if (url.pathname === '/api/canvas/snapshots' && req.method === 'GET') {
|
|
5358
|
-
return responseJson(listCanvasSnapshots({
|
|
5359
|
-
limit: parsePositiveIntegerParam(url.searchParams.get('limit')),
|
|
5360
|
-
query: url.searchParams.get('q') ?? url.searchParams.get('query') ?? undefined,
|
|
5361
|
-
before: url.searchParams.get('before') ?? undefined,
|
|
5362
|
-
after: url.searchParams.get('after') ?? undefined,
|
|
5363
|
-
all: url.searchParams.get('all') === 'true',
|
|
5364
|
-
}));
|
|
5365
|
-
}
|
|
5366
|
-
|
|
5367
|
-
if (url.pathname === '/api/canvas/snapshots' && req.method === 'POST') {
|
|
5368
|
-
return handleSnapshotSave(req);
|
|
5369
|
-
}
|
|
5370
|
-
|
|
5371
|
-
if (url.pathname === '/api/canvas/snapshots/gc' && req.method === 'POST') {
|
|
5372
|
-
return handleSnapshotGc(req);
|
|
5373
|
-
}
|
|
5374
|
-
|
|
5375
|
-
if (url.pathname === '/api/canvas/snapshots/diff' && req.method === 'GET') {
|
|
5376
|
-
const name = url.searchParams.get('name') ?? url.searchParams.get('id') ?? '';
|
|
5377
|
-
if (!name.trim()) return responseJson({ ok: false, error: 'Missing snapshot name or id.' }, 400);
|
|
5378
|
-
const snapshot = canvasState.getSnapshotData(name);
|
|
5379
|
-
if (!snapshot) return responseJson({ ok: false, error: `Snapshot "${name}" not found.` }, 404);
|
|
5380
|
-
const diff = diffLayouts(snapshot.name, snapshot, canvasState.getLayout());
|
|
5381
|
-
return responseJson({ ok: true, text: formatDiff(diff), diff });
|
|
5382
|
-
}
|
|
5383
|
-
|
|
5384
|
-
if (url.pathname.startsWith('/api/canvas/snapshots/') && url.pathname.endsWith('/diff') && req.method === 'GET') {
|
|
5385
|
-
const id = decodeURIComponent(url.pathname.slice('/api/canvas/snapshots/'.length, -'/diff'.length));
|
|
5386
|
-
const snapshot = canvasState.getSnapshotData(id);
|
|
5387
|
-
if (!snapshot) return responseJson({ ok: false, error: `Snapshot "${id}" not found.` }, 404);
|
|
5388
|
-
const diff = diffLayouts(snapshot.name, snapshot, canvasState.getLayout());
|
|
5389
|
-
return responseJson({ ok: true, text: formatDiff(diff), diff });
|
|
5390
|
-
}
|
|
5391
|
-
|
|
5392
|
-
if (url.pathname.startsWith('/api/canvas/snapshots/') && req.method === 'POST') {
|
|
5393
|
-
const id = decodeURIComponent(url.pathname.slice('/api/canvas/snapshots/'.length));
|
|
5394
|
-
const result = await restoreCanvasSnapshot(id);
|
|
5395
|
-
if (!result.ok) return responseText('Snapshot not found', 404);
|
|
5396
|
-
broadcastWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
5397
|
-
return responseJson({ ok: true });
|
|
5398
|
-
}
|
|
5399
|
-
|
|
5400
|
-
if (url.pathname.startsWith('/api/canvas/snapshots/') && req.method === 'DELETE') {
|
|
5401
|
-
const id = url.pathname.split('/').pop() ?? '';
|
|
5402
|
-
const result = deleteCanvasSnapshot(id);
|
|
5403
|
-
if (!result.ok) return responseText('Snapshot not found', 404);
|
|
5404
|
-
return responseJson({ ok: true });
|
|
5405
|
-
}
|
|
5406
|
-
|
|
5407
|
-
// Context pins API
|
|
5408
|
-
if (url.pathname === '/api/canvas/context-pins' && req.method === 'POST') {
|
|
5409
|
-
return handleContextPinsUpdate(req);
|
|
5410
|
-
}
|
|
5411
|
-
|
|
5412
3639
|
if (url.pathname === '/api/canvas/pinned-context' && req.method === 'GET') {
|
|
5413
3640
|
return handleGetPinnedContext();
|
|
5414
3641
|
}
|
|
5415
3642
|
|
|
5416
|
-
|
|
5417
|
-
return handleGetAxState();
|
|
5418
|
-
}
|
|
3643
|
+
// GET /api/canvas/ax migrated to the operation registry (plan-007 Slice B.1).
|
|
5419
3644
|
|
|
5420
3645
|
if (url.pathname === '/api/canvas/ax' && req.method === 'PATCH') {
|
|
5421
3646
|
return handleAxStatePatch(req);
|
|
5422
3647
|
}
|
|
5423
3648
|
|
|
5424
3649
|
if (url.pathname === '/api/canvas/ax/context' && req.method === 'GET') {
|
|
5425
|
-
return handleGetAxContext();
|
|
3650
|
+
return handleGetAxContext(url);
|
|
3651
|
+
}
|
|
3652
|
+
|
|
3653
|
+
if (url.pathname === '/api/canvas/ax/activity' && req.method === 'POST') {
|
|
3654
|
+
return handleAxActivityIngest(req);
|
|
5426
3655
|
}
|
|
5427
3656
|
|
|
5428
3657
|
if (url.pathname === '/api/canvas/ax/surface-snapshot' && req.method === 'GET') {
|
|
@@ -5433,187 +3662,85 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
5433
3662
|
return handleOpenExternalSurface(req);
|
|
5434
3663
|
}
|
|
5435
3664
|
|
|
5436
|
-
|
|
5437
|
-
return handleAxFocusUpdate(req);
|
|
5438
|
-
}
|
|
5439
|
-
|
|
5440
|
-
if (url.pathname === '/api/canvas/ax/event' && req.method === 'POST') {
|
|
5441
|
-
return handleAxEventAdd(req);
|
|
5442
|
-
}
|
|
5443
|
-
|
|
5444
|
-
if (url.pathname === '/api/canvas/ax/steer' && req.method === 'POST') {
|
|
5445
|
-
return handleAxSteer(req);
|
|
5446
|
-
}
|
|
3665
|
+
// POST /api/canvas/ax/focus migrated to the operation registry (plan-007 Slice B.1).
|
|
5447
3666
|
|
|
5448
|
-
|
|
5449
|
-
|
|
5450
|
-
|
|
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.
|
|
5451
3670
|
|
|
5452
3671
|
if (url.pathname === '/api/canvas/ax/work' && req.method === 'GET') {
|
|
5453
3672
|
return handleAxWorkList();
|
|
5454
3673
|
}
|
|
5455
3674
|
|
|
5456
|
-
|
|
5457
|
-
|
|
5458
|
-
}
|
|
5459
|
-
|
|
5460
|
-
if (url.pathname.startsWith('/api/canvas/ax/work/') && req.method === 'PATCH') {
|
|
5461
|
-
const workItemId = decodeURIComponent(url.pathname.slice('/api/canvas/ax/work/'.length));
|
|
5462
|
-
return handleAxWorkUpdate(req, workItemId);
|
|
5463
|
-
}
|
|
3675
|
+
// POST /api/canvas/ax/work + PATCH /api/canvas/ax/work/:id migrated to
|
|
3676
|
+
// the operation registry (plan-007 Slice B wave 2).
|
|
5464
3677
|
|
|
5465
3678
|
if (url.pathname === '/api/canvas/ax/approval' && req.method === 'GET') {
|
|
5466
3679
|
return handleAxApprovalList();
|
|
5467
3680
|
}
|
|
5468
3681
|
|
|
5469
|
-
|
|
5470
|
-
|
|
5471
|
-
}
|
|
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).
|
|
5472
3684
|
|
|
5473
|
-
|
|
5474
|
-
|
|
5475
|
-
url.pathname.slice('/api/canvas/ax/approval/'.length, -'/resolve'.length),
|
|
5476
|
-
);
|
|
5477
|
-
return handleAxApprovalResolve(req, approvalId);
|
|
5478
|
-
}
|
|
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).
|
|
5479
3687
|
|
|
5480
|
-
|
|
5481
|
-
|
|
5482
|
-
}
|
|
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.
|
|
5483
3690
|
|
|
5484
3691
|
if (url.pathname === '/api/canvas/ax/review' && req.method === 'GET') {
|
|
5485
3692
|
return handleAxReviewList();
|
|
5486
3693
|
}
|
|
5487
3694
|
|
|
5488
|
-
|
|
5489
|
-
|
|
5490
|
-
}
|
|
5491
|
-
|
|
5492
|
-
if (url.pathname.startsWith('/api/canvas/ax/review/') && req.method === 'PATCH') {
|
|
5493
|
-
const reviewId = decodeURIComponent(url.pathname.slice('/api/canvas/ax/review/'.length));
|
|
5494
|
-
return handleAxReviewUpdate(req, reviewId);
|
|
5495
|
-
}
|
|
3695
|
+
// POST /api/canvas/ax/review + PATCH /api/canvas/ax/review/:id migrated
|
|
3696
|
+
// to the operation registry (plan-007 Slice B wave 2).
|
|
5496
3697
|
|
|
5497
3698
|
if (url.pathname === '/api/canvas/ax/host-capability' && req.method === 'GET') {
|
|
5498
3699
|
return handleAxHostCapabilityGet();
|
|
5499
3700
|
}
|
|
5500
3701
|
|
|
5501
|
-
|
|
5502
|
-
return handleAxHostCapabilityReport(req);
|
|
5503
|
-
}
|
|
3702
|
+
// PUT /api/canvas/ax/host-capability migrated to the operation registry (plan-007 Slice B.1).
|
|
5504
3703
|
|
|
5505
3704
|
if (url.pathname === '/api/canvas/ax/interaction' && req.method === 'POST') {
|
|
5506
3705
|
return handleAxInteraction(req);
|
|
5507
3706
|
}
|
|
5508
3707
|
|
|
5509
|
-
|
|
5510
|
-
|
|
5511
|
-
|
|
5512
|
-
|
|
5513
|
-
if (url.pathname.startsWith('/api/canvas/ax/delivery/') && url.pathname.endsWith('/mark') && req.method === 'POST') {
|
|
5514
|
-
const deliveryId = decodeURIComponent(
|
|
5515
|
-
url.pathname.slice('/api/canvas/ax/delivery/'.length, -'/mark'.length),
|
|
5516
|
-
);
|
|
5517
|
-
return handleAxDeliveryMark(deliveryId);
|
|
5518
|
-
}
|
|
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.
|
|
5519
3711
|
|
|
5520
3712
|
if (url.pathname === '/api/canvas/ax/elicitation' && req.method === 'GET') {
|
|
5521
3713
|
return handleAxElicitationList();
|
|
5522
3714
|
}
|
|
5523
3715
|
|
|
5524
|
-
|
|
5525
|
-
|
|
5526
|
-
}
|
|
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).
|
|
5527
3718
|
|
|
5528
|
-
|
|
5529
|
-
|
|
5530
|
-
url.pathname.slice('/api/canvas/ax/elicitation/'.length, -'/respond'.length),
|
|
5531
|
-
);
|
|
5532
|
-
return handleAxElicitationRespond(req, elicitationId);
|
|
5533
|
-
}
|
|
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).
|
|
5534
3721
|
|
|
5535
3722
|
if (url.pathname === '/api/canvas/ax/mode' && req.method === 'GET') {
|
|
5536
3723
|
return handleAxModeList();
|
|
5537
3724
|
}
|
|
5538
3725
|
|
|
5539
|
-
|
|
5540
|
-
|
|
5541
|
-
}
|
|
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).
|
|
5542
3728
|
|
|
5543
|
-
|
|
5544
|
-
|
|
5545
|
-
url.pathname.slice('/api/canvas/ax/mode/'.length, -'/resolve'.length),
|
|
5546
|
-
);
|
|
5547
|
-
return handleAxModeResolve(req, modeId);
|
|
5548
|
-
}
|
|
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).
|
|
5549
3731
|
|
|
5550
3732
|
if (url.pathname === '/api/canvas/ax/command' && req.method === 'GET') {
|
|
5551
3733
|
return handleAxCommandList();
|
|
5552
3734
|
}
|
|
5553
3735
|
|
|
5554
|
-
|
|
5555
|
-
|
|
5556
|
-
}
|
|
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.
|
|
5557
3738
|
|
|
5558
3739
|
if (url.pathname === '/api/canvas/ax/policy' && req.method === 'GET') {
|
|
5559
3740
|
return handleAxPolicyGet();
|
|
5560
3741
|
}
|
|
5561
3742
|
|
|
5562
|
-
|
|
5563
|
-
return handleAxPolicySet(req);
|
|
5564
|
-
}
|
|
5565
|
-
|
|
5566
|
-
// Spatial context API
|
|
5567
|
-
if (url.pathname === '/api/canvas/spatial-context' && req.method === 'GET') {
|
|
5568
|
-
const layout = canvasState.getLayout();
|
|
5569
|
-
const spatial = buildSpatialContext(layout.nodes, layout.edges, canvasState.contextPinnedNodeIds, layout.annotations);
|
|
5570
|
-
return responseJson(spatial);
|
|
5571
|
-
}
|
|
5572
|
-
|
|
5573
|
-
// Search API
|
|
5574
|
-
if (url.pathname === '/api/canvas/search' && req.method === 'GET') {
|
|
5575
|
-
const q = url.searchParams.get('q') ?? '';
|
|
5576
|
-
if (!q.trim()) {
|
|
5577
|
-
return responseJson({ results: [], query: q });
|
|
5578
|
-
}
|
|
5579
|
-
const results = searchNodes(canvasState.getLayout().nodes, q);
|
|
5580
|
-
return responseJson({ results, query: q });
|
|
5581
|
-
}
|
|
5582
|
-
|
|
5583
|
-
// Group API
|
|
5584
|
-
if (url.pathname === '/api/canvas/group' && req.method === 'POST') {
|
|
5585
|
-
return handleCanvasCreateGroup(req);
|
|
5586
|
-
}
|
|
5587
|
-
|
|
5588
|
-
if (url.pathname === '/api/canvas/group/add' && req.method === 'POST') {
|
|
5589
|
-
return handleCanvasGroupNodes(req);
|
|
5590
|
-
}
|
|
5591
|
-
|
|
5592
|
-
if (url.pathname === '/api/canvas/group/ungroup' && req.method === 'POST') {
|
|
5593
|
-
return handleCanvasUngroupNodes(req);
|
|
5594
|
-
}
|
|
5595
|
-
|
|
5596
|
-
// Arrange / Focus / Clear API (for agent CLI)
|
|
5597
|
-
if (url.pathname === '/api/canvas/arrange' && req.method === 'POST') {
|
|
5598
|
-
return handleCanvasArrange(req);
|
|
5599
|
-
}
|
|
5600
|
-
|
|
5601
|
-
if (url.pathname === '/api/canvas/focus' && req.method === 'POST') {
|
|
5602
|
-
return handleCanvasFocus(req);
|
|
5603
|
-
}
|
|
5604
|
-
|
|
5605
|
-
if (url.pathname === '/api/canvas/fit' && req.method === 'POST') {
|
|
5606
|
-
return handleCanvasFit(req);
|
|
5607
|
-
}
|
|
5608
|
-
|
|
5609
|
-
if (url.pathname === '/api/canvas/clear' && req.method === 'POST') {
|
|
5610
|
-
for (const node of canvasState.getLayout().nodes) {
|
|
5611
|
-
closeNodeAppSession(node);
|
|
5612
|
-
}
|
|
5613
|
-
clearCanvas();
|
|
5614
|
-
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
5615
|
-
return responseJson({ ok: true });
|
|
5616
|
-
}
|
|
3743
|
+
// POST /api/canvas/ax/policy migrated to the operation registry (plan-007 Slice B.1).
|
|
5617
3744
|
|
|
5618
3745
|
// Code graph API
|
|
5619
3746
|
if (url.pathname === '/api/canvas/code-graph' && req.method === 'GET') {
|
|
@@ -5621,54 +3748,10 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
5621
3748
|
return responseJson(summary);
|
|
5622
3749
|
}
|
|
5623
3750
|
|
|
5624
|
-
if (url.pathname === '/api/canvas/json-render' && req.method === 'POST') {
|
|
5625
|
-
return handleCanvasAddJsonRender(req);
|
|
5626
|
-
}
|
|
5627
|
-
|
|
5628
|
-
if (url.pathname === '/api/canvas/json-render/stream' && req.method === 'POST') {
|
|
5629
|
-
return handleJsonRenderStream(req);
|
|
5630
|
-
}
|
|
5631
|
-
|
|
5632
|
-
if (url.pathname === '/api/canvas/graph' && req.method === 'POST') {
|
|
5633
|
-
return handleCanvasAddGraph(req);
|
|
5634
|
-
}
|
|
5635
|
-
|
|
5636
3751
|
if (url.pathname === '/api/canvas/prompt' && req.method === 'POST') {
|
|
5637
3752
|
return handleCanvasPrompt(req);
|
|
5638
3753
|
}
|
|
5639
3754
|
|
|
5640
|
-
// Undo/Redo/History API
|
|
5641
|
-
if (url.pathname === '/api/canvas/undo' && req.method === 'POST') {
|
|
5642
|
-
const entry = mutationHistory.undo();
|
|
5643
|
-
if (!entry) return responseJson({ ok: false, description: 'Nothing to undo' });
|
|
5644
|
-
await syncCanvasRuntimeBackends();
|
|
5645
|
-
emitPrimaryWorkbenchEvent('canvas-viewport-update', { viewport: canvasState.viewport });
|
|
5646
|
-
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
5647
|
-
return responseJson({ ok: true, description: `Undid: ${entry.description}` });
|
|
5648
|
-
}
|
|
5649
|
-
|
|
5650
|
-
if (url.pathname === '/api/canvas/redo' && req.method === 'POST') {
|
|
5651
|
-
const entry = mutationHistory.redo();
|
|
5652
|
-
if (!entry) return responseJson({ ok: false, description: 'Nothing to redo' });
|
|
5653
|
-
await syncCanvasRuntimeBackends();
|
|
5654
|
-
emitPrimaryWorkbenchEvent('canvas-viewport-update', { viewport: canvasState.viewport });
|
|
5655
|
-
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
5656
|
-
return responseJson({ ok: true, description: `Redid: ${entry.description}` });
|
|
5657
|
-
}
|
|
5658
|
-
|
|
5659
|
-
if (url.pathname === '/api/canvas/history' && req.method === 'GET') {
|
|
5660
|
-
return responseJson({
|
|
5661
|
-
text: mutationHistory.toHumanReadable(),
|
|
5662
|
-
entries: mutationHistory.getSummaries(),
|
|
5663
|
-
canUndo: mutationHistory.canUndo(),
|
|
5664
|
-
canRedo: mutationHistory.canRedo(),
|
|
5665
|
-
});
|
|
5666
|
-
}
|
|
5667
|
-
|
|
5668
|
-
if (url.pathname === '/api/canvas/validate' && req.method === 'GET') {
|
|
5669
|
-
return handleCanvasValidate();
|
|
5670
|
-
}
|
|
5671
|
-
|
|
5672
3755
|
if (url.pathname === '/api/ext-app/call-tool' && req.method === 'POST') {
|
|
5673
3756
|
return handleExtAppCallTool(req);
|
|
5674
3757
|
}
|