tledger 0.3.0 → 0.4.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.
@@ -0,0 +1,31 @@
1
+ export const SOURCE_STATUSES = Object.freeze([
2
+ "verified-current",
3
+ "explicit-snapshot",
4
+ "unchecked-cache",
5
+ "stale-fallback",
6
+ ]);
7
+
8
+ const SOURCE_STATUS_LABELS = Object.freeze({
9
+ "verified-current": "VERIFIED CURRENT",
10
+ "stale-fallback": "STALE FALLBACK",
11
+ "unchecked-cache": "UNCHECKED CACHE",
12
+ "explicit-snapshot": "EXPLICIT SNAPSHOT",
13
+ });
14
+
15
+ export function sourceStatusLabel(sourceStatus = "unchecked-cache") {
16
+ const label = SOURCE_STATUS_LABELS[sourceStatus];
17
+ if (!label) {
18
+ throw new Error(`Unknown report source status: ${String(sourceStatus)}`);
19
+ }
20
+ return label;
21
+ }
22
+
23
+ export function sourceStatusLine(sourceStatus = "unchecked-cache") {
24
+ return `PROVENANCE · ${sourceStatusLabel(sourceStatus)}`;
25
+ }
26
+
27
+ export function snapshotFreshnessDetail(freshness) {
28
+ return freshness?.status === "fresh" || freshness?.status === "stale"
29
+ ? `${freshness.status} · ${freshness.ageLabel}`
30
+ : "age unknown";
31
+ }
@@ -8,10 +8,22 @@ import {
8
8
  INTERACTIVE_HELP,
9
9
  } from "./token-ledger-controls.mjs";
10
10
  import {
11
+ MAX_SAFE_TOKEN_COUNT,
12
+ checkedFiniteAdd,
13
+ checkedTokenAdd,
14
+ scaledOutputTokens,
15
+ tokenValue,
11
16
  usageBucketsInRange,
12
17
  usageCallCount,
13
18
  usageThreadIds,
14
19
  } from "../lib/token-ledger-usage.mjs";
20
+ import { calendarDateParts } from "../lib/token-ledger-calendar.mjs";
21
+ import { sanitizeTerminalText } from "../lib/token-ledger-terminal-text.mjs";
22
+ import {
23
+ snapshotFreshnessDetail,
24
+ sourceStatusLine,
25
+ } from "./token-ledger-source-status.mjs";
26
+ import { historyScopeLabel } from "../lib/token-ledger-collection.mjs";
15
27
 
16
28
  const RESET = "\u001b[0m";
17
29
  const PRIMARY_STYLE = [38, 2, 255, 255, 255];
@@ -20,6 +32,7 @@ const ACCENT_STYLE = [38, 2, 51, 156, 255];
20
32
  const BORDER_STYLE = [38, 2, 88, 88, 88];
21
33
  const TRACK_STYLE = [38, 2, 59, 59, 59];
