free-coding-models 0.1.87 → 0.1.89

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.
@@ -104,6 +104,7 @@ export function createKeyHandler(ctx) {
104
104
  MODELS,
105
105
  sources,
106
106
  getApiKey,
107
+ getProxySettings,
107
108
  resolveApiKeys,
108
109
  addApiKey,
109
110
  removeApiKey,
@@ -125,6 +126,7 @@ export function createKeyHandler(ctx) {
125
126
  ENV_VAR_NAMES,
126
127
  ensureProxyRunning,
127
128
  syncToOpenCode,
129
+ cleanupOpenCodeProxyConfig,
128
130
  restoreOpenCodeBackup,
129
131
  checkForUpdateDetailed,
130
132
  runUpdate,
@@ -132,7 +134,10 @@ export function createKeyHandler(ctx) {
132
134
  startOpenCodeDesktop,
133
135
  startOpenCode,
134
136
  startProxyAndLaunch,
137
+ startExternalTool,
135
138
  buildProxyTopologyFromConfig,
139
+ isProxyEnabledForConfig,
140
+ getToolModeOrder,
136
141
  startRecommendAnalysis,
137
142
  stopRecommendAnalysis,
138
143
  sendFeatureRequest,
@@ -261,6 +266,7 @@ export function createKeyHandler(ctx) {
261
266
  sortAsc: state.sortDirection === 'asc',
262
267
  pingInterval: state.pingInterval,
263
268
  hideUnconfiguredModels: state.hideUnconfiguredModels,
269
+ proxy: getProxySettings(state.config),
264
270
  })
265
271
  setActiveProfile(state.config, name)
266
272
  state.activeProfile = name
@@ -558,12 +564,17 @@ export function createKeyHandler(ctx) {
558
564
 
559
565
  // ─── Settings overlay keyboard handling ───────────────────────────────────
560
566
  if (state.settingsOpen) {
567
+ const proxySettings = getProxySettings(state.config)
561
568
  const providerKeys = Object.keys(sources)
562
569
  const updateRowIdx = providerKeys.length
563
- // 📖 Profile rows start after update row — one row per saved profile
570
+ const proxyEnabledRowIdx = updateRowIdx + 1
571
+ const proxySyncRowIdx = updateRowIdx + 2
572
+ const proxyPortRowIdx = updateRowIdx + 3
573
+ const proxyCleanupRowIdx = updateRowIdx + 4
574
+ // 📖 Profile rows start after maintenance + proxy rows — one row per saved profile
564
575
  const savedProfiles = listProfiles(state.config)
565
- const profileStartIdx = updateRowIdx + 1
566
- const maxRowIdx = savedProfiles.length > 0 ? profileStartIdx + savedProfiles.length - 1 : updateRowIdx
576
+ const profileStartIdx = updateRowIdx + 5
577
+ const maxRowIdx = savedProfiles.length > 0 ? profileStartIdx + savedProfiles.length - 1 : proxyCleanupRowIdx
567
578
 
568
579
  // 📖 Edit/Add-key mode: capture typed characters for the API key
569
580
  if (state.settingsEditMode || state.settingsAddKeyMode) {
@@ -608,12 +619,40 @@ export function createKeyHandler(ctx) {
608
619
  return
609
620
  }
610
621
 
622
+ // 📖 Dedicated inline editor for the preferred proxy port. 0 = OS auto-port.
623
+ if (state.settingsProxyPortEditMode) {
624
+ if (key.name === 'return') {
625
+ const raw = state.settingsProxyPortBuffer.trim()
626
+ const parsed = raw === '' ? 0 : Number.parseInt(raw, 10)
627
+ if (!Number.isInteger(parsed) || parsed < 0 || parsed > 65535) {
628
+ state.settingsSyncStatus = { type: 'error', msg: '❌ Proxy port must be 0 (auto) or a number between 1 and 65535' }
629
+ return
630
+ }
631
+ if (!state.config.settings) state.config.settings = {}
632
+ state.config.settings.proxy = { ...proxySettings, preferredPort: parsed }
633
+ saveConfig(state.config)
634
+ state.settingsProxyPortEditMode = false
635
+ state.settingsProxyPortBuffer = ''
636
+ state.settingsSyncStatus = { type: 'success', msg: `✅ Preferred proxy port saved: ${parsed === 0 ? 'auto' : parsed}` }
637
+ } else if (key.name === 'escape') {
638
+ state.settingsProxyPortEditMode = false
639
+ state.settingsProxyPortBuffer = ''
640
+ } else if (key.name === 'backspace') {
641
+ state.settingsProxyPortBuffer = state.settingsProxyPortBuffer.slice(0, -1)
642
+ } else if (str && /^[0-9]$/.test(str) && state.settingsProxyPortBuffer.length < 5) {
643
+ state.settingsProxyPortBuffer += str
644
+ }
645
+ return
646
+ }
647
+
611
648
  // 📖 Normal settings navigation
612
649
  if (key.name === 'escape' || key.name === 'p') {
613
650
  // 📖 Close settings — rebuild results to reflect provider changes
614
651
  state.settingsOpen = false
615
652
  state.settingsEditMode = false
616
653
  state.settingsAddKeyMode = false
654
+ state.settingsProxyPortEditMode = false
655
+ state.settingsProxyPortBuffer = ''
617
656
  state.settingsEditBuffer = ''
618
657
  state.settingsSyncStatus = null // 📖 Clear sync status on close
619
658
  // 📖 Rebuild results: add models from newly enabled providers, remove disabled
@@ -694,6 +733,21 @@ export function createKeyHandler(ctx) {
694
733
  return
695
734
  }
696
735
 
736
+ if (state.settingsCursor === proxyPortRowIdx) {
737
+ state.settingsProxyPortEditMode = true
738
+ state.settingsProxyPortBuffer = String(proxySettings.preferredPort || 0)
739
+ return
740
+ }
741
+
742
+ if (state.settingsCursor === proxyCleanupRowIdx) {
743
+ const cleaned = cleanupOpenCodeProxyConfig()
744
+ state.settingsSyncStatus = {
745
+ type: 'success',
746
+ msg: `✅ Proxy cleanup done (${cleaned.removedProvider ? 'provider removed' : 'no provider found'}, ${cleaned.removedModel ? 'default model cleared' : 'default model unchanged'})`,
747
+ }
748
+ return
749
+ }
750
+
697
751
  // 📖 Profile row: Enter → load the selected profile (apply its settings live)
698
752
  if (state.settingsCursor >= profileStartIdx && savedProfiles.length > 0) {
699
753
  const profileIdx = state.settingsCursor - profileStartIdx
@@ -729,10 +783,27 @@ export function createKeyHandler(ctx) {
729
783
  }
730
784
 
731
785
  if (key.name === 'space') {
732
- if (state.settingsCursor === updateRowIdx) return
786
+ if (state.settingsCursor === updateRowIdx || state.settingsCursor === proxyPortRowIdx || state.settingsCursor === proxyCleanupRowIdx) return
733
787
  // 📖 Profile rows don't respond to Space
734
788
  if (state.settingsCursor >= profileStartIdx) return
735
789
 
790
+ if (state.settingsCursor === proxyEnabledRowIdx || state.settingsCursor === proxySyncRowIdx) {
791
+ if (!state.config.settings) state.config.settings = {}
792
+ state.config.settings.proxy = {
793
+ ...proxySettings,
794
+ enabled: state.settingsCursor === proxyEnabledRowIdx ? !proxySettings.enabled : proxySettings.enabled,
795
+ syncToOpenCode: state.settingsCursor === proxySyncRowIdx ? !proxySettings.syncToOpenCode : proxySettings.syncToOpenCode,
796
+ }
797
+ saveConfig(state.config)
798
+ state.settingsSyncStatus = {
799
+ type: 'success',
800
+ msg: state.settingsCursor === proxyEnabledRowIdx
801
+ ? `✅ Proxy mode ${state.config.settings.proxy.enabled ? 'enabled' : 'disabled'}`
802
+ : `✅ OpenCode proxy sync ${state.config.settings.proxy.syncToOpenCode ? 'enabled' : 'disabled'}`,
803
+ }
804
+ return
805
+ }
806
+
736
807
  // 📖 Toggle enabled/disabled for selected provider
737
808
  const pk = providerKeys[state.settingsCursor]
738
809
  if (!state.config.providers) state.config.providers = {}
@@ -785,6 +856,14 @@ export function createKeyHandler(ctx) {
785
856
  // 📖 S key: sync FCM provider entries to OpenCode config (merge, don't replace)
786
857
  if (key.name === 's' && !key.shift && !key.ctrl) {
787
858
  try {
859
+ if (!proxySettings.enabled) {
860
+ state.settingsSyncStatus = { type: 'error', msg: '⚠ Enable Proxy mode first if you want to sync fcm-proxy into OpenCode' }
861
+ return
862
+ }
863
+ if (!proxySettings.syncToOpenCode) {
864
+ state.settingsSyncStatus = { type: 'error', msg: '⚠ Enable "Persist proxy in OpenCode" first, or use the direct OpenCode flow only' }
865
+ return
866
+ }
788
867
  // 📖 Sync now also ensures proxy is running, so OpenCode can use fcm-proxy immediately.
789
868
  const started = await ensureProxyRunning(state.config)
790
869
  const result = syncToOpenCode(state.config, sources, mergedModels, {
@@ -847,6 +926,8 @@ export function createKeyHandler(ctx) {
847
926
  state.settingsCursor = 0
848
927
  state.settingsEditMode = false
849
928
  state.settingsAddKeyMode = false
929
+ state.settingsProxyPortEditMode = false
930
+ state.settingsProxyPortBuffer = ''
850
931
  state.settingsEditBuffer = ''
851
932
  state.settingsScrollOffset = 0
852
933
  return
@@ -875,6 +956,7 @@ export function createKeyHandler(ctx) {
875
956
  sortAsc: state.sortDirection === 'asc',
876
957
  pingInterval: state.pingInterval,
877
958
  hideUnconfiguredModels: state.hideUnconfiguredModels,
959
+ proxy: getProxySettings(state.config),
878
960
  })
879
961
  setActiveProfile(state.config, 'default')
880
962
  state.activeProfile = 'default'
@@ -1054,9 +1136,9 @@ export function createKeyHandler(ctx) {
1054
1136
  return
1055
1137
  }
1056
1138
 
1057
- // 📖 Mode toggle key: Z = cycle through modes (CLI Desktop → OpenClaw)
1139
+ // 📖 Mode toggle key: Z cycles through the supported tool targets.
1058
1140
  if (key.name === 'z') {
1059
- const modeOrder = ['opencode', 'opencode-desktop', 'openclaw']
1141
+ const modeOrder = getToolModeOrder()
1060
1142
  const currentIndex = modeOrder.indexOf(state.mode)
1061
1143
  const nextIndex = (currentIndex + 1) % modeOrder.length
1062
1144
  state.mode = modeOrder[nextIndex]
@@ -1094,6 +1176,12 @@ export function createKeyHandler(ctx) {
1094
1176
  return
1095
1177
  }
1096
1178
 
1179
+ // 📖 Esc can dismiss the narrow-terminal warning immediately without quitting the app.
1180
+ if (key.name === 'escape' && state.terminalCols > 0 && state.terminalCols < 166) {
1181
+ state.widthWarningDismissed = true
1182
+ return
1183
+ }
1184
+
1097
1185
  if (key.name === 'return') { // Enter
1098
1186
  // 📖 Use the cached visible+sorted array — guaranteed to match what's on screen
1099
1187
  const selected = state.visibleSorted[state.cursor]
@@ -1121,7 +1209,7 @@ export function createKeyHandler(ctx) {
1121
1209
  const selectedApiKey = getApiKey(state.config, selected.providerKey)
1122
1210
  if (!selectedApiKey) {
1123
1211
  console.log(chalk.yellow(` Warning: No API key configured for ${selected.providerKey}.`))
1124
- console.log(chalk.yellow(` OpenCode may not be able to use ${selected.label}.`))
1212
+ console.log(chalk.yellow(` The selected tool may not be able to use ${selected.label}.`))
1125
1213
  console.log(chalk.dim(` Set ${ENV_VAR_NAMES[selected.providerKey] || selected.providerKey.toUpperCase() + '_API_KEY'} or configure via settings (P key).`))
1126
1214
  console.log()
1127
1215
  }
@@ -1132,15 +1220,19 @@ export function createKeyHandler(ctx) {
1132
1220
  await startOpenClaw(userSelected, apiKey)
1133
1221
  } else if (state.mode === 'opencode-desktop') {
1134
1222
  await startOpenCodeDesktop(userSelected, state.config)
1135
- } else {
1223
+ } else if (state.mode === 'opencode') {
1136
1224
  const topology = buildProxyTopologyFromConfig(state.config)
1137
- if (topology.accounts.length === 0) {
1138
- console.log(chalk.yellow(' No API keys found for proxy model catalog. Falling back to direct flow.'))
1139
- console.log()
1140
- await startOpenCode(userSelected, state.config)
1141
- } else {
1225
+ if (isProxyEnabledForConfig(state.config) && topology.accounts.length > 0) {
1142
1226
  await startProxyAndLaunch(userSelected, state.config)
1227
+ } else {
1228
+ if (isProxyEnabledForConfig(state.config) && topology.accounts.length === 0) {
1229
+ console.log(chalk.yellow(' Proxy mode is enabled, but no proxy-capable API keys were found. Falling back to direct flow.'))
1230
+ console.log()
1231
+ }
1232
+ await startOpenCode(userSelected, state.config)
1143
1233
  }
1234
+ } else {
1235
+ await startExternalTool(state.mode, userSelected, state.config)
1144
1236
  }
1145
1237
  process.exit(0)
1146
1238
  }
@@ -128,6 +128,30 @@ export function mergeOcConfig(ocConfig, mergedModels, proxyInfo = {}) {
128
128
  return ocConfig
129
129
  }
130
130
 
131
+ /**
132
+ * Pure cleanup: remove only the persisted FCM proxy provider and any default
133
+ * model that still points at it. Other OpenCode providers stay untouched.
134
+ *
135
+ * @param {Object} ocConfig - Existing OpenCode config object (will be mutated in-place)
136
+ * @returns {{ removedProvider: boolean, removedModel: boolean, config: Object }}
137
+ */
138
+ export function removeFcmProxyFromConfig(ocConfig) {
139
+ if (!ocConfig || typeof ocConfig !== 'object') {
140
+ return { removedProvider: false, removedModel: false, config: {} }
141
+ }
142
+
143
+ const hadProvider = Boolean(ocConfig.provider?.[FCM_PROVIDER_ID])
144
+ if (hadProvider) {
145
+ delete ocConfig.provider[FCM_PROVIDER_ID]
146
+ if (Object.keys(ocConfig.provider).length === 0) delete ocConfig.provider
147
+ }
148
+
149
+ const hadModel = typeof ocConfig.model === 'string' && ocConfig.model.startsWith(`${FCM_PROVIDER_ID}/`)
150
+ if (hadModel) delete ocConfig.model
151
+
152
+ return { removedProvider: hadProvider, removedModel: hadModel, config: ocConfig }
153
+ }
154
+
131
155
  /**
132
156
  * MERGE the single FCM proxy provider into OpenCode config.
133
157
  *
@@ -157,3 +181,20 @@ export function syncToOpenCode(fcmConfig, _sources, mergedModels, proxyInfo = {}
157
181
  path: OC_CONFIG_PATH,
158
182
  }
159
183
  }
184
+
185
+ /**
186
+ * Remove the persisted FCM proxy provider from OpenCode's config on disk.
187
+ * This is the user-facing cleanup operation for "proxy uninstall".
188
+ *
189
+ * @returns {{ removedProvider: boolean, removedModel: boolean, path: string }}
190
+ */
191
+ export function cleanupOpenCodeProxyConfig() {
192
+ const oc = loadOpenCodeConfig()
193
+ const result = removeFcmProxyFromConfig(oc)
194
+ saveOpenCodeConfig(result.config)
195
+ return {
196
+ removedProvider: result.removedProvider,
197
+ removedModel: result.removedModel,
198
+ path: OC_CONFIG_PATH,
199
+ }
200
+ }
package/src/opencode.js CHANGED
@@ -23,6 +23,7 @@
23
23
  * - `startProxyAndLaunch` — Start fcm-proxy then launch OpenCode
24
24
  * - `autoStartProxyIfSynced` — Auto-start proxy if opencode.json has fcm-proxy
25
25
  * - `ensureProxyRunning` — Ensure proxy is running (start or reuse)
26
+ * - `isProxyEnabledForConfig` — Check whether proxy mode is opted in
26
27
  *
27
28
  * @see src/opencode-sync.js — syncToOpenCode/load/save utilities
28
29
  * @see src/proxy-server.js — ProxyServer implementation
@@ -40,7 +41,7 @@ import { sources } from '../sources.js'
40
41
  import { resolveCloudflareUrl } from './ping.js'
41
42
  import { ProxyServer } from './proxy-server.js'
42
43
  import { loadOpenCodeConfig, saveOpenCodeConfig, syncToOpenCode } from './opencode-sync.js'
43
- import { getApiKey, resolveApiKeys } from './config.js'
44
+ import { getApiKey, getProxySettings, resolveApiKeys } from './config.js'
44
45
  import { ENV_VAR_NAMES, OPENCODE_MODEL_MAP, isWindows, isMac, isLinux } from './provider-metadata.js'
45
46
  import { setActiveProxy } from './render-table.js'
46
47
 
@@ -559,10 +560,25 @@ export function buildProxyTopologyFromConfig(fcmConfig) {
559
560
  return { accounts, proxyModels }
560
561
  }
561
562
 
563
+ /**
564
+ * 📖 Proxy mode is opt-in. Both launch-time proxying and persistent sync rely on
565
+ * 📖 this single helper so settings/profile changes behave consistently.
566
+ *
567
+ * @param {object} fcmConfig
568
+ * @returns {boolean}
569
+ */
570
+ export function isProxyEnabledForConfig(fcmConfig) {
571
+ return getProxySettings(fcmConfig).enabled === true
572
+ }
573
+
562
574
  export async function ensureProxyRunning(fcmConfig, { forceRestart = false } = {}) {
563
575
  registerExitHandlers()
564
576
  proxyCleanedUp = false
565
577
 
578
+ if (!isProxyEnabledForConfig(fcmConfig)) {
579
+ throw new Error('Proxy mode is disabled in Settings')
580
+ }
581
+
566
582
  if (forceRestart && activeProxy) {
567
583
  await cleanupProxy()
568
584
  }
@@ -587,7 +603,9 @@ export async function ensureProxyRunning(fcmConfig, { forceRestart = false } = {
587
603
  }
588
604
 
589
605
  const proxyToken = `fcm_${randomUUID().replace(/-/g, '')}`
590
- const proxy = new ProxyServer({ accounts, proxyApiKey: proxyToken })
606
+ const proxySettings = getProxySettings(fcmConfig)
607
+ const preferredPort = Number.isInteger(proxySettings.preferredPort) ? proxySettings.preferredPort : 0
608
+ const proxy = new ProxyServer({ port: preferredPort, accounts, proxyApiKey: proxyToken })
591
609
  const { port } = await proxy.start()
592
610
  activeProxy = proxy
593
611
  setActiveProxy(activeProxy)
@@ -598,6 +616,9 @@ export async function ensureProxyRunning(fcmConfig, { forceRestart = false } = {
598
616
 
599
617
  export async function autoStartProxyIfSynced(fcmConfig, state) {
600
618
  try {
619
+ const proxySettings = getProxySettings(fcmConfig)
620
+ if (!proxySettings.enabled || !proxySettings.syncToOpenCode) return
621
+
601
622
  const ocConfig = loadOpenCodeConfig()
602
623
  if (!ocConfig?.provider?.['fcm-proxy']) return
603
624
 
package/src/overlays.js CHANGED
@@ -23,6 +23,7 @@ export function createOverlayRenderers(state, deps) {
23
23
  PROVIDER_METADATA,
24
24
  LOCAL_VERSION,
25
25
  getApiKey,
26
+ getProxySettings,
26
27
  resolveApiKeys,
27
28
  isProviderEnabled,
28
29
  listProfiles,
@@ -49,6 +50,16 @@ export function createOverlayRenderers(state, deps) {
49
50
  getPingModel,
50
51
  } = deps
51
52
 
53
+ // 📖 Keep log token formatting aligned with the main table so the same totals
54
+ // 📖 read the same everywhere in the TUI.
55
+ const formatLogTokens = (totalTokens) => {
56
+ const safeTotal = Number(totalTokens) || 0
57
+ if (safeTotal <= 0) return '--'
58
+ if (safeTotal >= 999_500) return `${(safeTotal / 1_000_000).toFixed(2)}M`
59
+ if (safeTotal >= 1_000) return `${(safeTotal / 1_000).toFixed(2)}k`
60
+ return String(Math.floor(safeTotal))
61
+ }
62
+
52
63
  // ─── Settings screen renderer ─────────────────────────────────────────────
53
64
  // 📖 renderSettings: Draw the settings overlay in the alt screen buffer.
54
65
  // 📖 Shows all providers with their API key (masked) + enabled state.
@@ -57,6 +68,11 @@ export function createOverlayRenderers(state, deps) {
57
68
  function renderSettings() {
58
69
  const providerKeys = Object.keys(sources)
59
70
  const updateRowIdx = providerKeys.length
71
+ const proxyEnabledRowIdx = updateRowIdx + 1
72
+ const proxySyncRowIdx = updateRowIdx + 2
73
+ const proxyPortRowIdx = updateRowIdx + 3
74
+ const proxyCleanupRowIdx = updateRowIdx + 4
75
+ const proxySettings = getProxySettings(state.config)
60
76
  const EL = '\x1b[K'
61
77
  const lines = []
62
78
  const cursorLineByRow = {}
@@ -163,9 +179,37 @@ export function createOverlayRenderers(state, deps) {
163
179
  lines.push(chalk.red(` ${state.settingsUpdateError}`))
164
180
  }
165
181
 
182
+ lines.push('')
183
+ lines.push(` ${chalk.bold('🔀 Proxy')}`)
184
+ lines.push(` ${chalk.dim(' ' + '─'.repeat(112))}`)
185
+ lines.push('')
186
+
187
+ const proxyEnabledBullet = state.settingsCursor === proxyEnabledRowIdx ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
188
+ const proxyEnabledRow = `${proxyEnabledBullet}${chalk.bold('Proxy mode (opt-in)').padEnd(44)} ${proxySettings.enabled ? chalk.greenBright('Enabled') : chalk.dim('Disabled by default')}`
189
+ cursorLineByRow[proxyEnabledRowIdx] = lines.length
190
+ lines.push(state.settingsCursor === proxyEnabledRowIdx ? chalk.bgRgb(20, 45, 60)(proxyEnabledRow) : proxyEnabledRow)
191
+
192
+ const proxySyncBullet = state.settingsCursor === proxySyncRowIdx ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
193
+ const proxySyncRow = `${proxySyncBullet}${chalk.bold('Persist proxy in OpenCode').padEnd(44)} ${proxySettings.syncToOpenCode ? chalk.greenBright('Enabled') : chalk.dim('Disabled')}`
194
+ cursorLineByRow[proxySyncRowIdx] = lines.length
195
+ lines.push(state.settingsCursor === proxySyncRowIdx ? chalk.bgRgb(20, 45, 60)(proxySyncRow) : proxySyncRow)
196
+
197
+ const proxyPortBullet = state.settingsCursor === proxyPortRowIdx ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
198
+ const proxyPortValue = state.settingsProxyPortEditMode && state.settingsCursor === proxyPortRowIdx
199
+ ? chalk.cyanBright(`${state.settingsProxyPortBuffer}▏`)
200
+ : (proxySettings.preferredPort === 0 ? chalk.dim('auto (OS-assigned)') : chalk.green(String(proxySettings.preferredPort)))
201
+ const proxyPortRow = `${proxyPortBullet}${chalk.bold('Preferred proxy port').padEnd(44)} ${proxyPortValue}`
202
+ cursorLineByRow[proxyPortRowIdx] = lines.length
203
+ lines.push(state.settingsCursor === proxyPortRowIdx ? chalk.bgRgb(20, 45, 60)(proxyPortRow) : proxyPortRow)
204
+
205
+ const proxyCleanupBullet = state.settingsCursor === proxyCleanupRowIdx ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
206
+ const proxyCleanupRow = `${proxyCleanupBullet}${chalk.bold('Clean OpenCode proxy config').padEnd(44)} ${chalk.dim('Enter removes fcm-proxy from opencode.json')}`
207
+ cursorLineByRow[proxyCleanupRowIdx] = lines.length
208
+ lines.push(state.settingsCursor === proxyCleanupRowIdx ? chalk.bgRgb(45, 30, 30)(proxyCleanupRow) : proxyCleanupRow)
209
+
166
210
  // 📖 Profiles section — list saved profiles with active indicator + delete support
167
211
  const savedProfiles = listProfiles(state.config)
168
- const profileStartIdx = updateRowIdx + 1
212
+ const profileStartIdx = updateRowIdx + 5
169
213
  const maxRowIdx = savedProfiles.length > 0 ? profileStartIdx + savedProfiles.length - 1 : updateRowIdx
170
214
 
171
215
  lines.push('')
@@ -194,8 +238,10 @@ export function createOverlayRenderers(state, deps) {
194
238
  lines.push('')
195
239
  if (state.settingsEditMode) {
196
240
  lines.push(chalk.dim(' Type API key • Enter Save • Esc Cancel'))
241
+ } else if (state.settingsProxyPortEditMode) {
242
+ lines.push(chalk.dim(' Type proxy port (0 = auto) • Enter Save • Esc Cancel'))
197
243
  } else {
198
- lines.push(chalk.dim(' ↑↓ Navigate • Enter Edit key • + Add key • - Remove key • Space Toggle • T Test key • S Sync→OpenCode • R Restore backup • U Updates • ⌫ Delete profile • Esc Close'))
244
+ lines.push(chalk.dim(' ↑↓ Navigate • Enter Edit/Run • + Add key • - Remove key • Space Toggle • T Test key • S Sync→OpenCode • R Restore backup • U Updates • ⌫ Delete profile • Esc Close'))
199
245
  }
200
246
  // 📖 Show sync/restore status message if set
201
247
  if (state.settingsSyncStatus) {
@@ -283,12 +329,12 @@ export function createOverlayRenderers(state, deps) {
283
329
  lines.push(` ${chalk.yellow('W')} Toggle ping mode ${chalk.dim('(speed 2s → normal 10s → slow 30s → forced 4s)')}`)
284
330
  lines.push(` ${chalk.yellow('E')} Toggle configured models only ${chalk.dim('(enabled by default, persisted globally + in profiles)')}`)
285
331
  lines.push(` ${chalk.yellow('X')} Toggle token log page ${chalk.dim('(shows recent request usage from request-log.jsonl)')}`)
286
- lines.push(` ${chalk.yellow('Z')} Cycle tool mode ${chalk.dim('(OpenCode CLIOpenCode DesktopOpenClaw)')}`)
332
+ lines.push(` ${chalk.yellow('Z')} Cycle tool mode ${chalk.dim('(OpenCode Desktop OpenClaw Crush Goose)')}`)
287
333
  lines.push(` ${chalk.yellow('F')} Toggle favorite on selected row ${chalk.dim('(⭐ pinned at top, persisted)')}`)
288
334
  lines.push(` ${chalk.yellow('Q')} Smart Recommend ${chalk.dim('(🎯 find the best model for your task — questionnaire + live analysis)')}`)
289
335
  lines.push(` ${chalk.rgb(57, 255, 20).bold('J')} Request Feature ${chalk.dim('(📝 send anonymous feedback to the project team)')}`)
290
336
  lines.push(` ${chalk.rgb(255, 87, 51).bold('I')} Report Bug ${chalk.dim('(🐛 send anonymous bug report to the project team)')}`)
291
- lines.push(` ${chalk.yellow('P')} Open settings ${chalk.dim('(manage API keys, provider toggles, manual update)')}`)
337
+ lines.push(` ${chalk.yellow('P')} Open settings ${chalk.dim('(manage API keys, provider toggles, proxy, manual update)')}`)
292
338
  lines.push(` ${chalk.yellow('Shift+P')} Cycle config profile ${chalk.dim('(switch between saved profiles live)')}`)
293
339
  lines.push(` ${chalk.yellow('Shift+S')} Save current config as a named profile ${chalk.dim('(inline prompt — type name + Enter)')}`)
294
340
  lines.push(` ${chalk.dim('Profiles store: favorites, sort, tier filter, ping interval, configured-only filter, API keys.')}`)
@@ -311,12 +357,24 @@ export function createOverlayRenderers(state, deps) {
311
357
  lines.push(` ${chalk.cyan('free-coding-models --opencode')} ${chalk.dim('OpenCode CLI mode')}`)
312
358
  lines.push(` ${chalk.cyan('free-coding-models --opencode-desktop')} ${chalk.dim('OpenCode Desktop mode')}`)
313
359
  lines.push(` ${chalk.cyan('free-coding-models --openclaw')} ${chalk.dim('OpenClaw mode')}`)
360
+ lines.push(` ${chalk.cyan('free-coding-models --crush')} ${chalk.dim('Crush mode')}`)
361
+ lines.push(` ${chalk.cyan('free-coding-models --goose')} ${chalk.dim('Goose mode')}`)
362
+ // 📖 Temporarily disabled launchers kept out of the public help until their flows are hardened.
363
+ // lines.push(` ${chalk.cyan('free-coding-models --aider')} ${chalk.dim('Aider mode')}`)
364
+ // lines.push(` ${chalk.cyan('free-coding-models --claude-code')} ${chalk.dim('Claude Code proxy mode')}`)
365
+ // lines.push(` ${chalk.cyan('free-coding-models --codex')} ${chalk.dim('Codex CLI proxy mode')}`)
366
+ // lines.push(` ${chalk.cyan('free-coding-models --gemini')} ${chalk.dim('Gemini CLI proxy mode')}`)
367
+ // lines.push(` ${chalk.cyan('free-coding-models --qwen')} ${chalk.dim('Qwen Code mode')}`)
368
+ // lines.push(` ${chalk.cyan('free-coding-models --openhands')} ${chalk.dim('OpenHands mode')}`)
369
+ // lines.push(` ${chalk.cyan('free-coding-models --amp')} ${chalk.dim('Amp mode')}`)
370
+ // lines.push(` ${chalk.cyan('free-coding-models --pi')} ${chalk.dim('Pi mode')}`)
314
371
  lines.push(` ${chalk.cyan('free-coding-models --best')} ${chalk.dim('Only top tiers (A+, S, S+)')}`)
315
372
  lines.push(` ${chalk.cyan('free-coding-models --fiable')} ${chalk.dim('10s reliability analysis')}`)
316
373
  lines.push(` ${chalk.cyan('free-coding-models --tier S|A|B|C')} ${chalk.dim('Filter by tier letter')}`)
317
374
  lines.push(` ${chalk.cyan('free-coding-models --no-telemetry')} ${chalk.dim('Disable telemetry for this run')}`)
318
375
  lines.push(` ${chalk.cyan('free-coding-models --recommend')} ${chalk.dim('Auto-open Smart Recommend on start')}`)
319
376
  lines.push(` ${chalk.cyan('free-coding-models --profile <name>')} ${chalk.dim('Load a saved config profile')}`)
377
+ lines.push(` ${chalk.cyan('free-coding-models --clean-proxy')} ${chalk.dim('Remove persisted fcm-proxy config from OpenCode')}`)
320
378
  lines.push(` ${chalk.dim('Flags can be combined: --openclaw --tier S')}`)
321
379
  lines.push('')
322
380
  // 📖 Help overlay can be longer than viewport, so keep a dedicated scroll offset.
@@ -336,35 +394,39 @@ export function createOverlayRenderers(state, deps) {
336
394
  const lines = []
337
395
  lines.push('')
338
396
  lines.push(` ${chalk.bold('📋 Request Log')} ${chalk.dim('— recent requests • ↑↓ scroll • X or Esc close')}`)
397
+ lines.push(chalk.dim(' Works only when the multi-account proxy is enabled and requests go through it.'))
398
+ lines.push(chalk.dim(' Direct provider launches do not currently write into this log.'))
339
399
  lines.push('')
340
400
 
341
401
  // 📖 Load recent log entries — bounded read, newest-first, malformed lines skipped.
342
402
  const logRows = loadRecentLogs({ limit: 200 })
403
+ const totalTokens = logRows.reduce((sum, row) => sum + (Number(row.tokens) || 0), 0)
343
404
 
344
405
  if (logRows.length === 0) {
345
406
  lines.push(chalk.dim(' No log entries found.'))
346
407
  lines.push(chalk.dim(' Logs are written to ~/.free-coding-models/request-log.jsonl'))
347
408
  lines.push(chalk.dim(' when requests are proxied through the multi-account rotation proxy.'))
409
+ lines.push(chalk.dim(' Direct provider launches do not currently feed this token log.'))
348
410
  } else {
411
+ lines.push(` ${chalk.bold('Total Consumed:')} ${chalk.greenBright(formatLogTokens(totalTokens))}`)
412
+ lines.push('')
349
413
  // 📖 Column widths for the log table
350
414
  const W_TIME = 19
351
- const W_TYPE = 18
352
415
  const W_PROV = 14
353
416
  const W_MODEL = 36
354
417
  const W_STATUS = 8
355
- const W_TOKENS = 9
418
+ const W_TOKENS = 12
356
419
  const W_LAT = 10
357
420
 
358
421
  // 📖 Header row
359
422
  const hTime = chalk.dim('Time'.padEnd(W_TIME))
360
- const hType = chalk.dim('Type'.padEnd(W_TYPE))
361
423
  const hProv = chalk.dim('Provider'.padEnd(W_PROV))
362
424
  const hModel = chalk.dim('Model'.padEnd(W_MODEL))
363
425
  const hStatus = chalk.dim('Status'.padEnd(W_STATUS))
364
- const hTok = chalk.dim('Used'.padEnd(W_TOKENS))
426
+ const hTok = chalk.dim('Tokens Used'.padEnd(W_TOKENS))
365
427
  const hLat = chalk.dim('Latency'.padEnd(W_LAT))
366
- lines.push(` ${hTime} ${hType} ${hProv} ${hModel} ${hStatus} ${hTok} ${hLat}`)
367
- lines.push(chalk.dim(' ' + '─'.repeat(W_TIME + W_TYPE + W_PROV + W_MODEL + W_STATUS + W_TOKENS + W_LAT + 12)))
428
+ lines.push(` ${hTime} ${hProv} ${hModel} ${hStatus} ${hTok} ${hLat}`)
429
+ lines.push(chalk.dim(' ' + '─'.repeat(W_TIME + W_PROV + W_MODEL + W_STATUS + W_TOKENS + W_LAT + 10)))
368
430
 
369
431
  for (const row of logRows) {
370
432
  // 📖 Format time as HH:MM:SS (strip the date part for compactness)
@@ -389,17 +451,16 @@ export function createOverlayRenderers(state, deps) {
389
451
  statusCell = chalk.dim(sc.padEnd(W_STATUS))
390
452
  }
391
453
 
392
- const tokStr = row.tokens > 0 ? String(row.tokens) : '--'
454
+ const tokStr = formatLogTokens(row.tokens)
393
455
  const latStr = row.latency > 0 ? `${row.latency}ms` : '--'
394
456
 
395
457
  const timeCell = chalk.dim(timeStr.slice(0, W_TIME).padEnd(W_TIME))
396
- const typeCell = chalk.magenta((row.requestType || '--').slice(0, W_TYPE).padEnd(W_TYPE))
397
458
  const provCell = chalk.cyan(row.provider.slice(0, W_PROV).padEnd(W_PROV))
398
459
  const modelCell = chalk.white(row.model.slice(0, W_MODEL).padEnd(W_MODEL))
399
460
  const tokCell = chalk.dim(tokStr.padEnd(W_TOKENS))
400
461
  const latCell = chalk.dim(latStr.padEnd(W_LAT))
401
462
 
402
- lines.push(` ${timeCell} ${typeCell} ${provCell} ${modelCell} ${statusCell} ${tokCell} ${latCell}`)
463
+ lines.push(` ${timeCell} ${provCell} ${modelCell} ${statusCell} ${tokCell} ${latCell}`)
403
464
  }
404
465
  }
405
466