free-coding-models 0.3.56 → 0.3.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,4 +1,4 @@
1
- ## [0.3.56] - 2026-05-04
1
+ ## [0.3.57] - 2026-05-04
2
2
 
3
3
  ### Added
4
4
 
@@ -27,9 +27,17 @@
27
27
  - **Provider filters now derive from the catalog** — Command Palette provider filters are generated from `sources`, so new providers do not need a hardcoded UI update.
28
28
  - **Router model set expanded** — Default router candidate selection now supports up to 8 models and uses refreshed high-ranking defaults.
29
29
  - **External tool configs generalized** — OpenCode and Kilo can now auto-configure newly added OpenAI-compatible providers through shared provider metadata.
30
+ - **Footer collapse removed** — The main TUI table now always keeps the full footer visible, matching the sticky header behavior and avoiding hidden navigation hints.
31
+ - **Main table header simplified** — Removed the extra `Search / Tier / Provider / Verdict / Health` filter row from the primary TUI table. Active tier/provider state still appears in the title row, and the column header now sits directly under the app/version row.
32
+ - **Router UI hidden from the main flow** — Removed the visible Smart Router upgrade banner, footer daemon status, help entry, command palette entry, and README TUI shortcut documentation. The router implementation stays available, but the main table no longer advertises it.
33
+ - **Footer now sticks to the terminal bottom** — The footer is padded into the bottom rows even when only a few models are visible, while the header remains fixed at the top.
34
+ - **Release docs narrowed for router** — README now documents router CLI commands without promoting the in-TUI dashboard path to normal users.
30
35
 
31
36
  ### Fixed
32
37
 
38
+ - **Sticky TUI header visibility** — Fixed the main table line budget so the app/version row, column headers, model rows, and footer are all reserved before rows are rendered. This prevents the alternate screen from scrolling the header out of view.
39
+ - **Shift+R router shortcut restored** — Re-enabled `Shift+R` as an unadvertised tester shortcut after it had been temporarily disabled.
40
+ - **Footer line budgeting hardened** — Optional update, custom-filter, and release-date footer rows are now included in viewport calculations so temporary footer rows cannot push the table header off-screen.
33
41
  - **Router upstream hardening** — Fixed unsupported request parameter stripping, retryable failover behavior, content-type canonicalization, and long-stream timeout handling.
34
42
  - **Router auth/quota semantics** — Router now returns 401/429 when all candidates fail because of auth or quota instead of masking those cases as 503.
35
43
  - **OpenRouter sync-set filtering** — Router discovery no longer drops `openrouter/free` and `openrouter/owl-alpha` just because their IDs do not end with `:free`.
package/README.md CHANGED
@@ -2,17 +2,21 @@
2
2
  <img src="https://img.shields.io/npm/v/free-coding-models?color=76b900&label=npm&logo=npm" alt="npm version">
3
3
  <img src="https://img.shields.io/node/v/free-coding-models?color=76b900&logo=node.js" alt="node version">
4
4
  <img src="https://img.shields.io/npm/l/free-coding-models?color=76b900" alt="license">
5
- <img src="https://img.shields.io/badge/models-180-76b900?logo=nvidia" alt="models count">
5
+ <img src="https://img.shields.io/badge/models-170+-76b900?logo=nvidia" alt="models count">
6
6
  <img src="https://img.shields.io/badge/providers-16-blue" alt="providers count">
7
7
  </p>
8
8
 
9
+ <p align="center">
10
+ <img src="logo.webp" alt="free-coding-models logo" width="128">
11
+ </p>
12
+
9
13
  <h1 align="center">free-coding-models</h1>
10
14
 
11
15
 
12
16
 
13
17
  <p align="center">
14
18
  <strong>Find the fastest free coding model in seconds</strong><br>
