free-coding-models 0.3.9 → 0.3.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,14 +4,11 @@
4
4
  *
5
5
  * @details
6
6
  * This module encapsulates the full onKeyPress switch used by the TUI,
7
- * including settings navigation, install-endpoint flow, overlays, profile management, and
7
+ * including settings navigation, install-endpoint flow, overlays, and
8
8
  * tool launch actions. It also keeps the live key bindings aligned with the
9
9
  * highlighted letters shown in the table headers.
10
10
  *
11
- * 📖 Key J opens the FCM Proxy V2 overlay directly (with daemon status refresh).
12
11
  * 📖 Key I opens the unified "Feedback, bugs & requests" overlay.
13
- * 📖 The proxy overlay handles sync toggle and cleanup for the current Z-selected tool,
14
- * plus all daemon management actions.
15
12
  *
16
13
  * It also owns the "test key" model selection used by the Settings overlay.
17
14
  * Some providers expose models in `/v1/models` that are not actually callable
@@ -31,7 +28,8 @@
31
28
  */
32
29
 
33
30
  import { loadChangelog } from './changelog-loader.js'
34
- import { resolveProxySyncToolMode } from './proxy-sync.js'
31
+ import { loadConfig, replaceConfigContents } from './config.js'
32
+ import { cleanupLegacyProxyArtifacts } from './legacy-proxy-cleanup.js'
35
33
 
36
34
  // 📖 Some providers need an explicit probe model because the first catalog entry
37
35
  // 📖 is not guaranteed to be accepted by their chat endpoint.
@@ -153,30 +151,23 @@ export function buildProviderTestDetail(providerLabel, outcome, attempts = [], d
153
151
  }
154
152
 
