@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.
- 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 +42 -38
- 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 +36 -23
- package/src/plugins/notifications/visual-flash.ts +31 -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 +68 -57
- package/src/plugins/registry.ts +80 -71
- 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/persistence-service.ts +206 -0
- 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/retention.ts +146 -0
- 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 +314 -183
- 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 +320 -169
- package/src/tui/components/SettingsModal.tsx +593 -269
- 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 +164 -151
- 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 +238 -183
- package/src/tui/contexts/RealTimeActivityContext.tsx +70 -36
- package/src/tui/contexts/StorageContext.tsx +95 -149
- 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 +7 -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 +164 -102
- package/src/tui/views/HistoricalTrendsView.tsx +579 -250
- package/src/tui/views/ProjectsView.tsx +495 -243
- package/src/tui/views/RealTimeDashboard.tsx +176 -116
- package/src/tui/views/SettingsView.tsx +225 -189
- 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,34 +1,32 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
} from
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
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 { seedPreviousTotals } from "@/storage/persistence-service.ts";
|
|
13
|
+
import { insertProviderSnapshotBatch } from "@/storage/repos/providerSnapshots.ts";
|
|
14
|
+
import { insertUsageEventBatch } from "@/storage/repos/usageEvents.ts";
|
|
15
|
+
import { incrementalVacuum, pruneOldData } from "@/storage/retention.ts";
|
|
16
|
+
import type { ProviderSnapshotInsert, UsageEventInsert } from "@/storage/types.ts";
|
|
17
|
+
import { useDemoMode } from "./DemoModeContext.tsx";
|
|
16
18
|
|
|
17
19
|
interface StorageContextValue {
|
|
18
20
|
isReady: boolean;
|
|
19
21
|
appRunId: number | null;
|
|
20
22
|
recordProviderSnapshots: (snapshots: ProviderSnapshotInsert[]) => void;
|
|
21
23
|
recordUsageEvents: (events: UsageEventInsert[]) => void;
|
|
22
|
-
recordAgentSession: (
|
|
23
|
-
session: AgentSessionUpsert,
|
|
24
|
-
snapshot: Omit<AgentSessionSnapshotInsert, 'agentSessionId'>,
|
|
25
|
-
streams: Omit<AgentSessionStreamSnapshotRow, 'agentSessionSnapshotId'>[]
|
|
26
|
-
) => number | null;
|
|
27
24
|
}
|
|
28
25
|
|
|
29
26
|
const StorageContext = createContext<StorageContextValue | null>(null);
|
|
30
27
|
|
|
31
|
-
const
|
|
28
|
+
const PROVIDER_SNAPSHOT_INTERVAL_MS = 300_000;
|
|
29
|
+
const PRUNE_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
|
|
32
30
|
|
|
33
31
|
interface StorageProviderProps {
|
|
34
32
|
children: ReactNode;
|
|
@@ -38,8 +36,6 @@ export function StorageProvider({ children }: StorageProviderProps) {
|
|
|
38
36
|
const [isReady, setIsReady] = useState(false);
|
|
39
37
|
const [appRunId, setAppRunId] = useState<number | null>(null);
|
|
40
38
|
const lastProviderSnapshotRef = useRef<Map<string, number>>(new Map());
|
|
41
|
-
const lastSessionSnapshotRef = useRef<Map<string, number>>(new Map());
|
|
42
|
-
const previousTotalsRef = useRef<Map<string, StreamTotals>>(new Map());
|
|
43
39
|
const { demoMode, simulator } = useDemoMode();
|
|
44
40
|
|
|
45
41
|
useEffect(() => {
|
|
@@ -55,28 +51,29 @@ export function StorageProvider({ children }: StorageProviderProps) {
|
|
|
55
51
|
async function init() {
|
|
56
52
|
try {
|
|
57
53
|
await initDatabase();
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
costUsd: row.costUsd,
|
|
69
|
-
requestCount: row.requestCount,
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
} catch (err) {
|
|
73
|
-
console.error('Failed to seed previous totals from DB:', err);
|
|
54
|
+
|
|
55
|
+
// Prune expired snapshots before seeding — keeps startup fast
|
|
56
|
+
try {
|
|
57
|
+
const pruneResult = pruneOldData();
|
|
58
|
+
if (pruneResult.agentSessionSnapshots > 0 || pruneResult.providerSnapshots > 0) {
|
|
59
|
+
console.log(
|
|
60
|
+
`[retention] Pruned ${pruneResult.agentSessionSnapshots} session snapshots, ` +
|
|
61
|
+
`${pruneResult.providerSnapshots} provider snapshots in ${pruneResult.durationMs}ms`,
|
|
62
|
+
);
|
|
63
|
+
incrementalVacuum();
|
|
74
64
|
}
|
|
65
|
+
} catch (err) {
|
|
66
|
+
console.error("Failed to prune old data:", err);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (mounted) {
|
|
70
|
+
// Seed the persistence service with previous stream totals from DB
|
|
71
|
+
seedPreviousTotals();
|
|
75
72
|
setIsReady(true);
|
|
76
73
|
setAppRunId(getAppRunId());
|
|
77
74
|
}
|
|
78
75
|
} catch (err) {
|
|
79
|
-
console.error(
|
|
76
|
+
console.error("Failed to initialize database:", err);
|
|
80
77
|
}
|
|
81
78
|
}
|
|
82
79
|
|
|
@@ -90,109 +87,42 @@ export function StorageProvider({ children }: StorageProviderProps) {
|
|
|
90
87
|
};
|
|
91
88
|
}, [demoMode]);
|
|
92
89
|
|
|
93
|
-
const recordProviderSnapshots = useCallback(
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
});
|
|
101
|
-
|
|
102
|
-
if (filtered.length === 0) return;
|
|
103
|
-
|
|
104
|
-
try {
|
|
105
|
-
insertProviderSnapshotBatch(filtered);
|
|
106
|
-
for (const s of filtered) {
|
|
107
|
-
lastProviderSnapshotRef.current.set(s.provider, now);
|
|
108
|
-
}
|
|
109
|
-
} catch (err) {
|
|
110
|
-
console.error('Failed to record provider snapshots:', err);
|
|
111
|
-
}
|
|
112
|
-
}, [isReady, demoMode]);
|
|
113
|
-
|
|
114
|
-
const recordUsageEvents = useCallback((events: UsageEventInsert[]) => {
|
|
115
|
-
if (!isReady || demoMode || events.length === 0) return;
|
|
116
|
-
|
|
117
|
-
try {
|
|
118
|
-
insertUsageEventBatch(events);
|
|
119
|
-
} catch (err) {
|
|
120
|
-
console.error('Failed to record usage events:', err);
|
|
121
|
-
}
|
|
122
|
-
}, [isReady, demoMode]);
|
|
123
|
-
|
|
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;
|
|
90
|
+
const recordProviderSnapshots = useCallback(
|
|
91
|
+
(snapshots: ProviderSnapshotInsert[]) => {
|
|
92
|
+
if (!isReady || demoMode || snapshots.length === 0) return;
|
|
130
93
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
94
|
+
const now = Date.now();
|
|
95
|
+
const filtered = snapshots.filter((s) => {
|
|
96
|
+
const lastTs = lastProviderSnapshotRef.current.get(s.provider) ?? 0;
|
|
97
|
+
return now - lastTs >= PROVIDER_SNAPSHOT_INTERVAL_MS;
|
|
98
|
+
});
|
|
134
99
|
|
|
135
|
-
|
|
136
|
-
return null;
|
|
137
|
-
}
|
|
100
|
+
if (filtered.length === 0) return;
|
|
138
101
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
});
|
|
102
|
+
try {
|
|
103
|
+
insertProviderSnapshotBatch(filtered);
|
|
104
|
+
for (const s of filtered) {
|
|
105
|
+
lastProviderSnapshotRef.current.set(s.provider, now);
|
|
181
106
|
}
|
|
182
|
-
|
|
183
|
-
|
|
107
|
+
} catch (err) {
|
|
108
|
+
console.error("Failed to record provider snapshots:", err);
|
|
184
109
|
}
|
|
110
|
+
},
|
|
111
|
+
[isReady, demoMode],
|
|
112
|
+
);
|
|
185
113
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
114
|
+
const recordUsageEvents = useCallback(
|
|
115
|
+
(events: UsageEventInsert[]) => {
|
|
116
|
+
if (!isReady || demoMode || events.length === 0) return;
|
|
189
117
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
118
|
+
try {
|
|
119
|
+
insertUsageEventBatch(events);
|
|
120
|
+
} catch (err) {
|
|
121
|
+
console.error("Failed to record usage events:", err);
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
[isReady, demoMode],
|
|
125
|
+
);
|
|
196
126
|
|
|
197
127
|
useEffect(() => {
|
|
198
128
|
if (!demoMode || !isReady || !simulator) return;
|
|
@@ -204,25 +134,41 @@ export function StorageProvider({ children }: StorageProviderProps) {
|
|
|
204
134
|
return () => clearInterval(interval);
|
|
205
135
|
}, [demoMode, isReady, simulator]);
|
|
206
136
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
{
|
|
218
|
-
|
|
137
|
+
// Periodic data retention pruning (every hour)
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
if (demoMode || !isReady) return;
|
|
140
|
+
|
|
141
|
+
const intervalId = setInterval(() => {
|
|
142
|
+
try {
|
|
143
|
+
const result = pruneOldData();
|
|
144
|
+
if (result.agentSessionSnapshots > 0 || result.providerSnapshots > 0) {
|
|
145
|
+
incrementalVacuum();
|
|
146
|
+
}
|
|
147
|
+
} catch {
|
|
148
|
+
// Silently ignore periodic prune failures
|
|
149
|
+
}
|
|
150
|
+
}, PRUNE_INTERVAL_MS);
|
|
151
|
+
|
|
152
|
+
return () => clearInterval(intervalId);
|
|
153
|
+
}, [demoMode, isReady]);
|
|
154
|
+
|
|
155
|
+
const value: StorageContextValue = useMemo(
|
|
156
|
+
() => ({
|
|
157
|
+
isReady,
|
|
158
|
+
appRunId,
|
|
159
|
+
recordProviderSnapshots,
|
|
160
|
+
recordUsageEvents,
|
|
161
|
+
}),
|
|
162
|
+
[isReady, appRunId, recordProviderSnapshots, recordUsageEvents],
|
|
219
163
|
);
|
|
164
|
+
|
|
165
|
+
return <StorageContext.Provider value={value}>{children}</StorageContext.Provider>;
|
|
220
166
|
}
|
|
221
167
|
|
|
222
168
|
export function useStorage(): StorageContextValue {
|
|
223
169
|
const context = useContext(StorageContext);
|
|
224
170
|
if (!context) {
|
|
225
|
-
throw new Error(
|
|
171
|
+
throw new Error("useStorage must be used within StorageProvider");
|
|
226
172
|
}
|
|
227
173
|
return context;
|
|
228
174
|
}
|
|
@@ -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
|
}
|