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.
Files changed (55) hide show
  1. package/README.md +71 -25
  2. package/dist/artifacts.js +25 -1
  3. package/dist/errors.js +7 -0
  4. package/dist/executors/configs/fetch-gitlab-diff-config.js +3 -0
  5. package/dist/executors/configs/opencode-config.js +6 -0
  6. package/dist/executors/fetch-gitlab-diff-executor.js +26 -0
  7. package/dist/executors/jira-fetch-executor.js +8 -2
  8. package/dist/executors/opencode-executor.js +35 -0
  9. package/dist/gitlab.js +199 -5
  10. package/dist/index.js +215 -144
  11. package/dist/interactive-ui.js +363 -37
  12. package/dist/jira.js +116 -14
  13. package/dist/pipeline/auto-flow.js +1 -1
  14. package/dist/pipeline/declarative-flows.js +44 -6
  15. package/dist/pipeline/flow-catalog.js +73 -0
  16. package/dist/pipeline/flow-specs/auto.json +183 -1
  17. package/dist/pipeline/flow-specs/gitlab-diff-review.json +226 -0
  18. package/dist/pipeline/flow-specs/gitlab-review.json +1 -31
  19. package/dist/pipeline/flow-specs/opencode/auto-opencode.json +1365 -0
  20. package/dist/pipeline/flow-specs/opencode/bugz/bug-analyze-opencode.json +382 -0
  21. package/dist/pipeline/flow-specs/opencode/bugz/bug-fix-opencode.json +56 -0
  22. package/dist/pipeline/flow-specs/opencode/gitlab/gitlab-diff-review-opencode.json +308 -0
  23. package/dist/pipeline/flow-specs/opencode/gitlab/gitlab-review-opencode.json +437 -0
  24. package/dist/pipeline/flow-specs/opencode/gitlab/mr-description-opencode.json +117 -0
  25. package/dist/pipeline/flow-specs/opencode/go/run-go-linter-loop-opencode.json +321 -0
  26. package/dist/pipeline/flow-specs/opencode/go/run-go-tests-loop-opencode.json +321 -0
  27. package/dist/pipeline/flow-specs/opencode/implement-opencode.json +64 -0
  28. package/dist/pipeline/flow-specs/opencode/plan-opencode.json +603 -0
  29. package/dist/pipeline/flow-specs/opencode/review/review-fix-opencode.json +209 -0
  30. package/dist/pipeline/flow-specs/opencode/review/review-opencode.json +452 -0
  31. package/dist/pipeline/flow-specs/opencode/task-describe-opencode.json +148 -0
  32. package/dist/pipeline/flow-specs/plan.json +183 -1
  33. package/dist/pipeline/node-registry.js +80 -8
  34. package/dist/pipeline/nodes/fetch-gitlab-diff-node.js +34 -0
  35. package/dist/pipeline/nodes/flow-run-node.js +2 -2
  36. package/dist/pipeline/nodes/jira-fetch-node.js +26 -2
  37. package/dist/pipeline/nodes/opencode-prompt-node.js +32 -0
  38. package/dist/pipeline/nodes/planning-questions-form-node.js +69 -0
  39. package/dist/pipeline/nodes/user-input-node.js +9 -1
  40. package/dist/pipeline/prompt-registry.js +3 -1
  41. package/dist/pipeline/registry.js +10 -0
  42. package/dist/pipeline/spec-loader.js +48 -3
  43. package/dist/pipeline/spec-types.js +43 -1
  44. package/dist/pipeline/spec-validator.js +53 -7
  45. package/dist/pipeline/value-resolver.js +15 -1
  46. package/dist/prompts.js +47 -12
  47. package/dist/runtime/process-runner.js +45 -1
  48. package/dist/scope.js +24 -32
  49. package/dist/structured-artifact-schemas.json +154 -1
  50. package/dist/structured-artifacts.js +2 -0
  51. package/dist/user-input.js +7 -0
  52. package/package.json +1 -1
  53. package/dist/pipeline/flow-specs/preflight.json +0 -206
  54. package/dist/pipeline/flow-specs/run-linter-loop.json +0 -155
  55. package/dist/pipeline/flow-specs/run-tests-loop.json +0 -155
@@ -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 flow = this.options.flows[index];
379
- if (!flow) {
483
+ const selectedItem = this.visibleFlowItems[index];
484
+ if (!selectedItem) {
380
485
  return;
381
486
  }
382
- this.selectedFlowId = flow.id;
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.busy || this.confirm.hidden || this.hasActiveForm() || !this.confirmSession) {
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
- await this.options.onRun(flowId, selectedAction === "ok" ? "restart" : selectedAction);
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 flow | Enter: confirm run | h: help | Esc: close | Tab: switch pane | q: exit `);
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.flowList.setItems(this.options.flows.map((flow) => flow.label));
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
- "Enter открыть подтверждение запуска",
701
+ "Up / Down выбрать папку или flow",
702
+ "Right раскрыть папку",
703
+ "Left свернуть папку или перейти к родителю",
704
+ "Enter раскрыть папку или открыть запуск flow",
569
705
  "Enter подтвердить запуск в модалке",
570
- "Esc закрыть help или модалку",
571
- "h / F1 открыть или закрыть help",
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.label),
713
+ ...this.options.flows.map((flow) => flow.treePath.join("/")),
578
714
  ].join("\n")));
579
- this.footer.setContent(" Up/Down: select flow | Enter: confirm run | h: help | Tab: switch pane | q: exit ");
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.selectedFlowId;
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 flow | Enter: confirm run | h: help | Tab: switch pane | q: exit ");
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 flow = this.flowMap.get(this.selectedFlowId);
859
- const description = flow?.description?.trim() || "Описание для этого flow пока не задано.";
860
- this.description.setContent(renderMarkdownToTerminal(stripAnsi(description)));
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
- const preferredFlowId = this.busy ? this.activeFlowId() : this.selectedFlowId;
889
- return this.flowMap.get(preferredFlowId);
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("Flow structure is not available.");
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.confirmSession.resumeAvailable
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 = session.resumeAvailable
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 === "resume" ? "Resume" : action === "restart" ? "Restart" : action === "ok" ? "OK" : "Cancel";
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 flow = this.flowMap.get(this.selectedFlowId);
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;