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 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 || 'avg',
366
- sortDirection: startupProfileSettings?.sortAsc === false ? 'desc' : 'asc',
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
- state.tierFilterMode = startupProfileSettings?.tierFilter ? Math.max(0, TIER_CYCLE.indexOf(startupProfileSettings.tierFilter)) : 0
610
- state.originFilterMode = 0
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.12",
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",
@@ -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
- // 📖 Profile rows start after maintenance + proxy rows — one row per saved profile
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 + 5
888
- const maxRowIdx = savedProfiles.length > 0 ? profileStartIdx + savedProfiles.length - 1 : proxyCleanupRowIdx
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 + 5
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
  }
@@ -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)