free-coding-models 0.5.28 → 0.5.30
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/README.md +1 -1
- package/bin/free-coding-models.js +10 -2
- package/changelog/v0.5.29.md +19 -0
- package/changelog/v0.5.30.md +15 -0
- package/package.json +1 -1
- package/sources.js +3 -2
- package/src/core/config.js +1 -14
- package/src/core/endpoint-installer.js +5 -9
- package/src/core/legacy-proxy-cleanup.js +2 -0
- package/src/core/model-merger.js +4 -13
- package/src/core/playground.js +6 -4
- package/src/core/router-daemon.js +101 -43
- package/src/core/router-dashboard.js +63 -25
- package/src/core/shared-helpers.js +117 -0
- package/src/core/sync-set.js +11 -24
- package/src/core/tool-launchers.js +24 -92
- package/src/core/utils.js +26 -100
- package/src/tui/app.js +2 -2
- package/src/tui/command-palette.js +1 -0
- package/src/tui/key-handler.js +77 -20
- package/src/tui/render-table.js +3 -3
- package/web/dist/assets/index-CsFt3qt5.css +1 -0
- package/web/dist/assets/index-I6N91rH7.js +40 -0
- package/web/dist/favicon.ico +0 -0
- package/web/dist/favicons/apple-touch-icon.png +0 -0
- package/web/dist/favicons/favicon-16x16.png +0 -0
- package/web/dist/favicons/favicon-192x192.png +0 -0
- package/web/dist/favicons/favicon-32x32.png +0 -0
- package/web/dist/favicons/favicon-48x48.png +0 -0
- package/web/dist/favicons/favicon-512x512.png +0 -0
- package/web/dist/favicons/favicon-96x96.png +0 -0
- package/web/dist/favicons/favicon.ico +0 -0
- package/web/dist/favicons/mstile-150x150.png +0 -0
- package/web/dist/favicons/mstile-310x150.png +0 -0
- package/web/dist/favicons/mstile-310x310.png +0 -0
- package/web/dist/favicons/mstile-512x512.png +0 -0
- package/web/dist/favicons/mstile-70x70.png +0 -0
- package/web/dist/index.html +2 -2
- package/web/public/favicon.ico +0 -0
- package/web/public/favicons/apple-touch-icon.png +0 -0
- package/web/public/favicons/favicon-16x16.png +0 -0
- package/web/public/favicons/favicon-192x192.png +0 -0
- package/web/public/favicons/favicon-32x32.png +0 -0
- package/web/public/favicons/favicon-48x48.png +0 -0
- package/web/public/favicons/favicon-512x512.png +0 -0
- package/web/public/favicons/favicon-96x96.png +0 -0
- package/web/public/favicons/favicon.ico +0 -0
- package/web/public/favicons/mstile-150x150.png +0 -0
- package/web/public/favicons/mstile-310x150.png +0 -0
- package/web/public/favicons/mstile-310x310.png +0 -0
- package/web/public/favicons/mstile-512x512.png +0 -0
- package/web/public/favicons/mstile-70x70.png +0 -0
- package/web/server.js +34 -4
- package/web/src/components/dashboard/ExpandedDetailRow.jsx +10 -113
- package/web/src/components/dashboard/ExpandedDetailRow.module.css +9 -0
- package/web/src/components/playground/PlaygroundChat.jsx +527 -0
- package/web/src/components/playground/PlaygroundChat.module.css +382 -0
- package/web/src/components/playground/PlaygroundView.jsx +104 -442
- package/web/src/components/playground/PlaygroundView.module.css +10 -0
- package/web/src/components/router/RouterView.jsx +268 -56
- package/web/src/components/router/RouterView.module.css +416 -1
- package/src/core/product-flags.js +0 -9
- package/web/dist/assets/index-BRFowJv5.css +0 -1
- package/web/dist/assets/index-Pr3waI0-.js +0 -41
package/src/core/utils.js
CHANGED
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
* @exports sortResults, filterByTier, findBestModel, parseArgs
|
|
42
42
|
* @exports scoreModelForTask, getTopRecommendations
|
|
43
43
|
* @exports TIER_ORDER, VERDICT_ORDER, TIER_LETTER_MAP, TASK_TYPES, PRIORITY_TYPES, CONTEXT_BUDGETS
|
|
44
|
+
* @exports parseCtxToK, parseSweToNum, formatCtxWindow, labelFromId, NEW_MODELS, getVersionStatusInfo, formatResultsAsJSON
|
|
44
45
|
*
|
|
45
46
|
* @see bin/free-coding-models.js — main CLI that imports these utils
|
|
46
47
|
* @see sources.js — model definitions consumed by these functions
|
|
@@ -115,80 +116,27 @@ export const getAvg = (r) => {
|
|
|
115
116
|
//
|
|
116
117
|
// 📖 The "wasUpBefore" check is key — it distinguishes between a model that's
|
|
117
118
|
// temporarily flaky vs one that was never reachable in the first place.
|
|
119
|
+
// 📖 NEW_MODEL_DURATION_MS — how long a model shows the 🆕 badge after being added.
|
|
120
|
+
// 📖 5 days in milliseconds.
|
|
121
|
+
export const NEW_MODEL_DURATION_MS = 5 * 24 * 60 * 60 * 1000
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 📖 Check if a model should display the 🆕 badge.
|
|
125
|
+
* 📖 A model is "new" if its addedDate is within the last 5 days.
|
|
126
|
+
* 📖 `addedDate` comes from sources.js as the optional 6th element of model tuples.
|
|
127
|
+
* @param {string|null|undefined} addedDate — ISO date string (e.g. '2026-06-10')
|
|
128
|
+
* @returns {boolean}
|
|
129
|
+
*/
|
|
130
|
+
export function isNewModel(addedDate) {
|
|
131
|
+
if (!addedDate || typeof addedDate !== 'string') return false
|
|
132
|
+
const added = Date.parse(addedDate)
|
|
133
|
+
if (!Number.isFinite(added)) return false
|
|
134
|
+
return (Date.now() - added) < NEW_MODEL_DURATION_MS
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 📖 NEW_MODELS kept for backward compat with web/website surfaces that
|
|
138
|
+
// 📖 don't have addedDate. Will be removed once all surfaces migrate.
|
|
118
139
|
export const NEW_MODELS = new Set([
|
|
119
|
-
'nvidia/nemotron-3-ultra-550b-a55b',
|
|
120
|
-
'@cf/meta/llama-3.2-90b-instruct',
|
|
121
|
-
'@cf/mistralai/mistral-7b-instruct-v0.2',
|
|
122
|
-
'@cf/google/gemma-2-9b-it',
|
|
123
|
-
'@cf/anthropic/claude-3-5-sonnet',
|
|
124
|
-
'@cf/openai/gpt-4o-mini',
|
|
125
|
-
'@cf/qwen/qwen3-30b-a3b-fp8',
|
|
126
|
-
'@cf/qwen/qwen2.5-coder-32b-instruct',
|
|
127
|
-
'@cf/meta/llama-3.3-70b-instruct-fp8-fast',
|
|
128
|
-
'@cf/google/gemma-4-26b-a4b-it',
|
|
129
|
-
'@cf/mistralai/mistral-small-3.1-24b-instruct',
|
|
130
|
-
'@cf/ibm/granite-4.0-h-micro',
|
|
131
|
-
'minimaxai/minimax-m2.7',
|
|
132
|
-
'z-ai/glm-5.1',
|
|
133
|
-
'moonshotai/kimi-k2.6',
|
|
134
|
-
'stepfun-ai/step-3.5-flash',
|
|
135
|
-
'stepfun-ai/step-3.7-flash',
|
|
136
|
-
'qwen/qwen3-coder-480b-a35b-instruct',
|
|
137
|
-
'qwen/qwen3.5-397b-a17b',
|
|
138
|
-
'meta/llama-4-maverick-17b-128e-instruct',
|
|
139
|
-
'mistralai/mistral-medium-3.5-128b',
|
|
140
|
-
'mistralai/mistral-small-4-119b-2603',
|
|
141
|
-
'qwen/qwen3.5-122b-a10b',
|
|
142
|
-
'mistralai/mistral-large-3-675b-instruct-2512',
|
|
143
|
-
'nvidia/nemotron-3-super-120b-a12b',
|
|
144
|
-
'nvidia/nemotron-3-nano-omni-30b-a3b-reasoning',
|
|
145
|
-
'google/gemma-4-31b-it',
|
|
146
|
-
'bytedance/seed-oss-36b-instruct',
|
|
147
|
-
'stockmark/stockmark-2-100b-instruct',
|
|
148
|
-
'mistralai/ministral-14b-instruct-2512',
|
|
149
|
-
'meta/llama-3.2-11b-vision-instruct',
|
|
150
|
-
'microsoft/phi-4-mini-instruct',
|
|
151
|
-
'gemma-3-12b-it',
|
|
152
|
-
'nvidia/nemotron-nano-9b-v2',
|
|
153
|
-
'openrouter/owl-alpha',
|
|
154
|
-
'nousresearch/hermes-3-llama-3.1-405b:free',
|
|
155
|
-
'nvidia/nemotron-nano-30b-a3b:free',
|
|
156
|
-
'cognitivecomputations/dolphin-mistral-24b-venice-edition:free',
|
|
157
|
-
'meta-llama/llama-3.3-70b-instruct:free',
|
|
158
|
-
'meta-llama/llama-3.2-3b-instruct:free',
|
|
159
|
-
'liquid/lfm-2.5-1.2b-instruct:free',
|
|
160
|
-
'liquid/lfm-2.5-1.2b-thinking:free',
|
|
161
|
-
'qwen3.7-max',
|
|
162
|
-
'qwen3-max',
|
|
163
|
-
'qwen3.6-plus',
|
|
164
|
-
'qwen3-235b-a22b',
|
|
165
|
-
'qwen3.5-plus',
|
|
166
|
-
'qwen3-coder-plus',
|
|
167
|
-
'qwen3-coder-next',
|
|
168
|
-
'qwen3.6-flash',
|
|
169
|
-
'qwen3.5-flash',
|
|
170
|
-
'qwen3-coder-flash',
|
|
171
|
-
'qwen3-32b',
|
|
172
|
-
'qwen3-coder-30b-a3b-instruct',
|
|
173
|
-
'holo2-30b-a3b',
|
|
174
|
-
'llama-3.3-70b-instruct',
|
|
175
|
-
'mistral-small-3.2-24b-instruct-2506',
|
|
176
|
-
'gemma-3-27b-it',
|
|
177
|
-
'qwen3.5-397b-a17b',
|
|
178
|
-
'qwen3-coder-30b-a3b-instruct',
|
|
179
|
-
'gpt-oss-120b',
|
|
180
|
-
'gpt-oss-20b',
|
|
181
|
-
'Meta-Llama-3_3-70B-Instruct',
|
|
182
|
-
'Qwen3-32B',
|
|
183
|
-
'Mistral-Small-3.2-24B-Instruct-2506',
|
|
184
|
-
'Mistral-7B-Instruct-v0.3',
|
|
185
|
-
'Mistral-Nemo-Instruct-2407',
|
|
186
|
-
'Qwen3.5-9B',
|
|
187
|
-
'big-pickle',
|
|
188
|
-
'deepseek-v4-flash-free',
|
|
189
|
-
'mimo-v2.5-free',
|
|
190
|
-
'nemotron-3-super-free',
|
|
191
|
-
'minimax-m3-free'
|
|
192
140
|
]);
|
|
193
141
|
|
|
194
142
|
export const getVerdict = (r) => {
|
|
@@ -376,34 +324,12 @@ export const sortResults = (results, sortColumn, sortDirection, { benchmarkResul
|
|
|
376
324
|
break
|
|
377
325
|
case 'swe': {
|
|
378
326
|
// 📖 Sort by SWE-bench score — higher is better
|
|
379
|
-
|
|
380
|
-
const parseSwe = (score) => {
|
|
381
|
-
if (!score || score === '—') return 0
|
|
382
|
-
const num = parseFloat(score.replace('%', ''))
|
|
383
|
-
return isNaN(num) ? 0 : num
|
|
384
|
-
}
|
|
385
|
-
cmp = parseSwe(a.sweScore) - parseSwe(b.sweScore)
|
|
327
|
+
cmp = parseSweToNum(a.sweScore) - parseSweToNum(b.sweScore)
|
|
386
328
|
break
|
|
387
329
|
}
|
|
388
330
|
case 'ctx': {
|
|
389
|
-
// 📖 Sort by context window size — larger is better
|
|
390
|
-
|
|
391
|
-
const parseCtx = (ctx) => {
|
|
392
|
-
if (!ctx || ctx === '—') return 0
|
|
393
|
-
const str = ctx.toLowerCase()
|
|
394
|
-
// 📖 Handle millions (1m = 1000k)
|
|
395
|
-
if (str.includes('m')) {
|
|
396
|
-
const num = parseFloat(str.replace('m', ''))
|
|
397
|
-
return num * 1000
|
|
398
|
-
}
|
|
399
|
-
// 📖 Handle thousands (128k)
|
|
400
|
-
if (str.includes('k')) {
|
|
401
|
-
const num = parseFloat(str.replace('k', ''))
|
|
402
|
-
return num
|
|
403
|
-
}
|
|
404
|
-
return 0
|
|
405
|
-
}
|
|
406
|
-
cmp = parseCtx(a.ctx) - parseCtx(b.ctx)
|
|
331
|
+
// 📖 Sort by context window size — larger is better (uses parseCtxToK)
|
|
332
|
+
cmp = parseCtxToK(a.ctx) - parseCtxToK(b.ctx)
|
|
407
333
|
break
|
|
408
334
|
}
|
|
409
335
|
case 'condition':
|
|
@@ -727,7 +653,7 @@ export const CONTEXT_BUDGETS = {
|
|
|
727
653
|
|
|
728
654
|
// 📖 parseCtxToK: Convert context window string ("128k", "1m", "200k") into numeric K tokens.
|
|
729
655
|
// 📖 Used by the scoring engine to compare against CONTEXT_BUDGETS thresholds.
|
|
730
|
-
function parseCtxToK(ctx) {
|
|
656
|
+
export function parseCtxToK(ctx) {
|
|
731
657
|
if (!ctx || ctx === '—') return 0
|
|
732
658
|
const str = ctx.toLowerCase()
|
|
733
659
|
if (str.includes('m')) return parseFloat(str.replace('m', '')) * 1000
|
|
@@ -756,7 +682,7 @@ export function labelFromId(id) {
|
|
|
756
682
|
|
|
757
683
|
// 📖 parseSweToNum: Convert SWE-bench score string ("49.2%", "73.1%") into a 0–100 number.
|
|
758
684
|
// 📖 Returns 0 for missing or invalid scores.
|
|
759
|
-
function parseSweToNum(sweScore) {
|
|
685
|
+
export function parseSweToNum(sweScore) {
|
|
760
686
|
if (!sweScore || sweScore === '—') return 0
|
|
761
687
|
const num = parseFloat(sweScore.replace('%', ''))
|
|
762
688
|
return isNaN(num) ? 0 : num
|
package/src/tui/app.js
CHANGED
|
@@ -307,8 +307,8 @@ export async function runApp(cliArgs, config, startupOptions = {}) {
|
|
|
307
307
|
|
|
308
308
|
let results = MODELS
|
|
309
309
|
.filter(([,,,,,providerKey]) => isProviderEnabled(config, providerKey))
|
|
310
|
-
.map(([modelId, label, tier, sweScore, ctx, providerKey], i) => ({
|
|
311
|
-
idx: i + 1, modelId, label, tier, sweScore, ctx, providerKey,
|
|
310
|
+
.map(([modelId, label, tier, sweScore, ctx, providerKey, addedDate], i) => ({
|
|
311
|
+
idx: i + 1, modelId, label, tier, sweScore, ctx, providerKey, addedDate: addedDate || null,
|
|
312
312
|
status: 'pending',
|
|
313
313
|
pings: [], // 📖 All ping results (ms or 'TIMEOUT')
|
|
314
314
|
httpCode: null,
|
|
@@ -202,6 +202,7 @@ const BASE_COMMAND_TREE = [
|
|
|
202
202
|
{ id: 'action-cycle-theme', label: 'Cycle theme', shortcut: 'G', icon: '🌗', description: 'Switch dark/light/auto', keywords: ['theme', 'dark', 'light', 'auto'] },
|
|
203
203
|
{ id: 'action-reset-view', label: 'Reset view', shortcut: 'N', icon: '\u{1F504}', description: 'Reset filters and sort', keywords: ['reset', 'view', 'sort', 'filters'] },
|
|
204
204
|
{ id: 'action-probe-404', label: 'Probe 404 Models', shortcut: 'Ctrl+Shift+P', icon: '\u{1F50D}', description: 'Test all configured models for 404/410 errors. Auto-hides broken models.', keywords: ['probe', '404', 'broken', 'dead', 'health', 'check', 'test', 'verify'] },
|
|
205
|
+
{ id: 'action-toggle-auto-hide-broken', label: 'Toggle Auto-hide Broken Models', icon: '\u{1F6A9}', description: 'Enable/disable auto-hiding of 404/410 models. Unhides all when disabled.', keywords: ['auto', 'hide', 'broken', '404', 'models', 'toggle', 'probe'] },
|
|
205
206
|
],
|
|
206
207
|
},
|
|
207
208
|
// 📖 Pages - directly at root level, not in submenu
|
package/src/tui/key-handler.js
CHANGED
|
@@ -39,6 +39,7 @@ import { join, dirname } from 'node:path'
|
|
|
39
39
|
import { fileURLToPath } from 'node:url'
|
|
40
40
|
import { spawn } from 'node:child_process'
|
|
41
41
|
import { cleanupLegacyProxyArtifacts } from '../core/legacy-proxy-cleanup.js'
|
|
42
|
+
import { sleep } from '../core/shared-helpers.js'
|
|
42
43
|
import { getLastLayout, COLUMN_SORT_MAP } from './render-table.js'
|
|
43
44
|
import { cycleThemeSetting, detectActiveTheme } from './theme.js'
|
|
44
45
|
import { syncShellEnv, ensureShellRcSource, removeShellEnv } from '../core/shell-env.js'
|
|
@@ -53,6 +54,7 @@ import {
|
|
|
53
54
|
cycleRouterDashboardProbeMode,
|
|
54
55
|
openRouterDashboardOverlay,
|
|
55
56
|
restartRouterDashboardDaemon,
|
|
57
|
+
setDashboardNotice,
|
|
56
58
|
toggleRouterDashboardProbePause,
|
|
57
59
|
} from '../core/router-dashboard.js'
|
|
58
60
|
import {
|
|
@@ -61,6 +63,7 @@ import {
|
|
|
61
63
|
handlePlaygroundKeypress,
|
|
62
64
|
} from '../core/playground.js'
|
|
63
65
|
import { benchmarkModel } from '../core/benchmark.js'
|
|
66
|
+
import { isPackageDevMode } from '../core/updater.js'
|
|
64
67
|
|
|
65
68
|
// 📖 Some providers need an explicit probe model because the first catalog entry
|
|
66
69
|
// 📖 is not guaranteed to be accepted by their chat endpoint.
|
|
@@ -76,6 +79,52 @@ const PROVIDER_TEST_MODEL_OVERRIDES = {
|
|
|
76
79
|
const SETTINGS_TEST_MAX_ATTEMPTS = 10
|
|
77
80
|
const SETTINGS_TEST_RETRY_DELAY_MS = 4000
|
|
78
81
|
|
|
82
|
+
// 📖 spawnDaemonCommand — spawns `--daemon-bg` or `--daemon-stop` and captures
|
|
83
|
+
// 📖 the JSON result from stdout so we can surface startup errors to the user.
|
|
84
|
+
// 📖 Falls back to a generic notice when stdout isn't parseable.
|
|
85
|
+
function spawnDaemonCommand(state, args) {
|
|
86
|
+
const binPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'bin', 'free-coding-models.js')
|
|
87
|
+
// 📖 Inherit FCM_DEV so the spawned daemon matches the TUI's dev/prod mode.
|
|
88
|
+
// 📖 Without this, a dev-mode TUI (git checkout) spawns a production daemon
|
|
89
|
+
// 📖 that writes to the wrong PID/port files and can't be discovered later.
|
|
90
|
+
// 📖 isPackageDevMode() detects git checkouts even without --dev or FCM_DEV=1.
|
|
91
|
+
const env = { ...process.env }
|
|
92
|
+
if (isPackageDevMode() && !env.FCM_DEV) env.FCM_DEV = '1'
|
|
93
|
+
const child = spawn('node', [binPath, ...args], {
|
|
94
|
+
detached: true,
|
|
95
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
96
|
+
env,
|
|
97
|
+
})
|
|
98
|
+
child.unref()
|
|
99
|
+
|
|
100
|
+
// 📖 Only capture stdout for start commands — stop doesn't need error surfacing.
|
|
101
|
+
if (!args.includes('--daemon-bg')) return
|
|
102
|
+
|
|
103
|
+
let stdout = ''
|
|
104
|
+
let stderr = ''
|
|
105
|
+
child.stdout?.on('data', (chunk) => { stdout += chunk })
|
|
106
|
+
child.stderr?.on('data', (chunk) => { stderr += chunk })
|
|
107
|
+
child.on('close', (code) => {
|
|
108
|
+
if (code === 0) return
|
|
109
|
+
// 📖 Try to parse the JSON result from --daemon-bg for a precise error message.
|
|
110
|
+
let errorMsg = null
|
|
111
|
+
try {
|
|
112
|
+
const parsed = JSON.parse(stdout.trim())
|
|
113
|
+
if (parsed?.error) errorMsg = parsed.error
|
|
114
|
+
} catch {}
|
|
115
|
+
if (!errorMsg && stderr.trim()) {
|
|
116
|
+
// 📖 Common startup crash reasons: port in use, config corruption, etc.
|
|
117
|
+
const lines = stderr.trim().split('\n').filter(l => !l.startsWith('node:'))
|
|
118
|
+
errorMsg = lines.slice(0, 2).join(' — ') || stderr.trim().slice(0, 120)
|
|
119
|
+
}
|
|
120
|
+
if (errorMsg) {
|
|
121
|
+
setDashboardNotice(state, 'error', `Daemon failed to start: ${errorMsg}`, 8000)
|
|
122
|
+
} else if (code !== 0) {
|
|
123
|
+
setDashboardNotice(state, 'error', `Daemon failed to start (exit code ${code})`, 8000)
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
79
128
|
// 📖 PROVIDER_AUTH_ENDPOINTS maps provider keys to their auth-check URL + method.
|
|
80
129
|
// 📖 For most providers this is the /models endpoint (returns 200=valid, 401=invalid).
|
|
81
130
|
// 📖 Providers without an auth-check endpoint use null (falls back to chat completion ping).
|
|
@@ -114,11 +163,7 @@ const PROVIDER_AUTH_ENDPOINTS = {
|
|
|
114
163
|
'ollama-cloud': { url: 'https://ollama.com/v1/models', method: 'GET' },
|
|
115
164
|
}
|
|
116
165
|
|
|
117
|
-
// 📖 Sleep
|
|
118
|
-
// 📖 back off between retries without leaking timer logic into the rest of the TUI.
|
|
119
|
-
function sleep(ms) {
|
|
120
|
-
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
121
|
-
}
|
|
166
|
+
// 📖 Sleep imported from shared-helpers.js
|
|
122
167
|
|
|
123
168
|
// 📖 testProviderKeyDirect: Fast auth-only check using /v1/account or /v1/models.
|
|
124
169
|
// 📖 Fires 3 parallel probes to get a fast decisive result (auth error vs timeout vs 200).
|
|
@@ -1613,6 +1658,7 @@ export function createKeyHandler(ctx) {
|
|
|
1613
1658
|
case 'action-toggle-favorite-mode': return toggleFavoritesDisplayMode()
|
|
1614
1659
|
case 'action-reset-view': return resetViewSettings()
|
|
1615
1660
|
case 'action-probe-404': return runBrokenModelProbe(state)
|
|
1661
|
+
case 'action-toggle-auto-hide-broken': return toggleAutoHideBrokenModels()
|
|
1616
1662
|
default:
|
|
1617
1663
|
return
|
|
1618
1664
|
}
|
|
@@ -1805,15 +1851,9 @@ export function createKeyHandler(ctx) {
|
|
|
1805
1851
|
// 📖 S: Toggle daemon start/stop
|
|
1806
1852
|
if (key.name === 's') {
|
|
1807
1853
|
const isRunning = state.routerDashboardStatus === 'ready' || state.routerDashboardStatus === 'partial'
|
|
1808
|
-
const binPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'bin', 'free-coding-models.js')
|
|
1809
1854
|
const args = isRunning ? ['--daemon-stop'] : ['--daemon-bg']
|
|
1810
|
-
|
|
1811
1855
|
state.routerDashboardStatus = 'loading'
|
|
1812
|
-
|
|
1813
|
-
detached: true,
|
|
1814
|
-
stdio: 'ignore',
|
|
1815
|
-
})
|
|
1816
|
-
child.unref()
|
|
1856
|
+
spawnDaemonCommand(state, args)
|
|
1817
1857
|
return
|
|
1818
1858
|
}
|
|
1819
1859
|
|
|
@@ -1824,15 +1864,9 @@ export function createKeyHandler(ctx) {
|
|
|
1824
1864
|
|
|
1825
1865
|
if ((state.routerDashboardCursorIndex ?? 0) === btnCursor) {
|
|
1826
1866
|
const isRunning = state.routerDashboardStatus === 'ready' || state.routerDashboardStatus === 'partial'
|
|
1827
|
-
const binPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'bin', 'free-coding-models.js')
|
|
1828
1867
|
const args = isRunning ? ['--daemon-stop'] : ['--daemon-bg']
|
|
1829
|
-
|
|
1830
1868
|
state.routerDashboardStatus = 'loading'
|
|
1831
|
-
|
|
1832
|
-
detached: true,
|
|
1833
|
-
stdio: 'ignore',
|
|
1834
|
-
})
|
|
1835
|
-
child.unref()
|
|
1869
|
+
spawnDaemonCommand(state, args)
|
|
1836
1870
|
} else if ((state.routerDashboardCursorIndex ?? 0) === installBtnCursor) {
|
|
1837
1871
|
state.routerDashboardOpen = false
|
|
1838
1872
|
state.installEndpointsOpen = true
|
|
@@ -2413,9 +2447,32 @@ export function createKeyHandler(ctx) {
|
|
|
2413
2447
|
const binPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'bin', 'free-coding-models.js')
|
|
2414
2448
|
const child = spawn('node', [binPath, '--daemon-bg'], {
|
|
2415
2449
|
detached: true,
|
|
2416
|
-
stdio: 'ignore',
|
|
2450
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
2451
|
+
env: { ...process.env, ...(isPackageDevMode() && !process.env.FCM_DEV ? { FCM_DEV: '1' } : {}) },
|
|
2417
2452
|
})
|
|
2418
2453
|
child.unref()
|
|
2454
|
+
|
|
2455
|
+
// 📖 Capture daemon startup output so we can show the real error reason
|
|
2456
|
+
let onboardingStdout = ''
|
|
2457
|
+
let onboardingStderr = ''
|
|
2458
|
+
child.stdout?.on('data', (chunk) => { onboardingStdout += chunk })
|
|
2459
|
+
child.stderr?.on('data', (chunk) => { onboardingStderr += chunk })
|
|
2460
|
+
child.on('close', (code) => {
|
|
2461
|
+
if (code === 0) return
|
|
2462
|
+
let detail = null
|
|
2463
|
+
try {
|
|
2464
|
+
const parsed = JSON.parse(onboardingStdout.trim())
|
|
2465
|
+
if (parsed?.error) detail = parsed.error
|
|
2466
|
+
} catch {}
|
|
2467
|
+
if (!detail && onboardingStderr.trim()) {
|
|
2468
|
+
const lines = onboardingStderr.trim().split('\n').filter(l => !l.startsWith('node:'))
|
|
2469
|
+
detail = lines.slice(0, 2).join(' — ') || onboardingStderr.trim().slice(0, 120)
|
|
2470
|
+
}
|
|
2471
|
+
if (state.routerOnboardingPhase === 'loading') {
|
|
2472
|
+
state.routerOnboardingPhase = 'error'
|
|
2473
|
+
state.routerOnboardingError = detail || `Daemon exited with code ${code}`
|
|
2474
|
+
}
|
|
2475
|
+
})
|
|
2419
2476
|
await new Promise((r) => setTimeout(r, 2000))
|
|
2420
2477
|
if (state.routerOnboardingPhase === 'loading') {
|
|
2421
2478
|
state.routerOnboardingPhase = 'success'
|
package/src/tui/render-table.js
CHANGED
|
@@ -49,7 +49,7 @@ import {
|
|
|
49
49
|
} from '../core/constants.js'
|
|
50
50
|
import { themeColors, currentPalette, getProviderRgb, getTierRgb, getReadableTextRgb, getTheme, THEME_BG_RGB } from './theme.js'
|
|
51
51
|
import { TIER_COLOR } from './tier-colors.js'
|
|
52
|
-
import { getAvg, getVerdict, getUptime, getStabilityScore, getVersionStatusInfo,
|
|
52
|
+
import { getAvg, getVerdict, getUptime, getStabilityScore, getVersionStatusInfo, isNewModel } from '../core/utils.js'
|
|
53
53
|
import { usagePlaceholderForProvider } from '../core/ping.js'
|
|
54
54
|
import { formatBenchmarkLatency, formatBenchmarkTps } from '../core/benchmark.js'
|
|
55
55
|
import { calculateViewport, sortResultsWithPinnedFavorites, padEndDisplay, displayWidth, stripAnsi, fadedRow } from './render-helpers.js'
|
|
@@ -656,13 +656,13 @@ export function renderTable({
|
|
|
656
656
|
: providerName
|
|
657
657
|
const source = themeColors.provider(r.providerKey, providerDisplay.padEnd(wSource))
|
|
658
658
|
// 📖 Prefix: ⭐ favorite > 🎯 recommended > 🆕 new — only one emoji, never shifts the line
|
|
659
|
-
const
|
|
659
|
+
const modelIsNew = isNewModel(r.addedDate)
|
|
660
660
|
let favoritePrefix = ''
|
|
661
661
|
if (r.isRecommended) {
|
|
662
662
|
favoritePrefix = '🎯 '
|
|
663
663
|
} else if (r.isFavorite) {
|
|
664
664
|
favoritePrefix = '⭐ '
|
|
665
|
-
} else if (
|
|
665
|
+
} else if (modelIsNew) {
|
|
666
666
|
favoritePrefix = '🆕 '
|
|
667
667
|
}
|
|
668
668
|
const prefixDisplayWidth = displayWidth(favoritePrefix)
|