free-coding-models 0.3.55 → 0.3.57
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 +55 -56
- package/README.md +214 -160
- package/bin/free-coding-models.js +46 -0
- package/package.json +2 -2
- package/sources.js +134 -310
- package/src/analysis.js +23 -10
- package/src/app.js +66 -27
- package/src/cache.js +1 -1
- package/src/cli-help.js +9 -0
- package/src/command-palette.js +15 -13
- package/src/config.js +201 -35
- package/src/constants.js +4 -4
- package/src/endpoint-installer.js +45 -1
- package/src/favorites.js +22 -0
- package/src/graphify-out/cache/089db1c1def873cf6d112f1590da4490e61e691aff0db41e006aa2fb15ba0656.json +1 -0
- package/src/graphify-out/cache/0b510b53cf1a1393fb52b1fc3bbbf88b63938e961ec5b82119a2e9715fee8bd7.json +1 -0
- package/src/graphify-out/cache/0ec9a95a326bde58e0316889018b278062d06d494d0f31ba177c9de71e5fed2d.json +1 -0
- package/src/graphify-out/cache/1548663a24a68dce740ebab1bd1d3091048c9604e9d067a1650a42a6d82541d4.json +1 -0
- package/src/graphify-out/cache/1783af63cb6d0dfb4d469009f71ac83a74ba0b33d48186ff2c6e63f9429e900a.json +1 -0
- package/src/graphify-out/cache/1e109f5eb5dc4fd285871c3613e32b6b14a8c225f4080ee34b51c7e1a1764571.json +1 -0
- package/src/graphify-out/cache/1eb24dbeb69b46c8bc1caf925df2f2a964af0f33aea143adf8ddf88e017db6ca.json +1 -0
- package/src/graphify-out/cache/21e1bcfed11685e8347243f9d8516072dda183266a4bfe22c52fb31753a446c8.json +1 -0
- package/src/graphify-out/cache/2327473478b9c4b1940bf7ef66c9ee960b3cba8d5302e56b625df8274246e0b4.json +1 -0
- package/src/graphify-out/cache/25955b81fd25454c8fa90fb71a47db8d1215cf621beb8ff3cbd580aaf011b4f3.json +1 -0
- package/src/graphify-out/cache/2739677f19c702f88f3de0a0bac475066adbda98709907ad3de967aef689f86d.json +1 -0
- package/src/graphify-out/cache/2bba03422f6b3ee7f5b5d29cc90314a064d259e5822a176657bda3e04505cf00.json +1 -0
- package/src/graphify-out/cache/2ddf1d2c6d10147b0402446bc71a7988187b79b6210dd7e7250be8c555b9ff35.json +1 -0
- package/src/graphify-out/cache/2ee07457a5767c95a57f8e9eb95b28f800044f35666e0715e9d88ad1103a092e.json +1 -0
- package/src/graphify-out/cache/2fe9f75dc2951c417f2c8dd22749092cf550dc67599f1c8d1866900dc6e9154e.json +1 -0
- package/src/graphify-out/cache/41c4b7c27e7fc3e2948d3a4bf95a72de2ed9a6f0463994babdce8ed2cc84598c.json +1 -0
- package/src/graphify-out/cache/5028defd54b7fbd3c7e444973e493de036e097e9b1d2a7cae7f19b88d68aacde.json +1 -0
- package/src/graphify-out/cache/5b133aba3fb16410c5b1fdbd1730039fc7fa1ac93abd99d7be08f60da70fc8d4.json +1 -0
- package/src/graphify-out/cache/74252e5b0978d85ab3421a3de1a9384aa282ffd2be2cfe7db2530139089f4275.json +1 -0
- package/src/graphify-out/cache/7695ebeea056095edd14332963cc43354ef3a097caf46f1e28d0f01369642901.json +1 -0
- package/src/graphify-out/cache/777aa7085c395a935c6556bbde182cd871edb61f3a685ed8068ec0c8f6fb0075.json +1 -0
- package/src/graphify-out/cache/82a723881980e82273c113def8315533d7da28827e300413d9ad30f27b7407df.json +1 -0
- package/src/graphify-out/cache/86b87c9603e6cd188f42c7eed3b86c291d48a781c223a707e74f3e7ed0c02a21.json +1 -0
- package/src/graphify-out/cache/890fead9a78cadaed560a2d2453916121fa605c3e43a334910ac4bc951a9ef6d.json +1 -0
- package/src/graphify-out/cache/89d3ea66f52783caa775ef9a30923d7d6225e1d8ae9e962f4741b8c7785dab1e.json +1 -0
- package/src/graphify-out/cache/8cc82cd9edce41f0e1c092f14a94fd52bf847addf3237b616dc5a9e505bd05bd.json +1 -0
- package/src/graphify-out/cache/93ba2e25e3ff7ad525f397902345fbd375df7315de7b402e20cc803c14eccde8.json +1 -0
- package/src/graphify-out/cache/99beed29580b9c7bfecfee794cb3d8e535fcf0eb3b92113108f88bdd0a8e79b3.json +1 -0
- package/src/graphify-out/cache/aeeb931fa477c65ce2e51d8149957350fa54225c613222bbbe8448998d1afd3d.json +1 -0
- package/src/graphify-out/cache/baf91bef5b5ecb2a476433b6cc0c48c563c54ee2d07fc3c192e543685e3e7222.json +1 -0
- package/src/graphify-out/cache/bd98b94ac4e9b92b6336d47b26e0366b51a4eaf0711d722f05f98dfae23ab42b.json +1 -0
- package/src/graphify-out/cache/bfcb51e9328e9cbfbee4f6fee0f56635d7b03488addc9f6c4e4b190b70a73362.json +1 -0
- package/src/graphify-out/cache/c0d3dabeb093aa758c49eadf41b87ecc96a16c1449c2670aaf48cbfc891d8da6.json +1 -0
- package/src/graphify-out/cache/c20d6630236f473c1406068c3ae205853e649b216495c93dfec055dd222c55cf.json +1 -0
- package/src/graphify-out/cache/c22b9122816bebce0a2f79af41a986559d01e00163dbcd579c5755621b4cb483.json +1 -0
- package/src/graphify-out/cache/ca556ec14453ddb8f9e0c5a832dac90d77111b9bad5f8c2d80d272e2e7a06371.json +1 -0
- package/src/graphify-out/cache/d6dbc9135dfa35a756b3b09b06700e4bc229fdccba11bb963f2ba44028e0bbae.json +1 -0
- package/src/graphify-out/cache/e1cf71276f1779d0fa075f79bd7c8a9fd0b8eef6932ac043137451b7c7fa7cbe.json +1 -0
- package/src/graphify-out/cache/e4b3be14494467df2d2ed389bc4f18f099021cb5bc355b901fa88387b2d8b8a2.json +1 -0
- package/src/graphify-out/cache/eaea0dded097f6f9553b654220046c6ec0c9be592a5973d906564ee60af34e0d.json +1 -0
- package/src/graphify-out/cache/ef07d0cd2675d1f79d2a2fdbf3bc3319687638751e9ce89b0d0d97ed1cd9f7e1.json +1 -0
- package/src/graphify-out/cache/f81272d6eb8aaff9e96d5a1d9f06777db70ac3652a646b951ded51f79871d733.json +1 -0
- package/src/graphify-out/cache/f9619dd92186f75a6dbda937e0c606647153918524cdb5763f956e6ec2a9e386.json +1 -0
- package/src/graphify-out/cache/fd88b1b2ff4bfcae08559d9c2aaeeb9a3f1e2f5cd8928762c311196956c170a5.json +1 -0
- package/src/key-handler.js +322 -114
- package/src/kilo.js +20 -1
- package/src/opencode.js +23 -2
- package/src/overlays.js +199 -98
- package/src/provider-metadata.js +26 -17
- package/src/quota-capabilities.js +6 -10
- package/src/render-helpers.js +38 -8
- package/src/render-table.js +119 -248
- package/src/router-daemon.js +1986 -0
- package/src/router-dashboard.js +902 -0
- package/src/sync-set.js +479 -0
- package/src/theme.js +4 -0
- package/src/tool-launchers.js +1 -0
- package/src/tool-metadata.js +6 -2
- package/src/utils.js +30 -6
- package/web/dist/assets/{index-C03JjCgA.js → index-DKHCzbK1.js} +2 -2
- package/web/dist/index.html +1 -1
package/src/key-handler.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* tool launch actions. It also keeps the live key bindings aligned with the
|
|
9
9
|
* highlighted letters shown in the table headers.
|
|
10
10
|
*
|
|
11
|
-
* 📖 Key I opens the
|
|
11
|
+
* 📖 Key I opens the changelog overlay.
|
|
12
12
|
*
|
|
13
13
|
* It also owns the "test key" model selection used by the Settings overlay.
|
|
14
14
|
* Anonymous telemetry hooks for model launches and a few high-signal settings
|
|
@@ -33,7 +33,10 @@
|
|
|
33
33
|
|
|
34
34
|
import { loadChangelog } from './changelog-loader.js'
|
|
35
35
|
import { getToolMeta, isModelCompatibleWithTool, getCompatibleTools, findSimilarCompatibleModels } from './tool-metadata.js'
|
|
36
|
-
import { loadConfig, replaceConfigContents } from './config.js'
|
|
36
|
+
import { loadConfig, saveConfig, replaceConfigContents } from './config.js'
|
|
37
|
+
import { join, dirname } from 'node:path'
|
|
38
|
+
import { fileURLToPath } from 'node:url'
|
|
39
|
+
import { spawn } from 'node:child_process'
|
|
37
40
|
import { cleanupLegacyProxyArtifacts } from './legacy-proxy-cleanup.js'
|
|
38
41
|
import { getLastLayout, COLUMN_SORT_MAP } from './render-table.js'
|
|
39
42
|
import { cycleThemeSetting, detectActiveTheme } from './theme.js'
|
|
@@ -42,12 +45,23 @@ import { buildCommandPaletteTree, flattenCommandTree, filterCommandPaletteEntrie
|
|
|
42
45
|
import { WIDTH_WARNING_MIN_COLS, VERDICT_CYCLE, HEALTH_CYCLE } from './constants.js'
|
|
43
46
|
import { scanAllToolConfigs, softDeleteModel } from './installed-models-manager.js'
|
|
44
47
|
import { startExternalTool } from './tool-launchers.js'
|
|
48
|
+
import {
|
|
49
|
+
clearRouterDashboardRequestLog,
|
|
50
|
+
closeRouterDashboardOverlay,
|
|
51
|
+
cycleRouterDashboardActiveSet,
|
|
52
|
+
cycleRouterDashboardProbeMode,
|
|
53
|
+
openRouterDashboardOverlay,
|
|
54
|
+
restartRouterDashboardDaemon,
|
|
55
|
+
toggleRouterDashboardProbePause,
|
|
56
|
+
} from './router-dashboard.js'
|
|
45
57
|
|
|
46
58
|
// 📖 Some providers need an explicit probe model because the first catalog entry
|
|
47
59
|
// 📖 is not guaranteed to be accepted by their chat endpoint.
|
|
48
60
|
const PROVIDER_TEST_MODEL_OVERRIDES = {
|
|
49
|
-
sambanova: ['DeepSeek-V3-
|
|
50
|
-
nvidia: ['deepseek-ai/deepseek-
|
|
61
|
+
sambanova: ['MiniMax-M2.5', 'DeepSeek-V3.1', 'DeepSeek-V3.2'],
|
|
62
|
+
nvidia: ['deepseek-ai/deepseek-v4-flash', 'openai/gpt-oss-120b'],
|
|
63
|
+
'github-models': ['openai/gpt-4.1-mini'],
|
|
64
|
+
mistral: ['mistral-small-latest', 'devstral-small-latest'],
|
|
51
65
|
}
|
|
52
66
|
|
|
53
67
|
// 📖 Settings key tests retry retryable failures across several models so a
|
|
@@ -67,6 +81,7 @@ const PROVIDER_AUTH_ENDPOINTS = {
|
|
|
67
81
|
cerebras: { url: 'https://api.cerebras.ai/v1/models', method: 'GET' },
|
|
68
82
|
sambanova: { url: 'https://api.sambanova.ai/v1/models', method: 'GET' },
|
|
69
83
|
openrouter: { url: 'https://openrouter.ai/api/v1/models', method: 'GET' },
|
|
84
|
+
mistral: { url: 'https://api.mistral.ai/v1/models', method: 'GET' },
|
|
70
85
|
huggingface: { url: 'https://router.huggingface.co/v1/models', method: 'GET' },
|
|
71
86
|
deepinfra: { url: 'https://api.deepinfra.com/v1/models', method: 'GET' },
|
|
72
87
|
fireworks: { url: 'https://api.fireworks.ai/v1/models', method: 'GET' },
|
|
@@ -79,6 +94,7 @@ const PROVIDER_AUTH_ENDPOINTS = {
|
|
|
79
94
|
ovhcloud: { url: 'https://oai.endpoints.kepler.ai.cloud.ovh.net/v1/models', method: 'GET' },
|
|
80
95
|
qwen: { url: 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1/models', method: 'GET' },
|
|
81
96
|
iflow: { url: 'https://apis.iflow.cn/v1/models', method: 'GET' },
|
|
97
|
+
'github-models': null, // 📖 GitHub Models catalog is public; use chat ping to validate the token.
|
|
82
98
|
replicate: null, // 📖 Replicate has no /models endpoint; use chat completions ping
|
|
83
99
|
cloudflare: null, // 📖 Workers AI has no auth-check endpoint; use ping only
|
|
84
100
|
zai: null, // 📖 ZAI undocumented; use ping only
|
|
@@ -275,7 +291,6 @@ export function createKeyHandler(ctx) {
|
|
|
275
291
|
sendUsageTelemetry,
|
|
276
292
|
startRecommendAnalysis,
|
|
277
293
|
stopRecommendAnalysis,
|
|
278
|
-
sendBugReport,
|
|
279
294
|
stopUi,
|
|
280
295
|
ping,
|
|
281
296
|
getPingModel,
|
|
@@ -363,6 +378,40 @@ export function createKeyHandler(ctx) {
|
|
|
363
378
|
})
|
|
364
379
|
}
|
|
365
380
|
|
|
381
|
+
async function syncFavoritesToRouter(selected) {
|
|
382
|
+
if (state.config?.router?.enabled !== true) return
|
|
383
|
+
const favorites = state.config.favorites || []
|
|
384
|
+
const selKey = toFavoriteKey(selected.providerKey, selected.modelId)
|
|
385
|
+
const chain = [selKey, ...favorites.filter((f) => f !== selKey)]
|
|
386
|
+
const models = chain.map((f, i) => {
|
|
387
|
+
const slashIdx = f.indexOf('/')
|
|
388
|
+
const provider = slashIdx >= 0 ? f.slice(0, slashIdx) : '?'
|
|
389
|
+
const model = slashIdx >= 0 ? f.slice(slashIdx + 1) : f
|
|
390
|
+
return { provider, model, priority: i + 1 }
|
|
391
|
+
})
|
|
392
|
+
try {
|
|
393
|
+
const port = await readDaemonPort()
|
|
394
|
+
if (!port) return
|
|
395
|
+
const baseUrl = `http://127.0.0.1:${port}`
|
|
396
|
+
const setPayload = { name: 'fast-coding', models, created: new Date().toISOString() }
|
|
397
|
+
await globalThis.fetch(`${baseUrl}/sets/fast-coding`, {
|
|
398
|
+
method: 'PUT',
|
|
399
|
+
headers: { 'Content-Type': 'application/json' },
|
|
400
|
+
body: JSON.stringify(setPayload),
|
|
401
|
+
})
|
|
402
|
+
await globalThis.fetch(`${baseUrl}/sets/fast-coding/activate`, { method: 'POST' })
|
|
403
|
+
} catch {}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async function readDaemonPort() {
|
|
407
|
+
try {
|
|
408
|
+
const { readFileSync } = await import('node:fs')
|
|
409
|
+
const raw = readFileSync(`${process.env.HOME}/.free-coding-models-daemon.port`, 'utf8').trim()
|
|
410
|
+
if (/^\d+$/.test(raw)) return Number(raw)
|
|
411
|
+
} catch {}
|
|
412
|
+
return null
|
|
413
|
+
}
|
|
414
|
+
|
|
366
415
|
async function launchSelectedModel(selected, options = {}) {
|
|
367
416
|
const { uiAlreadyStopped = false } = options
|
|
368
417
|
userSelected = { modelId: selected.modelId, label: selected.label, tier: selected.tier, providerKey: selected.providerKey }
|
|
@@ -373,7 +422,9 @@ export function createKeyHandler(ctx) {
|
|
|
373
422
|
stopUi()
|
|
374
423
|
}
|
|
375
424
|
|
|
376
|
-
// 📖
|
|
425
|
+
// 📖 If router is enabled, push [selected, ...favorites] to daemon as the active set
|
|
426
|
+
await syncFavoritesToRouter(selected)
|
|
427
|
+
|
|
377
428
|
if (selected.status === 'timeout') {
|
|
378
429
|
console.log(chalk.yellow(` ⚠ Selected: ${selected.label} (currently timing out)`))
|
|
379
430
|
} else if (selected.status === 'down') {
|
|
@@ -898,12 +949,7 @@ export function createKeyHandler(ctx) {
|
|
|
898
949
|
state.installEndpointsResult = null
|
|
899
950
|
}
|
|
900
951
|
|
|
901
|
-
|
|
902
|
-
state.feedbackOpen = true
|
|
903
|
-
state.bugReportBuffer = ''
|
|
904
|
-
state.bugReportStatus = 'idle'
|
|
905
|
-
state.bugReportError = null
|
|
906
|
-
}
|
|
952
|
+
|
|
907
953
|
|
|
908
954
|
function openChangelogOverlay() {
|
|
909
955
|
state.changelogOpen = true
|
|
@@ -928,6 +974,51 @@ export function createKeyHandler(ctx) {
|
|
|
928
974
|
}
|
|
929
975
|
}
|
|
930
976
|
|
|
977
|
+
|
|
978
|
+
// 📖 Token Usage screen — Shift+T from main table. Fetches daily token history
|
|
979
|
+
// 📖 from the daemon and renders a 7-day chart plus today/all-time breakdowns.
|
|
980
|
+
async function openTokenUsageOverlay() {
|
|
981
|
+
state.tokenUsageOpen = true
|
|
982
|
+
state.tokenUsageScrollOffset = 0
|
|
983
|
+
state.tokenUsageError = null
|
|
984
|
+
state.tokenUsageData = null
|
|
985
|
+
// 📖 Discover daemon port
|
|
986
|
+
let port = 19280
|
|
987
|
+
try {
|
|
988
|
+
const { readFileSync: rfs } = await import('node:fs')
|
|
989
|
+
const portPath = `${process.env.HOME}/.free-coding-models-daemon.port`
|
|
990
|
+
const savedPort = rfs(portPath, 'utf8').trim()
|
|
991
|
+
if (/^\d+$/.test(savedPort)) port = Number(savedPort)
|
|
992
|
+
} catch {}
|
|
993
|
+
state.tokenUsageLastFetchAt = Date.now()
|
|
994
|
+
try {
|
|
995
|
+
const controller = new AbortController()
|
|
996
|
+
const timer = setTimeout(() => controller.abort(), 2000)
|
|
997
|
+
const res = await globalThis.fetch(`http://127.0.0.1:${port}/stats/tokens`, { signal: controller.signal })
|
|
998
|
+
clearTimeout(timer)
|
|
999
|
+
if (!res.ok) {
|
|
1000
|
+
state.tokenUsageError = `Daemon returned HTTP ${res.status} — is the router running?`
|
|
1001
|
+
return
|
|
1002
|
+
}
|
|
1003
|
+
// 📖 Guard: res.json() can throw on malformed response body
|
|
1004
|
+
const text = await res.text()
|
|
1005
|
+
try {
|
|
1006
|
+
state.tokenUsageData = JSON.parse(text)
|
|
1007
|
+
} catch {
|
|
1008
|
+
state.tokenUsageError = 'Daemon returned invalid JSON — try restarting the daemon'
|
|
1009
|
+
}
|
|
1010
|
+
} catch (err) {
|
|
1011
|
+
state.tokenUsageError = err?.name === 'AbortError' ? 'Request timed out — is the router daemon running?' : (err?.message || 'Failed to fetch token stats')
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
function closeTokenUsageOverlay() {
|
|
1016
|
+
state.tokenUsageOpen = false
|
|
1017
|
+
state.tokenUsageScrollOffset = 0
|
|
1018
|
+
state.tokenUsageError = null
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
|
|
931
1022
|
function cycleToolMode() {
|
|
932
1023
|
const modeOrder = getToolModeOrder()
|
|
933
1024
|
const currentIndex = modeOrder.indexOf(state.mode)
|
|
@@ -1016,8 +1107,9 @@ export function createKeyHandler(ctx) {
|
|
|
1016
1107
|
|| state.installEndpointsOpen
|
|
1017
1108
|
|| state.toolInstallPromptOpen
|
|
1018
1109
|
|| state.installedModelsOpen
|
|
1110
|
+
|| state.routerDashboardOpen
|
|
1019
1111
|
|| state.recommendOpen
|
|
1020
|
-
|
|
1112
|
+
|
|
1021
1113
|
|| state.helpVisible
|
|
1022
1114
|
|| state.changelogOpen
|
|
1023
1115
|
}
|
|
@@ -1205,8 +1297,10 @@ export function createKeyHandler(ctx) {
|
|
|
1205
1297
|
state.helpScrollOffset = 0
|
|
1206
1298
|
return
|
|
1207
1299
|
case 'open-changelog': return openChangelogOverlay()
|
|
1208
|
-
|
|
1300
|
+
|
|
1209
1301
|
case 'open-recommend': return openRecommendOverlay()
|
|
1302
|
+
case 'open-router-dashboard': return openRouterDashboardOverlay(state)
|
|
1303
|
+
case 'open-token-usage': return openTokenUsageOverlay()
|
|
1210
1304
|
case 'open-install-endpoints': return openInstallEndpointsOverlay()
|
|
1211
1305
|
case 'open-installed-models': return openInstalledModelsOverlay()
|
|
1212
1306
|
case 'action-cycle-theme': return cycleGlobalTheme()
|
|
@@ -1333,13 +1427,66 @@ export function createKeyHandler(ctx) {
|
|
|
1333
1427
|
return
|
|
1334
1428
|
}
|
|
1335
1429
|
|
|
1336
|
-
if (!state.
|
|
1430
|
+
if (!state.settingsEditMode && !state.settingsAddKeyMode && key.name === 'g' && !key.ctrl && !key.meta) {
|
|
1337
1431
|
cycleGlobalTheme()
|
|
1338
1432
|
return
|
|
1339
1433
|
}
|
|
1340
1434
|
|
|
1341
1435
|
// 📖 Profile system removed - API keys now persist permanently across all sessions
|
|
1342
1436
|
|
|
1437
|
+
// 📖 Router Dashboard captures local dashboard controls while open so keys
|
|
1438
|
+
// 📖 like S/I/R/C do not leak through to sort/filter actions in the table.
|
|
1439
|
+
if (state.routerDashboardOpen) {
|
|
1440
|
+
if (key.ctrl && key.name === 'c') { exit(0); return }
|
|
1441
|
+
const pageStep = Math.max(1, (state.terminalRows || 1) - 4)
|
|
1442
|
+
|
|
1443
|
+
if (key.name === 'escape') {
|
|
1444
|
+
closeRouterDashboardOverlay(state)
|
|
1445
|
+
return
|
|
1446
|
+
}
|
|
1447
|
+
if (key.name === 'up' || key.name === 'k') {
|
|
1448
|
+
state.routerDashboardScrollOffset = Math.max(0, (state.routerDashboardScrollOffset || 0) - 1)
|
|
1449
|
+
return
|
|
1450
|
+
}
|
|
1451
|
+
if (key.name === 'down' || key.name === 'j') {
|
|
1452
|
+
state.routerDashboardScrollOffset = (state.routerDashboardScrollOffset || 0) + 1
|
|
1453
|
+
return
|
|
1454
|
+
}
|
|
1455
|
+
if (key.name === 'pageup') {
|
|
1456
|
+
state.routerDashboardScrollOffset = Math.max(0, (state.routerDashboardScrollOffset || 0) - pageStep)
|
|
1457
|
+
return
|
|
1458
|
+
}
|
|
1459
|
+
if (key.name === 'pagedown') {
|
|
1460
|
+
state.routerDashboardScrollOffset = (state.routerDashboardScrollOffset || 0) + pageStep
|
|
1461
|
+
return
|
|
1462
|
+
}
|
|
1463
|
+
if (key.name === 'home') {
|
|
1464
|
+
state.routerDashboardScrollOffset = 0
|
|
1465
|
+
return
|
|
1466
|
+
}
|
|
1467
|
+
if (key.name === 's') {
|
|
1468
|
+
try { await cycleRouterDashboardActiveSet(state) } catch {}
|
|
1469
|
+
return
|
|
1470
|
+
}
|
|
1471
|
+
if (key.name === 'i') {
|
|
1472
|
+
try { await cycleRouterDashboardProbeMode(state) } catch {}
|
|
1473
|
+
return
|
|
1474
|
+
}
|
|
1475
|
+
if (key.name === 'r') {
|
|
1476
|
+
restartRouterDashboardDaemon(state)
|
|
1477
|
+
return
|
|
1478
|
+
}
|
|
1479
|
+
if (key.name === 'c') {
|
|
1480
|
+
clearRouterDashboardRequestLog(state)
|
|
1481
|
+
return
|
|
1482
|
+
}
|
|
1483
|
+
if (key.name === 'p') {
|
|
1484
|
+
toggleRouterDashboardProbePause(state)
|
|
1485
|
+
return
|
|
1486
|
+
}
|
|
1487
|
+
return
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1343
1490
|
// 📖 Install Endpoints overlay: provider → tool → connection → scope → optional model subset.
|
|
1344
1491
|
if (state.installEndpointsOpen) {
|
|
1345
1492
|
if (key.ctrl && key.name === 'c') { exit(0); return }
|
|
@@ -1736,87 +1883,132 @@ export function createKeyHandler(ctx) {
|
|
|
1736
1883
|
return
|
|
1737
1884
|
}
|
|
1738
1885
|
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1886
|
+
|
|
1887
|
+
|
|
1888
|
+
// 📖 Help overlay: full keyboard navigation + key swallowing while overlay is open.
|
|
1889
|
+
if (state.helpVisible) {
|
|
1890
|
+
const pageStep = Math.max(1, (state.terminalRows || 1) - 2)
|
|
1891
|
+
if (key.name === 'escape' || (key.ctrl && key.name === 'h')) {
|
|
1892
|
+
state.helpVisible = false
|
|
1893
|
+
return
|
|
1894
|
+
}
|
|
1895
|
+
if (key.name === 'up' || key.name === 'k') { state.helpScrollOffset = Math.max(0, state.helpScrollOffset - 1); return }
|
|
1896
|
+
if (key.name === 'down' || key.name === 'j') { state.helpScrollOffset += 1; return }
|
|
1897
|
+
if (key.name === 'pageup') { state.helpScrollOffset = Math.max(0, state.helpScrollOffset - pageStep); return }
|
|
1898
|
+
if (key.name === 'pagedown') { state.helpScrollOffset += pageStep; return }
|
|
1899
|
+
if (key.name === 'home') { state.helpScrollOffset = 0; return }
|
|
1900
|
+
if (key.name === 'end') { state.helpScrollOffset = Number.MAX_SAFE_INTEGER; return }
|
|
1742
1901
|
if (key.ctrl && key.name === 'c') { exit(0); return }
|
|
1902
|
+
return
|
|
1903
|
+
}
|
|
1743
1904
|
|
|
1905
|
+
// 📖 Token Usage overlay: Shift+T shows token history chart and today/all-time breakdowns.
|
|
1906
|
+
if (state.tokenUsageOpen) {
|
|
1907
|
+
if (key.ctrl && key.name === 'c') { exit(0); return }
|
|
1908
|
+
const pageStep = Math.max(1, (state.terminalRows || 1) - 4)
|
|
1744
1909
|
if (key.name === 'escape') {
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
state.
|
|
1910
|
+
closeTokenUsageOverlay()
|
|
1911
|
+
return
|
|
1912
|
+
}
|
|
1913
|
+
if (key.name === 'up' || key.name === 'k') {
|
|
1914
|
+
state.tokenUsageScrollOffset = Math.max(0, state.tokenUsageScrollOffset - 1)
|
|
1915
|
+
return
|
|
1916
|
+
}
|
|
1917
|
+
if (key.name === 'down' || key.name === 'j') {
|
|
1918
|
+
state.tokenUsageScrollOffset += 1
|
|
1919
|
+
return
|
|
1920
|
+
}
|
|
1921
|
+
if (key.name === 'pageup') {
|
|
1922
|
+
state.tokenUsageScrollOffset = Math.max(0, state.tokenUsageScrollOffset - pageStep)
|
|
1923
|
+
return
|
|
1924
|
+
}
|
|
1925
|
+
if (key.name === 'pagedown') {
|
|
1926
|
+
state.tokenUsageScrollOffset += pageStep
|
|
1750
1927
|
return
|
|
1751
1928
|
}
|
|
1929
|
+
if (key.name === 'home') {
|
|
1930
|
+
state.tokenUsageScrollOffset = 0
|
|
1931
|
+
return
|
|
1932
|
+
}
|
|
1933
|
+
if (key.name === 'end') {
|
|
1934
|
+
state.tokenUsageScrollOffset = Number.MAX_SAFE_INTEGER
|
|
1935
|
+
return
|
|
1936
|
+
}
|
|
1937
|
+
return
|
|
1938
|
+
}
|
|
1752
1939
|
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
state.
|
|
1762
|
-
setTimeout(() => {
|
|
1763
|
-
state.feedbackOpen = false
|
|
1764
|
-
state.bugReportBuffer = ''
|
|
1765
|
-
state.bugReportStatus = 'idle'
|
|
1766
|
-
state.bugReportError = null
|
|
1767
|
-
}, 3000)
|
|
1768
|
-
} else {
|
|
1769
|
-
// 📖 Error — show error message, keep overlay open
|
|
1770
|
-
state.bugReportStatus = 'error'
|
|
1771
|
-
state.bugReportError = result.error || 'Unknown error'
|
|
1940
|
+
// 📖 Router Onboarding overlay: shown on first launch. Y=yes enable, N=not now, Esc=cancel.
|
|
1941
|
+
if (state.routerOnboardingOpen) {
|
|
1942
|
+
if (key.ctrl && key.name === 'c') { exit(0); return }
|
|
1943
|
+
if (state.routerOnboardingPhase === 'loading' || state.routerOnboardingPhase === 'success' || state.routerOnboardingPhase === 'error') {
|
|
1944
|
+
if (key.name === 'escape' || key.name === 'return') {
|
|
1945
|
+
state.routerOnboardingOpen = false
|
|
1946
|
+
// 📖 Mark onboarding as seen (don't show again)
|
|
1947
|
+
if (state.config?.router) {
|
|
1948
|
+
state.config.router.onboardingSeen = true
|
|
1772
1949
|
}
|
|
1950
|
+
return
|
|
1773
1951
|
}
|
|
1774
1952
|
return
|
|
1775
1953
|
}
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
// 📖
|
|
1779
|
-
if (state.
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
if (state.bugReportStatus === 'error') {
|
|
1783
|
-
state.bugReportStatus = 'idle'
|
|
1784
|
-
state.bugReportError = null
|
|
1954
|
+
if (key.name === 'escape' || key.name === 'n') {
|
|
1955
|
+
state.routerOnboardingOpen = false
|
|
1956
|
+
// 📖 Mark as seen and disabled
|
|
1957
|
+
if (state.config?.router) {
|
|
1958
|
+
state.config.router.onboardingSeen = true
|
|
1959
|
+
state.config.router.enabled = false
|
|
1785
1960
|
}
|
|
1786
1961
|
return
|
|
1787
1962
|
}
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1963
|
+
if (key.name === 'up' || key.name === 'k') {
|
|
1964
|
+
state.routerOnboardingCursor = 0
|
|
1965
|
+
return
|
|
1966
|
+
}
|
|
1967
|
+
if (key.name === 'down' || key.name === 'j') {
|
|
1968
|
+
state.routerOnboardingCursor = 1
|
|
1969
|
+
return
|
|
1970
|
+
}
|
|
1971
|
+
if (key.name === 'return' || key.name === 'y') {
|
|
1972
|
+
const shouldEnable = key.name === 'return' ? true : (state.routerOnboardingCursor === 0)
|
|
1973
|
+
if (!shouldEnable) {
|
|
1974
|
+
state.routerOnboardingOpen = false
|
|
1975
|
+
if (state.config?.router) {
|
|
1976
|
+
state.config.router.onboardingSeen = true
|
|
1977
|
+
state.config.router.enabled = false
|
|
1800
1978
|
}
|
|
1979
|
+
return
|
|
1801
1980
|
}
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1981
|
+
// 📖 Enable router: start daemon in background and mark onboarding seen
|
|
1982
|
+
state.routerOnboardingPhase = 'loading'
|
|
1983
|
+
state.routerOnboardingError = null
|
|
1984
|
+
void (async () => {
|
|
1985
|
+
try {
|
|
1986
|
+
const binPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'bin', 'free-coding-models.js')
|
|
1987
|
+
const child = spawn('node', [binPath, '--daemon-bg'], {
|
|
1988
|
+
detached: true,
|
|
1989
|
+
stdio: 'ignore',
|
|
1990
|
+
})
|
|
1991
|
+
child.unref()
|
|
1992
|
+
await new Promise((r) => setTimeout(r, 2000))
|
|
1993
|
+
if (state.routerOnboardingPhase === 'loading') {
|
|
1994
|
+
state.routerOnboardingPhase = 'success'
|
|
1995
|
+
if (state.config?.router) {
|
|
1996
|
+
state.config.router.enabled = true
|
|
1997
|
+
state.config.router.onboardingSeen = true
|
|
1998
|
+
saveConfig(state.config)
|
|
1999
|
+
}
|
|
2000
|
+
trackTelemetryEvent('app_router_install', { router_version: '0.4.0' })
|
|
2001
|
+
await new Promise((r) => setTimeout(r, 1500))
|
|
2002
|
+
state.routerOnboardingOpen = false
|
|
2003
|
+
openRouterDashboardOverlay(state)
|
|
2004
|
+
}
|
|
2005
|
+
} catch (err) {
|
|
2006
|
+
state.routerOnboardingPhase = 'error'
|
|
2007
|
+
state.routerOnboardingError = err?.message || 'Failed to start router'
|
|
2008
|
+
}
|
|
2009
|
+
})()
|
|
1811
2010
|
return
|
|
1812
2011
|
}
|
|
1813
|
-
if (key.name === 'up' || key.name === 'k') { state.helpScrollOffset = Math.max(0, state.helpScrollOffset - 1); return }
|
|
1814
|
-
if (key.name === 'down' || key.name === 'j') { state.helpScrollOffset += 1; return }
|
|
1815
|
-
if (key.name === 'pageup') { state.helpScrollOffset = Math.max(0, state.helpScrollOffset - pageStep); return }
|
|
1816
|
-
if (key.name === 'pagedown') { state.helpScrollOffset += pageStep; return }
|
|
1817
|
-
if (key.name === 'home') { state.helpScrollOffset = 0; return }
|
|
1818
|
-
if (key.name === 'end') { state.helpScrollOffset = Number.MAX_SAFE_INTEGER; return }
|
|
1819
|
-
if (key.ctrl && key.name === 'c') { exit(0); return }
|
|
1820
2012
|
return
|
|
1821
2013
|
}
|
|
1822
2014
|
|
|
@@ -2366,9 +2558,16 @@ export function createKeyHandler(ctx) {
|
|
|
2366
2558
|
|
|
2367
2559
|
// 📖 Profile system removed - API keys now persist permanently across all sessions
|
|
2368
2560
|
|
|
2369
|
-
// 📖 Shift+R
|
|
2370
|
-
|
|
2371
|
-
|
|
2561
|
+
// 📖 Shift+R intentionally stays unadvertised in the main UI, but remains
|
|
2562
|
+
// 📖 available as a tester entry point for the Router Dashboard.
|
|
2563
|
+
if (key.name === 'r' && key.shift && !key.ctrl && !key.meta) {
|
|
2564
|
+
openRouterDashboardOverlay(state)
|
|
2565
|
+
return
|
|
2566
|
+
}
|
|
2567
|
+
|
|
2568
|
+
// 📖 Shift+T: open the Token Usage screen.
|
|
2569
|
+
if (key.name === 't' && key.shift && !key.ctrl && !key.meta) {
|
|
2570
|
+
openTokenUsageOverlay()
|
|
2372
2571
|
return
|
|
2373
2572
|
}
|
|
2374
2573
|
|
|
@@ -2383,7 +2582,7 @@ export function createKeyHandler(ctx) {
|
|
|
2383
2582
|
// 📖 T is reserved for tier filter cycling. Y toggles favorites display mode.
|
|
2384
2583
|
// 📖 X clears the active custom text filter.
|
|
2385
2584
|
// 📖 D is now reserved for provider filter cycling
|
|
2386
|
-
// 📖 Shift+R is reserved for reset view
|
|
2585
|
+
// 📖 Shift+R is reserved for the Router Dashboard; reset view remains in Ctrl+P.
|
|
2387
2586
|
const sortKeys = {
|
|
2388
2587
|
'r': 'rank', 'o': 'origin', 'm': 'model',
|
|
2389
2588
|
'l': 'ping', 'a': 'avg', 's': 'swe', 'c': 'ctx', 'h': 'condition', 'v': 'verdict', 'b': 'stability', 'u': 'uptime'
|
|
@@ -2401,11 +2600,7 @@ export function createKeyHandler(ctx) {
|
|
|
2401
2600
|
return
|
|
2402
2601
|
}
|
|
2403
2602
|
|
|
2404
|
-
|
|
2405
|
-
if (key.name === 'i') {
|
|
2406
|
-
openFeedbackOverlay()
|
|
2407
|
-
return
|
|
2408
|
-
}
|
|
2603
|
+
|
|
2409
2604
|
|
|
2410
2605
|
// 📖 W cycles the supported ping modes:
|
|
2411
2606
|
// 📖 speed (2s) → normal (10s) → slow (30s) → forced (4s) → speed.
|
|
@@ -2417,16 +2612,6 @@ export function createKeyHandler(ctx) {
|
|
|
2417
2612
|
return
|
|
2418
2613
|
}
|
|
2419
2614
|
|
|
2420
|
-
// 📖 Ctrl+O: toggle footer visibility (collapse to single hint when hidden)
|
|
2421
|
-
if (key.ctrl && key.name === 'o' && !key.meta) {
|
|
2422
|
-
state.footerHidden = !state.footerHidden
|
|
2423
|
-
if (!state.config.settings || typeof state.config.settings !== 'object') state.config.settings = {}
|
|
2424
|
-
state.config.settings.footerHidden = state.footerHidden
|
|
2425
|
-
saveConfig(state.config)
|
|
2426
|
-
state.frame++ // 📖 Force immediate re-render
|
|
2427
|
-
return
|
|
2428
|
-
}
|
|
2429
|
-
|
|
2430
2615
|
// 📖 E toggles hiding models whose provider has no configured API key.
|
|
2431
2616
|
// 📖 The preference is saved globally.
|
|
2432
2617
|
if (key.name === 'e') {
|
|
@@ -2477,8 +2662,8 @@ export function createKeyHandler(ctx) {
|
|
|
2477
2662
|
return
|
|
2478
2663
|
}
|
|
2479
2664
|
|
|
2480
|
-
// 📖 Help overlay key:
|
|
2481
|
-
if (key.
|
|
2665
|
+
// 📖 Help overlay key: I = toggle help overlay
|
|
2666
|
+
if (key.name === 'i') {
|
|
2482
2667
|
state.helpVisible = !state.helpVisible
|
|
2483
2668
|
if (state.helpVisible) state.helpScrollOffset = 0
|
|
2484
2669
|
return
|
|
@@ -2502,6 +2687,28 @@ export function createKeyHandler(ctx) {
|
|
|
2502
2687
|
return
|
|
2503
2688
|
}
|
|
2504
2689
|
|
|
2690
|
+
if (key.shift && key.name === 'up') {
|
|
2691
|
+
const selected = state.visibleSorted?.[state.cursor]
|
|
2692
|
+
if (selected?.isFavorite) {
|
|
2693
|
+
reorderFavorite(state.config, selected.providerKey, selected.modelId, 'up')
|
|
2694
|
+
syncFavoriteFlags(state.results, state.config)
|
|
2695
|
+
applyTierFilter()
|
|
2696
|
+
refreshVisibleSorted({ resetCursor: false })
|
|
2697
|
+
}
|
|
2698
|
+
return
|
|
2699
|
+
}
|
|
2700
|
+
|
|
2701
|
+
if (key.shift && key.name === 'down') {
|
|
2702
|
+
const selected = state.visibleSorted?.[state.cursor]
|
|
2703
|
+
if (selected?.isFavorite) {
|
|
2704
|
+
reorderFavorite(state.config, selected.providerKey, selected.modelId, 'down')
|
|
2705
|
+
syncFavoriteFlags(state.results, state.config)
|
|
2706
|
+
applyTierFilter()
|
|
2707
|
+
refreshVisibleSorted({ resetCursor: false })
|
|
2708
|
+
}
|
|
2709
|
+
return
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2505
2712
|
if (key.name === 'up' || key.name === 'k') {
|
|
2506
2713
|
// 📖 Main list wrap navigation: top -> bottom on Up / K (vim-style).
|
|
2507
2714
|
const count = state.visibleSorted.length
|
|
@@ -2527,12 +2734,9 @@ export function createKeyHandler(ctx) {
|
|
|
2527
2734
|
}
|
|
2528
2735
|
|
|
2529
2736
|
if (key.name === 'return') { // Enter
|
|
2530
|
-
// 📖 Use the cached visible+sorted array — guaranteed to match what's on screen
|
|
2531
2737
|
const selected = state.visibleSorted[state.cursor]
|
|
2532
|
-
if (!selected) return
|
|
2738
|
+
if (!selected) return
|
|
2533
2739
|
|
|
2534
|
-
// 📖 Incompatibility intercept — if the model can't run on the active tool,
|
|
2535
|
-
// 📖 show the fallback overlay instead of launching. Lets user switch tool or pick similar model.
|
|
2536
2740
|
if (!isModelCompatibleWithTool(selected.providerKey, state.mode)) {
|
|
2537
2741
|
const compatTools = getCompatibleTools(selected.providerKey)
|
|
2538
2742
|
const similarModels = findSimilarCompatibleModels(
|
|
@@ -2714,10 +2918,7 @@ export function createMouseEventHandler(ctx) {
|
|
|
2714
2918
|
}
|
|
2715
2919
|
return
|
|
2716
2920
|
}
|
|
2717
|
-
|
|
2718
|
-
// 📖 Feedback overlay doesn't scroll — ignore
|
|
2719
|
-
return
|
|
2720
|
-
}
|
|
2921
|
+
|
|
2721
2922
|
if (state.commandPaletteOpen) {
|
|
2722
2923
|
// 📖 Command palette: scroll the results list
|
|
2723
2924
|
const count = state.commandPaletteResults?.length || 0
|
|
@@ -2763,6 +2964,11 @@ export function createMouseEventHandler(ctx) {
|
|
|
2763
2964
|
}
|
|
2764
2965
|
return
|
|
2765
2966
|
}
|
|
2967
|
+
if (state.routerDashboardOpen) {
|
|
2968
|
+
const step = evt.type === 'scroll-up' ? -3 : 3
|
|
2969
|
+
state.routerDashboardScrollOffset = Math.max(0, (state.routerDashboardScrollOffset || 0) + step)
|
|
2970
|
+
return
|
|
2971
|
+
}
|
|
2766
2972
|
|
|
2767
2973
|
// 📖 Main table scroll: move cursor up/down with wrap-around
|
|
2768
2974
|
const count = state.visibleSorted.length
|
|
@@ -2843,18 +3049,18 @@ export function createMouseEventHandler(ctx) {
|
|
|
2843
3049
|
return
|
|
2844
3050
|
}
|
|
2845
3051
|
|
|
3052
|
+
if (state.routerDashboardOpen) {
|
|
3053
|
+
closeRouterDashboardOverlay(state)
|
|
3054
|
+
return
|
|
3055
|
+
}
|
|
3056
|
+
|
|
2846
3057
|
if (state.incompatibleFallbackOpen) {
|
|
2847
3058
|
// 📖 Incompatible fallback: click closes
|
|
2848
3059
|
state.incompatibleFallbackOpen = false
|
|
2849
3060
|
return
|
|
2850
3061
|
}
|
|
2851
3062
|
|
|
2852
|
-
|
|
2853
|
-
// 📖 Feedback overlay: click anywhere closes (no scroll, no cursor)
|
|
2854
|
-
state.feedbackOpen = false
|
|
2855
|
-
state.feedbackInput = ''
|
|
2856
|
-
return
|
|
2857
|
-
}
|
|
3063
|
+
|
|
2858
3064
|
|
|
2859
3065
|
if (state.helpVisible) {
|
|
2860
3066
|
// 📖 Help overlay: click anywhere closes (same as K or Escape)
|
|
@@ -3004,6 +3210,8 @@ export function createMouseEventHandler(ctx) {
|
|
|
3004
3210
|
// 📖 Most are single-character keys; special cases like ctrl+p need special handling.
|
|
3005
3211
|
if (zone.key === 'ctrl+p') {
|
|
3006
3212
|
process.stdin.emit('keypress', '\x10', { name: 'p', ctrl: true, meta: false, shift: false })
|
|
3213
|
+
} else if (zone.key === 'shift+r') {
|
|
3214
|
+
process.stdin.emit('keypress', 'R', { name: 'r', ctrl: false, meta: false, shift: true })
|
|
3007
3215
|
} else {
|
|
3008
3216
|
process.stdin.emit('keypress', zone.key, { name: zone.key, ctrl: false, meta: false, shift: false })
|
|
3009
3217
|
}
|