free-coding-models 0.5.81 → 0.5.84
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 +25 -8
- package/bin/free-coding-models.js +20 -0
- package/changelog/v0.5.82.md +23 -0
- package/changelog/v0.5.83.md +22 -0
- package/changelog/v0.5.84.md +32 -0
- package/package.json +2 -2
- package/sources.js +167 -106
- package/src/core/benchmark.js +1 -1
- package/src/core/config.js +5 -0
- package/src/core/endpoint-installer.js +7 -2
- package/src/core/kilo.js +7 -0
- package/src/core/model-family.js +176 -0
- package/src/core/models-dev-index.js +1 -0
- package/src/core/opencode.js +14 -0
- package/src/core/ping.js +14 -4
- package/src/core/provider-key-tester.js +2 -1
- package/src/core/provider-metadata.js +11 -2
- package/src/core/quota-capabilities.js +1 -0
- package/src/core/router-daemon.js +47 -6
- package/src/core/router-dashboard.js +6 -1
- package/src/core/security.js +132 -65
- package/src/core/sync-set.js +1 -1
- package/src/core/tool-launchers.js +15 -5
- package/src/core/utils.js +88 -1
- package/src/data/benchmarks.json +6072 -216
- package/src/tui/app.js +7 -5
- package/src/tui/cli-help.js +4 -1
- package/src/tui/command-palette.js +3 -2
- package/src/tui/key-handler.js +172 -61
- package/src/tui/overlays.js +135 -94
- package/src/tui/render-helpers.js +42 -3
- package/src/tui/render-table.js +74 -4
- package/src/tui/theme.js +2 -0
- package/src/tui/tui-state.js +15 -1
- package/web/dist/assets/index-zdQvAW7c.js +40 -0
- package/web/dist/index.html +1 -1
- package/web/src/components/router/RouterView.jsx +38 -1
- package/web/dist/assets/index-Bb5I0K1-.js +0 -40
package/src/tui/app.js
CHANGED
|
@@ -143,7 +143,6 @@ import {
|
|
|
143
143
|
isProviderQuotaPaused,
|
|
144
144
|
listPausedProviders,
|
|
145
145
|
} from '../core/provider-cooldown.js'
|
|
146
|
-
import { checkConfigSecurity } from '../core/security.js'
|
|
147
146
|
import { buildCliHelpText } from './cli-help.js'
|
|
148
147
|
import { detectActiveTheme, THEME_BG_RGB, getTheme, patchThemeBg } from './theme.js'
|
|
149
148
|
|
|
@@ -271,10 +270,9 @@ export async function runApp(cliArgs, config, startupOptions = {}) {
|
|
|
271
270
|
detectActiveTheme(config.settings?.theme || 'auto')
|
|
272
271
|
|
|
273
272
|
// 📖 Check config file security — warn and offer auto-fix if permissions are too open
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
}
|
|
273
|
+
// 📖 (issue #173): this moved to bin/free-coding-models.js BEFORE runApp so the
|
|
274
|
+
// 📖 warning + prompt are visible and resolved before the alternate screen / raw
|
|
275
|
+
// 📖 mode take over. Do not call checkConfigSecurity() here again.
|
|
278
276
|
|
|
279
277
|
// 📖 Apply CLI overrides for settings
|
|
280
278
|
if (cliArgs.sortColumn) config.settings.sortColumn = cliArgs.sortColumn
|
|
@@ -1045,6 +1043,10 @@ export async function runApp(cliArgs, config, startupOptions = {}) {
|
|
|
1045
1043
|
probeTotal: state.probeTotal,
|
|
1046
1044
|
probeCompleted: state.probeCompleted,
|
|
1047
1045
|
probeHiddenCount: state.probeHiddenCount,
|
|
1046
|
+
// 📖 Space-expanded row (issue #168): "providerKey/modelId" or null
|
|
1047
|
+
expandedRowKey: state.expandedRowKey ?? null,
|
|
1048
|
+
// 📖 Non-fatal action error chip (issue #168): only shown while fresh
|
|
1049
|
+
actionErrorMsg: (state.actionErrorMsgUntil || 0) > Date.now() ? state.actionErrorMsg : null,
|
|
1048
1050
|
// 📖 Probe-cache (t1): footer chip stats
|
|
1049
1051
|
probeCacheHits: state.probeCacheHits || 0,
|
|
1050
1052
|
probeCacheMisses: state.probeCacheMisses || 0,
|
package/src/tui/cli-help.js
CHANGED
|
@@ -49,6 +49,7 @@ const CONFIG_FLAGS = [
|
|
|
49
49
|
{ flag: '--daemon-stop', description: 'Gracefully stop the FCM Router daemon' },
|
|
50
50
|
{ flag: '--sync-set [name]', description: 'Auto-discover and live-probe models into a router set' },
|
|
51
51
|
{ flag: '--config-dir <dir>', description: 'Store config.json + backups/ in <dir> (e.g. --config-dir ~/.config/free-coding-models for the XDG layout)' },
|
|
52
|
+
{ flag: '--fix-permissions, --yes, -y', description: 'Auto-fix insecure config file permissions (chmod 600) without prompting; safe for scripts and CI' },
|
|
52
53
|
{ flag: '--no-telemetry', description: 'Disable anonymous telemetry for this run' },
|
|
53
54
|
{ flag: '--help, -h', description: 'Print this help and exit' },
|
|
54
55
|
]
|
|
@@ -85,7 +86,9 @@ export function buildHowTheRouterWorksLines({ chalk = null, indent = '' } = {})
|
|
|
85
86
|
lines.push(body('Point any OpenAI-compatible client at http://localhost:19280/v1'))
|
|
86
87
|
lines.push(body('with model: "fcm". The daemon picks the healthiest model in'))
|
|
87
88
|
lines.push(body('your active set and forwards the request — with automatic'))
|
|
88
|
-
lines.push(body('failover if the first model 429s or 5xxs.'))
|
|
89
|
+
lines.push(body('failover if the first model 429s or 5xxs. Failover prefers the'))
|
|
90
|
+
lines.push(body('same model family on another provider (toggled per set as'))
|
|
91
|
+
lines.push(body('Family failover on the Router Dashboard) before switching family.'))
|
|
89
92
|
lines.push('')
|
|
90
93
|
|
|
91
94
|
lines.push(header('2. The pre-prompt (system message)'))
|
|
@@ -45,7 +45,7 @@ const TOOL_MODE_DESCRIPTIONS = {
|
|
|
45
45
|
hermes: 'Launch Hermes Agent with the selected model.',
|
|
46
46
|
'continue': 'Launch Continue CLI with the selected model.',
|
|
47
47
|
cline: 'Launch Cline CLI with the selected model.',
|
|
48
|
-
caveman: 'Caveman Code
|
|
48
|
+
caveman: 'Caveman Code - token-efficient coding agent (launch with Caveman tool only).',
|
|
49
49
|
jcode: 'Launch jcode coding agent with the selected model.',
|
|
50
50
|
}
|
|
51
51
|
|
|
@@ -201,7 +201,8 @@ const BASE_COMMAND_TREE = [
|
|
|
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
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-
|
|
204
|
+
{ id: 'action-probe-failed', label: 'Re-probe Failed Models', shortcut: 'Shift+P', icon: '\u{1F50D}', description: 'Re-test only rows showing errors (auth fail / 429 / 404 / timeout) - not the whole list.', keywords: ['probe', 'failed', 'retry', '429', 'auth', 'error', 'broken', 'rescan', 'health'] },
|
|
205
|
+
{ id: 'action-probe-404', label: 'Probe All Models (404)', 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', 'all'] },
|
|
205
206
|
{ id: 'action-toggle-auto-hide-broken', label: 'Toggle Auto-hide Broken Models', icon: '\u{1F6A9}', description: 'Enable/disable auto-hiding of 404/410 models. Unhides all when disabled.', keywords: ['auto', 'hide', 'broken', '404', 'models', 'toggle', 'probe'] },
|
|
206
207
|
],
|
|
207
208
|
},
|
package/src/tui/key-handler.js
CHANGED
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
handlePlaygroundKeypress,
|
|
52
52
|
} from '../core/playground.js'
|
|
53
53
|
import { benchmarkModel } from '../core/benchmark.js'
|
|
54
|
+
import { selectProbeFailedRows } from '../core/utils.js'
|
|
54
55
|
import { isPackageDevMode } from '../core/updater.js'
|
|
55
56
|
import {
|
|
56
57
|
runProviderKeyTest,
|
|
@@ -171,6 +172,11 @@ export function createKeyHandler(ctx) {
|
|
|
171
172
|
|
|
172
173
|
let userSelected = null
|
|
173
174
|
|
|
175
|
+
// 📖 Monotonic token for 404-probe runs (issue #168). Each probe start bumps it
|
|
176
|
+
// 📖 so stale reset timers from a previous run can detect they no longer own
|
|
177
|
+
// 📖 the probe counters and skip their reset instead of clobbering live values.
|
|
178
|
+
let probeRunSeq = 0
|
|
179
|
+
|
|
174
180
|
function resetToolInstallPrompt() {
|
|
175
181
|
state.toolInstallPromptOpen = false
|
|
176
182
|
state.toolInstallPromptCursor = 0
|
|
@@ -967,94 +973,143 @@ export function createKeyHandler(ctx) {
|
|
|
967
973
|
return Promise.all(workers).then(() => results)
|
|
968
974
|
}
|
|
969
975
|
|
|
970
|
-
// 📖
|
|
976
|
+
// 📖 setActionNotice: Surface a non-fatal error/info message as a footer chip.
|
|
977
|
+
// 📖 Part of the issue #168 "kicked out" fix: palette commands and the 404 probe
|
|
978
|
+
// 📖 used to run fire-and-forget, so any rejection became an unhandled promise
|
|
979
|
+
// 📖 rejection and Node terminated the whole TUI process. Failures now land here
|
|
980
|
+
// 📖 and the app keeps running. Auto-expires after `ms` milliseconds.
|
|
981
|
+
function setActionNotice(st, msg, ms = 8000) {
|
|
982
|
+
st.actionErrorMsg = String(msg).slice(0, 120)
|
|
983
|
+
st.actionErrorMsgUntil = Date.now() + ms
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// 📖 runBrokenModelProbe: Probe models for 404/broken endpoints.
|
|
971
987
|
// 📖 Sends a real chat-completion request to each model with an API key.
|
|
972
988
|
// 📖 Models returning 404 or 410 are auto-hidden when setting is enabled.
|
|
973
989
|
// 📖 Results flash live in the TUI table — models flip status in real time.
|
|
974
|
-
|
|
990
|
+
// 📖 opts.failedOnly (Shift+P, issue #168): probe ONLY rows currently showing
|
|
991
|
+
// 📖 errors (auth fail / 429 / 404 / timeout) so the user can retry 1-2 flaky
|
|
992
|
+
// 📖 providers without probing (and burning quota on) the whole list.
|
|
993
|
+
async function runBrokenModelProbe(state, { failedOnly = false } = {}) {
|
|
975
994
|
if (state.probeRunning) return
|
|
976
995
|
|
|
977
996
|
// 📖 Only probe models where the user has an API key configured
|
|
978
|
-
const
|
|
997
|
+
const candidates = failedOnly ? selectProbeFailedRows(state.results) : state.results
|
|
998
|
+
const probeable = candidates.filter(r => {
|
|
979
999
|
const apiKey = getApiKey(state.config, r.providerKey)
|
|
980
1000
|
return !!apiKey
|
|
981
1001
|
})
|
|
982
1002
|
|
|
983
|
-
if (probeable.length === 0)
|
|
1003
|
+
if (probeable.length === 0) {
|
|
1004
|
+
// 📖 Tell the user there was nothing to re-probe instead of failing silently.
|
|
1005
|
+
if (failedOnly) setActionNotice(state, 'No failed rows to re-probe - everything looks healthy')
|
|
1006
|
+
return
|
|
1007
|
+
}
|
|
984
1008
|
|
|
985
1009
|
state.probeRunning = true
|
|
986
1010
|
state.probeTotal = probeable.length
|
|
987
1011
|
state.probeCompleted = 0
|
|
988
1012
|
state.probeHiddenCount = 0
|
|
1013
|
+
// 📖 Run token (issue #168): lets a stale reset timer from an earlier probe
|
|
1014
|
+
// 📖 detect that a NEWER probe owns the counters and leave them alone.
|
|
1015
|
+
const runToken = ++probeRunSeq
|
|
989
1016
|
|
|
990
1017
|
const autoHide = state.config.settings?.autoHideBrokenModels !== false
|
|
991
1018
|
const HTTP_CODES_404 = new Set([404, '404'])
|
|
992
1019
|
const HTTP_CODES_GONE = new Set([410, '410'])
|
|
993
1020
|
|
|
994
|
-
|
|
995
|
-
const
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1021
|
+
try {
|
|
1022
|
+
const tasks = probeable.map(r => async () => {
|
|
1023
|
+
const apiKey = getApiKey(state.config, r.providerKey) ?? null
|
|
1024
|
+
const providerUrl = sources[r.providerKey]?.url ?? null
|
|
1025
|
+
if (!apiKey || !providerUrl) {
|
|
1026
|
+
state.probeCompleted++
|
|
1027
|
+
return { model: r, ok: false, reason: 'no_key' }
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// 📖 Single-increment rule (issue #168 kick-out fix): the old shape
|
|
1031
|
+
// 📖 incremented probeCompleted after the ping AND again in the catch
|
|
1032
|
+
// 📖 whenever a status update threw in between, so completed could
|
|
1033
|
+
// 📖 exceed total. The footer progress bar then rendered
|
|
1034
|
+
// 📖 '░'.repeat(negative) → RangeError → render loop crashed → the TUI
|
|
1035
|
+
// 📖 process exited ("kicked me out"). Now the ping is settled in its
|
|
1036
|
+
// 📖 own try/catch and the counter increments EXACTLY once per task,
|
|
1037
|
+
// 📖 after all fallible work that could double-fire it.
|
|
1038
|
+
let code = null
|
|
1039
|
+
let probeError = null
|
|
1040
|
+
try {
|
|
1041
|
+
const res = await ping(apiKey, r.modelId, r.providerKey, providerUrl)
|
|
1042
|
+
code = res.code
|
|
1043
|
+
} catch (err) {
|
|
1044
|
+
probeError = err?.message
|
|
1045
|
+
}
|
|
1001
1046
|
|
|
1002
|
-
try {
|
|
1003
|
-
const { code } = await ping(apiKey, r.modelId, r.providerKey, providerUrl)
|
|
1004
1047
|
state.probeCompleted++
|
|
1005
1048
|
|
|
1049
|
+
if (probeError) {
|
|
1050
|
+
return { model: r, ok: false, reason: 'error', error: probeError }
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1006
1053
|
if (HTTP_CODES_404.has(code) || HTTP_CODES_GONE.has(code)) {
|
|
1007
|
-
// 📖 Model is broken (404/410)
|
|
1054
|
+
// 📖 Model is broken (404/410) - mark it
|
|
1008
1055
|
r.status = 'down'
|
|
1009
1056
|
r.httpCode = String(code)
|
|
1010
1057
|
|
|
1011
1058
|
if (autoHide) {
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1059
|
+
try {
|
|
1060
|
+
const modelKey = `${r.providerKey}/${r.modelId}`
|
|
1061
|
+
if (!state.config.hiddenModels) state.config.hiddenModels = new Set()
|
|
1062
|
+
state.config.hiddenModels.add(modelKey)
|
|
1063
|
+
r.hidden = true
|
|
1064
|
+
state.probeHiddenCount++
|
|
1065
|
+
} catch { /* 📖 auto-hide must never kill the probe */ }
|
|
1017
1066
|
}
|
|
1018
1067
|
|
|
1019
1068
|
return { model: r, ok: false, code, reason: 'broken' }
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
if
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
if (code === '200') {
|
|
1072
|
+
// 📖 Model is alive - unhide if it was previously hidden by probe
|
|
1073
|
+
try {
|
|
1074
|
+
const modelKey = `${r.providerKey}/${r.modelId}`
|
|
1075
|
+
if (state.config.hiddenModels?.has(modelKey)) {
|
|
1076
|
+
state.config.hiddenModels.delete(modelKey)
|
|
1077
|
+
r.hidden = false
|
|
1078
|
+
}
|
|
1079
|
+
} catch { /* 📖 unhide must never kill the probe */ }
|
|
1027
1080
|
r.status = 'up'
|
|
1028
1081
|
return { model: r, ok: true, code }
|
|
1029
|
-
} else {
|
|
1030
|
-
// 📖 Other codes (401, 429, etc.) — leave status unchanged, don't hide
|
|
1031
|
-
return { model: r, ok: false, code, reason: 'other' }
|
|
1032
1082
|
}
|
|
1033
|
-
} catch (err) {
|
|
1034
|
-
state.probeCompleted++
|
|
1035
|
-
return { model: r, ok: false, reason: 'error', error: err?.message }
|
|
1036
|
-
}
|
|
1037
|
-
})
|
|
1038
1083
|
|
|
1039
|
-
|
|
1084
|
+
// 📖 Other codes (401, 429, etc.) - leave status unchanged, don't hide
|
|
1085
|
+
return { model: r, ok: false, code, reason: 'other' }
|
|
1086
|
+
})
|
|
1040
1087
|
|
|
1041
|
-
|
|
1042
|
-
if (autoHide && state.probeHiddenCount > 0) {
|
|
1043
|
-
saveConfig(state.config)
|
|
1044
|
-
}
|
|
1088
|
+
await runWithConcurrency(tasks, 5)
|
|
1045
1089
|
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
if (!state.probeRunning) {
|
|
1050
|
-
state.probeTotal = 0
|
|
1051
|
-
state.probeCompleted = 0
|
|
1052
|
-
// 📖 Don't reset probeHiddenCount immediately — user needs to see how many were hidden
|
|
1053
|
-
setTimeout(() => {
|
|
1054
|
-
state.probeHiddenCount = 0
|
|
1055
|
-
}, 5000)
|
|
1090
|
+
// 📖 Persist hidden models set to config
|
|
1091
|
+
if (autoHide && state.probeHiddenCount > 0) {
|
|
1092
|
+
saveConfig(state.config)
|
|
1056
1093
|
}
|
|
1057
|
-
}
|
|
1094
|
+
} catch (err) {
|
|
1095
|
+
// 📖 Never let a probe crash the TUI (issue #168): surface and keep running.
|
|
1096
|
+
setActionNotice(state, `Probe failed: ${err?.message || 'unknown error'}`)
|
|
1097
|
+
} finally {
|
|
1098
|
+
state.probeRunning = false
|
|
1099
|
+
// 📖 Keep totals visible for a few seconds so user can see results.
|
|
1100
|
+
// 📖 The run token makes a STALE timer from a previous probe a no-op if a
|
|
1101
|
+
// 📖 newer probe has started meanwhile (it owns the counters now).
|
|
1102
|
+
setTimeout(() => {
|
|
1103
|
+
if (runToken === probeRunSeq && !state.probeRunning) {
|
|
1104
|
+
state.probeTotal = 0
|
|
1105
|
+
state.probeCompleted = 0
|
|
1106
|
+
// 📖 Don't reset probeHiddenCount immediately - user needs to see how many were hidden
|
|
1107
|
+
setTimeout(() => {
|
|
1108
|
+
if (runToken === probeRunSeq) state.probeHiddenCount = 0
|
|
1109
|
+
}, 5000)
|
|
1110
|
+
}
|
|
1111
|
+
}, 3000)
|
|
1112
|
+
}
|
|
1058
1113
|
}
|
|
1059
1114
|
|
|
1060
1115
|
// 📖 runGlobalBenchmark: Benchmark all visible models with up to 5 concurrent requests.
|
|
@@ -1293,6 +1348,22 @@ export function createKeyHandler(ctx) {
|
|
|
1293
1348
|
state.commandPaletteResults = []
|
|
1294
1349
|
}
|
|
1295
1350
|
|
|
1351
|
+
// 📖 runPaletteCommandSafe: Execute a palette entry without ever letting a
|
|
1352
|
+
// 📖 rejection escape. ROOT CAUSE of the issue #168 "kicked out" bug: the
|
|
1353
|
+
// 📖 palette used to call executeCommandPaletteEntry() fire-and-forget, so a
|
|
1354
|
+
// 📖 rejected command promise (e.g. the async 404 probe hitting a save or
|
|
1355
|
+
// 📖 network edge case) became an unhandled promise rejection and Node's
|
|
1356
|
+
// 📖 default behavior (since v15) terminated the whole TUI process, dumping
|
|
1357
|
+
// 📖 the user out of the alternate screen. Failures now surface as a footer
|
|
1358
|
+
// 📖 notice and the TUI stays alive.
|
|
1359
|
+
async function runPaletteCommandSafe(entry) {
|
|
1360
|
+
try {
|
|
1361
|
+
await executeCommandPaletteEntry(entry)
|
|
1362
|
+
} catch (err) {
|
|
1363
|
+
setActionNotice(state, `Command failed: ${err?.message || err}`)
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1296
1367
|
function executeCommandPaletteEntry(entry) {
|
|
1297
1368
|
if (!entry?.id) return
|
|
1298
1369
|
|
|
@@ -1413,6 +1484,7 @@ export function createKeyHandler(ctx) {
|
|
|
1413
1484
|
case 'action-toggle-favorite': return toggleFavoriteOnSelectedRow()
|
|
1414
1485
|
case 'action-toggle-favorite-mode': return toggleFavoritesDisplayMode()
|
|
1415
1486
|
case 'action-reset-view': return resetViewSettings()
|
|
1487
|
+
case 'action-probe-failed': return runBrokenModelProbe(state, { failedOnly: true })
|
|
1416
1488
|
case 'action-probe-404': return runBrokenModelProbe(state)
|
|
1417
1489
|
case 'action-toggle-auto-hide-broken': return toggleAutoHideBrokenModels()
|
|
1418
1490
|
default:
|
|
@@ -1429,7 +1501,10 @@ export function createKeyHandler(ctx) {
|
|
|
1429
1501
|
if ((key.ctrl && key.name === 'c') || str === '\x03') { exit(0); return }
|
|
1430
1502
|
|
|
1431
1503
|
// 📖 Ctrl+P toggles the command palette from the main table only.
|
|
1432
|
-
|
|
1504
|
+
// 📖 The !key.shift guard matters (issue #168): many terminals report
|
|
1505
|
+
// 📖 Ctrl+Shift+P as ctrl+p with shift set (or as plain ctrl+p), and without
|
|
1506
|
+
// 📖 the guard the palette swallowed the 404 probe shortcut completely.
|
|
1507
|
+
if (key.ctrl && key.name === 'p' && !key.shift) {
|
|
1433
1508
|
if (state.commandPaletteOpen) {
|
|
1434
1509
|
closeCommandPalette()
|
|
1435
1510
|
return
|
|
@@ -1448,14 +1523,10 @@ export function createKeyHandler(ctx) {
|
|
|
1448
1523
|
return
|
|
1449
1524
|
}
|
|
1450
1525
|
|
|
1451
|
-
// 📖
|
|
1452
|
-
// 📖
|
|
1453
|
-
// 📖
|
|
1454
|
-
// 📖
|
|
1455
|
-
if (key.ctrl && key.shift && key.name === 'p') {
|
|
1456
|
-
await runBrokenModelProbe(state)
|
|
1457
|
-
return
|
|
1458
|
-
}
|
|
1526
|
+
// 📖 Note: the 404 probe keybindings (Shift+P = failed rows only, Ctrl+Shift+P
|
|
1527
|
+
// 📖 = all models) live in the main-table section below, AFTER the overlay
|
|
1528
|
+
// 📖 blocks, so overlays like the command palette swallow them instead of
|
|
1529
|
+
// 📖 triggering a probe behind a frozen screen (issue #168).
|
|
1459
1530
|
|
|
1460
1531
|
// 📖 Command palette captures the keyboard while active.
|
|
1461
1532
|
if (state.commandPaletteOpen) {
|
|
@@ -1493,7 +1564,9 @@ export function createKeyHandler(ctx) {
|
|
|
1493
1564
|
refreshCommandPaletteResults()
|
|
1494
1565
|
} else if (selected?.type === 'command') {
|
|
1495
1566
|
closeCommandPalette()
|
|
1496
|
-
|
|
1567
|
+
// 📖 Awaited via safe wrapper: a rejected command must never become an
|
|
1568
|
+
// 📖 unhandled promise rejection (issue #168 "kicked out" bug).
|
|
1569
|
+
runPaletteCommandSafe(selected)
|
|
1497
1570
|
}
|
|
1498
1571
|
return
|
|
1499
1572
|
}
|
|
@@ -1531,7 +1604,9 @@ export function createKeyHandler(ctx) {
|
|
|
1531
1604
|
refreshCommandPaletteResults()
|
|
1532
1605
|
} else {
|
|
1533
1606
|
closeCommandPalette()
|
|
1534
|
-
|
|
1607
|
+
// 📖 Awaited via safe wrapper: a rejected command must never become an
|
|
1608
|
+
// 📖 unhandled promise rejection (issue #168 "kicked out" bug).
|
|
1609
|
+
runPaletteCommandSafe(selected)
|
|
1535
1610
|
}
|
|
1536
1611
|
return
|
|
1537
1612
|
}
|
|
@@ -2829,6 +2904,37 @@ export function createKeyHandler(ctx) {
|
|
|
2829
2904
|
return // 📖 Swallow all other keys while settings is open
|
|
2830
2905
|
}
|
|
2831
2906
|
|
|
2907
|
+
// 📖 Space: toggle a 2-line detail card under the selected row (issue #168).
|
|
2908
|
+
// 📖 When columns are tight, provider and model specifics get truncated; the
|
|
2909
|
+
// 📖 detail card gives the selected row room to express them. Pressing Space
|
|
2910
|
+
// 📖 again (or moving the cursor) collapses it. Cursor moves clear the key in
|
|
2911
|
+
// 📖 the up/down handlers below, so the card always follows the highlight.
|
|
2912
|
+
if (key.name === 'space' && !key.ctrl && !key.meta && !key.shift) {
|
|
2913
|
+
const selected = state.visibleSorted[state.cursor]
|
|
2914
|
+
if (!selected) return
|
|
2915
|
+
const rowKey = `${selected.providerKey}/${selected.modelId}`
|
|
2916
|
+
state.expandedRowKey = state.expandedRowKey === rowKey ? null : rowKey
|
|
2917
|
+
return
|
|
2918
|
+
}
|
|
2919
|
+
|
|
2920
|
+
// 📖 Shift+P: Re-probe ONLY the rows currently showing errors (auth fail,
|
|
2921
|
+
// 📖 429, 404, timeout). Issue #168: users with 1-2 flaky rows want a cheap
|
|
2922
|
+
// 📖 rescan without probing (and burning quota on) the whole list.
|
|
2923
|
+
// 📖 Shift+P is the requested replacement for Ctrl+Shift+P, which most
|
|
2924
|
+
// 📖 terminals cannot send as a distinct key.
|
|
2925
|
+
if (key.name === 'p' && key.shift && !key.ctrl && !key.meta) {
|
|
2926
|
+
await runBrokenModelProbe(state, { failedOnly: true })
|
|
2927
|
+
return
|
|
2928
|
+
}
|
|
2929
|
+
|
|
2930
|
+
// 📖 Ctrl+Shift+P: Probe ALL configured models for 404/broken endpoints
|
|
2931
|
+
// 📖 (same as the "Probe all models" palette entry). Kept for terminals
|
|
2932
|
+
// 📖 that can report Ctrl+Shift combos; Shift+P covers everyone else.
|
|
2933
|
+
if (key.ctrl && key.shift && key.name === 'p') {
|
|
2934
|
+
await runBrokenModelProbe(state)
|
|
2935
|
+
return
|
|
2936
|
+
}
|
|
2937
|
+
|
|
2832
2938
|
// 📖 P key: open settings screen
|
|
2833
2939
|
if (key.name === 'p' && !key.shift && !key.ctrl && !key.meta) {
|
|
2834
2940
|
openSettingsOverlay()
|
|
@@ -3066,6 +3172,9 @@ export function createKeyHandler(ctx) {
|
|
|
3066
3172
|
const count = state.visibleSorted.length
|
|
3067
3173
|
if (count === 0) return
|
|
3068
3174
|
state.cursor = state.cursor > 0 ? state.cursor - 1 : count - 1
|
|
3175
|
+
// 📖 Moving the cursor collapses the Space detail card (issue #168) so it
|
|
3176
|
+
// 📖 always tracks the highlighted row instead of lingering on an old one.
|
|
3177
|
+
state.expandedRowKey = null
|
|
3069
3178
|
adjustScrollOffset(state)
|
|
3070
3179
|
return
|
|
3071
3180
|
}
|
|
@@ -3075,6 +3184,8 @@ export function createKeyHandler(ctx) {
|
|
|
3075
3184
|
const count = state.visibleSorted.length
|
|
3076
3185
|
if (count === 0) return
|
|
3077
3186
|
state.cursor = state.cursor < count - 1 ? state.cursor + 1 : 0
|
|
3187
|
+
// 📖 Moving the cursor collapses the Space detail card (see up handler).
|
|
3188
|
+
state.expandedRowKey = null
|
|
3078
3189
|
adjustScrollOffset(state)
|
|
3079
3190
|
return
|
|
3080
3191
|
}
|