agentweaver 0.1.9 → 0.1.11
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 +226 -200
- package/dist/artifacts.js +101 -56
- package/dist/errors.js +7 -0
- package/dist/executors/{codex-local-executor.js → codex-executor.js} +4 -4
- package/dist/executors/configs/{codex-local-config.js → codex-config.js} +1 -1
- package/dist/executors/configs/jira-fetch-config.js +2 -0
- package/dist/executors/configs/telegram-notifier-config.js +3 -0
- package/dist/executors/fetch-gitlab-diff-executor.js +1 -1
- package/dist/executors/fetch-gitlab-review-executor.js +1 -1
- package/dist/executors/git-commit-executor.js +25 -0
- package/dist/executors/telegram-notifier-executor.js +54 -0
- package/dist/flow-state.js +46 -1
- package/dist/gitlab.js +13 -8
- package/dist/index.js +507 -520
- package/dist/interactive-ui.js +495 -87
- package/dist/jira.js +52 -5
- package/dist/pipeline/auto-flow.js +6 -6
- package/dist/pipeline/context.js +1 -0
- package/dist/pipeline/declarative-flows.js +7 -4
- package/dist/pipeline/flow-catalog.js +60 -23
- package/dist/pipeline/flow-model-settings.js +77 -0
- package/dist/pipeline/flow-specs/auto-common.json +446 -0
- package/dist/pipeline/flow-specs/auto-golang.json +563 -0
- package/dist/pipeline/flow-specs/{bug-analyze.json → bugz/bug-analyze.json} +43 -25
- package/dist/pipeline/flow-specs/{bug-fix.json → bugz/bug-fix.json} +5 -4
- package/dist/pipeline/flow-specs/git-commit.json +196 -0
- package/dist/pipeline/flow-specs/{gitlab-diff-review.json → gitlab/gitlab-diff-review.json} +20 -50
- package/dist/pipeline/flow-specs/gitlab/gitlab-review.json +165 -0
- package/dist/pipeline/flow-specs/{mr-description.json → gitlab/mr-description.json} +17 -10
- package/dist/pipeline/flow-specs/{run-go-linter-loop.json → go/run-go-linter-loop.json} +40 -14
- package/dist/pipeline/flow-specs/{run-go-tests-loop.json → go/run-go-tests-loop.json} +40 -14
- package/dist/pipeline/flow-specs/implement.json +5 -4
- package/dist/pipeline/flow-specs/plan.json +40 -148
- package/dist/pipeline/flow-specs/{review-fix.json → review/review-fix.json} +73 -13
- package/dist/pipeline/flow-specs/review/review-loop.json +280 -0
- package/dist/pipeline/flow-specs/review/review-project.json +87 -0
- package/dist/pipeline/flow-specs/review/review.json +126 -0
- package/dist/pipeline/flow-specs/task-describe.json +191 -11
- package/dist/pipeline/launch-profile-config.js +38 -0
- package/dist/pipeline/node-registry.js +75 -45
- package/dist/pipeline/nodes/build-failure-summary-node.js +16 -29
- package/dist/pipeline/nodes/build-review-fix-prompt-node.js +36 -0
- package/dist/pipeline/nodes/codex-prompt-node.js +41 -0
- package/dist/pipeline/nodes/commit-message-form-node.js +79 -0
- package/dist/pipeline/nodes/git-commit-form-node.js +138 -0
- package/dist/pipeline/nodes/git-commit-node.js +28 -0
- package/dist/pipeline/nodes/git-status-node.js +221 -0
- package/dist/pipeline/nodes/gitlab-review-artifacts-node.js +10 -6
- package/dist/pipeline/nodes/jira-context-node.js +10 -0
- package/dist/pipeline/nodes/llm-prompt-node.js +62 -0
- package/dist/pipeline/nodes/plan-codex-node.js +1 -1
- package/dist/pipeline/nodes/read-file-node.js +11 -0
- package/dist/pipeline/nodes/review-findings-form-node.js +18 -14
- package/dist/pipeline/nodes/select-files-form-node.js +72 -0
- package/dist/pipeline/nodes/telegram-notifier-node.js +28 -0
- package/dist/pipeline/nodes/user-input-node.js +29 -8
- package/dist/pipeline/nodes/write-selection-file-node.js +46 -0
- package/dist/pipeline/prompt-registry.js +2 -4
- package/dist/pipeline/prompt-runtime.js +13 -3
- package/dist/pipeline/registry.js +6 -8
- package/dist/pipeline/spec-compiler.js +5 -0
- package/dist/pipeline/spec-loader.js +18 -7
- package/dist/pipeline/spec-types.js +7 -3
- package/dist/pipeline/spec-validator.js +4 -0
- package/dist/pipeline/types.js +1 -0
- package/dist/pipeline/value-resolver.js +40 -38
- package/dist/prompts.js +104 -110
- package/dist/runtime/agentweaver-home.js +8 -0
- package/dist/runtime/command-resolution.js +0 -38
- package/dist/runtime/env-loader.js +43 -0
- package/dist/runtime/process-runner.js +45 -1
- package/dist/structured-artifact-schema-registry.js +53 -0
- package/dist/structured-artifact-schemas.json +0 -20
- package/dist/structured-artifacts.js +3 -43
- package/dist/user-input.js +30 -2
- package/package.json +2 -6
- package/Dockerfile.codex +0 -56
- package/dist/executors/claude-executor.js +0 -46
- package/dist/executors/codex-docker-executor.js +0 -27
- package/dist/executors/configs/claude-config.js +0 -12
- package/dist/executors/configs/codex-docker-config.js +0 -10
- package/dist/executors/configs/verify-build-config.js +0 -7
- package/dist/executors/verify-build-executor.js +0 -123
- package/dist/pipeline/flow-specs/auto.json +0 -979
- package/dist/pipeline/flow-specs/gitlab-review.json +0 -317
- package/dist/pipeline/flow-specs/plan-opencode.json +0 -603
- package/dist/pipeline/flow-specs/preflight.json +0 -206
- package/dist/pipeline/flow-specs/review-project.json +0 -243
- package/dist/pipeline/flow-specs/review.json +0 -312
- package/dist/pipeline/flow-specs/run-linter-loop.json +0 -155
- package/dist/pipeline/flow-specs/run-tests-loop.json +0 -155
- package/dist/pipeline/flows/preflight-flow.js +0 -19
- package/dist/pipeline/nodes/claude-prompt-node.js +0 -54
- package/dist/pipeline/nodes/codex-docker-prompt-node.js +0 -32
- package/dist/pipeline/nodes/codex-local-prompt-node.js +0 -32
- package/dist/pipeline/nodes/review-claude-node.js +0 -38
- package/dist/pipeline/nodes/review-reply-codex-node.js +0 -40
- package/dist/pipeline/nodes/verify-build-node.js +0 -15
- package/dist/runtime/docker-runtime.js +0 -51
- package/docker-compose.yml +0 -445
- package/verify_build.sh +0 -105
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;
|
|
@@ -44,16 +142,21 @@ export class InteractiveUi {
|
|
|
44
142
|
scopeKey;
|
|
45
143
|
jiraIssueKey;
|
|
46
144
|
summaryVisible;
|
|
145
|
+
version;
|
|
47
146
|
constructor(options) {
|
|
48
147
|
this.options = options;
|
|
49
148
|
if (options.flows.length === 0) {
|
|
50
149
|
throw new Error("Interactive UI requires at least one flow.");
|
|
51
150
|
}
|
|
52
151
|
this.flowMap = new Map(options.flows.map((flow) => [flow.id, flow]));
|
|
53
|
-
this.
|
|
152
|
+
this.flowTree = buildFlowTree(options.flows);
|
|
153
|
+
this.selectedFlowId = options.flows[0]?.id ?? "auto-golang";
|
|
154
|
+
this.visibleFlowItems = this.computeVisibleFlowItems();
|
|
155
|
+
this.selectedFlowItemKey = this.visibleFlowItems[0]?.key ?? makeFlowKey(this.selectedFlowId);
|
|
54
156
|
this.scopeKey = options.scopeKey;
|
|
55
157
|
this.jiraIssueKey = options.jiraIssueKey ?? null;
|
|
56
158
|
this.summaryVisible = options.summaryText.trim().length > 0;
|
|
159
|
+
this.version = options.version ?? "";
|
|
57
160
|
this.screen = blessed.screen({
|
|
58
161
|
smartCSR: true,
|
|
59
162
|
fullUnicode: true,
|
|
@@ -338,6 +441,10 @@ export class InteractiveUi {
|
|
|
338
441
|
this.requestRender();
|
|
339
442
|
});
|
|
340
443
|
this.screen.key(["escape"], () => {
|
|
444
|
+
if (this.busy && this.confirm.hidden && this.help.hidden) {
|
|
445
|
+
this.openInterruptConfirm();
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
341
448
|
if (this.hasActiveForm()) {
|
|
342
449
|
this.cancelActiveForm();
|
|
343
450
|
return;
|
|
@@ -375,11 +482,15 @@ export class InteractiveUi {
|
|
|
375
482
|
if (this.hasActiveForm()) {
|
|
376
483
|
return;
|
|
377
484
|
}
|
|
378
|
-
const
|
|
379
|
-
if (!
|
|
485
|
+
const selectedItem = this.visibleFlowItems[index];
|
|
486
|
+
if (!selectedItem) {
|
|
380
487
|
return;
|
|
381
488
|
}
|
|
382
|
-
this.
|
|
489
|
+
this.selectedFlowItemKey = selectedItem.key;
|
|
490
|
+
if (selectedItem.kind === "flow") {
|
|
491
|
+
this.selectedFlowId = selectedItem.flow.id;
|
|
492
|
+
}
|
|
493
|
+
this.updateHeader();
|
|
383
494
|
this.renderDescription();
|
|
384
495
|
this.renderProgress();
|
|
385
496
|
this.requestRender();
|
|
@@ -388,8 +499,25 @@ export class InteractiveUi {
|
|
|
388
499
|
if (this.busy || this.confirm.visible || !this.help.hidden || this.hasActiveForm()) {
|
|
389
500
|
return;
|
|
390
501
|
}
|
|
502
|
+
const selectedItem = this.selectedFlowTreeItem();
|
|
503
|
+
if (selectedItem?.kind === "folder") {
|
|
504
|
+
this.toggleFlowFolder(selectedItem.key);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
391
507
|
await this.openConfirm();
|
|
392
508
|
});
|
|
509
|
+
this.flowList.key(["right"], () => {
|
|
510
|
+
if (this.hasActiveForm()) {
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
this.expandSelectedFlowFolder();
|
|
514
|
+
});
|
|
515
|
+
this.flowList.key(["left"], () => {
|
|
516
|
+
if (this.hasActiveForm()) {
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
this.collapseSelectedFlowFolderOrSelectParent();
|
|
520
|
+
});
|
|
393
521
|
this.flowList.key(["pageup"], () => {
|
|
394
522
|
if (this.hasActiveForm()) {
|
|
395
523
|
return;
|
|
@@ -475,20 +603,29 @@ export class InteractiveUi {
|
|
|
475
603
|
this.requestRender();
|
|
476
604
|
});
|
|
477
605
|
this.confirm.key(["enter"], async () => {
|
|
478
|
-
if (this.
|
|
606
|
+
if (this.confirm.hidden || !this.confirmSession) {
|
|
479
607
|
return;
|
|
480
608
|
}
|
|
481
|
-
|
|
609
|
+
if (this.hasActiveForm() && this.confirmSession.kind !== "interrupt") {
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
const { flowId, selectedAction, kind } = this.confirmSession;
|
|
482
613
|
if (selectedAction === "cancel") {
|
|
483
614
|
this.closeConfirm();
|
|
484
615
|
return;
|
|
485
616
|
}
|
|
617
|
+
if (kind === "interrupt") {
|
|
618
|
+
this.closeConfirm();
|
|
619
|
+
await this.options.onInterrupt(flowId);
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
486
622
|
this.closeConfirm();
|
|
487
623
|
this.setBusy(true, flowId);
|
|
488
624
|
this.clearFlowFailure(flowId);
|
|
489
625
|
this.setFlowDisplayState(flowId, null);
|
|
490
626
|
try {
|
|
491
|
-
|
|
627
|
+
const launchMode = selectedAction === "resume" ? "resume" : "restart";
|
|
628
|
+
await this.options.onRun(flowId, launchMode);
|
|
492
629
|
}
|
|
493
630
|
finally {
|
|
494
631
|
this.setBusy(false);
|
|
@@ -547,7 +684,7 @@ export class InteractiveUi {
|
|
|
547
684
|
else {
|
|
548
685
|
this.log.focus();
|
|
549
686
|
}
|
|
550
|
-
this.footer.setContent(` Focus: ${pane} | Up/Down: select
|
|
687
|
+
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
688
|
this.requestRender();
|
|
552
689
|
}
|
|
553
690
|
renderStaticContent() {
|
|
@@ -555,8 +692,7 @@ export class InteractiveUi {
|
|
|
555
692
|
this.summaryVisible = this.summaryText.length > 0;
|
|
556
693
|
this.applyRightPaneLayout();
|
|
557
694
|
this.updateHeader();
|
|
558
|
-
this.
|
|
559
|
-
this.flowList.select(this.options.flows.findIndex((flow) => flow.id === this.selectedFlowId));
|
|
695
|
+
this.renderFlowTreeList();
|
|
560
696
|
this.renderDescription();
|
|
561
697
|
this.renderSummary();
|
|
562
698
|
this.renderProgress();
|
|
@@ -564,31 +700,35 @@ export class InteractiveUi {
|
|
|
564
700
|
"AgentWeaver interactive mode",
|
|
565
701
|
"",
|
|
566
702
|
"Клавиши:",
|
|
567
|
-
"Up / Down выбрать flow",
|
|
568
|
-
"
|
|
703
|
+
"Up / Down выбрать папку или flow",
|
|
704
|
+
"Right раскрыть папку",
|
|
705
|
+
"Left свернуть папку или перейти к родителю",
|
|
706
|
+
"Enter раскрыть папку или открыть запуск flow",
|
|
569
707
|
"Enter подтвердить запуск в модалке",
|
|
570
|
-
"Esc закрыть help или
|
|
571
|
-
"
|
|
708
|
+
"Esc закрыть help/модалку или прервать running flow",
|
|
709
|
+
"F1 открыть или закрыть help",
|
|
572
710
|
"Tab переключить pane",
|
|
573
711
|
"Ctrl+L очистить лог",
|
|
574
712
|
"q / Ctrl+C выйти",
|
|
575
713
|
"",
|
|
576
714
|
"Доступные flow:",
|
|
577
|
-
...this.options.flows.map((flow) => flow.
|
|
715
|
+
...this.options.flows.map((flow) => flow.treePath.join("/")),
|
|
578
716
|
].join("\n")));
|
|
579
|
-
this.footer.setContent(" Up/Down: select
|
|
717
|
+
this.footer.setContent(" Up/Down: select | Left/Right: fold | Enter: toggle/run | Esc: close/interrupt | h: help | Tab: switch pane | q: exit ");
|
|
580
718
|
}
|
|
581
719
|
updateHeader() {
|
|
582
|
-
const current = this.currentFlowId ?? this.
|
|
720
|
+
const current = this.currentFlowId ?? this.selectedHeaderLabel();
|
|
583
721
|
const pathParts = this.options.cwd.split(path.sep).filter(Boolean);
|
|
584
722
|
const folderName = pathParts.slice(-3).join("/") || this.options.cwd;
|
|
585
723
|
const branchLabel = this.options.gitBranchName ? this.options.gitBranchName : "detached-head";
|
|
586
724
|
const flowLabel = `${current}${this.busy ? " {yellow-fg}[running]{/yellow-fg}" : ""}`;
|
|
587
725
|
const divider = " {gray-fg}│{/gray-fg} ";
|
|
726
|
+
const versionLabel = this.version ? `${divider}{bold}Version{/bold} {white-fg}${this.version}{/white-fg}` : "";
|
|
588
727
|
this.header.setContent([
|
|
589
728
|
"{bold}AgentWeaver{/bold}",
|
|
590
729
|
divider,
|
|
591
730
|
`{bold}Scope{/bold} {green-fg}${this.scopeKey}{/green-fg}`,
|
|
731
|
+
versionLabel,
|
|
592
732
|
this.jiraIssueKey ? `${divider}{bold}Jira{/bold} {yellow-fg}${this.jiraIssueKey}{/yellow-fg}` : "",
|
|
593
733
|
divider,
|
|
594
734
|
`{bold}Flow{/bold} ${flowLabel}`,
|
|
@@ -609,24 +749,122 @@ export class InteractiveUi {
|
|
|
609
749
|
}
|
|
610
750
|
return this.activeFormSession.form.fields[this.activeFormSession.currentFieldIndex] ?? null;
|
|
611
751
|
}
|
|
612
|
-
renderTextInputValue(value, placeholder) {
|
|
613
|
-
const
|
|
614
|
-
const
|
|
752
|
+
renderTextInputValue(value, placeholder, rows = 1) {
|
|
753
|
+
const rawLines = (value || placeholder || "Введите текст").split("\n");
|
|
754
|
+
const visibleLines = rawLines.slice(0, Math.max(1, rows));
|
|
755
|
+
const contentWidth = visibleLines.reduce((max, line) => Math.max(max, line.length), 0);
|
|
756
|
+
const frameWidth = Math.max(36, contentWidth + 6);
|
|
615
757
|
const innerWidth = Math.max(32, frameWidth - 4);
|
|
616
|
-
const
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
758
|
+
const renderedRows = Array.from({ length: Math.max(1, rows) }, (_, index) => {
|
|
759
|
+
const rawLine = visibleLines[index] ?? "";
|
|
760
|
+
const visibleText = rawLine.length > innerWidth - 2 ? `${rawLine.slice(0, innerWidth - 5)}...` : rawLine;
|
|
761
|
+
const color = value ? "white-fg" : "gray-fg";
|
|
762
|
+
return `{cyan-fg}│{/cyan-fg}{black-bg} ${index === 0 ? "{green-fg}>{/green-fg}" : " "} {${color}}${visibleText.padEnd(innerWidth - 2, " ")}{/${color}} {/black-bg}{cyan-fg}│{/cyan-fg}`;
|
|
763
|
+
});
|
|
764
|
+
return [`{cyan-fg}┌${"─".repeat(frameWidth - 2)}┐{/cyan-fg}`, ...renderedRows, `{cyan-fg}└${"─".repeat(frameWidth - 2)}┘{/cyan-fg}`];
|
|
765
|
+
}
|
|
766
|
+
formModalInnerHeight() {
|
|
767
|
+
const rawHeight = typeof this.formModal.height === "number" ? this.formModal.height : this.formModal?.lpos?.yi
|
|
768
|
+
? this.formModal.lpos.yl - this.formModal.lpos.yi + 1
|
|
769
|
+
: 0;
|
|
770
|
+
const paddingTop = Number(this.formModal.padding?.top ?? 0);
|
|
771
|
+
const paddingBottom = Number(this.formModal.padding?.bottom ?? 0);
|
|
772
|
+
return Math.max(6, rawHeight - 2 - paddingTop - paddingBottom);
|
|
773
|
+
}
|
|
774
|
+
formModalInnerWidth() {
|
|
775
|
+
const rawWidth = typeof this.formModal.width === "number" ? this.formModal.width : this.formModal?.lpos?.xi
|
|
776
|
+
? this.formModal.lpos.xl - this.formModal.lpos.xi + 1
|
|
777
|
+
: 0;
|
|
778
|
+
const paddingLeft = Number(this.formModal.padding?.left ?? 0);
|
|
779
|
+
const paddingRight = Number(this.formModal.padding?.right ?? 0);
|
|
780
|
+
return Math.max(24, rawWidth - 2 - paddingLeft - paddingRight);
|
|
781
|
+
}
|
|
782
|
+
wrapFormText(text, width) {
|
|
783
|
+
const normalized = text.trim();
|
|
784
|
+
if (!normalized) {
|
|
785
|
+
return [""];
|
|
786
|
+
}
|
|
787
|
+
const wrapped = [];
|
|
788
|
+
for (const paragraph of normalized.split("\n")) {
|
|
789
|
+
if (!paragraph.trim()) {
|
|
790
|
+
wrapped.push("");
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
793
|
+
let remaining = paragraph.trim();
|
|
794
|
+
while (remaining.length > width) {
|
|
795
|
+
let splitAt = remaining.lastIndexOf(" ", width);
|
|
796
|
+
if (splitAt <= 0) {
|
|
797
|
+
splitAt = width;
|
|
798
|
+
}
|
|
799
|
+
wrapped.push(remaining.slice(0, splitAt).trimEnd());
|
|
800
|
+
remaining = remaining.slice(splitAt).trimStart();
|
|
801
|
+
}
|
|
802
|
+
wrapped.push(remaining);
|
|
803
|
+
}
|
|
804
|
+
return wrapped.length > 0 ? wrapped : [""];
|
|
805
|
+
}
|
|
806
|
+
renderSelectableFieldWindow(field, value, currentOptionIndex, availableLines) {
|
|
807
|
+
const contentWidth = this.formModalInnerWidth();
|
|
808
|
+
const firstLineWidth = Math.max(12, contentWidth - 6);
|
|
809
|
+
const continuationWidth = Math.max(8, contentWidth - 6);
|
|
810
|
+
const descriptionWidth = Math.max(8, contentWidth - 4);
|
|
811
|
+
const renderedOptions = field.options.map((option, index) => {
|
|
812
|
+
const isCursor = index === currentOptionIndex;
|
|
813
|
+
const isSelected = field.type === "single-select"
|
|
814
|
+
? value === option.value
|
|
815
|
+
: Array.isArray(value) && value.includes(option.value);
|
|
816
|
+
const cursor = isCursor ? "{cyan-fg}>{/cyan-fg}" : " ";
|
|
817
|
+
const marker = isSelected ? "[x]" : "[ ]";
|
|
818
|
+
const labelLines = this.wrapFormText(option.label, firstLineWidth);
|
|
819
|
+
const itemLines = [`${cursor} ${marker} ${labelLines[0] ?? ""}`];
|
|
820
|
+
for (const continuation of labelLines.slice(1)) {
|
|
821
|
+
itemLines.push(` ${continuation.slice(0, continuationWidth)}`);
|
|
822
|
+
}
|
|
823
|
+
if (option.description?.trim()) {
|
|
824
|
+
for (const descriptionLine of this.wrapFormText(option.description, descriptionWidth)) {
|
|
825
|
+
itemLines.push(` {gray-fg}${descriptionLine}{/gray-fg}`);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
return itemLines;
|
|
829
|
+
});
|
|
830
|
+
if (renderedOptions.length === 0) {
|
|
831
|
+
return ["{gray-fg}Нет доступных вариантов.{/gray-fg}"];
|
|
832
|
+
}
|
|
833
|
+
let startIndex = 0;
|
|
834
|
+
let selectedStartLine = 0;
|
|
835
|
+
for (let index = 0; index < currentOptionIndex; index += 1) {
|
|
836
|
+
selectedStartLine += renderedOptions[index]?.length ?? 0;
|
|
837
|
+
}
|
|
838
|
+
let visibleLines = 0;
|
|
839
|
+
for (let index = 0; index < renderedOptions.length; index += 1) {
|
|
840
|
+
const itemHeight = renderedOptions[index]?.length ?? 0;
|
|
841
|
+
if (index < currentOptionIndex && selectedStartLine + itemHeight > availableLines) {
|
|
842
|
+
startIndex = index + 1;
|
|
843
|
+
selectedStartLine -= itemHeight;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
const output = [];
|
|
847
|
+
for (let index = startIndex; index < renderedOptions.length; index += 1) {
|
|
848
|
+
const itemLines = renderedOptions[index] ?? [];
|
|
849
|
+
if (output.length > 0 && output.length + itemLines.length > availableLines) {
|
|
850
|
+
break;
|
|
851
|
+
}
|
|
852
|
+
if (output.length === 0 && itemLines.length > availableLines) {
|
|
853
|
+
output.push(...itemLines.slice(0, availableLines));
|
|
854
|
+
break;
|
|
855
|
+
}
|
|
856
|
+
output.push(...itemLines);
|
|
857
|
+
visibleLines += itemLines.length;
|
|
858
|
+
if (visibleLines >= availableLines) {
|
|
859
|
+
break;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
return output;
|
|
625
863
|
}
|
|
626
864
|
renderActiveForm() {
|
|
627
865
|
if (!this.activeFormSession) {
|
|
628
866
|
this.formModal.hide();
|
|
629
|
-
this.footer.setContent(" Up/Down: select
|
|
867
|
+
this.footer.setContent(" Up/Down: select | Left/Right: fold | Enter: toggle/run | Esc: close/interrupt | h: help | Tab: switch pane | q: exit ");
|
|
630
868
|
this.requestRender();
|
|
631
869
|
return;
|
|
632
870
|
}
|
|
@@ -635,18 +873,20 @@ export class InteractiveUi {
|
|
|
635
873
|
if (!field) {
|
|
636
874
|
return;
|
|
637
875
|
}
|
|
638
|
-
const
|
|
876
|
+
const headerLines = [`{bold}${session.form.title}{/bold}`];
|
|
639
877
|
if (session.form.description?.trim()) {
|
|
640
|
-
|
|
641
|
-
|
|
878
|
+
headerLines.push("");
|
|
879
|
+
headerLines.push(session.form.description.trim());
|
|
642
880
|
}
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
881
|
+
headerLines.push("");
|
|
882
|
+
headerLines.push(`Field ${session.currentFieldIndex + 1}/${session.form.fields.length}`);
|
|
883
|
+
headerLines.push(`{yellow-fg}${field.label}{/yellow-fg}`);
|
|
646
884
|
if (field.help?.trim()) {
|
|
647
|
-
|
|
885
|
+
headerLines.push(field.help.trim());
|
|
648
886
|
}
|
|
649
|
-
|
|
887
|
+
headerLines.push("");
|
|
888
|
+
const footerLines = ["", "{green-fg}Ctrl+S{/green-fg}: submit", "{magenta-fg}Shift+Tab{/magenta-fg}: previous field", "{red-fg}Esc{/red-fg}: cancel"];
|
|
889
|
+
const lines = [...headerLines];
|
|
650
890
|
if (field.type === "boolean") {
|
|
651
891
|
const current = session.values[field.id] === true;
|
|
652
892
|
lines.push(`${current ? "[x]" : "[ ]"} ${field.label}`);
|
|
@@ -656,37 +896,24 @@ export class InteractiveUi {
|
|
|
656
896
|
}
|
|
657
897
|
else if (field.type === "text") {
|
|
658
898
|
const current = String(session.values[field.id] ?? "");
|
|
659
|
-
lines.push(...this.renderTextInputValue(current, field.placeholder));
|
|
899
|
+
lines.push(...this.renderTextInputValue(current, field.placeholder, field.multiline ? Math.max(1, field.rows ?? 3) : 1));
|
|
660
900
|
lines.push("");
|
|
661
|
-
lines.push("Type text, Backspace: delete");
|
|
662
|
-
lines.push("
|
|
901
|
+
lines.push(field.multiline ? "Type text, Enter: new line, Backspace: delete" : "Type text, Backspace: delete");
|
|
902
|
+
lines.push("Tab: next field");
|
|
663
903
|
}
|
|
664
904
|
else {
|
|
665
905
|
const currentOptionIndex = Math.min(session.currentOptionIndex, Math.max(0, field.options.length - 1));
|
|
666
906
|
session.currentOptionIndex = currentOptionIndex;
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
const value = session.values[field.id];
|
|
670
|
-
const isSelected = field.type === "single-select"
|
|
671
|
-
? value === option.value
|
|
672
|
-
: Array.isArray(value) && value.includes(option.value);
|
|
673
|
-
const cursor = isCursor ? "{cyan-fg}>{/cyan-fg}" : " ";
|
|
674
|
-
const marker = isSelected ? "[x]" : "[ ]";
|
|
675
|
-
lines.push(`${cursor} ${marker} ${option.label}`);
|
|
676
|
-
if (option.description?.trim()) {
|
|
677
|
-
lines.push(` {gray-fg}${option.description.trim()}{/gray-fg}`);
|
|
678
|
-
}
|
|
679
|
-
});
|
|
907
|
+
const availableLines = Math.max(3, this.formModalInnerHeight() - headerLines.length - footerLines.length - 3);
|
|
908
|
+
lines.push(...this.renderSelectableFieldWindow(field, session.values[field.id], currentOptionIndex, availableLines));
|
|
680
909
|
lines.push("");
|
|
681
910
|
lines.push("Up/Down: move");
|
|
682
911
|
lines.push("Space: select/toggle");
|
|
683
912
|
lines.push("Enter/Tab: next field");
|
|
684
913
|
}
|
|
685
|
-
lines.push(
|
|
686
|
-
lines.push("{green-fg}Ctrl+S{/green-fg}: submit");
|
|
687
|
-
lines.push("{magenta-fg}Shift+Tab{/magenta-fg}: previous field");
|
|
688
|
-
lines.push("{red-fg}Esc{/red-fg}: cancel");
|
|
914
|
+
lines.push(...footerLines);
|
|
689
915
|
this.formModal.setContent(lines.join("\n"));
|
|
916
|
+
this.formModal.setScroll(0);
|
|
690
917
|
this.formModal.show();
|
|
691
918
|
this.formModal.setFront();
|
|
692
919
|
this.formModal.focus();
|
|
@@ -744,7 +971,7 @@ export class InteractiveUi {
|
|
|
744
971
|
this.renderActiveForm();
|
|
745
972
|
}
|
|
746
973
|
}
|
|
747
|
-
appendActiveFormText(ch, key) {
|
|
974
|
+
appendActiveFormText(ch, key, appendNewline = false) {
|
|
748
975
|
const session = this.activeFormSession;
|
|
749
976
|
const field = this.currentFormField();
|
|
750
977
|
if (!session || !field || field.type !== "text") {
|
|
@@ -756,6 +983,11 @@ export class InteractiveUi {
|
|
|
756
983
|
this.renderActiveForm();
|
|
757
984
|
return;
|
|
758
985
|
}
|
|
986
|
+
if (appendNewline) {
|
|
987
|
+
session.values[field.id] = `${current}\n`;
|
|
988
|
+
this.renderActiveForm();
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
759
991
|
if (key.ctrl || key.meta || !ch || ch === "\r" || ch === "\n" || ch === "\t") {
|
|
760
992
|
return;
|
|
761
993
|
}
|
|
@@ -796,6 +1028,17 @@ export class InteractiveUi {
|
|
|
796
1028
|
session.reject(new TaskRunnerError(`User cancelled form '${session.form.formId}'.`));
|
|
797
1029
|
this.renderActiveForm();
|
|
798
1030
|
}
|
|
1031
|
+
interruptActiveForm(message = "Flow interrupted by user.") {
|
|
1032
|
+
if (!this.activeFormSession) {
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
const session = this.activeFormSession;
|
|
1036
|
+
this.activeFormSession = null;
|
|
1037
|
+
this.formModal.hide();
|
|
1038
|
+
this.focusPane("flows");
|
|
1039
|
+
session.reject(new FlowInterruptedError(message));
|
|
1040
|
+
this.renderActiveForm();
|
|
1041
|
+
}
|
|
799
1042
|
handleActiveFormKey(ch, key) {
|
|
800
1043
|
const field = this.currentFormField();
|
|
801
1044
|
if (!field) {
|
|
@@ -819,7 +1062,12 @@ export class InteractiveUi {
|
|
|
819
1062
|
}
|
|
820
1063
|
if (field.type === "text") {
|
|
821
1064
|
if (key.name === "enter") {
|
|
822
|
-
|
|
1065
|
+
if (field.multiline) {
|
|
1066
|
+
this.appendActiveFormText(ch, key, true);
|
|
1067
|
+
}
|
|
1068
|
+
else {
|
|
1069
|
+
this.moveActiveFormField(1);
|
|
1070
|
+
}
|
|
823
1071
|
return;
|
|
824
1072
|
}
|
|
825
1073
|
this.appendActiveFormText(ch, key);
|
|
@@ -855,22 +1103,33 @@ export class InteractiveUi {
|
|
|
855
1103
|
}
|
|
856
1104
|
}
|
|
857
1105
|
renderDescription() {
|
|
858
|
-
const
|
|
859
|
-
|
|
1106
|
+
const selectedItem = this.selectedFlowTreeItem();
|
|
1107
|
+
if (!selectedItem) {
|
|
1108
|
+
this.description.setContent("Flow structure is not available.");
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
if (selectedItem.kind === "folder") {
|
|
1112
|
+
const kindLabel = selectedItem.pathSegments[0] === "custom" ? "project-local" : "built-in";
|
|
1113
|
+
const folderDescription = [
|
|
1114
|
+
`Папка flow \`${selectedItem.pathSegments.join("/")}\`.`,
|
|
1115
|
+
"",
|
|
1116
|
+
`Источник: ${kindLabel}`,
|
|
1117
|
+
`Статус: ${this.expandedFlowFolders.has(selectedItem.key) ? "развёрнута" : "свёрнута"}`,
|
|
1118
|
+
].join("\n");
|
|
1119
|
+
this.description.setContent(renderMarkdownToTerminal(stripAnsi(folderDescription)));
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
const { flow } = selectedItem;
|
|
1123
|
+
const description = flow.description?.trim() || "Описание для этого flow пока не задано.";
|
|
860
1124
|
const details = [
|
|
861
|
-
|
|
862
|
-
flow
|
|
1125
|
+
`Путь: ${flow.treePath.join("/")}`,
|
|
1126
|
+
`Источник: ${flow.source === "project-local" ? "project-local" : "built-in"}`,
|
|
1127
|
+
flow.source === "project-local" && flow.sourcePath ? `Файл: ${flow.sourcePath}` : "",
|
|
863
1128
|
]
|
|
864
1129
|
.filter((line) => line.length > 0)
|
|
865
1130
|
.join("\n");
|
|
866
1131
|
this.description.setContent(renderMarkdownToTerminal(stripAnsi(details ? `${description}\n\n${details}` : description)));
|
|
867
1132
|
}
|
|
868
|
-
renderFlowListLabel(flow) {
|
|
869
|
-
if (flow.source === "project-local") {
|
|
870
|
-
return `{yellow-fg}${flow.label}{/yellow-fg}`;
|
|
871
|
-
}
|
|
872
|
-
return flow.label;
|
|
873
|
-
}
|
|
874
1133
|
createAdapter() {
|
|
875
1134
|
return {
|
|
876
1135
|
writeStdout: (text) => {
|
|
@@ -897,13 +1156,19 @@ export class InteractiveUi {
|
|
|
897
1156
|
return this.currentFlowId ?? this.selectedFlowId;
|
|
898
1157
|
}
|
|
899
1158
|
progressFlowDefinition() {
|
|
900
|
-
|
|
901
|
-
|
|
1159
|
+
if (this.busy) {
|
|
1160
|
+
return this.flowMap.get(this.activeFlowId());
|
|
1161
|
+
}
|
|
1162
|
+
const selectedItem = this.selectedFlowTreeItem();
|
|
1163
|
+
if (selectedItem?.kind === "flow") {
|
|
1164
|
+
return selectedItem.flow;
|
|
1165
|
+
}
|
|
1166
|
+
return undefined;
|
|
902
1167
|
}
|
|
903
1168
|
renderProgress() {
|
|
904
1169
|
const flow = this.progressFlowDefinition();
|
|
905
1170
|
if (!flow) {
|
|
906
|
-
this.progress.setContent("
|
|
1171
|
+
this.progress.setContent("Выберите конкретный flow в дереве, чтобы увидеть его прогресс.");
|
|
907
1172
|
return;
|
|
908
1173
|
}
|
|
909
1174
|
const flowState = this.flowState.flowId === flow.id
|
|
@@ -1165,34 +1430,47 @@ export class InteractiveUi {
|
|
|
1165
1430
|
if (!this.confirmSession) {
|
|
1166
1431
|
return;
|
|
1167
1432
|
}
|
|
1168
|
-
const actions = this.
|
|
1169
|
-
? ["resume", "restart", "cancel"]
|
|
1170
|
-
: this.confirmSession.hasExistingState
|
|
1171
|
-
? ["restart", "cancel"]
|
|
1172
|
-
: ["ok", "cancel"];
|
|
1433
|
+
const actions = this.confirmActions();
|
|
1173
1434
|
const currentIndex = actions.indexOf(this.confirmSession.selectedAction);
|
|
1174
1435
|
const nextIndex = (currentIndex + delta + actions.length) % actions.length;
|
|
1175
1436
|
this.confirmSession.selectedAction = (actions[nextIndex] ?? "cancel");
|
|
1176
1437
|
this.renderConfirm();
|
|
1177
1438
|
}
|
|
1439
|
+
confirmActions() {
|
|
1440
|
+
if (!this.confirmSession) {
|
|
1441
|
+
return ["cancel"];
|
|
1442
|
+
}
|
|
1443
|
+
if (this.confirmSession.kind === "interrupt") {
|
|
1444
|
+
return ["stop", "cancel"];
|
|
1445
|
+
}
|
|
1446
|
+
return this.confirmSession.resumeAvailable
|
|
1447
|
+
? ["resume", "restart", "cancel"]
|
|
1448
|
+
: this.confirmSession.hasExistingState
|
|
1449
|
+
? ["restart", "cancel"]
|
|
1450
|
+
: ["ok", "cancel"];
|
|
1451
|
+
}
|
|
1178
1452
|
renderConfirm() {
|
|
1179
1453
|
const session = this.confirmSession;
|
|
1180
1454
|
if (!session) {
|
|
1181
1455
|
return;
|
|
1182
1456
|
}
|
|
1183
1457
|
const flow = this.flowMap.get(session.flowId);
|
|
1184
|
-
const actions =
|
|
1185
|
-
? ["resume", "restart", "cancel"]
|
|
1186
|
-
: session.hasExistingState
|
|
1187
|
-
? ["restart", "cancel"]
|
|
1188
|
-
: ["ok", "cancel"];
|
|
1458
|
+
const actions = this.confirmActions();
|
|
1189
1459
|
const actionLabels = actions
|
|
1190
1460
|
.map((action) => {
|
|
1191
|
-
const label = action === "
|
|
1461
|
+
const label = action === "stop"
|
|
1462
|
+
? "Stop"
|
|
1463
|
+
: action === "resume"
|
|
1464
|
+
? "Resume"
|
|
1465
|
+
: action === "restart"
|
|
1466
|
+
? "Restart"
|
|
1467
|
+
: action === "ok"
|
|
1468
|
+
? "OK"
|
|
1469
|
+
: "Cancel";
|
|
1192
1470
|
return session.selectedAction === action ? `[ ${label} ]` : ` ${label} `;
|
|
1193
1471
|
})
|
|
1194
1472
|
.join(" ");
|
|
1195
|
-
const lines = [`Run flow "${flow?.label ?? session.flowId}"?`];
|
|
1473
|
+
const lines = [session.kind === "interrupt" ? `Interrupt flow "${flow?.label ?? session.flowId}"?` : `Run flow "${flow?.label ?? session.flowId}"?`];
|
|
1196
1474
|
if (session.details?.trim()) {
|
|
1197
1475
|
lines.push("", session.details.trim());
|
|
1198
1476
|
}
|
|
@@ -1214,12 +1492,17 @@ export class InteractiveUi {
|
|
|
1214
1492
|
this.requestRender();
|
|
1215
1493
|
}
|
|
1216
1494
|
async openConfirm() {
|
|
1217
|
-
const
|
|
1495
|
+
const selectedItem = this.selectedFlowTreeItem();
|
|
1496
|
+
if (!selectedItem || selectedItem.kind !== "flow") {
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1499
|
+
const flow = selectedItem.flow;
|
|
1218
1500
|
if (!flow) {
|
|
1219
1501
|
return;
|
|
1220
1502
|
}
|
|
1221
1503
|
const confirmation = await this.options.getRunConfirmation(flow.id);
|
|
1222
1504
|
this.confirmSession = {
|
|
1505
|
+
kind: "run",
|
|
1223
1506
|
flowId: flow.id,
|
|
1224
1507
|
resumeAvailable: confirmation.resumeAvailable,
|
|
1225
1508
|
hasExistingState: confirmation.hasExistingState,
|
|
@@ -1228,6 +1511,21 @@ export class InteractiveUi {
|
|
|
1228
1511
|
};
|
|
1229
1512
|
this.renderConfirm();
|
|
1230
1513
|
}
|
|
1514
|
+
openInterruptConfirm() {
|
|
1515
|
+
const flowId = this.currentFlowId;
|
|
1516
|
+
if (!flowId || this.confirm.visible) {
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
this.confirmSession = {
|
|
1520
|
+
kind: "interrupt",
|
|
1521
|
+
flowId,
|
|
1522
|
+
resumeAvailable: true,
|
|
1523
|
+
hasExistingState: true,
|
|
1524
|
+
details: "Текущий flow будет остановлен. Состояние сохранится, и его можно будет продолжить через Resume.",
|
|
1525
|
+
selectedAction: "stop",
|
|
1526
|
+
};
|
|
1527
|
+
this.renderConfirm();
|
|
1528
|
+
}
|
|
1231
1529
|
closeConfirm() {
|
|
1232
1530
|
this.confirmSession = null;
|
|
1233
1531
|
this.confirm.hide();
|
|
@@ -1364,6 +1662,116 @@ export class InteractiveUi {
|
|
|
1364
1662
|
this.renderProgress();
|
|
1365
1663
|
this.requestRender();
|
|
1366
1664
|
}
|
|
1665
|
+
computeVisibleFlowItems() {
|
|
1666
|
+
const items = [];
|
|
1667
|
+
const walk = (nodes, depth) => {
|
|
1668
|
+
for (const node of nodes) {
|
|
1669
|
+
if (node.kind === "folder") {
|
|
1670
|
+
items.push({
|
|
1671
|
+
kind: "folder",
|
|
1672
|
+
key: node.key,
|
|
1673
|
+
name: node.name,
|
|
1674
|
+
depth,
|
|
1675
|
+
pathSegments: [...node.pathSegments],
|
|
1676
|
+
});
|
|
1677
|
+
if (this.expandedFlowFolders.has(node.key)) {
|
|
1678
|
+
walk(node.children, depth + 1);
|
|
1679
|
+
}
|
|
1680
|
+
continue;
|
|
1681
|
+
}
|
|
1682
|
+
items.push({
|
|
1683
|
+
kind: "flow",
|
|
1684
|
+
key: node.key,
|
|
1685
|
+
name: node.name,
|
|
1686
|
+
depth,
|
|
1687
|
+
pathSegments: [...node.pathSegments],
|
|
1688
|
+
flow: node.flow,
|
|
1689
|
+
});
|
|
1690
|
+
}
|
|
1691
|
+
};
|
|
1692
|
+
walk(this.flowTree, 0);
|
|
1693
|
+
return items;
|
|
1694
|
+
}
|
|
1695
|
+
selectedFlowTreeItem() {
|
|
1696
|
+
return this.visibleFlowItems.find((item) => item.key === this.selectedFlowItemKey);
|
|
1697
|
+
}
|
|
1698
|
+
selectedHeaderLabel() {
|
|
1699
|
+
const selectedItem = this.selectedFlowTreeItem();
|
|
1700
|
+
if (!selectedItem) {
|
|
1701
|
+
return this.selectedFlowId;
|
|
1702
|
+
}
|
|
1703
|
+
return selectedItem.kind === "folder" ? selectedItem.pathSegments.join("/") : selectedItem.flow.label;
|
|
1704
|
+
}
|
|
1705
|
+
refreshVisibleFlowItems() {
|
|
1706
|
+
this.visibleFlowItems = this.computeVisibleFlowItems();
|
|
1707
|
+
if (!this.visibleFlowItems.some((item) => item.key === this.selectedFlowItemKey)) {
|
|
1708
|
+
this.selectedFlowItemKey = this.visibleFlowItems[0]?.key ?? makeFlowKey(this.selectedFlowId);
|
|
1709
|
+
}
|
|
1710
|
+
const selectedItem = this.selectedFlowTreeItem();
|
|
1711
|
+
if (selectedItem?.kind === "flow") {
|
|
1712
|
+
this.selectedFlowId = selectedItem.flow.id;
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
renderFlowTreeList() {
|
|
1716
|
+
this.refreshVisibleFlowItems();
|
|
1717
|
+
this.flowList.setItems(this.visibleFlowItems.map((item) => this.renderFlowTreeLabel(item)));
|
|
1718
|
+
const selectedIndex = this.visibleFlowItems.findIndex((item) => item.key === this.selectedFlowItemKey);
|
|
1719
|
+
this.flowList.select(selectedIndex >= 0 ? selectedIndex : 0);
|
|
1720
|
+
}
|
|
1721
|
+
renderFlowTreeLabel(item) {
|
|
1722
|
+
const indent = " ".repeat(item.depth);
|
|
1723
|
+
if (item.kind === "folder") {
|
|
1724
|
+
const expanded = this.expandedFlowFolders.has(item.key);
|
|
1725
|
+
const color = "cyan";
|
|
1726
|
+
return `${indent}{${color}-fg}${expanded ? "▾" : "▸"} ${item.name}{/${color}-fg}`;
|
|
1727
|
+
}
|
|
1728
|
+
const color = "white";
|
|
1729
|
+
return `${indent}{${color}-fg}• ${item.name}{/${color}-fg}`;
|
|
1730
|
+
}
|
|
1731
|
+
toggleFlowFolder(folderKey) {
|
|
1732
|
+
if (this.expandedFlowFolders.has(folderKey)) {
|
|
1733
|
+
this.expandedFlowFolders.delete(folderKey);
|
|
1734
|
+
}
|
|
1735
|
+
else {
|
|
1736
|
+
this.expandedFlowFolders.add(folderKey);
|
|
1737
|
+
}
|
|
1738
|
+
this.renderFlowTreeList();
|
|
1739
|
+
this.renderDescription();
|
|
1740
|
+
this.renderProgress();
|
|
1741
|
+
this.updateHeader();
|
|
1742
|
+
this.requestRender();
|
|
1743
|
+
}
|
|
1744
|
+
expandSelectedFlowFolder() {
|
|
1745
|
+
const selectedItem = this.selectedFlowTreeItem();
|
|
1746
|
+
if (!selectedItem || selectedItem.kind !== "folder" || this.expandedFlowFolders.has(selectedItem.key)) {
|
|
1747
|
+
return;
|
|
1748
|
+
}
|
|
1749
|
+
this.toggleFlowFolder(selectedItem.key);
|
|
1750
|
+
}
|
|
1751
|
+
collapseSelectedFlowFolderOrSelectParent() {
|
|
1752
|
+
const selectedItem = this.selectedFlowTreeItem();
|
|
1753
|
+
if (!selectedItem) {
|
|
1754
|
+
return;
|
|
1755
|
+
}
|
|
1756
|
+
if (selectedItem.kind === "folder" && this.expandedFlowFolders.has(selectedItem.key)) {
|
|
1757
|
+
this.toggleFlowFolder(selectedItem.key);
|
|
1758
|
+
return;
|
|
1759
|
+
}
|
|
1760
|
+
const parentPath = selectedItem.pathSegments.slice(0, -1);
|
|
1761
|
+
if (parentPath.length === 0) {
|
|
1762
|
+
return;
|
|
1763
|
+
}
|
|
1764
|
+
const parentKey = makeFolderKey(parentPath);
|
|
1765
|
+
if (!this.visibleFlowItems.some((item) => item.key === parentKey)) {
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1768
|
+
this.selectedFlowItemKey = parentKey;
|
|
1769
|
+
this.renderFlowTreeList();
|
|
1770
|
+
this.renderDescription();
|
|
1771
|
+
this.renderProgress();
|
|
1772
|
+
this.updateHeader();
|
|
1773
|
+
this.requestRender();
|
|
1774
|
+
}
|
|
1367
1775
|
updateRunningPanel() {
|
|
1368
1776
|
const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
1369
1777
|
const running = this.busy || this.currentNode !== null || this.currentExecutor !== null;
|