pmx-canvas 0.1.3 → 0.1.5
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 +138 -0
- package/dist/canvas/index.js +25 -25
- package/dist/json-render/index.css +1 -1
- package/dist/json-render/index.js +1 -1
- package/dist/types/json-render/charts/components.d.ts +2 -1
- package/dist/types/server/canvas-serialization.d.ts +1 -0
- package/dist/types/server/ext-app-lookup.d.ts +22 -0
- package/dist/types/server/image-source.d.ts +3 -0
- package/dist/types/server/index.d.ts +2 -0
- package/package.json +1 -1
- package/skills/pmx-canvas/SKILL.md +9 -5
- package/src/cli/agent.ts +78 -21
- package/src/client/state/sse-bridge.ts +14 -4
- package/src/json-render/charts/components.tsx +6 -4
- package/src/json-render/charts/extra-components.tsx +5 -5
- package/src/json-render/renderer/index.css +14 -0
- package/src/mcp/server.ts +16 -0
- package/src/server/canvas-operations.ts +11 -11
- package/src/server/canvas-schema.ts +15 -14
- package/src/server/canvas-serialization.ts +19 -1
- package/src/server/ext-app-lookup.ts +49 -0
- package/src/server/image-source.ts +206 -0
- package/src/server/index.ts +11 -15
- package/src/server/server.ts +65 -25
- package/src/server/web-artifacts.ts +1 -0
|
@@ -39,9 +39,10 @@ export declare const tooltipStyle: {
|
|
|
39
39
|
fontSize: number;
|
|
40
40
|
};
|
|
41
41
|
/** Shared wrapper for cartesian charts (Line + Bar). */
|
|
42
|
-
export declare function CartesianChart({ props, children, }: {
|
|
42
|
+
export declare function CartesianChart({ props, children, className, }: {
|
|
43
43
|
props: CartesianChartProps;
|
|
44
44
|
children: (data: Record<string, unknown>[]) => ReactNode;
|
|
45
|
+
className?: string;
|
|
45
46
|
}): import("react/jsx-runtime").JSX.Element;
|
|
46
47
|
declare function ChartLineChart({ props }: BaseComponentProps<CartesianChartProps>): import("react/jsx-runtime").JSX.Element;
|
|
47
48
|
declare function ChartBarChart({ props }: BaseComponentProps<CartesianChartProps>): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { CanvasLayout, CanvasNodeState, ViewportState } from './canvas-state.js';
|
|
2
2
|
import { type CanvasNodeProvenance } from './canvas-provenance.js';
|
|
3
3
|
export interface SerializedCanvasNode extends CanvasNodeState {
|
|
4
|
+
kind: string;
|
|
4
5
|
title: string | null;
|
|
5
6
|
content: string | null;
|
|
6
7
|
path: string | null;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the canvas node ID for a given ext-app `toolCallId`.
|
|
3
|
+
*
|
|
4
|
+
* v0.1.4 fixed a long-standing `ext-app-ext-app-…` double-prefix bug where
|
|
5
|
+
* both `nodeId` and `toolCallId` carried the `ext-app-` prefix. This helper
|
|
6
|
+
* encodes the lookup contract so it doesn't drift between the
|
|
7
|
+
* `PmxCanvas` SDK class and the HTTP server.
|
|
8
|
+
*
|
|
9
|
+
* Resolution order:
|
|
10
|
+
* 1. The direct prefixed form (`ext-app-<toolCallId>` if not already
|
|
11
|
+
* prefixed, otherwise `toolCallId` as-is).
|
|
12
|
+
* 2. The legacy `ext-app-ext-app-…` form, for canvases persisted before
|
|
13
|
+
* v0.1.4 and still on disk. Remove this fallback in v0.2.x.
|
|
14
|
+
* 3. A scan of the layout for any `mcp-app` ext-app node carrying that
|
|
15
|
+
* `toolCallId` in its data.
|
|
16
|
+
*/
|
|
17
|
+
import type { CanvasNodeState } from './canvas-state.js';
|
|
18
|
+
export interface ExtAppLookupSource {
|
|
19
|
+
getNode(id: string): CanvasNodeState | undefined;
|
|
20
|
+
listNodes(): readonly CanvasNodeState[];
|
|
21
|
+
}
|
|
22
|
+
export declare function findCanvasExtAppNodeId(toolCallId: string, source: ExtAppLookupSource): string | null;
|
|
@@ -195,6 +195,7 @@ export declare class PmxCanvas extends EventEmitter {
|
|
|
195
195
|
height?: number;
|
|
196
196
|
}): Promise<{
|
|
197
197
|
ok: true;
|
|
198
|
+
id?: string;
|
|
198
199
|
nodeId: string | null;
|
|
199
200
|
toolCallId: string;
|
|
200
201
|
sessionId: string;
|
|
@@ -202,6 +203,7 @@ export declare class PmxCanvas extends EventEmitter {
|
|
|
202
203
|
}>;
|
|
203
204
|
addDiagram(input: DiagramPresetOpenInput): Promise<{
|
|
204
205
|
ok: true;
|
|
206
|
+
id?: string;
|
|
205
207
|
nodeId: string | null;
|
|
206
208
|
toolCallId: string;
|
|
207
209
|
sessionId: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmx-canvas",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Spatial canvas workbench for coding agents — infinite 2D canvas with agent-native CLI, MCP integration, nodes, edges, file watching, and snapshots",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/server/index.ts",
|
|
@@ -84,8 +84,8 @@ pmx-canvas layout # Full canvas state
|
|
|
84
84
|
pmx-canvas status # Quick summary
|
|
85
85
|
pmx-canvas node add --type markdown --title "Plan"
|
|
86
86
|
pmx-canvas node add --type webpage --url https://example.com/docs
|
|
87
|
-
pmx-canvas node add --type web-artifact --title "Dashboard" --app-file ./App.tsx
|
|
88
87
|
pmx-canvas node add --type graph --graph-type bar --data '[{"x":"a","y":1}]' --x-key x --y-key y
|
|
88
|
+
pmx-canvas node add --type graph --graphType bar --data '[{"x":"a","y":1}]' --xKey x --yKey y
|
|
89
89
|
pmx-canvas external-app add --kind excalidraw --title "Diagram"
|
|
90
90
|
pmx-canvas node add --help --type webpage --json
|
|
91
91
|
pmx-canvas node schema --type json-render --component Table --summary
|
|
@@ -97,6 +97,8 @@ pmx-canvas arrange --layout flow
|
|
|
97
97
|
pmx-canvas focus <node-id> --no-pan # Select/raise without moving the user's viewport
|
|
98
98
|
pmx-canvas validate spec --type json-render --spec-file ./dashboard.json --summary
|
|
99
99
|
pmx-canvas web-artifact build --title "Dashboard" --app-file ./App.tsx --deps recharts --include-logs
|
|
100
|
+
pmx-canvas node list --type web-artifact --summary
|
|
101
|
+
pmx-canvas node list --type external-app --summary
|
|
100
102
|
pmx-canvas pin --list
|
|
101
103
|
pmx-canvas snapshot save --name "before-refactor"
|
|
102
104
|
pmx-canvas code-graph
|
|
@@ -107,6 +109,7 @@ pmx-canvas spatial
|
|
|
107
109
|
|
|
108
110
|
- `node add|list|get|update|remove` — manage nodes
|
|
109
111
|
- `node schema` — inspect running-server create schemas and canonical examples, with `--summary`, `--field`, and `--component` filters
|
|
112
|
+
- Graph CLI fields accept both kebab-case flags and camelCase schema names, e.g. `--graph-type`/`--graphType`, `--x-key`/`--xKey`, and `--bar-color`/`--barColor`.
|
|
110
113
|
- `edge add|list|remove` — manage edges
|
|
111
114
|
- Search-based edge selectors must be specific enough to resolve exactly one node. Queries like
|
|
112
115
|
`"DVT O3"` can be ambiguous; prefer the full visible title such as `"DVT O3 — GitOps"`.
|
|
@@ -119,6 +122,8 @@ pmx-canvas spatial
|
|
|
119
122
|
- `group create|add|remove` — manage groups
|
|
120
123
|
- `clear --yes` — destructive clear with explicit confirmation
|
|
121
124
|
- `validate spec` — validate json-render specs and graph payloads without creating nodes
|
|
125
|
+
- `web-artifact build` — build bundled React/Tailwind HTML artifacts; use `--deps` for extra packages and `--include-logs` only when raw logs are useful
|
|
126
|
+
- `external-app add --kind excalidraw` — create the hosted Excalidraw preset; response includes `id` and `nodeId` aliases for the same canvas node
|
|
122
127
|
- `serve status|stop` — inspect and stop daemonized servers started with `serve --daemon`
|
|
123
128
|
- `code-graph`, `spatial` — analysis commands
|
|
124
129
|
|
|
@@ -128,10 +133,9 @@ Current caveat:
|
|
|
128
133
|
through search later in the session. When you plan to curate an app-heavy comparison area,
|
|
129
134
|
capture node IDs immediately after creation and verify membership with `node get --summary`,
|
|
130
135
|
`layout --summary`, or the browser selection state instead of relying on search alone.
|
|
131
|
-
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
revisit it later.
|
|
136
|
+
- App-like nodes persist as `type: "mcp-app"` internally but serialized results include `kind`:
|
|
137
|
+
`web-artifact`, `external-app`, or `mcp-app`. Prefer `node list --type web-artifact` or
|
|
138
|
+
`node list --type external-app` when you need the operational subtype.
|
|
135
139
|
- Generic `pmx-canvas node add --type mcp-app` is intentionally not supported because app nodes
|
|
136
140
|
need app/session metadata. Use `pmx-canvas web-artifact build` for bundled React artifacts or
|
|
137
141
|
`pmx-canvas external-app add --kind excalidraw` for the Excalidraw preset.
|
package/src/cli/agent.ts
CHANGED
|
@@ -237,8 +237,9 @@ function parseStringListFlag(
|
|
|
237
237
|
flags: Record<string, string | true>,
|
|
238
238
|
name: string,
|
|
239
239
|
hint: string,
|
|
240
|
+
...aliases: string[]
|
|
240
241
|
): string[] | undefined {
|
|
241
|
-
const raw = getStringFlag(flags, name);
|
|
242
|
+
const raw = getStringFlag(flags, name, ...aliases);
|
|
242
243
|
if (raw === undefined) return undefined;
|
|
243
244
|
const trimmed = raw.trim();
|
|
244
245
|
if (!trimmed) {
|
|
@@ -295,6 +296,7 @@ function summarizeNodeResult(node: Record<string, unknown>): Record<string, unkn
|
|
|
295
296
|
...(node.ok !== undefined ? { ok: node.ok } : {}),
|
|
296
297
|
id: node.id ?? null,
|
|
297
298
|
type: node.type ?? null,
|
|
299
|
+
...(typeof node.kind === 'string' ? { kind: node.kind } : {}),
|
|
298
300
|
title: node.title ?? null,
|
|
299
301
|
...(typeof node.content === 'string' ? { contentPreview: truncateText(node.content) } : {}),
|
|
300
302
|
...(node.position !== undefined ? { position: node.position } : {}),
|
|
@@ -550,28 +552,39 @@ async function buildGraphRequestBody(
|
|
|
550
552
|
const data = parseRecordArrayJson(rawData, hint);
|
|
551
553
|
|
|
552
554
|
const body: Record<string, unknown> = {
|
|
553
|
-
graphType: getStringFlag(flags, 'graph-type') ?? 'line',
|
|
555
|
+
graphType: getStringFlag(flags, 'graph-type', 'graphType') ?? 'line',
|
|
554
556
|
data,
|
|
555
557
|
};
|
|
556
558
|
if (typeof flags.title === 'string') body.title = flags.title;
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
559
|
+
const xKey = getStringFlag(flags, 'x-key', 'xKey');
|
|
560
|
+
const yKey = getStringFlag(flags, 'y-key', 'yKey');
|
|
561
|
+
const zKey = getStringFlag(flags, 'z-key', 'zKey');
|
|
562
|
+
const nameKey = getStringFlag(flags, 'name-key', 'nameKey');
|
|
563
|
+
const valueKey = getStringFlag(flags, 'value-key', 'valueKey');
|
|
564
|
+
const axisKey = getStringFlag(flags, 'axis-key', 'axisKey');
|
|
565
|
+
if (xKey) body.xKey = xKey;
|
|
566
|
+
if (yKey) body.yKey = yKey;
|
|
567
|
+
if (zKey) body.zKey = zKey;
|
|
568
|
+
if (nameKey) body.nameKey = nameKey;
|
|
569
|
+
if (valueKey) body.valueKey = valueKey;
|
|
570
|
+
if (axisKey) body.axisKey = axisKey;
|
|
563
571
|
const metrics = parseStringListFlag(flags, 'metrics', 'Use a comma-separated list, e.g. --metrics north,south');
|
|
564
572
|
const series = parseStringListFlag(flags, 'series', 'Use a comma-separated list, e.g. --series north,south');
|
|
565
573
|
if (metrics) body.metrics = metrics;
|
|
566
574
|
if (series) body.series = series;
|
|
567
|
-
|
|
568
|
-
|
|
575
|
+
const barKey = getStringFlag(flags, 'bar-key', 'barKey');
|
|
576
|
+
const lineKey = getStringFlag(flags, 'line-key', 'lineKey');
|
|
577
|
+
if (barKey) body.barKey = barKey;
|
|
578
|
+
if (lineKey) body.lineKey = lineKey;
|
|
569
579
|
if (flags.aggregate === 'sum' || flags.aggregate === 'count' || flags.aggregate === 'avg') {
|
|
570
580
|
body.aggregate = flags.aggregate;
|
|
571
581
|
}
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
582
|
+
const color = getStringFlag(flags, 'color');
|
|
583
|
+
const barColor = getStringFlag(flags, 'bar-color', 'barColor');
|
|
584
|
+
const lineColor = getStringFlag(flags, 'line-color', 'lineColor');
|
|
585
|
+
if (color) body.color = color;
|
|
586
|
+
if (barColor) body.barColor = barColor;
|
|
587
|
+
if (lineColor) body.lineColor = lineColor;
|
|
575
588
|
|
|
576
589
|
const chartHeight = optionalPositiveFiniteFlag(flags, 'chart-height', 'Use a positive number, e.g. --chart-height 300');
|
|
577
590
|
const x = optionalFiniteFlag(flags, 'x', 'Use a finite number, e.g. --x 500');
|
|
@@ -860,6 +873,21 @@ function filterJsonRenderSchemaView(
|
|
|
860
873
|
// ── Commands ─────────────────────────────────────────────────
|
|
861
874
|
|
|
862
875
|
const COMMANDS: Record<string, { run: (args: string[]) => Promise<void>; help: string; examples: string[] }> = {};
|
|
876
|
+
const RESOURCE_COMMAND_ALIASES: Record<string, Record<string, string>> = {
|
|
877
|
+
node: {
|
|
878
|
+
delete: 'remove',
|
|
879
|
+
rm: 'remove',
|
|
880
|
+
},
|
|
881
|
+
edge: {
|
|
882
|
+
delete: 'remove',
|
|
883
|
+
rm: 'remove',
|
|
884
|
+
},
|
|
885
|
+
};
|
|
886
|
+
const RESOURCE_SUBCOMMAND_HINTS: Record<string, Record<string, string>> = {
|
|
887
|
+
node: {
|
|
888
|
+
pin: 'Use the top-level pin command instead: pmx-canvas pin <node-id>',
|
|
889
|
+
},
|
|
890
|
+
};
|
|
863
891
|
|
|
864
892
|
function cmd(
|
|
865
893
|
name: string,
|
|
@@ -1035,7 +1063,7 @@ cmd('node list', 'List all nodes on the canvas', [
|
|
|
1035
1063
|
let nodes = layout.nodes;
|
|
1036
1064
|
|
|
1037
1065
|
if (flags.type && flags.type !== true) {
|
|
1038
|
-
nodes = nodes.filter((n) => n.type === flags.type);
|
|
1066
|
+
nodes = nodes.filter((n) => n.type === flags.type || n.kind === flags.type);
|
|
1039
1067
|
}
|
|
1040
1068
|
|
|
1041
1069
|
if (flags.ids) {
|
|
@@ -1291,7 +1319,9 @@ cmd('status', 'Quick canvas summary', [
|
|
|
1291
1319
|
const typeCounts: Record<string, number> = {};
|
|
1292
1320
|
for (const n of layout.nodes) {
|
|
1293
1321
|
const data = isRecord(n.data) ? n.data : {};
|
|
1294
|
-
const t =
|
|
1322
|
+
const t = typeof n.kind === 'string'
|
|
1323
|
+
? n.kind
|
|
1324
|
+
: n.type === 'mcp-app' && data.hostMode === 'hosted' && typeof data.path === 'string'
|
|
1295
1325
|
? 'web-artifact'
|
|
1296
1326
|
: n.type as string;
|
|
1297
1327
|
typeCounts[t] = (typeCounts[t] || 0) + 1;
|
|
@@ -1364,7 +1394,9 @@ cmd('external-app add', 'Create a hosted external app node', [
|
|
|
1364
1394
|
});
|
|
1365
1395
|
|
|
1366
1396
|
const result = await api('POST', '/api/canvas/diagram', body);
|
|
1367
|
-
output(result)
|
|
1397
|
+
output(result && typeof result === 'object' && !Array.isArray(result) && 'nodeId' in result && !('id' in result)
|
|
1398
|
+
? { id: (result as { nodeId?: unknown }).nodeId, ...result }
|
|
1399
|
+
: result);
|
|
1368
1400
|
});
|
|
1369
1401
|
|
|
1370
1402
|
// ── pin ──────────────────────────────────────────────────────
|
|
@@ -2021,8 +2053,13 @@ function showCommandHelp(name: string): void {
|
|
|
2021
2053
|
console.log('\nSchema help:');
|
|
2022
2054
|
console.log(' pmx-canvas node add --help --type webpage');
|
|
2023
2055
|
console.log(' pmx-canvas node add --help --type json-render --component Table');
|
|
2056
|
+
console.log(' pmx-canvas node add --help --type graph');
|
|
2024
2057
|
console.log(' pmx-canvas node add --help --type webpage --json');
|
|
2025
2058
|
}
|
|
2059
|
+
if (name === 'node add' || name === 'validate spec') {
|
|
2060
|
+
console.log('\nGraph flags:');
|
|
2061
|
+
console.log(' Graph fields accept kebab-case CLI flags and camelCase schema names, e.g. --graph-type/--graphType and --x-key/--xKey');
|
|
2062
|
+
}
|
|
2026
2063
|
if (name === 'node schema') {
|
|
2027
2064
|
console.log('\nFilters:');
|
|
2028
2065
|
console.log(' --summary Show compact schema summaries');
|
|
@@ -2203,12 +2240,32 @@ export async function runAgentCli(args: string[]): Promise<void> {
|
|
|
2203
2240
|
// Unknown command — show help for the resource if it exists
|
|
2204
2241
|
const resourceCommands = Object.keys(COMMANDS).filter((k) => k.startsWith(oneWord + ' '));
|
|
2205
2242
|
if (resourceCommands.length > 0) {
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2243
|
+
if (args[1] === '--help' || args[1] === '-h') {
|
|
2244
|
+
console.log(`\nAvailable "${oneWord}" commands:\n`);
|
|
2245
|
+
for (const k of resourceCommands) {
|
|
2246
|
+
console.log(` pmx-canvas ${k.padEnd(20)} ${COMMANDS[k].help}`);
|
|
2247
|
+
}
|
|
2248
|
+
console.log('\nRun any command with --help for details.\n');
|
|
2249
|
+
return;
|
|
2209
2250
|
}
|
|
2210
|
-
|
|
2211
|
-
|
|
2251
|
+
const subcommand = args[1];
|
|
2252
|
+
const suggestion = subcommand ? RESOURCE_COMMAND_ALIASES[oneWord]?.[subcommand] : undefined;
|
|
2253
|
+
const extraHint = subcommand ? RESOURCE_SUBCOMMAND_HINTS[oneWord]?.[subcommand] : undefined;
|
|
2254
|
+
const available = resourceCommands
|
|
2255
|
+
.map((k) => k.slice(oneWord.length + 1))
|
|
2256
|
+
.sort()
|
|
2257
|
+
.join(', ');
|
|
2258
|
+
const hints = [
|
|
2259
|
+
suggestion ? `Did you mean: pmx-canvas ${oneWord} ${suggestion}?` : undefined,
|
|
2260
|
+
extraHint,
|
|
2261
|
+
`Available subcommands: ${available}`,
|
|
2262
|
+
].filter((hint): hint is string => typeof hint === 'string');
|
|
2263
|
+
die(
|
|
2264
|
+
subcommand
|
|
2265
|
+
? `Unknown ${oneWord} subcommand: "${subcommand}".`
|
|
2266
|
+
: `Missing ${oneWord} subcommand.`,
|
|
2267
|
+
hints.join(' '),
|
|
2268
|
+
);
|
|
2212
2269
|
}
|
|
2213
2270
|
|
|
2214
2271
|
die(`Unknown command: ${oneWord}`, 'Run: pmx-canvas --help');
|
|
@@ -166,7 +166,8 @@ function ensureMcpAppNode(data: Record<string, unknown>): void {
|
|
|
166
166
|
|
|
167
167
|
function ensureExtAppNode(data: Record<string, unknown>): void {
|
|
168
168
|
const toolCallId = data.toolCallId as string;
|
|
169
|
-
const
|
|
169
|
+
const eventNodeId = typeof data.nodeId === 'string' && data.nodeId.length > 0 ? data.nodeId : null;
|
|
170
|
+
const id = eventNodeId ?? (toolCallId.startsWith('ext-app-') ? toolCallId : `ext-app-${toolCallId}`);
|
|
170
171
|
const existing = nodes.value.get(id);
|
|
171
172
|
if (existing) {
|
|
172
173
|
updateNodeData(id, data);
|
|
@@ -232,8 +233,10 @@ function ensureExtAppNode(data: Record<string, unknown>): void {
|
|
|
232
233
|
}
|
|
233
234
|
|
|
234
235
|
function findExtAppNodeId(toolCallId: string): string | null {
|
|
235
|
-
const directId = `ext-app-${toolCallId}`;
|
|
236
|
+
const directId = toolCallId.startsWith('ext-app-') ? toolCallId : `ext-app-${toolCallId}`;
|
|
236
237
|
if (nodes.value.has(directId)) return directId;
|
|
238
|
+
const legacyDirectId = `ext-app-${toolCallId}`;
|
|
239
|
+
if (legacyDirectId !== directId && nodes.value.has(legacyDirectId)) return legacyDirectId;
|
|
237
240
|
for (const [nodeId, node] of nodes.value.entries()) {
|
|
238
241
|
if (
|
|
239
242
|
node.type === 'mcp-app' &&
|
|
@@ -246,6 +249,13 @@ function findExtAppNodeId(toolCallId: string): string | null {
|
|
|
246
249
|
return null;
|
|
247
250
|
}
|
|
248
251
|
|
|
252
|
+
function findExtAppEventNodeId(data: Record<string, unknown>): string | null {
|
|
253
|
+
const eventNodeId = typeof data.nodeId === 'string' && data.nodeId.length > 0 ? data.nodeId : null;
|
|
254
|
+
if (eventNodeId && nodes.value.has(eventNodeId)) return eventNodeId;
|
|
255
|
+
if (typeof data.toolCallId !== 'string' || !data.toolCallId) return null;
|
|
256
|
+
return findExtAppNodeId(data.toolCallId);
|
|
257
|
+
}
|
|
258
|
+
|
|
249
259
|
function findOnlyPendingExtAppNodeId(serverName: unknown, toolName: unknown): string | null {
|
|
250
260
|
if (typeof serverName !== 'string' || !serverName) return null;
|
|
251
261
|
if (typeof toolName !== 'string' || !toolName) return null;
|
|
@@ -542,7 +552,7 @@ function handleExtAppOpen(data: Record<string, unknown>): void {
|
|
|
542
552
|
function handleExtAppUpdate(data: Record<string, unknown>): void {
|
|
543
553
|
if (typeof data.toolCallId !== 'string' || !data.toolCallId) return;
|
|
544
554
|
const id =
|
|
545
|
-
|
|
555
|
+
findExtAppEventNodeId(data) ?? findOnlyPendingExtAppNodeId(data.serverName, data.toolName);
|
|
546
556
|
if (!id) return;
|
|
547
557
|
if (nodes.value.has(id)) {
|
|
548
558
|
updateNodeData(id, { html: data.html });
|
|
@@ -552,7 +562,7 @@ function handleExtAppUpdate(data: Record<string, unknown>): void {
|
|
|
552
562
|
function handleExtAppResult(data: Record<string, unknown>): void {
|
|
553
563
|
if (typeof data.toolCallId !== 'string' || !data.toolCallId) return;
|
|
554
564
|
const id =
|
|
555
|
-
|
|
565
|
+
findExtAppEventNodeId(data) ?? findOnlyPendingExtAppNodeId(data.serverName, data.toolName);
|
|
556
566
|
if (!id) return;
|
|
557
567
|
if (nodes.value.has(id)) {
|
|
558
568
|
if (data.success === false) {
|
|
@@ -103,15 +103,17 @@ export const tooltipStyle = {
|
|
|
103
103
|
export function CartesianChart({
|
|
104
104
|
props,
|
|
105
105
|
children,
|
|
106
|
+
className,
|
|
106
107
|
}: {
|
|
107
108
|
props: CartesianChartProps;
|
|
108
109
|
children: (data: Record<string, unknown>[]) => ReactNode;
|
|
110
|
+
className?: string;
|
|
109
111
|
}) {
|
|
110
112
|
const chartData = processChartData(props.data ?? [], props.xKey, props.yKey, props.aggregate);
|
|
111
113
|
const h = props.height ?? 300;
|
|
112
114
|
|
|
113
115
|
return (
|
|
114
|
-
<div className=
|
|
116
|
+
<div className={`pmx-chart${className ? ` ${className}` : ''}`}>
|
|
115
117
|
{props.title && <div className="pmx-chart__title">{props.title}</div>}
|
|
116
118
|
<ResponsiveContainer width="100%" height={h}>
|
|
117
119
|
{children(chartData)}
|
|
@@ -123,7 +125,7 @@ export function CartesianChart({
|
|
|
123
125
|
function ChartLineChart({ props }: BaseComponentProps<CartesianChartProps>) {
|
|
124
126
|
const stroke = props.color ?? CHART_COLORS[0];
|
|
125
127
|
return (
|
|
126
|
-
<CartesianChart props={props}>
|
|
128
|
+
<CartesianChart props={props} className="pmx-chart--line">
|
|
127
129
|
{(data) => (
|
|
128
130
|
<RechartsLineChart data={data}>
|
|
129
131
|
<CartesianGrid strokeDasharray="3 3" stroke="var(--border, #e5e5e5)" />
|
|
@@ -147,7 +149,7 @@ function ChartLineChart({ props }: BaseComponentProps<CartesianChartProps>) {
|
|
|
147
149
|
function ChartBarChart({ props }: BaseComponentProps<CartesianChartProps>) {
|
|
148
150
|
const fill = props.color ?? CHART_COLORS[0];
|
|
149
151
|
return (
|
|
150
|
-
<CartesianChart props={props}>
|
|
152
|
+
<CartesianChart props={props} className="pmx-chart--bar">
|
|
151
153
|
{(data) => (
|
|
152
154
|
<RechartsBarChart data={data}>
|
|
153
155
|
<CartesianGrid strokeDasharray="3 3" stroke="var(--border, #e5e5e5)" />
|
|
@@ -166,7 +168,7 @@ function ChartPieChart({ props }: BaseComponentProps<PieChartProps>) {
|
|
|
166
168
|
const h = props.height ?? 300;
|
|
167
169
|
|
|
168
170
|
return (
|
|
169
|
-
<div className="pmx-chart">
|
|
171
|
+
<div className="pmx-chart pmx-chart--pie">
|
|
170
172
|
{props.title && <div className="pmx-chart__title">{props.title}</div>}
|
|
171
173
|
<ResponsiveContainer width="100%" height={h}>
|
|
172
174
|
<RechartsPieChart>
|
|
@@ -45,7 +45,7 @@ function ChartAreaChart({ props }: BaseComponentProps<AreaChartProps>) {
|
|
|
45
45
|
const stroke = props.color ?? CHART_COLORS[0];
|
|
46
46
|
const gradientId = `pmx-area-${props.yKey ?? 'value'}`;
|
|
47
47
|
return (
|
|
48
|
-
<CartesianChart props={props}>
|
|
48
|
+
<CartesianChart props={props} className="pmx-chart--area">
|
|
49
49
|
{(data) => (
|
|
50
50
|
<RechartsAreaChart data={data}>
|
|
51
51
|
<defs>
|
|
@@ -88,7 +88,7 @@ function ChartScatterChart({ props }: BaseComponentProps<ScatterChartProps>) {
|
|
|
88
88
|
const h = props.height ?? 300;
|
|
89
89
|
|
|
90
90
|
return (
|
|
91
|
-
<div className="pmx-chart">
|
|
91
|
+
<div className="pmx-chart pmx-chart--scatter">
|
|
92
92
|
{props.title && <div className="pmx-chart__title">{props.title}</div>}
|
|
93
93
|
<ResponsiveContainer width="100%" height={h}>
|
|
94
94
|
<RechartsScatterChart>
|
|
@@ -118,7 +118,7 @@ function ChartRadarChart({ props }: BaseComponentProps<RadarChartProps>) {
|
|
|
118
118
|
const h = props.height ?? 320;
|
|
119
119
|
|
|
120
120
|
return (
|
|
121
|
-
<div className="pmx-chart">
|
|
121
|
+
<div className="pmx-chart pmx-chart--radar">
|
|
122
122
|
{props.title && <div className="pmx-chart__title">{props.title}</div>}
|
|
123
123
|
<ResponsiveContainer width="100%" height={h}>
|
|
124
124
|
<RechartsRadarChart data={data} outerRadius="75%">
|
|
@@ -163,7 +163,7 @@ function ChartStackedBarChart({ props }: BaseComponentProps<StackedBarChartProps
|
|
|
163
163
|
const h = props.height ?? 300;
|
|
164
164
|
|
|
165
165
|
return (
|
|
166
|
-
<div className="pmx-chart">
|
|
166
|
+
<div className="pmx-chart pmx-chart--stacked-bar">
|
|
167
167
|
{props.title && <div className="pmx-chart__title">{props.title}</div>}
|
|
168
168
|
<ResponsiveContainer width="100%" height={h}>
|
|
169
169
|
<RechartsBarChart data={chartData}>
|
|
@@ -234,7 +234,7 @@ function ChartComposedChart({ props }: BaseComponentProps<ComposedChartProps>) {
|
|
|
234
234
|
const h = props.height ?? 300;
|
|
235
235
|
|
|
236
236
|
return (
|
|
237
|
-
<div className="pmx-chart">
|
|
237
|
+
<div className="pmx-chart pmx-chart--composed">
|
|
238
238
|
{props.title && <div className="pmx-chart__title">{props.title}</div>}
|
|
239
239
|
<ResponsiveContainer width="100%" height={h}>
|
|
240
240
|
<RechartsComposedChart data={data}>
|
|
@@ -147,9 +147,23 @@ button {
|
|
|
147
147
|
|
|
148
148
|
.pmx-chart {
|
|
149
149
|
width: 100%;
|
|
150
|
+
min-width: 280px;
|
|
151
|
+
overflow-x: auto;
|
|
150
152
|
padding: 0.5rem 0;
|
|
151
153
|
}
|
|
152
154
|
|
|
155
|
+
.pmx-chart--line,
|
|
156
|
+
.pmx-chart--area,
|
|
157
|
+
.pmx-chart--scatter,
|
|
158
|
+
.pmx-chart--stacked-bar,
|
|
159
|
+
.pmx-chart--composed {
|
|
160
|
+
min-width: 320px;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.pmx-chart--radar {
|
|
164
|
+
min-width: 360px;
|
|
165
|
+
}
|
|
166
|
+
|
|
153
167
|
.pmx-chart__title {
|
|
154
168
|
font-size: 0.875rem;
|
|
155
169
|
font-weight: 600;
|
package/src/mcp/server.ts
CHANGED
|
@@ -279,10 +279,18 @@ export async function startMcpServer(): Promise<void> {
|
|
|
279
279
|
data: z.array(z.record(z.string(), z.unknown())).optional().describe('Graph dataset when type="graph"'),
|
|
280
280
|
xKey: z.string().optional().describe('X-axis key for line/bar graphs'),
|
|
281
281
|
yKey: z.string().optional().describe('Y-axis key for line/bar graphs'),
|
|
282
|
+
zKey: z.string().optional().describe('Optional bubble-size key for scatter charts'),
|
|
282
283
|
nameKey: z.string().optional().describe('Slice name key for pie graphs'),
|
|
283
284
|
valueKey: z.string().optional().describe('Slice value key for pie graphs'),
|
|
285
|
+
axisKey: z.string().optional().describe('Category key for radar charts'),
|
|
286
|
+
metrics: z.array(z.string()).optional().describe('Series keys to plot as radar polygons'),
|
|
287
|
+
series: z.array(z.string()).optional().describe('Series keys for stacked-bar segments'),
|
|
288
|
+
barKey: z.string().optional().describe('Bar series key for composed charts'),
|
|
289
|
+
lineKey: z.string().optional().describe('Line series key for composed charts'),
|
|
284
290
|
aggregate: z.enum(['sum', 'count', 'avg']).optional().describe('Optional aggregation for repeated keys'),
|
|
285
291
|
color: z.string().optional().describe('Optional graph color'),
|
|
292
|
+
barColor: z.string().optional().describe('Optional bar color for composed charts'),
|
|
293
|
+
lineColor: z.string().optional().describe('Optional line color for composed charts'),
|
|
286
294
|
height: z.number().optional().describe('Optional graph content height'),
|
|
287
295
|
},
|
|
288
296
|
async (input) => {
|
|
@@ -300,10 +308,18 @@ export async function startMcpServer(): Promise<void> {
|
|
|
300
308
|
data: input.data ?? [],
|
|
301
309
|
...(typeof input.xKey === 'string' ? { xKey: input.xKey } : {}),
|
|
302
310
|
...(typeof input.yKey === 'string' ? { yKey: input.yKey } : {}),
|
|
311
|
+
...(typeof input.zKey === 'string' ? { zKey: input.zKey } : {}),
|
|
303
312
|
...(typeof input.nameKey === 'string' ? { nameKey: input.nameKey } : {}),
|
|
304
313
|
...(typeof input.valueKey === 'string' ? { valueKey: input.valueKey } : {}),
|
|
314
|
+
...(typeof input.axisKey === 'string' ? { axisKey: input.axisKey } : {}),
|
|
315
|
+
...(Array.isArray(input.metrics) ? { metrics: input.metrics } : {}),
|
|
316
|
+
...(Array.isArray(input.series) ? { series: input.series } : {}),
|
|
317
|
+
...(typeof input.barKey === 'string' ? { barKey: input.barKey } : {}),
|
|
318
|
+
...(typeof input.lineKey === 'string' ? { lineKey: input.lineKey } : {}),
|
|
305
319
|
...(typeof input.aggregate === 'string' ? { aggregate: input.aggregate } : {}),
|
|
306
320
|
...(typeof input.color === 'string' ? { color: input.color } : {}),
|
|
321
|
+
...(typeof input.barColor === 'string' ? { barColor: input.barColor } : {}),
|
|
322
|
+
...(typeof input.lineColor === 'string' ? { lineColor: input.lineColor } : {}),
|
|
307
323
|
...(typeof input.height === 'number' ? { height: input.height } : {}),
|
|
308
324
|
},
|
|
309
325
|
});
|
|
@@ -4,7 +4,6 @@ import { recomputeCodeGraph } from './code-graph.js';
|
|
|
4
4
|
import {
|
|
5
5
|
canvasState,
|
|
6
6
|
type CanvasEdge,
|
|
7
|
-
IMAGE_MIME_MAP,
|
|
8
7
|
type CanvasNodeState,
|
|
9
8
|
type CanvasNodeUpdate,
|
|
10
9
|
type CanvasSnapshot,
|
|
@@ -36,6 +35,7 @@ import {
|
|
|
36
35
|
getWebpageFetchErrorDetails,
|
|
37
36
|
normalizeWebpageUrl,
|
|
38
37
|
} from './webpage-node.js';
|
|
38
|
+
import { validateLocalImageFile } from './image-source.js';
|
|
39
39
|
import { buildExcalidrawRestoreCheckpointToolInput, ensureExcalidrawCheckpointId, isExcalidrawCreateView } from './diagram-presets.js';
|
|
40
40
|
|
|
41
41
|
export type CanvasArrangeMode = 'grid' | 'column' | 'flow';
|
|
@@ -435,22 +435,14 @@ function buildImageNodeData(input: CanvasAddNodeInput): Record<string, unknown>
|
|
|
435
435
|
|
|
436
436
|
if (!isDataUri && !isUrl && src) {
|
|
437
437
|
const resolved = resolve(src);
|
|
438
|
-
const ext = resolved.split('.').pop()?.toLowerCase() ?? '';
|
|
439
438
|
const fileName = resolved.split('/').pop() ?? src;
|
|
440
|
-
const
|
|
441
|
-
if (!mime) {
|
|
442
|
-
throw new Error(
|
|
443
|
-
`Invalid image node: "${fileName}" has unsupported extension ".${ext}". ` +
|
|
444
|
-
`Accepted: ${Object.keys(IMAGE_MIME_MAP).join(', ')}. ` +
|
|
445
|
-
`For non-image files use type="file" (live viewer) or type="webpage" (URL) instead.`,
|
|
446
|
-
);
|
|
447
|
-
}
|
|
439
|
+
const { mimeType } = validateLocalImageFile(resolved);
|
|
448
440
|
return {
|
|
449
441
|
...(input.data ?? {}),
|
|
450
442
|
src: resolved,
|
|
451
443
|
title: input.title ?? fileName,
|
|
452
444
|
path: resolved,
|
|
453
|
-
mimeType
|
|
445
|
+
mimeType,
|
|
454
446
|
};
|
|
455
447
|
}
|
|
456
448
|
|
|
@@ -1210,12 +1202,20 @@ export async function executeCanvasBatch(
|
|
|
1210
1202
|
...(typeof args.title === 'string' ? { title: args.title } : {}),
|
|
1211
1203
|
...(typeof args.xKey === 'string' ? { xKey: args.xKey } : {}),
|
|
1212
1204
|
...(typeof args.yKey === 'string' ? { yKey: args.yKey } : {}),
|
|
1205
|
+
...(typeof args.zKey === 'string' ? { zKey: args.zKey } : {}),
|
|
1213
1206
|
...(typeof args.nameKey === 'string' ? { nameKey: args.nameKey } : {}),
|
|
1214
1207
|
...(typeof args.valueKey === 'string' ? { valueKey: args.valueKey } : {}),
|
|
1208
|
+
...(typeof args.axisKey === 'string' ? { axisKey: args.axisKey } : {}),
|
|
1209
|
+
...(Array.isArray(args.metrics) ? { metrics: args.metrics.filter((m): m is string => typeof m === 'string') } : {}),
|
|
1210
|
+
...(Array.isArray(args.series) ? { series: args.series.filter((s): s is string => typeof s === 'string') } : {}),
|
|
1211
|
+
...(typeof args.barKey === 'string' ? { barKey: args.barKey } : {}),
|
|
1212
|
+
...(typeof args.lineKey === 'string' ? { lineKey: args.lineKey } : {}),
|
|
1215
1213
|
...(args.aggregate === 'sum' || args.aggregate === 'count' || args.aggregate === 'avg'
|
|
1216
1214
|
? { aggregate: args.aggregate }
|
|
1217
1215
|
: {}),
|
|
1218
1216
|
...(typeof args.color === 'string' ? { color: args.color } : {}),
|
|
1217
|
+
...(typeof args.barColor === 'string' ? { barColor: args.barColor } : {}),
|
|
1218
|
+
...(typeof args.lineColor === 'string' ? { lineColor: args.lineColor } : {}),
|
|
1219
1219
|
...(typeof args.height === 'number' ? { height: args.height } : {}),
|
|
1220
1220
|
...(typeof args.x === 'number' ? { x: args.x } : {}),
|
|
1221
1221
|
...(typeof args.y === 'number' ? { y: args.y } : {}),
|
|
@@ -282,26 +282,27 @@ const CANVAS_CREATE_TYPES: CanvasCreateTypeSchema[] = [
|
|
|
282
282
|
type: '"line" | "bar" | "pie" | "area" | "scatter" | "radar" | "stacked-bar" | "composed"',
|
|
283
283
|
required: true,
|
|
284
284
|
description: 'Chart type. Aliases like "stack" and "combo" are normalized server-side.',
|
|
285
|
+
aliases: ['graph-type'],
|
|
285
286
|
},
|
|
286
|
-
{ name: 'data', type: 'Record<string, unknown>[]', required: true, description: 'Chart dataset.', aliases: ['data-json'] },
|
|
287
|
+
{ name: 'data', type: 'Record<string, unknown>[]', required: true, description: 'Chart dataset. The CLI also accepts piped JSON via --stdin.', aliases: ['data-json', 'data-file'] },
|
|
287
288
|
{ name: 'title', type: 'string', required: false, description: 'Optional graph title.' },
|
|
288
|
-
{ name: 'xKey', type: 'string', required: false, description: 'X-axis/category key for line, bar, area, scatter, stacked-bar, and composed charts.' },
|
|
289
|
-
{ name: 'yKey', type: 'string', required: false, description: 'Y-axis value key for line, bar, area, and scatter charts. Also used as a fallback bar key for composed charts.' },
|
|
290
|
-
{ name: 'zKey', type: 'string', required: false, description: 'Optional bubble-size key for scatter charts.' },
|
|
291
|
-
{ name: 'nameKey', type: 'string', required: false, description: 'Slice name key for pie graphs.' },
|
|
292
|
-
{ name: 'valueKey', type: 'string', required: false, description: 'Slice value key for pie graphs.' },
|
|
293
|
-
{ name: 'axisKey', type: 'string', required: false, description: 'Category key for radar charts.' },
|
|
289
|
+
{ name: 'xKey', type: 'string', required: false, description: 'X-axis/category key for line, bar, area, scatter, stacked-bar, and composed charts.', aliases: ['x-key'] },
|
|
290
|
+
{ name: 'yKey', type: 'string', required: false, description: 'Y-axis value key for line, bar, area, and scatter charts. Also used as a fallback bar key for composed charts.', aliases: ['y-key'] },
|
|
291
|
+
{ name: 'zKey', type: 'string', required: false, description: 'Optional bubble-size key for scatter charts.', aliases: ['z-key'] },
|
|
292
|
+
{ name: 'nameKey', type: 'string', required: false, description: 'Slice name key for pie graphs.', aliases: ['name-key'] },
|
|
293
|
+
{ name: 'valueKey', type: 'string', required: false, description: 'Slice value key for pie graphs.', aliases: ['value-key'] },
|
|
294
|
+
{ name: 'axisKey', type: 'string', required: false, description: 'Category key for radar charts.', aliases: ['axis-key'] },
|
|
294
295
|
{ name: 'metrics', type: 'string[]', required: false, description: 'Series keys to plot as radar polygons. Defaults to non-axis numeric columns.' },
|
|
295
296
|
{ name: 'series', type: 'string[]', required: false, description: 'Series keys for stacked-bar segments. Defaults to non-x numeric columns.' },
|
|
296
|
-
{ name: 'barKey', type: 'string', required: false, description: 'Bar series key for composed charts.' },
|
|
297
|
-
{ name: 'lineKey', type: 'string', required: false, description: 'Line series key for composed charts.' },
|
|
297
|
+
{ name: 'barKey', type: 'string', required: false, description: 'Bar series key for composed charts.', aliases: ['bar-key'] },
|
|
298
|
+
{ name: 'lineKey', type: 'string', required: false, description: 'Line series key for composed charts.', aliases: ['line-key'] },
|
|
298
299
|
{ name: 'aggregate', type: '"sum" | "count" | "avg"', required: false, description: 'Optional aggregation for repeated x-axis values in line, bar, area, and stacked-bar charts.' },
|
|
299
300
|
{ name: 'color', type: 'string', required: false, description: 'Optional series color for line, bar, area, and scatter charts.' },
|
|
300
|
-
{ name: 'barColor', type: 'string', required: false, description: 'Optional bar color for composed charts.' },
|
|
301
|
-
{ name: 'lineColor', type: 'string', required: false, description: 'Optional line color for composed charts.' },
|
|
302
|
-
{ name: 'height', type: 'number', required: false, description: 'Optional chart content height.' },
|
|
301
|
+
{ name: 'barColor', type: 'string', required: false, description: 'Optional bar color for composed charts.', aliases: ['bar-color'] },
|
|
302
|
+
{ name: 'lineColor', type: 'string', required: false, description: 'Optional line color for composed charts.', aliases: ['line-color'] },
|
|
303
|
+
{ name: 'height', type: 'number', required: false, description: 'Optional chart content height.', aliases: ['chart-height'] },
|
|
303
304
|
{ name: 'width', type: 'number', required: false, description: 'Optional node width.' },
|
|
304
|
-
{ name: 'nodeHeight', type: 'number', required: false, description: 'Optional node height.' },
|
|
305
|
+
{ name: 'nodeHeight', type: 'number', required: false, description: 'Optional node height (canvas frame). Distinct from `height`, which sets only the chart content height inside the node.', aliases: ['node-height'] },
|
|
305
306
|
],
|
|
306
307
|
example: {
|
|
307
308
|
title: 'Deploy Trend',
|
|
@@ -326,7 +327,7 @@ const CANVAS_CREATE_TYPES: CanvasCreateTypeSchema[] = [
|
|
|
326
327
|
endpoint: '/api/canvas/web-artifact',
|
|
327
328
|
fields: [
|
|
328
329
|
{ name: 'title', type: 'string', required: true, description: 'Artifact title used for default paths.' },
|
|
329
|
-
{ name: 'appTsx', type: 'string', required: true, description: 'Contents for src/App.tsx.', aliases: ['
|
|
330
|
+
{ name: 'appTsx', type: 'string', required: true, description: 'Contents for src/App.tsx. The CLI also accepts piped contents via --stdin.', aliases: ['app-file', 'app-tsx'] },
|
|
330
331
|
{ name: 'indexCss', type: 'string', required: false, description: 'Optional src/index.css contents.', aliases: ['index-css-file', 'index-css'] },
|
|
331
332
|
{ name: 'mainTsx', type: 'string', required: false, description: 'Optional src/main.tsx contents.', aliases: ['main-file', 'main-tsx'] },
|
|
332
333
|
{ name: 'indexHtml', type: 'string', required: false, description: 'Optional index.html contents.', aliases: ['index-html-file', 'index-html'] },
|