@jameslovespancakes/pi-plus 1.0.20 → 1.0.21

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 (34) hide show
  1. package/README.md +21 -3
  2. package/package.json +1 -1
  3. package/src/domains/workflows/index.ts +56 -104
  4. package/src/domains/workflows/runtime/advisory-challenge.ts +3 -3
  5. package/src/domains/workflows/runtime/agent-attempt.ts +3 -3
  6. package/src/domains/workflows/runtime/agent-options.ts +18 -0
  7. package/src/domains/workflows/runtime/agent-runner-types.ts +7 -3
  8. package/src/domains/workflows/runtime/agent-runner.ts +14 -6
  9. package/src/domains/workflows/runtime/agent-session.ts +34 -3
  10. package/src/domains/workflows/runtime/cancellation.ts +5 -0
  11. package/src/domains/workflows/runtime/engine.ts +19 -40
  12. package/src/domains/workflows/runtime/journal.ts +4 -4
  13. package/src/domains/workflows/runtime/live-agent.ts +37 -0
  14. package/src/domains/workflows/runtime/model-profiles.ts +2 -6
  15. package/src/domains/workflows/runtime/progress-types.ts +3 -1
  16. package/src/domains/workflows/runtime/progress.ts +54 -14
  17. package/src/domains/workflows/runtime/review/review-fix-workflow.ts +3 -3
  18. package/src/domains/workflows/runtime/types.ts +16 -15
  19. package/src/domains/workflows/runtime/ui/agent-transcript.ts +59 -0
  20. package/src/domains/workflows/runtime/ui/workflow-format.ts +6 -2
  21. package/src/domains/workflows/runtime/ui/workflow-inspector.ts +75 -61
  22. package/src/domains/workflows/runtime/ui/workflow-widget.ts +26 -66
  23. package/src/domains/workflows/runtime/workflow-advisory-utils.ts +5 -5
  24. package/src/domains/workflows/runtime/{background-workflows.ts → workflow-lifecycle.ts} +91 -75
  25. package/src/domains/workflows/runtime/workflow-management.ts +66 -0
  26. package/src/domains/workflows/runtime/workflow-run-controller.ts +16 -20
  27. package/src/domains/workflows/runtime/{workflow-run-background.ts → workflow-run-delivery.ts} +3 -3
  28. package/src/domains/workflows/runtime/workflow-run-record.ts +7 -4
  29. package/src/domains/workflows/workflows/code-review.ts +1 -1
  30. package/src/domains/workflows/workflows/diagnose.ts +1 -1
  31. package/src/domains/workflows/workflows/perf-review.ts +1 -1
  32. package/src/domains/workflows/workflows/refactor-scout.ts +1 -1
  33. package/src/domains/workflows/workflows/research.ts +4 -4
  34. package/src/domains/workflows/runtime/background-workflow-tool.ts +0 -75
