free-coding-models 0.3.19 → 0.3.21
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 +14 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/app.js +11 -34
- package/src/cli-help.js +1 -2
- package/src/config.js +0 -3
- package/src/constants.js +5 -0
- package/src/key-handler.js +3 -47
- package/src/overlays.js +1 -10
- package/src/render-table.js +119 -50
- package/src/testfcm.js +1 -1
- package/src/utils.js +0 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
+
## 0.3.21
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
- **Responsive table layout**: the TUI now adapts to narrow terminals by progressively shrinking columns and hiding least-useful ones. Compact mode shortens headers (`Lat. P`, `Avg. P`, `StaB.`, `PrOD…`) and tightens Provider (10 cols), Health (13 cols), Latest Ping (10 cols), and Avg Ping (8 cols). Below compact, Rank → Up% → Tier → Stability are hidden one by one. Minimum usable width: ~116 cols.
|
|
9
|
+
- **SWE% column tightened**: reduced from 9 to 6 columns wide — trims excess right padding without losing data.
|
|
10
|
+
- **Provider truncation in compact mode**: provider names longer than 5 chars are truncated to 4 chars + `…` (e.g. `Cere…`).
|
|
11
|
+
- **Health truncation in compact mode**: status text longer than 6 chars is truncated with `…` (e.g. `🔥 429 TR…`).
|
|
12
|
+
- **Removed "Used" column from TUI**: the token usage column has been removed from the main table as it was outdated.
|
|
13
|
+
- **Width guardrail threshold tightened**: the narrow-terminal warning now triggers only below 80 columns (instead of broader widths) and keeps the same 2-second auto-hide behavior.
|
|
14
|
+
- **Width warning is always enforced in narrow terminals**: removed the `Small Width Warnings` Settings toggle and the `--disable-widths-warning` runtime flag so the startup guardrail stays consistent.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- **`--premium` filter lock-in**: premium now applies a resettable startup preset (S-tier + verdict sort) instead of hard-locking an extra hidden elite-only filter.
|
|
18
|
+
|
|
5
19
|
## 0.3.19
|
|
6
20
|
|
|
7
21
|
### Added
|
package/README.md
CHANGED
|
@@ -133,7 +133,7 @@ free-coding-models --goose --tier S
|
|
|
133
133
|
# "I want NVIDIA's top models only"
|
|
134
134
|
free-coding-models --origin nvidia --tier S
|
|
135
135
|
|
|
136
|
-
# "
|
|
136
|
+
# "Start with an elite-focused preset, then adjust filters live"
|
|
137
137
|
free-coding-models --premium
|
|
138
138
|
|
|
139
139
|
# "I want to script this — give me JSON"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "free-coding-models",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.21",
|
|
4
4
|
"description": "Find the fastest coding LLM models in seconds — ping free models from multiple providers, pick the best one for OpenCode, Cursor, or any AI coding assistant.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"nvidia",
|
package/src/app.js
CHANGED
|
@@ -105,7 +105,7 @@ import { usageForRow as _usageForRow } from '../src/usage-reader.js'
|
|
|
105
105
|
import { buildProviderModelTokenKey, loadTokenUsageByProviderModel } from '../src/token-usage-reader.js'
|
|
106
106
|
import { parseOpenRouterResponse, fetchProviderQuota as _fetchProviderQuotaFromModule } from '../src/provider-quota-fetchers.js'
|
|
107
107
|
import { isKnownQuotaTelemetry } from '../src/quota-capabilities.js'
|
|
108
|
-
import { ALT_ENTER, ALT_LEAVE, ALT_HOME, PING_TIMEOUT, PING_INTERVAL, FPS, COL_MODEL, COL_MS, CELL_W, FRAMES, TIER_CYCLE, SETTINGS_OVERLAY_BG, HELP_OVERLAY_BG, RECOMMEND_OVERLAY_BG, OVERLAY_PANEL_WIDTH, TABLE_HEADER_LINES, TABLE_FOOTER_LINES, TABLE_FIXED_LINES, msCell, spinCell } from '../src/constants.js'
|
|
108
|
+
import { ALT_ENTER, ALT_LEAVE, ALT_HOME, PING_TIMEOUT, PING_INTERVAL, FPS, COL_MODEL, COL_MS, CELL_W, FRAMES, TIER_CYCLE, SETTINGS_OVERLAY_BG, HELP_OVERLAY_BG, RECOMMEND_OVERLAY_BG, OVERLAY_PANEL_WIDTH, TABLE_HEADER_LINES, TABLE_FOOTER_LINES, TABLE_FIXED_LINES, WIDTH_WARNING_MIN_COLS, msCell, spinCell } from '../src/constants.js'
|
|
109
109
|
import { TIER_COLOR } from '../src/tier-colors.js'
|
|
110
110
|
import { resolveCloudflareUrl, buildPingRequest, ping, extractQuotaPercent, getProviderQuotaPercentCached, usagePlaceholderForProvider } from '../src/ping.js'
|
|
111
111
|
import { runFiableMode, filterByTierOrExit, fetchOpenRouterFreeModels } from '../src/analysis.js'
|
|
@@ -192,9 +192,8 @@ export async function runApp(cliArgs, config) {
|
|
|
192
192
|
if (cliArgs.pingInterval) config.settings.pingInterval = cliArgs.pingInterval
|
|
193
193
|
if (cliArgs.hideUnconfigured) config.settings.hideUnconfiguredModels = true
|
|
194
194
|
if (cliArgs.showUnconfigured) config.settings.hideUnconfiguredModels = false
|
|
195
|
-
if (cliArgs.disableWidthsWarning) config.settings.disableWidthsWarning = true
|
|
196
195
|
|
|
197
|
-
// 📖 Apply premium mode
|
|
196
|
+
// 📖 Apply premium mode as an initial, user-resettable view preset.
|
|
198
197
|
if (cliArgs.premiumMode) {
|
|
199
198
|
config.settings.tierFilter = 'S'
|
|
200
199
|
config.settings.sortColumn = 'verdict'
|
|
@@ -383,13 +382,11 @@ export async function runApp(cliArgs, config) {
|
|
|
383
382
|
mode, // 📖 'opencode' or 'openclaw' — controls Enter action
|
|
384
383
|
tierFilterMode: 0, // 📖 Index into TIER_CYCLE (0=All, 1=S+, 2=S, ...)
|
|
385
384
|
originFilterMode: 0, // 📖 Index into ORIGIN_CYCLE (0=All, then providers)
|
|
386
|
-
premiumMode: cliArgs.premiumMode, // 📖 Special elite-only mode: S/S+ only, Health UP only, Perfect/Normal/Slow verdict only.
|
|
387
385
|
hideUnconfiguredModels: config.settings?.hideUnconfiguredModels === true, // 📖 Hide providers with no configured API key when true.
|
|
388
|
-
disableWidthsWarning: config.settings?.disableWidthsWarning ?? false, // 📖 Cached for runtime checks; keep it in sync with config.settings.
|
|
389
386
|
scrollOffset: 0, // 📖 First visible model index in viewport
|
|
390
387
|
terminalRows: process.stdout.rows || 24, // 📖 Current terminal height
|
|
391
388
|
terminalCols: process.stdout.columns || 80, // 📖 Current terminal width
|
|
392
|
-
widthWarningStartedAt: (process.stdout.columns || 80) <
|
|
389
|
+
widthWarningStartedAt: (process.stdout.columns || 80) < WIDTH_WARNING_MIN_COLS ? now : null, // 📖 Start immediately in very narrow viewports.
|
|
393
390
|
widthWarningDismissed: false, // 📖 Esc hides the narrow-terminal warning early for the current narrow-width session.
|
|
394
391
|
widthWarningShowCount: 0, // 📖 Counter for how many times the narrow-terminal warning has been shown (max 2 per session).
|
|
395
392
|
// 📖 Settings screen state (P key opens it)
|
|
@@ -465,12 +462,10 @@ export async function runApp(cliArgs, config) {
|
|
|
465
462
|
// 📖 Re-clamp viewport on terminal resize
|
|
466
463
|
process.stdout.on('resize', () => {
|
|
467
464
|
const prevCols = state.terminalCols
|
|
468
|
-
const widthsWarningDisabled = state.config.settings?.disableWidthsWarning === true
|
|
469
465
|
state.terminalRows = process.stdout.rows || 24
|
|
470
466
|
state.terminalCols = process.stdout.columns || 80
|
|
471
|
-
state.
|
|
472
|
-
|
|
473
|
-
if (prevCols >= 166 || state.widthWarningDismissed) {
|
|
467
|
+
if (state.terminalCols < WIDTH_WARNING_MIN_COLS) {
|
|
468
|
+
if (prevCols >= WIDTH_WARNING_MIN_COLS || state.widthWarningDismissed) {
|
|
474
469
|
state.widthWarningStartedAt = Date.now()
|
|
475
470
|
state.widthWarningDismissed = false
|
|
476
471
|
state.widthWarningShowCount++ // 📖 Increment counter when showing the warning again
|
|
@@ -630,15 +625,10 @@ export async function runApp(cliArgs, config) {
|
|
|
630
625
|
outputResults = outputResults.filter(r => ['S+', 'S', 'A+'].includes(r.tier))
|
|
631
626
|
}
|
|
632
627
|
|
|
633
|
-
// 📖 Apply premium mode
|
|
628
|
+
// 📖 Apply premium mode as a preselected tier family in JSON mode as well.
|
|
634
629
|
if (cliArgs.premiumMode) {
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
const isHealthUp = r.status === 'up'
|
|
638
|
-
const verdict = getVerdict(r)
|
|
639
|
-
const isGoodVerdict = ['Perfect', 'Normal', 'Slow'].includes(verdict)
|
|
640
|
-
return isEliteTier && isHealthUp && isGoodVerdict
|
|
641
|
-
})
|
|
630
|
+
const premiumTiers = TIER_LETTER_MAP.S || ['S+', 'S']
|
|
631
|
+
outputResults = outputResults.filter(r => premiumTiers.includes(r.tier))
|
|
642
632
|
}
|
|
643
633
|
|
|
644
634
|
// 📖 Sort by avg ping (ascending)
|
|
@@ -702,17 +692,6 @@ export async function runApp(cliArgs, config) {
|
|
|
702
692
|
const originHide = activeOrigin !== null && r.providerKey !== activeOrigin
|
|
703
693
|
r.hidden = tierHide || originHide
|
|
704
694
|
|
|
705
|
-
// 📖 Premium Mode: elite-only constraints (Health UP, Good Verdict, S/S+ only)
|
|
706
|
-
if (state.premiumMode && !r.hidden) {
|
|
707
|
-
const isEliteTier = r.tier === 'S' || r.tier === 'S+'
|
|
708
|
-
const isHealthUp = r.status === 'up'
|
|
709
|
-
const verdict = getVerdict(r)
|
|
710
|
-
const isGoodVerdict = ['Perfect', 'Normal', 'Slow'].includes(verdict)
|
|
711
|
-
|
|
712
|
-
if (!isEliteTier || !isHealthUp || !isGoodVerdict) {
|
|
713
|
-
r.hidden = true
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
695
|
})
|
|
717
696
|
return state.results
|
|
718
697
|
}
|
|
@@ -900,8 +879,7 @@ export async function runApp(cliArgs, config) {
|
|
|
900
879
|
state.settingsUpdateLatestVersion,
|
|
901
880
|
false,
|
|
902
881
|
state.startupLatestVersion,
|
|
903
|
-
state.versionAlertsEnabled
|
|
904
|
-
state.config.settings?.disableWidthsWarning ?? false
|
|
882
|
+
state.versionAlertsEnabled
|
|
905
883
|
)
|
|
906
884
|
}
|
|
907
885
|
tableContent = state.commandPaletteFrozenTable
|
|
@@ -933,8 +911,7 @@ export async function runApp(cliArgs, config) {
|
|
|
933
911
|
state.settingsUpdateLatestVersion,
|
|
934
912
|
false,
|
|
935
913
|
state.startupLatestVersion,
|
|
936
|
-
state.versionAlertsEnabled
|
|
937
|
-
state.config.settings?.disableWidthsWarning ?? false
|
|
914
|
+
state.versionAlertsEnabled
|
|
938
915
|
)
|
|
939
916
|
}
|
|
940
917
|
|
|
@@ -972,7 +949,7 @@ export async function runApp(cliArgs, config) {
|
|
|
972
949
|
const initialVisible = state.results.filter(r => !r.hidden)
|
|
973
950
|
state.visibleSorted = sortResultsWithPinnedFavorites(initialVisible, state.sortColumn, state.sortDirection)
|
|
974
951
|
|
|
975
|
-
process.stdout.write(ALT_HOME + renderTable(state.results, state.pendingPings, state.frame, state.cursor, state.sortColumn, state.sortDirection, state.pingInterval, state.lastPingTime, state.mode, state.tierFilterMode, state.scrollOffset, state.terminalRows, state.terminalCols, state.originFilterMode, null, state.pingMode, state.pingModeSource, state.hideUnconfiguredModels, state.widthWarningStartedAt, state.widthWarningDismissed, state.widthWarningShowCount, state.settingsUpdateState, state.settingsUpdateLatestVersion, false, state.startupLatestVersion, state.versionAlertsEnabled
|
|
952
|
+
process.stdout.write(ALT_HOME + renderTable(state.results, state.pendingPings, state.frame, state.cursor, state.sortColumn, state.sortDirection, state.pingInterval, state.lastPingTime, state.mode, state.tierFilterMode, state.scrollOffset, state.terminalRows, state.terminalCols, state.originFilterMode, null, state.pingMode, state.pingModeSource, state.hideUnconfiguredModels, state.widthWarningStartedAt, state.widthWarningDismissed, state.widthWarningShowCount, state.settingsUpdateState, state.settingsUpdateLatestVersion, false, state.startupLatestVersion, state.versionAlertsEnabled))
|
|
976
953
|
if (process.stdout.isTTY) {
|
|
977
954
|
process.stdout.flush && process.stdout.flush()
|
|
978
955
|
}
|
package/src/cli-help.js
CHANGED
|
@@ -26,14 +26,13 @@ const ANALYSIS_FLAGS = [
|
|
|
26
26
|
{ flag: '--json', description: 'Output results as JSON for scripts/automation' },
|
|
27
27
|
{ flag: '--tier <S|A|B|C>', description: 'Filter models by tier family' },
|
|
28
28
|
{ flag: '--recommend', description: 'Open Smart Recommend immediately on startup' },
|
|
29
|
-
{ flag: '--premium', description: '
|
|
29
|
+
{ flag: '--premium', description: 'Start with S-tier filter + verdict sort (you can reset it in-app)' },
|
|
30
30
|
{ flag: '--sort <column>', description: 'Sort by column (rank, tier, origin, model, ping, avg, swe, ctx, condition, verdict, uptime, stability, usage)' },
|
|
31
31
|
{ flag: '--desc | --asc', description: 'Set sort direction (descending or ascending)' },
|
|
32
32
|
{ flag: '--origin <provider>', description: 'Filter models by provider origin' },
|
|
33
33
|
{ flag: '--ping-interval <ms>', description: 'Override ping interval in milliseconds' },
|
|
34
34
|
{ flag: '--hide-unconfigured', description: 'Hide models without configured API keys' },
|
|
35
35
|
{ flag: '--show-unconfigured', description: 'Show all models regardless of API key config' },
|
|
36
|
-
{ flag: '--disable-widths-warning', description: 'Disable terminal width warning' },
|
|
37
36
|
]
|
|
38
37
|
|
|
39
38
|
const CONFIG_FLAGS = [
|
package/src/config.js
CHANGED
|
@@ -209,7 +209,6 @@ function normalizeSettingsSection(settings) {
|
|
|
209
209
|
return {
|
|
210
210
|
...safeSettings,
|
|
211
211
|
hideUnconfiguredModels: typeof safeSettings.hideUnconfiguredModels === 'boolean' ? safeSettings.hideUnconfiguredModels : true,
|
|
212
|
-
disableWidthsWarning: safeSettings.disableWidthsWarning === true,
|
|
213
212
|
theme: ['dark', 'light', 'auto'].includes(safeSettings.theme) ? safeSettings.theme : 'auto',
|
|
214
213
|
}
|
|
215
214
|
}
|
|
@@ -230,7 +229,6 @@ function normalizeProfileSettings(settings) {
|
|
|
230
229
|
return {
|
|
231
230
|
..._emptyProfileSettings(),
|
|
232
231
|
...safeSettings,
|
|
233
|
-
disableWidthsWarning: safeSettings.disableWidthsWarning === true,
|
|
234
232
|
theme: ['dark', 'light', 'auto'].includes(safeSettings.theme) ? safeSettings.theme : 'auto',
|
|
235
233
|
}
|
|
236
234
|
}
|
|
@@ -843,7 +841,6 @@ export function _emptyProfileSettings() {
|
|
|
843
841
|
pingInterval: 10000, // 📖 default ms between pings in the steady "normal" mode
|
|
844
842
|
hideUnconfiguredModels: true, // 📖 true = default to providers that are actually configured
|
|
845
843
|
preferredToolMode: 'opencode', // 📖 remember the last Z-selected launcher across app restarts
|
|
846
|
-
disableWidthsWarning: false, // 📖 Disable widths warning (default off)
|
|
847
844
|
theme: 'auto', // 📖 'auto' follows the terminal/OS theme, override with 'dark' or 'light' if needed
|
|
848
845
|
}
|
|
849
846
|
}
|
package/src/constants.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* - `CELL_W` is derived from `COL_MS` and used by `msCell` / `spinCell`.
|
|
18
18
|
* - `TABLE_HEADER_LINES` + `TABLE_FOOTER_LINES` = `TABLE_FIXED_LINES` must stay in sync
|
|
19
19
|
* with the actual number of lines rendered by `renderTable()` in bin/.
|
|
20
|
+
* - `WIDTH_WARNING_MIN_COLS` controls when the narrow-terminal startup warning appears.
|
|
20
21
|
* - Overlay background colours (chalk.bgRgb) make each overlay panel visually distinct.
|
|
21
22
|
*
|
|
22
23
|
* @functions
|
|
@@ -30,6 +31,7 @@
|
|
|
30
31
|
* FRAMES, TIER_CYCLE,
|
|
31
32
|
* SETTINGS_OVERLAY_BG, HELP_OVERLAY_BG, RECOMMEND_OVERLAY_BG, LOG_OVERLAY_BG,
|
|
32
33
|
* OVERLAY_PANEL_WIDTH,
|
|
34
|
+
* WIDTH_WARNING_MIN_COLS,
|
|
33
35
|
* TABLE_HEADER_LINES, TABLE_FOOTER_LINES, TABLE_FIXED_LINES,
|
|
34
36
|
* msCell, spinCell
|
|
35
37
|
*
|
|
@@ -83,6 +85,9 @@ export const LOG_OVERLAY_BG = chalk.bgRgb(0, 0, 0) // 📖 Dark blue-gree
|
|
|
83
85
|
// 📖 tint fills the panel consistently regardless of content length.
|
|
84
86
|
export const OVERLAY_PANEL_WIDTH = 116
|
|
85
87
|
|
|
88
|
+
// 📖 Narrow-terminal warning appears only below this width.
|
|
89
|
+
export const WIDTH_WARNING_MIN_COLS = 80
|
|
90
|
+
|
|
86
91
|
// 📖 Table row-budget constants — must stay in sync with renderTable()'s actual output.
|
|
87
92
|
// 📖 If this drifts, model rows overflow and can push the title row out of view.
|
|
88
93
|
export const TABLE_HEADER_LINES = 4 // 📖 title, spacer, column headers, separator
|
package/src/key-handler.js
CHANGED
|
@@ -32,6 +32,7 @@ import { loadConfig, replaceConfigContents } from './config.js'
|
|
|
32
32
|
import { cleanupLegacyProxyArtifacts } from './legacy-proxy-cleanup.js'
|
|
33
33
|
import { cycleThemeSetting, detectActiveTheme } from './theme.js'
|
|
34
34
|
import { buildCommandPaletteEntries, filterCommandPaletteEntries } from './command-palette.js'
|
|
35
|
+
import { WIDTH_WARNING_MIN_COLS } from './constants.js'
|
|
35
36
|
|
|
36
37
|
// 📖 Some providers need an explicit probe model because the first catalog entry
|
|
37
38
|
// 📖 is not guaranteed to be accepted by their chat endpoint.
|
|
@@ -447,38 +448,6 @@ export function createKeyHandler(ctx) {
|
|
|
447
448
|
}
|
|
448
449
|
}
|
|
449
450
|
|
|
450
|
-
// 📖 Keep the width-warning runtime state synced with the persisted Settings toggle
|
|
451
|
-
// 📖 so the overlay reacts immediately when the user enables or disables it.
|
|
452
|
-
function syncWidthsWarningState() {
|
|
453
|
-
const widthsWarningDisabled = state.config.settings?.disableWidthsWarning === true
|
|
454
|
-
state.disableWidthsWarning = widthsWarningDisabled
|
|
455
|
-
|
|
456
|
-
if (widthsWarningDisabled) {
|
|
457
|
-
state.widthWarningStartedAt = null
|
|
458
|
-
state.widthWarningDismissed = false
|
|
459
|
-
return
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
state.widthWarningShowCount = 0
|
|
463
|
-
if ((state.terminalCols || 80) < 166) {
|
|
464
|
-
state.widthWarningStartedAt = Date.now()
|
|
465
|
-
state.widthWarningDismissed = false
|
|
466
|
-
return
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
state.widthWarningStartedAt = null
|
|
470
|
-
state.widthWarningDismissed = false
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
// 📖 Toggle the width-warning setting and apply the effect immediately instead
|
|
474
|
-
// 📖 of waiting for a resize or restart.
|
|
475
|
-
function toggleWidthsWarningSetting() {
|
|
476
|
-
if (!state.config.settings) state.config.settings = {}
|
|
477
|
-
state.config.settings.disableWidthsWarning = !state.config.settings.disableWidthsWarning
|
|
478
|
-
syncWidthsWarningState()
|
|
479
|
-
saveConfig(state.config)
|
|
480
|
-
}
|
|
481
|
-
|
|
482
451
|
// 📖 Theme switches need to update both persisted preference and the live
|
|
483
452
|
// 📖 semantic palette immediately so every screen redraw adopts the new colors.
|
|
484
453
|
function applyThemeSetting(nextTheme) {
|
|
@@ -1384,8 +1353,7 @@ export function createKeyHandler(ctx) {
|
|
|
1384
1353
|
if (state.settingsOpen) {
|
|
1385
1354
|
const providerKeys = Object.keys(sources)
|
|
1386
1355
|
const updateRowIdx = providerKeys.length
|
|
1387
|
-
const
|
|
1388
|
-
const themeRowIdx = widthWarningRowIdx + 1
|
|
1356
|
+
const themeRowIdx = updateRowIdx + 1
|
|
1389
1357
|
const cleanupLegacyProxyRowIdx = themeRowIdx + 1
|
|
1390
1358
|
const changelogViewRowIdx = cleanupLegacyProxyRowIdx + 1
|
|
1391
1359
|
// 📖 Profile system removed - API keys now persist permanently across all sessions
|
|
@@ -1530,12 +1498,6 @@ export function createKeyHandler(ctx) {
|
|
|
1530
1498
|
return
|
|
1531
1499
|
}
|
|
1532
1500
|
|
|
1533
|
-
// 📖 Widths Warning toggle (Enter to toggle)
|
|
1534
|
-
if (state.settingsCursor === widthWarningRowIdx) {
|
|
1535
|
-
toggleWidthsWarningSetting()
|
|
1536
|
-
return
|
|
1537
|
-
}
|
|
1538
|
-
|
|
1539
1501
|
if (state.settingsCursor === themeRowIdx) {
|
|
1540
1502
|
cycleGlobalTheme()
|
|
1541
1503
|
return
|
|
@@ -1579,11 +1541,6 @@ export function createKeyHandler(ctx) {
|
|
|
1579
1541
|
cycleGlobalTheme()
|
|
1580
1542
|
return
|
|
1581
1543
|
}
|
|
1582
|
-
// 📖 Widths Warning toggle (disable/enable)
|
|
1583
|
-
if (state.settingsCursor === widthWarningRowIdx) {
|
|
1584
|
-
toggleWidthsWarningSetting()
|
|
1585
|
-
return
|
|
1586
|
-
}
|
|
1587
1544
|
// 📖 Profile system removed - API keys now persist permanently across all sessions
|
|
1588
1545
|
|
|
1589
1546
|
// 📖 Toggle enabled/disabled for selected provider
|
|
@@ -1598,7 +1555,6 @@ export function createKeyHandler(ctx) {
|
|
|
1598
1555
|
if (key.name === 't') {
|
|
1599
1556
|
if (
|
|
1600
1557
|
state.settingsCursor === updateRowIdx
|
|
1601
|
-
|| state.settingsCursor === widthWarningRowIdx
|
|
1602
1558
|
|| state.settingsCursor === themeRowIdx
|
|
1603
1559
|
|| state.settingsCursor === cleanupLegacyProxyRowIdx
|
|
1604
1560
|
|| state.settingsCursor === changelogViewRowIdx
|
|
@@ -1805,7 +1761,7 @@ export function createKeyHandler(ctx) {
|
|
|
1805
1761
|
}
|
|
1806
1762
|
|
|
1807
1763
|
// 📖 Esc can dismiss the narrow-terminal warning immediately without quitting the app.
|
|
1808
|
-
if (key.name === 'escape' && state.terminalCols > 0 && state.terminalCols <
|
|
1764
|
+
if (key.name === 'escape' && state.terminalCols > 0 && state.terminalCols < WIDTH_WARNING_MIN_COLS) {
|
|
1809
1765
|
state.widthWarningDismissed = true
|
|
1810
1766
|
return
|
|
1811
1767
|
}
|
package/src/overlays.js
CHANGED
|
@@ -93,8 +93,7 @@ export function createOverlayRenderers(state, deps) {
|
|
|
93
93
|
function renderSettings() {
|
|
94
94
|
const providerKeys = Object.keys(sources)
|
|
95
95
|
const updateRowIdx = providerKeys.length
|
|
96
|
-
const
|
|
97
|
-
const themeRowIdx = widthWarningRowIdx + 1
|
|
96
|
+
const themeRowIdx = updateRowIdx + 1
|
|
98
97
|
const cleanupLegacyProxyRowIdx = themeRowIdx + 1
|
|
99
98
|
const changelogViewRowIdx = cleanupLegacyProxyRowIdx + 1
|
|
100
99
|
const EL = '\x1b[K'
|
|
@@ -220,14 +219,6 @@ export function createOverlayRenderers(state, deps) {
|
|
|
220
219
|
const updateRow = `${bullet(updateCursor)}${themeColors.textBold(updateActionLabel).padEnd(44)} ${updateStatus}`
|
|
221
220
|
cursorLineByRow[updateRowIdx] = lines.length
|
|
222
221
|
lines.push(updateCursor ? themeColors.bgCursor(updateRow) : updateRow)
|
|
223
|
-
// 📖 Width warning visibility row for the startup narrow-terminal overlay.
|
|
224
|
-
const disableWidthsWarning = Boolean(state.config.settings?.disableWidthsWarning)
|
|
225
|
-
const widthWarningStatus = disableWidthsWarning
|
|
226
|
-
? themeColors.errorBold('🙈 Disabled')
|
|
227
|
-
: themeColors.successBold('👁 Enabled')
|
|
228
|
-
const widthWarningRow = `${bullet(state.settingsCursor === widthWarningRowIdx)}${themeColors.textBold('Small Width Warnings').padEnd(44)} ${widthWarningStatus}`
|
|
229
|
-
cursorLineByRow[widthWarningRowIdx] = lines.length
|
|
230
|
-
lines.push(state.settingsCursor === widthWarningRowIdx ? themeColors.bgCursor(widthWarningRow) : widthWarningRow)
|
|
231
222
|
const themeStatus = getThemeStatusLabel(activeThemeSetting())
|
|
232
223
|
const themeStatusColor = themeStatus.includes('Dark') ? themeColors.warningBold : themeColors.info
|
|
233
224
|
const themeRow = `${bullet(state.settingsCursor === themeRowIdx)}${themeColors.textBold('Global Theme').padEnd(44)} ${themeStatusColor(themeStatus)}`
|
package/src/render-table.js
CHANGED
|
@@ -41,13 +41,13 @@ import {
|
|
|
41
41
|
msCell,
|
|
42
42
|
spinCell,
|
|
43
43
|
PING_INTERVAL,
|
|
44
|
+
WIDTH_WARNING_MIN_COLS,
|
|
44
45
|
FRAMES
|
|
45
46
|
} from './constants.js'
|
|
46
47
|
import { themeColors, getProviderRgb, getTierRgb, getReadableTextRgb, getTheme } from './theme.js'
|
|
47
48
|
import { TIER_COLOR } from './tier-colors.js'
|
|
48
49
|
import { getAvg, getVerdict, getUptime, getStabilityScore, getVersionStatusInfo } from './utils.js'
|
|
49
50
|
import { usagePlaceholderForProvider } from './ping.js'
|
|
50
|
-
import { formatTokenTotalCompact } from './token-usage-reader.js'
|
|
51
51
|
import { calculateViewport, sortResultsWithPinnedFavorites, padEndDisplay, displayWidth } from './render-helpers.js'
|
|
52
52
|
import { getToolMeta } from './tool-metadata.js'
|
|
53
53
|
import { PROXY_DISABLED_NOTICE } from './product-flags.js'
|
|
@@ -67,7 +67,7 @@ export const PROVIDER_COLOR = new Proxy({}, {
|
|
|
67
67
|
})
|
|
68
68
|
|
|
69
69
|
// ─── renderTable: mode param controls footer hint text (opencode vs openclaw) ─────────
|
|
70
|
-
export function renderTable(results, pendingPings, frame, cursor = null, sortColumn = 'avg', sortDirection = 'asc', pingInterval = PING_INTERVAL, lastPingTime = Date.now(), mode = 'opencode', tierFilterMode = 0, scrollOffset = 0, terminalRows = 0, terminalCols = 0, originFilterMode = 0, legacyStatus = null, pingMode = 'normal', pingModeSource = 'auto', hideUnconfiguredModels = false, widthWarningStartedAt = null, widthWarningDismissed = false, widthWarningShowCount = 0, settingsUpdateState = 'idle', settingsUpdateLatestVersion = null, legacyFlag = false, startupLatestVersion = null, versionAlertsEnabled = true
|
|
70
|
+
export function renderTable(results, pendingPings, frame, cursor = null, sortColumn = 'avg', sortDirection = 'asc', pingInterval = PING_INTERVAL, lastPingTime = Date.now(), mode = 'opencode', tierFilterMode = 0, scrollOffset = 0, terminalRows = 0, terminalCols = 0, originFilterMode = 0, legacyStatus = null, pingMode = 'normal', pingModeSource = 'auto', hideUnconfiguredModels = false, widthWarningStartedAt = null, widthWarningDismissed = false, widthWarningShowCount = 0, settingsUpdateState = 'idle', settingsUpdateLatestVersion = null, legacyFlag = false, startupLatestVersion = null, versionAlertsEnabled = true) {
|
|
71
71
|
// 📖 Filter out hidden models for display
|
|
72
72
|
const visibleResults = results.filter(r => !r.hidden)
|
|
73
73
|
|
|
@@ -146,25 +146,68 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
146
146
|
|
|
147
147
|
// 📖 Column widths (generous spacing with margins)
|
|
148
148
|
const COL_SEP = getColumnSpacing()
|
|
149
|
+
const SEP_W = 3 // ' │ ' display width
|
|
150
|
+
const ROW_MARGIN = 2 // left margin ' '
|
|
149
151
|
const W_RANK = 6
|
|
150
152
|
const W_TIER = 6
|
|
151
153
|
const W_CTX = 6
|
|
152
154
|
const W_SOURCE = 14
|
|
153
155
|
const W_MODEL = 26
|
|
154
|
-
const W_SWE =
|
|
155
|
-
const W_PING = 14
|
|
156
|
-
const W_AVG = 11
|
|
156
|
+
const W_SWE = 6
|
|
157
157
|
const W_STATUS = 18
|
|
158
158
|
const W_VERDICT = 14
|
|
159
|
-
const W_STAB = 11
|
|
160
159
|
const W_UPTIME = 6
|
|
161
|
-
const W_TOKENS = 7
|
|
160
|
+
// const W_TOKENS = 7 // Used column removed
|
|
162
161
|
// const W_USAGE = 7 // Usage column removed
|
|
163
|
-
const MIN_TABLE_WIDTH =
|
|
162
|
+
const MIN_TABLE_WIDTH = WIDTH_WARNING_MIN_COLS
|
|
163
|
+
|
|
164
|
+
// 📖 Responsive column visibility: progressively hide least-useful columns
|
|
165
|
+
// 📖 and shorten header labels when terminal width is insufficient.
|
|
166
|
+
// 📖 Hiding order (least useful first): Rank → Up% → Tier → Stability
|
|
167
|
+
// 📖 Compact mode shrinks: Latest Ping→Lat. P (10), Avg Ping→Avg. P (8),
|
|
168
|
+
// 📖 Stability→StaB. (8), Provider→4chars+… (10), Health→6chars+… (13)
|
|
169
|
+
let wPing = 14
|
|
170
|
+
let wAvg = 11
|
|
171
|
+
let wStab = 11
|
|
172
|
+
let wSource = W_SOURCE
|
|
173
|
+
let wStatus = W_STATUS
|
|
174
|
+
let showRank = true
|
|
175
|
+
let showUptime = true
|
|
176
|
+
let showTier = true
|
|
177
|
+
let showStability = true
|
|
178
|
+
let isCompact = false
|
|
179
|
+
|
|
180
|
+
if (terminalCols > 0) {
|
|
181
|
+
// 📖 Dynamically compute needed row width from visible columns
|
|
182
|
+
const calcWidth = () => {
|
|
183
|
+
const cols = []
|
|
184
|
+
if (showRank) cols.push(W_RANK)
|
|
185
|
+
if (showTier) cols.push(W_TIER)
|
|
186
|
+
cols.push(W_SWE, W_CTX, W_MODEL, wSource, wPing, wAvg, wStatus, W_VERDICT)
|
|
187
|
+
if (showStability) cols.push(wStab)
|
|
188
|
+
if (showUptime) cols.push(W_UPTIME)
|
|
189
|
+
return ROW_MARGIN + cols.reduce((a, b) => a + b, 0) + (cols.length - 1) * SEP_W
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// 📖 Step 1: Compact mode — shorten labels and reduce column widths
|
|
193
|
+
if (calcWidth() > terminalCols) {
|
|
194
|
+
isCompact = true
|
|
195
|
+
wPing = 10 // 'Lat. P' instead of 'Latest Ping'
|
|
196
|
+
wAvg = 8 // 'Avg. P' instead of 'Avg Ping'
|
|
197
|
+
wStab = 8 // 'StaB.' instead of 'Stability'
|
|
198
|
+
wSource = 10 // Provider truncated to 4 chars + '…'
|
|
199
|
+
wStatus = 13 // Health truncated after 6 chars + '…'
|
|
200
|
+
}
|
|
201
|
+
// 📖 Steps 2–5: Progressive column hiding (least useful first)
|
|
202
|
+
if (calcWidth() > terminalCols) showRank = false
|
|
203
|
+
if (calcWidth() > terminalCols) showUptime = false
|
|
204
|
+
if (calcWidth() > terminalCols) showTier = false
|
|
205
|
+
if (calcWidth() > terminalCols) showStability = false
|
|
206
|
+
}
|
|
164
207
|
const warningDurationMs = 2_000
|
|
165
208
|
const elapsed = widthWarningStartedAt ? Math.max(0, Date.now() - widthWarningStartedAt) : warningDurationMs
|
|
166
209
|
const remainingMs = Math.max(0, warningDurationMs - elapsed)
|
|
167
|
-
const showWidthWarning = terminalCols > 0 && terminalCols < MIN_TABLE_WIDTH && !
|
|
210
|
+
const showWidthWarning = terminalCols > 0 && terminalCols < MIN_TABLE_WIDTH && !widthWarningDismissed && widthWarningShowCount < 2 && remainingMs > 0
|
|
168
211
|
|
|
169
212
|
if (showWidthWarning) {
|
|
170
213
|
const lines = []
|
|
@@ -217,13 +260,16 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
217
260
|
const modelH = 'Model'
|
|
218
261
|
const sweH = sortColumn === 'swe' ? dir + ' SWE%' : 'SWE%'
|
|
219
262
|
const ctxH = sortColumn === 'ctx' ? dir + ' CTX' : 'CTX'
|
|
220
|
-
|
|
221
|
-
const
|
|
263
|
+
// 📖 Compact labels: 'Lat. P' / 'Avg. P' / 'StaB.' to save horizontal space
|
|
264
|
+
const pingLabel = isCompact ? 'Lat. P' : 'Latest Ping'
|
|
265
|
+
const avgLabel = isCompact ? 'Avg. P' : 'Avg Ping'
|
|
266
|
+
const stabLabel = isCompact ? 'StaB.' : 'Stability'
|
|
267
|
+
const pingH = sortColumn === 'ping' ? dir + ' ' + pingLabel : pingLabel
|
|
268
|
+
const avgH = sortColumn === 'avg' ? dir + ' ' + avgLabel : avgLabel
|
|
222
269
|
const healthH = sortColumn === 'condition' ? dir + ' Health' : 'Health'
|
|
223
270
|
const verdictH = sortColumn === 'verdict' ? dir + ' Verdict' : 'Verdict'
|
|
224
|
-
const stabH = sortColumn === 'stability' ? dir + '
|
|
271
|
+
const stabH = sortColumn === 'stability' ? dir + ' ' + stabLabel : stabLabel
|
|
225
272
|
const uptimeH = sortColumn === 'uptime' ? dir + ' Up%' : 'Up%'
|
|
226
|
-
const tokensH = 'Used'
|
|
227
273
|
|
|
228
274
|
// 📖 Helper to colorize first letter for keyboard shortcuts
|
|
229
275
|
// 📖 IMPORTANT: Pad PLAIN TEXT first, then apply colors to avoid alignment issues
|
|
@@ -238,27 +284,31 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
238
284
|
// 📖 Now colorize after padding is calculated on plain text
|
|
239
285
|
const rankH_c = colorFirst(rankH, W_RANK)
|
|
240
286
|
const tierH_c = colorFirst('Tier', W_TIER)
|
|
241
|
-
const originLabel = 'Provider'
|
|
287
|
+
const originLabel = isCompact ? 'PrOD…' : 'Provider'
|
|
242
288
|
const originH_c = sortColumn === 'origin'
|
|
243
|
-
? themeColors.accentBold(originLabel.padEnd(
|
|
244
|
-
: (originFilterMode > 0 ? themeColors.accentBold(originLabel.padEnd(
|
|
289
|
+
? themeColors.accentBold(originLabel.padEnd(wSource))
|
|
290
|
+
: (originFilterMode > 0 ? themeColors.accentBold(originLabel.padEnd(wSource)) : (() => {
|
|
245
291
|
// 📖 Provider keeps O for sorting and D for provider-filter cycling.
|
|
246
|
-
|
|
247
|
-
const
|
|
292
|
+
// 📖 In compact mode, shorten to 'PrOD…' (4 chars + ellipsis) to save space.
|
|
293
|
+
const plain = isCompact ? 'PrOD…' : 'PrOviDer'
|
|
294
|
+
const padding = ' '.repeat(Math.max(0, wSource - plain.length))
|
|
295
|
+
if (isCompact) {
|
|
296
|
+
return themeColors.dim('Pr') + themeColors.hotkey('O') + themeColors.hotkey('D') + themeColors.dim('…' + padding)
|
|
297
|
+
}
|
|
248
298
|
return themeColors.dim('Pr') + themeColors.hotkey('O') + themeColors.dim('vi') + themeColors.hotkey('D') + themeColors.dim('er' + padding)
|
|
249
299
|
})())
|
|
250
300
|
const modelH_c = colorFirst(modelH, W_MODEL)
|
|
251
301
|
const sweH_c = sortColumn === 'swe' ? themeColors.accentBold(sweH.padEnd(W_SWE)) : colorFirst(sweH, W_SWE)
|
|
252
302
|
const ctxH_c = sortColumn === 'ctx' ? themeColors.accentBold(ctxH.padEnd(W_CTX)) : colorFirst(ctxH, W_CTX)
|
|
253
|
-
const pingH_c = sortColumn === 'ping' ? themeColors.accentBold(pingH.padEnd(
|
|
254
|
-
const avgH_c = sortColumn === 'avg' ? themeColors.accentBold(avgH.padEnd(
|
|
255
|
-
const healthH_c = sortColumn === 'condition' ? themeColors.accentBold(healthH.padEnd(
|
|
303
|
+
const pingH_c = sortColumn === 'ping' ? themeColors.accentBold(pingH.padEnd(wPing)) : colorFirst(pingLabel, wPing)
|
|
304
|
+
const avgH_c = sortColumn === 'avg' ? themeColors.accentBold(avgH.padEnd(wAvg)) : colorFirst(avgLabel, wAvg)
|
|
305
|
+
const healthH_c = sortColumn === 'condition' ? themeColors.accentBold(healthH.padEnd(wStatus)) : colorFirst('Health', wStatus)
|
|
256
306
|
const verdictH_c = sortColumn === 'verdict' ? themeColors.accentBold(verdictH.padEnd(W_VERDICT)) : colorFirst(verdictH, W_VERDICT)
|
|
257
307
|
// 📖 Custom colorization for Stability: highlight 'B' (the sort key) since 'S' is taken by SWE
|
|
258
|
-
const stabH_c = sortColumn === 'stability' ? themeColors.accentBold(stabH.padEnd(
|
|
259
|
-
const plain =
|
|
260
|
-
const padding = ' '.repeat(Math.max(0,
|
|
261
|
-
return themeColors.dim('Sta') + themeColors.hotkey('B') + themeColors.dim('ility' + padding)
|
|
308
|
+
const stabH_c = sortColumn === 'stability' ? themeColors.accentBold(stabH.padEnd(wStab)) : (() => {
|
|
309
|
+
const plain = stabLabel
|
|
310
|
+
const padding = ' '.repeat(Math.max(0, wStab - plain.length))
|
|
311
|
+
return themeColors.dim('Sta') + themeColors.hotkey('B') + themeColors.dim((isCompact ? '.' : 'ility') + padding)
|
|
262
312
|
})()
|
|
263
313
|
// 📖 Up% sorts on U, so keep the highlighted shortcut in the shared yellow sort-key color.
|
|
264
314
|
const uptimeH_c = sortColumn === 'uptime' ? themeColors.accentBold(uptimeH.padEnd(W_UPTIME)) : (() => {
|
|
@@ -266,10 +316,15 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
266
316
|
const padding = ' '.repeat(Math.max(0, W_UPTIME - plain.length))
|
|
267
317
|
return themeColors.hotkey('U') + themeColors.dim('p%' + padding)
|
|
268
318
|
})()
|
|
269
|
-
const tokensH_c = themeColors.dim(tokensH.padEnd(W_TOKENS))
|
|
270
319
|
// 📖 Usage column removed from UI – no header or separator for it.
|
|
271
|
-
// Header
|
|
272
|
-
|
|
320
|
+
// 📖 Header row: conditionally include columns based on responsive visibility
|
|
321
|
+
const headerParts = []
|
|
322
|
+
if (showRank) headerParts.push(rankH_c)
|
|
323
|
+
if (showTier) headerParts.push(tierH_c)
|
|
324
|
+
headerParts.push(sweH_c, ctxH_c, modelH_c, originH_c, pingH_c, avgH_c, healthH_c, verdictH_c)
|
|
325
|
+
if (showStability) headerParts.push(stabH_c)
|
|
326
|
+
if (showUptime) headerParts.push(uptimeH_c)
|
|
327
|
+
lines.push(' ' + headerParts.join(COL_SEP))
|
|
273
328
|
|
|
274
329
|
|
|
275
330
|
|
|
@@ -311,9 +366,13 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
311
366
|
const num = themeColors.dim(String(r.idx).padEnd(W_RANK))
|
|
312
367
|
const tier = tierFn(r.tier.padEnd(W_TIER))
|
|
313
368
|
// 📖 Keep terminal view provider-specific so each row is monitorable per provider
|
|
369
|
+
// 📖 In compact mode, truncate provider name to 4 chars + '…'
|
|
314
370
|
const providerNameRaw = sources[r.providerKey]?.name ?? r.providerKey ?? 'NIM'
|
|
315
371
|
const providerName = normalizeOriginLabel(providerNameRaw, r.providerKey)
|
|
316
|
-
const
|
|
372
|
+
const providerDisplay = isCompact && providerName.length > 5
|
|
373
|
+
? providerName.slice(0, 4) + '…'
|
|
374
|
+
: providerName
|
|
375
|
+
const source = themeColors.provider(r.providerKey, providerDisplay.padEnd(wSource))
|
|
317
376
|
// 📖 Favorites: always reserve 2 display columns at the start of Model column.
|
|
318
377
|
// 📖 🎯 (2 cols) for recommended, ⭐ (2 cols) for favorites, ' ' (2 spaces) for non-favorites — keeps alignment stable.
|
|
319
378
|
const favoritePrefix = r.isRecommended ? '🎯' : r.isFavorite ? '⭐' : ' '
|
|
@@ -345,7 +404,7 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
345
404
|
// 📖 Keep the row-local spinner small and inline so users can still read the last measured latency.
|
|
346
405
|
const buildLatestPingDisplay = (value) => {
|
|
347
406
|
const spinner = r.isPinging ? ` ${FRAMES[frame % FRAMES.length]}` : ''
|
|
348
|
-
return `${value}${spinner}`.padEnd(
|
|
407
|
+
return `${value}${spinner}`.padEnd(wPing)
|
|
349
408
|
}
|
|
350
409
|
|
|
351
410
|
// 📖 Latest ping - pings are objects: { ms, code }
|
|
@@ -353,7 +412,7 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
353
412
|
const latestPing = r.pings.length > 0 ? r.pings[r.pings.length - 1] : null
|
|
354
413
|
let pingCell
|
|
355
414
|
if (!latestPing) {
|
|
356
|
-
const placeholder = r.isPinging ? buildLatestPingDisplay('———') : '———'.padEnd(
|
|
415
|
+
const placeholder = r.isPinging ? buildLatestPingDisplay('———') : '———'.padEnd(wPing)
|
|
357
416
|
pingCell = themeColors.dim(placeholder)
|
|
358
417
|
} else if (latestPing.code === '200') {
|
|
359
418
|
// 📖 Success - show response time
|
|
@@ -364,7 +423,7 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
364
423
|
pingCell = themeColors.dim(buildLatestPingDisplay(String(latestPing.ms)))
|
|
365
424
|
} else {
|
|
366
425
|
// 📖 Error or timeout - show "———" (error code is already in Status column)
|
|
367
|
-
const placeholder = r.isPinging ? buildLatestPingDisplay('———') : '———'.padEnd(
|
|
426
|
+
const placeholder = r.isPinging ? buildLatestPingDisplay('———') : '———'.padEnd(wPing)
|
|
368
427
|
pingCell = themeColors.dim(placeholder)
|
|
369
428
|
}
|
|
370
429
|
|
|
@@ -372,10 +431,10 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
372
431
|
const avg = getAvg(r)
|
|
373
432
|
let avgCell
|
|
374
433
|
if (avg !== Infinity) {
|
|
375
|
-
const str = String(avg).padEnd(
|
|
434
|
+
const str = String(avg).padEnd(wAvg)
|
|
376
435
|
avgCell = avg < 500 ? themeColors.metricGood(str) : avg < 1500 ? themeColors.metricWarn(str) : themeColors.metricBad(str)
|
|
377
436
|
} else {
|
|
378
|
-
avgCell = themeColors.dim('———'.padEnd(
|
|
437
|
+
avgCell = themeColors.dim('———'.padEnd(wAvg))
|
|
379
438
|
}
|
|
380
439
|
|
|
381
440
|
// 📖 Status column - build plain text with emoji, pad, then colorize
|
|
@@ -423,7 +482,18 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
423
482
|
statusText = '?'
|
|
424
483
|
statusColor = themeColors.dim
|
|
425
484
|
}
|
|
426
|
-
|
|
485
|
+
// 📖 In compact mode, truncate health text after 6 visible chars + '…' to fit wStatus
|
|
486
|
+
const statusDisplayText = isCompact ? (() => {
|
|
487
|
+
// 📖 Strip emoji prefix to measure text length, then truncate if needed
|
|
488
|
+
const plainText = statusText.replace(/^[\p{Emoji_Presentation}\p{Extended_Pictographic}]\s*/u, '')
|
|
489
|
+
if (plainText.length > 6) {
|
|
490
|
+
const emojiMatch = statusText.match(/^([\p{Emoji_Presentation}\p{Extended_Pictographic}]\s*)/u)
|
|
491
|
+
const prefix = emojiMatch ? emojiMatch[1] : ''
|
|
492
|
+
return prefix + plainText.slice(0, 6) + '…'
|
|
493
|
+
}
|
|
494
|
+
return statusText
|
|
495
|
+
})() : statusText
|
|
496
|
+
const status = statusColor(padEndDisplay(statusDisplayText, wStatus))
|
|
427
497
|
|
|
428
498
|
// 📖 Verdict column - use getVerdict() for stability-aware verdicts, then render with emoji
|
|
429
499
|
const verdict = getVerdict(r)
|
|
@@ -479,15 +549,15 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
479
549
|
const stabScore = getStabilityScore(r)
|
|
480
550
|
let stabCell
|
|
481
551
|
if (stabScore < 0) {
|
|
482
|
-
stabCell = themeColors.dim('———'.padEnd(
|
|
552
|
+
stabCell = themeColors.dim('———'.padEnd(wStab))
|
|
483
553
|
} else if (stabScore >= 80) {
|
|
484
|
-
stabCell = themeColors.metricGood(String(stabScore).padEnd(
|
|
554
|
+
stabCell = themeColors.metricGood(String(stabScore).padEnd(wStab))
|
|
485
555
|
} else if (stabScore >= 60) {
|
|
486
|
-
stabCell = themeColors.metricOk(String(stabScore).padEnd(
|
|
556
|
+
stabCell = themeColors.metricOk(String(stabScore).padEnd(wStab))
|
|
487
557
|
} else if (stabScore >= 40) {
|
|
488
|
-
stabCell = themeColors.metricWarn(String(stabScore).padEnd(
|
|
558
|
+
stabCell = themeColors.metricWarn(String(stabScore).padEnd(wStab))
|
|
489
559
|
} else {
|
|
490
|
-
stabCell = themeColors.metricBad(String(stabScore).padEnd(
|
|
560
|
+
stabCell = themeColors.metricBad(String(stabScore).padEnd(wStab))
|
|
491
561
|
}
|
|
492
562
|
|
|
493
563
|
// 📖 Uptime column - percentage of successful pings
|
|
@@ -508,22 +578,21 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
508
578
|
// 📖 Model text now mirrors the provider hue so provider affinity is visible
|
|
509
579
|
// 📖 even before the eye reaches the Provider column.
|
|
510
580
|
const nameCell = themeColors.provider(r.providerKey, name, { bold: isCursor })
|
|
511
|
-
const sourceCursorText =
|
|
581
|
+
const sourceCursorText = providerDisplay.padEnd(wSource)
|
|
512
582
|
const sourceCell = isCursor ? themeColors.provider(r.providerKey, sourceCursorText, { bold: true }) : source
|
|
513
583
|
|
|
514
584
|
// 📖 Usage column removed from UI – no usage data displayed.
|
|
515
585
|
// (We keep the logic but do not render it.)
|
|
516
586
|
const usageCell = ''
|
|
517
587
|
|
|
518
|
-
// 📖
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
const row = ' ' + num + COL_SEP + tier + COL_SEP + sweCell + COL_SEP + ctxCell + COL_SEP + nameCell + COL_SEP + sourceCell + COL_SEP + pingCell + COL_SEP + avgCell + COL_SEP + status + COL_SEP + speedCell + COL_SEP + stabCell + COL_SEP + uptimeCell + COL_SEP + tokensCell
|
|
588
|
+
// 📖 Build row: conditionally include columns based on responsive visibility
|
|
589
|
+
const rowParts = []
|
|
590
|
+
if (showRank) rowParts.push(num)
|
|
591
|
+
if (showTier) rowParts.push(tier)
|
|
592
|
+
rowParts.push(sweCell, ctxCell, nameCell, sourceCell, pingCell, avgCell, status, speedCell)
|
|
593
|
+
if (showStability) rowParts.push(stabCell)
|
|
594
|
+
if (showUptime) rowParts.push(uptimeCell)
|
|
595
|
+
const row = ' ' + rowParts.join(COL_SEP)
|
|
527
596
|
|
|
528
597
|
if (isCursor) {
|
|
529
598
|
lines.push(themeColors.bgModelCursor(row))
|
package/src/testfcm.js
CHANGED
|
@@ -107,7 +107,7 @@ const TRANSCRIPT_FINDING_RULES = [
|
|
|
107
107
|
title: 'PTY width warning blocked the TUI flow',
|
|
108
108
|
severity: 'high',
|
|
109
109
|
regex: /please maximize your terminal|terminal is too small|reduce font size or maximize width/i,
|
|
110
|
-
task: 'Run `/testfcm` with
|
|
110
|
+
task: 'Run `/testfcm` with a wider PTY (at least 80 columns) before sending Enter.',
|
|
111
111
|
},
|
|
112
112
|
{
|
|
113
113
|
id: 'tool_missing',
|
package/src/utils.js
CHANGED
|
@@ -464,7 +464,6 @@ export function parseArgs(argv) {
|
|
|
464
464
|
const sortAscFlag = flags.includes('--asc')
|
|
465
465
|
const hideUnconfigured = flags.includes('--hide-unconfigured')
|
|
466
466
|
const showUnconfigured = flags.includes('--show-unconfigured')
|
|
467
|
-
const disableWidthsWarning = flags.includes('--disable-widths-warning')
|
|
468
467
|
|
|
469
468
|
let tierFilter = tierValueIdx !== -1 ? args[tierValueIdx].toUpperCase() : null
|
|
470
469
|
let sortColumn = sortValueIdx !== -1 ? args[sortValueIdx].toLowerCase() : null
|
|
@@ -501,7 +500,6 @@ export function parseArgs(argv) {
|
|
|
501
500
|
pingInterval,
|
|
502
501
|
hideUnconfigured,
|
|
503
502
|
showUnconfigured,
|
|
504
|
-
disableWidthsWarning,
|
|
505
503
|
premiumMode,
|
|
506
504
|
// 📖 Profile system removed - API keys now persist permanently across all sessions
|
|
507
505
|
recommendMode,
|