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.
- package/bin/free-coding-models.js +170 -161
- package/package.json +1 -1
|
@@ -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('
|
|
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(
|
|
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
|
-
// ───
|
|
169
|
-
// 📖 Shown
|
|
170
|
-
// 📖
|
|
171
|
-
// 📖 Returns '
|
|
172
|
-
async function
|
|
173
|
-
|
|
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
|
-
|
|
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
|
-
|
|
216
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
261
|
-
if (
|
|
262
|
-
if (
|
|
263
|
-
resolve(
|
|
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
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
// 📖
|
|
998
|
-
|
|
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
|
-
// 📖
|
|
1019
|
-
|
|
1020
|
-
|
|
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
|
-
// 📖
|
|
1043
|
-
|
|
1044
|
-
runUpdate(latestVersion)
|
|
1045
|
-
}
|
|
1057
|
+
// 📖 Default mode: OpenCode CLI
|
|
1058
|
+
let mode = 'opencode'
|
|
1046
1059
|
|
|
1047
|
-
// 📖
|
|
1048
|
-
|
|
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
|
-
// 📖
|
|
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
|
-
// 📖
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
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
|
-
|
|
1087
|
-
|
|
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
|
-
|
|
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
|
-
// 📖
|
|
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
|
-
|
|
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 =
|
|
1144
|
+
r.hidden = false
|
|
1132
1145
|
})
|
|
1133
1146
|
|
|
1134
|
-
return state.results
|
|
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
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
const
|
|
1178
|
-
const
|
|
1179
|
-
|
|
1180
|
-
state.
|
|
1181
|
-
|
|
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(
|
|
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(
|
|
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.
|
|
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",
|