15
- <sub>Track 180 models across 16 vetted free or free-limited AI providers in real time</sub><br> <sub> Install Free API endpoints to your favorite AI coding tool: <br>📦 OpenCode, 📦 OpenCode Desktop, 📦 OpenCode WebUI, 🦞 OpenClaw, 💘 Crush, 🪿 Goose, 🛠 Aider, ⚡️ Kilo CLI, 🐉 Qwen Code, 🤲 OpenHands, ⚡ Amp, 🔮 Hermes, ▶️ Continue, 🧠 Cline, 🛠️ Xcode, π Pi, 🦘 Rovo or ♊ Gemini in one keystroke</sub>
19
+ <sub>Track ~170 models across ~15 vetted free or free-limited AI providers in real time</sub><br> <sub> Install Free API endpoints to your favorite AI coding tool: <br>📦 OpenCode, 📦 OpenCode Desktop, 📦 OpenCode WebUI, 🦞 OpenClaw, 💘 Crush, 🪿 Goose, 🛠 Aider, ⚡️ Kilo CLI, 🐉 Qwen Code, 🤲 OpenHands, ⚡ Amp, 🔮 Hermes, ▶️ Continue, 🧠 Cline, 🛠️ Xcode, π Pi, 🦘 Rovo or ♊ Gemini in one keystroke</sub>
16
20
  </p>
17
21
 
18
22
 
