@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,4 +1,4 @@
1
- import { useColors } from '../contexts/ThemeContext.tsx';
1
+ import { useColors } from "../contexts/ThemeContext.tsx";
2
2
 
3
3
  interface UsageGaugeProps {
4
4
  label: string;
@@ -25,23 +25,20 @@ export function UsageGauge({
25
25
  const filledWidth = Math.round((percent / 100) * barWidth);
26
26
  const emptyWidth = barWidth - filledWidth;
27
27
 
28
- const fillColor = percent >= 90 ? colors.gaugeDanger :
29
- percent >= 70 ? colors.gaugeWarning :
30
- providerColor;
28
+ const fillColor =
29
+ percent >= 90 ? colors.gaugeDanger : percent >= 70 ? colors.gaugeWarning : providerColor;
31
30
 
32
- const filledBar = '█'.repeat(filledWidth);
33
- const emptyBar = '·'.repeat(emptyWidth);
31
+ const filledBar = "█".repeat(filledWidth);
32
+ const emptyBar = "·".repeat(emptyWidth);
34
33
 
35
- const resetText = resetsAt ? formatResetTime(resetsAt) : '';
34
+ const resetText = resetsAt ? formatResetTime(resetsAt) : "";
36
35
  const showWindowLabel = windowLabel && !label.toLowerCase().includes(windowLabel.toLowerCase());
37
36
 
38
37
  return (
39
38
  <box flexDirection="column" width={width}>
40
39
  <box flexDirection="row" justifyContent="space-between">
41
40
  <text fg={colors.textMuted}>{label}</text>
42
- <text fg={colors.text}>
43
- {usedPercent !== null ? `${Math.round(percent)}%` : '--'}
44
- </text>
41
+ <text fg={colors.text}>{usedPercent !== null ? `${Math.round(percent)}%` : "--"}</text>
45
42
  </box>
46
43
  <box flexDirection="row">
47
44
  <text>
@@ -51,8 +48,8 @@ export function UsageGauge({
51
48
  </box>
52
49
  {(showWindowLabel || resetText) && (
53
50
  <text fg={colors.textMuted}>
54
- {showWindowLabel ? windowLabel : ''}
55
- {showWindowLabel && resetText ? ' · ' : ''}
51
+ {showWindowLabel ? windowLabel : ""}
52
+ {showWindowLabel && resetText ? " · " : ""}
56
53
  {resetText}
57
54
  </text>
58
55
  )}
@@ -64,7 +61,7 @@ function formatResetTime(timestamp: number): string {
64
61
  const now = Date.now();
65
62
  const diff = timestamp - now;
66
63
 
67
- if (diff <= 0) return 'resets soon';
64
+ if (diff <= 0) return "resets soon";
68
65
 
69
66
  const minutes = Math.floor(diff / 60000);
70
67
  const hours = Math.floor(minutes / 60);
@@ -1,5 +1,5 @@
1
- export { Header } from './Header.tsx';
2
- export { StatusBar } from './StatusBar.tsx';
3
- export { UsageGauge } from './UsageGauge.tsx';
4
- export { ProviderCard } from './ProviderCard.tsx';
5
- export { DebugConsole } from './DebugConsole.tsx';
1
+ export { DebugConsole } from "./DebugConsole.tsx";
2
+ export { Header } from "./Header.tsx";
3
+ export { ProviderCard } from "./ProviderCard.tsx";
4
+ export { StatusBar } from "./StatusBar.tsx";
5
+ export { UsageGauge } from "./UsageGauge.tsx";
@@ -1,17 +1,25 @@
1
- import { createContext, useContext, useState, useEffect, useCallback, useRef, type ReactNode } from 'react';
2
- import type { AgentPlugin, SessionParseOptions } from '@/plugins/types/agent.ts';
3
- import type { AgentSessionAggregate, AgentInfo, AgentId } from '@/agents/types.ts';
4
- import { aggregateSessionUsage } from '@/agents/aggregator.ts';
5
- import { priceSessions } from '@/agents/costing.ts';
6
- import { pluginRegistry } from '@/plugins/registry.ts';
7
- import { createSandboxedHttpClient, createPluginLogger } from '@/plugins/sandbox.ts';
8
- import { createPluginContext } from '@/plugins/plugin-context-factory.ts';
9
- import { useLogs } from './LogContext.tsx';
10
- import { usePlugins } from './PluginContext.tsx';
11
- import { useStorage } from './StorageContext.tsx';
12
- import type { PricingSource } from '@/storage/types.ts';
13
- import { useDemoMode } from './DemoModeContext.tsx';
14
- import { useTimeWindow } from './TimeWindowContext.tsx';
1
+ import {
2
+ createContext,
3
+ type ReactNode,
4
+ useCallback,
5
+ useContext,
6
+ useEffect,
7
+ useRef,
8
+ useState,
9
+ } from "react";
10
+ import { aggregateSessionUsage } from "@/agents/aggregator.ts";
11
+ import { priceSessions } from "@/agents/costing.ts";
12
+ import type { AgentId, AgentInfo, AgentSessionAggregate } from "@/agents/types.ts";
13
+ import { createPluginContext } from "@/plugins/plugin-context-factory.ts";
14
+ import { pluginRegistry } from "@/plugins/registry.ts";
15
+ import { createPluginLogger, createSandboxedHttpClient } from "@/plugins/sandbox.ts";
16
+ import type { AgentPlugin, SessionParseOptions } from "@/plugins/types/agent.ts";
17
+ import { persistSessions } from "@/storage/persistence-service.ts";
18
+ import type { PricingSource } from "@/storage/types.ts";
19
+ import { useDemoMode } from "./DemoModeContext.tsx";
20
+ import { useLogs } from "./LogContext.tsx";
21
+ import { usePlugins } from "./PluginContext.tsx";
22
+ import { useTimeWindow } from "./TimeWindowContext.tsx";
15
23
 
16
24
  interface AgentSessionContextValue {
17
25
  sessions: AgentSessionAggregate[];
@@ -24,12 +32,6 @@ interface AgentSessionContextValue {
24
32
 
25
33
  const AgentSessionContext = createContext<AgentSessionContextValue | null>(null);
26
34
 
27
- const sessionPersistenceFingerprints = new Map<string, string>();
28
-
29
- function sessionFingerprint(session: AgentSessionAggregate): string {
30
- return `${session.lastActivityAt}:${session.totalCostUsd ?? 0}:${session.requestCount}`;
31
- }
32
-
33
35
  interface AgentSessionProviderProps {
34
36
  children: ReactNode;
35
37
  autoRefresh?: boolean;
@@ -48,13 +50,13 @@ export function AgentSessionProvider({
48
50
  const [error, setError] = useState<string | null>(null);
49
51
  const { debug, warn, error: logError } = useLogs();
50
52
  const { isInitialized: pluginsInitialized } = usePlugins();
51
- const { isReady: storageReady, recordAgentSession } = useStorage();
52
53
  const { demoMode, simulator } = useDemoMode();
53
54
  const { windowMs } = useTimeWindow();
54
55
  const hasBackfilled = useRef(false);
56
+ const isRefreshingRef = useRef(false);
55
57
 
56
58
  const discoverAgents = useCallback(async (): Promise<AgentInfo[]> => {
57
- const agentPlugins = pluginRegistry.getAll('agent');
59
+ const agentPlugins = pluginRegistry.getAll("agent");
58
60
  const discovered: AgentInfo[] = [];
59
61
 
60
62
  for (const plugin of agentPlugins) {
@@ -78,7 +80,7 @@ export function AgentSessionProvider({
78
80
  installed: false,
79
81
  sessionParsingSupported: false,
80
82
  };
81
- agentInfo.error = err instanceof Error ? err.message : 'Unknown error';
83
+ agentInfo.error = err instanceof Error ? err.message : "Unknown error";
82
84
  discovered.push(agentInfo);
83
85
  }
84
86
  }
@@ -86,144 +88,159 @@ export function AgentSessionProvider({
86
88
  return discovered;
87
89
  }, []);
88
90
 
89
- const fetchAgentSessions = useCallback(async (
90
- plugin: AgentPlugin,
91
- options: SessionParseOptions
92
- ): Promise<AgentSessionAggregate[]> => {
93
- const agentId = plugin.id;
94
- const agentName = plugin.name;
91
+ const fetchAgentSessions = useCallback(
92
+ async (plugin: AgentPlugin, options: SessionParseOptions): Promise<AgentSessionAggregate[]> => {
93
+ const agentId = plugin.id;
94
+ const agentName = plugin.name;
95
95
 
96
- const http = createSandboxedHttpClient(plugin.id, plugin.permissions);
97
- const logger = createPluginLogger(plugin.id);
98
- const ctx = { http, logger, config: {}, signal: AbortSignal.timeout(60_000) };
96
+ const http = createSandboxedHttpClient(plugin.id, plugin.permissions);
97
+ const logger = createPluginLogger(plugin.id);
98
+ const ctx = { http, logger, config: {}, signal: AbortSignal.timeout(60_000) };
99
99
 
100
- const rawSessions = await plugin.parseSessions(options, ctx);
100
+ const rawSessions = await plugin.parseSessions(options, ctx);
101
101
 
102
- if (rawSessions.length === 0) {
103
- return [];
104
- }
102
+ if (rawSessions.length === 0) {
103
+ return [];
104
+ }
105
105
 
106
- const aggregated = aggregateSessionUsage({
107
- agentId,
108
- agentName,
109
- rows: rawSessions,
110
- });
106
+ const aggregated = aggregateSessionUsage({
107
+ agentId,
108
+ agentName,
109
+ rows: rawSessions,
110
+ });
111
111
 
112
- return aggregated;
113
- }, []);
112
+ return aggregated;
113
+ },
114
+ [],
115
+ );
114
116
 
115
- const refreshSessions = useCallback(async (options: SessionParseOptions = {}) => {
116
- const isInitialLoad = sessions.length === 0;
117
- if (isInitialLoad) {
118
- setIsLoading(true);
119
- }
120
- setError(null);
117
+ const refreshSessions = useCallback(
118
+ async (options: SessionParseOptions = {}) => {
119
+ if (isRefreshingRef.current) return;
120
+ isRefreshingRef.current = true;
121
121
 
122
- try {
123
- if (demoMode && simulator) {
124
- const snapshot = simulator.tick();
125
- const demoSessions = snapshot.sessions;
126
- setSessions(demoSessions);
127
- setLastRefreshAt(Date.now());
128
- debug('Demo sessions refreshed', { count: demoSessions.length }, 'agent-sessions');
129
- return;
122
+ const isInitialLoad = sessions.length === 0;
123
+ if (isInitialLoad) {
124
+ setIsLoading(true);
130
125
  }
126
+ setError(null);
131
127
 
132
- debug('Starting session refresh...', undefined, 'agent-sessions');
128
+ try {
129
+ if (demoMode && simulator) {
130
+ const snapshot = simulator.tick();
131
+ const demoSessions = snapshot.sessions;
132
+ setSessions(demoSessions);
133
+ setLastRefreshAt(Date.now());
134
+ debug("Demo sessions refreshed", { count: demoSessions.length }, "agent-sessions");
135
+ return;
136
+ }
133
137
 
134
- const discoveredAgents = await discoverAgents();
135
- setAgents(discoveredAgents);
138
+ debug("Starting session refresh...", undefined, "agent-sessions");
136
139
 
137
- const agentPlugins = pluginRegistry.getAll('agent');
138
- const allAggregates: AgentSessionAggregate[] = [];
140
+ const discoveredAgents = await discoverAgents();
141
+ setAgents(discoveredAgents);
139
142
 
140
- for (const plugin of agentPlugins) {
141
- if (!plugin.capabilities.sessionParsing) {
142
- debug(`Skipping ${plugin.id}: session parsing not supported`, undefined, 'agent-sessions');
143
- continue;
144
- }
143
+ const agentPlugins = pluginRegistry.getAll("agent");
144
+ const allAggregates: AgentSessionAggregate[] = [];
145
145
 
146
- try {
147
- const pluginCtx = createPluginContext(plugin.id, plugin.permissions);
148
- const installed = await plugin.isInstalled(pluginCtx);
149
- if (!installed) {
150
- debug(`Skipping ${plugin.id}: not installed`, undefined, 'agent-sessions');
146
+ for (const plugin of agentPlugins) {
147
+ if (!plugin.capabilities.sessionParsing) {
148
+ debug(
149
+ `Skipping ${plugin.id}: session parsing not supported`,
150
+ undefined,
151
+ "agent-sessions",
152
+ );
151
153
  continue;
152
154
  }
153
155
 
154
- const aggregates = await fetchAgentSessions(plugin, options);
155
- allAggregates.push(...aggregates);
156
- debug(`Fetched ${aggregates.length} sessions from ${plugin.id}`, undefined, 'agent-sessions');
157
- } catch (err) {
158
- const errorMsg = err instanceof Error ? err.message : 'Unknown error';
159
- warn(`Failed to fetch sessions from ${plugin.id}: ${errorMsg}`, undefined, 'agent-sessions');
156
+ try {
157
+ const pluginCtx = createPluginContext(plugin.id, plugin.permissions);
158
+ const installed = await plugin.isInstalled(pluginCtx);
159
+ if (!installed) {
160
+ debug(`Skipping ${plugin.id}: not installed`, undefined, "agent-sessions");
161
+ continue;
162
+ }
163
+
164
+ const aggregates = await fetchAgentSessions(plugin, options);
165
+ allAggregates.push(...aggregates);
166
+ debug(
167
+ `Fetched ${aggregates.length} sessions from ${plugin.id}`,
168
+ undefined,
169
+ "agent-sessions",
170
+ );
171
+ } catch (err) {
172
+ const errorMsg = err instanceof Error ? err.message : "Unknown error";
173
+ warn(
174
+ `Failed to fetch sessions from ${plugin.id}: ${errorMsg}`,
175
+ undefined,
176
+ "agent-sessions",
177
+ );
178
+ }
160
179
  }
161
- }
162
180
 
163
- const pricedSessions = await priceSessions(allAggregates);
181
+ const pricedSessions = await priceSessions(allAggregates);
164
182
 
165
- pricedSessions.sort((a, b) => b.lastActivityAt - a.lastActivityAt);
183
+ pricedSessions.sort((a, b) => b.lastActivityAt - a.lastActivityAt);
166
184
 
167
- if (storageReady) {
185
+ // Persist to storage — the persistence service handles throttling
186
+ // internally (fingerprint gate + 5-minute per-session throttle).
187
+ // Safe to call every tick; state lives on globalThis, survives HMR.
168
188
  const now = Date.now();
169
- let persistedCount = 0;
170
- for (const session of pricedSessions) {
171
- const fp = sessionFingerprint(session);
172
- const prevFp = sessionPersistenceFingerprints.get(session.sessionId);
173
- if (prevFp === fp) continue;
174
-
175
- sessionPersistenceFingerprints.set(session.sessionId, fp);
176
- persistedCount++;
177
-
178
- recordAgentSession(
179
- {
180
- agentId: session.agentId,
181
- sessionId: session.sessionId,
182
- projectPath: session.projectPath ?? null,
183
- startedAt: session.startedAt ?? null,
184
- lastSeenAt: now,
185
- },
186
- {
187
- timestamp: now,
188
- lastActivityAt: session.lastActivityAt,
189
- status: session.status,
190
- totalInputTokens: session.totals.input,
191
- totalOutputTokens: session.totals.output,
192
- totalCacheReadTokens: session.totals.cacheRead ?? 0,
193
- totalCacheWriteTokens: session.totals.cacheWrite ?? 0,
194
- totalCostUsd: session.totalCostUsd ?? 0,
195
- requestCount: session.requestCount,
196
- },
197
- session.streams.map(s => ({
198
- provider: s.providerId,
199
- model: s.modelId,
200
- inputTokens: s.tokens.input,
201
- outputTokens: s.tokens.output,
202
- cacheReadTokens: s.tokens.cacheRead ?? 0,
203
- cacheWriteTokens: s.tokens.cacheWrite ?? 0,
204
- costUsd: s.costUsd ?? 0,
205
- requestCount: s.requestCount,
206
- pricingSource: (s.pricingSource as PricingSource) ?? null,
207
- }))
208
- );
209
- }
210
- debug(`Persisted ${persistedCount}/${pricedSessions.length} changed sessions to storage`, undefined, 'agent-sessions');
211
- }
212
-
213
- setSessions(pricedSessions);
214
- setLastRefreshAt(Date.now());
189
+ const persistData = pricedSessions.map((session) => ({
190
+ session: {
191
+ agentId: session.agentId,
192
+ sessionId: session.sessionId,
193
+ projectPath: session.projectPath ?? null,
194
+ startedAt: session.startedAt ?? null,
195
+ lastSeenAt: now,
196
+ },
197
+ snapshot: {
198
+ timestamp: now,
199
+ lastActivityAt: session.lastActivityAt,
200
+ status: session.status,
201
+ totalInputTokens: session.totals.input,
202
+ totalOutputTokens: session.totals.output,
203
+ totalCacheReadTokens: session.totals.cacheRead ?? 0,
204
+ totalCacheWriteTokens: session.totals.cacheWrite ?? 0,
205
+ totalCostUsd: session.totalCostUsd ?? 0,
206
+ requestCount: session.requestCount,
207
+ },
208
+ streams: session.streams.map((s) => ({
209
+ provider: s.providerId,
210
+ model: s.modelId,
211
+ inputTokens: s.tokens.input,
212
+ outputTokens: s.tokens.output,
213
+ cacheReadTokens: s.tokens.cacheRead ?? 0,
214
+ cacheWriteTokens: s.tokens.cacheWrite ?? 0,
215
+ costUsd: s.costUsd ?? 0,
216
+ requestCount: s.requestCount,
217
+ pricingSource: (s.pricingSource as PricingSource) ?? null,
218
+ })),
219
+ }));
220
+ const persistedCount = persistSessions(persistData);
221
+ debug(
222
+ `Persisted ${persistedCount}/${pricedSessions.length} sessions to storage`,
223
+ undefined,
224
+ "agent-sessions",
225
+ );
226
+
227
+ setSessions(pricedSessions);
228
+ setLastRefreshAt(Date.now());
215
229
 
216
- debug(`Session refresh complete`, { count: pricedSessions.length }, 'agent-sessions');
217
- } catch (err) {
218
- const errorMsg = err instanceof Error ? err.message : 'Unknown error';
219
- logError(`Session refresh failed: ${errorMsg}`, undefined, 'agent-sessions');
220
- setError(errorMsg);
221
- } finally {
222
- if (isInitialLoad) {
223
- setIsLoading(false);
230
+ debug(`Session refresh complete`, { count: pricedSessions.length }, "agent-sessions");
231
+ } catch (err) {
232
+ const errorMsg = err instanceof Error ? err.message : "Unknown error";
233
+ logError(`Session refresh failed: ${errorMsg}`, undefined, "agent-sessions");
234
+ setError(errorMsg);
235
+ } finally {
236
+ isRefreshingRef.current = false;
237
+ if (isInitialLoad) {
238
+ setIsLoading(false);
239
+ }
224
240
  }
225
- }
226
- }, [sessions.length, discoverAgents, fetchAgentSessions, debug, warn, logError, storageReady, recordAgentSession, demoMode]);
241
+ },
242
+ [sessions.length, discoverAgents, fetchAgentSessions, debug, warn, logError, demoMode],
243
+ );
227
244
 
228
245
  const refreshSessionsRef = useRef(refreshSessions);
229
246
  refreshSessionsRef.current = refreshSessions;
@@ -243,15 +260,15 @@ export function AgentSessionProvider({
243
260
  useEffect(() => {
244
261
  if (!autoRefresh) return;
245
262
 
246
- debug(`Setting up session refresh interval: ${refreshInterval}ms`, undefined, 'agent-sessions');
247
-
263
+ debug(`Setting up session refresh interval: ${refreshInterval}ms`, undefined, "agent-sessions");
264
+
248
265
  const intervalId = setInterval(() => {
249
- debug('Interval tick - refreshing sessions', undefined, 'agent-sessions');
266
+ debug("Interval tick - refreshing sessions", undefined, "agent-sessions");
250
267
  refreshSessionsRef.current();
251
268
  }, refreshInterval);
252
269
 
253
270
  return () => {
254
- debug('Clearing session refresh interval', undefined, 'agent-sessions');
271
+ debug("Clearing session refresh interval", undefined, "agent-sessions");
255
272
  clearInterval(intervalId);
256
273
  };
257
274
  }, [autoRefresh, refreshInterval, debug]);
@@ -265,29 +282,25 @@ export function AgentSessionProvider({
265
282
  refreshSessions,
266
283
  };
267
284
 
268
- return (
269
- <AgentSessionContext.Provider value={value}>
270
- {children}
271
- </AgentSessionContext.Provider>
272
- );
285
+ return <AgentSessionContext.Provider value={value}>{children}</AgentSessionContext.Provider>;
273
286
  }
274
287
 
275
288
  export function useAgentSessions(): AgentSessionContextValue {
276
289
  const context = useContext(AgentSessionContext);
277
290
  if (!context) {
278
- throw new Error('useAgentSessions must be used within AgentSessionProvider');
291
+ throw new Error("useAgentSessions must be used within AgentSessionProvider");
279
292
  }
280
293
  return context;
281
294
  }
282
295
 
283
296
  export function useActiveSessions(): AgentSessionAggregate[] {
284
297
  const { sessions } = useAgentSessions();
285
- return sessions.filter(s => s.status === 'active');
298
+ return sessions.filter((s) => s.status === "active");
286
299
  }
287
300
 
288
301
  export function useSessionsByAgent(agentId: AgentId): AgentSessionAggregate[] {
289
302
  const { sessions } = useAgentSessions();
290
- return sessions.filter(s => s.agentId === agentId);
303
+ return sessions.filter((s) => s.agentId === agentId);
291
304
  }
292
305
 
293
306
  export function useTotalCost(): number {
@@ -302,6 +315,6 @@ export function useTotalTokens(): { input: number; output: number } {
302
315
  input: acc.input + s.totals.input,
303
316
  output: acc.output + s.totals.output,
304
317
  }),
305
- { input: 0, output: 0 }
318
+ { input: 0, output: 0 },
306
319
  );
307
320
  }
@@ -1,14 +1,14 @@
1
- import { createContext, useContext, useState, useEffect, useCallback, type ReactNode } from 'react';
2
- import { loadConfig, saveConfig, DEFAULT_CONFIG, type AppConfig } from '@/config/schema.ts';
3
- import { useDemoMode } from './DemoModeContext.tsx';
1
+ import { createContext, type ReactNode, useCallback, useContext, useEffect, useState } from "react";
2
+ import { type AppConfig, DEFAULT_CONFIG, loadConfig, saveConfig } from "@/config/schema.ts";
3
+ import { useDemoMode } from "./DemoModeContext.tsx";
4
4
 
5
5
  interface ConfigContextValue {
6
6
  config: AppConfig;
7
7
  isLoading: boolean;
8
8
  updateConfig: (updates: Partial<AppConfig>) => void;
9
- updateRefresh: (updates: Partial<AppConfig['refresh']>) => void;
10
- updateDisplay: (updates: Partial<AppConfig['display']>) => void;
11
- updateNotifications: (updates: Partial<AppConfig['notifications']>) => void;
9
+ updateRefresh: (updates: Partial<AppConfig["refresh"]>) => void;
10
+ updateDisplay: (updates: Partial<AppConfig["display"]>) => void;
11
+ updateNotifications: (updates: Partial<AppConfig["notifications"]>) => void;
12
12
  resetToDefaults: () => void;
13
13
  saveNow: () => Promise<void>;
14
14
  }
@@ -32,21 +32,23 @@ export function ConfigProvider({ children, initialConfig }: ConfigProviderProps)
32
32
  },
33
33
  display: {
34
34
  ...DEFAULT_CONFIG.display,
35
- defaultTimeWindow: '1h',
35
+ defaultTimeWindow: "1h",
36
36
  sidebarCollapsed: false,
37
37
  },
38
38
  budgets: {
39
39
  daily: 50,
40
40
  weekly: 200,
41
41
  monthly: 800,
42
- currency: 'USD',
42
+ currency: "USD",
43
43
  },
44
44
  alerts: {
45
45
  ...DEFAULT_CONFIG.alerts,
46
46
  criticalPercent: 90,
47
47
  },
48
48
  };
49
- const [config, setConfig] = useState<AppConfig>(initialConfig ?? (demoMode ? demoConfig : DEFAULT_CONFIG));
49
+ const [config, setConfig] = useState<AppConfig>(
50
+ initialConfig ?? (demoMode ? demoConfig : DEFAULT_CONFIG),
51
+ );
50
52
  const [isLoading, setIsLoading] = useState(!initialConfig);
51
53
  const [pendingSave, setPendingSave] = useState(false);
52
54
 
@@ -79,7 +81,7 @@ export function ConfigProvider({ children, initialConfig }: ConfigProviderProps)
79
81
  setPendingSave(true);
80
82
  }, []);
81
83
 
82
- const updateRefresh = useCallback((updates: Partial<AppConfig['refresh']>) => {
84
+ const updateRefresh = useCallback((updates: Partial<AppConfig["refresh"]>) => {
83
85
  setConfig((prev) => ({
84
86
  ...prev,
85
87
  refresh: { ...prev.refresh, ...updates },
@@ -87,7 +89,7 @@ export function ConfigProvider({ children, initialConfig }: ConfigProviderProps)
87
89
  setPendingSave(true);
88
90
  }, []);
89
91
 
90
- const updateDisplay = useCallback((updates: Partial<AppConfig['display']>) => {
92
+ const updateDisplay = useCallback((updates: Partial<AppConfig["display"]>) => {
91
93
  setConfig((prev) => ({
92
94
  ...prev,
93
95
  display: { ...prev.display, ...updates },
@@ -95,7 +97,7 @@ export function ConfigProvider({ children, initialConfig }: ConfigProviderProps)
95
97
  setPendingSave(true);
96
98
  }, []);
97
99
 
98
- const updateNotifications = useCallback((updates: Partial<AppConfig['notifications']>) => {
100
+ const updateNotifications = useCallback((updates: Partial<AppConfig["notifications"]>) => {
99
101
  setConfig((prev) => ({
100
102
  ...prev,
101
103
  notifications: { ...prev.notifications, ...updates },
@@ -135,7 +137,7 @@ export function ConfigProvider({ children, initialConfig }: ConfigProviderProps)
135
137
  export function useConfig(): ConfigContextValue {
136
138
  const context = useContext(ConfigContext);
137
139
  if (!context) {
138
- throw new Error('useConfig must be used within a ConfigProvider');
140
+ throw new Error("useConfig must be used within a ConfigProvider");
139
141
  }
140
142
  return context;
141
143
  }