pi-subagents 0.23.1 → 0.24.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/CHANGELOG.md +13 -0
- package/README.md +13 -76
- package/package.json +1 -1
- package/prompts/parallel-cleanup.md +11 -1
- package/prompts/parallel-review.md +11 -1
- package/skills/pi-subagents/SKILL.md +11 -12
- package/src/agents/agent-serializer.ts +0 -42
- package/src/agents/agents.ts +1 -1
- package/src/extension/index.ts +2 -2
- package/src/runs/background/async-status.ts +16 -50
- package/src/runs/background/run-status.ts +8 -9
- package/src/runs/foreground/chain-clarify.ts +183 -218
- package/src/shared/status-format.ts +49 -0
- package/src/shared/types.ts +0 -5
- package/src/slash/slash-commands.ts +0 -74
- package/src/tui/render.ts +32 -58
- package/src/agents/agent-templates.ts +0 -60
- package/src/manager-ui/agent-manager-chain-detail.ts +0 -164
- package/src/manager-ui/agent-manager-detail.ts +0 -235
- package/src/manager-ui/agent-manager-edit.ts +0 -456
- package/src/manager-ui/agent-manager-list.ts +0 -283
- package/src/manager-ui/agent-manager-parallel.ts +0 -302
- package/src/manager-ui/agent-manager.ts +0 -732
- package/src/tui/subagents-status.ts +0 -621
- package/src/tui/text-editor.ts +0 -286
|
@@ -1,621 +0,0 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import * as path from "node:path";
|
|
3
|
-
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
4
|
-
import type { Component, TUI } from "@mariozechner/pi-tui";
|
|
5
|
-
import { matchesKey, truncateToWidth, wrapTextWithAnsi } from "@mariozechner/pi-tui";
|
|
6
|
-
import { type AsyncRunOverlayData, type AsyncRunOverlayOptions, type AsyncRunSummary, formatAsyncRunProgressLabel, listAsyncRunsForOverlay } from "../runs/background/async-status.ts";
|
|
7
|
-
import { ASYNC_DIR } from "../shared/types.ts";
|
|
8
|
-
import { formatDuration, formatTokens, shortenPath } from "../shared/formatters.ts";
|
|
9
|
-
import { formatScrollInfo, renderFooter, renderHeader, row } from "./render-helpers.ts";
|
|
10
|
-
|
|
11
|
-
const AUTO_REFRESH_MS = 2000;
|
|
12
|
-
const DETAIL_EVENT_LIMIT = 8;
|
|
13
|
-
const OUTPUT_TAIL_LINES = 20;
|
|
14
|
-
const DETAIL_FILE_TAIL_BYTES = 64 * 1024;
|
|
15
|
-
const DETAIL_VIEWPORT_HEIGHT = 18;
|
|
16
|
-
|
|
17
|
-
interface StatusRow {
|
|
18
|
-
kind: "section" | "run";
|
|
19
|
-
label: string;
|
|
20
|
-
run?: AsyncRunSummary;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
type AsyncRunStep = AsyncRunSummary["steps"][number];
|
|
24
|
-
|
|
25
|
-
interface ChainStepSpan {
|
|
26
|
-
stepIndex: number;
|
|
27
|
-
start: number;
|
|
28
|
-
count: number;
|
|
29
|
-
isParallel: boolean;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
interface StatusOverlayDeps {
|
|
33
|
-
sessionId: string;
|
|
34
|
-
listRunsForOverlay?: (asyncDirRoot: string, options?: AsyncRunOverlayOptions) => AsyncRunOverlayData;
|
|
35
|
-
refreshMs?: number;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function statusColor(theme: Theme, status: AsyncRunSummary["state"]): string {
|
|
39
|
-
switch (status) {
|
|
40
|
-
case "running": return theme.fg("warning", status);
|
|
41
|
-
case "queued": return theme.fg("accent", status);
|
|
42
|
-
case "complete": return theme.fg("success", status);
|
|
43
|
-
case "failed": return theme.fg("error", status);
|
|
44
|
-
case "paused": return theme.fg("warning", status);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function stepStatusColor(theme: Theme, status: string): string {
|
|
49
|
-
if (status === "running") return theme.fg("warning", status);
|
|
50
|
-
if (status === "pending") return theme.fg("dim", status);
|
|
51
|
-
if (status === "complete" || status === "completed") return theme.fg("success", status);
|
|
52
|
-
if (status === "failed") return theme.fg("error", status);
|
|
53
|
-
if (status === "paused") return theme.fg("warning", status);
|
|
54
|
-
return status;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function stepGlyph(theme: Theme, status: string): string {
|
|
58
|
-
if (status === "running") return theme.fg("accent", "▶");
|
|
59
|
-
if (status === "complete" || status === "completed") return theme.fg("success", "✓");
|
|
60
|
-
if (status === "failed") return theme.fg("error", "✗");
|
|
61
|
-
if (status === "paused") return theme.fg("warning", "■");
|
|
62
|
-
return theme.fg("dim", "◦");
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function runGlyph(theme: Theme, status: AsyncRunSummary["state"]): string {
|
|
66
|
-
if (status === "running") return theme.fg("accent", "▶");
|
|
67
|
-
if (status === "queued") return theme.fg("dim", "◦");
|
|
68
|
-
if (status === "complete") return theme.fg("success", "✓");
|
|
69
|
-
if (status === "paused") return theme.fg("warning", "■");
|
|
70
|
-
return theme.fg("error", "✗");
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function runLabel(theme: Theme, run: AsyncRunSummary, selected: boolean): string {
|
|
74
|
-
const prefix = selected ? theme.fg("accent", ">") : " ";
|
|
75
|
-
const stepLabel = formatAsyncRunProgressLabel(run);
|
|
76
|
-
const cwd = shortenPath(run.cwd ?? run.asyncDir);
|
|
77
|
-
const mode = ((theme as { bold?: (value: string) => string }).bold?.(run.mode)) ?? run.mode;
|
|
78
|
-
return `${prefix} ${runGlyph(theme, run.state)} ${mode} · ${stepLabel} · ${run.id.slice(0, 8)} · ${cwd}`;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function selectedIndex(rows: StatusRow[], cursor: number): number {
|
|
82
|
-
const runRows = rows.filter((row) => row.kind === "run");
|
|
83
|
-
if (runRows.length === 0) return -1;
|
|
84
|
-
return Math.max(0, Math.min(cursor, runRows.length - 1));
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function selectedRun(rows: StatusRow[], cursor: number): AsyncRunSummary | undefined {
|
|
88
|
-
const runRows = rows.filter((row) => row.kind === "run");
|
|
89
|
-
const index = selectedIndex(rows, cursor);
|
|
90
|
-
return index >= 0 ? runRows[index]?.run : undefined;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function buildRows(active: AsyncRunSummary[], recent: AsyncRunSummary[]): StatusRow[] {
|
|
94
|
-
const rows: StatusRow[] = [];
|
|
95
|
-
if (active.length > 0) {
|
|
96
|
-
rows.push({ kind: "section", label: "Active" });
|
|
97
|
-
for (const run of active) rows.push({ kind: "run", label: run.id, run });
|
|
98
|
-
}
|
|
99
|
-
if (recent.length > 0) {
|
|
100
|
-
rows.push({ kind: "section", label: "Recent" });
|
|
101
|
-
for (const run of recent) rows.push({ kind: "run", label: run.id, run });
|
|
102
|
-
}
|
|
103
|
-
return rows;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function buildChainStepSpans(run: AsyncRunSummary): ChainStepSpan[] {
|
|
107
|
-
const total = run.chainStepCount ?? run.steps.length;
|
|
108
|
-
const groups = [...(run.parallelGroups ?? [])].sort((a, b) => a.stepIndex - b.stepIndex);
|
|
109
|
-
const spans: ChainStepSpan[] = [];
|
|
110
|
-
let flatIndex = 0;
|
|
111
|
-
for (let stepIndex = 0; stepIndex < total; stepIndex++) {
|
|
112
|
-
const group = groups.find((candidate) => candidate.stepIndex === stepIndex);
|
|
113
|
-
if (group) {
|
|
114
|
-
spans.push({ stepIndex, start: group.start, count: group.count, isParallel: true });
|
|
115
|
-
flatIndex = Math.max(flatIndex, group.start + group.count);
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
spans.push({ stepIndex, start: flatIndex, count: flatIndex < run.steps.length ? 1 : 0, isParallel: false });
|
|
119
|
-
flatIndex++;
|
|
120
|
-
}
|
|
121
|
-
return spans;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function aggregateStepStatus(steps: AsyncRunStep[]): string {
|
|
125
|
-
if (steps.some((step) => step.status === "running")) return "running";
|
|
126
|
-
if (steps.some((step) => step.status === "failed")) return "failed";
|
|
127
|
-
if (steps.some((step) => step.status === "paused")) return "paused";
|
|
128
|
-
if (steps.length > 0 && steps.every((step) => step.status === "complete" || step.status === "completed")) return "complete";
|
|
129
|
-
return "pending";
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function compactStepStats(step: AsyncRunStep): string {
|
|
133
|
-
const stats: string[] = [];
|
|
134
|
-
if (step.toolCount !== undefined) stats.push(`${step.toolCount} tools`);
|
|
135
|
-
if (step.tokens) stats.push(`${formatTokens(step.tokens.total)} tok`);
|
|
136
|
-
if (step.durationMs !== undefined) stats.push(formatDuration(step.durationMs));
|
|
137
|
-
return stats.join(" · ");
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function resolveRunPath(asyncDir: string, filePath: string): string {
|
|
141
|
-
return path.isAbsolute(filePath) ? filePath : path.join(asyncDir, filePath);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function readTailText(filePath: string): { text?: string; warning?: string } {
|
|
145
|
-
let fd: number | undefined;
|
|
146
|
-
try {
|
|
147
|
-
const stat = fs.statSync(filePath);
|
|
148
|
-
if (!stat.isFile()) return { warning: `not a file: ${filePath}` };
|
|
149
|
-
const start = Math.max(0, stat.size - DETAIL_FILE_TAIL_BYTES);
|
|
150
|
-
const length = stat.size - start;
|
|
151
|
-
const buffer = Buffer.alloc(length);
|
|
152
|
-
fd = fs.openSync(filePath, "r");
|
|
153
|
-
const bytesRead = fs.readSync(fd, buffer, 0, length, start);
|
|
154
|
-
return { text: buffer.subarray(0, bytesRead).toString("utf-8") };
|
|
155
|
-
} catch (error) {
|
|
156
|
-
const code = typeof error === "object" && error !== null && "code" in error
|
|
157
|
-
? (error as NodeJS.ErrnoException).code
|
|
158
|
-
: undefined;
|
|
159
|
-
return { warning: code === "ENOENT" ? `missing ${path.basename(filePath)}: ${filePath}` : `failed to read ${filePath}: ${error instanceof Error ? error.message : String(error)}` };
|
|
160
|
-
} finally {
|
|
161
|
-
if (fd !== undefined) {
|
|
162
|
-
try {
|
|
163
|
-
fs.closeSync(fd);
|
|
164
|
-
} catch {
|
|
165
|
-
// Best effort cleanup after a bounded detail-view read.
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function readTailLines(filePath: string, maxLines: number): { lines: string[]; warning?: string } {
|
|
172
|
-
const tail = readTailText(filePath);
|
|
173
|
-
if (tail.warning) return { lines: [], warning: tail.warning };
|
|
174
|
-
const lines = (tail.text ?? "").split("\n").map((line) => line.trimEnd()).filter((line) => line.trim());
|
|
175
|
-
return { lines: lines.slice(Math.max(0, lines.length - maxLines)) };
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function formatEventTimestamp(value: unknown): string | undefined {
|
|
179
|
-
if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
|
|
180
|
-
return new Date(value).toISOString();
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function formatEventLine(value: Record<string, unknown>): string {
|
|
184
|
-
const type = typeof value.type === "string" ? value.type : "event";
|
|
185
|
-
const ts = formatEventTimestamp(value.ts ?? value.observedAt);
|
|
186
|
-
const stepIndex = typeof value.stepIndex === "number" ? ` step ${value.stepIndex + 1}` : "";
|
|
187
|
-
const agent = typeof value.agent === "string"
|
|
188
|
-
? value.agent
|
|
189
|
-
: typeof value.subagentAgent === "string"
|
|
190
|
-
? value.subagentAgent
|
|
191
|
-
: undefined;
|
|
192
|
-
const status = typeof value.status === "string" ? value.status : undefined;
|
|
193
|
-
const exitCode = typeof value.exitCode === "number" ? `exit ${value.exitCode}` : undefined;
|
|
194
|
-
const event = value.event && typeof value.event === "object" && !Array.isArray(value.event)
|
|
195
|
-
? value.event as { message?: unknown }
|
|
196
|
-
: undefined;
|
|
197
|
-
const message = typeof value.message === "string"
|
|
198
|
-
? value.message
|
|
199
|
-
: typeof value.error === "string"
|
|
200
|
-
? value.error
|
|
201
|
-
: typeof event?.message === "string"
|
|
202
|
-
? event.message
|
|
203
|
-
: undefined;
|
|
204
|
-
return [ts, type, agent ? `${agent}${stepIndex}` : stepIndex.trim(), status, exitCode, message].filter(Boolean).join(" | ");
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function detailRows(content: string, width: number, innerW: number, theme: Theme): string[] {
|
|
208
|
-
const normalized = content.replace(/\t/g, " ");
|
|
209
|
-
const wrapped = wrapTextWithAnsi(normalized, innerW);
|
|
210
|
-
return (wrapped.length > 0 ? wrapped : [""]).map((line) => row(line, width, theme));
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function warningDetailRows(content: string, width: number, innerW: number, theme: Theme): string[] {
|
|
214
|
-
const normalized = content.replace(/\t/g, " ");
|
|
215
|
-
const wrapped = wrapTextWithAnsi(normalized, innerW);
|
|
216
|
-
return (wrapped.length > 0 ? wrapped : [""]).map((line) => row(theme.fg("warning", line), width, theme));
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function readRecentEvents(eventsPath: string, limit: number): { events: string[]; warning?: string } {
|
|
220
|
-
const tail = readTailText(eventsPath);
|
|
221
|
-
if (tail.warning) {
|
|
222
|
-
return tail.warning.startsWith("missing ") ? { events: [] } : { events: [], warning: tail.warning };
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
const events: string[] = [];
|
|
226
|
-
const lines = (tail.text ?? "").split("\n").filter((line) => line.trim());
|
|
227
|
-
for (let i = lines.length - 1; i >= 0 && events.length < limit; i--) {
|
|
228
|
-
try {
|
|
229
|
-
const parsed = JSON.parse(lines[i]!);
|
|
230
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) continue;
|
|
231
|
-
events.push(formatEventLine(parsed as Record<string, unknown>));
|
|
232
|
-
} catch {
|
|
233
|
-
// Skip malformed event records; async writers can be interrupted mid-line.
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
return { events: events.reverse() };
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
export class SubagentsStatusComponent implements Component {
|
|
240
|
-
private readonly width = 84;
|
|
241
|
-
private readonly viewportHeight = 12;
|
|
242
|
-
private readonly listRunsForOverlay: (asyncDirRoot: string, options?: AsyncRunOverlayOptions) => AsyncRunOverlayData;
|
|
243
|
-
private readonly sessionId: string;
|
|
244
|
-
private readonly refreshTimer: NodeJS.Timeout;
|
|
245
|
-
private screen: "list" | "detail" = "list";
|
|
246
|
-
private cursor = 0;
|
|
247
|
-
private scrollOffset = 0;
|
|
248
|
-
private detailScrollOffset = 0;
|
|
249
|
-
private detailRunId: string | undefined;
|
|
250
|
-
private active: AsyncRunSummary[] = [];
|
|
251
|
-
private recent: AsyncRunSummary[] = [];
|
|
252
|
-
private rows: StatusRow[] = [];
|
|
253
|
-
private errorMessage?: string;
|
|
254
|
-
private tui: TUI;
|
|
255
|
-
private theme: Theme;
|
|
256
|
-
private done: () => void;
|
|
257
|
-
|
|
258
|
-
constructor(
|
|
259
|
-
tui: TUI,
|
|
260
|
-
theme: Theme,
|
|
261
|
-
done: () => void,
|
|
262
|
-
deps: StatusOverlayDeps,
|
|
263
|
-
) {
|
|
264
|
-
this.tui = tui;
|
|
265
|
-
this.theme = theme;
|
|
266
|
-
this.done = done;
|
|
267
|
-
this.listRunsForOverlay = deps.listRunsForOverlay ?? listAsyncRunsForOverlay;
|
|
268
|
-
this.sessionId = deps.sessionId;
|
|
269
|
-
const refreshMs = deps.refreshMs ?? AUTO_REFRESH_MS;
|
|
270
|
-
this.reload();
|
|
271
|
-
this.refreshTimer = setInterval(() => {
|
|
272
|
-
this.reload();
|
|
273
|
-
this.tui.requestRender();
|
|
274
|
-
}, refreshMs);
|
|
275
|
-
this.refreshTimer.unref?.();
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
private reload(): void {
|
|
279
|
-
const previousSelectedId = selectedRun(this.rows, this.cursor)?.id;
|
|
280
|
-
try {
|
|
281
|
-
const overlayData = this.listRunsForOverlay(ASYNC_DIR, { recentLimit: 5, sessionId: this.sessionId });
|
|
282
|
-
this.active = overlayData.active;
|
|
283
|
-
this.recent = overlayData.recent;
|
|
284
|
-
this.rows = buildRows(this.active, this.recent);
|
|
285
|
-
this.errorMessage = undefined;
|
|
286
|
-
this.restoreSelection(previousSelectedId);
|
|
287
|
-
this.ensureScrollVisible();
|
|
288
|
-
} catch (error) {
|
|
289
|
-
this.active = [];
|
|
290
|
-
this.recent = [];
|
|
291
|
-
this.rows = [];
|
|
292
|
-
this.cursor = 0;
|
|
293
|
-
this.scrollOffset = 0;
|
|
294
|
-
this.errorMessage = error instanceof Error ? error.message : String(error);
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
private restoreSelection(previousSelectedId?: string): void {
|
|
299
|
-
const runRows = this.rows.filter((row) => row.kind === "run");
|
|
300
|
-
if (runRows.length === 0) {
|
|
301
|
-
this.cursor = 0;
|
|
302
|
-
return;
|
|
303
|
-
}
|
|
304
|
-
if (!previousSelectedId) {
|
|
305
|
-
this.cursor = Math.min(this.cursor, runRows.length - 1);
|
|
306
|
-
return;
|
|
307
|
-
}
|
|
308
|
-
const nextIndex = runRows.findIndex((row) => row.run?.id === previousSelectedId);
|
|
309
|
-
if (nextIndex !== -1) {
|
|
310
|
-
this.cursor = nextIndex;
|
|
311
|
-
return;
|
|
312
|
-
}
|
|
313
|
-
this.cursor = Math.min(this.cursor, runRows.length - 1);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
private ensureScrollVisible(): void {
|
|
317
|
-
if (this.rows.length <= this.viewportHeight) {
|
|
318
|
-
this.scrollOffset = 0;
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
321
|
-
const selected = selectedRun(this.rows, this.cursor);
|
|
322
|
-
if (!selected) {
|
|
323
|
-
this.scrollOffset = 0;
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
const rowIndex = this.rows.findIndex((row) => row.kind === "run" && row.run?.id === selected.id);
|
|
327
|
-
if (rowIndex === -1) return;
|
|
328
|
-
if (rowIndex < this.scrollOffset) this.scrollOffset = rowIndex;
|
|
329
|
-
if (rowIndex >= this.scrollOffset + this.viewportHeight) {
|
|
330
|
-
this.scrollOffset = rowIndex - this.viewportHeight + 1;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
private renderRunDetails(run: AsyncRunSummary, width: number, innerW: number): string[] {
|
|
335
|
-
const lines = [
|
|
336
|
-
row(`cwd: ${truncateToWidth(shortenPath(run.cwd ?? run.asyncDir), innerW - 5)}`, width, this.theme),
|
|
337
|
-
];
|
|
338
|
-
if (run.outputFile) {
|
|
339
|
-
lines.push(row(`output: ${truncateToWidth(shortenPath(run.outputFile), innerW - 8)}`, width, this.theme));
|
|
340
|
-
}
|
|
341
|
-
if (run.sessionFile) {
|
|
342
|
-
lines.push(row(`session: ${truncateToWidth(shortenPath(run.sessionFile), innerW - 9)}`, width, this.theme));
|
|
343
|
-
}
|
|
344
|
-
if (run.mode === "chain" && (run.chainStepCount !== undefined || run.parallelGroups?.length)) {
|
|
345
|
-
lines.push(...this.renderChainProgressRows(run, width, innerW));
|
|
346
|
-
} else if (run.mode === "parallel") {
|
|
347
|
-
lines.push(...this.renderAgentRows(run, width, innerW));
|
|
348
|
-
} else {
|
|
349
|
-
lines.push(...this.renderStepRows(run, width, innerW));
|
|
350
|
-
}
|
|
351
|
-
return lines;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
private formatStepActivity(step: AsyncRunStep): string {
|
|
355
|
-
if (!step.lastActivityAt) return "";
|
|
356
|
-
if (step.activityState === "needs_attention") return `no activity for ${formatDuration(Math.max(0, Date.now() - step.lastActivityAt))}`;
|
|
357
|
-
if (step.activityState === "active_long_running") return `active but long-running; last activity ${formatDuration(Math.max(0, Date.now() - step.lastActivityAt))} ago`;
|
|
358
|
-
return `active ${formatDuration(Math.max(0, Date.now() - step.lastActivityAt))} ago`;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
private renderStepRows(run: AsyncRunSummary, width: number, innerW: number, options: { wrap?: boolean } = {}): string[] {
|
|
362
|
-
const lines: string[] = [];
|
|
363
|
-
for (const step of run.steps) {
|
|
364
|
-
const model = step.model ? ` | ${step.model}` : "";
|
|
365
|
-
const attempts = step.attemptedModels && step.attemptedModels.length > 1
|
|
366
|
-
? ` | attempts ${step.attemptedModels.length}`
|
|
367
|
-
: "";
|
|
368
|
-
const duration = step.durationMs !== undefined ? ` | ${formatDuration(step.durationMs)}` : "";
|
|
369
|
-
const tokens = step.tokens ? ` | ${formatTokens(step.tokens.total)} tok` : "";
|
|
370
|
-
const activity = this.formatStepActivity(step);
|
|
371
|
-
const line = ` ${step.index + 1}. ${step.agent} | ${stepStatusColor(this.theme, step.status)}${activity ? ` | ${activity}` : ""}${model}${attempts}${duration}${tokens}`;
|
|
372
|
-
if (options.wrap) {
|
|
373
|
-
lines.push(...detailRows(line, width, innerW, this.theme));
|
|
374
|
-
} else {
|
|
375
|
-
lines.push(row(truncateToWidth(line, innerW), width, this.theme));
|
|
376
|
-
}
|
|
377
|
-
if (step.error) {
|
|
378
|
-
if (options.wrap) {
|
|
379
|
-
lines.push(...detailRows(` ${step.error}`, width, innerW, this.theme));
|
|
380
|
-
} else {
|
|
381
|
-
lines.push(row(truncateToWidth(` ${step.error}`, innerW), width, this.theme));
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
if (run.steps.length === 0) {
|
|
386
|
-
lines.push(row(this.theme.fg("dim", " No step details available yet."), width, this.theme));
|
|
387
|
-
}
|
|
388
|
-
return lines;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
private renderStructuredStepRow(prefix: string, step: AsyncRunStep, width: number, innerW: number, errorIndent: string): string[] {
|
|
392
|
-
const suffix = [this.formatStepActivity(step), step.model, compactStepStats(step)].filter(Boolean).join(" · ");
|
|
393
|
-
const lines = detailRows(`${prefix}${step.agent} · ${stepStatusColor(this.theme, step.status)}${suffix ? ` · ${suffix}` : ""}`, width, innerW, this.theme);
|
|
394
|
-
if (step.error) lines.push(...detailRows(`${errorIndent}${step.error}`, width, innerW, this.theme));
|
|
395
|
-
return lines;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
private renderAgentRows(run: AsyncRunSummary, width: number, innerW: number): string[] {
|
|
399
|
-
if (run.steps.length === 0) return [row(this.theme.fg("dim", " No agent details available yet."), width, this.theme)];
|
|
400
|
-
const lines: string[] = [];
|
|
401
|
-
const total = run.steps.length;
|
|
402
|
-
for (const [index, step] of run.steps.entries()) {
|
|
403
|
-
lines.push(...this.renderStructuredStepRow(` ${stepGlyph(this.theme, step.status)} Agent ${index + 1}/${total}: `, step, width, innerW, " "));
|
|
404
|
-
}
|
|
405
|
-
return lines;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
private renderChainProgressRows(run: AsyncRunSummary, width: number, innerW: number): string[] {
|
|
409
|
-
if (run.steps.length === 0) return [row(this.theme.fg("dim", " No step details available yet."), width, this.theme)];
|
|
410
|
-
const lines: string[] = [];
|
|
411
|
-
const spans = buildChainStepSpans(run);
|
|
412
|
-
const total = run.chainStepCount ?? spans.length;
|
|
413
|
-
for (const span of spans) {
|
|
414
|
-
const steps = run.steps.slice(span.start, span.start + span.count);
|
|
415
|
-
const status = aggregateStepStatus(steps);
|
|
416
|
-
if (span.isParallel) {
|
|
417
|
-
const running = steps.filter((step) => step.status === "running").length;
|
|
418
|
-
const done = steps.filter((step) => step.status === "complete" || step.status === "completed").length;
|
|
419
|
-
const failed = steps.filter((step) => step.status === "failed").length;
|
|
420
|
-
const paused = steps.filter((step) => step.status === "paused").length;
|
|
421
|
-
const outcomeCounts = [`${done}/${span.count} done`];
|
|
422
|
-
if (failed > 0) outcomeCounts.push(`${failed} failed`);
|
|
423
|
-
if (paused > 0) outcomeCounts.push(`${paused} paused`);
|
|
424
|
-
if (running > 0) outcomeCounts.unshift(running === 1 ? "1 agent running" : `${running} agents running`);
|
|
425
|
-
const label = `${stepGlyph(this.theme, status)} Step ${span.stepIndex + 1}/${total}: parallel group · ${outcomeCounts.join(" · ")}`;
|
|
426
|
-
lines.push(...detailRows(` ${label}`, width, innerW, this.theme));
|
|
427
|
-
for (const [localIndex, step] of steps.entries()) {
|
|
428
|
-
lines.push(...this.renderStructuredStepRow(` ${stepGlyph(this.theme, step.status)} Agent ${localIndex + 1}/${span.count}: `, step, width, innerW, " "));
|
|
429
|
-
}
|
|
430
|
-
continue;
|
|
431
|
-
}
|
|
432
|
-
const step = steps[0];
|
|
433
|
-
if (!step) {
|
|
434
|
-
lines.push(row(this.theme.fg("dim", ` ◦ Step ${span.stepIndex + 1}/${total}: pending`), width, this.theme));
|
|
435
|
-
continue;
|
|
436
|
-
}
|
|
437
|
-
lines.push(...this.renderStructuredStepRow(` ${stepGlyph(this.theme, step.status)} Step ${span.stepIndex + 1}/${total}: `, step, width, innerW, " "));
|
|
438
|
-
}
|
|
439
|
-
return lines;
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
private renderDetail(run: AsyncRunSummary, width: number, innerW: number): string[] {
|
|
443
|
-
const stepLabel = formatAsyncRunProgressLabel(run);
|
|
444
|
-
const duration = run.endedAt !== undefined
|
|
445
|
-
? formatDuration(Math.max(0, run.endedAt - run.startedAt))
|
|
446
|
-
: formatDuration(Math.max(0, Date.now() - run.startedAt));
|
|
447
|
-
const activity = run.lastActivityAt
|
|
448
|
-
? run.activityState === "needs_attention"
|
|
449
|
-
? `no activity for ${formatDuration(Math.max(0, Date.now() - run.lastActivityAt))}`
|
|
450
|
-
: run.activityState === "active_long_running"
|
|
451
|
-
? `active but long-running; last activity ${formatDuration(Math.max(0, Date.now() - run.lastActivityAt))} ago`
|
|
452
|
-
: `active ${formatDuration(Math.max(0, Date.now() - run.lastActivityAt))} ago`
|
|
453
|
-
: undefined;
|
|
454
|
-
|
|
455
|
-
const body: string[] = [];
|
|
456
|
-
body.push(...detailRows(`${runGlyph(this.theme, run.state)} ${run.mode} · ${stepLabel} · ${statusColor(this.theme, run.state)} · ${duration}`, width, innerW, this.theme));
|
|
457
|
-
if (activity) body.push(...detailRows(activity, width, innerW, this.theme));
|
|
458
|
-
body.push(row("", width, this.theme));
|
|
459
|
-
if (run.mode === "chain" && (run.chainStepCount !== undefined || run.parallelGroups?.length)) {
|
|
460
|
-
body.push(row(this.theme.fg("accent", run.state === "running" ? "Chain progress" : "Chain results"), width, this.theme));
|
|
461
|
-
body.push(...this.renderChainProgressRows(run, width, innerW));
|
|
462
|
-
} else if (run.mode === "parallel") {
|
|
463
|
-
body.push(row(this.theme.fg("accent", "Agents"), width, this.theme));
|
|
464
|
-
body.push(...this.renderAgentRows(run, width, innerW));
|
|
465
|
-
} else {
|
|
466
|
-
body.push(row(this.theme.fg("accent", "Steps"), width, this.theme));
|
|
467
|
-
body.push(...this.renderStepRows(run, width, innerW, { wrap: true }));
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
const eventsPath = path.join(run.asyncDir, "events.jsonl");
|
|
471
|
-
const eventResult = readRecentEvents(eventsPath, DETAIL_EVENT_LIMIT);
|
|
472
|
-
body.push(row("", width, this.theme));
|
|
473
|
-
body.push(row(this.theme.fg("accent", "Recent events"), width, this.theme));
|
|
474
|
-
if (eventResult.warning) body.push(...warningDetailRows(eventResult.warning, width, innerW, this.theme));
|
|
475
|
-
if (eventResult.events.length === 0 && !eventResult.warning) body.push(row(this.theme.fg("dim", " No events recorded."), width, this.theme));
|
|
476
|
-
for (const event of eventResult.events) {
|
|
477
|
-
body.push(...detailRows(` ${event}`, width, innerW, this.theme));
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
body.push(row("", width, this.theme));
|
|
481
|
-
body.push(row(this.theme.fg("accent", "Output tail"), width, this.theme));
|
|
482
|
-
if (run.outputFile) {
|
|
483
|
-
const outputPath = resolveRunPath(run.asyncDir, run.outputFile);
|
|
484
|
-
const tail = readTailLines(outputPath, OUTPUT_TAIL_LINES);
|
|
485
|
-
if (tail.warning) body.push(...warningDetailRows(tail.warning, width, innerW, this.theme));
|
|
486
|
-
else if (tail.lines.length === 0) body.push(row(this.theme.fg("dim", " No output yet."), width, this.theme));
|
|
487
|
-
for (const line of tail.lines) body.push(...detailRows(` ${line}`, width, innerW, this.theme));
|
|
488
|
-
} else {
|
|
489
|
-
body.push(row(this.theme.fg("dim", " No output file recorded."), width, this.theme));
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
body.push(row("", width, this.theme));
|
|
493
|
-
body.push(row(this.theme.fg("accent", "Paths"), width, this.theme));
|
|
494
|
-
body.push(row(truncateToWidth(` cwd: ${shortenPath(run.cwd ?? run.asyncDir)}`, innerW), width, this.theme));
|
|
495
|
-
body.push(row(truncateToWidth(` asyncDir: ${shortenPath(run.asyncDir)}`, innerW), width, this.theme));
|
|
496
|
-
if (run.outputFile) body.push(row(truncateToWidth(` outputFile: ${shortenPath(resolveRunPath(run.asyncDir, run.outputFile))}`, innerW), width, this.theme));
|
|
497
|
-
if (run.sessionFile) body.push(row(truncateToWidth(` sessionFile: ${shortenPath(run.sessionFile)}`, innerW), width, this.theme));
|
|
498
|
-
if (run.sessionDir) body.push(row(truncateToWidth(` sessionDir: ${shortenPath(run.sessionDir)}`, innerW), width, this.theme));
|
|
499
|
-
const logPath = path.join(run.asyncDir, `subagent-log-${run.id}.md`);
|
|
500
|
-
if (fs.existsSync(logPath)) body.push(row(truncateToWidth(` runLog: ${shortenPath(logPath)}`, innerW), width, this.theme));
|
|
501
|
-
|
|
502
|
-
const maxOffset = Math.max(0, body.length - DETAIL_VIEWPORT_HEIGHT);
|
|
503
|
-
this.detailScrollOffset = Math.min(this.detailScrollOffset, maxOffset);
|
|
504
|
-
const visibleBody = body.slice(this.detailScrollOffset, this.detailScrollOffset + DETAIL_VIEWPORT_HEIGHT);
|
|
505
|
-
const above = this.detailScrollOffset;
|
|
506
|
-
const below = Math.max(0, body.length - (this.detailScrollOffset + visibleBody.length));
|
|
507
|
-
const scrollInfo = formatScrollInfo(above, below);
|
|
508
|
-
return [
|
|
509
|
-
renderHeader(`Subagent Run ${run.id.slice(0, 8)}`, width, this.theme),
|
|
510
|
-
...visibleBody,
|
|
511
|
-
scrollInfo ? row(this.theme.fg("dim", scrollInfo), width, this.theme) : row("", width, this.theme),
|
|
512
|
-
renderFooter(" ↑↓ scroll esc summary q close read-only detail ", width, this.theme),
|
|
513
|
-
];
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
handleInput(data: string): void {
|
|
517
|
-
if (this.screen === "detail" && matchesKey(data, "escape")) {
|
|
518
|
-
this.screen = "list";
|
|
519
|
-
this.detailRunId = undefined;
|
|
520
|
-
this.tui.requestRender();
|
|
521
|
-
return;
|
|
522
|
-
}
|
|
523
|
-
if (matchesKey(data, "escape") || matchesKey(data, "q") || matchesKey(data, "ctrl+c")) {
|
|
524
|
-
this.done();
|
|
525
|
-
return;
|
|
526
|
-
}
|
|
527
|
-
if (this.screen === "detail") {
|
|
528
|
-
if (matchesKey(data, "up")) {
|
|
529
|
-
this.detailScrollOffset = Math.max(0, this.detailScrollOffset - 1);
|
|
530
|
-
this.tui.requestRender();
|
|
531
|
-
return;
|
|
532
|
-
}
|
|
533
|
-
if (matchesKey(data, "down")) {
|
|
534
|
-
this.detailScrollOffset++;
|
|
535
|
-
this.tui.requestRender();
|
|
536
|
-
return;
|
|
537
|
-
}
|
|
538
|
-
if (matchesKey(data, "pageup")) {
|
|
539
|
-
this.detailScrollOffset = Math.max(0, this.detailScrollOffset - DETAIL_VIEWPORT_HEIGHT);
|
|
540
|
-
this.tui.requestRender();
|
|
541
|
-
return;
|
|
542
|
-
}
|
|
543
|
-
if (matchesKey(data, "pagedown")) {
|
|
544
|
-
this.detailScrollOffset += DETAIL_VIEWPORT_HEIGHT;
|
|
545
|
-
this.tui.requestRender();
|
|
546
|
-
}
|
|
547
|
-
return;
|
|
548
|
-
}
|
|
549
|
-
if (matchesKey(data, "return")) {
|
|
550
|
-
const selected = selectedRun(this.rows, this.cursor);
|
|
551
|
-
if (selected) {
|
|
552
|
-
this.screen = "detail";
|
|
553
|
-
this.detailRunId = selected.id;
|
|
554
|
-
this.detailScrollOffset = 0;
|
|
555
|
-
this.tui.requestRender();
|
|
556
|
-
}
|
|
557
|
-
return;
|
|
558
|
-
}
|
|
559
|
-
if (matchesKey(data, "up")) {
|
|
560
|
-
this.cursor = Math.max(0, this.cursor - 1);
|
|
561
|
-
this.ensureScrollVisible();
|
|
562
|
-
this.tui.requestRender();
|
|
563
|
-
return;
|
|
564
|
-
}
|
|
565
|
-
if (matchesKey(data, "down")) {
|
|
566
|
-
const maxCursor = Math.max(0, this.rows.filter((row) => row.kind === "run").length - 1);
|
|
567
|
-
this.cursor = Math.min(maxCursor, this.cursor + 1);
|
|
568
|
-
this.ensureScrollVisible();
|
|
569
|
-
this.tui.requestRender();
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
render(width: number): string[] {
|
|
574
|
-
const w = Math.min(width, this.width);
|
|
575
|
-
const innerW = w - 2;
|
|
576
|
-
const selected = selectedRun(this.rows, this.cursor);
|
|
577
|
-
if (this.screen === "detail") {
|
|
578
|
-
const detailRun = this.rows.find((row) => row.kind === "run" && row.run?.id === this.detailRunId)?.run;
|
|
579
|
-
if (detailRun) return this.renderDetail(detailRun, w, innerW);
|
|
580
|
-
return [
|
|
581
|
-
renderHeader("Subagent Run", w, this.theme),
|
|
582
|
-
row(this.theme.fg("warning", "Selected run is no longer available."), w, this.theme),
|
|
583
|
-
renderFooter(" esc summary q close ", w, this.theme),
|
|
584
|
-
];
|
|
585
|
-
}
|
|
586
|
-
const lines: string[] = [renderHeader("Subagents Status", w, this.theme)];
|
|
587
|
-
const rows = this.rows.length > 0 ? this.rows : [{ kind: "section" as const, label: "No async runs found" }];
|
|
588
|
-
const visibleRows = rows.slice(this.scrollOffset, this.scrollOffset + this.viewportHeight);
|
|
589
|
-
for (const statusRow of visibleRows) {
|
|
590
|
-
if (statusRow.kind === "section") {
|
|
591
|
-
lines.push(row(this.theme.fg("accent", statusRow.label), w, this.theme));
|
|
592
|
-
continue;
|
|
593
|
-
}
|
|
594
|
-
const isSelected = selected?.id === statusRow.run?.id;
|
|
595
|
-
lines.push(row(truncateToWidth(runLabel(this.theme, statusRow.run!, isSelected), innerW), w, this.theme));
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
const above = this.scrollOffset;
|
|
599
|
-
const below = Math.max(0, rows.length - (this.scrollOffset + visibleRows.length));
|
|
600
|
-
const scrollInfo = formatScrollInfo(above, below);
|
|
601
|
-
if (scrollInfo) lines.push(row(this.theme.fg("dim", scrollInfo), w, this.theme));
|
|
602
|
-
else lines.push(row("", w, this.theme));
|
|
603
|
-
|
|
604
|
-
if (this.errorMessage) {
|
|
605
|
-
lines.push(row(this.theme.fg("error", truncateToWidth(this.errorMessage, innerW)), w, this.theme));
|
|
606
|
-
} else if (selected) {
|
|
607
|
-
lines.push(row(this.theme.fg("accent", `Selected: ${selected.id}`), w, this.theme));
|
|
608
|
-
lines.push(...this.renderRunDetails(selected, w, innerW));
|
|
609
|
-
} else {
|
|
610
|
-
lines.push(row(this.theme.fg("dim", "No runs selected."), w, this.theme));
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
const footer = `↑↓ select enter detail esc close summary view ${this.active.length} active / ${this.recent.length} recent`;
|
|
614
|
-
lines.push(renderFooter(truncateToWidth(footer, innerW), w, this.theme));
|
|
615
|
-
return lines;
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
dispose(): void {
|
|
619
|
-
clearInterval(this.refreshTimer);
|
|
620
|
-
}
|
|
621
|
-
}
|