@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,31 +1,31 @@
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 { LimitGauge } from './LimitGauge.tsx';
6
- import { usePulse } from '../hooks/usePulse.ts';
1
+ import { useTerminalDimensions } from "@opentui/react";
2
+ import { useMemo } from "react";
3
+ import { useConfig } from "../contexts/ConfigContext.tsx";
4
+ import { useColors } from "../contexts/ThemeContext.tsx";
5
+ import { usePulse } from "../hooks/usePulse.ts";
6
+ import { LimitGauge } from "./LimitGauge.tsx";
7
7
 
8
8
  function interpolatePulseColor(intensity: number, baseColor: string, dimColor: string): string {
9
9
  const dimAmount = 0.4;
10
10
  const t = intensity * dimAmount;
11
-
11
+
12
12
  const parseHex = (hex: string) => {
13
- const h = hex.replace('#', '');
13
+ const h = hex.replace("#", "");
14
14
  return {
15
15
  r: parseInt(h.substring(0, 2), 16),
16
16
  g: parseInt(h.substring(2, 4), 16),
17
17
  b: parseInt(h.substring(4, 6), 16),
18
18
  };
19
19
  };
20
-
20
+
21
21
  const base = parseHex(baseColor);
22
22
  const dim = parseHex(dimColor);
23
-
23
+
24
24
  const r = Math.round(base.r + (dim.r - base.r) * t);
25
25
  const g = Math.round(base.g + (dim.g - base.g) * t);
26
26
  const b = Math.round(base.b + (dim.b - base.b) * t);
27
-
28
- return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
27
+
28
+ return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
29
29
  }
30
30
 
