@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,23 +1,24 @@
1
- import { useRef, useEffect } from 'react';
2
- import { useKeyboard } from '@opentui/react';
3
- import { useInputFocus } from '../contexts/InputContext.tsx';
4
- import { useTimeWindow } from '../contexts/TimeWindowContext.tsx';
5
- import { useToastContext } from '../contexts/ToastContext.tsx';
6
- import { useAgentSessions } from '../contexts/AgentSessionContext.tsx';
7
- import { copyToClipboard } from '@/utils/clipboard.ts';
8
- import type { AgentSessionAggregate } from '../../agents/types.ts';
9
- import type { DriverDimension, SidebarMode } from '../components/SmartSidebar.tsx';
1
+ import { useKeyboard } from "@opentui/react";
2
+ import { useEffect, useRef } from "react";
3
+ import { copyToClipboard } from "@/utils/clipboard.ts";
4
+ import type { AgentSessionAggregate } from "../../agents/types.ts";
5
+ import type { DriverDimension, SidebarMode } from "../components/SmartSidebar.tsx";
6
+ import { useAgentSessions } from "../contexts/AgentSessionContext.tsx";
7
+ import { useInputFocus } from "../contexts/InputContext.tsx";
8
+ import { useTimeWindow } from "../contexts/TimeWindowContext.tsx";
9
+ import { useToastContext } from "../contexts/ToastContext.tsx";
10
10
 
