free-coding-models 0.3.50 → 0.3.52

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,41 +1,43 @@
1
- ## [0.3.50] - 2026-04-11
1
+ ## [0.3.52] - 2026-04-18
2
+
3
+ ### Added
4
+
5
+ - **OpenCode WebUI Support** — Added `--opencode-web` flag to open the OpenCode WebUI dashboard after configuring the selected model. This mirrors the existing `--opencode-desktop` behavior.
6
+
7
+ ## [0.3.51] - 2026-04-11
2
8
 
3
9
  ### Changed
4
10
 
5
- - **Providers reordered by generosity of free tier** — All 25 providers are now sorted from most generous to least generous in the README, TUI Settings page, and `D` key filter cycling. No more hunting for the best free option.
6
-
7
- - **Free tier limits corrected across all providers** — Verified and corrected free tier limits for every provider using live web research. Key corrections:
8
- - **Groq**: 30 RPM, 1K-14.4K req/day (previously listed as "30-50 RPM per model")
9
- - **Google AI Studio**: 15-60 RPM, 250-1.5K req/day (previously listed as "14.4K req/day, 30/min")
10
- - **Together AI**: ❌ **No free tier** — requires $5 minimum purchase. Removed from the "free" recommendation.
11
- - **iFlow**: ⚠️ **Shutting down April 17, 2026** — marked with warning in README and sources.js
12
-
13
- - **README subtitle updated** Now says "ranked by generosity of free tier (most generous first)" instead of "ranked by SWE-bench"
14
-
15
- ### Provider generosity ranking (most generous first)
16
-
17
- 1. Groq (30 RPM, 1K-14.4K req/day)
18
- 2. Cerebras (1M tokens/day)
19
- 3. Google AI Studio (15-60 RPM, 250-1.5K req/day)
20
- 4. NVIDIA NIM (~40 RPM)
21
- 5. Cloudflare Workers AI (10K neurons/day)
22
- 6. OpenRouter (50 req/day free)
23
- 7. DeepInfra (200 concurrent requests)
24
- 8. HuggingFace (~$0.10/month)
25
- 9. Perplexity (~50 RPM tiered)
26
- 10. SambaNova (generous dev quota)
27
- 11. Fireworks AI ($1 credits)
28
- 12. Hyperbolic ($1 credits)
29
- 13. OVHcloud AI (2 req/min/IP free)
30
- 14. Replicate (6 req/min free)
31
- 15. Codestral (30 RPM, 2K req/day)
32
- 16. ZAI (generous free quota)
33
- 17. Scaleway (1M tokens)
34
- 18. Alibaba DashScope (1M tokens/90 days)
35
- 19. SiliconFlow (100 req/day + $1 credits)
36
- 20. Rovo Dev CLI (5M tokens/day)
37
- 21. Gemini CLI (1K req/day)
38
- 22. Chutes AI (free community GPU)
39
- 23. OpenCode Zen (free with account)
40
- 24. Together AI (❌ no free tier)
41
- 25. iFlow (⚠️ shutting down April 17, 2026)
11
+ - **NVIDIA NIM moved to #1** — Now listed first in README, TUI Settings page, and `D` key filter cycling (per user request). Provider order across all surfaces is now: NVIDIA NIM → Groq → Cerebras → Google AI Studio → Cloudflare → ... → iFlow.
12
+
13
+ ### Added
14
+
15
+ - **Provider generosity ranking** README subtitle now includes "ranked by free tier generosity" and the full 25-provider ranking table. This reflects the same order used in the TUI Settings screen and `D` key cycling.
16
+
17
+ ### Provider order (as shown in TUI and README)
18
+
19
+ 1. NVIDIA NIM (~40 RPM, 46 models)
20
+ 2. Groq (30 RPM, 1K-14.4K req/day, 8 models)
21
+ 3. Cerebras (30 RPM, 1M tokens/day, 4 models)
22
+ 4. Google AI Studio (15-60 RPM, 250-1.5K req/day, 6 models)
23
+ 5. Cloudflare Workers AI (10K neurons/day, 15 models)
24
+ 6. OpenRouter (50 req/day free, 25 models)
25
+ 7. DeepInfra (200 concurrent requests, 4 models)
26
+ 8. HuggingFace (~$0.10/month, 2 models)
27
+ 9. Perplexity (~50 RPM tiered, 4 models)
28
+ 10. SambaNova (generous dev quota, 13 models)
29
+ 11. Fireworks AI ($1 credits, 4 models)
30
+ 12. Hyperbolic ($1 credits, 13 models)
31
+ 13. OVHcloud AI (2 req/min/IP free, 8 models)
32
+ 14. Replicate (6 req/min free, 2 models)
33
+ 15. Codestral (30 RPM, 2K req/day, 1 model)
34
+ 16. ZAI (generous free quota, 7 models)
35
+ 17. Scaleway (1M tokens, 10 models)
36
+ 18. Alibaba DashScope (1M tokens/90 days, 11 models)
37
+ 19. SiliconFlow (100 req/day + $1 credits, 6 models)
38
+ 20. Rovo Dev CLI (5M tokens/day, 5 models)
39
+ 21. Gemini CLI (1K req/day, 3 models)
40
+ 22. Chutes AI (free community GPU, 4 models)
41
+ 23. OpenCode Zen (free with account, 7 models)
42
+ 24. Together AI ( no free tier, 19 models)
43
+ 25. iFlow (⚠️ shutting down April 17, 2026, 11 models)
package/README.md CHANGED
@@ -11,10 +11,12 @@
11
11
  </p>
