@youngjurry/pi-agents 0.8.1 → 0.10.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/CHANGELOG.md +15 -0
- package/README.md +110 -4
- package/control.ts +71 -58
- package/index.ts +21 -31
- package/package.json +1 -1
- package/settings.ts +8 -16
- package/storage.ts +0 -74
- package/types.ts +14 -4
- package/viewer.ts +51 -51
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.10.0 - 2026-09-10
|
|
4
|
+
|
|
5
|
+
- Remove all automatic compatibility, path translation, and file-moving logic for legacy Agent storage.
|
|
6
|
+
- Standardize storage, settings, custom-entry identifiers, widget keys, and status keys on the `pi-agents` name.
|
|
7
|
+
- Document an explicit, collision-safe one-time command for users who need to migrate pre-0.10.0 sessions manually.
|
|
8
|
+
- Keep fresh installations free of redundant legacy checks and writes.
|
|
9
|
+
|
|
10
|
+
## 0.9.0 - 2026-09-10
|
|
11
|
+
|
|
12
|
+
- Render `/agent-usage` directly in the normal TUI transcript through Pi's TUI-only custom-entry API instead of opening an overlay.
|
|
13
|
+
- Add main-model and sub-agent-model usage breakdowns using the actual response model reported by each assistant call.
|
|
14
|
+
- Show per-model input, output, cache reads/writes, total tokens, cost, contributing Agent-session count, and usage-record count.
|
|
15
|
+
- Keep unattributable tool and summary usage in a separate `Tools/summaries` bucket.
|
|
16
|
+
- Preserve strict separation from LLM context and built-in `/session` cache accounting.
|
|
17
|
+
|
|
3
18
|
## 0.8.1 - 2026-09-10
|
|
4
19
|
|
|
5
20
|
- Remove automatic legacy-file archival and its full main-session scan; the extension no longer archives or deletes user session data.
|
package/README.md
CHANGED
|
@@ -64,7 +64,9 @@ Pi's built-in `/session` remains the authoritative view of the main Agent and it
|
|
|
64
64
|
/agent-usage
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
-
The
|
|
67
|
+
The command writes a detailed report directly into the normal TUI transcript, like `/session`; it does not open an overlay. The report includes main, sub-agent, and combined input/output/cache/token/cost totals, followed by separate main-model and sub-agent-model breakdowns. Each model row shows its tokens, prompt/output/cache details, number of contributing Agent sessions, and number of usage records. Non-model tool and summary usage is kept in an explicit `Tools/summaries` bucket rather than being misattributed to a model.
|
|
68
|
+
|
|
69
|
+
The report is stored as a TUI-only custom entry so it remains outside LLM context and does not affect `/session` message or token accounting. Reading the report does not load or wake child AgentSessions. Pi currently has no extension hook that can add child tokens to built-in `/session` while excluding them from that command's cache statistics.
|
|
68
70
|
|
|
69
71
|
## Tools
|
|
70
72
|
|
|
@@ -187,10 +189,8 @@ The settings file is optional, but spawning requires a model from either the tas
|
|
|
187
189
|
- Child sessions persist under `~/.pi/agent/pi-agents/roots/<root-session-id>/sessions/` and reload lazily
|
|
188
190
|
- Full final answers persist under `~/.pi/agent/pi-agents/roots/<root-session-id>/results/`
|
|
189
191
|
- Each root storage group records its owning main-session file in `owner.json`
|
|
190
|
-
- The
|
|
192
|
+
- The extension reads and writes only `~/.pi/agent/pi-agents/` and `settings.json`; it contains no automatic legacy migration, archival, or deletion logic
|
|
191
193
|
- Resuming an existing main session removes groups whose owning main-session file has been deleted; new sessions and `/reload` do not trigger grouped cleanup
|
|
192
|
-
- Referenced legacy flat child files are migrated when their main session is resumed
|
|
193
|
-
- The extension never automatically archives or deletes legacy flat files
|
|
194
194
|
- Parents receive a compact completion notice instead of the full answer; use `list_agents(view="results")` or read the result file on demand
|
|
195
195
|
- Notices to a busy agent are queued safely: `wait_agent` returns them in its own result, and any leftovers are delivered right after a successful recipient turn
|
|
196
196
|
- `wait_agent` sends only newly queued mailbox notices to the model; its child status tree excludes the active caller, is folded in the TUI by default, and can be toggled with `Ctrl+O`
|
|
@@ -205,3 +205,109 @@ The settings file is optional, but spawning requires a model from either the tas
|
|
|
205
205
|
- All agents share the same cwd and filesystem
|
|
206
206
|
|
|
207
207
|
Use `/agents` to browse the tree and inspect read-only child transcripts. A compact live tree appears below the editor while child agents exist and shows each active agent's `provider/model` identifier and effective thinking level.
|
|
208
|
+
|
|
209
|
+
## Manual migration from versions before 0.10.0
|
|
210
|
+
|
|
211
|
+
Version 0.10.0 removes all runtime compatibility code for the former `codex-agents` names. Existing users who cannot see an old Agent tree, or who still have `~/.pi/agent/codex-agents/` or `agents-setting.json`, should **close every Pi process first** and run the following command once. It renames the storage/settings paths and updates the old custom-entry identifiers and persisted file paths in main and child session JSONL files. It refuses to merge conflicting old and new paths automatically.
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
python3 - <<'PY'
|
|
215
|
+
from pathlib import Path
|
|
216
|
+
import json
|
|
217
|
+
import os
|
|
218
|
+
import stat
|
|
219
|
+
|
|
220
|
+
agent_dir = Path.home() / ".pi" / "agent"
|
|
221
|
+
old_root = agent_dir / "codex-agents"
|
|
222
|
+
new_root = agent_dir / "pi-agents"
|
|
223
|
+
|
|
224
|
+
if old_root.exists():
|
|
225
|
+
if new_root.exists():
|
|
226
|
+
raise SystemExit(
|
|
227
|
+
f"Refusing to merge because both {old_root} and {new_root} exist. "
|
|
228
|
+
"Back them up and reconcile them manually first."
|
|
229
|
+
)
|
|
230
|
+
old_root.rename(new_root)
|
|
231
|
+
|
|
232
|
+
old_settings = new_root / "agents-setting.json"
|
|
233
|
+
new_settings = new_root / "settings.json"
|
|
234
|
+
if old_settings.exists():
|
|
235
|
+
if new_settings.exists():
|
|
236
|
+
raise SystemExit(
|
|
237
|
+
f"Refusing to overwrite {new_settings}; reconcile it with {old_settings} manually."
|
|
238
|
+
)
|
|
239
|
+
old_settings.rename(new_settings)
|
|
240
|
+
|
|
241
|
+
custom_types = {
|
|
242
|
+
"codex-agents": "pi-agents",
|
|
243
|
+
"codex-agents-state": "pi-agents-state",
|
|
244
|
+
"codex-agents-child-meta": "pi-agents-child-meta",
|
|
245
|
+
"codex-agents-fork-context": "pi-agents-fork-context",
|
|
246
|
+
}
|
|
247
|
+
old_prefix = str(old_root)
|
|
248
|
+
new_prefix = str(new_root)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def migrate(value):
|
|
252
|
+
changed = False
|
|
253
|
+
if isinstance(value, dict):
|
|
254
|
+
output = {}
|
|
255
|
+
for key, child in value.items():
|
|
256
|
+
if key == "customType" and isinstance(child, str) and child in custom_types:
|
|
257
|
+
output[key] = custom_types[child]
|
|
258
|
+
changed = True
|
|
259
|
+
elif key in {"sessionFile", "resultFile"} and isinstance(child, str) and (
|
|
260
|
+
child == old_prefix or child.startswith(old_prefix + os.sep)
|
|
261
|
+
):
|
|
262
|
+
output[key] = new_prefix + child[len(old_prefix):]
|
|
263
|
+
changed = True
|
|
264
|
+
else:
|
|
265
|
+
output[key], child_changed = migrate(child)
|
|
266
|
+
changed |= child_changed
|
|
267
|
+
return output, changed
|
|
268
|
+
if isinstance(value, list):
|
|
269
|
+
output = []
|
|
270
|
+
for child in value:
|
|
271
|
+
migrated, child_changed = migrate(child)
|
|
272
|
+
output.append(migrated)
|
|
273
|
+
changed |= child_changed
|
|
274
|
+
return output, changed
|
|
275
|
+
return value, False
|
|
276
|
+
|
|
277
|
+
files = set((agent_dir / "sessions").rglob("*.jsonl"))
|
|
278
|
+
if new_root.exists():
|
|
279
|
+
files.update(new_root.rglob("*.jsonl"))
|
|
280
|
+
|
|
281
|
+
changed_files = 0
|
|
282
|
+
for file in sorted(files):
|
|
283
|
+
temporary = file.with_name(file.name + ".pi-agents-migrate")
|
|
284
|
+
touched = False
|
|
285
|
+
try:
|
|
286
|
+
with file.open("r", encoding="utf-8") as source, temporary.open("w", encoding="utf-8") as target:
|
|
287
|
+
for line in source:
|
|
288
|
+
try:
|
|
289
|
+
value = json.loads(line)
|
|
290
|
+
except json.JSONDecodeError:
|
|
291
|
+
target.write(line)
|
|
292
|
+
continue
|
|
293
|
+
value, line_changed = migrate(value)
|
|
294
|
+
target.write(
|
|
295
|
+
json.dumps(value, ensure_ascii=False, separators=(",", ":")) + "\n"
|
|
296
|
+
if line_changed else line
|
|
297
|
+
)
|
|
298
|
+
touched |= line_changed
|
|
299
|
+
if touched:
|
|
300
|
+
os.chmod(temporary, stat.S_IMODE(file.stat().st_mode))
|
|
301
|
+
os.replace(temporary, file)
|
|
302
|
+
changed_files += 1
|
|
303
|
+
else:
|
|
304
|
+
temporary.unlink()
|
|
305
|
+
except BaseException:
|
|
306
|
+
temporary.unlink(missing_ok=True)
|
|
307
|
+
raise
|
|
308
|
+
|
|
309
|
+
print(f"Migration complete: updated {changed_files} JSONL file(s). Restart Pi or run /reload.")
|
|
310
|
+
PY
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
Fresh installations do not need this command.
|
package/control.ts
CHANGED
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
type AgentRole,
|
|
48
48
|
type AgentRoleView,
|
|
49
49
|
type AgentTranscriptView,
|
|
50
|
+
type AgentUsageBreakdownEntry,
|
|
50
51
|
type AgentUsageReport,
|
|
51
52
|
type AgentUsageTotals,
|
|
52
53
|
type AgentView,
|
|
@@ -55,7 +56,7 @@ import {
|
|
|
55
56
|
type PersistedTreeState,
|
|
56
57
|
type RootBinding,
|
|
57
58
|
} from "./types.ts";
|
|
58
|
-
import { getAgentStorageDirectory
|
|
59
|
+
import { getAgentStorageDirectory } from "./storage.ts";
|
|
59
60
|
|
|
60
61
|
const DEFAULT_WAIT_TIMEOUT_MS = 30_000;
|
|
61
62
|
const MIN_WAIT_TIMEOUT_MS = 10_000;
|
|
@@ -126,23 +127,77 @@ function addUsageTotals(target: AgentUsageTotals, source: AgentUsageTotals): voi
|
|
|
126
127
|
target.cost += source.cost;
|
|
127
128
|
}
|
|
128
129
|
|
|
129
|
-
|
|
130
|
+
interface SessionUsageDetails {
|
|
131
|
+
totals: AgentUsageTotals;
|
|
132
|
+
breakdown: Map<string, AgentUsageBreakdownEntry>;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function addAttributedUsage(
|
|
136
|
+
breakdown: Map<string, AgentUsageBreakdownEntry>,
|
|
137
|
+
key: string,
|
|
138
|
+
usage: Usage,
|
|
139
|
+
): void {
|
|
140
|
+
let item = breakdown.get(key);
|
|
141
|
+
if (!item) {
|
|
142
|
+
item = { key, usage: emptyUsageTotals(), sessionCount: 1, operations: 0 };
|
|
143
|
+
breakdown.set(key, item);
|
|
144
|
+
}
|
|
145
|
+
addUsage(item.usage, usage);
|
|
146
|
+
item.operations++;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function sessionUsageDetails(sessionManager: Pick<SessionManager, "getEntries">): SessionUsageDetails {
|
|
130
150
|
const totals = emptyUsageTotals();
|
|
151
|
+
const breakdown = new Map<string, AgentUsageBreakdownEntry>();
|
|
131
152
|
for (const entry of sessionManager.getEntries()) {
|
|
132
153
|
if ((entry.type === "branch_summary" || entry.type === "compaction") && entry.usage) {
|
|
133
154
|
addUsage(totals, entry.usage);
|
|
155
|
+
addAttributedUsage(breakdown, "Tools/summaries", entry.usage);
|
|
134
156
|
}
|
|
135
157
|
if (entry.type !== "message") continue;
|
|
136
158
|
const message = entry.message;
|
|
137
|
-
if (message.role === "assistant"
|
|
159
|
+
if (message.role === "assistant") {
|
|
160
|
+
addUsage(totals, message.usage);
|
|
161
|
+
const responseModel = (message as typeof message & { responseModel?: string }).responseModel;
|
|
162
|
+
addAttributedUsage(breakdown, `${message.provider}/${responseModel ?? message.model}`, message.usage);
|
|
163
|
+
} else if (message.role === "toolResult" && message.usage) {
|
|
164
|
+
addUsage(totals, message.usage);
|
|
165
|
+
addAttributedUsage(breakdown, "Tools/summaries", message.usage);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return { totals, breakdown };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function mergeUsageBreakdown(
|
|
172
|
+
target: Map<string, AgentUsageBreakdownEntry>,
|
|
173
|
+
source: Map<string, AgentUsageBreakdownEntry>,
|
|
174
|
+
): void {
|
|
175
|
+
for (const item of source.values()) {
|
|
176
|
+
let aggregate = target.get(item.key);
|
|
177
|
+
if (!aggregate) {
|
|
178
|
+
aggregate = { key: item.key, usage: emptyUsageTotals(), sessionCount: 0, operations: 0 };
|
|
179
|
+
target.set(item.key, aggregate);
|
|
180
|
+
}
|
|
181
|
+
addUsageTotals(aggregate.usage, item.usage);
|
|
182
|
+
aggregate.sessionCount += item.sessionCount;
|
|
183
|
+
aggregate.operations += item.operations;
|
|
138
184
|
}
|
|
139
|
-
return totals;
|
|
140
185
|
}
|
|
141
186
|
|
|
142
187
|
function normalizeCost(value: number): number {
|
|
143
188
|
return Math.round(value * 1_000_000_000) / 1_000_000_000;
|
|
144
189
|
}
|
|
145
190
|
|
|
191
|
+
function finalizeBreakdown(breakdown: Map<string, AgentUsageBreakdownEntry>): AgentUsageBreakdownEntry[] {
|
|
192
|
+
return [...breakdown.values()]
|
|
193
|
+
.map((item) => ({
|
|
194
|
+
...item,
|
|
195
|
+
usage: { ...item.usage, cost: normalizeCost(item.usage.cost) },
|
|
196
|
+
}))
|
|
197
|
+
.filter((item) => item.usage.total > 0 || item.usage.cost > 0)
|
|
198
|
+
.sort((left, right) => right.usage.total - left.usage.total || left.key.localeCompare(right.key));
|
|
199
|
+
}
|
|
200
|
+
|
|
146
201
|
function normalizeAgentName(name: string): string {
|
|
147
202
|
const normalized = name.trim();
|
|
148
203
|
if (!normalized) throw new Error("task_name must not be empty");
|
|
@@ -309,28 +364,6 @@ export class AgentControl {
|
|
|
309
364
|
session.extensionRunner.setUIContext(proxiedUi, root.ctx.mode);
|
|
310
365
|
}
|
|
311
366
|
|
|
312
|
-
private migrateStoredFile(file: string | undefined, directory: string): { path: string | undefined; migrated: boolean } {
|
|
313
|
-
if (!file) return { path: undefined, migrated: false };
|
|
314
|
-
const original = path.resolve(file);
|
|
315
|
-
const source = resolveMigratedStoragePath(original);
|
|
316
|
-
const translated = source !== original;
|
|
317
|
-
const destinationDirectory = path.resolve(directory);
|
|
318
|
-
if (path.dirname(source) === destinationDirectory) return { path: source, migrated: translated };
|
|
319
|
-
const target = path.join(destinationDirectory, path.basename(source));
|
|
320
|
-
try {
|
|
321
|
-
fs.mkdirSync(destinationDirectory, { recursive: true });
|
|
322
|
-
if (fs.existsSync(source)) {
|
|
323
|
-
if (fs.existsSync(target)) throw new Error(`agent storage migration target already exists: ${target}`);
|
|
324
|
-
fs.renameSync(source, target);
|
|
325
|
-
return { path: target, migrated: true };
|
|
326
|
-
}
|
|
327
|
-
if (fs.existsSync(target)) return { path: target, migrated: true };
|
|
328
|
-
} catch {
|
|
329
|
-
// Keep the original path and fail safely during lazy loading if it becomes unavailable.
|
|
330
|
-
}
|
|
331
|
-
return { path: source, migrated: false };
|
|
332
|
-
}
|
|
333
|
-
|
|
334
367
|
private configureRootStorage(ctx: ExtensionContext, sessionId: string): void {
|
|
335
368
|
const safeSessionId = sessionId.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
336
369
|
const rootDirectory = path.join(this.rootStorageDirectory, safeSessionId);
|
|
@@ -1166,14 +1199,16 @@ export class AgentControl {
|
|
|
1166
1199
|
|
|
1167
1200
|
getUsage(ctx: ExtensionContext): AgentUsageReport {
|
|
1168
1201
|
this.callerPath(ctx);
|
|
1169
|
-
const
|
|
1202
|
+
const mainDetails = sessionUsageDetails(ctx.sessionManager);
|
|
1203
|
+
const main = mainDetails.totals;
|
|
1170
1204
|
const subagents = emptyUsageTotals();
|
|
1205
|
+
const subagentBreakdown = new Map<string, AgentUsageBreakdownEntry>();
|
|
1171
1206
|
let unreadableSubagents = 0;
|
|
1172
1207
|
const countedSessionIds = new Set<string>();
|
|
1173
1208
|
for (const record of this.agentsByPath.values()) {
|
|
1174
1209
|
try {
|
|
1175
1210
|
const liveManager = record.session?.sessionManager;
|
|
1176
|
-
const storedFile = record.sessionFile
|
|
1211
|
+
const storedFile = record.sessionFile;
|
|
1177
1212
|
if (!liveManager && (!storedFile || !fs.existsSync(storedFile))) {
|
|
1178
1213
|
unreadableSubagents++;
|
|
1179
1214
|
continue;
|
|
@@ -1182,7 +1217,9 @@ export class AgentControl {
|
|
|
1182
1217
|
const sessionId = manager.getSessionId();
|
|
1183
1218
|
if (countedSessionIds.has(sessionId)) continue;
|
|
1184
1219
|
countedSessionIds.add(sessionId);
|
|
1185
|
-
|
|
1220
|
+
const details = sessionUsageDetails(manager);
|
|
1221
|
+
addUsageTotals(subagents, details.totals);
|
|
1222
|
+
mergeUsageBreakdown(subagentBreakdown, details.breakdown);
|
|
1186
1223
|
} catch {
|
|
1187
1224
|
unreadableSubagents++;
|
|
1188
1225
|
}
|
|
@@ -1197,6 +1234,8 @@ export class AgentControl {
|
|
|
1197
1234
|
main,
|
|
1198
1235
|
subagents,
|
|
1199
1236
|
combined,
|
|
1237
|
+
mainBreakdown: finalizeBreakdown(mainDetails.breakdown),
|
|
1238
|
+
subagentBreakdown: finalizeBreakdown(subagentBreakdown),
|
|
1200
1239
|
subagentCount: this.agentsByPath.size,
|
|
1201
1240
|
unreadableSubagents,
|
|
1202
1241
|
};
|
|
@@ -1337,20 +1376,14 @@ export class AgentControl {
|
|
|
1337
1376
|
if (entry.type === "custom" && entry.customType === STATE_ENTRY_TYPE && isPersistedState(entry.data)) latest = entry.data;
|
|
1338
1377
|
}
|
|
1339
1378
|
if (!latest || latest.rootSessionId !== ctx.sessionManager.getSessionId()) return;
|
|
1340
|
-
let migratedAnyFile = false;
|
|
1341
1379
|
for (const persisted of latest.agents) {
|
|
1342
1380
|
const status: AgentLifecycleStatus = persisted.status === "queued" || (persisted.status === "pending_init" && Boolean(persisted.queuedMessage))
|
|
1343
1381
|
? "queued"
|
|
1344
1382
|
: persisted.status === "running" || persisted.status === "pending_init"
|
|
1345
1383
|
? "interrupted"
|
|
1346
1384
|
: persisted.status;
|
|
1347
|
-
const migratedSession = this.migrateStoredFile(persisted.sessionFile, this.childSessionDirectory);
|
|
1348
|
-
const migratedResult = this.migrateStoredFile(persisted.resultFile, this.agentResultDirectory);
|
|
1349
|
-
migratedAnyFile ||= migratedSession.migrated || migratedResult.migrated;
|
|
1350
1385
|
const record: AgentRecord = {
|
|
1351
1386
|
...persisted,
|
|
1352
|
-
sessionFile: migratedSession.path,
|
|
1353
|
-
resultFile: migratedResult.path,
|
|
1354
1387
|
status,
|
|
1355
1388
|
statusMessage: status === "queued"
|
|
1356
1389
|
? "waiting for an execution slot"
|
|
@@ -1365,7 +1398,6 @@ export class AgentControl {
|
|
|
1365
1398
|
this.pathBySessionId.set(record.id, record.path);
|
|
1366
1399
|
if (record.nickname) this.usedNicknames.add(record.nickname);
|
|
1367
1400
|
}
|
|
1368
|
-
if (migratedAnyFile) this.persistState();
|
|
1369
1401
|
}
|
|
1370
1402
|
|
|
1371
1403
|
private forkContextFromSessionManager(sessionManager: SessionManager): AgentMessage[] {
|
|
@@ -1388,29 +1420,10 @@ export class AgentControl {
|
|
|
1388
1420
|
if (!record.sessionFile) throw new Error(`agent ${record.path} has no persisted session file`);
|
|
1389
1421
|
if (!this.root) throw new Error("root session is not bound");
|
|
1390
1422
|
await this.evictForResidency(record.path);
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
record.sessionFile = migratedSessionFile;
|
|
1394
|
-
this.persistState();
|
|
1395
|
-
}
|
|
1396
|
-
let sessionManager: SessionManager;
|
|
1397
|
-
if (fs.existsSync(record.sessionFile)) {
|
|
1398
|
-
sessionManager = SessionManager.open(record.sessionFile);
|
|
1399
|
-
} else {
|
|
1400
|
-
// Older queue releases persisted only a future path. Recreate a durable
|
|
1401
|
-
// session with the recorded identity; its lost fork context is unrecoverable.
|
|
1402
|
-
sessionManager = SessionManager.create(this.root.cwd, this.childSessionDirectory, { id: record.id });
|
|
1403
|
-
sessionManager.appendCustomEntry(CHILD_META_ENTRY_TYPE, {
|
|
1404
|
-
path: record.path,
|
|
1405
|
-
parentPath: record.parentPath,
|
|
1406
|
-
rootSessionId: this.root.sessionId,
|
|
1407
|
-
role: record.role,
|
|
1408
|
-
});
|
|
1409
|
-
sessionManager.appendCustomEntry(FORK_CONTEXT_ENTRY_TYPE, { messages: [] });
|
|
1410
|
-
record.sessionFile = this.persistQueuedSession(sessionManager);
|
|
1411
|
-
this.persistState();
|
|
1412
|
-
sessionManager = SessionManager.open(record.sessionFile);
|
|
1423
|
+
if (!fs.existsSync(record.sessionFile)) {
|
|
1424
|
+
throw new Error(`agent ${record.path} session file does not exist: ${record.sessionFile}`);
|
|
1413
1425
|
}
|
|
1426
|
+
const sessionManager = SessionManager.open(record.sessionFile);
|
|
1414
1427
|
const forkContext = this.forkContextFromSessionManager(sessionManager);
|
|
1415
1428
|
const role = resolveRole(this.root.cwd, this.root.ctx.isProjectTrusted(), record.role);
|
|
1416
1429
|
const settingsManager = SettingsManager.create(this.root.cwd, getAgentDir());
|
package/index.ts
CHANGED
|
@@ -5,13 +5,19 @@ import { Text, truncateToWidth } from "@earendil-works/pi-tui";
|
|
|
5
5
|
import { AgentControl } from "./control.ts";
|
|
6
6
|
import { getAgentSettingsPath, loadAgentSettings, resolveAgentLimits } from "./settings.ts";
|
|
7
7
|
import { createCollaborationTools } from "./tools.ts";
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
import {
|
|
9
|
+
EXTENSION_ID,
|
|
10
|
+
ROOT_PATH,
|
|
11
|
+
USAGE_ENTRY_TYPE,
|
|
12
|
+
type AgentLifecycleStatus,
|
|
13
|
+
type AgentUsageReport,
|
|
14
|
+
type AgentView,
|
|
15
|
+
} from "./types.ts";
|
|
16
|
+
import { AgentPickerComponent, AgentTranscriptViewer, formatAgentUsage, renderAgentUsage } from "./viewer.ts";
|
|
11
17
|
|
|
12
18
|
const SELF_PATH = fileURLToPath(import.meta.url);
|
|
13
|
-
const WIDGET_KEY = "
|
|
14
|
-
const STATUS_KEY = "
|
|
19
|
+
const WIDGET_KEY = "pi-agents-tree";
|
|
20
|
+
const STATUS_KEY = "pi-agents";
|
|
15
21
|
const PROMPT_MARKER = "<multi_agent_role>";
|
|
16
22
|
|
|
17
23
|
function statusIcon(status: AgentLifecycleStatus): string {
|
|
@@ -85,8 +91,7 @@ class AgentTreeWidget {
|
|
|
85
91
|
invalidate(): void {}
|
|
86
92
|
}
|
|
87
93
|
|
|
88
|
-
export default function
|
|
89
|
-
const storageMigration = migrateLegacyAgentStorage();
|
|
94
|
+
export default function piAgentsExtension(pi: ExtensionAPI): void {
|
|
90
95
|
const limits = resolveAgentLimits(loadAgentSettings(), getAgentSettingsPath());
|
|
91
96
|
const control = new AgentControl(
|
|
92
97
|
pi,
|
|
@@ -100,7 +105,6 @@ export default function codexAgentsExtension(pi: ExtensionAPI): void {
|
|
|
100
105
|
|
|
101
106
|
let activeContext: ExtensionContext | undefined;
|
|
102
107
|
let widgetTui: { requestRender(): void } | undefined;
|
|
103
|
-
let storageMigrationReported = false;
|
|
104
108
|
|
|
105
109
|
const updateUi = () => {
|
|
106
110
|
const ctx = activeContext;
|
|
@@ -127,13 +131,6 @@ export default function codexAgentsExtension(pi: ExtensionAPI): void {
|
|
|
127
131
|
pi.on("session_start", (event, ctx) => {
|
|
128
132
|
activeContext = ctx;
|
|
129
133
|
control.bindRoot(ctx);
|
|
130
|
-
if (!storageMigrationReported) {
|
|
131
|
-
storageMigrationReported = true;
|
|
132
|
-
if (storageMigration.movedEntries > 0) {
|
|
133
|
-
ctx.ui.notify(`Migrated agent storage to ~/.pi/agent/pi-agents (${storageMigration.movedEntries} entries).`, "info");
|
|
134
|
-
}
|
|
135
|
-
for (const warning of storageMigration.warnings) ctx.ui.notify(`Agent storage migration: ${warning}`, "warning");
|
|
136
|
-
}
|
|
137
134
|
const resumedExistingSession = event.reason === "resume"
|
|
138
135
|
|| (event.reason === "startup" && ctx.sessionManager.getEntries().some((entry) => entry.type === "message"));
|
|
139
136
|
if (resumedExistingSession) {
|
|
@@ -209,32 +206,25 @@ export default function codexAgentsExtension(pi: ExtensionAPI): void {
|
|
|
209
206
|
return new Text(`${header}\n${theme.fg("customMessageText", body)}`, 1, 0);
|
|
210
207
|
});
|
|
211
208
|
|
|
209
|
+
pi.registerEntryRenderer<AgentUsageReport>(USAGE_ENTRY_TYPE, (entry, _options, theme) => {
|
|
210
|
+
return entry.data ? renderAgentUsage(entry.data, theme) : undefined;
|
|
211
|
+
});
|
|
212
|
+
|
|
212
213
|
pi.registerCommand("agent-usage", {
|
|
213
|
-
description: "Show main
|
|
214
|
+
description: "Show detailed main and sub-agent token usage by model",
|
|
214
215
|
handler: async (_args, ctx) => {
|
|
215
216
|
activeContext = ctx;
|
|
216
|
-
let report;
|
|
217
|
+
let report: AgentUsageReport;
|
|
217
218
|
try {
|
|
218
219
|
report = control.getUsage(ctx);
|
|
219
220
|
} catch (error) {
|
|
220
221
|
ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
|
|
221
222
|
return;
|
|
222
223
|
}
|
|
223
|
-
if (ctx.mode
|
|
224
|
+
if (ctx.mode === "tui") {
|
|
225
|
+
pi.appendEntry(USAGE_ENTRY_TYPE, report);
|
|
226
|
+
} else {
|
|
224
227
|
ctx.ui.notify(formatAgentUsage(report), "info");
|
|
225
|
-
return;
|
|
226
|
-
}
|
|
227
|
-
const releaseUserOverlay = control.beginUserOverlay();
|
|
228
|
-
try {
|
|
229
|
-
await ctx.ui.custom<void>(
|
|
230
|
-
(_tui, theme, keybindings, done) => new AgentUsageViewer(theme, keybindings, report, done),
|
|
231
|
-
{
|
|
232
|
-
overlay: true,
|
|
233
|
-
overlayOptions: { anchor: "center", width: "62%", maxHeight: "70%", margin: 1 },
|
|
234
|
-
},
|
|
235
|
-
);
|
|
236
|
-
} finally {
|
|
237
|
-
releaseUserOverlay();
|
|
238
228
|
}
|
|
239
229
|
},
|
|
240
230
|
});
|
package/package.json
CHANGED
package/settings.ts
CHANGED
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
-
import * as path from "node:path";
|
|
3
2
|
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
4
3
|
import {
|
|
5
4
|
clampThinkingLevel,
|
|
6
5
|
getSupportedThinkingLevels,
|
|
7
6
|
type Model,
|
|
8
7
|
} from "@earendil-works/pi-ai";
|
|
9
|
-
import {
|
|
10
|
-
getAgentSettingsPath as getCurrentAgentSettingsPath,
|
|
11
|
-
getLegacyAgentSettingsPaths,
|
|
12
|
-
} from "./storage.ts";
|
|
8
|
+
import { getAgentSettingsPath as getCurrentAgentSettingsPath } from "./storage.ts";
|
|
13
9
|
|
|
14
10
|
export const DEFAULT_MAX_CONCURRENT_SUBAGENTS = 3;
|
|
15
11
|
export const DEFAULT_MAX_RESIDENT_SUBAGENTS = 3;
|
|
@@ -56,34 +52,30 @@ export function selectAgentThinkingLevel(
|
|
|
56
52
|
}
|
|
57
53
|
|
|
58
54
|
export function loadAgentSettings(filePath = getAgentSettingsPath()): AgentSettings {
|
|
59
|
-
|
|
60
|
-
if (!fs.existsSync(resolvedPath) && path.resolve(filePath) === path.resolve(getAgentSettingsPath())) {
|
|
61
|
-
resolvedPath = getLegacyAgentSettingsPaths().find((candidate) => fs.existsSync(candidate)) ?? resolvedPath;
|
|
62
|
-
}
|
|
63
|
-
if (!fs.existsSync(resolvedPath)) return {};
|
|
55
|
+
if (!fs.existsSync(filePath)) return {};
|
|
64
56
|
|
|
65
57
|
let value: unknown;
|
|
66
58
|
try {
|
|
67
|
-
value = JSON.parse(fs.readFileSync(
|
|
59
|
+
value = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
68
60
|
} catch (error) {
|
|
69
61
|
const message = error instanceof Error ? error.message : String(error);
|
|
70
|
-
throw new Error(`failed to read agent settings at ${
|
|
62
|
+
throw new Error(`failed to read agent settings at ${filePath}: ${message}`);
|
|
71
63
|
}
|
|
72
64
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
73
|
-
throw new Error(`agent settings at ${
|
|
65
|
+
throw new Error(`agent settings at ${filePath} must contain a JSON object`);
|
|
74
66
|
}
|
|
75
67
|
|
|
76
68
|
const raw = value as Record<string, unknown>;
|
|
77
69
|
const settings: AgentSettings = {};
|
|
78
70
|
if (raw.defaultModel !== undefined) {
|
|
79
71
|
if (typeof raw.defaultModel !== "string" || !raw.defaultModel.trim()) {
|
|
80
|
-
throw new Error(`defaultModel in ${
|
|
72
|
+
throw new Error(`defaultModel in ${filePath} must be a non-empty provider/model string`);
|
|
81
73
|
}
|
|
82
74
|
settings.defaultModel = raw.defaultModel.trim();
|
|
83
75
|
}
|
|
84
76
|
if (raw.defaultThinkingLevel !== undefined) {
|
|
85
77
|
if (typeof raw.defaultThinkingLevel !== "string" || !CHILD_THINKING_LEVELS.includes(raw.defaultThinkingLevel as ThinkingLevel)) {
|
|
86
|
-
throw new Error(`defaultThinkingLevel in ${
|
|
78
|
+
throw new Error(`defaultThinkingLevel in ${filePath} must be one of: ${CHILD_THINKING_LEVELS.join(", ")}`);
|
|
87
79
|
}
|
|
88
80
|
settings.defaultThinkingLevel = raw.defaultThinkingLevel as ThinkingLevel;
|
|
89
81
|
}
|
|
@@ -91,7 +83,7 @@ export function loadAgentSettings(filePath = getAgentSettingsPath()): AgentSetti
|
|
|
91
83
|
const limit = raw[key];
|
|
92
84
|
if (limit === undefined) continue;
|
|
93
85
|
if (typeof limit !== "number" || !Number.isSafeInteger(limit) || limit < 1) {
|
|
94
|
-
throw new Error(`${key} in ${
|
|
86
|
+
throw new Error(`${key} in ${filePath} must be a positive integer`);
|
|
95
87
|
}
|
|
96
88
|
settings[key] = limit;
|
|
97
89
|
}
|
package/storage.ts
CHANGED
|
@@ -1,87 +1,13 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
1
|
import * as path from "node:path";
|
|
3
2
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
4
3
|
|
|
5
4
|
const STORAGE_DIRECTORY_NAME = "pi-agents";
|
|
6
|
-
const LEGACY_STORAGE_DIRECTORY_NAME = "codex-agents";
|
|
7
5
|
const SETTINGS_FILE_NAME = "settings.json";
|
|
8
|
-
const LEGACY_SETTINGS_FILE_NAME = "agents-setting.json";
|
|
9
|
-
|
|
10
|
-
export interface StorageMigrationReport {
|
|
11
|
-
movedEntries: number;
|
|
12
|
-
warnings: string[];
|
|
13
|
-
}
|
|
14
6
|
|
|
15
7
|
export function getAgentStorageDirectory(): string {
|
|
16
8
|
return path.join(getAgentDir(), STORAGE_DIRECTORY_NAME);
|
|
17
9
|
}
|
|
18
10
|
|
|
19
|
-
export function getLegacyAgentStorageDirectory(): string {
|
|
20
|
-
return path.join(getAgentDir(), LEGACY_STORAGE_DIRECTORY_NAME);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
11
|
export function getAgentSettingsPath(): string {
|
|
24
12
|
return path.join(getAgentStorageDirectory(), SETTINGS_FILE_NAME);
|
|
25
13
|
}
|
|
26
|
-
|
|
27
|
-
export function getLegacyAgentSettingsPaths(): string[] {
|
|
28
|
-
return [
|
|
29
|
-
path.join(getAgentStorageDirectory(), LEGACY_SETTINGS_FILE_NAME),
|
|
30
|
-
path.join(getLegacyAgentStorageDirectory(), LEGACY_SETTINGS_FILE_NAME),
|
|
31
|
-
];
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function mergeWithoutOverwrite(source: string, destination: string, report: StorageMigrationReport): void {
|
|
35
|
-
if (!fs.existsSync(source)) return;
|
|
36
|
-
if (!fs.existsSync(destination)) {
|
|
37
|
-
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
38
|
-
fs.renameSync(source, destination);
|
|
39
|
-
report.movedEntries++;
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
const sourceStat = fs.statSync(source);
|
|
43
|
-
const destinationStat = fs.statSync(destination);
|
|
44
|
-
if (!sourceStat.isDirectory() || !destinationStat.isDirectory()) {
|
|
45
|
-
report.warnings.push(`storage migration left a conflicting path untouched: ${source}`);
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
for (const entry of fs.readdirSync(source)) {
|
|
49
|
-
mergeWithoutOverwrite(path.join(source, entry), path.join(destination, entry), report);
|
|
50
|
-
}
|
|
51
|
-
try {
|
|
52
|
-
if (fs.readdirSync(source).length === 0) fs.rmdirSync(source);
|
|
53
|
-
} catch {
|
|
54
|
-
// A partial migration remains readable through the legacy path fallback.
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Preserve the 0.7.x upgrade path. For a fresh installation this is only an
|
|
60
|
-
* existence check and performs no writes.
|
|
61
|
-
*/
|
|
62
|
-
export function migrateLegacyAgentStorage(): StorageMigrationReport {
|
|
63
|
-
const report: StorageMigrationReport = { movedEntries: 0, warnings: [] };
|
|
64
|
-
const source = getLegacyAgentStorageDirectory();
|
|
65
|
-
const destination = getAgentStorageDirectory();
|
|
66
|
-
try {
|
|
67
|
-
mergeWithoutOverwrite(source, destination, report);
|
|
68
|
-
const legacySettings = path.join(destination, LEGACY_SETTINGS_FILE_NAME);
|
|
69
|
-
const settings = getAgentSettingsPath();
|
|
70
|
-
if (fs.existsSync(legacySettings)) mergeWithoutOverwrite(legacySettings, settings, report);
|
|
71
|
-
} catch (error) {
|
|
72
|
-
report.warnings.push(error instanceof Error ? error.message : String(error));
|
|
73
|
-
}
|
|
74
|
-
return report;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** Translate paths persisted before the storage directory rename. */
|
|
78
|
-
export function resolveMigratedStoragePath(file: string): string {
|
|
79
|
-
const source = path.resolve(file);
|
|
80
|
-
const legacyRoot = path.resolve(getLegacyAgentStorageDirectory());
|
|
81
|
-
if (source !== legacyRoot && !source.startsWith(`${legacyRoot}${path.sep}`)) return source;
|
|
82
|
-
// A collision-safe merge leaves the legacy source in place. Prefer the exact
|
|
83
|
-
// persisted path instead of shadowing it with a different destination file.
|
|
84
|
-
if (fs.existsSync(source)) return source;
|
|
85
|
-
const translated = path.join(getAgentStorageDirectory(), path.relative(legacyRoot, source));
|
|
86
|
-
return fs.existsSync(translated) ? translated : source;
|
|
87
|
-
}
|
package/types.ts
CHANGED
|
@@ -2,10 +2,11 @@ import type { AgentMessage, ThinkingLevel } from "@earendil-works/pi-agent-core"
|
|
|
2
2
|
import type { Model } from "@earendil-works/pi-ai";
|
|
3
3
|
import type { AgentSession, ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
|
|
5
|
-
export const EXTENSION_ID = "
|
|
6
|
-
export const STATE_ENTRY_TYPE = "
|
|
7
|
-
export const CHILD_META_ENTRY_TYPE = "
|
|
8
|
-
export const FORK_CONTEXT_ENTRY_TYPE = "
|
|
5
|
+
export const EXTENSION_ID = "pi-agents";
|
|
6
|
+
export const STATE_ENTRY_TYPE = "pi-agents-state";
|
|
7
|
+
export const CHILD_META_ENTRY_TYPE = "pi-agents-child-meta";
|
|
8
|
+
export const FORK_CONTEXT_ENTRY_TYPE = "pi-agents-fork-context";
|
|
9
|
+
export const USAGE_ENTRY_TYPE = "pi-agents-usage";
|
|
9
10
|
export const ROOT_PATH = "/root";
|
|
10
11
|
export const DIRECT_AGENT_TOOL_NAMES = [
|
|
11
12
|
"spawn_agents",
|
|
@@ -113,10 +114,19 @@ export interface AgentUsageTotals {
|
|
|
113
114
|
cost: number;
|
|
114
115
|
}
|
|
115
116
|
|
|
117
|
+
export interface AgentUsageBreakdownEntry {
|
|
118
|
+
key: string;
|
|
119
|
+
usage: AgentUsageTotals;
|
|
120
|
+
sessionCount: number;
|
|
121
|
+
operations: number;
|
|
122
|
+
}
|
|
123
|
+
|
|
116
124
|
export interface AgentUsageReport {
|
|
117
125
|
main: AgentUsageTotals;
|
|
118
126
|
subagents: AgentUsageTotals;
|
|
119
127
|
combined: AgentUsageTotals;
|
|
128
|
+
mainBreakdown: AgentUsageBreakdownEntry[];
|
|
129
|
+
subagentBreakdown: AgentUsageBreakdownEntry[];
|
|
120
130
|
subagentCount: number;
|
|
121
131
|
unreadableSubagents: number;
|
|
122
132
|
}
|
package/viewer.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
import {
|
|
21
21
|
ROOT_PATH,
|
|
22
22
|
type AgentTranscriptView,
|
|
23
|
+
type AgentUsageBreakdownEntry,
|
|
23
24
|
type AgentUsageReport,
|
|
24
25
|
type AgentUsageTotals,
|
|
25
26
|
type AgentView,
|
|
@@ -88,66 +89,65 @@ function usageCacheRate(usage: AgentUsageTotals): string {
|
|
|
88
89
|
return prompt > 0 ? `${((usage.cacheRead / prompt) * 100).toFixed(1)}%` : "n/a";
|
|
89
90
|
}
|
|
90
91
|
|
|
92
|
+
function usageLine(label: string, usage: AgentUsageTotals): string {
|
|
93
|
+
const parts = [
|
|
94
|
+
`${label}: ${usage.total.toLocaleString()} tokens`,
|
|
95
|
+
`input ${usagePromptTokens(usage).toLocaleString()}`,
|
|
96
|
+
`output ${usage.output.toLocaleString()}`,
|
|
97
|
+
`cached ${usage.cacheRead.toLocaleString()} (${usageCacheRate(usage)})`,
|
|
98
|
+
];
|
|
99
|
+
if (usage.cacheWrite > 0) parts.push(`cache write ${usage.cacheWrite.toLocaleString()}`);
|
|
100
|
+
if (usage.cost > 0) parts.push(`$${usage.cost.toFixed(3)}`);
|
|
101
|
+
return parts.join(" · ");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function breakdownLines(
|
|
105
|
+
title: string,
|
|
106
|
+
items: AgentUsageBreakdownEntry[],
|
|
107
|
+
sessionLabel: string,
|
|
108
|
+
): string[] {
|
|
109
|
+
const lines = [title];
|
|
110
|
+
if (items.length === 0) {
|
|
111
|
+
lines.push(" (no billed usage)");
|
|
112
|
+
return lines;
|
|
113
|
+
}
|
|
114
|
+
for (const item of items) {
|
|
115
|
+
const sessions = `${item.sessionCount.toLocaleString()} ${sessionLabel}${item.sessionCount === 1 ? "" : "s"}`;
|
|
116
|
+
const operations = `${item.operations.toLocaleString()} usage record${item.operations === 1 ? "" : "s"}`;
|
|
117
|
+
lines.push(` ${item.key}`);
|
|
118
|
+
lines.push(` ${usageLine("Usage", item.usage)} · ${sessions} · ${operations}`);
|
|
119
|
+
}
|
|
120
|
+
return lines;
|
|
121
|
+
}
|
|
122
|
+
|
|
91
123
|
export function formatAgentUsage(report: AgentUsageReport): string {
|
|
124
|
+
const counted = report.subagentCount - report.unreadableSubagents;
|
|
92
125
|
const lines = [
|
|
93
126
|
"Agent Usage",
|
|
94
127
|
"",
|
|
95
|
-
|
|
96
|
-
`
|
|
97
|
-
`
|
|
128
|
+
"Totals",
|
|
129
|
+
` ${usageLine("Main agent", report.main)}`,
|
|
130
|
+
` ${usageLine("Sub-agents", report.subagents)}`,
|
|
131
|
+
` ${usageLine("Combined", report.combined)}`,
|
|
132
|
+
` Sub-agent sessions: ${counted.toLocaleString()}/${report.subagentCount.toLocaleString()} readable`,
|
|
133
|
+
"",
|
|
134
|
+
...breakdownLines("Main-agent models", report.mainBreakdown, "session"),
|
|
98
135
|
"",
|
|
99
|
-
|
|
136
|
+
...breakdownLines("Sub-agent models", report.subagentBreakdown, "agent"),
|
|
100
137
|
];
|
|
101
|
-
if (report.
|
|
102
|
-
if (report.unreadableSubagents > 0) lines.push(`Unreadable sessions: ${report.unreadableSubagents}`);
|
|
138
|
+
if (report.unreadableSubagents > 0) lines.push("", `Unreadable sub-agent sessions: ${report.unreadableSubagents.toLocaleString()}`);
|
|
103
139
|
return lines.join("\n");
|
|
104
140
|
}
|
|
105
141
|
|
|
106
|
-
export
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if (this.keybindings.matches(data, "tui.select.cancel") || this.keybindings.matches(data, "tui.select.confirm")) {
|
|
116
|
-
this.done();
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
render(width: number): string[] {
|
|
121
|
-
const innerWidth = Math.max(1, width - 2);
|
|
122
|
-
const row = (label: string, usage: AgentUsageTotals, includeCache: boolean): string => {
|
|
123
|
-
const cache = includeCache ? ` · cache ${usageCacheRate(usage)}` : "";
|
|
124
|
-
return ` ${this.theme.fg("dim", `${label}:`)} ${usage.total.toLocaleString()} tokens${cache}`;
|
|
125
|
-
};
|
|
126
|
-
const lines = [
|
|
127
|
-
framedRule(this.theme, innerWidth, "╭", "╮"),
|
|
128
|
-
framedRow(this.theme, ` ${this.theme.fg("accent", this.theme.bold("Agent Usage"))}`, innerWidth),
|
|
129
|
-
framedRule(this.theme, innerWidth, "├", "┤"),
|
|
130
|
-
framedRow(this.theme, row("Main agent", this.report.main, true), innerWidth),
|
|
131
|
-
framedRow(this.theme, row("Sub-agents", this.report.subagents, true), innerWidth),
|
|
132
|
-
framedRow(this.theme, row("Combined", this.report.combined, false), innerWidth),
|
|
133
|
-
framedRow(this.theme, "", innerWidth),
|
|
134
|
-
framedRow(this.theme, ` ${this.theme.fg("dim", "Sub-agents counted:")} ${this.report.subagentCount - this.report.unreadableSubagents}/${this.report.subagentCount}`, innerWidth),
|
|
135
|
-
];
|
|
136
|
-
if (this.report.combined.cost > 0) {
|
|
137
|
-
lines.push(framedRow(this.theme, ` ${this.theme.fg("dim", "Combined cost:")} $${this.report.combined.cost.toFixed(3)}`, innerWidth));
|
|
138
|
-
}
|
|
139
|
-
if (this.report.unreadableSubagents > 0) {
|
|
140
|
-
lines.push(framedRow(this.theme, ` ${this.theme.fg("warning", `Unreadable sessions: ${this.report.unreadableSubagents}`)}`, innerWidth));
|
|
141
|
-
}
|
|
142
|
-
lines.push(
|
|
143
|
-
framedRule(this.theme, innerWidth, "├", "┤"),
|
|
144
|
-
framedRow(this.theme, ` ${this.theme.fg("dim", "Enter / Esc close")}`, innerWidth),
|
|
145
|
-
framedRule(this.theme, innerWidth, "╰", "╯"),
|
|
146
|
-
);
|
|
147
|
-
return lines;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
invalidate(): void {}
|
|
142
|
+
export function renderAgentUsage(report: AgentUsageReport, theme: Theme): Text {
|
|
143
|
+
const headings = new Set(["Agent Usage", "Totals", "Main-agent models", "Sub-agent models"]);
|
|
144
|
+
const rendered = formatAgentUsage(report)
|
|
145
|
+
.split("\n")
|
|
146
|
+
.map((line) => headings.has(line)
|
|
147
|
+
? (line === "Agent Usage" ? theme.bold(line) : theme.bold(theme.fg("accent", line)))
|
|
148
|
+
: line.startsWith("Unreadable ") ? theme.fg("warning", line) : line)
|
|
149
|
+
.join("\n");
|
|
150
|
+
return new Text(rendered, 1, 0);
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
export class AgentPickerComponent {
|