free-coding-models 0.3.55 → 0.3.57
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 +55 -56
- package/README.md +214 -160
- package/bin/free-coding-models.js +46 -0
- package/package.json +2 -2
- package/sources.js +134 -310
- package/src/analysis.js +23 -10
- package/src/app.js +66 -27
- package/src/cache.js +1 -1
- package/src/cli-help.js +9 -0
- package/src/command-palette.js +15 -13
- package/src/config.js +201 -35
- package/src/constants.js +4 -4
- package/src/endpoint-installer.js +45 -1
- package/src/favorites.js +22 -0
- package/src/graphify-out/cache/089db1c1def873cf6d112f1590da4490e61e691aff0db41e006aa2fb15ba0656.json +1 -0
- package/src/graphify-out/cache/0b510b53cf1a1393fb52b1fc3bbbf88b63938e961ec5b82119a2e9715fee8bd7.json +1 -0
- package/src/graphify-out/cache/0ec9a95a326bde58e0316889018b278062d06d494d0f31ba177c9de71e5fed2d.json +1 -0
- package/src/graphify-out/cache/1548663a24a68dce740ebab1bd1d3091048c9604e9d067a1650a42a6d82541d4.json +1 -0
- package/src/graphify-out/cache/1783af63cb6d0dfb4d469009f71ac83a74ba0b33d48186ff2c6e63f9429e900a.json +1 -0
- package/src/graphify-out/cache/1e109f5eb5dc4fd285871c3613e32b6b14a8c225f4080ee34b51c7e1a1764571.json +1 -0
- package/src/graphify-out/cache/1eb24dbeb69b46c8bc1caf925df2f2a964af0f33aea143adf8ddf88e017db6ca.json +1 -0
- package/src/graphify-out/cache/21e1bcfed11685e8347243f9d8516072dda183266a4bfe22c52fb31753a446c8.json +1 -0
- package/src/graphify-out/cache/2327473478b9c4b1940bf7ef66c9ee960b3cba8d5302e56b625df8274246e0b4.json +1 -0
- package/src/graphify-out/cache/25955b81fd25454c8fa90fb71a47db8d1215cf621beb8ff3cbd580aaf011b4f3.json +1 -0
- package/src/graphify-out/cache/2739677f19c702f88f3de0a0bac475066adbda98709907ad3de967aef689f86d.json +1 -0
- package/src/graphify-out/cache/2bba03422f6b3ee7f5b5d29cc90314a064d259e5822a176657bda3e04505cf00.json +1 -0
- package/src/graphify-out/cache/2ddf1d2c6d10147b0402446bc71a7988187b79b6210dd7e7250be8c555b9ff35.json +1 -0
- package/src/graphify-out/cache/2ee07457a5767c95a57f8e9eb95b28f800044f35666e0715e9d88ad1103a092e.json +1 -0
- package/src/graphify-out/cache/2fe9f75dc2951c417f2c8dd22749092cf550dc67599f1c8d1866900dc6e9154e.json +1 -0
- package/src/graphify-out/cache/41c4b7c27e7fc3e2948d3a4bf95a72de2ed9a6f0463994babdce8ed2cc84598c.json +1 -0
- package/src/graphify-out/cache/5028defd54b7fbd3c7e444973e493de036e097e9b1d2a7cae7f19b88d68aacde.json +1 -0
- package/src/graphify-out/cache/5b133aba3fb16410c5b1fdbd1730039fc7fa1ac93abd99d7be08f60da70fc8d4.json +1 -0
- package/src/graphify-out/cache/74252e5b0978d85ab3421a3de1a9384aa282ffd2be2cfe7db2530139089f4275.json +1 -0
- package/src/graphify-out/cache/7695ebeea056095edd14332963cc43354ef3a097caf46f1e28d0f01369642901.json +1 -0
- package/src/graphify-out/cache/777aa7085c395a935c6556bbde182cd871edb61f3a685ed8068ec0c8f6fb0075.json +1 -0
- package/src/graphify-out/cache/82a723881980e82273c113def8315533d7da28827e300413d9ad30f27b7407df.json +1 -0
- package/src/graphify-out/cache/86b87c9603e6cd188f42c7eed3b86c291d48a781c223a707e74f3e7ed0c02a21.json +1 -0
- package/src/graphify-out/cache/890fead9a78cadaed560a2d2453916121fa605c3e43a334910ac4bc951a9ef6d.json +1 -0
- package/src/graphify-out/cache/89d3ea66f52783caa775ef9a30923d7d6225e1d8ae9e962f4741b8c7785dab1e.json +1 -0
- package/src/graphify-out/cache/8cc82cd9edce41f0e1c092f14a94fd52bf847addf3237b616dc5a9e505bd05bd.json +1 -0
- package/src/graphify-out/cache/93ba2e25e3ff7ad525f397902345fbd375df7315de7b402e20cc803c14eccde8.json +1 -0
- package/src/graphify-out/cache/99beed29580b9c7bfecfee794cb3d8e535fcf0eb3b92113108f88bdd0a8e79b3.json +1 -0
- package/src/graphify-out/cache/aeeb931fa477c65ce2e51d8149957350fa54225c613222bbbe8448998d1afd3d.json +1 -0
- package/src/graphify-out/cache/baf91bef5b5ecb2a476433b6cc0c48c563c54ee2d07fc3c192e543685e3e7222.json +1 -0
- package/src/graphify-out/cache/bd98b94ac4e9b92b6336d47b26e0366b51a4eaf0711d722f05f98dfae23ab42b.json +1 -0
- package/src/graphify-out/cache/bfcb51e9328e9cbfbee4f6fee0f56635d7b03488addc9f6c4e4b190b70a73362.json +1 -0
- package/src/graphify-out/cache/c0d3dabeb093aa758c49eadf41b87ecc96a16c1449c2670aaf48cbfc891d8da6.json +1 -0
- package/src/graphify-out/cache/c20d6630236f473c1406068c3ae205853e649b216495c93dfec055dd222c55cf.json +1 -0
- package/src/graphify-out/cache/c22b9122816bebce0a2f79af41a986559d01e00163dbcd579c5755621b4cb483.json +1 -0
- package/src/graphify-out/cache/ca556ec14453ddb8f9e0c5a832dac90d77111b9bad5f8c2d80d272e2e7a06371.json +1 -0
- package/src/graphify-out/cache/d6dbc9135dfa35a756b3b09b06700e4bc229fdccba11bb963f2ba44028e0bbae.json +1 -0
- package/src/graphify-out/cache/e1cf71276f1779d0fa075f79bd7c8a9fd0b8eef6932ac043137451b7c7fa7cbe.json +1 -0
- package/src/graphify-out/cache/e4b3be14494467df2d2ed389bc4f18f099021cb5bc355b901fa88387b2d8b8a2.json +1 -0
- package/src/graphify-out/cache/eaea0dded097f6f9553b654220046c6ec0c9be592a5973d906564ee60af34e0d.json +1 -0
- package/src/graphify-out/cache/ef07d0cd2675d1f79d2a2fdbf3bc3319687638751e9ce89b0d0d97ed1cd9f7e1.json +1 -0
- package/src/graphify-out/cache/f81272d6eb8aaff9e96d5a1d9f06777db70ac3652a646b951ded51f79871d733.json +1 -0
- package/src/graphify-out/cache/f9619dd92186f75a6dbda937e0c606647153918524cdb5763f956e6ec2a9e386.json +1 -0
- package/src/graphify-out/cache/fd88b1b2ff4bfcae08559d9c2aaeeb9a3f1e2f5cd8928762c311196956c170a5.json +1 -0
- package/src/key-handler.js +322 -114
- package/src/kilo.js +20 -1
- package/src/opencode.js +23 -2
- package/src/overlays.js +199 -98
- package/src/provider-metadata.js +26 -17
- package/src/quota-capabilities.js +6 -10
- package/src/render-helpers.js +38 -8
- package/src/render-table.js +119 -248
- package/src/router-daemon.js +1986 -0
- package/src/router-dashboard.js +902 -0
- package/src/sync-set.js +479 -0
- package/src/theme.js +4 -0
- package/src/tool-launchers.js +1 -0
- package/src/tool-metadata.js +6 -2
- package/src/utils.js +30 -6
- package/web/dist/assets/{index-C03JjCgA.js → index-DKHCzbK1.js} +2 -2
- package/web/dist/index.html +1 -1
package/src/render-helpers.js
CHANGED
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
*/
|
|
48
48
|
|
|
49
49
|
import chalk from 'chalk'
|
|
50
|
-
import { OVERLAY_PANEL_WIDTH, TABLE_FIXED_LINES } from './constants.js'
|
|
50
|
+
import { OVERLAY_PANEL_WIDTH, TABLE_FIXED_LINES, TABLE_HEADER_LINES, TABLE_FOOTER_LINES } from './constants.js'
|
|
51
51
|
import { sortResults } from './utils.js'
|
|
52
52
|
|
|
53
53
|
// 📖 stripAnsi: Remove ANSI color/control sequences to estimate visible text width before padding.
|
|
@@ -65,13 +65,32 @@ export function maskApiKey(key) {
|
|
|
65
65
|
|
|
66
66
|
// 📖 displayWidth: Calculate display width of a string in terminal columns.
|
|
67
67
|
// 📖 Emojis and other wide characters occupy 2 columns, variation selectors (U+FE0F) are zero-width.
|
|
68
|
+
// 📖 Keycap sequences (digit/# + FE0F + 20E3, e.g. 1️⃣) render as a single 2-cell glyph.
|
|
68
69
|
// 📖 This avoids pulling in a full `string-width` dependency for a lightweight CLI tool.
|
|
69
70
|
export function displayWidth(str) {
|
|
70
71
|
const plain = stripAnsi(String(str))
|
|
72
|
+
const codepoints = [...plain]
|
|
71
73
|
let w = 0
|
|
72
|
-
for (
|
|
74
|
+
for (let i = 0; i < codepoints.length; i++) {
|
|
75
|
+
const ch = codepoints[i]
|
|
73
76
|
const cp = ch.codePointAt(0)
|
|
74
|
-
|
|
77
|
+
|
|
78
|
+
// Keycap sequence detection: ASCII digit / # / * followed by optional FE0F then 20E3 → +2 (single emoji glyph)
|
|
79
|
+
const isKeycapBase = (cp >= 0x30 && cp <= 0x39) || cp === 0x23 || cp === 0x2A
|
|
80
|
+
if (isKeycapBase) {
|
|
81
|
+
let j = i + 1
|
|
82
|
+
let sawFe0f = false
|
|
83
|
+
if (j < codepoints.length && codepoints[j].codePointAt(0) === 0xFE0F) { sawFe0f = true; j++ }
|
|
84
|
+
if (j < codepoints.length && codepoints[j].codePointAt(0) === 0x20E3) {
|
|
85
|
+
w += 2
|
|
86
|
+
i = j // 📖 skip the consumed FE0F (if any) and the 20E3
|
|
87
|
+
continue
|
|
88
|
+
}
|
|
89
|
+
// 📖 Not a keycap, fall through to normal handling
|
|
90
|
+
void sawFe0f
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Zero-width: variation selectors (FE00-FE0F), zero-width joiner/non-joiner, lone combining keycap
|
|
75
94
|
if ((cp >= 0xFE00 && cp <= 0xFE0F) || cp === 0x200D || cp === 0x200C || cp === 0x20E3) continue
|
|
76
95
|
// Wide: CJK, emoji (most above U+1F000), fullwidth forms
|
|
77
96
|
if (
|
|
@@ -146,14 +165,24 @@ export function sliceOverlayLines(lines, offset, terminalRows) {
|
|
|
146
165
|
|
|
147
166
|
// ─── Table viewport calculation ────────────────────────────────────────────────
|
|
148
167
|
|
|
168
|
+
// 📖 getTableFixedLines: Resolve the non-model line budget for the main table.
|
|
169
|
+
// 📖 Header and full footer are always visible in the main table, with optional
|
|
170
|
+
// 📖 extra fixed rows for temporary banners.
|
|
171
|
+
export function getTableFixedLines({ extraFixedLines = 0 } = {}) {
|
|
172
|
+
return TABLE_HEADER_LINES + TABLE_FOOTER_LINES + Math.max(0, extraFixedLines)
|
|
173
|
+
}
|
|
174
|
+
|
|
149
175
|
// 📖 calculateViewport: Computes the visible slice of model rows that fits in the terminal.
|
|
150
176
|
// 📖 When scroll indicators are needed, they each consume 1 line from the model budget.
|
|
151
|
-
// 📖 `
|
|
152
|
-
// 📖 viewport permanently for the normal case.
|
|
177
|
+
// 📖 `lineBudget` lets callers reserve temporary footer/header rows without shrinking
|
|
178
|
+
// 📖 the viewport permanently for the normal case.
|
|
153
179
|
// 📖 Returns { startIdx, endIdx, hasAbove, hasBelow } for rendering.
|
|
154
|
-
export function calculateViewport(terminalRows, scrollOffset, totalModels,
|
|
180
|
+
export function calculateViewport(terminalRows, scrollOffset, totalModels, lineBudget = 0) {
|
|
155
181
|
if (terminalRows <= 0) return { startIdx: 0, endIdx: totalModels, hasAbove: false, hasBelow: false }
|
|
156
|
-
|
|
182
|
+
const fixedLines = typeof lineBudget === 'number'
|
|
183
|
+
? TABLE_FIXED_LINES + Math.max(0, lineBudget)
|
|
184
|
+
: getTableFixedLines(lineBudget)
|
|
185
|
+
let maxSlots = terminalRows - fixedLines
|
|
157
186
|
if (maxSlots < 1) maxSlots = 1
|
|
158
187
|
if (totalModels <= maxSlots) return { startIdx: 0, endIdx: totalModels, hasAbove: false, hasBelow: false }
|
|
159
188
|
|
|
@@ -206,7 +235,8 @@ export function sortResultsWithPinnedFavorites(results, sortColumn, sortDirectio
|
|
|
206
235
|
// 📖 Modifies st.scrollOffset in-place, returns undefined.
|
|
207
236
|
export function adjustScrollOffset(st) {
|
|
208
237
|
const total = st.visibleSorted ? st.visibleSorted.length : st.results.filter(r => !r.hidden).length
|
|
209
|
-
|
|
238
|
+
const fixedLines = getTableFixedLines()
|
|
239
|
+
let maxSlots = st.terminalRows - fixedLines
|
|
210
240
|
if (maxSlots < 1) maxSlots = 1
|
|
211
241
|
if (total <= maxSlots) { st.scrollOffset = 0; return }
|
|
212
242
|
// Ensure cursor is not above the visible window
|
package/src/render-table.js
CHANGED
|
@@ -36,19 +36,18 @@ import chalk from 'chalk'
|
|
|
36
36
|
import { createRequire } from 'module'
|
|
37
37
|
import { sources } from '../sources.js'
|
|
38
38
|
import {
|
|
39
|
-
TABLE_FIXED_LINES,
|
|
40
39
|
COL_MODEL,
|
|
41
40
|
TIER_CYCLE,
|
|
42
41
|
msCell,
|
|
43
42
|
spinCell,
|
|
44
43
|
PING_INTERVAL,
|
|
45
44
|
WIDTH_WARNING_MIN_COLS,
|
|
45
|
+
TABLE_FOOTER_LINES,
|
|
46
46
|
FRAMES
|
|
47
47
|
} from './constants.js'
|
|
48
48
|
import { themeColors, getProviderRgb, getTierRgb, getReadableTextRgb, getTheme } from './theme.js'
|
|
49
49
|
import { TIER_COLOR } from './tier-colors.js'
|
|
50
50
|
import { getAvg, getVerdict, getUptime, getStabilityScore, getVersionStatusInfo } from './utils.js'
|
|
51
|
-
import { VERDICT_CYCLE } from './constants.js'
|
|
52
51
|
import { usagePlaceholderForProvider } from './ping.js'
|
|
53
52
|
import { calculateViewport, sortResultsWithPinnedFavorites, padEndDisplay, displayWidth } from './render-helpers.js'
|
|
54
53
|
import { getToolMeta, TOOL_METADATA, TOOL_MODE_ORDER, isModelCompatibleWithTool } from './tool-metadata.js'
|
|
@@ -58,10 +57,6 @@ import { detectPackageManager, getManualInstallCmd } from './updater.js'
|
|
|
58
57
|
const require = createRequire(import.meta.url)
|
|
59
58
|
const { version: LOCAL_VERSION } = require('../package.json')
|
|
60
59
|
|
|
61
|
-
// 📖 HEALTH_CYCLE: cycles through health/status states (local constant for render-table.js)
|
|
62
|
-
// VERDICT_CYCLE is now imported from constants.js
|
|
63
|
-
const HEALTH_CYCLE = [null, 'up', 'timeout', 'down', 'auth_error', 'noauth', 'pending']
|
|
64
|
-
|
|
65
60
|
// 📖 Mouse support: column boundary map updated every frame by renderTable().
|
|
66
61
|
// 📖 Each entry maps a column name to its display X-start and X-end (1-based, inclusive).
|
|
67
62
|
// 📖 headerRow is the 1-based terminal row of the column header line.
|
|
@@ -109,9 +104,10 @@ export const PROVIDER_COLOR = new Proxy({}, {
|
|
|
109
104
|
})
|
|
110
105
|
|
|
111
106
|
// ─── renderTable: mode param controls footer hint text (opencode vs openclaw) ─────────
|
|
112
|
-
export function renderTable(results, pendingPings, frame, cursor = null, sortColumn = 'avg', sortDirection = 'asc', pingInterval = PING_INTERVAL, lastPingTime = Date.now(), mode = 'opencode', tierFilterMode = 0, scrollOffset = 0, terminalRows = 0, terminalCols = 0, originFilterMode = 0, legacyStatus = null, pingMode = 'normal', pingModeSource = 'auto', hideUnconfiguredModels = false, widthWarningStartedAt = null, widthWarningDismissed = false, widthWarningShowCount = 0, settingsUpdateState = 'idle', settingsUpdateLatestVersion = null, legacyFlag = false, startupLatestVersion = null, versionAlertsEnabled = true, favoritesPinnedAndSticky = false, customTextFilter = null, lastReleaseDate = null,
|
|
107
|
+
export function renderTable(results, pendingPings, frame, cursor = null, sortColumn = 'avg', sortDirection = 'asc', pingInterval = PING_INTERVAL, lastPingTime = Date.now(), mode = 'opencode', tierFilterMode = 0, scrollOffset = 0, terminalRows = 0, terminalCols = 0, originFilterMode = 0, legacyStatus = null, pingMode = 'normal', pingModeSource = 'auto', hideUnconfiguredModels = false, widthWarningStartedAt = null, widthWarningDismissed = false, widthWarningShowCount = 0, settingsUpdateState = 'idle', settingsUpdateLatestVersion = null, legacyFlag = false, startupLatestVersion = null, versionAlertsEnabled = true, favoritesPinnedAndSticky = false, customTextFilter = null, lastReleaseDate = null, legacyFooterHidden = false, verdictFilterMode = 0, healthFilterMode = 0, routerFooterRunning = false, routerFooterActiveSet = null, routerFooterTodayTokens = 0, routerFooterAllTimeTokens = 0, routerFooterRequests = 0) {
|
|
113
108
|
// 📖 Filter out hidden models for display
|
|
114
109
|
const visibleResults = results.filter(r => !r.hidden)
|
|
110
|
+
void legacyFooterHidden
|
|
115
111
|
|
|
116
112
|
const up = visibleResults.filter(r => r.status === 'up').length
|
|
117
113
|
const down = visibleResults.filter(r => r.status === 'down').length
|
|
@@ -323,80 +319,8 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
323
319
|
themeColors.warning(`⏳ ${timeout}`) + themeColors.dim(' timeout ') +
|
|
324
320
|
themeColors.error(`❌ ${down}`) + themeColors.dim(' down ') +
|
|
325
321
|
'',
|
|
326
|
-
'',
|
|
327
322
|
]
|
|
328
323
|
|
|
329
|
-
// 📖 Filter bar — llmfit-style horizontal filter pills (1 dedicated row above table)
|
|
330
|
-
// 📖 Each block: title with hotkey hint + active value colored by filter state
|
|
331
|
-
{
|
|
332
|
-
const filterParts = []
|
|
333
|
-
const filterSep = themeColors.dim(' │ ')
|
|
334
|
-
const blockSep = ' │ '
|
|
335
|
-
|
|
336
|
-
// 📖 Search filter block — shows active text filter or prompt
|
|
337
|
-
if (customTextFilter && customTextFilter.trim()) {
|
|
338
|
-
const badgeText = ` Search "/" ${blockSep} ${customTextFilter.trim().slice(0, 20)} `
|
|
339
|
-
filterParts.push(themeColors.badge(badgeText, [52, 120, 88], [255, 255, 255]))
|
|
340
|
-
} else {
|
|
341
|
-
filterParts.push(themeColors.dim(' Search "/" '))
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
// 📖 Tier filter block — T key cycles through TIER_CYCLE
|
|
345
|
-
if (tierFilterMode > 0) {
|
|
346
|
-
const tierLabel = TIER_CYCLE_NAMES[tierFilterMode]
|
|
347
|
-
const tierBg = getTierRgb(tierLabel)
|
|
348
|
-
filterParts.push(themeColors.badge(` Tier (${tierLabel}) `, tierBg, [255, 255, 255]))
|
|
349
|
-
} else {
|
|
350
|
-
filterParts.push(themeColors.dim(' Tier (T) '))
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// 📖 Provider filter block — D key cycles through providers
|
|
354
|
-
if (originFilterMode > 0) {
|
|
355
|
-
const originKeys = [null, ...Object.keys(sources)]
|
|
356
|
-
const activeOriginKey = originKeys[originFilterMode]
|
|
357
|
-
const activeOriginName = activeOriginKey ? sources[activeOriginKey]?.name ?? activeOriginKey : null
|
|
358
|
-
if (activeOriginName) {
|
|
359
|
-
const normName = normalizeOriginLabel(activeOriginName, activeOriginKey)
|
|
360
|
-
const providerRgb = PROVIDER_COLOR[activeOriginKey] || [255, 255, 255]
|
|
361
|
-
filterParts.push(themeColors.badge(` Provider (${normName}) `, providerRgb, [255, 255, 255]))
|
|
362
|
-
}
|
|
363
|
-
} else {
|
|
364
|
-
filterParts.push(themeColors.dim(' Provider (D) '))
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
// 📖 Verdict filter block — V key cycles through verdicts
|
|
368
|
-
if (verdictFilterMode > 0) {
|
|
369
|
-
const verdictLabel = VERDICT_CYCLE[verdictFilterMode]
|
|
370
|
-
const verdictColors = {
|
|
371
|
-
'Perfect': themeColors.success,
|
|
372
|
-
'Normal': themeColors.metricGood,
|
|
373
|
-
'Slow': (t) => chalk.bold.rgb(...getTierRgb('A-'))(t),
|
|
374
|
-
'Spiky': (t) => chalk.bold.rgb(...getTierRgb('A+'))(t),
|
|
375
|
-
'Very Slow': (t) => chalk.bold.rgb(...getTierRgb('B+'))(t),
|
|
376
|
-
'Overloaded': (t) => chalk.bold.rgb(...getTierRgb('B'))(t),
|
|
377
|
-
'Unstable': themeColors.errorBold,
|
|
378
|
-
'Not Active': themeColors.dim,
|
|
379
|
-
'Pending': themeColors.dim,
|
|
380
|
-
}
|
|
381
|
-
const vc = verdictColors[verdictLabel] || themeColors.accent
|
|
382
|
-
filterParts.push(themeColors.badge(` Verdict (${verdictLabel}) `, [20, 20, 20], vc === themeColors.dim ? [130, 130, 130] : [255, 255, 255]))
|
|
383
|
-
} else {
|
|
384
|
-
filterParts.push(themeColors.dim(' Verdict (V) '))
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
// 📖 Health filter block — H key cycles through health states
|
|
388
|
-
if (healthFilterMode > 0) {
|
|
389
|
-
const healthLabel = HEALTH_CYCLE[healthFilterMode]
|
|
390
|
-
const healthDisplay = healthLabel === 'auth_error' ? 'Auth Err' : healthLabel === 'noauth' ? 'No Key' : healthLabel.charAt(0).toUpperCase() + healthLabel.slice(1)
|
|
391
|
-
const healthBg = healthLabel === 'up' ? [52, 120, 88] : healthLabel === 'timeout' ? [180, 130, 0] : healthLabel === 'down' ? [120, 40, 40] : [60, 60, 60]
|
|
392
|
-
filterParts.push(themeColors.badge(` Health (${healthDisplay}) `, healthBg, [255, 255, 255]))
|
|
393
|
-
} else {
|
|
394
|
-
filterParts.push(themeColors.dim(' Health (H) '))
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
lines.push(filterParts.join(blockSep))
|
|
398
|
-
}
|
|
399
|
-
|
|
400
324
|
// 📖 Header row with sorting indicators
|
|
401
325
|
// 📖 NOTE: padEnd on chalk strings counts ANSI codes, breaking alignment
|
|
402
326
|
// 📖 Solution: build plain text first, then colorize
|
|
@@ -495,8 +419,11 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
495
419
|
|
|
496
420
|
// 📖 Viewport clipping: only render models that fit on screen
|
|
497
421
|
const hasCustomFilter = typeof customTextFilter === 'string' && customTextFilter.trim().length > 0
|
|
498
|
-
const
|
|
499
|
-
const
|
|
422
|
+
const hasReleaseFooter = typeof lastReleaseDate === 'string' && lastReleaseDate.trim().length > 0
|
|
423
|
+
const extraFooterLines = (versionStatus.isOutdated ? 1 : 0) + (hasCustomFilter ? 1 : 0) + (hasReleaseFooter ? 1 : 0)
|
|
424
|
+
const vp = calculateViewport(terminalRows, scrollOffset, sorted.length, {
|
|
425
|
+
extraFixedLines: extraFooterLines,
|
|
426
|
+
})
|
|
500
427
|
const paintSweScore = (score, paddedText) => {
|
|
501
428
|
if (score >= 70) return chalk.bold.rgb(...getTierRgb('S+'))(paddedText)
|
|
502
429
|
if (score >= 60) return chalk.bold.rgb(...getTierRgb('S'))(paddedText)
|
|
@@ -537,10 +464,14 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
537
464
|
? providerName.slice(0, 4) + '…'
|
|
538
465
|
: providerName
|
|
539
466
|
const source = themeColors.provider(r.providerKey, providerDisplay.padEnd(wSource))
|
|
540
|
-
// 📖 Favorites
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
467
|
+
// 📖 Favorites marked with a single ⭐ — no ranking numbers
|
|
468
|
+
let favoritePrefix = ''
|
|
469
|
+
if (r.isRecommended) {
|
|
470
|
+
favoritePrefix = '🎯 '
|
|
471
|
+
} else if (r.isFavorite) {
|
|
472
|
+
favoritePrefix = '⭐ '
|
|
473
|
+
}
|
|
474
|
+
const prefixDisplayWidth = displayWidth(favoritePrefix)
|
|
544
475
|
const nameWidth = Math.max(0, W_MODEL - prefixDisplayWidth)
|
|
545
476
|
const name = favoritePrefix + r.label.slice(0, nameWidth).padEnd(nameWidth)
|
|
546
477
|
const sweScore = r.sweScore ?? '—'
|
|
@@ -786,7 +717,13 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
786
717
|
lines.push(themeColors.dim(` ... ${sorted.length - vp.endIdx} more below ...`))
|
|
787
718
|
}
|
|
788
719
|
|
|
789
|
-
|
|
720
|
+
// 📖 Blank lines keep the footer glued to the bottom without touching the sticky header.
|
|
721
|
+
if (terminalRows > 0) {
|
|
722
|
+
const footerLineCount = TABLE_FOOTER_LINES + extraFooterLines
|
|
723
|
+
const blankCount = Math.max(0, terminalRows - lines.length - footerLineCount)
|
|
724
|
+
for (let i = 0; i < blankCount; i++) lines.push('')
|
|
725
|
+
}
|
|
726
|
+
|
|
790
727
|
// 📖 Footer hints keep only navigation and secondary actions now that the
|
|
791
728
|
// 📖 active tool target is already visible in the header badge.
|
|
792
729
|
const hotkey = (keyLabel, text) => themeColors.hotkey(keyLabel) + themeColors.dim(text)
|
|
@@ -794,8 +731,6 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
794
731
|
// 📖 states are obvious even when the user misses the smaller header badges.
|
|
795
732
|
const configuredBadgeBg = getTheme() === 'dark' ? [52, 120, 88] : [195, 234, 206]
|
|
796
733
|
const activeHotkey = (keyLabel, text, bg) => themeColors.badge(`${keyLabel}${text}`, bg, getReadableTextRgb(bg))
|
|
797
|
-
const favoritesModeBg = favoritesPinnedAndSticky ? [157, 122, 48] : [95, 95, 95]
|
|
798
|
-
const favoritesModeLabel = favoritesPinnedAndSticky ? ' Favorites Pinned' : ' Favorites Normal'
|
|
799
734
|
|
|
800
735
|
// 📖 Mouse support: build footer hotkey zones alongside the footer lines.
|
|
801
736
|
// 📖 Each zone records { key, row (1-based terminal row), xStart, xEnd (1-based display cols) }.
|
|
@@ -807,21 +742,19 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
807
742
|
{
|
|
808
743
|
const parts = [
|
|
809
744
|
{ text: ' ', key: null },
|
|
810
|
-
{ text: 'F
|
|
745
|
+
{ text: 'F Favorite', key: 'f' },
|
|
811
746
|
{ text: ' • ', key: null },
|
|
812
|
-
{ text: 'Y'
|
|
747
|
+
{ text: 'Y Fav Mode', key: 'y' },
|
|
813
748
|
{ text: ' • ', key: null },
|
|
814
749
|
{ text: tierFilterMode > 0 ? `T Tier (${activeTierLabel})` : 'T Tier', key: 't' },
|
|
815
750
|
{ text: ' • ', key: null },
|
|
816
751
|
{ text: originFilterMode > 0 ? `D Provider (${activeOriginLabel})` : 'D Provider', key: 'd' },
|
|
817
752
|
{ text: ' • ', key: null },
|
|
818
|
-
{ text: 'E
|
|
753
|
+
{ text: 'E Active only', key: 'e' },
|
|
819
754
|
{ text: ' • ', key: null },
|
|
820
755
|
{ text: 'P Settings', key: 'p' },
|
|
821
756
|
{ text: ' • ', key: null },
|
|
822
|
-
{ text: '
|
|
823
|
-
{ text: ' • ', key: null },
|
|
824
|
-
{ text: 'Ctrl+H Help', key: 'ctrl+h' },
|
|
757
|
+
{ text: 'I Help', key: 'i' },
|
|
825
758
|
]
|
|
826
759
|
const footerRow1 = lines.length + 1 // 📖 1-based terminal row (line hasn't been pushed yet)
|
|
827
760
|
let xPos = 1
|
|
@@ -832,170 +765,108 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
832
765
|
}
|
|
833
766
|
}
|
|
834
767
|
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
(
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
(
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
{ text: ' • ', key: null },
|
|
870
|
-
{ text: 'I Feedback, bugs & requests', key: 'i' },
|
|
871
|
-
]
|
|
872
|
-
const footerRow2 = lines.length + 1
|
|
873
|
-
let xPos = 1
|
|
874
|
-
for (const part of parts) {
|
|
875
|
-
const w = displayWidth(part.text)
|
|
876
|
-
if (part.key) footerHotkeys.push({ key: part.key, row: footerRow2, xStart: xPos, xEnd: xPos + w - 1 })
|
|
877
|
-
xPos += w
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
// 📖 Line 2: command palette (highlighted as new), recommend, feedback, and extended hints.
|
|
882
|
-
// 📖 CTRL+P ⚡️ Command Palette uses neon-green-on-dark-green background to highlight the feature.
|
|
883
|
-
const paletteLabel = chalk.bgRgb(0, 60, 0).rgb(57, 255, 20).bold(' CTRL+P ⚡️ Command Palette ')
|
|
884
|
-
lines.push(
|
|
885
|
-
' ' + paletteLabel + themeColors.dim(` • `) +
|
|
886
|
-
hotkey('Q', ' Smart Recommend') + themeColors.dim(` • `) +
|
|
887
|
-
hotkey('G', ' Theme') + themeColors.dim(` • `) +
|
|
888
|
-
hotkey('I', ' Feedback, bugs & requests')
|
|
889
|
-
)
|
|
890
|
-
// 📖 Proxy status is now shown via the badge in line 2 above — no need for a dedicated line
|
|
891
|
-
const footerLine =
|
|
892
|
-
themeColors.footerLove(' Made with 💖 & ☕ by \x1b]8;;https://github.com/vava-nessa\x1b\\vava-nessa\x1b]8;;\x1b\\') +
|
|
893
|
-
themeColors.dim(' • ') +
|
|
894
|
-
'⭐ ' +
|
|
895
|
-
themeColors.link('\x1b]8;;https://github.com/vava-nessa/free-coding-models\x1b\\Star on GitHub\x1b]8;;\x1b\\') +
|
|
896
|
-
themeColors.dim(' • ') +
|
|
897
|
-
'🤝 ' +
|
|
898
|
-
themeColors.warning('\x1b]8;;https://github.com/vava-nessa/free-coding-models/graphs/contributors\x1b\\Contributors\x1b]8;;\x1b\\') +
|
|
899
|
-
themeColors.dim(' • ') +
|
|
900
|
-
'☕ ' +
|
|
901
|
-
themeColors.footerCoffee('\x1b]8;;https://buymeacoffee.com/vavanessadev\x1b\\Buy me a coffee\x1b]8;;\x1b\\')
|
|
902
|
-
lines.push(footerLine)
|
|
903
|
-
|
|
904
|
-
if (versionStatus.isOutdated) {
|
|
905
|
-
const updateMsg = ` 🚀⬆️ UPDATE AVAILABLE — v${LOCAL_VERSION} → v${versionStatus.latestVersion} • Click here or press Shift+U to update 🚀⬆️ `
|
|
906
|
-
const paddedBanner = terminalCols > 0
|
|
907
|
-
? updateMsg + ' '.repeat(Math.max(0, terminalCols - displayWidth(updateMsg)))
|
|
908
|
-
: updateMsg
|
|
909
|
-
const fluoGreenBanner = chalk.bgRgb(57, 255, 20).rgb(0, 0, 0).bold(paddedBanner)
|
|
910
|
-
const updateBannerRow = lines.length + 1
|
|
911
|
-
_lastLayout.updateBannerRow = updateBannerRow
|
|
912
|
-
footerHotkeys.push({ key: 'update-click', row: updateBannerRow, xStart: 1, xEnd: Math.max(terminalCols, displayWidth(updateMsg)) })
|
|
913
|
-
lines.push(fluoGreenBanner)
|
|
914
|
-
} else {
|
|
915
|
-
_lastLayout.updateBannerRow = 0
|
|
768
|
+
lines.push(
|
|
769
|
+
' ' + hotkey('F', ' Favorite') +
|
|
770
|
+
themeColors.dim(` • `) +
|
|
771
|
+
hotkey('Y', ' Fav Mode') +
|
|
772
|
+
themeColors.dim(` • `) +
|
|
773
|
+
(tierFilterMode > 0
|
|
774
|
+
? activeHotkey('T', ` Tier (${activeTierLabel})`, getTierRgb(activeTierLabel))
|
|
775
|
+
: hotkey('T', ' Tier')) +
|
|
776
|
+
themeColors.dim(` • `) +
|
|
777
|
+
(originFilterMode > 0
|
|
778
|
+
? activeHotkey('D', ` Provider (${activeOriginLabel})`, PROVIDER_COLOR[[null, ...Object.keys(sources)][originFilterMode]] || [255, 255, 255])
|
|
779
|
+
: hotkey('D', ' Provider')) +
|
|
780
|
+
themeColors.dim(` • `) +
|
|
781
|
+
(hideUnconfiguredModels ? activeHotkey('E', ' Active only', configuredBadgeBg) : hotkey('E', ' Active only')) +
|
|
782
|
+
themeColors.dim(` • `) +
|
|
783
|
+
hotkey('P', ' Settings') +
|
|
784
|
+
themeColors.dim(` • `) +
|
|
785
|
+
hotkey('I', ' Help')
|
|
786
|
+
)
|
|
787
|
+
|
|
788
|
+
// 📖 Line 2: command palette + GitHub
|
|
789
|
+
{
|
|
790
|
+
const cpText = ' Ctrl+P Cmd Palette '
|
|
791
|
+
const parts = [
|
|
792
|
+
{ text: ' ', key: null },
|
|
793
|
+
{ text: cpText, key: 'ctrl+p' },
|
|
794
|
+
{ text: ' ', key: null },
|
|
795
|
+
]
|
|
796
|
+
const footerRow2 = lines.length + 1
|
|
797
|
+
let xPos = 1
|
|
798
|
+
for (const part of parts) {
|
|
799
|
+
const w = displayWidth(part.text)
|
|
800
|
+
if (part.key) footerHotkeys.push({ key: part.key, row: footerRow2, xStart: xPos, xEnd: xPos + w - 1 })
|
|
801
|
+
xPos += w
|
|
916
802
|
}
|
|
803
|
+
}
|
|
917
804
|
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
805
|
+
// 📖 Line 2: command palette (highlighted as new) + GitHub link.
|
|
806
|
+
// 📖 Ctrl+P Cmd Palette uses neon-green-on-dark-green background to highlight the feature.
|
|
807
|
+
const paletteLabel = chalk.bgRgb(0, 60, 0).rgb(57, 255, 20).bold(' Ctrl+P Cmd Palette ')
|
|
808
|
+
const starLink = '⭐ ' + themeColors.link('\x1b]8;;https://github.com/vava-nessa/free-coding-models\x1b\\GitHub\x1b]8;;\x1b\\')
|
|
809
|
+
lines.push(
|
|
810
|
+
' ' + paletteLabel + themeColors.dim(` • `) + starLink + themeColors.dim(` • `) +
|
|
811
|
+
chalk.rgb(255, 168, 209).bold('\x1b]8;;https://x.com/vavanessadev\x1b\\Support me by following me on X ! @vavanessadev\x1b]8;;\x1b\\')
|
|
812
|
+
)
|
|
813
|
+
|
|
814
|
+
if (versionStatus.isOutdated) {
|
|
815
|
+
const updateMsg = ` 🚀⬆️ UPDATE AVAILABLE — v${LOCAL_VERSION} → v${versionStatus.latestVersion} • Click here or press Shift+U to update 🚀⬆️ `
|
|
816
|
+
const paddedBanner = terminalCols > 0
|
|
817
|
+
? updateMsg + ' '.repeat(Math.max(0, terminalCols - displayWidth(updateMsg)))
|
|
818
|
+
: updateMsg
|
|
819
|
+
const fluoGreenBanner = chalk.bgRgb(57, 255, 20).rgb(0, 0, 0).bold(paddedBanner)
|
|
820
|
+
const updateBannerRow = lines.length + 1
|
|
821
|
+
_lastLayout.updateBannerRow = updateBannerRow
|
|
822
|
+
footerHotkeys.push({ key: 'update-click', row: updateBannerRow, xStart: 1, xEnd: Math.max(terminalCols, displayWidth(updateMsg)) })
|
|
823
|
+
lines.push(fluoGreenBanner)
|
|
824
|
+
} else {
|
|
825
|
+
_lastLayout.updateBannerRow = 0
|
|
826
|
+
}
|
|
935
827
|
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
xPos += w
|
|
954
|
-
}
|
|
955
|
-
}
|
|
828
|
+
// 📖 Optional active text-filter badge — surfaced inline if a custom filter is active.
|
|
829
|
+
// 📖 Changelog moved to Settings (P), Ctrl+C Exit moved to Help (Ctrl+H), Discord
|
|
830
|
+
// 📖 moved to onboarding + Settings — no more orphan hint lines down here.
|
|
831
|
+
let filterBadge = ''
|
|
832
|
+
if (hasCustomFilter) {
|
|
833
|
+
const normalizedFilter = customTextFilter.trim().replace(/\s+/g, ' ')
|
|
834
|
+
const filterPrefix = 'X Disable filter: "'
|
|
835
|
+
const filterSuffix = '"'
|
|
836
|
+
const baseBadgeWidth = displayWidth(` ${filterPrefix}${filterSuffix} `)
|
|
837
|
+
const availableFilterWidth = terminalCols > 0
|
|
838
|
+
? Math.max(8, terminalCols - 4 - baseBadgeWidth)
|
|
839
|
+
: normalizedFilter.length
|
|
840
|
+
const visibleFilter = normalizedFilter.length > availableFilterWidth
|
|
841
|
+
? `${normalizedFilter.slice(0, Math.max(3, availableFilterWidth - 3))}...`
|
|
842
|
+
: normalizedFilter
|
|
843
|
+
filterBadge = chalk.bgYellow.black.bold(` ${filterPrefix}${visibleFilter}${filterSuffix} `)
|
|
844
|
+
}
|
|
956
845
|
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
themeColors.dim('Ctrl+C Exit') +
|
|
968
|
-
(releaseLabel ? themeColors.dim(' • ') + releaseLabel : '')
|
|
969
|
-
)
|
|
970
|
-
|
|
971
|
-
// 📖 Discord link at the very bottom of the TUI
|
|
972
|
-
lines.push(
|
|
973
|
-
' 💬 ' +
|
|
974
|
-
themeColors.footerDiscord('\x1b]8;;https://discord.gg/ZTNFHvvCkU\x1b\\Join the Discord\x1b]8;;\x1b\\') +
|
|
975
|
-
themeColors.dim(' → ') +
|
|
976
|
-
themeColors.footerDiscord('https://discord.gg/ZTNFHvvCkU')
|
|
977
|
-
)
|
|
978
|
-
} else {
|
|
979
|
-
// 📖 Collapsed footer: single line with toggle hint
|
|
980
|
-
lines.push(
|
|
981
|
-
' ' + themeColors.hotkey('Ctrl+O') + themeColors.dim(' Toggle Footer') +
|
|
982
|
-
themeColors.dim(' • Ctrl+C Exit')
|
|
983
|
-
)
|
|
846
|
+
if (hasCustomFilter) {
|
|
847
|
+
// 📖 Mouse support: register click zone for the X-clear filter badge
|
|
848
|
+
const lastFooterRow = lines.length + 1
|
|
849
|
+
const badgePlain = `X Disable filter: "${customTextFilter.trim().replace(/\s+/g, ' ')}"`
|
|
850
|
+
const fullText = ' ' + ` ${badgePlain} `
|
|
851
|
+
const xStart = 3 // 📖 after the leading 2 spaces
|
|
852
|
+
const xEnd = xStart + displayWidth(` ${badgePlain} `) - 1
|
|
853
|
+
footerHotkeys.push({ key: 'x', row: lastFooterRow, xStart, xEnd })
|
|
854
|
+
void fullText
|
|
855
|
+
lines.push(' ' + filterBadge)
|
|
984
856
|
}
|
|
985
857
|
|
|
858
|
+
const releaseLabel = lastReleaseDate
|
|
859
|
+
? chalk.rgb(255, 182, 193)(`Last release: ${lastReleaseDate}`)
|
|
860
|
+
: ''
|
|
861
|
+
|
|
862
|
+
if (releaseLabel) lines.push(' ' + releaseLabel)
|
|
986
863
|
_lastLayout.footerHotkeys = footerHotkeys
|
|
987
864
|
|
|
988
865
|
// 📖 Append \x1b[K (erase to EOL) to each line so leftover chars from previous
|
|
989
|
-
// 📖 frames are cleared. \x1b[J
|
|
990
|
-
// 📖
|
|
866
|
+
// 📖 frames are cleared. \x1b[J clears stale content below without adding a
|
|
867
|
+
// 📖 newline that could scroll the alternate screen.
|
|
991
868
|
const EL = '\x1b[K'
|
|
992
869
|
const cleared = lines.map(l => l + EL)
|
|
993
|
-
if (
|
|
994
|
-
// 📖 When footer is hidden, \x1b[J erases stale footer content below the cursor
|
|
995
|
-
cleared.push('\x1b[J')
|
|
996
|
-
} else {
|
|
997
|
-
const remaining = terminalRows > 0 ? Math.max(0, terminalRows - cleared.length) : 0
|
|
998
|
-
for (let i = 0; i < remaining; i++) cleared.push(EL)
|
|
999
|
-
}
|
|
870
|
+
if (cleared.length > 0) cleared[cleared.length - 1] += '\x1b[J'
|
|
1000
871
|
return cleared.join('\n')
|
|
1001
872
|
}
|