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
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Definitions for the Tufte primitive chart components in ./tufte-components.tsx.
3
+ *
4
+ * Kept separate from ./definitions.ts and ./extra-definitions.ts so the
5
+ * original chart catalogs stay untouched and the merge in ./catalog.ts is the
6
+ * only contact surface.
7
+ */
8
+
9
+ import { z } from 'zod';
10
+
11
+ export const tufteChartComponentDefinitions = {
12
+ Sparkline: {
13
+ props: z.object({
14
+ title: z.string().nullable(),
15
+ data: z.array(z.record(z.string(), z.unknown())),
16
+ valueKey: z.string(),
17
+ color: z.string().nullable(),
18
+ fill: z.boolean().nullable(),
19
+ showEndDot: z.boolean().nullable(),
20
+ showMinMax: z.boolean().nullable(),
21
+ showValue: z.boolean().nullable(),
22
+ height: z.number().nullable(),
23
+ }),
24
+ description:
25
+ 'Word-sized sparkline: a single trend line with no axes, grid, or labels. Optional end dot, min/max markers, light area fill, and an inline last value. The canonical Tufte primitive for showing a trajectory in minimal space.',
26
+ example: {
27
+ title: 'Latency p95',
28
+ data: [{ t: 0, ms: 120 }, { t: 1, ms: 138 }, { t: 2, ms: 117 }, { t: 3, ms: 152 }, { t: 4, ms: 109 }],
29
+ valueKey: 'ms',
30
+ color: null,
31
+ fill: true,
32
+ showEndDot: true,
33
+ showMinMax: false,
34
+ showValue: true,
35
+ height: null,
36
+ },
37
+ },
38
+
39
+ DotPlot: {
40
+ props: z.object({
41
+ title: z.string().nullable(),
42
+ data: z.array(z.record(z.string(), z.unknown())),
43
+ labelKey: z.string(),
44
+ valueKey: z.string(),
45
+ color: z.string().nullable(),
46
+ sort: z.enum(['asc', 'desc', 'none']).nullable(),
47
+ height: z.number().nullable(),
48
+ }),
49
+ description:
50
+ 'Cleveland dot plot: categorical labels down the Y axis, one dot per category positioned by value on X. Higher data-ink ratio than a bar chart for ranked comparison. Sorts descending by default.',
51
+ example: {
52
+ title: 'Build time by package',
53
+ data: [
54
+ { pkg: 'core', seconds: 42 },
55
+ { pkg: 'client', seconds: 31 },
56
+ { pkg: 'mcp', seconds: 18 },
57
+ { pkg: 'cli', seconds: 9 },
58
+ ],
59
+ labelKey: 'pkg',
60
+ valueKey: 'seconds',
61
+ color: null,
62
+ sort: 'desc',
63
+ height: null,
64
+ },
65
+ },
66
+
67
+ BulletChart: {
68
+ props: z.object({
69
+ title: z.string().nullable(),
70
+ data: z.array(z.record(z.string(), z.unknown())),
71
+ labelKey: z.string().nullable(),
72
+ valueKey: z.string(),
73
+ targetKey: z.string().nullable(),
74
+ rangesKey: z.string().nullable(),
75
+ color: z.string().nullable(),
76
+ height: z.number().nullable(),
77
+ }),
78
+ description:
79
+ "Stephen Few's bullet graph: a measure bar against grayscale qualitative bands with a target tick and per-row scale ticks. Compact KPI-vs-target display. Provide per-row `ranges` (ascending band thresholds) and `target`.",
80
+ example: {
81
+ title: 'Quarterly KPIs vs target',
82
+ data: [
83
+ { label: 'Revenue', value: 84, target: 90, ranges: [50, 75, 100] },
84
+ { label: 'NPS', value: 67, target: 60, ranges: [40, 60, 80] },
85
+ { label: 'Uptime', value: 99, target: 99.9, ranges: [95, 99, 100] },
86
+ ],
87
+ labelKey: 'label',
88
+ valueKey: 'value',
89
+ targetKey: 'target',
90
+ rangesKey: 'ranges',
91
+ color: null,
92
+ height: null,
93
+ },
94
+ },
95
+
96
+ Slopegraph: {
97
+ props: z.object({
98
+ title: z.string().nullable(),
99
+ data: z.array(z.record(z.string(), z.unknown())),
100
+ labelKey: z.string(),
101
+ beforeKey: z.string(),
102
+ afterKey: z.string(),
103
+ beforeLabel: z.string().nullable(),
104
+ afterLabel: z.string().nullable(),
105
+ color: z.string().nullable(),
106
+ colorByDirection: z.boolean().nullable(),
107
+ height: z.number().nullable(),
108
+ }),
109
+ description:
110
+ "Tufte slopegraph: two value columns (before/after) with a connecting line per category. Lines use one neutral ink by default; set colorByDirection to accent rising lines and mute falling ones. Ideal for paired change across many items.",
111
+ example: {
112
+ title: 'Coverage before/after refactor',
113
+ data: [
114
+ { module: 'auth', before: 62, after: 81 },
115
+ { module: 'canvas', before: 74, after: 78 },
116
+ { module: 'mcp', before: 55, after: 49 },
117
+ ],
118
+ labelKey: 'module',
119
+ beforeKey: 'before',
120
+ afterKey: 'after',
121
+ beforeLabel: 'Before',
122
+ afterLabel: 'After',
123
+ color: null,
124
+ colorByDirection: null,
125
+ height: null,
126
+ },
127
+ },
128
+ } as const;
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Custom json-render directives available in PMX Canvas specs.
3
+ *
4
+ * Directives let agent specs declare formatting/derivation ($format, $math,
5
+ * $concat, $count, $truncate, $pluralize, $join) instead of pre-formatting
6
+ * strings. Registered into the iframe renderer via <JSONUIProvider directives>.
7
+ */
8
+ import type { DirectiveDefinition } from '@json-render/core';
9
+ import { standardDirectives } from '@json-render/directives';
10
+
11
+ /**
12
+ * Directives enabled in the PMX Canvas viewer. `standardDirectives` covers the
13
+ * seven stateless directives. The $t i18n directive is intentionally omitted —
14
+ * it is a factory requiring locale config and PMX Canvas has no locale source.
15
+ */
16
+ export const pmxCanvasDirectives: DirectiveDefinition[] = [...standardDirectives];
17
+
18
+ /**
19
+ * The $-prefixed keys the renderer can actually resolve: core built-in bindings
20
+ * plus the names of the directives registered above. Anything else (e.g. a
21
+ * hallucinated `$path`) is NOT a valid dynamic expression — to read a value from
22
+ * state by path the correct binding is `$state`.
23
+ */
24
+ const KNOWN_DYNAMIC_KEYS = new Set<string>([
25
+ '$state',
26
+ '$item',
27
+ '$index',
28
+ '$bindState',
29
+ '$bindItem',
30
+ '$cond',
31
+ '$computed',
32
+ '$template',
33
+ ...pmxCanvasDirectives.map((directive) => directive.name),
34
+ ]);
35
+
36
+ /**
37
+ * True when a prop value is a render-time dynamic expression — a directive
38
+ * (`$format`/`$math`/…) or an existing binding (`$state`/`$item`/`$bindItem`/
39
+ * `$cond`/`$template`/`$computed`). These objects are resolved inside the
40
+ * renderer, so the server-side validators must leave them untouched instead of
41
+ * string-coercing them to `"[object Object]"` or rejecting them as the wrong
42
+ * primitive type. An object whose only `$`-key is unrecognized (e.g. `$path`)
43
+ * is NOT dynamic — the renderer has no directive to resolve it.
44
+ */
45
+ export function isDynamicPropValue(value: unknown): boolean {
46
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) return false;
47
+ return Object.keys(value as Record<string, unknown>).some((key) => KNOWN_DYNAMIC_KEYS.has(key));
48
+ }
49
+
50
+ /**
51
+ * Returns the offending key when a prop value looks like a dynamic expression
52
+ * (it has `$`-prefixed keys) but none of them is a recognized binding or
53
+ * registered directive — e.g. `{ "$path": "…" }`. Such objects pass a naive
54
+ * "has a $-key" check yet render as `"[object Object]"` because the renderer has
55
+ * no directive to resolve them. Returns null for plain values and for genuinely
56
+ * dynamic expressions.
57
+ */
58
+ export function findUnknownDirectiveKey(value: unknown): string | null {
59
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) return null;
60
+ const dollarKeys = Object.keys(value as Record<string, unknown>).filter((key) => key.startsWith('$'));
61
+ if (dollarKeys.length === 0) return null;
62
+ if (dollarKeys.some((key) => KNOWN_DYNAMIC_KEYS.has(key))) return null;
63
+ return dollarKeys[0] ?? null;
64
+ }
@@ -204,12 +204,76 @@ button {
204
204
  color: var(--destructive);
205
205
  }
