free-coding-models 0.3.60 → 0.3.63
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 +15 -5
- package/package.json +1 -1
- package/src/app.js +14 -2
- package/src/endpoint-installer.js +17 -0
- package/src/favorites.js +22 -1
- package/src/key-handler.js +143 -23
- package/src/overlays.js +6 -4
- package/src/render-table.js +5 -3
- package/src/router-daemon.js +11 -9
- package/src/router-dashboard.js +214 -59
- package/web/dist/assets/{index-BGUOjKTu.js → index-B_N6UmGP.js} +1 -1
- package/web/dist/index.html +1 -1
package/src/router-dashboard.js
CHANGED
|
@@ -39,12 +39,14 @@
|
|
|
39
39
|
* @see ./key-handler.js — dashboard key bindings
|
|
40
40
|
*/
|
|
41
41
|
|
|
42
|
+
import chalk from 'chalk'
|
|
42
43
|
import { existsSync, readFileSync } from 'node:fs'
|
|
43
44
|
import { displayWidth, padEndDisplay, sliceOverlayLines, tintOverlayLines } from './render-helpers.js'
|
|
44
45
|
import { ROUTER_DEFAULT_PORT, ROUTER_MAX_PORT, ROUTER_PID_PATH, ROUTER_PORT_PATH, getRouterPortRange } from './router-daemon.js'
|
|
45
|
-
import { themeColors } from './theme.js'
|
|
46
|
+
import { themeColors, getTierRgb } from './theme.js'
|
|
46
47
|
import { formatTokenTotalCompact } from './token-usage-reader.js'
|
|
47
48
|
import { sendUsageTelemetry } from './telemetry.js'
|
|
49
|
+
import { getAvg, getVerdict } from './utils.js'
|
|
48
50
|
|
|
49
51
|
export const ROUTER_DASHBOARD_POLL_INTERVAL_MS = 2000
|
|
50
52
|
export const ROUTER_DASHBOARD_FETCH_TIMEOUT_MS = 1200
|
|
@@ -505,6 +507,7 @@ export function startRouterDashboardEventStream(state, options = {}) {
|
|
|
505
507
|
export function openRouterDashboardOverlay(state) {
|
|
506
508
|
state.routerDashboardOpen = true
|
|
507
509
|
state.routerDashboardScrollOffset = 0
|
|
510
|
+
state.routerDashboardCursorIndex = 0
|
|
508
511
|
state.routerDashboardStatus = state.routerDashboardStatus || 'loading'
|
|
509
512
|
startRouterDashboardPolling(state)
|
|
510
513
|
// 📖 Fire app_router_install on first Shift+R dashboard open for upgrade-path users
|
|
@@ -521,6 +524,7 @@ export function openRouterDashboardOverlay(state) {
|
|
|
521
524
|
export function closeRouterDashboardOverlay(state) {
|
|
522
525
|
state.routerDashboardOpen = false
|
|
523
526
|
state.routerDashboardScrollOffset = 0
|
|
527
|
+
state.routerDashboardCursorIndex = 0
|
|
524
528
|
stopRouterDashboardClient(state)
|
|
525
529
|
}
|
|
526
530
|
|
|
@@ -800,73 +804,209 @@ export function renderRouterDashboard(state, deps = {}) {
|
|
|
800
804
|
const status = state.routerDashboardStatus || 'idle'
|
|
801
805
|
const width = Math.max(80, state.terminalCols || 80)
|
|
802
806
|
const separator = themeColors.dim('─'.repeat(Math.max(20, width - 6)))
|
|
803
|
-
const requestRows = requestLogRows(state, snapshot)
|
|
804
|
-
const eventStatus = state.routerDashboardEventStatus || 'idle'
|
|
805
|
-
const updatedAt = state.routerDashboardLastUpdatedAt
|
|
806
|
-
? new Date(state.routerDashboardLastUpdatedAt).toLocaleTimeString()
|
|
807
|
-
: 'never'
|
|
808
807
|
|
|
809
|
-
|
|
810
|
-
|
|
808
|
+
// 📖 Loading animation frames for the "starting" state — reuses the same
|
|
809
|
+
// 📖 visual language as the ping indicators in the main table.
|
|
810
|
+
const LOADING_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
|
|
811
|
+
const loadingGlyph = LOADING_FRAMES[(state.frame || 0) % LOADING_FRAMES.length]
|
|
812
|
+
|
|
813
|
+
// ── Big Status Banner ───────────────────────────────────────────────────────
|
|
814
|
+
// 📖 Visual hierarchy: the daemon status is THE most important info, shown as
|
|
815
|
+
// 📖 a large colored block that users can understand at a single glance.
|
|
816
|
+
const bannerWidth = Math.max(40, width - 6)
|
|
817
|
+
const isRunning = status === 'ready' || status === 'partial'
|
|
818
|
+
const isLoading = status === 'loading'
|
|
819
|
+
const isStopped = !isRunning && !isLoading
|
|
820
|
+
|
|
821
|
+
let bannerText, bannerBgRgb, bannerFgRgb
|
|
822
|
+
if (isRunning) {
|
|
823
|
+
bannerText = ' ROUTER DAEMON RUNNING '
|
|
824
|
+
bannerBgRgb = [22, 120, 60] // green
|
|
825
|
+
bannerFgRgb = [255, 255, 255]
|
|
826
|
+
} else if (isLoading) {
|
|
827
|
+
bannerText = ` ROUTER DAEMON STARTING ${loadingGlyph} `
|
|
828
|
+
bannerBgRgb = [180, 100, 0] // orange
|
|
829
|
+
bannerFgRgb = [255, 255, 255]
|
|
830
|
+
} else {
|
|
831
|
+
bannerText = ' ROUTER DAEMON STOPPED '
|
|
832
|
+
bannerBgRgb = [160, 30, 30] // red
|
|
833
|
+
bannerFgRgb = [255, 255, 255]
|
|
834
|
+
}
|
|
835
|
+
// 📖 Center the text inside a full-width colored bar
|
|
836
|
+
const bannerPadTotal = Math.max(0, bannerWidth - displayWidth(bannerText))
|
|
837
|
+
const bannerPadLeft = Math.floor(bannerPadTotal / 2)
|
|
838
|
+
const bannerPadRight = bannerPadTotal - bannerPadLeft
|
|
839
|
+
const bannerLine = ' '.repeat(bannerPadLeft) + bannerText + ' '.repeat(bannerPadRight)
|
|
840
|
+
const paintBanner = chalk.bgRgb(...bannerBgRgb).rgb(...bannerFgRgb).bold
|
|
841
|
+
|
|
811
842
|
lines.push('')
|
|
812
|
-
lines.push(`
|
|
813
|
-
lines.push(
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
const
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
843
|
+
lines.push(` ${paintBanner(bannerLine)}`)
|
|
844
|
+
lines.push('')
|
|
845
|
+
|
|
846
|
+
// ── Quick Setup (connection info) ───────────────────────────────────────────
|
|
847
|
+
const port = snapshot.port || state.routerDashboardPort || '—'
|
|
848
|
+
const baseUrl = isRunning ? `http://localhost:${port}/v1` : `http://localhost:${port}/v1`
|
|
849
|
+
lines.push(` ${themeColors.textBold('Quick Setup')} ${themeColors.dim('— paste into your coding tool')}`)
|
|
850
|
+
lines.push(` ${themeColors.dim('URL')} ${themeColors.info(baseUrl)}`)
|
|
851
|
+
lines.push(` ${themeColors.dim('Model')} ${themeColors.info('fcm')}`)
|
|
852
|
+
lines.push(` ${themeColors.dim('API Key')} ${themeColors.info('fcm-local')}`)
|
|
853
|
+
if (isRunning) {
|
|
854
|
+
lines.push(` ${themeColors.dim('Uptime')} ${themeColors.success(formatRouterDuration(snapshot.uptimeSeconds))} ${themeColors.dim('Requests routed:')} ${themeColors.info(String(snapshot.requestsRouted))}`)
|
|
821
855
|
}
|
|
822
856
|
lines.push(` ${separator}`)
|
|
823
857
|
lines.push('')
|
|
824
858
|
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
859
|
+
// ── Favorites / Router Fallback Models ──────────────────────────────────────
|
|
860
|
+
// 📖 Instead of the old "sets" system, show the user's favorites from the main
|
|
861
|
+
// 📖 table as the router fallback chain. #1 = tried first, #2 = next, etc.
|
|
862
|
+
lines.push(` ${themeColors.textBold('Router Models')} ${themeColors.dim('— your favorites, in fallback order')}`)
|
|
863
|
+
lines.push(` ${themeColors.dim('Star models with F in the main table. Shift+↑↓ to reorder here.')}`)
|
|
864
|
+
lines.push('')
|
|
865
|
+
|
|
866
|
+
// 📖 Build the favorites list — pull from config.favorites + daemon health data
|
|
867
|
+
const favorites = Array.isArray(state.config?.favorites) ? state.config.favorites : []
|
|
868
|
+
const cursor = state.routerDashboardCursorIndex ?? 0
|
|
869
|
+
|
|
870
|
+
if (favorites.length === 0) {
|
|
871
|
+
lines.push(` ${themeColors.warning('No favorites yet.')} ${themeColors.dim('Press Esc, then F on any model to add it.')}`)
|
|
828
872
|
} else {
|
|
829
|
-
// 📖
|
|
830
|
-
// 📖 router order column. Capped at 10 because the active set should never
|
|
831
|
-
// 📖 contain more than 10 priority slots in practice.
|
|
873
|
+
// 📖 Priority keycap glyphs for the fallback order
|
|
832
874
|
const KEYCAPS = ['1️⃣','2️⃣','3️⃣','4️⃣','5️⃣','6️⃣','7️⃣','8️⃣','9️⃣','🔟']
|
|
833
|
-
const priorityGlyph = (
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
875
|
+
const priorityGlyph = (i) => i < KEYCAPS.length ? KEYCAPS[i] : `${i + 1}.`
|
|
876
|
+
|
|
877
|
+
// 📖 Match favorites to daemon health data for live status
|
|
878
|
+
const healthByKey = new Map()
|
|
879
|
+
for (const m of snapshot.models) {
|
|
880
|
+
healthByKey.set(`${m.provider}/${m.model}`, m)
|
|
837
881
|
}
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
const
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
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
|
+
|
|
886
|
+
for (let i = 0; i < favorites.length; i++) {
|
|
887
|
+
const favKey = favorites[i]
|
|
888
|
+
const health = healthByKey.get(favKey)
|
|
889
|
+
const isCursorRow = i === cursor
|
|
890
|
+
|
|
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))
|
|
924
|
+
} else {
|
|
925
|
+
healthLabel = themeColors.dim(padEndDisplay('⏳ Pending', 16))
|
|
926
|
+
}
|
|
927
|
+
|
|
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
|
+
}
|
|
959
|
+
|
|
960
|
+
const rowText = ` ${padEndDisplay(priorityGlyph(i), 4)} ${padEndDisplay(favKey, 42)} ${padEndDisplay(healthLabel, 16)} ${padEndDisplay(avgPingDisplay, 8)} ${verdictDisplay}`
|
|
961
|
+
|
|
962
|
+
if (isCursorRow) {
|
|
963
|
+
lines.push(themeColors.bgCursor(rowText + ' '.repeat(Math.max(0, width - displayWidth(rowText) - 3))))
|
|
964
|
+
} else {
|
|
965
|
+
lines.push(rowText)
|
|
966
|
+
}
|
|
853
967
|
}
|
|
854
|
-
if (snapshot.models.length > 12) lines.push(themeColors.dim(` … ${snapshot.models.length - 12} more models in active set`))
|
|
855
968
|
}
|
|
856
969
|
|
|
970
|
+
// 📖 Toggle Daemon Button
|
|
857
971
|
lines.push('')
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
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
|
+
}
|
|
861
982
|
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
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))))
|
|
866
991
|
} else {
|
|
867
|
-
|
|
992
|
+
lines.push(installBtnRowText)
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
lines.push('')
|
|
996
|
+
lines.push(` ${separator}`)
|
|
997
|
+
lines.push('')
|
|
998
|
+
|
|
999
|
+
// ── Token Summary (compact) ─────────────────────────────────────────────────
|
|
1000
|
+
lines.push(` ${themeColors.textBold('Tokens')} ${themeColors.dim('Today:')} ${themeColors.info(formatTokenTotalCompact(snapshot.tokens.today.total_tokens))} ${themeColors.dim('All-time:')} ${themeColors.info(formatTokenTotalCompact(snapshot.tokens.all_time.total_tokens))} ${themeColors.dim('Requests:')} ${snapshot.tokens.today.requests}/${snapshot.tokens.all_time.requests}`)
|
|
1001
|
+
|
|
1002
|
+
// ── Live Request Log (compact) ──────────────────────────────────────────────
|
|
1003
|
+
const requestRows = requestLogRows(state, snapshot)
|
|
1004
|
+
if (requestRows.length > 0) {
|
|
1005
|
+
lines.push('')
|
|
1006
|
+
lines.push(` ${themeColors.textBold('Recent Requests')}`)
|
|
1007
|
+
const header = ` ${padEndDisplay('Time', 10)} ${padEndDisplay('Model', 34)} ${padEndDisplay('Status', 8)} ${padEndDisplay('Latency', 9)} Detail`
|
|
868
1008
|
lines.push(themeColors.dim(header))
|
|
869
|
-
for (const row of requestRows) {
|
|
1009
|
+
for (const row of requestRows.slice(0, 6)) {
|
|
870
1010
|
const atMs = Date.parse(row.at)
|
|
871
1011
|
const time = Number.isFinite(atMs) ? new Date(atMs).toLocaleTimeString() : '—'
|
|
872
1012
|
const statusText = String(row.status)
|
|
@@ -882,18 +1022,33 @@ export function renderRouterDashboard(state, deps = {}) {
|
|
|
882
1022
|
`${compactText(row.model, 34)} ` +
|
|
883
1023
|
`${padEndDisplay(statusColor(statusText), 8)} ` +
|
|
884
1024
|
`${padEndDisplay(latency, 9)} ` +
|
|
885
|
-
`${
|
|
886
|
-
`${compactText(detail, Math.max(10, width - 78)).trimEnd()}`
|
|
1025
|
+
`${compactText(detail, Math.max(10, width - 68)).trimEnd()}`
|
|
887
1026
|
)
|
|
888
1027
|
}
|
|
889
1028
|
}
|
|
890
1029
|
|
|
1030
|
+
// ── Health check speed ──────────────────────────────────────────────────────
|
|
1031
|
+
const probeLabel = snapshot.probeMode === 'eco' ? 'Slow'
|
|
1032
|
+
: snapshot.probeMode === 'aggressive' ? 'Fast'
|
|
1033
|
+
: 'Normal'
|
|
1034
|
+
|
|
1035
|
+
// ── Error/Notice display ────────────────────────────────────────────────────
|
|
1036
|
+
if (state.routerDashboardError && isStopped) {
|
|
1037
|
+
lines.push('')
|
|
1038
|
+
lines.push(` ${themeColors.dim('Press')} ${themeColors.hotkey('S')} ${themeColors.dim('to start it now.')}`)
|
|
1039
|
+
} else if (state.routerDashboardError) {
|
|
1040
|
+
lines.push('')
|
|
1041
|
+
lines.push(` ${themeColors.warning(state.routerDashboardError)}`)
|
|
1042
|
+
}
|
|
1043
|
+
const notice = renderNotice(state.routerDashboardNotice)
|
|
1044
|
+
if (notice) {
|
|
1045
|
+
lines.push('')
|
|
1046
|
+
lines.push(notice)
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
// ── Footer ──────────────────────────────────────────────────────────────────
|
|
891
1050
|
lines.push('')
|
|
892
|
-
|
|
893
|
-
? `${state.routerDashboardEvents[0].event} @ ${new Date(state.routerDashboardEvents[0].at).toLocaleTimeString()}`
|
|
894
|
-
: 'none'
|
|
895
|
-
lines.push(` ${themeColors.dim(`Endpoint: ${state.routerDashboardBaseUrl || 'not connected'} • Last SSE event: ${lastEvent}`)}`)
|
|
896
|
-
lines.push(` ${themeColors.hotkey('S')} ${themeColors.dim('Switch set')} ${themeColors.dim('•')} ${themeColors.hotkey('I')} ${themeColors.dim('Probe mode')} ${themeColors.dim('•')} ${themeColors.hotkey('R')} ${themeColors.dim('Restart (Phase 7)')} ${themeColors.dim('•')} ${themeColors.hotkey('C')} ${themeColors.dim('Clear log')} ${themeColors.dim('•')} ${themeColors.hotkey('P')} ${themeColors.dim('Pause probes (disabled)')} ${themeColors.dim('•')} ${themeColors.hotkey('Esc')} ${themeColors.dim('Back')}`)
|
|
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')}`)
|
|
897
1052
|
|
|
898
1053
|
const { visible, offset } = sliceOverlayLines(lines, state.routerDashboardScrollOffset || 0, state.terminalRows || 24)
|
|
899
1054
|
state.routerDashboardScrollOffset = offset
|