@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,23 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import { ThemePicker } from
|
|
13
|
-
|
|
14
|
-
type SettingCategory =
|
|
1
|
+
import { useKeyboard, useTerminalDimensions } from "@opentui/react";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
3
|
+
import type { AppConfig, SparklineOrientation, SparklineStyle } from "@/config/schema.ts";
|
|
4
|
+
import { formatBudgetDisplay, parseCurrencyInput } from "@/utils/currency.ts";
|
|
5
|
+
import { useConfig } from "../contexts/ConfigContext.tsx";
|
|
6
|
+
import { useDemoMode } from "../contexts/DemoModeContext.tsx";
|
|
7
|
+
import { useInputFocus } from "../contexts/InputContext.tsx";
|
|
8
|
+
import { usePlugins } from "../contexts/PluginContext.tsx";
|
|
9
|
+
import { useColors, useTheme } from "../contexts/ThemeContext.tsx";
|
|
10
|
+
import { useToastContext } from "../contexts/ToastContext.tsx";
|
|
11
|
+
import { ModalBackdrop, Z_INDEX } from "./ModalBackdrop.tsx";
|
|
12
|
+
import { ThemePicker } from "./ThemePicker.tsx";
|
|
13
|
+
|
|
14
|
+
type SettingCategory = "refresh" | "display" | "budgets" | "alerts" | "notifications";
|
|
15
15
|
|
|
16
16
|
interface SettingItem {
|
|
17
17
|
key: string;
|
|
18
18
|
label: string;
|
|
19
19
|
category: SettingCategory;
|
|
20
|
-
type:
|
|
20
|
+
type: "toggle" | "select" | "number";
|
|
21
21
|
options?: string[];
|
|
22
22
|
getValue: (config: AppConfig) => string | number | boolean | null;
|
|
23
23
|
setValue: (config: AppConfig, value: string | number | boolean | null) => AppConfig;
|
|
@@ -25,182 +25,191 @@ interface SettingItem {
|
|
|
25
25
|
|
|
26
26
|
const BASE_SETTINGS: SettingItem[] = [
|
|
27
27
|
{
|
|
28
|
-
key:
|
|
29
|
-
label:
|
|
30
|
-
category:
|
|
31
|
-
type:
|
|
32
|
-
options: [
|
|
28
|
+
key: "intervalMs",
|
|
29
|
+
label: "Refresh Interval",
|
|
30
|
+
category: "refresh",
|
|
31
|
+
type: "select",
|
|
32
|
+
options: ["30s", "1m", "2m", "5m"],
|
|
33
33
|
getValue: (c) => {
|
|
34
34
|
const ms = c.refresh.intervalMs;
|
|
35
|
-
if (ms <= 30000) return
|
|
36
|
-
if (ms <= 60000) return
|
|
37
|
-
if (ms <= 120000) return
|
|
38
|
-
return
|
|
35
|
+
if (ms <= 30000) return "30s";
|
|
36
|
+
if (ms <= 60000) return "1m";
|
|
37
|
+
if (ms <= 120000) return "2m";
|
|
38
|
+
return "5m";
|
|
39
39
|
},
|
|
40
40
|
setValue: (c, v) => {
|
|
41
|
-
const map: Record<string, number> = {
|
|
41
|
+
const map: Record<string, number> = { "30s": 30000, "1m": 60000, "2m": 120000, "5m": 300000 };
|
|
42
42
|
return { ...c, refresh: { ...c.refresh, intervalMs: map[v as string] ?? 60000 } };
|
|
43
43
|
},
|
|
44
44
|
},
|
|
45
45
|
{
|
|
46
|
-
key:
|
|
47
|
-
label:
|
|
48
|
-
category:
|
|
49
|
-
type:
|
|
46
|
+
key: "pauseAutoRefresh",
|
|
47
|
+
label: "Pause Auto-Refresh",
|
|
48
|
+
category: "refresh",
|
|
49
|
+
type: "toggle",
|
|
50
50
|
getValue: (c) => c.refresh.pauseAutoRefresh,
|
|
51
51
|
setValue: (c, v) => ({ ...c, refresh: { ...c.refresh, pauseAutoRefresh: v as boolean } }),
|
|
52
52
|
},
|
|
53
53
|
{
|
|
54
|
-
key:
|
|
55
|
-
label:
|
|
56
|
-
category:
|
|
57
|
-
type:
|
|
58
|
-
options: [
|
|
54
|
+
key: "defaultTimeWindow",
|
|
55
|
+
label: "Default Time Window",
|
|
56
|
+
category: "display",
|
|
57
|
+
type: "select",
|
|
58
|
+
options: ["5m", "15m", "1h", "24h", "7d", "30d", "all"],
|
|
59
59
|
getValue: (c) => c.display.defaultTimeWindow,
|
|
60
|
-
setValue: (c, v) => ({
|
|
60
|
+
setValue: (c, v) => ({
|
|
61
|
+
...c,
|
|
62
|
+
display: { ...c.display, defaultTimeWindow: v as AppConfig["display"]["defaultTimeWindow"] },
|
|
63
|
+
}),
|
|
61
64
|
},
|
|
62
65
|
{
|
|
63
|
-
key:
|
|
64
|
-
label:
|
|
65
|
-
category:
|
|
66
|
-
type:
|
|
66
|
+
key: "sidebarCollapsed",
|
|
67
|
+
label: "Sidebar Collapsed",
|
|
68
|
+
category: "display",
|
|
69
|
+
type: "toggle",
|
|
67
70
|
getValue: (c) => c.display.sidebarCollapsed,
|
|
68
71
|
setValue: (c, v) => ({ ...c, display: { ...c.display, sidebarCollapsed: v as boolean } }),
|
|
69
72
|
},
|
|
70
73
|
{
|
|
71
|
-
key:
|
|
72
|
-
label:
|
|
73
|
-
category:
|
|
74
|
-
type:
|
|
75
|
-
options: [
|
|
74
|
+
key: "timeFormat",
|
|
75
|
+
label: "Time Format",
|
|
76
|
+
category: "display",
|
|
77
|
+
type: "select",
|
|
78
|
+
options: ["12h", "24h"],
|
|
76
79
|
getValue: (c) => c.display.timeFormat,
|
|
77
|
-
setValue: (c, v) => ({ ...c, display: { ...c.display, timeFormat: v as
|
|
80
|
+
setValue: (c, v) => ({ ...c, display: { ...c.display, timeFormat: v as "12h" | "24h" } }),
|
|
78
81
|
},
|
|
79
82
|
{
|
|
80
|
-
key:
|
|
81
|
-
label:
|
|
82
|
-
category:
|
|
83
|
-
type:
|
|
84
|
-
options: [
|
|
83
|
+
key: "sparklineStyle",
|
|
84
|
+
label: "Sparkline Style",
|
|
85
|
+
category: "display",
|
|
86
|
+
type: "select",
|
|
87
|
+
options: ["braille", "block"],
|
|
85
88
|
getValue: (c) => c.display.sparkline.style,
|
|
86
|
-
setValue: (c, v) => ({
|
|
87
|
-
...c,
|
|
88
|
-
display: {
|
|
89
|
-
...c.display,
|
|
90
|
-
sparkline: { ...c.display.sparkline, style: v as SparklineStyle }
|
|
91
|
-
}
|
|
89
|
+
setValue: (c, v) => ({
|
|
90
|
+
...c,
|
|
91
|
+
display: {
|
|
92
|
+
...c.display,
|
|
93
|
+
sparkline: { ...c.display.sparkline, style: v as SparklineStyle },
|
|
94
|
+
},
|
|
92
95
|
}),
|
|
93
96
|
},
|
|
94
97
|
{
|
|
95
|
-
key:
|
|
96
|
-
label:
|
|
97
|
-
category:
|
|
98
|
-
type:
|
|
99
|
-
options: [
|
|
98
|
+
key: "sparklineOrientation",
|
|
99
|
+
label: "Sparkline Direction",
|
|
100
|
+
category: "display",
|
|
101
|
+
type: "select",
|
|
102
|
+
options: ["up", "down"],
|
|
100
103
|
getValue: (c) => c.display.sparkline.orientation,
|
|
101
|
-
setValue: (c, v) => ({
|
|
102
|
-
...c,
|
|
103
|
-
display: {
|
|
104
|
-
...c.display,
|
|
105
|
-
sparkline: { ...c.display.sparkline, orientation: v as SparklineOrientation }
|
|
106
|
-
}
|
|
104
|
+
setValue: (c, v) => ({
|
|
105
|
+
...c,
|
|
106
|
+
display: {
|
|
107
|
+
...c.display,
|
|
108
|
+
sparkline: { ...c.display.sparkline, orientation: v as SparklineOrientation },
|
|
109
|
+
},
|
|
107
110
|
}),
|
|
108
111
|
},
|
|
109
112
|
{
|
|
110
|
-
key:
|
|
111
|
-
label:
|
|
112
|
-
category:
|
|
113
|
-
type:
|
|
113
|
+
key: "sparklineBaseline",
|
|
114
|
+
label: "Sparkline Baseline",
|
|
115
|
+
category: "display",
|
|
116
|
+
type: "toggle",
|
|
114
117
|
getValue: (c) => c.display.sparkline.showBaseline,
|
|
115
|
-
setValue: (c, v) => ({
|
|
116
|
-
...c,
|
|
117
|
-
display: {
|
|
118
|
-
...c.display,
|
|
119
|
-
sparkline: { ...c.display.sparkline, showBaseline: v as boolean }
|
|
120
|
-
}
|
|
118
|
+
setValue: (c, v) => ({
|
|
119
|
+
...c,
|
|
120
|
+
display: {
|
|
121
|
+
...c.display,
|
|
122
|
+
sparkline: { ...c.display.sparkline, showBaseline: v as boolean },
|
|
123
|
+
},
|
|
121
124
|
}),
|
|
122
125
|
},
|
|
123
126
|
{
|
|
124
|
-
key:
|
|
125
|
-
label:
|
|
126
|
-
category:
|
|
127
|
-
type:
|
|
127
|
+
key: "theme",
|
|
128
|
+
label: "Theme",
|
|
129
|
+
category: "display",
|
|
130
|
+
type: "select",
|
|
128
131
|
options: [], // Handled by ThemePicker
|
|
129
132
|
getValue: (c) => c.display.theme,
|
|
130
133
|
setValue: (c) => c, // Handled by ThemePicker
|
|
131
134
|
},
|
|
132
135
|
{
|
|
133
|
-
key:
|
|
134
|
-
label:
|
|
135
|
-
category:
|
|
136
|
-
type:
|
|
136
|
+
key: "dailyBudget",
|
|
137
|
+
label: "Daily Budget ($)",
|
|
138
|
+
category: "budgets",
|
|
139
|
+
type: "number",
|
|
137
140
|
getValue: (c) => c.budgets.daily,
|
|
138
141
|
setValue: (c, v) => ({ ...c, budgets: { ...c.budgets, daily: v as number | null } }),
|
|
139
142
|
},
|
|
140
143
|
{
|
|
141
|
-
key:
|
|
142
|
-
label:
|
|
143
|
-
category:
|
|
144
|
-
type:
|
|
144
|
+
key: "weeklyBudget",
|
|
145
|
+
label: "Weekly Budget ($)",
|
|
146
|
+
category: "budgets",
|
|
147
|
+
type: "number",
|
|
145
148
|
getValue: (c) => c.budgets.weekly,
|
|
146
149
|
setValue: (c, v) => ({ ...c, budgets: { ...c.budgets, weekly: v as number | null } }),
|
|
147
150
|
},
|
|
148
151
|
{
|
|
149
|
-
key:
|
|
150
|
-
label:
|
|
151
|
-
category:
|
|
152
|
-
type:
|
|
152
|
+
key: "monthlyBudget",
|
|
153
|
+
label: "Monthly Budget ($)",
|
|
154
|
+
category: "budgets",
|
|
155
|
+
type: "number",
|
|
153
156
|
getValue: (c) => c.budgets.monthly,
|
|
154
157
|
setValue: (c, v) => ({ ...c, budgets: { ...c.budgets, monthly: v as number | null } }),
|
|
155
158
|
},
|
|
156
159
|
{
|
|
157
|
-
key:
|
|
158
|
-
label:
|
|
159
|
-
category:
|
|
160
|
-
type:
|
|
161
|
-
options: [
|
|
160
|
+
key: "warningPercent",
|
|
161
|
+
label: "Warning Threshold (%)",
|
|
162
|
+
category: "alerts",
|
|
163
|
+
type: "select",
|
|
164
|
+
options: ["55%", "60%", "65%", "70%", "75%", "80%", "85%", "90%", "95%"],
|
|
162
165
|
getValue: (c) => `${c.alerts.warningPercent}%`,
|
|
163
166
|
setValue: (c, v) => {
|
|
164
|
-
const percent = parseInt((v as string).replace(
|
|
167
|
+
const percent = parseInt((v as string).replace("%", ""), 10);
|
|
165
168
|
return { ...c, alerts: { ...c.alerts, warningPercent: percent } };
|
|
166
169
|
},
|
|
167
170
|
},
|
|
168
171
|
{
|
|
169
|
-
key:
|
|
170
|
-
label:
|
|
171
|
-
category:
|
|
172
|
-
type:
|
|
173
|
-
options: [
|
|
172
|
+
key: "criticalPercent",
|
|
173
|
+
label: "Critical Threshold (%)",
|
|
174
|
+
category: "alerts",
|
|
175
|
+
type: "select",
|
|
176
|
+
options: ["75%", "80%", "85%", "90%", "95%", "98%", "100%"],
|
|
174
177
|
getValue: (c) => `${c.alerts.criticalPercent}%`,
|
|
175
178
|
setValue: (c, v) => {
|
|
176
|
-
const percent = parseInt((v as string).replace(
|
|
179
|
+
const percent = parseInt((v as string).replace("%", ""), 10);
|
|
177
180
|
return { ...c, alerts: { ...c.alerts, criticalPercent: percent } };
|
|
178
181
|
},
|
|
179
182
|
},
|
|
180
183
|
{
|
|
181
|
-
key:
|
|
182
|
-
label:
|
|
183
|
-
category:
|
|
184
|
-
type:
|
|
184
|
+
key: "toastsEnabled",
|
|
185
|
+
label: "Show Toast Notifications",
|
|
186
|
+
category: "notifications",
|
|
187
|
+
type: "toggle",
|
|
185
188
|
getValue: (c) => c.notifications.toastsEnabled,
|
|
186
|
-
setValue: (c, v) => ({
|
|
189
|
+
setValue: (c, v) => ({
|
|
190
|
+
...c,
|
|
191
|
+
notifications: { ...c.notifications, toastsEnabled: v as boolean },
|
|
192
|
+
}),
|
|
187
193
|
},
|
|
188
194
|
{
|
|
189
|
-
key:
|
|
190
|
-
label:
|
|
191
|
-
category:
|
|
192
|
-
type:
|
|
195
|
+
key: "soundEnabled",
|
|
196
|
+
label: "Sound Alerts",
|
|
197
|
+
category: "notifications",
|
|
198
|
+
type: "toggle",
|
|
193
199
|
getValue: (c) => c.notifications.soundEnabled,
|
|
194
|
-
setValue: (c, v) => ({
|
|
200
|
+
setValue: (c, v) => ({
|
|
201
|
+
...c,
|
|
202
|
+
notifications: { ...c.notifications, soundEnabled: v as boolean },
|
|
203
|
+
}),
|
|
195
204
|
},
|
|
196
205
|
];
|
|
197
206
|
|
|
198
207
|
const CATEGORIES: { id: SettingCategory; label: string }[] = [
|
|
199
|
-
{ id:
|
|
200
|
-
{ id:
|
|
201
|
-
{ id:
|
|
202
|
-
{ id:
|
|
203
|
-
{ id:
|
|
208
|
+
{ id: "refresh", label: "Refresh" },
|
|
209
|
+
{ id: "display", label: "Display" },
|
|
210
|
+
{ id: "budgets", label: "Budgets" },
|
|
211
|
+
{ id: "alerts", label: "Alerts" },
|
|
212
|
+
{ id: "notifications", label: "Notifications" },
|
|
204
213
|
];
|
|
205
214
|
|
|
206
215
|
interface SettingsModalProps {
|
|
@@ -217,17 +226,17 @@ export function SettingsModal({ onClose }: SettingsModalProps) {
|
|
|
217
226
|
const { setInputFocused } = useInputFocus();
|
|
218
227
|
const { width: termWidth, height: termHeight } = useTerminalDimensions();
|
|
219
228
|
|
|
220
|
-
const [selectedCategory, setSelectedCategory] = useState<SettingCategory>(
|
|
229
|
+
const [selectedCategory, setSelectedCategory] = useState<SettingCategory>("refresh");
|
|
221
230
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
222
|
-
const [focusedPane, setFocusedPane] = useState<
|
|
231
|
+
const [focusedPane, setFocusedPane] = useState<"categories" | "settings">("categories");
|
|
223
232
|
const [showThemePicker, setShowThemePicker] = useState(false);
|
|
224
|
-
|
|
233
|
+
|
|
225
234
|
const [editingSettingKey, setEditingSettingKey] = useState<string | null>(null);
|
|
226
|
-
const [inputValue, setInputValue] = useState(
|
|
235
|
+
const [inputValue, setInputValue] = useState("");
|
|
227
236
|
|
|
228
237
|
const width = Math.min(termWidth - 4, 100);
|
|
229
238
|
const height = Math.min(termHeight - 4, 28);
|
|
230
|
-
|
|
239
|
+
|
|
231
240
|
// Calculate available height for settings list
|
|
232
241
|
// Modal height - header(1) - footer(1) - outer padding(2) - inner borders(4) - title row with margin(2)
|
|
233
242
|
const settingsAreaHeight = height - 1 - 1 - 2 - 4 - 2 - (demoMode ? 1 : 0);
|
|
@@ -236,8 +245,8 @@ export function SettingsModal({ onClose }: SettingsModalProps) {
|
|
|
236
245
|
|
|
237
246
|
const settings = BASE_SETTINGS;
|
|
238
247
|
|
|
239
|
-
const categorySettings = settings.filter(s => s.category === selectedCategory);
|
|
240
|
-
|
|
248
|
+
const categorySettings = settings.filter((s) => s.category === selectedCategory);
|
|
249
|
+
|
|
241
250
|
// Calculate scroll offset to keep selected item visible
|
|
242
251
|
const scrollOffset = useMemo(() => {
|
|
243
252
|
if (categorySettings.length <= visibleSettingsCount) return 0;
|
|
@@ -245,7 +254,7 @@ export function SettingsModal({ onClose }: SettingsModalProps) {
|
|
|
245
254
|
if (selectedIndex < visibleSettingsCount - 1) return 0;
|
|
246
255
|
return Math.min(selectedIndex - visibleSettingsCount + 2, maxOffset);
|
|
247
256
|
}, [selectedIndex, categorySettings.length, visibleSettingsCount]);
|
|
248
|
-
|
|
257
|
+
|
|
249
258
|
const visibleSettings = categorySettings.slice(scrollOffset, scrollOffset + visibleSettingsCount);
|
|
250
259
|
const hasMoreAbove = scrollOffset > 0;
|
|
251
260
|
const hasMoreBelow = scrollOffset + visibleSettingsCount < categorySettings.length;
|
|
@@ -258,45 +267,45 @@ export function SettingsModal({ onClose }: SettingsModalProps) {
|
|
|
258
267
|
const handleSave = useCallback(async () => {
|
|
259
268
|
try {
|
|
260
269
|
await saveNow();
|
|
261
|
-
showToast(
|
|
270
|
+
showToast("Settings saved");
|
|
262
271
|
} catch {
|
|
263
|
-
showToast(
|
|
272
|
+
showToast("Failed to save settings", "error");
|
|
264
273
|
}
|
|
265
274
|
}, [saveNow, showToast]);
|
|
266
275
|
|
|
267
276
|
const handleReset = useCallback(() => {
|
|
268
277
|
resetToDefaults();
|
|
269
|
-
showToast(
|
|
278
|
+
showToast("Reset to defaults");
|
|
270
279
|
}, [resetToDefaults, showToast]);
|
|
271
280
|
|
|
272
281
|
const startEditingNumber = useCallback(() => {
|
|
273
282
|
const setting = categorySettings[selectedIndex];
|
|
274
|
-
if (!setting || setting.type !==
|
|
275
|
-
|
|
283
|
+
if (!setting || setting.type !== "number") return;
|
|
284
|
+
|
|
276
285
|
const currentValue = setting.getValue(config);
|
|
277
|
-
setInputValue(currentValue === null ?
|
|
286
|
+
setInputValue(currentValue === null ? "" : String(currentValue));
|
|
278
287
|
setEditingSettingKey(setting.key);
|
|
279
288
|
}, [categorySettings, selectedIndex, config]);
|
|
280
289
|
|
|
281
290
|
const commitNumberEdit = useCallback(() => {
|
|
282
291
|
if (editingSettingKey === null) return;
|
|
283
|
-
|
|
284
|
-
const setting = categorySettings.find(s => s.key === editingSettingKey);
|
|
285
|
-
if (!setting || setting.type !==
|
|
292
|
+
|
|
293
|
+
const setting = categorySettings.find((s) => s.key === editingSettingKey);
|
|
294
|
+
if (!setting || setting.type !== "number") {
|
|
286
295
|
setEditingSettingKey(null);
|
|
287
|
-
setInputValue(
|
|
296
|
+
setInputValue("");
|
|
288
297
|
return;
|
|
289
298
|
}
|
|
290
|
-
|
|
291
|
-
const parsed = inputValue.trim() ===
|
|
299
|
+
|
|
300
|
+
const parsed = inputValue.trim() === "" ? null : parseCurrencyInput(inputValue);
|
|
292
301
|
updateConfig(setting.setValue(config, parsed));
|
|
293
302
|
setEditingSettingKey(null);
|
|
294
|
-
setInputValue(
|
|
303
|
+
setInputValue("");
|
|
295
304
|
}, [editingSettingKey, categorySettings, inputValue, config, updateConfig]);
|
|
296
305
|
|
|
297
306
|
const cancelNumberEdit = useCallback(() => {
|
|
298
307
|
setEditingSettingKey(null);
|
|
299
|
-
setInputValue(
|
|
308
|
+
setInputValue("");
|
|
300
309
|
}, []);
|
|
301
310
|
|
|
302
311
|
const toggleCurrentSetting = useCallback(() => {
|
|
@@ -306,9 +315,9 @@ export function SettingsModal({ onClose }: SettingsModalProps) {
|
|
|
306
315
|
const currentValue = setting.getValue(config);
|
|
307
316
|
let newConfig: AppConfig;
|
|
308
317
|
|
|
309
|
-
if (setting.type ===
|
|
318
|
+
if (setting.type === "toggle") {
|
|
310
319
|
newConfig = setting.setValue(config, !currentValue);
|
|
311
|
-
} else if (setting.type ===
|
|
320
|
+
} else if (setting.type === "select" && setting.options && setting.options.length > 0) {
|
|
312
321
|
const currentIdx = setting.options.indexOf(currentValue as string);
|
|
313
322
|
const nextIdx = (currentIdx + 1) % setting.options.length;
|
|
314
323
|
const newValue = setting.options[nextIdx]!;
|
|
@@ -324,90 +333,90 @@ export function SettingsModal({ onClose }: SettingsModalProps) {
|
|
|
324
333
|
if (showThemePicker) return;
|
|
325
334
|
|
|
326
335
|
if (editingSettingKey !== null) {
|
|
327
|
-
if (key.ctrl && (key.name ===
|
|
336
|
+
if (key.ctrl && (key.name === "p" || key.name === "s")) {
|
|
328
337
|
return;
|
|
329
338
|
}
|
|
330
|
-
if (key.name ===
|
|
339
|
+
if (key.name === "escape") {
|
|
331
340
|
cancelNumberEdit();
|
|
332
341
|
return;
|
|
333
342
|
}
|
|
334
|
-
if (key.name ===
|
|
343
|
+
if (key.name === "return") {
|
|
335
344
|
commitNumberEdit();
|
|
336
345
|
return;
|
|
337
346
|
}
|
|
338
|
-
if (key.name ===
|
|
339
|
-
setInputValue(prev => prev.slice(0, -1));
|
|
347
|
+
if (key.name === "backspace") {
|
|
348
|
+
setInputValue((prev) => prev.slice(0, -1));
|
|
340
349
|
return;
|
|
341
350
|
}
|
|
342
351
|
if (key.sequence && /^[0-9.]$/.test(key.sequence)) {
|
|
343
|
-
setInputValue(prev => prev + key.sequence);
|
|
352
|
+
setInputValue((prev) => prev + key.sequence);
|
|
344
353
|
return;
|
|
345
354
|
}
|
|
346
355
|
return;
|
|
347
356
|
}
|
|
348
357
|
|
|
349
|
-
if (key.name ===
|
|
358
|
+
if (key.name === "escape") {
|
|
350
359
|
onClose();
|
|
351
360
|
return;
|
|
352
361
|
}
|
|
353
362
|
|
|
354
|
-
if (key.ctrl && key.name ===
|
|
363
|
+
if (key.ctrl && key.name === "s") {
|
|
355
364
|
handleSave();
|
|
356
365
|
return;
|
|
357
366
|
}
|
|
358
367
|
|
|
359
|
-
if (key.shift && key.name ===
|
|
368
|
+
if (key.shift && key.name === "r") {
|
|
360
369
|
handleReset();
|
|
361
370
|
return;
|
|
362
371
|
}
|
|
363
372
|
|
|
364
|
-
if (key.name ===
|
|
365
|
-
setFocusedPane(p => p ===
|
|
373
|
+
if (key.name === "tab") {
|
|
374
|
+
setFocusedPane((p) => (p === "categories" ? "settings" : "categories"));
|
|
366
375
|
return;
|
|
367
376
|
}
|
|
368
377
|
|
|
369
|
-
if (focusedPane ===
|
|
370
|
-
const catIdx = CATEGORIES.findIndex(c => c.id === selectedCategory);
|
|
371
|
-
if (key.name ===
|
|
378
|
+
if (focusedPane === "categories") {
|
|
379
|
+
const catIdx = CATEGORIES.findIndex((c) => c.id === selectedCategory);
|
|
380
|
+
if (key.name === "down" || key.name === "j") {
|
|
372
381
|
const nextIdx = Math.min(catIdx + 1, CATEGORIES.length - 1);
|
|
373
382
|
setSelectedCategory(CATEGORIES[nextIdx]!.id);
|
|
374
383
|
setSelectedIndex(0);
|
|
375
|
-
} else if (key.name ===
|
|
384
|
+
} else if (key.name === "up" || key.name === "k") {
|
|
376
385
|
const prevIdx = Math.max(catIdx - 1, 0);
|
|
377
386
|
setSelectedCategory(CATEGORIES[prevIdx]!.id);
|
|
378
387
|
setSelectedIndex(0);
|
|
379
|
-
} else if (key.name ===
|
|
380
|
-
setFocusedPane(
|
|
388
|
+
} else if (key.name === "return" || key.name === "right" || key.name === "l") {
|
|
389
|
+
setFocusedPane("settings");
|
|
381
390
|
}
|
|
382
391
|
return;
|
|
383
392
|
}
|
|
384
393
|
|
|
385
|
-
if (focusedPane ===
|
|
386
|
-
if (key.name ===
|
|
387
|
-
setSelectedIndex(i => Math.min(i + 1, categorySettings.length - 1));
|
|
388
|
-
} else if (key.name ===
|
|
389
|
-
setSelectedIndex(i => Math.max(i - 1, 0));
|
|
390
|
-
} else if (key.name ===
|
|
394
|
+
if (focusedPane === "settings") {
|
|
395
|
+
if (key.name === "down" || key.name === "j") {
|
|
396
|
+
setSelectedIndex((i) => Math.min(i + 1, categorySettings.length - 1));
|
|
397
|
+
} else if (key.name === "up" || key.name === "k") {
|
|
398
|
+
setSelectedIndex((i) => Math.max(i - 1, 0));
|
|
399
|
+
} else if (key.name === "return" || key.name === "space") {
|
|
391
400
|
const setting = categorySettings[selectedIndex];
|
|
392
|
-
if (setting?.key ===
|
|
401
|
+
if (setting?.key === "theme") {
|
|
393
402
|
setShowThemePicker(true);
|
|
394
|
-
} else if (setting?.type ===
|
|
403
|
+
} else if (setting?.type === "number") {
|
|
395
404
|
startEditingNumber();
|
|
396
405
|
} else {
|
|
397
406
|
toggleCurrentSetting();
|
|
398
407
|
}
|
|
399
|
-
} else if (key.name ===
|
|
408
|
+
} else if (key.name === "left" || key.name === "h") {
|
|
400
409
|
const setting = categorySettings[selectedIndex];
|
|
401
|
-
if (setting?.type ===
|
|
410
|
+
if (setting?.type === "select" && setting.options && setting.options.length > 0) {
|
|
402
411
|
const currentValue = setting.getValue(config) as string;
|
|
403
412
|
const currentIdx = setting.options.indexOf(currentValue);
|
|
404
413
|
const prevIdx = (currentIdx - 1 + setting.options.length) % setting.options.length;
|
|
405
414
|
const newValue = setting.options[prevIdx]!;
|
|
406
415
|
updateConfig(setting.setValue(config, newValue));
|
|
407
416
|
}
|
|
408
|
-
} else if (key.name ===
|
|
417
|
+
} else if (key.name === "right" || key.name === "l") {
|
|
409
418
|
const setting = categorySettings[selectedIndex];
|
|
410
|
-
if (setting?.type ===
|
|
419
|
+
if (setting?.type === "select" && setting.options && setting.options.length > 0) {
|
|
411
420
|
const currentValue = setting.getValue(config) as string;
|
|
412
421
|
const currentIdx = setting.options.indexOf(currentValue);
|
|
413
422
|
const nextIdx = (currentIdx + 1) % setting.options.length;
|
|
@@ -430,23 +439,25 @@ export function SettingsModal({ onClose }: SettingsModalProps) {
|
|
|
430
439
|
backgroundColor={colors.background}
|
|
431
440
|
overflow="hidden"
|
|
432
441
|
>
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
442
|
+
<box
|
|
443
|
+
flexDirection="row"
|
|
444
|
+
justifyContent="space-between"
|
|
445
|
+
paddingX={1}
|
|
446
|
+
backgroundColor={colors.foreground}
|
|
447
|
+
height={1}
|
|
448
|
+
flexShrink={0}
|
|
449
|
+
>
|
|
450
|
+
<text fg={colors.primary}>
|
|
451
|
+
<strong>SETTINGS</strong>
|
|
452
|
+
</text>
|
|
453
|
+
<text fg={colors.textSubtle}>Esc:close ^S:save Shift+R:reset</text>
|
|
454
|
+
</box>
|
|
444
455
|
|
|
445
456
|
{demoMode && (
|
|
446
457
|
<box flexDirection="row" height={1} paddingLeft={1} gap={2}>
|
|
447
458
|
<text fg={colors.warning}>DEMO MODE</text>
|
|
448
459
|
<text fg={colors.textMuted}>
|
|
449
|
-
Seed: {seed ??
|
|
460
|
+
Seed: {seed ?? "default"} | Preset: {preset ?? "normal"}
|
|
450
461
|
</text>
|
|
451
462
|
</box>
|
|
452
463
|
)}
|
|
@@ -456,20 +467,23 @@ export function SettingsModal({ onClose }: SettingsModalProps) {
|
|
|
456
467
|
flexDirection="column"
|
|
457
468
|
width={20}
|
|
458
469
|
border
|
|
459
|
-
borderStyle={focusedPane ===
|
|
460
|
-
borderColor={focusedPane ===
|
|
470
|
+
borderStyle={focusedPane === "categories" ? "double" : "single"}
|
|
471
|
+
borderColor={focusedPane === "categories" ? colors.primary : colors.border}
|
|
461
472
|
padding={1}
|
|
462
473
|
>
|
|
463
474
|
{CATEGORIES.map((cat) => {
|
|
464
475
|
const isActive = cat.id === selectedCategory;
|
|
465
|
-
const isFocusedActive = isActive && focusedPane ===
|
|
476
|
+
const isFocusedActive = isActive && focusedPane === "categories";
|
|
466
477
|
return (
|
|
467
478
|
<box key={cat.id} height={1}>
|
|
468
479
|
<text
|
|
469
|
-
fg={
|
|
480
|
+
fg={
|
|
481
|
+
isFocusedActive ? colors.background : isActive ? colors.primary : colors.text
|
|
482
|
+
}
|
|
470
483
|
{...(isFocusedActive ? { bg: colors.primary } : {})}
|
|
471
484
|
>
|
|
472
|
-
{isActive ?
|
|
485
|
+
{isActive ? "▸ " : " "}
|
|
486
|
+
{cat.label}
|
|
473
487
|
</text>
|
|
474
488
|
</box>
|
|
475
489
|
);
|
|
@@ -480,8 +494,8 @@ export function SettingsModal({ onClose }: SettingsModalProps) {
|
|
|
480
494
|
flexDirection="column"
|
|
481
495
|
flexGrow={1}
|
|
482
496
|
border
|
|
483
|
-
borderStyle={focusedPane ===
|
|
484
|
-
borderColor={focusedPane ===
|
|
497
|
+
borderStyle={focusedPane === "settings" ? "double" : "single"}
|
|
498
|
+
borderColor={focusedPane === "settings" ? colors.primary : colors.border}
|
|
485
499
|
padding={0}
|
|
486
500
|
overflow="hidden"
|
|
487
501
|
>
|
|
@@ -511,30 +525,32 @@ export function SettingsModal({ onClose }: SettingsModalProps) {
|
|
|
511
525
|
<box flexDirection="column" flexGrow={1} padding={1}>
|
|
512
526
|
<box flexDirection="row" justifyContent="space-between" marginBottom={1} height={1}>
|
|
513
527
|
<text fg={colors.textMuted}>
|
|
514
|
-
{CATEGORIES.find(c => c.id === selectedCategory)?.label.toUpperCase()}
|
|
528
|
+
{CATEGORIES.find((c) => c.id === selectedCategory)?.label.toUpperCase()}
|
|
515
529
|
</text>
|
|
516
530
|
{(hasMoreAbove || hasMoreBelow) && (
|
|
517
531
|
<text fg={colors.textSubtle}>
|
|
518
|
-
{hasMoreAbove ?
|
|
532
|
+
{hasMoreAbove ? "▲" : " "}
|
|
533
|
+
{hasMoreBelow ? "▼" : " "}
|
|
519
534
|
</text>
|
|
520
535
|
)}
|
|
521
536
|
</box>
|
|
522
537
|
|
|
523
538
|
{visibleSettings.map((setting) => {
|
|
524
|
-
const realIdx = categorySettings.findIndex(s => s.key === setting.key);
|
|
525
|
-
const isSelected = realIdx === selectedIndex && focusedPane ===
|
|
539
|
+
const realIdx = categorySettings.findIndex((s) => s.key === setting.key);
|
|
540
|
+
const isSelected = realIdx === selectedIndex && focusedPane === "settings";
|
|
526
541
|
const value = setting.getValue(config);
|
|
527
|
-
const isEditingThis =
|
|
542
|
+
const isEditingThis =
|
|
543
|
+
setting.type === "number" && editingSettingKey === setting.key;
|
|
528
544
|
|
|
529
545
|
let displayValue: string;
|
|
530
|
-
if (setting.key ===
|
|
531
|
-
const themeName = themes.find(t => t.id === value)?.name ?? value;
|
|
546
|
+
if (setting.key === "theme") {
|
|
547
|
+
const themeName = themes.find((t) => t.id === value)?.name ?? value;
|
|
532
548
|
displayValue = `${themeName} ▸`;
|
|
533
|
-
} else if (setting.type ===
|
|
534
|
-
displayValue = value ?
|
|
535
|
-
} else if (setting.type ===
|
|
549
|
+
} else if (setting.type === "toggle") {
|
|
550
|
+
displayValue = value ? "● ON" : "○ OFF";
|
|
551
|
+
} else if (setting.type === "select") {
|
|
536
552
|
displayValue = `◂ ${value} ▸`;
|
|
537
|
-
} else if (setting.type ===
|
|
553
|
+
} else if (setting.type === "number" && !isEditingThis) {
|
|
538
554
|
displayValue = formatBudgetDisplay(value as number | null);
|
|
539
555
|
} else {
|
|
540
556
|
displayValue = String(value);
|
|
@@ -549,7 +565,7 @@ export function SettingsModal({ onClose }: SettingsModalProps) {
|
|
|
549
565
|
paddingX={1}
|
|
550
566
|
{...(isSelected && !isEditingThis ? { backgroundColor: colors.primary } : {})}
|
|
551
567
|
>
|
|
552
|
-
<text
|
|
568
|
+
<text
|
|
553
569
|
flexGrow={1}
|
|
554
570
|
fg={isSelected && !isEditingThis ? colors.background : colors.text}
|
|
555
571
|
>
|
|
@@ -557,7 +573,8 @@ export function SettingsModal({ onClose }: SettingsModalProps) {
|
|
|
557
573
|
</text>
|
|
558
574
|
{isEditingThis ? (
|
|
559
575
|
<text fg={colors.text}>
|
|
560
|
-
${inputValue}
|
|
576
|
+
${inputValue}
|
|
577
|
+
<span fg={colors.primary}>█</span>
|
|
561
578
|
</text>
|
|
562
579
|
) : (
|
|
563
580
|
<text fg={isSelected ? colors.background : colors.textMuted}>
|
|
@@ -572,15 +589,15 @@ export function SettingsModal({ onClose }: SettingsModalProps) {
|
|
|
572
589
|
</box>
|
|
573
590
|
</box>
|
|
574
591
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
592
|
+
<box flexDirection="row" height={1} paddingX={1} backgroundColor={colors.foreground}>
|
|
593
|
+
<text fg={colors.textSubtle}>
|
|
594
|
+
{editingSettingKey !== null
|
|
595
|
+
? "Type value Enter:save Esc:cancel"
|
|
596
|
+
: showThemePicker
|
|
597
|
+
? "↑↓:navigate ←→:scheme Tab:switch Enter:apply Esc:cancel"
|
|
598
|
+
: "Tab:switch ↑↓:navigate ←→:adjust Enter:edit"}
|
|
599
|
+
</text>
|
|
600
|
+
</box>
|
|
584
601
|
</box>
|
|
585
602
|
</ModalBackdrop>
|
|
586
603
|
);
|