free-coding-models 0.5.26 → 0.5.28
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 +14 -9
- package/changelog/v0.5.27.md +17 -0
- package/changelog/v0.5.28.md +5 -0
- package/package.json +1 -1
- package/sources.js +98 -1
- package/src/core/config.js +29 -4
- package/src/core/provider-metadata.js +72 -1
- package/src/core/quota-capabilities.js +5 -0
- package/src/core/router-daemon.js +84 -2
- package/src/core/setup.js +5 -4
- package/src/tui/app.js +21 -4
- package/src/tui/command-palette.js +2 -1
- package/src/tui/key-handler.js +157 -3
- package/src/tui/overlays.js +25 -6
- package/src/tui/render-table.js +18 -2
- package/src/tui/theme.js +10 -0
- package/src/tui/tui-filters.js +1 -1
- package/src/tui/tui-state.js +6 -0
- package/web/dist/assets/index-BRFowJv5.css +1 -0
- package/web/dist/assets/index-Pr3waI0-.js +41 -0
- package/web/dist/index.html +2 -2
- package/web/server.js +9 -0
- package/web/src/components/atoms/HealthCell.jsx +12 -7
- package/web/src/components/atoms/HealthCell.module.css +4 -0
- package/web/src/components/atoms/StatusDot.jsx +5 -2
- package/web/src/components/atoms/StatusDot.module.css +12 -7
- package/web/src/components/dashboard/ModelTable.jsx +5 -2
- package/web/src/components/dashboard/ModelTable.module.css +9 -0
- package/web/src/components/settings/SettingsView.jsx +4 -4
- package/web/dist/assets/index-BDV-Qeuc.js +0 -41
- package/web/dist/assets/index-avN2GM3H.css +0 -1
|
@@ -200,7 +200,8 @@ const BASE_COMMAND_TREE = [
|
|
|
200
200
|
],
|
|
201
201
|
},
|
|
202
202
|
{ id: 'action-cycle-theme', label: 'Cycle theme', shortcut: 'G', icon: '🌗', description: 'Switch dark/light/auto', keywords: ['theme', 'dark', 'light', 'auto'] },
|
|
203
|
-
{ id: 'action-reset-view', label: 'Reset view', shortcut: 'N', icon: '
|
|
203
|
+
{ id: 'action-reset-view', label: 'Reset view', shortcut: 'N', icon: '\u{1F504}', description: 'Reset filters and sort', keywords: ['reset', 'view', 'sort', 'filters'] },
|
|
204
|
+
{ id: 'action-probe-404', label: 'Probe 404 Models', shortcut: 'Ctrl+Shift+P', icon: '\u{1F50D}', description: 'Test all configured models for 404/410 errors. Auto-hides broken models.', keywords: ['probe', '404', 'broken', 'dead', 'health', 'check', 'test', 'verify'] },
|
|
204
205
|
],
|
|
205
206
|
},
|
|
206
207
|
// 📖 Pages - directly at root level, not in submenu
|
package/src/tui/key-handler.js
CHANGED
|
@@ -107,6 +107,11 @@ const PROVIDER_AUTH_ENDPOINTS = {
|
|
|
107
107
|
zai: null, // 📖 ZAI undocumented; use ping only
|
|
108
108
|
googleai: null, // 📖 Google AI Studio has no OpenAI-compatible /models; use ping
|
|
109
109
|
'opencode-zen': null, // 📖 OpenCode Zen uses OpenCode auth only; use ping
|
|
110
|
+
kilo: { url: 'https://api.kilo.ai/api/gateway/models', method: 'GET' },
|
|
111
|
+
llm7: { url: 'https://api.llm7.io/v1/models', method: 'GET' },
|
|
112
|
+
routeway: { url: 'https://api.routeway.ai/v1/models', method: 'GET' },
|
|
113
|
+
novita: { url: 'https://api.novita.ai/openai/v1/models', method: 'GET' },
|
|
114
|
+
'ollama-cloud': { url: 'https://ollama.com/v1/models', method: 'GET' },
|
|
110
115
|
}
|
|
111
116
|
|
|
112
117
|
// 📖 Sleep helper kept local to this module so the Settings key test flow can
|
|
@@ -787,6 +792,42 @@ export function createKeyHandler(ctx) {
|
|
|
787
792
|
})
|
|
788
793
|
}
|
|
789
794
|
|
|
795
|
+
// 📖 toggleAutoHideBrokenModels: Toggle auto-hiding of 404/410 models from probe.
|
|
796
|
+
// 📖 When disabling, unhide all previously hidden models so they become visible again.
|
|
797
|
+
function toggleAutoHideBrokenModels() {
|
|
798
|
+
if (!state.config.settings || typeof state.config.settings !== 'object') state.config.settings = {}
|
|
799
|
+
const wasEnabled = state.config.settings.autoHideBrokenModels !== false
|
|
800
|
+
state.config.settings.autoHideBrokenModels = !wasEnabled
|
|
801
|
+
|
|
802
|
+
if (!wasEnabled) {
|
|
803
|
+
// 📖 Just enabled — models that were hidden before enabling will be picked up on next probe
|
|
804
|
+
} else {
|
|
805
|
+
// 📖 Just disabled — unhide all probe-hidden models so user can see them again
|
|
806
|
+
if (state.config.hiddenModels instanceof Set && state.config.hiddenModels.size > 0) {
|
|
807
|
+
const keysToUnhide = [...state.config.hiddenModels]
|
|
808
|
+
state.config.hiddenModels.clear()
|
|
809
|
+
for (const key of keysToUnhide) {
|
|
810
|
+
const [pk, ...rest] = key.split('/')
|
|
811
|
+
const modelId = rest.join('/')
|
|
812
|
+
const result = state.results.find(r => r.providerKey === pk && r.modelId === modelId)
|
|
813
|
+
if (result) result.hidden = false
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
saveConfig(state.config)
|
|
819
|
+
const hiddenCount = state.config.hiddenModels instanceof Set ? state.config.hiddenModels.size : 0
|
|
820
|
+
state.settingsSyncStatus = {
|
|
821
|
+
type: 'success',
|
|
822
|
+
msg: state.config.settings.autoHideBrokenModels
|
|
823
|
+
? `✅ Auto-hide enabled — broken models (404/410) from Ctrl+Shift+P probe are hidden automatically.${hiddenCount ? ` (${hiddenCount} currently hidden)` : ''}`
|
|
824
|
+
: '✅ Auto-hide disabled — all models are visible. Previously hidden models have been unhidden.',
|
|
825
|
+
}
|
|
826
|
+
trackAppAction('auto_hide_broken_models_toggled', {
|
|
827
|
+
enabled: state.config.settings.autoHideBrokenModels !== false,
|
|
828
|
+
})
|
|
829
|
+
}
|
|
830
|
+
|
|
790
831
|
function toggleShellEnv() {
|
|
791
832
|
if (!state.config.settings) state.config.settings = {}
|
|
792
833
|
const currentlyEnabled = state.config.settings.shellEnvEnabled === true
|
|
@@ -1127,6 +1168,96 @@ export function createKeyHandler(ctx) {
|
|
|
1127
1168
|
return Promise.all(workers).then(() => results)
|
|
1128
1169
|
}
|
|
1129
1170
|
|
|
1171
|
+
// 📖 runBrokenModelProbe: Probe all configured models for 404/broken endpoints.
|
|
1172
|
+
// 📖 Sends a real chat-completion request to each model with an API key.
|
|
1173
|
+
// 📖 Models returning 404 or 410 are auto-hidden when setting is enabled.
|
|
1174
|
+
// 📖 Results flash live in the TUI table — models flip status in real time.
|
|
1175
|
+
async function runBrokenModelProbe(state) {
|
|
1176
|
+
if (state.probeRunning) return
|
|
1177
|
+
|
|
1178
|
+
// 📖 Only probe models where the user has an API key configured
|
|
1179
|
+
const probeable = state.results.filter(r => {
|
|
1180
|
+
const apiKey = getApiKey(state.config, r.providerKey)
|
|
1181
|
+
return !!apiKey
|
|
1182
|
+
})
|
|
1183
|
+
|
|
1184
|
+
if (probeable.length === 0) return
|
|
1185
|
+
|
|
1186
|
+
state.probeRunning = true
|
|
1187
|
+
state.probeTotal = probeable.length
|
|
1188
|
+
state.probeCompleted = 0
|
|
1189
|
+
state.probeHiddenCount = 0
|
|
1190
|
+
|
|
1191
|
+
const autoHide = state.config.settings?.autoHideBrokenModels !== false
|
|
1192
|
+
const HTTP_CODES_404 = new Set([404, '404'])
|
|
1193
|
+
const HTTP_CODES_GONE = new Set([410, '410'])
|
|
1194
|
+
|
|
1195
|
+
const tasks = probeable.map(r => async () => {
|
|
1196
|
+
const apiKey = getApiKey(state.config, r.providerKey) ?? null
|
|
1197
|
+
const providerUrl = sources[r.providerKey]?.url ?? null
|
|
1198
|
+
if (!apiKey || !providerUrl) {
|
|
1199
|
+
state.probeCompleted++
|
|
1200
|
+
return { model: r, ok: false, reason: 'no_key' }
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
try {
|
|
1204
|
+
const { code } = await ping(apiKey, r.modelId, r.providerKey, providerUrl)
|
|
1205
|
+
state.probeCompleted++
|
|
1206
|
+
|
|
1207
|
+
if (HTTP_CODES_404.has(code) || HTTP_CODES_GONE.has(code)) {
|
|
1208
|
+
// 📖 Model is broken (404/410) — mark it
|
|
1209
|
+
r.status = 'down'
|
|
1210
|
+
r.httpCode = String(code)
|
|
1211
|
+
|
|
1212
|
+
if (autoHide) {
|
|
1213
|
+
const modelKey = `${r.providerKey}/${r.modelId}`
|
|
1214
|
+
if (!state.config.hiddenModels) state.config.hiddenModels = new Set()
|
|
1215
|
+
state.config.hiddenModels.add(modelKey)
|
|
1216
|
+
r.hidden = true
|
|
1217
|
+
state.probeHiddenCount++
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
return { model: r, ok: false, code, reason: 'broken' }
|
|
1221
|
+
} else if (code === '200') {
|
|
1222
|
+
// 📖 Model is alive — unhide if it was previously hidden by probe
|
|
1223
|
+
const modelKey = `${r.providerKey}/${r.modelId}`
|
|
1224
|
+
if (state.config.hiddenModels?.has(modelKey)) {
|
|
1225
|
+
state.config.hiddenModels.delete(modelKey)
|
|
1226
|
+
r.hidden = false
|
|
1227
|
+
}
|
|
1228
|
+
r.status = 'up'
|
|
1229
|
+
return { model: r, ok: true, code }
|
|
1230
|
+
} else {
|
|
1231
|
+
// 📖 Other codes (401, 429, etc.) — leave status unchanged, don't hide
|
|
1232
|
+
return { model: r, ok: false, code, reason: 'other' }
|
|
1233
|
+
}
|
|
1234
|
+
} catch (err) {
|
|
1235
|
+
state.probeCompleted++
|
|
1236
|
+
return { model: r, ok: false, reason: 'error', error: err?.message }
|
|
1237
|
+
}
|
|
1238
|
+
})
|
|
1239
|
+
|
|
1240
|
+
await runWithConcurrency(tasks, 5)
|
|
1241
|
+
|
|
1242
|
+
// 📖 Persist hidden models set to config
|
|
1243
|
+
if (autoHide && state.probeHiddenCount > 0) {
|
|
1244
|
+
saveConfig(state.config)
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
state.probeRunning = false
|
|
1248
|
+
// 📖 Keep totals visible for a few seconds so user can see results
|
|
1249
|
+
setTimeout(() => {
|
|
1250
|
+
if (!state.probeRunning) {
|
|
1251
|
+
state.probeTotal = 0
|
|
1252
|
+
state.probeCompleted = 0
|
|
1253
|
+
// 📖 Don't reset probeHiddenCount immediately — user needs to see how many were hidden
|
|
1254
|
+
setTimeout(() => {
|
|
1255
|
+
state.probeHiddenCount = 0
|
|
1256
|
+
}, 5000)
|
|
1257
|
+
}
|
|
1258
|
+
}, 3000)
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1130
1261
|
// 📖 runGlobalBenchmark: Benchmark all visible models with up to 5 concurrent requests.
|
|
1131
1262
|
// 📖 Results are stored in state.benchmarkResults (same format as individual benchmarks).
|
|
1132
1263
|
async function runGlobalBenchmark(state) {
|
|
@@ -1481,6 +1612,7 @@ export function createKeyHandler(ctx) {
|
|
|
1481
1612
|
case 'action-toggle-favorite': return toggleFavoriteOnSelectedRow()
|
|
1482
1613
|
case 'action-toggle-favorite-mode': return toggleFavoritesDisplayMode()
|
|
1483
1614
|
case 'action-reset-view': return resetViewSettings()
|
|
1615
|
+
case 'action-probe-404': return runBrokenModelProbe(state)
|
|
1484
1616
|
default:
|
|
1485
1617
|
return
|
|
1486
1618
|
}
|
|
@@ -1507,13 +1639,22 @@ export function createKeyHandler(ctx) {
|
|
|
1507
1639
|
}
|
|
1508
1640
|
|
|
1509
1641
|
// 📖 Ctrl+U: Global AI Speed Benchmark (benchmark all visible models, 5 concurrent)
|
|
1510
|
-
//
|
|
1511
|
-
//
|
|
1642
|
+
// Also handles the raw \x15 byte as a fallback for terminals where readline doesn't
|
|
1643
|
+
// set key.ctrl properly (same pattern as Ctrl+C → \x03 fallback).
|
|
1512
1644
|
if ((key.ctrl && key.name === 'u') || str === '\x15') {
|
|
1513
1645
|
await runGlobalBenchmark(state)
|
|
1514
1646
|
return
|
|
1515
1647
|
}
|
|
1516
1648
|
|
|
1649
|
+
// 📖 Ctrl+Shift+P: Probe all configured models for 404/broken endpoints.
|
|
1650
|
+
// 📖 Sends a real chat-completion request to every model that has an API key.
|
|
1651
|
+
// 📖 Models returning 404/410 are auto-hidden (when setting is enabled).
|
|
1652
|
+
// 📖 Results flash live in the table — models flip from down→up in real time.
|
|
1653
|
+
if (key.ctrl && key.shift && key.name === 'p') {
|
|
1654
|
+
await runBrokenModelProbe(state)
|
|
1655
|
+
return
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1517
1658
|
// 📖 Command palette captures the keyboard while active.
|
|
1518
1659
|
if (state.commandPaletteOpen) {
|
|
1519
1660
|
if (key.ctrl && key.name === 'c') { exit(0); return }
|
|
@@ -2529,7 +2670,8 @@ export function createKeyHandler(ctx) {
|
|
|
2529
2670
|
const themeRowIdx = updateRowIdx + 1
|
|
2530
2671
|
const favoritesModeRowIdx = themeRowIdx + 1
|
|
2531
2672
|
const startupAiSpeedScanRowIdx = favoritesModeRowIdx + 1
|
|
2532
|
-
const
|
|
2673
|
+
const autoHideBrokenModelsRowIdx = startupAiSpeedScanRowIdx + 1
|
|
2674
|
+
const cleanupLegacyProxyRowIdx = autoHideBrokenModelsRowIdx + 1
|
|
2533
2675
|
const changelogViewRowIdx = cleanupLegacyProxyRowIdx + 1
|
|
2534
2676
|
const shellEnvRowIdx = changelogViewRowIdx + 1
|
|
2535
2677
|
// 📖 Profile system removed - API keys now persist permanently across all sessions
|
|
@@ -2691,6 +2833,12 @@ export function createKeyHandler(ctx) {
|
|
|
2691
2833
|
return
|
|
2692
2834
|
}
|
|
2693
2835
|
|
|
2836
|
+
// 📖 Auto-hide broken models toggle
|
|
2837
|
+
if (state.settingsCursor === autoHideBrokenModelsRowIdx) {
|
|
2838
|
+
toggleAutoHideBrokenModels()
|
|
2839
|
+
return
|
|
2840
|
+
}
|
|
2841
|
+
|
|
2694
2842
|
if (state.settingsCursor === cleanupLegacyProxyRowIdx) {
|
|
2695
2843
|
runLegacyProxyCleanup()
|
|
2696
2844
|
return
|
|
@@ -2748,6 +2896,11 @@ export function createKeyHandler(ctx) {
|
|
|
2748
2896
|
toggleStartupAiSpeedScan()
|
|
2749
2897
|
return
|
|
2750
2898
|
}
|
|
2899
|
+
// 📖 Auto-hide broken models toggle (space)
|
|
2900
|
+
if (state.settingsCursor === autoHideBrokenModelsRowIdx) {
|
|
2901
|
+
toggleAutoHideBrokenModels()
|
|
2902
|
+
return
|
|
2903
|
+
}
|
|
2751
2904
|
// 📖 Profile system removed - API keys now persist permanently across all sessions
|
|
2752
2905
|
|
|
2753
2906
|
// 📖 Toggle enabled/disabled for selected provider
|
|
@@ -2765,6 +2918,7 @@ export function createKeyHandler(ctx) {
|
|
|
2765
2918
|
|| state.settingsCursor === themeRowIdx
|
|
2766
2919
|
|| state.settingsCursor === favoritesModeRowIdx
|
|
2767
2920
|
|| state.settingsCursor === startupAiSpeedScanRowIdx
|
|
2921
|
+
|| state.settingsCursor === autoHideBrokenModelsRowIdx
|
|
2768
2922
|
|| state.settingsCursor === cleanupLegacyProxyRowIdx
|
|
2769
2923
|
|| state.settingsCursor === changelogViewRowIdx
|
|
2770
2924
|
) return
|
package/src/tui/overlays.js
CHANGED
|
@@ -24,6 +24,7 @@ import { buildCliHelpLines } from './cli-help.js'
|
|
|
24
24
|
import { renderRouterDashboard as renderRouterDashboardOverlay } from '../core/router-dashboard.js'
|
|
25
25
|
import { renderPlayground as renderPlaygroundOverlay } from '../core/playground.js'
|
|
26
26
|
import { themeColors, getThemeStatusLabel, getProviderRgb } from './theme.js'
|
|
27
|
+
import { getProviderBillingNote, getProviderLabelWithBilling } from '../core/provider-metadata.js'
|
|
27
28
|
|
|
28
29
|
export function createOverlayRenderers(state, deps) {
|
|
29
30
|
const {
|
|
@@ -120,7 +121,8 @@ export function createOverlayRenderers(state, deps) {
|
|
|
120
121
|
const themeRowIdx = updateRowIdx + 1
|
|
121
122
|
const favoritesModeRowIdx = themeRowIdx + 1
|
|
122
123
|
const startupAiSpeedScanRowIdx = favoritesModeRowIdx + 1
|
|
123
|
-
const
|
|
124
|
+
const autoHideBrokenModelsRowIdx = startupAiSpeedScanRowIdx + 1
|
|
125
|
+
const cleanupLegacyProxyRowIdx = autoHideBrokenModelsRowIdx + 1
|
|
124
126
|
const changelogViewRowIdx = cleanupLegacyProxyRowIdx + 1
|
|
125
127
|
const shellEnvRowIdx = changelogViewRowIdx + 1
|
|
126
128
|
const EL = '\x1b[K'
|
|
@@ -183,13 +185,16 @@ export function createOverlayRenderers(state, deps) {
|
|
|
183
185
|
else if (testResult === 'rate_limited') testBadge = themeColors.warning('[Rate limit ⏳]')
|
|
184
186
|
else if (testResult === 'no_callable_model') testBadge = chalk.rgb(...getProviderRgb('openrouter'))('[No model ⚠]')
|
|
185
187
|
else if (testResult === 'fail') testBadge = themeColors.error('[Test ❌]')
|
|
186
|
-
// 📖 No truncation of rate limits - overlay now uses 100% terminal width
|
|
187
|
-
|
|
188
|
+
// 📖 No truncation of rate limits - overlay now uses 100% terminal width.
|
|
189
|
+
// 📖 Paid/credits-required providers get an explicit money marker + parenthesized detail.
|
|
190
|
+
const billingNote = getProviderBillingNote(pk)
|
|
191
|
+
const rateSummary = themeColors.dim(`${meta.rateLimits || 'No limit info'}${billingNote ? ` ${billingNote}` : ''}`)
|
|
188
192
|
|
|
189
193
|
const enabledBadge = enabled ? themeColors.successBold('✅') : themeColors.errorBold('❌')
|
|
190
194
|
// 📖 Color provider names the same way as in the main table
|
|
191
195
|
const providerRgb = PROVIDER_COLOR[pk] ?? [105, 190, 245]
|
|
192
|
-
const
|
|
196
|
+
const providerLabel = getProviderLabelWithBilling(pk, src.name || pk)
|
|
197
|
+
const providerName = chalk.bold.rgb(...providerRgb)(providerLabel.slice(0, 24).padEnd(24))
|
|
193
198
|
|
|
194
199
|
const row = `${bullet(isCursor)}[ ${enabledBadge} ] ${providerName} ${padEndDisplay(keyDisplay, 30)} ${testBadge} ${rateSummary}`
|
|
195
200
|
cursorLineByRow[i] = lines.length
|
|
@@ -205,9 +210,12 @@ export function createOverlayRenderers(state, deps) {
|
|
|
205
210
|
const setupStatus = selectedKey ? themeColors.success('API key detected ✅') : themeColors.warning('API key missing ⚠')
|
|
206
211
|
// 📖 Color the provider name in the setup instructions header
|
|
207
212
|
const selectedProviderRgb = PROVIDER_COLOR[selectedProviderKey] ?? [105, 190, 245]
|
|
208
|
-
const
|
|
209
|
-
|
|
213
|
+
const selectedProviderLabel = getProviderLabelWithBilling(selectedProviderKey, selectedSource.name || selectedProviderKey)
|
|
214
|
+
const selectedBillingNote = getProviderBillingNote(selectedProviderKey)
|
|
215
|
+
const coloredProviderName = chalk.bold.rgb(...selectedProviderRgb)(selectedProviderLabel)
|
|
216
|
+
lines.push(` ${themeColors.textBold('Setup Instructions')} — ${coloredProviderName}${selectedBillingNote ? ' ' + themeColors.warning(selectedBillingNote) : ''}`)
|
|
210
217
|
lines.push(themeColors.dim(` 1) Create a ${selectedMeta.label || selectedSource.name} account: ${selectedMeta.signupUrl || 'signup link missing'}`))
|
|
218
|
+
if (selectedBillingNote) lines.push(themeColors.warning(` 💰 Paid provider note: ${selectedBillingNote}`))
|
|
211
219
|
lines.push(themeColors.dim(` 2) ${selectedMeta.signupHint || 'Generate an API key and paste it with Enter on this row'}`))
|
|
212
220
|
lines.push(themeColors.dim(` 3) Press ${themeColors.hotkey('T')} to test your key. Status: ${setupStatus}`))
|
|
213
221
|
if (selectedProviderKey === 'cloudflare') {
|
|
@@ -270,6 +278,16 @@ export function createOverlayRenderers(state, deps) {
|
|
|
270
278
|
cursorLineByRow[startupAiSpeedScanRowIdx] = lines.length
|
|
271
279
|
lines.push(state.settingsCursor === startupAiSpeedScanRowIdx ? themeColors.bgCursorSettingsList(startupAiSpeedScanRow) : startupAiSpeedScanRow)
|
|
272
280
|
|
|
281
|
+
// 📖 Auto-hide broken models row: toggles auto-hiding of 404/410 models from probe.
|
|
282
|
+
const autoHideEnabled = state.config.settings?.autoHideBrokenModels !== false
|
|
283
|
+
const hiddenCount = state.config.hiddenModels instanceof Set ? state.config.hiddenModels.size : 0
|
|
284
|
+
const autoHideStatus = autoHideEnabled
|
|
285
|
+
? themeColors.successBold(`✅ Enabled (${hiddenCount} hidden)`)
|
|
286
|
+
: themeColors.errorBold('❌ Disabled')
|
|
287
|
+
const autoHideRow = `${bullet(state.settingsCursor === autoHideBrokenModelsRowIdx)}${themeColors.textBold('Auto-hide Broken Models').padEnd(44)} ${autoHideStatus}`
|
|
288
|
+
cursorLineByRow[autoHideBrokenModelsRowIdx] = lines.length
|
|
289
|
+
lines.push(state.settingsCursor === autoHideBrokenModelsRowIdx ? themeColors.bgCursorSettingsList(autoHideRow) : autoHideRow)
|
|
290
|
+
|
|
273
291
|
if (updateState === 'error' && state.settingsUpdateError) {
|
|
274
292
|
lines.push(themeColors.error(` ${state.settingsUpdateError}`))
|
|
275
293
|
}
|
|
@@ -943,6 +961,7 @@ export function createOverlayRenderers(state, deps) {
|
|
|
943
961
|
lines.push(` ${key('Ctrl+P')} Open ⚡️ command palette ${hint('(search and run actions quickly)')}`)
|
|
944
962
|
lines.push(` ${key('Ctrl+A')} AI Speed Test ${hint('(benchmark selected model → time + TPS)')}`)
|
|
945
963
|
lines.push(` ${key('Ctrl+U')} Global AI Speed Test ${hint('(benchmark all models; Settings can auto-run it on startup)')}`)
|
|
964
|
+
lines.push(` ${key('Ctrl+Shift+P')} Probe 404 Models ${hint('(test all configured models; auto-hide broken 404/410)')}`)
|
|
946
965
|
lines.push(` ${key('E')} Cycle filter mode ${hint('(Normal → Configured only → Usable only)')}`)
|
|
947
966
|
lines.push(` ${key('Z')} Cycle tool mode ${hint('(📦 OpenCode → π Pi → 🪼 jcode → 📦 Desktop → 🦞 OpenClaw → 💘 Crush → 🪿 Goose → 🛠 Aider → 🐉 Qwen → 🤲 OpenHands → ⚡ Amp)')}`)
|
|
948
967
|
lines.push(` ${key('F')} Toggle favorite on selected row ${hint('(1️⃣2️⃣3️⃣ = router fallback order, capped at 🔟)')}`)
|
package/src/tui/render-table.js
CHANGED
|
@@ -192,6 +192,10 @@ export function renderTable({
|
|
|
192
192
|
benchmarkResults = {},
|
|
193
193
|
benchmarkRunning = new Set(),
|
|
194
194
|
headerFlashColumn = null,
|
|
195
|
+
probeRunning = false,
|
|
196
|
+
probeTotal = 0,
|
|
197
|
+
probeCompleted = 0,
|
|
198
|
+
probeHiddenCount = 0,
|
|
195
199
|
} = _) {
|
|
196
200
|
// 📖 Filter out hidden models for display
|
|
197
201
|
const visibleResults = results.filter(r => !r.hidden)
|
|
@@ -1157,13 +1161,25 @@ export function renderTable({
|
|
|
1157
1161
|
const speedTestLabel = chalk.bgRgb(...currentPalette().badgeSpeedTestBg).rgb(...currentPalette().badgeSpeedTestFg).bold(' NEW ⭐️ Ctrl+A 🤖 AI Speed Test ')
|
|
1158
1162
|
const globalBenchmarkLabel = chalk.bgRgb(...currentPalette().badgeBenchmarkBg).rgb(...currentPalette().badgeBenchmarkFg).bold(' NEW Ctrl+U : Global AI Speed Test (Uses a lot of requests!) ')
|
|
1159
1163
|
|
|
1160
|
-
// 📖
|
|
1161
|
-
|
|
1164
|
+
// 📖 Probe badge: show progress when 404 probe is running or recently completed
|
|
1165
|
+
let probeLabel = ''
|
|
1166
|
+
if (probeRunning) {
|
|
1167
|
+
const pct = probeTotal > 0 ? Math.round((probeCompleted / probeTotal) * 100) : 0
|
|
1168
|
+
const bar = '█'.repeat(Math.floor(pct / 5)) + '░'.repeat(20 - Math.floor(pct / 5))
|
|
1169
|
+
probeLabel = chalk.bgRgb(180, 40, 40).rgb(255, 255, 255).bold(` 🔍 Probe ${bar} ${probeCompleted}/${probeTotal} `)
|
|
1170
|
+
} else if (probeHiddenCount > 0 && probeTotal > 0) {
|
|
1171
|
+
probeLabel = chalk.bgRgb(120, 60, 60).rgb(255, 200, 200).bold(` 🔍 Probe done: ${probeHiddenCount} broken model${probeHiddenCount > 1 ? 's' : ''} hidden `)
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
// 📖 Line 3: Speed Test + Global Benchmark + Probe + Last release
|
|
1175
|
+
if (releaseLabel || speedTestLabel || globalBenchmarkLabel || probeLabel) {
|
|
1162
1176
|
const parts = [
|
|
1163
1177
|
{ text: ' ', key: null },
|
|
1164
1178
|
{ text: speedTestLabel, key: 'a' },
|
|
1165
1179
|
{ text: ' ', key: null },
|
|
1166
1180
|
{ text: globalBenchmarkLabel, key: 'u' },
|
|
1181
|
+
{ text: probeLabel ? ' ' : '', key: null },
|
|
1182
|
+
{ text: probeLabel, key: null },
|
|
1167
1183
|
{ text: ' ', key: null },
|
|
1168
1184
|
{ text: releaseLabel, key: null },
|
|
1169
1185
|
]
|
package/src/tui/theme.js
CHANGED
|
@@ -188,6 +188,11 @@ const PROVIDER_PALETTES = {
|
|
|
188
188
|
zai: [150, 208, 255],
|
|
189
189
|
iflow: [211, 229, 101],
|
|
190
190
|
'opencode-zen': [185, 146, 255],
|
|
191
|
+
kilo: [120, 255, 190],
|
|
192
|
+
llm7: [180, 255, 140],
|
|
193
|
+
routeway: [130, 210, 255],
|
|
194
|
+
novita: [255, 185, 120],
|
|
195
|
+
'ollama-cloud': [230, 230, 230],
|
|
191
196
|
},
|
|
192
197
|
light: {
|
|
193
198
|
nvidia: [0, 126, 73],
|
|
@@ -213,6 +218,11 @@ const PROVIDER_PALETTES = {
|
|
|
213
218
|
zai: [0, 104, 171],
|
|
214
219
|
iflow: [107, 130, 0],
|
|
215
220
|
'opencode-zen': [108, 58, 183],
|
|
221
|
+
kilo: [0, 130, 82],
|
|
222
|
+
llm7: [73, 130, 0],
|
|
223
|
+
routeway: [0, 105, 180],
|
|
224
|
+
novita: [173, 84, 0],
|
|
225
|
+
'ollama-cloud': [88, 88, 88],
|
|
216
226
|
},
|
|
217
227
|
}
|
|
218
228
|
|
package/src/tui/tui-filters.js
CHANGED
|
@@ -58,7 +58,7 @@ export function createTuiFilters(state, { sources, getApiKey, PROVIDER_METADATA
|
|
|
58
58
|
// 📖 CLI-only tools and Zen models don't need traditional API keys —
|
|
59
59
|
// 📖 they authenticate via their own CLI login flow, so "configured only" should never hide them.
|
|
60
60
|
const providerMeta = PROVIDER_METADATA[r.providerKey]
|
|
61
|
-
const noKeyNeeded = providerMeta?.cliOnly || providerMeta?.zenOnly
|
|
61
|
+
const noKeyNeeded = providerMeta?.cliOnly || providerMeta?.zenOnly || providerMeta?.noKeyNeeded
|
|
62
62
|
// 📖 E toggles "Show only configured & working models":
|
|
63
63
|
// 📖 hide models where provider has no key, or where the health status is noauth/auth_error (but keep timeout and 429)
|
|
64
64
|
const badHealth = r.status === 'noauth' || r.status === 'auth_error'
|
package/src/tui/tui-state.js
CHANGED
|
@@ -280,6 +280,12 @@ export function createTuiState({
|
|
|
280
280
|
globalBenchmarkTotal: 0,
|
|
281
281
|
globalBenchmarkCompleted: 0,
|
|
282
282
|
|
|
283
|
+
// 📖 404 Probe state (Ctrl+Shift+P)
|
|
284
|
+
probeRunning: false,
|
|
285
|
+
probeTotal: 0,
|
|
286
|
+
probeCompleted: 0,
|
|
287
|
+
probeHiddenCount: 0,
|
|
288
|
+
|
|
283
289
|
// 📖 Header click flash animation: briefly highlights the clicked column header
|
|
284
290
|
// 📖 with an inverse/bright style for ~250ms (3 frames at 12 FPS).
|
|
285
291
|
headerFlashColumn: null, // 📖 Column name being flashed (null = no flash active)
|