11
11
  function formatSessionSummary(session: AgentSessionAggregate): string {
12
12
  const effectiveTokens = session.totals.input + session.totals.output;
13
- const cost = session.totalCostUsd?.toFixed(4) ?? '0.00';
14
- const primaryModel = session.streams[0]?.modelId ?? 'unknown';
13
+ const cost = session.totalCostUsd?.toFixed(4) ?? "0.00";
14
+ const primaryModel = session.streams[0]?.modelId ?? "unknown";
15
15
  const duration = Math.round((session.lastActivityAt - session.startedAt) / 1000);
16
- const durationStr = duration > 3600
17
- ? `${Math.floor(duration / 3600)}h ${Math.floor((duration % 3600) / 60)}m`
18
- : duration > 60
19
- ? `${Math.floor(duration / 60)}m ${duration % 60}s`
20
- : `${duration}s`;
16
+ const durationStr =
17
+ duration > 3600
18
+ ? `${Math.floor(duration / 3600)}h ${Math.floor((duration % 3600) / 60)}m`
19
+ : duration > 60
20
+ ? `${Math.floor(duration / 60)}m ${duration % 60}s`
21
+ : `${duration}s`;
21
22
 
22
23
  const cacheRead = session.totals.cacheRead ?? 0;
23
24
  const cacheWrite = session.totals.cacheWrite ?? 0;
@@ -36,22 +37,24 @@ function formatSessionSummary(session: AgentSessionAggregate): string {
36
37
  if (cacheRead > 0 || cacheWrite > 0) {
37
38
  const totalInput = session.totals.input + cacheRead + cacheWrite;
38
39
  const hitRate = totalInput > 0 ? Math.round((cacheRead / totalInput) * 100) : 0;
39
- lines.push(`Cache: ${hitRate}% hit (read: ${cacheRead.toLocaleString()}, write: ${cacheWrite.toLocaleString()})`);
40
+ lines.push(
41
+ `Cache: ${hitRate}% hit (read: ${cacheRead.toLocaleString()}, write: ${cacheWrite.toLocaleString()})`,
42
+ );
40
43
  }
41
44
 
42
45
  if (session.projectPath) lines.push(`Project: ${session.projectPath}`);
43
- return lines.join('\n');
46
+ return lines.join("\n");
44
47
  }
45
48
 
46
49
  interface DashboardKeyboardState {
47
50
  showHelp: boolean;
48
51
  showSessionDrawer: boolean;
49
52
  selectedRow: number;
50
- focusedPanel: 'sessions' | 'sidebar' | 'limits';
53
+ focusedPanel: "sessions" | "sidebar" | "limits";
51
54
  sidebarCollapsed: boolean;
52
55
  filterQuery: string;
53
56
  isFiltering: boolean;
54
- sortField: 'cost' | 'tokens' | 'time';
57
+ sortField: "cost" | "tokens" | "time";
55
58
  pendingG: boolean;
56
59
  scrollOffset: number;
57
60
  limitSelectedIndex: number;
@@ -67,11 +70,13 @@ interface DashboardKeyboardActions {
67
70
  openSessionDrawer: () => void;
68
71
  closeSessionDrawer: () => void;
69
72
  setSelectedRow: (fn: (prev: number) => number) => void;
70
- setFocusedPanel: (fn: (prev: 'sessions' | 'sidebar' | 'limits') => 'sessions' | 'sidebar' | 'limits') => void;
73
+ setFocusedPanel: (
74
+ fn: (prev: "sessions" | "sidebar" | "limits") => "sessions" | "sidebar" | "limits",
75
+ ) => void;
71
76
  setSidebarCollapsed: (fn: (prev: boolean) => boolean) => void;
72
77
  setFilterQuery: (fn: (prev: string) => string) => void;
73
78
  setIsFiltering: (val: boolean) => void;
74
- setSortField: (fn: (prev: 'cost' | 'tokens' | 'time') => 'cost' | 'tokens' | 'time') => void;
79
+ setSortField: (fn: (prev: "cost" | "tokens" | "time") => "cost" | "tokens" | "time") => void;
75
80
  setPendingG: (val: boolean) => void;
76
81
  setScrollOffset: (val: number) => void;
77
82
  setLimitSelectedIndex: (fn: (prev: number) => number) => void;
@@ -120,7 +125,20 @@ export function useDashboardKeyboard({
120
125
  selectedDriverIndexRef.current = state.selectedDriverIndex;
121
126
  activeDriverFilterRef.current = state.activeDriverFilter;
122
127
  sidebarModeRef.current = state.sidebarMode;
123
- }, [state.isFiltering, state.showHelp, state.showSessionDrawer, state.pendingG, processedSessions, state.focusedPanel, state.providerCount, state.filterQuery, state.driverDimension, state.selectedDriverIndex, state.activeDriverFilter, state.sidebarMode]);
128
+ }, [
129
+ state.isFiltering,
130
+ state.showHelp,
131
+ state.showSessionDrawer,
132
+ state.pendingG,
133
+ processedSessions,
134
+ state.focusedPanel,
135
+ state.providerCount,
136
+ state.filterQuery,
137
+ state.driverDimension,
138
+ state.selectedDriverIndex,
139
+ state.activeDriverFilter,
140
+ state.sidebarMode,
141
+ ]);
124
142
 
125
143
  const { isInputFocused } = useInputFocus();
126
144
 
@@ -129,19 +147,19 @@ export function useDashboardKeyboard({
129
147
  if (key.ctrl) {
130
148
  return;
131
149
  }
132
-
150
+
133
151
  // Allow filter mode to handle its own input (no <input> element in RealTimeDashboard)
134
152
  if (isInputFocused && !isFilteringRef.current) {
135
153
  return;
136
154
  }
137
155
 
138
- if (key.sequence === '?' || (key.shift && key.name === '/')) {
139
- actions.setShowHelp(prev => !prev);
156
+ if (key.sequence === "?" || (key.shift && key.name === "/")) {
157
+ actions.setShowHelp((prev) => !prev);
140
158
  return;
141
159
  }
142
160
 
143
161
  if (modalOpenRef.current) {
144
- if (key.name === 'escape' || key.name === 'q' || key.sequence === '?') {
162
+ if (key.name === "escape" || key.name === "q" || key.sequence === "?") {
145
163
  actions.setShowHelp(() => false);
146
164
  actions.closeSessionDrawer();
147
165
  return;
@@ -149,17 +167,19 @@ export function useDashboardKeyboard({
149
167
 
150
168
  const selectedSession = processedSessions[state.selectedRow];
151
169
  if (state.showSessionDrawer && selectedSession) {
152
- if (key.name === 'c') {
170
+ if (key.name === "c") {
153
171
  const summary = formatSessionSummary(selectedSession);
154
- copyToClipboard(summary).then(() => {
155
- showToast('Copied to clipboard');
156
- }).catch(() => {
157
- showToast('Copy failed', 'error');
158
- });
172
+ copyToClipboard(summary)
173
+ .then(() => {
174
+ showToast("Copied to clipboard");
175
+ })
176
+ .catch(() => {
177
+ showToast("Copy failed", "error");
178
+ });
159
179
  return;
160
180
  }
161
- if (key.name === 'x') {
162
- showToast('Export not yet implemented', 'info');
181
+ if (key.name === "x") {
182
+ showToast("Export not yet implemented", "info");
163
183
  return;
164
184
  }
165
185
  }
@@ -167,99 +187,99 @@ export function useDashboardKeyboard({
167
187
  }
168
188
 
169
189
  if (isFilteringRef.current) {
170
- if (key.name === 'enter' || key.name === 'return') {
190
+ if (key.name === "enter" || key.name === "return") {
171
191
  actions.setIsFiltering(false);
172
192
  setInputFocused(false);
173
193
  return;
174
194
  }
175
- if (key.name === 'escape') {
176
- actions.setFilterQuery(() => '');
195
+ if (key.name === "escape") {
196
+ actions.setFilterQuery(() => "");
177
197
  actions.setIsFiltering(false);
178
198
  setInputFocused(false);
179
199
  return;
180
200
  }
181
- if (key.name === 'backspace') {
182
- actions.setFilterQuery(q => q.slice(0, -1));
201
+ if (key.name === "backspace") {
202
+ actions.setFilterQuery((q) => q.slice(0, -1));
183
203
  return;
184
204
  }
185
205
  if (key.sequence && key.sequence.length === 1 && /^[a-zA-Z0-9\-_./]$/.test(key.sequence)) {
186
- actions.setFilterQuery(q => q + key.sequence);
206
+ actions.setFilterQuery((q) => q + key.sequence);
187
207
  return;
188
208
  }
189
209
  return;
190
210
  }
191
211
 
192
- if (key.name === 'tab' && !key.shift) {
193
- actions.setFocusedPanel(curr => {
194
- if (curr === 'sessions') return 'limits';
195
- if (curr === 'limits') return 'sidebar';
196
- return 'sessions';
212
+ if (key.name === "tab" && !key.shift) {
213
+ actions.setFocusedPanel((curr) => {
214
+ if (curr === "sessions") return "limits";
215
+ if (curr === "limits") return "sidebar";
216
+ return "sessions";
197
217
  });
198
218
  return;
199
219
  }
200
- if (key.name === 'tab' && key.shift) {
201
- actions.setFocusedPanel(curr => {
202
- if (curr === 'sessions') return 'sidebar';
203
- if (curr === 'sidebar') return 'limits';
204
- return 'sessions';
220
+ if (key.name === "tab" && key.shift) {
221
+ actions.setFocusedPanel((curr) => {
222
+ if (curr === "sessions") return "sidebar";
223
+ if (curr === "sidebar") return "limits";
224
+ return "sessions";
205
225
  });
206
226
  return;
207
227
  }
208
228
 
209
- if (key.name === 'l' && focusedPanelRef.current !== 'limits') {
210
- actions.setFocusedPanel(() => 'limits');
229
+ if (key.name === "l" && focusedPanelRef.current !== "limits") {
230
+ actions.setFocusedPanel(() => "limits");
211
231
  return;
212
232
  }
213
233
 
214
- if (key.name === 'i') {
215
- actions.setSidebarCollapsed(curr => !curr);
234
+ if (key.name === "i") {
235
+ actions.setSidebarCollapsed((curr) => !curr);
216
236
  return;
217
237
  }
218
238
 
219
- if (key.name === '/' || key.sequence === '/') {
239
+ if (key.name === "/" || key.sequence === "/") {
220
240
  actions.setIsFiltering(true);
221
241
  setInputFocused(true);
222
242
  return;
223
243
  }
224
244
 
225
- if (key.name === 's') {
226
- actions.setSortField(curr => curr === 'cost' ? 'tokens' : 'cost');
245
+ if (key.name === "s") {
246
+ actions.setSortField((curr) => (curr === "cost" ? "tokens" : "cost"));
227
247
  return;
228
248
  }
229
249
 
230
250
  // Clear applied filter with Escape (when not in typing mode)
231
- if (key.name === 'escape' && filterQueryRef.current) {
232
- actions.setFilterQuery(() => '');
251
+ if (key.name === "escape" && filterQueryRef.current) {
252
+ actions.setFilterQuery(() => "");
233
253
  return;
234
254
  }
235
255
 
236
- if (key.name === 't' && key.shift) {
256
+ if (key.name === "t" && key.shift) {
237
257
  cycleWindowBack();
238
258
  return;
239
259
  }
240
260
 
241
- if (key.name === 't') {
261
+ if (key.name === "t") {
242
262
  cycleWindow();
243
263
  return;
244
264
  }
245
265
 
246
- if (key.name === 'r') {
266
+ if (key.name === "r") {
247
267
  refreshSessions();
248
268
  return;
249
269
  }
250
270
 
251
- if (focusedPanelRef.current === 'sessions') {
271
+ if (focusedPanelRef.current === "sessions") {
252
272
  const sessions = sessionsRef.current;
253
- if (key.name === 'down' || key.name === 'j') {
273
+ if (key.name === "down" || key.name === "j") {
254
274
  actions.setPendingG(false);
255
- actions.setSelectedRow(curr => Math.min(curr + 1, sessions.length - 1));
256
- } else if (key.name === 'up' || key.name === 'k') {
275
+ actions.setSelectedRow((curr) => Math.min(curr + 1, sessions.length - 1));
276
+ } else if (key.name === "up" || key.name === "k") {
257
277
  actions.setPendingG(false);
258
- actions.setSelectedRow(curr => Math.max(curr - 1, 0));
259
- } else if (key.shift && key.name === 'g') {
278
+ actions.setSelectedRow((curr) => Math.max(curr - 1, 0));
279
+ } else if (key.shift && key.name === "g") {
260
280
  actions.setPendingG(false);
261
281
  actions.setSelectedRow(() => sessions.length - 1);
262
- } else if (key.name === 'g') {
282
+ } else if (key.name === "g") {
263
283
  if (pendingGRef.current) {
264
284
  actions.setSelectedRow(() => 0);
265
285
  actions.setScrollOffset(0);
@@ -268,7 +288,7 @@ export function useDashboardKeyboard({
268
288
  actions.setPendingG(true);
269
289
  setTimeout(() => actions.setPendingG(false), 500);
270
290
  }
271
- } else if (key.name === 'return' && sessions.length > 0) {
291
+ } else if (key.name === "return" && sessions.length > 0) {
272
292
  actions.setPendingG(false);
273
293
  actions.openSessionDrawer();
274
294
  } else {
@@ -276,63 +296,63 @@ export function useDashboardKeyboard({
276
296
  }
277
297
  }
278
298
 
279
- if (focusedPanelRef.current === 'limits') {
299
+ if (focusedPanelRef.current === "limits") {
280
300
  const maxIndex = Math.max(0, providerCountRef.current - 1);
281
- if (key.name === 'left' || key.name === 'h') {
282
- actions.setLimitSelectedIndex(curr => Math.max(curr - 1, 0));
301
+ if (key.name === "left" || key.name === "h") {
302
+ actions.setLimitSelectedIndex((curr) => Math.max(curr - 1, 0));
283
303
  return;
284
- } else if (key.name === 'right' || key.name === 'l') {
285
- actions.setLimitSelectedIndex(curr => Math.min(curr + 1, maxIndex));
304
+ } else if (key.name === "right" || key.name === "l") {
305
+ actions.setLimitSelectedIndex((curr) => Math.min(curr + 1, maxIndex));
286
306
  return;
287
- } else if (key.name === 'escape') {
288
- actions.setFocusedPanel(() => 'sessions');
307
+ } else if (key.name === "escape") {
308
+ actions.setFocusedPanel(() => "sessions");
289
309
  actions.setLimitSelectedIndex(() => 0);
290
310
  return;
291
311
  }
292
312
  }
293
313
 
294
- if (focusedPanelRef.current === 'sidebar') {
295
- if (key.name === 'down' || key.name === 'j') {
296
- actions.setSelectedDriverIndex(curr => curr + 1);
314
+ if (focusedPanelRef.current === "sidebar") {
315
+ if (key.name === "down" || key.name === "j") {
316
+ actions.setSelectedDriverIndex((curr) => curr + 1);
297
317
  return;
298
- } else if (key.name === 'up' || key.name === 'k') {
299
- actions.setSelectedDriverIndex(curr => Math.max(curr - 1, 0));
318
+ } else if (key.name === "up" || key.name === "k") {
319
+ actions.setSelectedDriverIndex((curr) => Math.max(curr - 1, 0));
300
320
  return;
301
321
  }
302
-
303
- if (key.name === 'return') {
322
+
323
+ if (key.name === "return") {
304
324
  if (activeDriverFilterRef.current !== null) {
305
325
  actions.setActiveDriverFilter(null);
306
326
  } else {
307
- actions.setActiveDriverFilter('__TOGGLE_SELECTED__');
327
+ actions.setActiveDriverFilter("__TOGGLE_SELECTED__");
308
328
  }
309
329
  return;
310
330
  }
311
-
312
- if (key.name === 'm') {
313
- actions.setDriverDimension(() => 'model');
331
+
332
+ if (key.name === "m") {
333
+ actions.setDriverDimension(() => "model");
314
334
  actions.setSelectedDriverIndex(() => 0);
315
335
  return;
316
- } else if (key.name === 'p') {
317
- actions.setDriverDimension(() => 'project');
336
+ } else if (key.name === "p") {
337
+ actions.setDriverDimension(() => "project");
318
338
  actions.setSelectedDriverIndex(() => 0);
319
339
  return;
320
- } else if (key.name === 'a') {
321
- actions.setDriverDimension(() => 'agent');
340
+ } else if (key.name === "a") {
341
+ actions.setDriverDimension(() => "agent");
322
342
  actions.setSelectedDriverIndex(() => 0);
323
343
  return;
324
344
  }
325
-
326
- if (key.name === 'b') {
345
+
346
+ if (key.name === "b") {
327
347
  cycleBudgetLock();
328
348
  return;
329
349
  }
330
-
331
- if (key.name === 'escape') {
350
+
351
+ if (key.name === "escape") {
332
352
  if (activeDriverFilterRef.current !== null) {
333
353
  actions.setActiveDriverFilter(null);
334
354
  } else {
335
- actions.setFocusedPanel(() => 'sessions');
355
+ actions.setFocusedPanel(() => "sessions");
336
356
  }
337
357
  return;
338
358
  }
@@ -1,6 +1,6 @@
1
- import { useState, useMemo, useRef, useEffect } from 'react';
2
- import { totalTokenCount } from '../../agents/types.ts';
3
- import type { AgentSessionAggregate, AgentSessionStream } from '../../agents/types.ts';
1
+ import { useEffect, useMemo, useRef, useState } from "react";
2
+ import type { AgentSessionAggregate, AgentSessionStream } from "../../agents/types.ts";
3
+ import { totalTokenCount } from "../../agents/types.ts";
4
4
 
5
5
  export interface DashboardDeltas {
6
6
  cost: number;
@@ -14,16 +14,16 @@ export interface UseDashboardStateResult {
14
14
  setShowDebugInspector: React.Dispatch<React.SetStateAction<boolean>>;
15
15
  selectedRow: number;
16
16
  setSelectedRow: React.Dispatch<React.SetStateAction<number>>;
17
- focusedPanel: 'sessions' | 'sidebar';
18
- setFocusedPanel: React.Dispatch<React.SetStateAction<'sessions' | 'sidebar'>>;
17
+ focusedPanel: "sessions" | "sidebar";
18
+ setFocusedPanel: React.Dispatch<React.SetStateAction<"sessions" | "sidebar">>;
19
19
  sidebarCollapsed: boolean;
20
20
  setSidebarCollapsed: React.Dispatch<React.SetStateAction<boolean>>;
21
21
  filterQuery: string;
22
22
  setFilterQuery: React.Dispatch<React.SetStateAction<string>>;
23
23
  isFiltering: boolean;
24
24
  setIsFiltering: React.Dispatch<React.SetStateAction<boolean>>;
25
- sortField: 'cost' | 'tokens' | 'time';
26
- setSortField: React.Dispatch<React.SetStateAction<'cost' | 'tokens' | 'time'>>;
25
+ sortField: "cost" | "tokens" | "time";
26
+ setSortField: React.Dispatch<React.SetStateAction<"cost" | "tokens" | "time">>;
27
27
  deltas: DashboardDeltas;
28
28
  processedSessions: AgentSessionAggregate[];
29
29
  totalCost: number;
@@ -36,19 +36,19 @@ export function useDashboardState(sessions: AgentSessionAggregate[]): UseDashboa
36
36
  const [showHelp, setShowHelp] = useState(false);
37
37
  const [showDebugInspector, setShowDebugInspector] = useState(false);
38
38
  const [selectedRow, setSelectedRow] = useState(0);
39
- const [focusedPanel, setFocusedPanel] = useState<'sessions' | 'sidebar'>('sessions');
39
+ const [focusedPanel, setFocusedPanel] = useState<"sessions" | "sidebar">("sessions");
40
40
  const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
41
- const [filterQuery, setFilterQuery] = useState('');
41
+ const [filterQuery, setFilterQuery] = useState("");
42
42
  const [isFiltering, setIsFiltering] = useState(false);
43
- const [sortField, setSortField] = useState<'cost' | 'tokens' | 'time'>('cost');
44
-
45
- const historyRef = useRef<{time: number, cost: number, tokens: number}[]>([]);
43
+ const [sortField, setSortField] = useState<"cost" | "tokens" | "time">("cost");
44
+
45
+ const historyRef = useRef<{ time: number; cost: number; tokens: number }[]>([]);
46
46
  const [deltas, setDeltas] = useState<DashboardDeltas>({ cost: 0, tokens: 0 });
47
47
 
48
48
  const totalCost = sessions.reduce((acc, s) => acc + (s.totalCostUsd ?? 0), 0);
49
49
  const totalTokens = sessions.reduce((acc, s) => acc + totalTokenCount(s.totals), 0);
50
50
  const totalRequests = sessions.reduce((acc, s) => acc + s.requestCount, 0);
51
- const activeCount = sessions.filter(s => s.status === 'active').length;
51
+ const activeCount = sessions.filter((s) => s.status === "active").length;
52
52
 
53
53
  useEffect(() => {
54
54
  const currentTime = Date.now();
@@ -59,30 +59,32 @@ export function useDashboardState(sessions: AgentSessionAggregate[]): UseDashboa
59
59
  if (fiveMinAgo) {
60
60
  setDeltas({
61
61
  cost: totalCost - fiveMinAgo.cost,
62
- tokens: totalTokens - fiveMinAgo.tokens
62
+ tokens: totalTokens - fiveMinAgo.tokens,
63
63
  });
64
64
  }
65
65
  }, [sessions, totalCost, totalTokens]);
66
66
 
67
67
  const processedSessions = useMemo(() => {
68
68
  let result = [...sessions];
69
-
69
+
70
70
  if (filterQuery) {
71
71
  const q = filterQuery.toLowerCase();
72
- result = result.filter(s =>
73
- s.agentName.toLowerCase().includes(q) ||
74
- s.streams.some((st: AgentSessionStream) => st.modelId.toLowerCase().includes(q)) ||
75
- (s.projectPath?.toLowerCase().includes(q) ?? false)
72
+ result = result.filter(
73
+ (s) =>
74
+ s.agentName.toLowerCase().includes(q) ||
75
+ s.streams.some((st: AgentSessionStream) => st.modelId.toLowerCase().includes(q)) ||
76
+ (s.projectPath?.toLowerCase().includes(q) ?? false),
76
77
  );
77
78
  }
78
79
 
79
80
  result.sort((a, b) => {
80
- const aActive = a.status === 'active' ? 1 : 0;
81
- const bActive = b.status === 'active' ? 1 : 0;
81
+ const aActive = a.status === "active" ? 1 : 0;
82
+ const bActive = b.status === "active" ? 1 : 0;
82
83
  if (bActive !== aActive) return bActive - aActive;
83
-
84
- if (sortField === 'cost') return (b.totalCostUsd ?? 0) - (a.totalCostUsd ?? 0);
85
- if (sortField === 'tokens') return (b.totals.input + b.totals.output) - (a.totals.input + a.totals.output);
84
+
85
+ if (sortField === "cost") return (b.totalCostUsd ?? 0) - (a.totalCostUsd ?? 0);
86
+ if (sortField === "tokens")
87
+ return b.totals.input + b.totals.output - (a.totals.input + a.totals.output);
86
88
  return b.lastActivityAt - a.lastActivityAt;
87
89
  });
88
90