@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.
Files changed (158) hide show
  1. package/README.md +2 -5
  2. package/package.json +8 -5
  3. package/src/agents/aggregator.ts +39 -19
  4. package/src/agents/costing.ts +11 -9
  5. package/src/agents/index.ts +7 -9
  6. package/src/agents/types.ts +2 -2
  7. package/src/cli.ts +28 -23
  8. package/src/config/schema.test.ts +42 -40
  9. package/src/config/schema.ts +26 -26
  10. package/src/demo/simulator.ts +161 -122
  11. package/src/plugins/agents/index.ts +2 -2
  12. package/src/plugins/auth-sources.ts +42 -38
  13. package/src/plugins/lifecycle.ts +32 -32
  14. package/src/plugins/loader.test.ts +49 -49
  15. package/src/plugins/loader.ts +67 -63
  16. package/src/plugins/notification-bus.ts +21 -16
  17. package/src/plugins/notifications/index.ts +2 -2
  18. package/src/plugins/notifications/terminal-bell.ts +29 -23
  19. package/src/plugins/notifications/visual-flash.ts +21 -19
  20. package/src/plugins/npm-installer.ts +23 -25
  21. package/src/plugins/plugin-context-factory.ts +10 -10
  22. package/src/plugins/plugin-host.ts +12 -5
  23. package/src/plugins/providers/anthropic.ts +64 -57
  24. package/src/plugins/providers/antigravity.ts +81 -69
  25. package/src/plugins/providers/codex.ts +42 -42
  26. package/src/plugins/providers/gemini.ts +106 -87
  27. package/src/plugins/providers/github-copilot.ts +49 -45
  28. package/src/plugins/providers/index.ts +10 -12
  29. package/src/plugins/providers/minimax.ts +50 -44
  30. package/src/plugins/providers/openai-api.ts +82 -62
  31. package/src/plugins/providers/opencode-zen.ts +46 -39
  32. package/src/plugins/providers/perplexity.ts +55 -46
  33. package/src/plugins/providers/zai.ts +66 -57
  34. package/src/plugins/registry.ts +47 -43
  35. package/src/plugins/sandbox-guard.test.ts +45 -48
  36. package/src/plugins/sandbox-guard.ts +20 -25
  37. package/src/plugins/sandbox.ts +26 -48
  38. package/src/plugins/themes/catppuccin-latte.ts +29 -29
  39. package/src/plugins/themes/catppuccin-mocha.ts +29 -29
  40. package/src/plugins/themes/claude-code.ts +29 -29
  41. package/src/plugins/themes/dracula.ts +29 -29
  42. package/src/plugins/themes/github-light.ts +29 -29
  43. package/src/plugins/themes/gruvbox-dark.ts +29 -29
  44. package/src/plugins/themes/gruvbox-light.ts +29 -29
  45. package/src/plugins/themes/index.ts +15 -15
  46. package/src/plugins/themes/kanagawa.ts +29 -29
  47. package/src/plugins/themes/nord.ts +29 -29
  48. package/src/plugins/themes/one-dark.ts +29 -29
  49. package/src/plugins/themes/opencode.ts +31 -31
  50. package/src/plugins/themes/rose-pine-dawn.ts +29 -29
  51. package/src/plugins/themes/rose-pine.ts +29 -29
  52. package/src/plugins/themes/solarized-light.ts +29 -29
  53. package/src/plugins/themes/tokyo-night.ts +29 -29
  54. package/src/plugins/types/agent.ts +5 -5
  55. package/src/plugins/types/base.ts +35 -24
  56. package/src/plugins/types/index.ts +9 -9
  57. package/src/plugins/types/notification.ts +12 -12
  58. package/src/plugins/types/provider.ts +12 -8
  59. package/src/plugins/types/theme.ts +31 -23
  60. package/src/plugins/update-checker.ts +6 -6
  61. package/src/pricing/estimator.test.ts +39 -44
  62. package/src/pricing/estimator.ts +7 -13
  63. package/src/pricing/fallback.test.ts +30 -34
  64. package/src/pricing/fallback.ts +54 -27
  65. package/src/pricing/index.ts +18 -12
  66. package/src/pricing/models-dev.ts +6 -6
  67. package/src/storage/database.ts +56 -46
  68. package/src/storage/db.ts +12 -12
  69. package/src/storage/index.ts +44 -45
  70. package/src/storage/migrations/index.ts +2 -2
  71. package/src/storage/paths.ts +13 -13
  72. package/src/storage/repos/agentSessions.ts +33 -23
  73. package/src/storage/repos/providerSnapshots.ts +5 -5
  74. package/src/storage/repos/usageEvents.ts +40 -28
  75. package/src/storage/types.ts +5 -5
  76. package/src/tui/App.tsx +180 -137
  77. package/src/tui/components/CommandPalette.tsx +46 -34
  78. package/src/tui/components/DebugConsole.tsx +39 -33
  79. package/src/tui/components/DebugPanel.tsx +295 -177
  80. package/src/tui/components/FooterHints.tsx +2 -2
  81. package/src/tui/components/GhostProviderCard.tsx +37 -35
  82. package/src/tui/components/Header.tsx +68 -35
  83. package/src/tui/components/HelpOverlay.tsx +99 -29
  84. package/src/tui/components/InlineGauge.tsx +10 -7
  85. package/src/tui/components/InlineSparkline.tsx +17 -11
  86. package/src/tui/components/KpiStrip.tsx +128 -98
  87. package/src/tui/components/LimitGauge.tsx +96 -59
  88. package/src/tui/components/ModalBackdrop.tsx +4 -4
  89. package/src/tui/components/ProviderAggregateStrip.tsx +69 -38
  90. package/src/tui/components/ProviderCard.tsx +232 -226
  91. package/src/tui/components/ProviderDetailPanel.tsx +133 -58
  92. package/src/tui/components/ProviderLimitsPanel.tsx +126 -89
  93. package/src/tui/components/ProvidersList.tsx +215 -126
  94. package/src/tui/components/SessionDetailsDrawer.tsx +191 -94
  95. package/src/tui/components/SessionsTable.tsx +313 -166
  96. package/src/tui/components/SettingsModal.tsx +231 -214
  97. package/src/tui/components/SidebarBreakdown.tsx +30 -16
  98. package/src/tui/components/Skeleton.tsx +24 -17
  99. package/src/tui/components/SmartSidebar.tsx +360 -258
  100. package/src/tui/components/Sparkline.tsx +64 -52
  101. package/src/tui/components/Spinner.tsx +3 -3
  102. package/src/tui/components/StatusBar.tsx +27 -27
  103. package/src/tui/components/ThemePicker.tsx +67 -66
  104. package/src/tui/components/Toast.tsx +9 -6
  105. package/src/tui/components/UsageGauge.tsx +10 -13
  106. package/src/tui/components/index.ts +5 -5
  107. package/src/tui/contexts/AgentSessionContext.tsx +193 -144
  108. package/src/tui/contexts/ConfigContext.tsx +15 -13
  109. package/src/tui/contexts/DashboardRuntimeContext.tsx +26 -26
  110. package/src/tui/contexts/DemoModeContext.tsx +22 -15
  111. package/src/tui/contexts/DrawerContext.tsx +11 -9
  112. package/src/tui/contexts/InputContext.tsx +2 -2
  113. package/src/tui/contexts/LogContext.tsx +62 -47
  114. package/src/tui/contexts/PluginContext.tsx +232 -183
  115. package/src/tui/contexts/RealTimeActivityContext.tsx +70 -36
  116. package/src/tui/contexts/StorageContext.tsx +151 -117
  117. package/src/tui/contexts/ThemeContext.tsx +21 -16
  118. package/src/tui/contexts/TimeWindowContext.tsx +72 -61
  119. package/src/tui/contexts/ToastContext.tsx +10 -7
  120. package/src/tui/contexts/index.ts +9 -5
  121. package/src/tui/createApp.tsx +8 -12
  122. package/src/tui/debug/captureFrame.ts +25 -28
  123. package/src/tui/debug/snapshot.tsx +179 -162
  124. package/src/tui/driver/assertions.ts +30 -22
  125. package/src/tui/driver/cli.ts +231 -221
  126. package/src/tui/driver/coverage.ts +27 -44
  127. package/src/tui/driver/demo-snapshot.ts +33 -33
  128. package/src/tui/driver/diff.ts +26 -32
  129. package/src/tui/driver/driver.ts +43 -48
  130. package/src/tui/driver/index.ts +36 -40
  131. package/src/tui/driver/recorder.ts +37 -48
  132. package/src/tui/driver/test-workflow.ts +51 -51
  133. package/src/tui/driver/test.ts +25 -25
  134. package/src/tui/hooks/index.ts +24 -20
  135. package/src/tui/hooks/useAnimatedValue.ts +13 -18
  136. package/src/tui/hooks/useDashboardKeyboard.ts +118 -98
  137. package/src/tui/hooks/useDashboardState.ts +26 -24
  138. package/src/tui/hooks/useEmaActivity.ts +82 -74
  139. package/src/tui/hooks/useEntranceAnimation.ts +11 -11
  140. package/src/tui/hooks/useExitAnimation.ts +3 -3
  141. package/src/tui/hooks/usePulse.ts +4 -4
  142. package/src/tui/hooks/useSafeRenderer.ts +5 -5
  143. package/src/tui/hooks/useSessionEnrichedProviders.ts +150 -0
  144. package/src/tui/hooks/useValueFlash.ts +10 -14
  145. package/src/tui/index.tsx +29 -6
  146. package/src/tui/shutdown.ts +19 -0
  147. package/src/tui/utils/providerColor.ts +1 -1
  148. package/src/tui/views/Dashboard.tsx +127 -102
  149. package/src/tui/views/HistoricalTrendsView.tsx +579 -250
  150. package/src/tui/views/ProjectsView.tsx +466 -237
  151. package/src/tui/views/RealTimeDashboard.tsx +150 -111
  152. package/src/tui/views/SettingsView.tsx +222 -191
  153. package/src/tui/views/index.ts +2 -2
  154. package/src/types/opentui-test-utils.d.ts +7 -7
  155. package/src/utils/clipboard.ts +29 -29
  156. package/src/utils/currency.test.ts +53 -53
  157. package/src/utils/currency.ts +35 -32
  158. package/src/version.ts +1 -1
