free-coding-models 0.3.17 → 0.3.18
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 +13 -0
- package/README.md +9 -1
- package/package.json +1 -1
- package/src/app.js +18 -2
- package/src/config.js +3 -3
- package/src/key-handler.js +177 -46
- package/src/openclaw.js +39 -5
- package/src/opencode.js +2 -1
- package/src/overlays.js +300 -207
- package/src/render-helpers.js +1 -1
- package/src/render-table.js +140 -177
- package/src/theme.js +291 -43
- package/src/tier-colors.js +15 -17
- package/src/tool-bootstrap.js +310 -0
- package/src/tool-launchers.js +12 -7
- package/src/ui-config.js +24 -31
package/src/render-table.js
CHANGED
|
@@ -43,7 +43,7 @@ import {
|
|
|
43
43
|
PING_INTERVAL,
|
|
44
44
|
FRAMES
|
|
45
45
|
} from './constants.js'
|
|
46
|
-
import { themeColors } from './theme.js'
|
|
46
|
+
import { themeColors, getProviderRgb, getTierRgb, getReadableTextRgb, getTheme } from './theme.js'
|
|
47
47
|
import { TIER_COLOR } from './tier-colors.js'
|
|
48
48
|
import { getAvg, getVerdict, getUptime, getStabilityScore, getVersionStatusInfo } from './utils.js'
|
|
49
49
|
import { usagePlaceholderForProvider } from './ping.js'
|
|
@@ -51,27 +51,7 @@ import { formatTokenTotalCompact } from './token-usage-reader.js'
|
|
|
51
51
|
import { calculateViewport, sortResultsWithPinnedFavorites, padEndDisplay, displayWidth } from './render-helpers.js'
|
|
52
52
|
import { getToolMeta } from './tool-metadata.js'
|
|
53
53
|
import { PROXY_DISABLED_NOTICE } from './product-flags.js'
|
|
54
|
-
|
|
55
|
-
const ACTIVE_FILTER_BG_BY_TIER = {
|
|
56
|
-
'S+': [57, 255, 20],
|
|
57
|
-
'S': [57, 255, 20],
|
|
58
|
-
'A+': [160, 255, 60],
|
|
59
|
-
'A': [255, 224, 130],
|
|
60
|
-
'A-': [255, 204, 128],
|
|
61
|
-
'B+': [255, 171, 64],
|
|
62
|
-
'B': [239, 83, 80],
|
|
63
|
-
'C': [186, 104, 200],
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// 📖 Import UI configuration for consistent styling
|
|
67
|
-
import { VERTICAL_SEPARATOR, COLUMN_SPACING } from './ui-config.js';
|
|
68
|
-
|
|
69
|
-
// 📖 Column separator (vertical bar) is now defined in ui-config.js
|
|
70
|
-
// const VERTICAL_SEPARATOR = chalk.rgb(255, 140, 0).dim('│');
|
|
71
|
-
// const COL_SEP = ` ${VERTICAL_SEPARATOR} `; // Replaced by imported COLUMN_SPACING
|
|
72
|
-
|
|
73
|
-
// 📖 Column spacing is now defined in ui-config.js
|
|
74
|
-
const COL_SEP = COLUMN_SPACING;
|
|
54
|
+
import { getColumnSpacing } from './ui-config.js'
|
|
75
55
|
|
|
76
56
|
const require = createRequire(import.meta.url)
|
|
77
57
|
const { version: LOCAL_VERSION } = require('../package.json')
|
|
@@ -79,28 +59,12 @@ const { version: LOCAL_VERSION } = require('../package.json')
|
|
|
79
59
|
// 📖 Provider column palette: soft pastel rainbow so each provider stays easy
|
|
80
60
|
// 📖 to spot without turning the table into a harsh neon wall.
|
|
81
61
|
// 📖 Exported for use in overlays (settings screen) and logs.
|
|
82
|
-
export const PROVIDER_COLOR = {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
huggingface: [255, 245, 157],
|
|
89
|
-
replicate: [187, 222, 251],
|
|
90
|
-
deepinfra: [178, 223, 219],
|
|
91
|
-
fireworks: [255, 205, 210],
|
|
92
|
-
codestral: [248, 187, 208],
|
|
93
|
-
hyperbolic: [255, 171, 145],
|
|
94
|
-
scaleway: [129, 212, 250],
|
|
95
|
-
googleai: [187, 222, 251],
|
|
96
|
-
siliconflow: [178, 235, 242],
|
|
97
|
-
together: [255, 241, 118],
|
|
98
|
-
cloudflare: [255, 204, 128],
|
|
99
|
-
perplexity: [244, 143, 177],
|
|
100
|
-
qwen: [255, 224, 130],
|
|
101
|
-
zai: [174, 213, 255],
|
|
102
|
-
iflow: [220, 231, 117],
|
|
103
|
-
}
|
|
62
|
+
export const PROVIDER_COLOR = new Proxy({}, {
|
|
63
|
+
get(_target, providerKey) {
|
|
64
|
+
if (typeof providerKey !== 'string') return undefined
|
|
65
|
+
return getProviderRgb(providerKey)
|
|
66
|
+
},
|
|
67
|
+
})
|
|
104
68
|
|
|
105
69
|
// ─── renderTable: mode param controls footer hint text (opencode vs openclaw) ─────────
|
|
106
70
|
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, disableWidthsWarning = false) {
|
|
@@ -122,23 +86,23 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
122
86
|
|
|
123
87
|
const intervalSec = Math.round(pingInterval / 1000)
|
|
124
88
|
const pingModeMeta = {
|
|
125
|
-
speed: { label: 'fast', color:
|
|
126
|
-
normal: { label: 'normal', color:
|
|
127
|
-
slow: { label: 'slow', color:
|
|
128
|
-
forced: { label: 'forced', color:
|
|
89
|
+
speed: { label: 'fast', color: themeColors.warningBold },
|
|
90
|
+
normal: { label: 'normal', color: themeColors.accentBold },
|
|
91
|
+
slow: { label: 'slow', color: themeColors.info },
|
|
92
|
+
forced: { label: 'forced', color: themeColors.errorBold },
|
|
129
93
|
}
|
|
130
94
|
const activePingMode = pingModeMeta[pingMode] ?? pingModeMeta.normal
|
|
131
95
|
const pingProgressText = `${completedPings}/${totalVisible}`
|
|
132
96
|
const nextCountdownColor = secondsUntilNext > 8
|
|
133
|
-
?
|
|
97
|
+
? themeColors.errorBold
|
|
134
98
|
: secondsUntilNext >= 4
|
|
135
|
-
?
|
|
99
|
+
? themeColors.warningBold
|
|
136
100
|
: secondsUntilNext < 1
|
|
137
|
-
?
|
|
138
|
-
:
|
|
101
|
+
? themeColors.successBold
|
|
102
|
+
: themeColors.success
|
|
139
103
|
const pingControlBadge =
|
|
140
104
|
activePingMode.color(' [ ') +
|
|
141
|
-
|
|
105
|
+
themeColors.hotkey('W') +
|
|
142
106
|
activePingMode.color(` Ping Interval : ${intervalSec}s (${activePingMode.label}) - ${pingProgressText} - next : `) +
|
|
143
107
|
nextCountdownColor(`${secondsUntilNextLabel}s`) +
|
|
144
108
|
activePingMode.color(' ]')
|
|
@@ -146,11 +110,9 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
146
110
|
// 📖 Tool badge keeps the active launch target visible in the header, so the
|
|
147
111
|
// 📖 footer no longer needs a redundant Enter action or mode toggle reminder.
|
|
148
112
|
const toolMeta = getToolMeta(mode)
|
|
149
|
-
const toolBadgeColor = mode === 'openclaw'
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const modeBadge = toolBadgeColor(' [ ') + chalk.yellow.bold('Z') + toolBadgeColor(` Tool : ${toolMeta.label} ]`)
|
|
153
|
-
const activeHeaderBadge = (text, bg = [57, 255, 20], fg = [0, 0, 0]) => chalk.bgRgb(...bg).rgb(...fg).bold(` ${text} `)
|
|
113
|
+
const toolBadgeColor = mode === 'openclaw' ? themeColors.warningBold : themeColors.accentBold
|
|
114
|
+
const modeBadge = toolBadgeColor(' [ ') + themeColors.hotkey('Z') + toolBadgeColor(` Tool : ${toolMeta.label} ]`)
|
|
115
|
+
const activeHeaderBadge = (text, bg) => themeColors.badge(text, bg, getReadableTextRgb(bg))
|
|
154
116
|
const versionStatus = getVersionStatusInfo(settingsUpdateState, settingsUpdateLatestVersion, startupLatestVersion, versionAlertsEnabled)
|
|
155
117
|
|
|
156
118
|
// 📖 Tier filter badge shown when filtering is active (shows exact tier name)
|
|
@@ -159,7 +121,7 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
159
121
|
let activeTierLabel = ''
|
|
160
122
|
if (tierFilterMode > 0) {
|
|
161
123
|
activeTierLabel = TIER_CYCLE_NAMES[tierFilterMode]
|
|
162
|
-
const tierBg =
|
|
124
|
+
const tierBg = getTierRgb(activeTierLabel)
|
|
163
125
|
tierBadge = ` ${activeHeaderBadge(`TIER (${activeTierLabel})`, tierBg)}`
|
|
164
126
|
}
|
|
165
127
|
|
|
@@ -178,13 +140,12 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
178
140
|
if (activeOriginName) {
|
|
179
141
|
activeOriginLabel = normalizeOriginLabel(activeOriginName, activeOriginKey)
|
|
180
142
|
const providerRgb = PROVIDER_COLOR[activeOriginKey] || [255, 255, 255]
|
|
181
|
-
originBadge = ` ${activeHeaderBadge(`PROVIDER (${activeOriginLabel})`,
|
|
143
|
+
originBadge = ` ${activeHeaderBadge(`PROVIDER (${activeOriginLabel})`, providerRgb)}`
|
|
182
144
|
}
|
|
183
145
|
}
|
|
184
146
|
|
|
185
|
-
|
|
186
|
-
|
|
187
147
|
// 📖 Column widths (generous spacing with margins)
|
|
148
|
+
const COL_SEP = getColumnSpacing()
|
|
188
149
|
const W_RANK = 6
|
|
189
150
|
const W_TIER = 6
|
|
190
151
|
const W_CTX = 6
|
|
@@ -215,18 +176,18 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
215
176
|
const padLeft2 = Math.max(0, Math.floor((terminalCols - warning2.length) / 2))
|
|
216
177
|
const padLeft3 = Math.max(0, Math.floor((terminalCols - warning3.length) / 2))
|
|
217
178
|
for (let i = 0; i < blankLines; i++) lines.push('')
|
|
218
|
-
lines.push(' '.repeat(padLeft) +
|
|
179
|
+
lines.push(' '.repeat(padLeft) + themeColors.errorBold(warning))
|
|
219
180
|
lines.push('')
|
|
220
|
-
lines.push(' '.repeat(padLeft2) +
|
|
181
|
+
lines.push(' '.repeat(padLeft2) + themeColors.error(warning2))
|
|
221
182
|
lines.push('')
|
|
222
|
-
lines.push(' '.repeat(padLeft3) +
|
|
183
|
+
lines.push(' '.repeat(padLeft3) + themeColors.error(warning3))
|
|
223
184
|
lines.push('')
|
|
224
|
-
lines.push(' '.repeat(Math.max(0, Math.floor((terminalCols - 34) / 2))) +
|
|
185
|
+
lines.push(' '.repeat(Math.max(0, Math.floor((terminalCols - 34) / 2))) + themeColors.warning(`this message will hide in ${(remainingMs / 1000).toFixed(1)}s`))
|
|
225
186
|
const barTotal = Math.max(0, Math.min(terminalCols - 4, 30))
|
|
226
187
|
const barFill = Math.round((elapsed / warningDurationMs) * barTotal)
|
|
227
|
-
const barStr =
|
|
188
|
+
const barStr = themeColors.success('█'.repeat(barFill)) + themeColors.dim('░'.repeat(barTotal - barFill))
|
|
228
189
|
lines.push(' '.repeat(Math.max(0, Math.floor((terminalCols - barTotal) / 2))) + barStr)
|
|
229
|
-
lines.push(' '.repeat(Math.max(0, Math.floor((terminalCols - 20) / 2))) +
|
|
190
|
+
lines.push(' '.repeat(Math.max(0, Math.floor((terminalCols - 20) / 2))) + themeColors.dim('press esc to dismiss'))
|
|
230
191
|
while (terminalRows > 0 && lines.length < terminalRows) lines.push('')
|
|
231
192
|
const EL = '\x1b[K'
|
|
232
193
|
return lines.map(line => line + EL).join('\n')
|
|
@@ -236,11 +197,11 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
236
197
|
const sorted = sortResultsWithPinnedFavorites(visibleResults, sortColumn, sortDirection)
|
|
237
198
|
|
|
238
199
|
const lines = [
|
|
239
|
-
` ${
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
200
|
+
` ${themeColors.accentBold(`🚀 free-coding-models v${LOCAL_VERSION}`)}${modeBadge}${pingControlBadge}${tierBadge}${originBadge}${chalk.reset('')} ` +
|
|
201
|
+
themeColors.dim('📦 ') + themeColors.accentBold(`${completedPings}/${totalVisible}`) + themeColors.dim(' ') +
|
|
202
|
+
themeColors.success(`✅ ${up}`) + themeColors.dim(' up ') +
|
|
203
|
+
themeColors.warning(`⏳ ${timeout}`) + themeColors.dim(' timeout ') +
|
|
204
|
+
themeColors.error(`❌ ${down}`) + themeColors.dim(' down ') +
|
|
244
205
|
'',
|
|
245
206
|
'',
|
|
246
207
|
]
|
|
@@ -263,16 +224,15 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
263
224
|
const stabH = sortColumn === 'stability' ? dir + ' Stability' : 'Stability'
|
|
264
225
|
const uptimeH = sortColumn === 'uptime' ? dir + ' Up%' : 'Up%'
|
|
265
226
|
const tokensH = 'Used'
|
|
266
|
-
const usageH = sortColumn === 'usage' ? dir + ' Usage' : 'Usage'
|
|
267
227
|
|
|
268
228
|
// 📖 Helper to colorize first letter for keyboard shortcuts
|
|
269
229
|
// 📖 IMPORTANT: Pad PLAIN TEXT first, then apply colors to avoid alignment issues
|
|
270
|
-
const colorFirst = (text, width, colorFn =
|
|
230
|
+
const colorFirst = (text, width, colorFn = themeColors.hotkey) => {
|
|
271
231
|
const first = text[0]
|
|
272
232
|
const rest = text.slice(1)
|
|
273
233
|
const plainText = first + rest
|
|
274
234
|
const padding = ' '.repeat(Math.max(0, width - plainText.length))
|
|
275
|
-
return colorFn(first) +
|
|
235
|
+
return colorFn(first) + themeColors.dim(rest + padding)
|
|
276
236
|
}
|
|
277
237
|
|
|
278
238
|
// 📖 Now colorize after padding is calculated on plain text
|
|
@@ -280,33 +240,33 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
280
240
|
const tierH_c = colorFirst('Tier', W_TIER)
|
|
281
241
|
const originLabel = 'Provider'
|
|
282
242
|
const originH_c = sortColumn === 'origin'
|
|
283
|
-
?
|
|
284
|
-
: (originFilterMode > 0 ?
|
|
243
|
+
? themeColors.accentBold(originLabel.padEnd(W_SOURCE))
|
|
244
|
+
: (originFilterMode > 0 ? themeColors.accentBold(originLabel.padEnd(W_SOURCE)) : (() => {
|
|
285
245
|
// 📖 Provider keeps O for sorting and D for provider-filter cycling.
|
|
286
246
|
const plain = 'PrOviDer'
|
|
287
247
|
const padding = ' '.repeat(Math.max(0, W_SOURCE - plain.length))
|
|
288
|
-
return
|
|
248
|
+
return themeColors.dim('Pr') + themeColors.hotkey('O') + themeColors.dim('vi') + themeColors.hotkey('D') + themeColors.dim('er' + padding)
|
|
289
249
|
})())
|
|
290
250
|
const modelH_c = colorFirst(modelH, W_MODEL)
|
|
291
|
-
const sweH_c = sortColumn === 'swe' ?
|
|
292
|
-
const ctxH_c = sortColumn === 'ctx' ?
|
|
293
|
-
const pingH_c = sortColumn === 'ping' ?
|
|
294
|
-
const avgH_c = sortColumn === 'avg' ?
|
|
295
|
-
const healthH_c = sortColumn === 'condition' ?
|
|
296
|
-
const verdictH_c = sortColumn === 'verdict' ?
|
|
251
|
+
const sweH_c = sortColumn === 'swe' ? themeColors.accentBold(sweH.padEnd(W_SWE)) : colorFirst(sweH, W_SWE)
|
|
252
|
+
const ctxH_c = sortColumn === 'ctx' ? themeColors.accentBold(ctxH.padEnd(W_CTX)) : colorFirst(ctxH, W_CTX)
|
|
253
|
+
const pingH_c = sortColumn === 'ping' ? themeColors.accentBold(pingH.padEnd(W_PING)) : colorFirst('Latest Ping', W_PING)
|
|
254
|
+
const avgH_c = sortColumn === 'avg' ? themeColors.accentBold(avgH.padEnd(W_AVG)) : colorFirst('Avg Ping', W_AVG)
|
|
255
|
+
const healthH_c = sortColumn === 'condition' ? themeColors.accentBold(healthH.padEnd(W_STATUS)) : colorFirst('Health', W_STATUS)
|
|
256
|
+
const verdictH_c = sortColumn === 'verdict' ? themeColors.accentBold(verdictH.padEnd(W_VERDICT)) : colorFirst(verdictH, W_VERDICT)
|
|
297
257
|
// 📖 Custom colorization for Stability: highlight 'B' (the sort key) since 'S' is taken by SWE
|
|
298
|
-
const stabH_c = sortColumn === 'stability' ?
|
|
258
|
+
const stabH_c = sortColumn === 'stability' ? themeColors.accentBold(stabH.padEnd(W_STAB)) : (() => {
|
|
299
259
|
const plain = 'Stability'
|
|
300
260
|
const padding = ' '.repeat(Math.max(0, W_STAB - plain.length))
|
|
301
|
-
return
|
|
261
|
+
return themeColors.dim('Sta') + themeColors.hotkey('B') + themeColors.dim('ility' + padding)
|
|
302
262
|
})()
|
|
303
263
|
// 📖 Up% sorts on U, so keep the highlighted shortcut in the shared yellow sort-key color.
|
|
304
|
-
const uptimeH_c = sortColumn === 'uptime' ?
|
|
264
|
+
const uptimeH_c = sortColumn === 'uptime' ? themeColors.accentBold(uptimeH.padEnd(W_UPTIME)) : (() => {
|
|
305
265
|
const plain = 'Up%'
|
|
306
266
|
const padding = ' '.repeat(Math.max(0, W_UPTIME - plain.length))
|
|
307
|
-
return
|
|
267
|
+
return themeColors.hotkey('U') + themeColors.dim('p%' + padding)
|
|
308
268
|
})()
|
|
309
|
-
const tokensH_c =
|
|
269
|
+
const tokensH_c = themeColors.dim(tokensH.padEnd(W_TOKENS))
|
|
310
270
|
// 📖 Usage column removed from UI – no header or separator for it.
|
|
311
271
|
// Header without Usage column (column order: Rank, Tier, SWE%, CTX, Model, Provider, Latest Ping, Avg Ping, Health, Verdict, Stability, Up%, Used)
|
|
312
272
|
lines.push(' ' + rankH_c + COL_SEP + tierH_c + COL_SEP + sweH_c + COL_SEP + ctxH_c + COL_SEP + modelH_c + COL_SEP + originH_c + COL_SEP + pingH_c + COL_SEP + avgH_c + COL_SEP + healthH_c + COL_SEP + verdictH_c + COL_SEP + stabH_c + COL_SEP + uptimeH_c + COL_SEP + tokensH_c)
|
|
@@ -316,42 +276,50 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
316
276
|
if (sorted.length === 0) {
|
|
317
277
|
lines.push('')
|
|
318
278
|
if (hideUnconfiguredModels) {
|
|
319
|
-
lines.push(` ${
|
|
320
|
-
lines.push(` ${
|
|
279
|
+
lines.push(` ${themeColors.errorBold('Press P to configure your API key.')}`)
|
|
280
|
+
lines.push(` ${themeColors.dim('No configured provider currently exposes visible models in the table.')}`)
|
|
321
281
|
} else {
|
|
322
|
-
lines.push(` ${
|
|
282
|
+
lines.push(` ${themeColors.warningBold('No models match the current filters.')}`)
|
|
323
283
|
}
|
|
324
284
|
}
|
|
325
285
|
|
|
326
286
|
// 📖 Viewport clipping: only render models that fit on screen
|
|
327
287
|
const extraFooterLines = versionStatus.isOutdated ? 1 : 0
|
|
328
288
|
const vp = calculateViewport(terminalRows, scrollOffset, sorted.length, extraFooterLines)
|
|
289
|
+
const paintSweScore = (score, paddedText) => {
|
|
290
|
+
if (score >= 70) return chalk.bold.rgb(...getTierRgb('S+'))(paddedText)
|
|
291
|
+
if (score >= 60) return chalk.bold.rgb(...getTierRgb('S'))(paddedText)
|
|
292
|
+
if (score >= 50) return chalk.bold.rgb(...getTierRgb('A+'))(paddedText)
|
|
293
|
+
if (score >= 40) return chalk.rgb(...getTierRgb('A'))(paddedText)
|
|
294
|
+
if (score >= 35) return chalk.rgb(...getTierRgb('A-'))(paddedText)
|
|
295
|
+
if (score >= 30) return chalk.rgb(...getTierRgb('B+'))(paddedText)
|
|
296
|
+
if (score >= 20) return chalk.rgb(...getTierRgb('B'))(paddedText)
|
|
297
|
+
return chalk.rgb(...getTierRgb('C'))(paddedText)
|
|
298
|
+
}
|
|
329
299
|
|
|
330
300
|
if (vp.hasAbove) {
|
|
331
|
-
lines.push(
|
|
301
|
+
lines.push(themeColors.dim(` ... ${vp.startIdx} more above ...`))
|
|
332
302
|
}
|
|
333
303
|
|
|
334
304
|
for (let i = vp.startIdx; i < vp.endIdx; i++) {
|
|
335
305
|
const r = sorted[i]
|
|
336
|
-
const tierFn = TIER_COLOR[r.tier] ?? (
|
|
306
|
+
const tierFn = TIER_COLOR[r.tier] ?? ((text) => themeColors.text(text))
|
|
337
307
|
|
|
338
308
|
const isCursor = cursor !== null && i === cursor
|
|
339
309
|
|
|
340
310
|
// 📖 Left-aligned columns - pad plain text first, then colorize
|
|
341
|
-
const num =
|
|
311
|
+
const num = themeColors.dim(String(r.idx).padEnd(W_RANK))
|
|
342
312
|
const tier = tierFn(r.tier.padEnd(W_TIER))
|
|
343
313
|
// 📖 Keep terminal view provider-specific so each row is monitorable per provider
|
|
344
314
|
const providerNameRaw = sources[r.providerKey]?.name ?? r.providerKey ?? 'NIM'
|
|
345
315
|
const providerName = normalizeOriginLabel(providerNameRaw, r.providerKey)
|
|
346
|
-
const
|
|
347
|
-
const source = chalk.rgb(...providerRgb)(providerName.padEnd(W_SOURCE))
|
|
316
|
+
const source = themeColors.provider(r.providerKey, providerName.padEnd(W_SOURCE))
|
|
348
317
|
// 📖 Favorites: always reserve 2 display columns at the start of Model column.
|
|
349
318
|
// 📖 🎯 (2 cols) for recommended, ⭐ (2 cols) for favorites, ' ' (2 spaces) for non-favorites — keeps alignment stable.
|
|
350
319
|
const favoritePrefix = r.isRecommended ? '🎯' : r.isFavorite ? '⭐' : ' '
|
|
351
320
|
const prefixDisplayWidth = 2
|
|
352
321
|
const nameWidth = Math.max(0, W_MODEL - prefixDisplayWidth)
|
|
353
322
|
const name = favoritePrefix + r.label.slice(0, nameWidth).padEnd(nameWidth)
|
|
354
|
-
const modelColor = chalk.rgb(...providerRgb)
|
|
355
323
|
const sweScore = r.sweScore ?? '—'
|
|
356
324
|
// 📖 SWE% colorized on the same gradient as Tier:
|
|
357
325
|
// ≥70% bright neon green (S+), ≥60% green (S), ≥50% yellow-green (A+),
|
|
@@ -359,27 +327,20 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
359
327
|
// ≥20% red (B), <20% dark red (C), '—' dim
|
|
360
328
|
let sweCell
|
|
361
329
|
if (sweScore === '—') {
|
|
362
|
-
sweCell =
|
|
330
|
+
sweCell = themeColors.dim(sweScore.padEnd(W_SWE))
|
|
363
331
|
} else {
|
|
364
332
|
const sweVal = parseFloat(sweScore)
|
|
365
333
|
const swePadded = sweScore.padEnd(W_SWE)
|
|
366
|
-
|
|
367
|
-
else if (sweVal >= 60) sweCell = chalk.bold.rgb(80, 220, 0)(swePadded)
|
|
368
|
-
else if (sweVal >= 50) sweCell = chalk.bold.rgb(170, 210, 0)(swePadded)
|
|
369
|
-
else if (sweVal >= 40) sweCell = chalk.rgb(240, 190, 0)(swePadded)
|
|
370
|
-
else if (sweVal >= 35) sweCell = chalk.rgb(255, 130, 0)(swePadded)
|
|
371
|
-
else if (sweVal >= 30) sweCell = chalk.rgb(255, 70, 0)(swePadded)
|
|
372
|
-
else if (sweVal >= 20) sweCell = chalk.rgb(210, 20, 0)(swePadded)
|
|
373
|
-
else sweCell = chalk.rgb(140, 0, 0)(swePadded)
|
|
334
|
+
sweCell = paintSweScore(sweVal, swePadded)
|
|
374
335
|
}
|
|
375
336
|
|
|
376
337
|
// 📖 Context window column - colorized by size (larger = better)
|
|
377
338
|
const ctxRaw = r.ctx ?? '—'
|
|
378
339
|
const ctxCell = ctxRaw !== '—' && (ctxRaw.includes('128k') || ctxRaw.includes('200k') || ctxRaw.includes('1m'))
|
|
379
|
-
?
|
|
340
|
+
? themeColors.metricGood(ctxRaw.padEnd(W_CTX))
|
|
380
341
|
: ctxRaw !== '—' && (ctxRaw.includes('32k') || ctxRaw.includes('64k'))
|
|
381
|
-
?
|
|
382
|
-
:
|
|
342
|
+
? themeColors.metricOk(ctxRaw.padEnd(W_CTX))
|
|
343
|
+
: themeColors.dim(ctxRaw.padEnd(W_CTX))
|
|
383
344
|
|
|
384
345
|
// 📖 Keep the row-local spinner small and inline so users can still read the last measured latency.
|
|
385
346
|
const buildLatestPingDisplay = (value) => {
|
|
@@ -393,18 +354,18 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
393
354
|
let pingCell
|
|
394
355
|
if (!latestPing) {
|
|
395
356
|
const placeholder = r.isPinging ? buildLatestPingDisplay('———') : '———'.padEnd(W_PING)
|
|
396
|
-
pingCell =
|
|
357
|
+
pingCell = themeColors.dim(placeholder)
|
|
397
358
|
} else if (latestPing.code === '200') {
|
|
398
359
|
// 📖 Success - show response time
|
|
399
360
|
const str = buildLatestPingDisplay(String(latestPing.ms))
|
|
400
|
-
pingCell = latestPing.ms < 500 ?
|
|
361
|
+
pingCell = latestPing.ms < 500 ? themeColors.metricGood(str) : latestPing.ms < 1500 ? themeColors.metricWarn(str) : themeColors.metricBad(str)
|
|
401
362
|
} else if (latestPing.code === '401') {
|
|
402
363
|
// 📖 401 = no API key but server IS reachable — still show latency in dim
|
|
403
|
-
pingCell =
|
|
364
|
+
pingCell = themeColors.dim(buildLatestPingDisplay(String(latestPing.ms)))
|
|
404
365
|
} else {
|
|
405
366
|
// 📖 Error or timeout - show "———" (error code is already in Status column)
|
|
406
367
|
const placeholder = r.isPinging ? buildLatestPingDisplay('———') : '———'.padEnd(W_PING)
|
|
407
|
-
pingCell =
|
|
368
|
+
pingCell = themeColors.dim(placeholder)
|
|
408
369
|
}
|
|
409
370
|
|
|
410
371
|
// 📖 Avg ping (just number, no "ms")
|
|
@@ -412,9 +373,9 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
412
373
|
let avgCell
|
|
413
374
|
if (avg !== Infinity) {
|
|
414
375
|
const str = String(avg).padEnd(W_AVG)
|
|
415
|
-
avgCell = avg < 500 ?
|
|
376
|
+
avgCell = avg < 500 ? themeColors.metricGood(str) : avg < 1500 ? themeColors.metricWarn(str) : themeColors.metricBad(str)
|
|
416
377
|
} else {
|
|
417
|
-
avgCell =
|
|
378
|
+
avgCell = themeColors.dim('———'.padEnd(W_AVG))
|
|
418
379
|
}
|
|
419
380
|
|
|
420
381
|
// 📖 Status column - build plain text with emoji, pad, then colorize
|
|
@@ -423,21 +384,21 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
423
384
|
if (r.status === 'noauth') {
|
|
424
385
|
// 📖 Server responded but needs an API key — shown dimly since it IS reachable
|
|
425
386
|
statusText = `🔑 NO KEY`
|
|
426
|
-
statusColor =
|
|
387
|
+
statusColor = themeColors.dim
|
|
427
388
|
} else if (r.status === 'auth_error') {
|
|
428
389
|
// 📖 A key is configured but the provider rejected it — keep this distinct
|
|
429
390
|
// 📖 from "no key" so configured-only mode does not look misleading.
|
|
430
391
|
statusText = `🔐 AUTH FAIL`
|
|
431
|
-
statusColor =
|
|
392
|
+
statusColor = themeColors.errorBold
|
|
432
393
|
} else if (r.status === 'pending') {
|
|
433
394
|
statusText = `${FRAMES[frame % FRAMES.length]} wait`
|
|
434
|
-
statusColor =
|
|
395
|
+
statusColor = themeColors.warning
|
|
435
396
|
} else if (r.status === 'up') {
|
|
436
397
|
statusText = `✅ UP`
|
|
437
|
-
statusColor =
|
|
398
|
+
statusColor = themeColors.success
|
|
438
399
|
} else if (r.status === 'timeout') {
|
|
439
400
|
statusText = `⏳ TIMEOUT`
|
|
440
|
-
statusColor =
|
|
401
|
+
statusColor = themeColors.warning
|
|
441
402
|
} else if (r.status === 'down') {
|
|
442
403
|
const code = r.httpCode ?? 'ERR'
|
|
443
404
|
// 📖 Different emojis for different error codes
|
|
@@ -457,10 +418,10 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
457
418
|
}
|
|
458
419
|
const emoji = errorEmojis[code] || '❌'
|
|
459
420
|
statusText = `${emoji} ${errorLabels[code] || code}`
|
|
460
|
-
statusColor =
|
|
421
|
+
statusColor = themeColors.error
|
|
461
422
|
} else {
|
|
462
423
|
statusText = '?'
|
|
463
|
-
statusColor =
|
|
424
|
+
statusColor = themeColors.dim
|
|
464
425
|
}
|
|
465
426
|
const status = statusColor(padEndDisplay(statusText, W_STATUS))
|
|
466
427
|
|
|
@@ -471,43 +432,43 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
471
432
|
switch (verdict) {
|
|
472
433
|
case 'Perfect':
|
|
473
434
|
verdictText = 'Perfect 🚀'
|
|
474
|
-
verdictColor =
|
|
435
|
+
verdictColor = themeColors.successBold
|
|
475
436
|
break
|
|
476
437
|
case 'Normal':
|
|
477
438
|
verdictText = 'Normal ✅'
|
|
478
|
-
verdictColor =
|
|
439
|
+
verdictColor = themeColors.metricGood
|
|
479
440
|
break
|
|
480
441
|
case 'Spiky':
|
|
481
442
|
verdictText = 'Spiky 📈'
|
|
482
|
-
verdictColor = (
|
|
443
|
+
verdictColor = (text) => chalk.bold.rgb(...getTierRgb('A+'))(text)
|
|
483
444
|
break
|
|
484
445
|
case 'Slow':
|
|
485
446
|
verdictText = 'Slow 🐢'
|
|
486
|
-
verdictColor = (
|
|
447
|
+
verdictColor = (text) => chalk.bold.rgb(...getTierRgb('A-'))(text)
|
|
487
448
|
break
|
|
488
449
|
case 'Very Slow':
|
|
489
450
|
verdictText = 'Very Slow 🐌'
|
|
490
|
-
verdictColor = (
|
|
451
|
+
verdictColor = (text) => chalk.bold.rgb(...getTierRgb('B+'))(text)
|
|
491
452
|
break
|
|
492
453
|
case 'Overloaded':
|
|
493
454
|
verdictText = 'Overloaded 🔥'
|
|
494
|
-
verdictColor = (
|
|
455
|
+
verdictColor = (text) => chalk.bold.rgb(...getTierRgb('B'))(text)
|
|
495
456
|
break
|
|
496
457
|
case 'Unstable':
|
|
497
458
|
verdictText = 'Unstable ⚠️'
|
|
498
|
-
verdictColor =
|
|
459
|
+
verdictColor = themeColors.errorBold
|
|
499
460
|
break
|
|
500
461
|
case 'Not Active':
|
|
501
462
|
verdictText = 'Not Active 👻'
|
|
502
|
-
verdictColor =
|
|
463
|
+
verdictColor = themeColors.dim
|
|
503
464
|
break
|
|
504
465
|
case 'Pending':
|
|
505
466
|
verdictText = 'Pending ⏳'
|
|
506
|
-
verdictColor =
|
|
467
|
+
verdictColor = themeColors.dim
|
|
507
468
|
break
|
|
508
469
|
default:
|
|
509
470
|
verdictText = 'Unusable 💀'
|
|
510
|
-
verdictColor = (
|
|
471
|
+
verdictColor = (text) => chalk.bold.rgb(...getTierRgb('C'))(text)
|
|
511
472
|
break
|
|
512
473
|
}
|
|
513
474
|
// 📖 Use padEndDisplay to account for emoji display width (2 cols each) so all rows align
|
|
@@ -518,15 +479,15 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
518
479
|
const stabScore = getStabilityScore(r)
|
|
519
480
|
let stabCell
|
|
520
481
|
if (stabScore < 0) {
|
|
521
|
-
stabCell =
|
|
482
|
+
stabCell = themeColors.dim('———'.padEnd(W_STAB))
|
|
522
483
|
} else if (stabScore >= 80) {
|
|
523
|
-
stabCell =
|
|
484
|
+
stabCell = themeColors.metricGood(String(stabScore).padEnd(W_STAB))
|
|
524
485
|
} else if (stabScore >= 60) {
|
|
525
|
-
stabCell =
|
|
486
|
+
stabCell = themeColors.metricOk(String(stabScore).padEnd(W_STAB))
|
|
526
487
|
} else if (stabScore >= 40) {
|
|
527
|
-
stabCell =
|
|
488
|
+
stabCell = themeColors.metricWarn(String(stabScore).padEnd(W_STAB))
|
|
528
489
|
} else {
|
|
529
|
-
stabCell =
|
|
490
|
+
stabCell = themeColors.metricBad(String(stabScore).padEnd(W_STAB))
|
|
530
491
|
}
|
|
531
492
|
|
|
532
493
|
// 📖 Uptime column - percentage of successful pings
|
|
@@ -535,20 +496,20 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
535
496
|
const uptimeStr = uptimePercent + '%'
|
|
536
497
|
let uptimeCell
|
|
537
498
|
if (uptimePercent >= 90) {
|
|
538
|
-
uptimeCell =
|
|
499
|
+
uptimeCell = themeColors.metricGood(uptimeStr.padEnd(W_UPTIME))
|
|
539
500
|
} else if (uptimePercent >= 70) {
|
|
540
|
-
uptimeCell =
|
|
501
|
+
uptimeCell = themeColors.metricWarn(uptimeStr.padEnd(W_UPTIME))
|
|
541
502
|
} else if (uptimePercent >= 50) {
|
|
542
|
-
uptimeCell = chalk.rgb(
|
|
503
|
+
uptimeCell = chalk.rgb(...getTierRgb('A-'))(uptimeStr.padEnd(W_UPTIME))
|
|
543
504
|
} else {
|
|
544
|
-
uptimeCell =
|
|
505
|
+
uptimeCell = themeColors.metricBad(uptimeStr.padEnd(W_UPTIME))
|
|
545
506
|
}
|
|
546
507
|
|
|
547
508
|
// 📖 Model text now mirrors the provider hue so provider affinity is visible
|
|
548
509
|
// 📖 even before the eye reaches the Provider column.
|
|
549
|
-
const nameCell =
|
|
510
|
+
const nameCell = themeColors.provider(r.providerKey, name, { bold: isCursor })
|
|
550
511
|
const sourceCursorText = providerName.padEnd(W_SOURCE)
|
|
551
|
-
const sourceCell = isCursor ?
|
|
512
|
+
const sourceCell = isCursor ? themeColors.provider(r.providerKey, sourceCursorText, { bold: true }) : source
|
|
552
513
|
|
|
553
514
|
// 📖 Usage column removed from UI – no usage data displayed.
|
|
554
515
|
// (We keep the logic but do not render it.)
|
|
@@ -558,8 +519,8 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
558
519
|
// 📖 exact provider/model pair, loaded from the local usage snapshot file at startup.
|
|
559
520
|
const tokenTotal = Number(r.totalTokens) || 0
|
|
560
521
|
const tokensCell = tokenTotal > 0
|
|
561
|
-
?
|
|
562
|
-
:
|
|
522
|
+
? themeColors.metricOk(formatTokenTotalCompact(tokenTotal).padEnd(W_TOKENS))
|
|
523
|
+
: themeColors.dim('0'.padEnd(W_TOKENS))
|
|
563
524
|
|
|
564
525
|
// 📖 Build row with double space between columns (order: Rank, Tier, SWE%, CTX, Model, Provider, Latest Ping, Avg Ping, Health, Verdict, Stability, Up%, Used)
|
|
565
526
|
const row = ' ' + num + COL_SEP + tier + COL_SEP + sweCell + COL_SEP + ctxCell + COL_SEP + nameCell + COL_SEP + sourceCell + COL_SEP + pingCell + COL_SEP + avgCell + COL_SEP + status + COL_SEP + speedCell + COL_SEP + stabCell + COL_SEP + uptimeCell + COL_SEP + tokensCell
|
|
@@ -577,62 +538,64 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
577
538
|
}
|
|
578
539
|
|
|
579
540
|
if (vp.hasBelow) {
|
|
580
|
-
lines.push(
|
|
541
|
+
lines.push(themeColors.dim(` ... ${sorted.length - vp.endIdx} more below ...`))
|
|
581
542
|
}
|
|
582
543
|
|
|
583
544
|
lines.push('')
|
|
584
545
|
// 📖 Footer hints keep only navigation and secondary actions now that the
|
|
585
546
|
// 📖 active tool target is already visible in the header badge.
|
|
586
|
-
const hotkey = (keyLabel, text) =>
|
|
547
|
+
const hotkey = (keyLabel, text) => themeColors.hotkey(keyLabel) + themeColors.dim(text)
|
|
587
548
|
// 📖 Active filter pills use a loud green background so tier/provider/configured-only
|
|
588
549
|
// 📖 states are obvious even when the user misses the smaller header badges.
|
|
589
|
-
const
|
|
550
|
+
const configuredBadgeBg = getTheme() === 'dark' ? [52, 120, 88] : [195, 234, 206]
|
|
551
|
+
const activeHotkey = (keyLabel, text, bg) => themeColors.badge(`${keyLabel}${text}`, bg, getReadableTextRgb(bg))
|
|
590
552
|
// 📖 Line 1: core navigation + filtering shortcuts
|
|
591
553
|
lines.push(
|
|
592
554
|
hotkey('F', ' Toggle Favorite') +
|
|
593
|
-
|
|
555
|
+
themeColors.dim(` • `) +
|
|
594
556
|
(tierFilterMode > 0
|
|
595
|
-
? activeHotkey('T', ` Tier (${activeTierLabel})`,
|
|
557
|
+
? activeHotkey('T', ` Tier (${activeTierLabel})`, getTierRgb(activeTierLabel))
|
|
596
558
|
: hotkey('T', ' Tier')) +
|
|
597
|
-
|
|
559
|
+
themeColors.dim(` • `) +
|
|
598
560
|
(originFilterMode > 0
|
|
599
|
-
? activeHotkey('D', ` Provider (${activeOriginLabel})`,
|
|
561
|
+
? activeHotkey('D', ` Provider (${activeOriginLabel})`, PROVIDER_COLOR[[null, ...Object.keys(sources)][originFilterMode]] || [255, 255, 255])
|
|
600
562
|
: hotkey('D', ' Provider')) +
|
|
601
|
-
|
|
602
|
-
(hideUnconfiguredModels ? activeHotkey('E', ' Configured Models Only') : hotkey('E', ' Configured Models Only')) +
|
|
603
|
-
|
|
563
|
+
themeColors.dim(` • `) +
|
|
564
|
+
(hideUnconfiguredModels ? activeHotkey('E', ' Configured Models Only', configuredBadgeBg) : hotkey('E', ' Configured Models Only')) +
|
|
565
|
+
themeColors.dim(` • `) +
|
|
604
566
|
hotkey('P', ' Settings') +
|
|
605
|
-
|
|
567
|
+
themeColors.dim(` • `) +
|
|
606
568
|
hotkey('K', ' Help')
|
|
607
569
|
)
|
|
608
570
|
// 📖 Line 2: install flow, recommend, feedback, and extended hints.
|
|
609
571
|
lines.push(
|
|
610
|
-
|
|
611
|
-
hotkey('Y', ' Install endpoints') +
|
|
612
|
-
hotkey('Q', ' Smart Recommend') +
|
|
572
|
+
themeColors.dim(` `) +
|
|
573
|
+
hotkey('Y', ' Install endpoints') + themeColors.dim(` • `) +
|
|
574
|
+
hotkey('Q', ' Smart Recommend') + themeColors.dim(` • `) +
|
|
575
|
+
hotkey('G', ' Theme') + themeColors.dim(` • `) +
|
|
613
576
|
hotkey('I', ' Feedback, bugs & requests')
|
|
614
577
|
)
|
|
615
578
|
// 📖 Proxy status is now shown via the J badge in line 2 above — no need for a dedicated line
|
|
616
579
|
const footerLine =
|
|
617
|
-
|
|
618
|
-
|
|
580
|
+
themeColors.footerLove(' Made with 💖 & ☕ by \x1b]8;;https://github.com/vava-nessa\x1b\\vava-nessa\x1b]8;;\x1b\\') +
|
|
581
|
+
themeColors.dim(' • ') +
|
|
619
582
|
'⭐ ' +
|
|
620
|
-
|
|
621
|
-
|
|
583
|
+
themeColors.link('\x1b]8;;https://github.com/vava-nessa/free-coding-models\x1b\\Star on GitHub\x1b]8;;\x1b\\') +
|
|
584
|
+
themeColors.dim(' • ') +
|
|
622
585
|
'🤝 ' +
|
|
623
|
-
|
|
624
|
-
|
|
586
|
+
themeColors.warning('\x1b]8;;https://github.com/vava-nessa/free-coding-models/graphs/contributors\x1b\\Contributors\x1b]8;;\x1b\\') +
|
|
587
|
+
themeColors.dim(' • ') +
|
|
625
588
|
'☕ ' +
|
|
626
|
-
|
|
627
|
-
|
|
589
|
+
themeColors.footerCoffee('\x1b]8;;https://buymeacoffee.com/vavanessadev\x1b\\Buy me a coffee\x1b]8;;\x1b\\') +
|
|
590
|
+
themeColors.dim(' • ') +
|
|
628
591
|
'💬 ' +
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
592
|
+
themeColors.footerDiscord('\x1b]8;;https://discord.gg/ZTNFHvvCkU\x1b\\Discord\x1b]8;;\x1b\\') +
|
|
593
|
+
themeColors.dim(' → ') +
|
|
594
|
+
themeColors.footerDiscord('https://discord.gg/ZTNFHvvCkU') +
|
|
595
|
+
themeColors.dim(' • ') +
|
|
596
|
+
themeColors.hotkey('N') + themeColors.dim(' Changelog') +
|
|
597
|
+
themeColors.dim(' • ') +
|
|
598
|
+
themeColors.dim('Ctrl+C Exit')
|
|
636
599
|
lines.push(footerLine)
|
|
637
600
|
|
|
638
601
|
if (versionStatus.isOutdated) {
|
|
@@ -646,7 +609,7 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
646
609
|
|
|
647
610
|
// 📖 Stable release notice: keep the bridge rebuild status explicit in the main UI
|
|
648
611
|
// 📖 so users do not go hunting for hidden controls that are disabled on purpose.
|
|
649
|
-
const bridgeNotice = chalk.
|
|
612
|
+
const bridgeNotice = chalk.italic.rgb(...getTierRgb('A-'))(` ${PROXY_DISABLED_NOTICE}`)
|
|
650
613
|
lines.push(bridgeNotice)
|
|
651
614
|
|
|
652
615
|
// 📖 Append \x1b[K (erase to EOL) to each line so leftover chars from previous
|