free-coding-models 0.3.54 → 0.3.56
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 +47 -49
- package/README.md +236 -160
- package/bin/free-coding-models.js +46 -0
- package/package.json +2 -2
- package/sources.js +133 -309
- package/src/analysis.js +23 -10
- package/src/app.js +113 -7
- package/src/cache.js +1 -1
- package/src/cli-help.js +9 -0
- package/src/command-palette.js +16 -12
- package/src/config.js +199 -32
- 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 +316 -13
- package/src/kilo.js +20 -1
- package/src/opencode.js +24 -3
- package/src/overlays.js +206 -5
- package/src/provider-metadata.js +26 -17
- package/src/quota-capabilities.js +6 -10
- package/src/render-table.js +37 -4
- package/src/router-daemon.js +1986 -0
- package/src/router-dashboard.js +893 -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-D2ban2S-.js → index-DNRCaWPi.js} +2 -2
- package/web/dist/index.html +1 -1
package/src/overlays.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* @details
|
|
6
6
|
* This module centralizes all overlay rendering in one place:
|
|
7
|
-
* - Settings, Install Endpoints, Command Palette, Help, Smart Recommend, Feedback, Changelog
|
|
7
|
+
* - Settings, Install Endpoints, Command Palette, Help, Smart Recommend, Feedback, Changelog, Router Dashboard
|
|
8
8
|
* - Settings diagnostics for provider key tests, including wrapped retry/error details
|
|
9
9
|
* - Recommend analysis timer orchestration and progress updates
|
|
10
10
|
*
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*
|
|
16
16
|
* → Functions:
|
|
17
17
|
* - `createOverlayRenderers` — returns renderer + analysis helpers + overlayLayout
|
|
18
|
+
* - `renderRouterDashboard` — mounts the Smart Model Router dashboard renderer
|
|
18
19
|
*
|
|
19
20
|
* @exports { createOverlayRenderers }
|
|
20
21
|
* @see ./key-handler.js — handles keypresses for all overlay interactions
|
|
@@ -22,6 +23,7 @@
|
|
|
22
23
|
|
|
23
24
|
import { loadChangelog } from './changelog-loader.js'
|
|
24
25
|
import { buildCliHelpLines } from './cli-help.js'
|
|
26
|
+
import { renderRouterDashboard as renderRouterDashboardOverlay } from './router-dashboard.js'
|
|
25
27
|
import { themeColors, getThemeStatusLabel, getProviderRgb } from './theme.js'
|
|
26
28
|
|
|
27
29
|
export function createOverlayRenderers(state, deps) {
|
|
@@ -882,7 +884,7 @@ export function createOverlayRenderers(state, deps) {
|
|
|
882
884
|
lines.push(` ${label('CTX')} Context window size (128k, 200k, 256k, 1m, etc.) ${hint('Sort:')} ${key('C')}`)
|
|
883
885
|
lines.push(` ${hint('Bigger context = the model can read more of your codebase at once without forgetting.')}`)
|
|
884
886
|
lines.push('')
|
|
885
|
-
lines.push(` ${label('Model')} Model name (
|
|
887
|
+
lines.push(` ${label('Model')} Model name (①②③ = favorite order) ${hint('Sort:')} ${key('M')} ${hint('Favorite:')} ${key('F')}`)
|
|
886
888
|
lines.push(` ${hint('Star the ones you like. Press Y to switch between pinned mode and normal filter/sort mode.')}`)
|
|
887
889
|
lines.push('')
|
|
888
890
|
lines.push(` ${label('Provider')} Provider source (NIM, Groq, Cerebras, etc.) ${hint('Sort:')} ${key('O')} ${hint('Cycle:')} ${key('D')}`)
|
|
@@ -923,15 +925,17 @@ export function createOverlayRenderers(state, deps) {
|
|
|
923
925
|
lines.push(` ${key('Ctrl+P')} Open ⚡️ command palette ${hint('(search and run actions quickly)')}`)
|
|
924
926
|
lines.push(` ${key('E')} Toggle configured models only ${hint('(enabled by default)')}`)
|
|
925
927
|
lines.push(` ${key('Z')} Cycle tool mode ${hint('(📦 OpenCode → π Pi → 🪼 jcode → 📦 Desktop → 🦞 OpenClaw → 💘 Crush → 🪿 Goose → 🛠 Aider → 🐉 Qwen → 🤲 OpenHands → ⚡ Amp → 🦘 Rovo → ♊ Gemini)')}`)
|
|
926
|
-
lines.push(` ${key('F')} Toggle favorite on selected row ${hint('(
|
|
928
|
+
lines.push(` ${key('F')} Toggle favorite on selected row ${hint('(①②③ = router fallback order)')}`)
|
|
929
|
+
lines.push(` ${key('⇧↑/⇧↓')} Reorder selected favorite up/down ${hint('(changes router priority)')}`)
|
|
927
930
|
lines.push(` ${key('Y')} Toggle favorites mode ${hint('(Pinned + always visible ↔ Normal filter/sort behavior)')}`)
|
|
928
931
|
lines.push(` ${key('X')} Clear active text filter ${hint('(remove custom query applied from ⚡️ Command Palette)')}`)
|
|
929
932
|
lines.push(` ${key('Q')} Smart Recommend ${hint('(🎯 find the best model for your task — questionnaire + live analysis)')}`)
|
|
933
|
+
lines.push(` ${key('Shift+R')} Router Dashboard ${hint('(🔀 daemon health, circuit breakers, tokens, request log)')}`)
|
|
930
934
|
lines.push(` ${key('G')} Cycle theme ${hint('(auto → dark → light)')}`)
|
|
931
935
|
lines.push(` ${themeColors.errorBold('I')} Feedback, bugs & requests ${hint('(📝 send anonymous feedback, bug reports, or feature requests)')}`)
|
|
932
936
|
lines.push(` ${key('P')} Open settings ${hint('(manage API keys, provider toggles, updates, legacy cleanup)')}`)
|
|
933
937
|
// 📖 Profile system removed - API keys now persist permanently across all sessions
|
|
934
|
-
lines.push(` ${key('
|
|
938
|
+
lines.push(` ${key('Ctrl+P')} Reset view settings ${hint('(search "Reset view" in the command palette)')}`)
|
|
935
939
|
lines.push(` ${key('N')} Changelog ${hint('(📋 browse all versions, Enter to view details)')}`)
|
|
936
940
|
lines.push(` ${key('Ctrl+H')} / ${key('Esc')} Show/hide this help`)
|
|
937
941
|
lines.push(` ${key('Ctrl+C')} Exit`)
|
|
@@ -1394,6 +1398,10 @@ export function createOverlayRenderers(state, deps) {
|
|
|
1394
1398
|
if (state.recommendPingTimer) { clearInterval(state.recommendPingTimer); state.recommendPingTimer = null }
|
|
1395
1399
|
}
|
|
1396
1400
|
|
|
1401
|
+
function renderRouterDashboard() {
|
|
1402
|
+
return renderRouterDashboardOverlay(state, { LOCAL_VERSION })
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1397
1405
|
// ─── Incompatible fallback overlay ─────────────────────────────────────────
|
|
1398
1406
|
// 📖 renderIncompatibleFallback shows when user presses Enter on a model that
|
|
1399
1407
|
// 📖 is NOT compatible with the active tool. Two sections:
|
|
@@ -1490,6 +1498,195 @@ export function createOverlayRenderers(state, deps) {
|
|
|
1490
1498
|
return cleared.join('\n')
|
|
1491
1499
|
}
|
|
1492
1500
|
|
|
1501
|
+
|
|
1502
|
+
// ─── Token Usage screen renderer ───────────────────────────────────────────
|
|
1503
|
+
// 📖 renderTokenUsage: shows today/all-time breakdowns, by-model breakdown,
|
|
1504
|
+
// 📖 and a 7-day bar chart. Triggered by Shift+T from the main table.
|
|
1505
|
+
// 📖 Data fetched from GET /stats/tokens on the daemon.
|
|
1506
|
+
function renderTokenUsage() {
|
|
1507
|
+
const EL = '\x1b[K'
|
|
1508
|
+
const lines = []
|
|
1509
|
+
const cursorLineByRow = {}
|
|
1510
|
+
|
|
1511
|
+
lines.push('')
|
|
1512
|
+
lines.push(` ${themeColors.accent('🚀')} ${themeColors.accentBold('free-coding-models')} ${themeColors.dim(`v${LOCAL_VERSION}`)}`)
|
|
1513
|
+
lines.push(` ${themeColors.textBold('📊 Token Usage')} ${themeColors.dim('Shift+T from main table')}`)
|
|
1514
|
+
lines.push('')
|
|
1515
|
+
|
|
1516
|
+
const data = state.tokenUsageData
|
|
1517
|
+
|
|
1518
|
+
if (state.tokenUsageError) {
|
|
1519
|
+
lines.push(` ${themeColors.warning(state.tokenUsageError)}`)
|
|
1520
|
+
lines.push('')
|
|
1521
|
+
lines.push(themeColors.dim(' Press Shift+S to start the router daemon first, then reopen this screen.'))
|
|
1522
|
+
lines.push(themeColors.dim(' Esc to return to the main table'))
|
|
1523
|
+
const { visible, offset } = sliceOverlayLines(lines, state.tokenUsageScrollOffset, state.terminalRows)
|
|
1524
|
+
state.tokenUsageScrollOffset = offset
|
|
1525
|
+
const tintedLines = tintOverlayLines(visible, themeColors.overlayBgSettings, state.terminalCols)
|
|
1526
|
+
return tintedLines.map((l) => l + EL).join('\n')
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
if (!data) {
|
|
1530
|
+
lines.push(themeColors.dim(' Loading token stats...'))
|
|
1531
|
+
const { visible, offset } = sliceOverlayLines(lines, state.tokenUsageScrollOffset, state.terminalRows)
|
|
1532
|
+
state.tokenUsageScrollOffset = offset
|
|
1533
|
+
const tintedLines = tintOverlayLines(visible, themeColors.overlayBgSettings, state.terminalCols)
|
|
1534
|
+
return tintedLines.map((l) => l + EL).join('\n')
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
const today = data.today || {}
|
|
1538
|
+
const allTime = data.all_time || {}
|
|
1539
|
+
const dailyData = data.daily || {}
|
|
1540
|
+
|
|
1541
|
+
const todayTotal = today.total_tokens || 0
|
|
1542
|
+
const todayPrompt = today.prompt_tokens || 0
|
|
1543
|
+
const todayCompletion = today.completion_tokens || 0
|
|
1544
|
+
const todayReq = today.requests || 0
|
|
1545
|
+
const allTimeTotal = allTime.total_tokens || 0
|
|
1546
|
+
const allTimeReq = allTime.requests || 0
|
|
1547
|
+
const firstTracked = allTime.first_tracked || null
|
|
1548
|
+
|
|
1549
|
+
lines.push(` ${themeColors.textBold('TODAY')} ${themeColors.dim(new Date().toISOString().slice(0, 10))} ${themeColors.dim('|')} ${themeColors.textBold('ALL TIME')}`)
|
|
1550
|
+
lines.push(` ${themeColors.dim('─'.repeat(40))} ${themeColors.dim('─'.repeat(30))}`)
|
|
1551
|
+
lines.push(` ${themeColors.textBold('Total:')} ${themeColors.info(formatTokenTotalCompact(todayTotal))} tok ${themeColors.dim('│')} ${themeColors.textBold('Total:')} ${themeColors.info(formatTokenTotalCompact(allTimeTotal))} tok`)
|
|
1552
|
+
lines.push(` ${themeColors.textBold('Prompt:')} ${themeColors.dim(formatTokenTotalCompact(todayPrompt))} tok ${themeColors.dim('│')} ${themeColors.textBold('Requests:')} ${themeColors.dim(String(allTimeReq))}`)
|
|
1553
|
+
lines.push(` ${themeColors.textBold('Completion:')} ${themeColors.dim(formatTokenTotalCompact(todayCompletion))} tok ${themeColors.dim('│')} ${themeColors.textBold('Since:')} ${themeColors.dim(firstTracked ? new Date(firstTracked).toLocaleDateString() : '—')}`)
|
|
1554
|
+
lines.push(` ${themeColors.textBold('Requests:')} ${themeColors.dim(String(todayReq))} ${themeColors.dim('│')}`)
|
|
1555
|
+
|
|
1556
|
+
const byModel = today.by_model || {}
|
|
1557
|
+
const sortedModels = Object.entries(byModel)
|
|
1558
|
+
.map(([key, val]) => {
|
|
1559
|
+
// 📖 val can be a number (legacy) or { total, prompt, completion } object
|
|
1560
|
+
const total = (val && typeof val === 'object' && !Array.isArray(val)) ? (val.total || 0) : Number(val) || 0
|
|
1561
|
+
return { key, total }
|
|
1562
|
+
})
|
|
1563
|
+
.filter((m) => m.total > 0)
|
|
1564
|
+
.sort((a, b) => b.total - a.total)
|
|
1565
|
+
.slice(0, 8)
|
|
1566
|
+
|
|
1567
|
+
lines.push('')
|
|
1568
|
+
lines.push(` ${themeColors.textBold('TOP MODELS TODAY')}`)
|
|
1569
|
+
if (sortedModels.length === 0) {
|
|
1570
|
+
lines.push(themeColors.dim(' No usage tracked yet today.'))
|
|
1571
|
+
} else {
|
|
1572
|
+
const maxTotal = sortedModels[0]?.total || 1
|
|
1573
|
+
for (const m of sortedModels) {
|
|
1574
|
+
const barLen = Math.max(2, Math.round((m.total / maxTotal) * 28))
|
|
1575
|
+
const bar = themeColors.success('█'.repeat(barLen)) + themeColors.dim('░'.repeat(28 - barLen))
|
|
1576
|
+
const pct = todayTotal > 0 ? Math.round((m.total / todayTotal) * 100) : 0
|
|
1577
|
+
lines.push(` ${bar} ${themeColors.textBold(formatTokenTotalCompact(m.total))} tok ${themeColors.dim(`${pct}% ${m.key}`)}`)
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
lines.push('')
|
|
1582
|
+
lines.push(` ${themeColors.textBold('LAST 7 DAYS')}`)
|
|
1583
|
+
const dayLabels = []
|
|
1584
|
+
const dayTotals = []
|
|
1585
|
+
for (let i = 6; i >= 0; i--) {
|
|
1586
|
+
const d = new Date()
|
|
1587
|
+
d.setDate(d.getDate() - i)
|
|
1588
|
+
const key = d.toISOString().slice(0, 10)
|
|
1589
|
+
const dayData = dailyData[key]
|
|
1590
|
+
const total = dayData?.total_tokens || 0
|
|
1591
|
+
dayLabels.push(d.toLocaleDateString('en-US', { weekday: 'short' }))
|
|
1592
|
+
dayTotals.push(total)
|
|
1593
|
+
}
|
|
1594
|
+
const maxDay = Math.max(...dayTotals, 1)
|
|
1595
|
+
lines.push(` ${dayLabels.map((l, i) => themeColors.dim(padEndDisplay(l, 6))).join(' ')}`)
|
|
1596
|
+
const barHeights = [14, 10, 7, 4]
|
|
1597
|
+
for (const bh of barHeights) {
|
|
1598
|
+
const row = dayTotals.map((t) => {
|
|
1599
|
+
const filled = Math.round((t / maxDay) * bh)
|
|
1600
|
+
const bar = themeColors.info('█'.repeat(filled)) + themeColors.dim('░'.repeat(bh - filled))
|
|
1601
|
+
return padEndDisplay(bar, 6)
|
|
1602
|
+
})
|
|
1603
|
+
lines.push(` ${row.join(' ')}`)
|
|
1604
|
+
}
|
|
1605
|
+
const totalRow = dayTotals.map((t) => padEndDisplay(themeColors.textBold(formatTokenTotalCompact(t)), 6))
|
|
1606
|
+
lines.push(` ${totalRow.join(' ')}`)
|
|
1607
|
+
|
|
1608
|
+
lines.push('')
|
|
1609
|
+
lines.push(themeColors.dim(' Esc Back to main table • Shift+R Router Dashboard'))
|
|
1610
|
+
|
|
1611
|
+
const { visible, offset } = sliceOverlayLines(lines, state.tokenUsageScrollOffset, state.terminalRows)
|
|
1612
|
+
state.tokenUsageScrollOffset = offset
|
|
1613
|
+
const tintedLines = tintOverlayLines(visible, themeColors.overlayBgSettings, state.terminalCols)
|
|
1614
|
+
return tintedLines.map((l) => l + EL).join('\n')
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
// ─── Router Onboarding overlay renderer ─────────────────────────────────────
|
|
1618
|
+
// 📖 renderRouterOnboarding: shown on first launch (no config.router) or
|
|
1619
|
+
// 📖 first launch after upgrade (existing config but router.onboardingSeen !== true).
|
|
1620
|
+
// 📖 Two options: Enable (Y) or Not now (N). Phase 6 — Smart Model Router.
|
|
1621
|
+
function renderRouterOnboarding() {
|
|
1622
|
+
const EL = '\x1b[K'
|
|
1623
|
+
const lines = []
|
|
1624
|
+
const cursorLineByRow = {}
|
|
1625
|
+
|
|
1626
|
+
lines.push('')
|
|
1627
|
+
lines.push(` ${themeColors.accent('🚀')} ${themeColors.accentBold('free-coding-models')} ${themeColors.dim(`v${LOCAL_VERSION}`)}`)
|
|
1628
|
+
lines.push(` ${themeColors.textBold('🔀 Smart Router Available!')}`)
|
|
1629
|
+
lines.push('')
|
|
1630
|
+
lines.push(themeColors.dim(' FCM can run a background daemon that automatically'))
|
|
1631
|
+
lines.push(themeColors.dim(' routes your requests to the fastest healthy model —'))
|
|
1632
|
+
lines.push(themeColors.dim(' with zero manual intervention after initial setup.'))
|
|
1633
|
+
lines.push('')
|
|
1634
|
+
|
|
1635
|
+
const options = [
|
|
1636
|
+
{ label: 'Yes, enable the router', hint: 'Recommended — creates default set and starts daemon', key: 'Y' },
|
|
1637
|
+
{ label: 'Not now', hint: 'You can enable it later from the TUI', key: 'N' },
|
|
1638
|
+
]
|
|
1639
|
+
|
|
1640
|
+
if (state.routerOnboardingPhase === 'loading') {
|
|
1641
|
+
lines.push(themeColors.info(' Enabling router, please wait...'))
|
|
1642
|
+
} else if (state.routerOnboardingPhase === 'success') {
|
|
1643
|
+
lines.push(themeColors.success(' ✅ Router enabled! Dashboard opening...'))
|
|
1644
|
+
lines.push(themeColors.dim(' Shift+R to reopen the dashboard anytime'))
|
|
1645
|
+
} else if (state.routerOnboardingPhase === 'error') {
|
|
1646
|
+
lines.push(themeColors.error(` ❌ ${state.routerOnboardingError || 'Failed to enable router'}`))
|
|
1647
|
+
lines.push(themeColors.dim(' Press Esc or Enter to continue to the main table'))
|
|
1648
|
+
} else {
|
|
1649
|
+
for (let i = 0; i < options.length; i++) {
|
|
1650
|
+
const opt = options[i]
|
|
1651
|
+
const isCursor = i === state.routerOnboardingCursor
|
|
1652
|
+
const keyLabel = themeColors.hotkey(` ${opt.key}]`)
|
|
1653
|
+
const row = `${bullet(isCursor)}${keyLabel} ${isCursor ? themeColors.textBold(opt.label) : themeColors.text(opt.label)}`
|
|
1654
|
+
cursorLineByRow[i] = lines.length
|
|
1655
|
+
lines.push(isCursor ? themeColors.bgCursorSettingsList(row) : row)
|
|
1656
|
+
lines.push(themeColors.dim(` ${opt.hint}`))
|
|
1657
|
+
lines.push('')
|
|
1658
|
+
}
|
|
1659
|
+
lines.push(themeColors.dim(' ↑↓ Navigate • Enter Select • Esc Skip for now'))
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
const targetLine = cursorLineByRow[state.routerOnboardingCursor] ?? 0
|
|
1663
|
+
state.routerOnboardingScrollOffset = keepOverlayTargetVisible(state.routerOnboardingScrollOffset, targetLine, lines.length, state.terminalRows)
|
|
1664
|
+
const { visible, offset } = sliceOverlayLines(lines, state.routerOnboardingScrollOffset, state.terminalRows)
|
|
1665
|
+
state.routerOnboardingScrollOffset = offset
|
|
1666
|
+
const tintedLines = tintOverlayLines(visible, themeColors.overlayBgSettings, state.terminalCols)
|
|
1667
|
+
return tintedLines.map((l) => l + EL).join('\n')
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
// ─── Router upgrade banner (inline in main table, not an overlay) ─────────────
|
|
1671
|
+
// 📖 renderRouterUpgradeBanner: non-blocking notification at top of the table
|
|
1672
|
+
// 📖 shown once to existing users who haven't seen router yet. Auto-dismisses after 10s.
|
|
1673
|
+
function renderRouterUpgradeBanner() {
|
|
1674
|
+
const EL = '\x1b[K'
|
|
1675
|
+
const now = Date.now()
|
|
1676
|
+
const BANNER_TTL_MS = 10_000
|
|
1677
|
+
// Dismissed or already seen in this session?
|
|
1678
|
+
if (state.routerUpgradeBannerDismissedAt > 0) return ''
|
|
1679
|
+
if (state.routerUpgradeBannerShownAt === 0) state.routerUpgradeBannerShownAt = now
|
|
1680
|
+
if (now - state.routerUpgradeBannerShownAt > BANNER_TTL_MS) {
|
|
1681
|
+
state.routerUpgradeBannerDismissedAt = now
|
|
1682
|
+
return ''
|
|
1683
|
+
}
|
|
1684
|
+
const remaining = Math.ceil((BANNER_TTL_MS - (now - state.routerUpgradeBannerShownAt)) / 1000)
|
|
1685
|
+
const msg = ` ${themeColors.accentBold('🆕')} ${themeColors.textBold('Smart Router is now available!')} ${themeColors.dim('Press')} ${themeColors.hotkey('Shift+R')} ${themeColors.dim('to set it up.')} ${themeColors.dim(`(dismisses in ${remaining}s)`)}`
|
|
1686
|
+
const pad = state.terminalCols > displayWidth(msg) ? ' '.repeat(Math.max(0, state.terminalCols - displayWidth(msg))) : ''
|
|
1687
|
+
return themeColors.warningBold(msg + pad)
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1493
1690
|
return {
|
|
1494
1691
|
renderSettings,
|
|
1495
1692
|
renderInstallEndpoints,
|
|
@@ -1500,9 +1697,13 @@ export function createOverlayRenderers(state, deps) {
|
|
|
1500
1697
|
renderFeedback,
|
|
1501
1698
|
renderChangelog,
|
|
1502
1699
|
renderInstalledModels,
|
|
1700
|
+
renderRouterDashboard,
|
|
1503
1701
|
renderIncompatibleFallback,
|
|
1702
|
+
renderTokenUsage,
|
|
1703
|
+
renderRouterOnboarding,
|
|
1704
|
+
renderRouterUpgradeBanner,
|
|
1504
1705
|
startRecommendAnalysis,
|
|
1505
1706
|
stopRecommendAnalysis,
|
|
1506
|
-
overlayLayout,
|
|
1707
|
+
overlayLayout,
|
|
1507
1708
|
}
|
|
1508
1709
|
}
|
package/src/provider-metadata.js
CHANGED
|
@@ -45,22 +45,17 @@ export const ENV_VAR_NAMES = {
|
|
|
45
45
|
cerebras: 'CEREBRAS_API_KEY',
|
|
46
46
|
sambanova: 'SAMBANOVA_API_KEY',
|
|
47
47
|
openrouter: 'OPENROUTER_API_KEY',
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
fireworks: 'FIREWORKS_API_KEY',
|
|
52
|
-
codestral: 'CODESTRAL_API_KEY',
|
|
53
|
-
hyperbolic: 'HYPERBOLIC_API_KEY',
|
|
48
|
+
'github-models': 'GITHUB_TOKEN',
|
|
49
|
+
mistral: 'MISTRAL_API_KEY',
|
|
50
|
+
codestral: 'MISTRAL_API_KEY',
|
|
54
51
|
scaleway: 'SCALEWAY_API_KEY',
|
|
55
52
|
googleai: 'GOOGLE_API_KEY',
|
|
56
|
-
siliconflow:'SILICONFLOW_API_KEY',
|
|
57
|
-
together: 'TOGETHER_API_KEY',
|
|
58
53
|
cloudflare: 'CLOUDFLARE_API_TOKEN',
|
|
59
|
-
perplexity: 'PERPLEXITY_API_KEY',
|
|
60
54
|
zai: 'ZAI_API_KEY',
|
|
61
55
|
gemini: 'GEMINI_API_KEY',
|
|
62
|
-
chutes: 'CHUTES_API_KEY',
|
|
63
56
|
ovhcloud: 'OVH_AI_ENDPOINTS_ACCESS_TOKEN',
|
|
57
|
+
qwen: 'DASHSCOPE_API_KEY',
|
|
58
|
+
'opencode-zen': 'OPENCODE_ZEN_API_KEY',
|
|
64
59
|
}
|
|
65
60
|
|
|
66
61
|
// 📖 OPENCODE_MODEL_MAP: sparse table of model IDs that differ between sources.js and OpenCode's
|
|
@@ -103,7 +98,7 @@ export const PROVIDER_METADATA = {
|
|
|
103
98
|
color: chalk.rgb(255, 224, 178),
|
|
104
99
|
signupUrl: 'https://cloud.sambanova.ai/apis',
|
|
105
100
|
signupHint: 'SambaCloud portal → Create API key',
|
|
106
|
-
rateLimits: '
|
|
101
|
+
rateLimits: 'Small developer quota; useful for light coding and smoke tests',
|
|
107
102
|
},
|
|
108
103
|
openrouter: {
|
|
109
104
|
label: 'OpenRouter',
|
|
@@ -113,6 +108,20 @@ export const PROVIDER_METADATA = {
|
|
|
113
108
|
rateLimits: 'Free on :free: 50/day <$10, 1000/day ≥$10 (20 req/min)',
|
|
114
109
|
detailedLimits: 'No credits (or <$10) → 50 requests/day (20 req/min)\n≥ $10 in credits → 1000 requests/day (20 req/min)\n• Free models (:free) never consume credits\n• Failed requests count toward quota\n• Quota resets daily at midnight UTC\n• Free-tier models may be rate-limited during peak hours',
|
|
115
110
|
},
|
|
111
|
+
'github-models': {
|
|
112
|
+
label: 'GitHub Models',
|
|
113
|
+
color: chalk.rgb(183, 201, 255),
|
|
114
|
+
signupUrl: 'https://models.github.ai',
|
|
115
|
+
signupHint: 'Use a GitHub token with Models access (GITHUB_TOKEN works in GitHub contexts)',
|
|
116
|
+
rateLimits: 'Quota depends on GitHub/Copilot tier; no separate provider billing',
|
|
117
|
+
},
|
|
118
|
+
mistral: {
|
|
119
|
+
label: 'Mistral La Plateforme',
|
|
120
|
+
color: chalk.rgb(255, 196, 120),
|
|
121
|
+
signupUrl: 'https://console.mistral.ai/api-keys',
|
|
122
|
+
signupHint: 'La Plateforme → API keys (MISTRAL_API_KEY)',
|
|
123
|
+
rateLimits: 'Experiment plan: free evaluation tier with limited RPS/TPM/monthly tokens',
|
|
124
|
+
},
|
|
116
125
|
huggingface: {
|
|
117
126
|
label: 'Hugging Face Inference',
|
|
118
127
|
color: chalk.rgb(255, 245, 157),
|
|
@@ -146,9 +155,9 @@ export const PROVIDER_METADATA = {
|
|
|
146
155
|
codestral: {
|
|
147
156
|
label: 'Mistral Codestral',
|
|
148
157
|
color: chalk.rgb(248, 187, 208),
|
|
149
|
-
signupUrl: 'https://
|
|
150
|
-
signupHint: '
|
|
151
|
-
rateLimits: '30 req/min, 2000/day',
|
|
158
|
+
signupUrl: 'https://console.mistral.ai/api-keys',
|
|
159
|
+
signupHint: 'La Plateforme → API keys (MISTRAL_API_KEY; CODESTRAL_API_KEY also works)',
|
|
160
|
+
rateLimits: 'Codestral free access: 30 req/min, 2000/day',
|
|
152
161
|
},
|
|
153
162
|
hyperbolic: {
|
|
154
163
|
label: 'Hyperbolic',
|
|
@@ -169,7 +178,7 @@ export const PROVIDER_METADATA = {
|
|
|
169
178
|
color: chalk.rgb(187, 222, 251),
|
|
170
179
|
signupUrl: 'https://aistudio.google.com/apikey',
|
|
171
180
|
signupHint: 'Get API key',
|
|
172
|
-
rateLimits: '
|
|
181
|
+
rateLimits: 'Gemini free quotas vary by model and region',
|
|
173
182
|
},
|
|
174
183
|
siliconflow: {
|
|
175
184
|
label: 'SiliconFlow',
|
|
@@ -211,7 +220,7 @@ export const PROVIDER_METADATA = {
|
|
|
211
220
|
color: chalk.rgb(174, 213, 255),
|
|
212
221
|
signupUrl: 'https://z.ai',
|
|
213
222
|
signupHint: 'Sign up and generate an API key',
|
|
214
|
-
rateLimits: 'Free tier
|
|
223
|
+
rateLimits: 'Free tier: Flash models only in this catalog',
|
|
215
224
|
},
|
|
216
225
|
iflow: {
|
|
217
226
|
label: 'iFlow',
|
|
@@ -240,7 +249,7 @@ export const PROVIDER_METADATA = {
|
|
|
240
249
|
color: chalk.rgb(66, 165, 245), // blue
|
|
241
250
|
signupUrl: 'https://github.com/google-gemini/gemini-cli',
|
|
242
251
|
signupHint: 'Install: npm install -g @google/gemini-cli',
|
|
243
|
-
rateLimits: 'Free tier: 1,000 req/day
|
|
252
|
+
rateLimits: 'Free tier: 1,000 req/day with personal Google account',
|
|
244
253
|
cliOnly: true,
|
|
245
254
|
},
|
|
246
255
|
'opencode-zen': {
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
* @property {'header'|'endpoint'|'unknown'} telemetryType
|
|
30
30
|
* @property {boolean} [supportsEndpoint]
|
|
31
31
|
* @property {'percent'|'ok'} usageDisplay
|
|
32
|
-
* @property {'rolling'|'daily'|'unknown'|'none'} resetCadence
|
|
32
|
+
* @property {'rolling'|'daily'|'monthly'|'unknown'|'none'} resetCadence
|
|
33
33
|
*/
|
|
34
34
|
|
|
35
35
|
/** @type {Record<string, ProviderCapability>} */
|
|
@@ -39,26 +39,22 @@ export const PROVIDER_CAPABILITIES = {
|
|
|
39
39
|
groq: { telemetryType: 'header', supportsEndpoint: false, usageDisplay: 'percent', resetCadence: 'daily' },
|
|
40
40
|
cerebras: { telemetryType: 'header', supportsEndpoint: false, usageDisplay: 'percent', resetCadence: 'unknown' },
|
|
41
41
|
sambanova: { telemetryType: 'header', supportsEndpoint: false, usageDisplay: 'percent', resetCadence: 'unknown' },
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
together: { telemetryType: 'header', supportsEndpoint: false, usageDisplay: 'percent', resetCadence: 'unknown' },
|
|
45
|
-
hyperbolic: { telemetryType: 'header', supportsEndpoint: false, usageDisplay: 'percent', resetCadence: 'unknown' },
|
|
42
|
+
'github-models': { telemetryType: 'header', supportsEndpoint: false, usageDisplay: 'percent', resetCadence: 'unknown' },
|
|
43
|
+
mistral: { telemetryType: 'header', supportsEndpoint: false, usageDisplay: 'percent', resetCadence: 'monthly' },
|
|
46
44
|
scaleway: { telemetryType: 'header', supportsEndpoint: false, usageDisplay: 'percent', resetCadence: 'unknown' },
|
|
47
45
|
googleai: { telemetryType: 'header', supportsEndpoint: false, usageDisplay: 'percent', resetCadence: 'daily' },
|
|
48
46
|
codestral: { telemetryType: 'header', supportsEndpoint: false, usageDisplay: 'percent', resetCadence: 'daily' },
|
|
49
|
-
perplexity: { telemetryType: 'header', supportsEndpoint: false, usageDisplay: 'percent', resetCadence: 'unknown' },
|
|
50
47
|
qwen: { telemetryType: 'header', supportsEndpoint: false, usageDisplay: 'percent', resetCadence: 'unknown' },
|
|
48
|
+
ovhcloud: { telemetryType: 'header', supportsEndpoint: false, usageDisplay: 'percent', resetCadence: 'unknown' },
|
|
51
49
|
|
|
52
50
|
// Providers that have a dedicated usage/credits endpoint
|
|
53
51
|
openrouter: { telemetryType: 'endpoint', supportsEndpoint: true, usageDisplay: 'percent', resetCadence: 'unknown' },
|
|
54
|
-
siliconflow: { telemetryType: 'endpoint', supportsEndpoint: true, usageDisplay: 'ok', resetCadence: 'unknown' },
|
|
55
52
|
|
|
56
53
|
// Providers with no reliable quota signal
|
|
57
|
-
huggingface: { telemetryType: 'unknown', supportsEndpoint: false, usageDisplay: 'ok', resetCadence: 'none' },
|
|
58
|
-
replicate: { telemetryType: 'unknown', supportsEndpoint: false, usageDisplay: 'ok', resetCadence: 'none' },
|
|
59
54
|
cloudflare: { telemetryType: 'unknown', supportsEndpoint: false, usageDisplay: 'ok', resetCadence: 'daily' },
|
|
60
55
|
zai: { telemetryType: 'unknown', supportsEndpoint: false, usageDisplay: 'ok', resetCadence: 'none' },
|
|
61
|
-
|
|
56
|
+
gemini: { telemetryType: 'unknown', supportsEndpoint: false, usageDisplay: 'ok', resetCadence: 'daily' },
|
|
57
|
+
'opencode-zen': { telemetryType: 'unknown', supportsEndpoint: false, usageDisplay: 'ok', resetCadence: 'unknown' },
|
|
62
58
|
}
|
|
63
59
|
|
|
64
60
|
/** Fallback for unrecognized providers */
|
package/src/render-table.js
CHANGED
|
@@ -54,6 +54,7 @@ import { calculateViewport, sortResultsWithPinnedFavorites, padEndDisplay, displ
|
|
|
54
54
|
import { getToolMeta, TOOL_METADATA, TOOL_MODE_ORDER, isModelCompatibleWithTool } from './tool-metadata.js'
|
|
55
55
|
import { getColumnSpacing } from './ui-config.js'
|
|
56
56
|
import { detectPackageManager, getManualInstallCmd } from './updater.js'
|
|
57
|
+
import { formatTokenTotalCompact } from './token-usage-reader.js'
|
|
57
58
|
|
|
58
59
|
const require = createRequire(import.meta.url)
|
|
59
60
|
const { version: LOCAL_VERSION } = require('../package.json')
|
|
@@ -109,7 +110,7 @@ export const PROVIDER_COLOR = new Proxy({}, {
|
|
|
109
110
|
})
|
|
110
111
|
|
|
111
112
|
// ─── renderTable: mode param controls footer hint text (opencode vs openclaw) ─────────
|
|
112
|
-
export function renderTable(results, pendingPings, frame, cursor = null, sortColumn = 'avg', sortDirection = 'asc', pingInterval = PING_INTERVAL, lastPingTime = Date.now(), mode = 'opencode', tierFilterMode = 0, scrollOffset = 0, terminalRows = 0, terminalCols = 0, originFilterMode = 0, legacyStatus = null, pingMode = 'normal', pingModeSource = 'auto', hideUnconfiguredModels = false, widthWarningStartedAt = null, widthWarningDismissed = false, widthWarningShowCount = 0, settingsUpdateState = 'idle', settingsUpdateLatestVersion = null, legacyFlag = false, startupLatestVersion = null, versionAlertsEnabled = true, favoritesPinnedAndSticky = false, customTextFilter = null, lastReleaseDate = null, footerHidden = false, verdictFilterMode = 0, healthFilterMode = 0) {
|
|
113
|
+
export function renderTable(results, pendingPings, frame, cursor = null, sortColumn = 'avg', sortDirection = 'asc', pingInterval = PING_INTERVAL, lastPingTime = Date.now(), mode = 'opencode', tierFilterMode = 0, scrollOffset = 0, terminalRows = 0, terminalCols = 0, originFilterMode = 0, legacyStatus = null, pingMode = 'normal', pingModeSource = 'auto', hideUnconfiguredModels = false, widthWarningStartedAt = null, widthWarningDismissed = false, widthWarningShowCount = 0, settingsUpdateState = 'idle', settingsUpdateLatestVersion = null, legacyFlag = false, startupLatestVersion = null, versionAlertsEnabled = true, favoritesPinnedAndSticky = false, customTextFilter = null, lastReleaseDate = null, footerHidden = false, verdictFilterMode = 0, healthFilterMode = 0, routerFooterRunning = false, routerFooterActiveSet = null, routerFooterTodayTokens = 0, routerFooterAllTimeTokens = 0, routerFooterRequests = 0) {
|
|
113
114
|
// 📖 Filter out hidden models for display
|
|
114
115
|
const visibleResults = results.filter(r => !r.hidden)
|
|
115
116
|
|
|
@@ -537,9 +538,15 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
537
538
|
? providerName.slice(0, 4) + '…'
|
|
538
539
|
: providerName
|
|
539
540
|
const source = themeColors.provider(r.providerKey, providerDisplay.padEnd(wSource))
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
541
|
+
const CIRCLED = ['①','②','③','④','⑤','⑥','⑦','⑧','⑨','⑩','⑪','⑫','⑬','⑭','⑮','⑯','⑰','⑱','⑲','⑳']
|
|
542
|
+
let favoritePrefix = ' '
|
|
543
|
+
if (r.isRecommended) {
|
|
544
|
+
favoritePrefix = '🎯'
|
|
545
|
+
} else if (r.isFavorite && r.favoriteRank < CIRCLED.length) {
|
|
546
|
+
favoritePrefix = CIRCLED[r.favoriteRank]
|
|
547
|
+
} else if (r.isFavorite) {
|
|
548
|
+
favoritePrefix = '⭐'
|
|
549
|
+
}
|
|
543
550
|
const prefixDisplayWidth = 2
|
|
544
551
|
const nameWidth = Math.max(0, W_MODEL - prefixDisplayWidth)
|
|
545
552
|
const name = favoritePrefix + r.label.slice(0, nameWidth).padEnd(nameWidth)
|
|
@@ -865,6 +872,8 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
865
872
|
{ text: ' • ', key: null },
|
|
866
873
|
{ text: 'Q Smart Recommend', key: 'q' },
|
|
867
874
|
{ text: ' • ', key: null },
|
|
875
|
+
{ text: 'Shift+R Router', key: 'shift+r' },
|
|
876
|
+
{ text: ' • ', key: null },
|
|
868
877
|
{ text: 'G Theme', key: 'g' },
|
|
869
878
|
{ text: ' • ', key: null },
|
|
870
879
|
{ text: 'I Feedback, bugs & requests', key: 'i' },
|
|
@@ -884,6 +893,7 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
884
893
|
lines.push(
|
|
885
894
|
' ' + paletteLabel + themeColors.dim(` • `) +
|
|
886
895
|
hotkey('Q', ' Smart Recommend') + themeColors.dim(` • `) +
|
|
896
|
+
hotkey('Shift+R', ' Router') + themeColors.dim(` • `) +
|
|
887
897
|
hotkey('G', ' Theme') + themeColors.dim(` • `) +
|
|
888
898
|
hotkey('I', ' Feedback, bugs & requests')
|
|
889
899
|
)
|
|
@@ -968,6 +978,27 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
968
978
|
(releaseLabel ? themeColors.dim(' • ') + releaseLabel : '')
|
|
969
979
|
)
|
|
970
980
|
|
|
981
|
+
// 📖 Router token stats + daemon status in the footer (shown when router is enabled)
|
|
982
|
+
if (routerFooterRunning) {
|
|
983
|
+
const todayStr = formatTokenTotalCompact(routerFooterTodayTokens)
|
|
984
|
+
const allTimeStr = formatTokenTotalCompact(routerFooterAllTimeTokens)
|
|
985
|
+
const reqStr = String(routerFooterRequests)
|
|
986
|
+
const setLabel = routerFooterActiveSet ? themeColors.info(routerFooterActiveSet) : themeColors.dim('?')
|
|
987
|
+
lines.push(
|
|
988
|
+
' ' + themeColors.success('●') + ' ' +
|
|
989
|
+
themeColors.dim('Router:') + ' ' + setLabel +
|
|
990
|
+
themeColors.dim(' • Today:') + ' ' + themeColors.textBold(todayStr + ' tok') +
|
|
991
|
+
themeColors.dim(' • All-time:') + ' ' + themeColors.textBold(allTimeStr + ' tok') +
|
|
992
|
+
themeColors.dim(' • ' + reqStr + ' req')
|
|
993
|
+
)
|
|
994
|
+
} else {
|
|
995
|
+
lines.push(
|
|
996
|
+
' ' + themeColors.error('○') + ' ' +
|
|
997
|
+
themeColors.dim('Router:') + ' ' + themeColors.dim('daemon not running') +
|
|
998
|
+
themeColors.dim(' • Shift+R Dashboard')
|
|
999
|
+
)
|
|
1000
|
+
}
|
|
1001
|
+
|
|
971
1002
|
// 📖 Discord link at the very bottom of the TUI
|
|
972
1003
|
lines.push(
|
|
973
1004
|
' 💬 ' +
|
|
@@ -979,6 +1010,8 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
979
1010
|
// 📖 Collapsed footer: single line with toggle hint
|
|
980
1011
|
lines.push(
|
|
981
1012
|
' ' + themeColors.hotkey('Ctrl+O') + themeColors.dim(' Toggle Footer') +
|
|
1013
|
+
themeColors.dim(' • ') +
|
|
1014
|
+
themeColors.hotkey('Shift+R') + themeColors.dim(' Router') +
|
|
982
1015
|
themeColors.dim(' • Ctrl+C Exit')
|
|
983
1016
|
)
|
|
984
1017
|
}
|