@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,216 +0,0 @@
1
- // Based on pi-subagents by Nico Bailon (https://github.com/nicobailon/pi-subagents)
2
- // MIT License
3
- import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
4
- import * as fs from "node:fs";
5
- import * as path from "node:path";
6
- import { renderWidget } from "./render.ts";
7
- import { formatControlNoticeMessage } from "./subagent-control.ts";
8
- import {
9
- type AsyncJobState,
10
- type ControlEvent,
11
- type SubagentState,
12
- POLL_INTERVAL_MS,
13
- SUBAGENT_CONTROL_EVENT,
14
- SUBAGENT_CONTROL_INTERCOM_EVENT,
15
- } from "./types.ts";
16
- import { readStatus } from "./utils.ts";
17
-
18
- interface AsyncJobTrackerOptions {
19
- completionRetentionMs?: number;
20
- pollIntervalMs?: number;
21
- }
22
-
23
- export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: SubagentState, asyncDirRoot: string, options: AsyncJobTrackerOptions = {}): {
24
- ensurePoller: () => void;
25
- handleStarted: (data: unknown) => void;
26
- handleComplete: (data: unknown) => void;
27
- resetJobs: (ctx?: ExtensionContext) => void;
28
- } {
29
- const completionRetentionMs = options.completionRetentionMs ?? 10000;
30
- const pollIntervalMs = options.pollIntervalMs ?? POLL_INTERVAL_MS;
31
- const rerenderWidget = (ctx: ExtensionContext, jobs = Array.from(state.asyncJobs.values())) => {
32
- renderWidget(ctx, jobs);
33
- ctx.ui.requestRender?.();
34
- };
35
- const scheduleCleanup = (asyncId: string) => {
36
- const existingTimer = state.cleanupTimers.get(asyncId);
37
- if (existingTimer) clearTimeout(existingTimer);
38
- const timer = setTimeout(() => {
39
- state.cleanupTimers.delete(asyncId);
40
- state.asyncJobs.delete(asyncId);
41
- if (state.lastUiContext) {
42
- rerenderWidget(state.lastUiContext);
43
- }
44
- }, completionRetentionMs);
45
- state.cleanupTimers.set(asyncId, timer);
46
- };
47
- const emitNewControlEvents = (job: AsyncJobState) => {
48
- const eventsPath = path.join(job.asyncDir, "events.jsonl");
49
- let fd: number;
50
- try {
51
- fd = fs.openSync(eventsPath, "r");
52
- } catch (error) {
53
- if ((error as NodeJS.ErrnoException).code === "ENOENT") return;
54
- console.error(`Failed to open async control events for '${job.asyncDir}':`, error);
55
- return;
56
- }
57
- try {
58
- const stat = fs.fstatSync(fd);
59
- const cursor = stat.size < (job.controlEventCursor ?? 0) ? 0 : (job.controlEventCursor ?? 0);
60
- if (stat.size <= cursor) return;
61
- const buffer = Buffer.alloc(stat.size - cursor);
62
- fs.readSync(fd, buffer, 0, buffer.length, cursor);
63
- const lastNewline = buffer.lastIndexOf(0x0a);
64
- if (lastNewline === -1) return;
65
- job.controlEventCursor = cursor + lastNewline + 1;
66
- for (const line of buffer.subarray(0, lastNewline).toString("utf-8").split("\n")) {
67
- if (!line.trim()) continue;
68
- let parsed: unknown;
69
- try {
70
- parsed = JSON.parse(line);
71
- } catch {
72
- // Ignore malformed completed records but keep the poller alive for later events.
73
- continue;
74
- }
75
- if (!parsed || typeof parsed !== "object" || (parsed as { type?: unknown }).type !== "subagent.control") continue;
76
- const record = parsed as { event?: ControlEvent; channels?: string[]; childIntercomTarget?: string; noticeText?: string; intercom?: { to?: string; message?: string } };
77
- if (!record.event || !Array.isArray(record.channels)) continue;
78
- const payload = {
79
- event: record.event,
80
- source: "async" as const,
81
- asyncDir: job.asyncDir,
82
- childIntercomTarget: record.childIntercomTarget,
83
- noticeText: record.noticeText ?? formatControlNoticeMessage(record.event, record.childIntercomTarget),
84
- };
85
- if (record.channels.includes("event")) {
86
- pi.events.emit(SUBAGENT_CONTROL_EVENT, payload);
87
- }
88
- if (record.channels.includes("intercom") && record.intercom?.to && record.intercom.message) {
89
- pi.events.emit(SUBAGENT_CONTROL_INTERCOM_EVENT, {
90
- ...payload,
91
- to: record.intercom.to,
92
- message: record.intercom.message,
93
- });
94
- }
95
- }
96
- } catch (error) {
97
- console.error(`Failed to read async control events for '${job.asyncDir}':`, error);
98
- } finally {
99
- fs.closeSync(fd);
100
- }
101
- };
102
-
103
- const ensurePoller = () => {
104
- if (state.poller) return;
105
- state.poller = setInterval(() => {
106
- if (state.asyncJobs.size === 0) {
107
- if (state.lastUiContext?.hasUI) rerenderWidget(state.lastUiContext, []);
108
- if (state.poller) {
109
- clearInterval(state.poller);
110
- state.poller = null;
111
- }
112
- return;
113
- }
114
-
115
- for (const job of state.asyncJobs.values()) {
116
- try {
117
- emitNewControlEvents(job);
118
- const status = readStatus(job.asyncDir);
119
- if (status) {
120
- const previousStatus = job.status;
121
- job.status = status.state;
122
- job.activityState = status.activityState;
123
- job.lastActivityAt = status.lastActivityAt ?? job.lastActivityAt;
124
- job.currentTool = status.currentTool ?? job.currentTool;
125
- job.currentToolStartedAt = status.currentToolStartedAt ?? job.currentToolStartedAt;
126
- job.mode = status.mode;
127
- job.currentStep = status.currentStep ?? job.currentStep;
128
- job.stepsTotal = status.steps?.length ?? job.stepsTotal;
129
- job.startedAt = status.startedAt ?? job.startedAt;
130
- job.updatedAt = status.lastUpdate ?? Date.now();
131
- if (status.steps?.length) {
132
- job.agents = status.steps.map((step) => step.agent);
133
- }
134
- job.sessionDir = status.sessionDir ?? job.sessionDir;
135
- job.outputFile = status.outputFile ?? job.outputFile;
136
- job.totalTokens = status.totalTokens ?? job.totalTokens;
137
- job.sessionFile = status.sessionFile ?? job.sessionFile;
138
- if ((job.status === "complete" || job.status === "failed" || job.status === "paused") && previousStatus !== job.status) {
139
- scheduleCleanup(job.asyncId);
140
- }
141
- continue;
142
- }
143
- job.status = job.status === "queued" ? "running" : job.status;
144
- job.updatedAt = Date.now();
145
- } catch (error) {
146
- console.error(`Failed to read async status for '${job.asyncDir}':`, error);
147
- job.status = "failed";
148
- job.updatedAt = Date.now();
149
- }
150
- }
151
-
152
- if (state.lastUiContext?.hasUI) rerenderWidget(state.lastUiContext);
153
- }, pollIntervalMs);
154
- state.poller.unref?.();
155
- };
156
-
157
- const handleStarted = (data: unknown) => {
158
- const info = data as {
159
- id?: string;
160
- asyncDir?: string;
161
- agent?: string;
162
- chain?: string[];
163
- };
164
- if (!info.id) return;
165
- const now = Date.now();
166
- const asyncDir = info.asyncDir ?? path.join(asyncDirRoot, info.id);
167
- const agents = info.chain && info.chain.length > 0 ? info.chain : info.agent ? [info.agent] : undefined;
168
- state.asyncJobs.set(info.id, {
169
- asyncId: info.id,
170
- asyncDir,
171
- status: "queued",
172
- mode: info.chain ? "chain" : "single",
173
- agents,
174
- stepsTotal: agents?.length,
175
- startedAt: now,
176
- updatedAt: now,
177
- });
178
- ensurePoller();
179
- if (state.lastUiContext) {
180
- rerenderWidget(state.lastUiContext);
181
- }
182
- };
183
-
184
- const handleComplete = (data: unknown) => {
185
- const result = data as { id?: string; success?: boolean; asyncDir?: string };
186
- const asyncId = result.id;
187
- if (!asyncId) return;
188
- const job = state.asyncJobs.get(asyncId);
189
- if (job) {
190
- job.status = result.success ? "complete" : "failed";
191
- job.updatedAt = Date.now();
192
- if (result.asyncDir) job.asyncDir = result.asyncDir;
193
- }
194
- if (state.lastUiContext) {
195
- rerenderWidget(state.lastUiContext);
196
- }
197
- scheduleCleanup(asyncId);
198
- };
199
-
200
- const resetJobs = (ctx?: ExtensionContext) => {
201
- for (const timer of state.cleanupTimers.values()) {
202
- clearTimeout(timer);
203
- }
204
- state.cleanupTimers.clear();
205
- state.asyncJobs.clear();
206
- state.foregroundControls?.clear();
207
- state.lastForegroundControlId = null;
208
- state.resultFileCoalescer.clear();
209
- if (ctx?.hasUI) {
210
- state.lastUiContext = ctx;
211
- rerenderWidget(ctx, []);
212
- }
213
- };
214
-
215
- return { ensurePoller, handleStarted, handleComplete, resetJobs };
216
- }
@@ -1,241 +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 { formatDuration, formatTokens, shortenPath } from "./formatters.ts";
6
- import { type ActivityState, type AsyncStatus, type TokenUsage } from "./types.ts";
7
- import { DEFAULT_CONTROL_CONFIG, deriveActivityState } from "./subagent-control.ts";
8
- import { readStatus } from "./utils.ts";
9
-
10
- export interface AsyncRunStepSummary {
11
- index: number;
12
- agent: string;
13
- status: string;
14
- activityState?: ActivityState;
15
- lastActivityAt?: number;
16
- currentTool?: string;
17
- currentToolStartedAt?: number;
18
- durationMs?: number;
19
- tokens?: TokenUsage;
20
- skills?: string[];
21
- model?: string;
22
- attemptedModels?: string[];
23
- error?: string;
24
- }
25
-
26
- export interface AsyncRunSummary {
27
- id: string;
28
- asyncDir: string;
29
- state: "queued" | "running" | "complete" | "failed" | "paused";
30
- activityState?: ActivityState;
31
- lastActivityAt?: number;
32
- currentTool?: string;
33
- currentToolStartedAt?: number;
34
- mode: "single" | "chain";
35
- cwd?: string;
36
- startedAt: number;
37
- lastUpdate?: number;
38
- endedAt?: number;
39
- currentStep?: number;
40
- steps: AsyncRunStepSummary[];
41
- sessionDir?: string;
42
- outputFile?: string;
43
- totalTokens?: TokenUsage;
44
- sessionFile?: string;
45
- }
46
-
47
- export interface AsyncRunListOptions {
48
- states?: Array<AsyncRunSummary["state"]>;
49
- limit?: number;
50
- }
51
-
52
- export interface AsyncRunOverlayData {
53
- active: AsyncRunSummary[];
54
- recent: AsyncRunSummary[];
55
- }
56
-
57
- function getErrorMessage(error: unknown): string {
58
- return error instanceof Error ? error.message : String(error);
59
- }
60
-
61
- function isNotFoundError(error: unknown): boolean {
62
- return typeof error === "object"
63
- && error !== null
64
- && "code" in error
65
- && (error as NodeJS.ErrnoException).code === "ENOENT";
66
- }
67
-
68
- function isAsyncRunDir(root: string, entry: string): boolean {
69
- const entryPath = path.join(root, entry);
70
- try {
71
- return fs.statSync(entryPath).isDirectory();
72
- } catch (error) {
73
- if (isNotFoundError(error)) return false;
74
- throw new Error(`Failed to inspect async run path '${entryPath}': ${getErrorMessage(error)}`, {
75
- cause: error instanceof Error ? error : undefined,
76
- });
77
- }
78
- }
79
-
80
- function outputFileMtime(outputFile: string | undefined): number | undefined {
81
- if (!outputFile) return undefined;
82
- try {
83
- return fs.statSync(outputFile).mtimeMs;
84
- } catch {
85
- return undefined;
86
- }
87
- }
88
-
89
- function deriveAsyncActivityState(asyncDir: string, status: AsyncStatus): { activityState?: ActivityState; lastActivityAt?: number } {
90
- if (status.state !== "running") return { activityState: status.activityState, lastActivityAt: status.lastActivityAt };
91
- const outputPath = status.outputFile ? (path.isAbsolute(status.outputFile) ? status.outputFile : path.join(asyncDir, status.outputFile)) : undefined;
92
- const currentStep = typeof status.currentStep === "number" ? status.steps?.[status.currentStep] : undefined;
93
- const lastActivityAt = status.lastActivityAt ?? outputFileMtime(outputPath) ?? currentStep?.lastActivityAt ?? currentStep?.startedAt ?? status.startedAt;
94
- return {
95
- lastActivityAt,
96
- activityState: status.activityState ?? deriveActivityState({
97
- config: DEFAULT_CONTROL_CONFIG,
98
- startedAt: status.startedAt,
99
- lastActivityAt,
100
- }),
101
- };
102
- }
103
-
104
- function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string }): AsyncRunSummary {
105
- const { activityState, lastActivityAt } = deriveAsyncActivityState(asyncDir, status);
106
- return {
107
- id: status.runId || path.basename(asyncDir),
108
- asyncDir,
109
- state: status.state,
110
- activityState,
111
- lastActivityAt,
112
- currentTool: status.currentTool,
113
- currentToolStartedAt: status.currentToolStartedAt,
114
- mode: status.mode,
115
- cwd: status.cwd,
116
- startedAt: status.startedAt,
117
- lastUpdate: status.lastUpdate,
118
- endedAt: status.endedAt,
119
- currentStep: status.currentStep,
120
- steps: (status.steps ?? []).map((step, index) => {
121
- const stepActivityState = step.activityState ?? (step.status === "running" ? activityState : undefined);
122
- const stepLastActivityAt = step.lastActivityAt ?? (step.status === "running" ? lastActivityAt : undefined);
123
- return {
124
- index,
125
- agent: step.agent,
126
- status: step.status,
127
- ...(stepActivityState ? { activityState: stepActivityState } : {}),
128
- ...(stepLastActivityAt ? { lastActivityAt: stepLastActivityAt } : {}),
129
- ...(step.currentTool ? { currentTool: step.currentTool } : {}),
130
- ...(step.currentToolStartedAt ? { currentToolStartedAt: step.currentToolStartedAt } : {}),
131
- ...(step.durationMs !== undefined ? { durationMs: step.durationMs } : {}),
132
- ...(step.tokens ? { tokens: step.tokens } : {}),
133
- ...(step.skills ? { skills: step.skills } : {}),
134
- ...(step.model ? { model: step.model } : {}),
135
- ...(step.attemptedModels ? { attemptedModels: step.attemptedModels } : {}),
136
- ...(step.error ? { error: step.error } : {}),
137
- };
138
- }),
139
- ...(status.sessionDir ? { sessionDir: status.sessionDir } : {}),
140
- ...(status.outputFile ? { outputFile: status.outputFile } : {}),
141
- ...(status.totalTokens ? { totalTokens: status.totalTokens } : {}),
142
- ...(status.sessionFile ? { sessionFile: status.sessionFile } : {}),
143
- };
144
- }
145
-
146
- function sortRuns(runs: AsyncRunSummary[]): AsyncRunSummary[] {
147
- const rank = (state: AsyncRunSummary["state"]): number => {
148
- switch (state) {
149
- case "running": return 0;
150
- case "queued": return 1;
151
- case "failed": return 2;
152
- case "paused": return 2;
153
- case "complete": return 3;
154
- }
155
- };
156
- return [...runs].sort((a, b) => {
157
- const byState = rank(a.state) - rank(b.state);
158
- if (byState !== 0) return byState;
159
- const aTime = a.lastUpdate ?? a.endedAt ?? a.startedAt;
160
- const bTime = b.lastUpdate ?? b.endedAt ?? b.startedAt;
161
- return bTime - aTime;
162
- });
163
- }
164
-
165
- export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions = {}): AsyncRunSummary[] {
166
- let entries: string[];
167
- try {
168
- entries = fs.readdirSync(asyncDirRoot).filter((entry) => isAsyncRunDir(asyncDirRoot, entry));
169
- } catch (error) {
170
- if (isNotFoundError(error)) return [];
171
- throw new Error(`Failed to list async runs in '${asyncDirRoot}': ${getErrorMessage(error)}`, {
172
- cause: error instanceof Error ? error : undefined,
173
- });
174
- }
175
-
176
- const allowedStates = options.states ? new Set(options.states) : undefined;
177
- const runs: AsyncRunSummary[] = [];
178
- for (const entry of entries) {
179
- const asyncDir = path.join(asyncDirRoot, entry);
180
- const status = readStatus(asyncDir) as (AsyncStatus & { cwd?: string }) | null;
181
- if (!status) continue;
182
- const summary = statusToSummary(asyncDir, status);
183
- if (allowedStates && !allowedStates.has(summary.state)) continue;
184
- runs.push(summary);
185
- }
186
-
187
- const sorted = sortRuns(runs);
188
- return options.limit !== undefined ? sorted.slice(0, options.limit) : sorted;
189
- }
190
-
191
- export function listAsyncRunsForOverlay(asyncDirRoot: string, recentLimit = 5): AsyncRunOverlayData {
192
- const all = listAsyncRuns(asyncDirRoot);
193
- const recent = all
194
- .filter((run) => run.state === "complete" || run.state === "failed" || run.state === "paused")
195
- .sort((a, b) => (b.lastUpdate ?? b.endedAt ?? b.startedAt) - (a.lastUpdate ?? a.endedAt ?? a.startedAt))
196
- .slice(0, recentLimit);
197
- return {
198
- active: all.filter((run) => run.state === "queued" || run.state === "running"),
199
- recent,
200
- };
201
- }
202
-
203
- function formatActivityFacts(input: { activityState?: ActivityState; lastActivityAt?: number; currentTool?: string; currentToolStartedAt?: number }): string | undefined {
204
- if (input.currentTool && input.currentToolStartedAt) return `tool ${input.currentTool} ${formatDuration(Math.max(0, Date.now() - input.currentToolStartedAt))}`;
205
- if (!input.lastActivityAt) return input.activityState === "needs_attention" ? "needs attention" : undefined;
206
- const elapsed = formatDuration(Math.max(0, Date.now() - input.lastActivityAt));
207
- return input.activityState === "needs_attention" ? `no activity for ${elapsed}` : `active ${elapsed} ago`;
208
- }
209
-
210
- function formatStepLine(step: AsyncRunStepSummary): string {
211
- const parts = [`${step.index + 1}. ${step.agent}`, step.status];
212
- const activity = formatActivityFacts(step);
213
- if (activity) parts.push(activity);
214
- if (step.model) parts.push(step.model);
215
- if (step.durationMs !== undefined) parts.push(formatDuration(step.durationMs));
216
- if (step.tokens) parts.push(`${formatTokens(step.tokens.total)} tok`);
217
- return parts.join(" | ");
218
- }
219
-
220
- function formatRunHeader(run: AsyncRunSummary): string {
221
- const stepCount = run.steps.length || 1;
222
- const stepLabel = run.currentStep !== undefined ? `step ${run.currentStep + 1}/${stepCount}` : `steps ${stepCount}`;
223
- const cwd = run.cwd ? shortenPath(run.cwd) : shortenPath(run.asyncDir);
224
- const activity = formatActivityFacts(run);
225
- return `${run.id} | ${run.state}${activity ? ` | ${activity}` : ""} | ${run.mode} | ${stepLabel} | ${cwd}`;
226
- }
227
-
228
- export function formatAsyncRunList(runs: AsyncRunSummary[], heading = "Active async runs"): string {
229
- if (runs.length === 0) return `No ${heading.toLowerCase()}.`;
230
-
231
- const lines = [`${heading}: ${runs.length}`, ""];
232
- for (const run of runs) {
233
- lines.push(`- ${formatRunHeader(run)}`);
234
- for (const step of run.steps) {
235
- lines.push(` ${formatStepLine(step)}`);
236
- }
237
- if (run.sessionFile) lines.push(` session: ${shortenPath(run.sessionFile)}`);
238
- lines.push("");
239
- }
240
- return lines.join("\n").trimEnd();
241
- }