pmx-canvas 0.7.3 → 0.8.2
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/THIRD_PARTY_LICENSES.txt +331 -0
- package/.github/extensions/pmx-canvas/extension.mjs +25 -172
- package/.github/extensions/pmx-canvas/mcp-bootstrap.ts +198 -0
- package/.github/extensions/pmx-canvas/runtime.d.mts +41 -0
- package/.github/extensions/pmx-canvas/runtime.json +3 -0
- package/.github/extensions/pmx-canvas/runtime.mjs +194 -0
- package/.github/extensions/pmx-canvas/start-mcp.mjs +51 -0
- package/.github/extensions/pmx-canvas/tools.json +2560 -0
- package/CHANGELOG.md +64 -0
- package/Readme.md +21 -7
- package/dist/canvas/fonts/OFL.txt +93 -0
- package/dist/canvas/fonts/ibm-plex-mono-latin-400-normal.woff2 +0 -0
- package/dist/canvas/fonts/ibm-plex-mono-latin-500-normal.woff2 +0 -0
- package/dist/canvas/fonts/ibm-plex-mono-latin-ext-400-normal.woff2 +0 -0
- package/dist/canvas/fonts/ibm-plex-mono-latin-ext-500-normal.woff2 +0 -0
- package/dist/canvas/fonts/ibm-plex-sans-latin-400-normal.woff2 +0 -0
- package/dist/canvas/fonts/ibm-plex-sans-latin-500-normal.woff2 +0 -0
- package/dist/canvas/fonts/ibm-plex-sans-latin-600-normal.woff2 +0 -0
- package/dist/canvas/fonts/ibm-plex-sans-latin-ext-400-normal.woff2 +0 -0
- package/dist/canvas/fonts/ibm-plex-sans-latin-ext-500-normal.woff2 +0 -0
- package/dist/canvas/fonts/ibm-plex-sans-latin-ext-600-normal.woff2 +0 -0
- package/dist/canvas/global.css +726 -674
- package/dist/canvas/index.js +90 -88
- package/dist/canvas/mcp-app.html +7879 -0
- package/dist/canvas/mermaid-entry.js +1 -1
- package/dist/canvas/surface-theme.css +151 -46
- package/dist/json-render/index.css +81 -1
- package/dist/json-render/index.js +1 -1
- package/dist/types/cli/commands/record.d.ts +1 -1
- package/dist/types/client/canvas/NodeContextMark.d.ts +24 -0
- package/dist/types/client/canvas/SessionReceipt.d.ts +3 -1
- package/dist/types/client/icons.d.ts +1 -2
- package/dist/types/client/mcp-app/index.d.ts +1 -0
- package/dist/types/client/nodes/HtmlNode.d.ts +1 -0
- package/dist/types/client/nodes/ax-node-actions.d.ts +2 -2
- package/dist/types/client/nodes/markdown-sanitize.d.ts +2 -0
- package/dist/types/client/nodes/use-workbench-asset.d.ts +2 -0
- package/dist/types/client/state/attention-store.d.ts +0 -5
- package/dist/types/client/state/context-status-store.d.ts +53 -0
- package/dist/types/client/state/intent-bridge.d.ts +3 -0
- package/dist/types/client/state/near-pin-store.d.ts +16 -0
- package/dist/types/client/state/presence-store.d.ts +5 -0
- package/dist/types/client/state/session-store.d.ts +33 -3
- package/dist/types/client/state/workbench-transport.d.ts +16 -0
- package/dist/types/client/theme/tokens.d.ts +1 -3
- package/dist/types/client/types.d.ts +9 -0
- package/dist/types/json-render/server.d.ts +1 -1
- package/dist/types/mcp/app-server.d.ts +3 -0
- package/dist/types/mcp/canvas-access.d.ts +1 -1
- package/dist/types/mcp/canvas-app.d.ts +4 -0
- package/dist/types/mcp/workbench-app.d.ts +4 -0
- package/dist/types/server/agent-presence.d.ts +1 -1
- package/dist/types/server/ax-state.d.ts +1 -1
- package/dist/types/server/canvas-db.d.ts +8 -0
- package/dist/types/server/canvas-geometry.d.ts +25 -0
- package/dist/types/server/canvas-operations.d.ts +1 -11
- package/dist/types/server/canvas-state.d.ts +25 -3
- package/dist/types/server/context-brief.d.ts +8 -1
- package/dist/types/server/context-reads.d.ts +30 -7
- package/dist/types/server/index.d.ts +6 -1
- package/dist/types/server/mcp-app-runtime.d.ts +1 -0
- package/dist/types/server/operations/http.d.ts +2 -0
- package/dist/types/server/operations/invoker.d.ts +4 -2
- package/dist/types/server/operations/ops/nodes.d.ts +0 -6
- package/dist/types/server/server.d.ts +1 -0
- package/dist/types/server/spatial-analysis.d.ts +2 -0
- package/dist/types/shared/agent-context.d.ts +22 -0
- package/dist/types/shared/agent-presence.d.ts +3 -10
- package/dist/types/shared/canvas-app.d.ts +54 -0
- package/dist/types/shared/themes.d.ts +1 -1
- package/docs/RELEASE.md +7 -3
- package/docs/agent-plugin.md +123 -0
- package/docs/ax-state-contract.md +40 -0
- package/docs/cli.md +7 -2
- package/docs/design/AgentContext.dc.html +186 -0
- package/docs/design/Before.dc.html +208 -0
- package/docs/design/BoardPins.dc.html +151 -0
- package/docs/design/Chrome.dc.html +211 -0
- package/docs/design/Context.dc.html +239 -0
- package/docs/design/Flows.dc.html +181 -0
- package/docs/design/Home.dc.html +209 -0
- package/docs/design/HomeGraph.dc.html +211 -0
- package/docs/design/HomeMap.dc.html +117 -0
- package/docs/design/HomeTeam.dc.html +176 -0
- package/docs/design/Light.dc.html +28 -0
- package/docs/design/Main.dc.html +280 -0
- package/docs/design/NearPin.dc.html +260 -0
- package/docs/design/Pane600.dc.html +120 -0
- package/docs/design/Relations.dc.html +297 -0
- package/docs/design/Themes.dc.html +121 -0
- package/docs/design/TokenSystem.dc.html +136 -0
- package/docs/design/Tokens.dc.html +163 -0
- package/docs/design/Types1.dc.html +177 -0
- package/docs/design/Types2.dc.html +226 -0
- package/docs/design/Types3.dc.html +195 -0
- package/docs/design/Uses.dc.html +152 -0
- package/docs/design/Vision.dc.html +116 -0
- package/docs/design/Zoom.dc.html +82 -0
- package/docs/design/canvas.json +274 -0
- package/docs/design.md +217 -0
- package/docs/environment.md +1 -1
- package/docs/http-api.md +86 -16
- package/docs/mcp.md +202 -4
- package/docs/node-types.md +34 -10
- package/docs/plans/plan-009-tech-debt-backlog.md +1 -1
- package/docs/plans/plan-015-design-foundation-wave-0.md +177 -0
- package/docs/plans/plan-016-context-made-visible-wave-1.md +148 -0
- package/docs/product-vision-2026-09.md +47 -7
- package/docs/screenshot.png +0 -0
- package/docs/sdk.md +1 -0
- package/package.json +10 -4
- package/plugins/pmx-canvas/LICENSE +21 -0
- package/plugins/pmx-canvas/THIRD_PARTY_LICENSES.txt +331 -0
- package/plugins/pmx-canvas/assets/preview.png +0 -0
- package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/extension.mjs +930 -0
- package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/runtime.json +3 -0
- package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/runtime.mjs +194 -0
- package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/steering-delivery.mjs +79 -0
- package/plugins/pmx-canvas/mcp.json +12 -0
- package/plugins/pmx-canvas/plugin.json +24 -0
- package/plugins/pmx-canvas/runtime.json +3 -0
- package/plugins/pmx-canvas/runtime.mjs +194 -0
- package/plugins/pmx-canvas/start-mcp.mjs +51 -0
- package/plugins/pmx-canvas/tools.json +2560 -0
- package/skills/pmx-canvas/SKILL.md +29 -9
- package/skills/pmx-canvas/evals/evals.json +62 -0
- package/skills/pmx-canvas/references/ax-html-control-surface.md +6 -0
- package/skills/pmx-canvas/references/full-reference.md +11 -7
- package/skills/pmx-canvas/references/github-copilot-app-adapter.md +20 -8
- package/skills/pmx-canvas/references/installing-pmx-canvas.md +6 -6
- package/skills/pmx-canvas-testing/SKILL.md +5 -4
- package/src/cli/commands/copilot.ts +5 -0
- package/src/cli/commands/nodes.ts +3 -2
- package/src/cli/commands/view.ts +1 -1
- package/src/cli/index.ts +10 -4
- package/src/client/App.tsx +6 -1
- package/src/client/canvas/AgentPresenceLayer.tsx +3 -3
- package/src/client/canvas/AnnotationLayer.tsx +1 -1
- package/src/client/canvas/CanvasNode.tsx +32 -49
- package/src/client/canvas/CanvasViewport.tsx +0 -2
- package/src/client/canvas/CommandPalette.tsx +3 -2
- package/src/client/canvas/ContextMenu.tsx +11 -0
- package/src/client/canvas/EdgeLayer.tsx +1 -1
- package/src/client/canvas/ExportDialog.tsx +33 -6
- package/src/client/canvas/IntentLayer.tsx +2 -2
- package/src/client/canvas/Minimap.tsx +12 -7
- package/src/client/canvas/NodeContextMark.tsx +156 -0
- package/src/client/canvas/SessionPanel.tsx +4 -7
- package/src/client/canvas/SessionReceipt.tsx +114 -17
- package/src/client/canvas/ShortcutOverlay.tsx +6 -1
- package/src/client/canvas/SnapshotPanel.tsx +11 -2
- package/src/client/canvas/ToolRail.tsx +12 -9
- package/src/client/canvas/TopBar.tsx +16 -11
- package/src/client/icons.tsx +2 -12
- package/src/client/mcp-app/index.tsx +137 -0
- package/src/client/mcp-app/style.css +10 -0
- package/src/client/nodes/AttachmentNode.tsx +8 -1
- package/src/client/nodes/AxStepControls.tsx +1 -1
- package/src/client/nodes/ContextNode.tsx +9 -9
- package/src/client/nodes/ExtAppFrame.tsx +13 -8
- package/src/client/nodes/FileNode.tsx +8 -5
- package/src/client/nodes/HtmlNode.tsx +12 -2
- package/src/client/nodes/ImageNode.tsx +4 -2
- package/src/client/nodes/InlineMarkdownEditor.tsx +2 -1
- package/src/client/nodes/LedgerNode.tsx +2 -2
- package/src/client/nodes/MarkdownNode.tsx +44 -9
- package/src/client/nodes/McpAppNode.tsx +6 -4
- package/src/client/nodes/PromptNode.tsx +5 -17
- package/src/client/nodes/ResponseNode.tsx +3 -15
- package/src/client/nodes/StatusNode.tsx +2 -2
- package/src/client/nodes/StatusSummary.tsx +3 -1
- package/src/client/nodes/TraceNode.tsx +6 -4
- package/src/client/nodes/WebpageNode.tsx +1 -1
- package/src/client/nodes/ax-node-actions.ts +2 -2
- package/src/client/nodes/iframe-document-url.ts +3 -2
- package/src/client/nodes/markdown-sanitize.ts +10 -0
- package/src/client/nodes/surface-url.ts +10 -0
- package/src/client/nodes/use-surface-frame.ts +3 -1
- package/src/client/nodes/use-workbench-asset.ts +26 -0
- package/src/client/state/attention-bridge.ts +0 -8
- package/src/client/state/attention-store.ts +0 -17
- package/src/client/state/canvas-store.ts +4 -2
- package/src/client/state/context-status-store.ts +66 -0
- package/src/client/state/human-store.ts +2 -0
- package/src/client/state/iframe-mode.ts +5 -0
- package/src/client/state/intent-bridge.ts +10 -4
- package/src/client/state/near-pin-store.ts +53 -0
- package/src/client/state/presence-store.ts +5 -10
- package/src/client/state/session-store.ts +106 -9
- package/src/client/state/sse-bridge.ts +26 -7
- package/src/client/state/theme-override.ts +1 -1
- package/src/client/state/workbench-transport.ts +36 -0
- package/src/client/theme/global.css +726 -674
- package/src/client/theme/surface-theme.css +151 -46
- package/src/client/theme/tokens.ts +7 -11
- package/src/client/types.ts +10 -0
- package/src/json-render/renderer/index.css +6 -1
- package/src/json-render/renderer/index.tsx +0 -1
- package/src/json-render/server.ts +71 -14
- package/src/mcp/app-server.ts +64 -0
- package/src/mcp/canvas-access.ts +14 -11
- package/src/mcp/canvas-app.ts +318 -0
- package/src/mcp/server.ts +1 -1
- package/src/mcp/workbench-app.ts +211 -0
- package/src/server/agent-presence.ts +45 -6
- package/src/server/ax-context.ts +1 -1
- package/src/server/ax-state.ts +1 -1
- package/src/server/board-export.ts +5 -2
- package/src/server/canvas-db.ts +46 -12
- package/src/server/canvas-geometry.ts +64 -0
- package/src/server/canvas-operations.ts +7 -25
- package/src/server/canvas-state.ts +105 -15
- package/src/server/canvas-validation.ts +19 -4
- package/src/server/context-brief.ts +39 -2
- package/src/server/context-reads.ts +87 -15
- package/src/server/demo-state.json +31 -31
- package/src/server/html-surface.ts +4 -5
- package/src/server/index.ts +41 -5
- package/src/server/mcp-app-runtime.ts +11 -0
- package/src/server/operations/composites.ts +4 -3
- package/src/server/operations/http.ts +4 -4
- package/src/server/operations/invoker.ts +13 -4
- package/src/server/operations/mcp.ts +6 -0
- package/src/server/operations/ops/ax-read.ts +1 -1
- package/src/server/operations/ops/ax-timeline.ts +37 -0
- package/src/server/operations/ops/boards.ts +1 -1
- package/src/server/operations/ops/canvas-wire.ts +3 -6
- package/src/server/operations/ops/intent.ts +31 -2
- package/src/server/operations/ops/json-render.ts +6 -2
- package/src/server/operations/ops/nodes.ts +8 -76
- package/src/server/operations/ops/query.ts +8 -2
- package/src/server/operations/ops/snapshots.ts +4 -1
- package/src/server/operations/ops/viewport.ts +29 -5
- package/src/server/operations/registry.ts +41 -5
- package/src/server/scope-fence.ts +55 -10
- package/src/server/server.ts +83 -5
- package/src/server/spatial-analysis.ts +2 -2
- package/src/server/workboard.ts +6 -1
- package/src/{server → shared}/agent-context.ts +2 -2
- package/src/shared/agent-presence.ts +3 -15
- package/src/shared/canvas-app.ts +62 -0
- package/src/shared/themes.ts +5 -5
- package/src/client/canvas/FocusFieldLayer.tsx +0 -116
- package/src/client/canvas/kind-colors.ts +0 -23
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
1
2
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
2
3
|
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
3
4
|
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
@@ -18,6 +19,13 @@ import type { McpUiClientCapabilities, McpUiResourceCsp, McpUiResourceMeta } fro
|
|
|
18
19
|
import { getToolUiResourceUri } from '@modelcontextprotocol/ext-apps/app-bridge';
|
|
19
20
|
import { normalizeExtAppToolResult } from './ext-app-tool-result.js';
|
|
20
21
|
|
|
22
|
+
// A restriction follows async rehydration spawned by an embedded operation,
|
|
23
|
+
// without disabling independent requests from the local workbench.
|
|
24
|
+
const externalMcpDisabled = new AsyncLocalStorage<boolean>();
|
|
25
|
+
export function withoutExternalMcp<T>(run: () => T): T {
|
|
26
|
+
return externalMcpDisabled.run(true, run);
|
|
27
|
+
}
|
|
28
|
+
|
|
21
29
|
export interface ExternalMcpHttpTransportConfig {
|
|
22
30
|
type: 'http';
|
|
23
31
|
url: string;
|
|
@@ -210,6 +218,9 @@ async function createSession(
|
|
|
210
218
|
serverName?: string,
|
|
211
219
|
timeoutMs?: number,
|
|
212
220
|
): Promise<McpAppSession> {
|
|
221
|
+
if (externalMcpDisabled.getStore()) {
|
|
222
|
+
throw new Error('Open external MCP apps from the local PMX workbench; embedded actions cannot launch them.');
|
|
223
|
+
}
|
|
213
224
|
const transport = buildTransport(transportConfig);
|
|
214
225
|
const client = new Client({ name: 'pmx-canvas-app-host', version: '0.1.0' }, { capabilities: clientCapabilities });
|
|
215
226
|
await client.connect(transport, requestOptions(timeoutMs));
|
|
@@ -323,14 +323,15 @@ export const compositeToolDefinitions: CompositeToolDefinition[] = [
|
|
|
323
323
|
{
|
|
324
324
|
toolName: 'canvas_ax_timeline',
|
|
325
325
|
description:
|
|
326
|
-
'Read or write the bounded PMX AX timeline. Action "read" returns recent agent-events, evidence, and steering messages plus counts (limit); "record-event" records a normalized agent-event (kind prompt|assistant-message|tool-start|tool-result|failure|approval|steering, summary, detail, data); "add-evidence" records an evidence item (kind logs|tool-result|screenshot|file|diff|test-output, title, body, ref); "send-steering" records a steering message (message); "reads" returns the context read log — which canvas context each agent read and which pinned nodes reached it — with a per-consumer summary (limit). Timeline rows persist for diagnostics/continuity but are not restored by snapshots; exposed via canvas://ax-timeline.',
|
|
327
|
-
actionSummary: 'read | record-event | add-evidence | send-steering | reads',
|
|
326
|
+
'Read or write the bounded PMX AX timeline. Action "read" returns recent agent-events, evidence, and steering messages plus counts (limit); "record-event" records a normalized agent-event (kind prompt|assistant-message|tool-start|tool-result|failure|approval|steering, summary, detail, data); "add-evidence" records an evidence item (kind logs|tool-result|screenshot|file|diff|test-output, title, body, ref); "send-steering" records a steering message (message); "reads" returns the context read log — which canvas context each agent read and which pinned nodes reached it — with a per-consumer summary (limit); "read-status" returns, per node on a board, when an agent last read its content, who, and the content revision it read (board). Timeline rows persist for diagnostics/continuity but are not restored by snapshots; exposed via canvas://ax-timeline.',
|
|
327
|
+
actionSummary: 'read | record-event | add-evidence | send-steering | reads | read-status',
|
|
328
328
|
actions: {
|
|
329
329
|
read: 'ax.timeline.get',
|
|
330
330
|
'record-event': 'ax.event.record',
|
|
331
331
|
'add-evidence': 'ax.evidence.add',
|
|
332
332
|
'send-steering': 'ax.steer',
|
|
333
333
|
reads: 'ax.reads.list',
|
|
334
|
+
'read-status': 'ax.reads.status',
|
|
334
335
|
},
|
|
335
336
|
},
|
|
336
337
|
{
|
|
@@ -346,7 +347,7 @@ export const compositeToolDefinitions: CompositeToolDefinition[] = [
|
|
|
346
347
|
{
|
|
347
348
|
toolName: 'canvas_intent',
|
|
348
349
|
description:
|
|
349
|
-
'Ghost Cursor of Intent — announce the spatial move you are ABOUT to make so the canvas paints a faint pre-commit placeholder (legibility: the human sees the next move forming, and can veto it). Action "signal" registers an intent (kind create|move|connect|remove|edit; pass position for create
|
|
350
|
+
'Ghost Cursor of Intent — announce the spatial move you are ABOUT to make so the canvas paints a faint pre-commit placeholder (legibility: the human sees the next move forming, and can veto it). Action "signal" registers an intent (kind create|move|connect|remove|edit; pass position for move — omit it for create to ghost where the canvas will place the node, and the linked create lands there — nodeId for move/edit/remove, edge for connect; optional label, reason, confidence 0..1, seq, ttlMs, and a stable id); "update" patches a live intent by id; "clear" abandons/dissolves it. To make veto authoritative, pass the returned id as intentId on the real canvas_node/canvas_edge/canvas_group/canvas_render mutation: vetoed or expired intents are rejected, and a successful linked mutation settles the ghost automatically. Intents are ephemeral presence: never persisted, never snapshotted, auto-expire (~8s).',
|
|
350
351
|
actionSummary: 'signal | update | clear',
|
|
351
352
|
actions: {
|
|
352
353
|
signal: 'intent.signal',
|
|
@@ -20,7 +20,7 @@ function responseJson(data: unknown, status = 200): Response {
|
|
|
20
20
|
});
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
function
|
|
23
|
+
export function matchOperationPath(template: string, pathname: string): Record<string, string> | null {
|
|
24
24
|
const templateSegments = template.split('/');
|
|
25
25
|
const pathSegments = pathname.split('/');
|
|
26
26
|
if (templateSegments.length !== pathSegments.length) return null;
|
|
@@ -58,7 +58,7 @@ export async function readJsonValue(req: Request): Promise<unknown> {
|
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
async function
|
|
61
|
+
export async function readOperationInput(
|
|
62
62
|
req: Request,
|
|
63
63
|
params: Record<string, string>,
|
|
64
64
|
url: URL,
|
|
@@ -80,12 +80,12 @@ export async function dispatchOperationRoute(req: Request, url: URL): Promise<Re
|
|
|
80
80
|
for (const op of listOperations()) {
|
|
81
81
|
const route = op.http;
|
|
82
82
|
if (!route || route.method !== req.method) continue;
|
|
83
|
-
const params =
|
|
83
|
+
const params = matchOperationPath(route.path, url.pathname);
|
|
84
84
|
if (!params) continue;
|
|
85
85
|
try {
|
|
86
86
|
const input = route.readInput
|
|
87
87
|
? await route.readInput(req, params, url)
|
|
88
|
-
: await
|
|
88
|
+
: await readOperationInput(req, params, url);
|
|
89
89
|
// The workbench marks its own HTTP calls: a human dragging/editing in
|
|
90
90
|
// the browser is not agent activity, so it never synthesizes an
|
|
91
91
|
// auto-ghost. Everything else (CLI, scripts, adapters) counts as agent.
|
|
@@ -32,13 +32,17 @@ export class LocalOperationInvoker implements OperationInvoker {
|
|
|
32
32
|
private readonly proxiedRead: boolean;
|
|
33
33
|
|
|
34
34
|
/** `source` labels this caller's agent presence ('mcp', 'sdk', …). */
|
|
35
|
-
constructor(
|
|
35
|
+
constructor(
|
|
36
|
+
source = 'api',
|
|
37
|
+
private readonly allowExternalMcp = true,
|
|
38
|
+
) {
|
|
36
39
|
this.source = agentSourceLabel(source);
|
|
37
40
|
this.proxiedRead = source === 'mcp';
|
|
38
41
|
}
|
|
39
42
|
|
|
40
43
|
async invoke(name: string, input: Record<string, unknown>): Promise<unknown> {
|
|
41
|
-
|
|
44
|
+
const args = this.allowExternalMcp ? input : { ...input, allowExternalMcp: false };
|
|
45
|
+
return await executeOperation(name, defaultPresenceSource(name, args, this.source), {
|
|
42
46
|
source: this.source,
|
|
43
47
|
proxiedRead: this.proxiedRead,
|
|
44
48
|
});
|
|
@@ -56,14 +60,19 @@ export class HttpOperationInvoker implements OperationInvoker {
|
|
|
56
60
|
private readonly proxiedRead: boolean;
|
|
57
61
|
|
|
58
62
|
/** `source` labels this caller's agent presence on the server ('cli', 'mcp', …). */
|
|
59
|
-
constructor(
|
|
63
|
+
constructor(
|
|
64
|
+
baseUrl: string,
|
|
65
|
+
source = 'api',
|
|
66
|
+
private readonly allowExternalMcp = true,
|
|
67
|
+
) {
|
|
60
68
|
this.baseUrl = baseUrl.replace(/\/$/, '');
|
|
61
69
|
this.source = agentSourceLabel(source);
|
|
62
70
|
this.proxiedRead = source === 'mcp';
|
|
63
71
|
}
|
|
64
72
|
|
|
65
73
|
async invoke(name: string, rawInput: Record<string, unknown>): Promise<unknown> {
|
|
66
|
-
const
|
|
74
|
+
const args = this.allowExternalMcp ? rawInput : { ...rawInput, allowExternalMcp: false };
|
|
75
|
+
const input = defaultPresenceSource(name, args, this.source);
|
|
67
76
|
const op = getOperation(name);
|
|
68
77
|
const route = op.http;
|
|
69
78
|
if (!route) throw new OperationError(`Operation "${name}" has no HTTP route.`, 400);
|
|
@@ -43,6 +43,12 @@ async function invokeMcpOperation(
|
|
|
43
43
|
.filter((item): item is Extract<(typeof formatted.content)[number], { type: 'text' }> => item.type === 'text')
|
|
44
44
|
.map((item) => item.text)
|
|
45
45
|
.join('');
|
|
46
|
+
if (op.name === 'node.get') {
|
|
47
|
+
// Compact responses can still deliver text. Metadata without text or
|
|
48
|
+
// data must not mark unseen content read.
|
|
49
|
+
const node = JSON.parse(text) as { data?: unknown; content?: string | null };
|
|
50
|
+
if (node.data === undefined && !node.content) return formatted;
|
|
51
|
+
}
|
|
46
52
|
try {
|
|
47
53
|
await host.recordContextRead(
|
|
48
54
|
contextReadFromPayload(
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { agentPresence } from '../../agent-presence.js';
|
|
15
15
|
import { z } from 'zod';
|
|
16
|
-
import { buildAgentContextPreamble, serializeNodeForAgentContext } from '
|
|
16
|
+
import { buildAgentContextPreamble, serializeNodeForAgentContext } from '../../../shared/agent-context.js';
|
|
17
17
|
import { buildCanvasAxContext, buildCanvasAxSurfaceSnapshot } from '../../ax-context.js';
|
|
18
18
|
import { applyAxInteraction } from '../../ax-interaction.js';
|
|
19
19
|
import {
|
|
@@ -502,6 +502,10 @@ const axReadsRecordShape = {
|
|
|
502
502
|
source: z.string().optional().describe('Transport label (default "api").'),
|
|
503
503
|
pinnedNodeIds: z.array(z.string()).optional().describe('Pins at read time; defaults to the current pins.'),
|
|
504
504
|
deliveredNodeIds: z.array(z.string()).describe('Pinned ids present in what the agent received.'),
|
|
505
|
+
readNodeIds: z
|
|
506
|
+
.array(z.string())
|
|
507
|
+
.optional()
|
|
508
|
+
.describe('Ids of every serialized node in what the agent received; defaults to deliveredNodeIds.'),
|
|
505
509
|
bytes: z.number().int().nonnegative().describe('Size of what the agent received.'),
|
|
506
510
|
boardId: z.string().nullable().optional().describe('Board that was read, captured before delivery.'),
|
|
507
511
|
};
|
|
@@ -527,6 +531,7 @@ const axReadsRecordOperation = defineOperation<z.infer<typeof axReadsRecordSchem
|
|
|
527
531
|
agentId: input.agentId ?? null,
|
|
528
532
|
pinnedNodeIds: input.pinnedNodeIds ?? [...canvasState.contextPinnedNodeIds],
|
|
529
533
|
deliveredNodeIds: input.deliveredNodeIds,
|
|
534
|
+
...(input.readNodeIds ? { readNodeIds: input.readNodeIds } : {}),
|
|
530
535
|
bytes: input.bytes,
|
|
531
536
|
},
|
|
532
537
|
input.boardId === undefined ? canvasState.activeBoardId : input.boardId,
|
|
@@ -535,6 +540,37 @@ const axReadsRecordOperation = defineOperation<z.infer<typeof axReadsRecordSchem
|
|
|
535
540
|
},
|
|
536
541
|
});
|
|
537
542
|
|
|
543
|
+
const axReadsStatusShape = {
|
|
544
|
+
board: z.string().optional().describe('Board id; defaults to the open board.'),
|
|
545
|
+
};
|
|
546
|
+
const axReadsStatusSchema = z.looseObject(axReadsStatusShape);
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Per node: when an agent last read its content, who, and at which revision
|
|
550
|
+
* (vs the node's revision now); and per pin: who pinned it, when and why.
|
|
551
|
+
*/
|
|
552
|
+
const axReadsStatusOperation = defineOperation<z.infer<typeof axReadsStatusSchema>, Record<string, unknown>>({
|
|
553
|
+
name: 'ax.reads.status',
|
|
554
|
+
mutates: false,
|
|
555
|
+
input: axReadsStatusSchema,
|
|
556
|
+
inputShape: axReadsStatusShape,
|
|
557
|
+
http: {
|
|
558
|
+
method: 'GET',
|
|
559
|
+
path: '/api/canvas/ax/context-status',
|
|
560
|
+
},
|
|
561
|
+
handler: (input) => {
|
|
562
|
+
const boardId =
|
|
563
|
+
typeof input.board === 'string' && input.board.trim() ? input.board.trim() : canvasState.activeBoardId;
|
|
564
|
+
const pins =
|
|
565
|
+
boardId === canvasState.activeBoardId
|
|
566
|
+
? canvasState.getContextPinMeta()
|
|
567
|
+
: boardId
|
|
568
|
+
? (canvasState.readBoard(boardId, false)?.state.contextPinMeta ?? {})
|
|
569
|
+
: {};
|
|
570
|
+
return { ok: true, boardId, nodes: canvasState.getNodeReadStatus(boardId), pins };
|
|
571
|
+
},
|
|
572
|
+
});
|
|
573
|
+
|
|
538
574
|
export const axTimelineOperations: Operation[] = [
|
|
539
575
|
axEventRecordOperation,
|
|
540
576
|
axEvidenceAddOperation,
|
|
@@ -545,4 +581,5 @@ export const axTimelineOperations: Operation[] = [
|
|
|
545
581
|
axCommandInvokeOperation,
|
|
546
582
|
axReadsListOperation,
|
|
547
583
|
axReadsRecordOperation,
|
|
584
|
+
axReadsStatusOperation,
|
|
548
585
|
];
|
|
@@ -15,7 +15,7 @@ import { z } from 'zod';
|
|
|
15
15
|
import { normalizeBoardCategory } from '../../../shared/boards.js';
|
|
16
16
|
import { canvasState, type CanvasLayout } from '../../canvas-state.js';
|
|
17
17
|
import { openCanvasBoard } from '../../canvas-operations.js';
|
|
18
|
-
import { summarizeNodeForAgentContext } from '
|
|
18
|
+
import { summarizeNodeForAgentContext } from '../../../shared/agent-context.js';
|
|
19
19
|
import { defineOperation, OperationError, type Operation, type OperationContext } from '../types.js';
|
|
20
20
|
|
|
21
21
|
export function boardsPayload(): Record<string, unknown> {
|
|
@@ -70,15 +70,15 @@ const applyUpdatesSchema = z.looseObject(applyUpdatesShape);
|
|
|
70
70
|
|
|
71
71
|
const applyUpdatesOperation = defineOperation<z.infer<typeof applyUpdatesSchema>, Record<string, unknown>>({
|
|
72
72
|
name: 'canvas.apply-updates',
|
|
73
|
-
mutates:
|
|
73
|
+
mutates: true,
|
|
74
74
|
input: applyUpdatesSchema,
|
|
75
75
|
inputShape: applyUpdatesShape,
|
|
76
76
|
http: {
|
|
77
77
|
method: 'POST',
|
|
78
78
|
path: '/api/canvas/update',
|
|
79
79
|
},
|
|
80
|
-
//
|
|
81
|
-
handler: (input
|
|
80
|
+
// The registry emits the layout update, as for other mutating operations.
|
|
81
|
+
handler: (input) => {
|
|
82
82
|
const body: Record<string, unknown> = input;
|
|
83
83
|
const updates = Array.isArray(body.updates) ? body.updates : [];
|
|
84
84
|
const result =
|
|
@@ -94,9 +94,6 @@ const applyUpdatesOperation = defineOperation<z.infer<typeof applyUpdatesSchema>
|
|
|
94
94
|
return suppressedResult;
|
|
95
95
|
})()
|
|
96
96
|
: applyCanvasNodeUpdates(updates);
|
|
97
|
-
if (result.applied > 0) {
|
|
98
|
-
ctx.emit('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
99
|
-
}
|
|
100
97
|
return { ok: true, ...result };
|
|
101
98
|
},
|
|
102
99
|
});
|
|
@@ -15,9 +15,12 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { z } from 'zod';
|
|
17
17
|
import { INTENT_EDGE_TYPES, INTENT_KINDS } from '../../../shared/ax-intent.js';
|
|
18
|
+
import { canvasState } from '../../canvas-state.js';
|
|
18
19
|
import { intentRegistry } from '../../intent-registry.js';
|
|
20
|
+
import { findOpenCanvasPosition } from '../../placement.js';
|
|
19
21
|
import { readJsonValue } from '../http.js';
|
|
20
22
|
import { defineOperation, OperationError, type Operation } from '../types.js';
|
|
23
|
+
import { defaultNodeSize } from './nodes.js';
|
|
21
24
|
|
|
22
25
|
const positionShape = z.object({ x: z.number(), y: z.number() });
|
|
23
26
|
|
|
@@ -28,7 +31,11 @@ const intentSignalShape = {
|
|
|
28
31
|
.enum(INTENT_KINDS)
|
|
29
32
|
.optional()
|
|
30
33
|
.describe('create | move | connect | remove | edit — the move about to be made.'),
|
|
31
|
-
position: positionShape
|
|
34
|
+
position: positionShape
|
|
35
|
+
.optional()
|
|
36
|
+
.describe(
|
|
37
|
+
'World coords: the destination of a move, or where a create forms. Omit it for a create to ghost where the canvas will place the node.',
|
|
38
|
+
),
|
|
32
39
|
nodeId: z.string().optional().describe('The existing node a move/edit/remove targets.'),
|
|
33
40
|
edge: z
|
|
34
41
|
.object({ from: z.string(), to: z.string(), type: z.enum(INTENT_EDGE_TYPES) })
|
|
@@ -46,6 +53,24 @@ const intentSignalShape = {
|
|
|
46
53
|
|
|
47
54
|
const intentSignalSchema = z.looseObject(intentSignalShape);
|
|
48
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Where a create without a position will land: the same auto-placement the
|
|
58
|
+
* create ops use, with live create ghosts as obstacles so staged ghosts do not
|
|
59
|
+
* stack. An agent announcing work before it knows the layout must not have to
|
|
60
|
+
* guess a position (a guessed 0,0 put every first ghost in the top-left corner).
|
|
61
|
+
*/
|
|
62
|
+
function openPositionForCreate(nodeType: unknown): { x: number; y: number } {
|
|
63
|
+
const size = defaultNodeSize(typeof nodeType === 'string' ? nodeType : 'markdown');
|
|
64
|
+
const ghosts = intentRegistry
|
|
65
|
+
.list()
|
|
66
|
+
.flatMap((intent) =>
|
|
67
|
+
intent.kind === 'create' && intent.position
|
|
68
|
+
? [{ position: intent.position, size: defaultNodeSize(intent.nodeType ?? 'markdown') }]
|
|
69
|
+
: [],
|
|
70
|
+
);
|
|
71
|
+
return findOpenCanvasPosition([...canvasState.getLayout().nodes, ...ghosts], size.width, size.height);
|
|
72
|
+
}
|
|
73
|
+
|
|
49
74
|
const intentSignalOperation = defineOperation<z.infer<typeof intentSignalSchema>, Record<string, unknown>>({
|
|
50
75
|
name: 'intent.signal',
|
|
51
76
|
mutates: false,
|
|
@@ -53,7 +78,11 @@ const intentSignalOperation = defineOperation<z.infer<typeof intentSignalSchema>
|
|
|
53
78
|
inputShape: intentSignalShape,
|
|
54
79
|
http: { method: 'POST', path: '/api/canvas/ax/intent' },
|
|
55
80
|
handler: (input) => {
|
|
56
|
-
const
|
|
81
|
+
const placed =
|
|
82
|
+
input.kind === 'create' && !input.position
|
|
83
|
+
? { ...input, position: openPositionForCreate(input.nodeType) }
|
|
84
|
+
: input;
|
|
85
|
+
const intent = intentRegistry.signal(placed);
|
|
57
86
|
return { ok: true, intent } as unknown as Record<string, unknown>;
|
|
58
87
|
},
|
|
59
88
|
});
|
|
@@ -50,8 +50,8 @@ import {
|
|
|
50
50
|
isRecord,
|
|
51
51
|
pickFiniteNumber,
|
|
52
52
|
pickPositiveNumber,
|
|
53
|
-
resolveCreateGeometry,
|
|
54
53
|
} from './nodes.js';
|
|
54
|
+
import { resolveCreateGeometry } from '../../canvas-geometry.js';
|
|
55
55
|
|
|
56
56
|
/** Legacy server.ts parseGraphPayloadData: a graph dataset must be an array of records. */
|
|
57
57
|
export function parseGraphPayloadData(value: unknown): Array<Record<string, unknown>> | null {
|
|
@@ -309,7 +309,11 @@ const jsonRenderStreamOperation = defineOperation<z.infer<typeof jsonRenderStrea
|
|
|
309
309
|
// node degrades to the bare { ok, id, nodeId } payload.
|
|
310
310
|
let created: Record<string, unknown> = { ok: true, id, nodeId: id };
|
|
311
311
|
try {
|
|
312
|
-
const node = (await host.invoker().invoke('node.get', {
|
|
312
|
+
const node = (await host.invoker().invoke('node.get', {
|
|
313
|
+
id,
|
|
314
|
+
includeBlobs: true,
|
|
315
|
+
contextDelivery: false,
|
|
316
|
+
})) as CanvasNodeState;
|
|
313
317
|
created = { ok: true, node: compactNodePayload(node), id, nodeId: id };
|
|
314
318
|
} catch {
|
|
315
319
|
// keep the bare payload (legacy c.getNode → undefined path)
|
|
@@ -22,13 +22,13 @@ import {
|
|
|
22
22
|
removeCanvasNode,
|
|
23
23
|
resolveHtmlContent,
|
|
24
24
|
scheduleCodeGraphRecompute,
|
|
25
|
-
validateCanvasNodePatch,
|
|
26
25
|
MARKDOWN_NODE_DEFAULT_SIZE,
|
|
27
26
|
MCP_APP_NODE_DEFAULT_SIZE,
|
|
28
27
|
IMAGE_NODE_DEFAULT_SIZE,
|
|
29
28
|
LEDGER_NODE_DEFAULT_SIZE,
|
|
30
29
|
type CanvasSizeAdjustment,
|
|
31
30
|
} from '../../canvas-operations.js';
|
|
31
|
+
import { resolveCreateGeometry, resolvePatchGeometry, validateCanvasNodePatch } from '../../canvas-geometry.js';
|
|
32
32
|
import { normalizeNodeAxCapabilities } from '../../ax-interaction.js';
|
|
33
33
|
import { applyFileContentToNodeData, readFileNodeContent } from '../../file-content.js';
|
|
34
34
|
import { unwatchFileForNode, watchFileForNode } from '../../file-watcher.js';
|
|
@@ -126,81 +126,6 @@ export function pickPositiveNumber(record: Record<string, unknown>, key: string)
|
|
|
126
126
|
return value !== undefined && value > 0 ? value : undefined;
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
function normalizeGeometryInput(body: Record<string, unknown>): {
|
|
130
|
-
x?: number;
|
|
131
|
-
y?: number;
|
|
132
|
-
width?: number;
|
|
133
|
-
height?: number;
|
|
134
|
-
position?: { x?: number; y?: number };
|
|
135
|
-
size?: { width?: number; height?: number };
|
|
136
|
-
} {
|
|
137
|
-
const position = getRecord(body.position);
|
|
138
|
-
const size = getRecord(body.size);
|
|
139
|
-
return {
|
|
140
|
-
...(pickFiniteNumber(body, 'x') !== undefined ? { x: pickFiniteNumber(body, 'x') } : {}),
|
|
141
|
-
...(pickFiniteNumber(body, 'y') !== undefined ? { y: pickFiniteNumber(body, 'y') } : {}),
|
|
142
|
-
...(pickFiniteNumber(body, 'width') !== undefined ? { width: pickFiniteNumber(body, 'width') } : {}),
|
|
143
|
-
...(pickFiniteNumber(body, 'height') !== undefined ? { height: pickFiniteNumber(body, 'height') } : {}),
|
|
144
|
-
...(position
|
|
145
|
-
? {
|
|
146
|
-
position: {
|
|
147
|
-
...(pickFiniteNumber(position, 'x') !== undefined ? { x: pickFiniteNumber(position, 'x') } : {}),
|
|
148
|
-
...(pickFiniteNumber(position, 'y') !== undefined ? { y: pickFiniteNumber(position, 'y') } : {}),
|
|
149
|
-
},
|
|
150
|
-
}
|
|
151
|
-
: {}),
|
|
152
|
-
...(size
|
|
153
|
-
? {
|
|
154
|
-
size: {
|
|
155
|
-
...(pickFiniteNumber(size, 'width') !== undefined ? { width: pickFiniteNumber(size, 'width') } : {}),
|
|
156
|
-
...(pickFiniteNumber(size, 'height') !== undefined ? { height: pickFiniteNumber(size, 'height') } : {}),
|
|
157
|
-
},
|
|
158
|
-
}
|
|
159
|
-
: {}),
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export function resolveCreateGeometry(body: Record<string, unknown>): {
|
|
164
|
-
x?: number;
|
|
165
|
-
y?: number;
|
|
166
|
-
width?: number;
|
|
167
|
-
height?: number;
|
|
168
|
-
} {
|
|
169
|
-
const geometry = normalizeGeometryInput(body);
|
|
170
|
-
const x = geometry.x ?? geometry.position?.x;
|
|
171
|
-
const y = geometry.y ?? geometry.position?.y;
|
|
172
|
-
const width = geometry.width ?? geometry.size?.width;
|
|
173
|
-
const height = geometry.height ?? geometry.size?.height;
|
|
174
|
-
return {
|
|
175
|
-
...(x !== undefined ? { x } : {}),
|
|
176
|
-
...(y !== undefined ? { y } : {}),
|
|
177
|
-
...(width !== undefined ? { width } : {}),
|
|
178
|
-
...(height !== undefined ? { height } : {}),
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function resolvePatchGeometry(
|
|
183
|
-
body: Record<string, unknown>,
|
|
184
|
-
existing: CanvasNodeState,
|
|
185
|
-
): {
|
|
186
|
-
position?: { x: number; y: number };
|
|
187
|
-
size?: { width: number; height: number };
|
|
188
|
-
} {
|
|
189
|
-
const geometry = normalizeGeometryInput(body);
|
|
190
|
-
const x = geometry.x ?? geometry.position?.x;
|
|
191
|
-
const y = geometry.y ?? geometry.position?.y;
|
|
192
|
-
const width = geometry.width ?? geometry.size?.width;
|
|
193
|
-
const height = geometry.height ?? geometry.size?.height;
|
|
194
|
-
return {
|
|
195
|
-
...(x !== undefined || y !== undefined
|
|
196
|
-
? { position: { x: x ?? existing.position.x, y: y ?? existing.position.y } }
|
|
197
|
-
: {}),
|
|
198
|
-
...(width !== undefined || height !== undefined
|
|
199
|
-
? { size: { width: width ?? existing.size.width, height: height ?? existing.size.height } }
|
|
200
|
-
: {}),
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
|
|
204
129
|
type StringListField = { value?: string[]; error?: string };
|
|
205
130
|
|
|
206
131
|
function parseStringListField(field: string, value: unknown): StringListField {
|
|
@@ -1057,6 +982,7 @@ const nodeGetOperation = defineOperation<z.infer<typeof nodeGetSchema>, Serializ
|
|
|
1057
982
|
|
|
1058
983
|
const nodeUpdateShape = {
|
|
1059
984
|
id: z.string().describe('Node ID to update'),
|
|
985
|
+
expectedContentRevision: z.number().int().nonnegative().optional().describe('Reject an edit based on stale content'),
|
|
1060
986
|
intentId: z
|
|
1061
987
|
.string()
|
|
1062
988
|
.optional()
|
|
@@ -1199,6 +1125,12 @@ const nodeUpdateOperation = defineOperation<z.infer<typeof nodeUpdateSchema>, Re
|
|
|
1199
1125
|
const id = input.id;
|
|
1200
1126
|
const existing = canvasState.getNode(id);
|
|
1201
1127
|
if (!existing) throw new OperationError(`Node "${id}" not found.`, 404);
|
|
1128
|
+
if (input.expectedContentRevision !== undefined) {
|
|
1129
|
+
if (body.refresh === true) throw new OperationError('Conditional edits do not support webpage refresh.');
|
|
1130
|
+
if (input.expectedContentRevision !== (existing.contentRevision ?? 0)) {
|
|
1131
|
+
throw new OperationError('This node changed. Reopen the editor before saving your changes.', 409);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1202
1134
|
if (existing.type === 'webpage' && body.refresh === true) {
|
|
1203
1135
|
const rawUrl = typeof body.url === 'string' ? body.url : undefined;
|
|
1204
1136
|
let url: string | undefined;
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
type ContextBriefLibraryBoard,
|
|
29
29
|
type ContextBriefSourceEntry,
|
|
30
30
|
} from '../../context-brief.js';
|
|
31
|
-
import { summarizeNodeForAgentContext } from '
|
|
31
|
+
import { summarizeNodeForAgentContext } from '../../../shared/agent-context.js';
|
|
32
32
|
import { defineOperation, OperationError, type Operation, type OperationMcpToolHost } from '../types.js';
|
|
33
33
|
import { buildSummaryFromLayout, isRecord } from './nodes.js';
|
|
34
34
|
import { readTargetBoard } from './boards.js';
|
|
@@ -46,6 +46,7 @@ const pinShape = {
|
|
|
46
46
|
.unknown()
|
|
47
47
|
.optional()
|
|
48
48
|
.describe('set: replace all pins, add: add to existing pins, remove: unpin these nodes (default: set)'),
|
|
49
|
+
reason: z.unknown().optional().describe('Why these nodes matter; kept with newly pinned nodes and shown on hover.'),
|
|
49
50
|
};
|
|
50
51
|
|
|
51
52
|
const pinSchema = z.looseObject(pinShape);
|
|
@@ -69,6 +70,10 @@ const pinOperation = defineOperation<z.infer<typeof pinSchema>, Record<string, u
|
|
|
69
70
|
.enum(['set', 'add', 'remove'])
|
|
70
71
|
.optional()
|
|
71
72
|
.describe('set: replace all pins, add: add to existing pins, remove: unpin these nodes (default: set)'),
|
|
73
|
+
reason: z
|
|
74
|
+
.string()
|
|
75
|
+
.optional()
|
|
76
|
+
.describe('Why these nodes matter; kept with newly pinned nodes and shown to the human on hover.'),
|
|
72
77
|
},
|
|
73
78
|
// The wire body is { ok, count } (legacy HTTP shape); the tool reports the
|
|
74
79
|
// resulting pin list, so re-read it from the host. (Legacy RemoteCanvasAccess
|
|
@@ -91,7 +96,8 @@ const pinOperation = defineOperation<z.infer<typeof pinSchema>, Record<string, u
|
|
|
91
96
|
// Legacy 'set' capped at MAX_PINS BEFORE setCanvasContextPins dedupes —
|
|
92
97
|
// replicated as-is. add/remove (formerly client-side in the MCP access
|
|
93
98
|
// layer) pass through; setCanvasContextPins normalizes them.
|
|
94
|
-
const
|
|
99
|
+
const reason = typeof body.reason === 'string' && body.reason.trim() ? body.reason.trim().slice(0, 280) : undefined;
|
|
100
|
+
const result = setCanvasContextPins(mode === 'set' ? nodeIds.slice(0, MAX_PINS) : nodeIds, mode, reason);
|
|
95
101
|
ctx.emit('context-pins-changed', { count: result.count, nodeIds: result.nodeIds });
|
|
96
102
|
return { ok: true, count: result.count };
|
|
97
103
|
},
|
|
@@ -318,7 +318,7 @@ const snapshotRestoreOperation = defineOperation<z.infer<typeof snapshotRestoreS
|
|
|
318
318
|
};
|
|
319
319
|
},
|
|
320
320
|
},
|
|
321
|
-
handler: async ({ id }) => {
|
|
321
|
+
handler: async ({ id }, ctx) => {
|
|
322
322
|
// Awaits only the synchronous restore; restoreCanvasSnapshot fires the
|
|
323
323
|
// async ext-app rehydration itself and schedules a deferred
|
|
324
324
|
// canvas-layout-update through setCanvasLayoutUpdateEmitter when it
|
|
@@ -326,6 +326,9 @@ const snapshotRestoreOperation = defineOperation<z.infer<typeof snapshotRestoreS
|
|
|
326
326
|
// add a manual emit here or the frame is doubled.
|
|
327
327
|
const result = await restoreCanvasSnapshot(id ?? '');
|
|
328
328
|
if (!result.ok) throw new OperationError('Snapshot not found', 404);
|
|
329
|
+
const nodeIds = [...canvasState.contextPinnedNodeIds];
|
|
330
|
+
ctx.emit('context-pins-changed', { count: nodeIds.length, nodeIds });
|
|
331
|
+
ctx.emit('ax-state-changed', {});
|
|
329
332
|
return { ok: true };
|
|
330
333
|
},
|
|
331
334
|
});
|
|
@@ -15,7 +15,13 @@
|
|
|
15
15
|
import { z } from 'zod';
|
|
16
16
|
import { tourSchema, derivedTour } from '../../../shared/tour.js';
|
|
17
17
|
import { canvasState } from '../../canvas-state.js';
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
arrangeCanvasNodes,
|
|
20
|
+
clearCanvas,
|
|
21
|
+
fitCanvasView,
|
|
22
|
+
getClientViewportSize,
|
|
23
|
+
setClientViewportSize,
|
|
24
|
+
} from '../../canvas-operations.js';
|
|
19
25
|
import { validateCanvasLayout } from '../../canvas-validation.js';
|
|
20
26
|
import { defineOperation, OperationError, type Operation } from '../types.js';
|
|
21
27
|
import { closeNodeAppSession, isRecord } from './nodes.js';
|
|
@@ -230,6 +236,10 @@ const viewportSetShape = {
|
|
|
230
236
|
x: z.unknown().optional().describe('Viewport x offset'),
|
|
231
237
|
y: z.unknown().optional().describe('Viewport y offset'),
|
|
232
238
|
scale: z.unknown().optional().describe('Viewport zoom scale'),
|
|
239
|
+
center: z
|
|
240
|
+
.unknown()
|
|
241
|
+
.optional()
|
|
242
|
+
.describe('World point {x, y} to centre in the canvas area (instead of x/y); uses scale or the current zoom'),
|
|
233
243
|
recordHistory: z.unknown().optional().describe('Pass false to skip the undo-history entry'),
|
|
234
244
|
clientWidth: z.unknown().optional().describe("The reporting client's window width in CSS pixels"),
|
|
235
245
|
clientHeight: z.unknown().optional().describe("The reporting client's window height in CSS pixels"),
|
|
@@ -255,12 +265,26 @@ const viewportSetOperation = defineOperation<z.infer<typeof viewportSetSchema>,
|
|
|
255
265
|
setClientViewportSize(body.clientWidth, body.clientHeight);
|
|
256
266
|
// A size-only report is not a camera command. Echoing the stored camera
|
|
257
267
|
// here would cancel an in-flight browser focus with its stale fallback.
|
|
258
|
-
|
|
259
|
-
|
|
268
|
+
const center = body.center;
|
|
269
|
+
if (center === undefined && ![body.x, body.y, body.scale].some((value) => typeof value === 'number'))
|
|
270
|
+
return { ok: true };
|
|
271
|
+
const scale = typeof body.scale === 'number' ? body.scale : canvasState.viewport.scale;
|
|
272
|
+
let next = {
|
|
260
273
|
x: typeof body.x === 'number' ? body.x : canvasState.viewport.x,
|
|
261
274
|
y: typeof body.y === 'number' ? body.y : canvasState.viewport.y,
|
|
262
|
-
scale
|
|
275
|
+
scale,
|
|
263
276
|
};
|
|
277
|
+
if (center !== undefined) {
|
|
278
|
+
if (!isRecord(center) || typeof center.x !== 'number' || typeof center.y !== 'number') {
|
|
279
|
+
throw new OperationError('center must be a world point {x, y}.');
|
|
280
|
+
}
|
|
281
|
+
if (body.x !== undefined || body.y !== undefined) throw new OperationError('Pass center or x/y, not both.');
|
|
282
|
+
// Screen-space translate (architecture rule 9): put the world point at
|
|
283
|
+
// the middle of the canvas area the browser last reported (fit's default
|
|
284
|
+
// size when no tab has reported one).
|
|
285
|
+
const area = getClientViewportSize() ?? { width: 1440, height: 900 };
|
|
286
|
+
next = { x: area.width / 2 - center.x * scale, y: area.height / 2 - center.y * scale, scale };
|
|
287
|
+
}
|
|
264
288
|
if (body.recordHistory === false) {
|
|
265
289
|
canvasState.withSuppressedRecording(() => {
|
|
266
290
|
canvasState.setViewport(next);
|
|
@@ -269,7 +293,7 @@ const viewportSetOperation = defineOperation<z.infer<typeof viewportSetSchema>,
|
|
|
269
293
|
canvasState.setViewport(next);
|
|
270
294
|
}
|
|
271
295
|
ctx.emit('canvas-viewport-update', { viewport: canvasState.viewport });
|
|
272
|
-
return { ok: true };
|
|
296
|
+
return { ok: true, viewport: canvasState.viewport };
|
|
273
297
|
},
|
|
274
298
|
});
|
|
275
299
|
|