@tokentop/ttop 0.2.2 → 0.3.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.
Files changed (158) hide show
  1. package/README.md +2 -5
  2. package/package.json +8 -5
  3. package/src/agents/aggregator.ts +39 -19
  4. package/src/agents/costing.ts +11 -9
  5. package/src/agents/index.ts +7 -9
  6. package/src/agents/types.ts +2 -2
  7. package/src/cli.ts +28 -23
  8. package/src/config/schema.test.ts +42 -40
  9. package/src/config/schema.ts +26 -26
  10. package/src/demo/simulator.ts +161 -122
  11. package/src/plugins/agents/index.ts +2 -2
  12. package/src/plugins/auth-sources.ts +42 -38
  13. package/src/plugins/lifecycle.ts +32 -32
  14. package/src/plugins/loader.test.ts +49 -49
  15. package/src/plugins/loader.ts +67 -63
  16. package/src/plugins/notification-bus.ts +21 -16
  17. package/src/plugins/notifications/index.ts +2 -2
  18. package/src/plugins/notifications/terminal-bell.ts +29 -23
  19. package/src/plugins/notifications/visual-flash.ts +21 -19
  20. package/src/plugins/npm-installer.ts +23 -25
  21. package/src/plugins/plugin-context-factory.ts +10 -10
  22. package/src/plugins/plugin-host.ts +12 -5
  23. package/src/plugins/providers/anthropic.ts +64 -57
  24. package/src/plugins/providers/antigravity.ts +81 -69
  25. package/src/plugins/providers/codex.ts +42 -42
  26. package/src/plugins/providers/gemini.ts +106 -87
  27. package/src/plugins/providers/github-copilot.ts +49 -45
  28. package/src/plugins/providers/index.ts +10 -12
  29. package/src/plugins/providers/minimax.ts +50 -44
  30. package/src/plugins/providers/openai-api.ts +82 -62
  31. package/src/plugins/providers/opencode-zen.ts +46 -39
  32. package/src/plugins/providers/perplexity.ts +55 -46
  33. package/src/plugins/providers/zai.ts +66 -57
  34. package/src/plugins/registry.ts +47 -43
  35. package/src/plugins/sandbox-guard.test.ts +45 -48
  36. package/src/plugins/sandbox-guard.ts +20 -25
  37. package/src/plugins/sandbox.ts +26 -48
  38. package/src/plugins/themes/catppuccin-latte.ts +29 -29
  39. package/src/plugins/themes/catppuccin-mocha.ts +29 -29
  40. package/src/plugins/themes/claude-code.ts +29 -29
  41. package/src/plugins/themes/dracula.ts +29 -29
  42. package/src/plugins/themes/github-light.ts +29 -29
  43. package/src/plugins/themes/gruvbox-dark.ts +29 -29
  44. package/src/plugins/themes/gruvbox-light.ts +29 -29
  45. package/src/plugins/themes/index.ts +15 -15
  46. package/src/plugins/themes/kanagawa.ts +29 -29
  47. package/src/plugins/themes/nord.ts +29 -29
  48. package/src/plugins/themes/one-dark.ts +29 -29
  49. package/src/plugins/themes/opencode.ts +31 -31
  50. package/src/plugins/themes/rose-pine-dawn.ts +29 -29
  51. package/src/plugins/themes/rose-pine.ts +29 -29
  52. package/src/plugins/themes/solarized-light.ts +29 -29
  53. package/src/plugins/themes/tokyo-night.ts +29 -29
  54. package/src/plugins/types/agent.ts +5 -5
  55. package/src/plugins/types/base.ts +35 -24
  56. package/src/plugins/types/index.ts +9 -9
  57. package/src/plugins/types/notification.ts +12 -12
  58. package/src/plugins/types/provider.ts +12 -8
  59. package/src/plugins/types/theme.ts +31 -23
  60. package/src/plugins/update-checker.ts +6 -6
  61. package/src/pricing/estimator.test.ts +39 -44
  62. package/src/pricing/estimator.ts +7 -13
  63. package/src/pricing/fallback.test.ts +30 -34
  64. package/src/pricing/fallback.ts +54 -27
  65. package/src/pricing/index.ts +18 -12
  66. package/src/pricing/models-dev.ts +6 -6
  67. package/src/storage/database.ts +56 -46
  68. package/src/storage/db.ts +12 -12
  69. package/src/storage/index.ts +44 -45
  70. package/src/storage/migrations/index.ts +2 -2
  71. package/src/storage/paths.ts +13 -13
  72. package/src/storage/repos/agentSessions.ts +33 -23
  73. package/src/storage/repos/providerSnapshots.ts +5 -5
  74. package/src/storage/repos/usageEvents.ts +40 -28
  75. package/src/storage/types.ts +5 -5
  76. package/src/tui/App.tsx +180 -137
  77. package/src/tui/components/CommandPalette.tsx +46 -34
  78. package/src/tui/components/DebugConsole.tsx +39 -33
  79. package/src/tui/components/DebugPanel.tsx +295 -177
  80. package/src/tui/components/FooterHints.tsx +2 -2
  81. package/src/tui/components/GhostProviderCard.tsx +37 -35
  82. package/src/tui/components/Header.tsx +68 -35
  83. package/src/tui/components/HelpOverlay.tsx +99 -29
  84. package/src/tui/components/InlineGauge.tsx +10 -7
  85. package/src/tui/components/InlineSparkline.tsx +17 -11
  86. package/src/tui/components/KpiStrip.tsx +128 -98
  87. package/src/tui/components/LimitGauge.tsx +96 -59
  88. package/src/tui/components/ModalBackdrop.tsx +4 -4
  89. package/src/tui/components/ProviderAggregateStrip.tsx +69 -38
  90. package/src/tui/components/ProviderCard.tsx +232 -226
  91. package/src/tui/components/ProviderDetailPanel.tsx +133 -58
  92. package/src/tui/components/ProviderLimitsPanel.tsx +126 -89
  93. package/src/tui/components/ProvidersList.tsx +215 -126
  94. package/src/tui/components/SessionDetailsDrawer.tsx +191 -94
  95. package/src/tui/components/SessionsTable.tsx +313 -166
  96. package/src/tui/components/SettingsModal.tsx +231 -214
  97. package/src/tui/components/SidebarBreakdown.tsx +30 -16
  98. package/src/tui/components/Skeleton.tsx +24 -17
  99. package/src/tui/components/SmartSidebar.tsx +360 -258
  100. package/src/tui/components/Sparkline.tsx +64 -52
  101. package/src/tui/components/Spinner.tsx +3 -3
  102. package/src/tui/components/StatusBar.tsx +27 -27
  103. package/src/tui/components/ThemePicker.tsx +67 -66
  104. package/src/tui/components/Toast.tsx +9 -6
  105. package/src/tui/components/UsageGauge.tsx +10 -13
  106. package/src/tui/components/index.ts +5 -5
  107. package/src/tui/contexts/AgentSessionContext.tsx +193 -144
  108. package/src/tui/contexts/ConfigContext.tsx +15 -13
  109. package/src/tui/contexts/DashboardRuntimeContext.tsx +26 -26
  110. package/src/tui/contexts/DemoModeContext.tsx +22 -15
  111. package/src/tui/contexts/DrawerContext.tsx +11 -9
  112. package/src/tui/contexts/InputContext.tsx +2 -2
  113. package/src/tui/contexts/LogContext.tsx +62 -47
  114. package/src/tui/contexts/PluginContext.tsx +232 -183
  115. package/src/tui/contexts/RealTimeActivityContext.tsx +70 -36
  116. package/src/tui/contexts/StorageContext.tsx +151 -117
  117. package/src/tui/contexts/ThemeContext.tsx +21 -16
  118. package/src/tui/contexts/TimeWindowContext.tsx +72 -61
  119. package/src/tui/contexts/ToastContext.tsx +10 -7
  120. package/src/tui/contexts/index.ts +9 -5
  121. package/src/tui/createApp.tsx +8 -12
  122. package/src/tui/debug/captureFrame.ts +25 -28
  123. package/src/tui/debug/snapshot.tsx +179 -162
  124. package/src/tui/driver/assertions.ts +30 -22
  125. package/src/tui/driver/cli.ts +231 -221
  126. package/src/tui/driver/coverage.ts +27 -44
  127. package/src/tui/driver/demo-snapshot.ts +33 -33
  128. package/src/tui/driver/diff.ts +26 -32
  129. package/src/tui/driver/driver.ts +43 -48
  130. package/src/tui/driver/index.ts +36 -40
  131. package/src/tui/driver/recorder.ts +37 -48
  132. package/src/tui/driver/test-workflow.ts +51 -51
  133. package/src/tui/driver/test.ts +25 -25
  134. package/src/tui/hooks/index.ts +24 -20
  135. package/src/tui/hooks/useAnimatedValue.ts +13 -18
  136. package/src/tui/hooks/useDashboardKeyboard.ts +118 -98
  137. package/src/tui/hooks/useDashboardState.ts +26 -24
  138. package/src/tui/hooks/useEmaActivity.ts +82 -74
  139. package/src/tui/hooks/useEntranceAnimation.ts +11 -11
  140. package/src/tui/hooks/useExitAnimation.ts +3 -3
  141. package/src/tui/hooks/usePulse.ts +4 -4
  142. package/src/tui/hooks/useSafeRenderer.ts +5 -5
  143. package/src/tui/hooks/useSessionEnrichedProviders.ts +150 -0
  144. package/src/tui/hooks/useValueFlash.ts +10 -14
  145. package/src/tui/index.tsx +29 -6
  146. package/src/tui/shutdown.ts +19 -0
  147. package/src/tui/utils/providerColor.ts +1 -1
  148. package/src/tui/views/Dashboard.tsx +127 -102
  149. package/src/tui/views/HistoricalTrendsView.tsx +579 -250
  150. package/src/tui/views/ProjectsView.tsx +466 -237
  151. package/src/tui/views/RealTimeDashboard.tsx +150 -111
  152. package/src/tui/views/SettingsView.tsx +222 -191
  153. package/src/tui/views/index.ts +2 -2
  154. package/src/types/opentui-test-utils.d.ts +7 -7
  155. package/src/utils/clipboard.ts +29 -29
  156. package/src/utils/currency.test.ts +53 -53
  157. package/src/utils/currency.ts +35 -32
  158. package/src/version.ts +1 -1