@@ -1,7 +1,8 @@
1
- import type { Theme } from "@earendil-works/pi-coding-agent";
1
+ import { CustomEditor, getSelectListTheme, type KeybindingsManager, type Theme } from "@earendil-works/pi-coding-agent";
2
+ import { AgentTranscriptView } from "./agent-transcript.ts";
2
3
  import {
3
4
  Box,
4
- Input,
5
+ Editor,
5
6
  Markdown,
6
7
  matchesKey,
7
8
  Text,
@@ -16,21 +17,25 @@ import {
16
17
  import type { AgentChatMessage, AgentRowSnapshot, WorkflowProgressSnapshot } from "../progress-types.ts";
17
18
  import type { WorkflowProgressSource } from "../types.ts";
18
19
  import { unknownErrorMessage } from "../unknown-error.ts";
19
- import { formatWorkflowUsageLine } from "../usage.ts";
20
+ import { agentModelName, thinkingLabel } from "./workflow-widget.ts";
20
21
  import { formatDuration, statusIcon, truncateDisplay } from "./workflow-format.ts";
21
22
  import {
22
23
  centerWorkflowViewerViewport,
23
24
  fitWorkflowViewerRow,
24
25
  fitWorkflowViewerRows,
25
- workflowViewerHeight,
26
26
  } from "./workflow-viewer-layout.ts";
27
27
 
28
+ export const WORKFLOW_INSPECTOR_OVERLAY_OPTIONS = {
29
+ overlay: true,
30
+ overlayOptions: { anchor: "center", width: "100%", maxHeight: "100%", margin: 0 },
31
+ } as const;
32
+
28
33
  export interface WorkflowInspectorOutcome {
29
34
  readonly label: string;
30
35
  readonly text: string;
31
36
  }
32
37
 
33
- type WorkflowInspectorLiveSource = Pick<WorkflowProgressSource, "conversation" | "followUp">;
38
+ type WorkflowInspectorLiveSource = Pick<WorkflowProgressSource, "conversation" | "followUp" | "stopAgent" | "transcript">;
34
39
 
35
40
  interface BoardAgent {
36
41
  readonly agent: AgentRowSnapshot;
@@ -45,6 +50,7 @@ interface BoardRow {
45
50
  /** Modal board for inspecting workflow agents and sending targeted follow-ups. */
46
51
  export class WorkflowInspector implements Focusable {
47
52
  private selected = 0;
53
+ private width = 0;
48
54
  private detailAgentId: number | undefined;
49
55
  private detailScroll = 0;
50
56
  private clickRows = new Map<number, number>();
@@ -52,7 +58,8 @@ export class WorkflowInspector implements Focusable {
52
58
  private followUpError: string | undefined;
53
59
  private sending = false;
54
60
  private _focused = false;
55
- private readonly input: Input;
61
+ private readonly input: Editor;
62
+ private readonly transcriptViews = new Map<number, AgentTranscriptView>();
56
63
  private readonly snapshotProvider: () => WorkflowProgressSnapshot;
57
64
  private readonly tui: Pick<TUI, "requestRender" | "terminal">;
58
65
  private readonly theme: Theme;
@@ -67,6 +74,7 @@ export class WorkflowInspector implements Focusable {
67
74
  close: () => void,
68
75
  outcome?: WorkflowInspectorOutcome,
69
76
  live?: WorkflowInspectorLiveSource,
77
+ keybindings?: KeybindingsManager,
70
78
  ) {
71
79
  this.snapshotProvider = snapshotProvider;
72
80
  this.tui = tui;
@@ -74,11 +82,10 @@ export class WorkflowInspector implements Focusable {
74
82
  this.close = close;
75
83
  this.outcome = outcome;
76
84
  this.live = live;
77
- this.input = new Input({
78
- prompt: this.theme.fg("accent", "› "),
79
- placeholder: "Send a follow-up to this agent…",
80
- placeholderStyle: (text) => this.theme.fg("dim", text),
81
- });
85
+ const editorTheme = { borderColor: (text: string) => this.theme.fg("border", text), selectList: getSelectListTheme() };
86
+ this.input = keybindings
87
+ ? new CustomEditor(tui as TUI, editorTheme, keybindings)
88
+ : new Editor(tui as TUI, editorTheme);
82
89
  this.input.onSubmit = (value) => void this.submitFollowUp(value);
83
90
  }
84
91
 
@@ -93,14 +100,16 @@ export class WorkflowInspector implements Focusable {
93
100
 
94
101
  handleInput(data: string): void {
95
102
  if (matchesKey(data, "escape") || (data === "q" && this.detailAgentId === undefined)) {
96
- this.close();
103
+ if (this.detailAgentId !== undefined) this.closeDetails();
104
+ else this.close();
97
105
  return;
98
106
  }
99
107
 
100
108
  if (this.detailAgentId !== undefined) {
101
109
  if (matchesKey(data, "pageUp")) this.scrollChat(6);
102
110
  else if (matchesKey(data, "pageDown")) this.scrollChat(-6);
103
- else if (matchesKey(data, "backspace") && this.input.getValue().length === 0) this.closeDetails();
111
+ else if (matchesKey(data, "backspace") && this.input.getText().length === 0) this.closeDetails();
112
+ else if (matchesKey(data, "alt+enter") && this.canMessageSelectedAgent()) void this.submitFollowUp(this.input.getText(), false);
104
113
  else if (this.canMessageSelectedAgent()) this.input.handleInput(data);
105
114
  this.tui.requestRender();
106
115
  return;
@@ -113,6 +122,7 @@ export class WorkflowInspector implements Focusable {
113
122
  else if (matchesKey(data, "pageDown")) this.select(this.selected + 8, count);
114
123
  else if (matchesKey(data, "home") || data === "g") this.select(0, count);
115
124
  else if (matchesKey(data, "end") || data === "G") this.select(count - 1, count);
125
+ else if (data === "x" || data === "X") this.stopSelectedAgent();
116
126
  else if (matchesKey(data, "return") || matchesKey(data, "enter") || data === " ") this.openDetails(count);
117
127
  }
118
128
 
@@ -123,7 +133,7 @@ export class WorkflowInspector implements Focusable {
123
133
  return { handled: true, focus: true, render: true };
124
134
  }
125
135
  if (event.type === "click" && event.button === "left" && event.y === this.inputRow && this.canMessageSelectedAgent()) {
126
- const result = this.input.handleMouse({ ...event, y: 0 });
136
+ const result = this.input.handleMouse({ ...event, y: event.y - (this.inputRow ?? 0) });
127
137
  return { handled: true, focus: true, render: true, ...result };
128
138
  }
129
139
  return undefined;
@@ -143,6 +153,8 @@ export class WorkflowInspector implements Focusable {
143
153
  }
144
154
 
145
155
  render(width: number): string[] {
156
+ this.width = width;
157
+ if (!this.canInspect() && this.detailAgentId !== undefined) this.closeDetails();
146
158
  const outerWidth = Math.max(4, width);
147
159
  const innerWidth = Math.max(1, outerWidth - 4);
148
160
  const snapshot = this.snapshotProvider();
@@ -154,7 +166,7 @@ export class WorkflowInspector implements Focusable {
154
166
  : agents.find((entry) => entry.agent.id === this.detailAgentId);
155
167
  if (this.detailAgentId !== undefined && !selected) this.closeDetails();
156
168
 
157
- const totalHeight = workflowViewerHeight(this.tui.terminal.rows);
169
+ const totalHeight = Math.max(3, this.tui.terminal.rows - 1);
158
170
  const innerHeight = Math.max(1, totalHeight - 2);
159
171
  const interior = selected
160
172
  ? this.chatInterior(selected, innerWidth, innerHeight)
@@ -196,7 +208,7 @@ export class WorkflowInspector implements Focusable {
196
208
  ` ${this.columns(width)}`,
197
209
  ...body,
198
210
  this.theme.fg("dim", "─".repeat(width)),
199
- ` ${this.theme.fg("dim", `↑↓ select · click/enter inspect · ${viewport.percentage}% · esc close`)}`,
211
+ ` ${this.followUpError ? this.theme.fg("error", this.followUpError) : this.theme.fg("dim", `↑↓ select · ${this.canInspect() ? "enter inspect" : "resize to inspect (80×24)"} · X stop · esc back`)}`,
200
212
  ], height);
201
213
  }
202
214
 
@@ -207,35 +219,36 @@ export class WorkflowInspector implements Focusable {
207
219
  ): string[] {
208
220
  this.clickRows.clear();
209
221
  const { agent } = entry;
210
- const bodyHeight = Math.max(0, height - 6);
222
+ const canMessage = agent.status === "running" && this.live !== undefined;
223
+ this.input.focused = this._focused && canMessage;
224
+ const editorLines = canMessage ? this.input.render(Math.max(1, width - 1))
225
+ : [this.theme.fg("dim", agent.status === "queued" ? "Agent has not started yet." : "Agent has finished.")];
226
+ const transcript = this.live?.transcript?.(agent.id);
227
+ const queue = [...(transcript?.steering ?? []), ...(transcript?.followUp ?? [])];
228
+ const queueLines = queue.length ? [this.theme.fg("dim", truncateDisplay(`Queued: ${queue.join(" · ")}`, width))] : [];
229
+ const bodyHeight = Math.max(0, height - 5 - editorLines.length - queueLines.length);
211
230
  const rows = this.chatRows(agent, width);
212
231
  const maxStart = Math.max(0, rows.length - bodyHeight);
213
232
  const start = Math.max(0, maxStart - this.detailScroll);
214
233
  const body = fitWorkflowViewerRows(rows.slice(start, start + bodyHeight), bodyHeight);
215
234
  const elapsed = agent.startedAt === undefined ? "queued" : formatDuration((agent.doneAt ?? Date.now()) - agent.startedAt);
216
235
  const details = [
217
- shortModel(agent.model ?? "host default"),
218
- entry.phase,
236
+ agentModelName(agent),
237
+ thinkingLabel(agent.thinkingLevel),
219
238
  elapsed,
220
- `${agent.toolUses} tool${agent.toolUses === 1 ? "" : "s"}`,
221
239
  ].join(this.theme.fg("dim", " · "));
222
- const canMessage = agent.status === "running" && this.live !== undefined;
223
- this.input.focused = this._focused && canMessage;
224
- this.inputRow = canMessage ? height - 1 : undefined;
225
- const inputLine = canMessage
226
- ? this.input.render(Math.max(1, width - 1))[0] ?? ""
227
- : this.theme.fg("dim", agent.status === "queued" ? "Agent has not started yet." : "Agent has finished.");
240
+ this.inputRow = canMessage ? 4 + bodyHeight + queueLines.length : undefined;
228
241
  const help = this.followUpError
229
242
  ? this.theme.fg("error", truncateDisplay(this.followUpError, Math.max(1, width - 1)))
230
- : this.theme.fg("dim", "enter send · page up/down scroll · backspace agents · esc close");
243
+ : this.theme.fg("dim", "enter steer · alt+enter queue · /model · /thinking · esc back");
231
244
 
232
245
  return fitWorkflowViewerRows([
233
246
  ` ${statusIcon(agent.status, this.theme)} ${this.theme.fg("accent", this.theme.bold(agent.label))}`,
234
247
  ` ${details}`,
235
248
  this.theme.fg("dim", "─".repeat(width)),
236
249
  ...body,
237
- this.theme.fg("dim", "─".repeat(width)),
238
- ` ${inputLine}`,
250
+ ...queueLines,
251
+ ...editorLines,
239
252
  ` ${help}`,
240
253
  ], height);
241
254
  }
@@ -250,8 +263,23 @@ export class WorkflowInspector implements Focusable {
250
263
  this.tui.requestRender();
251
264
  }
252
265
 
266
+ private canInspect(): boolean {
267
+ return this.width >= 80 && this.tui.terminal.rows >= 24;
268
+ }
269
+
270
+ private stopSelectedAgent(): void {
271
+ const agent = this.agents()[this.selected]?.agent;
272
+ if (!agent || !this.live?.stopAgent) return;
273
+ try {
274
+ this.live.stopAgent(agent.id);
275
+ } catch (error) {
276
+ this.followUpError = unknownErrorMessage(error);
277
+ }
278
+ this.tui.requestRender();
279
+ }
280
+
253
281
  private openDetails(count: number): void {
254
- if (count === 0) return;
282
+ if (count === 0 || !this.canInspect()) return;
255
283
  const entry = this.agents()[this.selected];
256
284
  if (!entry) return;
257
285
  this.detailAgentId = entry.agent.id;
@@ -265,7 +293,7 @@ export class WorkflowInspector implements Focusable {
265
293
  this.detailAgentId = undefined;
266
294
  this.detailScroll = 0;
267
295
  this.followUpError = undefined;
268
- this.input.setValue("");
296
+ this.input.setText("");
269
297
  this.input.focused = false;
270
298
  this.tui.requestRender();
271
299
  }
@@ -280,20 +308,20 @@ export class WorkflowInspector implements Focusable {
280
308
  return this.agents().some((entry) => entry.agent.id === this.detailAgentId && entry.agent.status === "running");
281
309
  }
282
310
 
283
- private async submitFollowUp(value: string): Promise<void> {
311
+ private async submitFollowUp(value: string, steer = true): Promise<void> {
284
312
  const message = value.trim();
285
313
  const agentId = this.detailAgentId;
286
314
  if (!message || agentId === undefined || !this.live || this.sending) return;
287
315
  this.sending = true;
288
316
  this.followUpError = undefined;
289
- this.input.setValue("");
317
+ this.input.setText("");
290
318
  this.tui.requestRender();
291
319
  try {
292
- await this.live.followUp(agentId, message);
320
+ await this.live.followUp(agentId, message, steer);
293
321
  this.detailScroll = 0;
294
322
  } catch (error) {
295
323
  this.followUpError = unknownErrorMessage(error);
296
- this.input.setValue(message);
324
+ this.input.setText(message);
297
325
  } finally {
298
326
  this.sending = false;
299
327
  this.tui.requestRender();
@@ -319,6 +347,12 @@ export class WorkflowInspector implements Focusable {
319
347
  }
320
348
 
321
349
  private chatRows(agent: AgentRowSnapshot, width: number): string[] {
350
+ const transcript = this.live?.transcript?.(agent.id);
351
+ if (transcript) {
352
+ let view = this.transcriptViews.get(agent.id);
353
+ if (!view) { view = new AgentTranscriptView(); this.transcriptViews.set(agent.id, view); }
354
+ return view.render(transcript, width, this.tui as TUI, transcript.cwd ?? process.cwd());
355
+ }
322
356
  const messages = this.live?.conversation(agent.id) ?? [];
323
357
  if (messages.length === 0) return [` ${this.theme.fg("dim", "Waiting for agent activity…")}`];
324
358
  return messages.flatMap((message) => this.chatMessageRows(message, width));
@@ -381,8 +415,8 @@ export class WorkflowInspector implements Focusable {
381
415
  const modelWidth = Math.max(12, Math.floor(width * 0.28));
382
416
  const activityWidth = Math.max(8, width - taskWidth - modelWidth - 7);
383
417
  const task = this.cell(agent.label, taskWidth, agent.status === "failed" ? "error" : "text");
384
- const model = this.cell(shortModel(agent.model ?? "host default"), modelWidth, "muted");
385
- const activity = this.cell(agentActivity(agent), activityWidth, agent.status === "failed" ? "error" : "dim");
418
+ const model = this.cell(agentModelName(agent), modelWidth, "muted");
419
+ const activity = this.cell(thinkingLabel(agent.thinkingLevel), activityWidth, "dim");
386
420
  const row = `${marker} ${statusIcon(agent.status, this.theme)} ${task} ${model} ${activity}`;
387
421
  return selected ? this.theme.bg("selectedBg", fitWorkflowViewerRow(row, width)) : fitWorkflowViewerRow(row, width);
388
422
  }
@@ -390,21 +424,15 @@ export class WorkflowInspector implements Focusable {
390
424
  private columns(width: number): string {
391
425
  const taskWidth = Math.max(12, Math.floor(width * 0.36));
392
426
  const modelWidth = Math.max(12, Math.floor(width * 0.28));
393
- return this.theme.fg("dim", ` ${"Task".padEnd(taskWidth + 2)}${"Model".padEnd(modelWidth + 1)}Activity`);
427
+ return this.theme.fg("dim", ` ${"Task".padEnd(taskWidth + 2)}${"Model".padEnd(modelWidth + 1)}Thinking`);
394
428
  }
395
429
 
396
430
  private summary(snapshot: WorkflowProgressSnapshot, agents: readonly BoardAgent[]): string {
397
- const counts = { queued: 0, running: 0, done: 0, failed: 0 };
398
- for (const { agent } of agents) counts[agent.status]++;
431
+ const done = agents.filter(({ agent }) => agent.status === "done").length;
399
432
  const parts = [
400
- this.theme.fg("accent", snapshot.currentPhase),
401
- `${counts.running} running`,
402
- `${counts.queued} queued`,
403
- this.theme.fg("success", `${counts.done} done`),
404
- ...(counts.failed > 0 ? [this.theme.fg("error", `${counts.failed} failed`)] : []),
433
+ `${done}/${agents.length} done`,
405
434
  formatDuration((snapshot.doneAt ?? Date.now()) - snapshot.startedAt),
406
- formatWorkflowUsageLine(snapshot.usage) ?? "",
407
- ].filter(Boolean);
435
+ ];
408
436
  return parts.join(this.theme.fg("dim", " · "));
409
437
  }
410
438
 
@@ -419,17 +447,3 @@ export class WorkflowInspector implements Focusable {
419
447
  return `${this.theme.fg("border", "│")} ${fitted}${padding} ${this.theme.fg("border", "│")}`;
420
448
  }
421
449
  }
422
-
423
- function shortModel(model: string): string {
424
- return model.replace(/^openai-codex\//, "codex/").replace(/^anthropic\//, "");
425
- }
426
-
427
- function agentActivity(agent: AgentRowSnapshot): string {
428
- if (agent.status === "queued") return "queued";
429
- if (agent.status === "failed") return agent.error ?? "failed";
430
- const parts: string[] = [];
431
- if (agent.lastTool) parts.push(agent.lastTool);
432
- if (agent.toolUses > 0) parts.push(`${agent.toolUses} tool${agent.toolUses === 1 ? "" : "s"}`);
433
- if (agent.startedAt !== undefined) parts.push(formatDuration((agent.doneAt ?? Date.now()) - agent.startedAt));
434
- return parts.join(" · ") || agent.status;
435
- }
@@ -1,78 +1,38 @@
1
1
  import type { Theme } from "@earendil-works/pi-coding-agent";
2
+ import { visibleWidth } from "@earendil-works/pi-tui";
2
3
  import type { AgentRowSnapshot, WorkflowProgressSnapshot } from "../progress-types.ts";
3
- import { formatWorkflowUsageLine } from "../usage.ts";
4
4
  import { formatDuration, statusIcon } from "./workflow-format.ts";
5
5
  import { toDisplayLine } from "./display-text.ts";
6
6
 
7
- const MAX_WIDGET_LINES = 9;
8
-
9
- /** Compact live board shown above the editor. */
10
- export function renderWorkflowWidgetLines(snapshot: WorkflowProgressSnapshot, theme: Theme): string[] {
11
- const agents = snapshot.phases.flatMap((phase) => phase.agents);
12
- const ordered = [...agents].sort((left, right) => statusOrder(left.status) - statusOrder(right.status));
13
- const counts = countAgents(agents);
14
- const elapsed = formatDuration((snapshot.doneAt ?? Date.now()) - snapshot.startedAt);
15
- const active = counts.running + counts.queued;
16
- const icon = active > 0 ? theme.fg("accent", "●") : theme.fg("success", "✓");
17
- const summary = [
18
- `${counts.running} running`,
19
- `${counts.queued} queued`,
20
- `${counts.done}/${counts.total} done`,
21
- ...(counts.failed > 0 ? [`${counts.failed} failed`] : []),
22
- elapsed,
23
- ].join(` ${theme.fg("dim", "·")} `);
24
- const lines = [
25
- `${icon} ${theme.bold(snapshot.title)} ${theme.fg("dim", `· ${snapshot.currentPhase} · ${summary}`)}`,
26
- ];
27
-
28
- const footer = footerLine(snapshot, theme);
29
- const bodyLimit = Math.max(0, MAX_WIDGET_LINES - lines.length - 1);
30
- for (const agent of ordered.slice(0, bodyLimit)) lines.push(agentLine(agent, theme));
31
- const hidden = ordered.length - Math.min(ordered.length, bodyLimit);
32
- if (hidden > 0 && lines.length < MAX_WIDGET_LINES) {
33
- lines.push(theme.fg("dim", ` +${hidden} more · /workflow`));
34
- } else if (footer && lines.length < MAX_WIDGET_LINES) {
35
- lines.push(footer);
36
- }
37
- return lines.map((line) => line.replace(/[\r\n]+/g, " "));
38
- }
39
-
40
- interface AgentCounts {
41
- queued: number;
42
- running: number;
43
- done: number;
44
- failed: number;
45
- total: number;
7
+ export function thinkingLabel(level?: string): string {
8
+ if (!level) return "—";
9
+ return level === "xhigh" ? "XHigh" : level[0].toUpperCase() + level.slice(1);
46
10
  }
47
11
 
48
- function countAgents(agents: readonly AgentRowSnapshot[]): AgentCounts {
49
- const counts: AgentCounts = { queued: 0, running: 0, done: 0, failed: 0, total: agents.length };
50
- for (const agent of agents) counts[agent.status]++;
51
- return counts;
12
+ export function agentModelName(agent: AgentRowSnapshot): string {
13
+ return toDisplayLine(agent.modelName ?? agent.model ?? "—", 48);
52
14
  }
53
15
 
54
- function statusOrder(status: AgentRowSnapshot["status"]): number {
55
- return status === "running" ? 0 : status === "queued" ? 1 : status === "failed" ? 2 : 3;
56
- }
57
-
58
- function agentLine(agent: AgentRowSnapshot, theme: Theme): string {
59
- const model = toDisplayLine(shortModel(agent.model ?? "host default"), 48);
60
- const details: string[] = [];
61
- if (agent.lastTool) details.push(toDisplayLine(agent.lastTool, 32));
62
- if (agent.toolUses > 0) details.push(`${agent.toolUses} tool${agent.toolUses === 1 ? "" : "s"}`);
63
- if (agent.startedAt !== undefined) details.push(formatDuration((agent.doneAt ?? Date.now()) - agent.startedAt));
64
- const activity = details.length > 0 ? ` ${theme.fg("dim", `· ${details.join(" · ")}`)}` : "";
65
- const color = agent.status === "failed" ? "error" : agent.status === "running" ? "text" : "muted";
66
- return ` ${statusIcon(agent.status, theme)} ${theme.fg(color, toDisplayLine(agent.label, 64))} ${theme.fg("accent", model)}${activity}`;
67
- }
68
-
69
- function footerLine(snapshot: WorkflowProgressSnapshot, theme: Theme): string {
70
- const usage = formatWorkflowUsageLine(snapshot.usage);
71
- const latest = snapshot.logs.at(-1);
72
- const parts = [usage, latest ? toDisplayLine(latest, 100) : undefined, "/workflow"].filter(Boolean);
73
- return theme.fg("dim", ` ${parts.join(" · ")}`);
16
+ /** Compact live board; detailed activity is available through /workflow. */
17
+ export function renderWorkflowWidgetLines(snapshot: WorkflowProgressSnapshot, theme: Theme): string[] {
18
+ const agents = snapshot.phases.flatMap((phase) => phase.agents);
19
+ const ordered = [...agents].sort((a, b) => order(a.status) - order(b.status));
20
+ const done = agents.filter((agent) => agent.status === "done").length;
21
+ const elapsed = formatDuration((snapshot.doneAt ?? Date.now()) - snapshot.startedAt);
22
+ const shown = ordered.slice(0, 7);
23
+ const names = shown.map((agent) => toDisplayLine(agent.label, 32));
24
+ const models = shown.map(agentModelName);
25
+ const nameWidth = Math.max(0, ...names.map(visibleWidth));
26
+ const modelWidth = Math.max(0, ...models.map(visibleWidth));
27
+ const pad = (text: string, width: number) => text + " ".repeat(Math.max(0, width - visibleWidth(text)));
28
+ const lines = [`${theme.bold(snapshot.title)} ${theme.fg("dim", `· ${done}/${agents.length} done · ${elapsed}`)}`];
29
+ shown.forEach((agent, index) => lines.push(
30
+ ` ${statusIcon(agent.status, theme)} ${pad(names[index], nameWidth)} ${theme.fg("muted", pad(models[index], modelWidth))} ${theme.fg("dim", thinkingLabel(agent.thinkingLevel))}`,
31
+ ));
32
+ if (agents.length > shown.length) lines.push(theme.fg("dim", ` +${agents.length - shown.length} more · /workflow`));
33
+ return lines;
74
34
  }
75
35
 
76
- function shortModel(model: string): string {
77
- return model.replace(/^openai-codex\//, "codex/").replace(/^anthropic\//, "");
36
+ function order(status: AgentRowSnapshot["status"]): number {
37
+ return status === "running" || status === "stopping" ? 0 : status === "queued" ? 1 : 2;
78
38
  }
@@ -50,7 +50,7 @@ export function publishVerifiedKeptProgress(
50
50
  }
51
51
 
52
52
  export interface LensVerificationPipelineOptions {
53
- api: Pick<WorkflowApi, "agent" | "parallel" | "phase" | "progress" | "log">;
53
+ api: Pick<WorkflowApi, "agent" | "modelProfile" | "parallel" | "phase" | "progress" | "log">;
54
54
  lenses: readonly AdvisoryLens[];
55
55
  perLens: number;
56
56
  finderPhase?: "Find" | "Hypothesize";
@@ -77,7 +77,7 @@ export async function runLensVerificationPipeline(
77
77
  const result = await api.agent(finderPrompt(lens), {
78
78
  phase: finderPhase, label: `${finderPhase.toLowerCase()}:${lens.label}`,
79
79
  tools: DEFAULT_ADVISORY_TOOLS, toolHints: DEFAULT_ADVISORY_TOOL_HINTS,
80
- profile: "small", schema: AdvisoryCandidatesSchema,
80
+ ...api.modelProfile("small"), schema: AdvisoryCandidatesSchema,
81
81
  });
82
82
  const raw = identifyCandidates(result.candidates.slice(0, perLens), lens.label);
83
83
  rawCandidates += raw.length;
@@ -105,7 +105,7 @@ export async function runLensVerificationPipeline(
105
105
  const judged = await api.agent(`${verifierPrompt(candidate)}\nCandidate record: ${JSON.stringify(candidate)}`, {
106
106
  phase: "Verify", label: `verify:${location.file.split("/").pop() ?? location.file}`,
107
107
  tools: DEFAULT_ADVISORY_TOOLS, toolHints: DEFAULT_ADVISORY_TOOL_HINTS,
108
- profile: "small", schema: AdvisoryVerdictSchema,
108
+ ...api.modelProfile("small"), schema: AdvisoryVerdictSchema,
109
109
  });
110
110
  recordVerdictProgress(api.progress, candidate, judged, () => { refuted += 1; });
111
111
  return { ...candidate, verdict: judged.verdict, evidence: judged.evidence, confidence: judged.confidence };
@@ -115,7 +115,7 @@ export async function runLensVerificationPipeline(
115
115
  }
116
116
 
117
117
  export async function synthesizeAdvisoryReport(
118
- api: Pick<WorkflowApi, "agent" | "parallel" | "phase">,
118
+ api: Pick<WorkflowApi, "agent" | "modelProfile" | "parallel" | "phase">,
119
119
  prompt: string,
120
120
  ranked: readonly AdvisoryVerified[],
121
121
  coverage: AdvisoryStageCoverage[],
@@ -123,7 +123,7 @@ export async function synthesizeAdvisoryReport(
123
123
  api.phase("Synthesize");
124
124
  const [report] = await collectAdvisoryStage(api, "Synthesize", [{ id: "synthesize", run: () => api.agent(
125
125
  SYNTHESIS_ID_INSTRUCTIONS + prompt,
126
- { phase: "Synthesize", label: "synthesize", tools: [], profile: "medium", resume: "read-only", schema: AdvisorySynthesisSchema },
126
+ { phase: "Synthesize", label: "synthesize", tools: [], ...api.modelProfile("medium"), resume: "read-only", schema: AdvisorySynthesisSchema },
127
127
  ) }], coverage);
128
128
  return resolveAdvisorySynthesis(report, ranked, {
129
129
  impact: "Impact not restated by verification.",