@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,22 +1,21 @@
1
- import { useState, useEffect, useMemo, useRef, useCallback } from 'react';
2
- import { useTerminalDimensions } from '@opentui/react';
3
- import type { ScrollBoxRenderable } from '@opentui/core';
4
- import { useColors } from '../contexts/ThemeContext.tsx';
5
- import { usePlugins } from '../contexts/PluginContext.tsx';
6
- import { useAgentSessions } from '../contexts/AgentSessionContext.tsx';
7
- import { useTimeWindow } from '../contexts/TimeWindowContext.tsx';
8
- import { useConfig } from '../contexts/ConfigContext.tsx';
9
- import { useDashboardRuntime } from '../contexts/DashboardRuntimeContext.tsx';
10
- import { useDrawer } from '../contexts/DrawerContext.tsx';
11
- import { useDashboardKeyboard } from '../hooks/useDashboardKeyboard.ts';
12
- import { getProviderColor } from '../utils/providerColor.ts';
13
- import { notificationBus } from '@/plugins/notification-bus.ts';
14
-
15
- import { KpiStrip } from '../components/KpiStrip.tsx';
16
- import { SessionsTable } from '../components/SessionsTable.tsx';
17
- import { SmartSidebar, getSidebarMode, type DriverDimension } from '../components/SmartSidebar.tsx';
18
- import { ProviderLimitsPanel } from '../components/ProviderLimitsPanel.tsx';
19
- import { HelpOverlay } from '../components/HelpOverlay.tsx';
1
+ import type { ScrollBoxRenderable } from "@opentui/core";
2
+ import { useTerminalDimensions } from "@opentui/react";
3
+ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
4
+ import { notificationBus } from "@/plugins/notification-bus.ts";
5
+ import { HelpOverlay } from "../components/HelpOverlay.tsx";
6
+ import { KpiStrip } from "../components/KpiStrip.tsx";
7
+ import { ProviderLimitsPanel } from "../components/ProviderLimitsPanel.tsx";
8
+ import { SessionsTable } from "../components/SessionsTable.tsx";
9
+ import { type DriverDimension, getSidebarMode, SmartSidebar } from "../components/SmartSidebar.tsx";
10
+ import { useAgentSessions } from "../contexts/AgentSessionContext.tsx";
11
+ import { useConfig } from "../contexts/ConfigContext.tsx";
12
+ import { useDashboardRuntime } from "../contexts/DashboardRuntimeContext.tsx";
13
+ import { useDrawer } from "../contexts/DrawerContext.tsx";
14
+ import { usePlugins } from "../contexts/PluginContext.tsx";
15
+ import { useColors } from "../contexts/ThemeContext.tsx";
16
+ import { useTimeWindow } from "../contexts/TimeWindowContext.tsx";
17
+ import { useDashboardKeyboard } from "../hooks/useDashboardKeyboard.ts";
18
+ import { getProviderColor } from "../utils/providerColor.ts";
20
19
 
