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.
- package/CHANGELOG.md +40 -0
- package/README.md +112 -1134
- package/bin/free-coding-models.js +34 -188
- package/package.json +2 -3
- package/src/cli-help.js +0 -18
- package/src/config.js +17 -351
- package/src/endpoint-installer.js +26 -64
- package/src/favorites.js +0 -14
- package/src/key-handler.js +74 -641
- package/src/legacy-proxy-cleanup.js +432 -0
- package/src/openclaw.js +69 -108
- package/src/opencode-config.js +48 -0
- package/src/opencode.js +6 -248
- package/src/overlays.js +26 -550
- package/src/product-flags.js +14 -0
- package/src/render-helpers.js +2 -34
- package/src/render-table.js +14 -33
- package/src/testfcm.js +90 -43
- package/src/token-usage-reader.js +9 -38
- package/src/tool-launchers.js +235 -409
- package/src/tool-metadata.js +0 -7
- package/src/utils.js +8 -77
- package/bin/fcm-proxy-daemon.js +0 -242
- package/src/account-manager.js +0 -634
- package/src/anthropic-translator.js +0 -440
- package/src/daemon-manager.js +0 -527
- package/src/error-classifier.js +0 -154
- package/src/log-reader.js +0 -195
- package/src/opencode-sync.js +0 -200
- package/src/proxy-server.js +0 -1477
- package/src/proxy-sync.js +0 -565
- package/src/proxy-topology.js +0 -85
- package/src/request-transformer.js +0 -180
- package/src/responses-translator.js +0 -423
- package/src/token-stats.js +0 -320
package/src/key-handler.js
CHANGED
|
@@ -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,
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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 === '
|
|
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.
|
|
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
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
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
|
|
1045
|
+
saveConfig(state.config)
|
|
1118
1046
|
return
|
|
1119
1047
|
}
|
|
1120
1048
|
|
|
1121
|
-
|
|
1122
|
-
|
|
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
|
|
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 (
|
|
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
|
|
1085
|
+
saveConfig(state.config)
|
|
1194
1086
|
return
|
|
1195
1087
|
}
|
|
1196
|
-
// 📖 Profile
|
|
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
|
|
1095
|
+
saveConfig(state.config)
|
|
1205
1096
|
return
|
|
1206
1097
|
}
|
|
1207
1098
|
|
|
1208
1099
|
if (key.name === 't') {
|
|
1209
|
-
if (
|
|
1210
|
-
|
|
1211
|
-
|
|
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
|
-
|
|
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
|
-
// 📖
|
|
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
|
-
// 📖
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
}
|