free-coding-models 0.3.23 → 0.3.24
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 +35 -0
- package/README.md +72 -17
- package/package.json +1 -1
- package/sources.js +60 -0
- package/src/app.js +22 -5
- package/src/command-palette.js +3 -1
- package/src/endpoint-installer.js +2 -1
- package/src/key-handler.js +197 -5
- package/src/overlays.js +98 -1
- package/src/provider-metadata.js +25 -0
- package/src/render-helpers.js +1 -1
- package/src/render-table.js +49 -3
- package/src/theme.js +6 -0
- package/src/tool-bootstrap.js +22 -0
- package/src/tool-launchers.js +93 -2
- package/src/tool-metadata.js +94 -11
- package/src/utils.js +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,41 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
---
|
|
3
3
|
|
|
4
|
+
## [0.3.24] - 2026-03-19
|
|
5
|
+
|
|
6
|
+
### Added
|
|
7
|
+
- **Unique emoji per tool** — Every CLI tool now has a dedicated emoji shown in the Compatible column, Z-cycle badge, command palette, help overlay, and README (📦 OpenCode, 🦞 OpenClaw, 💘 Crush, 🪿 Goose, π Pi, 🛠 Aider, 🐉 Qwen, 🤲 OpenHands, ⚡ Amp, 🦘 Rovo, ♊ Gemini)
|
|
8
|
+
- **Merged compat column** — OpenCode CLI and Desktop share 📦 in a single slot (11 slots instead of 12 separate initials)
|
|
9
|
+
- **COMPAT_COLUMN_SLOTS** — New export in tool-metadata.js for merged compatible-column rendering
|
|
10
|
+
- **Width warning now always shows** - Terminal width warning displays every time terminal is resized below 80 columns (previously limited to 2 shows per session)
|
|
11
|
+
- **Gemini CLI integration** - New CLI-only tool provider with 3 models (Gemini 3 Pro 🆕, Gemini 2.5 Pro, Gemini 2.5 Flash)
|
|
12
|
+
- **Rovo Dev CLI integration** - New CLI-only tool provider with Claude Sonnet 4 🆕
|
|
13
|
+
- **Tool compatibility alerts** - When trying to launch Rovo/Gemini models with wrong tool, shows alert and offers to switch
|
|
14
|
+
- **Auto-install detection** - Prompt to install CLI tools when binary not found (Rovo/Gemini)
|
|
15
|
+
- **OpenAI-compatible API support for Gemini** - Gemini CLI can use custom providers via environment variables
|
|
16
|
+
- **New CLI flags** - Added `--rovo` and `--gemini` launch options
|
|
17
|
+
- **"🆕" badges** - Mark newly added models in the table (Claude Sonnet 4, Gemini 3 Pro)
|
|
18
|
+
- **OpenCode Zen free models** - 5 new free models (Big Pickle, GPT 5 Nano, MiMo V2 Flash Free, MiniMax M2.5 Free, Nemotron 3 Super Free) exclusive to OpenCode CLI/Desktop via `opencode-zen` provider
|
|
19
|
+
- **"Compatible with" column** - New TUI column showing colored emojis for each tool a model supports; incompatible tools show dim spaces
|
|
20
|
+
- **Tool color system** - Each of the 12 supported tools now has a unique RGB color and emoji used in the Z-cycle badge and compatibility column
|
|
21
|
+
- **Incompatible model highlighting** - When a tool mode is active (via Z), models that can't run with that tool get a dark red background for instant visibility — they stay in their normal sorted position (not pushed to the bottom)
|
|
22
|
+
- **Tool compatibility functions** - `getCompatibleTools()` and `isModelCompatibleWithTool()` in tool-metadata.js for programmatic compatibility checks
|
|
23
|
+
- **Incompatible model fallback overlay** - When pressing Enter on a model that can't run on the active tool (red-highlighted row), an in-TUI overlay appears with two options: (1) switch to a compatible tool, or (2) pick a similar model by SWE score that works with the current tool
|
|
24
|
+
- **findSimilarCompatibleModels()** - New function in tool-metadata.js that finds models with closest SWE scores compatible with the active tool
|
|
25
|
+
- **Updated provider/model counts** - Now 23 providers with 171 models (was 20/160)
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
- **Z key cycle** - Rovo and Gemini added to tool mode cycle (last in order)
|
|
29
|
+
- **Tool metadata** - Removed `initial` field (replaced by emojis), added `cliOnly` flag for CLI-only tools, `emoji` and `color` properties for all 12 tools
|
|
30
|
+
- **Provider metadata** - Added Rovo, Gemini, and OpenCode Zen provider information
|
|
31
|
+
- **Responsive column hiding** - Compatible column hides first (before Rank) on narrow terminals
|
|
32
|
+
- **Key handler** - Zen models auto-switch to OpenCode CLI on launch; API key warnings skip Zen models
|
|
33
|
+
- **Documentation** - Updated README with CLI-only tools section, Zen models, compatibility matrix
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
- **Missing import error** - Fixed `getToolMeta` not defined in key-handler.js
|
|
37
|
+
- **CLI-only tools API key requirement** - Gemini CLI and Rovo Dev CLI no longer require API keys to be configured before launching; these tools manage their own authentication
|
|
38
|
+
|
|
4
39
|
## 0.3.23
|
|
5
40
|
|
|
6
41
|
### Added
|
package/README.md
CHANGED
|
@@ -2,15 +2,15 @@
|
|
|
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-
|
|
6
|
-
<img src="https://img.shields.io/badge/providers-
|
|
5
|
+
<img src="https://img.shields.io/badge/models-174-76b900?logo=nvidia" alt="models count">
|
|
6
|
+
<img src="https://img.shields.io/badge/providers-23-blue" alt="providers count">
|
|
7
7
|
</p>
|
|
8
8
|
|
|
9
9
|
<h1 align="center">free-coding-models</h1>
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
12
12
|
<strong>Find the fastest free coding model in seconds</strong><br>
|
|
13
|
-
<sub>Ping
|
|
13
|
+
<sub>Ping 174 models across 23 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, π Pi, 🦘 Rovo or ♊ Gemini in one keystroke</sub>
|
|
14
14
|
</p>
|
|
15
15
|
|
|
16
16
|
|
|
@@ -47,7 +47,7 @@ create a free account on one of the [providers](#-list-of-free-ai-providers)
|
|
|
47
47
|
|
|
48
48
|
## 💡 Why this tool?
|
|
49
49
|
|
|
50
|
-
There are **
|
|
50
|
+
There are **174+ free coding models** scattered across 23 providers. Which one is fastest right now? Which one is actually stable versus just lucky on the last ping?
|
|
51
51
|
|
|
52
52
|
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%).
|
|
53
53
|
|
|
@@ -61,7 +61,7 @@ It then writes the model you pick directly into your coding tool's config — so
|
|
|
61
61
|
|
|
62
62
|
Create a free account on one provider below to get started:
|
|
63
63
|
|
|
64
|
-
**
|
|
64
|
+
**174 coding models** across 23 providers, ranked by [SWE-bench Verified](https://www.swebench.com).
|
|
65
65
|
|
|
66
66
|
| Provider | Models | Tier range | Free tier | Env var |
|
|
67
67
|
|----------|--------|-----------|-----------|--------|
|
|
@@ -85,6 +85,9 @@ Create a free account on one provider below to get started:
|
|
|
85
85
|
| [Cloudflare Workers AI](https://dash.cloudflare.com) | 6 | S → B | Free: 10k neurons/day, text-gen 300 RPM | `CLOUDFLARE_API_TOKEN` + `CLOUDFLARE_ACCOUNT_ID` |
|
|
86
86
|
| [Perplexity API](https://www.perplexity.ai/settings/api) | 4 | A+ → B | Tiered limits by spend (default ~50 RPM) | `PERPLEXITY_API_KEY` |
|
|
87
87
|
| [Replicate](https://replicate.com/account/api-tokens) | 1 | A- | 6 req/min (no payment) – up to 3,000 RPM with payment | `REPLICATE_API_TOKEN` |
|
|
88
|
+
| [Rovo Dev CLI](https://www.atlassian.com/rovo) | 1 | S+ | 5M tokens/day (beta) | CLI tool 🦘 |
|
|
89
|
+
| [Gemini CLI](https://github.com/google-gemini/gemini-cli) | 3 | S+ → A+ | 1,000 req/day | CLI tool ♊ |
|
|
90
|
+
| [OpenCode Zen](https://opencode.ai/zen) | 8 | S+ → A+ | Free with OpenCode account | Zen models ✨ |
|
|
88
91
|
|
|
89
92
|
> 💡 One key is enough. Add more at any time with **`P`** inside the TUI.
|
|
90
93
|
|
|
@@ -121,7 +124,7 @@ Need to fix contrast because your terminal theme is fighting the TUI? Press **`G
|
|
|
121
124
|
↑↓ navigate → Enter to launch
|
|
122
125
|
```
|
|
123
126
|
|
|
124
|
-
The model you select is automatically written into your tool's config (OpenCode, OpenClaw, Crush, etc.) and the tool opens immediately. Done.
|
|
127
|
+
The model you select is automatically written into your tool's config (📦 OpenCode, 🦞 OpenClaw, 💘 Crush, etc.) and the tool opens immediately. Done.
|
|
125
128
|
|
|
126
129
|
If the active CLI tool is missing, FCM now catches it before launch, offers a tiny Yes/No install prompt, installs the tool with its official global command, then resumes the same model launch automatically.
|
|
127
130
|
|
|
@@ -157,19 +160,69 @@ free-coding-models --openclaw --origin groq
|
|
|
157
160
|
|
|
158
161
|
| Flag | Launches |
|
|
159
162
|
|------|----------|
|
|
160
|
-
| `--opencode` | OpenCode CLI |
|
|
161
|
-
| `--opencode-desktop` | OpenCode Desktop |
|
|
162
|
-
| `--openclaw` | OpenClaw |
|
|
163
|
-
| `--crush` | Crush |
|
|
164
|
-
| `--goose` | Goose |
|
|
165
|
-
| `--aider` | Aider |
|
|
166
|
-
| `--qwen` | Qwen Code |
|
|
167
|
-
| `--openhands` | OpenHands |
|
|
168
|
-
| `--amp` | Amp |
|
|
169
|
-
| `--pi` | Pi |
|
|
163
|
+
| `--opencode` | 📦 OpenCode CLI |
|
|
164
|
+
| `--opencode-desktop` | 📦 OpenCode Desktop |
|
|
165
|
+
| `--openclaw` | 🦞 OpenClaw |
|
|
166
|
+
| `--crush` | 💘 Crush |
|
|
167
|
+
| `--goose` | 🪿 Goose |
|
|
168
|
+
| `--aider` | 🛠 Aider |
|
|
169
|
+
| `--qwen` | 🐉 Qwen Code |
|
|
170
|
+
| `--openhands` | 🤲 OpenHands |
|
|
171
|
+
| `--amp` | ⚡ Amp |
|
|
172
|
+
| `--pi` | π Pi |
|
|
173
|
+
| `--rovo` | 🦘 Rovo Dev CLI |
|
|
174
|
+
| `--gemini` | ♊ Gemini CLI |
|
|
170
175
|
|
|
171
176
|
Press **`Z`** in the TUI to cycle between tools without restarting.
|
|
172
177
|
|
|
178
|
+
### CLI-Only Tools
|
|
179
|
+
|
|
180
|
+
**🦘 Rovo Dev CLI**
|
|
181
|
+
- Provider: [Atlassian Rovo](https://www.atlassian.com/rovo)
|
|
182
|
+
- Install: [Installation Guide](https://support.atlassian.com/rovo/docs/install-and-run-rovo-dev-cli-on-your-device/)
|
|
183
|
+
- Free tier: 5M tokens/day (beta, requires Atlassian account)
|
|
184
|
+
- Model: Claude Sonnet 4 (72.7% SWE-bench)
|
|
185
|
+
- Launch: `free-coding-models --rovo` or press `Z` until Rovo mode
|
|
186
|
+
- Features: Jira/Confluence integration, MCP server support
|
|
187
|
+
|
|
188
|
+
**♊ Gemini CLI**
|
|
189
|
+
- Provider: [Google Gemini](https://github.com/google-gemini/gemini-cli)
|
|
190
|
+
- Install: `npm install -g @google/gemini-cli`
|
|
191
|
+
- Free tier: 1,000 requests/day (personal Google account, no credit card)
|
|
192
|
+
- Models: Gemini 3 Pro (76.2% SWE-bench), Gemini 2.5 Pro, Gemini 2.5 Flash
|
|
193
|
+
- Launch: `free-coding-models --gemini` or press `Z` until Gemini mode
|
|
194
|
+
- Features: OpenAI-compatible API support, MCP server support, Google Search grounding
|
|
195
|
+
|
|
196
|
+
**Note:** When launching these tools via `Z` key or command palette, if the current mode doesn't match the tool, you'll see a confirmation alert asking to switch to the correct tool before launching.
|
|
197
|
+
|
|
198
|
+
### OpenCode Zen Free Models
|
|
199
|
+
|
|
200
|
+
[OpenCode Zen](https://opencode.ai/zen) is a hosted AI gateway offering 8 free coding models exclusively through OpenCode CLI and OpenCode Desktop. These models are **not** available through other tools.
|
|
201
|
+
|
|
202
|
+
| Model | Tier | SWE-bench | Context |
|
|
203
|
+
|-------|------|-----------|---------|
|
|
204
|
+
| Big Pickle | S+ | 72.0% | 200k |
|
|
205
|
+
| MiniMax M2.5 Free | S+ | 80.2% | 200k |
|
|
206
|
+
| MiMo V2 Pro Free | S+ | 78.0% | 1M |
|
|
207
|
+
| MiMo V2 Omni Free | S | 64.0% | 128k |
|
|
208
|
+
| MiMo V2 Flash Free | S+ | 73.4% | 256k |
|
|
209
|
+
| Nemotron 3 Super Free | A+ | 52.0% | 128k |
|
|
210
|
+
| GPT 5 Nano | S | 65.0% | 128k |
|
|
211
|
+
| Trinity Large Preview Free | S | 62.0% | 128k |
|
|
212
|
+
|
|
213
|
+
To use Zen models: sign up at [opencode.ai/auth](https://opencode.ai/auth) and enter your Zen API key via `P` (Settings). Zen models appear in the main table and auto-switch to OpenCode CLI on launch.
|
|
214
|
+
|
|
215
|
+
### Tool Compatibility
|
|
216
|
+
|
|
217
|
+
The TUI shows a **"Compatible with"** column displaying colored emojis for each supported tool. When a tool mode is active (via `Z`), models incompatible with that tool are highlighted with a dark red background so you can instantly see which models work with your current tool.
|
|
218
|
+
|
|
219
|
+
| Model Type | Compatible Tools |
|
|
220
|
+
|------------|-----------------|
|
|
221
|
+
| Regular (NVIDIA, Groq, etc.) | All tools except 🦘 Rovo and ♊ Gemini |
|
|
222
|
+
| Rovo | 🦘 Rovo Dev CLI only |
|
|
223
|
+
| Gemini | ♊ Gemini CLI only |
|
|
224
|
+
| OpenCode Zen | 📦 OpenCode CLI and 📦 OpenCode Desktop only |
|
|
225
|
+
|
|
173
226
|
→ **[Full flags reference](./docs/flags.md)**
|
|
174
227
|
|
|
175
228
|
---
|
|
@@ -204,7 +257,7 @@ Press **`Z`** in the TUI to cycle between tools without restarting.
|
|
|
204
257
|
|
|
205
258
|
## ✨ Features
|
|
206
259
|
|
|
207
|
-
- **Parallel pings** — all
|
|
260
|
+
- **Parallel pings** — all 174 models tested simultaneously via native `fetch`
|
|
208
261
|
- **Adaptive monitoring** — 2s burst for 60s → 10s normal → 30s idle
|
|
209
262
|
- **Stability score** — composite 0–100 (p95 latency, jitter, spike rate, uptime)
|
|
210
263
|
- **Smart ranking** — top 3 highlighted 🥇🥈🥉
|
|
@@ -215,6 +268,8 @@ Press **`Z`** in the TUI to cycle between tools without restarting.
|
|
|
215
268
|
- **⚡️ Command Palette** — `Ctrl+P` opens a searchable action launcher for filters, sorting, overlays, and quick toggles
|
|
216
269
|
- **Install Endpoints** — push a full provider catalog into any tool's config (from Settings `P` or ⚡️ Command Palette)
|
|
217
270
|
- **Missing tool bootstrap** — detect absent CLIs, offer one-click install, then continue the selected launch automatically
|
|
271
|
+
- **Tool compatibility matrix** — colored emojis show which tools each model supports; incompatible rows highlighted in dark red when a tool mode is active
|
|
272
|
+
- **OpenCode Zen models** — 8 free models exclusive to OpenCode CLI/Desktop, powered by the Zen AI gateway
|
|
218
273
|
- **Width guardrail** — shows a warning instead of a broken table in narrow terminals
|
|
219
274
|
- **Readable everywhere** — semantic theme palette keeps table rows, overlays, badges, and help screens legible in dark and light terminals
|
|
220
275
|
- **Global theme switch** — `G` cycles `auto`, `dark`, and `light` live without restarting
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "free-coding-models",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.24",
|
|
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
|
@@ -344,6 +344,40 @@ export const iflow = [
|
|
|
344
344
|
['qwen3-max', 'Qwen3 Max', 'A+', '55.0%', '256k'],
|
|
345
345
|
]
|
|
346
346
|
|
|
347
|
+
// 📖 Rovo Dev CLI source - https://www.atlassian.com/rovo
|
|
348
|
+
// 📖 CLI tool only - no API endpoint - requires 'acli rovodev run'
|
|
349
|
+
// 📖 Install: https://support.atlassian.com/rovo/docs/install-and-run-rovo-dev-cli-on-your-device/
|
|
350
|
+
// 📖 Free tier: 5M tokens/day (beta) - Claude Sonnet 4 (72.7% SWE-bench)
|
|
351
|
+
// 📖 Requires Atlassian account + Rovo Dev activated on your site
|
|
352
|
+
export const rovo = [
|
|
353
|
+
['anthropic/claude-sonnet-4', 'Claude Sonnet 4 🆕', 'S+', '72.7%', '200k'],
|
|
354
|
+
]
|
|
355
|
+
|
|
356
|
+
// 📖 Gemini CLI source - https://github.com/google-gemini/gemini-cli
|
|
357
|
+
// 📖 CLI tool with OpenAI-compatible API support
|
|
358
|
+
// 📖 Install: npm install -g @google/gemini-cli
|
|
359
|
+
// 📖 Free tier: 1,000 req/day with personal Google account (no credit card)
|
|
360
|
+
// 📖 Models: Gemini 3 Pro (76.2% SWE-bench), Gemini 2.5 Pro, Gemini 2.5 Flash
|
|
361
|
+
// 📖 Supports custom OpenAI-compatible providers via GEMINI_API_BASE_URL
|
|
362
|
+
export const gemini = [
|
|
363
|
+
['google/gemini-3-pro', 'Gemini 3 Pro 🆕', 'S+', '76.2%', '1M'],
|
|
364
|
+
['google/gemini-2.5-pro', 'Gemini 2.5 Pro', 'S+', '63.2%', '1M'],
|
|
365
|
+
['google/gemini-2.5-flash', 'Gemini 2.5 Flash', 'A+', '50.0%', '1M'],
|
|
366
|
+
]
|
|
367
|
+
|
|
368
|
+
// 📖 OpenCode Zen free models — hosted AI gateway accessed through OpenCode CLI/Desktop
|
|
369
|
+
// 📖 Endpoint: https://opencode.ai/zen/v1/... — requires OpenCode Zen API key
|
|
370
|
+
// 📖 These models are FREE on the Zen platform and only run on OpenCode CLI or OpenCode Desktop
|
|
371
|
+
// 📖 Login: https://opencode.ai/auth — get your Zen API key
|
|
372
|
+
// 📖 Config: set provider to opencode/<model-id> in OpenCode config
|
|
373
|
+
export const opencodeZen = [
|
|
374
|
+
['big-pickle', 'Big Pickle 🆕', 'S+', '72.0%', '200k'],
|
|
375
|
+
['gpt-5-nano', 'GPT 5 Nano 🆕', 'S', '65.0%', '128k'],
|
|
376
|
+
['mimo-v2-flash-free', 'MiMo V2 Flash Free 🆕', 'S+', '73.4%', '256k'],
|
|
377
|
+
['minimax-m2.5-free', 'MiniMax M2.5 Free 🆕', 'S+', '80.2%', '200k'],
|
|
378
|
+
['nemotron-3-super-free', 'Nemotron 3 Super Free 🆕', 'A+', '52.0%', '128k'],
|
|
379
|
+
]
|
|
380
|
+
|
|
347
381
|
// 📖 All sources combined - used by the main script
|
|
348
382
|
// 📖 Each source has: name (display), url (API endpoint), models (array of model tuples)
|
|
349
383
|
export const sources = {
|
|
@@ -447,6 +481,32 @@ export const sources = {
|
|
|
447
481
|
url: 'https://apis.iflow.cn/v1/chat/completions',
|
|
448
482
|
models: iflow,
|
|
449
483
|
},
|
|
484
|
+
// 📖 CLI-only tools (no API endpoint - launched directly)
|
|
485
|
+
rovo: {
|
|
486
|
+
name: 'Rovo Dev CLI',
|
|
487
|
+
url: null, // CLI tool - no API endpoint
|
|
488
|
+
models: rovo,
|
|
489
|
+
cliOnly: true,
|
|
490
|
+
installUrl: 'https://support.atlassian.com/rovo/docs/install-and-run-rovo-dev-cli-on-your-device/',
|
|
491
|
+
binary: 'acli',
|
|
492
|
+
checkArgs: ['rovodev', '--help'],
|
|
493
|
+
},
|
|
494
|
+
gemini: {
|
|
495
|
+
name: 'Gemini CLI',
|
|
496
|
+
url: null, // CLI tool - no API endpoint (can use OpenAI-compatible via env)
|
|
497
|
+
models: gemini,
|
|
498
|
+
cliOnly: true,
|
|
499
|
+
installUrl: 'https://github.com/google-gemini/gemini-cli',
|
|
500
|
+
binary: 'gemini',
|
|
501
|
+
checkArgs: ['--version'],
|
|
502
|
+
},
|
|
503
|
+
// 📖 OpenCode Zen free models — hosted AI gateway, only runs on OpenCode CLI / Desktop
|
|
504
|
+
'opencode-zen': {
|
|
505
|
+
name: 'OpenCode Zen',
|
|
506
|
+
url: 'https://opencode.ai/zen/v1/chat/completions',
|
|
507
|
+
models: opencodeZen,
|
|
508
|
+
zenOnly: true,
|
|
509
|
+
},
|
|
450
510
|
}
|
|
451
511
|
|
|
452
512
|
// 📖 Flatten all models from all sources — each entry includes providerKey as 6th element
|
package/src/app.js
CHANGED
|
@@ -233,6 +233,8 @@ export async function runApp(cliArgs, config) {
|
|
|
233
233
|
openhands: cliArgs.openHandsMode,
|
|
234
234
|
amp: cliArgs.ampMode,
|
|
235
235
|
pi: cliArgs.piMode,
|
|
236
|
+
rovo: cliArgs.rovoMode,
|
|
237
|
+
gemini: cliArgs.geminiMode,
|
|
236
238
|
}
|
|
237
239
|
return flagByMode[toolMode] === true
|
|
238
240
|
})
|
|
@@ -389,7 +391,7 @@ export async function runApp(cliArgs, config) {
|
|
|
389
391
|
terminalCols: process.stdout.columns || 80, // 📖 Current terminal width
|
|
390
392
|
widthWarningStartedAt: (process.stdout.columns || 80) < WIDTH_WARNING_MIN_COLS ? now : null, // 📖 Start immediately in very narrow viewports.
|
|
391
393
|
widthWarningDismissed: false, // 📖 Esc hides the narrow-terminal warning early for the current narrow-width session.
|
|
392
|
-
widthWarningShowCount: 0, // 📖
|
|
394
|
+
widthWarningShowCount: 0, // 📖 No longer used — kept for backward compatibility. Warning now shows every time terminal is too small.
|
|
393
395
|
// 📖 Settings screen state (P key opens it)
|
|
394
396
|
settingsOpen: false, // 📖 Whether settings overlay is active
|
|
395
397
|
settingsCursor: 0, // 📖 Which provider row is selected in settings
|
|
@@ -434,6 +436,15 @@ export async function runApp(cliArgs, config) {
|
|
|
434
436
|
toolInstallPromptModel: null,
|
|
435
437
|
toolInstallPromptPlan: null,
|
|
436
438
|
toolInstallPromptErrorMsg: null,
|
|
439
|
+
// 📖 Incompatible model fallback overlay — shown when user presses Enter on a red-highlighted model.
|
|
440
|
+
// 📖 Offers two options: switch to a compatible tool, or pick a similar SWE-scored model.
|
|
441
|
+
incompatibleFallbackOpen: false,
|
|
442
|
+
incompatibleFallbackCursor: 0,
|
|
443
|
+
incompatibleFallbackScrollOffset: 0,
|
|
444
|
+
incompatibleFallbackModel: null, // 📖 The incompatible model the user tried to launch
|
|
445
|
+
incompatibleFallbackTools: [], // 📖 Compatible tools for the selected model
|
|
446
|
+
incompatibleFallbackSimilarModels: [], // 📖 Similar SWE models compatible with current tool
|
|
447
|
+
incompatibleFallbackSection: 'tools', // 📖 'tools' or 'models' — which section cursor is in
|
|
437
448
|
// 📖 Smart Recommend overlay state (Q key opens it)
|
|
438
449
|
recommendOpen: false, // 📖 Whether the recommend overlay is active
|
|
439
450
|
recommendPhase: 'questionnaire', // 📖 'questionnaire'|'analyzing'|'results' — current phase
|
|
@@ -472,7 +483,6 @@ export async function runApp(cliArgs, config) {
|
|
|
472
483
|
if (prevCols >= WIDTH_WARNING_MIN_COLS || state.widthWarningDismissed) {
|
|
473
484
|
state.widthWarningStartedAt = Date.now()
|
|
474
485
|
state.widthWarningDismissed = false
|
|
475
|
-
state.widthWarningShowCount++ // 📖 Increment counter when showing the warning again
|
|
476
486
|
} else if (!state.widthWarningStartedAt) {
|
|
477
487
|
state.widthWarningStartedAt = Date.now()
|
|
478
488
|
}
|
|
@@ -684,7 +694,11 @@ export async function runApp(cliArgs, config) {
|
|
|
684
694
|
r.hidden = false
|
|
685
695
|
return
|
|
686
696
|
}
|
|
687
|
-
|
|
697
|
+
// 📖 CLI-only tools (rovo, gemini) and Zen models don't need traditional API keys —
|
|
698
|
+
// 📖 they authenticate via their own CLI login flow, so "configured only" should never hide them.
|
|
699
|
+
const providerMeta = PROVIDER_METADATA[r.providerKey]
|
|
700
|
+
const noKeyNeeded = providerMeta?.cliOnly || providerMeta?.zenOnly
|
|
701
|
+
const unconfiguredHide = state.hideUnconfiguredModels && !noKeyNeeded && !getApiKey(state.config, r.providerKey)
|
|
688
702
|
if (unconfiguredHide) {
|
|
689
703
|
r.hidden = true
|
|
690
704
|
return
|
|
@@ -800,6 +814,7 @@ export async function runApp(cliArgs, config) {
|
|
|
800
814
|
startOpenCode,
|
|
801
815
|
startExternalTool,
|
|
802
816
|
getToolModeOrder,
|
|
817
|
+
getToolMeta,
|
|
803
818
|
getToolInstallPlan,
|
|
804
819
|
isToolInstalled,
|
|
805
820
|
installToolWithPlan,
|
|
@@ -862,7 +877,7 @@ export async function runApp(cliArgs, config) {
|
|
|
862
877
|
refreshAutoPingMode()
|
|
863
878
|
state.frame++
|
|
864
879
|
// 📖 Cache visible+sorted models each frame so Enter handler always matches the display
|
|
865
|
-
if (!state.settingsOpen && !state.installEndpointsOpen && !state.toolInstallPromptOpen && !state.recommendOpen && !state.feedbackOpen && !state.changelogOpen && !state.commandPaletteOpen) {
|
|
880
|
+
if (!state.settingsOpen && !state.installEndpointsOpen && !state.toolInstallPromptOpen && !state.incompatibleFallbackOpen && !state.recommendOpen && !state.feedbackOpen && !state.changelogOpen && !state.commandPaletteOpen) {
|
|
866
881
|
const visible = state.results.filter(r => !r.hidden)
|
|
867
882
|
state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection, {
|
|
868
883
|
pinFavorites: state.favoritesPinnedAndSticky,
|
|
@@ -945,6 +960,8 @@ export async function runApp(cliArgs, config) {
|
|
|
945
960
|
? overlays.renderInstallEndpoints()
|
|
946
961
|
: state.toolInstallPromptOpen
|
|
947
962
|
? overlays.renderToolInstallPrompt()
|
|
963
|
+
: state.incompatibleFallbackOpen
|
|
964
|
+
? overlays.renderIncompatibleFallback()
|
|
948
965
|
: state.commandPaletteOpen
|
|
949
966
|
? tableContent + overlays.renderCommandPalette()
|
|
950
967
|
: state.recommendOpen
|
|
@@ -975,7 +992,7 @@ export async function runApp(cliArgs, config) {
|
|
|
975
992
|
pinFavorites: state.favoritesPinnedAndSticky,
|
|
976
993
|
})
|
|
977
994
|
|
|
978
|
-
|
|
995
|
+
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))
|
|
979
996
|
if (process.stdout.isTTY) {
|
|
980
997
|
process.stdout.flush && process.stdout.flush()
|
|
981
998
|
}
|
package/src/command-palette.js
CHANGED
|
@@ -28,13 +28,15 @@ const TOOL_MODE_DESCRIPTIONS = {
|
|
|
28
28
|
qwen: 'Launch Qwen Code using the selected provider model.',
|
|
29
29
|
openhands: 'Launch OpenHands with the selected model endpoint.',
|
|
30
30
|
amp: 'Launch Amp with this model as active target.',
|
|
31
|
+
rovo: 'Rovo Dev CLI model (launch with Rovo tool only).',
|
|
32
|
+
gemini: 'Gemini CLI model (launch with Gemini tool only).',
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
const TOOL_MODE_COMMANDS = TOOL_MODE_ORDER.map((toolMode) => {
|
|
34
36
|
const meta = TOOL_METADATA[toolMode] || { label: toolMode, emoji: '🧰' }
|
|
35
37
|
return {
|
|
36
38
|
id: `action-set-tool-${toolMode}`,
|
|
37
|
-
label: meta.label
|
|
39
|
+
label: `${meta.emoji} ${meta.label}`,
|
|
38
40
|
toolMode,
|
|
39
41
|
icon: meta.emoji,
|
|
40
42
|
description: TOOL_MODE_DESCRIPTIONS[toolMode] || 'Set this as the active launch target.',
|
|
@@ -48,7 +48,8 @@ import { getApiKey, saveConfig } from './config.js'
|
|
|
48
48
|
import { ENV_VAR_NAMES, PROVIDER_METADATA } from './provider-metadata.js'
|
|
49
49
|
import { getToolMeta } from './tool-metadata.js'
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
// 📖 CLI-only providers (rovo, gemini) and Zen-only (opencode-zen) cannot be installed into other tools.
|
|
52
|
+
const DIRECT_INSTALL_UNSUPPORTED_PROVIDERS = new Set(['replicate', 'zai', 'rovo', 'gemini', 'opencode-zen'])
|
|
52
53
|
// 📖 Install Endpoints only lists tools whose persisted config shape is actually supported here.
|
|
53
54
|
// 📖 Claude Code, Codex, and Gemini stay out while their dedicated bridges are being rebuilt.
|
|
54
55
|
const INSTALL_TARGET_MODES = ['opencode', 'opencode-desktop', 'openclaw', 'crush', 'goose', 'pi', 'aider', 'qwen', 'openhands', 'amp']
|
package/src/key-handler.js
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
30
|
import { loadChangelog } from './changelog-loader.js'
|
|
31
|
+
import { getToolMeta, isModelCompatibleWithTool, getCompatibleTools, findSimilarCompatibleModels } from './tool-metadata.js'
|
|
31
32
|
import { loadConfig, replaceConfigContents } from './config.js'
|
|
32
33
|
import { cleanupLegacyProxyArtifacts } from './legacy-proxy-cleanup.js'
|
|
33
34
|
import { cycleThemeSetting, detectActiveTheme } from './theme.js'
|
|
@@ -247,9 +248,61 @@ export function createKeyHandler(ctx) {
|
|
|
247
248
|
}
|
|
248
249
|
console.log()
|
|
249
250
|
|
|
250
|
-
// 📖
|
|
251
|
-
// 📖
|
|
252
|
-
|
|
251
|
+
// 📖 CLI-only tool compatibility checks:
|
|
252
|
+
// 📖 Case A: Active tool mode is CLI-only (rovo/gemini) but selected model doesn't belong to it
|
|
253
|
+
// 📖 Case B: Selected model belongs to a CLI-only provider but active mode is something else
|
|
254
|
+
// 📖 Case C: Selected model is from opencode-zen but active mode is not opencode/opencode-desktop
|
|
255
|
+
const activeMeta = getToolMeta(state.mode)
|
|
256
|
+
const isActiveModeCliOnly = activeMeta.cliOnly === true
|
|
257
|
+
const isModelFromCliOnly = selected.providerKey === 'rovo' || selected.providerKey === 'gemini'
|
|
258
|
+
const isModelFromZen = selected.providerKey === 'opencode-zen'
|
|
259
|
+
const modelBelongsToActiveMode = selected.providerKey === state.mode
|
|
260
|
+
|
|
261
|
+
// 📖 Case A: User is in Rovo/Gemini mode but selected a model from a different provider
|
|
262
|
+
if (isActiveModeCliOnly && !modelBelongsToActiveMode) {
|
|
263
|
+
const availableModels = MODELS.filter(m => m[5] === state.mode)
|
|
264
|
+
console.log(chalk.yellow(` ⚠ ${activeMeta.label} can only launch its own models.`))
|
|
265
|
+
console.log(chalk.yellow(` "${selected.label}" is not a ${activeMeta.label} model.`))
|
|
266
|
+
console.log()
|
|
267
|
+
if (availableModels.length > 0) {
|
|
268
|
+
console.log(chalk.cyan(` Available ${activeMeta.label} models:`))
|
|
269
|
+
for (const m of availableModels) {
|
|
270
|
+
console.log(chalk.white(` • ${m[1]} (${m[2]} tier, ${m[3]} SWE, ${m[4]} ctx)`))
|
|
271
|
+
}
|
|
272
|
+
console.log()
|
|
273
|
+
}
|
|
274
|
+
console.log(chalk.dim(` Switch to another tool mode with Z, or select a ${activeMeta.label} model.`))
|
|
275
|
+
console.log()
|
|
276
|
+
process.exit(0)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// 📖 Case B: Selected model is from a CLI-only provider but active mode is different
|
|
280
|
+
if (isModelFromCliOnly && !modelBelongsToActiveMode) {
|
|
281
|
+
const modelMeta = getToolMeta(selected.providerKey)
|
|
282
|
+
console.log(chalk.yellow(` ⚠ ${selected.label} is a ${modelMeta.label}-exclusive model.`))
|
|
283
|
+
console.log(chalk.yellow(` Your current tool is: ${activeMeta.label}`))
|
|
284
|
+
console.log()
|
|
285
|
+
console.log(chalk.cyan(` Switching to ${modelMeta.label} and launching...`))
|
|
286
|
+
setToolMode(selected.providerKey)
|
|
287
|
+
console.log(chalk.green(` ✓ Switched to ${modelMeta.label}`))
|
|
288
|
+
console.log()
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// 📖 Case C: Zen model selected but active mode is not OpenCode CLI / OpenCode Desktop
|
|
292
|
+
// 📖 Auto-switch to OpenCode CLI since Zen models only run on OpenCode
|
|
293
|
+
if (isModelFromZen && state.mode !== 'opencode' && state.mode !== 'opencode-desktop') {
|
|
294
|
+
console.log(chalk.yellow(` ⚠ ${selected.label} is an OpenCode Zen model.`))
|
|
295
|
+
console.log(chalk.yellow(` Zen models only run on OpenCode CLI or OpenCode Desktop.`))
|
|
296
|
+
console.log(chalk.yellow(` Your current tool is: ${activeMeta.label}`))
|
|
297
|
+
console.log()
|
|
298
|
+
console.log(chalk.cyan(` Switching to OpenCode CLI and launching...`))
|
|
299
|
+
setToolMode('opencode')
|
|
300
|
+
console.log(chalk.green(` ✓ Switched to OpenCode CLI`))
|
|
301
|
+
console.log()
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// 📖 OpenClaw, CLI-only tools, and Zen models manage auth differently — skip API key warning for them.
|
|
305
|
+
if (state.mode !== 'openclaw' && !isModelFromCliOnly && !isModelFromZen) {
|
|
253
306
|
const selectedApiKey = getApiKey(state.config, selected.providerKey)
|
|
254
307
|
if (!selectedApiKey) {
|
|
255
308
|
console.log(chalk.yellow(` Warning: No API key configured for ${selected.providerKey}.`))
|
|
@@ -259,6 +312,41 @@ export function createKeyHandler(ctx) {
|
|
|
259
312
|
}
|
|
260
313
|
}
|
|
261
314
|
|
|
315
|
+
// 📖 CLI-only tool auto-install check — verify the CLI binary is available before launch.
|
|
316
|
+
const toolModeForProvider = selected.providerKey
|
|
317
|
+
if (isModelFromCliOnly && !isToolInstalled(toolModeForProvider)) {
|
|
318
|
+
const installPlan = getToolInstallPlan(toolModeForProvider)
|
|
319
|
+
if (installPlan.supported) {
|
|
320
|
+
console.log()
|
|
321
|
+
console.log(chalk.yellow(` ⚠ ${getToolMeta(toolModeForProvider).label} is not installed.`))
|
|
322
|
+
console.log(chalk.dim(` ${installPlan.summary}`))
|
|
323
|
+
if (installPlan.note) console.log(chalk.dim(` Note: ${installPlan.note}`))
|
|
324
|
+
console.log()
|
|
325
|
+
console.log(chalk.cyan(` 📦 Auto-installing ${getToolMeta(toolModeForProvider).label}...`))
|
|
326
|
+
console.log()
|
|
327
|
+
|
|
328
|
+
const installResult = await installToolWithPlan(installPlan)
|
|
329
|
+
if (!installResult.ok) {
|
|
330
|
+
console.log(chalk.red(` X Tool installation failed with exit code ${installResult.exitCode}.`))
|
|
331
|
+
if (installPlan.docsUrl) console.log(chalk.dim(` Docs: ${installPlan.docsUrl}`))
|
|
332
|
+
console.log()
|
|
333
|
+
process.exit(installResult.exitCode || 1)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// 📖 Verify tool is now installed
|
|
337
|
+
if (!isToolInstalled(toolModeForProvider)) {
|
|
338
|
+
console.log(chalk.yellow(' ⚠ The installer finished, but the tool is still not reachable from this terminal session.'))
|
|
339
|
+
console.log(chalk.dim(' Restart your shell or add the tool bin directory to PATH, then retry the launch.'))
|
|
340
|
+
if (installPlan.docsUrl) console.log(chalk.dim(` Docs: ${installPlan.docsUrl}`))
|
|
341
|
+
console.log()
|
|
342
|
+
process.exit(1)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
console.log(chalk.green(' ✓ Tool installed successfully. Continuing with the selected model...'))
|
|
346
|
+
console.log()
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
262
350
|
let exitCode = 0
|
|
263
351
|
if (state.mode === 'openclaw') {
|
|
264
352
|
exitCode = await startOpenClaw(userSelected, state.config, { launchCli: true })
|
|
@@ -1200,6 +1288,85 @@ export function createKeyHandler(ctx) {
|
|
|
1200
1288
|
return
|
|
1201
1289
|
}
|
|
1202
1290
|
|
|
1291
|
+
// 📖 Incompatible fallback overlay: ↑↓ navigate across tool + model sections, Enter confirms, Esc cancels.
|
|
1292
|
+
// 📖 Cursor is a flat index: 0..N-1 = compatible tools, N..N+M-1 = similar models.
|
|
1293
|
+
if (state.incompatibleFallbackOpen) {
|
|
1294
|
+
if (key.ctrl && key.name === 'c') { exit(0); return }
|
|
1295
|
+
|
|
1296
|
+
const tools = state.incompatibleFallbackTools || []
|
|
1297
|
+
const similarModels = state.incompatibleFallbackSimilarModels || []
|
|
1298
|
+
const totalItems = tools.length + similarModels.length
|
|
1299
|
+
|
|
1300
|
+
if (key.name === 'escape') {
|
|
1301
|
+
// 📖 Close the overlay and go back to the main table
|
|
1302
|
+
state.incompatibleFallbackOpen = false
|
|
1303
|
+
state.incompatibleFallbackCursor = 0
|
|
1304
|
+
state.incompatibleFallbackScrollOffset = 0
|
|
1305
|
+
state.incompatibleFallbackModel = null
|
|
1306
|
+
state.incompatibleFallbackTools = []
|
|
1307
|
+
state.incompatibleFallbackSimilarModels = []
|
|
1308
|
+
state.incompatibleFallbackSection = 'tools'
|
|
1309
|
+
return
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
if (key.name === 'up' && totalItems > 0) {
|
|
1313
|
+
state.incompatibleFallbackCursor = state.incompatibleFallbackCursor > 0
|
|
1314
|
+
? state.incompatibleFallbackCursor - 1
|
|
1315
|
+
: totalItems - 1
|
|
1316
|
+
state.incompatibleFallbackSection = state.incompatibleFallbackCursor < tools.length ? 'tools' : 'models'
|
|
1317
|
+
return
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
if (key.name === 'down' && totalItems > 0) {
|
|
1321
|
+
state.incompatibleFallbackCursor = state.incompatibleFallbackCursor < totalItems - 1
|
|
1322
|
+
? state.incompatibleFallbackCursor + 1
|
|
1323
|
+
: 0
|
|
1324
|
+
state.incompatibleFallbackSection = state.incompatibleFallbackCursor < tools.length ? 'tools' : 'models'
|
|
1325
|
+
return
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
if (key.name === 'return' && totalItems > 0) {
|
|
1329
|
+
const cursor = state.incompatibleFallbackCursor
|
|
1330
|
+
const fallbackModel = state.incompatibleFallbackModel
|
|
1331
|
+
|
|
1332
|
+
// 📖 Close overlay state first
|
|
1333
|
+
state.incompatibleFallbackOpen = false
|
|
1334
|
+
state.incompatibleFallbackCursor = 0
|
|
1335
|
+
state.incompatibleFallbackScrollOffset = 0
|
|
1336
|
+
state.incompatibleFallbackModel = null
|
|
1337
|
+
state.incompatibleFallbackTools = []
|
|
1338
|
+
state.incompatibleFallbackSimilarModels = []
|
|
1339
|
+
state.incompatibleFallbackSection = 'tools'
|
|
1340
|
+
|
|
1341
|
+
if (cursor < tools.length) {
|
|
1342
|
+
// 📖 Section 1: Switch to the selected compatible tool, then launch the original model
|
|
1343
|
+
const selectedToolKey = tools[cursor]
|
|
1344
|
+
setToolMode(selectedToolKey)
|
|
1345
|
+
// 📖 Find the full result object for the original model to pass to launchSelectedModel
|
|
1346
|
+
const fullModel = state.results.find(
|
|
1347
|
+
r => r.providerKey === fallbackModel.providerKey && r.modelId === fallbackModel.modelId
|
|
1348
|
+
)
|
|
1349
|
+
if (fullModel) {
|
|
1350
|
+
await launchSelectedModel(fullModel)
|
|
1351
|
+
}
|
|
1352
|
+
} else {
|
|
1353
|
+
// 📖 Section 2: Launch the selected similar model instead
|
|
1354
|
+
const modelIdx = cursor - tools.length
|
|
1355
|
+
const selectedSimilar = similarModels[modelIdx]
|
|
1356
|
+
if (selectedSimilar) {
|
|
1357
|
+
const fullModel = state.results.find(
|
|
1358
|
+
r => r.providerKey === selectedSimilar.providerKey && r.modelId === selectedSimilar.modelId
|
|
1359
|
+
)
|
|
1360
|
+
if (fullModel) {
|
|
1361
|
+
await launchSelectedModel(fullModel)
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
return
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1203
1370
|
// 📖 Feedback overlay: intercept ALL keys while overlay is active.
|
|
1204
1371
|
// 📖 Enter → send to Discord, Esc → cancel, Backspace → delete char, printable → append to buffer.
|
|
1205
1372
|
if (state.feedbackOpen) {
|
|
@@ -1809,8 +1976,6 @@ export function createKeyHandler(ctx) {
|
|
|
1809
1976
|
|
|
1810
1977
|
// 📖 Profile system removed - API keys now persist permanently across all sessions
|
|
1811
1978
|
|
|
1812
|
-
// 📖 Profile system removed - API keys now persist permanently across all sessions
|
|
1813
|
-
|
|
1814
1979
|
// 📖 Shift+R: reset all UI view settings to defaults (tier, sort, provider) and clear persisted config
|
|
1815
1980
|
if (key.name === 'r' && key.shift) {
|
|
1816
1981
|
resetViewSettings()
|
|
@@ -1944,6 +2109,33 @@ export function createKeyHandler(ctx) {
|
|
|
1944
2109
|
// 📖 Use the cached visible+sorted array — guaranteed to match what's on screen
|
|
1945
2110
|
const selected = state.visibleSorted[state.cursor]
|
|
1946
2111
|
if (!selected) return // 📖 Guard: empty visible list (all filtered out)
|
|
2112
|
+
|
|
2113
|
+
// 📖 Incompatibility intercept — if the model can't run on the active tool,
|
|
2114
|
+
// 📖 show the fallback overlay instead of launching. Lets user switch tool or pick similar model.
|
|
2115
|
+
if (!isModelCompatibleWithTool(selected.providerKey, state.mode)) {
|
|
2116
|
+
const compatTools = getCompatibleTools(selected.providerKey)
|
|
2117
|
+
const similarModels = findSimilarCompatibleModels(
|
|
2118
|
+
selected.sweScore || '-',
|
|
2119
|
+
state.mode,
|
|
2120
|
+
state.results.filter(r => r.providerKey !== selected.providerKey || r.modelId !== selected.modelId),
|
|
2121
|
+
3
|
|
2122
|
+
)
|
|
2123
|
+
state.incompatibleFallbackOpen = true
|
|
2124
|
+
state.incompatibleFallbackCursor = 0
|
|
2125
|
+
state.incompatibleFallbackScrollOffset = 0
|
|
2126
|
+
state.incompatibleFallbackModel = {
|
|
2127
|
+
modelId: selected.modelId,
|
|
2128
|
+
label: selected.label,
|
|
2129
|
+
tier: selected.tier,
|
|
2130
|
+
providerKey: selected.providerKey,
|
|
2131
|
+
sweScore: selected.sweScore || '-',
|
|
2132
|
+
}
|
|
2133
|
+
state.incompatibleFallbackTools = compatTools
|
|
2134
|
+
state.incompatibleFallbackSimilarModels = similarModels
|
|
2135
|
+
state.incompatibleFallbackSection = 'tools'
|
|
2136
|
+
return
|
|
2137
|
+
}
|
|
2138
|
+
|
|
1947
2139
|
if (shouldCheckMissingTool(state.mode) && !isToolInstalled(state.mode)) {
|
|
1948
2140
|
state.toolInstallPromptOpen = true
|
|
1949
2141
|
state.toolInstallPromptCursor = 0
|
package/src/overlays.js
CHANGED
|
@@ -764,7 +764,7 @@ export function createOverlayRenderers(state, deps) {
|
|
|
764
764
|
lines.push(` ${key('W')} Toggle ping mode ${hint('(speed 2s → normal 10s → slow 30s → forced 4s)')}`)
|
|
765
765
|
lines.push(` ${key('Ctrl+P')} Open ⚡️ command palette ${hint('(search and run actions quickly)')}`)
|
|
766
766
|
lines.push(` ${key('E')} Toggle configured models only ${hint('(enabled by default)')}`)
|
|
767
|
-
lines.push(` ${key('Z')} Cycle tool mode ${hint('(OpenCode → Desktop → OpenClaw → Crush → Goose → Pi → Aider → Qwen → OpenHands → Amp)')}`)
|
|
767
|
+
lines.push(` ${key('Z')} Cycle tool mode ${hint('(📦 OpenCode → 📦 Desktop → 🦞 OpenClaw → 💘 Crush → 🪿 Goose → π Pi → 🛠 Aider → 🐉 Qwen → 🤲 OpenHands → ⚡ Amp → 🦘 Rovo → ♊ Gemini)')}`)
|
|
768
768
|
lines.push(` ${key('F')} Toggle favorite on selected row ${hint('(⭐ persisted across sessions)')}`)
|
|
769
769
|
lines.push(` ${key('Y')} Toggle favorites mode ${hint('(Pinned + always visible ↔ Normal filter/sort behavior)')}`)
|
|
770
770
|
lines.push(` ${key('X')} Clear active text filter ${hint('(remove custom query applied from ⚡️ Command Palette)')}`)
|
|
@@ -1217,6 +1217,102 @@ export function createOverlayRenderers(state, deps) {
|
|
|
1217
1217
|
if (state.recommendPingTimer) { clearInterval(state.recommendPingTimer); state.recommendPingTimer = null }
|
|
1218
1218
|
}
|
|
1219
1219
|
|
|
1220
|
+
// ─── Incompatible fallback overlay ─────────────────────────────────────────
|
|
1221
|
+
// 📖 renderIncompatibleFallback shows when user presses Enter on a model that
|
|
1222
|
+
// 📖 is NOT compatible with the active tool. Two sections:
|
|
1223
|
+
// 📖 Section 1: "Switch to a compatible tool" — lists tools the model CAN run on
|
|
1224
|
+
// 📖 Section 2: "Use a similar model" — lists SWE-similar models compatible with current tool
|
|
1225
|
+
// 📖 Cursor navigates a flat list across both sections. Enter executes, Esc cancels.
|
|
1226
|
+
function renderIncompatibleFallback() {
|
|
1227
|
+
const EL = '\x1b[K'
|
|
1228
|
+
const lines = []
|
|
1229
|
+
const cursorLineByRow = {}
|
|
1230
|
+
|
|
1231
|
+
const model = state.incompatibleFallbackModel
|
|
1232
|
+
const tools = state.incompatibleFallbackTools || []
|
|
1233
|
+
const similarModels = state.incompatibleFallbackSimilarModels || []
|
|
1234
|
+
const totalItems = tools.length + similarModels.length
|
|
1235
|
+
const activeMeta = getToolMeta(state.mode)
|
|
1236
|
+
|
|
1237
|
+
lines.push(` ${chalk.cyanBright('🚀')} ${chalk.bold.cyanBright('free-coding-models')}`)
|
|
1238
|
+
lines.push(` ${chalk.bold('⚠️ Incompatible Model')}`)
|
|
1239
|
+
lines.push('')
|
|
1240
|
+
|
|
1241
|
+
if (!model) {
|
|
1242
|
+
lines.push(chalk.red(' No model data available.'))
|
|
1243
|
+
lines.push('')
|
|
1244
|
+
lines.push(chalk.dim(' Esc Close'))
|
|
1245
|
+
} else {
|
|
1246
|
+
// 📖 Header: explain why it's incompatible
|
|
1247
|
+
const tierFn = TIER_COLOR[model.tier] ?? ((text) => themeColors.text(text))
|
|
1248
|
+
lines.push(` ${themeColors.textBold(model.label)} ${tierFn(model.tier)}`)
|
|
1249
|
+
lines.push(chalk.dim(` This model cannot run on ${activeMeta.emoji} ${activeMeta.label}.`))
|
|
1250
|
+
lines.push('')
|
|
1251
|
+
|
|
1252
|
+
// 📖 Section 1: Switch to a compatible tool
|
|
1253
|
+
if (tools.length > 0) {
|
|
1254
|
+
lines.push(` ${themeColors.textBold('Switch to a compatible tool:')}`)
|
|
1255
|
+
lines.push('')
|
|
1256
|
+
|
|
1257
|
+
for (let i = 0; i < tools.length; i++) {
|
|
1258
|
+
const toolKey = tools[i]
|
|
1259
|
+
const meta = getToolMeta(toolKey)
|
|
1260
|
+
const [r, g, b] = meta.color || [200, 200, 200]
|
|
1261
|
+
const coloredLabel = chalk.rgb(r, g, b)(`${meta.emoji} ${meta.label}`)
|
|
1262
|
+
const isCursor = state.incompatibleFallbackCursor === i
|
|
1263
|
+
const bullet = isCursor ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
1264
|
+
const row = `${bullet}${coloredLabel}`
|
|
1265
|
+
cursorLineByRow[i] = lines.length
|
|
1266
|
+
lines.push(isCursor ? themeColors.bgCursorInstall(row) : row)
|
|
1267
|
+
}
|
|
1268
|
+
lines.push('')
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
// 📖 Section 2: Use a similar model
|
|
1272
|
+
if (similarModels.length > 0) {
|
|
1273
|
+
lines.push(` ${themeColors.textBold('Or pick a similar model for')} ${activeMeta.emoji} ${themeColors.textBold(activeMeta.label + ':')}`)
|
|
1274
|
+
lines.push('')
|
|
1275
|
+
|
|
1276
|
+
for (let i = 0; i < similarModels.length; i++) {
|
|
1277
|
+
const sm = similarModels[i]
|
|
1278
|
+
const flatIdx = tools.length + i
|
|
1279
|
+
const tierFnSm = TIER_COLOR[sm.tier] ?? ((text) => themeColors.text(text))
|
|
1280
|
+
const isCursor = state.incompatibleFallbackCursor === flatIdx
|
|
1281
|
+
const bullet = isCursor ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
1282
|
+
const sweLabel = sm.sweScore !== '-' ? `SWE ${sm.sweScore}` : 'SWE —'
|
|
1283
|
+
const row = `${bullet}${themeColors.textBold(sm.label)} ${tierFnSm(sm.tier)} ${chalk.dim(sweLabel)}`
|
|
1284
|
+
cursorLineByRow[flatIdx] = lines.length
|
|
1285
|
+
lines.push(isCursor ? themeColors.bgCursorInstall(row) : row)
|
|
1286
|
+
}
|
|
1287
|
+
lines.push('')
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
if (totalItems === 0) {
|
|
1291
|
+
lines.push(chalk.yellow(' No compatible tools or similar models found.'))
|
|
1292
|
+
lines.push('')
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
lines.push(chalk.dim(' ↑↓ Navigate • Enter Confirm • Esc Cancel'))
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
lines.push('')
|
|
1299
|
+
|
|
1300
|
+
// 📖 Scroll management — same pattern as other overlays
|
|
1301
|
+
const targetLine = cursorLineByRow[state.incompatibleFallbackCursor] ?? 0
|
|
1302
|
+
state.incompatibleFallbackScrollOffset = keepOverlayTargetVisible(
|
|
1303
|
+
state.incompatibleFallbackScrollOffset,
|
|
1304
|
+
targetLine,
|
|
1305
|
+
lines.length,
|
|
1306
|
+
state.terminalRows
|
|
1307
|
+
)
|
|
1308
|
+
const { visible, offset } = sliceOverlayLines(lines, state.incompatibleFallbackScrollOffset, state.terminalRows)
|
|
1309
|
+
state.incompatibleFallbackScrollOffset = offset
|
|
1310
|
+
|
|
1311
|
+
const tintedLines = tintOverlayLines(visible, themeColors.overlayBgSettings, state.terminalCols)
|
|
1312
|
+
const cleared = tintedLines.map(l => l + EL)
|
|
1313
|
+
return cleared.join('\n')
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1220
1316
|
return {
|
|
1221
1317
|
renderSettings,
|
|
1222
1318
|
renderInstallEndpoints,
|
|
@@ -1226,6 +1322,7 @@ export function createOverlayRenderers(state, deps) {
|
|
|
1226
1322
|
renderRecommend,
|
|
1227
1323
|
renderFeedback,
|
|
1228
1324
|
renderChangelog,
|
|
1325
|
+
renderIncompatibleFallback,
|
|
1229
1326
|
startRecommendAnalysis,
|
|
1230
1327
|
stopRecommendAnalysis,
|
|
1231
1328
|
}
|
package/src/provider-metadata.js
CHANGED
|
@@ -58,6 +58,7 @@ export const ENV_VAR_NAMES = {
|
|
|
58
58
|
cloudflare: 'CLOUDFLARE_API_TOKEN',
|
|
59
59
|
perplexity: 'PERPLEXITY_API_KEY',
|
|
60
60
|
zai: 'ZAI_API_KEY',
|
|
61
|
+
gemini: 'GEMINI_API_KEY',
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
// 📖 OPENCODE_MODEL_MAP: sparse table of model IDs that differ between sources.js and OpenCode's
|
|
@@ -225,4 +226,28 @@ export const PROVIDER_METADATA = {
|
|
|
225
226
|
signupHint: 'Install @mariozechner/pi-coding-agent and set ANTHROPIC_API_KEY',
|
|
226
227
|
rateLimits: 'Depends on provider subscription (e.g., Anthropic, OpenAI)',
|
|
227
228
|
},
|
|
229
|
+
rovo: {
|
|
230
|
+
label: 'Rovo Dev CLI',
|
|
231
|
+
color: chalk.rgb(148, 163, 184), // slate blue
|
|
232
|
+
signupUrl: 'https://www.atlassian.com/rovo',
|
|
233
|
+
signupHint: 'Install ACLI and run: acli rovodev auth login',
|
|
234
|
+
rateLimits: 'Free tier: 5M tokens/day (beta, requires Atlassian account)',
|
|
235
|
+
cliOnly: true,
|
|
236
|
+
},
|
|
237
|
+
gemini: {
|
|
238
|
+
label: 'Gemini CLI',
|
|
239
|
+
color: chalk.rgb(66, 165, 245), // blue
|
|
240
|
+
signupUrl: 'https://github.com/google-gemini/gemini-cli',
|
|
241
|
+
signupHint: 'Install: npm install -g @google/gemini-cli',
|
|
242
|
+
rateLimits: 'Free tier: 1,000 req/day (personal Google account, no credit card)',
|
|
243
|
+
cliOnly: true,
|
|
244
|
+
},
|
|
245
|
+
'opencode-zen': {
|
|
246
|
+
label: 'OpenCode Zen',
|
|
247
|
+
color: chalk.rgb(139, 92, 246), // violet — distinctive from other providers
|
|
248
|
+
signupUrl: 'https://opencode.ai/auth',
|
|
249
|
+
signupHint: 'Login at opencode.ai/auth to get your Zen API key',
|
|
250
|
+
rateLimits: 'Free tier models — requires OpenCode Zen API key',
|
|
251
|
+
zenOnly: true,
|
|
252
|
+
},
|
|
228
253
|
}
|
package/src/render-helpers.js
CHANGED
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
* - chalk: Terminal colors and formatting
|
|
37
37
|
* - ../src/constants.js: OVERLAY_PANEL_WIDTH, TABLE_FIXED_LINES
|
|
38
38
|
* - ../src/utils.js: sortResults
|
|
39
|
+
* - ../src/tool-metadata.js: isModelCompatibleWithTool (for compatible-first partition)
|
|
39
40
|
*
|
|
40
41
|
* ⚙️ Configuration:
|
|
41
42
|
* - OVERLAY_PANEL_WIDTH: Fixed width for overlay panels (from constants.js)
|
|
@@ -184,7 +185,6 @@ export function sortResultsWithPinnedFavorites(results, sortColumn, sortDirectio
|
|
|
184
185
|
)
|
|
185
186
|
return [...recommendedRows, ...nonRecommendedRows]
|
|
186
187
|
}
|
|
187
|
-
|
|
188
188
|
const recommendedRows = results
|
|
189
189
|
.filter((r) => r.isRecommended && !r.isFavorite)
|
|
190
190
|
.sort((a, b) => (b.recommendScore || 0) - (a.recommendScore || 0))
|
package/src/render-table.js
CHANGED
|
@@ -50,7 +50,7 @@ import { TIER_COLOR } from './tier-colors.js'
|
|
|
50
50
|
import { getAvg, getVerdict, getUptime, getStabilityScore, getVersionStatusInfo } from './utils.js'
|
|
51
51
|
import { usagePlaceholderForProvider } from './ping.js'
|
|
52
52
|
import { calculateViewport, sortResultsWithPinnedFavorites, padEndDisplay, displayWidth } from './render-helpers.js'
|
|
53
|
-
import { getToolMeta } from './tool-metadata.js'
|
|
53
|
+
import { getToolMeta, TOOL_METADATA, TOOL_MODE_ORDER, COMPAT_COLUMN_SLOTS, getCompatibleTools, isModelCompatibleWithTool } from './tool-metadata.js'
|
|
54
54
|
import { getColumnSpacing } from './ui-config.js'
|
|
55
55
|
|
|
56
56
|
const require = createRequire(import.meta.url)
|
|
@@ -109,9 +109,12 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
109
109
|
|
|
110
110
|
// 📖 Tool badge keeps the active launch target visible in the header, so the
|
|
111
111
|
// 📖 footer no longer needs a redundant Enter action or mode toggle reminder.
|
|
112
|
+
// 📖 Tool name is colored with its unique tool color for quick recognition.
|
|
112
113
|
const toolMeta = getToolMeta(mode)
|
|
113
114
|
const toolBadgeColor = mode === 'openclaw' ? themeColors.warningBold : themeColors.accentBold
|
|
114
|
-
const
|
|
115
|
+
const toolColor = toolMeta.color ? chalk.rgb(...toolMeta.color) : toolBadgeColor
|
|
116
|
+
const modeBadge = toolBadgeColor(' [ ') + themeColors.hotkey('Z') + toolBadgeColor(' Tool : ') + toolColor.bold(`${toolMeta.emoji} ${toolMeta.label}`) + toolBadgeColor(' ]')
|
|
117
|
+
|
|
115
118
|
const activeHeaderBadge = (text, bg) => themeColors.badge(text, bg, getReadableTextRgb(bg))
|
|
116
119
|
const versionStatus = getVersionStatusInfo(settingsUpdateState, settingsUpdateLatestVersion, startupLatestVersion, versionAlertsEnabled)
|
|
117
120
|
|
|
@@ -157,6 +160,7 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
157
160
|
const W_STATUS = 18
|
|
158
161
|
const W_VERDICT = 14
|
|
159
162
|
const W_UPTIME = 6
|
|
163
|
+
const W_COMPAT = 22 // 📖 "Compatible with" column — 11 emoji slots (10×2 + 1×1 for π + 1 padding)
|
|
160
164
|
// const W_TOKENS = 7 // Used column removed
|
|
161
165
|
// const W_USAGE = 7 // Usage column removed
|
|
162
166
|
const MIN_TABLE_WIDTH = WIDTH_WARNING_MIN_COLS
|
|
@@ -176,6 +180,7 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
176
180
|
let showUptime = true
|
|
177
181
|
let showTier = true
|
|
178
182
|
let showStability = true
|
|
183
|
+
let showCompat = true // 📖 "Compatible with" column — hidden on narrow terminals
|
|
179
184
|
let isCompact = false
|
|
180
185
|
|
|
181
186
|
if (terminalCols > 0) {
|
|
@@ -187,6 +192,7 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
187
192
|
cols.push(W_SWE, W_CTX, W_MODEL, wSource, wPing, wAvg, wStatus, W_VERDICT)
|
|
188
193
|
if (showStability) cols.push(wStab)
|
|
189
194
|
if (showUptime) cols.push(W_UPTIME)
|
|
195
|
+
if (showCompat) cols.push(W_COMPAT)
|
|
190
196
|
return ROW_MARGIN + cols.reduce((a, b) => a + b, 0) + (cols.length - 1) * SEP_W
|
|
191
197
|
}
|
|
192
198
|
|
|
@@ -200,6 +206,7 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
200
206
|
wStatus = 13 // Health truncated after 6 chars + '…'
|
|
201
207
|
}
|
|
202
208
|
// 📖 Steps 2–5: Progressive column hiding (least useful first)
|
|
209
|
+
if (calcWidth() > terminalCols) showCompat = false
|
|
203
210
|
if (calcWidth() > terminalCols) showRank = false
|
|
204
211
|
if (calcWidth() > terminalCols) showUptime = false
|
|
205
212
|
if (calcWidth() > terminalCols) showTier = false
|
|
@@ -208,7 +215,7 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
208
215
|
const warningDurationMs = 2_000
|
|
209
216
|
const elapsed = widthWarningStartedAt ? Math.max(0, Date.now() - widthWarningStartedAt) : warningDurationMs
|
|
210
217
|
const remainingMs = Math.max(0, warningDurationMs - elapsed)
|
|
211
|
-
const showWidthWarning = terminalCols > 0 && terminalCols < MIN_TABLE_WIDTH && !widthWarningDismissed &&
|
|
218
|
+
const showWidthWarning = terminalCols > 0 && terminalCols < MIN_TABLE_WIDTH && !widthWarningDismissed && remainingMs > 0
|
|
212
219
|
|
|
213
220
|
if (showWidthWarning) {
|
|
214
221
|
const lines = []
|
|
@@ -319,6 +326,14 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
319
326
|
const padding = ' '.repeat(Math.max(0, W_UPTIME - plain.length))
|
|
320
327
|
return themeColors.hotkey('U') + themeColors.dim('p%' + padding)
|
|
321
328
|
})()
|
|
329
|
+
// 📖 "Compatible with" column header — show all tool emojis in their colors as the header
|
|
330
|
+
const compatHeaderEmojis = COMPAT_COLUMN_SLOTS.map(slot => {
|
|
331
|
+
return chalk.rgb(...slot.color)(slot.emoji)
|
|
332
|
+
}).join('')
|
|
333
|
+
// 📖 padEndDisplay accounts for emoji widths (most are 2-wide, π is 1-wide)
|
|
334
|
+
const compatHeaderRaw = COMPAT_COLUMN_SLOTS.reduce((w, slot) => w + displayWidth(slot.emoji), 0)
|
|
335
|
+
const compatHeaderPad = Math.max(0, W_COMPAT - compatHeaderRaw)
|
|
336
|
+
const compatH_c = compatHeaderEmojis + ' '.repeat(compatHeaderPad)
|
|
322
337
|
// 📖 Usage column removed from UI – no header or separator for it.
|
|
323
338
|
// 📖 Header row: conditionally include columns based on responsive visibility
|
|
324
339
|
const headerParts = []
|
|
@@ -327,6 +342,7 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
327
342
|
headerParts.push(sweH_c, ctxH_c, modelH_c, originH_c, pingH_c, avgH_c, healthH_c, verdictH_c)
|
|
328
343
|
if (showStability) headerParts.push(stabH_c)
|
|
329
344
|
if (showUptime) headerParts.push(uptimeH_c)
|
|
345
|
+
if (showCompat) headerParts.push(compatH_c)
|
|
330
346
|
lines.push(' ' + headerParts.join(COL_SEP))
|
|
331
347
|
|
|
332
348
|
|
|
@@ -585,6 +601,31 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
585
601
|
const sourceCursorText = providerDisplay.padEnd(wSource)
|
|
586
602
|
const sourceCell = isCursor ? themeColors.provider(r.providerKey, sourceCursorText, { bold: true }) : source
|
|
587
603
|
|
|
604
|
+
// 📖 "Compatible with" column — show colored emojis for compatible tools
|
|
605
|
+
// 📖 Each slot in COMPAT_COLUMN_SLOTS maps to one or more tool keys.
|
|
606
|
+
// 📖 OpenCode CLI + Desktop are merged into a single 📦 slot.
|
|
607
|
+
let compatCell = ''
|
|
608
|
+
if (showCompat) {
|
|
609
|
+
const compatTools = getCompatibleTools(r.providerKey)
|
|
610
|
+
let compatDisplayWidth = 0
|
|
611
|
+
const emojiCells = COMPAT_COLUMN_SLOTS.map(slot => {
|
|
612
|
+
const isCompat = slot.toolKeys.some(tk => compatTools.includes(tk))
|
|
613
|
+
const ew = displayWidth(slot.emoji)
|
|
614
|
+
compatDisplayWidth += isCompat ? ew : ew
|
|
615
|
+
if (isCompat) {
|
|
616
|
+
return chalk.rgb(...slot.color)(slot.emoji)
|
|
617
|
+
}
|
|
618
|
+
// 📖 Replace incompatible emoji with dim spaces matching its display width
|
|
619
|
+
return themeColors.dim(' '.repeat(ew))
|
|
620
|
+
}).join('')
|
|
621
|
+
// 📖 Pad to W_COMPAT — account for actual emoji display widths
|
|
622
|
+
const extraPad = Math.max(0, W_COMPAT - compatDisplayWidth)
|
|
623
|
+
compatCell = emojiCells + ' '.repeat(extraPad)
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// 📖 Check if this model is incompatible with the active tool mode
|
|
627
|
+
const isIncompatible = !isModelCompatibleWithTool(r.providerKey, mode)
|
|
628
|
+
|
|
588
629
|
// 📖 Usage column removed from UI – no usage data displayed.
|
|
589
630
|
// (We keep the logic but do not render it.)
|
|
590
631
|
const usageCell = ''
|
|
@@ -596,10 +637,15 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
596
637
|
rowParts.push(sweCell, ctxCell, nameCell, sourceCell, pingCell, avgCell, status, speedCell)
|
|
597
638
|
if (showStability) rowParts.push(stabCell)
|
|
598
639
|
if (showUptime) rowParts.push(uptimeCell)
|
|
640
|
+
if (showCompat) rowParts.push(compatCell)
|
|
599
641
|
const row = ' ' + rowParts.join(COL_SEP)
|
|
600
642
|
|
|
601
643
|
if (isCursor) {
|
|
602
644
|
lines.push(themeColors.bgModelCursor(row))
|
|
645
|
+
} else if (isIncompatible) {
|
|
646
|
+
// 📖 Dark red background for models incompatible with the active tool mode.
|
|
647
|
+
// 📖 This visually warns the user that selecting this model won't work with their current tool.
|
|
648
|
+
lines.push(chalk.bgRgb(60, 15, 15).rgb(180, 130, 130)(row))
|
|
603
649
|
} else if (r.isRecommended) {
|
|
604
650
|
// 📖 Medium green background for recommended models (distinguishable from favorites)
|
|
605
651
|
lines.push(themeColors.bgModelRecommended(row))
|
package/src/theme.js
CHANGED
|
@@ -141,6 +141,9 @@ const PROVIDER_PALETTES = {
|
|
|
141
141
|
qwen: [255, 213, 128],
|
|
142
142
|
zai: [150, 208, 255],
|
|
143
143
|
iflow: [211, 229, 101],
|
|
144
|
+
rovo: [148, 163, 184],
|
|
145
|
+
gemini: [66, 165, 245],
|
|
146
|
+
'opencode-zen': [185, 146, 255],
|
|
144
147
|
},
|
|
145
148
|
light: {
|
|
146
149
|
nvidia: [0, 126, 73],
|
|
@@ -163,6 +166,9 @@ const PROVIDER_PALETTES = {
|
|
|
163
166
|
qwen: [132, 89, 0],
|
|
164
167
|
zai: [0, 104, 171],
|
|
165
168
|
iflow: [107, 130, 0],
|
|
169
|
+
rovo: [90, 100, 126],
|
|
170
|
+
gemini: [15, 97, 175],
|
|
171
|
+
'opencode-zen': [108, 58, 183],
|
|
166
172
|
},
|
|
167
173
|
}
|
|
168
174
|
|
package/src/tool-bootstrap.js
CHANGED
|
@@ -217,6 +217,28 @@ export const TOOL_BOOTSTRAP_METADATA = {
|
|
|
217
217
|
},
|
|
218
218
|
},
|
|
219
219
|
},
|
|
220
|
+
rovo: {
|
|
221
|
+
binary: 'acli',
|
|
222
|
+
docsUrl: 'https://support.atlassian.com/rovo/docs/install-and-run-rovo-dev-cli-on-your-device/',
|
|
223
|
+
install: {
|
|
224
|
+
default: {
|
|
225
|
+
shellCommand: 'npm install -g acli',
|
|
226
|
+
summary: 'Rovo Dev CLI requires ACLI installation. Visit the documentation for platform-specific instructions.',
|
|
227
|
+
note: 'Rovo is an Atlassian tool that requires an Atlassian account with Rovo Dev activated.',
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
gemini: {
|
|
232
|
+
binary: 'gemini',
|
|
233
|
+
docsUrl: 'https://github.com/google-gemini/gemini-cli',
|
|
234
|
+
install: {
|
|
235
|
+
default: {
|
|
236
|
+
shellCommand: 'npm install -g @google/gemini-cli',
|
|
237
|
+
summary: 'Install Gemini CLI globally via npm.',
|
|
238
|
+
note: 'After installation, run `gemini` to authenticate with your Google account.',
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
},
|
|
220
242
|
}
|
|
221
243
|
|
|
222
244
|
export function getToolBootstrapMeta(mode) {
|
package/src/tool-launchers.js
CHANGED
|
@@ -41,7 +41,7 @@ import { sources } from '../sources.js'
|
|
|
41
41
|
import { PROVIDER_COLOR } from './render-table.js'
|
|
42
42
|
import { getApiKey } from './config.js'
|
|
43
43
|
import { ENV_VAR_NAMES, isWindows } from './provider-metadata.js'
|
|
44
|
-
import { getToolMeta } from './tool-metadata.js'
|
|
44
|
+
import { getToolMeta, TOOL_METADATA } from './tool-metadata.js'
|
|
45
45
|
import { PROVIDER_METADATA } from './provider-metadata.js'
|
|
46
46
|
import { resolveToolBinaryPath } from './tool-bootstrap.js'
|
|
47
47
|
|
|
@@ -378,6 +378,57 @@ function writeOpenHandsEnv(model, apiKey, baseUrl, paths = getDefaultToolPaths()
|
|
|
378
378
|
return { filePath, backupPath }
|
|
379
379
|
}
|
|
380
380
|
|
|
381
|
+
/**
|
|
382
|
+
* 📖 writeRovoConfig - Configure Rovo Dev CLI model selection
|
|
383
|
+
*
|
|
384
|
+
* Rovo Dev CLI uses ~/.rovodev/config.yml for configuration.
|
|
385
|
+
* We write the model ID to the config file before launching.
|
|
386
|
+
*
|
|
387
|
+
* @param {Object} model - Selected model with modelId
|
|
388
|
+
* @param {string} configPath - Path to Rovo config file
|
|
389
|
+
* @returns {{ filePath: string, backupPath: string | null }}
|
|
390
|
+
*/
|
|
391
|
+
function writeRovoConfig(model, configPath = join(homedir(), '.rovodev', 'config.yml')) {
|
|
392
|
+
const backupPath = backupIfExists(configPath)
|
|
393
|
+
const config = {
|
|
394
|
+
agent: {
|
|
395
|
+
modelId: model.modelId,
|
|
396
|
+
},
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
ensureDir(configPath)
|
|
400
|
+
writeFileSync(configPath, `agent:\n modelId: "${model.modelId}"\n`)
|
|
401
|
+
return { filePath: configPath, backupPath }
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* 📖 buildGeminiEnv - Build environment variables for Gemini CLI
|
|
406
|
+
*
|
|
407
|
+
* Gemini CLI supports OpenAI-compatible APIs via environment variables:
|
|
408
|
+
* - GEMINI_API_BASE_URL: Custom API endpoint
|
|
409
|
+
* - GEMINI_API_KEY: API key for custom endpoint
|
|
410
|
+
*
|
|
411
|
+
* @param {Object} model - Selected model with providerKey
|
|
412
|
+
* @param {Object} config - Full app config
|
|
413
|
+
* @param {Object} options - Env options
|
|
414
|
+
* @returns {NodeJS.ProcessEnv}
|
|
415
|
+
*/
|
|
416
|
+
function buildGeminiEnv(model, config, options = {}) {
|
|
417
|
+
const providerKey = model.providerKey || 'gemini'
|
|
418
|
+
const apiKey = getApiKey(config, providerKey)
|
|
419
|
+
const baseUrl = getProviderBaseUrl(providerKey)
|
|
420
|
+
|
|
421
|
+
const env = cloneInheritedEnv(process.env, SANITIZED_TOOL_ENV_KEYS)
|
|
422
|
+
|
|
423
|
+
// If we have a custom API key and base URL, configure OpenAI-compatible mode
|
|
424
|
+
if (apiKey && baseUrl && options.includeProviderEnv) {
|
|
425
|
+
env.GEMINI_API_BASE_URL = baseUrl
|
|
426
|
+
env.GEMINI_API_KEY = apiKey
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return env
|
|
430
|
+
}
|
|
431
|
+
|
|
381
432
|
function printConfigArtifacts(toolName, artifacts = []) {
|
|
382
433
|
for (const artifact of artifacts) {
|
|
383
434
|
if (!artifact?.path) continue
|
|
@@ -420,7 +471,9 @@ export function prepareExternalToolLaunch(mode, model, config, options = {}) {
|
|
|
420
471
|
inheritedEnv: options.inheritedEnv,
|
|
421
472
|
})
|
|
422
473
|
|
|
423
|
-
|
|
474
|
+
const isCliOnlyTool = TOOL_METADATA[mode]?.cliOnly === true
|
|
475
|
+
|
|
476
|
+
if (!apiKey && mode !== 'amp' && !isCliOnlyTool) {
|
|
424
477
|
const providerRgb = PROVIDER_COLOR[model.providerKey] ?? [105, 190, 245]
|
|
425
478
|
const providerName = sources[model.providerKey]?.name || model.providerKey
|
|
426
479
|
const coloredProviderName = chalk.bold.rgb(...providerRgb)(providerName)
|
|
@@ -544,6 +597,34 @@ export function prepareExternalToolLaunch(mode, model, config, options = {}) {
|
|
|
544
597
|
}
|
|
545
598
|
}
|
|
546
599
|
|
|
600
|
+
if (mode === 'rovo') {
|
|
601
|
+
const result = writeRovoConfig(model, join(homedir(), '.rovodev', 'config.yml'), paths)
|
|
602
|
+
console.log(chalk.dim(` 📖 Rovo Dev CLI configured with model: ${model.modelId}`))
|
|
603
|
+
return {
|
|
604
|
+
command: 'acli',
|
|
605
|
+
args: ['rovodev', 'run'],
|
|
606
|
+
env,
|
|
607
|
+
apiKey: null,
|
|
608
|
+
baseUrl: null,
|
|
609
|
+
meta,
|
|
610
|
+
configArtifacts: [{ path: result.filePath, backupPath: result.backupPath, label: 'config' }],
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
if (mode === 'gemini') {
|
|
615
|
+
const geminiEnv = buildGeminiEnv(model, config, { includeProviderEnv: options.includeProviderEnv })
|
|
616
|
+
console.log(chalk.dim(` 📖 Gemini CLI will use model: ${model.modelId}`))
|
|
617
|
+
return {
|
|
618
|
+
command: 'gemini',
|
|
619
|
+
args: [],
|
|
620
|
+
env: { ...env, ...geminiEnv },
|
|
621
|
+
apiKey: geminiEnv.GEMINI_API_KEY || null,
|
|
622
|
+
baseUrl: geminiEnv.GEMINI_API_BASE_URL || null,
|
|
623
|
+
meta,
|
|
624
|
+
configArtifacts: [],
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
547
628
|
return {
|
|
548
629
|
blocked: true,
|
|
549
630
|
exitCode: 1,
|
|
@@ -598,6 +679,16 @@ export async function startExternalTool(mode, model, config) {
|
|
|
598
679
|
return spawnCommand(resolveLaunchCommand(mode, launchPlan.command), launchPlan.args, launchPlan.env)
|
|
599
680
|
}
|
|
600
681
|
|
|
682
|
+
if (mode === 'rovo') {
|
|
683
|
+
console.log(chalk.dim(` 📖 Launching Rovo Dev CLI in interactive mode...`))
|
|
684
|
+
return spawnCommand(resolveLaunchCommand(mode, launchPlan.command), launchPlan.args, launchPlan.env)
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
if (mode === 'gemini') {
|
|
688
|
+
console.log(chalk.dim(` 📖 Launching Gemini CLI...`))
|
|
689
|
+
return spawnCommand(resolveLaunchCommand(mode, launchPlan.command), launchPlan.args, launchPlan.env)
|
|
690
|
+
}
|
|
691
|
+
|
|
601
692
|
console.log(chalk.red(` X Unsupported external tool mode: ${mode}`))
|
|
602
693
|
return 1
|
|
603
694
|
}
|
package/src/tool-metadata.js
CHANGED
|
@@ -19,21 +19,44 @@
|
|
|
19
19
|
* → `getToolMeta` — return display metadata for one mode
|
|
20
20
|
* → `getToolModeOrder` — stable mode cycle order for the `Z` hotkey
|
|
21
21
|
*
|
|
22
|
-
* @exports TOOL_METADATA, TOOL_MODE_ORDER, getToolMeta, getToolModeOrder
|
|
22
|
+
* @exports TOOL_METADATA, TOOL_MODE_ORDER, COMPAT_COLUMN_SLOTS, getToolMeta, getToolModeOrder
|
|
23
23
|
*/
|
|
24
|
+
// 📖 Each tool has a unique `color` RGB tuple used for the "Compatible with" column
|
|
25
|
+
// 📖 and for coloring the tool name in the Z cycle header badge.
|
|
26
|
+
// 📖 `emoji` is the unique icon shown everywhere (header badge, compat column, palette, overlays).
|
|
27
|
+
// 📖 OpenCode CLI and Desktop share 📦 — they are the same platform, split only for launch logic.
|
|
24
28
|
export const TOOL_METADATA = {
|
|
25
|
-
opencode:
|
|
26
|
-
'opencode-desktop': { label: 'OpenCode Desktop', emoji: '
|
|
27
|
-
openclaw:
|
|
28
|
-
crush:
|
|
29
|
-
goose:
|
|
30
|
-
pi:
|
|
31
|
-
aider:
|
|
32
|
-
qwen:
|
|
33
|
-
openhands:
|
|
34
|
-
amp:
|
|
29
|
+
opencode: { label: 'OpenCode CLI', emoji: '📦', flag: '--opencode', color: [110, 214, 255] },
|
|
30
|
+
'opencode-desktop': { label: 'OpenCode Desktop', emoji: '📦', flag: '--opencode-desktop', color: [149, 205, 255] },
|
|
31
|
+
openclaw: { label: 'OpenClaw', emoji: '🦞', flag: '--openclaw', color: [255, 129, 129] },
|
|
32
|
+
crush: { label: 'Crush', emoji: '💘', flag: '--crush', color: [255, 168, 209] },
|
|
33
|
+
goose: { label: 'Goose', emoji: '🪿', flag: '--goose', color: [132, 235, 168] },
|
|
34
|
+
pi: { label: 'Pi', emoji: 'π', flag: '--pi', color: [173, 216, 230] },
|
|
35
|
+
aider: { label: 'Aider', emoji: '🛠', flag: '--aider', color: [255, 208, 102] },
|
|
36
|
+
qwen: { label: 'Qwen Code', emoji: '🐉', flag: '--qwen', color: [255, 213, 128] },
|
|
37
|
+
openhands: { label: 'OpenHands', emoji: '🤲', flag: '--openhands', color: [228, 191, 239] },
|
|
38
|
+
amp: { label: 'Amp', emoji: '⚡', flag: '--amp', color: [255, 232, 98] },
|
|
39
|
+
rovo: { label: 'Rovo Dev CLI', emoji: '🦘', flag: '--rovo', color: [148, 163, 184], cliOnly: true },
|
|
40
|
+
gemini: { label: 'Gemini CLI', emoji: '♊', flag: '--gemini', color: [66, 165, 245], cliOnly: true },
|
|
35
41
|
}
|
|
36
42
|
|
|
43
|
+
// 📖 Deduplicated emoji order for the "Compatible with" column.
|
|
44
|
+
// 📖 OpenCode CLI + Desktop are merged into a single 📦 slot since they share compatibility.
|
|
45
|
+
// 📖 Each slot maps to one or more toolKeys for compatibility checking.
|
|
46
|
+
export const COMPAT_COLUMN_SLOTS = [
|
|
47
|
+
{ emoji: '📦', toolKeys: ['opencode', 'opencode-desktop'], color: [110, 214, 255] },
|
|
48
|
+
{ emoji: '🦞', toolKeys: ['openclaw'], color: [255, 129, 129] },
|
|
49
|
+
{ emoji: '💘', toolKeys: ['crush'], color: [255, 168, 209] },
|
|
50
|
+
{ emoji: '🪿', toolKeys: ['goose'], color: [132, 235, 168] },
|
|
51
|
+
{ emoji: 'π', toolKeys: ['pi'], color: [173, 216, 230] },
|
|
52
|
+
{ emoji: '🛠', toolKeys: ['aider'], color: [255, 208, 102] },
|
|
53
|
+
{ emoji: '🐉', toolKeys: ['qwen'], color: [255, 213, 128] },
|
|
54
|
+
{ emoji: '🤲', toolKeys: ['openhands'], color: [228, 191, 239] },
|
|
55
|
+
{ emoji: '⚡', toolKeys: ['amp'], color: [255, 232, 98] },
|
|
56
|
+
{ emoji: '🦘', toolKeys: ['rovo'], color: [148, 163, 184] },
|
|
57
|
+
{ emoji: '♊', toolKeys: ['gemini'], color: [66, 165, 245] },
|
|
58
|
+
]
|
|
59
|
+
|
|
37
60
|
export const TOOL_MODE_ORDER = [
|
|
38
61
|
'opencode',
|
|
39
62
|
'opencode-desktop',
|
|
@@ -45,6 +68,8 @@ export const TOOL_MODE_ORDER = [
|
|
|
45
68
|
'qwen',
|
|
46
69
|
'openhands',
|
|
47
70
|
'amp',
|
|
71
|
+
'rovo',
|
|
72
|
+
'gemini',
|
|
48
73
|
]
|
|
49
74
|
|
|
50
75
|
export function getToolMeta(mode) {
|
|
@@ -54,3 +79,61 @@ export function getToolMeta(mode) {
|
|
|
54
79
|
export function getToolModeOrder() {
|
|
55
80
|
return [...TOOL_MODE_ORDER]
|
|
56
81
|
}
|
|
82
|
+
|
|
83
|
+
// 📖 Regular tools: all tools EXCEPT rovo, gemini (which are CLI-only exclusives).
|
|
84
|
+
// 📖 Used as the default compatible set for normal provider models.
|
|
85
|
+
const REGULAR_TOOLS = Object.keys(TOOL_METADATA).filter(k => !TOOL_METADATA[k].cliOnly)
|
|
86
|
+
|
|
87
|
+
// 📖 Zen-only tools: OpenCode Zen models can ONLY run on OpenCode CLI / OpenCode Desktop.
|
|
88
|
+
const ZEN_COMPATIBLE_TOOLS = ['opencode', 'opencode-desktop']
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 📖 Returns the list of tool keys a model is compatible with.
|
|
92
|
+
* - Rovo models → only 'rovo'
|
|
93
|
+
* - Gemini models → only 'gemini'
|
|
94
|
+
* - OpenCode Zen models → only 'opencode', 'opencode-desktop'
|
|
95
|
+
* - Regular models → all non-cliOnly tools
|
|
96
|
+
* @param {string} providerKey — the source key from sources.js (e.g. 'nvidia', 'rovo', 'opencode-zen')
|
|
97
|
+
* @returns {string[]} — array of compatible tool keys
|
|
98
|
+
*/
|
|
99
|
+
export function getCompatibleTools(providerKey) {
|
|
100
|
+
if (providerKey === 'rovo') return ['rovo']
|
|
101
|
+
if (providerKey === 'gemini') return ['gemini']
|
|
102
|
+
if (providerKey === 'opencode-zen') return ZEN_COMPATIBLE_TOOLS
|
|
103
|
+
return REGULAR_TOOLS
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 📖 Checks whether a model from the given provider can run on the specified tool mode.
|
|
108
|
+
* @param {string} providerKey — source key
|
|
109
|
+
* @param {string} toolMode — active tool mode
|
|
110
|
+
* @returns {boolean}
|
|
111
|
+
*/
|
|
112
|
+
export function isModelCompatibleWithTool(providerKey, toolMode) {
|
|
113
|
+
return getCompatibleTools(providerKey).includes(toolMode)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 📖 Finds compatible models with a similar SWE score to the selected one.
|
|
118
|
+
* 📖 Used by the incompatibility fallback overlay to suggest alternatives.
|
|
119
|
+
* @param {string} selectedSwe — SWE score string like '72.0%' or '-'
|
|
120
|
+
* @param {string} toolMode — current active tool mode
|
|
121
|
+
* @param {Array} allResults — the state.results array (each has .providerKey, .modelId, .label, .tier, .sweScore)
|
|
122
|
+
* @param {number} [maxResults=3] — max suggestions to return
|
|
123
|
+
* @returns {{ modelId: string, label: string, tier: string, sweScore: string, providerKey: string, sweDelta: number }[]}
|
|
124
|
+
*/
|
|
125
|
+
export function findSimilarCompatibleModels(selectedSwe, toolMode, allResults, maxResults = 3) {
|
|
126
|
+
const targetSwe = parseFloat(selectedSwe) || 0
|
|
127
|
+
return allResults
|
|
128
|
+
.filter(r => !r.hidden && isModelCompatibleWithTool(r.providerKey, toolMode))
|
|
129
|
+
.map(r => ({
|
|
130
|
+
modelId: r.modelId,
|
|
131
|
+
label: r.label,
|
|
132
|
+
tier: r.tier,
|
|
133
|
+
sweScore: r.sweScore || '-',
|
|
134
|
+
providerKey: r.providerKey,
|
|
135
|
+
sweDelta: Math.abs((parseFloat(r.sweScore) || 0) - targetSwe),
|
|
136
|
+
}))
|
|
137
|
+
.sort((a, b) => a.sweDelta - b.sweDelta)
|
|
138
|
+
.slice(0, maxResults)
|
|
139
|
+
}
|
package/src/utils.js
CHANGED
|
@@ -454,6 +454,8 @@ export function parseArgs(argv) {
|
|
|
454
454
|
const openHandsMode = flags.includes('--openhands')
|
|
455
455
|
const ampMode = flags.includes('--amp')
|
|
456
456
|
const piMode = flags.includes('--pi')
|
|
457
|
+
const rovoMode = flags.includes('--rovo')
|
|
458
|
+
const geminiMode = flags.includes('--gemini')
|
|
457
459
|
const noTelemetry = flags.includes('--no-telemetry')
|
|
458
460
|
const jsonMode = flags.includes('--json')
|
|
459
461
|
const helpMode = flags.includes('--help') || flags.includes('-h')
|
|
@@ -490,6 +492,8 @@ export function parseArgs(argv) {
|
|
|
490
492
|
openHandsMode,
|
|
491
493
|
ampMode,
|
|
492
494
|
piMode,
|
|
495
|
+
rovoMode,
|
|
496
|
+
geminiMode,
|
|
493
497
|
noTelemetry,
|
|
494
498
|
jsonMode,
|
|
495
499
|
helpMode,
|