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.
Files changed (101) hide show
  1. package/README.md +226 -200
  2. package/dist/artifacts.js +101 -56
  3. package/dist/errors.js +7 -0
  4. package/dist/executors/{codex-local-executor.js → codex-executor.js} +4 -4
  5. package/dist/executors/configs/{codex-local-config.js → codex-config.js} +1 -1
  6. package/dist/executors/configs/jira-fetch-config.js +2 -0
  7. package/dist/executors/configs/telegram-notifier-config.js +3 -0
  8. package/dist/executors/fetch-gitlab-diff-executor.js +1 -1
  9. package/dist/executors/fetch-gitlab-review-executor.js +1 -1
  10. package/dist/executors/git-commit-executor.js +25 -0
  11. package/dist/executors/telegram-notifier-executor.js +54 -0
  12. package/dist/flow-state.js +46 -1
  13. package/dist/gitlab.js +13 -8
  14. package/dist/index.js +507 -520
  15. package/dist/interactive-ui.js +495 -87
  16. package/dist/jira.js +52 -5
  17. package/dist/pipeline/auto-flow.js +6 -6
  18. package/dist/pipeline/context.js +1 -0
  19. package/dist/pipeline/declarative-flows.js +7 -4
  20. package/dist/pipeline/flow-catalog.js +60 -23
  21. package/dist/pipeline/flow-model-settings.js +77 -0
  22. package/dist/pipeline/flow-specs/auto-common.json +446 -0
  23. package/dist/pipeline/flow-specs/auto-golang.json +563 -0
  24. package/dist/pipeline/flow-specs/{bug-analyze.json → bugz/bug-analyze.json} +43 -25
  25. package/dist/pipeline/flow-specs/{bug-fix.json → bugz/bug-fix.json} +5 -4
  26. package/dist/pipeline/flow-specs/git-commit.json +196 -0
  27. package/dist/pipeline/flow-specs/{gitlab-diff-review.json → gitlab/gitlab-diff-review.json} +20 -50
  28. package/dist/pipeline/flow-specs/gitlab/gitlab-review.json +165 -0
  29. package/dist/pipeline/flow-specs/{mr-description.json → gitlab/mr-description.json} +17 -10
  30. package/dist/pipeline/flow-specs/{run-go-linter-loop.json → go/run-go-linter-loop.json} +40 -14
  31. package/dist/pipeline/flow-specs/{run-go-tests-loop.json → go/run-go-tests-loop.json} +40 -14
  32. package/dist/pipeline/flow-specs/implement.json +5 -4
  33. package/dist/pipeline/flow-specs/plan.json +40 -148
  34. package/dist/pipeline/flow-specs/{review-fix.json → review/review-fix.json} +73 -13
  35. package/dist/pipeline/flow-specs/review/review-loop.json +280 -0
  36. package/dist/pipeline/flow-specs/review/review-project.json +87 -0
  37. package/dist/pipeline/flow-specs/review/review.json +126 -0
  38. package/dist/pipeline/flow-specs/task-describe.json +191 -11
  39. package/dist/pipeline/launch-profile-config.js +38 -0
  40. package/dist/pipeline/node-registry.js +75 -45
  41. package/dist/pipeline/nodes/build-failure-summary-node.js +16 -29
  42. package/dist/pipeline/nodes/build-review-fix-prompt-node.js +36 -0
  43. package/dist/pipeline/nodes/codex-prompt-node.js +41 -0
  44. package/dist/pipeline/nodes/commit-message-form-node.js +79 -0
  45. package/dist/pipeline/nodes/git-commit-form-node.js +138 -0
  46. package/dist/pipeline/nodes/git-commit-node.js +28 -0
  47. package/dist/pipeline/nodes/git-status-node.js +221 -0
  48. package/dist/pipeline/nodes/gitlab-review-artifacts-node.js +10 -6
  49. package/dist/pipeline/nodes/jira-context-node.js +10 -0
  50. package/dist/pipeline/nodes/llm-prompt-node.js +62 -0
  51. package/dist/pipeline/nodes/plan-codex-node.js +1 -1
  52. package/dist/pipeline/nodes/read-file-node.js +11 -0
  53. package/dist/pipeline/nodes/review-findings-form-node.js +18 -14
  54. package/dist/pipeline/nodes/select-files-form-node.js +72 -0
  55. package/dist/pipeline/nodes/telegram-notifier-node.js +28 -0
  56. package/dist/pipeline/nodes/user-input-node.js +29 -8
  57. package/dist/pipeline/nodes/write-selection-file-node.js +46 -0
  58. package/dist/pipeline/prompt-registry.js +2 -4
  59. package/dist/pipeline/prompt-runtime.js +13 -3
  60. package/dist/pipeline/registry.js +6 -8
  61. package/dist/pipeline/spec-compiler.js +5 -0
  62. package/dist/pipeline/spec-loader.js +18 -7
  63. package/dist/pipeline/spec-types.js +7 -3
  64. package/dist/pipeline/spec-validator.js +4 -0
  65. package/dist/pipeline/types.js +1 -0
  66. package/dist/pipeline/value-resolver.js +40 -38
  67. package/dist/prompts.js +104 -110
  68. package/dist/runtime/agentweaver-home.js +8 -0
  69. package/dist/runtime/command-resolution.js +0 -38
  70. package/dist/runtime/env-loader.js +43 -0
  71. package/dist/runtime/process-runner.js +45 -1
  72. package/dist/structured-artifact-schema-registry.js +53 -0
  73. package/dist/structured-artifact-schemas.json +0 -20
  74. package/dist/structured-artifacts.js +3 -43
  75. package/dist/user-input.js +30 -2
  76. package/package.json +2 -6
  77. package/Dockerfile.codex +0 -56
  78. package/dist/executors/claude-executor.js +0 -46
  79. package/dist/executors/codex-docker-executor.js +0 -27
  80. package/dist/executors/configs/claude-config.js +0 -12
  81. package/dist/executors/configs/codex-docker-config.js +0 -10
  82. package/dist/executors/configs/verify-build-config.js +0 -7
  83. package/dist/executors/verify-build-executor.js +0 -123
  84. package/dist/pipeline/flow-specs/auto.json +0 -979
  85. package/dist/pipeline/flow-specs/gitlab-review.json +0 -317
  86. package/dist/pipeline/flow-specs/plan-opencode.json +0 -603
  87. package/dist/pipeline/flow-specs/preflight.json +0 -206
  88. package/dist/pipeline/flow-specs/review-project.json +0 -243
  89. package/dist/pipeline/flow-specs/review.json +0 -312
  90. package/dist/pipeline/flow-specs/run-linter-loop.json +0 -155
  91. package/dist/pipeline/flow-specs/run-tests-loop.json +0 -155
  92. package/dist/pipeline/flows/preflight-flow.js +0 -19
  93. package/dist/pipeline/nodes/claude-prompt-node.js +0 -54
  94. package/dist/pipeline/nodes/codex-docker-prompt-node.js +0 -32
  95. package/dist/pipeline/nodes/codex-local-prompt-node.js +0 -32
  96. package/dist/pipeline/nodes/review-claude-node.js +0 -38
  97. package/dist/pipeline/nodes/review-reply-codex-node.js +0 -40
  98. package/dist/pipeline/nodes/verify-build-node.js +0 -15
  99. package/dist/runtime/docker-runtime.js +0 -51
  100. package/docker-compose.yml +0 -445
  101. package/verify_build.sh +0 -105