21
20
  export function RealTimeDashboard() {
22
21
  const colors = useColors();
@@ -27,51 +26,61 @@ export function RealTimeDashboard() {
27
26
  const { config } = useConfig();
28
27
  const { activity, sparkData, deltas } = useDashboardRuntime();
29
28
 
30
-
31
29
  const sidebarMode = getSidebarMode(terminalWidth);
32
30
  const showLargeHeader = terminalHeight >= 35;
33
31
  const showProviderLimitsPanel = terminalHeight >= 24;
34
32
 
35
33
  // Visible-row count for scroll tracking — every non-session-data line must be counted.
36
34
  const visibleRows = (() => {
37
- const appChrome = (showLargeHeader ? 7 : 1) + 1; // App.tsx: Header + StatusBar
38
- const outerPadding = 2; // padding={1} top + bottom
39
- const kpi = 5; // KpiStrip fragment: h4 cards + h1 rule
40
- const limits = !showProviderLimitsPanel ? 0 // ProviderLimitsPanel
41
- : terminalHeight < 30 ? (sidebarMode === 'hidden' ? 2 : 1) // compact (+budget bar)
42
- : 4; // normal/wide bordered box
35
+ const appChrome = (showLargeHeader ? 7 : 1) + 1; // App.tsx: Header + StatusBar
36
+ const outerPadding = 2; // padding={1} top + bottom
37
+ const kpi = 5; // KpiStrip fragment: h4 cards + h1 rule
38
+ const limits = !showProviderLimitsPanel
39
+ ? 0 // ProviderLimitsPanel
40
+ : terminalHeight < 30
41
+ ? sidebarMode === "hidden"
42
+ ? 2
43
+ : 1 // compact (+budget bar)
44
+ : 4; // normal/wide bordered box
43
45
  const tableChrome = 4 + (terminalHeight >= 30 ? 1 : 0); // borders + header + columns + inspector
44
- const footer = 1; // keyboard shortcut bar
46
+ const footer = 1; // keyboard shortcut bar
45
47
  // gap={1} between rendered children; KpiStrip fragment injects 2 children
46
48
  const children = 2 + (showProviderLimitsPanel ? 1 : 0) + 1 + 1;
47
49
  const gaps = children - 1;
48
- return Math.max(1, terminalHeight - appChrome - outerPadding - kpi - limits - tableChrome - footer - gaps);
50
+ return Math.max(
51
+ 1,
52
+ terminalHeight - appChrome - outerPadding - kpi - limits - tableChrome - footer - gaps,
53
+ );
49
54
  })();
50
55
 
51
56
  const { showDrawer, isOpen: showSessionDrawer } = useDrawer();
52
-
57
+
53
58
  const [showHelp, setShowHelp] = useState(false);
54
59
  const [selectedRow, setSelectedRow] = useState(0);
55
- const [focusedPanel, setFocusedPanel] = useState<'sessions' | 'sidebar' | 'limits'>('sessions');
60
+ const [focusedPanel, setFocusedPanel] = useState<"sessions" | "sidebar" | "limits">("sessions");
56
61
  const [sidebarCollapsed, setSidebarCollapsed] = useState(config.display.sidebarCollapsed);
57
- const [filterQuery, setFilterQuery] = useState('');
62
+ const [filterQuery, setFilterQuery] = useState("");
58
63
  const [isFiltering, setIsFiltering] = useState(false);
59
- const [sortField, setSortField] = useState<'cost' | 'tokens' | 'time'>('cost');
64
+ const [sortField, setSortField] = useState<"cost" | "tokens" | "time">("cost");
60
65
  const [pendingG, setPendingG] = useState(false);
61
- const [scrollOffset, setScrollOffset] = useState(0);
66
+ const scrollOffsetRef = useRef(0);
67
+ const setScrollOffset = useCallback((val: number) => {
68
+ scrollOffsetRef.current = val;
69
+ sessionsScrollboxRef.current?.scrollTo(val);
70
+ }, []);
62
71
  const [limitSelectedIndex, setLimitSelectedIndex] = useState(0);
63
- const [driverDimension, setDriverDimension] = useState<DriverDimension>('model');
72
+ const [driverDimension, setDriverDimension] = useState<DriverDimension>("model");
64
73
  const [selectedDriverIndex, setSelectedDriverIndex] = useState(0);
65
74
  const [activeDriverFilter, setActiveDriverFilter] = useState<string | null>(null);
66
75
 
67
- const effectiveSidebarCollapsed = sidebarCollapsed || sidebarMode === 'hidden';
68
- const showBudgetInLimits = sidebarMode === 'hidden';
76
+ const effectiveSidebarCollapsed = sidebarCollapsed || sidebarMode === "hidden";
77
+ const showBudgetInLimits = sidebarMode === "hidden";
69
78
 
70
79
  const sessionsScrollboxRef = useRef<ScrollBoxRenderable>(null);
71
80
 
72
81
  const configuredProviders = useMemo(() => {
73
82
  return Array.from(providers.values())
74
- .filter(p => p.configured)
83
+ .filter((p) => p.configured)
75
84
  .sort((a, b) => getMaxUsedPercent(b) - getMaxUsedPercent(a));
76
85
  }, [providers]);
77
86
 
@@ -91,19 +100,20 @@ export function RealTimeDashboard() {
91
100
 
92
101
  const baseFilteredSessions = useMemo(() => {
93
102
  let result = [...agentSessions];
94
-
103
+
95
104
  if (windowMs !== null) {
96
105
  const cutoff = Date.now() - windowMs;
97
- result = result.filter(s => s.lastActivityAt >= cutoff);
106
+ result = result.filter((s) => s.lastActivityAt >= cutoff);
98
107
  }
99
-
108
+
100
109
  if (filterQuery) {
101
110
  const q = filterQuery.toLowerCase();
102
- result = result.filter(s =>
103
- s.agentName.toLowerCase().includes(q) ||
104
- s.streams.some(st => st.modelId.toLowerCase().includes(q)) ||
105
- (s.projectPath?.toLowerCase().includes(q) ?? false) ||
106
- (s.sessionName?.toLowerCase().includes(q) ?? false)
111
+ result = result.filter(
112
+ (s) =>
113
+ s.agentName.toLowerCase().includes(q) ||
114
+ s.streams.some((st) => st.modelId.toLowerCase().includes(q)) ||
115
+ (s.projectPath?.toLowerCase().includes(q) ?? false) ||
116
+ (s.sessionName?.toLowerCase().includes(q) ?? false),
107
117
  );
108
118
  }
109
119
 
@@ -112,13 +122,19 @@ export function RealTimeDashboard() {
112
122
 
113
123
  const drivers = useMemo(() => {
114
124
  const stats: Record<string, { cost: number }> = {};
115
- baseFilteredSessions.forEach(s => {
116
- s.streams.forEach(st => {
125
+ baseFilteredSessions.forEach((s) => {
126
+ s.streams.forEach((st) => {
117
127
  let key: string;
118
128
  switch (driverDimension) {
119
- case 'model': key = st.modelId; break;
120
- case 'project': key = s.projectPath?.split('/').pop() ?? 'unknown'; break;
121
- case 'agent': key = s.agentName; break;
129
+ case "model":
130
+ key = st.modelId;
131
+ break;
132
+ case "project":
133
+ key = s.projectPath?.split("/").pop() ?? "unknown";
134
+ break;
135
+ case "agent":
136
+ key = s.agentName;
137
+ break;
122
138
  }
123
139
  if (!stats[key]) stats[key] = { cost: 0 };
124
140
  stats[key]!.cost += st.costUsd ?? 0;
@@ -138,7 +154,7 @@ export function RealTimeDashboard() {
138
154
  }, [drivers.length, selectedDriverIndex]);
139
155
 
140
156
  useEffect(() => {
141
- if (activeDriverFilter === '__TOGGLE_SELECTED__') {
157
+ if (activeDriverFilter === "__TOGGLE_SELECTED__") {
142
158
  const driver = drivers[selectedDriverIndex];
143
159
  setActiveDriverFilter(driver?.id ?? null);
144
160
  }
@@ -146,27 +162,30 @@ export function RealTimeDashboard() {
146
162
 
147
163
  const processedSessions = useMemo(() => {
148
164
  let result = [...baseFilteredSessions];
149
-
150
- if (activeDriverFilter && activeDriverFilter !== '__TOGGLE_SELECTED__') {
151
- result = result.filter(s => {
165
+
166
+ if (activeDriverFilter && activeDriverFilter !== "__TOGGLE_SELECTED__") {
167
+ result = result.filter((s) => {
152
168
  switch (driverDimension) {
153
- case 'model':
154
- return s.streams.some(st => st.modelId === activeDriverFilter);
155
- case 'project':
156
- return (s.projectPath?.split('/').pop() ?? 'unknown') === activeDriverFilter;
157
- case 'agent':
169
+ case "model":
170
+ return s.streams.some((st) => st.modelId === activeDriverFilter);
171
+ case "project":
172
+ return (s.projectPath?.split("/").pop() ?? "unknown") === activeDriverFilter;
173
+ case "agent":
158
174
  return s.agentName === activeDriverFilter;
175
+ default:
176
+ return true;
159
177
  }
160
178
  });
161
179
  }
162
180
 
163
181
  result.sort((a, b) => {
164
- const aActive = a.status === 'active' ? 1 : 0;
165
- const bActive = b.status === 'active' ? 1 : 0;
182
+ const aActive = a.status === "active" ? 1 : 0;
183
+ const bActive = b.status === "active" ? 1 : 0;
166
184
  if (bActive !== aActive) return bActive - aActive;
167
-
168
- if (sortField === 'cost') return (b.totalCostUsd ?? 0) - (a.totalCostUsd ?? 0);
169
- if (sortField === 'tokens') return (b.totals.input + b.totals.output) - (a.totals.input + a.totals.output);
185
+
186
+ if (sortField === "cost") return (b.totalCostUsd ?? 0) - (a.totalCostUsd ?? 0);
187
+ if (sortField === "tokens")
188
+ return b.totals.input + b.totals.output - (a.totals.input + a.totals.output);
170
189
  return b.lastActivityAt - a.lastActivityAt;
171
190
  });
172
191
 
@@ -176,34 +195,31 @@ export function RealTimeDashboard() {
176
195
  useEffect(() => {
177
196
  if (processedSessions.length === 0) {
178
197
  if (selectedRow !== 0) setSelectedRow(0);
179
- if (scrollOffset !== 0) setScrollOffset(0);
198
+ if (scrollOffsetRef.current !== 0) setScrollOffset(0);
180
199
  return;
181
200
  }
182
-
201
+
183
202
  const maxRow = processedSessions.length - 1;
184
203
  if (selectedRow > maxRow) {
185
204
  setSelectedRow(maxRow);
186
205
  return;
187
206
  }
188
- }, [processedSessions.length, selectedRow, scrollOffset]);
207
+ }, [processedSessions.length, selectedRow, setScrollOffset]);
189
208
 
190
- useEffect(() => {
209
+ useLayoutEffect(() => {
191
210
  if (!sessionsScrollboxRef.current || processedSessions.length === 0) return;
192
211
 
193
- let newOffset = scrollOffset;
212
+ let newOffset = scrollOffsetRef.current;
194
213
 
195
- if (selectedRow < scrollOffset) {
214
+ if (selectedRow < newOffset) {
196
215
  newOffset = selectedRow;
197
- } else if (selectedRow >= scrollOffset + visibleRows) {
216
+ } else if (selectedRow >= newOffset + visibleRows) {
198
217
  newOffset = selectedRow - visibleRows + 1;
199
218
  }
200
219
 
201
- if (newOffset !== scrollOffset) {
202
- setScrollOffset(newOffset);
203
- }
204
-
220
+ scrollOffsetRef.current = newOffset;
205
221
  sessionsScrollboxRef.current.scrollTo(newOffset);
206
- }, [selectedRow, processedSessions.length, scrollOffset, visibleRows]);
222
+ }, [selectedRow, processedSessions.length, visibleRows]);
207
223
 
208
224
  const openSessionDrawer = useCallback(() => {
209
225
  const session = processedSessions[selectedRow];
@@ -225,7 +241,7 @@ export function RealTimeDashboard() {
225
241
  isFiltering,
226
242
  sortField,
227
243
  pendingG,
228
- scrollOffset,
244
+ scrollOffset: scrollOffsetRef.current,
229
245
  limitSelectedIndex,
230
246
  providerCount: configuredProviders.length,
231
247
  driverDimension,
@@ -253,14 +269,16 @@ export function RealTimeDashboard() {
253
269
  processedSessions,
254
270
  });
255
271
 
256
- const windowedKpis = useMemo(() => ({
257
- cost: processedSessions.reduce((acc, s) => acc + (s.totalCostUsd ?? 0), 0),
258
- tokens: processedSessions.reduce((acc, s) => acc + s.totals.input + s.totals.output, 0),
259
- requests: processedSessions.reduce((acc, s) => acc + s.requestCount, 0),
260
- }), [processedSessions]);
272
+ const windowedKpis = useMemo(
273
+ () => ({
274
+ cost: processedSessions.reduce((acc, s) => acc + (s.totalCostUsd ?? 0), 0),
275
+ tokens: processedSessions.reduce((acc, s) => acc + s.totals.input + s.totals.output, 0),
276
+ requests: processedSessions.reduce((acc, s) => acc + s.requestCount, 0),
277
+ }),
278
+ [processedSessions],
279
+ );
261
280
 
262
281
  const budgetPeriodCost = useMemo(() => {
263
-
264
282
  let dailyCost = 0;
265
283
  let weeklyCost = 0;
266
284
  let monthlyCost = 0;
@@ -275,10 +293,14 @@ export function RealTimeDashboard() {
275
293
  }, [agentSessions]);
276
294
 
277
295
  useEffect(() => {
278
- const checks: Array<{ cost: number; limit: number | null; type: 'daily' | 'weekly' | 'monthly' }> = [
279
- { cost: budgetPeriodCost.daily, limit: config.budgets.daily, type: 'daily' },
280
- { cost: budgetPeriodCost.weekly, limit: config.budgets.weekly, type: 'weekly' },
281
- { cost: budgetPeriodCost.monthly, limit: config.budgets.monthly, type: 'monthly' },
296
+ const checks: Array<{
297
+ cost: number;
298
+ limit: number | null;
299
+ type: "daily" | "weekly" | "monthly";
300
+ }> = [
301
+ { cost: budgetPeriodCost.daily, limit: config.budgets.daily, type: "daily" },
302
+ { cost: budgetPeriodCost.weekly, limit: config.budgets.weekly, type: "weekly" },
303
+ { cost: budgetPeriodCost.monthly, limit: config.budgets.monthly, type: "monthly" },
282
304
  ];
283
305
  for (const { cost, limit, type } of checks) {
284
306
  if (limit !== null && limit > 0) {
@@ -289,20 +311,23 @@ export function RealTimeDashboard() {
289
311
 
290
312
  const getBudgetCost = () => {
291
313
  switch (budgetType) {
292
- case 'daily': return budgetPeriodCost.daily;
293
- case 'weekly': return budgetPeriodCost.weekly;
294
- case 'monthly': return budgetPeriodCost.monthly;
295
- default: return windowedKpis.cost;
314
+ case "daily":
315
+ return budgetPeriodCost.daily;
316
+ case "weekly":
317
+ return budgetPeriodCost.weekly;
318
+ case "monthly":
319
+ return budgetPeriodCost.monthly;
320
+ default:
321
+ return windowedKpis.cost;
296
322
  }
297
323
  };
298
324
 
299
- const activeCount = agentSessions.filter(s => s.status === 'active').length;
325
+ const activeCount = agentSessions.filter((s) => s.status === "active").length;
300
326
 
301
327
  return (
302
328
  <box flexDirection="column" flexGrow={1} padding={1} gap={1} overflow="hidden">
303
329
  {showHelp && <HelpOverlay />}
304
330
 
305
-
306
331
  <KpiStrip
307
332
  totalCost={windowedKpis.cost}
308
333
  totalTokens={windowedKpis.tokens}
@@ -314,10 +339,14 @@ export function RealTimeDashboard() {
314
339
  activity={activity}
315
340
  sparkData={sparkData}
316
341
  budget={{
317
- limit: budgetType === 'daily' ? config.budgets.daily
318
- : budgetType === 'weekly' ? config.budgets.weekly
319
- : budgetType === 'monthly' ? config.budgets.monthly
320
- : null,
342
+ limit:
343
+ budgetType === "daily"
344
+ ? config.budgets.daily
345
+ : budgetType === "weekly"
346
+ ? config.budgets.weekly
347
+ : budgetType === "monthly"
348
+ ? config.budgets.monthly
349
+ : null,
321
350
  budgetCost: getBudgetCost(),
322
351
  budgetType,
323
352
  budgetTypeLabel,
@@ -327,23 +356,27 @@ export function RealTimeDashboard() {
327
356
  />
328
357
 
329
358
  <ProviderLimitsPanel
330
- providers={configuredProviders.map(p => ({
359
+ providers={configuredProviders.map((p) => ({
331
360
  id: p.plugin.id,
332
361
  name: p.plugin.name,
333
362
  usedPercent: getMaxUsedPercent(p),
334
363
  color: providerColorOf(p.plugin.id),
335
364
  ...(p.usage?.error ? { error: p.usage.error } : {}),
336
365
  }))}
337
- focused={focusedPanel === 'limits'}
366
+ focused={focusedPanel === "limits"}
338
367
  selectedIndex={limitSelectedIndex}
339
368
  showBudgetBar={showBudgetInLimits}
340
369
  budget={{
341
370
  totalCost: windowedKpis.cost,
342
371
  budgetCost: getBudgetCost(),
343
- limit: budgetType === 'daily' ? config.budgets.daily
344
- : budgetType === 'weekly' ? config.budgets.weekly
345
- : budgetType === 'monthly' ? config.budgets.monthly
346
- : null,
372
+ limit:
373
+ budgetType === "daily"
374
+ ? config.budgets.daily
375
+ : budgetType === "weekly"
376
+ ? config.budgets.weekly
377
+ : budgetType === "monthly"
378
+ ? config.budgets.monthly
379
+ : null,
347
380
  budgetType,
348
381
  budgetTypeLabel,
349
382
  warningPercent: config.alerts.warningPercent,
@@ -376,16 +409,22 @@ export function RealTimeDashboard() {
376
409
  />
377
410
  )}
378
411
  </box>
379
-
412
+
380
413
  <box flexDirection="row" paddingLeft={1} height={1} flexShrink={0}>
381
414
  <text fg={colors.textSubtle} height={1} flexGrow={1}>
382
- {isFiltering ? 'Type to filter Esc cancel Enter apply' :
383
- filterQuery ? `Esc clear / edit filter ↑↓ navigate s sort` :
384
- activeDriverFilter ? `Filter: ${activeDriverFilter} Esc clear` :
385
- focusedPanel === 'sessions' ? '/ filter ↑↓ navigate Enter details s sort l limits Tab next ⇧Tab prev' :
386
- focusedPanel === 'limits' ? '←→ select provider Tab next ⇧Tab prev Esc back' :
387
- focusedPanel === 'sidebar' ? '↑↓ select Enter filter m/p/a dimension b budget Tab next ⇧Tab prev' :
388
- '/ filter i sidebar Tab/⇧Tab switch ? help'}
415
+ {isFiltering
416
+ ? "Type to filter Esc cancel Enter apply"
417
+ : filterQuery
418
+ ? `Esc clear / edit filter ↑↓ navigate s sort`
419
+ : activeDriverFilter
420
+ ? `Filter: ${activeDriverFilter} Esc clear`
421
+ : focusedPanel === "sessions"
422
+ ? "/ filter ↑↓ navigate Enter details s sort l limits Tab next ⇧Tab prev"
423
+ : focusedPanel === "limits"
424
+ ? "←→ select provider Tab next ⇧Tab prev Esc back"
425
+ : focusedPanel === "sidebar"
426
+ ? "↑↓ select Enter filter m/p/a dimension b budget Tab next ⇧Tab prev"
427
+ : "/ filter i sidebar Tab/⇧Tab switch ? help"}
389
428
  </text>
390
429
  </box>
391
430
  </box>