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 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
- # "Show me only elite models that are currently healthy"
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.18",
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: show only S‑tier models sorted by verdict
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) < 166 && !(config.settings?.disableWidthsWarning ?? false) ? now : null, // 📖 Start immediately only when warnings are enabled in a narrow viewport.
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.disableWidthsWarning = widthsWarningDisabled
466
- if (state.terminalCols < 166 && !widthsWarningDisabled) {
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 filter if specified: elite-only (S/S+, UP, Good Verdict)
628
+ // 📖 Apply premium mode as a preselected tier family in JSON mode as well.
628
629
  if (cliArgs.premiumMode) {
629
- outputResults = outputResults.filter(r => {
630
- const isEliteTier = r.tier === 'S' || r.tier === 'S+'
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
- : 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, state.config.settings?.disableWidthsWarning ?? false)
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, state.config.settings?.disableWidthsWarning ?? false))
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: 'Show only S/S+ models with perfect health and good verdict' },
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