@pi-unipi/subagents 0.1.13 → 0.2.3

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 (48) hide show
  1. package/package.json +1 -1
  2. package/src/__tests__/badge-generation.test.ts +244 -0
  3. package/src/agent-manager.ts +12 -1
  4. package/src/agent-runner.ts +23 -8
  5. package/src/conversation-viewer.ts +299 -0
  6. package/src/index.ts +432 -49
  7. package/src/types.ts +49 -0
  8. package/src/widget.ts +332 -72
  9. package/dist/agent-manager.d.ts +0 -72
  10. package/dist/agent-manager.d.ts.map +0 -1
  11. package/dist/agent-manager.js +0 -258
  12. package/dist/agent-manager.js.map +0 -1
  13. package/dist/agent-runner.d.ts +0 -50
  14. package/dist/agent-runner.d.ts.map +0 -1
  15. package/dist/agent-runner.js +0 -238
  16. package/dist/agent-runner.js.map +0 -1
  17. package/dist/config.d.ts +0 -24
  18. package/dist/config.d.ts.map +0 -1
  19. package/dist/config.js +0 -132
  20. package/dist/config.js.map +0 -1
  21. package/dist/custom-agents.d.ts +0 -14
  22. package/dist/custom-agents.d.ts.map +0 -1
  23. package/dist/custom-agents.js +0 -106
  24. package/dist/custom-agents.js.map +0 -1
  25. package/dist/file-lock.d.ts +0 -42
  26. package/dist/file-lock.d.ts.map +0 -1
  27. package/dist/file-lock.js +0 -91
  28. package/dist/file-lock.js.map +0 -1
  29. package/dist/index.d.ts +0 -9
  30. package/dist/index.d.ts.map +0 -1
  31. package/dist/index.js +0 -301
  32. package/dist/index.js.map +0 -1
  33. package/dist/model-resolver.d.ts +0 -19
  34. package/dist/model-resolver.d.ts.map +0 -1
  35. package/dist/model-resolver.js +0 -61
  36. package/dist/model-resolver.js.map +0 -1
  37. package/dist/prompts.d.ts +0 -13
  38. package/dist/prompts.d.ts.map +0 -1
  39. package/dist/prompts.js +0 -31
  40. package/dist/prompts.js.map +0 -1
  41. package/dist/types.d.ts +0 -79
  42. package/dist/types.d.ts.map +0 -1
  43. package/dist/types.js +0 -6
  44. package/dist/types.js.map +0 -1
  45. package/dist/widget.d.ts +0 -26
  46. package/dist/widget.d.ts.map +0 -1
  47. package/dist/widget.js +0 -162
  48. package/dist/widget.js.map +0 -1
package/src/types.ts CHANGED
@@ -13,6 +13,39 @@ export type AgentType = string;
13
13
  /** Built-in agent type names. */
14
14
  export const BUILTIN_TYPES = ["explore", "work"] as const;
15
15
 
16
+ /** Read-only tool names for explore agents. */
17
+ const READ_ONLY_TOOLS = ["read", "bash", "grep", "find", "ls"];
18
+
19
+ /** All write-capable tool names. */
20
+ const ALL_TOOLS = ["read", "bash", "edit", "write", "grep", "find", "ls"];
21
+
22
+ /** Built-in agent configurations. */
23
+ export const BUILTIN_CONFIGS: Record<string, AgentConfig> = {
24
+ explore: {
25
+ name: "explore",
26
+ displayName: "Explore",
27
+ description: "Read-only exploration agent for parallel file reads and searches.",
28
+ builtinToolNames: READ_ONLY_TOOLS,
29
+ disallowedTools: ["edit", "write"],
30
+ extensions: false,
31
+ skills: false,
32
+ systemPrompt: "You are an explore agent. Read files, search code, and report findings. Do NOT modify any files.",
33
+ promptMode: "append",
34
+ source: "builtin",
35
+ },
36
+ work: {
37
+ name: "work",
38
+ displayName: "Worker",
39
+ description: "Write-capable worker agent with transparent file locking.",
40
+ builtinToolNames: ALL_TOOLS,
41
+ extensions: false,
42
+ skills: false,
43
+ systemPrompt: "You are a worker agent. Implement changes, write code, and complete tasks. Use the provided tools to make the requested modifications.",
44
+ promptMode: "append",
45
+ source: "builtin",
46
+ },
47
+ } as const;
48
+
16
49
  /** Memory scope for persistent agent memory. */