12
12
 
13
13
  <h1 align="center">free-coding-models</h1>
14
+
15
+
14
16
 
15
17
  <p align="center">
16
18
  <strong>Find the fastest free coding model in seconds</strong><br>
17
- <sub>Ping 238 models across 25 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, 🔮 Hermes, ▶️ Continue, 🧠 Cline, 🛠️ Xcode, π Pi, 🦘 Rovo or ♊ Gemini in one keystroke</sub>
19
+ <sub>Ping 238 models across 25 AI Free providers in real-time </sub><br> <sub> Install Free API endpoints to your favorite AI coding tool: <br>📦 OpenCode, 📦 OpenCode Desktop, 📦 OpenCode WebUI, 🦞 OpenClaw, 💘 Crush, 🪿 Goose, 🛠 Aider, 🐉 Qwen Code, 🤲 OpenHands, ⚡ Amp, 🔮 Hermes, ▶️ Continue, 🧠 Cline, 🛠️ Xcode, π Pi, 🦘 Rovo or ♊ Gemini in one keystroke</sub>
18
20
  </p>
19
21
 
20
22
 
@@ -72,14 +74,14 @@ It then writes the model you pick directly into your coding tool's config — so
72
74
 
73
75
  Create a free account on one provider below to get started:
74
76
 
75
- **238 coding models** across 25 providers, ranked by generosity of free tier (most generous first).
77
+ **238 coding models** across 25 providers, ranked by free tier generosity.
76
78
 
77
79
  | # | Provider | Models | Tier range | Free tier | Env var |
78
80
  |---|----------|--------|-----------|-----------|--------|
