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 +9 -1
- package/README.md +10 -32
- package/package.json +1 -1
- package/sources.js +2 -2
- package/src/app.js +20 -87
- package/src/command-palette.js +1 -3
- package/src/config.js +2 -3
- package/src/constants.js +4 -4
- package/src/key-handler.js +12 -104
- package/src/overlays.js +20 -120
- package/src/provider-metadata.js +1 -1
- package/src/render-helpers.js +38 -8
- package/src/render-table.js +116 -278
- package/src/router-dashboard.js +10 -1
- package/web/dist/assets/{index-DNRCaWPi.js → index-DKHCzbK1.js} +1 -1
- package/web/dist/index.html +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
## [0.3.
|
|
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-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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,
|
|
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
|
|
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) {
|
package/src/command-palette.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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`
|
|
19
|
-
*
|
|
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 =
|
|
105
|
-
export const TABLE_FOOTER_LINES =
|
|
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 ────────────────────────────────────────────
|