@pi-unipi/subagents 0.1.12 → 0.2.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.
- package/dist/__tests__/config.test.d.ts +11 -0
- package/dist/__tests__/config.test.d.ts.map +1 -0
- package/dist/__tests__/config.test.js +196 -0
- package/dist/__tests__/config.test.js.map +1 -0
- package/dist/__tests__/esc-propagation.test.d.ts +10 -0
- package/dist/__tests__/esc-propagation.test.d.ts.map +1 -0
- package/dist/__tests__/esc-propagation.test.js +140 -0
- package/dist/__tests__/esc-propagation.test.js.map +1 -0
- package/dist/__tests__/file-lock.test.d.ts +12 -0
- package/dist/__tests__/file-lock.test.d.ts.map +1 -0
- package/dist/__tests__/file-lock.test.js +187 -0
- package/dist/__tests__/file-lock.test.js.map +1 -0
- package/dist/__tests__/workflow-integration.test.d.ts +12 -0
- package/dist/__tests__/workflow-integration.test.d.ts.map +1 -0
- package/dist/__tests__/workflow-integration.test.js +261 -0
- package/dist/__tests__/workflow-integration.test.js.map +1 -0
- package/dist/agent-manager.d.ts +4 -1
- package/dist/agent-manager.d.ts.map +1 -1
- package/dist/agent-manager.js +10 -0
- package/dist/agent-manager.js.map +1 -1
- package/dist/agent-runner.d.ts +2 -1
- package/dist/agent-runner.d.ts.map +1 -1
- package/dist/agent-runner.js +23 -7
- package/dist/agent-runner.js.map +1 -1
- package/dist/conversation-viewer.d.ts +40 -0
- package/dist/conversation-viewer.d.ts.map +1 -0
- package/dist/conversation-viewer.js +276 -0
- package/dist/conversation-viewer.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +410 -58
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +17 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +30 -0
- package/dist/types.js.map +1 -1
- package/dist/widget.d.ts +32 -3
- package/dist/widget.d.ts.map +1 -1
- package/dist/widget.js +298 -56
- package/dist/widget.js.map +1 -1
- package/package.json +1 -1
- package/src/agent-manager.ts +12 -1
- package/src/agent-runner.ts +23 -8
- package/src/conversation-viewer.ts +299 -0
- package/src/index.ts +411 -49
- package/src/types.ts +49 -0
- package/src/widget.ts +332 -72
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
|
-
*
|
|
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
|
-
|
|
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
|
-
/**
|
|
27
|
-
function
|
|
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
|
|
30
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
88
|
-
const
|
|
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 (!
|
|
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
|
-
//
|
|
121
|
-
this.
|
|
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
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
|
329
|
+
const headingColor = hasActive ? "accent" : "dim";
|
|
330
|
+
const headingIcon = hasActive ? "●" : "○";
|
|
139
331
|
|
|
140
|
-
//
|
|
141
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
344
|
+
if (toolCount > 0) parts.push(`${toolCount} tool use${toolCount === 1 ? "" : "s"}`);
|
|
155
345
|
if (tokens) parts.push(tokens);
|
|
156
|
-
parts.push(
|
|
346
|
+
parts.push(elapsed);
|
|
157
347
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
);
|
|
170
|
-
}
|
|
354
|
+
truncateToWidth(theme.fg("dim", "│ ") + theme.fg("dim", ` ⎿ ${activity}`), w),
|
|
355
|
+
];
|
|
356
|
+
});
|
|
171
357
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
|
188
|
-
this.
|
|
189
|
-
|
|
190
|
-
|
|
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
|
}
|