@gotgenes/pi-subagents 7.2.4 → 7.2.6

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.
@@ -2,7 +2,8 @@
2
2
  import { wrapTextWithAnsi } from "@earendil-works/pi-tui";
3
3
  import { AgentTypeRegistry } from "#src/config/agent-types";
4
4
  import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
5
- import type { ModelRegistry } from "#src/session/model-resolver";
5
+ import { type ModelRegistry, resolveModel } from "#src/session/model-resolver";
6
+ import { getModelLabelFromConfig } from "#src/tools/helpers";
6
7
  import type { AgentConfig, AgentRecord } from "#src/types";
7
8
  import type { AgentActivityTracker } from "#src/ui/agent-activity-tracker";
8
9
  import { createAgentConfigEditor } from "#src/ui/agent-config-editor";
@@ -10,7 +11,7 @@ import { createAgentCreationWizard } from "#src/ui/agent-creation-wizard";
10
11
  import type { AgentFileOps } from "#src/ui/agent-file-ops";
11
12
  import { formatDuration, getDisplayName } from "#src/ui/display";
12
13
 
13
- // ---- Deps interface ----
14
+ // ---- Narrow interfaces ----
14
15
 
15
16
  /** Narrow manager interface for menu operations. */
16
17
  export interface AgentMenuManager {
@@ -43,19 +44,6 @@ export interface AgentActivityReader {
43
44
  get(id: string): AgentActivityTracker | undefined;
44
45
  }
45
46
 
46
- export interface AgentMenuDeps {
47
- manager: AgentMenuManager;
48
- registry: AgentTypeRegistry;
49
- agentActivity: AgentActivityReader;
50
- /** Resolve model label for a given agent type + registry. */
51
- getModelLabel: (type: string, registry?: ModelRegistry) => string;
52
- /** Settings manager — owns in-memory values and persistence. */
53
- settings: AgentMenuSettings;
54
- fileOps: AgentFileOps;
55
- personalAgentsDir: string;
56
- projectAgentsDir: string;
57
- }
58
-
59
47
  // ---- Narrow UI context types ----
60
48
 
61
49
  /** Narrow UI interface — only the ctx.ui methods menu handlers actually call. */
@@ -68,48 +56,74 @@ export interface MenuUI {
68
56
  custom<R>(component: any, options?: any): Promise<R>;
69
57
  }
70
58
 
71
- // ---- Factory ----
59
+ // ---- Class ----
72
60
 
73
61
  /**
74
- * Create the `/agents` command handler.
75
- * Returns a function suitable for `pi.registerCommand("agents", { handler })`.
62
+ * Handler for the `/agents` slash command.
63
+ *
64
+ * Call `handle(ctx)` from the Pi command registration to open the interactive menu.
76
65
  */
77
- export function createAgentsMenuHandler({
78
- manager,
79
- registry,
80
- agentActivity,
81
- getModelLabel,
82
- settings,
83
- fileOps,
84
- personalAgentsDir,
85
- projectAgentsDir,
86
- }: AgentMenuDeps) {
87
- const editor = createAgentConfigEditor(
88
- fileOps,
89
- registry,
90
- personalAgentsDir,
91
- projectAgentsDir,
92
- );
93
-
94
- const wizard = createAgentCreationWizard({
95
- fileOps,
96
- manager,
97
- registry,
98
- personalAgentsDir,
99
- projectAgentsDir,
100
- });
101
-
102
- async function showAgentsMenu(
66
+ export class AgentsMenuHandler {
67
+ private readonly editor: ReturnType<typeof createAgentConfigEditor>;
68
+ private readonly wizard: ReturnType<typeof createAgentCreationWizard>;
69
+
70
+ constructor(
71
+ private readonly manager: AgentMenuManager,
72
+ private readonly registry: AgentTypeRegistry,
73
+ private readonly agentActivity: AgentActivityReader,
74
+ private readonly settings: AgentMenuSettings,
75
+ private readonly fileOps: AgentFileOps,
76
+ private readonly personalAgentsDir: string,
77
+ private readonly projectAgentsDir: string,
78
+ ) {
79
+ this.editor = createAgentConfigEditor(
80
+ fileOps,
81
+ registry,
82
+ personalAgentsDir,
83
+ projectAgentsDir,
84
+ );
85
+ this.wizard = createAgentCreationWizard({
86
+ fileOps,
87
+ manager,
88
+ registry,
89
+ personalAgentsDir,
90
+ projectAgentsDir,
91
+ });
92
+ }
93
+
94
+ async handle({
95
+ ui,
96
+ modelRegistry,
97
+ parentSnapshot,
98
+ }: {
99
+ ui: MenuUI;
100
+ modelRegistry: ModelRegistry;
101
+ parentSnapshot: ParentSnapshot;
102
+ }): Promise<void> {
103
+ await this.showAgentsMenu(ui, modelRegistry, parentSnapshot);
104
+ }
105
+
106
+ private getModelLabel(type: string, modelRegistry?: ModelRegistry): string {
107
+ const cfg = this.registry.resolveAgentConfig(type);
108
+ if (!cfg.model) return "inherit";
109
+ if (modelRegistry) {
110
+ const resolved = resolveModel(cfg.model, modelRegistry);
111
+ if (typeof resolved === "string") return "inherit";
112
+ }
113
+ return getModelLabelFromConfig(cfg.model);
114
+ }
115
+
116
+ private async showAgentsMenu(
103
117
  ui: MenuUI,
104
118
  modelRegistry: ModelRegistry,
105
119
  parentSnapshot: ParentSnapshot,
106
- ) {
107
- registry.reload();
108
- const allNames = registry.getAllTypes();
120
+ ): Promise<void> {
121
+ this.registry.reload();
122
+ const allNames = this.registry.getAllTypes();
109
123
 
110
124
  const options: string[] = [];
111
125
 
112
- const agents = manager.listAgents();
126
+ const agents = this.manager.listAgents();
113
127
  if (agents.length > 0) {
114
128
  const running = agents.filter(
115
129
  (a) => a.status === "running" || a.status === "queued",
@@ -144,21 +158,21 @@ export function createAgentsMenuHandler({
144
158
  if (!choice) return;
145
159
 
146
160
  if (choice.startsWith("Running agents (")) {
147
- await showRunningAgents(ui);
148
- await showAgentsMenu(ui, modelRegistry, parentSnapshot);
161
+ await this.showRunningAgents(ui);
162
+ await this.showAgentsMenu(ui, modelRegistry, parentSnapshot);
149
163
  } else if (choice.startsWith("Agent types (")) {
150
- await showAllAgentsList(ui, modelRegistry);
151
- await showAgentsMenu(ui, modelRegistry, parentSnapshot);
164
+ await this.showAllAgentsList(ui, modelRegistry);
165
+ await this.showAgentsMenu(ui, modelRegistry, parentSnapshot);
152
166
  } else if (choice === "Create new agent") {
153
- await wizard.showCreateWizard(ui, parentSnapshot);
167
+ await this.wizard.showCreateWizard(ui, parentSnapshot);
154
168
  } else if (choice === "Settings") {
155
- await showSettings(ui);
156
- await showAgentsMenu(ui, modelRegistry, parentSnapshot);
169
+ await this.showSettings(ui);
170
+ await this.showAgentsMenu(ui, modelRegistry, parentSnapshot);
157
171
  }
158
172
  }
159
173
 
160
- async function showAllAgentsList(ui: MenuUI, modelRegistry: ModelRegistry) {
161
- const allNames = registry.getAllTypes();
174
+ private async showAllAgentsList(ui: MenuUI, modelRegistry: ModelRegistry): Promise<void> {
175
+ const allNames = this.registry.getAllTypes();
162
176
  if (allNames.length === 0) {
163
177
  ui.notify("No agents.", "info");
164
178
  return;
@@ -173,9 +187,9 @@ export function createAgentsMenuHandler({
173
187
  };
174
188
 
175
189
  const entries = allNames.map((name) => {
176
- const cfg = registry.resolveAgentConfig(name);
190
+ const cfg = this.registry.resolveAgentConfig(name);
177
191
  const disabled = cfg.enabled === false;
178
- const model = getModelLabel(name, modelRegistry);
192
+ const model = this.getModelLabel(name, modelRegistry);
179
193
  const indicator = sourceIndicator(cfg);
180
194
  const prefix = `${indicator}${name} · ${model}`;
181
195
  const desc = disabled ? "(disabled)" : cfg.description;
@@ -184,11 +198,11 @@ export function createAgentsMenuHandler({
184
198
  const maxPrefix = Math.max(...entries.map((e) => e.prefix.length));
185
199
 
186
200
  const hasCustom = allNames.some((n) => {
187
- const c = registry.resolveAgentConfig(n);
201
+ const c = this.registry.resolveAgentConfig(n);
188
202
  return !c.isDefault && c.enabled !== false;
189
203
  });
190
204
  const hasDisabled = allNames.some(
191
- (n) => registry.resolveAgentConfig(n).enabled === false,
205
+ (n) => this.registry.resolveAgentConfig(n).enabled === false,
192
206
  );
193
207
  const legendParts: string[] = [];
194
208
  if (hasCustom) legendParts.push("• = project ◦ = global");
@@ -207,21 +221,21 @@ export function createAgentsMenuHandler({
207
221
  .split(" · ")[0]
208
222
  .replace(/^[•◦✕\s]+/, "")
209
223
  .trim();
210
- if (registry.resolveType(agentName) != null) {
211
- await editor.showAgentDetail(ui, agentName);
212
- await showAllAgentsList(ui, modelRegistry);
224
+ if (this.registry.resolveType(agentName) != null) {
225
+ await this.editor.showAgentDetail(ui, agentName);
226
+ await this.showAllAgentsList(ui, modelRegistry);
213
227
  }
214
228
  }
215
229
 
216
- async function showRunningAgents(ui: MenuUI) {
217
- const agents = manager.listAgents();
230
+ private async showRunningAgents(ui: MenuUI): Promise<void> {
231
+ const agents = this.manager.listAgents();
218
232
  if (agents.length === 0) {
219
233
  ui.notify("No agents.", "info");
220
234
  return;
221
235
  }
222
236
 
223
237
  const options = agents.map((a) => {
224
- const dn = getDisplayName(a.type, registry);
238
+ const dn = getDisplayName(a.type, this.registry);
225
239
  const dur = formatDuration(a.startedAt, a.completedAt);
226
240
  return `${dn} (${a.description}) · ${a.toolUses} tools · ${a.status} · ${dur}`;
227
241
  });
@@ -233,11 +247,11 @@ export function createAgentsMenuHandler({
233
247
  if (idx < 0) return;
234
248
  const record = agents[idx];
235
249
 
236
- await viewAgentConversation(ui, record);
237
- await showRunningAgents(ui);
250
+ await this.viewAgentConversation(ui, record);
251
+ await this.showRunningAgents(ui);
238
252
  }
239
253
 
240
- async function viewAgentConversation(ui: MenuUI, record: AgentRecord) {
254
+ private async viewAgentConversation(ui: MenuUI, record: AgentRecord): Promise<void> {
241
255
  const session = record.session;
242
256
  if (!session) {
243
257
  ui.notify(
@@ -250,7 +264,7 @@ export function createAgentsMenuHandler({
250
264
  const { ConversationViewer, VIEWPORT_HEIGHT_PCT } = await import(
251
265
  "./conversation-viewer"
252
266
  );
253
- const activity = agentActivity.get(record.id);
267
+ const activity = this.agentActivity.get(record.id);
254
268
 
255
269
  await ui.custom<undefined>(
256
270
  (tui: any, theme: any, _keybindings: any, done: any) => {
@@ -261,7 +275,7 @@ export function createAgentsMenuHandler({
261
275
  activity,
262
276
  theme,
263
277
  done,
264
- registry,
278
+ registry: this.registry,
265
279
  wrapText: wrapTextWithAnsi,
266
280
  });
267
281
  },
@@ -276,23 +290,23 @@ export function createAgentsMenuHandler({
276
290
  );
277
291
  }
278
292
 
279
- async function showSettings(ui: MenuUI) {
293
+ private async showSettings(ui: MenuUI): Promise<void> {
280
294
  const choice = await ui.select("Settings", [
281
- `Max concurrency (current: ${settings.maxConcurrent})`,
282
- `Default max turns (current: ${settings.defaultMaxTurns ?? "unlimited"})`,
283
- `Grace turns (current: ${settings.graceTurns})`,
295
+ `Max concurrency (current: ${this.settings.maxConcurrent})`,
296
+ `Default max turns (current: ${this.settings.defaultMaxTurns ?? "unlimited"})`,
297
+ `Grace turns (current: ${this.settings.graceTurns})`,
284
298
  ]);
285
299
  if (!choice) return;
286
300
 
287
301
  if (choice.startsWith("Max concurrency")) {
288
302
  const val = await ui.input(
289
303
  "Max concurrent background agents",
290
- String(settings.maxConcurrent),
304
+ String(this.settings.maxConcurrent),
291
305
  );
292
306
  if (val) {
293
307
  const n = parseInt(val, 10);
294
308
  if (n >= 1) {
295
- const toast = settings.applyMaxConcurrent(n);
309
+ const toast = this.settings.applyMaxConcurrent(n);
296
310
  ui.notify(toast.message, toast.level);
297
311
  } else {
298
312
  ui.notify("Must be a positive integer.", "warning");
@@ -301,12 +315,12 @@ export function createAgentsMenuHandler({
301
315
  } else if (choice.startsWith("Default max turns")) {
302
316
  const val = await ui.input(
303
317
  "Default max turns before wrap-up (0 = unlimited)",
304
- String(settings.defaultMaxTurns ?? 0),
318
+ String(this.settings.defaultMaxTurns ?? 0),
305
319
  );
306
320
  if (val) {
307
321
  const n = parseInt(val, 10);
308
322
  if (n >= 0) {
309
- const toast = settings.applyDefaultMaxTurns(n);
323
+ const toast = this.settings.applyDefaultMaxTurns(n);
310
324
  ui.notify(toast.message, toast.level);
311
325
  } else {
312
326
  ui.notify("Must be 0 (unlimited) or a positive integer.", "warning");
@@ -315,12 +329,12 @@ export function createAgentsMenuHandler({
315
329
  } else if (choice.startsWith("Grace turns")) {
316
330
  const val = await ui.input(
317
331
  "Grace turns after wrap-up steer",
318
- String(settings.graceTurns),
332
+ String(this.settings.graceTurns),
319
333
  );
320
334
  if (val) {
321
335
  const n = parseInt(val, 10);
322
336
  if (n >= 1) {
323
- const toast = settings.applyGraceTurns(n);
337
+ const toast = this.settings.applyGraceTurns(n);
324
338
  ui.notify(toast.message, toast.level);
325
339
  } else {
326
340
  ui.notify("Must be a positive integer.", "warning");
@@ -328,16 +342,4 @@ export function createAgentsMenuHandler({
328
342
  }
329
343
  }
330
344
  }
331
-
332
- return async ({
333
- ui,
334
- modelRegistry,
335
- parentSnapshot,
336
- }: {
337
- ui: MenuUI;
338
- modelRegistry: ModelRegistry;
339
- parentSnapshot: ParentSnapshot;
340
- }) => {
341
- await showAgentsMenu(ui, modelRegistry, parentSnapshot);
342
- };
343
345
  }
@@ -127,6 +127,135 @@ export function renderRunningLines(
127
127
  /** Maximum number of rendered lines before overflow collapse kicks in. */
128
128
  const MAX_WIDGET_LINES = 12;
129
129
 
130
+ interface AgentCategories {
131
+ running: WidgetAgent[];
132
+ queued: WidgetAgent[];
133
+ finished: WidgetAgent[];
134
+ }
135
+
136
+ /** Partition agents into rendering buckets. */
137
+ function categorizeAgents(
138
+ agents: readonly WidgetAgent[],
139
+ shouldShowFinished: (agentId: string, status: string) => boolean,
140
+ ): AgentCategories {
141
+ return {
142
+ running: agents.filter(a => a.status === "running"),
143
+ queued: agents.filter(a => a.status === "queued"),
144
+ finished: agents.filter(
145
+ a => a.status !== "running" && a.status !== "queued" && a.completedAt != null
146
+ && shouldShowFinished(a.id, a.status),
147
+ ),
148
+ };
149
+ }
150
+
151
+ interface WidgetSections {
152
+ finishedLines: string[];
153
+ runningLines: [string, string][];
154
+ queuedLine: string | undefined;
155
+ }
156
+
157
+ /** Render each agent bucket into pre-formatted lines with ├─ tree connectors. */
158
+ function buildSections(
159
+ categories: AgentCategories,
160
+ activityMap: ReadonlyMap<string, WidgetActivity>,
161
+ registry: AgentConfigLookup,
162
+ spinnerFrame: number,
163
+ theme: Theme,
164
+ truncate: (line: string) => string,
165
+ ): WidgetSections {
166
+ const finishedLines: string[] = [];
167
+ for (const a of categories.finished) {
168
+ finishedLines.push(truncate(theme.fg("dim", "\u251C\u2500") + " " + renderFinishedLine(a, activityMap.get(a.id), registry, theme)));
169
+ }
170
+
171
+ const runningLines: [string, string][] = [];
172
+ for (const a of categories.running) {
173
+ const [header, act] = renderRunningLines(a, activityMap.get(a.id), registry, spinnerFrame, theme);
174
+ runningLines.push([
175
+ truncate(theme.fg("dim", "\u251C\u2500") + ` ${header}`),
176
+ truncate(theme.fg("dim", "\u2502 ") + act),
177
+ ]);
178
+ }
179
+
180
+ const queuedLine = categories.queued.length > 0
181
+ ? truncate(theme.fg("dim", "\u251C\u2500") + ` ${theme.fg("muted", "\u25E6")} ${theme.fg("dim", `${categories.queued.length} queued`)}`)
182
+ : undefined;
183
+
184
+ return { finishedLines, runningLines, queuedLine };
185
+ }
186
+
187
+ /**
188
+ * Assemble widget lines when total body fits within MAX_WIDGET_LINES.
189
+ * Fixes the last tree connector: ├─ → └─, and │ → space for the running-agent activity line.
190
+ */
191
+ function assembleWithinBudget(heading: string, sections: WidgetSections): string[] {
192
+ const { finishedLines, runningLines, queuedLine } = sections;
193
+ const lines: string[] = [heading, ...finishedLines];
194
+ for (const pair of runningLines) lines.push(...pair);
195
+ if (queuedLine) lines.push(queuedLine);
196
+
197
+ // Fix last connector: swap \u251C\u2500 \u2192 \u2514\u2500.
198
+ if (lines.length > 1) {
199
+ const last = lines.length - 1;
200
+ lines[last] = lines[last].replace("\u251C\u2500", "\u2514\u2500");
201
+ if (runningLines.length > 0 && !queuedLine) {
202
+ if (last >= 2) {
203
+ lines[last - 1] = lines[last - 1].replace("\u251C\u2500", "\u2514\u2500");
204
+ lines[last] = lines[last].replace("\u2502 ", " ");
205
+ }
206
+ }
207
+ }
208
+ return lines;
209
+ }
210
+
211
+ /**
212
+ * Assemble widget lines when total body exceeds MAX_WIDGET_LINES.
213
+ * Prioritizes running > queued > finished and appends an overflow indicator.
214
+ */
215
+ function assembleOverflow(
216
+ heading: string,
217
+ sections: WidgetSections,
218
+ maxBody: number,
219
+ truncate: (line: string) => string,
220
+ theme: Theme,
221
+ ): string[] {
222
+ const { finishedLines, runningLines, queuedLine } = sections;
223
+ const lines: string[] = [heading];
224
+ let budget = maxBody - 1;
225
+ let hiddenRunning = 0;
226
+ let hiddenFinished = 0;
227
+
228
+ for (const pair of runningLines) {
229
+ if (budget >= 2) {
230
+ lines.push(...pair);
231
+ budget -= 2;
232
+ } else {
233
+ hiddenRunning++;
234
+ }
235
+ }
236
+
237
+ if (queuedLine && budget >= 1) {
238
+ lines.push(queuedLine);
239
+ budget--;
240
+ }
241
+
242
+ for (const fl of finishedLines) {
243
+ if (budget >= 1) {
244
+ lines.push(fl);
245
+ budget--;
246
+ } else {
247
+ hiddenFinished++;
248
+ }
249
+ }
250
+
251
+ const overflowParts: string[] = [];
252
+ if (hiddenRunning > 0) overflowParts.push(`${hiddenRunning} running`);
253
+ if (hiddenFinished > 0) overflowParts.push(`${hiddenFinished} finished`);
254
+ const overflowText = overflowParts.join(", ");
255
+ lines.push(truncate(theme.fg("dim", "\u2514\u2500") + ` ${theme.fg("dim", `+${hiddenRunning + hiddenFinished} more (${overflowText})`)}`));
256
+ return lines;
257
+ }
258
+
130
259
  /** Pure rendering of the widget body. Returns lines to display. */
131
260
  export function renderWidgetLines(params: {
132
261
  agents: readonly WidgetAgent[];
@@ -139,12 +268,7 @@ export function renderWidgetLines(params: {
139
268
  }): string[] {
140
269
  const { agents, activityMap, registry, spinnerFrame, terminalWidth, theme, shouldShowFinished } = params;
141
270
 
142
- const running = agents.filter(a => a.status === "running");
143
- const queued = agents.filter(a => a.status === "queued");
144
- const finished = agents.filter(a =>
145
- a.status !== "running" && a.status !== "queued" && a.completedAt
146
- && shouldShowFinished(a.id, a.status),
147
- );
271
+ const { running, queued, finished } = categorizeAgents(agents, shouldShowFinished);
148
272
 
149
273
  const hasActive = running.length > 0 || queued.length > 0;
150
274
  const hasFinished = finished.length > 0;
@@ -155,82 +279,22 @@ export function renderWidgetLines(params: {
155
279
  const headingColor = hasActive ? "accent" : "dim";
156
280
  const headingIcon = hasActive ? "\u25CF" : "\u25CB";
157
281
 
158
- // Build sections separately for overflow-aware assembly.
159
- const finishedLines: string[] = [];
160
- for (const a of finished) {
161
- finishedLines.push(truncate(theme.fg("dim", "\u251C\u2500") + " " + renderFinishedLine(a, activityMap.get(a.id), registry, theme)));
162
- }
163
-
164
- const runningLines: [string, string][] = [];
165
- for (const a of running) {
166
- const [header, act] = renderRunningLines(a, activityMap.get(a.id), registry, spinnerFrame, theme);
167
- runningLines.push([
168
- truncate(theme.fg("dim", "\u251C\u2500") + ` ${header}`),
169
- truncate(theme.fg("dim", "\u2502 ") + act),
170
- ]);
171
- }
172
-
173
- const queuedLine = queued.length > 0
174
- ? truncate(theme.fg("dim", "\u251C\u2500") + ` ${theme.fg("muted", "\u25E6")} ${theme.fg("dim", `${queued.length} queued`)}`)
175
- : undefined;
282
+ const { finishedLines, runningLines, queuedLine } = buildSections(
283
+ { running, queued, finished },
284
+ activityMap,
285
+ registry,
286
+ spinnerFrame,
287
+ theme,
288
+ truncate,
289
+ );
176
290
 
177
291
  // Assemble with overflow cap (heading takes 1 line).
178
292
  const maxBody = MAX_WIDGET_LINES - 1;
179
293
  const totalBody = finishedLines.length + runningLines.length * 2 + (queuedLine ? 1 : 0);
180
-
181
- const lines: string[] = [truncate(theme.fg(headingColor, headingIcon) + " " + theme.fg(headingColor, "Agents"))];
294
+ const heading = truncate(theme.fg(headingColor, headingIcon) + " " + theme.fg(headingColor, "Agents"));
182
295
 
183
296
  if (totalBody <= maxBody) {
184
- lines.push(...finishedLines);
185
- for (const pair of runningLines) lines.push(...pair);
186
- if (queuedLine) lines.push(queuedLine);
187
-
188
- // Fix last connector: swap \u251C\u2500 \u2192 \u2514\u2500 and \u2502 \u2192 space for activity lines.
189
- if (lines.length > 1) {
190
- const last = lines.length - 1;
191
- lines[last] = lines[last].replace("\u251C\u2500", "\u2514\u2500");
192
- if (runningLines.length > 0 && !queuedLine) {
193
- if (last >= 2) {
194
- lines[last - 1] = lines[last - 1].replace("\u251C\u2500", "\u2514\u2500");
195
- lines[last] = lines[last].replace("\u2502 ", " ");
196
- }
197
- }
198
- }
199
- } else {
200
- // Overflow — prioritize: running > queued > finished.
201
- let budget = maxBody - 1;
202
- let hiddenRunning = 0;
203
- let hiddenFinished = 0;
204
-
205
- for (const pair of runningLines) {
206
- if (budget >= 2) {
207
- lines.push(...pair);
208
- budget -= 2;
209
- } else {
210
- hiddenRunning++;
211
- }
212
- }
213
-
214
- if (queuedLine && budget >= 1) {
215
- lines.push(queuedLine);
216
- budget--;
217
- }
218
-
219
- for (const fl of finishedLines) {
220
- if (budget >= 1) {
221
- lines.push(fl);
222
- budget--;
223
- } else {
224
- hiddenFinished++;
225
- }
226
- }
227
-
228
- const overflowParts: string[] = [];
229
- if (hiddenRunning > 0) overflowParts.push(`${hiddenRunning} running`);
230
- if (hiddenFinished > 0) overflowParts.push(`${hiddenFinished} finished`);
231
- const overflowText = overflowParts.join(", ");
232
- lines.push(truncate(theme.fg("dim", "\u2514\u2500") + ` ${theme.fg("dim", `+${hiddenRunning + hiddenFinished} more (${overflowText})`)}`));
297
+ return assembleWithinBudget(heading, { finishedLines, runningLines, queuedLine });
233
298
  }
234
-
235
- return lines;
299
+ return assembleOverflow(heading, { finishedLines, runningLines, queuedLine }, maxBody, truncate, theme);
236
300
  }