31
31
  interface ProviderData {
@@ -44,7 +44,7 @@ interface BudgetData {
44
44
  totalCost: number;
45
45
  budgetCost: number;
46
46
  limit: number | null;
47
- budgetType: 'daily' | 'weekly' | 'monthly' | 'none';
47
+ budgetType: "daily" | "weekly" | "monthly" | "none";
48
48
  budgetTypeLabel: string;
49
49
  warningPercent: number;
50
50
  criticalPercent: number;
@@ -58,31 +58,31 @@ interface ProviderLimitsPanelProps {
58
58
  showBudgetBar?: boolean;
59
59
  }
60
60
 
61
- type LayoutMode = 'hidden' | 'compact' | 'normal' | 'wide';
61
+ type LayoutMode = "hidden" | "compact" | "normal" | "wide";
62
62
 
63
63
  function getLayoutMode(width: number, height: number): LayoutMode {
64
- if (height < 24) return 'hidden';
65
- if (height < 30) return 'compact';
66
- if (width >= 140) return 'wide';
67
- return 'normal';
64
+ if (height < 24) return "hidden";
65
+ if (height < 30) return "compact";
66
+ if (width >= 140) return "wide";
67
+ return "normal";
68
68
  }
69
69
 
70
70
  function sortByUrgency(
71
- providers: ProviderData[],
72
- warningThreshold: number,
73
- criticalThreshold: number
71
+ providers: ProviderData[],
72
+ warningThreshold: number,
73
+ criticalThreshold: number,
74
74
  ): ProviderData[] {
75
75
  return [...providers].sort((a, b) => {
76
76
  const aIsCritical = a.usedPercent >= criticalThreshold;
77
77
  const bIsCritical = b.usedPercent >= criticalThreshold;
78
78
  const aIsWarning = a.usedPercent >= warningThreshold;
79
79
  const bIsWarning = b.usedPercent >= warningThreshold;
80
-
80
+
81
81
  if (aIsCritical && !bIsCritical) return -1;
82
82
  if (!aIsCritical && bIsCritical) return 1;
83
83
  if (aIsWarning && !bIsWarning) return -1;
84
84
  if (!aIsWarning && bIsWarning) return 1;
85
-
85
+
86
86
  return b.usedPercent - a.usedPercent;
87
87
  });
88
88
  }
@@ -97,12 +97,12 @@ function formatCurrency(val: number): string {
97
97
  function makeProgressBar(percent: number, width: number): string {
98
98
  const clamped = Math.max(0, Math.min(100, percent));
99
99
  const filledWidth = Math.round((clamped / 100) * width);
100
- return '█'.repeat(filledWidth) + '·'.repeat(width - filledWidth);
100
+ return "█".repeat(filledWidth) + "·".repeat(width - filledWidth);
101
101
  }
102
102
 
103
- export function ProviderLimitsPanel({
104
- providers,
105
- focused = false,
103
+ export function ProviderLimitsPanel({
104
+ providers,
105
+ focused = false,
106
106
  selectedIndex = 0,
107
107
  budget,
108
108
  showBudgetBar = false,
@@ -110,24 +110,26 @@ export function ProviderLimitsPanel({
110
110
  const colors = useColors();
111
111
  const { config } = useConfig();
112
112
  const { width: termWidth, height: termHeight } = useTerminalDimensions();
113
-
113
+
114
114
  const warningThreshold = config.alerts.warningPercent;
115
115
  const criticalThreshold = config.alerts.criticalPercent;
116
-
117
- const hasBudgetLimit = !!(budget?.limit && budget.budgetType !== 'none');
118
- const budgetPercent = hasBudgetLimit && budget?.limit ? (budget.budgetCost / budget.limit) * 100 : 0;
116
+
117
+ const hasBudgetLimit = !!(budget?.limit && budget.budgetType !== "none");
118
+ const budgetPercent =
119
+ hasBudgetLimit && budget?.limit ? (budget.budgetCost / budget.limit) * 100 : 0;
119
120
  const isBudgetCritical = hasBudgetLimit && budget && budgetPercent >= budget.criticalPercent;
120
- const isBudgetWarning = hasBudgetLimit && budget && budgetPercent >= budget.warningPercent && !isBudgetCritical;
121
-
121
+ const isBudgetWarning =
122
+ hasBudgetLimit && budget && budgetPercent >= budget.warningPercent && !isBudgetCritical;
123
+
122
124
  const criticalPulseStep = usePulse({ enabled: !!isBudgetCritical, intervalMs: 80 });
123
125
  const warningPulseStep = usePulse({ enabled: !!isBudgetWarning, intervalMs: 200 });
124
-
125
- const pulseIntensity = isBudgetCritical
126
- ? Math.abs(Math.sin(criticalPulseStep * 0.3))
127
- : isBudgetWarning
126
+
127
+ const pulseIntensity = isBudgetCritical
128
+ ? Math.abs(Math.sin(criticalPulseStep * 0.3))
129
+ : isBudgetWarning
128
130
  ? Math.abs(Math.sin(warningPulseStep * 0.25))
129
131
  : 0;
130
-
132
+
131
133
  const getBudgetColor = () => {
132
134
  if (!budget?.limit) return colors.textMuted;
133
135
  if (isBudgetCritical) {
@@ -138,37 +140,44 @@ export function ProviderLimitsPanel({
138
140
  }
139
141
  return colors.success;
140
142
  };
141
-
143
+
142
144
  const budgetColor = getBudgetColor();
143
-
145
+
144
146
  const layoutMode = getLayoutMode(termWidth, termHeight);
145
147
  const sortedProviders = useMemo(
146
- () => sortByUrgency(providers, warningThreshold, criticalThreshold),
147
- [providers, warningThreshold, criticalThreshold]
148
+ () => sortByUrgency(providers, warningThreshold, criticalThreshold),
149
+ [providers, warningThreshold, criticalThreshold],
148
150
  );
149
-
150
- if (layoutMode === 'hidden' || sortedProviders.length === 0) {
151
+
152
+ if (layoutMode === "hidden" || sortedProviders.length === 0) {
151
153
  return null;
152
154
  }
153
-
155
+
154
156
  const safeSelectedIndex = Math.min(selectedIndex, sortedProviders.length - 1);
155
-
156
- if (layoutMode === 'compact') {
157
+
158
+ if (layoutMode === "compact") {
157
159
  const baseMaxShow = 4;
158
160
  const hasMore = sortedProviders.length > baseMaxShow;
159
161
  const maxShow = hasMore ? baseMaxShow - 1 : baseMaxShow;
160
- const startIndex = Math.max(0, Math.min(safeSelectedIndex - maxShow + 1, sortedProviders.length - maxShow));
162
+ const startIndex = Math.max(
163
+ 0,
164
+ Math.min(safeSelectedIndex - maxShow + 1, sortedProviders.length - maxShow),
165
+ );
161
166
  const showLeftArrow = hasMore && startIndex > 0;
162
167
  const showRightArrow = hasMore && startIndex + maxShow < sortedProviders.length;
163
168
  const shown = sortedProviders.slice(startIndex, startIndex + maxShow);
164
169
  const remaining = sortedProviders.length - startIndex - maxShow;
165
-
170
+
166
171
  return (
167
172
  <box flexDirection="column" height={showBudgetBar && budget ? 2 : 1} overflow="hidden">
168
- <box flexDirection="row" height={1} paddingX={1} gap={1} overflow="hidden">
169
- <text fg={colors.textMuted} height={1}>LIMITS:</text>
173
+ <box flexDirection="row" height={1} paddingX={1} gap={1} overflow="hidden">
174
+ <text fg={colors.textMuted} height={1}>
175
+ LIMITS:
176
+ </text>
170
177
  {showLeftArrow && (
171
- <text fg={colors.primary} height={1}>◀</text>
178
+ <text fg={colors.primary} height={1}>
179
+ ◀
180
+ </text>
172
181
  )}
173
182
  {shown.map((p, idx) => (
174
183
  <LimitGauge
@@ -178,61 +187,80 @@ export function ProviderLimitsPanel({
178
187
  color={p.color}
179
188
  {...(p.error ? { error: p.error } : {})}
180
189
  compact={true}
181
- selected={focused && (startIndex + idx) === safeSelectedIndex}
190
+ selected={focused && startIndex + idx === safeSelectedIndex}
182
191
  warningThreshold={warningThreshold}
183
192
  criticalThreshold={criticalThreshold}
184
193
  />
185
194
  ))}
186
195
  {showRightArrow && (
187
- <text fg={colors.primary} height={1}>▶ +{remaining}</text>
196
+ <text fg={colors.primary} height={1}>
197
+ ▶ +{remaining}
198
+ </text>
188
199
  )}
189
200
  </box>
190
201
  {showBudgetBar && budget && (
191
202
  <box flexDirection="row" height={1} paddingX={1} gap={1} overflow="hidden">
192
- <text fg={colors.textMuted} height={1}>{budget.budgetType === 'none' ? 'TOTAL:' : `${budget.budgetTypeLabel.toUpperCase()}:`}</text>
203
+ <text fg={colors.textMuted} height={1}>
204
+ {budget.budgetType === "none" ? "TOTAL:" : `${budget.budgetTypeLabel.toUpperCase()}:`}
205
+ </text>
193
206
  {hasBudgetLimit ? (
194
207
  <>
195
- <text fg={budgetColor} height={1}>{makeProgressBar(budgetPercent, 12)}</text>
196
- <text fg={budgetColor} height={1}>{Math.round(budgetPercent)}%</text>
197
- <text fg={colors.text} height={1}>{formatCurrency(budget.budgetCost)}</text>
198
- <text fg={colors.textMuted} height={1}>/{formatCurrency(budget.limit!)}</text>
208
+ <text fg={budgetColor} height={1}>
209
+ {makeProgressBar(budgetPercent, 12)}
210
+ </text>
211
+ <text fg={budgetColor} height={1}>
212
+ {Math.round(budgetPercent)}%
213
+ </text>
214
+ <text fg={colors.text} height={1}>
215
+ {formatCurrency(budget.budgetCost)}
216
+ </text>
217
+ <text fg={colors.textMuted} height={1}>
218
+ /{formatCurrency(budget.limit!)}
219
+ </text>
199
220
  </>
200
221
  ) : (
201
- <text fg={colors.text} height={1}>{formatCurrency(budget.totalCost)}</text>
222
+ <text fg={colors.text} height={1}>
223
+ {formatCurrency(budget.totalCost)}
224
+ </text>
202
225
  )}
203
226
  </box>
204
227
  )}
205
228
  </box>
206
229
  );
207
230
  }
208
-
209
- if (layoutMode === 'wide') {
231
+
232
+ if (layoutMode === "wide") {
210
233
  const baseMaxShow = 5;
211
234
  const hasMore = sortedProviders.length > baseMaxShow;
212
235
  const maxShow = hasMore ? baseMaxShow - 1 : baseMaxShow;
213
- const startIndex = Math.max(0, Math.min(safeSelectedIndex - maxShow + 1, sortedProviders.length - maxShow));
236
+ const startIndex = Math.max(
237
+ 0,
238
+ Math.min(safeSelectedIndex - maxShow + 1, sortedProviders.length - maxShow),
239
+ );
214
240
  const showLeftArrow = hasMore && startIndex > 0;
215
241
  const showRightArrow = hasMore && startIndex + maxShow < sortedProviders.length;
216
242
  const shown = sortedProviders.slice(startIndex, startIndex + maxShow);
217
-
243
+
218
244
  return (
219
- <box
220
- flexDirection="column"
221
- border
222
- borderStyle={focused ? 'double' : 'single'}
223
- borderColor={focused ? colors.primary : colors.border}
224
- overflow="hidden"
225
- height={4}
245
+ <box
246
+ flexDirection="column"
247
+ border
248
+ borderStyle={focused ? "double" : "single"}
249
+ borderColor={focused ? colors.primary : colors.border}
250
+ overflow="hidden"
251
+ height={4}
226
252
  flexShrink={0}
227
253
  >
228
254
  <box flexDirection="row" paddingLeft={2} height={1}>
229
255
  <text fg={colors.textMuted} height={1}>
230
- PROVIDER LIMITS {focused ? '(←→ navigate, Tab exit)' : ''}
256
+ PROVIDER LIMITS {focused ? "(←→ navigate, Tab exit)" : ""}
231
257
  </text>
232
258
  </box>
233
259
  <box flexDirection="row" gap={3} overflow="hidden" height={1} paddingLeft={2}>
234
260
  {showLeftArrow && (
235
- <text fg={colors.primary} height={1}>◀</text>
261
+ <text fg={colors.primary} height={1}>
262
+ ◀
263
+ </text>
236
264
  )}
237
265
  {shown.map((p, idx) => (
238
266
  <LimitGauge
@@ -244,45 +272,52 @@ export function ProviderLimitsPanel({
244
272
  {...(p.resetTime ? { resetTime: p.resetTime } : {})}
245
273
  labelWidth={14}
246
274
  barWidth={12}
247
- selected={focused && (startIndex + idx) === safeSelectedIndex}
275
+ selected={focused && startIndex + idx === safeSelectedIndex}
248
276
  warningThreshold={warningThreshold}
249
277
  criticalThreshold={criticalThreshold}
250
278
  />
251
279
  ))}
252
280
  {showRightArrow && (
253
- <text fg={colors.primary} height={1}>▶</text>
281
+ <text fg={colors.primary} height={1}>
282
+ ▶
283
+ </text>
254
284
  )}
255
285
  </box>
256
286
  </box>
257
287
  );
258
288
  }
259
-
289
+
260
290
  const baseMaxShow = 4;
261
291
  const hasMore = sortedProviders.length > baseMaxShow;
262
292
  const maxShow = hasMore ? baseMaxShow - 1 : baseMaxShow;
263
- const startIndex = Math.max(0, Math.min(safeSelectedIndex - maxShow + 1, sortedProviders.length - maxShow));
293
+ const startIndex = Math.max(
294
+ 0,
295
+ Math.min(safeSelectedIndex - maxShow + 1, sortedProviders.length - maxShow),
296
+ );
264
297
  const showLeftArrow = hasMore && startIndex > 0;
265
298
  const showRightArrow = hasMore && startIndex + maxShow < sortedProviders.length;
266
299
  const shown = sortedProviders.slice(startIndex, startIndex + maxShow);
267
-
300
+
268
301
  return (
269
- <box
270
- flexDirection="column"
271
- border
272
- borderStyle={focused ? 'double' : 'single'}
273
- borderColor={focused ? colors.primary : colors.border}
274
- overflow="hidden"
275
- height={4}
302
+ <box
303
+ flexDirection="column"
304
+ border
305
+ borderStyle={focused ? "double" : "single"}
306
+ borderColor={focused ? colors.primary : colors.border}
307
+ overflow="hidden"
308
+ height={4}
276
309
  flexShrink={0}
277
310
  >
278
311
  <box flexDirection="row" paddingLeft={2} height={1}>
279
312
  <text fg={colors.textMuted} height={1}>
280
- PROVIDER LIMITS {focused ? '(←→ navigate, Tab exit)' : ''}
313
+ PROVIDER LIMITS {focused ? "(←→ navigate, Tab exit)" : ""}
281
314
  </text>
282
315
  </box>
283
316
  <box flexDirection="row" gap={2} overflow="hidden" height={1} paddingLeft={2}>
284
317
  {showLeftArrow && (
285
- <text fg={colors.primary} height={1}>◀</text>
318
+ <text fg={colors.primary} height={1}>
319
+ ◀
320
+ </text>
286
321
  )}
287
322
  {shown.map((p, idx) => (
288
323
  <LimitGauge
@@ -294,13 +329,15 @@ export function ProviderLimitsPanel({
294
329
  {...(p.resetTime ? { resetTime: p.resetTime } : {})}
295
330
  labelWidth={12}
296
331
  barWidth={10}
297
- selected={focused && (startIndex + idx) === safeSelectedIndex}
332
+ selected={focused && startIndex + idx === safeSelectedIndex}
298
333
  warningThreshold={warningThreshold}
299
334
  criticalThreshold={criticalThreshold}
300
335
  />
301
336
  ))}
302
337
  {showRightArrow && (
303
- <text fg={colors.primary} height={1}>▶</text>
338
+ <text fg={colors.primary} height={1}>
339
+ ▶
340
+ </text>
304
341
  )}
305
342
  </box>
306
343
  </box>