free-coding-models 0.3.18 → 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 +27 -0
- package/README.md +3 -1
- package/package.json +1 -1
- package/src/app.js +96 -32
- package/src/cli-help.js +1 -2
- package/src/command-palette.js +170 -0
- package/src/config.js +0 -3
- package/src/constants.js +5 -0
- package/src/key-handler.js +318 -143
- package/src/overlays.js +127 -11
- package/src/render-table.js +120 -50
- package/src/testfcm.js +1 -1
- package/src/theme.js +3 -0
- package/src/utils.js +0 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,33 @@
|
|
|
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
|
+
|
|
19
|
+
## 0.3.19
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- **Command palette overlay (`Ctrl+P`)**: Added a searchable floating palette with fuzzy matching so users can quickly run filters, sorts, overlays, and global actions.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
- **Main footer and help discoverability**: surfaced `Ctrl+P` in table hints and Help overlay so the new command launcher is visible immediately.
|
|
26
|
+
- **Command palette spacing polish**: added two-character inner padding around the floating palette so the overlay feels less cramped and visually cleaner.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
- **Command palette visual jitter**: background ping cycles now pause while the command palette is open so table rows stop reshuffling during command search.
|
|
30
|
+
- **Command palette background freeze**: while the palette is open, the full table behind it is now frozen (spinner glyphs, ping countdown, and dynamic row updates) and resumes instantly on close.
|
|
31
|
+
|
|
5
32
|
## 0.3.18
|
|
6
33
|
|
|
7
34
|
### 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"
|
|
@@ -176,6 +176,7 @@ Press **`Z`** in the TUI to cycle between tools without restarting.
|
|
|
176
176
|
| `E` | Toggle configured-only mode |
|
|
177
177
|
| `F` | Favorite / unfavorite model |
|
|
178
178
|
| `G` | Cycle global theme (`Auto → Dark → Light`) |
|
|
179
|
+
| `Ctrl+P` | Open command palette (search + run actions) |
|
|
179
180
|
| `R/S/C/M/O/L/A/H/V/B/U` | Sort columns |
|
|
180
181
|
| `P` | Settings (API keys, providers, updates, theme) |
|
|
181
182
|
| `Y` | Install Endpoints (push provider into tool config) |
|
|
@@ -200,6 +201,7 @@ Press **`Z`** in the TUI to cycle between tools without restarting.
|
|
|
200
201
|
- **Configured-only default** — only shows providers you have keys for
|
|
201
202
|
- **Keyless latency** — models ping even without an API key (show 🔑 NO KEY)
|
|
202
203
|
- **Smart Recommend** — questionnaire picks the best model for your task type
|
|
204
|
+
- **Command Palette** — `Ctrl+P` opens a searchable action launcher for filters, sorting, overlays, and quick toggles
|
|
203
205
|
- **Install Endpoints** — push a full provider catalog into any tool's config (`Y`)
|
|
204
206
|
- **Missing tool bootstrap** — detect absent CLIs, offer one-click install, then continue the selected launch automatically
|
|
205
207
|
- **Width guardrail** — shows a warning instead of a broken table in narrow terminals
|
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)
|
|
@@ -406,6 +403,12 @@ export async function runApp(cliArgs, config) {
|
|
|
406
403
|
settingsUpdateError: null, // 📖 Last update-check error message for maintenance row
|
|
407
404
|
config, // 📖 Live reference to the config object (updated on save)
|
|
408
405
|
visibleSorted: [], // 📖 Cached visible+sorted models — shared between render loop and key handlers
|
|
406
|
+
commandPaletteOpen: false, // 📖 Whether the Ctrl+P command palette overlay is active.
|
|
407
|
+
commandPaletteQuery: '', // 📖 Current command palette search query.
|
|
408
|
+
commandPaletteCursor: 0, // 📖 Selected command index in the filtered command list.
|
|
409
|
+
commandPaletteScrollOffset: 0, // 📖 Vertical scroll offset for the command palette result viewport.
|
|
410
|
+
commandPaletteResults: [], // 📖 Cached fuzzy-filtered command entries for the command palette.
|
|
411
|
+
commandPaletteFrozenTable: null, // 📖 Frozen table snapshot rendered behind the command palette overlay.
|
|
409
412
|
helpVisible: false, // 📖 Whether the help overlay (K key) is active
|
|
410
413
|
settingsScrollOffset: 0, // 📖 Vertical scroll offset for Settings overlay viewport
|
|
411
414
|
helpScrollOffset: 0, // 📖 Vertical scroll offset for Help overlay viewport
|
|
@@ -459,12 +462,10 @@ export async function runApp(cliArgs, config) {
|
|
|
459
462
|
// 📖 Re-clamp viewport on terminal resize
|
|
460
463
|
process.stdout.on('resize', () => {
|
|
461
464
|
const prevCols = state.terminalCols
|
|
462
|
-
const widthsWarningDisabled = state.config.settings?.disableWidthsWarning === true
|
|
463
465
|
state.terminalRows = process.stdout.rows || 24
|
|
464
466
|
state.terminalCols = process.stdout.columns || 80
|
|
465
|
-
state.
|
|
466
|
-
|
|
467
|
-
if (prevCols >= 166 || state.widthWarningDismissed) {
|
|
467
|
+
if (state.terminalCols < WIDTH_WARNING_MIN_COLS) {
|
|
468
|
+
if (prevCols >= WIDTH_WARNING_MIN_COLS || state.widthWarningDismissed) {
|
|
468
469
|
state.widthWarningStartedAt = Date.now()
|
|
469
470
|
state.widthWarningDismissed = false
|
|
470
471
|
state.widthWarningShowCount++ // 📖 Increment counter when showing the warning again
|
|
@@ -624,15 +625,10 @@ export async function runApp(cliArgs, config) {
|
|
|
624
625
|
outputResults = outputResults.filter(r => ['S+', 'S', 'A+'].includes(r.tier))
|
|
625
626
|
}
|
|
626
627
|
|
|
627
|
-
// 📖 Apply premium mode
|
|
628
|
+
// 📖 Apply premium mode as a preselected tier family in JSON mode as well.
|
|
628
629
|
if (cliArgs.premiumMode) {
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
const isHealthUp = r.status === 'up'
|
|
632
|
-
const verdict = getVerdict(r)
|
|
633
|
-
const isGoodVerdict = ['Perfect', 'Normal', 'Slow'].includes(verdict)
|
|
634
|
-
return isEliteTier && isHealthUp && isGoodVerdict
|
|
635
|
-
})
|
|
630
|
+
const premiumTiers = TIER_LETTER_MAP.S || ['S+', 'S']
|
|
631
|
+
outputResults = outputResults.filter(r => premiumTiers.includes(r.tier))
|
|
636
632
|
}
|
|
637
633
|
|
|
638
634
|
// 📖 Sort by avg ping (ascending)
|
|
@@ -696,17 +692,6 @@ export async function runApp(cliArgs, config) {
|
|
|
696
692
|
const originHide = activeOrigin !== null && r.providerKey !== activeOrigin
|
|
697
693
|
r.hidden = tierHide || originHide
|
|
698
694
|
|
|
699
|
-
// 📖 Premium Mode: elite-only constraints (Health UP, Good Verdict, S/S+ only)
|
|
700
|
-
if (state.premiumMode && !r.hidden) {
|
|
701
|
-
const isEliteTier = r.tier === 'S' || r.tier === 'S+'
|
|
702
|
-
const isHealthUp = r.status === 'up'
|
|
703
|
-
const verdict = getVerdict(r)
|
|
704
|
-
const isGoodVerdict = ['Perfect', 'Normal', 'Slow'].includes(verdict)
|
|
705
|
-
|
|
706
|
-
if (!isEliteTier || !isHealthUp || !isGoodVerdict) {
|
|
707
|
-
r.hidden = true
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
695
|
})
|
|
711
696
|
return state.results
|
|
712
697
|
}
|
|
@@ -761,6 +746,7 @@ export async function runApp(cliArgs, config) {
|
|
|
761
746
|
getToolMeta,
|
|
762
747
|
getToolInstallPlan,
|
|
763
748
|
padEndDisplay,
|
|
749
|
+
displayWidth,
|
|
764
750
|
})
|
|
765
751
|
|
|
766
752
|
onKeyPress = createKeyHandler({
|
|
@@ -858,16 +844,85 @@ export async function runApp(cliArgs, config) {
|
|
|
858
844
|
refreshAutoPingMode()
|
|
859
845
|
state.frame++
|
|
860
846
|
// 📖 Cache visible+sorted models each frame so Enter handler always matches the display
|
|
861
|
-
if (!state.settingsOpen && !state.installEndpointsOpen && !state.toolInstallPromptOpen && !state.recommendOpen && !state.feedbackOpen && !state.changelogOpen) {
|
|
847
|
+
if (!state.settingsOpen && !state.installEndpointsOpen && !state.toolInstallPromptOpen && !state.recommendOpen && !state.feedbackOpen && !state.changelogOpen && !state.commandPaletteOpen) {
|
|
862
848
|
const visible = state.results.filter(r => !r.hidden)
|
|
863
849
|
state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection)
|
|
864
850
|
}
|
|
851
|
+
let tableContent = null
|
|
852
|
+
if (state.commandPaletteOpen) {
|
|
853
|
+
if (!state.commandPaletteFrozenTable) {
|
|
854
|
+
// 📖 Freeze the full table (including countdown and spinner glyphs) while
|
|
855
|
+
// 📖 the command palette is open so the background remains perfectly static.
|
|
856
|
+
state.commandPaletteFrozenTable = renderTable(
|
|
857
|
+
state.results,
|
|
858
|
+
state.pendingPings,
|
|
859
|
+
state.frame,
|
|
860
|
+
state.cursor,
|
|
861
|
+
state.sortColumn,
|
|
862
|
+
state.sortDirection,
|
|
863
|
+
state.pingInterval,
|
|
864
|
+
state.lastPingTime,
|
|
865
|
+
state.mode,
|
|
866
|
+
state.tierFilterMode,
|
|
867
|
+
state.scrollOffset,
|
|
868
|
+
state.terminalRows,
|
|
869
|
+
state.terminalCols,
|
|
870
|
+
state.originFilterMode,
|
|
871
|
+
null,
|
|
872
|
+
state.pingMode,
|
|
873
|
+
state.pingModeSource,
|
|
874
|
+
state.hideUnconfiguredModels,
|
|
875
|
+
state.widthWarningStartedAt,
|
|
876
|
+
state.widthWarningDismissed,
|
|
877
|
+
state.widthWarningShowCount,
|
|
878
|
+
state.settingsUpdateState,
|
|
879
|
+
state.settingsUpdateLatestVersion,
|
|
880
|
+
false,
|
|
881
|
+
state.startupLatestVersion,
|
|
882
|
+
state.versionAlertsEnabled
|
|
883
|
+
)
|
|
884
|
+
}
|
|
885
|
+
tableContent = state.commandPaletteFrozenTable
|
|
886
|
+
} else {
|
|
887
|
+
state.commandPaletteFrozenTable = null
|
|
888
|
+
tableContent = renderTable(
|
|
889
|
+
state.results,
|
|
890
|
+
state.pendingPings,
|
|
891
|
+
state.frame,
|
|
892
|
+
state.cursor,
|
|
893
|
+
state.sortColumn,
|
|
894
|
+
state.sortDirection,
|
|
895
|
+
state.pingInterval,
|
|
896
|
+
state.lastPingTime,
|
|
897
|
+
state.mode,
|
|
898
|
+
state.tierFilterMode,
|
|
899
|
+
state.scrollOffset,
|
|
900
|
+
state.terminalRows,
|
|
901
|
+
state.terminalCols,
|
|
902
|
+
state.originFilterMode,
|
|
903
|
+
null,
|
|
904
|
+
state.pingMode,
|
|
905
|
+
state.pingModeSource,
|
|
906
|
+
state.hideUnconfiguredModels,
|
|
907
|
+
state.widthWarningStartedAt,
|
|
908
|
+
state.widthWarningDismissed,
|
|
909
|
+
state.widthWarningShowCount,
|
|
910
|
+
state.settingsUpdateState,
|
|
911
|
+
state.settingsUpdateLatestVersion,
|
|
912
|
+
false,
|
|
913
|
+
state.startupLatestVersion,
|
|
914
|
+
state.versionAlertsEnabled
|
|
915
|
+
)
|
|
916
|
+
}
|
|
917
|
+
|
|
865
918
|
const content = state.settingsOpen
|
|
866
919
|
? overlays.renderSettings()
|
|
867
920
|
: state.installEndpointsOpen
|
|
868
921
|
? overlays.renderInstallEndpoints()
|
|
869
922
|
: state.toolInstallPromptOpen
|
|
870
923
|
? overlays.renderToolInstallPrompt()
|
|
924
|
+
: state.commandPaletteOpen
|
|
925
|
+
? tableContent + overlays.renderCommandPalette()
|
|
871
926
|
: state.recommendOpen
|
|
872
927
|
? overlays.renderRecommend()
|
|
873
928
|
: state.feedbackOpen
|
|
@@ -876,7 +931,7 @@ export async function runApp(cliArgs, config) {
|
|
|
876
931
|
? overlays.renderHelp()
|
|
877
932
|
: state.changelogOpen
|
|
878
933
|
? overlays.renderChangelog()
|
|
879
|
-
:
|
|
934
|
+
: tableContent
|
|
880
935
|
process.stdout.write(ALT_HOME + content)
|
|
881
936
|
if (process.stdout.isTTY) {
|
|
882
937
|
process.stdout.flush && process.stdout.flush()
|
|
@@ -894,7 +949,7 @@ export async function runApp(cliArgs, config) {
|
|
|
894
949
|
const initialVisible = state.results.filter(r => !r.hidden)
|
|
895
950
|
state.visibleSorted = sortResultsWithPinnedFavorites(initialVisible, state.sortColumn, state.sortDirection)
|
|
896
951
|
|
|
897
|
-
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))
|
|
898
953
|
if (process.stdout.isTTY) {
|
|
899
954
|
process.stdout.flush && process.stdout.flush()
|
|
900
955
|
}
|
|
@@ -920,6 +975,15 @@ export async function runApp(cliArgs, config) {
|
|
|
920
975
|
const runPingCycle = async () => {
|
|
921
976
|
try {
|
|
922
977
|
refreshAutoPingMode()
|
|
978
|
+
|
|
979
|
+
// 📖 Command palette intentionally pauses background ping bursts to avoid
|
|
980
|
+
// 📖 visible row jitter while users type and navigate commands.
|
|
981
|
+
if (state.commandPaletteOpen) {
|
|
982
|
+
state.lastPingTime = Date.now()
|
|
983
|
+
scheduleNextPing()
|
|
984
|
+
return
|
|
985
|
+
}
|
|
986
|
+
|
|
923
987
|
state.lastPingTime = Date.now()
|
|
924
988
|
|
|
925
989
|
// 📖 Refresh persisted usage snapshots each cycle so background usage data appears live in table.
|
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 = [
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file command-palette.js
|
|
3
|
+
* @description Command palette registry and fuzzy search helpers for the main TUI.
|
|
4
|
+
*
|
|
5
|
+
* @functions
|
|
6
|
+
* → `buildCommandPaletteEntries` — builds the current command list with dynamic provider/tier context
|
|
7
|
+
* → `fuzzyMatchCommand` — scores a query against one string and returns match positions
|
|
8
|
+
* → `filterCommandPaletteEntries` — returns sorted command matches for a query
|
|
9
|
+
*
|
|
10
|
+
* @exports { COMMAND_CATEGORY_ORDER, buildCommandPaletteEntries, fuzzyMatchCommand, filterCommandPaletteEntries }
|
|
11
|
+
*
|
|
12
|
+
* @see src/key-handler.js
|
|
13
|
+
* @see src/overlays.js
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export const COMMAND_CATEGORY_ORDER = ['Filters', 'Sort', 'Pages', 'Actions']
|
|
17
|
+
|
|
18
|
+
const COMMANDS = [
|
|
19
|
+
// 📖 Filters
|
|
20
|
+
{ id: 'filter-tier-all', category: 'Filters', label: 'Filter tiers: all', shortcut: 'T', keywords: ['filter', 'tier', 'all'] },
|
|
21
|
+
{ id: 'filter-tier-splus', category: 'Filters', label: 'Filter tiers: S+', shortcut: null, keywords: ['filter', 'tier', 's+'] },
|
|
22
|
+
{ id: 'filter-tier-s', category: 'Filters', label: 'Filter tiers: S', shortcut: null, keywords: ['filter', 'tier', 's'] },
|
|
23
|
+
{ id: 'filter-tier-aplus', category: 'Filters', label: 'Filter tiers: A+', shortcut: null, keywords: ['filter', 'tier', 'a+'] },
|
|
24
|
+
{ id: 'filter-tier-a', category: 'Filters', label: 'Filter tiers: A', shortcut: null, keywords: ['filter', 'tier', 'a'] },
|
|
25
|
+
{ id: 'filter-tier-aminus', category: 'Filters', label: 'Filter tiers: A-', shortcut: null, keywords: ['filter', 'tier', 'a-'] },
|
|
26
|
+
{ id: 'filter-tier-bplus', category: 'Filters', label: 'Filter tiers: B+', shortcut: null, keywords: ['filter', 'tier', 'b+'] },
|
|
27
|
+
{ id: 'filter-tier-b', category: 'Filters', label: 'Filter tiers: B', shortcut: null, keywords: ['filter', 'tier', 'b'] },
|
|
28
|
+
{ id: 'filter-tier-c', category: 'Filters', label: 'Filter tiers: C', shortcut: null, keywords: ['filter', 'tier', 'c'] },
|
|
29
|
+
{ id: 'filter-provider-cycle', category: 'Filters', label: 'Filter provider: cycle', shortcut: 'D', keywords: ['filter', 'provider', 'origin'] },
|
|
30
|
+
{ id: 'filter-configured-toggle', category: 'Filters', label: 'Toggle configured-only models', shortcut: 'E', keywords: ['filter', 'configured', 'keys'] },
|
|
31
|
+
|
|
32
|
+
// 📖 Sorting
|
|
33
|
+
{ id: 'sort-rank', category: 'Sort', label: 'Sort by rank', shortcut: 'R', keywords: ['sort', 'rank'] },
|
|
34
|
+
{ id: 'sort-tier', category: 'Sort', label: 'Sort by tier', shortcut: null, keywords: ['sort', 'tier'] },
|
|
35
|
+
{ id: 'sort-provider', category: 'Sort', label: 'Sort by provider', shortcut: 'O', keywords: ['sort', 'origin', 'provider'] },
|
|
36
|
+
{ id: 'sort-model', category: 'Sort', label: 'Sort by model name', shortcut: 'M', keywords: ['sort', 'model', 'name'] },
|
|
37
|
+
{ id: 'sort-latest-ping', category: 'Sort', label: 'Sort by latest ping', shortcut: 'L', keywords: ['sort', 'latest', 'ping'] },
|
|
38
|
+
{ id: 'sort-avg-ping', category: 'Sort', label: 'Sort by average ping', shortcut: 'A', keywords: ['sort', 'avg', 'average', 'ping'] },
|
|
39
|
+
{ id: 'sort-swe', category: 'Sort', label: 'Sort by SWE score', shortcut: 'S', keywords: ['sort', 'swe', 'score'] },
|
|
40
|
+
{ id: 'sort-ctx', category: 'Sort', label: 'Sort by context window', shortcut: 'C', keywords: ['sort', 'context', 'ctx'] },
|
|
41
|
+
{ id: 'sort-health', category: 'Sort', label: 'Sort by health', shortcut: 'H', keywords: ['sort', 'health', 'condition'] },
|
|
42
|
+
{ id: 'sort-verdict', category: 'Sort', label: 'Sort by verdict', shortcut: 'V', keywords: ['sort', 'verdict'] },
|
|
43
|
+
{ id: 'sort-stability', category: 'Sort', label: 'Sort by stability', shortcut: 'B', keywords: ['sort', 'stability'] },
|
|
44
|
+
{ id: 'sort-uptime', category: 'Sort', label: 'Sort by uptime', shortcut: 'U', keywords: ['sort', 'uptime'] },
|
|
45
|
+
|
|
46
|
+
// 📖 Pages / overlays
|
|
47
|
+
{ id: 'open-settings', category: 'Pages', label: 'Open settings', shortcut: 'P', keywords: ['settings', 'config', 'api key'] },
|
|
48
|
+
{ id: 'open-help', category: 'Pages', label: 'Open help', shortcut: 'K', keywords: ['help', 'shortcuts', 'hotkeys'] },
|
|
49
|
+
{ id: 'open-changelog', category: 'Pages', label: 'Open changelog', shortcut: 'N', keywords: ['changelog', 'release'] },
|
|
50
|
+
{ id: 'open-feedback', category: 'Pages', label: 'Open feedback', shortcut: 'I', keywords: ['feedback', 'bug', 'request'] },
|
|
51
|
+
{ id: 'open-recommend', category: 'Pages', label: 'Open smart recommend', shortcut: 'Q', keywords: ['recommend', 'best model'] },
|
|
52
|
+
{ id: 'open-install-endpoints', category: 'Pages', label: 'Open install endpoints', shortcut: 'Y', keywords: ['install', 'endpoints', 'providers'] },
|
|
53
|
+
|
|
54
|
+
// 📖 Actions
|
|
55
|
+
{ id: 'action-cycle-theme', category: 'Actions', label: 'Cycle theme', shortcut: 'G', keywords: ['theme', 'dark', 'light', 'auto'] },
|
|
56
|
+
{ id: 'action-cycle-tool-mode', category: 'Actions', label: 'Cycle tool mode', shortcut: 'Z', keywords: ['tool', 'mode', 'launcher'] },
|
|
57
|
+
{ id: 'action-cycle-ping-mode', category: 'Actions', label: 'Cycle ping mode', shortcut: 'W', keywords: ['ping', 'cadence', 'speed', 'slow'] },
|
|
58
|
+
{ id: 'action-toggle-favorite', category: 'Actions', label: 'Toggle favorite on selected model', shortcut: 'F', keywords: ['favorite', 'star'] },
|
|
59
|
+
{ id: 'action-reset-view', category: 'Actions', label: 'Reset view settings', shortcut: 'Shift+R', keywords: ['reset', 'view', 'sort', 'filters'] },
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
const ID_TO_TIER = {
|
|
63
|
+
'filter-tier-all': null,
|
|
64
|
+
'filter-tier-splus': 'S+',
|
|
65
|
+
'filter-tier-s': 'S',
|
|
66
|
+
'filter-tier-aplus': 'A+',
|
|
67
|
+
'filter-tier-a': 'A',
|
|
68
|
+
'filter-tier-aminus': 'A-',
|
|
69
|
+
'filter-tier-bplus': 'B+',
|
|
70
|
+
'filter-tier-b': 'B',
|
|
71
|
+
'filter-tier-c': 'C',
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function buildCommandPaletteEntries() {
|
|
75
|
+
return COMMANDS.map((entry) => ({
|
|
76
|
+
...entry,
|
|
77
|
+
tierValue: Object.prototype.hasOwnProperty.call(ID_TO_TIER, entry.id) ? ID_TO_TIER[entry.id] : undefined,
|
|
78
|
+
}))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 📖 Fuzzy matching optimized for short command labels and keyboard aliases.
|
|
83
|
+
* @param {string} query
|
|
84
|
+
* @param {string} text
|
|
85
|
+
* @returns {{ matched: boolean, score: number, positions: number[] }}
|
|
86
|
+
*/
|
|
87
|
+
export function fuzzyMatchCommand(query, text) {
|
|
88
|
+
const q = (query || '').trim().toLowerCase()
|
|
89
|
+
const t = (text || '').toLowerCase()
|
|
90
|
+
|
|
91
|
+
if (!q) return { matched: true, score: 0, positions: [] }
|
|
92
|
+
if (!t) return { matched: false, score: 0, positions: [] }
|
|
93
|
+
|
|
94
|
+
let qIdx = 0
|
|
95
|
+
const positions = []
|
|
96
|
+
for (let i = 0; i < t.length && qIdx < q.length; i++) {
|
|
97
|
+
if (q[qIdx] === t[i]) {
|
|
98
|
+
positions.push(i)
|
|
99
|
+
qIdx++
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (qIdx !== q.length) return { matched: false, score: 0, positions: [] }
|
|
104
|
+
|
|
105
|
+
let score = q.length * 10
|
|
106
|
+
|
|
107
|
+
// 📖 Bonus when matches are contiguous.
|
|
108
|
+
for (let i = 1; i < positions.length; i++) {
|
|
109
|
+
if (positions[i] === positions[i - 1] + 1) score += 5
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 📖 Bonus for word boundaries and prefix matches.
|
|
113
|
+
for (const pos of positions) {
|
|
114
|
+
if (pos === 0) score += 8
|
|
115
|
+
else {
|
|
116
|
+
const prev = t[pos - 1]
|
|
117
|
+
if (prev === ' ' || prev === ':' || prev === '-' || prev === '/') score += 6
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 📖 Small penalty for very long labels so focused labels float up.
|
|
122
|
+
score -= Math.max(0, t.length - q.length)
|
|
123
|
+
|
|
124
|
+
return { matched: true, score, positions }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* 📖 Filter and rank command palette entries by fuzzy score.
|
|
129
|
+
* @param {Array<{ id: string, label: string, category: string, keywords?: string[] }>} entries
|
|
130
|
+
* @param {string} query
|
|
131
|
+
* @returns {Array<{ id: string, label: string, category: string, shortcut?: string|null, keywords?: string[], score: number, matchPositions: number[] }>}
|
|
132
|
+
*/
|
|
133
|
+
export function filterCommandPaletteEntries(entries, query) {
|
|
134
|
+
const normalizedQuery = (query || '').trim()
|
|
135
|
+
|
|
136
|
+
const ranked = []
|
|
137
|
+
for (const entry of entries) {
|
|
138
|
+
const labelMatch = fuzzyMatchCommand(normalizedQuery, entry.label)
|
|
139
|
+
let bestScore = labelMatch.score
|
|
140
|
+
let matchPositions = labelMatch.positions
|
|
141
|
+
let matched = labelMatch.matched
|
|
142
|
+
|
|
143
|
+
if (!matched && Array.isArray(entry.keywords)) {
|
|
144
|
+
for (const keyword of entry.keywords) {
|
|
145
|
+
const keywordMatch = fuzzyMatchCommand(normalizedQuery, keyword)
|
|
146
|
+
if (!keywordMatch.matched) continue
|
|
147
|
+
matched = true
|
|
148
|
+
// 📖 Keyword matches should rank below direct label matches.
|
|
149
|
+
const keywordScore = Math.max(1, keywordMatch.score - 7)
|
|
150
|
+
if (keywordScore > bestScore) {
|
|
151
|
+
bestScore = keywordScore
|
|
152
|
+
matchPositions = []
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!matched) continue
|
|
158
|
+
ranked.push({ ...entry, score: bestScore, matchPositions })
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
ranked.sort((a, b) => {
|
|
162
|
+
if (b.score !== a.score) return b.score - a.score
|
|
163
|
+
const aCat = COMMAND_CATEGORY_ORDER.indexOf(a.category)
|
|
164
|
+
const bCat = COMMAND_CATEGORY_ORDER.indexOf(b.category)
|
|
165
|
+
if (aCat !== bCat) return aCat - bCat
|
|
166
|
+
return a.label.localeCompare(b.label)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
return ranked
|
|
170
|
+
}
|
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
|