free-coding-models 0.3.80 β 0.4.1
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 +4 -1
- package/changelog/v0.3.81.md +14 -0
- package/changelog/v0.4.0.md +23 -0
- package/package.json +1 -1
- package/src/app.js +21 -1
- package/src/benchmark.js +78 -34
- package/src/cli-help.js +1 -1
- package/src/config.js +3 -1
- package/src/key-handler.js +82 -8
- package/src/overlays.js +15 -4
- package/src/render-helpers.js +4 -3
- package/src/render-table.js +196 -92
- package/src/setup.js +16 -6
- package/src/theme.js +1 -1
- package/src/tui-state.js +5 -0
- package/src/utils.js +25 -1
- package/web/dist/assets/{index-DDz3_efL.js β index-A9aoSZsh.js} +1 -1
- package/web/dist/index.html +1 -1
package/src/render-table.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* with consistent alignment, colorization, and viewport clipping.
|
|
9
9
|
*
|
|
10
10
|
* π― Key features:
|
|
11
|
-
* - Full table layout with tier, latency, stability, uptime, token totals, and usage columns
|
|
11
|
+
* - Full table layout with micro verdict indicator, tier, latency, stability, uptime, token totals, and usage columns
|
|
12
12
|
* - Hotkey-aware header lettering so highlighted letters always match live sort/filter keys
|
|
13
13
|
* - Emoji-aware padding via padEndDisplay for aligned verdict/status cells
|
|
14
14
|
* - Viewport clipping with above/below indicators
|
|
@@ -45,7 +45,7 @@ import {
|
|
|
45
45
|
TABLE_FOOTER_LINES,
|
|
46
46
|
FRAMES
|
|
47
47
|
} from './constants.js'
|
|
48
|
-
import { themeColors, getProviderRgb, getTierRgb, getReadableTextRgb, getTheme } from './theme.js'
|
|
48
|
+
import { themeColors, currentPalette, 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
51
|
import { usagePlaceholderForProvider } from './ping.js'
|
|
@@ -79,6 +79,7 @@ export function getLastLayout() { return _lastLayout }
|
|
|
79
79
|
|
|
80
80
|
// π Column name β sort key mapping for mouse click-to-sort on header row
|
|
81
81
|
const COLUMN_SORT_MAP = {
|
|
82
|
+
mood: 'verdict',
|
|
82
83
|
rank: 'rank',
|
|
83
84
|
tier: null, // π Tier column click cycles tier filter rather than sorting
|
|
84
85
|
swe: 'swe',
|
|
@@ -91,8 +92,8 @@ const COLUMN_SORT_MAP = {
|
|
|
91
92
|
verdict: 'verdict',
|
|
92
93
|
stability: 'stability',
|
|
93
94
|
uptime: 'uptime',
|
|
94
|
-
aiLatency:
|
|
95
|
-
tps:
|
|
95
|
+
aiLatency: 'aiLatency',
|
|
96
|
+
tps: 'tps',
|
|
96
97
|
}
|
|
97
98
|
export { COLUMN_SORT_MAP }
|
|
98
99
|
|
|
@@ -186,7 +187,8 @@ export function renderTable({
|
|
|
186
187
|
routerFooterRequests = 0,
|
|
187
188
|
benchmarkResults = {},
|
|
188
189
|
benchmarkRunning = new Set(),
|
|
189
|
-
|
|
190
|
+
headerFlashColumn = null,
|
|
191
|
+
} = _) {
|
|
190
192
|
// π Filter out hidden models for display
|
|
191
193
|
const visibleResults = results.filter(r => !r.hidden)
|
|
192
194
|
|
|
@@ -270,16 +272,17 @@ export function renderTable({
|
|
|
270
272
|
const COL_SEP = getColumnSpacing()
|
|
271
273
|
const SEP_W = 3 // ' β ' display width
|
|
272
274
|
const ROW_MARGIN = 2 // left margin ' '
|
|
275
|
+
const W_MOOD = 2
|
|
273
276
|
const W_RANK = 6
|
|
274
277
|
const W_TIER = 5
|
|
275
278
|
const W_CTX = 4
|
|
276
279
|
const W_SOURCE = 14
|
|
277
280
|
const W_MODEL = 26
|
|
278
281
|
const W_SWE = 5
|
|
279
|
-
const W_STATUS =
|
|
280
|
-
const W_VERDICT =
|
|
282
|
+
const W_STATUS = 17
|
|
283
|
+
const W_VERDICT = 13
|
|
281
284
|
const W_UPTIME = 6
|
|
282
|
-
const W_AI_LATENCY =
|
|
285
|
+
const W_AI_LATENCY = 17
|
|
283
286
|
const W_TPS = 5
|
|
284
287
|
|
|
285
288
|
// const W_TOKENS = 7 // Used column removed
|
|
@@ -289,11 +292,12 @@ export function renderTable({
|
|
|
289
292
|
// π Responsive column visibility: progressively hide least-useful columns
|
|
290
293
|
// π and shorten header labels when terminal width is insufficient.
|
|
291
294
|
// π Hiding order (least useful first): Rank β AI Latency/TPS β Up% β Tier β Stability
|
|
292
|
-
// π
|
|
293
|
-
// π
|
|
295
|
+
// π Ping columns stay compact because the cell values are tiny numbers without a "ms" suffix.
|
|
296
|
+
// π Both ping columns use the same 9-char width so Last Ping and Avg Ping fit cleanly.
|
|
297
|
+
// π Compact mode also shrinks StabilityβStaB. (8), Providerβ4chars+β¦ (7), Healthβ6chars+β¦ (13).
|
|
294
298
|
// π Breakpoints are computed dynamically from active column widths.
|
|
295
|
-
let wPing =
|
|
296
|
-
let wAvg =
|
|
299
|
+
let wPing = 9
|
|
300
|
+
let wAvg = 9
|
|
297
301
|
let wStab = 11
|
|
298
302
|
let wSource = W_SOURCE
|
|
299
303
|
let wStatus = W_STATUS
|
|
@@ -309,6 +313,7 @@ export function renderTable({
|
|
|
309
313
|
// π Dynamically compute needed row width from visible columns
|
|
310
314
|
const calcWidth = () => {
|
|
311
315
|
const cols = []
|
|
316
|
+
cols.push(W_MOOD)
|
|
312
317
|
if (showRank) cols.push(W_RANK)
|
|
313
318
|
if (showTier) cols.push(W_TIER)
|
|
314
319
|
cols.push(W_SWE, W_CTX, W_MODEL, wSource, wPing, wAvg, wStatus, W_VERDICT)
|
|
@@ -321,8 +326,8 @@ export function renderTable({
|
|
|
321
326
|
// π Step 1: Compact mode β shorten labels and reduce column widths
|
|
322
327
|
if (calcWidth() > terminalCols) {
|
|
323
328
|
isCompact = true
|
|
324
|
-
wPing = 9 // '
|
|
325
|
-
wAvg =
|
|
329
|
+
wPing = 9 // 'Last Ping' stays compact and matches Avg Ping width
|
|
330
|
+
wAvg = 9 // 'Avg Ping' stays aligned with Last Ping
|
|
326
331
|
wStab = 8 // 'StaB.' instead of 'Stability'
|
|
327
332
|
wSource = 7 // Provider truncated to 4 chars + 'β¦', 7 cols total
|
|
328
333
|
wStatus = 13 // Health truncated after 6 chars + 'β¦'
|
|
@@ -341,6 +346,7 @@ export function renderTable({
|
|
|
341
346
|
// π matching exactly what renderTable paints so click-to-sort hits the right column.
|
|
342
347
|
{
|
|
343
348
|
const colDefs = []
|
|
349
|
+
colDefs.push({ name: 'mood', width: W_MOOD })
|
|
344
350
|
if (showRank) colDefs.push({ name: 'rank', width: W_RANK })
|
|
345
351
|
if (showTier) colDefs.push({ name: 'tier', width: W_TIER })
|
|
346
352
|
colDefs.push({ name: 'swe', width: W_SWE })
|
|
@@ -402,6 +408,7 @@ export function renderTable({
|
|
|
402
408
|
// π Sort models using the shared helper
|
|
403
409
|
const sorted = sortResultsWithPinnedFavorites(visibleResults, sortColumn, sortDirection, {
|
|
404
410
|
pinFavorites: favoritesPinnedAndSticky,
|
|
411
|
+
benchmarkResults,
|
|
405
412
|
})
|
|
406
413
|
|
|
407
414
|
const lines = [
|
|
@@ -418,24 +425,20 @@ export function renderTable({
|
|
|
418
425
|
// π Solution: build plain text first, then colorize
|
|
419
426
|
const dir = sortDirection === 'asc' ? 'β' : 'β'
|
|
420
427
|
|
|
421
|
-
|
|
422
|
-
const
|
|
423
|
-
const
|
|
424
|
-
const
|
|
425
|
-
const
|
|
426
|
-
const
|
|
427
|
-
|
|
428
|
-
const
|
|
429
|
-
const
|
|
430
|
-
const
|
|
431
|
-
const
|
|
432
|
-
const
|
|
433
|
-
const
|
|
434
|
-
const
|
|
435
|
-
// π Stability: in non-compact the arrow eats 2 chars ('β '), so truncate to fit wStab.
|
|
436
|
-
// π Compact is fine because 'β StaB.' (7) < wStab (8).
|
|
437
|
-
const stabH = sortColumn === 'stability' ? (dir + (isCompact ? ' ' + stabLabel : 'Stability')) : stabLabel
|
|
438
|
-
const uptimeH = sortColumn === 'uptime' ? (dir + 'Up%') : 'Up%'
|
|
428
|
+
// π Plain header labels β arrows are appended dynamically below.
|
|
429
|
+
const moodLabel = 'β'
|
|
430
|
+
const rankLabel = 'Rank'
|
|
431
|
+
const tierLabel = 'Tier'
|
|
432
|
+
const originLabel = isCompact ? 'PrODβ¦' : 'Provider'
|
|
433
|
+
const modelLabel = 'Model'
|
|
434
|
+
const sweLabel = 'SWE%'
|
|
435
|
+
const ctxLabel = 'CTX'
|
|
436
|
+
const pingLabel = 'Last Ping'
|
|
437
|
+
const avgLabel = 'Avg Ping'
|
|
438
|
+
const healthLabel = 'Health'
|
|
439
|
+
const verdictLabel = 'Verdict'
|
|
440
|
+
const stabLabel = isCompact ? 'StaB.' : 'Stability'
|
|
441
|
+
const uptimeLabel = 'Up%'
|
|
439
442
|
|
|
440
443
|
// π Helper to colorize first letter for keyboard shortcuts
|
|
441
444
|
// π IMPORTANT: Pad PLAIN TEXT first, then apply colors to avoid alignment issues
|
|
@@ -447,50 +450,116 @@ export function renderTable({
|
|
|
447
450
|
return colorFn(first) + themeColors.dim(rest + padding)
|
|
448
451
|
}
|
|
449
452
|
|
|
450
|
-
// π
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
// π
|
|
474
|
-
|
|
453
|
+
// π Flash animation: when a column header is clicked, it briefly renders with
|
|
454
|
+
// π a vivid inverse style (bright accent bg + white bold fg) for ~250ms.
|
|
455
|
+
// π This gives satisfying visual feedback that the click was registered.
|
|
456
|
+
const flashHeader = (plainText, width) => {
|
|
457
|
+
const padded = plainText.length <= width ? plainText.padEnd(width) : plainText.slice(0, width)
|
|
458
|
+
const [r, g, b] = currentPalette().accentStrong
|
|
459
|
+
return chalk.bold.rgb(255, 255, 255).bgRgb(r, g, b)(padded)
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// π Sort-active header: renders the column header with a subtle background color
|
|
463
|
+
// π to visually indicate which column is currently sorted.
|
|
464
|
+
// π Includes the β/β arrow and bold white text on a tinted background.
|
|
465
|
+
// π If the arrow prefix doesn't fit, appends it: 'SWEβ' instead of 'β SWE%'.
|
|
466
|
+
const sortActiveHeader = (label, width) => {
|
|
467
|
+
const arrow = dir
|
|
468
|
+
const prefixed = arrow + ' ' + label
|
|
469
|
+
const text = prefixed.length <= width ? prefixed : label + arrow
|
|
470
|
+
const padded = text.padEnd(width).slice(0, width)
|
|
471
|
+
// π Subtle dark accent background β visible but not overwhelming.
|
|
472
|
+
const bg = currentPalette().cursor.defaultBg
|
|
473
|
+
return chalk.bold.rgb(255, 255, 255).bgRgb(...bg)(padded)
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// π Now colorize each column header.
|
|
477
|
+
// π Three rendering states per column:
|
|
478
|
+
// π 1. FLASH β headerFlashColumn matches β vivid inverse style (click feedback)
|
|
479
|
+
// π 2. ACTIVE β sortColumn matches β subtle bg + β/β arrow
|
|
480
|
+
// π 3. DEFAULT β normal dim text with highlighted first letter
|
|
481
|
+
|
|
482
|
+
// π Helper: pick the right style for a standard column header.
|
|
483
|
+
// π colKey = sort key (e.g. 'rank', 'swe'), label = plain text, width = column width.
|
|
484
|
+
const headerStyle = (colKey, label, width) => {
|
|
485
|
+
const arrowText = dir + ' ' + label
|
|
486
|
+
const flashText = arrowText.length <= width ? arrowText : label + dir
|
|
487
|
+
if (headerFlashColumn === colKey) return flashHeader(flashText, width)
|
|
488
|
+
if (sortColumn === colKey) return sortActiveHeader(label, width)
|
|
489
|
+
return colorFirst(label, width)
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const moodH_c = (() => {
|
|
493
|
+
// π Tiny verdict indicator column: keep it emoji-only, no arrow, so it stays 2 cells wide.
|
|
494
|
+
const padded = padEndDisplay(moodLabel, W_MOOD)
|
|
495
|
+
if (headerFlashColumn === 'verdict') return chalk.bold.rgb(255, 255, 255).bgRgb(...currentPalette().accentStrong)(padded)
|
|
496
|
+
if (sortColumn === 'verdict') return chalk.bold.rgb(255, 255, 255).bgRgb(...currentPalette().cursor.defaultBg)(padded)
|
|
497
|
+
return themeColors.hotkey(padded)
|
|
498
|
+
})()
|
|
499
|
+
const rankH_c = headerStyle('rank', rankLabel, W_RANK)
|
|
500
|
+
const tierH_c = (() => {
|
|
501
|
+
if (headerFlashColumn === 'tier') return flashHeader(tierLabel, W_TIER)
|
|
502
|
+
return colorFirst(tierLabel, W_TIER)
|
|
503
|
+
})()
|
|
504
|
+
const modelH_c = headerStyle('model', modelLabel, W_MODEL)
|
|
505
|
+
const sweH_c = headerStyle('swe', sweLabel, W_SWE)
|
|
506
|
+
const ctxH_c = headerStyle('ctx', ctxLabel, W_CTX)
|
|
507
|
+
const pingH_c = headerStyle('ping', pingLabel, wPing)
|
|
508
|
+
const avgH_c = headerStyle('avg', avgLabel, wAvg)
|
|
509
|
+
const healthH_c = headerStyle('condition', healthLabel, wStatus)
|
|
510
|
+
const verdictH_c = headerStyle('verdict', verdictLabel, W_VERDICT)
|
|
511
|
+
const stabH_c = (() => {
|
|
512
|
+
if (headerFlashColumn === 'stability') {
|
|
513
|
+
const ft = (dir + ' ' + stabLabel).length <= wStab ? dir + ' ' + stabLabel : stabLabel + dir
|
|
514
|
+
return flashHeader(ft, wStab)
|
|
515
|
+
}
|
|
516
|
+
if (sortColumn === 'stability') return sortActiveHeader(stabLabel, wStab)
|
|
475
517
|
const plain = stabLabel
|
|
476
518
|
const padding = ' '.repeat(Math.max(0, wStab - plain.length))
|
|
477
519
|
return themeColors.dim('Sta') + themeColors.hotkey('B') + themeColors.dim((isCompact ? '.' : 'ility') + padding)
|
|
478
520
|
})()
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
521
|
+
const uptimeH_c = (() => {
|
|
522
|
+
if (headerFlashColumn === 'uptime') {
|
|
523
|
+
const ft = (dir + ' ' + uptimeLabel).length <= W_UPTIME ? dir + ' ' + uptimeLabel : uptimeLabel + dir
|
|
524
|
+
return flashHeader(ft, W_UPTIME)
|
|
525
|
+
}
|
|
526
|
+
if (sortColumn === 'uptime') return sortActiveHeader(uptimeLabel, W_UPTIME)
|
|
527
|
+
const padding = ' '.repeat(Math.max(0, W_UPTIME - uptimeLabel.length))
|
|
483
528
|
return themeColors.hotkey('U') + themeColors.dim('p%' + padding)
|
|
484
529
|
})()
|
|
530
|
+
const originH_c = (() => {
|
|
531
|
+
if (headerFlashColumn === 'origin') {
|
|
532
|
+
const ft = (dir + ' ' + originLabel).length <= wSource ? dir + ' ' + originLabel : originLabel + dir
|
|
533
|
+
return flashHeader(ft, wSource)
|
|
534
|
+
}
|
|
535
|
+
if (sortColumn === 'origin') return sortActiveHeader(originLabel, wSource)
|
|
536
|
+
if (originFilterMode > 0) return themeColors.accentBold(originLabel.padEnd(wSource))
|
|
537
|
+
const plain = isCompact ? 'PrODβ¦' : 'PrOviDer'
|
|
538
|
+
const padding = ' '.repeat(Math.max(0, wSource - plain.length))
|
|
539
|
+
if (isCompact) {
|
|
540
|
+
return themeColors.dim('Pr') + themeColors.hotkey('O') + themeColors.hotkey('D') + themeColors.dim('β¦' + padding)
|
|
541
|
+
}
|
|
542
|
+
return themeColors.dim('Pr') + themeColors.hotkey('O') + themeColors.dim('vi') + themeColors.hotkey('D') + themeColors.dim('er' + padding)
|
|
543
|
+
})()
|
|
485
544
|
|
|
486
545
|
// π Benchmark headers β split the old combined AI Speed field into latency + throughput.
|
|
487
546
|
const aiLatencyLabel = isCompact ? 'AI Lat.' : 'AI Latency'
|
|
488
547
|
const aiLatencyH_c = (() => {
|
|
548
|
+
if (headerFlashColumn === 'aiLatency') {
|
|
549
|
+
const ft = (dir + ' ' + aiLatencyLabel).length <= wAiLatency ? dir + ' ' + aiLatencyLabel : aiLatencyLabel + dir
|
|
550
|
+
return flashHeader(ft, wAiLatency)
|
|
551
|
+
}
|
|
552
|
+
if (sortColumn === 'aiLatency') return sortActiveHeader(aiLatencyLabel, wAiLatency)
|
|
489
553
|
const plain = aiLatencyLabel
|
|
490
554
|
const padding = ' '.repeat(Math.max(0, wAiLatency - plain.length))
|
|
491
555
|
return themeColors.dim(plain + padding)
|
|
492
556
|
})()
|
|
493
557
|
const tpsH_c = (() => {
|
|
558
|
+
if (headerFlashColumn === 'tps') {
|
|
559
|
+
const ft = (dir + ' ' + 'TPS').length <= W_TPS ? dir + ' ' + 'TPS' : 'TPS' + dir
|
|
560
|
+
return flashHeader(ft, W_TPS)
|
|
561
|
+
}
|
|
562
|
+
if (sortColumn === 'tps') return sortActiveHeader('TPS', W_TPS)
|
|
494
563
|
const plain = 'TPS'
|
|
495
564
|
const padding = ' '.repeat(Math.max(0, W_TPS - plain.length))
|
|
496
565
|
return themeColors.dim(plain + padding)
|
|
@@ -498,7 +567,7 @@ export function renderTable({
|
|
|
498
567
|
|
|
499
568
|
// π Usage column removed from UI β no header or separator for it.
|
|
500
569
|
// π Header row: conditionally include columns based on responsive visibility
|
|
501
|
-
const headerParts = []
|
|
570
|
+
const headerParts = [moodH_c]
|
|
502
571
|
if (showRank) headerParts.push(rankH_c)
|
|
503
572
|
if (showTier) headerParts.push(tierH_c)
|
|
504
573
|
headerParts.push(sweH_c, ctxH_c, modelH_c, originH_c, pingH_c, avgH_c, healthH_c, verdictH_c)
|
|
@@ -715,52 +784,63 @@ export function renderTable({
|
|
|
715
784
|
|
|
716
785
|
// π Verdict column - use getVerdict() for stability-aware verdicts, then render with emoji
|
|
717
786
|
const verdict = getVerdict(r)
|
|
718
|
-
let verdictText, verdictColor
|
|
787
|
+
let verdictText, verdictIcon, verdictColor
|
|
719
788
|
// π Verdict colors follow the same greenβred gradient as TIER_COLOR / SWE%
|
|
720
789
|
switch (verdict) {
|
|
721
790
|
case 'Perfect':
|
|
722
|
-
|
|
791
|
+
verdictIcon = 'π©'
|
|
792
|
+
verdictText = `${verdictIcon} Perfect`
|
|
723
793
|
verdictColor = themeColors.successBold
|
|
724
794
|
break
|
|
725
795
|
case 'Normal':
|
|
726
|
-
|
|
796
|
+
verdictIcon = 'π’'
|
|
797
|
+
verdictText = `${verdictIcon} Normal`
|
|
727
798
|
verdictColor = themeColors.metricGood
|
|
728
799
|
break
|
|
729
800
|
case 'Spiky':
|
|
730
|
-
|
|
801
|
+
verdictIcon = 'π‘'
|
|
802
|
+
verdictText = `${verdictIcon} Spiky`
|
|
731
803
|
verdictColor = (text) => chalk.bold.rgb(...getTierRgb('A+'))(text)
|
|
732
804
|
break
|
|
733
805
|
case 'Slow':
|
|
734
|
-
|
|
806
|
+
verdictIcon = 'π '
|
|
807
|
+
verdictText = `${verdictIcon} Slow`
|
|
735
808
|
verdictColor = (text) => chalk.bold.rgb(...getTierRgb('A-'))(text)
|
|
736
809
|
break
|
|
737
810
|
case 'Very Slow':
|
|
738
|
-
|
|
811
|
+
verdictIcon = 'π΄'
|
|
812
|
+
verdictText = `${verdictIcon} Very Slow`
|
|
739
813
|
verdictColor = (text) => chalk.bold.rgb(...getTierRgb('B+'))(text)
|
|
740
814
|
break
|
|
741
815
|
case 'Overloaded':
|
|
742
|
-
|
|
816
|
+
verdictIcon = 'π₯'
|
|
817
|
+
verdictText = `${verdictIcon} Overloaded`
|
|
743
818
|
verdictColor = (text) => chalk.bold.rgb(...getTierRgb('B'))(text)
|
|
744
819
|
break
|
|
745
820
|
case 'Unstable':
|
|
746
|
-
|
|
821
|
+
verdictIcon = 'β οΈ'
|
|
822
|
+
verdictText = `${verdictIcon} Unstable`
|
|
747
823
|
verdictColor = themeColors.errorBold
|
|
748
824
|
break
|
|
749
825
|
case 'Not Active':
|
|
750
|
-
|
|
826
|
+
verdictIcon = 'β«'
|
|
827
|
+
verdictText = `${verdictIcon} Not Active`
|
|
751
828
|
verdictColor = themeColors.dim
|
|
752
829
|
break
|
|
753
830
|
case 'Pending':
|
|
754
|
-
|
|
831
|
+
verdictIcon = 'β³'
|
|
832
|
+
verdictText = `${verdictIcon} Pending`
|
|
755
833
|
verdictColor = themeColors.dim
|
|
756
834
|
break
|
|
757
835
|
default:
|
|
758
|
-
|
|
836
|
+
verdictIcon = 'π'
|
|
837
|
+
verdictText = `${verdictIcon} Unusable`
|
|
759
838
|
verdictColor = (text) => chalk.bold.rgb(...getTierRgb('C'))(text)
|
|
760
839
|
break
|
|
761
840
|
}
|
|
762
841
|
// π Use padEndDisplay to account for emoji display width (2 cols each) so all rows align
|
|
763
842
|
const speedCell = verdictColor(padEndDisplay(verdictText, W_VERDICT))
|
|
843
|
+
const moodCell = padEndDisplay(verdictIcon, W_MOOD)
|
|
764
844
|
|
|
765
845
|
// π Stability column - composite score (0β100) from p95 + jitter + spikes + uptime
|
|
766
846
|
// π Left-aligned to sit flush under the column header
|
|
@@ -807,30 +887,54 @@ export function renderTable({
|
|
|
807
887
|
const usageCell = ''
|
|
808
888
|
|
|
809
889
|
// π AI Latency + TPS columns β same benchmark result, split into two readable metrics.
|
|
890
|
+
// π Benchmark results are shown regardless of health status (up/timeout/down/429/noauth).
|
|
891
|
+
// π If benchmark failed β red dash. Error details live in the Health column.
|
|
892
|
+
// π If no benchmark has been run yet, show dim dash.
|
|
893
|
+
// π Retry badge (β»N) is colored blue and spaced from the main value.
|
|
810
894
|
const benchmarkKey = `${r.providerKey}/${r.modelId}`
|
|
811
895
|
const benchmarkResult = benchmarkResults[benchmarkKey]
|
|
812
896
|
const isBenchmarkRunning = benchmarkRunning.has(benchmarkKey)
|
|
813
|
-
const
|
|
814
|
-
const
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
const
|
|
818
|
-
?
|
|
819
|
-
:
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
897
|
+
const hasBenchmark = benchmarkResult || isBenchmarkRunning
|
|
898
|
+
const benchmarkOk = benchmarkResult && benchmarkResult.ok
|
|
899
|
+
|
|
900
|
+
// π Build latency cell: value + blue retry badge
|
|
901
|
+
const latParsed = isBenchmarkRunning
|
|
902
|
+
? formatBenchmarkLatency(benchmarkResult, { running: true, frame })
|
|
903
|
+
: benchmarkOk
|
|
904
|
+
? formatBenchmarkLatency(benchmarkResult)
|
|
905
|
+
: { text: 'β', retryBadge: '' }
|
|
906
|
+
const latValue = benchmarkOk
|
|
907
|
+
? themeColors.metricGood(latParsed.text)
|
|
908
|
+
: hasBenchmark
|
|
909
|
+
? themeColors.metricBad(latParsed.text)
|
|
910
|
+
: themeColors.dim(latParsed.text)
|
|
911
|
+
const latBadge = latParsed.retryBadge
|
|
912
|
+
? themeColors.info(' ' + latParsed.retryBadge)
|
|
913
|
+
: ''
|
|
914
|
+
const latBadgeWidth = latParsed.retryBadge ? displayWidth(' ' + latParsed.retryBadge) : 0
|
|
915
|
+
const latPad = wAiLatency - displayWidth(latParsed.text) - latBadgeWidth
|
|
916
|
+
const latencyCell = latValue + latBadge + themeColors.dim(''.padEnd(Math.max(0, latPad)))
|
|
917
|
+
|
|
918
|
+
// π Build TPS cell: value + blue retry badge
|
|
919
|
+
const tpsParsed = isBenchmarkRunning
|
|
920
|
+
? formatBenchmarkTps(benchmarkResult, { running: true, frame })
|
|
921
|
+
: benchmarkOk
|
|
922
|
+
? formatBenchmarkTps(benchmarkResult)
|
|
923
|
+
: { text: 'β', retryBadge: '' }
|
|
924
|
+
const tpsValue = benchmarkOk || isBenchmarkRunning
|
|
925
|
+
? themeColors.metricGood(tpsParsed.text)
|
|
926
|
+
: hasBenchmark
|
|
927
|
+
? themeColors.metricBad(tpsParsed.text)
|
|
928
|
+
: themeColors.dim(tpsParsed.text)
|
|
929
|
+
const tpsBadge = tpsParsed.retryBadge
|
|
930
|
+
? themeColors.info(' ' + tpsParsed.retryBadge)
|
|
931
|
+
: ''
|
|
932
|
+
const tpsBadgeWidth = tpsParsed.retryBadge ? displayWidth(' ' + tpsParsed.retryBadge) : 0
|
|
933
|
+
const tpsPad = W_TPS - displayWidth(tpsParsed.text) - tpsBadgeWidth
|
|
934
|
+
const tpsCell = tpsValue + tpsBadge + themeColors.dim(''.padEnd(Math.max(0, tpsPad)))
|
|
831
935
|
|
|
832
936
|
// π Build row: conditionally include columns based on responsive visibility
|
|
833
|
-
const rowParts = []
|
|
937
|
+
const rowParts = [moodCell]
|
|
834
938
|
if (showRank) rowParts.push(num)
|
|
835
939
|
if (showTier) rowParts.push(tier)
|
|
836
940
|
rowParts.push(sweCell, ctxCell, nameCell, sourceCell, pingCell, avgCell, status, speedCell)
|
package/src/setup.js
CHANGED
|
@@ -16,8 +16,9 @@
|
|
|
16
16
|
* 1. Builds a `providers` list from `Object.keys(sources)` so new providers added to
|
|
17
17
|
* sources.js automatically appear in the wizard without any code changes here.
|
|
18
18
|
* 2. Uses `readline.createInterface` for line-at-a-time input (not raw mode).
|
|
19
|
-
* 3.
|
|
20
|
-
* 4.
|
|
19
|
+
* 3. Asks whether the opt-in startup AI Speed Test should run on every launch.
|
|
20
|
+
* 4. Calls `saveConfig(config)` once after collecting all answers.
|
|
21
|
+
* 5. Returns the nvidia key (or the first entered key) for backward-compatibility with
|
|
21
22
|
* the `main()` caller that originally checked for `nvidiKey !== null` before continuing.
|
|
22
23
|
*
|
|
23
24
|
* @functions
|
|
@@ -86,18 +87,27 @@ export async function promptApiKey(config) {
|
|
|
86
87
|
}
|
|
87
88
|
}
|
|
88
89
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
// π Check at least one key was entered
|
|
90
|
+
// π Check at least one key was entered before asking optional behavior questions.
|
|
92
91
|
const anyKey = Object.values(config.apiKeys).some(v => v)
|
|
93
92
|
if (!anyKey) {
|
|
93
|
+
rl.close()
|
|
94
94
|
return null
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
console.log(chalk.bold(' β‘ Startup AI Speed Scan'))
|
|
98
|
+
console.log(chalk.dim(' FCM can automatically run the Ctrl+U benchmark after launch to fill AI Latency + TPS.'))
|
|
99
|
+
console.log(chalk.dim(' This uses real provider requests, so it is opt-in and can be changed later in Settings.'))
|
|
100
|
+
const autoBenchmarkAnswer = await ask(chalk.dim(' Run the AI Speed Scan automatically on every launch? (y/N): '))
|
|
101
|
+
if (!config.settings || typeof config.settings !== 'object') config.settings = {}
|
|
102
|
+
config.settings.runAiSpeedTestOnStartup = ['y', 'yes', 'oui', 'o'].includes(autoBenchmarkAnswer.toLowerCase())
|
|
103
|
+
console.log()
|
|
104
|
+
|
|
105
|
+
rl.close()
|
|
106
|
+
|
|
97
107
|
saveConfig(config)
|
|
98
108
|
const savedCount = Object.values(config.apiKeys).filter(v => v).length
|
|
99
109
|
console.log(chalk.green(` β
${savedCount} key(s) saved to ~/.free-coding-models.json`))
|
|
100
|
-
console.log(chalk.dim(' You can add
|
|
110
|
+
console.log(chalk.dim(' You can add/change keys and toggle Startup AI Speed Scan anytime with the ') + chalk.yellow('P') + chalk.dim(' key in the TUI.'))
|
|
101
111
|
console.log()
|
|
102
112
|
|
|
103
113
|
// π Return nvidia key for backward-compat (main() checks it exists before continuing)
|
package/src/theme.js
CHANGED
package/src/tui-state.js
CHANGED
|
@@ -273,5 +273,10 @@ export function createTuiState({
|
|
|
273
273
|
globalBenchmarkRunning: false,
|
|
274
274
|
globalBenchmarkTotal: 0,
|
|
275
275
|
globalBenchmarkCompleted: 0,
|
|
276
|
+
|
|
277
|
+
// π Header click flash animation: briefly highlights the clicked column header
|
|
278
|
+
// π with an inverse/bright style for ~250ms (3 frames at 12 FPS).
|
|
279
|
+
headerFlashColumn: null, // π Column name being flashed (null = no flash active)
|
|
280
|
+
headerFlashUntilFrame: 0, // π Frame number when flash expires
|
|
276
281
|
}
|
|
277
282
|
}
|
package/src/utils.js
CHANGED
|
@@ -235,7 +235,7 @@ export const getStabilityScore = (r) => {
|
|
|
235
235
|
// - 'stability' (B key) β stability score (0β100, higher = more stable)
|
|
236
236
|
//
|
|
237
237
|
// π sortDirection 'asc' = ascending (smallest first), 'desc' = descending (largest first)
|
|
238
|
-
export const sortResults = (results, sortColumn, sortDirection) => {
|
|
238
|
+
export const sortResults = (results, sortColumn, sortDirection, { benchmarkResults = {} } = {}) => {
|
|
239
239
|
return [...results].sort((a, b) => {
|
|
240
240
|
let cmp = 0
|
|
241
241
|
|
|
@@ -317,6 +317,30 @@ export const sortResults = (results, sortColumn, sortDirection) => {
|
|
|
317
317
|
// π Models with no data (-1) sort to the bottom
|
|
318
318
|
cmp = getStabilityScore(a) - getStabilityScore(b)
|
|
319
319
|
break
|
|
320
|
+
case 'aiLatency': {
|
|
321
|
+
// π Sort by AI benchmark latency (totalMs). Lower = better.
|
|
322
|
+
// π Models without benchmark data sort to the bottom.
|
|
323
|
+
const aKey = `${a.providerKey}/${a.modelId}`
|
|
324
|
+
const bKey = `${b.providerKey}/${b.modelId}`
|
|
325
|
+
const aBench = benchmarkResults[aKey]
|
|
326
|
+
const bBench = benchmarkResults[bKey]
|
|
327
|
+
const aMs = (aBench?.ok && aBench.totalMs != null) ? aBench.totalMs : Infinity
|
|
328
|
+
const bMs = (bBench?.ok && bBench.totalMs != null) ? bBench.totalMs : Infinity
|
|
329
|
+
cmp = aMs - bMs
|
|
330
|
+
break
|
|
331
|
+
}
|
|
332
|
+
case 'tps': {
|
|
333
|
+
// π Sort by benchmark throughput (tokens/second). Higher = better.
|
|
334
|
+
// π Models without benchmark data sort to the bottom.
|
|
335
|
+
const aKey2 = `${a.providerKey}/${a.modelId}`
|
|
336
|
+
const bKey2 = `${b.providerKey}/${b.modelId}`
|
|
337
|
+
const aBench2 = benchmarkResults[aKey2]
|
|
338
|
+
const bBench2 = benchmarkResults[bKey2]
|
|
339
|
+
const aTps = (aBench2?.ok && aBench2.tokensPerSecond != null) ? aBench2.tokensPerSecond : -1
|
|
340
|
+
const bTps = (bBench2?.ok && bBench2.tokensPerSecond != null) ? bBench2.tokensPerSecond : -1
|
|
341
|
+
cmp = aTps - bTps
|
|
342
|
+
break
|
|
343
|
+
}
|
|
320
344
|
case 'usage':
|
|
321
345
|
// π Sort by quota usage percent (usagePercent numeric field, 0β100)
|
|
322
346
|
// π Models with no usage data (undefined/null) are treated as 0 β stable tie-break
|