free-coding-models 0.3.11 → 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,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 = {}
@@ -285,23 +228,11 @@ const updateRowIdx = providerKeys.length
285
228
  lines.push(chalk.red(` ${state.settingsUpdateError}`))
286
229
  }
287
230
 
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)
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)
305
236
 
306
237
  // 📖 Changelog viewer row
307
238
  const changelogViewBullet = state.settingsCursor === changelogViewRowIdx ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
@@ -314,10 +245,8 @@ const updateRowIdx = providerKeys.length
314
245
  lines.push('')
315
246
  if (state.settingsEditMode) {
316
247
  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
248
  } 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'))
249
+ lines.push(chalk.dim(' ↑↓ Navigate • Enter Edit/Run • + Add key • - Remove key • Space Toggle • T Test key • U Updates • Esc Close'))
321
250
  }
322
251
  // 📖 Show sync/restore status message if set
323
252
  if (state.settingsSyncStatus) {
@@ -355,7 +284,7 @@ const updateRowIdx = providerKeys.length
355
284
  }
356
285
 
357
286
  // ─── Install Endpoints overlay renderer ───────────────────────────────────
358
- // 📖 renderInstallEndpoints drives the provider → tool → connection → scope → model flow
287
+ // 📖 renderInstallEndpoints drives the provider → tool → scope → model flow
359
288
  // 📖 behind the `Y` hotkey. It deliberately reuses the same overlay viewport
360
289
  // 📖 helpers as Settings so long provider/model lists stay navigable.
361
290
  function renderInstallEndpoints() {
@@ -364,8 +293,7 @@ const updateRowIdx = providerKeys.length
364
293
  const cursorLineByRow = {}
365
294
  const providerChoices = getConfiguredInstallableProviders(state.config)
366
295
  const toolChoices = getInstallTargetModes()
367
- const connectionChoices = CONNECTION_MODES || []
368
- const totalSteps = 5
296
+ const totalSteps = 4
369
297
  const scopeChoices = [
370
298
  {
371
299
  key: 'all',
@@ -391,11 +319,7 @@ const updateRowIdx = providerKeys.length
391
319
  })()
392
320
  : '—'
393
321
 
394
- const selectedConnectionLabel = state.installEndpointsConnectionMode === 'proxy'
395
- ? 'FCM Proxy V2'
396
- : state.installEndpointsConnectionMode === 'direct'
397
- ? 'Direct Provider'
398
- : '—'
322
+ const selectedConnectionLabel = 'Direct Provider'
399
323
 
400
324
  lines.push('')
401
325
  // 📖 Branding header
@@ -439,7 +363,7 @@ const updateRowIdx = providerKeys.length
439
363
  const label = `${meta.emoji} ${meta.label}`
440
364
  const note = toolMode.startsWith('opencode')
441
365
  ? chalk.dim('shared config file')
442
- : ['claude-code', 'codex', 'openhands'].includes(toolMode)
366
+ : toolMode === 'openhands'
443
367
  ? chalk.dim('env file (~/.fcm-*-env)')
444
368
  : chalk.dim('managed config install')
445
369
  const bullet = isCursor ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
@@ -450,26 +374,8 @@ const updateRowIdx = providerKeys.length
450
374
 
451
375
  lines.push('')
452
376
  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
377
  } else if (state.installEndpointsPhase === 'scope') {
472
- 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')}`)
473
379
  lines.push(chalk.dim(` Provider: ${selectedProviderLabel} • Tool: ${selectedToolLabel} • ${selectedConnectionLabel}`))
474
380
  lines.push('')
475
381
 
@@ -488,7 +394,7 @@ const updateRowIdx = providerKeys.length
488
394
  const models = getProviderCatalogModels(state.installEndpointsProviderKey)
489
395
  const selectedCount = state.installEndpointsSelectedModelIds.size
490
396
 
491
- 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')}`)
492
398
  lines.push(chalk.dim(` Provider: ${selectedProviderLabel} • Tool: ${selectedToolLabel} • ${selectedConnectionLabel}`))
493
399
  lines.push(chalk.dim(` Selected: ${selectedCount}/${models.length}`))
494
400
  lines.push('')
@@ -590,8 +496,8 @@ const updateRowIdx = providerKeys.length
590
496
  lines.push(` ${chalk.cyan('Up%')} Uptime — ratio of successful pings to total pings ${chalk.dim('Sort:')} ${chalk.yellow('U')}`)
591
497
  lines.push(` ${chalk.dim('If a model only works half the time, you\'ll waste time retrying. Higher = more reliable.')}`)
592
498
  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.')}`)
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.')}`)
595
501
  lines.push('')
596
502
 
597
503
 
@@ -604,14 +510,12 @@ const updateRowIdx = providerKeys.length
604
510
  lines.push(` ${chalk.bold('Controls')}`)
605
511
  lines.push(` ${chalk.yellow('W')} Toggle ping mode ${chalk.dim('(speed 2s → normal 10s → slow 30s → forced 4s)')}`)
606
512
  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)')}`)
