tledger 0.1.4 → 0.2.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.
@@ -1,23 +1,25 @@
1
1
  import {
2
- modelColorKey,
3
- modelDisplayName,
4
- } from "../lib/token-ledger-models.mjs";
2
+ normalizeQuotaTimeline,
3
+ weeklyQuotaObservations,
4
+ } from "./token-ledger-trend.mjs";
5
5
 
6
6
  const RESET = "\u001b[0m";
7
- const DIM = [38, 5, 245];
8
- const TEAL = [38, 5, 80];
7
+ const PRIMARY_STYLE = [38, 2, 255, 255, 255];
8
+ const SECONDARY_STYLE = [38, 2, 155, 155, 155];
9
+ const ACCENT_STYLE = [38, 2, 51, 156, 255];
10
+ const BORDER_STYLE = [38, 2, 88, 88, 88];
11
+ const TRACK_STYLE = [38, 2, 59, 59, 59];
9
12
  export const MODEL_COLORS = {
10
- sol: [38, 5, 67],
11
- luna: [38, 5, 80],
12
- terra: [38, 5, 179],
13
- gpt: [38, 5, 176],
14
- daybreakBlue: [38, 5, 69],
15
- autoReview: [38, 5, 153],
16
- other: [38, 5, 60],
13
+ sol: [38, 2, 120, 185, 242],
14
+ luna: ACCENT_STYLE,
15
+ terra: [38, 2, 214, 168, 95],
16
+ gpt: [38, 2, 174, 139, 219],
17
+ other: [38, 2, 116, 125, 144],
17
18
  };
18
- const TEXT_STYLE = [38, 5, 255];
19
- const TITLE_STYLE = [1, 38, 5, 255];
20
- const SUBTITLE_STYLE = [38, 5, 245];
19
+ const TEXT_STYLE = PRIMARY_STYLE;
20
+ const TITLE_STYLE = [1, ...PRIMARY_STYLE];
21
+ const SUBTITLE_STYLE = SECONDARY_STYLE;
22
+ const SELECTED_BACKGROUND = "\u001b[48;2;42;42;42m";
21
23
 
