@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
@@ -0,0 +1,19 @@
1
+ type ShutdownFn = () => Promise<void>;
2
+
3
+ let _shutdown: ShutdownFn | null = null;
4
+ let _inProgress = false;
5
+
6
+ export function registerShutdown(fn: ShutdownFn): void {
7
+ _shutdown = fn;
8
+ }
9
+
10
+ /** Safe to call multiple times — only the first invocation executes. */
11
+ export async function triggerShutdown(): Promise<void> {
12
+ if (_inProgress) return;
13
+ _inProgress = true;
14
+ try {
15
+ await _shutdown?.();
16
+ } catch {
17
+ /* swallow — don't let failed cleanup prevent exit */
18
+ }
19
+ }
@@ -1,4 +1,4 @@
1
- import type { ProviderState } from '../contexts/PluginContext.tsx';
1
+ import type { ProviderState } from "../contexts/PluginContext.tsx";
2
2
 
3
3
  let cachedAliases: Map<string, string> | null = null;
4
4
  let cachedProviderRef: Map<string, ProviderState> | null = null;
@@ -1,27 +1,37 @@
1
- import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
2
- import { useKeyboard, useTerminalDimensions } from '@opentui/react';
3
- import type { ScrollBoxRenderable, BoxRenderable, InputRenderable } from '@opentui/core';
4
- import { useColors } from '../contexts/ThemeContext.tsx';
5
- import { usePlugins, type ProviderState } from '../contexts/PluginContext.tsx';
6
- import { useInputFocus } from '../contexts/InputContext.tsx';
7
- import { ProviderCard } from '../components/ProviderCard.tsx';
8
- import { GhostProviderCard } from '../components/GhostProviderCard.tsx';
9
- import { ProvidersList } from '../components/ProvidersList.tsx';
10
- import { ProviderAggregateStrip } from '../components/ProviderAggregateStrip.tsx';
11
- type SortMode = 'name' | 'usage' | 'status';
12
- type ViewMode = 'cards' | 'list';
1
+ import type { BoxRenderable, InputRenderable, ScrollBoxRenderable } from "@opentui/core";
2
+ import { useKeyboard, useTerminalDimensions } from "@opentui/react";
3
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
+ import { GhostProviderCard } from "../components/GhostProviderCard.tsx";
5
+ import { ProviderAggregateStrip } from "../components/ProviderAggregateStrip.tsx";
6
+ import { ProviderCard } from "../components/ProviderCard.tsx";
7
+ import { ProvidersList } from "../components/ProvidersList.tsx";
8
+ import { useAgentSessions } from "../contexts/AgentSessionContext.tsx";
9
+ import { useInputFocus } from "../contexts/InputContext.tsx";
10
+ import { type ProviderState, usePlugins } from "../contexts/PluginContext.tsx";
11
+ import { useColors } from "../contexts/ThemeContext.tsx";
12
+ import { useSessionEnrichedProviders } from "../hooks/useSessionEnrichedProviders.ts";
13
+
14
+ type SortMode = "name" | "usage" | "status";
15
+ type ViewMode = "cards" | "list";
13
16
 
