@tokentop/ttop 0.2.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/README.md +2 -5
  2. package/package.json +8 -5
  3. package/src/agents/aggregator.ts +39 -19
  4. package/src/agents/costing.ts +11 -9
  5. package/src/agents/index.ts +7 -9
  6. package/src/agents/types.ts +2 -2
  7. package/src/cli.ts +28 -23
  8. package/src/config/schema.test.ts +42 -40
  9. package/src/config/schema.ts +26 -26
  10. package/src/demo/simulator.ts +161 -122
  11. package/src/plugins/agents/index.ts +2 -2
  12. package/src/plugins/auth-sources.ts +42 -38
  13. package/src/plugins/lifecycle.ts +42 -38
  14. package/src/plugins/loader.test.ts +49 -49
  15. package/src/plugins/loader.ts +67 -63
  16. package/src/plugins/notification-bus.ts +21 -16
  17. package/src/plugins/notifications/index.ts +2 -2
  18. package/src/plugins/notifications/terminal-bell.ts +36 -23
  19. package/src/plugins/notifications/visual-flash.ts +31 -19
  20. package/src/plugins/npm-installer.ts +23 -25
  21. package/src/plugins/plugin-context-factory.ts +10 -10
  22. package/src/plugins/plugin-host.ts +12 -5
  23. package/src/plugins/providers/anthropic.ts +64 -57
  24. package/src/plugins/providers/antigravity.ts +81 -69
  25. package/src/plugins/providers/codex.ts +42 -42
  26. package/src/plugins/providers/gemini.ts +106 -87
  27. package/src/plugins/providers/github-copilot.ts +49 -45
  28. package/src/plugins/providers/index.ts +10 -12
  29. package/src/plugins/providers/minimax.ts +50 -44
  30. package/src/plugins/providers/openai-api.ts +82 -62
  31. package/src/plugins/providers/opencode-zen.ts +46 -39
  32. package/src/plugins/providers/perplexity.ts +55 -46
  33. package/src/plugins/providers/zai.ts +68 -57
  34. package/src/plugins/registry.ts +80 -71
  35. package/src/plugins/sandbox-guard.test.ts +45 -48
  36. package/src/plugins/sandbox-guard.ts +20 -25
  37. package/src/plugins/sandbox.ts +26 -48
  38. package/src/plugins/themes/catppuccin-latte.ts +29 -29
  39. package/src/plugins/themes/catppuccin-mocha.ts +29 -29
  40. package/src/plugins/themes/claude-code.ts +29 -29
  41. package/src/plugins/themes/dracula.ts +29 -29
  42. package/src/plugins/themes/github-light.ts +29 -29
  43. package/src/plugins/themes/gruvbox-dark.ts +29 -29
  44. package/src/plugins/themes/gruvbox-light.ts +29 -29
  45. package/src/plugins/themes/index.ts +15 -15
  46. package/src/plugins/themes/kanagawa.ts +29 -29
  47. package/src/plugins/themes/nord.ts +29 -29
  48. package/src/plugins/themes/one-dark.ts +29 -29
  49. package/src/plugins/themes/opencode.ts +31 -31
  50. package/src/plugins/themes/rose-pine-dawn.ts +29 -29
  51. package/src/plugins/themes/rose-pine.ts +29 -29
  52. package/src/plugins/themes/solarized-light.ts +29 -29
  53. package/src/plugins/themes/tokyo-night.ts +29 -29
  54. package/src/plugins/types/agent.ts +5 -5
  55. package/src/plugins/types/base.ts +35 -24
  56. package/src/plugins/types/index.ts +9 -9
  57. package/src/plugins/types/notification.ts +12 -12
  58. package/src/plugins/types/provider.ts +12 -8
  59. package/src/plugins/types/theme.ts +31 -23
  60. package/src/plugins/update-checker.ts +6 -6
  61. package/src/pricing/estimator.test.ts +39 -44
  62. package/src/pricing/estimator.ts +7 -13
  63. package/src/pricing/fallback.test.ts +30 -34
  64. package/src/pricing/fallback.ts +54 -27
  65. package/src/pricing/index.ts +18 -12
  66. package/src/pricing/models-dev.ts +6 -6
  67. package/src/storage/database.ts +56 -46
  68. package/src/storage/db.ts +12 -12
  69. package/src/storage/index.ts +44 -45
  70. package/src/storage/migrations/index.ts +2 -2
  71. package/src/storage/paths.ts +13 -13
  72. package/src/storage/persistence-service.ts +206 -0
  73. package/src/storage/repos/agentSessions.ts +33 -23
  74. package/src/storage/repos/providerSnapshots.ts +5 -5
  75. package/src/storage/repos/usageEvents.ts +40 -28
  76. package/src/storage/retention.ts +146 -0
  77. package/src/storage/types.ts +5 -5
  78. package/src/tui/App.tsx +180 -137
  79. package/src/tui/components/CommandPalette.tsx +46 -34
  80. package/src/tui/components/DebugConsole.tsx +39 -33
  81. package/src/tui/components/DebugPanel.tsx +314 -183
  82. package/src/tui/components/FooterHints.tsx +2 -2
  83. package/src/tui/components/GhostProviderCard.tsx +37 -35
  84. package/src/tui/components/Header.tsx +68 -35
  85. package/src/tui/components/HelpOverlay.tsx +99 -29
  86. package/src/tui/components/InlineGauge.tsx +10 -7
  87. package/src/tui/components/InlineSparkline.tsx +17 -11
  88. package/src/tui/components/KpiStrip.tsx +128 -98
  89. package/src/tui/components/LimitGauge.tsx +96 -59
  90. package/src/tui/components/ModalBackdrop.tsx +4 -4
  91. package/src/tui/components/ProviderAggregateStrip.tsx +69 -38
  92. package/src/tui/components/ProviderCard.tsx +232 -226
  93. package/src/tui/components/ProviderDetailPanel.tsx +133 -58
  94. package/src/tui/components/ProviderLimitsPanel.tsx +126 -89
  95. package/src/tui/components/ProvidersList.tsx +215 -126
  96. package/src/tui/components/SessionDetailsDrawer.tsx +191 -94
  97. package/src/tui/components/SessionsTable.tsx +320 -169
  98. package/src/tui/components/SettingsModal.tsx +593 -269
  99. package/src/tui/components/SidebarBreakdown.tsx +30 -16
  100. package/src/tui/components/Skeleton.tsx +24 -17
  101. package/src/tui/components/SmartSidebar.tsx +360 -258
  102. package/src/tui/components/Sparkline.tsx +64 -52
  103. package/src/tui/components/Spinner.tsx +3 -3
  104. package/src/tui/components/StatusBar.tsx +27 -27
  105. package/src/tui/components/ThemePicker.tsx +67 -66
  106. package/src/tui/components/Toast.tsx +9 -6
  107. package/src/tui/components/UsageGauge.tsx +10 -13
  108. package/src/tui/components/index.ts +5 -5
  109. package/src/tui/contexts/AgentSessionContext.tsx +164 -151
  110. package/src/tui/contexts/ConfigContext.tsx +15 -13
  111. package/src/tui/contexts/DashboardRuntimeContext.tsx +26 -26
  112. package/src/tui/contexts/DemoModeContext.tsx +22 -15
  113. package/src/tui/contexts/DrawerContext.tsx +11 -9
  114. package/src/tui/contexts/InputContext.tsx +2 -2
  115. package/src/tui/contexts/LogContext.tsx +62 -47
  116. package/src/tui/contexts/PluginContext.tsx +238 -183
  117. package/src/tui/contexts/RealTimeActivityContext.tsx +70 -36
  118. package/src/tui/contexts/StorageContext.tsx +95 -149
  119. package/src/tui/contexts/ThemeContext.tsx +21 -16
  120. package/src/tui/contexts/TimeWindowContext.tsx +72 -61
  121. package/src/tui/contexts/ToastContext.tsx +10 -7
  122. package/src/tui/contexts/index.ts +9 -5
  123. package/src/tui/createApp.tsx +8 -12
  124. package/src/tui/debug/captureFrame.ts +25 -28
  125. package/src/tui/debug/snapshot.tsx +179 -162
  126. package/src/tui/driver/assertions.ts +30 -22
  127. package/src/tui/driver/cli.ts +231 -221
  128. package/src/tui/driver/coverage.ts +27 -44
  129. package/src/tui/driver/demo-snapshot.ts +33 -33
  130. package/src/tui/driver/diff.ts +26 -32
  131. package/src/tui/driver/driver.ts +43 -48
  132. package/src/tui/driver/index.ts +36 -40
  133. package/src/tui/driver/recorder.ts +37 -48
  134. package/src/tui/driver/test-workflow.ts +51 -51
  135. package/src/tui/driver/test.ts +25 -25
  136. package/src/tui/hooks/index.ts +24 -20
  137. package/src/tui/hooks/useAnimatedValue.ts +13 -18
  138. package/src/tui/hooks/useDashboardKeyboard.ts +118 -98
  139. package/src/tui/hooks/useDashboardState.ts +26 -24
  140. package/src/tui/hooks/useEmaActivity.ts +82 -74
  141. package/src/tui/hooks/useEntranceAnimation.ts +11 -11
  142. package/src/tui/hooks/useExitAnimation.ts +7 -3
  143. package/src/tui/hooks/usePulse.ts +4 -4
  144. package/src/tui/hooks/useSafeRenderer.ts +5 -5
  145. package/src/tui/hooks/useSessionEnrichedProviders.ts +150 -0
  146. package/src/tui/hooks/useValueFlash.ts +10 -14
  147. package/src/tui/index.tsx +29 -6
  148. package/src/tui/shutdown.ts +19 -0
  149. package/src/tui/utils/providerColor.ts +1 -1
  150. package/src/tui/views/Dashboard.tsx +164 -102
  151. package/src/tui/views/HistoricalTrendsView.tsx +579 -250
  152. package/src/tui/views/ProjectsView.tsx +495 -243
  153. package/src/tui/views/RealTimeDashboard.tsx +176 -116
  154. package/src/tui/views/SettingsView.tsx +225 -189
  155. package/src/tui/views/index.ts +2 -2
  156. package/src/types/opentui-test-utils.d.ts +7 -7
  157. package/src/utils/clipboard.ts +29 -29
  158. package/src/utils/currency.test.ts +53 -53
  159. package/src/utils/currency.ts +35 -32
  160. package/src/version.ts +1 -1
