agentweaver 0.1.9 → 0.1.10
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/README.md +60 -28
- package/dist/artifacts.js +1 -1
- package/dist/errors.js +7 -0
- package/dist/index.js +66 -34
- package/dist/interactive-ui.js +351 -44
- package/dist/pipeline/declarative-flows.js +7 -4
- package/dist/pipeline/flow-catalog.js +28 -21
- package/dist/pipeline/flow-specs/opencode/auto-opencode.json +1365 -0
- package/dist/pipeline/flow-specs/opencode/bugz/bug-analyze-opencode.json +382 -0
- package/dist/pipeline/flow-specs/opencode/bugz/bug-fix-opencode.json +56 -0
- package/dist/pipeline/flow-specs/opencode/gitlab/gitlab-diff-review-opencode.json +308 -0
- package/dist/pipeline/flow-specs/opencode/gitlab/gitlab-review-opencode.json +437 -0
- package/dist/pipeline/flow-specs/opencode/gitlab/mr-description-opencode.json +117 -0
- package/dist/pipeline/flow-specs/opencode/go/run-go-linter-loop-opencode.json +321 -0
- package/dist/pipeline/flow-specs/opencode/go/run-go-tests-loop-opencode.json +321 -0
- package/dist/pipeline/flow-specs/opencode/implement-opencode.json +64 -0
- package/dist/pipeline/flow-specs/{plan-opencode.json → opencode/plan-opencode.json} +4 -4
- package/dist/pipeline/flow-specs/opencode/review/review-fix-opencode.json +209 -0
- package/dist/pipeline/flow-specs/opencode/review/review-opencode.json +452 -0
- package/dist/pipeline/flow-specs/opencode/task-describe-opencode.json +148 -0
- package/dist/pipeline/spec-loader.js +18 -7
- package/dist/runtime/process-runner.js +45 -1
- package/package.json +1 -1
- package/dist/pipeline/flow-specs/preflight.json +0 -206
- package/dist/pipeline/flow-specs/run-linter-loop.json +0 -155
- package/dist/pipeline/flow-specs/run-tests-loop.json +0 -155
package/dist/interactive-ui.js
CHANGED
|
@@ -2,10 +2,104 @@ import path from "node:path";
|
|
|
2
2
|
import blessed from "neo-blessed";
|
|
3
3
|
import { renderMarkdownToTerminal } from "./markdown.js";
|
|
4
4
|
import { setOutputAdapter, stripAnsi } from "./tui.js";
|
|
5
|
-
import { TaskRunnerError } from "./errors.js";
|
|
5
|
+
import { FlowInterruptedError, TaskRunnerError } from "./errors.js";
|
|
6
6
|
import { buildInitialUserInputValues, validateUserInputValues, } from "./user-input.js";
|
|
7
7
|
const CONFIRM_MIN_WIDTH = 44;
|
|
8
8
|
const CONFIRM_MIN_HEIGHT = 8;
|
|
9
|
+
function compareTreeNames(left, right) {
|
|
10
|
+
return left.localeCompare(right, "ru");
|
|
11
|
+
}
|
|
12
|
+
function makeFolderKey(pathSegments) {
|
|
13
|
+
return `folder:${pathSegments.join("/")}`;
|
|
14
|
+
}
|
|
15
|
+
function makeFlowKey(flowId) {
|
|
16
|
+
return `flow:${flowId}`;
|
|
17
|
+
}
|
|
18
|
+
function buildFlowTree(flows) {
|
|
19
|
+
const roots = new Map();
|
|
20
|
+
const ensureFolder = (pathSegments) => {
|
|
21
|
+
const firstSegment = pathSegments[0];
|
|
22
|
+
if (!firstSegment) {
|
|
23
|
+
throw new Error("Flow tree folder path cannot be empty.");
|
|
24
|
+
}
|
|
25
|
+
const rootFolder = roots.get(firstSegment);
|
|
26
|
+
let currentFolder;
|
|
27
|
+
if (rootFolder) {
|
|
28
|
+
currentFolder = rootFolder;
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
currentFolder = {
|
|
32
|
+
kind: "folder",
|
|
33
|
+
key: makeFolderKey([firstSegment]),
|
|
34
|
+
name: firstSegment,
|
|
35
|
+
pathSegments: [firstSegment],
|
|
36
|
+
children: [],
|
|
37
|
+
};
|
|
38
|
+
roots.set(firstSegment, currentFolder);
|
|
39
|
+
}
|
|
40
|
+
for (let index = 1; index < pathSegments.length; index += 1) {
|
|
41
|
+
const segment = pathSegments[index] ?? "";
|
|
42
|
+
const folderPath = pathSegments.slice(0, index + 1);
|
|
43
|
+
let nextFolder = currentFolder.children.find((child) => child.kind === "folder" && child.name === segment);
|
|
44
|
+
if (!nextFolder) {
|
|
45
|
+
nextFolder = {
|
|
46
|
+
kind: "folder",
|
|
47
|
+
key: makeFolderKey(folderPath),
|
|
48
|
+
name: segment,
|
|
49
|
+
pathSegments: folderPath,
|
|
50
|
+
children: [],
|
|
51
|
+
};
|
|
52
|
+
currentFolder.children.push(nextFolder);
|
|
53
|
+
}
|
|
54
|
+
currentFolder = nextFolder;
|
|
55
|
+
}
|
|
56
|
+
return currentFolder;
|
|
57
|
+
};
|
|
58
|
+
for (const flow of flows) {
|
|
59
|
+
if (flow.treePath.length === 0) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const folderPath = flow.treePath.slice(0, -1);
|
|
63
|
+
const leafName = flow.treePath[flow.treePath.length - 1] ?? flow.id;
|
|
64
|
+
const parent = ensureFolder(folderPath);
|
|
65
|
+
parent.children.push({
|
|
66
|
+
kind: "flow",
|
|
67
|
+
key: makeFlowKey(flow.id),
|
|
68
|
+
name: leafName,
|
|
69
|
+
pathSegments: [...flow.treePath],
|
|
70
|
+
flow,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
const sortNodes = (nodes) => [...nodes]
|
|
74
|
+
.sort((left, right) => {
|
|
75
|
+
if (left.kind !== right.kind) {
|
|
76
|
+
return left.kind === "folder" ? -1 : 1;
|
|
77
|
+
}
|
|
78
|
+
return compareTreeNames(left.name, right.name);
|
|
79
|
+
})
|
|
80
|
+
.map((node) => node.kind === "folder"
|
|
81
|
+
? {
|
|
82
|
+
...node,
|
|
83
|
+
children: sortNodes(node.children),
|
|
84
|
+
}
|
|
85
|
+
: node);
|
|
86
|
+
const orderedRootNames = ["custom", "default"];
|
|
87
|
+
const sortedRoots = [...roots.values()].sort((left, right) => {
|
|
88
|
+
const leftIndex = orderedRootNames.indexOf(left.name);
|
|
89
|
+
const rightIndex = orderedRootNames.indexOf(right.name);
|
|
90
|
+
if (leftIndex !== -1 || rightIndex !== -1) {
|
|
91
|
+
if (leftIndex === -1) {
|
|
92
|
+
return 1;
|
|
93
|
+
}
|
|
94
|
+
if (rightIndex === -1) {
|
|
95
|
+
return -1;
|
|
96
|
+
}
|
|
97
|
+
return leftIndex - rightIndex;
|
|
98
|
+
}
|
|
99
|
+
return compareTreeNames(left.name, right.name);
|
|
100
|
+
});
|
|
101
|
+
return sortNodes(sortedRoots);
|
|
102
|
+
}
|
|
9
103
|
export class InteractiveUi {
|
|
10
104
|
options;
|
|
11
105
|
screen;
|
|
@@ -21,9 +115,13 @@ export class InteractiveUi {
|
|
|
21
115
|
confirm;
|
|
22
116
|
formModal;
|
|
23
117
|
flowMap;
|
|
118
|
+
flowTree;
|
|
119
|
+
expandedFlowFolders = new Set();
|
|
120
|
+
visibleFlowItems = [];
|
|
24
121
|
busy = false;
|
|
25
122
|
currentFlowId = null;
|
|
26
123
|
selectedFlowId;
|
|
124
|
+
selectedFlowItemKey;
|
|
27
125
|
summaryText = "";
|
|
28
126
|
focusedPane = "flows";
|
|
29
127
|
currentNode = null;
|
|
@@ -50,7 +148,10 @@ export class InteractiveUi {
|
|
|
50
148
|
throw new Error("Interactive UI requires at least one flow.");
|
|
51
149
|
}
|
|
52
150
|
this.flowMap = new Map(options.flows.map((flow) => [flow.id, flow]));
|
|
151
|
+
this.flowTree = buildFlowTree(options.flows);
|
|
53
152
|
this.selectedFlowId = options.flows[0]?.id ?? "auto";
|
|
153
|
+
this.visibleFlowItems = this.computeVisibleFlowItems();
|
|
154
|
+
this.selectedFlowItemKey = this.visibleFlowItems[0]?.key ?? makeFlowKey(this.selectedFlowId);
|
|
54
155
|
this.scopeKey = options.scopeKey;
|
|
55
156
|
this.jiraIssueKey = options.jiraIssueKey ?? null;
|
|
56
157
|
this.summaryVisible = options.summaryText.trim().length > 0;
|
|
@@ -338,6 +439,10 @@ export class InteractiveUi {
|
|
|
338
439
|
this.requestRender();
|
|
339
440
|
});
|
|
340
441
|
this.screen.key(["escape"], () => {
|
|
442
|
+
if (this.busy && this.confirm.hidden && this.help.hidden) {
|
|
443
|
+
this.openInterruptConfirm();
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
341
446
|
if (this.hasActiveForm()) {
|
|
342
447
|
this.cancelActiveForm();
|
|
343
448
|
return;
|
|
@@ -375,11 +480,15 @@ export class InteractiveUi {
|
|
|
375
480
|
if (this.hasActiveForm()) {
|
|
376
481
|
return;
|
|
377
482
|
}
|
|
378
|
-
const
|
|
379
|
-
if (!
|
|
483
|
+
const selectedItem = this.visibleFlowItems[index];
|
|
484
|
+
if (!selectedItem) {
|
|
380
485
|
return;
|
|
381
486
|
}
|
|
382
|
-
this.
|
|
487
|
+
this.selectedFlowItemKey = selectedItem.key;
|
|
488
|
+
if (selectedItem.kind === "flow") {
|
|
489
|
+
this.selectedFlowId = selectedItem.flow.id;
|
|
490
|
+
}
|
|
491
|
+
this.updateHeader();
|
|
383
492
|
this.renderDescription();
|
|
384
493
|
this.renderProgress();
|
|
385
494
|
this.requestRender();
|
|
@@ -388,8 +497,25 @@ export class InteractiveUi {
|
|
|
388
497
|
if (this.busy || this.confirm.visible || !this.help.hidden || this.hasActiveForm()) {
|
|
389
498
|
return;
|
|
390
499
|
}
|
|
500
|
+
const selectedItem = this.selectedFlowTreeItem();
|
|
501
|
+
if (selectedItem?.kind === "folder") {
|
|
502
|
+
this.toggleFlowFolder(selectedItem.key);
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
391
505
|
await this.openConfirm();
|
|
392
506
|
});
|
|
507
|
+
this.flowList.key(["right"], () => {
|
|
508
|
+
if (this.hasActiveForm()) {
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
this.expandSelectedFlowFolder();
|
|
512
|
+
});
|
|
513
|
+
this.flowList.key(["left"], () => {
|
|
514
|
+
if (this.hasActiveForm()) {
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
this.collapseSelectedFlowFolderOrSelectParent();
|
|
518
|
+
});
|
|
393
519
|
this.flowList.key(["pageup"], () => {
|
|
394
520
|
if (this.hasActiveForm()) {
|
|
395
521
|
return;
|
|
@@ -475,20 +601,29 @@ export class InteractiveUi {
|
|
|
475
601
|
this.requestRender();
|
|
476
602
|
});
|
|
477
603
|
this.confirm.key(["enter"], async () => {
|
|
478
|
-
if (this.
|
|
604
|
+
if (this.confirm.hidden || !this.confirmSession) {
|
|
479
605
|
return;
|
|
480
606
|
}
|
|
481
|
-
|
|
607
|
+
if (this.hasActiveForm() && this.confirmSession.kind !== "interrupt") {
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
const { flowId, selectedAction, kind } = this.confirmSession;
|
|
482
611
|
if (selectedAction === "cancel") {
|
|
483
612
|
this.closeConfirm();
|
|
484
613
|
return;
|
|
485
614
|
}
|
|
615
|
+
if (kind === "interrupt") {
|
|
616
|
+
this.closeConfirm();
|
|
617
|
+
await this.options.onInterrupt(flowId);
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
486
620
|
this.closeConfirm();
|
|
487
621
|
this.setBusy(true, flowId);
|
|
488
622
|
this.clearFlowFailure(flowId);
|
|
489
623
|
this.setFlowDisplayState(flowId, null);
|
|
490
624
|
try {
|
|
491
|
-
|
|
625
|
+
const launchMode = selectedAction === "resume" ? "resume" : "restart";
|
|
626
|
+
await this.options.onRun(flowId, launchMode);
|
|
492
627
|
}
|
|
493
628
|
finally {
|
|
494
629
|
this.setBusy(false);
|
|
@@ -547,7 +682,7 @@ export class InteractiveUi {
|
|
|
547
682
|
else {
|
|
548
683
|
this.log.focus();
|
|
549
684
|
}
|
|
550
|
-
this.footer.setContent(` Focus: ${pane} | Up/Down: select
|
|
685
|
+
this.footer.setContent(` Focus: ${pane} | Up/Down: select | Left/Right: fold | Enter: toggle/run | h: help | Esc: close/interrupt | Tab: switch pane | q: exit `);
|
|
551
686
|
this.requestRender();
|
|
552
687
|
}
|
|
553
688
|
renderStaticContent() {
|
|
@@ -555,8 +690,7 @@ export class InteractiveUi {
|
|
|
555
690
|
this.summaryVisible = this.summaryText.length > 0;
|
|
556
691
|
this.applyRightPaneLayout();
|
|
557
692
|
this.updateHeader();
|
|
558
|
-
this.
|
|
559
|
-
this.flowList.select(this.options.flows.findIndex((flow) => flow.id === this.selectedFlowId));
|
|
693
|
+
this.renderFlowTreeList();
|
|
560
694
|
this.renderDescription();
|
|
561
695
|
this.renderSummary();
|
|
562
696
|
this.renderProgress();
|
|
@@ -564,22 +698,24 @@ export class InteractiveUi {
|
|
|
564
698
|
"AgentWeaver interactive mode",
|
|
565
699
|
"",
|
|
566
700
|
"Клавиши:",
|
|
567
|
-
"Up / Down выбрать flow",
|
|
568
|
-
"
|
|
701
|
+
"Up / Down выбрать папку или flow",
|
|
702
|
+
"Right раскрыть папку",
|
|
703
|
+
"Left свернуть папку или перейти к родителю",
|
|
704
|
+
"Enter раскрыть папку или открыть запуск flow",
|
|
569
705
|
"Enter подтвердить запуск в модалке",
|
|
570
|
-
"Esc закрыть help или
|
|
571
|
-
"
|
|
706
|
+
"Esc закрыть help/модалку или прервать running flow",
|
|
707
|
+
"F1 открыть или закрыть help",
|
|
572
708
|
"Tab переключить pane",
|
|
573
709
|
"Ctrl+L очистить лог",
|
|
574
710
|
"q / Ctrl+C выйти",
|
|
575
711
|
"",
|
|
576
712
|
"Доступные flow:",
|
|
577
|
-
...this.options.flows.map((flow) => flow.
|
|
713
|
+
...this.options.flows.map((flow) => flow.treePath.join("/")),
|
|
578
714
|
].join("\n")));
|
|
579
|
-
this.footer.setContent(" Up/Down: select
|
|
715
|
+
this.footer.setContent(" Up/Down: select | Left/Right: fold | Enter: toggle/run | Esc: close/interrupt | h: help | Tab: switch pane | q: exit ");
|
|
580
716
|
}
|
|
581
717
|
updateHeader() {
|
|
582
|
-
const current = this.currentFlowId ?? this.
|
|
718
|
+
const current = this.currentFlowId ?? this.selectedHeaderLabel();
|
|
583
719
|
const pathParts = this.options.cwd.split(path.sep).filter(Boolean);
|
|
584
720
|
const folderName = pathParts.slice(-3).join("/") || this.options.cwd;
|
|
585
721
|
const branchLabel = this.options.gitBranchName ? this.options.gitBranchName : "detached-head";
|
|
@@ -626,7 +762,7 @@ export class InteractiveUi {
|
|
|
626
762
|
renderActiveForm() {
|
|
627
763
|
if (!this.activeFormSession) {
|
|
628
764
|
this.formModal.hide();
|
|
629
|
-
this.footer.setContent(" Up/Down: select
|
|
765
|
+
this.footer.setContent(" Up/Down: select | Left/Right: fold | Enter: toggle/run | Esc: close/interrupt | h: help | Tab: switch pane | q: exit ");
|
|
630
766
|
this.requestRender();
|
|
631
767
|
return;
|
|
632
768
|
}
|
|
@@ -796,6 +932,17 @@ export class InteractiveUi {
|
|
|
796
932
|
session.reject(new TaskRunnerError(`User cancelled form '${session.form.formId}'.`));
|
|
797
933
|
this.renderActiveForm();
|
|
798
934
|
}
|
|
935
|
+
interruptActiveForm(message = "Flow interrupted by user.") {
|
|
936
|
+
if (!this.activeFormSession) {
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
const session = this.activeFormSession;
|
|
940
|
+
this.activeFormSession = null;
|
|
941
|
+
this.formModal.hide();
|
|
942
|
+
this.focusPane("flows");
|
|
943
|
+
session.reject(new FlowInterruptedError(message));
|
|
944
|
+
this.renderActiveForm();
|
|
945
|
+
}
|
|
799
946
|
handleActiveFormKey(ch, key) {
|
|
800
947
|
const field = this.currentFormField();
|
|
801
948
|
if (!field) {
|
|
@@ -855,22 +1002,33 @@ export class InteractiveUi {
|
|
|
855
1002
|
}
|
|
856
1003
|
}
|
|
857
1004
|
renderDescription() {
|
|
858
|
-
const
|
|
859
|
-
|
|
1005
|
+
const selectedItem = this.selectedFlowTreeItem();
|
|
1006
|
+
if (!selectedItem) {
|
|
1007
|
+
this.description.setContent("Flow structure is not available.");
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
if (selectedItem.kind === "folder") {
|
|
1011
|
+
const kindLabel = selectedItem.pathSegments[0] === "custom" ? "project-local" : "built-in";
|
|
1012
|
+
const folderDescription = [
|
|
1013
|
+
`Папка flow \`${selectedItem.pathSegments.join("/")}\`.`,
|
|
1014
|
+
"",
|
|
1015
|
+
`Источник: ${kindLabel}`,
|
|
1016
|
+
`Статус: ${this.expandedFlowFolders.has(selectedItem.key) ? "развёрнута" : "свёрнута"}`,
|
|
1017
|
+
].join("\n");
|
|
1018
|
+
this.description.setContent(renderMarkdownToTerminal(stripAnsi(folderDescription)));
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
const { flow } = selectedItem;
|
|
1022
|
+
const description = flow.description?.trim() || "Описание для этого flow пока не задано.";
|
|
860
1023
|
const details = [
|
|
861
|
-
|
|
862
|
-
flow
|
|
1024
|
+
`Путь: ${flow.treePath.join("/")}`,
|
|
1025
|
+
`Источник: ${flow.source === "project-local" ? "project-local" : "built-in"}`,
|
|
1026
|
+
flow.source === "project-local" && flow.sourcePath ? `Файл: ${flow.sourcePath}` : "",
|
|
863
1027
|
]
|
|
864
1028
|
.filter((line) => line.length > 0)
|
|
865
1029
|
.join("\n");
|
|
866
1030
|
this.description.setContent(renderMarkdownToTerminal(stripAnsi(details ? `${description}\n\n${details}` : description)));
|
|
867
1031
|
}
|
|
868
|
-
renderFlowListLabel(flow) {
|
|
869
|
-
if (flow.source === "project-local") {
|
|
870
|
-
return `{yellow-fg}${flow.label}{/yellow-fg}`;
|
|
871
|
-
}
|
|
872
|
-
return flow.label;
|
|
873
|
-
}
|
|
874
1032
|
createAdapter() {
|
|
875
1033
|
return {
|
|
876
1034
|
writeStdout: (text) => {
|
|
@@ -897,13 +1055,19 @@ export class InteractiveUi {
|
|
|
897
1055
|
return this.currentFlowId ?? this.selectedFlowId;
|
|
898
1056
|
}
|
|
899
1057
|
progressFlowDefinition() {
|
|
900
|
-
|
|
901
|
-
|
|
1058
|
+
if (this.busy) {
|
|
1059
|
+
return this.flowMap.get(this.activeFlowId());
|
|
1060
|
+
}
|
|
1061
|
+
const selectedItem = this.selectedFlowTreeItem();
|
|
1062
|
+
if (selectedItem?.kind === "flow") {
|
|
1063
|
+
return selectedItem.flow;
|
|
1064
|
+
}
|
|
1065
|
+
return undefined;
|
|
902
1066
|
}
|
|
903
1067
|
renderProgress() {
|
|
904
1068
|
const flow = this.progressFlowDefinition();
|
|
905
1069
|
if (!flow) {
|
|
906
|
-
this.progress.setContent("
|
|
1070
|
+
this.progress.setContent("Выберите конкретный flow в дереве, чтобы увидеть его прогресс.");
|
|
907
1071
|
return;
|
|
908
1072
|
}
|
|
909
1073
|
const flowState = this.flowState.flowId === flow.id
|
|
@@ -1165,34 +1329,47 @@ export class InteractiveUi {
|
|
|
1165
1329
|
if (!this.confirmSession) {
|
|
1166
1330
|
return;
|
|
1167
1331
|
}
|
|
1168
|
-
const actions = this.
|
|
1169
|
-
? ["resume", "restart", "cancel"]
|
|
1170
|
-
: this.confirmSession.hasExistingState
|
|
1171
|
-
? ["restart", "cancel"]
|
|
1172
|
-
: ["ok", "cancel"];
|
|
1332
|
+
const actions = this.confirmActions();
|
|
1173
1333
|
const currentIndex = actions.indexOf(this.confirmSession.selectedAction);
|
|
1174
1334
|
const nextIndex = (currentIndex + delta + actions.length) % actions.length;
|
|
1175
1335
|
this.confirmSession.selectedAction = (actions[nextIndex] ?? "cancel");
|
|
1176
1336
|
this.renderConfirm();
|
|
1177
1337
|
}
|
|
1338
|
+
confirmActions() {
|
|
1339
|
+
if (!this.confirmSession) {
|
|
1340
|
+
return ["cancel"];
|
|
1341
|
+
}
|
|
1342
|
+
if (this.confirmSession.kind === "interrupt") {
|
|
1343
|
+
return ["stop", "cancel"];
|
|
1344
|
+
}
|
|
1345
|
+
return this.confirmSession.resumeAvailable
|
|
1346
|
+
? ["resume", "restart", "cancel"]
|
|
1347
|
+
: this.confirmSession.hasExistingState
|
|
1348
|
+
? ["restart", "cancel"]
|
|
1349
|
+
: ["ok", "cancel"];
|
|
1350
|
+
}
|
|
1178
1351
|
renderConfirm() {
|
|
1179
1352
|
const session = this.confirmSession;
|
|
1180
1353
|
if (!session) {
|
|
1181
1354
|
return;
|
|
1182
1355
|
}
|
|
1183
1356
|
const flow = this.flowMap.get(session.flowId);
|
|
1184
|
-
const actions =
|
|
1185
|
-
? ["resume", "restart", "cancel"]
|
|
1186
|
-
: session.hasExistingState
|
|
1187
|
-
? ["restart", "cancel"]
|
|
1188
|
-
: ["ok", "cancel"];
|
|
1357
|
+
const actions = this.confirmActions();
|
|
1189
1358
|
const actionLabels = actions
|
|
1190
1359
|
.map((action) => {
|
|
1191
|
-
const label = action === "
|
|
1360
|
+
const label = action === "stop"
|
|
1361
|
+
? "Stop"
|
|
1362
|
+
: action === "resume"
|
|
1363
|
+
? "Resume"
|
|
1364
|
+
: action === "restart"
|
|
1365
|
+
? "Restart"
|
|
1366
|
+
: action === "ok"
|
|
1367
|
+
? "OK"
|
|
1368
|
+
: "Cancel";
|
|
1192
1369
|
return session.selectedAction === action ? `[ ${label} ]` : ` ${label} `;
|
|
1193
1370
|
})
|
|
1194
1371
|
.join(" ");
|
|
1195
|
-
const lines = [`Run flow "${flow?.label ?? session.flowId}"?`];
|
|
1372
|
+
const lines = [session.kind === "interrupt" ? `Interrupt flow "${flow?.label ?? session.flowId}"?` : `Run flow "${flow?.label ?? session.flowId}"?`];
|
|
1196
1373
|
if (session.details?.trim()) {
|
|
1197
1374
|
lines.push("", session.details.trim());
|
|
1198
1375
|
}
|
|
@@ -1214,12 +1391,17 @@ export class InteractiveUi {
|
|
|
1214
1391
|
this.requestRender();
|
|
1215
1392
|
}
|
|
1216
1393
|
async openConfirm() {
|
|
1217
|
-
const
|
|
1394
|
+
const selectedItem = this.selectedFlowTreeItem();
|
|
1395
|
+
if (!selectedItem || selectedItem.kind !== "flow") {
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
const flow = selectedItem.flow;
|
|
1218
1399
|
if (!flow) {
|
|
1219
1400
|
return;
|
|
1220
1401
|
}
|
|
1221
1402
|
const confirmation = await this.options.getRunConfirmation(flow.id);
|
|
1222
1403
|
this.confirmSession = {
|
|
1404
|
+
kind: "run",
|
|
1223
1405
|
flowId: flow.id,
|
|
1224
1406
|
resumeAvailable: confirmation.resumeAvailable,
|
|
1225
1407
|
hasExistingState: confirmation.hasExistingState,
|
|
@@ -1228,6 +1410,21 @@ export class InteractiveUi {
|
|
|
1228
1410
|
};
|
|
1229
1411
|
this.renderConfirm();
|
|
1230
1412
|
}
|
|
1413
|
+
openInterruptConfirm() {
|
|
1414
|
+
const flowId = this.currentFlowId;
|
|
1415
|
+
if (!flowId || this.confirm.visible) {
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1418
|
+
this.confirmSession = {
|
|
1419
|
+
kind: "interrupt",
|
|
1420
|
+
flowId,
|
|
1421
|
+
resumeAvailable: true,
|
|
1422
|
+
hasExistingState: true,
|
|
1423
|
+
details: "Текущий flow будет остановлен. Состояние сохранится, и его можно будет продолжить через Resume.",
|
|
1424
|
+
selectedAction: "stop",
|
|
1425
|
+
};
|
|
1426
|
+
this.renderConfirm();
|
|
1427
|
+
}
|
|
1231
1428
|
closeConfirm() {
|
|
1232
1429
|
this.confirmSession = null;
|
|
1233
1430
|
this.confirm.hide();
|
|
@@ -1364,6 +1561,116 @@ export class InteractiveUi {
|
|
|
1364
1561
|
this.renderProgress();
|
|
1365
1562
|
this.requestRender();
|
|
1366
1563
|
}
|
|
1564
|
+
computeVisibleFlowItems() {
|
|
1565
|
+
const items = [];
|
|
1566
|
+
const walk = (nodes, depth) => {
|
|
1567
|
+
for (const node of nodes) {
|
|
1568
|
+
if (node.kind === "folder") {
|
|
1569
|
+
items.push({
|
|
1570
|
+
kind: "folder",
|
|
1571
|
+
key: node.key,
|
|
1572
|
+
name: node.name,
|
|
1573
|
+
depth,
|
|
1574
|
+
pathSegments: [...node.pathSegments],
|
|
1575
|
+
});
|
|
1576
|
+
if (this.expandedFlowFolders.has(node.key)) {
|
|
1577
|
+
walk(node.children, depth + 1);
|
|
1578
|
+
}
|
|
1579
|
+
continue;
|
|
1580
|
+
}
|
|
1581
|
+
items.push({
|
|
1582
|
+
kind: "flow",
|
|
1583
|
+
key: node.key,
|
|
1584
|
+
name: node.name,
|
|
1585
|
+
depth,
|
|
1586
|
+
pathSegments: [...node.pathSegments],
|
|
1587
|
+
flow: node.flow,
|
|
1588
|
+
});
|
|
1589
|
+
}
|
|
1590
|
+
};
|
|
1591
|
+
walk(this.flowTree, 0);
|
|
1592
|
+
return items;
|
|
1593
|
+
}
|
|
1594
|
+
selectedFlowTreeItem() {
|
|
1595
|
+
return this.visibleFlowItems.find((item) => item.key === this.selectedFlowItemKey);
|
|
1596
|
+
}
|
|
1597
|
+
selectedHeaderLabel() {
|
|
1598
|
+
const selectedItem = this.selectedFlowTreeItem();
|
|
1599
|
+
if (!selectedItem) {
|
|
1600
|
+
return this.selectedFlowId;
|
|
1601
|
+
}
|
|
1602
|
+
return selectedItem.kind === "folder" ? selectedItem.pathSegments.join("/") : selectedItem.flow.label;
|
|
1603
|
+
}
|
|
1604
|
+
refreshVisibleFlowItems() {
|
|
1605
|
+
this.visibleFlowItems = this.computeVisibleFlowItems();
|
|
1606
|
+
if (!this.visibleFlowItems.some((item) => item.key === this.selectedFlowItemKey)) {
|
|
1607
|
+
this.selectedFlowItemKey = this.visibleFlowItems[0]?.key ?? makeFlowKey(this.selectedFlowId);
|
|
1608
|
+
}
|
|
1609
|
+
const selectedItem = this.selectedFlowTreeItem();
|
|
1610
|
+
if (selectedItem?.kind === "flow") {
|
|
1611
|
+
this.selectedFlowId = selectedItem.flow.id;
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
renderFlowTreeList() {
|
|
1615
|
+
this.refreshVisibleFlowItems();
|
|
1616
|
+
this.flowList.setItems(this.visibleFlowItems.map((item) => this.renderFlowTreeLabel(item)));
|
|
1617
|
+
const selectedIndex = this.visibleFlowItems.findIndex((item) => item.key === this.selectedFlowItemKey);
|
|
1618
|
+
this.flowList.select(selectedIndex >= 0 ? selectedIndex : 0);
|
|
1619
|
+
}
|
|
1620
|
+
renderFlowTreeLabel(item) {
|
|
1621
|
+
const indent = " ".repeat(item.depth);
|
|
1622
|
+
if (item.kind === "folder") {
|
|
1623
|
+
const expanded = this.expandedFlowFolders.has(item.key);
|
|
1624
|
+
const color = "cyan";
|
|
1625
|
+
return `${indent}{${color}-fg}${expanded ? "▾" : "▸"} ${item.name}{/${color}-fg}`;
|
|
1626
|
+
}
|
|
1627
|
+
const color = "white";
|
|
1628
|
+
return `${indent}{${color}-fg}• ${item.name}{/${color}-fg}`;
|
|
1629
|
+
}
|
|
1630
|
+
toggleFlowFolder(folderKey) {
|
|
1631
|
+
if (this.expandedFlowFolders.has(folderKey)) {
|
|
1632
|
+
this.expandedFlowFolders.delete(folderKey);
|
|
1633
|
+
}
|
|
1634
|
+
else {
|
|
1635
|
+
this.expandedFlowFolders.add(folderKey);
|
|
1636
|
+
}
|
|
1637
|
+
this.renderFlowTreeList();
|
|
1638
|
+
this.renderDescription();
|
|
1639
|
+
this.renderProgress();
|
|
1640
|
+
this.updateHeader();
|
|
1641
|
+
this.requestRender();
|
|
1642
|
+
}
|
|
1643
|
+
expandSelectedFlowFolder() {
|
|
1644
|
+
const selectedItem = this.selectedFlowTreeItem();
|
|
1645
|
+
if (!selectedItem || selectedItem.kind !== "folder" || this.expandedFlowFolders.has(selectedItem.key)) {
|
|
1646
|
+
return;
|
|
1647
|
+
}
|
|
1648
|
+
this.toggleFlowFolder(selectedItem.key);
|
|
1649
|
+
}
|
|
1650
|
+
collapseSelectedFlowFolderOrSelectParent() {
|
|
1651
|
+
const selectedItem = this.selectedFlowTreeItem();
|
|
1652
|
+
if (!selectedItem) {
|
|
1653
|
+
return;
|
|
1654
|
+
}
|
|
1655
|
+
if (selectedItem.kind === "folder" && this.expandedFlowFolders.has(selectedItem.key)) {
|
|
1656
|
+
this.toggleFlowFolder(selectedItem.key);
|
|
1657
|
+
return;
|
|
1658
|
+
}
|
|
1659
|
+
const parentPath = selectedItem.pathSegments.slice(0, -1);
|
|
1660
|
+
if (parentPath.length === 0) {
|
|
1661
|
+
return;
|
|
1662
|
+
}
|
|
1663
|
+
const parentKey = makeFolderKey(parentPath);
|
|
1664
|
+
if (!this.visibleFlowItems.some((item) => item.key === parentKey)) {
|
|
1665
|
+
return;
|
|
1666
|
+
}
|
|
1667
|
+
this.selectedFlowItemKey = parentKey;
|
|
1668
|
+
this.renderFlowTreeList();
|
|
1669
|
+
this.renderDescription();
|
|
1670
|
+
this.renderProgress();
|
|
1671
|
+
this.updateHeader();
|
|
1672
|
+
this.requestRender();
|
|
1673
|
+
}
|
|
1367
1674
|
updateRunningPanel() {
|
|
1368
1675
|
const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
1369
1676
|
const running = this.busy || this.currentNode !== null || this.currentExecutor !== null;
|
|
@@ -39,18 +39,21 @@ export function loadDeclarativeFlow(flow) {
|
|
|
39
39
|
}
|
|
40
40
|
export function resolveNamedDeclarativeFlowRef(fileName, cwd) {
|
|
41
41
|
const projectMatches = listProjectFlowSpecFiles(cwd).filter((candidate) => path.basename(candidate) === fileName);
|
|
42
|
-
const
|
|
43
|
-
if (projectMatches.length > 0 &&
|
|
42
|
+
const builtInMatches = listBuiltInFlowSpecFiles().filter((candidate) => path.basename(candidate) === fileName);
|
|
43
|
+
if (projectMatches.length > 0 && builtInMatches.length > 0) {
|
|
44
44
|
throw new Error(`Ambiguous nested flow '${fileName}': both built-in and project-local specs exist in ${projectFlowSpecsDir(cwd)}.`);
|
|
45
45
|
}
|
|
46
46
|
if (projectMatches.length > 1) {
|
|
47
47
|
throw new Error(`Ambiguous project-local flow '${fileName}' in ${projectFlowSpecsDir(cwd)}.`);
|
|
48
48
|
}
|
|
49
|
+
if (builtInMatches.length > 1) {
|
|
50
|
+
throw new Error(`Ambiguous built-in flow '${fileName}'. Use unique nested flow file names.`);
|
|
51
|
+
}
|
|
49
52
|
if (projectMatches[0]) {
|
|
50
53
|
return { source: "project-local", filePath: projectMatches[0] };
|
|
51
54
|
}
|
|
52
|
-
if (
|
|
53
|
-
return { source: "built-in", fileName };
|
|
55
|
+
if (builtInMatches[0]) {
|
|
56
|
+
return { source: "built-in", fileName: builtInMatches[0] };
|
|
54
57
|
}
|
|
55
58
|
throw new Error(`Nested flow '${fileName}' was not found.`);
|
|
56
59
|
}
|