free-coding-models 0.5.86 → 0.5.88
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +109 -888
- package/bin/free-coding-models.js +4 -1
- package/changelog/v0.5.87.md +48 -0
- package/changelog/v0.5.88.md +16 -0
- package/package.json +13 -5
- package/sources.js +4 -4
- package/src/core/cache.js +3 -1
- package/src/core/config.js +43 -5
- package/src/core/endpoint-installer.js +23 -12
- package/src/core/installed-models-manager.js +43 -20
- package/src/core/kilo-config.js +51 -5
- package/src/core/kilo.js +5 -3
- package/src/core/legacy-proxy-cleanup.js +22 -5
- package/src/core/model-merger.js +4 -0
- package/src/core/models-drift.js +9 -0
- package/src/core/opencode-config.js +57 -5
- package/src/core/opencode.js +67 -41
- package/src/core/playground.js +7 -0
- package/src/core/probe-cache.js +35 -4
- package/src/core/provider-key-tester.js +10 -2
- package/src/core/provider-metadata.js +10 -0
- package/src/core/provider-quota-fetchers.js +14 -7
- package/src/core/router-daemon.js +208 -26
- package/src/core/runtime-telemetry.js +27 -1
- package/src/core/security.js +170 -24
- package/src/core/shared-helpers.js +12 -1
- package/src/core/shell-env.js +3 -1
- package/src/core/telemetry.js +18 -5
- package/src/core/tool-launchers.js +9 -6
- package/src/core/updater.js +28 -2
- package/src/core/utils.js +173 -71
- package/src/tui/app.js +38 -14
- package/src/tui/command-palette.js +22 -2
- package/src/tui/key-handler.js +123 -179
- package/src/tui/overlays.js +21 -71
- package/src/tui/render-helpers.js +62 -4
- package/src/tui/render-table.js +34 -3
- package/src/tui/theme.js +36 -5
- package/src/tui/tui-filters.js +28 -8
- package/src/tui/tui-state.js +22 -6
- package/web/dist/assets/index-CAzFIt8P.css +1 -0
- package/web/dist/assets/index-DFg1h0Nd.js +44 -0
- package/web/dist/index.html +5 -9
- package/web/index.html +3 -7
- package/web/server.js +97 -23
- package/web/vite.config.js +8 -0
- package/web/app.legacy.js +0 -900
- package/web/dist/assets/index-C5-Ywvv3.css +0 -1
- package/web/dist/assets/index-Dm-Xfby9.js +0 -40
- package/web/public/favicon.ico +0 -0
- package/web/public/favicons/apple-touch-icon.png +0 -0
- package/web/public/favicons/browserconfig.xml +0 -12
- package/web/public/favicons/favicon-16x16.png +0 -0
- package/web/public/favicons/favicon-192x192.png +0 -0
- package/web/public/favicons/favicon-32x32.png +0 -0
- package/web/public/favicons/favicon-48x48.png +0 -0
- package/web/public/favicons/favicon-512x512.png +0 -0
- package/web/public/favicons/favicon-96x96.png +0 -0
- package/web/public/favicons/favicon.ico +0 -0
- package/web/public/favicons/mstile-150x150.png +0 -0
- package/web/public/favicons/mstile-310x150.png +0 -0
- package/web/public/favicons/mstile-310x310.png +0 -0
- package/web/public/favicons/mstile-512x512.png +0 -0
- package/web/public/favicons/mstile-70x70.png +0 -0
- package/web/public/favicons/site.webmanifest +0 -25
- package/web/src/App.jsx +0 -521
- package/web/src/components/analytics/AnalyticsView.jsx +0 -147
- package/web/src/components/analytics/AnalyticsView.module.css +0 -190
- package/web/src/components/analytics/TokenUsagePanel.jsx +0 -105
- package/web/src/components/analytics/TokenUsagePanel.module.css +0 -126
- package/web/src/components/atoms/AILatencyCell.jsx +0 -38
- package/web/src/components/atoms/AILatencyCell.module.css +0 -51
- package/web/src/components/atoms/HealthCell.jsx +0 -143
- package/web/src/components/atoms/HealthCell.module.css +0 -60
- package/web/src/components/atoms/LastPingCell.jsx +0 -35
- package/web/src/components/atoms/LastPingCell.module.css +0 -45
- package/web/src/components/atoms/MoodCell.jsx +0 -25
- package/web/src/components/atoms/MoodCell.module.css +0 -6
- package/web/src/components/atoms/NoKeyIcon.jsx +0 -65
- package/web/src/components/atoms/NoKeyIcon.module.css +0 -30
- package/web/src/components/atoms/ProviderLogo.jsx +0 -187
- package/web/src/components/atoms/ProviderLogo.module.css +0 -103
- package/web/src/components/atoms/RankCell.jsx +0 -9
- package/web/src/components/atoms/RankCell.module.css +0 -9
- package/web/src/components/atoms/Sparkline.jsx +0 -63
- package/web/src/components/atoms/StabilityCell.jsx +0 -18
- package/web/src/components/atoms/StabilityCell.module.css +0 -13
- package/web/src/components/atoms/StatusDot.jsx +0 -13
- package/web/src/components/atoms/StatusDot.module.css +0 -29
- package/web/src/components/atoms/TPSCell.jsx +0 -36
- package/web/src/components/atoms/TPSCell.module.css +0 -44
- package/web/src/components/atoms/TierBadge.jsx +0 -10
- package/web/src/components/atoms/TierBadge.module.css +0 -29
- package/web/src/components/atoms/Toast.jsx +0 -25
- package/web/src/components/atoms/Toast.module.css +0 -41
- package/web/src/components/atoms/ToastContainer.jsx +0 -16
- package/web/src/components/atoms/ToastContainer.module.css +0 -10
- package/web/src/components/atoms/VerdictBadge.jsx +0 -36
- package/web/src/components/atoms/VerdictBadge.module.css +0 -40
- package/web/src/components/changelog/ChangelogView.jsx +0 -135
- package/web/src/components/changelog/ChangelogView.module.css +0 -159
- package/web/src/components/dashboard/DetailPanel.jsx +0 -217
- package/web/src/components/dashboard/DetailPanel.module.css +0 -260
- package/web/src/components/dashboard/ExpandedDetailRow.jsx +0 -361
- package/web/src/components/dashboard/ExpandedDetailRow.module.css +0 -356
- package/web/src/components/dashboard/ExportModal.jsx +0 -84
- package/web/src/components/dashboard/ExportModal.module.css +0 -99
- package/web/src/components/dashboard/FilterBar.jsx +0 -338
- package/web/src/components/dashboard/FilterBar.module.css +0 -342
- package/web/src/components/dashboard/ModelTable.jsx +0 -672
- package/web/src/components/dashboard/ModelTable.module.css +0 -446
- package/web/src/components/dashboard/ProviderDropdown.jsx +0 -156
- package/web/src/components/dashboard/ProviderDropdown.module.css +0 -248
- package/web/src/components/help/HelpView.jsx +0 -201
- package/web/src/components/help/HelpView.module.css +0 -156
- package/web/src/components/install/InstallEndpointsView.jsx +0 -309
- package/web/src/components/install/InstallEndpointsView.module.css +0 -427
- package/web/src/components/installed/InstalledModelsView.jsx +0 -89
- package/web/src/components/installed/InstalledModelsView.module.css +0 -200
- package/web/src/components/launch/IncompatibleFallbackModal.jsx +0 -69
- package/web/src/components/launch/LaunchButton.jsx +0 -30
- package/web/src/components/launch/LaunchButton.module.css +0 -35
- package/web/src/components/launch/LaunchModal.module.css +0 -125
- package/web/src/components/layout/Footer.jsx +0 -19
- package/web/src/components/layout/Footer.module.css +0 -10
- package/web/src/components/layout/Header.jsx +0 -265
- package/web/src/components/layout/Header.module.css +0 -357
- package/web/src/components/palette/CommandPalette.jsx +0 -301
- package/web/src/components/palette/CommandPalette.module.css +0 -121
- package/web/src/components/playground/PlaygroundChat.jsx +0 -527
- package/web/src/components/playground/PlaygroundChat.module.css +0 -382
- package/web/src/components/playground/PlaygroundView.jsx +0 -371
- package/web/src/components/playground/PlaygroundView.module.css +0 -504
- package/web/src/components/recommend/RecommendView.jsx +0 -121
- package/web/src/components/recommend/RecommendView.module.css +0 -57
- package/web/src/components/router/RouterView.jsx +0 -1212
- package/web/src/components/router/RouterView.module.css +0 -1289
- package/web/src/components/settings/SettingsView.jsx +0 -555
- package/web/src/components/settings/SettingsView.module.css +0 -550
- package/web/src/components/tools/ToolPicker.jsx +0 -86
- package/web/src/components/tools/ToolPicker.module.css +0 -97
- package/web/src/components/update/UpdateChip.jsx +0 -104
- package/web/src/components/update/UpdateChip.module.css +0 -146
- package/web/src/global.css +0 -298
- package/web/src/hooks/urlState.constants.js +0 -28
- package/web/src/hooks/useChangelog.js +0 -51
- package/web/src/hooks/useColumnSizing.js +0 -211
- package/web/src/hooks/useFavorites.js +0 -150
- package/web/src/hooks/useFilter.js +0 -310
- package/web/src/hooks/useInstalledModels.js +0 -42
- package/web/src/hooks/useRecommend.js +0 -67
- package/web/src/hooks/useRouterDashboard.js +0 -133
- package/web/src/hooks/useSSE.js +0 -73
- package/web/src/hooks/useSocket.js +0 -203
- package/web/src/hooks/useTheme.js +0 -83
- package/web/src/hooks/useTokenUsage.js +0 -103
- package/web/src/hooks/useToolMode.js +0 -77
- package/web/src/hooks/useUpdateChecker.js +0 -91
- package/web/src/hooks/useUrlState.js +0 -170
- package/web/src/main.jsx +0 -15
- package/web/src/utils/download.js +0 -15
- package/web/src/utils/format.js +0 -42
- package/web/src/utils/m3.js +0 -57
- package/web/src/utils/ranks.js +0 -37
- 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
|
-
}
|