agentweaver 0.1.15 → 0.1.16

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 (94) hide show
  1. package/README.md +26 -9
  2. package/dist/artifact-manifest.js +219 -0
  3. package/dist/artifacts.js +15 -0
  4. package/dist/doctor/checks/env-diagnostics.js +25 -0
  5. package/dist/doctor/checks/flow-readiness.js +15 -18
  6. package/dist/flow-state.js +75 -15
  7. package/dist/index.js +391 -175
  8. package/dist/interactive/blessed-session.js +361 -0
  9. package/dist/interactive/controller.js +1293 -0
  10. package/dist/interactive/create-interactive-session.js +5 -0
  11. package/dist/interactive/ink/index.js +576 -0
  12. package/dist/interactive/progress.js +245 -0
  13. package/dist/interactive/selectors.js +14 -0
  14. package/dist/interactive/session.js +1 -0
  15. package/dist/interactive/state.js +34 -0
  16. package/dist/interactive/tree.js +155 -0
  17. package/dist/interactive/types.js +1 -0
  18. package/dist/interactive/view-model.js +1 -0
  19. package/dist/interactive-ui.js +159 -194
  20. package/dist/pipeline/context.js +1 -0
  21. package/dist/pipeline/declarative-flow-runner.js +212 -6
  22. package/dist/pipeline/declarative-flows.js +27 -0
  23. package/dist/pipeline/execution-routing-config.js +15 -0
  24. package/dist/pipeline/flow-catalog.js +19 -3
  25. package/dist/pipeline/flow-run-resume.js +29 -0
  26. package/dist/pipeline/flow-specs/auto-common.json +89 -360
  27. package/dist/pipeline/flow-specs/auto-golang.json +58 -363
  28. package/dist/pipeline/flow-specs/auto-simple.json +141 -0
  29. package/dist/pipeline/flow-specs/bugz/bug-analyze.json +2 -0
  30. package/dist/pipeline/flow-specs/bugz/bug-fix.json +1 -0
  31. package/dist/pipeline/flow-specs/design-review/design-review-loop.json +304 -0
  32. package/dist/pipeline/flow-specs/design-review.json +10 -0
  33. package/dist/pipeline/flow-specs/gitlab/gitlab-diff-review.json +11 -0
  34. package/dist/pipeline/flow-specs/gitlab/gitlab-review.json +2 -0
  35. package/dist/pipeline/flow-specs/gitlab/mr-description.json +1 -0
  36. package/dist/pipeline/flow-specs/go/run-go-linter-loop.json +2 -0
  37. package/dist/pipeline/flow-specs/go/run-go-tests-loop.json +2 -0
  38. package/dist/pipeline/flow-specs/implement.json +13 -6
  39. package/dist/pipeline/flow-specs/instant-task.json +177 -0
  40. package/dist/pipeline/flow-specs/normalize-task-source.json +311 -0
  41. package/dist/pipeline/flow-specs/plan-revise.json +7 -1
  42. package/dist/pipeline/flow-specs/plan.json +48 -70
  43. package/dist/pipeline/flow-specs/review/review-fix.json +24 -4
  44. package/dist/pipeline/flow-specs/review/review-loop.json +351 -45
  45. package/dist/pipeline/flow-specs/review/review-project-loop.json +590 -0
  46. package/dist/pipeline/flow-specs/review/review-project.json +12 -0
  47. package/dist/pipeline/flow-specs/review/review.json +37 -31
  48. package/dist/pipeline/flow-specs/task-describe.json +2 -0
  49. package/dist/pipeline/flow-specs/task-source/jira-fetch.json +70 -0
  50. package/dist/pipeline/flow-specs/task-source/manual-input.json +216 -0
  51. package/dist/pipeline/node-registry.js +41 -1
  52. package/dist/pipeline/node-runner.js +3 -2
  53. package/dist/pipeline/nodes/build-review-fix-prompt-node.js +5 -1
  54. package/dist/pipeline/nodes/clear-ready-to-merge-node.js +11 -0
  55. package/dist/pipeline/nodes/commit-message-form-node.js +8 -0
  56. package/dist/pipeline/nodes/design-review-verdict-node.js +36 -0
  57. package/dist/pipeline/nodes/ensure-summary-json-node.js +13 -2
  58. package/dist/pipeline/nodes/fetch-gitlab-diff-node.js +19 -2
  59. package/dist/pipeline/nodes/fetch-gitlab-review-node.js +19 -2
  60. package/dist/pipeline/nodes/flow-run-node.js +226 -7
  61. package/dist/pipeline/nodes/git-commit-form-node.js +8 -0
  62. package/dist/pipeline/nodes/gitlab-review-artifacts-node.js +19 -2
  63. package/dist/pipeline/nodes/jira-fetch-node.js +50 -4
  64. package/dist/pipeline/nodes/llm-prompt-node.js +32 -12
  65. package/dist/pipeline/nodes/planning-bundle-node.js +10 -0
  66. package/dist/pipeline/nodes/review-verdict-node.js +86 -0
  67. package/dist/pipeline/nodes/select-files-form-node.js +8 -0
  68. package/dist/pipeline/nodes/structured-summary-node.js +24 -0
  69. package/dist/pipeline/nodes/user-input-node.js +38 -3
  70. package/dist/pipeline/nodes/write-selection-file-node.js +20 -4
  71. package/dist/pipeline/prompt-registry.js +3 -1
  72. package/dist/pipeline/prompt-runtime.js +4 -1
  73. package/dist/pipeline/review-iteration.js +26 -0
  74. package/dist/pipeline/spec-compiler.js +2 -0
  75. package/dist/pipeline/spec-types.js +3 -0
  76. package/dist/pipeline/spec-validator.js +14 -0
  77. package/dist/pipeline/value-resolver.js +74 -1
  78. package/dist/prompts.js +36 -14
  79. package/dist/review-severity.js +45 -0
  80. package/dist/runtime/artifact-registry.js +402 -0
  81. package/dist/runtime/design-review-input-contract.js +17 -16
  82. package/dist/runtime/env-loader.js +3 -0
  83. package/dist/runtime/execution-routing-store.js +134 -0
  84. package/dist/runtime/execution-routing.js +227 -0
  85. package/dist/runtime/interactive-execution-routing.js +462 -0
  86. package/dist/runtime/plan-revise-input-contract.js +35 -32
  87. package/dist/runtime/planning-bundle.js +123 -0
  88. package/dist/runtime/ready-to-merge.js +22 -1
  89. package/dist/runtime/review-input-contract.js +100 -0
  90. package/dist/structured-artifact-schema-registry.js +9 -0
  91. package/dist/structured-artifact-schemas.json +140 -1
  92. package/dist/structured-artifacts.js +77 -6
  93. package/dist/user-input.js +70 -3
  94. package/package.json +6 -3
