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.
@@ -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;
@@ -0,0 +1,3 @@
1
+ export declare function validateLocalImageFile(path: string): {
2
+ mimeType: string;
3
+ };
@@ -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",
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
- - Some `mcp-app` creation flows are still awkward to address immediately after creation. If a
132
- diagram/app flow gives you a session-oriented result first, resolve the final canvas node from
133
- live layout or `node list --type mcp-app` before you try to group it, wire edges to it, or
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
- if (typeof flags['x-key'] === 'string') body.xKey = flags['x-key'];
558
- if (typeof flags['y-key'] === 'string') body.yKey = flags['y-key'];
559
- if (typeof flags['z-key'] === 'string') body.zKey = flags['z-key'];
560
- if (typeof flags['name-key'] === 'string') body.nameKey = flags['name-key'];
561
- if (typeof flags['value-key'] === 'string') body.valueKey = flags['value-key'];
562
- if (typeof flags['axis-key'] === 'string') body.axisKey = flags['axis-key'];
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
- if (typeof flags['bar-key'] === 'string') body.barKey = flags['bar-key'];
568
- if (typeof flags['line-key'] === 'string') body.lineKey = flags['line-key'];
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
- if (typeof flags.color === 'string') body.color = flags.color;
573
- if (typeof flags['bar-color'] === 'string') body.barColor = flags['bar-color'];
574
- if (typeof flags['line-color'] === 'string') body.lineColor = flags['line-color'];
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 = n.type === 'mcp-app' && data.hostMode === 'hosted' && typeof data.path === 'string'
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
- console.log(`\nAvailable "${oneWord}" commands:\n`);
2207
- for (const k of resourceCommands) {
2208
- console.log(` pmx-canvas ${k.padEnd(20)} ${COMMANDS[k].help}`);
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
- console.log('\nRun any command with --help for details.\n');
2211
- return;
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 id = `ext-app-${toolCallId}`;
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
- findExtAppNodeId(data.toolCallId) ?? findOnlyPendingExtAppNodeId(data.serverName, data.toolName);
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
- findExtAppNodeId(data.toolCallId) ?? findOnlyPendingExtAppNodeId(data.serverName, data.toolName);
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="pmx-chart">
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 mime = IMAGE_MIME_MAP[ext];
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: mime,
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: ['stdin', 'app-file', 'app-tsx'] },
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'] },