@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.
Files changed (160) 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 +42 -38
  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 +36 -23
  19. package/src/plugins/notifications/visual-flash.ts +31 -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 +68 -57
  34. package/src/plugins/registry.ts +80 -71
  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/persistence-service.ts +206 -0
  73. package/src/storage/repos/agentSessions.ts +33 -23
  74. package/src/storage/repos/providerSnapshots.ts +5 -5
  75. package/src/storage/repos/usageEvents.ts +40 -28
  76. package/src/storage/retention.ts +146 -0
  77. package/src/storage/types.ts +5 -5
  78. package/src/tui/App.tsx +180 -137
  79. package/src/tui/components/CommandPalette.tsx +46 -34
  80. package/src/tui/components/DebugConsole.tsx +39 -33
  81. package/src/tui/components/DebugPanel.tsx +314 -183
  82. package/src/tui/components/FooterHints.tsx +2 -2
  83. package/src/tui/components/GhostProviderCard.tsx +37 -35
  84. package/src/tui/components/Header.tsx +68 -35
  85. package/src/tui/components/HelpOverlay.tsx +99 -29
  86. package/src/tui/components/InlineGauge.tsx +10 -7
  87. package/src/tui/components/InlineSparkline.tsx +17 -11
  88. package/src/tui/components/KpiStrip.tsx +128 -98
  89. package/src/tui/components/LimitGauge.tsx +96 -59
  90. package/src/tui/components/ModalBackdrop.tsx +4 -4
  91. package/src/tui/components/ProviderAggregateStrip.tsx +69 -38
  92. package/src/tui/components/ProviderCard.tsx +232 -226
  93. package/src/tui/components/ProviderDetailPanel.tsx +133 -58
  94. package/src/tui/components/ProviderLimitsPanel.tsx +126 -89
  95. package/src/tui/components/ProvidersList.tsx +215 -126
  96. package/src/tui/components/SessionDetailsDrawer.tsx +191 -94
  97. package/src/tui/components/SessionsTable.tsx +320 -169
  98. package/src/tui/components/SettingsModal.tsx +593 -269
  99. package/src/tui/components/SidebarBreakdown.tsx +30 -16
  100. package/src/tui/components/Skeleton.tsx +24 -17
  101. package/src/tui/components/SmartSidebar.tsx +360 -258
  102. package/src/tui/components/Sparkline.tsx +64 -52
  103. package/src/tui/components/Spinner.tsx +3 -3
  104. package/src/tui/components/StatusBar.tsx +27 -27
  105. package/src/tui/components/ThemePicker.tsx +67 -66
  106. package/src/tui/components/Toast.tsx +9 -6
  107. package/src/tui/components/UsageGauge.tsx +10 -13
  108. package/src/tui/components/index.ts +5 -5
  109. package/src/tui/contexts/AgentSessionContext.tsx +164 -151
  110. package/src/tui/contexts/ConfigContext.tsx +15 -13
  111. package/src/tui/contexts/DashboardRuntimeContext.tsx +26 -26
  112. package/src/tui/contexts/DemoModeContext.tsx +22 -15
  113. package/src/tui/contexts/DrawerContext.tsx +11 -9
  114. package/src/tui/contexts/InputContext.tsx +2 -2
  115. package/src/tui/contexts/LogContext.tsx +62 -47
  116. package/src/tui/contexts/PluginContext.tsx +238 -183
  117. package/src/tui/contexts/RealTimeActivityContext.tsx +70 -36
  118. package/src/tui/contexts/StorageContext.tsx +95 -149
  119. package/src/tui/contexts/ThemeContext.tsx +21 -16
  120. package/src/tui/contexts/TimeWindowContext.tsx +72 -61
  121. package/src/tui/contexts/ToastContext.tsx +10 -7
  122. package/src/tui/contexts/index.ts +9 -5
  123. package/src/tui/createApp.tsx +8 -12
  124. package/src/tui/debug/captureFrame.ts +25 -28
  125. package/src/tui/debug/snapshot.tsx +179 -162
  126. package/src/tui/driver/assertions.ts +30 -22
  127. package/src/tui/driver/cli.ts +231 -221
  128. package/src/tui/driver/coverage.ts +27 -44
  129. package/src/tui/driver/demo-snapshot.ts +33 -33
  130. package/src/tui/driver/diff.ts +26 -32
  131. package/src/tui/driver/driver.ts +43 -48
  132. package/src/tui/driver/index.ts +36 -40
  133. package/src/tui/driver/recorder.ts +37 -48
  134. package/src/tui/driver/test-workflow.ts +51 -51
  135. package/src/tui/driver/test.ts +25 -25
  136. package/src/tui/hooks/index.ts +24 -20
  137. package/src/tui/hooks/useAnimatedValue.ts +13 -18
  138. package/src/tui/hooks/useDashboardKeyboard.ts +118 -98
  139. package/src/tui/hooks/useDashboardState.ts +26 -24
  140. package/src/tui/hooks/useEmaActivity.ts +82 -74
  141. package/src/tui/hooks/useEntranceAnimation.ts +11 -11
  142. package/src/tui/hooks/useExitAnimation.ts +7 -3
  143. package/src/tui/hooks/usePulse.ts +4 -4
  144. package/src/tui/hooks/useSafeRenderer.ts +5 -5
  145. package/src/tui/hooks/useSessionEnrichedProviders.ts +150 -0
  146. package/src/tui/hooks/useValueFlash.ts +10 -14
  147. package/src/tui/index.tsx +29 -6
  148. package/src/tui/shutdown.ts +19 -0
  149. package/src/tui/utils/providerColor.ts +1 -1
  150. package/src/tui/views/Dashboard.tsx +164 -102
  151. package/src/tui/views/HistoricalTrendsView.tsx +579 -250
  152. package/src/tui/views/ProjectsView.tsx +495 -243
  153. package/src/tui/views/RealTimeDashboard.tsx +176 -116
  154. package/src/tui/views/SettingsView.tsx +225 -189
  155. package/src/tui/views/index.ts +2 -2
  156. package/src/types/opentui-test-utils.d.ts +7 -7
  157. package/src/utils/clipboard.ts +29 -29
  158. package/src/utils/currency.test.ts +53 -53
  159. package/src/utils/currency.ts +35 -32
  160. 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
  }