513
+ lines.push(` ${chalk.yellow('Z')} Cycle tool mode ${chalk.dim('(OpenCode Desktop OpenClaw → Crush → Goose → Pi → Aider → Qwen → OpenHands → Amp)')}`)
609
514
  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)')}`)
515
+ lines.push(` ${chalk.yellow('Y')} Install endpoints ${chalk.dim('(provider catalog → compatible tools, direct provider only)')}`)
611
516
  lines.push(` ${chalk.yellow('Q')} Smart Recommend ${chalk.dim('(🎯 find the best model for your task — questionnaire + live analysis)')}`)
612
517
  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)')}`)
518
+ lines.push(` ${chalk.yellow('P')} Open settings ${chalk.dim('(manage API keys, provider toggles, updates, legacy cleanup)')}`)
615
519
  // 📖 Profile system removed - API keys now persist permanently across all sessions
616
520
  lines.push(` ${chalk.yellow('Shift+R')} Reset view settings ${chalk.dim('(tier filter, sort, provider filter → defaults)')}`)
617
521
  lines.push(` ${chalk.yellow('N')} Changelog ${chalk.dim('(📋 browse all versions, Enter to view details)')}`)
@@ -622,7 +526,7 @@ const updateRowIdx = providerKeys.length
622
526
  lines.push(` ${chalk.yellow('↑↓')} Navigate rows`)
623
527
  lines.push(` ${chalk.yellow('PgUp/PgDn')} Jump by page`)
624
528
  lines.push(` ${chalk.yellow('Home/End')} Jump first/last row`)
625
- lines.push(` ${chalk.yellow('Enter')} Edit key / check-install update`)
529
+ lines.push(` ${chalk.yellow('Enter')} Edit key / run selected maintenance action`)
626
530
  lines.push(` ${chalk.yellow('Space')} Toggle provider enable/disable`)
627
531
  lines.push(` ${chalk.yellow('T')} Test selected provider key`)
628
532
  lines.push(` ${chalk.yellow('U')} Check updates manually`)
@@ -638,173 +542,6 @@ const updateRowIdx = providerKeys.length
638
542
  return cleared.join('\n')
639
543
  }
640
544
 
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
545
  // 📖 renderRecommend: Draw the Smart Recommend overlay with 3 phases:
809
546
  // 1. 'questionnaire' — ask 3 questions (task type, priority, context budget)
810
547
  // 2. 'analyzing' — loading screen with progress bar (10s, 2 pings/sec)
@@ -1219,235 +956,6 @@ const updateRowIdx = providerKeys.length
1219
956
  return cleared.join('\n')
1220
957
  }
1221
958
 
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
959
  // 📖 stopRecommendAnalysis: cleanup timers if user cancels during analysis
1452
960
  function stopRecommendAnalysis() {
1453
961
  if (state.recommendAnalysisTimer) { clearInterval(state.recommendAnalysisTimer); state.recommendAnalysisTimer = null }
@@ -1456,10 +964,8 @@ const updateRowIdx = providerKeys.length
1456
964
 
1457
965
  return {
1458
966
  renderSettings,
1459
- renderProxyDaemon,
1460
967
  renderInstallEndpoints,
1461
968
  renderHelp,
1462
- renderLog,
1463
969
  renderRecommend,
1464
970
  renderFeedback,
1465
971
  renderChangelog,