@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,10 +1,18 @@
1
- import { createContext, useContext, useState, useEffect, useRef, useCallback, type ReactNode } from 'react';
2
- import { pluginRegistry } from '@/plugins/registry.ts';
3
- import type { ActivityUpdate } from '@/plugins/types/agent.ts';
4
- import { createPluginContext } from '@/plugins/plugin-context-factory.ts';
5
- import { usePlugins } from './PluginContext.tsx';
6
- import { useLogs } from './LogContext.tsx';
7
- import { useDemoMode } from './DemoModeContext.tsx';
1
+ import {
2
+ createContext,
3
+ type ReactNode,
4
+ useCallback,
5
+ useContext,
6
+ useEffect,
7
+ useRef,
8
+ useState,
9
+ } from "react";
10
+ import { createPluginContext } from "@/plugins/plugin-context-factory.ts";
11
+ import { pluginRegistry } from "@/plugins/registry.ts";
12
+ import type { ActivityUpdate } from "@/plugins/types/agent.ts";
13
+ import { useDemoMode } from "./DemoModeContext.tsx";
14
+ import { useLogs } from "./LogContext.tsx";
15
+ import { usePlugins } from "./PluginContext.tsx";
8
16
 
9
17
  type ActivityListener = (delta: number, timestamp: number) => void;
10
18
 
@@ -16,14 +24,22 @@ interface RealTimeActivityContextValue {
16
24
 
17
25
  const RealTimeActivityContext = createContext<RealTimeActivityContextValue | null>(null);
18
26
 
27
+ const LAST_SEEN_MAX_AGE_MS = 5 * 60_000;
28
+ const LAST_SEEN_MAX_ENTRIES = 10_000;
29
+
30
+ interface LastSeenEntry {
31
+ tokens: number;
32
+ updatedAt: number;
33
+ }
34
+
19
35
  export function RealTimeActivityProvider({ children }: { children: ReactNode }) {
20
36
  const [lastActivityAt, setLastActivityAt] = useState<number | null>(null);
21
37
  const [isWatching, setIsWatching] = useState(false);
22
38
  const { isInitialized: pluginsInitialized } = usePlugins();
23
39
  const { debug, info } = useLogs();
24
40
  const { demoMode } = useDemoMode();
25
-
26
- const lastSeenRef = useRef<Map<string, number>>(new Map());
41
+
42
+ const lastSeenRef = useRef<Map<string, LastSeenEntry>>(new Map());
27
43
  const listenersRef = useRef<Set<ActivityListener>>(new Set());
28
44
  const cleanupRef = useRef<(() => void) | null>(null);
29
45
 
@@ -34,35 +50,39 @@ export function RealTimeActivityProvider({ children }: { children: ReactNode })
34
50
  };
35
51
  }, []);
36
52
 
