free-coding-models 0.3.11 → 0.3.13

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/src/overlays.js CHANGED
@@ -4,28 +4,24 @@
4
4
  *
5
5
  * @details
6
6
  * This module centralizes all overlay rendering in one place:
7
- * - Settings, Install Endpoints, Help, Log, Smart Recommend, Feedback, Changelog
8
- * - FCM Proxy V2 overlay with current-tool auto-sync toggle and cleanup
7
+ * - Settings, Install Endpoints, Help, Smart Recommend, Feedback, Changelog
9
8
  * - Settings diagnostics for provider key tests, including wrapped retry/error details
10
9
  * - Recommend analysis timer orchestration and progress updates
11
10
  *
12
11
  * The factory pattern keeps stateful UI logic isolated while still
13
12
  * allowing the main CLI to control shared state and dependencies.
14
13
  *
15
- * 📖 The proxy overlay rows are: Enable → Auto-sync current tool → Port → Cleanup → Install/Restart/Stop/Kill/Logs
16
14
  * 📖 Feedback overlay (I key) combines feature requests + bug reports in one left-aligned input
17
15
  *
18
16
  * → Functions:
19
17
  * - `createOverlayRenderers` — returns renderer + analysis helpers
20
18
  *
21
19
  * @exports { createOverlayRenderers }
22
- * @see ./proxy-sync.js — resolveProxySyncToolMode powers current-tool proxy sync hints
23
20
  * @see ./key-handler.js — handles keypresses for all overlay interactions
24
21
  */
25
22
 
26
23
  import { loadChangelog } from './changelog-loader.js'
27
24
  import { buildCliHelpLines } from './cli-help.js'
28
- import { resolveProxySyncToolMode } from './proxy-sync.js'
29
25
 
