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
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
|
-
|
|
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
|
}
|
|
@@ -797,7 +805,28 @@ async function buildWebArtifactRequestBody(
|
|
|
797
805
|
}
|
|
798
806
|
|
|
799
807
|
async function runWebArtifactBuildCommand(flags: Record<string, string | true>): Promise<void> {
|
|
800
|
-
const
|
|
808
|
+
const body = await buildWebArtifactRequestBody(flags);
|
|
809
|
+
// The build (init + dependency install + bundle) runs server-side and only
|
|
810
|
+
// returns a single HTTP response on completion, which can take minutes on a
|
|
811
|
+
// cold workspace. With no output an agent's tool wait expires before the node
|
|
812
|
+
// appears and the build looks hung. Emit a default-on heartbeat to stderr
|
|
813
|
+
// while the request is in flight — stdout (output) and the JSON response body
|
|
814
|
+
// stay untouched, so anything parsing stdout is unaffected.
|
|
815
|
+
const startedMs = Date.now();
|
|
816
|
+
process.stderr.write(
|
|
817
|
+
`[web-artifact] building "${String(body.title)}" — init + install + bundle (this can take a few minutes)...\n`,
|
|
818
|
+
);
|
|
819
|
+
const heartbeat = setInterval(() => {
|
|
820
|
+
const elapsedSeconds = Math.round((Date.now() - startedMs) / 1000);
|
|
821
|
+
process.stderr.write(`[web-artifact] still building... ${elapsedSeconds}s elapsed\n`);
|
|
822
|
+
}, 10_000);
|
|
823
|
+
if (typeof heartbeat.unref === 'function') heartbeat.unref();
|
|
824
|
+
let result: unknown;
|
|
825
|
+
try {
|
|
826
|
+
result = await api('POST', '/api/canvas/web-artifact', body, { allowErrorJson: true });
|
|
827
|
+
} finally {
|
|
828
|
+
clearInterval(heartbeat);
|
|
829
|
+
}
|
|
801
830
|
output(result);
|
|
802
831
|
if (isRecord(result) && result.ok === false) {
|
|
803
832
|
process.exit(1);
|
|
@@ -1102,6 +1131,7 @@ cmd('node add', 'Add a node to the canvas', [
|
|
|
1102
1131
|
'pmx-canvas node add --type file --content "src/index.ts"',
|
|
1103
1132
|
'pmx-canvas node add --type webpage --url "https://example.com/docs"',
|
|
1104
1133
|
'pmx-canvas node add --type html --title "Widget" --content "<main>Hello</main>"',
|
|
1134
|
+
'pmx-canvas node add --type html --title "Showcase" --content ./report.html (a .html path is read from disk; otherwise --content is raw HTML)',
|
|
1105
1135
|
'pmx-canvas node add --type html --primitive choice-grid --data-file ./options.json --title "Options"',
|
|
1106
1136
|
'pmx-canvas node add --type markdown --title "Note" --x 100 --y 200',
|
|
1107
1137
|
'pmx-canvas node add --type json-render --title "Ops Dashboard" --spec-file ./dashboard.json',
|
|
@@ -1801,6 +1831,275 @@ cmd('ax focus', 'Set or clear PMX AX focus without moving the viewport', [
|
|
|
1801
1831
|
output(await api('POST', '/api/canvas/ax/focus', { nodeIds, source: 'cli' }));
|
|
1802
1832
|
});
|
|
1803
1833
|
|
|
1834
|
+
cmd('ax event add', 'Record a normalized AX timeline event', [
|
|
1835
|
+
'pmx-canvas ax event add --kind tool-start --summary "ran tests"',
|
|
1836
|
+
'pmx-canvas ax event add --kind failure --summary "build broke" --detail "..." node1 node2',
|
|
1837
|
+
], async (args) => {
|
|
1838
|
+
const { positional, flags } = parseFlags(args);
|
|
1839
|
+
if (flags.help || flags.h) return showCommandHelp('ax event add');
|
|
1840
|
+
|
|
1841
|
+
const kind = requireFlag(flags, 'kind', 'pmx-canvas ax event add --kind <kind> --summary <text>');
|
|
1842
|
+
const summary = requireFlag(flags, 'summary', 'pmx-canvas ax event add --kind <kind> --summary <text>');
|
|
1843
|
+
const detail = getStringFlag(flags, 'detail');
|
|
1844
|
+
|
|
1845
|
+
output(await api('POST', '/api/canvas/ax/event', {
|
|
1846
|
+
kind,
|
|
1847
|
+
summary,
|
|
1848
|
+
...(detail ? { detail } : {}),
|
|
1849
|
+
...(positional.length > 0 ? { nodeIds: positional } : {}),
|
|
1850
|
+
source: 'cli',
|
|
1851
|
+
}));
|
|
1852
|
+
});
|
|
1853
|
+
|
|
1854
|
+
cmd('ax steer', 'Send a steering message to the active agent session', [
|
|
1855
|
+
'pmx-canvas ax steer "focus on the failing test first"',
|
|
1856
|
+
'pmx-canvas ax steer --message "stop and re-plan"',
|
|
1857
|
+
], async (args) => {
|
|
1858
|
+
const { positional, flags } = parseFlags(args);
|
|
1859
|
+
if (flags.help || flags.h) return showCommandHelp('ax steer');
|
|
1860
|
+
|
|
1861
|
+
const message = getStringFlag(flags, 'message') ?? positional.join(' ').trim();
|
|
1862
|
+
if (!message) {
|
|
1863
|
+
die('Missing steering message', 'pmx-canvas ax steer <message>');
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
output(await api('POST', '/api/canvas/ax/steer', { message, source: 'cli' }));
|
|
1867
|
+
});
|
|
1868
|
+
|
|
1869
|
+
cmd('ax timeline', 'Read the bounded AX timeline (events, evidence, steering)', [
|
|
1870
|
+
'pmx-canvas ax timeline',
|
|
1871
|
+
'pmx-canvas ax timeline --limit 100',
|
|
1872
|
+
], async (args) => {
|
|
1873
|
+
const { flags } = parseFlags(args);
|
|
1874
|
+
if (flags.help || flags.h) return showCommandHelp('ax timeline');
|
|
1875
|
+
|
|
1876
|
+
const limit = optionalNumberFlag(flags, 'limit', 'pmx-canvas ax timeline --limit <n>');
|
|
1877
|
+
output(await api('GET', `/api/canvas/ax/timeline${limit ? `?limit=${limit}` : ''}`));
|
|
1878
|
+
});
|
|
1879
|
+
|
|
1880
|
+
cmd('ax work add', 'Add a canvas-bound AX work item', [
|
|
1881
|
+
'pmx-canvas ax work add --title "Wire up auth" --status in-progress',
|
|
1882
|
+
'pmx-canvas ax work add --title "Review API" node1 node2',
|
|
1883
|
+
], async (args) => {
|
|
1884
|
+
const { positional, flags } = parseFlags(args);
|
|
1885
|
+
if (flags.help || flags.h) return showCommandHelp('ax work add');
|
|
1886
|
+
|
|
1887
|
+
const title = requireFlag(flags, 'title', 'pmx-canvas ax work add --title <text>');
|
|
1888
|
+
const status = getStringFlag(flags, 'status');
|
|
1889
|
+
const detail = getStringFlag(flags, 'detail');
|
|
1890
|
+
|
|
1891
|
+
output(await api('POST', '/api/canvas/ax/work', {
|
|
1892
|
+
title,
|
|
1893
|
+
...(status ? { status } : {}),
|
|
1894
|
+
...(detail ? { detail } : {}),
|
|
1895
|
+
...(positional.length > 0 ? { nodeIds: positional } : {}),
|
|
1896
|
+
source: 'cli',
|
|
1897
|
+
}));
|
|
1898
|
+
});
|
|
1899
|
+
|
|
1900
|
+
cmd('ax work update', 'Update a canvas-bound AX work item by ID', [
|
|
1901
|
+
'pmx-canvas ax work update <id> --status done',
|
|
1902
|
+
'pmx-canvas ax work update <id> --title "New title" --detail "..."',
|
|
1903
|
+
], async (args) => {
|
|
1904
|
+
const { positional, flags } = parseFlags(args);
|
|
1905
|
+
if (flags.help || flags.h) return showCommandHelp('ax work update');
|
|
1906
|
+
|
|
1907
|
+
const id = positional[0];
|
|
1908
|
+
if (!id) die('Missing work item ID', 'pmx-canvas ax work update <id> --status <status>');
|
|
1909
|
+
const title = getStringFlag(flags, 'title');
|
|
1910
|
+
const status = getStringFlag(flags, 'status');
|
|
1911
|
+
const detail = getStringFlag(flags, 'detail');
|
|
1912
|
+
|
|
1913
|
+
output(await api('PATCH', `/api/canvas/ax/work/${encodeURIComponent(id)}`, {
|
|
1914
|
+
...(title ? { title } : {}),
|
|
1915
|
+
...(status ? { status } : {}),
|
|
1916
|
+
...(detail ? { detail } : {}),
|
|
1917
|
+
...(positional.length > 1 ? { nodeIds: positional.slice(1) } : {}),
|
|
1918
|
+
source: 'cli',
|
|
1919
|
+
}));
|
|
1920
|
+
});
|
|
1921
|
+
|
|
1922
|
+
cmd('ax work list', 'List canvas-bound AX work items', [
|
|
1923
|
+
'pmx-canvas ax work list',
|
|
1924
|
+
], async (args) => {
|
|
1925
|
+
const { flags } = parseFlags(args);
|
|
1926
|
+
if (flags.help || flags.h) return showCommandHelp('ax work list');
|
|
1927
|
+
|
|
1928
|
+
output(await api('GET', '/api/canvas/ax/work'));
|
|
1929
|
+
});
|
|
1930
|
+
|
|
1931
|
+
cmd('ax approval request', 'Request a canvas-bound AX approval gate', [
|
|
1932
|
+
'pmx-canvas ax approval request --title "Deploy to prod"',
|
|
1933
|
+
'pmx-canvas ax approval request --title "Drop table" --action db.drop node1',
|
|
1934
|
+
], async (args) => {
|
|
1935
|
+
const { positional, flags } = parseFlags(args);
|
|
1936
|
+
if (flags.help || flags.h) return showCommandHelp('ax approval request');
|
|
1937
|
+
|
|
1938
|
+
const title = requireFlag(flags, 'title', 'pmx-canvas ax approval request --title <text>');
|
|
1939
|
+
const detail = getStringFlag(flags, 'detail');
|
|
1940
|
+
const action = getStringFlag(flags, 'action');
|
|
1941
|
+
|
|
1942
|
+
output(await api('POST', '/api/canvas/ax/approval', {
|
|
1943
|
+
title,
|
|
1944
|
+
...(detail ? { detail } : {}),
|
|
1945
|
+
...(action ? { action } : {}),
|
|
1946
|
+
...(positional.length > 0 ? { nodeIds: positional } : {}),
|
|
1947
|
+
source: 'cli',
|
|
1948
|
+
}));
|
|
1949
|
+
});
|
|
1950
|
+
|
|
1951
|
+
cmd('ax approval resolve', 'Resolve a pending AX approval gate by ID', [
|
|
1952
|
+
'pmx-canvas ax approval resolve <id> --decision approved',
|
|
1953
|
+
'pmx-canvas ax approval resolve <id> --decision rejected --resolution "too risky"',
|
|
1954
|
+
], async (args) => {
|
|
1955
|
+
const { positional, flags } = parseFlags(args);
|
|
1956
|
+
if (flags.help || flags.h) return showCommandHelp('ax approval resolve');
|
|
1957
|
+
|
|
1958
|
+
const id = positional[0];
|
|
1959
|
+
if (!id) die('Missing approval gate ID', 'pmx-canvas ax approval resolve <id> --decision <approved|rejected>');
|
|
1960
|
+
const decision = requireFlag(flags, 'decision', 'pmx-canvas ax approval resolve <id> --decision <approved|rejected>');
|
|
1961
|
+
if (decision !== 'approved' && decision !== 'rejected') {
|
|
1962
|
+
die('Invalid decision', 'pmx-canvas ax approval resolve <id> --decision <approved|rejected>');
|
|
1963
|
+
}
|
|
1964
|
+
const resolution = getStringFlag(flags, 'resolution');
|
|
1965
|
+
|
|
1966
|
+
output(await api('POST', `/api/canvas/ax/approval/${encodeURIComponent(id)}/resolve`, {
|
|
1967
|
+
decision,
|
|
1968
|
+
...(resolution ? { resolution } : {}),
|
|
1969
|
+
source: 'cli',
|
|
1970
|
+
}));
|
|
1971
|
+
});
|
|
1972
|
+
|
|
1973
|
+
cmd('ax approval list', 'List canvas-bound AX approval gates', [
|
|
1974
|
+
'pmx-canvas ax approval list',
|
|
1975
|
+
], async (args) => {
|
|
1976
|
+
const { flags } = parseFlags(args);
|
|
1977
|
+
if (flags.help || flags.h) return showCommandHelp('ax approval list');
|
|
1978
|
+
|
|
1979
|
+
output(await api('GET', '/api/canvas/ax/approval'));
|
|
1980
|
+
});
|
|
1981
|
+
|
|
1982
|
+
cmd('ax evidence add', 'Record an AX evidence item on the timeline', [
|
|
1983
|
+
'pmx-canvas ax evidence add --kind test-output --title "unit pass" --body "..."',
|
|
1984
|
+
'pmx-canvas ax evidence add --kind screenshot --title "before" --ref /tmp/before.png node1',
|
|
1985
|
+
], async (args) => {
|
|
1986
|
+
const { positional, flags } = parseFlags(args);
|
|
1987
|
+
if (flags.help || flags.h) return showCommandHelp('ax evidence add');
|
|
1988
|
+
|
|
1989
|
+
const kind = requireFlag(flags, 'kind', 'pmx-canvas ax evidence add --kind <kind> --title <text>');
|
|
1990
|
+
const title = requireFlag(flags, 'title', 'pmx-canvas ax evidence add --kind <kind> --title <text>');
|
|
1991
|
+
const body = getStringFlag(flags, 'body');
|
|
1992
|
+
const ref = getStringFlag(flags, 'ref');
|
|
1993
|
+
|
|
1994
|
+
output(await api('POST', '/api/canvas/ax/evidence', {
|
|
1995
|
+
kind,
|
|
1996
|
+
title,
|
|
1997
|
+
...(body ? { body } : {}),
|
|
1998
|
+
...(ref ? { ref } : {}),
|
|
1999
|
+
...(positional.length > 0 ? { nodeIds: positional } : {}),
|
|
2000
|
+
source: 'cli',
|
|
2001
|
+
}));
|
|
2002
|
+
});
|
|
2003
|
+
|
|
2004
|
+
cmd('ax review add', 'Add a canvas-bound AX review annotation', [
|
|
2005
|
+
'pmx-canvas ax review add --body "needs a test" --node node1',
|
|
2006
|
+
'pmx-canvas ax review add --body "off-by-one" --kind finding --severity error --file src/x.ts',
|
|
2007
|
+
], async (args) => {
|
|
2008
|
+
const { flags } = parseFlags(args);
|
|
2009
|
+
if (flags.help || flags.h) return showCommandHelp('ax review add');
|
|
2010
|
+
|
|
2011
|
+
const body = requireFlag(flags, 'body', 'pmx-canvas ax review add --body <text>');
|
|
2012
|
+
const kind = getStringFlag(flags, 'kind');
|
|
2013
|
+
const severity = getStringFlag(flags, 'severity');
|
|
2014
|
+
const anchorType = getStringFlag(flags, 'anchor');
|
|
2015
|
+
const nodeId = getStringFlag(flags, 'node');
|
|
2016
|
+
const file = getStringFlag(flags, 'file');
|
|
2017
|
+
const author = getStringFlag(flags, 'author');
|
|
2018
|
+
|
|
2019
|
+
output(await api('POST', '/api/canvas/ax/review', {
|
|
2020
|
+
body,
|
|
2021
|
+
...(kind ? { kind } : {}),
|
|
2022
|
+
...(severity ? { severity } : {}),
|
|
2023
|
+
...(anchorType ? { anchorType } : {}),
|
|
2024
|
+
...(nodeId ? { nodeId } : {}),
|
|
2025
|
+
...(file ? { file } : {}),
|
|
2026
|
+
...(author ? { author } : {}),
|
|
2027
|
+
source: 'cli',
|
|
2028
|
+
}));
|
|
2029
|
+
});
|
|
2030
|
+
|
|
2031
|
+
cmd('ax review list', 'List canvas-bound AX review annotations', [
|
|
2032
|
+
'pmx-canvas ax review list',
|
|
2033
|
+
], async (args) => {
|
|
2034
|
+
const { flags } = parseFlags(args);
|
|
2035
|
+
if (flags.help || flags.h) return showCommandHelp('ax review list');
|
|
2036
|
+
|
|
2037
|
+
output(await api('GET', '/api/canvas/ax/review'));
|
|
2038
|
+
});
|
|
2039
|
+
|
|
2040
|
+
cmd('ax host report', 'Report host/session capability to the canvas', [
|
|
2041
|
+
'pmx-canvas ax host report --host copilot --canvas --tools --session-messaging',
|
|
2042
|
+
'pmx-canvas ax host report --host codex --canvas --files',
|
|
2043
|
+
], async (args) => {
|
|
2044
|
+
const { flags } = parseFlags(args);
|
|
2045
|
+
if (flags.help || flags.h) return showCommandHelp('ax host report');
|
|
2046
|
+
|
|
2047
|
+
const host = getStringFlag(flags, 'host');
|
|
2048
|
+
|
|
2049
|
+
output(await api('PUT', '/api/canvas/ax/host-capability', {
|
|
2050
|
+
...(host ? { host } : {}),
|
|
2051
|
+
canvas: flags.canvas === true,
|
|
2052
|
+
hooks: flags.hooks === true,
|
|
2053
|
+
tools: flags.tools === true,
|
|
2054
|
+
sessionMessaging: flags['session-messaging'] === true,
|
|
2055
|
+
permissions: flags.permissions === true,
|
|
2056
|
+
files: flags.files === true,
|
|
2057
|
+
uiPrompts: flags['ui-prompts'] === true,
|
|
2058
|
+
source: 'cli',
|
|
2059
|
+
}));
|
|
2060
|
+
});
|
|
2061
|
+
|
|
2062
|
+
cmd('ax host status', 'Read the reported host/session capability', [
|
|
2063
|
+
'pmx-canvas ax host status',
|
|
2064
|
+
], async (args) => {
|
|
2065
|
+
const { flags } = parseFlags(args);
|
|
2066
|
+
if (flags.help || flags.h) return showCommandHelp('ax host status');
|
|
2067
|
+
|
|
2068
|
+
output(await api('GET', '/api/canvas/ax/host-capability'));
|
|
2069
|
+
});
|
|
2070
|
+
|
|
2071
|
+
// ── copilot install-extension ────────────────────────────────
|
|
2072
|
+
cmd('copilot install-extension', 'Install the bundled GitHub Copilot extension adapter', [
|
|
2073
|
+
'pmx-canvas copilot install-extension --dry-run',
|
|
2074
|
+
'pmx-canvas copilot install-extension --target .github/extensions/pmx-canvas/extension.mjs --yes',
|
|
2075
|
+
], async (args) => {
|
|
2076
|
+
const { flags } = parseFlags(args);
|
|
2077
|
+
if (flags.help || flags.h) return showCommandHelp('copilot install-extension');
|
|
2078
|
+
|
|
2079
|
+
const sourcePath = fileURLToPath(new URL('../../.github/extensions/pmx-canvas/extension.mjs', import.meta.url));
|
|
2080
|
+
if (!existsSync(sourcePath)) {
|
|
2081
|
+
die('Bundled Copilot extension adapter not found.', `Expected at ${sourcePath}`);
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
const targetPath = getStringFlag(flags, 'target')
|
|
2085
|
+
?? join(process.cwd(), '.github', 'extensions', 'pmx-canvas', 'extension.mjs');
|
|
2086
|
+
const dryRun = flags['dry-run'] === true;
|
|
2087
|
+
const targetExists = existsSync(targetPath);
|
|
2088
|
+
|
|
2089
|
+
if (dryRun) {
|
|
2090
|
+
output({ ok: true, dryRun: true, sourcePath, targetPath, targetExists, wrote: false });
|
|
2091
|
+
return;
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
if (targetExists && flags.yes !== true) {
|
|
2095
|
+
die('Target already exists. Re-run with --yes to overwrite.', `Target: ${targetPath}`);
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
2099
|
+
copyFileSync(sourcePath, targetPath);
|
|
2100
|
+
output({ ok: true, dryRun: false, sourcePath, targetPath, wrote: true });
|
|
2101
|
+
});
|
|
2102
|
+
|
|
1804
2103
|
// ── undo ─────────────────────────────────────────────────────
|
|
1805
2104
|
cmd('undo', 'Undo the last canvas mutation', [
|
|
1806
2105
|
'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);
|
|
@@ -216,6 +221,7 @@ export function ExtAppFrame({ node, expanded = false }: { node: CanvasNodeState;
|
|
|
216
221
|
let fallbackTimer: ReturnType<typeof setTimeout> | null = null;
|
|
217
222
|
let hostContextResizeObserver: ResizeObserver | null = null;
|
|
218
223
|
let hostContextRaf: number | null = null;
|
|
224
|
+
let readyNudgeRaf: number | null = null;
|
|
219
225
|
toolResultSentRef.current = false;
|
|
220
226
|
lastSentToolResultRef.current = undefined;
|
|
221
227
|
toolResultSendingRef.current = null;
|
|
@@ -261,6 +267,24 @@ export function ExtAppFrame({ node, expanded = false }: { node: CanvasNodeState;
|
|
|
261
267
|
});
|
|
262
268
|
};
|
|
263
269
|
|
|
270
|
+
// Re-deliver host context once the iframe has been laid out and painted.
|
|
271
|
+
// Canvas-backed widgets (e.g. Excalidraw) size their drawing surface from
|
|
272
|
+
// containerDimensions at first render; the single handshake-time delivery
|
|
273
|
+
// can land before the embedded frame has settled, leaving a black canvas
|
|
274
|
+
// until an expand/collapse forces a reflow. A double rAF lands after
|
|
275
|
+
// layout+paint, and sendHostContextChange always delivers (setHostContext
|
|
276
|
+
// would diff-suppress the identical context just sent at handshake).
|
|
277
|
+
const nudgeHostContextAfterLayout = () => {
|
|
278
|
+
if (readyNudgeRaf !== null) return;
|
|
279
|
+
readyNudgeRaf = requestAnimationFrame(() => {
|
|
280
|
+
readyNudgeRaf = requestAnimationFrame(() => {
|
|
281
|
+
readyNudgeRaf = null;
|
|
282
|
+
if (disposed || !bridgeReadyRef.current) return;
|
|
283
|
+
void bridge.sendHostContextChange?.(buildHostContext());
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
};
|
|
287
|
+
|
|
264
288
|
const bridge = new AppBridge(
|
|
265
289
|
null,
|
|
266
290
|
{ name: 'PMX Canvas', version: '1.0.0' },
|
|
@@ -283,7 +307,20 @@ export function ExtAppFrame({ node, expanded = false }: { node: CanvasNodeState;
|
|
|
283
307
|
width: node.size.width,
|
|
284
308
|
height: maxHeight,
|
|
285
309
|
});
|
|
286
|
-
|
|
310
|
+
const inlineFrameHeight = resolveExtAppInlineFrameHeight(height, hostDimensions.height);
|
|
311
|
+
iframe.style.height = `${inlineFrameHeight}px`;
|
|
312
|
+
const currentSize = nodes.value.get(nodeId)?.size ?? node.size;
|
|
313
|
+
const nodeHeight = Math.max(currentSize.height, inlineFrameHeight + AUTO_FIT_TITLEBAR_HEIGHT);
|
|
314
|
+
if (Math.abs(nodeHeight - currentSize.height) > 8) {
|
|
315
|
+
resizeNode(nodeId, { width: currentSize.width, height: nodeHeight });
|
|
316
|
+
if (sizePersistTimerRef.current !== null) {
|
|
317
|
+
window.clearTimeout(sizePersistTimerRef.current);
|
|
318
|
+
}
|
|
319
|
+
sizePersistTimerRef.current = window.setTimeout(() => {
|
|
320
|
+
persistLayout({ recordHistory: false });
|
|
321
|
+
sizePersistTimerRef.current = null;
|
|
322
|
+
}, 0);
|
|
323
|
+
}
|
|
287
324
|
}
|
|
288
325
|
return {};
|
|
289
326
|
};
|
|
@@ -386,6 +423,7 @@ export function ExtAppFrame({ node, expanded = false }: { node: CanvasNodeState;
|
|
|
386
423
|
void Promise.resolve(bridge.sendHostContextChange(buildHostContext(isExpanded ? 'fullscreen' : 'inline')))
|
|
387
424
|
.then(() => sendExtAppBootstrapState(bridge, latestToolInputRef.current, undefined))
|
|
388
425
|
.then(() => flushToolResult(bridge))
|
|
426
|
+
.then(() => nudgeHostContextAfterLayout())
|
|
389
427
|
.catch((err) => {
|
|
390
428
|
const msg = err instanceof Error ? err.message : String(err);
|
|
391
429
|
setError(`Bridge bootstrap failed: ${msg}`);
|
|
@@ -410,6 +448,7 @@ export function ExtAppFrame({ node, expanded = false }: { node: CanvasNodeState;
|
|
|
410
448
|
}
|
|
411
449
|
setStatus(bootstrapToolResult ? 'done' : 'ready');
|
|
412
450
|
setError(null);
|
|
451
|
+
nudgeHostContextAfterLayout();
|
|
413
452
|
})
|
|
414
453
|
.catch((err) => {
|
|
415
454
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -460,10 +499,18 @@ export function ExtAppFrame({ node, expanded = false }: { node: CanvasNodeState;
|
|
|
460
499
|
cancelAnimationFrame(hostContextRaf);
|
|
461
500
|
hostContextRaf = null;
|
|
462
501
|
}
|
|
502
|
+
if (readyNudgeRaf !== null) {
|
|
503
|
+
cancelAnimationFrame(readyNudgeRaf);
|
|
504
|
+
readyNudgeRaf = null;
|
|
505
|
+
}
|
|
463
506
|
bridgeReadyRef.current = false;
|
|
464
507
|
toolResultSendingRef.current = null;
|
|
465
508
|
themeUnsubRef.current?.();
|
|
466
509
|
themeUnsubRef.current = null;
|
|
510
|
+
if (sizePersistTimerRef.current !== null) {
|
|
511
|
+
window.clearTimeout(sizePersistTimerRef.current);
|
|
512
|
+
sizePersistTimerRef.current = null;
|
|
513
|
+
}
|
|
467
514
|
bridgeRef.current = null;
|
|
468
515
|
if (transportRef.current) {
|
|
469
516
|
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
|
|
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,
|
|
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',
|