pmx-canvas 0.1.26 → 0.1.28
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/.github/extensions/pmx-canvas/extension.mjs +191 -0
- package/CHANGELOG.md +110 -0
- package/Readme.md +74 -27
- package/dist/canvas/index.js +82 -82
- package/dist/json-render/index.css +1 -1
- package/dist/json-render/index.js +944 -164
- package/dist/types/json-render/catalog.d.ts +195 -20
- package/dist/types/json-render/charts/components.d.ts +17 -0
- package/dist/types/json-render/charts/definitions.d.ts +13 -1
- package/dist/types/json-render/charts/tufte-components.d.ts +65 -0
- package/dist/types/json-render/charts/tufte-definitions.d.ts +164 -0
- package/dist/types/json-render/directives.d.ts +33 -0
- package/dist/types/json-render/renderer/index.d.ts +1 -0
- package/dist/types/json-render/server.d.ts +32 -1
- package/dist/types/mcp/canvas-access.d.ts +62 -0
- package/dist/types/server/ax-state.d.ts +170 -0
- package/dist/types/server/canvas-db.d.ts +17 -1
- package/dist/types/server/canvas-operations.d.ts +53 -0
- package/dist/types/server/canvas-schema.d.ts +5 -1
- package/dist/types/server/canvas-state.d.ts +95 -4
- package/dist/types/server/index.d.ts +120 -3
- package/dist/types/server/mutation-history.d.ts +1 -1
- package/docs/cli.md +42 -0
- package/docs/http-api.md +64 -0
- package/docs/mcp.md +23 -5
- package/docs/node-types.md +1 -1
- package/docs/screenshots/codex-app.png +0 -0
- package/docs/screenshots/github-copilot-app.png +0 -0
- package/docs/sdk.md +23 -5
- package/package.json +10 -7
- package/skills/control-session-orchestrator/SKILL.md +359 -0
- package/skills/control-session-orchestrator/evals/evals.json +75 -0
- package/skills/data-analysis/SKILL.md +6 -0
- package/skills/pmx-canvas/SKILL.md +50 -4
- package/skills/pmx-canvas/references/github-copilot-app-adapter.md +6 -0
- package/skills/tufte-viz/SKILL.md +157 -0
- package/skills/tufte-viz/references/analytical-design.md +217 -0
- package/skills/tufte-viz/references/tufte-principles.md +147 -0
- package/src/cli/agent.ts +302 -3
- package/src/cli/index.ts +2 -1
- package/src/client/nodes/ExtAppFrame.tsx +48 -1
- package/src/client/nodes/McpAppNode.tsx +6 -2
- package/src/json-render/catalog.ts +22 -1
- package/src/json-render/charts/components.tsx +127 -15
- package/src/json-render/charts/definitions.ts +19 -2
- package/src/json-render/charts/extra-components.tsx +5 -4
- package/src/json-render/charts/tufte-components.tsx +395 -0
- package/src/json-render/charts/tufte-definitions.ts +128 -0
- package/src/json-render/directives.ts +64 -0
- package/src/json-render/renderer/index.css +107 -1
- package/src/json-render/renderer/index.tsx +33 -0
- package/src/json-render/server.ts +275 -5
- package/src/mcp/canvas-access.ts +264 -1
- package/src/mcp/server.ts +498 -9
- package/src/server/ax-context.ts +8 -3
- package/src/server/ax-state.ts +447 -0
- package/src/server/canvas-db.ts +184 -1
- package/src/server/canvas-operations.ts +123 -2
- package/src/server/canvas-schema.ts +27 -3
- package/src/server/canvas-state.ts +349 -2
- package/src/server/index.ts +259 -7
- package/src/server/mutation-history.ts +6 -0
- package/src/server/server.ts +442 -5
- package/src/server/web-artifacts.ts +31 -5
|
@@ -2,7 +2,9 @@ import { spawn } from 'node:child_process';
|
|
|
2
2
|
import { existsSync, readFileSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { buildAppHtml } from '@json-render/mcp/build-app-html';
|
|
5
|
+
import { applySpecPatch, parseSpecStreamLine, type Spec, type SpecStreamLine } from '@json-render/core';
|
|
5
6
|
import { allComponentDefinitions, catalog, validateShadcnElementProps, type JsonRenderIssue } from './catalog.js';
|
|
7
|
+
import { findUnknownDirectiveKey, isDynamicPropValue } from './directives.js';
|
|
6
8
|
|
|
7
9
|
export interface JsonRenderSpec {
|
|
8
10
|
root: string;
|
|
@@ -36,8 +38,23 @@ export interface GraphNodeInput {
|
|
|
36
38
|
lineKey?: string;
|
|
37
39
|
aggregate?: 'sum' | 'count' | 'avg';
|
|
38
40
|
color?: string;
|
|
41
|
+
colorBy?: 'series' | 'category' | 'value' | 'none';
|
|
42
|
+
highlight?: number | 'max' | 'min' | null;
|
|
39
43
|
barColor?: string;
|
|
40
44
|
lineColor?: string;
|
|
45
|
+
labelKey?: string;
|
|
46
|
+
beforeKey?: string;
|
|
47
|
+
afterKey?: string;
|
|
48
|
+
beforeLabel?: string;
|
|
49
|
+
afterLabel?: string;
|
|
50
|
+
targetKey?: string;
|
|
51
|
+
rangesKey?: string;
|
|
52
|
+
sort?: 'asc' | 'desc' | 'none';
|
|
53
|
+
fill?: boolean;
|
|
54
|
+
showEndDot?: boolean;
|
|
55
|
+
showMinMax?: boolean;
|
|
56
|
+
showValue?: boolean;
|
|
57
|
+
colorByDirection?: boolean;
|
|
41
58
|
height?: number;
|
|
42
59
|
showLegend?: boolean;
|
|
43
60
|
showLabels?: boolean;
|
|
@@ -59,7 +76,11 @@ export type GraphChartType =
|
|
|
59
76
|
| 'ScatterChart'
|
|
60
77
|
| 'RadarChart'
|
|
61
78
|
| 'StackedBarChart'
|
|
62
|
-
| 'ComposedChart'
|
|
79
|
+
| 'ComposedChart'
|
|
80
|
+
| 'Sparkline'
|
|
81
|
+
| 'DotPlot'
|
|
82
|
+
| 'BulletChart'
|
|
83
|
+
| 'Slopegraph';
|
|
63
84
|
|
|
64
85
|
const GRAPH_TYPE_ALIASES: Record<string, GraphChartType> = {
|
|
65
86
|
line: 'LineChart',
|
|
@@ -84,6 +105,15 @@ const GRAPH_TYPE_ALIASES: Record<string, GraphChartType> = {
|
|
|
84
105
|
composedchart: 'ComposedChart',
|
|
85
106
|
combo: 'ComposedChart',
|
|
86
107
|
combochart: 'ComposedChart',
|
|
108
|
+
sparkline: 'Sparkline',
|
|
109
|
+
spark: 'Sparkline',
|
|
110
|
+
dotplot: 'DotPlot',
|
|
111
|
+
dot: 'DotPlot',
|
|
112
|
+
cleveland: 'DotPlot',
|
|
113
|
+
bullet: 'BulletChart',
|
|
114
|
+
bulletchart: 'BulletChart',
|
|
115
|
+
slopegraph: 'Slopegraph',
|
|
116
|
+
slope: 'Slopegraph',
|
|
87
117
|
};
|
|
88
118
|
|
|
89
119
|
const COERCIBLE_STRING_PROPS = [
|
|
@@ -316,7 +346,12 @@ function normalizeElementProps(
|
|
|
316
346
|
const props = (stripNullishDeep(rawProps) as Record<string, unknown> | undefined) ?? {};
|
|
317
347
|
|
|
318
348
|
for (const key of COERCIBLE_STRING_PROPS) {
|
|
319
|
-
if (
|
|
349
|
+
if (
|
|
350
|
+
key in props &&
|
|
351
|
+
typeof props[key] !== 'string' &&
|
|
352
|
+
props[key] !== undefined &&
|
|
353
|
+
!isDynamicPropValue(props[key])
|
|
354
|
+
) {
|
|
320
355
|
props[key] = String(props[key]);
|
|
321
356
|
}
|
|
322
357
|
}
|
|
@@ -477,6 +512,24 @@ export function normalizeAndValidateJsonRenderSpec(spec: unknown): JsonRenderSpe
|
|
|
477
512
|
throw new Error('Missing root and elements in spec. Pass a complete {root, elements} document, or a single bare component object with a type field.');
|
|
478
513
|
}
|
|
479
514
|
|
|
515
|
+
// Reject an unrecognized $-keyed expression object in any element prop BEFORE
|
|
516
|
+
// normalization can string-coerce it to "[object Object]" (the reported $path
|
|
517
|
+
// symptom). The error names the offending key and points at $state, the
|
|
518
|
+
// correct path-read binding.
|
|
519
|
+
const elementsRecord = asRecord(specRecord.elements) ?? {};
|
|
520
|
+
for (const [elementKey, rawElement] of Object.entries(elementsRecord)) {
|
|
521
|
+
const props = asRecord(asRecord(rawElement)?.props);
|
|
522
|
+
if (!props) continue;
|
|
523
|
+
for (const [propKey, propValue] of Object.entries(props)) {
|
|
524
|
+
const unknownKey = findUnknownDirectiveKey(propValue);
|
|
525
|
+
if (unknownKey) {
|
|
526
|
+
throw new Error(
|
|
527
|
+
`Unknown directive "${unknownKey}" on elements.${elementKey}.props.${propKey}. Valid directives are $format, $math, $concat, $count, $truncate, $pluralize, $join; to read a value from state by path use { "$state": "/path" } (there is no $path directive).`,
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
480
533
|
const normalizedSpec = normalizeSpec(specRecord);
|
|
481
534
|
const validation = catalog.validate(normalizedSpec);
|
|
482
535
|
if (!validation.success || !validation.data) {
|
|
@@ -502,6 +555,86 @@ function inferKeysFromData(data: Array<Record<string, unknown>>, exclude: string
|
|
|
502
555
|
return Object.keys(first).filter((key) => !exclude.includes(key));
|
|
503
556
|
}
|
|
504
557
|
|
|
558
|
+
function collectDataKeys(data: Array<Record<string, unknown>>): Set<string> {
|
|
559
|
+
const keys = new Set<string>();
|
|
560
|
+
for (const row of data) {
|
|
561
|
+
for (const key of Object.keys(row)) keys.add(key);
|
|
562
|
+
}
|
|
563
|
+
return keys;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function assertGraphDataKeys(
|
|
567
|
+
data: Array<Record<string, unknown>>,
|
|
568
|
+
chartType: GraphChartType,
|
|
569
|
+
chartProps: Record<string, unknown>,
|
|
570
|
+
): void {
|
|
571
|
+
if (data.length === 0) return;
|
|
572
|
+
const available = collectDataKeys(data);
|
|
573
|
+
const required = new Set<string>();
|
|
574
|
+
|
|
575
|
+
const addString = (value: unknown): void => {
|
|
576
|
+
if (typeof value === 'string' && value.length > 0) required.add(value);
|
|
577
|
+
};
|
|
578
|
+
const addStringList = (value: unknown): void => {
|
|
579
|
+
if (!Array.isArray(value)) return;
|
|
580
|
+
for (const item of value) addString(item);
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
switch (chartType) {
|
|
584
|
+
case 'PieChart':
|
|
585
|
+
addString(chartProps.nameKey);
|
|
586
|
+
addString(chartProps.valueKey);
|
|
587
|
+
break;
|
|
588
|
+
case 'ScatterChart':
|
|
589
|
+
addString(chartProps.xKey);
|
|
590
|
+
addString(chartProps.yKey);
|
|
591
|
+
addString(chartProps.zKey);
|
|
592
|
+
break;
|
|
593
|
+
case 'RadarChart':
|
|
594
|
+
addString(chartProps.axisKey);
|
|
595
|
+
addStringList(chartProps.metrics);
|
|
596
|
+
break;
|
|
597
|
+
case 'StackedBarChart':
|
|
598
|
+
addString(chartProps.xKey);
|
|
599
|
+
addStringList(chartProps.series);
|
|
600
|
+
break;
|
|
601
|
+
case 'ComposedChart':
|
|
602
|
+
addString(chartProps.xKey);
|
|
603
|
+
addString(chartProps.barKey);
|
|
604
|
+
addString(chartProps.lineKey);
|
|
605
|
+
break;
|
|
606
|
+
case 'Sparkline':
|
|
607
|
+
addString(chartProps.valueKey);
|
|
608
|
+
break;
|
|
609
|
+
case 'DotPlot':
|
|
610
|
+
addString(chartProps.labelKey);
|
|
611
|
+
addString(chartProps.valueKey);
|
|
612
|
+
break;
|
|
613
|
+
case 'BulletChart':
|
|
614
|
+
addString(chartProps.labelKey);
|
|
615
|
+
addString(chartProps.valueKey);
|
|
616
|
+
addString(chartProps.targetKey);
|
|
617
|
+
addString(chartProps.rangesKey);
|
|
618
|
+
break;
|
|
619
|
+
case 'Slopegraph':
|
|
620
|
+
addString(chartProps.labelKey);
|
|
621
|
+
addString(chartProps.beforeKey);
|
|
622
|
+
addString(chartProps.afterKey);
|
|
623
|
+
break;
|
|
624
|
+
case 'AreaChart':
|
|
625
|
+
case 'BarChart':
|
|
626
|
+
case 'LineChart':
|
|
627
|
+
addString(chartProps.xKey);
|
|
628
|
+
addString(chartProps.yKey);
|
|
629
|
+
break;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const missing = [...required].filter((key) => !available.has(key));
|
|
633
|
+
if (missing.length === 0) return;
|
|
634
|
+
const availableList = [...available].sort().join(', ') || '(none)';
|
|
635
|
+
throw new Error(`Graph data key mismatch for ${chartType}: missing ${missing.join(', ')}. Available keys: ${availableList}.`);
|
|
636
|
+
}
|
|
637
|
+
|
|
505
638
|
export function buildGraphSpec(input: GraphNodeInput): JsonRenderSpec {
|
|
506
639
|
const title = input.title?.trim() || 'Graph';
|
|
507
640
|
const chartType = normalizeGraphType(input.graphType);
|
|
@@ -567,18 +700,66 @@ export function buildGraphSpec(input: GraphNodeInput): JsonRenderSpec {
|
|
|
567
700
|
chartProps.showLegend = input.showLegend !== false;
|
|
568
701
|
break;
|
|
569
702
|
}
|
|
703
|
+
case 'BarChart': {
|
|
704
|
+
const xKey = input.xKey ?? inferKeysFromData(input.data)[0] ?? 'label';
|
|
705
|
+
const yKey = input.yKey ?? inferKeysFromData(input.data, [xKey])[0] ?? 'value';
|
|
706
|
+
chartProps.xKey = xKey;
|
|
707
|
+
chartProps.yKey = yKey;
|
|
708
|
+
chartProps.aggregate = input.aggregate ?? null;
|
|
709
|
+
chartProps.color = input.color ?? null;
|
|
710
|
+
chartProps.colorBy = input.colorBy ?? 'series';
|
|
711
|
+
chartProps.highlight = input.highlight === undefined ? 'max' : input.highlight;
|
|
712
|
+
break;
|
|
713
|
+
}
|
|
714
|
+
case 'Sparkline': {
|
|
715
|
+
chartProps.valueKey = input.valueKey ?? input.yKey ?? 'value';
|
|
716
|
+
chartProps.color = input.color ?? null;
|
|
717
|
+
chartProps.fill = input.fill ?? null;
|
|
718
|
+
chartProps.showEndDot = input.showEndDot ?? null;
|
|
719
|
+
chartProps.showMinMax = input.showMinMax ?? null;
|
|
720
|
+
chartProps.showValue = input.showValue ?? null;
|
|
721
|
+
break;
|
|
722
|
+
}
|
|
723
|
+
case 'DotPlot': {
|
|
724
|
+
chartProps.labelKey = input.labelKey ?? input.xKey ?? 'label';
|
|
725
|
+
chartProps.valueKey = input.valueKey ?? input.yKey ?? 'value';
|
|
726
|
+
chartProps.color = input.color ?? null;
|
|
727
|
+
chartProps.sort = input.sort ?? null;
|
|
728
|
+
break;
|
|
729
|
+
}
|
|
730
|
+
case 'BulletChart': {
|
|
731
|
+
chartProps.labelKey = input.labelKey ?? input.xKey ?? null;
|
|
732
|
+
chartProps.valueKey = input.valueKey ?? input.yKey ?? 'value';
|
|
733
|
+
chartProps.targetKey = input.targetKey ?? null;
|
|
734
|
+
chartProps.rangesKey = input.rangesKey ?? null;
|
|
735
|
+
chartProps.color = input.color ?? null;
|
|
736
|
+
break;
|
|
737
|
+
}
|
|
738
|
+
case 'Slopegraph': {
|
|
739
|
+
chartProps.labelKey = input.labelKey ?? 'label';
|
|
740
|
+
chartProps.beforeKey = input.beforeKey ?? 'before';
|
|
741
|
+
chartProps.afterKey = input.afterKey ?? 'after';
|
|
742
|
+
chartProps.beforeLabel = input.beforeLabel ?? null;
|
|
743
|
+
chartProps.afterLabel = input.afterLabel ?? null;
|
|
744
|
+
chartProps.color = input.color ?? null;
|
|
745
|
+
chartProps.colorByDirection = input.colorByDirection ?? null;
|
|
746
|
+
break;
|
|
747
|
+
}
|
|
570
748
|
case 'AreaChart':
|
|
571
749
|
case 'LineChart':
|
|
572
|
-
case 'BarChart':
|
|
573
750
|
default: {
|
|
574
|
-
|
|
575
|
-
|
|
751
|
+
const xKey = input.xKey ?? inferKeysFromData(input.data)[0] ?? 'label';
|
|
752
|
+
const yKey = input.yKey ?? inferKeysFromData(input.data, [xKey])[0] ?? 'value';
|
|
753
|
+
chartProps.xKey = xKey;
|
|
754
|
+
chartProps.yKey = yKey;
|
|
576
755
|
chartProps.aggregate = input.aggregate ?? null;
|
|
577
756
|
chartProps.color = input.color ?? null;
|
|
578
757
|
break;
|
|
579
758
|
}
|
|
580
759
|
}
|
|
581
760
|
|
|
761
|
+
assertGraphDataKeys(input.data, chartType, chartProps);
|
|
762
|
+
|
|
582
763
|
return normalizeAndValidateJsonRenderSpec({
|
|
583
764
|
root: 'card',
|
|
584
765
|
elements: {
|
|
@@ -619,14 +800,101 @@ export function buildGraphConfig(input: GraphNodeInput): Record<string, unknown>
|
|
|
619
800
|
...(input.lineKey ? { lineKey: input.lineKey } : {}),
|
|
620
801
|
...(input.aggregate ? { aggregate: input.aggregate } : {}),
|
|
621
802
|
...(input.color ? { color: input.color } : {}),
|
|
803
|
+
...(input.colorBy ? { colorBy: input.colorBy } : {}),
|
|
804
|
+
...(input.highlight !== undefined ? { highlight: input.highlight } : {}),
|
|
622
805
|
...(input.barColor ? { barColor: input.barColor } : {}),
|
|
623
806
|
...(input.lineColor ? { lineColor: input.lineColor } : {}),
|
|
807
|
+
...(input.labelKey ? { labelKey: input.labelKey } : {}),
|
|
808
|
+
...(input.beforeKey ? { beforeKey: input.beforeKey } : {}),
|
|
809
|
+
...(input.afterKey ? { afterKey: input.afterKey } : {}),
|
|
810
|
+
...(input.beforeLabel ? { beforeLabel: input.beforeLabel } : {}),
|
|
811
|
+
...(input.afterLabel ? { afterLabel: input.afterLabel } : {}),
|
|
812
|
+
...(input.targetKey ? { targetKey: input.targetKey } : {}),
|
|
813
|
+
...(input.rangesKey ? { rangesKey: input.rangesKey } : {}),
|
|
814
|
+
...(input.sort ? { sort: input.sort } : {}),
|
|
815
|
+
...(typeof input.fill === 'boolean' ? { fill: input.fill } : {}),
|
|
816
|
+
...(typeof input.showEndDot === 'boolean' ? { showEndDot: input.showEndDot } : {}),
|
|
817
|
+
...(typeof input.showMinMax === 'boolean' ? { showMinMax: input.showMinMax } : {}),
|
|
818
|
+
...(typeof input.showValue === 'boolean' ? { showValue: input.showValue } : {}),
|
|
819
|
+
...(typeof input.colorByDirection === 'boolean' ? { colorByDirection: input.colorByDirection } : {}),
|
|
624
820
|
...(typeof input.height === 'number' ? { height: input.height } : {}),
|
|
625
821
|
...(typeof input.showLegend === 'boolean' ? { showLegend: input.showLegend } : {}),
|
|
626
822
|
...(typeof input.showLabels === 'boolean' ? { showLabels: input.showLabels } : {}),
|
|
627
823
|
};
|
|
628
824
|
}
|
|
629
825
|
|
|
826
|
+
/** The minimal spec a streaming json-render node starts from before any patches. */
|
|
827
|
+
export function emptyStreamingSpec(): JsonRenderSpec {
|
|
828
|
+
return { root: '', elements: {} };
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
const UNSAFE_PATCH_SEGMENTS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* Reject JSON-Pointer paths that would let a streamed patch reach a prototype
|
|
835
|
+
* chain (`__proto__`/`constructor`/`prototype`). The upstream applySpecPatch
|
|
836
|
+
* walks the path with plain property assignment and has no such guard, so an
|
|
837
|
+
* agent-supplied patch like `{op:'add',path:'/__proto__/x',value:...}` would
|
|
838
|
+
* otherwise pollute Object.prototype server-side. Segments are JSON-Pointer
|
|
839
|
+
* escaped (`~1`->`/`, `~0`->`~`); decode before comparing.
|
|
840
|
+
*/
|
|
841
|
+
function isSafePatchPath(path: string): boolean {
|
|
842
|
+
for (const rawSegment of path.split('/')) {
|
|
843
|
+
const segment = rawSegment.replace(/~1/g, '/').replace(/~0/g, '~');
|
|
844
|
+
if (UNSAFE_PATCH_SEGMENTS.has(segment)) return false;
|
|
845
|
+
}
|
|
846
|
+
return true;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/** Accept a SpecStream item as either a raw JSONL line or a JSON-Patch object. */
|
|
850
|
+
function coerceStreamPatch(item: unknown): SpecStreamLine | null {
|
|
851
|
+
const patch = (() => {
|
|
852
|
+
if (typeof item === 'string') return parseSpecStreamLine(item);
|
|
853
|
+
if (item && typeof item === 'object' && !Array.isArray(item)) {
|
|
854
|
+
const record = item as Record<string, unknown>;
|
|
855
|
+
if (typeof record.op === 'string' && typeof record.path === 'string') {
|
|
856
|
+
return item as SpecStreamLine;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
return null;
|
|
860
|
+
})();
|
|
861
|
+
if (!patch || typeof patch.path !== 'string' || !isSafePatchPath(patch.path)) return null;
|
|
862
|
+
return patch;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/**
|
|
866
|
+
* Apply a batch of SpecStream patches to the current spec, accumulating the
|
|
867
|
+
* result. The canvas is the source of truth — patches are applied server-side
|
|
868
|
+
* and the browser only renders the current accumulated spec, so there is no
|
|
869
|
+
* client-side reconciliation. Tolerant by design: malformed or inapplicable
|
|
870
|
+
* patches are skipped and counted, never thrown, so a partial stream keeps
|
|
871
|
+
* building toward the final spec.
|
|
872
|
+
*/
|
|
873
|
+
export function applyJsonRenderStreamPatches(
|
|
874
|
+
currentSpec: JsonRenderSpec,
|
|
875
|
+
items: unknown[],
|
|
876
|
+
): { spec: JsonRenderSpec; applied: number; skipped: number } {
|
|
877
|
+
let spec = currentSpec as Spec;
|
|
878
|
+
let applied = 0;
|
|
879
|
+
let skipped = 0;
|
|
880
|
+
for (const item of items) {
|
|
881
|
+
const patch = coerceStreamPatch(item);
|
|
882
|
+
if (!patch) {
|
|
883
|
+
skipped++;
|
|
884
|
+
continue;
|
|
885
|
+
}
|
|
886
|
+
try {
|
|
887
|
+
spec = applySpecPatch(spec, patch);
|
|
888
|
+
applied++;
|
|
889
|
+
} catch {
|
|
890
|
+
// Inapplicable patch (e.g. references a not-yet-added element); skip and
|
|
891
|
+
// keep streaming. A later patch may add the missing target.
|
|
892
|
+
skipped++;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
return { spec: spec as JsonRenderSpec, applied, skipped };
|
|
896
|
+
}
|
|
897
|
+
|
|
630
898
|
export function createJsonRenderNodeData(
|
|
631
899
|
nodeId: string,
|
|
632
900
|
title: string,
|
|
@@ -649,6 +917,7 @@ export async function buildJsonRenderViewerHtml(options: {
|
|
|
649
917
|
spec: JsonRenderSpec;
|
|
650
918
|
theme?: 'dark' | 'light' | 'high-contrast';
|
|
651
919
|
display?: 'expanded';
|
|
920
|
+
devtools?: boolean;
|
|
652
921
|
}): Promise<string> {
|
|
653
922
|
try {
|
|
654
923
|
await ensureJsonRenderBundle();
|
|
@@ -663,6 +932,7 @@ export async function buildJsonRenderViewerHtml(options: {
|
|
|
663
932
|
`window.__PMX_CANVAS_JSON_RENDER_SPEC__ = ${JSON.stringify(options.spec)};`,
|
|
664
933
|
...(options.theme ? [`window.__PMX_CANVAS_JSON_RENDER_THEME__ = ${JSON.stringify(options.theme)};`] : []),
|
|
665
934
|
...(options.display ? [`window.__PMX_CANVAS_JSON_RENDER_DISPLAY__ = ${JSON.stringify(options.display)};`] : []),
|
|
935
|
+
...(options.devtools ? ['window.__PMX_CANVAS_JSON_RENDER_DEVTOOLS__ = true;'] : []),
|
|
666
936
|
jsBundle,
|
|
667
937
|
].join('\n');
|
|
668
938
|
return buildAppHtml({
|