79
- | 1 | [Groq](https://console.groq.com/keys) | 8 | S → B | 30 RPM, 1K‑14.4K req/day (no credit card) | `GROQ_API_KEY` |
80
- | 2 | [Cerebras](https://cloud.cerebras.ai) | 4 | S+ → B | 30 RPM, 1M tokens/day (no credit card) | `CEREBRAS_API_KEY` |
81
- | 3 | [Google AI Studio](https://aistudio.google.com/apikey) | 6 | B+ → C | 15‑60 RPM, 250‑1.5K req/day (no credit card) | `GOOGLE_API_KEY` |
82
- | 4 | [NVIDIA NIM](https://build.nvidia.com) | 46 | S+ → C | ~40 RPM (no credit card) | `NVIDIA_API_KEY` |
81
+ | 1 | [NVIDIA NIM](https://build.nvidia.com) | 46 | S+C | ~40 RPM (no credit card) | `NVIDIA_API_KEY` |
82
+ | 2 | [Groq](https://console.groq.com/keys) | 8 | S → B | 30 RPM, 1K‑14.4K req/day (no credit card) | `GROQ_API_KEY` |
83
+ | 3 | [Cerebras](https://cloud.cerebras.ai) | 4 | S+ → B | 30 RPM, 1M tokens/day (no credit card) | `CEREBRAS_API_KEY` |
84
+ | 4 | [Google AI Studio](https://aistudio.google.com/apikey) | 6 | B+ → C | 15‑60 RPM, 250‑1.5K req/day (no credit card) | `GOOGLE_API_KEY` |
83
85
  | 5 | [Cloudflare Workers AI](https://dash.cloudflare.com) | 15 | S → B | 10K neurons/day, 300 RPM (no credit card) | `CLOUDFLARE_API_TOKEN` + `CLOUDFLARE_ACCOUNT_ID` |
84
86
  | 6 | [OpenRouter](https://openrouter.ai/keys) | 25 | S+ → C | 50 req/day free, 1K/day with $10 spend | `OPENROUTER_API_KEY` |
85
87
  | 7 | [DeepInfra](https://deepinfra.com/login) | 4 | A- → B+ | 200 concurrent requests (no credit card) | `DEEPINFRA_API_KEY` |
@@ -260,6 +262,7 @@ When launching the web dashboard, `free-coding-models` prefers `http://localhost
260
262
  |------|----------|
261
263
  | `--opencode` | 📦 OpenCode CLI |
262
264
  | `--opencode-desktop` | 📦 OpenCode Desktop |
265
+ | `--opencode-web` | 📦 OpenCode WebUI |
263
266
  | `--openclaw` | 🦞 OpenClaw |
264
267
  | `--crush` | 💘 Crush |
265
268
  | `--goose` | 🪿 Goose |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "free-coding-models",
3
- "version": "0.3.50",
3
+ "version": "0.3.52",
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
@@ -483,6 +483,11 @@ export const opencodeZen = [
483
483
  // 📖 Providers ordered by generosity of free tier (most generous first)
484
484
  // 📖 See README for full tier-by-tier comparison
485
485
  export const sources = {
486
+ nvidia: {
487
+ name: 'NIM',
488
+ url: 'https://integrate.api.nvidia.com/v1/chat/completions',
489
+ models: nvidiaNim,
490
+ },
486
491
  groq: {
487
492
  name: 'Groq',
488
493
  url: 'https://api.groq.com/openai/v1/chat/completions',
@@ -498,11 +503,6 @@ export const sources = {
498
503
  url: 'https://generativelanguage.googleapis.com/v1beta/openai/chat/completions',
499
504
  models: googleai,
500
505
  },
501
- nvidia: {
502
- name: 'NIM',
503
- url: 'https://integrate.api.nvidia.com/v1/chat/completions',
504
- models: nvidiaNim,
505
- },
506
506
  cloudflare: {
507
507
  name: 'Cloudflare AI',
508
508
  url: 'https://api.cloudflare.com/client/v4/accounts/{account_id}/ai/v1/chat/completions',
package/src/app.js CHANGED
@@ -105,7 +105,7 @@ import { usageForRow as _usageForRow } from '../src/usage-reader.js'
105
105
  import { buildProviderModelTokenKey, loadTokenUsageByProviderModel } from '../src/token-usage-reader.js'
106
106
  import { parseOpenRouterResponse, fetchProviderQuota as _fetchProviderQuotaFromModule } from '../src/provider-quota-fetchers.js'
107
107
  import { isKnownQuotaTelemetry } from '../src/quota-capabilities.js'
108
- import { ALT_ENTER, ALT_LEAVE, ALT_HOME, PING_TIMEOUT, PING_INTERVAL, FPS, COL_MODEL, COL_MS, CELL_W, FRAMES, TIER_CYCLE, SETTINGS_OVERLAY_BG, HELP_OVERLAY_BG, RECOMMEND_OVERLAY_BG, OVERLAY_PANEL_WIDTH, TABLE_HEADER_LINES, TABLE_FOOTER_LINES, TABLE_FIXED_LINES, WIDTH_WARNING_MIN_COLS, msCell, spinCell } from '../src/constants.js'
108
+ import { ALT_ENTER, ALT_LEAVE, ALT_HOME, PING_TIMEOUT, PING_INTERVAL, FPS, COL_MODEL, COL_MS, CELL_W, FRAMES, TIER_CYCLE, VERDICT_CYCLE, HEALTH_CYCLE, SETTINGS_OVERLAY_BG, HELP_OVERLAY_BG, RECOMMEND_OVERLAY_BG, OVERLAY_PANEL_WIDTH, TABLE_HEADER_LINES, TABLE_FOOTER_LINES, TABLE_FIXED_LINES, WIDTH_WARNING_MIN_COLS, msCell, spinCell } from '../src/constants.js'
109
109
  import { TIER_COLOR } from '../src/tier-colors.js'
110
110
  import { resolveCloudflareUrl, buildPingRequest, ping, extractQuotaPercent, getProviderQuotaPercentCached, usagePlaceholderForProvider } from '../src/ping.js'
111
111
  import { runFiableMode, filterByTierOrExit, fetchOpenRouterFreeModels } from '../src/analysis.js'
@@ -117,7 +117,7 @@ import { promptApiKey } from '../src/setup.js'
117
117
  import { syncShellEnv, ensureShellRcSource, promptShellEnvMigration, removeShellEnv } from '../src/shell-env.js'
118
118
  import { stripAnsi, maskApiKey, displayWidth, padEndDisplay, tintOverlayLines, keepOverlayTargetVisible, sliceOverlayLines, calculateViewport, sortResultsWithPinnedFavorites, adjustScrollOffset } from '../src/render-helpers.js'
119
119
  import { renderTable, PROVIDER_COLOR } from '../src/render-table.js'
120
- import { setOpenCodeModelData, startOpenCode, startOpenCodeDesktop } from '../src/opencode.js'
120
+ import { setOpenCodeModelData, startOpenCode, startOpenCodeDesktop, startOpenCodeWeb } from '../src/opencode.js'
121
121
  import { startOpenClaw } from '../src/openclaw.js'
122
122
  import { createOverlayRenderers } from '../src/overlays.js'
123
123
  import { createKeyHandler, createMouseEventHandler } from '../src/key-handler.js'
@@ -252,6 +252,7 @@ export async function runApp(cliArgs, config) {
252
252
  const flagByMode = {
253
253
  opencode: cliArgs.openCodeMode,
254
254
  'opencode-desktop': cliArgs.openCodeDesktopMode,
255
+ 'opencode-web': cliArgs.openCodeWebMode,
255
256
  openclaw: cliArgs.openClawMode,
256
257
  aider: cliArgs.aiderMode,
257
258
  crush: cliArgs.crushMode,
@@ -423,6 +424,8 @@ export async function runApp(cliArgs, config) {
423
424
  mode, // 📖 'opencode' or 'openclaw' — controls Enter action
424
425
  tierFilterMode: 0, // 📖 Index into TIER_CYCLE (0=All, 1=S+, 2=S, ...)
425
426
  originFilterMode: 0, // 📖 Index into ORIGIN_CYCLE (0=All, then providers)
427
+ verdictFilterMode: 0, // 📖 Index into VERDICT_CYCLE (0=All, then verdicts)
428
+ healthFilterMode: 0, // 📖 Index into HEALTH_CYCLE (0=All, then health states)
426
429
  hideUnconfiguredModels: config.settings?.hideUnconfiguredModels === true, // 📖 Hide providers with no configured API key when true.
427
430
  favoritesPinnedAndSticky: config.settings?.favoritesPinnedAndSticky === true, // 📖 false by default: favorites follow normal sort/filter rules until Y enables pinned+sticky mode.
428
431
  footerHidden: config.settings?.footerHidden === true, // 📖 true = footer is collapsed to a single toggle hint
@@ -739,6 +742,8 @@ export async function runApp(cliArgs, config) {
739
742
  function applyTierFilter() {
740
743
  const activeTier = TIER_CYCLE[state.tierFilterMode]
741
744
  const activeOrigin = ORIGIN_CYCLE[state.originFilterMode]
745
+ const activeVerdict = VERDICT_CYCLE[state.verdictFilterMode]
746
+ const activeHealth = HEALTH_CYCLE[state.healthFilterMode]
742
747
  state.results.forEach(r => {
743
748
  // 📖 Sticky-favorites mode keeps favorites visible regardless of configured-only, tier, or provider filters.
744
749
  if (state.favoritesPinnedAndSticky && r.isFavorite) {
@@ -754,12 +759,16 @@ export async function runApp(cliArgs, config) {
754
759
  r.hidden = true
755
760
  return
756
761
  }
757
- // 📖 Apply both tier and origin filters — model is hidden if it fails either
758
- // 📖 TIER_LETTER_MAP is used so --tier S also includes S+ models (tier family behavior).
762
+ // 📖 Apply tier, origin, verdict, and health filters — model is hidden if it fails any
759
763
  const allowedTiers = (activeTier && TIER_LETTER_MAP[activeTier]) ? TIER_LETTER_MAP[activeTier] : [activeTier]
760
764
  const tierHide = activeTier !== null && !allowedTiers.includes(r.tier)
761
765
  const originHide = activeOrigin !== null && r.providerKey !== activeOrigin
762
- if (tierHide || originHide) {
766
+ // 📖 Verdict filter: match against getVerdict(r) when active
767
+ const rVerdict = getVerdict(r)
768
+ const verdictHide = activeVerdict !== null && rVerdict !== activeVerdict
769
+ // 📖 Health filter: match against r.status when active
770
+ const healthHide = activeHealth !== null && r.status !== activeHealth
771
+ if (tierHide || originHide || verdictHide || healthHide) {
763
772
  r.hidden = true
764
773
  return
765
774
  }
@@ -864,6 +873,7 @@ export async function runApp(cliArgs, config) {
864
873
  runUpdate,
865
874
  startOpenClaw,
866
875
  startOpenCodeDesktop,
876
+ startOpenCodeWeb,
867
877
  startOpenCode,
868
878
  startExternalTool,
869
879
  getToolModeOrder,
@@ -1042,7 +1052,10 @@ export async function runApp(cliArgs, config) {
1042
1052
  state.versionAlertsEnabled,
1043
1053
  state.favoritesPinnedAndSticky,
1044
1054
  state.customTextFilter,
1045
- state.lastReleaseDate
1055
+ state.lastReleaseDate,
1056
+ state.footerHidden,
1057
+ state.verdictFilterMode,
1058
+ state.healthFilterMode
1046
1059
  )
1047
1060
  }
1048
1061
  tableContent = state.commandPaletteFrozenTable
@@ -1077,7 +1090,10 @@ export async function runApp(cliArgs, config) {
1077
1090
  state.versionAlertsEnabled,
1078
1091
  state.favoritesPinnedAndSticky,
1079
1092
  state.customTextFilter,
1080
- state.lastReleaseDate
1093
+ state.lastReleaseDate,
1094
+ state.footerHidden,
1095
+ state.verdictFilterMode,
1096
+ state.healthFilterMode
1081
1097
  )
1082
1098
  }
1083
1099
 
@@ -1121,7 +1137,7 @@ export async function runApp(cliArgs, config) {
1121
1137
  pinFavorites: state.favoritesPinnedAndSticky,
1122
1138
  })
1123
1139
 
1124
- process.stdout.write(ALT_HOME + renderTable(state.results, state.pendingPings, state.frame, state.cursor, state.sortColumn, state.sortDirection, state.pingInterval, state.lastPingTime, state.mode, state.tierFilterMode, state.scrollOffset, state.terminalRows, state.terminalCols, state.originFilterMode, null, state.pingMode, state.pingModeSource, state.hideUnconfiguredModels, state.widthWarningStartedAt, state.widthWarningDismissed, state.widthWarningShowCount, state.settingsUpdateState, state.settingsUpdateLatestVersion, false, state.startupLatestVersion, state.versionAlertsEnabled, state.favoritesPinnedAndSticky, state.customTextFilter, state.lastReleaseDate, state.footerHidden))
1140
+ process.stdout.write(ALT_HOME + renderTable(state.results, state.pendingPings, state.frame, state.cursor, state.sortColumn, state.sortDirection, state.pingInterval, state.lastPingTime, state.mode, state.tierFilterMode, state.scrollOffset, state.terminalRows, state.terminalCols, state.originFilterMode, null, state.pingMode, state.pingModeSource, state.hideUnconfiguredModels, state.widthWarningStartedAt, state.widthWarningDismissed, state.widthWarningShowCount, state.settingsUpdateState, state.settingsUpdateLatestVersion, false, state.startupLatestVersion, state.versionAlertsEnabled, state.favoritesPinnedAndSticky, state.customTextFilter, state.lastReleaseDate, state.footerHidden, state.verdictFilterMode, state.healthFilterMode))
1125
1141
  if (process.stdout.isTTY) {
1126
1142
  process.stdout.flush && process.stdout.flush()
1127
1143
  }
@@ -20,6 +20,7 @@ import { TOOL_METADATA, TOOL_MODE_ORDER } from './tool-metadata.js'
20
20
  const TOOL_MODE_DESCRIPTIONS = {
21
21
  opencode: 'Launch in OpenCode CLI with the selected model.',
22
22
  'opencode-desktop': 'Set model in shared config, then open OpenCode Desktop.',
23
+ 'opencode-web': 'Set model in shared config, then open OpenCode WebUI.',
23
24
  openclaw: 'Set default model in OpenClaw and launch it.',
24
25
  crush: 'Launch Crush with this provider/model pair.',
25
26
  goose: 'Launch Goose and preselect the active model.',
package/src/constants.js CHANGED
@@ -77,6 +77,14 @@ export const FRAMES = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','
77
77
  // 📖 Index 0 = no filter (show all), then each tier name in descending quality order.
78
78
  export const TIER_CYCLE = [null, 'S+', 'S', 'A+', 'A', 'A-', 'B+', 'B', 'C']
79
79
 
80
+ // 📖 VERDICT_CYCLE: cycles through health verdict labels (0=All, then each verdict).
81
+ // 📖 Based on VERDICT_ORDER from utils.js — includes all possible getVerdict() values.
82
+ export const VERDICT_CYCLE = [null, 'Perfect', 'Normal', 'Slow', 'Spiky', 'Very Slow', 'Overloaded', 'Unstable', 'Not Active', 'Pending']
83
+
84
+ // 📖 HEALTH_CYCLE: cycles through ping status states (0=All, then each status).
85
+ // 📖 Based on the status values in the app: up, timeout, down, auth_error, noauth, pending.
86
+ export const HEALTH_CYCLE = [null, 'up', 'timeout', 'down', 'auth_error', 'noauth', 'pending']
87
+
80
88
  // 📖 Overlay background chalk functions — each overlay panel has a distinct tint
81
89
  // 📖 so users can tell Settings, Help, Recommend, and Log panels apart at a glance.
82
90
  export const SETTINGS_OVERLAY_BG = chalk.bgRgb(0, 0, 0)
@@ -93,8 +101,8 @@ export const WIDTH_WARNING_MIN_COLS = 80
93
101
 
94
102
  // 📖 Table row-budget constants — must stay in sync with renderTable()'s actual output.
95
103
  // 📖 If this drifts, model rows overflow and can push the title row out of view.
96
- export const TABLE_HEADER_LINES = 4 // 📖 title, spacer, column headers, separator
97
- export const TABLE_FOOTER_LINES = 5 // 📖 spacer, hints line 1, hints line 2, spacer, credit+links
104
+ export const TABLE_HEADER_LINES = 5 // 📖 title, filter bar, spacer, column headers, separator
105
+ export const TABLE_FOOTER_LINES = 1 // 📖 single toggle-hint line when collapsed, full footer otherwise
98
106
  export const TABLE_FIXED_LINES = TABLE_HEADER_LINES + TABLE_FOOTER_LINES
99
107
 
100
108
  // ─── Small cell-formatting helpers ────────────────────────────────────────────
@@ -39,7 +39,7 @@ import { getLastLayout, COLUMN_SORT_MAP } from './render-table.js'
39
39
  import { cycleThemeSetting, detectActiveTheme } from './theme.js'
40
40
  import { syncShellEnv, ensureShellRcSource, removeShellEnv } from './shell-env.js'
41
41
  import { buildCommandPaletteTree, flattenCommandTree, filterCommandPaletteEntries } from './command-palette.js'
42
- import { WIDTH_WARNING_MIN_COLS } from './constants.js'
42
+ import { WIDTH_WARNING_MIN_COLS, VERDICT_CYCLE, HEALTH_CYCLE } from './constants.js'
43
43
  import { scanAllToolConfigs, softDeleteModel } from './installed-models-manager.js'
44
44
  import { startExternalTool } from './tool-launchers.js'
45
45
 
@@ -264,6 +264,7 @@ export function createKeyHandler(ctx) {
264
264
  runUpdate,
265
265
  startOpenClaw,
266
266
  startOpenCodeDesktop,
267
+ startOpenCodeWeb,
267
268
  startOpenCode,
268
269
  startExternalTool,
269
270
  getToolModeOrder,
@@ -495,6 +496,8 @@ export function createKeyHandler(ctx) {
495
496
  exitCode = await startOpenClaw(userSelected, state.config, { launchCli: true })
496
497
  } else if (state.mode === 'opencode-desktop') {
497
498
  exitCode = await startOpenCodeDesktop(userSelected, state.config)
499
+ } else if (state.mode === 'opencode-web') {
500
+ exitCode = await startOpenCodeWeb(userSelected, state.config)
498
501
  } else if (state.mode === 'opencode') {
499
502
  exitCode = await startOpenCode(userSelected, state.config)
500
503
  } else {
@@ -2408,13 +2411,13 @@ export function createKeyHandler(ctx) {
2408
2411
  return
2409
2412
  }
2410
2413
 
2411
- // 📖 Alt+W: toggle footer visibility (collapse to single hint when hidden)
2412
- // 📖 Note: readline doesn't set key.alt=true for ALT combos; detect via str starting with \x1b (ESC)
2413
- if (key.name === 'w' && str && str.startsWith('\x1b') && !key.ctrl && !key.meta) {
2414
+ // 📖 Ctrl+O: toggle footer visibility (collapse to single hint when hidden)
2415
+ if (key.ctrl && key.name === 'o' && !key.meta) {
2414
2416
  state.footerHidden = !state.footerHidden
2415
2417
  if (!state.config.settings || typeof state.config.settings !== 'object') state.config.settings = {}
2416
2418
  state.config.settings.footerHidden = state.footerHidden
2417
2419
  saveConfig(state.config)
2420
+ state.frame++ // 📖 Force immediate re-render
2418
2421
  return
2419
2422
  }
2420
2423
 
@@ -2450,6 +2453,24 @@ export function createKeyHandler(ctx) {
2450
2453
  return
2451
2454
  }
2452
2455
 
2456
+ // 📖 Verdict filter key: V = cycle through each verdict (All → Perfect → Normal → Slow → ... → All)
2457
+ if (key.name === 'v') {
2458
+ state.verdictFilterMode = (state.verdictFilterMode + 1) % VERDICT_CYCLE.length
2459
+ applyTierFilter()
2460
+ refreshVisibleSorted({ resetCursor: true })
2461
+ persistUiSettings()
2462
+ return
2463
+ }
2464
+
2465
+ // 📖 Health filter key: H = cycle through each health status (All → Up → Timeout → Down → ... → All)
2466
+ if (key.name === 'h') {
2467
+ state.healthFilterMode = (state.healthFilterMode + 1) % HEALTH_CYCLE.length
2468
+ applyTierFilter()
2469
+ refreshVisibleSorted({ resetCursor: true })
2470
+ persistUiSettings()
2471
+ return
2472
+ }
2473
+
2453
2474
  // 📖 Help overlay key: Ctrl+H = toggle help overlay
2454
2475
  if (key.ctrl && key.name === 'h') {
2455
2476
  state.helpVisible = !state.helpVisible
package/src/opencode.js CHANGED
@@ -543,6 +543,80 @@ export async function startOpenCode(model, fcmConfig) {
543
543
  await spawnOpenCode(['--model', modelRef], providerKey, fcmConfig)
544
544
  }
545
545
 
546
+ // ─── Start OpenCode Web ───────────────────────────────────────────────────────
547
+
548
+ export async function startOpenCodeWeb(model, fcmConfig) {
549
+ const providerKey = model.providerKey ?? 'nvidia'
550
+ const ocModelId = getOpenCodeModelId(providerKey, model.modelId)
551
+ const modelRef = `${providerKey}/${ocModelId}`
552
+
553
+ const launchWeb = async () => {
554
+ const { exec } = await import('child_process')
555
+ const url = 'https://opencode.ai'
556
+ let command
557
+ if (isMac) {
558
+ command = `open "${url}"`
559
+ } else if (isWindows) {
560
+ command = `start "${url}"`
561
+ } else {
562
+ command = `xdg-open "${url}"`
563
+ }
564
+ exec(command, (err) => {
565
+ if (err) {
566
+ console.error(chalk.red(' Could not open OpenCode WebUI'))
567
+ console.error(chalk.dim(` Please visit ${url} manually`))
568
+ }
569
+ })
570
+ }
571
+
572
+ // 📖 Mirror OpenCode Desktop behavior: set the model in opencode.json
573
+ const config = loadOpenCodeConfig()
574
+ const backupPath = `${getOpenCodeConfigPath()}.backup-${Date.now()}`
575
+
576
+ if (existsSync(getOpenCodeConfigPath())) {
577
+ copyFileSync(getOpenCodeConfigPath(), backupPath)
578
+ console.log(chalk.dim(` Backup: ${backupPath}`))
579
+ }
580
+
581
+ if (!config.provider) config.provider = {}
582
+
583
+ // 📖 Provider-specific config setup (same as CLI/Desktop)
584
+ if (providerKey === 'nvidia' && !config.provider.nvidia) {
585
+ config.provider.nvidia = {
586
+ npm: '@ai-sdk/openai-compatible',
587
+ name: 'NVIDIA NIM',
588
+ options: { baseURL: 'https://integrate.api.nvidia.com/v1', apiKey: '{env:NVIDIA_API_KEY}' },
589
+ models: {}
590
+ }
591
+ } else if (providerKey === 'groq' && !config.provider.groq) {
592
+ config.provider.groq = { options: { apiKey: '{env:GROQ_API_KEY}' }, models: {} }
593
+ } else if (providerKey === 'cerebras' && !config.provider.cerebras) {
594
+ config.provider.cerebras = {
595
+ npm: '@ai-sdk/openai-compatible',
596
+ name: 'Cerebras',
597
+ options: { baseURL: 'https://api.cerebras.ai/v1', apiKey: '{env:CEREBRAS_API_KEY}' },
598
+ models: {}
599
+ }
600
+ }
601
+ // ... other providers are handled as they are selected
602
+
603
+ console.log(chalk.green(` Setting ${chalk.bold(model.label)} as default (mirroring Desktop)...`))
604
+
605
+ if (providerKey !== 'opencode-zen' && config.provider[providerKey]) {
606
+ if (!config.provider[providerKey].models) config.provider[providerKey].models = {}
607
+ config.provider[providerKey].models[ocModelId] = { name: model.label }
608
+ }
609
+
610
+ config.model = providerKey === 'opencode-zen' ? `opencode/${ocModelId}` : modelRef
611
+ saveOpenCodeConfig(config)
612
+
613
+ console.log(chalk.dim(` Config saved to: ${getOpenCodeConfigPath()}`))
614
+ console.log(chalk.dim(' Opening OpenCode WebUI...'))
615
+ console.log()
616
+
617
+ await launchWeb()
618
+ }
619
+
546
620
  // ─── Start OpenCode Desktop ───────────────────────────────────────────────────
547
621
 
548
622
  export async function startOpenCodeDesktop(model, fcmConfig) {