206
206
 
207
+ .pmx-button {
208
+ display: inline-flex;
209
+ align-items: center;
210
+ justify-content: center;
211
+ gap: 0.5rem;
212
+ height: 2.25rem;
213
+ padding: 0.5rem 1rem;
214
+ border: 1px solid transparent;
215
+ border-radius: var(--radius);
216
+ font-size: 0.875rem;
217
+ font-weight: 500;
218
+ line-height: 1.25rem;
219
+ white-space: nowrap;
220
+ cursor: pointer;
221
+ transition: background-color 0.15s ease, border-color 0.15s ease, opacity 0.15s ease;
222
+ }
223
+
224
+ .pmx-button:disabled {
225
+ pointer-events: none;
226
+ opacity: 0.5;
227
+ }
228
+
229
+ .pmx-button--primary {
230
+ background: var(--primary);
231
+ color: var(--primary-foreground);
232
+ }
233
+
234
+ .pmx-button--secondary {
235
+ background: var(--secondary);
236
+ color: var(--secondary-foreground);
237
+ }
238
+
239
+ .pmx-button--destructive,
240
+ .pmx-button--danger {
241
+ background: var(--destructive);
242
+ color: var(--destructive-foreground);
243
+ }
244
+
245
+ .pmx-button--success {
246
+ background: var(--chart-2);
247
+ color: var(--primary-foreground);
248
+ }
249
+
250
+ .pmx-button--outline {
251
+ border-color: var(--border);
252
+ background: transparent;
253
+ color: var(--foreground);
254
+ }
255
+
256
+ .pmx-button--ghost {
257
+ background: transparent;
258
+ color: var(--foreground);
259
+ }
260
+
261
+ .pmx-button--ghost:hover,
262
+ .pmx-button--outline:hover {
263
+ background: color-mix(in oklch, var(--foreground) 8%, transparent);
264
+ }
265
+
207
266
  /* -- Chart components -- */
