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 +40 -38
- package/README.md +9 -6
- package/package.json +1 -1
- package/sources.js +5 -5
- package/src/app.js +24 -8
- package/src/command-palette.js +1 -0
- package/src/constants.js +10 -2
- package/src/key-handler.js +25 -4
- package/src/opencode.js +74 -0
- package/src/render-table.js +209 -127
- package/src/shell-env.js +3 -0
- package/src/tool-metadata.js +4 -2
- package/src/updater.js +2 -0
- package/src/utils.js +4 -2
- package/web/dist/assets/{index-DOtGtGLl.js → index-D49esfAN.js} +1 -1
- package/web/dist/index.html +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,41 +1,43 @@
|
|
|
1
|
-
## [0.3.
|
|
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
|
-
- **
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
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 | [
|
|
80
|
-
| 2 | [
|
|
81
|
-
| 3 | [
|
|
82
|
-
| 4 | [
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
}
|
package/src/command-palette.js
CHANGED
|
@@ -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 =
|
|
97
|
-
export const TABLE_FOOTER_LINES =
|
|
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 ────────────────────────────────────────────
|
package/src/key-handler.js
CHANGED
|
@@ -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
|
-
// 📖
|
|
2412
|
-
|
|
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) {
|