free-coding-models 0.2.15 → 0.3.0

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,20 +4,28 @@
4
4
  *
5
5
  * @details
6
6
  * This module centralizes all overlay rendering in one place:
7
- * - Settings, Install Endpoints, Help, Log, Smart Recommend, Feature Request, Bug Report, Changelog
7
+ * - Settings, Install Endpoints, Help, Log, Smart Recommend, Feedback, Changelog
8
+ * - FCM Proxy V2 overlay with tool selector, auto-sync toggle, and cleanup
8
9
  * - Settings diagnostics for provider key tests, including wrapped retry/error details
9
10
  * - Recommend analysis timer orchestration and progress updates
10
11
  *
11
12
  * The factory pattern keeps stateful UI logic isolated while still
12
13
  * allowing the main CLI to control shared state and dependencies.
13
14
  *
15
+ * 📖 The proxy overlay rows are: Enable → Active tool → Auto-sync → Port → Cleanup → Install/Restart/Stop/Kill/Logs
16
+ * 📖 Tool selector cycles through PROXY_SYNCABLE_TOOLS (12 tools from proxy-sync.js)
17
+ * 📖 Feedback overlay (I key) combines feature requests + bug reports in one left-aligned input
18
+ *
14
19
  * → Functions:
15
20
  * - `createOverlayRenderers` — returns renderer + analysis helpers
16
21
  *
17
22
  * @exports { createOverlayRenderers }
23
+ * @see ./proxy-sync.js — PROXY_SYNCABLE_TOOLS used by the tool selector
24
+ * @see ./key-handler.js — handles keypresses for all overlay interactions
18
25
  */
19
26
 
20
27
  import { loadChangelog } from './changelog-loader.js'
28
+ import { PROXY_SYNCABLE_TOOLS } from './proxy-sync.js'
21
29
 
