pi-subagents-lite 1.0.2 → 1.1.0
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/README.md +43 -6
- package/package.json +1 -1
- package/src/agent-manager.ts +81 -62
- package/src/agent-runner.ts +194 -167
- package/src/config-io.ts +9 -1
- package/src/config-mutator.ts +183 -0
- package/src/context.ts +1 -1
- package/src/format.ts +173 -0
- package/src/index.ts +124 -176
- package/src/menus.ts +188 -136
- package/src/model-precedence.ts +5 -0
- package/src/output-file.ts +1 -68
- package/src/renderer.ts +157 -0
- package/src/result-viewer.ts +2 -1
- package/src/state.ts +80 -0
- package/src/tool-execution.ts +145 -53
- package/src/types.ts +100 -31
- package/src/ui/agent-widget.ts +148 -145
- package/src/usage.ts +5 -0
- package/src/stop-agent-tool.ts +0 -77
package/src/types.ts
CHANGED
|
@@ -50,39 +50,16 @@ export interface AgentConfig {
|
|
|
50
50
|
|
|
51
51
|
export interface AgentRecord {
|
|
52
52
|
id: string;
|
|
53
|
-
type: SubagentType;
|
|
54
|
-
description: string;
|
|
55
|
-
status: "queued" | "running" | "completed" | "steered" | "aborted" | "stopped" | "error";
|
|
56
53
|
result?: string;
|
|
57
54
|
error?: string;
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
|
|
66
|
-
/** The tool_use_id from the original Agent tool call. */
|
|
67
|
-
toolCallId?: string;
|
|
68
|
-
/** Path to the streaming output transcript file. */
|
|
69
|
-
outputFile?: string;
|
|
70
|
-
/** Cleanup function for the output file stream subscription. */
|
|
71
|
-
outputCleanup?: () => void;
|
|
72
|
-
/**
|
|
73
|
-
* Lifetime usage breakdown, accumulated via `message_end` events. Survives
|
|
74
|
-
* compaction. Total = input + output + cacheWrite + cost (cacheRead deliberately
|
|
75
|
-
* excluded — see issue #38). Initialized to zeros at spawn.
|
|
76
|
-
*/
|
|
77
|
-
lifetimeUsage: LifetimeUsage;
|
|
78
|
-
/** Final turn count (set on completion). Used by widget after activity cleanup. */
|
|
79
|
-
turnCount?: number;
|
|
80
|
-
/** Max turns limit (from invocation or default). */
|
|
81
|
-
maxTurns?: number;
|
|
82
|
-
/** Number of times this agent's session has compacted. Initialized to 0 at spawn. */
|
|
83
|
-
compactionCount: number;
|
|
84
|
-
/** Resolved spawn params, captured for UI display. Fixed at spawn time. */
|
|
85
|
-
invocation?: AgentInvocation;
|
|
55
|
+
/** Lifecycle state: status, timestamps. */
|
|
56
|
+
lifecycle: AgentLifecycle;
|
|
57
|
+
/** Display-oriented info: type, description, output file, invocation. */
|
|
58
|
+
display: AgentDisplayInfo;
|
|
59
|
+
/** Execution internals: session, abort controller, pending steers. */
|
|
60
|
+
execution: AgentExecutionState;
|
|
61
|
+
/** Accumulated statistics: usage, tool uses, turns. */
|
|
62
|
+
stats: AgentAccumulatedStats;
|
|
86
63
|
}
|
|
87
64
|
|
|
88
65
|
export interface AgentInvocation {
|
|
@@ -102,6 +79,30 @@ export interface EnvInfo {
|
|
|
102
79
|
/** How many characters of agent ID to show in display. */
|
|
103
80
|
export const SHORT_ID_LENGTH = 8;
|
|
104
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Theme for terminal rendering — used by format.ts, renderer.ts, and UI widgets.
|
|
84
|
+
* Defined here (not in ui/agent-widget.ts) so non-UI modules can import it
|
|
85
|
+
* without depending on the UI layer.
|
|
86
|
+
*/
|
|
87
|
+
export type Theme = {
|
|
88
|
+
fg(color: string, text: string): string;
|
|
89
|
+
bg(color: string, text: string): string;
|
|
90
|
+
bold(text: string): string;
|
|
91
|
+
italic?: (text: string) => string;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/** Non-model keys in config.agent — preserved when clearing all overrides. */
|
|
95
|
+
export const CONFIG_AGENT_NON_MODEL_KEYS = [
|
|
96
|
+
"default",
|
|
97
|
+
"forceBackground",
|
|
98
|
+
"graceTurns",
|
|
99
|
+
"showCost",
|
|
100
|
+
"widgetMaxLines",
|
|
101
|
+
"widgetMaxLinesCompact",
|
|
102
|
+
"widgetCompact",
|
|
103
|
+
"widgetShortcut",
|
|
104
|
+
];
|
|
105
|
+
|
|
105
106
|
/** Reason for a context compaction event. */
|
|
106
107
|
export type CompactionReason = "manual" | "threshold" | "overflow";
|
|
107
108
|
|
|
@@ -111,4 +112,72 @@ export interface CompactionInfo {
|
|
|
111
112
|
tokensBefore: number;
|
|
112
113
|
}
|
|
113
114
|
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// Sub-object interfaces for decomposed AgentRecord
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
/** Possible agent lifecycle statuses. */
|
|
120
|
+
export type AgentStatus = "queued" | "running" | "completed" | "steered" | "aborted" | "stopped" | "error";
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Lifecycle state: when the agent started, completed, and its current status.
|
|
124
|
+
* Used by agent-manager (lifecycle control), menus (status display), widget (linger logic).
|
|
125
|
+
*/
|
|
126
|
+
export interface AgentLifecycle {
|
|
127
|
+
status: AgentStatus;
|
|
128
|
+
startedAt: number;
|
|
129
|
+
completedAt?: number;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Display-oriented fields: type name, description, output file, invocation params.
|
|
134
|
+
* Used by widget (rendering), menus (listing), renderer (display).
|
|
135
|
+
*/
|
|
136
|
+
export interface AgentDisplayInfo {
|
|
137
|
+
type: SubagentType;
|
|
138
|
+
description: string;
|
|
139
|
+
/** Path to the streaming output transcript file. */
|
|
140
|
+
outputFile?: string;
|
|
141
|
+
/** Resolved spawn params, captured for UI display. Fixed at spawn time. */
|
|
142
|
+
invocation?: AgentInvocation;
|
|
143
|
+
/** The tool_use_id from the original Agent tool call. */
|
|
144
|
+
toolCallId?: string;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Execution internals: session handle, abort controller, pending steers.
|
|
149
|
+
* Used by agent-manager (session lifecycle), tool-execution (steering, nudge).
|
|
150
|
+
*/
|
|
151
|
+
export interface AgentExecutionState {
|
|
152
|
+
session?: AgentSession;
|
|
153
|
+
abortController?: AbortController;
|
|
154
|
+
promise?: Promise<string>;
|
|
155
|
+
/** Steering messages queued before the session was ready. */
|
|
156
|
+
pendingSteers?: string[];
|
|
157
|
+
/** Cleanup function for the output file stream subscription. */
|
|
158
|
+
outputCleanup?: () => void;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Accumulated statistics: usage breakdown, tool uses, turn count.
|
|
163
|
+
* Used by widget (stats display), tool-execution (details building), menus (result viewer).
|
|
164
|
+
*/
|
|
165
|
+
export interface AgentAccumulatedStats {
|
|
166
|
+
/**
|
|
167
|
+
* Lifetime usage breakdown, accumulated via `message_end` events. Survives
|
|
168
|
+
* compaction. Total = input + output + cacheWrite + cost (cacheRead deliberately
|
|
169
|
+
* excluded — see issue #38). Initialized to zeros at spawn.
|
|
170
|
+
*/
|
|
171
|
+
lifetimeUsage: LifetimeUsage;
|
|
172
|
+
toolUses: number;
|
|
173
|
+
/** Final turn count (set on completion). Used by widget after activity cleanup. */
|
|
174
|
+
turnCount?: number;
|
|
175
|
+
/** Max turns limit (from invocation or default). */
|
|
176
|
+
maxTurns?: number;
|
|
177
|
+
/** Number of times this agent's session has compacted. Initialized to 0 at spawn. */
|
|
178
|
+
compactionCount: number;
|
|
179
|
+
/** Last-known context usage percentage (0–100), captured at completion. */
|
|
180
|
+
contextPercent?: number | null;
|
|
181
|
+
}
|
|
182
|
+
|
|
114
183
|
|
package/src/ui/agent-widget.ts
CHANGED
|
@@ -4,20 +4,23 @@
|
|
|
4
4
|
|
|
5
5
|
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
6
6
|
import type { AgentManager } from "../agent-manager.js";
|
|
7
|
-
import {
|
|
8
|
-
import type { AgentRecord, SubagentType } from "../types.js";
|
|
7
|
+
import type { AgentRecord, Theme } from "../types.js";
|
|
9
8
|
import {
|
|
10
|
-
|
|
9
|
+
formatCost,
|
|
11
10
|
getLifetimeTotal,
|
|
12
11
|
getSessionContextPercent,
|
|
13
12
|
type LifetimeUsage,
|
|
14
13
|
type SessionLike,
|
|
15
14
|
} from "../usage.js";
|
|
15
|
+
import { formatMs, buildStatsParts, getDisplayName } from "../format.js";
|
|
16
|
+
|
|
17
|
+
// Re-export Theme so existing consumers (model-selector, result-viewer) don't break
|
|
18
|
+
export type { Theme } from "../types.js";
|
|
16
19
|
|
|
17
20
|
// ---- Constants ----
|
|
18
21
|
|
|
19
22
|
/** Maximum number of rendered lines before overflow collapse kicks in. */
|
|
20
|
-
const
|
|
23
|
+
const DEFAULT_MAX_WIDGET_LINES = 12;
|
|
21
24
|
|
|
22
25
|
/** Braille spinner frames for animated running indicator. */
|
|
23
26
|
const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
@@ -58,11 +61,6 @@ const TOOL_DISPLAY: Record<string, string> = {
|
|
|
58
61
|
|
|
59
62
|
// ---- Types ----
|
|
60
63
|
|
|
61
|
-
export type Theme = {
|
|
62
|
-
fg(color: string, text: string): string;
|
|
63
|
-
bold(text: string): string;
|
|
64
|
-
italic?: (text: string) => string;
|
|
65
|
-
};
|
|
66
64
|
|
|
67
65
|
export type UICtx = {
|
|
68
66
|
setStatus(key: string, text: string | undefined): void;
|
|
@@ -99,95 +97,10 @@ export interface AgentActivity {
|
|
|
99
97
|
lifetimeUsage: LifetimeUsage;
|
|
100
98
|
}
|
|
101
99
|
|
|
100
|
+
// ---- Re-exports from format.ts (backward compatibility) ----
|
|
101
|
+
export { formatMs, buildStatsParts, getDisplayName } from "../format.js";
|
|
102
102
|
|
|
103
|
-
|
|
104
|
-
// ---- Formatting helpers ----
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Token count with optional context-fill % and compaction-count annotations.
|
|
108
|
-
* Thresholds for percent: <70% dim, 70–85% warning, ≥85% error.
|
|
109
|
-
* Compaction count rendered as `↻ N` in dim.
|
|
110
|
-
*
|
|
111
|
-
* "12.3k" — no annotations
|
|
112
|
-
* "12.3k(45%)" — percent only
|
|
113
|
-
* "12.3k(↻ 2)" — compactions only (e.g. right after compact)
|
|
114
|
-
* "12.3k(45%·↻ 2)" — both
|
|
115
|
-
*/
|
|
116
|
-
function formatSessionTokens(
|
|
117
|
-
tokens: number,
|
|
118
|
-
percent: number | null,
|
|
119
|
-
theme: Theme,
|
|
120
|
-
compactions = 0,
|
|
121
|
-
): string {
|
|
122
|
-
const tokenStr = formatTokens(tokens);
|
|
123
|
-
const annot: string[] = [];
|
|
124
|
-
if (percent !== null) {
|
|
125
|
-
const color = percent >= 85 ? "error" : percent >= 70 ? "warning" : "dim";
|
|
126
|
-
annot.push(theme.fg(color, `${Math.round(percent)}%`));
|
|
127
|
-
}
|
|
128
|
-
if (compactions > 0) {
|
|
129
|
-
annot.push(theme.fg("dim", `↻ ${compactions}`));
|
|
130
|
-
}
|
|
131
|
-
if (annot.length === 0) return tokenStr;
|
|
132
|
-
// Include closing paren in the last annotation's color span to prevent
|
|
133
|
-
// ANSI reset from leaving `)` in default color when wrapped in outer dim.
|
|
134
|
-
const lastIdx = annot.length - 1;
|
|
135
|
-
annot[lastIdx] += ")";
|
|
136
|
-
return `${tokenStr}(${annot.join("·")}`;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/** Format turn count with optional max limit: "5≤30⟳" or "5⟳". */
|
|
140
|
-
function formatTurns(turnCount: number, maxTurns?: number | null): string {
|
|
141
|
-
return maxTurns != null ? `${turnCount}≤${maxTurns}⟳ ` : `${turnCount}⟳ `;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/** Format milliseconds as a compact human-readable duration: "1h 1m 1s", "5m 37s", "10s", "<1s". */
|
|
145
|
-
export function formatMs(ms: number): string {
|
|
146
|
-
if (!Number.isFinite(ms) || ms < 1000) return "<1s";
|
|
147
|
-
|
|
148
|
-
const totalSeconds = Math.floor(ms / 1000);
|
|
149
|
-
const hours = Math.floor(totalSeconds / 3600);
|
|
150
|
-
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
151
|
-
const seconds = totalSeconds % 60;
|
|
152
|
-
|
|
153
|
-
const parts: string[] = [];
|
|
154
|
-
if (hours > 0) parts.push(`${hours}h`);
|
|
155
|
-
if (minutes > 0) parts.push(`${minutes}m`);
|
|
156
|
-
if (seconds > 0 || parts.length === 0) parts.push(`${seconds}s`);
|
|
157
|
-
|
|
158
|
-
return parts.join(" ");
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Build common stats parts: toolUses · turns · tokens with context %.
|
|
163
|
-
* Shared by AgentWidget and index.ts for consistent stats display.
|
|
164
|
-
*/
|
|
165
|
-
export function buildStatsParts(
|
|
166
|
-
args: {
|
|
167
|
-
toolUses: number;
|
|
168
|
-
turnCount?: number;
|
|
169
|
-
maxTurns?: number;
|
|
170
|
-
tokens: number;
|
|
171
|
-
contextPercent: number | null;
|
|
172
|
-
compactions: number;
|
|
173
|
-
},
|
|
174
|
-
theme: Theme,
|
|
175
|
-
): string[] {
|
|
176
|
-
const parts: string[] = [];
|
|
177
|
-
if (args.toolUses > 0) parts.push(`${args.toolUses}🛠 `);
|
|
178
|
-
if (args.turnCount != null) parts.push(formatTurns(args.turnCount, args.maxTurns));
|
|
179
|
-
if (args.tokens > 0) {
|
|
180
|
-
parts.push(formatSessionTokens(
|
|
181
|
-
args.tokens, args.contextPercent, theme, args.compactions,
|
|
182
|
-
));
|
|
183
|
-
}
|
|
184
|
-
return parts;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/** Get display name for any agent type (built-in or custom). */
|
|
188
|
-
export function getDisplayName(type: SubagentType): string {
|
|
189
|
-
return getConfig(type).displayName;
|
|
190
|
-
}
|
|
103
|
+
// ---- Widget-internal helpers ----
|
|
191
104
|
|
|
192
105
|
/**
|
|
193
106
|
* Wrap a stats line in dim ANSI codes, re-applying dim after any inner
|
|
@@ -245,6 +158,8 @@ export class AgentWidget {
|
|
|
245
158
|
/** Finished agents: agent ID → turns since finished. */
|
|
246
159
|
private finishedTurnAge = new Map<string, number>();
|
|
247
160
|
|
|
161
|
+
/** Whether to show cost in stats and status bar. */
|
|
162
|
+
private showCost = false;
|
|
248
163
|
|
|
249
164
|
/** Whether the widget callback is currently registered with the TUI. */
|
|
250
165
|
private widgetRegistered = false;
|
|
@@ -252,12 +167,67 @@ export class AgentWidget {
|
|
|
252
167
|
private tui: TUI | undefined;
|
|
253
168
|
/** Last status bar text, used to avoid redundant setStatus calls. */
|
|
254
169
|
private lastStatusText: string | undefined;
|
|
170
|
+
/** Pending tool expansion state from onTerminalInput (push-based, no polling). */
|
|
171
|
+
private pendingToolsExpanded: boolean | undefined;
|
|
172
|
+
|
|
173
|
+
/** Whether to use compact mode (1-line per agent). */
|
|
174
|
+
private compactMode = false;
|
|
175
|
+
|
|
176
|
+
/** Whether "force compact" mode is ON — overrides ctrl+o shortcut. */
|
|
177
|
+
private forceCompact = false;
|
|
178
|
+
|
|
179
|
+
/** Whether ctrl+o shortcut is enabled (syncs compact with toolsExpanded). */
|
|
180
|
+
private widgetShortcut = false;
|
|
181
|
+
|
|
182
|
+
/** Maximum lines for full mode. */
|
|
183
|
+
private maxLines = DEFAULT_MAX_WIDGET_LINES;
|
|
184
|
+
|
|
185
|
+
/** Maximum lines for compact mode. */
|
|
186
|
+
private maxLinesCompact = Math.floor(DEFAULT_MAX_WIDGET_LINES / 2);
|
|
255
187
|
|
|
256
188
|
constructor(
|
|
257
189
|
private manager: AgentManager,
|
|
258
190
|
private agentActivity: Map<string, AgentActivity>,
|
|
259
191
|
) {}
|
|
260
192
|
|
|
193
|
+
/** Set whether to show cost in stats and status bar. */
|
|
194
|
+
setShowCost(enabled: boolean) {
|
|
195
|
+
this.showCost = enabled;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Set compact mode (internal, for sync from ctrl+o). */
|
|
199
|
+
setCompactMode(enabled: boolean) {
|
|
200
|
+
if (this.compactMode === enabled) return;
|
|
201
|
+
this.compactMode = enabled;
|
|
202
|
+
this.update();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Set force compact mode — overrides ctrl+o shortcut. */
|
|
206
|
+
setForceCompact(enabled: boolean) {
|
|
207
|
+
this.forceCompact = enabled;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Set whether ctrl+o shortcut is enabled. */
|
|
211
|
+
setWidgetShortcut(enabled: boolean) {
|
|
212
|
+
this.widgetShortcut = enabled;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Notify widget that tool expansion state changed (push-based, no polling). */
|
|
216
|
+
notifyToolsExpansionChanged(expanded: boolean) {
|
|
217
|
+
this.pendingToolsExpanded = expanded;
|
|
218
|
+
this.update();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Set max lines for full mode. */
|
|
222
|
+
setMaxLines(lines: number) {
|
|
223
|
+
this.maxLines = lines;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Set max lines for compact mode. */
|
|
227
|
+
setMaxLinesCompact(lines: number) {
|
|
228
|
+
this.maxLinesCompact = lines;
|
|
229
|
+
}
|
|
230
|
+
|
|
261
231
|
/** Set the UI context (grabbed from first tool execution). */
|
|
262
232
|
setUICtx(ctx: UICtx) {
|
|
263
233
|
if (ctx !== this.uiCtx) {
|
|
@@ -297,9 +267,9 @@ export class AgentWidget {
|
|
|
297
267
|
const queued: AgentRecord[] = [];
|
|
298
268
|
const finished: AgentRecord[] = [];
|
|
299
269
|
for (const a of allAgents) {
|
|
300
|
-
if (a.status === "running") running.push(a);
|
|
301
|
-
else if (a.status === "queued") queued.push(a);
|
|
302
|
-
else if (a.completedAt && this.shouldShowFinished(a.id, a.status)) finished.push(a);
|
|
270
|
+
if (a.lifecycle.status === "running") running.push(a);
|
|
271
|
+
else if (a.lifecycle.status === "queued") queued.push(a);
|
|
272
|
+
else if (a.lifecycle.completedAt && this.shouldShowFinished(a.id, a.lifecycle.status)) finished.push(a);
|
|
303
273
|
}
|
|
304
274
|
return { running, queued, finished };
|
|
305
275
|
}
|
|
@@ -342,47 +312,44 @@ export class AgentWidget {
|
|
|
342
312
|
}
|
|
343
313
|
|
|
344
314
|
/** Render a finished agent line. */
|
|
345
|
-
private renderFinishedLine(a: {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
turnCount?: number; maxTurns?: number; session?: SessionLike;
|
|
350
|
-
outputFile?: string;
|
|
351
|
-
}, theme: Theme): string {
|
|
352
|
-
const name = getDisplayName(a.type);
|
|
353
|
-
const duration = formatMs((a.completedAt ?? Date.now()) - a.startedAt);
|
|
354
|
-
const { icon, statusText } = this.finishedIconAndStatus(a.status, a.error, theme);
|
|
315
|
+
private renderFinishedLine(a: AgentRecord, theme: Theme): string {
|
|
316
|
+
const name = getDisplayName(a.display.type);
|
|
317
|
+
const duration = formatMs((a.lifecycle.completedAt ?? Date.now()) - a.lifecycle.startedAt);
|
|
318
|
+
const { icon, statusText } = this.finishedIconAndStatus(a.lifecycle.status, a.error, theme);
|
|
355
319
|
|
|
356
320
|
const activity = this.agentActivity.get(a.id);
|
|
321
|
+
const usage = activity?.lifetimeUsage ?? a.stats.lifetimeUsage;
|
|
357
322
|
const statsParts = buildStatsParts({
|
|
358
|
-
toolUses: a.toolUses,
|
|
359
|
-
turnCount: activity?.turnCount ?? a.turnCount,
|
|
360
|
-
maxTurns: activity?.maxTurns ?? a.maxTurns,
|
|
361
|
-
tokens: getLifetimeTotal(
|
|
362
|
-
contextPercent: getSessionContextPercent(activity
|
|
363
|
-
compactions: a.compactionCount,
|
|
323
|
+
toolUses: a.stats.toolUses,
|
|
324
|
+
turnCount: activity?.turnCount ?? a.stats.turnCount,
|
|
325
|
+
maxTurns: activity?.maxTurns ?? a.stats.maxTurns,
|
|
326
|
+
tokens: getLifetimeTotal(usage),
|
|
327
|
+
contextPercent: activity?.session ? getSessionContextPercent(activity.session) : a.stats.contextPercent ?? null,
|
|
328
|
+
compactions: a.stats.compactionCount,
|
|
329
|
+
cost: this.showCost ? usage.cost : undefined,
|
|
364
330
|
}, theme);
|
|
365
331
|
statsParts.push(duration);
|
|
366
332
|
|
|
367
333
|
const statsLine = statsParts.join("·");
|
|
368
|
-
return `${icon} ${theme.fg("dim", name)} ${theme.fg("dim", a.description)} ${wrapInDim(theme, statsLine)}${statusText}`;
|
|
334
|
+
return `${icon} ${theme.fg("dim", name)} ${theme.fg("dim", a.display.description)} ${wrapInDim(theme, statsLine)}${statusText}`;
|
|
369
335
|
}
|
|
370
336
|
|
|
371
|
-
/** Build the stats line (toolUses · turns · tokens · elapsed) for a running agent. */
|
|
337
|
+
/** Build the stats line (toolUses · turns · tokens · cost · elapsed) for a running agent. */
|
|
372
338
|
private buildStatsLine(
|
|
373
|
-
agent:
|
|
339
|
+
agent: AgentRecord,
|
|
374
340
|
activity: AgentActivity | undefined,
|
|
375
341
|
theme: Theme,
|
|
376
342
|
): string {
|
|
377
343
|
const parts = buildStatsParts({
|
|
378
|
-
toolUses: activity?.toolUses ?? agent.toolUses,
|
|
344
|
+
toolUses: activity?.toolUses ?? agent.stats.toolUses,
|
|
379
345
|
turnCount: activity?.turnCount,
|
|
380
346
|
maxTurns: activity?.maxTurns,
|
|
381
347
|
tokens: getLifetimeTotal(activity?.lifetimeUsage),
|
|
382
348
|
contextPercent: getSessionContextPercent(activity?.session),
|
|
383
|
-
compactions: agent.compactionCount,
|
|
349
|
+
compactions: agent.stats.compactionCount,
|
|
350
|
+
cost: this.showCost ? activity?.lifetimeUsage?.cost : undefined,
|
|
384
351
|
}, theme);
|
|
385
|
-
parts.push(formatMs(Date.now() - agent.startedAt));
|
|
352
|
+
parts.push(formatMs(Date.now() - agent.lifecycle.startedAt));
|
|
386
353
|
return parts.join("·");
|
|
387
354
|
}
|
|
388
355
|
|
|
@@ -397,8 +364,8 @@ export class AgentWidget {
|
|
|
397
364
|
for (const a of finished) {
|
|
398
365
|
blocks.push({
|
|
399
366
|
header: truncate(`${theme.fg("dim", BRANCH)} ${this.renderFinishedLine(a, theme)}`),
|
|
400
|
-
continuations: a.outputFile
|
|
401
|
-
? [truncate(theme.fg("dim", `${VLINE} tail -f ${a.outputFile}`))]
|
|
367
|
+
continuations: a.display.outputFile
|
|
368
|
+
? [truncate(theme.fg("dim", `${VLINE} tail -f ${a.display.outputFile}`))]
|
|
402
369
|
: [],
|
|
403
370
|
});
|
|
404
371
|
}
|
|
@@ -415,21 +382,32 @@ export class AgentWidget {
|
|
|
415
382
|
const truncate = (line: string) => truncateToWidth(line, w);
|
|
416
383
|
const blocks: RenderBlock[] = [];
|
|
417
384
|
for (const a of running) {
|
|
418
|
-
const name = getDisplayName(a.type);
|
|
385
|
+
const name = getDisplayName(a.display.type);
|
|
419
386
|
const bg = this.agentActivity.get(a.id);
|
|
420
387
|
const statsLine = this.buildStatsLine(a, bg, theme);
|
|
421
388
|
const activity = bg ? describeActivity(bg.activeTools, bg.responseText) : THINKING_TEXT;
|
|
422
389
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
390
|
+
if (this.isCompact()) {
|
|
391
|
+
// Compact: single line with activity inline, truncated description
|
|
392
|
+
const desc = a.display.description.length > 30 ? a.display.description.slice(0, 27) + "..." : a.display.description;
|
|
393
|
+
const headerLine = `${BRANCH} ${theme.fg("accent", frame)} ${theme.bold(name)} ${desc} ${statsLine} ${theme.fg("dim", activity)}`;
|
|
394
|
+
blocks.push({
|
|
395
|
+
header: truncate(headerLine),
|
|
396
|
+
continuations: [],
|
|
397
|
+
});
|
|
398
|
+
} else {
|
|
399
|
+
// Full: header + continuation lines
|
|
400
|
+
const headerLine = `${BRANCH} ${theme.fg("accent", frame)} ${theme.bold(name)} ${a.display.description} ${statsLine}`;
|
|
401
|
+
blocks.push({
|
|
402
|
+
header: truncate(headerLine),
|
|
403
|
+
continuations: [
|
|
404
|
+
...(a.display.outputFile
|
|
405
|
+
? [truncate(`${VLINE} ` + theme.fg("dim", `${VLINE} tail -f ${a.display.outputFile}`))]
|
|
406
|
+
: []),
|
|
407
|
+
truncate(`${VLINE} ` + theme.fg("dim", `└ ${activity}`)),
|
|
408
|
+
],
|
|
409
|
+
});
|
|
410
|
+
}
|
|
433
411
|
}
|
|
434
412
|
return blocks;
|
|
435
413
|
}
|
|
@@ -455,6 +433,11 @@ export class AgentWidget {
|
|
|
455
433
|
* connectors in a single pass. Last visible block gets CORNER + spaces, all others
|
|
456
434
|
* keep BRANCH + VLINE.
|
|
457
435
|
*/
|
|
436
|
+
/** Whether the widget should render in compact mode. */
|
|
437
|
+
private isCompact(): boolean {
|
|
438
|
+
return this.forceCompact || (this.widgetShortcut && this.compactMode);
|
|
439
|
+
}
|
|
440
|
+
|
|
458
441
|
private renderWidget(tui: TUI, theme: Theme): string[] {
|
|
459
442
|
const { running, queued, finished } = this.categorizeAgents();
|
|
460
443
|
|
|
@@ -485,7 +468,8 @@ export class AgentWidget {
|
|
|
485
468
|
|
|
486
469
|
// ---- Overflow logic (works with blocks, not lines) ----
|
|
487
470
|
|
|
488
|
-
const
|
|
471
|
+
const maxBodyLines = this.isCompact() ? this.maxLinesCompact : this.maxLines;
|
|
472
|
+
const maxBody = maxBodyLines - 1; // heading takes 1 line
|
|
489
473
|
const totalBody = blocks.reduce((sum, b) => sum + 1 + b.continuations.length, 0);
|
|
490
474
|
|
|
491
475
|
const heading = `${theme.fg(headingColor, headingIcon)} ${theme.fg(headingColor, "Agents")}`;
|
|
@@ -601,21 +585,40 @@ export class AgentWidget {
|
|
|
601
585
|
}
|
|
602
586
|
|
|
603
587
|
/** Update the status bar text, only if it changed. */
|
|
604
|
-
private updateStatusBar(runningCount: number, queuedCount: number) {
|
|
605
|
-
const statusParts: string[] = [];
|
|
606
|
-
if (runningCount > 0) statusParts.push(`${runningCount} running`);
|
|
607
|
-
if (queuedCount > 0) statusParts.push(`${queuedCount} queued`);
|
|
588
|
+
private updateStatusBar(runningCount: number, queuedCount: number, running: AgentRecord[]) {
|
|
608
589
|
const total = runningCount + queuedCount;
|
|
609
|
-
|
|
610
|
-
if (
|
|
611
|
-
this.
|
|
612
|
-
|
|
590
|
+
let statusText = total > 0 ? `${total} agent${total === 1 ? "" : "s"}` : `agents`;
|
|
591
|
+
if (this.showCost) {
|
|
592
|
+
const sessionCost = this.manager.getTotalAgentCost();
|
|
593
|
+
// Also include in-flight running agents (not yet completed, so not in accumulator)
|
|
594
|
+
const runningCost = running.reduce((sum, a) => sum + a.stats.lifetimeUsage.cost, 0);
|
|
595
|
+
const totalCost = sessionCost + runningCost;
|
|
596
|
+
if (totalCost > 0) statusText += `: ${formatCost(totalCost)}`;
|
|
597
|
+
}
|
|
598
|
+
if (statusText !== this.lastStatusText) {
|
|
599
|
+
this.uiCtx?.setStatus(STATUS_KEY, statusText);
|
|
600
|
+
this.lastStatusText = statusText;
|
|
613
601
|
}
|
|
614
602
|
}
|
|
615
603
|
|
|
616
604
|
/** Force an immediate widget update. */
|
|
617
605
|
update() {
|
|
606
|
+
if (!this.manager) {
|
|
607
|
+
// Widget lost its manager reference (e.g., after session shutdown)
|
|
608
|
+
clearInterval(this.widgetInterval);
|
|
609
|
+
this.widgetInterval = undefined;
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
618
612
|
if (!this.uiCtx) return;
|
|
613
|
+
|
|
614
|
+
// Sync compact mode with tool expansion state (ctrl+o)
|
|
615
|
+
// Tools expanded → widget full, tools collapsed → widget compact
|
|
616
|
+
// Note: sync is triggered by onTerminalInput detecting ctrl+o, not polling
|
|
617
|
+
if (this.widgetShortcut && !this.forceCompact && this.pendingToolsExpanded !== undefined) {
|
|
618
|
+
this.compactMode = !this.pendingToolsExpanded;
|
|
619
|
+
this.pendingToolsExpanded = undefined;
|
|
620
|
+
}
|
|
621
|
+
|
|
619
622
|
const { running, queued, finished } = this.categorizeAgents();
|
|
620
623
|
|
|
621
624
|
const hasActive = running.length > 0 || queued.length > 0;
|
|
@@ -628,7 +631,7 @@ export class AgentWidget {
|
|
|
628
631
|
}
|
|
629
632
|
|
|
630
633
|
// Status bar — only call setStatus when the text actually changes
|
|
631
|
-
this.updateStatusBar(running.length, queued.length);
|
|
634
|
+
this.updateStatusBar(running.length, queued.length, running);
|
|
632
635
|
|
|
633
636
|
this.widgetFrame++;
|
|
634
637
|
|
package/src/usage.ts
CHANGED
|
@@ -36,6 +36,11 @@ export function formatTokens(count: number): string {
|
|
|
36
36
|
return `${count}`;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
/** Format cost as a dollar amount: "$0.00", "$0.01", "$1.23". */
|
|
40
|
+
export function formatCost(cost: number): string {
|
|
41
|
+
return `$${cost.toFixed(2)}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
39
44
|
/**
|
|
40
45
|
* Context-window utilization (0–100), or null when unavailable
|
|
41
46
|
* (no model contextWindow, or post-compaction before the next response).
|
package/src/stop-agent-tool.ts
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* stop-agent-tool.ts — StopAgent tool execute handler.
|
|
3
|
-
*
|
|
4
|
-
* Registered in index.ts alongside the Agent tool.
|
|
5
|
-
* Uses manager.abort(id) to stop running or queued agents.
|
|
6
|
-
*
|
|
7
|
-
* Response formats:
|
|
8
|
-
* - Success: "Stopped agent <short_id>"
|
|
9
|
-
* - Not found: "Agent <id> not found. Running agents: <type>·<short_id>, ..."
|
|
10
|
-
* - Already terminal: "Agent <id> is already <status>. Running agents: ..."
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
14
|
-
import { successResult, errorResult } from "./tool-execution.js";
|
|
15
|
-
import { manager } from "./index.js";
|
|
16
|
-
import { SHORT_ID_LENGTH } from "./types.js";
|
|
17
|
-
|
|
18
|
-
// ============================================================================
|
|
19
|
-
// Running agents list helper
|
|
20
|
-
// ============================================================================
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Build a compact list of running (or queued) agents.
|
|
24
|
-
* Format: "type·short_id, type·short_id" — one line, easy for LLM to parse.
|
|
25
|
-
*/
|
|
26
|
-
function formatRunningAgents(): string {
|
|
27
|
-
const agents = manager.listAgents().filter(
|
|
28
|
-
(a) => a.status === "running" || a.status === "queued",
|
|
29
|
-
);
|
|
30
|
-
|
|
31
|
-
if (agents.length === 0) return "none";
|
|
32
|
-
|
|
33
|
-
return agents
|
|
34
|
-
.map((a) => `${a.type}·${a.id.slice(0, SHORT_ID_LENGTH)}`)
|
|
35
|
-
.join(", ");
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// ============================================================================
|
|
39
|
-
// Execute handler
|
|
40
|
-
// ============================================================================
|
|
41
|
-
|
|
42
|
-
export async function executeStopAgentTool(
|
|
43
|
-
_toolCallId: string,
|
|
44
|
-
params: Record<string, unknown>,
|
|
45
|
-
_signal: AbortSignal | undefined,
|
|
46
|
-
_onUpdate: ((update: any) => void) | undefined,
|
|
47
|
-
_ctx: ExtensionContext,
|
|
48
|
-
): Promise<any> {
|
|
49
|
-
const agentId = params.agent_id as string | undefined;
|
|
50
|
-
|
|
51
|
-
if (!agentId) {
|
|
52
|
-
return errorResult("agent_id is required");
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const record = manager.getRecord(agentId);
|
|
56
|
-
|
|
57
|
-
if (!record) {
|
|
58
|
-
// Agent not found → return error + list of running agents
|
|
59
|
-
return errorResult(
|
|
60
|
-
`Agent ${agentId} not found. Running agents: ${formatRunningAgents()}`,
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Check if already in a terminal state (not running or queued)
|
|
65
|
-
if (record.status !== "running" && record.status !== "queued") {
|
|
66
|
-
return successResult(
|
|
67
|
-
`Agent ${agentId} is already ${record.status}. Running agents: ${formatRunningAgents()}`,
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Attempt to stop the running/queued agent
|
|
72
|
-
if (manager.abort(agentId)) {
|
|
73
|
-
return successResult(`Stopped agent ${agentId.slice(0, SHORT_ID_LENGTH)}`);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
return errorResult(`Failed to stop agent ${agentId}`);
|
|
77
|
-
}
|