free-coding-models 0.5.85 → 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 (164) hide show
  1. package/README.md +110 -881
  2. package/bin/free-coding-models.js +4 -1
  3. package/changelog/v0.5.86.md +10 -0
  4. package/changelog/v0.5.87.md +48 -0
  5. package/package.json +8 -5
  6. package/sources.js +4 -4
  7. package/src/core/cache.js +3 -1
  8. package/src/core/config.js +43 -5
  9. package/src/core/endpoint-installer.js +23 -12
  10. package/src/core/installed-models-manager.js +43 -20
  11. package/src/core/kilo-config.js +51 -5
  12. package/src/core/kilo.js +5 -3
  13. package/src/core/legacy-proxy-cleanup.js +22 -5
  14. package/src/core/model-merger.js +4 -0
  15. package/src/core/models-drift.js +9 -0
  16. package/src/core/opencode-config.js +57 -5
  17. package/src/core/opencode.js +67 -41
  18. package/src/core/playground.js +7 -0
  19. package/src/core/probe-cache.js +35 -4
  20. package/src/core/provider-key-tester.js +10 -2
  21. package/src/core/provider-metadata.js +10 -0
  22. package/src/core/provider-quota-fetchers.js +14 -7
  23. package/src/core/router-daemon.js +208 -26
  24. package/src/core/runtime-telemetry.js +27 -1
  25. package/src/core/shared-helpers.js +12 -1
  26. package/src/core/shell-env.js +3 -1
  27. package/src/core/telemetry.js +18 -5
  28. package/src/core/tool-launchers.js +9 -6
  29. package/src/core/updater.js +28 -2
  30. package/src/core/utils.js +76 -71
  31. package/src/tui/app.js +47 -16
  32. package/src/tui/command-palette.js +22 -2
  33. package/src/tui/key-handler.js +123 -179
  34. package/src/tui/overlays.js +35 -75
  35. package/src/tui/render-helpers.js +62 -4
  36. package/src/tui/render-table.js +46 -5
  37. package/src/tui/theme.js +36 -5
  38. package/src/tui/tui-filters.js +28 -8
  39. package/src/tui/tui-state.js +22 -6
  40. package/web/dist/assets/index-BpTpPS0g.js +44 -0
  41. package/web/dist/assets/index-CAzFIt8P.css +1 -0
  42. package/web/dist/index.html +5 -9
  43. package/web/index.html +3 -7
  44. package/web/server.js +97 -23
  45. package/web/vite.config.js +8 -0
  46. package/web/app.legacy.js +0 -900
  47. package/web/dist/assets/index-C5-Ywvv3.css +0 -1
  48. package/web/dist/assets/index-DWUely84.js +0 -40
  49. package/web/public/favicon.ico +0 -0
  50. package/web/public/favicons/apple-touch-icon.png +0 -0
  51. package/web/public/favicons/browserconfig.xml +0 -12
  52. package/web/public/favicons/favicon-16x16.png +0 -0
  53. package/web/public/favicons/favicon-192x192.png +0 -0
  54. package/web/public/favicons/favicon-32x32.png +0 -0
  55. package/web/public/favicons/favicon-48x48.png +0 -0
  56. package/web/public/favicons/favicon-512x512.png +0 -0
  57. package/web/public/favicons/favicon-96x96.png +0 -0
  58. package/web/public/favicons/favicon.ico +0 -0
  59. package/web/public/favicons/mstile-150x150.png +0 -0
  60. package/web/public/favicons/mstile-310x150.png +0 -0
  61. package/web/public/favicons/mstile-310x310.png +0 -0
  62. package/web/public/favicons/mstile-512x512.png +0 -0
  63. package/web/public/favicons/mstile-70x70.png +0 -0
  64. package/web/public/favicons/site.webmanifest +0 -25
  65. package/web/src/App.jsx +0 -521
  66. package/web/src/components/analytics/AnalyticsView.jsx +0 -147
  67. package/web/src/components/analytics/AnalyticsView.module.css +0 -190
  68. package/web/src/components/analytics/TokenUsagePanel.jsx +0 -105
  69. package/web/src/components/analytics/TokenUsagePanel.module.css +0 -126
  70. package/web/src/components/atoms/AILatencyCell.jsx +0 -38
  71. package/web/src/components/atoms/AILatencyCell.module.css +0 -51
  72. package/web/src/components/atoms/HealthCell.jsx +0 -143
  73. package/web/src/components/atoms/HealthCell.module.css +0 -60
  74. package/web/src/components/atoms/LastPingCell.jsx +0 -35
  75. package/web/src/components/atoms/LastPingCell.module.css +0 -45
  76. package/web/src/components/atoms/MoodCell.jsx +0 -25
  77. package/web/src/components/atoms/MoodCell.module.css +0 -6
  78. package/web/src/components/atoms/NoKeyIcon.jsx +0 -65
  79. package/web/src/components/atoms/NoKeyIcon.module.css +0 -30
  80. package/web/src/components/atoms/ProviderLogo.jsx +0 -187
  81. package/web/src/components/atoms/ProviderLogo.module.css +0 -103
  82. package/web/src/components/atoms/RankCell.jsx +0 -9
  83. package/web/src/components/atoms/RankCell.module.css +0 -9
  84. package/web/src/components/atoms/Sparkline.jsx +0 -63
  85. package/web/src/components/atoms/StabilityCell.jsx +0 -18
  86. package/web/src/components/atoms/StabilityCell.module.css +0 -13
  87. package/web/src/components/atoms/StatusDot.jsx +0 -13
  88. package/web/src/components/atoms/StatusDot.module.css +0 -29
  89. package/web/src/components/atoms/TPSCell.jsx +0 -36
  90. package/web/src/components/atoms/TPSCell.module.css +0 -44
  91. package/web/src/components/atoms/TierBadge.jsx +0 -10
  92. package/web/src/components/atoms/TierBadge.module.css +0 -29
  93. package/web/src/components/atoms/Toast.jsx +0 -25
  94. package/web/src/components/atoms/Toast.module.css +0 -41
  95. package/web/src/components/atoms/ToastContainer.jsx +0 -16
  96. package/web/src/components/atoms/ToastContainer.module.css +0 -10
  97. package/web/src/components/atoms/VerdictBadge.jsx +0 -36
  98. package/web/src/components/atoms/VerdictBadge.module.css +0 -40
  99. package/web/src/components/changelog/ChangelogView.jsx +0 -135
  100. package/web/src/components/changelog/ChangelogView.module.css +0 -159
  101. package/web/src/components/dashboard/DetailPanel.jsx +0 -217
  102. package/web/src/components/dashboard/DetailPanel.module.css +0 -260
  103. package/web/src/components/dashboard/ExpandedDetailRow.jsx +0 -361
  104. package/web/src/components/dashboard/ExpandedDetailRow.module.css +0 -356
  105. package/web/src/components/dashboard/ExportModal.jsx +0 -84
  106. package/web/src/components/dashboard/ExportModal.module.css +0 -99
  107. package/web/src/components/dashboard/FilterBar.jsx +0 -338
  108. package/web/src/components/dashboard/FilterBar.module.css +0 -342
  109. package/web/src/components/dashboard/ModelTable.jsx +0 -672
  110. package/web/src/components/dashboard/ModelTable.module.css +0 -446
  111. package/web/src/components/dashboard/ProviderDropdown.jsx +0 -156
  112. package/web/src/components/dashboard/ProviderDropdown.module.css +0 -248
  113. package/web/src/components/help/HelpView.jsx +0 -201
  114. package/web/src/components/help/HelpView.module.css +0 -156
  115. package/web/src/components/install/InstallEndpointsView.jsx +0 -309
  116. package/web/src/components/install/InstallEndpointsView.module.css +0 -427
  117. package/web/src/components/installed/InstalledModelsView.jsx +0 -89
  118. package/web/src/components/installed/InstalledModelsView.module.css +0 -200
  119. package/web/src/components/launch/IncompatibleFallbackModal.jsx +0 -69
  120. package/web/src/components/launch/LaunchButton.jsx +0 -30
  121. package/web/src/components/launch/LaunchButton.module.css +0 -35
  122. package/web/src/components/launch/LaunchModal.module.css +0 -125
  123. package/web/src/components/layout/Footer.jsx +0 -19
  124. package/web/src/components/layout/Footer.module.css +0 -10
  125. package/web/src/components/layout/Header.jsx +0 -265
  126. package/web/src/components/layout/Header.module.css +0 -357
  127. package/web/src/components/palette/CommandPalette.jsx +0 -301
  128. package/web/src/components/palette/CommandPalette.module.css +0 -121
  129. package/web/src/components/playground/PlaygroundChat.jsx +0 -527
  130. package/web/src/components/playground/PlaygroundChat.module.css +0 -382
  131. package/web/src/components/playground/PlaygroundView.jsx +0 -371
  132. package/web/src/components/playground/PlaygroundView.module.css +0 -504
  133. package/web/src/components/recommend/RecommendView.jsx +0 -121
  134. package/web/src/components/recommend/RecommendView.module.css +0 -57
  135. package/web/src/components/router/RouterView.jsx +0 -1212
  136. package/web/src/components/router/RouterView.module.css +0 -1289
  137. package/web/src/components/settings/SettingsView.jsx +0 -555
  138. package/web/src/components/settings/SettingsView.module.css +0 -550
  139. package/web/src/components/tools/ToolPicker.jsx +0 -86
  140. package/web/src/components/tools/ToolPicker.module.css +0 -97
  141. package/web/src/components/update/UpdateChip.jsx +0 -104
  142. package/web/src/components/update/UpdateChip.module.css +0 -146
  143. package/web/src/global.css +0 -298
  144. package/web/src/hooks/urlState.constants.js +0 -28
  145. package/web/src/hooks/useChangelog.js +0 -51
  146. package/web/src/hooks/useColumnSizing.js +0 -211
  147. package/web/src/hooks/useFavorites.js +0 -150
  148. package/web/src/hooks/useFilter.js +0 -310
  149. package/web/src/hooks/useInstalledModels.js +0 -42
  150. package/web/src/hooks/useRecommend.js +0 -67
  151. package/web/src/hooks/useRouterDashboard.js +0 -133
  152. package/web/src/hooks/useSSE.js +0 -73
  153. package/web/src/hooks/useSocket.js +0 -203
  154. package/web/src/hooks/useTheme.js +0 -83
  155. package/web/src/hooks/useTokenUsage.js +0 -103
  156. package/web/src/hooks/useToolMode.js +0 -77
  157. package/web/src/hooks/useUpdateChecker.js +0 -91
  158. package/web/src/hooks/useUrlState.js +0 -170
  159. package/web/src/main.jsx +0 -15
  160. package/web/src/utils/download.js +0 -15
  161. package/web/src/utils/format.js +0 -42
  162. package/web/src/utils/m3.js +0 -57
  163. package/web/src/utils/ranks.js +0 -37
  164. 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
- }