@gotgenes/pi-subagents 6.16.0 → 6.16.2

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.
@@ -1,7 +1,6 @@
1
- import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import type { AgentSpawnConfig } from "../agent-manager.js";
3
1
  import { AgentTypeRegistry } from "../agent-types.js";
4
2
  import type { ModelRegistry } from "../model-resolver.js";
3
+ import type { ParentSnapshot } from "../parent-snapshot.js";
5
4
  import type { AgentConfig, AgentRecord } from "../types.js";
6
5
  import type { AgentActivityTracker } from "./agent-activity-tracker.js";
7
6
  import { createAgentConfigEditor } from "./agent-config-editor.js";
@@ -16,7 +15,12 @@ export interface AgentMenuManager {
16
15
  listAgents: () => AgentRecord[];
17
16
  getRecord: (id: string) => AgentRecord | undefined;
18
17
  /** Used by generate wizard to spawn an agent that writes the .md file. */
19
- spawnAndWait: (ctx: ExtensionContext, type: string, prompt: string, opts: Omit<AgentSpawnConfig, "isBackground">) => Promise<AgentRecord>;
18
+ spawnAndWait: (
19
+ parentSnapshot: ParentSnapshot,
20
+ type: string,
21
+ prompt: string,
22
+ opts: { description: string; maxTurns: number },
23
+ ) => Promise<AgentRecord>;
20
24
  }
21
25
 
22
26
  /** Narrow settings interface required by the agent menu. */
