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
|
@@ -218,8 +218,17 @@ function escapeSurfaceHtml(value: string): string {
|
|
|
218
218
|
* of the main SPA bundle). Escaping is mandatory: the source is arbitrary node
|
|
219
219
|
* data and must not be parsed as markup.
|
|
220
220
|
*/
|
|
221
|
-
export function buildMermaidSurfaceHtml(source: string): string {
|
|
222
|
-
|
|
221
|
+
export function buildMermaidSurfaceHtml(source: string, inlineEntry?: string | null): string {
|
|
222
|
+
// srcdoc-rendered surfaces in hosts that block sub-frame subresources (the
|
|
223
|
+
// Claude Code desktop browser blocks the script request too, not just the
|
|
224
|
+
// frame document) get the renderer embedded as a data: URL — no network
|
|
225
|
+
// request for the host to block, and base64 has no `<` for the HTML script
|
|
226
|
+
// parser to trip on (a 3.5MB bundle inlined as literal script text hits the
|
|
227
|
+
// `<!--` script-data-escaped states and renders as page text).
|
|
228
|
+
const entry = inlineEntry
|
|
229
|
+
? `<script src="data:text/javascript;charset=utf-8;base64,${Buffer.from(inlineEntry, 'utf-8').toString('base64')}"></script>`
|
|
230
|
+
: '<script src="/canvas/mermaid-entry.js"></script>';
|
|
231
|
+
return `<pre class="mermaid-source" style="display:none">${escapeSurfaceHtml(source)}</pre>${entry}`;
|
|
223
232
|
}
|
|
224
233
|
|
|
225
234
|
export interface HtmlSurfaceOptions {
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import {
|
|
3
|
+
HUMAN_GRAB_TTL_MS,
|
|
4
|
+
HUMAN_PRESENCE_TTL_MS,
|
|
5
|
+
type HumanPresence,
|
|
6
|
+
type HumanPresenceSnapshot,
|
|
7
|
+
MAX_HUMANS,
|
|
8
|
+
} from '../shared/human-presence.js';
|
|
9
|
+
import { OperationError } from './operations/types.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Registry of open workbench tabs (rail-chrome-v2 phase 8). Fed by the
|
|
13
|
+
* browser's own `human.presence.set` heartbeats; broadcast as one coalesced
|
|
14
|
+
* `human-presence` SSE frame on every change including expiry, so clients
|
|
15
|
+
* never run their own expiry timers. `lockedNodes()` is the edit lock the
|
|
16
|
+
* operation registry consults for agent writes (user wins).
|
|
17
|
+
*/
|
|
18
|
+
export const HUMAN_PRESENCE_SHAPE = {
|
|
19
|
+
clientId: z.string().min(1).max(64),
|
|
20
|
+
name: z.string().min(1).max(40).optional(),
|
|
21
|
+
cursor: z.object({ x: z.number().finite(), y: z.number().finite() }).nullable().optional(),
|
|
22
|
+
grabbingNodeId: z.string().max(200).nullable().optional(),
|
|
23
|
+
/** Explicit leave (tab closing): drop the presence now. */
|
|
24
|
+
left: z.boolean().optional(),
|
|
25
|
+
};
|
|
26
|
+
const presenceSchema = z.object(HUMAN_PRESENCE_SHAPE);
|
|
27
|
+
|
|
28
|
+
interface Stored extends HumanPresence {
|
|
29
|
+
lastSeenMs: number;
|
|
30
|
+
grabSinceMs: number | null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type Emitter = (event: string, payload: Record<string, unknown>) => void;
|
|
34
|
+
|
|
35
|
+
export class HumanPresenceRegistry {
|
|
36
|
+
private readonly humans = new Map<string, Stored>();
|
|
37
|
+
private emit: Emitter = () => {};
|
|
38
|
+
private sweepTimer: ReturnType<typeof setInterval> | null = null;
|
|
39
|
+
private emitTimer: ReturnType<typeof setTimeout> | null = null;
|
|
40
|
+
|
|
41
|
+
setEmitter(emitter: Emitter | null): void {
|
|
42
|
+
this.emit = emitter ?? (() => {});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
set(raw: unknown, now = Date.now()): HumanPresence | null {
|
|
46
|
+
const parsed = presenceSchema.safeParse(raw);
|
|
47
|
+
if (!parsed.success) {
|
|
48
|
+
throw new OperationError(
|
|
49
|
+
`Invalid human presence: ${parsed.error.issues.map((issue) => issue.message).join('; ')}`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
const input = parsed.data;
|
|
53
|
+
if (input.left) {
|
|
54
|
+
if (this.humans.delete(input.clientId)) this.scheduleEmit();
|
|
55
|
+
this.maybeStopSweeper();
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
const existing = this.humans.get(input.clientId);
|
|
59
|
+
const stored: Stored = existing ?? {
|
|
60
|
+
clientId: input.clientId,
|
|
61
|
+
name: input.name ?? 'Human',
|
|
62
|
+
cursor: null,
|
|
63
|
+
grabbingNodeId: null,
|
|
64
|
+
lastSeenAt: new Date(now).toISOString(),
|
|
65
|
+
lastSeenMs: now,
|
|
66
|
+
grabSinceMs: null,
|
|
67
|
+
};
|
|
68
|
+
stored.lastSeenMs = now;
|
|
69
|
+
stored.lastSeenAt = new Date(now).toISOString();
|
|
70
|
+
if (input.name) stored.name = input.name;
|
|
71
|
+
if (input.cursor !== undefined) stored.cursor = input.cursor;
|
|
72
|
+
if (input.grabbingNodeId !== undefined) {
|
|
73
|
+
if (input.grabbingNodeId !== stored.grabbingNodeId) stored.grabSinceMs = input.grabbingNodeId ? now : null;
|
|
74
|
+
else if (input.grabbingNodeId) stored.grabSinceMs = now; // renewed
|
|
75
|
+
stored.grabbingNodeId = input.grabbingNodeId;
|
|
76
|
+
}
|
|
77
|
+
this.humans.set(input.clientId, stored);
|
|
78
|
+
while (this.humans.size > MAX_HUMANS) {
|
|
79
|
+
const oldest = [...this.humans.values()].sort((a, b) => a.lastSeenMs - b.lastSeenMs)[0];
|
|
80
|
+
if (!oldest) break;
|
|
81
|
+
this.humans.delete(oldest.clientId);
|
|
82
|
+
}
|
|
83
|
+
this.ensureSweeper();
|
|
84
|
+
this.scheduleEmit();
|
|
85
|
+
return this.publicView(stored);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
snapshot(now = Date.now()): HumanPresenceSnapshot {
|
|
89
|
+
this.sweep(now, { emit: false });
|
|
90
|
+
return { humans: [...this.humans.values()].map((stored) => this.publicView(stored)) };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Nodes a human is holding right now → that human's name. The edit lock. */
|
|
94
|
+
lockedNodes(now = Date.now()): Map<string, string> {
|
|
95
|
+
const locked = new Map<string, string>();
|
|
96
|
+
for (const human of this.humans.values()) {
|
|
97
|
+
if (human.grabbingNodeId && human.grabSinceMs !== null && now - human.grabSinceMs <= HUMAN_GRAB_TTL_MS) {
|
|
98
|
+
locked.set(human.grabbingNodeId, human.name);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return locked;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
reset(): void {
|
|
105
|
+
this.humans.clear();
|
|
106
|
+
if (this.emitTimer) {
|
|
107
|
+
clearTimeout(this.emitTimer);
|
|
108
|
+
this.emitTimer = null;
|
|
109
|
+
}
|
|
110
|
+
this.maybeStopSweeper();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private publicView(stored: Stored): HumanPresence {
|
|
114
|
+
const { lastSeenMs: _lastSeenMs, grabSinceMs, ...human } = stored;
|
|
115
|
+
// An expired grab reads as released even before the sweep drops it.
|
|
116
|
+
const grabbing =
|
|
117
|
+
human.grabbingNodeId && grabSinceMs !== null && Date.now() - grabSinceMs <= HUMAN_GRAB_TTL_MS
|
|
118
|
+
? human.grabbingNodeId
|
|
119
|
+
: null;
|
|
120
|
+
return { ...human, grabbingNodeId: grabbing };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private sweep(now = Date.now(), options: { emit?: boolean } = {}): void {
|
|
124
|
+
let changed = false;
|
|
125
|
+
for (const [key, human] of this.humans) {
|
|
126
|
+
if (human.lastSeenMs + HUMAN_PRESENCE_TTL_MS <= now) {
|
|
127
|
+
this.humans.delete(key);
|
|
128
|
+
changed = true;
|
|
129
|
+
} else if (human.grabbingNodeId && human.grabSinceMs !== null && now - human.grabSinceMs > HUMAN_GRAB_TTL_MS) {
|
|
130
|
+
human.grabbingNodeId = null;
|
|
131
|
+
human.grabSinceMs = null;
|
|
132
|
+
changed = true;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (changed && options.emit !== false) this.scheduleEmit();
|
|
136
|
+
this.maybeStopSweeper();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private scheduleEmit(): void {
|
|
140
|
+
if (this.emitTimer) return;
|
|
141
|
+
this.emitTimer = setTimeout(() => {
|
|
142
|
+
this.emitTimer = null;
|
|
143
|
+
this.emit('human-presence', this.snapshot() as unknown as Record<string, unknown>);
|
|
144
|
+
}, 16);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private ensureSweeper(): void {
|
|
148
|
+
if (this.sweepTimer) return;
|
|
149
|
+
this.sweepTimer = setInterval(() => this.sweep(), 1000);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private maybeStopSweeper(): void {
|
|
153
|
+
if (this.humans.size === 0 && this.sweepTimer) {
|
|
154
|
+
clearInterval(this.sweepTimer);
|
|
155
|
+
this.sweepTimer = null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export const humanPresence = new HumanPresenceRegistry();
|
package/src/server/index.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { agentPresence } from './agent-presence.js';
|
|
3
|
+
import { checkFenceTarget, type FenceTarget } from './scope-fence.js';
|
|
4
|
+
import type { AgentPhase, AgentPresence, AgentPresenceSnapshot } from '../shared/agent-presence.js';
|
|
2
5
|
import { canvasState, IMAGE_MIME_MAP } from './canvas-state.js';
|
|
3
6
|
import type { CanvasAnnotation, CanvasNodeState, CanvasEdge, CanvasLayout, ViewportState } from './canvas-state.js';
|
|
4
7
|
import { buildCanvasAxContext } from './ax-context.js';
|
|
@@ -132,6 +135,31 @@ function toSdkNode(node: CanvasNodeState): SdkCanvasNode {
|
|
|
132
135
|
return { ...serializeCanvasNode(node), nodeId: node.id };
|
|
133
136
|
}
|
|
134
137
|
|
|
138
|
+
/**
|
|
139
|
+
* The SDK bypasses the operation registry, so each mutation describes what it
|
|
140
|
+
* writes to the same fence core the registry uses (scope-fence.ts). Reads and
|
|
141
|
+
* presence/AX calls are never fenced.
|
|
142
|
+
*/
|
|
143
|
+
function assertInsideFence(opName: string, target: FenceTarget): void {
|
|
144
|
+
const refusal = checkFenceTarget(target, opName);
|
|
145
|
+
if (refusal) throw new Error(`Outside the agent scope: ${refusal}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function createTarget(input: { x?: number; y?: number }, nodeIds: string[] = []): FenceTarget {
|
|
149
|
+
return typeof input.x === 'number' && typeof input.y === 'number'
|
|
150
|
+
? { points: [{ x: input.x, y: input.y }], nodeIds }
|
|
151
|
+
: { unplacedCreate: true, nodeIds };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Presence `detail` for SDK writes, by the intent kind the wrapper commits. */
|
|
155
|
+
const SDK_PRESENCE_DETAIL: Record<PmxAxIntentKind, string> = {
|
|
156
|
+
create: 'node.add',
|
|
157
|
+
move: 'node.move',
|
|
158
|
+
edit: 'node.update',
|
|
159
|
+
remove: 'node.remove',
|
|
160
|
+
connect: 'edge.add',
|
|
161
|
+
};
|
|
162
|
+
|
|
135
163
|
export class PmxCanvas extends EventEmitter {
|
|
136
164
|
private _port: number;
|
|
137
165
|
private _server: string | null = null;
|
|
@@ -146,6 +174,27 @@ export class PmxCanvas extends EventEmitter {
|
|
|
146
174
|
allowedKinds: readonly PmxAxIntentKind[],
|
|
147
175
|
mutate: () => T,
|
|
148
176
|
settledNodeId: (result: T) => string | undefined,
|
|
177
|
+
): T {
|
|
178
|
+
const result = this.runIntentCommitInner(intentId, allowedKinds, mutate, settledNodeId);
|
|
179
|
+
// SDK writes bypass executeOperation, so they register agent presence
|
|
180
|
+
// here — the same derived `tooling` + focus-node touch the HTTP/MCP
|
|
181
|
+
// transports get, attributed to the attached session by the registry.
|
|
182
|
+
const focusNodeId = settledNodeId(result);
|
|
183
|
+
agentPresence.touch({
|
|
184
|
+
source: 'sdk',
|
|
185
|
+
phase: 'tooling',
|
|
186
|
+
detail: SDK_PRESENCE_DETAIL[allowedKinds[0] ?? 'edit'],
|
|
187
|
+
...(focusNodeId ? { focusNodeId } : {}),
|
|
188
|
+
op: true,
|
|
189
|
+
});
|
|
190
|
+
return result;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private runIntentCommitInner<T>(
|
|
194
|
+
intentId: string | undefined,
|
|
195
|
+
allowedKinds: readonly PmxAxIntentKind[],
|
|
196
|
+
mutate: () => T,
|
|
197
|
+
settledNodeId: (result: T) => string | undefined,
|
|
149
198
|
): T {
|
|
150
199
|
if (intentId === undefined) return mutate();
|
|
151
200
|
intentRegistry.beginCommit(intentId, allowedKinds);
|
|
@@ -246,6 +295,10 @@ export class PmxCanvas extends EventEmitter {
|
|
|
246
295
|
height?: number;
|
|
247
296
|
strictSize?: boolean;
|
|
248
297
|
}): SdkCanvasNode {
|
|
298
|
+
assertInsideFence(
|
|
299
|
+
'node.add',
|
|
300
|
+
createTarget(input, input.type === 'group' ? (input.childIds ?? input.children ?? []) : []),
|
|
301
|
+
);
|
|
249
302
|
return this.runIntentCommit(
|
|
250
303
|
input.intentId,
|
|
251
304
|
['create'],
|
|
@@ -296,6 +349,7 @@ export class PmxCanvas extends EventEmitter {
|
|
|
296
349
|
height?: number;
|
|
297
350
|
strictSize?: boolean;
|
|
298
351
|
}): Promise<{ ok: boolean; id: string; error?: string; fetch: { ok: boolean; error?: string } }> {
|
|
352
|
+
assertInsideFence('node.add', createTarget(input));
|
|
299
353
|
const mutate = async () => {
|
|
300
354
|
const { id } = addCanvasNode({
|
|
301
355
|
type: 'webpage',
|
|
@@ -330,6 +384,7 @@ export class PmxCanvas extends EventEmitter {
|
|
|
330
384
|
}
|
|
331
385
|
|
|
332
386
|
updateNode(id: string, patch: Partial<CanvasNodeState> & Record<string, unknown>): void {
|
|
387
|
+
assertInsideFence('node.update', { nodeIds: [id] });
|
|
333
388
|
const intentId = typeof patch.intentId === 'string' ? patch.intentId : undefined;
|
|
334
389
|
this.runIntentCommit(
|
|
335
390
|
intentId,
|
|
@@ -354,6 +409,7 @@ export class PmxCanvas extends EventEmitter {
|
|
|
354
409
|
|
|
355
410
|
/** Remove a node. Missing id throws (plan-005 unifies this across surfaces). */
|
|
356
411
|
removeNode(id: string, options?: { intentId?: string }): void {
|
|
412
|
+
assertInsideFence('node.remove', { nodeIds: [id] });
|
|
357
413
|
this.runIntentCommit(
|
|
358
414
|
options?.intentId,
|
|
359
415
|
['remove'],
|
|
@@ -382,6 +438,7 @@ export class PmxCanvas extends EventEmitter {
|
|
|
382
438
|
style?: CanvasEdge['style'];
|
|
383
439
|
animated?: boolean;
|
|
384
440
|
}): string {
|
|
441
|
+
assertInsideFence('edge.add', { nodeIds: [input.from ?? '', input.to ?? ''] });
|
|
385
442
|
return this.runIntentCommit(
|
|
386
443
|
input.intentId,
|
|
387
444
|
['connect'],
|
|
@@ -412,6 +469,8 @@ export class PmxCanvas extends EventEmitter {
|
|
|
412
469
|
}
|
|
413
470
|
|
|
414
471
|
removeEdge(id: string): void {
|
|
472
|
+
const edge = canvasState.getEdges().find((entry) => entry.id === id);
|
|
473
|
+
if (edge) assertInsideFence('edge.remove', { nodeIds: [edge.from, edge.to] });
|
|
415
474
|
removeCanvasEdge(id);
|
|
416
475
|
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
417
476
|
}
|
|
@@ -431,6 +490,7 @@ export class PmxCanvas extends EventEmitter {
|
|
|
431
490
|
color?: string;
|
|
432
491
|
childLayout?: 'grid' | 'column' | 'flow';
|
|
433
492
|
}): string {
|
|
493
|
+
assertInsideFence('group.create', { nodeIds: input.childIds ?? [] });
|
|
434
494
|
return this.runIntentCommit(
|
|
435
495
|
input.intentId,
|
|
436
496
|
['create'],
|
|
@@ -449,6 +509,7 @@ export class PmxCanvas extends EventEmitter {
|
|
|
449
509
|
childIds: string[],
|
|
450
510
|
options?: { childLayout?: 'grid' | 'column' | 'flow'; intentId?: string },
|
|
451
511
|
): boolean {
|
|
512
|
+
assertInsideFence('group.add', { nodeIds: [groupId, ...childIds] });
|
|
452
513
|
return this.runIntentCommit(
|
|
453
514
|
options?.intentId,
|
|
454
515
|
['edit'],
|
|
@@ -466,8 +527,9 @@ export class PmxCanvas extends EventEmitter {
|
|
|
466
527
|
);
|
|
467
528
|
}
|
|
468
529
|
|
|
469
|
-
/**
|
|
530
|
+
/** Dissolve a group: children released (into the enclosing group when nested), frame removed — one undo step. */
|
|
470
531
|
ungroupNodes(groupId: string, options?: { intentId?: string }): boolean {
|
|
532
|
+
assertInsideFence('group.remove', { nodeIds: [groupId] });
|
|
471
533
|
return this.runIntentCommit(
|
|
472
534
|
options?.intentId,
|
|
473
535
|
['edit'],
|
|
@@ -486,6 +548,7 @@ export class PmxCanvas extends EventEmitter {
|
|
|
486
548
|
}
|
|
487
549
|
|
|
488
550
|
clear(): void {
|
|
551
|
+
assertInsideFence('canvas.clear', { boardWide: true });
|
|
489
552
|
for (const node of canvasState.getLayout().nodes) {
|
|
490
553
|
if (node.type !== 'mcp-app') continue;
|
|
491
554
|
const sessionId = typeof node.data.appSessionId === 'string' ? node.data.appSessionId : '';
|
|
@@ -496,6 +559,7 @@ export class PmxCanvas extends EventEmitter {
|
|
|
496
559
|
}
|
|
497
560
|
|
|
498
561
|
arrange(layout?: 'grid' | 'column' | 'flow'): void {
|
|
562
|
+
assertInsideFence('arrange', { boardWide: true });
|
|
499
563
|
arrangeCanvasNodes(layout ?? 'grid');
|
|
500
564
|
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
501
565
|
}
|
|
@@ -562,8 +626,8 @@ export class PmxCanvas extends EventEmitter {
|
|
|
562
626
|
return steering;
|
|
563
627
|
}
|
|
564
628
|
|
|
565
|
-
markSteeringDelivered(id: string): boolean {
|
|
566
|
-
const ok = canvasState.markSteeringDelivered(id);
|
|
629
|
+
markSteeringDelivered(id: string, consumer?: string | null): boolean {
|
|
630
|
+
const ok = canvasState.markSteeringDelivered(id, consumer);
|
|
567
631
|
if (ok) emitPrimaryWorkbenchEvent('ax-event-created', { steeringDelivered: id });
|
|
568
632
|
return ok;
|
|
569
633
|
}
|
|
@@ -667,6 +731,16 @@ export class PmxCanvas extends EventEmitter {
|
|
|
667
731
|
return approvalGate;
|
|
668
732
|
}
|
|
669
733
|
|
|
734
|
+
/** Reopen a resolved (typically auto-held) approval gate with a fresh TTL. */
|
|
735
|
+
reopenApproval(id: string, options?: { ttlMs?: number; source?: PmxAxSource }): PmxAxApprovalGate | null {
|
|
736
|
+
const approvalGate = canvasState.reopenApproval(id, {
|
|
737
|
+
...(options?.ttlMs !== undefined ? { ttlMs: options.ttlMs } : {}),
|
|
738
|
+
source: options?.source ?? 'sdk',
|
|
739
|
+
});
|
|
740
|
+
if (approvalGate) emitPrimaryWorkbenchEvent('ax-state-changed', { approvalGate });
|
|
741
|
+
return approvalGate;
|
|
742
|
+
}
|
|
743
|
+
|
|
670
744
|
addEvidence(
|
|
671
745
|
input: {
|
|
672
746
|
kind: PmxAxEvidenceKind;
|
|
@@ -726,6 +800,30 @@ export class PmxCanvas extends EventEmitter {
|
|
|
726
800
|
return host;
|
|
727
801
|
}
|
|
728
802
|
|
|
803
|
+
/** Who is writing to this board right now, their phase, and the context budget. */
|
|
804
|
+
getAgentPresence(): AgentPresenceSnapshot {
|
|
805
|
+
return agentPresence.snapshot();
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* Update this SDK caller's presence (rail-chrome-v2 phase 2): phase, detail,
|
|
810
|
+
* focus node, cursor, and `attached` (true starts a session, false ends it).
|
|
811
|
+
*/
|
|
812
|
+
setAgentPresence(
|
|
813
|
+
input: {
|
|
814
|
+
agentId?: string | null;
|
|
815
|
+
label?: string;
|
|
816
|
+
phase?: AgentPhase;
|
|
817
|
+
detail?: string | null;
|
|
818
|
+
focusNodeId?: string | null;
|
|
819
|
+
cursor?: { x: number; y: number } | null;
|
|
820
|
+
attached?: boolean;
|
|
821
|
+
},
|
|
822
|
+
options?: { source?: PmxAxSource },
|
|
823
|
+
): AgentPresence {
|
|
824
|
+
return agentPresence.set(input, options?.source ?? 'sdk');
|
|
825
|
+
}
|
|
826
|
+
|
|
729
827
|
listElicitations(): PmxAxElicitation[] {
|
|
730
828
|
return canvasState.getElicitations();
|
|
731
829
|
}
|
|
@@ -957,9 +1055,9 @@ export class PmxCanvas extends EventEmitter {
|
|
|
957
1055
|
position?: { x: number; y: number };
|
|
958
1056
|
size?: { width: number; height: number };
|
|
959
1057
|
collapsed?: boolean;
|
|
960
|
-
dockPosition?: 'left' | 'right' | null;
|
|
961
1058
|
}>,
|
|
962
1059
|
): { applied: number; skipped: number } {
|
|
1060
|
+
assertInsideFence('node.update', { nodeIds: updates.map((update) => update.id) });
|
|
963
1061
|
return applyCanvasNodeUpdates(updates);
|
|
964
1062
|
}
|
|
965
1063
|
|
|
@@ -978,6 +1076,7 @@ export class PmxCanvas extends EventEmitter {
|
|
|
978
1076
|
}
|
|
979
1077
|
|
|
980
1078
|
async restoreSnapshot(id: string): Promise<{ ok: boolean }> {
|
|
1079
|
+
assertInsideFence('snapshot.restore', { boardWide: true });
|
|
981
1080
|
const result = await restoreCanvasSnapshot(id);
|
|
982
1081
|
if (result.ok) {
|
|
983
1082
|
emitPrimaryWorkbenchEvent('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
@@ -1076,6 +1175,7 @@ export class PmxCanvas extends EventEmitter {
|
|
|
1076
1175
|
url: string;
|
|
1077
1176
|
spec: JsonRenderSpec;
|
|
1078
1177
|
} {
|
|
1178
|
+
assertInsideFence('jsonrender.add', createTarget(input));
|
|
1079
1179
|
return this.runIntentCommit(
|
|
1080
1180
|
input.intentId,
|
|
1081
1181
|
['create'],
|
|
@@ -1160,6 +1260,7 @@ export class PmxCanvas extends EventEmitter {
|
|
|
1160
1260
|
* the html capability ceiling server-side; cannot escalate. */
|
|
1161
1261
|
axCapabilities?: { enabled?: boolean; allowed?: string[] };
|
|
1162
1262
|
}): SdkCanvasNode {
|
|
1263
|
+
assertInsideFence('node.add', createTarget(input));
|
|
1163
1264
|
return this.runIntentCommit(
|
|
1164
1265
|
input.intentId,
|
|
1165
1266
|
['create'],
|
|
@@ -1206,6 +1307,7 @@ export class PmxCanvas extends EventEmitter {
|
|
|
1206
1307
|
height?: number;
|
|
1207
1308
|
strictSize?: boolean;
|
|
1208
1309
|
}): { id: string; kind: HtmlPrimitiveKind; title: string; htmlBytes: number } {
|
|
1310
|
+
assertInsideFence('node.add', createTarget(input));
|
|
1209
1311
|
return this.runIntentCommit(
|
|
1210
1312
|
input.intentId,
|
|
1211
1313
|
['create'],
|
|
@@ -1243,6 +1345,7 @@ export class PmxCanvas extends EventEmitter {
|
|
|
1243
1345
|
}
|
|
1244
1346
|
|
|
1245
1347
|
addGraphNode(input: GraphNodeInput & { intentId?: string }): { id: string; url: string; spec: JsonRenderSpec } {
|
|
1348
|
+
assertInsideFence('graph.add', createTarget(input));
|
|
1246
1349
|
return this.runIntentCommit(
|
|
1247
1350
|
input.intentId,
|
|
1248
1351
|
['create'],
|
|
@@ -223,6 +223,19 @@ export class IntentRegistry {
|
|
|
223
223
|
* synchronous: once this method has accepted the intent, a later veto cannot
|
|
224
224
|
* race in between the check and the mutation.
|
|
225
225
|
*/
|
|
226
|
+
/**
|
|
227
|
+
* How a linked commit would fare right now. 'absent' = the ghost expired
|
|
228
|
+
* quietly (TTL sweep) with no veto — the mutation should proceed UNLINKED
|
|
229
|
+
* instead of failing: a slow spawn is not a human "no", and inside a batch
|
|
230
|
+
* the 409 poisoned every sibling op (orchestration review #5).
|
|
231
|
+
*/
|
|
232
|
+
commitDisposition(id: string): 'live' | 'vetoed' | 'committing' | 'absent' {
|
|
233
|
+
this.pruneVetoTombstones();
|
|
234
|
+
if (this.committingIntentIds.has(id)) return 'committing';
|
|
235
|
+
if (this.vetoedIntentIds.has(id)) return 'vetoed';
|
|
236
|
+
return this.intents.has(id) ? 'live' : 'absent';
|
|
237
|
+
}
|
|
238
|
+
|
|
226
239
|
beginCommit(id: string, allowedKinds: readonly PmxAxIntentKind[]): PmxAxIntent {
|
|
227
240
|
this.pruneVetoTombstones();
|
|
228
241
|
if (this.committingIntentIds.has(id)) {
|
|
@@ -21,6 +21,7 @@ export type MutationOp =
|
|
|
21
21
|
| 'updateNode'
|
|
22
22
|
| 'removeNode'
|
|
23
23
|
| 'addEdge'
|
|
24
|
+
| 'updateEdge'
|
|
24
25
|
| 'removeEdge'
|
|
25
26
|
| 'addAnnotation'
|
|
26
27
|
| 'removeAnnotation'
|
|
@@ -43,13 +44,20 @@ export type MutationOp =
|
|
|
43
44
|
| 'batch'
|
|
44
45
|
| 'viewport'
|
|
45
46
|
| 'groupNodes'
|
|
46
|
-
| '
|
|
47
|
+
| 'releaseGroupChildren';
|
|
48
|
+
|
|
49
|
+
/** Who made a mutation: the human in the workbench, or an agent through any transport. */
|
|
50
|
+
export type MutationActor = 'human' | 'agent';
|
|
47
51
|
|
|
48
52
|
export interface MutationEntry {
|
|
49
53
|
id: string;
|
|
50
54
|
timestamp: string;
|
|
51
55
|
description: string;
|
|
52
56
|
operationType: MutationOp;
|
|
57
|
+
actor: MutationActor;
|
|
58
|
+
/** Writer key (agentId, else transport source) for agent entries — lets a
|
|
59
|
+
* client prove "the top edit is MINE" before undoing on a multi-agent board. */
|
|
60
|
+
writer?: string;
|
|
53
61
|
forward: () => void;
|
|
54
62
|
inverse: () => void;
|
|
55
63
|
}
|
|
@@ -58,6 +66,8 @@ export interface MutationSummary {
|
|
|
58
66
|
id: string;
|
|
59
67
|
timestamp: string;
|
|
60
68
|
description: string;
|
|
69
|
+
actor: MutationActor;
|
|
70
|
+
writer?: string;
|
|
61
71
|
operationType: MutationOp;
|
|
62
72
|
isCurrent: boolean;
|
|
63
73
|
isUndone: boolean;
|
|
@@ -103,11 +113,13 @@ class MutationHistory {
|
|
|
103
113
|
* Record a new mutation. Truncates any redo-able future, then appends.
|
|
104
114
|
* If called while replaying (undo/redo), the call is silently ignored.
|
|
105
115
|
*/
|
|
106
|
-
record(entry: Omit<MutationEntry, 'id' | 'timestamp'>): void {
|
|
116
|
+
record(entry: Omit<MutationEntry, 'id' | 'timestamp' | 'actor'> & { actor?: MutationActor }): void {
|
|
107
117
|
if (this._replaying) return;
|
|
108
118
|
|
|
109
119
|
const full: MutationEntry = {
|
|
110
120
|
...entry,
|
|
121
|
+
actor: entry.actor ?? currentActor ?? 'agent',
|
|
122
|
+
...((entry.writer ?? currentWriter) ? { writer: entry.writer ?? currentWriter ?? undefined } : {}),
|
|
111
123
|
id: `mut-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`,
|
|
112
124
|
timestamp: new Date().toISOString(),
|
|
113
125
|
};
|
|
@@ -169,12 +181,31 @@ class MutationHistory {
|
|
|
169
181
|
id: e.id,
|
|
170
182
|
timestamp: e.timestamp,
|
|
171
183
|
description: e.description,
|
|
184
|
+
actor: e.actor,
|
|
185
|
+
...(e.writer ? { writer: e.writer } : {}),
|
|
172
186
|
operationType: e.operationType,
|
|
173
187
|
isCurrent: i === this.cursor,
|
|
174
188
|
isUndone: i > this.cursor,
|
|
175
189
|
}));
|
|
176
190
|
}
|
|
177
191
|
|
|
192
|
+
/** The entry Ctrl+Z would undo next (top of the shared stack), or null. */
|
|
193
|
+
top(): MutationSummary | null {
|
|
194
|
+
const entry = this.entries[this.cursor];
|
|
195
|
+
return entry
|
|
196
|
+
? {
|
|
197
|
+
id: entry.id,
|
|
198
|
+
timestamp: entry.timestamp,
|
|
199
|
+
description: entry.description,
|
|
200
|
+
actor: entry.actor,
|
|
201
|
+
...(entry.writer ? { writer: entry.writer } : {}),
|
|
202
|
+
operationType: entry.operationType,
|
|
203
|
+
isCurrent: true,
|
|
204
|
+
isUndone: false,
|
|
205
|
+
}
|
|
206
|
+
: null;
|
|
207
|
+
}
|
|
208
|
+
|
|
178
209
|
/** Human-readable timeline for the canvas://history resource. */
|
|
179
210
|
toHumanReadable(): string {
|
|
180
211
|
if (this.entries.length === 0) {
|
|
@@ -383,3 +414,17 @@ export function formatDiff(diff: SnapshotDiffResult): string {
|
|
|
383
414
|
// ── Singleton ────────────────────────────────────────────────────────
|
|
384
415
|
|
|
385
416
|
export const mutationHistory = new MutationHistory();
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Actor context for entries recorded while an operation runs. executeOperation
|
|
420
|
+
* sets it from the workbench marker around each op; writes outside the
|
|
421
|
+
* registry (the sync SDK) default to `agent`. Only the undo affordance reads
|
|
422
|
+
* it, so an async op interleaving with another request costs at most a
|
|
423
|
+
* mislabelled row — never a wrong undo.
|
|
424
|
+
*/
|
|
425
|
+
let currentActor: MutationActor | null = null;
|
|
426
|
+
let currentWriter: string | null = null;
|
|
427
|
+
export function setMutationActor(actor: MutationActor | null, writer: string | null = null): void {
|
|
428
|
+
currentActor = actor;
|
|
429
|
+
currentWriter = actor === 'agent' ? writer : null;
|
|
430
|
+
}
|
|
@@ -170,10 +170,11 @@ export const compositeToolDefinitions: CompositeToolDefinition[] = [
|
|
|
170
170
|
{
|
|
171
171
|
toolName: 'canvas_edge',
|
|
172
172
|
description:
|
|
173
|
-
'Add or remove an edge (connection) between two nodes. Action "add" connects two nodes (type: flow=sequential, depends-on=dependency, relation=general, references=cross-reference; from/to take node ids, or fromSearch/toSearch resolve by title/content); "remove" deletes an edge by id.',
|
|
173
|
+
'Add, update, or remove an edge (connection) between two nodes. Action "add" connects two nodes (type: flow=sequential, depends-on=dependency, relation=general, references=cross-reference; from/to take node ids, or fromSearch/toSearch resolve by title/content); "update" patches an existing edge in place (label, type, style, animated); "remove" deletes an edge by id.',
|
|
174
174
|
actionSummary: 'add | remove',
|
|
175
175
|
actions: {
|
|
176
176
|
add: 'edge.add',
|
|
177
|
+
update: 'edge.update',
|
|
177
178
|
remove: 'edge.remove',
|
|
178
179
|
},
|
|
179
180
|
},
|
|
@@ -250,13 +251,15 @@ export const compositeToolDefinitions: CompositeToolDefinition[] = [
|
|
|
250
251
|
{
|
|
251
252
|
toolName: 'canvas_ax_state',
|
|
252
253
|
description:
|
|
253
|
-
'Read or set host-agnostic PMX AX state. Action "get" reads the AX state + agent-ready context (pinned/focused context, focus field); "set-focus" places node IDs in the AX focus field (nodeIds); "set-policy" patches the tool/prompt policy (tools.allowed|excluded|approvalRequired, prompt.systemAppend|mode — merges with existing); "report-capability" records a host/session capability for diagnostics. Adapters may pass a source label (e.g. codex).',
|
|
254
|
-
actionSummary: 'get | set-focus | set-policy | report-capability',
|
|
254
|
+
'Read or set host-agnostic PMX AX state. Action "get" reads the AX state + agent-ready context (pinned/focused context, focus field); "set-focus" places node IDs in the AX focus field (nodeIds); "set-policy" patches the tool/prompt policy (tools.allowed|excluded|approvalRequired, prompt.systemAppend|mode — merges with existing); "report-capability" records a host/session capability for diagnostics; "presence" reads agent presence (who is writing, phase, attached session, context budget); "set-presence" updates this caller\'s presence (phase idle|thinking|tooling|waiting-approval, detail, focusNodeId, cursor, attached — attached:true starts a session, false ends it; contextUsage {used,total} reports the agent\'s real token window so the board\'s meter shows it instead of the pinned-context estimate). Adapters may pass a source label (e.g. codex).',
|
|
255
|
+
actionSummary: 'get | set-focus | set-policy | report-capability | presence | set-presence',
|
|
255
256
|
actions: {
|
|
256
257
|
get: 'ax.get',
|
|
257
258
|
'set-focus': 'ax.focus.set',
|
|
258
259
|
'set-policy': 'ax.policy.set',
|
|
259
260
|
'report-capability': 'ax.host-capability.report',
|
|
261
|
+
presence: 'ax.presence.get',
|
|
262
|
+
'set-presence': 'ax.presence.set',
|
|
260
263
|
},
|
|
261
264
|
},
|
|
262
265
|
{
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* the remaining legacy routes.
|
|
8
8
|
*/
|
|
9
9
|
import { executeOperation, listOperations } from './registry.js';
|
|
10
|
+
import { SOURCE_LABEL_RE } from '../agent-presence.js';
|
|
10
11
|
import { OperationError } from './types.js';
|
|
11
12
|
|
|
12
13
|
function responseJson(data: unknown, status = 200): Response {
|
|
@@ -88,7 +89,15 @@ export async function dispatchOperationRoute(req: Request, url: URL): Promise<Re
|
|
|
88
89
|
// the browser is not agent activity, so it never synthesizes an
|
|
89
90
|
// auto-ghost. Everything else (CLI, scripts, adapters) counts as agent.
|
|
90
91
|
const fromWorkbench = req.headers.get('x-pmx-workbench') === '1';
|
|
91
|
-
|
|
92
|
+
// Presence writer label: adapters/CLI may identify themselves; plain
|
|
93
|
+
// HTTP callers read as 'api'.
|
|
94
|
+
const sourceHeader = req.headers.get('x-pmx-source');
|
|
95
|
+
const source = sourceHeader && SOURCE_LABEL_RE.test(sourceHeader) ? sourceHeader : 'api';
|
|
96
|
+
const result = await executeOperation(op.name, input, {
|
|
97
|
+
suppressAutoGhost: fromWorkbench,
|
|
98
|
+
fromWorkbench,
|
|
99
|
+
source,
|
|
100
|
+
});
|
|
92
101
|
return responseJson(result, route.status ? route.status(result) : 200);
|
|
93
102
|
} catch (error) {
|
|
94
103
|
if (error instanceof OperationError) {
|
|
@@ -22,6 +22,8 @@ import { appOperations } from './ops/app.js';
|
|
|
22
22
|
import { intentOperations } from './ops/intent.js';
|
|
23
23
|
import { extAppOperations } from './ops/ext-app.js';
|
|
24
24
|
import { axReadOperations } from './ops/ax-read.js';
|
|
25
|
+
import { axPresenceOperations } from './ops/ax-presence.js';
|
|
26
|
+
import { humanPresenceOperations } from './ops/human-presence.js';
|
|
25
27
|
import { canvasWireOperations } from './ops/canvas-wire.js';
|
|
26
28
|
|
|
27
29
|
for (const op of [
|
|
@@ -44,6 +46,8 @@ for (const op of [
|
|
|
44
46
|
...intentOperations,
|
|
45
47
|
...extAppOperations,
|
|
46
48
|
...axReadOperations,
|
|
49
|
+
...axPresenceOperations,
|
|
50
|
+
...humanPresenceOperations,
|
|
47
51
|
...canvasWireOperations,
|
|
48
52
|
]) {
|
|
49
53
|
registerOperation(op);
|