pmx-canvas 0.1.8 → 0.1.10
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 +136 -0
- package/Readme.md +52 -54
- package/dist/canvas/index.js +52 -52
- package/dist/json-render/index.css +1 -1
- package/dist/json-render/index.js +92 -92
- package/dist/types/client/canvas/auto-fit.d.ts +5 -0
- package/dist/types/client/nodes/ExtAppFrame.d.ts +2 -0
- package/dist/types/json-render/catalog.d.ts +316 -310
- package/dist/types/json-render/server.d.ts +1 -0
- package/dist/types/server/canvas-operations.d.ts +49 -0
- package/dist/types/server/index.d.ts +9 -1
- package/package.json +1 -1
- package/skills/pmx-canvas/SKILL.md +13 -1
- package/skills/published-consumer-e2e/scripts/run-published-consumer-e2e.sh +1 -1
- package/skills/web-artifacts-builder/scripts/bundle-artifact.sh +11 -0
- package/src/cli/agent.ts +135 -18
- package/src/cli/index.ts +1 -1
- package/src/client/canvas/CanvasNode.tsx +5 -7
- package/src/client/canvas/auto-fit.ts +21 -0
- package/src/client/nodes/ExtAppFrame.tsx +4 -2
- package/src/json-render/catalog.ts +9 -0
- package/src/json-render/renderer/index.css +61 -0
- package/src/json-render/renderer/index.tsx +22 -0
- package/src/json-render/server.ts +27 -11
- package/src/mcp/server.ts +46 -14
- package/src/server/canvas-operations.ts +309 -20
- package/src/server/canvas-schema.ts +2 -0
- package/src/server/canvas-validation.ts +9 -2
- package/src/server/diagram-presets.ts +48 -2
- package/src/server/index.ts +45 -6
- package/src/server/server.ts +149 -35
|
@@ -48,6 +48,7 @@ export declare function inferJsonRenderNodeTitle(spec: JsonRenderSpec, fallback?
|
|
|
48
48
|
export declare function normalizeAndValidateJsonRenderSpec(spec: unknown): JsonRenderSpec;
|
|
49
49
|
export declare function normalizeGraphType(value: string): GraphChartType;
|
|
50
50
|
export declare function buildGraphSpec(input: GraphNodeInput): JsonRenderSpec;
|
|
51
|
+
export declare function buildGraphConfig(input: GraphNodeInput): Record<string, unknown>;
|
|
51
52
|
export declare function createJsonRenderNodeData(nodeId: string, title: string, spec: JsonRenderSpec, extra?: Record<string, unknown>): Record<string, unknown>;
|
|
52
53
|
export declare function buildJsonRenderViewerHtml(options: {
|
|
53
54
|
title: string;
|
|
@@ -2,6 +2,38 @@ import { type CanvasEdge, type CanvasNodeState, type CanvasNodeUpdate, type Canv
|
|
|
2
2
|
import { type GraphNodeInput, type JsonRenderNodeInput, type JsonRenderSpec } from '../json-render/server.js';
|
|
3
3
|
export type CanvasArrangeMode = 'grid' | 'column' | 'flow';
|
|
4
4
|
export type CanvasPinMode = 'set' | 'add' | 'remove';
|
|
5
|
+
export interface CanvasFitViewOptions {
|
|
6
|
+
width?: number;
|
|
7
|
+
height?: number;
|
|
8
|
+
padding?: number;
|
|
9
|
+
maxScale?: number;
|
|
10
|
+
nodeIds?: string[];
|
|
11
|
+
}
|
|
12
|
+
export interface CanvasFitViewResult {
|
|
13
|
+
ok: true;
|
|
14
|
+
viewport: {
|
|
15
|
+
x: number;
|
|
16
|
+
y: number;
|
|
17
|
+
scale: number;
|
|
18
|
+
};
|
|
19
|
+
nodeCount: number;
|
|
20
|
+
bounds: {
|
|
21
|
+
x: number;
|
|
22
|
+
y: number;
|
|
23
|
+
width: number;
|
|
24
|
+
height: number;
|
|
25
|
+
} | null;
|
|
26
|
+
}
|
|
27
|
+
export interface CanvasGraphNodeUpdateInput extends Partial<GraphNodeInput> {
|
|
28
|
+
spec?: unknown;
|
|
29
|
+
type?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface CanvasStructuredNodeUpdateInput extends Omit<CanvasGraphNodeUpdateInput, 'data'> {
|
|
32
|
+
content?: unknown;
|
|
33
|
+
data?: unknown;
|
|
34
|
+
arrangeLocked?: unknown;
|
|
35
|
+
chartHeight?: unknown;
|
|
36
|
+
}
|
|
5
37
|
interface CanvasAddNodeInput {
|
|
6
38
|
type: CanvasNodeState['type'];
|
|
7
39
|
title?: string;
|
|
@@ -34,6 +66,22 @@ interface CanvasNodeLookupInput {
|
|
|
34
66
|
id?: string;
|
|
35
67
|
search?: string;
|
|
36
68
|
}
|
|
69
|
+
export declare function hasStructuredNodeUpdateFields(input: Record<string, unknown>): boolean;
|
|
70
|
+
export declare function buildStructuredNodeUpdate(node: CanvasNodeState, input: CanvasStructuredNodeUpdateInput): {
|
|
71
|
+
data: Record<string, unknown>;
|
|
72
|
+
};
|
|
73
|
+
export declare function buildJsonRenderNodeUpdate(node: CanvasNodeState, input: {
|
|
74
|
+
title?: string;
|
|
75
|
+
spec: unknown;
|
|
76
|
+
}): {
|
|
77
|
+
data: Record<string, unknown>;
|
|
78
|
+
spec: JsonRenderSpec;
|
|
79
|
+
};
|
|
80
|
+
export declare function buildGraphNodeUpdate(node: CanvasNodeState, input: CanvasGraphNodeUpdateInput): {
|
|
81
|
+
data: Record<string, unknown>;
|
|
82
|
+
spec: JsonRenderSpec;
|
|
83
|
+
graphConfig: Record<string, unknown>;
|
|
84
|
+
};
|
|
37
85
|
export declare function primeCanvasRuntimeBackends(options?: {
|
|
38
86
|
forceRehydrateExtApps?: boolean;
|
|
39
87
|
}): {
|
|
@@ -144,6 +192,7 @@ export declare function createCanvasGraphNode(input: GraphNodeInput): {
|
|
|
144
192
|
spec: JsonRenderSpec;
|
|
145
193
|
node: CanvasNodeState;
|
|
146
194
|
};
|
|
195
|
+
export declare function fitCanvasView(options?: CanvasFitViewOptions): CanvasFitViewResult;
|
|
147
196
|
export declare function executeCanvasBatch(operations: CanvasBatchOperation[]): Promise<{
|
|
148
197
|
ok: boolean;
|
|
149
198
|
results: Array<Record<string, unknown>>;
|
|
@@ -2,6 +2,7 @@ import { EventEmitter } from 'node:events';
|
|
|
2
2
|
import type { CanvasNodeState, CanvasEdge, CanvasLayout } from './canvas-state.js';
|
|
3
3
|
import { searchNodes } from './spatial-analysis.js';
|
|
4
4
|
import { diffLayouts } from './mutation-history.js';
|
|
5
|
+
import { fitCanvasView } from './canvas-operations.js';
|
|
5
6
|
import { type WebArtifactBuildInput, type WebArtifactCanvasBuildResult } from './web-artifacts.js';
|
|
6
7
|
import { type ExternalMcpTransportConfig } from './mcp-app-runtime.js';
|
|
7
8
|
import { type DiagramPresetOpenInput } from './diagram-presets.js';
|
|
@@ -48,7 +49,7 @@ export declare class PmxCanvas extends EventEmitter {
|
|
|
48
49
|
id: string;
|
|
49
50
|
error?: string;
|
|
50
51
|
}>;
|
|
51
|
-
updateNode(id: string, patch: Partial<CanvasNodeState>): void;
|
|
52
|
+
updateNode(id: string, patch: Partial<CanvasNodeState> & Record<string, unknown>): void;
|
|
52
53
|
removeNode(id: string): void;
|
|
53
54
|
addEdge(input: {
|
|
54
55
|
from?: string;
|
|
@@ -89,6 +90,13 @@ export declare class PmxCanvas extends EventEmitter {
|
|
|
89
90
|
focused: string;
|
|
90
91
|
panned: boolean;
|
|
91
92
|
} | null;
|
|
93
|
+
fitView(options?: {
|
|
94
|
+
width?: number;
|
|
95
|
+
height?: number;
|
|
96
|
+
padding?: number;
|
|
97
|
+
maxScale?: number;
|
|
98
|
+
nodeIds?: string[];
|
|
99
|
+
}): ReturnType<typeof fitCanvasView>;
|
|
92
100
|
getLayout(): CanvasLayout;
|
|
93
101
|
getNode(id: string): CanvasNodeState | undefined;
|
|
94
102
|
search(query: string): ReturnType<typeof searchNodes>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmx-canvas",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
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",
|
|
@@ -143,6 +143,8 @@ pmx-canvas search "auth"
|
|
|
143
143
|
pmx-canvas open
|
|
144
144
|
pmx-canvas arrange --layout flow
|
|
145
145
|
pmx-canvas focus <node-id> --no-pan # Select/raise without moving the user's viewport
|
|
146
|
+
pmx-canvas fit --width 1440 --height 900 # Fit the whole board for screenshots/review
|
|
147
|
+
pmx-canvas node update <node-id> --spec-file ./dashboard.json
|
|
146
148
|
pmx-canvas validate spec --type json-render --spec-file ./dashboard.json --summary
|
|
147
149
|
pmx-canvas web-artifact build --title "Dashboard" --app-file ./App.tsx --deps recharts --include-logs
|
|
148
150
|
pmx-canvas node list --type web-artifact --summary
|
|
@@ -167,6 +169,7 @@ pmx-canvas spatial
|
|
|
167
169
|
`"DVT O3"` can be ambiguous; prefer the full visible title such as `"DVT O3 — GitOps"`.
|
|
168
170
|
- `search`, `layout`, `status`, `arrange`, `focus` — inspect and navigate the canvas. Prefer
|
|
169
171
|
`focus --no-pan` when you only need to select/raise a node without hijacking the human's camera.
|
|
172
|
+
- `fit [id ...]` — set the server viewport to fit the whole canvas or selected nodes before screenshots or whole-board review
|
|
170
173
|
- `open` — open the current workbench in the browser
|
|
171
174
|
- `pin --list|--clear|<ids...>` — manage context pins
|
|
172
175
|
- `undo`, `redo`, `history` — time travel
|
|
@@ -279,7 +282,9 @@ If a node type is rejected by `canvas_add_node`, call `canvas_describe_schema` a
|
|
|
279
282
|
|
|
280
283
|
**`canvas_update_node`** — Update an existing node
|
|
281
284
|
- `id` (required): node to update
|
|
282
|
-
- Any of: `title`, `content`, `
|
|
285
|
+
- Any of: `title`, `content`, `x`, `y`, `width`, `height`, `collapsed`, `arrangeLocked`, `data`
|
|
286
|
+
- For `json-render`, pass `spec` to update the rendered spec in place while preserving node ID, edges, pins, and position
|
|
287
|
+
- For `graph`, pass graph fields such as `graphType`, `data`, `xKey`, `yKey`, `color`, and `chartHeight` to rebuild the chart in place; use `height`/`nodeHeight` for frame geometry and `chartHeight` for chart content in CLI flows
|
|
283
288
|
- Use to update status nodes as work progresses
|
|
284
289
|
|
|
285
290
|
**`canvas_remove_node`** — Remove a node and all its connected edges
|
|
@@ -357,6 +362,10 @@ ID extraction for mixed tool responses:
|
|
|
357
362
|
- Returns the normalized json-render spec the server would accept
|
|
358
363
|
- Use this when you want a dry run before creating a `json-render` or `graph` node
|
|
359
364
|
|
|
365
|
+
**`canvas_fit_view`** — Fit viewport to all nodes or selected nodes
|
|
366
|
+
- Optional: `width`, `height`, `padding`, `maxScale`, `nodeIds`
|
|
367
|
+
- Use before screenshot workflows or whole-board review so the server viewport matches the intended camera
|
|
368
|
+
|
|
360
369
|
**Batch graph creation**
|
|
361
370
|
- Use `graph.add` inside `canvas_batch` / `pmx-canvas batch` when you need a graph node as part of
|
|
362
371
|
a larger one-shot build.
|
|
@@ -512,6 +521,8 @@ tools below operate on the live canvas state.
|
|
|
512
521
|
- Required: exactly one of `expression` (single JS expression) or `script` (multi-statement body)
|
|
513
522
|
- `script` is wrapped in an async IIFE, so top-level `await` works inside script bodies
|
|
514
523
|
- Useful for asserting DOM state after a sequence of canvas mutations
|
|
524
|
+
- Do not use `fetch()` inside `canvas_evaluate` to call PMX HTTP APIs; WebView security/CORS
|
|
525
|
+
restrictions can block those requests. Use the matching MCP tools instead.
|
|
515
526
|
- Example: read the count of rendered `.canvas-node` elements:
|
|
516
527
|
|
|
517
528
|
```typescript
|
|
@@ -666,6 +677,7 @@ All POST/PATCH endpoints accept `Content-Type: application/json`. Default base U
|
|
|
666
677
|
| POST | `/api/canvas/group/ungroup` | Ungroup |
|
|
667
678
|
| POST | `/api/canvas/arrange` | Auto-arrange |
|
|
668
679
|
| POST | `/api/canvas/focus` | Center viewport on node |
|
|
680
|
+
| POST | `/api/canvas/fit` | Fit viewport to canvas bounds or selected nodes |
|
|
669
681
|
| POST | `/api/canvas/clear` | Clear canvas |
|
|
670
682
|
| POST | `/api/canvas/update` | Batch update positions |
|
|
671
683
|
| GET | `/api/canvas/spatial-context` | Spatial clusters and reading order |
|
|
@@ -144,7 +144,7 @@ for _ in $(seq 1 120); do
|
|
|
144
144
|
|
|
145
145
|
NODE_COUNT="$(
|
|
146
146
|
curl -fsS "http://127.0.0.1:${PORT}/api/canvas/state" \
|
|
147
|
-
|
|
|
147
|
+
| python3 -c 'import json, sys; print(len(json.load(sys.stdin).get("nodes", [])))'
|
|
148
148
|
)"
|
|
149
149
|
|
|
150
150
|
if [[ "${NODE_COUNT}" -ge 18 ]]; then
|
|
@@ -109,6 +109,17 @@ function ensure_bundle_dependencies() {
|
|
|
109
109
|
fi
|
|
110
110
|
done
|
|
111
111
|
|
|
112
|
+
# package.json lists the deps, but node_modules may not be in sync (e.g. fresh
|
|
113
|
+
# CI checkout where node_modules is gitignored). Run a regular install so the
|
|
114
|
+
# local binaries actually exist before we hand off to Parcel. The build
|
|
115
|
+
# allowlist is already persisted in package.json's pnpm.onlyBuiltDependencies
|
|
116
|
+
# by the original `pnpm add` step, so plain `install` is enough here.
|
|
117
|
+
if [ ! -x "./node_modules/.bin/parcel" ]; then
|
|
118
|
+
echo "📦 Syncing bundling dependencies into node_modules..."
|
|
119
|
+
run_pnpm_quiet install
|
|
120
|
+
return 0
|
|
121
|
+
fi
|
|
122
|
+
|
|
112
123
|
echo "✅ Reusing existing bundling dependencies"
|
|
113
124
|
}
|
|
114
125
|
|
package/src/cli/agent.ts
CHANGED
|
@@ -241,6 +241,14 @@ function optionalPositiveFiniteFlagWithAliases(
|
|
|
241
241
|
return undefined;
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
+
function optionalBooleanFlag(flags: Record<string, string | true>, name: string, hint: string): boolean | undefined {
|
|
245
|
+
const val = flags[name];
|
|
246
|
+
if (val === undefined) return undefined;
|
|
247
|
+
if (val === true || val === 'true') return true;
|
|
248
|
+
if (val === 'false') return false;
|
|
249
|
+
die(`Invalid value for --${name}: ${String(val)}`, hint);
|
|
250
|
+
}
|
|
251
|
+
|
|
244
252
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
245
253
|
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
246
254
|
}
|
|
@@ -503,6 +511,41 @@ async function readTextInput(
|
|
|
503
511
|
die(options.requiredMessage, options.hint);
|
|
504
512
|
}
|
|
505
513
|
|
|
514
|
+
async function readOptionalTextInput(
|
|
515
|
+
flags: Record<string, string | true>,
|
|
516
|
+
options: {
|
|
517
|
+
fileFlags?: string[];
|
|
518
|
+
valueFlags?: string[];
|
|
519
|
+
allowStdin?: boolean;
|
|
520
|
+
label: string;
|
|
521
|
+
hint: string;
|
|
522
|
+
},
|
|
523
|
+
): Promise<string | undefined> {
|
|
524
|
+
for (const name of options.fileFlags ?? []) {
|
|
525
|
+
const path = getStringFlag(flags, name);
|
|
526
|
+
if (!path) continue;
|
|
527
|
+
try {
|
|
528
|
+
return readFileSync(path, 'utf-8');
|
|
529
|
+
} catch (error) {
|
|
530
|
+
die(
|
|
531
|
+
`Unable to read --${name}: ${error instanceof Error ? error.message : String(error)}`,
|
|
532
|
+
options.hint,
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
for (const name of options.valueFlags ?? []) {
|
|
538
|
+
const value = getStringFlag(flags, name);
|
|
539
|
+
if (value !== undefined) return value;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (options.allowStdin && flags.stdin) {
|
|
543
|
+
return await readStdin();
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return undefined;
|
|
547
|
+
}
|
|
548
|
+
|
|
506
549
|
function applyCommonGeometryFlags(
|
|
507
550
|
body: Record<string, unknown>,
|
|
508
551
|
flags: Record<string, string | true>,
|
|
@@ -518,6 +561,27 @@ function applyCommonGeometryFlags(
|
|
|
518
561
|
if (height !== undefined) body.height = height;
|
|
519
562
|
}
|
|
520
563
|
|
|
564
|
+
async function applyStructuredNodeUpdateFlags(
|
|
565
|
+
body: Record<string, unknown>,
|
|
566
|
+
flags: Record<string, string | true>,
|
|
567
|
+
): Promise<void> {
|
|
568
|
+
const specRaw = await readOptionalTextInput(flags, {
|
|
569
|
+
fileFlags: ['spec-file'],
|
|
570
|
+
valueFlags: ['spec-json'],
|
|
571
|
+
allowStdin: false,
|
|
572
|
+
label: 'JSON spec',
|
|
573
|
+
hint: 'Use: pmx-canvas node update <node-id> --spec-file ./new-spec.json',
|
|
574
|
+
});
|
|
575
|
+
if (specRaw !== undefined) {
|
|
576
|
+
body.spec = parseJsonValue(specRaw, 'JSON spec', 'Use: pmx-canvas node update <node-id> --spec-file ./new-spec.json');
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const graphPatch = await buildGraphRequestBody(flags, { requireData: false, allowStdin: false });
|
|
580
|
+
for (const [key, value] of Object.entries(graphPatch)) {
|
|
581
|
+
body[key === 'height' ? 'chartHeight' : key] = value;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
521
585
|
async function buildJsonRenderRequestBody(
|
|
522
586
|
flags: Record<string, string | true>,
|
|
523
587
|
): Promise<Record<string, unknown>> {
|
|
@@ -547,23 +611,30 @@ async function buildJsonRenderRequestBody(
|
|
|
547
611
|
|
|
548
612
|
async function buildGraphRequestBody(
|
|
549
613
|
flags: Record<string, string | true>,
|
|
614
|
+
options: { requireData?: boolean; allowStdin?: boolean } = {},
|
|
550
615
|
): Promise<Record<string, unknown>> {
|
|
616
|
+
const requireData = options.requireData !== false;
|
|
617
|
+
const allowStdin = options.allowStdin !== false;
|
|
551
618
|
const hint =
|
|
552
619
|
'Use: pmx-canvas node add --type graph --graph-type bar --data-file ./metrics.json --x-key label --y-key value';
|
|
553
|
-
|
|
620
|
+
|
|
621
|
+
const body: Record<string, unknown> = {
|
|
622
|
+
...(requireData ? { graphType: getStringFlag(flags, 'graph-type', 'graphType') ?? 'line' } : {}),
|
|
623
|
+
};
|
|
624
|
+
const rawData = await readOptionalTextInput(flags, {
|
|
554
625
|
fileFlags: ['data-file'],
|
|
555
626
|
valueFlags: ['data-json', 'data'],
|
|
556
|
-
allowStdin
|
|
627
|
+
allowStdin,
|
|
557
628
|
label: 'graph JSON dataset',
|
|
558
629
|
hint,
|
|
559
|
-
requiredMessage: 'Graph nodes require --data-file, --data-json, --data, or --stdin JSON data.',
|
|
560
630
|
});
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
631
|
+
if (rawData !== undefined) {
|
|
632
|
+
body.data = parseRecordArrayJson(rawData, hint);
|
|
633
|
+
} else if (requireData) {
|
|
634
|
+
die('Graph nodes require --data-file, --data-json, --data, or --stdin JSON data.', hint);
|
|
635
|
+
}
|
|
636
|
+
const graphType = getStringFlag(flags, 'graph-type', 'graphType');
|
|
637
|
+
if (graphType) body.graphType = graphType;
|
|
567
638
|
if (typeof flags.title === 'string') body.title = flags.title;
|
|
568
639
|
const xKey = getStringFlag(flags, 'x-key', 'xKey');
|
|
569
640
|
const yKey = getStringFlag(flags, 'y-key', 'yKey');
|
|
@@ -1151,6 +1222,9 @@ cmd('node update', 'Update a node by ID', [
|
|
|
1151
1222
|
'pmx-canvas node update <node-id> --content "Updated content"',
|
|
1152
1223
|
'pmx-canvas node update <node-id> --title "Moved" --x 500 --y 300',
|
|
1153
1224
|
'pmx-canvas node update <node-id> --width 840 --height 620',
|
|
1225
|
+
'pmx-canvas node update <node-id> --spec-file ./dashboard.json',
|
|
1226
|
+
'pmx-canvas node update <graph-id> --data-file ./metrics.json --chart-height 420',
|
|
1227
|
+
'pmx-canvas node update <node-id> --pinned true',
|
|
1154
1228
|
'pmx-canvas node update <node-id> --lock-arrange',
|
|
1155
1229
|
], async (args) => {
|
|
1156
1230
|
const { positional, flags } = parseFlags(args);
|
|
@@ -1160,6 +1234,7 @@ cmd('node update', 'Update a node by ID', [
|
|
|
1160
1234
|
if (!id) die('Missing node ID', 'pmx-canvas node update <node-id> --title "New Title"');
|
|
1161
1235
|
|
|
1162
1236
|
const body: Record<string, unknown> = {};
|
|
1237
|
+
await applyStructuredNodeUpdateFlags(body, flags);
|
|
1163
1238
|
if (flags.title && flags.title !== true) body.title = flags.title;
|
|
1164
1239
|
if (flags.content && flags.content !== true) body.content = flags.content;
|
|
1165
1240
|
if (flags.stdin) body.content = await readStdin();
|
|
@@ -1168,6 +1243,17 @@ cmd('node update', 'Update a node by ID', [
|
|
|
1168
1243
|
const y = optionalFiniteFlag(flags, 'y', 'Use a finite number, e.g. --y 300');
|
|
1169
1244
|
const width = optionalPositiveFiniteFlag(flags, 'width', 'Use a positive number, e.g. --width 840');
|
|
1170
1245
|
const height = optionalPositiveFiniteFlag(flags, 'height', 'Use a positive number, e.g. --height 620');
|
|
1246
|
+
const nodeHeight = optionalPositiveFiniteFlagWithAliases(
|
|
1247
|
+
flags,
|
|
1248
|
+
'Use a positive number, e.g. --node-height 620',
|
|
1249
|
+
'node-height',
|
|
1250
|
+
'nodeHeight',
|
|
1251
|
+
);
|
|
1252
|
+
if (height !== undefined && nodeHeight !== undefined) {
|
|
1253
|
+
die('Use either --height/--node-height, not both.');
|
|
1254
|
+
}
|
|
1255
|
+
const frameHeight = height ?? nodeHeight;
|
|
1256
|
+
const pinned = optionalBooleanFlag(flags, 'pinned', 'Use --pinned true or --pinned false');
|
|
1171
1257
|
if (flags['lock-arrange'] && flags['unlock-arrange']) {
|
|
1172
1258
|
die('Use either --lock-arrange or --unlock-arrange, not both.');
|
|
1173
1259
|
}
|
|
@@ -1177,7 +1263,7 @@ cmd('node update', 'Update a node by ID', [
|
|
|
1177
1263
|
? false
|
|
1178
1264
|
: undefined;
|
|
1179
1265
|
|
|
1180
|
-
if (x !== undefined || y !== undefined || width !== undefined ||
|
|
1266
|
+
if (x !== undefined || y !== undefined || width !== undefined || frameHeight !== undefined || arrangeLocked !== undefined) {
|
|
1181
1267
|
const existing = await api('GET', `/api/canvas/node/${encodeURIComponent(id)}`) as {
|
|
1182
1268
|
position: { x: number; y: number };
|
|
1183
1269
|
size: { width: number; height: number };
|
|
@@ -1191,25 +1277,24 @@ cmd('node update', 'Update a node by ID', [
|
|
|
1191
1277
|
};
|
|
1192
1278
|
}
|
|
1193
1279
|
|
|
1194
|
-
if (width !== undefined ||
|
|
1280
|
+
if (width !== undefined || frameHeight !== undefined) {
|
|
1195
1281
|
body.size = {
|
|
1196
1282
|
width: width ?? existing.size.width,
|
|
1197
|
-
height:
|
|
1283
|
+
height: frameHeight ?? existing.size.height,
|
|
1198
1284
|
};
|
|
1199
1285
|
}
|
|
1200
1286
|
|
|
1201
1287
|
if (arrangeLocked !== undefined) {
|
|
1202
|
-
body.
|
|
1203
|
-
...existing.data,
|
|
1204
|
-
arrangeLocked,
|
|
1205
|
-
};
|
|
1288
|
+
body.arrangeLocked = arrangeLocked;
|
|
1206
1289
|
}
|
|
1207
1290
|
}
|
|
1208
1291
|
|
|
1292
|
+
if (pinned !== undefined) body.pinned = pinned;
|
|
1293
|
+
|
|
1209
1294
|
if (Object.keys(body).length === 0) {
|
|
1210
1295
|
die(
|
|
1211
1296
|
'No updates specified',
|
|
1212
|
-
'Use --title, --content, --x, --y, --width, --height, --lock-arrange, --unlock-arrange, or --stdin',
|
|
1297
|
+
'Use --title, --content, --x, --y, --width, --height, --pinned, --lock-arrange, --unlock-arrange, or --stdin',
|
|
1213
1298
|
);
|
|
1214
1299
|
}
|
|
1215
1300
|
|
|
@@ -1396,6 +1481,29 @@ cmd('focus', 'Pan viewport to center on a node', [
|
|
|
1396
1481
|
output(result);
|
|
1397
1482
|
});
|
|
1398
1483
|
|
|
1484
|
+
cmd('fit', 'Fit the viewport to all nodes or a selected subset', [
|
|
1485
|
+
'pmx-canvas fit',
|
|
1486
|
+
'pmx-canvas fit --width 1440 --height 900 --padding 80',
|
|
1487
|
+
'pmx-canvas fit node-a node-b',
|
|
1488
|
+
], async (args) => {
|
|
1489
|
+
const { positional, flags } = parseFlags(args);
|
|
1490
|
+
if (flags.help || flags.h) return showCommandHelp('fit');
|
|
1491
|
+
|
|
1492
|
+
const body: Record<string, unknown> = {};
|
|
1493
|
+
const width = optionalPositiveFiniteFlag(flags, 'width', 'Use a positive number, e.g. --width 1440');
|
|
1494
|
+
const height = optionalPositiveFiniteFlag(flags, 'height', 'Use a positive number, e.g. --height 900');
|
|
1495
|
+
const padding = optionalPositiveFiniteFlag(flags, 'padding', 'Use a positive number, e.g. --padding 80');
|
|
1496
|
+
const maxScale = optionalPositiveFiniteFlag(flags, 'max-scale', 'Use a positive number, e.g. --max-scale 1');
|
|
1497
|
+
if (width !== undefined) body.width = width;
|
|
1498
|
+
if (height !== undefined) body.height = height;
|
|
1499
|
+
if (padding !== undefined) body.padding = padding;
|
|
1500
|
+
if (maxScale !== undefined) body.maxScale = maxScale;
|
|
1501
|
+
if (positional.length > 0) body.nodeIds = positional;
|
|
1502
|
+
|
|
1503
|
+
const result = await api('POST', '/api/canvas/fit', body);
|
|
1504
|
+
output(result);
|
|
1505
|
+
});
|
|
1506
|
+
|
|
1399
1507
|
// ── external-app add ─────────────────────────────────────────
|
|
1400
1508
|
cmd('external-app add', 'Create a hosted external app node', [
|
|
1401
1509
|
'pmx-canvas external-app add --kind excalidraw --title "Diagram"',
|
|
@@ -1816,7 +1924,7 @@ cmd('webview start', 'Start or replace the Bun.WebView automation session', [
|
|
|
1816
1924
|
if (chromeArgv.length > 0) body.chromeArgv = chromeArgv;
|
|
1817
1925
|
}
|
|
1818
1926
|
|
|
1819
|
-
const result = await api('POST', '/api/workbench/webview/start', body);
|
|
1927
|
+
const result = await api('POST', '/api/workbench/webview/start', body, { allowErrorJson: true });
|
|
1820
1928
|
output(result);
|
|
1821
1929
|
});
|
|
1822
1930
|
|
|
@@ -2112,6 +2220,13 @@ function showCommandHelp(name: string): void {
|
|
|
2112
2220
|
console.log('\nViewport:');
|
|
2113
2221
|
console.log(' --no-pan Select/raise the node without moving the viewport');
|
|
2114
2222
|
}
|
|
2223
|
+
if (name === 'fit') {
|
|
2224
|
+
console.log('\nViewport:');
|
|
2225
|
+
console.log(' --width <px> Viewport width used for fit math (default 1440)');
|
|
2226
|
+
console.log(' --height <px> Viewport height used for fit math (default 900)');
|
|
2227
|
+
console.log(' --padding <px> World-space padding around fitted nodes (default 60)');
|
|
2228
|
+
console.log(' --max-scale <scale> Maximum zoom scale (default 1)');
|
|
2229
|
+
}
|
|
2115
2230
|
if (name === 'external-app add') {
|
|
2116
2231
|
console.log('\nOptions:');
|
|
2117
2232
|
console.log(' --kind excalidraw External app kind to create');
|
|
@@ -2161,6 +2276,7 @@ Canvas commands:
|
|
|
2161
2276
|
pmx-canvas validate spec Validate json-render/graph payloads without creating nodes
|
|
2162
2277
|
pmx-canvas watch [options] Watch semantic canvas changes over SSE
|
|
2163
2278
|
pmx-canvas focus <id> Pan viewport to node
|
|
2279
|
+
pmx-canvas fit [id ...] Fit viewport to canvas or selected nodes
|
|
2164
2280
|
pmx-canvas external-app add Add hosted external apps like Excalidraw
|
|
2165
2281
|
pmx-canvas webview status Show WebView automation status
|
|
2166
2282
|
pmx-canvas webview start [options] Start or replace automation session
|
|
@@ -2222,6 +2338,7 @@ Examples:
|
|
|
2222
2338
|
pmx-canvas edge add --from-search "DVT O3 — GitOps" --to-search "deep work trend" --type relation
|
|
2223
2339
|
pmx-canvas search "authentication"
|
|
2224
2340
|
pmx-canvas open
|
|
2341
|
+
pmx-canvas fit --width 1440 --height 900
|
|
2225
2342
|
pmx-canvas layout --summary
|
|
2226
2343
|
pmx-canvas arrange --layout column
|
|
2227
2344
|
pmx-canvas batch --file ./canvas-ops.json
|
package/src/cli/index.ts
CHANGED
|
@@ -30,7 +30,7 @@ if (args.includes('--version') || args.includes('-v')) {
|
|
|
30
30
|
// If first arg is a known subcommand (not a --flag), route to the agent CLI.
|
|
31
31
|
const AGENT_COMMANDS = new Set([
|
|
32
32
|
'node', 'edge', 'search', 'layout', 'status', 'arrange', 'focus',
|
|
33
|
-
'pin', 'undo', 'redo', 'history', 'snapshot', 'diff', 'group', 'webview', 'open',
|
|
33
|
+
'fit', 'pin', 'undo', 'redo', 'history', 'snapshot', 'diff', 'group', 'webview', 'open',
|
|
34
34
|
'clear', 'code-graph', 'spatial', 'watch', 'web-artifact', 'external-app', 'graph', 'batch', 'validate', 'serve',
|
|
35
35
|
]);
|
|
36
36
|
|
|
@@ -28,6 +28,7 @@ import { removeNodeFromClient, updateNodeFromClient } from '../state/intent-brid
|
|
|
28
28
|
import { getNodeIcon } from '../icons';
|
|
29
29
|
import { EXPANDABLE_TYPES, TYPE_LABELS } from '../types';
|
|
30
30
|
import type { CanvasNodeState } from '../types';
|
|
31
|
+
import { computeAutoFitHeight, shouldAutoFitNode } from './auto-fit';
|
|
31
32
|
import { activeGuides, buildSnapCache, clearSnapCache, snapToGuides } from './snap-guides';
|
|
32
33
|
import { useNodeDrag } from './use-node-drag';
|
|
33
34
|
import { useNodeResize } from './use-node-resize';
|
|
@@ -141,12 +142,9 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
|
|
|
141
142
|
const bodyRef = useRef<HTMLDivElement>(null);
|
|
142
143
|
const hasAutoFit = useRef(false);
|
|
143
144
|
const autoFitPersistTimer = useRef<number | null>(null);
|
|
144
|
-
const AUTO_FIT_MAX = 600;
|
|
145
|
-
const TITLEBAR_HEIGHT = 37;
|
|
146
|
-
const isExtAppNode = node.type === 'mcp-app' && node.data.mode === 'ext-app';
|
|
147
145
|
|
|
148
146
|
useEffect(() => {
|
|
149
|
-
if (hasAutoFit.current || node
|
|
147
|
+
if (hasAutoFit.current || !shouldAutoFitNode(node)) return;
|
|
150
148
|
const body = bodyRef.current;
|
|
151
149
|
if (!body) return;
|
|
152
150
|
|
|
@@ -156,9 +154,9 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
|
|
|
156
154
|
return;
|
|
157
155
|
}
|
|
158
156
|
const contentHeight = body.scrollHeight;
|
|
159
|
-
|
|
157
|
+
const fitHeight = computeAutoFitHeight(node, contentHeight);
|
|
158
|
+
if (fitHeight === null) return;
|
|
160
159
|
|
|
161
|
-
const fitHeight = Math.min(contentHeight + TITLEBAR_HEIGHT, AUTO_FIT_MAX);
|
|
162
160
|
// Only resize if the fit height differs meaningfully from current
|
|
163
161
|
if (Math.abs(fitHeight - node.size.height) > 8) {
|
|
164
162
|
resizeNode(node.id, { width: node.size.width, height: fitHeight });
|
|
@@ -181,7 +179,7 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
|
|
|
181
179
|
autoFitPersistTimer.current = null;
|
|
182
180
|
}
|
|
183
181
|
};
|
|
184
|
-
}, [node.id, node.type,
|
|
182
|
+
}, [node.id, node.type, node.data.mode, node.collapsed, node.dockPosition, node.size.width, node.size.height]);
|
|
185
183
|
|
|
186
184
|
const isPinned = node.pinned;
|
|
187
185
|
const isTrace = node.type === 'trace';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { CanvasNodeState } from '../types';
|
|
2
|
+
|
|
3
|
+
export const AUTO_FIT_TITLEBAR_HEIGHT = 37;
|
|
4
|
+
export const AUTO_FIT_MAX_HEIGHT = 600;
|
|
5
|
+
|
|
6
|
+
function isExtAppNode(node: CanvasNodeState): boolean {
|
|
7
|
+
return node.type === 'mcp-app' && node.data.mode === 'ext-app';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function hasExplicitStructuredFrame(node: CanvasNodeState): boolean {
|
|
11
|
+
return node.type === 'graph' || node.type === 'json-render';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function shouldAutoFitNode(node: CanvasNodeState): boolean {
|
|
15
|
+
return !node.collapsed && !node.dockPosition && node.type !== 'group' && !isExtAppNode(node) && !hasExplicitStructuredFrame(node);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function computeAutoFitHeight(node: CanvasNodeState, contentHeight: number): number | null {
|
|
19
|
+
if (!shouldAutoFitNode(node) || contentHeight <= 0) return null;
|
|
20
|
+
return Math.min(contentHeight + AUTO_FIT_TITLEBAR_HEIGHT, AUTO_FIT_MAX_HEIGHT);
|
|
21
|
+
}
|
|
@@ -23,6 +23,8 @@ type DisplayMode = 'inline' | 'fullscreen' | 'pip';
|
|
|
23
23
|
const DEFAULT_EXT_APP_SANDBOX = 'allow-scripts allow-popups allow-popups-to-escape-sandbox';
|
|
24
24
|
|
|
25
25
|
interface ExtAppHostDimensionsTarget {
|
|
26
|
+
clientWidth?: number;
|
|
27
|
+
clientHeight?: number;
|
|
26
28
|
getBoundingClientRect(): Pick<DOMRectReadOnly, 'width' | 'height'>;
|
|
27
29
|
}
|
|
28
30
|
|
|
@@ -120,8 +122,8 @@ export function resolveExtAppContainerDimensions(
|
|
|
120
122
|
): { width: number; height: number } {
|
|
121
123
|
const rect = target?.getBoundingClientRect();
|
|
122
124
|
return {
|
|
123
|
-
width: positiveDimension(rect?.width ?? 0, fallback.width),
|
|
124
|
-
height: positiveDimension(rect?.height ?? 0, fallback.height),
|
|
125
|
+
width: positiveDimension(target?.clientWidth ?? 0, positiveDimension(rect?.width ?? 0, fallback.width)),
|
|
126
|
+
height: positiveDimension(target?.clientHeight ?? 0, positiveDimension(rect?.height ?? 0, fallback.height)),
|
|
125
127
|
};
|
|
126
128
|
}
|
|
127
129
|
|
|
@@ -7,13 +7,22 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { defineCatalog } from '@json-render/core';
|
|
10
|
+
import { z } from 'zod';
|
|
10
11
|
import { schema } from './schema.js';
|
|
11
12
|
import { shadcnComponentDefinitions } from '@json-render/shadcn/catalog';
|
|
12
13
|
import { chartComponentDefinitions } from './charts/definitions';
|
|
13
14
|
import { extraChartComponentDefinitions } from './charts/extra-definitions';
|
|
14
15
|
|
|
16
|
+
const badgeDefinition = shadcnComponentDefinitions.Badge;
|
|
17
|
+
|
|
15
18
|
export const allComponentDefinitions = {
|
|
16
19
|
...shadcnComponentDefinitions,
|
|
20
|
+
Badge: {
|
|
21
|
+
...badgeDefinition,
|
|
22
|
+
props: badgeDefinition.props.extend({
|
|
23
|
+
variant: z.enum(['default', 'secondary', 'destructive', 'outline', 'success', 'info', 'warning', 'error', 'danger']).nullable(),
|
|
24
|
+
}),
|
|
25
|
+
},
|
|
17
26
|
...chartComponentDefinitions,
|
|
18
27
|
...extraChartComponentDefinitions,
|
|
19
28
|
};
|
|
@@ -143,6 +143,67 @@ button {
|
|
|
143
143
|
cursor: pointer;
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
.pmx-badge {
|
|
147
|
+
display: inline-flex;
|
|
148
|
+
width: fit-content;
|
|
149
|
+
align-items: center;
|
|
150
|
+
justify-content: center;
|
|
151
|
+
gap: 0.25rem;
|
|
152
|
+
overflow: hidden;
|
|
153
|
+
white-space: nowrap;
|
|
154
|
+
border: 1px solid transparent;
|
|
155
|
+
border-radius: 9999px;
|
|
156
|
+
padding: 0.125rem 0.5rem;
|
|
157
|
+
font-size: 0.75rem;
|
|
158
|
+
font-weight: 500;
|
|
159
|
+
line-height: 1.25rem;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.pmx-badge--default {
|
|
163
|
+
background: var(--primary);
|
|
164
|
+
color: var(--primary-foreground);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.pmx-badge--secondary {
|
|
168
|
+
background: var(--secondary);
|
|
169
|
+
color: var(--secondary-foreground);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.pmx-badge--destructive {
|
|
173
|
+
background: var(--destructive);
|
|
174
|
+
color: var(--destructive-foreground);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.pmx-badge--outline {
|
|
178
|
+
border-color: var(--border);
|
|
179
|
+
color: var(--foreground);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.pmx-badge--success {
|
|
183
|
+
border-color: color-mix(in oklch, var(--chart-2) 45%, transparent);
|
|
184
|
+
background: color-mix(in oklch, var(--chart-2) 16%, transparent);
|
|
185
|
+
color: var(--chart-2);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.pmx-badge--info {
|
|
189
|
+
border-color: color-mix(in oklch, var(--chart-1) 45%, transparent);
|
|
190
|
+
background: color-mix(in oklch, var(--chart-1) 14%, transparent);
|
|
191
|
+
color: var(--chart-1);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.pmx-badge--warning {
|
|
195
|
+
border-color: color-mix(in oklch, var(--chart-3) 50%, transparent);
|
|
196
|
+
background: color-mix(in oklch, var(--chart-3) 18%, transparent);
|
|
197
|
+
color: var(--chart-3);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.pmx-badge--error,
|
|
201
|
+
.pmx-badge--danger {
|
|
202
|
+
border-color: color-mix(in oklch, var(--destructive) 55%, transparent);
|
|
203
|
+
background: color-mix(in oklch, var(--destructive) 18%, transparent);
|
|
204
|
+
color: var(--destructive);
|
|
205
|
+
}
|
|
206
|
+
|
|
146
207
|
/* -- Chart components -- */
|
|
147
208
|
|
|
148
209
|
.pmx-chart {
|