@@ -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.selectedFlowId = options.flows[0]?.id ?? "auto";
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 flow = this.options.flows[index];
379
- if (!flow) {
485
+ const selectedItem = this.visibleFlowItems[index];
486
+ if (!selectedItem) {
380
487
  return;
381
488
  }
382
- this.selectedFlowId = flow.id;
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.busy || this.confirm.hidden || this.hasActiveForm() || !this.confirmSession) {
606
+ if (this.confirm.hidden || !this.confirmSession) {
479
607
  return;
480
608
  }
481
- const { flowId, selectedAction } = this.confirmSession;
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
- await this.options.onRun(flowId, selectedAction === "ok" ? "restart" : selectedAction);
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 flow | Enter: confirm run | h: help | Esc: close | Tab: switch pane | q: exit `);
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.flowList.setItems(this.options.flows.map((flow) => this.renderFlowListLabel(flow)));
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
- "Enter открыть подтверждение запуска",
703
+ "Up / Down выбрать папку или flow",
704
+ "Right раскрыть папку",
705
+ "Left свернуть папку или перейти к родителю",
706
+ "Enter раскрыть папку или открыть запуск flow",
569
707
  "Enter подтвердить запуск в модалке",
570
- "Esc закрыть help или модалку",
571
- "h / F1 открыть или закрыть help",
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.label),
715
+ ...this.options.flows.map((flow) => flow.treePath.join("/")),
578
716
  ].join("\n")));
579
- this.footer.setContent(" Up/Down: select flow | Enter: confirm run | h: help | Tab: switch pane | q: exit ");
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.selectedFlowId;
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 rawText = value || placeholder || "Введите текст";
614
- const frameWidth = Math.max(36, rawText.length + 6);
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 visibleText = rawText.length > innerWidth - 2 ? `${rawText.slice(0, innerWidth - 5)}...` : rawText;
617
- const padded = value
618
- ? `{white-fg}${visibleText.padEnd(innerWidth - 2, " ")}{/white-fg}`
619
- : `{gray-fg}${visibleText.padEnd(innerWidth - 2, " ")}{/gray-fg}`;
620
- return [
621
- `{cyan-fg}┌${"─".repeat(frameWidth - 2)}┐{/cyan-fg}`,
622
- `{cyan-fg}{/cyan-fg}{black-bg} {green-fg}>{/green-fg} ${padded} {/black-bg}{cyan-fg}{/cyan-fg}`,
623
- `{cyan-fg}└${"─".repeat(frameWidth - 2)}┘{/cyan-fg}`,
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 flow | Enter: confirm run | h: help | Tab: switch pane | q: exit ");
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 lines = [`{bold}${session.form.title}{/bold}`];
876
+ const headerLines = [`{bold}${session.form.title}{/bold}`];
639
877
  if (session.form.description?.trim()) {
640
- lines.push("");
641
- lines.push(session.form.description.trim());
878
+ headerLines.push("");
879
+ headerLines.push(session.form.description.trim());
642
880
  }
643
- lines.push("");
644
- lines.push(`Field ${session.currentFieldIndex + 1}/${session.form.fields.length}`);
645
- lines.push(`{yellow-fg}${field.label}{/yellow-fg}`);
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
- lines.push(field.help.trim());
885
+ headerLines.push(field.help.trim());
648
886
  }
649
- lines.push("");
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("Enter/Tab: next field");
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
- field.options.forEach((option, index) => {
668
- const isCursor = index === currentOptionIndex;
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
- this.moveActiveFormField(1);
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 flow = this.flowMap.get(this.selectedFlowId);
859
- const description = flow?.description?.trim() || "Описание для этого flow пока не задано.";
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
- flow ? `Источник: ${flow.source === "project-local" ? "project-local" : "built-in"}` : "",
862
- flow?.source === "project-local" && flow.sourcePath ? `Файл: ${flow.sourcePath}` : "",
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
- const preferredFlowId = this.busy ? this.activeFlowId() : this.selectedFlowId;
901
- return this.flowMap.get(preferredFlowId);
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("Flow structure is not available.");
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.confirmSession.resumeAvailable
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 = session.resumeAvailable
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 === "resume" ? "Resume" : action === "restart" ? "Restart" : action === "ok" ? "OK" : "Cancel";
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 flow = this.flowMap.get(this.selectedFlowId);
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;