@@ -58,7 +62,7 @@ create a free account on one of the [providers](#-list-of-free-ai-providers)
58
62
 
59
63
  ## 💡 Why this tool?
60
64
 
61
- There are **180 cataloged free or free-limited coding models** across 16 vetted providers. Which one is fastest right now? Which one is actually stable versus just lucky on the last ping?
65
+ There are **~170 cataloged free or free-limited coding models** across ~15 vetted providers. Which one is fastest right now? Which one is actually stable versus just lucky on the last ping?
62
66
 
63
67
  This CLI pings them all in parallel, shows live latency, and calculates a **live Stability Score (0-100)**. Average latency alone is misleading if a model randomly spikes to 6 seconds; the stability score measures true reliability by combining **p95 latency** (30%), **jitter/variance** (30%), **spike rate** (20%), and **uptime** (20%).
64
68
 
@@ -72,7 +76,7 @@ It then writes the model you pick directly into your coding tool's config — so
72
76
 
73
77
  Create a free account on one provider below to get started:
74
78
 
75
- **180 coding models** across 16 active providers, ranked by practical free-tier usefulness.
79
+ **~170 coding models** across ~15 active providers, ranked by practical free-tier usefulness.
76
80
 
77
81
  | # | Provider | Models | Tier range | Free tier | Env var |
78
82
  |---|----------|--------|-----------|-----------|--------|
@@ -163,10 +167,6 @@ free-coding-models --web
163
167
  free-coding-models --daemon-bg
164
168
  free-coding-models --daemon-status
165
169
 
166
- # "I want to inspect the router without leaving the TUI"
167
- free-coding-models
168
- # then press Shift+R
169
-
170
170
  # "Start with an elite-focused preset, then adjust filters live"
171
171
  free-coding-models --premium
172
172
 
@@ -198,27 +198,6 @@ free-coding-models --sync-set
198
198
  free-coding-models --sync-set my-coding-set
199
199
  ```
200
200
 
201
- Inside the TUI, press **`Shift+R`** to open the Router Dashboard. It polls `/health` and `/stats`, listens to `/stream/events`, and shows daemon state, active set, probe mode, circuit breaker health, token totals, and the live routed request log.
202
-
203
- Dashboard keys:
204
-
205
- | Key | Action |
206
- |-----|--------|
207
- | `S` | Switch to the next router set |
208
- | `I` | Cycle probe mode (`eco → balanced → aggressive`) |
209
- | `C` | Clear the local dashboard request log |
210
- | `R` | Reserved for Phase 7 service-manager restart |
211
- | `P` | Reserved until probe pause/resume backend support exists |
212
- | `Esc` | Return to the main model table |
213
-
214
- Press **`Shift+S`** to open the Set Manager — create, rename, duplicate, delete model sets, and reorder models within each set with `Shift+↑` / `Shift+↓`.
215
-
216
- Press **`Shift+T`** to open the Token Usage screen — shows today/all-time token totals and a 7-day history chart with top models per day.
217
-
218
- When you first start the TUI with no router configured, an onboarding prompt appears asking if you want to enable the Smart Router. Existing users who haven't yet opted in see a dismissable upgrade banner at the top of the table.
219
-
220
- **`Shift+A`** opens the position picker — navigate where a model lands in the priority order within a set.
221
-
222
201
  Configure tools with:
223
202
 
224
203
  | Field | Value |
@@ -346,7 +325,6 @@ When a tool mode is active (via `Z`), models incompatible with that tool are hig
346
325
  | `G` | Cycle global theme (`Auto → Dark → Light`) |
347
326
  | `Ctrl+P` | Open ⚡️ command palette (search + run actions) |
348
327
  | `R/S/C/M/O/L/A/H/V/B/U` | Sort columns |
349
- | `Shift+R` | Router Dashboard (daemon health, circuits, tokens, request log) |
350
328
  | `Shift+U` | Update to latest version (when update available) |
351
329
  | `P` | Settings (API keys, providers, updates, theme) |
352
330
  | `Q` | Smart Recommend overlay |
@@ -379,7 +357,7 @@ When a tool mode is active (via `Z`), models incompatible with that tool are hig
379
357
 
380
358
  ## ✨ Features
381
359
 
382
- - **Parallel pings** — all 174 API/Zen-callable models tested simultaneously via native `fetch` (180 total cataloged models including CLI-only Gemini rows)
360
+ - **Parallel pings** — all ~165 API/Zen-callable models tested simultaneously via native `fetch` (~170 total cataloged models including CLI-only Gemini rows)
383
361
  - **Adaptive monitoring** — 2s burst for 60s → 10s normal → 30s idle
384
362
  - **Stability score** — composite 0–100 (p95 latency, jitter, spike rate, uptime)
385
363
  - **Smart ranking** — top 3 highlighted 🥇🥈🥉
@@ -418,7 +396,7 @@ We welcome contributions — issues, PRs, new provider integrations.
418
396
 
419
397
  ## ⚖️ Model Licensing & Commercial Use
420
398
 
421
- **Short answer:** The 180 cataloged models are API/CLI-served models where generated-output ownership is generally granted by the provider/model terms. Always verify current provider terms for high-stakes commercial use.
399
+ **Short answer:** The ~170 cataloged models are API/CLI-served models where generated-output ownership is generally granted by the provider/model terms. Always verify current provider terms for high-stakes commercial use.
422
400
 
423
401
  ### Output Ownership
424
402
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "free-coding-models",
3
- "version": "0.3.56",
3
+ "version": "0.3.57",
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/sources.js CHANGED
@@ -358,7 +358,7 @@ export const opencodeZen = [
358
358
  // 📖 See README for full tier-by-tier comparison
359
359
  export const sources = {
360
360
  nvidia: {
361
- name: 'NIM',
361
+ name: 'NVIDIA NIM',
362
362
  url: 'https://integrate.api.nvidia.com/v1/chat/completions',
363
363
  models: nvidiaNim,
364
364
  },
@@ -383,7 +383,7 @@ export const sources = {
383
383
  models: githubModels,
384
384
  },
385
385
  mistral: {
386
- name: 'Mistral La Plateforme',
386
+ name: 'Mistral LP',
387
387
  url: 'https://api.mistral.ai/v1/chat/completions',
388
388
  models: mistral,
389
389
  },
package/src/app.js CHANGED
@@ -110,7 +110,7 @@ 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'
112
112
  import { PROVIDER_METADATA, ENV_VAR_NAMES, isWindows, isMac } from '../src/provider-metadata.js'
113
- import { parseTelemetryEnv, isTelemetryDebugEnabled, telemetryDebug, ensureTelemetryConfig, getTelemetryDistinctId, getTelemetrySystem, getTelemetryTerminal, isTelemetryEnabled, sendUsageTelemetry, sendBugReport } from '../src/telemetry.js'
113
+ import { parseTelemetryEnv, isTelemetryDebugEnabled, telemetryDebug, ensureTelemetryConfig, getTelemetryDistinctId, getTelemetrySystem, getTelemetryTerminal, isTelemetryEnabled, sendUsageTelemetry } from '../src/telemetry.js'
114
114
  import { ensureFavoritesConfig, toFavoriteKey, syncFavoriteFlags, toggleFavoriteModel, reorderFavorite } from '../src/favorites.js'
115
115
  import { checkForUpdateDetailed, checkForUpdate, runUpdate, promptUpdateNotification, fetchLastReleaseDate } from './updater.js'
116
116
  import { promptApiKey } from '../src/setup.js'
@@ -229,21 +229,21 @@ export async function runApp(cliArgs, config) {
229
229
 
230
230
  // 📖 Shell env migration popup for existing users who haven't been asked yet
231
231
  // 📖 Only show when user has keys but shellEnvEnabled is still undefined (never prompted)
232
- if (hasAnyKey && config.settings.shellEnvEnabled === undefined) {
232
+ // 📖 shellEnvPromptSeen flag ensures it only shows ONCE even after adding new keys
233
+ if (hasAnyKey && config.settings.shellEnvEnabled === undefined && config.settings.shellEnvPromptSeen !== true) {
233
234
  const choice = await promptShellEnvMigration(config)
235
+ if (!config.settings) config.settings = {}
236
+ config.settings.shellEnvPromptSeen = true
234
237
  if (choice === 'enable') {
235
- if (!config.settings) config.settings = {}
236
238
  config.settings.shellEnvEnabled = true
237
239
  saveConfig(config)
238
240
  syncShellEnv(config)
239
241
  ensureShellRcSource()
240
242
  } else if (choice === 'never') {
241
- if (!config.settings) config.settings = {}
242
243
  config.settings.shellEnvEnabled = false
243
244
  saveConfig(config)
244
245
  }
245
246
  if (choice === 'skip') {
246
- if (!config.settings) config.settings = {}
247
247
  config.settings.shellEnvEnabled = false
248
248
  saveConfig(config)
249
249
  }
@@ -435,7 +435,6 @@ export async function runApp(cliArgs, config) {
435
435
  healthFilterMode: 0, // 📖 Index into HEALTH_CYCLE (0=All, then health states)
436
436
  hideUnconfiguredModels: config.settings?.hideUnconfiguredModels === true, // 📖 Hide providers with no configured API key when true.
437
437
  favoritesPinnedAndSticky: config.settings?.favoritesPinnedAndSticky === true, // 📖 false by default: favorites follow normal sort/filter rules until Y enables pinned+sticky mode.
438
- footerHidden: config.settings?.footerHidden === true, // 📖 true = footer is collapsed to a single toggle hint
439
438
  scrollOffset: 0, // 📖 First visible model index in viewport
440
439
  terminalRows: process.stdout.rows || 24, // 📖 Current terminal height
441
440
  terminalCols: process.stdout.columns || 80, // 📖 Current terminal width
@@ -508,11 +507,7 @@ export async function runApp(cliArgs, config) {
508
507
  recommendAnalysisTimer: null, // 📖 setInterval handle for the 10s analysis phase
509
508
  recommendPingTimer: null, // 📖 setInterval handle for 2 pings/sec during analysis
510
509
  recommendedKeys: new Set(), // 📖 Set of "providerKey/modelId" for recommended models (shown in main table)
511
- // 📖 Feedback state (J/I keys open it)
512
- feedbackOpen: false, // 📖 Whether the feedback overlay is active
513
- bugReportBuffer: '', // 📖 Typed characters for the feedback message
514
- bugReportStatus: 'idle', // 📖 'idle'|'sending'|'success'|'error' — webhook send status
515
- bugReportError: null, // 📖 Last webhook error message
510
+
516
511
  // 📖 OpenCode sync status (S key in settings)
517
512
  settingsSyncStatus: null, // 📖 { type: 'success'|'error', msg: string } — shown in settings footer
518
513
  // 📖 Changelog overlay state (N key opens it)
@@ -548,9 +543,6 @@ export async function runApp(cliArgs, config) {
548
543
  routerDashboardNotice: null,
549
544
  routerDashboardNoticeTimer: null,
550
545
  routerOnboardingScrollOffset: 0,
551
- // 📖 Router upgrade banner (shown once to existing users who haven't seen router)
552
- routerUpgradeBannerShownAt: 0, // 📖 Timestamp when banner was shown (0 = not shown)
553
- routerUpgradeBannerDismissedAt: 0, // 📖 Timestamp when banner was dismissed (0 = not dismissed)
554
546
  routerDashboardEverOpened: false, // 📖 Set to true the first time dashboard opens (used for upgrade-path telemetry)
555
547
  // 📖 Custom text filter (Ctrl+P palette → type text → Enter). Ephemeral — not saved to config.
556
548
  customTextFilter: null, // 📖 Active free-text filter string (null = off). Matches model name, ctx, provider key/name.
@@ -920,7 +912,6 @@ export async function runApp(cliArgs, config) {
920
912
  sendUsageTelemetry,
921
913
  startRecommendAnalysis: overlays.startRecommendAnalysis,
922
914
  stopRecommendAnalysis: overlays.stopRecommendAnalysis,
923
- sendBugReport,
924
915
  stopUi,
925
916
  ping,
926
917
  TASK_TYPES,
@@ -1024,7 +1015,7 @@ export async function runApp(cliArgs, config) {
1024
1015
  process.stdout.write(ALT_LEAVE);
1025
1016
  console.error(chalk.red('\n[TUI Error] An error occurred while handling a keypress.'));
1026
1017
  console.error(err);
1027
- console.error(chalk.yellow('\nPlease file an issue at https://github.com/vava-nessa/free-coding-models/issues or use the feedback form (I key) to report this to the author.'));
1018
+ console.error(chalk.yellow('\nPlease file an issue at https://github.com/vava-nessa/free-coding-models/issues or join the Discord to report this to the author.'));
1028
1019
  process.exit(1);
1029
1020
  }
1030
1021
  })
@@ -1048,12 +1039,14 @@ export async function runApp(cliArgs, config) {
1048
1039
  refreshAutoPingMode()
1049
1040
  state.frame++
1050
1041
  // 📖 Cache visible+sorted models each frame so Enter handler always matches the display
1051
- if (!state.settingsOpen && !state.installEndpointsOpen && !state.toolInstallPromptOpen && !state.incompatibleFallbackOpen && !state.recommendOpen && !state.feedbackOpen && !state.changelogOpen && !state.installedModelsOpen && !state.routerDashboardOpen && !state.commandPaletteOpen) {
1042
+ if (!state.settingsOpen && !state.installEndpointsOpen && !state.toolInstallPromptOpen && !state.incompatibleFallbackOpen && !state.recommendOpen && !state.changelogOpen && !state.installedModelsOpen && !state.routerDashboardOpen && !state.commandPaletteOpen) {
1052
1043
  const visible = state.results.filter(r => !r.hidden)
1053
1044
  state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection, {
1054
1045
  pinFavorites: state.favoritesPinnedAndSticky,
1055
1046
  })
1056
1047
  }
1048
+ const tableTerminalRows = state.terminalRows
1049
+
1057
1050
  let tableContent = null
1058
1051
  if (state.commandPaletteOpen) {
1059
1052
  if (!state.commandPaletteFrozenTable) {
@@ -1071,7 +1064,7 @@ export async function runApp(cliArgs, config) {
1071
1064
  state.mode,
1072
1065
  state.tierFilterMode,
1073
1066
  state.scrollOffset,
1074
- state.terminalRows,
1067
+ tableTerminalRows,
1075
1068
  state.terminalCols,
1076
1069
  state.originFilterMode,
1077
1070
  null,
@@ -1089,14 +1082,9 @@ export async function runApp(cliArgs, config) {
1089
1082
  state.favoritesPinnedAndSticky,
1090
1083
  state.customTextFilter,
1091
1084
  state.lastReleaseDate,
1092
- state.footerHidden,
1085
+ false,
1093
1086
  state.verdictFilterMode,
1094
- state.healthFilterMode,
1095
- state.routerFooterRunning,
1096
- state.routerFooterActiveSet,
1097
- state.routerFooterTodayTokens,
1098
- state.routerFooterAllTimeTokens,
1099
- state.routerFooterRequests
1087
+ state.healthFilterMode
1100
1088
  )
1101
1089
  }
1102
1090
  tableContent = state.commandPaletteFrozenTable
@@ -1114,7 +1102,7 @@ export async function runApp(cliArgs, config) {
1114
1102
  state.mode,
1115
1103
  state.tierFilterMode,
1116
1104
  state.scrollOffset,
1117
- state.terminalRows,
1105
+ tableTerminalRows,
1118
1106
  state.terminalCols,
1119
1107
  state.originFilterMode,
1120
1108
  null,
@@ -1132,23 +1120,12 @@ export async function runApp(cliArgs, config) {
1132
1120
  state.favoritesPinnedAndSticky,
1133
1121
  state.customTextFilter,
1134
1122
  state.lastReleaseDate,
1135
- state.footerHidden,
1123
+ false,
1136
1124
  state.verdictFilterMode,
1137
- state.healthFilterMode,
1138
- state.routerFooterRunning,
1139
- state.routerFooterActiveSet,
1140
- state.routerFooterTodayTokens,
1141
- state.routerFooterAllTimeTokens,
1142
- state.routerFooterRequests
1125
+ state.healthFilterMode
1143
1126
  )
1144
1127
  }
1145
1128
 
1146
- // 📖 Router upgrade banner: inline notification for existing users not yet seen router
1147
- if (!state.routerOnboardingOpen && !state.settingsOpen && !state.installEndpointsOpen && !state.toolInstallPromptOpen && !state.installedModelsOpen && !state.routerDashboardOpen && !state.tokenUsageOpen && !state.commandPaletteOpen && !state.recommendOpen && !state.feedbackOpen && !state.helpVisible && !state.changelogOpen && !state.incompatibleFallbackOpen) {
1148
- const banner = overlays.renderRouterUpgradeBanner()
1149
- if (banner) tableContent = banner + '\n' + tableContent
1150
- }
1151
-
1152
1129
  const content = state.settingsOpen
1153
1130
  ? overlays.renderSettings()
1154
1131
  : state.installEndpointsOpen
@@ -1169,9 +1146,7 @@ export async function runApp(cliArgs, config) {
1169
1146
  ? tableContent + overlays.renderCommandPalette()
1170
1147
  : state.recommendOpen
1171
1148
  ? overlays.renderRecommend()
1172
- : state.feedbackOpen
1173
- ? overlays.renderFeedback()
1174
- : state.helpVisible
1149
+ : state.helpVisible
1175
1150
  ? overlays.renderHelp()
1176
1151
  : state.changelogOpen
1177
1152
  ? overlays.renderChangelog()
@@ -1184,7 +1159,7 @@ export async function runApp(cliArgs, config) {
1184
1159
  process.stdout.write(ALT_LEAVE);
1185
1160
  console.error(chalk.red('\n[TUI Render Error] An error occurred during UI rendering.'));
1186
1161
  console.error(err);
1187
- console.error(chalk.yellow('\nPlease file an issue at https://github.com/vava-nessa/free-coding-models/issues or use the feedback form (I key) to report this to the author.'));
1162
+ console.error(chalk.yellow('\nPlease file an issue at https://github.com/vava-nessa/free-coding-models/issues or join the Discord to report this to the author.'));
1188
1163
  process.exit(1);
1189
1164
  }
1190
1165
  }, Math.round(1000 / FPS))
@@ -1195,7 +1170,7 @@ export async function runApp(cliArgs, config) {
1195
1170
  pinFavorites: state.favoritesPinnedAndSticky,
1196
1171
  })
1197
1172
 
1198
- 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.favoritesPinnedAndSticky, state.customTextFilter, state.lastReleaseDate, state.footerHidden, state.verdictFilterMode, state.healthFilterMode))
1173
+ 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.favoritesPinnedAndSticky, state.customTextFilter, state.lastReleaseDate, false, state.verdictFilterMode, state.healthFilterMode))
1199
1174
  if (process.stdout.isTTY) {
1200
1175
  process.stdout.flush && process.stdout.flush()
1201
1176
  }
@@ -1256,7 +1231,7 @@ export async function runApp(cliArgs, config) {
1256
1231
  process.stdout.write(ALT_LEAVE);
1257
1232
  console.error(chalk.red('\n[TUI Error] An error occurred in the ping loop.'));
1258
1233
  console.error(err);
1259
- console.error(chalk.yellow('\nPlease file an issue at https://github.com/vava-nessa/free-coding-models/issues or use the feedback form (I key) to report this to the author.'));
1234
+ console.error(chalk.yellow('\nPlease file an issue at https://github.com/vava-nessa/free-coding-models/issues or join the Discord to report this to the author.'));
1260
1235
  process.exit(1);
1261
1236
  }
1262
1237
  }
@@ -1283,48 +1258,6 @@ export async function runApp(cliArgs, config) {
1283
1258
  } catch {}
1284
1259
  }, VERSION_RECHECK_INTERVAL_MS)
1285
1260
 
1286
- // 📖 Router footer stats: poll daemon every 30s so the main table footer always
1287
- // 📖 shows live token counts and daemon status even when the Router Dashboard is closed.
1288
- const ROUTER_FOOTER_POLL_INTERVAL_MS = 30_000
1289
- const ROUTER_FOOTER_FETCH_TIMEOUT_MS = 1200
1290
-
1291
- async function fetchRouterFooterStats() {
1292
- try {
1293
- const controller = new AbortController()
1294
- const timer = setTimeout(() => controller.abort(), ROUTER_FOOTER_FETCH_TIMEOUT_MS)
1295
- const pidPath = `${process.env.HOME}/.free-coding-models-daemon.pid`
1296
- const portPath = `${process.env.HOME}/.free-coding-models-daemon.port`
1297
- let port = 19280
1298
- try {
1299
- const { readFileSync: rfs } = await import('node:fs')
1300
- const savedPort = rfs(portPath, 'utf8').trim()
1301
- if (/^\d+$/.test(savedPort)) port = Number(savedPort)
1302
- } catch {}
1303
- const res = await globalThis.fetch(`http://127.0.0.1:${port}/stats`, {
1304
- signal: controller.signal,
1305
- })
1306
- clearTimeout(timer)
1307
- if (!res.ok) { state.routerFooterRunning = false; return }
1308
- const raw = await res.json()
1309
- const tokens = raw.tokens || {}
1310
- const today = tokens.today || {}
1311
- const allTime = tokens.all_time || {}
1312
- state.routerFooterRunning = true
1313
- state.routerFooterActiveSet = raw.activeSet || null
1314
- state.routerFooterTodayTokens = today.total_tokens || 0
1315
- state.routerFooterAllTimeTokens = allTime.total_tokens || 0
1316
- state.routerFooterRequests = today.requests || 0
1317
- state.routerFooterLastFetchAt = Date.now()
1318
- } catch {
1319
- state.routerFooterRunning = false
1320
- }
1321
- }
1322
-
1323
- state.routerFooterPollTimer = setInterval(() => {
1324
- void fetchRouterFooterStats()
1325
- }, ROUTER_FOOTER_POLL_INTERVAL_MS)
1326
- void fetchRouterFooterStats() // 📖 Initial fetch immediately so footer is populated on first render
1327
-
1328
1261
  // 📖 Router ON by default — no onboarding prompt, just auto-enable silently.