22
24
  function colorsEnabled(options) {
23
25
  return options.forceColor ?? (!options.plain && !process.env.NO_COLOR && Boolean(process.stdout.isTTY));
@@ -29,16 +31,7 @@ function colorize(value, code, enabled) {
29
31
  }
30
32
 
31
33
  function stripAnsi(value) {
32
- return String(value)
33
- .replace(/\u001b\][^\u0007]*(?:\u0007|\u001b\\)/g, "")
34
- .replace(/\u001b\[[0-?]*[ -/]*[@-~]/g, "");
35
- }
36
-
37
- function sanitizeText(value) {
38
- return stripAnsi(value)
39
- .replace(/[\u0000-\u001f\u007f-\u009f]+/g, " ")
40
- .replace(/\s+/g, " ")
41
- .trim();
34
+ return String(value).replace(/\u001b\[[0-9;]*m/g, "");
42
35
  }
43
36
 
44
37
  function visibleLength(value) {
@@ -100,16 +93,23 @@ function plural(value, singular, pluralForm = `${singular}s`) {
100
93
  }
101
94
 
102
95
  function modelLabel(value) {
103
- const model = sanitizeText(value) || "Unknown model";
104
- return modelDisplayName(model);
96
+ const model = String(value || "Unknown model");
97
+ const lower = model.toLowerCase();
98
+ if (lower.includes("sol")) return "Sol";
99
+ if (lower.includes("luna")) return "Luna";
100
+ if (lower.includes("terra")) return "Terra";
101
+ if (lower.includes("gpt-5.5") || lower.includes("gpt-5.4")) return "GPT";
102
+ return "Other";
105
103
  }
106
104
 
107
105
  function modelColor(model) {
108
- return MODEL_COLORS[modelColorKey(model)] ?? MODEL_COLORS.other;
106
+ const key = String(model || "").toLowerCase();
107
+ return MODEL_COLORS[key] ?? MODEL_COLORS.other;
109
108
  }
110
109
 
111
110
  function usageTypeLabel(value) {
112
- const words = (sanitizeText(value) || "unknown")
111
+ const words = String(value || "unknown")
112
+ .trim()
113
113
  .replace(/[_-]+/g, " ")
114
114
  .split(/\s+/)
115
115
  .filter(Boolean);
@@ -124,11 +124,11 @@ function usageTypeLabel(value) {
124
124
  }
125
125
 
126
126
  function displayProject(row) {
127
- return sanitizeText(row.displayProject || row.project) || "Unlabelled activity";
127
+ return row.displayProject || row.project || "Unlabelled activity";
128
128
  }
129
129
 
130
130
  function dateLabel(bounds, range = "day") {
131
- if (range !== "day" && bounds.rangeDays !== 1 && bounds.startDateString && bounds.endDateString) {
131
+ if (range === "week" && bounds.startDateString && bounds.endDateString) {
132
132
  const startParts = bounds.startDateString.split("-").map(Number);
133
133
  const endParts = bounds.endDateString.split("-").map(Number);
134
134
  const monthNames = [
@@ -137,12 +137,7 @@ function dateLabel(bounds, range = "day") {
137
137
  ];
138
138
  const start = `${monthNames[startParts[1] - 1]} ${String(startParts[2]).padStart(2, "0")}`;
139
139
  const end = `${monthNames[endParts[1] - 1]} ${String(endParts[2]).padStart(2, "0")}`;
140
- if (bounds.startDateString === bounds.endDateString) {
141
- return `${end} ${endParts[0]}`;
142
- }
143
- return startParts[0] === endParts[0]
144
- ? `${start} – ${end} ${endParts[0]}`
145
- : `${start} ${startParts[0]} – ${end} ${endParts[0]}`;
140
+ return `${start} ${end} ${endParts[0]}`;
146
141
  }
147
142
  const parts = new Intl.DateTimeFormat("en-US", {
148
143
  timeZone: bounds.timeZone,
@@ -159,13 +154,6 @@ function dateLabel(bounds, range = "day") {
159
154
  return `${values.weekday} ${values.day} ${values.month} ${values.year}`.toUpperCase();
160
155
  }
161
156
 
162
- function rangeModeLabel(bounds, range = "day", compact = false) {
163
- if (range === "all") return "ALL";
164
- if (range === "day") return "DAY";
165
- const days = bounds.rangeDays ?? 1;
166
- return compact ? `${days}D` : `${days} ${days === 1 ? "DAY" : "DAYS"}`;
167
- }
168
-
169
157
  function modelTotals(events) {
170
158
  const totals = new Map();
171
159
  for (const event of events) {
@@ -180,9 +168,7 @@ function modelTotals(events) {
180
168
  function usageTypeTotals(events) {
181
169
  const totals = new Map();
182
170
  for (const event of events) {
183
- const key = modelLabel(event.model) === "Auto Review"
184
- ? "auto-review"
185
- : String(event.useType || "unknown").trim().toLowerCase() || "unknown";
171
+ const key = String(event.useType || "unknown").trim().toLowerCase() || "unknown";
186
172
  totals.set(key, (totals.get(key) ?? 0) + (Number(event.totalTokens) || 0));
187
173
  }
188
174
  return [...totals.entries()]
@@ -194,22 +180,17 @@ function usageTypeTotals(events) {
194
180
  .sort((left, right) => right.totalTokens - left.totalTokens);
195
181
  }
196
182
 
197
- function latestWeeklyQuotaObservation(observations) {
198
- const weekly = (observations ?? []).filter(
199
- (item) => Number(item.windowMinutes) === 10_080,
200
- );
201
- const accountWide = weekly.filter((item) => item.scope !== "named");
202
- const candidates = accountWide.length ? accountWide : weekly;
203
- return [...candidates]
204
- .sort(
205
- (left, right) =>
206
- new Date(left.timestamp).getTime() - new Date(right.timestamp).getTime(),
207
- )
208
- .at(-1) ?? null;
183
+ function latestWeeklyQuotaObservation(snapshot) {
184
+ // The epoch-keyed timeline drops stale readings from superseded windows,
185
+ // so the last entry is the newest reading of the currently-live window.
186
+ const normalized = normalizeQuotaTimeline(weeklyQuotaObservations(snapshot));
187
+ const latest = normalized.at(-1);
188
+ if (!latest) return null;
189
+ return { ...latest, usedPercent: latest.normalizedUsedPercent };
209
190
  }
210
191
 
211
192
  export function quotaCycleSummary(snapshot = {}, displayedEvents = []) {
212
- const observation = latestWeeklyQuotaObservation(snapshot.quotaObservations);
193
+ const observation = latestWeeklyQuotaObservation(snapshot);
213
194
  if (!observation) {
214
195
  return {
215
196
  available: false,
@@ -297,32 +278,6 @@ function summary(events) {
297
278
  const cached = Math.max(0, Number(event.cachedInputTokens) || 0);
298
279
  return sum + Math.min(input, cached);
299
280
  }, 0);
300
- const turnCount = (rows) => {
301
- const keys = new Set();
302
- for (const [index, event] of rows.entries()) {
303
- const turnId = String(event.turnId || "").trim();
304
- keys.add(turnId ? `turn:${turnId}` : `event:${event.id || index}`);
305
- }
306
- return keys.size;
307
- };
308
- const totalTurns = turnCount(events);
309
- const autoReviewEvents = events.filter(
310
- (event) => modelLabel(event.model) === "Auto Review",
311
- );
312
- const autoReviewTokens = autoReviewEvents.reduce(
313
- (sum, event) => sum + (Number(event.totalTokens) || 0),
314
- 0,
315
- );
316
- const autoReviewInputTokens = autoReviewEvents.reduce(
317
- (sum, event) => sum + Math.max(0, Number(event.inputTokens) || 0),
318
- 0,
319
- );
320
- const autoReviewCachedInputTokens = autoReviewEvents.reduce((sum, event) => {
321
- const input = Math.max(0, Number(event.inputTokens) || 0);
322
- const cached = Math.max(0, Number(event.cachedInputTokens) || 0);
323
- return sum + Math.min(input, cached);
324
- }, 0);
325
- const autoReviewTurns = turnCount(autoReviewEvents);
326
281
  return {
327
282
  totalTokens,
328
283
  calls,
@@ -333,48 +288,25 @@ function summary(events) {
333
288
  uncachedInputTokens: Math.max(0, inputTokens - cachedInputTokens),
334
289
  models: modelTotals(events),
335
290
  usageTypes: usageTypeTotals(events),
336
- autoReview: {
337
- present: autoReviewEvents.length > 0,
338
- totalTokens: autoReviewTokens,
339
- turns: autoReviewTurns,
340
- turnShare: totalTurns > 0 ? (autoReviewTurns / totalTurns) * 100 : 0,
341
- cachedInputShare: autoReviewInputTokens > 0
342
- ? (autoReviewCachedInputTokens / autoReviewInputTokens) * 100
343
- : 0,
344
- },
345
291
  };
346
292
  }
347
293
 
348
294
  function modelLegendItems(models, totalTokens) {
349
- return models
350
- .filter((model) => model.totalTokens > 0)
295
+ const known = new Map();
296
+ for (const model of models) {
297
+ const key = ["Sol", "Luna", "Terra", "GPT"].includes(model.model)
298
+ ? model.model
299
+ : "Other";
300
+ known.set(key, (known.get(key) ?? 0) + model.totalTokens);
301
+ }
302
+ return ["Luna", "Sol", "Terra", "GPT", "Other"]
351
303
  .map((model) => ({
352
- ...model,
353
- share: totalTokens > 0 ? (model.totalTokens / totalTokens) * 100 : 0,
304
+ model,
305
+ totalTokens: known.get(model) ?? 0,
306
+ share: totalTokens > 0 ? ((known.get(model) ?? 0) / totalTokens) * 100 : 0,
354
307
  }));
355
308
  }
356
309
 
357
- function visibleUsageTypeItems(items, limit = 5) {
358
- if (items.length <= limit) return items;
359
- const visible = items.slice(0, limit - 1);
360
- const autoReview = items.find((item) => item.key === "auto-review");
361
- if (autoReview && !visible.includes(autoReview)) {
362
- visible[visible.length - 1] = autoReview;
363
- visible.sort((left, right) => right.totalTokens - left.totalTokens);
364
- }
365
- const visibleKeys = new Set(visible.map((item) => item.key));
366
- return [
367
- ...visible,
368
- {
369
- key: "other",
370
- label: "Other",
371
- totalTokens: items
372
- .filter((item) => !visibleKeys.has(item.key))
373
- .reduce((sum, item) => sum + item.totalTokens, 0),
374
- },
375
- ];
376
- }
377
-
378
310
  function stackedBar(row, width, maximumTokens, options, enabled) {
379
311
  const symbol = options.ascii ? "#" : "█";
380
312
  const trackSymbol = options.ascii ? "." : "░";
@@ -404,7 +336,7 @@ function stackedBar(row, width, maximumTokens, options, enabled) {
404
336
  .join("");
405
337
  const blank = colorize(
406
338
  trackSymbol.repeat(Math.max(0, width - visibleLength(filled))),
407
- DIM,
339
+ TRACK_STYLE,
408
340
  enabled,
409
341
  );
410
342
  return `${filled}${blank}`;
@@ -426,9 +358,13 @@ function panelLines(rows, allRows, totalTokens, panelWidth, options, enabled) {
426
358
  panelWidth - labelWidth - shareWidth - totalWidth - 1 - rightPadding,
427
359
  );
428
360
  const maxTokens = allRows[0]?.totalTokens ?? 0;
429
- const rowOffset = Number.isInteger(options.rowOffset) ? options.rowOffset : 0;
361
+ const totalCredits = allRows.reduce((sum, item) => sum + item.rateCardCredits, 0) || 1;
362
+ const selectedIndex = Math.min(
363
+ Math.max(0, Math.trunc(Number(options.selectedIndex) || 0)),
364
+ Math.max(0, rows.length - 1),
365
+ );
430
366
  const lines = [];
431
- const heading = colorize("TOKENS BY PROJECT", TEAL, enabled);
367
+ const heading = colorize("TOKENS BY PROJECT", ACCENT_STYLE, enabled);
432
368
  const barHeaderWidth = barWidth;
433
369
  const maximumLabel = compactMode ? `${compact(maxTokens)} max` : `${compact(maxTokens)} (max)`;
434
370
  const axisLabel = barHeaderWidth >= maximumLabel.length + 2 ? maximumLabel : compact(maxTokens);
@@ -438,29 +374,33 @@ function panelLines(rows, allRows, totalTokens, panelWidth, options, enabled) {
438
374
  lines.push(
439
375
  `${fit(heading, labelWidth)}${fit(axisText, barHeaderWidth)}${fit("TOKENS", totalWidth, "right")}${fit("SHARE", shareWidth, "right")}${" ".repeat(rightPadding)}`,
440
376
  );
441
- lines.push(colorize("─".repeat(panelWidth), DIM, enabled));
377
+ lines.push(colorize("─".repeat(panelWidth), BORDER_STYLE, enabled));
442
378
 
443
379
  for (const [index, row] of rows.entries()) {
444
- const rankValue = rowOffset + index + 1;
445
380
  const share = totalTokens > 0 ? (row.totalTokens / totalTokens) * 100 : 0;
446
- const selected = !options.static && index === (options.selectedIndex ?? 0);
381
+ const selected = index === selectedIndex;
447
382
  const caretValue = selected ? (options.ascii ? ">" : "▶") : " ";
448
- const prefix = `${caretValue} ${rankValue}. `;
383
+ const prefix = `${caretValue} ${index + 1}. `;
449
384
  const projectText = truncateText(displayProject(row), labelWidth - prefix.length);
450
- const caret = selected ? colorize(caretValue, [1, ...TEAL], enabled) : caretValue;
451
- const rank = colorize(`${rankValue}.`, TITLE_STYLE, enabled);
385
+ const caret = selected ? colorize(caretValue, [1, ...ACCENT_STYLE], enabled) : caretValue;
386
+ const rank = colorize(`${index + 1}.`, TITLE_STYLE, enabled);
452
387
  const title = `${caret} ${rank} ${colorize(projectText, TITLE_STYLE, enabled)}`;
453
388
  const label = fit(title, labelWidth);
454
389
  const metrics = `${fit(compact(row.totalTokens), totalWidth, "right")}${fit(percent(share), shareWidth, "right")}${" ".repeat(rightPadding)}`;
455
390
  const bar = stackedBar(row, barWidth, maxTokens, options, enabled);
456
391
  const rowLine = `${label}${bar} ${metrics}`;
457
- const detail = `${plural(row.threads, "thread")} · ${percent(share)} of tokens`;
392
+ const creditShare = row.rateCardCredits > 0 && row.rateCardCredits <= Number.MAX_SAFE_INTEGER
393
+ ? row.rateCardCredits
394
+ : 0;
395
+ const detail = `${plural(row.threads, "thread")} · ${percent(
396
+ creditShare > 0 ? (creditShare / totalCredits) * 100 : 0,
397
+ )}${labelWidth >= 35 ? " credits" : ""}`;
458
398
  const detailText = truncateText(detail, labelWidth - 4);
459
399
  const subtitle = colorize(detailText, SUBTITLE_STYLE, enabled);
460
400
  const detailLine = `${fit(` ${subtitle}`, labelWidth)}${" ".repeat(barWidth + 1 + totalWidth + shareWidth + rightPadding)}`;
461
401
  if (selected && enabled && options.highlight !== false) {
462
- lines.push(`\u001b[48;5;236m${fit(rowLine, panelWidth)}${RESET}`);
463
- lines.push(`\u001b[48;5;236m${fit(detailLine, panelWidth)}${RESET}`);
402
+ lines.push(`${SELECTED_BACKGROUND}${fit(rowLine, panelWidth)}${RESET}`);
403
+ lines.push(`${SELECTED_BACKGROUND}${fit(detailLine, panelWidth)}${RESET}`);
464
404
  } else {
465
405
  lines.push(rowLine);
466
406
  lines.push(detailLine);
@@ -477,41 +417,36 @@ function sidebarLines(stats, panelWidth, enabled, options = {}, quota = null) {
477
417
  const push = (line = "") => {
478
418
  lines.push(`${" ".repeat(inset)}${fit(line, contentWidth)}${" ".repeat(inset)}`);
479
419
  };
480
- const divider = () => push(colorize("─".repeat(contentWidth), DIM, enabled));
481
- const heading = (value) => colorize(value, TEAL, enabled);
420
+ const divider = () => push(colorize("─".repeat(contentWidth), BORDER_STYLE, enabled));
421
+ const heading = (value) => colorize(value, ACCENT_STYLE, enabled);
482
422
  push(heading("MODEL MIX"));
483
423
  if (!compactSidebar) push();
484
- const modelShareWidth = 6;
485
- const modelNameWidth = Math.max(1, contentWidth - modelShareWidth - 2);
486
- const modelItems = modelLegendItems(stats.models, stats.totalTokens);
487
- const modelLimit = Number.isInteger(options.modelLimit)
488
- ? Math.max(0, options.modelLimit)
489
- : modelItems.length;
490
- const visibleModelItems = modelItems.slice(0, modelLimit);
491
- for (const item of visibleModelItems) {
424
+ for (const item of modelLegendItems(stats.models, stats.totalTokens)) {
492
425
  const swatch = colorize("■", modelColor(item.model), enabled);
493
- push(`${swatch} ${fit(item.model, modelNameWidth)}${fit(percent(item.share), modelShareWidth, "right")}`);
494
- if (item.model === "Auto Review" && stats.autoReview.present) {
495
- const turnLabel = stats.autoReview.turns === 1 ? "turn" : "turns";
496
- push(` ${compact(stats.autoReview.turns)} ${turnLabel} · ${percent(stats.autoReview.turnShare)}`);
497
- push(` ${compact(stats.autoReview.totalTokens)} · ${percent(stats.autoReview.cachedInputShare)} cached`);
498
- }
499
- }
500
- const hiddenModelCount = modelItems.length - visibleModelItems.length;
501
- if (hiddenModelCount > 0) {
502
- push(colorize(`… ${plural(hiddenModelCount, "more model")}`, DIM, enabled));
426
+ push(`${swatch} ${fit(item.model, Math.max(1, contentWidth - 10))}${fit(percent(item.share), 8, "right")}`);
503
427
  }
504
- if (!compactSidebar) push();
428
+ push();
505
429
  divider();
506
430
  push(heading("USAGE TYPE · TOKENS"));
507
431
  if (!compactSidebar) push();
508
- const usageItems = visibleUsageTypeItems(stats.usageTypes);
432
+ const usageItems = stats.usageTypes.length > 5
433
+ ? [
434
+ ...stats.usageTypes.slice(0, 4),
435
+ {
436
+ key: "other",
437
+ label: "Other",
438
+ totalTokens: stats.usageTypes
439
+ .slice(4)
440
+ .reduce((sum, item) => sum + item.totalTokens, 0),
441
+ },
442
+ ]
443
+ : stats.usageTypes;
509
444
  for (const item of usageItems) {
510
445
  const swatch = "■";
511
446
  const share = stats.totalTokens > 0 ? (item.totalTokens / stats.totalTokens) * 100 : 0;
512
447
  push(`${swatch} ${fit(item.label, Math.max(1, contentWidth - 10))}${fit(percent(share), 8, "right")}`);
513
448
  }
514
- if (!compactSidebar) push();
449
+ push();
515
450
  divider();
516
451
  push(heading("CACHE · INPUT"));
517
452
  if (!compactSidebar) push();
@@ -525,7 +460,7 @@ function sidebarLines(stats, panelWidth, enabled, options = {}, quota = null) {
525
460
  push(`${swatch} ${fit(item.label, Math.max(1, contentWidth - 10))}${fit(percent(share), 8, "right")}`);
526
461
  }
527
462
  if (quota?.available) {
528
- if (!compactSidebar) push();
463
+ push();
529
464
  divider();
530
465
  push(heading("RESET CYCLE"));
531
466
  if (!compactSidebar) push();
@@ -545,28 +480,29 @@ function panel(leftLines, rightLines, leftWidth, rightWidth, enabled, ascii) {
545
480
  ? { tl: "+", tr: "+", bl: "+", br: "+", h: "-", v: "|", tm: "+", bm: "+" }
546
481
  : { tl: "┌", tr: "┐", bl: "└", br: "┘", h: "─", v: "│", tm: "┬", bm: "┴" };
547
482
  const rows = Math.max(leftLines.length, rightLines?.length ?? 0);
548
- const top = `${glyphs.tl}${glyphs.h.repeat(leftWidth)}${rightLines ? glyphs.tm : glyphs.tr}${rightLines ? glyphs.h.repeat(rightWidth) + glyphs.tr : ""}`;
483
+ const border = (value) => colorize(value, BORDER_STYLE, enabled);
484
+ const top = border(`${glyphs.tl}${glyphs.h.repeat(leftWidth)}${rightLines ? glyphs.tm : glyphs.tr}${rightLines ? glyphs.h.repeat(rightWidth) + glyphs.tr : ""}`);
549
485
  const body = [];
550
486
  for (let index = 0; index < rows; index += 1) {
551
487
  const left = fit(leftLines[index] ?? "", leftWidth);
552
488
  if (rightLines) {
553
489
  const right = fit(rightLines[index] ?? "", rightWidth);
554
- body.push(`${glyphs.v}${left}${glyphs.v}${right}${glyphs.v}`);
490
+ body.push(`${border(glyphs.v)}${left}${border(glyphs.v)}${right}${border(glyphs.v)}`);
555
491
  } else {
556
- body.push(`${glyphs.v}${left}${glyphs.v}`);
492
+ body.push(`${border(glyphs.v)}${left}${border(glyphs.v)}`);
557
493
  }
558
494
  }
559
- const bottom = `${glyphs.bl}${glyphs.h.repeat(leftWidth)}${rightLines ? glyphs.bm : glyphs.br}${rightLines ? glyphs.h.repeat(rightWidth) + glyphs.br : ""}`;
495
+ const bottom = border(`${glyphs.bl}${glyphs.h.repeat(leftWidth)}${rightLines ? glyphs.bm : glyphs.br}${rightLines ? glyphs.h.repeat(rightWidth) + glyphs.br : ""}`);
560
496
  return [top, ...body, bottom];
561
497
  }
562
498
 
563
499
  function headerLines(stats, bounds, frameWidth, options, enabled) {
564
500
  const left = colorize("TOKEN LEDGER", TITLE_STYLE, enabled);
565
501
  const date = colorize(dateLabel(bounds, options.range), TEXT_STYLE, enabled);
566
- const mode = colorize(rangeModeLabel(bounds, options.range), [1, ...TEAL], enabled);
502
+ const mode = colorize(options.range === "week" ? "7 DAYS" : "DAY", [1, ...ACCENT_STYLE], enabled);
567
503
  const metric = (value, label) =>
568
- `${colorize(String(value), TITLE_STYLE, enabled)} ${colorize(label, DIM, enabled)}`;
569
- const separator = colorize("·", DIM, enabled);
504
+ `${colorize(String(value), TITLE_STYLE, enabled)} ${colorize(label, SECONDARY_STYLE, enabled)}`;
505
+ const separator = colorize("·", SECONDARY_STYLE, enabled);
570
506
  const join = ` ${separator} `;
571
507
  const alignHeader = (line) => fit(` ${line}`, frameWidth);
572
508
  const fullLine = [
@@ -582,18 +518,14 @@ function headerLines(stats, bounds, frameWidth, options, enabled) {
582
518
  return [alignHeader(fullLine)];
583
519
  }
584
520
 
585
- const crossesCalendarYear =
586
- bounds.startDateString?.slice(0, 4) !== bounds.endDateString?.slice(0, 4);
587
- const compactDate = (
588
- crossesCalendarYear
589
- ? dateLabel(bounds, options.range)
590
- : dateLabel(bounds, options.range).replace(/ 20\d{2}$/, "")
591
- ).replace(" – ", "–");
592
- const compactMode = rangeModeLabel(bounds, options.range, true);
521
+ const compactDate = dateLabel(bounds, options.range)
522
+ .replace(/ 20\d{2}$/, "")
523
+ .replace(" ", "–");
524
+ const compactMode = options.range === "week" ? "7D" : "DAY";
593
525
  const compactLine = [
594
526
  left,
595
527
  colorize(compactDate, TEXT_STYLE, enabled),
596
- colorize(compactMode, [1, ...TEAL], enabled),
528
+ colorize(compactMode, [1, ...ACCENT_STYLE], enabled),
597
529
  metric(`${compact(stats.totalTokens)}`, "T"),
598
530
  metric(stats.calls.toLocaleString("en-US"), "C"),
599
531
  metric(stats.threads.toLocaleString("en-US"), "TH"),
@@ -607,7 +539,7 @@ function headerLines(stats, bounds, frameWidth, options, enabled) {
607
539
  const minimalLine = [
608
540
  minimalTitle,
609
541
  colorize(compactDate.replaceAll(" ", ""), TEXT_STYLE, enabled),
610
- colorize(compactMode, [1, ...TEAL], enabled),
542
+ colorize(compactMode, [1, ...ACCENT_STYLE], enabled),
611
543
  compact(stats.totalTokens),
612
544
  compact(stats.calls),
613
545
  compact(stats.threads),
@@ -624,7 +556,7 @@ export function renderTerminal({ options, snapshot, bounds, events, rows, allRow
624
556
  const columns = options.width ?? (Number(process.stdout.columns) || 120);
625
557
  const frameWidth = Math.max(38, Math.min(158, columns - 2));
626
558
  const sideBySide = options.forceSideBySide ?? frameWidth >= 100;
627
- const sideWidth = sideBySide ? (stats.autoReview.present ? 28 : 26) : 0;
559
+ const sideWidth = sideBySide ? 26 : 0;
628
560
  const leftWidth = sideBySide ? frameWidth - sideWidth - 1 : frameWidth;
629
561
  const left = panelLines(rows, allRows, stats.totalTokens, leftWidth, options, enabled);
630
562
  const right = sideBySide ? sidebarLines(stats, sideWidth, enabled, options, quota) : null;
@@ -636,23 +568,21 @@ export function renderTerminal({ options, snapshot, bounds, events, rows, allRow
636
568
  lines.push("");
637
569
  lines.push(...sidebarLines(stats, frameWidth, enabled, options, quota));
638
570
  }
639
- if (!options.static && !options.hideHelp) {
640
- lines.push("");
641
- lines.push(
642
- colorize(
643
- options.ascii
644
- ? "[j/k] select [q/esc] quit"
645
- : "[↑↓ or j/k] select [q/esc] quit",
646
- DIM,
647
- enabled,
648
- ),
649
- );
650
- }
571
+ lines.push("");
572
+ lines.push(
573
+ colorize(
574
+ options.ascii
575
+ ? "[j/k] select [enter] inspect [d/w/m] range [q] quit"
576
+ : "[↑↓] select [enter] inspect [d/w/m] range [q] quit",
577
+ SECONDARY_STYLE,
578
+ enabled,
579
+ ),
580
+ );
651
581
  return lines.join("\n");
652
582
  }
653
583
 
654
- export const SCREEN_BASE = "\u001b[38;5;255m\u001b[48;2;16;16;18m";
655
- const PANEL_BASE = "\u001b[38;5;255m\u001b[48;2;5;5;6m";
584
+ export const SCREEN_BASE = "\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m";
585
+ const PANEL_BASE = "\u001b[38;2;255;255;255m\u001b[48;2;24;24;24m";
656
586
 
657
587
  function paintFullscreenLine(line, width, background, enabled) {
658
588
  const fitted = fit(line, width);
@@ -666,84 +596,30 @@ export function renderFullscreen({ options, snapshot, bounds, events, rows, allR
666
596
  const columns = Math.max(40, Number(width) || Number(process.stdout.columns) || 120);
667
597
  const screenHeight = Math.max(1, Number(height) || Number(process.stdout.rows) || 32);
668
598
  const frameWidth = Math.max(38, Math.min(158, columns - 4));
669
- const forceSideBySide = frameWidth >= 84;
670
- const staticBudget = Math.max(0, screenHeight - 1);
671
- let visibleRows = rows;
672
- let fullscreenOptions = {
673
- ...options,
674
- forceColor: enabled,
675
- forceSideBySide,
676
- highlight: false,
677
- compactSidebar: true,
678
- hideHelp: true,
679
- width: frameWidth + 2,
680
- };
681
-
682
- if (forceSideBySide && staticBudget >= 3) {
683
- const stats = summary(events);
684
- const quota = quotaCycleSummary(snapshot, events);
685
- const panelContentBudget = Math.max(1, staticBudget - 3);
686
- const visibleRowCount = Math.min(
687
- rows.length,
688
- Math.max(1, Math.floor((panelContentBudget - 2) / 2)),
689
- );
690
- const selectedIndex = Math.max(
691
- 0,
692
- Math.min(rows.length - 1, Number(options.selectedIndex) || 0),
693
- );
694
- const rowOffset = Math.min(
695
- Math.max(0, selectedIndex - visibleRowCount + 1),
696
- Math.max(0, rows.length - visibleRowCount),
697
- );
698
- visibleRows = rows.slice(rowOffset, rowOffset + visibleRowCount);
699
-
700
- const sideWidth = stats.autoReview.present ? 28 : 26;
701
- let modelLimit = modelLegendItems(stats.models, stats.totalTokens).length;
702
- while (
703
- modelLimit > 0 &&
704
- sidebarLines(
705
- stats,
706
- sideWidth,
707
- enabled,
708
- { ...fullscreenOptions, modelLimit },
709
- quota,
710
- ).length > panelContentBudget
711
- ) {
712
- modelLimit -= 1;
713
- }
714
-
715
- fullscreenOptions = {
716
- ...fullscreenOptions,
717
- modelLimit,
718
- rowOffset,
719
- selectedIndex: selectedIndex - rowOffset,
720
- };
721
- }
722
-
723
599
  const staticOutput = renderTerminal({
724
- options: fullscreenOptions,
600
+ options: {
601
+ ...options,
602
+ forceColor: enabled,
603
+ forceSideBySide: frameWidth >= 84,
604
+ highlight: false,
605
+ compactSidebar: true,
606
+ width: frameWidth + 2,
607
+ },
725
608
  snapshot,
726
609
  bounds,
727
610
  events,
728
- rows: visibleRows,
611
+ rows,
729
612
  allRows,
730
613
  });
731
- let staticLines = staticOutput.split("\n");
614
+ const staticLines = staticOutput.split("\n");
615
+ staticLines.pop();
732
616
  if (staticLines.at(-1) === "") staticLines.pop();
733
- const help = colorize("↑/↓ move • j/k move • q/esc quit", DIM, enabled);
734
- const includeSpacer = staticLines.length + 2 <= screenHeight;
735
- const availableStaticLines = Math.max(
736
- 0,
737
- screenHeight - 1 - (includeSpacer ? 1 : 0),
738
- );
739
- staticLines = staticLines.slice(0, availableStaticLines);
740
- const summaryLine = staticLines.shift();
617
+ const summaryLine = staticLines.shift() ?? "";
618
+ const help = colorize("↑/↓ move • j/k move • q/esc quit", SECONDARY_STYLE, enabled);
741
619
  const content = [
742
- ...(summaryLine === undefined
743
- ? []
744
- : [{ line: summaryLine, background: SCREEN_BASE }]),
620
+ { line: summaryLine, background: SCREEN_BASE },
745
621
  ...staticLines.map((line) => ({ line, background: PANEL_BASE })),
746
- ...(includeSpacer ? [{ line: "", background: SCREEN_BASE }] : []),
622
+ { line: "", background: SCREEN_BASE },
747
623
  { line: help, background: SCREEN_BASE },
748
624
  ];
749
625
  const topPadding = Math.max(0, Math.floor((screenHeight - content.length) / 2));