@tokentop/ttop 0.2.2 → 0.4.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 (160) 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 +42 -38
  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 +36 -23
  19. package/src/plugins/notifications/visual-flash.ts +31 -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 +68 -57
  34. package/src/plugins/registry.ts +80 -71
  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/persistence-service.ts +206 -0
  73. package/src/storage/repos/agentSessions.ts +33 -23
  74. package/src/storage/repos/providerSnapshots.ts +5 -5
  75. package/src/storage/repos/usageEvents.ts +40 -28
  76. package/src/storage/retention.ts +146 -0
  77. package/src/storage/types.ts +5 -5
  78. package/src/tui/App.tsx +180 -137
  79. package/src/tui/components/CommandPalette.tsx +46 -34
  80. package/src/tui/components/DebugConsole.tsx +39 -33
  81. package/src/tui/components/DebugPanel.tsx +314 -183
  82. package/src/tui/components/FooterHints.tsx +2 -2
  83. package/src/tui/components/GhostProviderCard.tsx +37 -35
  84. package/src/tui/components/Header.tsx +68 -35
  85. package/src/tui/components/HelpOverlay.tsx +99 -29
  86. package/src/tui/components/InlineGauge.tsx +10 -7
  87. package/src/tui/components/InlineSparkline.tsx +17 -11
  88. package/src/tui/components/KpiStrip.tsx +128 -98
  89. package/src/tui/components/LimitGauge.tsx +96 -59
  90. package/src/tui/components/ModalBackdrop.tsx +4 -4
  91. package/src/tui/components/ProviderAggregateStrip.tsx +69 -38
  92. package/src/tui/components/ProviderCard.tsx +232 -226
  93. package/src/tui/components/ProviderDetailPanel.tsx +133 -58
  94. package/src/tui/components/ProviderLimitsPanel.tsx +126 -89
  95. package/src/tui/components/ProvidersList.tsx +215 -126
  96. package/src/tui/components/SessionDetailsDrawer.tsx +191 -94
  97. package/src/tui/components/SessionsTable.tsx +320 -169
  98. package/src/tui/components/SettingsModal.tsx +593 -269
  99. package/src/tui/components/SidebarBreakdown.tsx +30 -16
  100. package/src/tui/components/Skeleton.tsx +24 -17
  101. package/src/tui/components/SmartSidebar.tsx +360 -258
  102. package/src/tui/components/Sparkline.tsx +64 -52
  103. package/src/tui/components/Spinner.tsx +3 -3
  104. package/src/tui/components/StatusBar.tsx +27 -27
  105. package/src/tui/components/ThemePicker.tsx +67 -66
  106. package/src/tui/components/Toast.tsx +9 -6
  107. package/src/tui/components/UsageGauge.tsx +10 -13
  108. package/src/tui/components/index.ts +5 -5
  109. package/src/tui/contexts/AgentSessionContext.tsx +164 -151
  110. package/src/tui/contexts/ConfigContext.tsx +15 -13
  111. package/src/tui/contexts/DashboardRuntimeContext.tsx +26 -26
  112. package/src/tui/contexts/DemoModeContext.tsx +22 -15
  113. package/src/tui/contexts/DrawerContext.tsx +11 -9
  114. package/src/tui/contexts/InputContext.tsx +2 -2
  115. package/src/tui/contexts/LogContext.tsx +62 -47
  116. package/src/tui/contexts/PluginContext.tsx +238 -183
  117. package/src/tui/contexts/RealTimeActivityContext.tsx +70 -36
  118. package/src/tui/contexts/StorageContext.tsx +95 -149
  119. package/src/tui/contexts/ThemeContext.tsx +21 -16
  120. package/src/tui/contexts/TimeWindowContext.tsx +72 -61
  121. package/src/tui/contexts/ToastContext.tsx +10 -7
  122. package/src/tui/contexts/index.ts +9 -5
  123. package/src/tui/createApp.tsx +8 -12
  124. package/src/tui/debug/captureFrame.ts +25 -28
  125. package/src/tui/debug/snapshot.tsx +179 -162
  126. package/src/tui/driver/assertions.ts +30 -22
  127. package/src/tui/driver/cli.ts +231 -221
  128. package/src/tui/driver/coverage.ts +27 -44
  129. package/src/tui/driver/demo-snapshot.ts +33 -33
  130. package/src/tui/driver/diff.ts +26 -32
  131. package/src/tui/driver/driver.ts +43 -48
  132. package/src/tui/driver/index.ts +36 -40
  133. package/src/tui/driver/recorder.ts +37 -48
  134. package/src/tui/driver/test-workflow.ts +51 -51
  135. package/src/tui/driver/test.ts +25 -25
  136. package/src/tui/hooks/index.ts +24 -20
  137. package/src/tui/hooks/useAnimatedValue.ts +13 -18
  138. package/src/tui/hooks/useDashboardKeyboard.ts +118 -98
  139. package/src/tui/hooks/useDashboardState.ts +26 -24
  140. package/src/tui/hooks/useEmaActivity.ts +82 -74
  141. package/src/tui/hooks/useEntranceAnimation.ts +11 -11
  142. package/src/tui/hooks/useExitAnimation.ts +7 -3
  143. package/src/tui/hooks/usePulse.ts +4 -4
  144. package/src/tui/hooks/useSafeRenderer.ts +5 -5
  145. package/src/tui/hooks/useSessionEnrichedProviders.ts +150 -0
  146. package/src/tui/hooks/useValueFlash.ts +10 -14
  147. package/src/tui/index.tsx +29 -6
  148. package/src/tui/shutdown.ts +19 -0
  149. package/src/tui/utils/providerColor.ts +1 -1
  150. package/src/tui/views/Dashboard.tsx +164 -102
  151. package/src/tui/views/HistoricalTrendsView.tsx +579 -250
  152. package/src/tui/views/ProjectsView.tsx +495 -243
  153. package/src/tui/views/RealTimeDashboard.tsx +176 -116
  154. package/src/tui/views/SettingsView.tsx +225 -189
  155. package/src/tui/views/index.ts +2 -2
  156. package/src/types/opentui-test-utils.d.ts +7 -7
  157. package/src/utils/clipboard.ts +29 -29
  158. package/src/utils/currency.test.ts +53 -53
  159. package/src/utils/currency.ts +35 -32
  160. 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, useLayoutEffect, 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,11 +39,17 @@ 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
