@tokentop/ttop 0.6.1 → 0.7.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/package.json +4 -4
- package/src/agents/aggregator.test.ts +269 -1
- package/src/agents/aggregator.ts +196 -8
- package/src/agents/costing.test.ts +584 -0
- package/src/agents/costing.ts +144 -30
- package/src/agents/types.ts +7 -0
- package/src/demo/simulator.test.ts +92 -0
- package/src/demo/simulator.ts +579 -211
- package/src/plugins/plugin-context-factory.ts +9 -1
- package/src/plugins/providers/anthropic.test.ts +145 -0
- package/src/plugins/providers/anthropic.ts +18 -12
- package/src/plugins/providers/chutes.ts +2 -0
- package/src/plugins/providers/github-copilot.ts +3 -0
- package/src/plugins/providers/minimax.ts +2 -0
- package/src/plugins/providers/openai-api.ts +4 -0
- package/src/plugins/providers/zai.ts +2 -0
- package/src/pricing/estimator.ts +7 -0
- package/src/pricing/index.ts +5 -0
- package/src/pricing/models-dev.test.ts +117 -0
- package/src/pricing/models-dev.ts +22 -0
- package/src/storage/persistence-service.test.ts +201 -0
- package/src/storage/persistence-service.ts +54 -5
- package/src/storage/repos/agentSessions.ts +49 -0
- package/src/tui/App.tsx +4 -4
- package/src/tui/components/SessionDetailsDrawer.tsx +29 -1
- package/src/tui/components/SessionsTable.tsx +25 -7
- package/src/tui/contexts/AgentSessionContext.tsx +59 -21
- package/src/tui/contexts/PluginContext.tsx +31 -10
- package/src/tui/utils/scrollFollow.test.ts +71 -0
- package/src/tui/utils/scrollFollow.ts +12 -0
- package/src/tui/views/Dashboard.tsx +0 -6
- package/src/tui/views/RealTimeDashboard.tsx +105 -38
- package/src/version.ts +1 -1
|
@@ -60,12 +60,6 @@ export function Dashboard() {
|
|
|
60
60
|
const cardRefs = useRef<(BoxRenderable | null)[]>([]);
|
|
61
61
|
const inputRef = useRef<InputRenderable>(null);
|
|
62
62
|
|
|
63
|
-
useEffect(() => {
|
|
64
|
-
if (isInitialized) {
|
|
65
|
-
refreshAllProviders();
|
|
66
|
-
}
|
|
67
|
-
}, [isInitialized]);
|
|
68
|
-
|
|
69
63
|
const getMaxUsage = useCallback((state: ProviderState) => {
|
|
70
64
|
if (!state.usage?.limits) return 0;
|
|
71
65
|
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import type { ScrollBoxRenderable } from "@opentui/core";
|
|
2
2
|
import { useTerminalDimensions } from "@opentui/react";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
type Dispatch,
|
|
5
|
+
type SetStateAction,
|
|
6
|
+
useCallback,
|
|
7
|
+
useEffect,
|
|
8
|
+
useLayoutEffect,
|
|
9
|
+
useMemo,
|
|
10
|
+
useRef,
|
|
11
|
+
useState,
|
|
12
|
+
} from "react";
|
|
4
13
|
import { notificationBus } from "@/plugins/notification-bus.ts";
|
|
5
14
|
import { classifyProviderError } from "@/utils/error-category.ts";
|
|
6
15
|
import { HelpOverlay } from "../components/HelpOverlay.tsx";
|
|
@@ -20,6 +29,7 @@ import { useDashboardKeyboard } from "../hooks/useDashboardKeyboard.ts";
|
|
|
20
29
|
import type { SortDirection, SortField } from "../types/sort.ts";
|
|
21
30
|
import { SORT_FIELDS } from "../types/sort.ts";
|
|
22
31
|
import { getProviderColor } from "../utils/providerColor.ts";
|
|
32
|
+
import { computeScrollOffset } from "../utils/scrollFollow.ts";
|
|
23
33
|
|
|
24
34
|
export function RealTimeDashboard() {
|
|
25
35
|
const colors = useColors();
|
|
@@ -77,7 +87,13 @@ export function RealTimeDashboard() {
|
|
|
77
87
|
const { showDrawer, isOpen: showSessionDrawer } = useDrawer();
|
|
78
88
|
|
|
79
89
|
const [showHelp, setShowHelp] = useState(false);
|
|
80
|
-
|
|
90
|
+
// Cursor and scroll offset share one state so batched j-presses advance
|
|
91
|
+
// both in lockstep — see setSelectedRow below.
|
|
92
|
+
const [nav, setNav] = useState<{ selectedRow: number; scrollOffset: number }>({
|
|
93
|
+
selectedRow: 0,
|
|
94
|
+
scrollOffset: 0,
|
|
95
|
+
});
|
|
96
|
+
const { selectedRow, scrollOffset } = nav;
|
|
81
97
|
const [focusedPanel, setFocusedPanel] = useState<"sessions" | "sidebar" | "limits">("sessions");
|
|
82
98
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(config.display.sidebarCollapsed);
|
|
83
99
|
const [filterQuery, setFilterQuery] = useState("");
|
|
@@ -86,12 +102,7 @@ export function RealTimeDashboard() {
|
|
|
86
102
|
const [sortDirection, setSortDirection] = useState<SortDirection>("desc");
|
|
87
103
|
const [showSortOverlay, setShowSortOverlay] = useState(false);
|
|
88
104
|
const [pendingG, setPendingG] = useState(false);
|
|
89
|
-
const scrollOffsetRef = useRef(0);
|
|
90
105
|
const selectedSessionIdRef = useRef<string | null>(null);
|
|
91
|
-
const setScrollOffset = useCallback((val: number) => {
|
|
92
|
-
scrollOffsetRef.current = val;
|
|
93
|
-
sessionsScrollboxRef.current?.scrollTo(val);
|
|
94
|
-
}, []);
|
|
95
106
|
const [limitSelectedIndex, setLimitSelectedIndex] = useState(0);
|
|
96
107
|
const [driverDimension, setDriverDimension] = useState<DriverDimension>("model");
|
|
97
108
|
const [selectedDriverIndex, setSelectedDriverIndex] = useState(0);
|
|
@@ -102,6 +113,32 @@ export function RealTimeDashboard() {
|
|
|
102
113
|
|
|
103
114
|
const sessionsScrollboxRef = useRef<ScrollBoxRenderable>(null);
|
|
104
115
|
|
|
116
|
+
const visibleRowsRef = useRef(visibleRows);
|
|
117
|
+
visibleRowsRef.current = visibleRows;
|
|
118
|
+
const sessionsLengthRef = useRef(0);
|
|
119
|
+
|
|
120
|
+
const setSelectedRow = useCallback<Dispatch<SetStateAction<number>>>((arg) => {
|
|
121
|
+
setNav((prev) => {
|
|
122
|
+
const next = typeof arg === "function" ? arg(prev.selectedRow) : arg;
|
|
123
|
+
const clamped = Math.max(0, next);
|
|
124
|
+
const newOffset = computeScrollOffset(
|
|
125
|
+
clamped,
|
|
126
|
+
prev.scrollOffset,
|
|
127
|
+
visibleRowsRef.current,
|
|
128
|
+
sessionsLengthRef.current,
|
|
129
|
+
);
|
|
130
|
+
if (clamped === prev.selectedRow && newOffset === prev.scrollOffset) return prev;
|
|
131
|
+
return { selectedRow: clamped, scrollOffset: newOffset };
|
|
132
|
+
});
|
|
133
|
+
}, []);
|
|
134
|
+
|
|
135
|
+
const setScrollOffset = useCallback((val: number) => {
|
|
136
|
+
setNav((prev) => {
|
|
137
|
+
if (val === prev.scrollOffset) return prev;
|
|
138
|
+
return { ...prev, scrollOffset: val };
|
|
139
|
+
});
|
|
140
|
+
}, []);
|
|
141
|
+
|
|
105
142
|
const configuredProviders = useMemo(() => {
|
|
106
143
|
return Array.from(providers.values())
|
|
107
144
|
.filter((p) => p.configured)
|
|
@@ -123,7 +160,9 @@ export function RealTimeDashboard() {
|
|
|
123
160
|
);
|
|
124
161
|
|
|
125
162
|
const baseFilteredSessions = useMemo(() => {
|
|
126
|
-
|
|
163
|
+
if (windowMs === null && !filterQuery) return agentSessions;
|
|
164
|
+
|
|
165
|
+
let result = agentSessions;
|
|
127
166
|
|
|
128
167
|
if (windowMs !== null) {
|
|
129
168
|
const cutoff = Date.now() - windowMs;
|
|
@@ -255,8 +294,12 @@ export function RealTimeDashboard() {
|
|
|
255
294
|
return result;
|
|
256
295
|
}, [baseFilteredSessions, activeDriverFilter, driverDimension, sortField, sortDirection]);
|
|
257
296
|
|
|
258
|
-
|
|
259
|
-
|
|
297
|
+
sessionsLengthRef.current = processedSessions.length;
|
|
298
|
+
|
|
299
|
+
// A keypress and a refresh tick can batch into one render; if they do,
|
|
300
|
+
// `prev` may be the user's new row while `selectedSessionIdRef` is still
|
|
301
|
+
// the session they moved *from*. Trusting `prev` when it resolves to a
|
|
302
|
+
// valid session prevents a snap-back bounce.
|
|
260
303
|
useLayoutEffect(() => {
|
|
261
304
|
if (processedSessions.length === 0) {
|
|
262
305
|
setSelectedRow(0);
|
|
@@ -265,24 +308,40 @@ export function RealTimeDashboard() {
|
|
|
265
308
|
return;
|
|
266
309
|
}
|
|
267
310
|
|
|
268
|
-
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
)
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
311
|
+
setSelectedRow((prev) => {
|
|
312
|
+
const currentSession = processedSessions[prev];
|
|
313
|
+
|
|
314
|
+
if (currentSession) {
|
|
315
|
+
if (currentSession.sessionId === selectedSessionIdRef.current) {
|
|
316
|
+
return prev;
|
|
317
|
+
}
|
|
318
|
+
if (selectedSessionIdRef.current) {
|
|
319
|
+
const trackedIndex = processedSessions.findIndex(
|
|
320
|
+
(s) => s.sessionId === selectedSessionIdRef.current,
|
|
321
|
+
);
|
|
322
|
+
if (trackedIndex !== -1 && trackedIndex !== prev) {
|
|
323
|
+
return trackedIndex;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
selectedSessionIdRef.current = currentSession.sessionId;
|
|
327
|
+
return prev;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (selectedSessionIdRef.current) {
|
|
331
|
+
const trackedIndex = processedSessions.findIndex(
|
|
332
|
+
(s) => s.sessionId === selectedSessionIdRef.current,
|
|
333
|
+
);
|
|
334
|
+
if (trackedIndex !== -1) return trackedIndex;
|
|
335
|
+
selectedSessionIdRef.current = null;
|
|
275
336
|
}
|
|
276
|
-
// Tracked session was filtered out — fall through to clamp
|
|
277
|
-
selectedSessionIdRef.current = null;
|
|
278
|
-
}
|
|
279
337
|
|
|
280
|
-
|
|
281
|
-
|
|
338
|
+
const clamped = Math.min(prev, processedSessions.length - 1);
|
|
339
|
+
selectedSessionIdRef.current = processedSessions[clamped]?.sessionId ?? null;
|
|
340
|
+
return clamped;
|
|
341
|
+
});
|
|
282
342
|
}, [processedSessions, setScrollOffset]);
|
|
283
343
|
|
|
284
|
-
|
|
285
|
-
useEffect(() => {
|
|
344
|
+
useLayoutEffect(() => {
|
|
286
345
|
const session = processedSessions[selectedRow];
|
|
287
346
|
if (session) {
|
|
288
347
|
selectedSessionIdRef.current = session.sessionId;
|
|
@@ -290,19 +349,22 @@ export function RealTimeDashboard() {
|
|
|
290
349
|
}, [selectedRow, processedSessions]);
|
|
291
350
|
|
|
292
351
|
useLayoutEffect(() => {
|
|
293
|
-
if (
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
352
|
+
if (processedSessions.length === 0) return;
|
|
353
|
+
setNav((prev) => {
|
|
354
|
+
const newOffset = computeScrollOffset(
|
|
355
|
+
prev.selectedRow,
|
|
356
|
+
prev.scrollOffset,
|
|
357
|
+
visibleRows,
|
|
358
|
+
processedSessions.length,
|
|
359
|
+
);
|
|
360
|
+
if (newOffset === prev.scrollOffset) return prev;
|
|
361
|
+
return { ...prev, scrollOffset: newOffset };
|
|
362
|
+
});
|
|
363
|
+
}, [processedSessions.length, visibleRows]);
|
|
302
364
|
|
|
303
|
-
|
|
304
|
-
sessionsScrollboxRef.current
|
|
305
|
-
}, [
|
|
365
|
+
useLayoutEffect(() => {
|
|
366
|
+
sessionsScrollboxRef.current?.scrollTo(scrollOffset);
|
|
367
|
+
}, [scrollOffset]);
|
|
306
368
|
|
|
307
369
|
const openSessionDrawer = useCallback(() => {
|
|
308
370
|
const session = processedSessions[selectedRow];
|
|
@@ -325,7 +387,7 @@ export function RealTimeDashboard() {
|
|
|
325
387
|
sortField,
|
|
326
388
|
sortDirection,
|
|
327
389
|
pendingG,
|
|
328
|
-
scrollOffset
|
|
390
|
+
scrollOffset,
|
|
329
391
|
limitSelectedIndex,
|
|
330
392
|
providerCount: configuredProviders.length,
|
|
331
393
|
driverDimension,
|
|
@@ -409,7 +471,10 @@ export function RealTimeDashboard() {
|
|
|
409
471
|
}
|
|
410
472
|
};
|
|
411
473
|
|
|
412
|
-
const activeCount =
|
|
474
|
+
const activeCount = useMemo(
|
|
475
|
+
() => agentSessions.reduce((n, s) => (s.status === "active" ? n + 1 : n), 0),
|
|
476
|
+
[agentSessions],
|
|
477
|
+
);
|
|
413
478
|
|
|
414
479
|
return (
|
|
415
480
|
<box flexDirection="column" flexGrow={1} padding={1} gap={1} overflow="hidden">
|
|
@@ -491,6 +556,8 @@ export function RealTimeDashboard() {
|
|
|
491
556
|
ref={sessionsScrollboxRef}
|
|
492
557
|
sessions={processedSessions}
|
|
493
558
|
selectedRow={selectedRow}
|
|
559
|
+
scrollOffset={scrollOffset}
|
|
560
|
+
visibleRows={visibleRows}
|
|
494
561
|
isLoading={isLoading}
|
|
495
562
|
isFiltering={isFiltering}
|
|
496
563
|
filterQuery={filterQuery}
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.
|
|
1
|
+
export const VERSION = "0.7.0"; // x-release-please-version
|