free-coding-models 0.3.9 → 0.3.12

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,20 +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
- listProfiles,
42
36
  TIER_CYCLE,
43
37
  SETTINGS_OVERLAY_BG,
44
38
  HELP_OVERLAY_BG,
45
39
  RECOMMEND_OVERLAY_BG,
46
- LOG_OVERLAY_BG,
47
40
  OVERLAY_PANEL_WIDTH,
48
41
  keepOverlayTargetVisible,
49
42
  sliceOverlayLines,
50
43
  tintOverlayLines,
51
- loadRecentLogs,
52
44
  TASK_TYPES,
53
45
  PRIORITY_TYPES,
54
46
  CONTEXT_BUDGETS,
@@ -63,7 +55,6 @@ export function createOverlayRenderers(state, deps) {
63
55
  getConfiguredInstallableProviders,
64
56
  getInstallTargetModes,
65
57
  getProviderCatalogModels,
66
- CONNECTION_MODES,
67
58
  getToolMeta,
68
59
  } = deps
69
60
 
@@ -90,54 +81,6 @@ export function createOverlayRenderers(state, deps) {
90
81
  return lines
91
82
  }
92
83
 
93
- // 📖 Keep log token formatting aligned with the main table so the same totals
94
- // 📖 read the same everywhere in the TUI.
95
- const formatLogTokens = (totalTokens) => {
96
- const safeTotal = Number(totalTokens) || 0
97
- if (safeTotal <= 0) return '--'
98
- if (safeTotal >= 999_500) return `${(safeTotal / 1_000_000).toFixed(2)}M`
99
- if (safeTotal >= 1_000) return `${(safeTotal / 1_000).toFixed(2)}k`
100
- return String(Math.floor(safeTotal))
101
- }
102
-
103
- // 📖 Colorize latency with gradient: green (<500ms) → orange (<1000ms) → yellow (<1500ms) → red (>=1500ms)
104
- const colorizeLatency = (latency, text) => {
105
- const ms = Number(latency) || 0
106
- if (ms <= 0) return chalk.dim(text)
107
- if (ms < 500) return chalk.greenBright(text)
108
- if (ms < 1000) return chalk.rgb(255, 165, 0)(text) // Orange
109
- if (ms < 1500) return chalk.yellow(text)
110
- return chalk.red(text)
111
- }
112
-
113
- // 📖 Colorize tokens with gradient: dim green (few) → bright green (many)
114
- const colorizeTokens = (tokens, text) => {
115
- const tok = Number(tokens) || 0
116
- if (tok <= 0) return chalk.dim(text)
117
- // Gradient: light green (low) → medium green → bright green (high, >30k)
118
- if (tok < 10_000) return chalk.hex('#90EE90')(text) // Light green
119
- if (tok < 30_000) return chalk.hex('#32CD32')(text) // Lime green
120
- return chalk.greenBright(text) // Full brightness green
121
- }
122
-
123
- // 📖 Get model color based on status code - distinct colors for each error type
124
- const getModelColorByStatus = (status) => {
125
- const sc = String(status)
126
- if (sc === '200') return chalk.greenBright // Success - bright green
127
- if (sc === '404') return chalk.rgb(139, 0, 0) // Not found - dark red
128
- if (sc === '400') return chalk.hex('#8B008B') // Bad request - dark magenta
129
- if (sc === '401') return chalk.hex('#9932CC') // Unauthorized - dark orchid
130
- if (sc === '403') return chalk.hex('#BA55D3') // Forbidden - medium orchid
131
- if (sc === '413') return chalk.hex('#FF6347') // Payload too large - tomato red
132
- if (sc === '429') return chalk.hex('#FFB90F') // Rate limit - dark orange
133
- if (sc === '500') return chalk.hex('#DC143C') // Internal server error - crimson
134
- if (sc === '502') return chalk.hex('#C71585') // Bad gateway - medium violet red
135
- if (sc === '503') return chalk.hex('#9370DB') // Service unavailable - medium purple
136
- if (sc.startsWith('5')) return chalk.magenta // Other 5xx - magenta
137
- if (sc === '0') return chalk.hex('#696969') // Timeout/error - dim gray
138
- return chalk.white // Unknown - white
139
- }
140
-
141
84
  // ─── Settings screen renderer ─────────────────────────────────────────────
142
85
  // 📖 renderSettings: Draw the settings overlay in the alt screen buffer.
143
86
  // 📖 Shows all providers with their API key (masked) + enabled state.
