agentweaver 0.1.8 → 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 +71 -25
- package/dist/artifacts.js +25 -1
- package/dist/errors.js +7 -0
- package/dist/executors/configs/fetch-gitlab-diff-config.js +3 -0
- package/dist/executors/configs/opencode-config.js +6 -0
- package/dist/executors/fetch-gitlab-diff-executor.js +26 -0
- package/dist/executors/jira-fetch-executor.js +8 -2
- package/dist/executors/opencode-executor.js +35 -0
- package/dist/gitlab.js +199 -5
- package/dist/index.js +215 -144
- package/dist/interactive-ui.js +363 -37
- package/dist/jira.js +116 -14
- package/dist/pipeline/auto-flow.js +1 -1
- package/dist/pipeline/declarative-flows.js +44 -6
- package/dist/pipeline/flow-catalog.js +73 -0
- package/dist/pipeline/flow-specs/auto.json +183 -1
- package/dist/pipeline/flow-specs/gitlab-diff-review.json +226 -0
- package/dist/pipeline/flow-specs/gitlab-review.json +1 -31
- 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/opencode/plan-opencode.json +603 -0
- 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/flow-specs/plan.json +183 -1
- package/dist/pipeline/node-registry.js +80 -8
- package/dist/pipeline/nodes/fetch-gitlab-diff-node.js +34 -0
- package/dist/pipeline/nodes/flow-run-node.js +2 -2
- package/dist/pipeline/nodes/jira-fetch-node.js +26 -2
- package/dist/pipeline/nodes/opencode-prompt-node.js +32 -0
- package/dist/pipeline/nodes/planning-questions-form-node.js +69 -0
- package/dist/pipeline/nodes/user-input-node.js +9 -1
- package/dist/pipeline/prompt-registry.js +3 -1
- package/dist/pipeline/registry.js +10 -0
- package/dist/pipeline/spec-loader.js +48 -3
- package/dist/pipeline/spec-types.js +43 -1
- package/dist/pipeline/spec-validator.js +53 -7
- package/dist/pipeline/value-resolver.js +15 -1
- package/dist/prompts.js +47 -12
- package/dist/runtime/process-runner.js +45 -1
- package/dist/scope.js +24 -32
- package/dist/structured-artifact-schemas.json +154 -1
- package/dist/structured-artifacts.js +2 -0
- package/dist/user-input.js +7 -0
- 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) {
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
if (this.hasActiveForm() && this.confirmSession.kind !== "interrupt") {
|
|
479
608
|
return;
|
|
480
609
|
}
|
|
481
|
-
const { flowId, selectedAction } = this.confirmSession;
|
|
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,9 +1002,32 @@ export class InteractiveUi {
|
|
|
855
1002
|
}
|
|
856
1003
|
}
|
|
857
1004
|
renderDescription() {
|
|
858
|
-
const
|
|
859
|
-
|
|
860
|
-
|
|
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 пока не задано.";
|
|
1023
|
+
const details = [
|
|
1024
|
+
`Путь: ${flow.treePath.join("/")}`,
|
|
1025
|
+
`Источник: ${flow.source === "project-local" ? "project-local" : "built-in"}`,
|
|
1026
|
+
flow.source === "project-local" && flow.sourcePath ? `Файл: ${flow.sourcePath}` : "",
|
|
1027
|
+
]
|
|
1028
|
+
.filter((line) => line.length > 0)
|
|
1029
|
+
.join("\n");
|
|
1030
|
+
this.description.setContent(renderMarkdownToTerminal(stripAnsi(details ? `${description}\n\n${details}` : description)));
|
|
861
1031
|
}
|
|
862
1032
|
createAdapter() {
|
|
863
1033
|
return {
|
|
@@ -885,13 +1055,19 @@ export class InteractiveUi {
|
|
|
885
1055
|
return this.currentFlowId ?? this.selectedFlowId;
|
|
886
1056
|
}
|
|
887
1057
|
progressFlowDefinition() {
|
|
888
|
-
|
|
889
|
-
|
|
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;
|
|
890
1066
|
}
|
|
891
1067
|
renderProgress() {
|
|
892
1068
|
const flow = this.progressFlowDefinition();
|
|
893
1069
|
if (!flow) {
|
|
894
|
-
this.progress.setContent("
|
|
1070
|
+
this.progress.setContent("Выберите конкретный flow в дереве, чтобы увидеть его прогресс.");
|
|
895
1071
|
return;
|
|
896
1072
|
}
|
|
897
1073
|
const flowState = this.flowState.flowId === flow.id
|
|
@@ -1153,34 +1329,47 @@ export class InteractiveUi {
|
|
|
1153
1329
|
if (!this.confirmSession) {
|
|
1154
1330
|
return;
|
|
1155
1331
|
}
|
|
1156
|
-
const actions = this.
|
|
1157
|
-
? ["resume", "restart", "cancel"]
|
|
1158
|
-
: this.confirmSession.hasExistingState
|
|
1159
|
-
? ["restart", "cancel"]
|
|
1160
|
-
: ["ok", "cancel"];
|
|
1332
|
+
const actions = this.confirmActions();
|
|
1161
1333
|
const currentIndex = actions.indexOf(this.confirmSession.selectedAction);
|
|
1162
1334
|
const nextIndex = (currentIndex + delta + actions.length) % actions.length;
|
|
1163
1335
|
this.confirmSession.selectedAction = (actions[nextIndex] ?? "cancel");
|
|
1164
1336
|
this.renderConfirm();
|
|
1165
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
|
+
}
|
|
1166
1351
|
renderConfirm() {
|
|
1167
1352
|
const session = this.confirmSession;
|
|
1168
1353
|
if (!session) {
|
|
1169
1354
|
return;
|
|
1170
1355
|
}
|
|
1171
1356
|
const flow = this.flowMap.get(session.flowId);
|
|
1172
|
-
const actions =
|
|
1173
|
-
? ["resume", "restart", "cancel"]
|
|
1174
|
-
: session.hasExistingState
|
|
1175
|
-
? ["restart", "cancel"]
|
|
1176
|
-
: ["ok", "cancel"];
|
|
1357
|
+
const actions = this.confirmActions();
|
|
1177
1358
|
const actionLabels = actions
|
|
1178
1359
|
.map((action) => {
|
|
1179
|
-
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";
|
|
1180
1369
|
return session.selectedAction === action ? `[ ${label} ]` : ` ${label} `;
|
|
1181
1370
|
})
|
|
1182
1371
|
.join(" ");
|
|
1183
|
-
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}"?`];
|
|
1184
1373
|
if (session.details?.trim()) {
|
|
1185
1374
|
lines.push("", session.details.trim());
|
|
1186
1375
|
}
|
|
@@ -1202,12 +1391,17 @@ export class InteractiveUi {
|
|
|
1202
1391
|
this.requestRender();
|
|
1203
1392
|
}
|
|
1204
1393
|
async openConfirm() {
|
|
1205
|
-
const
|
|
1394
|
+
const selectedItem = this.selectedFlowTreeItem();
|
|
1395
|
+
if (!selectedItem || selectedItem.kind !== "flow") {
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
const flow = selectedItem.flow;
|
|
1206
1399
|
if (!flow) {
|
|
1207
1400
|
return;
|
|
1208
1401
|
}
|
|
1209
1402
|
const confirmation = await this.options.getRunConfirmation(flow.id);
|
|
1210
1403
|
this.confirmSession = {
|
|
1404
|
+
kind: "run",
|
|
1211
1405
|
flowId: flow.id,
|
|
1212
1406
|
resumeAvailable: confirmation.resumeAvailable,
|
|
1213
1407
|
hasExistingState: confirmation.hasExistingState,
|
|
@@ -1216,6 +1410,21 @@ export class InteractiveUi {
|
|
|
1216
1410
|
};
|
|
1217
1411
|
this.renderConfirm();
|
|
1218
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
|
+
}
|
|
1219
1428
|
closeConfirm() {
|
|
1220
1429
|
this.confirmSession = null;
|
|
1221
1430
|
this.confirm.hide();
|
|
@@ -1226,6 +1435,13 @@ export class InteractiveUi {
|
|
|
1226
1435
|
if (this.activeFormSession) {
|
|
1227
1436
|
return Promise.reject(new TaskRunnerError("Another user input form is already active."));
|
|
1228
1437
|
}
|
|
1438
|
+
if (form.fields.length === 0) {
|
|
1439
|
+
return Promise.resolve({
|
|
1440
|
+
formId: form.formId,
|
|
1441
|
+
submittedAt: new Date().toISOString(),
|
|
1442
|
+
values: {},
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1229
1445
|
return new Promise((resolve, reject) => {
|
|
1230
1446
|
this.activeFormSession = {
|
|
1231
1447
|
form,
|
|
@@ -1345,6 +1561,116 @@ export class InteractiveUi {
|
|
|
1345
1561
|
this.renderProgress();
|
|
1346
1562
|
this.requestRender();
|
|
1347
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
|
+
}
|
|
1348
1674
|
updateRunningPanel() {
|
|
1349
1675
|
const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
1350
1676
|
const running = this.busy || this.currentNode !== null || this.currentExecutor !== null;
|