free-coding-models 0.3.62 → 0.3.64
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 +6 -6
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/app.js +31 -7
- package/src/command-palette.js +2 -2
- package/src/endpoint-installer.js +17 -0
- package/src/favorites.js +22 -1
- package/src/key-handler.js +135 -35
- package/src/overlays.js +8 -6
- package/src/render-table.js +15 -5
- package/src/router-daemon.js +11 -9
- package/src/router-dashboard.js +99 -22
- package/web/dist/assets/{index-A4ph-qbA.js → index-CwIXdKao.js} +1 -1
- package/web/dist/index.html +1 -1
package/src/render-table.js
CHANGED
|
@@ -104,7 +104,7 @@ export const PROVIDER_COLOR = new Proxy({}, {
|
|
|
104
104
|
})
|
|
105
105
|
|
|
106
106
|
// ─── renderTable: mode param controls footer hint text (opencode vs openclaw) ─────────
|
|
107
|
-
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, legacyFooterHidden = false, verdictFilterMode = 0, healthFilterMode = 0, routerFooterRunning = false, routerFooterActiveSet = null, routerFooterTodayTokens = 0, routerFooterAllTimeTokens = 0, routerFooterRequests = 0) {
|
|
107
|
+
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, legacyFooterHidden = false, verdictFilterMode = 0, healthFilterMode = 0, bestModeOnly = false, routerFooterRunning = false, routerFooterActiveSet = null, routerFooterTodayTokens = 0, routerFooterAllTimeTokens = 0, routerFooterRequests = 0) {
|
|
108
108
|
// 📖 Filter out hidden models for display
|
|
109
109
|
const visibleResults = results.filter(r => !r.hidden)
|
|
110
110
|
void legacyFooterHidden
|
|
@@ -747,7 +747,11 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
747
747
|
// 📖 Active filter pills use a loud green background so tier/provider/configured-only
|
|
748
748
|
// 📖 states are obvious even when the user misses the smaller header badges.
|
|
749
749
|
const configuredBadgeBg = getTheme() === 'dark' ? [52, 120, 88] : [195, 234, 206]
|
|
750
|
+
|
|
751
|
+
const configuredFilterActive = hideUnconfiguredModels || bestModeOnly
|
|
752
|
+
const configuredFilterText = bestModeOnly ? 'Usable only' : (hideUnconfiguredModels ? 'Configured only' : 'Active only')
|
|
750
753
|
const activeHotkey = (keyLabel, text, bg) => themeColors.badge(`${keyLabel}${text}`, bg, getReadableTextRgb(bg))
|
|
754
|
+
const activeFilterHotkey = (keyLabel, text, bg) => themeColors.hotkey(keyLabel) + themeColors.badge(text, bg, getReadableTextRgb(bg))
|
|
751
755
|
|
|
752
756
|
// 📖 Mouse support: build footer hotkey zones alongside the footer lines.
|
|
753
757
|
// 📖 Each zone records { key, row (1-based terminal row), xStart, xEnd (1-based display cols) }.
|
|
@@ -767,11 +771,13 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
767
771
|
{ text: ' • ', key: null },
|
|
768
772
|
{ text: originFilterMode > 0 ? `D Provider (${activeOriginLabel})` : 'D Provider', key: 'd' },
|
|
769
773
|
{ text: ' • ', key: null },
|
|
770
|
-
{ text:
|
|
774
|
+
{ text: `E ${configuredFilterText}`, key: 'e' },
|
|
771
775
|
{ text: ' • ', key: null },
|
|
772
776
|
{ text: 'P Settings', key: 'p' },
|
|
773
777
|
{ text: ' • ', key: null },
|
|
774
778
|
{ text: 'I Help', key: 'i' },
|
|
779
|
+
{ text: ' • ', key: null },
|
|
780
|
+
{ text: 'N Reset', key: 'n' },
|
|
775
781
|
]
|
|
776
782
|
const footerRow1 = lines.length + 1 // 📖 1-based terminal row (line hasn't been pushed yet)
|
|
777
783
|
let xPos = 1
|
|
@@ -795,11 +801,15 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
795
801
|
? activeHotkey('D', ` Provider (${activeOriginLabel})`, PROVIDER_COLOR[[null, ...Object.keys(sources)][originFilterMode]] || [255, 255, 255])
|
|
796
802
|
: hotkey('D', ' Provider')) +
|
|
797
803
|
themeColors.dim(` • `) +
|
|
798
|
-
(
|
|
804
|
+
(configuredFilterActive
|
|
805
|
+
? activeFilterHotkey('E', configuredFilterText, configuredBadgeBg)
|
|
806
|
+
: hotkey('E', ' Active only')) +
|
|
799
807
|
themeColors.dim(` • `) +
|
|
800
808
|
hotkey('P', ' Settings') +
|
|
801
809
|
themeColors.dim(` • `) +
|
|
802
|
-
hotkey('I', ' Help')
|
|
810
|
+
hotkey('I', ' Help') +
|
|
811
|
+
themeColors.dim(` • `) +
|
|
812
|
+
hotkey('N', ' Reset')
|
|
803
813
|
)
|
|
804
814
|
|
|
805
815
|
// 📖 Line 2: command palette + GitHub
|
|
@@ -825,7 +835,7 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
825
835
|
const starLink = '⭐ ' + themeColors.link('\x1b]8;;https://github.com/vava-nessa/free-coding-models\x1b\\GitHub\x1b]8;;\x1b\\')
|
|
826
836
|
lines.push(
|
|
827
837
|
' ' + paletteLabel + themeColors.dim(` • `) + starLink + themeColors.dim(` • `) +
|
|
828
|
-
chalk.rgb(255, 168, 209).bold('\x1b]8;;https://x.com/vavanessadev\x1b\\
|
|
838
|
+
chalk.rgb(255, 168, 209).bold('\x1b]8;;https://x.com/vavanessadev\x1b\\Follow @vavanessadev on X for updates and support\x1b]8;;\x1b\\')
|
|
829
839
|
)
|
|
830
840
|
|
|
831
841
|
if (versionStatus.isOutdated) {
|
package/src/router-daemon.js
CHANGED
|
@@ -637,7 +637,8 @@ class RouterRuntime {
|
|
|
637
637
|
reloadConfigFromDisk() {
|
|
638
638
|
try {
|
|
639
639
|
const nextConfig = loadConfig()
|
|
640
|
-
|
|
640
|
+
// 📖 Always rebuild the router set from favorites so UI toggles apply dynamically
|
|
641
|
+
ensureRouterConfigForDaemon(nextConfig, true)
|
|
641
642
|
this.config = nextConfig
|
|
642
643
|
this.refreshRouteState()
|
|
643
644
|
this.scheduleProbeLoop()
|
|
@@ -649,12 +650,10 @@ class RouterRuntime {
|
|
|
649
650
|
}
|
|
650
651
|
|
|
651
652
|
getApiKeyForProvider(providerKey) {
|
|
652
|
-
// 📖 Router background startup should work without inherited shell env, so
|
|
653
|
-
// 📖 config keys are primary. Env is only a fallback for headless sessions.
|
|
654
653
|
const configured = this.config?.apiKeys?.[providerKey]
|
|
655
654
|
if (Array.isArray(configured)) return configured.find(Boolean) || null
|
|
656
655
|
if (typeof configured === 'string' && configured.trim()) return configured.trim()
|
|
657
|
-
return
|
|
656
|
+
return null
|
|
658
657
|
}
|
|
659
658
|
|
|
660
659
|
getSet(setName = null) {
|
|
@@ -1052,15 +1051,18 @@ class RouterRuntime {
|
|
|
1052
1051
|
let errorCode = 'all_models_unavailable'
|
|
1053
1052
|
let errorType = 'service_unavailable'
|
|
1054
1053
|
if (health.length > 0) {
|
|
1055
|
-
|
|
1054
|
+
const allAuthError = health.length > 0 && health.every((h) => h.state === 'AUTH_ERROR')
|
|
1055
|
+
const allAuthOrQuota = health.length > 0 && health.every((h) => h.state === 'AUTH_ERROR' || quotaExhausted.includes(h.key))
|
|
1056
|
+
const allStaleOrUnsupported = health.every((h) => h.state === 'STALE' || h.state === 'UNSUPPORTED')
|
|
1057
|
+
if (allAuthError) {
|
|
1056
1058
|
statusCode = 401
|
|
1057
1059
|
errorCode = 'invalid_api_key'
|
|
1058
1060
|
errorType = 'invalid_request_error'
|
|
1059
|
-
} else if (
|
|
1061
|
+
} else if (allAuthOrQuota) {
|
|
1060
1062
|
statusCode = 429
|
|
1061
1063
|
errorCode = 'insufficient_quota'
|
|
1062
1064
|
errorType = 'insufficient_quota'
|
|
1063
|
-
} else if (
|
|
1065
|
+
} else if (allStaleOrUnsupported) {
|
|
1064
1066
|
statusCode = 400
|
|
1065
1067
|
errorCode = 'invalid_model'
|
|
1066
1068
|
errorType = 'invalid_request_error'
|
|
@@ -1808,7 +1810,7 @@ export function createRouterRuntimeForTest({ config, port = 0, logger = null, to
|
|
|
1808
1810
|
})
|
|
1809
1811
|
}
|
|
1810
1812
|
|
|
1811
|
-
function ensureRouterConfigForDaemon(config) {
|
|
1813
|
+
function ensureRouterConfigForDaemon(config, skipSave = false) {
|
|
1812
1814
|
// 📖 Always rebuild from favorites or defaults — no more manual set management
|
|
1813
1815
|
const favSet = buildRouterSetFromFavorites(config)
|
|
1814
1816
|
const activeSet = favSet || buildDefaultRouterSet(config)
|
|
@@ -1819,7 +1821,7 @@ function ensureRouterConfigForDaemon(config) {
|
|
|
1819
1821
|
activeSet: activeSet.name,
|
|
1820
1822
|
sets: { [activeSet.name]: activeSet },
|
|
1821
1823
|
})
|
|
1822
|
-
saveConfig(config)
|
|
1824
|
+
if (!skipSave) saveConfig(config)
|
|
1823
1825
|
return config.router
|
|
1824
1826
|
}
|
|
1825
1827
|
|
package/src/router-dashboard.js
CHANGED
|
@@ -43,9 +43,10 @@ import chalk from 'chalk'
|
|
|
43
43
|
import { existsSync, readFileSync } from 'node:fs'
|
|
44
44
|
import { displayWidth, padEndDisplay, sliceOverlayLines, tintOverlayLines } from './render-helpers.js'
|
|
45
45
|
import { ROUTER_DEFAULT_PORT, ROUTER_MAX_PORT, ROUTER_PID_PATH, ROUTER_PORT_PATH, getRouterPortRange } from './router-daemon.js'
|
|
46
|
-
import { themeColors } from './theme.js'
|
|
46
|
+
import { themeColors, getTierRgb } from './theme.js'
|
|
47
47
|
import { formatTokenTotalCompact } from './token-usage-reader.js'
|
|
48
48
|
import { sendUsageTelemetry } from './telemetry.js'
|
|
49
|
+
import { getAvg, getVerdict } from './utils.js'
|
|
49
50
|
|
|
50
51
|
export const ROUTER_DASHBOARD_POLL_INTERVAL_MS = 2000
|
|
51
52
|
export const ROUTER_DASHBOARD_FETCH_TIMEOUT_MS = 1200
|
|
@@ -859,7 +860,7 @@ export function renderRouterDashboard(state, deps = {}) {
|
|
|
859
860
|
// 📖 Instead of the old "sets" system, show the user's favorites from the main
|
|
860
861
|
// 📖 table as the router fallback chain. #1 = tried first, #2 = next, etc.
|
|
861
862
|
lines.push(` ${themeColors.textBold('Router Models')} ${themeColors.dim('— your favorites, in fallback order')}`)
|
|
862
|
-
lines.push(` ${themeColors.dim('Star models with F in the main table.
|
|
863
|
+
lines.push(` ${themeColors.dim('Star models with F in the main table. Shift+↑↓ to reorder here.')}`)
|
|
863
864
|
lines.push('')
|
|
864
865
|
|
|
865
866
|
// 📖 Build the favorites list — pull from config.favorites + daemon health data
|
|
@@ -879,33 +880,84 @@ export function renderRouterDashboard(state, deps = {}) {
|
|
|
879
880
|
healthByKey.set(`${m.provider}/${m.model}`, m)
|
|
880
881
|
}
|
|
881
882
|
|
|
883
|
+
// 📖 Column headers
|
|
884
|
+
lines.push(` ${themeColors.dim(padEndDisplay('PRI', 4))} ${themeColors.dim(padEndDisplay('MODEL', 42))} ${themeColors.dim(padEndDisplay('DAEMON STATUS', 16))} ${themeColors.dim(padEndDisplay('AVG PING', 8))} ${themeColors.dim('VERDICT')}`)
|
|
885
|
+
|
|
882
886
|
for (let i = 0; i < favorites.length; i++) {
|
|
883
887
|
const favKey = favorites[i]
|
|
884
888
|
const health = healthByKey.get(favKey)
|
|
885
889
|
const isCursorRow = i === cursor
|
|
886
890
|
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
if (
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
else if (
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
else
|
|
891
|
+
let healthLabel = themeColors.dim(padEndDisplay('—', 16))
|
|
892
|
+
|
|
893
|
+
const mainResult = state.results?.find(r => `${r.providerKey}/${r.modelId}` === favKey)
|
|
894
|
+
|
|
895
|
+
if (mainResult) {
|
|
896
|
+
let statusText, statusColor
|
|
897
|
+
if (mainResult.status === 'noauth') {
|
|
898
|
+
statusText = `🔑 NO KEY`
|
|
899
|
+
statusColor = themeColors.dim
|
|
900
|
+
} else if (mainResult.status === 'auth_error') {
|
|
901
|
+
statusText = `🔐 AUTH FAIL`
|
|
902
|
+
statusColor = themeColors.errorBold
|
|
903
|
+
} else if (mainResult.status === 'pending') {
|
|
904
|
+
statusText = `⏳ PENDING`
|
|
905
|
+
statusColor = themeColors.warning
|
|
906
|
+
} else if (mainResult.status === 'up') {
|
|
907
|
+
statusText = `✅ UP`
|
|
908
|
+
statusColor = themeColors.success
|
|
909
|
+
} else if (mainResult.status === 'timeout') {
|
|
910
|
+
statusText = `⏳ TIMEOUT`
|
|
911
|
+
statusColor = themeColors.warning
|
|
912
|
+
} else if (mainResult.status === 'down') {
|
|
913
|
+
const code = mainResult.httpCode ?? 'ERR'
|
|
914
|
+
const errorEmojis = { '429': '🔥', '404': '🚫', '500': '💥', '502': '🔌', '503': '🔒', '504': '⏰' }
|
|
915
|
+
const errorLabels = { '404': '404 NOT FOUND', '410': '410 GONE', '429': '429 TRY LATER', '500': '500 ERROR' }
|
|
916
|
+
const emoji = errorEmojis[code] || '❌'
|
|
917
|
+
statusText = `${emoji} ${errorLabels[code] || code}`
|
|
918
|
+
statusColor = themeColors.error
|
|
919
|
+
} else {
|
|
920
|
+
statusText = '?'
|
|
921
|
+
statusColor = themeColors.dim
|
|
922
|
+
}
|
|
923
|
+
healthLabel = statusColor(padEndDisplay(statusText, 16))
|
|
900
924
|
} else {
|
|
901
|
-
healthLabel = themeColors.dim('⏳ Pending')
|
|
925
|
+
healthLabel = themeColors.dim(padEndDisplay('⏳ Pending', 16))
|
|
902
926
|
}
|
|
903
927
|
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
928
|
+
// 📖 Get global metrics from main table state
|
|
929
|
+
let avgPingDisplay = themeColors.dim('———')
|
|
930
|
+
let verdictDisplay = themeColors.dim('Pending ⏳')
|
|
931
|
+
|
|
932
|
+
if (mainResult) {
|
|
933
|
+
// Avg Ping
|
|
934
|
+
const avg = getAvg(mainResult)
|
|
935
|
+
if (avg !== Infinity) {
|
|
936
|
+
const str = String(avg).padEnd(4)
|
|
937
|
+
avgPingDisplay = avg < 500 ? themeColors.metricGood(str) : avg < 1500 ? themeColors.metricWarn(str) : themeColors.metricBad(str)
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// Verdict
|
|
941
|
+
const verdict = getVerdict(mainResult)
|
|
942
|
+
let verdictText, verdictColor
|
|
943
|
+
switch (verdict) {
|
|
944
|
+
case 'Perfect': verdictText = 'Perfect 🚀'; verdictColor = themeColors.successBold; break
|
|
945
|
+
case 'Normal': verdictText = 'Normal ✅'; verdictColor = themeColors.metricGood; break
|
|
946
|
+
case 'Spiky': verdictText = 'Spiky 📈'; verdictColor = (t) => chalk.bold.rgb(...getTierRgb('A+'))(t); break
|
|
947
|
+
case 'Slow': verdictText = 'Slow 🐢'; verdictColor = (t) => chalk.bold.rgb(...getTierRgb('A-'))(t); break
|
|
948
|
+
case 'Very Slow': verdictText = 'Very Slow 🐌'; verdictColor = (t) => chalk.bold.rgb(...getTierRgb('B+'))(t); break
|
|
949
|
+
case 'Overloaded': verdictText = 'Overloaded 🔥'; verdictColor = (t) => chalk.bold.rgb(...getTierRgb('B'))(t); break
|
|
950
|
+
case 'Unstable': verdictText = 'Unstable ⚠️'; verdictColor = themeColors.errorBold; break
|
|
951
|
+
case 'Not Active': verdictText = 'Not Active 👻'; verdictColor = themeColors.dim; break
|
|
952
|
+
case 'Pending': verdictText = 'Pending ⏳'; verdictColor = themeColors.dim; break
|
|
953
|
+
default: verdictText = 'Unusable 💀'; verdictColor = (t) => chalk.bold.rgb(...getTierRgb('C'))(t); break
|
|
954
|
+
}
|
|
955
|
+
verdictDisplay = verdictColor(padEndDisplay(verdictText, 14))
|
|
956
|
+
} else {
|
|
957
|
+
verdictDisplay = padEndDisplay(verdictDisplay, 14)
|
|
958
|
+
}
|
|
907
959
|
|
|
908
|
-
const rowText = ` ${padEndDisplay(priorityGlyph(i), 4)} ${padEndDisplay(favKey, 42)} ${padEndDisplay(healthLabel,
|
|
960
|
+
const rowText = ` ${padEndDisplay(priorityGlyph(i), 4)} ${padEndDisplay(favKey, 42)} ${padEndDisplay(healthLabel, 16)} ${padEndDisplay(avgPingDisplay, 8)} ${verdictDisplay}`
|
|
909
961
|
|
|
910
962
|
if (isCursorRow) {
|
|
911
963
|
lines.push(themeColors.bgCursor(rowText + ' '.repeat(Math.max(0, width - displayWidth(rowText) - 3))))
|
|
@@ -915,6 +967,31 @@ export function renderRouterDashboard(state, deps = {}) {
|
|
|
915
967
|
}
|
|
916
968
|
}
|
|
917
969
|
|
|
970
|
+
// 📖 Toggle Daemon Button
|
|
971
|
+
lines.push('')
|
|
972
|
+
const btnCursor = favorites.length
|
|
973
|
+
const isBtnCursor = cursor === btnCursor
|
|
974
|
+
const btnText = isStopped ? '▶ Start Router Daemon' : '⏹ Stop Router Daemon'
|
|
975
|
+
const btnRowText = ` [ ${btnText} ]`
|
|
976
|
+
|
|
977
|
+
if (isBtnCursor) {
|
|
978
|
+
lines.push(themeColors.bgCursor(btnRowText + ' '.repeat(Math.max(0, width - displayWidth(btnRowText) - 3))))
|
|
979
|
+
} else {
|
|
980
|
+
lines.push(btnRowText)
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// 📖 Install Endpoint Button
|
|
984
|
+
const installBtnCursor = favorites.length + 1
|
|
985
|
+
const isInstallBtnCursor = cursor === installBtnCursor
|
|
986
|
+
const installBtnText = '🔌 Install Router Endpoint to CLI Tool'
|
|
987
|
+
const installBtnRowText = ` [ ${installBtnText} ]`
|
|
988
|
+
|
|
989
|
+
if (isInstallBtnCursor) {
|
|
990
|
+
lines.push(themeColors.bgCursor(installBtnRowText + ' '.repeat(Math.max(0, width - displayWidth(installBtnRowText) - 3))))
|
|
991
|
+
} else {
|
|
992
|
+
lines.push(installBtnRowText)
|
|
993
|
+
}
|
|
994
|
+
|
|
918
995
|
lines.push('')
|
|
919
996
|
lines.push(` ${separator}`)
|
|
920
997
|
lines.push('')
|
|
@@ -958,7 +1035,7 @@ export function renderRouterDashboard(state, deps = {}) {
|
|
|
958
1035
|
// ── Error/Notice display ────────────────────────────────────────────────────
|
|
959
1036
|
if (state.routerDashboardError && isStopped) {
|
|
960
1037
|
lines.push('')
|
|
961
|
-
lines.push(` ${themeColors.dim('
|
|
1038
|
+
lines.push(` ${themeColors.dim('Press')} ${themeColors.hotkey('S')} ${themeColors.dim('to start it now.')}`)
|
|
962
1039
|
} else if (state.routerDashboardError) {
|
|
963
1040
|
lines.push('')
|
|
964
1041
|
lines.push(` ${themeColors.warning(state.routerDashboardError)}`)
|
|
@@ -971,7 +1048,7 @@ export function renderRouterDashboard(state, deps = {}) {
|
|
|
971
1048
|
|
|
972
1049
|
// ── Footer ──────────────────────────────────────────────────────────────────
|
|
973
1050
|
lines.push('')
|
|
974
|
-
lines.push(` ${themeColors.hotkey('↑↓')} ${themeColors.dim('Navigate')} ${themeColors.dim('•')} ${themeColors.hotkey('
|
|
1051
|
+
lines.push(` ${themeColors.hotkey('↑↓')} ${themeColors.dim('Navigate')} ${themeColors.dim('•')} ${themeColors.hotkey('Shift+↑↓')} ${themeColors.dim('Reorder')} ${themeColors.dim('•')} ${themeColors.hotkey('S')} ${themeColors.dim(isStopped ? 'Start daemon' : 'Stop daemon')} ${themeColors.dim('•')} ${themeColors.hotkey('I')} ${themeColors.dim(`Health check: ${probeLabel}`)} ${themeColors.dim('•')} ${themeColors.hotkey('C')} ${themeColors.dim('Clear log')} ${themeColors.dim('•')} ${themeColors.hotkey('Esc')} ${themeColors.dim('Back')}`)
|
|
975
1052
|
|
|
976
1053
|
const { visible, offset } = sliceOverlayLines(lines, state.routerDashboardScrollOffset || 0, state.terminalRows || 24)
|
|
977
1054
|
state.routerDashboardScrollOffset = offset
|