@@ -3,21 +3,18 @@ import blessed from "neo-blessed";
3
3
  import { renderMarkdownToTerminal } from "./markdown.js";
4
4
  import { setOutputAdapter, stripAnsi } from "./tui.js";
5
5
  import { FlowInterruptedError, TaskRunnerError } from "./errors.js";
6
- import { buildInitialUserInputValues, validateUserInputValues, } from "./user-input.js";
6
+ import { buildProgressViewModel } from "./interactive/progress.js";
7
+ import { selectHeaderLabel } from "./interactive/selectors.js";
8
+ import { buildFlowTree, computeVisibleFlowItems, makeFlowKey, makeFolderKey } from "./interactive/tree.js";
9
+ import { buildInitialUserInputValues, normalizeUserInputFieldValue, resolveFieldDefinition, validateUserInputValues, } from "./user-input.js";
7
10
  const CONFIRM_MIN_WIDTH = 44;
8
11
  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
12
  function escapeBlessedTags(text) {
19
13
  return text.replace(/[{}]/g, (ch) => (ch === "{" ? "{open}" : "{close}"));
20
14
  }
15
+ function stripBlessedTags(text) {
16
+ return text.replace(/\{[^}]+\}/g, "");
17
+ }
21
18
  function textIndexToLineColumn(value, index) {
22
19
  const boundedIndex = Math.max(0, Math.min(value.length, index));
23
20
  const beforeCursor = value.slice(0, boundedIndex);
@@ -37,91 +34,6 @@ function textLineColumnToIndex(value, line, column) {
37
34
  const targetLine = lines[boundedLine] ?? "";
38
35
  return index + Math.max(0, Math.min(targetLine.length, column));
39
36
  }
40
- function buildFlowTree(flows) {
41
- const roots = new Map();
42
- const ensureFolder = (pathSegments) => {
43
- const firstSegment = pathSegments[0];
44
- if (!firstSegment) {
45
- throw new Error("Flow tree folder path cannot be empty.");
46
- }
47
- const rootFolder = roots.get(firstSegment);
48
- let currentFolder;
49
- if (rootFolder) {
50
- currentFolder = rootFolder;
51
- }
52
- else {
53
- currentFolder = {
54
- kind: "folder",
55
- key: makeFolderKey([firstSegment]),
56
- name: firstSegment,
57
- pathSegments: [firstSegment],
58
- children: [],
59
- };
60
- roots.set(firstSegment, currentFolder);
61
- }
62
- for (let index = 1; index < pathSegments.length; index += 1) {
63
- const segment = pathSegments[index] ?? "";
64
- const folderPath = pathSegments.slice(0, index + 1);
65
- let nextFolder = currentFolder.children.find((child) => child.kind === "folder" && child.name === segment);
66
- if (!nextFolder) {
67
- nextFolder = {
68
- kind: "folder",
69
- key: makeFolderKey(folderPath),
70
- name: segment,
71
- pathSegments: folderPath,
72
- children: [],
73
- };
74
- currentFolder.children.push(nextFolder);
75
- }
76
- currentFolder = nextFolder;
77
- }
78
- return currentFolder;
79
- };
80
- for (const flow of flows) {
81
- if (flow.treePath.length === 0) {
82
- continue;
83
- }
84
- const folderPath = flow.treePath.slice(0, -1);
85
- const leafName = flow.treePath[flow.treePath.length - 1] ?? flow.id;
86
- const parent = ensureFolder(folderPath);
87
- parent.children.push({
88
- kind: "flow",
89
- key: makeFlowKey(flow.id),
90
- name: leafName,
91
- pathSegments: [...flow.treePath],
92
- flow,
93
- });
94
- }
95
- const sortNodes = (nodes) => [...nodes]
96
- .sort((left, right) => {
97
- if (left.kind !== right.kind) {
98
- return left.kind === "folder" ? -1 : 1;
99
- }
100
- return compareTreeNames(left.name, right.name);
101
- })
102
- .map((node) => node.kind === "folder"
103
- ? {
104
- ...node,
105
- children: sortNodes(node.children),
106
- }
107
- : node);
108
- const orderedRootNames = ["custom", "default"];
109
- const sortedRoots = [...roots.values()].sort((left, right) => {
110
- const leftIndex = orderedRootNames.indexOf(left.name);
111
- const rightIndex = orderedRootNames.indexOf(right.name);
112
- if (leftIndex !== -1 || rightIndex !== -1) {
113
- if (leftIndex === -1) {
114
- return 1;
115
- }
116
- if (rightIndex === -1) {
117
- return -1;
118
- }
119
- return leftIndex - rightIndex;
120
- }
121
- return compareTreeNames(left.name, right.name);
122
- });
123
- return sortNodes(sortedRoots);
124
- }
125
37
  export class InteractiveUi {
126
38
  options;
127
39
  screen;
@@ -136,6 +48,7 @@ export class InteractiveUi {
136
48
  help;
137
49
  confirm;
138
50
  formModal;
51
+ formPreviewBox;
139
52
  formLineInput;
140
53
  formTextInput;
141
54
  formBooleanInput;
@@ -324,6 +237,29 @@ export class InteractiveUi {
324
237
  fg: "white",
325
238
  },
326
239
  });
240
+ this.formPreviewBox = blessed.box({
241
+ parent: this.formModal,
242
+ top: 0,
243
+ left: 0,
244
+ width: "100%-2",
245
+ height: 8,
246
+ hidden: true,
247
+ tags: true,
248
+ mouse: true,
249
+ keys: true,
250
+ vi: true,
251
+ scrollable: true,
252
+ alwaysScroll: true,
253
+ border: "line",
254
+ scrollbar: {
255
+ ch: " ",
256
+ inverse: true,
257
+ },
258
+ style: {
259
+ border: { fg: "cyan" },
260
+ fg: "white",
261
+ },
262
+ });
327
263
  this.formTextInput = blessed.box({
328
264
  parent: this.formModal,
329
265
  top: 0,
@@ -854,7 +790,12 @@ export class InteractiveUi {
854
790
  if (!this.activeFormSession) {
855
791
  return null;
856
792
  }
857
- return this.activeFormSession.form.fields[this.activeFormSession.currentFieldIndex] ?? null;
793
+ const field = this.activeFormSession.form.fields[this.activeFormSession.currentFieldIndex] ?? null;
794
+ if (!field) {
795
+ return null;
796
+ }
797
+ normalizeUserInputFieldValue(field, this.activeFormSession.values);
798
+ return resolveFieldDefinition(field, this.activeFormSession.values);
858
799
  }
859
800
  formModalInnerHeight() {
860
801
  const rawHeight = typeof this.formModal.height === "number" ? this.formModal.height : this.formModal?.lpos?.yi
@@ -872,9 +813,17 @@ export class InteractiveUi {
872
813
  const paddingRight = Number(this.formModal.padding?.right ?? 0);
873
814
  return Math.max(24, rawWidth - 2 - paddingLeft - paddingRight);
874
815
  }
816
+ renderedFormLineCount(lines) {
817
+ const width = Math.max(1, this.formModalInnerWidth() - 2);
818
+ return lines.reduce((total, line) => {
819
+ const visible = stripAnsi(stripBlessedTags(line));
820
+ return total + Math.max(1, Math.ceil(visible.length / width));
821
+ }, 0);
822
+ }
875
823
  renderActiveForm() {
876
824
  if (!this.activeFormSession) {
877
825
  this.formModal.hide();
826
+ this.hideFormPreviewBox();
878
827
  this.hideFormLineInput();
879
828
  this.hideFormTextInput();
880
829
  this.hideFormBooleanInput();
@@ -907,6 +856,7 @@ export class InteractiveUi {
907
856
  let footerHint = ` Form: Enter ${isLastField ? "submit" : "confirm"} | Tab next | Shift+Tab prev | Esc cancel `;
908
857
  let hintLines = [];
909
858
  if (field.type === "boolean") {
859
+ this.hideFormPreviewBox();
910
860
  this.hideFormLineInput();
911
861
  this.hideFormTextInput();
912
862
  this.hideFormSelectInput();
@@ -927,6 +877,7 @@ export class InteractiveUi {
927
877
  footerHint = ` Form: Space toggle | Enter ${isLastField ? "submit" : "next"} | Tab switch | Esc cancel `;
928
878
  }
929
879
  else if (field.type === "text") {
880
+ this.hideFormPreviewBox();
930
881
  this.hideFormBooleanInput();
931
882
  this.hideFormSelectInput();
932
883
  helperLines.push("{cyan-fg}Use the standard editor below.{/cyan-fg}");
@@ -958,8 +909,31 @@ export class InteractiveUi {
958
909
  this.hideFormLineInput();
959
910
  this.hideFormTextInput();
960
911
  this.hideFormBooleanInput();
961
- helperLines.push("{cyan-fg}Use the list below.{/cyan-fg}");
962
- this.ensureFormSelectInputVisible(headerLines.length + helperLines.length, 0);
912
+ const previewContent = session.form.preview?.trim() ?? "";
913
+ let previewScrollable = false;
914
+ if (previewContent.length > 0) {
915
+ this.hideFormPreviewBox();
916
+ helperLines.push("{cyan-fg}Review the content above, then choose an action below.{/cyan-fg}");
917
+ lines.push(...helperLines);
918
+ const previewLines = previewContent.split("\n");
919
+ const viewportHeight = this.formPreviewViewportHeight(this.renderedFormLineCount(lines), 0);
920
+ const maxOffset = Math.max(0, previewLines.length - viewportHeight);
921
+ session.previewScrollOffset = Math.max(0, Math.min(session.previewScrollOffset, maxOffset));
922
+ const visibleLines = previewLines.slice(session.previewScrollOffset, session.previewScrollOffset + viewportHeight);
923
+ lines.push("", ...visibleLines);
924
+ previewScrollable = maxOffset > 0;
925
+ if (previewScrollable) {
926
+ const firstVisibleLine = session.previewScrollOffset + 1;
927
+ const lastVisibleLine = session.previewScrollOffset + visibleLines.length;
928
+ lines.push("", `{gray-fg}Preview ${firstVisibleLine}-${lastVisibleLine} of ${previewLines.length}. Use PageUp/PageDown to scroll.{/gray-fg}`);
929
+ }
930
+ }
931
+ else {
932
+ this.hideFormPreviewBox();
933
+ helperLines.push("{cyan-fg}Use the list below.{/cyan-fg}");
934
+ lines.push(...helperLines);
935
+ }
936
+ this.ensureFormSelectInputVisible(this.renderedFormLineCount(lines), 0);
963
937
  const preferredOptionIndex = field.type === "single-select"
964
938
  ? this.selectedOptionIndexForField(field)
965
939
  : session.currentOptionIndex;
@@ -977,11 +951,17 @@ export class InteractiveUi {
977
951
  this.formSelectInput.select(currentOptionIndex);
978
952
  hintLines = [
979
953
  `Up/Down: move | Space: ${field.type === "single-select" ? "pick" : "toggle"} | Enter: ${isLastField ? "submit" : "confirm and next"}`,
980
- "Tab/Shift+Tab: switch field | Esc: cancel",
954
+ previewScrollable
955
+ ? "PageUp/PageDown: scroll preview | Tab/Shift+Tab: switch field | Esc: cancel"
956
+ : "Tab/Shift+Tab: switch field | Esc: cancel",
981
957
  ];
982
- footerHint = ` Form: Up/Down move | Enter ${isLastField ? "submit" : "next"} | Tab switch | Esc cancel `;
958
+ footerHint = previewScrollable
959
+ ? ` Form: Up/Down move | PageUp/PageDown preview | Enter ${isLastField ? "submit" : "next"} | Tab switch | Esc cancel `
960
+ : ` Form: Up/Down move | Enter ${isLastField ? "submit" : "next"} | Tab switch | Esc cancel `;
961
+ }
962
+ if (field.type !== "single-select" && field.type !== "multi-select") {
963
+ lines.push(...helperLines);
983
964
  }
984
- lines.push(...helperLines);
985
965
  this.formModal.setContent(lines.join("\n"));
986
966
  this.formModal.setScroll(0);
987
967
  this.formModal.show();
@@ -1018,14 +998,17 @@ export class InteractiveUi {
1018
998
  const current = String(this.activeFormSession.values[nextField.id] ?? "");
1019
999
  this.activeFormSession.currentTextCursorIndex = current.length;
1020
1000
  this.activeFormSession.currentOptionIndex = 0;
1001
+ this.activeFormSession.previewScrollOffset = 0;
1021
1002
  }
1022
1003
  else if (nextField?.type === "single-select" || nextField?.type === "multi-select") {
1023
1004
  this.activeFormSession.currentTextCursorIndex = 0;
1024
1005
  this.activeFormSession.currentOptionIndex = this.selectedOptionIndexForField(nextField);
1006
+ this.activeFormSession.previewScrollOffset = 0;
1025
1007
  }
1026
1008
  else {
1027
1009
  this.activeFormSession.currentTextCursorIndex = 0;
1028
1010
  this.activeFormSession.currentOptionIndex = 0;
1011
+ this.activeFormSession.previewScrollOffset = 0;
1029
1012
  }
1030
1013
  this.renderActiveForm();
1031
1014
  }
@@ -1145,17 +1128,39 @@ export class InteractiveUi {
1145
1128
  this.positionFormHint(this.formBooleanInput.top + this.formBooleanInput.height);
1146
1129
  }
1147
1130
  ensureFormSelectInputVisible(headerLineCount, footerLineCount) {
1148
- const reservedTop = Math.max(1, headerLineCount + 2);
1131
+ const previewVisible = this.formPreviewBox.visible;
1132
+ const reservedTop = previewVisible
1133
+ ? Number(this.formPreviewBox.top ?? 1) + Number(this.formPreviewBox.height ?? 6) + 1
1134
+ : Math.max(1, headerLineCount + 2);
1149
1135
  const availableHeight = Math.max(6, this.formModalInnerHeight() - reservedTop - footerLineCount - 2);
1150
1136
  this.formSelectInput.top = reservedTop;
1151
1137
  this.formSelectInput.left = 1;
1152
1138
  this.formSelectInput.width = Math.max(24, this.formModalInnerWidth() - 4);
1153
- this.formSelectInput.height = Math.max(4, availableHeight - 2);
1139
+ this.formSelectInput.height = previewVisible
1140
+ ? Math.max(5, availableHeight - 1)
1141
+ : Math.max(4, availableHeight - 2);
1154
1142
  this.formSelectInput.show();
1155
1143
  this.formSelectInput.setFront();
1156
1144
  this.formSelectInput.focus();
1157
1145
  this.positionFormHint(this.formSelectInput.top + this.formSelectInput.height);
1158
1146
  }
1147
+ ensureFormPreviewBoxVisible(headerLineCount, footerLineCount) {
1148
+ const reservedTop = Math.max(1, headerLineCount + 2);
1149
+ const hintHeight = 2;
1150
+ const selectHeight = Math.max(5, Math.min(10, Math.floor(this.formModalInnerHeight() * 0.32)));
1151
+ const availableHeight = Math.max(6, this.formModalInnerHeight() - reservedTop - footerLineCount - selectHeight - hintHeight - 3);
1152
+ this.formPreviewBox.top = reservedTop;
1153
+ this.formPreviewBox.left = 1;
1154
+ this.formPreviewBox.width = Math.max(24, this.formModalInnerWidth() - 4);
1155
+ this.formPreviewBox.height = Math.max(6, Math.min(12, availableHeight));
1156
+ }
1157
+ formPreviewViewportHeight(headerLineCount, footerLineCount) {
1158
+ const reservedTop = Math.max(1, headerLineCount + 2);
1159
+ const hintHeight = 2;
1160
+ const selectHeight = Math.max(5, Math.min(10, Math.floor(this.formModalInnerHeight() * 0.32)));
1161
+ const availableHeight = Math.max(4, this.formModalInnerHeight() - reservedTop - footerLineCount - selectHeight - hintHeight - 1);
1162
+ return Math.max(4, Math.min(10, availableHeight));
1163
+ }
1159
1164
  positionFormHint(top) {
1160
1165
  this.formHint.top = top;
1161
1166
  this.formHint.left = 1;
@@ -1179,6 +1184,12 @@ export class InteractiveUi {
1179
1184
  this.formTextInput.blur();
1180
1185
  }
1181
1186
  }
1187
+ hideFormPreviewBox() {
1188
+ this.formPreviewBox.hide();
1189
+ if (typeof this.formPreviewBox.blur === "function") {
1190
+ this.formPreviewBox.blur();
1191
+ }
1192
+ }
1182
1193
  hideFormBooleanInput() {
1183
1194
  this.formBooleanInput.hide();
1184
1195
  if (typeof this.formBooleanInput.blur === "function") {
@@ -1269,10 +1280,12 @@ export class InteractiveUi {
1269
1280
  };
1270
1281
  this.activeFormSession = null;
1271
1282
  this.formModal.hide();
1283
+ this.hideFormPreviewBox();
1272
1284
  this.hideFormLineInput();
1273
1285
  this.hideFormTextInput();
1274
1286
  this.hideFormBooleanInput();
1275
1287
  this.hideFormSelectInput();
1288
+ this.hideFormHint();
1276
1289
  this.focusPane("flows");
1277
1290
  session.resolve(result);
1278
1291
  this.renderActiveForm();
@@ -1289,10 +1302,12 @@ export class InteractiveUi {
1289
1302
  const session = this.activeFormSession;
1290
1303
  this.activeFormSession = null;
1291
1304
  this.formModal.hide();
1305
+ this.hideFormPreviewBox();
1292
1306
  this.hideFormLineInput();
1293
1307
  this.hideFormTextInput();
1294
1308
  this.hideFormBooleanInput();
1295
1309
  this.hideFormSelectInput();
1310
+ this.hideFormHint();
1296
1311
  this.focusPane("flows");
1297
1312
  session.reject(new TaskRunnerError(`User cancelled form '${session.form.formId}'.`));
1298
1313
  this.renderActiveForm();
@@ -1304,10 +1319,12 @@ export class InteractiveUi {
1304
1319
  const session = this.activeFormSession;
1305
1320
  this.activeFormSession = null;
1306
1321
  this.formModal.hide();
1322
+ this.hideFormPreviewBox();
1307
1323
  this.hideFormLineInput();
1308
1324
  this.hideFormTextInput();
1309
1325
  this.hideFormBooleanInput();
1310
1326
  this.hideFormSelectInput();
1327
+ this.hideFormHint();
1311
1328
  this.focusPane("flows");
1312
1329
  session.reject(new FlowInterruptedError(message));
1313
1330
  this.renderActiveForm();
@@ -1503,6 +1520,32 @@ export class InteractiveUi {
1503
1520
  this.requestRender();
1504
1521
  return;
1505
1522
  }
1523
+ if (key.name === "pageup" || key.name === "pagedown") {
1524
+ const session = this.activeFormSession;
1525
+ const previewContent = session?.form.preview?.trim() ?? "";
1526
+ if (session && previewContent.length > 0) {
1527
+ const previewLines = previewContent.split("\n");
1528
+ const headerLines = [`{bold}${session.form.title}{/bold}`];
1529
+ if (session.form.description?.trim()) {
1530
+ headerLines.push("");
1531
+ headerLines.push(session.form.description.trim());
1532
+ }
1533
+ headerLines.push("");
1534
+ headerLines.push(`Field ${session.currentFieldIndex + 1}/${session.form.fields.length}`);
1535
+ headerLines.push(`{yellow-fg}${field.label}{/yellow-fg}`);
1536
+ if (field.help?.trim()) {
1537
+ headerLines.push(field.help.trim());
1538
+ }
1539
+ headerLines.push("");
1540
+ headerLines.push("{cyan-fg}Review the content above, then choose an action below.{/cyan-fg}");
1541
+ const viewportHeight = this.formPreviewViewportHeight(this.renderedFormLineCount(headerLines), 0);
1542
+ if (previewLines.length > viewportHeight) {
1543
+ session.previewScrollOffset = Math.max(0, Math.min(previewLines.length - viewportHeight, session.previewScrollOffset + (key.name === "pageup" ? -viewportHeight : viewportHeight)));
1544
+ this.renderActiveForm();
1545
+ return;
1546
+ }
1547
+ }
1548
+ }
1506
1549
  if (key.name === "space") {
1507
1550
  this.syncActiveSelectFieldValue();
1508
1551
  this.toggleActiveFormValue();
@@ -1587,70 +1630,22 @@ export class InteractiveUi {
1587
1630
  : this.currentFlowId === flow.id
1588
1631
  ? this.flowState.executionState
1589
1632
  : null;
1633
+ const progressViewModel = buildProgressViewModel(flow, flowState);
1590
1634
  const lines = [`{bold}${flow.label}{/bold}`, ""];
1591
- let anchorLine = null;
1592
- let sawExecutedItem = false;
1593
- const rememberAnchor = (status) => {
1594
- if (status === "running") {
1595
- anchorLine = lines.length;
1596
- sawExecutedItem = true;
1597
- return;
1598
- }
1599
- if (status === "done" || status === "skipped") {
1600
- sawExecutedItem = true;
1601
- return;
1602
- }
1603
- if (status === "pending" && sawExecutedItem && anchorLine === null) {
1604
- anchorLine = lines.length;
1605
- }
1606
- };
1607
- for (const item of this.visiblePhaseItems(flow, flowState)) {
1608
- if (item.kind === "group") {
1609
- const visiblePhases = item.phases.filter((phase) => this.shouldDisplayPhase(flow, flowState, phase));
1610
- if (visiblePhases.length === 0) {
1611
- continue;
1612
- }
1613
- const groupStatus = this.statusForGroup(flow, visiblePhases, flowState);
1614
- rememberAnchor(groupStatus);
1615
- lines.push(`${this.symbolForGroup(flow.id, flow, visiblePhases, flowState)} ${this.colorizeProgressLabel(item.label, groupStatus)}`);
1616
- for (const phase of visiblePhases) {
1617
- const phaseState = flowState?.phases.find((candidate) => candidate.id === phase.id);
1618
- const phaseStatus = this.displayStatusForPhase(flowState, flow, phase, phaseState?.status ?? null);
1619
- rememberAnchor(phaseStatus);
1620
- lines.push(` ${this.symbolForStatus(flow.id, phaseStatus)} ${this.colorizeProgressLabel(this.displayPhaseId(phase), phaseStatus)}`);
1621
- for (const step of phase.steps) {
1622
- const stepState = phaseState?.steps.find((candidate) => candidate.id === step.id);
1623
- const stepStatus = this.displayStatusForStep(flowState, flow, phase, stepState?.status ?? null);
1624
- rememberAnchor(stepStatus);
1625
- lines.push(` ${this.symbolForStatus(flow.id, stepStatus)} ${this.colorizeProgressLabel(step.id, stepStatus)}`);
1626
- }
1627
- }
1628
- lines.push("");
1629
- continue;
1630
- }
1631
- const phase = item.phase;
1632
- if (!this.shouldDisplayPhase(flow, flowState, phase)) {
1635
+ for (const item of progressViewModel.items) {
1636
+ if (item.kind === "termination") {
1637
+ const symbol = item.status === "done" ? "{green-fg}✓{/green-fg}" : "{yellow-fg}■{/yellow-fg}";
1638
+ lines.push(`${symbol} ${this.colorizeProgressLabel(item.label, item.status)}`);
1639
+ lines.push(`{gray-fg}${item.detail}{/gray-fg}`);
1633
1640
  continue;
1634
1641
  }
1635
- const phaseState = flowState?.phases.find((candidate) => candidate.id === phase.id);
1636
- const phaseStatus = this.displayStatusForPhase(flowState, flow, phase, phaseState?.status ?? null);
1637
- rememberAnchor(phaseStatus);
1638
- lines.push(`${this.symbolForStatus(flow.id, phaseStatus)} ${this.colorizeProgressLabel(this.displayPhaseId(phase), phaseStatus)}`);
1639
- for (const step of phase.steps) {
1640
- const stepState = phaseState?.steps.find((candidate) => candidate.id === step.id);
1641
- const stepStatus = this.displayStatusForStep(flowState, flow, phase, stepState?.status ?? null);
1642
- rememberAnchor(stepStatus);
1643
- lines.push(` ${this.symbolForStatus(flow.id, stepStatus)} ${this.colorizeProgressLabel(step.id, stepStatus)}`);
1644
- }
1645
- lines.push("");
1646
- }
1647
- if (flowState?.terminated) {
1648
- lines.push(`{green-fg}✓{/green-fg} {green-fg}Flow completed successfully{/green-fg}`);
1649
- lines.push(`{gray-fg}Reason: ${flowState.terminationReason ?? "flow terminated"}{/gray-fg}`);
1642
+ const indent = " ".repeat(item.depth);
1643
+ lines.push(`${indent}${this.symbolForStatus(flow.id, item.status)} ${this.colorizeProgressLabel(item.label, item.status)}`);
1650
1644
  }
1651
1645
  this.progress.setContent(lines.join("\n").trimEnd());
1652
- if (this.busy && this.activeFlowId() === flow.id && anchorLine !== null) {
1646
+ if (this.busy && this.activeFlowId() === flow.id && progressViewModel.anchorIndex !== null) {
1653
1647
  const viewportHeight = Math.max(1, Number(this.progress.height) - 2);
1648
+ const anchorLine = progressViewModel.anchorIndex + 2;
1654
1649
  const targetScroll = Math.max(0, anchorLine - Math.floor(viewportHeight / 2));
1655
1650
  this.progress.setScroll(targetScroll);
1656
1651
  }
@@ -1969,6 +1964,7 @@ export class InteractiveUi {
1969
1964
  currentFieldIndex: 0,
1970
1965
  currentOptionIndex: initialOptionIndex,
1971
1966
  currentTextCursorIndex: initialCursorIndex,
1967
+ previewScrollOffset: 0,
1972
1968
  resolve,
1973
1969
  reject,
1974
1970
  };
@@ -2083,44 +2079,13 @@ export class InteractiveUi {
2083
2079
  this.requestRender();
2084
2080
  }
2085
2081
  computeVisibleFlowItems() {
2086
- const items = [];
2087
- const walk = (nodes, depth) => {
2088
- for (const node of nodes) {
2089
- if (node.kind === "folder") {
2090
- items.push({
2091
- kind: "folder",
2092
- key: node.key,
2093
- name: node.name,
2094
- depth,
2095
- pathSegments: [...node.pathSegments],
2096
- });
2097
- if (this.expandedFlowFolders.has(node.key)) {
2098
- walk(node.children, depth + 1);
2099
- }
2100
- continue;
2101
- }
2102
- items.push({
2103
- kind: "flow",
2104
- key: node.key,
2105
- name: node.name,
2106
- depth,
2107
- pathSegments: [...node.pathSegments],
2108
- flow: node.flow,
2109
- });
2110
- }
2111
- };
2112
- walk(this.flowTree, 0);
2113
- return items;
2082
+ return computeVisibleFlowItems(this.flowTree, this.expandedFlowFolders);
2114
2083
  }
2115
2084
  selectedFlowTreeItem() {
2116
2085
  return this.visibleFlowItems.find((item) => item.key === this.selectedFlowItemKey);
2117
2086
  }
2118
2087
  selectedHeaderLabel() {
2119
- const selectedItem = this.selectedFlowTreeItem();
2120
- if (!selectedItem) {
2121
- return this.selectedFlowId;
2122
- }
2123
- return selectedItem.kind === "folder" ? selectedItem.pathSegments.join("/") : selectedItem.flow.label;
2088
+ return selectHeaderLabel(this.selectedFlowTreeItem(), this.selectedFlowId);
2124
2089
  }
2125
2090
  refreshVisibleFlowItems() {
2126
2091
  this.visibleFlowItems = this.computeVisibleFlowItems();
@@ -17,5 +17,6 @@ export function createPipelineContext(input) {
17
17
  nodes: createNodeRegistry(),
18
18
  ...(input.setSummary ? { setSummary: input.setSummary } : {}),
19
19
  ...(input.requestUserInput ? { requestUserInput: input.requestUserInput } : {}),
20
+ ...(input.executionRouting ? { executionRouting: input.executionRouting } : {}),
20
21
  };
21
22
  }