@@ -52,35 +56,58 @@ export interface AgentMenuDeps {
52
56
 
53
57
  // ---- Narrow UI context types ----
54
58
 
59
+ /** Narrow UI interface — only the ctx.ui methods menu handlers actually call. */
60
+ export interface MenuUI {
61
+ select(title: string, options: string[]): Promise<string | undefined>;
62
+ confirm(title: string, message: string): Promise<boolean>;
63
+ input(title: string, defaultValue?: string): Promise<string | undefined>;
64
+ notify(message: string, level: "info" | "warning" | "error"): void;
65
+ editor(title: string, content: string): Promise<string | undefined>;
66
+ custom<R>(component: any, options?: any): Promise<R>;
67
+ }
68
+
55
69
  // ---- Factory ----
56
70
 
57
71
  /**
58
72
  * Create the `/agents` command handler.
59
73
  * Returns a function suitable for `pi.registerCommand("agents", { handler })`.
60
74
  */
61
- export function createAgentsMenuHandler(deps: AgentMenuDeps) {
62
- const editor = createAgentConfigEditor({
63
- fileOps: deps.fileOps,
64
- registry: deps.registry,
65
- personalAgentsDir: deps.personalAgentsDir,
66
- projectAgentsDir: deps.projectAgentsDir,
67
- });
75
+ export function createAgentsMenuHandler({
76
+ manager,
77
+ registry,
78
+ agentActivity,
79
+ getModelLabel,
80
+ settings,
81
+ fileOps,
82
+ personalAgentsDir,
83
+ projectAgentsDir,
84
+ }: AgentMenuDeps) {
85
+ const editor = createAgentConfigEditor(
86
+ fileOps,
87
+ registry,
88
+ personalAgentsDir,
89
+ projectAgentsDir,
90
+ );
68
91
 
69
92
  const wizard = createAgentCreationWizard({
70
- fileOps: deps.fileOps,
71
- manager: deps.manager,
72
- registry: deps.registry,
73
- personalAgentsDir: deps.personalAgentsDir,
74
- projectAgentsDir: deps.projectAgentsDir,
93
+ fileOps,
94
+ manager,
95
+ registry,
96
+ personalAgentsDir,
97
+ projectAgentsDir,
75
98
  });
76
99
 
77
- async function showAgentsMenu(ctx: ExtensionContext) {
78
- deps.registry.reload();
79
- const allNames = deps.registry.getAllTypes();
100
+ async function showAgentsMenu(
101
+ ui: MenuUI,
102
+ modelRegistry: ModelRegistry,
103
+ parentSnapshot: ParentSnapshot,
104
+ ) {
105
+ registry.reload();
106
+ const allNames = registry.getAllTypes();
80
107
 
81
108
  const options: string[] = [];
82
109
 
83
- const agents = deps.manager.listAgents();
110
+ const agents = manager.listAgents();
84
111
  if (agents.length > 0) {
85
112
  const running = agents.filter(
86
113
  (a) => a.status === "running" || a.status === "queued",
@@ -108,30 +135,30 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
108
135
  : "";
109
136
 
110
137
  if (noAgentsMsg) {
111
- ctx.ui.notify(noAgentsMsg, "info");
138
+ ui.notify(noAgentsMsg, "info");
112
139
  }
113
140
 
114
- const choice = await ctx.ui.select("Agents", options);
141
+ const choice = await ui.select("Agents", options);
115
142
  if (!choice) return;
116
143
 
117
144
  if (choice.startsWith("Running agents (")) {
118
- await showRunningAgents(ctx);
119
- await showAgentsMenu(ctx);
145
+ await showRunningAgents(ui);
146
+ await showAgentsMenu(ui, modelRegistry, parentSnapshot);
120
147
  } else if (choice.startsWith("Agent types (")) {
121
- await showAllAgentsList(ctx);
122
- await showAgentsMenu(ctx);
148
+ await showAllAgentsList(ui, modelRegistry);
149
+ await showAgentsMenu(ui, modelRegistry, parentSnapshot);
123
150
  } else if (choice === "Create new agent") {
124
- await wizard.showCreateWizard(ctx);
151
+ await wizard.showCreateWizard(ui, parentSnapshot);
125
152
  } else if (choice === "Settings") {
126
- await showSettings(ctx);
127
- await showAgentsMenu(ctx);
153
+ await showSettings(ui);
154
+ await showAgentsMenu(ui, modelRegistry, parentSnapshot);
128
155
  }
129
156
  }
130
157
 
131
- async function showAllAgentsList(ctx: ExtensionContext) {
132
- const allNames = deps.registry.getAllTypes();
158
+ async function showAllAgentsList(ui: MenuUI, modelRegistry: ModelRegistry) {
159
+ const allNames = registry.getAllTypes();
133
160
  if (allNames.length === 0) {
134
- ctx.ui.notify("No agents.", "info");
161
+ ui.notify("No agents.", "info");
135
162
  return;
136
163
  }
137
164
 
@@ -144,9 +171,9 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
144
171
  };
145
172
 
146
173
  const entries = allNames.map((name) => {
147
- const cfg = deps.registry.resolveAgentConfig(name);
174
+ const cfg = registry.resolveAgentConfig(name);
148
175
  const disabled = cfg.enabled === false;
149
- const model = deps.getModelLabel(name, ctx.modelRegistry);
176
+ const model = getModelLabel(name, modelRegistry);
150
177
  const indicator = sourceIndicator(cfg);
151
178
  const prefix = `${indicator}${name} · ${model}`;
152
179
  const desc = disabled ? "(disabled)" : cfg.description;
@@ -155,10 +182,12 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
155
182
  const maxPrefix = Math.max(...entries.map((e) => e.prefix.length));
156
183
 
157
184
  const hasCustom = allNames.some((n) => {
158
- const c = deps.registry.resolveAgentConfig(n);
185
+ const c = registry.resolveAgentConfig(n);
159
186
  return !c.isDefault && c.enabled !== false;
160
187
  });
161
- const hasDisabled = allNames.some((n) => deps.registry.resolveAgentConfig(n).enabled === false);
188
+ const hasDisabled = allNames.some(
189
+ (n) => registry.resolveAgentConfig(n).enabled === false,
190
+ );
162
191
  const legendParts: string[] = [];
163
192
  if (hasCustom) legendParts.push("• = project ◦ = global");
164
193
  if (hasDisabled) legendParts.push("✕ = disabled");
@@ -169,47 +198,47 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
169
198
  );
170
199
  if (legend) options.push(legend);
171
200
 
172
- const choice = await ctx.ui.select("Agent types", options);
201
+ const choice = await ui.select("Agent types", options);
173
202
  if (!choice) return;
174
203
 
175
204
  const agentName = choice
176
205
  .split(" · ")[0]
177
206
  .replace(/^[•◦✕\s]+/, "")
178
207
  .trim();
179
- if (deps.registry.resolveType(agentName) != null) {
180
- await editor.showAgentDetail(ctx, agentName);
181
- await showAllAgentsList(ctx);
208
+ if (registry.resolveType(agentName) != null) {
209
+ await editor.showAgentDetail(ui, agentName);
210
+ await showAllAgentsList(ui, modelRegistry);
182
211
  }
183
212
  }
184
213
 
185
- async function showRunningAgents(ctx: ExtensionContext) {
186
- const agents = deps.manager.listAgents();
214
+ async function showRunningAgents(ui: MenuUI) {
215
+ const agents = manager.listAgents();
187
216
  if (agents.length === 0) {
188
- ctx.ui.notify("No agents.", "info");
217
+ ui.notify("No agents.", "info");
189
218
  return;
190
219
  }
191
220
 
192
221
  const options = agents.map((a) => {
193
- const dn = getDisplayName(a.type, deps.registry);
222
+ const dn = getDisplayName(a.type, registry);
194
223
  const dur = formatDuration(a.startedAt, a.completedAt);
195
224
  return `${dn} (${a.description}) · ${a.toolUses} tools · ${a.status} · ${dur}`;
196
225
  });
197
226
 
198
- const choice = await ctx.ui.select("Running agents", options);
227
+ const choice = await ui.select("Running agents", options);
199
228
  if (!choice) return;
200
229
 
201
230
  const idx = options.indexOf(choice);
202
231
  if (idx < 0) return;
203
232
  const record = agents[idx];
204
233
 
205
- await viewAgentConversation(ctx, record);
206
- await showRunningAgents(ctx);
234
+ await viewAgentConversation(ui, record);
235
+ await showRunningAgents(ui);
207
236
  }
208
237
 
209
- async function viewAgentConversation(ctx: ExtensionContext, record: AgentRecord) {
238
+ async function viewAgentConversation(ui: MenuUI, record: AgentRecord) {
210
239
  const session = record.session;
211
240
  if (!session) {
212
- ctx.ui.notify(
241
+ ui.notify(
213
242
  `Agent is ${record.status === "queued" ? "queued" : "expired"} — no session available.`,
214
243
  "info",
215
244
  );
@@ -219,11 +248,19 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
219
248
  const { ConversationViewer, VIEWPORT_HEIGHT_PCT } = await import(
220
249
  "./conversation-viewer.js"
221
250
  );
222
- const activity = deps.agentActivity.get(record.id);
251
+ const activity = agentActivity.get(record.id);
223
252
 
224
- await ctx.ui.custom<undefined>(
253
+ await ui.custom<undefined>(
225
254
  (tui: any, theme: any, _keybindings: any, done: any) => {
226
- return new ConversationViewer({ tui, session, record, activity, theme, done, registry: deps.registry });
255
+ return new ConversationViewer({
256
+ tui,
257
+ session,
258
+ record,
259
+ activity,
260
+ theme,
261
+ done,
262
+ registry,
263
+ });
227
264
  },
228
265
  {
229
266
  overlay: true,
@@ -236,61 +273,68 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
236
273
  );
237
274
  }
238
275
 
239
- async function showSettings(ctx: ExtensionContext) {
240
- const choice = await ctx.ui.select("Settings", [
241
- `Max concurrency (current: ${deps.settings.maxConcurrent})`,
242
- `Default max turns (current: ${deps.settings.defaultMaxTurns ?? "unlimited"})`,
243
- `Grace turns (current: ${deps.settings.graceTurns})`,
276
+ async function showSettings(ui: MenuUI) {
277
+ const choice = await ui.select("Settings", [
278
+ `Max concurrency (current: ${settings.maxConcurrent})`,
279
+ `Default max turns (current: ${settings.defaultMaxTurns ?? "unlimited"})`,
280
+ `Grace turns (current: ${settings.graceTurns})`,
244
281
  ]);
245
282
  if (!choice) return;
246
283
 
247
284
  if (choice.startsWith("Max concurrency")) {
248
- const val = await ctx.ui.input(
285
+ const val = await ui.input(
249
286
  "Max concurrent background agents",
250
- String(deps.settings.maxConcurrent),
287
+ String(settings.maxConcurrent),
251
288
  );
252
289
  if (val) {
253
290
  const n = parseInt(val, 10);
254
291
  if (n >= 1) {
255
- const toast = deps.settings.applyMaxConcurrent(n);
256
- ctx.ui.notify(toast.message, toast.level);
292
+ const toast = settings.applyMaxConcurrent(n);
293
+ ui.notify(toast.message, toast.level);
257
294
  } else {
258
- ctx.ui.notify("Must be a positive integer.", "warning");
295
+ ui.notify("Must be a positive integer.", "warning");
259
296
  }
260
297
  }
261
298
  } else if (choice.startsWith("Default max turns")) {
262
- const val = await ctx.ui.input(
299
+ const val = await ui.input(
263
300
  "Default max turns before wrap-up (0 = unlimited)",
264
- String(deps.settings.defaultMaxTurns ?? 0),
301
+ String(settings.defaultMaxTurns ?? 0),
265
302
  );
266
303
  if (val) {
267
304
  const n = parseInt(val, 10);
268
305
  if (n >= 0) {
269
- const toast = deps.settings.applyDefaultMaxTurns(n);
270
- ctx.ui.notify(toast.message, toast.level);
306
+ const toast = settings.applyDefaultMaxTurns(n);
307
+ ui.notify(toast.message, toast.level);
271
308
  } else {
272
- ctx.ui.notify("Must be 0 (unlimited) or a positive integer.", "warning");
309
+ ui.notify("Must be 0 (unlimited) or a positive integer.", "warning");
273
310
  }
274
311
  }
275
312
  } else if (choice.startsWith("Grace turns")) {
276
- const val = await ctx.ui.input(
313
+ const val = await ui.input(
277
314
  "Grace turns after wrap-up steer",
278
- String(deps.settings.graceTurns),
315
+ String(settings.graceTurns),
279
316
  );
280
317
  if (val) {
281
318
  const n = parseInt(val, 10);
282
319
  if (n >= 1) {
283
- const toast = deps.settings.applyGraceTurns(n);
284
- ctx.ui.notify(toast.message, toast.level);
320
+ const toast = settings.applyGraceTurns(n);
321
+ ui.notify(toast.message, toast.level);
285
322
  } else {
286
- ctx.ui.notify("Must be a positive integer.", "warning");
323
+ ui.notify("Must be a positive integer.", "warning");
287
324
  }
288
325
  }
289
326
  }
290
327
  }
291
328
 
292
- // Return the handler function
293
- return async (ctx: ExtensionContext) => {
294
- await showAgentsMenu(ctx);
329
+ return async ({
330
+ ui,
331
+ modelRegistry,
332
+ parentSnapshot,
333
+ }: {
334
+ ui: MenuUI;
335
+ modelRegistry: ModelRegistry;
336
+ parentSnapshot: ParentSnapshot;
337
+ }) => {
338
+ await showAgentsMenu(ui, modelRegistry, parentSnapshot);
295
339
  };
296
340
  }
@@ -5,28 +5,11 @@
5
5
  * Uses the callback form of setWidget for themed rendering.
6
6
  */
7
7
 
8
- import { truncateToWidth } from "@earendil-works/pi-tui";
9
8
  import type { AgentManager } from "../agent-manager.js";
10
9
  import { AgentTypeRegistry } from "../agent-types.js";
11
- import type { SubagentType } from "../types.js";
12
- import { getLifetimeTotal, getSessionContextPercent } from "../usage.js";
13
10
  import type { AgentActivityTracker } from "./agent-activity-tracker.js";
14
- import {
15
- describeActivity,
16
- ERROR_STATUSES,
17
- formatMs,
18
- formatSessionTokens,
19
- formatTurns,
20
- getDisplayName,
21
- getPromptModeLabel,
22
- SPINNER,
23
- type Theme,
24
- } from "./display.js";
25
-
26
- // ---- Constants ----
27
-
28
- /** Maximum number of rendered lines before overflow collapse kicks in. */
29
- const MAX_WIDGET_LINES = 12;
11
+ import { ERROR_STATUSES, type Theme } from "./display.js";
12
+ import { renderWidgetLines } from "./widget-renderer.js";
30
13
 
31
14
  // ---- Types ----
32
15
 
@@ -113,176 +96,17 @@ export class AgentWidget {
113
96
  }
114
97
  }
115
98
 
116
- /** Render a finished agent line. */
117
- private renderFinishedLine(a: { id: string; type: SubagentType; status: string; description: string; toolUses: number; startedAt: number; completedAt?: number; error?: string }, theme: Theme): string {
118
- const name = getDisplayName(a.type, this.registry);
119
- const modeLabel = getPromptModeLabel(a.type, this.registry);
120
- const duration = formatMs((a.completedAt ?? Date.now()) - a.startedAt);
121
-
122
- let icon: string;
123
- let statusText: string;
124
- if (a.status === "completed") {
125
- icon = theme.fg("success", "✓");
126
- statusText = "";
127
- } else if (a.status === "steered") {
128
- icon = theme.fg("warning", "✓");
129
- statusText = theme.fg("warning", " (turn limit)");
130
- } else if (a.status === "stopped") {
131
- icon = theme.fg("dim", "■");
132
- statusText = theme.fg("dim", " stopped");
133
- } else if (a.status === "error") {
134
- icon = theme.fg("error", "✗");
135
- const errMsg = a.error ? `: ${a.error.slice(0, 60)}` : "";
136
- statusText = theme.fg("error", ` error${errMsg}`);
137
- } else {
138
- // aborted
139
- icon = theme.fg("error", "✗");
140
- statusText = theme.fg("warning", " aborted");
141
- }
142
-
143
- const parts: string[] = [];
144
- const activity = this.agentActivity.get(a.id);
145
- if (activity) parts.push(formatTurns(activity.turnCount, activity.maxTurns));
146
- if (a.toolUses > 0) parts.push(`${a.toolUses} tool use${a.toolUses === 1 ? "" : "s"}`);
147
- parts.push(duration);
148
-
149
- const modeTag = modeLabel ? ` ${theme.fg("dim", `(${modeLabel})`)}` : "";
150
- return `${icon} ${theme.fg("dim", name)}${modeTag} ${theme.fg("dim", a.description)} ${theme.fg("dim", "·")} ${theme.fg("dim", parts.join(" · "))}${statusText}`;
151
- }
152
-
153
- /**
154
- * Render the widget content. Called from the registered widget's render() callback,
155
- * reading live state each time instead of capturing it in a closure.
156
- */
99
+ /** Delegate rendering to the pure widget-renderer module. */
157
100
  private renderWidget(tui: any, theme: Theme): string[] {
158
- const allAgents = this.manager.listAgents();
159
- const running = allAgents.filter(a => a.status === "running");
160
- const queued = allAgents.filter(a => a.status === "queued");
161
- const finished = allAgents.filter(a =>
162
- a.status !== "running" && a.status !== "queued" && a.completedAt
163
- && this.shouldShowFinished(a.id, a.status),
164
- );
165
-
166
- const hasActive = running.length > 0 || queued.length > 0;
167
- const hasFinished = finished.length > 0;
168
-
169
- // Nothing to show — return empty (widget will be unregistered by update())
170
- if (!hasActive && !hasFinished) return [];
171
-
172
- const w = tui.terminal.columns;
173
- const truncate = (line: string) => truncateToWidth(line, w);
174
- const headingColor = hasActive ? "accent" : "dim";
175
- const headingIcon = hasActive ? "●" : "○";
176
- const frame = SPINNER[this.widgetFrame % SPINNER.length];
177
-
178
- // Build sections separately for overflow-aware assembly.
179
- // Each running agent = 2 lines (header + activity), finished = 1 line, queued = 1 line.
180
-
181
- const finishedLines: string[] = [];
182
- for (const a of finished) {
183
- finishedLines.push(truncate(theme.fg("dim", "├─") + " " + this.renderFinishedLine(a, theme)));
184
- }
185
-
186
- const runningLines: string[][] = []; // each entry is [header, activity]
187
- for (const a of running) {
188
- const name = getDisplayName(a.type, this.registry);
189
- const modeLabel = getPromptModeLabel(a.type, this.registry);
190
- const modeTag = modeLabel ? ` ${theme.fg("dim", `(${modeLabel})`)}` : "";
191
- const elapsed = formatMs(Date.now() - a.startedAt);
192
-
193
- const bg = this.agentActivity.get(a.id);
194
- const tokens = getLifetimeTotal(a.lifetimeUsage);
195
- const contextPercent = getSessionContextPercent(a.session);
196
- const tokenText = tokens > 0 ? formatSessionTokens(tokens, contextPercent, theme, a.compactionCount) : "";
197
-
198
- const parts: string[] = [];
199
- if (bg) parts.push(formatTurns(bg.turnCount, bg.maxTurns));
200
- if (a.toolUses > 0) parts.push(`${a.toolUses} tool use${a.toolUses === 1 ? "" : "s"}`);
201
- if (tokenText) parts.push(tokenText);
202
- parts.push(elapsed);
203
- const statsText = parts.join(" · ");
204
-
205
- const activity = bg ? describeActivity(bg.activeTools, bg.responseText) : "thinking…";
206
-
207
- runningLines.push([
208
- truncate(theme.fg("dim", "├─") + ` ${theme.fg("accent", frame)} ${theme.bold(name)}${modeTag} ${theme.fg("muted", a.description)} ${theme.fg("dim", "·")} ${theme.fg("dim", statsText)}`),
209
- truncate(theme.fg("dim", "│ ") + theme.fg("dim", ` ⎿ ${activity}`)),
210
- ]);
211
- }
212
-
213
- const queuedLine = queued.length > 0
214
- ? truncate(theme.fg("dim", "├─") + ` ${theme.fg("muted", "◦")} ${theme.fg("dim", `${queued.length} queued`)}`)
215
- : undefined;
216
-
217
- // Assemble with overflow cap (heading + overflow indicator = 2 reserved lines).
218
- const maxBody = MAX_WIDGET_LINES - 1; // heading takes 1 line
219
- const totalBody = finishedLines.length + runningLines.length * 2 + (queuedLine ? 1 : 0);
220
-
221
- const lines: string[] = [truncate(theme.fg(headingColor, headingIcon) + " " + theme.fg(headingColor, "Agents"))];
222
-
223
- if (totalBody <= maxBody) {
224
- // Everything fits — add all lines and fix up connectors for the last item.
225
- lines.push(...finishedLines);
226
- for (const pair of runningLines) lines.push(...pair);
227
- if (queuedLine) lines.push(queuedLine);
228
-
229
- // Fix last connector: swap ├─ → └─ and │ → space for activity lines.
230
- if (lines.length > 1) {
231
- const last = lines.length - 1;
232
- lines[last] = lines[last].replace("├─", "└─");
233
- // If last item is a running agent activity line, fix indent of that line
234
- // and fix the header line above it.
235
- if (runningLines.length > 0 && !queuedLine) {
236
- // The last two lines are the last running agent's header + activity.
237
- if (last >= 2) {
238
- lines[last - 1] = lines[last - 1].replace("├─", "└─");
239
- lines[last] = lines[last].replace("│ ", " ");
240
- }
241
- }
242
- }
243
- } else {
244
- // Overflow — prioritize: running > queued > finished.
245
- // Reserve 1 line for overflow indicator.
246
- let budget = maxBody - 1;
247
- let hiddenRunning = 0;
248
- let hiddenFinished = 0;
249
-
250
- // 1. Running agents (2 lines each)
251
- for (const pair of runningLines) {
252
- if (budget >= 2) {
253
- lines.push(...pair);
254
- budget -= 2;
255
- } else {
256
- hiddenRunning++;
257
- }
258
- }
259
-
260
- // 2. Queued line
261
- if (queuedLine && budget >= 1) {
262
- lines.push(queuedLine);
263
- budget--;
264
- }
265
-
266
- // 3. Finished agents
267
- for (const fl of finishedLines) {
268
- if (budget >= 1) {
269
- lines.push(fl);
270
- budget--;
271
- } else {
272
- hiddenFinished++;
273
- }
274
- }
275
-
276
- // Overflow summary
277
- const overflowParts: string[] = [];
278
- if (hiddenRunning > 0) overflowParts.push(`${hiddenRunning} running`);
279
- if (hiddenFinished > 0) overflowParts.push(`${hiddenFinished} finished`);
280
- const overflowText = overflowParts.join(", ");
281
- lines.push(truncate(theme.fg("dim", "└─") + ` ${theme.fg("dim", `+${hiddenRunning + hiddenFinished} more (${overflowText})`)}`)
282
- );
283
- }
284
-
285
- return lines;
101
+ return renderWidgetLines({
102
+ agents: this.manager.listAgents(),
103
+ activityMap: this.agentActivity,
104
+ registry: this.registry,
105
+ spinnerFrame: this.widgetFrame,
106
+ terminalWidth: tui.terminal.columns,
107
+ theme,
108
+ shouldShowFinished: (id, status) => this.shouldShowFinished(id, status),
109
+ });
286
110
  }
287
111
 
288
112
  /** Force an immediate widget update. */
package/src/ui/display.ts CHANGED
@@ -94,7 +94,8 @@ export function formatSessionTokens(
94
94
  annot.push(theme.fg("dim", `↻${compactions}`));
95
95
  }
96
96
  if (annot.length === 0) return tokenStr;
97
- return `${tokenStr} (${annot.join(" · ")})`;
97
+ const sep = theme.fg("dim", " · ");
98
+ return `${tokenStr} ${theme.fg("dim", "(")}${annot.join(sep)}${theme.fg("dim", ")")}`;
98
99
  }
99
100
 
100
101
  /** Format turn count with optional max limit: "⟳5≤30" or "⟳5". */