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