@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,4 +1,4 @@
|
|
|
1
|
-
import { useColors } from
|
|
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 =
|
|
29
|
-
|
|
30
|
-
providerColor;
|
|
28
|
+
const fillColor =
|
|
29
|
+
percent >= 90 ? colors.gaugeDanger : percent >= 70 ? colors.gaugeWarning : providerColor;
|
|
31
30
|
|
|
32
|
-
const filledBar =
|
|
33
|
-
const emptyBar =
|
|
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
|
|
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 {
|
|
2
|
-
export {
|
|
3
|
-
export {
|
|
4
|
-
export {
|
|
5
|
-
export {
|
|
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 {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import type {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
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 type { PricingSource } from "@/storage/types.ts";
|
|
18
|
+
import { useDemoMode } from "./DemoModeContext.tsx";
|
|
19
|
+
import { useLogs } from "./LogContext.tsx";
|
|
20
|
+
import { usePlugins } from "./PluginContext.tsx";
|
|
21
|
+
import { useStorage } from "./StorageContext.tsx";
|
|
22
|
+
import { useTimeWindow } from "./TimeWindowContext.tsx";
|
|
15
23
|
|
|
16
24
|
interface AgentSessionContextValue {
|
|
17
25
|
sessions: AgentSessionAggregate[];
|
|
@@ -24,6 +32,7 @@ interface AgentSessionContextValue {
|
|
|
24
32
|
|
|
25
33
|
const AgentSessionContext = createContext<AgentSessionContextValue | null>(null);
|
|
26
34
|
|
|
35
|
+
const MAX_PERSISTENCE_FINGERPRINTS = 500;
|
|
27
36
|
const sessionPersistenceFingerprints = new Map<string, string>();
|
|
28
37
|
|
|
29
38
|
function sessionFingerprint(session: AgentSessionAggregate): string {
|
|
@@ -52,9 +61,10 @@ export function AgentSessionProvider({
|
|
|
52
61
|
const { demoMode, simulator } = useDemoMode();
|
|
53
62
|
const { windowMs } = useTimeWindow();
|
|
54
63
|
const hasBackfilled = useRef(false);
|
|
64
|
+
const isRefreshingRef = useRef(false);
|
|
55
65
|
|
|
56
66
|
const discoverAgents = useCallback(async (): Promise<AgentInfo[]> => {
|
|
57
|
-
const agentPlugins = pluginRegistry.getAll(
|
|
67
|
+
const agentPlugins = pluginRegistry.getAll("agent");
|
|
58
68
|
const discovered: AgentInfo[] = [];
|
|
59
69
|
|
|
60
70
|
for (const plugin of agentPlugins) {
|
|
@@ -78,7 +88,7 @@ export function AgentSessionProvider({
|
|
|
78
88
|
installed: false,
|
|
79
89
|
sessionParsingSupported: false,
|
|
80
90
|
};
|
|
81
|
-
agentInfo.error = err instanceof Error ? err.message :
|
|
91
|
+
agentInfo.error = err instanceof Error ? err.message : "Unknown error";
|
|
82
92
|
discovered.push(agentInfo);
|
|
83
93
|
}
|
|
84
94
|
}
|
|
@@ -86,144 +96,187 @@ export function AgentSessionProvider({
|
|
|
86
96
|
return discovered;
|
|
87
97
|
}, []);
|
|
88
98
|
|
|
89
|
-
const fetchAgentSessions = useCallback(
|
|
90
|
-
plugin: AgentPlugin,
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const agentId = plugin.id;
|
|
94
|
-
const agentName = plugin.name;
|
|
99
|
+
const fetchAgentSessions = useCallback(
|
|
100
|
+
async (plugin: AgentPlugin, options: SessionParseOptions): Promise<AgentSessionAggregate[]> => {
|
|
101
|
+
const agentId = plugin.id;
|
|
102
|
+
const agentName = plugin.name;
|
|
95
103
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
104
|
+
const http = createSandboxedHttpClient(plugin.id, plugin.permissions);
|
|
105
|
+
const logger = createPluginLogger(plugin.id);
|
|
106
|
+
const ctx = { http, logger, config: {}, signal: AbortSignal.timeout(60_000) };
|
|
99
107
|
|
|
100
|
-
|
|
108
|
+
const rawSessions = await plugin.parseSessions(options, ctx);
|
|
101
109
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
110
|
+
if (rawSessions.length === 0) {
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
105
113
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
114
|
+
const aggregated = aggregateSessionUsage({
|
|
115
|
+
agentId,
|
|
116
|
+
agentName,
|
|
117
|
+
rows: rawSessions,
|
|
118
|
+
});
|
|
111
119
|
|
|
112
|
-
|
|
113
|
-
|
|
120
|
+
return aggregated;
|
|
121
|
+
},
|
|
122
|
+
[],
|
|
123
|
+
);
|
|
114
124
|
|
|
115
|
-
const refreshSessions = useCallback(
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
setError(null);
|
|
125
|
+
const refreshSessions = useCallback(
|
|
126
|
+
async (options: SessionParseOptions = {}) => {
|
|
127
|
+
if (isRefreshingRef.current) return;
|
|
128
|
+
isRefreshingRef.current = true;
|
|
121
129
|
|
|
122
|
-
|
|
123
|
-
if (
|
|
124
|
-
|
|
125
|
-
const demoSessions = snapshot.sessions;
|
|
126
|
-
setSessions(demoSessions);
|
|
127
|
-
setLastRefreshAt(Date.now());
|
|
128
|
-
debug('Demo sessions refreshed', { count: demoSessions.length }, 'agent-sessions');
|
|
129
|
-
return;
|
|
130
|
+
const isInitialLoad = sessions.length === 0;
|
|
131
|
+
if (isInitialLoad) {
|
|
132
|
+
setIsLoading(true);
|
|
130
133
|
}
|
|
134
|
+
setError(null);
|
|
131
135
|
|
|
132
|
-
|
|
136
|
+
try {
|
|
137
|
+
if (demoMode && simulator) {
|
|
138
|
+
const snapshot = simulator.tick();
|
|
139
|
+
const demoSessions = snapshot.sessions;
|
|
140
|
+
setSessions(demoSessions);
|
|
141
|
+
setLastRefreshAt(Date.now());
|
|
142
|
+
debug("Demo sessions refreshed", { count: demoSessions.length }, "agent-sessions");
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
133
145
|
|
|
134
|
-
|
|
135
|
-
setAgents(discoveredAgents);
|
|
146
|
+
debug("Starting session refresh...", undefined, "agent-sessions");
|
|
136
147
|
|
|
137
|
-
|
|
138
|
-
|
|
148
|
+
const discoveredAgents = await discoverAgents();
|
|
149
|
+
setAgents(discoveredAgents);
|
|
139
150
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
debug(`Skipping ${plugin.id}: session parsing not supported`, undefined, 'agent-sessions');
|
|
143
|
-
continue;
|
|
144
|
-
}
|
|
151
|
+
const agentPlugins = pluginRegistry.getAll("agent");
|
|
152
|
+
const allAggregates: AgentSessionAggregate[] = [];
|
|
145
153
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
154
|
+
for (const plugin of agentPlugins) {
|
|
155
|
+
if (!plugin.capabilities.sessionParsing) {
|
|
156
|
+
debug(
|
|
157
|
+
`Skipping ${plugin.id}: session parsing not supported`,
|
|
158
|
+
undefined,
|
|
159
|
+
"agent-sessions",
|
|
160
|
+
);
|
|
151
161
|
continue;
|
|
152
162
|
}
|
|
153
163
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
164
|
+
try {
|
|
165
|
+
const pluginCtx = createPluginContext(plugin.id, plugin.permissions);
|
|
166
|
+
const installed = await plugin.isInstalled(pluginCtx);
|
|
167
|
+
if (!installed) {
|
|
168
|
+
debug(`Skipping ${plugin.id}: not installed`, undefined, "agent-sessions");
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const aggregates = await fetchAgentSessions(plugin, options);
|
|
173
|
+
allAggregates.push(...aggregates);
|
|
174
|
+
debug(
|
|
175
|
+
`Fetched ${aggregates.length} sessions from ${plugin.id}`,
|
|
176
|
+
undefined,
|
|
177
|
+
"agent-sessions",
|
|
178
|
+
);
|
|
179
|
+
} catch (err) {
|
|
180
|
+
const errorMsg = err instanceof Error ? err.message : "Unknown error";
|
|
181
|
+
warn(
|
|
182
|
+
`Failed to fetch sessions from ${plugin.id}: ${errorMsg}`,
|
|
183
|
+
undefined,
|
|
184
|
+
"agent-sessions",
|
|
185
|
+
);
|
|
186
|
+
}
|
|
160
187
|
}
|
|
161
|
-
}
|
|
162
188
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
189
|
+
const pricedSessions = await priceSessions(allAggregates);
|
|
190
|
+
|
|
191
|
+
pricedSessions.sort((a, b) => b.lastActivityAt - a.lastActivityAt);
|
|
192
|
+
|
|
193
|
+
if (storageReady) {
|
|
194
|
+
const now = Date.now();
|
|
195
|
+
let persistedCount = 0;
|
|
196
|
+
for (const session of pricedSessions) {
|
|
197
|
+
const fp = sessionFingerprint(session);
|
|
198
|
+
const prevFp = sessionPersistenceFingerprints.get(session.sessionId);
|
|
199
|
+
if (prevFp === fp) continue;
|
|
200
|
+
|
|
201
|
+
sessionPersistenceFingerprints.set(session.sessionId, fp);
|
|
202
|
+
persistedCount++;
|
|
203
|
+
|
|
204
|
+
recordAgentSession(
|
|
205
|
+
{
|
|
206
|
+
agentId: session.agentId,
|
|
207
|
+
sessionId: session.sessionId,
|
|
208
|
+
projectPath: session.projectPath ?? null,
|
|
209
|
+
startedAt: session.startedAt ?? null,
|
|
210
|
+
lastSeenAt: now,
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
timestamp: now,
|
|
214
|
+
lastActivityAt: session.lastActivityAt,
|
|
215
|
+
status: session.status,
|
|
216
|
+
totalInputTokens: session.totals.input,
|
|
217
|
+
totalOutputTokens: session.totals.output,
|
|
218
|
+
totalCacheReadTokens: session.totals.cacheRead ?? 0,
|
|
219
|
+
totalCacheWriteTokens: session.totals.cacheWrite ?? 0,
|
|
220
|
+
totalCostUsd: session.totalCostUsd ?? 0,
|
|
221
|
+
requestCount: session.requestCount,
|
|
222
|
+
},
|
|
223
|
+
session.streams.map((s) => ({
|
|
224
|
+
provider: s.providerId,
|
|
225
|
+
model: s.modelId,
|
|
226
|
+
inputTokens: s.tokens.input,
|
|
227
|
+
outputTokens: s.tokens.output,
|
|
228
|
+
cacheReadTokens: s.tokens.cacheRead ?? 0,
|
|
229
|
+
cacheWriteTokens: s.tokens.cacheWrite ?? 0,
|
|
230
|
+
costUsd: s.costUsd ?? 0,
|
|
231
|
+
requestCount: s.requestCount,
|
|
232
|
+
pricingSource: (s.pricingSource as PricingSource) ?? null,
|
|
233
|
+
})),
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
if (sessionPersistenceFingerprints.size > MAX_PERSISTENCE_FINGERPRINTS) {
|
|
237
|
+
const excess = sessionPersistenceFingerprints.size - MAX_PERSISTENCE_FINGERPRINTS;
|
|
238
|
+
const keys = sessionPersistenceFingerprints.keys();
|
|
239
|
+
for (let i = 0; i < excess; i++) {
|
|
240
|
+
const next = keys.next();
|
|
241
|
+
if (next.done) break;
|
|
242
|
+
sessionPersistenceFingerprints.delete(next.value);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
debug(
|
|
247
|
+
`Persisted ${persistedCount}/${pricedSessions.length} changed sessions to storage`,
|
|
248
|
+
undefined,
|
|
249
|
+
"agent-sessions",
|
|
208
250
|
);
|
|
209
251
|
}
|
|
210
|
-
debug(`Persisted ${persistedCount}/${pricedSessions.length} changed sessions to storage`, undefined, 'agent-sessions');
|
|
211
|
-
}
|
|
212
252
|
|
|
213
|
-
|
|
214
|
-
|
|
253
|
+
setSessions(pricedSessions);
|
|
254
|
+
setLastRefreshAt(Date.now());
|
|
215
255
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
256
|
+
debug(`Session refresh complete`, { count: pricedSessions.length }, "agent-sessions");
|
|
257
|
+
} catch (err) {
|
|
258
|
+
const errorMsg = err instanceof Error ? err.message : "Unknown error";
|
|
259
|
+
logError(`Session refresh failed: ${errorMsg}`, undefined, "agent-sessions");
|
|
260
|
+
setError(errorMsg);
|
|
261
|
+
} finally {
|
|
262
|
+
isRefreshingRef.current = false;
|
|
263
|
+
if (isInitialLoad) {
|
|
264
|
+
setIsLoading(false);
|
|
265
|
+
}
|
|
224
266
|
}
|
|
225
|
-
}
|
|
226
|
-
|
|
267
|
+
},
|
|
268
|
+
[
|
|
269
|
+
sessions.length,
|
|
270
|
+
discoverAgents,
|
|
271
|
+
fetchAgentSessions,
|
|
272
|
+
debug,
|
|
273
|
+
warn,
|
|
274
|
+
logError,
|
|
275
|
+
storageReady,
|
|
276
|
+
recordAgentSession,
|
|
277
|
+
demoMode,
|
|
278
|
+
],
|
|
279
|
+
);
|
|
227
280
|
|
|
228
281
|
const refreshSessionsRef = useRef(refreshSessions);
|
|
229
282
|
refreshSessionsRef.current = refreshSessions;
|
|
@@ -243,15 +296,15 @@ export function AgentSessionProvider({
|
|
|
243
296
|
useEffect(() => {
|
|
244
297
|
if (!autoRefresh) return;
|
|
245
298
|
|
|
246
|
-
debug(`Setting up session refresh interval: ${refreshInterval}ms`, undefined,
|
|
247
|
-
|
|
299
|
+
debug(`Setting up session refresh interval: ${refreshInterval}ms`, undefined, "agent-sessions");
|
|
300
|
+
|
|
248
301
|
const intervalId = setInterval(() => {
|
|
249
|
-
debug(
|
|
302
|
+
debug("Interval tick - refreshing sessions", undefined, "agent-sessions");
|
|
250
303
|
refreshSessionsRef.current();
|
|
251
304
|
}, refreshInterval);
|
|
252
305
|
|
|
253
306
|
return () => {
|
|
254
|
-
debug(
|
|
307
|
+
debug("Clearing session refresh interval", undefined, "agent-sessions");
|
|
255
308
|
clearInterval(intervalId);
|
|
256
309
|
};
|
|
257
310
|
}, [autoRefresh, refreshInterval, debug]);
|
|
@@ -265,29 +318,25 @@ export function AgentSessionProvider({
|
|
|
265
318
|
refreshSessions,
|
|
266
319
|
};
|
|
267
320
|
|
|
268
|
-
return
|
|
269
|
-
<AgentSessionContext.Provider value={value}>
|
|
270
|
-
{children}
|
|
271
|
-
</AgentSessionContext.Provider>
|
|
272
|
-
);
|
|
321
|
+
return <AgentSessionContext.Provider value={value}>{children}</AgentSessionContext.Provider>;
|
|
273
322
|
}
|
|
274
323
|
|
|
275
324
|
export function useAgentSessions(): AgentSessionContextValue {
|
|
276
325
|
const context = useContext(AgentSessionContext);
|
|
277
326
|
if (!context) {
|
|
278
|
-
throw new Error(
|
|
327
|
+
throw new Error("useAgentSessions must be used within AgentSessionProvider");
|
|
279
328
|
}
|
|
280
329
|
return context;
|
|
281
330
|
}
|
|
282
331
|
|
|
283
332
|
export function useActiveSessions(): AgentSessionAggregate[] {
|
|
284
333
|
const { sessions } = useAgentSessions();
|
|
285
|
-
return sessions.filter(s => s.status ===
|
|
334
|
+
return sessions.filter((s) => s.status === "active");
|
|
286
335
|
}
|
|
287
336
|
|
|
288
337
|
export function useSessionsByAgent(agentId: AgentId): AgentSessionAggregate[] {
|
|
289
338
|
const { sessions } = useAgentSessions();
|
|
290
|
-
return sessions.filter(s => s.agentId === agentId);
|
|
339
|
+
return sessions.filter((s) => s.agentId === agentId);
|
|
291
340
|
}
|
|
292
341
|
|
|
293
342
|
export function useTotalCost(): number {
|
|
@@ -302,6 +351,6 @@ export function useTotalTokens(): { input: number; output: number } {
|
|
|
302
351
|
input: acc.input + s.totals.input,
|
|
303
352
|
output: acc.output + s.totals.output,
|
|
304
353
|
}),
|
|
305
|
-
{ input: 0, output: 0 }
|
|
354
|
+
{ input: 0, output: 0 },
|
|
306
355
|
);
|
|
307
356
|
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { createContext,
|
|
2
|
-
import {
|
|
3
|
-
import { useDemoMode } from
|
|
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[
|
|
10
|
-
updateDisplay: (updates: Partial<AppConfig[
|
|
11
|
-
updateNotifications: (updates: Partial<AppConfig[
|
|
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:
|
|
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:
|
|
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>(
|
|
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[
|
|
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[
|
|
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[
|
|
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(
|
|
140
|
+
throw new Error("useConfig must be used within a ConfigProvider");
|
|
139
141
|
}
|
|
140
142
|
return context;
|
|
141
143
|
}
|