free-coding-models 0.2.17 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,8 +5,13 @@
5
5
  * @details
6
6
  * This module encapsulates the full onKeyPress switch used by the TUI,
7
7
  * including settings navigation, install-endpoint flow, overlays, profile management, and
8
- * OpenCode/OpenClaw launch actions. It also keeps the live key bindings
9
- * aligned with the highlighted letters shown in the table headers.
8
+ * tool launch actions. It also keeps the live key bindings aligned with the
9
+ * highlighted letters shown in the table headers.
10
+ *
11
+ * 📖 Key J opens the FCM Proxy V2 overlay directly (with daemon status refresh).
12
+ * 📖 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.
10
15
  *
11
16
  * It also owns the "test key" model selection used by the Settings overlay.
12
17
  * Some providers expose models in `/v1/models` that are not actually callable
@@ -26,6 +31,7 @@
26
31
  */
27
32
 
28
33
  import { loadChangelog } from './changelog-loader.js'
34
+ import { resolveProxySyncToolMode } from './proxy-sync.js'
29
35
 
30
36
  // 📖 Some providers need an explicit probe model because the first catalog entry
31
37
  // 📖 is not guaranteed to be accepted by their chat endpoint.
@@ -181,7 +187,7 @@ export function createKeyHandler(ctx) {
181
187
  ENV_VAR_NAMES,
182
188
  ensureProxyRunning,
183
189
  syncToOpenCode,
184
- cleanupOpenCodeProxyConfig,
190
+ cleanupToolConfig,
185
191
  restoreOpenCodeBackup,
186
192
  checkForUpdateDetailed,
187
193
  runUpdate,
@@ -195,7 +201,6 @@ export function createKeyHandler(ctx) {
195
201
  getToolModeOrder,
196
202
  startRecommendAnalysis,
197
203
  stopRecommendAnalysis,
198
- sendFeatureRequest,
199
204
  sendBugReport,
200
205
  stopUi,
201
206
  ping,
@@ -619,81 +624,14 @@ export function createKeyHandler(ctx) {
619
624
  return
620
625
  }
621
626
 
622
- // 📖 Feature Request overlay: intercept ALL keys while overlay is active.
627
+ // 📖 Feedback overlay: intercept ALL keys while overlay is active.
623
628
  // 📖 Enter → send to Discord, Esc → cancel, Backspace → delete char, printable → append to buffer.
624
- if (state.featureRequestOpen) {
629
+ if (state.feedbackOpen) {
625
630
  if (key.ctrl && key.name === 'c') { exit(0); return }
626
631
 
627
632
  if (key.name === 'escape') {
628
- // 📖 Cancel feature request — close overlay
629
- state.featureRequestOpen = false
630
- state.featureRequestBuffer = ''
631
- state.featureRequestStatus = 'idle'
632
- state.featureRequestError = null
633
- return
634
- }
635
-
636
- if (key.name === 'return') {
637
- // 📖 Send feature request to Discord webhook
638
- const message = state.featureRequestBuffer.trim()
639
- if (message.length > 0 && state.featureRequestStatus !== 'sending') {
640
- state.featureRequestStatus = 'sending'
641
- const result = await sendFeatureRequest(message)
642
- if (result.success) {
643
- // 📖 Success — show confirmation briefly, then close overlay after 3 seconds
644
- state.featureRequestStatus = 'success'
645
- setTimeout(() => {
646
- state.featureRequestOpen = false
647
- state.featureRequestBuffer = ''
648
- state.featureRequestStatus = 'idle'
649
- state.featureRequestError = null
650
- }, 3000)
651
- } else {
652
- // 📖 Error — show error message, keep overlay open
653
- state.featureRequestStatus = 'error'
654
- state.featureRequestError = result.error || 'Unknown error'
655
- }
656
- }
657
- return
658
- }
659
-
660
- if (key.name === 'backspace') {
661
- // 📖 Don't allow editing while sending or after success
662
- if (state.featureRequestStatus === 'sending' || state.featureRequestStatus === 'success') return
663
- state.featureRequestBuffer = state.featureRequestBuffer.slice(0, -1)
664
- // 📖 Clear error status when user starts editing again
665
- if (state.featureRequestStatus === 'error') {
666
- state.featureRequestStatus = 'idle'
667
- state.featureRequestError = null
668
- }
669
- return
670
- }
671
-
672
- // 📖 Append printable characters (str is the raw character typed)
673
- // 📖 Limit to 500 characters (Discord embed description limit)
674
- if (str && str.length === 1 && !key.ctrl && !key.meta) {
675
- // 📖 Don't allow editing while sending or after success
676
- if (state.featureRequestStatus === 'sending' || state.featureRequestStatus === 'success') return
677
- if (state.featureRequestBuffer.length < 500) {
678
- state.featureRequestBuffer += str
679
- // 📖 Clear error status when user starts editing again
680
- if (state.featureRequestStatus === 'error') {
681
- state.featureRequestStatus = 'idle'
682
- state.featureRequestError = null
683
- }
684
- }
685
- }
686
- return
687
- }
688
-
689
- // 📖 Bug Report overlay: intercept ALL keys while overlay is active.
690
- // 📖 Enter → send to Discord, Esc → cancel, Backspace → delete char, printable → append to buffer.
691
- if (state.bugReportOpen) {
692
- if (key.ctrl && key.name === 'c') { exit(0); return }
693
-
694
- if (key.name === 'escape') {
695
- // 📖 Cancel bug report — close overlay
696
- state.bugReportOpen = false
633
+ // 📖 Cancel feedback — close overlay
634
+ state.feedbackOpen = false
697
635
  state.bugReportBuffer = ''
698
636
  state.bugReportStatus = 'idle'
699
637
  state.bugReportError = null
@@ -701,7 +639,7 @@ export function createKeyHandler(ctx) {
701
639
  }
702
640
 
703
641
  if (key.name === 'return') {
704
- // 📖 Send bug report to Discord webhook
642
+ // 📖 Send feedback to Discord webhook
705
643
  const message = state.bugReportBuffer.trim()
706
644
  if (message.length > 0 && state.bugReportStatus !== 'sending') {
707
645
  state.bugReportStatus = 'sending'
@@ -710,7 +648,7 @@ export function createKeyHandler(ctx) {
710
648
  // 📖 Success — show confirmation briefly, then close overlay after 3 seconds
711
649
  state.bugReportStatus = 'success'
712
650
  setTimeout(() => {
713
- state.bugReportOpen = false
651
+ state.feedbackOpen = false
714
652
  state.bugReportBuffer = ''
715
653
  state.bugReportStatus = 'idle'
716
654
  state.bugReportError = null
@@ -1022,14 +960,11 @@ export function createKeyHandler(ctx) {
1022
960
  const proxySettings = getProxySettings(state.config)
1023
961
  const providerKeys = Object.keys(sources)
1024
962
  const updateRowIdx = providerKeys.length
1025
- const proxyEnabledRowIdx = updateRowIdx + 1
1026
- const proxySyncRowIdx = updateRowIdx + 2
1027
- const proxyPortRowIdx = updateRowIdx + 3
1028
- const proxyCleanupRowIdx = updateRowIdx + 4
1029
- const changelogViewRowIdx = updateRowIdx + 5
1030
- // 📖 Profile rows start after maintenance + proxy rows + changelog row — one row per saved profile
963
+ const proxyDaemonRowIdx = updateRowIdx + 1
964
+ const changelogViewRowIdx = updateRowIdx + 2
965
+ // 📖 Profile rows start after maintenance + proxy/daemon + changelog
1031
966
  const savedProfiles = listProfiles(state.config)
1032
- const profileStartIdx = updateRowIdx + 6
967
+ const profileStartIdx = updateRowIdx + 3
1033
968
  const maxRowIdx = savedProfiles.length > 0 ? profileStartIdx + savedProfiles.length - 1 : changelogViewRowIdx
1034
969
 
1035
970
  // 📖 Edit/Add-key mode: capture typed characters for the API key
@@ -1075,32 +1010,6 @@ export function createKeyHandler(ctx) {
1075
1010
  return
1076
1011
  }
1077
1012
 
1078
- // 📖 Dedicated inline editor for the preferred proxy port. 0 = OS auto-port.
1079
- if (state.settingsProxyPortEditMode) {
1080
- if (key.name === 'return') {
1081
- const raw = state.settingsProxyPortBuffer.trim()
1082
- const parsed = raw === '' ? 0 : Number.parseInt(raw, 10)
1083
- if (!Number.isInteger(parsed) || parsed < 0 || parsed > 65535) {
1084
- state.settingsSyncStatus = { type: 'error', msg: '❌ Proxy port must be 0 (auto) or a number between 1 and 65535' }
1085
- return
1086
- }
1087
- if (!state.config.settings) state.config.settings = {}
1088
- state.config.settings.proxy = { ...proxySettings, preferredPort: parsed }
1089
- saveConfig(state.config)
1090
- state.settingsProxyPortEditMode = false
1091
- state.settingsProxyPortBuffer = ''
1092
- state.settingsSyncStatus = { type: 'success', msg: `✅ Preferred proxy port saved: ${parsed === 0 ? 'auto' : parsed}` }
1093
- } else if (key.name === 'escape') {
1094
- state.settingsProxyPortEditMode = false
1095
- state.settingsProxyPortBuffer = ''
1096
- } else if (key.name === 'backspace') {
1097
- state.settingsProxyPortBuffer = state.settingsProxyPortBuffer.slice(0, -1)
1098
- } else if (str && /^[0-9]$/.test(str) && state.settingsProxyPortBuffer.length < 5) {
1099
- state.settingsProxyPortBuffer += str
1100
- }
1101
- return
1102
- }
1103
-
1104
1013
  // 📖 Normal settings navigation
1105
1014
  if (key.name === 'escape' || key.name === 'p') {
1106
1015
  // 📖 Close settings — rebuild results to reflect provider changes
@@ -1189,18 +1098,20 @@ export function createKeyHandler(ctx) {
1189
1098
  return
1190
1099
  }
1191
1100
 
1192
- if (state.settingsCursor === proxyPortRowIdx) {
1193
- state.settingsProxyPortEditMode = true
1194
- state.settingsProxyPortBuffer = String(proxySettings.preferredPort || 0)
1195
- return
1196
- }
1197
-
1198
- if (state.settingsCursor === proxyCleanupRowIdx) {
1199
- const cleaned = cleanupOpenCodeProxyConfig()
1200
- state.settingsSyncStatus = {
1201
- type: 'success',
1202
- msg: `✅ Proxy cleanup done (${cleaned.removedProvider ? 'provider removed' : 'no provider found'}, ${cleaned.removedModel ? 'default model cleared' : 'default model unchanged'})`,
1203
- }
1101
+ // 📖 Proxy & Daemon row: Enter → open dedicated overlay
1102
+ if (state.settingsCursor === proxyDaemonRowIdx) {
1103
+ state.settingsOpen = false
1104
+ state.proxyDaemonOpen = true
1105
+ state.proxyDaemonCursor = 0
1106
+ state.proxyDaemonScrollOffset = 0
1107
+ state.proxyDaemonMessage = null
1108
+ // 📖 Refresh daemon status when entering
1109
+ try {
1110
+ const { getDaemonStatus: _gds } = await import('./daemon-manager.js')
1111
+ const st = await _gds()
1112
+ state.daemonStatus = st.status
1113
+ state.daemonInfo = st.info || null
1114
+ } catch { /* ignore */ }
1204
1115
  return
1205
1116
  }
1206
1117
 
@@ -1250,27 +1161,10 @@ export function createKeyHandler(ctx) {
1250
1161
  }
1251
1162
 
1252
1163
  if (key.name === 'space') {
1253
- if (state.settingsCursor === updateRowIdx || state.settingsCursor === proxyPortRowIdx || state.settingsCursor === proxyCleanupRowIdx || state.settingsCursor === changelogViewRowIdx) return
1164
+ if (state.settingsCursor === updateRowIdx || state.settingsCursor === proxyDaemonRowIdx || state.settingsCursor === changelogViewRowIdx) return
1254
1165
  // 📖 Profile rows don't respond to Space
1255
1166
  if (state.settingsCursor >= profileStartIdx) return
1256
1167
 
1257
- if (state.settingsCursor === proxyEnabledRowIdx || state.settingsCursor === proxySyncRowIdx) {
1258
- if (!state.config.settings) state.config.settings = {}
1259
- state.config.settings.proxy = {
1260
- ...proxySettings,
1261
- enabled: state.settingsCursor === proxyEnabledRowIdx ? !proxySettings.enabled : proxySettings.enabled,
1262
- syncToOpenCode: state.settingsCursor === proxySyncRowIdx ? !proxySettings.syncToOpenCode : proxySettings.syncToOpenCode,
1263
- }
1264
- saveConfig(state.config)
1265
- state.settingsSyncStatus = {
1266
- type: 'success',
1267
- msg: state.settingsCursor === proxyEnabledRowIdx
1268
- ? `✅ Proxy mode ${state.config.settings.proxy.enabled ? 'enabled' : 'disabled'}`
1269
- : `✅ OpenCode proxy sync ${state.config.settings.proxy.syncToOpenCode ? 'enabled' : 'disabled'}`,
1270
- }
1271
- return
1272
- }
1273
-
1274
1168
  // 📖 Toggle enabled/disabled for selected provider
1275
1169
  const pk = providerKeys[state.settingsCursor]
1276
1170
  if (!state.config.providers) state.config.providers = {}
@@ -1281,7 +1175,7 @@ export function createKeyHandler(ctx) {
1281
1175
  }
1282
1176
 
1283
1177
  if (key.name === 't') {
1284
- if (state.settingsCursor === updateRowIdx || state.settingsCursor === proxyPortRowIdx || state.settingsCursor === proxyCleanupRowIdx || state.settingsCursor === changelogViewRowIdx) return
1178
+ if (state.settingsCursor === updateRowIdx || state.settingsCursor === proxyDaemonRowIdx || state.settingsCursor === changelogViewRowIdx) return
1285
1179
  // 📖 Profile rows don't respond to T (test key)
1286
1180
  if (state.settingsCursor >= profileStartIdx) return
1287
1181
 
@@ -1387,6 +1281,274 @@ export function createKeyHandler(ctx) {
1387
1281
  return // 📖 Swallow all other keys while settings is open
1388
1282
  }
1389
1283
 
1284
+ // ─── Proxy & Daemon overlay keyboard handling ─────────────────────────────
1285
+ if (state.proxyDaemonOpen) {
1286
+ const proxySettings = getProxySettings(state.config)
1287
+ const ROW_PROXY_ENABLED = 0
1288
+ const ROW_PROXY_SYNC = 1
1289
+ const ROW_PROXY_PORT = 2
1290
+ const ROW_PROXY_CLEANUP = 3
1291
+ const ROW_DAEMON_INSTALL = 4
1292
+ const ROW_DAEMON_RESTART = 5
1293
+ const ROW_DAEMON_STOP = 6
1294
+ const ROW_DAEMON_KILL = 7
1295
+ const ROW_DAEMON_LOGS = 8
1296
+
1297
+ const daemonStatus = state.daemonStatus || 'not-installed'
1298
+ const daemonIsInstalled = daemonStatus === 'running' || daemonStatus === 'stopped' || daemonStatus === 'unhealthy' || daemonStatus === 'stale'
1299
+ const maxRow = daemonIsInstalled ? ROW_DAEMON_LOGS : ROW_DAEMON_INSTALL
1300
+
1301
+ // 📖 Port edit mode (same as old Settings behavior)
1302
+ if (state.settingsProxyPortEditMode) {
1303
+ if (key.name === 'return') {
1304
+ const parsed = parseInt(state.settingsProxyPortBuffer, 10)
1305
+ if (isNaN(parsed) || parsed < 0 || parsed > 65535) {
1306
+ state.proxyDaemonMessage = { type: 'error', msg: '❌ Port must be 0 (auto) or 1–65535', ts: Date.now() }
1307
+ return
1308
+ }
1309
+ if (!state.config.settings) state.config.settings = {}
1310
+ state.config.settings.proxy = { ...proxySettings, preferredPort: parsed }
1311
+ saveConfig(state.config)
1312
+ state.settingsProxyPortEditMode = false
1313
+ state.settingsProxyPortBuffer = ''
1314
+ state.proxyDaemonMessage = { type: 'success', msg: `✅ Preferred port saved: ${parsed === 0 ? 'auto' : parsed}`, ts: Date.now() }
1315
+ } else if (key.name === 'escape') {
1316
+ state.settingsProxyPortEditMode = false
1317
+ state.settingsProxyPortBuffer = ''
1318
+ } else if (key.name === 'backspace') {
1319
+ state.settingsProxyPortBuffer = state.settingsProxyPortBuffer.slice(0, -1)
1320
+ } else if (str && /^[0-9]$/.test(str) && state.settingsProxyPortBuffer.length < 5) {
1321
+ state.settingsProxyPortBuffer += str
1322
+ }
1323
+ return
1324
+ }
1325
+
1326
+ // 📖 Escape → back to Settings
1327
+ if (key.name === 'escape') {
1328
+ state.proxyDaemonOpen = false
1329
+ state.settingsOpen = true
1330
+ state.settingsProxyPortEditMode = false
1331
+ state.settingsProxyPortBuffer = ''
1332
+ return
1333
+ }
1334
+
1335
+ // 📖 Navigation
1336
+ if (key.name === 'up' && state.proxyDaemonCursor > 0) { state.proxyDaemonCursor--; return }
1337
+ if (key.name === 'down' && state.proxyDaemonCursor < maxRow) { state.proxyDaemonCursor++; return }
1338
+ if (key.name === 'home') { state.proxyDaemonCursor = 0; return }
1339
+ if (key.name === 'end') { state.proxyDaemonCursor = maxRow; return }
1340
+ if (key.name === 'pageup') { state.proxyDaemonCursor = Math.max(0, state.proxyDaemonCursor - 5); return }
1341
+ if (key.name === 'pagedown') { state.proxyDaemonCursor = Math.min(maxRow, state.proxyDaemonCursor + 5); return }
1342
+
1343
+ // 📖 Proxy sync now follows the current Z-selected tool automatically.
1344
+ const currentToolMode = state.mode || 'opencode'
1345
+ const currentProxyTool = resolveProxySyncToolMode(currentToolMode)
1346
+
1347
+ // 📖 Space toggles on proxy rows
1348
+ if (key.name === 'space') {
1349
+ if (state.proxyDaemonCursor === ROW_PROXY_ENABLED) {
1350
+ if (!state.config.settings) state.config.settings = {}
1351
+ state.config.settings.proxy = { ...proxySettings, enabled: !proxySettings.enabled }
1352
+ saveConfig(state.config)
1353
+ state.proxyDaemonMessage = { type: 'success', msg: `✅ Proxy mode ${state.config.settings.proxy.enabled ? 'enabled' : 'disabled'}`, ts: Date.now() }
1354
+ return
1355
+ }
1356
+ if (state.proxyDaemonCursor === ROW_PROXY_SYNC) {
1357
+ if (!currentProxyTool) {
1358
+ state.proxyDaemonMessage = { type: 'warning', msg: '⚠ Current tool does not support persisted proxy sync', ts: Date.now() }
1359
+ return
1360
+ }
1361
+ if (!state.config.settings) state.config.settings = {}
1362
+ state.config.settings.proxy = { ...proxySettings, syncToOpenCode: !proxySettings.syncToOpenCode }
1363
+ saveConfig(state.config)
1364
+ const { getToolMeta } = await import('./tool-metadata.js')
1365
+ const toolLabel = getToolMeta(currentProxyTool).label
1366
+ state.proxyDaemonMessage = { type: 'success', msg: `✅ Auto-sync to ${toolLabel} ${state.config.settings.proxy.syncToOpenCode ? 'enabled' : 'disabled'}`, ts: Date.now() }
1367
+ return
1368
+ } else {
1369
+ return
1370
+ }
1371
+ }
1372
+
1373
+ // 📖 Enter on proxy rows
1374
+ if (key.name === 'return') {
1375
+ // 📖 Proxy enabled — toggle
1376
+ if (state.proxyDaemonCursor === ROW_PROXY_ENABLED) {
1377
+ if (!state.config.settings) state.config.settings = {}
1378
+ state.config.settings.proxy = { ...proxySettings, enabled: !proxySettings.enabled }
1379
+ saveConfig(state.config)
1380
+ state.proxyDaemonMessage = { type: 'success', msg: `✅ Proxy mode ${state.config.settings.proxy.enabled ? 'enabled' : 'disabled'}`, ts: Date.now() }
1381
+ return
1382
+ }
1383
+
1384
+ // 📖 Auto-sync toggle
1385
+ if (state.proxyDaemonCursor === ROW_PROXY_SYNC) {
1386
+ if (!currentProxyTool) {
1387
+ state.proxyDaemonMessage = { type: 'warning', msg: '⚠ Current tool does not support persisted proxy sync', ts: Date.now() }
1388
+ return
1389
+ }
1390
+ if (!state.config.settings) state.config.settings = {}
1391
+ state.config.settings.proxy = { ...proxySettings, syncToOpenCode: !proxySettings.syncToOpenCode }
1392
+ saveConfig(state.config)
1393
+ const { getToolMeta } = await import('./tool-metadata.js')
1394
+ const toolLabel = getToolMeta(currentProxyTool).label
1395
+ state.proxyDaemonMessage = { type: 'success', msg: `✅ Auto-sync to ${toolLabel} ${state.config.settings.proxy.syncToOpenCode ? 'enabled' : 'disabled'}`, ts: Date.now() }
1396
+ return
1397
+ }
1398
+
1399
+ // 📖 Port — enter edit mode
1400
+ if (state.proxyDaemonCursor === ROW_PROXY_PORT) {
1401
+ state.settingsProxyPortEditMode = true
1402
+ state.settingsProxyPortBuffer = String(proxySettings.preferredPort || 0)
1403
+ return
1404
+ }
1405
+
1406
+ // 📖 Clean proxy config — generalized for active tool
1407
+ if (state.proxyDaemonCursor === ROW_PROXY_CLEANUP) {
1408
+ if (!currentProxyTool) {
1409
+ state.proxyDaemonMessage = { type: 'warning', msg: '⚠ Current tool has no persisted proxy config to clean', ts: Date.now() }
1410
+ return
1411
+ }
1412
+ const result = cleanupToolConfig(currentProxyTool)
1413
+ const { getToolMeta } = await import('./tool-metadata.js')
1414
+ const toolLabel = getToolMeta(currentProxyTool).label
1415
+ if (result.success) {
1416
+ state.proxyDaemonMessage = { type: 'success', msg: `✅ ${toolLabel} proxy config cleaned — all fcm-* entries removed`, ts: Date.now() }
1417
+ } else {
1418
+ state.proxyDaemonMessage = { type: 'error', msg: `❌ Cleanup failed: ${result.error}`, ts: Date.now() }
1419
+ }
1420
+ return
1421
+ }
1422
+
1423
+ // 📖 Install / Uninstall daemon
1424
+ if (state.proxyDaemonCursor === ROW_DAEMON_INSTALL) {
1425
+ const { getDaemonStatus: _gds, installDaemon, uninstallDaemon, getPlatformSupport } = await import('./daemon-manager.js')
1426
+ const platform = getPlatformSupport()
1427
+ if (!platform.supported) {
1428
+ state.proxyDaemonMessage = { type: 'warning', msg: `⚠ ${platform.reason}`, ts: Date.now() }
1429
+ return
1430
+ }
1431
+ const current = await _gds()
1432
+ if (current.status === 'not-installed') {
1433
+ // 📖 Install daemon
1434
+ if (!proxySettings.enabled) {
1435
+ state.config.settings.proxy.enabled = true
1436
+ }
1437
+ state.config.settings.proxy.daemonEnabled = true
1438
+ state.config.settings.proxy.daemonConsent = new Date().toISOString()
1439
+ if (!state.config.settings.proxy.preferredPort || state.config.settings.proxy.preferredPort === 0) {
1440
+ state.config.settings.proxy.preferredPort = 18045
1441
+ }
1442
+ saveConfig(state.config)
1443
+ const result = installDaemon()
1444
+ if (result.success) {
1445
+ state.proxyDaemonMessage = { type: 'success', msg: '✅ FCM Proxy V2 background service installed and started!', ts: Date.now() }
1446
+ const ns = await _gds()
1447
+ state.daemonStatus = ns.status
1448
+ state.daemonInfo = ns.info || null
1449
+ } else {
1450
+ state.proxyDaemonMessage = { type: 'error', msg: `❌ Install failed: ${result.error}`, ts: Date.now() }
1451
+ }
1452
+ } else {
1453
+ // 📖 Uninstall daemon
1454
+ const result = uninstallDaemon()
1455
+ state.config.settings.proxy.daemonEnabled = false
1456
+ saveConfig(state.config)
1457
+ if (result.success) {
1458
+ state.proxyDaemonMessage = { type: 'success', msg: '✅ FCM Proxy V2 background service uninstalled.', ts: Date.now() }
1459
+ state.daemonStatus = 'not-installed'
1460
+ state.daemonInfo = null
1461
+ } else {
1462
+ state.proxyDaemonMessage = { type: 'error', msg: `❌ Uninstall failed: ${result.error}`, ts: Date.now() }
1463
+ }
1464
+ }
1465
+ return
1466
+ }
1467
+
1468
+ // 📖 Restart daemon
1469
+ if (state.proxyDaemonCursor === ROW_DAEMON_RESTART) {
1470
+ const { restartDaemon, getDaemonStatus: _gds } = await import('./daemon-manager.js')
1471
+ const result = restartDaemon()
1472
+ if (result.success) {
1473
+ state.proxyDaemonMessage = { type: 'success', msg: '✅ FCM Proxy V2 service restarted.', ts: Date.now() }
1474
+ // 📖 Wait a bit for the daemon to start up
1475
+ setTimeout(async () => {
1476
+ try {
1477
+ const ns = await _gds()
1478
+ state.daemonStatus = ns.status
1479
+ state.daemonInfo = ns.info || null
1480
+ } catch { /* ignore */ }
1481
+ }, 2000)
1482
+ } else {
1483
+ state.proxyDaemonMessage = { type: 'error', msg: `❌ Restart failed: ${result.error}`, ts: Date.now() }
1484
+ }
1485
+ return
1486
+ }
1487
+
1488
+ // 📖 Stop daemon (SIGTERM)
1489
+ if (state.proxyDaemonCursor === ROW_DAEMON_STOP) {
1490
+ const { stopDaemon, getDaemonStatus: _gds } = await import('./daemon-manager.js')
1491
+ const result = stopDaemon()
1492
+ if (result.success) {
1493
+ const warning = result.willRestart ? ' (service may auto-restart it)' : ''
1494
+ state.proxyDaemonMessage = { type: 'success', msg: `✅ FCM Proxy V2 service stopped.${warning}`, ts: Date.now() }
1495
+ setTimeout(async () => {
1496
+ try {
1497
+ const ns = await _gds()
1498
+ state.daemonStatus = ns.status
1499
+ state.daemonInfo = ns.info || null
1500
+ } catch { /* ignore */ }
1501
+ }, 1500)
1502
+ } else {
1503
+ state.proxyDaemonMessage = { type: 'error', msg: `❌ Stop failed: ${result.error}`, ts: Date.now() }
1504
+ }
1505
+ return
1506
+ }
1507
+
1508
+ // 📖 Force kill daemon (SIGKILL) — emergency
1509
+ if (state.proxyDaemonCursor === ROW_DAEMON_KILL) {
1510
+ const { killDaemonProcess, getDaemonStatus: _gds } = await import('./daemon-manager.js')
1511
+ const result = killDaemonProcess()
1512
+ if (result.success) {
1513
+ state.proxyDaemonMessage = { type: 'success', msg: '✅ FCM Proxy V2 service force-killed (SIGKILL).', ts: Date.now() }
1514
+ const ns = await _gds()
1515
+ state.daemonStatus = ns.status
1516
+ state.daemonInfo = ns.info || null
1517
+ } else {
1518
+ state.proxyDaemonMessage = { type: 'error', msg: `❌ Kill failed: ${result.error}`, ts: Date.now() }
1519
+ }
1520
+ return
1521
+ }
1522
+
1523
+ // 📖 View daemon logs
1524
+ if (state.proxyDaemonCursor === ROW_DAEMON_LOGS) {
1525
+ const { getDaemonLogPath } = await import('./daemon-manager.js')
1526
+ const logPath = getDaemonLogPath()
1527
+ try {
1528
+ const { readFileSync, existsSync } = await import('node:fs')
1529
+ if (!existsSync(logPath)) {
1530
+ state.proxyDaemonMessage = { type: 'warning', msg: `⚠ No log file found at ${logPath}`, ts: Date.now() }
1531
+ return
1532
+ }
1533
+ const content = readFileSync(logPath, 'utf8')
1534
+ const logLines = content.split('\n')
1535
+ const last50 = logLines.slice(-50).join('\n')
1536
+ // 📖 Display in the log overlay (repurpose log view)
1537
+ state.proxyDaemonOpen = false
1538
+ state.logVisible = true
1539
+ state.logScrollOffset = 0
1540
+ state._daemonLogContent = last50
1541
+ state.proxyDaemonMessage = { type: 'success', msg: `📖 Showing last ${Math.min(50, logLines.length)} lines from ${logPath}`, ts: Date.now() }
1542
+ } catch (err) {
1543
+ state.proxyDaemonMessage = { type: 'error', msg: `❌ Could not read logs: ${err.message}`, ts: Date.now() }
1544
+ }
1545
+ return
1546
+ }
1547
+ }
1548
+
1549
+ return // 📖 Swallow all other keys while proxy/daemon overlay is open
1550
+ }
1551
+
1390
1552
  // 📖 P key: open settings screen
1391
1553
  if (key.name === 'p' && !key.shift) {
1392
1554
  state.settingsOpen = true
@@ -1397,6 +1559,13 @@ export function createKeyHandler(ctx) {
1397
1559
  state.settingsProxyPortBuffer = ''
1398
1560
  state.settingsEditBuffer = ''
1399
1561
  state.settingsScrollOffset = 0
1562
+ // 📖 Refresh daemon status when opening settings
1563
+ import('./daemon-manager.js').then(dm => {
1564
+ dm.getDaemonStatus().then(s => {
1565
+ state.daemonStatus = s.status
1566
+ state.daemonInfo = s.info || null
1567
+ }).catch(() => {})
1568
+ }).catch(() => {})
1400
1569
  return
1401
1570
  }
1402
1571
 
@@ -1593,18 +1762,25 @@ export function createKeyHandler(ctx) {
1593
1762
  return
1594
1763
  }
1595
1764
 
1596
- // 📖 J key: open Feature Request overlay (anonymous Discord feedback)
1765
+ // 📖 J key: open FCM Proxy V2 settings overlay directly (bypasses Settings screen)
1597
1766
  if (key.name === 'j') {
1598
- state.featureRequestOpen = true
1599
- state.featureRequestBuffer = ''
1600
- state.featureRequestStatus = 'idle'
1601
- state.featureRequestError = null
1767
+ state.proxyDaemonOpen = true
1768
+ state.proxyDaemonCursor = 0
1769
+ state.proxyDaemonScrollOffset = 0
1770
+ state.proxyDaemonMessage = null
1771
+ // 📖 Refresh daemon status when entering
1772
+ try {
1773
+ const { getDaemonStatus: _gds } = await import('./daemon-manager.js')
1774
+ const st = await _gds()
1775
+ state.daemonStatus = st.status
1776
+ state.daemonInfo = st.info || null
1777
+ } catch { /* ignore */ }
1602
1778
  return
1603
1779
  }
1604
1780
 
1605
- // 📖 I key: open Bug Report overlay (anonymous Discord bug reports)
1781
+ // 📖 I key: open Feedback overlay (anonymous Discord feedback)
1606
1782
  if (key.name === 'i') {
1607
- state.bugReportOpen = true
1783
+ state.feedbackOpen = true
1608
1784
  state.bugReportBuffer = ''
1609
1785
  state.bugReportStatus = 'idle'
1610
1786
  state.bugReportError = null