@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,12 +1,12 @@
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';
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: 'sessions' | 'sidebar' | 'limits';
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 === '—') return '—';
24
- const normalized = projectPath.replace(/\\/g, '/');
25
- return normalized.split('/').filter(Boolean).pop() ?? projectPath;
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
- 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`;
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
- if (str.length <= maxLength) return str;
67
- const side = Math.floor((maxLength - 1) / 2);
68
- return str.slice(0, side) + '…' + str.slice(-side);
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 '$0';
73
- if (cost < 0.01) return '<$0.01';
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, writeCost = 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 = session.requestCount > 0
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({ session, isSelected, isWide, nameMaxWidth, getProviderColor, isExiting, exitIntensity, skipEntrance, terminalWidth }: SessionRowProps) {
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 === 'active';
145
+ const isActive = session.status === "active";
136
146
  const entranceIntensity = useEntranceAnimation({ durationMs: 500 });
137
147
  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
-
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 ?? 'unknown';
153
- const modelId = primaryStream?.modelId ?? 'unknown';
169
+ const providerId = primaryStream?.providerId ?? "unknown";
170
+ const modelId = primaryStream?.modelId ?? "unknown";
154
171
  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
-
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
- const baseTokenColor = colors.text;
171
- const tokenColorBeforeFade = tokenFlash > 0
172
- ? interpolateColor(tokenFlash, baseTokenColor, '#ffffff')
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 = costFlash > 0
182
- ? interpolateColor(costFlash, baseCostColor, '#ffffff')
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
- <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
- });
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 = terminalWidth >= 160 ? 44 : terminalWidth >= 140 ? 40 : terminalWidth >= 105 ? 36 : terminalWidth >= 95 ? 32 : terminalWidth >= 85 ? 28 : 0;
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 ? Math.max(8, tableInner - WIDE_FIXED) : Math.max(1, tableInner - narrowFixed);
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, {
@@ -305,50 +395,107 @@ export const SessionsTable = forwardRef(function SessionsTable(
305
395
  flexDirection="column"
306
396
  flexGrow={2}
307
397
  border
308
- borderStyle={focusedPanel === 'sessions' ? 'double' : 'single'}
309
- borderColor={focusedPanel === 'sessions' ? colors.primary : colors.border}
398
+ borderStyle={focusedPanel === "sessions" ? "double" : "single"}
399
+ borderColor={focusedPanel === "sessions" ? colors.primary : colors.border}
310
400
  overflow="hidden"
311
401
  >
312
- <box flexDirection="row" paddingLeft={2} paddingRight={1} height={1} justifyContent="space-between" overflow="hidden">
402
+ <box
403
+ flexDirection="row"
404
+ paddingLeft={2}
405
+ paddingRight={1}
406
+ height={1}
407
+ justifyContent="space-between"
408
+ overflow="hidden"
409
+ >
313
410
  <text height={1} fg={colors.textMuted}>
314
- SESSIONS{filterQuery ? ` [${isFiltering ? 'Filter: ' : ''}${filterQuery}]` : ''}{isLoading ? ' ⟳' : ' '}
411
+ SESSIONS{filterQuery ? ` [${isFiltering ? "Filter: " : ""}${filterQuery}]` : ""}
412
+ {isLoading ? " ⟳" : " "}
413
+ </text>
414
+ <text height={1} fg={colors.textMuted}>
415
+ [{windowLabel}] {sessions.length} sessions
315
416
  </text>
316
- <text height={1} fg={colors.textMuted}>[{windowLabel}] {sessions.length} sessions</text>
317
417
  </box>
318
418
 
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
- )}
419
+ {isWide ? (
420
+ <box flexDirection="row" paddingRight={1} height={1} gap={1}>
421
+ <text width={2} height={1} fg={colors.textMuted}>
422
+ {" "}
423
+ </text>
424
+ <text width={8} height={1} fg={colors.textMuted}>
425
+ ID{" "}
426
+ </text>
427
+ <text width={12} height={1} fg={colors.textMuted}>
428
+ AGENT{" "}
429
+ </text>
430
+ <text width={18} height={1} fg={colors.textMuted}>
431
+ MODEL{" "}
432
+ </text>
433
+ <text width={5} height={1} fg={colors.textMuted}>
434
+ {" "}
435
+ REQ{" "}
436
+ </text>
437
+ <text width={14} height={1} fg={colors.textMuted}>
438
+ {" "}
439
+ TOKENS{" "}
440
+ </text>
441
+ <text width={8} height={1} fg={colors.textMuted}>
442
+ {" "}
443
+ COST{" "}
444
+ </text>
445
+ <text width={14} height={1} fg={colors.textMuted}>
446
+ PROJECT{" "}
447
+ </text>
448
+ <text flexGrow={1} height={1} fg={colors.textMuted}>
449
+ NAME
450
+ </text>
451
+ <text width={6} height={1} fg={colors.textMuted}>
452
+ {" "}
453
+ DUR{" "}
454
+ </text>
455
+ <text width={5} height={1} fg={colors.textMuted}>
456
+ LAST{" "}
457
+ </text>
458
+ <text width={2} height={1} fg={colors.textMuted}>
459
+ {" "}
460
+ </text>
461
+ </box>
462
+ ) : (
463
+ <box flexDirection="row" paddingRight={1} height={1}>
464
+ <text width={2} height={1} fg={colors.textMuted}>
465
+ {" "}
466
+ </text>
467
+ <text width={9} height={1} fg={colors.textMuted}>
468
+ AGENT{" "}
469
+ </text>
470
+ <text width={isTight ? 13 : 16} height={1} fg={colors.textMuted}>
471
+ {isTight ? "MODEL " : "MODEL "}
472
+ </text>
473
+ <text width={isTight ? 9 : 14} height={1} fg={colors.textMuted}>
474
+ {isTight ? " TOKENS " : " TOKENS "}
475
+ </text>
476
+ <text width={7} height={1} fg={colors.textMuted}>
477
+ {" "}
478
+ COST{" "}
479
+ </text>
480
+ <text flexGrow={1} height={1} fg={colors.textMuted}>
481
+ {nameMaxWidth >= 7 ? "PROJECT" : nameMaxWidth >= 4 ? "PROJ" : ""}
482
+ </text>
483
+ <text width={5} height={1} fg={colors.textMuted}>
484
+ LAST{" "}
485
+ </text>
486
+ <text width={2} height={1} fg={colors.textMuted}>
487
+ {" "}
488
+ </text>
489
+ </box>
490
+ )}
346
491
 
347
492
  <scrollbox ref={ref} flexGrow={1}>
348
493
  <box flexDirection="column">
349
494
  {sessions.length === 0 && animatedSessions.length === 0 && (
350
495
  <box paddingLeft={2}>
351
- <text fg={colors.textMuted}>{isLoading ? 'Loading sessions...' : 'No sessions found'}</text>
496
+ <text fg={colors.textMuted}>
497
+ {isLoading ? "Loading sessions..." : "No sessions found"}
498
+ </text>
352
499
  </box>
353
500
  )}
354
501
  {animatedSessions.map((entry) => {
@@ -372,7 +519,7 @@ export const SessionsTable = forwardRef(function SessionsTable(
372
519
  </scrollbox>
373
520
  {terminalHeight >= 30 && (
374
521
  <box height={1} paddingX={2} flexShrink={0}>
375
- {inspector && focusedPanel === 'sessions' ? (
522
+ {inspector && focusedPanel === "sessions" ? (
376
523
  <text height={1} overflow="hidden">
377
524
  <span fg={colors.textMuted}>↯ Cache read </span>
378
525
  <span fg={colors.text}>{formatTokensCompact(inspector.cacheRead)}</span>