free-coding-models 0.3.62 → 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.
@@ -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. Ctrl+↑↓ to reorder here.')}`)
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
- // 📖 Human-readable health label instead of circuit breaker jargon
888
- let healthLabel
889
- if (!isRunning) {
890
- healthLabel = themeColors.dim('—')
891
- } else if (health) {
892
- const st = safeString(health.state, 'UNKNOWN').toUpperCase()
893
- if (st === 'CLOSED') healthLabel = themeColors.success('✅ Healthy')
894
- else if (st === 'HALF_OPEN') healthLabel = themeColors.warning('⚠️ Recovering')
895
- else if (st === 'OPEN') healthLabel = themeColors.error('❌ Down')
896
- else if (st === 'AUTH_ERROR') healthLabel = themeColors.error('🔑 Auth Error')
897
- else if (st === 'STALE') healthLabel = themeColors.dim('💀 Stale')
898
- else if (st === 'UNSUPPORTED') healthLabel = themeColors.dim('✖ Unsupported')
899
- else healthLabel = themeColors.dim(`? ${st}`)
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
- const latency = health && Number.isFinite(health.last_latency_ms)
905
- ? themeColors.dim(`${Math.round(health.last_latency_ms)}ms`)
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, 18)} ${latency}`
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('Start the daemon with:')} ${themeColors.info('free-coding-models --daemon-bg')}`)
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('Ctrl+↑↓')} ${themeColors.dim('Reorder')} ${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')}`)
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