pmx-canvas 0.3.4 → 0.4.0
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 +53 -0
- package/Readme.md +11 -3
- package/dist/canvas/index.js +67 -67
- package/dist/types/client/index.d.ts +1 -1
- package/dist/types/client/nodes/ExtAppFrame.d.ts +10 -0
- package/dist/types/client/state/node-factory.d.ts +29 -0
- package/dist/types/client/uuid-polyfill.d.ts +0 -0
- package/dist/types/server/canvas-db.d.ts +0 -1
- package/dist/types/server/canvas-state.d.ts +14 -15
- package/dist/types/server/operations/composites.d.ts +6 -21
- package/dist/types/server/server.d.ts +1 -0
- package/docs/cli.md +5 -0
- package/docs/environment.md +2 -2
- package/docs/http-api.md +25 -1
- package/docs/mcp.md +21 -30
- package/docs/plans/plan-009-tech-debt-backlog.md +13 -12
- package/docs/screenshot.png +0 -0
- package/docs/tech-debt-assessment-2026-06.md +5 -5
- package/package.json +7 -7
- package/skills/pmx-canvas/SKILL.md +16 -6
- package/skills/pmx-canvas/evals/evals.json +2 -2
- package/skills/pmx-canvas/references/full-reference.md +17 -15
- package/src/cli/commands/history.ts +1 -1
- package/src/cli/commands/nodes.ts +31 -1
- package/src/cli/index.ts +8 -2
- package/src/cli/shared.ts +1 -0
- package/src/client/index.tsx +2 -0
- package/src/client/nodes/ExtAppFrame.tsx +17 -2
- package/src/client/state/node-factory.ts +72 -0
- package/src/client/state/sse-bridge.ts +209 -86
- package/src/client/uuid-polyfill.ts +14 -0
- package/src/server/canvas-db.ts +0 -8
- package/src/server/canvas-schema.ts +2 -8
- package/src/server/canvas-state.ts +51 -151
- package/src/server/index.ts +3 -9
- package/src/server/operations/composites.ts +24 -21
- package/src/server/operations/ops/snapshots.ts +6 -7
- package/src/server/operations/registry.ts +4 -1
- package/src/server/server.ts +170 -74
|
@@ -172,7 +172,7 @@
|
|
|
172
172
|
"id": 7,
|
|
173
173
|
"name": "snapshot-before-changes",
|
|
174
174
|
"prompt": "I want to reorganize the canvas to explore a different approach, but I don't want to lose what's there now. Can you save the current state, clear it, and set up a fresh workspace for the new approach? If it doesn't work out I want to go back.",
|
|
175
|
-
"expected_output": "Takes a named snapshot of current state, then clears the canvas. Mentions that
|
|
175
|
+
"expected_output": "Takes a named snapshot of current state, then clears the canvas. Mentions that canvas_snapshot { action: \"restore\" } can bring back the old state. Does NOT just clear without snapshotting.",
|
|
176
176
|
"assertions": [
|
|
177
177
|
{
|
|
178
178
|
"name": "takes-snapshot",
|
|
@@ -186,7 +186,7 @@
|
|
|
186
186
|
},
|
|
187
187
|
{
|
|
188
188
|
"name": "mentions-restore",
|
|
189
|
-
"description": "Tells the user how to restore (
|
|
189
|
+
"description": "Tells the user how to restore (canvas_snapshot action restore) if they want to go back",
|
|
190
190
|
"type": "output_check"
|
|
191
191
|
}
|
|
192
192
|
]
|
|
@@ -73,7 +73,7 @@ section below.
|
|
|
73
73
|
3. **Read before write** — `canvas_query { action: "search", query }` (or `pmx-canvas search`)
|
|
74
74
|
to find existing nodes; avoid duplicates. Use `canvas_query { action: "layout" }` only when
|
|
75
75
|
you need the full board.
|
|
76
|
-
4. **Snapshot before destructive work** — `canvas_snapshot { name }` before clear/major
|
|
76
|
+
4. **Snapshot before destructive work** — `canvas_snapshot { action: "save", name }` before clear/major
|
|
77
77
|
reorg; restore if needed.
|
|
78
78
|
5. **Signal then mutate (default behavior)** — signal with `canvas_intent { action: "signal", … }`
|
|
79
79
|
to telegraph the move before nearly every mutation, then create with the right composite: `canvas_node` (markdown/status/file/webpage/html
|
|
@@ -315,6 +315,7 @@ pmx-canvas spatial
|
|
|
315
315
|
- `group create|add|remove` — manage groups
|
|
316
316
|
- `clear --yes` — destructive clear with explicit confirmation
|
|
317
317
|
- `validate spec` — validate json-render specs and graph payloads without creating nodes
|
|
318
|
+
- `node update` — returns a compact `{ ok, id, nodeId, position, size, updatedFields }` envelope by default; pass `--full` for the complete node payload. Full ext-app/mcp-app payloads embed the bundled app HTML and tool results (hundreds of KB — a no-op ext-app resize once returned ~940 KB), so in agent turns keep the default envelope and read content via `node get --summary`
|
|
318
319
|
- `web-artifact build` — build bundled React/Tailwind HTML artifacts; use `--deps` for extra packages and `--include-logs` only when raw logs are useful. The build pipeline runs bash scripts (POSIX-only) — it is not supported on native Windows; already-built artifacts render everywhere
|
|
319
320
|
- `external-app add --kind excalidraw` — create the hosted Excalidraw preset; response includes `id` and `nodeId` aliases for the same canvas node
|
|
320
321
|
- `serve status|stop` — inspect and stop daemonized servers started with `serve --daemon`
|
|
@@ -429,7 +430,7 @@ single-purpose tools behind an `action` (and, for `canvas_ax_gate`, a `kind`) di
|
|
|
429
430
|
> registered — use the composites instead. The authoritative legacy→composite mapping table lives
|
|
430
431
|
> in [`docs/mcp.md`](../../../docs/mcp.md) — this skill does not re-enumerate the removed names.
|
|
431
432
|
|
|
432
|
-
### The
|
|
433
|
+
### The 16 composites
|
|
433
434
|
|
|
434
435
|
| Composite | `action` values | What it does |
|
|
435
436
|
|-----------|-----------------|--------------|
|
|
@@ -438,6 +439,7 @@ single-purpose tools behind an `action` (and, for `canvas_ax_gate`, a `kind`) di
|
|
|
438
439
|
| `canvas_edge` | `add` · `remove` | Connect / disconnect nodes |
|
|
439
440
|
| `canvas_group` | `create` · `add` · `ungroup` | Manage spatial group containers |
|
|
440
441
|
| `canvas_history` | `undo` · `redo` | Time travel through the mutation ring buffer |
|
|
442
|
+
| `canvas_snapshot` | `save` · `list` · `restore` · `delete` · `gc` · `diff` | Named snapshots: save/list/restore/delete, garbage-collect old ones, diff current canvas vs a snapshot (`diff` takes `snapshot`, not `id`) |
|
|
441
443
|
| `canvas_view` | `arrange` · `focus` · `fit` · `clear` · `remove-annotation` | Auto-arrange, pan-to-node, fit viewport, clear the board, delete a human-drawn annotation by id |
|
|
442
444
|
| `canvas_query` | `search` · `layout` · `validate` | Find nodes by keyword, read full layout, or **`validate`** the board for node collisions / group-containment / dangling edges |
|
|
443
445
|
| `canvas_app` | `open-mcp-app` · `diagram` · `build-artifact` | Hosted MCP apps, the Excalidraw diagram preset, and bundled web artifacts (folds `canvas_open_mcp_app` / `canvas_add_diagram` / `canvas_build_web_artifact`) |
|
|
@@ -505,10 +507,9 @@ investigation board" intent, then run the batch with that linked `intentId` (use
|
|
|
505
507
|
|
|
506
508
|
These stay separate by design (trust-boundary, firehose, execution-intent, or not-yet-consolidated
|
|
507
509
|
surfaces): `canvas_batch`, `canvas_pin_nodes`, `canvas_screenshot`, `canvas_ax_interaction`,
|
|
508
|
-
`canvas_ingest_activity`, `canvas_invoke_command
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
the current save-snapshot tool, so the composite cannot land additively).
|
|
510
|
+
`canvas_ingest_activity`, `canvas_invoke_command`. Snapshots moved into the `canvas_snapshot`
|
|
511
|
+
composite in v0.4.0 (actions `save | list | restore | delete | gc | diff`); the 6 legacy snapshot
|
|
512
|
+
standalones were removed after their deprecated 0.3.x window.
|
|
512
513
|
|
|
513
514
|
`canvas_batch` supports exactly these ops: `node.add`, `node.update`, `node.remove`, `graph.add`,
|
|
514
515
|
`edge.add`, `edge.remove`, `group.create`, `group.add`, `group.remove`, `pin.set`/`pin.add`/
|
|
@@ -650,7 +651,7 @@ ID extraction for mixed tool responses:
|
|
|
650
651
|
|
|
651
652
|
**`canvas_pin_nodes`** (standalone) — Set, add, or remove pinned context nodes. Use `{ nodeIds: [...] }` — the field is `nodeIds`, not `ids`.
|
|
652
653
|
|
|
653
|
-
**`
|
|
654
|
+
**`canvas_snapshot { action: "diff" }`** — Compare current canvas state with a saved snapshot. Requires `{ snapshot: "<snapshot-id-or-name>" }`; there is no implicit previous-snapshot default.
|
|
654
655
|
|
|
655
656
|
**`canvas_render { action: "describe-schema" }`** — Inspect the running server's create schemas and
|
|
656
657
|
canonical examples. Use before generating structured payloads when you need the authoritative current
|
|
@@ -766,13 +767,14 @@ ranked matches with content snippets. Use instead of parsing the full layout to
|
|
|
766
767
|
### History & Snapshots
|
|
767
768
|
|
|
768
769
|
- **`canvas_history { action: "undo" }`** / **`canvas_history { action: "redo" }`** — step the mutation ring buffer.
|
|
769
|
-
- **`canvas_snapshot`**
|
|
770
|
-
- **`
|
|
771
|
-
- **`
|
|
770
|
+
- **`canvas_snapshot { action: "save", name }`** — save a named snapshot. Returns `{ ok, id, snapshot }` (flat `id` aliases `snapshot.id`).
|
|
771
|
+
- **`canvas_snapshot { action: "list" }`** / **`{ action: "gc", keep }`** / **`{ action: "delete", id }`** — manage saved snapshots.
|
|
772
|
+
- **`canvas_snapshot { action: "restore", id }`** — restore from a snapshot.
|
|
773
|
+
- **`canvas_snapshot { action: "diff", snapshot }`** — compare current canvas against a saved snapshot (added/removed/modified nodes & edges). Note the field is `snapshot` (id or name), not `id`.
|
|
772
774
|
|
|
773
775
|
### Canvas Management
|
|
774
776
|
|
|
775
|
-
**`canvas_view { action: "clear" }`** — Remove all nodes and edges. **Always `canvas_snapshot` first** —
|
|
777
|
+
**`canvas_view { action: "clear" }`** — Remove all nodes and edges. **Always `canvas_snapshot { action: "save" }` first** —
|
|
776
778
|
this is irreversible without a prior snapshot.
|
|
777
779
|
|
|
778
780
|
### Browser Automation (WebView)
|
|
@@ -1405,19 +1407,19 @@ Monitor ongoing processes:
|
|
|
1405
1407
|
|
|
1406
1408
|
Show two states side by side for the human to compare:
|
|
1407
1409
|
|
|
1408
|
-
1. Take a snapshot before changes: `canvas_snapshot` with name "before-X"
|
|
1410
|
+
1. Take a snapshot before changes: `canvas_snapshot { action: "save" }` with name "before-X"
|
|
1409
1411
|
2. Make changes to the canvas
|
|
1410
|
-
3. Use `
|
|
1412
|
+
3. Use `canvas_snapshot { action: "diff", snapshot }` to show what changed
|
|
1411
1413
|
4. Or: create two groups ("Before" and "After") with corresponding nodes
|
|
1412
1414
|
|
|
1413
1415
|
### Save and Start Fresh
|
|
1414
1416
|
|
|
1415
1417
|
When the human wants to explore a different approach without losing current work:
|
|
1416
1418
|
|
|
1417
|
-
1. **First**, save the current state: `canvas_snapshot` with a descriptive name
|
|
1419
|
+
1. **First**, save the current state: `canvas_snapshot { action: "save" }` with a descriptive name
|
|
1418
1420
|
2. **Then** clear: `canvas_view { action: "clear" }` (never clear without snapshotting first)
|
|
1419
1421
|
3. Set up the new workspace with initial nodes
|
|
1420
|
-
4. Tell the human the snapshot name and that `
|
|
1422
|
+
4. Tell the human the snapshot name and that `canvas_snapshot { action: "restore" }` can bring everything back
|
|
1421
1423
|
|
|
1422
1424
|
## Best Practices
|
|
1423
1425
|
|
|
@@ -193,7 +193,7 @@ cmd('snapshot delete', 'Delete a saved snapshot', ['pmx-canvas snapshot delete <
|
|
|
193
193
|
});
|
|
194
194
|
|
|
195
195
|
async function runSnapshotDiff(args: string[]): Promise<void> {
|
|
196
|
-
const { positional
|
|
196
|
+
const { positional } = parseFlags(args);
|
|
197
197
|
const snapshot = positional[0];
|
|
198
198
|
if (!snapshot) die('Missing snapshot ID or name', 'pmx-canvas snapshot diff <snapshot-id-or-name>');
|
|
199
199
|
const result = await invokeOperation('snapshot.diff', { id: snapshot });
|
|
@@ -830,6 +830,7 @@ cmd(
|
|
|
830
830
|
'pmx-canvas node update <node-id> --dock-position right',
|
|
831
831
|
'pmx-canvas node update <node-id> --dock-position none # undock back to the canvas',
|
|
832
832
|
'pmx-canvas node update <node-id> --lock-arrange',
|
|
833
|
+
'pmx-canvas node update <node-id> --width 840 --full # full node payload instead of the compact envelope',
|
|
833
834
|
],
|
|
834
835
|
async (args) => {
|
|
835
836
|
const { positional, flags } = parseFlags(args);
|
|
@@ -932,7 +933,36 @@ cmd(
|
|
|
932
933
|
}
|
|
933
934
|
|
|
934
935
|
const result = await invokeOperation('node.update', { id, ...body });
|
|
935
|
-
|
|
936
|
+
if (flags.full || flags.verbose) {
|
|
937
|
+
output(result);
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
// Finding R (0.3.4 report): the raw update result echoes the FULL node —
|
|
941
|
+
// ~940 KB for an ext-app (bundled html + tool result) — which can consume a
|
|
942
|
+
// large share of an agent turn for a simple resize. Default to a compact
|
|
943
|
+
// envelope; --full restores the complete response.
|
|
944
|
+
const updatedNode = isRecord(result) && isRecord(result.node) ? result.node : undefined;
|
|
945
|
+
// Raw geometry flags also land in the body alongside their structured
|
|
946
|
+
// position/size forms (the graph-patch builder copies them through); report
|
|
947
|
+
// only the canonical field once.
|
|
948
|
+
const structuredDupes: Record<string, string> = {
|
|
949
|
+
width: 'size',
|
|
950
|
+
height: 'size',
|
|
951
|
+
nodeHeight: 'size',
|
|
952
|
+
x: 'position',
|
|
953
|
+
y: 'position',
|
|
954
|
+
};
|
|
955
|
+
const updatedFields = Object.keys(body)
|
|
956
|
+
.filter((key) => !(structuredDupes[key] && body[structuredDupes[key]] !== undefined))
|
|
957
|
+
.sort();
|
|
958
|
+
output({
|
|
959
|
+
ok: isRecord(result) && result.ok !== undefined ? result.ok : true,
|
|
960
|
+
id,
|
|
961
|
+
nodeId: id,
|
|
962
|
+
...(updatedNode && isRecord(updatedNode.position) ? { position: updatedNode.position } : {}),
|
|
963
|
+
...(updatedNode && isRecord(updatedNode.size) ? { size: updatedNode.size } : {}),
|
|
964
|
+
updatedFields,
|
|
965
|
+
});
|
|
936
966
|
},
|
|
937
967
|
);
|
|
938
968
|
|
package/src/cli/index.ts
CHANGED
|
@@ -163,7 +163,10 @@ function runMcpServerProcess(): Promise<void> {
|
|
|
163
163
|
const serveSubcommand = firstArg === 'serve' ? (args[1] ?? '') : '';
|
|
164
164
|
|
|
165
165
|
if (firstArg === 'serve' && (serveSubcommand === 'status' || serveSubcommand === 'stop')) {
|
|
166
|
-
const port = parseInt(
|
|
166
|
+
const port = parseInt(
|
|
167
|
+
readOption('port') ?? process.env.PMX_WEB_CANVAS_PORT ?? process.env.PMX_CANVAS_PORT ?? '4313',
|
|
168
|
+
10,
|
|
169
|
+
);
|
|
167
170
|
const daemonLogFile = resolve(readOption('log-file') ?? `.pmx-canvas/daemon-${port}.log`);
|
|
168
171
|
const daemonPidFile = resolve(readOption('pid-file') ?? `.pmx-canvas/daemon-${port}.pid`);
|
|
169
172
|
const daemonWaitMs = readNumberOption('wait-ms') ?? 10_000;
|
|
@@ -208,7 +211,10 @@ if (AGENT_COMMANDS.has(routedCommand) && routedCommand !== 'serve') {
|
|
|
208
211
|
await runMcpServerProcess();
|
|
209
212
|
} else {
|
|
210
213
|
// "serve" is also accessible via flags (backward compat)
|
|
211
|
-
const port = parseInt(
|
|
214
|
+
const port = parseInt(
|
|
215
|
+
readOption('port') ?? process.env.PMX_WEB_CANVAS_PORT ?? process.env.PMX_CANVAS_PORT ?? '4313',
|
|
216
|
+
10,
|
|
217
|
+
);
|
|
212
218
|
const demo = hasFlag('demo');
|
|
213
219
|
const noOpen = hasFlag('no-open');
|
|
214
220
|
const daemon = hasFlag('daemon');
|
package/src/cli/shared.ts
CHANGED
package/src/client/index.tsx
CHANGED
|
@@ -253,6 +253,19 @@ window.parent.postMessage({ source: '${EXT_APP_BOOT_BEACON_SOURCE}', token: ${JS
|
|
|
253
253
|
</script>`;
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
+
/**
|
|
257
|
+
* Base styles injected into every ext-app document (Finding Q, 0.3.4 report):
|
|
258
|
+
* apps aspect-fit their drawn surface to the frame width, and on narrow/tall
|
|
259
|
+
* tiles the leftover region shows the app document's own body background —
|
|
260
|
+
* pure black for the hosted Excalidraw bundle, in every engine. Making the
|
|
261
|
+
* app document transparent lets the host iframe's theme background
|
|
262
|
+
* (var(--c-panel)) show through the letterbox instead. Background only — app
|
|
263
|
+
* content, its drawn scene, and its own foreground styling are untouched.
|
|
264
|
+
*/
|
|
265
|
+
export function buildExtAppSurfaceBaseStyles(): string {
|
|
266
|
+
return '<style data-pmx-canvas-ext-app-base>html, body { background: transparent !important; }</style>';
|
|
267
|
+
}
|
|
268
|
+
|
|
256
269
|
export function injectExtAppAxBridgeScript(html: string, axBridgeScript: string): string {
|
|
257
270
|
if (!axBridgeScript) return html;
|
|
258
271
|
const headMatch = /<head\b[^>]*>/i.exec(html);
|
|
@@ -359,7 +372,7 @@ export function ExtAppFrame({ node, expanded = false }: { node: CanvasNodeState;
|
|
|
359
372
|
const bootBeaconScript =
|
|
360
373
|
typeof html === 'string' && html.length > 0 ? buildExtAppBootBeaconScript(axToken, nodeId) : '';
|
|
361
374
|
const iframeDocument = useIframeDocument(
|
|
362
|
-
injectExtAppAxBridgeScript(html ?? '', bootBeaconScript + axBridgeScript),
|
|
375
|
+
injectExtAppAxBridgeScript(html ?? '', buildExtAppSurfaceBaseStyles() + bootBeaconScript + axBridgeScript),
|
|
363
376
|
iframeSandbox,
|
|
364
377
|
);
|
|
365
378
|
|
|
@@ -688,7 +701,9 @@ export function ExtAppFrame({ node, expanded = false }: { node: CanvasNodeState;
|
|
|
688
701
|
|
|
689
702
|
bridge.onsandboxready = async () => {
|
|
690
703
|
await bridge.sendSandboxResourceReady({
|
|
691
|
-
|
|
704
|
+
// Same Finding Q base styles for the sandbox-proxy path: the inner
|
|
705
|
+
// resource document is the one that letterboxes on narrow tiles.
|
|
706
|
+
html: injectExtAppAxBridgeScript(html, buildExtAppSurfaceBaseStyles()),
|
|
692
707
|
sandbox: DEFAULT_EXT_APP_SANDBOX,
|
|
693
708
|
...(resourceMeta?.csp ? { csp: resourceMeta.csp } : {}),
|
|
694
709
|
...(resourceMeta?.permissions ? { permissions: resourceMeta.permissions } : {}),
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { CanvasNodeState } from '../types';
|
|
2
|
+
|
|
3
|
+
// ── Client node-creation defaults (plan-009 H5, client half) ──
|
|
4
|
+
// Single source of truth for the frame (position/size/zIndex/flags) a NEW
|
|
5
|
+
// client-created node gets when its creating event carries no explicit
|
|
6
|
+
// geometry. Extracted from sse-bridge, which kept one `makeNode` helper plus
|
|
7
|
+
// three inline node literals (ext-app open, prompt, response) duplicating
|
|
8
|
+
// these defaults.
|
|
9
|
+
//
|
|
10
|
+
// Divergences found during extraction — documented, NOT silently unified:
|
|
11
|
+
// - `canvas-store.ts` holds no node-creation defaults today. Human-created
|
|
12
|
+
// nodes go through `createNodeFromClient` (intent-bridge → POST
|
|
13
|
+
// /api/canvas/node) and get the SERVER's `defaultNodeSize`
|
|
14
|
+
// (src/server/operations/ops/nodes.ts), which differs from this table for
|
|
15
|
+
// most types (e.g. its fallback is 360x200). The server half of H5
|
|
16
|
+
// (`syncEventToCanvasState`, `ensureDefaultDockedNodes`) is deliberately
|
|
17
|
+
// deferred — do not "fix" that mismatch from this file.
|
|
18
|
+
// - The server seeds `status-main`/`context-main` docked AND collapsed
|
|
19
|
+
// (`ensureDefaultDockedNodes` in src/server/server.ts); the SSE bridge
|
|
20
|
+
// creates the same ids expanded, with `context-main` undocked. Client
|
|
21
|
+
// behavior is preserved as-is.
|
|
22
|
+
// - sse-bridge's prompt literal hardcoded `height: 400` beside a table entry
|
|
23
|
+
// of the same value; both now read the table (no rendering change).
|
|
24
|
+
|
|
25
|
+
export const DEFAULT_POSITIONS: Record<CanvasNodeState['type'], { x: number; y: number; w: number; h: number }> &
|
|
26
|
+
Record<'prompt' | 'response', { x: number; y: number; w: number; h: number }> = {
|
|
27
|
+
status: { x: 40, y: 80, w: 300, h: 120 },
|
|
28
|
+
markdown: { x: 380, y: 80, w: 720, h: 600 },
|
|
29
|
+
context: { x: 1130, y: 80, w: 320, h: 400 },
|
|
30
|
+
'mcp-app': { x: 380, y: 720, w: 960, h: 600 },
|
|
31
|
+
webpage: { x: 380, y: 80, w: 520, h: 420 },
|
|
32
|
+
'json-render': { x: 380, y: 720, w: 840, h: 620 },
|
|
33
|
+
graph: { x: 380, y: 720, w: 760, h: 520 },
|
|
34
|
+
ledger: { x: 1130, y: 520, w: 320, h: 280 },
|
|
35
|
+
trace: { x: 40, y: 900, w: 200, h: 56 },
|
|
36
|
+
file: { x: 380, y: 80, w: 720, h: 600 },
|
|
37
|
+
image: { x: 380, y: 80, w: 720, h: 520 },
|
|
38
|
+
html: { x: 380, y: 80, w: 720, h: 640 },
|
|
39
|
+
group: { x: 220, y: 60, w: 840, h: 560 },
|
|
40
|
+
prompt: { x: 380, y: 1260, w: 520, h: 400 },
|
|
41
|
+
response: { x: 380, y: 1480, w: 720, h: 400 },
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Build a canvas node with the shared client defaults. `position`/`size`
|
|
46
|
+
* default to the type's DEFAULT_POSITIONS entry; callers that compute
|
|
47
|
+
* placement (auto-placement, event-supplied geometry) pass explicit values.
|
|
48
|
+
* Status nodes sit at zIndex 0 (background chrome), everything else at 1.
|
|
49
|
+
*/
|
|
50
|
+
export function makeNodeState(
|
|
51
|
+
id: string,
|
|
52
|
+
type: CanvasNodeState['type'],
|
|
53
|
+
data: Record<string, unknown>,
|
|
54
|
+
options: {
|
|
55
|
+
position?: { x: number; y: number };
|
|
56
|
+
size?: { width: number; height: number };
|
|
57
|
+
dockPosition?: 'left' | 'right' | null;
|
|
58
|
+
} = {},
|
|
59
|
+
): CanvasNodeState {
|
|
60
|
+
const defaults = DEFAULT_POSITIONS[type];
|
|
61
|
+
return {
|
|
62
|
+
id,
|
|
63
|
+
type,
|
|
64
|
+
position: options.position ?? { x: defaults.x, y: defaults.y },
|
|
65
|
+
size: options.size ?? { width: defaults.w, height: defaults.h },
|
|
66
|
+
zIndex: type === 'status' ? 0 : 1,
|
|
67
|
+
collapsed: false,
|
|
68
|
+
pinned: false,
|
|
69
|
+
dockPosition: options.dockPosition ?? null,
|
|
70
|
+
data,
|
|
71
|
+
};
|
|
72
|
+
}
|