@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,12 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
1
|
+
import type { ScrollBoxRenderable } from "@opentui/core";
|
|
2
|
+
import { useTerminalDimensions } from "@opentui/react";
|
|
3
|
+
import { forwardRef, memo, type Ref, useCallback, useRef } from "react";
|
|
4
|
+
import type { AgentSessionAggregate } from "../../agents/types.ts";
|
|
5
|
+
import { useColors } from "../contexts/ThemeContext.tsx";
|
|
6
|
+
import { useAnimatedValue } from "../hooks/useAnimatedValue.ts";
|
|
7
|
+
import { applyEntranceFade, useEntranceAnimation } from "../hooks/useEntranceAnimation.ts";
|
|
8
|
+
import { useExitAnimation } from "../hooks/useExitAnimation.ts";
|
|
9
|
+
import { interpolateColor, useValueFlash } from "../hooks/useValueFlash.ts";
|
|
10
10
|
|
|
11
11
|
interface SessionsTableProps {
|
|
12
12
|
sessions: AgentSessionAggregate[];
|
|
@@ -14,22 +14,22 @@ interface SessionsTableProps {
|
|
|
14
14
|
isLoading: boolean;
|
|
15
15
|
isFiltering: boolean;
|
|
16
16
|
filterQuery: string;
|
|
17
|
-
focusedPanel:
|
|
17
|
+
focusedPanel: "sessions" | "sidebar" | "limits";
|
|
18
18
|
windowLabel: string;
|
|
19
19
|
getProviderColor: (id: string) => string;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
function extractRepoName(projectPath: string | null): string {
|
|
23
|
-
if (!projectPath || projectPath ===
|
|
24
|
-
const normalized = projectPath.replace(/\\/g,
|
|
25
|
-
return normalized.split(
|
|
23
|
+
if (!projectPath || projectPath === "—") return "—";
|
|
24
|
+
const normalized = projectPath.replace(/\\/g, "/");
|
|
25
|
+
return normalized.split("/").filter(Boolean).pop() ?? projectPath;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
function formatRelativeTime(timestamp: number): string {
|
|
29
29
|
const now = Date.now();
|
|
30
30
|
const diffMs = now - timestamp;
|
|
31
31
|
const diffSec = Math.floor(diffMs / 1000);
|
|
32
|
-
|
|
32
|
+
|
|
33
33
|
if (diffSec < 60) return `${diffSec}s`;
|
|
34
34
|
const diffMin = Math.floor(diffSec / 60);
|
|
35
35
|
if (diffMin < 60) return `${diffMin}m`;
|
|
@@ -43,7 +43,7 @@ function formatDuration(startedAt: number, endedAt?: number): string {
|
|
|
43
43
|
const end = endedAt ?? Date.now();
|
|
44
44
|
const diffMs = end - startedAt;
|
|
45
45
|
const diffSec = Math.floor(diffMs / 1000);
|
|
46
|
-
|
|
46
|
+
|
|
47
47
|
if (diffSec < 60) return `${diffSec}s`;
|
|
48
48
|
const diffMin = Math.floor(diffSec / 60);
|
|
49
49
|
if (diffMin < 60) return `${diffMin}m`;
|
|
@@ -54,23 +54,23 @@ function formatDuration(startedAt: number, endedAt?: number): string {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
function formatCost(costUsd: number): string {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
57
|
+
if (costUsd === 0) return "$0";
|
|
58
|
+
if (costUsd < 0.01) return "$0.00";
|
|
59
|
+
if (costUsd < 10) return `$${costUsd.toFixed(2)}`;
|
|
60
|
+
if (costUsd < 100) return `$${costUsd.toFixed(1)}`;
|
|
61
|
+
if (costUsd < 1000) return `$${Math.floor(costUsd)}`;
|
|
62
|
+
return `$${(costUsd / 1000).toFixed(1)}k`;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
function truncateMiddle(str: string, maxLength: number): string {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
if (str.length <= maxLength) return str;
|
|
67
|
+
const side = Math.floor((maxLength - 1) / 2);
|
|
68
|
+
return str.slice(0, side) + "…" + str.slice(-side);
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
function formatCostVal(cost: number): string {
|
|
72
|
-
if (cost === 0) return
|
|
73
|
-
if (cost < 0.01) return
|
|
72
|
+
if (cost === 0) return "$0";
|
|
73
|
+
if (cost < 0.01) return "<$0.01";
|
|
74
74
|
if (cost < 10) return `$${cost.toFixed(2)}`;
|
|
75
75
|
if (cost < 100) return `$${cost.toFixed(1)}`;
|
|
76
76
|
if (cost < 1000) return `$${Math.round(cost)}`;
|
|
@@ -96,16 +96,16 @@ function getInspectorData(session: AgentSessionAggregate) {
|
|
|
96
96
|
const cacheWrite = session.totals.cacheWrite ?? 0;
|
|
97
97
|
if (cacheRead === 0 && cacheWrite === 0) return null;
|
|
98
98
|
|
|
99
|
-
let readCost = 0,
|
|
99
|
+
let readCost = 0,
|
|
100
|
+
writeCost = 0;
|
|
100
101
|
for (const stream of session.streams) {
|
|
101
102
|
if (!stream.costBreakdown) continue;
|
|
102
103
|
readCost += stream.costBreakdown.cacheRead ?? 0;
|
|
103
104
|
writeCost += stream.costBreakdown.cacheWrite ?? 0;
|
|
104
105
|
}
|
|
105
106
|
|
|
106
|
-
const costPerReq =
|
|
107
|
-
? (session.totalCostUsd ?? 0) / session.requestCount
|
|
108
|
-
: 0;
|
|
107
|
+
const costPerReq =
|
|
108
|
+
session.requestCount > 0 ? (session.totalCostUsd ?? 0) / session.requestCount : 0;
|
|
109
109
|
|
|
110
110
|
return { cacheRead, cacheWrite, readCost, writeCost, costPerReq };
|
|
111
111
|
}
|
|
@@ -130,134 +130,211 @@ function getActivityFadeColor(lastActivityAt: number, baseColor: string, dimColo
|
|
|
130
130
|
return interpolateColor(t, baseColor, dimColor);
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
const SessionRow = memo(function SessionRow({
|
|
133
|
+
const SessionRow = memo(function SessionRow({
|
|
134
|
+
session,
|
|
135
|
+
isSelected,
|
|
136
|
+
isWide,
|
|
137
|
+
nameMaxWidth,
|
|
138
|
+
getProviderColor,
|
|
139
|
+
isExiting,
|
|
140
|
+
exitIntensity,
|
|
141
|
+
skipEntrance,
|
|
142
|
+
terminalWidth,
|
|
143
|
+
}: SessionRowProps) {
|
|
134
144
|
const colors = useColors();
|
|
135
|
-
const isActive = session.status ===
|
|
145
|
+
const isActive = session.status === "active";
|
|
136
146
|
const entranceIntensity = useEntranceAnimation({ durationMs: 500 });
|
|
137
147
|
const skipRef = useRef(skipEntrance);
|
|
138
|
-
const currentIntensity = isExiting
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
148
|
+
const currentIntensity = isExiting
|
|
149
|
+
? (exitIntensity ?? 0)
|
|
150
|
+
: skipRef.current
|
|
151
|
+
? 1
|
|
152
|
+
: entranceIntensity;
|
|
153
|
+
|
|
154
|
+
const effectiveTokens = session.totals.input + session.totals.output;
|
|
155
|
+
const cacheRead = session.totals.cacheRead ?? 0;
|
|
156
|
+
const hasCacheData = cacheRead > 0;
|
|
157
|
+
const costUsd = session.totalCostUsd ?? 0;
|
|
158
|
+
|
|
159
|
+
const animatedTokens = useAnimatedValue(effectiveTokens, { durationMs: 300, precision: 0 });
|
|
160
|
+
const animatedCost = useAnimatedValue(costUsd, { durationMs: 300, precision: 4 });
|
|
161
|
+
|
|
162
|
+
const { intensity: tokenFlash } = useValueFlash(effectiveTokens, {
|
|
163
|
+
durationMs: 400,
|
|
164
|
+
threshold: 10,
|
|
165
|
+
});
|
|
166
|
+
const { intensity: costFlash } = useValueFlash(costUsd, { durationMs: 400, threshold: 0.001 });
|
|
167
|
+
|
|
151
168
|
const primaryStream = session.streams[0];
|
|
152
|
-
const providerId = primaryStream?.providerId ??
|
|
153
|
-
const modelId = primaryStream?.modelId ??
|
|
169
|
+
const providerId = primaryStream?.providerId ?? "unknown";
|
|
170
|
+
const modelId = primaryStream?.modelId ?? "unknown";
|
|
154
171
|
const baseProviderColor = getProviderColor(providerId);
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
172
|
+
const repoName = extractRepoName(session.projectPath ?? "—");
|
|
173
|
+
// In wide mode, project is fixed width (14), so truncate to 13 chars + ellipsis if needed
|
|
174
|
+
// In narrow mode, project is flex, so use nameMaxWidth (which is actually projectMaxWidth in narrow mode context)
|
|
175
|
+
const projectDisplay = isWide
|
|
176
|
+
? repoName.length > 13
|
|
177
|
+
? repoName.slice(0, 12) + "…"
|
|
178
|
+
: repoName.padEnd(13)
|
|
179
|
+
: repoName.length > nameMaxWidth
|
|
180
|
+
? repoName.slice(0, nameMaxWidth - 1) + "…"
|
|
181
|
+
: repoName;
|
|
182
|
+
|
|
162
183
|
const dimColor = colors.background;
|
|
163
184
|
const fade = (color: string) => applyEntranceFade(currentIntensity, color, dimColor);
|
|
164
|
-
|
|
165
|
-
const baseStatusColor = isActive
|
|
185
|
+
|
|
186
|
+
const baseStatusColor = isActive
|
|
166
187
|
? getActivityFadeColor(session.lastActivityAt, colors.success, colors.textMuted)
|
|
167
188
|
: colors.textMuted;
|
|
168
189
|
const statusColor = fade(baseStatusColor);
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const tokenColorBeforeFade =
|
|
172
|
-
? interpolateColor(tokenFlash, baseTokenColor,
|
|
173
|
-
: baseTokenColor;
|
|
190
|
+
|
|
191
|
+
const baseTokenColor = colors.text;
|
|
192
|
+
const tokenColorBeforeFade =
|
|
193
|
+
tokenFlash > 0 ? interpolateColor(tokenFlash, baseTokenColor, "#ffffff") : baseTokenColor;
|
|
174
194
|
const tokenColor = fade(tokenColorBeforeFade);
|
|
175
|
-
|
|
195
|
+
|
|
176
196
|
const providerColor = fade(baseProviderColor);
|
|
177
197
|
const textColor = fade(colors.text);
|
|
178
198
|
const textSubtleColor = fade(colors.textSubtle);
|
|
179
199
|
const textMutedColor = fade(colors.textMuted);
|
|
180
200
|
const baseCostColor = colors.warning;
|
|
181
|
-
const costColorBeforeFade =
|
|
182
|
-
? interpolateColor(costFlash, baseCostColor,
|
|
183
|
-
: baseCostColor;
|
|
201
|
+
const costColorBeforeFade =
|
|
202
|
+
costFlash > 0 ? interpolateColor(costFlash, baseCostColor, "#ffffff") : baseCostColor;
|
|
184
203
|
const costColor = fade(costColorBeforeFade);
|
|
185
|
-
|
|
204
|
+
|
|
186
205
|
const formatTokensVal = (val: number): string => {
|
|
187
206
|
if (val >= 1000000) return `${(val / 1000000).toFixed(1)}M`;
|
|
188
207
|
if (val >= 1000) return `${(val / 1000).toFixed(1)}K`;
|
|
189
208
|
return `${val}`;
|
|
190
209
|
};
|
|
191
|
-
|
|
192
|
-
const railChar = isSelected ?
|
|
210
|
+
|
|
211
|
+
const railChar = isSelected ? "▌" : " ";
|
|
193
212
|
const railColor = fade(colors.primary);
|
|
194
213
|
const rowBg = isSelected ? colors.borderMuted : undefined;
|
|
195
|
-
|
|
214
|
+
|
|
196
215
|
const lastActivity = formatRelativeTime(session.lastActivityAt);
|
|
197
216
|
const duration = formatDuration(session.startedAt, session.endedAt);
|
|
198
217
|
const costDisplay = formatCost(animatedCost);
|
|
199
|
-
|
|
200
|
-
const modelDisplay = modelId.split('/').pop()?.slice(0, 15) ?? modelId;
|
|
201
|
-
const streamCount = session.streams.length;
|
|
202
|
-
const modelWithCount = streamCount > 1 ? `${modelDisplay.slice(0, 12)}+${streamCount - 1}` : modelDisplay;
|
|
203
|
-
|
|
204
|
-
const baseLastColor = isActive
|
|
205
|
-
? getActivityFadeColor(session.lastActivityAt, colors.text, colors.textMuted)
|
|
206
|
-
: colors.textMuted;
|
|
207
|
-
const lastColor = fade(baseLastColor);
|
|
208
|
-
|
|
209
|
-
const bgProp = rowBg ? { bg: rowBg } : {};
|
|
210
|
-
|
|
211
|
-
const sessionIdShort = session.sessionId.slice(-7);
|
|
212
|
-
// In wide mode, name is flex, so use nameMaxWidth
|
|
213
|
-
// In narrow mode, name is not shown
|
|
214
|
-
const sessionNameDisplay = session.sessionName ? truncateMiddle(session.sessionName, isWide ? nameMaxWidth : 25) : '—';
|
|
215
|
-
|
|
216
|
-
if (isWide) {
|
|
217
|
-
return (
|
|
218
|
-
<box flexDirection="row" paddingRight={1} height={1} gap={1} {...(rowBg ? { backgroundColor: rowBg } : {})}>
|
|
219
|
-
<text width={2} height={1} fg={railColor} {...bgProp}>{railChar}</text>
|
|
220
|
-
<text width={8} height={1} fg={textMutedColor} {...bgProp}>{sessionIdShort}</text>
|
|
221
|
-
<text width={12} height={1} fg={isSelected ? textColor : textSubtleColor} {...bgProp}>{session.agentName.padEnd(11)}</text>
|
|
222
|
-
<text width={18} height={1} fg={providerColor} {...bgProp}>{modelWithCount.padEnd(17)}</text>
|
|
223
|
-
<text width={5} height={1} fg={textMutedColor} {...bgProp}>{String(session.requestCount).padStart(4)}</text>
|
|
224
|
-
<text width={14} height={1} {...bgProp}>
|
|
225
|
-
<span fg={tokenColor}>{formatTokensVal(animatedTokens).padStart(7)}</span>
|
|
226
|
-
<span fg={colors.textMuted}>{hasCacheData ? ` ↯${formatCacheBadge(cacheRead)}` : ''}</span>
|
|
227
|
-
</text>
|
|
228
|
-
<text width={8} height={1} fg={costColor} {...bgProp}>{costDisplay.padStart(7)}</text>
|
|
229
|
-
<text width={14} height={1} fg={textSubtleColor} {...bgProp}>{projectDisplay}</text>
|
|
230
|
-
<text flexGrow={1} height={1} fg={textMutedColor} {...bgProp}>{sessionNameDisplay}</text>
|
|
231
|
-
<text width={6} height={1} fg={textMutedColor} {...bgProp}>{duration.padStart(5)}</text>
|
|
232
|
-
<text width={5} height={1} fg={lastColor} {...bgProp}>{lastActivity.padStart(4)}</text>
|
|
233
|
-
<text width={2} height={1} fg={statusColor} {...bgProp}>{isActive ? '●' : '○'}</text>
|
|
234
|
-
</box>
|
|
235
|
-
);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
const showCacheBadge = terminalWidth >= 105;
|
|
239
|
-
const hasSidebar = terminalWidth >= 85;
|
|
240
|
-
const isTight = !isWide && hasSidebar && terminalWidth < 105;
|
|
241
|
-
const narrowModelW = isTight ? 13 : 16;
|
|
242
|
-
const narrowTokenW = isTight ? 9 : 14;
|
|
243
|
-
const narrowModelMaxChars = narrowModelW - 1;
|
|
244
218
|
|
|
219
|
+
const modelDisplay = modelId.split("/").pop()?.slice(0, 15) ?? modelId;
|
|
220
|
+
const streamCount = session.streams.length;
|
|
221
|
+
const modelWithCount =
|
|
222
|
+
streamCount > 1 ? `${modelDisplay.slice(0, 12)}+${streamCount - 1}` : modelDisplay;
|
|
223
|
+
|
|
224
|
+
const baseLastColor = isActive
|
|
225
|
+
? getActivityFadeColor(session.lastActivityAt, colors.text, colors.textMuted)
|
|
226
|
+
: colors.textMuted;
|
|
227
|
+
const lastColor = fade(baseLastColor);
|
|
228
|
+
|
|
229
|
+
const bgProp = rowBg ? { bg: rowBg } : {};
|
|
230
|
+
|
|
231
|
+
const sessionIdShort = session.sessionId.slice(-7);
|
|
232
|
+
// In wide mode, name is flex, so use nameMaxWidth
|
|
233
|
+
// In narrow mode, name is not shown
|
|
234
|
+
const sessionNameDisplay = session.sessionName
|
|
235
|
+
? truncateMiddle(session.sessionName, isWide ? nameMaxWidth : 25)
|
|
236
|
+
: "—";
|
|
237
|
+
|
|
238
|
+
if (isWide) {
|
|
245
239
|
return (
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
240
|
+
<box
|
|
241
|
+
flexDirection="row"
|
|
242
|
+
paddingRight={1}
|
|
243
|
+
height={1}
|
|
244
|
+
gap={1}
|
|
245
|
+
{...(rowBg ? { backgroundColor: rowBg } : {})}
|
|
246
|
+
>
|
|
247
|
+
<text width={2} height={1} fg={railColor} {...bgProp}>
|
|
248
|
+
{railChar}
|
|
249
|
+
</text>
|
|
250
|
+
<text width={8} height={1} fg={textMutedColor} {...bgProp}>
|
|
251
|
+
{sessionIdShort}
|
|
252
|
+
</text>
|
|
253
|
+
<text width={12} height={1} fg={isSelected ? textColor : textSubtleColor} {...bgProp}>
|
|
254
|
+
{session.agentName.padEnd(11)}
|
|
255
|
+
</text>
|
|
256
|
+
<text width={18} height={1} fg={providerColor} {...bgProp}>
|
|
257
|
+
{modelWithCount.padEnd(17)}
|
|
258
|
+
</text>
|
|
259
|
+
<text width={5} height={1} fg={textMutedColor} {...bgProp}>
|
|
260
|
+
{String(session.requestCount).padStart(4)}
|
|
261
|
+
</text>
|
|
262
|
+
<text width={14} height={1} {...bgProp}>
|
|
263
|
+
<span fg={tokenColor}>{formatTokensVal(animatedTokens).padStart(7)}</span>
|
|
264
|
+
<span fg={colors.textMuted}>
|
|
265
|
+
{hasCacheData ? ` ↯${formatCacheBadge(cacheRead)}` : ""}
|
|
266
|
+
</span>
|
|
267
|
+
</text>
|
|
268
|
+
<text width={8} height={1} fg={costColor} {...bgProp}>
|
|
269
|
+
{costDisplay.padStart(7)}
|
|
270
|
+
</text>
|
|
271
|
+
<text width={14} height={1} fg={textSubtleColor} {...bgProp}>
|
|
272
|
+
{projectDisplay}
|
|
273
|
+
</text>
|
|
274
|
+
<text flexGrow={1} height={1} fg={textMutedColor} {...bgProp}>
|
|
275
|
+
{sessionNameDisplay}
|
|
276
|
+
</text>
|
|
277
|
+
<text width={6} height={1} fg={textMutedColor} {...bgProp}>
|
|
278
|
+
{duration.padStart(5)}
|
|
279
|
+
</text>
|
|
280
|
+
<text width={5} height={1} fg={lastColor} {...bgProp}>
|
|
281
|
+
{lastActivity.padStart(4)}
|
|
282
|
+
</text>
|
|
283
|
+
<text width={2} height={1} fg={statusColor} {...bgProp}>
|
|
284
|
+
{isActive ? "●" : "○"}
|
|
285
|
+
</text>
|
|
286
|
+
</box>
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const showCacheBadge = terminalWidth >= 105;
|
|
291
|
+
const hasSidebar = terminalWidth >= 85;
|
|
292
|
+
const isTight = !isWide && hasSidebar && terminalWidth < 105;
|
|
293
|
+
const narrowModelW = isTight ? 13 : 16;
|
|
294
|
+
const narrowTokenW = isTight ? 9 : 14;
|
|
295
|
+
const narrowModelMaxChars = narrowModelW - 1;
|
|
296
|
+
|
|
297
|
+
return (
|
|
298
|
+
<box
|
|
299
|
+
flexDirection="row"
|
|
300
|
+
paddingRight={1}
|
|
301
|
+
height={1}
|
|
302
|
+
{...(rowBg ? { backgroundColor: rowBg } : {})}
|
|
303
|
+
>
|
|
304
|
+
<text width={2} height={1} fg={railColor} {...bgProp}>
|
|
305
|
+
{railChar}
|
|
306
|
+
</text>
|
|
307
|
+
<text width={9} height={1} fg={isSelected ? textColor : textSubtleColor} {...bgProp}>
|
|
308
|
+
{session.agentName.length > 8
|
|
309
|
+
? session.agentName.slice(0, 8) + "…"
|
|
310
|
+
: session.agentName.padEnd(8)}
|
|
311
|
+
</text>
|
|
312
|
+
<text width={narrowModelW} height={1} fg={providerColor} {...bgProp}>
|
|
313
|
+
{modelWithCount.length > narrowModelMaxChars
|
|
314
|
+
? modelWithCount.slice(0, narrowModelMaxChars - 1) + "…"
|
|
315
|
+
: modelWithCount.padEnd(narrowModelMaxChars)}
|
|
316
|
+
</text>
|
|
317
|
+
<text width={narrowTokenW} height={1} {...bgProp}>
|
|
318
|
+
<span fg={tokenColor}>{formatTokensVal(animatedTokens).padStart(7)}</span>
|
|
319
|
+
<span fg={colors.textMuted}>
|
|
320
|
+
{hasCacheData && showCacheBadge ? ` ↯${formatCacheBadge(cacheRead)}` : ""}
|
|
321
|
+
</span>
|
|
322
|
+
</text>
|
|
323
|
+
<text width={7} height={1} fg={costColor} {...bgProp}>
|
|
324
|
+
{formatCostVal(costUsd).padStart(6)}
|
|
325
|
+
</text>
|
|
326
|
+
<text flexGrow={1} height={1} fg={textSubtleColor} {...bgProp}>
|
|
327
|
+
{projectDisplay}
|
|
328
|
+
</text>
|
|
329
|
+
<text width={5} height={1} fg={lastColor} {...bgProp}>
|
|
330
|
+
{lastActivity.padStart(4)}
|
|
331
|
+
</text>
|
|
332
|
+
<text width={2} height={1} fg={statusColor} {...bgProp}>
|
|
333
|
+
{isActive ? "●" : "○"}
|
|
334
|
+
</text>
|
|
335
|
+
</box>
|
|
336
|
+
);
|
|
337
|
+
});
|
|
261
338
|
|
|
262
339
|
const WIDE_THRESHOLD = 140;
|
|
263
340
|
|
|
@@ -272,7 +349,7 @@ export const SessionsTable = forwardRef(function SessionsTable(
|
|
|
272
349
|
windowLabel,
|
|
273
350
|
getProviderColor,
|
|
274
351
|
}: SessionsTableProps,
|
|
275
|
-
ref: Ref<ScrollBoxRenderable
|
|
352
|
+
ref: Ref<ScrollBoxRenderable>,
|
|
276
353
|
) {
|
|
277
354
|
const colors = useColors();
|
|
278
355
|
const { width: terminalWidth, height: terminalHeight } = useTerminalDimensions();
|
|
@@ -282,13 +359,26 @@ export const SessionsTable = forwardRef(function SessionsTable(
|
|
|
282
359
|
// Sidebar width depends on terminal width (see SmartSidebar.getSidebarWidth).
|
|
283
360
|
// Table inner width = terminalWidth - sidebarWidth - borders(4) - outerMargins(2).
|
|
284
361
|
// Name column = tableInnerWidth - fixedColumnsTotal.
|
|
285
|
-
const sidebarW =
|
|
362
|
+
const sidebarW =
|
|
363
|
+
terminalWidth >= 160
|
|
364
|
+
? 44
|
|
365
|
+
: terminalWidth >= 140
|
|
366
|
+
? 40
|
|
367
|
+
: terminalWidth >= 105
|
|
368
|
+
? 36
|
|
369
|
+
: terminalWidth >= 95
|
|
370
|
+
? 32
|
|
371
|
+
: terminalWidth >= 85
|
|
372
|
+
? 28
|
|
373
|
+
: 0;
|
|
286
374
|
const WIDE_FIXED = 106;
|
|
287
375
|
const hasSidebar = sidebarW > 0;
|
|
288
376
|
const isTight = !isWide && hasSidebar && terminalWidth < 105;
|
|
289
377
|
const narrowFixed = isTight ? 49 : 57;
|
|
290
378
|
const tableInner = terminalWidth - sidebarW - 6;
|
|
291
|
-
const nameMaxWidth = isWide
|
|
379
|
+
const nameMaxWidth = isWide
|
|
380
|
+
? Math.max(8, tableInner - WIDE_FIXED)
|
|
381
|
+
: Math.max(1, tableInner - narrowFixed);
|
|
292
382
|
|
|
293
383
|
const getSessionKey = useCallback((s: AgentSessionAggregate) => s.sessionId, []);
|
|
294
384
|
const { items: animatedSessions, isBulkChange } = useExitAnimation(sessions, {
|
|
@@ -296,6 +386,10 @@ export const SessionsTable = forwardRef(function SessionsTable(
|
|
|
296
386
|
getKey: getSessionKey,
|
|
297
387
|
});
|
|
298
388
|
|
|
389
|
+
// Track bulk changes to force scrollbox remount (clears ghost text)
|
|
390
|
+
const bulkCountRef = useRef(0);
|
|
391
|
+
if (isBulkChange) bulkCountRef.current++;
|
|
392
|
+
|
|
299
393
|
let activeIndex = 0;
|
|
300
394
|
const selectedSession = sessions[selectedRow] ?? null;
|
|
301
395
|
const inspector = selectedSession ? getInspectorData(selectedSession) : null;
|
|
@@ -305,50 +399,107 @@ export const SessionsTable = forwardRef(function SessionsTable(
|
|
|
305
399
|
flexDirection="column"
|
|
306
400
|
flexGrow={2}
|
|
307
401
|
border
|
|
308
|
-
borderStyle={focusedPanel ===
|
|
309
|
-
borderColor={focusedPanel ===
|
|
402
|
+
borderStyle={focusedPanel === "sessions" ? "double" : "single"}
|
|
403
|
+
borderColor={focusedPanel === "sessions" ? colors.primary : colors.border}
|
|
310
404
|
overflow="hidden"
|
|
311
405
|
>
|
|
312
|
-
<box
|
|
406
|
+
<box
|
|
407
|
+
flexDirection="row"
|
|
408
|
+
paddingLeft={2}
|
|
409
|
+
paddingRight={1}
|
|
410
|
+
height={1}
|
|
411
|
+
justifyContent="space-between"
|
|
412
|
+
overflow="hidden"
|
|
413
|
+
>
|
|
313
414
|
<text height={1} fg={colors.textMuted}>
|
|
314
|
-
SESSIONS{filterQuery ? ` [${isFiltering ?
|
|
415
|
+
SESSIONS{filterQuery ? ` [${isFiltering ? "Filter: " : ""}${filterQuery}]` : ""}
|
|
416
|
+
{isLoading ? " ⟳" : " "}
|
|
417
|
+
</text>
|
|
418
|
+
<text height={1} fg={colors.textMuted}>
|
|
419
|
+
[{windowLabel}] {sessions.length} sessions
|
|
315
420
|
</text>
|
|
316
|
-
<text height={1} fg={colors.textMuted}>[{windowLabel}] {sessions.length} sessions</text>
|
|
317
421
|
</box>
|
|
318
422
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
423
|
+
{isWide ? (
|
|
424
|
+
<box flexDirection="row" paddingRight={1} height={1} gap={1}>
|
|
425
|
+
<text width={2} height={1} fg={colors.textMuted}>
|
|
426
|
+
{" "}
|
|
427
|
+
</text>
|
|
428
|
+
<text width={8} height={1} fg={colors.textMuted}>
|
|
429
|
+
ID{" "}
|
|
430
|
+
</text>
|
|
431
|
+
<text width={12} height={1} fg={colors.textMuted}>
|
|
432
|
+
AGENT{" "}
|
|
433
|
+
</text>
|
|
434
|
+
<text width={18} height={1} fg={colors.textMuted}>
|
|
435
|
+
MODEL{" "}
|
|
436
|
+
</text>
|
|
437
|
+
<text width={5} height={1} fg={colors.textMuted}>
|
|
438
|
+
{" "}
|
|
439
|
+
REQ{" "}
|
|
440
|
+
</text>
|
|
441
|
+
<text width={14} height={1} fg={colors.textMuted}>
|
|
442
|
+
{" "}
|
|
443
|
+
TOKENS{" "}
|
|
444
|
+
</text>
|
|
445
|
+
<text width={8} height={1} fg={colors.textMuted}>
|
|
446
|
+
{" "}
|
|
447
|
+
COST{" "}
|
|
448
|
+
</text>
|
|
449
|
+
<text width={14} height={1} fg={colors.textMuted}>
|
|
450
|
+
PROJECT{" "}
|
|
451
|
+
</text>
|
|
452
|
+
<text flexGrow={1} height={1} fg={colors.textMuted}>
|
|
453
|
+
NAME
|
|
454
|
+
</text>
|
|
455
|
+
<text width={6} height={1} fg={colors.textMuted}>
|
|
456
|
+
{" "}
|
|
457
|
+
DUR{" "}
|
|
458
|
+
</text>
|
|
459
|
+
<text width={5} height={1} fg={colors.textMuted}>
|
|
460
|
+
LAST{" "}
|
|
461
|
+
</text>
|
|
462
|
+
<text width={2} height={1} fg={colors.textMuted}>
|
|
463
|
+
{" "}
|
|
464
|
+
</text>
|
|
465
|
+
</box>
|
|
466
|
+
) : (
|
|
467
|
+
<box flexDirection="row" paddingRight={1} height={1}>
|
|
468
|
+
<text width={2} height={1} fg={colors.textMuted}>
|
|
469
|
+
{" "}
|
|
470
|
+
</text>
|
|
471
|
+
<text width={9} height={1} fg={colors.textMuted}>
|
|
472
|
+
AGENT{" "}
|
|
473
|
+
</text>
|
|
474
|
+
<text width={isTight ? 13 : 16} height={1} fg={colors.textMuted}>
|
|
475
|
+
{isTight ? "MODEL " : "MODEL "}
|
|
476
|
+
</text>
|
|
477
|
+
<text width={isTight ? 9 : 14} height={1} fg={colors.textMuted}>
|
|
478
|
+
{isTight ? " TOKENS " : " TOKENS "}
|
|
479
|
+
</text>
|
|
480
|
+
<text width={7} height={1} fg={colors.textMuted}>
|
|
481
|
+
{" "}
|
|
482
|
+
COST{" "}
|
|
483
|
+
</text>
|
|
484
|
+
<text flexGrow={1} height={1} fg={colors.textMuted}>
|
|
485
|
+
{nameMaxWidth >= 7 ? "PROJECT" : nameMaxWidth >= 4 ? "PROJ" : ""}
|
|
486
|
+
</text>
|
|
487
|
+
<text width={5} height={1} fg={colors.textMuted}>
|
|
488
|
+
LAST{" "}
|
|
489
|
+
</text>
|
|
490
|
+
<text width={2} height={1} fg={colors.textMuted}>
|
|
491
|
+
{" "}
|
|
492
|
+
</text>
|
|
493
|
+
</box>
|
|
494
|
+
)}
|
|
495
|
+
|
|
496
|
+
<scrollbox key={`sessions-sb-${bulkCountRef.current}`} ref={ref} flexGrow={1}>
|
|
497
|
+
<box flexDirection="column" flexGrow={1}>
|
|
349
498
|
{sessions.length === 0 && animatedSessions.length === 0 && (
|
|
350
499
|
<box paddingLeft={2}>
|
|
351
|
-
<text fg={colors.textMuted}>
|
|
500
|
+
<text fg={colors.textMuted}>
|
|
501
|
+
{isLoading ? "Loading sessions..." : "No sessions found"}
|
|
502
|
+
</text>
|
|
352
503
|
</box>
|
|
353
504
|
)}
|
|
354
505
|
{animatedSessions.map((entry) => {
|
|
@@ -372,7 +523,7 @@ export const SessionsTable = forwardRef(function SessionsTable(
|
|
|
372
523
|
</scrollbox>
|
|
373
524
|
{terminalHeight >= 30 && (
|
|
374
525
|
<box height={1} paddingX={2} flexShrink={0}>
|
|
375
|
-
{inspector && focusedPanel ===
|
|
526
|
+
{inspector && focusedPanel === "sessions" ? (
|
|
376
527
|
<text height={1} overflow="hidden">
|
|
377
528
|
<span fg={colors.textMuted}>↯ Cache read </span>
|
|
378
529
|
<span fg={colors.text}>{formatTokensCompact(inspector.cacheRead)}</span>
|