@tokentop/ttop 0.2.2 → 0.3.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 (158) 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 +32 -32
  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 +29 -23
  19. package/src/plugins/notifications/visual-flash.ts +21 -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 +66 -57
  34. package/src/plugins/registry.ts +47 -43
  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/repos/agentSessions.ts +33 -23
  73. package/src/storage/repos/providerSnapshots.ts +5 -5
  74. package/src/storage/repos/usageEvents.ts +40 -28
  75. package/src/storage/types.ts +5 -5
  76. package/src/tui/App.tsx +180 -137
  77. package/src/tui/components/CommandPalette.tsx +46 -34
  78. package/src/tui/components/DebugConsole.tsx +39 -33
  79. package/src/tui/components/DebugPanel.tsx +295 -177
  80. package/src/tui/components/FooterHints.tsx +2 -2
  81. package/src/tui/components/GhostProviderCard.tsx +37 -35
  82. package/src/tui/components/Header.tsx +68 -35
  83. package/src/tui/components/HelpOverlay.tsx +99 -29
  84. package/src/tui/components/InlineGauge.tsx +10 -7
  85. package/src/tui/components/InlineSparkline.tsx +17 -11
  86. package/src/tui/components/KpiStrip.tsx +128 -98
  87. package/src/tui/components/LimitGauge.tsx +96 -59
  88. package/src/tui/components/ModalBackdrop.tsx +4 -4
  89. package/src/tui/components/ProviderAggregateStrip.tsx +69 -38
  90. package/src/tui/components/ProviderCard.tsx +232 -226
  91. package/src/tui/components/ProviderDetailPanel.tsx +133 -58
  92. package/src/tui/components/ProviderLimitsPanel.tsx +126 -89
  93. package/src/tui/components/ProvidersList.tsx +215 -126
  94. package/src/tui/components/SessionDetailsDrawer.tsx +191 -94
  95. package/src/tui/components/SessionsTable.tsx +313 -166
  96. package/src/tui/components/SettingsModal.tsx +231 -214
  97. package/src/tui/components/SidebarBreakdown.tsx +30 -16
  98. package/src/tui/components/Skeleton.tsx +24 -17
  99. package/src/tui/components/SmartSidebar.tsx +360 -258
  100. package/src/tui/components/Sparkline.tsx +64 -52
  101. package/src/tui/components/Spinner.tsx +3 -3
  102. package/src/tui/components/StatusBar.tsx +27 -27
  103. package/src/tui/components/ThemePicker.tsx +67 -66
  104. package/src/tui/components/Toast.tsx +9 -6
  105. package/src/tui/components/UsageGauge.tsx +10 -13
  106. package/src/tui/components/index.ts +5 -5
  107. package/src/tui/contexts/AgentSessionContext.tsx +193 -144
  108. package/src/tui/contexts/ConfigContext.tsx +15 -13
  109. package/src/tui/contexts/DashboardRuntimeContext.tsx +26 -26
  110. package/src/tui/contexts/DemoModeContext.tsx +22 -15
  111. package/src/tui/contexts/DrawerContext.tsx +11 -9
  112. package/src/tui/contexts/InputContext.tsx +2 -2
  113. package/src/tui/contexts/LogContext.tsx +62 -47
  114. package/src/tui/contexts/PluginContext.tsx +232 -183
  115. package/src/tui/contexts/RealTimeActivityContext.tsx +70 -36
  116. package/src/tui/contexts/StorageContext.tsx +151 -117
  117. package/src/tui/contexts/ThemeContext.tsx +21 -16
  118. package/src/tui/contexts/TimeWindowContext.tsx +72 -61
  119. package/src/tui/contexts/ToastContext.tsx +10 -7
  120. package/src/tui/contexts/index.ts +9 -5
  121. package/src/tui/createApp.tsx +8 -12
  122. package/src/tui/debug/captureFrame.ts +25 -28
  123. package/src/tui/debug/snapshot.tsx +179 -162
  124. package/src/tui/driver/assertions.ts +30 -22
  125. package/src/tui/driver/cli.ts +231 -221
  126. package/src/tui/driver/coverage.ts +27 -44
  127. package/src/tui/driver/demo-snapshot.ts +33 -33
  128. package/src/tui/driver/diff.ts +26 -32
  129. package/src/tui/driver/driver.ts +43 -48
  130. package/src/tui/driver/index.ts +36 -40
  131. package/src/tui/driver/recorder.ts +37 -48
  132. package/src/tui/driver/test-workflow.ts +51 -51
  133. package/src/tui/driver/test.ts +25 -25
  134. package/src/tui/hooks/index.ts +24 -20
  135. package/src/tui/hooks/useAnimatedValue.ts +13 -18
  136. package/src/tui/hooks/useDashboardKeyboard.ts +118 -98
  137. package/src/tui/hooks/useDashboardState.ts +26 -24
  138. package/src/tui/hooks/useEmaActivity.ts +82 -74
  139. package/src/tui/hooks/useEntranceAnimation.ts +11 -11
  140. package/src/tui/hooks/useExitAnimation.ts +3 -3
  141. package/src/tui/hooks/usePulse.ts +4 -4
  142. package/src/tui/hooks/useSafeRenderer.ts +5 -5
  143. package/src/tui/hooks/useSessionEnrichedProviders.ts +150 -0
  144. package/src/tui/hooks/useValueFlash.ts +10 -14
  145. package/src/tui/index.tsx +29 -6
  146. package/src/tui/shutdown.ts +19 -0
  147. package/src/tui/utils/providerColor.ts +1 -1
  148. package/src/tui/views/Dashboard.tsx +127 -102
  149. package/src/tui/views/HistoricalTrendsView.tsx +579 -250
  150. package/src/tui/views/ProjectsView.tsx +466 -237
  151. package/src/tui/views/RealTimeDashboard.tsx +150 -111
  152. package/src/tui/views/SettingsView.tsx +222 -191
  153. package/src/tui/views/index.ts +2 -2
  154. package/src/types/opentui-test-utils.d.ts +7 -7
  155. package/src/utils/clipboard.ts +29 -29
  156. package/src/utils/currency.test.ts +53 -53
  157. package/src/utils/currency.ts +35 -32
  158. package/src/version.ts +1 -1
