free-coding-models 0.3.15 → 0.3.17

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,8 +2,21 @@
2
2
 
3
3
  ---
4
4
 
5
+ ## 0.3.17
6
+
7
+ ### Added
8
+ - **Auto Light/Dark Theme**: Implemented automatic detection of the user's terminal theme (dark or light) so that the TUI is always readable. Added semantic color tokens, and users can override the theme as `dark`, `light`, or `auto` via the Settings interface.
9
+
10
+ ## 0.3.16
11
+
12
+ ### Added
13
+ - **iFlow free coding models**: Added the iFlow provider to the README and TUI, supporting `deepseek-v3`, `mini-max-m2.5`, etc.
14
+
5
15
  ## 0.3.15
6
16
 
17
+ ### Added
18
+ - **GLM-4.7-Flash and GLM-4.5-Flash models**: Added ZAI's free coding models GLM-4.7-Flash and GLM-4.5-Flash to the ZAI provider. Both models are rated S tier with 59.2% SWE-bench scores and are completely free with unlimited API access.
19
+
7
20
  ### Changed
8
21
  - Added vertical column separators (gentle dark orange) for clearer column separation and removed the horizontal separator line in the main TUI.
9
22
 
package/README.md CHANGED
@@ -1,184 +1,251 @@
1
- # free-coding-models
1
+ <p align="center">
2
+ <img src="https://img.shields.io/npm/v/free-coding-models?color=76b900&label=npm&logo=npm" alt="npm version">
3
+ <img src="https://img.shields.io/node/v/free-coding-models?color=76b900&logo=node.js" alt="node version">
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-160-76b900?logo=nvidia" alt="models count">
6
+ <img src="https://img.shields.io/badge/providers-20-blue" alt="providers count">
7
+ </p>
2
8
 
3
- `free-coding-models` is a terminal UI to compare free coding models across providers, monitor live health/latency, and launch supported coding tools with the selected model.
9
+ <h1 align="center">free-coding-models</h1>
4
10
 
5
- It is built around direct provider integrations. The old global proxy bridge has been removed from the product and is being rewritten from scratch, so only the stable direct-launch workflow is exposed for now.
11
+ <p align="center">
12
+ <strong>Find the fastest free coding model in seconds</strong><br>
13
+ <sub>Ping 160 models across 20 AI Free providers in real-time </sub><br><sub> Install Free API endpoints to your favorite AI coding tool: <br>OpenCode, OpenClaw, Crush, Goose, Aider, Qwen Code, OpenHands, Amp or Pi in one keystroke</sub>
14
+ </p>
6
15
 
7
- ## Install
8
16
 
9
- ```bash
10
- pnpm install
11
- pnpm start
12
- ```
13
17
 
14
- To install globally:
18
+ <p align="center">
15
19
 
16
20
  ```bash
17
21
  npm install -g free-coding-models
18
22
  free-coding-models
19
23
  ```
20
24
 
21
- ## What It Does
25
+ </p>
26
+
27
+ <p align="center">
28
+ <a href="#-why-this-tool">Why</a> •
29
+ <a href="#-quick-start">Quick Start</a> •
30
+ <a href="#-providers">Providers</a> •
31
+ <a href="#-usage">Usage</a> •
32
+ <a href="#-tui-keys">TUI Keys</a> •
33
+ <a href="#-contributing">Contributing</a>
34
+ </p>
35
+
36
+ <p align="center">
37
+ <img src="demo.gif" alt="free-coding-models demo" width="100%">
38
+ </p>
39
+
40
+ <p align="center">
41
+ <sub>Made with ❤️ and ☕ by <a href="https://vanessadepraute.dev">Vanessa Depraute</a> (aka <a href="https://vavanessa.dev">Vava-Nessa</a>)</sub>
42
+ </p>
43
+
44
+ ---
45
+
46
+ ## 💡 Why this tool?
22
47
 
23
- - Lists free coding models from the providers defined in [`sources.js`](./sources.js)
24
- - Pings models continuously and shows latency, uptime, stability, verdict, and usage snapshots
25
- - Lets you filter, sort, favorite, and compare models inside a full-screen TUI
26
- - Launches supported coding tools with the currently selected model, after writing that exact selection as the tool default
27
- - Installs provider catalogs into compatible external tool configs through the `Y` flow
48
+ There are **160+ free coding models** scattered across 20 providers. Which one is fastest right now? Which one is actually stable versus just lucky on the last ping?
28
49
 
29
- ## Stable Product Surface
50
+ 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%).
30
51
 
31
- The public launcher set is currently:
52
+ It then writes the model you pick directly into your coding tool's config — so you go from "which model?" to "coding" in under 10 seconds.
32
53
 
33
- - `OpenCode CLI`
34
- - `OpenCode Desktop`
35
- - `OpenClaw`
36
- - `Crush`
37
- - `Goose`
38
- - `Pi`
39
- - `Aider`
40
- - `Qwen Code`
41
- - `OpenHands`
42
- - `Amp`
54
+ ---
43
55
 
44
- Temporarily removed from the public app while the bridge is being rebuilt:
56
+ ## Quick Start
45
57
 
46
- - `Claude Code`
47
- - `Codex`
48
- - `Gemini`
49
- - the old FCM global proxy / daemon / log overlay flow
58
+ **① Get a free API key** — you only need one to get started:
50
59
 