30
26
  export function createOverlayRenderers(state, deps) {
31
27
  const {
@@ -35,19 +31,16 @@ export function createOverlayRenderers(state, deps) {
35
31
  PROVIDER_COLOR,
36
32
  LOCAL_VERSION,
37
33
  getApiKey,
38
- getProxySettings,
39
34
  resolveApiKeys,
40
35
  isProviderEnabled,
41
36
  TIER_CYCLE,
42
37
  SETTINGS_OVERLAY_BG,
43
38
  HELP_OVERLAY_BG,
44
39
  RECOMMEND_OVERLAY_BG,
45
- LOG_OVERLAY_BG,
46
40
  OVERLAY_PANEL_WIDTH,
47
41
  keepOverlayTargetVisible,
48
42
  sliceOverlayLines,
49
43
  tintOverlayLines,
50
- loadRecentLogs,
51
44
  TASK_TYPES,
52
45
  PRIORITY_TYPES,
53
46
  CONTEXT_BUDGETS,
@@ -62,7 +55,6 @@ export function createOverlayRenderers(state, deps) {
62
55
  getConfiguredInstallableProviders,
63
56
  getInstallTargetModes,
64
57
  getProviderCatalogModels,
65
- CONNECTION_MODES,
66
58
  getToolMeta,
67
59
  } = deps
68
60
 
@@ -89,54 +81,6 @@ export function createOverlayRenderers(state, deps) {
89
81
  return lines
90
82
  }
91
83
 
92
- // 📖 Keep log token formatting aligned with the main table so the same totals
93
- // 📖 read the same everywhere in the TUI.
94
- const formatLogTokens = (totalTokens) => {
95
- const safeTotal = Number(totalTokens) || 0
96
- if (safeTotal <= 0) return '--'
97
- if (safeTotal >= 999_500) return `${(safeTotal / 1_000_000).toFixed(2)}M`
98
- if (safeTotal >= 1_000) return `${(safeTotal / 1_000).toFixed(2)}k`
99
- return String(Math.floor(safeTotal))
100
- }
101
-
102
- // 📖 Colorize latency with gradient: green (<500ms) → orange (<1000ms) → yellow (<1500ms) → red (>=1500ms)
103
- const colorizeLatency = (latency, text) => {
104
- const ms = Number(latency) || 0
105
- if (ms <= 0) return chalk.dim(text)
106
- if (ms < 500) return chalk.greenBright(text)
107
- if (ms < 1000) return chalk.rgb(255, 165, 0)(text) // Orange
108
- if (ms < 1500) return chalk.yellow(text)
109
- return chalk.red(text)
110
- }
111
-
112
- // 📖 Colorize tokens with gradient: dim green (few) → bright green (many)
113
- const colorizeTokens = (tokens, text) => {
114
- const tok = Number(tokens) || 0
115
- if (tok <= 0) return chalk.dim(text)
116
- // Gradient: light green (low) → medium green → bright green (high, >30k)
117
- if (tok < 10_000) return chalk.hex('#90EE90')(text) // Light green
118
- if (tok < 30_000) return chalk.hex('#32CD32')(text) // Lime green
119
- return chalk.greenBright(text) // Full brightness green
120
- }
121
-
122
- // 📖 Get model color based on status code - distinct colors for each error type
123
- const getModelColorByStatus = (status) => {
124
- const sc = String(status)
125
- if (sc === '200') return chalk.greenBright // Success - bright green
126
- if (sc === '404') return chalk.rgb(139, 0, 0) // Not found - dark red
127
- if (sc === '400') return chalk.hex('#8B008B') // Bad request - dark magenta
128
- if (sc === '401') return chalk.hex('#9932CC') // Unauthorized - dark orchid
129
- if (sc === '403') return chalk.hex('#BA55D3') // Forbidden - medium orchid
130
- if (sc === '413') return chalk.hex('#FF6347') // Payload too large - tomato red
131
- if (sc === '429') return chalk.hex('#FFB90F') // Rate limit - dark orange
132
- if (sc === '500') return chalk.hex('#DC143C') // Internal server error - crimson
133
- if (sc === '502') return chalk.hex('#C71585') // Bad gateway - medium violet red
134
- if (sc === '503') return chalk.hex('#9370DB') // Service unavailable - medium purple
135
- if (sc.startsWith('5')) return chalk.magenta // Other 5xx - magenta
136
- if (sc === '0') return chalk.hex('#696969') // Timeout/error - dim gray
137
- return chalk.white // Unknown - white
138
- }
139
-
140
84
  // ─── Settings screen renderer ─────────────────────────────────────────────
141
85
  // 📖 renderSettings: Draw the settings overlay in the alt screen buffer.
142
86
  // 📖 Shows all providers with their API key (masked) + enabled state.
@@ -144,11 +88,10 @@ export function createOverlayRenderers(state, deps) {
144
88
  // 📖 Key "T" in settings = test API key for selected provider.
145
89
  function renderSettings() {
146
90
  const providerKeys = Object.keys(sources)
147
- const updateRowIdx = providerKeys.length
148
- const widthWarningRowIdx = updateRowIdx + 1
149
- const proxyDaemonRowIdx = widthWarningRowIdx + 1
150
- const changelogViewRowIdx = proxyDaemonRowIdx + 1
151
- const proxySettings = getProxySettings(state.config)
91
+ const updateRowIdx = providerKeys.length
92
+ const widthWarningRowIdx = updateRowIdx + 1
93
+ const cleanupLegacyProxyRowIdx = widthWarningRowIdx + 1
94
+ const changelogViewRowIdx = cleanupLegacyProxyRowIdx + 1
152
95
  const EL = '\x1b[K'
153
96
  const lines = []
154
97
  const cursorLineByRow = {}
@@ -274,34 +217,24 @@ const updateRowIdx = providerKeys.length
274
217
  const updateRow = `${updateBullet}${chalk.bold(updateActionLabel).padEnd(44)} ${updateStatus}`
275
218
  cursorLineByRow[updateRowIdx] = lines.length
276
219
  lines.push(updateCursor ? chalk.bgRgb(30, 30, 60)(updateRow) : updateRow)
277
- // 📖 Widths Warning toggle row (disable widths warning)
220
+ // 📖 Width warning visibility row for the startup narrow-terminal overlay.
278
221
  const disableWidthsWarning = Boolean(state.config.settings?.disableWidthsWarning)
279
222
  const widthWarningBullet = state.settingsCursor === widthWarningRowIdx ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
280
- const widthWarningStatus = disableWidthsWarning ? chalk.greenBright('DISABLED') : chalk.dim('enabled')
281
- const widthWarningRow = `${widthWarningBullet}${chalk.bold('Disable Widths Warning').padEnd(44)} ${widthWarningStatus}`
223
+ const widthWarningStatus = disableWidthsWarning
224
+ ? chalk.redBright('🙈 Disabled')
225
+ : chalk.greenBright('👁 Enabled')
226
+ const widthWarningRow = `${widthWarningBullet}${chalk.bold('Small Width Warnings').padEnd(44)} ${widthWarningStatus}`
282
227
  cursorLineByRow[widthWarningRowIdx] = lines.length
283
228
  lines.push(state.settingsCursor === widthWarningRowIdx ? chalk.bgRgb(30, 30, 60)(widthWarningRow) : widthWarningRow)
284
229
  if (updateState === 'error' && state.settingsUpdateError) {
285
230
  lines.push(chalk.red(` ${state.settingsUpdateError}`))
286
231
  }
287
232
 
288
- // 📖 FCM Proxy V2 single row that opens a dedicated overlay
289
- lines.push('')
290
- lines.push(` ${chalk.bold('📡 FCM Proxy V2')}`)
291
- lines.push(` ${chalk.dim(' ' + '─'.repeat(separatorWidth))}`)
292
- lines.push('')
293
-
294
- const proxyDaemonBullet = state.settingsCursor === proxyDaemonRowIdx ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
295
- const proxyStatus = proxySettings.enabled ? chalk.greenBright('Proxy ON') : chalk.dim('Proxy OFF')
296
- const daemonStatus = state.daemonStatus || 'not-installed'
297
- let daemonBadge
298
- if (daemonStatus === 'running') daemonBadge = chalk.greenBright('Service ON')
299
- else if (daemonStatus === 'stopped') daemonBadge = chalk.yellow('Service stopped')
300
- else if (daemonStatus === 'stale' || daemonStatus === 'unhealthy') daemonBadge = chalk.red('Service ' + daemonStatus)
301
- else daemonBadge = chalk.dim('Service OFF')
302
- const proxyDaemonRow = `${proxyDaemonBullet}${chalk.bold('FCM Proxy V2 settings →').padEnd(44)} ${proxyStatus} ${chalk.dim('•')} ${daemonBadge}`
303
- cursorLineByRow[proxyDaemonRowIdx] = lines.length
304
- lines.push(state.settingsCursor === proxyDaemonRowIdx ? chalk.bgRgb(20, 45, 60)(proxyDaemonRow) : proxyDaemonRow)
233
+ // 📖 Cleanup row removes stale proxy-era config left behind by older builds.
234
+ const cleanupLegacyProxyBullet = state.settingsCursor === cleanupLegacyProxyRowIdx ? chalk.bold.cyan('') : chalk.dim(' ')
235
+ const cleanupLegacyProxyRow = `${cleanupLegacyProxyBullet}${chalk.bold('Clean Legacy Proxy Config').padEnd(44)} ${chalk.magentaBright('Enter remove discontinued bridge leftovers')}`
236
+ cursorLineByRow[cleanupLegacyProxyRowIdx] = lines.length
237
+ lines.push(state.settingsCursor === cleanupLegacyProxyRowIdx ? chalk.bgRgb(55, 25, 55)(cleanupLegacyProxyRow) : cleanupLegacyProxyRow)
305
238
 
306
239
  // 📖 Changelog viewer row
307
240
  const changelogViewBullet = state.settingsCursor === changelogViewRowIdx ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
@@ -314,10 +247,8 @@ const updateRowIdx = providerKeys.length
314
247
  lines.push('')
315
248
  if (state.settingsEditMode) {
316
249
  lines.push(chalk.dim(' Type API key • Enter Save • Esc Cancel'))
317
- } else if (state.settingsProxyPortEditMode) {
318
- lines.push(chalk.dim(' Type proxy port (0 = auto) • Enter Save • Esc Cancel'))
319
250
  } else {
320
- lines.push(chalk.dim(' ↑↓ Navigate • Enter Edit/Run • + Add key • - Remove key • Space Toggle • T Test key • S Sync→OpenCode • R Restore backup • U Updates • Esc Close'))
251
+ lines.push(chalk.dim(' ↑↓ Navigate • Enter Edit/Run • + Add key • - Remove key • Space Toggle • T Test key • U Updates • Esc Close'))
321
252
  }
322
253
  // 📖 Show sync/restore status message if set
323
254
  if (state.settingsSyncStatus) {
@@ -355,7 +286,7 @@ const updateRowIdx = providerKeys.length
355
286
  }
356
287
 
357
288
  // ─── Install Endpoints overlay renderer ───────────────────────────────────
358
- // 📖 renderInstallEndpoints drives the provider → tool → connection → scope → model flow
289
+ // 📖 renderInstallEndpoints drives the provider → tool → scope → model flow
359
290
  // 📖 behind the `Y` hotkey. It deliberately reuses the same overlay viewport
360
291
  // 📖 helpers as Settings so long provider/model lists stay navigable.
361
292
  function renderInstallEndpoints() {
@@ -364,8 +295,7 @@ const updateRowIdx = providerKeys.length
364
295
  const cursorLineByRow = {}
365
296
  const providerChoices = getConfiguredInstallableProviders(state.config)
366
297
  const toolChoices = getInstallTargetModes()
367
- const connectionChoices = CONNECTION_MODES || []
368
- const totalSteps = 5
298
+ const totalSteps = 4
369
299
  const scopeChoices = [
370
300
  {
371
301
  key: 'all',
@@ -391,11 +321,7 @@ const updateRowIdx = providerKeys.length
391
321
  })()
392
322
  : '—'
393
323
 
394
- const selectedConnectionLabel = state.installEndpointsConnectionMode === 'proxy'
395
- ? 'FCM Proxy V2'
396
- : state.installEndpointsConnectionMode === 'direct'
397
- ? 'Direct Provider'
398
- : '—'
324
+ const selectedConnectionLabel = 'Direct Provider'
399
325
 
400
326
  lines.push('')
401
327
  // 📖 Branding header
@@ -439,7 +365,7 @@ const updateRowIdx = providerKeys.length
439
365
  const label = `${meta.emoji} ${meta.label}`
440
366
  const note = toolMode.startsWith('opencode')
441
367
  ? chalk.dim('shared config file')
442
- : ['claude-code', 'codex', 'openhands'].includes(toolMode)
368
+ : toolMode === 'openhands'
443
369
  ? chalk.dim('env file (~/.fcm-*-env)')
444
370
  : chalk.dim('managed config install')
445
371
  const bullet = isCursor ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
@@ -450,26 +376,8 @@ const updateRowIdx = providerKeys.length
450
376
 
451
377
  lines.push('')
452
378
  lines.push(chalk.dim(' ↑↓ Navigate • Enter Choose tool • Esc Back'))
453
- } else if (state.installEndpointsPhase === 'connection') {
454
- // 📖 Step 3: Choose connection mode — Direct Provider vs FCM Proxy
455
- lines.push(` ${chalk.bold(`Step 3/${totalSteps}`)} ${chalk.cyan('Choose connection mode')}`)
456
- lines.push(chalk.dim(` Provider: ${selectedProviderLabel} • Tool: ${selectedToolLabel}`))
457
- lines.push('')
458
-
459
- connectionChoices.forEach((mode, idx) => {
460
- const isCursor = idx === state.installEndpointsCursor
461
- const bullet = isCursor ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
462
- const icon = mode.key === 'proxy' ? '🔄' : '⚡'
463
- const row = `${bullet}${icon} ${chalk.bold(mode.label)}`
464
- cursorLineByRow[idx] = lines.length
465
- lines.push(isCursor ? chalk.bgRgb(24, 44, 62)(row) : row)
466
- lines.push(chalk.dim(` ${mode.hint}`))
467
- lines.push('')
468
- })
469
-
470
- lines.push(chalk.dim(' Enter Continue • Esc Back'))
471
379
  } else if (state.installEndpointsPhase === 'scope') {
472
- lines.push(` ${chalk.bold(`Step 4/${totalSteps}`)} ${chalk.cyan('Choose the install scope')}`)
380
+ lines.push(` ${chalk.bold(`Step 3/${totalSteps}`)} ${chalk.cyan('Choose the install scope')}`)
473
381
  lines.push(chalk.dim(` Provider: ${selectedProviderLabel} • Tool: ${selectedToolLabel} • ${selectedConnectionLabel}`))
474
382
  lines.push('')
475
383
 
@@ -488,7 +396,7 @@ const updateRowIdx = providerKeys.length
488
396
  const models = getProviderCatalogModels(state.installEndpointsProviderKey)
489
397
  const selectedCount = state.installEndpointsSelectedModelIds.size
490
398
 
491
- lines.push(` ${chalk.bold(`Step 5/${totalSteps}`)} ${chalk.cyan('Choose which models to install')}`)
399
+ lines.push(` ${chalk.bold(`Step 4/${totalSteps}`)} ${chalk.cyan('Choose which models to install')}`)
492
400
  lines.push(chalk.dim(` Provider: ${selectedProviderLabel} • Tool: ${selectedToolLabel} • ${selectedConnectionLabel}`))
493
401
  lines.push(chalk.dim(` Selected: ${selectedCount}/${models.length}`))
494
402
  lines.push('')
@@ -590,8 +498,8 @@ const updateRowIdx = providerKeys.length
590
498
  lines.push(` ${chalk.cyan('Up%')} Uptime — ratio of successful pings to total pings ${chalk.dim('Sort:')} ${chalk.yellow('U')}`)
591
499
  lines.push(` ${chalk.dim('If a model only works half the time, you\'ll waste time retrying. Higher = more reliable.')}`)
592
500
  lines.push('')
593
- lines.push(` ${chalk.cyan('Used')} Total prompt+completion tokens consumed in logs for this exact provider/model pair`)
594
- lines.push(` ${chalk.dim('Loaded once at startup from request-log.jsonl. Displayed in K tokens, or M tokens above one million.')}`)
501
+ lines.push(` ${chalk.cyan('Used')} Historical prompt+completion tokens tracked for this exact provider/model pair`)
502
+ lines.push(` ${chalk.dim('Loaded from local stats snapshots. Displayed in K tokens, or M tokens above one million.')}`)
595
503
  lines.push('')
596
504
 
597
505
 
@@ -604,14 +512,12 @@ const updateRowIdx = providerKeys.length
604
512
  lines.push(` ${chalk.bold('Controls')}`)
605
513
  lines.push(` ${chalk.yellow('W')} Toggle ping mode ${chalk.dim('(speed 2s → normal 10s → slow 30s → forced 4s)')}`)
606
514
  lines.push(` ${chalk.yellow('E')} Toggle configured models only ${chalk.dim('(enabled by default)')}`)
607
- lines.push(` ${chalk.yellow('X')} Toggle token log page ${chalk.dim('(shows recent request usage from request-log.jsonl)')}`)
608
- lines.push(` ${chalk.yellow('Z')} Cycle tool mode ${chalk.dim('(OpenCode → Desktop → OpenClaw → Crush → Goose → Pi → Aider → Claude Code → Codex → Gemini → Qwen → OpenHands → Amp)')}`)
515
+ lines.push(` ${chalk.yellow('Z')} Cycle tool mode ${chalk.dim('(OpenCode Desktop OpenClaw → Crush → Goose → Pi → Aider → Qwen → OpenHands → Amp)')}`)
609
516
  lines.push(` ${chalk.yellow('F')} Toggle favorite on selected row ${chalk.dim('(⭐ pinned at top, persisted)')}`)
610
- lines.push(` ${chalk.yellow('Y')} Install endpoints ${chalk.dim('(provider catalog → compatible tools, Direct or FCM Proxy V2)')}`)
517
+ lines.push(` ${chalk.yellow('Y')} Install endpoints ${chalk.dim('(provider catalog → compatible tools, direct provider only)')}`)
611
518
  lines.push(` ${chalk.yellow('Q')} Smart Recommend ${chalk.dim('(🎯 find the best model for your task — questionnaire + live analysis)')}`)
612
519
  lines.push(` ${chalk.rgb(255, 87, 51).bold('I')} Feedback, bugs & requests ${chalk.dim('(📝 send anonymous feedback, bug reports, or feature requests)')}`)
613
- lines.push(` ${chalk.yellow('J')} FCM Proxy V2 settings ${chalk.dim('(📡 open proxy configuration and background service management)')}`)
614
- lines.push(` ${chalk.yellow('P')} Open settings ${chalk.dim('(manage API keys, provider toggles, proxy, manual update)')}`)
520
+ lines.push(` ${chalk.yellow('P')} Open settings ${chalk.dim('(manage API keys, provider toggles, updates, legacy cleanup)')}`)
615
521
  // 📖 Profile system removed - API keys now persist permanently across all sessions
616
522
  lines.push(` ${chalk.yellow('Shift+R')} Reset view settings ${chalk.dim('(tier filter, sort, provider filter → defaults)')}`)
617
523
  lines.push(` ${chalk.yellow('N')} Changelog ${chalk.dim('(📋 browse all versions, Enter to view details)')}`)
@@ -622,7 +528,7 @@ const updateRowIdx = providerKeys.length
622
528
  lines.push(` ${chalk.yellow('↑↓')} Navigate rows`)
623
529
  lines.push(` ${chalk.yellow('PgUp/PgDn')} Jump by page`)
624
530
  lines.push(` ${chalk.yellow('Home/End')} Jump first/last row`)
625
- lines.push(` ${chalk.yellow('Enter')} Edit key / check-install update`)
531
+ lines.push(` ${chalk.yellow('Enter')} Edit key / run selected maintenance action`)
626
532
  lines.push(` ${chalk.yellow('Space')} Toggle provider enable/disable`)
627
533
  lines.push(` ${chalk.yellow('T')} Test selected provider key`)
628
534
  lines.push(` ${chalk.yellow('U')} Check updates manually`)
@@ -638,173 +544,6 @@ const updateRowIdx = providerKeys.length
638
544
  return cleared.join('\n')
639
545
  }
640
546
 
641
- // ─── Log page overlay renderer ────────────────────────────────────────────
642
- // 📖 renderLog: Draw the log page overlay showing recent requests from
643
- // 📖 ~/.free-coding-models/request-log.jsonl, newest-first.
644
- // 📖 Toggled with X key. Esc or X closes.
645
- function renderLog() {
646
- const EL = '\x1b[K'
647
- const lines = []
648
-
649
- // 📖 Branding header
650
- lines.push(` ${chalk.cyanBright('🚀')} ${chalk.bold.cyanBright('free-coding-models')} ${chalk.dim(`v${LOCAL_VERSION}`)}`)
651
- lines.push(` ${chalk.bold('📋 Request Log')}`)
652
- lines.push('')
653
- lines.push(chalk.dim(' — recent requests • ↑↓ scroll • A toggle all/500 • X or Esc close'))
654
- lines.push(chalk.dim(' Works only when the multi-account proxy is enabled and requests go through it.'))
655
- lines.push(chalk.dim(' Direct provider launches do not currently write into this log.'))
656
-
657
- // 📖 Load recent log entries — bounded read, newest-first, malformed lines skipped.
658
- // 📖 Show up to 500 entries by default, or all if logShowAll is true.
659
- const logLimit = state.logShowAll ? Number.MAX_SAFE_INTEGER : 500
660
- const logRows = loadRecentLogs({ limit: logLimit })
661
- const totalTokens = logRows.reduce((sum, row) => sum + (Number(row.tokens) || 0), 0)
662
-
663
- if (logRows.length === 0) {
664
- lines.push(chalk.dim(' No log entries found.'))
665
- lines.push(chalk.dim(' Logs are written to ~/.free-coding-models/request-log.jsonl'))
666
- lines.push(chalk.dim(' when requests are proxied through the multi-account rotation proxy.'))
667
- lines.push(chalk.dim(' Direct provider launches do not currently feed this token log.'))
668
- } else {
669
- lines.push(` ${chalk.bold('Total Consumed:')} ${chalk.greenBright(formatLogTokens(totalTokens))}`)
670
- lines.push('')
671
- // 📖 Column widths for the log table
672
- const W_TIME = 19
673
- const W_PROV = 14
674
- const W_MODEL = 44
675
- const W_ROUTE = 18
676
- const W_STATUS = 8
677
- const W_TOKENS = 12
678
- const W_LAT = 10
679
-
680
- // 📖 Header row
681
- const hTime = chalk.dim('Time'.padEnd(W_TIME))
682
- const hProv = chalk.dim('Provider'.padEnd(W_PROV))
683
- const hModel = chalk.dim('Model'.padEnd(W_MODEL))
684
- const hRoute = chalk.dim('Route'.padEnd(W_ROUTE))
685
- const hStatus = chalk.dim('Status'.padEnd(W_STATUS))
686
- const hTok = chalk.dim('Tokens Used'.padEnd(W_TOKENS))
687
- const hLat = chalk.dim('Latency'.padEnd(W_LAT))
688
-
689
- // 📖 Show mode indicator (all vs limited)
690
- const modeBadge = state.logShowAll
691
- ? chalk.yellow.bold('ALL')
692
- : chalk.cyan.bold('500')
693
- const countBadge = chalk.dim(`Showing ${logRows.length} entries`)
694
-
695
- lines.push(` ${hTime} ${hProv} ${hModel} ${hRoute} ${hStatus} ${hTok} ${hLat}`)
696
- lines.push(` ${chalk.dim('─'.repeat(W_TIME + W_PROV + W_MODEL + W_ROUTE + W_STATUS + W_TOKENS + W_LAT + 12))} ${modeBadge} ${countBadge}`)
697
-
698
- for (const row of logRows) {
699
- // 📖 Format time as HH:MM:SS (strip the date part for compactness)
700
- let timeStr = row.time
701
- try {
702
- const d = new Date(row.time)
703
- if (!Number.isNaN(d.getTime())) {
704
- timeStr = d.toISOString().replace('T', ' ').slice(0, 19)
705
- }
706
- } catch { /* keep raw */ }
707
-
708
- const requestedModelLabel = row.requestedModel || ''
709
- // 📖 Always show "requested → actual" if they differ, not just when switched
710
- const displayModel = requestedModelLabel && requestedModelLabel !== row.model
711
- ? `${requestedModelLabel} → ${row.model}`
712
- : row.model
713
-
714
- // 📖 Color-code status with distinct colors for each error type
715
- let statusCell
716
- const sc = String(row.status)
717
- if (sc === '200') {
718
- statusCell = chalk.greenBright(sc.padEnd(W_STATUS))
719
- } else if (sc === '404') {
720
- statusCell = chalk.rgb(139, 0, 0).bold(sc.padEnd(W_STATUS)) // Dark red for 404
721
- } else if (sc === '400') {
722
- statusCell = chalk.hex('#8B008B').bold(sc.padEnd(W_STATUS)) // Dark magenta
723
- } else if (sc === '401') {
724
- statusCell = chalk.hex('#9932CC').bold(sc.padEnd(W_STATUS)) // Dark orchid
725
- } else if (sc === '403') {
726
- statusCell = chalk.hex('#BA55D3').bold(sc.padEnd(W_STATUS)) // Medium orchid
727
- } else if (sc === '413') {
728
- statusCell = chalk.hex('#FF6347').bold(sc.padEnd(W_STATUS)) // Tomato red
729
- } else if (sc === '429') {
730
- statusCell = chalk.hex('#FFB90F').bold(sc.padEnd(W_STATUS)) // Dark orange
731
- } else if (sc === '500') {
732
- statusCell = chalk.hex('#DC143C').bold(sc.padEnd(W_STATUS)) // Crimson
733
- } else if (sc === '502') {
734
- statusCell = chalk.hex('#C71585').bold(sc.padEnd(W_STATUS)) // Medium violet red
735
- } else if (sc === '503') {
736
- statusCell = chalk.hex('#9370DB').bold(sc.padEnd(W_STATUS)) // Medium purple
737
- } else if (sc.startsWith('5')) {
738
- statusCell = chalk.magenta(sc.padEnd(W_STATUS)) // Other 5xx - magenta
739
- } else if (sc === '0') {
740
- statusCell = chalk.hex('#696969')(sc.padEnd(W_STATUS)) // Dim gray for timeout
741
- } else {
742
- statusCell = chalk.dim(sc.padEnd(W_STATUS))
743
- }
744
-
745
- const tokStr = formatLogTokens(row.tokens)
746
- const latStr = row.latency > 0 ? `${row.latency}ms` : '--'
747
- const routeLabel = row.switched
748
- ? `SWITCHED ↻ ${row.switchReason || 'fallback'}`
749
- : 'direct'
750
-
751
- // 📖 Detect failed requests with zero tokens - these get special red highlighting
752
- const isFailedWithZeroTokens = row.status !== '200' && (!row.tokens || Number(row.tokens) === 0)
753
-
754
- const timeCell = chalk.dim(timeStr.slice(0, W_TIME).padEnd(W_TIME))
755
- // 📖 Provider display: Use pretty label if available, otherwise raw key.
756
- // 📖 All these logs are from FCM Proxy V2.
757
- const providerLabel = PROVIDER_METADATA[row.provider]?.label || row.provider
758
- const providerRgb = PROVIDER_COLOR[row.provider] ?? [105, 190, 245]
759
- const provCell = chalk.bold.rgb(...providerRgb)(providerLabel.slice(0, W_PROV).padEnd(W_PROV))
760
-
761
- // 📖 Color model based on status - red for failed requests with zero tokens
762
- let modelCell
763
- if (isFailedWithZeroTokens) {
764
- modelCell = chalk.red.bold(displayModel.slice(0, W_MODEL).padEnd(W_MODEL))
765
- } else {
766
- const modelColorFn = getModelColorByStatus(row.status)
767
- modelCell = row.switched
768
- ? chalk.bold.rgb(255, 210, 90)(displayModel.slice(0, W_MODEL).padEnd(W_MODEL))
769
- : modelColorFn(displayModel.slice(0, W_MODEL).padEnd(W_MODEL))
770
- }
771
-
772
- const routeCell = row.switched
773
- ? chalk.bgRgb(120, 25, 25).yellow.bold(` ${routeLabel.slice(0, W_ROUTE - 2).padEnd(W_ROUTE - 2)} `)
774
- : chalk.dim(routeLabel.padEnd(W_ROUTE))
775
-
776
- // 📖 Colorize tokens - red cross emoji for failed requests with zero tokens
777
- let tokCell
778
- if (isFailedWithZeroTokens) {
779
- tokCell = chalk.red.bold('✗'.padEnd(W_TOKENS))
780
- } else {
781
- tokCell = colorizeTokens(row.tokens, tokStr.padEnd(W_TOKENS))
782
- }
783
-
784
- // 📖 Colorize latency with gradient (green → orange → yellow → red)
785
- const latCell = colorizeLatency(row.latency, latStr.padEnd(W_LAT))
786
-
787
- // 📖 Build the row line - add dark red background for failed requests with zero tokens
788
- const rowText = ` ${timeCell} ${provCell} ${modelCell} ${routeCell} ${statusCell} ${tokCell} ${latCell}`
789
- if (isFailedWithZeroTokens) {
790
- lines.push(chalk.bgRgb(40, 0, 0)(rowText))
791
- } else {
792
- lines.push(rowText)
793
- }
794
- }
795
- }
796
-
797
- lines.push('')
798
- lines.push(chalk.dim(` Showing up to 200 most recent entries • X or Esc close`))
799
- lines.push('')
800
-
801
- const { visible, offset } = sliceOverlayLines(lines, state.logScrollOffset, state.terminalRows)
802
- state.logScrollOffset = offset
803
- const tintedLines = tintOverlayLines(visible, LOG_OVERLAY_BG, state.terminalCols)
804
- const cleared = tintedLines.map(l => l + EL)
805
- return cleared.join('\n')
806
- }
807
-
808
547
  // 📖 renderRecommend: Draw the Smart Recommend overlay with 3 phases:
809
548
  // 1. 'questionnaire' — ask 3 questions (task type, priority, context budget)
810
549
  // 2. 'analyzing' — loading screen with progress bar (10s, 2 pings/sec)
@@ -1219,235 +958,6 @@ const updateRowIdx = providerKeys.length
1219
958
  return cleared.join('\n')
1220
959
  }
1221
960
 
1222
- // ─── FCM Proxy V2 overlay renderer ──────────────────────────────────────────
1223
- // 📖 renderProxyDaemon: Dedicated full-page overlay for FCM Proxy V2 configuration
1224
- // 📖 and background service management. Opened from Settings → "FCM Proxy V2 settings →".
1225
- // 📖 Contains all proxy toggles, service status/actions, explanations, and emergency kill.
1226
- function renderProxyDaemon() {
1227
- const EL = '\x1b[K'
1228
- const lines = []
1229
- const cursorLineByRow = {}
1230
- const proxySettings = getProxySettings(state.config)
1231
-
1232
- // 📖 Row indices — these control cursor navigation
1233
- const ROW_PROXY_ENABLED = 0
1234
- const ROW_PROXY_SYNC = 1
1235
- const ROW_PROXY_PORT = 2
1236
- const ROW_PROXY_CLEANUP = 3
1237
- const ROW_DAEMON_INSTALL = 4
1238
- const ROW_DAEMON_RESTART = 5
1239
- const ROW_DAEMON_STOP = 6
1240
- const ROW_DAEMON_KILL = 7
1241
- const ROW_DAEMON_LOGS = 8
1242
-
1243
- const daemonStatus = state.daemonStatus || 'not-installed'
1244
- const daemonInfo = state.daemonInfo
1245
- const daemonIsActive = daemonStatus === 'running' || daemonStatus === 'unhealthy' || daemonStatus === 'stale'
1246
- const daemonIsInstalled = daemonIsActive || daemonStatus === 'stopped'
1247
-
1248
- // 📖 Compute max row — hide daemon action rows when daemon not installed
1249
- let maxRow = ROW_DAEMON_INSTALL
1250
- if (daemonIsInstalled) maxRow = ROW_DAEMON_LOGS
1251
-
1252
- // 📖 Header
1253
- lines.push(` ${chalk.cyanBright('🚀')} ${chalk.bold.cyanBright('free-coding-models')} ${chalk.dim(`v${LOCAL_VERSION}`)}`)
1254
- lines.push(` ${chalk.bold('📡 FCM Proxy V2 Manager')}`)
1255
- lines.push(` ${chalk.dim('— Esc back to Settings • ↑↓ navigate • Enter select')}`)
1256
- lines.push('')
1257
- lines.push(` ${chalk.bgRed.white.bold(' ⚠ EXPERIMENTAL ')} ${chalk.red('This feature is under active development and may not work as expected.')}`)
1258
- lines.push(` ${chalk.red('Found a bug? Press')} ${chalk.bold.white('I')} ${chalk.red('on the main screen or join our Discord to report issues & suggest improvements.')}`)
1259
- lines.push('')
1260
-
1261
- // 📖 Feedback message (auto-clears after 5s)
1262
- const msg = state.proxyDaemonMessage
1263
- if (msg && (Date.now() - msg.ts < 5000)) {
1264
- const msgColor = msg.type === 'success' ? chalk.greenBright : msg.type === 'warning' ? chalk.yellow : chalk.red
1265
- lines.push(` ${msgColor(msg.msg)}`)
1266
- lines.push('')
1267
- }
1268
-
1269
- // ────────────────────────────── PROXY SECTION ──────────────────────────────
1270
- lines.push(` ${chalk.bold('🔀 Proxy Configuration')}`)
1271
- lines.push(` ${chalk.dim(' ─────────────────────────────────────────────')}`)
1272
- lines.push('')
1273
- lines.push(` ${chalk.dim(' The local proxy groups all your provider API keys into a single')}`)
1274
- lines.push(` ${chalk.dim(' endpoint. Tools like OpenCode, Claude Code, Goose, etc. connect')}`)
1275
- lines.push(` ${chalk.dim(' to this proxy which handles key rotation, rate limiting, and failover.')}`)
1276
- lines.push('')
1277
-
1278
- // 📖 Proxy sync now always follows the currently selected Z-mode when supported.
1279
- const currentToolMode = state.mode || 'opencode'
1280
- const currentToolMeta = getToolMeta(currentToolMode)
1281
- const currentToolLabel = `${currentToolMeta.emoji} ${currentToolMeta.label}`
1282
- const proxySyncTool = resolveProxySyncToolMode(currentToolMode)
1283
- const proxySyncHint = proxySyncTool
1284
- ? chalk.dim(` Current tool: ${currentToolLabel}`)
1285
- : chalk.yellow(` Current tool: ${currentToolLabel} (launcher-only, no persisted proxy config)`)
1286
- lines.push(proxySyncHint)
1287
- lines.push('')
1288
-
1289
- // 📖 Row 0: Proxy enabled toggle
1290
- const r0b = state.proxyDaemonCursor === ROW_PROXY_ENABLED ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
1291
- const r0val = proxySettings.enabled ? chalk.greenBright('Enabled') : chalk.dim('Disabled (opt-in)')
1292
- const r0 = `${r0b}${chalk.bold('Proxy mode').padEnd(44)} ${r0val}`
1293
- cursorLineByRow[ROW_PROXY_ENABLED] = lines.length
1294
- lines.push(state.proxyDaemonCursor === ROW_PROXY_ENABLED ? chalk.bgRgb(20, 45, 60)(r0) : r0)
1295
-
1296
- // 📖 Row 1: Auto-sync proxy config to the current tool when that tool supports persisted sync.
1297
- const r2b = state.proxyDaemonCursor === ROW_PROXY_SYNC ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
1298
- const r2val = proxySettings.syncToOpenCode ? chalk.greenBright('Enabled') : chalk.dim('Disabled')
1299
- const r2label = proxySyncTool
1300
- ? `Auto-sync proxy to ${currentToolMeta.label}`
1301
- : 'Auto-sync proxy to current tool'
1302
- const r2note = proxySyncTool ? '' : ` ${chalk.dim('(unavailable for this mode)')}`
1303
- const r2 = `${r2b}${chalk.bold(r2label).padEnd(44)} ${r2val}${r2note}`
1304
- cursorLineByRow[ROW_PROXY_SYNC] = lines.length
1305
- lines.push(state.proxyDaemonCursor === ROW_PROXY_SYNC ? chalk.bgRgb(20, 45, 60)(r2) : r2)
1306
-
1307
- // 📖 Row 2: Preferred port
1308
- const r3b = state.proxyDaemonCursor === ROW_PROXY_PORT ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
1309
- const r3val = state.settingsProxyPortEditMode && state.proxyDaemonCursor === ROW_PROXY_PORT
1310
- ? chalk.cyanBright(`${state.settingsProxyPortBuffer}▏`)
1311
- : (proxySettings.preferredPort === 0 ? chalk.dim('auto (OS-assigned)') : chalk.green(String(proxySettings.preferredPort)))
1312
- const r3 = `${r3b}${chalk.bold('Preferred proxy port').padEnd(44)} ${r3val}`
1313
- cursorLineByRow[ROW_PROXY_PORT] = lines.length
1314
- lines.push(state.proxyDaemonCursor === ROW_PROXY_PORT ? chalk.bgRgb(20, 45, 60)(r3) : r3)
1315
-
1316
- // 📖 Row 3: Clean current tool proxy config
1317
- const r4b = state.proxyDaemonCursor === ROW_PROXY_CLEANUP ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
1318
- const r4title = proxySyncTool
1319
- ? `Clean ${currentToolMeta.label} proxy config`
1320
- : `Clean ${currentToolMeta.label} proxy config`
1321
- const r4hint = proxySyncTool
1322
- ? chalk.dim('Enter → removes all fcm-* entries')
1323
- : chalk.dim('Unavailable for this mode')
1324
- const r4 = `${r4b}${chalk.bold(r4title).padEnd(44)} ${r4hint}`
1325
- cursorLineByRow[ROW_PROXY_CLEANUP] = lines.length
1326
- lines.push(state.proxyDaemonCursor === ROW_PROXY_CLEANUP ? chalk.bgRgb(45, 30, 30)(r4) : r4)
1327
-
1328
- // ────────────────────────────── DAEMON SECTION ─────────────────────────────
1329
- lines.push('')
1330
- lines.push(` ${chalk.bold('📡 FCM Proxy V2 Background Service')}`)
1331
- lines.push(` ${chalk.dim(' ─────────────────────────────────────────────')}`)
1332
- lines.push('')
1333
- lines.push(` ${chalk.dim(' The background service keeps FCM Proxy V2 running 24/7 — even when')}`)
1334
- lines.push(` ${chalk.dim(' the TUI is closed or after a reboot. Claude Code, Gemini CLI, and')}`)
1335
- lines.push(` ${chalk.dim(' all tools stay connected at all times.')}`)
1336
- lines.push('')
1337
-
1338
- // 📖 Status display
1339
- let daemonStatusLine = ` ${chalk.bold(' Status:')} `
1340
- if (daemonStatus === 'running') {
1341
- daemonStatusLine += chalk.greenBright('● Running')
1342
- if (daemonInfo) daemonStatusLine += chalk.dim(` — PID ${daemonInfo.pid} • Port ${daemonInfo.port} • ${daemonInfo.accountCount || '?'} accounts • ${daemonInfo.modelCount || '?'} models`)
1343
- } else if (daemonStatus === 'stopped') {
1344
- daemonStatusLine += chalk.yellow('○ Stopped') + chalk.dim(' — service installed but not running')
1345
- } else if (daemonStatus === 'stale') {
1346
- daemonStatusLine += chalk.red('⚠ Stale') + chalk.dim(' — service crashed, PID no longer alive')
1347
- } else if (daemonStatus === 'unhealthy') {
1348
- daemonStatusLine += chalk.red('⚠ Unhealthy') + chalk.dim(' — PID alive but health check failed')
1349
- } else {
1350
- daemonStatusLine += chalk.dim('○ Not installed')
1351
- }
1352
- lines.push(daemonStatusLine)
1353
-
1354
- // 📖 Version mismatch warning
1355
- if (daemonInfo?.version && daemonInfo.version !== LOCAL_VERSION) {
1356
- lines.push(` ${chalk.yellow(` ⚠ Version mismatch: service v${daemonInfo.version} vs FCM v${LOCAL_VERSION}`)}`)
1357
- lines.push(` ${chalk.dim(' Restart or reinstall the service to apply the update.')}`)
1358
- }
1359
-
1360
- // 📖 Uptime
1361
- if (daemonStatus === 'running' && daemonInfo?.startedAt) {
1362
- const upSec = Math.floor((Date.now() - new Date(daemonInfo.startedAt).getTime()) / 1000)
1363
- const upMin = Math.floor(upSec / 60)
1364
- const upHr = Math.floor(upMin / 60)
1365
- const uptimeStr = upHr > 0 ? `${upHr}h ${upMin % 60}m` : upMin > 0 ? `${upMin}m ${upSec % 60}s` : `${upSec}s`
1366
- lines.push(` ${chalk.dim(` Uptime: ${uptimeStr}`)}`)
1367
- }
1368
-
1369
- lines.push('')
1370
-
1371
- // 📖 Row 5: Install / Uninstall
1372
- const d0b = state.proxyDaemonCursor === ROW_DAEMON_INSTALL ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
1373
- const d0label = daemonIsInstalled ? 'Uninstall service' : 'Install background service'
1374
- const d0hint = daemonIsInstalled
1375
- ? chalk.dim('Enter → stop service + remove config')
1376
- : chalk.dim('Enter → install as OS service (launchd/systemd)')
1377
- const d0 = `${d0b}${chalk.bold(d0label).padEnd(44)} ${d0hint}`
1378
- cursorLineByRow[ROW_DAEMON_INSTALL] = lines.length
1379
- lines.push(state.proxyDaemonCursor === ROW_DAEMON_INSTALL ? chalk.bgRgb(daemonIsInstalled ? 45 : 20, daemonIsInstalled ? 30 : 45, daemonIsInstalled ? 30 : 40)(d0) : d0)
1380
-
1381
- // 📖 Rows 6-9 only shown when service is installed
1382
- if (daemonIsInstalled) {
1383
- // 📖 Row 6: Restart
1384
- const d1b = state.proxyDaemonCursor === ROW_DAEMON_RESTART ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
1385
- const d1 = `${d1b}${chalk.bold('Restart service').padEnd(44)} ${chalk.dim('Enter → stop + start via OS service manager')}`
1386
- cursorLineByRow[ROW_DAEMON_RESTART] = lines.length
1387
- lines.push(state.proxyDaemonCursor === ROW_DAEMON_RESTART ? chalk.bgRgb(20, 45, 60)(d1) : d1)
1388
-
1389
- // 📖 Row 7: Stop (SIGTERM)
1390
- const d2b = state.proxyDaemonCursor === ROW_DAEMON_STOP ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
1391
- const d2warn = chalk.dim(' (service may auto-restart)')
1392
- const d2 = `${d2b}${chalk.bold('Stop service').padEnd(44)} ${chalk.dim('Enter → graceful shutdown (SIGTERM)')}${d2warn}`
1393
- cursorLineByRow[ROW_DAEMON_STOP] = lines.length
1394
- lines.push(state.proxyDaemonCursor === ROW_DAEMON_STOP ? chalk.bgRgb(45, 40, 20)(d2) : d2)
1395
-
1396
- // 📖 Row 8: Force kill (SIGKILL) — emergency
1397
- const d3b = state.proxyDaemonCursor === ROW_DAEMON_KILL ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
1398
- const d3 = `${d3b}${chalk.bold.red('Force kill service').padEnd(44)} ${chalk.dim('Enter → SIGKILL — emergency only')}`
1399
- cursorLineByRow[ROW_DAEMON_KILL] = lines.length
1400
- lines.push(state.proxyDaemonCursor === ROW_DAEMON_KILL ? chalk.bgRgb(60, 20, 20)(d3) : d3)
1401
-
1402
- // 📖 Row 9: View logs
1403
- const d4b = state.proxyDaemonCursor === ROW_DAEMON_LOGS ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
1404
- const d4 = `${d4b}${chalk.bold('View service logs').padEnd(44)} ${chalk.dim('Enter → show last 50 log lines')}`
1405
- cursorLineByRow[ROW_DAEMON_LOGS] = lines.length
1406
- lines.push(state.proxyDaemonCursor === ROW_DAEMON_LOGS ? chalk.bgRgb(30, 30, 50)(d4) : d4)
1407
- }
1408
-
1409
- // ────────────────────────────── INFO SECTION ───────────────────────────────
1410
- lines.push('')
1411
- lines.push(` ${chalk.bold('ℹ How it works')}`)
1412
- lines.push(` ${chalk.dim(' ─────────────────────────────────────────────')}`)
1413
- lines.push('')
1414
- lines.push(` ${chalk.dim(' 📖 The proxy starts a local HTTP server on 127.0.0.1 (localhost only).')}`)
1415
- lines.push(` ${chalk.dim(' 📖 External tools connect to it as if it were OpenAI/Anthropic.')}`)
1416
- lines.push(` ${chalk.dim(' 📖 The proxy rotates between your API keys across all providers.')}`)
1417
- lines.push('')
1418
- lines.push(` ${chalk.dim(' 📖 The background service adds persistence: install it once, and the proxy')}`)
1419
- lines.push(` ${chalk.dim(' 📖 starts automatically at login and survives reboots.')}`)
1420
- lines.push('')
1421
- lines.push(` ${chalk.dim(' 📖 Claude Code support: FCM Proxy V2 translates Anthropic wire format')}`)
1422
- lines.push(` ${chalk.dim(' 📖 (POST /v1/messages) to OpenAI format for upstream providers.')}`)
1423
- lines.push('')
1424
- if (process.platform === 'darwin') {
1425
- lines.push(` ${chalk.dim(' 📦 macOS: launchd LaunchAgent at ~/Library/LaunchAgents/com.fcm.proxy.plist')}`)
1426
- } else if (process.platform === 'linux') {
1427
- lines.push(` ${chalk.dim(' 📦 Linux: systemd user service at ~/.config/systemd/user/fcm-proxy.service')}`)
1428
- } else {
1429
- lines.push(` ${chalk.dim(' ⚠ Windows: background service not supported — use in-process proxy (starts with TUI)')}`)
1430
- }
1431
- lines.push('')
1432
-
1433
- // 📖 Clamp cursor
1434
- if (state.proxyDaemonCursor > maxRow) state.proxyDaemonCursor = maxRow
1435
-
1436
- // 📖 Scrolling and tinting
1437
- const PROXY_DAEMON_BG = chalk.bgRgb(15, 25, 45)
1438
- const targetLine = cursorLineByRow[state.proxyDaemonCursor] ?? 0
1439
- state.proxyDaemonScrollOffset = keepOverlayTargetVisible(
1440
- state.proxyDaemonScrollOffset,
1441
- targetLine,
1442
- lines.length,
1443
- state.terminalRows
1444
- )
1445
- const { visible, offset } = sliceOverlayLines(lines, state.proxyDaemonScrollOffset, state.terminalRows)
1446
- state.proxyDaemonScrollOffset = offset
1447
- const tintedLines = tintOverlayLines(visible, PROXY_DAEMON_BG, state.terminalCols)
1448
- return tintedLines.map(l => l + EL).join('\n')
1449
- }
1450
-
1451
961
  // 📖 stopRecommendAnalysis: cleanup timers if user cancels during analysis
1452
962
  function stopRecommendAnalysis() {
1453
963
  if (state.recommendAnalysisTimer) { clearInterval(state.recommendAnalysisTimer); state.recommendAnalysisTimer = null }
@@ -1456,10 +966,8 @@ const updateRowIdx = providerKeys.length
1456
966
 
1457
967
  return {
1458
968
  renderSettings,
1459
- renderProxyDaemon,
1460
969
  renderInstallEndpoints,
1461
970
  renderHelp,
1462
- renderLog,
1463
971
  renderRecommend,
1464
972
  renderFeedback,
1465
973
  renderChangelog,