@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,22 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { useTerminalDimensions } from
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import { HelpOverlay } from '../components/HelpOverlay.tsx';
|
|
1
|
+
import type { ScrollBoxRenderable } from "@opentui/core";
|
|
2
|
+
import { useTerminalDimensions } from "@opentui/react";
|
|
3
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { notificationBus } from "@/plugins/notification-bus.ts";
|
|
5
|
+
import { HelpOverlay } from "../components/HelpOverlay.tsx";
|
|
6
|
+
import { KpiStrip } from "../components/KpiStrip.tsx";
|
|
7
|
+
import { ProviderLimitsPanel } from "../components/ProviderLimitsPanel.tsx";
|
|
8
|
+
import { SessionsTable } from "../components/SessionsTable.tsx";
|
|
9
|
+
import { type DriverDimension, getSidebarMode, SmartSidebar } from "../components/SmartSidebar.tsx";
|
|
10
|
+
import { useAgentSessions } from "../contexts/AgentSessionContext.tsx";
|
|
11
|
+
import { useConfig } from "../contexts/ConfigContext.tsx";
|
|
12
|
+
import { useDashboardRuntime } from "../contexts/DashboardRuntimeContext.tsx";
|
|
13
|
+
import { useDrawer } from "../contexts/DrawerContext.tsx";
|
|
14
|
+
import { usePlugins } from "../contexts/PluginContext.tsx";
|
|
15
|
+
import { useColors } from "../contexts/ThemeContext.tsx";
|
|
16
|
+
import { useTimeWindow } from "../contexts/TimeWindowContext.tsx";
|
|
17
|
+
import { useDashboardKeyboard } from "../hooks/useDashboardKeyboard.ts";
|
|
18
|
+
import { getProviderColor } from "../utils/providerColor.ts";
|
|
20
19
|
|
|
21
20
|
export function RealTimeDashboard() {
|
|
22
21
|
const colors = useColors();
|
|
@@ -27,51 +26,62 @@ export function RealTimeDashboard() {
|
|
|
27
26
|
const { config } = useConfig();
|
|
28
27
|
const { activity, sparkData, deltas } = useDashboardRuntime();
|
|
29
28
|
|
|
30
|
-
|
|
31
29
|
const sidebarMode = getSidebarMode(terminalWidth);
|
|
32
30
|
const showLargeHeader = terminalHeight >= 35;
|
|
33
31
|
const showProviderLimitsPanel = terminalHeight >= 24;
|
|
34
32
|
|
|
35
33
|
// Visible-row count for scroll tracking — every non-session-data line must be counted.
|
|
36
34
|
const visibleRows = (() => {
|
|
37
|
-
const appChrome = (showLargeHeader ? 7 : 1) + 1;
|
|
38
|
-
const outerPadding = 2;
|
|
39
|
-
const kpi = 5;
|
|
40
|
-
const limits = !showProviderLimitsPanel
|
|
41
|
-
|
|
42
|
-
:
|
|
35
|
+
const appChrome = (showLargeHeader ? 7 : 1) + 1; // App.tsx: Header + StatusBar
|
|
36
|
+
const outerPadding = 2; // padding={1} top + bottom
|
|
37
|
+
const kpi = 5; // KpiStrip fragment: h4 cards + h1 rule
|
|
38
|
+
const limits = !showProviderLimitsPanel
|
|
39
|
+
? 0 // ProviderLimitsPanel
|
|
40
|
+
: terminalHeight < 30
|
|
41
|
+
? sidebarMode === "hidden"
|
|
42
|
+
? 2
|
|
43
|
+
: 1 // compact (+budget bar)
|
|
44
|
+
: 4; // normal/wide bordered box
|
|
43
45
|
const tableChrome = 4 + (terminalHeight >= 30 ? 1 : 0); // borders + header + columns + inspector
|
|
44
|
-
const footer = 1;
|
|
46
|
+
const footer = 1; // keyboard shortcut bar
|
|
45
47
|
// gap={1} between rendered children; KpiStrip fragment injects 2 children
|
|
46
48
|
const children = 2 + (showProviderLimitsPanel ? 1 : 0) + 1 + 1;
|
|
47
49
|
const gaps = children - 1;
|
|
48
|
-
return Math.max(
|
|
50
|
+
return Math.max(
|
|
51
|
+
1,
|
|
52
|
+
terminalHeight - appChrome - outerPadding - kpi - limits - tableChrome - footer - gaps,
|
|
53
|
+
);
|
|
49
54
|
})();
|
|
50
55
|
|
|
51
56
|
const { showDrawer, isOpen: showSessionDrawer } = useDrawer();
|
|
52
|
-
|
|
57
|
+
|
|
53
58
|
const [showHelp, setShowHelp] = useState(false);
|
|
54
59
|
const [selectedRow, setSelectedRow] = useState(0);
|
|
55
|
-
const [focusedPanel, setFocusedPanel] = useState<
|
|
60
|
+
const [focusedPanel, setFocusedPanel] = useState<"sessions" | "sidebar" | "limits">("sessions");
|
|
56
61
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(config.display.sidebarCollapsed);
|
|
57
|
-
const [filterQuery, setFilterQuery] = useState(
|
|
62
|
+
const [filterQuery, setFilterQuery] = useState("");
|
|
58
63
|
const [isFiltering, setIsFiltering] = useState(false);
|
|
59
|
-
const [sortField, setSortField] = useState<
|
|
64
|
+
const [sortField, setSortField] = useState<"cost" | "tokens" | "time">("cost");
|
|
60
65
|
const [pendingG, setPendingG] = useState(false);
|
|
61
|
-
const
|
|
66
|
+
const scrollOffsetRef = useRef(0);
|
|
67
|
+
const selectedSessionIdRef = useRef<string | null>(null);
|
|
68
|
+
const setScrollOffset = useCallback((val: number) => {
|
|
69
|
+
scrollOffsetRef.current = val;
|
|
70
|
+
sessionsScrollboxRef.current?.scrollTo(val);
|
|
71
|
+
}, []);
|
|
62
72
|
const [limitSelectedIndex, setLimitSelectedIndex] = useState(0);
|
|
63
|
-
const [driverDimension, setDriverDimension] = useState<DriverDimension>(
|
|
73
|
+
const [driverDimension, setDriverDimension] = useState<DriverDimension>("model");
|
|
64
74
|
const [selectedDriverIndex, setSelectedDriverIndex] = useState(0);
|
|
65
75
|
const [activeDriverFilter, setActiveDriverFilter] = useState<string | null>(null);
|
|
66
76
|
|
|
67
|
-
const effectiveSidebarCollapsed = sidebarCollapsed || sidebarMode ===
|
|
68
|
-
const showBudgetInLimits = sidebarMode ===
|
|
77
|
+
const effectiveSidebarCollapsed = sidebarCollapsed || sidebarMode === "hidden";
|
|
78
|
+
const showBudgetInLimits = sidebarMode === "hidden";
|
|
69
79
|
|
|
70
80
|
const sessionsScrollboxRef = useRef<ScrollBoxRenderable>(null);
|
|
71
81
|
|
|
72
82
|
const configuredProviders = useMemo(() => {
|
|
73
83
|
return Array.from(providers.values())
|
|
74
|
-
.filter(p => p.configured)
|
|
84
|
+
.filter((p) => p.configured)
|
|
75
85
|
.sort((a, b) => getMaxUsedPercent(b) - getMaxUsedPercent(a));
|
|
76
86
|
}, [providers]);
|
|
77
87
|
|
|
@@ -91,19 +101,20 @@ export function RealTimeDashboard() {
|
|
|
91
101
|
|
|
92
102
|
const baseFilteredSessions = useMemo(() => {
|
|
93
103
|
let result = [...agentSessions];
|
|
94
|
-
|
|
104
|
+
|
|
95
105
|
if (windowMs !== null) {
|
|
96
106
|
const cutoff = Date.now() - windowMs;
|
|
97
|
-
result = result.filter(s => s.lastActivityAt >= cutoff);
|
|
107
|
+
result = result.filter((s) => s.lastActivityAt >= cutoff);
|
|
98
108
|
}
|
|
99
|
-
|
|
109
|
+
|
|
100
110
|
if (filterQuery) {
|
|
101
111
|
const q = filterQuery.toLowerCase();
|
|
102
|
-
result = result.filter(
|
|
103
|
-
s
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
112
|
+
result = result.filter(
|
|
113
|
+
(s) =>
|
|
114
|
+
s.agentName.toLowerCase().includes(q) ||
|
|
115
|
+
s.streams.some((st) => st.modelId.toLowerCase().includes(q)) ||
|
|
116
|
+
(s.projectPath?.toLowerCase().includes(q) ?? false) ||
|
|
117
|
+
(s.sessionName?.toLowerCase().includes(q) ?? false),
|
|
107
118
|
);
|
|
108
119
|
}
|
|
109
120
|
|
|
@@ -112,13 +123,19 @@ export function RealTimeDashboard() {
|
|
|
112
123
|
|
|
113
124
|
const drivers = useMemo(() => {
|
|
114
125
|
const stats: Record<string, { cost: number }> = {};
|
|
115
|
-
baseFilteredSessions.forEach(s => {
|
|
116
|
-
s.streams.forEach(st => {
|
|
126
|
+
baseFilteredSessions.forEach((s) => {
|
|
127
|
+
s.streams.forEach((st) => {
|
|
117
128
|
let key: string;
|
|
118
129
|
switch (driverDimension) {
|
|
119
|
-
case
|
|
120
|
-
|
|
121
|
-
|
|
130
|
+
case "model":
|
|
131
|
+
key = st.modelId;
|
|
132
|
+
break;
|
|
133
|
+
case "project":
|
|
134
|
+
key = s.projectPath?.split("/").pop() ?? "unknown";
|
|
135
|
+
break;
|
|
136
|
+
case "agent":
|
|
137
|
+
key = s.agentName;
|
|
138
|
+
break;
|
|
122
139
|
}
|
|
123
140
|
if (!stats[key]) stats[key] = { cost: 0 };
|
|
124
141
|
stats[key]!.cost += st.costUsd ?? 0;
|
|
@@ -138,7 +155,7 @@ export function RealTimeDashboard() {
|
|
|
138
155
|
}, [drivers.length, selectedDriverIndex]);
|
|
139
156
|
|
|
140
157
|
useEffect(() => {
|
|
141
|
-
if (activeDriverFilter ===
|
|
158
|
+
if (activeDriverFilter === "__TOGGLE_SELECTED__") {
|
|
142
159
|
const driver = drivers[selectedDriverIndex];
|
|
143
160
|
setActiveDriverFilter(driver?.id ?? null);
|
|
144
161
|
}
|
|
@@ -146,64 +163,84 @@ export function RealTimeDashboard() {
|
|
|
146
163
|
|
|
147
164
|
const processedSessions = useMemo(() => {
|
|
148
165
|
let result = [...baseFilteredSessions];
|
|
149
|
-
|
|
150
|
-
if (activeDriverFilter && activeDriverFilter !==
|
|
151
|
-
result = result.filter(s => {
|
|
166
|
+
|
|
167
|
+
if (activeDriverFilter && activeDriverFilter !== "__TOGGLE_SELECTED__") {
|
|
168
|
+
result = result.filter((s) => {
|
|
152
169
|
switch (driverDimension) {
|
|
153
|
-
case
|
|
154
|
-
return s.streams.some(st => st.modelId === activeDriverFilter);
|
|
155
|
-
case
|
|
156
|
-
return (s.projectPath?.split(
|
|
157
|
-
case
|
|
170
|
+
case "model":
|
|
171
|
+
return s.streams.some((st) => st.modelId === activeDriverFilter);
|
|
172
|
+
case "project":
|
|
173
|
+
return (s.projectPath?.split("/").pop() ?? "unknown") === activeDriverFilter;
|
|
174
|
+
case "agent":
|
|
158
175
|
return s.agentName === activeDriverFilter;
|
|
176
|
+
default:
|
|
177
|
+
return true;
|
|
159
178
|
}
|
|
160
179
|
});
|
|
161
180
|
}
|
|
162
181
|
|
|
163
182
|
result.sort((a, b) => {
|
|
164
|
-
const aActive = a.status ===
|
|
165
|
-
const bActive = b.status ===
|
|
183
|
+
const aActive = a.status === "active" ? 1 : 0;
|
|
184
|
+
const bActive = b.status === "active" ? 1 : 0;
|
|
166
185
|
if (bActive !== aActive) return bActive - aActive;
|
|
167
|
-
|
|
168
|
-
if (sortField ===
|
|
169
|
-
if (sortField ===
|
|
186
|
+
|
|
187
|
+
if (sortField === "cost") return (b.totalCostUsd ?? 0) - (a.totalCostUsd ?? 0);
|
|
188
|
+
if (sortField === "tokens")
|
|
189
|
+
return b.totals.input + b.totals.output - (a.totals.input + a.totals.output);
|
|
170
190
|
return b.lastActivityAt - a.lastActivityAt;
|
|
171
191
|
});
|
|
172
192
|
|
|
173
193
|
return result;
|
|
174
194
|
}, [baseFilteredSessions, activeDriverFilter, driverDimension, sortField]);
|
|
175
195
|
|
|
176
|
-
|
|
196
|
+
// Stabilize selection across re-sorts: track selected session by ID
|
|
197
|
+
// so the highlight follows the same session when live data causes reordering.
|
|
198
|
+
useLayoutEffect(() => {
|
|
177
199
|
if (processedSessions.length === 0) {
|
|
178
|
-
|
|
179
|
-
|
|
200
|
+
setSelectedRow(0);
|
|
201
|
+
setScrollOffset(0);
|
|
202
|
+
selectedSessionIdRef.current = null;
|
|
180
203
|
return;
|
|
181
204
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
205
|
+
|
|
206
|
+
if (selectedSessionIdRef.current) {
|
|
207
|
+
const newIndex = processedSessions.findIndex(
|
|
208
|
+
(s) => s.sessionId === selectedSessionIdRef.current,
|
|
209
|
+
);
|
|
210
|
+
if (newIndex !== -1) {
|
|
211
|
+
setSelectedRow(newIndex);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
// Tracked session was filtered out — fall through to clamp
|
|
215
|
+
selectedSessionIdRef.current = null;
|
|
187
216
|
}
|
|
188
|
-
}, [processedSessions.length, selectedRow, scrollOffset]);
|
|
189
217
|
|
|
218
|
+
// Clamp to valid range (functional update avoids selectedRow in deps)
|
|
219
|
+
setSelectedRow((prev) => Math.min(prev, processedSessions.length - 1));
|
|
220
|
+
}, [processedSessions, setScrollOffset]);
|
|
221
|
+
|
|
222
|
+
// Record which session the user selected (runs after navigation and stabilization)
|
|
190
223
|
useEffect(() => {
|
|
224
|
+
const session = processedSessions[selectedRow];
|
|
225
|
+
if (session) {
|
|
226
|
+
selectedSessionIdRef.current = session.sessionId;
|
|
227
|
+
}
|
|
228
|
+
}, [selectedRow, processedSessions]);
|
|
229
|
+
|
|
230
|
+
useLayoutEffect(() => {
|
|
191
231
|
if (!sessionsScrollboxRef.current || processedSessions.length === 0) return;
|
|
192
232
|
|
|
193
|
-
let newOffset =
|
|
233
|
+
let newOffset = scrollOffsetRef.current;
|
|
194
234
|
|
|
195
|
-
if (selectedRow <
|
|
235
|
+
if (selectedRow < newOffset) {
|
|
196
236
|
newOffset = selectedRow;
|
|
197
|
-
} else if (selectedRow >=
|
|
237
|
+
} else if (selectedRow >= newOffset + visibleRows) {
|
|
198
238
|
newOffset = selectedRow - visibleRows + 1;
|
|
199
239
|
}
|
|
200
240
|
|
|
201
|
-
|
|
202
|
-
setScrollOffset(newOffset);
|
|
203
|
-
}
|
|
204
|
-
|
|
241
|
+
scrollOffsetRef.current = newOffset;
|
|
205
242
|
sessionsScrollboxRef.current.scrollTo(newOffset);
|
|
206
|
-
}, [selectedRow, processedSessions.length,
|
|
243
|
+
}, [selectedRow, processedSessions.length, visibleRows]);
|
|
207
244
|
|
|
208
245
|
const openSessionDrawer = useCallback(() => {
|
|
209
246
|
const session = processedSessions[selectedRow];
|
|
@@ -225,7 +262,7 @@ export function RealTimeDashboard() {
|
|
|
225
262
|
isFiltering,
|
|
226
263
|
sortField,
|
|
227
264
|
pendingG,
|
|
228
|
-
scrollOffset,
|
|
265
|
+
scrollOffset: scrollOffsetRef.current,
|
|
229
266
|
limitSelectedIndex,
|
|
230
267
|
providerCount: configuredProviders.length,
|
|
231
268
|
driverDimension,
|
|
@@ -253,14 +290,16 @@ export function RealTimeDashboard() {
|
|
|
253
290
|
processedSessions,
|
|
254
291
|
});
|
|
255
292
|
|
|
256
|
-
const windowedKpis = useMemo(
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
293
|
+
const windowedKpis = useMemo(
|
|
294
|
+
() => ({
|
|
295
|
+
cost: processedSessions.reduce((acc, s) => acc + (s.totalCostUsd ?? 0), 0),
|
|
296
|
+
tokens: processedSessions.reduce((acc, s) => acc + s.totals.input + s.totals.output, 0),
|
|
297
|
+
requests: processedSessions.reduce((acc, s) => acc + s.requestCount, 0),
|
|
298
|
+
}),
|
|
299
|
+
[processedSessions],
|
|
300
|
+
);
|
|
261
301
|
|
|
262
302
|
const budgetPeriodCost = useMemo(() => {
|
|
263
|
-
|
|
264
303
|
let dailyCost = 0;
|
|
265
304
|
let weeklyCost = 0;
|
|
266
305
|
let monthlyCost = 0;
|
|
@@ -275,10 +314,14 @@ export function RealTimeDashboard() {
|
|
|
275
314
|
}, [agentSessions]);
|
|
276
315
|
|
|
277
316
|
useEffect(() => {
|
|
278
|
-
const checks: Array<{
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
317
|
+
const checks: Array<{
|
|
318
|
+
cost: number;
|
|
319
|
+
limit: number | null;
|
|
320
|
+
type: "daily" | "weekly" | "monthly";
|
|
321
|
+
}> = [
|
|
322
|
+
{ cost: budgetPeriodCost.daily, limit: config.budgets.daily, type: "daily" },
|
|
323
|
+
{ cost: budgetPeriodCost.weekly, limit: config.budgets.weekly, type: "weekly" },
|
|
324
|
+
{ cost: budgetPeriodCost.monthly, limit: config.budgets.monthly, type: "monthly" },
|
|
282
325
|
];
|
|
283
326
|
for (const { cost, limit, type } of checks) {
|
|
284
327
|
if (limit !== null && limit > 0) {
|
|
@@ -289,20 +332,23 @@ export function RealTimeDashboard() {
|
|
|
289
332
|
|
|
290
333
|
const getBudgetCost = () => {
|
|
291
334
|
switch (budgetType) {
|
|
292
|
-
case
|
|
293
|
-
|
|
294
|
-
case
|
|
295
|
-
|
|
335
|
+
case "daily":
|
|
336
|
+
return budgetPeriodCost.daily;
|
|
337
|
+
case "weekly":
|
|
338
|
+
return budgetPeriodCost.weekly;
|
|
339
|
+
case "monthly":
|
|
340
|
+
return budgetPeriodCost.monthly;
|
|
341
|
+
default:
|
|
342
|
+
return windowedKpis.cost;
|
|
296
343
|
}
|
|
297
344
|
};
|
|
298
345
|
|
|
299
|
-
const activeCount = agentSessions.filter(s => s.status ===
|
|
346
|
+
const activeCount = agentSessions.filter((s) => s.status === "active").length;
|
|
300
347
|
|
|
301
348
|
return (
|
|
302
349
|
<box flexDirection="column" flexGrow={1} padding={1} gap={1} overflow="hidden">
|
|
303
350
|
{showHelp && <HelpOverlay />}
|
|
304
351
|
|
|
305
|
-
|
|
306
352
|
<KpiStrip
|
|
307
353
|
totalCost={windowedKpis.cost}
|
|
308
354
|
totalTokens={windowedKpis.tokens}
|
|
@@ -314,10 +360,14 @@ export function RealTimeDashboard() {
|
|
|
314
360
|
activity={activity}
|
|
315
361
|
sparkData={sparkData}
|
|
316
362
|
budget={{
|
|
317
|
-
limit:
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
363
|
+
limit:
|
|
364
|
+
budgetType === "daily"
|
|
365
|
+
? config.budgets.daily
|
|
366
|
+
: budgetType === "weekly"
|
|
367
|
+
? config.budgets.weekly
|
|
368
|
+
: budgetType === "monthly"
|
|
369
|
+
? config.budgets.monthly
|
|
370
|
+
: null,
|
|
321
371
|
budgetCost: getBudgetCost(),
|
|
322
372
|
budgetType,
|
|
323
373
|
budgetTypeLabel,
|
|
@@ -327,23 +377,27 @@ export function RealTimeDashboard() {
|
|
|
327
377
|
/>
|
|
328
378
|
|
|
329
379
|
<ProviderLimitsPanel
|
|
330
|
-
providers={configuredProviders.map(p => ({
|
|
380
|
+
providers={configuredProviders.map((p) => ({
|
|
331
381
|
id: p.plugin.id,
|
|
332
382
|
name: p.plugin.name,
|
|
333
383
|
usedPercent: getMaxUsedPercent(p),
|
|
334
384
|
color: providerColorOf(p.plugin.id),
|
|
335
385
|
...(p.usage?.error ? { error: p.usage.error } : {}),
|
|
336
386
|
}))}
|
|
337
|
-
focused={focusedPanel ===
|
|
387
|
+
focused={focusedPanel === "limits"}
|
|
338
388
|
selectedIndex={limitSelectedIndex}
|
|
339
389
|
showBudgetBar={showBudgetInLimits}
|
|
340
390
|
budget={{
|
|
341
391
|
totalCost: windowedKpis.cost,
|
|
342
392
|
budgetCost: getBudgetCost(),
|
|
343
|
-
limit:
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
393
|
+
limit:
|
|
394
|
+
budgetType === "daily"
|
|
395
|
+
? config.budgets.daily
|
|
396
|
+
: budgetType === "weekly"
|
|
397
|
+
? config.budgets.weekly
|
|
398
|
+
: budgetType === "monthly"
|
|
399
|
+
? config.budgets.monthly
|
|
400
|
+
: null,
|
|
347
401
|
budgetType,
|
|
348
402
|
budgetTypeLabel,
|
|
349
403
|
warningPercent: config.alerts.warningPercent,
|
|
@@ -376,16 +430,22 @@ export function RealTimeDashboard() {
|
|
|
376
430
|
/>
|
|
377
431
|
)}
|
|
378
432
|
</box>
|
|
379
|
-
|
|
433
|
+
|
|
380
434
|
<box flexDirection="row" paddingLeft={1} height={1} flexShrink={0}>
|
|
381
435
|
<text fg={colors.textSubtle} height={1} flexGrow={1}>
|
|
382
|
-
{isFiltering
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
436
|
+
{isFiltering
|
|
437
|
+
? "Type to filter Esc cancel Enter apply"
|
|
438
|
+
: filterQuery
|
|
439
|
+
? `Esc clear / edit filter ↑↓ navigate s sort`
|
|
440
|
+
: activeDriverFilter
|
|
441
|
+
? `Filter: ${activeDriverFilter} Esc clear`
|
|
442
|
+
: focusedPanel === "sessions"
|
|
443
|
+
? "/ filter ↑↓ navigate Enter details s sort l limits Tab next ⇧Tab prev"
|
|
444
|
+
: focusedPanel === "limits"
|
|
445
|
+
? "←→ select provider Tab next ⇧Tab prev Esc back"
|
|
446
|
+
: focusedPanel === "sidebar"
|
|
447
|
+
? "↑↓ select Enter filter m/p/a dimension b budget Tab next ⇧Tab prev"
|
|
448
|
+
: "/ filter i sidebar Tab/⇧Tab switch ? help"}
|
|
389
449
|
</text>
|
|
390
450
|
</box>
|
|
391
451
|
</box>
|