agentweaver 0.1.2 → 0.1.3

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 (50) hide show
  1. package/README.md +11 -10
  2. package/dist/artifacts.js +24 -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/index.js +388 -451
  9. package/dist/interactive-ui.js +451 -194
  10. package/dist/jira.js +3 -1
  11. package/dist/pipeline/auto-flow.js +9 -0
  12. package/dist/pipeline/context.js +2 -0
  13. package/dist/pipeline/declarative-flow-runner.js +246 -0
  14. package/dist/pipeline/declarative-flows.js +24 -0
  15. package/dist/pipeline/flow-specs/auto.json +471 -0
  16. package/dist/pipeline/flow-specs/implement.json +47 -0
  17. package/dist/pipeline/flow-specs/plan.json +88 -0
  18. package/dist/pipeline/flow-specs/preflight.json +174 -0
  19. package/dist/pipeline/flow-specs/review-fix.json +76 -0
  20. package/dist/pipeline/flow-specs/review.json +233 -0
  21. package/dist/pipeline/flow-specs/test-fix.json +24 -0
  22. package/dist/pipeline/flow-specs/test-linter-fix.json +24 -0
  23. package/dist/pipeline/flow-specs/test.json +19 -0
  24. package/dist/pipeline/flows/implement-flow.js +3 -4
  25. package/dist/pipeline/flows/preflight-flow.js +17 -57
  26. package/dist/pipeline/flows/review-fix-flow.js +3 -4
  27. package/dist/pipeline/flows/review-flow.js +8 -4
  28. package/dist/pipeline/flows/test-fix-flow.js +3 -4
  29. package/dist/pipeline/node-registry.js +71 -0
  30. package/dist/pipeline/node-runner.js +9 -3
  31. package/dist/pipeline/nodes/build-failure-summary-node.js +4 -4
  32. package/dist/pipeline/nodes/claude-prompt-node.js +54 -0
  33. package/dist/pipeline/nodes/claude-summary-node.js +12 -6
  34. package/dist/pipeline/nodes/codex-docker-prompt-node.js +1 -0
  35. package/dist/pipeline/nodes/codex-local-prompt-node.js +32 -0
  36. package/dist/pipeline/nodes/file-check-node.js +15 -0
  37. package/dist/pipeline/nodes/summary-file-load-node.js +16 -0
  38. package/dist/pipeline/nodes/task-summary-node.js +12 -6
  39. package/dist/pipeline/prompt-registry.js +22 -0
  40. package/dist/pipeline/prompt-runtime.js +18 -0
  41. package/dist/pipeline/registry.js +0 -2
  42. package/dist/pipeline/spec-compiler.js +200 -0
  43. package/dist/pipeline/spec-loader.js +14 -0
  44. package/dist/pipeline/spec-types.js +1 -0
  45. package/dist/pipeline/spec-validator.js +290 -0
  46. package/dist/pipeline/value-resolver.js +199 -0
  47. package/dist/prompts.js +1 -3
  48. package/dist/runtime/process-runner.js +24 -23
  49. package/dist/tui.js +39 -0
  50. 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,239 @@ 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