+ const selectedProviderIdRef = useRef<string | null>(null);
36
43
 
44
+ useEffect(() => {
45
+ isFilteringRef.current = isFiltering;
46
+ }, [isFiltering]);
47
+ useEffect(() => {
48
+ expandedIndexRef.current = expandedIndex;
49
+ }, [expandedIndex]);
50
+ useEffect(() => {
51
+ focusedIndexRef.current = focusedIndex;
52
+ }, [focusedIndex]);
37
53
  useEffect(() => {
38
54
  setInputFocused(isFiltering);
39
55
  return () => setInputFocused(false);
@@ -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
- const providerList = Array.from(providers.values());
68
-
83
+ const providerList = useMemo(() => Array.from(providers.values()), [providers]);
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,68 @@ 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
+ // Stabilize selection across re-sorts: track selected provider by ID
131
+ // so the highlight follows the same provider when live data causes reordering.
132
+ useLayoutEffect(() => {
133
+ if (focusedIndex === null) return;
120
134
 
121
- setFocusedIndex((current) => {
122
- if (current === null) {
123
- return direction === 1 ? 0 : total - 1;
135
+ if (selectedProviderIdRef.current && focusedIndex < configured.length) {
136
+ // Early return if current index already points to tracked provider
137
+ // (prevents infinite re-render loop when providerList creates new array refs)
138
+ if (configured[focusedIndex]?.plugin.id === selectedProviderIdRef.current) return;
139
+
140
+ const newIndex = configured.findIndex((p) => p.plugin.id === selectedProviderIdRef.current);
141
+ if (newIndex !== -1) {
142
+ if (newIndex !== focusedIndex) setFocusedIndex(newIndex);
143
+ return;
124
144
  }
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]);
145
+ selectedProviderIdRef.current = null;
146
+ }
147
+
148
+ const includeUnconfigured = viewMode === "cards" && showUnconfigured;
149
+ const total = configured.length + (includeUnconfigured ? unconfigured.length : 0);
150
+ if (total === 0) {
151
+ setFocusedIndex(null);
152
+ } else if (focusedIndex >= total) {
153
+ setFocusedIndex(total - 1);
154
+ }
155
+ }, [filteredAndSortedProviders, unconfigured.length, showUnconfigured, viewMode]);
156
+
157
+ // Record which provider the user selected
158
+ useEffect(() => {
159
+ if (focusedIndex !== null && focusedIndex < configured.length) {
160
+ const provider = configured[focusedIndex];
161
+ if (provider) selectedProviderIdRef.current = provider.plugin.id;
162
+ } else {
163
+ selectedProviderIdRef.current = null;
164
+ }
165
+ }, [focusedIndex, configured]);
166
+
167
+ const cycleFocus = useCallback(
168
+ (direction: 1 | -1) => {
169
+ const includeUnconfigured = viewMode === "cards" && showUnconfigured;
170
+ const total = configured.length + (includeUnconfigured ? unconfigured.length : 0);
171
+ if (total === 0) return;
172
+
173
+ setFocusedIndex((current) => {
174
+ if (current === null) {
175
+ return direction === 1 ? 0 : total - 1;
176
+ }
177
+ const next = current + direction;
178
+ if (next < 0) return total - 1;
179
+ if (next >= total) return 0;
180
+ return next;
181
+ });
182
+ },
183
+ [configured.length, unconfigured.length, showUnconfigured, viewMode],
184
+ );
131
185
 
132
186
  const toggleExpanded = useCallback(() => {
133
187
  const current = focusedIndexRef.current;
134
188
  if (current === null) return;
135
- setExpandedIndex(prev => prev === current ? null : current);
189
+ setExpandedIndex((prev) => (prev === current ? null : current));
136
190
  }, []);
137
191
 
138
192
  const refreshSelected = useCallback(() => {
@@ -146,53 +200,53 @@ export function Dashboard() {
146
200
 
147
201
  useKeyboard((key) => {
148
202
  if (isFilteringRef.current) {
149
- if (key.name === 'escape') {
203
+ if (key.name === "escape") {
150
204
  setIsFiltering(false);
151
- setFilterQuery('');
205
+ setFilterQuery("");
152
206
  return;
153
207
  }
154
- if (key.name === 'enter' || key.name === 'return') {
208
+ if (key.name === "enter" || key.name === "return") {
155
209
  setIsFiltering(false);
156
210
  return;
157
211
  }
158
- return;
212
+ return;
159
213
  }
160
214
 
161
- if (key.name === 'down' || key.name === 'j') {
215
+ if (key.name === "down" || key.name === "j") {
162
216
  cycleFocus(1);
163
- } else if (key.name === 'up' || key.name === 'k') {
217
+ } else if (key.name === "up" || key.name === "k") {
164
218
  cycleFocus(-1);
165
- } else if (key.name === 'tab' && !key.shift) {
219
+ } else if (key.name === "tab" && !key.shift) {
166
220
  cycleFocus(1);
167
- } else if (key.name === 'tab' && key.shift) {
221
+ } else if (key.name === "tab" && key.shift) {
168
222
  cycleFocus(-1);
169
- } else if (key.name === 'escape') {
223
+ } else if (key.name === "escape") {
170
224
  if (expandedIndexRef.current !== null) {
171
225
  setExpandedIndex(null);
172
226
  } else if (focusedIndex !== null) {
173
227
  setFocusedIndex(null);
174
228
  } else if (filterQuery) {
175
- setFilterQuery('');
229
+ setFilterQuery("");
176
230
  }
177
- } else if (key.name === 'enter' || key.name === 'return') {
231
+ } else if (key.name === "enter" || key.name === "return") {
178
232
  toggleExpanded();
179
- } else if (key.name === '/' || key.name === 'f') {
233
+ } else if (key.name === "/" || key.name === "f") {
180
234
  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';
235
+ } else if (key.name === "s") {
236
+ setSortMode((current) => {
237
+ if (current === "status") return "usage";
238
+ if (current === "usage") return "name";
239
+ return "status";
186
240
  });
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') {
241
+ } else if (key.name === "v") {
242
+ setViewMode((current) => (current === "cards" ? "list" : "cards"));
243
+ } else if (key.name === "u") {
244
+ setShowUnconfigured((current) => !current);
245
+ } else if (key.name === "x") {
246
+ setAtRiskFocus((current) => !current);
247
+ } else if (key.sequence === "R") {
194
248
  refreshAllProviders();
195
- } else if (key.sequence === 'r' && focusedIndex !== null) {
249
+ } else if (key.sequence === "r" && focusedIndex !== null) {
196
250
  refreshSelected();
197
251
  }
198
252
  });
@@ -210,9 +264,18 @@ export function Dashboard() {
210
264
 
211
265
  return (
212
266
  <box flexDirection="column" flexGrow={1} padding={1} gap={0}>
213
- <ProviderAggregateStrip providers={atRiskFocus ? configured : providerList.filter(p => p.configured)} />
267
+ <ProviderAggregateStrip
268
+ providers={atRiskFocus ? configured : providerList.filter((p) => p.configured)}
269
+ />
214
270
 
215
- <box flexDirection="row" gap={2} alignItems="center" height={1} justifyContent="space-between" paddingX={1}>
271
+ <box
272
+ flexDirection="row"
273
+ gap={2}
274
+ alignItems="center"
275
+ height={1}
276
+ justifyContent="space-between"
277
+ paddingX={1}
278
+ >
216
279
  <box flexDirection="row" gap={2} alignItems="center">
217
280
  {isFiltering ? (
218
281
  <box flexDirection="row" gap={1} alignItems="center">
@@ -231,12 +294,12 @@ export function Dashboard() {
231
294
  </box>
232
295
  ) : (
233
296
  <text fg={colors.textSubtle}>
234
- {filterQuery ? `Filter: "${filterQuery}"` : '/ filter'}
297
+ {filterQuery ? `Filter: "${filterQuery}"` : "/ filter"}
235
298
  </text>
236
299
  )}
237
-
300
+
238
301
  <text fg={colors.textSubtle}>|</text>
239
-
302
+
240
303
  <text>
241
304
  <span fg={colors.textSubtle}>Sort: </span>
242
305
  <span fg={colors.primary}>{sortMode.toUpperCase()}</span>
@@ -246,7 +309,7 @@ export function Dashboard() {
246
309
 
247
310
  <text>
248
311
  <span fg={colors.textSubtle}>View: </span>
249
- <span fg={colors.primary}>{viewMode === 'cards' ? 'Cards' : 'Console'}</span>
312
+ <span fg={colors.primary}>{viewMode === "cards" ? "Cards" : "Console"}</span>
250
313
  </text>
251
314
 
252
315
  {atRiskFocus && (
@@ -258,11 +321,12 @@ export function Dashboard() {
258
321
  </box>
259
322
 
260
323
  <text fg={colors.textMuted}>
261
- {totalConfigured} configured{totalUnconfigured > 0 ? `, ${totalUnconfigured} unconfigured` : ''}
324
+ {totalConfigured} configured
325
+ {totalUnconfigured > 0 ? `, ${totalUnconfigured} unconfigured` : ""}
262
326
  </text>
263
327
  </box>
264
328
 
265
- {viewMode === 'list' ? (
329
+ {viewMode === "list" ? (
266
330
  <box flexDirection="column" flexGrow={1}>
267
331
  <ProvidersList
268
332
  providers={configured}
@@ -285,15 +349,12 @@ export function Dashboard() {
285
349
  },
286
350
  }}
287
351
  >
288
- <box
289
- ref={containerRef}
290
- flexDirection="row"
291
- flexWrap="wrap"
292
- gap={1}
293
- >
352
+ <box ref={containerRef} flexDirection="row" flexWrap="wrap" gap={1}>
294
353
  {configured.map((state, index) => (
295
354
  <ProviderCard
296
- ref={(el) => { cardRefs.current[index] = el; }}
355
+ ref={(el) => {
356
+ cardRefs.current[index] = el;
357
+ }}
297
358
  key={state.plugin.id}
298
359
  name={state.plugin.name}
299
360
  configured={state.configured}
@@ -304,22 +365,23 @@ export function Dashboard() {
304
365
  onFocus={() => setFocusedIndex(index)}
305
366
  />
306
367
  ))}
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
- ))}
368
+
369
+ {showUnconfigured &&
370
+ unconfigured.map((state, index) => (
371
+ <GhostProviderCard
372
+ key={state.plugin.id}
373
+ name={state.plugin.name}
374
+ focused={focusedIndex === configured.length + index && !isFiltering}
375
+ onFocus={() => setFocusedIndex(configured.length + index)}
376
+ />
377
+ ))}
316
378
  </box>
317
379
  </scrollbox>
318
380
  </box>
319
381
  ) : (
320
382
  <box flexGrow={1} justifyContent="center" alignItems="center">
321
383
  <text fg={colors.textMuted}>
322
- {filterQuery ? 'No matching providers found' : 'No configured providers'}
384
+ {filterQuery ? "No matching providers found" : "No configured providers"}
323
385
  </text>
324
386
  </box>
325
387
  )}
@@ -329,10 +391,10 @@ export function Dashboard() {
329
391
  <box flexDirection="row" paddingLeft={1} height={1}>
330
392
  <text fg={colors.textSubtle} height={1}>
331
393
  {isFiltering
332
- ? 'Type to filter Esc cancel Enter apply'
394
+ ? "Type to filter Esc cancel Enter apply"
333
395
  : 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'}
396
+ ? "↑↓ nav Enter detail / filter s sort v view u unconf x risk"
397
+ : "↑↓ navigate Enter detail / filter s sort v view u unconfigured x at-risk R refresh"}
336
398
  </text>
337
399
  </box>
338
400
  </box>