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.
Files changed (65) hide show
  1. package/CHANGELOG.md +159 -0
  2. package/Readme.md +19 -6
  3. package/dist/canvas/global.css +123 -2
  4. package/dist/canvas/index.js +103 -68
  5. package/dist/json-render/index.js +109 -109
  6. package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
  7. package/dist/types/client/icons.d.ts +2 -0
  8. package/dist/types/client/nodes/HtmlNode.d.ts +12 -1
  9. package/dist/types/client/state/canvas-store.d.ts +2 -0
  10. package/dist/types/client/types.d.ts +3 -2
  11. package/dist/types/json-render/charts/components.d.ts +5 -1
  12. package/dist/types/json-render/renderer/index.d.ts +1 -0
  13. package/dist/types/json-render/server.d.ts +1 -0
  14. package/dist/types/mcp/canvas-access.d.ts +3 -0
  15. package/dist/types/server/canvas-operations.d.ts +4 -0
  16. package/dist/types/server/canvas-schema.d.ts +19 -3
  17. package/dist/types/server/canvas-serialization.d.ts +1 -0
  18. package/dist/types/server/canvas-state.d.ts +6 -2
  19. package/dist/types/server/html-node-summary.d.ts +2 -0
  20. package/dist/types/server/html-primitives.d.ts +42 -0
  21. package/dist/types/server/index.d.ts +26 -0
  22. package/docs/cli.md +4 -1
  23. package/docs/http-api.md +11 -1
  24. package/docs/mcp.md +10 -4
  25. package/docs/node-types.md +54 -4
  26. package/docs/screenshot.png +0 -0
  27. package/docs/sdk.md +12 -0
  28. package/package.json +1 -1
  29. package/skills/pmx-canvas/SKILL.md +17 -3
  30. package/skills/pmx-canvas/references/html-primitives.md +132 -0
  31. package/src/cli/agent.ts +159 -5
  32. package/src/cli/index.ts +1 -1
  33. package/src/client/App.tsx +21 -2
  34. package/src/client/canvas/AnnotationLayer.tsx +33 -12
  35. package/src/client/canvas/CanvasViewport.tsx +88 -7
  36. package/src/client/canvas/CommandPalette.tsx +2 -2
  37. package/src/client/canvas/ContextMenu.tsx +2 -2
  38. package/src/client/canvas/ExpandedNodeOverlay.tsx +112 -3
  39. package/src/client/canvas/auto-fit.ts +5 -1
  40. package/src/client/icons.tsx +13 -0
  41. package/src/client/nodes/HtmlNode.tsx +125 -13
  42. package/src/client/nodes/McpAppNode.tsx +12 -4
  43. package/src/client/state/canvas-store.ts +15 -5
  44. package/src/client/state/sse-bridge.ts +5 -4
  45. package/src/client/theme/global.css +123 -2
  46. package/src/client/types.ts +2 -1
  47. package/src/json-render/charts/components.tsx +41 -7
  48. package/src/json-render/charts/extra-components.tsx +13 -12
  49. package/src/json-render/renderer/index.tsx +1 -0
  50. package/src/json-render/server.ts +3 -1
  51. package/src/mcp/canvas-access.ts +54 -1
  52. package/src/mcp/server.ts +98 -28
  53. package/src/server/agent-context.ts +39 -0
  54. package/src/server/canvas-operations.ts +99 -38
  55. package/src/server/canvas-provenance.ts +8 -6
  56. package/src/server/canvas-schema.ts +94 -3
  57. package/src/server/canvas-serialization.ts +16 -4
  58. package/src/server/canvas-state.ts +9 -4
  59. package/src/server/demo-state.json +1143 -0
  60. package/src/server/demo.ts +25 -777
  61. package/src/server/html-node-summary.ts +141 -0
  62. package/src/server/html-primitives.ts +1300 -0
  63. package/src/server/index.ts +63 -3
  64. package/src/server/server.ts +154 -17
  65. package/src/server/spatial-analysis.ts +5 -3
@@ -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: { html: input.html },
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';
@@ -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(24, Math.max(1, body.width))
1263
- : 4;
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: 'freehand',
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
- ...(label ? { label } : {}),
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' && typeof body.html === 'string'
1428
- ? { ...(extraData ?? {}), html: body.html }
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 "graph".' }, 400);
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 spec = node.data.spec;
1980
- if (!spec || typeof spec !== 'object') {
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: spec as { root: string; elements: Record<string, unknown>; state?: Record<string, unknown> },
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);