@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,12 +1,13 @@
1
- import { useMemo } from 'react';
2
- import { useTerminalDimensions } from '@opentui/react';
3
- import { useColors } from '../contexts/ThemeContext.tsx';
4
- import type { ProviderState } from '../contexts/PluginContext.tsx';
5
- import { InlineGauge } from './InlineGauge.tsx';
6
- import { InlineSparkline } from './InlineSparkline.tsx';
7
- import { ProviderDetailPanel } from './ProviderDetailPanel.tsx';
1
+ import { useTerminalDimensions } from "@opentui/react";
2
+ import type React from "react";
3
+ import { useMemo } from "react";
4
+ import type { ProviderState } from "../contexts/PluginContext.tsx";
5
+ import { useColors } from "../contexts/ThemeContext.tsx";
6
+ import { InlineGauge } from "./InlineGauge.tsx";
7
+ import { InlineSparkline } from "./InlineSparkline.tsx";
8
+ import { ProviderDetailPanel } from "./ProviderDetailPanel.tsx";
8
9
 
9
- type FieldState = 'actual' | 'estimated' | 'unavailable';
10
+ type FieldState = "actual" | "estimated" | "unavailable";
10
11
 
11
12
  interface FieldValue {
12
13
  text: string;
@@ -14,42 +15,55 @@ interface FieldValue {
14
15
  }
15
16
 
16
17
  function pad(str: string, len: number): string {
17
- return str.length >= len ? str.slice(0, len) : str + ' '.repeat(len - str.length);
18
+ return str.length >= len ? str.slice(0, len) : str + " ".repeat(len - str.length);
18
19
  }
19
20
 
20
21
  function padStart(str: string, len: number): string {
21
- return str.length >= len ? str.slice(0, len) : ' '.repeat(len - str.length) + str;
22
+ return str.length >= len ? str.slice(0, len) : " ".repeat(len - str.length) + str;
23
+ }
24
+
25
+ function fmtCost(val: number): string {
26
+ if (val >= 1000) return `$${(val / 1000).toFixed(1)}k`;
27
+ if (val >= 100) return `$${Math.round(val)}`;
28
+ if (val >= 10) return `$${val.toFixed(1)}`;
29
+ return `$${val.toFixed(2)}`;
22
30
  }
23
31
 
24
32
  function formatTokens(val: number | undefined | null): FieldValue {
25
- if (val === undefined || val === null) return { text: '—', state: 'unavailable' };
26
- const formatted = val >= 1_000_000 ? `${(val / 1_000_000).toFixed(1)}M` :
27
- val >= 1_000 ? `${(val / 1_000).toFixed(1)}K` :
28
- String(Math.round(val));
29
- return { text: formatted, state: 'actual' };
33
+ if (val === undefined || val === null) return { text: "—", state: "unavailable" };
34
+ const formatted =
35
+ val >= 1_000_000
36
+ ? `${(val / 1_000_000).toFixed(1)}M`
37
+ : val >= 1_000
38
+ ? `${(val / 1_000).toFixed(1)}K`
39
+ : String(Math.round(val));
40
+ return { text: formatted, state: "actual" };
30
41
  }
31
42
 
32
- type StatusGroup = 'error' | 'warn' | 'ok';
43
+ type StatusGroup = "error" | "warn" | "ok";
33
44
 
34
- function getStatusInfo(state: ProviderState, colors: ReturnType<typeof useColors>): {
45
+ function getStatusInfo(
46
+ state: ProviderState,
47
+ colors: ReturnType<typeof useColors>,
48
+ ): {
35
49
  icon: string;
36
50
  color: string;
37
51
  group: StatusGroup;
38
52
  } {
39
53
  if (state.usage?.error) {
40
- return { icon: '✗', color: colors.error, group: 'error' };
54
+ return { icon: "✗", color: colors.error, group: "error" };
41
55
  }
42
56
  if (state.usage?.limitReached) {
43
- return { icon: '⚠', color: colors.warning, group: 'warn' };
57
+ return { icon: "⚠", color: colors.warning, group: "warn" };
44
58
  }
45
59
  const maxUsage = getMaxUsage(state);
46
60
  if (maxUsage >= 80) {
47
- return { icon: '!', color: colors.warning, group: 'warn' };
61
+ return { icon: "!", color: colors.warning, group: "warn" };
48
62
  }
49
63
  if (state.loading) {
50
- return { icon: '◌', color: colors.info, group: 'ok' };
64
+ return { icon: "◌", color: colors.info, group: "ok" };
51
65
  }
52
- return { icon: '●', color: colors.success, group: 'ok' };
66
+ return { icon: "●", color: colors.success, group: "ok" };
53
67
  }
54
68
 
55
69
  function getMaxUsage(state: ProviderState): number {
@@ -64,25 +78,25 @@ function getMaxUsage(state: ProviderState): number {
64
78
  }
65
79
 
66
80
  function getNextReset(state: ProviderState): string {
67
- if (!state.usage?.limits) return '—';
81
+ if (!state.usage?.limits) return "—";
68
82
 
69
83
  const items = state.usage.limits.items ?? [];
70
84
  let resetsAt: Date | null = null;
71
85
 
72
86
  if (items.length > 0) {
73
- const withReset = items.filter(i => i.resetsAt);
87
+ const withReset = items.filter((i) => i.resetsAt);
74
88
  if (withReset.length > 0) {
75
- resetsAt = new Date(Math.min(...withReset.map(i => new Date(i.resetsAt!).getTime())));
89
+ resetsAt = new Date(Math.min(...withReset.map((i) => new Date(i.resetsAt!).getTime())));
76
90
  }
77
91
  } else if (state.usage.limits.primary?.resetsAt) {
78
92
  resetsAt = new Date(state.usage.limits.primary.resetsAt);
79
93
  }
80
94
 
81
- if (!resetsAt) return '—';
95
+ if (!resetsAt) return "—";
82
96
 
83
97
  const now = Date.now();
84
98
  const diff = resetsAt.getTime() - now;
85
- if (diff <= 0) return 'now';
99
+ if (diff <= 0) return "now";
86
100
 
87
101
  const hours = Math.floor(diff / (1000 * 60 * 60));
88
102
  const mins = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
@@ -95,31 +109,37 @@ function getNextReset(state: ProviderState): string {
95
109
  }
96
110
 
97
111
  function getCostToday(state: ProviderState): FieldValue {
98
- if (!state.usage?.cost) return { text: '—', state: 'unavailable' };
112
+ if (!state.usage?.cost) return { text: "—", state: "unavailable" };
99
113
  const cost = state.usage.cost;
114
+ if (cost.estimatedDaily) {
115
+ return { text: `~${fmtCost(cost.estimatedDaily.total)}`, state: "estimated" };
116
+ }
100
117
  if (cost.actual) {
101
- return { text: `$${cost.actual.total.toFixed(2)}`, state: 'actual' };
118
+ return { text: fmtCost(cost.actual.total), state: "actual" };
102
119
  }
103
120
  if (cost.estimated) {
104
- return { text: `~$${cost.estimated.total.toFixed(2)}`, state: 'estimated' };
121
+ return { text: `~${fmtCost(cost.estimated.total)}`, state: "estimated" };
105
122
  }
106
- return { text: '—', state: 'unavailable' };
123
+ return { text: "—", state: "unavailable" };
107
124
  }
108
125
 
109
126
  function getCostMtd(state: ProviderState): FieldValue {
110
- if (!state.usage?.cost) return { text: '—', state: 'unavailable' };
127
+ if (!state.usage?.cost) return { text: "—", state: "unavailable" };
111
128
  const cost = state.usage.cost;
129
+ if (cost.estimatedMonthly) {
130
+ return { text: `~${fmtCost(cost.estimatedMonthly.total)}`, state: "estimated" };
131
+ }
112
132
  if (cost.actual) {
113
- return { text: `$${cost.actual.total.toFixed(2)}`, state: 'actual' };
133
+ return { text: fmtCost(cost.actual.total), state: "actual" };
114
134
  }
115
135
  if (cost.estimated) {
116
- return { text: `~$${cost.estimated.total.toFixed(2)}`, state: 'estimated' };
136
+ return { text: `~${fmtCost(cost.estimated.total)}`, state: "estimated" };
117
137
  }
118
- return { text: '—', state: 'unavailable' };
138
+ return { text: "—", state: "unavailable" };
119
139
  }
120
140
 
121
141
  function getTotalTokens(state: ProviderState): FieldValue {
122
- if (!state.usage?.tokens) return { text: '—', state: 'unavailable' };
142
+ if (!state.usage?.tokens) return { text: "—", state: "unavailable" };
123
143
  const t = state.usage.tokens;
124
144
  const total = t.input + t.output + (t.cacheRead ?? 0) + (t.cacheWrite ?? 0);
125
145
  return formatTokens(total);
@@ -142,45 +162,74 @@ interface LayoutTier {
142
162
  function getLayoutTier(termWidth: number): LayoutTier {
143
163
  if (termWidth >= 140) {
144
164
  return {
145
- nameWidth: 20, planWidth: 18, gaugeWidth: 28,
146
- showPlan: true, showToday: true, showMtd: true,
147
- showTokens: true, showReset: true, showTrend: true, sparkWidth: 12,
165
+ nameWidth: 20,
166
+ planWidth: 18,
167
+ gaugeWidth: 28,
168
+ showPlan: true,
169
+ showToday: true,
170
+ showMtd: true,
171
+ showTokens: true,
172
+ showReset: true,
173
+ showTrend: true,
174
+ sparkWidth: 12,
148
175
  };
149
176
  }
150
177
  if (termWidth >= 120) {
151
178
  return {
152
- nameWidth: 18, planWidth: 16, gaugeWidth: 24,
153
- showPlan: true, showToday: true, showMtd: true,
154
- showTokens: true, showReset: true, showTrend: true, sparkWidth: 10,
179
+ nameWidth: 18,
180
+ planWidth: 16,
181
+ gaugeWidth: 24,
182
+ showPlan: true,
183
+ showToday: true,
184
+ showMtd: true,
185
+ showTokens: true,
186
+ showReset: true,
187
+ showTrend: true,
188
+ sparkWidth: 10,
155
189
  };
156
190
  }
157
191
  if (termWidth >= 100) {
158
192
  return {
159
- nameWidth: 16, planWidth: 12, gaugeWidth: 20,
160
- showPlan: true, showToday: true, showMtd: true,
161
- showTokens: true, showReset: true, showTrend: true, sparkWidth: 8,
193
+ nameWidth: 16,
194
+ planWidth: 12,
195
+ gaugeWidth: 20,
196
+ showPlan: true,
197
+ showToday: true,
198
+ showMtd: true,
199
+ showTokens: true,
200
+ showReset: true,
201
+ showTrend: true,
202
+ sparkWidth: 8,
162
203
  };
163
204
  }
164
205
  if (termWidth >= 80) {
165
206
  return {
166
- nameWidth: 14, planWidth: 10, gaugeWidth: 18,
167
- showPlan: true, showToday: true, showMtd: false,
168
- showTokens: false, showReset: true, showTrend: false, sparkWidth: 0,
207
+ nameWidth: 14,
208
+ planWidth: 10,
209
+ gaugeWidth: 18,
210
+ showPlan: true,
211
+ showToday: true,
212
+ showMtd: false,
213
+ showTokens: false,
214
+ showReset: true,
215
+ showTrend: false,
216
+ sparkWidth: 0,
169
217
  };
170
218
  }
171
219
  return {
172
- nameWidth: 12, planWidth: 0, gaugeWidth: 14,
173
- showPlan: false, showToday: true, showMtd: false,
174
- showTokens: false, showReset: false, showTrend: false, sparkWidth: 0,
220
+ nameWidth: 12,
221
+ planWidth: 0,
222
+ gaugeWidth: 14,
223
+ showPlan: false,
224
+ showToday: true,
225
+ showMtd: false,
226
+ showTokens: false,
227
+ showReset: false,
228
+ showTrend: false,
229
+ sparkWidth: 0,
175
230
  };
176
231
  }
177
232
 
178
- interface GroupedProvider {
179
- state: ProviderState;
180
- status: ReturnType<typeof getStatusInfo>;
181
- originalIndex: number;
182
- }
183
-
184
233
  export interface ProvidersListProps {
185
234
  providers: ProviderState[];
186
235
  selectedIndex: number | null;
@@ -188,51 +237,57 @@ export interface ProvidersListProps {
188
237
  expandedIndex: number | null;
189
238
  }
190
239
 
191
- export function ProvidersList({ providers, selectedIndex, onSelect, expandedIndex }: ProvidersListProps) {
240
+ export function ProvidersList({
241
+ providers,
242
+ selectedIndex,
243
+ onSelect,
244
+ expandedIndex,
245
+ }: ProvidersListProps) {
192
246
  const colors = useColors();
193
247
  const { width: termWidth } = useTerminalDimensions();
194
248
 
195
249
  const layout = getLayoutTier(termWidth);
196
250
  const {
197
- nameWidth, planWidth, gaugeWidth, sparkWidth,
198
- showPlan, showToday, showMtd, showTokens, showReset, showTrend,
251
+ nameWidth,
252
+ planWidth,
253
+ gaugeWidth,
254
+ sparkWidth,
255
+ showPlan,
256
+ showToday,
257
+ showMtd,
258
+ showTokens,
259
+ showReset,
260
+ showTrend,
199
261
  } = layout;
200
262
 
201
- const grouped = useMemo(() => {
202
- const groups: Record<StatusGroup, GroupedProvider[]> = { error: [], warn: [], ok: [] };
203
-
204
- providers.forEach((state, idx) => {
205
- const status = getStatusInfo(state, colors);
206
- groups[status.group].push({ state, status, originalIndex: idx });
207
- });
208
-
209
- return groups;
210
- }, [providers, colors]);
211
-
212
- const groupOrder: StatusGroup[] = ['error', 'warn', 'ok'];
263
+ const providerStatuses = useMemo(
264
+ () => providers.map((state) => getStatusInfo(state, colors)),
265
+ [providers, colors],
266
+ );
213
267
 
214
268
  const fieldColor = (fv: FieldValue): string => {
215
- if (fv.state === 'estimated') return colors.textMuted;
216
- if (fv.state === 'unavailable') return colors.textSubtle;
269
+ if (fv.state === "estimated") return colors.textMuted;
270
+ if (fv.state === "unavailable") return colors.textSubtle;
217
271
  return colors.text;
218
272
  };
219
273
 
220
274
  const selBg = colors.selection;
221
275
 
222
- const renderRow = (item: GroupedProvider) => {
223
- const { state, status, originalIndex } = item;
224
- const isSelected = originalIndex === selectedIndex;
276
+ const renderRow = (idx: number) => {
277
+ const state = providers[idx]!;
278
+ const status = providerStatuses[idx]!;
279
+ const isSelected = idx === selectedIndex;
225
280
  const providerColor = state.plugin.meta?.brandColor ?? colors.primary;
226
281
  const costToday = getCostToday(state);
227
282
  const costMtd = getCostMtd(state);
228
283
  const tokens = getTotalTokens(state);
229
284
  const maxUsage = getMaxUsage(state);
230
- const plan = state.usage?.planType ?? '—';
285
+ const plan = state.usage?.planType ?? "—";
231
286
  const reset = getNextReset(state);
232
287
 
233
288
  const rowBg = isSelected ? selBg : undefined;
234
289
  const bgProp = rowBg ? { bg: rowBg } : {};
235
- const railChar = isSelected ? '▌' : ' ';
290
+ const railChar = isSelected ? "▌" : " ";
236
291
 
237
292
  return (
238
293
  <box
@@ -241,11 +296,11 @@ export function ProvidersList({ providers, selectedIndex, onSelect, expandedInde
241
296
  height={1}
242
297
  flexGrow={1}
243
298
  focusable
244
- onMouseDown={() => onSelect(originalIndex)}
299
+ onMouseDown={() => onSelect(idx)}
245
300
  {...(rowBg ? { backgroundColor: rowBg } : {})}
246
301
  >
247
302
  <text width={2} fg={isSelected ? colors.primary : colors.textSubtle} height={1} {...bgProp}>
248
- {railChar + ' '}
303
+ {railChar + " "}
249
304
  </text>
250
305
  <text width={3} fg={status.color} height={1} {...bgProp}>
251
306
  {pad(`${status.icon} `, 3)}
@@ -262,17 +317,22 @@ export function ProvidersList({ providers, selectedIndex, onSelect, expandedInde
262
317
  {isSelected ? (
263
318
  <text width={gaugeWidth + 5} fg={colors.text} height={1} {...bgProp}>
264
319
  {pad(
265
- '█'.repeat(Math.round((maxUsage / 100) * gaugeWidth)) +
266
- '·'.repeat(gaugeWidth - Math.round((maxUsage / 100) * gaugeWidth)) +
267
- ' ' + (maxUsage > 0 ? `${Math.round(maxUsage)}%` : '—'),
320
+ "█".repeat(Math.round((maxUsage / 100) * gaugeWidth)) +
321
+ "·".repeat(gaugeWidth - Math.round((maxUsage / 100) * gaugeWidth)) +
322
+ " " +
323
+ (maxUsage > 0 ? `${Math.round(maxUsage)}%` : "—"),
268
324
  gaugeWidth + 5,
269
325
  )}
270
326
  </text>
271
327
  ) : (
272
328
  <>
273
- <InlineGauge percent={maxUsage > 0 ? maxUsage : null} width={gaugeWidth} color={providerColor} />
329
+ <InlineGauge
330
+ percent={maxUsage > 0 ? maxUsage : null}
331
+ width={gaugeWidth}
332
+ color={providerColor}
333
+ />
274
334
  <text width={5} fg={maxUsage >= 80 ? colors.warning : colors.text} height={1}>
275
- {padStart(maxUsage > 0 ? `${Math.round(maxUsage)}%` : '—', 5)}
335
+ {padStart(maxUsage > 0 ? `${Math.round(maxUsage)}%` : "—", 5)}
276
336
  </text>
277
337
  </>
278
338
  )}
@@ -301,11 +361,13 @@ export function ProvidersList({ providers, selectedIndex, onSelect, expandedInde
301
361
  <box width={sparkWidth + 1} height={1} overflow="hidden">
302
362
  {isSelected ? (
303
363
  <text width={sparkWidth + 1} fg={colors.textMuted} height={1} {...bgProp}>
304
- {' ' + '⣀'.repeat(sparkWidth)}
364
+ {" " + "⣀".repeat(sparkWidth)}
305
365
  </text>
306
366
  ) : (
307
367
  <>
308
- <text width={1} height={1}>{' '}</text>
368
+ <text width={1} height={1}>
369
+ {" "}
370
+ </text>
309
371
  <InlineSparkline history={state.history} width={sparkWidth} />
310
372
  </>
311
373
  )}
@@ -315,65 +377,92 @@ export function ProvidersList({ providers, selectedIndex, onSelect, expandedInde
315
377
  );
316
378
  };
317
379
 
318
- const renderSeparator = (label: string) => (
319
- <box height={1} paddingX={1} key={`sep-${label}`}>
380
+ const renderSeparator = (label: string, afterIndex: number) => (
381
+ <box height={1} paddingX={1} key={`sep-${afterIndex}-${label}`}>
320
382
  <text fg={colors.textSubtle} height={1}>
321
- {'─'.repeat(3)} {label} {'─'.repeat(Math.max(0, termWidth - label.length - 10))}
383
+ {"─".repeat(3)} {label} {"─".repeat(Math.max(0, termWidth - label.length - 10))}
322
384
  </text>
323
385
  </box>
324
386
  );
325
387
 
326
388
  const renderHeader = () => (
327
389
  <box flexDirection="row" height={1}>
328
- <text width={2} fg={colors.textMuted} height={1}>{pad('', 2)}</text>
329
- <text width={3} fg={colors.textMuted} height={1}>{pad('', 3)}</text>
330
- <text width={nameWidth} fg={colors.textMuted} height={1}>{pad('PROVIDER', nameWidth)}</text>
331
- {showPlan && <text width={planWidth} fg={colors.textMuted} height={1}>{pad('PLAN', planWidth)}</text>}
332
- <text width={gaugeWidth + 5} fg={colors.textMuted} height={1}>{pad('HEADROOM', gaugeWidth + 5)}</text>
333
- {showToday && <text width={9} fg={colors.textMuted} height={1}>{padStart('$TODAY', 9)}</text>}
334
- {showMtd && <text width={9} fg={colors.textMuted} height={1}>{padStart('$MTD', 9)}</text>}
335
- {showTokens && <text width={8} fg={colors.textMuted} height={1}>{padStart('TOKENS', 8)}</text>}
336
- {showReset && <text width={9} fg={colors.textMuted} height={1}>{padStart('RESET', 9)}</text>}
337
- {showTrend && <text width={sparkWidth + 1} fg={colors.textMuted} height={1}>{' ' + pad('TREND', sparkWidth)}</text>}
390
+ <text width={2} fg={colors.textMuted} height={1}>
391
+ {pad("", 2)}
392
+ </text>
393
+ <text width={3} fg={colors.textMuted} height={1}>
394
+ {pad("", 3)}
395
+ </text>
396
+ <text width={nameWidth} fg={colors.textMuted} height={1}>
397
+ {pad("PROVIDER", nameWidth)}
398
+ </text>
399
+ {showPlan && (
400
+ <text width={planWidth} fg={colors.textMuted} height={1}>
401
+ {pad("PLAN", planWidth)}
402
+ </text>
403
+ )}
404
+ <text width={gaugeWidth + 5} fg={colors.textMuted} height={1}>
405
+ {pad("HEADROOM", gaugeWidth + 5)}
406
+ </text>
407
+ {showToday && (
408
+ <text width={9} fg={colors.textMuted} height={1}>
409
+ {padStart("$TODAY", 9)}
410
+ </text>
411
+ )}
412
+ {showMtd && (
413
+ <text width={9} fg={colors.textMuted} height={1}>
414
+ {padStart("$MTD", 9)}
415
+ </text>
416
+ )}
417
+ {showTokens && (
418
+ <text width={8} fg={colors.textMuted} height={1}>
419
+ {padStart("TOKENS", 8)}
420
+ </text>
421
+ )}
422
+ {showReset && (
423
+ <text width={9} fg={colors.textMuted} height={1}>
424
+ {padStart("RESET", 9)}
425
+ </text>
426
+ )}
427
+ {showTrend && (
428
+ <text width={sparkWidth + 1} fg={colors.textMuted} height={1}>
429
+ {" " + pad("TREND", sparkWidth)}
430
+ </text>
431
+ )}
338
432
  </box>
339
433
  );
340
434
 
341
- const rows: JSX.Element[] = [];
342
- let isFirst = true;
435
+ const rows: React.ReactNode[] = [];
436
+ let currentGroup: StatusGroup | null = null;
343
437
 
344
- for (const group of groupOrder) {
345
- const items = grouped[group];
346
- if (items.length === 0) continue;
438
+ for (let idx = 0; idx < providers.length; idx++) {
439
+ const status = providerStatuses[idx]!;
347
440
 
348
- if (!isFirst) {
349
- const label = group === 'error' ? 'ERRORS' : group === 'warn' ? 'WARNINGS' : 'OK';
350
- rows.push(renderSeparator(label));
351
- } else if (group !== 'ok' && items.length > 0) {
352
- const label = group === 'error' ? 'ERRORS' : 'WARNINGS';
353
- rows.push(renderSeparator(label));
354
- }
355
-
356
- for (const item of items) {
357
- rows.push(renderRow(item));
358
- if (expandedIndex !== null && item.originalIndex === expandedIndex) {
359
- rows.push(
360
- <box key={`detail-${item.state.plugin.id}`} paddingX={1}>
361
- <ProviderDetailPanel provider={item.state} />
362
- </box>
363
- );
441
+ if (status.group !== currentGroup) {
442
+ const label =
443
+ status.group === "error" ? "ERRORS" : status.group === "warn" ? "WARNINGS" : "OK";
444
+ if (currentGroup !== null || status.group !== "ok") {
445
+ rows.push(renderSeparator(label, idx));
364
446
  }
447
+ currentGroup = status.group;
365
448
  }
366
449
 
367
- isFirst = false;
450
+ rows.push(renderRow(idx));
451
+
452
+ if (expandedIndex !== null && idx === expandedIndex) {
453
+ rows.push(
454
+ <box key={`detail-${providers[idx]!.plugin.id}`} paddingX={1}>
455
+ <ProviderDetailPanel provider={providers[idx]!} />
456
+ </box>,
457
+ );
458
+ }
368
459
  }
369
460
 
370
461
  return (
371
462
  <box flexDirection="column" flexGrow={1}>
372
463
  {renderHeader()}
373
464
  <scrollbox flexGrow={1}>
374
- <box flexDirection="column">
375
- {rows}
376
- </box>
465
+ <box flexDirection="column">{rows}</box>
377
466
  </scrollbox>
378
467
  </box>
379
468
  );