@@ -145,11 +88,10 @@ export function createOverlayRenderers(state, deps) {
145
88
  // 📖 Key "T" in settings = test API key for selected provider.
146
89
  function renderSettings() {
147
90
  const providerKeys = Object.keys(sources)
148
- const updateRowIdx = providerKeys.length
149
- const widthWarningRowIdx = updateRowIdx + 1
150
- const proxyDaemonRowIdx = widthWarningRowIdx + 1
151
- const changelogViewRowIdx = proxyDaemonRowIdx + 1
152
- 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
153
95
  const EL = '\x1b[K'
154
96
  const lines = []
155
97
  const cursorLineByRow = {}
@@ -286,23 +228,11 @@ const updateRowIdx = providerKeys.length
286
228
  lines.push(chalk.red(` ${state.settingsUpdateError}`))
287
229
  }
288
230
 
289
- // 📖 FCM Proxy V2 single row that opens a dedicated overlay
290
- lines.push('')
291
- lines.push(` ${chalk.bold('📡 FCM Proxy V2')}`)
292
- lines.push(` ${chalk.dim(' ' + '─'.repeat(separatorWidth))}`)
293
- lines.push('')
294
-
295
- const proxyDaemonBullet = state.settingsCursor === proxyDaemonRowIdx ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
296
- const proxyStatus = proxySettings.enabled ? chalk.greenBright('Proxy ON') : chalk.dim('Proxy OFF')
297
- const daemonStatus = state.daemonStatus || 'not-installed'
298
- let daemonBadge
299
- if (daemonStatus === 'running') daemonBadge = chalk.greenBright('Service ON')
300
- else if (daemonStatus === 'stopped') daemonBadge = chalk.yellow('Service stopped')
301
- else if (daemonStatus === 'stale' || daemonStatus === 'unhealthy') daemonBadge = chalk.red('Service ' + daemonStatus)
302
- else daemonBadge = chalk.dim('Service OFF')
303
- const proxyDaemonRow = `${proxyDaemonBullet}${chalk.bold('FCM Proxy V2 settings →').padEnd(44)} ${proxyStatus} ${chalk.dim('•')} ${daemonBadge}`
304
- cursorLineByRow[proxyDaemonRowIdx] = lines.length
305
- lines.push(state.settingsCursor === proxyDaemonRowIdx ? chalk.bgRgb(20, 45, 60)(proxyDaemonRow) : proxyDaemonRow)
231
+ // 📖 Cleanup row removes stale proxy-era config left behind by older builds.
232
+ const cleanupLegacyProxyBullet = state.settingsCursor === cleanupLegacyProxyRowIdx ? chalk.bold.cyan('') : chalk.dim(' ')
233
+ const cleanupLegacyProxyRow = `${cleanupLegacyProxyBullet}${chalk.bold('Clean Legacy Proxy Config').padEnd(44)} ${chalk.magentaBright('Enter remove discontinued bridge leftovers')}`
234
+ cursorLineByRow[cleanupLegacyProxyRowIdx] = lines.length
235
+ lines.push(state.settingsCursor === cleanupLegacyProxyRowIdx ? chalk.bgRgb(55, 25, 55)(cleanupLegacyProxyRow) : cleanupLegacyProxyRow)
306
236
 
307
237
  // 📖 Changelog viewer row
308
238
  const changelogViewBullet = state.settingsCursor === changelogViewRowIdx ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
@@ -310,41 +240,13 @@ const updateRowIdx = providerKeys.length
310
240
  cursorLineByRow[changelogViewRowIdx] = lines.length
311
241
  lines.push(state.settingsCursor === changelogViewRowIdx ? chalk.bgRgb(30, 45, 30)(changelogViewRow) : changelogViewRow)
312
242
 
313
- // 📖 Profiles section list saved profiles with active indicator + delete support
314
- const savedProfiles = listProfiles(state.config)
315
- const profileStartIdx = updateRowIdx + 5
316
- const maxRowIdx = savedProfiles.length > 0 ? profileStartIdx + savedProfiles.length - 1 : changelogViewRowIdx
317
-
318
- lines.push('')
319
- lines.push(` ${chalk.bold('📋 Profiles')} ${chalk.dim(savedProfiles.length > 0 ? `(${savedProfiles.length} saved)` : '(none — press Shift+S in main view to save)')}`)
320
- lines.push(` ${chalk.dim(' ' + '─'.repeat(separatorWidth))}`)
321
- lines.push('')
322
-
323
- if (savedProfiles.length === 0) {
324
- lines.push(chalk.dim(' No saved profiles. Press Shift+S in the main table to save your current settings as a profile.'))
325
- } else {
326
- for (let i = 0; i < savedProfiles.length; i++) {
327
- const pName = savedProfiles[i]
328
- const rowIdx = profileStartIdx + i
329
- const isCursor = state.settingsCursor === rowIdx
330
- const isActive = state.activeProfile === pName
331
- const activeBadge = isActive ? chalk.greenBright(' ✅ active') : ''
332
- const bullet = isCursor ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
333
- const profileLabel = chalk.rgb(200, 150, 255).bold(pName.padEnd(30))
334
- const deleteHint = isCursor ? chalk.dim(' Enter→Load • Backspace→Delete') : ''
335
- const row = `${bullet}${profileLabel}${activeBadge}${deleteHint}`
336
- cursorLineByRow[rowIdx] = lines.length
337
- lines.push(isCursor ? chalk.bgRgb(40, 20, 60)(row) : row)
338
- }
339
- }
243
+ // 📖 Profile system removed - API keys now persist permanently across all sessions
340
244
 
341
245
  lines.push('')
342
246
  if (state.settingsEditMode) {
343
247
  lines.push(chalk.dim(' Type API key • Enter Save • Esc Cancel'))
344
- } else if (state.settingsProxyPortEditMode) {
345
- lines.push(chalk.dim(' Type proxy port (0 = auto) • Enter Save • Esc Cancel'))
346
248
  } else {
347
- 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 • ⌫ Delete profile • Esc Close'))
249
+ lines.push(chalk.dim(' ↑↓ Navigate • Enter Edit/Run • + Add key • - Remove key • Space Toggle • T Test key • U Updates • Esc Close'))
348
250
  }
349
251
  // 📖 Show sync/restore status message if set
350
252
  if (state.settingsSyncStatus) {
@@ -382,7 +284,7 @@ const profileStartIdx = updateRowIdx + 5
382
284
  }
383
285
 
384
286
  // ─── Install Endpoints overlay renderer ───────────────────────────────────
385
- // 📖 renderInstallEndpoints drives the provider → tool → connection → scope → model flow
287
+ // 📖 renderInstallEndpoints drives the provider → tool → scope → model flow
386
288
  // 📖 behind the `Y` hotkey. It deliberately reuses the same overlay viewport
387
289
  // 📖 helpers as Settings so long provider/model lists stay navigable.
388
290
  function renderInstallEndpoints() {
@@ -391,8 +293,7 @@ const profileStartIdx = updateRowIdx + 5
391
293
  const cursorLineByRow = {}
392
294
  const providerChoices = getConfiguredInstallableProviders(state.config)
393
295
  const toolChoices = getInstallTargetModes()
394
- const connectionChoices = CONNECTION_MODES || []
395
- const totalSteps = 5
296
+ const totalSteps = 4
396
297
  const scopeChoices = [
397
298
  {
398
299
  key: 'all',
@@ -418,11 +319,7 @@ const profileStartIdx = updateRowIdx + 5
418
319
  })()
419
320
  : '—'
420
321
 
421
- const selectedConnectionLabel = state.installEndpointsConnectionMode === 'proxy'
422
- ? 'FCM Proxy V2'
423
- : state.installEndpointsConnectionMode === 'direct'
424
- ? 'Direct Provider'
425
- : '—'
322
+ const selectedConnectionLabel = 'Direct Provider'
426
323
 
427
324
  lines.push('')
428
325
  // 📖 Branding header
@@ -466,7 +363,7 @@ const profileStartIdx = updateRowIdx + 5
466
363
  const label = `${meta.emoji} ${meta.label}`
467
364
  const note = toolMode.startsWith('opencode')
468
365
  ? chalk.dim('shared config file')
469
- : ['claude-code', 'codex', 'openhands'].includes(toolMode)
366
+ : toolMode === 'openhands'
470
367
  ? chalk.dim('env file (~/.fcm-*-env)')
471
368
  : chalk.dim('managed config install')
472
369
  const bullet = isCursor ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
@@ -477,26 +374,8 @@ const profileStartIdx = updateRowIdx + 5
477
374
 
478
375
  lines.push('')
479
376
  lines.push(chalk.dim(' ↑↓ Navigate • Enter Choose tool • Esc Back'))
480
- } else if (state.installEndpointsPhase === 'connection') {
481
- // 📖 Step 3: Choose connection mode — Direct Provider vs FCM Proxy
482
- lines.push(` ${chalk.bold(`Step 3/${totalSteps}`)} ${chalk.cyan('Choose connection mode')}`)
483
- lines.push(chalk.dim(` Provider: ${selectedProviderLabel} • Tool: ${selectedToolLabel}`))
484
- lines.push('')
485
-
486
- connectionChoices.forEach((mode, idx) => {
487
- const isCursor = idx === state.installEndpointsCursor
488
- const bullet = isCursor ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
489
- const icon = mode.key === 'proxy' ? '🔄' : '⚡'
490
- const row = `${bullet}${icon} ${chalk.bold(mode.label)}`
491
- cursorLineByRow[idx] = lines.length
492
- lines.push(isCursor ? chalk.bgRgb(24, 44, 62)(row) : row)
493
- lines.push(chalk.dim(` ${mode.hint}`))
494
- lines.push('')
495
- })
496
-
497
- lines.push(chalk.dim(' Enter Continue • Esc Back'))
498
377
  } else if (state.installEndpointsPhase === 'scope') {
499
- lines.push(` ${chalk.bold(`Step 4/${totalSteps}`)} ${chalk.cyan('Choose the install scope')}`)
378
+ lines.push(` ${chalk.bold(`Step 3/${totalSteps}`)} ${chalk.cyan('Choose the install scope')}`)
500
379
  lines.push(chalk.dim(` Provider: ${selectedProviderLabel} • Tool: ${selectedToolLabel} • ${selectedConnectionLabel}`))
501
380
  lines.push('')
502
381
 
@@ -515,7 +394,7 @@ const profileStartIdx = updateRowIdx + 5
515
394
  const models = getProviderCatalogModels(state.installEndpointsProviderKey)
516
395
  const selectedCount = state.installEndpointsSelectedModelIds.size
517
396
 
518
- lines.push(` ${chalk.bold(`Step 5/${totalSteps}`)} ${chalk.cyan('Choose which models to install')}`)
397
+ lines.push(` ${chalk.bold(`Step 4/${totalSteps}`)} ${chalk.cyan('Choose which models to install')}`)
519
398
  lines.push(chalk.dim(` Provider: ${selectedProviderLabel} • Tool: ${selectedToolLabel} • ${selectedConnectionLabel}`))
520
399
  lines.push(chalk.dim(` Selected: ${selectedCount}/${models.length}`))
521
400
  lines.push('')
@@ -617,8 +496,8 @@ const profileStartIdx = updateRowIdx + 5
617
496
  lines.push(` ${chalk.cyan('Up%')} Uptime — ratio of successful pings to total pings ${chalk.dim('Sort:')} ${chalk.yellow('U')}`)
618
497
  lines.push(` ${chalk.dim('If a model only works half the time, you\'ll waste time retrying. Higher = more reliable.')}`)
619
498
  lines.push('')
620
- lines.push(` ${chalk.cyan('Used')} Total prompt+completion tokens consumed in logs for this exact provider/model pair`)
621
- lines.push(` ${chalk.dim('Loaded once at startup from request-log.jsonl. Displayed in K tokens, or M tokens above one million.')}`)
499
+ lines.push(` ${chalk.cyan('Used')} Historical prompt+completion tokens tracked for this exact provider/model pair`)
500
+ lines.push(` ${chalk.dim('Loaded from local stats snapshots. Displayed in K tokens, or M tokens above one million.')}`)
622
501
  lines.push('')
623
502
 
624
503
 
@@ -630,19 +509,14 @@ const profileStartIdx = updateRowIdx + 5
630
509
  lines.push('')
631
510
  lines.push(` ${chalk.bold('Controls')}`)
632
511
  lines.push(` ${chalk.yellow('W')} Toggle ping mode ${chalk.dim('(speed 2s → normal 10s → slow 30s → forced 4s)')}`)
633
- lines.push(` ${chalk.yellow('E')} Toggle configured models only ${chalk.dim('(enabled by default, persisted globally + in profiles)')}`)
634
- lines.push(` ${chalk.yellow('X')} Toggle token log page ${chalk.dim('(shows recent request usage from request-log.jsonl)')}`)
635
- lines.push(` ${chalk.yellow('Z')} Cycle tool mode ${chalk.dim('(OpenCode → Desktop → OpenClaw → Crush → Goose → Pi → Aider → Claude Code → Codex → Gemini → Qwen → OpenHands → Amp)')}`)
512
+ lines.push(` ${chalk.yellow('E')} Toggle configured models only ${chalk.dim('(enabled by default)')}`)
513
+ lines.push(` ${chalk.yellow('Z')} Cycle tool mode ${chalk.dim('(OpenCode Desktop OpenClaw → Crush → Goose → Pi → Aider → Qwen → OpenHands → Amp)')}`)
636
514
  lines.push(` ${chalk.yellow('F')} Toggle favorite on selected row ${chalk.dim('(⭐ pinned at top, persisted)')}`)
637
- lines.push(` ${chalk.yellow('Y')} Install endpoints ${chalk.dim('(provider catalog → compatible tools, Direct or FCM Proxy V2)')}`)
515
+ lines.push(` ${chalk.yellow('Y')} Install endpoints ${chalk.dim('(provider catalog → compatible tools, direct provider only)')}`)
638
516
  lines.push(` ${chalk.yellow('Q')} Smart Recommend ${chalk.dim('(🎯 find the best model for your task — questionnaire + live analysis)')}`)
639
517
  lines.push(` ${chalk.rgb(255, 87, 51).bold('I')} Feedback, bugs & requests ${chalk.dim('(📝 send anonymous feedback, bug reports, or feature requests)')}`)
640
- lines.push(` ${chalk.yellow('J')} FCM Proxy V2 settings ${chalk.dim('(📡 open proxy configuration and background service management)')}`)
641
- lines.push(` ${chalk.yellow('P')} Open settings ${chalk.dim('(manage API keys, provider toggles, proxy, manual update)')}`)
642
- lines.push(` ${chalk.yellow('Shift+P')} Cycle config profile ${chalk.dim('(switch between saved profiles live)')}`)
643
- lines.push(` ${chalk.yellow('Shift+S')} Save current config as a named profile ${chalk.dim('(inline prompt — type name + Enter)')}`)
644
- lines.push(` ${chalk.dim('Profiles store: favorites, sort, tier filter, ping interval, configured-only filter, API keys.')}`)
645
- lines.push(` ${chalk.dim('Use --profile <name> to load a profile on startup.')}`)
518
+ lines.push(` ${chalk.yellow('P')} Open settings ${chalk.dim('(manage API keys, provider toggles, updates, legacy cleanup)')}`)
519
+ // 📖 Profile system removed - API keys now persist permanently across all sessions
646
520
  lines.push(` ${chalk.yellow('Shift+R')} Reset view settings ${chalk.dim('(tier filter, sort, provider filter → defaults)')}`)
647
521
  lines.push(` ${chalk.yellow('N')} Changelog ${chalk.dim('(📋 browse all versions, Enter to view details)')}`)
648
522
  lines.push(` ${chalk.yellow('K')} / ${chalk.yellow('Esc')} Show/hide this help`)
@@ -652,7 +526,7 @@ const profileStartIdx = updateRowIdx + 5
652
526
  lines.push(` ${chalk.yellow('↑↓')} Navigate rows`)
653
527
  lines.push(` ${chalk.yellow('PgUp/PgDn')} Jump by page`)
654
528
  lines.push(` ${chalk.yellow('Home/End')} Jump first/last row`)
655
- lines.push(` ${chalk.yellow('Enter')} Edit key / check-install update`)
529
+ lines.push(` ${chalk.yellow('Enter')} Edit key / run selected maintenance action`)
656
530
  lines.push(` ${chalk.yellow('Space')} Toggle provider enable/disable`)
657
531
  lines.push(` ${chalk.yellow('T')} Test selected provider key`)
658
532
  lines.push(` ${chalk.yellow('U')} Check updates manually`)
@@ -668,173 +542,6 @@ const profileStartIdx = updateRowIdx + 5
668
542
  return cleared.join('\n')
669
543
  }
670
544
 
671
- // ─── Log page overlay renderer ────────────────────────────────────────────
672
- // 📖 renderLog: Draw the log page overlay showing recent requests from
673
- // 📖 ~/.free-coding-models/request-log.jsonl, newest-first.
674
- // 📖 Toggled with X key. Esc or X closes.
675
- function renderLog() {
676
- const EL = '\x1b[K'
677
- const lines = []
678
-
679
- // 📖 Branding header
680
- lines.push(` ${chalk.cyanBright('🚀')} ${chalk.bold.cyanBright('free-coding-models')} ${chalk.dim(`v${LOCAL_VERSION}`)}`)
681
- lines.push(` ${chalk.bold('📋 Request Log')}`)
682
- lines.push('')
683
- lines.push(chalk.dim(' — recent requests • ↑↓ scroll • A toggle all/500 • X or Esc close'))
684
- lines.push(chalk.dim(' Works only when the multi-account proxy is enabled and requests go through it.'))
685
- lines.push(chalk.dim(' Direct provider launches do not currently write into this log.'))
686
-
687
- // 📖 Load recent log entries — bounded read, newest-first, malformed lines skipped.
688
- // 📖 Show up to 500 entries by default, or all if logShowAll is true.
689
- const logLimit = state.logShowAll ? Number.MAX_SAFE_INTEGER : 500
690
- const logRows = loadRecentLogs({ limit: logLimit })
691
- const totalTokens = logRows.reduce((sum, row) => sum + (Number(row.tokens) || 0), 0)
692
-
693
- if (logRows.length === 0) {
694
- lines.push(chalk.dim(' No log entries found.'))
695
- lines.push(chalk.dim(' Logs are written to ~/.free-coding-models/request-log.jsonl'))
696
- lines.push(chalk.dim(' when requests are proxied through the multi-account rotation proxy.'))
697
- lines.push(chalk.dim(' Direct provider launches do not currently feed this token log.'))
698
- } else {
699
- lines.push(` ${chalk.bold('Total Consumed:')} ${chalk.greenBright(formatLogTokens(totalTokens))}`)
700
- lines.push('')
701
- // 📖 Column widths for the log table
702
- const W_TIME = 19
703
- const W_PROV = 14
704
- const W_MODEL = 44
705
- const W_ROUTE = 18
706
- const W_STATUS = 8
707
- const W_TOKENS = 12
708
- const W_LAT = 10
709
-
710
- // 📖 Header row
711
- const hTime = chalk.dim('Time'.padEnd(W_TIME))
712
- const hProv = chalk.dim('Provider'.padEnd(W_PROV))
713
- const hModel = chalk.dim('Model'.padEnd(W_MODEL))
714
- const hRoute = chalk.dim('Route'.padEnd(W_ROUTE))
715
- const hStatus = chalk.dim('Status'.padEnd(W_STATUS))
716
- const hTok = chalk.dim('Tokens Used'.padEnd(W_TOKENS))
717
- const hLat = chalk.dim('Latency'.padEnd(W_LAT))
718
-
719
- // 📖 Show mode indicator (all vs limited)
720
- const modeBadge = state.logShowAll
721
- ? chalk.yellow.bold('ALL')
722
- : chalk.cyan.bold('500')
723
- const countBadge = chalk.dim(`Showing ${logRows.length} entries`)
724
-
725
- lines.push(` ${hTime} ${hProv} ${hModel} ${hRoute} ${hStatus} ${hTok} ${hLat}`)
726
- lines.push(` ${chalk.dim('─'.repeat(W_TIME + W_PROV + W_MODEL + W_ROUTE + W_STATUS + W_TOKENS + W_LAT + 12))} ${modeBadge} ${countBadge}`)
727
-
728
- for (const row of logRows) {
729
- // 📖 Format time as HH:MM:SS (strip the date part for compactness)
730
- let timeStr = row.time
731
- try {
732
- const d = new Date(row.time)
733
- if (!Number.isNaN(d.getTime())) {
734
- timeStr = d.toISOString().replace('T', ' ').slice(0, 19)
735
- }
736
- } catch { /* keep raw */ }
737
-
738
- const requestedModelLabel = row.requestedModel || ''
739
- // 📖 Always show "requested → actual" if they differ, not just when switched
740
- const displayModel = requestedModelLabel && requestedModelLabel !== row.model
741
- ? `${requestedModelLabel} → ${row.model}`
742
- : row.model
743
-
744
- // 📖 Color-code status with distinct colors for each error type
745
- let statusCell
746
- const sc = String(row.status)
747
- if (sc === '200') {
748
- statusCell = chalk.greenBright(sc.padEnd(W_STATUS))
749
- } else if (sc === '404') {
750
- statusCell = chalk.rgb(139, 0, 0).bold(sc.padEnd(W_STATUS)) // Dark red for 404
751
- } else if (sc === '400') {
752
- statusCell = chalk.hex('#8B008B').bold(sc.padEnd(W_STATUS)) // Dark magenta
753
- } else if (sc === '401') {
754
- statusCell = chalk.hex('#9932CC').bold(sc.padEnd(W_STATUS)) // Dark orchid
755
- } else if (sc === '403') {
756
- statusCell = chalk.hex('#BA55D3').bold(sc.padEnd(W_STATUS)) // Medium orchid
757
- } else if (sc === '413') {
758
- statusCell = chalk.hex('#FF6347').bold(sc.padEnd(W_STATUS)) // Tomato red
759
- } else if (sc === '429') {
760
- statusCell = chalk.hex('#FFB90F').bold(sc.padEnd(W_STATUS)) // Dark orange
761
- } else if (sc === '500') {
762
- statusCell = chalk.hex('#DC143C').bold(sc.padEnd(W_STATUS)) // Crimson
763
- } else if (sc === '502') {
764
- statusCell = chalk.hex('#C71585').bold(sc.padEnd(W_STATUS)) // Medium violet red
765
- } else if (sc === '503') {
766
- statusCell = chalk.hex('#9370DB').bold(sc.padEnd(W_STATUS)) // Medium purple
767
- } else if (sc.startsWith('5')) {
768
- statusCell = chalk.magenta(sc.padEnd(W_STATUS)) // Other 5xx - magenta
769
- } else if (sc === '0') {
770
- statusCell = chalk.hex('#696969')(sc.padEnd(W_STATUS)) // Dim gray for timeout
771
- } else {
772
- statusCell = chalk.dim(sc.padEnd(W_STATUS))
773
- }
774
-
775
- const tokStr = formatLogTokens(row.tokens)
776
- const latStr = row.latency > 0 ? `${row.latency}ms` : '--'
777
- const routeLabel = row.switched
778
- ? `SWITCHED ↻ ${row.switchReason || 'fallback'}`
779
- : 'direct'
780
-
781
- // 📖 Detect failed requests with zero tokens - these get special red highlighting
782
- const isFailedWithZeroTokens = row.status !== '200' && (!row.tokens || Number(row.tokens) === 0)
783
-
784
- const timeCell = chalk.dim(timeStr.slice(0, W_TIME).padEnd(W_TIME))
785
- // 📖 Provider display: Use pretty label if available, otherwise raw key.
786
- // 📖 All these logs are from FCM Proxy V2.
787
- const providerLabel = PROVIDER_METADATA[row.provider]?.label || row.provider
788
- const providerRgb = PROVIDER_COLOR[row.provider] ?? [105, 190, 245]
789
- const provCell = chalk.bold.rgb(...providerRgb)(providerLabel.slice(0, W_PROV).padEnd(W_PROV))
790
-
791
- // 📖 Color model based on status - red for failed requests with zero tokens
792
- let modelCell
793
- if (isFailedWithZeroTokens) {
794
- modelCell = chalk.red.bold(displayModel.slice(0, W_MODEL).padEnd(W_MODEL))
795
- } else {
796
- const modelColorFn = getModelColorByStatus(row.status)
797
- modelCell = row.switched
798
- ? chalk.bold.rgb(255, 210, 90)(displayModel.slice(0, W_MODEL).padEnd(W_MODEL))
799
- : modelColorFn(displayModel.slice(0, W_MODEL).padEnd(W_MODEL))
800
- }
801
-
802
- const routeCell = row.switched
803
- ? chalk.bgRgb(120, 25, 25).yellow.bold(` ${routeLabel.slice(0, W_ROUTE - 2).padEnd(W_ROUTE - 2)} `)
804
- : chalk.dim(routeLabel.padEnd(W_ROUTE))
805
-
806
- // 📖 Colorize tokens - red cross emoji for failed requests with zero tokens
807
- let tokCell
808
- if (isFailedWithZeroTokens) {
809
- tokCell = chalk.red.bold('✗'.padEnd(W_TOKENS))
810
- } else {
811
- tokCell = colorizeTokens(row.tokens, tokStr.padEnd(W_TOKENS))
812
- }
813
-
814
- // 📖 Colorize latency with gradient (green → orange → yellow → red)
815
- const latCell = colorizeLatency(row.latency, latStr.padEnd(W_LAT))
816
-
817
- // 📖 Build the row line - add dark red background for failed requests with zero tokens
818
- const rowText = ` ${timeCell} ${provCell} ${modelCell} ${routeCell} ${statusCell} ${tokCell} ${latCell}`
819
- if (isFailedWithZeroTokens) {
820
- lines.push(chalk.bgRgb(40, 0, 0)(rowText))
821
- } else {
822
- lines.push(rowText)
823
- }
824
- }
825
- }
826
-
827
- lines.push('')
828
- lines.push(chalk.dim(` Showing up to 200 most recent entries • X or Esc close`))
829
- lines.push('')
830
-
831
- const { visible, offset } = sliceOverlayLines(lines, state.logScrollOffset, state.terminalRows)
832
- state.logScrollOffset = offset
833
- const tintedLines = tintOverlayLines(visible, LOG_OVERLAY_BG, state.terminalCols)
834
- const cleared = tintedLines.map(l => l + EL)
835
- return cleared.join('\n')
836
- }
837
-
838
545
  // 📖 renderRecommend: Draw the Smart Recommend overlay with 3 phases:
839
546
  // 1. 'questionnaire' — ask 3 questions (task type, priority, context budget)
840
547
  // 2. 'analyzing' — loading screen with progress bar (10s, 2 pings/sec)
@@ -1249,235 +956,6 @@ const profileStartIdx = updateRowIdx + 5
1249
956
  return cleared.join('\n')
1250
957
  }