@@ -1,14 +1,14 @@
1
- import { useMemo } from 'react';
2
- import { useTerminalDimensions } from '@opentui/react';
3
- import { useColors } from '../contexts/ThemeContext.tsx';
4
- import { useConfig } from '../contexts/ConfigContext.tsx';
5
- import { useTimeWindow } from '../contexts/TimeWindowContext.tsx';
6
- import { usePulse } from '../hooks/usePulse.ts';
7
- import type { AgentSessionAggregate, AgentSessionStream } from '../../agents/types.ts';
1
+ import { useTerminalDimensions } from "@opentui/react";
2
+ import { useMemo } from "react";
3
+ import type { AgentSessionAggregate, AgentSessionStream } from "../../agents/types.ts";
4
+ import { useConfig } from "../contexts/ConfigContext.tsx";
5
+ import { useColors } from "../contexts/ThemeContext.tsx";
6
+ import { useTimeWindow } from "../contexts/TimeWindowContext.tsx";
7
+ import { usePulse } from "../hooks/usePulse.ts";
8
8
 
9
- export type DriverDimension = 'model' | 'project' | 'agent';
9
+ export type DriverDimension = "model" | "project" | "agent";
10
10
 
11
- export type SidebarMode = 'full' | 'compact' | 'minimal' | 'micro' | 'hidden';
11
+ export type SidebarMode = "full" | "compact" | "minimal" | "micro" | "hidden";
12
12
 
