pmx-canvas 0.1.26 → 0.1.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/extensions/pmx-canvas/extension.mjs +191 -0
- package/CHANGELOG.md +110 -0
- package/Readme.md +74 -27
- package/dist/canvas/index.js +82 -82
- package/dist/json-render/index.css +1 -1
- package/dist/json-render/index.js +944 -164
- package/dist/types/json-render/catalog.d.ts +195 -20
- package/dist/types/json-render/charts/components.d.ts +17 -0
- package/dist/types/json-render/charts/definitions.d.ts +13 -1
- package/dist/types/json-render/charts/tufte-components.d.ts +65 -0
- package/dist/types/json-render/charts/tufte-definitions.d.ts +164 -0
- package/dist/types/json-render/directives.d.ts +33 -0
- package/dist/types/json-render/renderer/index.d.ts +1 -0
- package/dist/types/json-render/server.d.ts +32 -1
- package/dist/types/mcp/canvas-access.d.ts +62 -0
- package/dist/types/server/ax-state.d.ts +170 -0
- package/dist/types/server/canvas-db.d.ts +17 -1
- package/dist/types/server/canvas-operations.d.ts +53 -0
- package/dist/types/server/canvas-schema.d.ts +5 -1
- package/dist/types/server/canvas-state.d.ts +95 -4
- package/dist/types/server/index.d.ts +120 -3
- package/dist/types/server/mutation-history.d.ts +1 -1
- package/docs/cli.md +42 -0
- package/docs/http-api.md +64 -0
- package/docs/mcp.md +23 -5
- package/docs/node-types.md +1 -1
- package/docs/screenshots/codex-app.png +0 -0
- package/docs/screenshots/github-copilot-app.png +0 -0
- package/docs/sdk.md +23 -5
- package/package.json +10 -7
- package/skills/control-session-orchestrator/SKILL.md +359 -0
- package/skills/control-session-orchestrator/evals/evals.json +75 -0
- package/skills/data-analysis/SKILL.md +6 -0
- package/skills/pmx-canvas/SKILL.md +50 -4
- package/skills/pmx-canvas/references/github-copilot-app-adapter.md +6 -0
- package/skills/tufte-viz/SKILL.md +157 -0
- package/skills/tufte-viz/references/analytical-design.md +217 -0
- package/skills/tufte-viz/references/tufte-principles.md +147 -0
- package/src/cli/agent.ts +302 -3
- package/src/cli/index.ts +2 -1
- package/src/client/nodes/ExtAppFrame.tsx +48 -1
- package/src/client/nodes/McpAppNode.tsx +6 -2
- package/src/json-render/catalog.ts +22 -1
- package/src/json-render/charts/components.tsx +127 -15
- package/src/json-render/charts/definitions.ts +19 -2
- package/src/json-render/charts/extra-components.tsx +5 -4
- package/src/json-render/charts/tufte-components.tsx +395 -0
- package/src/json-render/charts/tufte-definitions.ts +128 -0
- package/src/json-render/directives.ts +64 -0
- package/src/json-render/renderer/index.css +107 -1
- package/src/json-render/renderer/index.tsx +33 -0
- package/src/json-render/server.ts +275 -5
- package/src/mcp/canvas-access.ts +264 -1
- package/src/mcp/server.ts +498 -9
- package/src/server/ax-context.ts +8 -3
- package/src/server/ax-state.ts +447 -0
- package/src/server/canvas-db.ts +184 -1
- package/src/server/canvas-operations.ts +123 -2
- package/src/server/canvas-schema.ts +27 -3
- package/src/server/canvas-state.ts +349 -2
- package/src/server/index.ts +259 -7
- package/src/server/mutation-history.ts +6 -0
- package/src/server/server.ts +442 -5
- package/src/server/web-artifacts.ts +31 -5
|
@@ -22,9 +22,11 @@ import { searchNodes } from './spatial-analysis.js';
|
|
|
22
22
|
import { getCanvasNodeTitle, serializeCanvasNodeCompact, type SerializedCanvasNode } from './canvas-serialization.js';
|
|
23
23
|
import { computeAutoArrange } from '../shared/auto-arrange.js';
|
|
24
24
|
import {
|
|
25
|
+
applyJsonRenderStreamPatches,
|
|
25
26
|
buildGraphSpec,
|
|
26
27
|
buildGraphConfig,
|
|
27
28
|
createJsonRenderNodeData,
|
|
29
|
+
emptyStreamingSpec,
|
|
28
30
|
GRAPH_NODE_SIZE,
|
|
29
31
|
inferJsonRenderNodeTitle,
|
|
30
32
|
JSON_RENDER_NODE_SIZE,
|
|
@@ -105,6 +107,12 @@ interface CanvasAddNodeInput {
|
|
|
105
107
|
|
|
106
108
|
export const MARKDOWN_NODE_DEFAULT_SIZE = { width: 640, height: 420 };
|
|
107
109
|
export const MCP_APP_NODE_DEFAULT_SIZE = { width: 960, height: 600 };
|
|
110
|
+
// Image and ledger nodes previously fell through to the generic 360x200 frame,
|
|
111
|
+
// which clipped content (a 360-wide image / log stream is cramped). Give them
|
|
112
|
+
// roomier defaults; height still auto-fits to content (see auto-fit.ts), so the
|
|
113
|
+
// width bump is the reliable lever.
|
|
114
|
+
export const IMAGE_NODE_DEFAULT_SIZE = { width: 480, height: 360 };
|
|
115
|
+
export const LEDGER_NODE_DEFAULT_SIZE = { width: 420, height: 280 };
|
|
108
116
|
|
|
109
117
|
interface CanvasCreateGroupInput {
|
|
110
118
|
title?: string;
|
|
@@ -827,6 +835,39 @@ export function scheduleCodeGraphRecompute(onComplete?: () => void): void {
|
|
|
827
835
|
}, 300);
|
|
828
836
|
}
|
|
829
837
|
|
|
838
|
+
/**
|
|
839
|
+
* Resolve an html-node `html` field that may be a path to a local .html/.htm file.
|
|
840
|
+
*
|
|
841
|
+
* If the string looks like a bare filesystem path to an existing HTML file
|
|
842
|
+
* (no markup, no newlines, short, ends in .html/.htm, exists on disk), read the
|
|
843
|
+
* file and return its contents. Otherwise return the string unchanged as raw HTML.
|
|
844
|
+
* On read failure, fall back to the raw string and warn — never throw.
|
|
845
|
+
*
|
|
846
|
+
* This is a local dev tool, so reading a user-pointed-at local file is acceptable;
|
|
847
|
+
* the markup/newline guards prevent misclassifying genuine HTML as a path.
|
|
848
|
+
*/
|
|
849
|
+
export function resolveHtmlContent(html: string): string {
|
|
850
|
+
const trimmed = html.trim();
|
|
851
|
+
const looksLikePath =
|
|
852
|
+
trimmed.length > 0 &&
|
|
853
|
+
trimmed.length <= 1024 &&
|
|
854
|
+
!trimmed.includes('\n') &&
|
|
855
|
+
!trimmed.includes('<') &&
|
|
856
|
+
/\.html?$/i.test(trimmed);
|
|
857
|
+
if (!looksLikePath) return html;
|
|
858
|
+
|
|
859
|
+
const resolved = resolve(trimmed);
|
|
860
|
+
if (!existsSync(resolved) || !statSync(resolved).isFile()) return html;
|
|
861
|
+
|
|
862
|
+
try {
|
|
863
|
+
return readFileSync(resolved, 'utf-8');
|
|
864
|
+
} catch (error) {
|
|
865
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
866
|
+
console.warn(`[pmx-canvas] html node: failed to read "${resolved}" (${message}); treating --content as raw HTML.`);
|
|
867
|
+
return html;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
830
871
|
export function addCanvasNode(input: CanvasAddNodeInput): {
|
|
831
872
|
id: string;
|
|
832
873
|
node: CanvasNodeState;
|
|
@@ -1471,6 +1512,78 @@ export function createCanvasJsonRenderNode(
|
|
|
1471
1512
|
return { id, url: String(node.data.url), spec, node };
|
|
1472
1513
|
}
|
|
1473
1514
|
|
|
1515
|
+
/**
|
|
1516
|
+
* Create an empty streaming json-render node. Unlike createCanvasJsonRenderNode
|
|
1517
|
+
* this does NOT validate a complete spec — the node starts blank and is filled
|
|
1518
|
+
* in by appendCanvasJsonRenderStream as SpecStream patches arrive.
|
|
1519
|
+
*/
|
|
1520
|
+
export function createCanvasStreamingJsonRenderNode(input: {
|
|
1521
|
+
title?: string;
|
|
1522
|
+
x?: number;
|
|
1523
|
+
y?: number;
|
|
1524
|
+
width?: number;
|
|
1525
|
+
height?: number;
|
|
1526
|
+
strictSize?: boolean;
|
|
1527
|
+
}): { id: string; url: string; spec: JsonRenderSpec; node: CanvasNodeState } {
|
|
1528
|
+
const spec = emptyStreamingSpec();
|
|
1529
|
+
const width = input.width ?? JSON_RENDER_NODE_SIZE.width;
|
|
1530
|
+
const height = input.height ?? JSON_RENDER_NODE_SIZE.height;
|
|
1531
|
+
const position =
|
|
1532
|
+
input.x !== undefined && input.y !== undefined
|
|
1533
|
+
? { x: input.x, y: input.y }
|
|
1534
|
+
: findOpenCanvasPosition(canvasState.getLayout().nodes, width, height);
|
|
1535
|
+
const id = `ui-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
|
|
1536
|
+
const node: CanvasNodeState = {
|
|
1537
|
+
id,
|
|
1538
|
+
type: 'json-render',
|
|
1539
|
+
position,
|
|
1540
|
+
size: { width, height },
|
|
1541
|
+
zIndex: 1,
|
|
1542
|
+
collapsed: false,
|
|
1543
|
+
pinned: false,
|
|
1544
|
+
dockPosition: null,
|
|
1545
|
+
data: createJsonRenderNodeData(id, input.title?.trim() || 'Streaming', spec, {
|
|
1546
|
+
viewerType: 'json-render',
|
|
1547
|
+
streamStatus: 'open',
|
|
1548
|
+
specVersion: 0,
|
|
1549
|
+
...(input.strictSize ? { strictSize: true } : {}),
|
|
1550
|
+
}),
|
|
1551
|
+
};
|
|
1552
|
+
|
|
1553
|
+
canvasState.addJsonRenderNode(node);
|
|
1554
|
+
return { id, url: String(node.data.url), spec, node };
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
/**
|
|
1558
|
+
* Apply a batch of SpecStream patches to an existing json-render node, bumping
|
|
1559
|
+
* its specVersion so the browser reloads the viewer with the accumulated spec.
|
|
1560
|
+
*/
|
|
1561
|
+
export function appendCanvasJsonRenderStream(
|
|
1562
|
+
nodeId: string,
|
|
1563
|
+
patches: unknown[],
|
|
1564
|
+
done: boolean,
|
|
1565
|
+
):
|
|
1566
|
+
| { ok: true; applied: number; skipped: number; specVersion: number; elementCount: number; streamStatus: 'open' | 'closed' }
|
|
1567
|
+
| { ok: false; error: string } {
|
|
1568
|
+
const node = canvasState.getNode(nodeId);
|
|
1569
|
+
if (!node) return { ok: false, error: `Node "${nodeId}" not found.` };
|
|
1570
|
+
if (node.type !== 'json-render') return { ok: false, error: `Node "${nodeId}" is not a json-render node.` };
|
|
1571
|
+
|
|
1572
|
+
const currentSpec = (node.data.spec as JsonRenderSpec | undefined) ?? emptyStreamingSpec();
|
|
1573
|
+
const { spec, applied, skipped } = applyJsonRenderStreamPatches(currentSpec, patches);
|
|
1574
|
+
const prevVersion = typeof node.data.specVersion === 'number' ? node.data.specVersion : 0;
|
|
1575
|
+
const specVersion = prevVersion + 1;
|
|
1576
|
+
const streamStatus: 'open' | 'closed' = done ? 'closed' : 'open';
|
|
1577
|
+
|
|
1578
|
+
canvasState.updateNode(nodeId, {
|
|
1579
|
+
data: { ...node.data, spec, specVersion, streamStatus },
|
|
1580
|
+
});
|
|
1581
|
+
|
|
1582
|
+
const elementCount =
|
|
1583
|
+
spec.elements && typeof spec.elements === 'object' ? Object.keys(spec.elements).length : 0;
|
|
1584
|
+
return { ok: true, applied, skipped, specVersion, elementCount, streamStatus };
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1474
1587
|
export function createCanvasGraphNode(
|
|
1475
1588
|
input: GraphNodeInput,
|
|
1476
1589
|
): { id: string; url: string; spec: JsonRenderSpec; node: CanvasNodeState } {
|
|
@@ -1664,14 +1777,22 @@ export async function executeCanvasBatch(
|
|
|
1664
1777
|
? MARKDOWN_NODE_DEFAULT_SIZE.width
|
|
1665
1778
|
: type === 'mcp-app'
|
|
1666
1779
|
? MCP_APP_NODE_DEFAULT_SIZE.width
|
|
1667
|
-
:
|
|
1780
|
+
: type === 'image'
|
|
1781
|
+
? IMAGE_NODE_DEFAULT_SIZE.width
|
|
1782
|
+
: type === 'ledger'
|
|
1783
|
+
? LEDGER_NODE_DEFAULT_SIZE.width
|
|
1784
|
+
: 360,
|
|
1668
1785
|
defaultHeight: type === 'html'
|
|
1669
1786
|
? 640
|
|
1670
1787
|
: type === 'markdown'
|
|
1671
1788
|
? MARKDOWN_NODE_DEFAULT_SIZE.height
|
|
1672
1789
|
: type === 'mcp-app'
|
|
1673
1790
|
? MCP_APP_NODE_DEFAULT_SIZE.height
|
|
1674
|
-
:
|
|
1791
|
+
: type === 'image'
|
|
1792
|
+
? IMAGE_NODE_DEFAULT_SIZE.height
|
|
1793
|
+
: type === 'ledger'
|
|
1794
|
+
? LEDGER_NODE_DEFAULT_SIZE.height
|
|
1795
|
+
: 200,
|
|
1675
1796
|
fileMode: 'auto',
|
|
1676
1797
|
});
|
|
1677
1798
|
result = { ok: true, ...serializeCreatedNode(created.node) };
|
|
@@ -56,6 +56,10 @@ const CANONICAL_GRAPH_TYPES = [
|
|
|
56
56
|
'radar',
|
|
57
57
|
'stacked-bar',
|
|
58
58
|
'composed',
|
|
59
|
+
'sparkline',
|
|
60
|
+
'dot-plot',
|
|
61
|
+
'bullet',
|
|
62
|
+
'slopegraph',
|
|
59
63
|
] as const;
|
|
60
64
|
|
|
61
65
|
type CanvasGraphType = typeof CANONICAL_GRAPH_TYPES[number];
|
|
@@ -406,9 +410,9 @@ const CANVAS_CREATE_TYPES: CanvasCreateTypeSchema[] = [
|
|
|
406
410
|
fields: [
|
|
407
411
|
{
|
|
408
412
|
name: 'graphType',
|
|
409
|
-
type: '"line" | "bar" | "pie" | "area" | "scatter" | "radar" | "stacked-bar" | "composed"',
|
|
413
|
+
type: '"line" | "bar" | "pie" | "area" | "scatter" | "radar" | "stacked-bar" | "composed" | "sparkline" | "dot-plot" | "bullet" | "slopegraph"',
|
|
410
414
|
required: true,
|
|
411
|
-
description: 'Chart type. Aliases like "stack" and "
|
|
415
|
+
description: 'Chart type. Includes the Tufte primitives sparkline, dot-plot (Cleveland), bullet (Few KPI vs target), and slopegraph (paired before/after). Aliases like "stack", "combo", "dot", and "slope" are normalized server-side.',
|
|
412
416
|
aliases: ['graph-type'],
|
|
413
417
|
},
|
|
414
418
|
{ name: 'data', type: 'Record<string, unknown>[]', required: true, description: 'Chart dataset. The CLI also accepts piped JSON via --stdin.', aliases: ['data-json', 'data-file'] },
|
|
@@ -417,7 +421,7 @@ const CANVAS_CREATE_TYPES: CanvasCreateTypeSchema[] = [
|
|
|
417
421
|
{ name: 'yKey', type: 'string', required: false, description: 'Y-axis value key for line, bar, area, and scatter charts. Also used as a fallback bar key for composed charts.', aliases: ['y-key'] },
|
|
418
422
|
{ name: 'zKey', type: 'string', required: false, description: 'Optional bubble-size key for scatter charts.', aliases: ['z-key'] },
|
|
419
423
|
{ name: 'nameKey', type: 'string', required: false, description: 'Slice name key for pie graphs.', aliases: ['name-key'] },
|
|
420
|
-
{ name: 'valueKey', type: 'string', required: false, description: '
|
|
424
|
+
{ name: 'valueKey', type: 'string', required: false, description: 'Value key for pie slices, sparkline, dot-plot, and the bullet measure.', aliases: ['value-key'] },
|
|
421
425
|
{ name: 'axisKey', type: 'string', required: false, description: 'Category key for radar charts.', aliases: ['axis-key'] },
|
|
422
426
|
{ name: 'metrics', type: 'string[]', required: false, description: 'Series keys to plot as radar polygons. Defaults to non-axis numeric columns.' },
|
|
423
427
|
{ name: 'series', type: 'string[]', required: false, description: 'Series keys for stacked-bar segments. Defaults to non-x numeric columns.' },
|
|
@@ -427,6 +431,14 @@ const CANVAS_CREATE_TYPES: CanvasCreateTypeSchema[] = [
|
|
|
427
431
|
{ name: 'color', type: 'string', required: false, description: 'Optional series color for line, bar, area, and scatter charts.' },
|
|
428
432
|
{ name: 'barColor', type: 'string', required: false, description: 'Optional bar color for composed charts.', aliases: ['bar-color'] },
|
|
429
433
|
{ name: 'lineColor', type: 'string', required: false, description: 'Optional line color for composed charts.', aliases: ['line-color'] },
|
|
434
|
+
{ name: 'colorBy', type: '"series" | "category" | "value" | "none"', required: false, description: 'Bar charts only: how bars are colored. Default "series" (single accent + one highlighted bar). "category" rotates the palette, "value" shades by magnitude, "none" is flat. Color should encode data, not decorate.', aliases: ['color-by'] },
|
|
435
|
+
{ name: 'highlight', type: 'number | "max" | "min"', required: false, description: 'Bar charts (colorBy="series") only: which bar gets the accent — "max" (default), "min", a 0-based index, or null for no emphasis.' },
|
|
436
|
+
{ name: 'labelKey', type: 'string', required: false, description: 'Category label key for dot-plot, bullet, and slopegraph rows.', aliases: ['label-key'] },
|
|
437
|
+
{ name: 'targetKey', type: 'string', required: false, description: 'Per-row target value key for bullet charts.', aliases: ['target-key'] },
|
|
438
|
+
{ name: 'rangesKey', type: 'string', required: false, description: 'Per-row qualitative band thresholds (number[]) key for bullet charts.', aliases: ['ranges-key'] },
|
|
439
|
+
{ name: 'beforeKey', type: 'string', required: false, description: 'Left-column value key for slopegraph.', aliases: ['before-key'] },
|
|
440
|
+
{ name: 'afterKey', type: 'string', required: false, description: 'Right-column value key for slopegraph.', aliases: ['after-key'] },
|
|
441
|
+
{ name: 'sort', type: '"asc" | "desc" | "none"', required: false, description: 'Row sort order for dot-plot (defaults to desc).' },
|
|
430
442
|
{ name: 'height', type: 'number', required: false, description: 'Optional chart content height.', aliases: ['chart-height'] },
|
|
431
443
|
{ name: 'showLegend', type: 'boolean', required: false, description: 'Show chart legend when supported; pass false for compact node layouts.', aliases: ['show-legend'] },
|
|
432
444
|
{ name: 'showLabels', type: 'boolean', required: false, description: 'Show direct labels when supported, such as pie slice labels; defaults to true.', aliases: ['show-labels'] },
|
|
@@ -502,6 +514,7 @@ export function describeCanvasSchema(): {
|
|
|
502
514
|
jsonRender: {
|
|
503
515
|
rootShape: Record<string, string>;
|
|
504
516
|
components: JsonRenderComponentDescriptor[];
|
|
517
|
+
directives: Array<{ name: string; usage: string }>;
|
|
505
518
|
};
|
|
506
519
|
graph: {
|
|
507
520
|
graphTypes: CanvasGraphType[];
|
|
@@ -526,6 +539,16 @@ export function describeCanvasSchema(): {
|
|
|
526
539
|
state: 'record<string, unknown> | optional',
|
|
527
540
|
},
|
|
528
541
|
components: clone(describeJsonRenderCatalog()),
|
|
542
|
+
directives: [
|
|
543
|
+
{ name: '$state', usage: '{ "$state": "/path/to/value" } — read a value from the state model by path (one-way). Use this to bind a value by path; there is no $path directive.' },
|
|
544
|
+
{ name: '$format', usage: '{ "$format": "currency"|"number"|"percent"|"date", "value": <num|state-ref>, "currency"?: "USD", "locale"?, "style"?, "options"? } — Intl-formatted string' },
|
|
545
|
+
{ name: '$math', usage: '{ "$math": "add"|"subtract"|"multiply"|"divide"|"mod"|"min"|"max"|"round"|"floor"|"ceil"|"abs", "a": <num>, "b"?: <num> }' },
|
|
546
|
+
{ name: '$concat', usage: '{ "$concat": [<value>, <value>, ...] } — join values into one string' },
|
|
547
|
+
{ name: '$count', usage: '{ "$count": <array|state-ref> } — length of an array' },
|
|
548
|
+
{ name: '$truncate', usage: '{ "$truncate": <string>, "length": <num>, "suffix"?: "…" }' },
|
|
549
|
+
{ name: '$pluralize', usage: '{ "$pluralize": <count>, "one": "item", "other": "items" }' },
|
|
550
|
+
{ name: '$join', usage: '{ "$join": <array>, "separator"?: ", " }' },
|
|
551
|
+
],
|
|
529
552
|
},
|
|
530
553
|
graph: {
|
|
531
554
|
graphTypes: [...CANONICAL_GRAPH_TYPES],
|
|
@@ -537,6 +560,7 @@ export function describeCanvasSchema(): {
|
|
|
537
560
|
'canvas_add_html_node',
|
|
538
561
|
'canvas_add_html_primitive',
|
|
539
562
|
'canvas_add_json_render_node',
|
|
563
|
+
'canvas_stream_json_render_node',
|
|
540
564
|
'canvas_add_graph_node',
|
|
541
565
|
'canvas_build_web_artifact',
|
|
542
566
|
'canvas_open_mcp_app',
|
|
@@ -29,8 +29,19 @@ import {
|
|
|
29
29
|
isDbPopulated,
|
|
30
30
|
checkpointCanvasDb,
|
|
31
31
|
finalizeCanvasDbForClose,
|
|
32
|
+
appendAxEventToDB,
|
|
33
|
+
appendAxEvidenceToDB,
|
|
34
|
+
appendAxSteeringToDB,
|
|
35
|
+
markAxSteeringDeliveredInDB,
|
|
36
|
+
loadAxEventsFromDB,
|
|
37
|
+
loadAxEvidenceFromDB,
|
|
38
|
+
loadAxSteeringFromDB,
|
|
39
|
+
loadAxTimelineSummaryFromDB,
|
|
40
|
+
upsertAxHostCapabilityToDB,
|
|
41
|
+
loadAxHostCapabilityFromDB,
|
|
32
42
|
type PersistedCanvasState,
|
|
33
43
|
type CanvasTheme,
|
|
44
|
+
type AxTimelineQuery,
|
|
34
45
|
} from './canvas-db.js';
|
|
35
46
|
import { normalizeCanvasTheme } from './canvas-db.js';
|
|
36
47
|
import {
|
|
@@ -43,10 +54,34 @@ import {
|
|
|
43
54
|
} from './placement.js';
|
|
44
55
|
import {
|
|
45
56
|
createEmptyAxState,
|
|
57
|
+
createEmptyAxHostCapability,
|
|
46
58
|
normalizeAxState,
|
|
59
|
+
normalizeAxHostCapability,
|
|
60
|
+
createAxWorkItem,
|
|
61
|
+
createAxApprovalGate,
|
|
62
|
+
createAxReviewAnnotation,
|
|
63
|
+
createAxEvent,
|
|
64
|
+
createAxEvidence,
|
|
65
|
+
createAxSteeringMessage,
|
|
47
66
|
type PmxAxFocusState,
|
|
48
67
|
type PmxAxSource,
|
|
49
68
|
type PmxAxState,
|
|
69
|
+
type PmxAxWorkItem,
|
|
70
|
+
type PmxAxWorkItemStatus,
|
|
71
|
+
type PmxAxApprovalGate,
|
|
72
|
+
type PmxAxReviewAnnotation,
|
|
73
|
+
type PmxAxReviewKind,
|
|
74
|
+
type PmxAxReviewSeverity,
|
|
75
|
+
type PmxAxReviewStatus,
|
|
76
|
+
type PmxAxReviewAnchorType,
|
|
77
|
+
type PmxAxReviewRegion,
|
|
78
|
+
type PmxAxEvent,
|
|
79
|
+
type PmxAxEventKind,
|
|
80
|
+
type PmxAxEvidence,
|
|
81
|
+
type PmxAxEvidenceKind,
|
|
82
|
+
type PmxAxSteeringMessage,
|
|
83
|
+
type PmxAxHostCapability,
|
|
84
|
+
type PmxAxTimelineSummary,
|
|
50
85
|
} from './ax-state.js';
|
|
51
86
|
|
|
52
87
|
function logCanvasStateWarning(action: string, error: unknown, details?: Record<string, unknown>): void {
|
|
@@ -209,10 +244,10 @@ export interface CanvasNodeUpdate {
|
|
|
209
244
|
dockPosition?: 'left' | 'right' | null;
|
|
210
245
|
}
|
|
211
246
|
|
|
212
|
-
export type CanvasChangeType = 'pins' | 'nodes' | 'ax';
|
|
247
|
+
export type CanvasChangeType = 'pins' | 'nodes' | 'ax' | 'ax-timeline';
|
|
213
248
|
|
|
214
249
|
export interface MutationRecordInfo {
|
|
215
|
-
operationType: 'addNode' | 'updateNode' | 'removeNode' | 'addEdge' | 'removeEdge' | 'addAnnotation' | 'removeAnnotation' | 'clear' | 'restoreSnapshot' | 'setPins' | 'setAxFocus' | 'arrange' | 'batch' | 'groupNodes' | 'ungroupNodes' | 'viewport';
|
|
250
|
+
operationType: 'addNode' | 'updateNode' | 'removeNode' | 'addEdge' | 'removeEdge' | 'addAnnotation' | 'removeAnnotation' | 'clear' | 'restoreSnapshot' | 'setPins' | 'setAxFocus' | 'addWorkItem' | 'updateWorkItem' | 'requestApproval' | 'resolveApproval' | 'addReviewAnnotation' | 'updateReviewAnnotation' | 'arrange' | 'batch' | 'groupNodes' | 'ungroupNodes' | 'viewport';
|
|
216
251
|
description: string;
|
|
217
252
|
forward: () => void;
|
|
218
253
|
inverse: () => void;
|
|
@@ -255,6 +290,14 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
255
290
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
256
291
|
}
|
|
257
292
|
|
|
293
|
+
function replaceById<T extends { id: string }>(list: T[], item: T): T[] {
|
|
294
|
+
const idx = list.findIndex((x) => x.id === item.id);
|
|
295
|
+
if (idx === -1) return [...list, item];
|
|
296
|
+
const copy = list.slice();
|
|
297
|
+
copy[idx] = item;
|
|
298
|
+
return copy;
|
|
299
|
+
}
|
|
300
|
+
|
|
258
301
|
function isPersistedBlobRef(value: unknown): value is PersistedBlobRef {
|
|
259
302
|
return isRecord(value) &&
|
|
260
303
|
value.__pmxCanvasBlob === 'v1' &&
|
|
@@ -273,6 +316,7 @@ class CanvasStateManager {
|
|
|
273
316
|
private _theme: CanvasTheme = 'dark';
|
|
274
317
|
private _contextPinnedNodeIds = new Set<string>();
|
|
275
318
|
private _axState: PmxAxState = createEmptyAxState();
|
|
319
|
+
private _axHostCapability: PmxAxHostCapability | null = null;
|
|
276
320
|
private _workspaceRoot = process.cwd();
|
|
277
321
|
|
|
278
322
|
// ── Change listeners (for MCP resource notifications) ──────
|
|
@@ -832,6 +876,14 @@ class CanvasStateManager {
|
|
|
832
876
|
|
|
833
877
|
/** Load canvas state from SQLite (or legacy JSON fallback). Call once on server startup. */
|
|
834
878
|
loadFromDisk(options: LoadFromDiskOptions = {}): boolean {
|
|
879
|
+
// Host capability lives in its own table (not snapshotted / not in PmxAxState).
|
|
880
|
+
if (this._db) {
|
|
881
|
+
try {
|
|
882
|
+
this._axHostCapability = loadAxHostCapabilityFromDB(this._db);
|
|
883
|
+
} catch (error) {
|
|
884
|
+
logCanvasStateWarning('load host capability failed', error, {});
|
|
885
|
+
}
|
|
886
|
+
}
|
|
835
887
|
// Try SQLite first (only if DB has been populated)
|
|
836
888
|
if (this._db && isDbPopulated(this._db)) {
|
|
837
889
|
try {
|
|
@@ -1683,6 +1735,298 @@ class CanvasStateManager {
|
|
|
1683
1735
|
return this.setAxFocus([], { source: 'system' });
|
|
1684
1736
|
}
|
|
1685
1737
|
|
|
1738
|
+
// ── Work items (canvas-bound; snapshotted via getAxState blob) ────
|
|
1739
|
+
getWorkItems(): PmxAxWorkItem[] {
|
|
1740
|
+
return this.getAxState().workItems;
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
addWorkItem(
|
|
1744
|
+
input: { title: string; status?: PmxAxWorkItemStatus; detail?: string | null; nodeIds?: string[] },
|
|
1745
|
+
options: { source?: PmxAxSource } = {},
|
|
1746
|
+
): PmxAxWorkItem {
|
|
1747
|
+
const oldAxState = this.getAxState();
|
|
1748
|
+
const item = createAxWorkItem(input, options.source ?? 'api', this.currentNodeIdSet());
|
|
1749
|
+
this.applyAxState({ ...oldAxState, workItems: [...oldAxState.workItems, item] });
|
|
1750
|
+
const applied = this.getAxState();
|
|
1751
|
+
this.scheduleSave();
|
|
1752
|
+
this.notifyChange('ax');
|
|
1753
|
+
this.recordMutation({
|
|
1754
|
+
operationType: 'addWorkItem',
|
|
1755
|
+
description: `Added work item "${item.title}"`,
|
|
1756
|
+
forward: this.suppressed(() => { this.applyAxState(applied); this.scheduleSave(); this.notifyChange('ax'); }),
|
|
1757
|
+
inverse: this.suppressed(() => { this.applyAxState(oldAxState); this.scheduleSave(); this.notifyChange('ax'); }),
|
|
1758
|
+
});
|
|
1759
|
+
return applied.workItems.find((w) => w.id === item.id) ?? item;
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
updateWorkItem(
|
|
1763
|
+
id: string,
|
|
1764
|
+
patch: { title?: string; status?: PmxAxWorkItemStatus; detail?: string | null; nodeIds?: string[] },
|
|
1765
|
+
options: { source?: PmxAxSource } = {},
|
|
1766
|
+
): PmxAxWorkItem | null {
|
|
1767
|
+
const oldAxState = this.getAxState();
|
|
1768
|
+
const existing = oldAxState.workItems.find((w) => w.id === id);
|
|
1769
|
+
if (!existing) return null;
|
|
1770
|
+
const merged: PmxAxWorkItem = {
|
|
1771
|
+
...existing,
|
|
1772
|
+
...(patch.title !== undefined ? { title: patch.title } : {}),
|
|
1773
|
+
...(patch.status !== undefined ? { status: patch.status } : {}),
|
|
1774
|
+
...(patch.detail !== undefined ? { detail: patch.detail } : {}),
|
|
1775
|
+
...(patch.nodeIds !== undefined ? { nodeIds: patch.nodeIds.filter((n) => this.nodes.has(n)) } : {}),
|
|
1776
|
+
updatedAt: new Date().toISOString(),
|
|
1777
|
+
source: options.source ?? existing.source,
|
|
1778
|
+
};
|
|
1779
|
+
this.applyAxState({ ...oldAxState, workItems: replaceById(oldAxState.workItems, merged) });
|
|
1780
|
+
const applied = this.getAxState();
|
|
1781
|
+
this.scheduleSave();
|
|
1782
|
+
this.notifyChange('ax');
|
|
1783
|
+
this.recordMutation({
|
|
1784
|
+
operationType: 'updateWorkItem',
|
|
1785
|
+
description: `Updated work item ${id}`,
|
|
1786
|
+
forward: this.suppressed(() => { this.applyAxState(applied); this.scheduleSave(); this.notifyChange('ax'); }),
|
|
1787
|
+
inverse: this.suppressed(() => { this.applyAxState(oldAxState); this.scheduleSave(); this.notifyChange('ax'); }),
|
|
1788
|
+
});
|
|
1789
|
+
return applied.workItems.find((w) => w.id === id) ?? null;
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
// ── Approval gates (canvas-bound) ─────────────────────────────────
|
|
1793
|
+
getApprovalGates(): PmxAxApprovalGate[] {
|
|
1794
|
+
return this.getAxState().approvalGates;
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
requestApproval(
|
|
1798
|
+
input: { title: string; detail?: string | null; action?: string | null; nodeIds?: string[] },
|
|
1799
|
+
options: { source?: PmxAxSource } = {},
|
|
1800
|
+
): PmxAxApprovalGate {
|
|
1801
|
+
const oldAxState = this.getAxState();
|
|
1802
|
+
const gate = createAxApprovalGate(input, options.source ?? 'api', this.currentNodeIdSet());
|
|
1803
|
+
this.applyAxState({ ...oldAxState, approvalGates: [...oldAxState.approvalGates, gate] });
|
|
1804
|
+
const applied = this.getAxState();
|
|
1805
|
+
this.scheduleSave();
|
|
1806
|
+
this.notifyChange('ax');
|
|
1807
|
+
this.recordMutation({
|
|
1808
|
+
operationType: 'requestApproval',
|
|
1809
|
+
description: `Requested approval "${gate.title}"`,
|
|
1810
|
+
forward: this.suppressed(() => { this.applyAxState(applied); this.scheduleSave(); this.notifyChange('ax'); }),
|
|
1811
|
+
inverse: this.suppressed(() => { this.applyAxState(oldAxState); this.scheduleSave(); this.notifyChange('ax'); }),
|
|
1812
|
+
});
|
|
1813
|
+
return applied.approvalGates.find((g) => g.id === gate.id) ?? gate;
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
resolveApproval(
|
|
1817
|
+
id: string,
|
|
1818
|
+
decision: 'approved' | 'rejected',
|
|
1819
|
+
options: { resolution?: string; source?: PmxAxSource } = {},
|
|
1820
|
+
): PmxAxApprovalGate | null {
|
|
1821
|
+
const oldAxState = this.getAxState();
|
|
1822
|
+
const gate = oldAxState.approvalGates.find((g) => g.id === id);
|
|
1823
|
+
if (!gate || gate.status !== 'pending') return null;
|
|
1824
|
+
const resolved: PmxAxApprovalGate = {
|
|
1825
|
+
...gate,
|
|
1826
|
+
status: decision,
|
|
1827
|
+
resolvedAt: new Date().toISOString(),
|
|
1828
|
+
resolution: options.resolution ?? null,
|
|
1829
|
+
source: options.source ?? gate.source,
|
|
1830
|
+
};
|
|
1831
|
+
this.applyAxState({ ...oldAxState, approvalGates: replaceById(oldAxState.approvalGates, resolved) });
|
|
1832
|
+
const applied = this.getAxState();
|
|
1833
|
+
this.scheduleSave();
|
|
1834
|
+
this.notifyChange('ax');
|
|
1835
|
+
this.recordMutation({
|
|
1836
|
+
operationType: 'resolveApproval',
|
|
1837
|
+
description: `Resolved approval ${id} -> ${decision}`,
|
|
1838
|
+
forward: this.suppressed(() => { this.applyAxState(applied); this.scheduleSave(); this.notifyChange('ax'); }),
|
|
1839
|
+
inverse: this.suppressed(() => { this.applyAxState(oldAxState); this.scheduleSave(); this.notifyChange('ax'); }),
|
|
1840
|
+
});
|
|
1841
|
+
return applied.approvalGates.find((g) => g.id === id) ?? null;
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
// ── Review annotations (canvas-bound) ─────────────────────────────
|
|
1845
|
+
getReviewAnnotations(): PmxAxReviewAnnotation[] {
|
|
1846
|
+
return this.getAxState().reviewAnnotations;
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
addReviewAnnotation(
|
|
1850
|
+
input: {
|
|
1851
|
+
body: string;
|
|
1852
|
+
kind?: PmxAxReviewKind;
|
|
1853
|
+
severity?: PmxAxReviewSeverity;
|
|
1854
|
+
anchorType?: PmxAxReviewAnchorType;
|
|
1855
|
+
nodeId?: string | null;
|
|
1856
|
+
file?: string | null;
|
|
1857
|
+
region?: PmxAxReviewRegion | null;
|
|
1858
|
+
author?: string | null;
|
|
1859
|
+
},
|
|
1860
|
+
options: { source?: PmxAxSource } = {},
|
|
1861
|
+
): PmxAxReviewAnnotation | null {
|
|
1862
|
+
// Validate the node anchor up front. A node-anchored review whose nodeId is
|
|
1863
|
+
// missing or unknown would otherwise be silently dropped by
|
|
1864
|
+
// normalizeAxForCurrentNodes after apply, yet still returned as a phantom
|
|
1865
|
+
// success object — false success / silent data loss. Reject instead so the
|
|
1866
|
+
// HTTP/MCP layers surface ok:false / 4xx.
|
|
1867
|
+
const anchorType = input.anchorType ?? 'node';
|
|
1868
|
+
if (anchorType === 'node' && (typeof input.nodeId !== 'string' || !this.currentNodeIdSet().has(input.nodeId))) {
|
|
1869
|
+
return null;
|
|
1870
|
+
}
|
|
1871
|
+
const oldAxState = this.getAxState();
|
|
1872
|
+
const annotation = createAxReviewAnnotation(input, options.source ?? 'api');
|
|
1873
|
+
this.applyAxState({ ...oldAxState, reviewAnnotations: [...oldAxState.reviewAnnotations, annotation] });
|
|
1874
|
+
const applied = this.getAxState();
|
|
1875
|
+
this.scheduleSave();
|
|
1876
|
+
this.notifyChange('ax');
|
|
1877
|
+
this.recordMutation({
|
|
1878
|
+
operationType: 'addReviewAnnotation',
|
|
1879
|
+
description: `Added review ${annotation.kind} (${annotation.severity})`,
|
|
1880
|
+
forward: this.suppressed(() => { this.applyAxState(applied); this.scheduleSave(); this.notifyChange('ax'); }),
|
|
1881
|
+
inverse: this.suppressed(() => { this.applyAxState(oldAxState); this.scheduleSave(); this.notifyChange('ax'); }),
|
|
1882
|
+
});
|
|
1883
|
+
return applied.reviewAnnotations.find((r) => r.id === annotation.id) ?? annotation;
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
updateReviewAnnotation(
|
|
1887
|
+
id: string,
|
|
1888
|
+
patch: { body?: string; status?: PmxAxReviewStatus; severity?: PmxAxReviewSeverity; kind?: PmxAxReviewKind },
|
|
1889
|
+
options: { source?: PmxAxSource } = {},
|
|
1890
|
+
): PmxAxReviewAnnotation | null {
|
|
1891
|
+
const oldAxState = this.getAxState();
|
|
1892
|
+
const existing = oldAxState.reviewAnnotations.find((r) => r.id === id);
|
|
1893
|
+
if (!existing) return null;
|
|
1894
|
+
const merged: PmxAxReviewAnnotation = {
|
|
1895
|
+
...existing,
|
|
1896
|
+
...(patch.body !== undefined ? { body: patch.body } : {}),
|
|
1897
|
+
...(patch.status !== undefined ? { status: patch.status } : {}),
|
|
1898
|
+
...(patch.severity !== undefined ? { severity: patch.severity } : {}),
|
|
1899
|
+
...(patch.kind !== undefined ? { kind: patch.kind } : {}),
|
|
1900
|
+
updatedAt: new Date().toISOString(),
|
|
1901
|
+
source: options.source ?? existing.source,
|
|
1902
|
+
};
|
|
1903
|
+
this.applyAxState({ ...oldAxState, reviewAnnotations: replaceById(oldAxState.reviewAnnotations, merged) });
|
|
1904
|
+
const applied = this.getAxState();
|
|
1905
|
+
this.scheduleSave();
|
|
1906
|
+
this.notifyChange('ax');
|
|
1907
|
+
this.recordMutation({
|
|
1908
|
+
operationType: 'updateReviewAnnotation',
|
|
1909
|
+
description: `Updated review ${id}`,
|
|
1910
|
+
forward: this.suppressed(() => { this.applyAxState(applied); this.scheduleSave(); this.notifyChange('ax'); }),
|
|
1911
|
+
inverse: this.suppressed(() => { this.applyAxState(oldAxState); this.scheduleSave(); this.notifyChange('ax'); }),
|
|
1912
|
+
});
|
|
1913
|
+
return applied.reviewAnnotations.find((r) => r.id === id) ?? null;
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
// ── Host capability (own table; reported by adapters) ─────────────
|
|
1917
|
+
getHostCapability(): PmxAxHostCapability | null {
|
|
1918
|
+
return this._axHostCapability;
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
setHostCapability(input: unknown, _options: { source?: PmxAxSource } = {}): PmxAxHostCapability {
|
|
1922
|
+
const cap = normalizeAxHostCapability(
|
|
1923
|
+
isRecord(input)
|
|
1924
|
+
? { ...input, reportedAt: new Date().toISOString() }
|
|
1925
|
+
: { reportedAt: new Date().toISOString() },
|
|
1926
|
+
) ?? createEmptyAxHostCapability();
|
|
1927
|
+
this._axHostCapability = cap;
|
|
1928
|
+
if (this._db) {
|
|
1929
|
+
try {
|
|
1930
|
+
upsertAxHostCapabilityToDB(this._db, cap);
|
|
1931
|
+
} catch (error) {
|
|
1932
|
+
logCanvasStateWarning('save host capability failed', error, {});
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
this.notifyChange('ax');
|
|
1936
|
+
return cap;
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
// ── Timeline (DB-direct; NOT in _axState; NOT history-recorded) ───
|
|
1940
|
+
recordAxEvent(
|
|
1941
|
+
input: { kind: PmxAxEventKind; summary: string; detail?: string | null; nodeIds?: string[]; data?: Record<string, unknown> | null },
|
|
1942
|
+
options: { source?: PmxAxSource } = {},
|
|
1943
|
+
): PmxAxEvent {
|
|
1944
|
+
const draft = createAxEvent(input, options.source ?? 'api');
|
|
1945
|
+
if (this._db) {
|
|
1946
|
+
try {
|
|
1947
|
+
const ev = appendAxEventToDB(this._db, draft);
|
|
1948
|
+
this.notifyChange('ax-timeline');
|
|
1949
|
+
return ev;
|
|
1950
|
+
} catch (error) {
|
|
1951
|
+
logCanvasStateWarning('record ax event failed', error, { id: draft.id });
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
this.notifyChange('ax-timeline');
|
|
1955
|
+
return { ...draft, seq: 0 };
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
addEvidence(
|
|
1959
|
+
input: { kind: PmxAxEvidenceKind; title: string; body?: string | null; ref?: string | null; nodeIds?: string[]; data?: Record<string, unknown> | null },
|
|
1960
|
+
options: { source?: PmxAxSource } = {},
|
|
1961
|
+
): PmxAxEvidence {
|
|
1962
|
+
const draft = createAxEvidence(input, options.source ?? 'api');
|
|
1963
|
+
if (this._db) {
|
|
1964
|
+
try {
|
|
1965
|
+
const ev = appendAxEvidenceToDB(this._db, draft);
|
|
1966
|
+
this.notifyChange('ax-timeline');
|
|
1967
|
+
return ev;
|
|
1968
|
+
} catch (error) {
|
|
1969
|
+
logCanvasStateWarning('add evidence failed', error, { id: draft.id });
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
this.notifyChange('ax-timeline');
|
|
1973
|
+
return { ...draft, seq: 0 };
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
recordSteeringMessage(message: string, options: { source?: PmxAxSource } = {}): PmxAxSteeringMessage {
|
|
1977
|
+
const draft = createAxSteeringMessage(message, options.source ?? 'api');
|
|
1978
|
+
if (this._db) {
|
|
1979
|
+
try {
|
|
1980
|
+
const s = appendAxSteeringToDB(this._db, draft);
|
|
1981
|
+
this.notifyChange('ax-timeline');
|
|
1982
|
+
return s;
|
|
1983
|
+
} catch (error) {
|
|
1984
|
+
logCanvasStateWarning('record steering failed', error, { id: draft.id });
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
this.notifyChange('ax-timeline');
|
|
1988
|
+
return { ...draft, seq: 0 };
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
markSteeringDelivered(id: string): boolean {
|
|
1992
|
+
if (!this._db) return false;
|
|
1993
|
+
try {
|
|
1994
|
+
const ok = markAxSteeringDeliveredInDB(this._db, id);
|
|
1995
|
+
if (ok) this.notifyChange('ax-timeline');
|
|
1996
|
+
return ok;
|
|
1997
|
+
} catch (error) {
|
|
1998
|
+
logCanvasStateWarning('mark steering delivered failed', error, { id });
|
|
1999
|
+
return false;
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
getAxEvents(q: AxTimelineQuery = {}): PmxAxEvent[] {
|
|
2004
|
+
return this._db ? loadAxEventsFromDB(this._db, q) : [];
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
getAxEvidence(q: AxTimelineQuery = {}): PmxAxEvidence[] {
|
|
2008
|
+
return this._db ? loadAxEvidenceFromDB(this._db, q) : [];
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
getAxSteering(q: AxTimelineQuery & { onlyPending?: boolean } = {}): PmxAxSteeringMessage[] {
|
|
2012
|
+
return this._db ? loadAxSteeringFromDB(this._db, q) : [];
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
getAxTimelineSummary(): PmxAxTimelineSummary {
|
|
2016
|
+
return this._db
|
|
2017
|
+
? loadAxTimelineSummaryFromDB(this._db)
|
|
2018
|
+
: { recentEvents: [], recentEvidence: [], pendingSteering: [], counts: { events: 0, evidence: 0, steering: 0 } };
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
getAxTimeline(q: AxTimelineQuery = {}): { events: PmxAxEvent[]; evidence: PmxAxEvidence[]; steering: PmxAxSteeringMessage[]; summary: PmxAxTimelineSummary } {
|
|
2022
|
+
return {
|
|
2023
|
+
events: this.getAxEvents(q),
|
|
2024
|
+
evidence: this.getAxEvidence(q),
|
|
2025
|
+
steering: this.getAxSteering(q),
|
|
2026
|
+
summary: this.getAxTimelineSummary(),
|
|
2027
|
+
};
|
|
2028
|
+
}
|
|
2029
|
+
|
|
1686
2030
|
setContextPins(nodeIds: string[]): void {
|
|
1687
2031
|
const oldPins = Array.from(this._contextPinnedNodeIds);
|
|
1688
2032
|
this._contextPinnedNodeIds.clear();
|
|
@@ -1814,6 +2158,9 @@ class CanvasStateManager {
|
|
|
1814
2158
|
this.edges.clear();
|
|
1815
2159
|
this.annotations.clear();
|
|
1816
2160
|
this._contextPinnedNodeIds.clear();
|
|
2161
|
+
// Clears canvas-bound AX state (focus, work items, approvals, review annotations).
|
|
2162
|
+
// Timeline tables (ax_events/ax_evidence/ax_steering) and host capability are
|
|
2163
|
+
// deliberately retained per the AX state-partition policy.
|
|
1817
2164
|
this._axState = createEmptyAxState();
|
|
1818
2165
|
this._viewport = { x: 0, y: 0, scale: 1 };
|
|
1819
2166
|
this.scheduleSave();
|