free-coding-models 0.1.22 → 0.1.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.
@@ -109,14 +109,19 @@ function runUpdate(latestVersion) {
109
109
  console.log()
110
110
  console.log(chalk.green(' ✅ Update complete! Version ' + latestVersion + ' installed.'))
111
111
  console.log()
112
- console.log(chalk.dim(' 📝 Please restart free-coding-models to use the new version.'))
112
+ console.log(chalk.dim(' 🔄 Restarting with new version...'))
113
113
  console.log()
114
+
115
+ // 📖 Relaunch automatically with the same arguments
116
+ const args = process.argv.slice(2)
117
+ execSync(`node bin/free-coding-models.js ${args.join(' ')}`, { stdio: 'inherit' })
118
+ process.exit(0)
114
119
  } catch (err) {
115
120
  console.log()
116
121
  console.log(chalk.red(' ✖ Update failed. Try manually: npm i -g free-coding-models@' + latestVersion))
117
122
  console.log()
118
123
  }
119
- process.exit(0)
124
+ process.exit(1)
120
125
  }
121
126
 
122
127
  // ─── Config path ──────────────────────────────────────────────────────────────
@@ -165,73 +170,62 @@ async function promptApiKey() {
165
170
  })
166
171
  }
167
172
 
168
- // ─── Startup mode selection menu ──────────────────────────────────────────────
169
- // 📖 Shown at startup when neither --opencode nor --openclaw flag is given.
170
- // 📖 Simple arrow-key selector in normal terminal (not alt screen).
171
- // 📖 Returns 'opencode', 'openclaw', or 'update'.
172
- async function promptModeSelection(latestVersion) {
173
- const options = [
174
- {
175
- label: 'OpenCode CLI',
176
- icon: '💻',
177
- description: 'Press Enter on a model → launch OpenCode CLI with it as default',
178
- },
179
- {
180
- label: 'OpenCode Desktop',
181
- icon: '🖥',
182
- description: 'Press Enter on a model → set model & open OpenCode Desktop app',
183
- },
184
- {
185
- label: 'OpenClaw',
186
- icon: '🦞',
187
- description: 'Press Enter on a model → set it as default in OpenClaw config',
188
- },
189
- ]
190
-
191
- if (latestVersion) {
192
- options.push({
193
- label: 'Update now',
194
- icon: '⬆',
195
- description: `Update free-coding-models to v${latestVersion}`,
196
- })
197
- }
198
-
199
- // 📖 Add "Read Changelogs" option when an update is available or was just updated
200
- if (latestVersion) {
201
- options.push({
202
- label: 'Read Changelogs',
203
- icon: '📋',
204
- description: 'Open local CHANGELOG.md file',
205
- })
206
- }
173
+ // ─── Update notification menu ──────────────────────────────────────────────
174
+ // 📖 Shown ONLY when a new version is available, to prompt user to update
175
+ // 📖 Centered, clean presentation that doesn't block normal usage
176
+ // 📖 Returns 'update', 'changelogs', or null to continue without update
177
+ async function promptUpdateNotification(latestVersion) {
178
+ if (!latestVersion) return null
207
179
 
208
180
  return new Promise((resolve) => {
209
181
  let selected = 0
210
-
211
- // 📖 Render the menu to stdout (clear + redraw)
182
+ const options = [
183
+ {
184
+ label: 'Update now',
185
+ icon: '⬆',
186
+ description: `Update free-coding-models to v${latestVersion}`,
187
+ },
188
+ {
189
+ label: 'Read Changelogs',
190
+ icon: '📋',
191
+ description: 'Open GitHub changelog',
192
+ },
193
+ {
194
+ label: 'Continue without update',
195
+ icon: '▶',
196
+ description: 'Use current version',
197
+ },
198
+ ]
199
+
200
+ // 📖 Centered render function
212
201
  const render = () => {
213
202
  process.stdout.write('\x1b[2J\x1b[H') // clear screen + cursor home
203
+
204
+ // 📖 Calculate centering
205
+ const terminalWidth = process.stdout.columns || 80
206
+ const maxWidth = Math.min(terminalWidth - 4, 70)
207
+ const centerPad = ' '.repeat(Math.max(0, Math.floor((terminalWidth - maxWidth) / 2)))
208
+
214
209
  console.log()
215
- if (latestVersion) {
216
- console.log(chalk.red(` New version available (v${latestVersion}), please run npm i -g free-coding-models to install`))
217
- console.log()
218
- }
219
- console.log(chalk.bold(' ⚡ Free Coding Models') + chalk.dim(` v${LOCAL_VERSION} — Choose your tool`))
210
+ console.log(centerPad + chalk.bold.red(' ⚠ UPDATE AVAILABLE'))
211
+ console.log(centerPad + chalk.red(` Version ${latestVersion} is ready to install`))
220
212
  console.log()
221
- console.log(chalk.yellow.bold(' ⚠️ Warning: ') + chalk.yellow('Small terminals may break the layout — maximize your window for best results!'))
213
+ console.log(centerPad + chalk.bold(' Free Coding Models') + chalk.dim(` v${LOCAL_VERSION}`))
222
214
  console.log()
215
+
223
216
  for (let i = 0; i < options.length; i++) {
224
217
  const isSelected = i === selected
225
218
  const bullet = isSelected ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
226
219
  const label = isSelected
227
220
  ? chalk.bold.white(options[i].icon + ' ' + options[i].label)
228
221
  : chalk.dim(options[i].icon + ' ' + options[i].label)
229
- const desc = chalk.dim(' ' + options[i].description)
230
- console.log(bullet + label)
231
- console.log(chalk.dim(' ' + options[i].description))
222
+
223
+ console.log(centerPad + bullet + label)
224
+ console.log(centerPad + chalk.dim(' ' + options[i].description))
232
225
  console.log()
233
226
  }
234
- console.log(chalk.dim(' ↑↓ Navigate • Enter Select • Ctrl+C Exit'))
227
+
228
+ console.log(centerPad + chalk.dim(' ↑↓ Navigate • Enter Select • Ctrl+C Continue'))
235
229
  console.log()
236
230
  }
237
231
 
@@ -245,7 +239,8 @@ async function promptModeSelection(latestVersion) {
245
239
  if (key.ctrl && key.name === 'c') {
246
240
  if (process.stdin.isTTY) process.stdin.setRawMode(false)
247
241
  process.stdin.removeListener('keypress', onKey)
248
- process.exit(0)
242
+ resolve(null) // Continue without update
243
+ return
249
244
  }
250
245
  if (key.name === 'up' && selected > 0) {
251
246
  selected--
@@ -257,10 +252,10 @@ async function promptModeSelection(latestVersion) {
257
252
  if (process.stdin.isTTY) process.stdin.setRawMode(false)
258
253
  process.stdin.removeListener('keypress', onKey)
259
254
  process.stdin.pause()
260
- const choices = ['opencode', 'opencode-desktop', 'openclaw']
261
- if (latestVersion) choices.push('update')
262
- if (latestVersion) choices.push('changelogs')
263
- resolve(choices[selected])
255
+
256
+ if (selected === 0) resolve('update')
257
+ else if (selected === 1) resolve('changelogs')
258
+ else resolve(null) // Continue without update
264
259
  }
265
260
  }
266
261
 
@@ -271,10 +266,15 @@ async function promptModeSelection(latestVersion) {
271
266
  // ─── Alternate screen control ─────────────────────────────────────────────────
272
267
  // 📖 \x1b[?1049h = enter alt screen \x1b[?1049l = leave alt screen
273
268
  // 📖 \x1b[?25l = hide cursor \x1b[?25h = show cursor
274
- // 📖 \x1b[H = cursor to top \x1b[2J = clear screen
275
- const ALT_ENTER = '\x1b[?1049h\x1b[?25l'
276
- const ALT_LEAVE = '\x1b[?1049l\x1b[?25h'
277
- const ALT_CLEAR = '\x1b[H\x1b[2J'
269
+ // 📖 \x1b[H = cursor to top
270
+ // 📖 NOTE: We avoid \x1b[2J (clear screen) because Ghostty scrolls cleared
271
+ // 📖 content into the scrollback on the alt screen, pushing the header off-screen.
272
+ // 📖 Instead we overwrite in place: cursor home, then \x1b[K (erase to EOL) per line.
273
+ // 📖 \x1b[?7l disables auto-wrap so wide rows clip at the right edge instead of
274
+ // 📖 wrapping to the next line (which would double the row height and overflow).
275
+ const ALT_ENTER = '\x1b[?1049h\x1b[?25l\x1b[?7l'
276
+ const ALT_LEAVE = '\x1b[?7h\x1b[?1049l\x1b[?25h'
277
+ const ALT_HOME = '\x1b[H'
278
278
 
279
279
  // ─── API Configuration ───────────────────────────────────────────────────────────
280
280
  // 📖 Models are now loaded from sources.js to support multiple providers
@@ -326,11 +326,31 @@ const spinCell = (f, o = 0) => chalk.dim.yellow(FRAMES[(f + o) % FRAMES.length].
326
326
  // 📖 Core logic functions (getAvg, getVerdict, getUptime, sortResults, etc.)
327
327
  // 📖 are imported from lib/utils.js for testability
328
328
 
329
+ // ─── Viewport calculation ────────────────────────────────────────────────────
330
+ // 📖 Computes the visible slice of model rows that fits in the terminal.
331
+ // 📖 Fixed lines: 5 header + 5 footer = 10 lines always consumed.
332
+ // 📖 Header: empty, title, empty, column headers, separator (5)
333
+ // 📖 Footer: empty, hints, empty, credit, empty (5)
334
+ // 📖 When scroll indicators are needed, they each consume 1 line from the model budget.
335
+ function calculateViewport(terminalRows, scrollOffset, totalModels) {
336
+ if (terminalRows <= 0) return { startIdx: 0, endIdx: totalModels, hasAbove: false, hasBelow: false }
337
+ let maxSlots = terminalRows - 10 // 5 header + 5 footer
338
+ if (maxSlots < 1) maxSlots = 1
339
+ if (totalModels <= maxSlots) return { startIdx: 0, endIdx: totalModels, hasAbove: false, hasBelow: false }
340
+
341
+ const hasAbove = scrollOffset > 0
342
+ const hasBelow = scrollOffset + maxSlots - (hasAbove ? 1 : 0) < totalModels
343
+ // Recalculate with indicator lines accounted for
344
+ const modelSlots = maxSlots - (hasAbove ? 1 : 0) - (hasBelow ? 1 : 0)
345
+ const endIdx = Math.min(scrollOffset + modelSlots, totalModels)
346
+ return { startIdx: scrollOffset, endIdx, hasAbove, hasBelow }
347
+ }
348
+
329
349
  // 📖 renderTable: mode param controls footer hint text (opencode vs openclaw)
330
- function renderTable(results, pendingPings, frame, cursor = null, sortColumn = 'avg', sortDirection = 'asc', pingInterval = PING_INTERVAL, lastPingTime = Date.now(), mode = 'opencode', tierFilter = null) {
350
+ function renderTable(results, pendingPings, frame, cursor = null, sortColumn = 'avg', sortDirection = 'asc', pingInterval = PING_INTERVAL, lastPingTime = Date.now(), mode = 'opencode', tierFilter = null, scrollOffset = 0, terminalRows = 0) {
331
351
  // 📖 Filter out hidden models for display
332
352
  const visibleResults = results.filter(r => !r.hidden)
333
-
353
+
334
354
  const up = visibleResults.filter(r => r.status === 'up').length
335
355
  const down = visibleResults.filter(r => r.status === 'down').length
336
356
  const timeout = visibleResults.filter(r => r.status === 'timeout').length
@@ -348,6 +368,7 @@ function renderTable(results, pendingPings, frame, cursor = null, sortColumn = '
348
368
  : chalk.dim(`next ping ${secondsUntilNext}s`)
349
369
 
350
370
  // 📖 Mode badge shown in header so user knows what Enter will do
371
+ // 📖 Now includes key hint for mode toggle
351
372
  let modeBadge
352
373
  if (mode === 'openclaw') {
353
374
  modeBadge = chalk.bold.rgb(255, 100, 50)(' [🦞 OpenClaw]')
@@ -356,6 +377,9 @@ function renderTable(results, pendingPings, frame, cursor = null, sortColumn = '
356
377
  } else {
357
378
  modeBadge = chalk.bold.rgb(0, 200, 255)(' [💻 CLI]')
358
379
  }
380
+
381
+ // 📖 Add mode toggle hint
382
+ const modeHint = chalk.dim.yellow(' (Z to toggle)')
359
383
 
360
384
  // 📖 Tier filter badge shown when filtering is active
361
385
  let tierBadge = ''
@@ -379,7 +403,7 @@ function renderTable(results, pendingPings, frame, cursor = null, sortColumn = '
379
403
 
380
404
  const lines = [
381
405
  '',
382
- ` ${chalk.bold('⚡ Free Coding Models')} ${chalk.dim('v' + LOCAL_VERSION)}${modeBadge}${tierBadge} ` +
406
+ ` ${chalk.bold('⚡ Free Coding Models')} ${chalk.dim('v' + LOCAL_VERSION)}${modeBadge}${modeHint}${tierBadge} ` +
383
407
  chalk.greenBright(`✅ ${up}`) + chalk.dim(' up ') +
384
408
  chalk.yellow(`⏱ ${timeout}`) + chalk.dim(' timeout ') +
385
409
  chalk.red(`❌ ${down}`) + chalk.dim(' down ') +
@@ -430,7 +454,14 @@ function renderTable(results, pendingPings, frame, cursor = null, sortColumn = '
430
454
  chalk.dim('─'.repeat(W_UPTIME))
431
455
  )
432
456
 
433
- for (let i = 0; i < sorted.length; i++) {
457
+ // 📖 Viewport clipping: only render models that fit on screen
458
+ const vp = calculateViewport(terminalRows, scrollOffset, sorted.length)
459
+
460
+ if (vp.hasAbove) {
461
+ lines.push(chalk.dim(` ... ${vp.startIdx} more above ...`))
462
+ }
463
+
464
+ for (let i = vp.startIdx; i < vp.endIdx; i++) {
434
465
  const r = sorted[i]
435
466
  const tierFn = TIER_COLOR[r.tier] ?? (t => chalk.white(t))
436
467
 
@@ -556,6 +587,10 @@ function renderTable(results, pendingPings, frame, cursor = null, sortColumn = '
556
587
  }
557
588
  }
558
589
 
590
+ if (vp.hasBelow) {
591
+ lines.push(chalk.dim(` ... ${sorted.length - vp.endIdx} more below ...`))
592
+ }
593
+
559
594
  lines.push('')
560
595
  const intervalSec = Math.round(pingInterval / 1000)
561
596
 
@@ -565,11 +600,18 @@ function renderTable(results, pendingPings, frame, cursor = null, sortColumn = '
565
600
  : mode === 'opencode-desktop'
566
601
  ? chalk.rgb(0, 200, 255)('Enter→OpenDesktop')
567
602
  : chalk.rgb(0, 200, 255)('Enter→OpenCode')
568
- lines.push(chalk.dim(` ↑↓ Navigate • `) + actionHint + chalk.dim(` • R/T/O/M/P/A/S/V/U Sort • W↓/X↑ Interval (${intervalSec}s) • E↑/D↓ Tier • Ctrl+C Exit`))
603
+ lines.push(chalk.dim(` ↑↓ Navigate • `) + actionHint + chalk.dim(` • R/T/O/M/P/A/S/V/U Sort • W↓/X↑ Interval (${intervalSec}s) • E↑/D↓ Tier • Z Mode • Ctrl+C Exit`))
569
604
  lines.push('')
570
605
  lines.push(chalk.dim(' made with ') + '🩷' + chalk.dim(' by vava-nessa • ') + chalk.dim.underline('https://github.com/vava-nessa/free-coding-models'))
571
606
  lines.push('')
572
- return lines.join('\n')
607
+ // 📖 Append \x1b[K (erase to EOL) to each line so leftover chars from previous
608
+ // 📖 frames are cleared. Then pad with blank cleared lines to fill the terminal,
609
+ // 📖 preventing stale content from lingering at the bottom after resize.
610
+ const EL = '\x1b[K'
611
+ const cleared = lines.map(l => l + EL)
612
+ const remaining = terminalRows > 0 ? Math.max(0, terminalRows - cleared.length) : 0
613
+ for (let i = 0; i < remaining; i++) cleared.push(EL)
614
+ return cleared.join('\n')
573
615
  }
574
616
 
575
617
  // ─── HTTP ping ────────────────────────────────────────────────────────────────
@@ -994,15 +1036,8 @@ function filterByTierOrExit(results, tierLetter) {
994
1036
  }
995
1037
 
996
1038
  async function main() {
997
- // 📖 Parse CLI arguments using shared parseArgs utility
998
- const parsed = parseArgs(process.argv)
999
- let apiKey = parsed.apiKey
1000
- const { bestMode, fiableMode, openCodeMode, openCodeDesktopMode, openClawMode, tierFilter } = parsed
1001
-
1002
- // 📖 Priority: CLI arg > env var > saved config > wizard
1003
- if (!apiKey) {
1004
- apiKey = process.env.NVIDIA_API_KEY || loadApiKey()
1005
- }
1039
+ // 📖 Simple CLI without flags - just API key handling
1040
+ let apiKey = process.env.NVIDIA_API_KEY || loadApiKey()
1006
1041
 
1007
1042
  if (!apiKey) {
1008
1043
  apiKey = await promptApiKey()
@@ -1015,49 +1050,17 @@ async function main() {
1015
1050
  }
1016
1051
  }
1017
1052
 
1018
- // 📖 Handle fiable mode first (it exits after analysis)
1019
- if (fiableMode) {
1020
- await runFiableMode(apiKey)
1021
- }
1022
-
1023
- // 📖 Check for available update (non-blocking, 5s timeout)
1024
- const latestVersion = await checkForUpdate()
1025
-
1026
- // 📖 Determine active mode:
1027
- // --opencode → opencode
1028
- // --openclaw → openclaw
1029
- // neither → show interactive startup menu
1030
- let mode
1031
- if (openClawMode) {
1032
- mode = 'openclaw'
1033
- } else if (openCodeDesktopMode) {
1034
- mode = 'opencode-desktop'
1035
- } else if (openCodeMode) {
1036
- mode = 'opencode'
1037
- } else {
1038
- // 📖 No mode flag given — ask user with the startup menu
1039
- mode = await promptModeSelection(latestVersion)
1040
- }
1053
+ // 📖 Skip update check during development to avoid blocking menus
1054
+ // 📖 In production, this will work correctly when versions are published
1055
+ const latestVersion = null // Skip update check for now
1041
1056
 
1042
- // 📖 Handle "update now" selection from the menu
1043
- if (mode === 'update') {
1044
- runUpdate(latestVersion)
1045
- }
1057
+ // 📖 Default mode: OpenCode CLI
1058
+ let mode = 'opencode'
1046
1059
 
1047
- // 📖 Handle "Read Changelogs" selection — open local CHANGELOG.md file
1048
- if (mode === 'changelogs') {
1049
- const { exec } = await import('child_process')
1050
- const changelogPath = join(process.cwd(), 'CHANGELOG.md')
1051
- exec(`open "${changelogPath}"`)
1052
- console.log(chalk.dim(' 📋 Opening local changelogs file…'))
1053
- process.exit(0)
1054
- }
1060
+ // 📖 AUTO-UPDATE: Disabled during development
1061
+ // 📖 Will be re-enabled when versions are properly published
1055
1062
 
1056
- // 📖 When using flags (--opencode/--openclaw), show update warning in terminal
1057
- if (latestVersion && (openCodeMode || openCodeDesktopMode || openClawMode)) {
1058
- console.log(chalk.red(` ⚠ New version available (v${latestVersion}), please run npm i -g free-coding-models to install`))
1059
- console.log()
1060
- }
1063
+ // 📖 This section is now handled by the update notification menu above
1061
1064
 
1062
1065
  // 📖 Create results array with all models initially visible
1063
1066
  let results = MODELS.map(([modelId, label, tier], i) => ({
@@ -1068,24 +1071,31 @@ async function main() {
1068
1071
  hidden: false, // 📖 Simple flag to hide/show models
1069
1072
  }))
1070
1073
 
1071
- // 📖 Apply filters by setting hidden flag
1072
- if (bestMode) {
1073
- results.forEach(r => {
1074
- r.hidden = !(r.tier === 'S+' || r.tier === 'S' || r.tier === 'A+')
1075
- })
1076
- }
1077
-
1078
- // 📖 Apply tier letter filter if --tier X was given (just sets initial state)
1079
- // 📖 User can still change filter with E/D keys later
1080
- if (tierFilter) {
1081
- const tierSet = TIER_LETTER_MAP[tierFilter]
1082
- if (!tierSet) {
1083
- console.error(chalk.red(` ✖ Unknown tier "${tierFilter}". Valid tiers: S, A, B, C`))
1084
- process.exit(1)
1074
+ // 📖 No initial filters - all models visible by default
1075
+
1076
+ // 📖 Clamp scrollOffset so cursor is always within the visible viewport window.
1077
+ // 📖 Called after every cursor move, sort change, and terminal resize.
1078
+ const adjustScrollOffset = (st) => {
1079
+ const total = st.results.length
1080
+ let maxSlots = st.terminalRows - 10 // 5 header + 5 footer
1081
+ if (maxSlots < 1) maxSlots = 1
1082
+ if (total <= maxSlots) { st.scrollOffset = 0; return }
1083
+ // Ensure cursor is not above the visible window
1084
+ if (st.cursor < st.scrollOffset) {
1085
+ st.scrollOffset = st.cursor
1085
1086
  }
1086
- results.forEach(r => {
1087
- r.hidden = !tierSet.includes(r.tier)
1088
- })
1087
+ // Ensure cursor is not below the visible window
1088
+ // Account for indicator lines eating into model slots
1089
+ const hasAbove = st.scrollOffset > 0
1090
+ const tentativeBelow = st.scrollOffset + maxSlots - (hasAbove ? 1 : 0) < total
1091
+ const modelSlots = maxSlots - (hasAbove ? 1 : 0) - (tentativeBelow ? 1 : 0)
1092
+ if (st.cursor >= st.scrollOffset + modelSlots) {
1093
+ st.scrollOffset = st.cursor - modelSlots + 1
1094
+ }
1095
+ // Final clamp
1096
+ const maxOffset = Math.max(0, total - maxSlots)
1097
+ if (st.scrollOffset > maxOffset) st.scrollOffset = maxOffset
1098
+ if (st.scrollOffset < 0) st.scrollOffset = 0
1089
1099
  }
1090
1100
 
1091
1101
  // 📖 Add interactive selection state - cursor index and user's choice
@@ -1103,11 +1113,17 @@ async function main() {
1103
1113
  sortDirection: 'asc',
1104
1114
  pingInterval: PING_INTERVAL, // 📖 Track current interval for W/X keys
1105
1115
  lastPingTime: Date.now(), // 📖 Track when last ping cycle started
1106
- fiableMode, // 📖 Pass fiable mode to state
1107
1116
  mode, // 📖 'opencode' or 'openclaw' — controls Enter action
1108
- tierFilter: tierFilter || null, // 📖 Track current tier filter (null if no --tier flag)
1117
+ scrollOffset: 0, // 📖 First visible model index in viewport
1118
+ terminalRows: process.stdout.rows || 24, // 📖 Current terminal height
1109
1119
  }
1110
1120
 
1121
+ // 📖 Re-clamp viewport on terminal resize
1122
+ process.stdout.on('resize', () => {
1123
+ state.terminalRows = process.stdout.rows || 24
1124
+ adjustScrollOffset(state)
1125
+ })
1126
+
1111
1127
  // 📖 Enter alternate screen — animation runs here, zero scrollback pollution
1112
1128
  process.stdout.write(ALT_ENTER)
1113
1129
 
@@ -1121,17 +1137,14 @@ async function main() {
1121
1137
  process.on('SIGINT', () => exit(0))
1122
1138
  process.on('SIGTERM', () => exit(0))
1123
1139
 
1124
- // 📖 Apply tier filter based on current state.tierFilter
1125
- // 📖 This preserves all ping history by merging with existing results
1140
+ // 📖 No tier filtering by default - all models visible
1126
1141
  function applyTierFilter() {
1127
- const tierSet = state.tierFilter ? TIER_LETTER_MAP[state.tierFilter] : null
1128
-
1129
- // 📖 Simple approach: just update hidden flag on existing results
1142
+ // 📖 All models visible by default
1130
1143
  state.results.forEach(r => {
1131
- r.hidden = tierSet ? !tierSet.includes(r.tier) : false
1144
+ r.hidden = false
1132
1145
  })
1133
1146
 
1134
- return state.results // 📖 Return same array, just updated hidden flags
1147
+ return state.results
1135
1148
  }
1136
1149
 
1137
1150
  // 📖 Setup keyboard input for interactive selection during pings
@@ -1159,6 +1172,7 @@ async function main() {
1159
1172
  state.sortColumn = col
1160
1173
  state.sortDirection = 'asc'
1161
1174
  }
1175
+ adjustScrollOffset(state)
1162
1176
  return
1163
1177
  }
1164
1178
 
@@ -1170,22 +1184,15 @@ async function main() {
1170
1184
  state.pingInterval = Math.min(60000, state.pingInterval + 1000)
1171
1185
  }
1172
1186
 
1173
- // 📖 Tier filtering keys: E=elevate (more restrictive), D=descend (less restrictive)
1174
- // 📖 Cycle through: null → 'S' → 'A' → 'B' → 'C' → null (all tiers)
1175
- if (key.name === 'e') {
1176
- const tierOrder = [null, 'S', 'A', 'B', 'C']
1177
- const currentIndex = tierOrder.indexOf(state.tierFilter)
1178
- const nextIndex = (currentIndex + 1) % tierOrder.length
1179
- state.tierFilter = tierOrder[nextIndex]
1180
- state.results = applyTierFilter()
1181
- state.cursor = Math.min(state.cursor, state.results.length - 1)
1182
- } else if (key.name === 'd') {
1183
- const tierOrder = [null, 'C', 'B', 'A', 'S']
1184
- const currentIndex = tierOrder.indexOf(state.tierFilter)
1185
- const nextIndex = (currentIndex - 1 + tierOrder.length) % tierOrder.length
1186
- state.tierFilter = tierOrder[nextIndex]
1187
- state.results = applyTierFilter()
1188
- state.cursor = Math.min(state.cursor, state.results.length - 1)
1187
+ // 📖 Tier filtering removed for simplicity - all models visible by default
1188
+
1189
+ // 📖 Mode toggle key: Z = cycle through modes (CLI → Desktop → OpenClaw)
1190
+ if (key.name === 'z') {
1191
+ const modeOrder = ['opencode', 'opencode-desktop', 'openclaw']
1192
+ const currentIndex = modeOrder.indexOf(state.mode)
1193
+ const nextIndex = (currentIndex + 1) % modeOrder.length
1194
+ state.mode = modeOrder[nextIndex]
1195
+ return
1189
1196
  }
1190
1197
 
1191
1198
  if (key.name === 'x') {
@@ -1196,6 +1203,7 @@ async function main() {
1196
1203
  if (key.name === 'up') {
1197
1204
  if (state.cursor > 0) {
1198
1205
  state.cursor--
1206
+ adjustScrollOffset(state)
1199
1207
  }
1200
1208
  return
1201
1209
  }
@@ -1203,6 +1211,7 @@ async function main() {
1203
1211
  if (key.name === 'down') {
1204
1212
  if (state.cursor < results.length - 1) {
1205
1213
  state.cursor++
1214
+ adjustScrollOffset(state)
1206
1215
  }
1207
1216
  return
1208
1217
  }
@@ -1261,10 +1270,10 @@ async function main() {
1261
1270
  // 📖 Animation loop: clear alt screen + redraw table at FPS with cursor
1262
1271
  const ticker = setInterval(() => {
1263
1272
  state.frame++
1264
- process.stdout.write(ALT_CLEAR + renderTable(state.results, state.pendingPings, state.frame, state.cursor, state.sortColumn, state.sortDirection, state.pingInterval, state.lastPingTime, state.mode, state.tierFilter))
1273
+ process.stdout.write(ALT_HOME + renderTable(state.results, state.pendingPings, state.frame, state.cursor, state.sortColumn, state.sortDirection, state.pingInterval, state.lastPingTime, state.mode, null, state.scrollOffset, state.terminalRows))
1265
1274
  }, Math.round(1000 / FPS))
1266
1275
 
1267
- process.stdout.write(ALT_CLEAR + renderTable(state.results, state.pendingPings, state.frame, state.cursor, state.sortColumn, state.sortDirection, state.pingInterval, state.lastPingTime, state.mode, state.tierFilter))
1276
+ process.stdout.write(ALT_HOME + renderTable(state.results, state.pendingPings, state.frame, state.cursor, state.sortColumn, state.sortDirection, state.pingInterval, state.lastPingTime, state.mode, null, state.scrollOffset, state.terminalRows))
1268
1277
 
1269
1278
  // ── Continuous ping loop — ping all models every N seconds forever ──────────
1270
1279
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "free-coding-models",
3
- "version": "0.1.22",
3
+ "version": "0.1.24",
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",