@@ -1,20 +1,20 @@
1
- import { useState, useCallback, useMemo } from 'react';
2
- import { useKeyboard, useTerminalDimensions } from '@opentui/react';
3
- import { useColors } from '../contexts/ThemeContext.tsx';
4
- import { useToastContext } from '../contexts/ToastContext.tsx';
5
- import { useConfig } from '../contexts/ConfigContext.tsx';
6
- import { usePlugins } from '../contexts/PluginContext.tsx';
7
- import { useDemoMode } from '../contexts/DemoModeContext.tsx';
8
- import { type AppConfig } from '@/config/schema.ts';
9
- import type { ConfigField } from '@/plugins/types/base.ts';
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 = 'refresh' | 'display' | 'budgets' | 'alerts' | 'notifications' | 'plugins';
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: 'toggle' | 'select' | 'number';
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: 'intervalMs',
26
- label: 'Refresh Interval',
27
- category: 'refresh',
28
- type: 'select',
29
- options: ['30s', '1m', '2m', '5m'],
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 '30s';
33
- if (ms <= 60000) return '1m';
34
- if (ms <= 120000) return '2m';
35
- return '5m';
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> = { '30s': 30000, '1m': 60000, '2m': 120000, '5m': 300000 };
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: 'pauseAutoRefresh',
44
- label: 'Pause Auto-Refresh',
45
- category: 'refresh',
46
- type: 'toggle',
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: 'defaultTimeWindow',
52
- label: 'Default Time Window',
53
- category: 'display',
54
- type: 'select',
55
- options: ['5m', '15m', '1h', '24h', '7d', '30d', 'all'],
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) => ({ ...c, display: { ...c.display, defaultTimeWindow: v as AppConfig['display']['defaultTimeWindow'] } }),
57
+ setValue: (c, v) => ({
58
+ ...c,
59
+ display: { ...c.display, defaultTimeWindow: v as AppConfig["display"]["defaultTimeWindow"] },
60
+ }),
58
61
  },