17
50
  export type MemoryScope = "user" | "project" | "local";
18
51
 
@@ -84,3 +117,19 @@ export interface AgentActivity {
84
117
  responseText: string;
85
118
  session?: AgentSession;
86
119
  }
120
+
121
+ /** Details attached to custom notification messages for visual rendering. */
122
+ export interface NotificationDetails {
123
+ id: string;
124
+ description: string;
125
+ status: string;
126
+ toolUses: number;
127
+ turnCount: number;
128
+ maxTurns?: number;
129
+ totalTokens: number;
130
+ durationMs: number;
131
+ error?: string;
132
+ resultPreview: string;
133
+ /** Additional agents in a group notification. */
134
+ others?: NotificationDetails[];
135
+ }
package/src/widget.ts CHANGED
@@ -1,16 +1,43 @@
1
1
  /**
2
2
  * @pi-unipi/subagents — Live widget
3
3
  *
4
- * Shows running agents above the editor.
5
- * Uses setWidget with tui.requestRender() for updates.
4
+ * Shows running/completed agents above the editor with:
5
+ * - Animated braille spinners
6
+ * - Finished agent lingering (1 turn success, 2 turns error)
7
+ * - Priority-based overflow (running > queued > finished)
8
+ * - Status bar integration
9
+ * - Activity description grouping
10
+ * - ANSI-aware truncation via pi-tui
6
11
  */
7
12
 
13
+ import { truncateToWidth } from "@mariozechner/pi-tui";
8
14
  import type { AgentManager } from "./agent-manager.js";
9
15
  import type { AgentActivity } from "./types.js";
10
16
 
11
- /** Spinner frames (braille). */
17
+ // ---- Constants ----
18
+
19
+ /** Maximum lines the widget may render. */
20
+ const MAX_WIDGET_LINES = 12;
21
+
22
+ /** Braille spinner frames. */
12
23
  const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
13
24
 
25
+ /** Statuses that indicate error/non-success (for linger behavior). */
26
+ const ERROR_STATUSES = new Set(["error", "aborted", "stopped"]);
27
+
28
+ /** Tool name → human-readable action for activity descriptions. */
29
+ const TOOL_DISPLAY: Record<string, string> = {
30
+ read: "reading",
31
+ bash: "running command",
32
+ edit: "editing",
33
+ write: "writing",
34
+ grep: "searching",
35
+ find: "finding files",
36
+ ls: "listing",
37
+ };
38
+
39
+ // ---- Formatting helpers ----
40
+
14
41
  /** Format duration. */