155
153
  export function createKeyHandler(ctx) {
156
- const {
154
+ const {
157
155
  state,
158
156
  exit,
159
157
  cliArgs,
160
158
  MODELS,
161
159
  sources,
162
160
  getApiKey,
163
- getProxySettings,
164
161
  resolveApiKeys,
165
162
  addApiKey,
166
163
  removeApiKey,
167
164
  isProviderEnabled,
168
- listProfiles,
169
- loadProfile,
170
- deleteProfile,
171
- saveAsProfile,
172
- setActiveProfile,
173
165
  saveConfig,
174
166
  persistApiKeysForProvider,
175
167
  getConfiguredInstallableProviders,
176
168
  getInstallTargetModes,
177
169
  getProviderCatalogModels,
178
170
  installProviderEndpoints,
179
- CONNECTION_MODES,
180
171
  syncFavoriteFlags,
181
172
  toggleFavoriteModel,
182
173
  sortResultsWithPinnedFavorites,
@@ -186,19 +177,12 @@ export function createKeyHandler(ctx) {
186
177
  TIER_CYCLE,
187
178
  ORIGIN_CYCLE,
188
179
  ENV_VAR_NAMES,
189
- ensureProxyRunning,
190
- syncToOpenCode,
191
- cleanupToolConfig,
192
- restoreOpenCodeBackup,
193
180
  checkForUpdateDetailed,
194
181
  runUpdate,
195
182
  startOpenClaw,
196
183
  startOpenCodeDesktop,
197
184
  startOpenCode,
198
- startProxyAndLaunch,
199
185
  startExternalTool,
200
- buildProxyTopologyFromConfig,
201
- isProxyEnabledForConfig,
202
186
  getToolModeOrder,
203
187
  startRecommendAnalysis,
204
188
  stopRecommendAnalysis,
@@ -211,7 +195,6 @@ export function createKeyHandler(ctx) {
211
195
  CONTEXT_BUDGETS,
212
196
  toFavoriteKey,
213
197
  mergedModels,
214
- apiKey,
215
198
  chalk,
216
199
  setPingMode,
217
200
  noteUserActivity,
@@ -334,6 +317,39 @@ export function createKeyHandler(ctx) {
334
317
  runUpdate(latestVersion)
335
318
  }
336
319
 
320
+ // 📖 The old multi-tool proxy is discontinued. This maintenance action clears
321
+ // 📖 stale config/env/service leftovers so users stay on the stable direct path.
322
+ function runLegacyProxyCleanup() {
323
+ const summary = cleanupLegacyProxyArtifacts()
324
+ replaceConfigContents(state.config, loadConfig())
325
+
326
+ if (summary.errors.length > 0) {
327
+ const cleanedTargets = summary.removedFiles.length + summary.updatedFiles.length
328
+ const partialDetail = summary.changed
329
+ ? `Cleaned ${cleanedTargets} legacy paths, but ${summary.errors.length} items still need manual cleanup.`
330
+ : `Cleanup hit ${summary.errors.length} file errors.`
331
+ state.settingsSyncStatus = {
332
+ type: 'error',
333
+ msg: `⚠️ Proxy cleanup was partial. ${partialDetail} The old bridge is discontinued while a more stable replacement is being built.`,
334
+ }
335
+ return
336
+ }
337
+
338
+ if (summary.changed) {
339
+ const cleanedTargets = summary.removedFiles.length + summary.updatedFiles.length
340
+ state.settingsSyncStatus = {
341
+ type: 'success',
342
+ msg: `ℹ️ Removed discontinued proxy leftovers from ${cleanedTargets} path${cleanedTargets === 1 ? '' : 's'}. A much more stable replacement is coming soon.`,
343
+ }
344
+ return
345
+ }
346
+
347
+ state.settingsSyncStatus = {
348
+ type: 'success',
349
+ msg: 'ℹ️ No discontinued proxy config was found. You are already on the stable direct-provider setup.',
350
+ }
351
+ }
352
+
337
353
  function resetInstallEndpointsOverlay() {
338
354
  state.installEndpointsOpen = false
339
355
  state.installEndpointsPhase = 'providers'
@@ -382,46 +398,7 @@ export function createKeyHandler(ctx) {
382
398
  if (!key) return
383
399
  noteUserActivity()
384
400
 
385
- // 📖 Profile save mode: intercept ALL keys while inline name input is active.
386
- // 📖 Enter → save, Esc → cancel, Backspace → delete char, printable → append to buffer.
387
- if (state.profileSaveMode) {
388
- if (key.ctrl && key.name === 'c') { exit(0); return }
389
- if (key.name === 'escape') {
390
- // 📖 Cancel profile save — discard typed name
391
- state.profileSaveMode = false
392
- state.profileSaveBuffer = ''
393
- return
394
- }
395
- if (key.name === 'return') {
396
- // 📖 Confirm profile save — persist current TUI settings under typed name
397
- const name = state.profileSaveBuffer.trim()
398
- if (name.length > 0) {
399
- saveAsProfile(state.config, name, {
400
- tierFilter: TIER_CYCLE[state.tierFilterMode],
401
- sortColumn: state.sortColumn,
402
- sortAsc: state.sortDirection === 'asc',
403
- pingInterval: state.pingInterval,
404
- hideUnconfiguredModels: state.hideUnconfiguredModels,
405
- proxy: getProxySettings(state.config),
406
- })
407
- setActiveProfile(state.config, name)
408
- state.activeProfile = name
409
- saveConfig(state.config, { replaceProfileNames: [name] })
410
- }
411
- state.profileSaveMode = false
412
- state.profileSaveBuffer = ''
413
- return
414
- }
415
- if (key.name === 'backspace') {
416
- state.profileSaveBuffer = state.profileSaveBuffer.slice(0, -1)
417
- return
418
- }
419
- // 📖 Append printable characters (str is the raw character typed)
420
- if (str && str.length === 1 && !key.ctrl && !key.meta) {
421
- state.profileSaveBuffer += str
422
- }
423
- return
424
- }
401
+ // 📖 Profile system removed - API keys now persist permanently across all sessions
425
402
 
426
403
  // 📖 Install Endpoints overlay: provider → tool → connection → scope → optional model subset.
427
404
  if (state.installEndpointsOpen) {
@@ -429,7 +406,6 @@ export function createKeyHandler(ctx) {
429
406
 
430
407
  const providerChoices = getConfiguredInstallableProviders(state.config)
431
408
  const toolChoices = getInstallTargetModes()
432
- const connectionChoices = CONNECTION_MODES || []
433
409
  const modelChoices = state.installEndpointsProviderKey
434
410
  ? getProviderCatalogModels(state.installEndpointsProviderKey)
435
411
  : []
@@ -438,7 +414,6 @@ export function createKeyHandler(ctx) {
438
414
  const maxIndexByPhase = () => {
439
415
  if (state.installEndpointsPhase === 'providers') return Math.max(0, providerChoices.length - 1)
440
416
  if (state.installEndpointsPhase === 'tools') return Math.max(0, toolChoices.length - 1)
441
- if (state.installEndpointsPhase === 'connection') return Math.max(0, connectionChoices.length - 1)
442
417
  if (state.installEndpointsPhase === 'scope') return 1
443
418
  if (state.installEndpointsPhase === 'models') return Math.max(0, modelChoices.length - 1)
444
419
  return 0
@@ -481,18 +456,12 @@ export function createKeyHandler(ctx) {
481
456
  state.installEndpointsScrollOffset = 0
482
457
  return
483
458
  }
484
- if (state.installEndpointsPhase === 'connection') {
459
+ if (state.installEndpointsPhase === 'scope') {
485
460
  state.installEndpointsPhase = 'tools'
486
461
  state.installEndpointsCursor = 0
487
462
  state.installEndpointsScrollOffset = 0
488
463
  return
489
464
  }
490
- if (state.installEndpointsPhase === 'scope') {
491
- state.installEndpointsPhase = 'connection'
492
- state.installEndpointsCursor = state.installEndpointsConnectionMode === 'proxy' ? 1 : 0
493
- state.installEndpointsScrollOffset = 0
494
- return
495
- }
496
465
  if (state.installEndpointsPhase === 'models') {
497
466
  state.installEndpointsPhase = 'scope'
498
467
  state.installEndpointsCursor = state.installEndpointsScope === 'selected' ? 1 : 0
@@ -525,20 +494,7 @@ export function createKeyHandler(ctx) {
525
494
  const selectedToolMode = toolChoices[state.installEndpointsCursor]
526
495
  if (!selectedToolMode) return
527
496
  state.installEndpointsToolMode = selectedToolMode
528
- state.installEndpointsPhase = 'connection'
529
- state.installEndpointsCursor = 0
530
- state.installEndpointsScrollOffset = 0
531
- state.installEndpointsErrorMsg = null
532
- }
533
- return
534
- }
535
-
536
- // 📖 Connection mode phase: Direct Provider vs FCM Proxy
537
- if (state.installEndpointsPhase === 'connection') {
538
- if (key.name === 'return') {
539
- const selected = connectionChoices[state.installEndpointsCursor]
540
- if (!selected) return
541
- state.installEndpointsConnectionMode = selected.key
497
+ state.installEndpointsConnectionMode = 'direct'
542
498
  state.installEndpointsPhase = 'scope'
543
499
  state.installEndpointsCursor = 0
544
500
  state.installEndpointsScrollOffset = 0
@@ -709,29 +665,6 @@ export function createKeyHandler(ctx) {
709
665
  return
710
666
  }
711
667
 
712
- // 📖 Log page overlay: full keyboard navigation + key swallowing while overlay is open.
713
- if (state.logVisible) {
714
- const pageStep = Math.max(1, (state.terminalRows || 1) - 2)
715
- if (key.name === 'escape' || key.name === 'x') {
716
- state.logVisible = false
717
- return
718
- }
719
- // 📖 A key: toggle between showing all logs and limited to 500
720
- if (key.name === 'a') {
721
- state.logShowAll = !state.logShowAll
722
- state.logScrollOffset = 0
723
- return
724
- }
725
- if (key.name === 'up') { state.logScrollOffset = Math.max(0, state.logScrollOffset - 1); return }
726
- if (key.name === 'down') { state.logScrollOffset += 1; return }
727
- if (key.name === 'pageup') { state.logScrollOffset = Math.max(0, state.logScrollOffset - pageStep); return }
728
- if (key.name === 'pagedown') { state.logScrollOffset += pageStep; return }
729
- if (key.name === 'home') { state.logScrollOffset = 0; return }
730
- if (key.name === 'end') { state.logScrollOffset = Number.MAX_SAFE_INTEGER; return }
731
- if (key.ctrl && key.name === 'c') { exit(0); return }
732
- return
733
- }
734
-
735
668
  // 📖 Changelog overlay: two-phase (index + details) with keyboard navigation
736
669
  if (state.changelogOpen) {
737
670
  const pageStep = Math.max(1, (state.terminalRows || 1) - 2)
@@ -958,16 +891,13 @@ export function createKeyHandler(ctx) {
958
891
 
959
892
  // ─── Settings overlay keyboard handling ───────────────────────────────────
960
893
  if (state.settingsOpen) {
961
- const proxySettings = getProxySettings(state.config)
962
894
  const providerKeys = Object.keys(sources)
963
- const updateRowIdx = providerKeys.length
964
- const widthWarningRowIdx = updateRowIdx + 1
965
- const proxyDaemonRowIdx = widthWarningRowIdx + 1
966
- const changelogViewRowIdx = proxyDaemonRowIdx + 1
967
- // 📖 Profile rows start after maintenance + width warning + proxy/daemon + changelog
968
- const savedProfiles = listProfiles(state.config)
969
- const profileStartIdx = updateRowIdx + 5
970
- const maxRowIdx = savedProfiles.length > 0 ? profileStartIdx + savedProfiles.length - 1 : changelogViewRowIdx
895
+ const updateRowIdx = providerKeys.length
896
+ const widthWarningRowIdx = updateRowIdx + 1
897
+ const cleanupLegacyProxyRowIdx = widthWarningRowIdx + 1
898
+ const changelogViewRowIdx = cleanupLegacyProxyRowIdx + 1
899
+ // 📖 Profile system removed - API keys now persist permanently across all sessions
900
+ const maxRowIdx = changelogViewRowIdx
971
901
 
972
902
  // 📖 Edit/Add-key mode: capture typed characters for the API key
973
903
  if (state.settingsEditMode || state.settingsAddKeyMode) {
@@ -1028,8 +958,6 @@ const updateRowIdx = providerKeys.length
1028
958
  state.settingsOpen = false
1029
959
  state.settingsEditMode = false
1030
960
  state.settingsAddKeyMode = false
1031
- state.settingsProxyPortEditMode = false
1032
- state.settingsProxyPortBuffer = ''
1033
961
  state.settingsEditBuffer = ''
1034
962
  state.settingsSyncStatus = null // 📖 Clear sync status on close
1035
963
  // 📖 Rebuild results: add models from newly enabled providers, remove disabled
@@ -1114,24 +1042,12 @@ const updateRowIdx = providerKeys.length
1114
1042
  if (state.settingsCursor === widthWarningRowIdx) {
1115
1043
  if (!state.config.settings) state.config.settings = {}
1116
1044
  state.config.settings.disableWidthsWarning = !state.config.settings.disableWidthsWarning
1117
- saveConfig(state.config, { replaceProfileNames: state.activeProfile ? [state.activeProfile] : [] })
1045
+ saveConfig(state.config)
1118
1046
  return
1119
1047
  }
1120
1048
 
1121
- // 📖 Proxy & Daemon row: Enter → open dedicated overlay
1122
- if (state.settingsCursor === proxyDaemonRowIdx) {
1123
- state.settingsOpen = false
1124
- state.proxyDaemonOpen = true
1125
- state.proxyDaemonCursor = 0
1126
- state.proxyDaemonScrollOffset = 0
1127
- state.proxyDaemonMessage = null
1128
- // 📖 Refresh daemon status when entering
1129
- try {
1130
- const { getDaemonStatus: _gds } = await import('./daemon-manager.js')
1131
- const st = await _gds()
1132
- state.daemonStatus = st.status
1133
- state.daemonInfo = st.info || null
1134
- } catch { /* ignore */ }
1049
+ if (state.settingsCursor === cleanupLegacyProxyRowIdx) {
1050
+ runLegacyProxyCleanup()
1135
1051
  return
1136
1052
  }
1137
1053
 
@@ -1146,35 +1062,7 @@ const updateRowIdx = providerKeys.length
1146
1062
  return
1147
1063
  }
1148
1064
 
1149
- // 📖 Profile row: Enter load the selected profile (apply its settings live)
1150
- if (state.settingsCursor >= profileStartIdx && savedProfiles.length > 0) {
1151
- const profileIdx = state.settingsCursor - profileStartIdx
1152
- const profileName = savedProfiles[profileIdx]
1153
- if (profileName) {
1154
- const settings = loadProfile(state.config, profileName)
1155
- if (settings) {
1156
- state.sortColumn = settings.sortColumn || 'avg'
1157
- state.sortDirection = settings.sortAsc ? 'asc' : 'desc'
1158
- setPingMode(intervalToPingMode(settings.pingInterval || PING_INTERVAL), 'manual')
1159
- if (settings.tierFilter) {
1160
- const tierIdx = TIER_CYCLE.indexOf(settings.tierFilter)
1161
- if (tierIdx >= 0) state.tierFilterMode = tierIdx
1162
- } else {
1163
- state.tierFilterMode = 0
1164
- }
1165
- state.activeProfile = profileName
1166
- syncFavoriteFlags(state.results, state.config)
1167
- applyTierFilter()
1168
- const visible = state.results.filter(r => !r.hidden)
1169
- state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection)
1170
- saveConfig(state.config, {
1171
- replaceApiKeys: true,
1172
- replaceFavorites: true,
1173
- })
1174
- }
1175
- }
1176
- return
1177
- }
1065
+ // 📖 Profile system removed - API keys now persist permanently across all sessions
1178
1066
 
1179
1067
  // 📖 Enter edit mode for the selected provider's key
1180
1068
  const pk = providerKeys[state.settingsCursor]
@@ -1185,30 +1073,36 @@ const updateRowIdx = providerKeys.length
1185
1073
 
1186
1074
  if (key.name === 'space') {
1187
1075
  // 📖 Exclude certain rows from space toggle
1188
- if (state.settingsCursor === updateRowIdx || state.settingsCursor === proxyDaemonRowIdx || state.settingsCursor === changelogViewRowIdx) return
1076
+ if (
1077
+ state.settingsCursor === updateRowIdx
1078
+ || state.settingsCursor === cleanupLegacyProxyRowIdx
1079
+ || state.settingsCursor === changelogViewRowIdx
1080
+ ) return
1189
1081
  // 📖 Widths Warning toggle (disable/enable)
1190
1082
  if (state.settingsCursor === widthWarningRowIdx) {
1191
1083
  if (!state.config.settings) state.config.settings = {}
1192
1084
  state.config.settings.disableWidthsWarning = !state.config.settings.disableWidthsWarning
1193
- saveConfig(state.config, { replaceProfileNames: state.activeProfile ? [state.activeProfile] : [] })
1085
+ saveConfig(state.config)
1194
1086
  return
1195
1087
  }
1196
- // 📖 Profile rows don't respond to Space
1197
- if (state.settingsCursor >= profileStartIdx) return
1088
+ // 📖 Profile system removed - API keys now persist permanently across all sessions
1198
1089
 
1199
1090
  // 📖 Toggle enabled/disabled for selected provider
1200
1091
  const pk = providerKeys[state.settingsCursor]
1201
1092
  if (!state.config.providers) state.config.providers = {}
1202
1093
  if (!state.config.providers[pk]) state.config.providers[pk] = { enabled: true }
1203
1094
  state.config.providers[pk].enabled = !isProviderEnabled(state.config, pk)
1204
- saveConfig(state.config, { replaceProfileNames: state.activeProfile ? [state.activeProfile] : [] })
1095
+ saveConfig(state.config)
1205
1096
  return
1206
1097
  }
1207
1098
 
1208
1099
  if (key.name === 't') {
1209
- if (state.settingsCursor === updateRowIdx || state.settingsCursor === proxyDaemonRowIdx || state.settingsCursor === changelogViewRowIdx) return
1210
- // 📖 Profile rows don't respond to T (test key)
1211
- if (state.settingsCursor >= profileStartIdx) return
1100
+ if (
1101
+ state.settingsCursor === updateRowIdx
1102
+ || state.settingsCursor === cleanupLegacyProxyRowIdx
1103
+ || state.settingsCursor === changelogViewRowIdx
1104
+ ) return
1105
+ // 📖 Profile system removed - API keys now persist permanently across all sessions
1212
1106
 
1213
1107
  // 📖 Test the selected provider's key (fires a real ping)
1214
1108
  const pk = providerKeys[state.settingsCursor]
@@ -1221,71 +1115,10 @@ const updateRowIdx = providerKeys.length
1221
1115
  return
1222
1116
  }
1223
1117
 
1224
- // 📖 Backspace on a profile row delete that profile
1225
- if (key.name === 'backspace' && state.settingsCursor >= profileStartIdx && savedProfiles.length > 0) {
1226
- const profileIdx = state.settingsCursor - profileStartIdx
1227
- const profileName = savedProfiles[profileIdx]
1228
- if (profileName) {
1229
- deleteProfile(state.config, profileName)
1230
- // 📖 If the deleted profile was active, clear active state
1231
- if (state.activeProfile === profileName) {
1232
- setActiveProfile(state.config, null)
1233
- state.activeProfile = null
1234
- }
1235
- saveConfig(state.config, { removedProfileNames: [profileName] })
1236
- // 📖 Re-clamp cursor after deletion (profile list just got shorter)
1237
- const newProfiles = listProfiles(state.config)
1238
- const newMaxRowIdx = newProfiles.length > 0 ? profileStartIdx + newProfiles.length - 1 : changelogViewRowIdx
1239
- if (state.settingsCursor > newMaxRowIdx) {
1240
- state.settingsCursor = Math.max(0, newMaxRowIdx)
1241
- }
1242
- }
1243
- return
1244
- }
1118
+ // 📖 Profile system removed - API keys now persist permanently across all sessions
1245
1119
 
1246
1120
  if (key.ctrl && key.name === 'c') { exit(0); return }
1247
1121
 
1248
- // 📖 S key: sync FCM provider entries to OpenCode config (merge, don't replace)
1249
- if (key.name === 's' && !key.shift && !key.ctrl) {
1250
- try {
1251
- if (!proxySettings.enabled) {
1252
- state.settingsSyncStatus = { type: 'error', msg: '⚠ Enable Proxy mode first if you want to sync fcm-proxy into OpenCode' }
1253
- return
1254
- }
1255
- if (!proxySettings.syncToOpenCode) {
1256
- state.settingsSyncStatus = { type: 'error', msg: '⚠ Enable "Persist proxy in OpenCode" first, or use the direct OpenCode flow only' }
1257
- return
1258
- }
1259
- // 📖 Sync now also ensures proxy is running, so OpenCode can use fcm-proxy immediately.
1260
- const started = await ensureProxyRunning(state.config)
1261
- const result = syncToOpenCode(state.config, sources, mergedModels, {
1262
- proxyPort: started.port,
1263
- proxyToken: started.proxyToken,
1264
- availableModelSlugs: started.availableModelSlugs,
1265
- })
1266
- state.settingsSyncStatus = {
1267
- type: 'success',
1268
- msg: `✅ Synced ${result.providerKey} (${result.modelCount} models), proxy running on :${started.port}`,
1269
- }
1270
- } catch (err) {
1271
- state.settingsSyncStatus = { type: 'error', msg: `❌ Sync failed: ${err.message}` }
1272
- }
1273
- return
1274
- }
1275
-
1276
- // 📖 R key: restore OpenCode config from backup (opencode.json.bak)
1277
- if (key.name === 'r' && !key.shift && !key.ctrl) {
1278
- try {
1279
- const restored = restoreOpenCodeBackup()
1280
- state.settingsSyncStatus = restored
1281
- ? { type: 'success', msg: '✅ OpenCode config restored from backup' }
1282
- : { type: 'error', msg: '⚠ No backup found (opencode.json.bak)' }
1283
- } catch (err) {
1284
- state.settingsSyncStatus = { type: 'error', msg: `❌ Restore failed: ${err.message}` }
1285
- }
1286
- return
1287
- }
1288
-
1289
1122
  // 📖 + key: open add-key input (empty buffer) — appends new key on Enter
1290
1123
  if ((str === '+' || key.name === '+') && state.settingsCursor < providerKeys.length) {
1291
1124
  state.settingsEditBuffer = '' // 📖 Start with empty buffer (not existing key)
@@ -1316,291 +1149,14 @@ const updateRowIdx = providerKeys.length
1316
1149
  return // 📖 Swallow all other keys while settings is open
1317
1150
  }
1318
1151
 
1319
- // ─── Proxy & Daemon overlay keyboard handling ─────────────────────────────
1320
- if (state.proxyDaemonOpen) {
1321
- const proxySettings = getProxySettings(state.config)
1322
- const ROW_PROXY_ENABLED = 0
1323
- const ROW_PROXY_SYNC = 1
1324
- const ROW_PROXY_PORT = 2
1325
- const ROW_PROXY_CLEANUP = 3
1326
- const ROW_DAEMON_INSTALL = 4
1327
- const ROW_DAEMON_RESTART = 5
1328
- const ROW_DAEMON_STOP = 6
1329
- const ROW_DAEMON_KILL = 7
1330
- const ROW_DAEMON_LOGS = 8
1331
-
1332
- const daemonStatus = state.daemonStatus || 'not-installed'
1333
- const daemonIsInstalled = daemonStatus === 'running' || daemonStatus === 'stopped' || daemonStatus === 'unhealthy' || daemonStatus === 'stale'
1334
- const maxRow = daemonIsInstalled ? ROW_DAEMON_LOGS : ROW_DAEMON_INSTALL
1335
-
1336
- // 📖 Port edit mode (same as old Settings behavior)
1337
- if (state.settingsProxyPortEditMode) {
1338
- if (key.name === 'return') {
1339
- const parsed = parseInt(state.settingsProxyPortBuffer, 10)
1340
- if (isNaN(parsed) || parsed < 0 || parsed > 65535) {
1341
- state.proxyDaemonMessage = { type: 'error', msg: '❌ Port must be 0 (auto) or 1–65535', ts: Date.now() }
1342
- return
1343
- }
1344
- if (!state.config.settings) state.config.settings = {}
1345
- state.config.settings.proxy = { ...proxySettings, preferredPort: parsed }
1346
- saveConfig(state.config, { replaceProfileNames: state.activeProfile ? [state.activeProfile] : [] })
1347
- state.settingsProxyPortEditMode = false
1348
- state.settingsProxyPortBuffer = ''
1349
- state.proxyDaemonMessage = { type: 'success', msg: `✅ Preferred port saved: ${parsed === 0 ? 'auto' : parsed}`, ts: Date.now() }
1350
- } else if (key.name === 'escape') {
1351
- state.settingsProxyPortEditMode = false
1352
- state.settingsProxyPortBuffer = ''
1353
- } else if (key.name === 'backspace') {
1354
- state.settingsProxyPortBuffer = state.settingsProxyPortBuffer.slice(0, -1)
1355
- } else if (str && /^[0-9]$/.test(str) && state.settingsProxyPortBuffer.length < 5) {
1356
- state.settingsProxyPortBuffer += str
1357
- }
1358
- return
1359
- }
1360
-
1361
- // 📖 Escape → back to Settings
1362
- if (key.name === 'escape') {
1363
- state.proxyDaemonOpen = false
1364
- state.settingsOpen = true
1365
- state.settingsProxyPortEditMode = false
1366
- state.settingsProxyPortBuffer = ''
1367
- return
1368
- }
1369
-
1370
- // 📖 Navigation
1371
- if (key.name === 'up' && state.proxyDaemonCursor > 0) { state.proxyDaemonCursor--; return }
1372
- if (key.name === 'down' && state.proxyDaemonCursor < maxRow) { state.proxyDaemonCursor++; return }
1373
- if (key.name === 'home') { state.proxyDaemonCursor = 0; return }
1374
- if (key.name === 'end') { state.proxyDaemonCursor = maxRow; return }
1375
- if (key.name === 'pageup') { state.proxyDaemonCursor = Math.max(0, state.proxyDaemonCursor - 5); return }
1376
- if (key.name === 'pagedown') { state.proxyDaemonCursor = Math.min(maxRow, state.proxyDaemonCursor + 5); return }
1377
-
1378
- // 📖 Proxy sync now follows the current Z-selected tool automatically.
1379
- const currentToolMode = state.mode || 'opencode'
1380
- const currentProxyTool = resolveProxySyncToolMode(currentToolMode)
1381
-
1382
- // 📖 Space toggles on proxy rows
1383
- if (key.name === 'space') {
1384
- if (state.proxyDaemonCursor === ROW_PROXY_ENABLED) {
1385
- if (!state.config.settings) state.config.settings = {}
1386
- state.config.settings.proxy = { ...proxySettings, enabled: !proxySettings.enabled }
1387
- saveConfig(state.config, { replaceProfileNames: state.activeProfile ? [state.activeProfile] : [] })
1388
- state.proxyDaemonMessage = { type: 'success', msg: `✅ Proxy mode ${state.config.settings.proxy.enabled ? 'enabled' : 'disabled'}`, ts: Date.now() }
1389
- return
1390
- }
1391
- if (state.proxyDaemonCursor === ROW_PROXY_SYNC) {
1392
- if (!currentProxyTool) {
1393
- state.proxyDaemonMessage = { type: 'warning', msg: '⚠ Current tool does not support persisted proxy sync', ts: Date.now() }
1394
- return
1395
- }
1396
- if (!state.config.settings) state.config.settings = {}
1397
- state.config.settings.proxy = { ...proxySettings, syncToOpenCode: !proxySettings.syncToOpenCode }
1398
- saveConfig(state.config, { replaceProfileNames: state.activeProfile ? [state.activeProfile] : [] })
1399
- const { getToolMeta } = await import('./tool-metadata.js')
1400
- const toolLabel = getToolMeta(currentProxyTool).label
1401
- state.proxyDaemonMessage = { type: 'success', msg: `✅ Auto-sync to ${toolLabel} ${state.config.settings.proxy.syncToOpenCode ? 'enabled' : 'disabled'}`, ts: Date.now() }
1402
- return
1403
- } else {
1404
- return
1405
- }
1406
- }
1407
-
1408
- // 📖 Enter on proxy rows
1409
- if (key.name === 'return') {
1410
- // 📖 Proxy enabled — toggle
1411
- if (state.proxyDaemonCursor === ROW_PROXY_ENABLED) {
1412
- if (!state.config.settings) state.config.settings = {}
1413
- state.config.settings.proxy = { ...proxySettings, enabled: !proxySettings.enabled }
1414
- saveConfig(state.config, { replaceProfileNames: state.activeProfile ? [state.activeProfile] : [] })
1415
- state.proxyDaemonMessage = { type: 'success', msg: `✅ Proxy mode ${state.config.settings.proxy.enabled ? 'enabled' : 'disabled'}`, ts: Date.now() }
1416
- return
1417
- }
1418
-
1419
- // 📖 Auto-sync toggle
1420
- if (state.proxyDaemonCursor === ROW_PROXY_SYNC) {
1421
- if (!currentProxyTool) {
1422
- state.proxyDaemonMessage = { type: 'warning', msg: '⚠ Current tool does not support persisted proxy sync', ts: Date.now() }
1423
- return
1424
- }
1425
- if (!state.config.settings) state.config.settings = {}
1426
- state.config.settings.proxy = { ...proxySettings, syncToOpenCode: !proxySettings.syncToOpenCode }
1427
- saveConfig(state.config, { replaceProfileNames: state.activeProfile ? [state.activeProfile] : [] })
1428
- const { getToolMeta } = await import('./tool-metadata.js')
1429
- const toolLabel = getToolMeta(currentProxyTool).label
1430
- state.proxyDaemonMessage = { type: 'success', msg: `✅ Auto-sync to ${toolLabel} ${state.config.settings.proxy.syncToOpenCode ? 'enabled' : 'disabled'}`, ts: Date.now() }
1431
- return
1432
- }
1433
-
1434
- // 📖 Port — enter edit mode
1435
- if (state.proxyDaemonCursor === ROW_PROXY_PORT) {
1436
- state.settingsProxyPortEditMode = true
1437
- state.settingsProxyPortBuffer = String(proxySettings.preferredPort || 0)
1438
- return
1439
- }
1440
-
1441
- // 📖 Clean proxy config — generalized for active tool
1442
- if (state.proxyDaemonCursor === ROW_PROXY_CLEANUP) {
1443
- if (!currentProxyTool) {
1444
- state.proxyDaemonMessage = { type: 'warning', msg: '⚠ Current tool has no persisted proxy config to clean', ts: Date.now() }
1445
- return
1446
- }
1447
- const result = cleanupToolConfig(currentProxyTool)
1448
- const { getToolMeta } = await import('./tool-metadata.js')
1449
- const toolLabel = getToolMeta(currentProxyTool).label
1450
- if (result.success) {
1451
- state.proxyDaemonMessage = { type: 'success', msg: `✅ ${toolLabel} proxy config cleaned — all fcm-* entries removed`, ts: Date.now() }
1452
- } else {
1453
- state.proxyDaemonMessage = { type: 'error', msg: `❌ Cleanup failed: ${result.error}`, ts: Date.now() }
1454
- }
1455
- return
1456
- }
1457
-
1458
- // 📖 Install / Uninstall daemon
1459
- if (state.proxyDaemonCursor === ROW_DAEMON_INSTALL) {
1460
- const { getDaemonStatus: _gds, installDaemon, uninstallDaemon, getPlatformSupport } = await import('./daemon-manager.js')
1461
- const platform = getPlatformSupport()
1462
- if (!platform.supported) {
1463
- state.proxyDaemonMessage = { type: 'warning', msg: `⚠ ${platform.reason}`, ts: Date.now() }
1464
- return
1465
- }
1466
- const current = await _gds()
1467
- if (current.status === 'not-installed') {
1468
- // 📖 Install daemon
1469
- if (!proxySettings.enabled) {
1470
- state.config.settings.proxy.enabled = true
1471
- }
1472
- state.config.settings.proxy.daemonEnabled = true
1473
- state.config.settings.proxy.daemonConsent = new Date().toISOString()
1474
- if (!state.config.settings.proxy.preferredPort || state.config.settings.proxy.preferredPort === 0) {
1475
- state.config.settings.proxy.preferredPort = 18045
1476
- }
1477
- saveConfig(state.config, { replaceProfileNames: state.activeProfile ? [state.activeProfile] : [] })
1478
- const result = installDaemon()
1479
- if (result.success) {
1480
- state.proxyDaemonMessage = { type: 'success', msg: '✅ FCM Proxy V2 background service installed and started!', ts: Date.now() }
1481
- const ns = await _gds()
1482
- state.daemonStatus = ns.status
1483
- state.daemonInfo = ns.info || null
1484
- } else {
1485
- state.proxyDaemonMessage = { type: 'error', msg: `❌ Install failed: ${result.error}`, ts: Date.now() }
1486
- }
1487
- } else {
1488
- // 📖 Uninstall daemon
1489
- const result = uninstallDaemon()
1490
- state.config.settings.proxy.daemonEnabled = false
1491
- saveConfig(state.config, { replaceProfileNames: state.activeProfile ? [state.activeProfile] : [] })
1492
- if (result.success) {
1493
- state.proxyDaemonMessage = { type: 'success', msg: '✅ FCM Proxy V2 background service uninstalled.', ts: Date.now() }
1494
- state.daemonStatus = 'not-installed'
1495
- state.daemonInfo = null
1496
- } else {
1497
- state.proxyDaemonMessage = { type: 'error', msg: `❌ Uninstall failed: ${result.error}`, ts: Date.now() }
1498
- }
1499
- }
1500
- return
1501
- }
1502
-
1503
- // 📖 Restart daemon
1504
- if (state.proxyDaemonCursor === ROW_DAEMON_RESTART) {
1505
- const { restartDaemon, getDaemonStatus: _gds } = await import('./daemon-manager.js')
1506
- const result = restartDaemon()
1507
- if (result.success) {
1508
- state.proxyDaemonMessage = { type: 'success', msg: '✅ FCM Proxy V2 service restarted.', ts: Date.now() }
1509
- // 📖 Wait a bit for the daemon to start up
1510
- setTimeout(async () => {
1511
- try {
1512
- const ns = await _gds()
1513
- state.daemonStatus = ns.status
1514
- state.daemonInfo = ns.info || null
1515
- } catch { /* ignore */ }
1516
- }, 2000)
1517
- } else {
1518
- state.proxyDaemonMessage = { type: 'error', msg: `❌ Restart failed: ${result.error}`, ts: Date.now() }
1519
- }
1520
- return
1521
- }
1522
-
1523
- // 📖 Stop daemon (SIGTERM)
1524
- if (state.proxyDaemonCursor === ROW_DAEMON_STOP) {
1525
- const { stopDaemon, getDaemonStatus: _gds } = await import('./daemon-manager.js')
1526
- const result = stopDaemon()
1527
- if (result.success) {
1528
- const warning = result.willRestart ? ' (service may auto-restart it)' : ''
1529
- state.proxyDaemonMessage = { type: 'success', msg: `✅ FCM Proxy V2 service stopped.${warning}`, ts: Date.now() }
1530
- setTimeout(async () => {
1531
- try {
1532
- const ns = await _gds()
1533
- state.daemonStatus = ns.status
1534
- state.daemonInfo = ns.info || null
1535
- } catch { /* ignore */ }
1536
- }, 1500)
1537
- } else {
1538
- state.proxyDaemonMessage = { type: 'error', msg: `❌ Stop failed: ${result.error}`, ts: Date.now() }
1539
- }
1540
- return
1541
- }
1542
-
1543
- // 📖 Force kill daemon (SIGKILL) — emergency
1544
- if (state.proxyDaemonCursor === ROW_DAEMON_KILL) {
1545
- const { killDaemonProcess, getDaemonStatus: _gds } = await import('./daemon-manager.js')
1546
- const result = killDaemonProcess()
1547
- if (result.success) {
1548
- state.proxyDaemonMessage = { type: 'success', msg: '✅ FCM Proxy V2 service force-killed (SIGKILL).', ts: Date.now() }
1549
- const ns = await _gds()
1550
- state.daemonStatus = ns.status
1551
- state.daemonInfo = ns.info || null
1552
- } else {
1553
- state.proxyDaemonMessage = { type: 'error', msg: `❌ Kill failed: ${result.error}`, ts: Date.now() }
1554
- }
1555
- return
1556
- }
1557
-
1558
- // 📖 View daemon logs
1559
- if (state.proxyDaemonCursor === ROW_DAEMON_LOGS) {
1560
- const { getDaemonLogPath } = await import('./daemon-manager.js')
1561
- const logPath = getDaemonLogPath()
1562
- try {
1563
- const { readFileSync, existsSync } = await import('node:fs')
1564
- if (!existsSync(logPath)) {
1565
- state.proxyDaemonMessage = { type: 'warning', msg: `⚠ No log file found at ${logPath}`, ts: Date.now() }
1566
- return
1567
- }
1568
- const content = readFileSync(logPath, 'utf8')
1569
- const logLines = content.split('\n')
1570
- const last50 = logLines.slice(-50).join('\n')
1571
- // 📖 Display in the log overlay (repurpose log view)
1572
- state.proxyDaemonOpen = false
1573
- state.logVisible = true
1574
- state.logScrollOffset = 0
1575
- state._daemonLogContent = last50
1576
- state.proxyDaemonMessage = { type: 'success', msg: `📖 Showing last ${Math.min(50, logLines.length)} lines from ${logPath}`, ts: Date.now() }
1577
- } catch (err) {
1578
- state.proxyDaemonMessage = { type: 'error', msg: `❌ Could not read logs: ${err.message}`, ts: Date.now() }
1579
- }
1580
- return
1581
- }
1582
- }
1583
-
1584
- return // 📖 Swallow all other keys while proxy/daemon overlay is open
1585
- }
1586
-
1587
1152
  // 📖 P key: open settings screen
1588
1153
  if (key.name === 'p' && !key.shift) {
1589
1154
  state.settingsOpen = true
1590
1155
  state.settingsCursor = 0
1591
1156
  state.settingsEditMode = false
1592
1157
  state.settingsAddKeyMode = false
1593
- state.settingsProxyPortEditMode = false
1594
- state.settingsProxyPortBuffer = ''
1595
1158
  state.settingsEditBuffer = ''
1596
1159
  state.settingsScrollOffset = 0
1597
- // 📖 Refresh daemon status when opening settings
1598
- import('./daemon-manager.js').then(dm => {
1599
- dm.getDaemonStatus().then(s => {
1600
- state.daemonStatus = s.status
1601
- state.daemonInfo = s.info || null
1602
- }).catch(() => {})
1603
- }).catch(() => {})
1604
1160
  return
1605
1161
  }
1606
1162
 
@@ -1632,70 +1188,9 @@ const updateRowIdx = providerKeys.length
1632
1188
  return
1633
1189
  }
1634
1190
 
1635
- // 📖 Shift+P: cycle through profiles (or show profile picker)
1636
- if (key.name === 'p' && key.shift) {
1637
- const profiles = listProfiles(state.config)
1638
- if (profiles.length === 0) {
1639
- // 📖 No profiles saved — save current config as 'default' profile
1640
- saveAsProfile(state.config, 'default', {
1641
- tierFilter: TIER_CYCLE[state.tierFilterMode],
1642
- sortColumn: state.sortColumn,
1643
- sortAsc: state.sortDirection === 'asc',
1644
- pingInterval: state.pingInterval,
1645
- hideUnconfiguredModels: state.hideUnconfiguredModels,
1646
- proxy: getProxySettings(state.config),
1647
- })
1648
- setActiveProfile(state.config, 'default')
1649
- state.activeProfile = 'default'
1650
- saveConfig(state.config, { replaceProfileNames: ['default'] })
1651
- } else {
1652
- // 📖 Cycle to next profile (or back to null = raw config)
1653
- const currentIdx = state.activeProfile ? profiles.indexOf(state.activeProfile) : -1
1654
- const nextIdx = (currentIdx + 1) % (profiles.length + 1) // +1 for "no profile"
1655
- if (nextIdx === profiles.length) {
1656
- // 📖 Back to raw config (no profile)
1657
- setActiveProfile(state.config, null)
1658
- state.activeProfile = null
1659
- saveConfig(state.config)
1660
- } else {
1661
- const nextProfile = profiles[nextIdx]
1662
- const settings = loadProfile(state.config, nextProfile)
1663
- if (settings) {
1664
- // 📖 Apply profile's TUI settings to live state
1665
- state.sortColumn = settings.sortColumn || 'avg'
1666
- state.sortDirection = settings.sortAsc ? 'asc' : 'desc'
1667
- setPingMode(intervalToPingMode(settings.pingInterval || PING_INTERVAL), 'manual')
1668
- if (settings.tierFilter) {
1669
- const tierIdx = TIER_CYCLE.indexOf(settings.tierFilter)
1670
- if (tierIdx >= 0) state.tierFilterMode = tierIdx
1671
- } else {
1672
- state.tierFilterMode = 0
1673
- }
1674
- state.hideUnconfiguredModels = settings.hideUnconfiguredModels === true
1675
- state.activeProfile = nextProfile
1676
- // 📖 Rebuild favorites from profile data
1677
- syncFavoriteFlags(state.results, state.config)
1678
- applyTierFilter()
1679
- const visible = state.results.filter(r => !r.hidden)
1680
- state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection)
1681
- state.cursor = 0
1682
- state.scrollOffset = 0
1683
- saveConfig(state.config, {
1684
- replaceApiKeys: true,
1685
- replaceFavorites: true,
1686
- })
1687
- }
1688
- }
1689
- }
1690
- return
1691
- }
1191
+ // 📖 Profile system removed - API keys now persist permanently across all sessions
1692
1192
 
1693
- // 📖 Shift+S: enter profile save mode inline text prompt for typing a profile name
1694
- if (key.name === 's' && key.shift) {
1695
- state.profileSaveMode = true
1696
- state.profileSaveBuffer = ''
1697
- return
1698
- }
1193
+ // 📖 Profile system removed - API keys now persist permanently across all sessions
1699
1194
 
1700
1195
  // 📖 Helper: persist current UI view settings (tier, provider, sort) to config.settings
1701
1196
  // 📖 Called after every T / D / sort key so preferences survive session restarts.
@@ -1705,16 +1200,7 @@ const updateRowIdx = providerKeys.length
1705
1200
  state.config.settings.originFilter = ORIGIN_CYCLE[state.originFilterMode] ?? null
1706
1201
  state.config.settings.sortColumn = state.sortColumn
1707
1202
  state.config.settings.sortAsc = state.sortDirection === 'asc'
1708
- // 📖 Mirror into active profile too so profile captures live preferences
1709
- if (state.activeProfile && state.config.profiles?.[state.activeProfile]) {
1710
- const profile = state.config.profiles[state.activeProfile]
1711
- if (!profile.settings || typeof profile.settings !== 'object') profile.settings = {}
1712
- profile.settings.tierFilter = state.config.settings.tierFilter
1713
- profile.settings.originFilter = state.config.settings.originFilter
1714
- profile.settings.sortColumn = state.config.settings.sortColumn
1715
- profile.settings.sortAsc = state.config.settings.sortAsc
1716
- }
1717
- saveConfig(state.config, { replaceProfileNames: state.activeProfile ? [state.activeProfile] : [] })
1203
+ saveConfig(state.config)
1718
1204
  }
1719
1205
 
1720
1206
  // 📖 Shift+R: reset all UI view settings to defaults (tier, sort, provider) and clear persisted config
@@ -1728,17 +1214,7 @@ const updateRowIdx = providerKeys.length
1728
1214
  delete state.config.settings.originFilter
1729
1215
  delete state.config.settings.sortColumn
1730
1216
  delete state.config.settings.sortAsc
1731
- // 📖 Also clear in active profile if loaded
1732
- if (state.activeProfile && state.config.profiles?.[state.activeProfile]) {
1733
- const profile = state.config.profiles[state.activeProfile]
1734
- if (profile.settings) {
1735
- delete profile.settings.tierFilter
1736
- delete profile.settings.originFilter
1737
- delete profile.settings.sortColumn
1738
- delete profile.settings.sortAsc
1739
- }
1740
- }
1741
- saveConfig(state.config, { replaceProfileNames: state.activeProfile ? [state.activeProfile] : [] })
1217
+ saveConfig(state.config)
1742
1218
  applyTierFilter()
1743
1219
  const visible = state.results.filter(r => !r.hidden)
1744
1220
  state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection)
@@ -1800,22 +1276,6 @@ const updateRowIdx = providerKeys.length
1800
1276
  return
1801
1277
  }
1802
1278
 
1803
- // 📖 J key: open FCM Proxy V2 settings overlay directly (bypasses Settings screen)
1804
- if (key.name === 'j') {
1805
- state.proxyDaemonOpen = true
1806
- state.proxyDaemonCursor = 0
1807
- state.proxyDaemonScrollOffset = 0
1808
- state.proxyDaemonMessage = null
1809
- // 📖 Refresh daemon status when entering
1810
- try {
1811
- const { getDaemonStatus: _gds } = await import('./daemon-manager.js')
1812
- const st = await _gds()
1813
- state.daemonStatus = st.status
1814
- state.daemonInfo = st.info || null
1815
- } catch { /* ignore */ }
1816
- return
1817
- }
1818
-
1819
1279
  // 📖 I key: open Feedback overlay (anonymous Discord feedback)
1820
1280
  if (key.name === 'i') {
1821
1281
  state.feedbackOpen = true
@@ -1835,17 +1295,12 @@ const updateRowIdx = providerKeys.length
1835
1295
  }
1836
1296
 
1837
1297
  // 📖 E toggles hiding models whose provider has no configured API key.
1838
- // 📖 The preference is saved globally and mirrored into the active profile.
1298
+ // 📖 The preference is saved globally.
1839
1299
  if (key.name === 'e') {
1840
1300
  state.hideUnconfiguredModels = !state.hideUnconfiguredModels
1841
1301
  if (!state.config.settings || typeof state.config.settings !== 'object') state.config.settings = {}
1842
1302
  state.config.settings.hideUnconfiguredModels = state.hideUnconfiguredModels
1843
- if (state.activeProfile && state.config.profiles?.[state.activeProfile]) {
1844
- const profile = state.config.profiles[state.activeProfile]
1845
- if (!profile.settings || typeof profile.settings !== 'object') profile.settings = {}
1846
- profile.settings.hideUnconfiguredModels = state.hideUnconfiguredModels
1847
- }
1848
- saveConfig(state.config, { replaceProfileNames: state.activeProfile ? [state.activeProfile] : [] })
1303
+ saveConfig(state.config)
1849
1304
  applyTierFilter()
1850
1305
  const visible = state.results.filter(r => !r.hidden)
1851
1306
  state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection)
@@ -1907,20 +1362,7 @@ const updateRowIdx = providerKeys.length
1907
1362
  state.mode = modeOrder[nextIndex]
1908
1363
  if (!state.config.settings || typeof state.config.settings !== 'object') state.config.settings = {}
1909
1364
  state.config.settings.preferredToolMode = state.mode
1910
- if (state.activeProfile && state.config.profiles?.[state.activeProfile]) {
1911
- const profile = state.config.profiles[state.activeProfile]
1912
- if (!profile.settings || typeof profile.settings !== 'object') profile.settings = {}
1913
- profile.settings.preferredToolMode = state.mode
1914
- }
1915
- saveConfig(state.config, { replaceProfileNames: state.activeProfile ? [state.activeProfile] : [] })
1916
- return
1917
- }
1918
-
1919
- // 📖 X key: toggle the log page overlay (shows recent requests from request-log.jsonl).
1920
- // 📖 NOTE: X was previously used for ping-interval increase; that binding moved to '='.
1921
- if (key.name === 'x') {
1922
- state.logVisible = !state.logVisible
1923
- if (state.logVisible) state.logScrollOffset = 0
1365
+ saveConfig(state.config)
1924
1366
  return
1925
1367
  }
1926
1368
 
@@ -1988,20 +1430,11 @@ const updateRowIdx = providerKeys.length
1988
1430
 
1989
1431
  // 📖 Dispatch to the correct integration based on active mode
1990
1432
  if (state.mode === 'openclaw') {
1991
- await startOpenClaw(userSelected, apiKey)
1433
+ await startOpenClaw(userSelected, state.config)
1992
1434
  } else if (state.mode === 'opencode-desktop') {
1993
1435
  await startOpenCodeDesktop(userSelected, state.config)
1994
1436
  } else if (state.mode === 'opencode') {
1995
- const topology = buildProxyTopologyFromConfig(state.config)
1996
- if (isProxyEnabledForConfig(state.config) && topology.accounts.length > 0) {
1997
- await startProxyAndLaunch(userSelected, state.config)
1998
- } else {
1999
- if (isProxyEnabledForConfig(state.config) && topology.accounts.length === 0) {
2000
- console.log(chalk.yellow(' Proxy mode is enabled, but no proxy-capable API keys were found. Falling back to direct flow.'))
2001
- console.log()
2002
- }
2003
- await startOpenCode(userSelected, state.config)
2004
- }
1437
+ await startOpenCode(userSelected, state.config)
2005
1438
  } else {
2006
1439
  await startExternalTool(state.mode, userSelected, state.config)
2007
1440
  }