@tokentop/ttop 0.2.2 → 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.
Files changed (160) hide show
  1. package/README.md +2 -5
  2. package/package.json +8 -5
  3. package/src/agents/aggregator.ts +39 -19
  4. package/src/agents/costing.ts +11 -9
  5. package/src/agents/index.ts +7 -9
  6. package/src/agents/types.ts +2 -2
  7. package/src/cli.ts +28 -23
  8. package/src/config/schema.test.ts +42 -40
  9. package/src/config/schema.ts +26 -26
  10. package/src/demo/simulator.ts +161 -122
  11. package/src/plugins/agents/index.ts +2 -2
  12. package/src/plugins/auth-sources.ts +42 -38
  13. package/src/plugins/lifecycle.ts +42 -38
  14. package/src/plugins/loader.test.ts +49 -49
  15. package/src/plugins/loader.ts +67 -63
  16. package/src/plugins/notification-bus.ts +21 -16
  17. package/src/plugins/notifications/index.ts +2 -2
  18. package/src/plugins/notifications/terminal-bell.ts +36 -23
  19. package/src/plugins/notifications/visual-flash.ts +31 -19
  20. package/src/plugins/npm-installer.ts +23 -25
  21. package/src/plugins/plugin-context-factory.ts +10 -10
  22. package/src/plugins/plugin-host.ts +12 -5
  23. package/src/plugins/providers/anthropic.ts +64 -57
  24. package/src/plugins/providers/antigravity.ts +81 -69
  25. package/src/plugins/providers/codex.ts +42 -42
  26. package/src/plugins/providers/gemini.ts +106 -87
  27. package/src/plugins/providers/github-copilot.ts +49 -45
  28. package/src/plugins/providers/index.ts +10 -12
  29. package/src/plugins/providers/minimax.ts +50 -44
  30. package/src/plugins/providers/openai-api.ts +82 -62
  31. package/src/plugins/providers/opencode-zen.ts +46 -39
  32. package/src/plugins/providers/perplexity.ts +55 -46
  33. package/src/plugins/providers/zai.ts +68 -57
  34. package/src/plugins/registry.ts +80 -71
  35. package/src/plugins/sandbox-guard.test.ts +45 -48
  36. package/src/plugins/sandbox-guard.ts +20 -25
  37. package/src/plugins/sandbox.ts +26 -48
  38. package/src/plugins/themes/catppuccin-latte.ts +29 -29
  39. package/src/plugins/themes/catppuccin-mocha.ts +29 -29
  40. package/src/plugins/themes/claude-code.ts +29 -29
  41. package/src/plugins/themes/dracula.ts +29 -29
  42. package/src/plugins/themes/github-light.ts +29 -29
  43. package/src/plugins/themes/gruvbox-dark.ts +29 -29
  44. package/src/plugins/themes/gruvbox-light.ts +29 -29
  45. package/src/plugins/themes/index.ts +15 -15
  46. package/src/plugins/themes/kanagawa.ts +29 -29
  47. package/src/plugins/themes/nord.ts +29 -29
  48. package/src/plugins/themes/one-dark.ts +29 -29
  49. package/src/plugins/themes/opencode.ts +31 -31
  50. package/src/plugins/themes/rose-pine-dawn.ts +29 -29
  51. package/src/plugins/themes/rose-pine.ts +29 -29
  52. package/src/plugins/themes/solarized-light.ts +29 -29
  53. package/src/plugins/themes/tokyo-night.ts +29 -29
  54. package/src/plugins/types/agent.ts +5 -5
  55. package/src/plugins/types/base.ts +35 -24
  56. package/src/plugins/types/index.ts +9 -9
  57. package/src/plugins/types/notification.ts +12 -12
  58. package/src/plugins/types/provider.ts +12 -8
  59. package/src/plugins/types/theme.ts +31 -23
  60. package/src/plugins/update-checker.ts +6 -6
  61. package/src/pricing/estimator.test.ts +39 -44
  62. package/src/pricing/estimator.ts +7 -13
  63. package/src/pricing/fallback.test.ts +30 -34
  64. package/src/pricing/fallback.ts +54 -27
  65. package/src/pricing/index.ts +18 -12
  66. package/src/pricing/models-dev.ts +6 -6
  67. package/src/storage/database.ts +56 -46
  68. package/src/storage/db.ts +12 -12
  69. package/src/storage/index.ts +44 -45
  70. package/src/storage/migrations/index.ts +2 -2
  71. package/src/storage/paths.ts +13 -13
  72. package/src/storage/persistence-service.ts +206 -0
  73. package/src/storage/repos/agentSessions.ts +33 -23
  74. package/src/storage/repos/providerSnapshots.ts +5 -5
  75. package/src/storage/repos/usageEvents.ts +40 -28
  76. package/src/storage/retention.ts +146 -0
  77. package/src/storage/types.ts +5 -5
  78. package/src/tui/App.tsx +180 -137
  79. package/src/tui/components/CommandPalette.tsx +46 -34
  80. package/src/tui/components/DebugConsole.tsx +39 -33
  81. package/src/tui/components/DebugPanel.tsx +314 -183
  82. package/src/tui/components/FooterHints.tsx +2 -2
  83. package/src/tui/components/GhostProviderCard.tsx +37 -35
  84. package/src/tui/components/Header.tsx +68 -35
  85. package/src/tui/components/HelpOverlay.tsx +99 -29
  86. package/src/tui/components/InlineGauge.tsx +10 -7
  87. package/src/tui/components/InlineSparkline.tsx +17 -11
  88. package/src/tui/components/KpiStrip.tsx +128 -98
  89. package/src/tui/components/LimitGauge.tsx +96 -59
  90. package/src/tui/components/ModalBackdrop.tsx +4 -4
  91. package/src/tui/components/ProviderAggregateStrip.tsx +69 -38
  92. package/src/tui/components/ProviderCard.tsx +232 -226
  93. package/src/tui/components/ProviderDetailPanel.tsx +133 -58
  94. package/src/tui/components/ProviderLimitsPanel.tsx +126 -89
  95. package/src/tui/components/ProvidersList.tsx +215 -126
  96. package/src/tui/components/SessionDetailsDrawer.tsx +191 -94
  97. package/src/tui/components/SessionsTable.tsx +320 -169
  98. package/src/tui/components/SettingsModal.tsx +593 -269
  99. package/src/tui/components/SidebarBreakdown.tsx +30 -16
  100. package/src/tui/components/Skeleton.tsx +24 -17
  101. package/src/tui/components/SmartSidebar.tsx +360 -258
  102. package/src/tui/components/Sparkline.tsx +64 -52
  103. package/src/tui/components/Spinner.tsx +3 -3
  104. package/src/tui/components/StatusBar.tsx +27 -27
  105. package/src/tui/components/ThemePicker.tsx +67 -66
  106. package/src/tui/components/Toast.tsx +9 -6
  107. package/src/tui/components/UsageGauge.tsx +10 -13
  108. package/src/tui/components/index.ts +5 -5
  109. package/src/tui/contexts/AgentSessionContext.tsx +164 -151
  110. package/src/tui/contexts/ConfigContext.tsx +15 -13
  111. package/src/tui/contexts/DashboardRuntimeContext.tsx +26 -26
  112. package/src/tui/contexts/DemoModeContext.tsx +22 -15
  113. package/src/tui/contexts/DrawerContext.tsx +11 -9
  114. package/src/tui/contexts/InputContext.tsx +2 -2
  115. package/src/tui/contexts/LogContext.tsx +62 -47
  116. package/src/tui/contexts/PluginContext.tsx +238 -183
  117. package/src/tui/contexts/RealTimeActivityContext.tsx +70 -36
  118. package/src/tui/contexts/StorageContext.tsx +95 -149
  119. package/src/tui/contexts/ThemeContext.tsx +21 -16
  120. package/src/tui/contexts/TimeWindowContext.tsx +72 -61
  121. package/src/tui/contexts/ToastContext.tsx +10 -7
  122. package/src/tui/contexts/index.ts +9 -5
  123. package/src/tui/createApp.tsx +8 -12
  124. package/src/tui/debug/captureFrame.ts +25 -28
  125. package/src/tui/debug/snapshot.tsx +179 -162
  126. package/src/tui/driver/assertions.ts +30 -22
  127. package/src/tui/driver/cli.ts +231 -221
  128. package/src/tui/driver/coverage.ts +27 -44
  129. package/src/tui/driver/demo-snapshot.ts +33 -33
  130. package/src/tui/driver/diff.ts +26 -32
  131. package/src/tui/driver/driver.ts +43 -48
  132. package/src/tui/driver/index.ts +36 -40
  133. package/src/tui/driver/recorder.ts +37 -48
  134. package/src/tui/driver/test-workflow.ts +51 -51
  135. package/src/tui/driver/test.ts +25 -25
  136. package/src/tui/hooks/index.ts +24 -20
  137. package/src/tui/hooks/useAnimatedValue.ts +13 -18
  138. package/src/tui/hooks/useDashboardKeyboard.ts +118 -98
  139. package/src/tui/hooks/useDashboardState.ts +26 -24
  140. package/src/tui/hooks/useEmaActivity.ts +82 -74
  141. package/src/tui/hooks/useEntranceAnimation.ts +11 -11
  142. package/src/tui/hooks/useExitAnimation.ts +7 -3
  143. package/src/tui/hooks/usePulse.ts +4 -4
  144. package/src/tui/hooks/useSafeRenderer.ts +5 -5
  145. package/src/tui/hooks/useSessionEnrichedProviders.ts +150 -0
  146. package/src/tui/hooks/useValueFlash.ts +10 -14
  147. package/src/tui/index.tsx +29 -6
  148. package/src/tui/shutdown.ts +19 -0
  149. package/src/tui/utils/providerColor.ts +1 -1
  150. package/src/tui/views/Dashboard.tsx +164 -102
  151. package/src/tui/views/HistoricalTrendsView.tsx +579 -250
  152. package/src/tui/views/ProjectsView.tsx +495 -243
  153. package/src/tui/views/RealTimeDashboard.tsx +176 -116
  154. package/src/tui/views/SettingsView.tsx +225 -189
  155. package/src/tui/views/index.ts +2 -2
  156. package/src/types/opentui-test-utils.d.ts +7 -7
  157. package/src/utils/clipboard.ts +29 -29
  158. package/src/utils/currency.test.ts +53 -53
  159. package/src/utils/currency.ts +35 -32
  160. package/src/version.ts +1 -1
