free-coding-models 0.3.25 → 0.3.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/CHANGELOG.md +62 -2
- package/README.md +23 -1
- package/package.json +1 -1
- package/sources.js +37 -19
- package/src/app.js +33 -6
- package/src/command-palette.js +1 -0
- package/src/installed-models-manager.js +636 -0
- package/src/key-handler.js +187 -14
- package/src/overlays.js +111 -2
- package/src/render-table.js +26 -10
- package/src/updater.js +127 -36
package/src/key-handler.js
CHANGED
|
@@ -35,6 +35,8 @@ import { getLastLayout, COLUMN_SORT_MAP } from './render-table.js'
|
|
|
35
35
|
import { cycleThemeSetting, detectActiveTheme } from './theme.js'
|
|
36
36
|
import { buildCommandPaletteTree, flattenCommandTree, filterCommandPaletteEntries } from './command-palette.js'
|
|
37
37
|
import { WIDTH_WARNING_MIN_COLS } from './constants.js'
|
|
38
|
+
import { scanAllToolConfigs, softDeleteModel } from './installed-models-manager.js'
|
|
39
|
+
import { startExternalTool } from './tool-launchers.js'
|
|
38
40
|
|
|
39
41
|
// 📖 Some providers need an explicit probe model because the first catalog entry
|
|
40
42
|
// 📖 is not guaranteed to be accepted by their chat endpoint.
|
|
@@ -687,6 +689,21 @@ export function createKeyHandler(ctx) {
|
|
|
687
689
|
state.changelogSelectedVersion = null
|
|
688
690
|
}
|
|
689
691
|
|
|
692
|
+
function openInstalledModelsOverlay() {
|
|
693
|
+
state.installedModelsOpen = true
|
|
694
|
+
state.installedModelsCursor = 0
|
|
695
|
+
state.installedModelsScrollOffset = 0
|
|
696
|
+
state.installedModelsErrorMsg = 'Scanning...'
|
|
697
|
+
|
|
698
|
+
try {
|
|
699
|
+
const results = scanAllToolConfigs()
|
|
700
|
+
state.installedModelsData = results
|
|
701
|
+
state.installedModelsErrorMsg = null
|
|
702
|
+
} catch (err) {
|
|
703
|
+
state.installedModelsErrorMsg = err.message || 'Failed to scan tool configs'
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
690
707
|
function cycleToolMode() {
|
|
691
708
|
const modeOrder = getToolModeOrder()
|
|
692
709
|
const currentIndex = modeOrder.indexOf(state.mode)
|
|
@@ -774,6 +791,7 @@ export function createKeyHandler(ctx) {
|
|
|
774
791
|
return state.settingsOpen
|
|
775
792
|
|| state.installEndpointsOpen
|
|
776
793
|
|| state.toolInstallPromptOpen
|
|
794
|
+
|| state.installedModelsOpen
|
|
777
795
|
|| state.recommendOpen
|
|
778
796
|
|| state.feedbackOpen
|
|
779
797
|
|| state.helpVisible
|
|
@@ -825,6 +843,21 @@ export function createKeyHandler(ctx) {
|
|
|
825
843
|
})
|
|
826
844
|
}
|
|
827
845
|
|
|
846
|
+
// 📖 Inject a high-priority update entry at the top when a newer version is known.
|
|
847
|
+
const updateVersion = state.startupLatestVersion
|
|
848
|
+
if (updateVersion && state.versionAlertsEnabled) {
|
|
849
|
+
state.commandPaletteResults.unshift({
|
|
850
|
+
id: 'action-update-now',
|
|
851
|
+
label: `⬆️ UPDATE NOW — v${updateVersion} available (recommended!)`,
|
|
852
|
+
type: 'command',
|
|
853
|
+
depth: 0,
|
|
854
|
+
hasChildren: false,
|
|
855
|
+
isExpanded: false,
|
|
856
|
+
updateVersion,
|
|
857
|
+
keywords: ['update', 'upgrade', 'version', 'install'],
|
|
858
|
+
})
|
|
859
|
+
}
|
|
860
|
+
|
|
828
861
|
if (state.commandPaletteCursor >= state.commandPaletteResults.length) {
|
|
829
862
|
state.commandPaletteCursor = Math.max(0, state.commandPaletteResults.length - 1)
|
|
830
863
|
}
|
|
@@ -851,6 +884,14 @@ export function createKeyHandler(ctx) {
|
|
|
851
884
|
function executeCommandPaletteEntry(entry) {
|
|
852
885
|
if (!entry?.id) return
|
|
853
886
|
|
|
887
|
+
// 📖 Update action: stop TUI cleanly and run the npm update + relaunch.
|
|
888
|
+
if (entry.id === 'action-update-now' && entry.updateVersion) {
|
|
889
|
+
closeCommandPalette()
|
|
890
|
+
stopUi({ resetRawMode: true })
|
|
891
|
+
runUpdate(entry.updateVersion)
|
|
892
|
+
return
|
|
893
|
+
}
|
|
894
|
+
|
|
854
895
|
if (entry.id.startsWith('action-set-ping-') && entry.pingMode) {
|
|
855
896
|
setPingMode(entry.pingMode, 'manual')
|
|
856
897
|
return
|
|
@@ -943,6 +984,7 @@ export function createKeyHandler(ctx) {
|
|
|
943
984
|
case 'open-feedback': return openFeedbackOverlay()
|
|
944
985
|
case 'open-recommend': return openRecommendOverlay()
|
|
945
986
|
case 'open-install-endpoints': return openInstallEndpointsOverlay()
|
|
987
|
+
case 'open-installed-models': return openInstalledModelsOverlay()
|
|
946
988
|
case 'action-cycle-theme': return cycleGlobalTheme()
|
|
947
989
|
case 'action-cycle-tool-mode': return cycleToolMode()
|
|
948
990
|
case 'action-cycle-ping-mode': {
|
|
@@ -963,6 +1005,10 @@ export function createKeyHandler(ctx) {
|
|
|
963
1005
|
if (!key) return
|
|
964
1006
|
noteUserActivity()
|
|
965
1007
|
|
|
1008
|
+
// 📖 Ctrl+C: always exit immediately, checked FIRST to prevent any other key binding from swallowing it.
|
|
1009
|
+
// 📖 Also handles the raw \x03 byte as a fallback for terminals where readline doesn't set key.ctrl properly.
|
|
1010
|
+
if ((key.ctrl && key.name === 'c') || str === '\x03') { exit(0); return }
|
|
1011
|
+
|
|
966
1012
|
// 📖 Ctrl+P toggles the command palette from the main table only.
|
|
967
1013
|
if (key.ctrl && key.name === 'p') {
|
|
968
1014
|
if (state.commandPaletteOpen) {
|
|
@@ -1289,6 +1335,104 @@ export function createKeyHandler(ctx) {
|
|
|
1289
1335
|
return
|
|
1290
1336
|
}
|
|
1291
1337
|
|
|
1338
|
+
// ─── Installed Models overlay keyboard handling ───────────────────────────
|
|
1339
|
+
if (state.installedModelsOpen) {
|
|
1340
|
+
if (key.ctrl && key.name === 'c') { exit(0); return }
|
|
1341
|
+
|
|
1342
|
+
const scanResults = state.installedModelsData || []
|
|
1343
|
+
let maxIndex = 0
|
|
1344
|
+
for (const toolResult of scanResults) {
|
|
1345
|
+
maxIndex += 1
|
|
1346
|
+
maxIndex += toolResult.models.length
|
|
1347
|
+
}
|
|
1348
|
+
if (maxIndex > 0) maxIndex--
|
|
1349
|
+
|
|
1350
|
+
const pageStep = Math.max(1, (state.terminalRows || 1) - 4)
|
|
1351
|
+
|
|
1352
|
+
if (key.name === 'up' || (key.shift && key.name === 'tab')) {
|
|
1353
|
+
state.installedModelsCursor = Math.max(0, state.installedModelsCursor - 1)
|
|
1354
|
+
return
|
|
1355
|
+
}
|
|
1356
|
+
if (key.name === 'down' || key.name === 'tab') {
|
|
1357
|
+
state.installedModelsCursor = Math.min(maxIndex, state.installedModelsCursor + 1)
|
|
1358
|
+
return
|
|
1359
|
+
}
|
|
1360
|
+
if (key.name === 'pageup') {
|
|
1361
|
+
state.installedModelsCursor = Math.max(0, state.installedModelsCursor - pageStep)
|
|
1362
|
+
return
|
|
1363
|
+
}
|
|
1364
|
+
if (key.name === 'pagedown') {
|
|
1365
|
+
state.installedModelsCursor = Math.min(maxIndex, state.installedModelsCursor + pageStep)
|
|
1366
|
+
return
|
|
1367
|
+
}
|
|
1368
|
+
if (key.name === 'home') {
|
|
1369
|
+
state.installedModelsCursor = 0
|
|
1370
|
+
return
|
|
1371
|
+
}
|
|
1372
|
+
if (key.name === 'end') {
|
|
1373
|
+
state.installedModelsCursor = maxIndex
|
|
1374
|
+
return
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
if (key.name === 'escape') {
|
|
1378
|
+
state.installedModelsOpen = false
|
|
1379
|
+
state.installedModelsCursor = 0
|
|
1380
|
+
return
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
if (key.name === 'return') {
|
|
1384
|
+
let currentIdx = 0
|
|
1385
|
+
for (const toolResult of scanResults) {
|
|
1386
|
+
if (currentIdx === state.installedModelsCursor) {
|
|
1387
|
+
return
|
|
1388
|
+
}
|
|
1389
|
+
currentIdx++
|
|
1390
|
+
for (const model of toolResult.models) {
|
|
1391
|
+
if (currentIdx === state.installedModelsCursor) {
|
|
1392
|
+
const selectedModel = {
|
|
1393
|
+
modelId: model.modelId,
|
|
1394
|
+
providerKey: model.providerKey,
|
|
1395
|
+
label: model.label,
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
state.installedModelsOpen = false
|
|
1399
|
+
await startExternalTool(toolResult.toolMode, selectedModel, state.config)
|
|
1400
|
+
return
|
|
1401
|
+
}
|
|
1402
|
+
currentIdx++
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
if (key.name === 'd') {
|
|
1408
|
+
let currentIdx = 0
|
|
1409
|
+
for (const toolResult of scanResults) {
|
|
1410
|
+
currentIdx++
|
|
1411
|
+
for (const model of toolResult.models) {
|
|
1412
|
+
if (currentIdx === state.installedModelsCursor) {
|
|
1413
|
+
softDeleteModel(toolResult.toolMode, model.modelId)
|
|
1414
|
+
.then((result) => {
|
|
1415
|
+
if (result.success) {
|
|
1416
|
+
openInstalledModelsOverlay()
|
|
1417
|
+
} else {
|
|
1418
|
+
state.installedModelsErrorMsg = `Failed to disable: ${result.error}`
|
|
1419
|
+
setTimeout(() => { state.installedModelsErrorMsg = null }, 3000)
|
|
1420
|
+
}
|
|
1421
|
+
})
|
|
1422
|
+
.catch((err) => {
|
|
1423
|
+
state.installedModelsErrorMsg = `Failed to disable: ${err.message}`
|
|
1424
|
+
setTimeout(() => { state.installedModelsErrorMsg = null }, 3000)
|
|
1425
|
+
})
|
|
1426
|
+
return
|
|
1427
|
+
}
|
|
1428
|
+
currentIdx++
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
return
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1292
1436
|
// 📖 Incompatible fallback overlay: ↑↓ navigate across tool + model sections, Enter confirms, Esc cancels.
|
|
1293
1437
|
// 📖 Cursor is a flat index: 0..N-1 = compatible tools, N..N+M-1 = similar models.
|
|
1294
1438
|
if (state.incompatibleFallbackOpen) {
|
|
@@ -1438,12 +1582,12 @@ export function createKeyHandler(ctx) {
|
|
|
1438
1582
|
// 📖 Help overlay: full keyboard navigation + key swallowing while overlay is open.
|
|
1439
1583
|
if (state.helpVisible) {
|
|
1440
1584
|
const pageStep = Math.max(1, (state.terminalRows || 1) - 2)
|
|
1441
|
-
if (key.name === 'escape' || key.name === '
|
|
1585
|
+
if (key.name === 'escape' || (key.ctrl && key.name === 'h')) {
|
|
1442
1586
|
state.helpVisible = false
|
|
1443
1587
|
return
|
|
1444
1588
|
}
|
|
1445
|
-
if (key.name === 'up') { state.helpScrollOffset = Math.max(0, state.helpScrollOffset - 1); return }
|
|
1446
|
-
if (key.name === 'down') { state.helpScrollOffset += 1; return }
|
|
1589
|
+
if (key.name === 'up' || key.name === 'k') { state.helpScrollOffset = Math.max(0, state.helpScrollOffset - 1); return }
|
|
1590
|
+
if (key.name === 'down' || key.name === 'j') { state.helpScrollOffset += 1; return }
|
|
1447
1591
|
if (key.name === 'pageup') { state.helpScrollOffset = Math.max(0, state.helpScrollOffset - pageStep); return }
|
|
1448
1592
|
if (key.name === 'pagedown') { state.helpScrollOffset += pageStep; return }
|
|
1449
1593
|
if (key.name === 'home') { state.helpScrollOffset = 0; return }
|
|
@@ -1983,6 +2127,13 @@ export function createKeyHandler(ctx) {
|
|
|
1983
2127
|
return
|
|
1984
2128
|
}
|
|
1985
2129
|
|
|
2130
|
+
// 📖 Shift+U: trigger immediate update when a newer version is known.
|
|
2131
|
+
if (key.name === 'u' && key.shift && state.startupLatestVersion && state.versionAlertsEnabled) {
|
|
2132
|
+
stopUi({ resetRawMode: true })
|
|
2133
|
+
runUpdate(state.startupLatestVersion)
|
|
2134
|
+
return
|
|
2135
|
+
}
|
|
2136
|
+
|
|
1986
2137
|
// 📖 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
|
|
1987
2138
|
// 📖 T is reserved for tier filter cycling. Y toggles favorites display mode.
|
|
1988
2139
|
// 📖 X clears the active custom text filter.
|
|
@@ -2052,8 +2203,8 @@ export function createKeyHandler(ctx) {
|
|
|
2052
2203
|
return
|
|
2053
2204
|
}
|
|
2054
2205
|
|
|
2055
|
-
// 📖 Help overlay key:
|
|
2056
|
-
if (key.name === '
|
|
2206
|
+
// 📖 Help overlay key: Ctrl+H = toggle help overlay
|
|
2207
|
+
if (key.ctrl && key.name === 'h') {
|
|
2057
2208
|
state.helpVisible = !state.helpVisible
|
|
2058
2209
|
if (state.helpVisible) state.helpScrollOffset = 0
|
|
2059
2210
|
return
|
|
@@ -2077,8 +2228,8 @@ export function createKeyHandler(ctx) {
|
|
|
2077
2228
|
return
|
|
2078
2229
|
}
|
|
2079
2230
|
|
|
2080
|
-
if (key.name === 'up') {
|
|
2081
|
-
// 📖 Main list wrap navigation: top -> bottom on Up.
|
|
2231
|
+
if (key.name === 'up' || key.name === 'k') {
|
|
2232
|
+
// 📖 Main list wrap navigation: top -> bottom on Up / K (vim-style).
|
|
2082
2233
|
const count = state.visibleSorted.length
|
|
2083
2234
|
if (count === 0) return
|
|
2084
2235
|
state.cursor = state.cursor > 0 ? state.cursor - 1 : count - 1
|
|
@@ -2086,8 +2237,8 @@ export function createKeyHandler(ctx) {
|
|
|
2086
2237
|
return
|
|
2087
2238
|
}
|
|
2088
2239
|
|
|
2089
|
-
if (key.name === 'down') {
|
|
2090
|
-
// 📖 Main list wrap navigation: bottom -> top on Down.
|
|
2240
|
+
if (key.name === 'down' || key.name === 'j') {
|
|
2241
|
+
// 📖 Main list wrap navigation: bottom -> top on Down / J (vim-style).
|
|
2091
2242
|
const count = state.visibleSorted.length
|
|
2092
2243
|
if (count === 0) return
|
|
2093
2244
|
state.cursor = state.cursor < count - 1 ? state.cursor + 1 : 0
|
|
@@ -2095,11 +2246,6 @@ export function createKeyHandler(ctx) {
|
|
|
2095
2246
|
return
|
|
2096
2247
|
}
|
|
2097
2248
|
|
|
2098
|
-
if (key.name === 'c' && key.ctrl) { // Ctrl+C
|
|
2099
|
-
exit(0)
|
|
2100
|
-
return
|
|
2101
|
-
}
|
|
2102
|
-
|
|
2103
2249
|
// 📖 Esc can dismiss the narrow-terminal warning immediately without quitting the app.
|
|
2104
2250
|
if (key.name === 'escape' && state.terminalCols > 0 && state.terminalCols < WIDTH_WARNING_MIN_COLS) {
|
|
2105
2251
|
state.widthWarningDismissed = true
|
|
@@ -2327,6 +2473,22 @@ export function createMouseEventHandler(ctx) {
|
|
|
2327
2473
|
}
|
|
2328
2474
|
return
|
|
2329
2475
|
}
|
|
2476
|
+
if (state.installedModelsOpen) {
|
|
2477
|
+
const scanResults = state.installedModelsData || []
|
|
2478
|
+
let maxIndex = 0
|
|
2479
|
+
for (const toolResult of scanResults) {
|
|
2480
|
+
maxIndex += 1
|
|
2481
|
+
maxIndex += toolResult.models.length
|
|
2482
|
+
}
|
|
2483
|
+
if (maxIndex > 0) maxIndex--
|
|
2484
|
+
|
|
2485
|
+
if (evt.type === 'scroll-up') {
|
|
2486
|
+
state.installedModelsCursor = Math.max(0, (state.installedModelsCursor || 0) - 1)
|
|
2487
|
+
} else {
|
|
2488
|
+
state.installedModelsCursor = Math.min(maxIndex, (state.installedModelsCursor || 0) + 1)
|
|
2489
|
+
}
|
|
2490
|
+
return
|
|
2491
|
+
}
|
|
2330
2492
|
|
|
2331
2493
|
// 📖 Main table scroll: move cursor up/down with wrap-around
|
|
2332
2494
|
const count = state.visibleSorted.length
|
|
@@ -2402,6 +2564,11 @@ export function createMouseEventHandler(ctx) {
|
|
|
2402
2564
|
return
|
|
2403
2565
|
}
|
|
2404
2566
|
|
|
2567
|
+
if (state.installedModelsOpen) {
|
|
2568
|
+
state.installedModelsOpen = false
|
|
2569
|
+
return
|
|
2570
|
+
}
|
|
2571
|
+
|
|
2405
2572
|
if (state.incompatibleFallbackOpen) {
|
|
2406
2573
|
// 📖 Incompatible fallback: click closes
|
|
2407
2574
|
state.incompatibleFallbackOpen = false
|
|
@@ -2553,6 +2720,12 @@ export function createMouseEventHandler(ctx) {
|
|
|
2553
2720
|
if (layout.footerHotkeys && layout.footerHotkeys.length > 0) {
|
|
2554
2721
|
const zone = layout.footerHotkeys.find(z => y === z.row && x >= z.xStart && x <= z.xEnd)
|
|
2555
2722
|
if (zone) {
|
|
2723
|
+
// 📖 Update banner click: stop TUI and run the npm update + relaunch.
|
|
2724
|
+
if (zone.key === 'update-click' && state.startupLatestVersion && state.versionAlertsEnabled) {
|
|
2725
|
+
stopUi({ resetRawMode: true })
|
|
2726
|
+
runUpdate(state.startupLatestVersion)
|
|
2727
|
+
return
|
|
2728
|
+
}
|
|
2556
2729
|
// 📖 Map the footer zone key to a synthetic keypress.
|
|
2557
2730
|
// 📖 Most are single-character keys; special cases like ctrl+p need special handling.
|
|
2558
2731
|
if (zone.key === 'ctrl+p') {
|
package/src/overlays.js
CHANGED
|
@@ -479,6 +479,114 @@ export function createOverlayRenderers(state, deps) {
|
|
|
479
479
|
return cleared.join('\n')
|
|
480
480
|
}
|
|
481
481
|
|
|
482
|
+
// ─── Installed Models Manager overlay renderer ─────────────────────────────
|
|
483
|
+
// 📖 renderInstalledModels displays all models configured in external tools
|
|
484
|
+
// 📖 Shows tool configs, model lists, and provides actions (Launch, Disable, Reinstall)
|
|
485
|
+
function renderInstalledModels() {
|
|
486
|
+
const EL = '\x1b[K'
|
|
487
|
+
const lines = []
|
|
488
|
+
const cursorLineByRow = {}
|
|
489
|
+
|
|
490
|
+
lines.push('')
|
|
491
|
+
lines.push(` ${themeColors.accent('🚀')} ${themeColors.accentBold('free-coding-models')} ${themeColors.dim(`v${LOCAL_VERSION}`)}`)
|
|
492
|
+
lines.push(` ${themeColors.textBold('🗂️ Installed Models Manager')}`)
|
|
493
|
+
lines.push('')
|
|
494
|
+
lines.push(themeColors.dim(' — models configured in your tools'))
|
|
495
|
+
|
|
496
|
+
if (state.installedModelsErrorMsg) {
|
|
497
|
+
lines.push(` ${themeColors.warning(state.installedModelsErrorMsg)}`)
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (state.installedModelsErrorMsg === 'Scanning...') {
|
|
501
|
+
lines.push(themeColors.dim(' Scanning tool configs, please wait...'))
|
|
502
|
+
const targetLine = 5
|
|
503
|
+
state.installedModelsScrollOffset = keepOverlayTargetVisible(
|
|
504
|
+
state.installedModelsScrollOffset,
|
|
505
|
+
targetLine,
|
|
506
|
+
lines.length,
|
|
507
|
+
state.terminalRows
|
|
508
|
+
)
|
|
509
|
+
const { visible, offset } = sliceOverlayLines(lines, state.installedModelsScrollOffset, state.terminalRows)
|
|
510
|
+
state.installedModelsScrollOffset = offset
|
|
511
|
+
|
|
512
|
+
overlayLayout.installedModelsCursorToLine = cursorLineByRow
|
|
513
|
+
overlayLayout.installedModelsScrollOffset = offset
|
|
514
|
+
|
|
515
|
+
const tintedLines = tintOverlayLines(visible, themeColors.overlayBgSettings, state.terminalCols)
|
|
516
|
+
const cleared = tintedLines.map((l) => l + EL)
|
|
517
|
+
return cleared.join('\n')
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
lines.push('')
|
|
521
|
+
|
|
522
|
+
const scanResults = state.installedModelsData || []
|
|
523
|
+
|
|
524
|
+
if (scanResults.length === 0) {
|
|
525
|
+
lines.push(themeColors.dim(' No tool configs found.'))
|
|
526
|
+
lines.push(themeColors.dim(' Install a tool (Goose, Crush, Aider, etc.) to get started.'))
|
|
527
|
+
} else {
|
|
528
|
+
let globalIdx = 0
|
|
529
|
+
|
|
530
|
+
for (const toolResult of scanResults) {
|
|
531
|
+
const { toolMode, toolLabel, toolEmoji, configPath, isValid, hasManagedMarker, models } = toolResult
|
|
532
|
+
|
|
533
|
+
lines.push('')
|
|
534
|
+
const isCursor = globalIdx === state.installedModelsCursor
|
|
535
|
+
|
|
536
|
+
const statusIcon = isValid ? themeColors.successBold('✅') : themeColors.errorBold('⚠️')
|
|
537
|
+
const toolHeader = `${bullet(isCursor)}${toolEmoji} ${themeColors.textBold(toolLabel)} ${statusIcon}`
|
|
538
|
+
cursorLineByRow[globalIdx++] = lines.length
|
|
539
|
+
lines.push(isCursor ? themeColors.bgCursor(toolHeader) : toolHeader)
|
|
540
|
+
|
|
541
|
+
const configShortPath = configPath.replace(process.env.HOME || homedir(), '~')
|
|
542
|
+
lines.push(` ${themeColors.dim(configShortPath)}`)
|
|
543
|
+
|
|
544
|
+
if (!isValid) {
|
|
545
|
+
lines.push(themeColors.dim(' ⚠️ Config invalid or missing'))
|
|
546
|
+
} else if (models.length === 0) {
|
|
547
|
+
lines.push(themeColors.dim(' No models configured'))
|
|
548
|
+
} else {
|
|
549
|
+
const managedBadge = hasManagedMarker ? themeColors.info('• Managed by FCM') : themeColors.dim('• External config')
|
|
550
|
+
lines.push(` ${themeColors.success(`${models.length} model${models.length > 1 ? 's' : ''} configured`)} ${managedBadge}`)
|
|
551
|
+
|
|
552
|
+
for (const model of models) {
|
|
553
|
+
const isModelCursor = globalIdx === state.installedModelsCursor
|
|
554
|
+
const tierBadge = model.tier !== '-' ? themeColors.info(model.tier.padEnd(2)) : themeColors.dim(' ')
|
|
555
|
+
const externalBadge = model.isExternal ? themeColors.dim('[external]') : ''
|
|
556
|
+
|
|
557
|
+
const modelRow = ` • ${model.label} ${tierBadge} ${externalBadge}`
|
|
558
|
+
cursorLineByRow[globalIdx++] = lines.length
|
|
559
|
+
lines.push(isModelCursor ? themeColors.bgCursor(modelRow) : modelRow)
|
|
560
|
+
|
|
561
|
+
if (isModelCursor) {
|
|
562
|
+
lines.push(` ${themeColors.dim('[Enter] Launch [D] Disable')}`)
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
lines.push('')
|
|
570
|
+
lines.push(themeColors.dim(' ↑↓ Navigate Enter=Launch D=Disable Esc=Close'))
|
|
571
|
+
|
|
572
|
+
const targetLine = cursorLineByRow[state.installedModelsCursor] ?? 0
|
|
573
|
+
state.installedModelsScrollOffset = keepOverlayTargetVisible(
|
|
574
|
+
state.installedModelsScrollOffset,
|
|
575
|
+
targetLine,
|
|
576
|
+
lines.length,
|
|
577
|
+
state.terminalRows
|
|
578
|
+
)
|
|
579
|
+
const { visible, offset } = sliceOverlayLines(lines, state.installedModelsScrollOffset, state.terminalRows)
|
|
580
|
+
state.installedModelsScrollOffset = offset
|
|
581
|
+
|
|
582
|
+
overlayLayout.installedModelsCursorToLine = cursorLineByRow
|
|
583
|
+
overlayLayout.installedModelsScrollOffset = offset
|
|
584
|
+
|
|
585
|
+
const tintedLines = tintOverlayLines(visible, themeColors.overlayBgSettings, state.terminalCols)
|
|
586
|
+
const cleared = tintedLines.map((l) => l + EL)
|
|
587
|
+
return cleared.join('\n')
|
|
588
|
+
}
|
|
589
|
+
|
|
482
590
|
// ─── Missing-tool install confirmation overlay ────────────────────────────
|
|
483
591
|
// 📖 renderToolInstallPrompt keeps the user inside the TUI long enough to
|
|
484
592
|
// 📖 confirm the auto-install, then the key handler exits the alt screen and
|
|
@@ -796,7 +904,7 @@ export function createOverlayRenderers(state, deps) {
|
|
|
796
904
|
lines.push('')
|
|
797
905
|
lines.push(` ${heading('Main TUI')}`)
|
|
798
906
|
lines.push(` ${heading('Navigation')}`)
|
|
799
|
-
lines.push(` ${key('↑↓')}
|
|
907
|
+
lines.push(` ${key('↑↓ / J/K')} Navigate rows ${hint('(J/K = vim-style scroll)')}`)
|
|
800
908
|
lines.push(` ${key('Enter')} Select model and launch`)
|
|
801
909
|
lines.push(` ${hint('If the active CLI is missing, FCM offers a one-click install prompt first.')}`)
|
|
802
910
|
lines.push('')
|
|
@@ -815,7 +923,7 @@ export function createOverlayRenderers(state, deps) {
|
|
|
815
923
|
// 📖 Profile system removed - API keys now persist permanently across all sessions
|
|
816
924
|
lines.push(` ${key('Shift+R')} Reset view settings ${hint('(tier filter, sort, provider filter → defaults)')}`)
|
|
817
925
|
lines.push(` ${key('N')} Changelog ${hint('(📋 browse all versions, Enter to view details)')}`)
|
|
818
|
-
lines.push(` ${key('
|
|
926
|
+
lines.push(` ${key('Ctrl+H')} / ${key('Esc')} Show/hide this help`)
|
|
819
927
|
lines.push(` ${key('Ctrl+C')} Exit`)
|
|
820
928
|
lines.push('')
|
|
821
929
|
lines.push(` ${heading('Settings (P)')}`)
|
|
@@ -1381,6 +1489,7 @@ export function createOverlayRenderers(state, deps) {
|
|
|
1381
1489
|
renderRecommend,
|
|
1382
1490
|
renderFeedback,
|
|
1383
1491
|
renderChangelog,
|
|
1492
|
+
renderInstalledModels,
|
|
1384
1493
|
renderIncompatibleFallback,
|
|
1385
1494
|
startRecommendAnalysis,
|
|
1386
1495
|
stopRecommendAnalysis,
|
package/src/render-table.js
CHANGED
|
@@ -52,6 +52,7 @@ import { usagePlaceholderForProvider } from './ping.js'
|
|
|
52
52
|
import { calculateViewport, sortResultsWithPinnedFavorites, padEndDisplay, displayWidth } from './render-helpers.js'
|
|
53
53
|
import { getToolMeta, TOOL_METADATA, TOOL_MODE_ORDER, isModelCompatibleWithTool } from './tool-metadata.js'
|
|
54
54
|
import { getColumnSpacing } from './ui-config.js'
|
|
55
|
+
import { detectPackageManager, getManualInstallCmd } from './updater.js'
|
|
55
56
|
|
|
56
57
|
const require = createRequire(import.meta.url)
|
|
57
58
|
const { version: LOCAL_VERSION } = require('../package.json')
|
|
@@ -71,6 +72,7 @@ let _lastLayout = {
|
|
|
71
72
|
hasAboveIndicator: false, // 📖 whether "... N more above ..." is shown
|
|
72
73
|
hasBelowIndicator: false, // 📖 whether "... N more below ..." is shown
|
|
73
74
|
footerHotkeys: [], // 📖 Array of { key, row, xStart, xEnd } for footer click zones
|
|
75
|
+
updateBannerRow: 0, // 📖 1-based terminal row of the fluorescent update banner (0 = none)
|
|
74
76
|
}
|
|
75
77
|
export function getLastLayout() { return _lastLayout }
|
|
76
78
|
|
|
@@ -102,7 +104,7 @@ export const PROVIDER_COLOR = new Proxy({}, {
|
|
|
102
104
|
})
|
|
103
105
|
|
|
104
106
|
// ─── renderTable: mode param controls footer hint text (opencode vs openclaw) ─────────
|
|
105
|
-
export function renderTable(results, pendingPings, frame, cursor = null, sortColumn = 'avg', sortDirection = 'asc', pingInterval = PING_INTERVAL, lastPingTime = Date.now(), mode = 'opencode', tierFilterMode = 0, scrollOffset = 0, terminalRows = 0, terminalCols = 0, originFilterMode = 0, legacyStatus = null, pingMode = 'normal', pingModeSource = 'auto', hideUnconfiguredModels = false, widthWarningStartedAt = null, widthWarningDismissed = false, widthWarningShowCount = 0, settingsUpdateState = 'idle', settingsUpdateLatestVersion = null, legacyFlag = false, startupLatestVersion = null, versionAlertsEnabled = true, favoritesPinnedAndSticky = false, customTextFilter = null) {
|
|
107
|
+
export function renderTable(results, pendingPings, frame, cursor = null, sortColumn = 'avg', sortDirection = 'asc', pingInterval = PING_INTERVAL, lastPingTime = Date.now(), mode = 'opencode', tierFilterMode = 0, scrollOffset = 0, terminalRows = 0, terminalCols = 0, originFilterMode = 0, legacyStatus = null, pingMode = 'normal', pingModeSource = 'auto', hideUnconfiguredModels = false, widthWarningStartedAt = null, widthWarningDismissed = false, widthWarningShowCount = 0, settingsUpdateState = 'idle', settingsUpdateLatestVersion = null, legacyFlag = false, startupLatestVersion = null, versionAlertsEnabled = true, favoritesPinnedAndSticky = false, customTextFilter = null, lastReleaseDate = null) {
|
|
106
108
|
// 📖 Filter out hidden models for display
|
|
107
109
|
const visibleResults = results.filter(r => !r.hidden)
|
|
108
110
|
|
|
@@ -741,7 +743,9 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
741
743
|
{ text: ' • ', key: null },
|
|
742
744
|
{ text: 'P Settings', key: 'p' },
|
|
743
745
|
{ text: ' • ', key: null },
|
|
744
|
-
{ text: 'K
|
|
746
|
+
{ text: 'J/K Navigate', key: null },
|
|
747
|
+
{ text: ' • ', key: null },
|
|
748
|
+
{ text: 'Ctrl+H Help', key: 'ctrl+h' },
|
|
745
749
|
]
|
|
746
750
|
const footerRow1 = lines.length + 1 // 📖 1-based terminal row (line hasn't been pushed yet)
|
|
747
751
|
let xPos = 1
|
|
@@ -769,7 +773,9 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
769
773
|
themeColors.dim(` • `) +
|
|
770
774
|
hotkey('P', ' Settings') +
|
|
771
775
|
themeColors.dim(` • `) +
|
|
772
|
-
|
|
776
|
+
themeColors.dim('J/K Navigate') +
|
|
777
|
+
themeColors.dim(` • `) +
|
|
778
|
+
themeColors.dim('Ctrl+H Help')
|
|
773
779
|
)
|
|
774
780
|
|
|
775
781
|
// 📖 Line 2: command palette, recommend, feedback, theme
|
|
@@ -803,7 +809,7 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
803
809
|
hotkey('G', ' Theme') + themeColors.dim(` • `) +
|
|
804
810
|
hotkey('I', ' Feedback, bugs & requests')
|
|
805
811
|
)
|
|
806
|
-
// 📖 Proxy status is now shown via the
|
|
812
|
+
// 📖 Proxy status is now shown via the badge in line 2 above — no need for a dedicated line
|
|
807
813
|
const footerLine =
|
|
808
814
|
themeColors.footerLove(' Made with 💖 & ☕ by \x1b]8;;https://github.com/vava-nessa\x1b\\vava-nessa\x1b]8;;\x1b\\') +
|
|
809
815
|
themeColors.dim(' • ') +
|
|
@@ -823,12 +829,17 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
823
829
|
lines.push(footerLine)
|
|
824
830
|
|
|
825
831
|
if (versionStatus.isOutdated) {
|
|
826
|
-
const
|
|
832
|
+
const updateMsg = ` 🚀⬆️ UPDATE AVAILABLE — v${LOCAL_VERSION} → v${versionStatus.latestVersion} • Click here or press Shift+U to update 🚀⬆️ `
|
|
827
833
|
const paddedBanner = terminalCols > 0
|
|
828
|
-
?
|
|
829
|
-
:
|
|
830
|
-
|
|
831
|
-
lines.
|
|
834
|
+
? updateMsg + ' '.repeat(Math.max(0, terminalCols - displayWidth(updateMsg)))
|
|
835
|
+
: updateMsg
|
|
836
|
+
const fluoGreenBanner = chalk.bgRgb(57, 255, 20).rgb(0, 0, 0).bold(paddedBanner)
|
|
837
|
+
const updateBannerRow = lines.length + 1
|
|
838
|
+
_lastLayout.updateBannerRow = updateBannerRow
|
|
839
|
+
footerHotkeys.push({ key: 'update-click', row: updateBannerRow, xStart: 1, xEnd: Math.max(terminalCols, displayWidth(updateMsg)) })
|
|
840
|
+
lines.push(fluoGreenBanner)
|
|
841
|
+
} else {
|
|
842
|
+
_lastLayout.updateBannerRow = 0
|
|
832
843
|
}
|
|
833
844
|
|
|
834
845
|
// 📖 Final footer line: changelog + optional active text-filter badge + exit hint.
|
|
@@ -872,13 +883,18 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
872
883
|
|
|
873
884
|
_lastLayout.footerHotkeys = footerHotkeys
|
|
874
885
|
|
|
886
|
+
const releaseLabel = lastReleaseDate
|
|
887
|
+
? chalk.rgb(255, 182, 193)(`Last release: ${lastReleaseDate}`)
|
|
888
|
+
: ''
|
|
889
|
+
|
|
875
890
|
lines.push(
|
|
876
891
|
' ' + themeColors.hotkey('N') + themeColors.dim(' Changelog') +
|
|
877
892
|
(filterBadge
|
|
878
893
|
? themeColors.dim(' • ') + filterBadge
|
|
879
894
|
: '') +
|
|
880
895
|
themeColors.dim(' • ') +
|
|
881
|
-
themeColors.dim('Ctrl+C Exit')
|
|
896
|
+
themeColors.dim('Ctrl+C Exit') +
|
|
897
|
+
(releaseLabel ? themeColors.dim(' • ') + releaseLabel : '')
|
|
882
898
|
)
|
|
883
899
|
|
|
884
900
|
// 📖 Append \x1b[K (erase to EOL) to each line so leftover chars from previous
|