@@ -1,12 +1,12 @@
1
- import { useTerminalDimensions } from '@opentui/react';
2
- import { useColors } from '../contexts/ThemeContext.tsx';
3
- import { useTimeWindow, type TimeWindow } from '../contexts/TimeWindowContext.tsx';
4
- import { useConfig } from '../contexts/ConfigContext.tsx';
5
- import { Sparkline } from './Sparkline.tsx';
6
- import { useValueFlash, interpolateColor } from '../hooks/useValueFlash.ts';
7
- import { useAnimatedValue } from '../hooks/useAnimatedValue.ts';
1
+ import { useTerminalDimensions } from "@opentui/react";
2
+ import { useConfig } from "../contexts/ConfigContext.tsx";
3
+ import { useColors } from "../contexts/ThemeContext.tsx";
4
+ import { type TimeWindow, useTimeWindow } from "../contexts/TimeWindowContext.tsx";
5
+ import { useAnimatedValue } from "../hooks/useAnimatedValue.ts";
6
+ import { interpolateColor, useValueFlash } from "../hooks/useValueFlash.ts";
7
+ import { Sparkline } from "./Sparkline.tsx";
8
8
 
9
- type MetricType = 'cost' | 'tokens' | 'requests' | 'rate' | 'default';
9
+ type MetricType = "cost" | "tokens" | "requests" | "rate" | "default";
10
10
 
