agentweaver 0.1.2 → 0.1.4

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 (61) hide show
  1. package/README.md +58 -23
  2. package/dist/artifacts.js +58 -2
  3. package/dist/executors/claude-executor.js +12 -2
  4. package/dist/executors/claude-summary-executor.js +1 -1
  5. package/dist/executors/codex-docker-executor.js +1 -1
  6. package/dist/executors/codex-local-executor.js +1 -1
  7. package/dist/executors/configs/claude-config.js +2 -1
  8. package/dist/executors/verify-build-executor.js +110 -9
  9. package/dist/index.js +466 -452
  10. package/dist/interactive-ui.js +538 -194
  11. package/dist/jira.js +3 -1
  12. package/dist/pipeline/auto-flow.js +9 -0
  13. package/dist/pipeline/checks.js +5 -0
  14. package/dist/pipeline/context.js +2 -0
  15. package/dist/pipeline/declarative-flow-runner.js +262 -0
  16. package/dist/pipeline/declarative-flows.js +24 -0
  17. package/dist/pipeline/flow-specs/auto.json +485 -0
  18. package/dist/pipeline/flow-specs/bug-analyze.json +140 -0
  19. package/dist/pipeline/flow-specs/bug-fix.json +44 -0
  20. package/dist/pipeline/flow-specs/implement.json +47 -0
  21. package/dist/pipeline/flow-specs/mr-description.json +61 -0
  22. package/dist/pipeline/flow-specs/plan.json +88 -0
  23. package/dist/pipeline/flow-specs/preflight.json +174 -0
  24. package/dist/pipeline/flow-specs/review-fix.json +76 -0
  25. package/dist/pipeline/flow-specs/review.json +233 -0
  26. package/dist/pipeline/flow-specs/run-linter-loop.json +149 -0
  27. package/dist/pipeline/flow-specs/run-tests-loop.json +149 -0
  28. package/dist/pipeline/flow-specs/task-describe.json +61 -0
  29. package/dist/pipeline/flow-specs/test-fix.json +24 -0
  30. package/dist/pipeline/flow-specs/test-linter-fix.json +24 -0
  31. package/dist/pipeline/flow-specs/test.json +19 -0
  32. package/dist/pipeline/flows/implement-flow.js +3 -4
  33. package/dist/pipeline/flows/preflight-flow.js +17 -57
  34. package/dist/pipeline/flows/review-fix-flow.js +3 -4
  35. package/dist/pipeline/flows/review-flow.js +8 -4
  36. package/dist/pipeline/flows/test-fix-flow.js +3 -4
  37. package/dist/pipeline/node-registry.js +74 -0
  38. package/dist/pipeline/node-runner.js +9 -3
  39. package/dist/pipeline/nodes/build-failure-summary-node.js +4 -4
  40. package/dist/pipeline/nodes/claude-prompt-node.js +54 -0
  41. package/dist/pipeline/nodes/claude-summary-node.js +12 -6
  42. package/dist/pipeline/nodes/codex-docker-prompt-node.js +1 -0
  43. package/dist/pipeline/nodes/codex-local-prompt-node.js +32 -0
  44. package/dist/pipeline/nodes/file-check-node.js +15 -0
  45. package/dist/pipeline/nodes/flow-run-node.js +40 -0
  46. package/dist/pipeline/nodes/summary-file-load-node.js +16 -0
  47. package/dist/pipeline/nodes/task-summary-node.js +12 -6
  48. package/dist/pipeline/nodes/verify-build-node.js +1 -0
  49. package/dist/pipeline/prompt-registry.js +27 -0
  50. package/dist/pipeline/prompt-runtime.js +18 -0
  51. package/dist/pipeline/registry.js +0 -2
  52. package/dist/pipeline/spec-compiler.js +213 -0
  53. package/dist/pipeline/spec-loader.js +14 -0
  54. package/dist/pipeline/spec-types.js +1 -0
  55. package/dist/pipeline/spec-validator.js +302 -0
  56. package/dist/pipeline/value-resolver.js +217 -0
  57. package/dist/prompts.js +22 -3
  58. package/dist/runtime/process-runner.js +24 -23
  59. package/dist/structured-artifacts.js +178 -0
  60. package/dist/tui.js +39 -0
  61. package/package.json +2 -2
