pmx-canvas 0.4.4 → 0.4.6
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 +108 -0
- package/Readme.md +52 -4
- package/dist/canvas/global.css +115 -11
- package/dist/canvas/index.js +93 -72
- package/dist/canvas/mermaid-entry.js +3563 -0
- package/dist/canvas/surface-theme.css +2 -2
- package/dist/json-render/index.css +1 -1
- package/dist/types/cli/agent.d.ts +1 -0
- package/dist/types/cli/commands/skills.d.ts +1 -1
- package/dist/types/cli/commands/smoke.d.ts +1 -0
- package/dist/types/client/mermaid-entry.d.ts +11 -0
- package/dist/types/client/nodes/DiffNode.d.ts +20 -0
- package/dist/types/client/nodes/ExtAppFrame.d.ts +0 -2
- package/dist/types/client/nodes/MermaidNode.d.ts +12 -0
- package/dist/types/client/state/intent-store.d.ts +6 -0
- package/dist/types/client/types.d.ts +2 -2
- package/dist/types/server/ax-state-manager.d.ts +5 -0
- package/dist/types/server/ax-state.d.ts +9 -3
- package/dist/types/server/canvas-db.d.ts +3 -2
- package/dist/types/server/canvas-provenance.d.ts +1 -1
- package/dist/types/server/canvas-state.d.ts +22 -1
- package/dist/types/server/canvas-validation.d.ts +34 -1
- package/dist/types/server/html-surface.d.ts +8 -0
- package/dist/types/server/index.d.ts +5 -0
- package/dist/types/server/operations/ops/ax-shared.d.ts +4 -1
- package/dist/types/server/operations/ops/nodes.d.ts +1 -1
- package/dist/types/server/operations/registry.d.ts +9 -1
- package/dist/types/server/workboard.d.ts +46 -0
- package/dist/types/shared/ax-intent.d.ts +6 -0
- package/dist/types/shared/ax-surface-protocol.d.ts +7 -0
- package/docs/cli.md +12 -0
- package/docs/environment.md +1 -0
- package/docs/http-api.md +12 -0
- package/docs/mcp.md +1 -1
- package/docs/node-types.md +2 -0
- package/docs/screenshot.png +0 -0
- package/docs/screenshots/amp-orb-portal.png +0 -0
- package/docs/screenshots/claude-code-desktop.png +0 -0
- package/docs/screenshots/codex-app.png +0 -0
- package/docs/screenshots/github-copilot-app.png +0 -0
- package/package.json +4 -3
- package/skills/pmx-canvas/SKILL.md +71 -24
- package/skills/pmx-canvas-orchestration/SKILL.md +82 -0
- package/skills/pmx-canvas-testing/SKILL.md +3 -0
- package/src/cli/agent.ts +2 -0
- package/src/cli/commands/ax.ts +28 -3
- package/src/cli/commands/skills.ts +1 -1
- package/src/cli/commands/smoke.ts +212 -0
- package/src/cli/index.ts +15 -2
- package/src/cli/shared.ts +1 -0
- package/src/client/canvas/CanvasNode.tsx +12 -1
- package/src/client/canvas/CanvasViewport.tsx +6 -0
- package/src/client/canvas/ExpandedNodeOverlay.tsx +10 -0
- package/src/client/canvas/IntentLayer.tsx +8 -6
- package/src/client/canvas/Minimap.tsx +2 -0
- package/src/client/canvas/auto-fit.ts +2 -0
- package/src/client/icons.tsx +4 -0
- package/src/client/mermaid-entry.ts +76 -0
- package/src/client/nodes/DiffNode.tsx +114 -0
- package/src/client/nodes/ExtAppFrame.tsx +259 -84
- package/src/client/nodes/MermaidNode.tsx +81 -0
- package/src/client/state/intent-bridge.ts +12 -3
- package/src/client/state/intent-store.ts +47 -2
- package/src/client/state/node-factory.ts +2 -0
- package/src/client/state/sse-bridge.ts +28 -0
- package/src/client/theme/global.css +115 -11
- package/src/client/theme/surface-theme.css +2 -2
- package/src/client/types.ts +6 -0
- package/src/mcp/server.ts +22 -6
- package/src/server/ax-interaction.ts +6 -0
- package/src/server/ax-state-manager.ts +27 -6
- package/src/server/ax-state.ts +36 -3
- package/src/server/canvas-db.ts +83 -60
- package/src/server/canvas-operations.ts +39 -9
- package/src/server/canvas-provenance.ts +2 -0
- package/src/server/canvas-schema.ts +33 -0
- package/src/server/canvas-state.ts +78 -6
- package/src/server/canvas-validation.ts +79 -0
- package/src/server/html-surface.ts +11 -0
- package/src/server/index.ts +25 -6
- package/src/server/intent-registry.ts +2 -0
- package/src/server/operations/composites.ts +3 -2
- package/src/server/operations/http.ts +5 -1
- package/src/server/operations/ops/ax-shared.ts +20 -1
- package/src/server/operations/ops/ax-state.ts +4 -7
- package/src/server/operations/ops/ax-timeline.ts +26 -3
- package/src/server/operations/ops/ax-work.ts +27 -1
- package/src/server/operations/ops/batch.ts +11 -1
- package/src/server/operations/ops/json-render.ts +44 -0
- package/src/server/operations/ops/nodes.ts +24 -2
- package/src/server/operations/ops/viewport.ts +8 -1
- package/src/server/operations/registry.ts +122 -1
- package/src/server/server.ts +58 -3
- package/src/server/workboard.ts +159 -0
- package/src/shared/ax-intent.ts +6 -0
- package/src/shared/ax-surface-protocol.ts +8 -0
- package/src/shared/surface.ts +2 -0
- package/src/shared/themes.ts +1 -1
package/src/server/index.ts
CHANGED
|
@@ -542,15 +542,22 @@ export class PmxCanvas extends EventEmitter {
|
|
|
542
542
|
nodeIds?: string[];
|
|
543
543
|
data?: Record<string, unknown> | null;
|
|
544
544
|
},
|
|
545
|
-
options?: { source?: PmxAxSource },
|
|
545
|
+
options?: { source?: PmxAxSource; agentId?: string | null },
|
|
546
546
|
): PmxAxEvent {
|
|
547
|
-
const event = canvasState.recordAxEvent(input, { source: options?.source ?? 'sdk' });
|
|
547
|
+
const event = canvasState.recordAxEvent(input, { source: options?.source ?? 'sdk', agentId: options?.agentId });
|
|
548
548
|
emitPrimaryWorkbenchEvent('ax-event-created', { event });
|
|
549
549
|
return event;
|
|
550
550
|
}
|
|
551
551
|
|
|
552
|
-
sendSteering(
|
|
553
|
-
|
|
552
|
+
sendSteering(
|
|
553
|
+
message: string,
|
|
554
|
+
options?: { source?: PmxAxSource; agentId?: string | null; target?: string | null },
|
|
555
|
+
): PmxAxSteeringMessage {
|
|
556
|
+
const steering = canvasState.recordSteeringMessage(message, {
|
|
557
|
+
source: options?.source ?? 'sdk',
|
|
558
|
+
agentId: options?.agentId,
|
|
559
|
+
target: options?.target,
|
|
560
|
+
});
|
|
554
561
|
emitPrimaryWorkbenchEvent('ax-event-created', { steering });
|
|
555
562
|
return steering;
|
|
556
563
|
}
|
|
@@ -604,7 +611,13 @@ export class PmxCanvas extends EventEmitter {
|
|
|
604
611
|
}
|
|
605
612
|
|
|
606
613
|
addWorkItem(
|
|
607
|
-
input: {
|
|
614
|
+
input: {
|
|
615
|
+
title: string;
|
|
616
|
+
status?: PmxAxWorkItemStatus;
|
|
617
|
+
detail?: string | null;
|
|
618
|
+
nodeIds?: string[];
|
|
619
|
+
agentId?: string | null;
|
|
620
|
+
},
|
|
608
621
|
options?: { source?: PmxAxSource },
|
|
609
622
|
): PmxAxWorkItem {
|
|
610
623
|
const workItem = canvasState.addWorkItem(input, { source: options?.source ?? 'sdk' });
|
|
@@ -614,7 +627,13 @@ export class PmxCanvas extends EventEmitter {
|
|
|
614
627
|
|
|
615
628
|
updateWorkItem(
|
|
616
629
|
id: string,
|
|
617
|
-
patch: {
|
|
630
|
+
patch: {
|
|
631
|
+
title?: string;
|
|
632
|
+
status?: PmxAxWorkItemStatus;
|
|
633
|
+
detail?: string | null;
|
|
634
|
+
nodeIds?: string[];
|
|
635
|
+
agentId?: string | null;
|
|
636
|
+
},
|
|
618
637
|
options?: { source?: PmxAxSource },
|
|
619
638
|
): PmxAxWorkItem | null {
|
|
620
639
|
const workItem = canvasState.updateWorkItem(id, patch, { source: options?.source ?? 'sdk' });
|
|
@@ -50,6 +50,7 @@ const intentSignalSchema = z.looseObject({
|
|
|
50
50
|
seq: z.number().int().min(0).max(9999).optional(),
|
|
51
51
|
ttlMs: z.number().positive().max(MAX_INTENT_TTL_MS).optional(),
|
|
52
52
|
source: z.string().max(60).optional(),
|
|
53
|
+
auto: z.boolean().optional(),
|
|
53
54
|
});
|
|
54
55
|
|
|
55
56
|
const intentUpdateSchema = z.looseObject({
|
|
@@ -146,6 +147,7 @@ export class IntentRegistry {
|
|
|
146
147
|
...(typeof input.confidence === 'number' ? { confidence: input.confidence } : {}),
|
|
147
148
|
...(typeof input.seq === 'number' ? { seq: input.seq } : {}),
|
|
148
149
|
...(input.source ? { source: input.source } : {}),
|
|
150
|
+
...(input.auto === true ? { auto: true } : {}),
|
|
149
151
|
createdAt: existing?.createdAt ?? now,
|
|
150
152
|
expiresAt: now + ttl,
|
|
151
153
|
};
|
|
@@ -156,14 +156,15 @@ export const compositeToolDefinitions: CompositeToolDefinition[] = [
|
|
|
156
156
|
{
|
|
157
157
|
toolName: 'canvas_render',
|
|
158
158
|
description:
|
|
159
|
-
'Spec-driven content: discover the schema, validate a payload, or create a json-render / graph node. Action "describe-schema" returns the json-render component catalog + create schemas; "validate" checks a spec or graph payload without creating a node; "add-json-render" creates a json-render node from a complete spec; "stream-json-render" progressively builds one from SpecStream patches (omit nodeId to create, pass it back to append, done=true to finish); "add-graph" creates a chart node (line, bar, pie, area, scatter, radar, stacked-bar, composed, sparkline, dot-plot, bullet, slopegraph).',
|
|
160
|
-
actionSummary: 'describe-schema | validate | add-json-render | stream-json-render | add-graph',
|
|
159
|
+
'Spec-driven content: discover the schema, validate a payload, or create a json-render / graph node. Action "describe-schema" returns the json-render component catalog + create schemas; "validate" checks a spec or graph payload without creating a node; "add-json-render" creates a json-render node from a complete spec; "stream-json-render" progressively builds one from SpecStream patches (omit nodeId to create, pass it back to append, done=true to finish); "add-graph" creates a chart node (line, bar, pie, area, scatter, radar, stacked-bar, composed, sparkline, dot-plot, bullet, slopegraph); "workboard" materializes a live AX work-item board (a json-render node with a column per status) that auto-refreshes whenever work items change — idempotent, rebuilding the existing board in place.',
|
|
160
|
+
actionSummary: 'describe-schema | validate | add-json-render | stream-json-render | add-graph | workboard',
|
|
161
161
|
actions: {
|
|
162
162
|
'describe-schema': 'schema.describe',
|
|
163
163
|
validate: 'spec.validate',
|
|
164
164
|
'add-json-render': 'jsonrender.add',
|
|
165
165
|
'stream-json-render': 'jsonrender.stream',
|
|
166
166
|
'add-graph': 'graph.add',
|
|
167
|
+
workboard: 'render.workboard',
|
|
167
168
|
},
|
|
168
169
|
},
|
|
169
170
|
{
|
|
@@ -84,7 +84,11 @@ export async function dispatchOperationRoute(req: Request, url: URL): Promise<Re
|
|
|
84
84
|
const input = route.readInput
|
|
85
85
|
? await route.readInput(req, params, url)
|
|
86
86
|
: await defaultReadInput(req, params, url);
|
|
87
|
-
|
|
87
|
+
// The workbench marks its own HTTP calls: a human dragging/editing in
|
|
88
|
+
// the browser is not agent activity, so it never synthesizes an
|
|
89
|
+
// auto-ghost. Everything else (CLI, scripts, adapters) counts as agent.
|
|
90
|
+
const fromWorkbench = req.headers.get('x-pmx-workbench') === '1';
|
|
91
|
+
const result = await executeOperation(op.name, input, { suppressAutoGhost: fromWorkbench });
|
|
88
92
|
return responseJson(result, route.status ? route.status(result) : 200);
|
|
89
93
|
} catch (error) {
|
|
90
94
|
if (error instanceof OperationError) {
|
|
@@ -12,11 +12,30 @@ import { z } from 'zod';
|
|
|
12
12
|
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
13
13
|
import type { PmxAxSource } from '../../ax-state.js';
|
|
14
14
|
|
|
15
|
-
export const AX_SOURCES = [
|
|
15
|
+
export const AX_SOURCES = [
|
|
16
|
+
'agent',
|
|
17
|
+
'amp',
|
|
18
|
+
'api',
|
|
19
|
+
'browser',
|
|
20
|
+
'cli',
|
|
21
|
+
'codex',
|
|
22
|
+
'copilot',
|
|
23
|
+
'mcp',
|
|
24
|
+
'sdk',
|
|
25
|
+
'system',
|
|
26
|
+
] as const;
|
|
16
27
|
|
|
17
28
|
/** Zod schema for the optional `source` field shared by AX MCP tool shapes. */
|
|
18
29
|
export const AX_SOURCE_SHAPE = z.enum(AX_SOURCES).optional().describe('Optional host/source label. Defaults to mcp.');
|
|
19
30
|
|
|
31
|
+
/** Zod schema for the optional `agentId` field shared by AX MCP tool shapes. */
|
|
32
|
+
export const AX_AGENT_ID_SHAPE = z
|
|
33
|
+
.string()
|
|
34
|
+
.min(1)
|
|
35
|
+
.max(80)
|
|
36
|
+
.optional()
|
|
37
|
+
.describe('Per-agent identity within the host (orchestration); source stays the host label.');
|
|
38
|
+
|
|
20
39
|
/** An absent or unrecognized source falls back to the per-surface default. */
|
|
21
40
|
export function normalizeAxSource(value: unknown, fallback: PmxAxSource): PmxAxSource {
|
|
22
41
|
return (AX_SOURCES as readonly string[]).includes(value as string) ? (value as PmxAxSource) : fallback;
|
|
@@ -27,7 +27,7 @@ import { buildCanvasAxContext } from '../../ax-context.js';
|
|
|
27
27
|
import type { PmxAxPolicy } from '../../ax-state.js';
|
|
28
28
|
import { defineOperation, type Operation } from '../types.js';
|
|
29
29
|
import { isRecord } from './nodes.js';
|
|
30
|
-
import { normalizeAxNodeIds, normalizeAxSource } from './ax-shared.js';
|
|
30
|
+
import { AX_SOURCES, normalizeAxNodeIds, normalizeAxSource } from './ax-shared.js';
|
|
31
31
|
|
|
32
32
|
// ── ax.get (canvas_get_ax) ────────────────────────────────────
|
|
33
33
|
|
|
@@ -112,7 +112,7 @@ const axFocusSetOperation = defineOperation<z.infer<typeof axFocusSetSchema>, Re
|
|
|
112
112
|
extraShape: {
|
|
113
113
|
nodeIds: z.array(z.string()).describe('Node IDs to place in the AX focus field. Missing nodes are ignored.'),
|
|
114
114
|
source: z
|
|
115
|
-
.enum(
|
|
115
|
+
.enum(AX_SOURCES)
|
|
116
116
|
.optional()
|
|
117
117
|
.describe(
|
|
118
118
|
'Optional host/source label for adapter-originated focus. Defaults to mcp. Use codex from the Codex app adapter.',
|
|
@@ -167,7 +167,7 @@ const axPolicySetOperation = defineOperation<z.infer<typeof axPolicySetSchema>,
|
|
|
167
167
|
})
|
|
168
168
|
.optional(),
|
|
169
169
|
prompt: z.object({ systemAppend: z.string().optional(), mode: z.string().optional() }).optional(),
|
|
170
|
-
source: z.enum(
|
|
170
|
+
source: z.enum(AX_SOURCES).optional(),
|
|
171
171
|
},
|
|
172
172
|
// Legacy MCP tool forwarded only the present tools/prompt fields (source split out).
|
|
173
173
|
buildInput: (input) => ({
|
|
@@ -236,10 +236,7 @@ const axHostCapabilityReportOperation = defineOperation<
|
|
|
236
236
|
files: z.boolean().optional(),
|
|
237
237
|
uiPrompts: z.boolean().optional(),
|
|
238
238
|
raw: z.record(z.string(), z.unknown()).optional().describe('Optional raw capability payload for diagnostics.'),
|
|
239
|
-
source: z
|
|
240
|
-
.enum(['agent', 'api', 'browser', 'cli', 'codex', 'copilot', 'mcp', 'sdk', 'system'])
|
|
241
|
-
.optional()
|
|
242
|
-
.describe('Optional host/source label. Defaults to mcp.'),
|
|
239
|
+
source: z.enum(AX_SOURCES).optional().describe('Optional host/source label. Defaults to mcp.'),
|
|
243
240
|
},
|
|
244
241
|
buildInput: (input) => ({ ...input, source: normalizeAxSource(input.source, 'mcp') }),
|
|
245
242
|
formatResult: (result) => {
|
|
@@ -51,7 +51,13 @@ import { buildPendingAxActivity, isAxEventKind, isAxEvidenceKind } from '../../a
|
|
|
51
51
|
import type { PmxAxEventKind, PmxAxEvidenceKind } from '../../ax-state.js';
|
|
52
52
|
import { defineOperation, OperationError, type Operation } from '../types.js';
|
|
53
53
|
import { isRecord } from './nodes.js';
|
|
54
|
-
import {
|
|
54
|
+
import {
|
|
55
|
+
AX_AGENT_ID_SHAPE,
|
|
56
|
+
AX_SOURCE_SHAPE,
|
|
57
|
+
axJsonResult,
|
|
58
|
+
normalizeAxNodeIds,
|
|
59
|
+
normalizeAxSource,
|
|
60
|
+
} from './ax-shared.js';
|
|
55
61
|
|
|
56
62
|
const AX_EVENT_KINDS = [
|
|
57
63
|
'prompt',
|
|
@@ -73,6 +79,7 @@ const axEventRecordShape = {
|
|
|
73
79
|
nodeIds: z.unknown().optional().describe('Optional node IDs this event relates to.'),
|
|
74
80
|
data: z.unknown().optional().describe('Optional structured data payload.'),
|
|
75
81
|
source: z.unknown().optional().describe('Optional host/source label. Defaults to mcp.'),
|
|
82
|
+
agentId: z.unknown().optional().describe('Optional per-agent identity within the host.'),
|
|
76
83
|
};
|
|
77
84
|
|
|
78
85
|
const axEventRecordSchema = z.looseObject(axEventRecordShape);
|
|
@@ -97,6 +104,7 @@ const axEventRecordOperation = defineOperation<z.infer<typeof axEventRecordSchem
|
|
|
97
104
|
nodeIds: z.array(z.string()).optional().describe('Optional node IDs this event relates to.'),
|
|
98
105
|
data: z.record(z.string(), z.unknown()).optional().describe('Optional structured data payload.'),
|
|
99
106
|
source: AX_SOURCE_SHAPE,
|
|
107
|
+
agentId: AX_AGENT_ID_SHAPE,
|
|
100
108
|
},
|
|
101
109
|
buildInput: (input) => ({ ...input, source: normalizeAxSource(input.source, 'mcp') }),
|
|
102
110
|
formatResult: axJsonResult,
|
|
@@ -113,7 +121,10 @@ const axEventRecordOperation = defineOperation<z.infer<typeof axEventRecordSchem
|
|
|
113
121
|
nodeIds: normalizeAxNodeIds(input.nodeIds),
|
|
114
122
|
data: isRecord(input.data) ? input.data : null,
|
|
115
123
|
},
|
|
116
|
-
{
|
|
124
|
+
{
|
|
125
|
+
source: normalizeAxSource(input.source, 'api'),
|
|
126
|
+
...(typeof input.agentId === 'string' ? { agentId: input.agentId } : {}),
|
|
127
|
+
},
|
|
117
128
|
);
|
|
118
129
|
ctx.emit('ax-event-created', { event });
|
|
119
130
|
return { ok: true, event } as unknown as Record<string, unknown>;
|
|
@@ -184,6 +195,8 @@ const axEvidenceAddOperation = defineOperation<z.infer<typeof axEvidenceAddSchem
|
|
|
184
195
|
const axSteerShape = {
|
|
185
196
|
message: z.unknown().optional().describe('The steering instruction to deliver to the active agent session.'),
|
|
186
197
|
source: z.unknown().optional().describe('Optional host/source label. Defaults to mcp.'),
|
|
198
|
+
agentId: z.unknown().optional().describe('Optional per-agent identity within the host.'),
|
|
199
|
+
target: z.unknown().optional().describe('Optional agentId/consumer this steer is addressed to. Omit to broadcast.'),
|
|
187
200
|
};
|
|
188
201
|
|
|
189
202
|
const axSteerSchema = z.looseObject(axSteerShape);
|
|
@@ -204,6 +217,13 @@ const axSteerOperation = defineOperation<z.infer<typeof axSteerSchema>, Record<s
|
|
|
204
217
|
extraShape: {
|
|
205
218
|
message: z.string().describe('The steering instruction to deliver to the active agent session.'),
|
|
206
219
|
source: AX_SOURCE_SHAPE,
|
|
220
|
+
agentId: AX_AGENT_ID_SHAPE,
|
|
221
|
+
target: z
|
|
222
|
+
.string()
|
|
223
|
+
.min(1)
|
|
224
|
+
.max(80)
|
|
225
|
+
.optional()
|
|
226
|
+
.describe('The agentId/consumer this steer is addressed to. Omit to broadcast to all consumers.'),
|
|
207
227
|
},
|
|
208
228
|
buildInput: (input) => ({ ...input, source: normalizeAxSource(input.source, 'mcp') }),
|
|
209
229
|
formatResult: axJsonResult,
|
|
@@ -214,6 +234,8 @@ const axSteerOperation = defineOperation<z.infer<typeof axSteerSchema>, Record<s
|
|
|
214
234
|
}
|
|
215
235
|
const steering = canvasState.recordSteeringMessage(input.message, {
|
|
216
236
|
source: normalizeAxSource(input.source, 'api'),
|
|
237
|
+
...(typeof input.agentId === 'string' ? { agentId: input.agentId } : {}),
|
|
238
|
+
...(typeof input.target === 'string' ? { target: input.target } : {}),
|
|
217
239
|
});
|
|
218
240
|
ctx.emit('ax-event-created', { steering });
|
|
219
241
|
return { ok: true, steering } as unknown as Record<string, unknown>;
|
|
@@ -328,7 +350,8 @@ const axDeliveryMarkOperation = defineOperation<z.infer<typeof axDeliveryMarkSch
|
|
|
328
350
|
},
|
|
329
351
|
mcp: {
|
|
330
352
|
toolName: 'canvas_mark_ax_delivery',
|
|
331
|
-
description:
|
|
353
|
+
description:
|
|
354
|
+
'Mark a PMX AX steering message as delivered so it is not handed out again. Compare-and-set: `delivered:true` only on the actual undelivered→delivered transition — a false means the id was unknown OR another consumer already marked it, so do NOT treat the message as your delivery.',
|
|
332
355
|
extraShape: {
|
|
333
356
|
id: z.string().describe('The steering message id to mark delivered.'),
|
|
334
357
|
},
|
|
@@ -57,7 +57,14 @@ import type {
|
|
|
57
57
|
} from '../../ax-state.js';
|
|
58
58
|
import { defineOperation, OperationError, type Operation } from '../types.js';
|
|
59
59
|
import { isRecord } from './nodes.js';
|
|
60
|
-
import {
|
|
60
|
+
import {
|
|
61
|
+
AX_AGENT_ID_SHAPE,
|
|
62
|
+
AX_SOURCE_SHAPE,
|
|
63
|
+
AX_SOURCES,
|
|
64
|
+
axJsonResult,
|
|
65
|
+
normalizeAxNodeIds,
|
|
66
|
+
normalizeAxSource,
|
|
67
|
+
} from './ax-shared.js';
|
|
61
68
|
|
|
62
69
|
const AX_WORK_ITEM_STATUSES = new Set(['todo', 'in-progress', 'blocked', 'done', 'cancelled']);
|
|
63
70
|
function normalizeAxWorkItemStatus(value: unknown): PmxAxWorkItemStatus | undefined {
|
|
@@ -98,6 +105,7 @@ const axWorkCreateShape = {
|
|
|
98
105
|
detail: z.unknown().optional().describe('Optional longer description.'),
|
|
99
106
|
nodeIds: z.unknown().optional().describe('Optional node IDs this work item is tied to.'),
|
|
100
107
|
source: z.unknown().optional().describe('Optional host/source label. Defaults to mcp.'),
|
|
108
|
+
agentId: z.unknown().optional().describe('Optional per-agent identity within the host.'),
|
|
101
109
|
};
|
|
102
110
|
|
|
103
111
|
const axWorkCreateSchema = z.looseObject(axWorkCreateShape);
|
|
@@ -124,6 +132,7 @@ const axWorkCreateOperation = defineOperation<z.infer<typeof axWorkCreateSchema>
|
|
|
124
132
|
detail: z.string().optional().describe('Optional longer description.'),
|
|
125
133
|
nodeIds: z.array(z.string()).optional().describe('Optional node IDs this work item is tied to.'),
|
|
126
134
|
source: AX_SOURCE_SHAPE,
|
|
135
|
+
agentId: AX_AGENT_ID_SHAPE,
|
|
127
136
|
},
|
|
128
137
|
buildInput: (input) => ({ ...input, source: normalizeAxSource(input.source, 'mcp') }),
|
|
129
138
|
formatResult: axJsonResult,
|
|
@@ -146,10 +155,18 @@ const axWorkCreateOperation = defineOperation<z.infer<typeof axWorkCreateSchema>
|
|
|
146
155
|
...(status ? { status } : {}),
|
|
147
156
|
...(typeof input.detail === 'string' ? { detail: input.detail } : {}),
|
|
148
157
|
...(Array.isArray(input.nodeIds) ? { nodeIds: normalizeAxNodeIds(input.nodeIds) } : {}),
|
|
158
|
+
...(typeof input.agentId === 'string' ? { agentId: input.agentId } : {}),
|
|
149
159
|
},
|
|
150
160
|
{ source: normalizeAxSource(input.source, 'api') },
|
|
151
161
|
);
|
|
152
162
|
ctx.emit('ax-state-changed', { workItem });
|
|
163
|
+
// The status mirror writes data.axWorkStatus onto linked nodes, but this
|
|
164
|
+
// op is mutates:false (AX writes must not spam layout frames) — so emit
|
|
165
|
+
// the ONE layout frame ourselves when links exist, or open boards never
|
|
166
|
+
// see the chip until an unrelated mutation happens to refresh them.
|
|
167
|
+
if (workItem.nodeIds.length > 0) {
|
|
168
|
+
ctx.emit('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
169
|
+
}
|
|
153
170
|
return { ok: true, workItem } as unknown as Record<string, unknown>;
|
|
154
171
|
},
|
|
155
172
|
});
|
|
@@ -163,6 +180,7 @@ const axWorkUpdateShape = {
|
|
|
163
180
|
detail: z.unknown().optional().describe('New detail text.'),
|
|
164
181
|
nodeIds: z.unknown().optional().describe('Replacement node IDs.'),
|
|
165
182
|
source: z.unknown().optional().describe('Optional host/source label. Defaults to mcp.'),
|
|
183
|
+
agentId: z.unknown().optional().describe('Optional per-agent identity within the host.'),
|
|
166
184
|
};
|
|
167
185
|
|
|
168
186
|
const axWorkUpdateSchema = z.looseObject(axWorkUpdateShape);
|
|
@@ -187,6 +205,7 @@ const axWorkUpdateOperation = defineOperation<z.infer<typeof axWorkUpdateSchema>
|
|
|
187
205
|
detail: z.string().optional().describe('New detail text.'),
|
|
188
206
|
nodeIds: z.array(z.string()).optional().describe('Replacement node IDs.'),
|
|
189
207
|
source: AX_SOURCE_SHAPE,
|
|
208
|
+
agentId: AX_AGENT_ID_SHAPE,
|
|
190
209
|
},
|
|
191
210
|
buildInput: (input) => ({ ...input, source: normalizeAxSource(input.source, 'mcp') }),
|
|
192
211
|
formatResult: axJsonResult,
|
|
@@ -207,11 +226,18 @@ const axWorkUpdateOperation = defineOperation<z.infer<typeof axWorkUpdateSchema>
|
|
|
207
226
|
...(status ? { status } : {}),
|
|
208
227
|
...(typeof input.detail === 'string' || input.detail === null ? { detail: input.detail as string | null } : {}),
|
|
209
228
|
...(Array.isArray(input.nodeIds) ? { nodeIds: normalizeAxNodeIds(input.nodeIds) } : {}),
|
|
229
|
+
...(typeof input.agentId === 'string' ? { agentId: input.agentId } : {}),
|
|
210
230
|
},
|
|
211
231
|
{ source: normalizeAxSource(input.source, 'api') },
|
|
212
232
|
);
|
|
213
233
|
if (!workItem) throw new OperationError('work item not found.', 404);
|
|
214
234
|
ctx.emit('ax-state-changed', { workItem });
|
|
235
|
+
// Same as create: the mirror touched node data only if the patch carried
|
|
236
|
+
// status or nodeIds — emit the layout frame those chips need (this op is
|
|
237
|
+
// mutates:false, so nothing else will).
|
|
238
|
+
if (input.status !== undefined || input.nodeIds !== undefined) {
|
|
239
|
+
ctx.emit('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
240
|
+
}
|
|
215
241
|
return { ok: true, workItem } as unknown as Record<string, unknown>;
|
|
216
242
|
},
|
|
217
243
|
});
|
|
@@ -238,7 +238,9 @@ async function runBatch(operations: BatchEntry[]): Promise<BatchEnvelope> {
|
|
|
238
238
|
continue;
|
|
239
239
|
}
|
|
240
240
|
const dispatch = resolveDispatch(operation.op, args);
|
|
241
|
-
|
|
241
|
+
// Batch churn is exempt from auto-ghosts (matches the skill's explicit
|
|
242
|
+
// ghost-cursor exemption for batch operations).
|
|
243
|
+
const raw = await executeOperation(dispatch.name, dispatch.args, { suppressAutoGhost: true });
|
|
242
244
|
const result = shapeBatchEntry(operation.op, raw);
|
|
243
245
|
results.push(result);
|
|
244
246
|
if (typeof operation.assign === 'string' && operation.assign.trim().length > 0) {
|
|
@@ -373,6 +375,14 @@ const batchOperation = defineOperation<z.infer<typeof batchSchema>, BatchEnvelop
|
|
|
373
375
|
// ctx.emit). After the loop completes, emit ONE final canvas-layout-update.
|
|
374
376
|
const envelope = await runWithSuppressedEmits(() => runBatch(operations));
|
|
375
377
|
ctx.emit('canvas-layout-update', { layout: canvasState.getLayout() });
|
|
378
|
+
// The suppression window also swallows the pins op's context-pins-changed
|
|
379
|
+
// frame, leaving the client's pin state (menus, indicators) stale until
|
|
380
|
+
// reload (0.4.5 report Finding X). If the batch touched pins, follow the
|
|
381
|
+
// layout frame with ONE final pins frame carrying the end state.
|
|
382
|
+
if (operations.some((entry) => entry.op.startsWith('pin.'))) {
|
|
383
|
+
const nodeIds = Array.from(canvasState.contextPinnedNodeIds);
|
|
384
|
+
ctx.emit('context-pins-changed', { count: nodeIds.length, nodeIds });
|
|
385
|
+
}
|
|
376
386
|
return envelope;
|
|
377
387
|
},
|
|
378
388
|
});
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
} from '../../canvas-operations.js';
|
|
40
40
|
import { describeCanvasSchema, validateStructuredCanvasPayload } from '../../canvas-schema.js';
|
|
41
41
|
import { isHtmlPrimitiveKind } from '../../html-primitives.js';
|
|
42
|
+
import { renderWorkboard } from '../../workboard.js';
|
|
42
43
|
import { defineOperation, OperationError, type Operation } from '../types.js';
|
|
43
44
|
import {
|
|
44
45
|
buildNodeResponse,
|
|
@@ -311,6 +312,48 @@ const jsonRenderStreamOperation = defineOperation<z.infer<typeof jsonRenderStrea
|
|
|
311
312
|
// Wire shape is `{ id, url, ok, applied, ... }` — handler output verbatim.
|
|
312
313
|
});
|
|
313
314
|
|
|
315
|
+
// ── render.workboard ──────────────────────────────────────────
|
|
316
|
+
|
|
317
|
+
const workboardRenderShape = {
|
|
318
|
+
intentId: z
|
|
319
|
+
.string()
|
|
320
|
+
.optional()
|
|
321
|
+
.catch(undefined)
|
|
322
|
+
.describe('Ghost intent id returned by canvas_intent signal. A vetoed or expired intent blocks this mutation.'),
|
|
323
|
+
x: z.number().optional().catch(undefined).describe('Optional X position (only used when creating the board)'),
|
|
324
|
+
y: z.number().optional().catch(undefined).describe('Optional Y position (only used when creating the board)'),
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
const workboardRenderSchema = z.looseObject(workboardRenderShape);
|
|
328
|
+
|
|
329
|
+
const workboardRenderOperation = defineOperation<
|
|
330
|
+
z.infer<typeof workboardRenderSchema>,
|
|
331
|
+
ReturnType<typeof renderWorkboard>
|
|
332
|
+
>({
|
|
333
|
+
name: 'render.workboard',
|
|
334
|
+
mutates: true,
|
|
335
|
+
input: workboardRenderSchema,
|
|
336
|
+
inputShape: workboardRenderShape,
|
|
337
|
+
http: {
|
|
338
|
+
method: 'POST',
|
|
339
|
+
path: '/api/canvas/workboard',
|
|
340
|
+
},
|
|
341
|
+
mcp: {
|
|
342
|
+
toolName: 'canvas_render_workboard',
|
|
343
|
+
description:
|
|
344
|
+
'Materialize a live work-item board: a json-render node with one column per status (todo → in-progress → blocked → done → cancelled) showing every AX work item as a card (title, agentId chip, detail). Idempotent: rebuilds the existing board node in place when one exists, otherwise creates it. The board auto-refreshes whenever work items change.',
|
|
345
|
+
},
|
|
346
|
+
handler: (input) => {
|
|
347
|
+
const body: Record<string, unknown> = input;
|
|
348
|
+
const x = pickFiniteNumber(body, 'x');
|
|
349
|
+
const y = pickFiniteNumber(body, 'y');
|
|
350
|
+
return renderWorkboard({
|
|
351
|
+
...(x !== undefined ? { x } : {}),
|
|
352
|
+
...(y !== undefined ? { y } : {}),
|
|
353
|
+
});
|
|
354
|
+
},
|
|
355
|
+
});
|
|
356
|
+
|
|
314
357
|
// ── graph.add ─────────────────────────────────────────────────
|
|
315
358
|
|
|
316
359
|
const graphAddShape = {
|
|
@@ -829,6 +872,7 @@ const specValidateOperation = defineOperation<z.infer<typeof specValidateSchema>
|
|
|
829
872
|
export const jsonRenderOperations: Operation[] = [
|
|
830
873
|
jsonRenderAddOperation,
|
|
831
874
|
jsonRenderStreamOperation,
|
|
875
|
+
workboardRenderOperation,
|
|
832
876
|
graphAddOperation,
|
|
833
877
|
schemaDescribeOperation,
|
|
834
878
|
specValidateOperation,
|
|
@@ -55,6 +55,8 @@ export const NODE_TYPES = [
|
|
|
55
55
|
'ledger',
|
|
56
56
|
'trace',
|
|
57
57
|
'file',
|
|
58
|
+
'diff',
|
|
59
|
+
'mermaid',
|
|
58
60
|
'image',
|
|
59
61
|
'mcp-app',
|
|
60
62
|
'webpage',
|
|
@@ -69,6 +71,15 @@ export function defaultNodeSize(type: string): { width: number; height: number }
|
|
|
69
71
|
switch (type) {
|
|
70
72
|
case 'html':
|
|
71
73
|
return { width: 720, height: 640 };
|
|
74
|
+
case 'file':
|
|
75
|
+
// Code preview needs real width and enough height for the header bar +
|
|
76
|
+
// a useful snippet — the generic default rendered as a content-less bar
|
|
77
|
+
// (0.4.4 portal feedback).
|
|
78
|
+
return { width: 520, height: 360 };
|
|
79
|
+
case 'diff':
|
|
80
|
+
return { width: 640, height: 420 };
|
|
81
|
+
case 'mermaid':
|
|
82
|
+
return { width: 640, height: 460 };
|
|
72
83
|
case 'markdown':
|
|
73
84
|
return MARKDOWN_NODE_DEFAULT_SIZE;
|
|
74
85
|
case 'mcp-app':
|
|
@@ -415,7 +426,16 @@ export function createBasicCanvasNode(
|
|
|
415
426
|
try {
|
|
416
427
|
const { node, needsCodeGraphRecompute } = addCanvasNode({
|
|
417
428
|
type: type as CanvasNodeState['type'],
|
|
418
|
-
...(typeof body.title === 'string'
|
|
429
|
+
...(typeof body.title === 'string'
|
|
430
|
+
? { title: body.title }
|
|
431
|
+
: // Untitled file nodes get the filename — otherwise the title bar
|
|
432
|
+
// reads as a bare "FILE" chip with nothing identifying the file.
|
|
433
|
+
type === 'file' && typeof content === 'string' && content.trim()
|
|
434
|
+
? // Separator-agnostic: Windows paths use backslashes (0.3.2's
|
|
435
|
+
// basename fix class — a '/'-only split makes the whole path
|
|
436
|
+
// the title there).
|
|
437
|
+
{ title: content.trim().split(/[\\/]/).pop() }
|
|
438
|
+
: {}),
|
|
419
439
|
...(typeof content === 'string' ? { content } : {}),
|
|
420
440
|
...(htmlMergedData && Object.keys(htmlMergedData).length > 0 ? { data: htmlMergedData } : {}),
|
|
421
441
|
...(type === 'trace' && typeof body.toolName === 'string' ? { toolName: body.toolName } : {}),
|
|
@@ -769,7 +789,7 @@ const nodeAddOperation = defineOperation<z.infer<typeof nodeAddSchema>, NodeAddR
|
|
|
769
789
|
mcp: {
|
|
770
790
|
toolName: 'canvas_add_node',
|
|
771
791
|
description:
|
|
772
|
-
'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.',
|
|
792
|
+
'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, diff, mermaid, 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.',
|
|
773
793
|
extraShape: {
|
|
774
794
|
type: z
|
|
775
795
|
.enum([
|
|
@@ -779,6 +799,8 @@ const nodeAddOperation = defineOperation<z.infer<typeof nodeAddSchema>, NodeAddR
|
|
|
779
799
|
'ledger',
|
|
780
800
|
'trace',
|
|
781
801
|
'file',
|
|
802
|
+
'diff',
|
|
803
|
+
'mermaid',
|
|
782
804
|
'image',
|
|
783
805
|
'webpage',
|
|
784
806
|
'mcp-app',
|
|
@@ -121,7 +121,14 @@ const focusOperation = defineOperation<z.infer<typeof focusSchema>, Record<strin
|
|
|
121
121
|
if (!node) throw new OperationError(`Node "${nodeId}" not found.`, 404);
|
|
122
122
|
const noPan = body.noPan === true;
|
|
123
123
|
if (!noPan) {
|
|
124
|
-
|
|
124
|
+
// The viewport is a SCREEN-space translate: screen = world * scale + v
|
|
125
|
+
// (CanvasViewport's matrix(scale,0,0,scale,x,y)), the same convention
|
|
126
|
+
// fitCanvasView and the client's focusNode use. So park the node's
|
|
127
|
+
// top-left at a fixed screen-space margin — 64/96 screen px, which
|
|
128
|
+
// clears the 44px floating toolbar at ANY zoom (0.4.5 report Finding Z:
|
|
129
|
+
// the old world-space margin shrank below the toolbar at low scales).
|
|
130
|
+
const scale = canvasState.viewport.scale || 1;
|
|
131
|
+
canvasState.setViewport({ x: 64 - node.position.x * scale, y: 96 - node.position.y * scale });
|
|
125
132
|
} else {
|
|
126
133
|
const maxZ = canvasState.getLayout().nodes.reduce((max, layoutNode) => Math.max(max, layoutNode.zIndex), 0);
|
|
127
134
|
canvasState.updateNode(nodeId, { zIndex: maxZ + 1 });
|