@@ -1,17 +1,23 @@
1
- import { useState, useMemo, useEffect } from 'react';
2
- import { useKeyboard, useTerminalDimensions } from '@opentui/react';
3
- import { useColors } from '../contexts/ThemeContext.tsx';
4
- import { useStorageReady } from '../contexts/StorageContext.tsx';
5
- import { useDemoMode } from '../contexts/DemoModeContext.tsx';
6
- import { queryUsageTimeSeries, queryProviderDailyCosts, queryModelDailyCosts, queryProjectDailyCosts, isDatabaseInitialized } from '@/storage/index.ts';
1
+ import { useKeyboard, useTerminalDimensions } from "@opentui/react";
2
+ import { useEffect, useMemo, useState } from "react";
3
+ import {
4
+ isDatabaseInitialized,
5
+ queryModelDailyCosts,
6
+ queryProjectDailyCosts,
7
+ queryProviderDailyCosts,
8
+ queryUsageTimeSeries,
9
+ } from "@/storage/index.ts";
10
+ import { useDemoMode } from "../contexts/DemoModeContext.tsx";
11
+ import { useStorageReady } from "../contexts/StorageContext.tsx";
12
+ import { useColors } from "../contexts/ThemeContext.tsx";
7
13
 
8
14
  // ============================================================================
9
15
  // Types
10
16
  // ============================================================================
11
17
 
12
- type TimePeriod = '7d' | '30d' | '90d';
13
- type MetricType = 'cost' | 'tokens' | 'requests';
14
- type BreakdownDimension = 'provider' | 'model' | 'project' | 'off';
18
+ type TimePeriod = "7d" | "30d" | "90d";
19
+ type MetricType = "cost" | "tokens" | "requests";
20
+ type BreakdownDimension = "provider" | "model" | "project" | "off";
15
21
 
