pmx-canvas 0.1.29 → 0.1.30
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 +161 -0
- package/Readme.md +20 -10
- package/dist/canvas/global.css +13 -0
- package/dist/canvas/index.js +80 -163
- package/dist/canvas/surface-theme.css +142 -0
- package/dist/json-render/index.js +103 -103
- package/dist/types/client/nodes/HtmlNode.d.ts +0 -7
- package/dist/types/client/nodes/ax-node-actions.d.ts +18 -0
- package/dist/types/client/nodes/surface-url.d.ts +22 -0
- package/dist/types/client/state/attention-bridge.d.ts +3 -0
- package/dist/types/client/state/intent-bridge.d.ts +17 -0
- package/dist/types/json-render/renderer/index.d.ts +2 -0
- package/dist/types/json-render/schema.d.ts +2 -0
- package/dist/types/json-render/server.d.ts +2 -0
- package/dist/types/mcp/canvas-access.d.ts +47 -0
- package/dist/types/server/ax-interaction.d.ts +210 -0
- package/dist/types/server/ax-state.d.ts +67 -1
- package/dist/types/server/canvas-db.d.ts +4 -0
- package/dist/types/server/canvas-serialization.d.ts +2 -0
- package/dist/types/server/canvas-state.d.ts +47 -2
- package/dist/types/server/html-surface.d.ts +40 -0
- package/dist/types/server/index.d.ts +50 -2
- package/dist/types/server/mutation-history.d.ts +1 -1
- package/dist/types/server/placement.d.ts +1 -1
- package/dist/types/shared/surface.d.ts +19 -0
- package/docs/cli.md +30 -0
- package/docs/http-api.md +55 -0
- package/docs/mcp.md +40 -2
- package/docs/node-types.md +26 -0
- package/docs/plans/plan-004-pmx-ax-primitives.md +623 -394
- package/docs/sdk.md +20 -0
- package/package.json +2 -2
- package/skills/pmx-canvas/SKILL.md +107 -9
- package/src/cli/agent.ts +177 -0
- package/src/client/canvas/CanvasNode.tsx +8 -4
- package/src/client/canvas/ExpandedNodeOverlay.tsx +12 -0
- package/src/client/nodes/ContextNode.tsx +17 -0
- package/src/client/nodes/ExtAppFrame.tsx +40 -3
- package/src/client/nodes/FileNode.tsx +26 -0
- package/src/client/nodes/HtmlNode.tsx +60 -188
- package/src/client/nodes/McpAppNode.tsx +47 -2
- package/src/client/nodes/StatusNode.tsx +20 -0
- package/src/client/nodes/ax-node-actions.ts +39 -0
- package/src/client/nodes/surface-url.ts +48 -0
- package/src/client/state/attention-bridge.ts +5 -0
- package/src/client/state/intent-bridge.ts +33 -0
- package/src/client/theme/global.css +13 -0
- package/src/client/theme/surface-theme.css +142 -0
- package/src/json-render/renderer/index.tsx +31 -0
- package/src/json-render/schema.ts +4 -0
- package/src/json-render/server.ts +13 -0
- package/src/mcp/canvas-access.ts +195 -0
- package/src/mcp/server.ts +232 -2
- package/src/server/ax-context.ts +3 -0
- package/src/server/ax-interaction.ts +549 -0
- package/src/server/ax-state.ts +188 -2
- package/src/server/canvas-db.ts +20 -0
- package/src/server/canvas-operations.ts +11 -0
- package/src/server/canvas-serialization.ts +9 -0
- package/src/server/canvas-state.ts +177 -16
- package/src/server/html-surface.ts +170 -0
- package/src/server/index.ts +98 -0
- package/src/server/mutation-history.ts +5 -0
- package/src/server/placement.ts +5 -1
- package/src/server/server.ts +305 -0
- package/src/shared/surface.ts +38 -0
package/src/server/ax-state.ts
CHANGED
|
@@ -13,7 +13,7 @@ export interface PmxAxFocusState {
|
|
|
13
13
|
// ── New enums ──────────────────────────────────────────────────────
|
|
14
14
|
export type PmxAxEventKind =
|
|
15
15
|
| 'prompt' | 'assistant-message' | 'tool-start' | 'tool-result'
|
|
16
|
-
| 'failure' | 'approval' | 'steering';
|
|
16
|
+
| 'failure' | 'approval' | 'steering' | 'command';
|
|
17
17
|
export type PmxAxEvidenceKind =
|
|
18
18
|
| 'logs' | 'tool-result' | 'screenshot' | 'file' | 'diff' | 'test-output';
|
|
19
19
|
export type PmxAxWorkItemStatus = 'todo' | 'in-progress' | 'blocked' | 'done' | 'cancelled';
|
|
@@ -141,6 +141,9 @@ export interface PmxAxState {
|
|
|
141
141
|
workItems: PmxAxWorkItem[];
|
|
142
142
|
approvalGates: PmxAxApprovalGate[];
|
|
143
143
|
reviewAnnotations: PmxAxReviewAnnotation[];
|
|
144
|
+
elicitations: PmxAxElicitation[];
|
|
145
|
+
modeRequests: PmxAxModeRequest[];
|
|
146
|
+
policy: PmxAxPolicy;
|
|
144
147
|
}
|
|
145
148
|
|
|
146
149
|
export interface PmxAxPinnedContext {
|
|
@@ -166,6 +169,9 @@ export interface PmxAxContext {
|
|
|
166
169
|
workItems: PmxAxWorkItem[];
|
|
167
170
|
approvalGates: PmxAxApprovalGate[];
|
|
168
171
|
reviewAnnotations: PmxAxReviewAnnotation[];
|
|
172
|
+
elicitations: PmxAxElicitation[];
|
|
173
|
+
modeRequests: PmxAxModeRequest[];
|
|
174
|
+
policy: PmxAxPolicy;
|
|
169
175
|
timeline: PmxAxTimelineSummary;
|
|
170
176
|
host: PmxAxHostCapability | null;
|
|
171
177
|
}
|
|
@@ -199,7 +205,69 @@ function normalizeNodeIds(value: unknown, validNodeIds?: Set<string>): string[]
|
|
|
199
205
|
return ids;
|
|
200
206
|
}
|
|
201
207
|
|
|
202
|
-
const AX_EVENT_KINDS = new Set<PmxAxEventKind>(['prompt', 'assistant-message', 'tool-start', 'tool-result', 'failure', 'approval', 'steering']);
|
|
208
|
+
const AX_EVENT_KINDS = new Set<PmxAxEventKind>(['prompt', 'assistant-message', 'tool-start', 'tool-result', 'failure', 'approval', 'steering', 'command']);
|
|
209
|
+
|
|
210
|
+
// ── Command registry (plan-004 Phase 5) ────────────────────────
|
|
211
|
+
// Named, registry-gated PMX intents — NOT arbitrary execution. Invoking a command
|
|
212
|
+
// records a timeline event a host/agent can observe and act on; unknown names are
|
|
213
|
+
// rejected. Seeded with conservative built-ins; project-extensible later.
|
|
214
|
+
export interface PmxAxCommandDescriptor {
|
|
215
|
+
name: string;
|
|
216
|
+
description: string;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export const AX_COMMAND_REGISTRY: Record<string, PmxAxCommandDescriptor> = {
|
|
220
|
+
'pmx.plan': { name: 'pmx.plan', description: 'Enter planning: outline the approach before executing.' },
|
|
221
|
+
'pmx.execute': { name: 'pmx.execute', description: 'Proceed from plan to execution.' },
|
|
222
|
+
'pmx.promote-context': { name: 'pmx.promote-context', description: 'Promote focused/pinned nodes into durable agent context.' },
|
|
223
|
+
'pmx.summarize': { name: 'pmx.summarize', description: 'Summarize the current canvas/work state.' },
|
|
224
|
+
'pmx.review': { name: 'pmx.review', description: 'Start a review pass over the focused nodes.' },
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
export function isAxCommand(name: unknown): name is string {
|
|
228
|
+
return typeof name === 'string' && Object.prototype.hasOwnProperty.call(AX_COMMAND_REGISTRY, name);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function listAxCommands(): PmxAxCommandDescriptor[] {
|
|
232
|
+
return Object.values(AX_COMMAND_REGISTRY);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ── Tool/prompt policy (plan-004 Phase 5) ──────────────────────
|
|
236
|
+
// Declarative policy PMX stores and exposes; host adapters READ and enforce it.
|
|
237
|
+
// PMX does not mutate host prompts or block tools itself. Default = empty (no
|
|
238
|
+
// restrictions) — that is the conservative answer to "how much by default".
|
|
239
|
+
export interface PmxAxPolicy {
|
|
240
|
+
tools: { allowed: string[]; excluded: string[]; approvalRequired: string[] };
|
|
241
|
+
prompt: { systemAppend: string | null; mode: string | null };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function createEmptyAxPolicy(): PmxAxPolicy {
|
|
245
|
+
return {
|
|
246
|
+
tools: { allowed: [], excluded: [], approvalRequired: [] },
|
|
247
|
+
prompt: { systemAppend: null, mode: null },
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function normalizeStringArray(value: unknown): string[] {
|
|
252
|
+
return Array.isArray(value) ? value.filter((v): v is string => typeof v === 'string') : [];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function normalizeAxPolicy(input: unknown): PmxAxPolicy {
|
|
256
|
+
if (!isRecord(input)) return createEmptyAxPolicy();
|
|
257
|
+
const tools = isRecord(input.tools) ? input.tools : {};
|
|
258
|
+
const prompt = isRecord(input.prompt) ? input.prompt : {};
|
|
259
|
+
return {
|
|
260
|
+
tools: {
|
|
261
|
+
allowed: normalizeStringArray(tools.allowed),
|
|
262
|
+
excluded: normalizeStringArray(tools.excluded),
|
|
263
|
+
approvalRequired: normalizeStringArray(tools.approvalRequired),
|
|
264
|
+
},
|
|
265
|
+
prompt: {
|
|
266
|
+
systemAppend: optionalString(prompt.systemAppend),
|
|
267
|
+
mode: optionalString(prompt.mode),
|
|
268
|
+
},
|
|
269
|
+
};
|
|
270
|
+
}
|
|
203
271
|
const AX_EVIDENCE_KINDS = new Set<PmxAxEvidenceKind>(['logs', 'tool-result', 'screenshot', 'file', 'diff', 'test-output']);
|
|
204
272
|
const AX_WORK_STATUSES = new Set<PmxAxWorkItemStatus>(['todo', 'in-progress', 'blocked', 'done', 'cancelled']);
|
|
205
273
|
const AX_APPROVAL_STATUSES = new Set<PmxAxApprovalStatus>(['pending', 'approved', 'rejected']);
|
|
@@ -241,6 +309,108 @@ export function createEmptyAxFocusState(): PmxAxFocusState {
|
|
|
241
309
|
};
|
|
242
310
|
}
|
|
243
311
|
|
|
312
|
+
// ── Elicitation (structured human input request) — plan-004 Phase 5 ──
|
|
313
|
+
|
|
314
|
+
export type PmxAxElicitationStatus = 'pending' | 'answered' | 'cancelled';
|
|
315
|
+
const AX_ELICITATION_STATUSES = new Set<PmxAxElicitationStatus>(['pending', 'answered', 'cancelled']);
|
|
316
|
+
|
|
317
|
+
export interface PmxAxElicitation {
|
|
318
|
+
id: string;
|
|
319
|
+
prompt: string;
|
|
320
|
+
fields: string[];
|
|
321
|
+
status: PmxAxElicitationStatus;
|
|
322
|
+
response: Record<string, unknown> | null;
|
|
323
|
+
nodeIds: string[];
|
|
324
|
+
createdAt: string;
|
|
325
|
+
resolvedAt: string | null;
|
|
326
|
+
source: PmxAxSource | null;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export function normalizeAxElicitation(input: unknown, validNodeIds?: Set<string>): PmxAxElicitation | null {
|
|
330
|
+
if (!isRecord(input) || typeof input.id !== 'string') return null;
|
|
331
|
+
return {
|
|
332
|
+
id: input.id,
|
|
333
|
+
prompt: typeof input.prompt === 'string' ? input.prompt : '(input requested)',
|
|
334
|
+
fields: Array.isArray(input.fields) ? input.fields.filter((f): f is string => typeof f === 'string') : [],
|
|
335
|
+
status: AX_ELICITATION_STATUSES.has(input.status as PmxAxElicitationStatus) ? input.status as PmxAxElicitationStatus : 'pending',
|
|
336
|
+
response: isRecord(input.response) ? input.response : null,
|
|
337
|
+
nodeIds: normalizeNodeIds(input.nodeIds, validNodeIds),
|
|
338
|
+
createdAt: normalizeTimestamp(input.createdAt) ?? nowIso(),
|
|
339
|
+
resolvedAt: normalizeTimestamp(input.resolvedAt),
|
|
340
|
+
source: normalizeSource(input.source),
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export function createAxElicitation(
|
|
345
|
+
input: { prompt: string; fields?: string[]; nodeIds?: string[] },
|
|
346
|
+
source: PmxAxSource | null,
|
|
347
|
+
validNodeIds?: Set<string>,
|
|
348
|
+
): PmxAxElicitation {
|
|
349
|
+
return {
|
|
350
|
+
id: axId('elic'),
|
|
351
|
+
prompt: input.prompt,
|
|
352
|
+
fields: Array.isArray(input.fields) ? input.fields.filter((f) => typeof f === 'string') : [],
|
|
353
|
+
status: 'pending',
|
|
354
|
+
response: null,
|
|
355
|
+
nodeIds: normalizeNodeIds(input.nodeIds, validNodeIds),
|
|
356
|
+
createdAt: nowIso(),
|
|
357
|
+
resolvedAt: null,
|
|
358
|
+
source,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// ── Mode request (plan/execute/autonomous transition) — plan-004 Phase 5 ──
|
|
363
|
+
|
|
364
|
+
export type PmxAxMode = 'plan' | 'execute' | 'autonomous';
|
|
365
|
+
export type PmxAxModeRequestStatus = 'pending' | 'approved' | 'rejected';
|
|
366
|
+
const AX_MODES = new Set<PmxAxMode>(['plan', 'execute', 'autonomous']);
|
|
367
|
+
const AX_MODE_REQUEST_STATUSES = new Set<PmxAxModeRequestStatus>(['pending', 'approved', 'rejected']);
|
|
368
|
+
|
|
369
|
+
export interface PmxAxModeRequest {
|
|
370
|
+
id: string;
|
|
371
|
+
mode: PmxAxMode;
|
|
372
|
+
reason: string | null;
|
|
373
|
+
status: PmxAxModeRequestStatus;
|
|
374
|
+
nodeIds: string[];
|
|
375
|
+
createdAt: string;
|
|
376
|
+
resolvedAt: string | null;
|
|
377
|
+
resolution: string | null;
|
|
378
|
+
source: PmxAxSource | null;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export function normalizeAxModeRequest(input: unknown, validNodeIds?: Set<string>): PmxAxModeRequest | null {
|
|
382
|
+
if (!isRecord(input) || typeof input.id !== 'string') return null;
|
|
383
|
+
return {
|
|
384
|
+
id: input.id,
|
|
385
|
+
mode: AX_MODES.has(input.mode as PmxAxMode) ? input.mode as PmxAxMode : 'execute',
|
|
386
|
+
reason: optionalString(input.reason),
|
|
387
|
+
status: AX_MODE_REQUEST_STATUSES.has(input.status as PmxAxModeRequestStatus) ? input.status as PmxAxModeRequestStatus : 'pending',
|
|
388
|
+
nodeIds: normalizeNodeIds(input.nodeIds, validNodeIds),
|
|
389
|
+
createdAt: normalizeTimestamp(input.createdAt) ?? nowIso(),
|
|
390
|
+
resolvedAt: normalizeTimestamp(input.resolvedAt),
|
|
391
|
+
resolution: optionalString(input.resolution),
|
|
392
|
+
source: normalizeSource(input.source),
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export function createAxModeRequest(
|
|
397
|
+
input: { mode: PmxAxMode; reason?: string | null; nodeIds?: string[] },
|
|
398
|
+
source: PmxAxSource | null,
|
|
399
|
+
validNodeIds?: Set<string>,
|
|
400
|
+
): PmxAxModeRequest {
|
|
401
|
+
return {
|
|
402
|
+
id: axId('mode'),
|
|
403
|
+
mode: input.mode,
|
|
404
|
+
reason: input.reason ?? null,
|
|
405
|
+
status: 'pending',
|
|
406
|
+
nodeIds: normalizeNodeIds(input.nodeIds, validNodeIds),
|
|
407
|
+
createdAt: nowIso(),
|
|
408
|
+
resolvedAt: null,
|
|
409
|
+
resolution: null,
|
|
410
|
+
source,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
244
414
|
export function createEmptyAxState(): PmxAxState {
|
|
245
415
|
return {
|
|
246
416
|
version: 1,
|
|
@@ -248,6 +418,9 @@ export function createEmptyAxState(): PmxAxState {
|
|
|
248
418
|
workItems: [],
|
|
249
419
|
approvalGates: [],
|
|
250
420
|
reviewAnnotations: [],
|
|
421
|
+
elicitations: [],
|
|
422
|
+
modeRequests: [],
|
|
423
|
+
policy: createEmptyAxPolicy(),
|
|
251
424
|
};
|
|
252
425
|
}
|
|
253
426
|
|
|
@@ -538,6 +711,13 @@ export function normalizeAxState(input: unknown, validNodeIds?: Set<string>): Pm
|
|
|
538
711
|
reviewAnnotations: Array.isArray(input.reviewAnnotations)
|
|
539
712
|
? input.reviewAnnotations.map((r) => normalizeAxReviewAnnotation(r, validNodeIds)).filter((r): r is PmxAxReviewAnnotation => r !== null)
|
|
540
713
|
: [],
|
|
714
|
+
elicitations: Array.isArray(input.elicitations)
|
|
715
|
+
? input.elicitations.map((e) => normalizeAxElicitation(e, validNodeIds)).filter((e): e is PmxAxElicitation => e !== null)
|
|
716
|
+
: [],
|
|
717
|
+
modeRequests: Array.isArray(input.modeRequests)
|
|
718
|
+
? input.modeRequests.map((m) => normalizeAxModeRequest(m, validNodeIds)).filter((m): m is PmxAxModeRequest => m !== null)
|
|
719
|
+
: [],
|
|
720
|
+
policy: normalizeAxPolicy(input.policy),
|
|
541
721
|
};
|
|
542
722
|
}
|
|
543
723
|
|
|
@@ -549,6 +729,9 @@ export function buildAxContext(input: {
|
|
|
549
729
|
workItems: PmxAxWorkItem[];
|
|
550
730
|
approvalGates: PmxAxApprovalGate[];
|
|
551
731
|
reviewAnnotations: PmxAxReviewAnnotation[];
|
|
732
|
+
elicitations: PmxAxElicitation[];
|
|
733
|
+
modeRequests: PmxAxModeRequest[];
|
|
734
|
+
policy: PmxAxPolicy;
|
|
552
735
|
timeline: PmxAxTimelineSummary;
|
|
553
736
|
host: PmxAxHostCapability | null;
|
|
554
737
|
}): PmxAxContext {
|
|
@@ -567,6 +750,9 @@ export function buildAxContext(input: {
|
|
|
567
750
|
workItems: input.workItems,
|
|
568
751
|
approvalGates: input.approvalGates,
|
|
569
752
|
reviewAnnotations: input.reviewAnnotations,
|
|
753
|
+
elicitations: input.elicitations,
|
|
754
|
+
modeRequests: input.modeRequests,
|
|
755
|
+
policy: input.policy,
|
|
570
756
|
timeline: input.timeline,
|
|
571
757
|
host: input.host,
|
|
572
758
|
};
|
package/src/server/canvas-db.ts
CHANGED
|
@@ -901,6 +901,26 @@ export function loadAxSteeringFromDB(db: Database, q: AxTimelineQuery & { onlyPe
|
|
|
901
901
|
.filter((s): s is PmxAxSteeringMessage => s !== null);
|
|
902
902
|
}
|
|
903
903
|
|
|
904
|
+
export function loadPendingAxSteeringFromDB(
|
|
905
|
+
db: Database,
|
|
906
|
+
options: { consumer?: string; limit?: number } = {},
|
|
907
|
+
): PmxAxSteeringMessage[] {
|
|
908
|
+
interface Row { seq: number; id: string; message: string; delivered: number; created_at: string; source: string | null }
|
|
909
|
+
// FIFO (oldest undelivered first); exclude the consumer's own steering in SQL
|
|
910
|
+
// so the LIMIT is applied AFTER loop-prevention, not before.
|
|
911
|
+
const limit = clampTimelineLimit(options.limit);
|
|
912
|
+
const rows = options.consumer
|
|
913
|
+
? db.query<Row, [string, number]>(
|
|
914
|
+
'SELECT * FROM ax_steering WHERE delivered = 0 AND (source IS NULL OR source != ?) ORDER BY seq ASC LIMIT ?',
|
|
915
|
+
).all(options.consumer, limit)
|
|
916
|
+
: db.query<Row, [number]>(
|
|
917
|
+
'SELECT * FROM ax_steering WHERE delivered = 0 ORDER BY seq ASC LIMIT ?',
|
|
918
|
+
).all(limit);
|
|
919
|
+
return rows
|
|
920
|
+
.map((r) => normalizeAxSteeringMessage({ ...r, createdAt: r.created_at, delivered: r.delivered === 1 }))
|
|
921
|
+
.filter((s): s is PmxAxSteeringMessage => s !== null);
|
|
922
|
+
}
|
|
923
|
+
|
|
904
924
|
function countRows(db: Database, table: 'ax_events' | 'ax_evidence' | 'ax_steering'): number {
|
|
905
925
|
return Number(db.query<{ n: number }, []>(`SELECT COUNT(*) AS n FROM ${table}`).get()?.n ?? 0);
|
|
906
926
|
}
|
|
@@ -1467,6 +1467,10 @@ export function groupCanvasNodes(
|
|
|
1467
1467
|
): { ok: boolean } {
|
|
1468
1468
|
return {
|
|
1469
1469
|
ok: canvasState.groupNodes(groupId, childIds, {
|
|
1470
|
+
// Preserve existing child positions unless an explicit layout is asked
|
|
1471
|
+
// for — matching createCanvasGroup and the batch group.add path. Without
|
|
1472
|
+
// this, grouping silently auto-packs the nodes into a grid.
|
|
1473
|
+
preservePositions: options.childLayout === undefined,
|
|
1470
1474
|
...(options.childLayout ? { layout: options.childLayout } : {}),
|
|
1471
1475
|
}),
|
|
1472
1476
|
};
|
|
@@ -1836,6 +1840,13 @@ export async function executeCanvasBatch(
|
|
|
1836
1840
|
result = { ok: true, ...(updated ? serializeCreatedNode(updated) : { id }) };
|
|
1837
1841
|
break;
|
|
1838
1842
|
}
|
|
1843
|
+
case 'node.remove': {
|
|
1844
|
+
const id = typeof args.id === 'string' ? args.id : '';
|
|
1845
|
+
const { removed } = removeCanvasNode(id);
|
|
1846
|
+
if (!removed) throw new Error(`Node "${id}" not found.`);
|
|
1847
|
+
result = { ok: true, id, removed: true };
|
|
1848
|
+
break;
|
|
1849
|
+
}
|
|
1839
1850
|
case 'graph.add': {
|
|
1840
1851
|
const created = createCanvasGraphNode({
|
|
1841
1852
|
graphType: String(args.graphType ?? 'line'),
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
type CanvasNodeProvenance,
|
|
7
7
|
} from './canvas-provenance.js';
|
|
8
8
|
import { getCanvasNodeKind as getSharedCanvasNodeKind } from '../shared/canvas-node-kind.js';
|
|
9
|
+
import { canOpenNodeAsSurface } from '../shared/surface.js';
|
|
9
10
|
|
|
10
11
|
export interface SerializedCanvasNode extends CanvasNodeState {
|
|
11
12
|
kind: string;
|
|
@@ -13,6 +14,7 @@ export interface SerializedCanvasNode extends CanvasNodeState {
|
|
|
13
14
|
content: string | null;
|
|
14
15
|
path: string | null;
|
|
15
16
|
url: string | null;
|
|
17
|
+
surfaceUrl: string | null;
|
|
16
18
|
provenance: CanvasNodeProvenance | null;
|
|
17
19
|
}
|
|
18
20
|
|
|
@@ -101,6 +103,12 @@ export function getCanvasNodeContent(node: CanvasNodeState): string | null {
|
|
|
101
103
|
?? null;
|
|
102
104
|
}
|
|
103
105
|
|
|
106
|
+
export function getCanvasNodeSurfaceUrl(node: CanvasNodeState, data: Record<string, unknown>): string | null {
|
|
107
|
+
return canOpenNodeAsSurface(node.type, data)
|
|
108
|
+
? `/api/canvas/surface/${encodeURIComponent(node.id)}`
|
|
109
|
+
: null;
|
|
110
|
+
}
|
|
111
|
+
|
|
104
112
|
export function serializeCanvasNode(node: CanvasNodeState): SerializedCanvasNode {
|
|
105
113
|
const data = normalizeCanvasNodeData(node.type, node.data);
|
|
106
114
|
const normalizedNode = { ...node, data };
|
|
@@ -112,6 +120,7 @@ export function serializeCanvasNode(node: CanvasNodeState): SerializedCanvasNode
|
|
|
112
120
|
content: getCanvasNodeContent(normalizedNode),
|
|
113
121
|
path: pickString(data.path),
|
|
114
122
|
url: pickString(data.url),
|
|
123
|
+
surfaceUrl: getCanvasNodeSurfaceUrl(node, data),
|
|
115
124
|
provenance: pickProvenance(data.provenance),
|
|
116
125
|
};
|
|
117
126
|
}
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
loadAxEventsFromDB,
|
|
37
37
|
loadAxEvidenceFromDB,
|
|
38
38
|
loadAxSteeringFromDB,
|
|
39
|
+
loadPendingAxSteeringFromDB,
|
|
39
40
|
loadAxTimelineSummaryFromDB,
|
|
40
41
|
upsertAxHostCapabilityToDB,
|
|
41
42
|
loadAxHostCapabilityFromDB,
|
|
@@ -63,6 +64,17 @@ import {
|
|
|
63
64
|
createAxEvent,
|
|
64
65
|
createAxEvidence,
|
|
65
66
|
createAxSteeringMessage,
|
|
67
|
+
createAxElicitation,
|
|
68
|
+
createAxModeRequest,
|
|
69
|
+
isAxCommand,
|
|
70
|
+
listAxCommands,
|
|
71
|
+
AX_COMMAND_REGISTRY,
|
|
72
|
+
normalizeAxPolicy,
|
|
73
|
+
type PmxAxElicitation,
|
|
74
|
+
type PmxAxModeRequest,
|
|
75
|
+
type PmxAxMode,
|
|
76
|
+
type PmxAxCommandDescriptor,
|
|
77
|
+
type PmxAxPolicy,
|
|
66
78
|
type PmxAxFocusState,
|
|
67
79
|
type PmxAxSource,
|
|
68
80
|
type PmxAxState,
|
|
@@ -247,7 +259,7 @@ export interface CanvasNodeUpdate {
|
|
|
247
259
|
export type CanvasChangeType = 'pins' | 'nodes' | 'ax' | 'ax-timeline';
|
|
248
260
|
|
|
249
261
|
export interface MutationRecordInfo {
|
|
250
|
-
operationType: 'addNode' | 'updateNode' | 'removeNode' | 'addEdge' | 'removeEdge' | 'addAnnotation' | 'removeAnnotation' | 'clear' | 'restoreSnapshot' | 'setPins' | 'setAxFocus' | 'addWorkItem' | 'updateWorkItem' | 'requestApproval' | 'resolveApproval' | 'addReviewAnnotation' | 'updateReviewAnnotation' | 'arrange' | 'batch' | 'groupNodes' | 'ungroupNodes' | 'viewport';
|
|
262
|
+
operationType: 'addNode' | 'updateNode' | 'removeNode' | 'addEdge' | 'removeEdge' | 'addAnnotation' | 'removeAnnotation' | 'clear' | 'restoreSnapshot' | 'setPins' | 'setAxFocus' | 'addWorkItem' | 'updateWorkItem' | 'requestApproval' | 'resolveApproval' | 'addReviewAnnotation' | 'updateReviewAnnotation' | 'requestElicitation' | 'respondElicitation' | 'requestMode' | 'resolveModeRequest' | 'setPolicy' | 'arrange' | 'batch' | 'groupNodes' | 'ungroupNodes' | 'viewport';
|
|
251
263
|
description: string;
|
|
252
264
|
forward: () => void;
|
|
253
265
|
inverse: () => void;
|
|
@@ -1366,11 +1378,11 @@ class CanvasStateManager {
|
|
|
1366
1378
|
this.nodes.set(id, nextNode);
|
|
1367
1379
|
const parentGroupId = existing.data.parentGroup as string | undefined;
|
|
1368
1380
|
if (parentGroupId) {
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1381
|
+
// Moving or resizing a grouped child re-fits the group frame but must NOT
|
|
1382
|
+
// repack siblings — that would discard their explicit positions and the
|
|
1383
|
+
// moved child's requested coordinates. Compaction is opt-in (group
|
|
1384
|
+
// create/add with childLayout, or arrange).
|
|
1385
|
+
this.recomputeParentGroupBounds(parentGroupId);
|
|
1374
1386
|
this.reflowAllGroups();
|
|
1375
1387
|
}
|
|
1376
1388
|
this.scheduleSave();
|
|
@@ -1561,7 +1573,7 @@ class CanvasStateManager {
|
|
|
1561
1573
|
applyUpdates(updates: CanvasNodeUpdate[], options: ApplyUpdatesOptions = {}): { applied: number; skipped: number } {
|
|
1562
1574
|
let applied = 0;
|
|
1563
1575
|
let skipped = 0;
|
|
1564
|
-
const touchedParentGroups = new
|
|
1576
|
+
const touchedParentGroups = new Set<string>();
|
|
1565
1577
|
const oldSnapshots = new Map<string, CanvasNodeState>();
|
|
1566
1578
|
const appliedUpdates: CanvasNodeUpdate[] = [];
|
|
1567
1579
|
const explicitPositionUpdateIds = new Set(
|
|
@@ -1615,19 +1627,17 @@ class CanvasStateManager {
|
|
|
1615
1627
|
}));
|
|
1616
1628
|
const parentGroupId = existing.data.parentGroup as string | undefined;
|
|
1617
1629
|
if (parentGroupId) {
|
|
1618
|
-
|
|
1619
|
-
entry.compact = entry.compact || nextPatch.size !== undefined;
|
|
1620
|
-
touchedParentGroups.set(parentGroupId, entry);
|
|
1630
|
+
touchedParentGroups.add(parentGroupId);
|
|
1621
1631
|
}
|
|
1622
1632
|
applied++;
|
|
1623
1633
|
}
|
|
1624
1634
|
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1635
|
+
// Moving or resizing a grouped child re-fits the group frame, but must NOT
|
|
1636
|
+
// repack siblings — that would discard their explicit positions and the
|
|
1637
|
+
// moved child's requested coordinates. Compaction is opt-in, applied only
|
|
1638
|
+
// through an explicit layout (group create/add with childLayout, or arrange).
|
|
1639
|
+
for (const groupId of touchedParentGroups) {
|
|
1640
|
+
this.recomputeParentGroupBounds(groupId);
|
|
1631
1641
|
}
|
|
1632
1642
|
if (touchedParentGroups.size > 0) this.reflowAllGroups();
|
|
1633
1643
|
|
|
@@ -1918,6 +1928,148 @@ class CanvasStateManager {
|
|
|
1918
1928
|
return this._axHostCapability;
|
|
1919
1929
|
}
|
|
1920
1930
|
|
|
1931
|
+
getElicitations(): PmxAxElicitation[] {
|
|
1932
|
+
return this.getAxState().elicitations;
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
requestElicitation(
|
|
1936
|
+
input: { prompt: string; fields?: string[]; nodeIds?: string[] },
|
|
1937
|
+
options: { source?: PmxAxSource } = {},
|
|
1938
|
+
): PmxAxElicitation {
|
|
1939
|
+
const oldAxState = this.getAxState();
|
|
1940
|
+
const elicitation = createAxElicitation(input, options.source ?? 'api', this.currentNodeIdSet());
|
|
1941
|
+
this.applyAxState({ ...oldAxState, elicitations: [...oldAxState.elicitations, elicitation] });
|
|
1942
|
+
const applied = this.getAxState();
|
|
1943
|
+
this.scheduleSave();
|
|
1944
|
+
this.notifyChange('ax');
|
|
1945
|
+
this.recordMutation({
|
|
1946
|
+
operationType: 'requestElicitation',
|
|
1947
|
+
description: `Requested elicitation "${elicitation.prompt}"`,
|
|
1948
|
+
forward: this.suppressed(() => { this.applyAxState(applied); this.scheduleSave(); this.notifyChange('ax'); }),
|
|
1949
|
+
inverse: this.suppressed(() => { this.applyAxState(oldAxState); this.scheduleSave(); this.notifyChange('ax'); }),
|
|
1950
|
+
});
|
|
1951
|
+
return applied.elicitations.find((e) => e.id === elicitation.id) ?? elicitation;
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
respondElicitation(
|
|
1955
|
+
id: string,
|
|
1956
|
+
response: Record<string, unknown>,
|
|
1957
|
+
options: { source?: PmxAxSource } = {},
|
|
1958
|
+
): PmxAxElicitation | null {
|
|
1959
|
+
const oldAxState = this.getAxState();
|
|
1960
|
+
const existing = oldAxState.elicitations.find((e) => e.id === id);
|
|
1961
|
+
if (!existing || existing.status !== 'pending') return null;
|
|
1962
|
+
const merged: PmxAxElicitation = {
|
|
1963
|
+
...existing,
|
|
1964
|
+
status: 'answered',
|
|
1965
|
+
response,
|
|
1966
|
+
resolvedAt: new Date().toISOString(),
|
|
1967
|
+
source: options.source ?? existing.source,
|
|
1968
|
+
};
|
|
1969
|
+
this.applyAxState({ ...oldAxState, elicitations: replaceById(oldAxState.elicitations, merged) });
|
|
1970
|
+
const applied = this.getAxState();
|
|
1971
|
+
this.scheduleSave();
|
|
1972
|
+
this.notifyChange('ax');
|
|
1973
|
+
this.recordMutation({
|
|
1974
|
+
operationType: 'respondElicitation',
|
|
1975
|
+
description: `Answered elicitation ${id}`,
|
|
1976
|
+
forward: this.suppressed(() => { this.applyAxState(applied); this.scheduleSave(); this.notifyChange('ax'); }),
|
|
1977
|
+
inverse: this.suppressed(() => { this.applyAxState(oldAxState); this.scheduleSave(); this.notifyChange('ax'); }),
|
|
1978
|
+
});
|
|
1979
|
+
return applied.elicitations.find((e) => e.id === id) ?? null;
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
getModeRequests(): PmxAxModeRequest[] {
|
|
1983
|
+
return this.getAxState().modeRequests;
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
requestMode(
|
|
1987
|
+
input: { mode: PmxAxMode; reason?: string | null; nodeIds?: string[] },
|
|
1988
|
+
options: { source?: PmxAxSource } = {},
|
|
1989
|
+
): PmxAxModeRequest {
|
|
1990
|
+
const oldAxState = this.getAxState();
|
|
1991
|
+
const request = createAxModeRequest(input, options.source ?? 'api', this.currentNodeIdSet());
|
|
1992
|
+
this.applyAxState({ ...oldAxState, modeRequests: [...oldAxState.modeRequests, request] });
|
|
1993
|
+
const applied = this.getAxState();
|
|
1994
|
+
this.scheduleSave();
|
|
1995
|
+
this.notifyChange('ax');
|
|
1996
|
+
this.recordMutation({
|
|
1997
|
+
operationType: 'requestMode',
|
|
1998
|
+
description: `Requested mode "${request.mode}"`,
|
|
1999
|
+
forward: this.suppressed(() => { this.applyAxState(applied); this.scheduleSave(); this.notifyChange('ax'); }),
|
|
2000
|
+
inverse: this.suppressed(() => { this.applyAxState(oldAxState); this.scheduleSave(); this.notifyChange('ax'); }),
|
|
2001
|
+
});
|
|
2002
|
+
return applied.modeRequests.find((m) => m.id === request.id) ?? request;
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
resolveModeRequest(
|
|
2006
|
+
id: string,
|
|
2007
|
+
decision: 'approved' | 'rejected',
|
|
2008
|
+
options: { resolution?: string; source?: PmxAxSource } = {},
|
|
2009
|
+
): PmxAxModeRequest | null {
|
|
2010
|
+
const oldAxState = this.getAxState();
|
|
2011
|
+
const existing = oldAxState.modeRequests.find((m) => m.id === id);
|
|
2012
|
+
if (!existing || existing.status !== 'pending') return null;
|
|
2013
|
+
const merged: PmxAxModeRequest = {
|
|
2014
|
+
...existing,
|
|
2015
|
+
status: decision,
|
|
2016
|
+
resolvedAt: new Date().toISOString(),
|
|
2017
|
+
resolution: options.resolution ?? null,
|
|
2018
|
+
source: options.source ?? existing.source,
|
|
2019
|
+
};
|
|
2020
|
+
this.applyAxState({ ...oldAxState, modeRequests: replaceById(oldAxState.modeRequests, merged) });
|
|
2021
|
+
const applied = this.getAxState();
|
|
2022
|
+
this.scheduleSave();
|
|
2023
|
+
this.notifyChange('ax');
|
|
2024
|
+
this.recordMutation({
|
|
2025
|
+
operationType: 'resolveModeRequest',
|
|
2026
|
+
description: `Resolved mode request ${id} -> ${decision}`,
|
|
2027
|
+
forward: this.suppressed(() => { this.applyAxState(applied); this.scheduleSave(); this.notifyChange('ax'); }),
|
|
2028
|
+
inverse: this.suppressed(() => { this.applyAxState(oldAxState); this.scheduleSave(); this.notifyChange('ax'); }),
|
|
2029
|
+
});
|
|
2030
|
+
return applied.modeRequests.find((m) => m.id === id) ?? null;
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
getCommandRegistry(): PmxAxCommandDescriptor[] {
|
|
2034
|
+
return listAxCommands();
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
/** Invoke a registry-gated PMX command intent — records a timeline event (no execution). */
|
|
2038
|
+
invokeCommand(name: string, args: Record<string, unknown> | null = null, options: { source?: PmxAxSource } = {}): PmxAxEvent | null {
|
|
2039
|
+
if (!isAxCommand(name)) return null;
|
|
2040
|
+
return this.recordAxEvent(
|
|
2041
|
+
{ kind: 'command', summary: name, detail: AX_COMMAND_REGISTRY[name].description, data: { command: name, ...(args ? { args } : {}) } },
|
|
2042
|
+
options,
|
|
2043
|
+
);
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
getPolicy(): PmxAxPolicy {
|
|
2047
|
+
return this.getAxState().policy;
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
/** Merge a declarative tool/prompt policy patch (canvas-bound, snapshotted). */
|
|
2051
|
+
setPolicy(
|
|
2052
|
+
patch: { tools?: Partial<PmxAxPolicy['tools']>; prompt?: Partial<PmxAxPolicy['prompt']> },
|
|
2053
|
+
_options: { source?: PmxAxSource } = {},
|
|
2054
|
+
): PmxAxPolicy {
|
|
2055
|
+
const oldAxState = this.getAxState();
|
|
2056
|
+
const merged = normalizeAxPolicy({
|
|
2057
|
+
tools: { ...oldAxState.policy.tools, ...(patch.tools ?? {}) },
|
|
2058
|
+
prompt: { ...oldAxState.policy.prompt, ...(patch.prompt ?? {}) },
|
|
2059
|
+
});
|
|
2060
|
+
this.applyAxState({ ...oldAxState, policy: merged });
|
|
2061
|
+
const applied = this.getAxState();
|
|
2062
|
+
this.scheduleSave();
|
|
2063
|
+
this.notifyChange('ax');
|
|
2064
|
+
this.recordMutation({
|
|
2065
|
+
operationType: 'setPolicy',
|
|
2066
|
+
description: 'Updated AX policy',
|
|
2067
|
+
forward: this.suppressed(() => { this.applyAxState(applied); this.scheduleSave(); this.notifyChange('ax'); }),
|
|
2068
|
+
inverse: this.suppressed(() => { this.applyAxState(oldAxState); this.scheduleSave(); this.notifyChange('ax'); }),
|
|
2069
|
+
});
|
|
2070
|
+
return applied.policy;
|
|
2071
|
+
}
|
|
2072
|
+
|
|
1921
2073
|
setHostCapability(input: unknown, _options: { source?: PmxAxSource } = {}): PmxAxHostCapability {
|
|
1922
2074
|
const cap = normalizeAxHostCapability(
|
|
1923
2075
|
isRecord(input)
|
|
@@ -2012,6 +2164,15 @@ class CanvasStateManager {
|
|
|
2012
2164
|
return this._db ? loadAxSteeringFromDB(this._db, q) : [];
|
|
2013
2165
|
}
|
|
2014
2166
|
|
|
2167
|
+
/**
|
|
2168
|
+
* Undelivered steering for a consumer (Phase 4 delivery). Excludes messages
|
|
2169
|
+
* whose source equals the consumer to prevent delivery loops (e.g. Copilot
|
|
2170
|
+
* should not be handed back steering it originated).
|
|
2171
|
+
*/
|
|
2172
|
+
getPendingSteering(options: { consumer?: string; limit?: number } = {}): PmxAxSteeringMessage[] {
|
|
2173
|
+
return this._db ? loadPendingAxSteeringFromDB(this._db, options) : [];
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2015
2176
|
getAxTimelineSummary(): PmxAxTimelineSummary {
|
|
2016
2177
|
return this._db
|
|
2017
2178
|
? loadAxTimelineSummaryFromDB(this._db)
|