@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.
- package/README.md +2 -5
- package/package.json +8 -5
- package/src/agents/aggregator.ts +39 -19
- package/src/agents/costing.ts +11 -9
- package/src/agents/index.ts +7 -9
- package/src/agents/types.ts +2 -2
- package/src/cli.ts +28 -23
- package/src/config/schema.test.ts +42 -40
- package/src/config/schema.ts +26 -26
- package/src/demo/simulator.ts +161 -122
- package/src/plugins/agents/index.ts +2 -2
- package/src/plugins/auth-sources.ts +42 -38
- package/src/plugins/lifecycle.ts +32 -32
- package/src/plugins/loader.test.ts +49 -49
- package/src/plugins/loader.ts +67 -63
- package/src/plugins/notification-bus.ts +21 -16
- package/src/plugins/notifications/index.ts +2 -2
- package/src/plugins/notifications/terminal-bell.ts +29 -23
- package/src/plugins/notifications/visual-flash.ts +21 -19
- package/src/plugins/npm-installer.ts +23 -25
- package/src/plugins/plugin-context-factory.ts +10 -10
- package/src/plugins/plugin-host.ts +12 -5
- package/src/plugins/providers/anthropic.ts +64 -57
- package/src/plugins/providers/antigravity.ts +81 -69
- package/src/plugins/providers/codex.ts +42 -42
- package/src/plugins/providers/gemini.ts +106 -87
- package/src/plugins/providers/github-copilot.ts +49 -45
- package/src/plugins/providers/index.ts +10 -12
- package/src/plugins/providers/minimax.ts +50 -44
- package/src/plugins/providers/openai-api.ts +82 -62
- package/src/plugins/providers/opencode-zen.ts +46 -39
- package/src/plugins/providers/perplexity.ts +55 -46
- package/src/plugins/providers/zai.ts +66 -57
- package/src/plugins/registry.ts +47 -43
- package/src/plugins/sandbox-guard.test.ts +45 -48
- package/src/plugins/sandbox-guard.ts +20 -25
- package/src/plugins/sandbox.ts +26 -48
- package/src/plugins/themes/catppuccin-latte.ts +29 -29
- package/src/plugins/themes/catppuccin-mocha.ts +29 -29
- package/src/plugins/themes/claude-code.ts +29 -29
- package/src/plugins/themes/dracula.ts +29 -29
- package/src/plugins/themes/github-light.ts +29 -29
- package/src/plugins/themes/gruvbox-dark.ts +29 -29
- package/src/plugins/themes/gruvbox-light.ts +29 -29
- package/src/plugins/themes/index.ts +15 -15
- package/src/plugins/themes/kanagawa.ts +29 -29
- package/src/plugins/themes/nord.ts +29 -29
- package/src/plugins/themes/one-dark.ts +29 -29
- package/src/plugins/themes/opencode.ts +31 -31
- package/src/plugins/themes/rose-pine-dawn.ts +29 -29
- package/src/plugins/themes/rose-pine.ts +29 -29
- package/src/plugins/themes/solarized-light.ts +29 -29
- package/src/plugins/themes/tokyo-night.ts +29 -29
- package/src/plugins/types/agent.ts +5 -5
- package/src/plugins/types/base.ts +35 -24
- package/src/plugins/types/index.ts +9 -9
- package/src/plugins/types/notification.ts +12 -12
- package/src/plugins/types/provider.ts +12 -8
- package/src/plugins/types/theme.ts +31 -23
- package/src/plugins/update-checker.ts +6 -6
- package/src/pricing/estimator.test.ts +39 -44
- package/src/pricing/estimator.ts +7 -13
- package/src/pricing/fallback.test.ts +30 -34
- package/src/pricing/fallback.ts +54 -27
- package/src/pricing/index.ts +18 -12
- package/src/pricing/models-dev.ts +6 -6
- package/src/storage/database.ts +56 -46
- package/src/storage/db.ts +12 -12
- package/src/storage/index.ts +44 -45
- package/src/storage/migrations/index.ts +2 -2
- package/src/storage/paths.ts +13 -13
- package/src/storage/repos/agentSessions.ts +33 -23
- package/src/storage/repos/providerSnapshots.ts +5 -5
- package/src/storage/repos/usageEvents.ts +40 -28
- package/src/storage/types.ts +5 -5
- package/src/tui/App.tsx +180 -137
- package/src/tui/components/CommandPalette.tsx +46 -34
- package/src/tui/components/DebugConsole.tsx +39 -33
- package/src/tui/components/DebugPanel.tsx +295 -177
- package/src/tui/components/FooterHints.tsx +2 -2
- package/src/tui/components/GhostProviderCard.tsx +37 -35
- package/src/tui/components/Header.tsx +68 -35
- package/src/tui/components/HelpOverlay.tsx +99 -29
- package/src/tui/components/InlineGauge.tsx +10 -7
- package/src/tui/components/InlineSparkline.tsx +17 -11
- package/src/tui/components/KpiStrip.tsx +128 -98
- package/src/tui/components/LimitGauge.tsx +96 -59
- package/src/tui/components/ModalBackdrop.tsx +4 -4
- package/src/tui/components/ProviderAggregateStrip.tsx +69 -38
- package/src/tui/components/ProviderCard.tsx +232 -226
- package/src/tui/components/ProviderDetailPanel.tsx +133 -58
- package/src/tui/components/ProviderLimitsPanel.tsx +126 -89
- package/src/tui/components/ProvidersList.tsx +215 -126
- package/src/tui/components/SessionDetailsDrawer.tsx +191 -94
- package/src/tui/components/SessionsTable.tsx +313 -166
- package/src/tui/components/SettingsModal.tsx +231 -214
- package/src/tui/components/SidebarBreakdown.tsx +30 -16
- package/src/tui/components/Skeleton.tsx +24 -17
- package/src/tui/components/SmartSidebar.tsx +360 -258
- package/src/tui/components/Sparkline.tsx +64 -52
- package/src/tui/components/Spinner.tsx +3 -3
- package/src/tui/components/StatusBar.tsx +27 -27
- package/src/tui/components/ThemePicker.tsx +67 -66
- package/src/tui/components/Toast.tsx +9 -6
- package/src/tui/components/UsageGauge.tsx +10 -13
- package/src/tui/components/index.ts +5 -5
- package/src/tui/contexts/AgentSessionContext.tsx +193 -144
- package/src/tui/contexts/ConfigContext.tsx +15 -13
- package/src/tui/contexts/DashboardRuntimeContext.tsx +26 -26
- package/src/tui/contexts/DemoModeContext.tsx +22 -15
- package/src/tui/contexts/DrawerContext.tsx +11 -9
- package/src/tui/contexts/InputContext.tsx +2 -2
- package/src/tui/contexts/LogContext.tsx +62 -47
- package/src/tui/contexts/PluginContext.tsx +232 -183
- package/src/tui/contexts/RealTimeActivityContext.tsx +70 -36
- package/src/tui/contexts/StorageContext.tsx +151 -117
- package/src/tui/contexts/ThemeContext.tsx +21 -16
- package/src/tui/contexts/TimeWindowContext.tsx +72 -61
- package/src/tui/contexts/ToastContext.tsx +10 -7
- package/src/tui/contexts/index.ts +9 -5
- package/src/tui/createApp.tsx +8 -12
- package/src/tui/debug/captureFrame.ts +25 -28
- package/src/tui/debug/snapshot.tsx +179 -162
- package/src/tui/driver/assertions.ts +30 -22
- package/src/tui/driver/cli.ts +231 -221
- package/src/tui/driver/coverage.ts +27 -44
- package/src/tui/driver/demo-snapshot.ts +33 -33
- package/src/tui/driver/diff.ts +26 -32
- package/src/tui/driver/driver.ts +43 -48
- package/src/tui/driver/index.ts +36 -40
- package/src/tui/driver/recorder.ts +37 -48
- package/src/tui/driver/test-workflow.ts +51 -51
- package/src/tui/driver/test.ts +25 -25
- package/src/tui/hooks/index.ts +24 -20
- package/src/tui/hooks/useAnimatedValue.ts +13 -18
- package/src/tui/hooks/useDashboardKeyboard.ts +118 -98
- package/src/tui/hooks/useDashboardState.ts +26 -24
- package/src/tui/hooks/useEmaActivity.ts +82 -74
- package/src/tui/hooks/useEntranceAnimation.ts +11 -11
- package/src/tui/hooks/useExitAnimation.ts +3 -3
- package/src/tui/hooks/usePulse.ts +4 -4
- package/src/tui/hooks/useSafeRenderer.ts +5 -5
- package/src/tui/hooks/useSessionEnrichedProviders.ts +150 -0
- package/src/tui/hooks/useValueFlash.ts +10 -14
- package/src/tui/index.tsx +29 -6
- package/src/tui/shutdown.ts +19 -0
- package/src/tui/utils/providerColor.ts +1 -1
- package/src/tui/views/Dashboard.tsx +127 -102
- package/src/tui/views/HistoricalTrendsView.tsx +579 -250
- package/src/tui/views/ProjectsView.tsx +466 -237
- package/src/tui/views/RealTimeDashboard.tsx +150 -111
- package/src/tui/views/SettingsView.tsx +222 -191
- package/src/tui/views/index.ts +2 -2
- package/src/types/opentui-test-utils.d.ts +7 -7
- package/src/utils/clipboard.ts +29 -29
- package/src/utils/currency.test.ts +53 -53
- package/src/utils/currency.ts +35 -32
- package/src/version.ts +1 -1
|
@@ -1,10 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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,
|
|
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(
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
70
|
+
},
|
|
71
|
+
[debug],
|
|
72
|
+
);
|
|
53
73
|
|
|
54
74
|
useEffect(() => {
|
|
55
75
|
if (demoMode || !pluginsInitialized) return;
|
|
56
76
|
|
|
57
|
-
const agentPlugins = pluginRegistry.getAll(
|
|
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,
|
|
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 =>
|
|
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
|
-
|
|
93
|
-
|
|
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
|
-
|
|
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
|
-
}, [
|
|
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(
|
|
150
|
+
throw new Error("useRealTimeActivity must be used within RealTimeActivityProvider");
|
|
117
151
|
}
|
|
118
152
|
return context;
|
|
119
153
|
}
|
|
@@ -1,18 +1,31 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
import {
|
|
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,
|
|
25
|
-
streams: Omit<AgentSessionStreamSnapshotRow,
|
|
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(
|
|
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(
|
|
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(
|
|
94
|
-
|
|
119
|
+
const recordProviderSnapshots = useCallback(
|
|
120
|
+
(snapshots: ProviderSnapshotInsert[]) => {
|
|
121
|
+
if (!isReady || demoMode || snapshots.length === 0) return;
|
|
95
122
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
129
|
+
if (filtered.length === 0) return;
|
|
103
130
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}, [isReady, demoMode]);
|
|
139
|
+
},
|
|
140
|
+
[isReady, demoMode],
|
|
141
|
+
);
|
|
113
142
|
|
|
114
|
-
const recordUsageEvents = useCallback(
|
|
115
|
-
|
|
143
|
+
const recordUsageEvents = useCallback(
|
|
144
|
+
(events: UsageEventInsert[]) => {
|
|
145
|
+
if (!isReady || demoMode || events.length === 0) return;
|
|
116
146
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
164
|
+
const sessionKey = `${session.agentId}:${session.sessionId}`;
|
|
165
|
+
const now = Date.now();
|
|
166
|
+
const lastTs = lastSessionSnapshotRef.current.get(sessionKey) ?? 0;
|
|
134
167
|
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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
|
-
|
|
184
|
-
|
|
216
|
+
if (usageEvents.length > 0) {
|
|
217
|
+
insertUsageEventBatch(usageEvents);
|
|
218
|
+
}
|
|
185
219
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
220
|
+
capMapSize(lastSessionSnapshotRef.current, MAX_TRACKED_SESSIONS);
|
|
221
|
+
capMapSize(previousTotalsRef.current, MAX_TRACKED_STREAMS);
|
|
189
222
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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(
|
|
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
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
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(
|
|
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 ===
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
}
|