pmx-canvas 0.2.7 → 0.3.1
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 +148 -1
- package/Readme.md +13 -11
- package/dist/canvas/index.js +2 -2
- package/dist/json-render/index.css +1 -1
- package/dist/types/cli/daemon.d.ts +74 -0
- package/dist/types/cli/watch.d.ts +2 -2
- package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
- package/dist/types/client/canvas/CommandPalette.d.ts +1 -1
- package/dist/types/client/canvas/Minimap.d.ts +1 -1
- package/dist/types/client/nodes/FileNode.d.ts +1 -1
- package/dist/types/client/nodes/ImageNode.d.ts +1 -1
- package/dist/types/client/nodes/InlineFormatBar.d.ts +1 -1
- package/dist/types/client/nodes/MarkdownNode.d.ts +1 -1
- package/dist/types/client/nodes/PromptNode.d.ts +1 -1
- package/dist/types/client/nodes/ResponseNode.d.ts +1 -1
- package/dist/types/mcp/canvas-access.d.ts +0 -7
- package/dist/types/server/ax-state-manager.d.ts +3 -2
- package/dist/types/server/bundled-skills.d.ts +3 -3
- package/dist/types/server/canvas-schema.d.ts +1 -1
- package/dist/types/server/canvas-state.d.ts +2 -0
- package/dist/types/server/ext-app-lookup.d.ts +1 -3
- package/dist/types/server/html-primitives.d.ts +1 -1
- package/dist/types/server/index.d.ts +4 -4
- package/dist/types/server/operations/composites.d.ts +29 -23
- package/dist/types/server/operations/http.d.ts +2 -1
- package/dist/types/server/operations/index.d.ts +1 -1
- package/dist/types/server/operations/ops/ax-read.d.ts +2 -0
- package/dist/types/server/operations/ops/canvas-wire.d.ts +2 -0
- package/dist/types/server/operations/ops/ext-app.d.ts +2 -0
- package/dist/types/shared/ax-intent.d.ts +1 -1
- package/docs/RELEASE.md +23 -8
- package/docs/ax-host-adapter-contract.md +7 -7
- package/docs/ax-state-contract.md +7 -6
- package/docs/bun-webview-integration.md +5 -5
- package/docs/cli.md +1 -1
- package/docs/http-api.md +35 -3
- package/docs/mcp.md +121 -100
- package/docs/node-types.md +45 -35
- package/docs/plans/plan-006-mcp-tool-consolidation.md +3 -1
- package/docs/plans/plan-008-registry-finish.md +2 -0
- package/docs/plans/plan-009-tech-debt-backlog.md +51 -0
- package/docs/screenshot.png +0 -0
- package/docs/tech-debt-assessment-2026-06.md +1 -1
- package/docs/tech-debt-assessment-2026-07.md +135 -0
- package/package.json +5 -2
- package/skills/data-analysis/SKILL.md +3 -3
- package/skills/frontend-design/SKILL.md +3 -2
- package/skills/json-render-mcp/SKILL.md +4 -3
- package/skills/playwright-cli/SKILL.md +1 -1
- package/skills/pmx-canvas/SKILL.md +12 -7
- package/skills/pmx-canvas/evals/evals.json +2 -2
- package/skills/pmx-canvas/references/ax-html-control-surface.md +3 -1
- package/skills/pmx-canvas/references/excalidraw-diagram-authoring.md +2 -2
- package/skills/pmx-canvas/references/full-reference.md +58 -56
- package/skills/pmx-canvas/references/html-primitives.md +3 -3
- package/skills/tufte-viz/SKILL.md +5 -4
- package/skills/web-artifacts-builder/SKILL.md +4 -4
- package/src/cli/agent.ts +1861 -1548
- package/src/cli/daemon.ts +460 -0
- package/src/cli/index.ts +63 -326
- package/src/cli/watch.ts +2 -10
- package/src/client/App.tsx +48 -46
- package/src/client/canvas/AttentionHistory.tsx +11 -1
- package/src/client/canvas/CanvasNode.tsx +41 -29
- package/src/client/canvas/CanvasViewport.tsx +101 -66
- package/src/client/canvas/CommandPalette.tsx +61 -27
- package/src/client/canvas/ContextMenu.tsx +13 -20
- package/src/client/canvas/ContextPinBar.tsx +1 -5
- package/src/client/canvas/ContextPinHud.tsx +1 -6
- package/src/client/canvas/DockedNode.tsx +4 -4
- package/src/client/canvas/EdgeLayer.tsx +37 -36
- package/src/client/canvas/ExpandedNodeOverlay.tsx +69 -45
- package/src/client/canvas/FocusFieldLayer.tsx +20 -22
- package/src/client/canvas/IntentLayer.tsx +31 -14
- package/src/client/canvas/Minimap.tsx +11 -16
- package/src/client/canvas/SelectionBar.tsx +4 -11
- package/src/client/canvas/ShortcutOverlay.tsx +3 -1
- package/src/client/canvas/SnapshotPanel.tsx +77 -95
- package/src/client/canvas/auto-fit.ts +15 -14
- package/src/client/canvas/snap-guides.ts +12 -12
- package/src/client/canvas/use-node-resize.ts +1 -5
- package/src/client/canvas/use-pan-zoom.ts +25 -26
- package/src/client/ext-app/bridge.ts +3 -12
- package/src/client/icons.tsx +63 -20
- package/src/client/nodes/ContextNode.tsx +14 -25
- package/src/client/nodes/ExtAppFrame.tsx +60 -39
- package/src/client/nodes/FileNode.tsx +74 -62
- package/src/client/nodes/GroupNode.tsx +4 -6
- package/src/client/nodes/HtmlNode.tsx +76 -46
- package/src/client/nodes/ImageNode.tsx +18 -27
- package/src/client/nodes/InlineFormatBar.tsx +4 -21
- package/src/client/nodes/InlineMarkdownEditor.tsx +0 -1
- package/src/client/nodes/LedgerNode.tsx +10 -4
- package/src/client/nodes/MarkdownNode.tsx +3 -10
- package/src/client/nodes/McpAppNode.tsx +26 -22
- package/src/client/nodes/MdFormatBar.tsx +10 -7
- package/src/client/nodes/PromptNode.tsx +23 -51
- package/src/client/nodes/ResponseNode.tsx +3 -13
- package/src/client/nodes/StatusNode.tsx +5 -9
- package/src/client/nodes/StatusSummary.tsx +2 -8
- package/src/client/nodes/WebpageNode.tsx +20 -14
- package/src/client/nodes/iframe-document-url.ts +25 -16
- package/src/client/nodes/image-warnings.ts +1 -7
- package/src/client/nodes/md-format.ts +20 -5
- package/src/client/state/attention-bridge.ts +4 -9
- package/src/client/state/attention-store.ts +1 -7
- package/src/client/state/canvas-store.ts +52 -36
- package/src/client/state/intent-bridge.ts +176 -112
- package/src/client/state/intent-store.ts +4 -1
- package/src/client/state/sse-bridge.ts +53 -70
- package/src/json-render/catalog.ts +12 -16
- package/src/json-render/charts/components.tsx +16 -20
- package/src/json-render/charts/extra-components.tsx +8 -16
- package/src/json-render/charts/extra-definitions.ts +1 -2
- package/src/json-render/charts/tufte-components.tsx +37 -20
- package/src/json-render/charts/tufte-definitions.ts +8 -2
- package/src/json-render/renderer/index.tsx +42 -22
- package/src/json-render/schema.ts +6 -3
- package/src/json-render/server.ts +33 -39
- package/src/mcp/canvas-access.ts +40 -121
- package/src/mcp/server.ts +206 -271
- package/src/server/agent-context.ts +63 -36
- package/src/server/ax-context.ts +7 -5
- package/src/server/ax-interaction.ts +176 -43
- package/src/server/ax-state-manager.ts +185 -41
- package/src/server/ax-state.ts +142 -47
- package/src/server/bundled-skills.ts +3 -3
- package/src/server/canvas-db.ts +213 -95
- package/src/server/canvas-operations.ts +177 -120
- package/src/server/canvas-provenance.ts +1 -4
- package/src/server/canvas-schema.ts +500 -102
- package/src/server/canvas-serialization.ts +27 -35
- package/src/server/canvas-state.ts +155 -58
- package/src/server/chart-template.ts +4 -5
- package/src/server/code-graph.ts +20 -7
- package/src/server/diagram-presets.ts +28 -29
- package/src/server/ext-app-lookup.ts +4 -20
- package/src/server/html-node-summary.ts +19 -10
- package/src/server/html-primitives.ts +326 -97
- package/src/server/html-surface.ts +6 -9
- package/src/server/image-source.ts +6 -4
- package/src/server/index.ts +321 -218
- package/src/server/intent-registry.ts +3 -6
- package/src/server/mcp-app-candidate.ts +5 -10
- package/src/server/mcp-app-host.ts +14 -38
- package/src/server/mcp-app-runtime.ts +12 -20
- package/src/server/mutation-history.ts +15 -5
- package/src/server/operations/composites.ts +38 -40
- package/src/server/operations/http.ts +5 -5
- package/src/server/operations/index.ts +7 -1
- package/src/server/operations/invoker.ts +4 -3
- package/src/server/operations/mcp.ts +29 -36
- package/src/server/operations/ops/annotation.ts +122 -10
- package/src/server/operations/ops/app.ts +102 -77
- package/src/server/operations/ops/ax-await.ts +17 -10
- package/src/server/operations/ops/ax-read.ts +347 -0
- package/src/server/operations/ops/ax-shared.ts +2 -7
- package/src/server/operations/ops/ax-state.ts +32 -14
- package/src/server/operations/ops/ax-timeline.ts +32 -19
- package/src/server/operations/ops/ax-work.ts +54 -37
- package/src/server/operations/ops/batch.ts +39 -14
- package/src/server/operations/ops/canvas-wire.ts +91 -0
- package/src/server/operations/ops/edges.ts +37 -25
- package/src/server/operations/ops/ext-app.ts +346 -0
- package/src/server/operations/ops/groups.ts +49 -20
- package/src/server/operations/ops/intent.ts +18 -12
- package/src/server/operations/ops/json-render.ts +239 -98
- package/src/server/operations/ops/nodes.ts +300 -111
- package/src/server/operations/ops/query.ts +62 -33
- package/src/server/operations/ops/snapshots.ts +35 -26
- package/src/server/operations/ops/validate.ts +2 -1
- package/src/server/operations/ops/viewport.ts +60 -16
- package/src/server/operations/ops/webview.ts +44 -18
- package/src/server/operations/registry.ts +2 -3
- package/src/server/operations/types.ts +7 -5
- package/src/server/operations/webview-runner.ts +1 -3
- package/src/server/placement.ts +8 -18
- package/src/server/server.ts +121 -1012
- package/src/server/spatial-analysis.ts +39 -25
- package/src/server/trace-manager.ts +3 -8
- package/src/server/web-artifacts.ts +23 -27
- package/src/server/webpage-node.ts +5 -13
- package/src/shared/auto-arrange.ts +12 -5
- package/src/shared/ax-intent.ts +1 -1
- package/src/shared/content-height-reporter.ts +8 -6
- package/src/shared/ext-app-tool-result.ts +2 -6
- package/src/shared/placement.ts +1 -4
- package/src/shared/semantic-attention.ts +39 -37
- package/src/shared/surface.ts +8 -4
|
@@ -19,7 +19,16 @@ import { tufteChartComponents } from '../charts/tufte-components';
|
|
|
19
19
|
import { pmxCanvasDirectives } from '../directives';
|
|
20
20
|
import { JsonRenderDevtools } from '@json-render/devtools-react';
|
|
21
21
|
|
|
22
|
-
type BadgeVariant =
|
|
22
|
+
type BadgeVariant =
|
|
23
|
+
| 'default'
|
|
24
|
+
| 'secondary'
|
|
25
|
+
| 'destructive'
|
|
26
|
+
| 'outline'
|
|
27
|
+
| 'success'
|
|
28
|
+
| 'info'
|
|
29
|
+
| 'warning'
|
|
30
|
+
| 'error'
|
|
31
|
+
| 'danger';
|
|
23
32
|
type BadgeProps = {
|
|
24
33
|
text: string;
|
|
25
34
|
variant?: BadgeVariant | null;
|
|
@@ -30,11 +39,7 @@ function Badge({ props }: { props: BadgeProps }) {
|
|
|
30
39
|
const variant = props.variant;
|
|
31
40
|
const resolvedVariant = variant ?? 'default';
|
|
32
41
|
return (
|
|
33
|
-
<span
|
|
34
|
-
data-slot="badge"
|
|
35
|
-
data-variant={resolvedVariant}
|
|
36
|
-
className={`pmx-badge pmx-badge--${resolvedVariant}`}
|
|
37
|
-
>
|
|
42
|
+
<span data-slot="badge" data-variant={resolvedVariant} className={`pmx-badge pmx-badge--${resolvedVariant}`}>
|
|
38
43
|
{props.text}
|
|
39
44
|
</span>
|
|
40
45
|
);
|
|
@@ -111,9 +116,17 @@ function AxStateSync() {
|
|
|
111
116
|
// validates + submits through the capability-gated endpoint). Convention-based
|
|
112
117
|
// opt-in: spec authors name the action handler after the AX interaction type.
|
|
113
118
|
const AX_INTERACTION_HANDLER_NAMES = [
|
|
114
|
-
'ax.event.record',
|
|
115
|
-
'ax.
|
|
116
|
-
'ax.
|
|
119
|
+
'ax.event.record',
|
|
120
|
+
'ax.steer',
|
|
121
|
+
'ax.work.create',
|
|
122
|
+
'ax.work.update',
|
|
123
|
+
'ax.evidence.add',
|
|
124
|
+
'ax.approval.request',
|
|
125
|
+
'ax.review.add',
|
|
126
|
+
'ax.focus.set',
|
|
127
|
+
'ax.elicitation.request',
|
|
128
|
+
'ax.mode.request',
|
|
129
|
+
'ax.command.invoke',
|
|
117
130
|
] as const;
|
|
118
131
|
|
|
119
132
|
function buildAxHandlers(): Record<string, (params: Record<string, unknown>) => void> {
|
|
@@ -126,12 +139,15 @@ function buildAxHandlers(): Record<string, (params: Record<string, unknown>) =>
|
|
|
126
139
|
// is no JS surface for a Promise-style ack here, so we don't stamp a correlationId.
|
|
127
140
|
for (const type of AX_INTERACTION_HANDLER_NAMES) {
|
|
128
141
|
handlers[type] = (params: Record<string, unknown>) => {
|
|
129
|
-
window.parent.postMessage(
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
142
|
+
window.parent.postMessage(
|
|
143
|
+
{
|
|
144
|
+
source: 'pmx-canvas-ax',
|
|
145
|
+
token,
|
|
146
|
+
nodeId,
|
|
147
|
+
interaction: { type, payload: params && typeof params === 'object' ? params : {} },
|
|
148
|
+
},
|
|
149
|
+
'*',
|
|
150
|
+
);
|
|
135
151
|
};
|
|
136
152
|
}
|
|
137
153
|
return handlers;
|
|
@@ -176,16 +192,22 @@ function App() {
|
|
|
176
192
|
|
|
177
193
|
// Seed AX state under a reserved `/ax` key so specs can bind { "$state": "/ax/workItems" }.
|
|
178
194
|
const axState = window.__PMX_CANVAS_AX_STATE__;
|
|
179
|
-
const initialState =
|
|
180
|
-
? { ...(spec.state ?? {}), ax: axState }
|
|
181
|
-
: spec.state ?? undefined;
|
|
195
|
+
const initialState =
|
|
196
|
+
axState !== undefined && axState !== null ? { ...(spec.state ?? {}), ax: axState } : (spec.state ?? undefined);
|
|
182
197
|
|
|
183
198
|
// Standalone "Open as site" tab (#65): fill the browser viewport instead of the
|
|
184
199
|
// in-canvas card height. The chart child flex-grows; useChartFrameHeight measures
|
|
185
200
|
// the full viewport in this mode. Embedded/expanded keep the padded min-height box.
|
|
186
201
|
const isSite = window.__PMX_CANVAS_JSON_RENDER_DISPLAY__ === 'site';
|
|
187
202
|
const containerStyle = isSite
|
|
188
|
-
? {
|
|
203
|
+
? {
|
|
204
|
+
display: 'flex',
|
|
205
|
+
flexDirection: 'column' as const,
|
|
206
|
+
height: '100dvh',
|
|
207
|
+
minHeight: '100dvh',
|
|
208
|
+
padding: 0,
|
|
209
|
+
boxSizing: 'border-box' as const,
|
|
210
|
+
}
|
|
189
211
|
: { minHeight: '100vh', padding: 16, boxSizing: 'border-box' as const };
|
|
190
212
|
return (
|
|
191
213
|
<div style={containerStyle}>
|
|
@@ -199,9 +221,7 @@ function App() {
|
|
|
199
221
|
<div style={isSite ? { flex: 1, minHeight: 0 } : undefined}>
|
|
200
222
|
<Renderer spec={spec} registry={registry} loading={false} />
|
|
201
223
|
</div>
|
|
202
|
-
{window.__PMX_CANVAS_JSON_RENDER_DEVTOOLS__ ?
|
|
203
|
-
<JsonRenderDevtools position="right" />
|
|
204
|
-
) : null}
|
|
224
|
+
{window.__PMX_CANVAS_JSON_RENDER_DEVTOOLS__ ? <JsonRenderDevtools position="right" /> : null}
|
|
205
225
|
</JSONUIProvider>
|
|
206
226
|
</div>
|
|
207
227
|
);
|
|
@@ -34,11 +34,13 @@ export const schema = defineSchema(
|
|
|
34
34
|
builtInActions: [
|
|
35
35
|
{
|
|
36
36
|
name: 'setState',
|
|
37
|
-
description:
|
|
37
|
+
description:
|
|
38
|
+
'Update a value in the state model at the given statePath. Params: { statePath: string, value: any }',
|
|
38
39
|
},
|
|
39
40
|
{
|
|
40
41
|
name: 'pushState',
|
|
41
|
-
description:
|
|
42
|
+
description:
|
|
43
|
+
'Append an item to an array in state. Params: { statePath: string, value: any, clearStatePath?: string }. Value can contain {"$state":"/path"} refs and "$id" for auto IDs.',
|
|
42
44
|
},
|
|
43
45
|
{
|
|
44
46
|
name: 'removeState',
|
|
@@ -46,7 +48,8 @@ export const schema = defineSchema(
|
|
|
46
48
|
},
|
|
47
49
|
{
|
|
48
50
|
name: 'validateForm',
|
|
49
|
-
description:
|
|
51
|
+
description:
|
|
52
|
+
'Validate all registered form fields and write the result to state. Params: { statePath?: string }. Defaults to /formValidation. Result: { valid: boolean, errors: Record<string, string[]> }.',
|
|
50
53
|
},
|
|
51
54
|
],
|
|
52
55
|
defaultRules: [
|
|
@@ -212,9 +212,7 @@ async function ensureJsonRenderBundle(): Promise<void> {
|
|
|
212
212
|
// Avoid live source-vs-dist rebuild checks here because Bun's bundler can stall on
|
|
213
213
|
// the @json-render/shadcn dependency graph during request-time viewer generation.
|
|
214
214
|
const needsBuild =
|
|
215
|
-
!existsSync(jsPath) ||
|
|
216
|
-
!existsSync(cssPath) ||
|
|
217
|
-
process.env.PMX_CANVAS_FORCE_JSON_RENDER_REBUILD === '1';
|
|
215
|
+
!existsSync(jsPath) || !existsSync(cssPath) || process.env.PMX_CANVAS_FORCE_JSON_RENDER_REBUILD === '1';
|
|
218
216
|
|
|
219
217
|
if (needsBuild) {
|
|
220
218
|
await rebuildJsonRenderBundle();
|
|
@@ -273,7 +271,7 @@ function normalizeItemArray(value: unknown): unknown {
|
|
|
273
271
|
if (hasString(item)) return { label: item, value: item };
|
|
274
272
|
const record = asRecord(item);
|
|
275
273
|
const label = hasString(record?.label) ? record.label : hasString(record?.text) ? record.text : null;
|
|
276
|
-
const resolvedValue = hasString(record?.value) ? record.value : label ?? `option-${index + 1}
|
|
274
|
+
const resolvedValue = hasString(record?.value) ? record.value : (label ?? `option-${index + 1}`);
|
|
277
275
|
return {
|
|
278
276
|
label: label ?? resolvedValue,
|
|
279
277
|
value: resolvedValue,
|
|
@@ -283,11 +281,7 @@ function normalizeItemArray(value: unknown): unknown {
|
|
|
283
281
|
|
|
284
282
|
function normalizeStringMatrix(value: unknown): unknown {
|
|
285
283
|
if (!Array.isArray(value)) return value;
|
|
286
|
-
return value.map((row) => (
|
|
287
|
-
Array.isArray(row)
|
|
288
|
-
? row.map((cell) => String(cell ?? ''))
|
|
289
|
-
: [String(row ?? '')]
|
|
290
|
-
));
|
|
284
|
+
return value.map((row) => (Array.isArray(row) ? row.map((cell) => String(cell ?? '')) : [String(row ?? '')]));
|
|
291
285
|
}
|
|
292
286
|
|
|
293
287
|
function normalizeButtonVariant(value: unknown): unknown {
|
|
@@ -347,12 +341,7 @@ function normalizeElementProps(
|
|
|
347
341
|
const props = (stripNullishDeep(rawProps) as Record<string, unknown> | undefined) ?? {};
|
|
348
342
|
|
|
349
343
|
for (const key of COERCIBLE_STRING_PROPS) {
|
|
350
|
-
if (
|
|
351
|
-
key in props &&
|
|
352
|
-
typeof props[key] !== 'string' &&
|
|
353
|
-
props[key] !== undefined &&
|
|
354
|
-
!isDynamicPropValue(props[key])
|
|
355
|
-
) {
|
|
344
|
+
if (key in props && typeof props[key] !== 'string' && props[key] !== undefined && !isDynamicPropValue(props[key])) {
|
|
356
345
|
props[key] = String(props[key]);
|
|
357
346
|
}
|
|
358
347
|
}
|
|
@@ -516,7 +505,9 @@ export function inferJsonRenderNodeTitle(spec: JsonRenderSpec, fallback = 'json-
|
|
|
516
505
|
export function normalizeAndValidateJsonRenderSpec(spec: unknown): JsonRenderSpec {
|
|
517
506
|
const specRecord = asRecord(normalizeJsonRenderInput(spec));
|
|
518
507
|
if (!specRecord || typeof specRecord.root !== 'string' || !asRecord(specRecord.elements)) {
|
|
519
|
-
throw new Error(
|
|
508
|
+
throw new Error(
|
|
509
|
+
'Missing root and elements in spec. Pass a complete {root, elements} document, or a single bare component object with a type field.',
|
|
510
|
+
);
|
|
520
511
|
}
|
|
521
512
|
|
|
522
513
|
// Reject an unrecognized $-keyed expression object in any element prop BEFORE
|
|
@@ -576,10 +567,7 @@ function collectDataKeys(data: Array<Record<string, unknown>>): Set<string> {
|
|
|
576
567
|
* as "value") without an explicit valueKey, instead of failing the data-key
|
|
577
568
|
* check on a reasonable guess.
|
|
578
569
|
*/
|
|
579
|
-
function firstPresentDataKey(
|
|
580
|
-
data: Array<Record<string, unknown>>,
|
|
581
|
-
candidates: string[],
|
|
582
|
-
): string | undefined {
|
|
570
|
+
function firstPresentDataKey(data: Array<Record<string, unknown>>, candidates: string[]): string | undefined {
|
|
583
571
|
const keys = collectDataKeys(data);
|
|
584
572
|
return candidates.find((key) => keys.has(key));
|
|
585
573
|
}
|
|
@@ -653,7 +641,9 @@ function assertGraphDataKeys(
|
|
|
653
641
|
const missing = [...required].filter((key) => !available.has(key));
|
|
654
642
|
if (missing.length === 0) return;
|
|
655
643
|
const availableList = [...available].sort().join(', ') || '(none)';
|
|
656
|
-
throw new Error(
|
|
644
|
+
throw new Error(
|
|
645
|
+
`Graph data key mismatch for ${chartType}: missing ${missing.join(', ')}. Available keys: ${availableList}.`,
|
|
646
|
+
);
|
|
657
647
|
}
|
|
658
648
|
|
|
659
649
|
export function buildGraphSpec(input: GraphNodeInput): JsonRenderSpec {
|
|
@@ -685,11 +675,11 @@ export function buildGraphSpec(input: GraphNodeInput): JsonRenderSpec {
|
|
|
685
675
|
}
|
|
686
676
|
case 'RadarChart': {
|
|
687
677
|
const axisKey = input.axisKey ?? input.xKey ?? 'axis';
|
|
688
|
-
const metrics = input.metrics?.length
|
|
689
|
-
? input.metrics
|
|
690
|
-
: inferKeysFromData(input.data, [axisKey]);
|
|
678
|
+
const metrics = input.metrics?.length ? input.metrics : inferKeysFromData(input.data, [axisKey]);
|
|
691
679
|
if (metrics.length === 0) {
|
|
692
|
-
throw new Error(
|
|
680
|
+
throw new Error(
|
|
681
|
+
'RadarChart requires at least one metric key (provide `metrics` or include numeric columns in `data`).',
|
|
682
|
+
);
|
|
693
683
|
}
|
|
694
684
|
chartProps.axisKey = axisKey;
|
|
695
685
|
chartProps.metrics = metrics;
|
|
@@ -698,11 +688,11 @@ export function buildGraphSpec(input: GraphNodeInput): JsonRenderSpec {
|
|
|
698
688
|
}
|
|
699
689
|
case 'StackedBarChart': {
|
|
700
690
|
const xKey = input.xKey ?? 'label';
|
|
701
|
-
const series = input.series?.length
|
|
702
|
-
? input.series
|
|
703
|
-
: inferKeysFromData(input.data, [xKey]);
|
|
691
|
+
const series = input.series?.length ? input.series : inferKeysFromData(input.data, [xKey]);
|
|
704
692
|
if (series.length === 0) {
|
|
705
|
-
throw new Error(
|
|
693
|
+
throw new Error(
|
|
694
|
+
'StackedBarChart requires at least one series key (provide `series` or include numeric columns in `data`).',
|
|
695
|
+
);
|
|
706
696
|
}
|
|
707
697
|
chartProps.xKey = xKey;
|
|
708
698
|
chartProps.series = series;
|
|
@@ -713,9 +703,10 @@ export function buildGraphSpec(input: GraphNodeInput): JsonRenderSpec {
|
|
|
713
703
|
case 'ComposedChart': {
|
|
714
704
|
chartProps.xKey = input.xKey ?? 'label';
|
|
715
705
|
chartProps.barKey = input.barKey ?? input.yKey ?? 'value';
|
|
716
|
-
chartProps.lineKey =
|
|
717
|
-
|
|
718
|
-
??
|
|
706
|
+
chartProps.lineKey =
|
|
707
|
+
input.lineKey ??
|
|
708
|
+
inferKeysFromData(input.data, [chartProps.xKey as string, chartProps.barKey as string])[0] ??
|
|
709
|
+
'rate';
|
|
719
710
|
chartProps.barColor = input.barColor ?? null;
|
|
720
711
|
chartProps.lineColor = input.lineColor ?? null;
|
|
721
712
|
chartProps.showLegend = input.showLegend !== false;
|
|
@@ -952,7 +943,8 @@ export async function buildJsonRenderViewerHtml(options: {
|
|
|
952
943
|
* node grows to it. Off for strictSize / user-resized nodes (they fill-down). */
|
|
953
944
|
fitContent?: boolean;
|
|
954
945
|
}): Promise<string> {
|
|
955
|
-
const sanitizeAxValue = (v?: string): string =>
|
|
946
|
+
const sanitizeAxValue = (v?: string): string =>
|
|
947
|
+
typeof v === 'string' ? v.replace(/[^A-Za-z0-9_-]/g, '').slice(0, 80) : '';
|
|
956
948
|
try {
|
|
957
949
|
await ensureJsonRenderBundle();
|
|
958
950
|
const dir = bundleDir();
|
|
@@ -967,12 +959,14 @@ export async function buildJsonRenderViewerHtml(options: {
|
|
|
967
959
|
...(options.theme ? [`window.__PMX_CANVAS_JSON_RENDER_THEME__ = ${JSON.stringify(options.theme)};`] : []),
|
|
968
960
|
...(options.display ? [`window.__PMX_CANVAS_JSON_RENDER_DISPLAY__ = ${JSON.stringify(options.display)};`] : []),
|
|
969
961
|
...(options.devtools ? ['window.__PMX_CANVAS_JSON_RENDER_DEVTOOLS__ = true;'] : []),
|
|
970
|
-
...(options.nodeId && options.axToken
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
962
|
+
...(options.nodeId && options.axToken
|
|
963
|
+
? [
|
|
964
|
+
`window.__PMX_CANVAS_JSON_RENDER_NODE_ID__ = ${JSON.stringify(sanitizeAxValue(options.nodeId))};`,
|
|
965
|
+
`window.__PMX_CANVAS_AX_TOKEN__ = ${JSON.stringify(sanitizeAxValue(options.axToken))};`,
|
|
966
|
+
// Read-side AX state: seed for initial render + bound under /ax for specs.
|
|
967
|
+
`window.__PMX_CANVAS_AX_STATE__ = ${JSON.stringify(options.axState ?? null).replace(/</g, '\\u003c')};`,
|
|
968
|
+
]
|
|
969
|
+
: []),
|
|
976
970
|
...(options.fitContent ? ['window.__PMX_CANVAS_FIT_CONTENT__ = true;'] : []),
|
|
977
971
|
jsBundle,
|
|
978
972
|
].join('\n');
|
package/src/mcp/canvas-access.ts
CHANGED
|
@@ -10,21 +10,14 @@ import {
|
|
|
10
10
|
type PmxCanvas,
|
|
11
11
|
} from '../server/index.js';
|
|
12
12
|
import type { PmxAxSource } from '../server/ax-state.js';
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
type RefreshWebpageNodeResult = Awaited<ReturnType<PmxCanvas['refreshWebpageNode']>>;
|
|
20
|
-
// openMcpApp / addDiagram / buildWebArtifact CanvasAccess methods + their type
|
|
21
|
-
// aliases removed with the standalone MCP tools (plan-008 Wave 4): those tools
|
|
22
|
-
// migrated to the operation registry (mcpapp.open / diagram.open /
|
|
13
|
+
import { HttpOperationInvoker, LocalOperationInvoker, type OperationInvoker } from '../server/operations/index.js';
|
|
14
|
+
|
|
15
|
+
// openMcpApp / addDiagram / buildWebArtifact / refreshWebpageNode / addHtmlNode /
|
|
16
|
+
// addHtmlPrimitive CanvasAccess methods + their type aliases removed with the
|
|
17
|
+
// standalone MCP tools (plan-008 Wave 4; refresh/html tools removed v0.3.0):
|
|
18
|
+
// those tools migrated to the operation registry (mcpapp.open / diagram.open /
|
|
23
19
|
// webartifact.build) and the composite/registry tools dispatch via the invoker,
|
|
24
20
|
// not CanvasAccess. The public SDK PmxCanvas methods are unchanged.
|
|
25
|
-
type AddHtmlNodeInput = Parameters<PmxCanvas['addHtmlNode']>[0];
|
|
26
|
-
type AddHtmlPrimitiveInput = Parameters<PmxCanvas['addHtmlPrimitive']>[0];
|
|
27
|
-
type AddHtmlPrimitiveResult = ReturnType<PmxCanvas['addHtmlPrimitive']>;
|
|
28
21
|
type AxStateResult = ReturnType<PmxCanvas['getAxState']>;
|
|
29
22
|
type AxContextResult = ReturnType<PmxCanvas['getAxContext']>;
|
|
30
23
|
type SubmitAxInteractionInput = Parameters<PmxCanvas['submitAxInteraction']>[0];
|
|
@@ -56,11 +49,6 @@ interface HealthResponse {
|
|
|
56
49
|
workspace?: string;
|
|
57
50
|
}
|
|
58
51
|
|
|
59
|
-
interface NodeResponse {
|
|
60
|
-
id?: string;
|
|
61
|
-
node?: { id?: string };
|
|
62
|
-
}
|
|
63
|
-
|
|
64
52
|
export interface CanvasAccess {
|
|
65
53
|
readonly port: number;
|
|
66
54
|
readonly remoteBaseUrl: string | null;
|
|
@@ -68,16 +56,16 @@ export interface CanvasAccess {
|
|
|
68
56
|
invoker(): OperationInvoker;
|
|
69
57
|
getLayout(): Promise<CanvasLayout>;
|
|
70
58
|
getNode(id: string): Promise<CanvasNodeState | undefined>;
|
|
71
|
-
refreshWebpageNode(id: string, url?: string): Promise<RefreshWebpageNodeResult>;
|
|
72
|
-
addHtmlNode(input: AddHtmlNodeInput): Promise<string>;
|
|
73
|
-
addHtmlPrimitive(input: AddHtmlPrimitiveInput): Promise<AddHtmlPrimitiveResult>;
|
|
74
59
|
getAxState(): Promise<AxStateResult>;
|
|
75
60
|
getAxContext(options?: { consumer?: string }): Promise<AxContextResult>;
|
|
76
61
|
getAxTimeline(query?: GetAxTimelineQuery): Promise<GetAxTimelineResult>;
|
|
77
62
|
listWorkItems(): Promise<ListWorkItemsResult>;
|
|
78
63
|
listApprovalGates(): Promise<ListApprovalGatesResult>;
|
|
79
64
|
listReviewAnnotations(): Promise<ListReviewAnnotationsResult>;
|
|
80
|
-
submitAxInteraction(
|
|
65
|
+
submitAxInteraction(
|
|
66
|
+
input: SubmitAxInteractionInput,
|
|
67
|
+
options?: { source?: PmxAxSource },
|
|
68
|
+
): Promise<SubmitAxInteractionResult>;
|
|
81
69
|
getPendingSteering(options?: { consumer?: string; limit?: number }): Promise<GetPendingSteeringResult>;
|
|
82
70
|
listElicitations(): Promise<ListElicitationsResult>;
|
|
83
71
|
listModeRequests(): Promise<ListModeRequestsResult>;
|
|
@@ -119,20 +107,6 @@ class LocalCanvasAccess implements CanvasAccess {
|
|
|
119
107
|
return this.canvas.getNode(id);
|
|
120
108
|
}
|
|
121
109
|
|
|
122
|
-
async refreshWebpageNode(id: string, url?: string): Promise<RefreshWebpageNodeResult> {
|
|
123
|
-
return await this.canvas.refreshWebpageNode(id, url);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
async addHtmlNode(input: AddHtmlNodeInput): Promise<string> {
|
|
127
|
-
// PmxCanvas.addHtmlNode returns the created node; the CanvasAccess contract
|
|
128
|
-
// is a bare id string, so extract it (mirrors addNode above).
|
|
129
|
-
return this.canvas.addHtmlNode(input).id;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
async addHtmlPrimitive(input: AddHtmlPrimitiveInput): Promise<AddHtmlPrimitiveResult> {
|
|
133
|
-
return this.canvas.addHtmlPrimitive(input);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
110
|
async getAxState(): Promise<AxStateResult> {
|
|
137
111
|
return this.canvas.getAxState();
|
|
138
112
|
}
|
|
@@ -145,7 +119,10 @@ class LocalCanvasAccess implements CanvasAccess {
|
|
|
145
119
|
return this.canvas.getAxTimeline(query);
|
|
146
120
|
}
|
|
147
121
|
|
|
148
|
-
async submitAxInteraction(
|
|
122
|
+
async submitAxInteraction(
|
|
123
|
+
input: SubmitAxInteractionInput,
|
|
124
|
+
options?: { source?: PmxAxSource },
|
|
125
|
+
): Promise<SubmitAxInteractionResult> {
|
|
149
126
|
return this.canvas.submitAxInteraction(input, { source: options?.source ?? 'mcp' });
|
|
150
127
|
}
|
|
151
128
|
|
|
@@ -238,9 +215,10 @@ class RemoteCanvasAccess implements CanvasAccess {
|
|
|
238
215
|
}
|
|
239
216
|
}
|
|
240
217
|
if (!response.ok) {
|
|
241
|
-
const error =
|
|
242
|
-
|
|
243
|
-
|
|
218
|
+
const error =
|
|
219
|
+
parsed && typeof parsed === 'object' && 'error' in parsed
|
|
220
|
+
? String((parsed as { error?: unknown }).error)
|
|
221
|
+
: `HTTP ${response.status}`;
|
|
244
222
|
if (path === '/api/canvas/batch' && parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
245
223
|
return parsed as T;
|
|
246
224
|
}
|
|
@@ -249,17 +227,6 @@ class RemoteCanvasAccess implements CanvasAccess {
|
|
|
249
227
|
return parsed as T;
|
|
250
228
|
}
|
|
251
229
|
|
|
252
|
-
private async requestNodeId(method: string, path: string, body?: unknown): Promise<string> {
|
|
253
|
-
const response = await this.requestJson<NodeResponse>(method, path, body);
|
|
254
|
-
const id = typeof response.id === 'string'
|
|
255
|
-
? response.id
|
|
256
|
-
: typeof response.node?.id === 'string'
|
|
257
|
-
? response.node.id
|
|
258
|
-
: '';
|
|
259
|
-
if (!id) throw new Error('Canvas response did not include a node id.');
|
|
260
|
-
return id;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
230
|
async getLayout(): Promise<CanvasLayout> {
|
|
264
231
|
return await this.requestJson<CanvasLayout>('GET', '/api/canvas/state?includeBlobs=true');
|
|
265
232
|
}
|
|
@@ -268,7 +235,7 @@ class RemoteCanvasAccess implements CanvasAccess {
|
|
|
268
235
|
const response = await fetch(`${this.remoteBaseUrl}/api/canvas/node/${encodeURIComponent(id)}?includeBlobs=true`);
|
|
269
236
|
if (response.status === 404) return undefined;
|
|
270
237
|
const text = await response.text();
|
|
271
|
-
let parsed: unknown
|
|
238
|
+
let parsed: unknown;
|
|
272
239
|
if (text.length > 0) {
|
|
273
240
|
try {
|
|
274
241
|
parsed = JSON.parse(text) as unknown;
|
|
@@ -277,73 +244,15 @@ class RemoteCanvasAccess implements CanvasAccess {
|
|
|
277
244
|
}
|
|
278
245
|
}
|
|
279
246
|
if (!response.ok) {
|
|
280
|
-
const error =
|
|
281
|
-
|
|
282
|
-
|
|
247
|
+
const error =
|
|
248
|
+
parsed && typeof parsed === 'object' && 'error' in parsed
|
|
249
|
+
? String((parsed as { error?: unknown }).error)
|
|
250
|
+
: `HTTP ${response.status}`;
|
|
283
251
|
throw new Error(error);
|
|
284
252
|
}
|
|
285
253
|
return parsed as CanvasNodeState;
|
|
286
254
|
}
|
|
287
255
|
|
|
288
|
-
async refreshWebpageNode(id: string, url?: string): Promise<RefreshWebpageNodeResult> {
|
|
289
|
-
return await this.requestJson<RefreshWebpageNodeResult>('POST', `/api/canvas/node/${encodeURIComponent(id)}/refresh`, {
|
|
290
|
-
...(url ? { url } : {}),
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
async addHtmlNode(input: AddHtmlNodeInput): Promise<string> {
|
|
295
|
-
const {
|
|
296
|
-
summary,
|
|
297
|
-
agentSummary,
|
|
298
|
-
description,
|
|
299
|
-
presentation,
|
|
300
|
-
slideTitles,
|
|
301
|
-
embeddedNodeIds,
|
|
302
|
-
embeddedUrls,
|
|
303
|
-
axCapabilities,
|
|
304
|
-
...rest
|
|
305
|
-
} = input as AddHtmlNodeInput & {
|
|
306
|
-
summary?: string;
|
|
307
|
-
agentSummary?: string;
|
|
308
|
-
description?: string;
|
|
309
|
-
presentation?: boolean;
|
|
310
|
-
slideTitles?: string[];
|
|
311
|
-
embeddedNodeIds?: string[];
|
|
312
|
-
embeddedUrls?: string[];
|
|
313
|
-
axCapabilities?: { enabled?: boolean; allowed?: string[] };
|
|
314
|
-
};
|
|
315
|
-
return await this.requestNodeId('POST', '/api/canvas/node', {
|
|
316
|
-
type: 'html',
|
|
317
|
-
...rest,
|
|
318
|
-
data: {
|
|
319
|
-
...(typeof summary === 'string' ? { summary } : {}),
|
|
320
|
-
...(typeof agentSummary === 'string' ? { agentSummary } : {}),
|
|
321
|
-
...(typeof description === 'string' ? { description } : {}),
|
|
322
|
-
...(presentation === true ? { presentation: true } : {}),
|
|
323
|
-
...(Array.isArray(slideTitles) ? { slideTitles } : {}),
|
|
324
|
-
...(Array.isArray(embeddedNodeIds) ? { embeddedNodeIds } : {}),
|
|
325
|
-
...(Array.isArray(embeddedUrls) ? { embeddedUrls } : {}),
|
|
326
|
-
...(axCapabilities ? { axCapabilities } : {}),
|
|
327
|
-
},
|
|
328
|
-
});
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
async addHtmlPrimitive(input: AddHtmlPrimitiveInput): Promise<AddHtmlPrimitiveResult> {
|
|
332
|
-
const response = await this.requestJson<{
|
|
333
|
-
id?: string;
|
|
334
|
-
node?: { id?: string };
|
|
335
|
-
primitive?: { kind?: string; title?: string; htmlBytes?: number };
|
|
336
|
-
}>('POST', '/api/canvas/node', { type: 'html', ...input, primitive: input.kind });
|
|
337
|
-
const id = typeof response.id === 'string' ? response.id : response.node?.id;
|
|
338
|
-
if (!id) throw new Error('html primitive response did not include a node id.');
|
|
339
|
-
return {
|
|
340
|
-
id,
|
|
341
|
-
kind: input.kind,
|
|
342
|
-
title: response.primitive?.title ?? input.title ?? input.kind,
|
|
343
|
-
htmlBytes: response.primitive?.htmlBytes ?? 0,
|
|
344
|
-
};
|
|
345
|
-
}
|
|
346
|
-
|
|
347
256
|
async getHistory(): Promise<HistoryResult> {
|
|
348
257
|
return await this.requestJson<HistoryResult>('GET', '/api/canvas/history');
|
|
349
258
|
}
|
|
@@ -364,7 +273,10 @@ class RemoteCanvasAccess implements CanvasAccess {
|
|
|
364
273
|
return await this.requestJson<GetAxTimelineResult>('GET', `/api/canvas/ax/timeline${qs}`);
|
|
365
274
|
}
|
|
366
275
|
|
|
367
|
-
async submitAxInteraction(
|
|
276
|
+
async submitAxInteraction(
|
|
277
|
+
input: SubmitAxInteractionInput,
|
|
278
|
+
options?: { source?: PmxAxSource },
|
|
279
|
+
): Promise<SubmitAxInteractionResult> {
|
|
368
280
|
// The interaction endpoint returns its structured outcome (ok/code/error) in
|
|
369
281
|
// the body for both accepted and rejected interactions, so read the body
|
|
370
282
|
// regardless of HTTP status rather than throwing on a denial.
|
|
@@ -419,12 +331,18 @@ class RemoteCanvasAccess implements CanvasAccess {
|
|
|
419
331
|
}
|
|
420
332
|
|
|
421
333
|
async listApprovalGates(): Promise<ListApprovalGatesResult> {
|
|
422
|
-
const response = await this.requestJson<{ approvalGates?: ListApprovalGatesResult }>(
|
|
334
|
+
const response = await this.requestJson<{ approvalGates?: ListApprovalGatesResult }>(
|
|
335
|
+
'GET',
|
|
336
|
+
'/api/canvas/ax/approval',
|
|
337
|
+
);
|
|
423
338
|
return response.approvalGates ?? [];
|
|
424
339
|
}
|
|
425
340
|
|
|
426
341
|
async listReviewAnnotations(): Promise<ListReviewAnnotationsResult> {
|
|
427
|
-
const response = await this.requestJson<{ reviewAnnotations?: ListReviewAnnotationsResult }>(
|
|
342
|
+
const response = await this.requestJson<{ reviewAnnotations?: ListReviewAnnotationsResult }>(
|
|
343
|
+
'GET',
|
|
344
|
+
'/api/canvas/ax/review',
|
|
345
|
+
);
|
|
428
346
|
return response.reviewAnnotations ?? [];
|
|
429
347
|
}
|
|
430
348
|
|
|
@@ -495,7 +413,7 @@ async function readHealth(baseUrl: string): Promise<HealthResponse | null> {
|
|
|
495
413
|
try {
|
|
496
414
|
const response = await fetch(`${baseUrl}/health`, { signal: AbortSignal.timeout(400) });
|
|
497
415
|
if (!response.ok) return null;
|
|
498
|
-
return await response.json() as HealthResponse;
|
|
416
|
+
return (await response.json()) as HealthResponse;
|
|
499
417
|
} catch {
|
|
500
418
|
return null;
|
|
501
419
|
}
|
|
@@ -539,7 +457,9 @@ export function shouldAttachToExistingDaemon(
|
|
|
539
457
|
occupant: { ok?: boolean; workspace?: unknown } | null,
|
|
540
458
|
allowSplit: boolean,
|
|
541
459
|
): boolean {
|
|
542
|
-
return
|
|
460
|
+
return (
|
|
461
|
+
!allowSplit && occupant?.ok === true && typeof occupant.workspace === 'string' && occupant.workspace.length > 0
|
|
462
|
+
);
|
|
543
463
|
}
|
|
544
464
|
|
|
545
465
|
/**
|
|
@@ -645,8 +565,7 @@ export async function createCanvasAccess(): Promise<CanvasAccess> {
|
|
|
645
565
|
const boundPort = canvas.port;
|
|
646
566
|
if (boundPort !== port) {
|
|
647
567
|
const occupant = await readHealth(occupantBaseUrl);
|
|
648
|
-
const occupantWorkspace =
|
|
649
|
-
typeof occupant?.workspace === 'string' ? ` (serving ${occupant.workspace})` : '';
|
|
568
|
+
const occupantWorkspace = typeof occupant?.workspace === 'string' ? ` (serving ${occupant.workspace})` : '';
|
|
650
569
|
process.stderr.write(
|
|
651
570
|
`[pmx-canvas] preferred port ${port} was in use${occupantWorkspace}; ` +
|
|
652
571
|
`started this canvas on port ${boundPort} instead. To share one canvas, run the daemon ` +
|