pmx-canvas 0.4.7 → 0.5.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/.github/extensions/pmx-canvas/extension.mjs +154 -5
- package/.github/extensions/pmx-canvas/steering-delivery.mjs +79 -0
- package/CHANGELOG.md +156 -0
- package/Readme.md +72 -23
- package/dist/canvas/global.css +3069 -796
- package/dist/canvas/index.js +104 -104
- package/dist/json-render/index.css +1 -1
- package/dist/types/cli/agent.d.ts +1 -0
- package/dist/types/cli/commands/pump.d.ts +9 -0
- package/dist/types/client/App.d.ts +1 -1
- package/dist/types/client/canvas/AgentPresenceLayer.d.ts +11 -0
- package/dist/types/client/canvas/BarHint.d.ts +22 -0
- package/dist/types/client/canvas/CanvasNode.d.ts +5 -0
- package/dist/types/client/canvas/CanvasViewport.d.ts +4 -4
- package/dist/types/client/canvas/CommandBar.d.ts +10 -0
- package/dist/types/client/canvas/ConnectionBanner.d.ts +13 -0
- package/dist/types/client/canvas/ContextMenu.d.ts +6 -0
- package/dist/types/client/canvas/EdgeLayer.d.ts +2 -1
- package/dist/types/client/canvas/EmptyState.d.ts +11 -0
- package/dist/types/client/canvas/ExternalWriters.d.ts +5 -0
- package/dist/types/client/canvas/HumanPresenceLayer.d.ts +8 -0
- package/dist/types/client/canvas/Minimap.d.ts +12 -1
- package/dist/types/client/canvas/ScopeFenceLayer.d.ts +8 -0
- package/dist/types/client/canvas/SelectionBar.d.ts +6 -0
- package/dist/types/client/canvas/SessionPanel.d.ts +1 -0
- package/dist/types/client/canvas/SessionReceipt.d.ts +17 -0
- package/dist/types/client/canvas/TextPrompt.d.ts +25 -0
- package/dist/types/client/canvas/ToolRail.d.ts +21 -0
- package/dist/types/client/canvas/TopBar.d.ts +7 -0
- package/dist/types/client/canvas/canvas-area.d.ts +25 -0
- package/dist/types/client/canvas/create-in-view.d.ts +18 -0
- package/dist/types/client/canvas/import-files.d.ts +8 -0
- package/dist/types/client/canvas/kind-colors.d.ts +3 -0
- package/dist/types/client/canvas/use-focus-trap.d.ts +14 -0
- package/dist/types/client/canvas/use-now.d.ts +6 -0
- package/dist/types/client/icons.d.ts +11 -4
- package/dist/types/client/nodes/ExtAppFrame.d.ts +32 -0
- package/dist/types/client/nodes/GroupNode.d.ts +6 -1
- package/dist/types/client/nodes/MarkdownNode.d.ts +2 -0
- package/dist/types/client/nodes/inline-editor-commands.d.ts +3 -4
- package/dist/types/client/state/canvas-store.d.ts +49 -5
- package/dist/types/client/state/human-store.d.ts +26 -0
- package/dist/types/client/state/iframe-mode.d.ts +1 -0
- package/dist/types/client/state/intent-bridge.d.ts +37 -5
- package/dist/types/client/state/node-factory.d.ts +0 -1
- package/dist/types/client/state/presence-store.d.ts +78 -0
- package/dist/types/client/state/session-store.d.ts +176 -0
- package/dist/types/client/types.d.ts +2 -1
- package/dist/types/client/utils/platform.d.ts +2 -0
- package/dist/types/server/agent-presence.d.ts +192 -0
- package/dist/types/server/ax-gate-ttl.d.ts +6 -0
- package/dist/types/server/ax-interaction.d.ts +4 -10
- package/dist/types/server/ax-state-manager.d.ts +21 -2
- package/dist/types/server/ax-state.d.ts +49 -3
- package/dist/types/server/ax-wait.d.ts +13 -0
- package/dist/types/server/canvas-db.d.ts +23 -1
- package/dist/types/server/canvas-operations.d.ts +1 -0
- package/dist/types/server/canvas-state.d.ts +27 -13
- package/dist/types/server/canvas-validation.d.ts +0 -14
- package/dist/types/server/html-surface.d.ts +1 -1
- package/dist/types/server/human-presence.d.ts +40 -0
- package/dist/types/server/index.d.ts +29 -3
- package/dist/types/server/intent-registry.d.ts +7 -0
- package/dist/types/server/mutation-history.d.ts +15 -2
- package/dist/types/server/operations/invoker.d.ts +13 -2
- package/dist/types/server/operations/ops/ax-presence.d.ts +2 -0
- package/dist/types/server/operations/ops/human-presence.d.ts +3 -0
- package/dist/types/server/operations/registry.d.ts +11 -0
- package/dist/types/server/operations/types.d.ts +1 -1
- package/dist/types/server/scope-fence.d.ts +62 -0
- package/dist/types/server/server.d.ts +0 -12
- package/dist/types/shared/agent-presence.d.ts +130 -0
- package/dist/types/shared/approval-gates.d.ts +19 -0
- package/dist/types/shared/auto-arrange.d.ts +0 -1
- package/dist/types/shared/ax-kinds.d.ts +11 -0
- package/dist/types/shared/human-presence.d.ts +31 -0
- package/dist/types/shared/iframe-probe.d.ts +8 -0
- package/dist/types/shared/scope-fence.d.ts +21 -0
- package/docs/RELEASE.md +54 -0
- package/docs/ax-host-adapter-contract.md +30 -14
- package/docs/cli.md +37 -1
- package/docs/environment.md +3 -0
- package/docs/http-api.md +222 -5
- package/docs/mcp.md +6 -5
- package/docs/node-types.md +3 -1
- package/docs/plans/plan-009-tech-debt-backlog.md +13 -0
- package/docs/screenshot.png +0 -0
- package/docs/screenshots/multi-agent-live.png +0 -0
- package/docs/screenshots/workbench-live-dark.png +0 -0
- package/docs/screenshots/workbench-live-light.png +0 -0
- package/docs/sdk.md +1 -1
- package/package.json +1 -1
- package/skills/pmx-canvas/SKILL.md +109 -18
- package/skills/pmx-canvas/references/codex-app-adapter.md +28 -0
- package/skills/pmx-canvas/references/full-reference.md +55 -16
- package/skills/pmx-canvas/references/github-copilot-app-adapter.md +41 -24
- package/skills/pmx-canvas-orchestration/SKILL.md +56 -2
- package/skills/pmx-canvas-testing/SKILL.md +57 -0
- package/src/cli/agent.ts +1 -0
- package/src/cli/commands/copilot.ts +15 -8
- package/src/cli/commands/nodes.ts +1 -19
- package/src/cli/commands/pump.ts +252 -0
- package/src/cli/index.ts +1 -5
- package/src/cli/shared.ts +1 -1
- package/src/client/App.tsx +209 -585
- package/src/client/canvas/AgentPresenceLayer.tsx +74 -0
- package/src/client/canvas/AttentionHistory.tsx +0 -10
- package/src/client/canvas/BarHint.tsx +49 -0
- package/src/client/canvas/CanvasNode.tsx +488 -136
- package/src/client/canvas/CanvasViewport.tsx +190 -94
- package/src/client/canvas/CommandBar.tsx +140 -0
- package/src/client/canvas/CommandPalette.tsx +133 -58
- package/src/client/canvas/ConnectionBanner.tsx +65 -0
- package/src/client/canvas/ContextMenu.tsx +88 -27
- package/src/client/canvas/ContextPinBar.tsx +2 -2
- package/src/client/canvas/EdgeLayer.tsx +121 -38
- package/src/client/canvas/EmptyState.tsx +93 -0
- package/src/client/canvas/ExpandedNodeOverlay.tsx +64 -118
- package/src/client/canvas/ExternalWriters.tsx +308 -0
- package/src/client/canvas/FocusFieldLayer.tsx +12 -1
- package/src/client/canvas/HumanPresenceLayer.tsx +45 -0
- package/src/client/canvas/IntentLayer.tsx +1 -1
- package/src/client/canvas/Minimap.tsx +117 -190
- package/src/client/canvas/ScopeFenceLayer.tsx +38 -0
- package/src/client/canvas/SelectionBar.tsx +175 -23
- package/src/client/canvas/SessionPanel.tsx +481 -0
- package/src/client/canvas/SessionReceipt.tsx +141 -0
- package/src/client/canvas/ShortcutOverlay.tsx +21 -6
- package/src/client/canvas/SnapshotPanel.tsx +166 -80
- package/src/client/canvas/TextPrompt.tsx +128 -0
- package/src/client/canvas/ToolRail.tsx +447 -0
- package/src/client/canvas/TopBar.tsx +308 -0
- package/src/client/canvas/auto-fit.ts +1 -5
- package/src/client/canvas/canvas-area.ts +41 -0
- package/src/client/canvas/create-in-view.ts +48 -0
- package/src/client/canvas/import-files.ts +64 -0
- package/src/client/canvas/kind-colors.ts +22 -0
- package/src/client/canvas/snap-guides.ts +1 -1
- package/src/client/canvas/use-focus-trap.ts +54 -0
- package/src/client/canvas/use-now.ts +17 -0
- package/src/client/canvas/use-pan-zoom.ts +13 -2
- package/src/client/icons.tsx +57 -18
- package/src/client/nodes/ExtAppFrame.tsx +79 -2
- package/src/client/nodes/GroupNode.tsx +9 -28
- package/src/client/nodes/InlineFormatBar.tsx +4 -3
- package/src/client/nodes/InlineMarkdownEditor.tsx +27 -0
- package/src/client/nodes/MarkdownNode.tsx +59 -24
- package/src/client/nodes/MdFormatBar.tsx +2 -4
- package/src/client/nodes/inline-editor-commands.ts +17 -6
- package/src/client/nodes/use-surface-frame.ts +3 -1
- package/src/client/state/attention-bridge.ts +3 -0
- package/src/client/state/canvas-store.ts +273 -84
- package/src/client/state/human-store.ts +175 -0
- package/src/client/state/iframe-mode.ts +33 -9
- package/src/client/state/intent-bridge.ts +75 -13
- package/src/client/state/node-factory.ts +2 -8
- package/src/client/state/presence-store.ts +168 -0
- package/src/client/state/session-store.ts +419 -0
- package/src/client/state/sse-bridge.ts +93 -30
- package/src/client/theme/global.css +3069 -796
- package/src/client/types.ts +3 -1
- package/src/client/utils/platform.ts +6 -1
- package/src/mcp/canvas-access.ts +2 -2
- package/src/server/agent-presence.ts +755 -0
- package/src/server/ax-context.ts +45 -0
- package/src/server/ax-gate-ttl.ts +61 -0
- package/src/server/ax-interaction.ts +29 -1
- package/src/server/ax-state-manager.ts +78 -6
- package/src/server/ax-state.ts +82 -15
- package/src/server/ax-wait.ts +33 -0
- package/src/server/canvas-db.ts +237 -61
- package/src/server/canvas-operations.ts +17 -8
- package/src/server/canvas-state.ts +131 -58
- package/src/server/canvas-validation.ts +3 -53
- package/src/server/demo-state.json +2 -70
- package/src/server/html-surface.ts +11 -2
- package/src/server/human-presence.ts +160 -0
- package/src/server/index.ts +107 -4
- package/src/server/intent-registry.ts +13 -0
- package/src/server/mutation-history.ts +47 -2
- package/src/server/operations/composites.ts +6 -3
- package/src/server/operations/http.ts +10 -1
- package/src/server/operations/index.ts +4 -0
- package/src/server/operations/invoker.ts +33 -6
- package/src/server/operations/ops/ax-presence.ts +62 -0
- package/src/server/operations/ops/ax-read.ts +8 -0
- package/src/server/operations/ops/ax-state.ts +106 -1
- package/src/server/operations/ops/ax-timeline.ts +54 -5
- package/src/server/operations/ops/ax-work.ts +46 -0
- package/src/server/operations/ops/batch.ts +3 -1
- package/src/server/operations/ops/edges.ts +88 -2
- package/src/server/operations/ops/groups.ts +4 -3
- package/src/server/operations/ops/human-presence.ts +35 -0
- package/src/server/operations/ops/json-render.ts +11 -1
- package/src/server/operations/ops/nodes.ts +65 -16
- package/src/server/operations/ops/query.ts +1 -0
- package/src/server/operations/ops/snapshots.ts +16 -0
- package/src/server/operations/registry.ts +196 -1
- package/src/server/operations/types.ts +1 -1
- package/src/server/scope-fence.ts +207 -0
- package/src/server/server.ts +177 -79
- package/src/server/trace-manager.ts +0 -2
- package/src/server/web-artifacts.ts +0 -1
- package/src/shared/agent-presence.ts +162 -0
- package/src/shared/approval-gates.ts +35 -0
- package/src/shared/auto-arrange.ts +121 -13
- package/src/shared/ax-kinds.ts +23 -0
- package/src/shared/human-presence.ts +30 -0
- package/src/shared/iframe-probe.ts +8 -0
- package/src/shared/scope-fence.ts +31 -0
- package/dist/types/client/canvas/DockedNode.d.ts +0 -4
- package/src/client/canvas/DockedNode.tsx +0 -165
package/src/server/canvas-db.ts
CHANGED
|
@@ -65,7 +65,6 @@ const SCHEMA_SQL = `
|
|
|
65
65
|
z_index INTEGER NOT NULL DEFAULT 0,
|
|
66
66
|
collapsed INTEGER NOT NULL DEFAULT 0,
|
|
67
67
|
pinned INTEGER NOT NULL DEFAULT 0,
|
|
68
|
-
dock_position TEXT,
|
|
69
68
|
data TEXT NOT NULL
|
|
70
69
|
);
|
|
71
70
|
|
|
@@ -119,7 +118,6 @@ const SCHEMA_SQL = `
|
|
|
119
118
|
z_index INTEGER NOT NULL DEFAULT 0,
|
|
120
119
|
collapsed INTEGER NOT NULL DEFAULT 0,
|
|
121
120
|
pinned INTEGER NOT NULL DEFAULT 0,
|
|
122
|
-
dock_position TEXT,
|
|
123
121
|
data TEXT NOT NULL,
|
|
124
122
|
PRIMARY KEY (snapshot_id, id)
|
|
125
123
|
);
|
|
@@ -209,6 +207,13 @@ const SCHEMA_SQL = `
|
|
|
209
207
|
);
|
|
210
208
|
CREATE INDEX IF NOT EXISTS idx_ax_steering_seq ON ax_steering (seq);
|
|
211
209
|
|
|
210
|
+
CREATE TABLE IF NOT EXISTS ax_steering_deliveries (
|
|
211
|
+
steering_id TEXT NOT NULL,
|
|
212
|
+
consumer TEXT NOT NULL,
|
|
213
|
+
at TEXT NOT NULL,
|
|
214
|
+
PRIMARY KEY (steering_id, consumer)
|
|
215
|
+
);
|
|
216
|
+
|
|
212
217
|
CREATE TABLE IF NOT EXISTS ax_host_capabilities (
|
|
213
218
|
host TEXT PRIMARY KEY,
|
|
214
219
|
reported_at TEXT NOT NULL,
|
|
@@ -273,7 +278,13 @@ export function openCanvasDb(dbPath: string): Database {
|
|
|
273
278
|
|
|
274
279
|
const db = new Database(dbPath);
|
|
275
280
|
db.exec('PRAGMA journal_mode=WAL');
|
|
276
|
-
|
|
281
|
+
// FULL, not NORMAL: under WAL, NORMAL lets a host/OS-level crash lose the most
|
|
282
|
+
// recent commits (a clean process exit loses nothing either way). The reporter of
|
|
283
|
+
// issue #22 declined FULL specifically because a save rewrote the whole board, so
|
|
284
|
+
// an fsync per commit scaled with board size. Saves are incremental now — a
|
|
285
|
+
// typical save is a row or two — so that objection is gone and the durability is
|
|
286
|
+
// worth the fsync.
|
|
287
|
+
db.exec('PRAGMA synchronous=FULL');
|
|
277
288
|
db.exec('PRAGMA busy_timeout=5000');
|
|
278
289
|
db.exec(SCHEMA_SQL);
|
|
279
290
|
|
|
@@ -295,38 +306,116 @@ export function checkpointCanvasDb(db: Database): void {
|
|
|
295
306
|
db.exec('PRAGMA wal_checkpoint(TRUNCATE)');
|
|
296
307
|
}
|
|
297
308
|
|
|
309
|
+
/**
|
|
310
|
+
* Delete blob rows nothing references any more. Blobs are content-addressed and
|
|
311
|
+
* written with INSERT OR IGNORE, so editing a node's html supersedes its old blob
|
|
312
|
+
* and leaves it orphaned — without this, `canvas.db` (which is git-committable)
|
|
313
|
+
* would grow without bound once externalization applies to ordinary html nodes.
|
|
314
|
+
*
|
|
315
|
+
* References are collected by scanning the raw persisted JSON for sha256 fields
|
|
316
|
+
* rather than parsing every node. That deliberately OVER-approximates: a stray
|
|
317
|
+
* 64-hex string keeps a blob alive that could have been dropped, which wastes a
|
|
318
|
+
* little space. The opposite error would delete live content, so the scan is
|
|
319
|
+
* biased to the safe side on purpose. Snapshot rows are scanned too — a snapshot
|
|
320
|
+
* restore must still find its blobs.
|
|
321
|
+
*/
|
|
322
|
+
export function gcBlobsInDB(db: Database): number {
|
|
323
|
+
const referenced = new Set<string>();
|
|
324
|
+
const collect = (sql: string): void => {
|
|
325
|
+
for (const row of db.query<{ data: string }, []>(sql).all()) {
|
|
326
|
+
if (!row.data) continue;
|
|
327
|
+
for (const match of row.data.matchAll(/"sha256"\s*:\s*"([0-9a-f]{64})"/g)) referenced.add(match[1]);
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
collect('SELECT data FROM nodes');
|
|
331
|
+
collect('SELECT data FROM snapshot_nodes');
|
|
332
|
+
|
|
333
|
+
const all = db.query<{ sha256: string }, []>('SELECT sha256 FROM blobs').all();
|
|
334
|
+
const orphans = all.filter((row) => !referenced.has(row.sha256));
|
|
335
|
+
if (orphans.length === 0) return 0;
|
|
336
|
+
const remove = db.prepare('DELETE FROM blobs WHERE sha256 = ?');
|
|
337
|
+
const transaction = db.transaction(() => {
|
|
338
|
+
for (const row of orphans) remove.run(row.sha256);
|
|
339
|
+
});
|
|
340
|
+
transaction();
|
|
341
|
+
return orphans.length;
|
|
342
|
+
}
|
|
343
|
+
|
|
298
344
|
export function finalizeCanvasDbForClose(db: Database): void {
|
|
299
345
|
checkpointCanvasDb(db);
|
|
300
|
-
|
|
346
|
+
// The WAL→DELETE switch takes an exclusive lock, so it succeeds only when no
|
|
347
|
+
// other connection has the DB open — and that makes it the GC gate. GC deletes
|
|
348
|
+
// blob rows a second live process may still depend on: its in-memory nodes
|
|
349
|
+
// hold content-addressed refs, not content, so a GC'd blob is unrecoverable
|
|
350
|
+
// once that process re-saves (review of #22, reproduced). Locked ⇒ someone
|
|
351
|
+
// else is on this canvas: leave WAL in place and keep every blob.
|
|
352
|
+
try {
|
|
353
|
+
db.exec('PRAGMA journal_mode=DELETE');
|
|
354
|
+
} catch {
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
gcBlobsInDB(db);
|
|
301
358
|
}
|
|
302
359
|
|
|
303
360
|
// ── State Persistence ───────────────────────────────────────────
|
|
304
361
|
|
|
362
|
+
/**
|
|
363
|
+
* Drop rows whose id is no longer in the live state. Reads only the id column
|
|
364
|
+
* (index-only scan) so it stays cheap on large boards.
|
|
365
|
+
*/
|
|
366
|
+
function deleteMissingRows(
|
|
367
|
+
db: Database,
|
|
368
|
+
table: 'nodes' | 'edges' | 'annotations' | 'context_pins',
|
|
369
|
+
idColumn: 'id' | 'node_id',
|
|
370
|
+
keep: Set<string>,
|
|
371
|
+
): void {
|
|
372
|
+
const rows = db.query<{ id: string }, []>(`SELECT ${idColumn} AS id FROM ${table}`).all();
|
|
373
|
+
if (rows.length === 0) return;
|
|
374
|
+
const remove = db.prepare(`DELETE FROM ${table} WHERE ${idColumn} = ?`);
|
|
375
|
+
for (const row of rows) {
|
|
376
|
+
if (!keep.has(row.id)) remove.run(row.id);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
305
380
|
export function saveStateToDB(db: Database, state: PersistedCanvasState): void {
|
|
306
381
|
const transaction = db.transaction(() => {
|
|
307
|
-
//
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
//
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
382
|
+
// Rows are upserted only when a column actually differs, and rows that are
|
|
383
|
+
// gone are deleted by id. This used to be DELETE-all + INSERT-all, which
|
|
384
|
+
// rewrote every row on every debounced save: dragging one node rewrote the
|
|
385
|
+
// whole board, multi-MB html payloads included, so the write cost scaled
|
|
386
|
+
// with board size instead of change size (issue #22). The
|
|
387
|
+
// `WHERE ... IS NOT excluded....` guard makes an unchanged row a true
|
|
388
|
+
// no-op — SQLite performs no update, so no page is written. The comparison
|
|
389
|
+
// is against the DB itself rather than an in-memory cache, so nothing can
|
|
390
|
+
// go stale and silently skip a write that was actually needed.
|
|
391
|
+
const upsertMeta = db.prepare(
|
|
392
|
+
`INSERT INTO meta (key, value) VALUES (?, ?)
|
|
393
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value
|
|
394
|
+
WHERE value IS NOT excluded.value`,
|
|
395
|
+
);
|
|
396
|
+
upsertMeta.run('theme', normalizeCanvasTheme(state.theme));
|
|
397
|
+
upsertMeta.run('viewport_x', String(state.viewport.x));
|
|
398
|
+
upsertMeta.run('viewport_y', String(state.viewport.y));
|
|
399
|
+
upsertMeta.run('viewport_scale', String(state.viewport.scale));
|
|
320
400
|
// Mark DB as populated (for migration detection)
|
|
321
|
-
|
|
401
|
+
upsertMeta.run('state_populated', '1');
|
|
322
402
|
|
|
323
403
|
// Save nodes
|
|
324
|
-
const
|
|
325
|
-
`INSERT INTO nodes (id, type, pos_x, pos_y, width, height, z_index, collapsed, pinned,
|
|
326
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?,
|
|
404
|
+
const upsertNode = db.prepare(
|
|
405
|
+
`INSERT INTO nodes (id, type, pos_x, pos_y, width, height, z_index, collapsed, pinned, data)
|
|
406
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
407
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
408
|
+
type = excluded.type, pos_x = excluded.pos_x, pos_y = excluded.pos_y,
|
|
409
|
+
width = excluded.width, height = excluded.height, z_index = excluded.z_index,
|
|
410
|
+
collapsed = excluded.collapsed, pinned = excluded.pinned, data = excluded.data
|
|
411
|
+
WHERE type IS NOT excluded.type OR pos_x IS NOT excluded.pos_x
|
|
412
|
+
OR pos_y IS NOT excluded.pos_y OR width IS NOT excluded.width
|
|
413
|
+
OR height IS NOT excluded.height OR z_index IS NOT excluded.z_index
|
|
414
|
+
OR collapsed IS NOT excluded.collapsed OR pinned IS NOT excluded.pinned
|
|
415
|
+
OR data IS NOT excluded.data`,
|
|
327
416
|
);
|
|
328
417
|
for (const node of state.nodes) {
|
|
329
|
-
|
|
418
|
+
upsertNode.run(
|
|
330
419
|
node.id,
|
|
331
420
|
node.type,
|
|
332
421
|
node.position.x,
|
|
@@ -336,18 +425,24 @@ export function saveStateToDB(db: Database, state: PersistedCanvasState): void {
|
|
|
336
425
|
node.zIndex,
|
|
337
426
|
node.collapsed ? 1 : 0,
|
|
338
427
|
node.pinned ? 1 : 0,
|
|
339
|
-
node.dockPosition,
|
|
340
428
|
JSON.stringify(node.data),
|
|
341
429
|
);
|
|
342
430
|
}
|
|
431
|
+
deleteMissingRows(db, 'nodes', 'id', new Set(state.nodes.map((node) => node.id)));
|
|
343
432
|
|
|
344
433
|
// Save edges
|
|
345
|
-
const
|
|
434
|
+
const upsertEdge = db.prepare(
|
|
346
435
|
`INSERT INTO edges (id, from_node, to_node, type, label, style, animated)
|
|
347
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
436
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
437
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
438
|
+
from_node = excluded.from_node, to_node = excluded.to_node, type = excluded.type,
|
|
439
|
+
label = excluded.label, style = excluded.style, animated = excluded.animated
|
|
440
|
+
WHERE from_node IS NOT excluded.from_node OR to_node IS NOT excluded.to_node
|
|
441
|
+
OR type IS NOT excluded.type OR label IS NOT excluded.label
|
|
442
|
+
OR style IS NOT excluded.style OR animated IS NOT excluded.animated`,
|
|
348
443
|
);
|
|
349
444
|
for (const edge of state.edges) {
|
|
350
|
-
|
|
445
|
+
upsertEdge.run(
|
|
351
446
|
edge.id,
|
|
352
447
|
edge.from,
|
|
353
448
|
edge.to,
|
|
@@ -357,14 +452,24 @@ export function saveStateToDB(db: Database, state: PersistedCanvasState): void {
|
|
|
357
452
|
edge.animated ? 1 : 0,
|
|
358
453
|
);
|
|
359
454
|
}
|
|
455
|
+
deleteMissingRows(db, 'edges', 'id', new Set(state.edges.map((edge) => edge.id)));
|
|
360
456
|
|
|
361
457
|
// Save annotations
|
|
362
|
-
const
|
|
458
|
+
const annotations = state.annotations ?? [];
|
|
459
|
+
const upsertAnnotation = db.prepare(
|
|
363
460
|
`INSERT INTO annotations (id, type, points, bounds, color, width, text, label, created_at)
|
|
364
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
461
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
462
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
463
|
+
type = excluded.type, points = excluded.points, bounds = excluded.bounds,
|
|
464
|
+
color = excluded.color, width = excluded.width, text = excluded.text,
|
|
465
|
+
label = excluded.label, created_at = excluded.created_at
|
|
466
|
+
WHERE type IS NOT excluded.type OR points IS NOT excluded.points
|
|
467
|
+
OR bounds IS NOT excluded.bounds OR color IS NOT excluded.color
|
|
468
|
+
OR width IS NOT excluded.width OR text IS NOT excluded.text
|
|
469
|
+
OR label IS NOT excluded.label OR created_at IS NOT excluded.created_at`,
|
|
365
470
|
);
|
|
366
|
-
for (const annotation of
|
|
367
|
-
|
|
471
|
+
for (const annotation of annotations) {
|
|
472
|
+
upsertAnnotation.run(
|
|
368
473
|
annotation.id,
|
|
369
474
|
annotation.type,
|
|
370
475
|
JSON.stringify(annotation.points),
|
|
@@ -376,17 +481,21 @@ export function saveStateToDB(db: Database, state: PersistedCanvasState): void {
|
|
|
376
481
|
annotation.createdAt,
|
|
377
482
|
);
|
|
378
483
|
}
|
|
484
|
+
deleteMissingRows(db, 'annotations', 'id', new Set(annotations.map((annotation) => annotation.id)));
|
|
379
485
|
|
|
380
|
-
// Save context pins
|
|
381
|
-
const insertPin = db.prepare('INSERT INTO context_pins (node_id) VALUES (?)');
|
|
486
|
+
// Save context pins (node_id is the whole row — nothing to update)
|
|
487
|
+
const insertPin = db.prepare('INSERT OR IGNORE INTO context_pins (node_id) VALUES (?)');
|
|
382
488
|
for (const pinId of state.contextPins) {
|
|
383
489
|
insertPin.run(pinId);
|
|
384
490
|
}
|
|
491
|
+
deleteMissingRows(db, 'context_pins', 'node_id', new Set(state.contextPins));
|
|
385
492
|
|
|
386
|
-
db.run(
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
493
|
+
db.run(
|
|
494
|
+
`INSERT INTO ax_state (key, value) VALUES (?, ?)
|
|
495
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value
|
|
496
|
+
WHERE value IS NOT excluded.value`,
|
|
497
|
+
['state', JSON.stringify(state.ax ?? createEmptyAxState())],
|
|
498
|
+
);
|
|
390
499
|
});
|
|
391
500
|
|
|
392
501
|
transaction();
|
|
@@ -429,7 +538,6 @@ export function loadStateFromDB(db: Database): PersistedCanvasState | null {
|
|
|
429
538
|
z_index: number;
|
|
430
539
|
collapsed: number;
|
|
431
540
|
pinned: number;
|
|
432
|
-
dock_position: string | null;
|
|
433
541
|
data: string;
|
|
434
542
|
}
|
|
435
543
|
const nodeRows = db.query<NodeRow, []>('SELECT * FROM nodes').all();
|
|
@@ -441,7 +549,6 @@ export function loadStateFromDB(db: Database): PersistedCanvasState | null {
|
|
|
441
549
|
zIndex: row.z_index,
|
|
442
550
|
collapsed: row.collapsed === 1,
|
|
443
551
|
pinned: row.pinned === 1,
|
|
444
|
-
dockPosition: row.dock_position as CanvasNodeState['dockPosition'],
|
|
445
552
|
data: JSON.parse(row.data) as Record<string, unknown>,
|
|
446
553
|
}));
|
|
447
554
|
|
|
@@ -549,8 +656,8 @@ export function saveSnapshotToDB(db: Database, snapshot: CanvasSnapshot, state:
|
|
|
549
656
|
|
|
550
657
|
// Insert snapshot nodes
|
|
551
658
|
const insertNode = db.prepare(
|
|
552
|
-
`INSERT INTO snapshot_nodes (snapshot_id, id, type, pos_x, pos_y, width, height, z_index, collapsed, pinned,
|
|
553
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
|
659
|
+
`INSERT INTO snapshot_nodes (snapshot_id, id, type, pos_x, pos_y, width, height, z_index, collapsed, pinned, data)
|
|
660
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
554
661
|
);
|
|
555
662
|
for (const node of state.nodes) {
|
|
556
663
|
insertNode.run(
|
|
@@ -564,7 +671,6 @@ export function saveSnapshotToDB(db: Database, snapshot: CanvasSnapshot, state:
|
|
|
564
671
|
node.zIndex,
|
|
565
672
|
node.collapsed ? 1 : 0,
|
|
566
673
|
node.pinned ? 1 : 0,
|
|
567
|
-
node.dockPosition,
|
|
568
674
|
JSON.stringify(node.data),
|
|
569
675
|
);
|
|
570
676
|
}
|
|
@@ -674,7 +780,6 @@ export function loadSnapshotFromDB(
|
|
|
674
780
|
z_index: number;
|
|
675
781
|
collapsed: number;
|
|
676
782
|
pinned: number;
|
|
677
|
-
dock_position: string | null;
|
|
678
783
|
data: string;
|
|
679
784
|
}
|
|
680
785
|
const nodeRows = db
|
|
@@ -688,7 +793,6 @@ export function loadSnapshotFromDB(
|
|
|
688
793
|
zIndex: row.z_index,
|
|
689
794
|
collapsed: row.collapsed === 1,
|
|
690
795
|
pinned: row.pinned === 1,
|
|
691
|
-
dockPosition: row.dock_position as CanvasNodeState['dockPosition'],
|
|
692
796
|
data: JSON.parse(row.data) as Record<string, unknown>,
|
|
693
797
|
}));
|
|
694
798
|
|
|
@@ -812,6 +916,10 @@ export function listSnapshotsFromDB(db: Database, options: CanvasSnapshotListOpt
|
|
|
812
916
|
}));
|
|
813
917
|
}
|
|
814
918
|
|
|
919
|
+
export function renameSnapshotInDB(db: Database, id: string, name: string): boolean {
|
|
920
|
+
return db.run('UPDATE snapshots SET name = ? WHERE id = ?', [name, id]).changes > 0;
|
|
921
|
+
}
|
|
922
|
+
|
|
815
923
|
export function deleteSnapshotFromDB(db: Database, id: string): boolean {
|
|
816
924
|
const transaction = db.transaction(() => {
|
|
817
925
|
db.run('DELETE FROM snapshot_nodes WHERE snapshot_id = ?', [id]);
|
|
@@ -873,6 +981,9 @@ function trimAxTable(db: Database, table: 'ax_events' | 'ax_evidence' | 'ax_stee
|
|
|
873
981
|
db.run(`DELETE FROM ${table} WHERE seq <= (SELECT seq FROM ${table} ORDER BY seq DESC LIMIT 1 OFFSET ?)`, [
|
|
874
982
|
AX_TIMELINE_RETENTION,
|
|
875
983
|
]);
|
|
984
|
+
if (table === 'ax_steering') {
|
|
985
|
+
db.run('DELETE FROM ax_steering_deliveries WHERE steering_id NOT IN (SELECT id FROM ax_steering)');
|
|
986
|
+
}
|
|
876
987
|
}
|
|
877
988
|
|
|
878
989
|
function readLastSeq(db: Database, table: 'ax_events' | 'ax_evidence' | 'ax_steering'): number {
|
|
@@ -930,10 +1041,28 @@ export function appendAxSteeringToDB(db: Database, s: Omit<PmxAxSteeringMessage,
|
|
|
930
1041
|
return { ...s, seq };
|
|
931
1042
|
}
|
|
932
1043
|
|
|
933
|
-
export function markAxSteeringDeliveredInDB(db: Database, id: string): boolean {
|
|
934
|
-
//
|
|
935
|
-
//
|
|
936
|
-
//
|
|
1044
|
+
export function markAxSteeringDeliveredInDB(db: Database, id: string, consumer?: string | null): boolean {
|
|
1045
|
+
// ADDRESSED steer (target set): global compare-and-set as before — one
|
|
1046
|
+
// legitimate recipient, true only on the undelivered→delivered transition.
|
|
1047
|
+
// BROADCAST + consumer: per-consumer delivery ("all workers: stop" must
|
|
1048
|
+
// reach every worker) — the mark records THIS consumer's pickup and the
|
|
1049
|
+
// message stays pending for everyone else; true iff this consumer had not
|
|
1050
|
+
// marked it before. BROADCAST without a consumer: the legacy anonymous ack
|
|
1051
|
+
// (global CAS) that hides it from all consumers.
|
|
1052
|
+
const row = db.query<{ target: string | null }, [string]>('SELECT target FROM ax_steering WHERE id = ?').get(id);
|
|
1053
|
+
if (!row) return false;
|
|
1054
|
+
// An addressed steer belongs to its target. A different named consumer marking
|
|
1055
|
+
// it delivered would silently swallow the human's instruction (the target then
|
|
1056
|
+
// never sees it). A consumer-less mark is still the single legitimate host ack.
|
|
1057
|
+
if (row.target !== null && consumer && consumer !== row.target) return false;
|
|
1058
|
+
if (row.target === null && consumer) {
|
|
1059
|
+
const r = db.run('INSERT OR IGNORE INTO ax_steering_deliveries (steering_id, consumer, at) VALUES (?, ?, ?)', [
|
|
1060
|
+
id,
|
|
1061
|
+
consumer,
|
|
1062
|
+
new Date().toISOString(),
|
|
1063
|
+
]);
|
|
1064
|
+
return r.changes > 0;
|
|
1065
|
+
}
|
|
937
1066
|
const r = db.run('UPDATE ax_steering SET delivered = 1 WHERE id = ? AND delivered = 0', [id]);
|
|
938
1067
|
return r.changes > 0;
|
|
939
1068
|
}
|
|
@@ -1006,6 +1135,18 @@ interface AxSteeringRow {
|
|
|
1006
1135
|
target: string | null;
|
|
1007
1136
|
}
|
|
1008
1137
|
|
|
1138
|
+
/**
|
|
1139
|
+
* Broadcasts age out of PENDING (they stay on the timeline): per-consumer
|
|
1140
|
+
* delivery otherwise means every broadcast greets every FUTURE consumer key
|
|
1141
|
+
* forever — a brand-new pump inherited two-day-old "all workers" calls.
|
|
1142
|
+
* Addressed steers never expire here; they wait for their named consumer.
|
|
1143
|
+
*/
|
|
1144
|
+
export const BROADCAST_PENDING_TTL_MS = 15 * 60_000;
|
|
1145
|
+
|
|
1146
|
+
function broadcastCutoffIso(): string {
|
|
1147
|
+
return new Date(Date.now() - BROADCAST_PENDING_TTL_MS).toISOString();
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1009
1150
|
function mapAxSteeringRow(r: AxSteeringRow): PmxAxSteeringMessage | null {
|
|
1010
1151
|
return normalizeAxSteeringMessage({
|
|
1011
1152
|
...r,
|
|
@@ -1023,7 +1164,27 @@ export function loadAxSteeringFromDB(
|
|
|
1023
1164
|
? 'SELECT * FROM ax_steering WHERE delivered = 0 ORDER BY seq DESC LIMIT ?'
|
|
1024
1165
|
: 'SELECT * FROM ax_steering ORDER BY seq DESC LIMIT ?';
|
|
1025
1166
|
const rows = db.query<AxSteeringRow, [number]>(sql).all(clampTimelineLimit(q.limit));
|
|
1026
|
-
|
|
1167
|
+
const deliveredTo = new Map<string, string[]>();
|
|
1168
|
+
for (const row of db
|
|
1169
|
+
.query<{ steering_id: string; consumer: string }, []>('SELECT steering_id, consumer FROM ax_steering_deliveries')
|
|
1170
|
+
.all()) {
|
|
1171
|
+
if (!deliveredTo.has(row.steering_id)) deliveredTo.set(row.steering_id, []);
|
|
1172
|
+
deliveredTo.get(row.steering_id)!.push(row.consumer);
|
|
1173
|
+
}
|
|
1174
|
+
return rows
|
|
1175
|
+
.map((row) => {
|
|
1176
|
+
const mapped = mapAxSteeringRow(row);
|
|
1177
|
+
if (!mapped) return null;
|
|
1178
|
+
const pickups = deliveredTo.get(mapped.id) ?? [];
|
|
1179
|
+
// A broadcast reads as delivered once ANY consumer picked it up; the
|
|
1180
|
+
// per-consumer list is exposed for fleet UIs ("picked up by 3").
|
|
1181
|
+
return {
|
|
1182
|
+
...mapped,
|
|
1183
|
+
delivered: mapped.delivered || pickups.length > 0,
|
|
1184
|
+
...(pickups.length > 0 ? { deliveredTo: pickups } : {}),
|
|
1185
|
+
};
|
|
1186
|
+
})
|
|
1187
|
+
.filter((s): s is PmxAxSteeringMessage => s !== null);
|
|
1027
1188
|
}
|
|
1028
1189
|
|
|
1029
1190
|
export function loadPendingAxSteeringFromDB(
|
|
@@ -1036,13 +1197,20 @@ export function loadPendingAxSteeringFromDB(
|
|
|
1036
1197
|
const limit = clampTimelineLimit(options.limit);
|
|
1037
1198
|
const rows = options.consumer
|
|
1038
1199
|
? db
|
|
1039
|
-
.query<AxSteeringRow, [string, string, string, number]>(
|
|
1040
|
-
|
|
1200
|
+
.query<AxSteeringRow, [string, string, string, string, string, number]>(
|
|
1201
|
+
// A broadcast stays pending PER CONSUMER: it leaves this consumer's
|
|
1202
|
+
// queue only once THIS consumer marked it (or an anonymous global ack
|
|
1203
|
+
// set delivered).
|
|
1204
|
+
'SELECT * FROM ax_steering s WHERE s.delivered = 0 AND (s.source IS NULL OR s.source != ?) AND (s.agent_id IS NULL OR s.agent_id != ?) AND (s.target IS NULL OR s.target = ?) AND (s.target IS NOT NULL OR s.created_at > ?) AND NOT EXISTS (SELECT 1 FROM ax_steering_deliveries d WHERE d.steering_id = s.id AND d.consumer = ?) ORDER BY s.seq ASC LIMIT ?',
|
|
1041
1205
|
)
|
|
1042
|
-
.all(options.consumer, options.consumer, options.consumer, limit)
|
|
1206
|
+
.all(options.consumer, options.consumer, options.consumer, broadcastCutoffIso(), options.consumer, limit)
|
|
1043
1207
|
: db
|
|
1044
|
-
.query<AxSteeringRow, [number]>(
|
|
1045
|
-
|
|
1208
|
+
.query<AxSteeringRow, [string, number]>(
|
|
1209
|
+
// No consumer given: only broadcasts — addressed steering is claimable
|
|
1210
|
+
// solely by the consumer it names.
|
|
1211
|
+
'SELECT * FROM ax_steering WHERE delivered = 0 AND target IS NULL AND created_at > ? ORDER BY seq ASC LIMIT ?',
|
|
1212
|
+
)
|
|
1213
|
+
.all(broadcastCutoffIso(), limit);
|
|
1046
1214
|
return rows.map(mapAxSteeringRow).filter((s): s is PmxAxSteeringMessage => s !== null);
|
|
1047
1215
|
}
|
|
1048
1216
|
|
|
@@ -1061,25 +1229,33 @@ export function loadNewestPendingAxSteeringFromDB(
|
|
|
1061
1229
|
const limit = clampTimelineLimit(options.limit);
|
|
1062
1230
|
const rows = options.consumer
|
|
1063
1231
|
? db
|
|
1064
|
-
.query<AxSteeringRow, [string, string, string, number]>(
|
|
1065
|
-
'SELECT * FROM ax_steering WHERE delivered = 0 AND (source IS NULL OR source != ?) AND (agent_id IS NULL OR agent_id != ?) AND (target IS NULL OR target = ?) ORDER BY seq DESC LIMIT ?',
|
|
1232
|
+
.query<AxSteeringRow, [string, string, string, string, string, number]>(
|
|
1233
|
+
'SELECT * FROM ax_steering s WHERE s.delivered = 0 AND (s.source IS NULL OR s.source != ?) AND (s.agent_id IS NULL OR s.agent_id != ?) AND (s.target IS NULL OR s.target = ?) AND (s.target IS NOT NULL OR s.created_at > ?) AND NOT EXISTS (SELECT 1 FROM ax_steering_deliveries d WHERE d.steering_id = s.id AND d.consumer = ?) ORDER BY s.seq DESC LIMIT ?',
|
|
1066
1234
|
)
|
|
1067
|
-
.all(options.consumer, options.consumer, options.consumer, limit)
|
|
1235
|
+
.all(options.consumer, options.consumer, options.consumer, broadcastCutoffIso(), options.consumer, limit)
|
|
1068
1236
|
: db
|
|
1069
|
-
.query<AxSteeringRow, [number]>(
|
|
1237
|
+
.query<AxSteeringRow, [number]>(
|
|
1238
|
+
'SELECT * FROM ax_steering WHERE delivered = 0 AND target IS NULL ORDER BY seq DESC LIMIT ?',
|
|
1239
|
+
)
|
|
1070
1240
|
.all(limit);
|
|
1071
1241
|
return rows.map(mapAxSteeringRow).filter((s): s is PmxAxSteeringMessage => s !== null);
|
|
1072
1242
|
}
|
|
1073
1243
|
|
|
1074
1244
|
/** Total undelivered steering for a consumer (loop-safe — excludes the consumer's own; target-scoped). */
|
|
1075
1245
|
export function countPendingAxSteeringFromDB(db: Database, consumer?: string): number {
|
|
1246
|
+
// Must mirror loadPendingAxSteeringFromDB exactly (per-consumer deliveries,
|
|
1247
|
+
// broadcast TTL) — the roster shows this as "what would this consumer claim".
|
|
1076
1248
|
const n = consumer
|
|
1077
1249
|
? db
|
|
1078
|
-
.query<{ n: number }, [string, string, string]>(
|
|
1079
|
-
'SELECT COUNT(*) AS n FROM ax_steering WHERE delivered = 0 AND (source IS NULL OR source != ?) AND (agent_id IS NULL OR agent_id != ?) AND (target IS NULL OR target = ?)',
|
|
1250
|
+
.query<{ n: number }, [string, string, string, string, string]>(
|
|
1251
|
+
'SELECT COUNT(*) AS n FROM ax_steering s WHERE s.delivered = 0 AND (s.source IS NULL OR s.source != ?) AND (s.agent_id IS NULL OR s.agent_id != ?) AND (s.target IS NULL OR s.target = ?) AND (s.target IS NOT NULL OR s.created_at > ?) AND NOT EXISTS (SELECT 1 FROM ax_steering_deliveries d WHERE d.steering_id = s.id AND d.consumer = ?)',
|
|
1252
|
+
)
|
|
1253
|
+
.get(consumer, consumer, consumer, broadcastCutoffIso(), consumer)?.n
|
|
1254
|
+
: db
|
|
1255
|
+
.query<{ n: number }, [string]>(
|
|
1256
|
+
'SELECT COUNT(*) AS n FROM ax_steering WHERE delivered = 0 AND target IS NULL AND created_at > ?',
|
|
1080
1257
|
)
|
|
1081
|
-
.get(
|
|
1082
|
-
: db.query<{ n: number }, []>('SELECT COUNT(*) AS n FROM ax_steering WHERE delivered = 0').get()?.n;
|
|
1258
|
+
.get(broadcastCutoffIso())?.n;
|
|
1083
1259
|
return Number(n ?? 0);
|
|
1084
1260
|
}
|
|
1085
1261
|
|
|
@@ -946,7 +946,6 @@ export function addCanvasNode(input: CanvasAddNodeInput): {
|
|
|
946
946
|
zIndex: 1,
|
|
947
947
|
collapsed: false,
|
|
948
948
|
pinned: false,
|
|
949
|
-
dockPosition: null,
|
|
950
949
|
data,
|
|
951
950
|
};
|
|
952
951
|
|
|
@@ -1133,7 +1132,7 @@ export function removeCanvasNode(id: string): {
|
|
|
1133
1132
|
}
|
|
1134
1133
|
|
|
1135
1134
|
function isArrangeLocked(node: CanvasNodeState): boolean {
|
|
1136
|
-
return node.pinned || node.
|
|
1135
|
+
return node.pinned || node.data.arrangeLocked === true;
|
|
1137
1136
|
}
|
|
1138
1137
|
|
|
1139
1138
|
function collectArrangeExcludedNodeIds(nodes: CanvasNodeState[]): Set<string> {
|
|
@@ -1219,7 +1218,7 @@ function rectsOverlap(a: ArrangeObstacleRect, b: ArrangeObstacleRect): boolean {
|
|
|
1219
1218
|
|
|
1220
1219
|
function collectGridArrangeObstacles(nodes: CanvasNodeState[], excludedIds: Set<string>): ArrangeObstacleRect[] {
|
|
1221
1220
|
return nodes
|
|
1222
|
-
.filter((node) => excludedIds.has(node.id)
|
|
1221
|
+
.filter((node) => excludedIds.has(node.id))
|
|
1223
1222
|
.map((node) => ({
|
|
1224
1223
|
id: node.id,
|
|
1225
1224
|
position: { ...node.position },
|
|
@@ -1247,7 +1246,7 @@ function shiftGridUpdatesBelowObstacles(
|
|
|
1247
1246
|
): CanvasNodeUpdate[] {
|
|
1248
1247
|
if (updates.length === 0 || obstacles.length === 0) return updates;
|
|
1249
1248
|
|
|
1250
|
-
// Grid arrange only sees movable nodes, so preserved locked
|
|
1249
|
+
// Grid arrange only sees movable nodes, so preserved locked-group frames
|
|
1251
1250
|
// need a separate obstacle pass before applying the planned positions.
|
|
1252
1251
|
const nodesById = new Map(nodes.map((node) => [node.id, node]));
|
|
1253
1252
|
let shifted = updates;
|
|
@@ -1465,6 +1464,20 @@ export function addCanvasEdge(input: {
|
|
|
1465
1464
|
return edge;
|
|
1466
1465
|
}
|
|
1467
1466
|
|
|
1467
|
+
export function updateCanvasEdge(
|
|
1468
|
+
id: string,
|
|
1469
|
+
patch: Partial<Pick<CanvasEdge, 'type' | 'label' | 'style' | 'animated'>>,
|
|
1470
|
+
): CanvasEdge {
|
|
1471
|
+
if (!canvasState.getEdges().some((edge) => edge.id === id)) {
|
|
1472
|
+
throw new Error(`Edge "${id}" not found.`);
|
|
1473
|
+
}
|
|
1474
|
+
const updated = canvasState.updateEdge(id, patch);
|
|
1475
|
+
if (!updated) {
|
|
1476
|
+
throw new Error('Retyping would duplicate an existing edge.');
|
|
1477
|
+
}
|
|
1478
|
+
return updated;
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1468
1481
|
export function removeCanvasEdge(id: string): { removed: boolean } {
|
|
1469
1482
|
return { removed: canvasState.removeEdge(id) };
|
|
1470
1483
|
}
|
|
@@ -1512,7 +1525,6 @@ export function createCanvasGroup(input: CanvasCreateGroupInput): { id: string;
|
|
|
1512
1525
|
zIndex: 0,
|
|
1513
1526
|
collapsed: false,
|
|
1514
1527
|
pinned: false,
|
|
1515
|
-
dockPosition: null,
|
|
1516
1528
|
data,
|
|
1517
1529
|
});
|
|
1518
1530
|
|
|
@@ -1583,7 +1595,6 @@ export function createCanvasJsonRenderNode(input: JsonRenderNodeInput): {
|
|
|
1583
1595
|
zIndex: 1,
|
|
1584
1596
|
collapsed: false,
|
|
1585
1597
|
pinned: false,
|
|
1586
|
-
dockPosition: null,
|
|
1587
1598
|
data: createJsonRenderNodeData(id, input.title?.trim() || inferJsonRenderNodeTitle(spec), spec, {
|
|
1588
1599
|
viewerType: 'json-render',
|
|
1589
1600
|
...(input.strictSize ? { strictSize: true } : {}),
|
|
@@ -1627,7 +1638,6 @@ export function createCanvasStreamingJsonRenderNode(input: {
|
|
|
1627
1638
|
zIndex: 1,
|
|
1628
1639
|
collapsed: false,
|
|
1629
1640
|
pinned: false,
|
|
1630
|
-
dockPosition: null,
|
|
1631
1641
|
data: createJsonRenderNodeData(id, input.title?.trim() || 'Streaming', spec, {
|
|
1632
1642
|
viewerType: 'json-render',
|
|
1633
1643
|
streamStatus: 'open',
|
|
@@ -1703,7 +1713,6 @@ export function createCanvasGraphNode(input: GraphNodeInput): {
|
|
|
1703
1713
|
zIndex: 1,
|
|
1704
1714
|
collapsed: false,
|
|
1705
1715
|
pinned: false,
|
|
1706
|
-
dockPosition: null,
|
|
1707
1716
|
data: createJsonRenderNodeData(id, title, spec, {
|
|
1708
1717
|
viewerType: 'graph',
|
|
1709
1718
|
graphConfig: buildGraphConfig(input),
|