free-coding-models 0.1.23 → 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.
@@ -266,10 +266,15 @@ async function promptUpdateNotification(latestVersion) {
266
266
  // ─── Alternate screen control ─────────────────────────────────────────────────
267
267
  // 📖 \x1b[?1049h = enter alt screen \x1b[?1049l = leave alt screen
268
268
  // 📖 \x1b[?25l = hide cursor \x1b[?25h = show cursor
269
- // 📖 \x1b[H = cursor to top \x1b[2J = clear screen
270
- const ALT_ENTER = '\x1b[?1049h\x1b[?25l'
271
- const ALT_LEAVE = '\x1b[?1049l\x1b[?25h'
272
- 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'
273
278
 
274
279
  // ─── API Configuration ───────────────────────────────────────────────────────────
275
280
  // 📖 Models are now loaded from sources.js to support multiple providers
@@ -321,11 +326,31 @@ const spinCell = (f, o = 0) => chalk.dim.yellow(FRAMES[(f + o) % FRAMES.length].
321
326
  // 📖 Core logic functions (getAvg, getVerdict, getUptime, sortResults, etc.)
322
327
  // 📖 are imported from lib/utils.js for testability
323
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
+
324
349
  // 📖 renderTable: mode param controls footer hint text (opencode vs openclaw)
325
- 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) {
326
351
  // 📖 Filter out hidden models for display
327
352
  const visibleResults = results.filter(r => !r.hidden)
328
-
353
+
329
354
  const up = visibleResults.filter(r => r.status === 'up').length
330
355
  const down = visibleResults.filter(r => r.status === 'down').length
331
356
  const timeout = visibleResults.filter(r => r.status === 'timeout').length
@@ -429,7 +454,14 @@ function renderTable(results, pendingPings, frame, cursor = null, sortColumn = '
429
454
  chalk.dim('─'.repeat(W_UPTIME))
430
455
  )
431
456
 
432
- 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++) {
433
465
  const r = sorted[i]
434
466
  const tierFn = TIER_COLOR[r.tier] ?? (t => chalk.white(t))
435
467
 
@@ -555,6 +587,10 @@ function renderTable(results, pendingPings, frame, cursor = null, sortColumn = '
555
587
  }
556
588
  }
557
589
 
590
+ if (vp.hasBelow) {
591
+ lines.push(chalk.dim(` ... ${sorted.length - vp.endIdx} more below ...`))
592
+ }
593
+
558
594
  lines.push('')
559
595
  const intervalSec = Math.round(pingInterval / 1000)
560
596
 
@@ -568,7 +604,14 @@ function renderTable(results, pendingPings, frame, cursor = null, sortColumn = '
568
604
  lines.push('')
569
605
  lines.push(chalk.dim(' made with ') + '🩷' + chalk.dim(' by vava-nessa • ') + chalk.dim.underline('https://github.com/vava-nessa/free-coding-models'))
570
606
  lines.push('')
571
- 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')
572
615
  }
573
616
 
574
617
  // ─── HTTP ping ────────────────────────────────────────────────────────────────
@@ -1030,6 +1073,31 @@ async function main() {
1030
1073
 
1031
1074
  // 📖 No initial filters - all models visible by default
1032
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
1086
+ }
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
1099
+ }
1100
+
1033
1101
  // 📖 Add interactive selection state - cursor index and user's choice
1034
1102
  // 📖 sortColumn: 'rank'|'tier'|'origin'|'model'|'ping'|'avg'|'status'|'verdict'|'uptime'
1035
1103
  // 📖 sortDirection: 'asc' (default) or 'desc'
@@ -1046,8 +1114,16 @@ async function main() {
1046
1114
  pingInterval: PING_INTERVAL, // 📖 Track current interval for W/X keys
1047
1115
  lastPingTime: Date.now(), // 📖 Track when last ping cycle started
1048
1116
  mode, // 📖 'opencode' or 'openclaw' — controls Enter action
1117
+ scrollOffset: 0, // 📖 First visible model index in viewport
1118
+ terminalRows: process.stdout.rows || 24, // 📖 Current terminal height
1049
1119
  }
1050
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
+
1051
1127
  // 📖 Enter alternate screen — animation runs here, zero scrollback pollution
1052
1128
  process.stdout.write(ALT_ENTER)
1053
1129
 
@@ -1096,6 +1172,7 @@ async function main() {
1096
1172
  state.sortColumn = col
1097
1173
  state.sortDirection = 'asc'
1098
1174
  }
1175
+ adjustScrollOffset(state)
1099
1176
  return
1100
1177
  }
1101
1178
 
@@ -1126,6 +1203,7 @@ async function main() {
1126
1203
  if (key.name === 'up') {
1127
1204
  if (state.cursor > 0) {
1128
1205
  state.cursor--
1206
+ adjustScrollOffset(state)
1129
1207
  }
1130
1208
  return
1131
1209
  }
@@ -1133,6 +1211,7 @@ async function main() {
1133
1211
  if (key.name === 'down') {
1134
1212
  if (state.cursor < results.length - 1) {
1135
1213
  state.cursor++
1214
+ adjustScrollOffset(state)
1136
1215
  }
1137
1216
  return
1138
1217
  }
@@ -1191,10 +1270,10 @@ async function main() {
1191
1270
  // 📖 Animation loop: clear alt screen + redraw table at FPS with cursor
1192
1271
  const ticker = setInterval(() => {
1193
1272
  state.frame++
1194
- process.stdout.write(ALT_CLEAR + renderTable(state.results, state.pendingPings, state.frame, state.cursor, state.sortColumn, state.sortDirection, state.pingInterval, state.lastPingTime, state.mode, null))
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))
1195
1274
  }, Math.round(1000 / FPS))
1196
1275
 
1197
- process.stdout.write(ALT_CLEAR + renderTable(state.results, state.pendingPings, state.frame, state.cursor, state.sortColumn, state.sortDirection, state.pingInterval, state.lastPingTime, state.mode, null))
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))
1198
1277
 
1199
1278
  // ── Continuous ping loop — ping all models every N seconds forever ──────────
1200
1279
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "free-coding-models",
3
- "version": "0.1.23",
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",