+ lines.push(`${this.symbolForGroup(flow.id, item.phases, flowState)} ${item.label}`);
487
+ for (const phase of item.phases) {
488
+ const phaseState = flowState?.phases.find((candidate) => candidate.id === phase.id);
489
+ lines.push(` ${this.symbolForStatus(flow.id, phaseState?.status ?? "pending")} ${this.displayPhaseId(phase)}`);
490
+ for (const step of phase.steps) {
491
+ const stepState = phaseState?.steps.find((candidate) => candidate.id === step.id);
492
+ lines.push(` ${this.symbolForStatus(flow.id, stepState?.status ?? "pending")} ${step.id}`);
493
+ }
494
+ }
495
+ lines.push("");
496
+ continue;
497
+ }
498
+ const phase = item.phase;
499
+ const phaseState = flowState?.phases.find((candidate) => candidate.id === phase.id);
500
+ lines.push(`${this.symbolForStatus(flow.id, phaseState?.status ?? "pending")} ${phase.id}`);
501
+ for (const step of phase.steps) {
502
+ const stepState = phaseState?.steps.find((candidate) => candidate.id === step.id);
503
+ lines.push(` ${this.symbolForStatus(flow.id, stepState?.status ?? "pending")} ${step.id}`);
504
+ }
505
+ lines.push("");
506
+ }
507
+ if (flowState?.terminated) {
508
+ lines.push(`Stopped: ${flowState.terminationReason ?? "flow terminated"}`);
509
+ }
510
+ this.progress.setContent(lines.join("\n").trimEnd());
511
+ }
512
+ symbolForStatus(flowId, status) {
513
+ if (status === "done") {
514
+ return "✓";
515
+ }
516
+ if (status === "skipped") {
517
+ return "·";
518
+ }
519
+ if (status === "running") {
520
+ const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
521
+ return this.failedFlowId === flowId && !this.busy ? "×" : (frames[this.spinnerFrame] ?? "▶");
522
+ }
523
+ return "○";
524
+ }
525
+ symbolForGroup(flowId, phases, flowState) {
526
+ const statuses = phases.map((phase) => flowState?.phases.find((candidate) => candidate.id === phase.id)?.status ?? "pending");
527
+ if (statuses.some((status) => status === "running")) {
528
+ return this.symbolForStatus(flowId, "running");
529
+ }
530
+ if (statuses.every((status) => status === "skipped")) {
531
+ return "·";
532
+ }
533
+ if (statuses.every((status) => status === "done" || status === "skipped")) {
534
+ return "✓";
535
+ }
536
+ return "○";
537
+ }
538
+ groupPhases(flow) {
539
+ const items = [];
540
+ let index = 0;
541
+ while (index < flow.phases.length) {
542
+ const phase = flow.phases[index];
543
+ if (!phase) {
544
+ break;
545
+ }
546
+ const repeatLabel = this.repeatLabel(phase.repeatVars);
547
+ if (!repeatLabel) {
548
+ items.push({ kind: "phase", phase });
549
+ index += 1;
550
+ continue;
551
+ }
552
+ const phases = [phase];
553
+ let nextIndex = index + 1;
554
+ while (nextIndex < flow.phases.length) {
555
+ const candidate = flow.phases[nextIndex];
556
+ if (!candidate || this.repeatGroupKey(candidate.repeatVars) !== this.repeatGroupKey(phase.repeatVars)) {
557
+ break;
558
+ }
559
+ phases.push(candidate);
560
+ nextIndex += 1;
561
+ }
562
+ items.push({ kind: "group", label: repeatLabel, phases, seriesKey: this.repeatSeriesKey(phases) });
563
+ index = nextIndex;
564
+ }
565
+ return items;
566
+ }
567
+ visiblePhaseItems(flow, flowState) {
568
+ const pendingSeries = new Set();
569
+ return this.groupPhases(flow).filter((item) => {
570
+ if (item.kind === "phase") {
571
+ return true;
572
+ }
573
+ const hasState = item.phases.some((phase) => flowState?.phases.some((candidate) => candidate.id === phase.id));
574
+ if (hasState) {
575
+ return true;
576
+ }
577
+ if (pendingSeries.has(item.seriesKey)) {
578
+ return false;
579
+ }
580
+ pendingSeries.add(item.seriesKey);
581
+ return true;
582
+ });
583
+ }
584
+ repeatGroupKey(repeatVars) {
585
+ const entries = Object.entries(repeatVars).sort(([left], [right]) => left.localeCompare(right));
586
+ return JSON.stringify(entries);
587
+ }
588
+ repeatSeriesKey(phases) {
589
+ const repeatVarNames = Object.keys(phases[0]?.repeatVars ?? {}).sort();
590
+ const phaseNames = phases.map((phase) => this.displayPhaseId(phase));
591
+ return JSON.stringify({
592
+ repeatVarNames,
593
+ phaseNames,
594
+ });
595
+ }
596
+ repeatLabel(repeatVars) {
597
+ const entries = Object.entries(repeatVars);
598
+ if (entries.length === 0) {
599
+ return null;
600
+ }
601
+ if (entries.length === 1) {
602
+ const [key, value] = entries[0] ?? ["repeat", ""];
603
+ return `${key} ${value}`;
604
+ }
605
+ return entries.map(([key, value]) => `${key}=${value}`).join(", ");
606
+ }
607
+ displayPhaseId(phase) {
608
+ let result = phase.id;
609
+ for (const value of Object.values(phase.repeatVars)) {
610
+ const suffix = `_${String(value)}`;
611
+ if (result.endsWith(suffix)) {
612
+ result = result.slice(0, -suffix.length);
613
+ }
614
+ }
615
+ return result;
616
+ }
617
+ openConfirm() {
618
+ const flow = this.flowMap.get(this.selectedFlowId);
619
+ if (!flow) {
620
+ return;
621
+ }
622
+ this.confirm.setContent(`Run flow "${flow.label}"?\n\nEnter: yes Esc: no`);
623
+ this.confirm.show();
624
+ this.confirm.setFront();
625
+ this.confirm.focus();
626
+ this.requestRender();
627
+ }
628
+ closeConfirm() {
629
+ this.confirm.hide();
630
+ this.focusPane("flows");
631
+ this.requestRender();
632
+ }
431
633
  mount() {
432
634
  setOutputAdapter(this.createAdapter());
433
- this.focusPane("input");
635
+ this.focusPane("flows");
434
636
  }
