@tokentop/ttop 0.2.2
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/LICENSE +21 -0
- package/README.md +273 -0
- package/bin/ttop.js +49 -0
- package/package.json +68 -0
- package/src/agents/aggregator.ts +176 -0
- package/src/agents/costing.ts +74 -0
- package/src/agents/index.ts +12 -0
- package/src/agents/types.ts +71 -0
- package/src/cli.ts +167 -0
- package/src/config/schema.test.ts +166 -0
- package/src/config/schema.ts +165 -0
- package/src/demo/simulator.ts +745 -0
- package/src/plugins/agents/index.ts +2 -0
- package/src/plugins/auth-sources.ts +186 -0
- package/src/plugins/lifecycle.ts +177 -0
- package/src/plugins/loader.test.ts +195 -0
- package/src/plugins/loader.ts +282 -0
- package/src/plugins/notification-bus.ts +153 -0
- package/src/plugins/notifications/index.ts +2 -0
- package/src/plugins/notifications/terminal-bell.ts +66 -0
- package/src/plugins/notifications/visual-flash.ts +57 -0
- package/src/plugins/npm-installer.ts +164 -0
- package/src/plugins/plugin-context-factory.ts +67 -0
- package/src/plugins/plugin-host.ts +205 -0
- package/src/plugins/providers/anthropic.ts +325 -0
- package/src/plugins/providers/antigravity.ts +309 -0
- package/src/plugins/providers/codex.ts +250 -0
- package/src/plugins/providers/gemini.ts +391 -0
- package/src/plugins/providers/github-copilot.ts +194 -0
- package/src/plugins/providers/index.ts +12 -0
- package/src/plugins/providers/minimax.ts +223 -0
- package/src/plugins/providers/openai-api.ts +250 -0
- package/src/plugins/providers/opencode-zen.ts +125 -0
- package/src/plugins/providers/perplexity.ts +177 -0
- package/src/plugins/providers/zai.ts +225 -0
- package/src/plugins/registry.ts +251 -0
- package/src/plugins/sandbox-guard.test.ts +191 -0
- package/src/plugins/sandbox-guard.ts +155 -0
- package/src/plugins/sandbox.ts +138 -0
- package/src/plugins/themes/catppuccin-latte.ts +45 -0
- package/src/plugins/themes/catppuccin-mocha.ts +45 -0
- package/src/plugins/themes/claude-code.ts +45 -0
- package/src/plugins/themes/dracula.ts +45 -0
- package/src/plugins/themes/github-light.ts +45 -0
- package/src/plugins/themes/gruvbox-dark.ts +45 -0
- package/src/plugins/themes/gruvbox-light.ts +45 -0
- package/src/plugins/themes/index.ts +15 -0
- package/src/plugins/themes/kanagawa.ts +45 -0
- package/src/plugins/themes/nord.ts +45 -0
- package/src/plugins/themes/one-dark.ts +45 -0
- package/src/plugins/themes/opencode.ts +47 -0
- package/src/plugins/themes/rose-pine-dawn.ts +45 -0
- package/src/plugins/themes/rose-pine.ts +45 -0
- package/src/plugins/themes/solarized-light.ts +45 -0
- package/src/plugins/themes/tokyo-night.ts +49 -0
- package/src/plugins/types/agent.ts +108 -0
- package/src/plugins/types/base.ts +267 -0
- package/src/plugins/types/index.ts +19 -0
- package/src/plugins/types/notification.ts +51 -0
- package/src/plugins/types/provider.ts +233 -0
- package/src/plugins/types/theme.ts +70 -0
- package/src/plugins/update-checker.ts +177 -0
- package/src/pricing/estimator.test.ts +181 -0
- package/src/pricing/estimator.ts +80 -0
- package/src/pricing/fallback.test.ts +71 -0
- package/src/pricing/fallback.ts +62 -0
- package/src/pricing/index.ts +66 -0
- package/src/pricing/models-dev.ts +130 -0
- package/src/storage/database.ts +613 -0
- package/src/storage/db.ts +64 -0
- package/src/storage/index.ts +66 -0
- package/src/storage/migrations/index.ts +189 -0
- package/src/storage/paths.ts +24 -0
- package/src/storage/repos/agentSessions.ts +210 -0
- package/src/storage/repos/providerSnapshots.ts +116 -0
- package/src/storage/repos/usageEvents.ts +334 -0
- package/src/storage/types.ts +288 -0
- package/src/tui/App.tsx +381 -0
- package/src/tui/components/CommandPalette.tsx +166 -0
- package/src/tui/components/DebugConsole.tsx +161 -0
- package/src/tui/components/DebugPanel.tsx +660 -0
- package/src/tui/components/FooterHints.tsx +22 -0
- package/src/tui/components/GhostProviderCard.tsx +45 -0
- package/src/tui/components/Header.tsx +125 -0
- package/src/tui/components/HelpOverlay.tsx +45 -0
- package/src/tui/components/InlineGauge.tsx +34 -0
- package/src/tui/components/InlineSparkline.tsx +78 -0
- package/src/tui/components/KpiStrip.tsx +246 -0
- package/src/tui/components/LimitGauge.tsx +186 -0
- package/src/tui/components/ModalBackdrop.tsx +42 -0
- package/src/tui/components/ProviderAggregateStrip.tsx +200 -0
- package/src/tui/components/ProviderCard.tsx +309 -0
- package/src/tui/components/ProviderDetailPanel.tsx +237 -0
- package/src/tui/components/ProviderLimitsPanel.tsx +308 -0
- package/src/tui/components/ProvidersList.tsx +380 -0
- package/src/tui/components/SessionDetailsDrawer.tsx +317 -0
- package/src/tui/components/SessionsTable.tsx +401 -0
- package/src/tui/components/SettingsModal.tsx +587 -0
- package/src/tui/components/SidebarBreakdown.tsx +87 -0
- package/src/tui/components/Skeleton.tsx +102 -0
- package/src/tui/components/SmartSidebar.tsx +644 -0
- package/src/tui/components/Sparkline.tsx +180 -0
- package/src/tui/components/Spinner.tsx +36 -0
- package/src/tui/components/StatusBar.tsx +96 -0
- package/src/tui/components/ThemePicker.tsx +234 -0
- package/src/tui/components/Toast.tsx +61 -0
- package/src/tui/components/UsageGauge.tsx +80 -0
- package/src/tui/components/index.ts +5 -0
- package/src/tui/contexts/AgentSessionContext.tsx +307 -0
- package/src/tui/contexts/ConfigContext.tsx +141 -0
- package/src/tui/contexts/DashboardRuntimeContext.tsx +102 -0
- package/src/tui/contexts/DemoModeContext.tsx +60 -0
- package/src/tui/contexts/DrawerContext.tsx +42 -0
- package/src/tui/contexts/InputContext.tsx +30 -0
- package/src/tui/contexts/LogContext.tsx +150 -0
- package/src/tui/contexts/PluginContext.tsx +424 -0
- package/src/tui/contexts/RealTimeActivityContext.tsx +119 -0
- package/src/tui/contexts/StorageContext.tsx +233 -0
- package/src/tui/contexts/ThemeContext.tsx +95 -0
- package/src/tui/contexts/TimeWindowContext.tsx +158 -0
- package/src/tui/contexts/ToastContext.tsx +40 -0
- package/src/tui/contexts/index.ts +5 -0
- package/src/tui/createApp.tsx +47 -0
- package/src/tui/debug/captureFrame.ts +262 -0
- package/src/tui/debug/snapshot.tsx +644 -0
- package/src/tui/driver/assertions.ts +279 -0
- package/src/tui/driver/cli.ts +717 -0
- package/src/tui/driver/coverage.ts +204 -0
- package/src/tui/driver/demo-snapshot.ts +66 -0
- package/src/tui/driver/diff.ts +166 -0
- package/src/tui/driver/driver.ts +270 -0
- package/src/tui/driver/index.ts +53 -0
- package/src/tui/driver/recorder.ts +303 -0
- package/src/tui/driver/test-workflow.ts +115 -0
- package/src/tui/driver/test.ts +47 -0
- package/src/tui/hooks/index.ts +20 -0
- package/src/tui/hooks/useAnimatedValue.ts +113 -0
- package/src/tui/hooks/useDashboardKeyboard.ts +341 -0
- package/src/tui/hooks/useDashboardState.ts +116 -0
- package/src/tui/hooks/useEmaActivity.ts +223 -0
- package/src/tui/hooks/useEntranceAnimation.ts +62 -0
- package/src/tui/hooks/useExitAnimation.ts +137 -0
- package/src/tui/hooks/usePulse.ts +138 -0
- package/src/tui/hooks/useSafeRenderer.ts +24 -0
- package/src/tui/hooks/useValueFlash.ts +111 -0
- package/src/tui/index.tsx +50 -0
- package/src/tui/utils/providerColor.ts +32 -0
- package/src/tui/views/Dashboard.tsx +340 -0
- package/src/tui/views/HistoricalTrendsView.tsx +1431 -0
- package/src/tui/views/ProjectsView.tsx +1277 -0
- package/src/tui/views/RealTimeDashboard.tsx +393 -0
- package/src/tui/views/SettingsView.tsx +465 -0
- package/src/tui/views/index.ts +2 -0
- package/src/types/global.d.ts +5 -0
- package/src/types/opentui-test-utils.d.ts +18 -0
- package/src/utils/clipboard.ts +118 -0
- package/src/utils/currency.test.ts +106 -0
- package/src/utils/currency.ts +79 -0
- package/src/version.ts +1 -0
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import { forwardRef, memo, useCallback, useRef, type Ref } from 'react';
|
|
2
|
+
import type { ScrollBoxRenderable } from '@opentui/core';
|
|
3
|
+
import { useTerminalDimensions } from '@opentui/react';
|
|
4
|
+
import { useColors } from '../contexts/ThemeContext.tsx';
|
|
5
|
+
import type { AgentSessionAggregate } from '../../agents/types.ts';
|
|
6
|
+
import { useValueFlash, interpolateColor } from '../hooks/useValueFlash.ts';
|
|
7
|
+
import { useAnimatedValue } from '../hooks/useAnimatedValue.ts';
|
|
8
|
+
import { useEntranceAnimation, applyEntranceFade } from '../hooks/useEntranceAnimation.ts';
|
|
9
|
+
import { useExitAnimation } from '../hooks/useExitAnimation.ts';
|
|
10
|
+
|
|
11
|
+
interface SessionsTableProps {
|
|
12
|
+
sessions: AgentSessionAggregate[];
|
|
13
|
+
selectedRow: number;
|
|
14
|
+
isLoading: boolean;
|
|
15
|
+
isFiltering: boolean;
|
|
16
|
+
filterQuery: string;
|
|
17
|
+
focusedPanel: 'sessions' | 'sidebar' | 'limits';
|
|
18
|
+
windowLabel: string;
|
|
19
|
+
getProviderColor: (id: string) => string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function extractRepoName(projectPath: string | null): string {
|
|
23
|
+
if (!projectPath || projectPath === '—') return '—';
|
|
24
|
+
const normalized = projectPath.replace(/\\/g, '/');
|
|
25
|
+
return normalized.split('/').filter(Boolean).pop() ?? projectPath;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function formatRelativeTime(timestamp: number): string {
|
|
29
|
+
const now = Date.now();
|
|
30
|
+
const diffMs = now - timestamp;
|
|
31
|
+
const diffSec = Math.floor(diffMs / 1000);
|
|
32
|
+
|
|
33
|
+
if (diffSec < 60) return `${diffSec}s`;
|
|
34
|
+
const diffMin = Math.floor(diffSec / 60);
|
|
35
|
+
if (diffMin < 60) return `${diffMin}m`;
|
|
36
|
+
const diffHour = Math.floor(diffMin / 60);
|
|
37
|
+
if (diffHour < 24) return `${diffHour}h`;
|
|
38
|
+
const diffDay = Math.floor(diffHour / 24);
|
|
39
|
+
return `${diffDay}d`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function formatDuration(startedAt: number, endedAt?: number): string {
|
|
43
|
+
const end = endedAt ?? Date.now();
|
|
44
|
+
const diffMs = end - startedAt;
|
|
45
|
+
const diffSec = Math.floor(diffMs / 1000);
|
|
46
|
+
|
|
47
|
+
if (diffSec < 60) return `${diffSec}s`;
|
|
48
|
+
const diffMin = Math.floor(diffSec / 60);
|
|
49
|
+
if (diffMin < 60) return `${diffMin}m`;
|
|
50
|
+
const diffHour = Math.floor(diffMin / 60);
|
|
51
|
+
if (diffHour < 24) return `${diffHour}h${diffMin % 60}m`;
|
|
52
|
+
const diffDay = Math.floor(diffHour / 24);
|
|
53
|
+
return `${diffDay}d${diffHour % 24}h`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function formatCost(costUsd: number): string {
|
|
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
|
+
}
|
|
64
|
+
|
|
65
|
+
function truncateMiddle(str: string, maxLength: number): string {
|
|
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
|
+
}
|
|
70
|
+
|
|
71
|
+
function formatCostVal(cost: number): string {
|
|
72
|
+
if (cost === 0) return '$0';
|
|
73
|
+
if (cost < 0.01) return '<$0.01';
|
|
74
|
+
if (cost < 10) return `$${cost.toFixed(2)}`;
|
|
75
|
+
if (cost < 100) return `$${cost.toFixed(1)}`;
|
|
76
|
+
if (cost < 1000) return `$${Math.round(cost)}`;
|
|
77
|
+
return `$${(cost / 1000).toFixed(1)}k`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function formatTokensCompact(val: number): string {
|
|
81
|
+
if (val >= 1_000_000) return `${(val / 1_000_000).toFixed(1)}M`;
|
|
82
|
+
if (val >= 1_000) return `${(val / 1_000).toFixed(1)}K`;
|
|
83
|
+
return `${val}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function formatCacheBadge(val: number): string {
|
|
87
|
+
if (val >= 10_000_000) return `${Math.round(val / 1_000_000)}M`;
|
|
88
|
+
if (val >= 1_000_000) return `${(val / 1_000_000).toFixed(1)}M`;
|
|
89
|
+
if (val >= 100_000) return `${Math.round(val / 1_000)}K`;
|
|
90
|
+
if (val >= 1_000) return `${(val / 1_000).toFixed(1)}K`;
|
|
91
|
+
return `${val}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function getInspectorData(session: AgentSessionAggregate) {
|
|
95
|
+
const cacheRead = session.totals.cacheRead ?? 0;
|
|
96
|
+
const cacheWrite = session.totals.cacheWrite ?? 0;
|
|
97
|
+
if (cacheRead === 0 && cacheWrite === 0) return null;
|
|
98
|
+
|
|
99
|
+
let readCost = 0, writeCost = 0;
|
|
100
|
+
for (const stream of session.streams) {
|
|
101
|
+
if (!stream.costBreakdown) continue;
|
|
102
|
+
readCost += stream.costBreakdown.cacheRead ?? 0;
|
|
103
|
+
writeCost += stream.costBreakdown.cacheWrite ?? 0;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const costPerReq = session.requestCount > 0
|
|
107
|
+
? (session.totalCostUsd ?? 0) / session.requestCount
|
|
108
|
+
: 0;
|
|
109
|
+
|
|
110
|
+
return { cacheRead, cacheWrite, readCost, writeCost, costPerReq };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
interface SessionRowProps {
|
|
114
|
+
session: AgentSessionAggregate;
|
|
115
|
+
isSelected: boolean;
|
|
116
|
+
isWide: boolean;
|
|
117
|
+
nameMaxWidth: number;
|
|
118
|
+
getProviderColor: (id: string) => string;
|
|
119
|
+
isExiting?: boolean;
|
|
120
|
+
exitIntensity?: number;
|
|
121
|
+
skipEntrance?: boolean;
|
|
122
|
+
terminalWidth: number;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function getActivityFadeColor(lastActivityAt: number, baseColor: string, dimColor: string): string {
|
|
126
|
+
const secSinceActivity = (Date.now() - lastActivityAt) / 1000;
|
|
127
|
+
if (secSinceActivity < 5) return baseColor;
|
|
128
|
+
if (secSinceActivity > 60) return dimColor;
|
|
129
|
+
const t = (secSinceActivity - 5) / 55;
|
|
130
|
+
return interpolateColor(t, baseColor, dimColor);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const SessionRow = memo(function SessionRow({ session, isSelected, isWide, nameMaxWidth, getProviderColor, isExiting, exitIntensity, skipEntrance, terminalWidth }: SessionRowProps) {
|
|
134
|
+
const colors = useColors();
|
|
135
|
+
const isActive = session.status === 'active';
|
|
136
|
+
const entranceIntensity = useEntranceAnimation({ durationMs: 500 });
|
|
137
|
+
const skipRef = useRef(skipEntrance);
|
|
138
|
+
const currentIntensity = isExiting ? (exitIntensity ?? 0) : (skipRef.current ? 1 : entranceIntensity);
|
|
139
|
+
|
|
140
|
+
const effectiveTokens = session.totals.input + session.totals.output;
|
|
141
|
+
const cacheRead = session.totals.cacheRead ?? 0;
|
|
142
|
+
const hasCacheData = cacheRead > 0;
|
|
143
|
+
const costUsd = session.totalCostUsd ?? 0;
|
|
144
|
+
|
|
145
|
+
const animatedTokens = useAnimatedValue(effectiveTokens, { durationMs: 300, precision: 0 });
|
|
146
|
+
const animatedCost = useAnimatedValue(costUsd, { durationMs: 300, precision: 4 });
|
|
147
|
+
|
|
148
|
+
const { intensity: tokenFlash } = useValueFlash(effectiveTokens, { durationMs: 400, threshold: 10 });
|
|
149
|
+
const { intensity: costFlash } = useValueFlash(costUsd, { durationMs: 400, threshold: 0.001 });
|
|
150
|
+
|
|
151
|
+
const primaryStream = session.streams[0];
|
|
152
|
+
const providerId = primaryStream?.providerId ?? 'unknown';
|
|
153
|
+
const modelId = primaryStream?.modelId ?? 'unknown';
|
|
154
|
+
const baseProviderColor = getProviderColor(providerId);
|
|
155
|
+
const repoName = extractRepoName(session.projectPath ?? '—');
|
|
156
|
+
// In wide mode, project is fixed width (14), so truncate to 13 chars + ellipsis if needed
|
|
157
|
+
// In narrow mode, project is flex, so use nameMaxWidth (which is actually projectMaxWidth in narrow mode context)
|
|
158
|
+
const projectDisplay = isWide
|
|
159
|
+
? (repoName.length > 13 ? repoName.slice(0, 12) + '…' : repoName.padEnd(13))
|
|
160
|
+
: (repoName.length > nameMaxWidth ? repoName.slice(0, nameMaxWidth - 1) + '…' : repoName);
|
|
161
|
+
|
|
162
|
+
const dimColor = colors.background;
|
|
163
|
+
const fade = (color: string) => applyEntranceFade(currentIntensity, color, dimColor);
|
|
164
|
+
|
|
165
|
+
const baseStatusColor = isActive
|
|
166
|
+
? getActivityFadeColor(session.lastActivityAt, colors.success, colors.textMuted)
|
|
167
|
+
: colors.textMuted;
|
|
168
|
+
const statusColor = fade(baseStatusColor);
|
|
169
|
+
|
|
170
|
+
const baseTokenColor = colors.text;
|
|
171
|
+
const tokenColorBeforeFade = tokenFlash > 0
|
|
172
|
+
? interpolateColor(tokenFlash, baseTokenColor, '#ffffff')
|
|
173
|
+
: baseTokenColor;
|
|
174
|
+
const tokenColor = fade(tokenColorBeforeFade);
|
|
175
|
+
|
|
176
|
+
const providerColor = fade(baseProviderColor);
|
|
177
|
+
const textColor = fade(colors.text);
|
|
178
|
+
const textSubtleColor = fade(colors.textSubtle);
|
|
179
|
+
const textMutedColor = fade(colors.textMuted);
|
|
180
|
+
const baseCostColor = colors.warning;
|
|
181
|
+
const costColorBeforeFade = costFlash > 0
|
|
182
|
+
? interpolateColor(costFlash, baseCostColor, '#ffffff')
|
|
183
|
+
: baseCostColor;
|
|
184
|
+
const costColor = fade(costColorBeforeFade);
|
|
185
|
+
|
|
186
|
+
const formatTokensVal = (val: number): string => {
|
|
187
|
+
if (val >= 1000000) return `${(val / 1000000).toFixed(1)}M`;
|
|
188
|
+
if (val >= 1000) return `${(val / 1000).toFixed(1)}K`;
|
|
189
|
+
return `${val}`;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const railChar = isSelected ? '▌' : ' ';
|
|
193
|
+
const railColor = fade(colors.primary);
|
|
194
|
+
const rowBg = isSelected ? colors.borderMuted : undefined;
|
|
195
|
+
|
|
196
|
+
const lastActivity = formatRelativeTime(session.lastActivityAt);
|
|
197
|
+
const duration = formatDuration(session.startedAt, session.endedAt);
|
|
198
|
+
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
|
+
|
|
245
|
+
return (
|
|
246
|
+
<box flexDirection="row" paddingRight={1} height={1} {...(rowBg ? { backgroundColor: rowBg } : {})}>
|
|
247
|
+
<text width={2} height={1} fg={railColor} {...bgProp}>{railChar}</text>
|
|
248
|
+
<text width={9} height={1} fg={isSelected ? textColor : textSubtleColor} {...bgProp}>{session.agentName.length > 8 ? session.agentName.slice(0, 8) + '…' : session.agentName.padEnd(8)}</text>
|
|
249
|
+
<text width={narrowModelW} height={1} fg={providerColor} {...bgProp}>{modelWithCount.length > narrowModelMaxChars ? modelWithCount.slice(0, narrowModelMaxChars - 1) + '…' : modelWithCount.padEnd(narrowModelMaxChars)}</text>
|
|
250
|
+
<text width={narrowTokenW} height={1} {...bgProp}>
|
|
251
|
+
<span fg={tokenColor}>{formatTokensVal(animatedTokens).padStart(7)}</span>
|
|
252
|
+
<span fg={colors.textMuted}>{hasCacheData && showCacheBadge ? ` ↯${formatCacheBadge(cacheRead)}` : ''}</span>
|
|
253
|
+
</text>
|
|
254
|
+
<text width={7} height={1} fg={costColor} {...bgProp}>{formatCostVal(costUsd).padStart(6)}</text>
|
|
255
|
+
<text flexGrow={1} height={1} fg={textSubtleColor} {...bgProp}>{projectDisplay}</text>
|
|
256
|
+
<text width={5} height={1} fg={lastColor} {...bgProp}>{lastActivity.padStart(4)}</text>
|
|
257
|
+
<text width={2} height={1} fg={statusColor} {...bgProp}>{isActive ? '●' : '○'}</text>
|
|
258
|
+
</box>
|
|
259
|
+
);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
const WIDE_THRESHOLD = 140;
|
|
263
|
+
|
|
264
|
+
export const SessionsTable = forwardRef(function SessionsTable(
|
|
265
|
+
{
|
|
266
|
+
sessions,
|
|
267
|
+
selectedRow,
|
|
268
|
+
isLoading,
|
|
269
|
+
isFiltering,
|
|
270
|
+
filterQuery,
|
|
271
|
+
focusedPanel,
|
|
272
|
+
windowLabel,
|
|
273
|
+
getProviderColor,
|
|
274
|
+
}: SessionsTableProps,
|
|
275
|
+
ref: Ref<ScrollBoxRenderable>
|
|
276
|
+
) {
|
|
277
|
+
const colors = useColors();
|
|
278
|
+
const { width: terminalWidth, height: terminalHeight } = useTerminalDimensions();
|
|
279
|
+
const isWide = terminalWidth >= WIDE_THRESHOLD;
|
|
280
|
+
|
|
281
|
+
// Compute max name column width dynamically based on terminal width.
|
|
282
|
+
// Sidebar width depends on terminal width (see SmartSidebar.getSidebarWidth).
|
|
283
|
+
// Table inner width = terminalWidth - sidebarWidth - borders(4) - outerMargins(2).
|
|
284
|
+
// Name column = tableInnerWidth - fixedColumnsTotal.
|
|
285
|
+
const sidebarW = terminalWidth >= 160 ? 44 : terminalWidth >= 140 ? 40 : terminalWidth >= 105 ? 36 : terminalWidth >= 95 ? 32 : terminalWidth >= 85 ? 28 : 0;
|
|
286
|
+
const WIDE_FIXED = 106;
|
|
287
|
+
const hasSidebar = sidebarW > 0;
|
|
288
|
+
const isTight = !isWide && hasSidebar && terminalWidth < 105;
|
|
289
|
+
const narrowFixed = isTight ? 49 : 57;
|
|
290
|
+
const tableInner = terminalWidth - sidebarW - 6;
|
|
291
|
+
const nameMaxWidth = isWide ? Math.max(8, tableInner - WIDE_FIXED) : Math.max(1, tableInner - narrowFixed);
|
|
292
|
+
|
|
293
|
+
const getSessionKey = useCallback((s: AgentSessionAggregate) => s.sessionId, []);
|
|
294
|
+
const { items: animatedSessions, isBulkChange } = useExitAnimation(sessions, {
|
|
295
|
+
durationMs: 500,
|
|
296
|
+
getKey: getSessionKey,
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
let activeIndex = 0;
|
|
300
|
+
const selectedSession = sessions[selectedRow] ?? null;
|
|
301
|
+
const inspector = selectedSession ? getInspectorData(selectedSession) : null;
|
|
302
|
+
|
|
303
|
+
return (
|
|
304
|
+
<box
|
|
305
|
+
flexDirection="column"
|
|
306
|
+
flexGrow={2}
|
|
307
|
+
border
|
|
308
|
+
borderStyle={focusedPanel === 'sessions' ? 'double' : 'single'}
|
|
309
|
+
borderColor={focusedPanel === 'sessions' ? colors.primary : colors.border}
|
|
310
|
+
overflow="hidden"
|
|
311
|
+
>
|
|
312
|
+
<box flexDirection="row" paddingLeft={2} paddingRight={1} height={1} justifyContent="space-between" overflow="hidden">
|
|
313
|
+
<text height={1} fg={colors.textMuted}>
|
|
314
|
+
SESSIONS{filterQuery ? ` [${isFiltering ? 'Filter: ' : ''}${filterQuery}]` : ''}{isLoading ? ' ⟳' : ' '}
|
|
315
|
+
</text>
|
|
316
|
+
<text height={1} fg={colors.textMuted}>[{windowLabel}] {sessions.length} sessions</text>
|
|
317
|
+
</box>
|
|
318
|
+
|
|
319
|
+
{isWide ? (
|
|
320
|
+
<box flexDirection="row" paddingRight={1} height={1} gap={1}>
|
|
321
|
+
<text width={2} height={1} fg={colors.textMuted}> </text>
|
|
322
|
+
<text width={8} height={1} fg={colors.textMuted}>ID </text>
|
|
323
|
+
<text width={12} height={1} fg={colors.textMuted}>AGENT </text>
|
|
324
|
+
<text width={18} height={1} fg={colors.textMuted}>MODEL </text>
|
|
325
|
+
<text width={5} height={1} fg={colors.textMuted}> REQ </text>
|
|
326
|
+
<text width={14} height={1} fg={colors.textMuted}> TOKENS </text>
|
|
327
|
+
<text width={8} height={1} fg={colors.textMuted}> COST </text>
|
|
328
|
+
<text width={14} height={1} fg={colors.textMuted}>PROJECT </text>
|
|
329
|
+
<text flexGrow={1} height={1} fg={colors.textMuted}>NAME</text>
|
|
330
|
+
<text width={6} height={1} fg={colors.textMuted}> DUR </text>
|
|
331
|
+
<text width={5} height={1} fg={colors.textMuted}>LAST </text>
|
|
332
|
+
<text width={2} height={1} fg={colors.textMuted}> </text>
|
|
333
|
+
</box>
|
|
334
|
+
) : (
|
|
335
|
+
<box flexDirection="row" paddingRight={1} height={1}>
|
|
336
|
+
<text width={2} height={1} fg={colors.textMuted}> </text>
|
|
337
|
+
<text width={9} height={1} fg={colors.textMuted}>AGENT </text>
|
|
338
|
+
<text width={isTight ? 13 : 16} height={1} fg={colors.textMuted}>{isTight ? 'MODEL ' : 'MODEL '}</text>
|
|
339
|
+
<text width={isTight ? 9 : 14} height={1} fg={colors.textMuted}>{isTight ? ' TOKENS ' : ' TOKENS '}</text>
|
|
340
|
+
<text width={7} height={1} fg={colors.textMuted}> COST </text>
|
|
341
|
+
<text flexGrow={1} height={1} fg={colors.textMuted}>{nameMaxWidth >= 7 ? 'PROJECT' : nameMaxWidth >= 4 ? 'PROJ' : ''}</text>
|
|
342
|
+
<text width={5} height={1} fg={colors.textMuted}>LAST </text>
|
|
343
|
+
<text width={2} height={1} fg={colors.textMuted}> </text>
|
|
344
|
+
</box>
|
|
345
|
+
)}
|
|
346
|
+
|
|
347
|
+
<scrollbox ref={ref} flexGrow={1}>
|
|
348
|
+
<box flexDirection="column">
|
|
349
|
+
{sessions.length === 0 && animatedSessions.length === 0 && (
|
|
350
|
+
<box paddingLeft={2}>
|
|
351
|
+
<text fg={colors.textMuted}>{isLoading ? 'Loading sessions...' : 'No sessions found'}</text>
|
|
352
|
+
</box>
|
|
353
|
+
)}
|
|
354
|
+
{animatedSessions.map((entry) => {
|
|
355
|
+
const isSelectedRow = !entry.isExiting && activeIndex++ === selectedRow;
|
|
356
|
+
return (
|
|
357
|
+
<SessionRow
|
|
358
|
+
key={entry.item.sessionId}
|
|
359
|
+
session={entry.item}
|
|
360
|
+
isSelected={isSelectedRow}
|
|
361
|
+
isWide={isWide}
|
|
362
|
+
nameMaxWidth={nameMaxWidth}
|
|
363
|
+
getProviderColor={getProviderColor}
|
|
364
|
+
isExiting={entry.isExiting}
|
|
365
|
+
exitIntensity={entry.exitIntensity}
|
|
366
|
+
skipEntrance={isBulkChange}
|
|
367
|
+
terminalWidth={terminalWidth}
|
|
368
|
+
/>
|
|
369
|
+
);
|
|
370
|
+
})}
|
|
371
|
+
</box>
|
|
372
|
+
</scrollbox>
|
|
373
|
+
{terminalHeight >= 30 && (
|
|
374
|
+
<box height={1} paddingX={2} flexShrink={0}>
|
|
375
|
+
{inspector && focusedPanel === 'sessions' ? (
|
|
376
|
+
<text height={1} overflow="hidden">
|
|
377
|
+
<span fg={colors.textMuted}>↯ Cache read </span>
|
|
378
|
+
<span fg={colors.text}>{formatTokensCompact(inspector.cacheRead)}</span>
|
|
379
|
+
<span fg={colors.textMuted}> (</span>
|
|
380
|
+
<span fg={colors.warning}>{formatCostVal(inspector.readCost)}</span>
|
|
381
|
+
<span fg={colors.textMuted}>)</span>
|
|
382
|
+
{inspector.cacheWrite > 0 && (
|
|
383
|
+
<>
|
|
384
|
+
<span fg={colors.textMuted}> • Write </span>
|
|
385
|
+
<span fg={colors.text}>{formatTokensCompact(inspector.cacheWrite)}</span>
|
|
386
|
+
<span fg={colors.textMuted}> (</span>
|
|
387
|
+
<span fg={colors.warning}>{formatCostVal(inspector.writeCost)}</span>
|
|
388
|
+
<span fg={colors.textMuted}>)</span>
|
|
389
|
+
</>
|
|
390
|
+
)}
|
|
391
|
+
<span fg={colors.textMuted}> • $/req </span>
|
|
392
|
+
<span fg={colors.warning}>{formatCostVal(inspector.costPerReq)}</span>
|
|
393
|
+
</text>
|
|
394
|
+
) : (
|
|
395
|
+
<text height={1}> </text>
|
|
396
|
+
)}
|
|
397
|
+
</box>
|
|
398
|
+
)}
|
|
399
|
+
</box>
|
|
400
|
+
);
|
|
401
|
+
});
|