pi-workflow-engine 0.1.1 → 0.3.0
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.
- package/README.md +109 -56
- package/package.json +2 -1
- package/src/advisory-schema.ts +64 -0
- package/src/agent-runner.ts +8 -4
- package/src/engine.ts +18 -2
- package/src/index.ts +79 -14
- package/src/progress.ts +258 -23
- package/src/types.ts +29 -0
- package/src/ui/workflow-format.ts +83 -0
- package/src/ui/workflow-inspector.ts +248 -0
- package/src/ui/workflow-result-renderer.ts +195 -0
- package/src/ui/workflow-widget.ts +143 -0
- package/src/workflows.ts +4 -1
- package/workflows/code-review.ts +163 -58
- package/workflows/diagnose.ts +305 -0
- package/workflows/perf-review.ts +295 -0
- package/workflows/refactor-scout.ts +289 -0
package/src/progress.ts
CHANGED
|
@@ -1,9 +1,63 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { TUI } from "@earendil-works/pi-tui";
|
|
3
|
+
import type { WorkflowLaneItemStatus, WorkflowProgressEvent } from "./types.ts";
|
|
4
|
+
import { statusText } from "./ui/workflow-format.ts";
|
|
5
|
+
import { createWorkflowWidget, type WorkflowWidget } from "./ui/workflow-widget.ts";
|
|
6
|
+
|
|
7
|
+
export type AgentRowStatus = "queued" | "running" | "done" | "failed";
|
|
8
|
+
|
|
9
|
+
export interface AgentRowSnapshot {
|
|
10
|
+
readonly id: number;
|
|
11
|
+
readonly label: string;
|
|
12
|
+
readonly status: AgentRowStatus;
|
|
13
|
+
readonly startedAt?: number;
|
|
14
|
+
readonly doneAt?: number;
|
|
15
|
+
readonly toolUses: number;
|
|
16
|
+
readonly lastTool?: string;
|
|
17
|
+
readonly error?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface PhaseSnapshot {
|
|
21
|
+
readonly title: string;
|
|
22
|
+
readonly agents: readonly AgentRowSnapshot[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface WorkflowCounterSnapshot {
|
|
26
|
+
readonly key: string;
|
|
27
|
+
readonly label: string;
|
|
28
|
+
readonly value: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface WorkflowLaneItemSnapshot {
|
|
32
|
+
readonly lane: string;
|
|
33
|
+
readonly title: string;
|
|
34
|
+
readonly subtitle?: string;
|
|
35
|
+
readonly status: WorkflowLaneItemStatus;
|
|
36
|
+
readonly details?: string;
|
|
37
|
+
readonly createdAt: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface WorkflowProgressSnapshot {
|
|
41
|
+
readonly title: string;
|
|
42
|
+
readonly startedAt: number;
|
|
43
|
+
readonly doneAt?: number;
|
|
44
|
+
readonly currentPhase: string;
|
|
45
|
+
readonly phases: readonly PhaseSnapshot[];
|
|
46
|
+
readonly counters: readonly WorkflowCounterSnapshot[];
|
|
47
|
+
readonly summary: readonly [string, string | number][];
|
|
48
|
+
readonly lanes: readonly [string, readonly WorkflowLaneItemSnapshot[]][];
|
|
49
|
+
readonly logs: readonly string[];
|
|
50
|
+
}
|
|
2
51
|
|
|
3
52
|
interface AgentRow {
|
|
53
|
+
id: number;
|
|
4
54
|
label: string;
|
|
5
|
-
status:
|
|
55
|
+
status: AgentRowStatus;
|
|
56
|
+
startedAt?: number;
|
|
57
|
+
doneAt?: number;
|
|
58
|
+
toolUses: number;
|
|
6
59
|
lastTool?: string;
|
|
60
|
+
error?: string;
|
|
7
61
|
}
|
|
8
62
|
|
|
9
63
|
interface Phase {
|
|
@@ -11,21 +65,48 @@ interface Phase {
|
|
|
11
65
|
agents: AgentRow[];
|
|
12
66
|
}
|
|
13
67
|
|
|
68
|
+
interface WorkflowCounter {
|
|
69
|
+
key: string;
|
|
70
|
+
label: string;
|
|
71
|
+
value: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface WorkflowLaneItem {
|
|
75
|
+
lane: string;
|
|
76
|
+
title: string;
|
|
77
|
+
subtitle?: string;
|
|
78
|
+
status: WorkflowLaneItemStatus;
|
|
79
|
+
details?: string;
|
|
80
|
+
createdAt: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const LOG_LIMIT = 24;
|
|
84
|
+
|
|
14
85
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* so it never corrupts stdout.
|
|
86
|
+
* Tracks live workflow state for widgets, footer/status text, result renderers,
|
|
87
|
+
* and headless stderr breadcrumbs.
|
|
18
88
|
*/
|
|
19
89
|
export class ProgressTracker {
|
|
20
90
|
private readonly phases: Phase[] = [];
|
|
21
91
|
private readonly logs: string[] = [];
|
|
22
|
-
private
|
|
92
|
+
private readonly counters = new Map<string, WorkflowCounter>();
|
|
93
|
+
private readonly summary = new Map<string, string | number>();
|
|
94
|
+
private readonly lanes = new Map<string, WorkflowLaneItem[]>();
|
|
95
|
+
private readonly startedAt = Date.now();
|
|
96
|
+
private doneAt: number | undefined;
|
|
97
|
+
private currentPhase = "Workflow";
|
|
98
|
+
private nextAgentId = 1;
|
|
99
|
+
private widget: WorkflowWidget | undefined;
|
|
100
|
+
private widgetRegistered = false;
|
|
101
|
+
private tui: Pick<TUI, "requestRender"> | undefined;
|
|
102
|
+
private widgetInterval: ReturnType<typeof setInterval> | undefined;
|
|
103
|
+
private lastStatusText: string | undefined;
|
|
23
104
|
|
|
24
105
|
constructor(
|
|
25
106
|
private readonly ctx: ExtensionContext,
|
|
26
107
|
private readonly title: string,
|
|
27
108
|
) {
|
|
28
|
-
this.ensurePhase(this.
|
|
109
|
+
this.ensurePhase(this.currentPhase);
|
|
29
110
|
}
|
|
30
111
|
|
|
31
112
|
private ensurePhase(title: string): Phase {
|
|
@@ -38,7 +119,7 @@ export class ProgressTracker {
|
|
|
38
119
|
}
|
|
39
120
|
|
|
40
121
|
phase(title: string): void {
|
|
41
|
-
this.
|
|
122
|
+
this.currentPhase = title;
|
|
42
123
|
this.ensurePhase(title);
|
|
43
124
|
if (!this.ctx.hasUI) process.stderr.write(`[${this.title}] ${title}\n`);
|
|
44
125
|
this.render();
|
|
@@ -46,31 +127,121 @@ export class ProgressTracker {
|
|
|
46
127
|
|
|
47
128
|
log(message: string): void {
|
|
48
129
|
this.logs.push(message);
|
|
49
|
-
|
|
130
|
+
while (this.logs.length > LOG_LIMIT) this.logs.shift();
|
|
50
131
|
if (!this.ctx.hasUI) process.stderr.write(`[${this.title}] ${message}\n`);
|
|
51
132
|
this.render();
|
|
52
133
|
}
|
|
53
134
|
|
|
54
|
-
|
|
55
|
-
|
|
135
|
+
event(event: WorkflowProgressEvent): void {
|
|
136
|
+
switch (event.type) {
|
|
137
|
+
case "counter":
|
|
138
|
+
this.counters.set(event.key, { key: event.key, label: event.label, value: event.value });
|
|
139
|
+
break;
|
|
140
|
+
case "counter_delta": {
|
|
141
|
+
const current = this.counters.get(event.key);
|
|
142
|
+
this.counters.set(event.key, {
|
|
143
|
+
key: event.key,
|
|
144
|
+
label: event.label,
|
|
145
|
+
value: (current?.value ?? 0) + event.delta,
|
|
146
|
+
});
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
case "lane_item": {
|
|
150
|
+
const lane = this.lanes.get(event.lane) ?? [];
|
|
151
|
+
lane.push({
|
|
152
|
+
lane: event.lane,
|
|
153
|
+
title: event.title,
|
|
154
|
+
subtitle: event.subtitle,
|
|
155
|
+
status: event.status,
|
|
156
|
+
details: event.details,
|
|
157
|
+
createdAt: Date.now(),
|
|
158
|
+
});
|
|
159
|
+
this.lanes.set(event.lane, lane);
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
case "summary":
|
|
163
|
+
this.summary.set(event.key, event.value);
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
this.render();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
agentQueued(phase: string | undefined, label: string): number {
|
|
170
|
+
const id = this.nextAgentId++;
|
|
171
|
+
this.ensurePhase(phase ?? this.currentPhase).agents.push({ label, id, status: "queued", toolUses: 0 });
|
|
172
|
+
this.render();
|
|
173
|
+
return id;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
agentStart(phase: string | undefined, label: string, id?: number): void {
|
|
177
|
+
const row = id === undefined ? undefined : this.findRowById(id);
|
|
178
|
+
if (row) {
|
|
179
|
+
row.status = "running";
|
|
180
|
+
row.startedAt = Date.now();
|
|
181
|
+
row.error = undefined;
|
|
182
|
+
} else {
|
|
183
|
+
this.ensurePhase(phase ?? this.currentPhase).agents.push({
|
|
184
|
+
label,
|
|
185
|
+
id: this.nextAgentId++,
|
|
186
|
+
status: "running",
|
|
187
|
+
startedAt: Date.now(),
|
|
188
|
+
toolUses: 0,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
56
191
|
this.render();
|
|
57
192
|
}
|
|
58
193
|
|
|
59
|
-
agentTool(label: string, tool: string): void {
|
|
60
|
-
const row = this.findRow(label);
|
|
61
|
-
if (row)
|
|
194
|
+
agentTool(label: string, tool: string, id?: number): void {
|
|
195
|
+
const row = this.findRow(label, id);
|
|
196
|
+
if (row) {
|
|
197
|
+
row.lastTool = tool;
|
|
198
|
+
row.toolUses += 1;
|
|
199
|
+
}
|
|
62
200
|
this.render();
|
|
63
201
|
}
|
|
64
202
|
|
|
65
|
-
agentDone(label: string): void {
|
|
66
|
-
const row = this.findRow(label);
|
|
67
|
-
if (row && row.status
|
|
203
|
+
agentDone(label: string, id?: number): void {
|
|
204
|
+
const row = this.findRow(label, id);
|
|
205
|
+
if (row && row.status !== "failed") {
|
|
206
|
+
row.status = "done";
|
|
207
|
+
row.doneAt = Date.now();
|
|
208
|
+
}
|
|
68
209
|
this.render();
|
|
69
210
|
}
|
|
70
211
|
|
|
71
|
-
|
|
212
|
+
agentFailed(label: string, error: unknown, id?: number): void {
|
|
213
|
+
const row = this.findRow(label, id);
|
|
214
|
+
if (row) {
|
|
215
|
+
row.status = "failed";
|
|
216
|
+
row.doneAt = Date.now();
|
|
217
|
+
row.error = errorMessage(error);
|
|
218
|
+
}
|
|
219
|
+
this.render();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
snapshot(): WorkflowProgressSnapshot {
|
|
223
|
+
return {
|
|
224
|
+
title: this.title,
|
|
225
|
+
startedAt: this.startedAt,
|
|
226
|
+
doneAt: this.doneAt,
|
|
227
|
+
currentPhase: this.currentPhase,
|
|
228
|
+
phases: this.phases.map((phase) => ({
|
|
229
|
+
title: phase.title,
|
|
230
|
+
agents: phase.agents.map((agent) => ({ ...agent })),
|
|
231
|
+
})),
|
|
232
|
+
counters: [...this.counters.values()].map((counter) => ({ ...counter })),
|
|
233
|
+
summary: [...this.summary.entries()],
|
|
234
|
+
lanes: [...this.lanes.entries()].map(([lane, items]) => [lane, items.map((item) => ({ ...item }))]),
|
|
235
|
+
logs: [...this.logs],
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private findRow(label: string, id?: number): AgentRow | undefined {
|
|
240
|
+
if (id !== undefined) return this.findRowById(id);
|
|
72
241
|
for (let i = this.phases.length - 1; i >= 0; i--) {
|
|
73
|
-
const running = this.phases[i].agents.find(
|
|
242
|
+
const running = this.phases[i].agents.find(
|
|
243
|
+
(agent) => agent.label === label && (agent.status === "running" || agent.status === "queued"),
|
|
244
|
+
);
|
|
74
245
|
if (running) return running;
|
|
75
246
|
}
|
|
76
247
|
for (let i = this.phases.length - 1; i >= 0; i--) {
|
|
@@ -80,29 +251,93 @@ export class ProgressTracker {
|
|
|
80
251
|
return undefined;
|
|
81
252
|
}
|
|
82
253
|
|
|
254
|
+
private findRowById(id: number): AgentRow | undefined {
|
|
255
|
+
for (const phase of this.phases) {
|
|
256
|
+
const row = phase.agents.find((agent) => agent.id === id);
|
|
257
|
+
if (row) return row;
|
|
258
|
+
}
|
|
259
|
+
return undefined;
|
|
260
|
+
}
|
|
261
|
+
|
|
83
262
|
private lines(): string[] {
|
|
84
263
|
const out: string[] = [`⚙ ${this.title}`];
|
|
85
264
|
for (const phase of this.phases) {
|
|
86
265
|
if (phase.agents.length === 0 && phase.title === "Workflow") continue;
|
|
87
266
|
out.push(` ${phase.title}`);
|
|
88
267
|
for (const agent of phase.agents) {
|
|
89
|
-
const icon = agent.status === "done" ? "✓" : "⏳";
|
|
268
|
+
const icon = agent.status === "done" ? "✓" : agent.status === "failed" ? "✗" : agent.status === "queued" ? "○" : "⏳";
|
|
90
269
|
const tool = agent.status === "running" && agent.lastTool ? ` · ${agent.lastTool}` : "";
|
|
91
|
-
|
|
270
|
+
const error = agent.status === "failed" && agent.error ? ` · ${agent.error}` : "";
|
|
271
|
+
out.push(` ${icon} ${agent.label}${tool}${error}`);
|
|
92
272
|
}
|
|
93
273
|
}
|
|
94
|
-
|
|
274
|
+
const counterLine = [...this.counters.values()].map((counter) => `${counter.label}: ${counter.value}`).join(" · ");
|
|
275
|
+
if (counterLine) out.push(` ${counterLine}`);
|
|
276
|
+
for (const line of this.logs.slice(-6)) out.push(` · ${line}`);
|
|
95
277
|
return out;
|
|
96
278
|
}
|
|
97
279
|
|
|
98
280
|
private render(): void {
|
|
99
|
-
if (this.ctx.hasUI)
|
|
281
|
+
if (!this.ctx.hasUI) return;
|
|
282
|
+
this.ensureWidget();
|
|
283
|
+
this.widget?.invalidate();
|
|
284
|
+
this.tui?.requestRender();
|
|
285
|
+
this.publishStatus();
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private publishStatus(): void {
|
|
289
|
+
const next = statusText(this.snapshot(), this.ctx.ui.theme);
|
|
290
|
+
if (next === this.lastStatusText) return;
|
|
291
|
+
this.ctx.ui.setStatus("workflow", next);
|
|
292
|
+
this.lastStatusText = next;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
private ensureWidget(): void {
|
|
296
|
+
if (this.widgetRegistered) return;
|
|
297
|
+
this.widget = createWorkflowWidget(() => this.snapshot());
|
|
298
|
+
this.ctx.ui.setWidget(
|
|
299
|
+
"workflow",
|
|
300
|
+
(tui, theme) => {
|
|
301
|
+
this.tui = tui;
|
|
302
|
+
return {
|
|
303
|
+
render: (width?: number) => this.widget?.render(width ?? tui.terminal.columns, theme) ?? [],
|
|
304
|
+
invalidate: () => this.widget?.invalidate(),
|
|
305
|
+
};
|
|
306
|
+
},
|
|
307
|
+
{ placement: "aboveEditor" },
|
|
308
|
+
);
|
|
309
|
+
this.widgetRegistered = true;
|
|
310
|
+
this.startWidgetTimer();
|
|
100
311
|
}
|
|
101
312
|
|
|
102
|
-
|
|
313
|
+
private startWidgetTimer(): void {
|
|
314
|
+
if (this.widgetInterval !== undefined) return;
|
|
315
|
+
this.widgetInterval = setInterval(() => {
|
|
316
|
+
this.widget?.nextFrame();
|
|
317
|
+
this.tui?.requestRender();
|
|
318
|
+
}, 100);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
private stopWidgetTimer(): void {
|
|
322
|
+
if (this.widgetInterval === undefined) return;
|
|
323
|
+
clearInterval(this.widgetInterval);
|
|
324
|
+
this.widgetInterval = undefined;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** Clear live workflow surfaces; optionally leave a one-line final status. */
|
|
103
328
|
done(status?: string): void {
|
|
329
|
+
this.doneAt = Date.now();
|
|
330
|
+
this.stopWidgetTimer();
|
|
104
331
|
if (!this.ctx.hasUI) return;
|
|
105
332
|
this.ctx.ui.setWidget("workflow", undefined);
|
|
106
333
|
this.ctx.ui.setStatus("workflow", status);
|
|
334
|
+
this.lastStatusText = status;
|
|
335
|
+
this.widgetRegistered = false;
|
|
336
|
+
this.widget = undefined;
|
|
337
|
+
this.tui = undefined;
|
|
107
338
|
}
|
|
108
339
|
}
|
|
340
|
+
|
|
341
|
+
function errorMessage(error: unknown): string {
|
|
342
|
+
return error instanceof Error ? error.message : String(error);
|
|
343
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -8,7 +8,34 @@ export interface WorkflowMeta {
|
|
|
8
8
|
phases?: Array<{ title: string }>;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
export interface WorkflowRunStats {
|
|
12
|
+
files: number;
|
|
13
|
+
candidates: number;
|
|
14
|
+
verified: number;
|
|
15
|
+
kept: number;
|
|
16
|
+
[key: string]: string | number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface WorkflowRunOptions {
|
|
20
|
+
inspect?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
11
23
|
/** Options for a single `agent()` call. */
|
|
24
|
+
export type WorkflowLaneItemStatus = "pending" | "running" | "success" | "warning" | "error";
|
|
25
|
+
|
|
26
|
+
export type WorkflowProgressEvent =
|
|
27
|
+
| { type: "counter"; key: string; label: string; value: number }
|
|
28
|
+
| { type: "counter_delta"; key: string; label: string; delta: number }
|
|
29
|
+
| {
|
|
30
|
+
type: "lane_item";
|
|
31
|
+
lane: string;
|
|
32
|
+
title: string;
|
|
33
|
+
subtitle?: string;
|
|
34
|
+
status: WorkflowLaneItemStatus;
|
|
35
|
+
details?: string;
|
|
36
|
+
}
|
|
37
|
+
| { type: "summary"; key: string; value: string | number };
|
|
38
|
+
|
|
12
39
|
export interface AgentOptions<S extends TSchema = TSchema> {
|
|
13
40
|
/** Label shown in the progress tree (e.g. "find:logic-bugs"). */
|
|
14
41
|
label?: string;
|
|
@@ -44,6 +71,8 @@ export interface WorkflowApi {
|
|
|
44
71
|
phase(title: string): void;
|
|
45
72
|
/** Emit a progress line. */
|
|
46
73
|
log(message: string): void;
|
|
74
|
+
/** Emit a structured progress event for native workflow UI surfaces. */
|
|
75
|
+
progress(event: WorkflowProgressEvent): void;
|
|
47
76
|
/** Raw argument string passed after the workflow name. */
|
|
48
77
|
args: string;
|
|
49
78
|
/** Working directory of the host session (typically the repo root). */
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
|
+
import type { WorkflowProgressSnapshot } from "../progress.ts";
|
|
4
|
+
import type { WorkflowLaneItemStatus } from "../types.ts";
|
|
5
|
+
|
|
6
|
+
export type WorkflowDisplayStatus = WorkflowLaneItemStatus | "queued" | "done" | "failed";
|
|
7
|
+
export type WorkflowThemeColor = Parameters<Theme["fg"]>[0];
|
|
8
|
+
|
|
9
|
+
export function formatDuration(ms: number): string {
|
|
10
|
+
if (!Number.isFinite(ms) || ms <= 0) return "0s";
|
|
11
|
+
if (ms < 1_000) return `${Math.round(ms)}ms`;
|
|
12
|
+
|
|
13
|
+
const totalSeconds = Math.floor(ms / 1_000);
|
|
14
|
+
if (totalSeconds < 60) return `${totalSeconds}s`;
|
|
15
|
+
|
|
16
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
17
|
+
const seconds = totalSeconds % 60;
|
|
18
|
+
if (minutes < 60) return seconds === 0 ? `${minutes}m` : `${minutes}m ${seconds}s`;
|
|
19
|
+
|
|
20
|
+
const hours = Math.floor(minutes / 60);
|
|
21
|
+
const remainingMinutes = minutes % 60;
|
|
22
|
+
return remainingMinutes === 0 ? `${hours}h` : `${hours}h ${remainingMinutes}m`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function formatCount(n: number): string {
|
|
26
|
+
if (!Number.isFinite(n)) return "0";
|
|
27
|
+
const sign = n < 0 ? "-" : "";
|
|
28
|
+
const value = Math.abs(n);
|
|
29
|
+
if (value < 1_000) return `${Math.trunc(n)}`;
|
|
30
|
+
if (value < 1_000_000) return `${sign}${formatCompact(value / 1_000)}k`;
|
|
31
|
+
return `${sign}${formatCompact(value / 1_000_000)}m`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function statusIcon(status: WorkflowDisplayStatus, theme: Theme): string {
|
|
35
|
+
switch (status) {
|
|
36
|
+
case "success":
|
|
37
|
+
case "done":
|
|
38
|
+
return theme.fg("success", "✓");
|
|
39
|
+
case "warning":
|
|
40
|
+
return theme.fg("warning", "!");
|
|
41
|
+
case "error":
|
|
42
|
+
case "failed":
|
|
43
|
+
return theme.fg("error", "✗");
|
|
44
|
+
case "running":
|
|
45
|
+
return theme.fg("accent", "●");
|
|
46
|
+
case "queued":
|
|
47
|
+
case "pending":
|
|
48
|
+
return theme.fg("dim", "○");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function badge(label: string, color: WorkflowThemeColor, theme: Theme): string {
|
|
53
|
+
return theme.fg(color, `[${label}]`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function truncateDisplay(text: string, width: number): string {
|
|
57
|
+
if (width <= 0) return "";
|
|
58
|
+
return truncateToWidth(text, width);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function statusText(snapshot: WorkflowProgressSnapshot, theme: Theme): string | undefined {
|
|
62
|
+
if (snapshot.doneAt !== undefined) return undefined;
|
|
63
|
+
|
|
64
|
+
const agents = snapshot.phases.flatMap((phase) => phase.agents);
|
|
65
|
+
const complete = agents.filter((agent) => agent.status === "done" || agent.status === "failed").length;
|
|
66
|
+
const active = agents.filter((agent) => agent.status === "running" || agent.status === "queued").length;
|
|
67
|
+
const total = agents.length;
|
|
68
|
+
const kept = snapshot.counters.find((counter) => counter.key === "kept" || counter.label.toLowerCase() === "kept");
|
|
69
|
+
const displayName = snapshot.title === "code-review" ? "review" : snapshot.title;
|
|
70
|
+
|
|
71
|
+
const parts = [theme.fg("accent", displayName), theme.fg("muted", snapshot.currentPhase)];
|
|
72
|
+
if (total > 0) parts.push(theme.fg("muted", `${complete}/${total}`));
|
|
73
|
+
if (kept) parts.push(theme.fg("success", `${formatCount(kept.value)} kept`));
|
|
74
|
+
else if (active > 0) parts.push(theme.fg("muted", `${active} active`));
|
|
75
|
+
|
|
76
|
+
return parts.join(theme.fg("dim", " · "));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function formatCompact(value: number): string {
|
|
80
|
+
if (value >= 100) return `${Math.round(value)}`;
|
|
81
|
+
const rounded = Math.round(value * 10) / 10;
|
|
82
|
+
return Number.isInteger(rounded) ? `${rounded}` : rounded.toFixed(1);
|
|
83
|
+
}
|