@@ -5,28 +5,40 @@ export class InteractiveUi {
5
5
  options;
6
6
  screen;
7
7
  header;
8
- summary;
8
+ progress;
9
+ flowList;
9
10
  status;
10
- sidebar;
11
+ summary;
11
12
  log;
12
- input;
13
13
  footer;
14
14
  help;
15
- history;
16
- historyIndex;
15
+ confirm;
16
+ flowMap;
17
17
  busy = false;
18
- currentCommand = "idle";
18
+ currentFlowId = null;
19
+ selectedFlowId;
19
20
  summaryText = "";
20
- focusedPane = "input";
21
+ focusedPane = "flows";
21
22
  currentNode = null;
22
23
  currentExecutor = null;
23
24
  spinnerFrame = 0;
24
25
  spinnerTimer = null;
25
26
  runningStartedAt = null;
26
- constructor(options, history) {
27
+ renderScheduled = false;
28
+ logFlushTimer = null;
29
+ pendingLogLines = [];
30
+ flowState = {
31
+ flowId: null,
32
+ executionState: null,
33
+ };
34
+ failedFlowId = null;
35
+ constructor(options) {
27
36
  this.options = options;
28
- this.history = history;
29
- this.historyIndex = history.length;
37
+ if (options.flows.length === 0) {
38
+ throw new Error("Interactive UI requires at least one flow.");
39
+ }
40
+ this.flowMap = new Map(options.flows.map((flow) => [flow.id, flow]));
41
+ this.selectedFlowId = options.flows[0]?.id ?? "auto";
30
42
  this.screen = blessed.screen({
31
43
  smartCSR: true,
32
44
  fullUnicode: true,
@@ -51,14 +63,14 @@ export class InteractiveUi {
51
63
  fg: "white",
52
64
  },
53
65
  });
54
- this.sidebar = blessed.box({
66
+ this.progress = blessed.box({
55
67
  parent: this.screen,
56
68
  top: 3,
57
69
  left: 0,
58
- width: "28%",
59
- bottom: 10,
70
+ width: "34%",
71
+ height: "50%-1",
60
72
  tags: true,
61
- label: " Commands ",
73
+ label: " Current Flow ",
62
74
  padding: {
63
75
  left: 1,
64
76
  right: 1,
@@ -69,18 +81,21 @@ export class InteractiveUi {
69
81
  keys: true,
70
82
  vi: true,
71
83
  style: {
72
- border: { fg: "cyan" },
84
+ border: { fg: "green" },
73
85
  fg: "white",
74
86
  },
75
87
  });
76
- this.summary = blessed.box({
88
+ this.flowList = blessed.list({
77
89
  parent: this.screen,
78
- top: 3,
79
- left: "28%",
80
- width: "72%",
81
- height: 8,
90
+ top: "50%+2",
91
+ left: 0,
92
+ width: "34%",
93
+ bottom: 10,
94
+ keys: true,
95
+ vi: true,
96
+ mouse: true,
82
97
  tags: true,
83
- label: " Task Summary ",
98
+ label: " Flows ",
84
99
  padding: {
85
100
  left: 1,
86
101
  right: 1,
@@ -88,18 +103,47 @@ export class InteractiveUi {
88
103
  border: "line",
89
104
  scrollable: true,
90
105
  alwaysScroll: true,
106
+ style: {
107
+ border: { fg: "cyan" },
108
+ fg: "white",
109
+ selected: {
110
+ fg: "black",
111
+ bg: "green",
112
+ bold: true,
113
+ },
114
+ },
115
+ });
116
+ this.confirm = blessed.box({
117
+ parent: this.screen,
118
+ top: "center",
119
+ left: "center",
120
+ width: 44,
121
+ height: 8,
122
+ hidden: true,
123
+ tags: true,
124
+ label: " Confirm ",
125
+ padding: {
126
+ left: 1,
127
+ right: 1,
128
+ top: 1,
129
+ bottom: 1,
130
+ },
131
+ border: "line",
91
132
  keys: true,
92
133
  vi: true,
93
134
  style: {
94
- border: { fg: "green" },
135
+ border: { fg: "yellow" },
136
+ bg: undefined,
95
137
  fg: "white",
96
138
  },
139
+ align: "center",
140
+ valign: "middle",
97
141
  });
98
142
  this.status = blessed.box({
99
143
  parent: this.screen,
100
144
  bottom: 4,
101
145
  left: 0,
102
- width: "28%",
146
+ width: "34%",
103
147
  height: 6,
104
148
  tags: true,
105
149
  label: " Status ",
@@ -113,12 +157,34 @@ export class InteractiveUi {
113
157
  fg: "white",
114
158
  },
115
159
  });
160
+ this.summary = blessed.box({
161
+ parent: this.screen,
162
+ top: 3,
163
+ left: "34%",
164
+ width: "66%",
165
+ height: 12,
166
+ tags: true,
167
+ label: " Task Summary ",
168
+ padding: {
169
+ left: 1,
170
+ right: 1,
171
+ },
172
+ border: "line",
173
+ scrollable: true,
174
+ alwaysScroll: true,
175
+ keys: true,
176
+ vi: true,
177
+ style: {
178
+ border: { fg: "green" },
179
+ fg: "white",
180
+ },
181
+ });
116
182
  this.log = blessed.log({
117
183
  parent: this.screen,
118
- top: 11,
184
+ top: 15,
119
185
  bottom: 4,
120
- left: "28%",
121
- width: "72%",
186
+ left: "34%",
187
+ width: "66%",
122
188
  tags: false,
123
189
  label: " Activity ",
124
190
  padding: {
@@ -140,28 +206,6 @@ export class InteractiveUi {
140
206
  fg: "white",
141
207
  },
142
208
  });
143
- this.input = blessed.textbox({
144
- parent: this.screen,
145
- bottom: 1,
146
- left: 0,
147
- width: "100%",
148
- height: 3,
149
- keys: true,
150
- inputOnFocus: true,
151
- mouse: true,
152
- label: " command ",
153
- padding: {
154
- left: 1,
155
- },
156
- border: "line",
157
- style: {
158
- border: { fg: "magenta" },
159
- fg: "white",
160
- focus: {
161
- border: { fg: "magenta" },
162
- },
163
- },
164
- });
165
209
  this.footer = blessed.box({
166
210
  parent: this.screen,
167
211
  bottom: 0,
@@ -175,8 +219,8 @@ export class InteractiveUi {
175
219
  parent: this.screen,
176
220
  top: "center",
177
221
  left: "center",
178
- width: "70%",
179
- height: "65%",
222
+ width: "64%",
223
+ height: "52%",
180
224
  hidden: true,
181
225
  tags: true,
182
226
  label: " Help ",
@@ -202,212 +246,198 @@ export class InteractiveUi {
202
246
  this.screen.key(["C-c", "q"], () => {
203
247
  this.options.onExit();
204
248
  });
205
- this.screen.key(["f1", "?"], () => {
249
+ this.screen.key(["f1", "h", "?"], () => {
250
+ if (this.confirm.visible) {
251
+ return;
252
+ }
206
253
  this.help.hidden = !this.help.hidden;
207
254
  if (!this.help.hidden) {
208
255
  this.help.focus();
209
256
  }
210
257
  else {
211
- this.input.focus();
258
+ this.focusPane("flows");
212
259
  }
213
- this.screen.render();
260
+ this.requestRender();
214
261
  });
215
262
  this.screen.key(["escape"], () => {
216
- this.help.hide();
217
- this.focusPane("input");
218
- this.screen.render();
263
+ if (!this.help.hidden) {
264
+ this.help.hide();
265
+ this.focusPane("flows");
266
+ this.requestRender();
267
+ return;
268
+ }
269
+ if (this.confirm.visible) {
270
+ this.closeConfirm();
271
+ }
219
272
  });
220
273
  this.screen.key(["C-l"], () => {
221
274
  this.log.setContent("");
222
275
  this.appendLog("Log cleared.");
223
276
  });
277
+ this.screen.key(["tab"], () => {
278
+ if (this.confirm.visible || !this.help.hidden) {
279
+ return;
280
+ }
281
+ this.cycleFocus(1);
282
+ });
224
283
  this.screen.key(["S-tab"], () => {
284
+ if (this.confirm.visible || !this.help.hidden) {
285
+ return;
286
+ }
225
287
  this.cycleFocus(-1);
226
288
  });
227
- this.screen.key(["tab"], () => {
228
- if (this.focusedPane !== "input") {
229
- this.cycleFocus(1);
289
+ this.flowList.on("select item", (_item, index) => {
290
+ const flow = this.options.flows[index];
291
+ if (!flow) {
292
+ return;
293
+ }
294
+ this.selectedFlowId = flow.id;
295
+ this.renderProgress();
296
+ this.requestRender();
297
+ });
298
+ this.flowList.key(["enter"], () => {
299
+ if (this.busy || this.confirm.visible || !this.help.hidden) {
300
+ return;
230
301
  }
302
+ this.openConfirm();
231
303
  });
232
- this.screen.key(["C-j"], () => {
233
- this.focusPane("log");
304
+ this.flowList.key(["pageup"], () => {
305
+ this.flowList.scroll(-(this.flowList.height - 2));
306
+ this.requestRender();
234
307
  });
235
- this.screen.key(["C-k"], () => {
236
- this.focusPane("input");
308
+ this.flowList.key(["pagedown"], () => {
309
+ this.flowList.scroll(this.flowList.height - 2);
310
+ this.requestRender();
237
311
  });
238
312
  this.log.key(["up"], () => {
239
313
  this.log.scroll(-1);
240
- this.screen.render();
314
+ this.requestRender();
241
315
  });
242
316
  this.log.key(["down"], () => {
243
317
  this.log.scroll(1);
244
- this.screen.render();
318
+ this.requestRender();
245
319
  });
246
320
  this.log.key(["pageup"], () => {
247
321
  this.log.scroll(-(this.log.height - 2));
248
- this.screen.render();
322
+ this.requestRender();
249
323
  });
250
324
  this.log.key(["pagedown"], () => {
251
325
  this.log.scroll(this.log.height - 2);
252
- this.screen.render();
326
+ this.requestRender();
253
327
  });
254
328
  this.log.key(["home"], () => {
255
329
  this.log.setScroll(0);
256
- this.screen.render();
330
+ this.requestRender();
257
331
  });
258
332
  this.log.key(["end"], () => {
259
333
  this.log.setScrollPerc(100);
260
- this.screen.render();
334
+ this.requestRender();
261
335
  });
262
336
  this.summary.key(["pageup"], () => {
263
337
  this.summary.scroll(-(this.summary.height - 2));
264
- this.screen.render();
338
+ this.requestRender();
265
339
  });
266
340
  this.summary.key(["pagedown"], () => {
267
341
  this.summary.scroll(this.summary.height - 2);
268
- this.screen.render();
342
+ this.requestRender();
269
343
  });
270
- this.sidebar.key(["pageup"], () => {
271
- this.sidebar.scroll(-(this.sidebar.height - 2));
272
- this.screen.render();
344
+ this.progress.key(["pageup"], () => {
345
+ this.progress.scroll(-(this.progress.height - 2));
346
+ this.requestRender();
273
347
  });
274
- this.sidebar.key(["pagedown"], () => {
275
- this.sidebar.scroll(this.sidebar.height - 2);
276
- this.screen.render();
348
+ this.progress.key(["pagedown"], () => {
349
+ this.progress.scroll(this.progress.height - 2);
350
+ this.requestRender();
277
351
  });
278
- this.input.key(["up"], () => {
279
- if (this.history.length === 0) {
352
+ this.confirm.key(["enter"], async () => {
353
+ if (this.busy || this.confirm.hidden) {
280
354
  return;
281
355
  }
282
- this.historyIndex = Math.max(0, this.historyIndex - 1);
283
- this.input.setValue(this.history[this.historyIndex] ?? "");
284
- this.screen.render();
285
- });
286
- this.input.key(["down"], () => {
287
- if (this.history.length === 0) {
288
- return;
356
+ const flowId = this.selectedFlowId;
357
+ this.closeConfirm();
358
+ this.setBusy(true, flowId);
359
+ this.clearFlowFailure(flowId);
360
+ this.setFlowDisplayState(flowId, null);
361
+ try {
362
+ await this.options.onRun(flowId);
289
363
  }
290
- this.historyIndex = Math.min(this.history.length, this.historyIndex + 1);
291
- this.input.setValue(this.history[this.historyIndex] ?? "");
292
- this.screen.render();
293
- });
294
- this.input.key(["tab"], () => {
295
- const current = String(this.input.getValue() ?? "");
296
- const hit = this.options.commands.find((item) => item.startsWith(current.trim()));
297
- if (hit) {
298
- this.input.setValue(hit);
299
- this.screen.render();
364
+ finally {
365
+ this.setBusy(false);
366
+ this.focusPane("flows");
300
367
  }
301
368
  });
302
- this.input.key(["C-j"], () => {
303
- this.focusPane("log");
304
- });
305
- this.input.key(["C-u"], () => {
306
- this.focusPane("summary");
307
- });
308
- this.input.key(["C-h"], () => {
309
- this.focusPane("sidebar");
310
- });
311
- this.input.key(["S-tab"], () => {
312
- this.cycleFocus(-1);
313
- });
314
- this.log.key(["tab"], () => {
315
- this.cycleFocus(1);
316
- });
317
- this.log.key(["S-tab"], () => {
318
- this.cycleFocus(-1);
319
- });
320
- this.summary.key(["tab"], () => {
321
- this.cycleFocus(1);
322
- });
323
- this.summary.key(["S-tab"], () => {
324
- this.cycleFocus(-1);
325
- });
326
- this.sidebar.key(["tab"], () => {
327
- this.cycleFocus(1);
328
- });
329
- this.sidebar.key(["S-tab"], () => {
330
- this.cycleFocus(-1);
331
- });
332
- this.input.on("submit", async (value) => {
333
- const line = value.trim();
334
- this.input.clearValue();
335
- this.screen.render();
336
- if (!line || this.busy) {
337
- return;
338
- }
339
- this.history.push(line);
340
- this.historyIndex = this.history.length;
341
- this.appendLog(`> ${line}`);
342
- await this.options.onSubmit(line);
343
- this.focusPane("input");
369
+ this.confirm.key(["escape"], () => {
370
+ this.closeConfirm();
344
371
  });
345
372
  }
346
373
  cycleFocus(direction) {
347
- const panes = ["input", "log", "summary", "sidebar"];
374
+ const panes = ["flows", "progress", "summary", "log"];
348
375
  const currentIndex = panes.indexOf(this.focusedPane);
349
376
  const nextIndex = (currentIndex + direction + panes.length) % panes.length;
350
- this.focusPane(panes[nextIndex] ?? "input");
377
+ this.focusPane(panes[nextIndex] ?? "flows");
351
378
  }
352
379
  focusPane(pane) {
353
380
  this.focusedPane = pane;
354
- this.header.style.border.fg = "green";
355
- this.log.style.border.fg = pane === "log" ? "brightYellow" : "yellow";
381
+ this.flowList.style.border.fg = pane === "flows" ? "brightCyan" : "cyan";
382
+ this.progress.style.border.fg = pane === "progress" ? "brightGreen" : "green";
356
383
  this.summary.style.border.fg = pane === "summary" ? "brightGreen" : "green";
357
- this.sidebar.style.border.fg = pane === "sidebar" ? "brightCyan" : "cyan";
358
- this.input.style.border.fg = pane === "input" ? "brightMagenta" : "magenta";
359
- if (pane === "input") {
360
- this.input.focus();
384
+ this.log.style.border.fg = pane === "log" ? "brightYellow" : "yellow";
385
+ this.flowList.setLabel(pane === "flows" ? " ▶ Flows " : " Flows ");
386
+ this.progress.setLabel(pane === "progress" ? " ▶ Current Flow " : " Current Flow ");
387
+ this.summary.setLabel(pane === "summary" ? " ▶ Task Summary " : " Task Summary ");
388
+ this.log.setLabel(pane === "log" ? " ▶ Activity " : " Activity ");
389
+ if (pane === "flows") {
390
+ if (this.confirm.visible) {
391
+ this.confirm.focus();
392
+ }
393
+ else {
394
+ this.flowList.focus();
395
+ }
361
396
  }
362
- else if (pane === "log") {
363
- this.log.focus();
397
+ else if (pane === "progress") {
398
+ this.progress.focus();
364
399
  }
365
400
  else if (pane === "summary") {
366
401
  this.summary.focus();
367
402
  }
368
403
  else {
369
- this.sidebar.focus();
404
+ this.log.focus();
370
405
  }
371
- this.footer.setContent(` Focus: ${pane} | Enter: run command | Tab/Shift+Tab: switch pane | Ctrl+J: log | Ctrl+K: input | PgUp/PgDn: scroll | ?: help | q: exit `);
372
- this.screen.render();
406
+ this.footer.setContent(` Focus: ${pane} | Up/Down: select flow | Enter: confirm run | h: help | Esc: close | Tab: switch pane | q: exit `);
407
+ this.requestRender();
373
408
  }
374
409
  renderStaticContent() {
375
410
  this.summaryText = this.options.summaryText.trim();
376
411
  this.updateHeader();
412
+ this.flowList.setItems(this.options.flows.map((flow) => flow.label));
413
+ this.flowList.select(this.options.flows.findIndex((flow) => flow.id === this.selectedFlowId));
377
414
  this.renderSummary();
378
- this.sidebar.setContent([
379
- this.options.commands.join("\n"),
380
- "",
381
- "Keys:",
382
- "? / F1 help",
383
- "Ctrl+L clear log",
384
- "Tab complete",
385
- "q / Ctrl+C exit",
386
- ].join("\n"));
415
+ this.renderProgress();
387
416
  this.help.setContent(renderMarkdownToTerminal([
388
417
  "AgentWeaver interactive mode",
389
418
  "",
390
- "Use slash commands in the input box:",
391
- this.options.commands.join("\n"),
419
+ "Клавиши:",
420
+ "Up / Down выбрать flow",
421
+ "Enter открыть подтверждение запуска",
422
+ "Enter подтвердить запуск в модалке",
423
+ "Esc закрыть help или модалку",
424
+ "h / F1 открыть или закрыть help",
425
+ "Tab переключить pane",
426
+ "Ctrl+L очистить лог",
427
+ "q / Ctrl+C выйти",
392
428
  "",
393
- "Keys:",
394
- "Tab autocomplete command",
395
- "Up/Down history",
396
- "Ctrl+L clear log",
397
- "? or F1 toggle help",
398
- "Esc close help",
399
- "q / Ctrl+C exit",
429
+ "Доступные flow:",
430
+ ...this.options.flows.map((flow) => flow.label),
400
431
  ].join("\n")));
401
- this.footer.setContent(" Enter: run command | Tab: complete | Up/Down: history | ?: help | Ctrl+L: clear log | q: exit ");
432
+ this.footer.setContent(" Up/Down: select flow | Enter: confirm run | h: help | Tab: switch pane | q: exit ");
402
433
  }
403
434
  updateHeader() {
435
+ const current = this.currentFlowId ?? this.selectedFlowId;
404
436
  this.header.setContent(`{bold}AgentWeaver{/bold} {green-fg}${this.options.issueKey}{/green-fg}\n` +
405
- `cwd: ${this.options.cwd} current: ${this.currentCommand}`);
437
+ `cwd: ${this.options.cwd} current: ${current}${this.busy ? " {yellow-fg}[running]{/yellow-fg}" : ""}`);
406
438
  }
407
439
  renderSummary() {
408
- const summaryBody = this.summaryText
409
- ? this.summaryText
410
- : "Task summary is not available yet.";
440
+ const summaryBody = this.summaryText || "Task summary is not available yet.";
411
441
  this.summary.setContent(renderMarkdownToTerminal(stripAnsi(summaryBody)));
412
442
  }
413
443
  createAdapter() {
@@ -420,51 +450,326 @@ export class InteractiveUi {
420
450
  },
421
451
  supportsTransientStatus: false,
422
452
  supportsPassthrough: false,
423
- renderAuxiliaryOutput: false,
453
+ renderAuxiliaryOutput: true,
454
+ renderPanelsAsPlainText: true,
424
455
  setExecutionState: (state) => {
425
456
  this.currentNode = state.node;
426
457
  this.currentExecutor = state.executor;
427
458
  this.updateRunningPanel();
428
459
  },
460
+ setFlowState: (state) => {
461
+ this.setFlowDisplayState(state.flowId, state.executionState);
462
+ },
429
463
  };
430
464
  }
465
+ activeFlowId() {
466
+ return this.currentFlowId ?? this.selectedFlowId;
467
+ }
468
+ progressFlowDefinition() {
469
+ const preferredFlowId = this.busy ? this.activeFlowId() : this.selectedFlowId;
470
+ return this.flowMap.get(preferredFlowId);
471
+ }
472
+ renderProgress() {
473
+ const flow = this.progressFlowDefinition();
474
+ if (!flow) {
475
+ this.progress.setContent("Flow structure is not available.");
476
+ return;
477
+ }
478
+ const flowState = this.flowState.flowId === flow.id
479
+ ? this.flowState.executionState
480
+ : this.currentFlowId === flow.id
481
+ ? this.flowState.executionState
482
+ : null;
483
+ const lines = [flow.label, ""];
484
+ for (const item of this.visiblePhaseItems(flow, flowState)) {
485
+ if (item.kind === "group") {
486
+ const visiblePhases = item.phases.filter((phase) => this.shouldDisplayPhase(flow, flowState, phase));
487
+ if (visiblePhases.length === 0) {
488
+ continue;
489
+ }
490
+ lines.push(`${this.symbolForGroup(flow.id, flow, visiblePhases, flowState)} ${item.label}`);
491
+ for (const phase of visiblePhases) {
492
+ const phaseState = flowState?.phases.find((candidate) => candidate.id === phase.id);
493
+ const phaseStatus = this.displayStatusForPhase(flowState, flow, phase, phaseState?.status ?? null);
494
+ lines.push(` ${this.symbolForStatus(flow.id, phaseStatus)} ${this.displayPhaseId(phase)}`);
495
+ for (const step of phase.steps) {
496
+ const stepState = phaseState?.steps.find((candidate) => candidate.id === step.id);
497
+ const stepStatus = this.displayStatusForStep(flowState, flow, phase, stepState?.status ?? null);
498
+ lines.push(` ${this.symbolForStatus(flow.id, stepStatus)} ${step.id}`);
499
+ }
500
+ }
501
+ lines.push("");
502
+ continue;
503
+ }
504
+ const phase = item.phase;
505
+ if (!this.shouldDisplayPhase(flow, flowState, phase)) {
506
+ continue;
507
+ }
508
+ const phaseState = flowState?.phases.find((candidate) => candidate.id === phase.id);
509
+ const phaseStatus = this.displayStatusForPhase(flowState, flow, phase, phaseState?.status ?? null);
510
+ lines.push(`${this.symbolForStatus(flow.id, phaseStatus)} ${this.displayPhaseId(phase)}`);
511
+ for (const step of phase.steps) {
512
+ const stepState = phaseState?.steps.find((candidate) => candidate.id === step.id);
513
+ const stepStatus = this.displayStatusForStep(flowState, flow, phase, stepState?.status ?? null);
514
+ lines.push(` ${this.symbolForStatus(flow.id, stepStatus)} ${step.id}`);
515
+ }
516
+ lines.push("");
517
+ }
518
+ if (flowState?.terminated) {
519
+ lines.push(`✓ Flow completed successfully`);
520
+ lines.push(`Reason: ${flowState.terminationReason ?? "flow terminated"}`);
521
+ }
522
+ this.progress.setContent(lines.join("\n").trimEnd());
523
+ }
524
+ displayStatusForPhase(flowState, flow, phase, actualStatus) {
525
+ if (actualStatus) {
526
+ return actualStatus;
527
+ }
528
+ if (!flowState?.terminated) {
529
+ return "pending";
530
+ }
531
+ return this.isAfterTermination(flowState, flow, phase) ? "skipped" : "pending";
532
+ }
533
+ displayStatusForStep(flowState, flow, phase, actualStatus) {
534
+ if (actualStatus) {
535
+ return actualStatus;
536
+ }
537
+ if (!flowState?.terminated) {
538
+ return "pending";
539
+ }
540
+ return this.isAfterTermination(flowState, flow, phase) ? "skipped" : "pending";
541
+ }
542
+ symbolForStatus(flowId, status) {
543
+ if (status === "done") {
544
+ return "✓";
545
+ }
546
+ if (status === "skipped") {
547
+ return "·";
548
+ }
549
+ if (status === "running") {
550
+ const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
551
+ return this.failedFlowId === flowId && !this.busy ? "×" : (frames[this.spinnerFrame] ?? "▶");
552
+ }
553
+ return "○";
554
+ }
555
+ symbolForGroup(flowId, flow, phases, flowState) {
556
+ const statuses = phases.map((phase) => this.displayStatusForPhase(flowState, flow, phase, flowState?.phases.find((candidate) => candidate.id === phase.id)?.status ?? null));
557
+ if (statuses.some((status) => status === "running")) {
558
+ return this.symbolForStatus(flowId, "running");
559
+ }
560
+ if (statuses.every((status) => status === "skipped")) {
561
+ return "·";
562
+ }
563
+ if (statuses.every((status) => status === "done" || status === "skipped")) {
564
+ return "✓";
565
+ }
566
+ return "○";
567
+ }
568
+ groupPhases(flow) {
569
+ const items = [];
570
+ let index = 0;
571
+ while (index < flow.phases.length) {
572
+ const phase = flow.phases[index];
573
+ if (!phase) {
574
+ break;
575
+ }
576
+ const repeatLabel = this.repeatLabel(phase.repeatVars);
577
+ if (!repeatLabel) {
578
+ items.push({ kind: "phase", phase });
579
+ index += 1;
580
+ continue;
581
+ }
582
+ const phases = [phase];
583
+ let nextIndex = index + 1;
584
+ while (nextIndex < flow.phases.length) {
585
+ const candidate = flow.phases[nextIndex];
586
+ if (!candidate || this.repeatGroupKey(candidate.repeatVars) !== this.repeatGroupKey(phase.repeatVars)) {
587
+ break;
588
+ }
589
+ phases.push(candidate);
590
+ nextIndex += 1;
591
+ }
592
+ items.push({ kind: "group", label: repeatLabel, phases, seriesKey: this.repeatSeriesKey(phases) });
593
+ index = nextIndex;
594
+ }
595
+ return items;
596
+ }
597
+ visiblePhaseItems(flow, flowState) {
598
+ const pendingSeries = new Set();
599
+ return this.groupPhases(flow).filter((item) => {
600
+ if (item.kind === "phase") {
601
+ return this.shouldDisplayPhase(flow, flowState, item.phase);
602
+ }
603
+ const visiblePhases = item.phases.filter((phase) => this.shouldDisplayPhase(flow, flowState, phase));
604
+ const hasState = visiblePhases.some((phase) => flowState?.phases.some((candidate) => candidate.id === phase.id));
605
+ if (visiblePhases.length === 0) {
606
+ return false;
607
+ }
608
+ if (hasState) {
609
+ return true;
610
+ }
611
+ if (pendingSeries.has(item.seriesKey)) {
612
+ return false;
613
+ }
614
+ pendingSeries.add(item.seriesKey);
615
+ return true;
616
+ });
617
+ }
618
+ repeatGroupKey(repeatVars) {
619
+ const entries = Object.entries(repeatVars).sort(([left], [right]) => left.localeCompare(right));
620
+ return JSON.stringify(entries);
621
+ }
622
+ shouldDisplayPhase(flow, flowState, phase) {
623
+ const phaseState = flowState?.phases.find((candidate) => candidate.id === phase.id) ?? null;
624
+ if (!flowState) {
625
+ if (Object.keys(phase.repeatVars).length > 0) {
626
+ return false;
627
+ }
628
+ return !this.hasPreviousRepeatPhase(flow, phase);
629
+ }
630
+ if (Object.keys(phase.repeatVars).length === 0) {
631
+ if (!phaseState) {
632
+ return false;
633
+ }
634
+ if (phaseState?.status === "skipped" && flowState.terminated && this.isAfterTermination(flowState, flow, phase)) {
635
+ return false;
636
+ }
637
+ return true;
638
+ }
639
+ if (!phaseState) {
640
+ return false;
641
+ }
642
+ if (phaseState.status === "skipped" && flowState.terminated && this.isAfterTermination(flowState, flow, phase)) {
643
+ return false;
644
+ }
645
+ return true;
646
+ }
647
+ hasPreviousRepeatPhase(flow, phase) {
648
+ for (const candidate of flow.phases) {
649
+ if (candidate.id === phase.id) {
650
+ return false;
651
+ }
652
+ if (Object.keys(candidate.repeatVars).length > 0) {
653
+ return true;
654
+ }
655
+ }
656
+ return false;
657
+ }
658
+ repeatSeriesKey(phases) {
659
+ const repeatVarNames = Object.keys(phases[0]?.repeatVars ?? {}).sort();
660
+ const phaseNames = phases.map((phase) => this.displayPhaseId(phase));
661
+ return JSON.stringify({
662
+ repeatVarNames,
663
+ phaseNames,
664
+ });
665
+ }
666
+ repeatLabel(repeatVars) {
667
+ const entries = Object.entries(repeatVars).filter(([key]) => !key.endsWith("_minus_one"));
668
+ if (entries.length === 0) {
669
+ return null;
670
+ }
671
+ if (entries.length === 1) {
672
+ const [key, value] = entries[0] ?? ["repeat", ""];
673
+ return `${key} ${value}`;
674
+ }
675
+ return entries.map(([key, value]) => `${key}=${value}`).join(", ");
676
+ }
677
+ displayPhaseId(phase) {
678
+ let result = phase.id;
679
+ const values = Object.entries(phase.repeatVars)
680
+ .filter(([key]) => !key.endsWith("_minus_one"))
681
+ .map(([, value]) => value);
682
+ for (const value of values) {
683
+ const suffix = `_${String(value)}`;
684
+ if (result.endsWith(suffix)) {
685
+ result = result.slice(0, -suffix.length);
686
+ }
687
+ }
688
+ return result;
689
+ }
690
+ isAfterTermination(flowState, flow, phase) {
691
+ const terminationReason = flowState.terminationReason ?? "";
692
+ const match = /^Stopped by ([^:]+):/.exec(terminationReason);
693
+ if (!match) {
694
+ return false;
695
+ }
696
+ const stoppedPhaseId = match[1];
697
+ const stoppedIndex = flow.phases.findIndex((candidate) => candidate.id === stoppedPhaseId);
698
+ const currentIndex = flow.phases.findIndex((candidate) => candidate.id === phase.id);
699
+ if (stoppedIndex < 0 || currentIndex < 0) {
700
+ return false;
701
+ }
702
+ return currentIndex > stoppedIndex;
703
+ }
704
+ openConfirm() {
705
+ const flow = this.flowMap.get(this.selectedFlowId);
706
+ if (!flow) {
707
+ return;
708
+ }
709
+ this.confirm.setContent(`Run flow "${flow.label}"?\n\nEnter: yes Esc: no`);
710
+ this.confirm.show();
711
+ this.confirm.setFront();
712
+ this.confirm.focus();
713
+ this.requestRender();
714
+ }
715
+ closeConfirm() {
716
+ this.confirm.hide();
717
+ this.focusPane("flows");
718
+ this.requestRender();
719
+ }
431
720
  mount() {
432
721
  setOutputAdapter(this.createAdapter());
433
- this.focusPane("input");
722
+ this.focusPane("flows");
434
723
  }
435
724
  destroy() {
725
+ this.flushPendingLogLines();
436
726
  if (this.spinnerTimer) {
437
727
  clearInterval(this.spinnerTimer);
438
728
  this.spinnerTimer = null;
439
729
  }
730
+ if (this.logFlushTimer) {
731
+ clearTimeout(this.logFlushTimer);
732
+ this.logFlushTimer = null;
733
+ }
440
734
  setOutputAdapter(null);
441
735
  this.screen.destroy();
442
736
  }
443
- setBusy(busy, command) {
737
+ setBusy(busy, flowId) {
444
738
  this.busy = busy;
445
- this.currentCommand = command ?? (busy ? this.currentCommand : "idle");
739
+ this.currentFlowId = flowId ?? (busy ? this.currentFlowId : this.currentFlowId);
446
740
  if (busy && this.runningStartedAt === null) {
447
741
  this.runningStartedAt = Date.now();
448
742
  }
449
743
  else if (!busy && this.currentNode === null && this.currentExecutor === null) {
450
744
  this.runningStartedAt = null;
451
745
  }
746
+ if (!busy && flowId === undefined) {
747
+ this.currentFlowId = this.currentFlowId ?? this.selectedFlowId;
748
+ }
452
749
  this.updateHeader();
453
- this.header.setContent(`{bold}AgentWeaver{/bold} {green-fg}${this.options.issueKey}{/green-fg}\n` +
454
- `cwd: ${this.options.cwd} current: ${this.currentCommand}${busy ? " {yellow-fg}[running]{/yellow-fg}" : ""}`);
455
750
  this.updateRunningPanel();
456
- this.input.setLabel(busy ? " command [busy] " : " command ");
457
- this.screen.render();
751
+ this.renderProgress();
752
+ this.requestRender();
753
+ }
754
+ setFlowFailed(flowId) {
755
+ this.failedFlowId = flowId;
756
+ this.renderProgress();
757
+ this.requestRender();
758
+ }
759
+ clearFlowFailure(flowId) {
760
+ if (this.failedFlowId === flowId) {
761
+ this.failedFlowId = null;
762
+ }
458
763
  }
459
764
  setStatus(status) {
460
- this.currentCommand = status;
765
+ this.currentFlowId = status;
461
766
  this.updateHeader();
462
- this.screen.render();
767
+ this.requestRender();
463
768
  }
464
769
  setSummary(markdown) {
465
770
  this.summaryText = markdown.trim();
466
771
  this.renderSummary();
467
- this.screen.render();
772
+ this.requestRender();
468
773
  }
469
774
  appendLog(text) {
470
775
  const normalized = text
@@ -473,15 +778,23 @@ export class InteractiveUi {
473
778
  .join("\n")
474
779
  .trimEnd();
475
780
  if (!normalized) {
476
- this.log.add("");
781
+ this.pendingLogLines.push("");
477
782
  }
478
783
  else {
479
- for (const line of normalized.split("\n")) {
480
- this.log.add(line);
481
- }
784
+ this.pendingLogLines.push(...normalized.split("\n"));
482
785
  }
483
- this.log.setScrollPerc(100);
484
- this.screen.render();
786
+ this.scheduleLogFlush();
787
+ }
788
+ setFlowDisplayState(flowId, executionState) {
789
+ this.flowState = {
790
+ flowId,
791
+ executionState,
792
+ };
793
+ if (flowId) {
794
+ this.currentFlowId = flowId;
795
+ }
796
+ this.renderProgress();
797
+ this.requestRender();
485
798
  }
486
799
  updateRunningPanel() {
487
800
  const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
@@ -493,7 +806,8 @@ export class InteractiveUi {
493
806
  this.spinnerTimer = setInterval(() => {
494
807
  this.spinnerFrame = (this.spinnerFrame + 1) % frames.length;
495
808
  this.updateRunningPanel();
496
- this.screen.render();
809
+ this.renderProgress();
810
+ this.requestRender();
497
811
  }, 120);
498
812
  }
499
813
  else if (!running && this.spinnerTimer) {
@@ -509,7 +823,7 @@ export class InteractiveUi {
509
823
  const stateLine = `State: ${running ? `${spinner} running` : "idle"}`;
510
824
  const elapsedLine = `Time: ${elapsed}`;
511
825
  this.status.setContent([stateLine, elapsedLine, nodeLine, executorLine].join("\n"));
512
- this.screen.render();
826
+ this.requestRender();
513
827
  }
514
828
  formatElapsed(now) {
515
829
  if (this.runningStartedAt === null || now === null) {
@@ -517,8 +831,38 @@ export class InteractiveUi {
517
831
  }
518
832
  const totalSeconds = Math.max(0, Math.floor((now - this.runningStartedAt) / 1000));
519
833
  const hours = String(Math.floor(totalSeconds / 3600)).padStart(2, "0");
520
- const minutes = String(Math.floor((totalSeconds % 3600) / 60)).padStart(2, "0");
834
+ const minutes = String((totalSeconds % 3600) / 60 | 0).padStart(2, "0");
521
835
  const seconds = String(totalSeconds % 60).padStart(2, "0");
522
836
  return `${hours}:${minutes}:${seconds}`;
523
837
  }
838
+ scheduleLogFlush() {
839
+ if (this.logFlushTimer) {
840
+ return;
841
+ }
842
+ this.logFlushTimer = setTimeout(() => {
843
+ this.logFlushTimer = null;
844
+ this.flushPendingLogLines();
845
+ }, 50);
846
+ }
847
+ flushPendingLogLines() {
848
+ if (this.pendingLogLines.length === 0) {
849
+ return;
850
+ }
851
+ const lines = this.pendingLogLines.splice(0, this.pendingLogLines.length);
852
+ for (const line of lines) {
853
+ this.log.add(line);
854
+ }
855
+ this.log.setScrollPerc(100);
856
+ this.requestRender();
857
+ }
858
+ requestRender() {
859
+ if (this.renderScheduled) {
860
+ return;
861
+ }
862
+ this.renderScheduled = true;
863
+ setImmediate(() => {
864
+ this.renderScheduled = false;
865
+ this.screen.render();
866
+ });
867
+ }
524
868
  }