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
@@ -173,10 +173,18 @@ export function addNode(node: CanvasNodeState): void {
173
173
  }
174
174
 
175
175
  export function updateNode(id: string, patch: Partial<CanvasNodeState>): void {
176
+ updateNodeWithOptions(id, patch);
177
+ }
178
+
179
+ function updateNodeWithOptions(
180
+ id: string,
181
+ patch: Partial<CanvasNodeState>,
182
+ options: { skipGroupChildTranslation?: boolean } = {},
183
+ ): void {
176
184
  const existing = nodes.value.get(id);
177
185
  if (!existing) return;
178
186
  const next = new Map(nodes.value);
179
- if (existing.type === 'group' && patch.position) {
187
+ if (existing.type === 'group' && patch.position && options.skipGroupChildTranslation !== true) {
180
188
  const deltaX = patch.position.x - existing.position.x;
181
189
  const deltaY = patch.position.y - existing.position.y;
182
190
  if (deltaX !== 0 || deltaY !== 0) {
@@ -272,9 +280,11 @@ export function removeAnnotation(id: string): void {
272
280
  }
273
281
 
274
282
  export async function createAnnotationFromClient(input: {
283
+ type?: CanvasAnnotation['type'];
275
284
  points: CanvasAnnotation['points'];
276
285
  color: string;
277
286
  width: number;
287
+ text?: string;
278
288
  label?: string;
279
289
  }): Promise<{ ok: boolean }> {
280
290
  try {
@@ -748,10 +758,10 @@ export function autoArrange(): void {
748
758
  updateNode(id, { position });
749
759
  }
750
760
  for (const [groupId, bounds] of result.groupBounds.entries()) {
751
- updateNode(groupId, {
761
+ updateNodeWithOptions(groupId, {
752
762
  position: { x: bounds.x, y: bounds.y },
753
763
  size: { width: bounds.width, height: bounds.height },
754
- });
764
+ }, { skipGroupChildTranslation: true });
755
765
  }
756
766
  });
757
767
  persistLayout();
@@ -766,10 +776,10 @@ export function forceDirectedArrange(): void {
766
776
  updateNode(id, { position });
767
777
  }
768
778
  for (const [groupId, bounds] of result.groupBounds.entries()) {
769
- updateNode(groupId, {
779
+ updateNodeWithOptions(groupId, {
770
780
  position: { x: bounds.x, y: bounds.y },
771
781
  size: { width: bounds.width, height: bounds.height },
772
- });
782
+ }, { skipGroupChildTranslation: true });
773
783
  }
774
784
  });
775
785
  persistLayout();
@@ -291,9 +291,9 @@ function ensureLedgerNode(summary: Record<string, unknown>): void {
291
291
  function applyCanvasTheme(theme: string): void {
292
292
  const valid = theme === 'dark' || theme === 'light' || theme === 'high-contrast';
293
293
  if (!valid || canvasTheme.value === theme) return;
294
- canvasTheme.value = theme;
295
294
  document.documentElement.setAttribute('data-theme', theme);
296
295
  invalidateTokenCache();
296
+ canvasTheme.value = theme;
297
297
  }
298
298
 
299
299
  function isCanvasNodeType(value: unknown): value is CanvasNodeState['type'] {
@@ -388,21 +388,22 @@ function parseCanvasEdge(raw: Record<string, unknown>): CanvasEdge | null {
388
388
 
389
389
  function parseCanvasAnnotation(raw: Record<string, unknown>): CanvasAnnotation | null {
390
390
  if (typeof raw.id !== 'string' || !raw.id) return null;
391
- if (raw.type !== 'freehand') return null;
391
+ if (raw.type !== 'freehand' && raw.type !== 'text') return null;
392
392
  if (!Array.isArray(raw.points)) return null;
393
393
  const points = raw.points
394
394
  .map((point) => parseCanvasPosition(point))
395
395
  .filter((point): point is { x: number; y: number } => point !== null);
396
396
  const bounds = parseCanvasRect(raw.bounds);
397
- if (points.length < 2 || !bounds) return null;
397
+ if (points.length < (raw.type === 'text' ? 1 : 2) || !bounds) return null;
398
398
 
399
399
  return {
400
400
  id: raw.id,
401
- type: 'freehand',
401
+ type: raw.type,
402
402
  points,
403
403
  bounds,
404
404
  color: typeof raw.color === 'string' ? raw.color : '#f97316',
405
405
  width: typeof raw.width === 'number' ? raw.width : 4,
406
+ ...(typeof raw.text === 'string' ? { text: raw.text } : {}),
406
407
  ...(typeof raw.label === 'string' ? { label: raw.label } : {}),
407
408
  createdAt: typeof raw.createdAt === 'string' ? raw.createdAt : '',
408
409
  };
@@ -392,6 +392,19 @@ body,
392
392
  margin: 0.4em 0;
393
393
  color: var(--c-muted);
394
394
  }
395
+
396
+ .canvas-node .node-body ul,
397
+ .canvas-node .node-body ol {
398
+ margin: 0.4em 0;
399
+ padding-left: 0.25em;
400
+ list-style-position: inside;
401
+ }
402
+
403
+ .canvas-node .node-body li {
404
+ margin: 0.2em 0;
405
+ padding-left: 0.15em;
406
+ }
407
+
395
408
  .canvas-node .node-body a {
396
409
  color: var(--c-accent);
397
410
  text-decoration: none;
@@ -1481,14 +1494,14 @@ body,
1481
1494
  height: 1px;
1482
1495
  overflow: visible;
1483
1496
  pointer-events: none;
1484
- z-index: 45;
1497
+ z-index: 9000;
1485
1498
  }
1486
1499
 
1487
1500
  .annotation-capture-layer {
1488
1501
  position: absolute;
1489
1502
  inset: 0;
1490
1503
  z-index: 9996;
1491
- pointer-events: none;
1504
+ pointer-events: auto;
1492
1505
  background: color-mix(in srgb, var(--c-accent) 5%, transparent);
1493
1506
  }
1494
1507
 
@@ -1496,6 +1509,26 @@ body,
1496
1509
  background: color-mix(in srgb, var(--c-danger) 6%, transparent);
1497
1510
  }
1498
1511
 
1512
+ .annotation-capture-layer.text {
1513
+ background: color-mix(in srgb, var(--c-annotation) 4%, transparent);
1514
+ }
1515
+
1516
+ .annotation-text-input {
1517
+ position: absolute;
1518
+ z-index: 9997;
1519
+ min-width: 120px;
1520
+ max-width: min(560px, calc(100% - 24px));
1521
+ padding: 2px 4px;
1522
+ border: 1px solid var(--c-annotation);
1523
+ border-radius: 4px;
1524
+ background: color-mix(in srgb, var(--c-bg) 72%, transparent);
1525
+ color: var(--c-annotation);
1526
+ font-family: var(--font);
1527
+ font-weight: 700;
1528
+ line-height: 1.15;
1529
+ outline: none;
1530
+ }
1531
+
1499
1532
  /* ── Drop Zone (file drag-and-drop) ─────────────────────────── */
1500
1533
  .drop-zone-overlay {
1501
1534
  position: absolute;
@@ -2405,6 +2438,18 @@ body,
2405
2438
  border-color: var(--c-muted);
2406
2439
  }
2407
2440
 
2441
+ .expanded-action-btn.expanded-action-primary {
2442
+ background: var(--c-accent-12);
2443
+ border-color: var(--c-accent-30);
2444
+ color: var(--c-accent);
2445
+ }
2446
+
2447
+ .expanded-action-btn.expanded-action-primary:hover {
2448
+ background: var(--c-accent-25);
2449
+ border-color: var(--c-accent);
2450
+ color: var(--c-text);
2451
+ }
2452
+
2408
2453
  .expanded-action-btn.expanded-action-active {
2409
2454
  background: var(--c-warn-12);
2410
2455
  border-color: var(--c-warn-30);
@@ -2422,6 +2467,82 @@ body,
2422
2467
  padding: 0 4px;
2423
2468
  }
2424
2469
 
2470
+ .html-presentation-overlay {
2471
+ position: fixed;
2472
+ inset: 0;
2473
+ z-index: 10050;
2474
+ display: flex;
2475
+ flex-direction: column;
2476
+ gap: 14px;
2477
+ padding: clamp(12px, 2vw, 28px);
2478
+ background:
2479
+ radial-gradient(circle at top left, var(--c-accent-25), transparent 36rem),
2480
+ rgba(3, 7, 18, 0.96);
2481
+ color: var(--c-text);
2482
+ }
2483
+
2484
+ .html-presentation-toolbar {
2485
+ display: flex;
2486
+ align-items: center;
2487
+ justify-content: space-between;
2488
+ gap: 16px;
2489
+ flex-shrink: 0;
2490
+ padding: 10px 12px;
2491
+ border: 1px solid var(--c-line);
2492
+ border-radius: 16px;
2493
+ background: var(--c-panel-glass);
2494
+ box-shadow: 0 18px 50px var(--c-shadow-heavy);
2495
+ }
2496
+
2497
+ .html-presentation-kicker {
2498
+ color: var(--c-accent);
2499
+ font-size: 10px;
2500
+ font-weight: 800;
2501
+ letter-spacing: 0.14em;
2502
+ text-transform: uppercase;
2503
+ }
2504
+
2505
+ .html-presentation-title {
2506
+ max-width: min(72vw, 900px);
2507
+ overflow: hidden;
2508
+ color: var(--c-text);
2509
+ font-size: 14px;
2510
+ font-weight: 700;
2511
+ text-overflow: ellipsis;
2512
+ white-space: nowrap;
2513
+ }
2514
+
2515
+ .html-presentation-exit {
2516
+ flex-shrink: 0;
2517
+ padding: 8px 12px;
2518
+ border: 1px solid var(--c-line);
2519
+ border-radius: 999px;
2520
+ background: var(--c-panel-soft);
2521
+ color: var(--c-text-soft);
2522
+ cursor: pointer;
2523
+ font: 600 12px/1 var(--font);
2524
+ }
2525
+
2526
+ .html-presentation-exit:hover {
2527
+ border-color: var(--c-accent);
2528
+ color: var(--c-text);
2529
+ }
2530
+
2531
+ .html-presentation-stage {
2532
+ flex: 1;
2533
+ min-height: 0;
2534
+ display: flex;
2535
+ border-radius: 22px;
2536
+ background: var(--c-bg);
2537
+ box-shadow: 0 24px 90px rgba(0, 0, 0, 0.55);
2538
+ overflow: hidden;
2539
+ }
2540
+
2541
+ .html-node-frame-presentation {
2542
+ flex: 1;
2543
+ min-height: 0;
2544
+ }
2545
+
2425
2546
  /* ── Context pin button on node title bar ────────────────────── */
2426
2547
  .node-controls .ctx-pin-btn {
2427
2548
  color: var(--c-muted);
@@ -48,11 +48,12 @@ export interface CanvasAnnotationPoint {
48
48
 
49
49
  export interface CanvasAnnotation {
50
50
  id: string;
51
- type: 'freehand';
51
+ type: 'freehand' | 'text';
52
52
  points: CanvasAnnotationPoint[];
53
53
  bounds: { x: number; y: number; width: number; height: number };
54
54
  color: string;
55
55
  width: number;
56
+ text?: string;
56
57
  label?: string;
57
58
  createdAt: string;
58
59
  }
@@ -8,7 +8,7 @@
8
8
  * a responsive chart inside a styled container.
9
9
  */
10
10
 
11
- import type { ReactNode } from 'react';
11
+ import { useEffect, useRef, useState, type ReactNode } from 'react';
12
12
  import type { BaseComponentProps } from '@json-render/react';
13
13
  import {
14
14
  BarChart as RechartsBarChart,
@@ -106,6 +106,40 @@ export const polarChartMargin = { top: 18, right: 40, bottom: 30, left: 40 };
106
106
  export const axisTickMargin = 8;
107
107
  export const legendMargin = { top: 10 };
108
108
 
109
+ export function useChartFrameHeight(explicitHeight: number | null | undefined, fallbackHeight = 300) {
110
+ const frameRef = useRef<HTMLDivElement>(null);
111
+ const [autoHeight, setAutoHeight] = useState(fallbackHeight);
112
+
113
+ useEffect(() => {
114
+ const frame = frameRef.current;
115
+ if (!frame) return;
116
+
117
+ const updateHeight = () => {
118
+ const rect = frame.getBoundingClientRect();
119
+ const doc = document.documentElement;
120
+ const currentHeight = frame.getBoundingClientRect().height;
121
+ const overflow = Math.max(0, doc.scrollHeight - doc.clientHeight);
122
+ const available = overflow > 0 ? currentHeight - overflow : window.innerHeight - rect.top - 24;
123
+ setAutoHeight(Math.max(220, Math.round(available)));
124
+ };
125
+
126
+ updateHeight();
127
+ const observer = new ResizeObserver(updateHeight);
128
+ observer.observe(document.documentElement);
129
+ observer.observe(frame);
130
+ window.addEventListener('resize', updateHeight);
131
+ return () => {
132
+ observer.disconnect();
133
+ window.removeEventListener('resize', updateHeight);
134
+ };
135
+ }, [explicitHeight]);
136
+
137
+ return {
138
+ frameRef,
139
+ height: typeof explicitHeight === 'number' ? Math.min(explicitHeight, autoHeight) : autoHeight,
140
+ };
141
+ }
142
+
109
143
  /** Shared wrapper for cartesian charts (Line + Bar). */
110
144
  export function CartesianChart({
111
145
  props,
@@ -117,12 +151,12 @@ export function CartesianChart({
117
151
  className?: string;
118
152
  }) {
119
153
  const chartData = processChartData(props.data ?? [], props.xKey, props.yKey, props.aggregate);
120
- const h = props.height ?? 300;
154
+ const { frameRef, height } = useChartFrameHeight(props.height, 300);
121
155
 
122
156
  return (
123
- <div className={`pmx-chart${className ? ` ${className}` : ''}`}>
157
+ <div ref={frameRef} className={`pmx-chart${className ? ` ${className}` : ''}`}>
124
158
  {props.title && <div className="pmx-chart__title">{props.title}</div>}
125
- <ResponsiveContainer width="100%" height={h}>
159
+ <ResponsiveContainer width="100%" height={height}>
126
160
  {children(chartData)}
127
161
  </ResponsiveContainer>
128
162
  </div>
@@ -172,12 +206,12 @@ function ChartBarChart({ props }: BaseComponentProps<CartesianChartProps>) {
172
206
 
173
207
  function ChartPieChart({ props }: BaseComponentProps<PieChartProps>) {
174
208
  const data = props.data ?? [];
175
- const h = props.height ?? 300;
209
+ const { frameRef, height } = useChartFrameHeight(props.height, 300);
176
210
 
177
211
  return (
178
- <div className="pmx-chart pmx-chart--pie">
212
+ <div ref={frameRef} className="pmx-chart pmx-chart--pie">
179
213
  {props.title && <div className="pmx-chart__title">{props.title}</div>}
180
- <ResponsiveContainer width="100%" height={h}>
214
+ <ResponsiveContainer width="100%" height={height}>
181
215
  <RechartsPieChart margin={polarChartMargin}>
182
216
  <Tooltip contentStyle={tooltipStyle} />
183
217
  {props.showLegend !== false && <Legend wrapperStyle={legendMargin} />}
@@ -40,6 +40,7 @@ import {
40
40
  legendMargin,
41
41
  polarChartMargin,
42
42
  tooltipStyle,
43
+ useChartFrameHeight,
43
44
  type CartesianChartProps,
44
45
  } from './components';
45
46
 
@@ -89,12 +90,12 @@ interface ScatterChartProps {
89
90
  function ChartScatterChart({ props }: BaseComponentProps<ScatterChartProps>) {
90
91
  const fill = props.color ?? CHART_COLORS[0];
91
92
  const data = props.data ?? [];
92
- const h = props.height ?? 300;
93
+ const { frameRef, height } = useChartFrameHeight(props.height, 300);
93
94
 
94
95
  return (
95
- <div className="pmx-chart pmx-chart--scatter">
96
+ <div ref={frameRef} className="pmx-chart pmx-chart--scatter">
96
97
  {props.title && <div className="pmx-chart__title">{props.title}</div>}
97
- <ResponsiveContainer width="100%" height={h}>
98
+ <ResponsiveContainer width="100%" height={height}>
98
99
  <RechartsScatterChart margin={chartMargin}>
99
100
  <CartesianGrid strokeDasharray="3 3" stroke="var(--border, #e5e5e5)" />
100
101
  <XAxis type="number" dataKey={props.xKey} tick={axisStyle} tickMargin={axisTickMargin} name={props.xKey} />
@@ -120,12 +121,12 @@ interface RadarChartProps {
120
121
  function ChartRadarChart({ props }: BaseComponentProps<RadarChartProps>) {
121
122
  const data = props.data ?? [];
122
123
  const metrics = (props.metrics ?? []).filter((m) => typeof m === 'string' && m.length > 0);
123
- const h = props.height ?? 320;
124
+ const { frameRef, height } = useChartFrameHeight(props.height, 320);
124
125
 
125
126
  return (
126
- <div className="pmx-chart pmx-chart--radar">
127
+ <div ref={frameRef} className="pmx-chart pmx-chart--radar">
127
128
  {props.title && <div className="pmx-chart__title">{props.title}</div>}
128
- <ResponsiveContainer width="100%" height={h}>
129
+ <ResponsiveContainer width="100%" height={height}>
129
130
  <RechartsRadarChart data={data} outerRadius="66%" margin={polarChartMargin}>
130
131
  <PolarGrid stroke="var(--border, #e5e5e5)" />
131
132
  <PolarAngleAxis dataKey={props.axisKey} tick={axisStyle} />
@@ -166,12 +167,12 @@ function ChartStackedBarChart({ props }: BaseComponentProps<StackedBarChartProps
166
167
  const chartData = props.aggregate
167
168
  ? mergeAggregated(props.data ?? [], props.xKey, series, props.aggregate)
168
169
  : props.data ?? [];
169
- const h = props.height ?? 300;
170
+ const { frameRef, height } = useChartFrameHeight(props.height, 300);
170
171
 
171
172
  return (
172
- <div className="pmx-chart pmx-chart--stacked-bar">
173
+ <div ref={frameRef} className="pmx-chart pmx-chart--stacked-bar">
173
174
  {props.title && <div className="pmx-chart__title">{props.title}</div>}
174
- <ResponsiveContainer width="100%" height={h}>
175
+ <ResponsiveContainer width="100%" height={height}>
175
176
  <RechartsBarChart data={chartData} margin={chartMargin}>
176
177
  <CartesianGrid strokeDasharray="3 3" stroke="var(--border, #e5e5e5)" />
177
178
  <XAxis dataKey={props.xKey} tick={axisStyle} tickMargin={axisTickMargin} />
@@ -238,12 +239,12 @@ function ChartComposedChart({ props }: BaseComponentProps<ComposedChartProps>) {
238
239
  const data = props.data ?? [];
239
240
  const barFill = props.barColor ?? CHART_COLORS[0];
240
241
  const lineStroke = props.lineColor ?? CHART_COLORS[3];
241
- const h = props.height ?? 300;
242
+ const { frameRef, height } = useChartFrameHeight(props.height, 300);
242
243
 
243
244
  return (
244
- <div className="pmx-chart pmx-chart--composed">
245
+ <div ref={frameRef} className="pmx-chart pmx-chart--composed">
245
246
  {props.title && <div className="pmx-chart__title">{props.title}</div>}
246
- <ResponsiveContainer width="100%" height={h}>
247
+ <ResponsiveContainer width="100%" height={height}>
247
248
  <RechartsComposedChart data={data} margin={chartMargin}>
248
249
  <CartesianGrid strokeDasharray="3 3" stroke="var(--border, #e5e5e5)" />
249
250
  <XAxis dataKey={props.xKey} tick={axisStyle} tickMargin={axisTickMargin} />
@@ -49,6 +49,7 @@ declare global {
49
49
  interface Window {
50
50
  __PMX_CANVAS_JSON_RENDER_SPEC__?: Spec & { state?: Record<string, unknown> };
51
51
  __PMX_CANVAS_JSON_RENDER_THEME__?: string;
52
+ __PMX_CANVAS_JSON_RENDER_DISPLAY__?: string;
52
53
  }
53
54
  }
54
55
 
@@ -511,7 +511,7 @@ export function buildGraphSpec(input: GraphNodeInput): JsonRenderSpec {
511
511
 
512
512
  const chartProps: Record<string, unknown> = {
513
513
  data: input.data,
514
- height: input.height ?? 320,
514
+ ...(typeof input.height === 'number' ? { height: input.height } : {}),
515
515
  };
516
516
 
517
517
  switch (chartType) {
@@ -648,6 +648,7 @@ export async function buildJsonRenderViewerHtml(options: {
648
648
  title: string;
649
649
  spec: JsonRenderSpec;
650
650
  theme?: 'dark' | 'light' | 'high-contrast';
651
+ display?: 'expanded';
651
652
  }): Promise<string> {
652
653
  try {
653
654
  await ensureJsonRenderBundle();
@@ -661,6 +662,7 @@ export async function buildJsonRenderViewerHtml(options: {
661
662
  const boot = [
662
663
  `window.__PMX_CANVAS_JSON_RENDER_SPEC__ = ${JSON.stringify(options.spec)};`,
663
664
  ...(options.theme ? [`window.__PMX_CANVAS_JSON_RENDER_THEME__ = ${JSON.stringify(options.theme)};`] : []),
665
+ ...(options.display ? [`window.__PMX_CANVAS_JSON_RENDER_DISPLAY__ = ${JSON.stringify(options.display)};`] : []),
664
666
  jsBundle,
665
667
  ].join('\n');
666
668
  return buildAppHtml({
@@ -19,6 +19,8 @@ type AddDiagramInput = Parameters<PmxCanvas['addDiagram']>[0];
19
19
  type AddJsonRenderNodeInput = Parameters<PmxCanvas['addJsonRenderNode']>[0];
20
20
  type AddJsonRenderNodeResult = ReturnType<PmxCanvas['addJsonRenderNode']>;
21
21
  type AddHtmlNodeInput = Parameters<PmxCanvas['addHtmlNode']>[0];
22
+ type AddHtmlPrimitiveInput = Parameters<PmxCanvas['addHtmlPrimitive']>[0];
23
+ type AddHtmlPrimitiveResult = ReturnType<PmxCanvas['addHtmlPrimitive']>;
22
24
  type AddGraphNodeInput = Parameters<PmxCanvas['addGraphNode']>[0];
23
25
  type AddGraphNodeResult = ReturnType<PmxCanvas['addGraphNode']>;
24
26
  type UpdateNodePatch = Parameters<PmxCanvas['updateNode']>[1];
@@ -102,6 +104,7 @@ export interface CanvasAccess {
102
104
  addDiagram(input: AddDiagramInput): Promise<OpenMcpAppResult>;
103
105
  addJsonRenderNode(input: AddJsonRenderNodeInput): Promise<AddJsonRenderNodeResult>;
104
106
  addHtmlNode(input: AddHtmlNodeInput): Promise<string>;
107
+ addHtmlPrimitive(input: AddHtmlPrimitiveInput): Promise<AddHtmlPrimitiveResult>;
105
108
  addGraphNode(input: AddGraphNodeInput): Promise<AddGraphNodeResult>;
106
109
  buildWebArtifact(input: WebArtifactInput): Promise<WebArtifactResult>;
107
110
  updateNode(id: string, patch: UpdateNodePatch): Promise<void>;
@@ -188,6 +191,10 @@ class LocalCanvasAccess implements CanvasAccess {
188
191
  return this.canvas.addHtmlNode(input);
189
192
  }
190
193
 
194
+ async addHtmlPrimitive(input: AddHtmlPrimitiveInput): Promise<AddHtmlPrimitiveResult> {
195
+ return this.canvas.addHtmlPrimitive(input);
196
+ }
197
+
191
198
  async addGraphNode(input: AddGraphNodeInput): Promise<AddGraphNodeResult> {
192
199
  return this.canvas.addGraphNode(input);
193
200
  }
@@ -435,7 +442,53 @@ class RemoteCanvasAccess implements CanvasAccess {
435
442
  }
436
443
 
437
444
  async addHtmlNode(input: AddHtmlNodeInput): Promise<string> {
438
- return await this.requestNodeId('POST', '/api/canvas/node', { type: 'html', ...input });
445
+ const {
446
+ summary,
447
+ agentSummary,
448
+ description,
449
+ presentation,
450
+ slideTitles,
451
+ embeddedNodeIds,
452
+ embeddedUrls,
453
+ ...rest
454
+ } = input as AddHtmlNodeInput & {
455
+ summary?: string;
456
+ agentSummary?: string;
457
+ description?: string;
458
+ presentation?: boolean;
459
+ slideTitles?: string[];
460
+ embeddedNodeIds?: string[];
461
+ embeddedUrls?: string[];
462
+ };
463
+ return await this.requestNodeId('POST', '/api/canvas/node', {
464
+ type: 'html',
465
+ ...rest,
466
+ data: {
467
+ ...(typeof summary === 'string' ? { summary } : {}),
468
+ ...(typeof agentSummary === 'string' ? { agentSummary } : {}),
469
+ ...(typeof description === 'string' ? { description } : {}),
470
+ ...(presentation === true ? { presentation: true } : {}),
471
+ ...(Array.isArray(slideTitles) ? { slideTitles } : {}),
472
+ ...(Array.isArray(embeddedNodeIds) ? { embeddedNodeIds } : {}),
473
+ ...(Array.isArray(embeddedUrls) ? { embeddedUrls } : {}),
474
+ },
475
+ });
476
+ }
477
+
478
+ async addHtmlPrimitive(input: AddHtmlPrimitiveInput): Promise<AddHtmlPrimitiveResult> {
479
+ const response = await this.requestJson<{
480
+ id?: string;
481
+ node?: { id?: string };
482
+ primitive?: { kind?: string; title?: string; htmlBytes?: number };
483
+ }>('POST', '/api/canvas/node', { type: 'html', ...input, primitive: input.kind });
484
+ const id = typeof response.id === 'string' ? response.id : response.node?.id;
485
+ if (!id) throw new Error('html primitive response did not include a node id.');
486
+ return {
487
+ id,
488
+ kind: input.kind,
489
+ title: response.primitive?.title ?? input.title ?? input.kind,
490
+ htmlBytes: response.primitive?.htmlBytes ?? 0,
491
+ };
439
492
  }
440
493
 
441
494
  async addGraphNode(input: AddGraphNodeInput): Promise<AddGraphNodeResult> {