@@ -1,9 +1,11 @@
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';
1
+ import { RGBA } from "@opentui/core";
2
+ import { useTerminalDimensions } from "@opentui/react";
3
+ import { useMemo } from "react";
4
+ import { getSessionActivityTimeline, isDatabaseInitialized } from "@/storage/index.ts";
5
+ import type { AgentSessionAggregate } from "../../agents/types.ts";
6
+ import { usePlugins } from "../contexts/PluginContext.tsx";
7
+ import { useColors } from "../contexts/ThemeContext.tsx";
8
+ import { getProviderColor } from "../utils/providerColor.ts";
7
9
 
8
10
  const OVERLAY_BG = RGBA.fromValues(0.0, 0.0, 0.0, 0.5);
9
11
 
@@ -13,15 +15,15 @@ interface SessionDetailsDrawerProps {
13
15
  }
14
16
 
15
17
  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
+ if (count >= 1_000_000) return (count / 1_000_000).toFixed(1) + "M";
19
+ if (count >= 1_000) return (count / 1_000).toFixed(1) + "K";
18
20
  return count.toString();
19
21
  }
20
22
 
21
23
  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);
24
+ if (cost === undefined) return "$0.00";
25
+ if (cost < 0.01 && cost > 0) return "<$0.01";
26
+ return "$" + cost.toFixed(2);
25
27
  }