16
22
  interface ChartPoint {
17
23
  label: string;
@@ -46,17 +52,29 @@ interface PeriodSummary {
46
52
  const MS_PER_DAY = 24 * 60 * 60 * 1000;
47
53
 
48
54
  const METRIC_LABELS: Record<MetricType, string> = {
49
- cost: 'COST',
50
- tokens: 'TOKENS',
51
- requests: 'REQUESTS',
55
+ cost: "COST",
56
+ tokens: "TOKENS",
57
+ requests: "REQUESTS",
52
58
  };
53
59
 
54
60
  const CHART_CHARS = {
55
- h: '─', v: '│', tl: '╭', tr: '╮', bl: '╰', br: '╯', cross: '┼', t_l: '┤',
61
+ h: "─",
62
+ v: "│",
63
+ tl: "╭",
64
+ tr: "╮",
65
+ bl: "╰",
66
+ br: "╯",
67
+ cross: "┼",
68
+ t_l: "┤",
56
69
  };
57
70
 
58
71
  const GHOST_CHARS = {
59
- h: '╌', v: '╎', tl: '╭', tr: '╮', bl: '╰', br: '╯',
72
+ h: "╌",
73
+ v: "╎",
74
+ tl: "╭",
75
+ tr: "╮",
76
+ bl: "╰",
77
+ br: "╯",
60
78
  };
61
79
 
62
80
  // ============================================================================
@@ -84,53 +102,63 @@ function fmtNumber(val: number): string {
84
102
 
85
103
  function fmtMetricValue(val: number, metric: MetricType): string {
86
104
  switch (metric) {
87
- case 'cost': return fmtCurrency(val);
88
- case 'tokens': return fmtTokens(val);
89
- case 'requests': return fmtNumber(val);
105
+ case "cost":
106
+ return fmtCurrency(val);
107
+ case "tokens":
108
+ return fmtTokens(val);
109
+ case "requests":
110
+ return fmtNumber(val);
90
111
  }
91
112
  }
92
113
 
93
114
  function fmtYAxisLabel(val: number, metric: MetricType): string {
94
115
  switch (metric) {
95
- case 'cost': return `$${Math.round(val)}`.padStart(5);
96
- case 'tokens': return fmtTokens(val).padStart(5);
97
- case 'requests': return `${Math.round(val)}`.padStart(5);
116
+ case "cost":
117
+ return `$${Math.round(val)}`.padStart(5);
118
+ case "tokens":
119
+ return fmtTokens(val).padStart(5);
120
+ case "requests":
121
+ return `${Math.round(val)}`.padStart(5);
98
122
  }
99
123
  }
100
124
 
101
- function fmtDeltaPct(current: number, previous: number, hasPreviousData = true): { text: string; positive: boolean } {
125
+ function fmtDeltaPct(
126
+ current: number,
127
+ previous: number,
128
+ hasPreviousData = true,
129
+ ): { text: string; positive: boolean } {
102
130
  if (!hasPreviousData) {
103
- return { text: '—', positive: true };
131
+ return { text: "—", positive: true };
104
132
  }
105
133
  if (previous === 0) {
106
- return current > 0 ? { text: '▲ new', positive: false } : { text: '── 0%', positive: true };
134
+ return current > 0 ? { text: "▲ new", positive: false } : { text: "── 0%", positive: true };
107
135
  }
108
136
  const pct = ((current - previous) / previous) * 100;
109
- const sign = pct >= 0 ? '▲' : '▼';
137
+ const sign = pct >= 0 ? "▲" : "▼";
110
138
  return { text: `${sign} ${Math.abs(pct).toFixed(0)}%`, positive: pct <= 0 };
111
139
  }
112
140
 
113
141
  function miniSparkline(values: number[], width: number): string {
114
- if (values.length === 0) return ' '.repeat(width);
142
+ if (values.length === 0) return " ".repeat(width);
115
143
  const max = Math.max(...values, 0.001);
116
- const blocks = '▁▂▃▄▅▆▇█';
144
+ const blocks = "▁▂▃▄▅▆▇█";
117
145
  const step = Math.max(1, values.length / width);
118
- let result = '';
146
+ let result = "";
119
147
  for (let i = 0; i < width; i++) {
120
148
  const idx = Math.min(Math.floor(i * step), values.length - 1);
121
149
  const v = values[idx] ?? 0;
122
150
  const normalized = Math.min(7, Math.floor((v / max) * 8));
123
- result += v <= 0 ? '▁' : (blocks[normalized] ?? '▁');
151
+ result += v <= 0 ? "▁" : (blocks[normalized] ?? "▁");
124
152
  }
125
153
  return result;
126
154
  }
127
155
 
128
156
  function padRight(str: string, len: number): string {
129
- return str.length >= len ? str.slice(0, len) : str + ' '.repeat(len - str.length);
157
+ return str.length >= len ? str.slice(0, len) : str + " ".repeat(len - str.length);
130
158
  }
131
159
 
132
160
  function truncate(str: string, maxLen: number): string {
133
- return str.length <= maxLen ? str : str.slice(0, maxLen - 1) + '…';
161
+ return str.length <= maxLen ? str : str.slice(0, maxLen - 1) + "…";
134
162
  }
135
163
 
136
164
  // ============================================================================
@@ -138,24 +166,27 @@ function truncate(str: string, maxLen: number): string {
138
166
  // ============================================================================
139
167
 
140
168
  function getDaysBack(period: TimePeriod): number {
141
- return period === '30d' ? 30 : period === '90d' ? 90 : 7;
169
+ return period === "30d" ? 30 : period === "90d" ? 90 : 7;
142
170
  }
143
171
 
144
172
  function formatDateLabel(date: Date, period: TimePeriod, index: number): string {
145
- if (period === '7d') return date.toLocaleDateString('en-US', { weekday: 'short' });
146
- if (period === '30d') return index % 5 === 0 ? date.getUTCDate().toString() : '';
147
- return index % 15 === 0 ? `${date.getUTCMonth() + 1}/${date.getUTCDate()}` : '';
173
+ if (period === "7d") return date.toLocaleDateString("en-US", { weekday: "short" });
174
+ if (period === "30d") return index % 5 === 0 ? date.getUTCDate().toString() : "";
175
+ return index % 15 === 0 ? `${date.getUTCMonth() + 1}/${date.getUTCDate()}` : "";
148
176
  }
149
177
 
150
178
  function getDayName(date: Date): string {
151
- return date.toLocaleDateString('en-US', { weekday: 'short' });
179
+ return date.toLocaleDateString("en-US", { weekday: "short" });
152
180
  }
153
181
 
154
182
  function getMetricValue(point: ChartPoint, metric: MetricType): number {
155
183
  switch (metric) {
156
- case 'cost': return point.costUsd;
157
- case 'tokens': return point.tokens;
158
- case 'requests': return point.requestCount;
184
+ case "cost":
185
+ return point.costUsd;
186
+ case "tokens":
187
+ return point.tokens;
188
+ case "requests":
189
+ return point.requestCount;
159
190
  }
160
191
  }
161
192
 
@@ -163,7 +194,8 @@ function computeSummary(points: ChartPoint[], metric: MetricType): PeriodSummary
163
194
  const totalCost = points.reduce((sum, p) => sum + p.costUsd, 0);
164
195
  const totalTokens = points.reduce((sum, p) => sum + p.tokens, 0);
165
196
  const totalRequests = points.reduce((sum, p) => sum + p.requestCount, 0);
166
- const metricTotal = metric === 'tokens' ? totalTokens : metric === 'requests' ? totalRequests : totalCost;
197
+ const metricTotal =
198
+ metric === "tokens" ? totalTokens : metric === "requests" ? totalRequests : totalCost;
167
199
  const avgPerDay = points.length > 0 ? metricTotal / points.length : 0;
168
200
 
169
201
  let peakDay: { label: string; value: number } | null = null;
@@ -231,19 +263,30 @@ function fetchProviderBreakdown(period: TimePeriod): ProviderContribution[] {
231
263
  try {
232
264
  const rows = queryProviderDailyCosts(startMs, now, MS_PER_DAY);
233
265
 
234
- const providerMap = new Map<string, {
235
- cost: number; tokens: number; requests: number;
236
- dailyCosts: Map<number, number>;
237
- }>();
266
+ const providerMap = new Map<
267
+ string,
268
+ {
269
+ cost: number;
270
+ tokens: number;
271
+ requests: number;
272
+ dailyCosts: Map<number, number>;
273
+ }
274
+ >();
238
275
 
239
276
  for (const row of rows) {
240
277
  const entry = providerMap.get(row.provider) ?? {
241
- cost: 0, tokens: 0, requests: 0, dailyCosts: new Map(),
278
+ cost: 0,
279
+ tokens: 0,
280
+ requests: 0,
281
+ dailyCosts: new Map(),
242
282
  };
243
283
  entry.cost += row.costUsd;
244
284
  entry.tokens += row.tokens;
245
285
  entry.requests += row.requestCount;
246
- entry.dailyCosts.set(row.bucketStart, (entry.dailyCosts.get(row.bucketStart) ?? 0) + row.costUsd);
286
+ entry.dailyCosts.set(
287
+ row.bucketStart,
288
+ (entry.dailyCosts.get(row.bucketStart) ?? 0) + row.costUsd,
289
+ );
247
290
  providerMap.set(row.provider, entry);
248
291
  }
249
292
 
@@ -283,19 +326,30 @@ function fetchModelBreakdown(period: TimePeriod): ProviderContribution[] {
283
326
  try {
284
327
  const rows = queryModelDailyCosts(startMs, now, MS_PER_DAY);
285
328
 
286
- const modelMap = new Map<string, {
287
- cost: number; tokens: number; requests: number;
288
- dailyCosts: Map<number, number>;
289
- }>();
329
+ const modelMap = new Map<
330
+ string,
331
+ {
332
+ cost: number;
333
+ tokens: number;
334
+ requests: number;
335
+ dailyCosts: Map<number, number>;
336
+ }
337
+ >();
290
338
 
291
339
  for (const row of rows) {
292
340
  const entry = modelMap.get(row.model) ?? {
293
- cost: 0, tokens: 0, requests: 0, dailyCosts: new Map(),
341
+ cost: 0,
342
+ tokens: 0,
343
+ requests: 0,
344
+ dailyCosts: new Map(),
294
345
  };
295
346
  entry.cost += row.costUsd;
296
347
  entry.tokens += row.tokens;
297
348
  entry.requests += row.requestCount;
298
- entry.dailyCosts.set(row.bucketStart, (entry.dailyCosts.get(row.bucketStart) ?? 0) + row.costUsd);
349
+ entry.dailyCosts.set(
350
+ row.bucketStart,
351
+ (entry.dailyCosts.get(row.bucketStart) ?? 0) + row.costUsd,
352
+ );
299
353
  modelMap.set(row.model, entry);
300
354
  }
301
355
 
@@ -335,20 +389,31 @@ function fetchProjectBreakdown(period: TimePeriod): ProviderContribution[] {
335
389
  try {
336
390
  const rows = queryProjectDailyCosts(startMs, now, MS_PER_DAY);
337
391
 
338
- const projectMap = new Map<string, {
339
- cost: number; tokens: number; requests: number;
340
- dailyCosts: Map<number, number>;
341
- }>();
392
+ const projectMap = new Map<
393
+ string,
394
+ {
395
+ cost: number;
396
+ tokens: number;
397
+ requests: number;
398
+ dailyCosts: Map<number, number>;
399
+ }
400
+ >();
342
401
 
343
402
  for (const row of rows) {
344
- const displayName = row.projectPath.split('/').pop() ?? row.projectPath;
403
+ const displayName = row.projectPath.split("/").pop() ?? row.projectPath;
345
404
  const entry = projectMap.get(displayName) ?? {
346
- cost: 0, tokens: 0, requests: 0, dailyCosts: new Map(),
405
+ cost: 0,
406
+ tokens: 0,
407
+ requests: 0,
408
+ dailyCosts: new Map(),
347
409
  };
348
410
  entry.cost += row.costUsd;
349
411
  entry.tokens += row.tokens;
350
412
  entry.requests += row.requestCount;
351
- entry.dailyCosts.set(row.bucketStart, (entry.dailyCosts.get(row.bucketStart) ?? 0) + row.costUsd);
413
+ entry.dailyCosts.set(
414
+ row.bucketStart,
415
+ (entry.dailyCosts.get(row.bucketStart) ?? 0) + row.costUsd,
416
+ );
352
417
  projectMap.set(displayName, entry);
353
418
  }
354
419
 
@@ -383,19 +448,16 @@ function fetchProjectBreakdown(period: TimePeriod): ProviderContribution[] {
383
448
  // ============================================================================
384
449
 
385
450
  function fetchDemoTimeSeries(
386
- simulator: NonNullable<ReturnType<typeof useDemoMode>['simulator']>,
451
+ simulator: NonNullable<ReturnType<typeof useDemoMode>["simulator"]>,
387
452
  period: TimePeriod,
388
- offsetPeriods = 0
453
+ offsetPeriods = 0,
389
454
  ): ChartPoint[] {
390
455
  const daysBack = getDaysBack(period);
391
456
  const totalDays = daysBack * (1 + offsetPeriods);
392
457
  const historicalData = simulator.generateHistoricalCostData(totalDays);
393
458
 
394
459
  const sliceEnd = historicalData.length - offsetPeriods * daysBack;
395
- const periodData = historicalData.slice(
396
- Math.max(0, sliceEnd - daysBack),
397
- sliceEnd
398
- );
460
+ const periodData = historicalData.slice(Math.max(0, sliceEnd - daysBack), sliceEnd);
399
461
 
400
462
  return periodData.map((item, index) => {
401
463
  const d = new Date(item.date);
@@ -412,15 +474,20 @@ function fetchDemoTimeSeries(
412
474
  }
413
475
 
414
476
  function fetchDemoProviderBreakdown(
415
- simulator: NonNullable<ReturnType<typeof useDemoMode>['simulator']>,
416
- period: TimePeriod
477
+ simulator: NonNullable<ReturnType<typeof useDemoMode>["simulator"]>,
478
+ period: TimePeriod,
417
479
  ): ProviderContribution[] {
418
480
  const daysBack = getDaysBack(period);
419
481
  const byProvider = simulator.generateHistoricalCostDataByProvider(daysBack);
420
482
 
421
- const providerMap = new Map<string, {
422
- cost: number; tokens: number; requests: number;
423
- }>();
483
+ const providerMap = new Map<
484
+ string,
485
+ {
486
+ cost: number;
487
+ tokens: number;
488
+ requests: number;
489
+ }
490
+ >();
424
491
  const providerDayMap = new Map<string, Map<number, number>>();
425
492
 
426
493
  for (const entry of byProvider) {
@@ -430,7 +497,9 @@ function fetchDemoProviderBreakdown(
430
497
  providerDayMap.get(entry.provider)!.set(entry.date, entry.cost);
431
498
 
432
499
  const existing = providerMap.get(entry.provider) ?? {
433
- cost: 0, tokens: 0, requests: 0,
500
+ cost: 0,
501
+ tokens: 0,
502
+ requests: 0,
434
503
  };
435
504
  existing.cost += entry.cost;
436
505
  existing.tokens += entry.tokens;
@@ -439,12 +508,12 @@ function fetchDemoProviderBreakdown(
439
508
  }
440
509
 
441
510
  const totalCost = Array.from(providerMap.values()).reduce((sum, p) => sum + p.cost, 0);
442
- const allDates = [...new Set(byProvider.map(e => e.date))].sort((a, b) => a - b);
511
+ const allDates = [...new Set(byProvider.map((e) => e.date))].sort((a, b) => a - b);
443
512
 
444
513
  return Array.from(providerMap.entries())
445
514
  .map(([provider, d]) => {
446
515
  const dayMap = providerDayMap.get(provider) ?? new Map();
447
- const dailyCosts = allDates.map(date => dayMap.get(date) ?? 0);
516
+ const dailyCosts = allDates.map((date) => dayMap.get(date) ?? 0);
448
517
  return {
449
518
  provider,
450
519
  cost: d.cost,
@@ -459,8 +528,8 @@ function fetchDemoProviderBreakdown(
459
528
  }
460
529
 
461
530
  function fetchDemoModelBreakdown(
462
- simulator: NonNullable<ReturnType<typeof useDemoMode>['simulator']>,
463
- period: TimePeriod
531
+ simulator: NonNullable<ReturnType<typeof useDemoMode>["simulator"]>,
532
+ period: TimePeriod,
464
533
  ): ProviderContribution[] {
465
534
  const daysBack = getDaysBack(period);
466
535
  const byModel = simulator.generateHistoricalCostDataByModel(daysBack);
@@ -480,12 +549,12 @@ function fetchDemoModelBreakdown(
480
549
  }
481
550
 
482
551
  const totalCost = Array.from(modelMap.values()).reduce((sum, p) => sum + p.cost, 0);
483
- const allDates = [...new Set(byModel.map(e => e.date))].sort((a, b) => a - b);
552
+ const allDates = [...new Set(byModel.map((e) => e.date))].sort((a, b) => a - b);
484
553
 
485
554
  return Array.from(modelMap.entries())
486
555
  .map(([model, d]) => {
487
556
  const dayMap = modelDayMap.get(model) ?? new Map();
488
- const dailyCosts = allDates.map(date => dayMap.get(date) ?? 0);
557
+ const dailyCosts = allDates.map((date) => dayMap.get(date) ?? 0);
489
558
  return {
490
559
  provider: model,
491
560
  cost: d.cost,
@@ -500,8 +569,8 @@ function fetchDemoModelBreakdown(
500
569
  }
501
570
 
502
571
  function fetchDemoProjectBreakdown(
503
- simulator: NonNullable<ReturnType<typeof useDemoMode>['simulator']>,
504
- period: TimePeriod
572
+ simulator: NonNullable<ReturnType<typeof useDemoMode>["simulator"]>,
573
+ period: TimePeriod,
505
574
  ): ProviderContribution[] {
506
575
  const daysBack = getDaysBack(period);
507
576
  const byProject = simulator.generateHistoricalCostDataByProject(daysBack);
@@ -510,9 +579,11 @@ function fetchDemoProjectBreakdown(
510
579
  const projectDayMap = new Map<string, Map<number, number>>();
511
580
 
512
581
  for (const entry of byProject) {
513
- const displayName = entry.projectPath.split('/').pop() ?? entry.projectPath;
582
+ const displayName = entry.projectPath.split("/").pop() ?? entry.projectPath;
514
583
  if (!projectDayMap.has(displayName)) projectDayMap.set(displayName, new Map());
515
- projectDayMap.get(displayName)!.set(entry.date, (projectDayMap.get(displayName)!.get(entry.date) ?? 0) + entry.cost);
584
+ projectDayMap
585
+ .get(displayName)!
586
+ .set(entry.date, (projectDayMap.get(displayName)!.get(entry.date) ?? 0) + entry.cost);
516
587
 
517
588
  const existing = projectMap.get(displayName) ?? { cost: 0, tokens: 0, requests: 0 };
518
589
  existing.cost += entry.cost;
@@ -522,12 +593,12 @@ function fetchDemoProjectBreakdown(
522
593
  }
523
594
 
524
595
  const totalCost = Array.from(projectMap.values()).reduce((sum, p) => sum + p.cost, 0);
525
- const allDates = [...new Set(byProject.map(e => e.date))].sort((a, b) => a - b);
596
+ const allDates = [...new Set(byProject.map((e) => e.date))].sort((a, b) => a - b);
526
597
 
527
598
  return Array.from(projectMap.entries())
528
599
  .map(([project, d]) => {
529
600
  const dayMap = projectDayMap.get(project) ?? new Map();
530
- const dailyCosts = allDates.map(date => dayMap.get(date) ?? 0);
601
+ const dailyCosts = allDates.map((date) => dayMap.get(date) ?? 0);
531
602
  return {
532
603
  provider: project,
533
604
  cost: d.cost,
@@ -542,8 +613,8 @@ function fetchDemoProjectBreakdown(
542
613
  }
543
614
 
544
615
  function fetchDemoPrevContributors(
545
- simulator: NonNullable<ReturnType<typeof useDemoMode>['simulator']>,
546
- period: TimePeriod
616
+ simulator: NonNullable<ReturnType<typeof useDemoMode>["simulator"]>,
617
+ period: TimePeriod,
547
618
  ): ProviderContribution[] {
548
619
  const daysBack = getDaysBack(period);
549
620
  const allByProvider = simulator.generateHistoricalCostDataByProvider(daysBack * 2);
@@ -572,7 +643,7 @@ function fetchDemoPrevContributors(
572
643
  // AsciiChart component (enhanced with comparison + cursor)
573
644
  // ============================================================================
574
645
 
575
- type CellType = 'empty' | 'main' | 'ghost' | 'cursor';
646
+ type CellType = "empty" | "main" | "ghost" | "cursor";
576
647
 
577
648
  interface AsciiChartProps {
578
649
  data: ChartPoint[];
@@ -589,35 +660,53 @@ interface AsciiChartProps {
589
660
  }
590
661
 
591
662
  const AsciiChart = ({
592
- data, comparisonData, metric, cursorIndex,
593
- height, width, color, ghostColor, cursorColor, labelColor, gridColor,
663
+ data,
664
+ comparisonData,
665
+ metric,
666
+ cursorIndex,
667
+ height,
668
+ width,
669
+ color,
670
+ ghostColor,
671
+ cursorColor,
672
+ labelColor,
673
+ gridColor,
594
674
  }: AsciiChartProps) => {
595
675
  const chartHeight = height - 2;
596
676
  const yAxisWidth = 6;
597
677
  const chartWidth = width - yAxisWidth;
598
678
 
599
679
  if (data.length < 2 || chartHeight < 3 || chartWidth < 10) {
600
- return <box><text fg={labelColor}>Chart too small</text></box>;
680
+ return (
681
+ <box>
682
+ <text fg={labelColor}>Chart too small</text>
683
+ </box>
684
+ );
601
685
  }
602
686
 
603
- const values = data.map(d => getMetricValue(d, metric));
604
- const compValues = comparisonData?.map(d => getMetricValue(d, metric)) ?? [];
687
+ const values = data.map((d) => getMetricValue(d, metric));
688
+ const compValues = comparisonData?.map((d) => getMetricValue(d, metric)) ?? [];
605
689
  const allValues = [...values, ...compValues];
606
- const maxVal = Math.max(...allValues, metric === 'cost' ? 10 : 100) * 1.1;
690
+ const maxVal = Math.max(...allValues, metric === "cost" ? 10 : 100) * 1.1;
607
691
  const minVal = 0;
608
692
 
609
693
  const normalize = (v: number) =>
610
- Math.min(chartHeight - 1, Math.max(0, Math.floor(((v - minVal) / (maxVal - minVal)) * chartHeight)));
694
+ Math.min(
695
+ chartHeight - 1,
696
+ Math.max(0, Math.floor(((v - minVal) / (maxVal - minVal)) * chartHeight)),
697
+ );
611
698
 
612
- const gridChars: string[][] = Array.from({ length: chartHeight }, () => Array(chartWidth).fill(' '));
699
+ const gridChars: string[][] = Array.from({ length: chartHeight }, () =>
700
+ Array(chartWidth).fill(" "),
701
+ );
613
702
  const gridTypes: CellType[][] = Array.from({ length: chartHeight }, () =>
614
- Array<CellType>(chartWidth).fill('empty')
703
+ Array<CellType>(chartWidth).fill("empty"),
615
704
  );
616
705
 
617
706
  const plotLine = (
618
707
  vals: number[],
619
708
  chars: { h: string; v: string; tl: string; tr: string; bl: string; br: string },
620
- cellType: CellType
709
+ cellType: CellType,
621
710
  ) => {
622
711
  if (vals.length < 2) return;
623
712
  const normalized = vals.map(normalize);
@@ -639,23 +728,47 @@ const AsciiChart = ({
639
728
  }
640
729
 
641
730
  if (y2 > y1) {
642
- if (xMid < chartWidth && gridChars[y1]) { gridChars[y1]![xMid] = chars.br; gridTypes[y1]![xMid] = cellType; }
731
+ if (xMid < chartWidth && gridChars[y1]) {
732
+ gridChars[y1]![xMid] = chars.br;
733
+ gridTypes[y1]![xMid] = cellType;
734
+ }
643
735
  for (let y = y1 + 1; y < y2; y++) {
644
- if (xMid < chartWidth && gridChars[y]) { gridChars[y]![xMid] = chars.v; gridTypes[y]![xMid] = cellType; }
736
+ if (xMid < chartWidth && gridChars[y]) {
737
+ gridChars[y]![xMid] = chars.v;
738
+ gridTypes[y]![xMid] = cellType;
739
+ }
740
+ }
741
+ if (xMid < chartWidth && gridChars[y2]) {
742
+ gridChars[y2]![xMid] = chars.tl;
743
+ gridTypes[y2]![xMid] = cellType;
645
744
  }
646
- if (xMid < chartWidth && gridChars[y2]) { gridChars[y2]![xMid] = chars.tl; gridTypes[y2]![xMid] = cellType; }
647
745
  } else if (y2 < y1) {
648
- if (xMid < chartWidth && gridChars[y1]) { gridChars[y1]![xMid] = chars.tr; gridTypes[y1]![xMid] = cellType; }
746
+ if (xMid < chartWidth && gridChars[y1]) {
747
+ gridChars[y1]![xMid] = chars.tr;
748
+ gridTypes[y1]![xMid] = cellType;
749
+ }
649
750
  for (let y = y1 - 1; y > y2; y--) {
650
- if (xMid < chartWidth && gridChars[y]) { gridChars[y]![xMid] = chars.v; gridTypes[y]![xMid] = cellType; }
751
+ if (xMid < chartWidth && gridChars[y]) {
752
+ gridChars[y]![xMid] = chars.v;
753
+ gridTypes[y]![xMid] = cellType;
754
+ }
755
+ }
756
+ if (xMid < chartWidth && gridChars[y2]) {
757
+ gridChars[y2]![xMid] = chars.bl;
758
+ gridTypes[y2]![xMid] = cellType;
651
759
  }
652
- if (xMid < chartWidth && gridChars[y2]) { gridChars[y2]![xMid] = chars.bl; gridTypes[y2]![xMid] = cellType; }
653
760
  } else {
654
- if (xMid < chartWidth && gridChars[y1]) { gridChars[y1]![xMid] = chars.h; gridTypes[y1]![xMid] = cellType; }
761
+ if (xMid < chartWidth && gridChars[y1]) {
762
+ gridChars[y1]![xMid] = chars.h;
763
+ gridTypes[y1]![xMid] = cellType;
764
+ }
655
765
  }
656
766
 
657
767
  for (let x = xMid + 1; x < xEnd; x++) {
658
- if (x < chartWidth && gridChars[y2]) { gridChars[y2]![x] = chars.h; gridTypes[y2]![x] = cellType; }
768
+ if (x < chartWidth && gridChars[y2]) {
769
+ gridChars[y2]![x] = chars.h;
770
+ gridTypes[y2]![x] = cellType;
771
+ }
659
772
  }
660
773
 
661
774
  currentX += stepsPerPoint;
@@ -671,9 +784,9 @@ const AsciiChart = ({
671
784
 
672
785
  // Plot ghost first (comparison), then main on top
673
786
  if (compValues.length >= 2) {
674
- plotLine(compValues, GHOST_CHARS, 'ghost');
787
+ plotLine(compValues, GHOST_CHARS, "ghost");
675
788
  }
676
- plotLine(values, CHART_CHARS, 'main');
789
+ plotLine(values, CHART_CHARS, "main");
677
790
 
678
791
  // Cursor overlay — final pass, draws on top of chart data
679
792
  let cursorCol: number | null = null;
@@ -683,12 +796,12 @@ const AsciiChart = ({
683
796
  if (cursorCol >= 0 && cursorCol < chartWidth) {
684
797
  for (let y = 0; y < chartHeight; y++) {
685
798
  const cellType = gridTypes[y]?.[cursorCol];
686
- if (cellType === 'empty') {
687
- gridChars[y]![cursorCol] = '┆';
688
- } else if (cellType === 'main' || cellType === 'ghost') {
689
- gridChars[y]![cursorCol] = '╋';
799
+ if (cellType === "empty") {
800
+ gridChars[y]![cursorCol] = "┆";
801
+ } else if (cellType === "main" || cellType === "ghost") {
802
+ gridChars[y]![cursorCol] = "╋";
690
803
  }
691
- gridTypes[y]![cursorCol] = 'cursor';
804
+ gridTypes[y]![cursorCol] = "cursor";
692
805
  }
693
806
  }
694
807
  }
@@ -699,25 +812,35 @@ const AsciiChart = ({
699
812
  const rowVal = minVal + (r / (chartHeight - 1)) * (maxVal - minVal);
700
813
  const labelCount = Math.max(3, Math.min(7, Math.floor(chartHeight / 5) + 1));
701
814
  const labelStep = (chartHeight - 1) / (labelCount - 1);
702
- const showLabel = Array.from({ length: labelCount }, (_, i) => Math.round(i * labelStep)).includes(r);
703
- const label = showLabel ? fmtYAxisLabel(rowVal, metric) : ' ';
815
+ const showLabel = Array.from({ length: labelCount }, (_, i) =>
816
+ Math.round(i * labelStep),
817
+ ).includes(r);
818
+ const label = showLabel ? fmtYAxisLabel(rowVal, metric) : " ";
704
819
  const sep = r === 0 ? CHART_CHARS.cross : CHART_CHARS.t_l;
705
820
 
706
821
  rows.push(
707
822
  <box key={`row-${r}`} flexDirection="row" height={1}>
708
- <text width={5} height={1} fg={labelColor}>{label}</text>
709
- <text width={1} height={1} fg={gridColor}>{sep}</text>
823
+ <text width={5} height={1} fg={labelColor}>
824
+ {label}
825
+ </text>
826
+ <text width={1} height={1} fg={gridColor}>
827
+ {sep}
828
+ </text>
710
829
  <text flexGrow={1} height={1}>
711
830
  {(gridChars[r] ?? []).map((char, cx) => {
712
- const type = gridTypes[r]?.[cx] ?? 'empty';
831
+ const type = gridTypes[r]?.[cx] ?? "empty";
713
832
  let fg: string | undefined;
714
- if (type === 'main') fg = color;
715
- else if (type === 'ghost') fg = ghostColor;
716
- else if (type === 'cursor') fg = cursorColor;
717
- return <span key={cx} {...(fg ? { fg } : {})}>{char}</span>;
833
+ if (type === "main") fg = color;
834
+ else if (type === "ghost") fg = ghostColor;
835
+ else if (type === "cursor") fg = cursorColor;
836
+ return (
837
+ <span key={cx} {...(fg ? { fg } : {})}>
838
+ {char}
839
+ </span>
840
+ );
718
841
  })}
719
842
  </text>
720
- </box>
843
+ </box>,
721
844
  );
722
845
  }
723
846
 
@@ -730,22 +853,31 @@ const AsciiChart = ({
730
853
  const padLen = Math.max(0, cursorCol + yAxisWidth - 1);
731
854
  cursorIndicator = (
732
855
  <box height={1}>
733
- <text height={1} fg={cursorColor}>{' '.repeat(padLen)}{indicatorText}</text>
856
+ <text height={1} fg={cursorColor}>
857
+ {" ".repeat(padLen)}
858
+ {indicatorText}
859
+ </text>
734
860
  </box>
735
861
  );
736
862
  }
737
863
  }
738
864
 
739
865
  // X-axis labels
740
- const labelParts = data.map((d, i) => {
741
- if (!d.label) return '';
742
- if (cursorIndex === i) return `[${d.label}]`;
743
- return d.label;
744
- }).filter(l => l).join(' ').slice(0, chartWidth);
866
+ const labelParts = data
867
+ .map((d, i) => {
868
+ if (!d.label) return "";
869
+ if (cursorIndex === i) return `[${d.label}]`;
870
+ return d.label;
871
+ })
872
+ .filter((l) => l)
873
+ .join(" ")
874
+ .slice(0, chartWidth);
745
875
 
746
876
  const labelRow = (
747
877
  <box flexDirection="row" height={1} paddingLeft={yAxisWidth}>
748
- <text height={1} fg={labelColor}>{labelParts}</text>
878
+ <text height={1} fg={labelColor}>
879
+ {labelParts}
880
+ </text>
749
881
  </box>
750
882
  );
751
883
 
@@ -767,34 +899,69 @@ function PeriodSummarySection({ summary, metric }: { summary: PeriodSummary; met
767
899
 
768
900
  return (
769
901
  <box flexDirection="column" paddingX={1}>
770
- <text fg={colors.textMuted} height={1}><strong>PERIOD SUMMARY</strong></text>
902
+ <text fg={colors.textMuted} height={1}>
903
+ <strong>PERIOD SUMMARY</strong>
904
+ </text>
771
905
  <box flexDirection="row" justifyContent="space-between" height={1}>
772
- <text fg={colors.textMuted} height={1}>Total</text>
773
- <text fg={colors.success} height={1}><strong>{fmtMetricValue(metric === 'cost' ? summary.totalCost : metric === 'tokens' ? summary.totalTokens : summary.totalRequests, metric)}</strong></text>
906
+ <text fg={colors.textMuted} height={1}>
907
+ Total
908
+ </text>
909
+ <text fg={colors.success} height={1}>
910
+ <strong>
911
+ {fmtMetricValue(
912
+ metric === "cost"
913
+ ? summary.totalCost
914
+ : metric === "tokens"
915
+ ? summary.totalTokens
916
+ : summary.totalRequests,
917
+ metric,
918
+ )}
919
+ </strong>
920
+ </text>
774
921
  </box>
775
922
  <box flexDirection="row" justifyContent="space-between" height={1}>
776
- <text fg={colors.textMuted} height={1}>Avg/day</text>
777
- <text fg={colors.text} height={1}>{fmtMetricValue(summary.avgPerDay, metric)}</text>
923
+ <text fg={colors.textMuted} height={1}>
924
+ Avg/day
925
+ </text>
926
+ <text fg={colors.text} height={1}>
927
+ {fmtMetricValue(summary.avgPerDay, metric)}
928
+ </text>
778
929
  </box>
779
930
  {summary.peakDay && (
780
931
  <box flexDirection="row" justifyContent="space-between" height={1}>
781
- <text fg={colors.textMuted} height={1}>Peak</text>
782
- <text fg={colors.warning} height={1}>{summary.peakDay.label} {fmtMetricValue(summary.peakDay.value, metric)}</text>
932
+ <text fg={colors.textMuted} height={1}>
933
+ Peak
934
+ </text>
935
+ <text fg={colors.warning} height={1}>
936
+ {summary.peakDay.label} {fmtMetricValue(summary.peakDay.value, metric)}
937
+ </text>
783
938
  </box>
784
939
  )}
785
940
  {summary.lowDay && (
786
941
  <box flexDirection="row" justifyContent="space-between" height={1}>
787
- <text fg={colors.textMuted} height={1}>Low</text>
788
- <text fg={colors.info} height={1}>{summary.lowDay.label} {fmtMetricValue(summary.lowDay.value, metric)}</text>
942
+ <text fg={colors.textMuted} height={1}>
943
+ Low
944
+ </text>
945
+ <text fg={colors.info} height={1}>
946
+ {summary.lowDay.label} {fmtMetricValue(summary.lowDay.value, metric)}
947
+ </text>
789
948
  </box>
790
949
  )}
791
950
  <box flexDirection="row" justifyContent="space-between" height={1}>
792
- <text fg={colors.textMuted} height={1}>Requests</text>
793
- <text fg={colors.text} height={1}>{fmtNumber(summary.totalRequests)}</text>
951
+ <text fg={colors.textMuted} height={1}>
952
+ Requests
953
+ </text>
954
+ <text fg={colors.text} height={1}>
955
+ {fmtNumber(summary.totalRequests)}
956
+ </text>
794
957
  </box>
795
958
  <box flexDirection="row" justifyContent="space-between" height={1}>
796
- <text fg={colors.textMuted} height={1}>Tokens</text>
797
- <text fg={colors.text} height={1}>{fmtTokens(summary.totalTokens)}</text>
959
+ <text fg={colors.textMuted} height={1}>
960
+ Tokens
961
+ </text>
962
+ <text fg={colors.text} height={1}>
963
+ {fmtTokens(summary.totalTokens)}
964
+ </text>
798
965
  </box>
799
966
  </box>
800
967
  );
@@ -804,7 +971,15 @@ function PeriodSummarySection({ summary, metric }: { summary: PeriodSummary; met
804
971
  // InsightPanel — vs Previous Period
805
972
  // ============================================================================
806
973
 
807
- function DeltaSection({ current, previous, hasPrevData }: { current: PeriodSummary; previous: PeriodSummary; hasPrevData: boolean }) {
974
+ function DeltaSection({
975
+ current,
976
+ previous,
977
+ hasPrevData,
978
+ }: {
979
+ current: PeriodSummary;
980
+ previous: PeriodSummary;
981
+ hasPrevData: boolean;
982
+ }) {
808
983
  const colors = useColors();
809
984
  const costDelta = fmtDeltaPct(current.totalCost, previous.totalCost, hasPrevData);
810
985
  const tokensDelta = fmtDeltaPct(current.totalTokens, previous.totalTokens, hasPrevData);
@@ -812,18 +987,32 @@ function DeltaSection({ current, previous, hasPrevData }: { current: PeriodSumma
812
987
 
813
988
  return (
814
989
  <box flexDirection="column" paddingX={1}>
815
- <text fg={colors.textMuted} height={1}><strong>vs PREVIOUS</strong></text>
990
+ <text fg={colors.textMuted} height={1}>
991
+ <strong>vs PREVIOUS</strong>
992
+ </text>
816
993
  <box flexDirection="row" justifyContent="space-between" height={1}>
817
- <text fg={colors.textMuted} height={1}>Cost</text>
818
- <text fg={costDelta.positive ? colors.success : colors.error} height={1}>{costDelta.text}</text>
994
+ <text fg={colors.textMuted} height={1}>
995
+ Cost
996
+ </text>
997
+ <text fg={costDelta.positive ? colors.success : colors.error} height={1}>
998
+ {costDelta.text}
999
+ </text>
819
1000
  </box>
820
1001
  <box flexDirection="row" justifyContent="space-between" height={1}>
821
- <text fg={colors.textMuted} height={1}>Tokens</text>
822
- <text fg={tokensDelta.positive ? colors.success : colors.error} height={1}>{tokensDelta.text}</text>
1002
+ <text fg={colors.textMuted} height={1}>
1003
+ Tokens
1004
+ </text>
1005
+ <text fg={tokensDelta.positive ? colors.success : colors.error} height={1}>
1006
+ {tokensDelta.text}
1007
+ </text>
823
1008
  </box>
824
1009
  <box flexDirection="row" justifyContent="space-between" height={1}>
825
- <text fg={colors.textMuted} height={1}>Requests</text>
826
- <text fg={reqDelta.positive ? colors.success : colors.error} height={1}>{reqDelta.text}</text>
1010
+ <text fg={colors.textMuted} height={1}>
1011
+ Requests
1012
+ </text>
1013
+ <text fg={reqDelta.positive ? colors.success : colors.error} height={1}>
1014
+ {reqDelta.text}
1015
+ </text>
827
1016
  </box>
828
1017
  </box>
829
1018
  );
@@ -834,7 +1023,10 @@ function DeltaSection({ current, previous, hasPrevData }: { current: PeriodSumma
834
1023
  // ============================================================================
835
1024
 
836
1025
  function ComparisonTable({
837
- current, previous, currentContributors, previousContributors,
1026
+ current,
1027
+ previous,
1028
+ currentContributors,
1029
+ previousContributors,
838
1030
  }: {
839
1031
  current: PeriodSummary;
840
1032
  previous: PeriodSummary;
@@ -847,7 +1039,7 @@ function ComparisonTable({
847
1039
 
848
1040
  const providerDeltas: Array<{ name: string; delta: ReturnType<typeof fmtDeltaPct> }> = [];
849
1041
  for (const curr of currentContributors) {
850
- const prev = previousContributors.find(p => p.provider === curr.provider);
1042
+ const prev = previousContributors.find((p) => p.provider === curr.provider);
851
1043
  providerDeltas.push({
852
1044
  name: curr.provider,
853
1045
  delta: fmtDeltaPct(curr.cost, prev?.cost ?? 0),
@@ -856,46 +1048,84 @@ function ComparisonTable({
856
1048
 
857
1049
  return (
858
1050
  <box flexDirection="column" paddingX={1}>
859
- <text fg={colors.textMuted} height={1}><strong>COMPARISON</strong></text>
1051
+ <text fg={colors.textMuted} height={1}>
1052
+ <strong>COMPARISON</strong>
1053
+ </text>
860
1054
  <box flexDirection="row" height={1}>
861
- <text fg={colors.textMuted} width={10} height={1}>{padRight('', 10)}</text>
862
- <text fg={colors.textSubtle} width={8} height={1}>{'This'.padStart(8)}</text>
863
- <text fg={colors.textSubtle} width={8} height={1}>{'Prev'.padStart(8)}</text>
1055
+ <text fg={colors.textMuted} width={10} height={1}>
1056
+ {padRight("", 10)}
1057
+ </text>
1058
+ <text fg={colors.textSubtle} width={8} height={1}>
1059
+ {"This".padStart(8)}
1060
+ </text>
1061
+ <text fg={colors.textSubtle} width={8} height={1}>
1062
+ {"Prev".padStart(8)}
1063
+ </text>
864
1064
  </box>
865
1065
  <box flexDirection="row" height={1}>
866
- <text fg={colors.textMuted} width={10} height={1}>{padRight('Cost', 10)}</text>
867
- <text fg={colors.text} width={8} height={1}>{fmtCurrency(current.totalCost).padStart(8)}</text>
868
- <text fg={colors.textSubtle} width={8} height={1}>{fmtCurrency(previous.totalCost).padStart(8)}</text>
1066
+ <text fg={colors.textMuted} width={10} height={1}>
1067
+ {padRight("Cost", 10)}
1068
+ </text>
1069
+ <text fg={colors.text} width={8} height={1}>
1070
+ {fmtCurrency(current.totalCost).padStart(8)}
1071
+ </text>
1072
+ <text fg={colors.textSubtle} width={8} height={1}>
1073
+ {fmtCurrency(previous.totalCost).padStart(8)}
1074
+ </text>
869
1075
  </box>
870
1076
  <box flexDirection="row" height={1}>
871
- <text fg={colors.textMuted} width={10} height={1}>{padRight('Tokens', 10)}</text>
872
- <text fg={colors.text} width={8} height={1}>{fmtTokens(current.totalTokens).padStart(8)}</text>
873
- <text fg={colors.textSubtle} width={8} height={1}>{fmtTokens(previous.totalTokens).padStart(8)}</text>
1077
+ <text fg={colors.textMuted} width={10} height={1}>
1078
+ {padRight("Tokens", 10)}
1079
+ </text>
1080
+ <text fg={colors.text} width={8} height={1}>
1081
+ {fmtTokens(current.totalTokens).padStart(8)}
1082
+ </text>
1083
+ <text fg={colors.textSubtle} width={8} height={1}>
1084
+ {fmtTokens(previous.totalTokens).padStart(8)}
1085
+ </text>
874
1086
  </box>
875
1087
 
876
1088
  <box paddingX={0} height={1}>
877
- <text fg={colors.border} height={1}>{'─'.repeat(26)}</text>
1089
+ <text fg={colors.border} height={1}>
1090
+ {"─".repeat(26)}
1091
+ </text>
878
1092
  </box>
879
1093
 
880
1094
  <box flexDirection="row" justifyContent="space-between" height={1}>
881
- <text fg={colors.textMuted} height={1}>Cost Δ</text>
882
- <text fg={costDelta.positive ? colors.success : colors.error} height={1}>{costDelta.text}</text>
1095
+ <text fg={colors.textMuted} height={1}>
1096
+ Cost Δ
1097
+ </text>
1098
+ <text fg={costDelta.positive ? colors.success : colors.error} height={1}>
1099
+ {costDelta.text}
1100
+ </text>
883
1101
  </box>
884
1102
  <box flexDirection="row" justifyContent="space-between" height={1}>
885
- <text fg={colors.textMuted} height={1}>Tokens Δ</text>
886
- <text fg={tokensDelta.positive ? colors.success : colors.error} height={1}>{tokensDelta.text}</text>
1103
+ <text fg={colors.textMuted} height={1}>
1104
+ Tokens Δ
1105
+ </text>
1106
+ <text fg={tokensDelta.positive ? colors.success : colors.error} height={1}>
1107
+ {tokensDelta.text}
1108
+ </text>
887
1109
  </box>
888
1110
 
889
1111
  {providerDeltas.length > 0 && (
890
1112
  <box flexDirection="column">
891
1113
  <box paddingX={0} height={1}>
892
- <text fg={colors.border} height={1}>{'─'.repeat(26)}</text>
1114
+ <text fg={colors.border} height={1}>
1115
+ {"─".repeat(26)}
1116
+ </text>
893
1117
  </box>
894
- <text fg={colors.textMuted} height={1}><strong>BIGGEST CHANGES</strong></text>
895
- {providerDeltas.slice(0, 3).map(pd => (
1118
+ <text fg={colors.textMuted} height={1}>
1119
+ <strong>BIGGEST CHANGES</strong>
1120
+ </text>
1121
+ {providerDeltas.slice(0, 3).map((pd) => (
896
1122
  <box key={pd.name} flexDirection="row" justifyContent="space-between" height={1}>
897
- <text fg={colors.text} height={1}>{truncate(pd.name, 12)}</text>
898
- <text fg={pd.delta.positive ? colors.success : colors.error} height={1}>{pd.delta.text}</text>
1123
+ <text fg={colors.text} height={1}>
1124
+ {truncate(pd.name, 12)}
1125
+ </text>
1126
+ <text fg={pd.delta.positive ? colors.success : colors.error} height={1}>
1127
+ {pd.delta.text}
1128
+ </text>
899
1129
  </box>
900
1130
  ))}
901
1131
  </box>
@@ -909,7 +1139,9 @@ function ComparisonTable({
909
1139
  // ============================================================================
910
1140
 
911
1141
  function ContributorsSection({
912
- contributors, breakdown, panelWidth,
1142
+ contributors,
1143
+ breakdown,
1144
+ panelWidth,
913
1145
  }: {
914
1146
  contributors: ProviderContribution[];
915
1147
  breakdown: BreakdownDimension;
@@ -917,7 +1149,7 @@ function ContributorsSection({
917
1149
  }) {
918
1150
  const colors = useColors();
919
1151
 
920
- if (breakdown === 'off' || contributors.length === 0) return null;
1152
+ if (breakdown === "off" || contributors.length === 0) return null;
921
1153
 
922
1154
  // panelWidth includes border(2) + paddingX(2) from InsightPanel = 4 cols overhead
923
1155
  const innerWidth = panelWidth - 4;
@@ -927,14 +1159,21 @@ function ContributorsSection({
927
1159
 
928
1160
  return (
929
1161
  <box flexDirection="column" paddingX={1}>
930
- <text fg={colors.textMuted} height={1}><strong>TOP {breakdown === 'model' ? 'MODELS' : breakdown === 'project' ? 'PROJECTS' : 'PROVIDERS'}</strong></text>
1162
+ <text fg={colors.textMuted} height={1}>
1163
+ <strong>
1164
+ TOP{" "}
1165
+ {breakdown === "model" ? "MODELS" : breakdown === "project" ? "PROJECTS" : "PROVIDERS"}
1166
+ </strong>
1167
+ </text>
931
1168
  {contributors.map((c) => (
932
1169
  <box key={c.provider} flexDirection="column">
933
1170
  <box flexDirection="row" height={1}>
934
1171
  <text fg={colors.primary} width={nameWidth} height={1}>
935
1172
  {padRight(truncate(c.provider, nameWidth), nameWidth)}
936
1173
  </text>
937
- <text fg={colors.text} height={1}>{fmtCurrency(c.cost).padStart(costColWidth)}</text>
1174
+ <text fg={colors.text} height={1}>
1175
+ {fmtCurrency(c.cost).padStart(costColWidth)}
1176
+ </text>
938
1177
  </box>
939
1178
  <box flexDirection="row" height={1}>
940
1179
  <text fg={colors.textSubtle} width={5} height={1}>
@@ -955,7 +1194,12 @@ function ContributorsSection({
955
1194
  // ============================================================================
956
1195
 
957
1196
  function CursorDayDetail({
958
- currentDay, previousDay, metric, contributors, cursorIndex, breakdown,
1197
+ currentDay,
1198
+ previousDay,
1199
+ metric,
1200
+ contributors,
1201
+ cursorIndex,
1202
+ breakdown,
959
1203
  }: {
960
1204
  currentDay: ChartPoint;
961
1205
  previousDay: ChartPoint | null;
@@ -972,38 +1216,70 @@ function CursorDayDetail({
972
1216
 
973
1217
  return (
974
1218
  <box flexDirection="column" paddingX={1}>
975
- <text fg={colors.primary} height={1}><strong>{currentDay.dayName} DETAIL</strong></text>
1219
+ <text fg={colors.primary} height={1}>
1220
+ <strong>{currentDay.dayName} DETAIL</strong>
1221
+ </text>
976
1222
  <box flexDirection="row" justifyContent="space-between" height={1}>
977
- <text fg={colors.textMuted} height={1}>Cost</text>
978
- <text fg={colors.success} height={1}><strong>{fmtCurrency(currentDay.costUsd)}</strong></text>
1223
+ <text fg={colors.textMuted} height={1}>
1224
+ Cost
1225
+ </text>
1226
+ <text fg={colors.success} height={1}>
1227
+ <strong>{fmtCurrency(currentDay.costUsd)}</strong>
1228
+ </text>
979
1229
  </box>
980
1230
  <box flexDirection="row" justifyContent="space-between" height={1}>
981
- <text fg={colors.textMuted} height={1}>Tokens</text>
982
- <text fg={colors.text} height={1}>{fmtTokens(currentDay.tokens)}</text>
1231
+ <text fg={colors.textMuted} height={1}>
1232
+ Tokens
1233
+ </text>
1234
+ <text fg={colors.text} height={1}>
1235
+ {fmtTokens(currentDay.tokens)}
1236
+ </text>
983
1237
  </box>
984
1238
  <box flexDirection="row" justifyContent="space-between" height={1}>
985
- <text fg={colors.textMuted} height={1}>Requests</text>
986
- <text fg={colors.text} height={1}>{fmtNumber(currentDay.requestCount)}</text>
1239
+ <text fg={colors.textMuted} height={1}>
1240
+ Requests
1241
+ </text>
1242
+ <text fg={colors.text} height={1}>
1243
+ {fmtNumber(currentDay.requestCount)}
1244
+ </text>
987
1245
  </box>
988
1246
  {dayDelta && (
989
1247
  <box flexDirection="row" justifyContent="space-between" height={1}>
990
- <text fg={colors.textMuted} height={1}>vs prev day</text>
991
- <text fg={dayDelta.positive ? colors.success : colors.error} height={1}>{dayDelta.text}</text>
1248
+ <text fg={colors.textMuted} height={1}>
1249
+ vs prev day
1250
+ </text>
1251
+ <text fg={dayDelta.positive ? colors.success : colors.error} height={1}>
1252
+ {dayDelta.text}
1253
+ </text>
992
1254
  </box>
993
1255
  )}
994
1256
 
995
1257
  {contributors.length > 0 && (
996
1258
  <box flexDirection="column">
997
1259
  <box height={1}>
998
- <text fg={colors.border} height={1}>{'─'.repeat(20)}</text>
1260
+ <text fg={colors.border} height={1}>
1261
+ {"─".repeat(20)}
1262
+ </text>
999
1263
  </box>
1000
- <text fg={colors.textMuted} height={1}><strong>{breakdown === 'model' ? 'TOP MODELS' : breakdown === 'project' ? 'TOP PROJECTS' : 'PROVIDERS'}</strong></text>
1264
+ <text fg={colors.textMuted} height={1}>
1265
+ <strong>
1266
+ {breakdown === "model"
1267
+ ? "TOP MODELS"
1268
+ : breakdown === "project"
1269
+ ? "TOP PROJECTS"
1270
+ : "PROVIDERS"}
1271
+ </strong>
1272
+ </text>
1001
1273
  {contributors.map((c) => {
1002
1274
  const dayCost = c.dailyCosts[cursorIndex] ?? 0;
1003
1275
  return (
1004
1276
  <box key={c.provider} flexDirection="row" justifyContent="space-between" height={1}>
1005
- <text fg={colors.primary} height={1}>{truncate(c.provider, 12)}</text>
1006
- <text fg={colors.text} height={1}>{fmtCurrency(dayCost)}</text>
1277
+ <text fg={colors.primary} height={1}>
1278
+ {truncate(c.provider, 12)}
1279
+ </text>
1280
+ <text fg={colors.text} height={1}>
1281
+ {fmtCurrency(dayCost)}
1282
+ </text>
1007
1283
  </box>
1008
1284
  );
1009
1285
  })}
@@ -1018,7 +1294,9 @@ function CursorDayDetail({
1018
1294
  // ============================================================================
1019
1295
 
1020
1296
  function CondensedStrip({
1021
- summary, prevSummary, contributors,
1297
+ summary,
1298
+ prevSummary,
1299
+ contributors,
1022
1300
  }: {
1023
1301
  summary: PeriodSummary;
1024
1302
  prevSummary: PeriodSummary;
@@ -1030,21 +1308,30 @@ function CondensedStrip({
1030
1308
  return (
1031
1309
  <box flexDirection="row" height={2} paddingX={1} gap={2}>
1032
1310
  <box flexDirection="column">
1033
- <text fg={colors.textMuted} height={1}>Total: <span fg={colors.success}><strong>{fmtCurrency(summary.totalCost)}</strong></span></text>
1034
1311
  <text fg={colors.textMuted} height={1}>
1035
- {'Avg: '}{fmtCurrency(summary.avgPerDay)}{'/d '}
1312
+ Total:{" "}
1313
+ <span fg={colors.success}>
1314
+ <strong>{fmtCurrency(summary.totalCost)}</strong>
1315
+ </span>
1316
+ </text>
1317
+ <text fg={colors.textMuted} height={1}>
1318
+ {"Avg: "}
1319
+ {fmtCurrency(summary.avgPerDay)}
1320
+ {"/d "}
1036
1321
  <span fg={costDelta.positive ? colors.success : colors.error}>{costDelta.text}</span>
1037
1322
  </text>
1038
1323
  </box>
1039
1324
  {contributors.length > 0 && (
1040
1325
  <box flexDirection="column">
1041
- <text fg={colors.textMuted} height={1}>Top:</text>
1326
+ <text fg={colors.textMuted} height={1}>
1327
+ Top:
1328
+ </text>
1042
1329
  <text height={1}>
1043
1330
  {contributors.slice(0, 3).map((c, i) => (
1044
1331
  <span key={c.provider}>
1045
- {i > 0 ? ' ' : ''}
1332
+ {i > 0 ? " " : ""}
1046
1333
  <span fg={colors.primary}>{truncate(c.provider, 8)}</span>
1047
- <span fg={colors.text}>{' '}{fmtCurrency(c.cost)}</span>
1334
+ <span fg={colors.text}> {fmtCurrency(c.cost)}</span>
1048
1335
  </span>
1049
1336
  ))}
1050
1337
  </text>
@@ -1059,8 +1346,18 @@ function CondensedStrip({
1059
1346
  // ============================================================================
1060
1347
 
1061
1348
  function InsightPanel({
1062
- summary, prevSummary, contributors, previousContributors,
1063
- metric, breakdown, comparisonMode, cursorMode, cursorIndex, data, panelWidth, hasPrevData,
1349
+ summary,
1350
+ prevSummary,
1351
+ contributors,
1352
+ previousContributors,
1353
+ metric,
1354
+ breakdown,
1355
+ comparisonMode,
1356
+ cursorMode,
1357
+ cursorIndex,
1358
+ data,
1359
+ panelWidth,
1360
+ hasPrevData,
1064
1361
  }: {
1065
1362
  summary: PeriodSummary;
1066
1363
  prevSummary: PeriodSummary;
@@ -1140,13 +1437,17 @@ function InsightPanel({
1140
1437
  <PeriodSummarySection summary={summary} metric={metric} />
1141
1438
 
1142
1439
  <box paddingX={1} height={1}>
1143
- <text fg={colors.border} height={1}>{'─'.repeat(Math.max(0, panelWidth - 4))}</text>
1440
+ <text fg={colors.border} height={1}>
1441
+ {"─".repeat(Math.max(0, panelWidth - 4))}
1442
+ </text>
1144
1443
  </box>
1145
1444
 
1146
1445
  <DeltaSection current={summary} previous={prevSummary} hasPrevData={hasPrevData} />
1147
1446
 
1148
1447
  <box paddingX={1} height={1}>
1149
- <text fg={colors.border} height={1}>{'─'.repeat(Math.max(0, panelWidth - 4))}</text>
1448
+ <text fg={colors.border} height={1}>
1449
+ {"─".repeat(Math.max(0, panelWidth - 4))}
1450
+ </text>
1150
1451
  </box>
1151
1452
 
1152
1453
  <ContributorsSection
@@ -1169,9 +1470,9 @@ export function HistoricalTrendsView() {
1169
1470
  const { width: terminalWidth, height: terminalHeight } = useTerminalDimensions();
1170
1471
 
1171
1472
  // State
1172
- const [period, setPeriod] = useState<TimePeriod>('7d');
1173
- const [metric, setMetric] = useState<MetricType>('cost');
1174
- const [breakdown, setBreakdown] = useState<BreakdownDimension>('provider');
1473
+ const [period, setPeriod] = useState<TimePeriod>("7d");
1474
+ const [metric, setMetric] = useState<MetricType>("cost");
1475
+ const [breakdown, setBreakdown] = useState<BreakdownDimension>("provider");
1175
1476
  const [showInsight, setShowInsight] = useState(true);
1176
1477
  const [comparisonMode, setComparisonMode] = useState(false);
1177
1478
  const [cursorMode, setCursorMode] = useState(false);
@@ -1198,7 +1499,10 @@ export function HistoricalTrendsView() {
1198
1499
  const viewChrome = 8;
1199
1500
  // Narrow mode has condensed strip (2 rows) + vertical gap (1 row)
1200
1501
  const stripOverhead = isNarrow ? 3 : 0;
1201
- const chartHeight = Math.max(isNarrow ? 6 : 8, terminalHeight - appOverhead - viewChrome - stripOverhead);
1502
+ const chartHeight = Math.max(
1503
+ isNarrow ? 6 : 8,
1504
+ terminalHeight - appOverhead - viewChrome - stripOverhead,
1505
+ );
1202
1506
 
1203
1507
  // Fetch time series data
1204
1508
  useEffect(() => {
@@ -1214,17 +1518,18 @@ export function HistoricalTrendsView() {
1214
1518
 
1215
1519
  // Fetch breakdown contributors based on dimension
1216
1520
  useEffect(() => {
1217
- if (breakdown === 'off') {
1521
+ if (breakdown === "off") {
1218
1522
  setContributors([]);
1219
1523
  return;
1220
1524
  }
1221
1525
  if (demoMode && simulator) {
1222
- if (breakdown === 'model') setContributors(fetchDemoModelBreakdown(simulator, period));
1223
- else if (breakdown === 'project') setContributors(fetchDemoProjectBreakdown(simulator, period));
1526
+ if (breakdown === "model") setContributors(fetchDemoModelBreakdown(simulator, period));
1527
+ else if (breakdown === "project")
1528
+ setContributors(fetchDemoProjectBreakdown(simulator, period));
1224
1529
  else setContributors(fetchDemoProviderBreakdown(simulator, period));
1225
1530
  } else if (isStorageReady) {
1226
- if (breakdown === 'model') setContributors(fetchModelBreakdown(period));
1227
- else if (breakdown === 'project') setContributors(fetchProjectBreakdown(period));
1531
+ if (breakdown === "model") setContributors(fetchModelBreakdown(period));
1532
+ else if (breakdown === "project") setContributors(fetchProjectBreakdown(period));
1228
1533
  else setContributors(fetchProviderBreakdown(period));
1229
1534
  }
1230
1535
  }, [isStorageReady, period, breakdown, demoMode, simulator]);
@@ -1254,7 +1559,7 @@ export function HistoricalTrendsView() {
1254
1559
  requests: 0,
1255
1560
  dailyCosts: [],
1256
1561
  }))
1257
- .sort((a, b) => b.cost - a.cost)
1562
+ .sort((a, b) => b.cost - a.cost),
1258
1563
  );
1259
1564
  } catch {
1260
1565
  setPrevContributors([]);
@@ -1269,86 +1574,110 @@ export function HistoricalTrendsView() {
1269
1574
  // Computed values
1270
1575
  const summary = useMemo(() => computeSummary(data, metric), [data, metric]);
1271
1576
  const prevSummary = useMemo(() => computeSummary(prevData, metric), [prevData, metric]);
1272
- const hasData = data.some(p => p.costUsd > 0 || p.tokens > 0 || p.requestCount > 0);
1273
- const hasPrevData = prevData.some(p => p.costUsd > 0 || p.tokens > 0 || p.requestCount > 0);
1577
+ const hasData = data.some((p) => p.costUsd > 0 || p.tokens > 0 || p.requestCount > 0);
1578
+ const hasPrevData = prevData.some((p) => p.costUsd > 0 || p.tokens > 0 || p.requestCount > 0);
1274
1579
  const totalForMetric = useMemo(() => {
1275
1580
  switch (metric) {
1276
- case 'cost': return data.reduce((acc, p) => acc + p.costUsd, 0);
1277
- case 'tokens': return data.reduce((acc, p) => acc + p.tokens, 0);
1278
- case 'requests': return data.reduce((acc, p) => acc + p.requestCount, 0);
1581
+ case "cost":
1582
+ return data.reduce((acc, p) => acc + p.costUsd, 0);
1583
+ case "tokens":
1584
+ return data.reduce((acc, p) => acc + p.tokens, 0);
1585
+ case "requests":
1586
+ return data.reduce((acc, p) => acc + p.requestCount, 0);
1279
1587
  }
1280
1588
  }, [data, metric]);
1281
1589
 
1282
1590
  // Keyboard
1283
1591
  useKeyboard((key) => {
1284
- if (key.name === 'left' || key.name === 'h') {
1592
+ if (key.name === "left" || key.name === "h") {
1285
1593
  if (cursorMode) {
1286
- setCursorIndex(prev => Math.max(0, prev - 1));
1594
+ setCursorIndex((prev) => Math.max(0, prev - 1));
1287
1595
  } else {
1288
- setPeriod(prev => prev === '90d' ? '30d' : prev === '30d' ? '7d' : '7d');
1596
+ setPeriod((prev) => (prev === "90d" ? "30d" : prev === "30d" ? "7d" : "7d"));
1289
1597
  }
1290
1598
  }
1291
- if (key.name === 'right' || key.name === 'l') {
1599
+ if (key.name === "right" || key.name === "l") {
1292
1600
  if (cursorMode) {
1293
- setCursorIndex(prev => Math.min(data.length - 1, prev + 1));
1601
+ setCursorIndex((prev) => Math.min(data.length - 1, prev + 1));
1294
1602
  } else {
1295
- setPeriod(prev => prev === '7d' ? '30d' : prev === '30d' ? '90d' : '90d');
1603
+ setPeriod((prev) => (prev === "7d" ? "30d" : prev === "30d" ? "90d" : "90d"));
1296
1604
  }
1297
1605
  }
1298
1606
 
1299
- if (key.name === 'down' || key.name === 'j') {
1607
+ if (key.name === "down" || key.name === "j") {
1300
1608
  if (!cursorMode) {
1301
1609
  setCursorMode(true);
1302
1610
  setCursorIndex(0);
1303
1611
  } else {
1304
- setCursorIndex(prev => Math.min(data.length - 1, prev + 1));
1612
+ setCursorIndex((prev) => Math.min(data.length - 1, prev + 1));
1305
1613
  }
1306
1614
  }
1307
- if (key.name === 'up' || key.name === 'k') {
1615
+ if (key.name === "up" || key.name === "k") {
1308
1616
  if (!cursorMode) {
1309
1617
  setCursorMode(true);
1310
1618
  setCursorIndex(0);
1311
1619
  } else {
1312
- setCursorIndex(prev => Math.max(0, prev - 1));
1620
+ setCursorIndex((prev) => Math.max(0, prev - 1));
1313
1621
  }
1314
1622
  }
1315
1623
 
1316
- if (key.name === 'escape') {
1624
+ if (key.name === "escape") {
1317
1625
  if (cursorMode) setCursorMode(false);
1318
1626
  else if (comparisonMode) setComparisonMode(false);
1319
1627
  }
1320
1628
 
1321
- if (key.name === 'c') setComparisonMode(prev => !prev);
1322
- if (key.name === 'm') setMetric(prev => prev === 'cost' ? 'tokens' : prev === 'tokens' ? 'requests' : 'cost');
1323
- if (key.name === 'b') setBreakdown(prev => prev === 'provider' ? 'model' : prev === 'model' ? 'project' : prev === 'project' ? 'off' : 'provider');
1324
- if (key.name === 'i') setShowInsight(prev => !prev);
1629
+ if (key.name === "c") setComparisonMode((prev) => !prev);
1630
+ if (key.name === "m")
1631
+ setMetric((prev) => (prev === "cost" ? "tokens" : prev === "tokens" ? "requests" : "cost"));
1632
+ if (key.name === "b")
1633
+ setBreakdown((prev) =>
1634
+ prev === "provider"
1635
+ ? "model"
1636
+ : prev === "model"
1637
+ ? "project"
1638
+ : prev === "project"
1639
+ ? "off"
1640
+ : "provider",
1641
+ );
1642
+ if (key.name === "i") setShowInsight((prev) => !prev);
1325
1643
  });
1326
1644
 
1327
- const periodLabel = period === '7d' ? '7 days' : period === '30d' ? '30 days' : '90 days';
1645
+ const periodLabel = period === "7d" ? "7 days" : period === "30d" ? "30 days" : "90 days";
1328
1646
  const modeIndicators: string[] = [];
1329
- if (comparisonMode) modeIndicators.push('CMP');
1330
- if (cursorMode) modeIndicators.push('CURSOR');
1331
- if (metric !== 'cost') modeIndicators.push(METRIC_LABELS[metric]);
1647
+ if (comparisonMode) modeIndicators.push("CMP");
1648
+ if (cursorMode) modeIndicators.push("CURSOR");
1649
+ if (metric !== "cost") modeIndicators.push(METRIC_LABELS[metric]);
1332
1650
 
1333
1651
  return (
1334
- <box flexDirection="column" flexGrow={1} padding={1} border borderStyle="single" borderColor={colors.border}>
1652
+ <box
1653
+ flexDirection="column"
1654
+ flexGrow={1}
1655
+ padding={1}
1656
+ border
1657
+ borderStyle="single"
1658
+ borderColor={colors.border}
1659
+ >
1335
1660
  {/* Header */}
1336
1661
  <box flexDirection="row" justifyContent="space-between" height={1} marginBottom={1}>
1337
1662
  <text height={1}>
1338
- <span fg={colors.primary}><strong>{' '}{METRIC_LABELS[metric]} TREND{' '}</strong></span>
1663
+ <span fg={colors.primary}>
1664
+ <strong> {METRIC_LABELS[metric]} TREND </strong>
1665
+ </span>
1339
1666
  <span fg={colors.textMuted}>({periodLabel})</span>
1340
1667
  {modeIndicators.length > 0 && (
1341
- <span fg={colors.accent}>{' '}[{modeIndicators.join(' | ')}]</span>
1668
+ <span fg={colors.accent}> [{modeIndicators.join(" | ")}]</span>
1342
1669
  )}
1343
1670
  </text>
1344
1671
  <text height={1}>
1345
1672
  <span fg={colors.textMuted}>Total: </span>
1346
- <span fg={colors.success}><strong>{fmtMetricValue(totalForMetric, metric)}</strong></span>
1673
+ <span fg={colors.success}>
1674
+ <strong>{fmtMetricValue(totalForMetric, metric)}</strong>
1675
+ </span>
1347
1676
  </text>
1348
1677
  </box>
1349
1678
 
1350
1679
  {/* Main content */}
1351
- <box flexGrow={1} flexDirection={isNarrow ? 'column' : 'row'} gap={1}>
1680
+ <box flexGrow={1} flexDirection={isNarrow ? "column" : "row"} gap={1}>
1352
1681
  {/* Chart pane */}
1353
1682
  <box flexGrow={1} flexDirection="column" justifyContent="center" overflow="hidden">
1354
1683
  {!isStorageReady && !demoMode ? (
@@ -1376,8 +1705,9 @@ export function HistoricalTrendsView() {
1376
1705
  </box>
1377
1706
 
1378
1707
  {/* Insight panel */}
1379
- {showInsight && hasData && (
1380
- isNarrow ? (
1708
+ {showInsight &&
1709
+ hasData &&
1710
+ (isNarrow ? (
1381
1711
  <CondensedStrip
1382
1712
  summary={summary}
1383
1713
  prevSummary={prevSummary}
@@ -1398,8 +1728,7 @@ export function HistoricalTrendsView() {
1398
1728
  panelWidth={panelWidth}
1399
1729
  hasPrevData={hasPrevData}
1400
1730
  />
1401
- )
1402
- )}
1731
+ ))}
1403
1732
  </box>
1404
1733
 
1405
1734
  {/* Footer */}
@@ -1409,19 +1738,19 @@ export function HistoricalTrendsView() {
1409
1738
  <span>
1410
1739
  <span fg={colors.accent}>CURSOR</span>
1411
1740
  {isNarrow
1412
- ? ' ←→ move Esc exit c cmp m met'
1413
- : ' ←→ move Esc exit c compare m metric i panel'}
1741
+ ? " ←→ move Esc exit c cmp m met"
1742
+ : " ←→ move Esc exit c compare m metric i panel"}
1414
1743
  </span>
1415
1744
  ) : (
1416
1745
  <span>
1417
- <span fg={period === '7d' ? colors.primary : colors.textSubtle}>7d</span>
1418
- {' '}
1419
- <span fg={period === '30d' ? colors.primary : colors.textSubtle}>30d</span>
1420
- {' '}
1421
- <span fg={period === '90d' ? colors.primary : colors.textSubtle}>90d</span>
1746
+ <span fg={period === "7d" ? colors.primary : colors.textSubtle}>7d</span>
1747
+ {" "}
1748
+ <span fg={period === "30d" ? colors.primary : colors.textSubtle}>30d</span>
1749
+ {" "}
1750
+ <span fg={period === "90d" ? colors.primary : colors.textSubtle}>90d</span>
1422
1751
  {isNarrow
1423
- ? ' h/l per j/k cur c cmp m met b brk i pan'
1424
- : ' h/l period ↑↓ cursor c compare m metric b breakdown i panel'}
1752
+ ? " h/l per j/k cur c cmp m met b brk i pan"
1753
+ : " h/l period ↑↓ cursor c compare m metric b breakdown i panel"}
1425
1754
  </span>
1426
1755
  )}
1427
1756
  </text>