pmx-canvas 0.2.6 → 0.3.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/CHANGELOG.md +156 -1
- package/Readme.md +13 -11
- package/dist/canvas/index.js +18 -18
- package/dist/json-render/index.css +1 -1
- package/dist/types/mcp/canvas-access.d.ts +23 -7
- package/dist/types/server/ax-state-manager.d.ts +3 -2
- package/dist/types/server/bundled-skills.d.ts +3 -3
- package/dist/types/server/canvas-state.d.ts +2 -0
- package/dist/types/server/ext-app-lookup.d.ts +1 -3
- package/dist/types/server/operations/composites.d.ts +29 -23
- package/dist/types/server/operations/http.d.ts +2 -1
- package/dist/types/shared/ax-intent.d.ts +1 -1
- package/docs/RELEASE.md +23 -8
- package/docs/ax-host-adapter-contract.md +7 -7
- package/docs/ax-state-contract.md +7 -6
- package/docs/bun-webview-integration.md +5 -5
- package/docs/cli.md +1 -1
- package/docs/http-api.md +7 -3
- package/docs/mcp.md +121 -100
- package/docs/node-types.md +45 -35
- package/docs/plans/plan-006-mcp-tool-consolidation.md +3 -1
- package/docs/plans/plan-008-registry-finish.md +2 -0
- package/docs/plans/plan-009-tech-debt-backlog.md +51 -0
- package/docs/screenshot.png +0 -0
- package/docs/tech-debt-assessment-2026-06.md +1 -1
- package/docs/tech-debt-assessment-2026-07.md +135 -0
- package/package.json +1 -1
- package/skills/data-analysis/SKILL.md +3 -3
- package/skills/frontend-design/SKILL.md +3 -2
- package/skills/json-render-mcp/SKILL.md +4 -3
- package/skills/playwright-cli/SKILL.md +1 -1
- package/skills/pmx-canvas/SKILL.md +20 -15
- package/skills/pmx-canvas/evals/evals.json +2 -2
- package/skills/pmx-canvas/references/ax-html-control-surface.md +3 -1
- package/skills/pmx-canvas/references/excalidraw-diagram-authoring.md +2 -2
- package/skills/pmx-canvas/references/full-reference.md +65 -62
- package/skills/pmx-canvas/references/html-primitives.md +3 -3
- package/skills/tufte-viz/SKILL.md +5 -4
- package/skills/web-artifacts-builder/SKILL.md +4 -4
- package/src/client/App.tsx +2 -1
- package/src/mcp/canvas-access.ts +115 -107
- package/src/mcp/server.ts +74 -201
- package/src/server/ax-state-manager.ts +3 -2
- package/src/server/bundled-skills.ts +3 -3
- package/src/server/canvas-schema.ts +46 -29
- package/src/server/canvas-state.ts +5 -0
- package/src/server/code-graph.ts +1 -1
- package/src/server/ext-app-lookup.ts +1 -8
- package/src/server/index.ts +1 -1
- package/src/server/intent-registry.ts +1 -1
- package/src/server/operations/composites.ts +39 -39
- package/src/server/operations/http.ts +3 -2
- package/src/server/operations/mcp.ts +7 -6
- package/src/server/operations/ops/app.ts +4 -4
- package/src/server/operations/ops/nodes.ts +5 -5
- package/src/server/operations/ops/query.ts +19 -8
- package/src/server/operations/ops/snapshots.ts +6 -6
- package/src/server/server.ts +40 -4
- package/src/shared/ax-intent.ts +1 -1
package/src/server/index.ts
CHANGED
|
@@ -885,7 +885,7 @@ export class PmxCanvas extends EventEmitter {
|
|
|
885
885
|
|
|
886
886
|
setContextPins(nodeIds: string[], mode: 'set' | 'add' | 'remove' = 'set'): { count: number; nodeIds: string[] } {
|
|
887
887
|
const result = setCanvasContextPins(nodeIds, mode);
|
|
888
|
-
emitPrimaryWorkbenchEvent('
|
|
888
|
+
emitPrimaryWorkbenchEvent('context-pins-changed', { count: result.count, nodeIds: result.nodeIds });
|
|
889
889
|
return result;
|
|
890
890
|
}
|
|
891
891
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Intents are EPHEMERAL PRESENCE, deliberately modelled like the attention /
|
|
5
5
|
* timeline ephemerality layer rather than canvas-bound state:
|
|
6
6
|
* - an in-memory Map (NOT CanvasStateManager) — never serialized, never
|
|
7
|
-
* snapshotted, never returned by
|
|
7
|
+
* snapshotted, never returned by canvas_query (layout action);
|
|
8
8
|
* - count-capped (oldest evicted) and TTL-swept so a ghost can never linger;
|
|
9
9
|
* - emitted over the same workbench SSE stream as `ax-intent` /
|
|
10
10
|
* `ax-intent-clear` frames via an INJECTED emitter (server.ts wires it,
|
|
@@ -9,17 +9,22 @@
|
|
|
9
9
|
* to the standalone `canvas_add_edge` — same op, same arg mapping, same result
|
|
10
10
|
* shape — by construction. No handler logic lives here.
|
|
11
11
|
*
|
|
12
|
-
* Migration (docs/api-stability.md + plan-006): composites
|
|
13
|
-
* v0.2 alongside the legacy single-purpose tools
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* Migration (docs/api-stability.md + plan-006): composites landed ADDITIVELY in
|
|
13
|
+
* v0.2 alongside the legacy single-purpose tools. As of v0.3.0 the legacy tools
|
|
14
|
+
* those composites fold are REMOVED (registration-suppressed — see
|
|
15
|
+
* `compositeFoldedOpNames` below); every action here maps to a registry-backed
|
|
16
|
+
* operation (plan-005 slices 1–7 + plan-008 Wave 1) that is now reachable ONLY
|
|
17
|
+
* through its composite (or `canvas_batch`).
|
|
16
18
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
19
|
+
* Deferred to v0.4: the `canvas_snapshot` composite (still a name collision —
|
|
20
|
+
* see below). Its 6 legacy snapshot standalones (`canvas_snapshot`,
|
|
21
|
+
* `canvas_list_snapshots`, `canvas_restore`, `canvas_delete_snapshot`,
|
|
22
|
+
* `canvas_gc_snapshots`, `canvas_diff`) stay registered in v0.3.0 but are marked
|
|
23
|
+
* deprecated (description-prefixed) per docs/api-stability.md's
|
|
24
|
+
* deprecate-one-minor-before-removal rule. (`canvas_webview` shipped in plan-008
|
|
25
|
+
* Wave 3 via runner injection; `canvas_app` shipped in Wave 4 — open-mcp-app /
|
|
26
|
+
* diagram / build-artifact. Wave 5 folded the last three legacy tools
|
|
27
|
+
* deprecate-only — NO per-action input-injection mechanism was needed:
|
|
23
28
|
* `canvas_add_html_node` / `canvas_add_html_primitive` → `canvas_node` action
|
|
24
29
|
* "add" (type:"html" [+ primitive]); `canvas_refresh_webpage_node` → `canvas_node`
|
|
25
30
|
* action "update" (refresh:true). `canvas_screenshot` stays standalone — it
|
|
@@ -30,8 +35,8 @@
|
|
|
30
35
|
* name is ALREADY a legacy standalone tool (the save-snapshot tool, op
|
|
31
36
|
* `snapshot.save`), so it cannot be added additively without a name clash, and
|
|
32
37
|
* repurposing `canvas_snapshot` to be action-discriminated now would break
|
|
33
|
-
* existing callers. It lands in v0.
|
|
34
|
-
*
|
|
38
|
+
* existing callers. It lands in v0.4, in the same change that removes the 6
|
|
39
|
+
* kept-but-deprecated legacy snapshot tools and frees the name.
|
|
35
40
|
*
|
|
36
41
|
* This module must never import server.ts or index.ts.
|
|
37
42
|
*/
|
|
@@ -47,7 +52,7 @@ import { z, type ZodRawShape, type ZodTypeAny } from 'zod';
|
|
|
47
52
|
* - Two-discriminator (`canvas_ax_gate`, plan-007 Slice C): a `kind` × `action`
|
|
48
53
|
* matrix folds 9 ops into one tool. Set `extraDiscriminatorShape` (the `kind`
|
|
49
54
|
* enum), `memberOps` (the op names — used to derive the schema union + the
|
|
50
|
-
*
|
|
55
|
+
* folded-op set), `actionEnum` (the action discriminator values), and
|
|
51
56
|
* `resolveOp` (maps `{ kind, action }` → op name, or undefined for an invalid
|
|
52
57
|
* combo → a loud error at dispatch). The flat `actions` map is left empty for
|
|
53
58
|
* these; the matrix path uses `resolveOp` instead.
|
|
@@ -78,8 +83,9 @@ export interface CompositeToolDefinition {
|
|
|
78
83
|
actionEnum?: readonly string[];
|
|
79
84
|
/**
|
|
80
85
|
* Two-discriminator extension: every member op name. Used to build the schema
|
|
81
|
-
* union (all member-op fields, optional) and to
|
|
82
|
-
*
|
|
86
|
+
* union (all member-op fields, optional) and to populate the folded-op set
|
|
87
|
+
* (`compositeFoldedOpNames`) that suppresses each member op's standalone
|
|
88
|
+
* registration.
|
|
83
89
|
*/
|
|
84
90
|
memberOps?: string[];
|
|
85
91
|
/**
|
|
@@ -331,36 +337,30 @@ export const compositeToolDefinitions: CompositeToolDefinition[] = [
|
|
|
331
337
|
];
|
|
332
338
|
|
|
333
339
|
/**
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
*
|
|
338
|
-
*
|
|
339
|
-
* Deriving it
|
|
340
|
-
* folded action automatically
|
|
340
|
+
* Operation names FOLDED by a composite, DERIVED from the composites: every op
|
|
341
|
+
* a composite folds had its standalone single-purpose tool REMOVED in v0.3.0
|
|
342
|
+
* (see docs/api-stability.md). This set is the do-not-register list —
|
|
343
|
+
* `registerOperationTools` skips any op whose name is in it, since the op is
|
|
344
|
+
* only reachable through its composite (and through `canvas_batch`) now.
|
|
345
|
+
* Deriving it from the composites keeps suppression in lockstep with them — a
|
|
346
|
+
* newly folded action is automatically suppressed from standalone registration.
|
|
341
347
|
*/
|
|
342
|
-
export function
|
|
348
|
+
export function compositeFoldedOpNames(
|
|
343
349
|
definitions: CompositeToolDefinition[] = compositeToolDefinitions,
|
|
344
|
-
):
|
|
345
|
-
const
|
|
350
|
+
): Set<string> {
|
|
351
|
+
const names = new Set<string>();
|
|
346
352
|
for (const def of definitions) {
|
|
347
|
-
// Single-discriminator composites:
|
|
348
|
-
for (const
|
|
349
|
-
|
|
353
|
+
// Single-discriminator composites: the flat `actions` map values.
|
|
354
|
+
for (const opName of Object.values(def.actions)) {
|
|
355
|
+
names.add(opName);
|
|
350
356
|
}
|
|
351
|
-
// Two-discriminator composites (canvas_ax_gate):
|
|
352
|
-
//
|
|
353
|
-
|
|
354
|
-
if (def.memberOps && def.describeOp) {
|
|
357
|
+
// Two-discriminator composites (canvas_ax_gate): the explicit `memberOps`
|
|
358
|
+
// list (the flat `actions` map is empty for these).
|
|
359
|
+
if (def.memberOps) {
|
|
355
360
|
for (const opName of def.memberOps) {
|
|
356
|
-
|
|
357
|
-
if (!combo) continue;
|
|
358
|
-
notes.set(
|
|
359
|
-
opName,
|
|
360
|
-
`Deprecated: use ${def.toolName} with kind "${combo.kind}" action "${combo.action}". `,
|
|
361
|
-
);
|
|
361
|
+
names.add(opName);
|
|
362
362
|
}
|
|
363
363
|
}
|
|
364
364
|
}
|
|
365
|
-
return
|
|
365
|
+
return names;
|
|
366
366
|
}
|
|
@@ -38,7 +38,8 @@ function matchPath(template: string, pathname: string): Record<string, string> |
|
|
|
38
38
|
/**
|
|
39
39
|
* Shared body reader: preserves the parsed JSON value as-is (object, array,
|
|
40
40
|
* or primitive) — per-op `readInput` decides what to do with non-object
|
|
41
|
-
* bodies; the shared reader never coerces.
|
|
41
|
+
* bodies; the shared reader never coerces. A non-empty body that fails to
|
|
42
|
+
* parse is a 400 (OperationError), never a silent empty input.
|
|
42
43
|
*/
|
|
43
44
|
export async function readJsonValue(req: Request): Promise<unknown> {
|
|
44
45
|
let text = '';
|
|
@@ -51,7 +52,7 @@ export async function readJsonValue(req: Request): Promise<unknown> {
|
|
|
51
52
|
try {
|
|
52
53
|
return JSON.parse(text) as unknown;
|
|
53
54
|
} catch {
|
|
54
|
-
|
|
55
|
+
throw new OperationError('Malformed JSON body.');
|
|
55
56
|
}
|
|
56
57
|
}
|
|
57
58
|
|
|
@@ -12,7 +12,7 @@ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
12
12
|
import { getOperation, listOperations } from './registry.js';
|
|
13
13
|
import type { OperationInvoker } from './invoker.js';
|
|
14
14
|
import { OperationError, type OperationMcpToolHost } from './types.js';
|
|
15
|
-
import {
|
|
15
|
+
import { compositeFoldedOpNames, compositeToolDefinitions, type CompositeToolDefinition } from './composites.js';
|
|
16
16
|
|
|
17
17
|
export interface OperationToolHost extends OperationMcpToolHost {
|
|
18
18
|
invoker(): OperationInvoker;
|
|
@@ -22,16 +22,17 @@ export function registerOperationTools(
|
|
|
22
22
|
server: McpServer,
|
|
23
23
|
getHost: () => Promise<OperationToolHost>,
|
|
24
24
|
): void {
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
|
|
25
|
+
// Ops folded by a composite are NOT registered standalone: their legacy
|
|
26
|
+
// single-purpose tools were removed in v0.3.0 (docs/api-stability.md). The op
|
|
27
|
+
// itself is untouched — it stays reachable via its composite and canvas_batch.
|
|
28
|
+
const foldedOpNames = compositeFoldedOpNames();
|
|
28
29
|
for (const op of listOperations()) {
|
|
30
|
+
if (foldedOpNames.has(op.name)) continue;
|
|
29
31
|
const tool = op.mcp;
|
|
30
32
|
if (!tool) continue;
|
|
31
|
-
const note = deprecations.get(op.name);
|
|
32
33
|
server.tool(
|
|
33
34
|
tool.toolName,
|
|
34
|
-
|
|
35
|
+
tool.description,
|
|
35
36
|
{ ...op.inputShape, ...(tool.extraShape ?? {}) },
|
|
36
37
|
async (input: Record<string, unknown>) => {
|
|
37
38
|
try {
|
|
@@ -94,10 +94,10 @@ export async function openMcpAppCore(
|
|
|
94
94
|
// event is dropped, so the node would never be created and the just-opened
|
|
95
95
|
// external session would leak with a null nodeId. Reject loudly BEFORE opening
|
|
96
96
|
// any session rather than silently corrupting. mcpapp.open / diagram.open are
|
|
97
|
-
// not batchable — call
|
|
97
|
+
// not batchable — call canvas_app (action "open-mcp-app" / "diagram") instead.
|
|
98
98
|
if (isEmitSuppressed()) {
|
|
99
99
|
throw new OperationError(
|
|
100
|
-
'mcpapp.open / diagram.open cannot run inside canvas_batch: the canvas node is created from the ext-app-open SSE event, which batch suppresses. Call
|
|
100
|
+
'mcpapp.open / diagram.open cannot run inside canvas_batch: the canvas node is created from the ext-app-open SSE event, which batch suppresses. Call canvas_app with action "open-mcp-app" or "diagram" outside the batch.',
|
|
101
101
|
);
|
|
102
102
|
}
|
|
103
103
|
const targetNode = input.nodeId ? canvasState.getNode(input.nodeId) : undefined;
|
|
@@ -246,7 +246,7 @@ const openMcpAppOperation = defineOperation<z.infer<typeof openMcpAppSchema>, Op
|
|
|
246
246
|
mcp: {
|
|
247
247
|
toolName: 'canvas_open_mcp_app',
|
|
248
248
|
description:
|
|
249
|
-
'Connect to an external MCP server that declares a ui:// app resource, call the specified tool, and open the resulting MCP App inside a canvas mcp-app node. This is a full external-MCP transport call, not the CLI kind shortcut; use
|
|
249
|
+
'Connect to an external MCP server that declares a ui:// app resource, call the specified tool, and open the resulting MCP App inside a canvas mcp-app node. This is a full external-MCP transport call, not the CLI kind shortcut; use canvas_app action "diagram" for the built-in Excalidraw preset.',
|
|
250
250
|
formatResult: (result) => ({
|
|
251
251
|
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
|
|
252
252
|
}),
|
|
@@ -283,7 +283,7 @@ const diagramOperation = defineOperation<z.infer<typeof diagramSchema>, OpenMcpA
|
|
|
283
283
|
mcp: {
|
|
284
284
|
toolName: 'canvas_add_diagram',
|
|
285
285
|
description:
|
|
286
|
-
'Draw a hand-drawn diagram on the canvas via the hosted Excalidraw MCP app. Pass an array of Excalidraw elements (rectangles, ellipses, diamonds, arrows, text). The diagram opens inside an mcp-app node that supports fullscreen editing. For other MCP apps, use
|
|
286
|
+
'Draw a hand-drawn diagram on the canvas via the hosted Excalidraw MCP app. Pass an array of Excalidraw elements (rectangles, ellipses, diamonds, arrows, text). The diagram opens inside an mcp-app node that supports fullscreen editing. For other MCP apps, use canvas_app action "open-mcp-app".',
|
|
287
287
|
formatResult: (result) => ({
|
|
288
288
|
content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
|
|
289
289
|
}),
|
|
@@ -654,7 +654,7 @@ function createGroupNode(body: Record<string, unknown>): NodeAddResult {
|
|
|
654
654
|
|
|
655
655
|
const nodeAddShape = {
|
|
656
656
|
intentId: z.string().optional().catch(undefined).describe('Ghost intent id returned by canvas_intent signal. A vetoed or expired intent blocks this mutation.'),
|
|
657
|
-
type: z.string().optional().catch(undefined).describe('Node type (prefer
|
|
657
|
+
type: z.string().optional().catch(undefined).describe('Node type (prefer canvas_group {action:"create"} for groups)'),
|
|
658
658
|
title: z.string().optional().catch(undefined).describe('Node title'),
|
|
659
659
|
content: z.string().optional().catch(undefined).describe('Node content (markdown for markdown nodes, file path for file nodes, image path/URL/data-URI for image nodes, URL for webpage nodes)'),
|
|
660
660
|
path: z.string().optional().catch(undefined).describe('Compatibility alias for image node content. Prefer content for image paths.'),
|
|
@@ -665,7 +665,7 @@ const nodeAddShape = {
|
|
|
665
665
|
height: z.number().optional().catch(undefined).describe('Height in pixels (default: 600)'),
|
|
666
666
|
strictSize: z.boolean().optional().catch(undefined).describe('Keep explicit width/height fixed and scroll overflowing content instead of browser auto-fitting'),
|
|
667
667
|
children: z.unknown().optional().describe('Group-only alias for childIds. Node IDs to include in a generic group node.'),
|
|
668
|
-
childIds: z.unknown().optional().describe('Group-only field. Node IDs to include in a generic group node. Prefer
|
|
668
|
+
childIds: z.unknown().optional().describe('Group-only field. Node IDs to include in a generic group node. Prefer canvas_group {action:"create"} for groups.'),
|
|
669
669
|
childLayout: z.enum(['grid', 'column', 'flow']).optional().catch(undefined).describe('Group-only optional layout for grouped children.'),
|
|
670
670
|
color: z.string().optional().catch(undefined).describe('Group-only frame accent color.'),
|
|
671
671
|
toolName: z.string().optional().catch(undefined).describe('Trace node tool or operation label'),
|
|
@@ -708,12 +708,12 @@ const nodeAddOperation = defineOperation<z.infer<typeof nodeAddSchema>, NodeAddR
|
|
|
708
708
|
},
|
|
709
709
|
mcp: {
|
|
710
710
|
toolName: 'canvas_add_node',
|
|
711
|
-
description: 'Add a basic node to the canvas. Returns the created node with normalized title/content and rendered geometry. Supported here: markdown, status, context, ledger, trace, file, image, webpage, mcp-app, html, group. Dedicated
|
|
711
|
+
description: 'Add a basic node to the canvas. Returns the created node with normalized title/content and rendered geometry. Supported here: markdown, status, context, ledger, trace, file, image, webpage, mcp-app, html, group. Dedicated routes: json-render -> canvas_render {action:"add-json-render"}, graph -> canvas_render {action:"add-graph"}, web-artifact -> canvas_app {action:"build-artifact"}, external apps -> canvas_app {action:"open-mcp-app"}, groups -> canvas_group {action:"create"}. Call canvas_render {action:"describe-schema"} for the full nodeTypeRouting table.',
|
|
712
712
|
extraShape: {
|
|
713
713
|
type: z.enum(['markdown', 'status', 'context', 'ledger', 'trace', 'file', 'image', 'webpage', 'mcp-app', 'html', 'group'])
|
|
714
|
-
.describe('Node type (prefer
|
|
714
|
+
.describe('Node type (prefer canvas_group {action:"create"} for groups)'),
|
|
715
715
|
children: z.array(z.string()).optional().describe('Group-only alias for childIds. Node IDs to include in a generic group node.'),
|
|
716
|
-
childIds: z.array(z.string()).optional().describe('Group-only field. Node IDs to include in a generic group node. Prefer
|
|
716
|
+
childIds: z.array(z.string()).optional().describe('Group-only field. Node IDs to include in a generic group node. Prefer canvas_group {action:"create"} for groups.'),
|
|
717
717
|
axCapabilities: z.object({
|
|
718
718
|
enabled: z.boolean().optional(),
|
|
719
719
|
allowed: z.array(z.string()).optional(),
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
* - pin.set: legacy POST /api/canvas/context-pins emitted ONLY
|
|
7
7
|
* context-pins-changed (no canvas-layout-update), so mutates: false with a
|
|
8
8
|
* manual ctx.emit. The injected emitter adds the sessionId/timestamp fields
|
|
9
|
-
* the legacy handler set explicitly. (The SDK's setContextPins emits
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* the legacy handler set explicitly. (The SDK's setContextPins emits the
|
|
10
|
+
* same context-pins-changed event — its old layout-update drift was erased
|
|
11
|
+
* in v0.3.0.)
|
|
12
12
|
* - canvas.undo / canvas.redo: legacy handlers emitted canvas-viewport-update
|
|
13
13
|
* then canvas-layout-update only after an entry was actually undone/redone,
|
|
14
14
|
* so mutates: false with conditional manual emits. (The SDK's undo/redo also
|
|
@@ -87,7 +87,7 @@ const pinOperation = defineOperation<z.infer<typeof pinSchema>, Record<string, u
|
|
|
87
87
|
|
|
88
88
|
const searchShape = {
|
|
89
89
|
q: z.unknown().optional().describe('Search query — matches against node titles, content, and file paths'),
|
|
90
|
-
limit: z.unknown().optional().describe('Max results to return (default: 10
|
|
90
|
+
limit: z.unknown().optional().describe('Max results to return (default: all over HTTP, 10 via the MCP tool).'),
|
|
91
91
|
};
|
|
92
92
|
|
|
93
93
|
const searchSchema = z.looseObject(searchShape);
|
|
@@ -108,9 +108,12 @@ const searchOperation = defineOperation<z.infer<typeof searchSchema>, Record<str
|
|
|
108
108
|
query: z.string().describe('Search query — matches against node titles, content, and file paths'),
|
|
109
109
|
limit: z.number().optional().describe('Max results to return (default: 10)'),
|
|
110
110
|
},
|
|
111
|
-
//
|
|
112
|
-
// `
|
|
113
|
-
buildInput: (input) => ({
|
|
111
|
+
// Map the MCP-facing `query` arg onto the wire's `q`. The handler caps by
|
|
112
|
+
// `limit` on every transport; the MCP tool additionally defaults it to 10.
|
|
113
|
+
buildInput: (input) => ({
|
|
114
|
+
q: typeof input.query === 'string' ? input.query : '',
|
|
115
|
+
...(typeof input.limit === 'number' ? { limit: input.limit } : {}),
|
|
116
|
+
}),
|
|
114
117
|
formatResult: (result, input) => {
|
|
115
118
|
const body = isRecord(result) ? result : {};
|
|
116
119
|
const results = Array.isArray(body.results) ? body.results : [];
|
|
@@ -132,7 +135,15 @@ const searchOperation = defineOperation<z.infer<typeof searchSchema>, Record<str
|
|
|
132
135
|
if (!q.trim()) {
|
|
133
136
|
return { results: [], query: q };
|
|
134
137
|
}
|
|
135
|
-
|
|
138
|
+
const rawLimit = input.limit;
|
|
139
|
+
const limit = typeof rawLimit === 'number'
|
|
140
|
+
? rawLimit
|
|
141
|
+
: typeof rawLimit === 'string' && rawLimit.trim() !== '' ? Number(rawLimit) : Number.NaN;
|
|
142
|
+
const results = searchNodes(canvasState.getLayout().nodes, q);
|
|
143
|
+
return {
|
|
144
|
+
results: Number.isFinite(limit) && limit > 0 ? results.slice(0, Math.floor(limit)) : results,
|
|
145
|
+
query: q,
|
|
146
|
+
};
|
|
136
147
|
},
|
|
137
148
|
});
|
|
138
149
|
|
|
@@ -69,7 +69,7 @@ const snapshotListOperation = defineOperation<z.infer<typeof snapshotListSchema>
|
|
|
69
69
|
},
|
|
70
70
|
mcp: {
|
|
71
71
|
toolName: 'canvas_list_snapshots',
|
|
72
|
-
description: 'List saved canvas snapshots with IDs, names, timestamps, and node/edge counts. Defaults to the 20 newest snapshots; pass all=true to return every snapshot.',
|
|
72
|
+
description: 'Deprecated: folds into the canvas_snapshot composite in v0.4 (actions save|list|restore|delete|gc|diff). List saved canvas snapshots with IDs, names, timestamps, and node/edge counts. Defaults to the 20 newest snapshots; pass all=true to return every snapshot.',
|
|
73
73
|
extraShape: {
|
|
74
74
|
limit: z.number().optional().describe('Maximum snapshots to return (default: 20)'),
|
|
75
75
|
query: z.string().optional().describe('Optional case-insensitive ID/name filter'),
|
|
@@ -117,7 +117,7 @@ const snapshotSaveOperation = defineOperation<z.infer<typeof snapshotSaveSchema>
|
|
|
117
117
|
},
|
|
118
118
|
mcp: {
|
|
119
119
|
toolName: 'canvas_snapshot',
|
|
120
|
-
description: 'Save the current canvas state as a named snapshot. Snapshots persist to disk and can be restored later.',
|
|
120
|
+
description: 'Deprecated: folds into the canvas_snapshot composite in v0.4 (actions save|list|restore|delete|gc|diff). Save the current canvas state as a named snapshot. Snapshots persist to disk and can be restored later.',
|
|
121
121
|
extraShape: {
|
|
122
122
|
name: z.string().describe('Name for this snapshot (e.g., "before refactor", "investigation v2")'),
|
|
123
123
|
},
|
|
@@ -154,7 +154,7 @@ const snapshotGcOperation = defineOperation<z.infer<typeof snapshotGcSchema>, Re
|
|
|
154
154
|
},
|
|
155
155
|
mcp: {
|
|
156
156
|
toolName: 'canvas_gc_snapshots',
|
|
157
|
-
description: 'Delete old saved canvas snapshots, keeping the newest N snapshots. Use dryRun=true to preview deletions.',
|
|
157
|
+
description: 'Deprecated: folds into the canvas_snapshot composite in v0.4 (actions save|list|restore|delete|gc|diff). Delete old saved canvas snapshots, keeping the newest N snapshots. Use dryRun=true to preview deletions.',
|
|
158
158
|
extraShape: {
|
|
159
159
|
keep: z.number().optional().describe('Number of newest snapshots to keep (default: 20)'),
|
|
160
160
|
dryRun: z.boolean().optional().describe('Preview deletions without removing snapshot files'),
|
|
@@ -207,7 +207,7 @@ const snapshotDiffOperation = defineOperation<z.infer<typeof snapshotDiffSchema>
|
|
|
207
207
|
},
|
|
208
208
|
mcp: {
|
|
209
209
|
toolName: 'canvas_diff',
|
|
210
|
-
description: 'Compare the current canvas state against a saved snapshot. Shows added/removed/modified nodes and edges. Pass either a snapshot name or ID.',
|
|
210
|
+
description: 'Deprecated: folds into the canvas_snapshot composite in v0.4 (actions save|list|restore|delete|gc|diff). Compare the current canvas state against a saved snapshot. Shows added/removed/modified nodes and edges. Pass either a snapshot name or ID.',
|
|
211
211
|
extraShape: {
|
|
212
212
|
snapshot: z.string().describe('Snapshot name or ID to compare against'),
|
|
213
213
|
},
|
|
@@ -289,7 +289,7 @@ const snapshotRestoreOperation = defineOperation<z.infer<typeof snapshotRestoreS
|
|
|
289
289
|
},
|
|
290
290
|
mcp: {
|
|
291
291
|
toolName: 'canvas_restore',
|
|
292
|
-
description: 'Restore the canvas to a previously saved snapshot. Use canvas_snapshot to save first. Pass either the snapshot ID or name to restore.',
|
|
292
|
+
description: 'Deprecated: folds into the canvas_snapshot composite in v0.4 (actions save|list|restore|delete|gc|diff). Restore the canvas to a previously saved snapshot. Use canvas_snapshot to save first. Pass either the snapshot ID or name to restore.',
|
|
293
293
|
extraShape: {
|
|
294
294
|
id: z.string().describe('Snapshot ID or name to restore (from canvas_snapshot or snapshot list)'),
|
|
295
295
|
},
|
|
@@ -338,7 +338,7 @@ const snapshotDeleteOperation = defineOperation<z.infer<typeof snapshotDeleteSch
|
|
|
338
338
|
},
|
|
339
339
|
mcp: {
|
|
340
340
|
toolName: 'canvas_delete_snapshot',
|
|
341
|
-
description: 'Delete a saved snapshot by ID.',
|
|
341
|
+
description: 'Deprecated: folds into the canvas_snapshot composite in v0.4 (actions save|list|restore|delete|gc|diff). Delete a saved snapshot by ID.',
|
|
342
342
|
extraShape: {
|
|
343
343
|
id: z.string().describe('Snapshot ID to delete'),
|
|
344
344
|
},
|
package/src/server/server.ts
CHANGED
|
@@ -422,7 +422,10 @@ async function withCanvasAutomationWebViewTimeout<T>(task: Promise<T>, action: s
|
|
|
422
422
|
reject(
|
|
423
423
|
new Error(
|
|
424
424
|
`Timed out after ${getCanvasAutomationWebViewTimeoutMs()}ms while ${action}. ` +
|
|
425
|
-
'Bun.WebView may be unavailable in this environment
|
|
425
|
+
'The Bun.WebView backend may be slow to launch or unavailable in this environment ' +
|
|
426
|
+
'(the chrome backend is known-flaky on some hosts). On macOS prefer the webkit ' +
|
|
427
|
+
'backend (start --backend webkit), or raise PMX_CANVAS_WEBVIEW_TIMEOUT_MS for a ' +
|
|
428
|
+
'slow-but-available backend.',
|
|
426
429
|
),
|
|
427
430
|
);
|
|
428
431
|
}, getCanvasAutomationWebViewTimeoutMs());
|
|
@@ -917,7 +920,8 @@ function rotatePrimaryWorkbenchSessionIfNeeded(): void {
|
|
|
917
920
|
primaryWorkbenchSessionId = `pmx-${Date.now().toString(36)}`;
|
|
918
921
|
}
|
|
919
922
|
|
|
920
|
-
|
|
923
|
+
/** Null means a non-empty body failed to parse — handlers must 400, not silently no-op. */
|
|
924
|
+
async function readJson(req: Request): Promise<Record<string, unknown> | null> {
|
|
921
925
|
let text = '';
|
|
922
926
|
try {
|
|
923
927
|
text = await req.text();
|
|
@@ -934,10 +938,14 @@ async function readJson(req: Request): Promise<Record<string, unknown>> {
|
|
|
934
938
|
return value as Record<string, unknown>;
|
|
935
939
|
} catch (error) {
|
|
936
940
|
logWorkbenchWarning('readJson', error);
|
|
937
|
-
return
|
|
941
|
+
return null;
|
|
938
942
|
}
|
|
939
943
|
}
|
|
940
944
|
|
|
945
|
+
function malformedJsonResponse(): Response {
|
|
946
|
+
return responseJson({ ok: false, error: 'Malformed JSON body.' }, 400);
|
|
947
|
+
}
|
|
948
|
+
|
|
941
949
|
function htmlEscape(value: string): string {
|
|
942
950
|
return value
|
|
943
951
|
.replaceAll('&', '&')
|
|
@@ -1225,6 +1233,7 @@ function addFrameDocument(html: string, sandbox: string): string {
|
|
|
1225
1233
|
|
|
1226
1234
|
async function handleCreateFrameDocument(req: Request): Promise<Response> {
|
|
1227
1235
|
const body = await readJson(req);
|
|
1236
|
+
if (body === null) return malformedJsonResponse();
|
|
1228
1237
|
const html = body.html;
|
|
1229
1238
|
if (typeof html !== 'string' || !html) {
|
|
1230
1239
|
return responseJson({ ok: false, error: 'Frame document requires non-empty html.' }, 400);
|
|
@@ -1367,6 +1376,7 @@ function handleNodeSurface(pathname: string, url: URL): Response {
|
|
|
1367
1376
|
|
|
1368
1377
|
async function handleCanvasUpdate(req: Request): Promise<Response> {
|
|
1369
1378
|
const body = await readJson(req);
|
|
1379
|
+
if (body === null) return malformedJsonResponse();
|
|
1370
1380
|
const updates = Array.isArray(body.updates) ? body.updates : [];
|
|
1371
1381
|
const result = body.recordHistory === false
|
|
1372
1382
|
? (() => {
|
|
@@ -1385,6 +1395,7 @@ async function handleCanvasUpdate(req: Request): Promise<Response> {
|
|
|
1385
1395
|
|
|
1386
1396
|
async function handleCanvasViewport(req: Request): Promise<Response> {
|
|
1387
1397
|
const body = await readJson(req);
|
|
1398
|
+
if (body === null) return malformedJsonResponse();
|
|
1388
1399
|
const next = {
|
|
1389
1400
|
x: typeof body.x === 'number' ? body.x : canvasState.viewport.x,
|
|
1390
1401
|
y: typeof body.y === 'number' ? body.y : canvasState.viewport.y,
|
|
@@ -1435,6 +1446,7 @@ function parseAnnotationPoints(value: unknown): CanvasAnnotation['points'] {
|
|
|
1435
1446
|
|
|
1436
1447
|
async function handleCanvasAddAnnotation(req: Request): Promise<Response> {
|
|
1437
1448
|
const body = await readJson(req);
|
|
1449
|
+
if (body === null) return malformedJsonResponse();
|
|
1438
1450
|
const type = body.type === 'text' ? 'text' : 'freehand';
|
|
1439
1451
|
const points = parseAnnotationPoints(body.points);
|
|
1440
1452
|
if (points.length < (type === 'text' ? 1 : 2)) {
|
|
@@ -1521,6 +1533,7 @@ async function handleCanvasRefreshWebpageNode(nodeId: string, req: Request): Pro
|
|
|
1521
1533
|
}
|
|
1522
1534
|
|
|
1523
1535
|
const body = await readJson(req);
|
|
1536
|
+
if (body === null) return malformedJsonResponse();
|
|
1524
1537
|
const rawUrl = typeof body.url === 'string' ? body.url : undefined;
|
|
1525
1538
|
let url: string | undefined;
|
|
1526
1539
|
if (rawUrl && rawUrl.trim().length > 0) {
|
|
@@ -1541,6 +1554,7 @@ async function handleCanvasRefreshWebpageNode(nodeId: string, req: Request): Pro
|
|
|
1541
1554
|
|
|
1542
1555
|
async function handleCanvasThemeUpdate(req: Request): Promise<Response> {
|
|
1543
1556
|
const body = await readJson(req);
|
|
1557
|
+
if (body === null) return malformedJsonResponse();
|
|
1544
1558
|
const theme = normalizeCanvasTheme(body.theme, canvasState.theme);
|
|
1545
1559
|
const next = canvasState.setTheme(theme);
|
|
1546
1560
|
broadcastWorkbenchEvent('theme-changed', {
|
|
@@ -1820,6 +1834,7 @@ function handleRead(pathLike: string): Response {
|
|
|
1820
1834
|
|
|
1821
1835
|
async function handleExtAppCallTool(req: Request): Promise<Response> {
|
|
1822
1836
|
const body = await readJson(req);
|
|
1837
|
+
if (body === null) return malformedJsonResponse();
|
|
1823
1838
|
const sessionId = typeof body.sessionId === 'string' ? body.sessionId.trim() : '';
|
|
1824
1839
|
const toolName = typeof body.toolName === 'string' ? body.toolName.trim() : '';
|
|
1825
1840
|
if (!sessionId || !toolName) {
|
|
@@ -1890,6 +1905,7 @@ async function handleExtAppCallTool(req: Request): Promise<Response> {
|
|
|
1890
1905
|
|
|
1891
1906
|
async function handleExtAppReadResource(req: Request): Promise<Response> {
|
|
1892
1907
|
const body = await readJson(req);
|
|
1908
|
+
if (body === null) return malformedJsonResponse();
|
|
1893
1909
|
const sessionId = typeof body.sessionId === 'string' ? body.sessionId.trim() : '';
|
|
1894
1910
|
const uri = typeof body.uri === 'string' ? body.uri.trim() : '';
|
|
1895
1911
|
if (!sessionId || !uri) {
|
|
@@ -1906,6 +1922,7 @@ async function handleExtAppReadResource(req: Request): Promise<Response> {
|
|
|
1906
1922
|
|
|
1907
1923
|
async function handleExtAppListTools(req: Request): Promise<Response> {
|
|
1908
1924
|
const body = await readJson(req);
|
|
1925
|
+
if (body === null) return malformedJsonResponse();
|
|
1909
1926
|
const sessionId = typeof body.sessionId === 'string' ? body.sessionId.trim() : '';
|
|
1910
1927
|
if (!sessionId) return responseJson({ ok: false, error: 'Missing sessionId.' }, 400);
|
|
1911
1928
|
|
|
@@ -1919,6 +1936,7 @@ async function handleExtAppListTools(req: Request): Promise<Response> {
|
|
|
1919
1936
|
|
|
1920
1937
|
async function handleExtAppListResources(req: Request): Promise<Response> {
|
|
1921
1938
|
const body = await readJson(req);
|
|
1939
|
+
if (body === null) return malformedJsonResponse();
|
|
1922
1940
|
const sessionId = typeof body.sessionId === 'string' ? body.sessionId.trim() : '';
|
|
1923
1941
|
if (!sessionId) return responseJson({ ok: false, error: 'Missing sessionId.' }, 400);
|
|
1924
1942
|
|
|
@@ -1932,6 +1950,7 @@ async function handleExtAppListResources(req: Request): Promise<Response> {
|
|
|
1932
1950
|
|
|
1933
1951
|
async function handleExtAppListResourceTemplates(req: Request): Promise<Response> {
|
|
1934
1952
|
const body = await readJson(req);
|
|
1953
|
+
if (body === null) return malformedJsonResponse();
|
|
1935
1954
|
const sessionId = typeof body.sessionId === 'string' ? body.sessionId.trim() : '';
|
|
1936
1955
|
if (!sessionId) return responseJson({ ok: false, error: 'Missing sessionId.' }, 400);
|
|
1937
1956
|
|
|
@@ -1945,6 +1964,7 @@ async function handleExtAppListResourceTemplates(req: Request): Promise<Response
|
|
|
1945
1964
|
|
|
1946
1965
|
async function handleExtAppListPrompts(req: Request): Promise<Response> {
|
|
1947
1966
|
const body = await readJson(req);
|
|
1967
|
+
if (body === null) return malformedJsonResponse();
|
|
1948
1968
|
const sessionId = typeof body.sessionId === 'string' ? body.sessionId.trim() : '';
|
|
1949
1969
|
if (!sessionId) return responseJson({ ok: false, error: 'Missing sessionId.' }, 400);
|
|
1950
1970
|
|
|
@@ -1958,6 +1978,7 @@ async function handleExtAppListPrompts(req: Request): Promise<Response> {
|
|
|
1958
1978
|
|
|
1959
1979
|
async function handleExtAppModelContext(req: Request): Promise<Response> {
|
|
1960
1980
|
const body = await readJson(req);
|
|
1981
|
+
if (body === null) return malformedJsonResponse();
|
|
1961
1982
|
const nodeId = typeof body.nodeId === 'string' ? body.nodeId.trim() : '';
|
|
1962
1983
|
if (!nodeId) return responseJson({ ok: false, error: 'Missing nodeId.' }, 400);
|
|
1963
1984
|
|
|
@@ -2030,6 +2051,7 @@ function currentWorkbenchUrl(): string | null {
|
|
|
2030
2051
|
|
|
2031
2052
|
async function handleWorkbenchWebViewScreenshot(req: Request): Promise<Response> {
|
|
2032
2053
|
const body = await readJson(req);
|
|
2054
|
+
if (body === null) return malformedJsonResponse();
|
|
2033
2055
|
const format = body.format === 'jpeg' || body.format === 'webp' || body.format === 'png'
|
|
2034
2056
|
? body.format
|
|
2035
2057
|
: 'png';
|
|
@@ -2060,6 +2082,7 @@ async function handleWorkbenchWebViewScreenshot(req: Request): Promise<Response>
|
|
|
2060
2082
|
|
|
2061
2083
|
async function handleWorkbenchOpen(req: Request): Promise<Response> {
|
|
2062
2084
|
const body = await readJson(req);
|
|
2085
|
+
if (body === null) return malformedJsonResponse();
|
|
2063
2086
|
const pathLike = typeof body.path === 'string' ? body.path : '';
|
|
2064
2087
|
const safePath = resolveWorkspaceMarkdownPath(pathLike);
|
|
2065
2088
|
if (!safePath) return responseText('Invalid path', 400);
|
|
@@ -2071,6 +2094,7 @@ async function handleWorkbenchOpen(req: Request): Promise<Response> {
|
|
|
2071
2094
|
|
|
2072
2095
|
async function handleWorkbenchIntent(req: Request): Promise<Response> {
|
|
2073
2096
|
const body = await readJson(req);
|
|
2097
|
+
if (body === null) return malformedJsonResponse();
|
|
2074
2098
|
const rawType = typeof body.type === 'string' ? body.type.trim() : '';
|
|
2075
2099
|
if (!rawType) return responseText('Missing intent type', 400);
|
|
2076
2100
|
if (!ALLOWED_WORKBENCH_INTENTS.has(rawType as PrimaryWorkbenchIntent['type'])) {
|
|
@@ -2231,6 +2255,7 @@ function handleWorkbenchEvents(req: Request): Response {
|
|
|
2231
2255
|
|
|
2232
2256
|
async function handleSave(req: Request): Promise<Response> {
|
|
2233
2257
|
const body = await readJson(req);
|
|
2258
|
+
if (body === null) return malformedJsonResponse();
|
|
2234
2259
|
const pathLike = typeof body.path === 'string' ? body.path : '';
|
|
2235
2260
|
const safePath = resolveWorkspaceMarkdownPath(pathLike);
|
|
2236
2261
|
if (!safePath) return responseText('Invalid path', 400);
|
|
@@ -2247,6 +2272,7 @@ async function handleSave(req: Request): Promise<Response> {
|
|
|
2247
2272
|
|
|
2248
2273
|
async function handleRender(req: Request): Promise<Response> {
|
|
2249
2274
|
const body = await readJson(req);
|
|
2275
|
+
if (body === null) return malformedJsonResponse();
|
|
2250
2276
|
const markdown = typeof body.markdown === 'string' ? body.markdown : '';
|
|
2251
2277
|
const html =
|
|
2252
2278
|
(marked.parse(normalizeMarkdownExternalUrls(markdown), {
|
|
@@ -2268,6 +2294,7 @@ function buildSelectionContextPreamble(contextNodeIds: string[]): string {
|
|
|
2268
2294
|
|
|
2269
2295
|
async function handleCanvasPrompt(req: Request): Promise<Response> {
|
|
2270
2296
|
const body = await readJson(req);
|
|
2297
|
+
if (body === null) return malformedJsonResponse();
|
|
2271
2298
|
const text = typeof body.text === 'string' ? body.text.trim() : '';
|
|
2272
2299
|
if (!text) return responseText('Missing prompt text', 400);
|
|
2273
2300
|
|
|
@@ -2601,6 +2628,7 @@ function normalizeActivityReactions(input: Record<string, unknown>): {
|
|
|
2601
2628
|
// Report primitive A: ingest a harness-forwarded agent activity; the board auto-reacts.
|
|
2602
2629
|
async function handleAxActivityIngest(req: Request): Promise<Response> {
|
|
2603
2630
|
const body = await readJson(req);
|
|
2631
|
+
if (body === null) return malformedJsonResponse();
|
|
2604
2632
|
if (!isAxActivityKind(body.kind)) {
|
|
2605
2633
|
return responseJson({ ok: false, error: "activity requires a valid 'kind': one of tool-start, tool-result, failure, error, session-start, session-end, command, note." }, 400);
|
|
2606
2634
|
}
|
|
@@ -2647,6 +2675,7 @@ function handleGetAxSurfaceSnapshot(): Response {
|
|
|
2647
2675
|
// safe presentation query params like the current theme.
|
|
2648
2676
|
async function handleOpenExternalSurface(req: Request): Promise<Response> {
|
|
2649
2677
|
const body = await readJson(req);
|
|
2678
|
+
if (body === null) return malformedJsonResponse();
|
|
2650
2679
|
const nodeId = typeof body.nodeId === 'string' ? body.nodeId : '';
|
|
2651
2680
|
if (!nodeId) return responseJson({ ok: false, error: 'nodeId is required.' }, 400);
|
|
2652
2681
|
const node = canvasState.getNode(nodeId);
|
|
@@ -2667,6 +2696,7 @@ async function handleOpenExternalSurface(req: Request): Promise<Response> {
|
|
|
2667
2696
|
|
|
2668
2697
|
async function handleAxInteraction(req: Request): Promise<Response> {
|
|
2669
2698
|
const body = await readJson(req);
|
|
2699
|
+
if (body === null) return malformedJsonResponse();
|
|
2670
2700
|
const { result, events } = applyAxInteraction(canvasState, body, normalizeAxSource(body.source, 'api'));
|
|
2671
2701
|
for (const e of events) {
|
|
2672
2702
|
broadcastWorkbenchEvent(e.event, {
|
|
@@ -2708,6 +2738,7 @@ function handleAxPolicyGet(): Response {
|
|
|
2708
2738
|
|
|
2709
2739
|
async function handleAxStatePatch(req: Request): Promise<Response> {
|
|
2710
2740
|
const body = await readJson(req);
|
|
2741
|
+
if (body === null) return malformedJsonResponse();
|
|
2711
2742
|
if (!body.focus || typeof body.focus !== 'object' || Array.isArray(body.focus)) {
|
|
2712
2743
|
return responseJson({ ok: false, error: 'PATCH /api/canvas/ax currently requires a focus object.' }, 400);
|
|
2713
2744
|
}
|
|
@@ -3482,7 +3513,12 @@ export function startCanvasServer(options: CanvasServerOptions = {}): string | n
|
|
|
3482
3513
|
return typeof server.port === 'number' ? loopbackBaseUrl(server.port) : null;
|
|
3483
3514
|
}
|
|
3484
3515
|
|
|
3485
|
-
|
|
3516
|
+
// An explicit `options.workspaceRoot` wins. Otherwise honor PMX_CANVAS_WORKSPACE_ROOT
|
|
3517
|
+
// (Finding I escape hatch) before falling back to the launch cwd, so a host that
|
|
3518
|
+
// spawns from an incidental dir (e.g. ~/.copilot) can still pin the real project root
|
|
3519
|
+
// for the daemon it binds — not just for the MCP same-workspace lookup.
|
|
3520
|
+
const envWorkspaceRoot = process.env.PMX_CANVAS_WORKSPACE_ROOT?.trim();
|
|
3521
|
+
const workspaceRoot = options.workspaceRoot ?? (envWorkspaceRoot ? resolve(envWorkspaceRoot) : process.cwd());
|
|
3486
3522
|
activeWorkspaceRoot = normalizeWorkspaceRoot(workspaceRoot);
|
|
3487
3523
|
if (options.autoOpenBrowser !== undefined) {
|
|
3488
3524
|
primaryWorkbenchAutoOpenEnabled = options.autoOpenBrowser;
|
package/src/shared/ax-intent.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* agent is ABOUT to make (create / move / connect / remove / edit) so the canvas
|
|
6
6
|
* can paint a faint placeholder before the real mutation lands. Like a
|
|
7
7
|
* multiplayer cursor, it auto-expires, is count-capped, and never enters
|
|
8
|
-
* `
|
|
8
|
+
* `canvas_query` (`layout` action), `state.json`, or snapshots.
|
|
9
9
|
*
|
|
10
10
|
* This module is import-shared by the server (IntentRegistry + the intent ops)
|
|
11
11
|
* and the client (intent-store + IntentLayer); it must stay free of any
|