22
30
  export function createOverlayRenderers(state, deps) {
23
31
  const {
@@ -55,6 +63,8 @@ export function createOverlayRenderers(state, deps) {
55
63
  getConfiguredInstallableProviders,
56
64
  getInstallTargetModes,
57
65
  getProviderCatalogModels,
66
+ CONNECTION_MODES,
67
+ getToolMeta,
58
68
  } = deps
59
69
 
60
70
  // 📖 Wrap plain diagnostic text so long Settings messages stay readable inside
@@ -136,11 +146,8 @@ export function createOverlayRenderers(state, deps) {
136
146
  function renderSettings() {
137
147
  const providerKeys = Object.keys(sources)
138
148
  const updateRowIdx = providerKeys.length
139
- const proxyEnabledRowIdx = updateRowIdx + 1
140
- const proxySyncRowIdx = updateRowIdx + 2
141
- const proxyPortRowIdx = updateRowIdx + 3
142
- const proxyCleanupRowIdx = updateRowIdx + 4
143
- const changelogViewRowIdx = updateRowIdx + 5
149
+ const proxyDaemonRowIdx = updateRowIdx + 1
150
+ const changelogViewRowIdx = updateRowIdx + 2
144
151
  const proxySettings = getProxySettings(state.config)
145
152
  const EL = '\x1b[K'
146
153
  const lines = []
@@ -271,33 +278,23 @@ export function createOverlayRenderers(state, deps) {
271
278
  lines.push(chalk.red(` ${state.settingsUpdateError}`))
272
279
  }
273
280
 
281
+ // 📖 FCM Proxy V2 — single row that opens a dedicated overlay
274
282
  lines.push('')
275
- lines.push(` ${chalk.bold('🔀 Proxy')}`)
283
+ lines.push(` ${chalk.bold('📡 FCM Proxy V2')}`)
276
284
  lines.push(` ${chalk.dim(' ' + '─'.repeat(separatorWidth))}`)
277
285
  lines.push('')
278
286
 
279
- const proxyEnabledBullet = state.settingsCursor === proxyEnabledRowIdx ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
280
- const proxyEnabledRow = `${proxyEnabledBullet}${chalk.bold('Proxy mode (opt-in)').padEnd(44)} ${proxySettings.enabled ? chalk.greenBright('Enabled') : chalk.dim('Disabled by default')}`
281
- cursorLineByRow[proxyEnabledRowIdx] = lines.length
282
- lines.push(state.settingsCursor === proxyEnabledRowIdx ? chalk.bgRgb(20, 45, 60)(proxyEnabledRow) : proxyEnabledRow)
283
-
284
- const proxySyncBullet = state.settingsCursor === proxySyncRowIdx ? chalk.bold.cyan('') : chalk.dim(' ')
285
- const proxySyncRow = `${proxySyncBullet}${chalk.bold('Persist proxy in OpenCode').padEnd(44)} ${proxySettings.syncToOpenCode ? chalk.greenBright('Enabled') : chalk.dim('Disabled')}`
286
- cursorLineByRow[proxySyncRowIdx] = lines.length
287
- lines.push(state.settingsCursor === proxySyncRowIdx ? chalk.bgRgb(20, 45, 60)(proxySyncRow) : proxySyncRow)
288
-
289
- const proxyPortBullet = state.settingsCursor === proxyPortRowIdx ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
290
- const proxyPortValue = state.settingsProxyPortEditMode && state.settingsCursor === proxyPortRowIdx
291
- ? chalk.cyanBright(`${state.settingsProxyPortBuffer}▏`)
292
- : (proxySettings.preferredPort === 0 ? chalk.dim('auto (OS-assigned)') : chalk.green(String(proxySettings.preferredPort)))
293
- const proxyPortRow = `${proxyPortBullet}${chalk.bold('Preferred proxy port').padEnd(44)} ${proxyPortValue}`
294
- cursorLineByRow[proxyPortRowIdx] = lines.length
295
- lines.push(state.settingsCursor === proxyPortRowIdx ? chalk.bgRgb(20, 45, 60)(proxyPortRow) : proxyPortRow)
296
-
297
- const proxyCleanupBullet = state.settingsCursor === proxyCleanupRowIdx ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
298
- const proxyCleanupRow = `${proxyCleanupBullet}${chalk.bold('Clean OpenCode proxy config').padEnd(44)} ${chalk.dim('Enter removes fcm-proxy from opencode.json')}`
299
- cursorLineByRow[proxyCleanupRowIdx] = lines.length
300
- lines.push(state.settingsCursor === proxyCleanupRowIdx ? chalk.bgRgb(45, 30, 30)(proxyCleanupRow) : proxyCleanupRow)
287
+ const proxyDaemonBullet = state.settingsCursor === proxyDaemonRowIdx ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
288
+ const proxyStatus = proxySettings.enabled ? chalk.greenBright('Proxy ON') : chalk.dim('Proxy OFF')
289
+ const daemonStatus = state.daemonStatus || 'not-installed'
290
+ let daemonBadge
291
+ if (daemonStatus === 'running') daemonBadge = chalk.greenBright('Service ON')
292
+ else if (daemonStatus === 'stopped') daemonBadge = chalk.yellow('Service stopped')
293
+ else if (daemonStatus === 'stale' || daemonStatus === 'unhealthy') daemonBadge = chalk.red('Service ' + daemonStatus)
294
+ else daemonBadge = chalk.dim('Service OFF')
295
+ const proxyDaemonRow = `${proxyDaemonBullet}${chalk.bold('FCM Proxy V2 settings →').padEnd(44)} ${proxyStatus} ${chalk.dim('•')} ${daemonBadge}`
296
+ cursorLineByRow[proxyDaemonRowIdx] = lines.length
297
+ lines.push(state.settingsCursor === proxyDaemonRowIdx ? chalk.bgRgb(20, 45, 60)(proxyDaemonRow) : proxyDaemonRow)
301
298
 
302
299
  // 📖 Changelog viewer row
303
300
  const changelogViewBullet = state.settingsCursor === changelogViewRowIdx ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
@@ -307,7 +304,7 @@ export function createOverlayRenderers(state, deps) {
307
304
 
308
305
  // 📖 Profiles section — list saved profiles with active indicator + delete support
309
306
  const savedProfiles = listProfiles(state.config)
310
- const profileStartIdx = updateRowIdx + 6
307
+ const profileStartIdx = updateRowIdx + 3
311
308
  const maxRowIdx = savedProfiles.length > 0 ? profileStartIdx + savedProfiles.length - 1 : updateRowIdx
312
309
 
313
310
  lines.push('')
@@ -377,7 +374,7 @@ export function createOverlayRenderers(state, deps) {
377
374
  }
378
375
 
379
376
  // ─── Install Endpoints overlay renderer ───────────────────────────────────
380
- // 📖 renderInstallEndpoints drives the provider → tool → scope → model flow
377
+ // 📖 renderInstallEndpoints drives the provider → tool → connection → scope → model flow
381
378
  // 📖 behind the `Y` hotkey. It deliberately reuses the same overlay viewport
382
379
  // 📖 helpers as Settings so long provider/model lists stay navigable.
383
380
  function renderInstallEndpoints() {
@@ -386,6 +383,8 @@ export function createOverlayRenderers(state, deps) {
386
383
  const cursorLineByRow = {}
387
384
  const providerChoices = getConfiguredInstallableProviders(state.config)
388
385
  const toolChoices = getInstallTargetModes()
386
+ const connectionChoices = CONNECTION_MODES || []
387
+ const totalSteps = 5
389
388
  const scopeChoices = [
390
389
  {
391
390
  key: 'all',
@@ -401,18 +400,22 @@ export function createOverlayRenderers(state, deps) {
401
400
  const selectedProviderLabel = state.installEndpointsProviderKey
402
401
  ? (sources[state.installEndpointsProviderKey]?.name || state.installEndpointsProviderKey)
403
402
  : '—'
403
+
404
+ // 📖 Resolve tool label from metadata instead of hard-coded switch
404
405
  const selectedToolLabel = state.installEndpointsToolMode
405
- ? (state.installEndpointsToolMode === 'opencode-desktop'
406
- ? 'OpenCode Desktop (shared opencode.json)'
407
- : (state.installEndpointsToolMode === 'opencode'
408
- ? 'OpenCode CLI (shared opencode.json)'
409
- : state.installEndpointsToolMode === 'openclaw'
410
- ? 'OpenClaw'
411
- : state.installEndpointsToolMode === 'crush'
412
- ? 'Crush'
413
- : 'Goose'))
406
+ ? (() => {
407
+ const meta = getToolMeta(state.installEndpointsToolMode)
408
+ const suffix = state.installEndpointsToolMode.startsWith('opencode') ? ' (shared opencode.json)' : ''
409
+ return `${meta.label}${suffix}`
410
+ })()
414
411
  : '—'
415
412
 
413
+ const selectedConnectionLabel = state.installEndpointsConnectionMode === 'proxy'
414
+ ? 'FCM Proxy V2'
415
+ : state.installEndpointsConnectionMode === 'direct'
416
+ ? 'Direct Provider'
417
+ : '—'
418
+
416
419
  lines.push('')
417
420
  // 📖 Branding header
418
421
  lines.push(` ${chalk.cyanBright('🚀')} ${chalk.bold.cyanBright('free-coding-models')} ${chalk.dim(`v${LOCAL_VERSION}`)}`)
@@ -425,7 +428,7 @@ export function createOverlayRenderers(state, deps) {
425
428
  lines.push('')
426
429
 
427
430
  if (state.installEndpointsPhase === 'providers') {
428
- lines.push(` ${chalk.bold('Step 1/4')} ${chalk.cyan('Choose a configured provider')}`)
431
+ lines.push(` ${chalk.bold(`Step 1/${totalSteps}`)} ${chalk.cyan('Choose a configured provider')}`)
429
432
  lines.push('')
430
433
 
431
434
  if (providerChoices.length === 0) {
@@ -435,7 +438,7 @@ export function createOverlayRenderers(state, deps) {
435
438
  providerChoices.forEach((provider, idx) => {
436
439
  const isCursor = idx === state.installEndpointsCursor
437
440
  const bullet = isCursor ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
438
- const row = `${bullet}${chalk.bold(provider.label.padEnd(24))} ${chalk.dim(`${provider.modelCount} models`)}`
441
+ const row = `${bullet}${chalk.bold(provider.label.padEnd(24))} ${chalk.dim(`${provider.modelCount} models`)}`
439
442
  cursorLineByRow[idx] = lines.length
440
443
  lines.push(isCursor ? chalk.bgRgb(24, 44, 62)(row) : row)
441
444
  })
@@ -444,37 +447,51 @@ export function createOverlayRenderers(state, deps) {
444
447
  lines.push('')
445
448
  lines.push(chalk.dim(' ↑↓ Navigate • Enter Choose provider • Esc Close'))
446
449
  } else if (state.installEndpointsPhase === 'tools') {
447
- lines.push(` ${chalk.bold('Step 2/4')} ${chalk.cyan('Choose the target tool')}`)
450
+ lines.push(` ${chalk.bold(`Step 2/${totalSteps}`)} ${chalk.cyan('Choose the target tool')}`)
448
451
  lines.push(chalk.dim(` Provider: ${selectedProviderLabel}`))
449
452
  lines.push('')
450
453
 
454
+ // 📖 Use getToolMeta for labels instead of hard-coded ternary chains
451
455
  toolChoices.forEach((toolMode, idx) => {
452
456
  const isCursor = idx === state.installEndpointsCursor
453
- const label = toolMode === 'opencode-desktop'
454
- ? 'OpenCode Desktop'
455
- : toolMode === 'opencode'
456
- ? 'OpenCode CLI'
457
- : toolMode === 'openclaw'
458
- ? 'OpenClaw'
459
- : toolMode === 'crush'
460
- ? 'Crush'
461
- : 'Goose'
457
+ const meta = getToolMeta(toolMode)
458
+ const label = `${meta.emoji} ${meta.label}`
462
459
  const note = toolMode.startsWith('opencode')
463
460
  ? chalk.dim('shared config file')
464
- : chalk.dim('managed provider install')
461
+ : ['claude-code', 'codex', 'openhands'].includes(toolMode)
462
+ ? chalk.dim('env file (~/.fcm-*-env)')
463
+ : chalk.dim('managed config install')
465
464
  const bullet = isCursor ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
466
- const row = `${bullet}${chalk.bold(label.padEnd(22))} ${note}`
465
+ const row = `${bullet}${chalk.bold(label.padEnd(26))} ${note}`
467
466
  cursorLineByRow[idx] = lines.length
468
467
  lines.push(isCursor ? chalk.bgRgb(24, 44, 62)(row) : row)
469
468
  })
470
469
 
471
470
  lines.push('')
472
471
  lines.push(chalk.dim(' ↑↓ Navigate • Enter Choose tool • Esc Back'))
473
- } else if (state.installEndpointsPhase === 'scope') {
474
- lines.push(` ${chalk.bold('Step 3/4')} ${chalk.cyan('Choose the install scope')}`)
472
+ } else if (state.installEndpointsPhase === 'connection') {
473
+ // 📖 Step 3: Choose connection mode — Direct Provider vs FCM Proxy
474
+ lines.push(` ${chalk.bold(`Step 3/${totalSteps}`)} ${chalk.cyan('Choose connection mode')}`)
475
475
  lines.push(chalk.dim(` Provider: ${selectedProviderLabel} • Tool: ${selectedToolLabel}`))
476
476
  lines.push('')
477
477
 
478
+ connectionChoices.forEach((mode, idx) => {
479
+ const isCursor = idx === state.installEndpointsCursor
480
+ const bullet = isCursor ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
481
+ const icon = mode.key === 'proxy' ? '🔄' : '⚡'
482
+ const row = `${bullet}${icon} ${chalk.bold(mode.label)}`
483
+ cursorLineByRow[idx] = lines.length
484
+ lines.push(isCursor ? chalk.bgRgb(24, 44, 62)(row) : row)
485
+ lines.push(chalk.dim(` ${mode.hint}`))
486
+ lines.push('')
487
+ })
488
+
489
+ lines.push(chalk.dim(' Enter Continue • Esc Back'))
490
+ } else if (state.installEndpointsPhase === 'scope') {
491
+ lines.push(` ${chalk.bold(`Step 4/${totalSteps}`)} ${chalk.cyan('Choose the install scope')}`)
492
+ lines.push(chalk.dim(` Provider: ${selectedProviderLabel} • Tool: ${selectedToolLabel} • ${selectedConnectionLabel}`))
493
+ lines.push('')
494
+
478
495
  scopeChoices.forEach((scope, idx) => {
479
496
  const isCursor = idx === state.installEndpointsCursor
480
497
  const bullet = isCursor ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
@@ -490,8 +507,8 @@ export function createOverlayRenderers(state, deps) {
490
507
  const models = getProviderCatalogModels(state.installEndpointsProviderKey)
491
508
  const selectedCount = state.installEndpointsSelectedModelIds.size
492
509
 
493
- lines.push(` ${chalk.bold('Step 4/4')} ${chalk.cyan('Choose which models to install')}`)
494
- lines.push(chalk.dim(` Provider: ${selectedProviderLabel} • Tool: ${selectedToolLabel}`))
510
+ lines.push(` ${chalk.bold(`Step 5/${totalSteps}`)} ${chalk.cyan('Choose which models to install')}`)
511
+ lines.push(chalk.dim(` Provider: ${selectedProviderLabel} • Tool: ${selectedToolLabel} • ${selectedConnectionLabel}`))
495
512
  lines.push(chalk.dim(` Selected: ${selectedCount}/${models.length}`))
496
513
  lines.push('')
497
514
 
@@ -608,12 +625,12 @@ export function createOverlayRenderers(state, deps) {
608
625
  lines.push(` ${chalk.yellow('W')} Toggle ping mode ${chalk.dim('(speed 2s → normal 10s → slow 30s → forced 4s)')}`)
609
626
  lines.push(` ${chalk.yellow('E')} Toggle configured models only ${chalk.dim('(enabled by default, persisted globally + in profiles)')}`)
610
627
  lines.push(` ${chalk.yellow('X')} Toggle token log page ${chalk.dim('(shows recent request usage from request-log.jsonl)')}`)
611
- lines.push(` ${chalk.yellow('Z')} Cycle tool mode ${chalk.dim('(OpenCode → Desktop → OpenClaw → Crush → Goose)')}`)
628
+ lines.push(` ${chalk.yellow('Z')} Cycle tool mode ${chalk.dim('(OpenCode → Desktop → OpenClaw → Crush → Goose → Pi → Aider → Claude Code → Codex → Gemini → Qwen → OpenHands → Amp)')}`)
612
629
  lines.push(` ${chalk.yellow('F')} Toggle favorite on selected row ${chalk.dim('(⭐ pinned at top, persisted)')}`)
613
- lines.push(` ${chalk.yellow('Y')} Install endpoints ${chalk.dim('(provider catalog → OpenCode/OpenClaw/Crush/Goose, no proxy)')}`)
630
+ lines.push(` ${chalk.yellow('Y')} Install endpoints ${chalk.dim('(provider catalog → all tools, Direct or FCM Proxy V2)')}`)
614
631
  lines.push(` ${chalk.yellow('Q')} Smart Recommend ${chalk.dim('(🎯 find the best model for your task — questionnaire + live analysis)')}`)
615
- lines.push(` ${chalk.rgb(57, 255, 20).bold('J')} Request Feature ${chalk.dim('(📝 send anonymous feedback to the project team)')}`)
616
- lines.push(` ${chalk.rgb(255, 87, 51).bold('I')} Report Bug ${chalk.dim('(🐛 send anonymous bug report to the project team)')}`)
632
+ lines.push(` ${chalk.rgb(255, 87, 51).bold('I')} Feedback, bugs & requests ${chalk.dim('(📝 send anonymous feedback, bug reports, or feature requests)')}`)
633
+ lines.push(` ${chalk.yellow('J')} FCM Proxy V2 settings ${chalk.dim('(📡 open proxy configuration and background service management)')}`)
617
634
  lines.push(` ${chalk.yellow('P')} Open settings ${chalk.dim('(manage API keys, provider toggles, proxy, manual update)')}`)
618
635
  lines.push(` ${chalk.yellow('Shift+P')} Cycle config profile ${chalk.dim('(switch between saved profiles live)')}`)
619
636
  lines.push(` ${chalk.yellow('Shift+S')} Save current config as a named profile ${chalk.dim('(inline prompt — type name + Enter)')}`)
@@ -641,15 +658,14 @@ export function createOverlayRenderers(state, deps) {
641
658
  lines.push(` ${chalk.cyan('free-coding-models --openclaw')} ${chalk.dim('OpenClaw mode')}`)
642
659
  lines.push(` ${chalk.cyan('free-coding-models --crush')} ${chalk.dim('Crush mode')}`)
643
660
  lines.push(` ${chalk.cyan('free-coding-models --goose')} ${chalk.dim('Goose mode')}`)
644
- // 📖 Temporarily disabled launchers kept out of the public help until their flows are hardened.
645
- // lines.push(` ${chalk.cyan('free-coding-models --aider')} ${chalk.dim('Aider mode')}`)
646
- // lines.push(` ${chalk.cyan('free-coding-models --claude-code')} ${chalk.dim('Claude Code proxy mode')}`)
647
- // lines.push(` ${chalk.cyan('free-coding-models --codex')} ${chalk.dim('Codex CLI proxy mode')}`)
648
- // lines.push(` ${chalk.cyan('free-coding-models --gemini')} ${chalk.dim('Gemini CLI proxy mode')}`)
649
- // lines.push(` ${chalk.cyan('free-coding-models --qwen')} ${chalk.dim('Qwen Code mode')}`)
650
- // lines.push(` ${chalk.cyan('free-coding-models --openhands')} ${chalk.dim('OpenHands mode')}`)
651
- // lines.push(` ${chalk.cyan('free-coding-models --amp')} ${chalk.dim('Amp mode')}`)
652
- // lines.push(` ${chalk.cyan('free-coding-models --pi')} ${chalk.dim('Pi mode')}`)
661
+ lines.push(` ${chalk.cyan('free-coding-models --pi')} ${chalk.dim('Pi mode')}`)
662
+ lines.push(` ${chalk.cyan('free-coding-models --aider')} ${chalk.dim('Aider mode')}`)
663
+ lines.push(` ${chalk.cyan('free-coding-models --claude-code')} ${chalk.dim('Claude Code mode')}`)
664
+ lines.push(` ${chalk.cyan('free-coding-models --codex')} ${chalk.dim('Codex CLI mode')}`)
665
+ lines.push(` ${chalk.cyan('free-coding-models --gemini')} ${chalk.dim('Gemini CLI mode')}`)
666
+ lines.push(` ${chalk.cyan('free-coding-models --qwen')} ${chalk.dim('Qwen Code mode')}`)
667
+ lines.push(` ${chalk.cyan('free-coding-models --openhands')} ${chalk.dim('OpenHands mode')}`)
668
+ lines.push(` ${chalk.cyan('free-coding-models --amp')} ${chalk.dim('Amp mode')}`)
653
669
  lines.push(` ${chalk.cyan('free-coding-models --best')} ${chalk.dim('Only top tiers (A+, S, S+)')}`)
654
670
  lines.push(` ${chalk.cyan('free-coding-models --fiable')} ${chalk.dim('10s reliability analysis')}`)
655
671
  lines.push(` ${chalk.cyan('free-coding-models --tier S|A|B|C')} ${chalk.dim('Filter by tier letter')}`)
@@ -1036,117 +1052,10 @@ export function createOverlayRenderers(state, deps) {
1036
1052
  }, PING_RATE)
1037
1053
  }
1038
1054
 
1039
- // ─── Feature Request overlay renderer ─────────────────────────────────────
1040
- // 📖 renderFeatureRequest: Draw the overlay for anonymous Discord feedback.
1041
- // 📖 Shows an input field where users can type feature requests, then sends to Discord webhook.
1042
- function renderFeatureRequest() {
1043
- const EL = '\x1b[K'
1044
- const lines = []
1045
-
1046
- // 📖 Calculate available space for multi-line input (dynamic based on terminal width)
1047
- const maxInputWidth = state.terminalCols - 8 // 8 = padding (4 spaces each side)
1048
- const maxInputLines = 10 // Show up to 10 lines of input
1049
-
1050
- // 📖 Split buffer into lines for display (with wrapping)
1051
- const wrapText = (text, width) => {
1052
- const words = text.split(' ')
1053
- const lines = []
1054
- let currentLine = ''
1055
-
1056
- for (const word of words) {
1057
- const testLine = currentLine ? currentLine + ' ' + word : word
1058
- if (testLine.length <= width) {
1059
- currentLine = testLine
1060
- } else {
1061
- if (currentLine) lines.push(currentLine)
1062
- currentLine = word
1063
- }
1064
- }
1065
- if (currentLine) lines.push(currentLine)
1066
- return lines
1067
- }
1068
-
1069
- const inputLines = wrapText(state.featureRequestBuffer, maxInputWidth)
1070
- const displayLines = inputLines.slice(0, maxInputLines)
1071
-
1072
- // 📖 Branding header
1073
- lines.push('')
1074
- lines.push(` ${chalk.cyanBright('🚀')} ${chalk.bold.cyanBright('free-coding-models')} ${chalk.dim(`v${LOCAL_VERSION}`)}`)
1075
- lines.push(` ${chalk.bold.rgb(57, 255, 20)('📝 Feature Request')}`)
1076
- lines.push('')
1077
- lines.push(chalk.dim(' — send anonymous feedback to the project team'))
1078
- lines.push('')
1079
-
1080
- // 📖 Status messages (if any)
1081
- if (state.featureRequestStatus === 'sending') {
1082
- lines.push(` ${chalk.yellow('⏳ Sending...')}`)
1083
- lines.push('')
1084
- } else if (state.featureRequestStatus === 'success') {
1085
- lines.push(` ${chalk.greenBright.bold('✅ Successfully sent!')} ${chalk.dim('Closing overlay in 3 seconds...')}`)
1086
- lines.push('')
1087
- lines.push(` ${chalk.dim('Thank you for your feedback! Your feature request has been sent to the project team.')}`)
1088
- lines.push('')
1089
- } else if (state.featureRequestStatus === 'error') {
1090
- lines.push(` ${chalk.red('❌ Error:')} ${chalk.yellow(state.featureRequestError || 'Failed to send')}`)
1091
- lines.push(` ${chalk.dim('Press Backspace to edit, or Esc to close')}`)
1092
- lines.push('')
1093
- } else {
1094
- lines.push(` ${chalk.dim('Type your feature request below. Press Enter to send, Esc to cancel.')}`)
1095
- lines.push(` ${chalk.dim('Your message will be sent anonymously to the project team.')}`)
1096
- lines.push('')
1097
- }
1098
-
1099
- // 📖 Input box with border
1100
- lines.push(chalk.dim(` ┌─ ${chalk.cyan('Message')} ${chalk.dim(`(${state.featureRequestBuffer.length}/500 chars)`)} ─${'─'.repeat(maxInputWidth - 22)}┐`))
1101
-
1102
- // 📖 Display input lines (or placeholder if empty)
1103
- if (displayLines.length === 0 && state.featureRequestStatus === 'idle') {
1104
- lines.push(chalk.dim(` │${' '.repeat(maxInputWidth)}│`))
1105
- lines.push(chalk.dim(` │ ${chalk.white.italic('Type your message here...')}${' '.repeat(Math.max(0, maxInputWidth - 28))}│`))
1106
- } else {
1107
- for (const line of displayLines) {
1108
- const padded = line.padEnd(maxInputWidth)
1109
- lines.push(` │ ${chalk.white(padded)} │`)
1110
- }
1111
- }
1112
-
1113
- // 📖 Fill remaining space if needed
1114
- const linesToFill = Math.max(0, maxInputLines - Math.max(displayLines.length, 1))
1115
- for (let i = 0; i < linesToFill; i++) {
1116
- lines.push(chalk.dim(` │${' '.repeat(maxInputWidth)}│`))
1117
- }
1118
-
1119
- // 📖 Cursor indicator (only when not sending/success)
1120
- if (state.featureRequestStatus === 'idle' || state.featureRequestStatus === 'error') {
1121
- // Add cursor indicator to the last line
1122
- if (lines.length > 0 && displayLines.length > 0) {
1123
- const lastLineIdx = lines.findIndex(l => l.includes('│ ') && !l.includes('Message'))
1124
- if (lastLineIdx >= 0 && lastLineIdx < lines.length) {
1125
- // Add cursor blink
1126
- const lastLine = lines[lastLineIdx]
1127
- if (lastLine.includes('│')) {
1128
- lines[lastLineIdx] = lastLine.replace(/\s+│$/, chalk.rgb(57, 255, 20).bold('▏') + ' │')
1129
- }
1130
- }
1131
- }
1132
- }
1133
-
1134
- lines.push(chalk.dim(` └${'─'.repeat(maxInputWidth + 2)}┘`))
1135
-
1136
- lines.push('')
1137
- lines.push(chalk.dim(' Enter Send • Esc Cancel • Backspace Delete'))
1138
-
1139
- // 📖 Apply overlay tint and return
1140
- const FEATURE_REQUEST_OVERLAY_BG = chalk.bgRgb(0, 0, 0) // Dark blue-ish background (RGB: 26, 26, 46)
1141
- const tintedLines = tintOverlayLines(lines, FEATURE_REQUEST_OVERLAY_BG, state.terminalCols)
1142
- const cleared = tintedLines.map(l => l + EL)
1143
- return cleared.join('\n')
1144
- }
1145
-
1146
- // ─── Bug Report overlay renderer ─────────────────────────────────────────
1147
- // 📖 renderBugReport: Draw the overlay for anonymous Discord bug reports.
1148
- // 📖 Shows an input field where users can type bug reports, then sends to Discord webhook.
1149
- function renderBugReport() {
1055
+ // ─── Feedback overlay renderer ────────────────────────────────────────────
1056
+ // 📖 renderFeedback: Draw the overlay for anonymous Discord feedback.
1057
+ // 📖 Shows an input field where users can type feedback, bug reports, or any comments.
1058
+ function renderFeedback() {
1150
1059
  const EL = '\x1b[K'
1151
1060
  const lines = []
1152
1061
 
@@ -1179,9 +1088,9 @@ export function createOverlayRenderers(state, deps) {
1179
1088
  // 📖 Branding header
1180
1089
  lines.push('')
1181
1090
  lines.push(` ${chalk.cyanBright('🚀')} ${chalk.bold.cyanBright('free-coding-models')} ${chalk.dim(`v${LOCAL_VERSION}`)}`)
1182
- lines.push(` ${chalk.bold.rgb(255, 87, 51)('🐛 Bug Report')}`)
1091
+ lines.push(` ${chalk.bold.rgb(57, 255, 20)('📝 Feedback, bugs & requests')}`)
1183
1092
  lines.push('')
1184
- lines.push(chalk.dim(' — send anonymous bug reports to the project team'))
1093
+ lines.push(chalk.dim("don't hesitate to send us feedback, bug reports, or just your feeling about the app"))
1185
1094
  lines.push('')
1186
1095
 
1187
1096
  // 📖 Status messages (if any)
@@ -1191,55 +1100,35 @@ export function createOverlayRenderers(state, deps) {
1191
1100
  } else if (state.bugReportStatus === 'success') {
1192
1101
  lines.push(` ${chalk.greenBright.bold('✅ Successfully sent!')} ${chalk.dim('Closing overlay in 3 seconds...')}`)
1193
1102
  lines.push('')
1194
- lines.push(` ${chalk.dim('Thank you for your feedback! Your bug report has been sent to the project team.')}`)
1103
+ lines.push(` ${chalk.dim('Thank you for your feedback! It has been sent to the project team.')}`)
1195
1104
  lines.push('')
1196
1105
  } else if (state.bugReportStatus === 'error') {
1197
1106
  lines.push(` ${chalk.red('❌ Error:')} ${chalk.yellow(state.bugReportError || 'Failed to send')}`)
1198
1107
  lines.push(` ${chalk.dim('Press Backspace to edit, or Esc to close')}`)
1199
1108
  lines.push('')
1200
1109
  } else {
1201
- lines.push(` ${chalk.dim('Describe the bug you encountered. Press Enter to send, Esc to cancel.')}`)
1110
+ lines.push(` ${chalk.dim('Type your feedback below. Press Enter to send, Esc to cancel.')}`)
1202
1111
  lines.push(` ${chalk.dim('Your message will be sent anonymously to the project team.')}`)
1203
1112
  lines.push('')
1204
1113
  }
1205
1114
 
1206
- // 📖 Input box with border
1207
- lines.push(chalk.dim(` ┌─ ${chalk.cyan('Bug Details')} ${chalk.dim(`(${state.bugReportBuffer.length}/500 chars)`)} ─${'─'.repeat(maxInputWidth - 24)}┐`))
1208
-
1209
- // 📖 Display input lines (or placeholder if empty)
1210
- if (displayLines.length === 0 && state.bugReportStatus === 'idle') {
1211
- lines.push(chalk.dim(` │${' '.repeat(maxInputWidth)}│`))
1212
- lines.push(chalk.dim(` │ ${chalk.white.italic('Describe what happened...')}${' '.repeat(Math.max(0, maxInputWidth - 31))}│`))
1213
- } else {
1115
+ // 📖 Simple input area – left-aligned, framed by horizontal lines
1116
+ lines.push(` ${chalk.cyan('Message')} (${state.bugReportBuffer.length}/500 chars)`)
1117
+ lines.push(` ${chalk.dim('─'.repeat(maxInputWidth))}`)
1118
+ // 📖 Input lines — left-aligned, or placeholder when empty
1119
+ if (displayLines.length > 0) {
1214
1120
  for (const line of displayLines) {
1215
- const padded = line.padEnd(maxInputWidth)
1216
- lines.push(` │ ${chalk.white(padded)} │`)
1121
+ lines.push(` ${line}`)
1217
1122
  }
1218
- }
1219
-
1220
- // 📖 Fill remaining space if needed
1221
- const linesToFill = Math.max(0, maxInputLines - Math.max(displayLines.length, 1))
1222
- for (let i = 0; i < linesToFill; i++) {
1223
- lines.push(chalk.dim(` │${' '.repeat(maxInputWidth)}│`))
1224
- }
1225
-
1226
- // 📖 Cursor indicator (only when not sending/success)
1227
- if (state.bugReportStatus === 'idle' || state.bugReportStatus === 'error') {
1228
- // Add cursor indicator to the last line
1229
- if (lines.length > 0 && displayLines.length > 0) {
1230
- const lastLineIdx = lines.findIndex(l => l.includes('│ ') && !l.includes('Bug Details'))
1231
- if (lastLineIdx >= 0 && lastLineIdx < lines.length) {
1232
- // Add cursor blink
1233
- const lastLine = lines[lastLineIdx]
1234
- if (lastLine.includes('│')) {
1235
- lines[lastLineIdx] = lastLine.replace(/\s+│$/, chalk.rgb(255, 87, 51).bold('▏') + ' │')
1236
- }
1237
- }
1123
+ // 📖 Show cursor on last line
1124
+ if (state.bugReportStatus === 'idle' || state.bugReportStatus === 'error') {
1125
+ lines[lines.length - 1] += chalk.cyanBright('▏')
1238
1126
  }
1127
+ } else {
1128
+ const placeholderBR = state.bugReportStatus === 'idle' ? chalk.white.italic('Type your message here...') : ''
1129
+ lines.push(` ${placeholderBR}${chalk.cyanBright('▏')}`)
1239
1130
  }
1240
-
1241
- lines.push(chalk.dim(` └${'─'.repeat(maxInputWidth + 2)}┘`))
1242
-
1131
+ lines.push(` ${chalk.dim('─'.repeat(maxInputWidth))}`)
1243
1132
  lines.push('')
1244
1133
  lines.push(chalk.dim(' Enter Send • Esc Cancel • Backspace Delete'))
1245
1134
 
@@ -1290,12 +1179,32 @@ export function createOverlayRenderers(state, deps) {
1290
1179
  if (changes[key]) itemCount += changes[key].length
1291
1180
  }
1292
1181
 
1293
- // 📖 Format version line with selection highlight
1294
- const versionStr = ` v${version.padEnd(8)} — ${itemCount} ${itemCount === 1 ? 'change' : 'changes'}`
1182
+ // 📖 Build a short summary from the first few items (max ~15 words, stripped of markdown)
1183
+ const allItems = []
1184
+ for (const k of ['added', 'fixed', 'changed', 'updated']) {
1185
+ if (changes[k]) for (const item of changes[k]) allItems.push(item)
1186
+ }
1187
+ let summary = ''
1188
+ if (allItems.length > 0) {
1189
+ // 📖 Extract the bold title part if present, otherwise use the raw text
1190
+ const firstItem = allItems[0]
1191
+ const boldMatch = firstItem.match(/\*\*([^*]+)\*\*/)
1192
+ const rawText = boldMatch ? boldMatch[1] : firstItem.replace(/\*\*([^*]+)\*\*/g, '$1').replace(/`([^`]+)`/g, '$1')
1193
+ // 📖 Truncate to ~15 words max
1194
+ const words = rawText.split(/\s+/).slice(0, 15)
1195
+ summary = words.join(' ')
1196
+ if (rawText.split(/\s+/).length > 15) summary += '…'
1197
+ }
1198
+
1199
+ // 📖 Format version line with selection highlight + dim summary
1200
+ const countStr = `${itemCount} ${itemCount === 1 ? 'change' : 'changes'}`
1201
+ const prefix = ` v${version.padEnd(8)} — ${countStr}`
1295
1202
  if (isSelected) {
1296
- lines.push(chalk.inverse(versionStr))
1203
+ const full = summary ? `${prefix} · ${summary}` : prefix
1204
+ lines.push(chalk.inverse(full))
1297
1205
  } else {
1298
- lines.push(versionStr)
1206
+ const dimSummary = summary ? chalk.dim(` · ${summary}`) : ''
1207
+ lines.push(`${prefix}${dimSummary}`)
1299
1208
  }
1300
1209
  }
1301
1210
 
@@ -1332,6 +1241,17 @@ export function createOverlayRenderers(state, deps) {
1332
1241
  }
1333
1242
  }
1334
1243
 
1244
+ // 📖 Keep selected changelog row visible by scrolling the overlay viewport (index phase)
1245
+ if (state.changelogPhase === 'index') {
1246
+ const targetLine = 4 + state.changelogCursor // 📖 3 header lines + 1 blank = versions start at line 4
1247
+ state.changelogScrollOffset = keepOverlayTargetVisible(
1248
+ state.changelogScrollOffset,
1249
+ targetLine,
1250
+ lines.length,
1251
+ state.terminalRows
1252
+ )
1253
+ }
1254
+
1335
1255
  // 📖 Use scrolling with overlay handler
1336
1256
  const CHANGELOG_OVERLAY_BG = chalk.bgRgb(10, 40, 80) // Dark blue background
1337
1257
  const { visible, offset } = sliceOverlayLines(lines, state.changelogScrollOffset, state.terminalRows)
@@ -1341,6 +1261,226 @@ export function createOverlayRenderers(state, deps) {
1341
1261
  return cleared.join('\n')
1342
1262
  }
1343
1263
 
1264
+ // ─── FCM Proxy V2 overlay renderer ──────────────────────────────────────────
1265
+ // 📖 renderProxyDaemon: Dedicated full-page overlay for FCM Proxy V2 configuration
1266
+ // 📖 and background service management. Opened from Settings → "FCM Proxy V2 settings →".
1267
+ // 📖 Contains all proxy toggles, service status/actions, explanations, and emergency kill.
1268
+ function renderProxyDaemon() {
1269
+ const EL = '\x1b[K'
1270
+ const lines = []
1271
+ const cursorLineByRow = {}
1272
+ const proxySettings = getProxySettings(state.config)
1273
+
1274
+ // 📖 Row indices — these control cursor navigation
1275
+ const ROW_PROXY_ENABLED = 0
1276
+ const ROW_PROXY_TOOL = 1
1277
+ const ROW_PROXY_SYNC = 2
1278
+ const ROW_PROXY_PORT = 3
1279
+ const ROW_PROXY_CLEANUP = 4
1280
+ const ROW_DAEMON_INSTALL = 5
1281
+ const ROW_DAEMON_RESTART = 6
1282
+ const ROW_DAEMON_STOP = 7
1283
+ const ROW_DAEMON_KILL = 8
1284
+ const ROW_DAEMON_LOGS = 9
1285
+
1286
+ const daemonStatus = state.daemonStatus || 'not-installed'
1287
+ const daemonInfo = state.daemonInfo
1288
+ const daemonIsActive = daemonStatus === 'running' || daemonStatus === 'unhealthy' || daemonStatus === 'stale'
1289
+ const daemonIsInstalled = daemonIsActive || daemonStatus === 'stopped'
1290
+
1291
+ // 📖 Compute max row — hide daemon action rows when daemon not installed
1292
+ let maxRow = ROW_DAEMON_INSTALL
1293
+ if (daemonIsInstalled) maxRow = ROW_DAEMON_LOGS
1294
+
1295
+ // 📖 Header
1296
+ lines.push(` ${chalk.cyanBright('🚀')} ${chalk.bold.cyanBright('free-coding-models')} ${chalk.dim(`v${LOCAL_VERSION}`)}`)
1297
+ lines.push(` ${chalk.bold('📡 FCM Proxy V2 Manager')}`)
1298
+ lines.push(` ${chalk.dim('— Esc back to Settings • ↑↓ navigate • Enter select')}`)
1299
+ lines.push('')
1300
+ lines.push(` ${chalk.bgRed.white.bold(' ⚠ EXPERIMENTAL ')} ${chalk.red('This feature is under active development and may not work as expected.')}`)
1301
+ 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.')}`)
1302
+ lines.push('')
1303
+
1304
+ // 📖 Feedback message (auto-clears after 5s)
1305
+ const msg = state.proxyDaemonMessage
1306
+ if (msg && (Date.now() - msg.ts < 5000)) {
1307
+ const msgColor = msg.type === 'success' ? chalk.greenBright : msg.type === 'warning' ? chalk.yellow : chalk.red
1308
+ lines.push(` ${msgColor(msg.msg)}`)
1309
+ lines.push('')
1310
+ }
1311
+
1312
+ // ────────────────────────────── PROXY SECTION ──────────────────────────────
1313
+ lines.push(` ${chalk.bold('🔀 Proxy Configuration')}`)
1314
+ lines.push(` ${chalk.dim(' ─────────────────────────────────────────────')}`)
1315
+ lines.push('')
1316
+ lines.push(` ${chalk.dim(' The local proxy groups all your provider API keys into a single')}`)
1317
+ lines.push(` ${chalk.dim(' endpoint. Tools like OpenCode, Claude Code, Goose, etc. connect')}`)
1318
+ lines.push(` ${chalk.dim(' to this proxy which handles key rotation, rate limiting, and failover.')}`)
1319
+ lines.push('')
1320
+
1321
+ // 📖 Resolve active tool for proxy sync (persisted or fallback to Z-mode)
1322
+ const activeProxyTool = proxySettings.activeTool || state.mode || 'opencode'
1323
+ const activeToolMeta = getToolMeta(activeProxyTool)
1324
+ const activeToolLabel = `${activeToolMeta.emoji} ${activeToolMeta.label}`
1325
+
1326
+ // 📖 Row 0: Proxy enabled toggle
1327
+ const r0b = state.proxyDaemonCursor === ROW_PROXY_ENABLED ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
1328
+ const r0val = proxySettings.enabled ? chalk.greenBright('Enabled') : chalk.dim('Disabled (opt-in)')
1329
+ const r0 = `${r0b}${chalk.bold('Proxy mode').padEnd(44)} ${r0val}`
1330
+ cursorLineByRow[ROW_PROXY_ENABLED] = lines.length
1331
+ lines.push(state.proxyDaemonCursor === ROW_PROXY_ENABLED ? chalk.bgRgb(20, 45, 60)(r0) : r0)
1332
+
1333
+ // 📖 Row 1: Active tool selector — cycles through proxy-syncable tools
1334
+ const r1b = state.proxyDaemonCursor === ROW_PROXY_TOOL ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
1335
+ const r1 = `${r1b}${chalk.bold('Active tool').padEnd(44)} ${chalk.cyanBright(activeToolLabel)} ${chalk.dim('← Enter to cycle')}`
1336
+ cursorLineByRow[ROW_PROXY_TOOL] = lines.length
1337
+ lines.push(state.proxyDaemonCursor === ROW_PROXY_TOOL ? chalk.bgRgb(20, 45, 60)(r1) : r1)
1338
+
1339
+ // 📖 Row 2: Auto-sync proxy config to active tool
1340
+ const r2b = state.proxyDaemonCursor === ROW_PROXY_SYNC ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
1341
+ const r2val = proxySettings.syncToOpenCode ? chalk.greenBright('Enabled') : chalk.dim('Disabled')
1342
+ const r2 = `${r2b}${chalk.bold(`Auto-sync proxy to ${activeToolMeta.label}`).padEnd(44)} ${r2val}`
1343
+ cursorLineByRow[ROW_PROXY_SYNC] = lines.length
1344
+ lines.push(state.proxyDaemonCursor === ROW_PROXY_SYNC ? chalk.bgRgb(20, 45, 60)(r2) : r2)
1345
+
1346
+ // 📖 Row 3: Preferred port
1347
+ const r3b = state.proxyDaemonCursor === ROW_PROXY_PORT ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
1348
+ const r3val = state.settingsProxyPortEditMode && state.proxyDaemonCursor === ROW_PROXY_PORT
1349
+ ? chalk.cyanBright(`${state.settingsProxyPortBuffer}▏`)
1350
+ : (proxySettings.preferredPort === 0 ? chalk.dim('auto (OS-assigned)') : chalk.green(String(proxySettings.preferredPort)))
1351
+ const r3 = `${r3b}${chalk.bold('Preferred proxy port').padEnd(44)} ${r3val}`
1352
+ cursorLineByRow[ROW_PROXY_PORT] = lines.length
1353
+ lines.push(state.proxyDaemonCursor === ROW_PROXY_PORT ? chalk.bgRgb(20, 45, 60)(r3) : r3)
1354
+
1355
+ // 📖 Row 4: Clean tool proxy config
1356
+ const r4b = state.proxyDaemonCursor === ROW_PROXY_CLEANUP ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
1357
+ const r4 = `${r4b}${chalk.bold(`Clean ${activeToolMeta.label} proxy config`).padEnd(44)} ${chalk.dim('Enter → removes all fcm-* entries')}`
1358
+ cursorLineByRow[ROW_PROXY_CLEANUP] = lines.length
1359
+ lines.push(state.proxyDaemonCursor === ROW_PROXY_CLEANUP ? chalk.bgRgb(45, 30, 30)(r4) : r4)
1360
+
1361
+ // ────────────────────────────── DAEMON SECTION ─────────────────────────────
1362
+ lines.push('')
1363
+ lines.push(` ${chalk.bold('📡 FCM Proxy V2 Background Service')}`)
1364
+ lines.push(` ${chalk.dim(' ─────────────────────────────────────────────')}`)
1365
+ lines.push('')
1366
+ lines.push(` ${chalk.dim(' The background service keeps FCM Proxy V2 running 24/7 — even when')}`)
1367
+ lines.push(` ${chalk.dim(' the TUI is closed or after a reboot. Claude Code, Gemini CLI, and')}`)
1368
+ lines.push(` ${chalk.dim(' all tools stay connected at all times.')}`)
1369
+ lines.push('')
1370
+
1371
+ // 📖 Status display
1372
+ let daemonStatusLine = ` ${chalk.bold(' Status:')} `
1373
+ if (daemonStatus === 'running') {
1374
+ daemonStatusLine += chalk.greenBright('● Running')
1375
+ if (daemonInfo) daemonStatusLine += chalk.dim(` — PID ${daemonInfo.pid} • Port ${daemonInfo.port} • ${daemonInfo.accountCount || '?'} accounts • ${daemonInfo.modelCount || '?'} models`)
1376
+ } else if (daemonStatus === 'stopped') {
1377
+ daemonStatusLine += chalk.yellow('○ Stopped') + chalk.dim(' — service installed but not running')
1378
+ } else if (daemonStatus === 'stale') {
1379
+ daemonStatusLine += chalk.red('⚠ Stale') + chalk.dim(' — service crashed, PID no longer alive')
1380
+ } else if (daemonStatus === 'unhealthy') {
1381
+ daemonStatusLine += chalk.red('⚠ Unhealthy') + chalk.dim(' — PID alive but health check failed')
1382
+ } else {
1383
+ daemonStatusLine += chalk.dim('○ Not installed')
1384
+ }
1385
+ lines.push(daemonStatusLine)
1386
+
1387
+ // 📖 Version mismatch warning
1388
+ if (daemonInfo?.version && daemonInfo.version !== LOCAL_VERSION) {
1389
+ lines.push(` ${chalk.yellow(` ⚠ Version mismatch: service v${daemonInfo.version} vs FCM v${LOCAL_VERSION}`)}`)
1390
+ lines.push(` ${chalk.dim(' Restart or reinstall the service to apply the update.')}`)
1391
+ }
1392
+
1393
+ // 📖 Uptime
1394
+ if (daemonStatus === 'running' && daemonInfo?.startedAt) {
1395
+ const upSec = Math.floor((Date.now() - new Date(daemonInfo.startedAt).getTime()) / 1000)
1396
+ const upMin = Math.floor(upSec / 60)
1397
+ const upHr = Math.floor(upMin / 60)
1398
+ const uptimeStr = upHr > 0 ? `${upHr}h ${upMin % 60}m` : upMin > 0 ? `${upMin}m ${upSec % 60}s` : `${upSec}s`
1399
+ lines.push(` ${chalk.dim(` Uptime: ${uptimeStr}`)}`)
1400
+ }
1401
+
1402
+ lines.push('')
1403
+
1404
+ // 📖 Row 5: Install / Uninstall
1405
+ const d0b = state.proxyDaemonCursor === ROW_DAEMON_INSTALL ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
1406
+ const d0label = daemonIsInstalled ? 'Uninstall service' : 'Install background service'
1407
+ const d0hint = daemonIsInstalled
1408
+ ? chalk.dim('Enter → stop service + remove config')
1409
+ : chalk.dim('Enter → install as OS service (launchd/systemd)')
1410
+ const d0 = `${d0b}${chalk.bold(d0label).padEnd(44)} ${d0hint}`
1411
+ cursorLineByRow[ROW_DAEMON_INSTALL] = lines.length
1412
+ lines.push(state.proxyDaemonCursor === ROW_DAEMON_INSTALL ? chalk.bgRgb(daemonIsInstalled ? 45 : 20, daemonIsInstalled ? 30 : 45, daemonIsInstalled ? 30 : 40)(d0) : d0)
1413
+
1414
+ // 📖 Rows 6-9 only shown when service is installed
1415
+ if (daemonIsInstalled) {
1416
+ // 📖 Row 6: Restart
1417
+ const d1b = state.proxyDaemonCursor === ROW_DAEMON_RESTART ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
1418
+ const d1 = `${d1b}${chalk.bold('Restart service').padEnd(44)} ${chalk.dim('Enter → stop + start via OS service manager')}`
1419
+ cursorLineByRow[ROW_DAEMON_RESTART] = lines.length
1420
+ lines.push(state.proxyDaemonCursor === ROW_DAEMON_RESTART ? chalk.bgRgb(20, 45, 60)(d1) : d1)
1421
+
1422
+ // 📖 Row 7: Stop (SIGTERM)
1423
+ const d2b = state.proxyDaemonCursor === ROW_DAEMON_STOP ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
1424
+ const d2warn = chalk.dim(' (service may auto-restart)')
1425
+ const d2 = `${d2b}${chalk.bold('Stop service').padEnd(44)} ${chalk.dim('Enter → graceful shutdown (SIGTERM)')}${d2warn}`
1426
+ cursorLineByRow[ROW_DAEMON_STOP] = lines.length
1427
+ lines.push(state.proxyDaemonCursor === ROW_DAEMON_STOP ? chalk.bgRgb(45, 40, 20)(d2) : d2)
1428
+
1429
+ // 📖 Row 8: Force kill (SIGKILL) — emergency
1430
+ const d3b = state.proxyDaemonCursor === ROW_DAEMON_KILL ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
1431
+ const d3 = `${d3b}${chalk.bold.red('Force kill service').padEnd(44)} ${chalk.dim('Enter → SIGKILL — emergency only')}`
1432
+ cursorLineByRow[ROW_DAEMON_KILL] = lines.length
1433
+ lines.push(state.proxyDaemonCursor === ROW_DAEMON_KILL ? chalk.bgRgb(60, 20, 20)(d3) : d3)
1434
+
1435
+ // 📖 Row 9: View logs
1436
+ const d4b = state.proxyDaemonCursor === ROW_DAEMON_LOGS ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
1437
+ const d4 = `${d4b}${chalk.bold('View service logs').padEnd(44)} ${chalk.dim('Enter → show last 50 log lines')}`
1438
+ cursorLineByRow[ROW_DAEMON_LOGS] = lines.length
1439
+ lines.push(state.proxyDaemonCursor === ROW_DAEMON_LOGS ? chalk.bgRgb(30, 30, 50)(d4) : d4)
1440
+ }
1441
+
1442
+ // ────────────────────────────── INFO SECTION ───────────────────────────────
1443
+ lines.push('')
1444
+ lines.push(` ${chalk.bold('ℹ How it works')}`)
1445
+ lines.push(` ${chalk.dim(' ─────────────────────────────────────────────')}`)
1446
+ lines.push('')
1447
+ lines.push(` ${chalk.dim(' 📖 The proxy starts a local HTTP server on 127.0.0.1 (localhost only).')}`)
1448
+ lines.push(` ${chalk.dim(' 📖 External tools connect to it as if it were OpenAI/Anthropic.')}`)
1449
+ lines.push(` ${chalk.dim(' 📖 The proxy rotates between your API keys across all providers.')}`)
1450
+ lines.push('')
1451
+ lines.push(` ${chalk.dim(' 📖 The background service adds persistence: install it once, and the proxy')}`)
1452
+ lines.push(` ${chalk.dim(' 📖 starts automatically at login and survives reboots.')}`)
1453
+ lines.push('')
1454
+ lines.push(` ${chalk.dim(' 📖 Claude Code support: FCM Proxy V2 translates Anthropic wire format')}`)
1455
+ lines.push(` ${chalk.dim(' 📖 (POST /v1/messages) to OpenAI format for upstream providers.')}`)
1456
+ lines.push('')
1457
+ if (process.platform === 'darwin') {
1458
+ lines.push(` ${chalk.dim(' 📦 macOS: launchd LaunchAgent at ~/Library/LaunchAgents/com.fcm.proxy.plist')}`)
1459
+ } else if (process.platform === 'linux') {
1460
+ lines.push(` ${chalk.dim(' 📦 Linux: systemd user service at ~/.config/systemd/user/fcm-proxy.service')}`)
1461
+ } else {
1462
+ lines.push(` ${chalk.dim(' ⚠ Windows: background service not supported — use in-process proxy (starts with TUI)')}`)
1463
+ }
1464
+ lines.push('')
1465
+
1466
+ // 📖 Clamp cursor
1467
+ if (state.proxyDaemonCursor > maxRow) state.proxyDaemonCursor = maxRow
1468
+
1469
+ // 📖 Scrolling and tinting
1470
+ const PROXY_DAEMON_BG = chalk.bgRgb(15, 25, 45)
1471
+ const targetLine = cursorLineByRow[state.proxyDaemonCursor] ?? 0
1472
+ state.proxyDaemonScrollOffset = keepOverlayTargetVisible(
1473
+ state.proxyDaemonScrollOffset,
1474
+ targetLine,
1475
+ lines.length,
1476
+ state.terminalRows
1477
+ )
1478
+ const { visible, offset } = sliceOverlayLines(lines, state.proxyDaemonScrollOffset, state.terminalRows)
1479
+ state.proxyDaemonScrollOffset = offset
1480
+ const tintedLines = tintOverlayLines(visible, PROXY_DAEMON_BG, state.terminalCols)
1481
+ return tintedLines.map(l => l + EL).join('\n')
1482
+ }
1483
+
1344
1484
  // 📖 stopRecommendAnalysis: cleanup timers if user cancels during analysis
1345
1485
  function stopRecommendAnalysis() {
1346
1486
  if (state.recommendAnalysisTimer) { clearInterval(state.recommendAnalysisTimer); state.recommendAnalysisTimer = null }
@@ -1349,12 +1489,12 @@ export function createOverlayRenderers(state, deps) {
1349
1489
 
1350
1490
  return {
1351
1491
  renderSettings,
1492
+ renderProxyDaemon,
1352
1493
  renderInstallEndpoints,
1353
1494
  renderHelp,
1354
1495
  renderLog,
1355
1496
  renderRecommend,
1356
- renderFeatureRequest,
1357
- renderBugReport,
1497
+ renderFeedback,
1358
1498
  renderChangelog,
1359
1499
  startRecommendAnalysis,
1360
1500
  stopRecommendAnalysis,