pmx-canvas 0.4.5 → 0.4.7
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 +118 -0
- package/Readme.md +76 -36
- package/dist/canvas/global.css +169 -3
- package/dist/canvas/index.js +112 -87
- package/dist/canvas/mermaid-entry.js +3563 -0
- package/dist/json-render/index.css +1 -1
- package/dist/types/cli/agent.d.ts +1 -0
- package/dist/types/cli/commands/smoke.d.ts +1 -0
- package/dist/types/cli/shared.d.ts +13 -1
- package/dist/types/client/canvas/CanvasNode.d.ts +11 -0
- package/dist/types/client/canvas/EdgeLayer.d.ts +8 -0
- package/dist/types/client/mermaid-entry.d.ts +11 -0
- package/dist/types/client/nodes/AxStepControls.d.ts +4 -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/FileNode.d.ts +14 -0
- package/dist/types/client/nodes/MermaidNode.d.ts +12 -0
- package/dist/types/client/state/canvas-store.d.ts +10 -0
- package/dist/types/client/state/intent-bridge.d.ts +9 -0
- package/dist/types/client/types.d.ts +2 -2
- package/dist/types/server/ax-flow-loop.d.ts +5 -0
- package/dist/types/server/ax-interaction.d.ts +21 -2
- package/dist/types/server/canvas-operations.d.ts +5 -0
- package/dist/types/server/canvas-provenance.d.ts +1 -1
- package/dist/types/server/canvas-state.d.ts +18 -1
- package/dist/types/server/canvas-validation.d.ts +28 -1
- package/dist/types/server/file-content.d.ts +46 -0
- package/dist/types/server/html-primitives.d.ts +13 -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-work.d.ts +1 -0
- package/dist/types/server/operations/ops/nodes.d.ts +1 -1
- package/dist/types/server/server.d.ts +1 -0
- package/dist/types/server/web-artifacts.d.ts +14 -0
- package/dist/types/server/workboard.d.ts +46 -0
- package/dist/types/shared/ax-flow.d.ts +51 -0
- package/dist/types/shared/ax-surface-protocol.d.ts +7 -0
- package/dist/types/shared/node-sizes.d.ts +33 -0
- package/docs/cli.md +31 -2
- package/docs/environment.md +1 -0
- package/docs/http-api.md +29 -0
- package/docs/mcp.md +13 -1
- package/docs/node-types.md +47 -1
- package/docs/screenshot.png +0 -0
- package/package.json +5 -4
- package/skills/pmx-canvas/SKILL.md +123 -25
- package/skills/pmx-canvas/evals/evals.json +54 -3
- package/skills/pmx-canvas/references/full-reference.md +11 -6
- 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/history.ts +1 -1
- package/src/cli/commands/nodes.ts +2 -2
- package/src/cli/commands/query.ts +1 -1
- package/src/cli/commands/smoke.ts +212 -0
- package/src/cli/commands/view.ts +1 -1
- package/src/cli/index.ts +7 -0
- package/src/cli/shared.ts +35 -5
- package/src/client/App.tsx +28 -15
- package/src/client/canvas/CanvasNode.tsx +54 -5
- package/src/client/canvas/CanvasViewport.tsx +6 -0
- package/src/client/canvas/EdgeLayer.tsx +44 -24
- package/src/client/canvas/ExpandedNodeOverlay.tsx +21 -0
- package/src/client/canvas/Minimap.tsx +2 -0
- package/src/client/canvas/auto-fit.ts +12 -1
- package/src/client/canvas/use-pan-zoom.ts +16 -1
- package/src/client/icons.tsx +4 -0
- package/src/client/mermaid-entry.ts +76 -0
- package/src/client/nodes/AxStepControls.tsx +226 -0
- package/src/client/nodes/DiffNode.tsx +114 -0
- package/src/client/nodes/ExtAppFrame.tsx +259 -84
- package/src/client/nodes/FileNode.tsx +249 -51
- package/src/client/nodes/MermaidNode.tsx +81 -0
- package/src/client/state/canvas-store.ts +19 -0
- package/src/client/state/intent-bridge.ts +32 -0
- package/src/client/state/node-factory.ts +2 -0
- package/src/client/state/sse-bridge.ts +32 -1
- package/src/client/theme/global.css +169 -3
- package/src/client/types.ts +6 -0
- package/src/mcp/server.ts +19 -1
- package/src/server/ax-flow-loop.ts +114 -0
- package/src/server/ax-interaction.ts +305 -4
- package/src/server/canvas-operations.ts +83 -26
- package/src/server/canvas-provenance.ts +2 -0
- package/src/server/canvas-schema.ts +33 -0
- package/src/server/canvas-state.ts +46 -10
- package/src/server/canvas-validation.ts +88 -0
- package/src/server/demo-state.json +4605 -523
- package/src/server/demo.ts +24 -0
- package/src/server/file-content.ts +152 -0
- package/src/server/file-watcher.ts +7 -10
- package/src/server/html-primitives.ts +636 -0
- package/src/server/html-surface.ts +11 -0
- package/src/server/index.ts +25 -6
- package/src/server/operations/composites.ts +3 -2
- package/src/server/operations/ops/ax-work.ts +5 -0
- package/src/server/operations/ops/batch.ts +8 -0
- 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 +15 -2
- package/src/server/server.ts +154 -6
- package/src/server/web-artifacts.ts +109 -0
- package/src/server/workboard.ts +159 -0
- package/src/shared/ax-flow.ts +102 -0
- package/src/shared/ax-surface-protocol.ts +8 -0
- package/src/shared/node-sizes.ts +52 -0
- package/src/shared/surface.ts +2 -0
- package/dist/types/server/chart-template.d.ts +0 -29
- package/src/server/chart-template.ts +0 -448
|
@@ -643,6 +643,10 @@ body,
|
|
|
643
643
|
overflow: hidden;
|
|
644
644
|
text-overflow: ellipsis;
|
|
645
645
|
flex: 1;
|
|
646
|
+
/* The name is the one thing that must survive a cramped title bar. Everything
|
|
647
|
+
else in the bar is flex-shrink: 0, so without a floor the title absorbed all
|
|
648
|
+
the pressure and collapsed to an ellipsis on narrow nodes. */
|
|
649
|
+
min-width: 9ch;
|
|
646
650
|
min-width: 0;
|
|
647
651
|
}
|
|
648
652
|
|
|
@@ -670,6 +674,13 @@ body,
|
|
|
670
674
|
}
|
|
671
675
|
|
|
672
676
|
.canvas-node .node-ax-status {
|
|
677
|
+
/* A long status ("in-progress") must not push the node's NAME out of the bar;
|
|
678
|
+
the pill is a glance-level cue, the name is the identity. */
|
|
679
|
+
max-width: 11ch;
|
|
680
|
+
overflow: hidden;
|
|
681
|
+
text-overflow: ellipsis;
|
|
682
|
+
white-space: nowrap;
|
|
683
|
+
flex-shrink: 1;
|
|
673
684
|
font-size: calc(9px * var(--node-chrome-scale, 1));
|
|
674
685
|
padding: calc(1px * var(--node-chrome-scale, 1)) calc(6px * var(--node-chrome-scale, 1));
|
|
675
686
|
border-radius: calc(4px * var(--node-chrome-scale, 1));
|
|
@@ -1846,10 +1857,11 @@ html.is-node-resizing .ext-app-preview-catcher {
|
|
|
1846
1857
|
border-radius: 999px;
|
|
1847
1858
|
}
|
|
1848
1859
|
|
|
1849
|
-
/* Edge labels
|
|
1860
|
+
/* Edge labels — --edge-chrome-scale is set on the edge <svg> so labels keep a
|
|
1861
|
+
constant screen size while zoomed out (see edgeChromeScale in EdgeLayer). */
|
|
1850
1862
|
.edge-label {
|
|
1851
1863
|
font-family: var(--font);
|
|
1852
|
-
font-size: 11px;
|
|
1864
|
+
font-size: calc(11px * var(--edge-chrome-scale, 1));
|
|
1853
1865
|
pointer-events: none;
|
|
1854
1866
|
user-select: none;
|
|
1855
1867
|
}
|
|
@@ -1857,7 +1869,7 @@ html.is-node-resizing .ext-app-preview-catcher {
|
|
|
1857
1869
|
.edge-label-bg {
|
|
1858
1870
|
fill: var(--c-panel-glass);
|
|
1859
1871
|
stroke: var(--c-line);
|
|
1860
|
-
stroke-width: 0.5;
|
|
1872
|
+
stroke-width: calc(0.5 * var(--edge-chrome-scale, 1));
|
|
1861
1873
|
}
|
|
1862
1874
|
|
|
1863
1875
|
/* Trace nodes — compact pills for agent tool/subagent visualization */
|
|
@@ -1895,6 +1907,160 @@ html.is-node-resizing .ext-app-preview-catcher {
|
|
|
1895
1907
|
}
|
|
1896
1908
|
}
|
|
1897
1909
|
|
|
1910
|
+
/* Diff nodes — unified diff viewer with per-file sections */
|
|
1911
|
+
.diff-node {
|
|
1912
|
+
display: flex;
|
|
1913
|
+
flex-direction: column;
|
|
1914
|
+
gap: 10px;
|
|
1915
|
+
height: 100%;
|
|
1916
|
+
overflow: auto;
|
|
1917
|
+
font-family: var(--mono);
|
|
1918
|
+
font-size: 12px;
|
|
1919
|
+
color: var(--c-text);
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
.diff-node.diff-node-empty {
|
|
1923
|
+
align-items: center;
|
|
1924
|
+
justify-content: center;
|
|
1925
|
+
color: var(--c-muted);
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
.diff-node .diff-file {
|
|
1929
|
+
border: 1px solid var(--c-line);
|
|
1930
|
+
border-radius: 6px;
|
|
1931
|
+
overflow: hidden;
|
|
1932
|
+
flex-shrink: 0;
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
.diff-node .diff-file-header {
|
|
1936
|
+
padding: 4px 8px;
|
|
1937
|
+
background: var(--c-panel-overlay);
|
|
1938
|
+
border-bottom: 1px solid var(--c-line);
|
|
1939
|
+
color: var(--c-text-soft);
|
|
1940
|
+
font-weight: 600;
|
|
1941
|
+
white-space: nowrap;
|
|
1942
|
+
overflow: hidden;
|
|
1943
|
+
text-overflow: ellipsis;
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
.diff-node .diff-lines {
|
|
1947
|
+
margin: 0;
|
|
1948
|
+
padding: 4px 0;
|
|
1949
|
+
overflow-x: auto;
|
|
1950
|
+
font-family: inherit;
|
|
1951
|
+
font-size: inherit;
|
|
1952
|
+
line-height: 1.5;
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
.diff-node .diff-lines > div {
|
|
1956
|
+
padding: 0 8px;
|
|
1957
|
+
white-space: pre;
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
.diff-node .diff-hunk {
|
|
1961
|
+
color: var(--c-muted);
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
.diff-node .diff-line-add {
|
|
1965
|
+
color: var(--c-ok);
|
|
1966
|
+
background: var(--c-ok-10);
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
.diff-node .diff-line-remove {
|
|
1970
|
+
color: var(--c-danger);
|
|
1971
|
+
background: var(--c-danger-12);
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
.diff-node .diff-line-context {
|
|
1975
|
+
color: var(--c-text-soft);
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
/* File nodes — delimited-data tables and non-text placeholders */
|
|
1979
|
+
.file-table-shell {
|
|
1980
|
+
display: flex;
|
|
1981
|
+
flex-direction: column;
|
|
1982
|
+
height: 100%;
|
|
1983
|
+
min-height: 0;
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
.file-node-notice {
|
|
1987
|
+
flex-shrink: 0;
|
|
1988
|
+
padding: 4px 10px;
|
|
1989
|
+
color: var(--c-muted);
|
|
1990
|
+
font-size: 10px;
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
/* Own scroll port so a wide CSV scrolls inside the node instead of blowing out
|
|
1994
|
+
the layout — and so the sticky header row has something to stick to. */
|
|
1995
|
+
.file-table-wrap {
|
|
1996
|
+
flex: 1;
|
|
1997
|
+
min-height: 0;
|
|
1998
|
+
overflow: auto;
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
/* The shell/wrap prefix is load-bearing: these rules must out-specify the markdown
|
|
2002
|
+
table styles (`.canvas-node .node-body table` / `th` / `td`), whose 0.5em
|
|
2003
|
+
margin and translucent header background break the sticky header. Borders are
|
|
2004
|
+
separate, not collapsed — collapsed borders belong to the table element, so
|
|
2005
|
+
the header leaks the rows scrolling beneath it. */
|
|
2006
|
+
.file-table-shell .file-table-wrap .file-table {
|
|
2007
|
+
margin: 0;
|
|
2008
|
+
border-collapse: separate;
|
|
2009
|
+
border-spacing: 0;
|
|
2010
|
+
width: max-content;
|
|
2011
|
+
min-width: 100%;
|
|
2012
|
+
font-family: var(--mono);
|
|
2013
|
+
color: var(--c-text);
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
.file-table-shell .file-table-wrap .file-table th,
|
|
2017
|
+
.file-table-shell .file-table-wrap .file-table td {
|
|
2018
|
+
max-width: 320px;
|
|
2019
|
+
padding: 3px 8px;
|
|
2020
|
+
border: none;
|
|
2021
|
+
border-right: 1px solid var(--c-line);
|
|
2022
|
+
border-bottom: 1px solid var(--c-line);
|
|
2023
|
+
text-align: left;
|
|
2024
|
+
white-space: nowrap;
|
|
2025
|
+
overflow: hidden;
|
|
2026
|
+
text-overflow: ellipsis;
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
.file-table-shell .file-table-wrap .file-table th {
|
|
2030
|
+
position: sticky;
|
|
2031
|
+
top: 0;
|
|
2032
|
+
z-index: 1;
|
|
2033
|
+
/* Opaque in every theme — rows must not bleed through while scrolling. */
|
|
2034
|
+
background: var(--c-panel);
|
|
2035
|
+
color: var(--c-text-soft);
|
|
2036
|
+
font-weight: 600;
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
.file-table-shell .file-table-wrap .file-table tbody tr:nth-child(even) {
|
|
2040
|
+
background: var(--c-surface-subtle);
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
.file-node-placeholder {
|
|
2044
|
+
display: flex;
|
|
2045
|
+
flex-direction: column;
|
|
2046
|
+
align-items: center;
|
|
2047
|
+
justify-content: center;
|
|
2048
|
+
gap: 4px;
|
|
2049
|
+
height: 100%;
|
|
2050
|
+
padding: 16px;
|
|
2051
|
+
color: var(--c-muted);
|
|
2052
|
+
text-align: center;
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
.file-node-placeholder-name {
|
|
2056
|
+
color: var(--c-text-soft);
|
|
2057
|
+
word-break: break-all;
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
.file-node-link {
|
|
2061
|
+
color: var(--c-accent);
|
|
2062
|
+
}
|
|
2063
|
+
|
|
1898
2064
|
/* Docked node — compact toolbar-flanking panel */
|
|
1899
2065
|
.docked-node {
|
|
1900
2066
|
min-width: 200px;
|
package/src/client/types.ts
CHANGED
|
@@ -21,6 +21,8 @@ export interface CanvasNodeState {
|
|
|
21
21
|
| 'ledger'
|
|
22
22
|
| 'trace'
|
|
23
23
|
| 'file'
|
|
24
|
+
| 'diff'
|
|
25
|
+
| 'mermaid'
|
|
24
26
|
| 'image'
|
|
25
27
|
| 'html'
|
|
26
28
|
| 'group';
|
|
@@ -77,6 +79,8 @@ export const TYPE_LABELS: Record<CanvasNodeState['type'], string> = {
|
|
|
77
79
|
ledger: 'LOG',
|
|
78
80
|
trace: 'TRACE',
|
|
79
81
|
file: 'FILE',
|
|
82
|
+
diff: 'DIFF',
|
|
83
|
+
mermaid: 'MERMAID',
|
|
80
84
|
image: 'IMG',
|
|
81
85
|
html: 'HTML',
|
|
82
86
|
group: 'GROUP',
|
|
@@ -92,6 +96,8 @@ export const EXPANDABLE_TYPES = new Set<CanvasNodeState['type']>([
|
|
|
92
96
|
'context',
|
|
93
97
|
'ledger',
|
|
94
98
|
'file',
|
|
99
|
+
'diff',
|
|
100
|
+
'mermaid',
|
|
95
101
|
'image',
|
|
96
102
|
'html',
|
|
97
103
|
]);
|
package/src/mcp/server.ts
CHANGED
|
@@ -27,7 +27,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
27
27
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
28
28
|
import { z } from 'zod';
|
|
29
29
|
import { AX_SOURCES } from '../server/operations/ops/ax-shared.js';
|
|
30
|
-
import { canvasState, describeCanvasSchema } from '../server/index.js';
|
|
30
|
+
import { canvasState, describeCanvasSchema, stopCanvasServer } from '../server/index.js';
|
|
31
31
|
import { AX_INTERACTION_TYPES } from '../server/ax-interaction.js';
|
|
32
32
|
import { buildPendingAxActivity } from '../server/ax-state.js';
|
|
33
33
|
import { registerOperationTools, registerCompositeTools } from '../server/operations/index.js';
|
|
@@ -884,6 +884,24 @@ export async function startMcpServer(): Promise<void> {
|
|
|
884
884
|
// Connect via stdio
|
|
885
885
|
const transport = new StdioServerTransport();
|
|
886
886
|
await server.connect(transport);
|
|
887
|
+
|
|
888
|
+
// Exit when the stdio channel closes (0.4.5 report Finding Y): without this,
|
|
889
|
+
// a client that completes the official close sequence leaves this process
|
|
890
|
+
// orphaned — the auto-started canvas server and its timers keep the event
|
|
891
|
+
// loop alive forever. stopCanvasServer() checkpoints the SQLite DB before
|
|
892
|
+
// exit and is a no-op when this process only attached to a remote daemon.
|
|
893
|
+
const shutdown = (): void => {
|
|
894
|
+
try {
|
|
895
|
+
stopCanvasServer();
|
|
896
|
+
} catch {
|
|
897
|
+
// Exit regardless — a failed cleanup must not resurrect the orphan.
|
|
898
|
+
}
|
|
899
|
+
process.exit(0);
|
|
900
|
+
};
|
|
901
|
+
server.server.onclose = shutdown;
|
|
902
|
+
process.stdin.on('end', shutdown);
|
|
903
|
+
process.on('SIGINT', shutdown);
|
|
904
|
+
process.on('SIGTERM', shutdown);
|
|
887
905
|
}
|
|
888
906
|
|
|
889
907
|
// Allow direct execution: bun run src/mcp/server.ts
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side bounded loop for materialized AX flows.
|
|
3
|
+
*
|
|
4
|
+
* A flow's loop lives on the CANVAS, not in a panel: the anchor step node carries
|
|
5
|
+
* `data.axFlow.loop` (see shared/ax-flow.ts), so it survives a browser refresh, an
|
|
6
|
+
* iframe reload, and a server restart — where a panel-driven loop dies with its
|
|
7
|
+
* document. The driver is `canvasState`'s work-item change hook: whenever a step's
|
|
8
|
+
* work item reaches `done`, the next step is opened; when the LAST step finishes,
|
|
9
|
+
* the next run starts — until the run cap is reached or a human presses Stop.
|
|
10
|
+
*
|
|
11
|
+
* Bounds (non-negotiable, enforced here rather than trusted from node data):
|
|
12
|
+
* - advances only while `loop.running` is true
|
|
13
|
+
* - stops at `maxRuns`, itself clamped to AX_FLOW_LOOP_HARD_CAP (20)
|
|
14
|
+
* - a `blocked`/`cancelled` step halts advancement (the loop waits, it does not
|
|
15
|
+
* skip); a step whose work item no longer exists stops the loop outright
|
|
16
|
+
* - re-entrancy: every write this module makes fires the same work-item change
|
|
17
|
+
* hook, so `advancing` drops the nested call. State is recomputed from scratch
|
|
18
|
+
* on each pass, so dropping nested calls loses nothing.
|
|
19
|
+
*/
|
|
20
|
+
import { AX_FLOW_LOOP_HARD_CAP, readAxFlow, type AxFlowLoopState, type AxFlowStamp } from '../shared/ax-flow.js';
|
|
21
|
+
import type { PmxAxWorkItem } from './ax-state.js';
|
|
22
|
+
import { emitCanvasLayoutUpdate } from './canvas-operations.js';
|
|
23
|
+
import { canvasState, type CanvasNodeState } from './canvas-state.js';
|
|
24
|
+
import { refreshWorkboardNodes } from './workboard.js';
|
|
25
|
+
|
|
26
|
+
/** Re-entrancy guard: true while an advance pass is writing. */
|
|
27
|
+
let advancing = false;
|
|
28
|
+
|
|
29
|
+
/** Persist a new loop state on the anchor node (durable: this is what Stop writes). */
|
|
30
|
+
function writeLoopState(node: CanvasNodeState, flow: AxFlowStamp, loop: AxFlowLoopState): void {
|
|
31
|
+
const current = canvasState.getNode(node.id);
|
|
32
|
+
if (!current) return;
|
|
33
|
+
canvasState.updateNode(node.id, { data: { ...current.data, axFlow: { ...flow, loop } } });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Advance one flow by at most one step (or one run boundary). Returns true when
|
|
38
|
+
* anything changed, so the caller knows whether to broadcast a layout update.
|
|
39
|
+
*/
|
|
40
|
+
function advanceFlow(node: CanvasNodeState, flow: AxFlowStamp, byId: Map<string, PmxAxWorkItem>): boolean {
|
|
41
|
+
const items = flow.steps.map((step) => byId.get(step.workItemId));
|
|
42
|
+
if (items.some((item) => !item)) {
|
|
43
|
+
// The flow was partially dismantled (work items cleared/deleted). Stop rather
|
|
44
|
+
// than keep a loop armed against state it can no longer drive.
|
|
45
|
+
writeLoopState(node, flow, { ...flow.loop, running: false });
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
const statuses = items as PmxAxWorkItem[];
|
|
49
|
+
// A blocked/cancelled step is a deliberate halt — hold position, stay armed.
|
|
50
|
+
if (statuses.some((item) => item.status === 'blocked' || item.status === 'cancelled')) return false;
|
|
51
|
+
|
|
52
|
+
const openIndex = statuses.findIndex((item) => item.status !== 'done');
|
|
53
|
+
if (openIndex >= 0) {
|
|
54
|
+
// Every earlier step is done by construction. Open this one exactly once:
|
|
55
|
+
// an already in-progress step is the loop's own previous advance.
|
|
56
|
+
if (statuses[openIndex].status !== 'todo') return false;
|
|
57
|
+
canvasState.updateWorkItem(statuses[openIndex].id, { status: 'in-progress' }, { source: 'system' });
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Every step is done — one full run just completed.
|
|
62
|
+
const maxRuns = Math.min(flow.loop.maxRuns, AX_FLOW_LOOP_HARD_CAP);
|
|
63
|
+
const run = flow.loop.run + 1;
|
|
64
|
+
if (run >= maxRuns) {
|
|
65
|
+
// The cap is reached WITH this run recorded: no run `maxRuns + 1` is opened.
|
|
66
|
+
writeLoopState(node, flow, { running: false, run, maxRuns });
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
// Write the counter before re-opening the steps, so a crash mid-pass can never
|
|
70
|
+
// replay a run for free.
|
|
71
|
+
writeLoopState(node, flow, { running: true, run, maxRuns });
|
|
72
|
+
for (const step of flow.steps) {
|
|
73
|
+
canvasState.updateWorkItem(step.workItemId, { status: 'todo' }, { source: 'system' });
|
|
74
|
+
}
|
|
75
|
+
canvasState.updateWorkItem(flow.steps[0].workItemId, { status: 'in-progress' }, { source: 'system' });
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Advance every running flow loop on the canvas. Safe to call on any work-item
|
|
81
|
+
* change: flows that are not running, not ready, or already advanced are no-ops.
|
|
82
|
+
*/
|
|
83
|
+
export function advanceAxFlowLoops(): void {
|
|
84
|
+
if (advancing) return;
|
|
85
|
+
advancing = true;
|
|
86
|
+
try {
|
|
87
|
+
const anchors = canvasState
|
|
88
|
+
.getLayout()
|
|
89
|
+
.nodes.map((node) => ({ node, flow: readAxFlow(node.data) }))
|
|
90
|
+
.filter((entry): entry is { node: CanvasNodeState; flow: AxFlowStamp } => entry.flow?.loop.running === true);
|
|
91
|
+
if (anchors.length === 0) return;
|
|
92
|
+
const byId = new Map(canvasState.getWorkItems().map((item) => [item.id, item]));
|
|
93
|
+
let changed = false;
|
|
94
|
+
// Suppressed: loop bookkeeping (status chips, the run counter) is automatic
|
|
95
|
+
// and must not fill undo history — same contract as the live workboard.
|
|
96
|
+
canvasState.withSuppressedRecording(() => {
|
|
97
|
+
for (const { node, flow } of anchors) {
|
|
98
|
+
if (advanceFlow(node, flow, byId)) changed = true;
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
if (changed) emitCanvasLayoutUpdate();
|
|
102
|
+
} finally {
|
|
103
|
+
advancing = false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// canvasState exposes ONE work-item listener slot, so this module owns the
|
|
108
|
+
// composition: the live workboard refresh first (unchanged behaviour), then the
|
|
109
|
+
// flow advance. Importing workboard also guarantees ordering — its own
|
|
110
|
+
// registration runs while this module is still evaluating, so this one wins.
|
|
111
|
+
canvasState.setWorkItemsChangedListener(() => {
|
|
112
|
+
refreshWorkboardNodes();
|
|
113
|
+
advanceAxFlowLoops();
|
|
114
|
+
});
|