@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,317 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { useTerminalDimensions } from '@opentui/react';
|
|
3
|
+
import { RGBA } from '@opentui/core';
|
|
4
|
+
import type { AgentSessionAggregate } from '../../agents/types.ts';
|
|
5
|
+
import { useColors } from '../contexts/ThemeContext.tsx';
|
|
6
|
+
import { getSessionActivityTimeline, isDatabaseInitialized } from '@/storage/index.ts';
|
|
7
|
+
|
|
8
|
+
const OVERLAY_BG = RGBA.fromValues(0.0, 0.0, 0.0, 0.5);
|
|
9
|
+
|
|
10
|
+
interface SessionDetailsDrawerProps {
|
|
11
|
+
session: AgentSessionAggregate;
|
|
12
|
+
onClose: () => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function formatTokens(count: number): string {
|
|
16
|
+
if (count >= 1_000_000) return (count / 1_000_000).toFixed(1) + 'M';
|
|
17
|
+
if (count >= 1_000) return (count / 1_000).toFixed(1) + 'K';
|
|
18
|
+
return count.toString();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function formatCost(cost: number | undefined): string {
|
|
22
|
+
if (cost === undefined) return '$0.00';
|
|
23
|
+
if (cost < 0.01 && cost > 0) return '<$0.01';
|
|
24
|
+
return '$' + cost.toFixed(2);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function formatDuration(startedAt: number, lastActivityAt: number): string {
|
|
28
|
+
const ms = lastActivityAt - startedAt;
|
|
29
|
+
const seconds = Math.floor(ms / 1000);
|
|
30
|
+
const minutes = Math.floor(seconds / 60);
|
|
31
|
+
const hours = Math.floor(minutes / 60);
|
|
32
|
+
const days = Math.floor(hours / 24);
|
|
33
|
+
|
|
34
|
+
if (days > 0) return `${days}d ${hours % 24}h`;
|
|
35
|
+
if (hours > 0) return `${hours}h ${minutes % 60}m`;
|
|
36
|
+
if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
|
|
37
|
+
return `${seconds}s`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function formatTimelineLabel(timestamp: number, isMultiDay: boolean): string {
|
|
41
|
+
const date = new Date(timestamp);
|
|
42
|
+
if (isMultiDay) {
|
|
43
|
+
return date.toLocaleDateString([], { month: 'short', day: 'numeric' });
|
|
44
|
+
}
|
|
45
|
+
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const SPARK_CHARS = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
|
|
49
|
+
|
|
50
|
+
function buildSparkline(values: number[], width: number): string {
|
|
51
|
+
if (values.length === 0 || width <= 0) return '·'.repeat(Math.max(width, 1));
|
|
52
|
+
|
|
53
|
+
const bucketSize = Math.ceil(values.length / width);
|
|
54
|
+
const buckets: number[] = [];
|
|
55
|
+
|
|
56
|
+
for (let i = 0; i < width; i++) {
|
|
57
|
+
const start = i * bucketSize;
|
|
58
|
+
const end = Math.min(start + bucketSize, values.length);
|
|
59
|
+
const slice = values.slice(start, end);
|
|
60
|
+
buckets.push(slice.length > 0 ? slice.reduce((a, b) => a + b, 0) : 0);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const maxVal = Math.max(...buckets, 1);
|
|
64
|
+
|
|
65
|
+
return buckets.map(v => {
|
|
66
|
+
if (v === 0) return '·';
|
|
67
|
+
const idx = Math.min(Math.floor((v / maxVal) * SPARK_CHARS.length), SPARK_CHARS.length - 1);
|
|
68
|
+
return SPARK_CHARS[idx];
|
|
69
|
+
}).join('');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function truncateMiddle(str: string, maxLength: number): string {
|
|
73
|
+
if (str.length <= maxLength) return str;
|
|
74
|
+
const side = Math.floor((maxLength - 1) / 2);
|
|
75
|
+
return str.slice(0, side) + '…' + str.slice(-side);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function shortenPath(fullPath: string, maxLength: number): string {
|
|
79
|
+
if (!fullPath) return '—';
|
|
80
|
+
|
|
81
|
+
const normalized = fullPath.replace(/\\/g, '/');
|
|
82
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? '';
|
|
83
|
+
let path = normalized;
|
|
84
|
+
|
|
85
|
+
if (home && normalized.startsWith(home)) {
|
|
86
|
+
path = '~' + normalized.slice(home.length);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (path.length <= maxLength) return path;
|
|
90
|
+
|
|
91
|
+
const parts = path.split('/').filter(Boolean);
|
|
92
|
+
if (parts.length <= 2) return truncateMiddle(path, maxLength);
|
|
93
|
+
|
|
94
|
+
const first = parts[0];
|
|
95
|
+
const last = parts[parts.length - 1];
|
|
96
|
+
const secondLast = parts.length > 2 ? parts[parts.length - 2] : '';
|
|
97
|
+
|
|
98
|
+
const shortened = secondLast
|
|
99
|
+
? `${first}/…/${secondLast}/${last}`
|
|
100
|
+
: `${first}/…/${last}`;
|
|
101
|
+
|
|
102
|
+
return shortened.length <= maxLength ? shortened : truncateMiddle(path, maxLength);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function SessionDetailsDrawer({ session, onClose: _onClose }: SessionDetailsDrawerProps) {
|
|
106
|
+
void _onClose;
|
|
107
|
+
const colors = useColors();
|
|
108
|
+
const { width: termWidth, height: termHeight } = useTerminalDimensions();
|
|
109
|
+
|
|
110
|
+
const width = Math.max(70, Math.min(termWidth - 4, 100));
|
|
111
|
+
const height = Math.max(24, Math.min(termHeight - 4, 40));
|
|
112
|
+
const contentWidth = width - 4;
|
|
113
|
+
|
|
114
|
+
const duration = formatDuration(session.startedAt, session.lastActivityAt);
|
|
115
|
+
const projectName = session.projectPath?.split('/').pop() ?? 'N/A';
|
|
116
|
+
const fullPath = session.projectPath ?? '';
|
|
117
|
+
|
|
118
|
+
const sparklineInnerWidth = Math.max(contentWidth - 6, 20);
|
|
119
|
+
const sparkline = useMemo(() => {
|
|
120
|
+
if (!isDatabaseInitialized()) return '·'.repeat(sparklineInnerWidth);
|
|
121
|
+
const activity = getSessionActivityTimeline(session.sessionId);
|
|
122
|
+
if (activity.length === 0) return '·'.repeat(sparklineInnerWidth);
|
|
123
|
+
return buildSparkline(activity.map(a => a.tokens), sparklineInnerWidth);
|
|
124
|
+
}, [session.sessionId, sparklineInnerWidth]);
|
|
125
|
+
|
|
126
|
+
const cacheRead = session.totals.cacheRead ?? 0;
|
|
127
|
+
const cacheWrite = session.totals.cacheWrite ?? 0;
|
|
128
|
+
const hasCacheData = cacheRead > 0 || cacheWrite > 0;
|
|
129
|
+
const totalInput = session.totals.input + cacheRead + cacheWrite;
|
|
130
|
+
const cacheHitRate = totalInput > 0 ? Math.round((cacheRead / totalInput) * 100) : 0;
|
|
131
|
+
|
|
132
|
+
const sessionCostBreakdown = useMemo(() => {
|
|
133
|
+
let input = 0, output = 0, cr = 0, cw = 0;
|
|
134
|
+
let hasAny = false;
|
|
135
|
+
for (const stream of session.streams) {
|
|
136
|
+
if (!stream.costBreakdown) continue;
|
|
137
|
+
hasAny = true;
|
|
138
|
+
input += stream.costBreakdown.input;
|
|
139
|
+
output += stream.costBreakdown.output;
|
|
140
|
+
cr += stream.costBreakdown.cacheRead ?? 0;
|
|
141
|
+
cw += stream.costBreakdown.cacheWrite ?? 0;
|
|
142
|
+
}
|
|
143
|
+
if (!hasAny) return null;
|
|
144
|
+
return {
|
|
145
|
+
input,
|
|
146
|
+
output,
|
|
147
|
+
cacheRead: cr > 0 ? cr : undefined,
|
|
148
|
+
cacheWrite: cw > 0 ? cw : undefined,
|
|
149
|
+
};
|
|
150
|
+
}, [session.streams]);
|
|
151
|
+
|
|
152
|
+
const isMultiDay = (session.lastActivityAt - session.startedAt) > 24 * 60 * 60 * 1000;
|
|
153
|
+
const startTimeStr = formatTimelineLabel(session.startedAt, isMultiDay);
|
|
154
|
+
const endTimeStr = formatTimelineLabel(session.lastActivityAt, isMultiDay);
|
|
155
|
+
const timeGap = Math.max(0, sparklineInnerWidth - startTimeStr.length - endTimeStr.length);
|
|
156
|
+
const timelineLabel = startTimeStr + ' '.repeat(timeGap) + endTimeStr;
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<box
|
|
160
|
+
position="absolute"
|
|
161
|
+
left={0}
|
|
162
|
+
top={0}
|
|
163
|
+
width="100%"
|
|
164
|
+
height="100%"
|
|
165
|
+
justifyContent="center"
|
|
166
|
+
alignItems="center"
|
|
167
|
+
zIndex={100}
|
|
168
|
+
backgroundColor={OVERLAY_BG}
|
|
169
|
+
>
|
|
170
|
+
<box
|
|
171
|
+
width={width}
|
|
172
|
+
height={height}
|
|
173
|
+
border
|
|
174
|
+
borderStyle="double"
|
|
175
|
+
borderColor={colors.primary}
|
|
176
|
+
flexDirection="column"
|
|
177
|
+
padding={1}
|
|
178
|
+
backgroundColor={colors.background}
|
|
179
|
+
overflow="hidden"
|
|
180
|
+
>
|
|
181
|
+
<box flexDirection="row" justifyContent="space-between" height={1} marginBottom={1}>
|
|
182
|
+
<text height={1} fg={colors.primary}><strong>SESSION DETAILS</strong></text>
|
|
183
|
+
<text height={1} fg={colors.textMuted}>[Esc] Close</text>
|
|
184
|
+
</box>
|
|
185
|
+
|
|
186
|
+
<box flexDirection="column" marginBottom={0} border borderStyle="single" borderColor={colors.border} paddingX={1} paddingY={0} flexShrink={0}>
|
|
187
|
+
<box flexDirection="row" height={1} justifyContent="space-between" marginTop={0}>
|
|
188
|
+
<text height={1} overflow="hidden">
|
|
189
|
+
<span fg={colors.textMuted}>ID: </span>
|
|
190
|
+
<span fg={colors.text}><strong>{session.sessionId}</strong></span>
|
|
191
|
+
</text>
|
|
192
|
+
<text height={1}>
|
|
193
|
+
<span fg={colors.textMuted}>Status: </span>
|
|
194
|
+
<span fg={session.status === 'active' ? colors.success : colors.textMuted}>
|
|
195
|
+
{session.status.toUpperCase()}
|
|
196
|
+
</span>
|
|
197
|
+
</text>
|
|
198
|
+
</box>
|
|
199
|
+
|
|
200
|
+
{session.sessionName && (
|
|
201
|
+
<box flexDirection="row" height={1}>
|
|
202
|
+
<text height={1} overflow="hidden">
|
|
203
|
+
<span fg={colors.textMuted}>Name: </span>
|
|
204
|
+
<span fg={colors.primary}><strong>{session.sessionName}</strong></span>
|
|
205
|
+
</text>
|
|
206
|
+
</box>
|
|
207
|
+
)}
|
|
208
|
+
|
|
209
|
+
<box flexDirection="row" height={1} justifyContent="space-between">
|
|
210
|
+
<text height={1} overflow="hidden">
|
|
211
|
+
<span fg={colors.textMuted}>Agent: </span>
|
|
212
|
+
<span fg={colors.text}>{session.agentName}</span>
|
|
213
|
+
</text>
|
|
214
|
+
<text height={1}>
|
|
215
|
+
<span fg={colors.textMuted}>Project: </span>
|
|
216
|
+
<span fg={colors.text}><strong>{projectName}</strong></span>
|
|
217
|
+
</text>
|
|
218
|
+
</box>
|
|
219
|
+
|
|
220
|
+
<box flexDirection="row" height={1} marginBottom={0}>
|
|
221
|
+
<text height={1} overflow="hidden" fg={colors.textMuted}>
|
|
222
|
+
{shortenPath(fullPath, contentWidth - 4)}
|
|
223
|
+
</text>
|
|
224
|
+
</box>
|
|
225
|
+
</box>
|
|
226
|
+
|
|
227
|
+
<box flexDirection="column" marginBottom={0} border borderStyle="single" borderColor={colors.border} paddingX={1} paddingY={0} flexShrink={0}>
|
|
228
|
+
<box flexDirection="row" height={1} justifyContent="space-between" marginTop={0}>
|
|
229
|
+
<text height={1}>
|
|
230
|
+
<span fg={colors.textMuted}>Cost: </span>
|
|
231
|
+
<span fg={colors.success}><strong>{formatCost(session.totalCostUsd)}</strong></span>
|
|
232
|
+
</text>
|
|
233
|
+
<text height={1}>
|
|
234
|
+
<span fg={colors.textMuted}>Duration: </span>
|
|
235
|
+
<span fg={colors.text}>{duration}</span>
|
|
236
|
+
</text>
|
|
237
|
+
</box>
|
|
238
|
+
|
|
239
|
+
{sessionCostBreakdown && (
|
|
240
|
+
<box flexDirection="row" height={1} marginBottom={0}>
|
|
241
|
+
<text height={1} overflow="hidden">
|
|
242
|
+
<span fg={colors.textMuted}> </span>
|
|
243
|
+
<span fg={colors.text}>In {formatCost(sessionCostBreakdown.input)}</span>
|
|
244
|
+
<span fg={colors.textMuted}> │ </span>
|
|
245
|
+
<span fg={colors.text}>Out {formatCost(sessionCostBreakdown.output)}</span>
|
|
246
|
+
{(sessionCostBreakdown.cacheRead ?? 0) > 0 && (
|
|
247
|
+
<>
|
|
248
|
+
<span fg={colors.textMuted}> │ </span>
|
|
249
|
+
<span fg={colors.text}>Read {formatCost(sessionCostBreakdown.cacheRead)}</span>
|
|
250
|
+
</>
|
|
251
|
+
)}
|
|
252
|
+
{(sessionCostBreakdown.cacheWrite ?? 0) > 0 && (
|
|
253
|
+
<>
|
|
254
|
+
<span fg={colors.textMuted}> │ </span>
|
|
255
|
+
<span fg={colors.text}>Write {formatCost(sessionCostBreakdown.cacheWrite)}</span>
|
|
256
|
+
</>
|
|
257
|
+
)}
|
|
258
|
+
</text>
|
|
259
|
+
</box>
|
|
260
|
+
)}
|
|
261
|
+
|
|
262
|
+
<box flexDirection="row" height={1} marginTop={0}>
|
|
263
|
+
<text height={1}>
|
|
264
|
+
<span fg={colors.textMuted}>Tokens: </span>
|
|
265
|
+
<span fg={colors.text}>
|
|
266
|
+
{formatTokens(session.totals.input + session.totals.output)}
|
|
267
|
+
</span>
|
|
268
|
+
<span fg={colors.textMuted}> (In: {formatTokens(session.totals.input)} / Out: {formatTokens(session.totals.output)})</span>
|
|
269
|
+
</text>
|
|
270
|
+
</box>
|
|
271
|
+
|
|
272
|
+
{hasCacheData && (
|
|
273
|
+
<box flexDirection="row" height={1} marginBottom={0}>
|
|
274
|
+
<text height={1} overflow="hidden">
|
|
275
|
+
<span fg={colors.textMuted}>Cache: </span>
|
|
276
|
+
<span fg={cacheHitRate >= 50 ? colors.success : colors.warning}>{cacheHitRate}% hit</span>
|
|
277
|
+
<span fg={colors.textMuted}> — {formatTokens(cacheRead)} read / {formatTokens(cacheWrite)} write</span>
|
|
278
|
+
</text>
|
|
279
|
+
</box>
|
|
280
|
+
)}
|
|
281
|
+
</box>
|
|
282
|
+
|
|
283
|
+
<box flexDirection="column" height={4} marginBottom={0} border borderColor={colors.border} paddingX={1} flexShrink={0}>
|
|
284
|
+
<text height={1} fg={colors.primary} overflow="hidden">{sparkline}</text>
|
|
285
|
+
<text height={1} fg={colors.textMuted} overflow="hidden">{timelineLabel}</text>
|
|
286
|
+
</box>
|
|
287
|
+
|
|
288
|
+
<box flexDirection="row" height={1} paddingLeft={1} paddingRight={2} marginBottom={0}>
|
|
289
|
+
<text flexGrow={1} fg={colors.primary}><strong>MODEL</strong></text>
|
|
290
|
+
<box width={10} justifyContent="flex-end"><text fg={colors.primary}><strong>TOKENS</strong></text></box>
|
|
291
|
+
<box width={10} justifyContent="flex-end"><text fg={colors.primary}><strong>COST</strong></text></box>
|
|
292
|
+
</box>
|
|
293
|
+
|
|
294
|
+
<box flexDirection="column" flexGrow={1} border borderColor={colors.border} overflow="hidden">
|
|
295
|
+
<scrollbox flexGrow={1}>
|
|
296
|
+
{session.streams.map((stream, idx) => {
|
|
297
|
+
const modelName = stream.modelId.split('/').pop() ?? stream.modelId;
|
|
298
|
+
return (
|
|
299
|
+
<box key={`${stream.modelId}-${idx}`} flexDirection="row" height={1} paddingX={1}>
|
|
300
|
+
<text flexGrow={1} height={1} fg={colors.text} overflow="hidden">
|
|
301
|
+
{truncateMiddle(modelName, contentWidth - 25)}
|
|
302
|
+
</text>
|
|
303
|
+
<box width={10} height={1} justifyContent="flex-end">
|
|
304
|
+
<text fg={colors.textMuted}>{formatTokens(stream.tokens.input + stream.tokens.output)}</text>
|
|
305
|
+
</box>
|
|
306
|
+
<box width={10} height={1} justifyContent="flex-end">
|
|
307
|
+
<text fg={colors.success}>{formatCost(stream.costUsd)}</text>
|
|
308
|
+
</box>
|
|
309
|
+
</box>
|
|
310
|
+
);
|
|
311
|
+
})}
|
|
312
|
+
</scrollbox>
|
|
313
|
+
</box>
|
|
314
|
+
</box>
|
|
315
|
+
</box>
|
|
316
|
+
);
|
|
317
|
+
}
|