pmx-canvas 0.1.25 → 0.1.27

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 (63) hide show
  1. package/.github/extensions/pmx-canvas/extension.mjs +191 -0
  2. package/CHANGELOG.md +116 -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 +7 -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 +23 -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 +45 -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 +118 -2
  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 +19 -1
  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 +63 -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 +280 -2
  40. package/src/cli/index.ts +2 -1
  41. package/src/client/nodes/ExtAppFrame.tsx +23 -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 +97 -10
  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 +383 -0
  48. package/src/json-render/charts/tufte-definitions.ts +128 -0
  49. package/src/json-render/directives.ts +29 -0
  50. package/src/json-render/renderer/index.css +101 -0
  51. package/src/json-render/renderer/index.tsx +33 -0
  52. package/src/json-render/server.ts +257 -5
  53. package/src/mcp/canvas-access.ts +261 -0
  54. package/src/mcp/server.ts +500 -7
  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 +107 -0
  59. package/src/server/canvas-schema.ts +26 -3
  60. package/src/server/canvas-state.ts +349 -2
  61. package/src/server/index.ts +250 -2
  62. package/src/server/mutation-history.ts +6 -0
  63. package/src/server/server.ts +428 -2
@@ -0,0 +1,147 @@
1
+ # Core Tufte Principles
2
+
3
+ Reference for the fundamental principles from *The Visual Display of Quantitative Information*.
4
+
5
+ ## Table of Contents
6
+
7
+ 1. [Graphical Integrity](#graphical-integrity)
8
+ 2. [Data-Ink Ratio](#data-ink-ratio)
9
+ 3. [Chartjunk](#chartjunk)
10
+ 4. [Small Multiples](#small-multiples)
11
+ 5. [Data Density](#data-density)
12
+ 6. [The Tufte Test](#the-tufte-test)
13
+
14
+ ---
15
+
16
+ ## Graphical Integrity
17
+
18
+ The representation of numbers on a graphic should be directly proportional to the numerical quantities represented.
19
+
20
+ ### Lie Factor
21
+
22
+ ```
23
+ Lie Factor = (size of effect shown in graphic) / (size of effect in data)
24
+ ```
25
+
26
+ - A lie factor of 1.0 means perfect honesty
27
+ - Lie factors > 1.05 or < 0.95 distort perception
28
+ - Common sources of distortion:
29
+ - Truncated baselines (bar charts not starting at zero)
30
+ - Area/volume encoding where length would suffice
31
+ - 3D perspective making nearer elements appear larger
32
+ - Non-linear axis scaling without clear labeling
33
+
34
+ ### Principles of Graphical Integrity
35
+
36
+ 1. Show data variation, not design variation
37
+ 2. Use clear, detailed, thorough labeling to defeat distortion
38
+ 3. In time-series displays, standardize monetary units (deflate for inflation)
39
+ 4. The number of information-carrying dimensions should not exceed the number of dimensions in the data
40
+ 5. Context: show the data in context - "compared to what?"
41
+
42
+ ---
43
+
44
+ ## Data-Ink Ratio
45
+
46
+ The share of a graphic's ink devoted to non-redundant display of data-information.
47
+
48
+ ```
49
+ Data-Ink Ratio = (data-ink) / (total ink used in graphic)
50
+ ```
51
+
52
+ ### Maximizing Data-Ink
53
+
54
+ Within reason, the goal is to maximize the data-ink ratio:
55
+
56
+ 1. **Erase non-data-ink** - Remove elements that don't convey data (decorative borders, background fills, heavy axis boxes)
57
+ 2. **Erase redundant data-ink** - If the same information is encoded twice (e.g., both a label and a position), remove one
58
+ 3. **Revise and edit** - Iterate toward the simplest possible representation that retains all data meaning
59
+
60
+ ### Practical Applications
61
+
62
+ - Replace heavy gridlines with light ones, or remove entirely if direct labels suffice
63
+ - Use range-frame axes (axes that span only the data range, not arbitrary round numbers)
64
+ - Remove chart borders/boxes
65
+ - Use white gridlines on a light gray background (Tufte's "sparkline" aesthetic)
66
+ - Let data points serve as their own tick marks where possible
67
+
68
+ ---
69
+
70
+ ## Chartjunk
71
+
72
+ Non-data elements or redundant data elements that clutter a visualization without adding information.
73
+
74
+ ### Three Varieties
75
+
76
+ 1. **Unintentional optical art** - Moiré patterns from hatching, vibrating fills, and tight parallel lines that create visual interference
77
+ 2. **The grid** - Heavy, prominent grids that compete with or dominate the data
78
+ 3. **The duck** - Elaborate decorative structures built around the data (3D effects, pictorial embellishments, ornamental frames)
79
+
80
+ ### How to Eliminate
81
+
82
+ - Default to no background fill, no border, no grid
83
+ - Add gridlines only if the reader needs to extract precise values - and make them light/receding
84
+ - Never use 3D unless the data is inherently three-dimensional
85
+ - Remove legends when direct labeling is feasible
86
+ - Remove decorative icons, clip art, or illustrations layered onto the data area
87
+
88
+ ---
89
+
90
+ ## Small Multiples
91
+
92
+ A series of similar graphs or charts using the same scale and axes, allowing easy comparison across a varying condition.
93
+
94
+ ### When to Use
95
+
96
+ - Comparing the same measure across many categories (regions, time periods, groups)
97
+ - Showing change over time for multiple entities
98
+ - Exploring multivariate data by faceting on one dimension
99
+ - Whenever you're tempted to use color-coding to distinguish many overlapping series
100
+
101
+ ### Design Principles
102
+
103
+ 1. **Shared scales** - All panels must use identical axis ranges so position means the same thing everywhere
104
+ 2. **Consistent structure** - Same layout, same visual encoding in each panel
105
+ 3. **Minimal per-panel decoration** - Axis labels, ticks, and titles appear once (shared) rather than repeated in each panel
106
+ 4. **Clear ordering** - Arrange panels in a meaningful order (alphabetical, by outcome magnitude, by geography)
107
+ 5. **Reference elements** - Include a common reference (e.g., overall average) as a light/gray line in each panel for context
108
+
109
+ ### Density
110
+
111
+ Small multiples can be packed tightly. Each panel should be large enough to reveal the data pattern but small enough that the eye can compare across many panels in a single view.
112
+
113
+ ---
114
+
115
+ ## Data Density
116
+
117
+ The amount of data shown per unit area in a graphic.
118
+
119
+ ```
120
+ Data Density = (number of entries in data matrix) / (area of data graphic)
121
+ ```
122
+
123
+ ### High-Density Displays
124
+
125
+ - Most published graphics have far lower data density than they could achieve
126
+ - Sparklines: intense, simple, word-sized graphics embedded in text or tables
127
+ - Data tables with integrated graphical elements can achieve very high density
128
+ - The human eye can resolve very fine differences - don't underestimate the reader
129
+
130
+ ### Shrink Principle
131
+
132
+ Graphics can be shrunk far more than we usually think. A well-designed graphic retains meaning at small sizes because the data pattern (not the labels or decoration) carries the story.
133
+
134
+ ---
135
+
136
+ ## The Tufte Test
137
+
138
+ A synthesis checklist for evaluating any completed visualization:
139
+
140
+ 1. **Is the lie factor close to 1.0?** - No distortions in area, length, or position
141
+ 2. **Is the data-ink ratio high?** - Could anything be erased without information loss?
142
+ 3. **Is there zero chartjunk?** - No decoration, no moiré, no unnecessary 3D
143
+ 4. **Does it answer "compared to what?"** - Context, baseline, or reference is present
144
+ 5. **Is labeling clear and integrated?** - Labels sit close to the data they describe, not in a distant legend
145
+ 6. **Is the data density appropriate?** - For the story being told, is enough data shown? Could a table or sparkline show more?
146
+ 7. **Would small multiples work better?** - If more than ~4 series overlap, consider faceting
147
+ 8. **Does every element earn its ink?** - One final pass: point to each mark and ask what it communicates
package/src/cli/agent.ts CHANGED
@@ -11,7 +11,9 @@
11
11
  * - --yes for destructive actions, --dry-run for preview
12
12
  */
13
13
 
14
- import { readFileSync, writeFileSync } from 'node:fs';
14
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
15
+ import { dirname, join } from 'node:path';
16
+ import { fileURLToPath } from 'node:url';
15
17
  import { openUrlInExternalBrowser, wrapCanvasAutomationScript } from '../server/server.js';
16
18
  import { DEFAULT_EXCALIDRAW_ELEMENTS } from '../server/diagram-presets.js';
17
19
  import {
@@ -166,6 +168,7 @@ function parseFlags(args: string[]): { positional: string[]; flags: Record<strin
166
168
  'help', 'h', 'ids', 'stdin', 'yes', 'list', 'clear', 'set', 'animated', 'dry-run', 'all',
167
169
  'no-open-in-canvas', 'lock-arrange', 'unlock-arrange', 'json', 'compact',
168
170
  'verbose', 'include-logs', 'no-pan', 'schema', 'example', 'examples', 'strict-size', 'scroll-overflow',
171
+ 'report', 'canvas', 'hooks', 'tools', 'session-messaging', 'permissions', 'files', 'ui-prompts',
169
172
  ]);
170
173
  for (let i = 0; i < args.length; i++) {
171
174
  const arg = args[i];
@@ -183,7 +186,12 @@ function parseFlags(args: string[]): { positional: string[]; flags: Record<strin
183
186
  }
184
187
  }
185
188
  } else if (arg.startsWith('-') && arg.length === 2) {
186
- flags[arg.slice(1)] = true;
189
+ const key = arg.slice(1);
190
+ if (!BOOL_FLAGS.has(key) && i + 1 < args.length && !args[i + 1].startsWith('-')) {
191
+ flags[key] = args[++i];
192
+ } else {
193
+ flags[key] = true;
194
+ }
187
195
  } else {
188
196
  positional.push(arg);
189
197
  }
@@ -1102,6 +1110,7 @@ cmd('node add', 'Add a node to the canvas', [
1102
1110
  'pmx-canvas node add --type file --content "src/index.ts"',
1103
1111
  'pmx-canvas node add --type webpage --url "https://example.com/docs"',
1104
1112
  'pmx-canvas node add --type html --title "Widget" --content "<main>Hello</main>"',
1113
+ 'pmx-canvas node add --type html --title "Showcase" --content ./report.html (a .html path is read from disk; otherwise --content is raw HTML)',
1105
1114
  'pmx-canvas node add --type html --primitive choice-grid --data-file ./options.json --title "Options"',
1106
1115
  'pmx-canvas node add --type markdown --title "Note" --x 100 --y 200',
1107
1116
  'pmx-canvas node add --type json-render --title "Ops Dashboard" --spec-file ./dashboard.json',
@@ -1801,6 +1810,275 @@ cmd('ax focus', 'Set or clear PMX AX focus without moving the viewport', [
1801
1810
  output(await api('POST', '/api/canvas/ax/focus', { nodeIds, source: 'cli' }));
1802
1811
  });
1803
1812
 
1813
+ cmd('ax event add', 'Record a normalized AX timeline event', [
1814
+ 'pmx-canvas ax event add --kind tool-start --summary "ran tests"',
1815
+ 'pmx-canvas ax event add --kind failure --summary "build broke" --detail "..." node1 node2',
1816
+ ], async (args) => {
1817
+ const { positional, flags } = parseFlags(args);
1818
+ if (flags.help || flags.h) return showCommandHelp('ax event add');
1819
+
1820
+ const kind = requireFlag(flags, 'kind', 'pmx-canvas ax event add --kind <kind> --summary <text>');
1821
+ const summary = requireFlag(flags, 'summary', 'pmx-canvas ax event add --kind <kind> --summary <text>');
1822
+ const detail = getStringFlag(flags, 'detail');
1823
+
1824
+ output(await api('POST', '/api/canvas/ax/event', {
1825
+ kind,
1826
+ summary,
1827
+ ...(detail ? { detail } : {}),
1828
+ ...(positional.length > 0 ? { nodeIds: positional } : {}),
1829
+ source: 'cli',
1830
+ }));
1831
+ });
1832
+
1833
+ cmd('ax steer', 'Send a steering message to the active agent session', [
1834
+ 'pmx-canvas ax steer "focus on the failing test first"',
1835
+ 'pmx-canvas ax steer --message "stop and re-plan"',
1836
+ ], async (args) => {
1837
+ const { positional, flags } = parseFlags(args);
1838
+ if (flags.help || flags.h) return showCommandHelp('ax steer');
1839
+
1840
+ const message = getStringFlag(flags, 'message') ?? positional.join(' ').trim();
1841
+ if (!message) {
1842
+ die('Missing steering message', 'pmx-canvas ax steer <message>');
1843
+ }
1844
+
1845
+ output(await api('POST', '/api/canvas/ax/steer', { message, source: 'cli' }));
1846
+ });
1847
+
1848
+ cmd('ax timeline', 'Read the bounded AX timeline (events, evidence, steering)', [
1849
+ 'pmx-canvas ax timeline',
1850
+ 'pmx-canvas ax timeline --limit 100',
1851
+ ], async (args) => {
1852
+ const { flags } = parseFlags(args);
1853
+ if (flags.help || flags.h) return showCommandHelp('ax timeline');
1854
+
1855
+ const limit = optionalNumberFlag(flags, 'limit', 'pmx-canvas ax timeline --limit <n>');
1856
+ output(await api('GET', `/api/canvas/ax/timeline${limit ? `?limit=${limit}` : ''}`));
1857
+ });
1858
+
1859
+ cmd('ax work add', 'Add a canvas-bound AX work item', [
1860
+ 'pmx-canvas ax work add --title "Wire up auth" --status in-progress',
1861
+ 'pmx-canvas ax work add --title "Review API" node1 node2',
1862
+ ], async (args) => {
1863
+ const { positional, flags } = parseFlags(args);
1864
+ if (flags.help || flags.h) return showCommandHelp('ax work add');
1865
+
1866
+ const title = requireFlag(flags, 'title', 'pmx-canvas ax work add --title <text>');
1867
+ const status = getStringFlag(flags, 'status');
1868
+ const detail = getStringFlag(flags, 'detail');
1869
+
1870
+ output(await api('POST', '/api/canvas/ax/work', {
1871
+ title,
1872
+ ...(status ? { status } : {}),
1873
+ ...(detail ? { detail } : {}),
1874
+ ...(positional.length > 0 ? { nodeIds: positional } : {}),
1875
+ source: 'cli',
1876
+ }));
1877
+ });
1878
+
1879
+ cmd('ax work update', 'Update a canvas-bound AX work item by ID', [
1880
+ 'pmx-canvas ax work update <id> --status done',
1881
+ 'pmx-canvas ax work update <id> --title "New title" --detail "..."',
1882
+ ], async (args) => {
1883
+ const { positional, flags } = parseFlags(args);
1884
+ if (flags.help || flags.h) return showCommandHelp('ax work update');
1885
+
1886
+ const id = positional[0];
1887
+ if (!id) die('Missing work item ID', 'pmx-canvas ax work update <id> --status <status>');
1888
+ const title = getStringFlag(flags, 'title');
1889
+ const status = getStringFlag(flags, 'status');
1890
+ const detail = getStringFlag(flags, 'detail');
1891
+
1892
+ output(await api('PATCH', `/api/canvas/ax/work/${encodeURIComponent(id)}`, {
1893
+ ...(title ? { title } : {}),
1894
+ ...(status ? { status } : {}),
1895
+ ...(detail ? { detail } : {}),
1896
+ ...(positional.length > 1 ? { nodeIds: positional.slice(1) } : {}),
1897
+ source: 'cli',
1898
+ }));
1899
+ });
1900
+
1901
+ cmd('ax work list', 'List canvas-bound AX work items', [
1902
+ 'pmx-canvas ax work list',
1903
+ ], async (args) => {
1904
+ const { flags } = parseFlags(args);
1905
+ if (flags.help || flags.h) return showCommandHelp('ax work list');
1906
+
1907
+ output(await api('GET', '/api/canvas/ax/work'));
1908
+ });
1909
+
1910
+ cmd('ax approval request', 'Request a canvas-bound AX approval gate', [
1911
+ 'pmx-canvas ax approval request --title "Deploy to prod"',
1912
+ 'pmx-canvas ax approval request --title "Drop table" --action db.drop node1',
1913
+ ], async (args) => {
1914
+ const { positional, flags } = parseFlags(args);
1915
+ if (flags.help || flags.h) return showCommandHelp('ax approval request');
1916
+
1917
+ const title = requireFlag(flags, 'title', 'pmx-canvas ax approval request --title <text>');
1918
+ const detail = getStringFlag(flags, 'detail');
1919
+ const action = getStringFlag(flags, 'action');
1920
+
1921
+ output(await api('POST', '/api/canvas/ax/approval', {
1922
+ title,
1923
+ ...(detail ? { detail } : {}),
1924
+ ...(action ? { action } : {}),
1925
+ ...(positional.length > 0 ? { nodeIds: positional } : {}),
1926
+ source: 'cli',
1927
+ }));
1928
+ });
1929
+
1930
+ cmd('ax approval resolve', 'Resolve a pending AX approval gate by ID', [
1931
+ 'pmx-canvas ax approval resolve <id> --decision approved',
1932
+ 'pmx-canvas ax approval resolve <id> --decision rejected --resolution "too risky"',
1933
+ ], async (args) => {
1934
+ const { positional, flags } = parseFlags(args);
1935
+ if (flags.help || flags.h) return showCommandHelp('ax approval resolve');
1936
+
1937
+ const id = positional[0];
1938
+ if (!id) die('Missing approval gate ID', 'pmx-canvas ax approval resolve <id> --decision <approved|rejected>');
1939
+ const decision = requireFlag(flags, 'decision', 'pmx-canvas ax approval resolve <id> --decision <approved|rejected>');
1940
+ if (decision !== 'approved' && decision !== 'rejected') {
1941
+ die('Invalid decision', 'pmx-canvas ax approval resolve <id> --decision <approved|rejected>');
1942
+ }
1943
+ const resolution = getStringFlag(flags, 'resolution');
1944
+
1945
+ output(await api('POST', `/api/canvas/ax/approval/${encodeURIComponent(id)}/resolve`, {
1946
+ decision,
1947
+ ...(resolution ? { resolution } : {}),
1948
+ source: 'cli',
1949
+ }));
1950
+ });
1951
+
1952
+ cmd('ax approval list', 'List canvas-bound AX approval gates', [
1953
+ 'pmx-canvas ax approval list',
1954
+ ], async (args) => {
1955
+ const { flags } = parseFlags(args);
1956
+ if (flags.help || flags.h) return showCommandHelp('ax approval list');
1957
+
1958
+ output(await api('GET', '/api/canvas/ax/approval'));
1959
+ });
1960
+
1961
+ cmd('ax evidence add', 'Record an AX evidence item on the timeline', [
1962
+ 'pmx-canvas ax evidence add --kind test-output --title "unit pass" --body "..."',
1963
+ 'pmx-canvas ax evidence add --kind screenshot --title "before" --ref /tmp/before.png node1',
1964
+ ], async (args) => {
1965
+ const { positional, flags } = parseFlags(args);
1966
+ if (flags.help || flags.h) return showCommandHelp('ax evidence add');
1967
+
1968
+ const kind = requireFlag(flags, 'kind', 'pmx-canvas ax evidence add --kind <kind> --title <text>');
1969
+ const title = requireFlag(flags, 'title', 'pmx-canvas ax evidence add --kind <kind> --title <text>');
1970
+ const body = getStringFlag(flags, 'body');
1971
+ const ref = getStringFlag(flags, 'ref');
1972
+
1973
+ output(await api('POST', '/api/canvas/ax/evidence', {
1974
+ kind,
1975
+ title,
1976
+ ...(body ? { body } : {}),
1977
+ ...(ref ? { ref } : {}),
1978
+ ...(positional.length > 0 ? { nodeIds: positional } : {}),
1979
+ source: 'cli',
1980
+ }));
1981
+ });
1982
+
1983
+ cmd('ax review add', 'Add a canvas-bound AX review annotation', [
1984
+ 'pmx-canvas ax review add --body "needs a test" --node node1',
1985
+ 'pmx-canvas ax review add --body "off-by-one" --kind finding --severity error --file src/x.ts',
1986
+ ], async (args) => {
1987
+ const { flags } = parseFlags(args);
1988
+ if (flags.help || flags.h) return showCommandHelp('ax review add');
1989
+
1990
+ const body = requireFlag(flags, 'body', 'pmx-canvas ax review add --body <text>');
1991
+ const kind = getStringFlag(flags, 'kind');
1992
+ const severity = getStringFlag(flags, 'severity');
1993
+ const anchorType = getStringFlag(flags, 'anchor');
1994
+ const nodeId = getStringFlag(flags, 'node');
1995
+ const file = getStringFlag(flags, 'file');
1996
+ const author = getStringFlag(flags, 'author');
1997
+
1998
+ output(await api('POST', '/api/canvas/ax/review', {
1999
+ body,
2000
+ ...(kind ? { kind } : {}),
2001
+ ...(severity ? { severity } : {}),
2002
+ ...(anchorType ? { anchorType } : {}),
2003
+ ...(nodeId ? { nodeId } : {}),
2004
+ ...(file ? { file } : {}),
2005
+ ...(author ? { author } : {}),
2006
+ source: 'cli',
2007
+ }));
2008
+ });
2009
+
2010
+ cmd('ax review list', 'List canvas-bound AX review annotations', [
2011
+ 'pmx-canvas ax review list',
2012
+ ], async (args) => {
2013
+ const { flags } = parseFlags(args);
2014
+ if (flags.help || flags.h) return showCommandHelp('ax review list');
2015
+
2016
+ output(await api('GET', '/api/canvas/ax/review'));
2017
+ });
2018
+
2019
+ cmd('ax host report', 'Report host/session capability to the canvas', [
2020
+ 'pmx-canvas ax host report --host copilot --canvas --tools --session-messaging',
2021
+ 'pmx-canvas ax host report --host codex --canvas --files',
2022
+ ], async (args) => {
2023
+ const { flags } = parseFlags(args);
2024
+ if (flags.help || flags.h) return showCommandHelp('ax host report');
2025
+
2026
+ const host = getStringFlag(flags, 'host');
2027
+
2028
+ output(await api('PUT', '/api/canvas/ax/host-capability', {
2029
+ ...(host ? { host } : {}),
2030
+ canvas: flags.canvas === true,
2031
+ hooks: flags.hooks === true,
2032
+ tools: flags.tools === true,
2033
+ sessionMessaging: flags['session-messaging'] === true,
2034
+ permissions: flags.permissions === true,
2035
+ files: flags.files === true,
2036
+ uiPrompts: flags['ui-prompts'] === true,
2037
+ source: 'cli',
2038
+ }));
2039
+ });
2040
+
2041
+ cmd('ax host status', 'Read the reported host/session capability', [
2042
+ 'pmx-canvas ax host status',
2043
+ ], async (args) => {
2044
+ const { flags } = parseFlags(args);
2045
+ if (flags.help || flags.h) return showCommandHelp('ax host status');
2046
+
2047
+ output(await api('GET', '/api/canvas/ax/host-capability'));
2048
+ });
2049
+
2050
+ // ── copilot install-extension ────────────────────────────────
2051
+ cmd('copilot install-extension', 'Install the bundled GitHub Copilot extension adapter', [
2052
+ 'pmx-canvas copilot install-extension --dry-run',
2053
+ 'pmx-canvas copilot install-extension --target .github/extensions/pmx-canvas/extension.mjs --yes',
2054
+ ], async (args) => {
2055
+ const { flags } = parseFlags(args);
2056
+ if (flags.help || flags.h) return showCommandHelp('copilot install-extension');
2057
+
2058
+ const sourcePath = fileURLToPath(new URL('../../.github/extensions/pmx-canvas/extension.mjs', import.meta.url));
2059
+ if (!existsSync(sourcePath)) {
2060
+ die('Bundled Copilot extension adapter not found.', `Expected at ${sourcePath}`);
2061
+ }
2062
+
2063
+ const targetPath = getStringFlag(flags, 'target')
2064
+ ?? join(process.cwd(), '.github', 'extensions', 'pmx-canvas', 'extension.mjs');
2065
+ const dryRun = flags['dry-run'] === true;
2066
+ const targetExists = existsSync(targetPath);
2067
+
2068
+ if (dryRun) {
2069
+ output({ ok: true, dryRun: true, sourcePath, targetPath, targetExists, wrote: false });
2070
+ return;
2071
+ }
2072
+
2073
+ if (targetExists && flags.yes !== true) {
2074
+ die('Target already exists. Re-run with --yes to overwrite.', `Target: ${targetPath}`);
2075
+ }
2076
+
2077
+ mkdirSync(dirname(targetPath), { recursive: true });
2078
+ copyFileSync(sourcePath, targetPath);
2079
+ output({ ok: true, dryRun: false, sourcePath, targetPath, wrote: true });
2080
+ });
2081
+
1804
2082
  // ── undo ─────────────────────────────────────────────────────
1805
2083
  cmd('undo', 'Undo the last canvas mutation', [
1806
2084
  'pmx-canvas undo',
package/src/cli/index.ts CHANGED
@@ -32,7 +32,7 @@ if (args.includes('--version') || args.includes('-v')) {
32
32
  const AGENT_COMMANDS = new Set([
33
33
  'node', 'edge', 'json-render', 'search', 'layout', 'status', 'arrange', 'focus',
34
34
  'fit', 'screenshot', 'pin', 'ax', 'undo', 'redo', 'history', 'snapshot', 'diff', 'group', 'webview', 'open',
35
- 'clear', 'code-graph', 'spatial', 'watch', 'web-artifact', 'external-app', 'diagram', 'graph', 'html', 'batch', 'validate', 'serve',
35
+ 'clear', 'code-graph', 'spatial', 'watch', 'web-artifact', 'external-app', 'diagram', 'graph', 'html', 'batch', 'validate', 'serve', 'copilot',
36
36
  ]);
37
37
 
38
38
  const firstArg = args[0] ?? '';
@@ -563,6 +563,7 @@ Examples:
563
563
  pmx-canvas validate Check layout collisions
564
564
  pmx-canvas watch --events context-pin,move-end Watch semantic deltas
565
565
  pmx-canvas clear --dry-run Preview destructive op
566
+ pmx-canvas copilot install-extension --dry-run Preview Copilot adapter install
566
567
  `);
567
568
  process.exit(0);
568
569
  }
@@ -8,8 +8,12 @@ import {
8
8
  collapseExpandedNode,
9
9
  expandNode,
10
10
  expandedNodeId,
11
+ nodes,
12
+ persistLayout,
13
+ resizeNode,
11
14
  } from '../state/canvas-store';
12
15
  import type { CanvasNodeState } from '../types';
16
+ import { AUTO_FIT_TITLEBAR_HEIGHT } from '../canvas/auto-fit';
13
17
  import { useIframeDocument } from './iframe-document-url';
14
18
 
15
19
  type McpUiTheme = 'light' | 'dark';
@@ -120,6 +124,7 @@ export function ExtAppFrame({ node, expanded = false }: { node: CanvasNodeState;
120
124
  const iframeRef = useRef<HTMLIFrameElement>(null);
121
125
  const bridgeRef = useRef<AppBridge | null>(null);
122
126
  const transportRef = useRef<PostMessageTransport | null>(null);
127
+ const sizePersistTimerRef = useRef<number | null>(null);
123
128
  const latestToolInputRef = useRef<Record<string, unknown>>({});
124
129
  const latestToolResultRef = useRef<CallToolResult | undefined>(undefined);
125
130
  const toolResultSentRef = useRef(false);
@@ -283,7 +288,20 @@ export function ExtAppFrame({ node, expanded = false }: { node: CanvasNodeState;
283
288
  width: node.size.width,
284
289
  height: maxHeight,
285
290
  });
286
- iframe.style.height = `${resolveExtAppInlineFrameHeight(height, hostDimensions.height)}px`;
291
+ const inlineFrameHeight = resolveExtAppInlineFrameHeight(height, hostDimensions.height);
292
+ iframe.style.height = `${inlineFrameHeight}px`;
293
+ const currentSize = nodes.value.get(nodeId)?.size ?? node.size;
294
+ const nodeHeight = Math.max(currentSize.height, inlineFrameHeight + AUTO_FIT_TITLEBAR_HEIGHT);
295
+ if (Math.abs(nodeHeight - currentSize.height) > 8) {
296
+ resizeNode(nodeId, { width: currentSize.width, height: nodeHeight });
297
+ if (sizePersistTimerRef.current !== null) {
298
+ window.clearTimeout(sizePersistTimerRef.current);
299
+ }
300
+ sizePersistTimerRef.current = window.setTimeout(() => {
301
+ persistLayout({ recordHistory: false });
302
+ sizePersistTimerRef.current = null;
303
+ }, 0);
304
+ }
287
305
  }
288
306
  return {};
289
307
  };
@@ -464,6 +482,10 @@ export function ExtAppFrame({ node, expanded = false }: { node: CanvasNodeState;
464
482
  toolResultSendingRef.current = null;
465
483
  themeUnsubRef.current?.();
466
484
  themeUnsubRef.current = null;
485
+ if (sizePersistTimerRef.current !== null) {
486
+ window.clearTimeout(sizePersistTimerRef.current);
487
+ sizePersistTimerRef.current = null;
488
+ }
467
489
  bridgeRef.current = null;
468
490
  if (transportRef.current) {
469
491
  transportRef.current.close().catch((closeError) => {
@@ -2,12 +2,15 @@ import type { CanvasNodeState } from '../types';
2
2
  import { canvasTheme } from '../state/canvas-store';
3
3
  import { ExtAppFrame } from './ExtAppFrame';
4
4
 
5
- function withViewerParams(url: string, expanded: boolean): string {
5
+ function withViewerParams(url: string, expanded: boolean, specVersion?: number): string {
6
6
  if (!url) return url;
7
7
  try {
8
8
  const resolved = new URL(url, window.location.origin);
9
9
  resolved.searchParams.set('theme', canvasTheme.value === 'light' ? 'light' : 'dark');
10
10
  if (expanded) resolved.searchParams.set('display', 'expanded');
11
+ // Streaming json-render nodes bump specVersion as patches accumulate; including
12
+ // it in the src reloads the iframe so it re-reads the latest accumulated spec.
13
+ if (typeof specVersion === 'number') resolved.searchParams.set('v', String(specVersion));
11
14
  return resolved.toString();
12
15
  } catch {
13
16
  return url;
@@ -31,7 +34,8 @@ export function McpAppNode({ node, expanded = false }: { node: CanvasNodeState;
31
34
  return <ExtAppFrame node={node} expanded={expanded} />;
32
35
  }
33
36
 
34
- const url = withViewerParams((node.data.url as string) || '', expanded);
37
+ const specVersion = typeof node.data.specVersion === 'number' ? node.data.specVersion : undefined;
38
+ const url = withViewerParams((node.data.url as string) || '', expanded, specVersion);
35
39
  const sourceServer = (node.data.sourceServer as string) || '';
36
40
  const hostMode = (node.data.hostMode as string) || 'hosted';
37
41
  const fallbackReason = node.data.fallbackReason as string | undefined;
@@ -12,8 +12,11 @@ import { schema } from './schema.js';
12
12
  import { shadcnComponentDefinitions } from '@json-render/shadcn/catalog';
13
13
  import { chartComponentDefinitions } from './charts/definitions';
14
14
  import { extraChartComponentDefinitions } from './charts/extra-definitions';
15
+ import { tufteChartComponentDefinitions } from './charts/tufte-definitions';
16
+ import { isDynamicPropValue } from './directives.js';
15
17
 
16
18
  const badgeDefinition = shadcnComponentDefinitions.Badge;
19
+ const buttonDefinition = shadcnComponentDefinitions.Button;
17
20
 
18
21
  export const allComponentDefinitions = {
19
22
  ...shadcnComponentDefinitions,
@@ -23,8 +26,18 @@ export const allComponentDefinitions = {
23
26
  variant: z.enum(['default', 'secondary', 'destructive', 'outline', 'success', 'info', 'warning', 'error', 'danger']).nullable(),
24
27
  }),
25
28
  },
29
+ Button: {
30
+ ...buttonDefinition,
31
+ props: buttonDefinition.props.extend({
32
+ variant: z.enum(['primary', 'secondary', 'destructive', 'danger', 'outline', 'ghost', 'success']).nullable(),
33
+ }),
34
+ description:
35
+ 'Clickable button. Bind on.press for handler. Choose variant by intent — at most ONE primary per view (the main call to action); use secondary for supporting actions; destructive (alias danger) for delete/remove/irreversible actions; outline/ghost for low-emphasis or toolbar actions; success to confirm a positive result. Omitting variant renders primary, so reserve the default for the single main action and set an explicit variant on every other button.',
36
+ example: { label: 'Save changes', variant: 'primary' },
37
+ },
26
38
  ...chartComponentDefinitions,
27
39
  ...extraChartComponentDefinitions,
40
+ ...tufteChartComponentDefinitions,
28
41
  };
29
42
 
30
43
  export const catalog = defineCatalog(schema as never, {
@@ -241,8 +254,9 @@ export function validateShadcnElementProps(spec: unknown): JsonRenderValidationR
241
254
  const definition = allComponentDefinitions[element.type as keyof typeof allComponentDefinitions];
242
255
  if (!definition || !hasSafeParse(definition.props)) continue;
243
256
 
257
+ const elementProps = asRecord(element.props) ?? {};
244
258
  const parsed = definition.props.safeParse(
245
- normalizePropsForSchema(definition.props, asRecord(element.props) ?? {}),
259
+ normalizePropsForSchema(definition.props, elementProps),
246
260
  );
247
261
  if (parsed.success) continue;
248
262
 
@@ -250,6 +264,13 @@ export function validateShadcnElementProps(spec: unknown): JsonRenderValidationR
250
264
  const issuePath = Array.isArray(issue.path)
251
265
  ? issue.path.map((segment) => (typeof segment === 'symbol' ? String(segment) : segment))
252
266
  : [];
267
+ // A prop holding a `$`-keyed dynamic expression ($state/$item/$format/…)
268
+ // resolves to its real type at render time, so the static prop schema
269
+ // cannot type-check it — drop issues that point at such a prop.
270
+ const propKey = issuePath[0];
271
+ if (typeof propKey === 'string' && isDynamicPropValue(elementProps[propKey])) {
272
+ continue;
273
+ }
253
274
  issues.push({
254
275
  path: ['elements', elementKey, 'props', ...issuePath],
255
276
  message: issue.message ?? 'invalid value',