free-coding-models 0.2.12 → 0.2.13
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 +17 -0
- package/README.md +2 -0
- package/bin/free-coding-models.js +16 -6
- package/package.json +1 -1
- package/src/key-handler.js +149 -5
- package/src/overlays.js +105 -2
- package/src/render-table.js +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
+
## 0.2.13
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **Persist UI view settings**: Tier filter (T key), provider filter (D key), and sort order now persist across session restarts — settings are saved to `~/.free-coding-models.json` under `config.settings` and automatically restored on next startup. Settings also mirror into active profiles so profile switching captures live view preferences.
|
|
9
|
+
- When T cycles tier: S+ tier is now remembered for next session
|
|
10
|
+
- When D cycles provider: Filtered provider is now remembered
|
|
11
|
+
- When sort keys (R/O/M/L/A/S/C/H/V/B/U) change order: Sort column and direction are now remembered
|
|
12
|
+
- Profile loading has priority over global `config.settings` so saved profiles override global defaults
|
|
13
|
+
- **Reset view settings (Shift+R)**: New keyboard shortcut to instantly reset tier filter, provider filter, and sort order to defaults (All tier, no provider filter, avg sort ascending). Also clears persisted settings from `config.settings` so next restart returns to factory defaults.
|
|
14
|
+
- Useful when you've customized your view but want a fresh start
|
|
15
|
+
- Does not affect favorites, API keys, or other settings — only view state
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- **Help overlay (K key)**: Updated to document new Shift+R keybinding for resetting view settings
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
5
22
|
## 0.2.12
|
|
6
23
|
|
|
7
24
|
### Added
|
package/README.md
CHANGED
|
@@ -80,6 +80,7 @@ By Vanessa Depraute
|
|
|
80
80
|
- **📐 Stability score** — Composite 0–100 score measuring consistency (p95, jitter, spikes, uptime)
|
|
81
81
|
- **📊 Usage tracking** — Monitor remaining quota for each exact provider/model pair when the provider exposes it; otherwise the TUI shows a green dot instead of a misleading percentage.
|
|
82
82
|
- **📜 Request Log Overlay** — Press `X` to inspect recent proxied requests and token usage for exact provider/model pairs.
|
|
83
|
+
- **📋 Changelog Overlay** — Press `N` to browse all versions in an index, then `Enter` to view details for any version with full scroll support
|
|
83
84
|
- **🛠 MODEL_NOT_FOUND Rotation** — If a specific provider returns a 404 for a model, the TUI intelligently rotates through other available providers for the same model.
|
|
84
85
|
- **🔄 Auto-retry** — Timeout models keep getting retried, nothing is ever "given up on"
|
|
85
86
|
- **🎮 Interactive selection** — Navigate with arrow keys directly in the table, press Enter to act
|
|
@@ -931,6 +932,7 @@ This script:
|
|
|
931
932
|
- **Shift+P** — Cycle through saved profiles (switches live TUI settings)
|
|
932
933
|
- **Shift+S** — Save current TUI settings as a named profile (inline prompt)
|
|
933
934
|
- **Q** — Open Smart Recommend overlay (find the best model for your task)
|
|
935
|
+
- **N** — Open Changelog overlay (browse index of all versions, `Enter` to view details, `B` to go back)
|
|
934
936
|
- **W** — Cycle ping mode (`FAST` 2s → `NORMAL` 10s → `SLOW` 30s → `FORCED` 4s)
|
|
935
937
|
- **J / I** — Request feature / Report bug
|
|
936
938
|
- **K / Esc** — Show help overlay / Close overlay
|
|
@@ -362,8 +362,8 @@ async function main() {
|
|
|
362
362
|
frame: 0,
|
|
363
363
|
cursor: 0,
|
|
364
364
|
selectedModel: null,
|
|
365
|
-
sortColumn: startupProfileSettings?.sortColumn
|
|
366
|
-
sortDirection: startupProfileSettings?.sortAsc
|
|
365
|
+
sortColumn: startupProfileSettings?.sortColumn ?? config.settings?.sortColumn ?? 'avg',
|
|
366
|
+
sortDirection: (startupProfileSettings?.sortAsc ?? config.settings?.sortAsc ?? true) ? 'asc' : 'desc',
|
|
367
367
|
pingInterval: PING_MODE_INTERVALS.speed, // 📖 Effective live interval derived from the active ping mode.
|
|
368
368
|
pingMode: 'speed', // 📖 Current ping mode: speed | normal | slow | forced.
|
|
369
369
|
pingModeSource: 'startup', // 📖 Why this mode is active: startup | manual | auto | idle | activity.
|
|
@@ -444,6 +444,12 @@ async function main() {
|
|
|
444
444
|
logVisible: false, // 📖 Whether the log page overlay is active
|
|
445
445
|
logScrollOffset: 0, // 📖 Vertical scroll offset for log overlay viewport
|
|
446
446
|
logShowAll: false, // 📖 Show all logs (true) or limited to 500 (false)
|
|
447
|
+
// 📖 Changelog overlay state (N key opens it)
|
|
448
|
+
changelogOpen: false, // 📖 Whether the changelog overlay is active
|
|
449
|
+
changelogScrollOffset: 0, // 📖 Vertical scroll offset for changelog overlay viewport
|
|
450
|
+
changelogPhase: 'index', // 📖 'index' (all versions) | 'details' (specific version)
|
|
451
|
+
changelogCursor: 0, // 📖 Selected row in index phase
|
|
452
|
+
changelogSelectedVersion: null, // 📖 Which version to show details for
|
|
447
453
|
// 📖 Proxy startup status — set by autoStartProxyIfSynced, consumed by Task 3 indicator
|
|
448
454
|
// 📖 null = not configured/not attempted
|
|
449
455
|
// 📖 { phase: 'starting' } — proxy start in progress
|
|
@@ -606,8 +612,10 @@ async function main() {
|
|
|
606
612
|
|
|
607
613
|
// 📖 originFilterMode: index into ORIGIN_CYCLE, 0=All, then each provider key in order
|
|
608
614
|
const ORIGIN_CYCLE = [null, ...Object.keys(sources)]
|
|
609
|
-
|
|
610
|
-
state.
|
|
615
|
+
const resolvedTierFilter = startupProfileSettings?.tierFilter ?? config.settings?.tierFilter
|
|
616
|
+
state.tierFilterMode = resolvedTierFilter ? Math.max(0, TIER_CYCLE.indexOf(resolvedTierFilter)) : 0
|
|
617
|
+
const resolvedOriginFilter = config.settings?.originFilter
|
|
618
|
+
state.originFilterMode = resolvedOriginFilter ? Math.max(0, ORIGIN_CYCLE.indexOf(resolvedOriginFilter)) : 0
|
|
611
619
|
|
|
612
620
|
function applyTierFilter() {
|
|
613
621
|
const activeTier = TIER_CYCLE[state.tierFilterMode]
|
|
@@ -771,12 +779,12 @@ async function main() {
|
|
|
771
779
|
process.stdin.on('keypress', onKeyPress)
|
|
772
780
|
process.on('SIGCONT', noteUserActivity)
|
|
773
781
|
|
|
774
|
-
// 📖 Animation loop: render settings overlay, recommend overlay, help overlay, feature request overlay, bug report overlay, OR main table
|
|
782
|
+
// 📖 Animation loop: render settings overlay, recommend overlay, help overlay, feature request overlay, bug report overlay, changelog overlay, OR main table
|
|
775
783
|
ticker = setInterval(() => {
|
|
776
784
|
refreshAutoPingMode()
|
|
777
785
|
state.frame++
|
|
778
786
|
// 📖 Cache visible+sorted models each frame so Enter handler always matches the display
|
|
779
|
-
if (!state.settingsOpen && !state.installEndpointsOpen && !state.recommendOpen && !state.featureRequestOpen && !state.bugReportOpen) {
|
|
787
|
+
if (!state.settingsOpen && !state.installEndpointsOpen && !state.recommendOpen && !state.featureRequestOpen && !state.bugReportOpen && !state.changelogOpen) {
|
|
780
788
|
const visible = state.results.filter(r => !r.hidden)
|
|
781
789
|
state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection)
|
|
782
790
|
}
|
|
@@ -794,6 +802,8 @@ async function main() {
|
|
|
794
802
|
? overlays.renderHelp()
|
|
795
803
|
: state.logVisible
|
|
796
804
|
? overlays.renderLog()
|
|
805
|
+
: state.changelogOpen
|
|
806
|
+
? overlays.renderChangelog()
|
|
797
807
|
: renderTable(state.results, state.pendingPings, state.frame, state.cursor, state.sortColumn, state.sortDirection, state.pingInterval, state.lastPingTime, state.mode, state.tierFilterMode, state.scrollOffset, state.terminalRows, state.terminalCols, state.originFilterMode, state.activeProfile, state.profileSaveMode, state.profileSaveBuffer, state.proxyStartupStatus, state.pingMode, state.pingModeSource, state.hideUnconfiguredModels, state.widthWarningStartedAt, state.widthWarningDismissed, state.settingsUpdateState, state.settingsUpdateLatestVersion, getProxySettings(state.config).enabled === true, state.isOutdated, state.latestVersion)
|
|
798
808
|
process.stdout.write(ALT_HOME + content)
|
|
799
809
|
}, Math.round(1000 / FPS))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "free-coding-models",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.13",
|
|
4
4
|
"description": "Find the fastest coding LLM models in seconds — ping free models from multiple providers, pick the best one for OpenCode, Cursor, or any AI coding assistant.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"nvidia",
|
package/src/key-handler.js
CHANGED
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
* @exports { buildProviderModelsUrl, parseProviderModelIds, listProviderTestModels, classifyProviderTestOutcome, buildProviderTestDetail, createKeyHandler }
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
|
+
import { loadChangelog } from './changelog-loader.js'
|
|
29
|
+
|
|
28
30
|
// 📖 Some providers need an explicit probe model because the first catalog entry
|
|
29
31
|
// 📖 is not guaranteed to be accepted by their chat endpoint.
|
|
30
32
|
const PROVIDER_TEST_MODEL_OVERRIDES = {
|
|
@@ -766,6 +768,70 @@ export function createKeyHandler(ctx) {
|
|
|
766
768
|
return
|
|
767
769
|
}
|
|
768
770
|
|
|
771
|
+
// 📖 Changelog overlay: two-phase (index + details) with keyboard navigation
|
|
772
|
+
if (state.changelogOpen) {
|
|
773
|
+
const pageStep = Math.max(1, (state.terminalRows || 1) - 2)
|
|
774
|
+
const changelogData = loadChangelog()
|
|
775
|
+
const { versions } = changelogData
|
|
776
|
+
const versionList = Object.keys(versions).sort((a, b) => {
|
|
777
|
+
const aParts = a.split('.').map(Number)
|
|
778
|
+
const bParts = b.split('.').map(Number)
|
|
779
|
+
for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
|
|
780
|
+
const aVal = aParts[i] || 0
|
|
781
|
+
const bVal = bParts[i] || 0
|
|
782
|
+
if (bVal !== aVal) return bVal - aVal
|
|
783
|
+
}
|
|
784
|
+
return 0
|
|
785
|
+
})
|
|
786
|
+
|
|
787
|
+
// 📖 Close changelog overlay
|
|
788
|
+
if (key.name === 'escape' || key.name === 'n') {
|
|
789
|
+
state.changelogOpen = false
|
|
790
|
+
state.changelogPhase = 'index'
|
|
791
|
+
state.changelogCursor = 0
|
|
792
|
+
state.changelogSelectedVersion = null
|
|
793
|
+
return
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
if (state.changelogPhase === 'index') {
|
|
797
|
+
// 📖 INDEX PHASE: Navigate through versions
|
|
798
|
+
if (key.name === 'up') {
|
|
799
|
+
state.changelogCursor = Math.max(0, state.changelogCursor - 1)
|
|
800
|
+
return
|
|
801
|
+
}
|
|
802
|
+
if (key.name === 'down') {
|
|
803
|
+
state.changelogCursor = Math.min(versionList.length - 1, state.changelogCursor + 1)
|
|
804
|
+
return
|
|
805
|
+
}
|
|
806
|
+
if (key.name === 'home') { state.changelogCursor = 0; return }
|
|
807
|
+
if (key.name === 'end') { state.changelogCursor = versionList.length - 1; return }
|
|
808
|
+
if (key.name === 'return') {
|
|
809
|
+
// 📖 Enter details phase for selected version
|
|
810
|
+
state.changelogPhase = 'details'
|
|
811
|
+
state.changelogSelectedVersion = versionList[state.changelogCursor]
|
|
812
|
+
state.changelogScrollOffset = 0
|
|
813
|
+
return
|
|
814
|
+
}
|
|
815
|
+
} else if (state.changelogPhase === 'details') {
|
|
816
|
+
// 📖 DETAILS PHASE: Scroll through selected version details
|
|
817
|
+
if (key.name === 'b') {
|
|
818
|
+
// 📖 B = back to index
|
|
819
|
+
state.changelogPhase = 'index'
|
|
820
|
+
state.changelogScrollOffset = 0
|
|
821
|
+
return
|
|
822
|
+
}
|
|
823
|
+
if (key.name === 'up') { state.changelogScrollOffset = Math.max(0, state.changelogScrollOffset - 1); return }
|
|
824
|
+
if (key.name === 'down') { state.changelogScrollOffset += 1; return }
|
|
825
|
+
if (key.name === 'pageup') { state.changelogScrollOffset = Math.max(0, state.changelogScrollOffset - pageStep); return }
|
|
826
|
+
if (key.name === 'pagedown') { state.changelogScrollOffset += pageStep; return }
|
|
827
|
+
if (key.name === 'home') { state.changelogScrollOffset = 0; return }
|
|
828
|
+
if (key.name === 'end') { state.changelogScrollOffset = Number.MAX_SAFE_INTEGER; return }
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
if (key.ctrl && key.name === 'c') { exit(0); return }
|
|
832
|
+
return
|
|
833
|
+
}
|
|
834
|
+
|
|
769
835
|
// 📖 Smart Recommend overlay: full keyboard handling while overlay is open.
|
|
770
836
|
if (state.recommendOpen) {
|
|
771
837
|
if (key.ctrl && key.name === 'c') { exit(0); return }
|
|
@@ -882,10 +948,11 @@ export function createKeyHandler(ctx) {
|
|
|
882
948
|
const proxySyncRowIdx = updateRowIdx + 2
|
|
883
949
|
const proxyPortRowIdx = updateRowIdx + 3
|
|
884
950
|
const proxyCleanupRowIdx = updateRowIdx + 4
|
|
885
|
-
|
|
951
|
+
const changelogViewRowIdx = updateRowIdx + 5
|
|
952
|
+
// 📖 Profile rows start after maintenance + proxy rows + changelog row — one row per saved profile
|
|
886
953
|
const savedProfiles = listProfiles(state.config)
|
|
887
|
-
const profileStartIdx = updateRowIdx +
|
|
888
|
-
const maxRowIdx = savedProfiles.length > 0 ? profileStartIdx + savedProfiles.length - 1 :
|
|
954
|
+
const profileStartIdx = updateRowIdx + 6
|
|
955
|
+
const maxRowIdx = savedProfiles.length > 0 ? profileStartIdx + savedProfiles.length - 1 : changelogViewRowIdx
|
|
889
956
|
|
|
890
957
|
// 📖 Edit/Add-key mode: capture typed characters for the API key
|
|
891
958
|
if (state.settingsEditMode || state.settingsAddKeyMode) {
|
|
@@ -1059,6 +1126,17 @@ export function createKeyHandler(ctx) {
|
|
|
1059
1126
|
return
|
|
1060
1127
|
}
|
|
1061
1128
|
|
|
1129
|
+
// 📖 Changelog row: Enter → open changelog overlay
|
|
1130
|
+
if (state.settingsCursor === changelogViewRowIdx) {
|
|
1131
|
+
state.settingsOpen = false
|
|
1132
|
+
state.changelogOpen = true
|
|
1133
|
+
state.changelogPhase = 'index'
|
|
1134
|
+
state.changelogCursor = 0
|
|
1135
|
+
state.changelogSelectedVersion = null
|
|
1136
|
+
state.changelogScrollOffset = 0
|
|
1137
|
+
return
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1062
1140
|
// 📖 Profile row: Enter → load the selected profile (apply its settings live)
|
|
1063
1141
|
if (state.settingsCursor >= profileStartIdx && savedProfiles.length > 0) {
|
|
1064
1142
|
const profileIdx = state.settingsCursor - profileStartIdx
|
|
@@ -1094,7 +1172,7 @@ export function createKeyHandler(ctx) {
|
|
|
1094
1172
|
}
|
|
1095
1173
|
|
|
1096
1174
|
if (key.name === 'space') {
|
|
1097
|
-
if (state.settingsCursor === updateRowIdx || state.settingsCursor === proxyPortRowIdx || state.settingsCursor === proxyCleanupRowIdx) return
|
|
1175
|
+
if (state.settingsCursor === updateRowIdx || state.settingsCursor === proxyPortRowIdx || state.settingsCursor === proxyCleanupRowIdx || state.settingsCursor === changelogViewRowIdx) return
|
|
1098
1176
|
// 📖 Profile rows don't respond to Space
|
|
1099
1177
|
if (state.settingsCursor >= profileStartIdx) return
|
|
1100
1178
|
|
|
@@ -1125,7 +1203,7 @@ export function createKeyHandler(ctx) {
|
|
|
1125
1203
|
}
|
|
1126
1204
|
|
|
1127
1205
|
if (key.name === 't') {
|
|
1128
|
-
if (state.settingsCursor === updateRowIdx) return
|
|
1206
|
+
if (state.settingsCursor === updateRowIdx || state.settingsCursor === proxyPortRowIdx || state.settingsCursor === proxyCleanupRowIdx || state.settingsCursor === changelogViewRowIdx) return
|
|
1129
1207
|
// 📖 Profile rows don't respond to T (test key)
|
|
1130
1208
|
if (state.settingsCursor >= profileStartIdx) return
|
|
1131
1209
|
|
|
@@ -1333,9 +1411,60 @@ export function createKeyHandler(ctx) {
|
|
|
1333
1411
|
return
|
|
1334
1412
|
}
|
|
1335
1413
|
|
|
1414
|
+
// 📖 Helper: persist current UI view settings (tier, provider, sort) to config.settings
|
|
1415
|
+
// 📖 Called after every T / D / sort key so preferences survive session restarts.
|
|
1416
|
+
function persistUiSettings() {
|
|
1417
|
+
if (!state.config.settings || typeof state.config.settings !== 'object') state.config.settings = {}
|
|
1418
|
+
state.config.settings.tierFilter = TIER_CYCLE[state.tierFilterMode]
|
|
1419
|
+
state.config.settings.originFilter = ORIGIN_CYCLE[state.originFilterMode] ?? null
|
|
1420
|
+
state.config.settings.sortColumn = state.sortColumn
|
|
1421
|
+
state.config.settings.sortAsc = state.sortDirection === 'asc'
|
|
1422
|
+
// 📖 Mirror into active profile too so profile captures live preferences
|
|
1423
|
+
if (state.activeProfile && state.config.profiles?.[state.activeProfile]) {
|
|
1424
|
+
const profile = state.config.profiles[state.activeProfile]
|
|
1425
|
+
if (!profile.settings || typeof profile.settings !== 'object') profile.settings = {}
|
|
1426
|
+
profile.settings.tierFilter = state.config.settings.tierFilter
|
|
1427
|
+
profile.settings.originFilter = state.config.settings.originFilter
|
|
1428
|
+
profile.settings.sortColumn = state.config.settings.sortColumn
|
|
1429
|
+
profile.settings.sortAsc = state.config.settings.sortAsc
|
|
1430
|
+
}
|
|
1431
|
+
saveConfig(state.config)
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
// 📖 Shift+R: reset all UI view settings to defaults (tier, sort, provider) and clear persisted config
|
|
1435
|
+
if (key.name === 'r' && key.shift) {
|
|
1436
|
+
state.tierFilterMode = 0
|
|
1437
|
+
state.originFilterMode = 0
|
|
1438
|
+
state.sortColumn = 'avg'
|
|
1439
|
+
state.sortDirection = 'asc'
|
|
1440
|
+
if (!state.config.settings || typeof state.config.settings !== 'object') state.config.settings = {}
|
|
1441
|
+
delete state.config.settings.tierFilter
|
|
1442
|
+
delete state.config.settings.originFilter
|
|
1443
|
+
delete state.config.settings.sortColumn
|
|
1444
|
+
delete state.config.settings.sortAsc
|
|
1445
|
+
// 📖 Also clear in active profile if loaded
|
|
1446
|
+
if (state.activeProfile && state.config.profiles?.[state.activeProfile]) {
|
|
1447
|
+
const profile = state.config.profiles[state.activeProfile]
|
|
1448
|
+
if (profile.settings) {
|
|
1449
|
+
delete profile.settings.tierFilter
|
|
1450
|
+
delete profile.settings.originFilter
|
|
1451
|
+
delete profile.settings.sortColumn
|
|
1452
|
+
delete profile.settings.sortAsc
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
saveConfig(state.config)
|
|
1456
|
+
applyTierFilter()
|
|
1457
|
+
const visible = state.results.filter(r => !r.hidden)
|
|
1458
|
+
state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection)
|
|
1459
|
+
state.cursor = 0
|
|
1460
|
+
state.scrollOffset = 0
|
|
1461
|
+
return
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1336
1464
|
// 📖 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
|
|
1337
1465
|
// 📖 T is reserved for tier filter cycling. Y now opens the install-endpoints flow.
|
|
1338
1466
|
// 📖 D is now reserved for provider filter cycling
|
|
1467
|
+
// 📖 Shift+R is reserved for reset view settings
|
|
1339
1468
|
const sortKeys = {
|
|
1340
1469
|
'r': 'rank', 'o': 'origin', 'm': 'model',
|
|
1341
1470
|
'l': 'ping', 'a': 'avg', 's': 'swe', 'c': 'ctx', 'h': 'condition', 'v': 'verdict', 'b': 'stability', 'u': 'uptime', 'g': 'usage'
|
|
@@ -1355,6 +1484,7 @@ export function createKeyHandler(ctx) {
|
|
|
1355
1484
|
state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection)
|
|
1356
1485
|
state.cursor = 0
|
|
1357
1486
|
state.scrollOffset = 0
|
|
1487
|
+
persistUiSettings()
|
|
1358
1488
|
return
|
|
1359
1489
|
}
|
|
1360
1490
|
|
|
@@ -1440,6 +1570,7 @@ export function createKeyHandler(ctx) {
|
|
|
1440
1570
|
state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection)
|
|
1441
1571
|
state.cursor = 0
|
|
1442
1572
|
state.scrollOffset = 0
|
|
1573
|
+
persistUiSettings()
|
|
1443
1574
|
return
|
|
1444
1575
|
}
|
|
1445
1576
|
|
|
@@ -1452,6 +1583,7 @@ export function createKeyHandler(ctx) {
|
|
|
1452
1583
|
state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection)
|
|
1453
1584
|
state.cursor = 0
|
|
1454
1585
|
state.scrollOffset = 0
|
|
1586
|
+
persistUiSettings()
|
|
1455
1587
|
return
|
|
1456
1588
|
}
|
|
1457
1589
|
|
|
@@ -1462,6 +1594,18 @@ export function createKeyHandler(ctx) {
|
|
|
1462
1594
|
return
|
|
1463
1595
|
}
|
|
1464
1596
|
|
|
1597
|
+
// 📖 Changelog overlay key: N = toggle changelog overlay
|
|
1598
|
+
if (key.name === 'n') {
|
|
1599
|
+
state.changelogOpen = !state.changelogOpen
|
|
1600
|
+
if (state.changelogOpen) {
|
|
1601
|
+
state.changelogScrollOffset = 0
|
|
1602
|
+
state.changelogPhase = 'index'
|
|
1603
|
+
state.changelogCursor = 0
|
|
1604
|
+
state.changelogSelectedVersion = null
|
|
1605
|
+
}
|
|
1606
|
+
return
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1465
1609
|
// 📖 Mode toggle key: Z cycles through the supported tool targets.
|
|
1466
1610
|
if (key.name === 'z') {
|
|
1467
1611
|
const modeOrder = getToolModeOrder()
|
package/src/overlays.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* @details
|
|
6
6
|
* This module centralizes all overlay rendering in one place:
|
|
7
|
-
* - Settings, Install Endpoints, Help, Log, Smart Recommend, Feature Request, Bug Report
|
|
7
|
+
* - Settings, Install Endpoints, Help, Log, Smart Recommend, Feature Request, Bug Report, Changelog
|
|
8
8
|
* - Settings diagnostics for provider key tests, including wrapped retry/error details
|
|
9
9
|
* - Recommend analysis timer orchestration and progress updates
|
|
10
10
|
*
|
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
* @exports { createOverlayRenderers }
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
+
import { loadChangelog } from './changelog-loader.js'
|
|
21
|
+
|
|
20
22
|
export function createOverlayRenderers(state, deps) {
|
|
21
23
|
const {
|
|
22
24
|
chalk,
|
|
@@ -138,6 +140,7 @@ export function createOverlayRenderers(state, deps) {
|
|
|
138
140
|
const proxySyncRowIdx = updateRowIdx + 2
|
|
139
141
|
const proxyPortRowIdx = updateRowIdx + 3
|
|
140
142
|
const proxyCleanupRowIdx = updateRowIdx + 4
|
|
143
|
+
const changelogViewRowIdx = updateRowIdx + 5
|
|
141
144
|
const proxySettings = getProxySettings(state.config)
|
|
142
145
|
const EL = '\x1b[K'
|
|
143
146
|
const lines = []
|
|
@@ -296,9 +299,15 @@ export function createOverlayRenderers(state, deps) {
|
|
|
296
299
|
cursorLineByRow[proxyCleanupRowIdx] = lines.length
|
|
297
300
|
lines.push(state.settingsCursor === proxyCleanupRowIdx ? chalk.bgRgb(45, 30, 30)(proxyCleanupRow) : proxyCleanupRow)
|
|
298
301
|
|
|
302
|
+
// 📖 Changelog viewer row
|
|
303
|
+
const changelogViewBullet = state.settingsCursor === changelogViewRowIdx ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
304
|
+
const changelogViewRow = `${changelogViewBullet}${chalk.bold('View Changelog').padEnd(44)} ${chalk.dim('Enter browse version history')}`
|
|
305
|
+
cursorLineByRow[changelogViewRowIdx] = lines.length
|
|
306
|
+
lines.push(state.settingsCursor === changelogViewRowIdx ? chalk.bgRgb(30, 45, 30)(changelogViewRow) : changelogViewRow)
|
|
307
|
+
|
|
299
308
|
// 📖 Profiles section — list saved profiles with active indicator + delete support
|
|
300
309
|
const savedProfiles = listProfiles(state.config)
|
|
301
|
-
const profileStartIdx = updateRowIdx +
|
|
310
|
+
const profileStartIdx = updateRowIdx + 6
|
|
302
311
|
const maxRowIdx = savedProfiles.length > 0 ? profileStartIdx + savedProfiles.length - 1 : updateRowIdx
|
|
303
312
|
|
|
304
313
|
lines.push('')
|
|
@@ -610,6 +619,8 @@ export function createOverlayRenderers(state, deps) {
|
|
|
610
619
|
lines.push(` ${chalk.yellow('Shift+S')} Save current config as a named profile ${chalk.dim('(inline prompt — type name + Enter)')}`)
|
|
611
620
|
lines.push(` ${chalk.dim('Profiles store: favorites, sort, tier filter, ping interval, configured-only filter, API keys.')}`)
|
|
612
621
|
lines.push(` ${chalk.dim('Use --profile <name> to load a profile on startup.')}`)
|
|
622
|
+
lines.push(` ${chalk.yellow('Shift+R')} Reset view settings ${chalk.dim('(tier filter, sort, provider filter → defaults)')}`)
|
|
623
|
+
lines.push(` ${chalk.yellow('N')} Changelog ${chalk.dim('(📋 browse all versions, Enter to view details)')}`)
|
|
613
624
|
lines.push(` ${chalk.yellow('K')} / ${chalk.yellow('Esc')} Show/hide this help`)
|
|
614
625
|
lines.push(` ${chalk.yellow('Ctrl+C')} Exit`)
|
|
615
626
|
lines.push('')
|
|
@@ -1239,6 +1250,97 @@ export function createOverlayRenderers(state, deps) {
|
|
|
1239
1250
|
return cleared.join('\n')
|
|
1240
1251
|
}
|
|
1241
1252
|
|
|
1253
|
+
// ─── Changelog overlay renderer ───────────────────────────────────────────
|
|
1254
|
+
// 📖 renderChangelog: Two-phase overlay — index of all versions or details of one version
|
|
1255
|
+
function renderChangelog() {
|
|
1256
|
+
const EL = '\x1b[K'
|
|
1257
|
+
const lines = []
|
|
1258
|
+
const changelogData = loadChangelog()
|
|
1259
|
+
const { versions } = changelogData
|
|
1260
|
+
const versionList = Object.keys(versions).sort((a, b) => {
|
|
1261
|
+
const aParts = a.split('.').map(Number)
|
|
1262
|
+
const bParts = b.split('.').map(Number)
|
|
1263
|
+
for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
|
|
1264
|
+
const aVal = aParts[i] || 0
|
|
1265
|
+
const bVal = bParts[i] || 0
|
|
1266
|
+
if (bVal !== aVal) return bVal - aVal
|
|
1267
|
+
}
|
|
1268
|
+
return 0
|
|
1269
|
+
})
|
|
1270
|
+
|
|
1271
|
+
// 📖 Branding header
|
|
1272
|
+
lines.push(` ${chalk.cyanBright('🚀')} ${chalk.bold.cyanBright('free-coding-models')} ${chalk.dim(`v${LOCAL_VERSION}`)}`)
|
|
1273
|
+
|
|
1274
|
+
if (state.changelogPhase === 'index') {
|
|
1275
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1276
|
+
// 📖 INDEX PHASE: Show all versions with selection
|
|
1277
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1278
|
+
lines.push(` ${chalk.bold('📋 Changelog - All Versions')}`)
|
|
1279
|
+
lines.push(` ${chalk.dim('— ↑↓ navigate • Enter select • Esc close')}`)
|
|
1280
|
+
lines.push('')
|
|
1281
|
+
|
|
1282
|
+
for (let i = 0; i < versionList.length; i++) {
|
|
1283
|
+
const version = versionList[i]
|
|
1284
|
+
const changes = versions[version]
|
|
1285
|
+
const isSelected = i === state.changelogCursor
|
|
1286
|
+
|
|
1287
|
+
// 📖 Count items in this version
|
|
1288
|
+
let itemCount = 0
|
|
1289
|
+
for (const key of ['added', 'fixed', 'changed', 'updated']) {
|
|
1290
|
+
if (changes[key]) itemCount += changes[key].length
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
// 📖 Format version line with selection highlight
|
|
1294
|
+
const versionStr = ` v${version.padEnd(8)} — ${itemCount} ${itemCount === 1 ? 'change' : 'changes'}`
|
|
1295
|
+
if (isSelected) {
|
|
1296
|
+
lines.push(chalk.inverse(versionStr))
|
|
1297
|
+
} else {
|
|
1298
|
+
lines.push(versionStr)
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
lines.push('')
|
|
1303
|
+
lines.push(` ${chalk.dim(`Total: ${versionList.length} versions`)}`)
|
|
1304
|
+
|
|
1305
|
+
} else if (state.changelogPhase === 'details') {
|
|
1306
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1307
|
+
// 📖 DETAILS PHASE: Show detailed changes for selected version
|
|
1308
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1309
|
+
lines.push(` ${chalk.bold(`📋 v${state.changelogSelectedVersion}`)}`)
|
|
1310
|
+
lines.push(` ${chalk.dim('— ↑↓ / PgUp / PgDn scroll • B back • Esc close')}`)
|
|
1311
|
+
lines.push('')
|
|
1312
|
+
|
|
1313
|
+
const changes = versions[state.changelogSelectedVersion]
|
|
1314
|
+
if (changes) {
|
|
1315
|
+
const sections = { added: '✨ Added', fixed: '🐛 Fixed', changed: '🔄 Changed', updated: '📝 Updated' }
|
|
1316
|
+
for (const [key, label] of Object.entries(sections)) {
|
|
1317
|
+
if (changes[key] && changes[key].length > 0) {
|
|
1318
|
+
lines.push(` ${chalk.yellow(label)}`)
|
|
1319
|
+
for (const item of changes[key]) {
|
|
1320
|
+
// 📖 Unwrap markdown bold/code markers for display
|
|
1321
|
+
let displayText = item.replace(/\*\*([^*]+)\*\*/g, '$1').replace(/`([^`]+)`/g, '$1')
|
|
1322
|
+
// 📖 Wrap long lines
|
|
1323
|
+
const maxWidth = state.terminalCols - 16
|
|
1324
|
+
if (displayText.length > maxWidth) {
|
|
1325
|
+
displayText = displayText.substring(0, maxWidth - 3) + '…'
|
|
1326
|
+
}
|
|
1327
|
+
lines.push(` • ${displayText}`)
|
|
1328
|
+
}
|
|
1329
|
+
lines.push('')
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
// 📖 Use scrolling with overlay handler
|
|
1336
|
+
const CHANGELOG_OVERLAY_BG = chalk.bgRgb(10, 40, 80) // Dark blue background
|
|
1337
|
+
const { visible, offset } = sliceOverlayLines(lines, state.changelogScrollOffset, state.terminalRows)
|
|
1338
|
+
state.changelogScrollOffset = offset
|
|
1339
|
+
const tintedLines = tintOverlayLines(visible, CHANGELOG_OVERLAY_BG, state.terminalCols)
|
|
1340
|
+
const cleared = tintedLines.map(l => l + EL)
|
|
1341
|
+
return cleared.join('\n')
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1242
1344
|
// 📖 stopRecommendAnalysis: cleanup timers if user cancels during analysis
|
|
1243
1345
|
function stopRecommendAnalysis() {
|
|
1244
1346
|
if (state.recommendAnalysisTimer) { clearInterval(state.recommendAnalysisTimer); state.recommendAnalysisTimer = null }
|
|
@@ -1253,6 +1355,7 @@ export function createOverlayRenderers(state, deps) {
|
|
|
1253
1355
|
renderRecommend,
|
|
1254
1356
|
renderFeatureRequest,
|
|
1255
1357
|
renderBugReport,
|
|
1358
|
+
renderChangelog,
|
|
1256
1359
|
startRecommendAnalysis,
|
|
1257
1360
|
stopRecommendAnalysis,
|
|
1258
1361
|
}
|
package/src/render-table.js
CHANGED
|
@@ -672,6 +672,8 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
672
672
|
chalk.dim(' → ') +
|
|
673
673
|
chalk.rgb(200, 150, 255)('https://discord.gg/ZTNFHvvCkU') +
|
|
674
674
|
chalk.dim(' • ') +
|
|
675
|
+
chalk.yellow('N') + chalk.dim(' Changelog') +
|
|
676
|
+
chalk.dim(' • ') +
|
|
675
677
|
chalk.dim('Ctrl+C Exit')
|
|
676
678
|
}
|
|
677
679
|
lines.push(footerLine)
|