free-coding-models 0.2.17 → 0.3.1
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 +71 -0
- package/README.md +118 -44
- package/bin/fcm-proxy-daemon.js +239 -0
- package/bin/free-coding-models.js +146 -37
- package/package.json +3 -2
- package/src/account-manager.js +34 -0
- package/src/anthropic-translator.js +440 -0
- package/src/cli-help.js +108 -0
- package/src/config.js +25 -1
- package/src/daemon-manager.js +527 -0
- package/src/endpoint-installer.js +45 -19
- package/src/key-handler.js +324 -148
- package/src/opencode.js +47 -44
- package/src/overlays.js +282 -207
- package/src/proxy-server.js +746 -10
- package/src/proxy-sync.js +564 -0
- package/src/proxy-topology.js +80 -0
- package/src/render-helpers.js +4 -2
- package/src/render-table.js +56 -49
- package/src/responses-translator.js +423 -0
- package/src/tool-launchers.js +343 -26
- package/src/utils.js +31 -8
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 current-tool 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 → Auto-sync current tool → Port → Cleanup → Install/Restart/Stop/Kill/Logs
|
|
16
|
+
* 📖 Feedback overlay (I key) combines feature requests + bug reports in one left-aligned input
|
|
17
|
+
*
|
|
14
18
|
* → Functions:
|
|
15
19
|
* - `createOverlayRenderers` — returns renderer + analysis helpers
|
|
16
20
|
*
|
|
17
21
|
* @exports { createOverlayRenderers }
|
|
22
|
+
* @see ./proxy-sync.js — resolveProxySyncToolMode powers current-tool proxy sync hints
|
|
23
|
+
* @see ./key-handler.js — handles keypresses for all overlay interactions
|
|
18
24
|
*/
|
|
19
25
|
|
|
20
26
|
import { loadChangelog } from './changelog-loader.js'
|
|
27
|
+
import { buildCliHelpLines } from './cli-help.js'
|
|
28
|
+
import { resolveProxySyncToolMode } from './proxy-sync.js'
|
|
21
29
|
|
|
22
30
|
export function createOverlayRenderers(state, deps) {
|
|
23
31
|
const {
|
|
@@ -138,11 +146,8 @@ export function createOverlayRenderers(state, deps) {
|
|
|
138
146
|
function renderSettings() {
|
|
139
147
|
const providerKeys = Object.keys(sources)
|
|
140
148
|
const updateRowIdx = providerKeys.length
|
|
141
|
-
const
|
|
142
|
-
const
|
|
143
|
-
const proxyPortRowIdx = updateRowIdx + 3
|
|
144
|
-
const proxyCleanupRowIdx = updateRowIdx + 4
|
|
145
|
-
const changelogViewRowIdx = updateRowIdx + 5
|
|
149
|
+
const proxyDaemonRowIdx = updateRowIdx + 1
|
|
150
|
+
const changelogViewRowIdx = updateRowIdx + 2
|
|
146
151
|
const proxySettings = getProxySettings(state.config)
|
|
147
152
|
const EL = '\x1b[K'
|
|
148
153
|
const lines = []
|
|
@@ -273,33 +278,23 @@ export function createOverlayRenderers(state, deps) {
|
|
|
273
278
|
lines.push(chalk.red(` ${state.settingsUpdateError}`))
|
|
274
279
|
}
|
|
275
280
|
|
|
281
|
+
// 📖 FCM Proxy V2 — single row that opens a dedicated overlay
|
|
276
282
|
lines.push('')
|
|
277
|
-
lines.push(` ${chalk.bold('
|
|
283
|
+
lines.push(` ${chalk.bold('📡 FCM Proxy V2')}`)
|
|
278
284
|
lines.push(` ${chalk.dim(' ' + '─'.repeat(separatorWidth))}`)
|
|
279
285
|
lines.push('')
|
|
280
286
|
|
|
281
|
-
const
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const proxyPortValue = state.settingsProxyPortEditMode && state.settingsCursor === proxyPortRowIdx
|
|
293
|
-
? chalk.cyanBright(`${state.settingsProxyPortBuffer}▏`)
|
|
294
|
-
: (proxySettings.preferredPort === 0 ? chalk.dim('auto (OS-assigned)') : chalk.green(String(proxySettings.preferredPort)))
|
|
295
|
-
const proxyPortRow = `${proxyPortBullet}${chalk.bold('Preferred proxy port').padEnd(44)} ${proxyPortValue}`
|
|
296
|
-
cursorLineByRow[proxyPortRowIdx] = lines.length
|
|
297
|
-
lines.push(state.settingsCursor === proxyPortRowIdx ? chalk.bgRgb(20, 45, 60)(proxyPortRow) : proxyPortRow)
|
|
298
|
-
|
|
299
|
-
const proxyCleanupBullet = state.settingsCursor === proxyCleanupRowIdx ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
300
|
-
const proxyCleanupRow = `${proxyCleanupBullet}${chalk.bold('Clean OpenCode proxy config').padEnd(44)} ${chalk.dim('Enter removes fcm-proxy from opencode.json')}`
|
|
301
|
-
cursorLineByRow[proxyCleanupRowIdx] = lines.length
|
|
302
|
-
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)
|
|
303
298
|
|
|
304
299
|
// 📖 Changelog viewer row
|
|
305
300
|
const changelogViewBullet = state.settingsCursor === changelogViewRowIdx ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
@@ -309,7 +304,7 @@ export function createOverlayRenderers(state, deps) {
|
|
|
309
304
|
|
|
310
305
|
// 📖 Profiles section — list saved profiles with active indicator + delete support
|
|
311
306
|
const savedProfiles = listProfiles(state.config)
|
|
312
|
-
const profileStartIdx = updateRowIdx +
|
|
307
|
+
const profileStartIdx = updateRowIdx + 3
|
|
313
308
|
const maxRowIdx = savedProfiles.length > 0 ? profileStartIdx + savedProfiles.length - 1 : updateRowIdx
|
|
314
309
|
|
|
315
310
|
lines.push('')
|
|
@@ -416,7 +411,7 @@ export function createOverlayRenderers(state, deps) {
|
|
|
416
411
|
: '—'
|
|
417
412
|
|
|
418
413
|
const selectedConnectionLabel = state.installEndpointsConnectionMode === 'proxy'
|
|
419
|
-
? 'FCM Proxy'
|
|
414
|
+
? 'FCM Proxy V2'
|
|
420
415
|
: state.installEndpointsConnectionMode === 'direct'
|
|
421
416
|
? 'Direct Provider'
|
|
422
417
|
: '—'
|
|
@@ -632,10 +627,10 @@ export function createOverlayRenderers(state, deps) {
|
|
|
632
627
|
lines.push(` ${chalk.yellow('X')} Toggle token log page ${chalk.dim('(shows recent request usage from request-log.jsonl)')}`)
|
|
633
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)')}`)
|
|
634
629
|
lines.push(` ${chalk.yellow('F')} Toggle favorite on selected row ${chalk.dim('(⭐ pinned at top, persisted)')}`)
|
|
635
|
-
lines.push(` ${chalk.yellow('Y')} Install endpoints ${chalk.dim('(provider catalog →
|
|
630
|
+
lines.push(` ${chalk.yellow('Y')} Install endpoints ${chalk.dim('(provider catalog → compatible tools, Direct or FCM Proxy V2)')}`)
|
|
636
631
|
lines.push(` ${chalk.yellow('Q')} Smart Recommend ${chalk.dim('(🎯 find the best model for your task — questionnaire + live analysis)')}`)
|
|
637
|
-
lines.push(` ${chalk.rgb(
|
|
638
|
-
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)')}`)
|
|
639
634
|
lines.push(` ${chalk.yellow('P')} Open settings ${chalk.dim('(manage API keys, provider toggles, proxy, manual update)')}`)
|
|
640
635
|
lines.push(` ${chalk.yellow('Shift+P')} Cycle config profile ${chalk.dim('(switch between saved profiles live)')}`)
|
|
641
636
|
lines.push(` ${chalk.yellow('Shift+S')} Save current config as a named profile ${chalk.dim('(inline prompt — type name + Enter)')}`)
|
|
@@ -656,29 +651,7 @@ export function createOverlayRenderers(state, deps) {
|
|
|
656
651
|
lines.push(` ${chalk.yellow('U')} Check updates manually`)
|
|
657
652
|
lines.push(` ${chalk.yellow('Esc')} Close settings`)
|
|
658
653
|
lines.push('')
|
|
659
|
-
lines.push(
|
|
660
|
-
lines.push(` ${chalk.dim('Usage: free-coding-models [options]')}`)
|
|
661
|
-
lines.push(` ${chalk.cyan('free-coding-models --opencode')} ${chalk.dim('OpenCode CLI mode')}`)
|
|
662
|
-
lines.push(` ${chalk.cyan('free-coding-models --opencode-desktop')} ${chalk.dim('OpenCode Desktop mode')}`)
|
|
663
|
-
lines.push(` ${chalk.cyan('free-coding-models --openclaw')} ${chalk.dim('OpenClaw mode')}`)
|
|
664
|
-
lines.push(` ${chalk.cyan('free-coding-models --crush')} ${chalk.dim('Crush mode')}`)
|
|
665
|
-
lines.push(` ${chalk.cyan('free-coding-models --goose')} ${chalk.dim('Goose mode')}`)
|
|
666
|
-
lines.push(` ${chalk.cyan('free-coding-models --pi')} ${chalk.dim('Pi mode')}`)
|
|
667
|
-
lines.push(` ${chalk.cyan('free-coding-models --aider')} ${chalk.dim('Aider mode')}`)
|
|
668
|
-
lines.push(` ${chalk.cyan('free-coding-models --claude-code')} ${chalk.dim('Claude Code mode')}`)
|
|
669
|
-
lines.push(` ${chalk.cyan('free-coding-models --codex')} ${chalk.dim('Codex CLI mode')}`)
|
|
670
|
-
lines.push(` ${chalk.cyan('free-coding-models --gemini')} ${chalk.dim('Gemini CLI mode')}`)
|
|
671
|
-
lines.push(` ${chalk.cyan('free-coding-models --qwen')} ${chalk.dim('Qwen Code mode')}`)
|
|
672
|
-
lines.push(` ${chalk.cyan('free-coding-models --openhands')} ${chalk.dim('OpenHands mode')}`)
|
|
673
|
-
lines.push(` ${chalk.cyan('free-coding-models --amp')} ${chalk.dim('Amp mode')}`)
|
|
674
|
-
lines.push(` ${chalk.cyan('free-coding-models --best')} ${chalk.dim('Only top tiers (A+, S, S+)')}`)
|
|
675
|
-
lines.push(` ${chalk.cyan('free-coding-models --fiable')} ${chalk.dim('10s reliability analysis')}`)
|
|
676
|
-
lines.push(` ${chalk.cyan('free-coding-models --tier S|A|B|C')} ${chalk.dim('Filter by tier letter')}`)
|
|
677
|
-
lines.push(` ${chalk.cyan('free-coding-models --no-telemetry')} ${chalk.dim('Disable telemetry for this run')}`)
|
|
678
|
-
lines.push(` ${chalk.cyan('free-coding-models --recommend')} ${chalk.dim('Auto-open Smart Recommend on start')}`)
|
|
679
|
-
lines.push(` ${chalk.cyan('free-coding-models --profile <name>')} ${chalk.dim('Load a saved config profile')}`)
|
|
680
|
-
lines.push(` ${chalk.cyan('free-coding-models --clean-proxy')} ${chalk.dim('Remove persisted fcm-proxy config from OpenCode')}`)
|
|
681
|
-
lines.push(` ${chalk.dim('Flags can be combined: --openclaw --tier S')}`)
|
|
654
|
+
lines.push(...buildCliHelpLines({ chalk, indent: ' ', title: 'CLI Flags' }))
|
|
682
655
|
lines.push('')
|
|
683
656
|
// 📖 Help overlay can be longer than viewport, so keep a dedicated scroll offset.
|
|
684
657
|
const { visible, offset } = sliceOverlayLines(lines, state.helpScrollOffset, state.terminalRows)
|
|
@@ -1057,117 +1030,10 @@ export function createOverlayRenderers(state, deps) {
|
|
|
1057
1030
|
}, PING_RATE)
|
|
1058
1031
|
}
|
|
1059
1032
|
|
|
1060
|
-
// ───
|
|
1061
|
-
// 📖
|
|
1062
|
-
// 📖 Shows an input field where users can type
|
|
1063
|
-
function
|
|
1064
|
-
const EL = '\x1b[K'
|
|
1065
|
-
const lines = []
|
|
1066
|
-
|
|
1067
|
-
// 📖 Calculate available space for multi-line input (dynamic based on terminal width)
|
|
1068
|
-
const maxInputWidth = state.terminalCols - 8 // 8 = padding (4 spaces each side)
|
|
1069
|
-
const maxInputLines = 10 // Show up to 10 lines of input
|
|
1070
|
-
|
|
1071
|
-
// 📖 Split buffer into lines for display (with wrapping)
|
|
1072
|
-
const wrapText = (text, width) => {
|
|
1073
|
-
const words = text.split(' ')
|
|
1074
|
-
const lines = []
|
|
1075
|
-
let currentLine = ''
|
|
1076
|
-
|
|
1077
|
-
for (const word of words) {
|
|
1078
|
-
const testLine = currentLine ? currentLine + ' ' + word : word
|
|
1079
|
-
if (testLine.length <= width) {
|
|
1080
|
-
currentLine = testLine
|
|
1081
|
-
} else {
|
|
1082
|
-
if (currentLine) lines.push(currentLine)
|
|
1083
|
-
currentLine = word
|
|
1084
|
-
}
|
|
1085
|
-
}
|
|
1086
|
-
if (currentLine) lines.push(currentLine)
|
|
1087
|
-
return lines
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
const inputLines = wrapText(state.featureRequestBuffer, maxInputWidth)
|
|
1091
|
-
const displayLines = inputLines.slice(0, maxInputLines)
|
|
1092
|
-
|
|
1093
|
-
// 📖 Branding header
|
|
1094
|
-
lines.push('')
|
|
1095
|
-
lines.push(` ${chalk.cyanBright('🚀')} ${chalk.bold.cyanBright('free-coding-models')} ${chalk.dim(`v${LOCAL_VERSION}`)}`)
|
|
1096
|
-
lines.push(` ${chalk.bold.rgb(57, 255, 20)('📝 Feature Request')}`)
|
|
1097
|
-
lines.push('')
|
|
1098
|
-
lines.push(chalk.dim(' — send anonymous feedback to the project team'))
|
|
1099
|
-
lines.push('')
|
|
1100
|
-
|
|
1101
|
-
// 📖 Status messages (if any)
|
|
1102
|
-
if (state.featureRequestStatus === 'sending') {
|
|
1103
|
-
lines.push(` ${chalk.yellow('⏳ Sending...')}`)
|
|
1104
|
-
lines.push('')
|
|
1105
|
-
} else if (state.featureRequestStatus === 'success') {
|
|
1106
|
-
lines.push(` ${chalk.greenBright.bold('✅ Successfully sent!')} ${chalk.dim('Closing overlay in 3 seconds...')}`)
|
|
1107
|
-
lines.push('')
|
|
1108
|
-
lines.push(` ${chalk.dim('Thank you for your feedback! Your feature request has been sent to the project team.')}`)
|
|
1109
|
-
lines.push('')
|
|
1110
|
-
} else if (state.featureRequestStatus === 'error') {
|
|
1111
|
-
lines.push(` ${chalk.red('❌ Error:')} ${chalk.yellow(state.featureRequestError || 'Failed to send')}`)
|
|
1112
|
-
lines.push(` ${chalk.dim('Press Backspace to edit, or Esc to close')}`)
|
|
1113
|
-
lines.push('')
|
|
1114
|
-
} else {
|
|
1115
|
-
lines.push(` ${chalk.dim('Type your feature request below. Press Enter to send, Esc to cancel.')}`)
|
|
1116
|
-
lines.push(` ${chalk.dim('Your message will be sent anonymously to the project team.')}`)
|
|
1117
|
-
lines.push('')
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
// 📖 Input box with border
|
|
1121
|
-
lines.push(chalk.dim(` ┌─ ${chalk.cyan('Message')} ${chalk.dim(`(${state.featureRequestBuffer.length}/500 chars)`)} ─${'─'.repeat(maxInputWidth - 22)}┐`))
|
|
1122
|
-
|
|
1123
|
-
// 📖 Display input lines (or placeholder if empty)
|
|
1124
|
-
if (displayLines.length === 0 && state.featureRequestStatus === 'idle') {
|
|
1125
|
-
lines.push(chalk.dim(` │${' '.repeat(maxInputWidth)}│`))
|
|
1126
|
-
lines.push(chalk.dim(` │ ${chalk.white.italic('Type your message here...')}${' '.repeat(Math.max(0, maxInputWidth - 28))}│`))
|
|
1127
|
-
} else {
|
|
1128
|
-
for (const line of displayLines) {
|
|
1129
|
-
const padded = line.padEnd(maxInputWidth)
|
|
1130
|
-
lines.push(` │ ${chalk.white(padded)} │`)
|
|
1131
|
-
}
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
|
-
// 📖 Fill remaining space if needed
|
|
1135
|
-
const linesToFill = Math.max(0, maxInputLines - Math.max(displayLines.length, 1))
|
|
1136
|
-
for (let i = 0; i < linesToFill; i++) {
|
|
1137
|
-
lines.push(chalk.dim(` │${' '.repeat(maxInputWidth)}│`))
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
// 📖 Cursor indicator (only when not sending/success)
|
|
1141
|
-
if (state.featureRequestStatus === 'idle' || state.featureRequestStatus === 'error') {
|
|
1142
|
-
// Add cursor indicator to the last line
|
|
1143
|
-
if (lines.length > 0 && displayLines.length > 0) {
|
|
1144
|
-
const lastLineIdx = lines.findIndex(l => l.includes('│ ') && !l.includes('Message'))
|
|
1145
|
-
if (lastLineIdx >= 0 && lastLineIdx < lines.length) {
|
|
1146
|
-
// Add cursor blink
|
|
1147
|
-
const lastLine = lines[lastLineIdx]
|
|
1148
|
-
if (lastLine.includes('│')) {
|
|
1149
|
-
lines[lastLineIdx] = lastLine.replace(/\s+│$/, chalk.rgb(57, 255, 20).bold('▏') + ' │')
|
|
1150
|
-
}
|
|
1151
|
-
}
|
|
1152
|
-
}
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
|
-
lines.push(chalk.dim(` └${'─'.repeat(maxInputWidth + 2)}┘`))
|
|
1156
|
-
|
|
1157
|
-
lines.push('')
|
|
1158
|
-
lines.push(chalk.dim(' Enter Send • Esc Cancel • Backspace Delete'))
|
|
1159
|
-
|
|
1160
|
-
// 📖 Apply overlay tint and return
|
|
1161
|
-
const FEATURE_REQUEST_OVERLAY_BG = chalk.bgRgb(0, 0, 0) // Dark blue-ish background (RGB: 26, 26, 46)
|
|
1162
|
-
const tintedLines = tintOverlayLines(lines, FEATURE_REQUEST_OVERLAY_BG, state.terminalCols)
|
|
1163
|
-
const cleared = tintedLines.map(l => l + EL)
|
|
1164
|
-
return cleared.join('\n')
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
|
-
// ─── Bug Report overlay renderer ─────────────────────────────────────────
|
|
1168
|
-
// 📖 renderBugReport: Draw the overlay for anonymous Discord bug reports.
|
|
1169
|
-
// 📖 Shows an input field where users can type bug reports, then sends to Discord webhook.
|
|
1170
|
-
function renderBugReport() {
|
|
1033
|
+
// ─── Feedback overlay renderer ────────────────────────────────────────────
|
|
1034
|
+
// 📖 renderFeedback: Draw the overlay for anonymous Discord feedback.
|
|
1035
|
+
// 📖 Shows an input field where users can type feedback, bug reports, or any comments.
|
|
1036
|
+
function renderFeedback() {
|
|
1171
1037
|
const EL = '\x1b[K'
|
|
1172
1038
|
const lines = []
|
|
1173
1039
|
|
|
@@ -1200,9 +1066,9 @@ export function createOverlayRenderers(state, deps) {
|
|
|
1200
1066
|
// 📖 Branding header
|
|
1201
1067
|
lines.push('')
|
|
1202
1068
|
lines.push(` ${chalk.cyanBright('🚀')} ${chalk.bold.cyanBright('free-coding-models')} ${chalk.dim(`v${LOCAL_VERSION}`)}`)
|
|
1203
|
-
lines.push(` ${chalk.bold.rgb(
|
|
1069
|
+
lines.push(` ${chalk.bold.rgb(57, 255, 20)('📝 Feedback, bugs & requests')}`)
|
|
1204
1070
|
lines.push('')
|
|
1205
|
-
lines.push(chalk.dim(
|
|
1071
|
+
lines.push(chalk.dim(" — don't hesitate to send us feedback, bug reports, or just your feeling about the app"))
|
|
1206
1072
|
lines.push('')
|
|
1207
1073
|
|
|
1208
1074
|
// 📖 Status messages (if any)
|
|
@@ -1212,55 +1078,35 @@ export function createOverlayRenderers(state, deps) {
|
|
|
1212
1078
|
} else if (state.bugReportStatus === 'success') {
|
|
1213
1079
|
lines.push(` ${chalk.greenBright.bold('✅ Successfully sent!')} ${chalk.dim('Closing overlay in 3 seconds...')}`)
|
|
1214
1080
|
lines.push('')
|
|
1215
|
-
lines.push(` ${chalk.dim('Thank you for your feedback!
|
|
1081
|
+
lines.push(` ${chalk.dim('Thank you for your feedback! It has been sent to the project team.')}`)
|
|
1216
1082
|
lines.push('')
|
|
1217
1083
|
} else if (state.bugReportStatus === 'error') {
|
|
1218
1084
|
lines.push(` ${chalk.red('❌ Error:')} ${chalk.yellow(state.bugReportError || 'Failed to send')}`)
|
|
1219
1085
|
lines.push(` ${chalk.dim('Press Backspace to edit, or Esc to close')}`)
|
|
1220
1086
|
lines.push('')
|
|
1221
1087
|
} else {
|
|
1222
|
-
lines.push(` ${chalk.dim('
|
|
1088
|
+
lines.push(` ${chalk.dim('Type your feedback below. Press Enter to send, Esc to cancel.')}`)
|
|
1223
1089
|
lines.push(` ${chalk.dim('Your message will be sent anonymously to the project team.')}`)
|
|
1224
1090
|
lines.push('')
|
|
1225
1091
|
}
|
|
1226
1092
|
|
|
1227
|
-
// 📖
|
|
1228
|
-
lines.push(
|
|
1229
|
-
|
|
1230
|
-
// 📖
|
|
1231
|
-
if (displayLines.length
|
|
1232
|
-
lines.push(chalk.dim(` │${' '.repeat(maxInputWidth)}│`))
|
|
1233
|
-
lines.push(chalk.dim(` │ ${chalk.white.italic('Describe what happened...')}${' '.repeat(Math.max(0, maxInputWidth - 31))}│`))
|
|
1234
|
-
} else {
|
|
1093
|
+
// 📖 Simple input area – left-aligned, framed by horizontal lines
|
|
1094
|
+
lines.push(` ${chalk.cyan('Message')} (${state.bugReportBuffer.length}/500 chars)`)
|
|
1095
|
+
lines.push(` ${chalk.dim('─'.repeat(maxInputWidth))}`)
|
|
1096
|
+
// 📖 Input lines — left-aligned, or placeholder when empty
|
|
1097
|
+
if (displayLines.length > 0) {
|
|
1235
1098
|
for (const line of displayLines) {
|
|
1236
|
-
|
|
1237
|
-
lines.push(` │ ${chalk.white(padded)} │`)
|
|
1099
|
+
lines.push(` ${line}`)
|
|
1238
1100
|
}
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
const linesToFill = Math.max(0, maxInputLines - Math.max(displayLines.length, 1))
|
|
1243
|
-
for (let i = 0; i < linesToFill; i++) {
|
|
1244
|
-
lines.push(chalk.dim(` │${' '.repeat(maxInputWidth)}│`))
|
|
1245
|
-
}
|
|
1246
|
-
|
|
1247
|
-
// 📖 Cursor indicator (only when not sending/success)
|
|
1248
|
-
if (state.bugReportStatus === 'idle' || state.bugReportStatus === 'error') {
|
|
1249
|
-
// Add cursor indicator to the last line
|
|
1250
|
-
if (lines.length > 0 && displayLines.length > 0) {
|
|
1251
|
-
const lastLineIdx = lines.findIndex(l => l.includes('│ ') && !l.includes('Bug Details'))
|
|
1252
|
-
if (lastLineIdx >= 0 && lastLineIdx < lines.length) {
|
|
1253
|
-
// Add cursor blink
|
|
1254
|
-
const lastLine = lines[lastLineIdx]
|
|
1255
|
-
if (lastLine.includes('│')) {
|
|
1256
|
-
lines[lastLineIdx] = lastLine.replace(/\s+│$/, chalk.rgb(255, 87, 51).bold('▏') + ' │')
|
|
1257
|
-
}
|
|
1258
|
-
}
|
|
1101
|
+
// 📖 Show cursor on last line
|
|
1102
|
+
if (state.bugReportStatus === 'idle' || state.bugReportStatus === 'error') {
|
|
1103
|
+
lines[lines.length - 1] += chalk.cyanBright('▏')
|
|
1259
1104
|
}
|
|
1105
|
+
} else {
|
|
1106
|
+
const placeholderBR = state.bugReportStatus === 'idle' ? chalk.white.italic('Type your message here...') : ''
|
|
1107
|
+
lines.push(` ${placeholderBR}${chalk.cyanBright('▏')}`)
|
|
1260
1108
|
}
|
|
1261
|
-
|
|
1262
|
-
lines.push(chalk.dim(` └${'─'.repeat(maxInputWidth + 2)}┘`))
|
|
1263
|
-
|
|
1109
|
+
lines.push(` ${chalk.dim('─'.repeat(maxInputWidth))}`)
|
|
1264
1110
|
lines.push('')
|
|
1265
1111
|
lines.push(chalk.dim(' Enter Send • Esc Cancel • Backspace Delete'))
|
|
1266
1112
|
|
|
@@ -1393,6 +1239,235 @@ export function createOverlayRenderers(state, deps) {
|
|
|
1393
1239
|
return cleared.join('\n')
|
|
1394
1240
|
}
|
|
1395
1241
|
|
|
1242
|
+
// ─── FCM Proxy V2 overlay renderer ──────────────────────────────────────────
|
|
1243
|
+
// 📖 renderProxyDaemon: Dedicated full-page overlay for FCM Proxy V2 configuration
|
|
1244
|
+
// 📖 and background service management. Opened from Settings → "FCM Proxy V2 settings →".
|
|
1245
|
+
// 📖 Contains all proxy toggles, service status/actions, explanations, and emergency kill.
|
|
1246
|
+
function renderProxyDaemon() {
|
|
1247
|
+
const EL = '\x1b[K'
|
|
1248
|
+
const lines = []
|
|
1249
|
+
const cursorLineByRow = {}
|
|
1250
|
+
const proxySettings = getProxySettings(state.config)
|
|
1251
|
+
|
|
1252
|
+
// 📖 Row indices — these control cursor navigation
|
|
1253
|
+
const ROW_PROXY_ENABLED = 0
|
|
1254
|
+
const ROW_PROXY_SYNC = 1
|
|
1255
|
+
const ROW_PROXY_PORT = 2
|
|
1256
|
+
const ROW_PROXY_CLEANUP = 3
|
|
1257
|
+
const ROW_DAEMON_INSTALL = 4
|
|
1258
|
+
const ROW_DAEMON_RESTART = 5
|
|
1259
|
+
const ROW_DAEMON_STOP = 6
|
|
1260
|
+
const ROW_DAEMON_KILL = 7
|
|
1261
|
+
const ROW_DAEMON_LOGS = 8
|
|
1262
|
+
|
|
1263
|
+
const daemonStatus = state.daemonStatus || 'not-installed'
|
|
1264
|
+
const daemonInfo = state.daemonInfo
|
|
1265
|
+
const daemonIsActive = daemonStatus === 'running' || daemonStatus === 'unhealthy' || daemonStatus === 'stale'
|
|
1266
|
+
const daemonIsInstalled = daemonIsActive || daemonStatus === 'stopped'
|
|
1267
|
+
|
|
1268
|
+
// 📖 Compute max row — hide daemon action rows when daemon not installed
|
|
1269
|
+
let maxRow = ROW_DAEMON_INSTALL
|
|
1270
|
+
if (daemonIsInstalled) maxRow = ROW_DAEMON_LOGS
|
|
1271
|
+
|
|
1272
|
+
// 📖 Header
|
|
1273
|
+
lines.push(` ${chalk.cyanBright('🚀')} ${chalk.bold.cyanBright('free-coding-models')} ${chalk.dim(`v${LOCAL_VERSION}`)}`)
|
|
1274
|
+
lines.push(` ${chalk.bold('📡 FCM Proxy V2 Manager')}`)
|
|
1275
|
+
lines.push(` ${chalk.dim('— Esc back to Settings • ↑↓ navigate • Enter select')}`)
|
|
1276
|
+
lines.push('')
|
|
1277
|
+
lines.push(` ${chalk.bgRed.white.bold(' ⚠ EXPERIMENTAL ')} ${chalk.red('This feature is under active development and may not work as expected.')}`)
|
|
1278
|
+
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.')}`)
|
|
1279
|
+
lines.push('')
|
|
1280
|
+
|
|
1281
|
+
// 📖 Feedback message (auto-clears after 5s)
|
|
1282
|
+
const msg = state.proxyDaemonMessage
|
|
1283
|
+
if (msg && (Date.now() - msg.ts < 5000)) {
|
|
1284
|
+
const msgColor = msg.type === 'success' ? chalk.greenBright : msg.type === 'warning' ? chalk.yellow : chalk.red
|
|
1285
|
+
lines.push(` ${msgColor(msg.msg)}`)
|
|
1286
|
+
lines.push('')
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
// ────────────────────────────── PROXY SECTION ──────────────────────────────
|
|
1290
|
+
lines.push(` ${chalk.bold('🔀 Proxy Configuration')}`)
|
|
1291
|
+
lines.push(` ${chalk.dim(' ─────────────────────────────────────────────')}`)
|
|
1292
|
+
lines.push('')
|
|
1293
|
+
lines.push(` ${chalk.dim(' The local proxy groups all your provider API keys into a single')}`)
|
|
1294
|
+
lines.push(` ${chalk.dim(' endpoint. Tools like OpenCode, Claude Code, Goose, etc. connect')}`)
|
|
1295
|
+
lines.push(` ${chalk.dim(' to this proxy which handles key rotation, rate limiting, and failover.')}`)
|
|
1296
|
+
lines.push('')
|
|
1297
|
+
|
|
1298
|
+
// 📖 Proxy sync now always follows the currently selected Z-mode when supported.
|
|
1299
|
+
const currentToolMode = state.mode || 'opencode'
|
|
1300
|
+
const currentToolMeta = getToolMeta(currentToolMode)
|
|
1301
|
+
const currentToolLabel = `${currentToolMeta.emoji} ${currentToolMeta.label}`
|
|
1302
|
+
const proxySyncTool = resolveProxySyncToolMode(currentToolMode)
|
|
1303
|
+
const proxySyncHint = proxySyncTool
|
|
1304
|
+
? chalk.dim(` Current tool: ${currentToolLabel}`)
|
|
1305
|
+
: chalk.yellow(` Current tool: ${currentToolLabel} (launcher-only, no persisted proxy config)`)
|
|
1306
|
+
lines.push(proxySyncHint)
|
|
1307
|
+
lines.push('')
|
|
1308
|
+
|
|
1309
|
+
// 📖 Row 0: Proxy enabled toggle
|
|
1310
|
+
const r0b = state.proxyDaemonCursor === ROW_PROXY_ENABLED ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
1311
|
+
const r0val = proxySettings.enabled ? chalk.greenBright('Enabled') : chalk.dim('Disabled (opt-in)')
|
|
1312
|
+
const r0 = `${r0b}${chalk.bold('Proxy mode').padEnd(44)} ${r0val}`
|
|
1313
|
+
cursorLineByRow[ROW_PROXY_ENABLED] = lines.length
|
|
1314
|
+
lines.push(state.proxyDaemonCursor === ROW_PROXY_ENABLED ? chalk.bgRgb(20, 45, 60)(r0) : r0)
|
|
1315
|
+
|
|
1316
|
+
// 📖 Row 1: Auto-sync proxy config to the current tool when that tool supports persisted sync.
|
|
1317
|
+
const r2b = state.proxyDaemonCursor === ROW_PROXY_SYNC ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
1318
|
+
const r2val = proxySettings.syncToOpenCode ? chalk.greenBright('Enabled') : chalk.dim('Disabled')
|
|
1319
|
+
const r2label = proxySyncTool
|
|
1320
|
+
? `Auto-sync proxy to ${currentToolMeta.label}`
|
|
1321
|
+
: 'Auto-sync proxy to current tool'
|
|
1322
|
+
const r2note = proxySyncTool ? '' : ` ${chalk.dim('(unavailable for this mode)')}`
|
|
1323
|
+
const r2 = `${r2b}${chalk.bold(r2label).padEnd(44)} ${r2val}${r2note}`
|
|
1324
|
+
cursorLineByRow[ROW_PROXY_SYNC] = lines.length
|
|
1325
|
+
lines.push(state.proxyDaemonCursor === ROW_PROXY_SYNC ? chalk.bgRgb(20, 45, 60)(r2) : r2)
|
|
1326
|
+
|
|
1327
|
+
// 📖 Row 2: Preferred port
|
|
1328
|
+
const r3b = state.proxyDaemonCursor === ROW_PROXY_PORT ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
1329
|
+
const r3val = state.settingsProxyPortEditMode && state.proxyDaemonCursor === ROW_PROXY_PORT
|
|
1330
|
+
? chalk.cyanBright(`${state.settingsProxyPortBuffer}▏`)
|
|
1331
|
+
: (proxySettings.preferredPort === 0 ? chalk.dim('auto (OS-assigned)') : chalk.green(String(proxySettings.preferredPort)))
|
|
1332
|
+
const r3 = `${r3b}${chalk.bold('Preferred proxy port').padEnd(44)} ${r3val}`
|
|
1333
|
+
cursorLineByRow[ROW_PROXY_PORT] = lines.length
|
|
1334
|
+
lines.push(state.proxyDaemonCursor === ROW_PROXY_PORT ? chalk.bgRgb(20, 45, 60)(r3) : r3)
|
|
1335
|
+
|
|
1336
|
+
// 📖 Row 3: Clean current tool proxy config
|
|
1337
|
+
const r4b = state.proxyDaemonCursor === ROW_PROXY_CLEANUP ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
1338
|
+
const r4title = proxySyncTool
|
|
1339
|
+
? `Clean ${currentToolMeta.label} proxy config`
|
|
1340
|
+
: `Clean ${currentToolMeta.label} proxy config`
|
|
1341
|
+
const r4hint = proxySyncTool
|
|
1342
|
+
? chalk.dim('Enter → removes all fcm-* entries')
|
|
1343
|
+
: chalk.dim('Unavailable for this mode')
|
|
1344
|
+
const r4 = `${r4b}${chalk.bold(r4title).padEnd(44)} ${r4hint}`
|
|
1345
|
+
cursorLineByRow[ROW_PROXY_CLEANUP] = lines.length
|
|
1346
|
+
lines.push(state.proxyDaemonCursor === ROW_PROXY_CLEANUP ? chalk.bgRgb(45, 30, 30)(r4) : r4)
|
|
1347
|
+
|
|
1348
|
+
// ────────────────────────────── DAEMON SECTION ─────────────────────────────
|
|
1349
|
+
lines.push('')
|
|
1350
|
+
lines.push(` ${chalk.bold('📡 FCM Proxy V2 Background Service')}`)
|
|
1351
|
+
lines.push(` ${chalk.dim(' ─────────────────────────────────────────────')}`)
|
|
1352
|
+
lines.push('')
|
|
1353
|
+
lines.push(` ${chalk.dim(' The background service keeps FCM Proxy V2 running 24/7 — even when')}`)
|
|
1354
|
+
lines.push(` ${chalk.dim(' the TUI is closed or after a reboot. Claude Code, Gemini CLI, and')}`)
|
|
1355
|
+
lines.push(` ${chalk.dim(' all tools stay connected at all times.')}`)
|
|
1356
|
+
lines.push('')
|
|
1357
|
+
|
|
1358
|
+
// 📖 Status display
|
|
1359
|
+
let daemonStatusLine = ` ${chalk.bold(' Status:')} `
|
|
1360
|
+
if (daemonStatus === 'running') {
|
|
1361
|
+
daemonStatusLine += chalk.greenBright('● Running')
|
|
1362
|
+
if (daemonInfo) daemonStatusLine += chalk.dim(` — PID ${daemonInfo.pid} • Port ${daemonInfo.port} • ${daemonInfo.accountCount || '?'} accounts • ${daemonInfo.modelCount || '?'} models`)
|
|
1363
|
+
} else if (daemonStatus === 'stopped') {
|
|
1364
|
+
daemonStatusLine += chalk.yellow('○ Stopped') + chalk.dim(' — service installed but not running')
|
|
1365
|
+
} else if (daemonStatus === 'stale') {
|
|
1366
|
+
daemonStatusLine += chalk.red('⚠ Stale') + chalk.dim(' — service crashed, PID no longer alive')
|
|
1367
|
+
} else if (daemonStatus === 'unhealthy') {
|
|
1368
|
+
daemonStatusLine += chalk.red('⚠ Unhealthy') + chalk.dim(' — PID alive but health check failed')
|
|
1369
|
+
} else {
|
|
1370
|
+
daemonStatusLine += chalk.dim('○ Not installed')
|
|
1371
|
+
}
|
|
1372
|
+
lines.push(daemonStatusLine)
|
|
1373
|
+
|
|
1374
|
+
// 📖 Version mismatch warning
|
|
1375
|
+
if (daemonInfo?.version && daemonInfo.version !== LOCAL_VERSION) {
|
|
1376
|
+
lines.push(` ${chalk.yellow(` ⚠ Version mismatch: service v${daemonInfo.version} vs FCM v${LOCAL_VERSION}`)}`)
|
|
1377
|
+
lines.push(` ${chalk.dim(' Restart or reinstall the service to apply the update.')}`)
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
// 📖 Uptime
|
|
1381
|
+
if (daemonStatus === 'running' && daemonInfo?.startedAt) {
|
|
1382
|
+
const upSec = Math.floor((Date.now() - new Date(daemonInfo.startedAt).getTime()) / 1000)
|
|
1383
|
+
const upMin = Math.floor(upSec / 60)
|
|
1384
|
+
const upHr = Math.floor(upMin / 60)
|
|
1385
|
+
const uptimeStr = upHr > 0 ? `${upHr}h ${upMin % 60}m` : upMin > 0 ? `${upMin}m ${upSec % 60}s` : `${upSec}s`
|
|
1386
|
+
lines.push(` ${chalk.dim(` Uptime: ${uptimeStr}`)}`)
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
lines.push('')
|
|
1390
|
+
|
|
1391
|
+
// 📖 Row 5: Install / Uninstall
|
|
1392
|
+
const d0b = state.proxyDaemonCursor === ROW_DAEMON_INSTALL ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
1393
|
+
const d0label = daemonIsInstalled ? 'Uninstall service' : 'Install background service'
|
|
1394
|
+
const d0hint = daemonIsInstalled
|
|
1395
|
+
? chalk.dim('Enter → stop service + remove config')
|
|
1396
|
+
: chalk.dim('Enter → install as OS service (launchd/systemd)')
|
|
1397
|
+
const d0 = `${d0b}${chalk.bold(d0label).padEnd(44)} ${d0hint}`
|
|
1398
|
+
cursorLineByRow[ROW_DAEMON_INSTALL] = lines.length
|
|
1399
|
+
lines.push(state.proxyDaemonCursor === ROW_DAEMON_INSTALL ? chalk.bgRgb(daemonIsInstalled ? 45 : 20, daemonIsInstalled ? 30 : 45, daemonIsInstalled ? 30 : 40)(d0) : d0)
|
|
1400
|
+
|
|
1401
|
+
// 📖 Rows 6-9 only shown when service is installed
|
|
1402
|
+
if (daemonIsInstalled) {
|
|
1403
|
+
// 📖 Row 6: Restart
|
|
1404
|
+
const d1b = state.proxyDaemonCursor === ROW_DAEMON_RESTART ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
1405
|
+
const d1 = `${d1b}${chalk.bold('Restart service').padEnd(44)} ${chalk.dim('Enter → stop + start via OS service manager')}`
|
|
1406
|
+
cursorLineByRow[ROW_DAEMON_RESTART] = lines.length
|
|
1407
|
+
lines.push(state.proxyDaemonCursor === ROW_DAEMON_RESTART ? chalk.bgRgb(20, 45, 60)(d1) : d1)
|
|
1408
|
+
|
|
1409
|
+
// 📖 Row 7: Stop (SIGTERM)
|
|
1410
|
+
const d2b = state.proxyDaemonCursor === ROW_DAEMON_STOP ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
1411
|
+
const d2warn = chalk.dim(' (service may auto-restart)')
|
|
1412
|
+
const d2 = `${d2b}${chalk.bold('Stop service').padEnd(44)} ${chalk.dim('Enter → graceful shutdown (SIGTERM)')}${d2warn}`
|
|
1413
|
+
cursorLineByRow[ROW_DAEMON_STOP] = lines.length
|
|
1414
|
+
lines.push(state.proxyDaemonCursor === ROW_DAEMON_STOP ? chalk.bgRgb(45, 40, 20)(d2) : d2)
|
|
1415
|
+
|
|
1416
|
+
// 📖 Row 8: Force kill (SIGKILL) — emergency
|
|
1417
|
+
const d3b = state.proxyDaemonCursor === ROW_DAEMON_KILL ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
1418
|
+
const d3 = `${d3b}${chalk.bold.red('Force kill service').padEnd(44)} ${chalk.dim('Enter → SIGKILL — emergency only')}`
|
|
1419
|
+
cursorLineByRow[ROW_DAEMON_KILL] = lines.length
|
|
1420
|
+
lines.push(state.proxyDaemonCursor === ROW_DAEMON_KILL ? chalk.bgRgb(60, 20, 20)(d3) : d3)
|
|
1421
|
+
|
|
1422
|
+
// 📖 Row 9: View logs
|
|
1423
|
+
const d4b = state.proxyDaemonCursor === ROW_DAEMON_LOGS ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
1424
|
+
const d4 = `${d4b}${chalk.bold('View service logs').padEnd(44)} ${chalk.dim('Enter → show last 50 log lines')}`
|
|
1425
|
+
cursorLineByRow[ROW_DAEMON_LOGS] = lines.length
|
|
1426
|
+
lines.push(state.proxyDaemonCursor === ROW_DAEMON_LOGS ? chalk.bgRgb(30, 30, 50)(d4) : d4)
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
// ────────────────────────────── INFO SECTION ───────────────────────────────
|
|
1430
|
+
lines.push('')
|
|
1431
|
+
lines.push(` ${chalk.bold('ℹ How it works')}`)
|
|
1432
|
+
lines.push(` ${chalk.dim(' ─────────────────────────────────────────────')}`)
|
|
1433
|
+
lines.push('')
|
|
1434
|
+
lines.push(` ${chalk.dim(' 📖 The proxy starts a local HTTP server on 127.0.0.1 (localhost only).')}`)
|
|
1435
|
+
lines.push(` ${chalk.dim(' 📖 External tools connect to it as if it were OpenAI/Anthropic.')}`)
|
|
1436
|
+
lines.push(` ${chalk.dim(' 📖 The proxy rotates between your API keys across all providers.')}`)
|
|
1437
|
+
lines.push('')
|
|
1438
|
+
lines.push(` ${chalk.dim(' 📖 The background service adds persistence: install it once, and the proxy')}`)
|
|
1439
|
+
lines.push(` ${chalk.dim(' 📖 starts automatically at login and survives reboots.')}`)
|
|
1440
|
+
lines.push('')
|
|
1441
|
+
lines.push(` ${chalk.dim(' 📖 Claude Code support: FCM Proxy V2 translates Anthropic wire format')}`)
|
|
1442
|
+
lines.push(` ${chalk.dim(' 📖 (POST /v1/messages) to OpenAI format for upstream providers.')}`)
|
|
1443
|
+
lines.push('')
|
|
1444
|
+
if (process.platform === 'darwin') {
|
|
1445
|
+
lines.push(` ${chalk.dim(' 📦 macOS: launchd LaunchAgent at ~/Library/LaunchAgents/com.fcm.proxy.plist')}`)
|
|
1446
|
+
} else if (process.platform === 'linux') {
|
|
1447
|
+
lines.push(` ${chalk.dim(' 📦 Linux: systemd user service at ~/.config/systemd/user/fcm-proxy.service')}`)
|
|
1448
|
+
} else {
|
|
1449
|
+
lines.push(` ${chalk.dim(' ⚠ Windows: background service not supported — use in-process proxy (starts with TUI)')}`)
|
|
1450
|
+
}
|
|
1451
|
+
lines.push('')
|
|
1452
|
+
|
|
1453
|
+
// 📖 Clamp cursor
|
|
1454
|
+
if (state.proxyDaemonCursor > maxRow) state.proxyDaemonCursor = maxRow
|
|
1455
|
+
|
|
1456
|
+
// 📖 Scrolling and tinting
|
|
1457
|
+
const PROXY_DAEMON_BG = chalk.bgRgb(15, 25, 45)
|
|
1458
|
+
const targetLine = cursorLineByRow[state.proxyDaemonCursor] ?? 0
|
|
1459
|
+
state.proxyDaemonScrollOffset = keepOverlayTargetVisible(
|
|
1460
|
+
state.proxyDaemonScrollOffset,
|
|
1461
|
+
targetLine,
|
|
1462
|
+
lines.length,
|
|
1463
|
+
state.terminalRows
|
|
1464
|
+
)
|
|
1465
|
+
const { visible, offset } = sliceOverlayLines(lines, state.proxyDaemonScrollOffset, state.terminalRows)
|
|
1466
|
+
state.proxyDaemonScrollOffset = offset
|
|
1467
|
+
const tintedLines = tintOverlayLines(visible, PROXY_DAEMON_BG, state.terminalCols)
|
|
1468
|
+
return tintedLines.map(l => l + EL).join('\n')
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1396
1471
|
// 📖 stopRecommendAnalysis: cleanup timers if user cancels during analysis
|
|
1397
1472
|
function stopRecommendAnalysis() {
|
|
1398
1473
|
if (state.recommendAnalysisTimer) { clearInterval(state.recommendAnalysisTimer); state.recommendAnalysisTimer = null }
|
|
@@ -1401,12 +1476,12 @@ export function createOverlayRenderers(state, deps) {
|
|
|
1401
1476
|
|
|
1402
1477
|
return {
|
|
1403
1478
|
renderSettings,
|
|
1479
|
+
renderProxyDaemon,
|
|
1404
1480
|
renderInstallEndpoints,
|
|
1405
1481
|
renderHelp,
|
|
1406
1482
|
renderLog,
|
|
1407
1483
|
renderRecommend,
|
|
1408
|
-
|
|
1409
|
-
renderBugReport,
|
|
1484
|
+
renderFeedback,
|
|
1410
1485
|
renderChangelog,
|
|
1411
1486
|
startRecommendAnalysis,
|
|
1412
1487
|
stopRecommendAnalysis,
|