@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.
- package/README.md +2 -5
- package/package.json +8 -5
- package/src/agents/aggregator.ts +39 -19
- package/src/agents/costing.ts +11 -9
- package/src/agents/index.ts +7 -9
- package/src/agents/types.ts +2 -2
- package/src/cli.ts +28 -23
- package/src/config/schema.test.ts +42 -40
- package/src/config/schema.ts +26 -26
- package/src/demo/simulator.ts +161 -122
- package/src/plugins/agents/index.ts +2 -2
- package/src/plugins/auth-sources.ts +42 -38
- package/src/plugins/lifecycle.ts +42 -38
- package/src/plugins/loader.test.ts +49 -49
- package/src/plugins/loader.ts +67 -63
- package/src/plugins/notification-bus.ts +21 -16
- package/src/plugins/notifications/index.ts +2 -2
- package/src/plugins/notifications/terminal-bell.ts +36 -23
- package/src/plugins/notifications/visual-flash.ts +31 -19
- package/src/plugins/npm-installer.ts +23 -25
- package/src/plugins/plugin-context-factory.ts +10 -10
- package/src/plugins/plugin-host.ts +12 -5
- package/src/plugins/providers/anthropic.ts +64 -57
- package/src/plugins/providers/antigravity.ts +81 -69
- package/src/plugins/providers/codex.ts +42 -42
- package/src/plugins/providers/gemini.ts +106 -87
- package/src/plugins/providers/github-copilot.ts +49 -45
- package/src/plugins/providers/index.ts +10 -12
- package/src/plugins/providers/minimax.ts +50 -44
- package/src/plugins/providers/openai-api.ts +82 -62
- package/src/plugins/providers/opencode-zen.ts +46 -39
- package/src/plugins/providers/perplexity.ts +55 -46
- package/src/plugins/providers/zai.ts +68 -57
- package/src/plugins/registry.ts +80 -71
- package/src/plugins/sandbox-guard.test.ts +45 -48
- package/src/plugins/sandbox-guard.ts +20 -25
- package/src/plugins/sandbox.ts +26 -48
- package/src/plugins/themes/catppuccin-latte.ts +29 -29
- package/src/plugins/themes/catppuccin-mocha.ts +29 -29
- package/src/plugins/themes/claude-code.ts +29 -29
- package/src/plugins/themes/dracula.ts +29 -29
- package/src/plugins/themes/github-light.ts +29 -29
- package/src/plugins/themes/gruvbox-dark.ts +29 -29
- package/src/plugins/themes/gruvbox-light.ts +29 -29
- package/src/plugins/themes/index.ts +15 -15
- package/src/plugins/themes/kanagawa.ts +29 -29
- package/src/plugins/themes/nord.ts +29 -29
- package/src/plugins/themes/one-dark.ts +29 -29
- package/src/plugins/themes/opencode.ts +31 -31
- package/src/plugins/themes/rose-pine-dawn.ts +29 -29
- package/src/plugins/themes/rose-pine.ts +29 -29
- package/src/plugins/themes/solarized-light.ts +29 -29
- package/src/plugins/themes/tokyo-night.ts +29 -29
- package/src/plugins/types/agent.ts +5 -5
- package/src/plugins/types/base.ts +35 -24
- package/src/plugins/types/index.ts +9 -9
- package/src/plugins/types/notification.ts +12 -12
- package/src/plugins/types/provider.ts +12 -8
- package/src/plugins/types/theme.ts +31 -23
- package/src/plugins/update-checker.ts +6 -6
- package/src/pricing/estimator.test.ts +39 -44
- package/src/pricing/estimator.ts +7 -13
- package/src/pricing/fallback.test.ts +30 -34
- package/src/pricing/fallback.ts +54 -27
- package/src/pricing/index.ts +18 -12
- package/src/pricing/models-dev.ts +6 -6
- package/src/storage/database.ts +56 -46
- package/src/storage/db.ts +12 -12
- package/src/storage/index.ts +44 -45
- package/src/storage/migrations/index.ts +2 -2
- package/src/storage/paths.ts +13 -13
- package/src/storage/persistence-service.ts +206 -0
- package/src/storage/repos/agentSessions.ts +33 -23
- package/src/storage/repos/providerSnapshots.ts +5 -5
- package/src/storage/repos/usageEvents.ts +40 -28
- package/src/storage/retention.ts +146 -0
- package/src/storage/types.ts +5 -5
- package/src/tui/App.tsx +180 -137
- package/src/tui/components/CommandPalette.tsx +46 -34
- package/src/tui/components/DebugConsole.tsx +39 -33
- package/src/tui/components/DebugPanel.tsx +314 -183
- package/src/tui/components/FooterHints.tsx +2 -2
- package/src/tui/components/GhostProviderCard.tsx +37 -35
- package/src/tui/components/Header.tsx +68 -35
- package/src/tui/components/HelpOverlay.tsx +99 -29
- package/src/tui/components/InlineGauge.tsx +10 -7
- package/src/tui/components/InlineSparkline.tsx +17 -11
- package/src/tui/components/KpiStrip.tsx +128 -98
- package/src/tui/components/LimitGauge.tsx +96 -59
- package/src/tui/components/ModalBackdrop.tsx +4 -4
- package/src/tui/components/ProviderAggregateStrip.tsx +69 -38
- package/src/tui/components/ProviderCard.tsx +232 -226
- package/src/tui/components/ProviderDetailPanel.tsx +133 -58
- package/src/tui/components/ProviderLimitsPanel.tsx +126 -89
- package/src/tui/components/ProvidersList.tsx +215 -126
- package/src/tui/components/SessionDetailsDrawer.tsx +191 -94
- package/src/tui/components/SessionsTable.tsx +320 -169
- package/src/tui/components/SettingsModal.tsx +593 -269
- package/src/tui/components/SidebarBreakdown.tsx +30 -16
- package/src/tui/components/Skeleton.tsx +24 -17
- package/src/tui/components/SmartSidebar.tsx +360 -258
- package/src/tui/components/Sparkline.tsx +64 -52
- package/src/tui/components/Spinner.tsx +3 -3
- package/src/tui/components/StatusBar.tsx +27 -27
- package/src/tui/components/ThemePicker.tsx +67 -66
- package/src/tui/components/Toast.tsx +9 -6
- package/src/tui/components/UsageGauge.tsx +10 -13
- package/src/tui/components/index.ts +5 -5
- package/src/tui/contexts/AgentSessionContext.tsx +164 -151
- package/src/tui/contexts/ConfigContext.tsx +15 -13
- package/src/tui/contexts/DashboardRuntimeContext.tsx +26 -26
- package/src/tui/contexts/DemoModeContext.tsx +22 -15
- package/src/tui/contexts/DrawerContext.tsx +11 -9
- package/src/tui/contexts/InputContext.tsx +2 -2
- package/src/tui/contexts/LogContext.tsx +62 -47
- package/src/tui/contexts/PluginContext.tsx +238 -183
- package/src/tui/contexts/RealTimeActivityContext.tsx +70 -36
- package/src/tui/contexts/StorageContext.tsx +95 -149
- package/src/tui/contexts/ThemeContext.tsx +21 -16
- package/src/tui/contexts/TimeWindowContext.tsx +72 -61
- package/src/tui/contexts/ToastContext.tsx +10 -7
- package/src/tui/contexts/index.ts +9 -5
- package/src/tui/createApp.tsx +8 -12
- package/src/tui/debug/captureFrame.ts +25 -28
- package/src/tui/debug/snapshot.tsx +179 -162
- package/src/tui/driver/assertions.ts +30 -22
- package/src/tui/driver/cli.ts +231 -221
- package/src/tui/driver/coverage.ts +27 -44
- package/src/tui/driver/demo-snapshot.ts +33 -33
- package/src/tui/driver/diff.ts +26 -32
- package/src/tui/driver/driver.ts +43 -48
- package/src/tui/driver/index.ts +36 -40
- package/src/tui/driver/recorder.ts +37 -48
- package/src/tui/driver/test-workflow.ts +51 -51
- package/src/tui/driver/test.ts +25 -25
- package/src/tui/hooks/index.ts +24 -20
- package/src/tui/hooks/useAnimatedValue.ts +13 -18
- package/src/tui/hooks/useDashboardKeyboard.ts +118 -98
- package/src/tui/hooks/useDashboardState.ts +26 -24
- package/src/tui/hooks/useEmaActivity.ts +82 -74
- package/src/tui/hooks/useEntranceAnimation.ts +11 -11
- package/src/tui/hooks/useExitAnimation.ts +7 -3
- package/src/tui/hooks/usePulse.ts +4 -4
- package/src/tui/hooks/useSafeRenderer.ts +5 -5
- package/src/tui/hooks/useSessionEnrichedProviders.ts +150 -0
- package/src/tui/hooks/useValueFlash.ts +10 -14
- package/src/tui/index.tsx +29 -6
- package/src/tui/shutdown.ts +19 -0
- package/src/tui/utils/providerColor.ts +1 -1
- package/src/tui/views/Dashboard.tsx +164 -102
- package/src/tui/views/HistoricalTrendsView.tsx +579 -250
- package/src/tui/views/ProjectsView.tsx +495 -243
- package/src/tui/views/RealTimeDashboard.tsx +176 -116
- package/src/tui/views/SettingsView.tsx +225 -189
- package/src/tui/views/index.ts +2 -2
- package/src/types/opentui-test-utils.d.ts +7 -7
- package/src/utils/clipboard.ts +29 -29
- package/src/utils/currency.test.ts +53 -53
- package/src/utils/currency.ts +35 -32
- 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,27 +1,37 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { useKeyboard, useTerminalDimensions } from
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
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 {
|
|
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>(
|
|
24
|
-
const [viewMode, setViewMode] = useState<ViewMode>(
|
|
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
|
|
104
|
+
case "name":
|
|
89
105
|
return a.plugin.name.localeCompare(b.plugin.name);
|
|
90
|
-
case
|
|
106
|
+
case "usage":
|
|
91
107
|
return getMaxUsage(b) - getMaxUsage(a);
|
|
92
|
-
case
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
122
|
-
if
|
|
123
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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 ===
|
|
203
|
+
if (key.name === "escape") {
|
|
150
204
|
setIsFiltering(false);
|
|
151
|
-
setFilterQuery(
|
|
205
|
+
setFilterQuery("");
|
|
152
206
|
return;
|
|
153
207
|
}
|
|
154
|
-
if (key.name ===
|
|
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 ===
|
|
215
|
+
if (key.name === "down" || key.name === "j") {
|
|
162
216
|
cycleFocus(1);
|
|
163
|
-
} else if (key.name ===
|
|
217
|
+
} else if (key.name === "up" || key.name === "k") {
|
|
164
218
|
cycleFocus(-1);
|
|
165
|
-
} else if (key.name ===
|
|
219
|
+
} else if (key.name === "tab" && !key.shift) {
|
|
166
220
|
cycleFocus(1);
|
|
167
|
-
} else if (key.name ===
|
|
221
|
+
} else if (key.name === "tab" && key.shift) {
|
|
168
222
|
cycleFocus(-1);
|
|
169
|
-
} else if (key.name ===
|
|
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 ===
|
|
231
|
+
} else if (key.name === "enter" || key.name === "return") {
|
|
178
232
|
toggleExpanded();
|
|
179
|
-
} else if (key.name ===
|
|
233
|
+
} else if (key.name === "/" || key.name === "f") {
|
|
180
234
|
setIsFiltering(true);
|
|
181
|
-
} else if (key.name ===
|
|
182
|
-
setSortMode(current => {
|
|
183
|
-
if (current ===
|
|
184
|
-
if (current ===
|
|
185
|
-
return
|
|
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 ===
|
|
188
|
-
setViewMode(current => current ===
|
|
189
|
-
} else if (key.name ===
|
|
190
|
-
setShowUnconfigured(current => !current);
|
|
191
|
-
} else if (key.name ===
|
|
192
|
-
setAtRiskFocus(current => !current);
|
|
193
|
-
} else if (key.sequence ===
|
|
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 ===
|
|
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
|
|
267
|
+
<ProviderAggregateStrip
|
|
268
|
+
providers={atRiskFocus ? configured : providerList.filter((p) => p.configured)}
|
|
269
|
+
/>
|
|
214
270
|
|
|
215
|
-
<box
|
|
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}"` :
|
|
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 ===
|
|
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
|
|
324
|
+
{totalConfigured} configured
|
|
325
|
+
{totalUnconfigured > 0 ? `, ${totalUnconfigured} unconfigured` : ""}
|
|
262
326
|
</text>
|
|
263
327
|
</box>
|
|
264
328
|
|
|
265
|
-
{viewMode ===
|
|
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) => {
|
|
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 &&
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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 ?
|
|
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
|
-
?
|
|
394
|
+
? "Type to filter Esc cancel Enter apply"
|
|
333
395
|
: termWidth < 90
|
|
334
|
-
?
|
|
335
|
-
:
|
|
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>
|