@leing2021/super-pi 0.22.0 → 0.23.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.
Files changed (101) hide show
  1. package/README.md +38 -209
  2. package/extensions/ce-core/index.ts +190 -7
  3. package/extensions/ce-core/tools/parallel-subagent.ts +3 -3
  4. package/extensions/ce-core/tools/subagent.ts +2 -2
  5. package/package.json +4 -18
  6. package/rules/common/naming.md +154 -0
  7. package/skills/02-plan/SKILL.md +3 -2
  8. package/skills/03-work/SKILL.md +9 -6
  9. package/skills/04-review/SKILL.md +3 -2
  10. package/skills/06-next/SKILL.md +38 -10
  11. package/skills/06-next/references/recommendation-logic.md +2 -2
  12. package/skills/07-worktree/SKILL.md +9 -8
  13. package/skills/{09-help → 08-help}/SKILL.md +3 -3
  14. package/skills/{09-help → 08-help}/references/workflow-sequence.md +2 -1
  15. package/skills/references/language-detection.md +44 -0
  16. package/extensions/subagent/agent-management.ts +0 -595
  17. package/extensions/subagent/agent-manager-chain-detail.ts +0 -162
  18. package/extensions/subagent/agent-manager-detail.ts +0 -231
  19. package/extensions/subagent/agent-manager-edit.ts +0 -390
  20. package/extensions/subagent/agent-manager-list.ts +0 -278
  21. package/extensions/subagent/agent-manager-parallel.ts +0 -304
  22. package/extensions/subagent/agent-manager.ts +0 -705
  23. package/extensions/subagent/agent-scope.ts +0 -8
  24. package/extensions/subagent/agent-selection.ts +0 -25
  25. package/extensions/subagent/agent-serializer.ts +0 -123
  26. package/extensions/subagent/agent-templates.ts +0 -62
  27. package/extensions/subagent/agents/context-builder.md +0 -37
  28. package/extensions/subagent/agents/delegate.md +0 -9
  29. package/extensions/subagent/agents/oracle.md +0 -73
  30. package/extensions/subagent/agents/planner.md +0 -52
  31. package/extensions/subagent/agents/researcher.md +0 -50
  32. package/extensions/subagent/agents/reviewer.md +0 -38
  33. package/extensions/subagent/agents/scout.md +0 -48
  34. package/extensions/subagent/agents/worker.md +0 -52
  35. package/extensions/subagent/agents.ts +0 -761
  36. package/extensions/subagent/artifacts.ts +0 -100
  37. package/extensions/subagent/async-execution.ts +0 -520
  38. package/extensions/subagent/async-job-tracker.ts +0 -216
  39. package/extensions/subagent/async-status.ts +0 -241
  40. package/extensions/subagent/chain-clarify.ts +0 -1364
  41. package/extensions/subagent/chain-execution.ts +0 -853
  42. package/extensions/subagent/chain-serializer.ts +0 -126
  43. package/extensions/subagent/completion-dedupe.ts +0 -65
  44. package/extensions/subagent/doctor.ts +0 -200
  45. package/extensions/subagent/execution.ts +0 -738
  46. package/extensions/subagent/file-coalescer.ts +0 -42
  47. package/extensions/subagent/fork-context.ts +0 -63
  48. package/extensions/subagent/formatters.ts +0 -122
  49. package/extensions/subagent/frontmatter.ts +0 -31
  50. package/extensions/subagent/index.ts +0 -585
  51. package/extensions/subagent/intercom-bridge.ts +0 -240
  52. package/extensions/subagent/jsonl-writer.ts +0 -83
  53. package/extensions/subagent/model-fallback.ts +0 -108
  54. package/extensions/subagent/notify.ts +0 -110
  55. package/extensions/subagent/parallel-utils.ts +0 -108
  56. package/extensions/subagent/pi-args.ts +0 -138
  57. package/extensions/subagent/pi-spawn.ts +0 -100
  58. package/extensions/subagent/post-exit-stdio-guard.ts +0 -87
  59. package/extensions/subagent/prompt-template-bridge.ts +0 -399
  60. package/extensions/subagent/prompts/gather-context-and-clarify.md +0 -13
  61. package/extensions/subagent/prompts/parallel-cleanup.md +0 -42
  62. package/extensions/subagent/prompts/parallel-research.md +0 -50
  63. package/extensions/subagent/prompts/parallel-review.md +0 -40
  64. package/extensions/subagent/render-helpers.ts +0 -82
  65. package/extensions/subagent/render.ts +0 -836
  66. package/extensions/subagent/result-intercom.ts +0 -237
  67. package/extensions/subagent/result-watcher.ts +0 -171
  68. package/extensions/subagent/run-history.ts +0 -57
  69. package/extensions/subagent/run-status.ts +0 -136
  70. package/extensions/subagent/schemas.ts +0 -164
  71. package/extensions/subagent/session-tokens.ts +0 -50
  72. package/extensions/subagent/settings.ts +0 -367
  73. package/extensions/subagent/single-output.ts +0 -97
  74. package/extensions/subagent/skills.ts +0 -626
  75. package/extensions/subagent/slash-bridge.ts +0 -176
  76. package/extensions/subagent/slash-commands.ts +0 -303
  77. package/extensions/subagent/slash-live-state.ts +0 -294
  78. package/extensions/subagent/subagent-control.ts +0 -150
  79. package/extensions/subagent/subagent-executor.ts +0 -1899
  80. package/extensions/subagent/subagent-prompt-runtime.ts +0 -75
  81. package/extensions/subagent/subagent-runner.ts +0 -1470
  82. package/extensions/subagent/subagents-status.ts +0 -472
  83. package/extensions/subagent/text-editor.ts +0 -272
  84. package/extensions/subagent/top-level-async.ts +0 -15
  85. package/extensions/subagent/types.ts +0 -623
  86. package/extensions/subagent/utils.ts +0 -456
  87. package/extensions/subagent/worktree.ts +0 -579
  88. package/extensions/super-pi-extension/agents/ce-oracle.md +0 -109
  89. package/extensions/super-pi-extension/agents/ce-planner.md +0 -116
  90. package/extensions/super-pi-extension/agents/ce-reviewer.md +0 -99
  91. package/extensions/super-pi-extension/agents/ce-scout.md +0 -80
  92. package/extensions/super-pi-extension/agents/ce-worker.md +0 -78
  93. package/extensions/super-pi-extension/chains/ce-parallel-review.chain.md +0 -34
  94. package/extensions/super-pi-extension/chains/ce-review-only.chain.md +0 -12
  95. package/extensions/super-pi-extension/chains/ce-standard.chain.md +0 -28
  96. package/extensions/super-pi-extension/index.ts +0 -129
  97. package/extensions/super-pi-extension/model-sync.ts +0 -204
  98. package/skills/08-status/SKILL.md +0 -41
  99. package/skills/08-status/references/artifact-locations.md +0 -10
  100. package/skills/10-rules/SKILL.md +0 -76
  101. package/skills/pi-subagents/SKILL.md +0 -566
