free-coding-models 0.3.56 → 0.3.57
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -1
- package/README.md +10 -32
- package/package.json +1 -1
- package/sources.js +2 -2
- package/src/app.js +20 -87
- package/src/command-palette.js +1 -3
- package/src/config.js +2 -3
- package/src/constants.js +4 -4
- package/src/key-handler.js +12 -104
- package/src/overlays.js +20 -120
- package/src/provider-metadata.js +1 -1
- package/src/render-helpers.js +38 -8
- package/src/render-table.js +116 -278
- package/src/router-dashboard.js +10 -1
- package/web/dist/assets/{index-DNRCaWPi.js → index-DKHCzbK1.js} +1 -1
- package/web/dist/index.html +1 -1
package/src/render-table.js
CHANGED
|
@@ -36,33 +36,27 @@ import chalk from 'chalk'
|
|
|
36
36
|
import { createRequire } from 'module'
|
|
37
37
|
import { sources } from '../sources.js'
|
|
38
38
|
import {
|
|
39
|
-
TABLE_FIXED_LINES,
|
|
40
39
|
COL_MODEL,
|
|
41
40
|
TIER_CYCLE,
|
|
42
41
|
msCell,
|
|
43
42
|
spinCell,
|
|
44
43
|
PING_INTERVAL,
|
|
45
44
|
WIDTH_WARNING_MIN_COLS,
|
|
45
|
+
TABLE_FOOTER_LINES,
|
|
46
46
|
FRAMES
|
|
47
47
|
} from './constants.js'
|
|
48
48
|
import { themeColors, getProviderRgb, getTierRgb, getReadableTextRgb, getTheme } from './theme.js'
|
|
49
49
|
import { TIER_COLOR } from './tier-colors.js'
|
|
50
50
|
import { getAvg, getVerdict, getUptime, getStabilityScore, getVersionStatusInfo } from './utils.js'
|
|
51
|
-
import { VERDICT_CYCLE } from './constants.js'
|
|
52
51
|
import { usagePlaceholderForProvider } from './ping.js'
|
|
53
52
|
import { calculateViewport, sortResultsWithPinnedFavorites, padEndDisplay, displayWidth } from './render-helpers.js'
|
|
54
53
|
import { getToolMeta, TOOL_METADATA, TOOL_MODE_ORDER, isModelCompatibleWithTool } from './tool-metadata.js'
|
|
55
54
|
import { getColumnSpacing } from './ui-config.js'
|
|
56
55
|
import { detectPackageManager, getManualInstallCmd } from './updater.js'
|
|
57
|
-
import { formatTokenTotalCompact } from './token-usage-reader.js'
|
|
58
56
|
|
|
59
57
|
const require = createRequire(import.meta.url)
|
|
60
58
|
const { version: LOCAL_VERSION } = require('../package.json')
|
|
61
59
|
|
|
62
|
-
// 📖 HEALTH_CYCLE: cycles through health/status states (local constant for render-table.js)
|
|
63
|
-
// VERDICT_CYCLE is now imported from constants.js
|
|
64
|
-
const HEALTH_CYCLE = [null, 'up', 'timeout', 'down', 'auth_error', 'noauth', 'pending']
|
|
65
|
-
|
|
66
60
|
// 📖 Mouse support: column boundary map updated every frame by renderTable().
|
|
67
61
|
// 📖 Each entry maps a column name to its display X-start and X-end (1-based, inclusive).
|
|
68
62
|
// 📖 headerRow is the 1-based terminal row of the column header line.
|
|
@@ -110,9 +104,10 @@ export const PROVIDER_COLOR = new Proxy({}, {
|
|
|
110
104
|
})
|
|
111
105
|
|
|
112
106
|
// ─── renderTable: mode param controls footer hint text (opencode vs openclaw) ─────────
|
|
113
|
-
export function renderTable(results, pendingPings, frame, cursor = null, sortColumn = 'avg', sortDirection = 'asc', pingInterval = PING_INTERVAL, lastPingTime = Date.now(), mode = 'opencode', tierFilterMode = 0, scrollOffset = 0, terminalRows = 0, terminalCols = 0, originFilterMode = 0, legacyStatus = null, pingMode = 'normal', pingModeSource = 'auto', hideUnconfiguredModels = false, widthWarningStartedAt = null, widthWarningDismissed = false, widthWarningShowCount = 0, settingsUpdateState = 'idle', settingsUpdateLatestVersion = null, legacyFlag = false, startupLatestVersion = null, versionAlertsEnabled = true, favoritesPinnedAndSticky = false, customTextFilter = null, lastReleaseDate = null,
|
|
107
|
+
export function renderTable(results, pendingPings, frame, cursor = null, sortColumn = 'avg', sortDirection = 'asc', pingInterval = PING_INTERVAL, lastPingTime = Date.now(), mode = 'opencode', tierFilterMode = 0, scrollOffset = 0, terminalRows = 0, terminalCols = 0, originFilterMode = 0, legacyStatus = null, pingMode = 'normal', pingModeSource = 'auto', hideUnconfiguredModels = false, widthWarningStartedAt = null, widthWarningDismissed = false, widthWarningShowCount = 0, settingsUpdateState = 'idle', settingsUpdateLatestVersion = null, legacyFlag = false, startupLatestVersion = null, versionAlertsEnabled = true, favoritesPinnedAndSticky = false, customTextFilter = null, lastReleaseDate = null, legacyFooterHidden = false, verdictFilterMode = 0, healthFilterMode = 0, routerFooterRunning = false, routerFooterActiveSet = null, routerFooterTodayTokens = 0, routerFooterAllTimeTokens = 0, routerFooterRequests = 0) {
|
|
114
108
|
// 📖 Filter out hidden models for display
|
|
115
109
|
const visibleResults = results.filter(r => !r.hidden)
|
|
110
|
+
void legacyFooterHidden
|
|
116
111
|
|
|
117
112
|
const up = visibleResults.filter(r => r.status === 'up').length
|
|
118
113
|
const down = visibleResults.filter(r => r.status === 'down').length
|
|
@@ -324,80 +319,8 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
324
319
|
themeColors.warning(`⏳ ${timeout}`) + themeColors.dim(' timeout ') +
|
|
325
320
|
themeColors.error(`❌ ${down}`) + themeColors.dim(' down ') +
|
|
326
321
|
'',
|
|
327
|
-
'',
|
|
328
322
|
]
|
|
329
323
|
|
|
330
|
-
// 📖 Filter bar — llmfit-style horizontal filter pills (1 dedicated row above table)
|
|
331
|
-
// 📖 Each block: title with hotkey hint + active value colored by filter state
|
|
332
|
-
{
|
|
333
|
-
const filterParts = []
|
|
334
|
-
const filterSep = themeColors.dim(' │ ')
|
|
335
|
-
const blockSep = ' │ '
|
|
336
|
-
|
|
337
|
-
// 📖 Search filter block — shows active text filter or prompt
|
|
338
|
-
if (customTextFilter && customTextFilter.trim()) {
|
|
339
|
-
const badgeText = ` Search "/" ${blockSep} ${customTextFilter.trim().slice(0, 20)} `
|
|
340
|
-
filterParts.push(themeColors.badge(badgeText, [52, 120, 88], [255, 255, 255]))
|
|
341
|
-
} else {
|
|
342
|
-
filterParts.push(themeColors.dim(' Search "/" '))
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// 📖 Tier filter block — T key cycles through TIER_CYCLE
|
|
346
|
-
if (tierFilterMode > 0) {
|
|
347
|
-
const tierLabel = TIER_CYCLE_NAMES[tierFilterMode]
|
|
348
|
-
const tierBg = getTierRgb(tierLabel)
|
|
349
|
-
filterParts.push(themeColors.badge(` Tier (${tierLabel}) `, tierBg, [255, 255, 255]))
|
|
350
|
-
} else {
|
|
351
|
-
filterParts.push(themeColors.dim(' Tier (T) '))
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
// 📖 Provider filter block — D key cycles through providers
|
|
355
|
-
if (originFilterMode > 0) {
|
|
356
|
-
const originKeys = [null, ...Object.keys(sources)]
|
|
357
|
-
const activeOriginKey = originKeys[originFilterMode]
|
|
358
|
-
const activeOriginName = activeOriginKey ? sources[activeOriginKey]?.name ?? activeOriginKey : null
|
|
359
|
-
if (activeOriginName) {
|
|
360
|
-
const normName = normalizeOriginLabel(activeOriginName, activeOriginKey)
|
|
361
|
-
const providerRgb = PROVIDER_COLOR[activeOriginKey] || [255, 255, 255]
|
|
362
|
-
filterParts.push(themeColors.badge(` Provider (${normName}) `, providerRgb, [255, 255, 255]))
|
|
363
|
-
}
|
|
364
|
-
} else {
|
|
365
|
-
filterParts.push(themeColors.dim(' Provider (D) '))
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
// 📖 Verdict filter block — V key cycles through verdicts
|
|
369
|
-
if (verdictFilterMode > 0) {
|
|
370
|
-
const verdictLabel = VERDICT_CYCLE[verdictFilterMode]
|
|
371
|
-
const verdictColors = {
|
|
372
|
-
'Perfect': themeColors.success,
|
|
373
|
-
'Normal': themeColors.metricGood,
|
|
374
|
-
'Slow': (t) => chalk.bold.rgb(...getTierRgb('A-'))(t),
|
|
375
|
-
'Spiky': (t) => chalk.bold.rgb(...getTierRgb('A+'))(t),
|
|
376
|
-
'Very Slow': (t) => chalk.bold.rgb(...getTierRgb('B+'))(t),
|
|
377
|
-
'Overloaded': (t) => chalk.bold.rgb(...getTierRgb('B'))(t),
|
|
378
|
-
'Unstable': themeColors.errorBold,
|
|
379
|
-
'Not Active': themeColors.dim,
|
|
380
|
-
'Pending': themeColors.dim,
|
|
381
|
-
}
|
|
382
|
-
const vc = verdictColors[verdictLabel] || themeColors.accent
|
|
383
|
-
filterParts.push(themeColors.badge(` Verdict (${verdictLabel}) `, [20, 20, 20], vc === themeColors.dim ? [130, 130, 130] : [255, 255, 255]))
|
|
384
|
-
} else {
|
|
385
|
-
filterParts.push(themeColors.dim(' Verdict (V) '))
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
// 📖 Health filter block — H key cycles through health states
|
|
389
|
-
if (healthFilterMode > 0) {
|
|
390
|
-
const healthLabel = HEALTH_CYCLE[healthFilterMode]
|
|
391
|
-
const healthDisplay = healthLabel === 'auth_error' ? 'Auth Err' : healthLabel === 'noauth' ? 'No Key' : healthLabel.charAt(0).toUpperCase() + healthLabel.slice(1)
|
|
392
|
-
const healthBg = healthLabel === 'up' ? [52, 120, 88] : healthLabel === 'timeout' ? [180, 130, 0] : healthLabel === 'down' ? [120, 40, 40] : [60, 60, 60]
|
|
393
|
-
filterParts.push(themeColors.badge(` Health (${healthDisplay}) `, healthBg, [255, 255, 255]))
|
|
394
|
-
} else {
|
|
395
|
-
filterParts.push(themeColors.dim(' Health (H) '))
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
lines.push(filterParts.join(blockSep))
|
|
399
|
-
}
|
|
400
|
-
|
|
401
324
|
// 📖 Header row with sorting indicators
|
|
402
325
|
// 📖 NOTE: padEnd on chalk strings counts ANSI codes, breaking alignment
|
|
403
326
|
// 📖 Solution: build plain text first, then colorize
|
|
@@ -496,8 +419,11 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
496
419
|
|
|
497
420
|
// 📖 Viewport clipping: only render models that fit on screen
|
|
498
421
|
const hasCustomFilter = typeof customTextFilter === 'string' && customTextFilter.trim().length > 0
|
|
499
|
-
const
|
|
500
|
-
const
|
|
422
|
+
const hasReleaseFooter = typeof lastReleaseDate === 'string' && lastReleaseDate.trim().length > 0
|
|
423
|
+
const extraFooterLines = (versionStatus.isOutdated ? 1 : 0) + (hasCustomFilter ? 1 : 0) + (hasReleaseFooter ? 1 : 0)
|
|
424
|
+
const vp = calculateViewport(terminalRows, scrollOffset, sorted.length, {
|
|
425
|
+
extraFixedLines: extraFooterLines,
|
|
426
|
+
})
|
|
501
427
|
const paintSweScore = (score, paddedText) => {
|
|
502
428
|
if (score >= 70) return chalk.bold.rgb(...getTierRgb('S+'))(paddedText)
|
|
503
429
|
if (score >= 60) return chalk.bold.rgb(...getTierRgb('S'))(paddedText)
|
|
@@ -538,16 +464,14 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
538
464
|
? providerName.slice(0, 4) + '…'
|
|
539
465
|
: providerName
|
|
540
466
|
const source = themeColors.provider(r.providerKey, providerDisplay.padEnd(wSource))
|
|
541
|
-
|
|
542
|
-
let favoritePrefix = '
|
|
467
|
+
// 📖 Favorites marked with a single ⭐ — no ranking numbers
|
|
468
|
+
let favoritePrefix = ''
|
|
543
469
|
if (r.isRecommended) {
|
|
544
|
-
favoritePrefix = '🎯'
|
|
545
|
-
} else if (r.isFavorite && r.favoriteRank < CIRCLED.length) {
|
|
546
|
-
favoritePrefix = CIRCLED[r.favoriteRank]
|
|
470
|
+
favoritePrefix = '🎯 '
|
|
547
471
|
} else if (r.isFavorite) {
|
|
548
|
-
favoritePrefix = '⭐'
|
|
472
|
+
favoritePrefix = '⭐ '
|
|
549
473
|
}
|
|
550
|
-
const prefixDisplayWidth =
|
|
474
|
+
const prefixDisplayWidth = displayWidth(favoritePrefix)
|
|
551
475
|
const nameWidth = Math.max(0, W_MODEL - prefixDisplayWidth)
|
|
552
476
|
const name = favoritePrefix + r.label.slice(0, nameWidth).padEnd(nameWidth)
|
|
553
477
|
const sweScore = r.sweScore ?? '—'
|
|
@@ -793,7 +717,13 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
793
717
|
lines.push(themeColors.dim(` ... ${sorted.length - vp.endIdx} more below ...`))
|
|
794
718
|
}
|
|
795
719
|
|
|
796
|
-
|
|
720
|
+
// 📖 Blank lines keep the footer glued to the bottom without touching the sticky header.
|
|
721
|
+
if (terminalRows > 0) {
|
|
722
|
+
const footerLineCount = TABLE_FOOTER_LINES + extraFooterLines
|
|
723
|
+
const blankCount = Math.max(0, terminalRows - lines.length - footerLineCount)
|
|
724
|
+
for (let i = 0; i < blankCount; i++) lines.push('')
|
|
725
|
+
}
|
|
726
|
+
|
|
797
727
|
// 📖 Footer hints keep only navigation and secondary actions now that the
|
|
798
728
|
// 📖 active tool target is already visible in the header badge.
|
|
799
729
|
const hotkey = (keyLabel, text) => themeColors.hotkey(keyLabel) + themeColors.dim(text)
|
|
@@ -801,8 +731,6 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
801
731
|
// 📖 states are obvious even when the user misses the smaller header badges.
|
|
802
732
|
const configuredBadgeBg = getTheme() === 'dark' ? [52, 120, 88] : [195, 234, 206]
|
|
803
733
|
const activeHotkey = (keyLabel, text, bg) => themeColors.badge(`${keyLabel}${text}`, bg, getReadableTextRgb(bg))
|
|
804
|
-
const favoritesModeBg = favoritesPinnedAndSticky ? [157, 122, 48] : [95, 95, 95]
|
|
805
|
-
const favoritesModeLabel = favoritesPinnedAndSticky ? ' Favorites Pinned' : ' Favorites Normal'
|
|
806
734
|
|
|
807
735
|
// 📖 Mouse support: build footer hotkey zones alongside the footer lines.
|
|
808
736
|
// 📖 Each zone records { key, row (1-based terminal row), xStart, xEnd (1-based display cols) }.
|
|
@@ -814,21 +742,19 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
814
742
|
{
|
|
815
743
|
const parts = [
|
|
816
744
|
{ text: ' ', key: null },
|
|
817
|
-
{ text: 'F
|
|
745
|
+
{ text: 'F Favorite', key: 'f' },
|
|
818
746
|
{ text: ' • ', key: null },
|
|
819
|
-
{ text: 'Y'
|
|
747
|
+
{ text: 'Y Fav Mode', key: 'y' },
|
|
820
748
|
{ text: ' • ', key: null },
|
|
821
749
|
{ text: tierFilterMode > 0 ? `T Tier (${activeTierLabel})` : 'T Tier', key: 't' },
|
|
822
750
|
{ text: ' • ', key: null },
|
|
823
751
|
{ text: originFilterMode > 0 ? `D Provider (${activeOriginLabel})` : 'D Provider', key: 'd' },
|
|
824
752
|
{ text: ' • ', key: null },
|
|
825
|
-
{ text: 'E
|
|
753
|
+
{ text: 'E Active only', key: 'e' },
|
|
826
754
|
{ text: ' • ', key: null },
|
|
827
755
|
{ text: 'P Settings', key: 'p' },
|
|
828
756
|
{ text: ' • ', key: null },
|
|
829
|
-
{ text: '
|
|
830
|
-
{ text: ' • ', key: null },
|
|
831
|
-
{ text: 'Ctrl+H Help', key: 'ctrl+h' },
|
|
757
|
+
{ text: 'I Help', key: 'i' },
|
|
832
758
|
]
|
|
833
759
|
const footerRow1 = lines.length + 1 // 📖 1-based terminal row (line hasn't been pushed yet)
|
|
834
760
|
let xPos = 1
|
|
@@ -839,196 +765,108 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
839
765
|
}
|
|
840
766
|
}
|
|
841
767
|
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
(
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
(
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
{ text: ' • ', key: null },
|
|
877
|
-
{ text: 'G Theme', key: 'g' },
|
|
878
|
-
{ text: ' • ', key: null },
|
|
879
|
-
{ text: 'I Feedback, bugs & requests', key: 'i' },
|
|
880
|
-
]
|
|
881
|
-
const footerRow2 = lines.length + 1
|
|
882
|
-
let xPos = 1
|
|
883
|
-
for (const part of parts) {
|
|
884
|
-
const w = displayWidth(part.text)
|
|
885
|
-
if (part.key) footerHotkeys.push({ key: part.key, row: footerRow2, xStart: xPos, xEnd: xPos + w - 1 })
|
|
886
|
-
xPos += w
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
// 📖 Line 2: command palette (highlighted as new), recommend, feedback, and extended hints.
|
|
891
|
-
// 📖 CTRL+P ⚡️ Command Palette uses neon-green-on-dark-green background to highlight the feature.
|
|
892
|
-
const paletteLabel = chalk.bgRgb(0, 60, 0).rgb(57, 255, 20).bold(' CTRL+P ⚡️ Command Palette ')
|
|
893
|
-
lines.push(
|
|
894
|
-
' ' + paletteLabel + themeColors.dim(` • `) +
|
|
895
|
-
hotkey('Q', ' Smart Recommend') + themeColors.dim(` • `) +
|
|
896
|
-
hotkey('Shift+R', ' Router') + themeColors.dim(` • `) +
|
|
897
|
-
hotkey('G', ' Theme') + themeColors.dim(` • `) +
|
|
898
|
-
hotkey('I', ' Feedback, bugs & requests')
|
|
899
|
-
)
|
|
900
|
-
// 📖 Proxy status is now shown via the badge in line 2 above — no need for a dedicated line
|
|
901
|
-
const footerLine =
|
|
902
|
-
themeColors.footerLove(' Made with 💖 & ☕ by \x1b]8;;https://github.com/vava-nessa\x1b\\vava-nessa\x1b]8;;\x1b\\') +
|
|
903
|
-
themeColors.dim(' • ') +
|
|
904
|
-
'⭐ ' +
|
|
905
|
-
themeColors.link('\x1b]8;;https://github.com/vava-nessa/free-coding-models\x1b\\Star on GitHub\x1b]8;;\x1b\\') +
|
|
906
|
-
themeColors.dim(' • ') +
|
|
907
|
-
'🤝 ' +
|
|
908
|
-
themeColors.warning('\x1b]8;;https://github.com/vava-nessa/free-coding-models/graphs/contributors\x1b\\Contributors\x1b]8;;\x1b\\') +
|
|
909
|
-
themeColors.dim(' • ') +
|
|
910
|
-
'☕ ' +
|
|
911
|
-
themeColors.footerCoffee('\x1b]8;;https://buymeacoffee.com/vavanessadev\x1b\\Buy me a coffee\x1b]8;;\x1b\\')
|
|
912
|
-
lines.push(footerLine)
|
|
913
|
-
|
|
914
|
-
if (versionStatus.isOutdated) {
|
|
915
|
-
const updateMsg = ` 🚀⬆️ UPDATE AVAILABLE — v${LOCAL_VERSION} → v${versionStatus.latestVersion} • Click here or press Shift+U to update 🚀⬆️ `
|
|
916
|
-
const paddedBanner = terminalCols > 0
|
|
917
|
-
? updateMsg + ' '.repeat(Math.max(0, terminalCols - displayWidth(updateMsg)))
|
|
918
|
-
: updateMsg
|
|
919
|
-
const fluoGreenBanner = chalk.bgRgb(57, 255, 20).rgb(0, 0, 0).bold(paddedBanner)
|
|
920
|
-
const updateBannerRow = lines.length + 1
|
|
921
|
-
_lastLayout.updateBannerRow = updateBannerRow
|
|
922
|
-
footerHotkeys.push({ key: 'update-click', row: updateBannerRow, xStart: 1, xEnd: Math.max(terminalCols, displayWidth(updateMsg)) })
|
|
923
|
-
lines.push(fluoGreenBanner)
|
|
924
|
-
} else {
|
|
925
|
-
_lastLayout.updateBannerRow = 0
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
// 📖 Final footer line: changelog + optional active text-filter badge + exit hint.
|
|
929
|
-
let filterBadge = ''
|
|
930
|
-
if (hasCustomFilter) {
|
|
931
|
-
const normalizedFilter = customTextFilter.trim().replace(/\s+/g, ' ')
|
|
932
|
-
const filterPrefix = 'X Disable filter: "'
|
|
933
|
-
const filterSuffix = '"'
|
|
934
|
-
const separatorPlain = ' • '
|
|
935
|
-
const baseFooterPlain = ' N Changelog' + separatorPlain + 'Ctrl+C Exit'
|
|
936
|
-
const baseBadgeWidth = displayWidth(` ${filterPrefix}${filterSuffix} `)
|
|
937
|
-
const availableFilterWidth = terminalCols > 0
|
|
938
|
-
? Math.max(8, terminalCols - displayWidth(baseFooterPlain) - displayWidth(separatorPlain) - baseBadgeWidth)
|
|
939
|
-
: normalizedFilter.length
|
|
940
|
-
const visibleFilter = normalizedFilter.length > availableFilterWidth
|
|
941
|
-
? `${normalizedFilter.slice(0, Math.max(3, availableFilterWidth - 3))}...`
|
|
942
|
-
: normalizedFilter
|
|
943
|
-
filterBadge = chalk.bgYellow.black.bold(` ${filterPrefix}${visibleFilter}${filterSuffix} `)
|
|
768
|
+
lines.push(
|
|
769
|
+
' ' + hotkey('F', ' Favorite') +
|
|
770
|
+
themeColors.dim(` • `) +
|
|
771
|
+
hotkey('Y', ' Fav Mode') +
|
|
772
|
+
themeColors.dim(` • `) +
|
|
773
|
+
(tierFilterMode > 0
|
|
774
|
+
? activeHotkey('T', ` Tier (${activeTierLabel})`, getTierRgb(activeTierLabel))
|
|
775
|
+
: hotkey('T', ' Tier')) +
|
|
776
|
+
themeColors.dim(` • `) +
|
|
777
|
+
(originFilterMode > 0
|
|
778
|
+
? activeHotkey('D', ` Provider (${activeOriginLabel})`, PROVIDER_COLOR[[null, ...Object.keys(sources)][originFilterMode]] || [255, 255, 255])
|
|
779
|
+
: hotkey('D', ' Provider')) +
|
|
780
|
+
themeColors.dim(` • `) +
|
|
781
|
+
(hideUnconfiguredModels ? activeHotkey('E', ' Active only', configuredBadgeBg) : hotkey('E', ' Active only')) +
|
|
782
|
+
themeColors.dim(` • `) +
|
|
783
|
+
hotkey('P', ' Settings') +
|
|
784
|
+
themeColors.dim(` • `) +
|
|
785
|
+
hotkey('I', ' Help')
|
|
786
|
+
)
|
|
787
|
+
|
|
788
|
+
// 📖 Line 2: command palette + GitHub
|
|
789
|
+
{
|
|
790
|
+
const cpText = ' Ctrl+P Cmd Palette '
|
|
791
|
+
const parts = [
|
|
792
|
+
{ text: ' ', key: null },
|
|
793
|
+
{ text: cpText, key: 'ctrl+p' },
|
|
794
|
+
{ text: ' ', key: null },
|
|
795
|
+
]
|
|
796
|
+
const footerRow2 = lines.length + 1
|
|
797
|
+
let xPos = 1
|
|
798
|
+
for (const part of parts) {
|
|
799
|
+
const w = displayWidth(part.text)
|
|
800
|
+
if (part.key) footerHotkeys.push({ key: part.key, row: footerRow2, xStart: xPos, xEnd: xPos + w - 1 })
|
|
801
|
+
xPos += w
|
|
944
802
|
}
|
|
803
|
+
}
|
|
945
804
|
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
805
|
+
// 📖 Line 2: command palette (highlighted as new) + GitHub link.
|
|
806
|
+
// 📖 Ctrl+P Cmd Palette uses neon-green-on-dark-green background to highlight the feature.
|
|
807
|
+
const paletteLabel = chalk.bgRgb(0, 60, 0).rgb(57, 255, 20).bold(' Ctrl+P Cmd Palette ')
|
|
808
|
+
const starLink = '⭐ ' + themeColors.link('\x1b]8;;https://github.com/vava-nessa/free-coding-models\x1b\\GitHub\x1b]8;;\x1b\\')
|
|
809
|
+
lines.push(
|
|
810
|
+
' ' + paletteLabel + themeColors.dim(` • `) + starLink + themeColors.dim(` • `) +
|
|
811
|
+
chalk.rgb(255, 168, 209).bold('\x1b]8;;https://x.com/vavanessadev\x1b\\Support me by following me on X ! @vavanessadev\x1b]8;;\x1b\\')
|
|
812
|
+
)
|
|
813
|
+
|
|
814
|
+
if (versionStatus.isOutdated) {
|
|
815
|
+
const updateMsg = ` 🚀⬆️ UPDATE AVAILABLE — v${LOCAL_VERSION} → v${versionStatus.latestVersion} • Click here or press Shift+U to update 🚀⬆️ `
|
|
816
|
+
const paddedBanner = terminalCols > 0
|
|
817
|
+
? updateMsg + ' '.repeat(Math.max(0, terminalCols - displayWidth(updateMsg)))
|
|
818
|
+
: updateMsg
|
|
819
|
+
const fluoGreenBanner = chalk.bgRgb(57, 255, 20).rgb(0, 0, 0).bold(paddedBanner)
|
|
820
|
+
const updateBannerRow = lines.length + 1
|
|
821
|
+
_lastLayout.updateBannerRow = updateBannerRow
|
|
822
|
+
footerHotkeys.push({ key: 'update-click', row: updateBannerRow, xStart: 1, xEnd: Math.max(terminalCols, displayWidth(updateMsg)) })
|
|
823
|
+
lines.push(fluoGreenBanner)
|
|
824
|
+
} else {
|
|
825
|
+
_lastLayout.updateBannerRow = 0
|
|
826
|
+
}
|
|
966
827
|
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
const allTimeStr = formatTokenTotalCompact(routerFooterAllTimeTokens)
|
|
985
|
-
const reqStr = String(routerFooterRequests)
|
|
986
|
-
const setLabel = routerFooterActiveSet ? themeColors.info(routerFooterActiveSet) : themeColors.dim('?')
|
|
987
|
-
lines.push(
|
|
988
|
-
' ' + themeColors.success('●') + ' ' +
|
|
989
|
-
themeColors.dim('Router:') + ' ' + setLabel +
|
|
990
|
-
themeColors.dim(' • Today:') + ' ' + themeColors.textBold(todayStr + ' tok') +
|
|
991
|
-
themeColors.dim(' • All-time:') + ' ' + themeColors.textBold(allTimeStr + ' tok') +
|
|
992
|
-
themeColors.dim(' • ' + reqStr + ' req')
|
|
993
|
-
)
|
|
994
|
-
} else {
|
|
995
|
-
lines.push(
|
|
996
|
-
' ' + themeColors.error('○') + ' ' +
|
|
997
|
-
themeColors.dim('Router:') + ' ' + themeColors.dim('daemon not running') +
|
|
998
|
-
themeColors.dim(' • Shift+R Dashboard')
|
|
999
|
-
)
|
|
1000
|
-
}
|
|
828
|
+
// 📖 Optional active text-filter badge — surfaced inline if a custom filter is active.
|
|
829
|
+
// 📖 Changelog moved to Settings (P), Ctrl+C Exit moved to Help (Ctrl+H), Discord
|
|
830
|
+
// 📖 moved to onboarding + Settings — no more orphan hint lines down here.
|
|
831
|
+
let filterBadge = ''
|
|
832
|
+
if (hasCustomFilter) {
|
|
833
|
+
const normalizedFilter = customTextFilter.trim().replace(/\s+/g, ' ')
|
|
834
|
+
const filterPrefix = 'X Disable filter: "'
|
|
835
|
+
const filterSuffix = '"'
|
|
836
|
+
const baseBadgeWidth = displayWidth(` ${filterPrefix}${filterSuffix} `)
|
|
837
|
+
const availableFilterWidth = terminalCols > 0
|
|
838
|
+
? Math.max(8, terminalCols - 4 - baseBadgeWidth)
|
|
839
|
+
: normalizedFilter.length
|
|
840
|
+
const visibleFilter = normalizedFilter.length > availableFilterWidth
|
|
841
|
+
? `${normalizedFilter.slice(0, Math.max(3, availableFilterWidth - 3))}...`
|
|
842
|
+
: normalizedFilter
|
|
843
|
+
filterBadge = chalk.bgYellow.black.bold(` ${filterPrefix}${visibleFilter}${filterSuffix} `)
|
|
844
|
+
}
|
|
1001
845
|
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
)
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
lines.push(
|
|
1012
|
-
' ' + themeColors.hotkey('Ctrl+O') + themeColors.dim(' Toggle Footer') +
|
|
1013
|
-
themeColors.dim(' • ') +
|
|
1014
|
-
themeColors.hotkey('Shift+R') + themeColors.dim(' Router') +
|
|
1015
|
-
themeColors.dim(' • Ctrl+C Exit')
|
|
1016
|
-
)
|
|
846
|
+
if (hasCustomFilter) {
|
|
847
|
+
// 📖 Mouse support: register click zone for the X-clear filter badge
|
|
848
|
+
const lastFooterRow = lines.length + 1
|
|
849
|
+
const badgePlain = `X Disable filter: "${customTextFilter.trim().replace(/\s+/g, ' ')}"`
|
|
850
|
+
const fullText = ' ' + ` ${badgePlain} `
|
|
851
|
+
const xStart = 3 // 📖 after the leading 2 spaces
|
|
852
|
+
const xEnd = xStart + displayWidth(` ${badgePlain} `) - 1
|
|
853
|
+
footerHotkeys.push({ key: 'x', row: lastFooterRow, xStart, xEnd })
|
|
854
|
+
void fullText
|
|
855
|
+
lines.push(' ' + filterBadge)
|
|
1017
856
|
}
|
|
1018
857
|
|
|
858
|
+
const releaseLabel = lastReleaseDate
|
|
859
|
+
? chalk.rgb(255, 182, 193)(`Last release: ${lastReleaseDate}`)
|
|
860
|
+
: ''
|
|
861
|
+
|
|
862
|
+
if (releaseLabel) lines.push(' ' + releaseLabel)
|
|
1019
863
|
_lastLayout.footerHotkeys = footerHotkeys
|
|
1020
864
|
|
|
1021
865
|
// 📖 Append \x1b[K (erase to EOL) to each line so leftover chars from previous
|
|
1022
|
-
// 📖 frames are cleared. \x1b[J
|
|
1023
|
-
// 📖
|
|
866
|
+
// 📖 frames are cleared. \x1b[J clears stale content below without adding a
|
|
867
|
+
// 📖 newline that could scroll the alternate screen.
|
|
1024
868
|
const EL = '\x1b[K'
|
|
1025
869
|
const cleared = lines.map(l => l + EL)
|
|
1026
|
-
if (
|
|
1027
|
-
// 📖 When footer is hidden, \x1b[J erases stale footer content below the cursor
|
|
1028
|
-
cleared.push('\x1b[J')
|
|
1029
|
-
} else {
|
|
1030
|
-
const remaining = terminalRows > 0 ? Math.max(0, terminalRows - cleared.length) : 0
|
|
1031
|
-
for (let i = 0; i < remaining; i++) cleared.push(EL)
|
|
1032
|
-
}
|
|
870
|
+
if (cleared.length > 0) cleared[cleared.length - 1] += '\x1b[J'
|
|
1033
871
|
return cleared.join('\n')
|
|
1034
872
|
}
|
package/src/router-dashboard.js
CHANGED
|
@@ -826,6 +826,15 @@ export function renderRouterDashboard(state, deps = {}) {
|
|
|
826
826
|
if (snapshot.models.length === 0) {
|
|
827
827
|
lines.push(` ${themeColors.dim('No model health rows available yet. Start the daemon or wait for /stats to answer.')}`)
|
|
828
828
|
} else {
|
|
829
|
+
// 📖 Keycap emoji digits 1️⃣…🔟 — large, colorful, instantly readable in the
|
|
830
|
+
// 📖 router order column. Capped at 10 because the active set should never
|
|
831
|
+
// 📖 contain more than 10 priority slots in practice.
|
|
832
|
+
const KEYCAPS = ['1️⃣','2️⃣','3️⃣','4️⃣','5️⃣','6️⃣','7️⃣','8️⃣','9️⃣','🔟']
|
|
833
|
+
const priorityGlyph = (priority) => {
|
|
834
|
+
const n = Number(priority)
|
|
835
|
+
if (Number.isFinite(n) && n >= 1 && n <= KEYCAPS.length) return KEYCAPS[n - 1]
|
|
836
|
+
return '—'
|
|
837
|
+
}
|
|
829
838
|
const header = ` ${padEndDisplay('#', 4)} ${padEndDisplay('Provider', 12)} ${padEndDisplay('Model', 30)} ${padEndDisplay('State', 12)} ${padEndDisplay('P95', 8)} ${padEndDisplay('Up', 5)} Score`
|
|
830
839
|
lines.push(themeColors.dim(header))
|
|
831
840
|
for (const model of snapshot.models.slice(0, 12)) {
|
|
@@ -833,7 +842,7 @@ export function renderRouterDashboard(state, deps = {}) {
|
|
|
833
842
|
const score = Number.isFinite(model.score) ? model.score.toFixed(2) : '—'
|
|
834
843
|
const errorSuffix = model.last_error ? themeColors.dim(` ${compactText(model.last_error, 22).trimEnd()}`) : ''
|
|
835
844
|
lines.push(
|
|
836
|
-
` ${padEndDisplay(
|
|
845
|
+
` ${padEndDisplay(priorityGlyph(model.priority), 4)} ` +
|
|
837
846
|
`${compactText(model.provider, 12)} ` +
|
|
838
847
|
`${compactText(model.model, 30)} ` +
|
|
839
848
|
`${padEndDisplay(modelStateBadge(model.state), 12)} ` +
|