pmx-canvas 0.1.29 → 0.1.31
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/CHANGELOG.md +219 -0
- package/Readme.md +20 -10
- package/dist/canvas/global.css +51 -56
- package/dist/canvas/index.js +80 -163
- package/dist/canvas/surface-theme.css +142 -0
- package/dist/json-render/index.js +103 -103
- package/dist/types/client/nodes/HtmlNode.d.ts +0 -7
- package/dist/types/client/nodes/ax-node-actions.d.ts +18 -0
- package/dist/types/client/nodes/surface-url.d.ts +22 -0
- package/dist/types/client/state/attention-bridge.d.ts +3 -0
- package/dist/types/client/state/intent-bridge.d.ts +17 -0
- package/dist/types/json-render/renderer/index.d.ts +2 -0
- package/dist/types/json-render/schema.d.ts +2 -0
- package/dist/types/json-render/server.d.ts +2 -0
- package/dist/types/mcp/canvas-access.d.ts +47 -0
- package/dist/types/server/ax-interaction.d.ts +210 -0
- package/dist/types/server/ax-state.d.ts +67 -1
- package/dist/types/server/canvas-db.d.ts +4 -0
- package/dist/types/server/canvas-serialization.d.ts +2 -0
- package/dist/types/server/canvas-state.d.ts +54 -2
- package/dist/types/server/html-surface.d.ts +46 -0
- package/dist/types/server/index.d.ts +63 -5
- package/dist/types/server/mutation-history.d.ts +1 -1
- package/dist/types/server/placement.d.ts +1 -1
- package/dist/types/server/server.d.ts +12 -0
- package/dist/types/shared/surface.d.ts +19 -0
- package/docs/cli.md +30 -0
- package/docs/http-api.md +55 -0
- package/docs/mcp.md +40 -2
- package/docs/node-types.md +26 -0
- package/docs/plans/plan-004-pmx-ax-primitives.md +623 -394
- package/docs/sdk.md +23 -1
- package/package.json +2 -2
- package/skills/pmx-canvas/SKILL.md +107 -9
- package/src/cli/agent.ts +177 -0
- package/src/cli/index.ts +8 -1
- package/src/client/canvas/CanvasNode.tsx +8 -4
- package/src/client/canvas/DockedNode.tsx +38 -38
- package/src/client/canvas/ExpandedNodeOverlay.tsx +12 -0
- package/src/client/nodes/ContextNode.tsx +17 -0
- package/src/client/nodes/ExtAppFrame.tsx +40 -3
- package/src/client/nodes/FileNode.tsx +26 -0
- package/src/client/nodes/HtmlNode.tsx +60 -188
- package/src/client/nodes/McpAppNode.tsx +47 -2
- package/src/client/nodes/StatusNode.tsx +20 -0
- package/src/client/nodes/ax-node-actions.ts +39 -0
- package/src/client/nodes/surface-url.ts +48 -0
- package/src/client/state/attention-bridge.ts +5 -0
- package/src/client/state/intent-bridge.ts +33 -0
- package/src/client/theme/global.css +51 -56
- package/src/client/theme/surface-theme.css +142 -0
- package/src/json-render/renderer/index.tsx +31 -0
- package/src/json-render/schema.ts +4 -0
- package/src/json-render/server.ts +13 -0
- package/src/mcp/canvas-access.ts +198 -1
- package/src/mcp/server.ts +232 -2
- package/src/server/ax-context.ts +3 -0
- package/src/server/ax-interaction.ts +549 -0
- package/src/server/ax-state.ts +188 -2
- package/src/server/canvas-db.ts +20 -0
- package/src/server/canvas-operations.ts +11 -0
- package/src/server/canvas-serialization.ts +9 -0
- package/src/server/canvas-state.ts +201 -26
- package/src/server/html-surface.ts +190 -0
- package/src/server/index.ts +122 -7
- package/src/server/mutation-history.ts +5 -0
- package/src/server/placement.ts +5 -1
- package/src/server/server.ts +360 -0
- package/src/shared/surface.ts +38 -0
|
@@ -56,6 +56,10 @@
|
|
|
56
56
|
--mono: "IBM Plex Mono", "SF Mono", "Fira Code", monospace;
|
|
57
57
|
--radius: 10px;
|
|
58
58
|
--radius-sm: 6px;
|
|
59
|
+
/* Shared height for the top HUD row so the toolbar and the collapsed docked
|
|
60
|
+
status/context widgets that flank it line up to the same height. Matches the
|
|
61
|
+
toolbar's natural content height (icon buttons at 6px padding). */
|
|
62
|
+
--hud-bar-height: 44px;
|
|
59
63
|
}
|
|
60
64
|
|
|
61
65
|
:root[data-theme="light"] {
|
|
@@ -481,6 +485,8 @@ body,
|
|
|
481
485
|
align-items: center;
|
|
482
486
|
gap: 6px;
|
|
483
487
|
padding: 6px 10px;
|
|
488
|
+
min-height: var(--hud-bar-height);
|
|
489
|
+
box-sizing: border-box;
|
|
484
490
|
background: var(--c-panel-glass);
|
|
485
491
|
backdrop-filter: blur(12px);
|
|
486
492
|
border: 1px solid var(--c-line);
|
|
@@ -1409,6 +1415,38 @@ html.is-node-resizing .ext-app-preview-catcher {
|
|
|
1409
1415
|
max-width: 320px;
|
|
1410
1416
|
}
|
|
1411
1417
|
|
|
1418
|
+
/* Collapsed docked widget = a single menu-height pill that flanks the toolbar.
|
|
1419
|
+
Pinned to the same height as .canvas-toolbar so the top HUD row reads as one
|
|
1420
|
+
continuous bar (status on the left, context on the right). */
|
|
1421
|
+
.docked-node--collapsed {
|
|
1422
|
+
height: var(--hud-bar-height);
|
|
1423
|
+
box-sizing: border-box;
|
|
1424
|
+
justify-content: center;
|
|
1425
|
+
width: auto;
|
|
1426
|
+
/* Reset the base .docked-node min-width so the collapsed pill hugs its content
|
|
1427
|
+
(badge + count + controls) instead of stretching to a 200px bar. */
|
|
1428
|
+
min-width: 0;
|
|
1429
|
+
}
|
|
1430
|
+
.docked-node--collapsed .docked-node-header {
|
|
1431
|
+
height: 100%;
|
|
1432
|
+
padding: 0 10px;
|
|
1433
|
+
border-bottom: none;
|
|
1434
|
+
}
|
|
1435
|
+
.docked-node-count {
|
|
1436
|
+
min-width: 18px;
|
|
1437
|
+
height: 18px;
|
|
1438
|
+
padding: 0 5px;
|
|
1439
|
+
display: inline-flex;
|
|
1440
|
+
align-items: center;
|
|
1441
|
+
justify-content: center;
|
|
1442
|
+
border-radius: 9px;
|
|
1443
|
+
background: var(--c-accent);
|
|
1444
|
+
color: var(--c-contrast-fg);
|
|
1445
|
+
font-size: 10px;
|
|
1446
|
+
font-weight: 700;
|
|
1447
|
+
flex-shrink: 0;
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1412
1450
|
.docked-node-header {
|
|
1413
1451
|
display: flex;
|
|
1414
1452
|
align-items: center;
|
|
@@ -1473,6 +1511,19 @@ html.is-node-resizing .ext-app-preview-catcher {
|
|
|
1473
1511
|
border-radius: 0 0 calc(var(--radius) - 1px) calc(var(--radius) - 1px);
|
|
1474
1512
|
}
|
|
1475
1513
|
|
|
1514
|
+
/* Promote canvas iframes to their own GPU compositing layer. An iframe embedded
|
|
1515
|
+
in the zoom/pan-transformed canvas — especially near the heavy backdrop-filter
|
|
1516
|
+
blur used across the chrome and behind group frames — can intermittently paint
|
|
1517
|
+
blank until a resize/zoom forces a repaint (reported for grouped HTML nodes;
|
|
1518
|
+
the same class of compositor glitch as the earlier Excalidraw flicker). A no-op
|
|
1519
|
+
3D transform forces a stable layer so the iframe keeps painting through
|
|
1520
|
+
surrounding layout/stacking changes. */
|
|
1521
|
+
.mcp-app-frame,
|
|
1522
|
+
.html-node-frame {
|
|
1523
|
+
transform: translateZ(0);
|
|
1524
|
+
backface-visibility: hidden;
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1476
1527
|
/* ── Prompt nodes ──────────────────────────────────────────── */
|
|
1477
1528
|
.canvas-node:has(.prompt-node-inner) {
|
|
1478
1529
|
border-color: var(--c-accent-30);
|
|
@@ -1935,62 +1986,6 @@ html.is-node-resizing .ext-app-preview-catcher {
|
|
|
1935
1986
|
max-width: 200px;
|
|
1936
1987
|
}
|
|
1937
1988
|
|
|
1938
|
-
/* Context dock — collapsed pill mirrors Updates pill, sits above it */
|
|
1939
|
-
.context-dock-tab {
|
|
1940
|
-
position: fixed;
|
|
1941
|
-
top: 92px;
|
|
1942
|
-
right: 0;
|
|
1943
|
-
display: flex;
|
|
1944
|
-
align-items: center;
|
|
1945
|
-
gap: 8px;
|
|
1946
|
-
padding: 8px 12px 8px 14px;
|
|
1947
|
-
background: color-mix(in srgb, var(--c-panel-glass) 96%, transparent);
|
|
1948
|
-
backdrop-filter: blur(16px);
|
|
1949
|
-
border: 1px solid color-mix(in srgb, var(--c-line) 82%, var(--c-accent) 18%);
|
|
1950
|
-
border-right: 0;
|
|
1951
|
-
border-radius: 14px 0 0 14px;
|
|
1952
|
-
box-shadow: 0 12px 36px var(--c-shadow);
|
|
1953
|
-
color: var(--c-text);
|
|
1954
|
-
cursor: pointer;
|
|
1955
|
-
font: inherit;
|
|
1956
|
-
font-size: 11px;
|
|
1957
|
-
font-weight: 600;
|
|
1958
|
-
letter-spacing: 0.08em;
|
|
1959
|
-
text-transform: uppercase;
|
|
1960
|
-
z-index: 60;
|
|
1961
|
-
}
|
|
1962
|
-
|
|
1963
|
-
.context-dock-tab:hover {
|
|
1964
|
-
border-color: color-mix(in srgb, var(--c-accent) 40%, var(--c-line) 60%);
|
|
1965
|
-
color: var(--c-accent);
|
|
1966
|
-
}
|
|
1967
|
-
|
|
1968
|
-
.context-dock-tab svg {
|
|
1969
|
-
display: block;
|
|
1970
|
-
color: var(--c-accent);
|
|
1971
|
-
flex-shrink: 0;
|
|
1972
|
-
}
|
|
1973
|
-
|
|
1974
|
-
.context-dock-tab-label {
|
|
1975
|
-
white-space: nowrap;
|
|
1976
|
-
}
|
|
1977
|
-
|
|
1978
|
-
.context-dock-tab-badge {
|
|
1979
|
-
min-width: 18px;
|
|
1980
|
-
height: 18px;
|
|
1981
|
-
padding: 0 5px;
|
|
1982
|
-
display: inline-flex;
|
|
1983
|
-
align-items: center;
|
|
1984
|
-
justify-content: center;
|
|
1985
|
-
border-radius: 9px;
|
|
1986
|
-
background: var(--c-accent);
|
|
1987
|
-
color: var(--c-contrast-fg);
|
|
1988
|
-
font-size: 10px;
|
|
1989
|
-
font-weight: 700;
|
|
1990
|
-
letter-spacing: 0;
|
|
1991
|
-
text-transform: none;
|
|
1992
|
-
}
|
|
1993
|
-
|
|
1994
1989
|
/* Context dock — expanded panel anchored top-right edge.
|
|
1995
1990
|
Mutually exclusive with the Updates panel (see DockedNode.tsx and
|
|
1996
1991
|
AttentionHistory.tsx) — opening one collapses the other, so they can both
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* surface-theme.css — canonical theme token layer for embedded HTML surfaces.
|
|
3
|
+
*
|
|
4
|
+
* This is the same token set HtmlNode used to inline via buildThemeStyleBlock(),
|
|
5
|
+
* but expressed as a static, same-origin stylesheet so the server can serve a
|
|
6
|
+
* node's HTML surface (/api/canvas/surface/:nodeId) as a real standalone
|
|
7
|
+
* document. Sandboxed (opaque-origin) surface documents can still load this
|
|
8
|
+
* same-origin stylesheet, and live theme switching works by toggling the
|
|
9
|
+
* <html data-theme="..."> attribute — every theme block lives here.
|
|
10
|
+
*
|
|
11
|
+
* Tokens are written as LITERAL values per theme (not var() indirection) so that
|
|
12
|
+
* authored HTML reading them via getComputedStyle().getPropertyValue('--color-*')
|
|
13
|
+
* gets a resolved color, matching the previous inlined-style behavior.
|
|
14
|
+
*
|
|
15
|
+
* Core --c-* values MUST stay in sync with src/client/theme/global.css, and each
|
|
16
|
+
* --color-* alias mirrors its core token. Both are guarded by
|
|
17
|
+
* tests/unit/surface-theme-tokens.test.ts — update them together.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
:root {
|
|
21
|
+
/* ── Core palette (dark · ink + cyan) ────────────────────── */
|
|
22
|
+
--c-bg: #081524;
|
|
23
|
+
--c-panel: #0f1d31;
|
|
24
|
+
--c-panel-soft: #0a1729;
|
|
25
|
+
--c-line: #1b2c44;
|
|
26
|
+
--c-text: #e6eef7;
|
|
27
|
+
--c-text-soft: #c7d3ea;
|
|
28
|
+
--c-muted: #8ea3bd;
|
|
29
|
+
--c-dim: #5c6b80;
|
|
30
|
+
--c-accent: #4BBCFF;
|
|
31
|
+
--c-ok: #2fd07f;
|
|
32
|
+
--c-warn: #f4c542;
|
|
33
|
+
--c-warn-alt: #FFB300;
|
|
34
|
+
--c-danger: #ff6a7f;
|
|
35
|
+
--c-purple: #b07aff;
|
|
36
|
+
|
|
37
|
+
/* Common aliases authored HTML might use. */
|
|
38
|
+
--color-bg: #081524;
|
|
39
|
+
--color-panel: #0f1d31;
|
|
40
|
+
--color-surface: #0a1729;
|
|
41
|
+
--color-border: #1b2c44;
|
|
42
|
+
--color-text: #e6eef7;
|
|
43
|
+
--color-text-primary: #e6eef7;
|
|
44
|
+
--color-text-secondary: #c7d3ea;
|
|
45
|
+
--color-text-muted: #8ea3bd;
|
|
46
|
+
--color-text-dim: #5c6b80;
|
|
47
|
+
--color-accent: #4BBCFF;
|
|
48
|
+
--color-success: #2fd07f;
|
|
49
|
+
--color-warning: #f4c542;
|
|
50
|
+
--color-danger: #ff6a7f;
|
|
51
|
+
|
|
52
|
+
--font: "IBM Plex Sans", "SF Pro Text", "Avenir Next", system-ui, sans-serif;
|
|
53
|
+
--mono: "IBM Plex Mono", "SF Mono", "Fira Code", monospace;
|
|
54
|
+
--font-sans: "IBM Plex Sans", "SF Pro Text", "Avenir Next", system-ui, sans-serif;
|
|
55
|
+
--font-mono: "IBM Plex Mono", "SF Mono", "Fira Code", monospace;
|
|
56
|
+
|
|
57
|
+
color-scheme: dark light;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
:root[data-theme="light"] {
|
|
61
|
+
/* ── Core palette (light · paper + ink) ──────────────────── */
|
|
62
|
+
--c-bg: #F4EFE6;
|
|
63
|
+
--c-panel: #EFE7D4;
|
|
64
|
+
--c-panel-soft: #ECE4D0;
|
|
65
|
+
--c-line: #D6CBB4;
|
|
66
|
+
--c-text: #081524;
|
|
67
|
+
--c-text-soft: #3d4d63;
|
|
68
|
+
--c-muted: #5c6b80;
|
|
69
|
+
--c-dim: #8794a6;
|
|
70
|
+
--c-accent: #1A7ABF;
|
|
71
|
+
--c-ok: #1a9f55;
|
|
72
|
+
--c-warn: #c89b2a;
|
|
73
|
+
--c-warn-alt: #b8860b;
|
|
74
|
+
--c-danger: #d32f2f;
|
|
75
|
+
--c-purple: #7c4dff;
|
|
76
|
+
|
|
77
|
+
--color-bg: #F4EFE6;
|
|
78
|
+
--color-panel: #EFE7D4;
|
|
79
|
+
--color-surface: #ECE4D0;
|
|
80
|
+
--color-border: #D6CBB4;
|
|
81
|
+
--color-text: #081524;
|
|
82
|
+
--color-text-primary: #081524;
|
|
83
|
+
--color-text-secondary: #3d4d63;
|
|
84
|
+
--color-text-muted: #5c6b80;
|
|
85
|
+
--color-text-dim: #8794a6;
|
|
86
|
+
--color-accent: #1A7ABF;
|
|
87
|
+
--color-success: #1a9f55;
|
|
88
|
+
--color-warning: #c89b2a;
|
|
89
|
+
--color-danger: #d32f2f;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
:root[data-theme="high-contrast"] {
|
|
93
|
+
/* ── Core palette (high-contrast) ────────────────────────── */
|
|
94
|
+
--c-bg: #000000;
|
|
95
|
+
--c-panel: #0a0a0a;
|
|
96
|
+
--c-panel-soft: #0a0a0a;
|
|
97
|
+
--c-line: #ffffff;
|
|
98
|
+
--c-text: #ffffff;
|
|
99
|
+
--c-text-soft: #dddddd;
|
|
100
|
+
--c-muted: #aaaaaa;
|
|
101
|
+
--c-dim: #888888;
|
|
102
|
+
--c-accent: #00ffff;
|
|
103
|
+
--c-ok: #00ff00;
|
|
104
|
+
--c-warn: #ffff00;
|
|
105
|
+
--c-warn-alt: #ffcc00;
|
|
106
|
+
--c-danger: #ff0000;
|
|
107
|
+
--c-purple: #e040fb;
|
|
108
|
+
|
|
109
|
+
--color-bg: #000000;
|
|
110
|
+
--color-panel: #0a0a0a;
|
|
111
|
+
--color-surface: #0a0a0a;
|
|
112
|
+
--color-border: #ffffff;
|
|
113
|
+
--color-text: #ffffff;
|
|
114
|
+
--color-text-primary: #ffffff;
|
|
115
|
+
--color-text-secondary: #dddddd;
|
|
116
|
+
--color-text-muted: #aaaaaa;
|
|
117
|
+
--color-text-dim: #888888;
|
|
118
|
+
--color-accent: #00ffff;
|
|
119
|
+
--color-success: #00ff00;
|
|
120
|
+
--color-warning: #ffff00;
|
|
121
|
+
--color-danger: #ff0000;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
html,
|
|
125
|
+
body {
|
|
126
|
+
margin: 0;
|
|
127
|
+
padding: 0;
|
|
128
|
+
background: var(--c-bg);
|
|
129
|
+
color: var(--c-text);
|
|
130
|
+
font-family: var(--font, system-ui, sans-serif);
|
|
131
|
+
font-size: 14px;
|
|
132
|
+
line-height: 1.5;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
body {
|
|
136
|
+
padding: 16px;
|
|
137
|
+
box-sizing: border-box;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
a {
|
|
141
|
+
color: var(--c-accent);
|
|
142
|
+
}
|
|
@@ -79,9 +79,39 @@ declare global {
|
|
|
79
79
|
__PMX_CANVAS_JSON_RENDER_THEME__?: string;
|
|
80
80
|
__PMX_CANVAS_JSON_RENDER_DISPLAY__?: string;
|
|
81
81
|
__PMX_CANVAS_JSON_RENDER_DEVTOOLS__?: boolean;
|
|
82
|
+
__PMX_CANVAS_JSON_RENDER_NODE_ID__?: string;
|
|
83
|
+
__PMX_CANVAS_AX_TOKEN__?: string;
|
|
82
84
|
}
|
|
83
85
|
}
|
|
84
86
|
|
|
87
|
+
// AX interaction types a json-render spec can bind actions to. When an action
|
|
88
|
+
// named like one of these fires, we forward it to the parent canvas (which
|
|
89
|
+
// validates + submits through the capability-gated endpoint). Convention-based
|
|
90
|
+
// opt-in: spec authors name the action handler after the AX interaction type.
|
|
91
|
+
const AX_INTERACTION_HANDLER_NAMES = [
|
|
92
|
+
'ax.event.record', 'ax.steer', 'ax.work.create', 'ax.work.update',
|
|
93
|
+
'ax.evidence.add', 'ax.approval.request', 'ax.review.add', 'ax.focus.set',
|
|
94
|
+
'ax.elicitation.request', 'ax.mode.request', 'ax.command.invoke',
|
|
95
|
+
] as const;
|
|
96
|
+
|
|
97
|
+
function buildAxHandlers(): Record<string, (params: Record<string, unknown>) => void> {
|
|
98
|
+
const nodeId = window.__PMX_CANVAS_JSON_RENDER_NODE_ID__;
|
|
99
|
+
const token = window.__PMX_CANVAS_AX_TOKEN__;
|
|
100
|
+
const handlers: Record<string, (params: Record<string, unknown>) => void> = {};
|
|
101
|
+
if (!nodeId || !token) return handlers;
|
|
102
|
+
for (const type of AX_INTERACTION_HANDLER_NAMES) {
|
|
103
|
+
handlers[type] = (params: Record<string, unknown>) => {
|
|
104
|
+
window.parent.postMessage({
|
|
105
|
+
source: 'pmx-canvas-ax',
|
|
106
|
+
token,
|
|
107
|
+
nodeId,
|
|
108
|
+
interaction: { type, payload: params && typeof params === 'object' ? params : {} },
|
|
109
|
+
}, '*');
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
return handlers;
|
|
113
|
+
}
|
|
114
|
+
|
|
85
115
|
function syncPreferredTheme(): void {
|
|
86
116
|
const forced = window.__PMX_CANVAS_JSON_RENDER_THEME__;
|
|
87
117
|
if (forced) {
|
|
@@ -125,6 +155,7 @@ function App() {
|
|
|
125
155
|
registry={registry}
|
|
126
156
|
initialState={spec.state ?? undefined}
|
|
127
157
|
directives={pmxCanvasDirectives}
|
|
158
|
+
handlers={buildAxHandlers()}
|
|
128
159
|
>
|
|
129
160
|
<Renderer spec={spec} registry={registry} loading={false} />
|
|
130
161
|
{window.__PMX_CANVAS_JSON_RENDER_DEVTOOLS__ ? (
|
|
@@ -10,6 +10,10 @@ export const schema = defineSchema(
|
|
|
10
10
|
props: s.propsOf('catalog.components'),
|
|
11
11
|
children: s.array(s.string()),
|
|
12
12
|
visible: s.any(),
|
|
13
|
+
// Event→action bindings (on.press, on.change, …). Preserved through
|
|
14
|
+
// validation so spec authors can wire actions — including the ax.*
|
|
15
|
+
// handlers the viewer forwards to the canvas AX bridge.
|
|
16
|
+
on: s.any(),
|
|
13
17
|
}),
|
|
14
18
|
),
|
|
15
19
|
}),
|
|
@@ -459,6 +459,7 @@ function normalizeSpec(spec: Record<string, unknown>): Record<string, unknown> {
|
|
|
459
459
|
resolvedType !== element.type ||
|
|
460
460
|
JSON.stringify(normalizedProps) !== JSON.stringify(rawProps) ||
|
|
461
461
|
!('visible' in element) ||
|
|
462
|
+
!('on' in element) ||
|
|
462
463
|
!Array.isArray(element.children) ||
|
|
463
464
|
normalizedChildren.length !== element.children.length;
|
|
464
465
|
|
|
@@ -468,6 +469,10 @@ function normalizeSpec(spec: Record<string, unknown>): Record<string, unknown> {
|
|
|
468
469
|
type: resolvedType,
|
|
469
470
|
props: normalizedProps,
|
|
470
471
|
visible: 'visible' in element ? element.visible : true,
|
|
472
|
+
// The schema requires `on` (event→action bindings); default to an
|
|
473
|
+
// empty bindings object when absent so specs without any actions
|
|
474
|
+
// still validate (most elements have no `on`). Mirrors `visible`.
|
|
475
|
+
on: 'on' in element ? element.on : {},
|
|
471
476
|
children: normalizedChildren,
|
|
472
477
|
}
|
|
473
478
|
: rawElement;
|
|
@@ -491,6 +496,7 @@ function normalizeJsonRenderInput(spec: unknown): unknown {
|
|
|
491
496
|
root: {
|
|
492
497
|
...specRecord,
|
|
493
498
|
visible: 'visible' in specRecord ? specRecord.visible : true,
|
|
499
|
+
on: 'on' in specRecord ? specRecord.on : {},
|
|
494
500
|
children: Array.isArray(specRecord.children)
|
|
495
501
|
? specRecord.children.filter((child: unknown) => typeof child === 'string')
|
|
496
502
|
: [],
|
|
@@ -935,7 +941,10 @@ export async function buildJsonRenderViewerHtml(options: {
|
|
|
935
941
|
theme?: 'dark' | 'light' | 'high-contrast';
|
|
936
942
|
display?: 'expanded';
|
|
937
943
|
devtools?: boolean;
|
|
944
|
+
nodeId?: string;
|
|
945
|
+
axToken?: string;
|
|
938
946
|
}): Promise<string> {
|
|
947
|
+
const sanitizeAxValue = (v?: string): string => (typeof v === 'string' ? v.replace(/[^A-Za-z0-9_-]/g, '').slice(0, 80) : '');
|
|
939
948
|
try {
|
|
940
949
|
await ensureJsonRenderBundle();
|
|
941
950
|
const dir = bundleDir();
|
|
@@ -950,6 +959,10 @@ export async function buildJsonRenderViewerHtml(options: {
|
|
|
950
959
|
...(options.theme ? [`window.__PMX_CANVAS_JSON_RENDER_THEME__ = ${JSON.stringify(options.theme)};`] : []),
|
|
951
960
|
...(options.display ? [`window.__PMX_CANVAS_JSON_RENDER_DISPLAY__ = ${JSON.stringify(options.display)};`] : []),
|
|
952
961
|
...(options.devtools ? ['window.__PMX_CANVAS_JSON_RENDER_DEVTOOLS__ = true;'] : []),
|
|
962
|
+
...(options.nodeId && options.axToken ? [
|
|
963
|
+
`window.__PMX_CANVAS_JSON_RENDER_NODE_ID__ = ${JSON.stringify(sanitizeAxValue(options.nodeId))};`,
|
|
964
|
+
`window.__PMX_CANVAS_AX_TOKEN__ = ${JSON.stringify(sanitizeAxValue(options.axToken))};`,
|
|
965
|
+
] : []),
|
|
953
966
|
jsBundle,
|
|
954
967
|
].join('\n');
|
|
955
968
|
return buildAppHtml({
|
package/src/mcp/canvas-access.ts
CHANGED
|
@@ -40,6 +40,22 @@ type SetAxFocusResult = ReturnType<PmxCanvas['setAxFocus']>;
|
|
|
40
40
|
type RecordAxEventInput = Parameters<PmxCanvas['recordAxEvent']>[0];
|
|
41
41
|
type RecordAxEventResult = ReturnType<PmxCanvas['recordAxEvent']>;
|
|
42
42
|
type SendSteeringResult = ReturnType<PmxCanvas['sendSteering']>;
|
|
43
|
+
type SubmitAxInteractionInput = Parameters<PmxCanvas['submitAxInteraction']>[0];
|
|
44
|
+
type SubmitAxInteractionResult = ReturnType<PmxCanvas['submitAxInteraction']>;
|
|
45
|
+
type GetPendingSteeringResult = ReturnType<PmxCanvas['getPendingSteering']>;
|
|
46
|
+
type ListElicitationsResult = ReturnType<PmxCanvas['listElicitations']>;
|
|
47
|
+
type RequestElicitationInput = Parameters<PmxCanvas['requestElicitation']>[0];
|
|
48
|
+
type RequestElicitationResult = ReturnType<PmxCanvas['requestElicitation']>;
|
|
49
|
+
type RespondElicitationResult = ReturnType<PmxCanvas['respondElicitation']>;
|
|
50
|
+
type ListModeRequestsResult = ReturnType<PmxCanvas['listModeRequests']>;
|
|
51
|
+
type RequestModeInput = Parameters<PmxCanvas['requestMode']>[0];
|
|
52
|
+
type RequestModeResult = ReturnType<PmxCanvas['requestMode']>;
|
|
53
|
+
type ResolveModeRequestResult = ReturnType<PmxCanvas['resolveModeRequest']>;
|
|
54
|
+
type GetCommandRegistryResult = ReturnType<PmxCanvas['getCommandRegistry']>;
|
|
55
|
+
type InvokeCommandResult = ReturnType<PmxCanvas['invokeCommand']>;
|
|
56
|
+
type GetPolicyResult = ReturnType<PmxCanvas['getPolicy']>;
|
|
57
|
+
type SetPolicyInput = Parameters<PmxCanvas['setPolicy']>[0];
|
|
58
|
+
type SetPolicyResult = ReturnType<PmxCanvas['setPolicy']>;
|
|
43
59
|
type GetAxTimelineQuery = Parameters<PmxCanvas['getAxTimeline']>[0];
|
|
44
60
|
type GetAxTimelineResult = ReturnType<PmxCanvas['getAxTimeline']>;
|
|
45
61
|
type AddWorkItemInput = Parameters<PmxCanvas['addWorkItem']>[0];
|
|
@@ -166,6 +182,19 @@ export interface CanvasAccess {
|
|
|
166
182
|
listReviewAnnotations(): Promise<ListReviewAnnotationsResult>;
|
|
167
183
|
getHostCapability(): Promise<GetHostCapabilityResult>;
|
|
168
184
|
reportHostCapability(input: unknown, options?: { source?: PmxAxSource }): Promise<ReportHostCapabilityResult>;
|
|
185
|
+
submitAxInteraction(input: SubmitAxInteractionInput, options?: { source?: PmxAxSource }): Promise<SubmitAxInteractionResult>;
|
|
186
|
+
getPendingSteering(options?: { consumer?: string; limit?: number }): Promise<GetPendingSteeringResult>;
|
|
187
|
+
markSteeringDelivered(id: string): Promise<boolean>;
|
|
188
|
+
listElicitations(): Promise<ListElicitationsResult>;
|
|
189
|
+
requestElicitation(input: RequestElicitationInput, options?: { source?: PmxAxSource }): Promise<RequestElicitationResult>;
|
|
190
|
+
respondElicitation(id: string, response: Record<string, unknown>, options?: { source?: PmxAxSource }): Promise<RespondElicitationResult>;
|
|
191
|
+
listModeRequests(): Promise<ListModeRequestsResult>;
|
|
192
|
+
requestMode(input: RequestModeInput, options?: { source?: PmxAxSource }): Promise<RequestModeResult>;
|
|
193
|
+
resolveModeRequest(id: string, decision: 'approved' | 'rejected', options?: { resolution?: string; source?: PmxAxSource }): Promise<ResolveModeRequestResult>;
|
|
194
|
+
getCommandRegistry(): Promise<GetCommandRegistryResult>;
|
|
195
|
+
invokeCommand(name: string, args?: Record<string, unknown> | null, options?: { source?: PmxAxSource }): Promise<InvokeCommandResult>;
|
|
196
|
+
getPolicy(): Promise<GetPolicyResult>;
|
|
197
|
+
setPolicy(patch: SetPolicyInput, options?: { source?: PmxAxSource }): Promise<SetPolicyResult>;
|
|
169
198
|
clear(): Promise<void>;
|
|
170
199
|
search(query: string): Promise<SearchResult>;
|
|
171
200
|
undo(): Promise<UndoRedoResult>;
|
|
@@ -242,7 +271,9 @@ class LocalCanvasAccess implements CanvasAccess {
|
|
|
242
271
|
}
|
|
243
272
|
|
|
244
273
|
async addHtmlNode(input: AddHtmlNodeInput): Promise<string> {
|
|
245
|
-
|
|
274
|
+
// PmxCanvas.addHtmlNode returns the created node; the CanvasAccess contract
|
|
275
|
+
// is a bare id string, so extract it (mirrors addNode above).
|
|
276
|
+
return this.canvas.addHtmlNode(input).id;
|
|
246
277
|
}
|
|
247
278
|
|
|
248
279
|
async addHtmlPrimitive(input: AddHtmlPrimitiveInput): Promise<AddHtmlPrimitiveResult> {
|
|
@@ -329,6 +360,58 @@ class LocalCanvasAccess implements CanvasAccess {
|
|
|
329
360
|
return this.canvas.addWorkItem(input, { source: options?.source ?? 'mcp' });
|
|
330
361
|
}
|
|
331
362
|
|
|
363
|
+
async submitAxInteraction(input: SubmitAxInteractionInput, options?: { source?: PmxAxSource }): Promise<SubmitAxInteractionResult> {
|
|
364
|
+
return this.canvas.submitAxInteraction(input, { source: options?.source ?? 'mcp' });
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async getPendingSteering(options?: { consumer?: string; limit?: number }): Promise<GetPendingSteeringResult> {
|
|
368
|
+
return this.canvas.getPendingSteering(options);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async markSteeringDelivered(id: string): Promise<boolean> {
|
|
372
|
+
return this.canvas.markSteeringDelivered(id);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async listElicitations(): Promise<ListElicitationsResult> {
|
|
376
|
+
return this.canvas.listElicitations();
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async requestElicitation(input: RequestElicitationInput, options?: { source?: PmxAxSource }): Promise<RequestElicitationResult> {
|
|
380
|
+
return this.canvas.requestElicitation(input, { source: options?.source ?? 'mcp' });
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async respondElicitation(id: string, response: Record<string, unknown>, options?: { source?: PmxAxSource }): Promise<RespondElicitationResult> {
|
|
384
|
+
return this.canvas.respondElicitation(id, response, { source: options?.source ?? 'mcp' });
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async listModeRequests(): Promise<ListModeRequestsResult> {
|
|
388
|
+
return this.canvas.listModeRequests();
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async requestMode(input: RequestModeInput, options?: { source?: PmxAxSource }): Promise<RequestModeResult> {
|
|
392
|
+
return this.canvas.requestMode(input, { source: options?.source ?? 'mcp' });
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
async resolveModeRequest(id: string, decision: 'approved' | 'rejected', options?: { resolution?: string; source?: PmxAxSource }): Promise<ResolveModeRequestResult> {
|
|
396
|
+
return this.canvas.resolveModeRequest(id, decision, { ...(options ?? {}), source: options?.source ?? 'mcp' });
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
async getCommandRegistry(): Promise<GetCommandRegistryResult> {
|
|
400
|
+
return this.canvas.getCommandRegistry();
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
async invokeCommand(name: string, args?: Record<string, unknown> | null, options?: { source?: PmxAxSource }): Promise<InvokeCommandResult> {
|
|
404
|
+
return this.canvas.invokeCommand(name, args ?? null, { source: options?.source ?? 'mcp' });
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
async getPolicy(): Promise<GetPolicyResult> {
|
|
408
|
+
return this.canvas.getPolicy();
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async setPolicy(patch: SetPolicyInput, options?: { source?: PmxAxSource }): Promise<SetPolicyResult> {
|
|
412
|
+
return this.canvas.setPolicy(patch, { source: options?.source ?? 'mcp' });
|
|
413
|
+
}
|
|
414
|
+
|
|
332
415
|
async updateWorkItem(id: string, patch: UpdateWorkItemPatch, options?: { source?: PmxAxSource }): Promise<UpdateWorkItemResult> {
|
|
333
416
|
return this.canvas.updateWorkItem(id, patch, { source: options?.source ?? 'mcp' });
|
|
334
417
|
}
|
|
@@ -790,6 +873,120 @@ class RemoteCanvasAccess implements CanvasAccess {
|
|
|
790
873
|
return response.workItem;
|
|
791
874
|
}
|
|
792
875
|
|
|
876
|
+
async submitAxInteraction(input: SubmitAxInteractionInput, options?: { source?: PmxAxSource }): Promise<SubmitAxInteractionResult> {
|
|
877
|
+
// The interaction endpoint returns its structured outcome (ok/code/error) in
|
|
878
|
+
// the body for both accepted and rejected interactions, so read the body
|
|
879
|
+
// regardless of HTTP status rather than throwing on a denial.
|
|
880
|
+
const response = await fetch(`${this.remoteBaseUrl}/api/canvas/ax/interaction`, {
|
|
881
|
+
method: 'POST',
|
|
882
|
+
headers: { 'Content-Type': 'application/json' },
|
|
883
|
+
body: JSON.stringify({ ...input, source: options?.source ?? 'mcp' }),
|
|
884
|
+
});
|
|
885
|
+
const body = await response.json().catch(() => null);
|
|
886
|
+
if (body && typeof body === 'object') return body as SubmitAxInteractionResult;
|
|
887
|
+
throw new Error(`Remote canvas interaction failed with HTTP ${response.status}`);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
async getPendingSteering(options?: { consumer?: string; limit?: number }): Promise<GetPendingSteeringResult> {
|
|
891
|
+
const params = new URLSearchParams();
|
|
892
|
+
if (options?.consumer) params.set('consumer', options.consumer);
|
|
893
|
+
if (options?.limit) params.set('limit', String(options.limit));
|
|
894
|
+
const qs = params.toString();
|
|
895
|
+
const response = await this.requestJson<{ pending?: GetPendingSteeringResult }>(
|
|
896
|
+
'GET',
|
|
897
|
+
`/api/canvas/ax/delivery/pending${qs ? `?${qs}` : ''}`,
|
|
898
|
+
);
|
|
899
|
+
return response.pending ?? [];
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
async markSteeringDelivered(id: string): Promise<boolean> {
|
|
903
|
+
const response = await this.requestJson<{ delivered?: boolean }>(
|
|
904
|
+
'POST',
|
|
905
|
+
`/api/canvas/ax/delivery/${encodeURIComponent(id)}/mark`,
|
|
906
|
+
{},
|
|
907
|
+
);
|
|
908
|
+
return response.delivered ?? false;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
async listElicitations(): Promise<ListElicitationsResult> {
|
|
912
|
+
const r = await this.requestJson<{ elicitations?: ListElicitationsResult }>('GET', '/api/canvas/ax/elicitation');
|
|
913
|
+
return r.elicitations ?? [];
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
async requestElicitation(input: RequestElicitationInput, options?: { source?: PmxAxSource }): Promise<RequestElicitationResult> {
|
|
917
|
+
const r = await this.requestJson<{ elicitation?: RequestElicitationResult }>('POST', '/api/canvas/ax/elicitation', {
|
|
918
|
+
...input,
|
|
919
|
+
source: options?.source ?? 'mcp',
|
|
920
|
+
});
|
|
921
|
+
if (!r.elicitation) throw new Error('Remote canvas did not return an elicitation.');
|
|
922
|
+
return r.elicitation;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
async respondElicitation(id: string, response: Record<string, unknown>, options?: { source?: PmxAxSource }): Promise<RespondElicitationResult> {
|
|
926
|
+
const res = await fetch(`${this.remoteBaseUrl}/api/canvas/ax/elicitation/${encodeURIComponent(id)}/respond`, {
|
|
927
|
+
method: 'POST',
|
|
928
|
+
headers: { 'Content-Type': 'application/json' },
|
|
929
|
+
body: JSON.stringify({ response, source: options?.source ?? 'mcp' }),
|
|
930
|
+
});
|
|
931
|
+
if (res.status === 404) return null;
|
|
932
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
933
|
+
return (await res.json() as { elicitation?: RespondElicitationResult }).elicitation ?? null;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
async listModeRequests(): Promise<ListModeRequestsResult> {
|
|
937
|
+
const r = await this.requestJson<{ modeRequests?: ListModeRequestsResult }>('GET', '/api/canvas/ax/mode');
|
|
938
|
+
return r.modeRequests ?? [];
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
async requestMode(input: RequestModeInput, options?: { source?: PmxAxSource }): Promise<RequestModeResult> {
|
|
942
|
+
const r = await this.requestJson<{ modeRequest?: RequestModeResult }>('POST', '/api/canvas/ax/mode', {
|
|
943
|
+
...input,
|
|
944
|
+
source: options?.source ?? 'mcp',
|
|
945
|
+
});
|
|
946
|
+
if (!r.modeRequest) throw new Error('Remote canvas did not return a mode request.');
|
|
947
|
+
return r.modeRequest;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
async resolveModeRequest(id: string, decision: 'approved' | 'rejected', options?: { resolution?: string; source?: PmxAxSource }): Promise<ResolveModeRequestResult> {
|
|
951
|
+
const res = await fetch(`${this.remoteBaseUrl}/api/canvas/ax/mode/${encodeURIComponent(id)}/resolve`, {
|
|
952
|
+
method: 'POST',
|
|
953
|
+
headers: { 'Content-Type': 'application/json' },
|
|
954
|
+
body: JSON.stringify({ decision, ...(options?.resolution ? { resolution: options.resolution } : {}), source: options?.source ?? 'mcp' }),
|
|
955
|
+
});
|
|
956
|
+
if (res.status === 404) return null;
|
|
957
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
958
|
+
return (await res.json() as { modeRequest?: ResolveModeRequestResult }).modeRequest ?? null;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
async getCommandRegistry(): Promise<GetCommandRegistryResult> {
|
|
962
|
+
const r = await this.requestJson<{ commands?: GetCommandRegistryResult }>('GET', '/api/canvas/ax/command');
|
|
963
|
+
return r.commands ?? [];
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
async invokeCommand(name: string, args?: Record<string, unknown> | null, options?: { source?: PmxAxSource }): Promise<InvokeCommandResult> {
|
|
967
|
+
const r = await this.requestJson<{ event?: InvokeCommandResult }>('POST', '/api/canvas/ax/command', {
|
|
968
|
+
name,
|
|
969
|
+
...(args ? { args } : {}),
|
|
970
|
+
source: options?.source ?? 'mcp',
|
|
971
|
+
});
|
|
972
|
+
return r.event ?? null;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
async getPolicy(): Promise<GetPolicyResult> {
|
|
976
|
+
const r = await this.requestJson<{ policy?: GetPolicyResult }>('GET', '/api/canvas/ax/policy');
|
|
977
|
+
if (!r.policy) throw new Error('Remote canvas did not return a policy.');
|
|
978
|
+
return r.policy;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
async setPolicy(patch: SetPolicyInput, options?: { source?: PmxAxSource }): Promise<SetPolicyResult> {
|
|
982
|
+
const r = await this.requestJson<{ policy?: SetPolicyResult }>('POST', '/api/canvas/ax/policy', {
|
|
983
|
+
...patch,
|
|
984
|
+
source: options?.source ?? 'mcp',
|
|
985
|
+
});
|
|
986
|
+
if (!r.policy) throw new Error('Remote canvas did not return a policy.');
|
|
987
|
+
return r.policy;
|
|
988
|
+
}
|
|
989
|
+
|
|
793
990
|
async updateWorkItem(id: string, patch: UpdateWorkItemPatch, options?: { source?: PmxAxSource }): Promise<UpdateWorkItemResult> {
|
|
794
991
|
const response = await fetch(`${this.remoteBaseUrl}/api/canvas/ax/work/${encodeURIComponent(id)}`, {
|
|
795
992
|
method: 'PATCH',
|