free-coding-models 0.3.56 → 0.3.58

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.
@@ -8,7 +8,7 @@
8
8
  * tool launch actions. It also keeps the live key bindings aligned with the
9
9
  * highlighted letters shown in the table headers.
10
10
  *
11
- * 📖 Key I opens the unified "Feedback, bugs & requests" overlay.
11
+ * 📖 Key I opens the changelog overlay.
12
12
  *
13
13
  * It also owns the "test key" model selection used by the Settings overlay.
14
14
  * Anonymous telemetry hooks for model launches and a few high-signal settings
@@ -291,7 +291,6 @@ export function createKeyHandler(ctx) {
291
291
  sendUsageTelemetry,
292
292
  startRecommendAnalysis,
293
293
  stopRecommendAnalysis,
294
- sendBugReport,
295
294
  stopUi,
296
295
  ping,
297
296
  getPingModel,
@@ -950,12 +949,7 @@ export function createKeyHandler(ctx) {
950
949
  state.installEndpointsResult = null
951
950
  }
952
951
 
953
- function openFeedbackOverlay() {
954
- state.feedbackOpen = true
955
- state.bugReportBuffer = ''
956
- state.bugReportStatus = 'idle'
957
- state.bugReportError = null
958
- }
952
+
959
953
 
960
954
  function openChangelogOverlay() {
961
955
  state.changelogOpen = true
@@ -1115,7 +1109,7 @@ export function createKeyHandler(ctx) {
1115
1109
  || state.installedModelsOpen
1116
1110
  || state.routerDashboardOpen
1117
1111
  || state.recommendOpen
1118
- || state.feedbackOpen
1112
+
1119
1113
  || state.helpVisible
1120
1114
  || state.changelogOpen
1121
1115
  }
@@ -1303,7 +1297,7 @@ export function createKeyHandler(ctx) {
1303
1297
  state.helpScrollOffset = 0
1304
1298
  return
1305
1299
  case 'open-changelog': return openChangelogOverlay()
1306
- case 'open-feedback': return openFeedbackOverlay()
1300
+
1307
1301
  case 'open-recommend': return openRecommendOverlay()
1308
1302
  case 'open-router-dashboard': return openRouterDashboardOverlay(state)
1309
1303
  case 'open-token-usage': return openTokenUsageOverlay()
@@ -1433,7 +1427,7 @@ export function createKeyHandler(ctx) {
1433
1427
  return
1434
1428
  }
1435
1429
 
1436
- if (!state.feedbackOpen && !state.settingsEditMode && !state.settingsAddKeyMode && key.name === 'g' && !key.ctrl && !key.meta) {
1430
+ if (!state.settingsEditMode && !state.settingsAddKeyMode && key.name === 'g' && !key.ctrl && !key.meta) {
1437
1431
  cycleGlobalTheme()
1438
1432
  return
1439
1433
  }
@@ -1889,72 +1883,7 @@ export function createKeyHandler(ctx) {
1889
1883
  return
1890
1884
  }
1891
1885
 
1892
- // 📖 Feedback overlay: intercept ALL keys while overlay is active.
1893
- // 📖 Enter → send to Discord, Esc → cancel, Backspace → delete char, printable → append to buffer.
1894
- if (state.feedbackOpen) {
1895
- if (key.ctrl && key.name === 'c') { exit(0); return }
1896
-
1897
- if (key.name === 'escape') {
1898
- // 📖 Cancel feedback — close overlay
1899
- state.feedbackOpen = false
1900
- state.bugReportBuffer = ''
1901
- state.bugReportStatus = 'idle'
1902
- state.bugReportError = null
1903
- return
1904
- }
1905
-
1906
- if (key.name === 'return') {
1907
- // 📖 Send feedback to Discord webhook
1908
- const message = state.bugReportBuffer.trim()
1909
- if (message.length > 0 && state.bugReportStatus !== 'sending') {
1910
- state.bugReportStatus = 'sending'
1911
- const result = await sendBugReport(message)
1912
- if (result.success) {
1913
- // 📖 Success — show confirmation briefly, then close overlay after 3 seconds
1914
- state.bugReportStatus = 'success'
1915
- setTimeout(() => {
1916
- state.feedbackOpen = false
1917
- state.bugReportBuffer = ''
1918
- state.bugReportStatus = 'idle'
1919
- state.bugReportError = null
1920
- }, 3000)
1921
- } else {
1922
- // 📖 Error — show error message, keep overlay open
1923
- state.bugReportStatus = 'error'
1924
- state.bugReportError = result.error || 'Unknown error'
1925
- }
1926
- }
1927
- return
1928
- }
1929
1886
 
1930
- if (key.name === 'backspace') {
1931
- // 📖 Don't allow editing while sending or after success
1932
- if (state.bugReportStatus === 'sending' || state.bugReportStatus === 'success') return
1933
- state.bugReportBuffer = state.bugReportBuffer.slice(0, -1)
1934
- // 📖 Clear error status when user starts editing again
1935
- if (state.bugReportStatus === 'error') {
1936
- state.bugReportStatus = 'idle'
1937
- state.bugReportError = null
1938
- }
1939
- return
1940
- }
1941
-
1942
- // 📖 Append printable characters (str is the raw character typed)
1943
- // 📖 Limit to 500 characters (Discord embed description limit)
1944
- if (str && str.length === 1 && !key.ctrl && !key.meta) {
1945
- // 📖 Don't allow editing while sending or after success
1946
- if (state.bugReportStatus === 'sending' || state.bugReportStatus === 'success') return
1947
- if (state.bugReportBuffer.length < 500) {
1948
- state.bugReportBuffer += str
1949
- // 📖 Clear error status when user starts editing again
1950
- if (state.bugReportStatus === 'error') {
1951
- state.bugReportStatus = 'idle'
1952
- state.bugReportError = null
1953
- }
1954
- }
1955
- }
1956
- return
1957
- }
1958
1887
 
1959
1888
  // 📖 Help overlay: full keyboard navigation + key swallowing while overlay is open.
1960
1889
  if (state.helpVisible) {
@@ -2629,7 +2558,8 @@ export function createKeyHandler(ctx) {
2629
2558
 
2630
2559
  // 📖 Profile system removed - API keys now persist permanently across all sessions
2631
2560
 
2632
- // 📖 Shift+R: open the Smart Model Router dashboard from the main table.
2561
+ // 📖 Shift+R intentionally stays unadvertised in the main UI, but remains
2562
+ // 📖 available as a tester entry point for the Router Dashboard.
2633
2563
  if (key.name === 'r' && key.shift && !key.ctrl && !key.meta) {
2634
2564
  openRouterDashboardOverlay(state)
2635
2565
  return
@@ -2670,11 +2600,7 @@ export function createKeyHandler(ctx) {
2670
2600
  return
2671
2601
  }
2672
2602
 
2673
- // 📖 I key: open Feedback overlay (anonymous Discord feedback)
2674
- if (key.name === 'i') {
2675
- openFeedbackOverlay()
2676
- return
2677
- }
2603
+
2678
2604
 
2679
2605
  // 📖 W cycles the supported ping modes:
2680
2606
  // 📖 speed (2s) → normal (10s) → slow (30s) → forced (4s) → speed.
@@ -2686,17 +2612,7 @@ export function createKeyHandler(ctx) {
2686
2612
  return
2687
2613
  }
2688
2614
 
2689
- // 📖 Ctrl+O: toggle footer visibility (collapse to single hint when hidden)
2690
- if (key.ctrl && key.name === 'o' && !key.meta) {
2691
- state.footerHidden = !state.footerHidden
2692
- if (!state.config.settings || typeof state.config.settings !== 'object') state.config.settings = {}
2693
- state.config.settings.footerHidden = state.footerHidden
2694
- saveConfig(state.config)
2695
- state.frame++ // 📖 Force immediate re-render
2696
- return
2697
- }
2698
-
2699
- // 📖 E toggles hiding models whose provider has no configured API key.
2615
+ // 📖 E toggles "Show only configured & working models": hides models whose provider has no configured API key, or whose health status is noauth/auth_error (but keeps timeout and 429).
2700
2616
  // 📖 The preference is saved globally.
2701
2617
  if (key.name === 'e') {
2702
2618
  state.hideUnconfiguredModels = !state.hideUnconfiguredModels
@@ -2746,8 +2662,8 @@ export function createKeyHandler(ctx) {
2746
2662
  return
2747
2663
  }
2748
2664
 
2749
- // 📖 Help overlay key: Ctrl+H = toggle help overlay
2750
- if (key.ctrl && key.name === 'h') {
2665
+ // 📖 Help overlay key: I = toggle help overlay
2666
+ if (key.name === 'i') {
2751
2667
  state.helpVisible = !state.helpVisible
2752
2668
  if (state.helpVisible) state.helpScrollOffset = 0
2753
2669
  return
@@ -3002,10 +2918,7 @@ export function createMouseEventHandler(ctx) {
3002
2918
  }
3003
2919
  return
3004
2920
  }
3005
- if (state.feedbackOpen) {
3006
- // 📖 Feedback overlay doesn't scroll — ignore
3007
- return
3008
- }
2921
+
3009
2922
  if (state.commandPaletteOpen) {
3010
2923
  // 📖 Command palette: scroll the results list
3011
2924
  const count = state.commandPaletteResults?.length || 0
@@ -3147,12 +3060,7 @@ export function createMouseEventHandler(ctx) {
3147
3060
  return
3148
3061
  }
3149
3062
 
3150
- if (state.feedbackOpen) {
3151
- // 📖 Feedback overlay: click anywhere closes (no scroll, no cursor)
3152
- state.feedbackOpen = false
3153
- state.feedbackInput = ''
3154
- return
3155
- }
3063
+
3156
3064
 
3157
3065
  if (state.helpVisible) {
3158
3066
  // 📖 Help overlay: click anywhere closes (same as K or Escape)
package/src/overlays.js CHANGED
@@ -4,15 +4,13 @@
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, Router Dashboard
7
+ * - Settings, Install Endpoints, Command Palette, Help, Smart Recommend, 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
  *
11
11
  * The factory pattern keeps stateful UI logic isolated while still
12
12
  * allowing the main CLI to control shared state and dependencies.
13
13
  *
14
- * 📖 Feedback overlay (I key) combines feature requests + bug reports in one left-aligned input
15
- *
16
14
  * → Functions:
17
15
  * - `createOverlayRenderers` — returns renderer + analysis helpers + overlayLayout
18
16
  * - `renderRouterDashboard` — mounts the Smart Model Router dashboard renderer
@@ -299,12 +297,16 @@ export function createOverlayRenderers(state, deps) {
299
297
  }
300
298
  lines.push('')
301
299
 
302
- // 📖 Footer with credits
300
+ // 📖 Footer with credits + community links — Discord and Buy me a coffee
301
+ // 📖 live here (and in the onboarding) instead of the main TUI footer to
302
+ // 📖 keep the table chrome lean.
303
303
  lines.push('')
304
304
  lines.push(
305
305
  themeColors.dim(' ') +
306
306
  themeColors.footerLove('Made with 💖 & ☕ by ') +
307
307
  themeColors.link('\x1b]8;;https://github.com/vava-nessa\x1b\\vava-nessa\x1b]8;;\x1b\\') +
308
+ themeColors.dim(' • 💬 ') +
309
+ themeColors.footerDiscord('\x1b]8;;https://discord.gg/ZTNFHvvCkU\x1b\\Join the Discord\x1b]8;;\x1b\\') +
308
310
  themeColors.dim(' • ☕ ') +
309
311
  themeColors.footerCoffee('\x1b]8;;https://buymeacoffee.com/vavanessadev\x1b\\Buy me a coffee\x1b]8;;\x1b\\') +
310
312
  themeColors.dim(' • ') +
@@ -868,8 +870,9 @@ export function createOverlayRenderers(state, deps) {
868
870
  // 📖 Branding header
869
871
  lines.push(` ${themeColors.accent('🚀')} ${themeColors.accentBold('free-coding-models')} ${themeColors.dim(`v${LOCAL_VERSION}`)}`)
870
872
  lines.push(` ${heading('❓ Help & Keyboard Shortcuts')}`)
873
+ lines.push(` ${themeColors.successBold('🔑 Yellow = active key')}`)
871
874
  lines.push('')
872
- lines.push(` ${hint('— ↑↓ / PgUp / PgDn / Home / End scroll • K or Esc close')}`)
875
+ lines.push(` ${hint('— ↑↓ / PgUp / PgDn / Home / End scroll • K or ')}${themeColors.successBold('Esc close')}`)
873
876
  lines.push(` ${heading('Columns')}`)
874
877
  lines.push('')
875
878
  lines.push(` ${label('Rank')} SWE-bench rank (1 = best coding score) ${hint('Sort:')} ${key('R')}`)
@@ -884,7 +887,7 @@ export function createOverlayRenderers(state, deps) {
884
887
  lines.push(` ${label('CTX')} Context window size (128k, 200k, 256k, 1m, etc.) ${hint('Sort:')} ${key('C')}`)
885
888
  lines.push(` ${hint('Bigger context = the model can read more of your codebase at once without forgetting.')}`)
886
889
  lines.push('')
887
- lines.push(` ${label('Model')} Model name (①②③ = favorite order) ${hint('Sort:')} ${key('M')} ${hint('Favorite:')} ${key('F')}`)
890
+ lines.push(` ${label('Model')} Model name (1️⃣2️⃣3️⃣ = favorite order) ${hint('Sort:')} ${key('M')} ${hint('Favorite:')} ${key('F')}`)
888
891
  lines.push(` ${hint('Star the ones you like. Press Y to switch between pinned mode and normal filter/sort mode.')}`)
889
892
  lines.push('')
890
893
  lines.push(` ${label('Provider')} Provider source (NIM, Groq, Cerebras, etc.) ${hint('Sort:')} ${key('O')} ${hint('Cycle:')} ${key('D')}`)
@@ -925,19 +928,18 @@ export function createOverlayRenderers(state, deps) {
925
928
  lines.push(` ${key('Ctrl+P')} Open ⚡️ command palette ${hint('(search and run actions quickly)')}`)
926
929
  lines.push(` ${key('E')} Toggle configured models only ${hint('(enabled by default)')}`)
927
930
  lines.push(` ${key('Z')} Cycle tool mode ${hint('(📦 OpenCode → π Pi → 🪼 jcode → 📦 Desktop → 🦞 OpenClaw → 💘 Crush → 🪿 Goose → 🛠 Aider → 🐉 Qwen → 🤲 OpenHands → ⚡ Amp → 🦘 Rovo → ♊ Gemini)')}`)
928
- lines.push(` ${key('F')} Toggle favorite on selected row ${hint('(①②③ = router fallback order)')}`)
931
+ lines.push(` ${key('F')} Toggle favorite on selected row ${hint('(1️⃣2️⃣3️⃣ = router fallback order, capped at 🔟)')}`)
929
932
  lines.push(` ${key('⇧↑/⇧↓')} Reorder selected favorite up/down ${hint('(changes router priority)')}`)
930
933
  lines.push(` ${key('Y')} Toggle favorites mode ${hint('(Pinned + always visible ↔ Normal filter/sort behavior)')}`)
931
934
  lines.push(` ${key('X')} Clear active text filter ${hint('(remove custom query applied from ⚡️ Command Palette)')}`)
932
935
  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)')}`)
934
936
  lines.push(` ${key('G')} Cycle theme ${hint('(auto → dark → light)')}`)
935
- lines.push(` ${themeColors.errorBold('I')} Feedback, bugs & requests ${hint('(📝 send anonymous feedback, bug reports, or feature requests)')}`)
937
+
936
938
  lines.push(` ${key('P')} Open settings ${hint('(manage API keys, provider toggles, updates, legacy cleanup)')}`)
937
939
  // 📖 Profile system removed - API keys now persist permanently across all sessions
938
940
  lines.push(` ${key('Ctrl+P')} Reset view settings ${hint('(search "Reset view" in the command palette)')}`)
939
941
  lines.push(` ${key('N')} Changelog ${hint('(📋 browse all versions, Enter to view details)')}`)
940
- lines.push(` ${key('Ctrl+H')} / ${key('Esc')} Show/hide this help`)
942
+ lines.push(` ${key('I')} / ${key('Esc')} Show/hide this help`)
941
943
  lines.push(` ${key('Ctrl+C')} Exit`)
942
944
  lines.push('')
943
945
  lines.push(` ${heading('Settings (P)')}`)
@@ -1170,92 +1172,6 @@ export function createOverlayRenderers(state, deps) {
1170
1172
  }, PING_RATE)
1171
1173
  }
1172
1174
 
1173
- // ─── Feedback overlay renderer ────────────────────────────────────────────
1174
- // 📖 renderFeedback: Draw the overlay for anonymous Discord feedback.
1175
- // 📖 Shows an input field where users can type feedback, bug reports, or any comments.
1176
- function renderFeedback() {
1177
- const EL = '\x1b[K'
1178
- const lines = []
1179
-
1180
- // 📖 Calculate available space for multi-line input (dynamic based on terminal width)
1181
- const maxInputWidth = state.terminalCols - 8 // 8 = padding (4 spaces each side)
1182
- const maxInputLines = 10 // Show up to 10 lines of input
1183
-
1184
- // 📖 Split buffer into lines for display (with wrapping)
1185
- const wrapText = (text, width) => {
1186
- const words = text.split(' ')
1187
- const lines = []
1188
- let currentLine = ''
1189
-
1190
- for (const word of words) {
1191
- const testLine = currentLine ? currentLine + ' ' + word : word
1192
- if (testLine.length <= width) {
1193
- currentLine = testLine
1194
- } else {
1195
- if (currentLine) lines.push(currentLine)
1196
- currentLine = word
1197
- }
1198
- }
1199
- if (currentLine) lines.push(currentLine)
1200
- return lines
1201
- }
1202
-
1203
- const inputLines = wrapText(state.bugReportBuffer, maxInputWidth)
1204
- const displayLines = inputLines.slice(0, maxInputLines)
1205
-
1206
- // 📖 Branding header
1207
- lines.push('')
1208
- lines.push(` ${themeColors.accent('🚀')} ${themeColors.accentBold('free-coding-models')} ${themeColors.dim(`v${LOCAL_VERSION}`)}`)
1209
- lines.push(` ${themeColors.successBold('📝 Feedback, bugs & requests')}`)
1210
- lines.push('')
1211
- lines.push(themeColors.dim(" — don't hesitate to send us feedback, bug reports, or just your feeling about the app"))
1212
- lines.push('')
1213
-
1214
- // 📖 Status messages (if any)
1215
- if (state.bugReportStatus === 'sending') {
1216
- lines.push(` ${themeColors.warning('⏳ Sending...')}`)
1217
- lines.push('')
1218
- } else if (state.bugReportStatus === 'success') {
1219
- lines.push(` ${themeColors.successBold('✅ Successfully sent!')} ${themeColors.dim('Closing overlay in 3 seconds...')}`)
1220
- lines.push('')
1221
- lines.push(` ${themeColors.dim('Thank you for your feedback! It has been sent to the project team.')}`)
1222
- lines.push('')
1223
- } else if (state.bugReportStatus === 'error') {
1224
- lines.push(` ${themeColors.error('❌ Error:')} ${themeColors.warning(state.bugReportError || 'Failed to send')}`)
1225
- lines.push(` ${themeColors.dim('Press Backspace to edit, or Esc to close')}`)
1226
- lines.push('')
1227
- } else {
1228
- lines.push(` ${themeColors.dim('Type your feedback below. Press Enter to send, Esc to cancel.')}`)
1229
- lines.push(` ${themeColors.dim('Your message will be sent anonymously to the project team.')}`)
1230
- lines.push('')
1231
- }
1232
-
1233
- // 📖 Simple input area – left-aligned, framed by horizontal lines
1234
- lines.push(` ${themeColors.info('Message')} (${state.bugReportBuffer.length}/500 chars)`)
1235
- lines.push(` ${themeColors.dim('─'.repeat(maxInputWidth))}`)
1236
- // 📖 Input lines — left-aligned, or placeholder when empty
1237
- if (displayLines.length > 0) {
1238
- for (const line of displayLines) {
1239
- lines.push(` ${line}`)
1240
- }
1241
- // 📖 Show cursor on last line
1242
- if (state.bugReportStatus === 'idle' || state.bugReportStatus === 'error') {
1243
- lines[lines.length - 1] += themeColors.accentBold('▏')
1244
- }
1245
- } else {
1246
- const placeholderBR = state.bugReportStatus === 'idle' ? chalk.italic.rgb(...getProviderRgb('googleai'))('Type your message here...') : ''
1247
- lines.push(` ${placeholderBR}${themeColors.accentBold('▏')}`)
1248
- }
1249
- lines.push(` ${themeColors.dim('─'.repeat(maxInputWidth))}`)
1250
- lines.push('')
1251
- lines.push(themeColors.dim(' Enter Send • Esc Cancel • Backspace Delete'))
1252
-
1253
- // 📖 Apply overlay tint and return
1254
- const tintedLines = tintOverlayLines(lines, themeColors.overlayBgFeedback, state.terminalCols)
1255
- const cleared = tintedLines.map(l => l + EL)
1256
- return cleared.join('\n')
1257
- }
1258
-
1259
1175
  // ─── Changelog overlay renderer ───────────────────────────────────────────
1260
1176
  // 📖 renderChangelog: Two-phase overlay — index of all versions or details of one version
1261
1177
  function renderChangelog() {
@@ -1606,7 +1522,7 @@ export function createOverlayRenderers(state, deps) {
1606
1522
  lines.push(` ${totalRow.join(' ')}`)
1607
1523
 
1608
1524
  lines.push('')
1609
- lines.push(themeColors.dim(' Esc Back to main table • Shift+R Router Dashboard'))
1525
+ lines.push(themeColors.dim(' Esc Back to main table'))
1610
1526
 
1611
1527
  const { visible, offset } = sliceOverlayLines(lines, state.tokenUsageScrollOffset, state.terminalRows)
1612
1528
  state.tokenUsageScrollOffset = offset
@@ -1641,7 +1557,7 @@ export function createOverlayRenderers(state, deps) {
1641
1557
  lines.push(themeColors.info(' Enabling router, please wait...'))
1642
1558
  } else if (state.routerOnboardingPhase === 'success') {
1643
1559
  lines.push(themeColors.success(' ✅ Router enabled! Dashboard opening...'))
1644
- lines.push(themeColors.dim(' Shift+R to reopen the dashboard anytime'))
1560
+ lines.push(themeColors.dim(' Setup complete. Return to the main table to continue.'))
1645
1561
  } else if (state.routerOnboardingPhase === 'error') {
1646
1562
  lines.push(themeColors.error(` ❌ ${state.routerOnboardingError || 'Failed to enable router'}`))
1647
1563
  lines.push(themeColors.dim(' Press Esc or Enter to continue to the main table'))
@@ -1657,6 +1573,12 @@ export function createOverlayRenderers(state, deps) {
1657
1573
  lines.push('')
1658
1574
  }
1659
1575
  lines.push(themeColors.dim(' ↑↓ Navigate • Enter Select • Esc Skip for now'))
1576
+ lines.push('')
1577
+ lines.push(
1578
+ themeColors.dim(' 💬 ') +
1579
+ themeColors.footerDiscord('\x1b]8;;https://discord.gg/ZTNFHvvCkU\x1b\\Join the Discord community\x1b]8;;\x1b\\') +
1580
+ themeColors.dim(' • Get help, share feedback, follow updates')
1581
+ )
1660
1582
  }
1661
1583
 
1662
1584
  const targetLine = cursorLineByRow[state.routerOnboardingCursor] ?? 0
@@ -1667,26 +1589,6 @@ export function createOverlayRenderers(state, deps) {
1667
1589
  return tintedLines.map((l) => l + EL).join('\n')
1668
1590
  }
1669
1591
 
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
-
1690
1592
  return {
1691
1593
  renderSettings,
1692
1594
  renderInstallEndpoints,
@@ -1694,14 +1596,12 @@ export function createOverlayRenderers(state, deps) {
1694
1596
  renderCommandPalette,
1695
1597
  renderHelp,
1696
1598
  renderRecommend,
1697
- renderFeedback,
1698
1599
  renderChangelog,
1699
1600
  renderInstalledModels,
1700
1601
  renderRouterDashboard,
1701
1602
  renderIncompatibleFallback,
1702
1603
  renderTokenUsage,
1703
1604
  renderRouterOnboarding,
1704
- renderRouterUpgradeBanner,
1705
1605
  startRecommendAnalysis,
1706
1606
  stopRecommendAnalysis,
1707
1607
  overlayLayout,
@@ -116,7 +116,7 @@ export const PROVIDER_METADATA = {
116
116
  rateLimits: 'Quota depends on GitHub/Copilot tier; no separate provider billing',
117
117
  },
118
118
  mistral: {
119
- label: 'Mistral La Plateforme',
119
+ label: 'Mistral LP',
120
120
  color: chalk.rgb(255, 196, 120),
121
121
  signupUrl: 'https://console.mistral.ai/api-keys',
122
122
  signupHint: 'La Plateforme → API keys (MISTRAL_API_KEY)',
@@ -47,7 +47,7 @@
47
47
  */
48
48
 
49
49
  import chalk from 'chalk'
50
- import { OVERLAY_PANEL_WIDTH, TABLE_FIXED_LINES } from './constants.js'
50
+ import { OVERLAY_PANEL_WIDTH, TABLE_FIXED_LINES, TABLE_HEADER_LINES, TABLE_FOOTER_LINES } from './constants.js'
51
51
  import { sortResults } from './utils.js'
52
52
 
53
53
  // 📖 stripAnsi: Remove ANSI color/control sequences to estimate visible text width before padding.
@@ -65,13 +65,32 @@ export function maskApiKey(key) {
65
65
 
66
66
  // 📖 displayWidth: Calculate display width of a string in terminal columns.
67
67
  // 📖 Emojis and other wide characters occupy 2 columns, variation selectors (U+FE0F) are zero-width.
68
+ // 📖 Keycap sequences (digit/# + FE0F + 20E3, e.g. 1️⃣) render as a single 2-cell glyph.
68
69
  // 📖 This avoids pulling in a full `string-width` dependency for a lightweight CLI tool.
69
70
  export function displayWidth(str) {
70
71
  const plain = stripAnsi(String(str))
72
+ const codepoints = [...plain]
71
73
  let w = 0
72
- for (const ch of plain) {
74
+ for (let i = 0; i < codepoints.length; i++) {
75
+ const ch = codepoints[i]
73
76
  const cp = ch.codePointAt(0)
74
- // Zero-width: variation selectors (FE00-FE0F), zero-width joiner/non-joiner, combining marks
77
+
78
+ // Keycap sequence detection: ASCII digit / # / * followed by optional FE0F then 20E3 → +2 (single emoji glyph)
79
+ const isKeycapBase = (cp >= 0x30 && cp <= 0x39) || cp === 0x23 || cp === 0x2A
80
+ if (isKeycapBase) {
81
+ let j = i + 1
82
+ let sawFe0f = false
83
+ if (j < codepoints.length && codepoints[j].codePointAt(0) === 0xFE0F) { sawFe0f = true; j++ }
84
+ if (j < codepoints.length && codepoints[j].codePointAt(0) === 0x20E3) {
85
+ w += 2
86
+ i = j // 📖 skip the consumed FE0F (if any) and the 20E3
87
+ continue
88
+ }
89
+ // 📖 Not a keycap, fall through to normal handling
90
+ void sawFe0f
91
+ }
92
+
93
+ // Zero-width: variation selectors (FE00-FE0F), zero-width joiner/non-joiner, lone combining keycap
75
94
  if ((cp >= 0xFE00 && cp <= 0xFE0F) || cp === 0x200D || cp === 0x200C || cp === 0x20E3) continue
76
95
  // Wide: CJK, emoji (most above U+1F000), fullwidth forms
77
96
  if (
@@ -146,14 +165,24 @@ export function sliceOverlayLines(lines, offset, terminalRows) {
146
165
 
147
166
  // ─── Table viewport calculation ────────────────────────────────────────────────
148
167
 
168
+ // 📖 getTableFixedLines: Resolve the non-model line budget for the main table.
169
+ // 📖 Header and full footer are always visible in the main table, with optional
170
+ // 📖 extra fixed rows for temporary banners.
171
+ export function getTableFixedLines({ extraFixedLines = 0 } = {}) {
172
+ return TABLE_HEADER_LINES + TABLE_FOOTER_LINES + Math.max(0, extraFixedLines)
173
+ }
174
+
149
175
  // 📖 calculateViewport: Computes the visible slice of model rows that fits in the terminal.
150
176
  // 📖 When scroll indicators are needed, they each consume 1 line from the model budget.
151
- // 📖 `extraFixedLines` lets callers reserve temporary footer rows without shrinking the
152
- // 📖 viewport permanently for the normal case.
177
+ // 📖 `lineBudget` lets callers reserve temporary footer/header rows without shrinking
178
+ // 📖 the viewport permanently for the normal case.
153
179
  // 📖 Returns { startIdx, endIdx, hasAbove, hasBelow } for rendering.
154
- export function calculateViewport(terminalRows, scrollOffset, totalModels, extraFixedLines = 0) {
180
+ export function calculateViewport(terminalRows, scrollOffset, totalModels, lineBudget = 0) {
155
181
  if (terminalRows <= 0) return { startIdx: 0, endIdx: totalModels, hasAbove: false, hasBelow: false }
156
- let maxSlots = terminalRows - TABLE_FIXED_LINES - extraFixedLines
182
+ const fixedLines = typeof lineBudget === 'number'
183
+ ? TABLE_FIXED_LINES + Math.max(0, lineBudget)
184
+ : getTableFixedLines(lineBudget)
185
+ let maxSlots = terminalRows - fixedLines
157
186
  if (maxSlots < 1) maxSlots = 1
158
187
  if (totalModels <= maxSlots) return { startIdx: 0, endIdx: totalModels, hasAbove: false, hasBelow: false }
159
188
 
@@ -206,7 +235,8 @@ export function sortResultsWithPinnedFavorites(results, sortColumn, sortDirectio
206
235
  // 📖 Modifies st.scrollOffset in-place, returns undefined.
207
236
  export function adjustScrollOffset(st) {
208
237
  const total = st.visibleSorted ? st.visibleSorted.length : st.results.filter(r => !r.hidden).length
209
- let maxSlots = st.terminalRows - TABLE_FIXED_LINES
238
+ const fixedLines = getTableFixedLines()
239
+ let maxSlots = st.terminalRows - fixedLines
210
240
  if (maxSlots < 1) maxSlots = 1
211
241
  if (total <= maxSlots) { st.scrollOffset = 0; return }
212
242
  // Ensure cursor is not above the visible window