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.
Files changed (64) hide show
  1. package/.github/extensions/pmx-canvas/extension.mjs +191 -0
  2. package/CHANGELOG.md +110 -0
  3. package/Readme.md +74 -27
  4. package/dist/canvas/index.js +82 -82
  5. package/dist/json-render/index.css +1 -1
  6. package/dist/json-render/index.js +944 -164
  7. package/dist/types/json-render/catalog.d.ts +195 -20
  8. package/dist/types/json-render/charts/components.d.ts +17 -0
  9. package/dist/types/json-render/charts/definitions.d.ts +13 -1
  10. package/dist/types/json-render/charts/tufte-components.d.ts +65 -0
  11. package/dist/types/json-render/charts/tufte-definitions.d.ts +164 -0
  12. package/dist/types/json-render/directives.d.ts +33 -0
  13. package/dist/types/json-render/renderer/index.d.ts +1 -0
  14. package/dist/types/json-render/server.d.ts +32 -1
  15. package/dist/types/mcp/canvas-access.d.ts +62 -0
  16. package/dist/types/server/ax-state.d.ts +170 -0
  17. package/dist/types/server/canvas-db.d.ts +17 -1
  18. package/dist/types/server/canvas-operations.d.ts +53 -0
  19. package/dist/types/server/canvas-schema.d.ts +5 -1
  20. package/dist/types/server/canvas-state.d.ts +95 -4
  21. package/dist/types/server/index.d.ts +120 -3
  22. package/dist/types/server/mutation-history.d.ts +1 -1
  23. package/docs/cli.md +42 -0
  24. package/docs/http-api.md +64 -0
  25. package/docs/mcp.md +23 -5
  26. package/docs/node-types.md +1 -1
  27. package/docs/screenshots/codex-app.png +0 -0
  28. package/docs/screenshots/github-copilot-app.png +0 -0
  29. package/docs/sdk.md +23 -5
  30. package/package.json +10 -7
  31. package/skills/control-session-orchestrator/SKILL.md +359 -0
  32. package/skills/control-session-orchestrator/evals/evals.json +75 -0
  33. package/skills/data-analysis/SKILL.md +6 -0
  34. package/skills/pmx-canvas/SKILL.md +50 -4
  35. package/skills/pmx-canvas/references/github-copilot-app-adapter.md +6 -0
  36. package/skills/tufte-viz/SKILL.md +157 -0
  37. package/skills/tufte-viz/references/analytical-design.md +217 -0
  38. package/skills/tufte-viz/references/tufte-principles.md +147 -0
  39. package/src/cli/agent.ts +302 -3
  40. package/src/cli/index.ts +2 -1
  41. package/src/client/nodes/ExtAppFrame.tsx +48 -1
  42. package/src/client/nodes/McpAppNode.tsx +6 -2
  43. package/src/json-render/catalog.ts +22 -1
  44. package/src/json-render/charts/components.tsx +127 -15
  45. package/src/json-render/charts/definitions.ts +19 -2
  46. package/src/json-render/charts/extra-components.tsx +5 -4
  47. package/src/json-render/charts/tufte-components.tsx +395 -0
  48. package/src/json-render/charts/tufte-definitions.ts +128 -0
  49. package/src/json-render/directives.ts +64 -0
  50. package/src/json-render/renderer/index.css +107 -1
  51. package/src/json-render/renderer/index.tsx +33 -0
  52. package/src/json-render/server.ts +275 -5
  53. package/src/mcp/canvas-access.ts +264 -1
  54. package/src/mcp/server.ts +498 -9
  55. package/src/server/ax-context.ts +8 -3
  56. package/src/server/ax-state.ts +447 -0
  57. package/src/server/canvas-db.ts +184 -1
  58. package/src/server/canvas-operations.ts +123 -2
  59. package/src/server/canvas-schema.ts +27 -3
  60. package/src/server/canvas-state.ts +349 -2
  61. package/src/server/index.ts +259 -7
  62. package/src/server/mutation-history.ts +6 -0
  63. package/src/server/server.ts +442 -5
  64. 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 (key in props && typeof props[key] !== 'string' && props[key] !== undefined) {
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
- chartProps.xKey = input.xKey ?? 'label';
575
- chartProps.yKey = input.yKey ?? 'value';
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({