free-coding-models 0.5.86 → 0.5.87

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 (163) hide show
  1. package/README.md +110 -881
  2. package/bin/free-coding-models.js +4 -1
  3. package/changelog/v0.5.87.md +48 -0
  4. package/package.json +8 -5
  5. package/sources.js +4 -4
  6. package/src/core/cache.js +3 -1
  7. package/src/core/config.js +43 -5
  8. package/src/core/endpoint-installer.js +23 -12
  9. package/src/core/installed-models-manager.js +43 -20
  10. package/src/core/kilo-config.js +51 -5
  11. package/src/core/kilo.js +5 -3
  12. package/src/core/legacy-proxy-cleanup.js +22 -5
  13. package/src/core/model-merger.js +4 -0
  14. package/src/core/models-drift.js +9 -0
  15. package/src/core/opencode-config.js +57 -5
  16. package/src/core/opencode.js +67 -41
  17. package/src/core/playground.js +7 -0
  18. package/src/core/probe-cache.js +35 -4
  19. package/src/core/provider-key-tester.js +10 -2
  20. package/src/core/provider-metadata.js +10 -0
  21. package/src/core/provider-quota-fetchers.js +14 -7
  22. package/src/core/router-daemon.js +208 -26
  23. package/src/core/runtime-telemetry.js +27 -1
  24. package/src/core/shared-helpers.js +12 -1
  25. package/src/core/shell-env.js +3 -1
  26. package/src/core/telemetry.js +18 -5
  27. package/src/core/tool-launchers.js +9 -6
  28. package/src/core/updater.js +28 -2
  29. package/src/core/utils.js +76 -71
  30. package/src/tui/app.js +38 -14
  31. package/src/tui/command-palette.js +22 -2
  32. package/src/tui/key-handler.js +123 -179
  33. package/src/tui/overlays.js +21 -71
  34. package/src/tui/render-helpers.js +62 -4
  35. package/src/tui/render-table.js +34 -3
  36. package/src/tui/theme.js +36 -5
  37. package/src/tui/tui-filters.js +28 -8
  38. package/src/tui/tui-state.js +22 -6
  39. package/web/dist/assets/index-BpTpPS0g.js +44 -0
  40. package/web/dist/assets/index-CAzFIt8P.css +1 -0
  41. package/web/dist/index.html +5 -9
  42. package/web/index.html +3 -7
  43. package/web/server.js +97 -23
  44. package/web/vite.config.js +8 -0
  45. package/web/app.legacy.js +0 -900
  46. package/web/dist/assets/index-C5-Ywvv3.css +0 -1
  47. package/web/dist/assets/index-Dm-Xfby9.js +0 -40
  48. package/web/public/favicon.ico +0 -0
  49. package/web/public/favicons/apple-touch-icon.png +0 -0
  50. package/web/public/favicons/browserconfig.xml +0 -12
  51. package/web/public/favicons/favicon-16x16.png +0 -0
  52. package/web/public/favicons/favicon-192x192.png +0 -0
  53. package/web/public/favicons/favicon-32x32.png +0 -0
  54. package/web/public/favicons/favicon-48x48.png +0 -0
  55. package/web/public/favicons/favicon-512x512.png +0 -0
  56. package/web/public/favicons/favicon-96x96.png +0 -0
  57. package/web/public/favicons/favicon.ico +0 -0
  58. package/web/public/favicons/mstile-150x150.png +0 -0
  59. package/web/public/favicons/mstile-310x150.png +0 -0
  60. package/web/public/favicons/mstile-310x310.png +0 -0
  61. package/web/public/favicons/mstile-512x512.png +0 -0
  62. package/web/public/favicons/mstile-70x70.png +0 -0
  63. package/web/public/favicons/site.webmanifest +0 -25
  64. package/web/src/App.jsx +0 -521
  65. package/web/src/components/analytics/AnalyticsView.jsx +0 -147
  66. package/web/src/components/analytics/AnalyticsView.module.css +0 -190
  67. package/web/src/components/analytics/TokenUsagePanel.jsx +0 -105
  68. package/web/src/components/analytics/TokenUsagePanel.module.css +0 -126
  69. package/web/src/components/atoms/AILatencyCell.jsx +0 -38
  70. package/web/src/components/atoms/AILatencyCell.module.css +0 -51
  71. package/web/src/components/atoms/HealthCell.jsx +0 -143
  72. package/web/src/components/atoms/HealthCell.module.css +0 -60
  73. package/web/src/components/atoms/LastPingCell.jsx +0 -35
  74. package/web/src/components/atoms/LastPingCell.module.css +0 -45
  75. package/web/src/components/atoms/MoodCell.jsx +0 -25
  76. package/web/src/components/atoms/MoodCell.module.css +0 -6
  77. package/web/src/components/atoms/NoKeyIcon.jsx +0 -65
  78. package/web/src/components/atoms/NoKeyIcon.module.css +0 -30
  79. package/web/src/components/atoms/ProviderLogo.jsx +0 -187
  80. package/web/src/components/atoms/ProviderLogo.module.css +0 -103
  81. package/web/src/components/atoms/RankCell.jsx +0 -9
  82. package/web/src/components/atoms/RankCell.module.css +0 -9
  83. package/web/src/components/atoms/Sparkline.jsx +0 -63
  84. package/web/src/components/atoms/StabilityCell.jsx +0 -18
  85. package/web/src/components/atoms/StabilityCell.module.css +0 -13
  86. package/web/src/components/atoms/StatusDot.jsx +0 -13
  87. package/web/src/components/atoms/StatusDot.module.css +0 -29
  88. package/web/src/components/atoms/TPSCell.jsx +0 -36
  89. package/web/src/components/atoms/TPSCell.module.css +0 -44
  90. package/web/src/components/atoms/TierBadge.jsx +0 -10
  91. package/web/src/components/atoms/TierBadge.module.css +0 -29
  92. package/web/src/components/atoms/Toast.jsx +0 -25
  93. package/web/src/components/atoms/Toast.module.css +0 -41
  94. package/web/src/components/atoms/ToastContainer.jsx +0 -16
  95. package/web/src/components/atoms/ToastContainer.module.css +0 -10
  96. package/web/src/components/atoms/VerdictBadge.jsx +0 -36
  97. package/web/src/components/atoms/VerdictBadge.module.css +0 -40
  98. package/web/src/components/changelog/ChangelogView.jsx +0 -135
  99. package/web/src/components/changelog/ChangelogView.module.css +0 -159
  100. package/web/src/components/dashboard/DetailPanel.jsx +0 -217
  101. package/web/src/components/dashboard/DetailPanel.module.css +0 -260
  102. package/web/src/components/dashboard/ExpandedDetailRow.jsx +0 -361
  103. package/web/src/components/dashboard/ExpandedDetailRow.module.css +0 -356
  104. package/web/src/components/dashboard/ExportModal.jsx +0 -84
  105. package/web/src/components/dashboard/ExportModal.module.css +0 -99
  106. package/web/src/components/dashboard/FilterBar.jsx +0 -338
  107. package/web/src/components/dashboard/FilterBar.module.css +0 -342
  108. package/web/src/components/dashboard/ModelTable.jsx +0 -672
  109. package/web/src/components/dashboard/ModelTable.module.css +0 -446
  110. package/web/src/components/dashboard/ProviderDropdown.jsx +0 -156
  111. package/web/src/components/dashboard/ProviderDropdown.module.css +0 -248
  112. package/web/src/components/help/HelpView.jsx +0 -201
  113. package/web/src/components/help/HelpView.module.css +0 -156
  114. package/web/src/components/install/InstallEndpointsView.jsx +0 -309
  115. package/web/src/components/install/InstallEndpointsView.module.css +0 -427
  116. package/web/src/components/installed/InstalledModelsView.jsx +0 -89
  117. package/web/src/components/installed/InstalledModelsView.module.css +0 -200
  118. package/web/src/components/launch/IncompatibleFallbackModal.jsx +0 -69
  119. package/web/src/components/launch/LaunchButton.jsx +0 -30
  120. package/web/src/components/launch/LaunchButton.module.css +0 -35
  121. package/web/src/components/launch/LaunchModal.module.css +0 -125
  122. package/web/src/components/layout/Footer.jsx +0 -19
  123. package/web/src/components/layout/Footer.module.css +0 -10
  124. package/web/src/components/layout/Header.jsx +0 -265
  125. package/web/src/components/layout/Header.module.css +0 -357
  126. package/web/src/components/palette/CommandPalette.jsx +0 -301
  127. package/web/src/components/palette/CommandPalette.module.css +0 -121
  128. package/web/src/components/playground/PlaygroundChat.jsx +0 -527
  129. package/web/src/components/playground/PlaygroundChat.module.css +0 -382
  130. package/web/src/components/playground/PlaygroundView.jsx +0 -371
  131. package/web/src/components/playground/PlaygroundView.module.css +0 -504
  132. package/web/src/components/recommend/RecommendView.jsx +0 -121
  133. package/web/src/components/recommend/RecommendView.module.css +0 -57
  134. package/web/src/components/router/RouterView.jsx +0 -1212
  135. package/web/src/components/router/RouterView.module.css +0 -1289
  136. package/web/src/components/settings/SettingsView.jsx +0 -555
  137. package/web/src/components/settings/SettingsView.module.css +0 -550
  138. package/web/src/components/tools/ToolPicker.jsx +0 -86
  139. package/web/src/components/tools/ToolPicker.module.css +0 -97
  140. package/web/src/components/update/UpdateChip.jsx +0 -104
  141. package/web/src/components/update/UpdateChip.module.css +0 -146
  142. package/web/src/global.css +0 -298
  143. package/web/src/hooks/urlState.constants.js +0 -28
  144. package/web/src/hooks/useChangelog.js +0 -51
  145. package/web/src/hooks/useColumnSizing.js +0 -211
  146. package/web/src/hooks/useFavorites.js +0 -150
  147. package/web/src/hooks/useFilter.js +0 -310
  148. package/web/src/hooks/useInstalledModels.js +0 -42
  149. package/web/src/hooks/useRecommend.js +0 -67
  150. package/web/src/hooks/useRouterDashboard.js +0 -133
  151. package/web/src/hooks/useSSE.js +0 -73
  152. package/web/src/hooks/useSocket.js +0 -203
  153. package/web/src/hooks/useTheme.js +0 -83
  154. package/web/src/hooks/useTokenUsage.js +0 -103
  155. package/web/src/hooks/useToolMode.js +0 -77
  156. package/web/src/hooks/useUpdateChecker.js +0 -91
  157. package/web/src/hooks/useUrlState.js +0 -170
  158. package/web/src/main.jsx +0 -15
  159. package/web/src/utils/download.js +0 -15
  160. package/web/src/utils/format.js +0 -42
  161. package/web/src/utils/m3.js +0 -57
  162. package/web/src/utils/ranks.js +0 -37
  163. package/web/styles.legacy.css +0 -963
