@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,15 +1,15 @@
1
- import { useState, useRef, useEffect, useMemo } from 'react';
2
- import { useKeyboard, useTerminalDimensions } from '@opentui/react';
3
- import type { ScrollBoxRenderable } from '@opentui/core';
4
- import { useColors } from '../contexts/ThemeContext.tsx';
5
- import { useLogs, type LogEntry, type LogLevel } from '../contexts/LogContext.tsx';
6
- import { useInputFocus } from '../contexts/InputContext.tsx';
7
- import { pluginRegistry } from '@/plugins/registry.ts';
8
- import { checkAllPluginUpdates, type PluginUpdateInfo } from '@/plugins/update-checker.ts';
9
- import { ModalBackdrop, Z_INDEX } from './ModalBackdrop.tsx';
10
-
11
- type DebugTab = 'logs' | 'inspector' | 'plugins';
12
- type PluginSort = 'name' | 'type' | 'official';
1
+ import type { ScrollBoxRenderable } from "@opentui/core";
2
+ import { useKeyboard, useTerminalDimensions } from "@opentui/react";
3
+ import { useEffect, useMemo, useRef, useState } from "react";
4
+ import { pluginRegistry } from "@/plugins/registry.ts";
5
+ import { checkAllPluginUpdates, type PluginUpdateInfo } from "@/plugins/update-checker.ts";
6
+ import { useInputFocus } from "../contexts/InputContext.tsx";
7
+ import { type LogEntry, type LogLevel, useLogs } from "../contexts/LogContext.tsx";
8
+ import { useColors } from "../contexts/ThemeContext.tsx";
9
+ import { ModalBackdrop, Z_INDEX } from "./ModalBackdrop.tsx";
10
+
11
+ type DebugTab = "logs" | "inspector" | "plugins";
12
+ type PluginSort = "name" | "type" | "official";
13
13
 
