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.
- package/bin/free-coding-models.js +89 -10
- package/package.json +1 -1
|
@@ -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
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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",
|