37
- const handleActivityUpdate = useCallback((update: ActivityUpdate) => {
38
- const key = `${update.sessionId}:${update.messageId}`;
39
- const prevTokens = lastSeenRef.current.get(key) ?? 0;
40
- const newTokens = update.tokens.input + update.tokens.output + (update.tokens.reasoning ?? 0);
41
- const delta = Math.max(0, newTokens - prevTokens);
42
-
43
- if (delta > 0) {
44
- lastSeenRef.current.set(key, newTokens);
45
- setLastActivityAt(update.timestamp);
46
- debug(`Real-time activity: +${delta} tokens`, { sessionId: update.sessionId }, 'realtime');
47
-
48
- for (const listener of listenersRef.current) {
49
- listener(delta, update.timestamp);
53
+ const handleActivityUpdate = useCallback(
54
+ (update: ActivityUpdate) => {
55
+ const key = `${update.sessionId}:${update.messageId}`;
56
+ const prev = lastSeenRef.current.get(key);
57
+ const prevTokens = prev?.tokens ?? 0;
58
+ const newTokens = update.tokens.input + update.tokens.output + (update.tokens.reasoning ?? 0);
59
+ const delta = Math.max(0, newTokens - prevTokens);
60
+
61
+ if (delta > 0) {
62
+ lastSeenRef.current.set(key, { tokens: newTokens, updatedAt: Date.now() });
63
+ setLastActivityAt(update.timestamp);
64
+ debug(`Real-time activity: +${delta} tokens`, { sessionId: update.sessionId }, "realtime");
65
+
66
+ for (const listener of listenersRef.current) {
67
+ listener(delta, update.timestamp);
68
+ }
50
69
  }
51
- }
52
- }, [debug]);
70
+ },
71
+ [debug],
72
+ );
53
73
 
54
74
  useEffect(() => {
55
75
  if (demoMode || !pluginsInitialized) return;
56
76
 
57
- const agentPlugins = pluginRegistry.getAll('agent');
77
+ const agentPlugins = pluginRegistry.getAll("agent");
58
78
  const cleanups: (() => void)[] = [];
59
79
 
60
80
  for (const plugin of agentPlugins) {
61
81
  if (plugin.capabilities.realTimeTracking && plugin.startActivityWatch) {
62
82
  const ctx = createPluginContext(plugin.id, plugin.permissions);
63
- info(`Starting real-time activity watch for ${plugin.id}`, undefined, 'realtime');
83
+ info(`Starting real-time activity watch for ${plugin.id}`, undefined, "realtime");
64
84
  plugin.startActivityWatch(ctx, handleActivityUpdate);
65
-
85
+
66
86
  if (plugin.stopActivityWatch) {
67
87
  const stopFn = plugin.stopActivityWatch.bind(plugin, ctx);
68
88
  cleanups.push(stopFn);
@@ -73,7 +93,9 @@ export function RealTimeActivityProvider({ children }: { children: ReactNode })
73
93
  if (cleanups.length > 0) {
74
94
  setIsWatching(true);
75
95
  cleanupRef.current = () => {
76
- cleanups.forEach(fn => fn());
96
+ cleanups.forEach((fn) => {
97
+ fn();
98
+ });
77
99
  setIsWatching(false);
78
100
  };
79
101
  }
@@ -88,14 +110,28 @@ export function RealTimeActivityProvider({ children }: { children: ReactNode })
88
110
 
89
111
  useEffect(() => {
90
112
  const interval = setInterval(() => {
113
+ const map = lastSeenRef.current;
91
114
  const now = Date.now();
92
- if (lastActivityAt && now - lastActivityAt > 60000) {
93
- lastSeenRef.current.clear();
115
+
116
+ for (const [key, entry] of map) {
117
+ if (now - entry.updatedAt > LAST_SEEN_MAX_AGE_MS) {
118
+ map.delete(key);
119
+ }
94
120
  }
95
- }, 30000);
121
+
122
+ if (map.size > LAST_SEEN_MAX_ENTRIES) {
123
+ const excess = map.size - LAST_SEEN_MAX_ENTRIES;
124
+ const keys = map.keys();
125
+ for (let i = 0; i < excess; i++) {
126
+ const next = keys.next();
127
+ if (next.done) break;
128
+ map.delete(next.value);
129
+ }
130
+ }
131
+ }, 30_000);
96
132
 
97
133
  return () => clearInterval(interval);
98
- }, [lastActivityAt]);
134
+ }, []);
99
135
 
100
136
  const value: RealTimeActivityContextValue = {
101
137
  lastActivityAt,
@@ -104,16 +140,14 @@ export function RealTimeActivityProvider({ children }: { children: ReactNode })
104
140
  };
105
141
 
106
142
  return (
107
- <RealTimeActivityContext.Provider value={value}>
108
- {children}
109
- </RealTimeActivityContext.Provider>
143
+ <RealTimeActivityContext.Provider value={value}>{children}</RealTimeActivityContext.Provider>
110
144
  );
111
145
  }
112
146
 
113
147
  export function useRealTimeActivity(): RealTimeActivityContextValue {
114
148
  const context = useContext(RealTimeActivityContext);
115
149
  if (!context) {
116
- throw new Error('useRealTimeActivity must be used within RealTimeActivityProvider');
150
+ throw new Error("useRealTimeActivity must be used within RealTimeActivityProvider");
117
151
  }
118
152
  return context;
119
153
  }
@@ -1,18 +1,31 @@
1
- import { createContext, useContext, useEffect, useRef, useState, useCallback, useMemo, type ReactNode } from 'react';
2
- import { initDatabase, closeDatabase, isDatabaseInitialized, getAppRunId } from '@/storage/db.ts';
3
- import { insertProviderSnapshotBatch } from '@/storage/repos/providerSnapshots.ts';
4
- import { insertUsageEventBatch } from '@/storage/repos/usageEvents.ts';
5
- import { upsertAgentSession, insertAgentSessionSnapshot, getLatestStreamTotalsForAllSessions } from '@/storage/repos/agentSessions.ts';
1
+ import {
2
+ createContext,
3
+ type ReactNode,
4
+ useCallback,
5
+ useContext,
6
+ useEffect,
7
+ useMemo,
8
+ useRef,
9
+ useState,
10
+ } from "react";
11
+ import { closeDatabase, getAppRunId, initDatabase, isDatabaseInitialized } from "@/storage/db.ts";
12
+ import {
13
+ getLatestStreamTotalsForAllSessions,
14
+ insertAgentSessionSnapshot,
15
+ upsertAgentSession,
16
+ } from "@/storage/repos/agentSessions.ts";
17
+ import { insertProviderSnapshotBatch } from "@/storage/repos/providerSnapshots.ts";
18
+ import { insertUsageEventBatch } from "@/storage/repos/usageEvents.ts";
6
19
  import type {
7
- ProviderSnapshotInsert,
8
- UsageEventInsert,
9
- AgentSessionUpsert,
10
20
  AgentSessionSnapshotInsert,
11
21
  AgentSessionStreamSnapshotRow,
22
+ AgentSessionUpsert,
23
+ ProviderSnapshotInsert,
12
24
  StreamTotals,
13
- } from '@/storage/types.ts';
14
- import { computeStreamDelta } from '@/storage/types.ts';
15
- import { useDemoMode } from './DemoModeContext.tsx';
25
+ UsageEventInsert,
26
+ } from "@/storage/types.ts";
27
+ import { computeStreamDelta } from "@/storage/types.ts";
28
+ import { useDemoMode } from "./DemoModeContext.tsx";
16
29
 
17
30
  interface StorageContextValue {
18
31
  isReady: boolean;
@@ -21,14 +34,27 @@ interface StorageContextValue {
21
34
  recordUsageEvents: (events: UsageEventInsert[]) => void;
22
35
  recordAgentSession: (
23
36
  session: AgentSessionUpsert,
24
- snapshot: Omit<AgentSessionSnapshotInsert, 'agentSessionId'>,
25
- streams: Omit<AgentSessionStreamSnapshotRow, 'agentSessionSnapshotId'>[]
37
+ snapshot: Omit<AgentSessionSnapshotInsert, "agentSessionId">,
38
+ streams: Omit<AgentSessionStreamSnapshotRow, "agentSessionSnapshotId">[],
26
39
  ) => number | null;
27
40
  }
28
41
 
29
42
  const StorageContext = createContext<StorageContextValue | null>(null);
30
43
 
31
44
  const SNAPSHOT_INTERVAL_MS = 60_000;
45
+ const MAX_TRACKED_SESSIONS = 1_000;
46
+ const MAX_TRACKED_STREAMS = 5_000;
47
+
48
+ function capMapSize<K, V>(map: Map<K, V>, max: number): void {
49
+ if (map.size <= max) return;
50
+ const excess = map.size - max;
51
+ const keys = map.keys();
52
+ for (let i = 0; i < excess; i++) {
53
+ const next = keys.next();
54
+ if (next.done) break;
55
+ map.delete(next.value);
56
+ }
57
+ }
32
58
 
33
59
  interface StorageProviderProps {
34
60
  children: ReactNode;
@@ -70,13 +96,13 @@ export function StorageProvider({ children }: StorageProviderProps) {
70
96
  });
71
97
  }
72
98
  } catch (err) {
73
- console.error('Failed to seed previous totals from DB:', err);
99
+ console.error("Failed to seed previous totals from DB:", err);
74
100
  }