14
17
  export function Dashboard() {
15
18
  const colors = useColors();
16
19
  const { width: termWidth } = useTerminalDimensions();
17
- const { providers, isInitialized, refreshAllProviders, refreshProvider } = usePlugins();
20
+ const {
21
+ providers: rawProviders,
22
+ isInitialized,
23
+ refreshAllProviders,
24
+ refreshProvider,
25
+ } = usePlugins();
26
+ const { sessions } = useAgentSessions();
27
+ const providers = useSessionEnrichedProviders(rawProviders, sessions);
18
28
  const { setInputFocused } = useInputFocus();
19
29
  const [focusedIndex, setFocusedIndex] = useState<number | null>(null);
20
-
21
- const [filterQuery, setFilterQuery] = useState('');
30
+
31
+ const [filterQuery, setFilterQuery] = useState("");
22
32
  const [isFiltering, setIsFiltering] = useState(false);
23
- const [sortMode, setSortMode] = useState<SortMode>('status');
24
- const [viewMode, setViewMode] = useState<ViewMode>('list');
33
+ const [sortMode, setSortMode] = useState<SortMode>("status");
34
+ const [viewMode, setViewMode] = useState<ViewMode>("list");
25
35
  const [showUnconfigured, setShowUnconfigured] = useState(true);
26
36
  const [expandedIndex, setExpandedIndex] = useState<number | null>(null);
27
37
  const [atRiskFocus, setAtRiskFocus] = useState(false);
@@ -29,10 +39,16 @@ export function Dashboard() {
29
39
  const isFilteringRef = useRef(isFiltering);
30
40
  const expandedIndexRef = useRef(expandedIndex);
31
41
  const focusedIndexRef = useRef(focusedIndex);
32
-
33
- useEffect(() => { isFilteringRef.current = isFiltering; }, [isFiltering]);
34
- useEffect(() => { expandedIndexRef.current = expandedIndex; }, [expandedIndex]);
35
- useEffect(() => { focusedIndexRef.current = focusedIndex; }, [focusedIndex]);
42
+
43
+ useEffect(() => {
44
+ isFilteringRef.current = isFiltering;
45
+ }, [isFiltering]);
46
+ useEffect(() => {
47
+ expandedIndexRef.current = expandedIndex;
48
+ }, [expandedIndex]);
49
+ useEffect(() => {
50
+ focusedIndexRef.current = focusedIndex;
51
+ }, [focusedIndex]);
36
52
 
37
53
  useEffect(() => {
38
54
  setInputFocused(isFiltering);
@@ -52,25 +68,25 @@ export function Dashboard() {
52
68
 
53
69
  const getMaxUsage = useCallback((state: ProviderState) => {
54
70
  if (!state.usage?.limits) return 0;
55
-
71
+
56
72
  const items = state.usage.limits.items ?? [];
57
73
  if (items.length > 0) {
58
74
  return Math.max(...items.map((item) => item.usedPercent ?? 0));
59
75
  }
60
-
76
+
61
77
  const primary = state.usage.limits.primary?.usedPercent || 0;
62
78
  const secondary = state.usage.limits.secondary?.usedPercent || 0;
63
-
79
+
64
80
  return Math.max(primary, secondary);
65
81
  }, []);
66
82
 
67
83
  const providerList = Array.from(providers.values());
68
-
84
+
69
85
  const filteredAndSortedProviders = useMemo(() => {
70
86
  let result = providerList.filter((p) => p.configured);
71
87
 
72
88
  if (atRiskFocus) {
73
- result = result.filter(p => {
89
+ result = result.filter((p) => {
74
90
  if (p.usage?.error) return true;
75
91
  if (p.usage?.limitReached) return true;
76
92
  const maxUsage = getMaxUsage(p);
@@ -80,17 +96,17 @@ export function Dashboard() {
80
96
 
81
97
  if (filterQuery) {
82
98
  const query = filterQuery.toLowerCase();
83
- result = result.filter(p => p.plugin.name.toLowerCase().includes(query));
99
+ result = result.filter((p) => p.plugin.name.toLowerCase().includes(query));
84
100
  }
85
101
 
86
102
  result.sort((a, b) => {
87
103
  switch (sortMode) {
88
- case 'name':
104
+ case "name":
89
105
  return a.plugin.name.localeCompare(b.plugin.name);
90
- case 'usage':
106
+ case "usage":
91
107
  return getMaxUsage(b) - getMaxUsage(a);
92
- case 'status': {
93
- if (a.usage?.limitReached !== b.usage?.limitReached) {
108
+ case "status": {
109
+ if (!!a.usage?.limitReached !== !!b.usage?.limitReached) {
94
110
  return a.usage?.limitReached ? -1 : 1;
95
111
  }
96
112
  if (!!a.usage?.error !== !!b.usage?.error) {
@@ -109,30 +125,31 @@ export function Dashboard() {
109
125
  }, [providerList, filterQuery, sortMode, getMaxUsage, atRiskFocus]);
110
126
 
111
127
  const configured = filteredAndSortedProviders;
112
- const unconfigured = useMemo(() =>
113
- providerList.filter((p) => !p.configured),
114
- [providerList]
115
- );
128
+ const unconfigured = useMemo(() => providerList.filter((p) => !p.configured), [providerList]);
116
129
 
117
- const cycleFocus = useCallback((direction: 1 | -1) => {
118
- const total = configured.length + (showUnconfigured ? unconfigured.length : 0);
119
- if (total === 0) return;
130
+ const cycleFocus = useCallback(
131
+ (direction: 1 | -1) => {
132
+ const includeUnconfigured = viewMode === "cards" && showUnconfigured;
133
+ const total = configured.length + (includeUnconfigured ? unconfigured.length : 0);
134
+ if (total === 0) return;
120
135
 
121
- setFocusedIndex((current) => {
122
- if (current === null) {
123
- return direction === 1 ? 0 : total - 1;
124
- }
125
- const next = current + direction;
126
- if (next < 0) return null;
127
- if (next >= total) return null;
128
- return next;
129
- });
130
- }, [configured.length, unconfigured.length, showUnconfigured]);
136
+ setFocusedIndex((current) => {
137
+ if (current === null) {
138
+ return direction === 1 ? 0 : total - 1;
139
+ }
140
+ const next = current + direction;
141
+ if (next < 0) return total - 1;
142
+ if (next >= total) return 0;
143
+ return next;
144
+ });
145
+ },
146
+ [configured.length, unconfigured.length, showUnconfigured, viewMode],
147
+ );
131
148
 
132
149
  const toggleExpanded = useCallback(() => {
133
150
  const current = focusedIndexRef.current;
134
151
  if (current === null) return;
135
- setExpandedIndex(prev => prev === current ? null : current);
152
+ setExpandedIndex((prev) => (prev === current ? null : current));
136
153
  }, []);
137
154
 
138
155
  const refreshSelected = useCallback(() => {
@@ -146,53 +163,53 @@ export function Dashboard() {
146
163
 
147
164
  useKeyboard((key) => {
148
165
  if (isFilteringRef.current) {
149
- if (key.name === 'escape') {
166
+ if (key.name === "escape") {
150
167
  setIsFiltering(false);
151
- setFilterQuery('');
168
+ setFilterQuery("");
152
169
  return;
153
170
  }
154
- if (key.name === 'enter' || key.name === 'return') {
171
+ if (key.name === "enter" || key.name === "return") {
155
172
  setIsFiltering(false);
156
173
  return;
157
174
  }
158
- return;
175
+ return;
159
176
  }
160
177
 
161
- if (key.name === 'down' || key.name === 'j') {
178
+ if (key.name === "down" || key.name === "j") {
162
179
  cycleFocus(1);
163
- } else if (key.name === 'up' || key.name === 'k') {
180
+ } else if (key.name === "up" || key.name === "k") {
164
181
  cycleFocus(-1);
165
- } else if (key.name === 'tab' && !key.shift) {
182
+ } else if (key.name === "tab" && !key.shift) {
166
183
  cycleFocus(1);
167
- } else if (key.name === 'tab' && key.shift) {
184
+ } else if (key.name === "tab" && key.shift) {
168
185
  cycleFocus(-1);
169
- } else if (key.name === 'escape') {
186
+ } else if (key.name === "escape") {
170
187
  if (expandedIndexRef.current !== null) {
171
188
  setExpandedIndex(null);
172
189
  } else if (focusedIndex !== null) {
173
190
  setFocusedIndex(null);
174
191
  } else if (filterQuery) {
175
- setFilterQuery('');
192
+ setFilterQuery("");
176
193
  }
177
- } else if (key.name === 'enter' || key.name === 'return') {
194
+ } else if (key.name === "enter" || key.name === "return") {
178
195
  toggleExpanded();
179
- } else if (key.name === '/' || key.name === 'f') {
196
+ } else if (key.name === "/" || key.name === "f") {
180
197
  setIsFiltering(true);
181
- } else if (key.name === 's') {
182
- setSortMode(current => {
183
- if (current === 'status') return 'usage';
184
- if (current === 'usage') return 'name';
185
- return 'status';
198
+ } else if (key.name === "s") {
199
+ setSortMode((current) => {
200
+ if (current === "status") return "usage";
201
+ if (current === "usage") return "name";
202
+ return "status";
186
203
  });
187
- } else if (key.name === 'v') {
188
- setViewMode(current => current === 'cards' ? 'list' : 'cards');
189
- } else if (key.name === 'u') {
190
- setShowUnconfigured(current => !current);
191
- } else if (key.name === 'x') {
192
- setAtRiskFocus(current => !current);
193
- } else if (key.sequence === 'R') {
204
+ } else if (key.name === "v") {
205
+ setViewMode((current) => (current === "cards" ? "list" : "cards"));
206
+ } else if (key.name === "u") {
207
+ setShowUnconfigured((current) => !current);
208
+ } else if (key.name === "x") {
209
+ setAtRiskFocus((current) => !current);
210
+ } else if (key.sequence === "R") {
194
211
  refreshAllProviders();
195
- } else if (key.sequence === 'r' && focusedIndex !== null) {
212
+ } else if (key.sequence === "r" && focusedIndex !== null) {
196
213
  refreshSelected();
197
214
  }
198
215
  });
@@ -210,9 +227,18 @@ export function Dashboard() {
210
227
 
211
228
  return (
212
229
  <box flexDirection="column" flexGrow={1} padding={1} gap={0}>
213
- <ProviderAggregateStrip providers={atRiskFocus ? configured : providerList.filter(p => p.configured)} />
230
+ <ProviderAggregateStrip
231
+ providers={atRiskFocus ? configured : providerList.filter((p) => p.configured)}
232
+ />
214
233
 
215
- <box flexDirection="row" gap={2} alignItems="center" height={1} justifyContent="space-between" paddingX={1}>
234
+ <box
235
+ flexDirection="row"
236
+ gap={2}
237
+ alignItems="center"
238
+ height={1}
239
+ justifyContent="space-between"
240
+ paddingX={1}
241
+ >
216
242
  <box flexDirection="row" gap={2} alignItems="center">
217
243
  {isFiltering ? (
218
244
  <box flexDirection="row" gap={1} alignItems="center">
@@ -231,12 +257,12 @@ export function Dashboard() {
231
257
  </box>
232
258
  ) : (
233
259
  <text fg={colors.textSubtle}>
234
- {filterQuery ? `Filter: "${filterQuery}"` : '/ filter'}
260
+ {filterQuery ? `Filter: "${filterQuery}"` : "/ filter"}
235
261
  </text>
236
262
  )}
237
-
263
+
238
264
  <text fg={colors.textSubtle}>|</text>
239
-
265
+
240
266
  <text>
241
267
  <span fg={colors.textSubtle}>Sort: </span>
242
268
  <span fg={colors.primary}>{sortMode.toUpperCase()}</span>
@@ -246,7 +272,7 @@ export function Dashboard() {
246
272
 
247
273
  <text>
248
274
  <span fg={colors.textSubtle}>View: </span>
249
- <span fg={colors.primary}>{viewMode === 'cards' ? 'Cards' : 'Console'}</span>
275
+ <span fg={colors.primary}>{viewMode === "cards" ? "Cards" : "Console"}</span>
250
276
  </text>
251
277
 
252
278
  {atRiskFocus && (
@@ -258,11 +284,12 @@ export function Dashboard() {
258
284
  </box>
259
285
 
260
286
  <text fg={colors.textMuted}>
261
- {totalConfigured} configured{totalUnconfigured > 0 ? `, ${totalUnconfigured} unconfigured` : ''}
287
+ {totalConfigured} configured
288
+ {totalUnconfigured > 0 ? `, ${totalUnconfigured} unconfigured` : ""}
262
289
  </text>
263
290
  </box>
264
291
 
265
- {viewMode === 'list' ? (
292
+ {viewMode === "list" ? (
266
293
  <box flexDirection="column" flexGrow={1}>
267
294
  <ProvidersList
268
295
  providers={configured}
@@ -285,15 +312,12 @@ export function Dashboard() {
285
312
  },
286
313
  }}
287
314
  >
288
- <box
289
- ref={containerRef}
290
- flexDirection="row"
291
- flexWrap="wrap"
292
- gap={1}
293
- >
315
+ <box ref={containerRef} flexDirection="row" flexWrap="wrap" gap={1}>
294
316
  {configured.map((state, index) => (
295
317
  <ProviderCard
296
- ref={(el) => { cardRefs.current[index] = el; }}
318
+ ref={(el) => {
319
+ cardRefs.current[index] = el;
320
+ }}
297
321
  key={state.plugin.id}
298
322
  name={state.plugin.name}
299
323
  configured={state.configured}
@@ -304,22 +328,23 @@ export function Dashboard() {
304
328
  onFocus={() => setFocusedIndex(index)}
305
329
  />
306
330
  ))}
307
-
308
- {showUnconfigured && unconfigured.map((state, index) => (
309
- <GhostProviderCard
310
- key={state.plugin.id}
311
- name={state.plugin.name}
312
- focused={focusedIndex === configured.length + index && !isFiltering}
313
- onFocus={() => setFocusedIndex(configured.length + index)}
314
- />
315
- ))}
331
+
332
+ {showUnconfigured &&
333
+ unconfigured.map((state, index) => (
334
+ <GhostProviderCard
335
+ key={state.plugin.id}
336
+ name={state.plugin.name}
337
+ focused={focusedIndex === configured.length + index && !isFiltering}
338
+ onFocus={() => setFocusedIndex(configured.length + index)}
339
+ />
340
+ ))}
316
341
  </box>
317
342
  </scrollbox>
318
343
  </box>
319
344
  ) : (
320
345
  <box flexGrow={1} justifyContent="center" alignItems="center">
321
346
  <text fg={colors.textMuted}>
322
- {filterQuery ? 'No matching providers found' : 'No configured providers'}
347
+ {filterQuery ? "No matching providers found" : "No configured providers"}
323
348
  </text>
324
349
  </box>
325
350
  )}
@@ -329,10 +354,10 @@ export function Dashboard() {
329
354
  <box flexDirection="row" paddingLeft={1} height={1}>
330
355
  <text fg={colors.textSubtle} height={1}>
331
356
  {isFiltering
332
- ? 'Type to filter Esc cancel Enter apply'
357
+ ? "Type to filter Esc cancel Enter apply"
333
358
  : termWidth < 90
334
- ? '↑↓ nav Enter detail / filter s sort v view u unconf x risk'
335
- : '↑↓ navigate Enter detail / filter s sort v view u unconfigured x at-risk R refresh'}
359
+ ? "↑↓ nav Enter detail / filter s sort v view u unconf x risk"
360
+ : "↑↓ navigate Enter detail / filter s sort v view u unconfigured x at-risk R refresh"}
336
361
  </text>
337
362
  </box>
338
363
  </box>