@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
|
@@ -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
|
}
|
|
@@ -252,116 +284,118 @@ export function SettingsView() {
|
|
|
252
284
|
const colors = useColors();
|
|
253
285
|
const { showToast } = useToastContext();
|
|
254
286
|
const { config, isLoading, updateConfig, resetToDefaults, saveNow } = useConfig();
|
|
255
|
-
const { providers, notifications } = usePlugins();
|
|
287
|
+
const { providers, agents, themes, 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
296
|
const allPlugins = [
|
|
265
297
|
...Array.from(providers.values()).map((p) => p.plugin),
|
|
298
|
+
...agents,
|
|
299
|
+
...themes,
|
|
266
300
|
...notifications,
|
|
267
301
|
];
|
|
268
302
|
return buildPluginSettings(allPlugins);
|
|
269
|
-
}, [providers,
|
|
303
|
+
}, [providers, agents, themes, notifications]);
|
|
270
304
|
|
|
271
305
|
const allSettings = useMemo(() => [...SETTINGS, ...pluginSettings], [pluginSettings]);
|
|
272
|
-
const categorySettings = allSettings.filter(s => s.category === selectedCategory);
|
|
273
|
-
|
|
306
|
+
const categorySettings = allSettings.filter((s) => s.category === selectedCategory);
|
|
307
|
+
|
|
274
308
|
const helpText = useMemo(() => {
|
|
275
309
|
if (terminalWidth >= 100) {
|
|
276
|
-
return
|
|
310
|
+
return "Tab: switch panes ↑↓: navigate Enter/Space: toggle ←→: adjust Ctrl+S: save Shift+R: reset";
|
|
277
311
|
} else if (terminalWidth >= 80) {
|
|
278
|
-
return
|
|
312
|
+
return "Tab: switch ↑↓: nav Enter: toggle ←→: adjust ^S: save R: reset";
|
|
279
313
|
}
|
|
280
|
-
return
|
|
314
|
+
return "Tab ↑↓ Enter ←→ ^S:save R:reset";
|
|
281
315
|
}, [terminalWidth]);
|
|
282
|
-
|
|
316
|
+
|
|
283
317
|
const handleSave = useCallback(async () => {
|
|
284
318
|
try {
|
|
285
319
|
await saveNow();
|
|
286
|
-
showToast(
|
|
320
|
+
showToast("Settings saved");
|
|
287
321
|
} catch {
|
|
288
|
-
showToast(
|
|
322
|
+
showToast("Failed to save settings", "error");
|
|
289
323
|
}
|
|
290
324
|
}, [saveNow, showToast]);
|
|
291
|
-
|
|
325
|
+
|
|
292
326
|
const handleReset = useCallback(() => {
|
|
293
327
|
resetToDefaults();
|
|
294
|
-
showToast(
|
|
328
|
+
showToast("Reset to defaults");
|
|
295
329
|
}, [resetToDefaults, showToast]);
|
|
296
|
-
|
|
330
|
+
|
|
297
331
|
const toggleCurrentSetting = useCallback(() => {
|
|
298
332
|
const setting = categorySettings[selectedIndex];
|
|
299
333
|
if (!setting) return;
|
|
300
|
-
|
|
334
|
+
|
|
301
335
|
const currentValue = setting.getValue(config);
|
|
302
336
|
let newConfig: AppConfig;
|
|
303
|
-
|
|
304
|
-
if (setting.type ===
|
|
337
|
+
|
|
338
|
+
if (setting.type === "toggle") {
|
|
305
339
|
newConfig = setting.setValue(config, !currentValue);
|
|
306
|
-
} else if (setting.type ===
|
|
340
|
+
} else if (setting.type === "select" && setting.options) {
|
|
307
341
|
const currentIdx = setting.options.indexOf(currentValue as string);
|
|
308
342
|
const nextIdx = (currentIdx + 1) % setting.options.length;
|
|
309
343
|
newConfig = setting.setValue(config, setting.options[nextIdx]!);
|
|
310
344
|
} else {
|
|
311
345
|
return;
|
|
312
346
|
}
|
|
313
|
-
|
|
347
|
+
|
|
314
348
|
updateConfig(newConfig);
|
|
315
349
|
}, [categorySettings, selectedIndex, config, updateConfig]);
|
|
316
|
-
|
|
350
|
+
|
|
317
351
|
useKeyboard((key) => {
|
|
318
|
-
if (key.ctrl && key.name ===
|
|
352
|
+
if (key.ctrl && key.name === "s") {
|
|
319
353
|
handleSave();
|
|
320
354
|
return;
|
|
321
355
|
}
|
|
322
|
-
|
|
323
|
-
if (key.shift && key.name ===
|
|
356
|
+
|
|
357
|
+
if (key.shift && key.name === "r") {
|
|
324
358
|
handleReset();
|
|
325
359
|
return;
|
|
326
360
|
}
|
|
327
|
-
|
|
328
|
-
if (key.name ===
|
|
329
|
-
setFocusedPane(p => p ===
|
|
361
|
+
|
|
362
|
+
if (key.name === "tab") {
|
|
363
|
+
setFocusedPane((p) => (p === "categories" ? "settings" : "categories"));
|
|
330
364
|
return;
|
|
331
365
|
}
|
|
332
|
-
|
|
333
|
-
if (focusedPane ===
|
|
334
|
-
const catIdx = CATEGORIES.findIndex(c => c.id === selectedCategory);
|
|
335
|
-
if (key.name ===
|
|
366
|
+
|
|
367
|
+
if (focusedPane === "categories") {
|
|
368
|
+
const catIdx = CATEGORIES.findIndex((c) => c.id === selectedCategory);
|
|
369
|
+
if (key.name === "down" || key.name === "j") {
|
|
336
370
|
const nextIdx = Math.min(catIdx + 1, CATEGORIES.length - 1);
|
|
337
371
|
setSelectedCategory(CATEGORIES[nextIdx]!.id);
|
|
338
372
|
setSelectedIndex(0);
|
|
339
|
-
} else if (key.name ===
|
|
373
|
+
} else if (key.name === "up" || key.name === "k") {
|
|
340
374
|
const prevIdx = Math.max(catIdx - 1, 0);
|
|
341
375
|
setSelectedCategory(CATEGORIES[prevIdx]!.id);
|
|
342
376
|
setSelectedIndex(0);
|
|
343
377
|
}
|
|
344
378
|
return;
|
|
345
379
|
}
|
|
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 ===
|
|
380
|
+
|
|
381
|
+
if (focusedPane === "settings") {
|
|
382
|
+
if (key.name === "down" || key.name === "j") {
|
|
383
|
+
setSelectedIndex((i) => Math.min(i + 1, categorySettings.length - 1));
|
|
384
|
+
} else if (key.name === "up" || key.name === "k") {
|
|
385
|
+
setSelectedIndex((i) => Math.max(i - 1, 0));
|
|
386
|
+
} else if (key.name === "return" || key.name === "space") {
|
|
353
387
|
toggleCurrentSetting();
|
|
354
|
-
} else if (key.name ===
|
|
388
|
+
} else if (key.name === "left" || key.name === "h") {
|
|
355
389
|
const setting = categorySettings[selectedIndex];
|
|
356
|
-
if (setting?.type ===
|
|
390
|
+
if (setting?.type === "select" && setting.options) {
|
|
357
391
|
const currentValue = setting.getValue(config) as string;
|
|
358
392
|
const currentIdx = setting.options.indexOf(currentValue);
|
|
359
393
|
const prevIdx = (currentIdx - 1 + setting.options.length) % setting.options.length;
|
|
360
394
|
updateConfig(setting.setValue(config, setting.options[prevIdx]!));
|
|
361
395
|
}
|
|
362
|
-
} else if (key.name ===
|
|
396
|
+
} else if (key.name === "right" || key.name === "l") {
|
|
363
397
|
const setting = categorySettings[selectedIndex];
|
|
364
|
-
if (setting?.type ===
|
|
398
|
+
if (setting?.type === "select" && setting.options) {
|
|
365
399
|
const currentValue = setting.getValue(config) as string;
|
|
366
400
|
const currentIdx = setting.options.indexOf(currentValue);
|
|
367
401
|
const nextIdx = (currentIdx + 1) % setting.options.length;
|
|
@@ -370,7 +404,7 @@ export function SettingsView() {
|
|
|
370
404
|
}
|
|
371
405
|
}
|
|
372
406
|
});
|
|
373
|
-
|
|
407
|
+
|
|
374
408
|
if (isLoading) {
|
|
375
409
|
return (
|
|
376
410
|
<box flexDirection="column" flexGrow={1} justifyContent="center" alignItems="center">
|
|
@@ -378,85 +412,87 @@ export function SettingsView() {
|
|
|
378
412
|
</box>
|
|
379
413
|
);
|
|
380
414
|
}
|
|
381
|
-
|
|
415
|
+
|
|
382
416
|
return (
|
|
383
417
|
<box flexDirection="column" flexGrow={1} padding={1} gap={1}>
|
|
384
418
|
{demoMode && (
|
|
385
419
|
<box flexDirection="row" height={1} paddingLeft={1} gap={2}>
|
|
386
420
|
<text fg={colors.warning}>DEMO MODE</text>
|
|
387
421
|
<text fg={colors.textMuted}>
|
|
388
|
-
Seed: {seed ??
|
|
422
|
+
Seed: {seed ?? "default"} | Preset: {preset ?? "normal"}
|
|
389
423
|
</text>
|
|
390
424
|
</box>
|
|
391
425
|
)}
|
|
392
426
|
<box flexDirection="row" gap={1} flexGrow={1}>
|
|
393
|
-
<box
|
|
394
|
-
flexDirection="column"
|
|
395
|
-
width={20}
|
|
396
|
-
border
|
|
397
|
-
borderStyle={focusedPane ===
|
|
398
|
-
borderColor={focusedPane ===
|
|
427
|
+
<box
|
|
428
|
+
flexDirection="column"
|
|
429
|
+
width={20}
|
|
430
|
+
border
|
|
431
|
+
borderStyle={focusedPane === "categories" ? "double" : "single"}
|
|
432
|
+
borderColor={focusedPane === "categories" ? colors.primary : colors.border}
|
|
399
433
|
padding={1}
|
|
400
434
|
>
|
|
401
|
-
<text fg={colors.textMuted} marginBottom={1}>
|
|
435
|
+
<text fg={colors.textMuted} marginBottom={1}>
|
|
436
|
+
CATEGORIES
|
|
437
|
+
</text>
|
|
402
438
|
{CATEGORIES.map((cat) => {
|
|
403
439
|
const isActive = cat.id === selectedCategory;
|
|
404
|
-
const isFocusedActive = isActive && focusedPane ===
|
|
440
|
+
const isFocusedActive = isActive && focusedPane === "categories";
|
|
405
441
|
return (
|
|
406
442
|
<box key={cat.id} height={1}>
|
|
407
|
-
<text
|
|
443
|
+
<text
|
|
408
444
|
fg={isFocusedActive ? colors.background : isActive ? colors.primary : colors.text}
|
|
409
445
|
{...(isFocusedActive ? { bg: colors.primary } : {})}
|
|
410
446
|
>
|
|
411
|
-
{isActive ?
|
|
447
|
+
{isActive ? "▸ " : " "}
|
|
448
|
+
{cat.label}
|
|
412
449
|
</text>
|
|
413
450
|
</box>
|
|
414
451
|
);
|
|
415
452
|
})}
|
|
416
453
|
</box>
|
|
417
|
-
|
|
418
|
-
<box
|
|
419
|
-
flexDirection="column"
|
|
454
|
+
|
|
455
|
+
<box
|
|
456
|
+
flexDirection="column"
|
|
420
457
|
flexGrow={1}
|
|
421
|
-
border
|
|
422
|
-
borderStyle={focusedPane ===
|
|
423
|
-
borderColor={focusedPane ===
|
|
458
|
+
border
|
|
459
|
+
borderStyle={focusedPane === "settings" ? "double" : "single"}
|
|
460
|
+
borderColor={focusedPane === "settings" ? colors.primary : colors.border}
|
|
424
461
|
padding={1}
|
|
425
462
|
>
|
|
426
463
|
<text fg={colors.textMuted} marginBottom={1}>
|
|
427
|
-
{CATEGORIES.find(c => c.id === selectedCategory)?.label.toUpperCase()}
|
|
464
|
+
{CATEGORIES.find((c) => c.id === selectedCategory)?.label.toUpperCase()}
|
|
428
465
|
</text>
|
|
429
|
-
|
|
466
|
+
|
|
430
467
|
{categorySettings.map((setting, idx) => {
|
|
431
|
-
const isSelected = idx === selectedIndex && focusedPane ===
|
|
468
|
+
const isSelected = idx === selectedIndex && focusedPane === "settings";
|
|
432
469
|
const value = setting.getValue(config);
|
|
433
|
-
|
|
470
|
+
|
|
434
471
|
return (
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
<text fg={isSelected ? colors.background : colors.text}>
|
|
444
|
-
{setting.label}
|
|
445
|
-
</text>
|
|
472
|
+
<box
|
|
473
|
+
key={setting.key}
|
|
474
|
+
flexDirection="row"
|
|
475
|
+
justifyContent="space-between"
|
|
476
|
+
height={2}
|
|
477
|
+
paddingX={1}
|
|
478
|
+
{...(isSelected ? { backgroundColor: colors.primary } : {})}
|
|
479
|
+
>
|
|
480
|
+
<text fg={isSelected ? colors.background : colors.text}>{setting.label}</text>
|
|
446
481
|
<text fg={isSelected ? colors.background : colors.textMuted}>
|
|
447
|
-
{setting.type ===
|
|
448
|
-
?
|
|
449
|
-
|
|
482
|
+
{setting.type === "toggle"
|
|
483
|
+
? value
|
|
484
|
+
? "● ON"
|
|
485
|
+
: "○ OFF"
|
|
486
|
+
: setting.type === "select"
|
|
450
487
|
? `◂ ${value} ▸`
|
|
451
|
-
: String(value)
|
|
452
|
-
}
|
|
488
|
+
: String(value)}
|
|
453
489
|
</text>
|
|
454
490
|
</box>
|
|
455
491
|
);
|
|
456
492
|
})}
|
|
457
493
|
</box>
|
|
458
494
|
</box>
|
|
459
|
-
|
|
495
|
+
|
|
460
496
|
<box flexDirection="row" height={1} paddingLeft={1}>
|
|
461
497
|
<text fg={colors.textSubtle}>{helpText}</text>
|
|
462
498
|
</box>
|