@@ -1,472 +0,0 @@
1
- // Based on pi-subagents by Nico Bailon (https://github.com/nicobailon/pi-subagents)
2
- // MIT License
3
- import * as fs from "node:fs";
4
- import * as path from "node:path";
5
- import type { Theme } from "@mariozechner/pi-coding-agent";
6
- import type { Component, TUI } from "@mariozechner/pi-tui";
7
- import { matchesKey, truncateToWidth } from "@mariozechner/pi-tui";
8
- import { type AsyncRunOverlayData, type AsyncRunSummary, listAsyncRunsForOverlay } from "./async-status.ts";
9
- import { ASYNC_DIR } from "./types.ts";
10
- import { formatDuration, formatTokens, shortenPath } from "./formatters.ts";
11
- import { formatScrollInfo, renderFooter, renderHeader, row } from "./render-helpers.ts";
12
-
13
- const AUTO_REFRESH_MS = 2000;
14
- const DETAIL_EVENT_LIMIT = 8;
15
- const OUTPUT_TAIL_LINES = 20;
16
- const DETAIL_FILE_TAIL_BYTES = 64 * 1024;
17
- const DETAIL_VIEWPORT_HEIGHT = 18;
18
-
19
- interface StatusRow {
20
- kind: "section" | "run";
21
- label: string;
22
- run?: AsyncRunSummary;
23
- }
24
-
25
- interface StatusOverlayDeps {
26
- listRunsForOverlay?: (asyncDirRoot: string, recentLimit?: number) => AsyncRunOverlayData;
27
- refreshMs?: number;
28
- }
29
-
30
- function statusColor(theme: Theme, status: AsyncRunSummary["state"]): string {
31
- switch (status) {
32
- case "running": return theme.fg("warning", status);
33
- case "queued": return theme.fg("accent", status);
34
- case "complete": return theme.fg("success", status);
35
- case "failed": return theme.fg("error", status);
36
- case "paused": return theme.fg("warning", status);
37
- }
38
- }
39
-
40
- function stepStatusColor(theme: Theme, status: string): string {
41
- if (status === "running") return theme.fg("warning", status);
42
- if (status === "pending") return theme.fg("dim", status);
43
- if (status === "complete" || status === "completed") return theme.fg("success", status);
44
- if (status === "failed") return theme.fg("error", status);
45
- if (status === "paused") return theme.fg("warning", status);
46
- return status;
47
- }
48
-
49
- function runLabel(theme: Theme, run: AsyncRunSummary, selected: boolean): string {
50
- const prefix = selected ? theme.fg("accent", ">") : " ";
51
- const stepCount = run.steps.length || 1;
52
- const stepLabel = run.currentStep !== undefined ? `step ${run.currentStep + 1}/${stepCount}` : `steps ${stepCount}`;
53
- const cwd = shortenPath(run.cwd ?? run.asyncDir);
54
- return `${prefix} ${run.id.slice(0, 8)} ${statusColor(theme, run.state)} | ${run.mode} | ${stepLabel} | ${cwd}`;
55
- }
56
-
57
- function selectedIndex(rows: StatusRow[], cursor: number): number {
58
- const runRows = rows.filter((row) => row.kind === "run");
59
- if (runRows.length === 0) return -1;
60
- return Math.max(0, Math.min(cursor, runRows.length - 1));
61
- }
62
-
63
- function selectedRun(rows: StatusRow[], cursor: number): AsyncRunSummary | undefined {
64
- const runRows = rows.filter((row) => row.kind === "run");
65
- const index = selectedIndex(rows, cursor);
66
- return index >= 0 ? runRows[index]?.run : undefined;
67
- }
68
-
69
- function buildRows(active: AsyncRunSummary[], recent: AsyncRunSummary[]): StatusRow[] {
70
- const rows: StatusRow[] = [];
71
- if (active.length > 0) {
72
- rows.push({ kind: "section", label: "Active" });
73
- for (const run of active) rows.push({ kind: "run", label: run.id, run });
74
- }
75
- if (recent.length > 0) {
76
- rows.push({ kind: "section", label: "Recent" });
77
- for (const run of recent) rows.push({ kind: "run", label: run.id, run });
78
- }
79
- return rows;
80
- }
81
-
82
- function resolveRunPath(asyncDir: string, filePath: string): string {
83
- return path.isAbsolute(filePath) ? filePath : path.join(asyncDir, filePath);
84
- }
85
-
86
- function readTailText(filePath: string): { text?: string; warning?: string } {
87
- let fd: number | undefined;
88
- try {
89
- const stat = fs.statSync(filePath);
90
- if (!stat.isFile()) return { warning: `not a file: ${filePath}` };
91
- const start = Math.max(0, stat.size - DETAIL_FILE_TAIL_BYTES);
92
- const length = stat.size - start;
93
- const buffer = Buffer.alloc(length);
94
- fd = fs.openSync(filePath, "r");
95
- const bytesRead = fs.readSync(fd, buffer, 0, length, start);
96
- return { text: buffer.subarray(0, bytesRead).toString("utf-8") };
97
- } catch (error) {
98
- const code = typeof error === "object" && error !== null && "code" in error
99
- ? (error as NodeJS.ErrnoException).code
100
- : undefined;
101
- return { warning: code === "ENOENT" ? `missing ${path.basename(filePath)}: ${filePath}` : `failed to read ${filePath}: ${error instanceof Error ? error.message : String(error)}` };
102
- } finally {
103
- if (fd !== undefined) {
104
- try {
105
- fs.closeSync(fd);
106
- } catch {
107
- // Best effort cleanup after a bounded detail-view read.
108
- }
109
- }
110
- }
111
- }
112
-
113
- function readTailLines(filePath: string, maxLines: number): { lines: string[]; warning?: string } {
114
- const tail = readTailText(filePath);
115
- if (tail.warning) return { lines: [], warning: tail.warning };
116
- const lines = (tail.text ?? "").split("\n").map((line) => line.trimEnd()).filter((line) => line.trim());
117
- return { lines: lines.slice(Math.max(0, lines.length - maxLines)) };
118
- }
119
-
120
- function formatEventTimestamp(value: unknown): string | undefined {
121
- if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
122
- return new Date(value).toISOString();
123
- }
124
-
125
- function formatEventLine(value: Record<string, unknown>): string {
126
- const type = typeof value.type === "string" ? value.type : "event";
127
- const ts = formatEventTimestamp(value.ts ?? value.observedAt);
128
- const stepIndex = typeof value.stepIndex === "number" ? ` step ${value.stepIndex + 1}` : "";
129
- const agent = typeof value.agent === "string"
130
- ? value.agent
131
- : typeof value.subagentAgent === "string"
132
- ? value.subagentAgent
133
- : undefined;
134
- const status = typeof value.status === "string" ? value.status : undefined;
135
- const exitCode = typeof value.exitCode === "number" ? `exit ${value.exitCode}` : undefined;
136
- const event = value.event && typeof value.event === "object" && !Array.isArray(value.event)
137
- ? value.event as { message?: unknown }
138
- : undefined;
139
- const message = typeof value.message === "string"
140
- ? value.message
141
- : typeof value.error === "string"
142
- ? value.error
143
- : typeof event?.message === "string"
144
- ? event.message
145
- : undefined;
146
- return [ts, type, agent ? `${agent}${stepIndex}` : stepIndex.trim(), status, exitCode, message].filter(Boolean).join(" | ");
147
- }
148
-
149
- function readRecentEvents(eventsPath: string, limit: number): { events: string[]; warning?: string } {
150
- const tail = readTailText(eventsPath);
151
- if (tail.warning) {
152
- return tail.warning.startsWith("missing ") ? { events: [] } : { events: [], warning: tail.warning };
153
- }
154
-
155
- const events: string[] = [];
156
- const lines = (tail.text ?? "").split("\n").filter((line) => line.trim());
157
- for (let i = lines.length - 1; i >= 0 && events.length < limit; i--) {
158
- try {
159
- const parsed = JSON.parse(lines[i]!);
160
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) continue;
161
- events.push(formatEventLine(parsed as Record<string, unknown>));
162
- } catch {
163
- // Skip malformed event records; async writers can be interrupted mid-line.
164
- }
165
- }
166
- return { events: events.reverse() };
167
- }
168
-
169
- export class SubagentsStatusComponent implements Component {
170
- private readonly width = 84;
171
- private readonly viewportHeight = 12;
172
- private readonly listRunsForOverlay: (asyncDirRoot: string, recentLimit?: number) => AsyncRunOverlayData;
173
- private readonly refreshTimer: NodeJS.Timeout;
174
- private screen: "list" | "detail" = "list";
175
- private cursor = 0;
176
- private scrollOffset = 0;
177
- private detailScrollOffset = 0;
178
- private detailRunId: string | undefined;
179
- private active: AsyncRunSummary[] = [];
180
- private recent: AsyncRunSummary[] = [];
181
- private rows: StatusRow[] = [];
182
- private errorMessage?: string;
183
- private tui: TUI;
184
- private theme: Theme;
185
- private done: () => void;
186
-
187
- constructor(
188
- tui: TUI,
189
- theme: Theme,
190
- done: () => void,
191
- deps: StatusOverlayDeps = {},
192
- ) {
193
- this.tui = tui;
194
- this.theme = theme;
195
- this.done = done;
196
- this.listRunsForOverlay = deps.listRunsForOverlay ?? listAsyncRunsForOverlay;
197
- const refreshMs = deps.refreshMs ?? AUTO_REFRESH_MS;
198
- this.reload();
199
- this.refreshTimer = setInterval(() => {
200
- this.reload();
201
- this.tui.requestRender();
202
- }, refreshMs);
203
- this.refreshTimer.unref?.();
204
- }
205
-
206
- private reload(): void {
207
- const previousSelectedId = selectedRun(this.rows, this.cursor)?.id;
208
- try {
209
- const overlayData = this.listRunsForOverlay(ASYNC_DIR, 5);
210
- this.active = overlayData.active;
211
- this.recent = overlayData.recent;
212
- this.rows = buildRows(this.active, this.recent);
213
- this.errorMessage = undefined;
214
- this.restoreSelection(previousSelectedId);
215
- this.ensureScrollVisible();
216
- } catch (error) {
217
- this.active = [];
218
- this.recent = [];
219
- this.rows = [];
220
- this.cursor = 0;
221
- this.scrollOffset = 0;
222
- this.errorMessage = error instanceof Error ? error.message : String(error);
223
- }
224
- }
225
-
226
- private restoreSelection(previousSelectedId?: string): void {
227
- const runRows = this.rows.filter((row) => row.kind === "run");
228
- if (runRows.length === 0) {
229
- this.cursor = 0;
230
- return;
231
- }
232
- if (!previousSelectedId) {
233
- this.cursor = Math.min(this.cursor, runRows.length - 1);
234
- return;
235
- }
236
- const nextIndex = runRows.findIndex((row) => row.run?.id === previousSelectedId);
237
- if (nextIndex !== -1) {
238
- this.cursor = nextIndex;
239
- return;
240
- }
241
- this.cursor = Math.min(this.cursor, runRows.length - 1);
242
- }
243
-
244
- private ensureScrollVisible(): void {
245
- if (this.rows.length <= this.viewportHeight) {
246
- this.scrollOffset = 0;
247
- return;
248
- }
249
- const selected = selectedRun(this.rows, this.cursor);
250
- if (!selected) {
251
- this.scrollOffset = 0;
252
- return;
253
- }
254
- const rowIndex = this.rows.findIndex((row) => row.kind === "run" && row.run?.id === selected.id);
255
- if (rowIndex === -1) return;
256
- if (rowIndex < this.scrollOffset) this.scrollOffset = rowIndex;
257
- if (rowIndex >= this.scrollOffset + this.viewportHeight) {
258
- this.scrollOffset = rowIndex - this.viewportHeight + 1;
259
- }
260
- }
261
-
262
- private renderRunDetails(run: AsyncRunSummary, width: number, innerW: number): string[] {
263
- const lines = [
264
- row(`cwd: ${truncateToWidth(shortenPath(run.cwd ?? run.asyncDir), innerW - 5)}`, width, this.theme),
265
- ];
266
- if (run.outputFile) {
267
- lines.push(row(`output: ${truncateToWidth(shortenPath(run.outputFile), innerW - 8)}`, width, this.theme));
268
- }
269
- if (run.sessionFile) {
270
- lines.push(row(`session: ${truncateToWidth(shortenPath(run.sessionFile), innerW - 9)}`, width, this.theme));
271
- }
272
- lines.push(...this.renderStepRows(run, width, innerW));
273
- return lines;
274
- }
275
-
276
- private renderStepRows(run: AsyncRunSummary, width: number, innerW: number): string[] {
277
- const lines: string[] = [];
278
- for (const step of run.steps) {
279
- const model = step.model ? ` | ${step.model}` : "";
280
- const attempts = step.attemptedModels && step.attemptedModels.length > 1
281
- ? ` | attempts ${step.attemptedModels.length}`
282
- : "";
283
- const duration = step.durationMs !== undefined ? ` | ${formatDuration(step.durationMs)}` : "";
284
- const tokens = step.tokens ? ` | ${formatTokens(step.tokens.total)} tok` : "";
285
- const activity = step.lastActivityAt
286
- ? step.activityState === "needs_attention"
287
- ? ` | no activity for ${formatDuration(Math.max(0, Date.now() - step.lastActivityAt))}`
288
- : ` | active ${formatDuration(Math.max(0, Date.now() - step.lastActivityAt))} ago`
289
- : "";
290
- const line = ` ${step.index + 1}. ${step.agent} | ${stepStatusColor(this.theme, step.status)}${activity}${model}${attempts}${duration}${tokens}`;
291
- lines.push(row(truncateToWidth(line, innerW), width, this.theme));
292
- if (step.error) {
293
- lines.push(row(truncateToWidth(` ${step.error}`, innerW), width, this.theme));
294
- }
295
- }
296
- if (run.steps.length === 0) {
297
- lines.push(row(this.theme.fg("dim", " No step details available yet."), width, this.theme));
298
- }
299
- return lines;
300
- }
301
-
302
- private renderDetail(run: AsyncRunSummary, width: number, innerW: number): string[] {
303
- const stepCount = run.steps.length || 1;
304
- const stepLabel = run.currentStep !== undefined ? `step ${run.currentStep + 1}/${stepCount}` : `steps ${stepCount}`;
305
- const duration = run.endedAt !== undefined
306
- ? formatDuration(Math.max(0, run.endedAt - run.startedAt))
307
- : formatDuration(Math.max(0, Date.now() - run.startedAt));
308
- const activity = run.lastActivityAt
309
- ? run.activityState === "needs_attention"
310
- ? `no activity for ${formatDuration(Math.max(0, Date.now() - run.lastActivityAt))}`
311
- : `active ${formatDuration(Math.max(0, Date.now() - run.lastActivityAt))} ago`
312
- : undefined;
313
-
314
- const body: string[] = [];
315
- body.push(row(`${run.id} | ${statusColor(this.theme, run.state)} | ${run.mode} | ${stepLabel} | ${duration}`, width, this.theme));
316
- if (activity) body.push(row(truncateToWidth(activity, innerW), width, this.theme));
317
- body.push(row("", width, this.theme));
318
- body.push(row(this.theme.fg("accent", "Steps"), width, this.theme));
319
- body.push(...this.renderStepRows(run, width, innerW));
320
-
321
- const eventsPath = path.join(run.asyncDir, "events.jsonl");
322
- const eventResult = readRecentEvents(eventsPath, DETAIL_EVENT_LIMIT);
323
- body.push(row("", width, this.theme));
324
- body.push(row(this.theme.fg("accent", "Recent events"), width, this.theme));
325
- if (eventResult.warning) body.push(row(this.theme.fg("warning", truncateToWidth(eventResult.warning, innerW)), width, this.theme));
326
- if (eventResult.events.length === 0 && !eventResult.warning) body.push(row(this.theme.fg("dim", " No events recorded."), width, this.theme));
327
- for (const event of eventResult.events) {
328
- body.push(row(truncateToWidth(` ${event}`, innerW), width, this.theme));
329
- }
330
-
331
- body.push(row("", width, this.theme));
332
- body.push(row(this.theme.fg("accent", "Output tail"), width, this.theme));
333
- if (run.outputFile) {
334
- const outputPath = resolveRunPath(run.asyncDir, run.outputFile);
335
- const tail = readTailLines(outputPath, OUTPUT_TAIL_LINES);
336
- if (tail.warning) body.push(row(this.theme.fg("warning", truncateToWidth(tail.warning, innerW)), width, this.theme));
337
- else if (tail.lines.length === 0) body.push(row(this.theme.fg("dim", " No output yet."), width, this.theme));
338
- for (const line of tail.lines) body.push(row(truncateToWidth(` ${line}`, innerW), width, this.theme));
339
- } else {
340
- body.push(row(this.theme.fg("dim", " No output file recorded."), width, this.theme));
341
- }
342
-
343
- body.push(row("", width, this.theme));
344
- body.push(row(this.theme.fg("accent", "Paths"), width, this.theme));
345
- body.push(row(truncateToWidth(` cwd: ${shortenPath(run.cwd ?? run.asyncDir)}`, innerW), width, this.theme));
346
- body.push(row(truncateToWidth(` asyncDir: ${shortenPath(run.asyncDir)}`, innerW), width, this.theme));
347
- if (run.outputFile) body.push(row(truncateToWidth(` outputFile: ${shortenPath(resolveRunPath(run.asyncDir, run.outputFile))}`, innerW), width, this.theme));
348
- if (run.sessionFile) body.push(row(truncateToWidth(` sessionFile: ${shortenPath(run.sessionFile)}`, innerW), width, this.theme));
349
- if (run.sessionDir) body.push(row(truncateToWidth(` sessionDir: ${shortenPath(run.sessionDir)}`, innerW), width, this.theme));
350
- const logPath = path.join(run.asyncDir, `subagent-log-${run.id}.md`);
351
- if (fs.existsSync(logPath)) body.push(row(truncateToWidth(` runLog: ${shortenPath(logPath)}`, innerW), width, this.theme));
352
-
353
- const maxOffset = Math.max(0, body.length - DETAIL_VIEWPORT_HEIGHT);
354
- this.detailScrollOffset = Math.min(this.detailScrollOffset, maxOffset);
355
- const visibleBody = body.slice(this.detailScrollOffset, this.detailScrollOffset + DETAIL_VIEWPORT_HEIGHT);
356
- const above = this.detailScrollOffset;
357
- const below = Math.max(0, body.length - (this.detailScrollOffset + visibleBody.length));
358
- const scrollInfo = formatScrollInfo(above, below);
359
- return [
360
- renderHeader(`Subagent Run ${run.id.slice(0, 8)}`, width, this.theme),
361
- ...visibleBody,
362
- scrollInfo ? row(this.theme.fg("dim", scrollInfo), width, this.theme) : row("", width, this.theme),
363
- renderFooter(" ↑↓ scroll esc summary q close read-only detail ", width, this.theme),
364
- ];
365
- }
366
-
367
- handleInput(data: string): void {
368
- if (this.screen === "detail" && matchesKey(data, "escape")) {
369
- this.screen = "list";
370
- this.detailRunId = undefined;
371
- this.tui.requestRender();
372
- return;
373
- }
374
- if (matchesKey(data, "escape") || matchesKey(data, "q") || matchesKey(data, "ctrl+c")) {
375
- this.done();
376
- return;
377
- }
378
- if (this.screen === "detail") {
379
- if (matchesKey(data, "up")) {
380
- this.detailScrollOffset = Math.max(0, this.detailScrollOffset - 1);
381
- this.tui.requestRender();
382
- return;
383
- }
384
- if (matchesKey(data, "down")) {
385
- this.detailScrollOffset++;
386
- this.tui.requestRender();
387
- return;
388
- }
389
- if (matchesKey(data, "pageup")) {
390
- this.detailScrollOffset = Math.max(0, this.detailScrollOffset - DETAIL_VIEWPORT_HEIGHT);
391
- this.tui.requestRender();
392
- return;
393
- }
394
- if (matchesKey(data, "pagedown")) {
395
- this.detailScrollOffset += DETAIL_VIEWPORT_HEIGHT;
396
- this.tui.requestRender();
397
- }
398
- return;
399
- }
400
- if (matchesKey(data, "return")) {
401
- const selected = selectedRun(this.rows, this.cursor);
402
- if (selected) {
403
- this.screen = "detail";
404
- this.detailRunId = selected.id;
405
- this.detailScrollOffset = 0;
406
- this.tui.requestRender();
407
- }
408
- return;
409
- }
410
- if (matchesKey(data, "up")) {
411
- this.cursor = Math.max(0, this.cursor - 1);
412
- this.ensureScrollVisible();
413
- this.tui.requestRender();
414
- return;
415
- }
416
- if (matchesKey(data, "down")) {
417
- const maxCursor = Math.max(0, this.rows.filter((row) => row.kind === "run").length - 1);
418
- this.cursor = Math.min(maxCursor, this.cursor + 1);
419
- this.ensureScrollVisible();
420
- this.tui.requestRender();
421
- }
422
- }
423
-
424
- render(width: number): string[] {
425
- const w = Math.min(width, this.width);
426
- const innerW = w - 2;
427
- const selected = selectedRun(this.rows, this.cursor);
428
- if (this.screen === "detail") {
429
- const detailRun = this.rows.find((row) => row.kind === "run" && row.run?.id === this.detailRunId)?.run;
430
- if (detailRun) return this.renderDetail(detailRun, w, innerW);
431
- return [
432
- renderHeader("Subagent Run", w, this.theme),
433
- row(this.theme.fg("warning", "Selected run is no longer available."), w, this.theme),
434
- renderFooter(" esc summary q close ", w, this.theme),
435
- ];
436
- }
437
- const lines: string[] = [renderHeader("Subagents Status", w, this.theme)];
438
- const rows = this.rows.length > 0 ? this.rows : [{ kind: "section" as const, label: "No async runs found" }];
439
- const visibleRows = rows.slice(this.scrollOffset, this.scrollOffset + this.viewportHeight);
440
- for (const statusRow of visibleRows) {
441
- if (statusRow.kind === "section") {
442
- lines.push(row(this.theme.fg("accent", statusRow.label), w, this.theme));
443
- continue;
444
- }
445
- const isSelected = selected?.id === statusRow.run?.id;
446
- lines.push(row(truncateToWidth(runLabel(this.theme, statusRow.run!, isSelected), innerW), w, this.theme));
447
- }
448
-
449
- const above = this.scrollOffset;
450
- const below = Math.max(0, rows.length - (this.scrollOffset + visibleRows.length));
451
- const scrollInfo = formatScrollInfo(above, below);
452
- if (scrollInfo) lines.push(row(this.theme.fg("dim", scrollInfo), w, this.theme));
453
- else lines.push(row("", w, this.theme));
454
-
455
- if (this.errorMessage) {
456
- lines.push(row(this.theme.fg("error", truncateToWidth(this.errorMessage, innerW)), w, this.theme));
457
- } else if (selected) {
458
- lines.push(row(this.theme.fg("accent", `Selected: ${selected.id}`), w, this.theme));
459
- lines.push(...this.renderRunDetails(selected, w, innerW));
460
- } else {
461
- lines.push(row(this.theme.fg("dim", "No runs selected."), w, this.theme));
462
- }
463
-
464
- const footer = `↑↓ select enter detail esc close summary view ${this.active.length} active / ${this.recent.length} recent`;
465
- lines.push(renderFooter(truncateToWidth(footer, innerW), w, this.theme));
466
- return lines;
467
- }
468
-
469
- dispose(): void {
470
- clearInterval(this.refreshTimer);
471
- }
472
- }