75
101
  setIsReady(true);
76
102
  setAppRunId(getAppRunId());
77
103
  }
78
104
  } catch (err) {
79
- console.error('Failed to initialize database:', err);
105
+ console.error("Failed to initialize database:", err);
80
106
  }
81
107
  }
82
108
 
@@ -90,109 +116,118 @@ export function StorageProvider({ children }: StorageProviderProps) {
90
116
  };
91
117
  }, [demoMode]);
92
118
 
93
- const recordProviderSnapshots = useCallback((snapshots: ProviderSnapshotInsert[]) => {
94
- if (!isReady || demoMode || snapshots.length === 0) return;
119
+ const recordProviderSnapshots = useCallback(
120
+ (snapshots: ProviderSnapshotInsert[]) => {
121
+ if (!isReady || demoMode || snapshots.length === 0) return;
95
122
 
96
- const now = Date.now();
97
- const filtered = snapshots.filter(s => {
98
- const lastTs = lastProviderSnapshotRef.current.get(s.provider) ?? 0;
99
- return now - lastTs >= SNAPSHOT_INTERVAL_MS;
100
- });
123
+ const now = Date.now();
124
+ const filtered = snapshots.filter((s) => {
125
+ const lastTs = lastProviderSnapshotRef.current.get(s.provider) ?? 0;
126
+ return now - lastTs >= SNAPSHOT_INTERVAL_MS;
127
+ });
101
128
 
102
- if (filtered.length === 0) return;
129
+ if (filtered.length === 0) return;
103
130
 
104
- try {
105
- insertProviderSnapshotBatch(filtered);
106
- for (const s of filtered) {
107
- lastProviderSnapshotRef.current.set(s.provider, now);
131
+ try {
132
+ insertProviderSnapshotBatch(filtered);
133
+ for (const s of filtered) {
134
+ lastProviderSnapshotRef.current.set(s.provider, now);
135
+ }
136
+ } catch (err) {
137
+ console.error("Failed to record provider snapshots:", err);
108
138
  }
109
- } catch (err) {
110
- console.error('Failed to record provider snapshots:', err);
111
- }
112
- }, [isReady, demoMode]);
139
+ },
140
+ [isReady, demoMode],
141
+ );
113
142
 
114
- const recordUsageEvents = useCallback((events: UsageEventInsert[]) => {
115
- if (!isReady || demoMode || events.length === 0) return;
143
+ const recordUsageEvents = useCallback(
144
+ (events: UsageEventInsert[]) => {
145
+ if (!isReady || demoMode || events.length === 0) return;
116
146
 
117
- try {
118
- insertUsageEventBatch(events);
119
- } catch (err) {
120
- console.error('Failed to record usage events:', err);
121
- }
122
- }, [isReady, demoMode]);
147
+ try {
148
+ insertUsageEventBatch(events);
149
+ } catch (err) {
150
+ console.error("Failed to record usage events:", err);
151
+ }
152
+ },
153
+ [isReady, demoMode],
154
+ );
123
155
 
124
- const recordAgentSession = useCallback((
125
- session: AgentSessionUpsert,
126
- snapshot: Omit<AgentSessionSnapshotInsert, 'agentSessionId'>,
127
- streams: Omit<AgentSessionStreamSnapshotRow, 'agentSessionSnapshotId'>[]
128
- ): number | null => {
129
- if (!isReady || demoMode) return null;
156
+ const recordAgentSession = useCallback(
157
+ (
158
+ session: AgentSessionUpsert,
159
+ snapshot: Omit<AgentSessionSnapshotInsert, "agentSessionId">,
160
+ streams: Omit<AgentSessionStreamSnapshotRow, "agentSessionSnapshotId">[],
161
+ ): number | null => {
162
+ if (!isReady || demoMode) return null;
130
163
 
131
- const sessionKey = `${session.agentId}:${session.sessionId}`;
132
- const now = Date.now();
133
- const lastTs = lastSessionSnapshotRef.current.get(sessionKey) ?? 0;
164
+ const sessionKey = `${session.agentId}:${session.sessionId}`;
165
+ const now = Date.now();
166
+ const lastTs = lastSessionSnapshotRef.current.get(sessionKey) ?? 0;
134
167
 
135
- if (now - lastTs < SNAPSHOT_INTERVAL_MS) {
136
- return null;
137
- }
168
+ if (now - lastTs < SNAPSHOT_INTERVAL_MS) {
169
+ return null;
170
+ }
171
+
172
+ try {
173
+ const agentSessionId = upsertAgentSession(session);
174
+
175
+ const snapshotId = insertAgentSessionSnapshot({ ...snapshot, agentSessionId }, streams);
176
+
177
+ lastSessionSnapshotRef.current.set(sessionKey, now);
178
+
179
+ const usageEvents: UsageEventInsert[] = [];
180
+ for (const stream of streams) {
181
+ const streamKey = `${sessionKey}:${stream.provider}:${stream.model}`;
182
+ const current: StreamTotals = {
183
+ inputTokens: stream.inputTokens,
184
+ outputTokens: stream.outputTokens,
185
+ cacheReadTokens: stream.cacheReadTokens,
186
+ cacheWriteTokens: stream.cacheWriteTokens,
187
+ costUsd: stream.costUsd,
188
+ requestCount: stream.requestCount,
189
+ };
190
+
191
+ const previous = previousTotalsRef.current.get(streamKey);
192
+ const delta = computeStreamDelta(current, previous);
193
+
194
+ if (delta) {
195
+ usageEvents.push({
196
+ timestamp: now,
197
+ source: "agent",
198
+ provider: stream.provider,
199
+ model: stream.model,
200
+ agentId: session.agentId,
201
+ sessionId: session.sessionId,
202
+ projectPath: session.projectPath ?? null,
203
+ inputTokens: delta.inputTokens,
204
+ outputTokens: delta.outputTokens,
205
+ cacheReadTokens: delta.cacheReadTokens,
206
+ cacheWriteTokens: delta.cacheWriteTokens,
207
+ costUsd: delta.costUsd,
208
+ requestCount: delta.requestCount,
209
+ pricingSource: stream.pricingSource ?? null,
210
+ });
211
+ }
138
212
 
139
- try {
140
- const agentSessionId = upsertAgentSession(session);
141
-
142
- const snapshotId = insertAgentSessionSnapshot(
143
- { ...snapshot, agentSessionId },
144
- streams
145
- );
146
-
147
- lastSessionSnapshotRef.current.set(sessionKey, now);
148
-
149
- const usageEvents: UsageEventInsert[] = [];
150
- for (const stream of streams) {
151
- const streamKey = `${sessionKey}:${stream.provider}:${stream.model}`;
152
- const current: StreamTotals = {
153
- inputTokens: stream.inputTokens,
154
- outputTokens: stream.outputTokens,
155
- cacheReadTokens: stream.cacheReadTokens,
156
- cacheWriteTokens: stream.cacheWriteTokens,
157
- costUsd: stream.costUsd,
158
- requestCount: stream.requestCount,
159
- };
160
-
161
- const previous = previousTotalsRef.current.get(streamKey);
162
- const delta = computeStreamDelta(current, previous);
163
-
164
- if (delta) {
165
- usageEvents.push({
166
- timestamp: now,
167
- source: 'agent',
168
- provider: stream.provider,
169
- model: stream.model,
170
- agentId: session.agentId,
171
- sessionId: session.sessionId,
172
- projectPath: session.projectPath ?? null,
173
- inputTokens: delta.inputTokens,
174
- outputTokens: delta.outputTokens,
175
- cacheReadTokens: delta.cacheReadTokens,
176
- cacheWriteTokens: delta.cacheWriteTokens,
177
- costUsd: delta.costUsd,
178
- requestCount: delta.requestCount,
179
- pricingSource: stream.pricingSource ?? null,
180
- });
213
+ previousTotalsRef.current.set(streamKey, current);
181
214
  }
182
215
 
183
- previousTotalsRef.current.set(streamKey, current);
184
- }
216
+ if (usageEvents.length > 0) {
217
+ insertUsageEventBatch(usageEvents);
218
+ }
185
219
 
186
- if (usageEvents.length > 0) {
187
- insertUsageEventBatch(usageEvents);
188
- }
220
+ capMapSize(lastSessionSnapshotRef.current, MAX_TRACKED_SESSIONS);
221
+ capMapSize(previousTotalsRef.current, MAX_TRACKED_STREAMS);
189
222
 
190
- return snapshotId;
191
- } catch (err) {
192
- console.error('Failed to record agent session:', err);
193
- return null;
194
- }
195
- }, [isReady, demoMode]);
223
+ return snapshotId;
224
+ } catch (err) {
225
+ console.error("Failed to record agent session:", err);
226
+ return null;
227
+ }
228
+ },
229
+ [isReady, demoMode],
230
+ );
196
231
 
197
232
  useEffect(() => {
198
233
  if (!demoMode || !isReady || !simulator) return;
@@ -204,25 +239,24 @@ export function StorageProvider({ children }: StorageProviderProps) {
204
239
  return () => clearInterval(interval);
205
240
  }, [demoMode, isReady, simulator]);
206
241
 
207
- const value: StorageContextValue = useMemo(() => ({
208
- isReady,
209
- appRunId,
210
- recordProviderSnapshots,
211
- recordUsageEvents,
212
- recordAgentSession,
213
- }), [isReady, appRunId, recordProviderSnapshots, recordUsageEvents, recordAgentSession]);
214
-
215
- return (
216
- <StorageContext.Provider value={value}>
217
- {children}
218
- </StorageContext.Provider>
242
+ const value: StorageContextValue = useMemo(
243
+ () => ({
244
+ isReady,
245
+ appRunId,
246
+ recordProviderSnapshots,
247
+ recordUsageEvents,
248
+ recordAgentSession,
249
+ }),
250
+ [isReady, appRunId, recordProviderSnapshots, recordUsageEvents, recordAgentSession],
219
251
  );
252
+
253
+ return <StorageContext.Provider value={value}>{children}</StorageContext.Provider>;
220
254
  }
221
255
 
222
256
  export function useStorage(): StorageContextValue {
223
257
  const context = useContext(StorageContext);
224
258
  if (!context) {
225
- throw new Error('useStorage must be used within StorageProvider');
259
+ throw new Error("useStorage must be used within StorageProvider");
226
260
  }
227
261
  return context;
228
262
  }
@@ -1,6 +1,12 @@
1
- import { createContext, useContext, useState, type ReactNode } from 'react';
2
- import type { ThemePlugin, ThemeColors, ThemeComponents, ColorScheme, ColorSchemePreference } from '@/plugins/types/theme.ts';
3
- import { tokyoNightTheme } from '@/plugins/themes/tokyo-night.ts';
1
+ import { createContext, type ReactNode, useContext, useState } from "react";
2
+ import { tokyoNightTheme } from "@/plugins/themes/tokyo-night.ts";
3
+ import type {
4
+ ColorScheme,
5
+ ColorSchemePreference,
6
+ ThemeColors,
7
+ ThemeComponents,
8
+ ThemePlugin,
9
+ } from "@/plugins/types/theme.ts";
4
10
 
5
11
  interface ThemeContextValue {
6
12
  theme: ThemePlugin;
@@ -42,17 +48,13 @@ export function ThemeProvider({ initialTheme, children }: ThemeProviderProps) {
42
48
  setPreviewTheme,
43
49
  };
44
50
 
45
- return (
46
- <ThemeContext.Provider value={value}>
47
- {children}
48
- </ThemeContext.Provider>
49
- );
51
+ return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
50
52
  }
51
53
 
52
54
  export function useTheme(): ThemeContextValue {
53
55
  const context = useContext(ThemeContext);
54
56
  if (!context) {
55
- throw new Error('useTheme must be used within ThemeProvider');
57
+ throw new Error("useTheme must be used within ThemeProvider");
56
58
  }
57
59
  return context;
58
60
  }
@@ -70,16 +72,19 @@ export function resolveTheme(
70
72
  if (themes.length === 0) return tokyoNightTheme;
71
73
 
72
74
  const targetScheme: ColorScheme | null =
73
- schemePref === 'auto' ? detectedMode :
74
- schemePref === 'light' ? 'light' :
75
- schemePref === 'dark' ? 'dark' :
76
- null;
75
+ schemePref === "auto"
76
+ ? detectedMode
77
+ : schemePref === "light"
78
+ ? "light"
79
+ : schemePref === "dark"
80
+ ? "dark"
81
+ : null;
77
82
 
78
- const stored = themes.find(t => t.id === themeId);
83
+ const stored = themes.find((t) => t.id === themeId);
79
84
 
80
85
  if (!stored) {
81
86
  if (targetScheme) {
82
- return themes.find(t => t.colorScheme === targetScheme) ?? themes[0]!;
87
+ return themes.find((t) => t.colorScheme === targetScheme) ?? themes[0]!;
83
88
  }
84
89
  return themes[0]!;
85
90
  }
@@ -89,7 +94,7 @@ export function resolveTheme(
89
94
  }
90
95
 
91
96
  const familySibling = themes.find(
92
- t => t.family === stored.family && t.colorScheme === targetScheme && t.id !== stored.id,
97
+ (t) => t.family === stored.family && t.colorScheme === targetScheme && t.id !== stored.id,
93
98
  );
94
99
  return familySibling ?? stored;
95
100
  }