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
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
loadSnapshotFromDB,
|
|
26
26
|
listSnapshotsFromDB,
|
|
27
27
|
deleteSnapshotFromDB,
|
|
28
|
+
renameSnapshotInDB,
|
|
28
29
|
writeBlobToDB,
|
|
29
30
|
readBlobFromDB,
|
|
30
31
|
isDbPopulated,
|
|
@@ -185,7 +186,6 @@ export interface CanvasNodeState {
|
|
|
185
186
|
zIndex: number;
|
|
186
187
|
collapsed: boolean;
|
|
187
188
|
pinned: boolean;
|
|
188
|
-
dockPosition: 'left' | 'right' | null;
|
|
189
189
|
data: Record<string, unknown>;
|
|
190
190
|
}
|
|
191
191
|
|
|
@@ -235,7 +235,6 @@ export interface CanvasNodeUpdate {
|
|
|
235
235
|
position?: { x: number; y: number };
|
|
236
236
|
size?: { width: number; height: number };
|
|
237
237
|
collapsed?: boolean;
|
|
238
|
-
dockPosition?: 'left' | 'right' | null;
|
|
239
238
|
}
|
|
240
239
|
|
|
241
240
|
export type CanvasChangeType = 'pins' | 'nodes' | 'ax' | 'ax-timeline';
|
|
@@ -246,6 +245,7 @@ export interface MutationRecordInfo {
|
|
|
246
245
|
| 'updateNode'
|
|
247
246
|
| 'removeNode'
|
|
248
247
|
| 'addEdge'
|
|
248
|
+
| 'updateEdge'
|
|
249
249
|
| 'removeEdge'
|
|
250
250
|
| 'addAnnotation'
|
|
251
251
|
| 'removeAnnotation'
|
|
@@ -267,7 +267,7 @@ export interface MutationRecordInfo {
|
|
|
267
267
|
| 'arrange'
|
|
268
268
|
| 'batch'
|
|
269
269
|
| 'groupNodes'
|
|
270
|
-
| '
|
|
270
|
+
| 'releaseGroupChildren'
|
|
271
271
|
| 'viewport';
|
|
272
272
|
description: string;
|
|
273
273
|
forward: () => void;
|
|
@@ -288,20 +288,17 @@ function formatBatchUpdateDescription(updates: CanvasNodeUpdate[]): string {
|
|
|
288
288
|
let moved = 0;
|
|
289
289
|
let resized = 0;
|
|
290
290
|
let collapsed = 0;
|
|
291
|
-
let docked = 0;
|
|
292
291
|
|
|
293
292
|
for (const update of updates) {
|
|
294
293
|
if (update.position) moved++;
|
|
295
294
|
if (update.size) resized++;
|
|
296
295
|
if (update.collapsed !== undefined) collapsed++;
|
|
297
|
-
if (update.dockPosition !== undefined) docked++;
|
|
298
296
|
}
|
|
299
297
|
|
|
300
298
|
const parts: string[] = [];
|
|
301
299
|
if (moved > 0) parts.push(`${moved} moved`);
|
|
302
300
|
if (resized > 0) parts.push(`${resized} resized`);
|
|
303
301
|
if (collapsed > 0) parts.push(`${collapsed} collapsed`);
|
|
304
|
-
if (docked > 0) parts.push(`${docked} docked`);
|
|
305
302
|
|
|
306
303
|
const prefix = `Updated ${updates.length} node${updates.length === 1 ? '' : 's'}`;
|
|
307
304
|
return parts.length > 0 ? `${prefix} (${parts.join(', ')})` : prefix;
|
|
@@ -506,10 +503,6 @@ class CanvasStateManager {
|
|
|
506
503
|
}
|
|
507
504
|
|
|
508
505
|
private normalizeNode(node: CanvasNodeState): CanvasNodeState {
|
|
509
|
-
// Context nodes default to a right-docked, collapsed pill (see DockedNode.tsx),
|
|
510
|
-
// but that default is applied at CREATE time only — it must not be re-forced on
|
|
511
|
-
// every write, or the node could never be undocked. Undocking (dockPosition →
|
|
512
|
-
// null) is a deliberate user action and is respected here.
|
|
513
506
|
return {
|
|
514
507
|
...node,
|
|
515
508
|
data: normalizeCanvasNodeData(node.type, node.data),
|
|
@@ -874,16 +867,6 @@ class CanvasStateManager {
|
|
|
874
867
|
return false;
|
|
875
868
|
}
|
|
876
869
|
|
|
877
|
-
/**
|
|
878
|
-
* Whether this workspace's canvas DB already holds saved state. Used to gate
|
|
879
|
-
* brand-new-workspace seeding (e.g. the default docked status/context widgets)
|
|
880
|
-
* so we never add nodes to a canvas that already has content. Reflects the
|
|
881
|
-
* pre-run persisted flag until the next save.
|
|
882
|
-
*/
|
|
883
|
-
hasPersistedState(): boolean {
|
|
884
|
-
return this._db ? isDbPopulated(this._db) : false;
|
|
885
|
-
}
|
|
886
|
-
|
|
887
870
|
/** Debounced save — coalesces rapid mutations into a single write. */
|
|
888
871
|
private scheduleSave(): void {
|
|
889
872
|
if (!this._db) return;
|
|
@@ -1272,6 +1255,16 @@ class CanvasStateManager {
|
|
|
1272
1255
|
}
|
|
1273
1256
|
|
|
1274
1257
|
/** Delete a snapshot. */
|
|
1258
|
+
renameSnapshot(id: string, name: string): boolean {
|
|
1259
|
+
if (!this._db) return false;
|
|
1260
|
+
try {
|
|
1261
|
+
return renameSnapshotInDB(this._db, id, name);
|
|
1262
|
+
} catch (error) {
|
|
1263
|
+
logCanvasStateWarning('rename snapshot failed', error, { id, name });
|
|
1264
|
+
return false;
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1275
1268
|
deleteSnapshot(id: string): boolean {
|
|
1276
1269
|
// Try SQLite first
|
|
1277
1270
|
if (this._db) {
|
|
@@ -1320,14 +1313,6 @@ class CanvasStateManager {
|
|
|
1320
1313
|
}
|
|
1321
1314
|
|
|
1322
1315
|
addNode(node: CanvasNodeState): void {
|
|
1323
|
-
// Docking is EXPLICIT (0.4.6). Context nodes used to auto-dock to a collapsed
|
|
1324
|
-
// right-hand pill when created without a dock position, which meant an agent
|
|
1325
|
-
// creating a context node — even at explicit coordinates — got a node that
|
|
1326
|
-
// existed in the API and passed validation but never rendered on the canvas,
|
|
1327
|
-
// and any edge to it terminated in empty space (0.4.6 orb feedback #1). The
|
|
1328
|
-
// seeded HUD widgets (status-main / context-main) pass `dockPosition` on
|
|
1329
|
-
// create, so the docked pills are unaffected; anything else reaches the
|
|
1330
|
-
// canvas where the caller put it. Dock deliberately via node.update.
|
|
1331
1316
|
const cloned = structuredClone(this.normalizeNode(node));
|
|
1332
1317
|
this.nodes.set(node.id, cloned);
|
|
1333
1318
|
this.scheduleSave();
|
|
@@ -1385,29 +1370,35 @@ class CanvasStateManager {
|
|
|
1385
1370
|
const connectedEdges = existing ? this.getEdgesForNode(id).map((e) => structuredClone(e)) : [];
|
|
1386
1371
|
const cloned = existing ? structuredClone(existing) : null;
|
|
1387
1372
|
const oldAxState = this.getAxState();
|
|
1373
|
+
const wasPinned = this._contextPinnedNodeIds.has(id);
|
|
1374
|
+
// The enclosing group's membership before this removal — restored verbatim on undo.
|
|
1375
|
+
let enclosingBefore: { id: string; children: string[] } | null = null;
|
|
1376
|
+
// A removed group DISSOLVES: its children are released — into the enclosing
|
|
1377
|
+
// group when nested, else to the board. This is the one implementation of
|
|
1378
|
+
// ungroup, so the human's Ungroup and the agent's `group.remove` cannot differ.
|
|
1379
|
+
let released: string[] = [];
|
|
1388
1380
|
|
|
1389
|
-
// Prune from parent group's children list
|
|
1390
1381
|
if (existing) {
|
|
1391
1382
|
const parentGroupId = existing.data.parentGroup as string | undefined;
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
const pruned = children.filter((cid) => cid !== id);
|
|
1397
|
-
this.nodes.set(parentGroupId, { ...parent, data: { ...parent.data, children: pruned } });
|
|
1398
|
-
}
|
|
1399
|
-
}
|
|
1400
|
-
// If removing a group, clear parentGroup on all its children
|
|
1383
|
+
const parent = parentGroupId ? this.nodes.get(parentGroupId) : undefined;
|
|
1384
|
+
const enclosing = parent && parent.type === 'group' ? parent : null;
|
|
1385
|
+
if (enclosing)
|
|
1386
|
+
enclosingBefore = { id: enclosing.id, children: ((enclosing.data.children as string[]) ?? []).slice() };
|
|
1401
1387
|
if (existing.type === 'group') {
|
|
1402
|
-
|
|
1403
|
-
for (const cid of
|
|
1404
|
-
const child = this.nodes.get(cid)
|
|
1405
|
-
if (!child) continue;
|
|
1388
|
+
released = ((existing.data.children as string[]) ?? []).filter((cid) => this.nodes.has(cid));
|
|
1389
|
+
for (const cid of released) {
|
|
1390
|
+
const child = this.nodes.get(cid)!;
|
|
1406
1391
|
const d = { ...child.data };
|
|
1407
|
-
|
|
1392
|
+
if (enclosing) d.parentGroup = enclosing.id;
|
|
1393
|
+
else delete d.parentGroup;
|
|
1408
1394
|
this.nodes.set(cid, { ...child, data: d });
|
|
1409
1395
|
}
|
|
1410
1396
|
}
|
|
1397
|
+
if (enclosing && enclosingBefore) {
|
|
1398
|
+
// The removed node leaves the enclosing group; a dissolved group's children take its place.
|
|
1399
|
+
const children = enclosingBefore.children.flatMap((cid) => (cid === id ? released : [cid]));
|
|
1400
|
+
this.nodes.set(enclosing.id, { ...enclosing, data: { ...enclosing.data, children } });
|
|
1401
|
+
}
|
|
1411
1402
|
}
|
|
1412
1403
|
|
|
1413
1404
|
this.nodes.delete(id);
|
|
@@ -1450,15 +1441,36 @@ class CanvasStateManager {
|
|
|
1450
1441
|
);
|
|
1451
1442
|
}
|
|
1452
1443
|
if (cloned) {
|
|
1444
|
+
const title = (cloned.data.title as string) ?? id;
|
|
1453
1445
|
this.recordMutation({
|
|
1454
1446
|
operationType: 'removeNode',
|
|
1455
|
-
description:
|
|
1447
|
+
description:
|
|
1448
|
+
cloned.type === 'group'
|
|
1449
|
+
? `Dissolved group "${title}" — ${released.length} node${released.length === 1 ? '' : 's'} released`
|
|
1450
|
+
: `Removed ${cloned.type} node "${title}"`,
|
|
1456
1451
|
forward: this.suppressed(() => this.removeNode(id)),
|
|
1457
1452
|
inverse: this.suppressed(() => {
|
|
1458
1453
|
this.addNode(structuredClone(cloned));
|
|
1454
|
+
// A restored group takes its children back; the enclosing group's membership is restored verbatim.
|
|
1455
|
+
for (const cid of released) {
|
|
1456
|
+
const child = this.nodes.get(cid);
|
|
1457
|
+
if (child) this.nodes.set(cid, { ...child, data: { ...child.data, parentGroup: id } });
|
|
1458
|
+
}
|
|
1459
|
+
if (enclosingBefore) {
|
|
1460
|
+
const enclosing = this.nodes.get(enclosingBefore.id);
|
|
1461
|
+
if (enclosing) {
|
|
1462
|
+
this.nodes.set(enclosing.id, {
|
|
1463
|
+
...enclosing,
|
|
1464
|
+
data: { ...enclosing.data, children: enclosingBefore.children },
|
|
1465
|
+
});
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1459
1468
|
for (const edge of connectedEdges) this.addEdge(structuredClone(edge));
|
|
1469
|
+
if (wasPinned) this._contextPinnedNodeIds.add(id);
|
|
1460
1470
|
this.ax.applyPersistedAx(oldAxState);
|
|
1461
1471
|
this.scheduleSave();
|
|
1472
|
+
this.notifyChange('nodes');
|
|
1473
|
+
this.notifyChange('pins');
|
|
1462
1474
|
this.notifyChange('ax');
|
|
1463
1475
|
}),
|
|
1464
1476
|
});
|
|
@@ -1497,6 +1509,46 @@ class CanvasStateManager {
|
|
|
1497
1509
|
return true;
|
|
1498
1510
|
}
|
|
1499
1511
|
|
|
1512
|
+
updateEdge(id: string, patch: Partial<Pick<CanvasEdge, 'type' | 'label' | 'style' | 'animated'>>): CanvasEdge | null {
|
|
1513
|
+
const existing = this.edges.get(id);
|
|
1514
|
+
if (!existing) return null;
|
|
1515
|
+
// Retyping onto an existing from/to/type triple would create the duplicate
|
|
1516
|
+
// addEdge refuses.
|
|
1517
|
+
if (patch.type && patch.type !== existing.type) {
|
|
1518
|
+
for (const other of this.edges.values()) {
|
|
1519
|
+
if (other.id !== id && other.from === existing.from && other.to === existing.to && other.type === patch.type) {
|
|
1520
|
+
return null;
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
const before = structuredClone(existing);
|
|
1525
|
+
const updated: CanvasEdge = { ...existing, ...patch };
|
|
1526
|
+
// An explicit undefined in the patch clears the field (label/style/animated
|
|
1527
|
+
// are optional on the wire shape — never store undefined values).
|
|
1528
|
+
for (const key of ['label', 'style', 'animated'] as const) {
|
|
1529
|
+
if (key in patch && patch[key] === undefined) delete updated[key];
|
|
1530
|
+
}
|
|
1531
|
+
const after = structuredClone(updated);
|
|
1532
|
+
this.edges.set(id, updated);
|
|
1533
|
+
this.scheduleSave();
|
|
1534
|
+
this.notifyChange('nodes');
|
|
1535
|
+
this.recordMutation({
|
|
1536
|
+
operationType: 'updateEdge',
|
|
1537
|
+
description: `Updated ${updated.type} edge ${updated.from} → ${updated.to}`,
|
|
1538
|
+
forward: this.suppressed(() => {
|
|
1539
|
+
this.edges.set(id, structuredClone(after));
|
|
1540
|
+
this.scheduleSave();
|
|
1541
|
+
this.notifyChange('nodes');
|
|
1542
|
+
}),
|
|
1543
|
+
inverse: this.suppressed(() => {
|
|
1544
|
+
this.edges.set(id, structuredClone(before));
|
|
1545
|
+
this.scheduleSave();
|
|
1546
|
+
this.notifyChange('nodes');
|
|
1547
|
+
}),
|
|
1548
|
+
});
|
|
1549
|
+
return structuredClone(updated);
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1500
1552
|
removeEdge(id: string): boolean {
|
|
1501
1553
|
const existing = this.edges.get(id);
|
|
1502
1554
|
const cloned = existing ? structuredClone(existing) : null;
|
|
@@ -1612,9 +1664,6 @@ class CanvasStateManager {
|
|
|
1612
1664
|
if (update.collapsed !== undefined && update.collapsed !== existing.collapsed) {
|
|
1613
1665
|
nextPatch.collapsed = update.collapsed;
|
|
1614
1666
|
}
|
|
1615
|
-
if (update.dockPosition !== undefined && update.dockPosition !== existing.dockPosition) {
|
|
1616
|
-
nextPatch.dockPosition = update.dockPosition;
|
|
1617
|
-
}
|
|
1618
1667
|
if (Object.keys(nextPatch).length === 0) {
|
|
1619
1668
|
skipped++;
|
|
1620
1669
|
continue;
|
|
@@ -1815,7 +1864,7 @@ class CanvasStateManager {
|
|
|
1815
1864
|
}
|
|
1816
1865
|
|
|
1817
1866
|
requestApproval(
|
|
1818
|
-
input: { title: string; detail?: string | null; action?: string | null; nodeIds?: string[] },
|
|
1867
|
+
input: { title: string; detail?: string | null; action?: string | null; nodeIds?: string[]; ttlMs?: number },
|
|
1819
1868
|
options: { source?: PmxAxSource } = {},
|
|
1820
1869
|
): PmxAxApprovalGate {
|
|
1821
1870
|
return this.ax.requestApproval(input, options);
|
|
@@ -1823,12 +1872,17 @@ class CanvasStateManager {
|
|
|
1823
1872
|
|
|
1824
1873
|
resolveApproval(
|
|
1825
1874
|
id: string,
|
|
1826
|
-
decision: 'approved' | 'rejected',
|
|
1875
|
+
decision: 'approved' | 'rejected' | 'held',
|
|
1827
1876
|
options: { resolution?: string; source?: PmxAxSource } = {},
|
|
1828
1877
|
): PmxAxApprovalGate | null {
|
|
1829
1878
|
return this.ax.resolveApproval(id, decision, options);
|
|
1830
1879
|
}
|
|
1831
1880
|
|
|
1881
|
+
/** Reopen a resolved (typically auto-held) gate with a fresh TTL. */
|
|
1882
|
+
reopenApproval(id: string, options: { ttlMs?: number; source?: PmxAxSource } = {}): PmxAxApprovalGate | null {
|
|
1883
|
+
return this.ax.reopenApproval(id, options);
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1832
1886
|
// ── Review annotations (canvas-bound) ─────────────────────────────
|
|
1833
1887
|
getReviewAnnotations(): PmxAxReviewAnnotation[] {
|
|
1834
1888
|
return this.ax.getReviewAnnotations();
|
|
@@ -1933,7 +1987,11 @@ class CanvasStateManager {
|
|
|
1933
1987
|
|
|
1934
1988
|
/** Merge a declarative tool/prompt policy patch (canvas-bound, snapshotted). */
|
|
1935
1989
|
setPolicy(
|
|
1936
|
-
patch: {
|
|
1990
|
+
patch: {
|
|
1991
|
+
tools?: Partial<PmxAxPolicy['tools']>;
|
|
1992
|
+
prompt?: Partial<PmxAxPolicy['prompt']>;
|
|
1993
|
+
scope?: { nodeIds: string[]; padding?: number } | null;
|
|
1994
|
+
},
|
|
1937
1995
|
options: { source?: PmxAxSource } = {},
|
|
1938
1996
|
): PmxAxPolicy {
|
|
1939
1997
|
return this.ax.setPolicy(patch, options);
|
|
@@ -1978,8 +2036,8 @@ class CanvasStateManager {
|
|
|
1978
2036
|
return this.ax.recordSteeringMessage(message, options);
|
|
1979
2037
|
}
|
|
1980
2038
|
|
|
1981
|
-
markSteeringDelivered(id: string): boolean {
|
|
1982
|
-
return this.ax.markSteeringDelivered(id);
|
|
2039
|
+
markSteeringDelivered(id: string, consumer?: string | null): boolean {
|
|
2040
|
+
return this.ax.markSteeringDelivered(id, consumer);
|
|
1983
2041
|
}
|
|
1984
2042
|
|
|
1985
2043
|
ingestActivity(
|
|
@@ -2140,19 +2198,34 @@ class CanvasStateManager {
|
|
|
2140
2198
|
return true;
|
|
2141
2199
|
}
|
|
2142
2200
|
|
|
2143
|
-
/**
|
|
2201
|
+
/**
|
|
2202
|
+
* Ungroup = dissolve: the children become independent nodes (members of the
|
|
2203
|
+
* enclosing group when nested) and the frame is removed, in one undo step.
|
|
2204
|
+
* Delegates to `removeNode` so the human's Ungroup and the agent's
|
|
2205
|
+
* `group.remove` are the same operation. False when `groupId` is not a group.
|
|
2206
|
+
*/
|
|
2144
2207
|
ungroupNodes(groupId: string): boolean {
|
|
2145
2208
|
const group = this.nodes.get(groupId);
|
|
2146
2209
|
if (!group || group.type !== 'group') return false;
|
|
2210
|
+
this.removeNode(groupId);
|
|
2211
|
+
return true;
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
/**
|
|
2215
|
+
* Internal membership step: release every child but keep the frame (the
|
|
2216
|
+
* first half of "set this group's children to …"). Not an ungroup — a
|
|
2217
|
+
* human or agent ungroup dissolves the frame (`ungroupNodes`).
|
|
2218
|
+
*/
|
|
2219
|
+
releaseGroupChildren(groupId: string): boolean {
|
|
2220
|
+
const group = this.nodes.get(groupId);
|
|
2221
|
+
if (!group || group.type !== 'group') return false;
|
|
2147
2222
|
|
|
2148
2223
|
const childIds = (group.data.children as string[]) ?? [];
|
|
2149
2224
|
if (childIds.length === 0) return false;
|
|
2150
2225
|
|
|
2151
2226
|
const snapshot = childIds.slice();
|
|
2152
2227
|
|
|
2153
|
-
// Clear children from group
|
|
2154
2228
|
this.nodes.set(groupId, { ...group, data: { ...group.data, children: [] } });
|
|
2155
|
-
// Clear parentGroup from each child
|
|
2156
2229
|
for (const id of childIds) {
|
|
2157
2230
|
const child = this.nodes.get(id);
|
|
2158
2231
|
if (!child) continue;
|
|
@@ -2164,9 +2237,9 @@ class CanvasStateManager {
|
|
|
2164
2237
|
this.scheduleSave();
|
|
2165
2238
|
this.notifyChange('nodes');
|
|
2166
2239
|
this.recordMutation({
|
|
2167
|
-
operationType: '
|
|
2168
|
-
description: `
|
|
2169
|
-
forward: this.suppressed(() => this.
|
|
2240
|
+
operationType: 'releaseGroupChildren',
|
|
2241
|
+
description: `Released ${childIds.length} nodes from "${(group.data.title as string) ?? groupId}"`,
|
|
2242
|
+
forward: this.suppressed(() => this.releaseGroupChildren(groupId)),
|
|
2170
2243
|
inverse: this.suppressed(() => this.groupNodes(groupId, snapshot)),
|
|
2171
2244
|
});
|
|
2172
2245
|
return true;
|
|
@@ -31,26 +31,12 @@ export interface CanvasSizeWarning {
|
|
|
31
31
|
minHeight: number;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
export interface CanvasHiddenEdgeEndpoint {
|
|
35
|
-
edgeId: string;
|
|
36
|
-
nodeId: string;
|
|
37
|
-
nodeTitle: string | null;
|
|
38
|
-
dockPosition: 'left' | 'right';
|
|
39
|
-
}
|
|
40
|
-
|
|
41
34
|
export interface CanvasValidationResult {
|
|
42
35
|
ok: boolean;
|
|
43
36
|
collisions: CanvasValidationPair[];
|
|
44
37
|
containments: CanvasContainmentIssue[];
|
|
45
38
|
containmentViolations: CanvasContainmentIssue[];
|
|
46
39
|
missingEdgeEndpoints: Array<{ edgeId: string; from: string; to: string }>;
|
|
47
|
-
/**
|
|
48
|
-
* Edges whose endpoint node is DOCKED — it renders in the HUD column, not on
|
|
49
|
-
* the canvas, so the edge visually terminates in empty space even though both
|
|
50
|
-
* endpoint IDs resolve (0.4.6 orb feedback #1). Advisory: reported for
|
|
51
|
-
* diagnosis, but does NOT fail `ok` (see the note at the return site).
|
|
52
|
-
*/
|
|
53
|
-
hiddenEdgeEndpoints: CanvasHiddenEdgeEndpoint[];
|
|
54
40
|
/** Nodes below their type's readable minimum (advisory — does not fail `ok`). */
|
|
55
41
|
sizeWarnings: CanvasSizeWarning[];
|
|
56
42
|
summary: {
|
|
@@ -60,7 +46,6 @@ export interface CanvasValidationResult {
|
|
|
60
46
|
containments: number;
|
|
61
47
|
containmentViolations: number;
|
|
62
48
|
missingEdgeEndpoints: number;
|
|
63
|
-
hiddenEdgeEndpoints: number;
|
|
64
49
|
sizeWarnings: number;
|
|
65
50
|
};
|
|
66
51
|
}
|
|
@@ -74,10 +59,6 @@ function overlaps(a: CanvasNodeState, b: CanvasNodeState): boolean {
|
|
|
74
59
|
);
|
|
75
60
|
}
|
|
76
61
|
|
|
77
|
-
function participatesInCanvasCollisionValidation(node: CanvasNodeState): boolean {
|
|
78
|
-
return node.dockPosition === null;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
62
|
function fullyContains(group: CanvasNodeState, child: CanvasNodeState): boolean {
|
|
82
63
|
return (
|
|
83
64
|
child.position.x >= group.position.x &&
|
|
@@ -119,10 +100,8 @@ export function validateCanvasLayout(layout: CanvasLayout): CanvasValidationResu
|
|
|
119
100
|
|
|
120
101
|
for (let i = 0; i < layout.nodes.length; i++) {
|
|
121
102
|
const a = layout.nodes[i]!;
|
|
122
|
-
if (!participatesInCanvasCollisionValidation(a)) continue;
|
|
123
103
|
for (let j = i + 1; j < layout.nodes.length; j++) {
|
|
124
104
|
const b = layout.nodes[j]!;
|
|
125
|
-
if (!participatesInCanvasCollisionValidation(b)) continue;
|
|
126
105
|
if (!overlaps(a, b)) continue;
|
|
127
106
|
|
|
128
107
|
if (isGroupChildPair(a, b)) {
|
|
@@ -143,35 +122,13 @@ export function validateCanvasLayout(layout: CanvasLayout): CanvasValidationResu
|
|
|
143
122
|
.filter((edge) => !nodeIds.has(edge.from) || !nodeIds.has(edge.to))
|
|
144
123
|
.map((edge) => ({ edgeId: edge.id, from: edge.from, to: edge.to }));
|
|
145
124
|
|
|
146
|
-
// An edge to a DOCKED node cannot be drawn: docked nodes live in the HUD
|
|
147
|
-
// column, and the world-space edge layer skips them, so the edge trails off
|
|
148
|
-
// into empty canvas. Structural validation used to miss this entirely because
|
|
149
|
-
// both endpoint IDs resolve (0.4.6 orb feedback #1).
|
|
150
|
-
const dockedById = new Map(
|
|
151
|
-
layout.nodes.filter((node) => node.dockPosition != null).map((node) => [node.id, node] as const),
|
|
152
|
-
);
|
|
153
|
-
const hiddenEdgeEndpoints: CanvasHiddenEdgeEndpoint[] = layout.edges.flatMap((edge) =>
|
|
154
|
-
[edge.from, edge.to].flatMap((nodeId) => {
|
|
155
|
-
const docked = dockedById.get(nodeId);
|
|
156
|
-
if (!docked || docked.dockPosition == null) return [];
|
|
157
|
-
return [
|
|
158
|
-
{
|
|
159
|
-
edgeId: edge.id,
|
|
160
|
-
nodeId,
|
|
161
|
-
nodeTitle: getCanvasNodeTitle(docked),
|
|
162
|
-
dockPosition: docked.dockPosition,
|
|
163
|
-
},
|
|
164
|
-
];
|
|
165
|
-
}),
|
|
166
|
-
);
|
|
167
|
-
|
|
168
125
|
// Advisory: nodes below their type's readable floor (clipped/unreadable
|
|
169
126
|
// content — 0.4.5 report follow-up). Creation clamps these, but resized or
|
|
170
127
|
// strictSize-created nodes can still be undersized; surface them so an
|
|
171
|
-
// agent's validate pass sees the problem. Collapsed
|
|
172
|
-
//
|
|
128
|
+
// agent's validate pass sees the problem. Collapsed nodes render as bars,
|
|
129
|
+
// and strictSize is the deliberate opt-out — both skipped.
|
|
173
130
|
const sizeWarnings: CanvasSizeWarning[] = layout.nodes
|
|
174
|
-
.filter((node) => !node.collapsed && node.
|
|
131
|
+
.filter((node) => !node.collapsed && node.data?.strictSize !== true)
|
|
175
132
|
.flatMap((node) => {
|
|
176
133
|
const min = nodeMinSize(node.type);
|
|
177
134
|
if (!min || (node.size.width >= min.width && node.size.height >= min.height)) return [];
|
|
@@ -189,17 +146,11 @@ export function validateCanvasLayout(layout: CanvasLayout): CanvasValidationResu
|
|
|
189
146
|
});
|
|
190
147
|
|
|
191
148
|
return {
|
|
192
|
-
// `hiddenEdgeEndpoints` is ADVISORY (like sizeWarnings): a docked endpoint
|
|
193
|
-
// is a rendering observation, not a structural defect, and folding it into
|
|
194
|
-
// `ok` would flip previously-healthy boards to ok:false with no user change
|
|
195
|
-
// — `ok` is a documented response field of a frozen surface, so that is a
|
|
196
|
-
// contract break, not a patch. It is still reported for diagnosis.
|
|
197
149
|
ok: collisions.length === 0 && containmentViolations.length === 0 && missingEdgeEndpoints.length === 0,
|
|
198
150
|
collisions,
|
|
199
151
|
containments,
|
|
200
152
|
containmentViolations,
|
|
201
153
|
missingEdgeEndpoints,
|
|
202
|
-
hiddenEdgeEndpoints,
|
|
203
154
|
sizeWarnings,
|
|
204
155
|
summary: {
|
|
205
156
|
nodes: layout.nodes.length,
|
|
@@ -208,7 +159,6 @@ export function validateCanvasLayout(layout: CanvasLayout): CanvasValidationResu
|
|
|
208
159
|
containments: containments.length,
|
|
209
160
|
containmentViolations: containmentViolations.length,
|
|
210
161
|
missingEdgeEndpoints: missingEdgeEndpoints.length,
|
|
211
|
-
hiddenEdgeEndpoints: hiddenEdgeEndpoints.length,
|
|
212
162
|
sizeWarnings: sizeWarnings.length,
|
|
213
163
|
},
|
|
214
164
|
};
|