26
28
 
27
29
  function formatDuration(startedAt: number, lastActivityAt: number): string {
@@ -40,89 +42,93 @@ function formatDuration(startedAt: number, lastActivityAt: number): string {
40
42
  function formatTimelineLabel(timestamp: number, isMultiDay: boolean): string {
41
43
  const date = new Date(timestamp);
42
44
  if (isMultiDay) {
43
- return date.toLocaleDateString([], { month: 'short', day: 'numeric' });
45
+ return date.toLocaleDateString([], { month: "short", day: "numeric" });
44
46
  }
45
- return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
47
+ return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
46
48
  }
47
49
 
48
- const SPARK_CHARS = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
50
+ const SPARK_CHARS = ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"];
49
51
 
50
52
  function buildSparkline(values: number[], width: number): string {
51
- if (values.length === 0 || width <= 0) return '·'.repeat(Math.max(width, 1));
52
-
53
+ if (values.length === 0 || width <= 0) return "·".repeat(Math.max(width, 1));
54
+
53
55
  const bucketSize = Math.ceil(values.length / width);
54
56
  const buckets: number[] = [];
55
-
57
+
56
58
  for (let i = 0; i < width; i++) {
57
59
  const start = i * bucketSize;
58
60
  const end = Math.min(start + bucketSize, values.length);
59
61
  const slice = values.slice(start, end);
60
62
  buckets.push(slice.length > 0 ? slice.reduce((a, b) => a + b, 0) : 0);
61
63
  }
62
-
64
+
63
65
  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('');
66
+
67
+ return buckets
68
+ .map((v) => {
69
+ if (v === 0) return "·";
70
+ const idx = Math.min(Math.floor((v / maxVal) * SPARK_CHARS.length), SPARK_CHARS.length - 1);
71
+ return SPARK_CHARS[idx];
72
+ })
73
+ .join("");
70
74
  }
71
75
 
72
76
  function truncateMiddle(str: string, maxLength: number): string {
73
77
  if (str.length <= maxLength) return str;
74
78
  const side = Math.floor((maxLength - 1) / 2);
75
- return str.slice(0, side) + '…' + str.slice(-side);
79
+ return str.slice(0, side) + "…" + str.slice(-side);
76
80
  }
77
81
 
78
82
  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
+ if (!fullPath) return "—";
84
+
85
+ const normalized = fullPath.replace(/\\/g, "/");
86
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
83
87
  let path = normalized;
84
-
88
+
85
89
  if (home && normalized.startsWith(home)) {
86
- path = '~' + normalized.slice(home.length);
90
+ path = "~" + normalized.slice(home.length);
87
91
  }
88
-
92
+
89
93
  if (path.length <= maxLength) return path;
90
-
91
- const parts = path.split('/').filter(Boolean);
94
+
95
+ const parts = path.split("/").filter(Boolean);
92
96
  if (parts.length <= 2) return truncateMiddle(path, maxLength);
93
-
97
+
94
98
  const first = parts[0];
95
99
  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
-
100
+ const secondLast = parts.length > 2 ? parts[parts.length - 2] : "";
101
+
102
+ const shortened = secondLast ? `${first}/…/${secondLast}/${last}` : `${first}/…/${last}`;
103
+
102
104
  return shortened.length <= maxLength ? shortened : truncateMiddle(path, maxLength);
103
105
  }
104
106
 
105
107
  export function SessionDetailsDrawer({ session, onClose: _onClose }: SessionDetailsDrawerProps) {
106
108
  void _onClose;
107
109
  const colors = useColors();
110
+ const { providers } = usePlugins();
108
111
  const { width: termWidth, height: termHeight } = useTerminalDimensions();
109
-
112
+
110
113
  const width = Math.max(70, Math.min(termWidth - 4, 100));
111
114
  const height = Math.max(24, Math.min(termHeight - 4, 40));
112
115
  const contentWidth = width - 4;
113
-
116
+
114
117
  const duration = formatDuration(session.startedAt, session.lastActivityAt);
115
- const projectName = session.projectPath?.split('/').pop() ?? 'N/A';
116
- const fullPath = session.projectPath ?? '';
117
-
118
+ const projectName = session.projectPath?.split("/").pop() ?? "N/A";
119
+ const fullPath = session.projectPath ?? "";
120
+
118
121
  const sparklineInnerWidth = Math.max(contentWidth - 6, 20);
119
122
  const sparkline = useMemo(() => {
120
- if (!isDatabaseInitialized()) return '·'.repeat(sparklineInnerWidth);
123
+ if (!isDatabaseInitialized()) return "·".repeat(sparklineInnerWidth);
121
124
  const activity = getSessionActivityTimeline(session.sessionId);
122
- if (activity.length === 0) return '·'.repeat(sparklineInnerWidth);
123
- return buildSparkline(activity.map(a => a.tokens), sparklineInnerWidth);
125
+ if (activity.length === 0) return "·".repeat(sparklineInnerWidth);
126
+ return buildSparkline(
127
+ activity.map((a) => a.tokens),
128
+ sparklineInnerWidth,
129
+ );
124
130
  }, [session.sessionId, sparklineInnerWidth]);
125
-
131
+
126
132
  const cacheRead = session.totals.cacheRead ?? 0;
127
133
  const cacheWrite = session.totals.cacheWrite ?? 0;
128
134
  const hasCacheData = cacheRead > 0 || cacheWrite > 0;
@@ -130,7 +136,10 @@ export function SessionDetailsDrawer({ session, onClose: _onClose }: SessionDeta
130
136
  const cacheHitRate = totalInput > 0 ? Math.round((cacheRead / totalInput) * 100) : 0;
131
137
 
132
138
  const sessionCostBreakdown = useMemo(() => {
133
- let input = 0, output = 0, cr = 0, cw = 0;
139
+ let input = 0,
140
+ output = 0,
141
+ cr = 0,
142
+ cw = 0;
134
143
  let hasAny = false;
135
144
  for (const stream of session.streams) {
136
145
  if (!stream.costBreakdown) continue;
@@ -149,11 +158,11 @@ export function SessionDetailsDrawer({ session, onClose: _onClose }: SessionDeta
149
158
  };
150
159
  }, [session.streams]);
151
160
 
152
- const isMultiDay = (session.lastActivityAt - session.startedAt) > 24 * 60 * 60 * 1000;
161
+ const isMultiDay = session.lastActivityAt - session.startedAt > 24 * 60 * 60 * 1000;
153
162
  const startTimeStr = formatTimelineLabel(session.startedAt, isMultiDay);
154
163
  const endTimeStr = formatTimelineLabel(session.lastActivityAt, isMultiDay);
155
164
  const timeGap = Math.max(0, sparklineInnerWidth - startTimeStr.length - endTimeStr.length);
156
- const timelineLabel = startTimeStr + ' '.repeat(timeGap) + endTimeStr;
165
+ const timelineLabel = startTimeStr + " ".repeat(timeGap) + endTimeStr;
157
166
 
158
167
  return (
159
168
  <box
@@ -167,31 +176,46 @@ export function SessionDetailsDrawer({ session, onClose: _onClose }: SessionDeta
167
176
  zIndex={100}
168
177
  backgroundColor={OVERLAY_BG}
169
178
  >
170
- <box
179
+ <box
171
180
  width={width}
172
181
  height={height}
173
- border
174
- borderStyle="double"
175
- borderColor={colors.primary}
176
- flexDirection="column"
182
+ border
183
+ borderStyle="double"
184
+ borderColor={colors.primary}
185
+ flexDirection="column"
177
186
  padding={1}
178
187
  backgroundColor={colors.background}
179
188
  overflow="hidden"
180
189
  >
181
190
  <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>
191
+ <text height={1} fg={colors.primary}>
192
+ <strong>SESSION DETAILS</strong>
193
+ </text>
194
+ <text height={1} fg={colors.textMuted}>
195
+ [Esc] Close
196
+ </text>
184
197
  </box>
185
-
186
- <box flexDirection="column" marginBottom={0} border borderStyle="single" borderColor={colors.border} paddingX={1} paddingY={0} flexShrink={0}>
198
+
199
+ <box
200
+ flexDirection="column"
201
+ marginBottom={0}
202
+ border
203
+ borderStyle="single"
204
+ borderColor={colors.border}
205
+ paddingX={1}
206
+ paddingY={0}
207
+ flexShrink={0}
208
+ >
187
209
  <box flexDirection="row" height={1} justifyContent="space-between" marginTop={0}>
188
210
  <text height={1} overflow="hidden">
189
211
  <span fg={colors.textMuted}>ID: </span>
190
- <span fg={colors.text}><strong>{session.sessionId}</strong></span>
212
+ <span fg={colors.text}>
213
+ <strong>{session.sessionId}</strong>
214
+ </span>
191
215
  </text>
192
216
  <text height={1}>
193
217
  <span fg={colors.textMuted}>Status: </span>
194
- <span fg={session.status === 'active' ? colors.success : colors.textMuted}>
218
+ <span fg={session.status === "active" ? colors.success : colors.textMuted}>
195
219
  {session.status.toUpperCase()}
196
220
  </span>
197
221
  </text>
@@ -201,7 +225,9 @@ export function SessionDetailsDrawer({ session, onClose: _onClose }: SessionDeta
201
225
  <box flexDirection="row" height={1}>
202
226
  <text height={1} overflow="hidden">
203
227
  <span fg={colors.textMuted}>Name: </span>
204
- <span fg={colors.primary}><strong>{session.sessionName}</strong></span>
228
+ <span fg={colors.primary}>
229
+ <strong>{session.sessionName}</strong>
230
+ </span>
205
231
  </text>
206
232
  </box>
207
233
  )}
@@ -213,7 +239,9 @@ export function SessionDetailsDrawer({ session, onClose: _onClose }: SessionDeta
213
239
  </text>
214
240
  <text height={1}>
215
241
  <span fg={colors.textMuted}>Project: </span>
216
- <span fg={colors.text}><strong>{projectName}</strong></span>
242
+ <span fg={colors.text}>
243
+ <strong>{projectName}</strong>
244
+ </span>
217
245
  </text>
218
246
  </box>
219
247
 
@@ -224,11 +252,22 @@ export function SessionDetailsDrawer({ session, onClose: _onClose }: SessionDeta
224
252
  </box>
225
253
  </box>
226
254
 
227
- <box flexDirection="column" marginBottom={0} border borderStyle="single" borderColor={colors.border} paddingX={1} paddingY={0} flexShrink={0}>
255
+ <box
256
+ flexDirection="column"
257
+ marginBottom={0}
258
+ border
259
+ borderStyle="single"
260
+ borderColor={colors.border}
261
+ paddingX={1}
262
+ paddingY={0}
263
+ flexShrink={0}
264
+ >
228
265
  <box flexDirection="row" height={1} justifyContent="space-between" marginTop={0}>
229
266
  <text height={1}>
230
267
  <span fg={colors.textMuted}>Cost: </span>
231
- <span fg={colors.success}><strong>{formatCost(session.totalCostUsd)}</strong></span>
268
+ <span fg={colors.success}>
269
+ <strong>{formatCost(session.totalCostUsd)}</strong>
270
+ </span>
232
271
  </text>
233
272
  <text height={1}>
234
273
  <span fg={colors.textMuted}>Duration: </span>
@@ -239,7 +278,7 @@ export function SessionDetailsDrawer({ session, onClose: _onClose }: SessionDeta
239
278
  {sessionCostBreakdown && (
240
279
  <box flexDirection="row" height={1} marginBottom={0}>
241
280
  <text height={1} overflow="hidden">
242
- <span fg={colors.textMuted}> </span>
281
+ <span fg={colors.textMuted}> </span>
243
282
  <span fg={colors.text}>In {formatCost(sessionCostBreakdown.input)}</span>
244
283
  <span fg={colors.textMuted}> │ </span>
245
284
  <span fg={colors.text}>Out {formatCost(sessionCostBreakdown.output)}</span>
@@ -252,56 +291,114 @@ export function SessionDetailsDrawer({ session, onClose: _onClose }: SessionDeta
252
291
  {(sessionCostBreakdown.cacheWrite ?? 0) > 0 && (
253
292
  <>
254
293
  <span fg={colors.textMuted}> │ </span>
255
- <span fg={colors.text}>Write {formatCost(sessionCostBreakdown.cacheWrite)}</span>
294
+ <span fg={colors.text}>
295
+ Write {formatCost(sessionCostBreakdown.cacheWrite)}
296
+ </span>
256
297
  </>
257
298
  )}
258
299
  </text>
259
300
  </box>
260
301
  )}
261
-
302
+
262
303
  <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>
304
+ <text height={1}>
305
+ <span fg={colors.textMuted}>Tokens: </span>
306
+ <span fg={colors.text}>
307
+ {formatTokens(session.totals.input + session.totals.output)}
308
+ </span>
309
+ <span fg={colors.textMuted}>
310
+ {" "}
311
+ (In: {formatTokens(session.totals.input)} / Out:{" "}
312
+ {formatTokens(session.totals.output)})
313
+ </span>
314
+ </text>
270
315
  </box>
271
316
 
272
317
  {hasCacheData && (
273
318
  <box flexDirection="row" height={1} marginBottom={0}>
274
319
  <text height={1} overflow="hidden">
275
320
  <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>
321
+ <span fg={cacheHitRate >= 50 ? colors.success : colors.warning}>
322
+ {cacheHitRate}% hit
323
+ </span>
324
+ <span fg={colors.textMuted}>
325
+ {" "}
326
+ — {formatTokens(cacheRead)} read / {formatTokens(cacheWrite)} write
327
+ </span>
278
328
  </text>
279
329
  </box>
280
330
  )}
281
331
  </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>
332
+
333
+ <box
334
+ flexDirection="column"
335
+ height={4}
336
+ marginBottom={0}
337
+ border
338
+ borderColor={colors.border}
339
+ paddingX={1}
340
+ flexShrink={0}
341
+ >
342
+ <text height={1} fg={colors.primary} overflow="hidden">
343
+ {sparkline}
344
+ </text>
345
+ <text height={1} fg={colors.textMuted} overflow="hidden">
346
+ {timelineLabel}
347
+ </text>
286
348
  </box>
287
-
349
+
288
350
  <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>
351
+ <box width={12}>
352
+ <text fg={colors.primary}>
353
+ <strong>PROVIDER</strong>
354
+ </text>
355
+ </box>
356
+ <text flexGrow={1} fg={colors.primary}>
357
+ <strong>MODEL</strong>
358
+ </text>
359
+ <box width={10} justifyContent="flex-end">
360
+ <text fg={colors.primary}>
361
+ <strong>TOKENS</strong>
362
+ </text>
363
+ </box>
364
+ <box width={10} justifyContent="flex-end">
365
+ <text fg={colors.primary}>
366
+ <strong>COST</strong>
367
+ </text>
368
+ </box>
292
369
  </box>
293
-
294
- <box flexDirection="column" flexGrow={1} border borderColor={colors.border} overflow="hidden">
370
+
371
+ <box
372
+ flexDirection="column"
373
+ flexGrow={1}
374
+ border
375
+ borderColor={colors.border}
376
+ overflow="hidden"
377
+ >
295
378
  <scrollbox flexGrow={1}>
296
379
  {session.streams.map((stream, idx) => {
297
- const modelName = stream.modelId.split('/').pop() ?? stream.modelId;
380
+ const modelName = stream.modelId.split("/").pop() ?? stream.modelId;
381
+ const providerName =
382
+ stream.providerId.charAt(0).toUpperCase() + stream.providerId.slice(1);
383
+ const providerColor = getProviderColor(
384
+ stream.providerId,
385
+ providers,
386
+ colors.textMuted,
387
+ );
298
388
  return (
299
- <box key={`${stream.modelId}-${idx}`} flexDirection="row" height={1} paddingX={1}>
389
+ <box key={`${stream.modelId}-${idx}`} flexDirection="row" height={1} paddingX={1}>
390
+ <box width={12} height={1}>
391
+ <text height={1} fg={providerColor} overflow="hidden">
392
+ {truncateMiddle(providerName, 11)}
393
+ </text>
394
+ </box>
300
395
  <text flexGrow={1} height={1} fg={colors.text} overflow="hidden">
301
- {truncateMiddle(modelName, contentWidth - 25)}
396
+ {truncateMiddle(modelName, contentWidth - 37)}
302
397
  </text>
303
398
  <box width={10} height={1} justifyContent="flex-end">
304
- <text fg={colors.textMuted}>{formatTokens(stream.tokens.input + stream.tokens.output)}</text>
399
+ <text fg={colors.textMuted}>
400
+ {formatTokens(stream.tokens.input + stream.tokens.output)}
401
+ </text>
305
402
  </box>
306
403
  <box width={10} height={1} justifyContent="flex-end">
307
404
  <text fg={colors.success}>{formatCost(stream.costUsd)}</text>