51
- ## Quick Start
60
+ **160 coding models** across 20 providers, ranked by [SWE-bench Verified](https://www.swebench.com).
61
+
62
+ | Provider | Models | Tier range | Free tier | Env var |
63
+ |----------|--------|-----------|-----------|--------|
64
+ | [NVIDIA NIM](https://build.nvidia.com) | 44 | S+ → C | 40 req/min (no credit card needed) | `NVIDIA_API_KEY` |
65
+ | [iFlow](https://platform.iflow.cn) | 11 | S+ → A+ | Free for individuals (no req limits, 7-day key expiry) | `IFLOW_API_KEY` |
66
+ | [ZAI](https://z.ai) | 7 | S+ → S | Free tier (generous quota) | `ZAI_API_KEY` |
67
+ | [Alibaba DashScope](https://modelstudio.console.alibabacloud.com) | 8 | S+ → A | 1M free tokens per model (Singapore region, 90 days) | `DASHSCOPE_API_KEY` |
68
+ | [Groq](https://console.groq.com/keys) | 10 | S → B | 30‑50 RPM per model (varies by model) | `GROQ_API_KEY` |
69
+ | [Cerebras](https://cloud.cerebras.ai) | 7 | S+ → B | Generous free tier (developer tier 10× higher limits) | `CEREBRAS_API_KEY` |
70
+ | [SambaNova](https://sambanova.ai/developers) | 12 | S+ → B | Dev tier generous quota | `SAMBANOVA_API_KEY` |
71
+ | [OpenRouter](https://openrouter.ai/keys) | 11 | S+ → C | Free on :free: 50/day <$10, 1000/day ≥$10 (20 req/min) | `OPENROUTER_API_KEY` |
72
+ | [Hugging Face](https://huggingface.co/settings/tokens) | 2 | S → B | Free monthly credits (~$0.10) | `HUGGINGFACE_API_KEY` |
73
+ | [Together AI](https://api.together.ai/settings/api-keys) | 7 | S+ → A- | Credits/promos vary by account (check console) | `TOGETHER_API_KEY` |
74
+ | [DeepInfra](https://deepinfra.com/login) | 2 | A- → B+ | 200 concurrent requests (default) | `DEEPINFRA_API_KEY` |
75
+ | [Fireworks AI](https://fireworks.ai) | 2 | S | $1 credits – 10 req/min without payment | `FIREWORKS_API_KEY` |
76
+ | [Mistral Codestral](https://codestral.mistral.ai) | 1 | B+ | 30 req/min, 2000/day | `CODESTRAL_API_KEY` |
77
+ | [Hyperbolic](https://app.hyperbolic.ai/settings) | 10 | S+ → A- | $1 free trial credits | `HYPERBOLIC_API_KEY` |
78
+ | [Scaleway](https://console.scaleway.com/iam/api-keys) | 7 | S+ → B+ | 1M free tokens | `SCALEWAY_API_KEY` |
79
+ | [Google AI Studio](https://aistudio.google.com/apikey) | 3 | B → C | 14.4K req/day, 30/min | `GOOGLE_API_KEY` |
80
+ | [SiliconFlow](https://cloud.siliconflow.cn/account/ak) | 6 | S+ → A | Free models: usually 100 RPM, varies by model | `SILICONFLOW_API_KEY` |
81
+ | [Cloudflare Workers AI](https://dash.cloudflare.com) | 6 | S → B | Free: 10k neurons/day, text-gen 300 RPM | `CLOUDFLARE_API_TOKEN` + `CLOUDFLARE_ACCOUNT_ID` |
82
+ | [Perplexity API](https://www.perplexity.ai/settings/api) | 4 | A+ → B | Tiered limits by spend (default ~50 RPM) | `PERPLEXITY_API_KEY` |
83
+ | [Replicate](https://replicate.com/account/api-tokens) | 1 | A- | 6 req/min (no payment) – up to 3,000 RPM with payment | `REPLICATE_API_TOKEN` |
84
+
85
+ > 💡 One key is enough. Add more at any time with **`P`** inside the TUI.
86
+
87
+ ### Tier scale
88
+
89
+ | Tier | SWE-bench | Best for |
90
+ |------|-----------|----------|
91
+ | **S+** | ≥ 70% | Complex refactors, real-world GitHub issues |
92
+ | **S** | 60–70% | Most coding tasks, strong general use |
93
+ | **A+/A** | 40–60% | Solid alternatives, targeted programming |
94
+ | **A-/B+** | 30–40% | Smaller tasks, constrained infra |
95
+ | **B/C** | < 30% | Code completion, edge/minimal setups |
96
+
97
+ **② Install and run:**
52
98
 
53
99
  ```bash
100
+ npm install -g free-coding-models
54
101
  free-coding-models
55
102
  ```
56
103
 
57
- Useful startup flags:
104
+ On first run, you'll be prompted to enter your API key(s). You can skip providers and add more later with **`P`**.
58
105
 
59
- ```bash
60
- free-coding-models --opencode
61
- free-coding-models --openclaw --tier S
62
- free-coding-models --crush
63
- free-coding-models --json
64
- free-coding-models --recommend
65
- free-coding-models --help
106
+ **③ Pick a model and launch your tool:**
107
+
108
+ ```
109
+ ↑↓ navigate → Enter to launch
66
110
  ```
67
111
 
68
- Default tool mode with no launcher flag: `OpenCode CLI`
112
+ The model you select is automatically written into your tool's config (OpenCode, OpenClaw, Crush, etc.) and the tool opens immediately. Done.
69
113
 
70
- ## Main TUI Keys
114
+ > 💡 You can also run `free-coding-models --goose --tier S` to pre-filter to S-tier models for Goose before the TUI even opens.
71
115
 
72
- - `↑↓` navigate rows
73
- - `Enter` launch/select the current model in the active tool mode
74
- - `Z` cycle tool mode
75
- - `T` cycle tier filter
76
- - `D` cycle provider filter
77
- - `R/O/M/L/A/S/C/H/V/B/U/G` sort columns
78
- - `E` toggle configured models only
79
- - `F` favorite/unfavorite the selected model
80
- - `W` cycle ping cadence
81
- - `P` open Settings
82
- - `Y` open Install Endpoints
83
- - `Q` open Smart Recommend
84
- - `I` open feedback / bug report form
85
- - `N` open changelog
86
- - `K` open help
87
- - `Ctrl+C` exit
88
116
 
89
- ## Settings
90
117
 
91
- Press `P` to:
118
+ ## 🚀 Usage
92
119
 
93
- - add or remove provider API keys
94
- - enable or disable providers
95
- - test provider keys
96
- - check for updates
97
- - toggle the terminal width warning
98
- - clean discontinued proxy-era config left behind by older builds
120
+ ### Common scenarios
99
121
 
100
- The main TUI also shows a footer notice explaining that the external-tools bridge/proxy is intentionally disabled while it is being rebuilt.
122
+ ```bash
123
+ # "I want the most reliable model right now"
124
+ free-coding-models --fiable
101
125
 
102
- ## Install Endpoints
126
+ # "I want to configure Goose with an S-tier model"
127
+ free-coding-models --goose --tier S
103
128
 
104
- Press `Y` to install one configured provider into supported external tools.
129
+ # "I want NVIDIA's top models only"
130
+ free-coding-models --origin nvidia --tier S
105
131
 
106
- Current install flow:
132
+ # "Show me only elite models that are currently healthy"
133
+ free-coding-models --premium
107
134
 
108
- 1. Choose a configured provider
109
- 2. Choose a supported tool
110
- 3. Choose scope: all models or selected models
111
- 4. Write the managed config/env files
135
+ # "I want to script this — give me JSON"
136
+ free-coding-models --tier S --json | jq -r '.[0].modelId'
112
137
 
113
- This flow is direct-provider only now. The old proxy-backed install path has been removed.
138
+ # "I want to configure OpenClaw with Groq's fastest model"
139
+ free-coding-models --openclaw --origin groq
140
+ ```
114
141
 
115
- ## Tool Notes
142
+ ### Tool launcher flags
116
143
 
117
- When you press `Enter`, FCM now persists the selected model into the target tool before launch so the tool opens on the model you actually picked.
144
+ | Flag | Launches |
145
+ |------|----------|
146
+ | `--opencode` | OpenCode CLI |
147
+ | `--opencode-desktop` | OpenCode Desktop |
148
+ | `--openclaw` | OpenClaw |
149
+ | `--crush` | Crush |
150
+ | `--goose` | Goose |
151
+ | `--aider` | Aider |
152
+ | `--qwen` | Qwen Code |
153
+ | `--openhands` | OpenHands |
154
+ | `--amp` | Amp |
155
+ | `--pi` | Pi |
118
156
 
119
- ### OpenCode
157
+ Press **`Z`** in the TUI to cycle between tools without restarting.
120
158
 
121
- - `OpenCode CLI` and `OpenCode Desktop` share `~/.config/opencode/opencode.json`
122
- - Selecting a model and pressing `Enter` updates the config and launches the target mode
159
+ **[Full flags reference](./docs/flags.md)**
123
160
 
124
- ### OpenClaw
161
+ ---
125
162
 
126
- - `free-coding-models` writes the selected provider/model into `~/.openclaw/openclaw.json` as the primary default
127
- - OpenClaw itself is not launched by FCM
163
+ ## ⌨️ TUI Keys
128
164
 
129
- ### ZAI with OpenCode
165
+ | Key | Action |
166
+ |-----|--------|
167
+ | `↑↓` | Navigate models |
168
+ | `Enter` | Launch selected model in active tool |
169
+ | `Z` | Cycle target tool |
170
+ | `T` | Cycle tier filter |
171
+ | `D` | Cycle provider filter |
172
+ | `E` | Toggle configured-only mode |
173
+ | `F` | Favorite / unfavorite model |
174
+ | `R/S/C/M/O/L/A/H/V/B/U` | Sort columns |
175
+ | `P` | Settings (API keys, providers, updates) |
176
+ | `Y` | Install Endpoints (push provider into tool config) |
177
+ | `Q` | Smart Recommend overlay |
178
+ | `N` | Changelog |
179
+ | `W` | Cycle ping cadence |
180
+ | `I` | Feedback / bug report |
181
+ | `K` | Help overlay |
182
+ | `Ctrl+C` | Exit |
130
183
 
131
- ZAI still needs a small local compatibility bridge for OpenCode only, because ZAI uses `/api/coding/paas/v4/*` instead of standard `/v1/*` paths.
184
+ **[Stability score & column reference](./docs/stability.md)**
132
185
 
133
- That bridge is internal to the OpenCode launcher path and is still supported:
186
+ ---
134
187
 
135
- - it starts only when launching a ZAI model in OpenCode
136
- - it binds to localhost on a random port
137
- - it shuts down automatically when OpenCode exits
188
+ ## Features
138
189
 
139
- This is separate from the removed global multi-tool proxy system.
190
+ - **Parallel pings** all 160 models tested simultaneously via native `fetch`
191
+ - **Adaptive monitoring** — 2s burst for 60s → 10s normal → 30s idle
192
+ - **Stability score** — composite 0–100 (p95 latency, jitter, spike rate, uptime)
193
+ - **Smart ranking** — top 3 highlighted 🥇🥈🥉
194
+ - **Favorites** — pin models with `F`, persisted across sessions
195
+ - **Configured-only default** — only shows providers you have keys for
196
+ - **Keyless latency** — models ping even without an API key (show 🔑 NO KEY)
197
+ - **Smart Recommend** — questionnaire picks the best model for your task type
198
+ - **Install Endpoints** — push a full provider catalog into any tool's config (`Y`)
199
+ - **Width guardrail** — shows a warning instead of a broken table in narrow terminals
200
+ - **Auto-retry** — timeout models keep getting retried
140
201
 
141
- ## `/testfcm`
202
+ ---
142
203
 
143
- There is a repo-local harness for exercising the real TUI and launcher flow.
204
+ ## 📋 Contributing
144
205
 
145
- Available scripts:
206
+ We welcome contributions — issues, PRs, new provider integrations.
146
207
 
147
- ```bash
148
- pnpm test:fcm
149
- pnpm test:fcm:mock
150
- ```
208
+ **Q:** How accurate are the latency numbers?
209
+ **A:** Real round-trip times measured by your machine. Results depend on your network and provider load at that moment.
151
210
 
152
- `pnpm test:fcm:mock` uses the mock `crush` binary in `test/fixtures/mock-bin` so maintainers can validate the TUI → launcher plumbing without a real external CLI installed.
211
+ **Q:** Can I add a new provider?
212
+ **A:** Yes — see [`sources.js`](./sources.js) for the model catalog format.
153
213
 
154
- ## Development
214
+ **[Development guide](./docs/development.md)** · **[Config reference](./docs/config.md)** · **[Tool integrations](./docs/integrations.md)**
155
215
 
156
- Run the unit tests:
216
+ ---
157
217
 
158
- ```bash
159
- pnpm test
160
- ```
218
+ ## 📧 Support
161
219
 
162
- Run the app locally:
220
+ [GitHub Issues](https://github.com/vava-nessa/free-coding-models/issues) · [Discord](https://discord.gg/ZTNFHvvCkU)
163
221
 
164
- ```bash
165
- pnpm start
166
- ```
222
+ ---
223
+
224
+ ## 📄 License
167
225
 
168
- ## Architecture Notes
226
+ MIT © [vava](https://github.com/vava-nessa)
169
227
 
170
- - Main CLI entrypoint: [`bin/free-coding-models.js`](./bin/free-coding-models.js)
171
- - Pure helpers and sorting logic: [`src/utils.js`](./src/utils.js)
172
- - OpenCode launch/config helpers: [`src/opencode.js`](./src/opencode.js), [`src/opencode-config.js`](./src/opencode-config.js)
173
- - External tool launchers: [`src/tool-launchers.js`](./src/tool-launchers.js)
174
- - Endpoint installer flow: [`src/endpoint-installer.js`](./src/endpoint-installer.js)
228
+ ---
175
229
 
176
- ## Current Status
230
+ <p align="center">
231
+ <strong>Contributors</strong><br>
232
+ <a href="https://github.com/vava-nessa"><img src="https://avatars.githubusercontent.com/u/5466264?v=4&s=60" width="60" height="60" style="border-radius:50%" alt="vava-nessa"></a>
233
+ <a href="https://github.com/erwinh22"><img src="https://avatars.githubusercontent.com/u/6641858?v=4&s=60" width="60" height="60" style="border-radius:50%" alt="erwinh22"></a>
234
+ <a href="https://github.com/whit3rabbit"><img src="https://avatars.githubusercontent.com/u/12357518?v=4&s=60" width="60" height="60" style="border-radius:50%" alt="whit3rabbit"></a>
235
+ <a href="https://github.com/skylaweber"><img src="https://avatars.githubusercontent.com/u/172871734?v=4&s=60" width="60" height="60" style="border-radius:50%" alt="skylaweber"></a>
236
+ <a href="https://github.com/PhucTruong-ctrl"><img src="https://github.com/PhucTruong-ctrl.png?s=60" width="60" height="60" style="border-radius:50%" alt="PhucTruong-ctrl"></a>
237
+ <br>
238
+ <sub>
239
+ <a href="https://github.com/vava-nessa">vava-nessa</a> &middot;
240
+ <a href="https://github.com/erwinh22">erwinh22</a> &middot;
241
+ <a href="https://github.com/whit3rabbit">whit3rabbit</a> &middot;
242
+ <a href="https://github.com/skylaweber">skylaweber</a> &middot;
243
+ <a href="https://github.com/PhucTruong-ctrl">PhucTruong-ctrl</a>
244
+ </sub>
245
+ </p>
177
246
 
178
- The app surface is intentionally narrowed right now to keep releases stable:
179
247
 
180
- - direct provider launches are the supported path
181
- - the old cross-tool proxy stack has been removed from the app
182
- - Claude Code, Codex, and Gemini stay hidden until the rewrite is production-ready
183
248
 
184
- When that rewrite lands, it should come back as a separate, cleaner system rather than more patches on the old one.
249
+ <p align="center">
250
+ <sub>Anonymous usage data collected to improve the tool. No personal information ever.</sub>
251
+ </p>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "free-coding-models",
3
- "version": "0.3.15",
3
+ "version": "0.3.17",
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
@@ -248,8 +248,10 @@ export const zai = [
248
248
  // ── S+ tier — SWE-bench Verified ≥70% ──
249
249
  ['zai/glm-5', 'GLM-5', 'S+', '77.8%', '128k'],
250
250
  ['zai/glm-4.7', 'GLM-4.7', 'S+', '73.8%', '200k'],
251
+ ['zai/glm-4.7-flash', 'GLM-4.7-Flash', 'S', '59.2%', '200k'],
251
252
  ['zai/glm-4.5', 'GLM-4.5', 'S+', '75.0%', '128k'],
252
253
  ['zai/glm-4.5-air', 'GLM-4.5-Air', 'S+', '72.0%', '128k'],
254
+ ['zai/glm-4.5-flash', 'GLM-4.5-Flash', 'S', '59.2%', '128k'],
253
255
  ['zai/glm-4.6', 'GLM-4.6', 'S+', '70.0%', '128k'],
254
256
  ]
255
257
 
package/src/app.js CHANGED
@@ -126,6 +126,7 @@ import { getConfiguredInstallableProviders, installProviderEndpoints, refreshIns
126
126
  import { loadCache, saveCache, clearCache, getCacheAge } from '../src/cache.js'
127
127
  import { checkConfigSecurity } from '../src/security.js'
128
128
  import { buildCliHelpText } from '../src/cli-help.js'
129
+ import { detectActiveTheme } from '../src/theme.js'
129
130
 
130
131
  // 📖 mergedModels: cross-provider grouped model list (one entry per label, N providers each)
131
132
  // 📖 mergedModelByLabel: fast lookup map from display label → merged model entry
@@ -174,7 +175,8 @@ const LOCAL_VERSION = pkg.version
174
175
 
175
176
  export async function runApp(cliArgs, config) {
176
177
 
177
-
178
+ // 📖 Detect user active terminal theme
179
+ detectActiveTheme(config.settings?.theme || 'dark')
178
180
 
179
181
  // 📖 Check config file security — warn and offer auto-fix if permissions are too open
180
182
  const securityCheck = checkConfigSecurity()
package/src/config.js CHANGED
@@ -210,6 +210,7 @@ function normalizeSettingsSection(settings) {
210
210
  ...safeSettings,
211
211
  hideUnconfiguredModels: typeof safeSettings.hideUnconfiguredModels === 'boolean' ? safeSettings.hideUnconfiguredModels : true,
212
212
  disableWidthsWarning: safeSettings.disableWidthsWarning === true,
213
+ theme: ['dark', 'light', 'auto'].includes(safeSettings.theme) ? safeSettings.theme : 'dark',
213
214
  }
214
215
  }
215
216
 
@@ -230,6 +231,7 @@ function normalizeProfileSettings(settings) {
230
231
  ..._emptyProfileSettings(),
231
232
  ...safeSettings,
232
233
  disableWidthsWarning: safeSettings.disableWidthsWarning === true,
234
+ theme: ['dark', 'light', 'auto'].includes(safeSettings.theme) ? safeSettings.theme : 'dark',
233
235
  }
234
236
  }
235
237
 
@@ -842,6 +844,7 @@ export function _emptyProfileSettings() {
842
844
  hideUnconfiguredModels: true, // 📖 true = default to providers that are actually configured
843
845
  preferredToolMode: 'opencode', // 📖 remember the last Z-selected launcher across app restarts
844
846
  disableWidthsWarning: false, // 📖 Disable widths warning (default off)
847
+ theme: 'dark', // 📖 'dark', 'light', or 'auto'
845
848
  }
846
849
  }
847
850
 
@@ -200,6 +200,7 @@ export function createKeyHandler(ctx) {
200
200
  noteUserActivity,
201
201
  intervalToPingMode,
202
202
  PING_MODE_CYCLE,
203
+ themeRowIdx,
203
204
  setResults,
204
205
  readline,
205
206
  } = ctx
@@ -926,7 +927,8 @@ export function createKeyHandler(ctx) {
926
927
  const providerKeys = Object.keys(sources)
927
928
  const updateRowIdx = providerKeys.length
928
929
  const widthWarningRowIdx = updateRowIdx + 1
929
- const cleanupLegacyProxyRowIdx = widthWarningRowIdx + 1
930
+ const themeRowIdx = widthWarningRowIdx + 1
931
+ const cleanupLegacyProxyRowIdx = themeRowIdx + 1
930
932
  const changelogViewRowIdx = cleanupLegacyProxyRowIdx + 1
931
933
  // 📖 Profile system removed - API keys now persist permanently across all sessions
932
934
  const maxRowIdx = changelogViewRowIdx
@@ -1108,6 +1110,19 @@ export function createKeyHandler(ctx) {
1108
1110
  || state.settingsCursor === cleanupLegacyProxyRowIdx
1109
1111
  || state.settingsCursor === changelogViewRowIdx
1110
1112
  ) return
1113
+ // 📖 Theme configuration cycle inside settings
1114
+ if (state.settingsCursor === themeRowIdx) {
1115
+ const themes = ['dark', 'light', 'auto']
1116
+ const currentTheme = state.config.settings?.theme || 'dark'
1117
+ const nextIndex = (themes.indexOf(currentTheme) + 1) % themes.length
1118
+ state.config.settings.theme = themes[nextIndex]
1119
+ saveConfig(state.config)
1120
+ try {
1121
+ const { detectActiveTheme } = await import('../src/theme.js')
1122
+ detectActiveTheme(state.config.settings.theme)
1123
+ } catch {}
1124
+ return
1125
+ }
1111
1126
  // 📖 Widths Warning toggle (disable/enable)
1112
1127
  if (state.settingsCursor === widthWarningRowIdx) {
1113
1128
  toggleWidthsWarningSetting()
package/src/overlays.js CHANGED
@@ -22,6 +22,7 @@
22
22
 
23
23
  import { loadChangelog } from './changelog-loader.js'
24
24
  import { buildCliHelpLines } from './cli-help.js'
25
+ import { themeColors } from './theme.js'
25
26
 
26
27
  export function createOverlayRenderers(state, deps) {
27
28
  const {
@@ -34,9 +35,6 @@ export function createOverlayRenderers(state, deps) {
34
35
  resolveApiKeys,
35
36
  isProviderEnabled,
36
37
  TIER_CYCLE,
37
- SETTINGS_OVERLAY_BG,
38
- HELP_OVERLAY_BG,
39
- RECOMMEND_OVERLAY_BG,
40
38
  OVERLAY_PANEL_WIDTH,
41
39
  keepOverlayTargetVisible,
42
40
  sliceOverlayLines,
@@ -163,7 +161,7 @@ export function createOverlayRenderers(state, deps) {
163
161
 
164
162
  const row = `${bullet}[ ${enabledBadge} ] ${providerName} ${keyDisplay.padEnd(30)} ${testBadge} ${rateSummary}`
165
163
  cursorLineByRow[i] = lines.length
166
- lines.push(isCursor ? chalk.bgRgb(30, 30, 60)(row) : row)
164
+ lines.push(isCursor ? themeColors.bgCursor(row) : row)
167
165
  }
168
166
 
169
167
  lines.push('')
@@ -216,7 +214,7 @@ export function createOverlayRenderers(state, deps) {
216
214
  if (updateState === 'installing') updateStatus = chalk.cyan('Installing update…')
217
215
  const updateRow = `${updateBullet}${chalk.bold(updateActionLabel).padEnd(44)} ${updateStatus}`
218
216
  cursorLineByRow[updateRowIdx] = lines.length
219
- lines.push(updateCursor ? chalk.bgRgb(30, 30, 60)(updateRow) : updateRow)
217
+ lines.push(updateCursor ? themeColors.bgCursor(updateRow) : updateRow)
220
218
  // 📖 Width warning visibility row for the startup narrow-terminal overlay.
221
219
  const disableWidthsWarning = Boolean(state.config.settings?.disableWidthsWarning)
222
220
  const widthWarningBullet = state.settingsCursor === widthWarningRowIdx ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
@@ -225,7 +223,7 @@ export function createOverlayRenderers(state, deps) {
225
223
  : chalk.greenBright('👁 Enabled')
226
224
  const widthWarningRow = `${widthWarningBullet}${chalk.bold('Small Width Warnings').padEnd(44)} ${widthWarningStatus}`
227
225
  cursorLineByRow[widthWarningRowIdx] = lines.length
228
- lines.push(state.settingsCursor === widthWarningRowIdx ? chalk.bgRgb(30, 30, 60)(widthWarningRow) : widthWarningRow)
226
+ lines.push(state.settingsCursor === widthWarningRowIdx ? themeColors.bgCursor(widthWarningRow) : widthWarningRow)
229
227
  if (updateState === 'error' && state.settingsUpdateError) {
230
228
  lines.push(chalk.red(` ${state.settingsUpdateError}`))
231
229
  }
@@ -234,13 +232,13 @@ export function createOverlayRenderers(state, deps) {
234
232
  const cleanupLegacyProxyBullet = state.settingsCursor === cleanupLegacyProxyRowIdx ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
235
233
  const cleanupLegacyProxyRow = `${cleanupLegacyProxyBullet}${chalk.bold('Clean Legacy Proxy Config').padEnd(44)} ${chalk.magentaBright('Enter remove discontinued bridge leftovers')}`
236
234
  cursorLineByRow[cleanupLegacyProxyRowIdx] = lines.length
237
- lines.push(state.settingsCursor === cleanupLegacyProxyRowIdx ? chalk.bgRgb(55, 25, 55)(cleanupLegacyProxyRow) : cleanupLegacyProxyRow)
235
+ lines.push(state.settingsCursor === cleanupLegacyProxyRowIdx ? themeColors.bgCursorLegacy(cleanupLegacyProxyRow) : cleanupLegacyProxyRow)
238
236
 
239
237
  // 📖 Changelog viewer row
240
238
  const changelogViewBullet = state.settingsCursor === changelogViewRowIdx ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
241
239
  const changelogViewRow = `${changelogViewBullet}${chalk.bold('View Changelog').padEnd(44)} ${chalk.dim('Enter browse version history')}`
242
240
  cursorLineByRow[changelogViewRowIdx] = lines.length
243
- lines.push(state.settingsCursor === changelogViewRowIdx ? chalk.bgRgb(30, 45, 30)(changelogViewRow) : changelogViewRow)
241
+ lines.push(state.settingsCursor === changelogViewRowIdx ? themeColors.bgCursorSettingsList(changelogViewRow) : changelogViewRow)
244
242
 
245
243
  // 📖 Profile system removed - API keys now persist permanently across all sessions
246
244
 
@@ -280,7 +278,7 @@ export function createOverlayRenderers(state, deps) {
280
278
  const { visible, offset } = sliceOverlayLines(lines, state.settingsScrollOffset, state.terminalRows)
281
279
  state.settingsScrollOffset = offset
282
280
 
283
- const tintedLines = tintOverlayLines(visible, SETTINGS_OVERLAY_BG, state.terminalCols)
281
+ const tintedLines = tintOverlayLines(visible, themeColors.overlayBgSettings, state.terminalCols)
284
282
  const cleared = tintedLines.map(l => l + EL)
285
283
  return cleared.join('\n')
286
284
  }
@@ -347,7 +345,7 @@ export function createOverlayRenderers(state, deps) {
347
345
  const bullet = isCursor ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
348
346
  const row = `${bullet}${chalk.bold(provider.label.padEnd(24))} ${chalk.dim(`${provider.modelCount} models`)}`
349
347
  cursorLineByRow[idx] = lines.length
350
- lines.push(isCursor ? chalk.bgRgb(24, 44, 62)(row) : row)
348
+ lines.push(isCursor ? themeColors.bgCursorInstall(row) : row)
351
349
  })
352
350
  }
353
351
 
@@ -371,7 +369,7 @@ export function createOverlayRenderers(state, deps) {
371
369
  const bullet = isCursor ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
372
370
  const row = `${bullet}${chalk.bold(label.padEnd(26))} ${note}`
373
371
  cursorLineByRow[idx] = lines.length
374
- lines.push(isCursor ? chalk.bgRgb(24, 44, 62)(row) : row)
372
+ lines.push(isCursor ? themeColors.bgCursorInstall(row) : row)
375
373
  })
376
374
 
377
375
  lines.push('')
@@ -386,7 +384,7 @@ export function createOverlayRenderers(state, deps) {
386
384
  const bullet = isCursor ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
387
385
  const row = `${bullet}${chalk.bold(scope.label)}`
388
386
  cursorLineByRow[idx] = lines.length
389
- lines.push(isCursor ? chalk.bgRgb(24, 44, 62)(row) : row)
387
+ lines.push(isCursor ? themeColors.bgCursorInstall(row) : row)
390
388
  lines.push(chalk.dim(` ${scope.hint}`))
391
389
  lines.push('')
392
390
  })
@@ -409,7 +407,7 @@ export function createOverlayRenderers(state, deps) {
409
407
  const tier = chalk.cyan(model.tier.padEnd(2))
410
408
  const row = `${bullet}${checkbox} ${chalk.bold(model.label.padEnd(26))} ${tier} ${chalk.dim(model.ctx.padEnd(6))} ${chalk.dim(model.modelId)}`
411
409
  cursorLineByRow[idx] = lines.length
412
- lines.push(isCursor ? chalk.bgRgb(24, 44, 62)(row) : row)
410
+ lines.push(isCursor ? themeColors.bgCursorInstall(row) : row)
413
411
  })
414
412
 
415
413
  lines.push('')
@@ -443,7 +441,7 @@ export function createOverlayRenderers(state, deps) {
443
441
  const { visible, offset } = sliceOverlayLines(lines, state.installEndpointsScrollOffset, state.terminalRows)
444
442
  state.installEndpointsScrollOffset = offset
445
443
 
446
- const tintedLines = tintOverlayLines(visible, SETTINGS_OVERLAY_BG, state.terminalCols)
444
+ const tintedLines = tintOverlayLines(visible, themeColors.overlayBgSettings, state.terminalCols)
447
445
  const cleared = tintedLines.map((line) => line + EL)
448
446
  return cleared.join('\n')
449
447
  }
@@ -539,7 +537,7 @@ export function createOverlayRenderers(state, deps) {
539
537
  // 📖 Help overlay can be longer than viewport, so keep a dedicated scroll offset.
540
538
  const { visible, offset } = sliceOverlayLines(lines, state.helpScrollOffset, state.terminalRows)
541
539
  state.helpScrollOffset = offset
542
- const tintedLines = tintOverlayLines(visible, HELP_OVERLAY_BG, state.terminalCols)
540
+ const tintedLines = tintOverlayLines(visible, themeColors.overlayBgHelp, state.terminalCols)
543
541
  const cleared = tintedLines.map(l => l + EL)
544
542
  return cleared.join('\n')
545
543
  }
@@ -605,7 +603,7 @@ export function createOverlayRenderers(state, deps) {
605
603
  const opt = q.options[i]
606
604
  const isCursor = i === state.recommendCursor
607
605
  const bullet = isCursor ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
608
- const label = isCursor ? chalk.bold.white(opt.label) : chalk.white(opt.label)
606
+ const label = isCursor ? themeColors.textBold(opt.label) : themeColors.text(opt.label)
609
607
  lines.push(`${bullet}${label}`)
610
608
  }
611
609
 
@@ -666,9 +664,9 @@ export function createOverlayRenderers(state, deps) {
666
664
  const stabStr = stability === -1 ? '—' : String(stability)
667
665
 
668
666
  const isCursor = i === state.recommendCursor
669
- const highlight = isCursor ? chalk.bgRgb(20, 50, 25) : (s => s)
667
+ const highlight = isCursor ? themeColors.bgCursor : (s => s)
670
668
 
671
- lines.push(highlight(` ${medal} ${chalk.bold('#' + (i + 1))} ${chalk.bold.white(r.label)} ${chalk.dim('(' + providerName + ')')}`))
669
+ lines.push(highlight(` ${medal} ${chalk.bold('#' + (i + 1))} ${themeColors.textBold(r.label)} ${chalk.dim('(' + providerName + ')')}`))
672
670
  lines.push(highlight(` Score: ${chalk.bold.greenBright(String(rec.score) + '/100')} │ Tier: ${tierFn(r.tier)} │ SWE: ${chalk.cyan(sweStr)} │ Avg: ${chalk.yellow(avgStr)} │ CTX: ${chalk.cyan(ctxStr)} │ Stability: ${chalk.cyan(stabStr)}`))
673
671
  lines.push('')
674
672
  }
@@ -683,7 +681,7 @@ export function createOverlayRenderers(state, deps) {
683
681
  lines.push('')
684
682
  const { visible, offset } = sliceOverlayLines(lines, state.recommendScrollOffset, state.terminalRows)
685
683
  state.recommendScrollOffset = offset
686
- const tintedLines = tintOverlayLines(visible, RECOMMEND_OVERLAY_BG, state.terminalCols)
684
+ const tintedLines = tintOverlayLines(visible, themeColors.overlayBgRecommend, state.terminalCols)
687
685
  const cleared2 = tintedLines.map(l => l + EL)
688
686
  return cleared2.join('\n')
689
687
  }
@@ -34,7 +34,16 @@
34
34
  import chalk from 'chalk'
35
35
  import { createRequire } from 'module'
36
36
  import { sources } from '../sources.js'
37
- import { PING_INTERVAL, FRAMES } from './constants.js'
37
+ import {
38
+ TABLE_FIXED_LINES,
39
+ COL_MODEL,
40
+ TIER_CYCLE,
41
+ msCell,
42
+ spinCell,
43
+ PING_INTERVAL,
44
+ FRAMES
45
+ } from './constants.js'
46
+ import { themeColors } from './theme.js'
38
47
  import { TIER_COLOR } from './tier-colors.js'
39
48
  import { getAvg, getVerdict, getUptime, getStabilityScore, getVersionStatusInfo } from './utils.js'
40
49
  import { usagePlaceholderForProvider } from './ping.js'
@@ -54,9 +63,15 @@ const ACTIVE_FILTER_BG_BY_TIER = {
54
63
  'C': [186, 104, 200],
55
64
  }
56
65
 
57
- // 📖 Vertical separator for columns – gentle dark orange
58
- const VERTICAL_SEPARATOR = chalk.rgb(255, 140, 0).dim('│');
59
- const COL_SEP = ` ${VERTICAL_SEPARATOR} `;
66
+ // 📖 Import UI configuration for consistent styling
67
+ import { VERTICAL_SEPARATOR, COLUMN_SPACING } from './ui-config.js';
68
+
69
+ // 📖 Column separator (vertical bar) is now defined in ui-config.js
70
+ // const VERTICAL_SEPARATOR = chalk.rgb(255, 140, 0).dim('│');
71
+ // const COL_SEP = ` ${VERTICAL_SEPARATOR} `; // Replaced by imported COLUMN_SPACING
72
+
73
+ // 📖 Column spacing is now defined in ui-config.js
74
+ const COL_SEP = COLUMN_SPACING;
60
75
 
61
76
  const require = createRequire(import.meta.url)
62
77
  const { version: LOCAL_VERSION } = require('../package.json')
@@ -550,12 +565,12 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
550
565
  const row = ' ' + num + COL_SEP + tier + COL_SEP + sweCell + COL_SEP + ctxCell + COL_SEP + nameCell + COL_SEP + sourceCell + COL_SEP + pingCell + COL_SEP + avgCell + COL_SEP + status + COL_SEP + speedCell + COL_SEP + stabCell + COL_SEP + uptimeCell + COL_SEP + tokensCell
551
566
 
552
567
  if (isCursor) {
553
- lines.push(chalk.bgRgb(155, 55, 135)(row))
568
+ lines.push(themeColors.bgModelCursor(row))
554
569
  } else if (r.isRecommended) {
555
570
  // 📖 Medium green background for recommended models (distinguishable from favorites)
556
- lines.push(chalk.bgRgb(15, 40, 15)(row))
571
+ lines.push(themeColors.bgModelRecommended(row))
557
572
  } else if (r.isFavorite) {
558
- lines.push(chalk.bgRgb(88, 64, 10)(row))
573
+ lines.push(themeColors.bgModelFavorite(row))
559
574
  } else {
560
575
  lines.push(row)
561
576
  }
package/src/theme.js ADDED
@@ -0,0 +1,67 @@
1
+ /**
2
+ * @file theme.js
3
+ * @description Dynamic light/dark theme detector and semantic colour mappings.
4
+ */
5
+
6
+ import chalk from 'chalk'
7
+ import { execSync } from 'child_process'
8
+
9
+ let activeTheme = 'dark'
10
+
11
+ export function detectActiveTheme(configTheme) {
12
+ if (configTheme === 'dark' || configTheme === 'light') {
13
+ activeTheme = configTheme;
14
+ return activeTheme;
15
+ }
16
+
17
+ // Auto detect
18
+ const fgbg = process.env.COLORFGBG || '';
19
+ if (fgbg.includes(';15') || fgbg.includes(';base03')) {
20
+ activeTheme = 'light';
21
+ return activeTheme;
22
+ } else if (fgbg) {
23
+ activeTheme = 'dark';
24
+ return activeTheme;
25
+ }
26
+
27
+ if (process.platform === 'darwin') {
28
+ try {
29
+ const style = execSync('defaults read -g AppleInterfaceStyle 2>/dev/null', { timeout: 100 }).toString().trim();
30
+ activeTheme = style === 'Dark' ? 'dark' : 'light';
31
+ } catch {
32
+ activeTheme = 'light';
33
+ }
34
+ } else {
35
+ activeTheme = 'dark';
36
+ }
37
+
38
+ return activeTheme;
39
+ }
40
+
41
+ export function getTheme() {
42
+ return activeTheme;
43
+ }
44
+
45
+ // Semantic colors
46
+ export const themeColors = {
47
+ text: (str) => activeTheme === 'light' ? chalk.black(str) : chalk.white(str),
48
+ textBold: (str) => activeTheme === 'light' ? chalk.black.bold(str) : chalk.white.bold(str),
49
+ dim: (str) => activeTheme === 'light' ? chalk.gray(str) : chalk.dim(str),
50
+ dimYellow: (str) => activeTheme === 'light' ? chalk.rgb(180, 150, 0)(str) : chalk.dim.yellow(str),
51
+ bgCursor: (str) => activeTheme === 'light' ? chalk.bgRgb(220, 220, 240).black(str) : chalk.bgRgb(30, 30, 60)(str),
52
+ bgCursorInstall: (str) => activeTheme === 'light' ? chalk.bgRgb(220, 220, 240).black(str) : chalk.bgRgb(24, 44, 62)(str),
53
+ bgCursorSettingsList: (str) => activeTheme === 'light' ? chalk.bgRgb(220, 240, 220).black(str) : chalk.bgRgb(30, 45, 30)(str),
54
+ bgCursorLegacy: (str) => activeTheme === 'light' ? chalk.bgRgb(240, 220, 240).black(str) : chalk.bgRgb(55, 25, 55)(str),
55
+
56
+ bgModelCursor: (str) => activeTheme === 'light' ? chalk.bgRgb(230, 210, 230).black(str) : chalk.bgRgb(155, 55, 135)(str),
57
+ bgModelRecommended: (str) => activeTheme === 'light' ? chalk.bgRgb(200, 240, 200).black(str) : chalk.bgRgb(15, 40, 15)(str),
58
+ bgModelFavorite: (str) => activeTheme === 'light' ? chalk.bgRgb(250, 230, 190).black(str) : chalk.bgRgb(88, 64, 10)(str),
59
+
60
+ overlayBgSettings: (str) => activeTheme === 'light' ? chalk.bgRgb(245, 245, 250).black(str) : chalk.bgRgb(0, 0, 0).white(str),
61
+ overlayBgHelp: (str) => activeTheme === 'light' ? chalk.bgRgb(250, 250, 250).black(str) : chalk.bgRgb(0, 0, 0).white(str),
62
+ overlayBgRecommend: (str) => activeTheme === 'light' ? chalk.bgRgb(240, 250, 245).black(str) : chalk.bgRgb(0, 0, 0).white(str),
63
+ overlayBgFeedback: (str) => activeTheme === 'light' ? chalk.bgRgb(255, 245, 245).black(str) : chalk.bgRgb(46, 20, 20).white(str),
64
+
65
+ // Header badges text color override
66
+ badgeText: (str) => activeTheme === 'light' ? chalk.rgb(255, 255, 255)(str) : chalk.rgb(0, 0, 0)(str),
67
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * @file ui-config.js
3
+ * @description Central configuration for TUI visual styling.
4
+ *
5
+ * @details
6
+ * This module centralizes all visual styling constants for the TUI interface.
7
+ * By keeping colors, separators, and spacing in one place, it becomes easy to
8
+ * customize the look and feel without modifying rendering logic.
9
+ *
10
+ * 📖 Configuration:
11
+ * - BORDER_COLOR: Color of column separators (vertical bars)
12
+ * - BORDER_STYLE: Style of separators (dim, bold, etc.)
13
+ * - HORIZONTAL_SEPARATOR: Character used for horizontal lines
14
+ * - HORIZONTAL_STYLE: Style of horizontal lines
15
+ * - COLUMN_SPACING: Space between columns
16
+ *
17
+ * @see render-table.js - uses these constants for rendering
18
+ * @see tier-colors.js - for tier-specific color definitions
19
+ */
20
+
21
+ import chalk from 'chalk';
22
+
23
+ // 📖 Column separator (vertical bar) configuration
24
+ export const BORDER_COLOR = chalk.rgb(255, 140, 0); // Gentle dark orange
25
+ export const BORDER_STYLE = 'dim'; // Options: 'dim', 'bold', 'underline', 'inverse', etc.
26
+ export const VERTICAL_SEPARATOR = BORDER_COLOR[BORDER_STYLE]('│');
27
+
28
+ // 📖 Horizontal separator configuration
29
+ export const HORIZONTAL_SEPARATOR = '─'; // Unicode horizontal line
30
+ export const HORIZONTAL_STYLE = 'dim'; // Options: 'dim', 'bold', etc.
31
+ export const HORIZONTAL_LINE = chalk[HORIZONTAL_STYLE](HORIZONTAL_SEPARATOR);
32
+
33
+ // 📖 Column spacing configuration
34
+ export const COLUMN_SPACING = ` ${VERTICAL_SEPARATOR} `; // Space around vertical separator
35
+
36
+ // 📖 Optional: Add more UI styling constants here as needed
37
+ export const TABLE_PADDING = 1; // Padding around table edges
38
+
39
+ // 📖 Export all constants for easy import
40
+ export default {
41
+ BORDER_COLOR,
42
+ BORDER_STYLE,
43
+ VERTICAL_SEPARATOR,
44
+ HORIZONTAL_SEPARATOR,
45
+ HORIZONTAL_STYLE,
46
+ HORIZONTAL_LINE,
47
+ COLUMN_SPACING,
48
+ TABLE_PADDING
49
+ };