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
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scope fence enforcement (rail-chrome-v2 phase 4, design item 4).
|
|
3
|
+
*
|
|
4
|
+
* When the AX policy carries a scope fence, an attached agent may only WRITE
|
|
5
|
+
* inside it: existing-node writes must target fenced nodes, new nodes must
|
|
6
|
+
* land inside the fence's bounding box (fenced nodes + padding), and
|
|
7
|
+
* board-wide writes (arrange, clear, restore) are refused. Reads are never
|
|
8
|
+
* fenced, and neither are the human's own workbench writes — the fence is the
|
|
9
|
+
* human's tool, not a cage for the human.
|
|
10
|
+
*
|
|
11
|
+
* One core check (`checkFenceTarget`) serves two describers: the operation
|
|
12
|
+
* registry (`checkScopeFence`, every HTTP/MCP/CLI write) and the sync SDK,
|
|
13
|
+
* whose methods bypass the registry and describe their own targets.
|
|
14
|
+
*
|
|
15
|
+
* Safe default: a mutating op this module does not know how to scope is
|
|
16
|
+
* refused while a fence is set, rather than silently allowed. Likewise a
|
|
17
|
+
* write this module cannot resolve (an edge given by search, a missing id)
|
|
18
|
+
* is refused — fail closed, never a bypass.
|
|
19
|
+
*
|
|
20
|
+
* Trust note: the workbench marker is a self-reported header. Under the
|
|
21
|
+
* local single-workspace model that is by design (any local process may
|
|
22
|
+
* write; the safety model is human veto plus this fence), so the fence holds
|
|
23
|
+
* cooperating agents to the region the human granted — it is not an
|
|
24
|
+
* authentication boundary.
|
|
25
|
+
*/
|
|
26
|
+
import { type FenceRect, fenceRectFromNodes } from '../shared/scope-fence.js';
|
|
27
|
+
import { canvasState } from './canvas-state.js';
|
|
28
|
+
import type { Operation } from './operations/types.js';
|
|
29
|
+
|
|
30
|
+
/** Bounding box of the fenced nodes plus padding; null when none of them exist. */
|
|
31
|
+
export function scopeFenceRect(fence: { nodeIds: string[]; padding: number }): FenceRect | null {
|
|
32
|
+
const nodes = fence.nodeIds
|
|
33
|
+
.map((id) => canvasState.getNode(id))
|
|
34
|
+
.filter((node): node is NonNullable<typeof node> => node !== undefined);
|
|
35
|
+
return fenceRectFromNodes(nodes, fence.padding);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** What a write touches, in fence terms. */
|
|
39
|
+
export interface FenceTarget {
|
|
40
|
+
/** Existing nodes the write targets — every one must be fenced. */
|
|
41
|
+
nodeIds?: string[];
|
|
42
|
+
/** World points a write introduces (a new node, annotation points) — all must be inside the box. */
|
|
43
|
+
points?: Array<{ x: number; y: number }>;
|
|
44
|
+
/** A new node with no position: refused (it cannot be placed inside the fence). */
|
|
45
|
+
unplacedCreate?: boolean;
|
|
46
|
+
/** Rewrites the whole board — never allowed under a fence. */
|
|
47
|
+
boardWide?: boolean;
|
|
48
|
+
/** The op is not fence-aware — refused while a fence is set. */
|
|
49
|
+
unknown?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function inside(rect: FenceRect, x: number, y: number): boolean {
|
|
53
|
+
return x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Returns a human-readable refusal, or null when the target is inside the fence. */
|
|
57
|
+
export function checkFenceTarget(target: FenceTarget, opName: string, writerKey?: string): string | null {
|
|
58
|
+
const policy = canvasState.getPolicy();
|
|
59
|
+
// Per-agent territory (fleet orchestration) overrides the global fence for
|
|
60
|
+
// that writer; writers without a territory fall under the global fence.
|
|
61
|
+
const territory = writerKey ? policy.agentScopes[writerKey] : undefined;
|
|
62
|
+
const fence = territory ?? policy.scope;
|
|
63
|
+
if (!fence) return null;
|
|
64
|
+
const describe = territory
|
|
65
|
+
? `your assigned territory (${fence.nodeIds.length} node${fence.nodeIds.length === 1 ? '' : 's'}); reads are allowed — ask the orchestrator or the human to widen it`
|
|
66
|
+
: `the agent scope fence (${fence.nodeIds.length} node${fence.nodeIds.length === 1 ? '' : 's'}); reads are allowed — ask the human to widen the fence`;
|
|
67
|
+
if (target.boardWide) return `"${opName}" rewrites the whole board, which is outside ${describe}.`;
|
|
68
|
+
if (target.unknown) return `"${target.unknown}" is not fence-aware and was refused under ${describe}.`;
|
|
69
|
+
const fenced = new Set(fence.nodeIds);
|
|
70
|
+
for (const id of target.nodeIds ?? []) {
|
|
71
|
+
if (!fenced.has(id)) return `node "${id}" is outside ${describe}.`;
|
|
72
|
+
}
|
|
73
|
+
if (target.unplacedCreate) return `new nodes need an explicit x/y inside ${describe}.`;
|
|
74
|
+
if (target.points && target.points.length > 0) {
|
|
75
|
+
const rect = scopeFenceRect(fence);
|
|
76
|
+
if (!rect) return `no fenced node exists to place new content near — outside ${describe}.`;
|
|
77
|
+
for (const point of target.points) {
|
|
78
|
+
if (!Number.isFinite(point.x) || !Number.isFinite(point.y) || !inside(rect, point.x, point.y)) {
|
|
79
|
+
return `position (${point.x}, ${point.y}) is outside ${describe}.`;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function asRecord(value: unknown): Record<string, unknown> {
|
|
87
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record<string, unknown>) : {};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function stringList(value: unknown): string[] {
|
|
91
|
+
return Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === 'string') : [];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function str(value: unknown): string {
|
|
95
|
+
return typeof value === 'string' ? value : '';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Group membership as `node.add` / `node.update` accept it (mirrors `pickGroupChildIds`). */
|
|
99
|
+
function groupChildren(input: Record<string, unknown>): string[] {
|
|
100
|
+
if ('children' in input) return stringList(input.children);
|
|
101
|
+
if ('childIds' in input) return stringList(input.childIds);
|
|
102
|
+
const data = asRecord(input.data);
|
|
103
|
+
return 'children' in data ? stringList(data.children) : [];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function createTarget(input: Record<string, unknown>, extraNodeIds: string[] = []): FenceTarget {
|
|
107
|
+
const x = Number(input.x);
|
|
108
|
+
const y = Number(input.y);
|
|
109
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) return { unplacedCreate: true, nodeIds: extraNodeIds };
|
|
110
|
+
return { points: [{ x, y }], nodeIds: extraNodeIds };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Ops that rewrite the whole board — never allowed under a fence. */
|
|
114
|
+
const BOARD_WIDE_OPS = new Set(['arrange', 'canvas.clear', 'snapshot.restore', 'render.workboard']);
|
|
115
|
+
|
|
116
|
+
/** Describe what a registry op would write, in fence terms. */
|
|
117
|
+
export function describeOpTarget(op: Operation, rawInput: unknown): FenceTarget {
|
|
118
|
+
const input = asRecord(rawInput);
|
|
119
|
+
if (BOARD_WIDE_OPS.has(op.name)) return { boardWide: true };
|
|
120
|
+
switch (op.name) {
|
|
121
|
+
case 'node.update': {
|
|
122
|
+
// A group's membership change reparents (and may reposition) its children.
|
|
123
|
+
const nodeIds = [str(input.id), ...groupChildren(input)];
|
|
124
|
+
// A move's DESTINATION must also land inside the fence — otherwise an
|
|
125
|
+
// agent relocates a fenced node outward, and since the fence rect is
|
|
126
|
+
// derived from live node positions, that grows its own writable region
|
|
127
|
+
// past the human-seeded region.
|
|
128
|
+
const x = Number(input.x);
|
|
129
|
+
const y = Number(input.y);
|
|
130
|
+
if (Number.isFinite(x) && Number.isFinite(y)) return { points: [{ x, y }], nodeIds };
|
|
131
|
+
return { nodeIds };
|
|
132
|
+
}
|
|
133
|
+
case 'node.remove':
|
|
134
|
+
return { nodeIds: [str(input.id)] };
|
|
135
|
+
case 'node.add':
|
|
136
|
+
return createTarget(input, input.type === 'group' ? groupChildren(input) : []);
|
|
137
|
+
case 'jsonrender.add':
|
|
138
|
+
case 'graph.add':
|
|
139
|
+
return createTarget(input);
|
|
140
|
+
case 'jsonrender.stream':
|
|
141
|
+
// Appends to an existing node by `nodeId`, or creates one at x/y.
|
|
142
|
+
return typeof input.nodeId === 'string' ? { nodeIds: [input.nodeId] } : createTarget(input);
|
|
143
|
+
case 'edge.add':
|
|
144
|
+
// Search-resolved endpoints (`fromSearch`/`toSearch`) cannot be checked
|
|
145
|
+
// before resolution and so read as '' — refused, not bypassed.
|
|
146
|
+
return { nodeIds: [str(input.from), str(input.to)] };
|
|
147
|
+
case 'edge.remove': {
|
|
148
|
+
const id = str(input.id) || str(input.edge_id);
|
|
149
|
+
const edge = canvasState.getEdges().find((entry) => entry.id === id);
|
|
150
|
+
return edge ? { nodeIds: [edge.from, edge.to] } : {}; // missing edge: the op 404s on its own
|
|
151
|
+
}
|
|
152
|
+
case 'group.create':
|
|
153
|
+
return { nodeIds: stringList(input.childIds ?? input.children) };
|
|
154
|
+
case 'group.add':
|
|
155
|
+
case 'group.remove':
|
|
156
|
+
return { nodeIds: [str(input.groupId), ...stringList(input.childIds)] };
|
|
157
|
+
case 'annotation.add': {
|
|
158
|
+
const points = Array.isArray(input.points) ? input.points : [];
|
|
159
|
+
return {
|
|
160
|
+
points: points.map((point) => {
|
|
161
|
+
const p = asRecord(point);
|
|
162
|
+
return { x: Number(p.x), y: Number(p.y) };
|
|
163
|
+
}),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
case 'annotation.remove': {
|
|
167
|
+
const annotation = canvasState.getAnnotations().find((entry) => entry.id === str(input.id));
|
|
168
|
+
if (!annotation) return {};
|
|
169
|
+
const { bounds } = annotation;
|
|
170
|
+
return {
|
|
171
|
+
points: [
|
|
172
|
+
{ x: bounds.x, y: bounds.y },
|
|
173
|
+
{ x: bounds.x + bounds.width, y: bounds.y + bounds.height },
|
|
174
|
+
],
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
case 'node.focus':
|
|
178
|
+
return {}; // navigation, not a write
|
|
179
|
+
default:
|
|
180
|
+
return { unknown: op.name };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Registry entry point: refusal text or null. Only call for mutating, agent-originated ops. */
|
|
185
|
+
export function checkScopeFence(op: Operation, rawInput: unknown, writerKey?: string): string | null {
|
|
186
|
+
return checkFenceTarget(describeOpTarget(op, rawInput), op.name, writerKey);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* The GLOBAL fence belongs to the human: an agent must not clear, widen, or
|
|
191
|
+
* replace it through `ax.policy.set`. Per-agent territories (`agentScopes`)
|
|
192
|
+
* are the orchestration surface — an agent MAY set territories for OTHER
|
|
193
|
+
* writers (fencing its workers), but never its own: granting yourself a wider
|
|
194
|
+
* territory (or deleting it) is escalation.
|
|
195
|
+
*/
|
|
196
|
+
export function checkScopeOwnership(rawInput: unknown, writerKey?: string): string | null {
|
|
197
|
+
const input = asRecord(rawInput);
|
|
198
|
+
if ('scope' in input)
|
|
199
|
+
return 'the scope fence is set and cleared by the human in the session panel, not by the agent.';
|
|
200
|
+
const agentScopes = input.agentScopes;
|
|
201
|
+
if (writerKey && agentScopes && typeof agentScopes === 'object' && !Array.isArray(agentScopes)) {
|
|
202
|
+
if (Object.prototype.hasOwnProperty.call(agentScopes, writerKey)) {
|
|
203
|
+
return `an agent cannot set or clear its OWN territory ("${writerKey}") — only the human or a different writer (the orchestrator) may.`;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return null;
|
|
207
|
+
}
|
package/src/server/server.ts
CHANGED
|
@@ -74,9 +74,15 @@ import {
|
|
|
74
74
|
} from './canvas-operations.js';
|
|
75
75
|
import { dispatchOperationRoute, setOperationEventEmitter } from './operations/index.js';
|
|
76
76
|
import { intentRegistry } from './intent-registry.js';
|
|
77
|
+
import { agentPresence } from './agent-presence.js';
|
|
78
|
+
import { HUMAN_STARTED_SESSION_LABEL } from '../shared/agent-presence.js';
|
|
79
|
+
import { diffLayouts } from './mutation-history.js';
|
|
80
|
+
import { humanPresence } from './human-presence.js';
|
|
81
|
+
import { startGateTtlSweeper, stopGateTtlSweeper } from './ax-gate-ttl.js';
|
|
77
82
|
import { setWebviewRunner } from './operations/webview-runner.js';
|
|
78
83
|
import { closeNodeAppSession, nodeAppSessionId } from './operations/ops/nodes.js';
|
|
79
84
|
import { traceManager } from './trace-manager.js';
|
|
85
|
+
import { IFRAME_PROBE_MESSAGE_SOURCE } from '../shared/iframe-probe.js';
|
|
80
86
|
import { buildJsonRenderViewerHtml } from '../json-render/server.js';
|
|
81
87
|
import type { JsonRenderSpec } from '../json-render/server.js';
|
|
82
88
|
|
|
@@ -112,6 +118,80 @@ intentRegistry.setEmitter((event, payload) => {
|
|
|
112
118
|
emitPrimaryWorkbenchEvent(event, payload);
|
|
113
119
|
});
|
|
114
120
|
|
|
121
|
+
// Agent presence (rail-chrome-v2 phase 2) emits its `agent-presence` snapshot
|
|
122
|
+
// frames the same way — including TTL-expiry sweeps, so clients never run
|
|
123
|
+
// their own expiry ticker.
|
|
124
|
+
agentPresence.setEmitter((event, payload) => {
|
|
125
|
+
emitPrimaryWorkbenchEvent(event, payload);
|
|
126
|
+
});
|
|
127
|
+
humanPresence.setEmitter((event, payload) => {
|
|
128
|
+
emitPrimaryWorkbenchEvent(event, payload);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Session receipt (rail-chrome-v2 phase 5, design item 2). The snapshot is
|
|
132
|
+
// taken when a session ATTACHES — the board before the agent touched it — so
|
|
133
|
+
// the receipt's View diff shows exactly what the session did and a restore
|
|
134
|
+
// undoes it. (A detach-time snapshot would equal the current board.)
|
|
135
|
+
agentPresence.setSessionStartListener((presence) => {
|
|
136
|
+
if (canvasState.getLayout().nodes.length === 0) return null;
|
|
137
|
+
const startedAt = new Date().toISOString();
|
|
138
|
+
return canvasState.saveSnapshot(`Before session · ${presence.label} · ${startedAt.slice(11, 16)}`)?.id ?? null;
|
|
139
|
+
});
|
|
140
|
+
agentPresence.setSessionEndListener((presence, startSnapshotId, endedBy) => {
|
|
141
|
+
const ax = canvasState.getAxState();
|
|
142
|
+
const endedAt = new Date().toISOString();
|
|
143
|
+
let snapshot = startSnapshotId
|
|
144
|
+
? (canvasState.listSnapshots({ all: true }).find((entry) => entry.id === startSnapshotId) ?? null)
|
|
145
|
+
: null;
|
|
146
|
+
let unchanged = false;
|
|
147
|
+
// A session that changed NOTHING on the board leaves no snapshot behind —
|
|
148
|
+
// session churn (idle timeouts, receipt tests, attach/detach cycles) was
|
|
149
|
+
// flooding the History drawer with identical boards.
|
|
150
|
+
if (snapshot) {
|
|
151
|
+
const snapData = canvasState.getSnapshotData(snapshot.id);
|
|
152
|
+
if (snapData) {
|
|
153
|
+
const diff = diffLayouts(snapData.name, snapData, canvasState.getLayout());
|
|
154
|
+
if (
|
|
155
|
+
diff.addedNodes.length === 0 &&
|
|
156
|
+
diff.removedNodes.length === 0 &&
|
|
157
|
+
diff.modifiedNodes.length === 0 &&
|
|
158
|
+
diff.addedEdges.length === 0 &&
|
|
159
|
+
diff.removedEdges.length === 0
|
|
160
|
+
) {
|
|
161
|
+
canvasState.deleteSnapshot(snapshot.id);
|
|
162
|
+
snapshot = null;
|
|
163
|
+
unchanged = true;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// A human-started session was named after the agent that filled it; the
|
|
168
|
+
// snapshot taken at attach still carries the placeholder — settle it now.
|
|
169
|
+
if (snapshot?.name.includes(HUMAN_STARTED_SESSION_LABEL) && presence.label !== HUMAN_STARTED_SESSION_LABEL) {
|
|
170
|
+
const name = snapshot.name.replace(HUMAN_STARTED_SESSION_LABEL, presence.label);
|
|
171
|
+
if (canvasState.renameSnapshot(snapshot.id, name)) snapshot.name = name;
|
|
172
|
+
}
|
|
173
|
+
// Three DIFFERENT outcomes, never one "vetoed" pile: a cancelled item was
|
|
174
|
+
// withdrawn (duplicate, scratch), a rejected gate is a human's explicit no,
|
|
175
|
+
// and a held gate is a TTL expiry nobody answered. The receipt tells them apart.
|
|
176
|
+
const counts = {
|
|
177
|
+
items: ax.workItems.length,
|
|
178
|
+
done: ax.workItems.filter((item) => item.status === 'done').length,
|
|
179
|
+
cancelled: ax.workItems.filter((item) => item.status === 'cancelled').length,
|
|
180
|
+
rejected: ax.approvalGates.filter((gate) => gate.status === 'rejected').length,
|
|
181
|
+
held: ax.approvalGates.filter((gate) => gate.status === 'held').length,
|
|
182
|
+
};
|
|
183
|
+
// No agent `sessionId` here: the SSE envelope reserves that key for the
|
|
184
|
+
// workbench session and would overwrite it. The label is what the receipt shows.
|
|
185
|
+
emitPrimaryWorkbenchEvent('agent-session-ended', {
|
|
186
|
+
label: presence.label,
|
|
187
|
+
endedAt,
|
|
188
|
+
endedBy,
|
|
189
|
+
counts,
|
|
190
|
+
unchanged,
|
|
191
|
+
snapshot: snapshot ? { id: snapshot.id, name: snapshot.name } : null,
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
115
195
|
// Webview-runner wiring (plan-008 Wave 3): the webview ops never import this
|
|
116
196
|
// module — the Bun.WebView automation runner is injected here, mirroring the
|
|
117
197
|
// setOperationEventEmitter pattern. The closures call the real automation
|
|
@@ -236,7 +316,9 @@ interface BunWithOptionalWebView {
|
|
|
236
316
|
WebView?: CanvasWebViewConstructor;
|
|
237
317
|
}
|
|
238
318
|
|
|
239
|
-
|
|
319
|
+
// A COLD WebView launch on macOS regularly needs >5s (0.5.0 readiness: the
|
|
320
|
+
// default-backend start timed out, the immediate retry succeeded warm).
|
|
321
|
+
const DEFAULT_CANVAS_AUTOMATION_WEBVIEW_TIMEOUT_MS = 15_000;
|
|
240
322
|
|
|
241
323
|
export interface CanvasAutomationWebViewOptions {
|
|
242
324
|
backend?: 'webkit' | 'chrome';
|
|
@@ -1028,11 +1110,28 @@ function canvasSpaHtml(): string {
|
|
|
1028
1110
|
process.env.AMP_ORB ? '<script>window.__PMX_AMP_ORB = true;</script>' : ''
|
|
1029
1111
|
}
|
|
1030
1112
|
<script>window.__PMX_BOOT_SERVER_VERSION = ${JSON.stringify(serverPackageVersion())};</script>
|
|
1113
|
+
<script>window.__PMX_BOOT_BUNDLE_STAMP = ${JSON.stringify(canvasBundleStamp())};</script>
|
|
1031
1114
|
<script type="module" src="/canvas/index.js?v=${CANVAS_ASSET_VERSION}"></script>
|
|
1032
1115
|
</body>
|
|
1033
1116
|
</html>`;
|
|
1034
1117
|
}
|
|
1035
1118
|
|
|
1119
|
+
/**
|
|
1120
|
+
* Identity of the SERVED client bundle, read once per process: a tab whose
|
|
1121
|
+
* boot stamp differs from the live server's reloads itself on (re)connect.
|
|
1122
|
+
*/
|
|
1123
|
+
let _bundleStamp: string | null = null;
|
|
1124
|
+
function canvasBundleStamp(): string {
|
|
1125
|
+
if (_bundleStamp !== null) return _bundleStamp;
|
|
1126
|
+
try {
|
|
1127
|
+
const stat = statSync(join(resolveCanvasBundleDir(), 'index.js'));
|
|
1128
|
+
_bundleStamp = `${Math.round(stat.mtimeMs)}-${stat.size}`;
|
|
1129
|
+
} catch {
|
|
1130
|
+
_bundleStamp = 'unknown';
|
|
1131
|
+
}
|
|
1132
|
+
return _bundleStamp;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1036
1135
|
const CANVAS_STATIC_MIME: Record<string, string> = {
|
|
1037
1136
|
'.js': 'application/javascript',
|
|
1038
1137
|
'.css': 'text/css',
|
|
@@ -1090,6 +1189,17 @@ function resolveCanvasBundleDir(): string {
|
|
|
1090
1189
|
// the same bundle root that serves /canvas/surface-theme.css; null keeps the
|
|
1091
1190
|
// <link> fallback when the asset is missing (e.g. an unbuilt dev tree).
|
|
1092
1191
|
let _surfaceThemeCss: string | null | undefined;
|
|
1192
|
+
let _mermaidEntryJs: string | null | undefined;
|
|
1193
|
+
function mermaidEntryJsInline(): string | null {
|
|
1194
|
+
if (_mermaidEntryJs !== undefined) return _mermaidEntryJs;
|
|
1195
|
+
try {
|
|
1196
|
+
_mermaidEntryJs = readFileSync(resolve(resolveCanvasBundleDir(), 'mermaid-entry.js'), 'utf-8');
|
|
1197
|
+
} catch {
|
|
1198
|
+
_mermaidEntryJs = null;
|
|
1199
|
+
}
|
|
1200
|
+
return _mermaidEntryJs;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1093
1203
|
function surfaceThemeCssInline(): string | null {
|
|
1094
1204
|
if (_surfaceThemeCss !== undefined) return _surfaceThemeCss;
|
|
1095
1205
|
try {
|
|
@@ -1204,13 +1314,22 @@ function handleFrameDocument(pathname: string): Response {
|
|
|
1204
1314
|
// iframe hosts like the Amp orb portal block them, and the client then falls
|
|
1205
1315
|
// back to fetch() + srcdoc surfaces (see src/client/state/iframe-mode.ts).
|
|
1206
1316
|
function handleIframeProbe(): Response {
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1317
|
+
// The postMessage handshake IS the probe signal: hosts that block sub-frame
|
|
1318
|
+
// requests (the Claude Code desktop browser) still fire `load` on the error
|
|
1319
|
+
// page they commit, so only this document actually executing proves that
|
|
1320
|
+
// src-URL iframes work (see src/client/state/iframe-mode.ts).
|
|
1321
|
+
return new Response(
|
|
1322
|
+
`<!doctype html><title>pmx-canvas iframe probe</title><script>parent.postMessage({ source: ${JSON.stringify(
|
|
1323
|
+
IFRAME_PROBE_MESSAGE_SOURCE,
|
|
1324
|
+
)} }, "*")</script>`,
|
|
1325
|
+
{
|
|
1326
|
+
headers: {
|
|
1327
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
1328
|
+
'Cache-Control': 'no-store',
|
|
1329
|
+
'Content-Security-Policy': 'sandbox allow-scripts',
|
|
1330
|
+
},
|
|
1212
1331
|
},
|
|
1213
|
-
|
|
1332
|
+
);
|
|
1214
1333
|
}
|
|
1215
1334
|
|
|
1216
1335
|
// ── Ext-app recovery diagnostics (0.3.2 report Finding N) ──────
|
|
@@ -1310,18 +1429,22 @@ function handleNodeSurface(pathname: string, url: URL): Response {
|
|
|
1310
1429
|
// Display-only surface: same theme/height plumbing as html nodes, but no AX
|
|
1311
1430
|
// bridge and no presentation mode — the document is our own wrapper around
|
|
1312
1431
|
// escaped diagram source, rendered by /canvas/mermaid-entry.js.
|
|
1313
|
-
const
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1432
|
+
const inlineAssets = url.searchParams.get('inline-assets') === '1';
|
|
1433
|
+
const doc = buildHtmlSurfaceDocument(
|
|
1434
|
+
buildMermaidSurfaceHtml(source, inlineAssets ? mermaidEntryJsInline() : null),
|
|
1435
|
+
{
|
|
1436
|
+
theme,
|
|
1437
|
+
title: surfaceTitle,
|
|
1438
|
+
themeToken: url.searchParams.get('themeToken') ?? undefined,
|
|
1439
|
+
// Content-height reporter nonce (lets the node grow to fit the diagram).
|
|
1440
|
+
...(url.searchParams.get('frameToken')
|
|
1441
|
+
? { contentHeightToken: url.searchParams.get('frameToken') as string }
|
|
1442
|
+
: {}),
|
|
1443
|
+
// Inline the theme tokens so srcdoc-rendered surfaces (Amp orb portals)
|
|
1444
|
+
// are styled without depending on the <link> subresource.
|
|
1445
|
+
...(surfaceThemeCssInline() ? { inlineThemeCss: surfaceThemeCssInline() as string } : {}),
|
|
1446
|
+
},
|
|
1447
|
+
);
|
|
1325
1448
|
return surfaceHtmlResponse(doc, HTML_SURFACE_SANDBOX);
|
|
1326
1449
|
}
|
|
1327
1450
|
|
|
@@ -1931,8 +2054,11 @@ function buildWorkbenchConnectSnapshot(
|
|
|
1931
2054
|
path: primaryWorkbenchPath,
|
|
1932
2055
|
theme: canvasState.theme,
|
|
1933
2056
|
// Lets a long-lived client detect that the server was upgraded under it
|
|
1934
|
-
// (stale in-memory SPA — 0.4.5 report Finding W) and reload once.
|
|
2057
|
+
// (stale in-memory SPA — 0.4.5 report Finding W) and reload once. The
|
|
2058
|
+
// bundle stamp catches DEV rolls too: a rebuild+restart never bumps the
|
|
2059
|
+
// package version, so tabs silently kept month-old JS (rule 3's trap).
|
|
1935
2060
|
version: serverPackageVersion(),
|
|
2061
|
+
bundleStamp: canvasBundleStamp(),
|
|
1936
2062
|
timestamp: new Date().toISOString(),
|
|
1937
2063
|
},
|
|
1938
2064
|
});
|
|
@@ -1994,6 +2120,25 @@ function buildWorkbenchConnectSnapshot(
|
|
|
1994
2120
|
},
|
|
1995
2121
|
});
|
|
1996
2122
|
}
|
|
2123
|
+
// A fresh tab must not start blind to who is on the board: both presence
|
|
2124
|
+
// registries broadcast only on CHANGE, so without these frames a reload
|
|
2125
|
+
// shows no agent cursors/chips and no human cursors until the next move.
|
|
2126
|
+
events.push({
|
|
2127
|
+
event: 'agent-presence',
|
|
2128
|
+
payload: {
|
|
2129
|
+
...(agentPresence.snapshot() as unknown as Record<string, unknown>),
|
|
2130
|
+
reason: 'connect-snapshot',
|
|
2131
|
+
timestamp: new Date().toISOString(),
|
|
2132
|
+
},
|
|
2133
|
+
});
|
|
2134
|
+
events.push({
|
|
2135
|
+
event: 'human-presence',
|
|
2136
|
+
payload: {
|
|
2137
|
+
...(humanPresence.snapshot() as unknown as Record<string, unknown>),
|
|
2138
|
+
reason: 'connect-snapshot',
|
|
2139
|
+
timestamp: new Date().toISOString(),
|
|
2140
|
+
},
|
|
2141
|
+
});
|
|
1997
2142
|
return events;
|
|
1998
2143
|
}
|
|
1999
2144
|
|
|
@@ -2157,7 +2302,6 @@ async function handleCanvasPrompt(req: Request): Promise<Response> {
|
|
|
2157
2302
|
zIndex: 1,
|
|
2158
2303
|
collapsed: false,
|
|
2159
2304
|
pinned: false,
|
|
2160
|
-
dockPosition: null,
|
|
2161
2305
|
data: {
|
|
2162
2306
|
text,
|
|
2163
2307
|
turns: [],
|
|
@@ -2287,7 +2431,6 @@ async function handleCanvasPrompt(req: Request): Promise<Response> {
|
|
|
2287
2431
|
zIndex: 1,
|
|
2288
2432
|
collapsed: false,
|
|
2289
2433
|
pinned: false,
|
|
2290
|
-
dockPosition: null,
|
|
2291
2434
|
data: {
|
|
2292
2435
|
text,
|
|
2293
2436
|
turns: [{ role: 'user', text, status: 'pending' }],
|
|
@@ -2601,9 +2744,8 @@ function syncContextNodeToCanvasState(
|
|
|
2601
2744
|
position: { x: 1130, y: 80 },
|
|
2602
2745
|
size: { width: 320, height: 400 },
|
|
2603
2746
|
zIndex: 1,
|
|
2604
|
-
collapsed:
|
|
2747
|
+
collapsed: false,
|
|
2605
2748
|
pinned: false,
|
|
2606
|
-
dockPosition: 'right',
|
|
2607
2749
|
data: mergedData,
|
|
2608
2750
|
});
|
|
2609
2751
|
return;
|
|
@@ -2612,57 +2754,6 @@ function syncContextNodeToCanvasState(
|
|
|
2612
2754
|
canvasState.updateNode(id, { data: mergedData });
|
|
2613
2755
|
}
|
|
2614
2756
|
|
|
2615
|
-
/**
|
|
2616
|
-
* Seed the docked status (left) + context (right) widgets so a freshly opened
|
|
2617
|
-
* canvas shows them by default — the same nodes the agent-event path creates on
|
|
2618
|
-
* demand (`status-main`, `context-main`), just present from the start.
|
|
2619
|
-
*
|
|
2620
|
-
* First-run only: we bail if the workspace canvas already has persisted state,
|
|
2621
|
-
* so we never add them to a board with content, and — because first-run state is
|
|
2622
|
-
* persisted on save — deleting or undocking them later is respected (they are
|
|
2623
|
-
* not re-seeded). Create-if-missing keeps it idempotent if the agent path
|
|
2624
|
-
* already made one. Returns true if anything was seeded.
|
|
2625
|
-
*/
|
|
2626
|
-
export function ensureDefaultDockedNodes(): boolean {
|
|
2627
|
-
if (canvasState.hasPersistedState()) return false;
|
|
2628
|
-
let seeded = false;
|
|
2629
|
-
// NOTE: these node specs mirror the agent-event create paths below
|
|
2630
|
-
// (`canvas-status` for status-main, `syncContextNodeToCanvasState` for
|
|
2631
|
-
// context-main) — keep geometry/dock defaults in sync if you change them.
|
|
2632
|
-
if (!canvasState.getNode('status-main')) {
|
|
2633
|
-
canvasState.addNode({
|
|
2634
|
-
id: 'status-main',
|
|
2635
|
-
type: 'status',
|
|
2636
|
-
position: { x: 40, y: 80 },
|
|
2637
|
-
size: { width: 300, height: 120 },
|
|
2638
|
-
zIndex: 0,
|
|
2639
|
-
collapsed: true,
|
|
2640
|
-
pinned: false,
|
|
2641
|
-
dockPosition: 'left',
|
|
2642
|
-
data: { phase: 'idle', message: '', elapsed: 0 },
|
|
2643
|
-
});
|
|
2644
|
-
seeded = true;
|
|
2645
|
-
}
|
|
2646
|
-
if (!canvasState.getNode('context-main')) {
|
|
2647
|
-
canvasState.addNode({
|
|
2648
|
-
id: 'context-main',
|
|
2649
|
-
type: 'context',
|
|
2650
|
-
position: { x: 1130, y: 80 },
|
|
2651
|
-
size: { width: 320, height: 400 },
|
|
2652
|
-
zIndex: 1,
|
|
2653
|
-
collapsed: true,
|
|
2654
|
-
pinned: false,
|
|
2655
|
-
dockPosition: 'right',
|
|
2656
|
-
data: { cards: [], auxTabs: [] },
|
|
2657
|
-
});
|
|
2658
|
-
seeded = true;
|
|
2659
|
-
}
|
|
2660
|
-
if (seeded) {
|
|
2661
|
-
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
2662
|
-
}
|
|
2663
|
-
return seeded;
|
|
2664
|
-
}
|
|
2665
|
-
|
|
2666
2757
|
// Maps responseNodeId -> thread prompt node ID for O(1) routing of response events
|
|
2667
2758
|
const serverResponseToThreadMap = new Map<string, string>();
|
|
2668
2759
|
|
|
@@ -2683,7 +2774,6 @@ function syncEventToCanvasState(event: string, payload: PrimaryWorkbenchEventPay
|
|
|
2683
2774
|
zIndex: 1,
|
|
2684
2775
|
collapsed: false,
|
|
2685
2776
|
pinned: false,
|
|
2686
|
-
dockPosition: null,
|
|
2687
2777
|
data: { path, title, content: '', rendered: '' },
|
|
2688
2778
|
});
|
|
2689
2779
|
} else {
|
|
@@ -2772,7 +2862,6 @@ function syncEventToCanvasState(event: string, payload: PrimaryWorkbenchEventPay
|
|
|
2772
2862
|
zIndex: 1,
|
|
2773
2863
|
collapsed: false,
|
|
2774
2864
|
pinned: false,
|
|
2775
|
-
dockPosition: null,
|
|
2776
2865
|
data: dataPatch,
|
|
2777
2866
|
});
|
|
2778
2867
|
}
|
|
@@ -2836,6 +2925,12 @@ function syncEventToCanvasState(event: string, payload: PrimaryWorkbenchEventPay
|
|
|
2836
2925
|
utilization: payload.utilization,
|
|
2837
2926
|
nearLimit: payload.nearLimit,
|
|
2838
2927
|
});
|
|
2928
|
+
// The same numbers feed the top-bar context meter of the attached session.
|
|
2929
|
+
const used = Number(payload.currentTokens);
|
|
2930
|
+
const total = Number(payload.tokenLimit);
|
|
2931
|
+
if (Number.isFinite(used) && Number.isFinite(total) && total > 0) {
|
|
2932
|
+
agentPresence.reportContextUsage({ used: Math.max(0, used), total });
|
|
2933
|
+
}
|
|
2839
2934
|
} else if (event === 'aux-open') {
|
|
2840
2935
|
const existing = canvasState.getNode('context-main');
|
|
2841
2936
|
const auxTabs = Array.isArray(existing?.data.auxTabs)
|
|
@@ -2864,7 +2959,6 @@ function syncEventToCanvasState(event: string, payload: PrimaryWorkbenchEventPay
|
|
|
2864
2959
|
zIndex: 0,
|
|
2865
2960
|
collapsed: false,
|
|
2866
2961
|
pinned: false,
|
|
2867
|
-
dockPosition: 'left',
|
|
2868
2962
|
data: { phase: 'idle', message: '', elapsed: 0 },
|
|
2869
2963
|
});
|
|
2870
2964
|
}
|
|
@@ -2901,7 +2995,6 @@ function syncEventToCanvasState(event: string, payload: PrimaryWorkbenchEventPay
|
|
|
2901
2995
|
zIndex: 1,
|
|
2902
2996
|
collapsed: false,
|
|
2903
2997
|
pinned: false,
|
|
2904
|
-
dockPosition: null,
|
|
2905
2998
|
data: { content: '', status: 'streaming', promptNodeId },
|
|
2906
2999
|
});
|
|
2907
3000
|
|
|
@@ -3122,6 +3215,8 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
3122
3215
|
canvasState.setWorkspaceRoot(activeWorkspaceRoot);
|
|
3123
3216
|
canvasState.setTheme(initialCanvasThemeSetting as CanvasTheme);
|
|
3124
3217
|
const loaded = canvasState.loadFromDisk({ clearExisting: true });
|
|
3218
|
+
// Unattended-approval policy: hold pending gates whose TTL elapsed.
|
|
3219
|
+
startGateTtlSweeper((event, payload) => emitPrimaryWorkbenchEvent(event, payload));
|
|
3125
3220
|
setCanvasLayoutUpdateEmitter(() => {
|
|
3126
3221
|
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
3127
3222
|
});
|
|
@@ -3333,6 +3428,9 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
3333
3428
|
}
|
|
3334
3429
|
|
|
3335
3430
|
export function stopCanvasServer(): void {
|
|
3431
|
+
stopGateTtlSweeper();
|
|
3432
|
+
agentPresence.reset();
|
|
3433
|
+
humanPresence.reset();
|
|
3336
3434
|
intentRegistry.reset();
|
|
3337
3435
|
canvasState.close();
|
|
3338
3436
|
closeAllMcpAppSessions();
|
|
@@ -101,7 +101,6 @@ class TraceManager {
|
|
|
101
101
|
zIndex: 0,
|
|
102
102
|
collapsed: false,
|
|
103
103
|
pinned: true,
|
|
104
|
-
dockPosition: null,
|
|
105
104
|
data: {
|
|
106
105
|
toolName: payload.name,
|
|
107
106
|
category,
|
|
@@ -199,7 +198,6 @@ class TraceManager {
|
|
|
199
198
|
zIndex: 0,
|
|
200
199
|
collapsed: false,
|
|
201
200
|
pinned: true,
|
|
202
|
-
dockPosition: null,
|
|
203
201
|
data: {
|
|
204
202
|
toolName: payload.agentDisplayName ?? payload.agentName,
|
|
205
203
|
category: 'subagent' as TraceCategory,
|