59
62
  {
60
- key: 'sidebarCollapsed',
61
- label: 'Sidebar Collapsed',
62
- category: 'display',
63
- type: 'toggle',
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: 'timeFormat',
69
- label: 'Time Format',
70
- category: 'display',
71
- type: 'select',
72
- options: ['12h', '24h'],
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 '12h' | '24h' } }),
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: 'dailyBudget',
79
- label: 'Daily Budget ($)',
80
- category: 'budgets',
81
- type: 'select',
82
- options: ['None', '$10', '$25', '$50', '$100', '$200'],
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 'None';
88
+ if (b === null) return "None";
86
89
  return `$${b}`;
87
90
  },
88
91
  setValue: (c, v) => {
89
- const map: Record<string, number | null> = { 'None': null, '$10': 10, '$25': 25, '$50': 50, '$100': 100, '$200': 200 };
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: 'weeklyBudget',
95
- label: 'Weekly Budget ($)',
96
- category: 'budgets',
97
- type: 'select',
98
- options: ['None', '$50', '$100', '$200', '$500', '$1000'],
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 'None';
111
+ if (b === null) return "None";
102
112
  return `$${b}`;
103
113
  },
104
114
  setValue: (c, v) => {
105
- const map: Record<string, number | null> = { 'None': null, '$50': 50, '$100': 100, '$200': 200, '$500': 500, '$1000': 1000 };
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: 'monthlyBudget',
111
- label: 'Monthly Budget ($)',
112
- category: 'budgets',
113
- type: 'select',
114
- options: ['None', '$100', '$250', '$500', '$1000', '$2000'],
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 'None';
134
+ if (b === null) return "None";
118
135
  return `$${b}`;
119
136
  },
