free-coding-models 0.3.22 → 0.3.24
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/CHANGELOG.md +48 -0
- package/README.md +76 -19
- package/package.json +1 -1
- package/sources.js +60 -0
- package/src/app.js +43 -17
- package/src/command-palette.js +103 -6
- package/src/config.js +4 -2
- package/src/endpoint-installer.js +3 -2
- package/src/key-handler.js +311 -29
- package/src/overlays.js +119 -8
- package/src/provider-metadata.js +25 -0
- package/src/render-helpers.js +15 -2
- package/src/render-table.js +81 -7
- package/src/theme.js +6 -0
- package/src/tool-bootstrap.js +22 -0
- package/src/tool-launchers.js +93 -2
- package/src/tool-metadata.js +94 -11
- package/src/utils.js +5 -1
package/src/config.js
CHANGED
|
@@ -209,6 +209,7 @@ function normalizeSettingsSection(settings) {
|
|
|
209
209
|
return {
|
|
210
210
|
...safeSettings,
|
|
211
211
|
hideUnconfiguredModels: typeof safeSettings.hideUnconfiguredModels === 'boolean' ? safeSettings.hideUnconfiguredModels : true,
|
|
212
|
+
favoritesPinnedAndSticky: typeof safeSettings.favoritesPinnedAndSticky === 'boolean' ? safeSettings.favoritesPinnedAndSticky : false,
|
|
212
213
|
theme: ['dark', 'light', 'auto'].includes(safeSettings.theme) ? safeSettings.theme : 'auto',
|
|
213
214
|
}
|
|
214
215
|
}
|
|
@@ -831,7 +832,7 @@ export function isProviderEnabled(config, providerKey) {
|
|
|
831
832
|
/**
|
|
832
833
|
* 📖 _emptyProfileSettings: Default TUI settings.
|
|
833
834
|
*
|
|
834
|
-
* @returns {{ tierFilter: string|null, sortColumn: string, sortAsc: boolean, pingInterval: number, hideUnconfiguredModels: boolean, preferredToolMode: string }}
|
|
835
|
+
* @returns {{ tierFilter: string|null, sortColumn: string, sortAsc: boolean, pingInterval: number, hideUnconfiguredModels: boolean, favoritesPinnedAndSticky: boolean, preferredToolMode: string }}
|
|
835
836
|
*/
|
|
836
837
|
export function _emptyProfileSettings() {
|
|
837
838
|
return {
|
|
@@ -840,6 +841,7 @@ export function _emptyProfileSettings() {
|
|
|
840
841
|
sortAsc: true, // 📖 true = ascending (fastest first for latency)
|
|
841
842
|
pingInterval: 10000, // 📖 default ms between pings in the steady "normal" mode
|
|
842
843
|
hideUnconfiguredModels: true, // 📖 true = default to providers that are actually configured
|
|
844
|
+
favoritesPinnedAndSticky: false, // 📖 default mode keeps favorites as normal starred rows; press Y to pin+stick them.
|
|
843
845
|
preferredToolMode: 'opencode', // 📖 remember the last Z-selected launcher across app restarts
|
|
844
846
|
theme: 'auto', // 📖 'auto' follows the terminal/OS theme, override with 'dark' or 'light' if needed
|
|
845
847
|
}
|
|
@@ -848,7 +850,7 @@ export function _emptyProfileSettings() {
|
|
|
848
850
|
/**
|
|
849
851
|
* 📖 normalizeEndpointInstalls keeps the endpoint-install tracking list safe to replay.
|
|
850
852
|
*
|
|
851
|
-
* 📖 Each entry represents one managed catalog install performed through
|
|
853
|
+
* 📖 Each entry represents one managed catalog install performed through Install Endpoints:
|
|
852
854
|
* - `providerKey`: FCM provider identifier (`nvidia`, `groq`, ...)
|
|
853
855
|
* - `toolMode`: canonical tool id (`opencode`, `openclaw`, `crush`, `goose`)
|
|
854
856
|
* - `scope`: `all` or `selected`
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* @description Install and refresh FCM-managed provider catalogs inside external tool configs.
|
|
4
4
|
*
|
|
5
5
|
* @details
|
|
6
|
-
* 📖 This module powers the
|
|
6
|
+
* 📖 This module powers the Install Endpoints flow in the TUI.
|
|
7
7
|
* It lets users pick one configured provider, choose a target tool, then install either:
|
|
8
8
|
* - the full provider catalog (`all` models), or
|
|
9
9
|
* - a curated subset of specific models (`selected`)
|
|
@@ -48,7 +48,8 @@ import { getApiKey, saveConfig } from './config.js'
|
|
|
48
48
|
import { ENV_VAR_NAMES, PROVIDER_METADATA } from './provider-metadata.js'
|
|
49
49
|
import { getToolMeta } from './tool-metadata.js'
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
// 📖 CLI-only providers (rovo, gemini) and Zen-only (opencode-zen) cannot be installed into other tools.
|
|
52
|
+
const DIRECT_INSTALL_UNSUPPORTED_PROVIDERS = new Set(['replicate', 'zai', 'rovo', 'gemini', 'opencode-zen'])
|
|
52
53
|
// 📖 Install Endpoints only lists tools whose persisted config shape is actually supported here.
|
|
53
54
|
// 📖 Claude Code, Codex, and Gemini stay out while their dedicated bridges are being rebuilt.
|
|
54
55
|
const INSTALL_TARGET_MODES = ['opencode', 'opencode-desktop', 'openclaw', 'crush', 'goose', 'pi', 'aider', 'qwen', 'openhands', 'amp']
|
package/src/key-handler.js
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
30
|
import { loadChangelog } from './changelog-loader.js'
|
|
31
|
+
import { getToolMeta, isModelCompatibleWithTool, getCompatibleTools, findSimilarCompatibleModels } from './tool-metadata.js'
|
|
31
32
|
import { loadConfig, replaceConfigContents } from './config.js'
|
|
32
33
|
import { cleanupLegacyProxyArtifacts } from './legacy-proxy-cleanup.js'
|
|
33
34
|
import { cycleThemeSetting, detectActiveTheme } from './theme.js'
|
|
@@ -247,9 +248,61 @@ export function createKeyHandler(ctx) {
|
|
|
247
248
|
}
|
|
248
249
|
console.log()
|
|
249
250
|
|
|
250
|
-
// 📖
|
|
251
|
-
// 📖
|
|
252
|
-
|
|
251
|
+
// 📖 CLI-only tool compatibility checks:
|
|
252
|
+
// 📖 Case A: Active tool mode is CLI-only (rovo/gemini) but selected model doesn't belong to it
|
|
253
|
+
// 📖 Case B: Selected model belongs to a CLI-only provider but active mode is something else
|
|
254
|
+
// 📖 Case C: Selected model is from opencode-zen but active mode is not opencode/opencode-desktop
|
|
255
|
+
const activeMeta = getToolMeta(state.mode)
|
|
256
|
+
const isActiveModeCliOnly = activeMeta.cliOnly === true
|
|
257
|
+
const isModelFromCliOnly = selected.providerKey === 'rovo' || selected.providerKey === 'gemini'
|
|
258
|
+
const isModelFromZen = selected.providerKey === 'opencode-zen'
|
|
259
|
+
const modelBelongsToActiveMode = selected.providerKey === state.mode
|
|
260
|
+
|
|
261
|
+
// 📖 Case A: User is in Rovo/Gemini mode but selected a model from a different provider
|
|
262
|
+
if (isActiveModeCliOnly && !modelBelongsToActiveMode) {
|
|
263
|
+
const availableModels = MODELS.filter(m => m[5] === state.mode)
|
|
264
|
+
console.log(chalk.yellow(` ⚠ ${activeMeta.label} can only launch its own models.`))
|
|
265
|
+
console.log(chalk.yellow(` "${selected.label}" is not a ${activeMeta.label} model.`))
|
|
266
|
+
console.log()
|
|
267
|
+
if (availableModels.length > 0) {
|
|
268
|
+
console.log(chalk.cyan(` Available ${activeMeta.label} models:`))
|
|
269
|
+
for (const m of availableModels) {
|
|
270
|
+
console.log(chalk.white(` • ${m[1]} (${m[2]} tier, ${m[3]} SWE, ${m[4]} ctx)`))
|
|
271
|
+
}
|
|
272
|
+
console.log()
|
|
273
|
+
}
|
|
274
|
+
console.log(chalk.dim(` Switch to another tool mode with Z, or select a ${activeMeta.label} model.`))
|
|
275
|
+
console.log()
|
|
276
|
+
process.exit(0)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// 📖 Case B: Selected model is from a CLI-only provider but active mode is different
|
|
280
|
+
if (isModelFromCliOnly && !modelBelongsToActiveMode) {
|
|
281
|
+
const modelMeta = getToolMeta(selected.providerKey)
|
|
282
|
+
console.log(chalk.yellow(` ⚠ ${selected.label} is a ${modelMeta.label}-exclusive model.`))
|
|
283
|
+
console.log(chalk.yellow(` Your current tool is: ${activeMeta.label}`))
|
|
284
|
+
console.log()
|
|
285
|
+
console.log(chalk.cyan(` Switching to ${modelMeta.label} and launching...`))
|
|
286
|
+
setToolMode(selected.providerKey)
|
|
287
|
+
console.log(chalk.green(` ✓ Switched to ${modelMeta.label}`))
|
|
288
|
+
console.log()
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// 📖 Case C: Zen model selected but active mode is not OpenCode CLI / OpenCode Desktop
|
|
292
|
+
// 📖 Auto-switch to OpenCode CLI since Zen models only run on OpenCode
|
|
293
|
+
if (isModelFromZen && state.mode !== 'opencode' && state.mode !== 'opencode-desktop') {
|
|
294
|
+
console.log(chalk.yellow(` ⚠ ${selected.label} is an OpenCode Zen model.`))
|
|
295
|
+
console.log(chalk.yellow(` Zen models only run on OpenCode CLI or OpenCode Desktop.`))
|
|
296
|
+
console.log(chalk.yellow(` Your current tool is: ${activeMeta.label}`))
|
|
297
|
+
console.log()
|
|
298
|
+
console.log(chalk.cyan(` Switching to OpenCode CLI and launching...`))
|
|
299
|
+
setToolMode('opencode')
|
|
300
|
+
console.log(chalk.green(` ✓ Switched to OpenCode CLI`))
|
|
301
|
+
console.log()
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// 📖 OpenClaw, CLI-only tools, and Zen models manage auth differently — skip API key warning for them.
|
|
305
|
+
if (state.mode !== 'openclaw' && !isModelFromCliOnly && !isModelFromZen) {
|
|
253
306
|
const selectedApiKey = getApiKey(state.config, selected.providerKey)
|
|
254
307
|
if (!selectedApiKey) {
|
|
255
308
|
console.log(chalk.yellow(` Warning: No API key configured for ${selected.providerKey}.`))
|
|
@@ -259,6 +312,41 @@ export function createKeyHandler(ctx) {
|
|
|
259
312
|
}
|
|
260
313
|
}
|
|
261
314
|
|
|
315
|
+
// 📖 CLI-only tool auto-install check — verify the CLI binary is available before launch.
|
|
316
|
+
const toolModeForProvider = selected.providerKey
|
|
317
|
+
if (isModelFromCliOnly && !isToolInstalled(toolModeForProvider)) {
|
|
318
|
+
const installPlan = getToolInstallPlan(toolModeForProvider)
|
|
319
|
+
if (installPlan.supported) {
|
|
320
|
+
console.log()
|
|
321
|
+
console.log(chalk.yellow(` ⚠ ${getToolMeta(toolModeForProvider).label} is not installed.`))
|
|
322
|
+
console.log(chalk.dim(` ${installPlan.summary}`))
|
|
323
|
+
if (installPlan.note) console.log(chalk.dim(` Note: ${installPlan.note}`))
|
|
324
|
+
console.log()
|
|
325
|
+
console.log(chalk.cyan(` 📦 Auto-installing ${getToolMeta(toolModeForProvider).label}...`))
|
|
326
|
+
console.log()
|
|
327
|
+
|
|
328
|
+
const installResult = await installToolWithPlan(installPlan)
|
|
329
|
+
if (!installResult.ok) {
|
|
330
|
+
console.log(chalk.red(` X Tool installation failed with exit code ${installResult.exitCode}.`))
|
|
331
|
+
if (installPlan.docsUrl) console.log(chalk.dim(` Docs: ${installPlan.docsUrl}`))
|
|
332
|
+
console.log()
|
|
333
|
+
process.exit(installResult.exitCode || 1)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// 📖 Verify tool is now installed
|
|
337
|
+
if (!isToolInstalled(toolModeForProvider)) {
|
|
338
|
+
console.log(chalk.yellow(' ⚠ The installer finished, but the tool is still not reachable from this terminal session.'))
|
|
339
|
+
console.log(chalk.dim(' Restart your shell or add the tool bin directory to PATH, then retry the launch.'))
|
|
340
|
+
if (installPlan.docsUrl) console.log(chalk.dim(` Docs: ${installPlan.docsUrl}`))
|
|
341
|
+
console.log()
|
|
342
|
+
process.exit(1)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
console.log(chalk.green(' ✓ Tool installed successfully. Continuing with the selected model...'))
|
|
346
|
+
console.log()
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
262
350
|
let exitCode = 0
|
|
263
351
|
if (state.mode === 'openclaw') {
|
|
264
352
|
exitCode = await startOpenClaw(userSelected, state.config, { launchCli: true })
|
|
@@ -519,7 +607,9 @@ export function createKeyHandler(ctx) {
|
|
|
519
607
|
// 📖 Shared table refresh helper so command-palette and hotkeys keep identical behavior.
|
|
520
608
|
function refreshVisibleSorted({ resetCursor = true } = {}) {
|
|
521
609
|
const visible = state.results.filter(r => !r.hidden)
|
|
522
|
-
state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection
|
|
610
|
+
state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection, {
|
|
611
|
+
pinFavorites: state.favoritesPinnedAndSticky,
|
|
612
|
+
})
|
|
523
613
|
if (resetCursor) {
|
|
524
614
|
state.cursor = 0
|
|
525
615
|
state.scrollOffset = 0
|
|
@@ -600,12 +690,48 @@ export function createKeyHandler(ctx) {
|
|
|
600
690
|
const modeOrder = getToolModeOrder()
|
|
601
691
|
const currentIndex = modeOrder.indexOf(state.mode)
|
|
602
692
|
const nextIndex = (currentIndex + 1) % modeOrder.length
|
|
603
|
-
|
|
693
|
+
setToolMode(modeOrder[nextIndex])
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// 📖 Keep tool-mode changes centralized so keyboard shortcuts and command palette
|
|
697
|
+
// 📖 both persist to config exactly the same way.
|
|
698
|
+
function setToolMode(nextMode) {
|
|
699
|
+
if (!getToolModeOrder().includes(nextMode)) return
|
|
700
|
+
state.mode = nextMode
|
|
604
701
|
if (!state.config.settings || typeof state.config.settings !== 'object') state.config.settings = {}
|
|
605
702
|
state.config.settings.preferredToolMode = state.mode
|
|
606
703
|
saveConfig(state.config)
|
|
607
704
|
}
|
|
608
705
|
|
|
706
|
+
// 📖 Favorites display mode:
|
|
707
|
+
// 📖 - true => favorites stay pinned + always visible (legacy behavior)
|
|
708
|
+
// 📖 - false => favorites are just starred rows and obey normal sort/filter rules
|
|
709
|
+
function setFavoritesDisplayMode(nextPinned, { preserveSelection = true } = {}) {
|
|
710
|
+
const normalizedNextPinned = nextPinned !== false
|
|
711
|
+
if (state.favoritesPinnedAndSticky === normalizedNextPinned) return
|
|
712
|
+
|
|
713
|
+
const selected = preserveSelection ? state.visibleSorted[state.cursor] : null
|
|
714
|
+
const selectedKey = selected ? toFavoriteKey(selected.providerKey, selected.modelId) : null
|
|
715
|
+
|
|
716
|
+
state.favoritesPinnedAndSticky = normalizedNextPinned
|
|
717
|
+
if (!state.config.settings || typeof state.config.settings !== 'object') state.config.settings = {}
|
|
718
|
+
state.config.settings.favoritesPinnedAndSticky = state.favoritesPinnedAndSticky
|
|
719
|
+
saveConfig(state.config)
|
|
720
|
+
|
|
721
|
+
applyTierFilter()
|
|
722
|
+
refreshVisibleSorted({ resetCursor: false })
|
|
723
|
+
|
|
724
|
+
if (selectedKey) {
|
|
725
|
+
const selectedIdx = state.visibleSorted.findIndex((row) => toFavoriteKey(row.providerKey, row.modelId) === selectedKey)
|
|
726
|
+
if (selectedIdx >= 0) state.cursor = selectedIdx
|
|
727
|
+
adjustScrollOffset(state)
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function toggleFavoritesDisplayMode() {
|
|
732
|
+
setFavoritesDisplayMode(!state.favoritesPinnedAndSticky)
|
|
733
|
+
}
|
|
734
|
+
|
|
609
735
|
function resetViewSettings() {
|
|
610
736
|
state.tierFilterMode = 0
|
|
611
737
|
state.originFilterMode = 0
|
|
@@ -631,7 +757,7 @@ export function createKeyHandler(ctx) {
|
|
|
631
757
|
applyTierFilter()
|
|
632
758
|
refreshVisibleSorted({ resetCursor: false })
|
|
633
759
|
|
|
634
|
-
if (wasFavorite) {
|
|
760
|
+
if (wasFavorite && state.favoritesPinnedAndSticky) {
|
|
635
761
|
state.cursor = 0
|
|
636
762
|
state.scrollOffset = 0
|
|
637
763
|
return
|
|
@@ -654,11 +780,28 @@ export function createKeyHandler(ctx) {
|
|
|
654
780
|
}
|
|
655
781
|
|
|
656
782
|
function refreshCommandPaletteResults() {
|
|
783
|
+
const query = (state.commandPaletteQuery || '').trim()
|
|
657
784
|
const tree = buildCommandPaletteTree(state.results || [])
|
|
658
|
-
|
|
659
|
-
|
|
785
|
+
// 📖 Keep collapsed view clean when query is empty, but search across the
|
|
786
|
+
// 📖 full tree when users type so hidden submenu commands still appear.
|
|
787
|
+
let flat
|
|
788
|
+
if (query.length > 0) {
|
|
789
|
+
const expandedIds = new Set()
|
|
790
|
+
const collectExpandedIds = (nodes) => {
|
|
791
|
+
for (const node of nodes || []) {
|
|
792
|
+
if (Array.isArray(node.children) && node.children.length > 0) {
|
|
793
|
+
expandedIds.add(node.id)
|
|
794
|
+
collectExpandedIds(node.children)
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
collectExpandedIds(tree)
|
|
799
|
+
flat = flattenCommandTree(tree, expandedIds)
|
|
800
|
+
} else {
|
|
801
|
+
flat = flattenCommandTree(tree, state.commandPaletteExpandedIds)
|
|
802
|
+
}
|
|
803
|
+
state.commandPaletteResults = filterCommandPaletteEntries(flat, query)
|
|
660
804
|
|
|
661
|
-
const query = (state.commandPaletteQuery || '').trim()
|
|
662
805
|
if (query.length > 0) {
|
|
663
806
|
state.commandPaletteResults.unshift({
|
|
664
807
|
id: 'filter-custom-text-apply',
|
|
@@ -707,6 +850,21 @@ export function createKeyHandler(ctx) {
|
|
|
707
850
|
function executeCommandPaletteEntry(entry) {
|
|
708
851
|
if (!entry?.id) return
|
|
709
852
|
|
|
853
|
+
if (entry.id.startsWith('action-set-ping-') && entry.pingMode) {
|
|
854
|
+
setPingMode(entry.pingMode, 'manual')
|
|
855
|
+
return
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
if (entry.id.startsWith('action-set-tool-') && entry.toolMode) {
|
|
859
|
+
setToolMode(entry.toolMode)
|
|
860
|
+
return
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
if (entry.id.startsWith('action-favorites-mode-') && typeof entry.favoritesPinned === 'boolean') {
|
|
864
|
+
setFavoritesDisplayMode(entry.favoritesPinned)
|
|
865
|
+
return
|
|
866
|
+
}
|
|
867
|
+
|
|
710
868
|
if (entry.id.startsWith('filter-tier-')) {
|
|
711
869
|
setTierFilterFromCommand(entry.tier ?? null)
|
|
712
870
|
return
|
|
@@ -793,6 +951,7 @@ export function createKeyHandler(ctx) {
|
|
|
793
951
|
return
|
|
794
952
|
}
|
|
795
953
|
case 'action-toggle-favorite': return toggleFavoriteOnSelectedRow()
|
|
954
|
+
case 'action-toggle-favorite-mode': return toggleFavoritesDisplayMode()
|
|
796
955
|
case 'action-reset-view': return resetViewSettings()
|
|
797
956
|
default:
|
|
798
957
|
return
|
|
@@ -1129,6 +1288,85 @@ export function createKeyHandler(ctx) {
|
|
|
1129
1288
|
return
|
|
1130
1289
|
}
|
|
1131
1290
|
|
|
1291
|
+
// 📖 Incompatible fallback overlay: ↑↓ navigate across tool + model sections, Enter confirms, Esc cancels.
|
|
1292
|
+
// 📖 Cursor is a flat index: 0..N-1 = compatible tools, N..N+M-1 = similar models.
|
|
1293
|
+
if (state.incompatibleFallbackOpen) {
|
|
1294
|
+
if (key.ctrl && key.name === 'c') { exit(0); return }
|
|
1295
|
+
|
|
1296
|
+
const tools = state.incompatibleFallbackTools || []
|
|
1297
|
+
const similarModels = state.incompatibleFallbackSimilarModels || []
|
|
1298
|
+
const totalItems = tools.length + similarModels.length
|
|
1299
|
+
|
|
1300
|
+
if (key.name === 'escape') {
|
|
1301
|
+
// 📖 Close the overlay and go back to the main table
|
|
1302
|
+
state.incompatibleFallbackOpen = false
|
|
1303
|
+
state.incompatibleFallbackCursor = 0
|
|
1304
|
+
state.incompatibleFallbackScrollOffset = 0
|
|
1305
|
+
state.incompatibleFallbackModel = null
|
|
1306
|
+
state.incompatibleFallbackTools = []
|
|
1307
|
+
state.incompatibleFallbackSimilarModels = []
|
|
1308
|
+
state.incompatibleFallbackSection = 'tools'
|
|
1309
|
+
return
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
if (key.name === 'up' && totalItems > 0) {
|
|
1313
|
+
state.incompatibleFallbackCursor = state.incompatibleFallbackCursor > 0
|
|
1314
|
+
? state.incompatibleFallbackCursor - 1
|
|
1315
|
+
: totalItems - 1
|
|
1316
|
+
state.incompatibleFallbackSection = state.incompatibleFallbackCursor < tools.length ? 'tools' : 'models'
|
|
1317
|
+
return
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
if (key.name === 'down' && totalItems > 0) {
|
|
1321
|
+
state.incompatibleFallbackCursor = state.incompatibleFallbackCursor < totalItems - 1
|
|
1322
|
+
? state.incompatibleFallbackCursor + 1
|
|
1323
|
+
: 0
|
|
1324
|
+
state.incompatibleFallbackSection = state.incompatibleFallbackCursor < tools.length ? 'tools' : 'models'
|
|
1325
|
+
return
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
if (key.name === 'return' && totalItems > 0) {
|
|
1329
|
+
const cursor = state.incompatibleFallbackCursor
|
|
1330
|
+
const fallbackModel = state.incompatibleFallbackModel
|
|
1331
|
+
|
|
1332
|
+
// 📖 Close overlay state first
|
|
1333
|
+
state.incompatibleFallbackOpen = false
|
|
1334
|
+
state.incompatibleFallbackCursor = 0
|
|
1335
|
+
state.incompatibleFallbackScrollOffset = 0
|
|
1336
|
+
state.incompatibleFallbackModel = null
|
|
1337
|
+
state.incompatibleFallbackTools = []
|
|
1338
|
+
state.incompatibleFallbackSimilarModels = []
|
|
1339
|
+
state.incompatibleFallbackSection = 'tools'
|
|
1340
|
+
|
|
1341
|
+
if (cursor < tools.length) {
|
|
1342
|
+
// 📖 Section 1: Switch to the selected compatible tool, then launch the original model
|
|
1343
|
+
const selectedToolKey = tools[cursor]
|
|
1344
|
+
setToolMode(selectedToolKey)
|
|
1345
|
+
// 📖 Find the full result object for the original model to pass to launchSelectedModel
|
|
1346
|
+
const fullModel = state.results.find(
|
|
1347
|
+
r => r.providerKey === fallbackModel.providerKey && r.modelId === fallbackModel.modelId
|
|
1348
|
+
)
|
|
1349
|
+
if (fullModel) {
|
|
1350
|
+
await launchSelectedModel(fullModel)
|
|
1351
|
+
}
|
|
1352
|
+
} else {
|
|
1353
|
+
// 📖 Section 2: Launch the selected similar model instead
|
|
1354
|
+
const modelIdx = cursor - tools.length
|
|
1355
|
+
const selectedSimilar = similarModels[modelIdx]
|
|
1356
|
+
if (selectedSimilar) {
|
|
1357
|
+
const fullModel = state.results.find(
|
|
1358
|
+
r => r.providerKey === selectedSimilar.providerKey && r.modelId === selectedSimilar.modelId
|
|
1359
|
+
)
|
|
1360
|
+
if (fullModel) {
|
|
1361
|
+
await launchSelectedModel(fullModel)
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
return
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1132
1370
|
// 📖 Feedback overlay: intercept ALL keys while overlay is active.
|
|
1133
1371
|
// 📖 Enter → send to Discord, Esc → cancel, Backspace → delete char, printable → append to buffer.
|
|
1134
1372
|
if (state.feedbackOpen) {
|
|
@@ -1442,7 +1680,8 @@ export function createKeyHandler(ctx) {
|
|
|
1442
1680
|
const providerKeys = Object.keys(sources)
|
|
1443
1681
|
const updateRowIdx = providerKeys.length
|
|
1444
1682
|
const themeRowIdx = updateRowIdx + 1
|
|
1445
|
-
const
|
|
1683
|
+
const favoritesModeRowIdx = themeRowIdx + 1
|
|
1684
|
+
const cleanupLegacyProxyRowIdx = favoritesModeRowIdx + 1
|
|
1446
1685
|
const changelogViewRowIdx = cleanupLegacyProxyRowIdx + 1
|
|
1447
1686
|
// 📖 Profile system removed - API keys now persist permanently across all sessions
|
|
1448
1687
|
const maxRowIdx = changelogViewRowIdx
|
|
@@ -1523,10 +1762,7 @@ export function createKeyHandler(ctx) {
|
|
|
1523
1762
|
setResults(nextResults)
|
|
1524
1763
|
syncFavoriteFlags(state.results, state.config)
|
|
1525
1764
|
applyTierFilter()
|
|
1526
|
-
|
|
1527
|
-
state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection)
|
|
1528
|
-
if (state.cursor >= state.visibleSorted.length) state.cursor = Math.max(0, state.visibleSorted.length - 1)
|
|
1529
|
-
adjustScrollOffset(state)
|
|
1765
|
+
refreshVisibleSorted({ resetCursor: false })
|
|
1530
1766
|
// 📖 Re-ping all models that were 'noauth' (got 401 without key) but now have a key
|
|
1531
1767
|
// 📖 This makes the TUI react immediately when a user adds an API key in settings
|
|
1532
1768
|
const pingModel = getPingModel?.()
|
|
@@ -1591,6 +1827,11 @@ export function createKeyHandler(ctx) {
|
|
|
1591
1827
|
return
|
|
1592
1828
|
}
|
|
1593
1829
|
|
|
1830
|
+
if (state.settingsCursor === favoritesModeRowIdx) {
|
|
1831
|
+
toggleFavoritesDisplayMode()
|
|
1832
|
+
return
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1594
1835
|
if (state.settingsCursor === cleanupLegacyProxyRowIdx) {
|
|
1595
1836
|
runLegacyProxyCleanup()
|
|
1596
1837
|
return
|
|
@@ -1629,6 +1870,10 @@ export function createKeyHandler(ctx) {
|
|
|
1629
1870
|
cycleGlobalTheme()
|
|
1630
1871
|
return
|
|
1631
1872
|
}
|
|
1873
|
+
if (state.settingsCursor === favoritesModeRowIdx) {
|
|
1874
|
+
toggleFavoritesDisplayMode()
|
|
1875
|
+
return
|
|
1876
|
+
}
|
|
1632
1877
|
// 📖 Profile system removed - API keys now persist permanently across all sessions
|
|
1633
1878
|
|
|
1634
1879
|
// 📖 Toggle enabled/disabled for selected provider
|
|
@@ -1644,6 +1889,7 @@ export function createKeyHandler(ctx) {
|
|
|
1644
1889
|
if (
|
|
1645
1890
|
state.settingsCursor === updateRowIdx
|
|
1646
1891
|
|| state.settingsCursor === themeRowIdx
|
|
1892
|
+
|| state.settingsCursor === favoritesModeRowIdx
|
|
1647
1893
|
|| state.settingsCursor === cleanupLegacyProxyRowIdx
|
|
1648
1894
|
|| state.settingsCursor === changelogViewRowIdx
|
|
1649
1895
|
) return
|
|
@@ -1661,6 +1907,12 @@ export function createKeyHandler(ctx) {
|
|
|
1661
1907
|
return
|
|
1662
1908
|
}
|
|
1663
1909
|
|
|
1910
|
+
// 📖 Y toggles favorites display mode directly from Settings.
|
|
1911
|
+
if (key.name === 'y') {
|
|
1912
|
+
toggleFavoritesDisplayMode()
|
|
1913
|
+
return
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1664
1916
|
// 📖 Profile system removed - API keys now persist permanently across all sessions
|
|
1665
1917
|
|
|
1666
1918
|
if (key.ctrl && key.name === 'c') { exit(0); return }
|
|
@@ -1707,9 +1959,20 @@ export function createKeyHandler(ctx) {
|
|
|
1707
1959
|
return
|
|
1708
1960
|
}
|
|
1709
1961
|
|
|
1710
|
-
// 📖 Y key
|
|
1962
|
+
// 📖 Y key toggles favorites display mode (pinned+sticky vs normal rows).
|
|
1963
|
+
if (key.name === 'y' && !key.ctrl && !key.meta) {
|
|
1964
|
+
toggleFavoritesDisplayMode()
|
|
1965
|
+
return
|
|
1966
|
+
}
|
|
1711
1967
|
|
|
1712
|
-
// 📖
|
|
1968
|
+
// 📖 X clears the active free-text filter set from the command palette.
|
|
1969
|
+
if (key.name === 'x' && !key.ctrl && !key.meta) {
|
|
1970
|
+
if (!state.customTextFilter) return
|
|
1971
|
+
state.customTextFilter = null
|
|
1972
|
+
applyTierFilter()
|
|
1973
|
+
refreshVisibleSorted({ resetCursor: true })
|
|
1974
|
+
return
|
|
1975
|
+
}
|
|
1713
1976
|
|
|
1714
1977
|
// 📖 Profile system removed - API keys now persist permanently across all sessions
|
|
1715
1978
|
|
|
@@ -1720,7 +1983,8 @@ export function createKeyHandler(ctx) {
|
|
|
1720
1983
|
}
|
|
1721
1984
|
|
|
1722
1985
|
// 📖 Sorting keys: R=rank, O=origin, M=model, L=latest ping, A=avg ping, S=SWE-bench, C=context, H=health, V=verdict, B=stability, U=uptime, G=usage
|
|
1723
|
-
// 📖 T is reserved for tier filter cycling. Y
|
|
1986
|
+
// 📖 T is reserved for tier filter cycling. Y toggles favorites display mode.
|
|
1987
|
+
// 📖 X clears the active custom text filter.
|
|
1724
1988
|
// 📖 D is now reserved for provider filter cycling
|
|
1725
1989
|
// 📖 Shift+R is reserved for reset view settings
|
|
1726
1990
|
const sortKeys = {
|
|
@@ -1763,10 +2027,7 @@ export function createKeyHandler(ctx) {
|
|
|
1763
2027
|
state.config.settings.hideUnconfiguredModels = state.hideUnconfiguredModels
|
|
1764
2028
|
saveConfig(state.config)
|
|
1765
2029
|
applyTierFilter()
|
|
1766
|
-
|
|
1767
|
-
state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection)
|
|
1768
|
-
state.cursor = 0
|
|
1769
|
-
state.scrollOffset = 0
|
|
2030
|
+
refreshVisibleSorted({ resetCursor: true })
|
|
1770
2031
|
return
|
|
1771
2032
|
}
|
|
1772
2033
|
|
|
@@ -1775,10 +2036,7 @@ export function createKeyHandler(ctx) {
|
|
|
1775
2036
|
state.tierFilterMode = (state.tierFilterMode + 1) % TIER_CYCLE.length
|
|
1776
2037
|
applyTierFilter()
|
|
1777
2038
|
// 📖 Recompute visible sorted list and reset cursor to avoid stale index into new filtered set
|
|
1778
|
-
|
|
1779
|
-
state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection)
|
|
1780
|
-
state.cursor = 0
|
|
1781
|
-
state.scrollOffset = 0
|
|
2039
|
+
refreshVisibleSorted({ resetCursor: true })
|
|
1782
2040
|
persistUiSettings()
|
|
1783
2041
|
return
|
|
1784
2042
|
}
|
|
@@ -1788,10 +2046,7 @@ export function createKeyHandler(ctx) {
|
|
|
1788
2046
|
state.originFilterMode = (state.originFilterMode + 1) % ORIGIN_CYCLE.length
|
|
1789
2047
|
applyTierFilter()
|
|
1790
2048
|
// 📖 Recompute visible sorted list and reset cursor to avoid stale index into new filtered set
|
|
1791
|
-
|
|
1792
|
-
state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection)
|
|
1793
|
-
state.cursor = 0
|
|
1794
|
-
state.scrollOffset = 0
|
|
2049
|
+
refreshVisibleSorted({ resetCursor: true })
|
|
1795
2050
|
persistUiSettings()
|
|
1796
2051
|
return
|
|
1797
2052
|
}
|
|
@@ -1854,6 +2109,33 @@ export function createKeyHandler(ctx) {
|
|
|
1854
2109
|
// 📖 Use the cached visible+sorted array — guaranteed to match what's on screen
|
|
1855
2110
|
const selected = state.visibleSorted[state.cursor]
|
|
1856
2111
|
if (!selected) return // 📖 Guard: empty visible list (all filtered out)
|
|
2112
|
+
|
|
2113
|
+
// 📖 Incompatibility intercept — if the model can't run on the active tool,
|
|
2114
|
+
// 📖 show the fallback overlay instead of launching. Lets user switch tool or pick similar model.
|
|
2115
|
+
if (!isModelCompatibleWithTool(selected.providerKey, state.mode)) {
|
|
2116
|
+
const compatTools = getCompatibleTools(selected.providerKey)
|
|
2117
|
+
const similarModels = findSimilarCompatibleModels(
|
|
2118
|
+
selected.sweScore || '-',
|
|
2119
|
+
state.mode,
|
|
2120
|
+
state.results.filter(r => r.providerKey !== selected.providerKey || r.modelId !== selected.modelId),
|
|
2121
|
+
3
|
|
2122
|
+
)
|
|
2123
|
+
state.incompatibleFallbackOpen = true
|
|
2124
|
+
state.incompatibleFallbackCursor = 0
|
|
2125
|
+
state.incompatibleFallbackScrollOffset = 0
|
|
2126
|
+
state.incompatibleFallbackModel = {
|
|
2127
|
+
modelId: selected.modelId,
|
|
2128
|
+
label: selected.label,
|
|
2129
|
+
tier: selected.tier,
|
|
2130
|
+
providerKey: selected.providerKey,
|
|
2131
|
+
sweScore: selected.sweScore || '-',
|
|
2132
|
+
}
|
|
2133
|
+
state.incompatibleFallbackTools = compatTools
|
|
2134
|
+
state.incompatibleFallbackSimilarModels = similarModels
|
|
2135
|
+
state.incompatibleFallbackSection = 'tools'
|
|
2136
|
+
return
|
|
2137
|
+
}
|
|
2138
|
+
|
|
1857
2139
|
if (shouldCheckMissingTool(state.mode) && !isToolInstalled(state.mode)) {
|
|
1858
2140
|
state.toolInstallPromptOpen = true
|
|
1859
2141
|
state.toolInstallPromptCursor = 0
|