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