free-coding-models 0.2.17 → 0.3.0

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 tool cycling (ROW_PROXY_TOOL), sync toggle, cleanup
14
+ * for any syncable tool via cleanupToolConfig(), and 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
@@ -181,7 +186,7 @@ export function createKeyHandler(ctx) {
181
186
  ENV_VAR_NAMES,
182
187
  ensureProxyRunning,
183
188
  syncToOpenCode,
184
- cleanupOpenCodeProxyConfig,
189
+ cleanupToolConfig,
185
190
  restoreOpenCodeBackup,
186
191
  checkForUpdateDetailed,
187
192
  runUpdate,
@@ -195,7 +200,6 @@ export function createKeyHandler(ctx) {
195
200
  getToolModeOrder,
196
201
  startRecommendAnalysis,
197
202
  stopRecommendAnalysis,
198
- sendFeatureRequest,
199
203
  sendBugReport,
200
204
  stopUi,
201
205
  ping,
@@ -619,81 +623,14 @@ export function createKeyHandler(ctx) {
619
623
  return
620
624
  }
621
625
 
622
- // 📖 Feature Request overlay: intercept ALL keys while overlay is active.
626
+ // 📖 Feedback overlay: intercept ALL keys while overlay is active.
623
627
  // 📖 Enter → send to Discord, Esc → cancel, Backspace → delete char, printable → append to buffer.
624
- if (state.featureRequestOpen) {
628
+ if (state.feedbackOpen) {
625
629
  if (key.ctrl && key.name === 'c') { exit(0); return }
626
630
 
627
631
  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
632
+ // 📖 Cancel feedback — close overlay
633
+ state.feedbackOpen = false
697
634
  state.bugReportBuffer = ''
698
635
  state.bugReportStatus = 'idle'
699
636
  state.bugReportError = null
@@ -701,7 +638,7 @@ export function createKeyHandler(ctx) {
701
638
  }
702
639
 
703
640
  if (key.name === 'return') {
704
- // 📖 Send bug report to Discord webhook
641
+ // 📖 Send feedback to Discord webhook
705
642
  const message = state.bugReportBuffer.trim()
706
643
  if (message.length > 0 && state.bugReportStatus !== 'sending') {
707
644
  state.bugReportStatus = 'sending'
@@ -710,7 +647,7 @@ export function createKeyHandler(ctx) {
710
647
  // 📖 Success — show confirmation briefly, then close overlay after 3 seconds
711
648
  state.bugReportStatus = 'success'
712
649
  setTimeout(() => {
713
- state.bugReportOpen = false
650
+ state.feedbackOpen = false
714
651
  state.bugReportBuffer = ''
715
652
  state.bugReportStatus = 'idle'
716
653
  state.bugReportError = null
@@ -1022,14 +959,11 @@ export function createKeyHandler(ctx) {
1022
959
  const proxySettings = getProxySettings(state.config)
1023
960
  const providerKeys = Object.keys(sources)
1024
961
  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
962
+ const proxyDaemonRowIdx = updateRowIdx + 1
963
+ const changelogViewRowIdx = updateRowIdx + 2
964
+ // 📖 Profile rows start after maintenance + proxy/daemon + changelog
1031
965
  const savedProfiles = listProfiles(state.config)
1032
- const profileStartIdx = updateRowIdx + 6
966
+ const profileStartIdx = updateRowIdx + 3
1033
967
  const maxRowIdx = savedProfiles.length > 0 ? profileStartIdx + savedProfiles.length - 1 : changelogViewRowIdx
1034
968
 
1035
969
  // 📖 Edit/Add-key mode: capture typed characters for the API key
@@ -1075,32 +1009,6 @@ export function createKeyHandler(ctx) {
1075
1009
  return
1076
1010
  }
1077
1011
 
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
1012
  // 📖 Normal settings navigation
1105
1013
  if (key.name === 'escape' || key.name === 'p') {
1106
1014
  // 📖 Close settings — rebuild results to reflect provider changes
@@ -1189,18 +1097,20 @@ export function createKeyHandler(ctx) {
1189
1097
  return
1190
1098
  }
1191
1099
 
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
- }
1100
+ // 📖 Proxy & Daemon row: Enter → open dedicated overlay
1101
+ if (state.settingsCursor === proxyDaemonRowIdx) {
1102
+ state.settingsOpen = false
1103
+ state.proxyDaemonOpen = true
1104
+ state.proxyDaemonCursor = 0
1105
+ state.proxyDaemonScrollOffset = 0
1106
+ state.proxyDaemonMessage = null
1107
+ // 📖 Refresh daemon status when entering
1108
+ try {
1109
+ const { getDaemonStatus: _gds } = await import('./daemon-manager.js')
1110
+ const st = await _gds()
1111
+ state.daemonStatus = st.status
1112
+ state.daemonInfo = st.info || null
1113
+ } catch { /* ignore */ }
1204
1114
  return
1205
1115
  }
1206
1116
 
@@ -1250,27 +1160,10 @@ export function createKeyHandler(ctx) {
1250
1160
  }
1251
1161
 
1252
1162
  if (key.name === 'space') {
1253
- if (state.settingsCursor === updateRowIdx || state.settingsCursor === proxyPortRowIdx || state.settingsCursor === proxyCleanupRowIdx || state.settingsCursor === changelogViewRowIdx) return
1163
+ if (state.settingsCursor === updateRowIdx || state.settingsCursor === proxyDaemonRowIdx || state.settingsCursor === changelogViewRowIdx) return
1254
1164
  // 📖 Profile rows don't respond to Space
1255
1165
  if (state.settingsCursor >= profileStartIdx) return
1256
1166
 
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
1167
  // 📖 Toggle enabled/disabled for selected provider
1275
1168
  const pk = providerKeys[state.settingsCursor]
1276
1169
  if (!state.config.providers) state.config.providers = {}
@@ -1281,7 +1174,7 @@ export function createKeyHandler(ctx) {
1281
1174
  }
1282
1175
 
1283
1176
  if (key.name === 't') {
1284
- if (state.settingsCursor === updateRowIdx || state.settingsCursor === proxyPortRowIdx || state.settingsCursor === proxyCleanupRowIdx || state.settingsCursor === changelogViewRowIdx) return
1177
+ if (state.settingsCursor === updateRowIdx || state.settingsCursor === proxyDaemonRowIdx || state.settingsCursor === changelogViewRowIdx) return
1285
1178
  // 📖 Profile rows don't respond to T (test key)
1286
1179
  if (state.settingsCursor >= profileStartIdx) return
1287
1180
 
@@ -1387,6 +1280,278 @@ export function createKeyHandler(ctx) {
1387
1280
  return // 📖 Swallow all other keys while settings is open
1388
1281
  }
1389
1282
 
1283
+ // ─── Proxy & Daemon overlay keyboard handling ─────────────────────────────
1284
+ if (state.proxyDaemonOpen) {
1285
+ const proxySettings = getProxySettings(state.config)
1286
+ const ROW_PROXY_ENABLED = 0
1287
+ const ROW_PROXY_TOOL = 1
1288
+ const ROW_PROXY_SYNC = 2
1289
+ const ROW_PROXY_PORT = 3
1290
+ const ROW_PROXY_CLEANUP = 4
1291
+ const ROW_DAEMON_INSTALL = 5
1292
+ const ROW_DAEMON_RESTART = 6
1293
+ const ROW_DAEMON_STOP = 7
1294
+ const ROW_DAEMON_KILL = 8
1295
+ const ROW_DAEMON_LOGS = 9
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
+ // 📖 Resolve active proxy tool (persisted or fallback to Z-mode)
1344
+ const activeProxyTool = proxySettings.activeTool || state.mode || 'opencode'
1345
+
1346
+ // 📖 Space toggles on proxy rows
1347
+ if (key.name === 'space') {
1348
+ if (state.proxyDaemonCursor === ROW_PROXY_ENABLED) {
1349
+ if (!state.config.settings) state.config.settings = {}
1350
+ state.config.settings.proxy = { ...proxySettings, enabled: !proxySettings.enabled }
1351
+ saveConfig(state.config)
1352
+ state.proxyDaemonMessage = { type: 'success', msg: `✅ Proxy mode ${state.config.settings.proxy.enabled ? 'enabled' : 'disabled'}`, ts: Date.now() }
1353
+ return
1354
+ }
1355
+ if (state.proxyDaemonCursor === ROW_PROXY_TOOL) {
1356
+ // 📖 Space also cycles tool (same as Enter on this row)
1357
+ } else if (state.proxyDaemonCursor === ROW_PROXY_SYNC) {
1358
+ if (!state.config.settings) state.config.settings = {}
1359
+ state.config.settings.proxy = { ...proxySettings, syncToOpenCode: !proxySettings.syncToOpenCode }
1360
+ saveConfig(state.config)
1361
+ const { getToolMeta } = await import('./tool-metadata.js')
1362
+ const toolLabel = getToolMeta(activeProxyTool).label
1363
+ state.proxyDaemonMessage = { type: 'success', msg: `✅ Auto-sync to ${toolLabel} ${state.config.settings.proxy.syncToOpenCode ? 'enabled' : 'disabled'}`, ts: Date.now() }
1364
+ return
1365
+ } else {
1366
+ return
1367
+ }
1368
+ }
1369
+
1370
+ // 📖 Enter or Space on ROW_PROXY_TOOL → cycle active tool
1371
+ if ((key.name === 'return' || key.name === 'space') && state.proxyDaemonCursor === ROW_PROXY_TOOL) {
1372
+ const { PROXY_SYNCABLE_TOOLS } = await import('./proxy-sync.js')
1373
+ const currentIdx = PROXY_SYNCABLE_TOOLS.indexOf(activeProxyTool)
1374
+ const nextIdx = (currentIdx + 1) % PROXY_SYNCABLE_TOOLS.length
1375
+ const nextTool = PROXY_SYNCABLE_TOOLS[nextIdx]
1376
+ if (!state.config.settings) state.config.settings = {}
1377
+ state.config.settings.proxy = { ...proxySettings, activeTool: nextTool }
1378
+ saveConfig(state.config)
1379
+ const { getToolMeta } = await import('./tool-metadata.js')
1380
+ state.proxyDaemonMessage = { type: 'success', msg: `✅ Active tool: ${getToolMeta(nextTool).emoji} ${getToolMeta(nextTool).label}`, ts: Date.now() }
1381
+ return
1382
+ }
1383
+
1384
+ // 📖 Enter on proxy rows
1385
+ if (key.name === 'return') {
1386
+ // 📖 Proxy enabled — toggle
1387
+ if (state.proxyDaemonCursor === ROW_PROXY_ENABLED) {
1388
+ if (!state.config.settings) state.config.settings = {}
1389
+ state.config.settings.proxy = { ...proxySettings, enabled: !proxySettings.enabled }
1390
+ saveConfig(state.config)
1391
+ state.proxyDaemonMessage = { type: 'success', msg: `✅ Proxy mode ${state.config.settings.proxy.enabled ? 'enabled' : 'disabled'}`, ts: Date.now() }
1392
+ return
1393
+ }
1394
+
1395
+ // 📖 Auto-sync toggle
1396
+ if (state.proxyDaemonCursor === ROW_PROXY_SYNC) {
1397
+ if (!state.config.settings) state.config.settings = {}
1398
+ state.config.settings.proxy = { ...proxySettings, syncToOpenCode: !proxySettings.syncToOpenCode }
1399
+ saveConfig(state.config)
1400
+ const { getToolMeta } = await import('./tool-metadata.js')
1401
+ const toolLabel = getToolMeta(activeProxyTool).label
1402
+ state.proxyDaemonMessage = { type: 'success', msg: `✅ Auto-sync to ${toolLabel} ${state.config.settings.proxy.syncToOpenCode ? 'enabled' : 'disabled'}`, ts: Date.now() }
1403
+ return
1404
+ }
1405
+
1406
+ // 📖 Port — enter edit mode
1407
+ if (state.proxyDaemonCursor === ROW_PROXY_PORT) {
1408
+ state.settingsProxyPortEditMode = true
1409
+ state.settingsProxyPortBuffer = String(proxySettings.preferredPort || 0)
1410
+ return
1411
+ }
1412
+
1413
+ // 📖 Clean proxy config — generalized for active tool
1414
+ if (state.proxyDaemonCursor === ROW_PROXY_CLEANUP) {
1415
+ const result = cleanupToolConfig(activeProxyTool)
1416
+ const { getToolMeta } = await import('./tool-metadata.js')
1417
+ const toolLabel = getToolMeta(activeProxyTool).label
1418
+ if (result.success) {
1419
+ state.proxyDaemonMessage = { type: 'success', msg: `✅ ${toolLabel} proxy config cleaned — all fcm-* entries removed`, ts: Date.now() }
1420
+ } else {
1421
+ state.proxyDaemonMessage = { type: 'error', msg: `❌ Cleanup failed: ${result.error}`, ts: Date.now() }
1422
+ }
1423
+ return
1424
+ }
1425
+
1426
+ // 📖 Install / Uninstall daemon
1427
+ if (state.proxyDaemonCursor === ROW_DAEMON_INSTALL) {
1428
+ const { getDaemonStatus: _gds, installDaemon, uninstallDaemon, getPlatformSupport } = await import('./daemon-manager.js')
1429
+ const platform = getPlatformSupport()
1430
+ if (!platform.supported) {
1431
+ state.proxyDaemonMessage = { type: 'warning', msg: `⚠ ${platform.reason}`, ts: Date.now() }
1432
+ return
1433
+ }
1434
+ const current = await _gds()
1435
+ if (current.status === 'not-installed') {
1436
+ // 📖 Install daemon
1437
+ if (!proxySettings.enabled) {
1438
+ state.config.settings.proxy.enabled = true
1439
+ }
1440
+ state.config.settings.proxy.daemonEnabled = true
1441
+ state.config.settings.proxy.daemonConsent = new Date().toISOString()
1442
+ if (!state.config.settings.proxy.preferredPort || state.config.settings.proxy.preferredPort === 0) {
1443
+ state.config.settings.proxy.preferredPort = 18045
1444
+ }
1445
+ saveConfig(state.config)
1446
+ const result = installDaemon()
1447
+ if (result.success) {
1448
+ state.proxyDaemonMessage = { type: 'success', msg: '✅ FCM Proxy V2 background service installed and started!', ts: Date.now() }
1449
+ const ns = await _gds()
1450
+ state.daemonStatus = ns.status
1451
+ state.daemonInfo = ns.info || null
1452
+ } else {
1453
+ state.proxyDaemonMessage = { type: 'error', msg: `❌ Install failed: ${result.error}`, ts: Date.now() }
1454
+ }
1455
+ } else {
1456
+ // 📖 Uninstall daemon
1457
+ const result = uninstallDaemon()
1458
+ state.config.settings.proxy.daemonEnabled = false
1459
+ saveConfig(state.config)
1460
+ if (result.success) {
1461
+ state.proxyDaemonMessage = { type: 'success', msg: '✅ FCM Proxy V2 background service uninstalled.', ts: Date.now() }
1462
+ state.daemonStatus = 'not-installed'
1463
+ state.daemonInfo = null
1464
+ } else {
1465
+ state.proxyDaemonMessage = { type: 'error', msg: `❌ Uninstall failed: ${result.error}`, ts: Date.now() }
1466
+ }
1467
+ }
1468
+ return
1469
+ }
1470
+
1471
+ // 📖 Restart daemon
1472
+ if (state.proxyDaemonCursor === ROW_DAEMON_RESTART) {
1473
+ const { restartDaemon, getDaemonStatus: _gds } = await import('./daemon-manager.js')
1474
+ const result = restartDaemon()
1475
+ if (result.success) {
1476
+ state.proxyDaemonMessage = { type: 'success', msg: '✅ FCM Proxy V2 service restarted.', ts: Date.now() }
1477
+ // 📖 Wait a bit for the daemon to start up
1478
+ setTimeout(async () => {
1479
+ try {
1480
+ const ns = await _gds()
1481
+ state.daemonStatus = ns.status
1482
+ state.daemonInfo = ns.info || null
1483
+ } catch { /* ignore */ }
1484
+ }, 2000)
1485
+ } else {
1486
+ state.proxyDaemonMessage = { type: 'error', msg: `❌ Restart failed: ${result.error}`, ts: Date.now() }
1487
+ }
1488
+ return
1489
+ }
1490
+
1491
+ // 📖 Stop daemon (SIGTERM)
1492
+ if (state.proxyDaemonCursor === ROW_DAEMON_STOP) {
1493
+ const { stopDaemon, getDaemonStatus: _gds } = await import('./daemon-manager.js')
1494
+ const result = stopDaemon()
1495
+ if (result.success) {
1496
+ const warning = result.willRestart ? ' (service may auto-restart it)' : ''
1497
+ state.proxyDaemonMessage = { type: 'success', msg: `✅ FCM Proxy V2 service stopped.${warning}`, ts: Date.now() }
1498
+ setTimeout(async () => {
1499
+ try {
1500
+ const ns = await _gds()
1501
+ state.daemonStatus = ns.status
1502
+ state.daemonInfo = ns.info || null
1503
+ } catch { /* ignore */ }
1504
+ }, 1500)
1505
+ } else {
1506
+ state.proxyDaemonMessage = { type: 'error', msg: `❌ Stop failed: ${result.error}`, ts: Date.now() }
1507
+ }
1508
+ return
1509
+ }
1510
+
1511
+ // 📖 Force kill daemon (SIGKILL) — emergency
1512
+ if (state.proxyDaemonCursor === ROW_DAEMON_KILL) {
1513
+ const { killDaemonProcess, getDaemonStatus: _gds } = await import('./daemon-manager.js')
1514
+ const result = killDaemonProcess()
1515
+ if (result.success) {
1516
+ state.proxyDaemonMessage = { type: 'success', msg: '✅ FCM Proxy V2 service force-killed (SIGKILL).', ts: Date.now() }
1517
+ const ns = await _gds()
1518
+ state.daemonStatus = ns.status
1519
+ state.daemonInfo = ns.info || null
1520
+ } else {
1521
+ state.proxyDaemonMessage = { type: 'error', msg: `❌ Kill failed: ${result.error}`, ts: Date.now() }
1522
+ }
1523
+ return
1524
+ }
1525
+
1526
+ // 📖 View daemon logs
1527
+ if (state.proxyDaemonCursor === ROW_DAEMON_LOGS) {
1528
+ const { getDaemonLogPath } = await import('./daemon-manager.js')
1529
+ const logPath = getDaemonLogPath()
1530
+ try {
1531
+ const { readFileSync, existsSync } = await import('node:fs')
1532
+ if (!existsSync(logPath)) {
1533
+ state.proxyDaemonMessage = { type: 'warning', msg: `⚠ No log file found at ${logPath}`, ts: Date.now() }
1534
+ return
1535
+ }
1536
+ const content = readFileSync(logPath, 'utf8')
1537
+ const logLines = content.split('\n')
1538
+ const last50 = logLines.slice(-50).join('\n')
1539
+ // 📖 Display in the log overlay (repurpose log view)
1540
+ state.proxyDaemonOpen = false
1541
+ state.logVisible = true
1542
+ state.logScrollOffset = 0
1543
+ state._daemonLogContent = last50
1544
+ state.proxyDaemonMessage = { type: 'success', msg: `📖 Showing last ${Math.min(50, logLines.length)} lines from ${logPath}`, ts: Date.now() }
1545
+ } catch (err) {
1546
+ state.proxyDaemonMessage = { type: 'error', msg: `❌ Could not read logs: ${err.message}`, ts: Date.now() }
1547
+ }
1548
+ return
1549
+ }
1550
+ }
1551
+
1552
+ return // 📖 Swallow all other keys while proxy/daemon overlay is open
1553
+ }
1554
+
1390
1555
  // 📖 P key: open settings screen
1391
1556
  if (key.name === 'p' && !key.shift) {
1392
1557
  state.settingsOpen = true
@@ -1397,6 +1562,13 @@ export function createKeyHandler(ctx) {
1397
1562
  state.settingsProxyPortBuffer = ''
1398
1563
  state.settingsEditBuffer = ''
1399
1564
  state.settingsScrollOffset = 0
1565
+ // 📖 Refresh daemon status when opening settings
1566
+ import('./daemon-manager.js').then(dm => {
1567
+ dm.getDaemonStatus().then(s => {
1568
+ state.daemonStatus = s.status
1569
+ state.daemonInfo = s.info || null
1570
+ }).catch(() => {})
1571
+ }).catch(() => {})
1400
1572
  return
1401
1573
  }
1402
1574
 
@@ -1593,18 +1765,25 @@ export function createKeyHandler(ctx) {
1593
1765
  return
1594
1766
  }
1595
1767
 
1596
- // 📖 J key: open Feature Request overlay (anonymous Discord feedback)
1768
+ // 📖 J key: open FCM Proxy V2 settings overlay directly (bypasses Settings screen)
1597
1769
  if (key.name === 'j') {
1598
- state.featureRequestOpen = true
1599
- state.featureRequestBuffer = ''
1600
- state.featureRequestStatus = 'idle'
1601
- state.featureRequestError = null
1770
+ state.proxyDaemonOpen = true
1771
+ state.proxyDaemonCursor = 0
1772
+ state.proxyDaemonScrollOffset = 0
1773
+ state.proxyDaemonMessage = null
1774
+ // 📖 Refresh daemon status when entering
1775
+ try {
1776
+ const { getDaemonStatus: _gds } = await import('./daemon-manager.js')
1777
+ const st = await _gds()
1778
+ state.daemonStatus = st.status
1779
+ state.daemonInfo = st.info || null
1780
+ } catch { /* ignore */ }
1602
1781
  return
1603
1782
  }
1604
1783
 
1605
- // 📖 I key: open Bug Report overlay (anonymous Discord bug reports)
1784
+ // 📖 I key: open Feedback overlay (anonymous Discord feedback)
1606
1785
  if (key.name === 'i') {
1607
- state.bugReportOpen = true
1786
+ state.feedbackOpen = true
1608
1787
  state.bugReportBuffer = ''
1609
1788
  state.bugReportStatus = 'idle'
1610
1789
  state.bugReportError = null