435
637
  destroy() {
638
+ this.flushPendingLogLines();
436
639
  if (this.spinnerTimer) {
437
640
  clearInterval(this.spinnerTimer);
438
641
  this.spinnerTimer = null;
439
642
  }
643
+ if (this.logFlushTimer) {
644
+ clearTimeout(this.logFlushTimer);
645
+ this.logFlushTimer = null;
646
+ }
440
647
  setOutputAdapter(null);
441
648
  this.screen.destroy();
442
649
  }
443
- setBusy(busy, command) {
650
+ setBusy(busy, flowId) {
444
651
  this.busy = busy;
445
- this.currentCommand = command ?? (busy ? this.currentCommand : "idle");
652
+ this.currentFlowId = flowId ?? (busy ? this.currentFlowId : this.currentFlowId);
446
653
  if (busy && this.runningStartedAt === null) {
447
654
  this.runningStartedAt = Date.now();
448
655
  }
449
656
  else if (!busy && this.currentNode === null && this.currentExecutor === null) {
450
657
  this.runningStartedAt = null;
451
658
  }
659
+ if (!busy && flowId === undefined) {
660
+ this.currentFlowId = this.currentFlowId ?? this.selectedFlowId;
661
+ }
452
662
  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
663
  this.updateRunningPanel();
456
- this.input.setLabel(busy ? " command [busy] " : " command ");
457
- this.screen.render();
664
+ this.renderProgress();
665
+ this.requestRender();
666
+ }
667
+ setFlowFailed(flowId) {
668
+ this.failedFlowId = flowId;
669
+ this.renderProgress();
670
+ this.requestRender();
671
+ }
672
+ clearFlowFailure(flowId) {
673
+ if (this.failedFlowId === flowId) {
674
+ this.failedFlowId = null;
675
+ }
458
676
  }
459
677
  setStatus(status) {
460
- this.currentCommand = status;
678
+ this.currentFlowId = status;
461
679
  this.updateHeader();
462
- this.screen.render();
680
+ this.requestRender();
463
681
  }
464
682
  setSummary(markdown) {
465
683
  this.summaryText = markdown.trim();
466
684
  this.renderSummary();
467
- this.screen.render();
685
+ this.requestRender();
468
686
  }
469
687
  appendLog(text) {
470
688
  const normalized = text
@@ -473,15 +691,23 @@ export class InteractiveUi {
473
691
  .join("\n")
474
692
  .trimEnd();
475
693
  if (!normalized) {
476
- this.log.add("");
694
+ this.pendingLogLines.push("");
477
695
  }
478
696
  else {
479
- for (const line of normalized.split("\n")) {
480
- this.log.add(line);
481
- }
697
+ this.pendingLogLines.push(...normalized.split("\n"));
482
698
  }
483
- this.log.setScrollPerc(100);
484
- this.screen.render();
699
+ this.scheduleLogFlush();
700
+ }
701
+ setFlowDisplayState(flowId, executionState) {
702
+ this.flowState = {
703
+ flowId,
704
+ executionState,
705
+ };
706
+ if (flowId) {
707
+ this.currentFlowId = flowId;
708
+ }
709
+ this.renderProgress();
710
+ this.requestRender();
485
711
  }
486
712
  updateRunningPanel() {
487
713
  const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
@@ -493,7 +719,8 @@ export class InteractiveUi {
493
719
  this.spinnerTimer = setInterval(() => {
494
720
  this.spinnerFrame = (this.spinnerFrame + 1) % frames.length;
495
721
  this.updateRunningPanel();
496
- this.screen.render();
722
+ this.renderProgress();
723
+ this.requestRender();
497
724
  }, 120);
498
725
  }
499
726
  else if (!running && this.spinnerTimer) {
@@ -509,7 +736,7 @@ export class InteractiveUi {
509
736
  const stateLine = `State: ${running ? `${spinner} running` : "idle"}`;
510
737
  const elapsedLine = `Time: ${elapsed}`;
511
738
  this.status.setContent([stateLine, elapsedLine, nodeLine, executorLine].join("\n"));
512
- this.screen.render();
739
+ this.requestRender();
513
740
  }
514
741
  formatElapsed(now) {
515
742
  if (this.runningStartedAt === null || now === null) {
@@ -517,8 +744,38 @@ export class InteractiveUi {
517
744
  }
518
745
  const totalSeconds = Math.max(0, Math.floor((now - this.runningStartedAt) / 1000));
519
746
  const hours = String(Math.floor(totalSeconds / 3600)).padStart(2, "0");
520
- const minutes = String(Math.floor((totalSeconds % 3600) / 60)).padStart(2, "0");
747
+ const minutes = String((totalSeconds % 3600) / 60 | 0).padStart(2, "0");
521
748
  const seconds = String(totalSeconds % 60).padStart(2, "0");
522
749
  return `${hours}:${minutes}:${seconds}`;
523
750
  }
751
+ scheduleLogFlush() {
752
+ if (this.logFlushTimer) {
753
+ return;
754
+ }
755
+ this.logFlushTimer = setTimeout(() => {
756
+ this.logFlushTimer = null;
757
+ this.flushPendingLogLines();
758
+ }, 50);
759
+ }
760
+ flushPendingLogLines() {
761
+ if (this.pendingLogLines.length === 0) {
762
+ return;
763
+ }
764
+ const lines = this.pendingLogLines.splice(0, this.pendingLogLines.length);
765
+ for (const line of lines) {
766
+ this.log.add(line);
767
+ }
768
+ this.log.setScrollPerc(100);
769
+ this.requestRender();
770
+ }
771
+ requestRender() {
772
+ if (this.renderScheduled) {
773
+ return;
774
+ }
775
+ this.renderScheduled = true;
776
+ setImmediate(() => {
777
+ this.renderScheduled = false;
778
+ this.screen.render();
779
+ });
780
+ }
524
781
  }