tledger 0.1.4 → 0.2.1

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,29 @@
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
+ import {
6
+ INTERACTIVE_FOOTER,
7
+ INTERACTIVE_HELP,
8
+ } from "./token-ledger-controls.mjs";
5
9
 
6
10
  const RESET = "\u001b[0m";
7
- const DIM = [38, 5, 245];
8
- const TEAL = [38, 5, 80];
11
+ const PRIMARY_STYLE = [38, 2, 255, 255, 255];
12
+ const SECONDARY_STYLE = [38, 2, 155, 155, 155];
13
+ const ACCENT_STYLE = [38, 2, 51, 156, 255];
14
+ const BORDER_STYLE = [38, 2, 88, 88, 88];
15
+ const TRACK_STYLE = [38, 2, 59, 59, 59];
9
16
  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],
17
+ sol: [38, 2, 120, 185, 242],
18
+ luna: ACCENT_STYLE,
19
+ terra: [38, 2, 214, 168, 95],
20
+ gpt: [38, 2, 174, 139, 219],
21
+ other: [38, 2, 116, 125, 144],
17
22
  };
18
- const TEXT_STYLE = [38, 5, 255];
19
- const TITLE_STYLE = [1, 38, 5, 255];
20
- const SUBTITLE_STYLE = [38, 5, 245];
23
+ const TEXT_STYLE = PRIMARY_STYLE;
24
+ const TITLE_STYLE = [1, ...PRIMARY_STYLE];
25
+ const SUBTITLE_STYLE = SECONDARY_STYLE;
26
+ const SELECTED_BACKGROUND = "\u001b[48;2;42;42;42m";
21
27
 
