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.
@@ -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
- lines.push(` ${themeColors.accent('🚀')} ${themeColors.accentBold('free-coding-models')} ${themeColors.dim(LOCAL_VERSION ? `v${LOCAL_VERSION}` : '')}`)
810
- lines.push(` ${themeColors.textBold('🔀 FCM Router Dashboard')} ${themeColors.dim('Shift+R from main table')}`)
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(` Daemon: ${statusBadge(status, snapshot)} ${themeColors.dim('Port:')} ${themeColors.info(String(snapshot.port || state.routerDashboardPort || '—'))} ${themeColors.dim('PID:')} ${snapshot.pid || '—'} ${themeColors.dim('SSE:')} ${eventStatus}`)
813
- lines.push(` Set: ${themeColors.textBold(snapshot.activeSet)} ${themeColors.dim('Models:')} ${snapshot.activeModelCount} ${themeColors.dim('Uptime:')} ${formatRouterDuration(snapshot.uptimeSeconds)} ${themeColors.dim('Requests:')} ${snapshot.requestsRouted} ${themeColors.dim('In flight:')} ${snapshot.inFlight}`)
814
- lines.push(` Probes: ${themeColors.info(snapshot.probeMode)} ${themeColors.dim('Last probe:')} ${formatAge(snapshot.lastProbeAt)} ${themeColors.dim('Last refresh:')} ${updatedAt}`)
815
- if (state.routerDashboardError) lines.push(` ${themeColors.error(`Dashboard note: ${state.routerDashboardError}`)}`)
816
- if (state.routerDashboardEventError) lines.push(` ${themeColors.warning(`Event stream: ${state.routerDashboardEventError}`)}`)
817
- const notice = renderNotice(state.routerDashboardNotice)
818
- if (notice) lines.push(notice)
819
- if (snapshot.setCount === 0) {
820
- lines.push(` ${themeColors.warningBold('⚠ No router sets found. Press Y to install providers into FCM Router, then restart the daemon.')}`)
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
- lines.push(` ${themeColors.textBold('Model Health / Circuit Breakers')}`)
826
- if (snapshot.models.length === 0) {
827
- lines.push(` ${themeColors.dim('No model health rows available yet. Start the daemon or wait for /stats to answer.')}`)
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
- // 📖 Keycap emoji digits 1️⃣…🔟 large, colorful, instantly readable in the
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 = (priority) => {
834
- const n = Number(priority)
835
- if (Number.isFinite(n) && n >= 1 && n <= KEYCAPS.length) return KEYCAPS[n - 1]
836
- return '—'
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
- const header = ` ${padEndDisplay('#', 4)} ${padEndDisplay('Provider', 12)} ${padEndDisplay('Model', 30)} ${padEndDisplay('State', 12)} ${padEndDisplay('P95', 8)} ${padEndDisplay('Up', 5)} Score`
839
- lines.push(themeColors.dim(header))
840
- for (const model of snapshot.models.slice(0, 12)) {
841
- const latency = Number.isFinite(model.last_latency_ms) ? `${Math.round(model.last_latency_ms)}ms` : '—'
842
- const score = Number.isFinite(model.score) ? model.score.toFixed(2) : '—'
843
- const errorSuffix = model.last_error ? themeColors.dim(` ${compactText(model.last_error, 22).trimEnd()}`) : ''
844
- lines.push(
845
- ` ${padEndDisplay(priorityGlyph(model.priority), 4)} ` +
846
- `${compactText(model.provider, 12)} ` +
847
- `${compactText(model.model, 30)} ` +
848
- `${padEndDisplay(modelStateBadge(model.state), 12)} ` +
849
- `${padEndDisplay(latency, 8)} ` +
850
- `${padEndDisplay(formatPercent(model.uptime), 5)} ` +
851
- `${score}${errorSuffix}`
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
- lines.push(` ${themeColors.textBold('Token Summary')}`)
859
- lines.push(` Today: ${themeColors.info(formatTokenTotalCompact(snapshot.tokens.today.total_tokens))} tok ${themeColors.dim('Requests:')} ${snapshot.tokens.today.requests} ${themeColors.dim('Prompt/Completion:')} ${formatTokenTotalCompact(snapshot.tokens.today.prompt_tokens)} / ${formatTokenTotalCompact(snapshot.tokens.today.completion_tokens)}`)
860
- lines.push(` All-time: ${themeColors.info(formatTokenTotalCompact(snapshot.tokens.all_time.total_tokens))} tok ${themeColors.dim('Requests:')} ${snapshot.tokens.all_time.requests} ${themeColors.dim('Top today:')} ${compactText(topTokenModel(snapshot.tokens), Math.max(18, width - 58)).trimEnd()}`)
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
- lines.push('')
863
- lines.push(` ${themeColors.textBold('Live Request Log')}`)
864
- if (requestRows.length === 0) {
865
- lines.push(` ${themeColors.dim('No routed requests in the local dashboard log yet.')}`)
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
- const header = ` ${padEndDisplay('Time', 10)} ${padEndDisplay('Model', 34)} ${padEndDisplay('Status', 8)} ${padEndDisplay('Latency', 9)} ${padEndDisplay('Tokens', 9)} Detail`
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
- `${padEndDisplay(formatTokenTotalCompact(row.tokens), 9)} ` +
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
- const lastEvent = Array.isArray(state.routerDashboardEvents) && state.routerDashboardEvents[0]
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