free-coding-models 0.5.86 → 0.5.87
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 +110 -881
- package/bin/free-coding-models.js +4 -1
- package/changelog/v0.5.87.md +48 -0
- package/package.json +8 -5
- package/sources.js +4 -4
- package/src/core/cache.js +3 -1
- package/src/core/config.js +43 -5
- package/src/core/endpoint-installer.js +23 -12
- package/src/core/installed-models-manager.js +43 -20
- package/src/core/kilo-config.js +51 -5
- package/src/core/kilo.js +5 -3
- package/src/core/legacy-proxy-cleanup.js +22 -5
- package/src/core/model-merger.js +4 -0
- package/src/core/models-drift.js +9 -0
- package/src/core/opencode-config.js +57 -5
- package/src/core/opencode.js +67 -41
- package/src/core/playground.js +7 -0
- package/src/core/probe-cache.js +35 -4
- package/src/core/provider-key-tester.js +10 -2
- package/src/core/provider-metadata.js +10 -0
- package/src/core/provider-quota-fetchers.js +14 -7
- package/src/core/router-daemon.js +208 -26
- package/src/core/runtime-telemetry.js +27 -1
- package/src/core/shared-helpers.js +12 -1
- package/src/core/shell-env.js +3 -1
- package/src/core/telemetry.js +18 -5
- package/src/core/tool-launchers.js +9 -6
- package/src/core/updater.js +28 -2
- package/src/core/utils.js +76 -71
- package/src/tui/app.js +38 -14
- package/src/tui/command-palette.js +22 -2
- package/src/tui/key-handler.js +123 -179
- package/src/tui/overlays.js +21 -71
- package/src/tui/render-helpers.js +62 -4
- package/src/tui/render-table.js +34 -3
- package/src/tui/theme.js +36 -5
- package/src/tui/tui-filters.js +28 -8
- package/src/tui/tui-state.js +22 -6
- package/web/dist/assets/index-BpTpPS0g.js +44 -0
- package/web/dist/assets/index-CAzFIt8P.css +1 -0
- package/web/dist/index.html +5 -9
- package/web/index.html +3 -7
- package/web/server.js +97 -23
- package/web/vite.config.js +8 -0
- package/web/app.legacy.js +0 -900
- package/web/dist/assets/index-C5-Ywvv3.css +0 -1
- package/web/dist/assets/index-Dm-Xfby9.js +0 -40
- package/web/public/favicon.ico +0 -0
- package/web/public/favicons/apple-touch-icon.png +0 -0
- package/web/public/favicons/browserconfig.xml +0 -12
- 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/public/favicons/site.webmanifest +0 -25
- package/web/src/App.jsx +0 -521
- package/web/src/components/analytics/AnalyticsView.jsx +0 -147
- package/web/src/components/analytics/AnalyticsView.module.css +0 -190
- package/web/src/components/analytics/TokenUsagePanel.jsx +0 -105
- package/web/src/components/analytics/TokenUsagePanel.module.css +0 -126
- package/web/src/components/atoms/AILatencyCell.jsx +0 -38
- package/web/src/components/atoms/AILatencyCell.module.css +0 -51
- package/web/src/components/atoms/HealthCell.jsx +0 -143
- package/web/src/components/atoms/HealthCell.module.css +0 -60
- package/web/src/components/atoms/LastPingCell.jsx +0 -35
- package/web/src/components/atoms/LastPingCell.module.css +0 -45
- package/web/src/components/atoms/MoodCell.jsx +0 -25
- package/web/src/components/atoms/MoodCell.module.css +0 -6
- package/web/src/components/atoms/NoKeyIcon.jsx +0 -65
- package/web/src/components/atoms/NoKeyIcon.module.css +0 -30
- package/web/src/components/atoms/ProviderLogo.jsx +0 -187
- package/web/src/components/atoms/ProviderLogo.module.css +0 -103
- package/web/src/components/atoms/RankCell.jsx +0 -9
- package/web/src/components/atoms/RankCell.module.css +0 -9
- package/web/src/components/atoms/Sparkline.jsx +0 -63
- package/web/src/components/atoms/StabilityCell.jsx +0 -18
- package/web/src/components/atoms/StabilityCell.module.css +0 -13
- package/web/src/components/atoms/StatusDot.jsx +0 -13
- package/web/src/components/atoms/StatusDot.module.css +0 -29
- package/web/src/components/atoms/TPSCell.jsx +0 -36
- package/web/src/components/atoms/TPSCell.module.css +0 -44
- package/web/src/components/atoms/TierBadge.jsx +0 -10
- package/web/src/components/atoms/TierBadge.module.css +0 -29
- package/web/src/components/atoms/Toast.jsx +0 -25
- package/web/src/components/atoms/Toast.module.css +0 -41
- package/web/src/components/atoms/ToastContainer.jsx +0 -16
- package/web/src/components/atoms/ToastContainer.module.css +0 -10
- package/web/src/components/atoms/VerdictBadge.jsx +0 -36
- package/web/src/components/atoms/VerdictBadge.module.css +0 -40
- package/web/src/components/changelog/ChangelogView.jsx +0 -135
- package/web/src/components/changelog/ChangelogView.module.css +0 -159
- package/web/src/components/dashboard/DetailPanel.jsx +0 -217
- package/web/src/components/dashboard/DetailPanel.module.css +0 -260
- package/web/src/components/dashboard/ExpandedDetailRow.jsx +0 -361
- package/web/src/components/dashboard/ExpandedDetailRow.module.css +0 -356
- package/web/src/components/dashboard/ExportModal.jsx +0 -84
- package/web/src/components/dashboard/ExportModal.module.css +0 -99
- package/web/src/components/dashboard/FilterBar.jsx +0 -338
- package/web/src/components/dashboard/FilterBar.module.css +0 -342
- package/web/src/components/dashboard/ModelTable.jsx +0 -672
- package/web/src/components/dashboard/ModelTable.module.css +0 -446
- package/web/src/components/dashboard/ProviderDropdown.jsx +0 -156
- package/web/src/components/dashboard/ProviderDropdown.module.css +0 -248
- package/web/src/components/help/HelpView.jsx +0 -201
- package/web/src/components/help/HelpView.module.css +0 -156
- package/web/src/components/install/InstallEndpointsView.jsx +0 -309
- package/web/src/components/install/InstallEndpointsView.module.css +0 -427
- package/web/src/components/installed/InstalledModelsView.jsx +0 -89
- package/web/src/components/installed/InstalledModelsView.module.css +0 -200
- package/web/src/components/launch/IncompatibleFallbackModal.jsx +0 -69
- package/web/src/components/launch/LaunchButton.jsx +0 -30
- package/web/src/components/launch/LaunchButton.module.css +0 -35
- package/web/src/components/launch/LaunchModal.module.css +0 -125
- package/web/src/components/layout/Footer.jsx +0 -19
- package/web/src/components/layout/Footer.module.css +0 -10
- package/web/src/components/layout/Header.jsx +0 -265
- package/web/src/components/layout/Header.module.css +0 -357
- package/web/src/components/palette/CommandPalette.jsx +0 -301
- package/web/src/components/palette/CommandPalette.module.css +0 -121
- package/web/src/components/playground/PlaygroundChat.jsx +0 -527
- package/web/src/components/playground/PlaygroundChat.module.css +0 -382
- package/web/src/components/playground/PlaygroundView.jsx +0 -371
- package/web/src/components/playground/PlaygroundView.module.css +0 -504
- package/web/src/components/recommend/RecommendView.jsx +0 -121
- package/web/src/components/recommend/RecommendView.module.css +0 -57
- package/web/src/components/router/RouterView.jsx +0 -1212
- package/web/src/components/router/RouterView.module.css +0 -1289
- package/web/src/components/settings/SettingsView.jsx +0 -555
- package/web/src/components/settings/SettingsView.module.css +0 -550
- package/web/src/components/tools/ToolPicker.jsx +0 -86
- package/web/src/components/tools/ToolPicker.module.css +0 -97
- package/web/src/components/update/UpdateChip.jsx +0 -104
- package/web/src/components/update/UpdateChip.module.css +0 -146
- package/web/src/global.css +0 -298
- package/web/src/hooks/urlState.constants.js +0 -28
- package/web/src/hooks/useChangelog.js +0 -51
- package/web/src/hooks/useColumnSizing.js +0 -211
- package/web/src/hooks/useFavorites.js +0 -150
- package/web/src/hooks/useFilter.js +0 -310
- package/web/src/hooks/useInstalledModels.js +0 -42
- package/web/src/hooks/useRecommend.js +0 -67
- package/web/src/hooks/useRouterDashboard.js +0 -133
- package/web/src/hooks/useSSE.js +0 -73
- package/web/src/hooks/useSocket.js +0 -203
- package/web/src/hooks/useTheme.js +0 -83
- package/web/src/hooks/useTokenUsage.js +0 -103
- package/web/src/hooks/useToolMode.js +0 -77
- package/web/src/hooks/useUpdateChecker.js +0 -91
- package/web/src/hooks/useUrlState.js +0 -170
- package/web/src/main.jsx +0 -15
- package/web/src/utils/download.js +0 -15
- package/web/src/utils/format.js +0 -42
- package/web/src/utils/m3.js +0 -57
- package/web/src/utils/ranks.js +0 -37
- package/web/styles.legacy.css +0 -963
|
@@ -48,7 +48,7 @@ import { ENV_VAR_NAMES, isWindows } from './provider-metadata.js'
|
|
|
48
48
|
import { getToolMeta, TOOL_METADATA } from './tool-metadata.js'
|
|
49
49
|
import { PROVIDER_METADATA } from './provider-metadata.js'
|
|
50
50
|
import { resolveToolBinaryPath } from './tool-bootstrap.js'
|
|
51
|
-
import { ensureDir, readJson, writeJson } from './shared-helpers.js'
|
|
51
|
+
import { ensureDir, readJson, writeJson, shellSingleQuote } from './shared-helpers.js'
|
|
52
52
|
import { parseContextWindow } from './endpoint-installer.js'
|
|
53
53
|
import { resolveCloudflareUrl } from './ping.js'
|
|
54
54
|
|
|
@@ -70,9 +70,11 @@ function parseCtxToTokens(ctx) {
|
|
|
70
70
|
if (!match) return null
|
|
71
71
|
const num = parseFloat(match[1])
|
|
72
72
|
const suffix = match[2].toLowerCase()
|
|
73
|
-
// 📖
|
|
74
|
-
|
|
75
|
-
|
|
73
|
+
// 📖 Catalog ctx strings use DECIMAL semantics ("1M" = 1,000,000), matching
|
|
74
|
+
// 📖 parseContextWindow in endpoint-installer.js. The old 1024 multipliers made
|
|
75
|
+
// 📖 the two parsers disagree (1048576 vs 1000000) for the same catalog value.
|
|
76
|
+
if (suffix === 'k') return Math.round(num * 1000)
|
|
77
|
+
if (suffix === 'm') return Math.round(num * 1000 * 1000)
|
|
76
78
|
return Math.round(num)
|
|
77
79
|
}
|
|
78
80
|
|
|
@@ -414,10 +416,11 @@ function writeOpenHandsEnv(model, apiKey, baseUrl, paths = getDefaultToolPaths()
|
|
|
414
416
|
const backupPath = backupIfExists(filePath)
|
|
415
417
|
const lines = [
|
|
416
418
|
'# 📖 Managed by free-coding-models',
|
|
417
|
-
`
|
|
419
|
+
// 📖 Single-quote the key so quotes/$()/backticks in it stay literal on `source`.
|
|
420
|
+
`export OPENAI_API_KEY=${shellSingleQuote(apiKey)}`,
|
|
418
421
|
`export OPENAI_BASE_URL="${baseUrl}"`,
|
|
419
422
|
`export OPENAI_MODEL="${model.modelId}"`,
|
|
420
|
-
`export LLM_API_KEY
|
|
423
|
+
`export LLM_API_KEY=${shellSingleQuote(apiKey)}`,
|
|
421
424
|
`export LLM_BASE_URL="${baseUrl}"`,
|
|
422
425
|
`export LLM_MODEL="openai/${model.modelId}"`,
|
|
423
426
|
]
|
package/src/core/updater.js
CHANGED
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
* → resolveCurrentNpmInstallTarget() — Detect the npm prefix that owns the active package
|
|
32
32
|
* → getInstallArgs(pm, version) — Build correct { bin, args } per package manager
|
|
33
33
|
* → getManualInstallCmd(pm, version) — Human-readable install command string for error messages
|
|
34
|
+
* → isNewerVersion(remote, local) - Semver compare: true only when remote is greater
|
|
34
35
|
* → checkForUpdateDetailed() — Fetch npm latest with explicit error info
|
|
35
36
|
* → checkForUpdate() — Startup wrapper, returns version string or null
|
|
36
37
|
* → isPackageDevMode() — Detect git/dev checkouts that must not self-update
|
|
@@ -38,7 +39,7 @@
|
|
|
38
39
|
* → runUpdate(latestVersion) — Install new version via detected PM + relaunch
|
|
39
40
|
* @exports
|
|
40
41
|
* detectPackageManager, resolveCurrentNpmInstallTarget, getInstallArgs, getManualInstallCmd,
|
|
41
|
-
* checkForUpdateDetailed, checkForUpdate, isPackageDevMode,
|
|
42
|
+
* isNewerVersion, checkForUpdateDetailed, checkForUpdate, isPackageDevMode,
|
|
42
43
|
* enforceMandatoryStartupUpdate, runUpdate, fetchLastReleaseDate
|
|
43
44
|
*
|
|
44
45
|
* @see bin/free-coding-models.js — calls checkForUpdate() at startup and runUpdate() on confirm
|
|
@@ -232,6 +233,31 @@ export function getManualInstallCmd(pm, version, options = {}) {
|
|
|
232
233
|
return [bin, ...args].map(shellQuoteArg).join(' ')
|
|
233
234
|
}
|
|
234
235
|
|
|
236
|
+
/**
|
|
237
|
+
* 📖 isNewerVersion: dependency-free semver comparison of dotted numeric versions.
|
|
238
|
+
* 📖 Update availability used to be a string inequality, so a dist-tag rollback
|
|
239
|
+
* 📖 (remote < local) triggered a pointless "downgrade" install. Only a strictly
|
|
240
|
+
* 📖 GREATER remote version counts as an update now.
|
|
241
|
+
* @param {string} remoteVersion
|
|
242
|
+
* @param {string} localVersion
|
|
243
|
+
* @returns {boolean} true only when remoteVersion > localVersion
|
|
244
|
+
*/
|
|
245
|
+
export function isNewerVersion(remoteVersion, localVersion) {
|
|
246
|
+
const parseParts = (v) => String(v ?? '')
|
|
247
|
+
.trim()
|
|
248
|
+
.replace(/^v/i, '')
|
|
249
|
+
.split('.')
|
|
250
|
+
.map(part => parseInt(part, 10) || 0)
|
|
251
|
+
const remote = parseParts(remoteVersion)
|
|
252
|
+
const local = parseParts(localVersion)
|
|
253
|
+
const len = Math.max(remote.length, local.length)
|
|
254
|
+
for (let i = 0; i < len; i++) {
|
|
255
|
+
const diff = (remote[i] || 0) - (local[i] || 0)
|
|
256
|
+
if (diff !== 0) return diff > 0
|
|
257
|
+
}
|
|
258
|
+
return false
|
|
259
|
+
}
|
|
260
|
+
|
|
235
261
|
/**
|
|
236
262
|
* 📖 checkForUpdateDetailed: Fetch npm latest version with explicit error details.
|
|
237
263
|
* 📖 Used by settings manual-check flow to display meaningful status in the UI.
|
|
@@ -242,7 +268,7 @@ export async function checkForUpdateDetailed() {
|
|
|
242
268
|
const res = await fetch('https://registry.npmjs.org/free-coding-models/latest', { signal: AbortSignal.timeout(5000) })
|
|
243
269
|
if (!res.ok) return { latestVersion: null, error: `HTTP ${res.status}` }
|
|
244
270
|
const data = await res.json()
|
|
245
|
-
if (data.version && data.version
|
|
271
|
+
if (data.version && isNewerVersion(data.version, LOCAL_VERSION)) return { latestVersion: data.version, error: null }
|
|
246
272
|
return { latestVersion: null, error: null }
|
|
247
273
|
} catch (error) {
|
|
248
274
|
const message = error instanceof Error ? error.message : 'Unknown error'
|
package/src/core/utils.js
CHANGED
|
@@ -140,8 +140,13 @@ export const NEW_MODELS = new Set([
|
|
|
140
140
|
]);
|
|
141
141
|
|
|
142
142
|
export const getVerdict = (r) => {
|
|
143
|
+
// 📖 401/403 rows can still carry fast measurable pings, but calling them
|
|
144
|
+
// 📖 "Perfect" let --fiable / findBestModel recommend a model that cannot serve
|
|
145
|
+
// 📖 a single authenticated request. Auth-failing rows always get the
|
|
146
|
+
// 📖 least-trustworthy health label instead.
|
|
147
|
+
if (r.status === 'auth_error' || r.status === 'noauth') return 'Not Active'
|
|
143
148
|
const avg = getAvg(r)
|
|
144
|
-
const wasUpBefore = r.pings.length > 0 && r.pings.some(p => p.code === '200')
|
|
149
|
+
const wasUpBefore = (r.pings || []).length > 0 && r.pings.some(p => p.code === '200')
|
|
145
150
|
|
|
146
151
|
if (r.httpCode === '429') return 'Overloaded'
|
|
147
152
|
if ((r.status === 'timeout' || r.status === 'down') && wasUpBefore) return 'Unstable'
|
|
@@ -200,9 +205,10 @@ export const getVerdict = (r) => {
|
|
|
200
205
|
// 📖 Displayed as "Up%" column in the TUI — e.g., "85%" means 85% of pings got HTTP 200.
|
|
201
206
|
// 📖 This metric is useful for identifying models that are technically "up" but flaky.
|
|
202
207
|
export const getUptime = (r) => {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
208
|
+
const pings = r.pings || []
|
|
209
|
+
if (pings.length === 0) return 0
|
|
210
|
+
const successful = pings.filter(p => p.code === '200').length
|
|
211
|
+
return Math.round((successful / pings.length) * 100)
|
|
206
212
|
}
|
|
207
213
|
|
|
208
214
|
// 📖 getP95: Calculate the 95th percentile latency from measurable pings (HTTP 200/401).
|
|
@@ -303,11 +309,13 @@ export const sortResults = (results, sortColumn, sortDirection, { benchmarkResul
|
|
|
303
309
|
cmp = TIER_ORDER.indexOf(a.tier) - TIER_ORDER.indexOf(b.tier)
|
|
304
310
|
break
|
|
305
311
|
case 'origin':
|
|
306
|
-
// 📖 Sort by providerKey (or fallback to modelId prefix) for multi-provider support
|
|
307
|
-
|
|
312
|
+
// 📖 Sort by providerKey (or fallback to modelId prefix) for multi-provider support.
|
|
313
|
+
// 📖 Explicit 'en' locale: the default is ICU-dependent, so row order would
|
|
314
|
+
// 📖 otherwise vary between machines.
|
|
315
|
+
cmp = (a.providerKey ?? 'nvidia').localeCompare(b.providerKey ?? 'nvidia', 'en')
|
|
308
316
|
break
|
|
309
317
|
case 'model':
|
|
310
|
-
cmp = a.label.localeCompare(b.label)
|
|
318
|
+
cmp = a.label.localeCompare(b.label, 'en')
|
|
311
319
|
break
|
|
312
320
|
case 'ping': {
|
|
313
321
|
// 📖 Sort by LAST ping only — gives a real-time "right now" snapshot
|
|
@@ -333,7 +341,7 @@ export const sortResults = (results, sortColumn, sortDirection, { benchmarkResul
|
|
|
333
341
|
break
|
|
334
342
|
}
|
|
335
343
|
case 'condition':
|
|
336
|
-
cmp = a.status.localeCompare(b.status)
|
|
344
|
+
cmp = a.status.localeCompare(b.status, 'en')
|
|
337
345
|
break
|
|
338
346
|
case 'verdict': {
|
|
339
347
|
// 📖 Sort by verdict order — "Perfect" first, "Pending" last
|
|
@@ -391,6 +399,10 @@ export const sortResults = (results, sortColumn, sortDirection, { benchmarkResul
|
|
|
391
399
|
break
|
|
392
400
|
}
|
|
393
401
|
|
|
402
|
+
// 📖 Non-finite differences (e.g. Infinity - Infinity) collapse to 0: a NaN
|
|
403
|
+
// 📖 comparator result makes sort order machine-dependent and unstable.
|
|
404
|
+
if (!Number.isFinite(cmp)) cmp = 0
|
|
405
|
+
|
|
394
406
|
// 📖 Flip comparison for descending order
|
|
395
407
|
return sortDirection === 'asc' ? cmp : -cmp
|
|
396
408
|
})
|
|
@@ -485,7 +497,7 @@ export function findBestModel(results) {
|
|
|
485
497
|
// --daemon-status, --no-telemetry, --json, --help/-h (case-insensitive)
|
|
486
498
|
// --playground / playground subcommand (open the in-TUI chat playground)
|
|
487
499
|
// --fix-permissions / --yes / -y (auto-fix config permissions without prompting)
|
|
488
|
-
// - Value flag: --tier <letter> (
|
|
500
|
+
// - Value flag: --tier <letter> (also accepts the --tier=S equals form)
|
|
489
501
|
// - Probe-cache flags (t1):
|
|
490
502
|
// --reprobe / --no-cache (boolean) — force-rebuild the probe cache this run
|
|
491
503
|
// --probe-ttl <ms> (value) — override the 24h default TTL
|
|
@@ -500,62 +512,56 @@ export function findBestModel(results) {
|
|
|
500
512
|
// reprobeMode, probeTtlMs, showBrokenMode }
|
|
501
513
|
//
|
|
502
514
|
// 📖 Note: apiKey may be null here — the main CLI falls back to env vars and saved config.
|
|
515
|
+
|
|
516
|
+
// 📖 findValueFlag: collect every occurrence of a value flag, in both "--flag value"
|
|
517
|
+
// 📖 and "--flag=value" forms. Equals form used to be silently ignored (the lookup
|
|
518
|
+
// 📖 only matched the bare flag), and repeated flags used to leak their extra values
|
|
519
|
+
// 📖 into the positional API-key slot because only the first index was marked used.
|
|
520
|
+
// 📖 Returns { indices, values }: indices point at the VALUE args (for the equals
|
|
521
|
+
// 📖 form, at the "--flag=value" arg itself, which is already treated as a flag).
|
|
522
|
+
function findValueFlag(args, flag) {
|
|
523
|
+
const indices = []
|
|
524
|
+
const values = []
|
|
525
|
+
for (const [i, arg] of args.entries()) {
|
|
526
|
+
const lower = arg.toLowerCase()
|
|
527
|
+
if (lower.startsWith(`${flag}=`)) {
|
|
528
|
+
indices.push(i)
|
|
529
|
+
values.push(arg.slice(flag.length + 1))
|
|
530
|
+
} else if (lower === flag && args[i + 1] && !args[i + 1].startsWith('--')) {
|
|
531
|
+
indices.push(i + 1)
|
|
532
|
+
values.push(args[i + 1])
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return { indices, values }
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// 📖 First usable value of a flag, or null when the flag was not passed (or was
|
|
539
|
+
// 📖 passed with an empty value like "--tier=").
|
|
540
|
+
function firstFlagValue(found) {
|
|
541
|
+
return found.values.length > 0 && found.values[0] !== '' ? found.values[0] : null
|
|
542
|
+
}
|
|
543
|
+
|
|
503
544
|
export function parseArgs(argv) {
|
|
504
545
|
const args = argv.slice(2)
|
|
505
546
|
let apiKey = null
|
|
506
547
|
const flags = []
|
|
507
548
|
|
|
508
|
-
// 📖
|
|
509
|
-
|
|
510
|
-
const
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
const
|
|
516
|
-
const
|
|
517
|
-
|
|
518
|
-
: -1
|
|
519
|
-
|
|
520
|
-
const originIdx = args.findIndex(a => a.toLowerCase() === '--origin')
|
|
521
|
-
const originValueIdx = (originIdx !== -1 && args[originIdx + 1] && !args[originIdx + 1].startsWith('--'))
|
|
522
|
-
? originIdx + 1
|
|
523
|
-
: -1
|
|
524
|
-
|
|
525
|
-
const pingIntervalIdx = args.findIndex(a => a.toLowerCase() === '--ping-interval')
|
|
526
|
-
const pingIntervalValueIdx = (pingIntervalIdx !== -1 && args[pingIntervalIdx + 1] && !args[pingIntervalIdx + 1].startsWith('--'))
|
|
527
|
-
? pingIntervalIdx + 1
|
|
528
|
-
: -1
|
|
529
|
-
|
|
530
|
-
// 📖 --probe-ttl <ms> — override the 24h probe-cache TTL (power users / debugging)
|
|
531
|
-
const probeTtlIdx = args.findIndex(a => a.toLowerCase() === '--probe-ttl')
|
|
532
|
-
const probeTtlValueIdx = (probeTtlIdx !== -1 && args[probeTtlIdx + 1] && !args[probeTtlIdx + 1].startsWith('--'))
|
|
533
|
-
? probeTtlIdx + 1
|
|
534
|
-
: -1
|
|
535
|
-
|
|
536
|
-
// 📖 --sync-set [name] — auto-discover and live-probe models into a named router set
|
|
537
|
-
const syncSetIdx = args.findIndex(a => a.toLowerCase() === '--sync-set')
|
|
538
|
-
const syncSetValueIdx = (syncSetIdx !== -1 && args[syncSetIdx + 1] && !args[syncSetIdx + 1].startsWith('--'))
|
|
539
|
-
? syncSetIdx + 1
|
|
540
|
-
: -1
|
|
541
|
-
|
|
542
|
-
// 📖 --config-dir <dir> — override where FCM's config files (config.json + backups/)
|
|
543
|
-
// 📖 live. Any directory works, e.g. --config-dir ~/.config/free-coding-models for the
|
|
544
|
-
// 📖 XDG layout. The bin entry point persists this as FCM_CONFIG_DIR before imports.
|
|
545
|
-
const configDirIdx = args.findIndex(a => a.toLowerCase() === '--config-dir')
|
|
546
|
-
const configDirValueIdx = (configDirIdx !== -1 && args[configDirIdx + 1] && !args[configDirIdx + 1].startsWith('--'))
|
|
547
|
-
? configDirIdx + 1
|
|
548
|
-
: -1
|
|
549
|
+
// 📖 Resolve all value flags up front so every occurrence (space or equals form)
|
|
550
|
+
// 📖 can be marked consumed before the positional API-key scan runs.
|
|
551
|
+
const tierFlag = findValueFlag(args, '--tier')
|
|
552
|
+
const sortFlag = findValueFlag(args, '--sort')
|
|
553
|
+
const originFlag = findValueFlag(args, '--origin')
|
|
554
|
+
const pingIntervalFlag = findValueFlag(args, '--ping-interval')
|
|
555
|
+
const probeTtlFlag = findValueFlag(args, '--probe-ttl')
|
|
556
|
+
const syncSetFlag = findValueFlag(args, '--sync-set')
|
|
557
|
+
const configDirFlag = findValueFlag(args, '--config-dir')
|
|
558
|
+
const driftThresholdFlag = findValueFlag(args, '--drift-threshold')
|
|
549
559
|
|
|
550
560
|
// 📖 Set of arg indices that are values for flags (not API keys)
|
|
551
561
|
const skipIndices = new Set()
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
if (pingIntervalValueIdx !== -1) skipIndices.add(pingIntervalValueIdx)
|
|
556
|
-
if (syncSetValueIdx !== -1) skipIndices.add(syncSetValueIdx)
|
|
557
|
-
if (probeTtlValueIdx !== -1) skipIndices.add(probeTtlValueIdx)
|
|
558
|
-
if (configDirValueIdx !== -1) skipIndices.add(configDirValueIdx)
|
|
562
|
+
for (const found of [tierFlag, sortFlag, originFlag, pingIntervalFlag, probeTtlFlag, syncSetFlag, configDirFlag, driftThresholdFlag]) {
|
|
563
|
+
for (const idx of found.indices) skipIndices.add(idx)
|
|
564
|
+
}
|
|
559
565
|
|
|
560
566
|
for (const [i, arg] of args.entries()) {
|
|
561
567
|
// 📖 -y is a boolean flag (security auto-fix), never an API key
|
|
@@ -610,7 +616,7 @@ export function parseArgs(argv) {
|
|
|
610
616
|
|
|
611
617
|
// 📖 --sync-set [name] — auto-discover and populate a router set with best available models
|
|
612
618
|
const syncSetMode = flags.includes('--sync-set')
|
|
613
|
-
const syncSetName =
|
|
619
|
+
const syncSetName = firstFlagValue(syncSetFlag)
|
|
614
620
|
|
|
615
621
|
// 📖 --web / --gui / web subcommand — launch the web dashboard instead of the TUI
|
|
616
622
|
const webMode = flags.includes('--web') || flags.includes('--gui') || args[0] === 'web'
|
|
@@ -626,10 +632,11 @@ export function parseArgs(argv) {
|
|
|
626
632
|
const hideUnconfigured = flags.includes('--hide-unconfigured')
|
|
627
633
|
const showUnconfigured = flags.includes('--show-unconfigured')
|
|
628
634
|
|
|
629
|
-
let tierFilter =
|
|
630
|
-
let sortColumn =
|
|
631
|
-
let originFilter =
|
|
632
|
-
let
|
|
635
|
+
let tierFilter = firstFlagValue(tierFlag)?.toUpperCase() ?? null
|
|
636
|
+
let sortColumn = firstFlagValue(sortFlag)?.toLowerCase() ?? null
|
|
637
|
+
let originFilter = firstFlagValue(originFlag)
|
|
638
|
+
let pingIntervalRaw = firstFlagValue(pingIntervalFlag)
|
|
639
|
+
let pingInterval = pingIntervalRaw !== null ? parseInt(pingIntervalRaw, 10) : null
|
|
633
640
|
let sortDirection = sortDesc ? 'desc' : (sortAscFlag ? 'asc' : null)
|
|
634
641
|
|
|
635
642
|
// 📖 Profile system removed - API keys now persist permanently across all sessions
|
|
@@ -645,19 +652,14 @@ export function parseArgs(argv) {
|
|
|
645
652
|
// 📖 --probe-ttl overrides the 24h default; --show-broken un-hides broken models for this run.
|
|
646
653
|
const reprobeMode = flags.includes('--reprobe') || flags.includes('--no-cache')
|
|
647
654
|
const showBrokenMode = flags.includes('--show-broken')
|
|
648
|
-
const probeTtlRaw =
|
|
655
|
+
const probeTtlRaw = firstFlagValue(probeTtlFlag)
|
|
649
656
|
const probeTtlMs = probeTtlRaw !== null ? parseInt(probeTtlRaw, 10) : null
|
|
650
657
|
|
|
651
658
|
// 📖 Drift detection flags (t5):
|
|
652
659
|
// 📖 --check-drift (boolean) — print a drift report vs models.dev, exit non-zero on mismatch.
|
|
653
660
|
// 📖 --drift-threshold <N> (value) — only fail when N+ mismatches are found.
|
|
654
661
|
const checkDriftMode = flags.includes('--check-drift')
|
|
655
|
-
const
|
|
656
|
-
const driftThresholdValueIdx = (driftThresholdIdx !== -1 && args[driftThresholdIdx + 1] && !args[driftThresholdIdx + 1].startsWith('--'))
|
|
657
|
-
? driftThresholdIdx + 1
|
|
658
|
-
: -1
|
|
659
|
-
if (driftThresholdValueIdx !== -1) skipIndices.add(driftThresholdValueIdx)
|
|
660
|
-
const driftThresholdRaw = driftThresholdValueIdx !== -1 ? args[driftThresholdValueIdx] : null
|
|
662
|
+
const driftThresholdRaw = firstFlagValue(driftThresholdFlag)
|
|
661
663
|
const driftThreshold = driftThresholdRaw !== null ? parseInt(driftThresholdRaw, 10) : null
|
|
662
664
|
|
|
663
665
|
return {
|
|
@@ -716,7 +718,7 @@ export function parseArgs(argv) {
|
|
|
716
718
|
// 📖 Runtime telemetry flag (t3) — see src/core/runtime-telemetry.js
|
|
717
719
|
clearRuntimeMode,
|
|
718
720
|
// 📖 Config location flag — see src/core/config.js getConfigDir()
|
|
719
|
-
configDir:
|
|
721
|
+
configDir: firstFlagValue(configDirFlag),
|
|
720
722
|
// 📖 Security auto-fix flag - see src/core/security.js checkConfigSecurity()
|
|
721
723
|
fixPermissionsMode,
|
|
722
724
|
}
|
|
@@ -1031,6 +1033,9 @@ export function getVersionStatusInfo(updateState, latestVersion, startupLatestVe
|
|
|
1031
1033
|
* @returns {string} JSON string of formatted results
|
|
1032
1034
|
*/
|
|
1033
1035
|
export function formatResultsAsJSON(results, sortBy = 'avg', limit = 0) {
|
|
1036
|
+
// 📖 Only slice for a usable positive limit: a negative limit used to drop the
|
|
1037
|
+
// 📖 last row(s) via slice(0, -1), and 0/undefined means "all results".
|
|
1038
|
+
const effectiveLimit = Number.isInteger(limit) && limit > 0 ? limit : undefined
|
|
1034
1039
|
const formatted = results
|
|
1035
1040
|
.map((r, idx) => ({
|
|
1036
1041
|
rank: r.idx || idx + 1,
|
|
@@ -1050,7 +1055,7 @@ export function formatResultsAsJSON(results, sortBy = 'avg', limit = 0) {
|
|
|
1050
1055
|
status: r.status || null,
|
|
1051
1056
|
httpCode: r.httpCode || null
|
|
1052
1057
|
}))
|
|
1053
|
-
.slice(0,
|
|
1058
|
+
.slice(0, effectiveLimit)
|
|
1054
1059
|
|
|
1055
1060
|
return JSON.stringify(formatted, null, 2)
|
|
1056
1061
|
}
|
package/src/tui/app.js
CHANGED
|
@@ -93,7 +93,7 @@ import chalk from 'chalk'
|
|
|
93
93
|
import { createRequire } from 'module'
|
|
94
94
|
import { randomUUID } from 'crypto'
|
|
95
95
|
import { MODELS, sources } from '../../sources.js'
|
|
96
|
-
import { getAvg, getVerdict, getUptime, getP95, getJitter, getStabilityScore, sortResults, filterByTier,
|
|
96
|
+
import { getAvg, getVerdict, getUptime, getP95, getJitter, getStabilityScore, sortResults, filterByTier, parseArgs, TIER_ORDER, VERDICT_ORDER, TIER_LETTER_MAP, scoreModelForTask, getTopRecommendations, TASK_TYPES, PRIORITY_TYPES, CONTEXT_BUDGETS, formatResultsAsJSON } from '../core/utils.js'
|
|
97
97
|
import { loadConfig, saveConfig, getApiKey, resolveApiKeys, addApiKey, removeApiKey, isProviderEnabled, persistApiKeysForProvider } from '../core/config.js'
|
|
98
98
|
import { buildMergedModels } from '../core/model-merger.js'
|
|
99
99
|
import { loadOpenCodeConfig, saveOpenCodeConfig } from '../core/opencode-config.js'
|
|
@@ -101,7 +101,7 @@ import { usageForRow as _usageForRow } from '../core/usage-reader.js'
|
|
|
101
101
|
import { buildProviderModelTokenKey, loadTokenUsageByProviderModel } from '../core/token-usage-reader.js'
|
|
102
102
|
import { parseOpenRouterResponse, fetchProviderQuota as _fetchProviderQuotaFromModule, getAllQuotas as _getAllPassiveQuotasFromModule } from '../core/provider-quota-fetchers.js'
|
|
103
103
|
import { isKnownQuotaTelemetry } from '../core/quota-capabilities.js'
|
|
104
|
-
import { ALT_ENTER, ALT_LEAVE, ALT_HOME, PING_TIMEOUT, PING_INTERVAL, FPS,
|
|
104
|
+
import { ALT_ENTER, ALT_LEAVE, ALT_HOME, PING_TIMEOUT, PING_INTERVAL, FPS, 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 } from '../core/constants.js'
|
|
105
105
|
import { TIER_COLOR } from './tier-colors.js'
|
|
106
106
|
import { resolveCloudflareUrl, buildPingRequest, ping, extractQuotaPercent, getProviderQuotaPercentCached, usagePlaceholderForProvider } from '../core/ping.js'
|
|
107
107
|
import { runFiableMode, filterByTierOrExit, fetchOpenRouterFreeModels } from '../core/analysis.js'
|
|
@@ -109,12 +109,12 @@ import { PROVIDER_METADATA, ENV_VAR_NAMES, isWindows, isMac } from '../core/prov
|
|
|
109
109
|
import { parseTelemetryEnv, isTelemetryDebugEnabled, telemetryDebug, ensureTelemetryConfig, getTelemetryDistinctId, getTelemetrySystem, getTelemetryTerminal, isTelemetryEnabled, sendUsageTelemetry } from '../core/telemetry.js'
|
|
110
110
|
import { ensureFavoritesConfig, toFavoriteKey, syncFavoriteFlags, toggleFavoriteModel, reorderFavorite, pruneOrphanedFavorites } from '../core/favorites.js'
|
|
111
111
|
import { checkForUpdateDetailed, checkForUpdate, runUpdate, fetchLastReleaseDate } from '../core/updater.js'
|
|
112
|
-
import { createTuiState, PING_MODE_INTERVALS, PING_MODE_CYCLE, SPEED_MODE_DURATION_MS, IDLE_SLOW_AFTER_MS, intervalToPingMode } from './tui-state.js'
|
|
112
|
+
import { createTuiState, PING_MODE_INTERVALS, PING_MODE_CYCLE, SPEED_MODE_DURATION_MS, IDLE_SLOW_AFTER_MS, PING_HISTORY_CAP, intervalToPingMode } from './tui-state.js'
|
|
113
113
|
import { createPingLoop } from '../core/ping-loop.js'
|
|
114
114
|
import { createTuiFilters } from './tui-filters.js'
|
|
115
115
|
import { promptApiKey } from '../core/setup.js'
|
|
116
116
|
import { syncShellEnv, ensureShellRcSource, removeShellEnv } from '../core/shell-env.js'
|
|
117
|
-
import {
|
|
117
|
+
import { displayWidth, padEndDisplay, tintOverlayLines, keepOverlayTargetVisible, sliceOverlayLines, sortResultsWithPinnedFavorites, adjustScrollOffset, runWithConcurrency } from './render-helpers.js'
|
|
118
118
|
import { renderTable, PROVIDER_COLOR } from './render-table.js'
|
|
119
119
|
import { setOpenCodeModelData, startOpenCode, startOpenCodeDesktop, startOpenCodeWeb } from '../core/opencode.js'
|
|
120
120
|
import { startKilo } from '../core/kilo.js'
|
|
@@ -478,6 +478,12 @@ export async function runApp(cliArgs, config, startupOptions = {}) {
|
|
|
478
478
|
let ticker = null
|
|
479
479
|
let onKeyPress = null
|
|
480
480
|
let onMouseData = null // 📖 Mouse data listener — set after createMouseEventHandler
|
|
481
|
+
// 📖 The variables above hold raw handler functions, but what actually gets
|
|
482
|
+
// 📖 registered on process.stdin are the WRAPPERS below. stopUi used to remove
|
|
483
|
+
// 📖 the raw functions (a silent no-op) - it now removes these tracked
|
|
484
|
+
// 📖 wrappers so launch/update paths really detach the listeners.
|
|
485
|
+
let keypressWrapper = null
|
|
486
|
+
let mouseDataWrapper = null
|
|
481
487
|
let pingModel = null
|
|
482
488
|
|
|
483
489
|
// 📖 scheduleNextPing: wrapper that defers to the factory version, passing the current runPingCycle.
|
|
@@ -505,7 +511,9 @@ export async function runApp(cliArgs, config, startupOptions = {}) {
|
|
|
505
511
|
r.verdict = cachedModel.verdict
|
|
506
512
|
r.status = cachedModel.status
|
|
507
513
|
r.httpCode = cachedModel.httpCode
|
|
508
|
-
|
|
514
|
+
// 📖 Trim restored history to PING_HISTORY_CAP so an oversized cache
|
|
515
|
+
// 📖 file cannot instantly reintroduce the unbounded-growth problem.
|
|
516
|
+
r.pings = (cachedModel.pings || []).slice(-PING_HISTORY_CAP)
|
|
509
517
|
}
|
|
510
518
|
}
|
|
511
519
|
}
|
|
@@ -621,6 +629,13 @@ export async function runApp(cliArgs, config, startupOptions = {}) {
|
|
|
621
629
|
}
|
|
622
630
|
|
|
623
631
|
r.pings.push({ ms, code })
|
|
632
|
+
// 📖 Cap ping history (PING_HISTORY_CAP = 300): r.pings used to grow
|
|
633
|
+
// 📖 unbounded and getAvg/getP95/getVerdict/getStabilityScore rescan it
|
|
634
|
+
// 📖 per row at ~12 FPS. Stats (uptime, avg) now roll over the last
|
|
635
|
+
// 📖 300 pings instead of the entire session; the oldest entries shift out.
|
|
636
|
+
if (r.pings.length > PING_HISTORY_CAP) {
|
|
637
|
+
r.pings.splice(0, r.pings.length - PING_HISTORY_CAP)
|
|
638
|
+
}
|
|
624
639
|
|
|
625
640
|
// 📖 Probe-cache (t1): record the result so future runs can skip healthy models
|
|
626
641
|
// 📖 and auto-hide broken ones. Uses the module-level in-memory cache; flushed
|
|
@@ -767,8 +782,9 @@ export async function runApp(cliArgs, config, startupOptions = {}) {
|
|
|
767
782
|
clearTimeout(state.pingIntervalObj)
|
|
768
783
|
clearInterval(state.versionRecheckTimer)
|
|
769
784
|
stopRouterDashboardClient(state)
|
|
770
|
-
|
|
771
|
-
if (
|
|
785
|
+
// 📖 Remove the actual registered wrappers (see keypressWrapper/mouseDataWrapper).
|
|
786
|
+
if (keypressWrapper) process.stdin.removeListener('keypress', keypressWrapper)
|
|
787
|
+
if (mouseDataWrapper) process.stdin.removeListener('data', mouseDataWrapper)
|
|
772
788
|
if (process.stdin.isTTY && resetRawMode) process.stdin.setRawMode(false)
|
|
773
789
|
process.stdin.pause()
|
|
774
790
|
process.stdout.write(ALT_LEAVE)
|
|
@@ -837,6 +853,7 @@ export async function runApp(cliArgs, config, startupOptions = {}) {
|
|
|
837
853
|
sortResultsWithPinnedFavorites,
|
|
838
854
|
adjustScrollOffset,
|
|
839
855
|
applyTierFilter,
|
|
856
|
+
getPingModel: () => pingModel,
|
|
840
857
|
PING_INTERVAL,
|
|
841
858
|
TIER_CYCLE,
|
|
842
859
|
ORIGIN_CYCLE,
|
|
@@ -948,7 +965,7 @@ export async function runApp(cliArgs, config, startupOptions = {}) {
|
|
|
948
965
|
}
|
|
949
966
|
})
|
|
950
967
|
|
|
951
|
-
|
|
968
|
+
keypressWrapper = async (str, key) => {
|
|
952
969
|
try {
|
|
953
970
|
// 📖 Skip keypress events that originate from mouse escape sequences.
|
|
954
971
|
// 📖 readline may partially parse SGR mouse sequences as garbage keypresses.
|
|
@@ -963,18 +980,20 @@ export async function runApp(cliArgs, config, startupOptions = {}) {
|
|
|
963
980
|
console.error(chalk.yellow('\nPlease file an issue at https://github.com/vava-nessa/free-coding-models/issues or join the Discord to report this to the author.'));
|
|
964
981
|
process.exit(1);
|
|
965
982
|
}
|
|
966
|
-
}
|
|
983
|
+
}
|
|
984
|
+
process.stdin.on('keypress', keypressWrapper)
|
|
967
985
|
|
|
968
986
|
// 📖 Mouse data listener: parses SGR mouse escape sequences from raw stdin
|
|
969
987
|
// 📖 and dispatches structured events (click, scroll, double-click) to the mouse handler.
|
|
970
|
-
|
|
988
|
+
mouseDataWrapper = (data) => {
|
|
971
989
|
try {
|
|
972
990
|
if (onMouseData) onMouseData(data)
|
|
973
991
|
} catch (err) {
|
|
974
992
|
// 📖 Mouse errors are non-fatal — log and continue so the TUI doesn't crash.
|
|
975
993
|
// 📖 This could happen on terminals that send unexpected mouse sequences.
|
|
976
994
|
}
|
|
977
|
-
}
|
|
995
|
+
}
|
|
996
|
+
process.stdin.on('data', mouseDataWrapper)
|
|
978
997
|
|
|
979
998
|
process.on('SIGCONT', noteUserActivity)
|
|
980
999
|
|
|
@@ -1103,8 +1122,6 @@ export async function runApp(cliArgs, config, startupOptions = {}) {
|
|
|
1103
1122
|
? overlays.renderPlayground()
|
|
1104
1123
|
: state.tokenUsageOpen
|
|
1105
1124
|
? overlays.renderTokenUsage()
|
|
1106
|
-
: state.routerOnboardingOpen
|
|
1107
|
-
? overlays.renderRouterOnboarding()
|
|
1108
1125
|
: state.incompatibleFallbackOpen
|
|
1109
1126
|
? overlays.renderIncompatibleFallback()
|
|
1110
1127
|
: state.commandPaletteOpen
|
|
@@ -1207,7 +1224,14 @@ export async function runApp(cliArgs, config, startupOptions = {}) {
|
|
|
1207
1224
|
const initialDueModels = state.results.filter(r => !isCacheFresh(r.providerKey, r.modelId, {
|
|
1208
1225
|
ttlMs: state.probeCacheTtlMs,
|
|
1209
1226
|
}))
|
|
1210
|
-
|
|
1227
|
+
// 📖 Startup burst runs through runWithConcurrency(…, 5) - the same bounded
|
|
1228
|
+
// 📖 helper the 404 probe uses. The old Promise.all fired up to ~226
|
|
1229
|
+
// 📖 simultaneous pings at once, a guaranteed 429 storm on rate-limited
|
|
1230
|
+
// 📖 providers (and per-task errors are captured instead of rejecting the batch).
|
|
1231
|
+
const initialPing = runWithConcurrency(
|
|
1232
|
+
initialDueModels.map(r => () => pingModel(r)),
|
|
1233
|
+
5
|
|
1234
|
+
)
|
|
1211
1235
|
|
|
1212
1236
|
// 📖 Continuous ping loop with mode-driven cadence.
|
|
1213
1237
|
const runPingCycle = async () => {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* → `fuzzyMatchCommand` — scores a query against one string and returns match positions
|
|
10
10
|
* → `filterCommandPaletteEntries` — returns sorted command matches for a query
|
|
11
11
|
*
|
|
12
|
-
* @exports { buildCommandPaletteTree, flattenCommandTree, fuzzyMatchCommand, filterCommandPaletteEntries }
|
|
12
|
+
* @exports { buildCommandPaletteTree, flattenCommandTree, fuzzyMatchCommand, filterCommandPaletteEntries, COMMAND_PALETTE_MAX_RESULTS, clampPaletteCursor }
|
|
13
13
|
*
|
|
14
14
|
* @see src/key-handler.js
|
|
15
15
|
* @see src/overlays.js
|
|
@@ -17,6 +17,25 @@
|
|
|
17
17
|
|
|
18
18
|
import { TOOL_METADATA, TOOL_MODE_ORDER } from '../core/tool-metadata.js'
|
|
19
19
|
import { sources } from '../../sources.js'
|
|
20
|
+
import { sanitizePingInterval } from './tui-state.js'
|
|
21
|
+
|
|
22
|
+
// 📖 Max result rows the palette renders (overlays.js slices to this). The
|
|
23
|
+
// 📖 cursor must never point past the rendered slice, so key-handler clamps
|
|
24
|
+
// 📖 with clampPaletteCursor() against the same constant.
|
|
25
|
+
export const COMMAND_PALETTE_MAX_RESULTS = 80
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 📖 clampPaletteCursor: Keep the palette cursor inside the RENDERED slice.
|
|
29
|
+
* 📖 The overlay only paints the first COMMAND_PALETTE_MAX_RESULTS rows, so a
|
|
30
|
+
* 📖 cursor clamped against the full result list could sit on an invisible row.
|
|
31
|
+
* @param {number} cursor
|
|
32
|
+
* @param {number} totalResults
|
|
33
|
+
* @returns {number}
|
|
34
|
+
*/
|
|
35
|
+
export function clampPaletteCursor(cursor, totalResults) {
|
|
36
|
+
const max = Math.max(0, Math.min(totalResults, COMMAND_PALETTE_MAX_RESULTS) - 1)
|
|
37
|
+
return Math.min(Math.max(0, cursor), max)
|
|
38
|
+
}
|
|
20
39
|
|
|
21
40
|
const PROVIDER_FILTER_COMMANDS = Object.entries(sources).map(([providerKey, source]) => {
|
|
22
41
|
const label = source?.name || providerKey
|
|
@@ -229,7 +248,8 @@ export function buildCommandPaletteTree(visibleModels = [], config = null) {
|
|
|
229
248
|
|
|
230
249
|
// 📖 Update Normal mode label if config is provided
|
|
231
250
|
if (config) {
|
|
232
|
-
|
|
251
|
+
// 📖 Guard against 0/negative/garbage pingInterval (same rule as tui-state).
|
|
252
|
+
const normalInterval = sanitizePingInterval(config.settings?.pingInterval, 10000)
|
|
233
253
|
const normalSec = Math.round(normalInterval / 1000)
|
|
234
254
|
const actionsNode = tree.find(n => n.id === 'actions')
|
|
235
255
|
const pingNode = actionsNode?.children.find(n => n.id === 'action-ping-mode')
|