14
14
  interface DebugInspectorData {
15
15
  sessions: Array<{
@@ -41,21 +41,21 @@ interface DebugPanelProps {
41
41
  }
42
42
 
43
43
  function padRight(str: string, len: number): string {
44
- return str.length >= len ? str.slice(0, len) : str + ' '.repeat(len - str.length);
44
+ return str.length >= len ? str.slice(0, len) : str + " ".repeat(len - str.length);
45
45
  }
46
46
 
47
- const LOG_LEVELS: LogLevel[] = ['debug', 'info', 'warn', 'error'];
47
+ const LOG_LEVELS: LogLevel[] = ["debug", "info", "warn", "error"];
48
48
  const LOG_LEVEL_RANK: Record<LogLevel, number> = { debug: 0, info: 1, warn: 2, error: 3 };
49
49
 
50
50
  function filterByLevel(logs: LogEntry[], minLevel: LogLevel): LogEntry[] {
51
51
  const minRank = LOG_LEVEL_RANK[minLevel];
52
52
  if (minRank === 0) return logs;
53
- return logs.filter(e => LOG_LEVEL_RANK[e.level] >= minRank);
53
+ return logs.filter((e) => LOG_LEVEL_RANK[e.level] >= minRank);
54
54
  }
55
55
 
56
56
  function nextLevel(current: LogLevel): LogLevel {
57
57
  const idx = LOG_LEVELS.indexOf(current);
58
- return LOG_LEVELS[(idx + 1) % LOG_LEVELS.length] ?? 'debug';
58
+ return LOG_LEVELS[(idx + 1) % LOG_LEVELS.length] ?? "debug";
59
59
  }
60
60
 
61
61
  export function DebugPanel({ onClose, inspectorData }: DebugPanelProps) {
@@ -63,10 +63,10 @@ export function DebugPanel({ onClose, inspectorData }: DebugPanelProps) {
63
63
  const { width: termWidth, height: termHeight } = useTerminalDimensions();
64
64
  const { logs, clearLogs } = useLogs();
65
65
  const { setInputFocused } = useInputFocus();
66
- const [activeTab, setActiveTab] = useState<DebugTab>('logs');
66
+ const [activeTab, setActiveTab] = useState<DebugTab>("logs");
67
67
  const [follow, setFollow] = useState(true);
68
- const [minLevel, setMinLevel] = useState<LogLevel>('info');
69
- const [pluginSort, setPluginSort] = useState<PluginSort>('type');
68
+ const [minLevel, setMinLevel] = useState<LogLevel>("info");
69
+ const [pluginSort, setPluginSort] = useState<PluginSort>("type");
70
70
  const scrollboxRef = useRef<ScrollBoxRenderable>(null);
71
71
  const inspectorScrollboxRef = useRef<ScrollBoxRenderable>(null);
72
72
  const pluginsScrollboxRef = useRef<ScrollBoxRenderable>(null);
@@ -82,7 +82,7 @@ export function DebugPanel({ onClose, inspectorData }: DebugPanelProps) {
82
82
  }, [setInputFocused]);
83
83
 
84
84
  useEffect(() => {
85
- if (follow && activeTab === 'logs') {
85
+ if (follow && activeTab === "logs") {
86
86
  const scrollbox = scrollboxRef.current;
87
87
  if (scrollbox) {
88
88
  scrollbox.scrollTo(scrollbox.scrollHeight);
@@ -91,136 +91,136 @@ export function DebugPanel({ onClose, inspectorData }: DebugPanelProps) {
91
91
  }, [logs.length, follow, activeTab]);
92
92
 
93
93
  useKeyboard((key) => {
94
- if (key.name === 'escape') {
94
+ if (key.name === "escape") {
95
95
  onClose();
96
96
  return;
97
97
  }
98
98
 
99
- const tabs: DebugTab[] = ['logs', 'inspector', 'plugins'];
100
- if (key.name === 'tab' || key.sequence === '[' || key.sequence === ']') {
101
- setActiveTab(prev => tabs[(tabs.indexOf(prev) + 1) % tabs.length] ?? 'logs');
99
+ const tabs: DebugTab[] = ["logs", "inspector", "plugins"];
100
+ if (key.name === "tab" || key.sequence === "[" || key.sequence === "]") {
101
+ setActiveTab((prev) => tabs[(tabs.indexOf(prev) + 1) % tabs.length] ?? "logs");
102
102
  lastKeyRef.current = null;
103
103
  return;
104
104
  }
105
- if (key.name === '1') {
106
- setActiveTab('logs');
105
+ if (key.name === "1") {
106
+ setActiveTab("logs");
107
107
  lastKeyRef.current = null;
108
108
  return;
109
109
  }
110
- if (key.name === '2') {
111
- setActiveTab('inspector');
110
+ if (key.name === "2") {
111
+ setActiveTab("inspector");
112
112
  lastKeyRef.current = null;
113
113
  return;
114
114
  }
115
- if (key.name === '3') {
116
- setActiveTab('plugins');
115
+ if (key.name === "3") {
116
+ setActiveTab("plugins");
117
117
  lastKeyRef.current = null;
118
118
  return;
119
119
  }
120
120
 
121
- if (activeTab === 'logs') {
122
- if (key.name === 'f') {
123
- setFollow(prev => !prev);
121
+ if (activeTab === "logs") {
122
+ if (key.name === "f") {
123
+ setFollow((prev) => !prev);
124
124
  return;
125
125
  }
126
- if (key.name === 'l') {
127
- setMinLevel(prev => nextLevel(prev));
126
+ if (key.name === "l") {
127
+ setMinLevel((prev) => nextLevel(prev));
128
128
  return;
129
129
  }
130
- if (key.name === 'c') {
130
+ if (key.name === "c") {
131
131
  clearLogs();
132
132
  return;
133
133
  }
134
- if (key.name === 'down' || key.name === 'j') {
134
+ if (key.name === "down" || key.name === "j") {
135
135
  const box = scrollboxRef.current;
136
136
  if (box) box.scrollTo(Math.min(box.scrollTop + SCROLL_STEP, box.scrollHeight));
137
137
  setFollow(false);
138
138
  lastKeyRef.current = null;
139
139
  return;
140
140
  }
141
- if (key.name === 'up' || key.name === 'k') {
141
+ if (key.name === "up" || key.name === "k") {
142
142
  const box = scrollboxRef.current;
143
143
  if (box) box.scrollTo(Math.max(box.scrollTop - SCROLL_STEP, 0));
144
144
  setFollow(false);
145
145
  lastKeyRef.current = null;
146
146
  return;
147
147
  }
148
- if (key.shift && key.name === 'g') {
148
+ if (key.shift && key.name === "g") {
149
149
  scrollboxRef.current?.scrollTo(scrollboxRef.current.scrollHeight);
150
150
  lastKeyRef.current = null;
151
151
  return;
152
152
  }
153
- if (key.name === 'g') {
154
- if (lastKeyRef.current === 'g') {
153
+ if (key.name === "g") {
154
+ if (lastKeyRef.current === "g") {
155
155
  scrollboxRef.current?.scrollTo(0);
156
156
  lastKeyRef.current = null;
157
157
  } else {
158
- lastKeyRef.current = 'g';
158
+ lastKeyRef.current = "g";
159
159
  }
160
160
  return;
161
161
  }
162
162
  }
163
163
 
164
- if (activeTab === 'inspector') {
165
- if (key.name === 'down' || key.name === 'j') {
164
+ if (activeTab === "inspector") {
165
+ if (key.name === "down" || key.name === "j") {
166
166
  const box = inspectorScrollboxRef.current;
167
167
  if (box) box.scrollTo(Math.min(box.scrollTop + SCROLL_STEP, box.scrollHeight));
168
168
  lastKeyRef.current = null;
169
169
  return;
170
170
  }
171
- if (key.name === 'up' || key.name === 'k') {
171
+ if (key.name === "up" || key.name === "k") {
172
172
  const box = inspectorScrollboxRef.current;
173
173
  if (box) box.scrollTo(Math.max(box.scrollTop - SCROLL_STEP, 0));
174
174
  lastKeyRef.current = null;
175
175
  return;
176
176
  }
177
- if (key.shift && key.name === 'g') {
177
+ if (key.shift && key.name === "g") {
178
178
  inspectorScrollboxRef.current?.scrollTo(inspectorScrollboxRef.current.scrollHeight);
179
179
  lastKeyRef.current = null;
180
180
  return;
181
181
  }
182
- if (key.name === 'g') {
183
- if (lastKeyRef.current === 'g') {
182
+ if (key.name === "g") {
183
+ if (lastKeyRef.current === "g") {
184
184
  inspectorScrollboxRef.current?.scrollTo(0);
185
185
  lastKeyRef.current = null;
186
186
  } else {
187
- lastKeyRef.current = 'g';
187
+ lastKeyRef.current = "g";
188
188
  }
189
189
  return;
190
190
  }
191
191
  }
192
192
 
193
- if (activeTab === 'plugins') {
194
- if (key.name === 's') {
195
- setPluginSort(prev => {
196
- const order: PluginSort[] = ['name', 'type', 'official'];
197
- return order[(order.indexOf(prev) + 1) % order.length] ?? 'name';
193
+ if (activeTab === "plugins") {
194
+ if (key.name === "s") {
195
+ setPluginSort((prev) => {
196
+ const order: PluginSort[] = ["name", "type", "official"];
197
+ return order[(order.indexOf(prev) + 1) % order.length] ?? "name";
198
198
  });
199
199
  return;
200
200
  }
201
- if (key.name === 'down' || key.name === 'j') {
201
+ if (key.name === "down" || key.name === "j") {
202
202
  const box = pluginsScrollboxRef.current;
203
203
  if (box) box.scrollTo(Math.min(box.scrollTop + SCROLL_STEP, box.scrollHeight));
204
204
  lastKeyRef.current = null;
205
205
  return;
206
206
  }
207
- if (key.name === 'up' || key.name === 'k') {
207
+ if (key.name === "up" || key.name === "k") {
208
208
  const box = pluginsScrollboxRef.current;
209
209
  if (box) box.scrollTo(Math.max(box.scrollTop - SCROLL_STEP, 0));
210
210
  lastKeyRef.current = null;
211
211
  return;
212
212
  }
213
- if (key.shift && key.name === 'g') {
213
+ if (key.shift && key.name === "g") {
214
214
  pluginsScrollboxRef.current?.scrollTo(pluginsScrollboxRef.current.scrollHeight);
215
215
  lastKeyRef.current = null;
216
216
  return;
217
217
  }
218
- if (key.name === 'g') {
219
- if (lastKeyRef.current === 'g') {
218
+ if (key.name === "g") {
219
+ if (lastKeyRef.current === "g") {
220
220
  pluginsScrollboxRef.current?.scrollTo(0);
221
221
  lastKeyRef.current = null;
222
222
  } else {
223
- lastKeyRef.current = 'g';
223
+ lastKeyRef.current = "g";
224
224
  }
225
225
  return;
226
226
  }
@@ -237,10 +237,10 @@ export function DebugPanel({ onClose, inspectorData }: DebugPanelProps) {
237
237
  };
238
238
 
239
239
  const levelLabels: Record<LogLevel, string> = {
240
- debug: 'DBG',
241
- info: 'INF',
242
- warn: 'WRN',
243
- error: 'ERR',
240
+ debug: "DBG",
241
+ info: "INF",
242
+ warn: "WRN",
243
+ error: "ERR",
244
244
  };
245
245
 
246
246
  return (
@@ -255,38 +255,38 @@ export function DebugPanel({ onClose, inspectorData }: DebugPanelProps) {
255
255
  backgroundColor={colors.background}
256
256
  overflow="hidden"
257
257
  >
258
- <box
259
- flexDirection="row"
260
- justifyContent="space-between"
261
- paddingX={1}
262
- backgroundColor={colors.foreground}
263
- height={1}
264
- flexShrink={0}
265
- >
258
+ <box
259
+ flexDirection="row"
260
+ justifyContent="space-between"
261
+ paddingX={1}
262
+ backgroundColor={colors.foreground}
263
+ height={1}
264
+ flexShrink={0}
265
+ >
266
266
  <box flexDirection="row" gap={2}>
267
267
  <text
268
- fg={activeTab === 'logs' ? colors.background : colors.textMuted}
269
- {...(activeTab === 'logs' ? { bg: colors.primary } : {})}
268
+ fg={activeTab === "logs" ? colors.background : colors.textMuted}
269
+ {...(activeTab === "logs" ? { bg: colors.primary } : {})}
270
270
  >
271
- {' LOGS '}
271
+ {" LOGS "}
272
272
  </text>
273
273
  <text
274
- fg={activeTab === 'inspector' ? colors.background : colors.textMuted}
275
- {...(activeTab === 'inspector' ? { bg: colors.primary } : {})}
274
+ fg={activeTab === "inspector" ? colors.background : colors.textMuted}
275
+ {...(activeTab === "inspector" ? { bg: colors.primary } : {})}
276
276
  >
277
- {' INSPECTOR '}
277
+ {" INSPECTOR "}
278
278
  </text>
279
279
  <text
280
- fg={activeTab === 'plugins' ? colors.background : colors.textMuted}
281
- {...(activeTab === 'plugins' ? { bg: colors.primary } : {})}
280
+ fg={activeTab === "plugins" ? colors.background : colors.textMuted}
281
+ {...(activeTab === "plugins" ? { bg: colors.primary } : {})}
282
282
  >
283
- {' PLUGINS '}
283
+ {" PLUGINS "}
284
284
  </text>
285
285
  </box>
286
- <text fg={colors.textSubtle}>1/2/3:switch Esc:close</text>
286
+ <text fg={colors.textSubtle}>1/2/3:switch Esc:close</text>
287
287
  </box>
288
288
 
289
- {activeTab === 'logs' && (
289
+ {activeTab === "logs" && (
290
290
  <LogsTab
291
291
  logs={filterByLevel(logs, minLevel)}
292
292
  follow={follow}
@@ -297,35 +297,32 @@ export function DebugPanel({ onClose, inspectorData }: DebugPanelProps) {
297
297
  />
298
298
  )}
299
299
 
300
- {activeTab === 'inspector' && (
300
+ {activeTab === "inspector" && (
301
301
  <InspectorTab data={inspectorData} colors={colors} scrollboxRef={inspectorScrollboxRef} />
302
302
  )}
303
303
 
304
- {activeTab === 'plugins' && (
304
+ {activeTab === "plugins" && (
305
305
  <PluginsTab colors={colors} scrollboxRef={pluginsScrollboxRef} sortBy={pluginSort} />
306
306
  )}
307
307
 
308
- <box
309
- flexDirection="row"
310
- paddingX={1}
311
- backgroundColor={colors.foreground}
312
- height={1}
313
- flexShrink={0}
314
- >
315
- {activeTab === 'logs' && (
308
+ <box
309
+ flexDirection="row"
310
+ paddingX={1}
311
+ backgroundColor={colors.foreground}
312
+ height={1}
313
+ flexShrink={0}
314
+ >
315
+ {activeTab === "logs" && (
316
316
  <text fg={colors.textSubtle}>
317
- j/k:scroll f:follow{follow ? '(on)' : '(off)'} l:{minLevel.toUpperCase()} c:clear gg:top G:bottom
317
+ j/k:scroll f:follow{follow ? "(on)" : "(off)"} l:{minLevel.toUpperCase()} c:clear
318
+ gg:top G:bottom
318
319
  </text>
319
320
  )}
320
- {activeTab === 'inspector' && (
321
- <text fg={colors.textSubtle}>
322
- j/k:scroll gg:top G:bottom
323
- </text>
321
+ {activeTab === "inspector" && (
322
+ <text fg={colors.textSubtle}>j/k:scroll gg:top G:bottom</text>
324
323
  )}
325
- {activeTab === 'plugins' && (
326
- <text fg={colors.textSubtle}>
327
- j/k:scroll s:sort({pluginSort}) gg:top G:bottom
328
- </text>
324
+ {activeTab === "plugins" && (
325
+ <text fg={colors.textSubtle}>j/k:scroll s:sort({pluginSort}) gg:top G:bottom</text>
329
326
  )}
330
327
  </box>
331
328
  </box>
@@ -355,13 +352,16 @@ function LogsTab({ logs, follow, scrollboxRef, levelColors, levelLabels, colors
355
352
  }
356
353
 
357
354
  const visibleLogs = frozenLogsRef.current ?? logs.slice(-200);
358
- const pendingCount = frozenLogsRef.current ? Math.max(0, logs.length - frozenAtCountRef.current) : 0;
355
+ const pendingCount = frozenLogsRef.current
356
+ ? Math.max(0, logs.length - frozenAtCountRef.current)
357
+ : 0;
359
358
 
360
359
  return (
361
360
  <box flexDirection="column" flexGrow={1} overflow="hidden">
362
361
  <box paddingLeft={1} height={1} flexShrink={0}>
363
362
  <text fg={colors.textMuted}>
364
- {logs.length} entries{follow ? ' [FOLLOW]' : ` [PAUSED${pendingCount > 0 ? ` +${pendingCount} new` : ''}]`}
363
+ {logs.length} entries
364
+ {follow ? " [FOLLOW]" : ` [PAUSED${pendingCount > 0 ? ` +${pendingCount} new` : ""}]`}
365
365
  </text>
366
366
  </box>
367
367
  <scrollbox
@@ -407,24 +407,21 @@ interface LogLineProps {
407
407
  }
408
408
 
409
409
  function LogLine({ entry, levelColors, levelLabels, colors }: LogLineProps) {
410
- const time = new Date(entry.timestamp).toLocaleTimeString('en-US', {
410
+ const time = new Date(entry.timestamp).toLocaleTimeString("en-US", {
411
411
  hour12: false,
412
- hour: '2-digit',
413
- minute: '2-digit',
414
- second: '2-digit',
412
+ hour: "2-digit",
413
+ minute: "2-digit",
414
+ second: "2-digit",
415
415
  });
416
416
 
417
417
  const levelColor = levelColors[entry.level];
418
418
  const levelLabel = levelLabels[entry.level];
419
- const source = entry.source ? `[${entry.source}]` : '';
420
- const dataStr = entry.data ? ` ${JSON.stringify(entry.data)}` : '';
419
+ const source = entry.source ? `[${entry.source}]` : "";
420
+ const dataStr = entry.data ? ` ${JSON.stringify(entry.data)}` : "";
421
421
 
422
422
  return (
423
423
  <text height={1}>
424
- <span fg={colors.textSubtle}>{time}</span>
425
- {' '}
426
- <span fg={levelColor}>{levelLabel}</span>
427
- {' '}
424
+ <span fg={colors.textSubtle}>{time}</span> <span fg={levelColor}>{levelLabel}</span>{" "}
428
425
  {source && <span fg={colors.textMuted}>{source} </span>}
429
426
  <span fg={colors.text}>{entry.message}</span>
430
427
  {dataStr && <span fg={colors.textSubtle}>{dataStr}</span>}
@@ -452,31 +449,49 @@ function InspectorTab({ data, colors, scrollboxRef }: InspectorTabProps) {
452
449
 
453
450
  const { sessions, debugData, activity, sparkData } = data;
454
451
  const totalTokens = sessions.reduce((sum, s) => sum + s.totals.input + s.totals.output, 0);
455
- const activeSessions = sessions.filter(s => s.status === 'active');
452
+ const activeSessions = sessions.filter((s) => s.status === "active");
456
453
  const now = Date.now();
457
454
 
458
455
  if (isCompact) {
459
456
  return (
460
- <box flexDirection="column" flexGrow={1} paddingX={1} overflow="hidden">
457
+ <box flexDirection="column" flexGrow={1} paddingX={1} overflow="hidden">
461
458
  <box flexDirection="row" height={1} gap={2} flexShrink={0}>
462
459
  <text fg={colors.textMuted}>rate:{(activity.instantRate || 0).toFixed(0)}/s</text>
463
460
  <text fg={colors.textMuted}>avg:{(activity.avgRate || 0).toFixed(0)}/s</text>
464
461
  <text fg={colors.textMuted}>tokens:{totalTokens.toLocaleString()}</text>
465
- <text fg={activity.isSpike ? colors.warning : colors.textMuted}>{activity.isSpike ? 'SPIKE' : ''}</text>
462
+ <text fg={activity.isSpike ? colors.warning : colors.textMuted}>
463
+ {activity.isSpike ? "SPIKE" : ""}
464
+ </text>
466
465
  </box>
467
466
  <box flexDirection="row" marginTop={1} height={1}>
468
- <text width={18} fg={colors.textSubtle}>{padRight('SESSION', 18)}</text>
469
- <text width={10} fg={colors.textSubtle}>{padRight('AGENT', 10)}</text>
470
- <text width={8} fg={colors.textSubtle}>{padRight('STATUS', 8)}</text>
471
- <text width={10} fg={colors.textSubtle}>{padRight('TOKENS', 10)}</text>
467
+ <text width={18} fg={colors.textSubtle}>
468
+ {padRight("SESSION", 18)}
469
+ </text>
470
+ <text width={10} fg={colors.textSubtle}>
471
+ {padRight("AGENT", 10)}
472
+ </text>
473
+ <text width={8} fg={colors.textSubtle}>
474
+ {padRight("STATUS", 8)}
475
+ </text>
476
+ <text width={10} fg={colors.textSubtle}>
477
+ {padRight("TOKENS", 10)}
478
+ </text>
472
479
  </box>
473
480
  <scrollbox ref={scrollboxRef} flexGrow={1}>
474
- {sessions.slice(0, 15).map(s => (
481
+ {sessions.slice(0, 15).map((s) => (
475
482
  <box key={s.sessionId} flexDirection="row" height={1}>
476
- <text width={18} fg={colors.text}>{padRight(s.sessionId.slice(0, 17), 18)}</text>
477
- <text width={10} fg={colors.text}>{padRight(s.agentName.slice(0, 9), 10)}</text>
478
- <text width={8} fg={s.status === 'active' ? colors.success : colors.textMuted}>{padRight(s.status.slice(0, 7), 8)}</text>
479
- <text width={10} fg={colors.text}>{padRight((s.totals.input + s.totals.output).toLocaleString(), 10)}</text>
483
+ <text width={18} fg={colors.text}>
484
+ {padRight(s.sessionId.slice(0, 17), 18)}
485
+ </text>
486
+ <text width={10} fg={colors.text}>
487
+ {padRight(s.agentName.slice(0, 9), 10)}
488
+ </text>
489
+ <text width={8} fg={s.status === "active" ? colors.success : colors.textMuted}>
490
+ {padRight(s.status.slice(0, 7), 8)}
491
+ </text>
492
+ <text width={10} fg={colors.text}>
493
+ {padRight((s.totals.input + s.totals.output).toLocaleString(), 10)}
494
+ </text>
480
495
  </box>
481
496
  ))}
482
497
  </scrollbox>
@@ -487,53 +502,143 @@ function InspectorTab({ data, colors, scrollboxRef }: InspectorTabProps) {
487
502
  return (
488
503
  <box flexDirection="column" flexGrow={1} padding={1} overflow="hidden">
489
504
  <box flexDirection="row" gap={2} height={8} flexShrink={0}>
490
- <box flexDirection="column" flexGrow={1} border borderColor={colors.border} padding={1} overflow="hidden">
491
- <text height={1} fg={colors.primary}>{padRight('Bucket Data', 30)}</text>
492
- <text height={1} fg={colors.textMuted}>{padRight('deltaTokens: ' + debugData.lastDeltaTokens.toLocaleString(), 30)}</text>
493
- <text height={1} fg={colors.textMuted}>{padRight('dt: ' + debugData.lastDt.toFixed(3) + 's', 30)}</text>
494
- <text height={1} fg={colors.textMuted}>{padRight('shifted: ' + debugData.bucketsShifted, 30)}</text>
495
- <text height={1} fg={colors.textMuted}>{padRight('currBucket: ' + debugData.currentBucketValue.toFixed(1), 30)}</text>
496
- <text height={1} fg={colors.textMuted}>{padRight('buckets[5]: [' + sparkData.slice(-5).map(v => v.toFixed(0)).join(',') + ']', 30)}</text>
505
+ <box
506
+ flexDirection="column"
507
+ flexGrow={1}
508
+ border
509
+ borderColor={colors.border}
510
+ padding={1}
511
+ overflow="hidden"
512
+ >
513
+ <text height={1} fg={colors.primary}>
514
+ {padRight("Bucket Data", 30)}
515
+ </text>
516
+ <text height={1} fg={colors.textMuted}>
517
+ {padRight("deltaTokens: " + debugData.lastDeltaTokens.toLocaleString(), 30)}
518
+ </text>
519
+ <text height={1} fg={colors.textMuted}>
520
+ {padRight("dt: " + debugData.lastDt.toFixed(3) + "s", 30)}
521
+ </text>
522
+ <text height={1} fg={colors.textMuted}>
523
+ {padRight("shifted: " + debugData.bucketsShifted, 30)}
524
+ </text>
525
+ <text height={1} fg={colors.textMuted}>
526
+ {padRight("currBucket: " + debugData.currentBucketValue.toFixed(1), 30)}
527
+ </text>
528
+ <text height={1} fg={colors.textMuted}>
529
+ {padRight(
530
+ "buckets[5]: [" +
531
+ sparkData
532
+ .slice(-5)
533
+ .map((v) => v.toFixed(0))
534
+ .join(",") +
535
+ "]",
536
+ 30,
537
+ )}
538
+ </text>
497
539
  </box>
498
540
 
499
- <box flexDirection="column" flexGrow={1} border borderColor={colors.border} padding={1} overflow="hidden">
500
- <text height={1} fg={colors.primary}>{padRight('Activity State', 30)}</text>
501
- <text height={1} fg={colors.textMuted}>{padRight('instantRate: ' + (activity.instantRate || 0).toFixed(1) + '/s', 30)}</text>
502
- <text height={1} fg={colors.textMuted}>{padRight('avgRate: ' + (activity.avgRate || 0).toFixed(1) + '/s', 30)}</text>
503
- <text height={1} fg={colors.textMuted}>{padRight('isSpike: ' + (activity.isSpike ? 'YES' : 'no'), 30)}</text>
541
+ <box
542
+ flexDirection="column"
543
+ flexGrow={1}
544
+ border
545
+ borderColor={colors.border}
546
+ padding={1}
547
+ overflow="hidden"
548
+ >
549
+ <text height={1} fg={colors.primary}>
550
+ {padRight("Activity State", 30)}
551
+ </text>
552
+ <text height={1} fg={colors.textMuted}>
553
+ {padRight("instantRate: " + (activity.instantRate || 0).toFixed(1) + "/s", 30)}
554
+ </text>
555
+ <text height={1} fg={colors.textMuted}>
556
+ {padRight("avgRate: " + (activity.avgRate || 0).toFixed(1) + "/s", 30)}
557
+ </text>
558
+ <text height={1} fg={colors.textMuted}>
559
+ {padRight("isSpike: " + (activity.isSpike ? "YES" : "no"), 30)}
560
+ </text>
504
561
  </box>
505
562
 
506
- <box flexDirection="column" flexGrow={1} border borderColor={colors.border} padding={1} overflow="hidden">
507
- <text height={1} fg={colors.primary}>{padRight('Refresh Stats', 30)}</text>
508
- <text height={1} fg={colors.textMuted}>{padRight('count: ' + String(debugData.refreshCount), 30)}</text>
509
- <text height={1} fg={colors.textMuted}>{padRight('last: ' + new Date(debugData.lastRefreshTime).toLocaleTimeString(), 30)}</text>
510
- <text height={1} fg={colors.textMuted}>{padRight('age: ' + ((now - debugData.lastRefreshTime) / 1000).toFixed(1) + 's', 30)}</text>
511
- <text height={1} fg={colors.textMuted}>{padRight('tokens: ' + totalTokens.toLocaleString(), 30)}</text>
563
+ <box
564
+ flexDirection="column"
565
+ flexGrow={1}
566
+ border
567
+ borderColor={colors.border}
568
+ padding={1}
569
+ overflow="hidden"
570
+ >
571
+ <text height={1} fg={colors.primary}>
572
+ {padRight("Refresh Stats", 30)}
573
+ </text>
574
+ <text height={1} fg={colors.textMuted}>
575
+ {padRight("count: " + String(debugData.refreshCount), 30)}
576
+ </text>
577
+ <text height={1} fg={colors.textMuted}>
578
+ {padRight("last: " + new Date(debugData.lastRefreshTime).toLocaleTimeString(), 30)}
579
+ </text>
580
+ <text height={1} fg={colors.textMuted}>
581
+ {padRight("age: " + ((now - debugData.lastRefreshTime) / 1000).toFixed(1) + "s", 30)}
582
+ </text>
583
+ <text height={1} fg={colors.textMuted}>
584
+ {padRight("tokens: " + totalTokens.toLocaleString(), 30)}
585
+ </text>
512
586
  </box>
513
587
  </box>
514
588
 
515
- <box flexDirection="column" flexGrow={1} marginTop={1} border borderColor={colors.border} padding={1} overflow="hidden">
589
+ <box
590
+ flexDirection="column"
591
+ flexGrow={1}
592
+ marginTop={1}
593
+ border
594
+ borderColor={colors.border}
595
+ padding={1}
596
+ overflow="hidden"
597
+ >
516
598
  <text height={1} fg={colors.primary}>
517
- <strong>Sessions ({sessions.length} total, {activeSessions.length} active)</strong>
599
+ <strong>
600
+ Sessions ({sessions.length} total, {activeSessions.length} active)
601
+ </strong>
518
602
  </text>
519
603
  <box flexDirection="row" marginTop={1} height={1}>
520
- <text width={22} fg={colors.textSubtle}>{padRight('SESSION ID', 22)}</text>
521
- <text width={12} fg={colors.textSubtle}>{padRight('AGENT', 12)}</text>
522
- <text width={10} fg={colors.textSubtle}>{padRight('STATUS', 10)}</text>
523
- <text width={12} fg={colors.textSubtle}>{padRight('TOKENS', 12)}</text>
524
- <text width={10} fg={colors.textSubtle}>{padRight('AGE', 10)}</text>
604
+ <text width={22} fg={colors.textSubtle}>
605
+ {padRight("SESSION ID", 22)}
606
+ </text>
607
+ <text width={12} fg={colors.textSubtle}>
608
+ {padRight("AGENT", 12)}
609
+ </text>
610
+ <text width={10} fg={colors.textSubtle}>
611
+ {padRight("STATUS", 10)}
612
+ </text>
613
+ <text width={12} fg={colors.textSubtle}>
614
+ {padRight("TOKENS", 12)}
615
+ </text>
616
+ <text width={10} fg={colors.textSubtle}>
617
+ {padRight("AGE", 10)}
618
+ </text>
525
619
  </box>
526
620
  <scrollbox ref={scrollboxRef} flexGrow={1}>
527
- {sessions.slice(0, 15).map(s => {
621
+ {sessions.slice(0, 15).map((s) => {
528
622
  const age = now - s.lastActivityAt;
529
- const ageStr = age < 60000 ? `${(age / 1000).toFixed(0)}s` : `${(age / 60000).toFixed(1)}m`;
623
+ const ageStr =
624
+ age < 60000 ? `${(age / 1000).toFixed(0)}s` : `${(age / 60000).toFixed(1)}m`;
530
625
  return (
531
626
  <box key={s.sessionId} flexDirection="row" height={1}>
532
- <text width={22} fg={colors.text}>{padRight(s.sessionId.slice(0, 21), 22)}</text>
533
- <text width={12} fg={colors.text}>{padRight(s.agentName, 12)}</text>
534
- <text width={10} fg={s.status === 'active' ? colors.success : colors.textMuted}>{padRight(s.status, 10)}</text>
535
- <text width={12} fg={colors.text}>{padRight((s.totals.input + s.totals.output).toLocaleString(), 12)}</text>
536
- <text width={10} fg={age < 120000 ? colors.success : colors.textMuted}>{padRight(ageStr, 10)}</text>
627
+ <text width={22} fg={colors.text}>
628
+ {padRight(s.sessionId.slice(0, 21), 22)}
629
+ </text>
630
+ <text width={12} fg={colors.text}>
631
+ {padRight(s.agentName, 12)}
632
+ </text>
633
+ <text width={10} fg={s.status === "active" ? colors.success : colors.textMuted}>
634
+ {padRight(s.status, 10)}
635
+ </text>
636
+ <text width={12} fg={colors.text}>
637
+ {padRight((s.totals.input + s.totals.output).toLocaleString(), 12)}
638
+ </text>
639
+ <text width={10} fg={age < 120000 ? colors.success : colors.textMuted}>
640
+ {padRight(ageStr, 10)}
641
+ </text>
537
642
  </box>
538
643
  );
539
644
  })}
@@ -552,18 +657,21 @@ interface PluginsTabProps {
552
657
  const TYPE_ORDER: Record<string, number> = { provider: 0, agent: 1, theme: 2, notification: 3 };
553
658
 
554
659
  function isOfficialPlugin(pluginId: string, pluginType: string): boolean {
555
- const source = pluginRegistry.getSource(pluginType as 'provider' | 'agent' | 'theme' | 'notification', pluginId);
556
- return source === 'builtin';
660
+ const source = pluginRegistry.getSource(
661
+ pluginType as "provider" | "agent" | "theme" | "notification",
662
+ pluginId,
663
+ );
664
+ return source === "builtin";
557
665
  }
558
666
 
559
667
  function PluginsTab({ colors, scrollboxRef, sortBy }: PluginsTabProps) {
560
668
  const allPlugins = useMemo(() => {
561
669
  const plugins = pluginRegistry.getAllPlugins();
562
670
  return [...plugins].sort((a, b) => {
563
- if (sortBy === 'name') {
671
+ if (sortBy === "name") {
564
672
  return a.name.localeCompare(b.name);
565
673
  }
566
- if (sortBy === 'official') {
674
+ if (sortBy === "official") {
567
675
  const aOfficial = isOfficialPlugin(a.id, a.type) ? 0 : 1;
568
676
  const bOfficial = isOfficialPlugin(b.id, b.type) ? 0 : 1;
569
677
  if (aOfficial !== bOfficial) return aOfficial - bOfficial;
@@ -579,9 +687,7 @@ function PluginsTab({ colors, scrollboxRef, sortBy }: PluginsTabProps) {
579
687
  const [updates, setUpdates] = useState<Map<string, PluginUpdateInfo>>(new Map());
580
688
 
581
689
  useEffect(() => {
582
- const npmPlugins = allPlugins.filter(
583
- (p) => pluginRegistry.getSource(p.type, p.id) === 'npm',
584
- );
690
+ const npmPlugins = allPlugins.filter((p) => pluginRegistry.getSource(p.type, p.id) === "npm");
585
691
  if (npmPlugins.length === 0) return;
586
692
 
587
693
  checkAllPluginUpdates(
@@ -605,8 +711,8 @@ function PluginsTab({ colors, scrollboxRef, sortBy }: PluginsTabProps) {
605
711
  };
606
712
 
607
713
  const summary = Object.entries(typeCounts)
608
- .map(([type, count]) => `${count} ${type}${count !== 1 ? 's' : ''}`)
609
- .join(', ');
714
+ .map(([type, count]) => `${count} ${type}${count !== 1 ? "s" : ""}`)
715
+ .join(", ");
610
716
 
611
717
  const updatesAvailable = [...updates.values()].filter((u) => u.hasUpdate).length;
612
718
 
@@ -615,13 +721,21 @@ function PluginsTab({ colors, scrollboxRef, sortBy }: PluginsTabProps) {
615
721
  <box paddingLeft={1} height={1} flexShrink={0}>
616
722
  <text fg={colors.textMuted}>
617
723
  {allPlugins.length} plugins ({summary})
618
- {updatesAvailable > 0 ? ` · ${updatesAvailable} update${updatesAvailable !== 1 ? 's' : ''} available` : ''}
724
+ {updatesAvailable > 0
725
+ ? ` · ${updatesAvailable} update${updatesAvailable !== 1 ? "s" : ""} available`
726
+ : ""}
619
727
  </text>
620
728
  </box>
621
729
  <box flexDirection="row" paddingX={1} height={1} flexShrink={0}>
622
- <text width={22} fg={colors.textSubtle}>{padRight('PLUGIN', 22)}</text>
623
- <text width={14} fg={colors.textSubtle}>{padRight('TYPE', 14)}</text>
624
- <text width={14} fg={colors.textSubtle}>{padRight('VERSION', 14)}</text>
730
+ <text width={22} fg={colors.textSubtle}>
731
+ {padRight("PLUGIN", 22)}
732
+ </text>
733
+ <text width={14} fg={colors.textSubtle}>
734
+ {padRight("TYPE", 14)}
735
+ </text>
736
+ <text width={14} fg={colors.textSubtle}>
737
+ {padRight("VERSION", 14)}
738
+ </text>
625
739
  <text fg={colors.textSubtle}>DESCRIPTION</text>
626
740
  </box>
627
741
  <scrollbox ref={scrollboxRef} flexGrow={1}>
@@ -631,13 +745,13 @@ function PluginsTab({ colors, scrollboxRef, sortBy }: PluginsTabProps) {
631
745
  const updateInfo = updates.get(key);
632
746
  const official = isOfficialPlugin(plugin.id, plugin.type);
633
747
 
634
- let versionSuffix = '';
748
+ let versionSuffix = "";
635
749
  let versionColor = colors.textMuted;
636
750
  if (updateInfo?.hasUpdate && updateInfo.latestVersion) {
637
- versionSuffix = ' ↑';
751
+ versionSuffix = " ↑";
638
752
  versionColor = colors.warning;
639
753
  } else if (updateInfo && !updateInfo.hasUpdate && updateInfo.latestVersion) {
640
- versionSuffix = ' ✓';
754
+ versionSuffix = " ✓";
641
755
  versionColor = colors.success;
642
756
  }
643
757
 
@@ -647,9 +761,13 @@ function PluginsTab({ colors, scrollboxRef, sortBy }: PluginsTabProps) {
647
761
  {official && <span fg={colors.success}>❖ </span>}
648
762
  <span fg={colors.text}>{padRight(plugin.name, official ? 20 : 22)}</span>
649
763
  </text>
650
- <text width={14} fg={typeColors[plugin.type] ?? colors.textMuted}>{padRight(plugin.type, 14)}</text>
651
- <text width={14} fg={versionColor}>{padRight(plugin.version + versionSuffix, 14)}</text>
652
- <text fg={colors.textSubtle}>{plugin.meta?.description ?? ''}</text>
764
+ <text width={14} fg={typeColors[plugin.type] ?? colors.textMuted}>
765
+ {padRight(plugin.type, 14)}
766
+ </text>
767
+ <text width={14} fg={versionColor}>
768
+ {padRight(plugin.version + versionSuffix, 14)}
769
+ </text>
770
+ <text fg={colors.textSubtle}>{plugin.meta?.description ?? ""}</text>
653
771
  </box>
654
772
  );
655
773
  })}