@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.
- 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 +32 -32
- 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 +29 -23
- package/src/plugins/notifications/visual-flash.ts +21 -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 +66 -57
- package/src/plugins/registry.ts +47 -43
- 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/repos/agentSessions.ts +33 -23
- package/src/storage/repos/providerSnapshots.ts +5 -5
- package/src/storage/repos/usageEvents.ts +40 -28
- 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 +295 -177
- 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 +313 -166
- package/src/tui/components/SettingsModal.tsx +231 -214
- 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 +193 -144
- 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 +232 -183
- package/src/tui/contexts/RealTimeActivityContext.tsx +70 -36
- package/src/tui/contexts/StorageContext.tsx +151 -117
- 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 +3 -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 +127 -102
- package/src/tui/views/HistoricalTrendsView.tsx +579 -250
- package/src/tui/views/ProjectsView.tsx +466 -237
- package/src/tui/views/RealTimeDashboard.tsx +150 -111
- package/src/tui/views/SettingsView.tsx +222 -191
- 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
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { useConfig } from
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import
|
|
1
|
+
import { useKeyboard, useTerminalDimensions } from "@opentui/react";
|
|
2
|
+
import { useCallback, useMemo, useState } from "react";
|
|
3
|
+
import type { AppConfig } from "@/config/schema.ts";
|
|
4
|
+
import type { ConfigField } from "@/plugins/types/base.ts";
|
|
5
|
+
import { useConfig } from "../contexts/ConfigContext.tsx";
|
|
6
|
+
import { useDemoMode } from "../contexts/DemoModeContext.tsx";
|
|
7
|
+
import { usePlugins } from "../contexts/PluginContext.tsx";
|
|
8
|
+
import { useColors } from "../contexts/ThemeContext.tsx";
|
|
9
|
+
import { useToastContext } from "../contexts/ToastContext.tsx";
|
|
10
10
|
|
|
11
|
-
type SettingCategory =
|
|
11
|
+
type SettingCategory = "refresh" | "display" | "budgets" | "alerts" | "notifications" | "plugins";
|
|
12
12
|
|
|
13
13
|
interface SettingItem {
|
|
14
14
|
key: string;
|
|
15
15
|
label: string;
|
|
16
16
|
category: SettingCategory;
|
|
17
|
-
type:
|
|
17
|
+
type: "toggle" | "select" | "number";
|
|
18
18
|
options?: string[];
|
|
19
19
|
getValue: (config: AppConfig) => string | number | boolean;
|
|
20
20
|
setValue: (config: AppConfig, value: string | number | boolean) => AppConfig;
|
|
@@ -22,158 +22,190 @@ interface SettingItem {
|
|
|
22
22
|
|
|
23
23
|
const SETTINGS: SettingItem[] = [
|
|
24
24
|
{
|
|
25
|
-
key:
|
|
26
|
-
label:
|
|
27
|
-
category:
|
|
28
|
-
type:
|
|
29
|
-
options: [
|
|
25
|
+
key: "intervalMs",
|
|
26
|
+
label: "Refresh Interval",
|
|
27
|
+
category: "refresh",
|
|
28
|
+
type: "select",
|
|
29
|
+
options: ["30s", "1m", "2m", "5m"],
|
|
30
30
|
getValue: (c) => {
|
|
31
31
|
const ms = c.refresh.intervalMs;
|
|
32
|
-
if (ms <= 30000) return
|
|
33
|
-
if (ms <= 60000) return
|
|
34
|
-
if (ms <= 120000) return
|
|
35
|
-
return
|
|
32
|
+
if (ms <= 30000) return "30s";
|
|
33
|
+
if (ms <= 60000) return "1m";
|
|
34
|
+
if (ms <= 120000) return "2m";
|
|
35
|
+
return "5m";
|
|
36
36
|
},
|
|
37
37
|
setValue: (c, v) => {
|
|
38
|
-
const map: Record<string, number> = {
|
|
38
|
+
const map: Record<string, number> = { "30s": 30000, "1m": 60000, "2m": 120000, "5m": 300000 };
|
|
39
39
|
return { ...c, refresh: { ...c.refresh, intervalMs: map[v as string] ?? 60000 } };
|
|
40
40
|
},
|
|
41
41
|
},
|
|
42
42
|
{
|
|
43
|
-
key:
|
|
44
|
-
label:
|
|
45
|
-
category:
|
|
46
|
-
type:
|
|
43
|
+
key: "pauseAutoRefresh",
|
|
44
|
+
label: "Pause Auto-Refresh",
|
|
45
|
+
category: "refresh",
|
|
46
|
+
type: "toggle",
|
|
47
47
|
getValue: (c) => c.refresh.pauseAutoRefresh,
|
|
48
48
|
setValue: (c, v) => ({ ...c, refresh: { ...c.refresh, pauseAutoRefresh: v as boolean } }),
|
|
49
49
|
},
|
|
50
50
|
{
|
|
51
|
-
key:
|
|
52
|
-
label:
|
|
53
|
-
category:
|
|
54
|
-
type:
|
|
55
|
-
options: [
|
|
51
|
+
key: "defaultTimeWindow",
|
|
52
|
+
label: "Default Time Window",
|
|
53
|
+
category: "display",
|
|
54
|
+
type: "select",
|
|
55
|
+
options: ["5m", "15m", "1h", "24h", "7d", "30d", "all"],
|
|
56
56
|
getValue: (c) => c.display.defaultTimeWindow,
|
|
57
|
-
setValue: (c, v) => ({
|
|
57
|
+
setValue: (c, v) => ({
|
|
58
|
+
...c,
|
|
59
|
+
display: { ...c.display, defaultTimeWindow: v as AppConfig["display"]["defaultTimeWindow"] },
|
|
60
|
+
}),
|
|
58
61
|
},
|
|
59
62
|
{
|
|
60
|
-
key:
|
|
61
|
-
label:
|
|
62
|
-
category:
|
|
63
|
-
type:
|
|
63
|
+
key: "sidebarCollapsed",
|
|
64
|
+
label: "Sidebar Collapsed",
|
|
65
|
+
category: "display",
|
|
66
|
+
type: "toggle",
|
|
64
67
|
getValue: (c) => c.display.sidebarCollapsed,
|
|
65
68
|
setValue: (c, v) => ({ ...c, display: { ...c.display, sidebarCollapsed: v as boolean } }),
|
|
66
69
|
},
|
|
67
70
|
{
|
|
68
|
-
key:
|
|
69
|
-
label:
|
|
70
|
-
category:
|
|
71
|
-
type:
|
|
72
|
-
options: [
|
|
71
|
+
key: "timeFormat",
|
|
72
|
+
label: "Time Format",
|
|
73
|
+
category: "display",
|
|
74
|
+
type: "select",
|
|
75
|
+
options: ["12h", "24h"],
|
|
73
76
|
getValue: (c) => c.display.timeFormat,
|
|
74
|
-
setValue: (c, v) => ({ ...c, display: { ...c.display, timeFormat: v as
|
|
77
|
+
setValue: (c, v) => ({ ...c, display: { ...c.display, timeFormat: v as "12h" | "24h" } }),
|
|
75
78
|
},
|
|
76
79
|
// Color Scheme + Theme selection is handled by ThemePicker in SettingsModal
|
|
77
80
|
{
|
|
78
|
-
key:
|
|
79
|
-
label:
|
|
80
|
-
category:
|
|
81
|
-
type:
|
|
82
|
-
options: [
|
|
81
|
+
key: "dailyBudget",
|
|
82
|
+
label: "Daily Budget ($)",
|
|
83
|
+
category: "budgets",
|
|
84
|
+
type: "select",
|
|
85
|
+
options: ["None", "$10", "$25", "$50", "$100", "$200"],
|
|
83
86
|
getValue: (c) => {
|
|
84
87
|
const b = c.budgets.daily;
|
|
85
|
-
if (b === null) return
|
|
88
|
+
if (b === null) return "None";
|
|
86
89
|
return `$${b}`;
|
|
87
90
|
},
|
|
88
91
|
setValue: (c, v) => {
|
|
89
|
-
const map: Record<string, number | null> = {
|
|
92
|
+
const map: Record<string, number | null> = {
|
|
93
|
+
None: null,
|
|
94
|
+
$10: 10,
|
|
95
|
+
$25: 25,
|
|
96
|
+
$50: 50,
|
|
97
|
+
$100: 100,
|
|
98
|
+
$200: 200,
|
|
99
|
+
};
|
|
90
100
|
return { ...c, budgets: { ...c.budgets, daily: map[v as string] ?? null } };
|
|
91
101
|
},
|
|
92
102
|
},
|
|
93
103
|
{
|
|
94
|
-
key:
|
|
95
|
-
label:
|
|
96
|
-
category:
|
|
97
|
-
type:
|
|
98
|
-
options: [
|
|
104
|
+
key: "weeklyBudget",
|
|
105
|
+
label: "Weekly Budget ($)",
|
|
106
|
+
category: "budgets",
|
|
107
|
+
type: "select",
|
|
108
|
+
options: ["None", "$50", "$100", "$200", "$500", "$1000"],
|
|
99
109
|
getValue: (c) => {
|
|
100
110
|
const b = c.budgets.weekly;
|
|
101
|
-
if (b === null) return
|
|
111
|
+
if (b === null) return "None";
|
|
102
112
|
return `$${b}`;
|
|
103
113
|
},
|
|
104
114
|
setValue: (c, v) => {
|
|
105
|
-
const map: Record<string, number | null> = {
|
|
115
|
+
const map: Record<string, number | null> = {
|
|
116
|
+
None: null,
|
|
117
|
+
$50: 50,
|
|
118
|
+
$100: 100,
|
|
119
|
+
$200: 200,
|
|
120
|
+
$500: 500,
|
|
121
|
+
$1000: 1000,
|
|
122
|
+
};
|
|
106
123
|
return { ...c, budgets: { ...c.budgets, weekly: map[v as string] ?? null } };
|
|
107
124
|
},
|
|
108
125
|
},
|
|
109
126
|
{
|
|
110
|
-
key:
|
|
111
|
-
label:
|
|
112
|
-
category:
|
|
113
|
-
type:
|
|
114
|
-
options: [
|
|
127
|
+
key: "monthlyBudget",
|
|
128
|
+
label: "Monthly Budget ($)",
|
|
129
|
+
category: "budgets",
|
|
130
|
+
type: "select",
|
|
131
|
+
options: ["None", "$100", "$250", "$500", "$1000", "$2000"],
|
|
115
132
|
getValue: (c) => {
|
|
116
133
|
const b = c.budgets.monthly;
|
|
117
|
-
if (b === null) return
|
|
134
|
+
if (b === null) return "None";
|
|
118
135
|
return `$${b}`;
|
|
119
136
|
},
|
|
120
137
|
setValue: (c, v) => {
|
|
121
|
-
const map: Record<string, number | null> = {
|
|
138
|
+
const map: Record<string, number | null> = {
|
|
139
|
+
None: null,
|
|
140
|
+
$100: 100,
|
|
141
|
+
$250: 250,
|
|
142
|
+
$500: 500,
|
|
143
|
+
$1000: 1000,
|
|
144
|
+
$2000: 2000,
|
|
145
|
+
};
|
|
122
146
|
return { ...c, budgets: { ...c.budgets, monthly: map[v as string] ?? null } };
|
|
123
147
|
},
|
|
124
148
|
},
|
|
125
149
|
{
|
|
126
|
-
key:
|
|
127
|
-
label:
|
|
128
|
-
category:
|
|
129
|
-
type:
|
|
130
|
-
options: [
|
|
150
|
+
key: "warningPercent",
|
|
151
|
+
label: "Warning Threshold (%)",
|
|
152
|
+
category: "alerts",
|
|
153
|
+
type: "select",
|
|
154
|
+
options: ["55%", "60%", "65%", "70%", "75%", "80%", "85%", "90%", "95%"],
|
|
131
155
|
getValue: (c) => `${c.alerts.warningPercent}%`,
|
|
132
156
|
setValue: (c, v) => {
|
|
133
|
-
const percent = parseInt((v as string).replace(
|
|
157
|
+
const percent = parseInt((v as string).replace("%", ""), 10);
|
|
134
158
|
return { ...c, alerts: { ...c.alerts, warningPercent: percent } };
|
|
135
159
|
},
|
|
136
160
|
},
|
|
137
161
|
{
|
|
138
|
-
key:
|
|
139
|
-
label:
|
|
140
|
-
category:
|
|
141
|
-
type:
|
|
142
|
-
options: [
|
|
162
|
+
key: "criticalPercent",
|
|
163
|
+
label: "Critical Threshold (%)",
|
|
164
|
+
category: "alerts",
|
|
165
|
+
type: "select",
|
|
166
|
+
options: ["75%", "80%", "85%", "90%", "95%", "98%", "100%"],
|
|
143
167
|
getValue: (c) => `${c.alerts.criticalPercent}%`,
|
|
144
168
|
setValue: (c, v) => {
|
|
145
|
-
const percent = parseInt((v as string).replace(
|
|
169
|
+
const percent = parseInt((v as string).replace("%", ""), 10);
|
|
146
170
|
return { ...c, alerts: { ...c.alerts, criticalPercent: percent } };
|
|
147
171
|
},
|
|
148
172
|
},
|
|
149
173
|
{
|
|
150
|
-
key:
|
|
151
|
-
label:
|
|
152
|
-
category:
|
|
153
|
-
type:
|
|
174
|
+
key: "toastsEnabled",
|
|
175
|
+
label: "Show Toast Notifications",
|
|
176
|
+
category: "notifications",
|
|
177
|
+
type: "toggle",
|
|
154
178
|
getValue: (c) => c.notifications.toastsEnabled,
|
|
155
|
-
setValue: (c, v) => ({
|
|
179
|
+
setValue: (c, v) => ({
|
|
180
|
+
...c,
|
|
181
|
+
notifications: { ...c.notifications, toastsEnabled: v as boolean },
|
|
182
|
+
}),
|
|
156
183
|
},
|
|
157
184
|
{
|
|
158
|
-
key:
|
|
159
|
-
label:
|
|
160
|
-
category:
|
|
161
|
-
type:
|
|
185
|
+
key: "soundEnabled",
|
|
186
|
+
label: "Sound Alerts",
|
|
187
|
+
category: "notifications",
|
|
188
|
+
type: "toggle",
|
|
162
189
|
getValue: (c) => c.notifications.soundEnabled,
|
|
163
|
-
setValue: (c, v) => ({
|
|
190
|
+
setValue: (c, v) => ({
|
|
191
|
+
...c,
|
|
192
|
+
notifications: { ...c.notifications, soundEnabled: v as boolean },
|
|
193
|
+
}),
|
|
164
194
|
},
|
|
165
195
|
];
|
|
166
196
|
|
|
167
197
|
const CATEGORIES: { id: SettingCategory; label: string }[] = [
|
|
168
|
-
{ id:
|
|
169
|
-
{ id:
|
|
170
|
-
{ id:
|
|
171
|
-
{ id:
|
|
172
|
-
{ id:
|
|
173
|
-
{ id:
|
|
198
|
+
{ id: "refresh", label: "Refresh" },
|
|
199
|
+
{ id: "display", label: "Display" },
|
|
200
|
+
{ id: "budgets", label: "Budgets" },
|
|
201
|
+
{ id: "alerts", label: "Alerts" },
|
|
202
|
+
{ id: "notifications", label: "Notifications" },
|
|
203
|
+
{ id: "plugins", label: "Plugins" },
|
|
174
204
|
];
|
|
175
205
|
|
|
176
|
-
function buildPluginSettings(
|
|
206
|
+
function buildPluginSettings(
|
|
207
|
+
plugins: Array<{ id: string; name: string; configSchema?: Record<string, ConfigField> }>,
|
|
208
|
+
): SettingItem[] {
|
|
177
209
|
const items: SettingItem[] = [];
|
|
178
210
|
for (const plugin of plugins) {
|
|
179
211
|
if (!plugin.configSchema) continue;
|
|
@@ -198,38 +230,38 @@ function buildPluginSettings(plugins: Array<{ id: string; name: string; configSc
|
|
|
198
230
|
},
|
|
199
231
|
});
|
|
200
232
|
|
|
201
|
-
if (field.type ===
|
|
233
|
+
if (field.type === "boolean") {
|
|
202
234
|
items.push({
|
|
203
235
|
key: settingKey,
|
|
204
236
|
label,
|
|
205
|
-
category:
|
|
206
|
-
type:
|
|
237
|
+
category: "plugins" as SettingCategory,
|
|
238
|
+
type: "toggle",
|
|
207
239
|
getValue: (c) => getPluginValue(c) as boolean,
|
|
208
240
|
setValue: (c, v) => setPluginValue(c, v),
|
|
209
241
|
});
|
|
210
|
-
} else if (field.type ===
|
|
242
|
+
} else if (field.type === "select" && field.options) {
|
|
211
243
|
items.push({
|
|
212
244
|
key: settingKey,
|
|
213
245
|
label,
|
|
214
|
-
category:
|
|
215
|
-
type:
|
|
246
|
+
category: "plugins" as SettingCategory,
|
|
247
|
+
type: "select",
|
|
216
248
|
options: field.options.map((o) => o.label),
|
|
217
249
|
getValue: (c) => {
|
|
218
250
|
const val = getPluginValue(c) as string;
|
|
219
251
|
const opt = field.options?.find((o) => o.value === val);
|
|
220
|
-
return opt?.label ?? val ??
|
|
252
|
+
return opt?.label ?? val ?? "";
|
|
221
253
|
},
|
|
222
254
|
setValue: (c, v) => {
|
|
223
255
|
const opt = field.options?.find((o) => o.label === v);
|
|
224
256
|
return setPluginValue(c, opt?.value ?? v);
|
|
225
257
|
},
|
|
226
258
|
});
|
|
227
|
-
} else if (field.type ===
|
|
259
|
+
} else if (field.type === "number") {
|
|
228
260
|
items.push({
|
|
229
261
|
key: settingKey,
|
|
230
262
|
label,
|
|
231
|
-
category:
|
|
232
|
-
type:
|
|
263
|
+
category: "plugins" as SettingCategory,
|
|
264
|
+
type: "number",
|
|
233
265
|
getValue: (c) => (getPluginValue(c) as number) ?? 0,
|
|
234
266
|
setValue: (c, v) => setPluginValue(c, v),
|
|
235
267
|
});
|
|
@@ -237,9 +269,9 @@ function buildPluginSettings(plugins: Array<{ id: string; name: string; configSc
|
|
|
237
269
|
items.push({
|
|
238
270
|
key: settingKey,
|
|
239
271
|
label,
|
|
240
|
-
category:
|
|
241
|
-
type:
|
|
242
|
-
getValue: (c) => String(getPluginValue(c) ??
|
|
272
|
+
category: "plugins" as SettingCategory,
|
|
273
|
+
type: "select",
|
|
274
|
+
getValue: (c) => String(getPluginValue(c) ?? ""),
|
|
243
275
|
setValue: (c, v) => setPluginValue(c, v),
|
|
244
276
|
});
|
|
245
277
|
}
|
|
@@ -255,113 +287,110 @@ export function SettingsView() {
|
|
|
255
287
|
const { providers, notifications } = usePlugins();
|
|
256
288
|
const { demoMode, seed, preset } = useDemoMode();
|
|
257
289
|
const { width: terminalWidth } = useTerminalDimensions();
|
|
258
|
-
|
|
259
|
-
const [selectedCategory, setSelectedCategory] = useState<SettingCategory>(
|
|
290
|
+
|
|
291
|
+
const [selectedCategory, setSelectedCategory] = useState<SettingCategory>("refresh");
|
|
260
292
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
261
|
-
const [focusedPane, setFocusedPane] = useState<
|
|
262
|
-
|
|
293
|
+
const [focusedPane, setFocusedPane] = useState<"categories" | "settings">("settings");
|
|
294
|
+
|
|
263
295
|
const pluginSettings = useMemo(() => {
|
|
264
|
-
const allPlugins = [
|
|
265
|
-
...Array.from(providers.values()).map((p) => p.plugin),
|
|
266
|
-
...notifications,
|
|
267
|
-
];
|
|
296
|
+
const allPlugins = [...Array.from(providers.values()).map((p) => p.plugin), ...notifications];
|
|
268
297
|
return buildPluginSettings(allPlugins);
|
|
269
298
|
}, [providers, notifications, config]);
|
|
270
299
|
|
|
271
300
|
const allSettings = useMemo(() => [...SETTINGS, ...pluginSettings], [pluginSettings]);
|
|
272
|
-
const categorySettings = allSettings.filter(s => s.category === selectedCategory);
|
|
273
|
-
|
|
301
|
+
const categorySettings = allSettings.filter((s) => s.category === selectedCategory);
|
|
302
|
+
|
|
274
303
|
const helpText = useMemo(() => {
|
|
275
304
|
if (terminalWidth >= 100) {
|
|
276
|
-
return
|
|
305
|
+
return "Tab: switch panes ↑↓: navigate Enter/Space: toggle ←→: adjust Ctrl+S: save Shift+R: reset";
|
|
277
306
|
} else if (terminalWidth >= 80) {
|
|
278
|
-
return
|
|
307
|
+
return "Tab: switch ↑↓: nav Enter: toggle ←→: adjust ^S: save R: reset";
|
|
279
308
|
}
|
|
280
|
-
return
|
|
309
|
+
return "Tab ↑↓ Enter ←→ ^S:save R:reset";
|
|
281
310
|
}, [terminalWidth]);
|
|
282
|
-
|
|
311
|
+
|
|
283
312
|
const handleSave = useCallback(async () => {
|
|
284
313
|
try {
|
|
285
314
|
await saveNow();
|
|
286
|
-
showToast(
|
|
315
|
+
showToast("Settings saved");
|
|
287
316
|
} catch {
|
|
288
|
-
showToast(
|
|
317
|
+
showToast("Failed to save settings", "error");
|
|
289
318
|
}
|
|
290
319
|
}, [saveNow, showToast]);
|
|
291
|
-
|
|
320
|
+
|
|
292
321
|
const handleReset = useCallback(() => {
|
|
293
322
|
resetToDefaults();
|
|
294
|
-
showToast(
|
|
323
|
+
showToast("Reset to defaults");
|
|
295
324
|
}, [resetToDefaults, showToast]);
|
|
296
|
-
|
|
325
|
+
|
|
297
326
|
const toggleCurrentSetting = useCallback(() => {
|
|
298
327
|
const setting = categorySettings[selectedIndex];
|
|
299
328
|
if (!setting) return;
|
|
300
|
-
|
|
329
|
+
|
|
301
330
|
const currentValue = setting.getValue(config);
|
|
302
331
|
let newConfig: AppConfig;
|
|
303
|
-
|
|
304
|
-
if (setting.type ===
|
|
332
|
+
|
|
333
|
+
if (setting.type === "toggle") {
|
|
305
334
|
newConfig = setting.setValue(config, !currentValue);
|
|
306
|
-
} else if (setting.type ===
|
|
335
|
+
} else if (setting.type === "select" && setting.options) {
|
|
307
336
|
const currentIdx = setting.options.indexOf(currentValue as string);
|
|
308
337
|
const nextIdx = (currentIdx + 1) % setting.options.length;
|
|
309
338
|
newConfig = setting.setValue(config, setting.options[nextIdx]!);
|
|
310
339
|
} else {
|
|
311
340
|
return;
|
|
312
341
|
}
|
|
313
|
-
|
|
342
|
+
|
|
314
343
|
updateConfig(newConfig);
|
|
315
344
|
}, [categorySettings, selectedIndex, config, updateConfig]);
|
|
316
|
-
|
|
345
|
+
|
|
317
346
|
useKeyboard((key) => {
|
|
318
|
-
if (key.ctrl && key.name ===
|
|
347
|
+
if (key.ctrl && key.name === "s") {
|
|
319
348
|
handleSave();
|
|
320
349
|
return;
|
|
321
350
|
}
|
|
322
|
-
|
|
323
|
-
if (key.shift && key.name ===
|
|
351
|
+
|
|
352
|
+
if (key.shift && key.name === "r") {
|
|
324
353
|
handleReset();
|
|
325
354
|
return;
|
|
326
355
|
}
|
|
327
|
-
|
|
328
|
-
if (key.name ===
|
|
329
|
-
setFocusedPane(p => p ===
|
|
356
|
+
|
|
357
|
+
if (key.name === "tab") {
|
|
358
|
+
setFocusedPane((p) => (p === "categories" ? "settings" : "categories"));
|
|
330
359
|
return;
|
|
331
360
|
}
|
|
332
|
-
|
|
333
|
-
if (focusedPane ===
|
|
334
|
-
const catIdx = CATEGORIES.findIndex(c => c.id === selectedCategory);
|
|
335
|
-
if (key.name ===
|
|
361
|
+
|
|
362
|
+
if (focusedPane === "categories") {
|
|
363
|
+
const catIdx = CATEGORIES.findIndex((c) => c.id === selectedCategory);
|
|
364
|
+
if (key.name === "down" || key.name === "j") {
|
|
336
365
|
const nextIdx = Math.min(catIdx + 1, CATEGORIES.length - 1);
|
|
337
366
|
setSelectedCategory(CATEGORIES[nextIdx]!.id);
|
|
338
367
|
setSelectedIndex(0);
|
|
339
|
-
} else if (key.name ===
|
|
368
|
+
} else if (key.name === "up" || key.name === "k") {
|
|
340
369
|
const prevIdx = Math.max(catIdx - 1, 0);
|
|
341
370
|
setSelectedCategory(CATEGORIES[prevIdx]!.id);
|
|
342
371
|
setSelectedIndex(0);
|
|
343
372
|
}
|
|
344
373
|
return;
|
|
345
374
|
}
|
|
346
|
-
|
|
347
|
-
if (focusedPane ===
|
|
348
|
-
if (key.name ===
|
|
349
|
-
setSelectedIndex(i => Math.min(i + 1, categorySettings.length - 1));
|
|
350
|
-
} else if (key.name ===
|
|
351
|
-
setSelectedIndex(i => Math.max(i - 1, 0));
|
|
352
|
-
} else if (key.name ===
|
|
375
|
+
|
|
376
|
+
if (focusedPane === "settings") {
|
|
377
|
+
if (key.name === "down" || key.name === "j") {
|
|
378
|
+
setSelectedIndex((i) => Math.min(i + 1, categorySettings.length - 1));
|
|
379
|
+
} else if (key.name === "up" || key.name === "k") {
|
|
380
|
+
setSelectedIndex((i) => Math.max(i - 1, 0));
|
|
381
|
+
} else if (key.name === "return" || key.name === "space") {
|
|
353
382
|
toggleCurrentSetting();
|
|
354
|
-
} else if (key.name ===
|
|
383
|
+
} else if (key.name === "left" || key.name === "h") {
|
|
355
384
|
const setting = categorySettings[selectedIndex];
|
|
356
|
-
if (setting?.type ===
|
|
385
|
+
if (setting?.type === "select" && setting.options) {
|
|
357
386
|
const currentValue = setting.getValue(config) as string;
|
|
358
387
|
const currentIdx = setting.options.indexOf(currentValue);
|
|
359
388
|
const prevIdx = (currentIdx - 1 + setting.options.length) % setting.options.length;
|
|
360
389
|
updateConfig(setting.setValue(config, setting.options[prevIdx]!));
|
|
361
390
|
}
|
|
362
|
-
} else if (key.name ===
|
|
391
|
+
} else if (key.name === "right" || key.name === "l") {
|
|
363
392
|
const setting = categorySettings[selectedIndex];
|
|
364
|
-
if (setting?.type ===
|
|
393
|
+
if (setting?.type === "select" && setting.options) {
|
|
365
394
|
const currentValue = setting.getValue(config) as string;
|
|
366
395
|
const currentIdx = setting.options.indexOf(currentValue);
|
|
367
396
|
const nextIdx = (currentIdx + 1) % setting.options.length;
|
|
@@ -370,7 +399,7 @@ export function SettingsView() {
|
|
|
370
399
|
}
|
|
371
400
|
}
|
|
372
401
|
});
|
|
373
|
-
|
|
402
|
+
|
|
374
403
|
if (isLoading) {
|
|
375
404
|
return (
|
|
376
405
|
<box flexDirection="column" flexGrow={1} justifyContent="center" alignItems="center">
|
|
@@ -378,85 +407,87 @@ export function SettingsView() {
|
|
|
378
407
|
</box>
|
|
379
408
|
);
|
|
380
409
|
}
|
|
381
|
-
|
|
410
|
+
|
|
382
411
|
return (
|
|
383
412
|
<box flexDirection="column" flexGrow={1} padding={1} gap={1}>
|
|
384
413
|
{demoMode && (
|
|
385
414
|
<box flexDirection="row" height={1} paddingLeft={1} gap={2}>
|
|
386
415
|
<text fg={colors.warning}>DEMO MODE</text>
|
|
387
416
|
<text fg={colors.textMuted}>
|
|
388
|
-
Seed: {seed ??
|
|
417
|
+
Seed: {seed ?? "default"} | Preset: {preset ?? "normal"}
|
|
389
418
|
</text>
|
|
390
419
|
</box>
|
|
391
420
|
)}
|
|
392
421
|
<box flexDirection="row" gap={1} flexGrow={1}>
|
|
393
|
-
<box
|
|
394
|
-
flexDirection="column"
|
|
395
|
-
width={20}
|
|
396
|
-
border
|
|
397
|
-
borderStyle={focusedPane ===
|
|
398
|
-
borderColor={focusedPane ===
|
|
422
|
+
<box
|
|
423
|
+
flexDirection="column"
|
|
424
|
+
width={20}
|
|
425
|
+
border
|
|
426
|
+
borderStyle={focusedPane === "categories" ? "double" : "single"}
|
|
427
|
+
borderColor={focusedPane === "categories" ? colors.primary : colors.border}
|
|
399
428
|
padding={1}
|
|
400
429
|
>
|
|
401
|
-
<text fg={colors.textMuted} marginBottom={1}>
|
|
430
|
+
<text fg={colors.textMuted} marginBottom={1}>
|
|
431
|
+
CATEGORIES
|
|
432
|
+
</text>
|
|
402
433
|
{CATEGORIES.map((cat) => {
|
|
403
434
|
const isActive = cat.id === selectedCategory;
|
|
404
|
-
const isFocusedActive = isActive && focusedPane ===
|
|
435
|
+
const isFocusedActive = isActive && focusedPane === "categories";
|
|
405
436
|
return (
|
|
406
437
|
<box key={cat.id} height={1}>
|
|
407
|
-
<text
|
|
438
|
+
<text
|
|
408
439
|
fg={isFocusedActive ? colors.background : isActive ? colors.primary : colors.text}
|
|
409
440
|
{...(isFocusedActive ? { bg: colors.primary } : {})}
|
|
410
441
|
>
|
|
411
|
-
{isActive ?
|
|
442
|
+
{isActive ? "▸ " : " "}
|
|
443
|
+
{cat.label}
|
|
412
444
|
</text>
|
|
413
445
|
</box>
|
|
414
446
|
);
|
|
415
447
|
})}
|
|
416
448
|
</box>
|
|
417
|
-
|
|
418
|
-
<box
|
|
419
|
-
flexDirection="column"
|
|
449
|
+
|
|
450
|
+
<box
|
|
451
|
+
flexDirection="column"
|
|
420
452
|
flexGrow={1}
|
|
421
|
-
border
|
|
422
|
-
borderStyle={focusedPane ===
|
|
423
|
-
borderColor={focusedPane ===
|
|
453
|
+
border
|
|
454
|
+
borderStyle={focusedPane === "settings" ? "double" : "single"}
|
|
455
|
+
borderColor={focusedPane === "settings" ? colors.primary : colors.border}
|
|
424
456
|
padding={1}
|
|
425
457
|
>
|
|
426
458
|
<text fg={colors.textMuted} marginBottom={1}>
|
|
427
|
-
{CATEGORIES.find(c => c.id === selectedCategory)?.label.toUpperCase()}
|
|
459
|
+
{CATEGORIES.find((c) => c.id === selectedCategory)?.label.toUpperCase()}
|
|
428
460
|
</text>
|
|
429
|
-
|
|
461
|
+
|
|
430
462
|
{categorySettings.map((setting, idx) => {
|
|
431
|
-
const isSelected = idx === selectedIndex && focusedPane ===
|
|
463
|
+
const isSelected = idx === selectedIndex && focusedPane === "settings";
|
|
432
464
|
const value = setting.getValue(config);
|
|
433
|
-
|
|
465
|
+
|
|
434
466
|
return (
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
<text fg={isSelected ? colors.background : colors.text}>
|
|
444
|
-
{setting.label}
|
|
445
|
-
</text>
|
|
467
|
+
<box
|
|
468
|
+
key={setting.key}
|
|
469
|
+
flexDirection="row"
|
|
470
|
+
justifyContent="space-between"
|
|
471
|
+
height={2}
|
|
472
|
+
paddingX={1}
|
|
473
|
+
{...(isSelected ? { backgroundColor: colors.primary } : {})}
|
|
474
|
+
>
|
|
475
|
+
<text fg={isSelected ? colors.background : colors.text}>{setting.label}</text>
|
|
446
476
|
<text fg={isSelected ? colors.background : colors.textMuted}>
|
|
447
|
-
{setting.type ===
|
|
448
|
-
?
|
|
449
|
-
|
|
477
|
+
{setting.type === "toggle"
|
|
478
|
+
? value
|
|
479
|
+
? "● ON"
|
|
480
|
+
: "○ OFF"
|
|
481
|
+
: setting.type === "select"
|
|
450
482
|
? `◂ ${value} ▸`
|
|
451
|
-
: String(value)
|
|
452
|
-
}
|
|
483
|
+
: String(value)}
|
|
453
484
|
</text>
|
|
454
485
|
</box>
|
|
455
486
|
);
|
|
456
487
|
})}
|
|
457
488
|
</box>
|
|
458
489
|
</box>
|
|
459
|
-
|
|
490
|
+
|
|
460
491
|
<box flexDirection="row" height={1} paddingLeft={1}>
|
|
461
492
|
<text fg={colors.textSubtle}>{helpText}</text>
|
|
462
493
|
</box>
|