1329
1262
  const routerCfg = state.config?.router
1330
1263
  if (!routerCfg || routerCfg.onboardingSeen !== true || routerCfg.enabled !== true) {
@@ -208,10 +208,8 @@ const BASE_COMMAND_TREE = [
208
208
  { id: 'open-settings', label: 'Settings', shortcut: 'P', icon: '⚙️', type: 'page', description: 'API keys and preferences', keywords: ['settings', 'config', 'api key'] },
209
209
  { id: 'open-help', label: 'Help', shortcut: 'K', icon: '❓', type: 'page', description: 'Show all shortcuts', keywords: ['help', 'shortcuts', 'hotkeys'] },
210
210
  { id: 'open-changelog', label: 'Changelog', shortcut: 'N', icon: '📋', type: 'page', description: 'Version history', keywords: ['changelog', 'release'] },
211
- { id: 'open-feedback', label: 'Feedback', shortcut: 'I', icon: '📝', type: 'page', description: 'Report bugs or requests', keywords: ['feedback', 'bug', 'request'] },
211
+
212
212
  { id: 'open-recommend', label: 'Smart recommend', shortcut: 'Q', icon: '🎯', type: 'page', description: 'Find best model for task', keywords: ['recommend', 'best model'] },
213
- { id: 'open-router-dashboard', label: 'Router dashboard', shortcut: 'Shift+R', icon: '🔀', type: 'page', description: 'Inspect daemon health, circuits, tokens, and request log', keywords: ['router', 'daemon', 'dashboard', 'health', 'stats', 'tokens', 'circuit'] },
214
- { id: 'open-token-usage', label: 'Token usage', shortcut: 'Shift+T', icon: '📊', type: 'page', description: 'View token usage history, 7-day chart, today/all-time totals', keywords: ['token', 'usage', 'chart', 'history', 'router'] },
215
213
  { id: 'open-install-endpoints', label: 'Install endpoints', icon: '🔌', type: 'page', description: 'Install provider catalogs', keywords: ['install', 'endpoints', 'providers'] },
216
214
  { id: 'open-installed-models', label: 'Installed models', icon: '🗂️', type: 'page', description: 'View models configured in tools', keywords: ['installed', 'models', 'configured', 'tools', 'manager', 'goose', 'crush', 'aider'] },
217
215
  ]
package/src/config.js CHANGED
@@ -237,12 +237,12 @@ function normalizeProvidersSection(providers) {
237
237
 
238
238
  function normalizeSettingsSection(settings) {
239
239
  const safeSettings = isPlainObject(settings) ? { ...settings } : {}
240
+ delete safeSettings.footerHidden
240
241
  return {
241
242
  ...safeSettings,
242
243
  hideUnconfiguredModels: typeof safeSettings.hideUnconfiguredModels === 'boolean' ? safeSettings.hideUnconfiguredModels : true,
243
244
  favoritesPinnedAndSticky: typeof safeSettings.favoritesPinnedAndSticky === 'boolean' ? safeSettings.favoritesPinnedAndSticky : false,
244
245
  theme: ['dark', 'light', 'auto'].includes(safeSettings.theme) ? safeSettings.theme : 'auto',
245
- footerHidden: typeof safeSettings.footerHidden === 'boolean' ? safeSettings.footerHidden : false,
246
246
  }
247
247
  }
248
248
 
@@ -1007,7 +1007,7 @@ export function isProviderEnabled(config, providerKey) {
1007
1007
  /**
1008
1008
  * 📖 _emptyProfileSettings: Default TUI settings.
1009
1009
  *
1010
- * @returns {{ tierFilter: string|null, sortColumn: string, sortAsc: boolean, pingInterval: number, hideUnconfiguredModels: boolean, favoritesPinnedAndSticky: boolean, preferredToolMode: string, theme: string, footerHidden: boolean }}
1010
+ * @returns {{ tierFilter: string|null, sortColumn: string, sortAsc: boolean, pingInterval: number, hideUnconfiguredModels: boolean, favoritesPinnedAndSticky: boolean, preferredToolMode: string, theme: string }}
1011
1011
  */
1012
1012
  export function _emptyProfileSettings() {
1013
1013
  return {
@@ -1019,7 +1019,6 @@ export function _emptyProfileSettings() {
1019
1019
  favoritesPinnedAndSticky: false, // 📖 default mode keeps favorites as normal starred rows; press Y to pin+stick them.
1020
1020
  preferredToolMode: 'opencode', // 📖 remember the last Z-selected launcher across app restarts
1021
1021
  theme: 'auto', // 📖 'auto' follows the terminal/OS theme, override with 'dark' or 'light' if needed
1022
- footerHidden: false, // 📖 false = full footer shown; true = collapsed to a single "(W) Toggle Footer" hint
1023
1022
  }
1024
1023
  }
1025
1024
 
package/src/constants.js CHANGED
@@ -15,8 +15,8 @@
15
15
  * - `FPS` controls animation frame rate (braille spinner).
16
16
  * - `COL_MODEL` / `COL_MS` control legacy ping-column widths (retained for compat).
17
17
  * - `CELL_W` is derived from `COL_MS` and used by `msCell` / `spinCell`.
18
- * - `TABLE_HEADER_LINES` + `TABLE_FOOTER_LINES` = `TABLE_FIXED_LINES` must stay in sync
19
- * with the actual number of lines rendered by `renderTable()` in bin/.
18
+ * - `TABLE_HEADER_LINES` and footer line counts must stay in sync with the
19
+ * actual number of lines rendered by `renderTable()`.
20
20
  * - `WIDTH_WARNING_MIN_COLS` controls when the narrow-terminal startup warning appears.
21
21
  * - Overlay background colours (chalk.bgRgb) make each overlay panel visually distinct.
22
22
  *
@@ -101,8 +101,8 @@ export const WIDTH_WARNING_MIN_COLS = 80
101
101
 
102
102
  // 📖 Table row-budget constants — must stay in sync with renderTable()'s actual output.
103
103
  // 📖 If this drifts, model rows overflow and can push the title row out of view.
104
- export const TABLE_HEADER_LINES = 5 // 📖 title, filter bar, spacer, column headers, separator
105
- export const TABLE_FOOTER_LINES = 1 // 📖 single toggle-hint line when collapsed, full footer otherwise
104
+ export const TABLE_HEADER_LINES = 2 // 📖 title, column headers
105
+ export const TABLE_FOOTER_LINES = 2 // 📖 actions, links
106
106
  export const TABLE_FIXED_LINES = TABLE_HEADER_LINES + TABLE_FOOTER_LINES
107
107
 
108
108
  // ─── Small cell-formatting helpers ────────────────────────────────────────────