pmx-canvas 0.1.19 → 0.1.21
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 +159 -0
- package/Readme.md +19 -6
- package/dist/canvas/global.css +123 -2
- package/dist/canvas/index.js +103 -68
- package/dist/json-render/index.js +109 -109
- package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
- package/dist/types/client/icons.d.ts +2 -0
- package/dist/types/client/nodes/HtmlNode.d.ts +12 -1
- package/dist/types/client/state/canvas-store.d.ts +2 -0
- package/dist/types/client/types.d.ts +3 -2
- package/dist/types/json-render/charts/components.d.ts +5 -1
- package/dist/types/json-render/renderer/index.d.ts +1 -0
- package/dist/types/json-render/server.d.ts +1 -0
- package/dist/types/mcp/canvas-access.d.ts +3 -0
- package/dist/types/server/canvas-operations.d.ts +4 -0
- package/dist/types/server/canvas-schema.d.ts +19 -3
- package/dist/types/server/canvas-serialization.d.ts +1 -0
- package/dist/types/server/canvas-state.d.ts +6 -2
- package/dist/types/server/html-node-summary.d.ts +2 -0
- package/dist/types/server/html-primitives.d.ts +42 -0
- package/dist/types/server/index.d.ts +26 -0
- package/docs/cli.md +4 -1
- package/docs/http-api.md +11 -1
- package/docs/mcp.md +10 -4
- package/docs/node-types.md +54 -4
- package/docs/screenshot.png +0 -0
- package/docs/sdk.md +12 -0
- package/package.json +1 -1
- package/skills/pmx-canvas/SKILL.md +17 -3
- package/skills/pmx-canvas/references/html-primitives.md +132 -0
- package/src/cli/agent.ts +159 -5
- package/src/cli/index.ts +1 -1
- package/src/client/App.tsx +21 -2
- package/src/client/canvas/AnnotationLayer.tsx +33 -12
- package/src/client/canvas/CanvasViewport.tsx +88 -7
- package/src/client/canvas/CommandPalette.tsx +2 -2
- package/src/client/canvas/ContextMenu.tsx +2 -2
- package/src/client/canvas/ExpandedNodeOverlay.tsx +112 -3
- package/src/client/canvas/auto-fit.ts +5 -1
- package/src/client/icons.tsx +13 -0
- package/src/client/nodes/HtmlNode.tsx +125 -13
- package/src/client/nodes/McpAppNode.tsx +12 -4
- package/src/client/state/canvas-store.ts +15 -5
- package/src/client/state/sse-bridge.ts +5 -4
- package/src/client/theme/global.css +123 -2
- package/src/client/types.ts +2 -1
- package/src/json-render/charts/components.tsx +41 -7
- package/src/json-render/charts/extra-components.tsx +13 -12
- package/src/json-render/renderer/index.tsx +1 -0
- package/src/json-render/server.ts +3 -1
- package/src/mcp/canvas-access.ts +54 -1
- package/src/mcp/server.ts +98 -28
- package/src/server/agent-context.ts +39 -0
- package/src/server/canvas-operations.ts +99 -38
- package/src/server/canvas-provenance.ts +8 -6
- package/src/server/canvas-schema.ts +94 -3
- package/src/server/canvas-serialization.ts +16 -4
- package/src/server/canvas-state.ts +9 -4
- package/src/server/demo-state.json +1143 -0
- package/src/server/demo.ts +25 -777
- package/src/server/html-node-summary.ts +141 -0
- package/src/server/html-primitives.ts +1300 -0
- package/src/server/index.ts +63 -3
- package/src/server/server.ts +154 -17
- package/src/server/spatial-analysis.ts +5 -3
package/src/server/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { recomputeCodeGraph, buildCodeGraphSummary, formatCodeGraph } from './co
|
|
|
10
10
|
import {
|
|
11
11
|
addCanvasNode,
|
|
12
12
|
addCanvasEdge,
|
|
13
|
+
MARKDOWN_NODE_DEFAULT_SIZE,
|
|
13
14
|
applyCanvasNodeUpdates,
|
|
14
15
|
arrangeCanvasNodes,
|
|
15
16
|
clearCanvas,
|
|
@@ -39,6 +40,8 @@ import {
|
|
|
39
40
|
} from './canvas-operations.js';
|
|
40
41
|
import { validateCanvasLayout } from './canvas-validation.js';
|
|
41
42
|
import { describeCanvasSchema, validateStructuredCanvasPayload } from './canvas-schema.js';
|
|
43
|
+
import { buildHtmlPrimitive, getHtmlPrimitiveSemanticMetadata, isHtmlPrimitiveKind, listHtmlPrimitiveDescriptors } from './html-primitives.js';
|
|
44
|
+
import type { HtmlPrimitiveKind } from './html-primitives.js';
|
|
42
45
|
import {
|
|
43
46
|
buildWebArtifactOnCanvas,
|
|
44
47
|
type WebArtifactBuildInput,
|
|
@@ -178,8 +181,8 @@ export class PmxCanvas extends EventEmitter {
|
|
|
178
181
|
}
|
|
179
182
|
const { id, needsCodeGraphRecompute } = addCanvasNode({
|
|
180
183
|
...input,
|
|
181
|
-
defaultWidth: 360,
|
|
182
|
-
defaultHeight: 200,
|
|
184
|
+
defaultWidth: input.type === 'markdown' ? MARKDOWN_NODE_DEFAULT_SIZE.width : 360,
|
|
185
|
+
defaultHeight: input.type === 'markdown' ? MARKDOWN_NODE_DEFAULT_SIZE.height : 200,
|
|
183
186
|
fileMode: 'path',
|
|
184
187
|
...(input.strictSize ? { strictSize: true } : {}),
|
|
185
188
|
});
|
|
@@ -644,6 +647,13 @@ export class PmxCanvas extends EventEmitter {
|
|
|
644
647
|
addHtmlNode(input: {
|
|
645
648
|
html: string;
|
|
646
649
|
title?: string;
|
|
650
|
+
summary?: string;
|
|
651
|
+
agentSummary?: string;
|
|
652
|
+
description?: string;
|
|
653
|
+
presentation?: boolean;
|
|
654
|
+
slideTitles?: string[];
|
|
655
|
+
embeddedNodeIds?: string[];
|
|
656
|
+
embeddedUrls?: string[];
|
|
647
657
|
x?: number;
|
|
648
658
|
y?: number;
|
|
649
659
|
width?: number;
|
|
@@ -653,7 +663,16 @@ export class PmxCanvas extends EventEmitter {
|
|
|
653
663
|
const { id } = addCanvasNode({
|
|
654
664
|
type: 'html',
|
|
655
665
|
...(typeof input.title === 'string' ? { title: input.title } : {}),
|
|
656
|
-
data: {
|
|
666
|
+
data: {
|
|
667
|
+
html: input.html,
|
|
668
|
+
...(typeof input.summary === 'string' ? { summary: input.summary } : {}),
|
|
669
|
+
...(typeof input.agentSummary === 'string' ? { agentSummary: input.agentSummary } : {}),
|
|
670
|
+
...(typeof input.description === 'string' ? { description: input.description } : {}),
|
|
671
|
+
...(input.presentation === true ? { presentation: true } : {}),
|
|
672
|
+
...(Array.isArray(input.slideTitles) ? { slideTitles: input.slideTitles } : {}),
|
|
673
|
+
...(Array.isArray(input.embeddedNodeIds) ? { embeddedNodeIds: input.embeddedNodeIds } : {}),
|
|
674
|
+
...(Array.isArray(input.embeddedUrls) ? { embeddedUrls: input.embeddedUrls } : {}),
|
|
675
|
+
},
|
|
657
676
|
...(typeof input.x === 'number' ? { x: input.x } : {}),
|
|
658
677
|
...(typeof input.y === 'number' ? { y: input.y } : {}),
|
|
659
678
|
...(typeof input.width === 'number' ? { width: input.width } : {}),
|
|
@@ -666,6 +685,45 @@ export class PmxCanvas extends EventEmitter {
|
|
|
666
685
|
return id;
|
|
667
686
|
}
|
|
668
687
|
|
|
688
|
+
addHtmlPrimitive(input: {
|
|
689
|
+
kind: HtmlPrimitiveKind;
|
|
690
|
+
title?: string;
|
|
691
|
+
data?: Record<string, unknown>;
|
|
692
|
+
x?: number;
|
|
693
|
+
y?: number;
|
|
694
|
+
width?: number;
|
|
695
|
+
height?: number;
|
|
696
|
+
strictSize?: boolean;
|
|
697
|
+
}): { id: string; kind: HtmlPrimitiveKind; title: string; htmlBytes: number } {
|
|
698
|
+
const built = buildHtmlPrimitive({
|
|
699
|
+
kind: input.kind,
|
|
700
|
+
...(typeof input.title === 'string' ? { title: input.title } : {}),
|
|
701
|
+
...(input.data ? { data: input.data } : {}),
|
|
702
|
+
});
|
|
703
|
+
const { id } = addCanvasNode({
|
|
704
|
+
type: 'html',
|
|
705
|
+
title: built.title,
|
|
706
|
+
data: {
|
|
707
|
+
html: built.html,
|
|
708
|
+
htmlPrimitive: built.kind,
|
|
709
|
+
primitiveData: built.data,
|
|
710
|
+
description: built.summary,
|
|
711
|
+
agentSummary: typeof input.data?.agentSummary === 'string' ? input.data.agentSummary : built.summary,
|
|
712
|
+
...(typeof input.data?.summary === 'string' ? { summary: input.data.summary } : {}),
|
|
713
|
+
...getHtmlPrimitiveSemanticMetadata(built.data),
|
|
714
|
+
},
|
|
715
|
+
...(typeof input.x === 'number' ? { x: input.x } : {}),
|
|
716
|
+
...(typeof input.y === 'number' ? { y: input.y } : {}),
|
|
717
|
+
...(typeof input.width === 'number' ? { width: input.width } : {}),
|
|
718
|
+
...(typeof input.height === 'number' ? { height: input.height } : {}),
|
|
719
|
+
...(input.strictSize ? { strictSize: true } : {}),
|
|
720
|
+
defaultWidth: built.defaultSize.width,
|
|
721
|
+
defaultHeight: built.defaultSize.height,
|
|
722
|
+
});
|
|
723
|
+
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
724
|
+
return { id, kind: built.kind, title: built.title, htmlBytes: Buffer.byteLength(built.html, 'utf-8') };
|
|
725
|
+
}
|
|
726
|
+
|
|
669
727
|
addGraphNode(input: GraphNodeInput): { id: string; url: string; spec: JsonRenderSpec } {
|
|
670
728
|
const result = createCanvasGraphNode(input);
|
|
671
729
|
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
@@ -747,6 +805,7 @@ export type { SpatialCluster, SpatialContext, SpatialNeighbor, NodeSpatialInfo }
|
|
|
747
805
|
export { mutationHistory, diffLayouts, formatDiff } from './mutation-history.js';
|
|
748
806
|
export { recomputeCodeGraph, buildCodeGraphSummary, formatCodeGraph } from './code-graph.js';
|
|
749
807
|
export { describeCanvasSchema, validateStructuredCanvasPayload } from './canvas-schema.js';
|
|
808
|
+
export { buildHtmlPrimitive, getHtmlPrimitiveSemanticMetadata, isHtmlPrimitiveKind, listHtmlPrimitiveDescriptors } from './html-primitives.js';
|
|
750
809
|
export {
|
|
751
810
|
buildWebArtifactOnCanvas,
|
|
752
811
|
executeWebArtifactBuild,
|
|
@@ -771,4 +830,5 @@ export type {
|
|
|
771
830
|
WebArtifactCanvasOpenResult,
|
|
772
831
|
} from './web-artifacts.js';
|
|
773
832
|
export type { GraphNodeInput, JsonRenderNodeInput, JsonRenderSpec } from '../json-render/server.js';
|
|
833
|
+
export type { HtmlPrimitiveKind, HtmlPrimitiveDescriptor, HtmlPrimitiveInput, HtmlPrimitiveBuildResult } from './html-primitives.js';
|
|
774
834
|
export { traceManager } from './trace-manager.js';
|
package/src/server/server.ts
CHANGED
|
@@ -79,6 +79,7 @@ import { validateLocalImageFile } from './image-source.js';
|
|
|
79
79
|
import {
|
|
80
80
|
addCanvasNode,
|
|
81
81
|
addCanvasEdge,
|
|
82
|
+
MARKDOWN_NODE_DEFAULT_SIZE,
|
|
82
83
|
applyCanvasNodeUpdates,
|
|
83
84
|
buildStructuredNodeUpdate,
|
|
84
85
|
arrangeCanvasNodes,
|
|
@@ -110,6 +111,7 @@ import {
|
|
|
110
111
|
} from './canvas-operations.js';
|
|
111
112
|
import { validateCanvasLayout } from './canvas-validation.js';
|
|
112
113
|
import { describeCanvasSchema, validateStructuredCanvasPayload } from './canvas-schema.js';
|
|
114
|
+
import { buildHtmlPrimitive, getHtmlPrimitiveSemanticMetadata, isHtmlPrimitiveKind } from './html-primitives.js';
|
|
113
115
|
import {
|
|
114
116
|
EXCALIDRAW_READ_CHECKPOINT_TOOL,
|
|
115
117
|
EXCALIDRAW_SAVE_CHECKPOINT_TOOL,
|
|
@@ -129,6 +131,7 @@ import {
|
|
|
129
131
|
WEBPAGE_NODE_DEFAULT_SIZE,
|
|
130
132
|
normalizeWebpageUrl,
|
|
131
133
|
} from './webpage-node.js';
|
|
134
|
+
import type { JsonRenderSpec } from '../json-render/server.js';
|
|
132
135
|
|
|
133
136
|
const DEFAULT_HOST = '127.0.0.1';
|
|
134
137
|
const DEFAULT_PORT = 4313;
|
|
@@ -147,6 +150,40 @@ const canvasThemeSetting = (['dark', 'light', 'high-contrast'].includes(process.
|
|
|
147
150
|
: 'dark');
|
|
148
151
|
let lastWorkbenchContextCardsEnvelope: Record<string, unknown> | null = null;
|
|
149
152
|
|
|
153
|
+
function normalizeGraphViewerSpec(
|
|
154
|
+
node: { type: string; data: Record<string, unknown> },
|
|
155
|
+
spec: JsonRenderSpec,
|
|
156
|
+
display: string | null,
|
|
157
|
+
): JsonRenderSpec {
|
|
158
|
+
if (node.type !== 'graph') return spec;
|
|
159
|
+
const graphConfig = node.data.graphConfig;
|
|
160
|
+
if (
|
|
161
|
+
display !== 'expanded' &&
|
|
162
|
+
graphConfig &&
|
|
163
|
+
typeof graphConfig === 'object' &&
|
|
164
|
+
typeof (graphConfig as Record<string, unknown>).height === 'number'
|
|
165
|
+
) {
|
|
166
|
+
return spec;
|
|
167
|
+
}
|
|
168
|
+
const chart = spec.elements.chart;
|
|
169
|
+
if (!chart || typeof chart !== 'object') return spec;
|
|
170
|
+
const chartRecord = chart as Record<string, unknown>;
|
|
171
|
+
const props = chartRecord.props;
|
|
172
|
+
if (!props || typeof props !== 'object' || typeof (props as Record<string, unknown>).height !== 'number') return spec;
|
|
173
|
+
const nextProps = { ...(props as Record<string, unknown>) };
|
|
174
|
+
delete nextProps.height;
|
|
175
|
+
return {
|
|
176
|
+
...spec,
|
|
177
|
+
elements: {
|
|
178
|
+
...spec.elements,
|
|
179
|
+
chart: {
|
|
180
|
+
...chartRecord,
|
|
181
|
+
props: nextProps,
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
150
187
|
export interface PrimaryWorkbenchEventPayload {
|
|
151
188
|
[key: string]: unknown;
|
|
152
189
|
}
|
|
@@ -1238,6 +1275,15 @@ function annotationBounds(points: CanvasAnnotation['points']): CanvasAnnotation[
|
|
|
1238
1275
|
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
|
1239
1276
|
}
|
|
1240
1277
|
|
|
1278
|
+
function textAnnotationBounds(point: CanvasAnnotation['points'][number], text: string, width: number): CanvasAnnotation['bounds'] {
|
|
1279
|
+
return {
|
|
1280
|
+
x: point.x,
|
|
1281
|
+
y: point.y - width,
|
|
1282
|
+
width: Math.max(width, text.length * width * 0.62),
|
|
1283
|
+
height: width * 1.2,
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1241
1287
|
function parseAnnotationPoints(value: unknown): CanvasAnnotation['points'] {
|
|
1242
1288
|
if (!Array.isArray(value)) return [];
|
|
1243
1289
|
return value
|
|
@@ -1253,31 +1299,41 @@ function parseAnnotationPoints(value: unknown): CanvasAnnotation['points'] {
|
|
|
1253
1299
|
|
|
1254
1300
|
async function handleCanvasAddAnnotation(req: Request): Promise<Response> {
|
|
1255
1301
|
const body = await readJson(req);
|
|
1302
|
+
const type = body.type === 'text' ? 'text' : 'freehand';
|
|
1256
1303
|
const points = parseAnnotationPoints(body.points);
|
|
1257
|
-
if (points.length < 2) {
|
|
1258
|
-
return responseJson({ ok: false, error: 'Annotation requires at least two valid points.' }, 400);
|
|
1304
|
+
if (points.length < (type === 'text' ? 1 : 2)) {
|
|
1305
|
+
return responseJson({ ok: false, error: type === 'text' ? 'Text annotation requires a valid point.' : 'Annotation requires at least two valid points.' }, 400);
|
|
1259
1306
|
}
|
|
1260
1307
|
|
|
1308
|
+
const defaultWidth = type === 'text' ? 24 : 4;
|
|
1309
|
+
const maxWidth = type === 'text' ? 96 : 24;
|
|
1261
1310
|
const width = typeof body.width === 'number' && Number.isFinite(body.width)
|
|
1262
|
-
? Math.min(
|
|
1263
|
-
:
|
|
1311
|
+
? Math.min(maxWidth, Math.max(1, body.width))
|
|
1312
|
+
: defaultWidth;
|
|
1264
1313
|
const color = typeof body.color === 'string' && (body.color === 'currentColor' || /^#[0-9a-fA-F]{6}$/.test(body.color))
|
|
1265
1314
|
? body.color
|
|
1266
1315
|
: 'currentColor';
|
|
1267
1316
|
const label = typeof body.label === 'string' && body.label.trim().length > 0
|
|
1268
1317
|
? body.label.trim().slice(0, 160)
|
|
1269
1318
|
: undefined;
|
|
1319
|
+
const text = type === 'text' && typeof body.text === 'string' && body.text.trim().length > 0
|
|
1320
|
+
? body.text.trim().slice(0, 240)
|
|
1321
|
+
: undefined;
|
|
1322
|
+
if (type === 'text' && !text) {
|
|
1323
|
+
return responseJson({ ok: false, error: 'Text annotation requires text.' }, 400);
|
|
1324
|
+
}
|
|
1270
1325
|
const id = typeof body.id === 'string' && body.id.trim().length > 0
|
|
1271
1326
|
? body.id.trim().slice(0, 120)
|
|
1272
1327
|
: `ann-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1273
1328
|
const annotation: CanvasAnnotation = {
|
|
1274
1329
|
id,
|
|
1275
|
-
type
|
|
1330
|
+
type,
|
|
1276
1331
|
points,
|
|
1277
|
-
bounds: annotationBounds(points),
|
|
1332
|
+
bounds: type === 'text' ? textAnnotationBounds(points[0]!, text!, width) : annotationBounds(points),
|
|
1278
1333
|
color,
|
|
1279
1334
|
width,
|
|
1280
|
-
...(
|
|
1335
|
+
...(text ? { text } : {}),
|
|
1336
|
+
...(label ?? text ? { label: label ?? text } : {}),
|
|
1281
1337
|
createdAt: new Date().toISOString(),
|
|
1282
1338
|
};
|
|
1283
1339
|
|
|
@@ -1401,6 +1457,9 @@ async function handleCanvasAddNode(req: Request): Promise<Response> {
|
|
|
1401
1457
|
error: 'Node type "web-artifact" is created via POST /api/canvas/web-artifact with appTsx + title.',
|
|
1402
1458
|
}, 400);
|
|
1403
1459
|
}
|
|
1460
|
+
if (type === 'html-primitive') {
|
|
1461
|
+
return createCanvasHtmlPrimitiveNode(body);
|
|
1462
|
+
}
|
|
1404
1463
|
return responseJson({ ok: false, error: `Invalid node type: "${type}".` }, 400);
|
|
1405
1464
|
}
|
|
1406
1465
|
|
|
@@ -1408,6 +1467,10 @@ async function handleCanvasAddNode(req: Request): Promise<Response> {
|
|
|
1408
1467
|
return createCanvasWebpageNode(body);
|
|
1409
1468
|
}
|
|
1410
1469
|
|
|
1470
|
+
if (type === 'html' && (typeof body.primitive === 'string' || typeof body.kind === 'string')) {
|
|
1471
|
+
return createCanvasHtmlPrimitiveNode(body);
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1411
1474
|
const extraData = body.data && typeof body.data === 'object' && !Array.isArray(body.data)
|
|
1412
1475
|
? body.data as Record<string, unknown>
|
|
1413
1476
|
: undefined;
|
|
@@ -1424,8 +1487,18 @@ async function handleCanvasAddNode(req: Request): Promise<Response> {
|
|
|
1424
1487
|
: body.content;
|
|
1425
1488
|
// For html nodes, accept top-level `html` field and merge into data so callers
|
|
1426
1489
|
// can POST { type: 'html', title, html } without nesting under `data`.
|
|
1427
|
-
const htmlMergedData = type === 'html'
|
|
1428
|
-
? {
|
|
1490
|
+
const htmlMergedData = type === 'html'
|
|
1491
|
+
? {
|
|
1492
|
+
...(extraData ?? {}),
|
|
1493
|
+
...(typeof body.html === 'string' ? { html: body.html } : {}),
|
|
1494
|
+
...(typeof body.summary === 'string' ? { summary: body.summary } : {}),
|
|
1495
|
+
...(typeof body.agentSummary === 'string' ? { agentSummary: body.agentSummary } : {}),
|
|
1496
|
+
...(typeof body.description === 'string' ? { description: body.description } : {}),
|
|
1497
|
+
...(body.presentation === true ? { presentation: true } : {}),
|
|
1498
|
+
...(Array.isArray(body.slideTitles) ? { slideTitles: body.slideTitles } : {}),
|
|
1499
|
+
...(Array.isArray(body.embeddedNodeIds) ? { embeddedNodeIds: body.embeddedNodeIds } : {}),
|
|
1500
|
+
...(Array.isArray(body.embeddedUrls) ? { embeddedUrls: body.embeddedUrls } : {}),
|
|
1501
|
+
}
|
|
1429
1502
|
: extraData;
|
|
1430
1503
|
let added: ReturnType<typeof addCanvasNode>;
|
|
1431
1504
|
const geometry = resolveCreateGeometry(body);
|
|
@@ -1434,7 +1507,7 @@ async function handleCanvasAddNode(req: Request): Promise<Response> {
|
|
|
1434
1507
|
type: type as CanvasNodeState['type'],
|
|
1435
1508
|
...(typeof body.title === 'string' ? { title: body.title } : {}),
|
|
1436
1509
|
...(typeof content === 'string' ? { content } : {}),
|
|
1437
|
-
...(htmlMergedData ? { data: htmlMergedData } : {}),
|
|
1510
|
+
...(htmlMergedData && Object.keys(htmlMergedData).length > 0 ? { data: htmlMergedData } : {}),
|
|
1438
1511
|
...(type === 'trace' && typeof body.toolName === 'string' ? { toolName: body.toolName } : {}),
|
|
1439
1512
|
...(type === 'trace' && typeof body.category === 'string' ? { category: body.category } : {}),
|
|
1440
1513
|
...(type === 'trace' && typeof body.status === 'string' ? { status: body.status } : {}),
|
|
@@ -1443,8 +1516,8 @@ async function handleCanvasAddNode(req: Request): Promise<Response> {
|
|
|
1443
1516
|
...(type === 'trace' && typeof body.error === 'string' ? { error: body.error } : {}),
|
|
1444
1517
|
...(body.strictSize === true ? { strictSize: true } : {}),
|
|
1445
1518
|
...geometry,
|
|
1446
|
-
defaultWidth: type === 'html' ? 720 : 360,
|
|
1447
|
-
defaultHeight: type === 'html' ? 640 : 200,
|
|
1519
|
+
defaultWidth: type === 'html' ? 720 : type === 'markdown' ? MARKDOWN_NODE_DEFAULT_SIZE.width : 360,
|
|
1520
|
+
defaultHeight: type === 'html' ? 640 : type === 'markdown' ? MARKDOWN_NODE_DEFAULT_SIZE.height : 200,
|
|
1448
1521
|
fileMode: 'auto',
|
|
1449
1522
|
});
|
|
1450
1523
|
} catch (error) {
|
|
@@ -1462,6 +1535,47 @@ async function handleCanvasAddNode(req: Request): Promise<Response> {
|
|
|
1462
1535
|
return responseJson(buildNodeResponse(node));
|
|
1463
1536
|
}
|
|
1464
1537
|
|
|
1538
|
+
function createCanvasHtmlPrimitiveNode(body: Record<string, unknown>): Response {
|
|
1539
|
+
const rawKind = typeof body.primitive === 'string' ? body.primitive : body.kind;
|
|
1540
|
+
if (typeof rawKind !== 'string' || !isHtmlPrimitiveKind(rawKind)) {
|
|
1541
|
+
return responseJson({ ok: false, error: `Unknown HTML primitive: ${String(rawKind)}.` }, 400);
|
|
1542
|
+
}
|
|
1543
|
+
const data = isRecord(body.data) ? body.data : {};
|
|
1544
|
+
const built = buildHtmlPrimitive({
|
|
1545
|
+
kind: rawKind,
|
|
1546
|
+
...(typeof body.title === 'string' ? { title: body.title } : {}),
|
|
1547
|
+
data,
|
|
1548
|
+
});
|
|
1549
|
+
const geometry = resolveCreateGeometry(body);
|
|
1550
|
+
const { node } = addCanvasNode({
|
|
1551
|
+
type: 'html',
|
|
1552
|
+
title: built.title,
|
|
1553
|
+
data: {
|
|
1554
|
+
html: built.html,
|
|
1555
|
+
htmlPrimitive: built.kind,
|
|
1556
|
+
primitiveData: built.data,
|
|
1557
|
+
description: built.summary,
|
|
1558
|
+
agentSummary: typeof data.agentSummary === 'string' ? data.agentSummary : built.summary,
|
|
1559
|
+
...(typeof data.summary === 'string' ? { summary: data.summary } : {}),
|
|
1560
|
+
...getHtmlPrimitiveSemanticMetadata(built.data),
|
|
1561
|
+
},
|
|
1562
|
+
...(body.strictSize === true ? { strictSize: true } : {}),
|
|
1563
|
+
...geometry,
|
|
1564
|
+
defaultWidth: built.defaultSize.width,
|
|
1565
|
+
defaultHeight: built.defaultSize.height,
|
|
1566
|
+
});
|
|
1567
|
+
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
1568
|
+
return responseJson({
|
|
1569
|
+
...buildNodeResponse(node),
|
|
1570
|
+
primitive: {
|
|
1571
|
+
kind: built.kind,
|
|
1572
|
+
title: built.title,
|
|
1573
|
+
htmlBytes: Buffer.byteLength(built.html, 'utf-8'),
|
|
1574
|
+
defaultSize: built.defaultSize,
|
|
1575
|
+
},
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1465
1579
|
// ── Group operations ─────────────────────────────────────────
|
|
1466
1580
|
async function handleCanvasCreateGroup(req: Request): Promise<Response> {
|
|
1467
1581
|
const body = await readJson(req);
|
|
@@ -1799,8 +1913,8 @@ function handleCanvasDescribeSchema(): Response {
|
|
|
1799
1913
|
async function handleCanvasValidateSpec(req: Request): Promise<Response> {
|
|
1800
1914
|
const body = await readJson(req);
|
|
1801
1915
|
const rawType = typeof body.type === 'string' ? body.type.trim() : '';
|
|
1802
|
-
if (rawType !== 'json-render' && rawType !== 'graph') {
|
|
1803
|
-
return responseJson({ ok: false, error: 'Validation type must be "json-render" or "
|
|
1916
|
+
if (rawType !== 'json-render' && rawType !== 'graph' && rawType !== 'html-primitive') {
|
|
1917
|
+
return responseJson({ ok: false, error: 'Validation type must be "json-render", "graph", or "html-primitive".' }, 400);
|
|
1804
1918
|
}
|
|
1805
1919
|
|
|
1806
1920
|
try {
|
|
@@ -1815,6 +1929,23 @@ async function handleCanvasValidateSpec(req: Request): Promise<Response> {
|
|
|
1815
1929
|
}));
|
|
1816
1930
|
}
|
|
1817
1931
|
|
|
1932
|
+
if (rawType === 'html-primitive') {
|
|
1933
|
+
const kind = typeof body.kind === 'string'
|
|
1934
|
+
? body.kind
|
|
1935
|
+
: typeof body.primitive === 'string'
|
|
1936
|
+
? body.primitive
|
|
1937
|
+
: '';
|
|
1938
|
+
const data = isRecord(body.data) ? body.data : {};
|
|
1939
|
+
return responseJson(validateStructuredCanvasPayload({
|
|
1940
|
+
type: 'html-primitive',
|
|
1941
|
+
primitive: {
|
|
1942
|
+
kind,
|
|
1943
|
+
...(typeof body.title === 'string' ? { title: body.title } : {}),
|
|
1944
|
+
data,
|
|
1945
|
+
},
|
|
1946
|
+
}));
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1818
1949
|
const data = parseGraphPayloadData(body.data);
|
|
1819
1950
|
if (!data) {
|
|
1820
1951
|
return responseJson({ ok: false, error: 'Graph validation requires a data array.' }, 400);
|
|
@@ -1976,10 +2107,15 @@ async function handleJsonRenderView(url: URL): Promise<Response> {
|
|
|
1976
2107
|
return responseText('json-render node not found', 404);
|
|
1977
2108
|
}
|
|
1978
2109
|
|
|
1979
|
-
const
|
|
1980
|
-
if (!
|
|
2110
|
+
const rawSpec = node.data.spec;
|
|
2111
|
+
if (!rawSpec || typeof rawSpec !== 'object') {
|
|
1981
2112
|
return responseText('json-render spec missing', 404);
|
|
1982
2113
|
}
|
|
2114
|
+
const spec = normalizeGraphViewerSpec(
|
|
2115
|
+
{ type: node.type, data: node.data },
|
|
2116
|
+
rawSpec as { root: string; elements: Record<string, unknown>; state?: Record<string, unknown> },
|
|
2117
|
+
url.searchParams.get('display'),
|
|
2118
|
+
);
|
|
1983
2119
|
|
|
1984
2120
|
const themeValue = url.searchParams.get('theme');
|
|
1985
2121
|
const theme =
|
|
@@ -1989,8 +2125,9 @@ async function handleJsonRenderView(url: URL): Promise<Response> {
|
|
|
1989
2125
|
const title = (node.data.title as string) || node.id;
|
|
1990
2126
|
const html = await buildJsonRenderViewerHtml({
|
|
1991
2127
|
title,
|
|
1992
|
-
spec
|
|
2128
|
+
spec,
|
|
1993
2129
|
...(theme ? { theme } : {}),
|
|
2130
|
+
...(url.searchParams.get('display') === 'expanded' ? { display: 'expanded' as const } : {}),
|
|
1994
2131
|
});
|
|
1995
2132
|
return new Response(html, {
|
|
1996
2133
|
headers: {
|
|
@@ -160,7 +160,7 @@ function summarizeAnnotationForSpatialContext(
|
|
|
160
160
|
);
|
|
161
161
|
return {
|
|
162
162
|
id: annotation.id,
|
|
163
|
-
label: annotation.label ?? null,
|
|
163
|
+
label: annotation.label ?? annotation.text ?? null,
|
|
164
164
|
bounds: annotation.bounds,
|
|
165
165
|
targetNodeIds: targetNodes.map((node) => node.id),
|
|
166
166
|
targetNodeTitles,
|
|
@@ -312,9 +312,10 @@ export function searchNodes(
|
|
|
312
312
|
|
|
313
313
|
for (const node of nodes) {
|
|
314
314
|
const title = ((node.data.title as string) ?? '').toLowerCase();
|
|
315
|
-
const content = ((node.data.content as string) ?? (node.data.fileContent as string) ?? '').toLowerCase();
|
|
315
|
+
const content = ((node.data.content as string) ?? (node.data.agentSummary as string) ?? (node.data.contentSummary as string) ?? (node.data.description as string) ?? (node.data.fileContent as string) ?? '').toLowerCase();
|
|
316
316
|
const path = ((node.data.path as string) ?? '').toLowerCase();
|
|
317
317
|
const description = ((node.data.description as string) ?? '').toLowerCase();
|
|
318
|
+
const summary = ((node.data.summary as string) ?? (node.data.agentSummary as string) ?? (node.data.contentSummary as string) ?? '').toLowerCase();
|
|
318
319
|
const url = ((node.data.url as string) ?? '').toLowerCase();
|
|
319
320
|
|
|
320
321
|
let score = 0;
|
|
@@ -324,6 +325,7 @@ export function searchNodes(
|
|
|
324
325
|
if (path.includes(term)) score += 2;
|
|
325
326
|
if (url.includes(term)) score += 2;
|
|
326
327
|
if (description.includes(term)) score += 1;
|
|
328
|
+
if (summary.includes(term)) score += 1;
|
|
327
329
|
if (content.includes(term)) score += 1;
|
|
328
330
|
}
|
|
329
331
|
|
|
@@ -331,7 +333,7 @@ export function searchNodes(
|
|
|
331
333
|
|
|
332
334
|
// Extract a snippet around the first match in content
|
|
333
335
|
let snippet = '';
|
|
334
|
-
const fullContent = (node.data.content as string) ?? (node.data.fileContent as string) ?? '';
|
|
336
|
+
const fullContent = (node.data.content as string) ?? (node.data.agentSummary as string) ?? (node.data.contentSummary as string) ?? (node.data.description as string) ?? (node.data.fileContent as string) ?? '';
|
|
335
337
|
const matchIdx = fullContent.toLowerCase().indexOf(terms[0]);
|
|
336
338
|
if (matchIdx >= 0) {
|
|
337
339
|
const start = Math.max(0, matchIdx - 40);
|