1251
958
 
1252
- // ─── FCM Proxy V2 overlay renderer ──────────────────────────────────────────
1253
- // 📖 renderProxyDaemon: Dedicated full-page overlay for FCM Proxy V2 configuration
1254
- // 📖 and background service management. Opened from Settings → "FCM Proxy V2 settings →".
1255
- // 📖 Contains all proxy toggles, service status/actions, explanations, and emergency kill.
1256
- function renderProxyDaemon() {
1257
- const EL = '\x1b[K'
1258
- const lines = []
1259
- const cursorLineByRow = {}
1260
- const proxySettings = getProxySettings(state.config)
1261
-
1262
- // 📖 Row indices — these control cursor navigation
1263
- const ROW_PROXY_ENABLED = 0
1264
- const ROW_PROXY_SYNC = 1
1265
- const ROW_PROXY_PORT = 2
1266
- const ROW_PROXY_CLEANUP = 3
1267
- const ROW_DAEMON_INSTALL = 4
1268
- const ROW_DAEMON_RESTART = 5
1269
- const ROW_DAEMON_STOP = 6
1270
- const ROW_DAEMON_KILL = 7
1271
- const ROW_DAEMON_LOGS = 8
1272
-
1273
- const daemonStatus = state.daemonStatus || 'not-installed'
1274
- const daemonInfo = state.daemonInfo
1275
- const daemonIsActive = daemonStatus === 'running' || daemonStatus === 'unhealthy' || daemonStatus === 'stale'
1276
- const daemonIsInstalled = daemonIsActive || daemonStatus === 'stopped'
1277
-
1278
- // 📖 Compute max row — hide daemon action rows when daemon not installed
1279
- let maxRow = ROW_DAEMON_INSTALL
1280
- if (daemonIsInstalled) maxRow = ROW_DAEMON_LOGS
1281
-
1282
- // 📖 Header
1283
- lines.push(` ${chalk.cyanBright('🚀')} ${chalk.bold.cyanBright('free-coding-models')} ${chalk.dim(`v${LOCAL_VERSION}`)}`)
1284
- lines.push(` ${chalk.bold('📡 FCM Proxy V2 Manager')}`)
1285
- lines.push(` ${chalk.dim('— Esc back to Settings • ↑↓ navigate • Enter select')}`)
1286
- lines.push('')
1287
- lines.push(` ${chalk.bgRed.white.bold(' ⚠ EXPERIMENTAL ')} ${chalk.red('This feature is under active development and may not work as expected.')}`)
1288
- 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.')}`)
1289
- lines.push('')
1290
-
1291
- // 📖 Feedback message (auto-clears after 5s)
1292
- const msg = state.proxyDaemonMessage
1293
- if (msg && (Date.now() - msg.ts < 5000)) {
1294
- const msgColor = msg.type === 'success' ? chalk.greenBright : msg.type === 'warning' ? chalk.yellow : chalk.red
1295
- lines.push(` ${msgColor(msg.msg)}`)
1296
- lines.push('')
1297
- }
1298
-
1299
- // ────────────────────────────── PROXY SECTION ──────────────────────────────
1300
- lines.push(` ${chalk.bold('🔀 Proxy Configuration')}`)
1301
- lines.push(` ${chalk.dim(' ─────────────────────────────────────────────')}`)
1302
- lines.push('')
1303
- lines.push(` ${chalk.dim(' The local proxy groups all your provider API keys into a single')}`)
1304
- lines.push(` ${chalk.dim(' endpoint. Tools like OpenCode, Claude Code, Goose, etc. connect')}`)
1305
- lines.push(` ${chalk.dim(' to this proxy which handles key rotation, rate limiting, and failover.')}`)
1306
- lines.push('')
1307
-
1308
- // 📖 Proxy sync now always follows the currently selected Z-mode when supported.
1309
- const currentToolMode = state.mode || 'opencode'
1310
- const currentToolMeta = getToolMeta(currentToolMode)
1311
- const currentToolLabel = `${currentToolMeta.emoji} ${currentToolMeta.label}`
1312
- const proxySyncTool = resolveProxySyncToolMode(currentToolMode)
1313
- const proxySyncHint = proxySyncTool
1314
- ? chalk.dim(` Current tool: ${currentToolLabel}`)
1315
- : chalk.yellow(` Current tool: ${currentToolLabel} (launcher-only, no persisted proxy config)`)
1316
- lines.push(proxySyncHint)
1317
- lines.push('')
1318
-
1319
- // 📖 Row 0: Proxy enabled toggle
1320
- const r0b = state.proxyDaemonCursor === ROW_PROXY_ENABLED ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
1321
- const r0val = proxySettings.enabled ? chalk.greenBright('Enabled') : chalk.dim('Disabled (opt-in)')
1322
- const r0 = `${r0b}${chalk.bold('Proxy mode').padEnd(44)} ${r0val}`
1323
- cursorLineByRow[ROW_PROXY_ENABLED] = lines.length
1324
- lines.push(state.proxyDaemonCursor === ROW_PROXY_ENABLED ? chalk.bgRgb(20, 45, 60)(r0) : r0)
1325
-
1326
- // 📖 Row 1: Auto-sync proxy config to the current tool when that tool supports persisted sync.
1327
- const r2b = state.proxyDaemonCursor === ROW_PROXY_SYNC ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
1328
- const r2val = proxySettings.syncToOpenCode ? chalk.greenBright('Enabled') : chalk.dim('Disabled')
1329
- const r2label = proxySyncTool
1330
- ? `Auto-sync proxy to ${currentToolMeta.label}`
1331
- : 'Auto-sync proxy to current tool'
1332
- const r2note = proxySyncTool ? '' : ` ${chalk.dim('(unavailable for this mode)')}`
1333
- const r2 = `${r2b}${chalk.bold(r2label).padEnd(44)} ${r2val}${r2note}`
1334
- cursorLineByRow[ROW_PROXY_SYNC] = lines.length
1335
- lines.push(state.proxyDaemonCursor === ROW_PROXY_SYNC ? chalk.bgRgb(20, 45, 60)(r2) : r2)
1336
-
1337
- // 📖 Row 2: Preferred port
1338
- const r3b = state.proxyDaemonCursor === ROW_PROXY_PORT ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
1339
- const r3val = state.settingsProxyPortEditMode && state.proxyDaemonCursor === ROW_PROXY_PORT
1340
- ? chalk.cyanBright(`${state.settingsProxyPortBuffer}▏`)
1341
- : (proxySettings.preferredPort === 0 ? chalk.dim('auto (OS-assigned)') : chalk.green(String(proxySettings.preferredPort)))
1342
- const r3 = `${r3b}${chalk.bold('Preferred proxy port').padEnd(44)} ${r3val}`
1343
- cursorLineByRow[ROW_PROXY_PORT] = lines.length
1344
- lines.push(state.proxyDaemonCursor === ROW_PROXY_PORT ? chalk.bgRgb(20, 45, 60)(r3) : r3)
1345
-
1346
- // 📖 Row 3: Clean current tool proxy config
1347
- const r4b = state.proxyDaemonCursor === ROW_PROXY_CLEANUP ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
1348
- const r4title = proxySyncTool
1349
- ? `Clean ${currentToolMeta.label} proxy config`
1350
- : `Clean ${currentToolMeta.label} proxy config`
1351
- const r4hint = proxySyncTool
1352
- ? chalk.dim('Enter → removes all fcm-* entries')
1353
- : chalk.dim('Unavailable for this mode')
1354
- const r4 = `${r4b}${chalk.bold(r4title).padEnd(44)} ${r4hint}`
1355
- cursorLineByRow[ROW_PROXY_CLEANUP] = lines.length
1356
- lines.push(state.proxyDaemonCursor === ROW_PROXY_CLEANUP ? chalk.bgRgb(45, 30, 30)(r4) : r4)
1357
-
1358
- // ────────────────────────────── DAEMON SECTION ─────────────────────────────
1359
- lines.push('')
1360
- lines.push(` ${chalk.bold('📡 FCM Proxy V2 Background Service')}`)
1361
- lines.push(` ${chalk.dim(' ─────────────────────────────────────────────')}`)
1362
- lines.push('')
1363
- lines.push(` ${chalk.dim(' The background service keeps FCM Proxy V2 running 24/7 — even when')}`)
1364
- lines.push(` ${chalk.dim(' the TUI is closed or after a reboot. Claude Code, Gemini CLI, and')}`)
1365
- lines.push(` ${chalk.dim(' all tools stay connected at all times.')}`)
1366
- lines.push('')
1367
-
1368
- // 📖 Status display
1369
- let daemonStatusLine = ` ${chalk.bold(' Status:')} `
1370
- if (daemonStatus === 'running') {
1371
- daemonStatusLine += chalk.greenBright('● Running')
1372
- if (daemonInfo) daemonStatusLine += chalk.dim(` — PID ${daemonInfo.pid} • Port ${daemonInfo.port} • ${daemonInfo.accountCount || '?'} accounts • ${daemonInfo.modelCount || '?'} models`)
1373
- } else if (daemonStatus === 'stopped') {
1374
- daemonStatusLine += chalk.yellow('○ Stopped') + chalk.dim(' — service installed but not running')
1375
- } else if (daemonStatus === 'stale') {
1376
- daemonStatusLine += chalk.red('⚠ Stale') + chalk.dim(' — service crashed, PID no longer alive')
1377
- } else if (daemonStatus === 'unhealthy') {
1378
- daemonStatusLine += chalk.red('⚠ Unhealthy') + chalk.dim(' — PID alive but health check failed')
1379
- } else {
1380
- daemonStatusLine += chalk.dim('○ Not installed')
1381
- }
1382
- lines.push(daemonStatusLine)
1383
-
1384
- // 📖 Version mismatch warning
1385
- if (daemonInfo?.version && daemonInfo.version !== LOCAL_VERSION) {
1386
- lines.push(` ${chalk.yellow(` ⚠ Version mismatch: service v${daemonInfo.version} vs FCM v${LOCAL_VERSION}`)}`)
1387
- lines.push(` ${chalk.dim(' Restart or reinstall the service to apply the update.')}`)
1388
- }
1389
-
1390
- // 📖 Uptime
1391
- if (daemonStatus === 'running' && daemonInfo?.startedAt) {
1392
- const upSec = Math.floor((Date.now() - new Date(daemonInfo.startedAt).getTime()) / 1000)
1393
- const upMin = Math.floor(upSec / 60)
1394
- const upHr = Math.floor(upMin / 60)
1395
- const uptimeStr = upHr > 0 ? `${upHr}h ${upMin % 60}m` : upMin > 0 ? `${upMin}m ${upSec % 60}s` : `${upSec}s`
1396
- lines.push(` ${chalk.dim(` Uptime: ${uptimeStr}`)}`)
1397
- }
1398
-
1399
- lines.push('')
1400
-
1401
- // 📖 Row 5: Install / Uninstall
1402
- const d0b = state.proxyDaemonCursor === ROW_DAEMON_INSTALL ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
1403
- const d0label = daemonIsInstalled ? 'Uninstall service' : 'Install background service'
1404
- const d0hint = daemonIsInstalled
1405
- ? chalk.dim('Enter → stop service + remove config')
1406
- : chalk.dim('Enter → install as OS service (launchd/systemd)')
1407
- const d0 = `${d0b}${chalk.bold(d0label).padEnd(44)} ${d0hint}`
1408
- cursorLineByRow[ROW_DAEMON_INSTALL] = lines.length
1409
- lines.push(state.proxyDaemonCursor === ROW_DAEMON_INSTALL ? chalk.bgRgb(daemonIsInstalled ? 45 : 20, daemonIsInstalled ? 30 : 45, daemonIsInstalled ? 30 : 40)(d0) : d0)
1410
-
1411
- // 📖 Rows 6-9 only shown when service is installed
1412
- if (daemonIsInstalled) {
1413
- // 📖 Row 6: Restart
1414
- const d1b = state.proxyDaemonCursor === ROW_DAEMON_RESTART ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
1415
- const d1 = `${d1b}${chalk.bold('Restart service').padEnd(44)} ${chalk.dim('Enter → stop + start via OS service manager')}`
1416
- cursorLineByRow[ROW_DAEMON_RESTART] = lines.length
1417
- lines.push(state.proxyDaemonCursor === ROW_DAEMON_RESTART ? chalk.bgRgb(20, 45, 60)(d1) : d1)
1418
-
1419
- // 📖 Row 7: Stop (SIGTERM)
1420
- const d2b = state.proxyDaemonCursor === ROW_DAEMON_STOP ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
1421
- const d2warn = chalk.dim(' (service may auto-restart)')
1422
- const d2 = `${d2b}${chalk.bold('Stop service').padEnd(44)} ${chalk.dim('Enter → graceful shutdown (SIGTERM)')}${d2warn}`
1423
- cursorLineByRow[ROW_DAEMON_STOP] = lines.length
1424
- lines.push(state.proxyDaemonCursor === ROW_DAEMON_STOP ? chalk.bgRgb(45, 40, 20)(d2) : d2)
1425
-
1426
- // 📖 Row 8: Force kill (SIGKILL) — emergency
1427
- const d3b = state.proxyDaemonCursor === ROW_DAEMON_KILL ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
1428
- const d3 = `${d3b}${chalk.bold.red('Force kill service').padEnd(44)} ${chalk.dim('Enter → SIGKILL — emergency only')}`
1429
- cursorLineByRow[ROW_DAEMON_KILL] = lines.length
1430
- lines.push(state.proxyDaemonCursor === ROW_DAEMON_KILL ? chalk.bgRgb(60, 20, 20)(d3) : d3)
1431
-
1432
- // 📖 Row 9: View logs
1433
- const d4b = state.proxyDaemonCursor === ROW_DAEMON_LOGS ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
1434
- const d4 = `${d4b}${chalk.bold('View service logs').padEnd(44)} ${chalk.dim('Enter → show last 50 log lines')}`
1435
- cursorLineByRow[ROW_DAEMON_LOGS] = lines.length
1436
- lines.push(state.proxyDaemonCursor === ROW_DAEMON_LOGS ? chalk.bgRgb(30, 30, 50)(d4) : d4)
1437
- }
1438
-
1439
- // ────────────────────────────── INFO SECTION ───────────────────────────────
1440
- lines.push('')
1441
- lines.push(` ${chalk.bold('ℹ How it works')}`)
1442
- lines.push(` ${chalk.dim(' ─────────────────────────────────────────────')}`)
1443
- lines.push('')
1444
- lines.push(` ${chalk.dim(' 📖 The proxy starts a local HTTP server on 127.0.0.1 (localhost only).')}`)
1445
- lines.push(` ${chalk.dim(' 📖 External tools connect to it as if it were OpenAI/Anthropic.')}`)
1446
- lines.push(` ${chalk.dim(' 📖 The proxy rotates between your API keys across all providers.')}`)
1447
- lines.push('')
1448
- lines.push(` ${chalk.dim(' 📖 The background service adds persistence: install it once, and the proxy')}`)
1449
- lines.push(` ${chalk.dim(' 📖 starts automatically at login and survives reboots.')}`)
1450
- lines.push('')
1451
- lines.push(` ${chalk.dim(' 📖 Claude Code support: FCM Proxy V2 translates Anthropic wire format')}`)
1452
- lines.push(` ${chalk.dim(' 📖 (POST /v1/messages) to OpenAI format for upstream providers.')}`)
1453
- lines.push('')
1454
- if (process.platform === 'darwin') {
1455
- lines.push(` ${chalk.dim(' 📦 macOS: launchd LaunchAgent at ~/Library/LaunchAgents/com.fcm.proxy.plist')}`)
1456
- } else if (process.platform === 'linux') {
1457
- lines.push(` ${chalk.dim(' 📦 Linux: systemd user service at ~/.config/systemd/user/fcm-proxy.service')}`)
1458
- } else {
1459
- lines.push(` ${chalk.dim(' ⚠ Windows: background service not supported — use in-process proxy (starts with TUI)')}`)
1460
- }
1461
- lines.push('')
1462
-
1463
- // 📖 Clamp cursor
1464
- if (state.proxyDaemonCursor > maxRow) state.proxyDaemonCursor = maxRow
1465
-
1466
- // 📖 Scrolling and tinting
1467
- const PROXY_DAEMON_BG = chalk.bgRgb(15, 25, 45)
1468
- const targetLine = cursorLineByRow[state.proxyDaemonCursor] ?? 0
1469
- state.proxyDaemonScrollOffset = keepOverlayTargetVisible(
1470
- state.proxyDaemonScrollOffset,
1471
- targetLine,
1472
- lines.length,
1473
- state.terminalRows
1474
- )
1475
- const { visible, offset } = sliceOverlayLines(lines, state.proxyDaemonScrollOffset, state.terminalRows)
1476
- state.proxyDaemonScrollOffset = offset
1477
- const tintedLines = tintOverlayLines(visible, PROXY_DAEMON_BG, state.terminalCols)
1478
- return tintedLines.map(l => l + EL).join('\n')
1479
- }
1480
-
1481
959
  // 📖 stopRecommendAnalysis: cleanup timers if user cancels during analysis
1482
960
  function stopRecommendAnalysis() {
1483
961
  if (state.recommendAnalysisTimer) { clearInterval(state.recommendAnalysisTimer); state.recommendAnalysisTimer = null }
@@ -1486,10 +964,8 @@ const profileStartIdx = updateRowIdx + 5
1486
964
 
1487
965
  return {
1488
966
  renderSettings,
1489
- renderProxyDaemon,
1490
967
  renderInstallEndpoints,
1491
968
  renderHelp,
1492
- renderLog,
1493
969
  renderRecommend,
1494
970
  renderFeedback,
1495
971
  renderChangelog,