tledger 0.1.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/README.md +70 -0
- package/bin/token-ledger-terminal.mjs +693 -0
- package/bin/token-ledger-tui.mjs +95 -0
- package/bin/token-ledger.mjs +568 -0
- package/lib/token-ledger-collector.mjs +1248 -0
- package/package.json +43 -0
|
@@ -0,0 +1,693 @@
|
|
|
1
|
+
const RESET = "\u001b[0m";
|
|
2
|
+
const DIM = [38, 5, 245];
|
|
3
|
+
const TEAL = [38, 5, 80];
|
|
4
|
+
export const MODEL_COLORS = {
|
|
5
|
+
sol: [38, 5, 67],
|
|
6
|
+
luna: [38, 5, 80],
|
|
7
|
+
terra: [38, 5, 179],
|
|
8
|
+
gpt: [38, 5, 176],
|
|
9
|
+
autoReview: [38, 5, 153],
|
|
10
|
+
other: [38, 5, 60],
|
|
11
|
+
};
|
|
12
|
+
const TEXT_STYLE = [38, 5, 255];
|
|
13
|
+
const TITLE_STYLE = [1, 38, 5, 255];
|
|
14
|
+
const SUBTITLE_STYLE = [38, 5, 245];
|
|
15
|
+
|
|
16
|
+
function colorsEnabled(options) {
|
|
17
|
+
return options.forceColor ?? (!options.plain && !process.env.NO_COLOR && Boolean(process.stdout.isTTY));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function colorize(value, code, enabled) {
|
|
21
|
+
const codes = Array.isArray(code) ? code.join(";") : code;
|
|
22
|
+
return enabled ? `\u001b[${codes}m${value}${RESET}` : value;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function stripAnsi(value) {
|
|
26
|
+
return String(value)
|
|
27
|
+
.replace(/\u001b\][^\u0007]*(?:\u0007|\u001b\\)/g, "")
|
|
28
|
+
.replace(/\u001b\[[0-?]*[ -/]*[@-~]/g, "");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function sanitizeText(value) {
|
|
32
|
+
return stripAnsi(value)
|
|
33
|
+
.replace(/[\u0000-\u001f\u007f-\u009f]+/g, " ")
|
|
34
|
+
.replace(/\s+/g, " ")
|
|
35
|
+
.trim();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function visibleLength(value) {
|
|
39
|
+
return stripAnsi(value).length;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function truncateText(value, width) {
|
|
43
|
+
const text = String(value);
|
|
44
|
+
if (width <= 0) return "";
|
|
45
|
+
if (text.length <= width) return text;
|
|
46
|
+
if (width === 1) return "…";
|
|
47
|
+
|
|
48
|
+
const available = width - 1;
|
|
49
|
+
const prefix = text.slice(0, available);
|
|
50
|
+
const breakAt = prefix.lastIndexOf(" ");
|
|
51
|
+
const cleanPrefix = breakAt >= Math.floor(available * 0.6)
|
|
52
|
+
? prefix.slice(0, breakAt)
|
|
53
|
+
: prefix;
|
|
54
|
+
return `${cleanPrefix.replace(/\s+$/, "")}…`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function fit(value, width, alignment = "left") {
|
|
58
|
+
const text = String(value);
|
|
59
|
+
const length = visibleLength(text);
|
|
60
|
+
if (length > width) return truncateText(stripAnsi(text), width);
|
|
61
|
+
const padding = " ".repeat(width - length);
|
|
62
|
+
if (alignment === "right") return `${padding}${text}`;
|
|
63
|
+
if (alignment === "center") {
|
|
64
|
+
const left = Math.floor(padding.length / 2);
|
|
65
|
+
return `${" ".repeat(left)}${text}${" ".repeat(padding.length - left)}`;
|
|
66
|
+
}
|
|
67
|
+
return `${text}${padding}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function compact(value) {
|
|
71
|
+
if (!Number.isFinite(value)) return "—";
|
|
72
|
+
const absolute = Math.abs(value);
|
|
73
|
+
const units = [
|
|
74
|
+
[1_000_000_000, "B"],
|
|
75
|
+
[1_000_000, "M"],
|
|
76
|
+
[1_000, "K"],
|
|
77
|
+
];
|
|
78
|
+
for (const [divisor, suffix] of units) {
|
|
79
|
+
if (absolute >= divisor) {
|
|
80
|
+
const scaled = value / divisor;
|
|
81
|
+
const precision = scaled >= 100 ? 0 : scaled >= 10 ? 1 : 2;
|
|
82
|
+
return `${scaled.toFixed(precision)}${suffix}`;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return Math.round(value).toLocaleString("en-US");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function percent(value) {
|
|
89
|
+
return `${value.toFixed(value >= 10 ? 1 : 2)}%`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function plural(value, singular, pluralForm = `${singular}s`) {
|
|
93
|
+
return `${value.toLocaleString("en-US")} ${value === 1 ? singular : pluralForm}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function modelLabel(value) {
|
|
97
|
+
const model = sanitizeText(value) || "Unknown model";
|
|
98
|
+
const lower = model.toLowerCase().replaceAll("_", "-");
|
|
99
|
+
if (lower === "codex-auto-review" || lower.startsWith("codex-auto-review-")) {
|
|
100
|
+
return "Auto Review";
|
|
101
|
+
}
|
|
102
|
+
if (lower.includes("sol")) return "Sol";
|
|
103
|
+
if (lower.includes("luna")) return "Luna";
|
|
104
|
+
if (lower.includes("terra")) return "Terra";
|
|
105
|
+
if (lower.includes("gpt-5.5") || lower.includes("gpt-5.4")) return "GPT";
|
|
106
|
+
return "Other";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function modelColor(model) {
|
|
110
|
+
const key = String(model || "")
|
|
111
|
+
.toLowerCase()
|
|
112
|
+
.replace(/[\s_-]+/g, "-");
|
|
113
|
+
if (key === "auto-review") return MODEL_COLORS.autoReview;
|
|
114
|
+
return MODEL_COLORS[key] ?? MODEL_COLORS.other;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function usageTypeLabel(value) {
|
|
118
|
+
const words = (sanitizeText(value) || "unknown")
|
|
119
|
+
.replace(/[_-]+/g, " ")
|
|
120
|
+
.split(/\s+/)
|
|
121
|
+
.filter(Boolean);
|
|
122
|
+
if (words.length === 0) return "Unknown";
|
|
123
|
+
return words
|
|
124
|
+
.map((word) => {
|
|
125
|
+
const lower = word.toLowerCase();
|
|
126
|
+
if (["api", "cli", "sdk", "ui"].includes(lower)) return lower.toUpperCase();
|
|
127
|
+
return `${lower.charAt(0).toUpperCase()}${lower.slice(1)}`;
|
|
128
|
+
})
|
|
129
|
+
.join(" ");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function displayProject(row) {
|
|
133
|
+
return sanitizeText(row.displayProject || row.project) || "Unlabelled activity";
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function dateLabel(bounds, range = "day") {
|
|
137
|
+
if (range === "week" && bounds.startDateString && bounds.endDateString) {
|
|
138
|
+
const startParts = bounds.startDateString.split("-").map(Number);
|
|
139
|
+
const endParts = bounds.endDateString.split("-").map(Number);
|
|
140
|
+
const monthNames = [
|
|
141
|
+
"JAN", "FEB", "MAR", "APR", "MAY", "JUN",
|
|
142
|
+
"JUL", "AUG", "SEP", "OCT", "NOV", "DEC",
|
|
143
|
+
];
|
|
144
|
+
const start = `${monthNames[startParts[1] - 1]} ${String(startParts[2]).padStart(2, "0")}`;
|
|
145
|
+
const end = `${monthNames[endParts[1] - 1]} ${String(endParts[2]).padStart(2, "0")}`;
|
|
146
|
+
return `${start} – ${end} ${endParts[0]}`;
|
|
147
|
+
}
|
|
148
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
149
|
+
timeZone: bounds.timeZone,
|
|
150
|
+
weekday: "short",
|
|
151
|
+
day: "2-digit",
|
|
152
|
+
month: "short",
|
|
153
|
+
year: "numeric",
|
|
154
|
+
}).formatToParts(bounds.start);
|
|
155
|
+
const values = Object.fromEntries(
|
|
156
|
+
parts
|
|
157
|
+
.filter((part) => part.type !== "literal")
|
|
158
|
+
.map((part) => [part.type, part.value]),
|
|
159
|
+
);
|
|
160
|
+
return `${values.weekday} ${values.day} ${values.month} ${values.year}`.toUpperCase();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function modelTotals(events) {
|
|
164
|
+
const totals = new Map();
|
|
165
|
+
for (const event of events) {
|
|
166
|
+
const model = modelLabel(event.model);
|
|
167
|
+
totals.set(model, (totals.get(model) ?? 0) + (Number(event.totalTokens) || 0));
|
|
168
|
+
}
|
|
169
|
+
return [...totals.entries()]
|
|
170
|
+
.map(([model, totalTokens]) => ({ model, totalTokens }))
|
|
171
|
+
.sort((left, right) => right.totalTokens - left.totalTokens);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function usageTypeTotals(events) {
|
|
175
|
+
const totals = new Map();
|
|
176
|
+
for (const event of events) {
|
|
177
|
+
const key = modelLabel(event.model) === "Auto Review"
|
|
178
|
+
? "auto-review"
|
|
179
|
+
: String(event.useType || "unknown").trim().toLowerCase() || "unknown";
|
|
180
|
+
totals.set(key, (totals.get(key) ?? 0) + (Number(event.totalTokens) || 0));
|
|
181
|
+
}
|
|
182
|
+
return [...totals.entries()]
|
|
183
|
+
.map(([key, totalTokens]) => ({
|
|
184
|
+
key,
|
|
185
|
+
label: usageTypeLabel(key),
|
|
186
|
+
totalTokens,
|
|
187
|
+
}))
|
|
188
|
+
.sort((left, right) => right.totalTokens - left.totalTokens);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function latestWeeklyQuotaObservation(observations) {
|
|
192
|
+
const weekly = (observations ?? []).filter(
|
|
193
|
+
(item) => Number(item.windowMinutes) === 10_080,
|
|
194
|
+
);
|
|
195
|
+
const accountWide = weekly.filter((item) => item.scope !== "named");
|
|
196
|
+
const candidates = accountWide.length ? accountWide : weekly;
|
|
197
|
+
return [...candidates]
|
|
198
|
+
.sort(
|
|
199
|
+
(left, right) =>
|
|
200
|
+
new Date(left.timestamp).getTime() - new Date(right.timestamp).getTime(),
|
|
201
|
+
)
|
|
202
|
+
.at(-1) ?? null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function quotaCycleSummary(snapshot = {}, displayedEvents = []) {
|
|
206
|
+
const observation = latestWeeklyQuotaObservation(snapshot.quotaObservations);
|
|
207
|
+
if (!observation) {
|
|
208
|
+
return {
|
|
209
|
+
available: false,
|
|
210
|
+
usedPercent: null,
|
|
211
|
+
remainingPercent: null,
|
|
212
|
+
cycleTokens: 0,
|
|
213
|
+
displayedTokens: 0,
|
|
214
|
+
displayedSharePercent: null,
|
|
215
|
+
estimatedDisplayedBurnPercent: null,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const windowStartMs =
|
|
220
|
+
(Number(observation.resetsAt) - Number(observation.windowMinutes) * 60) * 1_000;
|
|
221
|
+
const resetAtMs = Number(observation.resetsAt) * 1_000;
|
|
222
|
+
const observedThroughMs = Math.min(
|
|
223
|
+
resetAtMs,
|
|
224
|
+
new Date(observation.eventCutoffAt ?? observation.timestamp).getTime(),
|
|
225
|
+
);
|
|
226
|
+
if (
|
|
227
|
+
!Number.isFinite(windowStartMs) ||
|
|
228
|
+
!Number.isFinite(resetAtMs) ||
|
|
229
|
+
!Number.isFinite(observedThroughMs) ||
|
|
230
|
+
observedThroughMs < windowStartMs
|
|
231
|
+
) {
|
|
232
|
+
return {
|
|
233
|
+
available: false,
|
|
234
|
+
usedPercent: null,
|
|
235
|
+
remainingPercent: null,
|
|
236
|
+
cycleTokens: 0,
|
|
237
|
+
displayedTokens: 0,
|
|
238
|
+
displayedSharePercent: null,
|
|
239
|
+
estimatedDisplayedBurnPercent: null,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const inObservedCycle = (event) => {
|
|
244
|
+
const eventMs = new Date(event.timestamp).getTime();
|
|
245
|
+
return (
|
|
246
|
+
Number.isFinite(eventMs) &&
|
|
247
|
+
eventMs >= windowStartMs &&
|
|
248
|
+
eventMs <= observedThroughMs
|
|
249
|
+
);
|
|
250
|
+
};
|
|
251
|
+
const sumTokens = (events) =>
|
|
252
|
+
events.reduce((sum, event) => sum + (Number(event.totalTokens) || 0), 0);
|
|
253
|
+
const cycleTokens = sumTokens((snapshot.events ?? []).filter(inObservedCycle));
|
|
254
|
+
const displayedTokens = sumTokens(displayedEvents.filter(inObservedCycle));
|
|
255
|
+
const usedPercent = Math.min(100, Math.max(0, Number(observation.usedPercent) || 0));
|
|
256
|
+
const displayedSharePercent = cycleTokens
|
|
257
|
+
? (displayedTokens / cycleTokens) * 100
|
|
258
|
+
: null;
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
available: true,
|
|
262
|
+
usedPercent,
|
|
263
|
+
remainingPercent: 100 - usedPercent,
|
|
264
|
+
cycleTokens,
|
|
265
|
+
displayedTokens,
|
|
266
|
+
displayedSharePercent,
|
|
267
|
+
estimatedDisplayedBurnPercent:
|
|
268
|
+
displayedSharePercent === null
|
|
269
|
+
? null
|
|
270
|
+
: (usedPercent * displayedSharePercent) / 100,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function summary(events) {
|
|
275
|
+
const totalTokens = events.reduce(
|
|
276
|
+
(sum, event) => sum + (Number(event.totalTokens) || 0),
|
|
277
|
+
0,
|
|
278
|
+
);
|
|
279
|
+
const calls = events.length;
|
|
280
|
+
const threadIds = new Set(events.map((event) => event.threadId).filter(Boolean));
|
|
281
|
+
const outputTokens = events.reduce(
|
|
282
|
+
(sum, event) => sum + (Number(event.outputTokens) || 0),
|
|
283
|
+
0,
|
|
284
|
+
);
|
|
285
|
+
const inputTokens = events.reduce(
|
|
286
|
+
(sum, event) => sum + (Number(event.inputTokens) || 0),
|
|
287
|
+
0,
|
|
288
|
+
);
|
|
289
|
+
const cachedInputTokens = events.reduce((sum, event) => {
|
|
290
|
+
const input = Math.max(0, Number(event.inputTokens) || 0);
|
|
291
|
+
const cached = Math.max(0, Number(event.cachedInputTokens) || 0);
|
|
292
|
+
return sum + Math.min(input, cached);
|
|
293
|
+
}, 0);
|
|
294
|
+
const turnCount = (rows) => {
|
|
295
|
+
const keys = new Set();
|
|
296
|
+
for (const [index, event] of rows.entries()) {
|
|
297
|
+
const turnId = String(event.turnId || "").trim();
|
|
298
|
+
keys.add(turnId ? `turn:${turnId}` : `event:${event.id || index}`);
|
|
299
|
+
}
|
|
300
|
+
return keys.size;
|
|
301
|
+
};
|
|
302
|
+
const totalTurns = turnCount(events);
|
|
303
|
+
const autoReviewEvents = events.filter(
|
|
304
|
+
(event) => modelLabel(event.model) === "Auto Review",
|
|
305
|
+
);
|
|
306
|
+
const autoReviewTokens = autoReviewEvents.reduce(
|
|
307
|
+
(sum, event) => sum + (Number(event.totalTokens) || 0),
|
|
308
|
+
0,
|
|
309
|
+
);
|
|
310
|
+
const autoReviewInputTokens = autoReviewEvents.reduce(
|
|
311
|
+
(sum, event) => sum + Math.max(0, Number(event.inputTokens) || 0),
|
|
312
|
+
0,
|
|
313
|
+
);
|
|
314
|
+
const autoReviewCachedInputTokens = autoReviewEvents.reduce((sum, event) => {
|
|
315
|
+
const input = Math.max(0, Number(event.inputTokens) || 0);
|
|
316
|
+
const cached = Math.max(0, Number(event.cachedInputTokens) || 0);
|
|
317
|
+
return sum + Math.min(input, cached);
|
|
318
|
+
}, 0);
|
|
319
|
+
const autoReviewTurns = turnCount(autoReviewEvents);
|
|
320
|
+
return {
|
|
321
|
+
totalTokens,
|
|
322
|
+
calls,
|
|
323
|
+
threads: threadIds.size,
|
|
324
|
+
outputTokens,
|
|
325
|
+
inputTokens,
|
|
326
|
+
cachedInputTokens,
|
|
327
|
+
uncachedInputTokens: Math.max(0, inputTokens - cachedInputTokens),
|
|
328
|
+
models: modelTotals(events),
|
|
329
|
+
usageTypes: usageTypeTotals(events),
|
|
330
|
+
autoReview: {
|
|
331
|
+
present: autoReviewEvents.length > 0,
|
|
332
|
+
totalTokens: autoReviewTokens,
|
|
333
|
+
turns: autoReviewTurns,
|
|
334
|
+
turnShare: totalTurns > 0 ? (autoReviewTurns / totalTurns) * 100 : 0,
|
|
335
|
+
cachedInputShare: autoReviewInputTokens > 0
|
|
336
|
+
? (autoReviewCachedInputTokens / autoReviewInputTokens) * 100
|
|
337
|
+
: 0,
|
|
338
|
+
},
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function modelLegendItems(models, totalTokens) {
|
|
343
|
+
const known = new Map();
|
|
344
|
+
for (const model of models) {
|
|
345
|
+
const key = ["Sol", "Luna", "Terra", "GPT", "Auto Review"].includes(model.model)
|
|
346
|
+
? model.model
|
|
347
|
+
: "Other";
|
|
348
|
+
known.set(key, (known.get(key) ?? 0) + model.totalTokens);
|
|
349
|
+
}
|
|
350
|
+
const order = ["Luna", "Sol", "Terra", "GPT"];
|
|
351
|
+
if ((known.get("Auto Review") ?? 0) > 0) order.push("Auto Review");
|
|
352
|
+
order.push("Other");
|
|
353
|
+
return order
|
|
354
|
+
.map((model) => ({
|
|
355
|
+
model,
|
|
356
|
+
totalTokens: known.get(model) ?? 0,
|
|
357
|
+
share: totalTokens > 0 ? ((known.get(model) ?? 0) / totalTokens) * 100 : 0,
|
|
358
|
+
}));
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function visibleUsageTypeItems(items, limit = 5) {
|
|
362
|
+
if (items.length <= limit) return items;
|
|
363
|
+
const visible = items.slice(0, limit - 1);
|
|
364
|
+
const autoReview = items.find((item) => item.key === "auto-review");
|
|
365
|
+
if (autoReview && !visible.includes(autoReview)) {
|
|
366
|
+
visible[visible.length - 1] = autoReview;
|
|
367
|
+
visible.sort((left, right) => right.totalTokens - left.totalTokens);
|
|
368
|
+
}
|
|
369
|
+
const visibleKeys = new Set(visible.map((item) => item.key));
|
|
370
|
+
return [
|
|
371
|
+
...visible,
|
|
372
|
+
{
|
|
373
|
+
key: "other",
|
|
374
|
+
label: "Other",
|
|
375
|
+
totalTokens: items
|
|
376
|
+
.filter((item) => !visibleKeys.has(item.key))
|
|
377
|
+
.reduce((sum, item) => sum + item.totalTokens, 0),
|
|
378
|
+
},
|
|
379
|
+
];
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function stackedBar(row, width, maximumTokens, options, enabled) {
|
|
383
|
+
const symbol = options.ascii ? "#" : "█";
|
|
384
|
+
const trackSymbol = options.ascii ? "." : "░";
|
|
385
|
+
const models = row.models.filter((model) => model.totalTokens > 0);
|
|
386
|
+
const total = row.totalTokens || 1;
|
|
387
|
+
const fillWidth = maximumTokens > 0
|
|
388
|
+
? Math.min(width, Math.max(1, Math.round((row.totalTokens / maximumTokens) * width)))
|
|
389
|
+
: 0;
|
|
390
|
+
const widths = models.length && fillWidth > 0
|
|
391
|
+
? models.map((model) => (model.totalTokens / total) * fillWidth)
|
|
392
|
+
: [];
|
|
393
|
+
const allocated = widths.map((value) => Math.floor(value));
|
|
394
|
+
let remainder = models.length
|
|
395
|
+
? fillWidth - allocated.reduce((sum, value) => sum + value, 0)
|
|
396
|
+
: 0;
|
|
397
|
+
const fractionalOrder = widths
|
|
398
|
+
.map((value, index) => ({ index, fraction: value - Math.floor(value) }))
|
|
399
|
+
.sort((left, right) => right.fraction - left.fraction);
|
|
400
|
+
for (let index = 0; index < remainder; index += 1) {
|
|
401
|
+
allocated[fractionalOrder[index % fractionalOrder.length]?.index ?? 0] += 1;
|
|
402
|
+
}
|
|
403
|
+
const filled = models
|
|
404
|
+
.map((model, index) => {
|
|
405
|
+
const label = modelLabel(model.model);
|
|
406
|
+
return colorize(symbol.repeat(allocated[index]), modelColor(label), enabled);
|
|
407
|
+
})
|
|
408
|
+
.join("");
|
|
409
|
+
const blank = colorize(
|
|
410
|
+
trackSymbol.repeat(Math.max(0, width - visibleLength(filled))),
|
|
411
|
+
DIM,
|
|
412
|
+
enabled,
|
|
413
|
+
);
|
|
414
|
+
return `${filled}${blank}`;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function panelLines(rows, allRows, totalTokens, panelWidth, options, enabled) {
|
|
418
|
+
const compactMode = panelWidth < 82;
|
|
419
|
+
const narrowMode = panelWidth < 60;
|
|
420
|
+
const shareWidth = narrowMode ? 6 : compactMode ? 7 : 8;
|
|
421
|
+
const totalWidth = narrowMode ? 8 : compactMode ? 9 : 10;
|
|
422
|
+
const minimumBarWidth = narrowMode ? 4 : 8;
|
|
423
|
+
const rightPadding = 2;
|
|
424
|
+
const preferredLabelWidth = compactMode ? 38 : 40;
|
|
425
|
+
const minimumLabelWidth = narrowMode ? 17 : 24;
|
|
426
|
+
const maximumLabelWidth = panelWidth - minimumBarWidth - shareWidth - totalWidth - 1 - rightPadding;
|
|
427
|
+
const labelWidth = Math.max(minimumLabelWidth, Math.min(preferredLabelWidth, maximumLabelWidth));
|
|
428
|
+
const barWidth = Math.max(
|
|
429
|
+
minimumBarWidth,
|
|
430
|
+
panelWidth - labelWidth - shareWidth - totalWidth - 1 - rightPadding,
|
|
431
|
+
);
|
|
432
|
+
const maxTokens = allRows[0]?.totalTokens ?? 0;
|
|
433
|
+
const lines = [];
|
|
434
|
+
const heading = colorize("TOKENS BY PROJECT", TEAL, enabled);
|
|
435
|
+
const barHeaderWidth = barWidth;
|
|
436
|
+
const maximumLabel = compactMode ? `${compact(maxTokens)} max` : `${compact(maxTokens)} (max)`;
|
|
437
|
+
const axisLabel = barHeaderWidth >= maximumLabel.length + 2 ? maximumLabel : compact(maxTokens);
|
|
438
|
+
const axisText = barHeaderWidth >= axisLabel.length + 2
|
|
439
|
+
? `0${" ".repeat(barHeaderWidth - axisLabel.length - 1)}${axisLabel}`
|
|
440
|
+
: truncateText(axisLabel, barHeaderWidth);
|
|
441
|
+
lines.push(
|
|
442
|
+
`${fit(heading, labelWidth)}${fit(axisText, barHeaderWidth)}${fit("TOKENS", totalWidth, "right")}${fit("SHARE", shareWidth, "right")}${" ".repeat(rightPadding)}`,
|
|
443
|
+
);
|
|
444
|
+
lines.push(colorize("─".repeat(panelWidth), DIM, enabled));
|
|
445
|
+
|
|
446
|
+
for (const [index, row] of rows.entries()) {
|
|
447
|
+
const share = totalTokens > 0 ? (row.totalTokens / totalTokens) * 100 : 0;
|
|
448
|
+
const selected = !options.static && index === (options.selectedIndex ?? 0);
|
|
449
|
+
const caretValue = selected ? (options.ascii ? ">" : "▶") : " ";
|
|
450
|
+
const prefix = `${caretValue} ${index + 1}. `;
|
|
451
|
+
const projectText = truncateText(displayProject(row), labelWidth - prefix.length);
|
|
452
|
+
const caret = selected ? colorize(caretValue, [1, ...TEAL], enabled) : caretValue;
|
|
453
|
+
const rank = colorize(`${index + 1}.`, TITLE_STYLE, enabled);
|
|
454
|
+
const title = `${caret} ${rank} ${colorize(projectText, TITLE_STYLE, enabled)}`;
|
|
455
|
+
const label = fit(title, labelWidth);
|
|
456
|
+
const metrics = `${fit(compact(row.totalTokens), totalWidth, "right")}${fit(percent(share), shareWidth, "right")}${" ".repeat(rightPadding)}`;
|
|
457
|
+
const bar = stackedBar(row, barWidth, maxTokens, options, enabled);
|
|
458
|
+
const rowLine = `${label}${bar} ${metrics}`;
|
|
459
|
+
const detail = `${plural(row.threads, "thread")} · ${percent(share)} of tokens`;
|
|
460
|
+
const detailText = truncateText(detail, labelWidth - 4);
|
|
461
|
+
const subtitle = colorize(detailText, SUBTITLE_STYLE, enabled);
|
|
462
|
+
const detailLine = `${fit(` ${subtitle}`, labelWidth)}${" ".repeat(barWidth + 1 + totalWidth + shareWidth + rightPadding)}`;
|
|
463
|
+
if (selected && enabled && options.highlight !== false) {
|
|
464
|
+
lines.push(`\u001b[48;5;236m${fit(rowLine, panelWidth)}${RESET}`);
|
|
465
|
+
lines.push(`\u001b[48;5;236m${fit(detailLine, panelWidth)}${RESET}`);
|
|
466
|
+
} else {
|
|
467
|
+
lines.push(rowLine);
|
|
468
|
+
lines.push(detailLine);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return lines.map((line) => fit(line, panelWidth));
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function sidebarLines(stats, panelWidth, enabled, options = {}, quota = null) {
|
|
475
|
+
const lines = [];
|
|
476
|
+
const compactSidebar = options.compactSidebar === true;
|
|
477
|
+
const inset = panelWidth >= 20 ? 2 : 1;
|
|
478
|
+
const contentWidth = Math.max(1, panelWidth - inset * 2);
|
|
479
|
+
const push = (line = "") => {
|
|
480
|
+
lines.push(`${" ".repeat(inset)}${fit(line, contentWidth)}${" ".repeat(inset)}`);
|
|
481
|
+
};
|
|
482
|
+
const divider = () => push(colorize("─".repeat(contentWidth), DIM, enabled));
|
|
483
|
+
const heading = (value) => colorize(value, TEAL, enabled);
|
|
484
|
+
push(heading("MODEL MIX"));
|
|
485
|
+
if (!compactSidebar) push();
|
|
486
|
+
for (const item of modelLegendItems(stats.models, stats.totalTokens)) {
|
|
487
|
+
const swatch = colorize("■", modelColor(item.model), enabled);
|
|
488
|
+
push(`${swatch} ${fit(item.model, Math.max(1, contentWidth - 10))}${fit(percent(item.share), 8, "right")}`);
|
|
489
|
+
if (item.model === "Auto Review" && stats.autoReview.present) {
|
|
490
|
+
const turnLabel = stats.autoReview.turns === 1 ? "turn" : "turns";
|
|
491
|
+
push(` ${compact(stats.autoReview.turns)} ${turnLabel} · ${percent(stats.autoReview.turnShare)}`);
|
|
492
|
+
push(` ${compact(stats.autoReview.totalTokens)} · ${percent(stats.autoReview.cachedInputShare)} cached`);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
if (!compactSidebar) push();
|
|
496
|
+
divider();
|
|
497
|
+
push(heading("USAGE TYPE · TOKENS"));
|
|
498
|
+
if (!compactSidebar) push();
|
|
499
|
+
const usageItems = visibleUsageTypeItems(stats.usageTypes);
|
|
500
|
+
for (const item of usageItems) {
|
|
501
|
+
const swatch = "■";
|
|
502
|
+
const share = stats.totalTokens > 0 ? (item.totalTokens / stats.totalTokens) * 100 : 0;
|
|
503
|
+
push(`${swatch} ${fit(item.label, Math.max(1, contentWidth - 10))}${fit(percent(share), 8, "right")}`);
|
|
504
|
+
}
|
|
505
|
+
if (!compactSidebar) push();
|
|
506
|
+
divider();
|
|
507
|
+
push(heading("CACHE · INPUT"));
|
|
508
|
+
if (!compactSidebar) push();
|
|
509
|
+
const cacheRows = [
|
|
510
|
+
{ label: "Cached", totalTokens: stats.cachedInputTokens },
|
|
511
|
+
{ label: "Uncached", totalTokens: stats.uncachedInputTokens },
|
|
512
|
+
];
|
|
513
|
+
for (const item of cacheRows) {
|
|
514
|
+
const swatch = "■";
|
|
515
|
+
const share = stats.inputTokens > 0 ? (item.totalTokens / stats.inputTokens) * 100 : 0;
|
|
516
|
+
push(`${swatch} ${fit(item.label, Math.max(1, contentWidth - 10))}${fit(percent(share), 8, "right")}`);
|
|
517
|
+
}
|
|
518
|
+
if (quota?.available) {
|
|
519
|
+
if (!compactSidebar) push();
|
|
520
|
+
divider();
|
|
521
|
+
push(heading("RESET CYCLE"));
|
|
522
|
+
if (!compactSidebar) push();
|
|
523
|
+
push("Used" + fit(percent(quota.usedPercent), 8, "right"));
|
|
524
|
+
push("Remaining" + fit(percent(quota.remainingPercent), 8, "right"));
|
|
525
|
+
if (quota.estimatedDisplayedBurnPercent !== null) {
|
|
526
|
+
const burn = "~" + percent(quota.estimatedDisplayedBurnPercent).replace("%", "") + " pts";
|
|
527
|
+
push("View burn" + fit(burn, 10, "right"));
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
if (!compactSidebar) push();
|
|
531
|
+
return lines.map((line) => fit(line, panelWidth));
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function panel(leftLines, rightLines, leftWidth, rightWidth, enabled, ascii) {
|
|
535
|
+
const glyphs = ascii
|
|
536
|
+
? { tl: "+", tr: "+", bl: "+", br: "+", h: "-", v: "|", tm: "+", bm: "+" }
|
|
537
|
+
: { tl: "┌", tr: "┐", bl: "└", br: "┘", h: "─", v: "│", tm: "┬", bm: "┴" };
|
|
538
|
+
const rows = Math.max(leftLines.length, rightLines?.length ?? 0);
|
|
539
|
+
const top = `${glyphs.tl}${glyphs.h.repeat(leftWidth)}${rightLines ? glyphs.tm : glyphs.tr}${rightLines ? glyphs.h.repeat(rightWidth) + glyphs.tr : ""}`;
|
|
540
|
+
const body = [];
|
|
541
|
+
for (let index = 0; index < rows; index += 1) {
|
|
542
|
+
const left = fit(leftLines[index] ?? "", leftWidth);
|
|
543
|
+
if (rightLines) {
|
|
544
|
+
const right = fit(rightLines[index] ?? "", rightWidth);
|
|
545
|
+
body.push(`${glyphs.v}${left}${glyphs.v}${right}${glyphs.v}`);
|
|
546
|
+
} else {
|
|
547
|
+
body.push(`${glyphs.v}${left}${glyphs.v}`);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
const bottom = `${glyphs.bl}${glyphs.h.repeat(leftWidth)}${rightLines ? glyphs.bm : glyphs.br}${rightLines ? glyphs.h.repeat(rightWidth) + glyphs.br : ""}`;
|
|
551
|
+
return [top, ...body, bottom];
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function headerLines(stats, bounds, frameWidth, options, enabled) {
|
|
555
|
+
const left = colorize("TOKEN LEDGER", TITLE_STYLE, enabled);
|
|
556
|
+
const date = colorize(dateLabel(bounds, options.range), TEXT_STYLE, enabled);
|
|
557
|
+
const mode = colorize(options.range === "week" ? "7 DAYS" : "DAY", [1, ...TEAL], enabled);
|
|
558
|
+
const metric = (value, label) =>
|
|
559
|
+
`${colorize(String(value), TITLE_STYLE, enabled)} ${colorize(label, DIM, enabled)}`;
|
|
560
|
+
const separator = colorize("·", DIM, enabled);
|
|
561
|
+
const join = ` ${separator} `;
|
|
562
|
+
const alignHeader = (line) => fit(` ${line}`, frameWidth);
|
|
563
|
+
const fullLine = [
|
|
564
|
+
left,
|
|
565
|
+
date,
|
|
566
|
+
mode,
|
|
567
|
+
metric(compact(stats.totalTokens), "TOKENS"),
|
|
568
|
+
metric(stats.calls.toLocaleString("en-US"), "CALLS"),
|
|
569
|
+
metric(stats.threads.toLocaleString("en-US"), "THREADS"),
|
|
570
|
+
metric(stats.projectCount.toLocaleString("en-US"), "PROJECTS"),
|
|
571
|
+
].join(join);
|
|
572
|
+
if (visibleLength(fullLine) < frameWidth) {
|
|
573
|
+
return [alignHeader(fullLine)];
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const compactDate = dateLabel(bounds, options.range)
|
|
577
|
+
.replace(/ 20\d{2}$/, "")
|
|
578
|
+
.replace(" – ", "–");
|
|
579
|
+
const compactMode = options.range === "week" ? "7D" : "DAY";
|
|
580
|
+
const compactLine = [
|
|
581
|
+
left,
|
|
582
|
+
colorize(compactDate, TEXT_STYLE, enabled),
|
|
583
|
+
colorize(compactMode, [1, ...TEAL], enabled),
|
|
584
|
+
metric(`${compact(stats.totalTokens)}`, "T"),
|
|
585
|
+
metric(stats.calls.toLocaleString("en-US"), "C"),
|
|
586
|
+
metric(stats.threads.toLocaleString("en-US"), "TH"),
|
|
587
|
+
metric(stats.projectCount.toLocaleString("en-US"), "P"),
|
|
588
|
+
].join(join);
|
|
589
|
+
if (visibleLength(compactLine) < frameWidth) {
|
|
590
|
+
return [alignHeader(compactLine)];
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const minimalTitle = colorize(frameWidth >= 45 ? "LEDGER" : "L", TITLE_STYLE, enabled);
|
|
594
|
+
const minimalLine = [
|
|
595
|
+
minimalTitle,
|
|
596
|
+
colorize(compactDate.replaceAll(" ", ""), TEXT_STYLE, enabled),
|
|
597
|
+
colorize(compactMode, [1, ...TEAL], enabled),
|
|
598
|
+
compact(stats.totalTokens),
|
|
599
|
+
compact(stats.calls),
|
|
600
|
+
compact(stats.threads),
|
|
601
|
+
compact(stats.projectCount),
|
|
602
|
+
].join(" ");
|
|
603
|
+
return [alignHeader(minimalLine)];
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
export function renderTerminal({ options, snapshot, bounds, events, rows, allRows }) {
|
|
607
|
+
const enabled = colorsEnabled(options);
|
|
608
|
+
const stats = summary(events);
|
|
609
|
+
const quota = quotaCycleSummary(snapshot, events);
|
|
610
|
+
stats.projectCount = allRows.length;
|
|
611
|
+
const columns = options.width ?? (Number(process.stdout.columns) || 120);
|
|
612
|
+
const frameWidth = Math.max(38, Math.min(158, columns - 2));
|
|
613
|
+
const sideBySide = options.forceSideBySide ?? frameWidth >= 100;
|
|
614
|
+
const sideWidth = sideBySide ? (stats.autoReview.present ? 28 : 26) : 0;
|
|
615
|
+
const leftWidth = sideBySide ? frameWidth - sideWidth - 1 : frameWidth;
|
|
616
|
+
const left = panelLines(rows, allRows, stats.totalTokens, leftWidth, options, enabled);
|
|
617
|
+
const right = sideBySide ? sidebarLines(stats, sideWidth, enabled, options, quota) : null;
|
|
618
|
+
const lines = [
|
|
619
|
+
...headerLines(stats, bounds, frameWidth, options, enabled),
|
|
620
|
+
...panel(left, right, leftWidth, sideWidth, enabled, options.ascii),
|
|
621
|
+
];
|
|
622
|
+
if (!sideBySide) {
|
|
623
|
+
lines.push("");
|
|
624
|
+
lines.push(...sidebarLines(stats, frameWidth, enabled, options, quota));
|
|
625
|
+
}
|
|
626
|
+
if (!options.static && !options.hideHelp) {
|
|
627
|
+
lines.push("");
|
|
628
|
+
lines.push(
|
|
629
|
+
colorize(
|
|
630
|
+
options.ascii
|
|
631
|
+
? "[j/k] select [q/esc] quit"
|
|
632
|
+
: "[↑↓ or j/k] select [q/esc] quit",
|
|
633
|
+
DIM,
|
|
634
|
+
enabled,
|
|
635
|
+
),
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
return lines.join("\n");
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
export const SCREEN_BASE = "\u001b[38;5;255m\u001b[48;2;16;16;18m";
|
|
642
|
+
const PANEL_BASE = "\u001b[38;5;255m\u001b[48;2;5;5;6m";
|
|
643
|
+
|
|
644
|
+
function paintFullscreenLine(line, width, background, enabled) {
|
|
645
|
+
const fitted = fit(line, width);
|
|
646
|
+
if (!enabled) return fitted;
|
|
647
|
+
const restored = fitted.replaceAll(RESET, `${RESET}${background}`);
|
|
648
|
+
return `${background}${restored}${RESET}`;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
export function renderFullscreen({ options, snapshot, bounds, events, rows, allRows, width, height }) {
|
|
652
|
+
const enabled = options.forceColor ?? colorsEnabled(options);
|
|
653
|
+
const columns = Math.max(40, Number(width) || Number(process.stdout.columns) || 120);
|
|
654
|
+
const screenHeight = Math.max(1, Number(height) || Number(process.stdout.rows) || 32);
|
|
655
|
+
const frameWidth = Math.max(38, Math.min(158, columns - 4));
|
|
656
|
+
const staticOutput = renderTerminal({
|
|
657
|
+
options: {
|
|
658
|
+
...options,
|
|
659
|
+
forceColor: enabled,
|
|
660
|
+
forceSideBySide: frameWidth >= 84,
|
|
661
|
+
highlight: false,
|
|
662
|
+
compactSidebar: true,
|
|
663
|
+
hideHelp: true,
|
|
664
|
+
width: frameWidth + 2,
|
|
665
|
+
},
|
|
666
|
+
snapshot,
|
|
667
|
+
bounds,
|
|
668
|
+
events,
|
|
669
|
+
rows,
|
|
670
|
+
allRows,
|
|
671
|
+
});
|
|
672
|
+
const staticLines = staticOutput.split("\n");
|
|
673
|
+
if (staticLines.at(-1) === "") staticLines.pop();
|
|
674
|
+
const summaryLine = staticLines.shift() ?? "";
|
|
675
|
+
const help = colorize("↑/↓ move • j/k move • q/esc quit", DIM, enabled);
|
|
676
|
+
const content = [
|
|
677
|
+
{ line: summaryLine, background: SCREEN_BASE },
|
|
678
|
+
...staticLines.map((line) => ({ line, background: PANEL_BASE })),
|
|
679
|
+
{ line: "", background: SCREEN_BASE },
|
|
680
|
+
{ line: help, background: SCREEN_BASE },
|
|
681
|
+
];
|
|
682
|
+
const topPadding = Math.max(0, Math.floor((screenHeight - content.length) / 2));
|
|
683
|
+
const screenLines = [
|
|
684
|
+
...Array.from({ length: topPadding }, () => ({ line: "", background: SCREEN_BASE })),
|
|
685
|
+
...content,
|
|
686
|
+
].slice(0, screenHeight);
|
|
687
|
+
while (screenLines.length < screenHeight) {
|
|
688
|
+
screenLines.push({ line: "", background: SCREEN_BASE });
|
|
689
|
+
}
|
|
690
|
+
return screenLines
|
|
691
|
+
.map(({ line, background }) => paintFullscreenLine(line, columns, background, enabled))
|
|
692
|
+
.join("\n");
|
|
693
|
+
}
|