13
13
  export interface DriverStats {
14
14
  id: string;
@@ -24,7 +24,7 @@ export interface DriverStats {
24
24
  export interface SmartSidebarProps {
25
25
  sessions: AgentSessionAggregate[];
26
26
  budgetCost: number;
27
- focusedPanel: 'sessions' | 'sidebar' | 'limits';
27
+ focusedPanel: "sessions" | "sidebar" | "limits";
28
28
  dimension: DriverDimension;
29
29
  selectedDriverIndex: number;
30
30
  activeDriverFilter: string | null;
@@ -34,24 +34,24 @@ export interface SmartSidebarProps {
34
34
  function interpolatePulseColor(intensity: number, baseColor: string, dimColor: string): string {
35
35
  const dimAmount = 0.4;
36
36
  const t = intensity * dimAmount;
37
-
37
+
38
38
  const parseHex = (hex: string) => {
39
- const h = hex.replace('#', '');
39
+ const h = hex.replace("#", "");
40
40
  return {
41
41
  r: parseInt(h.substring(0, 2), 16),
42
42
  g: parseInt(h.substring(2, 4), 16),
43
43
  b: parseInt(h.substring(4, 6), 16),
44
44
  };
45
45
  };
46
-
46
+
47
47
  const base = parseHex(baseColor);
48
48
  const dim = parseHex(dimColor);
49
-
49
+
50
50
  const r = Math.round(base.r + (dim.r - base.r) * t);
51
51
  const g = Math.round(base.g + (dim.g - base.g) * t);
52
52
  const b = Math.round(base.b + (dim.b - base.b) * t);
53
-
54
- return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
53
+
54
+ return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
55
55
  }
56
56
 
57
57
  function formatCurrency(val: number): string {
@@ -68,51 +68,56 @@ function formatBudget(val: number): string {
68
68
  }
69
69
 
70
70
  function formatDelta(val: number): string {
71
- if (val < 0.01) return '';
71
+ if (val < 0.01) return "";
72
72
  if (val >= 10) return `+$${val.toFixed(1)}`;
73
73
  return `+$${val.toFixed(2)}`;
74
74
  }
75
75
 
76
76
  function formatPercent(val: number): string {
77
- if (val < 1) return '<1%';
77
+ if (val < 1) return "<1%";
78
78
  return `${Math.round(val)}%`;
79
79
  }
80
80
 
81
81
  function truncateWithEllipsis(str: string, maxLen: number): string {
82
82
  if (str.length <= maxLen) return str;
83
- return str.slice(0, maxLen - 1) + '…';
83
+ return str.slice(0, maxLen - 1) + "…";
84
84
  }
85
85
 
86
86
  function getSidebarMode(width: number): SidebarMode {
87
- if (width >= 140) return 'full';
88
- if (width >= 105) return 'compact';
89
- if (width >= 95) return 'minimal';
90
- if (width >= 85) return 'micro';
91
- return 'hidden';
87
+ if (width >= 140) return "full";
88
+ if (width >= 105) return "compact";
89
+ if (width >= 95) return "minimal";
90
+ if (width >= 85) return "micro";
91
+ return "hidden";
92
92
  }
93
93
 
94
94
  function getSidebarWidth(mode: SidebarMode, terminalWidth: number): number {
95
- if (mode === 'micro') return 28;
96
- if (mode === 'minimal') return 32;
97
- if (mode === 'compact') return 36;
95
+ if (mode === "micro") return 28;
96
+ if (mode === "minimal") return 32;
97
+ if (mode === "compact") return 36;
98
98
  if (terminalWidth >= 160) return 44;
99
99
  if (terminalWidth >= 140) return 40;
100
100
  return 38;
101
101
  }
102
102
 
103
103
  function extractRepoName(projectPath: string | undefined | null): string {
104
- if (!projectPath) return 'unknown';
105
- const normalized = projectPath.replace(/\\/g, '/');
106
- return normalized.split('/').filter(Boolean).pop() ?? 'unknown';
104
+ if (!projectPath) return "unknown";
105
+ const normalized = projectPath.replace(/\\/g, "/");
106
+ return normalized.split("/").filter(Boolean).pop() ?? "unknown";
107
107
  }
108
108
 
109
- function makeProgressBar(percent: number, width: number, filledChar = '█', emptyChar = '·'): string {
109
+ function makeProgressBar(
110
+ percent: number,
111
+ width: number,
112
+ filledChar = "█",
113
+ emptyChar = "·",
114
+ ): string {
110
115
  const clamped = Math.max(0, Math.min(100, percent));
111
116
  const filledWidth = Math.round((clamped / 100) * width);
112
117
  return filledChar.repeat(filledWidth) + emptyChar.repeat(width - filledWidth);
113
118
  }
114
119
 
115
- function makeDivider(width: number, char = '─'): string {
120
+ function makeDivider(width: number, char = "─"): string {
116
121
  return char.repeat(Math.max(0, width - 2));
117
122
  }
118
123
 
@@ -127,27 +132,32 @@ function BudgetSection({ totalCost, budgetCost, mode, sidebarWidth }: BudgetSect
127
132
  const colors = useColors();
128
133
  const { config } = useConfig();
129
134
  const { budgetType, budgetTypeLabel, budgetLock } = useTimeWindow();
130
-
131
- const activeBudget = budgetType === 'daily' ? config.budgets.daily
132
- : budgetType === 'weekly' ? config.budgets.weekly
133
- : budgetType === 'monthly' ? config.budgets.monthly
134
- : null;
135
-
136
- const hasBudgetLimit = activeBudget !== null && budgetType !== 'none';
135
+
136
+ const activeBudget =
137
+ budgetType === "daily"
138
+ ? config.budgets.daily
139
+ : budgetType === "weekly"
140
+ ? config.budgets.weekly
141
+ : budgetType === "monthly"
142
+ ? config.budgets.monthly
143
+ : null;
144
+
145
+ const hasBudgetLimit = activeBudget !== null && budgetType !== "none";
137
146
  const budgetUsedPercent = hasBudgetLimit ? (budgetCost / activeBudget) * 100 : 0;
138
-
147
+
139
148
  const isCritical = hasBudgetLimit && budgetUsedPercent >= config.alerts.criticalPercent;
140
- const isWarning = hasBudgetLimit && budgetUsedPercent >= config.alerts.warningPercent && !isCritical;
141
-
149
+ const isWarning =
150
+ hasBudgetLimit && budgetUsedPercent >= config.alerts.warningPercent && !isCritical;
151
+
142
152
  const criticalPulseStep = usePulse({ enabled: isCritical, intervalMs: 80 });
143
153
  const warningPulseStep = usePulse({ enabled: isWarning, intervalMs: 200 });
144
-
145
- const pulseIntensity = isCritical
146
- ? Math.abs(Math.sin(criticalPulseStep * 0.3))
147
- : isWarning
154
+
155
+ const pulseIntensity = isCritical
156
+ ? Math.abs(Math.sin(criticalPulseStep * 0.3))
157
+ : isWarning
148
158
  ? Math.abs(Math.sin(warningPulseStep * 0.25))
149
159
  : 0;
150
-
160
+
151
161
  const getStatusColor = () => {
152
162
  if (isCritical) {
153
163
  return interpolatePulseColor(pulseIntensity, colors.error, colors.background);
@@ -157,82 +167,112 @@ function BudgetSection({ totalCost, budgetCost, mode, sidebarWidth }: BudgetSect
157
167
  }
158
168
  return colors.success;
159
169
  };
160
-
170
+
161
171
  const statusColor = hasBudgetLimit ? getStatusColor() : colors.text;
162
172
  const innerWidth = sidebarWidth - 4;
163
173
  const barWidth = Math.max(10, innerWidth - 6);
164
174
  const bar = makeProgressBar(budgetUsedPercent, barWidth);
165
-
166
- const isLocked = budgetLock !== 'sync';
167
- const lockSuffix = isLocked ? ' [locked]' : '';
168
- const headerLabel = budgetType === 'none' ? 'TOTAL COST' : `${budgetTypeLabel.toUpperCase()} BUDGET${lockSuffix}`;
169
-
170
- if (budgetType === 'none') {
171
- if (mode === 'micro') {
172
- return (
173
- <box flexDirection="row" height={1} paddingX={1}>
174
- <text fg={colors.textMuted} height={1}>Total: </text>
175
- <text fg={colors.text} height={1}><strong>{formatCurrency(totalCost)}</strong></text>
176
- </box>
177
- );
175
+
176
+ const isLocked = budgetLock !== "sync";
177
+ const lockSuffix = isLocked ? " [locked]" : "";
178
+ const headerLabel =
179
+ budgetType === "none" ? "TOTAL COST" : `${budgetTypeLabel.toUpperCase()} BUDGET${lockSuffix}`;
180
+
181
+ if (budgetType === "none") {
182
+ if (mode === "micro") {
183
+ return (
184
+ <box flexDirection="row" height={1} paddingX={1}>
185
+ <text fg={colors.textMuted} height={1}>
186
+ Total:{" "}
187
+ </text>
188
+ <text fg={colors.text} height={1}>
189
+ <strong>{formatCurrency(totalCost)}</strong>
190
+ </text>
191
+ </box>
192
+ );
178
193
  }
179
-
194
+
180
195
  return (
181
196
  <box flexDirection="column" paddingX={1}>
182
- <text fg={colors.textMuted} height={1}><strong>{headerLabel}</strong></text>
183
- <text fg={colors.text} height={1}><strong>{formatCurrency(totalCost)}</strong></text>
197
+ <text fg={colors.textMuted} height={1}>
198
+ <strong>{headerLabel}</strong>
199
+ </text>
200
+ <text fg={colors.text} height={1}>
201
+ <strong>{formatCurrency(totalCost)}</strong>
202
+ </text>
184
203
  </box>
185
204
  );
186
205
  }
187
-
206
+
188
207
  if (!activeBudget) {
189
- if (mode === 'micro') {
190
- return (
191
- <box flexDirection="row" height={1} paddingX={1}>
192
- <text fg={colors.textMuted} height={1}>No {budgetTypeLabel.toLowerCase()} budget</text>
193
- </box>
194
- );
208
+ if (mode === "micro") {
209
+ return (
210
+ <box flexDirection="row" height={1} paddingX={1}>
211
+ <text fg={colors.textMuted} height={1}>
212
+ No {budgetTypeLabel.toLowerCase()} budget
213
+ </text>
214
+ </box>
215
+ );
195
216
  }
196
-
197
- return (
198
- <box flexDirection="column" paddingX={1}>
199
- <text fg={colors.textMuted} height={1}><strong>{headerLabel}</strong></text>
200
- <text fg={colors.textSubtle} height={1}>Not set</text>
201
- <text fg={colors.textSubtle} height={1}>(, to configure)</text>
202
- </box>
203
- );
217
+
218
+ return (
219
+ <box flexDirection="column" paddingX={1}>
220
+ <text fg={colors.textMuted} height={1}>
221
+ <strong>{headerLabel}</strong>
222
+ </text>
223
+ <text fg={colors.textSubtle} height={1}>
224
+ Not set
225
+ </text>
226
+ <text fg={colors.textSubtle} height={1}>
227
+ (, to configure)
228
+ </text>
229
+ </box>
230
+ );
204
231
  }
205
-
206
- if (mode === 'micro') {
207
- return (
208
- <box flexDirection="column" paddingX={1}>
209
- <box flexDirection="row" height={1}>
210
- <text fg={colors.textMuted} height={1}>{budgetTypeLabel} </text>
211
- <text fg={statusColor} height={1}><strong>{formatPercent(budgetUsedPercent)}</strong></text>
212
- <text fg={colors.textMuted} height={1}> {formatBudget(budgetCost)}/{formatBudget(activeBudget)}</text>
213
- </box>
214
- </box>
215
- );
232
+
233
+ if (mode === "micro") {
234
+ return (
235
+ <box flexDirection="column" paddingX={1}>
236
+ <box flexDirection="row" height={1}>
237
+ <text fg={colors.textMuted} height={1}>
238
+ {budgetTypeLabel}{" "}
239
+ </text>
240
+ <text fg={statusColor} height={1}>
241
+ <strong>{formatPercent(budgetUsedPercent)}</strong>
242
+ </text>
243
+ <text fg={colors.textMuted} height={1}>
244
+ {" "}
245
+ {formatBudget(budgetCost)}/{formatBudget(activeBudget)}
246
+ </text>
247
+ </box>
248
+ </box>
249
+ );
216
250
  }
217
-
218
- return (
219
- <box flexDirection="column" paddingX={1}>
220
- <box flexDirection="row" height={1}>
221
- <text fg={colors.textMuted}><strong>{headerLabel}</strong></text>
222
- <text flexGrow={1}></text>
223
- <text fg={statusColor}><strong>{formatPercent(budgetUsedPercent)}</strong></text>
224
- </box>
225
-
226
- <box flexDirection="row" height={1}>
227
- <text fg={statusColor}>{bar}</text>
228
- </box>
229
-
230
- <box flexDirection="row" height={1}>
231
- <text fg={colors.text}><strong>{formatBudget(budgetCost)}</strong></text>
232
- <text fg={colors.textMuted}>/{formatBudget(activeBudget)}</text>
233
- </box>
234
- </box>
235
- );
251
+
252
+ return (
253
+ <box flexDirection="column" paddingX={1}>
254
+ <box flexDirection="row" height={1}>
255
+ <text fg={colors.textMuted}>
256
+ <strong>{headerLabel}</strong>
257
+ </text>
258
+ <text flexGrow={1}></text>
259
+ <text fg={statusColor}>
260
+ <strong>{formatPercent(budgetUsedPercent)}</strong>
261
+ </text>
262
+ </box>
263
+
264
+ <box flexDirection="row" height={1}>
265
+ <text fg={statusColor}>{bar}</text>
266
+ </box>
267
+
268
+ <box flexDirection="row" height={1}>
269
+ <text fg={colors.text}>
270
+ <strong>{formatBudget(budgetCost)}</strong>
271
+ </text>
272
+ <text fg={colors.textMuted}>/{formatBudget(activeBudget)}</text>
273
+ </box>
274
+ </box>
275
+ );
236
276
  }
237
277
 
238
278
  interface TopDriversSectionProps {
@@ -248,9 +288,9 @@ interface TopDriversSectionProps {
248
288
  }
249
289
 
250
290
  const DIMENSION_LABELS: Record<DriverDimension, string> = {
251
- model: 'MODELS',
252
- project: 'PROJECTS',
253
- agent: 'AGENTS',
291
+ model: "MODELS",
292
+ project: "PROJECTS",
293
+ agent: "AGENTS",
254
294
  };
255
295
 
256
296
  function TopDriversSection({
@@ -265,53 +305,64 @@ function TopDriversSection({
265
305
  getProviderColor,
266
306
  }: TopDriversSectionProps) {
267
307
  const colors = useColors();
268
-
308
+
269
309
  const displayDrivers = drivers.slice(0, maxDrivers);
270
310
  const innerWidth = sidebarWidth - 4;
271
-
272
- const showBars = mode === 'full' || mode === 'compact';
273
- const showDelta = mode === 'full';
274
- const showPercent = mode !== 'minimal';
275
-
311
+
312
+ const showBars = mode === "full" || mode === "compact";
313
+ const showDelta = mode === "full";
314
+ const showPercent = mode !== "minimal";
315
+
276
316
  const prefixWidth = 2;
277
- const nameWidth = mode === 'full' ? 14 : mode === 'compact' ? 12 : 10;
317
+ const nameWidth = mode === "full" ? 14 : mode === "compact" ? 12 : 10;
278
318
  const deltaWidth = showDelta ? 7 : 0;
279
319
  const percentWidth = showPercent ? 5 : 0;
280
- const barWidth = showBars ? Math.max(8, innerWidth - prefixWidth - nameWidth - deltaWidth - percentWidth - 2) : 0;
281
-
282
- const dimensionToggle = ['m', 'p', 'a'].map(k => {
283
- const isActive = (k === 'm' && dimension === 'model') ||
284
- (k === 'p' && dimension === 'project') ||
285
- (k === 'a' && dimension === 'agent');
286
- return isActive ? k.toUpperCase() : k;
287
- }).join('/');
288
-
289
- return (
290
- <box flexDirection="column" paddingX={1}>
291
- <box flexDirection="row" height={1}>
292
- <text fg={colors.textMuted}><strong>TOP {DIMENSION_LABELS[dimension]}</strong></text>
293
- <text flexGrow={1}></text>
294
- <text fg={colors.textSubtle}>[{dimensionToggle}]</text>
295
- </box>
296
-
320
+ const barWidth = showBars
321
+ ? Math.max(8, innerWidth - prefixWidth - nameWidth - deltaWidth - percentWidth - 2)
322
+ : 0;
323
+
324
+ const dimensionToggle = ["m", "p", "a"]
325
+ .map((k) => {
326
+ const isActive =
327
+ (k === "m" && dimension === "model") ||
328
+ (k === "p" && dimension === "project") ||
329
+ (k === "a" && dimension === "agent");
330
+ return isActive ? k.toUpperCase() : k;
331
+ })
332
+ .join("/");
333
+
334
+ return (
335
+ <box flexDirection="column" paddingX={1}>
336
+ <box flexDirection="row" height={1}>
337
+ <text fg={colors.textMuted}>
338
+ <strong>TOP {DIMENSION_LABELS[dimension]}</strong>
339
+ </text>
340
+ <text flexGrow={1}></text>
341
+ <text fg={colors.textSubtle}>[{dimensionToggle}]</text>
342
+ </box>
343
+
297
344
  {displayDrivers.length === 0 ? (
298
- <text fg={colors.textSubtle} height={1}>No data</text>
345
+ <text fg={colors.textSubtle} height={1}>
346
+ No data
347
+ </text>
299
348
  ) : (
300
349
  displayDrivers.map((driver, idx) => {
301
350
  const isSelected = isFocused && idx === selectedIndex;
302
351
  const isFiltered = activeFilter === driver.id;
303
-
304
- const hotPrefix = driver.isHot ? '▲' : ' ';
305
- const selectPrefix = isSelected ? '▸' : isFiltered ? '●' : ' ';
352
+
353
+ const hotPrefix = driver.isHot ? "▲" : " ";
354
+ const selectPrefix = isSelected ? "▸" : isFiltered ? "●" : " ";
306
355
  const displayName = truncateWithEllipsis(driver.displayName, nameWidth);
307
356
  const paddedName = displayName.padEnd(nameWidth);
308
- const delta = showDelta ? formatDelta(driver.delta5m).padStart(deltaWidth) : '';
309
- const percent = showPercent ? formatPercent(driver.costShare).padStart(percentWidth) : '';
310
- const bar = showBars ? makeProgressBar(driver.costShare, barWidth) : '';
311
-
357
+ const delta = showDelta ? formatDelta(driver.delta5m).padStart(deltaWidth) : "";
358
+ const percent = showPercent ? formatPercent(driver.costShare).padStart(percentWidth) : "";
359
+ const bar = showBars ? makeProgressBar(driver.costShare, barWidth) : "";
360
+
312
361
  const textColor = isSelected ? colors.text : colors.textSubtle;
313
- const nameColor = isSelected ? colors.primary : getProviderColor(driver.providerId ?? driver.id);
314
-
362
+ const nameColor = isSelected
363
+ ? colors.primary
364
+ : getProviderColor(driver.providerId ?? driver.id);
365
+
315
366
  if (showBars) {
316
367
  return (
317
368
  <box key={driver.id} flexDirection="column" height={2}>
@@ -323,12 +374,12 @@ function TopDriversSection({
323
374
  {showPercent && <text fg={colors.textMuted}>{percent}</text>}
324
375
  </box>
325
376
  <box flexDirection="row" height={1}>
326
- <text fg={isSelected ? colors.primary : colors.textSubtle}> {bar}</text>
377
+ <text fg={isSelected ? colors.primary : colors.textSubtle}> {bar}</text>
327
378
  </box>
328
379
  </box>
329
380
  );
330
381
  }
331
-
382
+
332
383
  return (
333
384
  <box key={driver.id} flexDirection="row" height={1}>
334
385
  <text fg={driver.isHot ? colors.warning : colors.textSubtle}>{hotPrefix}</text>
@@ -339,9 +390,11 @@ function TopDriversSection({
339
390
  );
340
391
  })
341
392
  )}
342
-
393
+
343
394
  {isFocused && displayDrivers.length > 0 && (
344
- <text fg={colors.textSubtle} height={1}>↑↓ select Enter filter</text>
395
+ <text fg={colors.textSubtle} height={1}>
396
+ ↑↓ select Enter filter
397
+ </text>
345
398
  )}
346
399
  </box>
347
400
  );
@@ -353,15 +406,17 @@ interface DividerProps {
353
406
 
354
407
  function Divider({ width }: DividerProps) {
355
408
  const colors = useColors();
356
- return (
357
- <box paddingX={1} height={1}>
358
- <text fg={colors.border} height={1}>{makeDivider(width)}</text>
359
- </box>
360
- );
409
+ return (
410
+ <box paddingX={1} height={1}>
411
+ <text fg={colors.border} height={1}>
412
+ {makeDivider(width)}
413
+ </text>
414
+ </box>
415
+ );
361
416
  }
362
417
 
363
418
  interface EfficiencyInsight {
364
- type: 'cache' | 'verbosity' | 'chatty' | 'fallback';
419
+ type: "cache" | "verbosity" | "chatty" | "fallback";
365
420
  primaryLabel: string;
366
421
  primaryValue: string;
367
422
  actionLine: string;
@@ -376,13 +431,16 @@ interface EfficiencySectionProps {
376
431
 
377
432
  function EfficiencySection({ sessions, mode }: EfficiencySectionProps) {
378
433
  const colors = useColors();
379
-
434
+
380
435
  const metrics = useMemo(() => {
381
- let totalInput = 0, totalOutput = 0, totalCacheRead = 0;
382
- let totalCost = 0, totalRequests = 0;
436
+ let totalInput = 0,
437
+ totalOutput = 0,
438
+ totalCacheRead = 0;
439
+ let totalCost = 0,
440
+ totalRequests = 0;
383
441
  let hasCacheData = false;
384
442
 
385
- sessions.forEach(s => {
443
+ sessions.forEach((s) => {
386
444
  s.streams.forEach((st: AgentSessionStream) => {
387
445
  totalInput += st.tokens.input;
388
446
  totalOutput += st.tokens.output;
@@ -393,23 +451,33 @@ function EfficiencySection({ sessions, mode }: EfficiencySectionProps) {
393
451
  });
394
452
  });
395
453
 
396
- const cacheHitRate = (totalInput + totalCacheRead) > 0
397
- ? (totalCacheRead / (totalInput + totalCacheRead)) * 100 : 0;
398
- const outputShare = (totalInput + totalOutput) > 0
399
- ? (totalOutput / (totalInput + totalOutput)) * 100 : 0;
454
+ const cacheHitRate =
455
+ totalInput + totalCacheRead > 0 ? (totalCacheRead / (totalInput + totalCacheRead)) * 100 : 0;
456
+ const outputShare =
457
+ totalInput + totalOutput > 0 ? (totalOutput / (totalInput + totalOutput)) * 100 : 0;
400
458
  const costPerReq = totalRequests > 0 ? totalCost / totalRequests : 0;
401
459
 
402
- return { totalInput, totalOutput, totalCacheRead, totalCost, totalRequests, hasCacheData, cacheHitRate, outputShare, costPerReq };
460
+ return {
461
+ totalInput,
462
+ totalOutput,
463
+ totalCacheRead,
464
+ totalCost,
465
+ totalRequests,
466
+ hasCacheData,
467
+ cacheHitRate,
468
+ outputShare,
469
+ costPerReq,
470
+ };
403
471
  }, [sessions]);
404
472
 
405
473
  const insight: EfficiencyInsight = useMemo(() => {
406
474
  // Priority 1: Cache Leverage
407
475
  if (metrics.hasCacheData && metrics.totalInput > 1000 && metrics.cacheHitRate < 80) {
408
476
  return {
409
- type: 'cache',
410
- primaryLabel: 'Cache hit:',
477
+ type: "cache",
478
+ primaryLabel: "Cache hit:",
411
479
  primaryValue: `${Math.round(metrics.cacheHitRate)}%`,
412
- actionLine: 'Keep sessions open; avoid resets',
480
+ actionLine: "Keep sessions open; avoid resets",
413
481
  secondaryLabel: `$/req: ${formatCurrency(metrics.costPerReq)} Out: ${Math.round(metrics.outputShare)}%`,
414
482
  isWarning: metrics.cacheHitRate < 20,
415
483
  };
@@ -417,10 +485,10 @@ function EfficiencySection({ sessions, mode }: EfficiencySectionProps) {
417
485
  // Priority 2: Output Verbosity
418
486
  if (metrics.totalInput + metrics.totalOutput > 1000 && metrics.outputShare > 65) {
419
487
  return {
420
- type: 'verbosity',
421
- primaryLabel: 'Output share:',
488
+ type: "verbosity",
489
+ primaryLabel: "Output share:",
422
490
  primaryValue: `${Math.round(metrics.outputShare)}%`,
423
- actionLine: 'Ask for diffs; less commentary',
491
+ actionLine: "Ask for diffs; less commentary",
424
492
  secondaryLabel: `$/req: ${formatCurrency(metrics.costPerReq)}`,
425
493
  isWarning: metrics.outputShare > 75,
426
494
  };
@@ -428,59 +496,79 @@ function EfficiencySection({ sessions, mode }: EfficiencySectionProps) {
428
496
  // Priority 3: Chatty Usage
429
497
  if (metrics.totalRequests > 10 && metrics.costPerReq > 0.15) {
430
498
  return {
431
- type: 'chatty',
432
- primaryLabel: '$/req:',
499
+ type: "chatty",
500
+ primaryLabel: "$/req:",
433
501
  primaryValue: formatCurrency(metrics.costPerReq),
434
- actionLine: 'Batch asks; fewer back-and-forth',
502
+ actionLine: "Batch asks; fewer back-and-forth",
435
503
  secondaryLabel: `${metrics.totalRequests} reqs`,
436
504
  isWarning: metrics.costPerReq > 0.25,
437
505
  };
438
506
  }
439
507
  // Priority 4: Fallback
440
508
  return {
441
- type: 'fallback',
509
+ type: "fallback",
442
510
  primaryLabel: `$/req: ${formatCurrency(metrics.costPerReq)}`,
443
511
  primaryValue: `Out: ${Math.round(metrics.outputShare)}%`,
444
- actionLine: 'Prefer patches; reduce re-asking',
512
+ actionLine: "Prefer patches; reduce re-asking",
445
513
  isWarning: false,
446
514
  };
447
515
  }, [metrics]);
448
516
 
449
- const isFull = mode === 'full';
517
+ const isFull = mode === "full";
450
518
  const metricColor = insight.isWarning ? colors.warning : colors.text;
451
519
 
452
- if (insight.type === 'fallback') {
520
+ if (insight.type === "fallback") {
453
521
  const cacheLabel = metrics.hasCacheData
454
522
  ? `Cache: ${Math.round(metrics.cacheHitRate)}%`
455
- : 'Cache: n/a';
456
-
457
- return (
458
- <box flexDirection="column" paddingX={1}>
459
- <text fg={colors.textMuted} height={1}><strong>EFFICIENCY</strong></text>
460
- <box flexDirection="row" height={1}>
461
- <text fg={colors.text} height={1}>{insight.primaryLabel} {insight.primaryValue}</text>
462
- </box>
463
- <text fg={colors.textSubtle} height={1}>{'-> '}{insight.actionLine}</text>
464
- {isFull && (
465
- <text fg={colors.textMuted} height={1}>Reqs: {metrics.totalRequests} {cacheLabel}</text>
466
- )}
467
- </box>
468
- );
523
+ : "Cache: n/a";
524
+
525
+ return (
526
+ <box flexDirection="column" paddingX={1}>
527
+ <text fg={colors.textMuted} height={1}>
528
+ <strong>EFFICIENCY</strong>
529
+ </text>
530
+ <box flexDirection="row" height={1}>
531
+ <text fg={colors.text} height={1}>
532
+ {insight.primaryLabel} {insight.primaryValue}
533
+ </text>
534
+ </box>
535
+ <text fg={colors.textSubtle} height={1}>
536
+ {"-> "}
537
+ {insight.actionLine}
538
+ </text>
539
+ {isFull && (
540
+ <text fg={colors.textMuted} height={1}>
541
+ Reqs: {metrics.totalRequests} {cacheLabel}
542
+ </text>
543
+ )}
544
+ </box>
545
+ );
469
546
  }
470
547
 
471
- return (
472
- <box flexDirection="column" paddingX={1}>
473
- <text fg={colors.textMuted} height={1}><strong>EFFICIENCY</strong></text>
474
- <box flexDirection="row" height={1}>
475
- <text fg={colors.textMuted} height={1}>{insight.primaryLabel} </text>
476
- <text fg={metricColor} height={1}>{insight.primaryValue}</text>
477
- </box>
478
- <text fg={colors.textSubtle} height={1}>{'-> '}{insight.actionLine}</text>
479
- {isFull && insight.secondaryLabel && (
480
- <text fg={colors.textMuted} height={1}>{insight.secondaryLabel}</text>
481
- )}
482
- </box>
483
- );
548
+ return (
549
+ <box flexDirection="column" paddingX={1}>
550
+ <text fg={colors.textMuted} height={1}>
551
+ <strong>EFFICIENCY</strong>
552
+ </text>
553
+ <box flexDirection="row" height={1}>
554
+ <text fg={colors.textMuted} height={1}>
555
+ {insight.primaryLabel}{" "}
556
+ </text>
557
+ <text fg={metricColor} height={1}>
558
+ {insight.primaryValue}
559
+ </text>
560
+ </box>
561
+ <text fg={colors.textSubtle} height={1}>
562
+ {"-> "}
563
+ {insight.actionLine}
564
+ </text>
565
+ {isFull && insight.secondaryLabel && (
566
+ <text fg={colors.textMuted} height={1}>
567
+ {insight.secondaryLabel}
568
+ </text>
569
+ )}
570
+ </box>
571
+ );
484
572
  }
485
573
 
486
574
  export function SmartSidebar({
@@ -494,63 +582,73 @@ export function SmartSidebar({
494
582
  }: SmartSidebarProps) {
495
583
  const colors = useColors();
496
584
  const { width: terminalWidth, height: terminalHeight } = useTerminalDimensions();
497
-
585
+
498
586
  const mode = getSidebarMode(terminalWidth);
499
587
  const sidebarWidth = getSidebarWidth(mode, terminalWidth);
500
- const isFocused = focusedPanel === 'sidebar';
501
-
502
- const showBars = mode === 'full' || mode === 'compact';
588
+ const isFocused = focusedPanel === "sidebar";
589
+
590
+ const showBars = mode === "full" || mode === "compact";
503
591
  const driverRowHeight = showBars ? 2 : 1;
504
-
592
+
505
593
  const sidebarInnerHeight = terminalHeight - 24;
506
594
  const budgetRows = 3;
507
595
  const dividerRows = 1;
508
596
  const driversHeaderRow = 1;
509
597
  const efficiencyRows = 4;
510
-
511
- const minHeightForEfficiency = budgetRows + dividerRows + driversHeaderRow + (2 * driverRowHeight) + dividerRows + efficiencyRows;
598
+
599
+ const minHeightForEfficiency =
600
+ budgetRows +
601
+ dividerRows +
602
+ driversHeaderRow +
603
+ 2 * driverRowHeight +
604
+ dividerRows +
605
+ efficiencyRows;
512
606
  const showEfficiency = sidebarInnerHeight >= minHeightForEfficiency;
513
-
514
- const reservedRows = budgetRows + dividerRows + driversHeaderRow + (showEfficiency ? dividerRows + efficiencyRows : 0);
607
+
608
+ const reservedRows =
609
+ budgetRows +
610
+ dividerRows +
611
+ driversHeaderRow +
612
+ (showEfficiency ? dividerRows + efficiencyRows : 0);
515
613
  const availableForDrivers = Math.max(0, sidebarInnerHeight - reservedRows);
516
614
  const maxDrivers = Math.max(1, Math.min(10, Math.floor(availableForDrivers / driverRowHeight)));
517
-
518
- const totalCost = useMemo(() =>
519
- sessions.reduce((sum, s) => sum + (s.totalCostUsd ?? 0), 0),
520
- [sessions]
615
+
616
+ const totalCost = useMemo(
617
+ () => sessions.reduce((sum, s) => sum + (s.totalCostUsd ?? 0), 0),
618
+ [sessions],
521
619
  );
522
-
620
+
523
621
  const drivers = useMemo((): DriverStats[] => {
524
622
  const stats: Record<string, { cost: number; tokens: number; delta5m: number }> = {};
525
623
  const providerForKey: Record<string, string> = {};
526
624
  const now = Date.now();
527
625
  const fiveMinAgo = now - 5 * 60 * 1000;
528
-
529
- sessions.forEach(s => {
626
+
627
+ sessions.forEach((s) => {
530
628
  s.streams.forEach((st: AgentSessionStream) => {
531
629
  let key: string;
532
630
  switch (dimension) {
533
- case 'model':
631
+ case "model":
534
632
  key = st.modelId;
535
633
  if (!providerForKey[key]) providerForKey[key] = st.providerId;
536
634
  break;
537
- case 'project':
635
+ case "project":
538
636
  key = extractRepoName(s.projectPath);
539
637
  break;
540
- case 'agent':
638
+ case "agent":
541
639
  key = s.agentName;
542
640
  break;
543
641
  }
544
-
642
+
545
643
  if (!stats[key]) {
546
644
  stats[key] = { cost: 0, tokens: 0, delta5m: 0 };
547
645
  }
548
-
646
+
549
647
  const cost = st.costUsd ?? 0;
550
648
  const entry = stats[key]!;
551
649
  entry.cost += cost;
552
650
  entry.tokens += st.tokens.input + st.tokens.output;
553
-
651
+
554
652
  if (s.lastActivityAt >= fiveMinAgo) {
555
653
  const sessionDuration = Math.max(1, s.lastActivityAt - s.startedAt);
556
654
  const recentWindow = Math.min(5 * 60 * 1000, now - fiveMinAgo);
@@ -559,12 +657,10 @@ export function SmartSidebar({
559
657
  }
560
658
  });
561
659
  });
562
-
660
+
563
661
  return Object.entries(stats)
564
662
  .map(([id, data]): DriverStats => {
565
- const displayName = dimension === 'model'
566
- ? (id.split('/').pop() ?? id)
567
- : id;
663
+ const displayName = dimension === "model" ? (id.split("/").pop() ?? id) : id;
568
664
  return {
569
665
  id,
570
666
  ...(providerForKey[id] != null ? { providerId: providerForKey[id] } : {}),
@@ -573,36 +669,36 @@ export function SmartSidebar({
573
669
  costShare: totalCost > 0 ? (data.cost / totalCost) * 100 : 0,
574
670
  tokens: data.tokens,
575
671
  delta5m: data.delta5m,
576
- isHot: data.delta5m > 0.10,
672
+ isHot: data.delta5m > 0.1,
577
673
  };
578
674
  })
579
675
  .sort((a, b) => b.cost - a.cost);
580
676
  }, [sessions, dimension, totalCost]);
581
-
582
- if (mode === 'hidden') {
677
+
678
+ if (mode === "hidden") {
583
679
  return null;
584
680
  }
585
-
681
+
586
682
  return (
587
683
  <box
588
684
  flexDirection="column"
589
685
  width={sidebarWidth}
590
686
  border
591
- borderStyle={isFocused ? 'double' : 'single'}
687
+ borderStyle={isFocused ? "double" : "single"}
592
688
  borderColor={isFocused ? colors.primary : colors.border}
593
689
  overflow="hidden"
594
690
  >
595
691
  <box flexDirection="column" flexGrow={1}>
596
- <BudgetSection
692
+ <BudgetSection
597
693
  totalCost={totalCost}
598
694
  budgetCost={budgetCost}
599
695
  mode={mode}
600
696
  sidebarWidth={sidebarWidth}
601
697
  />
602
-
603
- {mode !== 'micro' && <Divider width={sidebarWidth} />}
604
-
605
- {mode !== 'micro' && (
698
+
699
+ {mode !== "micro" && <Divider width={sidebarWidth} />}
700
+
701
+ {mode !== "micro" && (
606
702
  <TopDriversSection
607
703
  drivers={drivers}
608
704
  dimension={dimension}
@@ -615,26 +711,32 @@ export function SmartSidebar({
615
711
  getProviderColor={getProviderColor}
616
712
  />
617
713
  )}
618
-
619
- {mode === 'micro' && drivers[0] && (
620
- <box flexDirection="row" paddingX={1} height={1}>
621
- <text fg={colors.textMuted} height={1}>Top: </text>
622
- <text fg={getProviderColor(drivers[0].providerId ?? drivers[0].id)} height={1}>
623
- {truncateWithEllipsis(drivers[0].displayName, 10)}
624
- </text>
625
- <text fg={colors.text} height={1}> {formatCurrency(drivers[0].cost)}</text>
626
- {drivers[0].isHot && <text fg={colors.warning} height={1}>▲</text>}
627
- </box>
628
- )}
714
+
715
+ {mode === "micro" && drivers[0] && (
716
+ <box flexDirection="row" paddingX={1} height={1}>
717
+ <text fg={colors.textMuted} height={1}>
718
+ Top:{" "}
719
+ </text>
720
+ <text fg={getProviderColor(drivers[0].providerId ?? drivers[0].id)} height={1}>
721
+ {truncateWithEllipsis(drivers[0].displayName, 10)}
722
+ </text>
723
+ <text fg={colors.text} height={1}>
724
+ {" "}
725
+ {formatCurrency(drivers[0].cost)}
726
+ </text>
727
+ {drivers[0].isHot && (
728
+ <text fg={colors.warning} height={1}>
729
+ ▲
730
+ </text>
731
+ )}
732
+ </box>
733
+ )}
629
734
  </box>
630
-
631
- {showEfficiency && mode !== 'micro' && (
735
+
736
+ {showEfficiency && mode !== "micro" && (
632
737
  <box flexDirection="column">
633
738
  <Divider width={sidebarWidth} />
634
- <EfficiencySection
635
- sessions={sessions}
636
- mode={mode}
637
- />
739
+ <EfficiencySection sessions={sessions} mode={mode} />
638
740
  </box>
639
741
  )}
640
742
  </box>