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 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-160-76b900?logo=nvidia" alt="models count">
6
- <img src="https://img.shields.io/badge/providers-20-blue" alt="providers count">
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 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>
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 **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?
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
- **160 coding models** across 20 providers, ranked by [SWE-bench Verified](https://www.swebench.com).
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 160 models tested simultaneously via native `fetch`
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.23",
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, // 📖 Counter for how many times the narrow-terminal warning has been shown (max 2 per session).
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
- const unconfiguredHide = state.hideUnconfiguredModels && !getApiKey(state.config, r.providerKey)
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
- 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))
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
  }
@@ -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
- const DIRECT_INSTALL_UNSUPPORTED_PROVIDERS = new Set(['replicate', 'zai'])
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']
@@ -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
- // 📖 OpenClaw manages API keys inside its own config file. All other tools
251
- // 📖 still need a provider key to be useful, so keep the existing warning.
252
- if (state.mode !== 'openclaw') {
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
  }
@@ -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
  }
@@ -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))
@@ -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 modeBadge = toolBadgeColor(' [ ') + themeColors.hotkey('Z') + toolBadgeColor(` Tool : ${toolMeta.label} ]`)
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 && widthWarningShowCount < 2 && remainingMs > 0
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
 
@@ -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) {
@@ -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
- if (!apiKey && mode !== 'amp') {
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
  }
@@ -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: { label: 'OpenCode CLI', emoji: '💻', flag: '--opencode' },
26
- 'opencode-desktop': { label: 'OpenCode Desktop', emoji: '🖥', flag: '--opencode-desktop' },
27
- openclaw: { label: 'OpenClaw', emoji: '🦞', flag: '--openclaw' },
28
- crush: { label: 'Crush', emoji: '💘', flag: '--crush' },
29
- goose: { label: 'Goose', emoji: '🪿', flag: '--goose' },
30
- pi: { label: 'Pi', emoji: 'π', flag: '--pi' },
31
- aider: { label: 'Aider', emoji: '🛠', flag: '--aider' },
32
- qwen: { label: 'Qwen Code', emoji: '🌊', flag: '--qwen' },
33
- openhands: { label: 'OpenHands', emoji: '🤲', flag: '--openhands' },
34
- amp: { label: 'Amp', emoji: '⚡', flag: '--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,