@@ -1,555 +0,0 @@
1
- /**
2
- * @file web/src/components/settings/SettingsView.jsx
3
- * @description Full settings page — M2 parity with the TUI Settings overlay.
4
- * 📖 M1: API key management (per-provider cards: enable/disable, masked key,
5
- * 📖 reveal, copy, save, delete, search filter).
6
- * 📖 M2: theme dropdown, favorites display mode toggle, startup AI speed scan
7
- * 📖 toggle, shell-env export toggle, legacy proxy cleanup button, per-provider
8
- * 📖 test key button (calls /api/key/:provider/test), open Changelog link,
9
- * 📖 update status row.
10
- * @functions SettingsView → main settings page component
11
- */
12
- import { useState, useEffect, useCallback } from 'react'
13
- import {
14
- IconSettings, IconPlug, IconCircleCheck, IconKey, IconEye, IconEyeOff, IconCopy, IconTrash,
15
- IconBolt, IconCircleCheckFilled, IconHistory, IconRefresh, IconDownload, IconSun, IconStar,
16
- } from '@tabler/icons-react'
17
- import styles from './SettingsView.module.css'
18
- import { maskKey } from '../../utils/format.js'
19
-
20
- const TEST_OUTCOME_META = {
21
- ok: { label: 'OK', icon: IconCircleCheckFilled, className: 'testOk' },
22
- auth_error: { label: 'Auth error', icon: IconKey, className: 'testErr' },
23
- rate_limited: { label: 'Rate limited', icon: IconRefresh, className: 'testWarn' },
24
- no_callable_model: { label: 'No callable model', icon: IconKey, className: 'testWarn' },
25
- fail: { label: 'Failed', icon: IconKey, className: 'testErr' },
26
- missing_key: { label: 'Missing key', icon: IconKey, className: 'testNeutral' },
27
- }
28
-
29
- export default function SettingsView({ onToast, onOpenChangelog, onCheckForUpdate }) {
30
- const [config, setConfig] = useState(null)
31
- const [searchQuery, setSearchQuery] = useState('')
32
- const [expandedCards, setExpandedCards] = useState(new Set())
33
- const [revealedKeys, setRevealedKeys] = useState(new Set())
34
- const [keyInputs, setKeyInputs] = useState({})
35
- const [testResults, setTestResults] = useState({}) // { providerKey: { outcome, code?, detail? } }
36
- const [testingKeys, setTestingKeys] = useState(new Set())
37
- const [legacyCleanupMsg, setLegacyCleanupMsg] = useState(null)
38
-
39
- const loadConfig = useCallback(async () => {
40
- try {
41
- const resp = await fetch('/api/config')
42
- const data = await resp.json()
43
- setConfig(data)
44
- } catch {
45
- onToast?.('Failed to load settings', 'error')
46
- }
47
- }, [onToast])
48
-
49
- useEffect(() => { loadConfig() }, [loadConfig])
50
-
51
- const toggleCard = (key) => {
52
- setExpandedCards((prev) => {
53
- const next = new Set(prev)
54
- if (next.has(key)) next.delete(key)
55
- else next.add(key)
56
- return next
57
- })
58
- }
59
-
60
- const expandAll = () => {
61
- if (!config) return
62
- setExpandedCards(new Set(Object.keys(config.providers)))
63
- }
64
-
65
- const collapseAll = () => setExpandedCards(new Set())
66
-
67
- const toggleRevealKey = async (key) => {
68
- if (revealedKeys.has(key)) {
69
- setRevealedKeys((prev) => { const n = new Set(prev); n.delete(key); return n })
70
- return
71
- }
72
- try {
73
- const resp = await fetch(`/api/key/${key}`)
74
- const data = await resp.json()
75
- if (data.key) {
76
- setRevealedKeys((prev) => new Set(prev).add(key))
77
- }
78
- } catch {
79
- onToast?.('Failed to reveal key', 'error')
80
- }
81
- }
82
-
83
- const copyKey = async (key) => {
84
- try {
85
- const resp = await fetch(`/api/key/${key}`)
86
- const data = await resp.json()
87
- if (data.key) {
88
- await navigator.clipboard.writeText(data.key)
89
- onToast?.('API key copied to clipboard', 'success')
90
- } else {
91
- onToast?.('No key to copy', 'warning')
92
- }
93
- } catch {
94
- onToast?.('Failed to copy key', 'error')
95
- }
96
- }
97
-
98
- const saveKey = async (key) => {
99
- const value = keyInputs[key]?.trim()
100
- if (!value) {
101
- onToast?.('Please enter an API key', 'warning')
102
- return
103
- }
104
- try {
105
- const resp = await fetch('/api/settings', {
106
- method: 'POST',
107
- headers: { 'Content-Type': 'application/json' },
108
- body: JSON.stringify({ apiKeys: { [key]: value } }),
109
- })
110
- const result = await resp.json()
111
- if (result.success) {
112
- onToast?.(`API key for ${key} saved successfully!`, 'success')
113
- setKeyInputs((prev) => ({ ...prev, [key]: '' }))
114
- setRevealedKeys((prev) => { const n = new Set(prev); n.delete(key); return n })
115
- await loadConfig()
116
- setExpandedCards((prev) => new Set(prev).add(key))
117
- } else {
118
- onToast?.(result.error || 'Failed to save', 'error')
119
- }
120
- } catch {
121
- onToast?.('Network error while saving', 'error')
122
- }
123
- }
124
-
125
- const deleteKey = async (key) => {
126
- if (!confirm(`Are you sure you want to delete the API key for "${key}"?`)) return
127
- try {
128
- const resp = await fetch('/api/settings', {
129
- method: 'POST',
130
- headers: { 'Content-Type': 'application/json' },
131
- body: JSON.stringify({ apiKeys: { [key]: '' } }),
132
- })
133
- const result = await resp.json()
134
- if (result.success) {
135
- onToast?.(`API key for ${key} deleted`, 'info')
136
- setRevealedKeys((prev) => { const n = new Set(prev); n.delete(key); return n })
137
- await loadConfig()
138
- } else {
139
- onToast?.(result.error || 'Failed to delete', 'error')
140
- }
141
- } catch {
142
- onToast?.('Network error while deleting', 'error')
143
- }
144
- }
145
-
146
- const toggleProvider = async (key, enabled) => {
147
- try {
148
- const resp = await fetch('/api/settings', {
149
- method: 'POST',
150
- headers: { 'Content-Type': 'application/json' },
151
- body: JSON.stringify({ providers: { [key]: { enabled } } }),
152
- })
153
- const result = await resp.json()
154
- if (result.success) {
155
- onToast?.(`${key} ${enabled ? 'enabled' : 'disabled'}`, 'success')
156
- } else {
157
- onToast?.(result.error || 'Failed to toggle', 'error')
158
- }
159
- } catch {
160
- onToast?.('Network error', 'error')
161
- }
162
- }
163
-
164
- // 📖 M2: per-provider key test. Fires a parallel auth probe + chat ping
165
- // 📖 through /api/key/:provider/test and stores the outcome for badge display.
166
- const testKey = useCallback(async (key) => {
167
- if (testingKeys.has(key)) return
168
- setTestingKeys((prev) => new Set(prev).add(key))
169
- setTestResults((prev) => ({ ...prev, [key]: { outcome: 'pending' } }))
170
- try {
171
- const resp = await fetch(`/api/key/${encodeURIComponent(key)}/test`, { method: 'POST' })
172
- const data = await resp.json().catch(() => ({}))
173
- if (resp.ok) {
174
- setTestResults((prev) => ({ ...prev, [key]: data }))
175
- const meta = TEST_OUTCOME_META[data.outcome] || TEST_OUTCOME_META.fail
176
- onToast?.(`${key} test: ${meta.label}${data.code ? ` (HTTP ${data.code})` : ''}`, data.outcome === 'ok' ? 'success' : 'info')
177
- } else {
178
- setTestResults((prev) => ({ ...prev, [key]: { outcome: 'fail', detail: data.error || 'HTTP ' + resp.status } }))
179
- onToast?.(`${key} test failed: ${data.error || resp.statusText}`, 'error')
180
- }
181
- } catch (err) {
182
- setTestResults((prev) => ({ ...prev, [key]: { outcome: 'fail', detail: err.message } }))
183
- onToast?.(`${key} test failed: ${err.message}`, 'error')
184
- } finally {
185
- setTestingKeys((prev) => {
186
- const next = new Set(prev)
187
- next.delete(key)
188
- return next
189
- })
190
- }
191
- }, [testingKeys, onToast])
192
-
193
- // 📖 M2: feature toggles (theme / favorites mode / startup AI scan / shell env)
194
- // 📖 go through /api/settings/feature which persists to the same config file
195
- // 📖 the TUI uses. Theme is a tri-state string, not a boolean.
196
- const toggleFeature = useCallback(async (feature, value) => {
197
- try {
198
- const body = value === undefined ? { feature } : { feature, value }
199
- const resp = await fetch('/api/settings/feature', {
200
- method: 'POST',
201
- headers: { 'Content-Type': 'application/json' },
202
- body: JSON.stringify(body),
203
- })
204
- const data = await resp.json()
205
- if (data.success) {
206
- await loadConfig()
207
- onToast?.(`${feature} updated`, 'success')
208
- } else {
209
- onToast?.(data.error || 'Failed to update feature', 'error')
210
- }
211
- } catch {
212
- onToast?.('Network error', 'error')
213
- }
214
- }, [onToast])
215
-
216
- const setShellEnv = useCallback(async (enabled) => {
217
- try {
218
- const resp = await fetch('/api/shell-env/toggle', {
219
- method: 'POST',
220
- headers: { 'Content-Type': 'application/json' },
221
- body: JSON.stringify({ enabled }),
222
- })
223
- const data = await resp.json()
224
- if (data.success) {
225
- await loadConfig()
226
- onToast?.(`Shell env export ${data.enabled ? 'enabled' : 'disabled'} — restart your shell to apply.`, 'success')
227
- } else {
228
- onToast?.(data.error || 'Failed to toggle shell env', 'error')
229
- }
230
- } catch {
231
- onToast?.('Network error', 'error')
232
- }
233
- }, [onToast])
234
-
235
- const runLegacyCleanup = useCallback(async () => {
236
- if (!confirm('Remove discontinued proxy config leftovers? This is safe to run.')) return
237
- try {
238
- const resp = await fetch('/api/legacy-cleanup', { method: 'POST' })
239
- const data = await resp.json()
240
- const cleaned = (data.removedFiles?.length || 0) + (data.updatedFiles?.length || 0)
241
- if (data.changed) {
242
- setLegacyCleanupMsg(`Cleaned ${cleaned} legacy file(s). ${data.errors.length} error(s).`)
243
- onToast?.(`Legacy proxy cleanup: ${cleaned} file(s) cleaned.`, 'success')
244
- } else {
245
- setLegacyCleanupMsg('No discontinued proxy config was found. You are on the stable direct-provider setup.')
246
- onToast?.('No legacy proxy config found — already on stable setup.', 'info')
247
- }
248
- await loadConfig()
249
- } catch (err) {
250
- onToast?.(`Legacy cleanup failed: ${err.message}`, 'error')
251
- }
252
- }, [onToast])
253
-
254
- const onCheckUpdatesClick = useCallback(() => {
255
- onCheckForUpdate?.()
256
- }, [onCheckForUpdate])
257
-
258
- if (!config) {
259
- return (
260
- <div className={styles.page}>
261
- <div className={styles.loading}>Loading settings...</div>
262
- </div>
263
- )
264
- }
265
-
266
- const entries = Object.entries(config.providers)
267
- .filter(([, p]) => {
268
- if (!searchQuery) return true
269
- const q = searchQuery.toLowerCase()
270
- return `${p.name} ${p.displayName || ''} ${p.billingNote || ''}`.toLowerCase().includes(q)
271
- })
272
- .sort((a, b) => a[1].name.localeCompare(b[1].name))
273
-
274
- return (
275
- <div className={styles.page}>
276
- <div className={styles.pageHeader}>
277
- <h1 className={styles.pageTitle}>
278
- <IconSettings size={24} stroke={1.5} style={{ marginRight: 8, verticalAlign: 'middle' }} />
279
- Settings
280
- </h1>
281
- <p className={styles.pageSubtitle}>
282
- API keys, theme, favorites mode, shell env, and update controls.
283
- All settings are stored locally in <code>~/.free-coding-models.json</code>
284
- and shared with the TUI.
285
- </p>
286
- </div>
287
-
288
- {/* ── M2: global feature toggles ────────────────────────────────────── */}
289
- {config && (
290
- <section className={styles.featureSection}>
291
- <h2 className={styles.sectionHeading}>⚙️ Global settings</h2>
292
- <div className={styles.featureGrid}>
293
- {/* Theme */}
294
- <div className={styles.featureRow}>
295
- <div className={styles.featureLabel}>
296
- <IconSun size={16} stroke={1.5} />
297
- <div>
298
- <div className={styles.featureTitle}>Theme</div>
299
- <div className={styles.featureDesc}>Tri-state cycle. Auto follows your OS.</div>
300
- </div>
301
- </div>
302
- <select
303
- className={styles.select}
304
- value={config.settings?.theme || 'auto'}
305
- onChange={(e) => toggleFeature('theme', e.target.value)}
306
- >
307
- <option value="auto">Auto (OS)</option>
308
- <option value="dark">Dark</option>
309
- <option value="light">Light</option>
310
- </select>
311
- </div>
312
-
313
- {/* Favorites display mode */}
314
- <div className={styles.featureRow}>
315
- <div className={styles.featureLabel}>
316
- <IconStar size={16} stroke={1.5} />
317
- <div>
318
- <div className={styles.featureTitle}>Favorites pinned + always visible</div>
319
- <div className={styles.featureDesc}>Favorites bypass filters and stay on top (TUI: Y key).</div>
320
- </div>
321
- </div>
322
- <label className={styles.toggleSwitch}>
323
- <input
324
- type="checkbox"
325
- checked={Boolean(config.settings?.favoritesPinnedAndSticky)}
326
- onChange={(e) => toggleFeature('favoritesPinnedAndSticky', e.target.checked)}
327
- />
328
- <span className={styles.toggleSlider} />
329
- </label>
330
- </div>
331
-
332
- {/* Startup AI Speed Scan */}
333
- <div className={styles.featureRow}>
334
- <div className={styles.featureLabel}>
335
- <IconBolt size={16} stroke={1.5} />
336
- <div>
337
- <div className={styles.featureTitle}>Run AI Speed Test on startup</div>
338
- <div className={styles.featureDesc}>Auto-fire the global benchmark right after launch (TUI: U → 'Enable').</div>
339
- </div>
340
- </div>
341
- <label className={styles.toggleSwitch}>
342
- <input
343
- type="checkbox"
344
- checked={Boolean(config.settings?.runAiSpeedTestOnStartup)}
345
- onChange={(e) => toggleFeature('runAiSpeedTestOnStartup', e.target.checked)}
346
- />
347
- <span className={styles.toggleSlider} />
348
- </label>
349
- </div>
350
-
351
- {/* Shell env export */}
352
- <div className={styles.featureRow}>
353
- <div className={styles.featureLabel}>
354
- <IconCircleCheck size={16} stroke={1.5} />
355
- <div>
356
- <div className={styles.featureTitle}>Export API keys to shell rc</div>
357
- <div className={styles.featureDesc}>Write NVIDIA_API_KEY / GROQ_API_KEY / … to your shell rc file.</div>
358
- </div>
359
- </div>
360
- <label className={styles.toggleSwitch}>
361
- <input
362
- type="checkbox"
363
- checked={Boolean(config.settings?.shellEnvEnabled)}
364
- onChange={(e) => setShellEnv(e.target.checked)}
365
- />
366
- <span className={styles.toggleSlider} />
367
- </label>
368
- </div>
369
-
370
- {/* Update row */}
371
- <div className={styles.featureRow}>
372
- <div className={styles.featureLabel}>
373
- <IconDownload size={16} stroke={1.5} />
374
- <div>
375
- <div className={styles.featureTitle}>Check for updates</div>
376
- <div className={styles.featureDesc}>
377
- The header chip turns green when a newer npm version is available.
378
- Use the 'Update now' button to install.
379
- </div>
380
- </div>
381
- </div>
382
- <div className={styles.featureActions}>
383
- <button
384
- className={styles.smallBtn}
385
- onClick={onCheckUpdatesClick}
386
- >
387
- Check now
388
- </button>
389
- <button
390
- className={styles.smallBtn}
391
- onClick={() => onOpenChangelog?.(null)}
392
- >
393
- <IconHistory size={13} stroke={1.5} /> Changelog
394
- </button>
395
- </div>
396
- </div>
397
-
398
- {/* Legacy proxy cleanup */}
399
- <div className={styles.featureRow}>
400
- <div className={styles.featureLabel}>
401
- <IconRefresh size={16} stroke={1.5} />
402
- <div>
403
- <div className={styles.featureTitle}>Cleanup discontinued proxy artifacts</div>
404
- <div className={styles.featureDesc}>
405
- Remove old config / env / service leftovers from the old multi-tool proxy.
406
- </div>
407
- </div>
408
- </div>
409
- <button
410
- className={styles.smallBtn}
411
- onClick={runLegacyCleanup}
412
- >
413
- Run cleanup
414
- </button>
415
- </div>
416
- </div>
417
- {legacyCleanupMsg && (
418
- <div className={styles.notice}>{legacyCleanupMsg}</div>
419
- )}
420
- </section>
421
- )}
422
-
423
- <div className={styles.toolbar}>
424
- <div className={styles.toolbarSearch}>
425
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
426
- <circle cx="11" cy="11" r="8" /><line x1="21" y1="21" x2="16.65" y2="16.65" />
427
- </svg>
428
- <input
429
- type="text"
430
- placeholder="Search providers..."
431
- value={searchQuery}
432
- onChange={(e) => setSearchQuery(e.target.value)}
433
- autoComplete="off"
434
- />
435
- </div>
436
- <div className={styles.toolbarActions}>
437
- <button className={styles.toolbarBtn} onClick={expandAll}>Expand All</button>
438
- <button className={styles.toolbarBtn} onClick={collapseAll}>Collapse All</button>
439
- </div>
440
- </div>
441
-
442
- <div className={styles.providers}>
443
- {entries.map(([key, p]) => {
444
- const isExpanded = expandedCards.has(key)
445
- const isRevealed = revealedKeys.has(key)
446
-
447
- return (
448
- <div key={key} className={`${styles.card} ${isExpanded ? styles.cardExpanded : ''}`}>
449
- <div className={styles.cardHeader} onClick={() => toggleCard(key)}>
450
- <div className={styles.cardIcon}>
451
- <IconPlug size={20} stroke={1.5} />
452
- </div>
453
- <div className={styles.cardInfo}>
454
- <div className={styles.cardName}>{p.displayName || p.name}</div>
455
- <div className={styles.cardMeta}>{p.modelCount} models · {key}{p.billingNote ? ` · ${p.billingNote}` : ''}</div>
456
- </div>
457
- <span className={`${styles.cardStatus} ${p.hasKey ? styles.statusConfigured : styles.statusMissing}`}>
458
- {p.hasKey ? (
459
- <span style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
460
- <IconCircleCheck size={14} stroke={1.5} /> Active
461
- </span>
462
- ) : (
463
- <span style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
464
- <IconKey size={14} stroke={1.5} /> No Key
465
- </span>
466
- )}
467
- </span>
468
- <span className={`${styles.toggleIcon} ${isExpanded ? styles.toggleIconExpanded : ''}`}>▼</span>
469
- </div>
470
-
471
- <div className={styles.cardBody}>
472
- <div className={styles.cardContent}>
473
- {p.hasKey && testResults[key] && (
474
- <div className={`${styles.testBadge} ${styles[`test_${testResults[key].outcome}`] || ''}`}>
475
- {(() => {
476
- const meta = TEST_OUTCOME_META[testResults[key].outcome] || TEST_OUTCOME_META.fail
477
- const Icon = meta.icon
478
- return (
479
- <>
480
- <Icon size={12} stroke={1.5} />
481
- <span>Last test: {meta.label}{testResults[key].code ? ` (HTTP ${testResults[key].code})` : ''}</span>
482
- </>
483
- )
484
- })()}
485
- </div>
486
- )}
487
- {p.hasKey && (
488
- <div className={styles.keyGroup}>
489
- <label className={styles.keyLabel}>Current API Key</label>
490
- <div className={styles.keyDisplay}>
491
- <span className={styles.keyDisplayValue}>
492
- {isRevealed ? (p.maskedKey || '••••••••') : maskKey(p.maskedKey || '')}
493
- </span>
494
- <div className={styles.keyDisplayActions}>
495
- <button className={styles.actionBtn} onClick={() => toggleRevealKey(key)} title={isRevealed ? 'Hide' : 'Reveal'}>
496
- {isRevealed ? <IconEyeOff size={14} stroke={1.5} /> : <IconEye size={14} stroke={1.5} />}
497
- </button>
498
- <button className={styles.actionBtn} onClick={() => copyKey(key)} title="Copy">
499
- <IconCopy size={14} stroke={1.5} />
500
- </button>
501
- <button
502
- className={styles.actionBtn}
503
- onClick={() => testKey(key)}
504
- disabled={testingKeys.has(key)}
505
- title="Test this key against the provider (TUI: T key in Settings)"
506
- aria-label={`Test key for ${key}`}
507
- >
508
- {testingKeys.has(key) ? <span className={styles.testSpinner} /> : <IconBolt size={14} stroke={1.5} />}
509
- {testingKeys.has(key) ? 'Testing…' : 'Test'}
510
- </button>
511
- <button className={`${styles.actionBtn} ${styles.actionBtnDanger}`} onClick={() => deleteKey(key)} title="Delete Key">
512
- <IconTrash size={14} stroke={1.5} />
513
- </button>
514
- </div>
515
- </div>
516
- </div>
517
- )}
518
-
519
- <div className={styles.keyGroup}>
520
- <label className={styles.keyLabel}>{p.hasKey ? 'Update API Key' : 'Add API Key'}{p.billingNote ? ` 💰 ${p.billingNote}` : ''}</label>
521
- <div className={styles.keyInputRow}>
522
- <input
523
- type="password"
524
- className={styles.keyInput}
525
- placeholder="Enter your API key..."
526
- value={keyInputs[key] || ''}
527
- onChange={(e) => setKeyInputs((prev) => ({ ...prev, [key]: e.target.value }))}
528
- autoComplete="off"
529
- />
530
- <button className={styles.saveBtn} onClick={() => saveKey(key)}>
531
- {p.hasKey ? 'Update' : 'Save'}
532
- </button>
533
- </div>
534
- </div>
535
-
536
- <div className={styles.enabledRow}>
537
- <span className={styles.enabledLabel}>Provider Enabled</span>
538
- <label className={styles.toggleSwitch}>
539
- <input
540
- type="checkbox"
541
- defaultChecked={p.enabled !== false}
542
- onChange={(e) => toggleProvider(key, e.target.checked)}
543
- />
544
- <span className={styles.toggleSlider} />
545
- </label>
546
- </div>
547
- </div>
548
- </div>
549
- </div>
550
- )
551
- })}
552
- </div>
553
- </div>
554
- )
555
- }