11
11
  interface KPICardProps {
12
12
  title: string;
@@ -18,13 +18,13 @@ interface KPICardProps {
18
18
  }
19
19
 
20
20
  const TIME_WINDOW_EXPANDED: Record<TimeWindow, string> = {
21
- '5m': 'Last 5 Minutes',
22
- '15m': 'Last 15 Minutes',
23
- '1h': 'Last Hour',
24
- '24h': 'Last 24 Hours',
25
- '7d': 'Last 7 Days',
26
- '30d': 'Last 30 Days',
27
- 'all': 'All Time',
21
+ "5m": "Last 5 Minutes",
22
+ "15m": "Last 15 Minutes",
23
+ "1h": "Last Hour",
24
+ "24h": "Last 24 Hours",
25
+ "7d": "Last 7 Days",
26
+ "30d": "Last 30 Days",
27
+ all: "All Time",
28
28
  };
29
29
 
30
30
  interface TimeWindowCardProps {
@@ -35,56 +35,75 @@ function TimeWindowCard({ isCompact }: TimeWindowCardProps) {
35
35
  const colors = useColors();
36
36
  const { window: timeWindow } = useTimeWindow();
37
37
 
38
- const title = isCompact ? 'WINDOW' : 'TIME WINDOW (t)';
38
+ const title = isCompact ? "WINDOW" : "TIME WINDOW (t)";
39
39
  const value = isCompact ? timeWindow.toUpperCase() : TIME_WINDOW_EXPANDED[timeWindow];
40
- const hint = isCompact ? '(t)' : "Press 't' to change";
41
-
42
- return (
43
- <box
44
- flexDirection="column"
45
- paddingX={1}
46
- flexShrink={0}
47
- width={isCompact ? 9 : 22}
48
- >
49
- <text fg={colors.textMuted} height={1}>{title}</text>
50
- <text fg={colors.info} height={1}><strong>{value}</strong></text>
51
- <text fg={colors.textSubtle} height={1}>{hint}</text>
40
+ const hint = isCompact ? "(t)" : "Press 't' to change";
41
+
42
+ return (
43
+ <box flexDirection="column" paddingX={1} flexShrink={0} width={isCompact ? 9 : 22}>
44
+ <text fg={colors.textMuted} height={1}>
45
+ {title}
46
+ </text>
47
+ <text fg={colors.info} height={1}>
48
+ <strong>{value}</strong>
49
+ </text>
50
+ <text fg={colors.textSubtle} height={1}>
51
+ {hint}
52
+ </text>
52
53
  </box>
53
54
  );
54
55
  }
55
56
 
56
- function KPICard({ title, value, delta, subValue, metric = 'default', flashIntensity = 0 }: KPICardProps) {
57
+ function KPICard({
58
+ title,
59
+ value,
60
+ delta,
61
+ subValue,
62
+ metric = "default",
63
+ flashIntensity = 0,
64
+ }: KPICardProps) {
57
65
  const colors = useColors();
58
-
66
+
59
67
  const getBaseMetricColor = (m: MetricType): string => {
60
68
  switch (m) {
61
- case 'cost': return colors.success;
62
- case 'tokens': return colors.primary;
63
- case 'requests': return colors.secondary;
64
- case 'rate': return colors.warning;
65
- default: return colors.text;
69
+ case "cost":
70
+ return colors.success;
71
+ case "tokens":
72
+ return colors.primary;
73
+ case "requests":
74
+ return colors.secondary;
75
+ case "rate":
76
+ return colors.warning;
77
+ default:
78
+ return colors.text;
66
79
  }
67
80
  };
68
-
81
+
69
82
  const baseColor = getBaseMetricColor(metric);
70
- const flashHighlight = '#ffffff';
71
- const valueColor = flashIntensity > 0
72
- ? interpolateColor(flashIntensity, baseColor, flashHighlight)
73
- : baseColor;
74
-
83
+ const flashHighlight = "#ffffff";
84
+ const valueColor =
85
+ flashIntensity > 0 ? interpolateColor(flashIntensity, baseColor, flashHighlight) : baseColor;
86
+
75
87
  const deltaColor = colors.success;
76
-
88
+
77
89
  return (
78
- <box
79
- flexDirection="column"
80
- paddingLeft={1}
81
- paddingRight={2}
82
- flexGrow={1}
83
- >
84
- <text fg={colors.textMuted} height={1}>{title}</text>
85
- <text fg={valueColor} height={1}><strong>{value}</strong></text>
86
- {delta && <text fg={deltaColor} height={1}>{delta}</text>}
87
- {subValue && <text fg={colors.textMuted} height={1}>{subValue}</text>}
90
+ <box flexDirection="column" paddingLeft={1} paddingRight={2} flexGrow={1}>
91
+ <text fg={colors.textMuted} height={1}>
92
+ {title}
93
+ </text>
94
+ <text fg={valueColor} height={1}>
95
+ <strong>{value}</strong>
96
+ </text>
97
+ {delta && (
98
+ <text fg={deltaColor} height={1}>
99
+ {delta}
100
+ </text>
101
+ )}
102
+ {subValue && (
103
+ <text fg={colors.textMuted} height={1}>
104
+ {subValue}
105
+ </text>
106
+ )}
88
107
  </box>
89
108
  );
90
109
  }
@@ -97,7 +116,7 @@ export interface ActivityStatus {
97
116
  export interface BudgetInfo {
98
117
  limit: number | null;
99
118
  budgetCost: number;
100
- budgetType: 'daily' | 'weekly' | 'monthly' | 'none';
119
+ budgetType: "daily" | "weekly" | "monthly" | "none";
101
120
  budgetTypeLabel: string;
102
121
  warningPercent: number;
103
122
  criticalPercent: number;
@@ -131,51 +150,54 @@ export function KpiStrip({
131
150
  const { width: terminalWidth } = useTerminalDimensions();
132
151
  const { config } = useConfig();
133
152
  const { sparkline: sparklineConfig } = config.display;
134
-
153
+
135
154
  const animatedCost = useAnimatedValue(totalCost, { durationMs: 400, precision: 2 });
136
155
  const animatedTokens = useAnimatedValue(totalTokens, { durationMs: 400, precision: 0 });
137
156
  const animatedRequests = useAnimatedValue(totalRequests, { durationMs: 300, precision: 0 });
138
-
139
- const { intensity: costFlashIntensity } = useValueFlash(totalCost, {
140
- durationMs: 500,
157
+
158
+ const { intensity: costFlashIntensity } = useValueFlash(totalCost, {
159
+ durationMs: 500,
141
160
  increaseOnly: true,
142
- threshold: 0.001
161
+ threshold: 0.001,
143
162
  });
144
- const { intensity: tokenFlashIntensity } = useValueFlash(totalTokens, {
145
- durationMs: 400,
163
+ const { intensity: tokenFlashIntensity } = useValueFlash(totalTokens, {
164
+ durationMs: 400,
146
165
  increaseOnly: true,
147
- threshold: 10
166
+ threshold: 10,
148
167
  });
149
-
168
+
150
169
  const formatCurrency = (val: number) => `$${val.toFixed(2)}`;
151
- const formatTokens = (val: number) => val > 1000000 ? `${(val/1000000).toFixed(1)}M` : `${(val/1000).toFixed(1)}K`;
152
- const formatRate = (val: number) => val >= 1000 ? `${(val/1000).toFixed(1)}k` : `${Math.round(val)}`;
153
- const formatBurnTokens = (val: number) => val >= 1000 ? `${(val/1000).toFixed(1)}k` : `${Math.round(val)}`;
154
-
170
+ const formatTokens = (val: number) =>
171
+ val > 1000000 ? `${(val / 1000000).toFixed(1)}M` : `${(val / 1000).toFixed(1)}K`;
172
+ const formatRate = (val: number) =>
173
+ val >= 1000 ? `${(val / 1000).toFixed(1)}k` : `${Math.round(val)}`;
174
+ const formatBurnTokens = (val: number) =>
175
+ val >= 1000 ? `${(val / 1000).toFixed(1)}k` : `${Math.round(val)}`;
176
+
155
177
  const formatWindowLabel = (sec: number): string => {
156
178
  if (sec < 60) return `${Math.round(sec)}s`;
157
179
  if (sec < 3600) return `${Math.round(sec / 60)}m`;
158
180
  return `${(sec / 3600).toFixed(1)}h`;
159
181
  };
160
-
182
+
161
183
  const hasEnoughHistory = windowSec >= 30;
162
184
  const burnRateCostPerHour = hasEnoughHistory ? deltaCost * (3600 / windowSec) : 0;
163
185
  const burnRateTokensPerMin = hasEnoughHistory ? deltaTokens * (60 / windowSec) : 0;
164
186
  const windowLabel = formatWindowLabel(windowSec);
165
-
187
+
166
188
  const getActivityStatus = (): ActivityStatus => {
167
189
  const { instantRate, avgRate, isSpike } = activity;
168
- if (isSpike || instantRate >= 500) return { label: 'SPIKE', color: colors.error };
169
- if (avgRate >= 150) return { label: 'HOT', color: colors.warning };
170
- if (avgRate >= 50) return { label: 'BUSY', color: colors.success };
171
- if (avgRate >= 10) return { label: 'LOW', color: colors.textMuted };
172
- return { label: 'IDLE', color: colors.textSubtle };
190
+ if (isSpike || instantRate >= 500) return { label: "SPIKE", color: colors.error };
191
+ if (avgRate >= 150) return { label: "HOT", color: colors.warning };
192
+ if (avgRate >= 50) return { label: "BUSY", color: colors.success };
193
+ if (avgRate >= 10) return { label: "LOW", color: colors.textMuted };
194
+ return { label: "IDLE", color: colors.textSubtle };
173
195
  };
174
196
 
175
197
  const activityStatus = getActivityStatus();
176
-
198
+
177
199
  const isCompact = terminalWidth < 100;
178
- const sparklineWidth = isCompact
200
+ const sparklineWidth = isCompact
179
201
  ? Math.min(25, Math.max(15, Math.floor(terminalWidth * 0.2)))
180
202
  : Math.min(50, Math.max(20, Math.floor(terminalWidth * 0.25)));
181
203
 
@@ -188,36 +210,44 @@ export function KpiStrip({
188
210
  <>
189
211
  <box flexDirection="row" gap={0} height={4} flexShrink={0}>
190
212
  <TimeWindowCard isCompact={isCompact} />
191
- <KPICard
192
- title="COST"
193
- value={formatCurrency(animatedCost)}
194
- delta={hasEnoughHistory ? `+${formatCurrency(deltaCost)} (${windowLabel})` : 'gathering...'}
213
+ <KPICard
214
+ title="COST"
215
+ value={formatCurrency(animatedCost)}
216
+ delta={
217
+ hasEnoughHistory ? `+${formatCurrency(deltaCost)} (${windowLabel})` : "gathering..."
218
+ }
195
219
  metric="cost"
196
220
  flashIntensity={costFlashIntensity}
197
221
  />
198
- <KPICard
199
- title="TOKENS"
200
- value={formatTokens(animatedTokens)}
201
- delta={hasEnoughHistory ? `+${formatTokens(deltaTokens)} (${windowLabel})` : 'gathering...'}
222
+ <KPICard
223
+ title="TOKENS"
224
+ value={formatTokens(animatedTokens)}
225
+ delta={
226
+ hasEnoughHistory ? `+${formatTokens(deltaTokens)} (${windowLabel})` : "gathering..."
227
+ }
202
228
  metric="tokens"
203
229
  flashIntensity={tokenFlashIntensity}
204
230
  />
205
- <KPICard
206
- title="REQUESTS"
207
- value={Math.round(animatedRequests).toLocaleString()}
231
+ <KPICard
232
+ title="REQUESTS"
233
+ value={Math.round(animatedRequests).toLocaleString()}
208
234
  subValue={`${activeCount} active`}
209
235
  metric="requests"
210
236
  />
211
237
  {!isCompact && (
212
- <KPICard
213
- title="BURN RATE"
214
- value={hasEnoughHistory ? `${formatCurrency(burnRateCostPerHour)}/hr` : '--'}
215
- subValue={hasEnoughHistory ? `${formatBurnTokens(burnRateTokensPerMin)} tok/min` : 'gathering...'}
238
+ <KPICard
239
+ title="BURN RATE"
240
+ value={hasEnoughHistory ? `${formatCurrency(burnRateCostPerHour)}/hr` : "--"}
241
+ subValue={
242
+ hasEnoughHistory
243
+ ? `${formatBurnTokens(burnRateTokensPerMin)} tok/min`
244
+ : "gathering..."
245
+ }
216
246
  metric="rate"
217
247
  />
218
248
  )}
219
-
220
- <box flexDirection="column" flexGrow={1} paddingX={1} overflow="hidden">
249
+
250
+ <box flexDirection="column" flexGrow={1} paddingX={1} overflow="hidden">
221
251
  <box flexDirection="row" justifyContent="space-between">
222
252
  <text fg={colors.textMuted}>ACTIVITY</text>
223
253
  <text>
@@ -225,10 +255,10 @@ export function KpiStrip({
225
255
  <span fg={colors.textMuted}> {formatRate(activity.instantRate)}/s</span>
226
256
  </text>
227
257
  </box>
228
- <Sparkline
229
- data={sparkData}
230
- width={sparklineWidth}
231
- label="tok/s"
258
+ <Sparkline
259
+ data={sparkData}
260
+ width={sparklineWidth}
261
+ label="tok/s"
232
262
  fixedMax={sparkMax}
233
263
  thresholds={{ warning: 150, error: 500 }}
234
264
  style={sparklineConfig.style}
@@ -237,9 +267,9 @@ export function KpiStrip({
237
267
  />
238
268
  </box>
239
269
  </box>
240
-
270
+
241
271
  <box height={1} overflow="hidden">
242
- <text fg={colors.border}>{'─'.repeat(300)}</text>
272
+ <text fg={colors.border}>{"─".repeat(300)}</text>
243
273
  </box>
244
274
  </>
245
275
  );
@@ -1,6 +1,6 @@
1
- import type { ReactNode } from 'react';
2
- import { useColors } from '../contexts/ThemeContext.tsx';
3
- import { usePulse } from '../hooks/usePulse.ts';
1
+ import type { ReactNode } from "react";
2
+ import { useColors } from "../contexts/ThemeContext.tsx";
3
+ import { usePulse } from "../hooks/usePulse.ts";
4
4
 
5
5
  interface LimitGaugeProps {
6
6
  label: string;
@@ -17,58 +17,75 @@ interface LimitGaugeProps {
17
17
  criticalThreshold?: number;
18
18
  }
19
19
 
20
- const FRACTIONAL_BLOCKS = [' ', '▏', '▎', '▍', '▌', '▋', '▊', '▉', '█'];
20
+ const FRACTIONAL_BLOCKS = [" ", "▏", "▎", "▍", "▌", "▋", "▊", "▉", "█"];
21
21
 
22
- function renderFractionalBar(percent: number, width: number, filledColor: string, emptyColor: string): ReactNode[] {
22
+ function renderFractionalBar(
23
+ percent: number,
24
+ width: number,
25
+ filledColor: string,
26
+ emptyColor: string,
27
+ ): ReactNode[] {
23
28
  const totalSegments = width * 8;
24
29
  const filledSegments = Math.round((percent / 100) * totalSegments);
25
30
  const fullBlocks = Math.floor(filledSegments / 8);
26
31
  const remainder = filledSegments % 8;
27
32
  const emptyBlocks = width - fullBlocks - (remainder > 0 ? 1 : 0);
28
-
33
+
29
34
  const elements: ReactNode[] = [];
30
-
35
+
31
36
  if (fullBlocks > 0) {
32
- elements.push(<span key="full" fg={filledColor}>{'█'.repeat(fullBlocks)}</span>);
37
+ elements.push(
38
+ <span key="full" fg={filledColor}>
39
+ {"█".repeat(fullBlocks)}
40
+ </span>,
41
+ );
33
42
  }
34
-
43
+
35
44
  if (remainder > 0) {
36
- elements.push(<span key="partial" fg={filledColor}>{FRACTIONAL_BLOCKS[remainder]}</span>);
45
+ elements.push(
46
+ <span key="partial" fg={filledColor}>
47
+ {FRACTIONAL_BLOCKS[remainder]}
48
+ </span>,
49
+ );
37
50
  }
38
-
51
+
39
52
  if (emptyBlocks > 0) {
40
- elements.push(<span key="empty" fg={emptyColor}>{'─'.repeat(emptyBlocks)}</span>);
53
+ elements.push(
54
+ <span key="empty" fg={emptyColor}>
55
+ {"─".repeat(emptyBlocks)}
56
+ </span>,
57
+ );
41
58
  }
42
-
59
+
43
60
  return elements;
44
61
  }
45
62
 
46
63
  function interpolatePulseColor(intensity: number, baseColor: string, dimColor: string): string {
47
64
  const dimAmount = 0.4;
48
65
  const t = intensity * dimAmount;
49
-
66
+
50
67
  const parseHex = (hex: string) => {
51
- const h = hex.replace('#', '');
68
+ const h = hex.replace("#", "");
52
69
  return {
53
70
  r: parseInt(h.substring(0, 2), 16),
54
71
  g: parseInt(h.substring(2, 4), 16),
55
72
  b: parseInt(h.substring(4, 6), 16),
56
73
  };
57
74
  };
58
-
75
+
59
76
  const base = parseHex(baseColor);
60
77
  const dim = parseHex(dimColor);
61
-
78
+
62
79
  const r = Math.round(base.r + (dim.r - base.r) * t);
63
80
  const g = Math.round(base.g + (dim.g - base.g) * t);
64
81
  const b = Math.round(base.b + (dim.b - base.b) * t);
65
-
66
- return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
82
+
83
+ return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
67
84
  }
68
85
 
69
- export function LimitGauge({
70
- label,
71
- usedPercent,
86
+ export function LimitGauge({
87
+ label,
88
+ usedPercent,
72
89
  color,
73
90
  ghost = false,
74
91
  error,
@@ -81,34 +98,34 @@ export function LimitGauge({
81
98
  criticalThreshold = 95,
82
99
  }: LimitGaugeProps) {
83
100
  const colors = useColors();
84
-
101
+
85
102
  const percent = usedPercent ?? 0;
86
103
  const isCritical = percent >= criticalThreshold;
87
104
  const isWarning = percent >= warningThreshold && percent < criticalThreshold;
88
-
105
+
89
106
  // Critical: fast, intense pulse (80ms interval)
90
107
  // Warning: moderate pulse (200ms interval) to draw attention
91
108
  const criticalPulseStep = usePulse({ enabled: isCritical, intervalMs: 80 });
92
109
  const warningPulseStep = usePulse({ enabled: isWarning, intervalMs: 200 });
93
-
94
- const pulseIntensity = isCritical
95
- ? Math.abs(Math.sin(criticalPulseStep * 0.3))
96
- : isWarning
110
+
111
+ const pulseIntensity = isCritical
112
+ ? Math.abs(Math.sin(criticalPulseStep * 0.3))
113
+ : isWarning
97
114
  ? Math.abs(Math.sin(warningPulseStep * 0.25))
98
115
  : 0;
99
-
116
+
100
117
  const truncateLabel = (lbl: string, maxLen: number): string => {
101
118
  if (lbl.length <= maxLen) return lbl.padEnd(maxLen);
102
- return lbl.slice(0, maxLen - 1) + '…';
119
+ return lbl.slice(0, maxLen - 1) + "…";
103
120
  };
104
-
121
+
105
122
  if (compact) {
106
123
  const shortLabel = truncateLabel(label, 10);
107
- const percentStr = usedPercent !== null ? `${Math.round(percent)}%` : '--';
108
- const statusIcon = isCritical ? '!!' : isWarning ? '!' : '';
124
+ const percentStr = usedPercent !== null ? `${Math.round(percent)}%` : "--";
125
+ const statusIcon = isCritical ? "!!" : isWarning ? "!" : "";
109
126
  const statusColor = isCritical ? colors.error : isWarning ? color : colors.textMuted;
110
127
  const textColor = isCritical ? colors.error : isWarning ? color : colors.text;
111
-
128
+
112
129
  return (
113
130
  <text height={1}>
114
131
  {selected && <span fg={colors.primary}>▌</span>}
@@ -118,7 +135,7 @@ export function LimitGauge({
118
135
  </text>
119
136
  );
120
137
  }
121
-
138
+
122
139
  if (ghost) {
123
140
  const ghostLabel = truncateLabel(label, labelWidth);
124
141
  return (
@@ -126,57 +143,77 @@ export function LimitGauge({
126
143
  <text height={1}>
127
144
  <span fg={colors.textSubtle}>○ </span>
128
145
  <span fg={colors.textSubtle}>{ghostLabel} </span>
129
- <span fg={colors.textSubtle}>{'─'.repeat(barWidth)}</span>
130
- <span fg={colors.textSubtle}> --</span>
146
+ <span fg={colors.textSubtle}>{"─".repeat(barWidth)}</span>
147
+ <span fg={colors.textSubtle}> --</span>
131
148
  </text>
132
149
  </box>
133
150
  );
134
151
  }
135
-
152
+
136
153
  if (error) {
137
154
  const displayLabel = truncateLabel(label, labelWidth);
138
- const errorIcon = selected ? '▌' : '✗';
155
+ const errorIcon = selected ? "▌" : "✗";
139
156
  return (
140
- <box height={1} overflow="hidden" {...(selected ? { backgroundColor: colors.borderMuted } : {})}>
157
+ <box
158
+ height={1}
159
+ overflow="hidden"
160
+ {...(selected ? { backgroundColor: colors.borderMuted } : {})}
161
+ >
141
162
  <text height={1}>
142
163
  <span fg={selected ? colors.primary : colors.error}>{errorIcon} </span>
143
164
  <span fg={selected ? colors.text : colors.text}>{displayLabel} </span>
144
- <span fg={colors.error}>{'─'.repeat(barWidth)}</span>
165
+ <span fg={colors.error}>{"─".repeat(barWidth)}</span>
145
166
  <span fg={colors.error}> ERR</span>
146
167
  </text>
147
168
  </box>
148
169
  );
149
170
  }
150
-
151
- const barColor = isCritical
171
+
172
+ const barColor = isCritical
152
173
  ? interpolatePulseColor(pulseIntensity, colors.error, colors.background)
153
- : isWarning
174
+ : isWarning
154
175
  ? interpolatePulseColor(pulseIntensity, color, colors.background)
155
176
  : color;
156
-
157
- const statusIcon = isCritical ? '!!' : isWarning ? '!' : '●';
177
+
178
+ const statusIcon = isCritical ? "!!" : isWarning ? "!" : "●";
158
179
  const statusColor = isCritical ? colors.error : isWarning ? color : colors.success;
159
-
180
+
160
181
  const displayLabel = truncateLabel(label, labelWidth);
161
- const percentStr = usedPercent !== null ? `${Math.round(percent)}%`.padStart(4) : ' --';
162
-
182
+ const percentStr = usedPercent !== null ? `${Math.round(percent)}%`.padStart(4) : " --";
183
+
163
184
  const percentDisplay = isCritical ? (
164
- <span fg={colors.background} bg={colors.error}>{percentStr}</span>
185
+ <span fg={colors.background} bg={colors.error}>
186
+ {percentStr}
187
+ </span>
165
188
  ) : (
166
189
  <span fg={isWarning ? color : colors.textMuted}>{percentStr}</span>
167
190
  );
168
-
169
- const resetDisplay = resetTime ? (
170
- <span fg={colors.textSubtle}> {resetTime}</span>
171
- ) : null;
172
-
173
- const selectionIndicator = selected ? '▌' : statusIcon;
174
-
191
+
192
+ const resetDisplay = resetTime ? <span fg={colors.textSubtle}> {resetTime}</span> : null;
193
+
194
+ const selectionIndicator = selected ? "▌" : statusIcon;
195
+
175
196
  return (
176
- <box height={1} overflow="hidden" {...(selected ? { backgroundColor: colors.borderMuted } : {})}>
197
+ <box
198
+ height={1}
199
+ overflow="hidden"
200
+ {...(selected ? { backgroundColor: colors.borderMuted } : {})}
201
+ >
177
202
  <text height={1}>
178
203
  <span fg={selected ? colors.primary : statusColor}>{selectionIndicator} </span>
179
- <span fg={selected ? colors.text : (isCritical ? colors.error : isWarning ? color : colors.textMuted)}>{displayLabel} </span>
204
+ <span
205
+ fg={
206
+ selected
207
+ ? colors.text
208
+ : isCritical
209
+ ? colors.error
210
+ : isWarning
211
+ ? color
212
+ : colors.textMuted
213
+ }
214
+ >
215
+ {displayLabel}{" "}
216
+ </span>
180
217
  {renderFractionalBar(percent, barWidth, barColor, colors.border)}
181
218
  {percentDisplay}
182
219
  {resetDisplay}
@@ -1,5 +1,5 @@
1
- import type { ReactNode } from 'react';
2
- import { RGBA } from '@opentui/core';
1
+ import { RGBA } from "@opentui/core";
2
+ import type { ReactNode } from "react";
3
3
 
4
4
  /** Standardized z-index tiers for overlay layering */
5
5
  export const Z_INDEX = {
@@ -18,8 +18,8 @@ interface ModalBackdropProps {
18
18
  onBackdropClick?: () => void;
19
19
  }
20
20
 
21
- export function ModalBackdrop({
22
- children,
21
+ export function ModalBackdrop({
22
+ children,
23
23
  zIndex = Z_INDEX.MODAL,
24
24
  onBackdropClick,
25
25
  }: ModalBackdropProps) {