22
34
  export const MODEL_COLORS = {
35
+ astra: [38, 2, 232, 121, 249],
23
36
  sol: [38, 2, 120, 185, 242],
24
37
  luna: ACCENT_STYLE,
25
38
  terra: [38, 2, 214, 168, 95],
@@ -41,7 +54,7 @@ function colorize(value, code, enabled) {
41
54
  }
42
55
 
43
56
  function stripAnsi(value) {
44
- return String(value).replace(/\u001b\[[0-9;]*m/g, "");
57
+ return sanitizeTerminalText(value);
45
58
  }
46
59
 
47
60
  function visibleLength(value) {
@@ -111,6 +124,7 @@ function plural(value, singular, pluralForm = `${singular}s`) {
111
124
  function modelLabel(value) {
112
125
  const model = String(value || "Unknown model");
113
126
  const lower = model.toLowerCase();
127
+ if (lower.includes("astra")) return "Astra";
114
128
  if (lower.includes("sol")) return "Sol";
115
129
  if (lower.includes("luna")) return "Luna";
116
130
  if (lower.includes("terra")) return "Terra";
@@ -124,7 +138,7 @@ function modelColor(model) {
124
138
  }
125
139
 
126
140
  function usageTypeLabel(value) {
127
- const words = String(value || "unknown")
141
+ const words = sanitizeTerminalText(value || "unknown")
128
142
  .trim()
129
143
  .replace(/[_-]+/g, " ")
130
144
  .split(/\s+/)
@@ -140,7 +154,12 @@ function usageTypeLabel(value) {
140
154
  }
141
155
 
142
156
  function displayProject(row) {
143
- return row.displayProject || row.project || "Unlabelled activity";
157
+ const label = sanitizeTerminalText(
158
+ row.displayProject || row.project || "Unlabelled activity",
159
+ )
160
+ .replace(/\s+/g, " ")
161
+ .trim();
162
+ return label || "Unlabelled activity";
144
163
  }
145
164
 
146
165
  function isRollingRange(range) {
@@ -161,26 +180,37 @@ function dateLabel(bounds, range = "day", rollingLabel = "1 day") {
161
180
  const end = `${monthNames[endParts[1] - 1]} ${String(endParts[2]).padStart(2, "0")}`;
162
181
  return `${start} – ${end} ${endParts[0]}`;
163
182
  }
164
- const parts = new Intl.DateTimeFormat("en-US", {
165
- timeZone: bounds.timeZone,
183
+ const values = calendarDateParts(bounds.dateString, {
166
184
  weekday: "short",
167
185
  day: "2-digit",
168
186
  month: "short",
169
187
  year: "numeric",
170
- }).formatToParts(bounds.start);
171
- const values = Object.fromEntries(
172
- parts
173
- .filter((part) => part.type !== "literal")
174
- .map((part) => [part.type, part.value]),
175
- );
188
+ });
176
189
  return `${values.weekday} ${values.day} ${values.month} ${values.year}`.toUpperCase();
177
190
  }
178
191
 
179
192
  function modelTotals(events) {
180
193
  const totals = new Map();
194
+ let scale = 1;
195
+ let totalTokens = 0;
181
196
  for (const event of events) {
197
+ if (event?.invalidTokenRecord === true) continue;
182
198
  const model = modelLabel(event.model);
183
- totals.set(model, (totals.get(model) ?? 0) + (Number(event.totalTokens) || 0));
199
+ const allowFractional = event.rangeAllocationEstimated === true;
200
+ const tokens = tokenValue(event.totalTokens, { allowFractional });
201
+ const scaledTokens = tokens / scale;
202
+ totalTokens += scaledTokens;
203
+ totals.set(
204
+ model,
205
+ (totals.get(model) ?? 0) + scaledTokens,
206
+ );
207
+ const scaleFactor = Math.max(1, totalTokens / MAX_SAFE_TOKEN_COUNT);
208
+ if (scaleFactor === 1) continue;
209
+ for (const [modelName, value] of totals) {
210
+ totals.set(modelName, value / scaleFactor);
211
+ }
212
+ totalTokens = MAX_SAFE_TOKEN_COUNT;
213
+ scale *= scaleFactor;
184
214
  }
185
215
  return [...totals.entries()]
186
216
  .map(([model, totalTokens]) => ({ model, totalTokens }))
@@ -189,9 +219,28 @@ function modelTotals(events) {
189
219
 
190
220
  function usageTypeTotals(events) {
191
221
  const totals = new Map();
222
+ let scale = 1;
223
+ let totalTokens = 0;
192
224
  for (const event of events) {
193
- const key = String(event.useType || "unknown").trim().toLowerCase() || "unknown";
194
- totals.set(key, (totals.get(key) ?? 0) + (Number(event.totalTokens) || 0));
225
+ if (event?.invalidTokenRecord === true) continue;
226
+ const key = sanitizeTerminalText(event.useType || "unknown")
227
+ .trim()
228
+ .toLowerCase() || "unknown";
229
+ const allowFractional = event.rangeAllocationEstimated === true;
230
+ const tokens = tokenValue(event.totalTokens, { allowFractional });
231
+ const scaledTokens = tokens / scale;
232
+ totalTokens += scaledTokens;
233
+ totals.set(
234
+ key,
235
+ (totals.get(key) ?? 0) + scaledTokens,
236
+ );
237
+ const scaleFactor = Math.max(1, totalTokens / MAX_SAFE_TOKEN_COUNT);
238
+ if (scaleFactor === 1) continue;
239
+ for (const [usageType, value] of totals) {
240
+ totals.set(usageType, value / scaleFactor);
241
+ }
242
+ totalTokens = MAX_SAFE_TOKEN_COUNT;
243
+ scale *= scaleFactor;
195
244
  }
196
245
  return [...totals.entries()]
197
246
  .map(([key, totalTokens]) => ({
@@ -202,6 +251,19 @@ function usageTypeTotals(events) {
202
251
  .sort((left, right) => right.totalTokens - left.totalTokens);
203
252
  }
204
253
 
254
+ function addCacheTotals(state, inputTokens, cachedInputTokens) {
255
+ const nextInput = state.inputTokens + inputTokens / state.scale;
256
+ const nextCached = state.cachedInputTokens + cachedInputTokens / state.scale;
257
+ const scaleFactor = Math.max(
258
+ 1,
259
+ nextInput / MAX_SAFE_TOKEN_COUNT,
260
+ nextCached / MAX_SAFE_TOKEN_COUNT,
261
+ );
262
+ state.inputTokens = nextInput / scaleFactor;
263
+ state.cachedInputTokens = nextCached / scaleFactor;
264
+ state.scale *= scaleFactor;
265
+ }
266
+
205
267
  function latestWeeklyQuotaObservation(snapshot) {
206
268
  // The epoch-keyed timeline drops stale readings from superseded windows,
207
269
  // so the last entry is the newest reading of the currently-live window.
@@ -228,9 +290,12 @@ export function quotaCycleSummary(snapshot = {}, displayedEvents = []) {
228
290
  const windowStartMs =
229
291
  (Number(observation.resetsAt) - Number(observation.windowMinutes) * 60) * 1_000;
230
292
  const resetAtMs = Number(observation.resetsAt) * 1_000;
293
+ const observationThroughMs = Number.isFinite(observation.observedThroughMs)
294
+ ? observation.observedThroughMs
295
+ : new Date(observation.eventCutoffAt ?? observation.timestamp).getTime();
231
296
  const observedThroughMs = Math.min(
232
297
  resetAtMs,
233
- new Date(observation.eventCutoffAt ?? observation.timestamp).getTime(),
298
+ observationThroughMs,
234
299
  );
235
300
  if (
236
301
  !Number.isFinite(windowStartMs) ||
@@ -249,20 +314,43 @@ export function quotaCycleSummary(snapshot = {}, displayedEvents = []) {
249
314
  };
250
315
  }
251
316
 
252
- const sumUsage = (events) =>
253
- events.reduce(
254
- (acc, event) => {
255
- const tokens = Number(event.totalTokens) || 0;
256
- acc.tokens += tokens;
257
- const credits = eventCredits(event);
258
- if (Number.isFinite(credits) && credits >= 0) {
259
- acc.credits += credits;
260
- acc.ratedTokens += tokens;
261
- }
262
- return acc;
263
- },
264
- { tokens: 0, credits: 0, ratedTokens: 0 },
265
- );
317
+ const sumUsage = (events, initialScale = 1) => {
318
+ const acc = {
319
+ tokens: 0,
320
+ credits: 0,
321
+ ratedTokens: 0,
322
+ hasUnrated: false,
323
+ scale: Number.isFinite(initialScale) && initialScale >= 1
324
+ ? initialScale
325
+ : 1,
326
+ };
327
+ for (const event of events) {
328
+ if (event?.invalidTokenRecord === true) continue;
329
+ const allowFractional = event.rangeAllocationEstimated === true;
330
+ const tokens = tokenValue(event.totalTokens, { allowFractional });
331
+ const contribution = tokens / acc.scale;
332
+ const nextTokens = acc.tokens + contribution;
333
+ const scaleFactor = Math.max(
334
+ 1,
335
+ nextTokens / MAX_SAFE_TOKEN_COUNT,
336
+ );
337
+ if (scaleFactor > 1) {
338
+ acc.tokens /= scaleFactor;
339
+ acc.ratedTokens /= scaleFactor;
340
+ acc.scale *= scaleFactor;
341
+ }
342
+ const scaledContribution = contribution / scaleFactor;
343
+ acc.tokens += scaledContribution;
344
+ const credits = eventCredits(event);
345
+ if (Number.isFinite(credits) && credits >= 0) {
346
+ acc.credits = checkedFiniteAdd(acc.credits, credits);
347
+ acc.ratedTokens += scaledContribution;
348
+ } else if (tokens > 0) {
349
+ acc.hasUnrated = true;
350
+ }
351
+ }
352
+ return acc;
353
+ };
266
354
  const cycleEndMs = observedThroughMs + 1;
267
355
  const cycle = sumUsage(
268
356
  usageBucketsInRange(snapshot, windowStartMs, cycleEndMs),
@@ -273,6 +361,7 @@ export function quotaCycleSummary(snapshot = {}, displayedEvents = []) {
273
361
  windowStartMs,
274
362
  cycleEndMs,
275
363
  ),
364
+ cycle.scale,
276
365
  );
277
366
  const usedPercent = Math.min(100, Math.max(0, Number(observation.usedPercent) || 0));
278
367
  // The weekly meter weights usage by model, token type, and fast mode;
@@ -280,7 +369,10 @@ export function quotaCycleSummary(snapshot = {}, displayedEvents = []) {
280
369
  // when every event in the cycle is rated, and fall back to raw token
281
370
  // share otherwise.
282
371
  const creditsUsable =
283
- cycle.tokens > 0 && cycle.ratedTokens === cycle.tokens && cycle.credits > 0;
372
+ !cycle.hasUnrated &&
373
+ cycle.tokens > 0 &&
374
+ cycle.ratedTokens === cycle.tokens &&
375
+ cycle.credits > 0;
284
376
  const displayedSharePercent = creditsUsable
285
377
  ? (displayed.credits / cycle.credits) * 100
286
378
  : cycle.tokens
@@ -302,39 +394,44 @@ export function quotaCycleSummary(snapshot = {}, displayedEvents = []) {
302
394
  };
303
395
  }
304
396
 
305
- function summary(events) {
306
- const totalTokens = events.reduce(
307
- (sum, event) => sum + (Number(event.totalTokens) || 0),
397
+ function summary(events, projectRows) {
398
+ const totalTokens = projectRows.reduce(
399
+ (sum, row) => sum + row.totalTokens,
308
400
  0,
309
401
  );
310
402
  const calls = events.reduce(
311
- (sum, event) => sum + usageCallCount(event),
403
+ (sum, event) => {
404
+ const allowFractional = event?.rangeAllocationEstimated === true;
405
+ return checkedTokenAdd(sum, usageCallCount(event), { allowFractional });
406
+ },
312
407
  0,
313
408
  );
314
409
  const threadIds = new Set(
315
410
  events.flatMap((event) => usageThreadIds(event)),
316
411
  );
317
- const outputTokens = events.reduce(
318
- (sum, event) => sum + (Number(event.outputTokens) || 0),
319
- 0,
320
- );
321
- const inputTokens = events.reduce(
322
- (sum, event) => sum + (Number(event.inputTokens) || 0),
323
- 0,
324
- );
325
- const cachedInputTokens = events.reduce((sum, event) => {
326
- const input = Math.max(0, Number(event.inputTokens) || 0);
327
- const cached = Math.max(0, Number(event.cachedInputTokens) || 0);
328
- return sum + Math.min(input, cached);
329
- }, 0);
412
+ const outputTokens = scaledOutputTokens(events, totalTokens);
413
+ const cacheTotals = { inputTokens: 0, cachedInputTokens: 0, scale: 1 };
414
+ for (const event of events) {
415
+ if (event?.invalidTokenRecord === true) continue;
416
+ const allowFractional = event.rangeAllocationEstimated === true;
417
+ const input = tokenValue(event.inputTokens, { allowFractional });
418
+ const cached = Math.min(
419
+ input,
420
+ tokenValue(event.cachedInputTokens, { allowFractional }),
421
+ );
422
+ addCacheTotals(cacheTotals, input, cached);
423
+ }
330
424
  return {
331
425
  totalTokens,
332
426
  calls,
333
427
  threads: threadIds.size,
334
428
  outputTokens,
335
- inputTokens,
336
- cachedInputTokens,
337
- uncachedInputTokens: Math.max(0, inputTokens - cachedInputTokens),
429
+ inputTokens: cacheTotals.inputTokens,
430
+ cachedInputTokens: cacheTotals.cachedInputTokens,
431
+ uncachedInputTokens: Math.max(
432
+ 0,
433
+ cacheTotals.inputTokens - cacheTotals.cachedInputTokens,
434
+ ),
338
435
  models: modelTotals(events),
339
436
  usageTypes: usageTypeTotals(events),
340
437
  };
@@ -343,12 +440,12 @@ function summary(events) {
343
440
  function modelLegendItems(models, totalTokens) {
344
441
  const known = new Map();
345
442
  for (const model of models) {
346
- const key = ["Sol", "Luna", "Terra", "GPT"].includes(model.model)
443
+ const key = ["Astra", "Sol", "Luna", "Terra", "GPT"].includes(model.model)
347
444
  ? model.model
348
445
  : "Other";
349
446
  known.set(key, (known.get(key) ?? 0) + model.totalTokens);
350
447
  }
351
- return ["Luna", "Sol", "Terra", "GPT", "Other"]
448
+ return ["Astra", "Luna", "Sol", "Terra", "GPT", "Other"]
352
449
  .map((model) => ({
353
450
  model,
354
451
  totalTokens: known.get(model) ?? 0,
@@ -407,7 +504,10 @@ function panelLines(rows, allRows, totalTokens, panelWidth, options, enabled) {
407
504
  panelWidth - labelWidth - shareWidth - totalWidth - 1 - rightPadding,
408
505
  );
409
506
  const maxTokens = allRows[0]?.totalTokens ?? 0;
410
- const totalCredits = allRows.reduce((sum, item) => sum + item.rateCardCredits, 0) || 1;
507
+ const totalCredits = allRows.reduce(
508
+ (sum, item) => checkedFiniteAdd(sum, item.rateCardCredits),
509
+ 0,
510
+ ) || 1;
411
511
  const selectedIndex = Math.min(
412
512
  Math.max(0, Math.trunc(Number(options.selectedIndex) || 0)),
413
513
  Math.max(0, rows.length - 1),
@@ -486,7 +586,12 @@ function sidebarLines(stats, panelWidth, enabled, options = {}, quota = null) {
486
586
  label: "Other",
487
587
  totalTokens: stats.usageTypes
488
588
  .slice(4)
489
- .reduce((sum, item) => sum + item.totalTokens, 0),
589
+ .reduce(
590
+ (sum, item) => checkedTokenAdd(sum, item.totalTokens, {
591
+ allowFractional: true,
592
+ }),
593
+ 0,
594
+ ),
490
595
  },
491
596
  ]
492
597
  : stats.usageTypes;
@@ -546,13 +651,61 @@ function panel(leftLines, rightLines, leftWidth, rightWidth, enabled, ascii) {
546
651
  }
547
652
 
548
653
  function snapshotLine(freshness, enabled) {
549
- const detail = freshness?.status === "fresh" || freshness?.status === "stale"
550
- ? `${freshness.status} · ${freshness.ageLabel}`
551
- : "age unknown";
654
+ const detail = snapshotFreshnessDetail(freshness);
552
655
  return `${colorize("SNAPSHOT", ACCENT_STYLE, enabled)} ${colorize("·", SECONDARY_STYLE, enabled)} ${colorize(detail, SECONDARY_STYLE, enabled)}`;
553
656
  }
554
657
 
555
- function headerLines(stats, bounds, frameWidth, options, enabled, freshness) {
658
+ function provenanceLine(sourceStatus, enabled) {
659
+ return colorize(sourceStatusLine(sourceStatus), SECONDARY_STYLE, enabled);
660
+ }
661
+
662
+ function positiveCoverageCount(value) {
663
+ try {
664
+ const count = Number(value);
665
+ return Number.isFinite(count) && count > 0 ? count : 0;
666
+ } catch {
667
+ return 0;
668
+ }
669
+ }
670
+
671
+ export function incompleteSourceWarning(snapshot = {}) {
672
+ const coverage = snapshot?.coverage ?? {};
673
+ const parseErrors = positiveCoverageCount(coverage.parseErrors);
674
+ const invalidTokenRecords = positiveCoverageCount(coverage.invalidTokenRecords);
675
+ if (
676
+ parseErrors <= 0 &&
677
+ invalidTokenRecords <= 0 &&
678
+ coverage.sourceIncomplete !== true
679
+ ) {
680
+ return null;
681
+ }
682
+ const details = [];
683
+ if (parseErrors > 0) {
684
+ details.push(
685
+ `${parseErrors.toLocaleString("en-US")} PARSE ERROR${parseErrors === 1 ? "" : "S"}`,
686
+ );
687
+ }
688
+ if (invalidTokenRecords > 0) {
689
+ details.push(
690
+ `${invalidTokenRecords.toLocaleString("en-US")} INVALID TOKEN RECORD${invalidTokenRecords === 1 ? "" : "S"}`,
691
+ );
692
+ }
693
+ if (coverage.sourceIncomplete === true) {
694
+ details.push("INCOMPLETE SOURCE PROVENANCE");
695
+ }
696
+ return ["SOURCES INCOMPLETE", ...details].join(" · ");
697
+ }
698
+
699
+ function headerLines(
700
+ stats,
701
+ bounds,
702
+ frameWidth,
703
+ options,
704
+ enabled,
705
+ freshness,
706
+ sourceStatus,
707
+ snapshot,
708
+ ) {
556
709
  const left = colorize("TOKEN LEDGER", TITLE_STYLE, enabled);
557
710
  const date = colorize(
558
711
  dateLabel(bounds, options.range, options.rollingLabel),
@@ -572,6 +725,23 @@ function headerLines(stats, bounds, frameWidth, options, enabled, freshness) {
572
725
  const separator = colorize("·", SECONDARY_STYLE, enabled);
573
726
  const join = ` ${separator} `;
574
727
  const alignHeader = (line) => fit(` ${line}`, frameWidth);
728
+ const history = historyScopeLabel(snapshot);
729
+ const appendHistory = (lines) => history
730
+ ? [...lines, alignHeader(colorize(history, SECONDARY_STYLE, enabled))]
731
+ : lines;
732
+ const appendSnapshotMetadata = (lines) => {
733
+ const metadata = appendHistory([
734
+ ...lines,
735
+ ...(isRollingRange(options.range)
736
+ ? [alignHeader(snapshotLine(freshness, enabled))]
737
+ : []),
738
+ alignHeader(provenanceLine(sourceStatus, enabled)),
739
+ ]);
740
+ const warning = incompleteSourceWarning(snapshot);
741
+ return warning
742
+ ? [...metadata, alignHeader(colorize(warning, SECONDARY_STYLE, enabled))]
743
+ : metadata;
744
+ };
575
745
  const fullLine = [
576
746
  left,
577
747
  date,
@@ -582,9 +752,7 @@ function headerLines(stats, bounds, frameWidth, options, enabled, freshness) {
582
752
  metric(stats.projectCount.toLocaleString("en-US"), "PROJECTS"),
583
753
  ].join(join);
584
754
  if (visibleLength(fullLine) < frameWidth) {
585
- const lines = [alignHeader(fullLine)];
586
- if (isRollingRange(options.range)) lines.push(alignHeader(snapshotLine(freshness, enabled)));
587
- return lines;
755
+ return appendSnapshotMetadata([alignHeader(fullLine)]);
588
756
  }
589
757
 
590
758
  const compactDate = dateLabel(bounds, options.range, options.rollingLabel)
@@ -607,9 +775,7 @@ function headerLines(stats, bounds, frameWidth, options, enabled, freshness) {
607
775
  metric(stats.projectCount.toLocaleString("en-US"), "P"),
608
776
  ].join(join);
609
777
  if (visibleLength(compactLine) < frameWidth) {
610
- const lines = [alignHeader(compactLine)];
611
- if (isRollingRange(options.range)) lines.push(alignHeader(snapshotLine(freshness, enabled)));
612
- return lines;
778
+ return appendSnapshotMetadata([alignHeader(compactLine)]);
613
779
  }
614
780
 
615
781
  const minimalTitle = colorize(frameWidth >= 45 ? "LEDGER" : "L", TITLE_STYLE, enabled);
@@ -622,22 +788,21 @@ function headerLines(stats, bounds, frameWidth, options, enabled, freshness) {
622
788
  compact(stats.threads),
623
789
  compact(stats.projectCount),
624
790
  ].join(" ");
625
- const lines = [alignHeader(minimalLine)];
626
- if (isRollingRange(options.range)) lines.push(alignHeader(snapshotLine(freshness, enabled)));
627
- return lines;
791
+ return appendSnapshotMetadata([alignHeader(minimalLine)]);
628
792
  }
629
793
 
630
794
  export function renderTerminal({
631
795
  options,
632
796
  snapshot,
633
797
  snapshotFreshness,
798
+ sourceStatus = "unchecked-cache",
634
799
  bounds,
635
800
  events,
636
801
  rows,
637
802
  allRows,
638
803
  }) {
639
804
  const enabled = colorsEnabled(options);
640
- const stats = summary(events);
805
+ const stats = summary(events, allRows);
641
806
  const quota = quotaCycleSummary(snapshot, events);
642
807
  stats.projectCount = allRows.length;
643
808
  const columns = options.width ?? (Number(process.stdout.columns) || 120);
@@ -648,7 +813,16 @@ export function renderTerminal({
648
813
  const left = panelLines(rows, allRows, stats.totalTokens, leftWidth, options, enabled);
649
814
  const right = sideBySide ? sidebarLines(stats, sideWidth, enabled, options, quota) : null;
650
815
  const lines = [
651
- ...headerLines(stats, bounds, frameWidth, options, enabled, snapshotFreshness),
816
+ ...headerLines(
817
+ stats,
818
+ bounds,
819
+ frameWidth,
820
+ options,
821
+ enabled,
822
+ snapshotFreshness,
823
+ sourceStatus,
824
+ snapshot,
825
+ ),
652
826
  ...panel(left, right, leftWidth, sideWidth, enabled, options.ascii),
653
827
  ];
654
828
  if (!sideBySide) {
@@ -688,6 +862,7 @@ export function renderFullscreen({
688
862
  options,
689
863
  snapshot,
690
864
  snapshotFreshness,
865
+ sourceStatus = "unchecked-cache",
691
866
  bounds,
692
867
  events,
693
868
  rows,
@@ -710,6 +885,7 @@ export function renderFullscreen({
710
885
  },
711
886
  snapshot,
712
887
  snapshotFreshness,
888
+ sourceStatus,
713
889
  bounds,
714
890
  events,
715
891
  rows,