22
28
  function colorsEnabled(options) {
23
29
  return options.forceColor ?? (!options.plain && !process.env.NO_COLOR && Boolean(process.stdout.isTTY));
@@ -29,16 +35,7 @@ function colorize(value, code, enabled) {
29
35
  }
30
36
 
31
37
  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();
38
+ return String(value).replace(/\u001b\[[0-9;]*m/g, "");
42
39
  }
43
40
 
44
41
  function visibleLength(value) {
@@ -100,16 +97,23 @@ function plural(value, singular, pluralForm = `${singular}s`) {
100
97
  }
101
98
 
102
99
  function modelLabel(value) {
103
- const model = sanitizeText(value) || "Unknown model";
104
- return modelDisplayName(model);
100
+ const model = String(value || "Unknown model");
101
+ const lower = model.toLowerCase();
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";
105
107
  }
106
108
 
107
109
  function modelColor(model) {
108
- return MODEL_COLORS[modelColorKey(model)] ?? MODEL_COLORS.other;
110
+ const key = String(model || "").toLowerCase();
111
+ return MODEL_COLORS[key] ?? MODEL_COLORS.other;
109
112
  }
110
113
 
111
114
  function usageTypeLabel(value) {
112
- const words = (sanitizeText(value) || "unknown")
115
+ const words = String(value || "unknown")
116
+ .trim()
113
117
  .replace(/[_-]+/g, " ")
114
118
  .split(/\s+/)
115
119
  .filter(Boolean);
@@ -124,11 +128,12 @@ function usageTypeLabel(value) {
124
128
  }
125
129
 
126
130
  function displayProject(row) {
127
- return sanitizeText(row.displayProject || row.project) || "Unlabelled activity";
131
+ return row.displayProject || row.project || "Unlabelled activity";
128
132
  }
129
133
 
130
134
  function dateLabel(bounds, range = "day") {
131
- if (range !== "day" && bounds.rangeDays !== 1 && bounds.startDateString && bounds.endDateString) {
135
+ if (range === "rolling24h") return "LAST 24 HOURS";
136
+ if (range === "week" && bounds.startDateString && bounds.endDateString) {
132
137
  const startParts = bounds.startDateString.split("-").map(Number);
133
138
  const endParts = bounds.endDateString.split("-").map(Number);
134
139
  const monthNames = [
@@ -137,12 +142,7 @@ function dateLabel(bounds, range = "day") {
137
142
  ];
138
143
  const start = `${monthNames[startParts[1] - 1]} ${String(startParts[2]).padStart(2, "0")}`;
139
144
  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]}`;
145
+ return `${start} ${end} ${endParts[0]}`;
146
146
  }
147
147
  const parts = new Intl.DateTimeFormat("en-US", {
148
148
  timeZone: bounds.timeZone,
@@ -159,13 +159,6 @@ function dateLabel(bounds, range = "day") {
159
159
  return `${values.weekday} ${values.day} ${values.month} ${values.year}`.toUpperCase();
160
160
  }
161
161
 
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
162
  function modelTotals(events) {
170
163
  const totals = new Map();
171
164
  for (const event of events) {
@@ -180,9 +173,7 @@ function modelTotals(events) {
180
173
  function usageTypeTotals(events) {
181
174
  const totals = new Map();
182
175
  for (const event of events) {
183
- const key = modelLabel(event.model) === "Auto Review"
184
- ? "auto-review"
185
- : String(event.useType || "unknown").trim().toLowerCase() || "unknown";
176
+ const key = String(event.useType || "unknown").trim().toLowerCase() || "unknown";
186
177
  totals.set(key, (totals.get(key) ?? 0) + (Number(event.totalTokens) || 0));
187
178
  }
188
179
  return [...totals.entries()]
@@ -194,22 +185,17 @@ function usageTypeTotals(events) {
194
185
  .sort((left, right) => right.totalTokens - left.totalTokens);
195
186
  }
196
187
 
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;
188
+ function latestWeeklyQuotaObservation(snapshot) {
189
+ // The epoch-keyed timeline drops stale readings from superseded windows,
190
+ // so the last entry is the newest reading of the currently-live window.
191
+ const normalized = normalizeQuotaTimeline(weeklyQuotaObservations(snapshot));
192
+ const latest = normalized.at(-1);
193
+ if (!latest) return null;
194
+ return { ...latest, usedPercent: latest.normalizedUsedPercent };
209
195
  }
210
196
 
211
197
  export function quotaCycleSummary(snapshot = {}, displayedEvents = []) {
212
- const observation = latestWeeklyQuotaObservation(snapshot.quotaObservations);
198
+ const observation = latestWeeklyQuotaObservation(snapshot);
213
199
  if (!observation) {
214
200
  return {
215
201
  available: false,
@@ -297,32 +283,6 @@ function summary(events) {
297
283
  const cached = Math.max(0, Number(event.cachedInputTokens) || 0);
298
284
  return sum + Math.min(input, cached);
299
285
  }, 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
286
  return {
327
287
  totalTokens,
328
288
  calls,
@@ -333,48 +293,25 @@ function summary(events) {
333
293
  uncachedInputTokens: Math.max(0, inputTokens - cachedInputTokens),
334
294
  models: modelTotals(events),
335
295
  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
296
  };
346
297
  }
347
298
 
348
299
  function modelLegendItems(models, totalTokens) {
349
- return models
350
- .filter((model) => model.totalTokens > 0)
300
+ const known = new Map();
301
+ for (const model of models) {
302
+ const key = ["Sol", "Luna", "Terra", "GPT"].includes(model.model)
303
+ ? model.model
304
+ : "Other";
305
+ known.set(key, (known.get(key) ?? 0) + model.totalTokens);
306
+ }
307
+ return ["Luna", "Sol", "Terra", "GPT", "Other"]
351
308
  .map((model) => ({
352
- ...model,
353
- share: totalTokens > 0 ? (model.totalTokens / totalTokens) * 100 : 0,
309
+ model,
310
+ totalTokens: known.get(model) ?? 0,
311
+ share: totalTokens > 0 ? ((known.get(model) ?? 0) / totalTokens) * 100 : 0,
354
312
  }));
355
313
  }
356
314
 
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
315
  function stackedBar(row, width, maximumTokens, options, enabled) {
379
316
  const symbol = options.ascii ? "#" : "█";
380
317
  const trackSymbol = options.ascii ? "." : "░";
@@ -404,7 +341,7 @@ function stackedBar(row, width, maximumTokens, options, enabled) {
404
341
  .join("");
405
342
  const blank = colorize(
406
343
  trackSymbol.repeat(Math.max(0, width - visibleLength(filled))),
407
- DIM,
344
+ TRACK_STYLE,
408
345
  enabled,
409
346
  );
410
347
  return `${filled}${blank}`;
@@ -426,9 +363,13 @@ function panelLines(rows, allRows, totalTokens, panelWidth, options, enabled) {
426
363
  panelWidth - labelWidth - shareWidth - totalWidth - 1 - rightPadding,
427
364
  );
428
365
  const maxTokens = allRows[0]?.totalTokens ?? 0;
429
- const rowOffset = Number.isInteger(options.rowOffset) ? options.rowOffset : 0;
366
+ const totalCredits = allRows.reduce((sum, item) => sum + item.rateCardCredits, 0) || 1;
367
+ const selectedIndex = Math.min(
368
+ Math.max(0, Math.trunc(Number(options.selectedIndex) || 0)),
369
+ Math.max(0, rows.length - 1),
370
+ );
430
371
  const lines = [];
431
- const heading = colorize("TOKENS BY PROJECT", TEAL, enabled);
372
+ const heading = colorize("TOKENS BY PROJECT", ACCENT_STYLE, enabled);
432
373
  const barHeaderWidth = barWidth;
433
374
  const maximumLabel = compactMode ? `${compact(maxTokens)} max` : `${compact(maxTokens)} (max)`;
434
375
  const axisLabel = barHeaderWidth >= maximumLabel.length + 2 ? maximumLabel : compact(maxTokens);
@@ -438,29 +379,33 @@ function panelLines(rows, allRows, totalTokens, panelWidth, options, enabled) {
438
379
  lines.push(
439
380
  `${fit(heading, labelWidth)}${fit(axisText, barHeaderWidth)}${fit("TOKENS", totalWidth, "right")}${fit("SHARE", shareWidth, "right")}${" ".repeat(rightPadding)}`,
440
381
  );
441
- lines.push(colorize("─".repeat(panelWidth), DIM, enabled));
382
+ lines.push(colorize("─".repeat(panelWidth), BORDER_STYLE, enabled));
442
383
 
443
384
  for (const [index, row] of rows.entries()) {
444
- const rankValue = rowOffset + index + 1;
445
385
  const share = totalTokens > 0 ? (row.totalTokens / totalTokens) * 100 : 0;
446
- const selected = !options.static && index === (options.selectedIndex ?? 0);
386
+ const selected = index === selectedIndex;
447
387
  const caretValue = selected ? (options.ascii ? ">" : "▶") : " ";
448
- const prefix = `${caretValue} ${rankValue}. `;
388
+ const prefix = `${caretValue} ${index + 1}. `;
449
389
  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);
390
+ const caret = selected ? colorize(caretValue, [1, ...ACCENT_STYLE], enabled) : caretValue;
391
+ const rank = colorize(`${index + 1}.`, TITLE_STYLE, enabled);
452
392
  const title = `${caret} ${rank} ${colorize(projectText, TITLE_STYLE, enabled)}`;
453
393
  const label = fit(title, labelWidth);
454
394
  const metrics = `${fit(compact(row.totalTokens), totalWidth, "right")}${fit(percent(share), shareWidth, "right")}${" ".repeat(rightPadding)}`;
455
395
  const bar = stackedBar(row, barWidth, maxTokens, options, enabled);
456
396
  const rowLine = `${label}${bar} ${metrics}`;
457
- const detail = `${plural(row.threads, "thread")} · ${percent(share)} of tokens`;
397
+ const creditShare = row.rateCardCredits > 0 && row.rateCardCredits <= Number.MAX_SAFE_INTEGER
398
+ ? row.rateCardCredits
399
+ : 0;
400
+ const detail = `${plural(row.threads, "thread")} · ${percent(
401
+ creditShare > 0 ? (creditShare / totalCredits) * 100 : 0,
402
+ )}${labelWidth >= 35 ? " credits" : ""}`;
458
403
  const detailText = truncateText(detail, labelWidth - 4);
459
404
  const subtitle = colorize(detailText, SUBTITLE_STYLE, enabled);
460
405
  const detailLine = `${fit(` ${subtitle}`, labelWidth)}${" ".repeat(barWidth + 1 + totalWidth + shareWidth + rightPadding)}`;
461
406
  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}`);
407
+ lines.push(`${SELECTED_BACKGROUND}${fit(rowLine, panelWidth)}${RESET}`);
408
+ lines.push(`${SELECTED_BACKGROUND}${fit(detailLine, panelWidth)}${RESET}`);
464
409
  } else {
465
410
  lines.push(rowLine);
466
411
  lines.push(detailLine);
@@ -477,41 +422,36 @@ function sidebarLines(stats, panelWidth, enabled, options = {}, quota = null) {
477
422
  const push = (line = "") => {
478
423
  lines.push(`${" ".repeat(inset)}${fit(line, contentWidth)}${" ".repeat(inset)}`);
479
424
  };
480
- const divider = () => push(colorize("─".repeat(contentWidth), DIM, enabled));
481
- const heading = (value) => colorize(value, TEAL, enabled);
425
+ const divider = () => push(colorize("─".repeat(contentWidth), BORDER_STYLE, enabled));
426
+ const heading = (value) => colorize(value, ACCENT_STYLE, enabled);
482
427
  push(heading("MODEL MIX"));
483
428
  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) {
429
+ for (const item of modelLegendItems(stats.models, stats.totalTokens)) {
492
430
  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
- }
431
+ push(`${swatch} ${fit(item.model, Math.max(1, contentWidth - 10))}${fit(percent(item.share), 8, "right")}`);
499
432
  }
500
- const hiddenModelCount = modelItems.length - visibleModelItems.length;
501
- if (hiddenModelCount > 0) {
502
- push(colorize(`… ${plural(hiddenModelCount, "more model")}`, DIM, enabled));
503
- }
504
- if (!compactSidebar) push();
433
+ push();
505
434
  divider();
506
435
  push(heading("USAGE TYPE · TOKENS"));
507
436
  if (!compactSidebar) push();
508
- const usageItems = visibleUsageTypeItems(stats.usageTypes);
437
+ const usageItems = stats.usageTypes.length > 5
438
+ ? [
439
+ ...stats.usageTypes.slice(0, 4),
440
+ {
441
+ key: "other",
442
+ label: "Other",
443
+ totalTokens: stats.usageTypes
444
+ .slice(4)
445
+ .reduce((sum, item) => sum + item.totalTokens, 0),
446
+ },
447
+ ]
448
+ : stats.usageTypes;
509
449
  for (const item of usageItems) {
510
450
  const swatch = "■";
511
451
  const share = stats.totalTokens > 0 ? (item.totalTokens / stats.totalTokens) * 100 : 0;
512
452
  push(`${swatch} ${fit(item.label, Math.max(1, contentWidth - 10))}${fit(percent(share), 8, "right")}`);
513
453
  }
514
- if (!compactSidebar) push();
454
+ push();
515
455
  divider();
516
456
  push(heading("CACHE · INPUT"));
517
457
  if (!compactSidebar) push();
@@ -525,7 +465,7 @@ function sidebarLines(stats, panelWidth, enabled, options = {}, quota = null) {
525
465
  push(`${swatch} ${fit(item.label, Math.max(1, contentWidth - 10))}${fit(percent(share), 8, "right")}`);
526
466
  }
527
467
  if (quota?.available) {
528
- if (!compactSidebar) push();
468
+ push();
529
469
  divider();
530
470
  push(heading("RESET CYCLE"));
531
471
  if (!compactSidebar) push();
@@ -545,28 +485,41 @@ function panel(leftLines, rightLines, leftWidth, rightWidth, enabled, ascii) {
545
485
  ? { tl: "+", tr: "+", bl: "+", br: "+", h: "-", v: "|", tm: "+", bm: "+" }
546
486
  : { tl: "┌", tr: "┐", bl: "└", br: "┘", h: "─", v: "│", tm: "┬", bm: "┴" };
547
487
  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 : ""}`;
488
+ const border = (value) => colorize(value, BORDER_STYLE, enabled);
489
+ const top = border(`${glyphs.tl}${glyphs.h.repeat(leftWidth)}${rightLines ? glyphs.tm : glyphs.tr}${rightLines ? glyphs.h.repeat(rightWidth) + glyphs.tr : ""}`);
549
490
  const body = [];
550
491
  for (let index = 0; index < rows; index += 1) {
551
492
  const left = fit(leftLines[index] ?? "", leftWidth);
552
493
  if (rightLines) {
553
494
  const right = fit(rightLines[index] ?? "", rightWidth);
554
- body.push(`${glyphs.v}${left}${glyphs.v}${right}${glyphs.v}`);
495
+ body.push(`${border(glyphs.v)}${left}${border(glyphs.v)}${right}${border(glyphs.v)}`);
555
496
  } else {
556
- body.push(`${glyphs.v}${left}${glyphs.v}`);
497
+ body.push(`${border(glyphs.v)}${left}${border(glyphs.v)}`);
557
498
  }
558
499
  }
559
- const bottom = `${glyphs.bl}${glyphs.h.repeat(leftWidth)}${rightLines ? glyphs.bm : glyphs.br}${rightLines ? glyphs.h.repeat(rightWidth) + glyphs.br : ""}`;
500
+ const bottom = border(`${glyphs.bl}${glyphs.h.repeat(leftWidth)}${rightLines ? glyphs.bm : glyphs.br}${rightLines ? glyphs.h.repeat(rightWidth) + glyphs.br : ""}`);
560
501
  return [top, ...body, bottom];
561
502
  }
562
503
 
563
- function headerLines(stats, bounds, frameWidth, options, enabled) {
504
+ function snapshotLine(freshness, enabled) {
505
+ const detail = freshness?.status === "fresh" || freshness?.status === "stale"
506
+ ? `${freshness.status} · ${freshness.ageLabel}`
507
+ : "age unknown";
508
+ return `${colorize("SNAPSHOT", ACCENT_STYLE, enabled)} ${colorize("·", SECONDARY_STYLE, enabled)} ${colorize(detail, SECONDARY_STYLE, enabled)}`;
509
+ }
510
+
511
+ function headerLines(stats, bounds, frameWidth, options, enabled, freshness) {
564
512
  const left = colorize("TOKEN LEDGER", TITLE_STYLE, enabled);
565
513
  const date = colorize(dateLabel(bounds, options.range), TEXT_STYLE, enabled);
566
- const mode = colorize(rangeModeLabel(bounds, options.range), [1, ...TEAL], enabled);
514
+ const modeLabel = options.range === "rolling24h"
515
+ ? "24 HOURS"
516
+ : options.range === "week"
517
+ ? "7 DAYS"
518
+ : "DAY";
519
+ const mode = colorize(modeLabel, [1, ...ACCENT_STYLE], enabled);
567
520
  const metric = (value, label) =>
568
- `${colorize(String(value), TITLE_STYLE, enabled)} ${colorize(label, DIM, enabled)}`;
569
- const separator = colorize("·", DIM, enabled);
521
+ `${colorize(String(value), TITLE_STYLE, enabled)} ${colorize(label, SECONDARY_STYLE, enabled)}`;
522
+ const separator = colorize("·", SECONDARY_STYLE, enabled);
570
523
  const join = ` ${separator} `;
571
524
  const alignHeader = (line) => fit(` ${line}`, frameWidth);
572
525
  const fullLine = [
@@ -579,44 +532,58 @@ function headerLines(stats, bounds, frameWidth, options, enabled) {
579
532
  metric(stats.projectCount.toLocaleString("en-US"), "PROJECTS"),
580
533
  ].join(join);
581
534
  if (visibleLength(fullLine) < frameWidth) {
582
- return [alignHeader(fullLine)];
535
+ const lines = [alignHeader(fullLine)];
536
+ if (options.range === "rolling24h") lines.push(alignHeader(snapshotLine(freshness, enabled)));
537
+ return lines;
583
538
  }
584
539
 
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);
540
+ const compactDate = dateLabel(bounds, options.range)
541
+ .replace(/ 20\d{2}$/, "")
542
+ .replace(" ", "–");
543
+ const compactMode = options.range === "rolling24h"
544
+ ? "24H"
545
+ : options.range === "week"
546
+ ? "7D"
547
+ : "DAY";
593
548
  const compactLine = [
594
549
  left,
595
550
  colorize(compactDate, TEXT_STYLE, enabled),
596
- colorize(compactMode, [1, ...TEAL], enabled),
551
+ colorize(compactMode, [1, ...ACCENT_STYLE], enabled),
597
552
  metric(`${compact(stats.totalTokens)}`, "T"),
598
553
  metric(stats.calls.toLocaleString("en-US"), "C"),
599
554
  metric(stats.threads.toLocaleString("en-US"), "TH"),
600
555
  metric(stats.projectCount.toLocaleString("en-US"), "P"),
601
556
  ].join(join);
602
557
  if (visibleLength(compactLine) < frameWidth) {
603
- return [alignHeader(compactLine)];
558
+ const lines = [alignHeader(compactLine)];
559
+ if (options.range === "rolling24h") lines.push(alignHeader(snapshotLine(freshness, enabled)));
560
+ return lines;
604
561
  }
605
562
 
606
563
  const minimalTitle = colorize(frameWidth >= 45 ? "LEDGER" : "L", TITLE_STYLE, enabled);
607
564
  const minimalLine = [
608
565
  minimalTitle,
609
566
  colorize(compactDate.replaceAll(" ", ""), TEXT_STYLE, enabled),
610
- colorize(compactMode, [1, ...TEAL], enabled),
567
+ colorize(compactMode, [1, ...ACCENT_STYLE], enabled),
611
568
  compact(stats.totalTokens),
612
569
  compact(stats.calls),
613
570
  compact(stats.threads),
614
571
  compact(stats.projectCount),
615
572
  ].join(" ");
616
- return [alignHeader(minimalLine)];
573
+ const lines = [alignHeader(minimalLine)];
574
+ if (options.range === "rolling24h") lines.push(alignHeader(snapshotLine(freshness, enabled)));
575
+ return lines;
617
576
  }
618
577
 
619
- export function renderTerminal({ options, snapshot, bounds, events, rows, allRows }) {
578
+ export function renderTerminal({
579
+ options,
580
+ snapshot,
581
+ snapshotFreshness,
582
+ bounds,
583
+ events,
584
+ rows,
585
+ allRows,
586
+ }) {
620
587
  const enabled = colorsEnabled(options);
621
588
  const stats = summary(events);
622
589
  const quota = quotaCycleSummary(snapshot, events);
@@ -624,35 +591,31 @@ export function renderTerminal({ options, snapshot, bounds, events, rows, allRow
624
591
  const columns = options.width ?? (Number(process.stdout.columns) || 120);
625
592
  const frameWidth = Math.max(38, Math.min(158, columns - 2));
626
593
  const sideBySide = options.forceSideBySide ?? frameWidth >= 100;
627
- const sideWidth = sideBySide ? (stats.autoReview.present ? 28 : 26) : 0;
594
+ const sideWidth = sideBySide ? 26 : 0;
628
595
  const leftWidth = sideBySide ? frameWidth - sideWidth - 1 : frameWidth;
629
596
  const left = panelLines(rows, allRows, stats.totalTokens, leftWidth, options, enabled);
630
597
  const right = sideBySide ? sidebarLines(stats, sideWidth, enabled, options, quota) : null;
631
598
  const lines = [
632
- ...headerLines(stats, bounds, frameWidth, options, enabled),
599
+ ...headerLines(stats, bounds, frameWidth, options, enabled, snapshotFreshness),
633
600
  ...panel(left, right, leftWidth, sideWidth, enabled, options.ascii),
634
601
  ];
635
602
  if (!sideBySide) {
636
603
  lines.push("");
637
604
  lines.push(...sidebarLines(stats, frameWidth, enabled, options, quota));
638
605
  }
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
- }
606
+ lines.push("");
607
+ lines.push(
608
+ colorize(
609
+ options.ascii ? INTERACTIVE_FOOTER.ascii : INTERACTIVE_FOOTER.unicode,
610
+ SECONDARY_STYLE,
611
+ enabled,
612
+ ),
613
+ );
651
614
  return lines.join("\n");
652
615
  }
653
616
 
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";
617
+ export const SCREEN_BASE = "\u001b[38;2;255;255;255m\u001b[48;2;30;30;30m";
618
+ const PANEL_BASE = "\u001b[38;2;255;255;255m\u001b[48;2;24;24;24m";
656
619
 
657
620
  function paintFullscreenLine(line, width, background, enabled) {
658
621
  const fitted = fit(line, width);
@@ -661,89 +624,46 @@ function paintFullscreenLine(line, width, background, enabled) {
661
624
  return `${background}${restored}${RESET}`;
662
625
  }
663
626
 
664
- export function renderFullscreen({ options, snapshot, bounds, events, rows, allRows, width, height }) {
627
+ export function renderFullscreen({
628
+ options,
629
+ snapshot,
630
+ snapshotFreshness,
631
+ bounds,
632
+ events,
633
+ rows,
634
+ allRows,
635
+ width,
636
+ height,
637
+ }) {
665
638
  const enabled = options.forceColor ?? colorsEnabled(options);
666
639
  const columns = Math.max(40, Number(width) || Number(process.stdout.columns) || 120);
667
640
  const screenHeight = Math.max(1, Number(height) || Number(process.stdout.rows) || 32);
668
641
  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
642
  const staticOutput = renderTerminal({
724
- options: fullscreenOptions,
643
+ options: {
644
+ ...options,
645
+ forceColor: enabled,
646
+ forceSideBySide: frameWidth >= 84,
647
+ highlight: false,
648
+ compactSidebar: true,
649
+ width: frameWidth + 2,
650
+ },
725
651
  snapshot,
652
+ snapshotFreshness,
726
653
  bounds,
727
654
  events,
728
- rows: visibleRows,
655
+ rows,
729
656
  allRows,
730
657
  });
731
- let staticLines = staticOutput.split("\n");
658
+ const staticLines = staticOutput.split("\n");
659
+ staticLines.pop();
732
660
  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();
661
+ const summaryLine = staticLines.shift() ?? "";
662
+ const help = colorize(INTERACTIVE_HELP, SECONDARY_STYLE, enabled);
741
663
  const content = [
742
- ...(summaryLine === undefined
743
- ? []
744
- : [{ line: summaryLine, background: SCREEN_BASE }]),
664
+ { line: summaryLine, background: SCREEN_BASE },
745
665
  ...staticLines.map((line) => ({ line, background: PANEL_BASE })),
746
- ...(includeSpacer ? [{ line: "", background: SCREEN_BASE }] : []),
666
+ { line: "", background: SCREEN_BASE },
747
667
  { line: help, background: SCREEN_BASE },
748
668
  ];
749
669
  const topPadding = Math.max(0, Math.floor((screenHeight - content.length) / 2));