15
42
  function formatMs(ms: number): string {
16
43
  if (ms >= 60_000) return `${(ms / 60_000).toFixed(1)}m`;
@@ -18,62 +45,105 @@ function formatMs(ms: number): string {
18
45
  return `${ms}ms`;
19
46
  }
20
47
 
21
- /** Format turns. */
48
+ /** Format turns with optional max limit: "⟳5≤30" or "⟳5". */
22
49
  function formatTurns(turn: number, max?: number): string {
23
- return max ? `⟳${turn}≤${max}` : `⟳${turn}`;
50
+ return max != null ? `⟳${turn}≤${max}` : `⟳${turn}`;
24
51
  }
25
52
 
26
- /** Describe current activity from active tools. */
27
- function describeActivity(activeTools: Map<string, string>, responseText: string): string {
53
+ /** Format token count compactly: "33.8k token", "1.2M token". */
54
+ function formatTokens(count: number): string {
55
+ if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M token`;
56
+ if (count >= 1_000) return `${(count / 1_000).toFixed(1)}k token`;
57
+ return `${count} token`;
58
+ }
59
+
60
+ /**
61
+ * Build a human-readable activity string from currently-running tools.
62
+ * Groups by tool type with counts: "reading 3 files, searching 2 patterns".
63
+ */
64
+ function describeActivity(activeTools: Map<string, string>, responseText?: string): string {
28
65
  if (activeTools.size > 0) {
29
- const names = [...new Set(activeTools.values())];
30
- return names.join(", ") + "…";
66
+ const groups = new Map<string, number>();
67
+ for (const toolName of activeTools.values()) {
68
+ const action = TOOL_DISPLAY[toolName] ?? toolName;
69
+ groups.set(action, (groups.get(action) ?? 0) + 1);
70
+ }
71
+
72
+ const parts: string[] = [];
73
+ for (const [action, count] of groups) {
74
+ if (count > 1) {
75
+ parts.push(`${action} ${count} ${action === "searching" ? "patterns" : "files"}`);
76
+ } else {
77
+ parts.push(action);
78
+ }
79
+ }
80
+ return parts.join(", ") + "…";
31
81
  }
32
- if (responseText) {
33
- const lastLine = responseText.split("\n").pop()?.trim() ?? "";
34
- if (lastLine.length > 0) return lastLine.slice(0, 60) + (lastLine.length > 60 ? "" : "");
82
+
83
+ if (responseText && responseText.trim().length > 0) {
84
+ const lastLine = responseText.split("\n").find((l) => l.trim())?.trim() ?? "";
85
+ if (lastLine.length > 60) return lastLine.slice(0, 60) + "…";
86
+ if (lastLine.length > 0) return lastLine;
35
87
  }
88
+
36
89
  return "thinking…";
37
90
  }
38
91
 
92
+ // ---- Widget ----
93
+
39
94
  export class AgentWidget {
40
- private manager: AgentManager;
41
- private activity: Map<string, AgentActivity>;
42
95
  private spinnerFrame = 0;
43
96
  private timer?: ReturnType<typeof setInterval>;
44
97
  private uiCtx?: any;
45
98
  private tui?: any;
46
99
  private widgetRegistered = false;
100
+ /** Last content key — skips requestRender when only spinner changed. */
101
+ private lastContentKey = "";
47
102
 
48
- constructor(manager: AgentManager, activity: Map<string, AgentActivity>) {
49
- this.manager = manager;
50
- this.activity = activity;
51
- }
103
+ /** Tracks how many turns each finished agent has survived. */
104
+ private finishedTurnAge = new Map<string, number>();
105
+ /** How many extra turns error/aborted agents linger. */
106
+ private static readonly ERROR_LINGER_TURNS = 2;
107
+ /** Last status bar text for dedup. */
108
+ private lastStatusText: string | undefined;
109
+
110
+ constructor(
111
+ private manager: AgentManager,
112
+ private activity: Map<string, AgentActivity>,
113
+ ) {}
52
114
 
53
115
  setUICtx(ctx: any) {
54
116
  if (ctx !== this.uiCtx) {
55
117
  this.uiCtx = ctx;
56
118
  this.widgetRegistered = false;
57
119
  this.tui = undefined;
120
+ this.lastStatusText = undefined;
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Called on each new turn (tool_execution_start).
126
+ * Ages finished agents and clears those that have lingered long enough.
127
+ */
128
+ onTurnStart() {
129
+ for (const [id, age] of this.finishedTurnAge) {
130
+ this.finishedTurnAge.set(id, age + 1);
58
131
  }
132
+ this.update();
59
133
  }
60
134
 
61
135
  ensureTimer() {
62
136
  if (this.timer) return;
63
137
  this.timer = setInterval(() => {
64
138
  this.spinnerFrame = (this.spinnerFrame + 1) % SPINNER.length;
65
- this.triggerRender();
139
+ if (this.lastContentKey) this.triggerRender();
66
140
  }, 80);
67
141
  }
68
142
 
69
- markFinished(_id: string) {
70
- if (!this.manager.hasRunning()) {
71
- if (this.timer) {
72
- clearInterval(this.timer);
73
- this.timer = undefined;
74
- }
75
- // Clear widget after agent finishes
76
- this.triggerRender();
143
+ /** Record an agent as finished (call when agent completes). */
144
+ markFinished(agentId: string) {
145
+ if (!this.finishedTurnAge.has(agentId)) {
146
+ this.finishedTurnAge.set(agentId, 0);
77
147
  }
78
148
  }
79
149
 
@@ -81,23 +151,62 @@ export class AgentWidget {
81
151
  this.triggerRender();
82
152
  }
83
153
 
154
+ /** Check if a finished agent should still be shown. */
155
+ private shouldShowFinished(agentId: string, status: string): boolean {
156
+ const age = this.finishedTurnAge.get(agentId) ?? 0;
157
+ const maxAge = ERROR_STATUSES.has(status) ? AgentWidget.ERROR_LINGER_TURNS : 1;
158
+ return age < maxAge;
159
+ }
160
+
161
+ /**
162
+ * Build a content key capturing what's actually visible.
163
+ * Excludes spinner — spinner-only changes skip requestRender.
164
+ */
165
+ private buildContentKey(): string {
166
+ const allAgents = this.manager.listAgents();
167
+ const parts: string[] = [];
168
+ for (const a of allAgents) {
169
+ if (a.status === "running" || a.status === "queued") {
170
+ const act = this.activity.get(a.id);
171
+ parts.push(
172
+ `${a.id}:${a.status}:${a.toolUses}:${act?.turnCount ?? 0}:${act?.tokens ?? ""}:${describeActivity(act?.activeTools ?? new Map(), act?.responseText ?? "")}`,
173
+ );
174
+ } else if (a.completedAt && this.shouldShowFinished(a.id, a.status)) {
175
+ parts.push(`${a.id}:${a.status}:${a.toolUses}:finished`);
176
+ }
177
+ }
178
+ return parts.join("|");
179
+ }
180
+
84
181
  private triggerRender() {
85
182
  if (!this.uiCtx) return;
86
183
 
87
- const allAgents = this.manager.listAgents();
88
- const running = allAgents.filter((a) => a.status === "running" || a.status === "queued");
89
- const hasActive = running.length > 0;
184
+ const contentKey = this.buildContentKey();
185
+ const hasContent = contentKey.length > 0;
90
186
 
91
187
  // Nothing to show — clear widget
92
- if (!hasActive) {
188
+ if (!hasContent) {
93
189
  if (this.widgetRegistered) {
94
190
  this.uiCtx.setWidget("unipi-agents", undefined);
95
191
  this.widgetRegistered = false;
96
192
  this.tui = undefined;
193
+ this.lastContentKey = "";
194
+ }
195
+ if (this.lastStatusText !== undefined) {
196
+ this.uiCtx.setStatus?.("subagents", undefined);
197
+ this.lastStatusText = undefined;
198
+ }
199
+ // Clean up stale finished entries
200
+ const allAgents = this.manager.listAgents();
201
+ for (const [id] of this.finishedTurnAge) {
202
+ if (!allAgents.some((a) => a.id === id)) this.finishedTurnAge.delete(id);
97
203
  }
98
204
  return;
99
205
  }
100
206
 
207
+ // Status bar
208
+ this.updateStatusBar();
209
+
101
210
  // Register widget callback once
102
211
  if (!this.widgetRegistered) {
103
212
  this.uiCtx.setWidget(
@@ -105,7 +214,7 @@ export class AgentWidget {
105
214
  (tui: any, theme: any) => {
106
215
  this.tui = tui;
107
216
  return {
108
- render: () => this.renderWidget(tui, theme),
217
+ render: (width: number) => this.renderWidget(tui, theme, width),
109
218
  invalidate: () => {
110
219
  this.widgetRegistered = false;
111
220
  this.tui = undefined;
@@ -115,65 +224,210 @@ export class AgentWidget {
115
224
  { placement: "aboveEditor" },
116
225
  );
117
226
  this.widgetRegistered = true;
227
+ this.lastContentKey = "";
118
228
  }
119
229
 
120
- // Request re-render via TUI
121
- this.tui?.requestRender?.();
230
+ // Only request render when content actually changed
231
+ if (contentKey !== this.lastContentKey) {
232
+ this.lastContentKey = contentKey;
233
+ this.tui?.requestRender?.();
234
+ }
122
235
  }
123
236
 
124
- private renderWidget(tui: any, theme: any): string[] {
237
+ private updateStatusBar() {
238
+ if (!this.uiCtx?.setStatus) return;
239
+ const allAgents = this.manager.listAgents();
240
+ let runningCount = 0;
241
+ let queuedCount = 0;
242
+ for (const a of allAgents) {
243
+ if (a.status === "running") runningCount++;
244
+ else if (a.status === "queued") queuedCount++;
245
+ }
246
+
247
+ let newStatusText: string | undefined;
248
+ if (runningCount > 0 || queuedCount > 0) {
249
+ const parts: string[] = [];
250
+ if (runningCount > 0) parts.push(`${runningCount} running`);
251
+ if (queuedCount > 0) parts.push(`${queuedCount} queued`);
252
+ const total = runningCount + queuedCount;
253
+ newStatusText = `${parts.join(", ")} agent${total === 1 ? "" : "s"}`;
254
+ }
255
+
256
+ if (newStatusText !== this.lastStatusText) {
257
+ this.uiCtx.setStatus("subagents", newStatusText);
258
+ this.lastStatusText = newStatusText;
259
+ }
260
+ }
261
+
262
+ /** Render a finished agent line. */
263
+ private renderFinishedLine(
264
+ a: { id: string; type: string; status: string; description: string; toolUses: number; startedAt: number; completedAt?: number; error?: string },
265
+ theme: any,
266
+ w: number,
267
+ ): string {
268
+ const duration = formatMs((a.completedAt ?? Date.now()) - a.startedAt);
269
+
270
+ let icon: string;
271
+ let statusText: string;
272
+ if (a.status === "completed") {
273
+ icon = theme.fg("success", "✓");
274
+ statusText = "";
275
+ } else if (a.status === "stopped") {
276
+ icon = theme.fg("dim", "■");
277
+ statusText = theme.fg("dim", " stopped");
278
+ } else if (a.status === "error") {
279
+ icon = theme.fg("error", "✗");
280
+ const errMsg = a.error ? `: ${a.error.slice(0, 40)}` : "";
281
+ statusText = theme.fg("error", ` error${errMsg}`);
282
+ } else {
283
+ // aborted
284
+ icon = theme.fg("error", "✗");
285
+ statusText = theme.fg("warning", " aborted");
286
+ }
287
+
288
+ const parts: string[] = [];
289
+ const act = this.activity.get(a.id);
290
+ if (act) parts.push(formatTurns(act.turnCount, act.maxTurns));
291
+ if (a.toolUses > 0) parts.push(`${a.toolUses} tool use${a.toolUses === 1 ? "" : "s"}`);
292
+ parts.push(duration);
293
+
294
+ const line =
295
+ theme.fg("dim", "├─") +
296
+ " " +
297
+ icon +
298
+ " " +
299
+ theme.fg("dim", a.type) +
300
+ " " +
301
+ theme.fg("dim", a.description) +
302
+ " " +
303
+ theme.fg("dim", "·") +
304
+ " " +
305
+ theme.fg("dim", parts.join(" · ")) +
306
+ statusText;
307
+
308
+ return truncateToWidth(line, w);
309
+ }
310
+
311
+ private renderWidget(tui: any, theme: any, width?: number): string[] {
125
312
  const allAgents = this.manager.listAgents();
126
313
  const running = allAgents.filter((a) => a.status === "running");
127
314
  const queued = allAgents.filter((a) => a.status === "queued");
315
+ const finished = allAgents.filter(
316
+ (a) =>
317
+ a.status !== "running" &&
318
+ a.status !== "queued" &&
319
+ a.completedAt &&
320
+ this.shouldShowFinished(a.id, a.status),
321
+ );
128
322
 
129
- if (running.length === 0 && queued.length === 0) return [];
130
-
131
- const w = tui.terminal?.columns ?? 80;
132
- const truncate = (line: string) => {
133
- if (line.length <= w) return line;
134
- return line.slice(0, w - 1) + "…";
135
- };
323
+ const hasActive = running.length > 0 || queued.length > 0;
324
+ const hasFinished = finished.length > 0;
325
+ if (!hasActive && !hasFinished) return [];
136
326
 
327
+ const w = width ?? tui.terminal?.columns ?? 80;
137
328
  const frame = SPINNER[this.spinnerFrame % SPINNER.length];
138
- const lines: string[] = [];
329
+ const headingColor = hasActive ? "accent" : "dim";
330
+ const headingIcon = hasActive ? "●" : "○";
139
331
 
140
- // Heading
141
- lines.push(truncate(theme.fg("accent", "●") + " " + theme.fg("accent", "Agents")));
332
+ // Build sections: finished (1 line each), running (2 lines each), queued (1 line)
333
+ const finishedLines: string[] = finished.map((a) => this.renderFinishedLine(a, theme, w));
142
334
 
143
- // Running agents (2 lines each: header + activity)
144
- for (let i = 0; i < running.length; i++) {
145
- const a = running[i];
335
+ const runningLines: string[][] = running.map((a) => {
146
336
  const act = this.activity.get(a.id);
147
337
  const toolCount = a.toolUses;
148
338
  const tokens = act?.tokens ?? "";
149
- const duration = formatMs(Date.now() - a.startedAt);
339
+ const elapsed = formatMs(Date.now() - a.startedAt);
150
340
  const activity = act ? describeActivity(act.activeTools, act.responseText) : "starting…";
151
341
 
152
342
  const parts: string[] = [];
153
343
  if (act?.turnCount) parts.push(formatTurns(act.turnCount, act.maxTurns));
154
- if (toolCount > 0) parts.push(`${toolCount} tool uses`);
344
+ if (toolCount > 0) parts.push(`${toolCount} tool use${toolCount === 1 ? "" : "s"}`);
155
345
  if (tokens) parts.push(tokens);
156
- parts.push(duration);
346
+ parts.push(elapsed);
157
347
 
158
- const connector = i === running.length - 1 && queued.length === 0 ? "└─" : "├─";
159
- const activityConnector = i === running.length - 1 && queued.length === 0 ? " " : "│ ";
160
-
161
- lines.push(
162
- truncate(
163
- theme.fg("dim", connector) +
164
- ` ${theme.fg("accent", frame)} ${theme.bold(a.type)} ${theme.fg("dim", a.description)} · ${theme.fg("dim", parts.join(" · "))}`,
348
+ return [
349
+ truncateToWidth(
350
+ theme.fg("dim", "├─") +
351
+ ` ${theme.fg("accent", frame)} ${theme.bold(a.type)} ${theme.fg("dim", a.description)} ${theme.fg("dim", "·")} ${theme.fg("dim", parts.join(" · "))}`,
352
+ w,
165
353
  ),
166
- );
167
- lines.push(
168
- truncate(theme.fg("dim", activityConnector) + theme.fg("dim", `⎿ ${activity}`)),
169
- );
170
- }
354
+ truncateToWidth(theme.fg("dim", "│ ") + theme.fg("dim", ` ⎿ ${activity}`), w),
355
+ ];
356
+ });
171
357
 
172
- // Queued
173
- if (queued.length > 0) {
174
- lines.push(
175
- truncate(theme.fg("dim", "└─") + ` ${theme.fg("muted", "◦")} ${theme.fg("dim", `${queued.length} queued`)}`),
176
- );
358
+ const queuedLine =
359
+ queued.length > 0
360
+ ? truncateToWidth(
361
+ theme.fg("dim", "├─") + ` ${theme.fg("muted", "◦")} ${theme.fg("dim", `${queued.length} queued`)}`,
362
+ w,
363
+ )
364
+ : undefined;
365
+
366
+ // Assemble with overflow cap
367
+ const maxBody = MAX_WIDGET_LINES - 1; // heading takes 1 line
368
+ const totalBody = finishedLines.length + runningLines.length * 2 + (queuedLine ? 1 : 0);
369
+
370
+ const lines: string[] = [truncateToWidth(theme.fg(headingColor, headingIcon) + " " + theme.fg(headingColor, "Agents"), w)];
371
+
372
+ if (totalBody <= maxBody) {
373
+ // Everything fits
374
+ lines.push(...finishedLines);
375
+ for (const pair of runningLines) lines.push(...pair);
376
+ if (queuedLine) lines.push(queuedLine);
377
+
378
+ // Fix last connector: ├─ → └─
379
+ if (lines.length > 1) {
380
+ const last = lines.length - 1;
381
+ lines[last] = lines[last].replace("├─", "└─");
382
+ if (runningLines.length > 0 && !queuedLine && last >= 2) {
383
+ lines[last - 1] = lines[last - 1].replace("├─", "└─");
384
+ lines[last] = lines[last].replace("│ ", " ");
385
+ }
386
+ }
387
+ } else {
388
+ // Overflow — prioritize: running > queued > finished
389
+ let budget = maxBody - 1; // reserve 1 for overflow indicator
390
+ let hiddenRunning = 0;
391
+ let hiddenFinished = 0;
392
+
393
+ // 1. Running agents (2 lines each)
394
+ for (const pair of runningLines) {
395
+ if (budget >= 2) {
396
+ lines.push(...pair);
397
+ budget -= 2;
398
+ } else {
399
+ hiddenRunning++;
400
+ }
401
+ }
402
+
403
+ // 2. Queued
404
+ if (queuedLine && budget >= 1) {
405
+ lines.push(queuedLine);
406
+ budget--;
407
+ }
408
+
409
+ // 3. Finished
410
+ for (const fl of finishedLines) {
411
+ if (budget >= 1) {
412
+ lines.push(fl);
413
+ budget--;
414
+ } else {
415
+ hiddenFinished++;
416
+ }
417
+ }
418
+
419
+ // Overflow summary
420
+ const overflowParts: string[] = [];
421
+ if (hiddenRunning > 0) overflowParts.push(`${hiddenRunning} running`);
422
+ if (hiddenFinished > 0) overflowParts.push(`${hiddenFinished} finished`);
423
+ if (overflowParts.length > 0) {
424
+ lines.push(
425
+ truncateToWidth(
426
+ theme.fg("dim", "└─") + ` ${theme.fg("dim", `+${hiddenRunning + hiddenFinished} more (${overflowParts.join(", ")})`)}`,
427
+ w,
428
+ ),
429
+ );
430
+ }
177
431
  }
178
432
 
179
433
  return lines;
@@ -184,10 +438,16 @@ export class AgentWidget {
184
438
  clearInterval(this.timer);
185
439
  this.timer = undefined;
186
440
  }
187
- if (this.uiCtx && this.widgetRegistered) {
188
- this.uiCtx.setWidget("unipi-agents", undefined);
189
- this.widgetRegistered = false;
190
- this.tui = undefined;
441
+ if (this.uiCtx) {
442
+ if (this.widgetRegistered) {
443
+ this.uiCtx.setWidget("unipi-agents", undefined);
444
+ }
445
+ if (this.lastStatusText !== undefined) {
446
+ this.uiCtx.setStatus?.("subagents", undefined);
447
+ }
191
448
  }
449
+ this.widgetRegistered = false;
450
+ this.tui = undefined;
451
+ this.lastStatusText = undefined;
192
452
  }
193
453
  }
@@ -1,72 +0,0 @@
1
- /**
2
- * @pi-unipi/subagents — Agent manager
3
- *
4
- * Tracks agents, manages concurrency queue, handles spawn/resume/abort.
5
- * Background agents subject to concurrency limit. Foreground bypass queue.
6
- */
7
- import type { Model } from "@mariozechner/pi-ai";
8
- import type { AgentSession, ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
9
- import { type ToolActivity } from "./agent-runner.js";
10
- import { type ModelRegistry } from "./model-resolver.js";
11
- import type { AgentRecord, AgentType, ThinkingLevel } from "./types.js";
12
- import { FileLock } from "./file-lock.js";
13
- export type OnAgentComplete = (record: AgentRecord) => void;
14
- export type OnAgentStart = (record: AgentRecord) => void;
15
- interface SpawnOptions {
16
- description: string;
17
- model?: Model<any>;
18
- modelInput?: string;
19
- modelRegistry?: ModelRegistry;
20
- thinkingLevel?: ThinkingLevel;
21
- maxTurns?: number;
22
- isolated?: boolean;
23
- inheritContext?: boolean;
24
- isBackground?: boolean;
25
- onToolActivity?: (activity: ToolActivity) => void;
26
- onTextDelta?: (delta: string, fullText: string) => void;
27
- onSessionCreated?: (session: AgentSession) => void;
28
- onTurnEnd?: (turnCount: number) => void;
29
- }
30
- export declare class AgentManager {
31
- private agents;
32
- private cleanupInterval;
33
- private onComplete?;
34
- private onStart?;
35
- private maxConcurrent;
36
- /** Per-file transparent locking for write agents. */
37
- readonly fileLock: FileLock;
38
- /** Queue of background agents waiting to start. */
39
- private queue;
40
- /** Number of currently running background agents. */
41
- private runningBackground;
42
- constructor(onComplete?: OnAgentComplete, maxConcurrent?: number, onStart?: OnAgentStart);
43
- setMaxConcurrent(n: number): void;
44
- getMaxConcurrent(): number;
45
- /**
46
- * Spawn an agent. Returns ID immediately for background, waits for foreground.
47
- */
48
- spawn(pi: ExtensionAPI, ctx: ExtensionContext, type: AgentType, prompt: string, options: SpawnOptions): string;
49
- /** Actually start an agent. */
50
- private startAgent;
51
- /** Start queued agents up to concurrency limit. */
52
- private drainQueue;
53
- /**
54
- * Spawn and wait (foreground).
55
- */
56
- spawnAndWait(pi: ExtensionAPI, ctx: ExtensionContext, type: AgentType, prompt: string, options: Omit<SpawnOptions, "isBackground">): Promise<AgentRecord>;
57
- getRecord(id: string): AgentRecord | undefined;
58
- listAgents(): AgentRecord[];
59
- abort(id: string): boolean;
60
- /** Abort all agents (for ESC propagation). */
61
- abortAll(): number;
62
- /** Wait for all agents. */
63
- waitForAll(): Promise<void>;
64
- /** Whether any agents running or queued. */
65
- hasRunning(): boolean;
66
- /** Remove completed records. */
67
- clearCompleted(): void;
68
- private cleanup;
69
- dispose(): void;
70
- }
71
- export {};
72
- //# sourceMappingURL=agent-manager.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"agent-manager.d.ts","sourceRoot":"","sources":["../src/agent-manager.ts"],"names":[],"mappings":"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"}