free-coding-models 0.5.95 → 0.5.97
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/README.md +15 -11
- package/bin/free-coding-models.js +2 -0
- package/changelog/v0.5.96.md +22 -0
- package/changelog/v0.5.97.md +24 -0
- package/package.json +1 -1
- package/sources.js +197 -91
- package/src/core/config.js +28 -3
- package/src/core/i18n/index.js +49 -0
- package/src/core/i18n/locales/en.js +991 -0
- package/src/core/i18n/locales/zh-CN.js +991 -0
- package/src/core/opencode.js +15 -8
- package/src/core/ping.js +31 -1
- package/src/core/playground.js +20 -19
- package/src/core/provider-key-tester.js +1 -0
- package/src/core/provider-metadata.js +11 -2
- package/src/core/quota-capabilities.js +2 -0
- package/src/core/router-daemon.js +58 -8
- package/src/core/router-dashboard.js +15 -14
- package/src/core/router-v2/tui-dashboard.js +27 -26
- package/src/core/shell-env.js +6 -0
- package/src/core/utils.js +22 -0
- package/src/tui/app.js +2 -0
- package/src/tui/cli-help.js +45 -21
- package/src/tui/command-palette.js +19 -1
- package/src/tui/key-handler.js +21 -1
- package/src/tui/overlays.js +113 -107
- package/src/tui/render-helpers.js +4 -3
- package/src/tui/render-table.js +137 -132
- package/src/tui/theme.js +2 -0
- package/web/dist/assets/index-C2lIYp0O.js +45 -0
- package/web/dist/assets/index-DZFdxy2N.css +1 -0
- package/web/dist/index.html +2 -2
- package/web/server.js +58 -6
- package/web/dist/assets/index-BOpGJzSO.js +0 -45
- package/web/dist/assets/index-CCaxIOti.css +0 -1
package/src/tui/render-table.js
CHANGED
|
@@ -52,7 +52,8 @@ import { TIER_COLOR } from './tier-colors.js'
|
|
|
52
52
|
import { getAvg, getVerdict, getUptime, getStabilityScore, getVersionStatusInfo, isNewModel } from '../core/utils.js'
|
|
53
53
|
import { supportsUsagePercent } from '../core/quota-capabilities.js'
|
|
54
54
|
import { formatBenchmarkLatency, formatBenchmarkTps } from '../core/benchmark.js'
|
|
55
|
-
import { calculateViewport, sortResultsWithPinnedFavorites, padEndDisplay, displayWidth, stripAnsi, fadedRow } from './render-helpers.js'
|
|
55
|
+
import { calculateViewport, sortResultsWithPinnedFavorites, padEndDisplay, displayWidth, stripAnsi, fadedRow, truncateAnsiWidth } from './render-helpers.js'
|
|
56
|
+
import { getLocale, t } from '../core/i18n/index.js'
|
|
56
57
|
import { getToolMeta, TOOL_METADATA, TOOL_MODE_ORDER, isModelCompatibleWithTool } from '../core/tool-metadata.js'
|
|
57
58
|
import { getColumnSpacing } from './ui-config.js'
|
|
58
59
|
import { detectPackageManager, getManualInstallCmd } from '../core/updater.js'
|
|
@@ -251,10 +252,10 @@ export function renderTable({
|
|
|
251
252
|
|
|
252
253
|
const intervalSec = Math.round(pingInterval / 1000)
|
|
253
254
|
const pingModeMeta = {
|
|
254
|
-
speed: { label: '
|
|
255
|
-
normal: { label: '
|
|
256
|
-
slow: { label: '
|
|
257
|
-
forced: { label: '
|
|
255
|
+
speed: { label: t('filters.pingSpeedShort'), color: themeColors.warningBold },
|
|
256
|
+
normal: { label: t('filters.pingNormalShort'), color: themeColors.accentBold },
|
|
257
|
+
slow: { label: t('filters.pingSlowShort'), color: themeColors.info },
|
|
258
|
+
forced: { label: t('filters.pingForcedShort'), color: themeColors.errorBold },
|
|
258
259
|
}
|
|
259
260
|
const activePingMode = pingModeMeta[pingMode] ?? pingModeMeta.normal
|
|
260
261
|
const pingProgressText = `${completedPings}/${totalVisible}`
|
|
@@ -268,7 +269,7 @@ export function renderTable({
|
|
|
268
269
|
const pingControlBadge =
|
|
269
270
|
activePingMode.color(' [ ') +
|
|
270
271
|
themeColors.hotkey('W') +
|
|
271
|
-
activePingMode.color(`
|
|
272
|
+
activePingMode.color(` ${t('filters.pingInterval')} : ${intervalSec}s (${activePingMode.label}) - ${pingProgressText} - ${t('filters.nextPingShort')} : `) +
|
|
272
273
|
nextCountdownColor(`${secondsUntilNextLabel}s`) +
|
|
273
274
|
activePingMode.color(' ]')
|
|
274
275
|
|
|
@@ -278,7 +279,7 @@ export function renderTable({
|
|
|
278
279
|
const toolMeta = getToolMeta(mode)
|
|
279
280
|
const toolBadgeColor = mode === 'openclaw' ? themeColors.warningBold : themeColors.accentBold
|
|
280
281
|
const toolColor = toolMeta.color ? chalk.rgb(...toolMeta.color) : toolBadgeColor
|
|
281
|
-
const modeBadge = toolBadgeColor(' [ ') + themeColors.hotkey('Z') + toolBadgeColor('
|
|
282
|
+
const modeBadge = toolBadgeColor(' [ ') + themeColors.hotkey('Z') + toolBadgeColor(` ${t('filters.tool')} : `) + toolColor.bold(`${toolMeta.emoji} ${toolMeta.label}`) + toolBadgeColor(' ]')
|
|
282
283
|
|
|
283
284
|
const activeHeaderBadge = (text, bg) => themeColors.badge(text, bg, getReadableTextRgb(bg))
|
|
284
285
|
const versionStatus = getVersionStatusInfo(settingsUpdateState, settingsUpdateLatestVersion, startupLatestVersion, versionAlertsEnabled)
|
|
@@ -319,7 +320,7 @@ export function renderTable({
|
|
|
319
320
|
const W_MOOD = 2
|
|
320
321
|
const W_RANK = 6
|
|
321
322
|
const W_TIER = 5
|
|
322
|
-
const W_CTX = 4
|
|
323
|
+
const W_CTX = getLocale() === 'zh-CN' ? 6 : 4
|
|
323
324
|
const W_SOURCE = 15
|
|
324
325
|
// 📖 Provider names are display-capped at MAX_PROVIDER_NAME_LEN chars so an
|
|
325
326
|
// 📖 over-long label can never push the following cells and desalign the
|
|
@@ -435,25 +436,29 @@ export function renderTable({
|
|
|
435
436
|
if (showWidthWarning) {
|
|
436
437
|
const lines = []
|
|
437
438
|
const blankLines = Math.max(0, Math.floor(((terminalRows || 24) - 7) / 2))
|
|
438
|
-
const warning = '
|
|
439
|
-
const warning2 = '
|
|
440
|
-
const warning3 = '
|
|
441
|
-
const
|
|
442
|
-
|
|
443
|
-
|
|
439
|
+
const warning = `🖥️ ${t('dashboard.terminalMaximize')}`
|
|
440
|
+
const warning2 = `⚠️ ${t('dashboard.terminalTooSmall')}`
|
|
441
|
+
const warning3 = `📏 ${t('dashboard.terminalReduceFont')}`
|
|
442
|
+
const centeredWarning = (text, color) => {
|
|
443
|
+
const clipped = truncateAnsiWidth(text, terminalCols, { ellipsis: '' })
|
|
444
|
+
const left = Math.max(0, Math.floor((terminalCols - displayWidth(clipped)) / 2))
|
|
445
|
+
return ' '.repeat(left) + color(clipped)
|
|
446
|
+
}
|
|
444
447
|
for (let i = 0; i < blankLines; i++) lines.push('')
|
|
445
|
-
lines.push(
|
|
448
|
+
lines.push(centeredWarning(warning, themeColors.errorBold))
|
|
446
449
|
lines.push('')
|
|
447
|
-
lines.push(
|
|
450
|
+
lines.push(centeredWarning(warning2, themeColors.error))
|
|
448
451
|
lines.push('')
|
|
449
|
-
lines.push(
|
|
452
|
+
lines.push(centeredWarning(warning3, themeColors.error))
|
|
450
453
|
lines.push('')
|
|
451
|
-
|
|
454
|
+
const dismissCountdown = t('dashboard.warningWillHide', { seconds: (remainingMs / 1000).toFixed(1) })
|
|
455
|
+
lines.push(centeredWarning(dismissCountdown, themeColors.warning))
|
|
452
456
|
const barTotal = Math.max(0, Math.min(terminalCols - 4, 30))
|
|
453
457
|
const barFill = Math.round((elapsed / warningDurationMs) * barTotal)
|
|
454
458
|
const barStr = themeColors.success('█'.repeat(barFill)) + themeColors.dim('░'.repeat(barTotal - barFill))
|
|
455
459
|
lines.push(' '.repeat(Math.max(0, Math.floor((terminalCols - barTotal) / 2))) + barStr)
|
|
456
|
-
|
|
460
|
+
const dismissHint = t('dashboard.pressEscToDismiss')
|
|
461
|
+
lines.push(centeredWarning(dismissHint, themeColors.dim))
|
|
457
462
|
while (terminalRows > 0 && lines.length < terminalRows) lines.push('')
|
|
458
463
|
const EL = '\x1b[K'
|
|
459
464
|
return lines.map(line => line + EL).join('\n')
|
|
@@ -487,26 +492,26 @@ export function renderTable({
|
|
|
487
492
|
|
|
488
493
|
// 📖 Plain header labels — arrows are appended dynamically below.
|
|
489
494
|
const moodLabel = '❔'
|
|
490
|
-
const rankLabel = '
|
|
491
|
-
const tierLabel = '
|
|
492
|
-
const originLabel =
|
|
493
|
-
const modelLabel = '
|
|
494
|
-
const sweLabel = '
|
|
495
|
-
const ctxLabel = '
|
|
496
|
-
const pingLabel = '
|
|
497
|
-
const avgLabel = '
|
|
498
|
-
const healthLabel = '
|
|
499
|
-
const verdictLabel = '
|
|
500
|
-
const stabLabel =
|
|
501
|
-
const uptimeLabel = '
|
|
495
|
+
const rankLabel = t('dashboard.rank')
|
|
496
|
+
const tierLabel = t('dashboard.tier')
|
|
497
|
+
const originLabel = t('dashboard.provider')
|
|
498
|
+
const modelLabel = t('dashboard.model')
|
|
499
|
+
const sweLabel = t('dashboard.swe')
|
|
500
|
+
const ctxLabel = t('dashboard.context')
|
|
501
|
+
const pingLabel = t('dashboard.lastPing')
|
|
502
|
+
const avgLabel = t('dashboard.avgPingShort')
|
|
503
|
+
const healthLabel = t('dashboard.health')
|
|
504
|
+
const verdictLabel = t('dashboard.verdict')
|
|
505
|
+
const stabLabel = t('dashboard.stability')
|
|
506
|
+
const uptimeLabel = t('dashboard.uptimeShort')
|
|
502
507
|
|
|
503
508
|
// 📖 Helper to colorize first letter for keyboard shortcuts
|
|
504
509
|
// 📖 IMPORTANT: Pad PLAIN TEXT first, then apply colors to avoid alignment issues
|
|
505
510
|
const colorFirst = (text, width, colorFn = themeColors.hotkey) => {
|
|
506
|
-
const first = text
|
|
507
|
-
const rest =
|
|
511
|
+
const [first, ...restChars] = [...text]
|
|
512
|
+
const rest = restChars.join('')
|
|
508
513
|
const plainText = first + rest
|
|
509
|
-
const padding = ' '.repeat(Math.max(0, width - plainText
|
|
514
|
+
const padding = ' '.repeat(Math.max(0, width - displayWidth(plainText)))
|
|
510
515
|
return colorFn(first) + themeColors.dim(rest + padding)
|
|
511
516
|
}
|
|
512
517
|
|
|
@@ -514,7 +519,7 @@ export function renderTable({
|
|
|
514
519
|
// 📖 a vivid inverse style (bright accent bg + white bold fg) for ~250ms.
|
|
515
520
|
// 📖 This gives satisfying visual feedback that the click was registered.
|
|
516
521
|
const flashHeader = (plainText, width) => {
|
|
517
|
-
const padded = plainText
|
|
522
|
+
const padded = padEndDisplay(truncateAnsiWidth(plainText, width, { ellipsis: '' }), width)
|
|
518
523
|
const bg = currentPalette().accentStrong
|
|
519
524
|
const fg = getReadableTextRgb(bg)
|
|
520
525
|
return chalk.bold.rgb(...fg).bgRgb(...bg)(padded)
|
|
@@ -527,8 +532,8 @@ export function renderTable({
|
|
|
527
532
|
const sortActiveHeader = (label, width) => {
|
|
528
533
|
const arrow = dir
|
|
529
534
|
const prefixed = arrow + ' ' + label
|
|
530
|
-
const text = prefixed
|
|
531
|
-
const padded = text
|
|
535
|
+
const text = displayWidth(prefixed) <= width ? prefixed : label + arrow
|
|
536
|
+
const padded = padEndDisplay(truncateAnsiWidth(text, width, { ellipsis: '' }), width)
|
|
532
537
|
// 📖 Subtle dark accent background — visible but not overwhelming.
|
|
533
538
|
const bg = currentPalette().cursor.defaultBg
|
|
534
539
|
const fg = getReadableTextRgb(bg)
|
|
@@ -545,7 +550,7 @@ export function renderTable({
|
|
|
545
550
|
// 📖 colKey = sort key (e.g. 'rank', 'swe'), label = plain text, width = column width.
|
|
546
551
|
const headerStyle = (colKey, label, width) => {
|
|
547
552
|
const arrowText = dir + ' ' + label
|
|
548
|
-
const flashText = arrowText
|
|
553
|
+
const flashText = displayWidth(arrowText) <= width ? arrowText : label + dir
|
|
549
554
|
if (headerFlashColumn === colKey) return flashHeader(flashText, width)
|
|
550
555
|
if (sortColumn === colKey) return sortActiveHeader(label, width)
|
|
551
556
|
return colorFirst(label, width)
|
|
@@ -572,30 +577,26 @@ export function renderTable({
|
|
|
572
577
|
const verdictH_c = headerStyle('verdict', verdictLabel, W_VERDICT)
|
|
573
578
|
const stabH_c = (() => {
|
|
574
579
|
if (headerFlashColumn === 'stability') {
|
|
575
|
-
const ft = (dir + ' ' + stabLabel)
|
|
580
|
+
const ft = displayWidth(dir + ' ' + stabLabel) <= wStab ? dir + ' ' + stabLabel : stabLabel + dir
|
|
576
581
|
return flashHeader(ft, wStab)
|
|
577
582
|
}
|
|
578
|
-
|
|
579
|
-
const plain = stabLabel
|
|
580
|
-
const padding = ' '.repeat(Math.max(0, wStab - plain.length))
|
|
581
|
-
return themeColors.dim('Sta') + themeColors.hotkey('B') + themeColors.dim((isCompact ? '.' : 'ility') + padding)
|
|
583
|
+
return headerStyle('stability', stabLabel, wStab)
|
|
582
584
|
})()
|
|
583
585
|
const uptimeH_c = (() => {
|
|
584
586
|
if (headerFlashColumn === 'uptime') {
|
|
585
|
-
const ft = (dir + ' ' + uptimeLabel)
|
|
587
|
+
const ft = displayWidth(dir + ' ' + uptimeLabel) <= W_UPTIME ? dir + ' ' + uptimeLabel : uptimeLabel + dir
|
|
586
588
|
return flashHeader(ft, W_UPTIME)
|
|
587
589
|
}
|
|
588
|
-
|
|
589
|
-
const padding = ' '.repeat(Math.max(0, W_UPTIME - uptimeLabel.length))
|
|
590
|
-
return themeColors.hotkey('U') + themeColors.dim('p%' + padding)
|
|
590
|
+
return headerStyle('uptime', uptimeLabel, W_UPTIME)
|
|
591
591
|
})()
|
|
592
592
|
const originH_c = (() => {
|
|
593
593
|
if (headerFlashColumn === 'origin') {
|
|
594
|
-
const ft = (dir + ' ' + originLabel)
|
|
594
|
+
const ft = displayWidth(dir + ' ' + originLabel) <= wSource ? dir + ' ' + originLabel : originLabel + dir
|
|
595
595
|
return flashHeader(ft, wSource)
|
|
596
596
|
}
|
|
597
597
|
if (sortColumn === 'origin') return sortActiveHeader(originLabel, wSource)
|
|
598
|
-
if (originFilterMode > 0) return themeColors.accentBold(originLabel
|
|
598
|
+
if (originFilterMode > 0) return themeColors.accentBold(padEndDisplay(originLabel, wSource))
|
|
599
|
+
if (getLocale() === 'zh-CN') return headerStyle('origin', originLabel, wSource)
|
|
599
600
|
const plain = isCompact ? 'PrOD…' : 'PrOviDer'
|
|
600
601
|
const padding = ' '.repeat(Math.max(0, wSource - plain.length))
|
|
601
602
|
if (isCompact) {
|
|
@@ -605,16 +606,15 @@ export function renderTable({
|
|
|
605
606
|
})()
|
|
606
607
|
|
|
607
608
|
// 📖 Benchmark headers — split the old combined AI Speed field into latency + throughput.
|
|
608
|
-
const aiLatencyLabel = isCompact ? 'AI Lat.' : '
|
|
609
|
+
const aiLatencyLabel = isCompact && getLocale() === 'en' ? 'AI Lat.' : t('dashboard.aiLatency')
|
|
609
610
|
const aiLatencyH_c = (() => {
|
|
610
611
|
if (headerFlashColumn === 'aiLatency') {
|
|
611
|
-
const ft = (dir + ' ' + aiLatencyLabel)
|
|
612
|
+
const ft = displayWidth(dir + ' ' + aiLatencyLabel) <= wAiLatency ? dir + ' ' + aiLatencyLabel : aiLatencyLabel + dir
|
|
612
613
|
return flashHeader(ft, wAiLatency)
|
|
613
614
|
}
|
|
614
615
|
if (sortColumn === 'aiLatency') return sortActiveHeader(aiLatencyLabel, wAiLatency)
|
|
615
616
|
const plain = aiLatencyLabel
|
|
616
|
-
|
|
617
|
-
return themeColors.dim(plain + padding)
|
|
617
|
+
return themeColors.dim(padEndDisplay(plain, wAiLatency))
|
|
618
618
|
})()
|
|
619
619
|
const tpsH_c = (() => {
|
|
620
620
|
if (headerFlashColumn === 'tps') {
|
|
@@ -632,7 +632,7 @@ export function renderTable({
|
|
|
632
632
|
return flashHeader(ft, W_QUOTA)
|
|
633
633
|
}
|
|
634
634
|
if (sortColumn === 'usage') return sortActiveHeader('Quota', W_QUOTA)
|
|
635
|
-
return themeColors.dim('
|
|
635
|
+
return themeColors.dim(padEndDisplay(t('dashboard.quota'), W_QUOTA))
|
|
636
636
|
})()
|
|
637
637
|
|
|
638
638
|
// 📖 Header row: conditionally include columns based on responsive visibility
|
|
@@ -655,10 +655,10 @@ export function renderTable({
|
|
|
655
655
|
if (sorted.length === 0) {
|
|
656
656
|
lines.push('')
|
|
657
657
|
if (hideUnconfiguredModels) {
|
|
658
|
-
lines.push(` ${themeColors.errorBold('
|
|
659
|
-
lines.push(` ${themeColors.dim('
|
|
658
|
+
lines.push(` ${themeColors.errorBold(t('dashboard.configureApiKeyHint'))}`)
|
|
659
|
+
lines.push(` ${themeColors.dim(t('dashboard.noConfiguredVisibleModels'))}`)
|
|
660
660
|
} else {
|
|
661
|
-
lines.push(` ${themeColors.warningBold('
|
|
661
|
+
lines.push(` ${themeColors.warningBold(t('dashboard.noModels'))}`)
|
|
662
662
|
}
|
|
663
663
|
}
|
|
664
664
|
|
|
@@ -746,8 +746,8 @@ export function renderTable({
|
|
|
746
746
|
const isCursor = cursor !== null && i === cursor
|
|
747
747
|
|
|
748
748
|
// 📖 Left-aligned columns - pad plain text first, then colorize
|
|
749
|
-
const num = themeColors.dim(String(r.idx)
|
|
750
|
-
const tier = tierFn(r.tier
|
|
749
|
+
const num = themeColors.dim(padEndDisplay(String(r.idx), W_RANK))
|
|
750
|
+
const tier = tierFn(padEndDisplay(r.tier, W_TIER))
|
|
751
751
|
// 📖 Keep terminal view provider-specific so each row is monitorable per provider
|
|
752
752
|
// 📖 In compact mode, truncate provider name to 4 chars + '…'
|
|
753
753
|
// 📖 In normal mode, cap names at MAX_PROVIDER_NAME_LEN chars (13 + '…')
|
|
@@ -759,7 +759,7 @@ export function renderTable({
|
|
|
759
759
|
: providerName.length > MAX_PROVIDER_NAME_LEN
|
|
760
760
|
? providerName.slice(0, MAX_PROVIDER_NAME_LEN - 1).trimEnd() + '…'
|
|
761
761
|
: providerName
|
|
762
|
-
const source = themeColors.provider(r.providerKey, providerDisplay
|
|
762
|
+
const source = themeColors.provider(r.providerKey, padEndDisplay(providerDisplay, wSource))
|
|
763
763
|
// 📖 Prefix: ⭐ favorite > 🎯 recommended > 🆕 new — only one emoji, never shifts the line
|
|
764
764
|
const modelIsNew = isNewModel(r.addedDate)
|
|
765
765
|
let favoritePrefix = ''
|
|
@@ -782,10 +782,10 @@ export function renderTable({
|
|
|
782
782
|
// ≥20% red (B), <20% dark red (C), '—' dim
|
|
783
783
|
let sweCell
|
|
784
784
|
if (sweScore === '—') {
|
|
785
|
-
sweCell = themeColors.dim(sweScore
|
|
785
|
+
sweCell = themeColors.dim(padEndDisplay(sweScore, W_SWE))
|
|
786
786
|
} else {
|
|
787
787
|
const sweVal = parseFloat(sweScore)
|
|
788
|
-
const swePadded = sweScore
|
|
788
|
+
const swePadded = padEndDisplay(sweScore, W_SWE)
|
|
789
789
|
sweCell = paintSweScore(sweVal, swePadded)
|
|
790
790
|
}
|
|
791
791
|
|
|
@@ -793,31 +793,31 @@ export function renderTable({
|
|
|
793
793
|
const ctxRaw = r.ctx ?? '—'
|
|
794
794
|
let ctxCell
|
|
795
795
|
if (ctxRaw === '—') {
|
|
796
|
-
ctxCell = themeColors.dim(ctxRaw
|
|
796
|
+
ctxCell = themeColors.dim(padEndDisplay(ctxRaw, W_CTX))
|
|
797
797
|
} else {
|
|
798
798
|
const ctxMatch = ctxRaw.match(/^(\d+)k$|^(\d+)M$/)
|
|
799
799
|
if (ctxMatch) {
|
|
800
800
|
const numK = ctxMatch[1] ? parseInt(ctxMatch[1]) : parseInt(ctxMatch[2]) * 1024
|
|
801
801
|
ctxCell = numK <= 32
|
|
802
|
-
? themeColors.metricBad(ctxRaw
|
|
802
|
+
? themeColors.metricBad(padEndDisplay(ctxRaw, W_CTX))
|
|
803
803
|
: numK <= 64
|
|
804
|
-
? themeColors.metricWarn(ctxRaw
|
|
804
|
+
? themeColors.metricWarn(padEndDisplay(ctxRaw, W_CTX))
|
|
805
805
|
: numK <= 128
|
|
806
|
-
? chalk.rgb(...currentPalette().ctxGold).bold(ctxRaw
|
|
806
|
+
? chalk.rgb(...currentPalette().ctxGold).bold(padEndDisplay(ctxRaw, W_CTX))
|
|
807
807
|
: numK <= 256
|
|
808
|
-
? chalk.rgb(...currentPalette().ctxGreen).bold(ctxRaw
|
|
808
|
+
? chalk.rgb(...currentPalette().ctxGreen).bold(padEndDisplay(ctxRaw, W_CTX))
|
|
809
809
|
: numK <= 400
|
|
810
|
-
? chalk.rgb(...currentPalette().ctxTeal).bold(ctxRaw
|
|
811
|
-
: chalk.rgb(...currentPalette().ctxCyan).bold.underline(ctxRaw
|
|
810
|
+
? chalk.rgb(...currentPalette().ctxTeal).bold(padEndDisplay(ctxRaw, W_CTX))
|
|
811
|
+
: chalk.rgb(...currentPalette().ctxCyan).bold.underline(padEndDisplay(ctxRaw, W_CTX))
|
|
812
812
|
} else {
|
|
813
|
-
ctxCell = themeColors.dim(ctxRaw
|
|
813
|
+
ctxCell = themeColors.dim(padEndDisplay(ctxRaw, W_CTX))
|
|
814
814
|
}
|
|
815
815
|
}
|
|
816
816
|
|
|
817
817
|
// 📖 Keep the row-local spinner small and inline so users can still read the last measured latency.
|
|
818
818
|
const buildLatestPingDisplay = (value) => {
|
|
819
819
|
const spinner = r.isPinging ? ` ${FRAMES[frame % FRAMES.length]}` : ''
|
|
820
|
-
return `${value}${spinner}
|
|
820
|
+
return padEndDisplay(`${value}${spinner}`, wPing)
|
|
821
821
|
}
|
|
822
822
|
|
|
823
823
|
// 📖 Latest ping - pings are objects: { ms, code }
|
|
@@ -825,7 +825,7 @@ export function renderTable({
|
|
|
825
825
|
const latestPing = r.pings.length > 0 ? r.pings[r.pings.length - 1] : null
|
|
826
826
|
let pingCell
|
|
827
827
|
if (!latestPing) {
|
|
828
|
-
const placeholder = r.isPinging ? buildLatestPingDisplay('———') : '———'
|
|
828
|
+
const placeholder = r.isPinging ? buildLatestPingDisplay('———') : padEndDisplay('———', wPing)
|
|
829
829
|
pingCell = themeColors.dim(placeholder)
|
|
830
830
|
} else if (latestPing.code === '200') {
|
|
831
831
|
// 📖 Success - show response time
|
|
@@ -836,7 +836,7 @@ export function renderTable({
|
|
|
836
836
|
pingCell = themeColors.dim(buildLatestPingDisplay(String(latestPing.ms)))
|
|
837
837
|
} else {
|
|
838
838
|
// 📖 Error or timeout - show "———" (error code is already in Status column)
|
|
839
|
-
const placeholder = r.isPinging ? buildLatestPingDisplay('———') : '———'
|
|
839
|
+
const placeholder = r.isPinging ? buildLatestPingDisplay('———') : padEndDisplay('———', wPing)
|
|
840
840
|
pingCell = themeColors.dim(placeholder)
|
|
841
841
|
}
|
|
842
842
|
|
|
@@ -844,10 +844,10 @@ export function renderTable({
|
|
|
844
844
|
const avg = getAvg(r)
|
|
845
845
|
let avgCell
|
|
846
846
|
if (avg !== Infinity) {
|
|
847
|
-
const str = String(avg)
|
|
847
|
+
const str = padEndDisplay(String(avg), wAvg)
|
|
848
848
|
avgCell = avg < 500 ? themeColors.metricGood(str) : avg < 1500 ? themeColors.metricWarn(str) : themeColors.metricBad(str)
|
|
849
849
|
} else {
|
|
850
|
-
avgCell = themeColors.dim('———'
|
|
850
|
+
avgCell = themeColors.dim(padEndDisplay('———', wAvg))
|
|
851
851
|
}
|
|
852
852
|
|
|
853
853
|
// 📖 Status column - build plain text with emoji, pad, then colorize
|
|
@@ -855,21 +855,21 @@ export function renderTable({
|
|
|
855
855
|
let statusText, statusColor
|
|
856
856
|
if (r.status === 'noauth') {
|
|
857
857
|
// 📖 Server responded but needs an API key — shown dimly since it IS reachable
|
|
858
|
-
statusText = `🔑
|
|
858
|
+
statusText = `🔑 ${t('dashboard.noKeyShort')}`
|
|
859
859
|
statusColor = themeColors.dim
|
|
860
860
|
} else if (r.status === 'auth_error') {
|
|
861
861
|
// 📖 A key is configured but the provider rejected it — keep this distinct
|
|
862
862
|
// 📖 from "no key" so configured-only mode does not look misleading.
|
|
863
|
-
statusText = `🔐
|
|
863
|
+
statusText = `🔐 ${t('dashboard.authFailShort')}`
|
|
864
864
|
statusColor = themeColors.errorBold
|
|
865
865
|
} else if (r.status === 'pending') {
|
|
866
|
-
statusText = `${FRAMES[frame % FRAMES.length]} wait`
|
|
866
|
+
statusText = `${FRAMES[frame % FRAMES.length]} ${t('common.wait')}`
|
|
867
867
|
statusColor = themeColors.warning
|
|
868
868
|
} else if (r.status === 'up') {
|
|
869
|
-
statusText = `✅
|
|
869
|
+
statusText = `✅ ${t('dashboard.upShort')}`
|
|
870
870
|
statusColor = themeColors.success
|
|
871
871
|
} else if (r.status === 'timeout') {
|
|
872
|
-
statusText = `⏳
|
|
872
|
+
statusText = `⏳ ${t('filters.health.timeout').toUpperCase()}`
|
|
873
873
|
statusColor = themeColors.warning
|
|
874
874
|
} else if (r.status === 'down') {
|
|
875
875
|
const code = r.httpCode ?? 'ERR'
|
|
@@ -899,10 +899,10 @@ export function renderTable({
|
|
|
899
899
|
const statusDisplayText = isCompact ? (() => {
|
|
900
900
|
// 📖 Strip emoji prefix to measure text length, then truncate if needed
|
|
901
901
|
const plainText = statusText.replace(/^[\p{Emoji_Presentation}\p{Extended_Pictographic}]\s*/u, '')
|
|
902
|
-
if (plainText
|
|
902
|
+
if (displayWidth(plainText) > 6) {
|
|
903
903
|
const emojiMatch = statusText.match(/^([\p{Emoji_Presentation}\p{Extended_Pictographic}]\s*)/u)
|
|
904
904
|
const prefix = emojiMatch ? emojiMatch[1] : ''
|
|
905
|
-
return prefix + plainText
|
|
905
|
+
return prefix + truncateAnsiWidth(plainText, 6, { ellipsis: '…' })
|
|
906
906
|
}
|
|
907
907
|
return statusText
|
|
908
908
|
})() : statusText
|
|
@@ -915,53 +915,53 @@ export function renderTable({
|
|
|
915
915
|
switch (verdict) {
|
|
916
916
|
case 'Perfect':
|
|
917
917
|
verdictIcon = '🟩'
|
|
918
|
-
verdictText = `${verdictIcon}
|
|
918
|
+
verdictText = `${verdictIcon} ${t('filters.verdict.perfect')}`
|
|
919
919
|
verdictColor = themeColors.successBold
|
|
920
920
|
break
|
|
921
921
|
case 'Normal':
|
|
922
922
|
verdictIcon = '🟢'
|
|
923
|
-
verdictText = `${verdictIcon}
|
|
923
|
+
verdictText = `${verdictIcon} ${t('filters.verdict.normal')}`
|
|
924
924
|
verdictColor = themeColors.metricGood
|
|
925
925
|
break
|
|
926
926
|
case 'Spiky':
|
|
927
927
|
verdictIcon = '🟡'
|
|
928
|
-
verdictText = `${verdictIcon}
|
|
928
|
+
verdictText = `${verdictIcon} ${t('filters.verdict.spiky')}`
|
|
929
929
|
verdictColor = (text) => chalk.bold.rgb(...getTierRgb('A+'))(text)
|
|
930
930
|
break
|
|
931
931
|
case 'Slow':
|
|
932
932
|
verdictIcon = '🟠'
|
|
933
|
-
verdictText = `${verdictIcon}
|
|
933
|
+
verdictText = `${verdictIcon} ${t('filters.verdict.slow')}`
|
|
934
934
|
verdictColor = (text) => chalk.bold.rgb(...getTierRgb('A-'))(text)
|
|
935
935
|
break
|
|
936
936
|
case 'Very Slow':
|
|
937
937
|
verdictIcon = '🔴'
|
|
938
|
-
verdictText = `${verdictIcon}
|
|
938
|
+
verdictText = `${verdictIcon} ${t('filters.verdict.verySlow')}`
|
|
939
939
|
verdictColor = (text) => chalk.bold.rgb(...getTierRgb('B+'))(text)
|
|
940
940
|
break
|
|
941
941
|
case 'Overloaded':
|
|
942
942
|
verdictIcon = '🔥'
|
|
943
|
-
verdictText = `${verdictIcon}
|
|
943
|
+
verdictText = `${verdictIcon} ${t('filters.verdict.overloaded')}`
|
|
944
944
|
verdictColor = (text) => chalk.bold.rgb(...getTierRgb('B'))(text)
|
|
945
945
|
break
|
|
946
946
|
case 'Unstable':
|
|
947
947
|
// 📖 Avoid ⚠️ here: its variation selector has inconsistent terminal width and shifts the tiny ❔ column.
|
|
948
948
|
verdictIcon = '🟥'
|
|
949
|
-
verdictText = `${verdictIcon}
|
|
949
|
+
verdictText = `${verdictIcon} ${t('filters.verdict.unstable')}`
|
|
950
950
|
verdictColor = themeColors.errorBold
|
|
951
951
|
break
|
|
952
952
|
case 'Not Active':
|
|
953
953
|
verdictIcon = '⚫'
|
|
954
|
-
verdictText = `${verdictIcon}
|
|
954
|
+
verdictText = `${verdictIcon} ${t('filters.verdict.notActive')}`
|
|
955
955
|
verdictColor = themeColors.dim
|
|
956
956
|
break
|
|
957
957
|
case 'Pending':
|
|
958
958
|
verdictIcon = '⏳'
|
|
959
|
-
verdictText = `${verdictIcon}
|
|
959
|
+
verdictText = `${verdictIcon} ${t('dashboard.pending')}`
|
|
960
960
|
verdictColor = themeColors.dim
|
|
961
961
|
break
|
|
962
962
|
default:
|
|
963
963
|
verdictIcon = '💀'
|
|
964
|
-
verdictText = `${verdictIcon}
|
|
964
|
+
verdictText = `${verdictIcon} ${t('filters.verdict.unusable')}`
|
|
965
965
|
verdictColor = (text) => chalk.bold.rgb(...getTierRgb('C'))(text)
|
|
966
966
|
break
|
|
967
967
|
}
|
|
@@ -973,15 +973,15 @@ export function renderTable({
|
|
|
973
973
|
const stabScore = getStabilityScore(r)
|
|
974
974
|
let stabCell
|
|
975
975
|
if (stabScore < 0) {
|
|
976
|
-
stabCell = themeColors.dim('———'
|
|
976
|
+
stabCell = themeColors.dim(padEndDisplay('———', wStab))
|
|
977
977
|
} else if (stabScore >= 80) {
|
|
978
|
-
stabCell = themeColors.metricGood(String(stabScore)
|
|
978
|
+
stabCell = themeColors.metricGood(padEndDisplay(String(stabScore), wStab))
|
|
979
979
|
} else if (stabScore >= 60) {
|
|
980
|
-
stabCell = themeColors.metricOk(String(stabScore)
|
|
980
|
+
stabCell = themeColors.metricOk(padEndDisplay(String(stabScore), wStab))
|
|
981
981
|
} else if (stabScore >= 40) {
|
|
982
|
-
stabCell = themeColors.metricWarn(String(stabScore)
|
|
982
|
+
stabCell = themeColors.metricWarn(padEndDisplay(String(stabScore), wStab))
|
|
983
983
|
} else {
|
|
984
|
-
stabCell = themeColors.metricBad(String(stabScore)
|
|
984
|
+
stabCell = themeColors.metricBad(padEndDisplay(String(stabScore), wStab))
|
|
985
985
|
}
|
|
986
986
|
|
|
987
987
|
// 📖 Uptime column - percentage of successful pings
|
|
@@ -990,19 +990,19 @@ export function renderTable({
|
|
|
990
990
|
const uptimeStr = uptimePercent + '%'
|
|
991
991
|
let uptimeCell
|
|
992
992
|
if (uptimePercent >= 90) {
|
|
993
|
-
uptimeCell = themeColors.metricGood(uptimeStr
|
|
993
|
+
uptimeCell = themeColors.metricGood(padEndDisplay(uptimeStr, W_UPTIME))
|
|
994
994
|
} else if (uptimePercent >= 70) {
|
|
995
|
-
uptimeCell = themeColors.metricWarn(uptimeStr
|
|
995
|
+
uptimeCell = themeColors.metricWarn(padEndDisplay(uptimeStr, W_UPTIME))
|
|
996
996
|
} else if (uptimePercent >= 50) {
|
|
997
|
-
uptimeCell = chalk.rgb(...getTierRgb('A-'))(uptimeStr
|
|
997
|
+
uptimeCell = chalk.rgb(...getTierRgb('A-'))(padEndDisplay(uptimeStr, W_UPTIME))
|
|
998
998
|
} else {
|
|
999
|
-
uptimeCell = themeColors.metricBad(uptimeStr
|
|
999
|
+
uptimeCell = themeColors.metricBad(padEndDisplay(uptimeStr, W_UPTIME))
|
|
1000
1000
|
}
|
|
1001
1001
|
|
|
1002
1002
|
// 📖 Model text now mirrors the provider hue so provider affinity is visible
|
|
1003
1003
|
// 📖 even before the eye reaches the Provider column.
|
|
1004
1004
|
const nameCell = themeColors.provider(r.providerKey, name, { bold: isCursor })
|
|
1005
|
-
const sourceCursorText = providerDisplay
|
|
1005
|
+
const sourceCursorText = padEndDisplay(providerDisplay, wSource)
|
|
1006
1006
|
const sourceCell = isCursor ? themeColors.provider(r.providerKey, sourceCursorText, { bold: true }) : source
|
|
1007
1007
|
|
|
1008
1008
|
// 📖 Check if this model is incompatible with the active tool mode
|
|
@@ -1165,7 +1165,7 @@ export function renderTable({
|
|
|
1165
1165
|
const configuredBadgeBg = getTheme() === 'dark' ? [52, 120, 88] : [195, 234, 206]
|
|
1166
1166
|
|
|
1167
1167
|
const configuredFilterActive = hideUnconfiguredModels || bestModeOnly
|
|
1168
|
-
const configuredFilterText = bestModeOnly ? '
|
|
1168
|
+
const configuredFilterText = bestModeOnly ? t('dashboard.workingOnly') : (hideUnconfiguredModels ? t('filters.visibility.configured') : t('filters.visibility.all'))
|
|
1169
1169
|
const activeHotkey = (keyLabel, text, bg) => themeColors.badge(`${keyLabel}${text}`, bg, getReadableTextRgb(bg))
|
|
1170
1170
|
const activeFilterHotkey = (keyLabel, text, bg) => themeColors.hotkey(keyLabel) + themeColors.badge(text, bg, getReadableTextRgb(bg))
|
|
1171
1171
|
|
|
@@ -1179,23 +1179,23 @@ export function renderTable({
|
|
|
1179
1179
|
{
|
|
1180
1180
|
const parts = [
|
|
1181
1181
|
{ text: ' ', key: null },
|
|
1182
|
-
{ text:
|
|
1182
|
+
{ text: `F ${t('dashboard.favorite')}`, key: 'f' },
|
|
1183
1183
|
{ text: ' • ', key: null },
|
|
1184
|
-
{ text:
|
|
1184
|
+
{ text: `Y ${t('dashboard.favorites')}`, key: 'y' },
|
|
1185
1185
|
{ text: ' • ', key: null },
|
|
1186
|
-
{ text: tierFilterMode > 0 ? `T
|
|
1186
|
+
{ text: tierFilterMode > 0 ? `T ${t('dashboard.tier')} (${activeTierLabel})` : `T ${t('dashboard.tier')}`, key: 't' },
|
|
1187
1187
|
{ text: ' • ', key: null },
|
|
1188
|
-
{ text: originFilterMode > 0 ? `D
|
|
1188
|
+
{ text: originFilterMode > 0 ? `D ${t('dashboard.provider')} (${activeOriginLabel})` : `D ${t('dashboard.provider')}`, key: 'd' },
|
|
1189
1189
|
{ text: ' • ', key: null },
|
|
1190
1190
|
{ text: `E ${configuredFilterText}`, key: 'e' },
|
|
1191
1191
|
{ text: ' • ', key: null },
|
|
1192
|
-
{ text:
|
|
1192
|
+
{ text: `P ${t('settings.title')}`, key: 'p' },
|
|
1193
1193
|
{ text: ' • ', key: null },
|
|
1194
|
-
{ text:
|
|
1194
|
+
{ text: `I ${t('help.title')}`, key: 'i' },
|
|
1195
1195
|
{ text: ' • ', key: null },
|
|
1196
|
-
{ text:
|
|
1196
|
+
{ text: `N ${t('common.reset')}`, key: 'n' },
|
|
1197
1197
|
{ text: ' • ', key: null },
|
|
1198
|
-
{ text:
|
|
1198
|
+
{ text: `G ${t('settings.theme')}`, key: 'g' },
|
|
1199
1199
|
]
|
|
1200
1200
|
const footerRow1 = lines.length + 1 // 📖 1-based terminal row (line hasn't been pushed yet)
|
|
1201
1201
|
let xPos = 1
|
|
@@ -1207,34 +1207,34 @@ export function renderTable({
|
|
|
1207
1207
|
}
|
|
1208
1208
|
|
|
1209
1209
|
lines.push(
|
|
1210
|
-
' ' + hotkey('F',
|
|
1210
|
+
' ' + hotkey('F', ` ${t('dashboard.favorite')}`) +
|
|
1211
1211
|
themeColors.dim(` • `) +
|
|
1212
|
-
hotkey('Y',
|
|
1212
|
+
hotkey('Y', ` ${t('dashboard.favorites')}`) +
|
|
1213
1213
|
themeColors.dim(` • `) +
|
|
1214
1214
|
(tierFilterMode > 0
|
|
1215
1215
|
? activeHotkey('T', ` Tier (${activeTierLabel})`, getTierRgb(activeTierLabel))
|
|
1216
|
-
: hotkey('T',
|
|
1216
|
+
: hotkey('T', ` ${t('dashboard.tier')}`)) +
|
|
1217
1217
|
themeColors.dim(` • `) +
|
|
1218
1218
|
(originFilterMode > 0
|
|
1219
1219
|
? activeHotkey('D', ` Provider (${activeOriginLabel})`, PROVIDER_COLOR[[null, ...Object.keys(sources)][originFilterMode]] || [255, 255, 255])
|
|
1220
|
-
: hotkey('D',
|
|
1220
|
+
: hotkey('D', ` ${t('dashboard.provider')}`)) +
|
|
1221
1221
|
themeColors.dim(` • `) +
|
|
1222
1222
|
(configuredFilterActive
|
|
1223
1223
|
? activeFilterHotkey('E', configuredFilterText, configuredBadgeBg)
|
|
1224
|
-
: hotkey('E',
|
|
1224
|
+
: hotkey('E', ` ${t('filters.visibility.all')}`)) +
|
|
1225
1225
|
themeColors.dim(` • `) +
|
|
1226
|
-
hotkey('P',
|
|
1226
|
+
hotkey('P', ` ${t('settings.title')}`) +
|
|
1227
1227
|
themeColors.dim(` • `) +
|
|
1228
|
-
hotkey('I',
|
|
1228
|
+
hotkey('I', ` ${t('help.title')}`) +
|
|
1229
1229
|
themeColors.dim(` • `) +
|
|
1230
|
-
hotkey('N',
|
|
1230
|
+
hotkey('N', ` ${t('common.reset')}`) +
|
|
1231
1231
|
themeColors.dim(` • `) +
|
|
1232
|
-
themeColors.hotkey('G') + themeColors.infoBold(
|
|
1232
|
+
themeColors.hotkey('G') + themeColors.infoBold(` ${t('settings.theme')}`)
|
|
1233
1233
|
)
|
|
1234
1234
|
|
|
1235
1235
|
// 📖 Line 2: command palette + GitHub
|
|
1236
1236
|
{
|
|
1237
|
-
|
|
1237
|
+
const cpText = ` Ctrl+P ${t('palette.footerTitle')} `
|
|
1238
1238
|
const parts = [
|
|
1239
1239
|
{ text: ' ', key: null },
|
|
1240
1240
|
{ text: cpText, key: 'ctrl+p' },
|
|
@@ -1250,7 +1250,7 @@ export function renderTable({
|
|
|
1250
1250
|
}
|
|
1251
1251
|
|
|
1252
1252
|
// 📖 Line 2: command palette (simple color, no background) + GitHub link.
|
|
1253
|
-
const paletteLabel = chalk.rgb(...currentPalette().cmdPalette).bold(
|
|
1253
|
+
const paletteLabel = chalk.rgb(...currentPalette().cmdPalette).bold(`Ctrl+P ${t('palette.footerTitle')}`)
|
|
1254
1254
|
const starLink = '⭐ ' + themeColors.link('\x1b]8;;https://github.com/vava-nessa/free-coding-models\x1b\\GitHub\x1b]8;;\x1b\\')
|
|
1255
1255
|
lines.push(
|
|
1256
1256
|
' ' + paletteLabel + themeColors.dim(` • `) + starLink + themeColors.dim(` • `) +
|
|
@@ -1258,9 +1258,14 @@ export function renderTable({
|
|
|
1258
1258
|
)
|
|
1259
1259
|
|
|
1260
1260
|
if (versionStatus.isOutdated) {
|
|
1261
|
+
const retryHint = t('update.retryHint')
|
|
1262
|
+
const retryTail = ` • ${retryHint} `
|
|
1263
|
+
const warningText = updateWarningMessage && terminalCols > 0
|
|
1264
|
+
? truncateAnsiWidth(updateWarningMessage, Math.max(1, terminalCols - displayWidth(` ${retryTail}`)), { ellipsis: '…' })
|
|
1265
|
+
: updateWarningMessage
|
|
1261
1266
|
const updateMsg = updateWarningMessage
|
|
1262
|
-
? ` ${
|
|
1263
|
-
: ` 🚀⬆️
|
|
1267
|
+
? ` ${warningText}${retryTail}`
|
|
1268
|
+
: ` 🚀⬆️ ${t('update.available').toUpperCase()} — v${LOCAL_VERSION} → v${versionStatus.latestVersion} • ${t('update.clickToInstall')} 🚀⬆️ `
|
|
1264
1269
|
const paddedBanner = terminalCols > 0
|
|
1265
1270
|
? updateMsg + ' '.repeat(Math.max(0, terminalCols - displayWidth(updateMsg)))
|
|
1266
1271
|
: updateMsg
|
|
@@ -1281,14 +1286,14 @@ export function renderTable({
|
|
|
1281
1286
|
let filterBadge = ''
|
|
1282
1287
|
if (hasCustomFilter) {
|
|
1283
1288
|
const normalizedFilter = customTextFilter.trim().replace(/\s+/g, ' ')
|
|
1284
|
-
const filterPrefix =
|
|
1289
|
+
const filterPrefix = `X ${t('filters.disableFilter')}: "`
|
|
1285
1290
|
const filterSuffix = '"'
|
|
1286
1291
|
const baseBadgeWidth = displayWidth(` ${filterPrefix}${filterSuffix} `)
|
|
1287
1292
|
const availableFilterWidth = terminalCols > 0
|
|
1288
1293
|
? Math.max(8, terminalCols - 4 - baseBadgeWidth)
|
|
1289
|
-
: normalizedFilter
|
|
1290
|
-
const visibleFilter = normalizedFilter
|
|
1291
|
-
?
|
|
1294
|
+
: displayWidth(normalizedFilter)
|
|
1295
|
+
const visibleFilter = displayWidth(normalizedFilter) > availableFilterWidth
|
|
1296
|
+
? truncateAnsiWidth(normalizedFilter, availableFilterWidth, { ellipsis: '…' })
|
|
1292
1297
|
: normalizedFilter
|
|
1293
1298
|
filterBadge = chalk.bgYellow.black.bold(` ${filterPrefix}${visibleFilter}${filterSuffix} `)
|
|
1294
1299
|
}
|
|
@@ -1296,7 +1301,7 @@ export function renderTable({
|
|
|
1296
1301
|
if (hasCustomFilter) {
|
|
1297
1302
|
// 📖 Mouse support: register click zone for the X-clear filter badge
|
|
1298
1303
|
const lastFooterRow = lines.length + 1
|
|
1299
|
-
const badgePlain = `X
|
|
1304
|
+
const badgePlain = `X ${t('filters.disableFilter')}: "${customTextFilter.trim().replace(/\s+/g, ' ')}"`
|
|
1300
1305
|
const fullText = ' ' + ` ${badgePlain} `
|
|
1301
1306
|
const xStart = 3 // 📖 after the leading 2 spaces
|
|
1302
1307
|
const xEnd = xStart + displayWidth(` ${badgePlain} `) - 1
|
|
@@ -1308,8 +1313,8 @@ export function renderTable({
|
|
|
1308
1313
|
const releaseLabel = lastReleaseDate
|
|
1309
1314
|
? chalk.rgb(...currentPalette().releaseDate)(`Last release: ${lastReleaseDate}`)
|
|
1310
1315
|
: ''
|
|
1311
|
-
const speedTestLabel = chalk.bgRgb(...currentPalette().badgeSpeedTestBg).rgb(...currentPalette().badgeSpeedTestFg).bold(
|
|
1312
|
-
const globalBenchmarkLabel = chalk.bgRgb(...currentPalette().badgeBenchmarkBg).rgb(...currentPalette().badgeBenchmarkFg).bold(
|
|
1316
|
+
const speedTestLabel = chalk.bgRgb(...currentPalette().badgeSpeedTestBg).rgb(...currentPalette().badgeSpeedTestFg).bold(` ⭐️ Ctrl+A 🤖 ${t('dashboard.aiSpeedTest')} `)
|
|
1317
|
+
const globalBenchmarkLabel = chalk.bgRgb(...currentPalette().badgeBenchmarkBg).rgb(...currentPalette().badgeBenchmarkFg).bold(` Ctrl+U : ${t('dashboard.globalBenchmark')} `)
|
|
1313
1318
|
|
|
1314
1319
|
// 📖 Probe badge: show progress when 404 probe is running or recently completed.
|
|
1315
1320
|
// 📖 Bar width is clamped to 0-20 cells (issue #168 kick-out fix): a transient
|
|
@@ -1465,5 +1470,5 @@ export function renderTable({
|
|
|
1465
1470
|
// 📖 Every line is prefixed with the theme bg so content always renders on
|
|
1466
1471
|
// 📖 the correct background. \x1b[K fills to end-of-line. The app-level render
|
|
1467
1472
|
// 📖 loop applies patchThemeBg() to undo chalk's \x1b[49m resets globally.
|
|
1468
|
-
return cleared.join('\n')
|
|
1473
|
+
return cleared.map(line => terminalCols > 0 ? truncateAnsiWidth(line, terminalCols, { ellipsis: '' }) + '\x1b[K' : line).join('\n')
|
|
1469
1474
|
}
|