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/CHANGELOG.md +78 -0
- package/README.md +112 -39
- package/bin/fcm-proxy-daemon.js +239 -0
- package/bin/free-coding-models.js +105 -23
- package/package.json +3 -2
- package/src/account-manager.js +34 -0
- package/src/anthropic-translator.js +370 -0
- package/src/config.js +24 -1
- package/src/daemon-manager.js +527 -0
- package/src/endpoint-installer.js +187 -2
- package/src/key-handler.js +355 -150
- package/src/opencode.js +30 -32
- package/src/overlays.js +365 -225
- package/src/proxy-server.js +488 -6
- package/src/proxy-sync.js +552 -0
- package/src/proxy-topology.js +80 -0
- package/src/render-table.js +24 -15
- package/src/tool-launchers.js +138 -18
- package/src/tool-metadata.js +14 -14
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,
|
|
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
|
|
140
|
-
const
|
|
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('
|
|
283
|
+
lines.push(` ${chalk.bold('📡 FCM Proxy V2')}`)
|
|
276
284
|
lines.push(` ${chalk.dim(' ' + '─'.repeat(separatorWidth))}`)
|
|
277
285
|
lines.push('')
|
|
278
286
|
|
|
279
|
-
const
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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 +
|
|
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
|
-
? (
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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(
|
|
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(
|
|
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
|
|
454
|
-
|
|
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
|
-
:
|
|
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(
|
|
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 === '
|
|
474
|
-
|
|
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(
|
|
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 →
|
|
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(
|
|
616
|
-
lines.push(` ${chalk.
|
|
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
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
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
|
-
// ───
|
|
1040
|
-
// 📖
|
|
1041
|
-
// 📖 Shows an input field where users can type
|
|
1042
|
-
function
|
|
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(
|
|
1091
|
+
lines.push(` ${chalk.bold.rgb(57, 255, 20)('📝 Feedback, bugs & requests')}`)
|
|
1183
1092
|
lines.push('')
|
|
1184
|
-
lines.push(chalk.dim(
|
|
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!
|
|
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('
|
|
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
|
-
// 📖
|
|
1207
|
-
lines.push(
|
|
1208
|
-
|
|
1209
|
-
// 📖
|
|
1210
|
-
if (displayLines.length
|
|
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
|
-
|
|
1216
|
-
lines.push(` │ ${chalk.white(padded)} │`)
|
|
1121
|
+
lines.push(` ${line}`)
|
|
1217
1122
|
}
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
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
|
-
// 📖
|
|
1294
|
-
const
|
|
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
|
-
|
|
1203
|
+
const full = summary ? `${prefix} · ${summary}` : prefix
|
|
1204
|
+
lines.push(chalk.inverse(full))
|
|
1297
1205
|
} else {
|
|
1298
|
-
|
|
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
|
-
|
|
1357
|
-
renderBugReport,
|
|
1497
|
+
renderFeedback,
|
|
1358
1498
|
renderChangelog,
|
|
1359
1499
|
startRecommendAnalysis,
|
|
1360
1500
|
stopRecommendAnalysis,
|