120
137
  setValue: (c, v) => {
121
- const map: Record<string, number | null> = { 'None': null, '$100': 100, '$250': 250, '$500': 500, '$1000': 1000, '$2000': 2000 };
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: 'warningPercent',
127
- label: 'Warning Threshold (%)',
128
- category: 'alerts',
129
- type: 'select',
130
- options: ['55%', '60%', '65%', '70%', '75%', '80%', '85%', '90%', '95%'],
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('%', ''), 10);
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: 'criticalPercent',
139
- label: 'Critical Threshold (%)',
140
- category: 'alerts',
141
- type: 'select',
142
- options: ['75%', '80%', '85%', '90%', '95%', '98%', '100%'],
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('%', ''), 10);
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: 'toastsEnabled',
151
- label: 'Show Toast Notifications',
152
- category: 'notifications',
153
- type: 'toggle',
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) => ({ ...c, notifications: { ...c.notifications, toastsEnabled: v as boolean } }),
179
+ setValue: (c, v) => ({
180
+ ...c,
181
+ notifications: { ...c.notifications, toastsEnabled: v as boolean },
182
+ }),
156
183
  },
157
184
  {
158
- key: 'soundEnabled',
159
- label: 'Sound Alerts',
160
- category: 'notifications',
161
- type: 'toggle',
185
+ key: "soundEnabled",
186
+ label: "Sound Alerts",
187
+ category: "notifications",
188
+ type: "toggle",
162
189
  getValue: (c) => c.notifications.soundEnabled,
163
- setValue: (c, v) => ({ ...c, notifications: { ...c.notifications, soundEnabled: v as boolean } }),
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: 'refresh', label: 'Refresh' },
169
- { id: 'display', label: 'Display' },
170
- { id: 'budgets', label: 'Budgets' },
171
- { id: 'alerts', label: 'Alerts' },
172
- { id: 'notifications', label: 'Notifications' },
173
- { id: 'plugins', label: 'Plugins' },
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(plugins: Array<{ id: string; name: string; configSchema?: Record<string, ConfigField> }>): SettingItem[] {
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 === 'boolean') {
233
+ if (field.type === "boolean") {
202
234
  items.push({
203
235
  key: settingKey,
204
236
  label,
205
- category: 'plugins' as SettingCategory,
206
- type: 'toggle',
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 === 'select' && field.options) {
242
+ } else if (field.type === "select" && field.options) {
211
243
  items.push({
212
244
  key: settingKey,
213
245
  label,
214
- category: 'plugins' as SettingCategory,
215
- type: 'select',
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 === 'number') {
259
+ } else if (field.type === "number") {
228
260
  items.push({
229
261
  key: settingKey,
230
262
  label,
231
- category: 'plugins' as SettingCategory,
232
- type: 'number',
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: 'plugins' as SettingCategory,
241
- type: 'select',
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>('refresh');
290
+
291
+ const [selectedCategory, setSelectedCategory] = useState<SettingCategory>("refresh");
260
292
  const [selectedIndex, setSelectedIndex] = useState(0);
261
- const [focusedPane, setFocusedPane] = useState<'categories' | 'settings'>('settings');
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 'Tab: switch panes ↑↓: navigate Enter/Space: toggle ←→: adjust Ctrl+S: save Shift+R: reset';
305
+ return "Tab: switch panes ↑↓: navigate Enter/Space: toggle ←→: adjust Ctrl+S: save Shift+R: reset";
277
306
  } else if (terminalWidth >= 80) {
278
- return 'Tab: switch ↑↓: nav Enter: toggle ←→: adjust ^S: save R: reset';
307
+ return "Tab: switch ↑↓: nav Enter: toggle ←→: adjust ^S: save R: reset";
279
308
  }
280
- return 'Tab ↑↓ Enter ←→ ^S:save R:reset';
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('Settings saved');
315
+ showToast("Settings saved");
287
316
  } catch {
288
- showToast('Failed to save settings', 'error');
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('Reset to defaults');
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 === 'toggle') {
332
+
333
+ if (setting.type === "toggle") {
305
334
  newConfig = setting.setValue(config, !currentValue);
306
- } else if (setting.type === 'select' && setting.options) {
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 === 's') {
347
+ if (key.ctrl && key.name === "s") {
319
348
  handleSave();
320
349
  return;
321
350
  }
322
-
323
- if (key.shift && key.name === 'r') {
351
+
352
+ if (key.shift && key.name === "r") {
324
353
  handleReset();
325
354
  return;
326
355
  }
327
-
328
- if (key.name === 'tab') {
329
- setFocusedPane(p => p === 'categories' ? 'settings' : 'categories');
356
+
357
+ if (key.name === "tab") {
358
+ setFocusedPane((p) => (p === "categories" ? "settings" : "categories"));
330
359
  return;
331
360
  }
332
-
333
- if (focusedPane === 'categories') {
334
- const catIdx = CATEGORIES.findIndex(c => c.id === selectedCategory);
335
- if (key.name === 'down' || key.name === 'j') {
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 === 'up' || key.name === 'k') {
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 === 'settings') {
348
- if (key.name === 'down' || key.name === 'j') {
349
- setSelectedIndex(i => Math.min(i + 1, categorySettings.length - 1));
350
- } else if (key.name === 'up' || key.name === 'k') {
351
- setSelectedIndex(i => Math.max(i - 1, 0));
352
- } else if (key.name === 'return' || key.name === 'space') {
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 === 'left' || key.name === 'h') {
383
+ } else if (key.name === "left" || key.name === "h") {
355
384
  const setting = categorySettings[selectedIndex];
356
- if (setting?.type === 'select' && setting.options) {
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 === 'right' || key.name === 'l') {
391
+ } else if (key.name === "right" || key.name === "l") {
363
392
  const setting = categorySettings[selectedIndex];
364
- if (setting?.type === 'select' && setting.options) {
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 ?? 'default'} | Preset: {preset ?? 'normal'}
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 === 'categories' ? 'double' : 'single'}
398
- borderColor={focusedPane === 'categories' ? colors.primary : colors.border}
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}>CATEGORIES</text>
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 === 'categories';
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 ? '▸ ' : ' '}{cat.label}
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 === 'settings' ? 'double' : 'single'}
423
- borderColor={focusedPane === 'settings' ? colors.primary : colors.border}
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 === 'settings';
463
+ const isSelected = idx === selectedIndex && focusedPane === "settings";
432
464
  const value = setting.getValue(config);
433
-
465
+
434
466
  return (
435
- <box
436
- key={setting.key}
437
- flexDirection="row"
438
- justifyContent="space-between"
439
- height={2}
440
- paddingX={1}
441
- {...(isSelected ? { backgroundColor: colors.primary } : {})}
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 === 'toggle'
448
- ? (value ? '● ON' : '○ OFF')
449
- : setting.type === 'select'
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>