@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.
- package/CHANGELOG.md +21 -0
- package/docs/architecture/architecture.md +16 -150
- package/docs/architecture/history/phase-11-closure-to-class.md +100 -0
- package/docs/plans/0196-convert-runner-menu-to-classes.md +268 -0
- package/docs/plans/0205-decompose-render-widget-lines.md +140 -0
- package/docs/retro/0196-convert-runner-menu-to-classes.md +73 -0
- package/docs/retro/0205-decompose-render-widget-lines.md +36 -0
- package/package.json +1 -1
- package/src/index.ts +14 -28
- package/src/lifecycle/agent-runner.ts +12 -6
- package/src/ui/agent-menu.ts +96 -94
- package/src/ui/widget-renderer.ts +141 -77
package/src/ui/agent-menu.ts
CHANGED
|
@@ -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
|
|
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
|
-
// ----
|
|
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
|
-
// ----
|
|
59
|
+
// ---- Class ----
|
|
72
60
|
|
|
73
61
|
/**
|
|
74
|
-
*
|
|
75
|
-
*
|
|
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
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
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
|
}
|