@@ -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>('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
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, notifications, config]);
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 'Tab: switch panes ↑↓: navigate Enter/Space: toggle ←→: adjust Ctrl+S: save Shift+R: reset';
310
+ return "Tab: switch panes ↑↓: navigate Enter/Space: toggle ←→: adjust Ctrl+S: save Shift+R: reset";
277
311
  } else if (terminalWidth >= 80) {
278
- return 'Tab: switch ↑↓: nav Enter: toggle ←→: adjust ^S: save R: reset';
312
+ return "Tab: switch ↑↓: nav Enter: toggle ←→: adjust ^S: save R: reset";
279
313
  }
280
- return 'Tab ↑↓ Enter ←→ ^S:save R:reset';
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('Settings saved');
320
+ showToast("Settings saved");
287
321
  } catch {
288
- showToast('Failed to save settings', 'error');
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('Reset to defaults');
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 === 'toggle') {
337
+
338
+ if (setting.type === "toggle") {
305
339
  newConfig = setting.setValue(config, !currentValue);
306
- } else if (setting.type === 'select' && setting.options) {
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 === 's') {
352
+ if (key.ctrl && key.name === "s") {
319
353
  handleSave();
320
354
  return;
321
355
  }
322
-
323
- if (key.shift && key.name === 'r') {
356
+
357
+ if (key.shift && key.name === "r") {
324
358
  handleReset();
325
359
  return;
326
360
  }
327
-
328
- if (key.name === 'tab') {
329
- setFocusedPane(p => p === 'categories' ? 'settings' : 'categories');
361
+
362
+ if (key.name === "tab") {
363
+ setFocusedPane((p) => (p === "categories" ? "settings" : "categories"));
330
364
  return;
331
365
  }
332
-
333
- if (focusedPane === 'categories') {
334
- const catIdx = CATEGORIES.findIndex(c => c.id === selectedCategory);
335
- if (key.name === 'down' || key.name === 'j') {
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 === 'up' || key.name === 'k') {
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 === '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') {
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 === 'left' || key.name === 'h') {
388
+ } else if (key.name === "left" || key.name === "h") {
355
389
  const setting = categorySettings[selectedIndex];
356
- if (setting?.type === 'select' && setting.options) {
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 === 'right' || key.name === 'l') {
396
+ } else if (key.name === "right" || key.name === "l") {
363
397
  const setting = categorySettings[selectedIndex];
364
- if (setting?.type === 'select' && setting.options) {
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 ?? 'default'} | Preset: {preset ?? 'normal'}
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 === 'categories' ? 'double' : 'single'}
398
- borderColor={focusedPane === 'categories' ? colors.primary : colors.border}
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}>CATEGORIES</text>
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 === 'categories';
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 ? '▸ ' : ' '}{cat.label}
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 === 'settings' ? 'double' : 'single'}
423
- borderColor={focusedPane === 'settings' ? colors.primary : colors.border}
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 === 'settings';
468
+ const isSelected = idx === selectedIndex && focusedPane === "settings";
432
469
  const value = setting.getValue(config);
433
-
470
+
434
471
  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>
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 === 'toggle'
448
- ? (value ? '● ON' : '○ OFF')
449
- : setting.type === 'select'
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>