208
267
 
209
268
  .pmx-chart {
210
269
  width: 100%;
211
270
  min-width: 280px;
212
- overflow-x: auto;
271
+ /* overflow:visible (not overflow-x:auto) so the chart is NOT its own scroll
272
+ container. overflow-x:auto makes overflow-y compute to auto too (CSS quirk),
273
+ which — once a chart fills the viewport — added a second nested scrollbar on
274
+ top of the iframe document's. Let the single iframe document handle any
275
+ overflow instead. */
276
+ overflow: visible;
213
277
  padding: 0.5rem 0;
214
278
  }
215
279
 
@@ -225,6 +289,48 @@ button {
225
289
  min-width: 360px;
226
290
  }
227
291
 
292
+ .pmx-chart--dot-plot,
293
+ .pmx-chart--bullet,
294
+ .pmx-chart--slopegraph {
295
+ min-width: 320px;
296
+ }
297
+
298
+ .pmx-chart--sparkline {
299
+ min-width: 160px;
300
+ padding: 0.25rem 0;
301
+ }
302
+
303
+ .pmx-chart__sparkline-row {
304
+ display: flex;
305
+ align-items: center;
306
+ gap: 0.5rem;
307
+ }
308
+
309
+ .pmx-chart__sparkline-svg {
310
+ display: block;
311
+ width: 100%;
312
+ height: 36px;
313
+ }
314
+
315
+ .pmx-chart__sparkline-value {
316
+ font-size: 0.8125rem;
317
+ font-weight: 600;
318
+ font-variant-numeric: tabular-nums;
319
+ white-space: nowrap;
320
+ }
321
+
322
+ .pmx-chart__dot-plot-svg,
323
+ .pmx-chart__bullet-svg,
324
+ .pmx-chart__slopegraph-svg {
325
+ display: block;
326
+ }
327
+
328
+ .pmx-chart--dot-plot text,
329
+ .pmx-chart--bullet text,
330
+ .pmx-chart--slopegraph text {
331
+ font-variant-numeric: tabular-nums;
332
+ }
333
+
228
334
  .pmx-chart__title {
229
335
  font-size: 0.875rem;
230
336
  font-weight: 600;
@@ -14,6 +14,9 @@ import { shadcnComponents } from '@json-render/shadcn';
14
14
  import { catalog } from '../catalog';
15
15
  import { chartComponents } from '../charts/components';
16
16
  import { extraChartComponents } from '../charts/extra-components';
17
+ import { tufteChartComponents } from '../charts/tufte-components';
18
+ import { pmxCanvasDirectives } from '../directives';
19
+ import { JsonRenderDevtools } from '@json-render/devtools-react';
17
20
 
18
21
  type BadgeVariant = 'default' | 'secondary' | 'destructive' | 'outline' | 'success' | 'info' | 'warning' | 'error' | 'danger';
19
22
  type BadgeProps = {
@@ -36,12 +39,37 @@ function Badge({ props }: { props: BadgeProps }) {
36
39
  );
37
40
  }
38
41
 
42
+ type ButtonVariant = 'primary' | 'secondary' | 'destructive' | 'danger' | 'outline' | 'ghost' | 'success';
43
+ type ButtonProps = {
44
+ label: string;
45
+ variant?: ButtonVariant | null;
46
+ disabled?: boolean | null;
47
+ };
48
+
49
+ function Button({ props, emit }: { props: ButtonProps; emit: (event: string) => void }) {
50
+ const resolvedVariant = props.variant ?? 'primary';
51
+ return (
52
+ <button
53
+ type="button"
54
+ data-slot="button"
55
+ data-variant={resolvedVariant}
56
+ className={`pmx-button pmx-button--${resolvedVariant}`}
57
+ disabled={props.disabled ?? false}
58
+ onClick={() => emit('press')}
59
+ >
60
+ {props.label}
61
+ </button>
62
+ );
63
+ }
64
+
39
65
  const { registry } = defineRegistry(catalog as never, {
40
66
  components: {
41
67
  ...shadcnComponents,
42
68
  Badge,
69
+ Button,
43
70
  ...chartComponents,
44
71
  ...extraChartComponents,
72
+ ...tufteChartComponents,
45
73
  } as never,
46
74
  });
47
75
 
@@ -50,6 +78,7 @@ declare global {
50
78
  __PMX_CANVAS_JSON_RENDER_SPEC__?: Spec & { state?: Record<string, unknown> };
51
79
  __PMX_CANVAS_JSON_RENDER_THEME__?: string;
52
80
  __PMX_CANVAS_JSON_RENDER_DISPLAY__?: string;
81
+ __PMX_CANVAS_JSON_RENDER_DEVTOOLS__?: boolean;
53
82
  }
54
83
  }
55
84
 
@@ -95,8 +124,12 @@ function App() {
95
124
  <JSONUIProvider
96
125
  registry={registry}
97
126
  initialState={spec.state ?? undefined}
127
+ directives={pmxCanvasDirectives}
98
128
  >
99
129
  <Renderer spec={spec} registry={registry} loading={false} />
130
+ {window.__PMX_CANVAS_JSON_RENDER_DEVTOOLS__ ? (
131
+ <JsonRenderDevtools position="right" />
132
+ ) : null}
100
133
  </JSONUIProvider>
101
134
  </div>
102
135
  );