free-coding-models 0.3.18 → 0.3.21

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.
@@ -31,6 +31,8 @@ import { loadChangelog } from './changelog-loader.js'
31
31
  import { loadConfig, replaceConfigContents } from './config.js'
32
32
  import { cleanupLegacyProxyArtifacts } from './legacy-proxy-cleanup.js'
33
33
  import { cycleThemeSetting, detectActiveTheme } from './theme.js'
34
+ import { buildCommandPaletteEntries, filterCommandPaletteEntries } from './command-palette.js'
35
+ import { WIDTH_WARNING_MIN_COLS } from './constants.js'
34
36
 
35
37
  // 📖 Some providers need an explicit probe model because the first catalog entry
36
38
  // 📖 is not guaranteed to be accepted by their chat endpoint.
@@ -446,38 +448,6 @@ export function createKeyHandler(ctx) {
446
448
  }
447
449
  }
448
450
 
449
- // 📖 Keep the width-warning runtime state synced with the persisted Settings toggle
450
- // 📖 so the overlay reacts immediately when the user enables or disables it.
451
- function syncWidthsWarningState() {
452
- const widthsWarningDisabled = state.config.settings?.disableWidthsWarning === true
453
- state.disableWidthsWarning = widthsWarningDisabled
454
-
455
- if (widthsWarningDisabled) {
456
- state.widthWarningStartedAt = null
457
- state.widthWarningDismissed = false
458
- return
459
- }
460
-
461
- state.widthWarningShowCount = 0
462
- if ((state.terminalCols || 80) < 166) {
463
- state.widthWarningStartedAt = Date.now()
464
- state.widthWarningDismissed = false
465
- return
466
- }
467
-
468
- state.widthWarningStartedAt = null
469
- state.widthWarningDismissed = false
470
- }
471
-
472
- // 📖 Toggle the width-warning setting and apply the effect immediately instead
473
- // 📖 of waiting for a resize or restart.
474
- function toggleWidthsWarningSetting() {
475
- if (!state.config.settings) state.config.settings = {}
476
- state.config.settings.disableWidthsWarning = !state.config.settings.disableWidthsWarning
477
- syncWidthsWarningState()
478
- saveConfig(state.config)
479
- }
480
-
481
451
  // 📖 Theme switches need to update both persisted preference and the live
482
452
  // 📖 semantic palette immediately so every screen redraw adopts the new colors.
483
453
  function applyThemeSetting(nextTheme) {
@@ -536,10 +506,315 @@ export function createKeyHandler(ctx) {
536
506
  state.installEndpointsErrorMsg = null
537
507
  }
538
508
 
509
+ // 📖 Persist current table-view preferences so sort/filter state survives restarts.
510
+ function persistUiSettings() {
511
+ if (!state.config.settings || typeof state.config.settings !== 'object') state.config.settings = {}
512
+ state.config.settings.tierFilter = TIER_CYCLE[state.tierFilterMode]
513
+ state.config.settings.originFilter = ORIGIN_CYCLE[state.originFilterMode] ?? null
514
+ state.config.settings.sortColumn = state.sortColumn
515
+ state.config.settings.sortAsc = state.sortDirection === 'asc'
516
+ saveConfig(state.config)
517
+ }
518
+
519
+ // 📖 Shared table refresh helper so command-palette and hotkeys keep identical behavior.
520
+ function refreshVisibleSorted({ resetCursor = true } = {}) {
521
+ const visible = state.results.filter(r => !r.hidden)
522
+ state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection)
523
+ if (resetCursor) {
524
+ state.cursor = 0
525
+ state.scrollOffset = 0
526
+ return
527
+ }
528
+ if (state.cursor >= state.visibleSorted.length) state.cursor = Math.max(0, state.visibleSorted.length - 1)
529
+ adjustScrollOffset(state)
530
+ }
531
+
532
+ function setSortColumnFromCommand(col) {
533
+ if (state.sortColumn === col) {
534
+ state.sortDirection = state.sortDirection === 'asc' ? 'desc' : 'asc'
535
+ } else {
536
+ state.sortColumn = col
537
+ state.sortDirection = 'asc'
538
+ }
539
+ refreshVisibleSorted({ resetCursor: true })
540
+ persistUiSettings()
541
+ }
542
+
543
+ function setTierFilterFromCommand(tierLabel) {
544
+ const nextMode = tierLabel === null ? 0 : TIER_CYCLE.indexOf(tierLabel)
545
+ state.tierFilterMode = nextMode >= 0 ? nextMode : 0
546
+ applyTierFilter()
547
+ refreshVisibleSorted({ resetCursor: true })
548
+ persistUiSettings()
549
+ }
550
+
551
+ function openSettingsOverlay() {
552
+ state.settingsOpen = true
553
+ state.settingsCursor = 0
554
+ state.settingsEditMode = false
555
+ state.settingsAddKeyMode = false
556
+ state.settingsEditBuffer = ''
557
+ state.settingsScrollOffset = 0
558
+ }
559
+
560
+ function openRecommendOverlay() {
561
+ state.recommendOpen = true
562
+ state.recommendPhase = 'questionnaire'
563
+ state.recommendQuestion = 0
564
+ state.recommendCursor = 0
565
+ state.recommendAnswers = { taskType: null, priority: null, contextBudget: null }
566
+ state.recommendResults = []
567
+ state.recommendScrollOffset = 0
568
+ }
569
+
570
+ function openInstallEndpointsOverlay() {
571
+ state.installEndpointsOpen = true
572
+ state.installEndpointsPhase = 'providers'
573
+ state.installEndpointsCursor = 0
574
+ state.installEndpointsScrollOffset = 0
575
+ state.installEndpointsProviderKey = null
576
+ state.installEndpointsToolMode = null
577
+ state.installEndpointsConnectionMode = null
578
+ state.installEndpointsScope = null
579
+ state.installEndpointsSelectedModelIds = new Set()
580
+ state.installEndpointsErrorMsg = null
581
+ state.installEndpointsResult = null
582
+ }
583
+
584
+ function openFeedbackOverlay() {
585
+ state.feedbackOpen = true
586
+ state.bugReportBuffer = ''
587
+ state.bugReportStatus = 'idle'
588
+ state.bugReportError = null
589
+ }
590
+
591
+ function openChangelogOverlay() {
592
+ state.changelogOpen = true
593
+ state.changelogScrollOffset = 0
594
+ state.changelogPhase = 'index'
595
+ state.changelogCursor = 0
596
+ state.changelogSelectedVersion = null
597
+ }
598
+
599
+ function cycleToolMode() {
600
+ const modeOrder = getToolModeOrder()
601
+ const currentIndex = modeOrder.indexOf(state.mode)
602
+ const nextIndex = (currentIndex + 1) % modeOrder.length
603
+ state.mode = modeOrder[nextIndex]
604
+ if (!state.config.settings || typeof state.config.settings !== 'object') state.config.settings = {}
605
+ state.config.settings.preferredToolMode = state.mode
606
+ saveConfig(state.config)
607
+ }
608
+
609
+ function resetViewSettings() {
610
+ state.tierFilterMode = 0
611
+ state.originFilterMode = 0
612
+ state.sortColumn = 'avg'
613
+ state.sortDirection = 'asc'
614
+ if (!state.config.settings || typeof state.config.settings !== 'object') state.config.settings = {}
615
+ delete state.config.settings.tierFilter
616
+ delete state.config.settings.originFilter
617
+ delete state.config.settings.sortColumn
618
+ delete state.config.settings.sortAsc
619
+ saveConfig(state.config)
620
+ applyTierFilter()
621
+ refreshVisibleSorted({ resetCursor: true })
622
+ }
623
+
624
+ function toggleFavoriteOnSelectedRow() {
625
+ const selected = state.visibleSorted[state.cursor]
626
+ if (!selected) return
627
+ const wasFavorite = selected.isFavorite
628
+ toggleFavoriteModel(state.config, selected.providerKey, selected.modelId)
629
+ syncFavoriteFlags(state.results, state.config)
630
+ applyTierFilter()
631
+ refreshVisibleSorted({ resetCursor: false })
632
+
633
+ if (wasFavorite) {
634
+ state.cursor = 0
635
+ state.scrollOffset = 0
636
+ return
637
+ }
638
+
639
+ const selectedKey = toFavoriteKey(selected.providerKey, selected.modelId)
640
+ const newCursor = state.visibleSorted.findIndex(r => toFavoriteKey(r.providerKey, r.modelId) === selectedKey)
641
+ if (newCursor >= 0) state.cursor = newCursor
642
+ adjustScrollOffset(state)
643
+ }
644
+
645
+ function commandPaletteHasBlockingOverlay() {
646
+ return state.settingsOpen
647
+ || state.installEndpointsOpen
648
+ || state.toolInstallPromptOpen
649
+ || state.recommendOpen
650
+ || state.feedbackOpen
651
+ || state.helpVisible
652
+ || state.changelogOpen
653
+ }
654
+
655
+ function refreshCommandPaletteResults() {
656
+ const commands = buildCommandPaletteEntries()
657
+ state.commandPaletteResults = filterCommandPaletteEntries(commands, state.commandPaletteQuery)
658
+ if (state.commandPaletteCursor >= state.commandPaletteResults.length) {
659
+ state.commandPaletteCursor = Math.max(0, state.commandPaletteResults.length - 1)
660
+ }
661
+ }
662
+
663
+ function openCommandPalette() {
664
+ state.commandPaletteOpen = true
665
+ state.commandPaletteFrozenTable = null
666
+ state.commandPaletteQuery = ''
667
+ state.commandPaletteCursor = 0
668
+ state.commandPaletteScrollOffset = 0
669
+ refreshCommandPaletteResults()
670
+ }
671
+
672
+ function closeCommandPalette() {
673
+ state.commandPaletteOpen = false
674
+ state.commandPaletteFrozenTable = null
675
+ state.commandPaletteQuery = ''
676
+ state.commandPaletteCursor = 0
677
+ state.commandPaletteScrollOffset = 0
678
+ state.commandPaletteResults = []
679
+ }
680
+
681
+ function executeCommandPaletteEntry(entry) {
682
+ if (!entry?.id) return
683
+
684
+ if (entry.id.startsWith('filter-tier-')) {
685
+ setTierFilterFromCommand(entry.tierValue ?? null)
686
+ return
687
+ }
688
+
689
+ switch (entry.id) {
690
+ case 'filter-provider-cycle':
691
+ state.originFilterMode = (state.originFilterMode + 1) % ORIGIN_CYCLE.length
692
+ applyTierFilter()
693
+ refreshVisibleSorted({ resetCursor: true })
694
+ persistUiSettings()
695
+ return
696
+ case 'filter-configured-toggle':
697
+ state.hideUnconfiguredModels = !state.hideUnconfiguredModels
698
+ if (!state.config.settings || typeof state.config.settings !== 'object') state.config.settings = {}
699
+ state.config.settings.hideUnconfiguredModels = state.hideUnconfiguredModels
700
+ saveConfig(state.config)
701
+ applyTierFilter()
702
+ refreshVisibleSorted({ resetCursor: true })
703
+ return
704
+ case 'sort-rank': return setSortColumnFromCommand('rank')
705
+ case 'sort-tier': return setSortColumnFromCommand('tier')
706
+ case 'sort-provider': return setSortColumnFromCommand('origin')
707
+ case 'sort-model': return setSortColumnFromCommand('model')
708
+ case 'sort-latest-ping': return setSortColumnFromCommand('ping')
709
+ case 'sort-avg-ping': return setSortColumnFromCommand('avg')
710
+ case 'sort-swe': return setSortColumnFromCommand('swe')
711
+ case 'sort-ctx': return setSortColumnFromCommand('ctx')
712
+ case 'sort-health': return setSortColumnFromCommand('condition')
713
+ case 'sort-verdict': return setSortColumnFromCommand('verdict')
714
+ case 'sort-stability': return setSortColumnFromCommand('stability')
715
+ case 'sort-uptime': return setSortColumnFromCommand('uptime')
716
+ case 'open-settings': return openSettingsOverlay()
717
+ case 'open-help':
718
+ state.helpVisible = true
719
+ state.helpScrollOffset = 0
720
+ return
721
+ case 'open-changelog': return openChangelogOverlay()
722
+ case 'open-feedback': return openFeedbackOverlay()
723
+ case 'open-recommend': return openRecommendOverlay()
724
+ case 'open-install-endpoints': return openInstallEndpointsOverlay()
725
+ case 'action-cycle-theme': return cycleGlobalTheme()
726
+ case 'action-cycle-tool-mode': return cycleToolMode()
727
+ case 'action-cycle-ping-mode': {
728
+ const currentIdx = PING_MODE_CYCLE.indexOf(state.pingMode)
729
+ const nextIdx = currentIdx >= 0 ? (currentIdx + 1) % PING_MODE_CYCLE.length : 0
730
+ setPingMode(PING_MODE_CYCLE[nextIdx], 'manual')
731
+ return
732
+ }
733
+ case 'action-toggle-favorite': return toggleFavoriteOnSelectedRow()
734
+ case 'action-reset-view': return resetViewSettings()
735
+ default:
736
+ return
737
+ }
738
+ }
739
+
539
740
  return async (str, key) => {
540
741
  if (!key) return
541
742
  noteUserActivity()
542
743
 
744
+ // 📖 Ctrl+P toggles the command palette from the main table only.
745
+ if (key.ctrl && key.name === 'p') {
746
+ if (state.commandPaletteOpen) {
747
+ closeCommandPalette()
748
+ return
749
+ }
750
+ if (!commandPaletteHasBlockingOverlay()) {
751
+ openCommandPalette()
752
+ }
753
+ return
754
+ }
755
+
756
+ // 📖 Command palette captures the keyboard while active.
757
+ if (state.commandPaletteOpen) {
758
+ if (key.ctrl && key.name === 'c') { exit(0); return }
759
+
760
+ const pageStep = Math.max(1, (state.terminalRows || 1) - 10)
761
+
762
+ if (key.name === 'escape') {
763
+ closeCommandPalette()
764
+ return
765
+ }
766
+ if (key.name === 'up') {
767
+ const count = state.commandPaletteResults.length
768
+ if (count === 0) return
769
+ state.commandPaletteCursor = state.commandPaletteCursor > 0 ? state.commandPaletteCursor - 1 : count - 1
770
+ return
771
+ }
772
+ if (key.name === 'down') {
773
+ const count = state.commandPaletteResults.length
774
+ if (count === 0) return
775
+ state.commandPaletteCursor = state.commandPaletteCursor < count - 1 ? state.commandPaletteCursor + 1 : 0
776
+ return
777
+ }
778
+ if (key.name === 'pageup') {
779
+ state.commandPaletteCursor = Math.max(0, state.commandPaletteCursor - pageStep)
780
+ return
781
+ }
782
+ if (key.name === 'pagedown') {
783
+ const max = Math.max(0, state.commandPaletteResults.length - 1)
784
+ state.commandPaletteCursor = Math.min(max, state.commandPaletteCursor + pageStep)
785
+ return
786
+ }
787
+ if (key.name === 'home') {
788
+ state.commandPaletteCursor = 0
789
+ return
790
+ }
791
+ if (key.name === 'end') {
792
+ state.commandPaletteCursor = Math.max(0, state.commandPaletteResults.length - 1)
793
+ return
794
+ }
795
+ if (key.name === 'backspace') {
796
+ state.commandPaletteQuery = state.commandPaletteQuery.slice(0, -1)
797
+ state.commandPaletteCursor = 0
798
+ state.commandPaletteScrollOffset = 0
799
+ refreshCommandPaletteResults()
800
+ return
801
+ }
802
+ if (key.name === 'return') {
803
+ const selectedCommand = state.commandPaletteResults[state.commandPaletteCursor]
804
+ closeCommandPalette()
805
+ executeCommandPaletteEntry(selectedCommand)
806
+ return
807
+ }
808
+ if (str && str.length === 1 && !key.ctrl && !key.meta) {
809
+ state.commandPaletteQuery += str
810
+ state.commandPaletteCursor = 0
811
+ state.commandPaletteScrollOffset = 0
812
+ refreshCommandPaletteResults()
813
+ return
814
+ }
815
+ return
816
+ }
817
+
543
818
  if (!state.feedbackOpen && !state.settingsEditMode && !state.settingsAddKeyMode && key.name === 'g' && !key.ctrl && !key.meta) {
544
819
  cycleGlobalTheme()
545
820
  return
@@ -1078,8 +1353,7 @@ export function createKeyHandler(ctx) {
1078
1353
  if (state.settingsOpen) {
1079
1354
  const providerKeys = Object.keys(sources)
1080
1355
  const updateRowIdx = providerKeys.length
1081
- const widthWarningRowIdx = updateRowIdx + 1
1082
- const themeRowIdx = widthWarningRowIdx + 1
1356
+ const themeRowIdx = updateRowIdx + 1
1083
1357
  const cleanupLegacyProxyRowIdx = themeRowIdx + 1
1084
1358
  const changelogViewRowIdx = cleanupLegacyProxyRowIdx + 1
1085
1359
  // 📖 Profile system removed - API keys now persist permanently across all sessions
@@ -1224,12 +1498,6 @@ export function createKeyHandler(ctx) {
1224
1498
  return
1225
1499
  }
1226
1500
 
1227
- // 📖 Widths Warning toggle (Enter to toggle)
1228
- if (state.settingsCursor === widthWarningRowIdx) {
1229
- toggleWidthsWarningSetting()
1230
- return
1231
- }
1232
-
1233
1501
  if (state.settingsCursor === themeRowIdx) {
1234
1502
  cycleGlobalTheme()
1235
1503
  return
@@ -1273,11 +1541,6 @@ export function createKeyHandler(ctx) {
1273
1541
  cycleGlobalTheme()
1274
1542
  return
1275
1543
  }
1276
- // 📖 Widths Warning toggle (disable/enable)
1277
- if (state.settingsCursor === widthWarningRowIdx) {
1278
- toggleWidthsWarningSetting()
1279
- return
1280
- }
1281
1544
  // 📖 Profile system removed - API keys now persist permanently across all sessions
1282
1545
 
1283
1546
  // 📖 Toggle enabled/disabled for selected provider
@@ -1292,7 +1555,6 @@ export function createKeyHandler(ctx) {
1292
1555
  if (key.name === 't') {
1293
1556
  if (
1294
1557
  state.settingsCursor === updateRowIdx
1295
- || state.settingsCursor === widthWarningRowIdx
1296
1558
  || state.settingsCursor === themeRowIdx
1297
1559
  || state.settingsCursor === cleanupLegacyProxyRowIdx
1298
1560
  || state.settingsCursor === changelogViewRowIdx
@@ -1346,41 +1608,20 @@ export function createKeyHandler(ctx) {
1346
1608
  }
1347
1609
 
1348
1610
  // 📖 P key: open settings screen
1349
- if (key.name === 'p' && !key.shift) {
1350
- state.settingsOpen = true
1351
- state.settingsCursor = 0
1352
- state.settingsEditMode = false
1353
- state.settingsAddKeyMode = false
1354
- state.settingsEditBuffer = ''
1355
- state.settingsScrollOffset = 0
1611
+ if (key.name === 'p' && !key.shift && !key.ctrl && !key.meta) {
1612
+ openSettingsOverlay()
1356
1613
  return
1357
1614
  }
1358
1615
 
1359
1616
  // 📖 Q key: open Smart Recommend overlay
1360
1617
  if (key.name === 'q') {
1361
- state.recommendOpen = true
1362
- state.recommendPhase = 'questionnaire'
1363
- state.recommendQuestion = 0
1364
- state.recommendCursor = 0
1365
- state.recommendAnswers = { taskType: null, priority: null, contextBudget: null }
1366
- state.recommendResults = []
1367
- state.recommendScrollOffset = 0
1618
+ openRecommendOverlay()
1368
1619
  return
1369
1620
  }
1370
1621
 
1371
1622
  // 📖 Y key: open Install Endpoints flow for configured providers.
1372
1623
  if (key.name === 'y') {
1373
- state.installEndpointsOpen = true
1374
- state.installEndpointsPhase = 'providers'
1375
- state.installEndpointsCursor = 0
1376
- state.installEndpointsScrollOffset = 0
1377
- state.installEndpointsProviderKey = null
1378
- state.installEndpointsToolMode = null
1379
- state.installEndpointsConnectionMode = null
1380
- state.installEndpointsScope = null
1381
- state.installEndpointsSelectedModelIds = new Set()
1382
- state.installEndpointsErrorMsg = null
1383
- state.installEndpointsResult = null
1624
+ openInstallEndpointsOverlay()
1384
1625
  return
1385
1626
  }
1386
1627
 
@@ -1388,34 +1629,9 @@ export function createKeyHandler(ctx) {
1388
1629
 
1389
1630
  // 📖 Profile system removed - API keys now persist permanently across all sessions
1390
1631
 
1391
- // 📖 Helper: persist current UI view settings (tier, provider, sort) to config.settings
1392
- // 📖 Called after every T / D / sort key so preferences survive session restarts.
1393
- function persistUiSettings() {
1394
- if (!state.config.settings || typeof state.config.settings !== 'object') state.config.settings = {}
1395
- state.config.settings.tierFilter = TIER_CYCLE[state.tierFilterMode]
1396
- state.config.settings.originFilter = ORIGIN_CYCLE[state.originFilterMode] ?? null
1397
- state.config.settings.sortColumn = state.sortColumn
1398
- state.config.settings.sortAsc = state.sortDirection === 'asc'
1399
- saveConfig(state.config)
1400
- }
1401
-
1402
1632
  // 📖 Shift+R: reset all UI view settings to defaults (tier, sort, provider) and clear persisted config
1403
1633
  if (key.name === 'r' && key.shift) {
1404
- state.tierFilterMode = 0
1405
- state.originFilterMode = 0
1406
- state.sortColumn = 'avg'
1407
- state.sortDirection = 'asc'
1408
- if (!state.config.settings || typeof state.config.settings !== 'object') state.config.settings = {}
1409
- delete state.config.settings.tierFilter
1410
- delete state.config.settings.originFilter
1411
- delete state.config.settings.sortColumn
1412
- delete state.config.settings.sortAsc
1413
- saveConfig(state.config)
1414
- applyTierFilter()
1415
- const visible = state.results.filter(r => !r.hidden)
1416
- state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection)
1417
- state.cursor = 0
1418
- state.scrollOffset = 0
1634
+ resetViewSettings()
1419
1635
  return
1420
1636
  }
1421
1637
 
@@ -1430,54 +1646,19 @@ export function createKeyHandler(ctx) {
1430
1646
 
1431
1647
  if (sortKeys[key.name] && !key.ctrl && !key.shift) {
1432
1648
  const col = sortKeys[key.name]
1433
- // 📖 Toggle direction if same column, otherwise reset to asc
1434
- if (state.sortColumn === col) {
1435
- state.sortDirection = state.sortDirection === 'asc' ? 'desc' : 'asc'
1436
- } else {
1437
- state.sortColumn = col
1438
- state.sortDirection = 'asc'
1439
- }
1440
- // 📖 Recompute visible sorted list and reset cursor to top to avoid stale index
1441
- const visible = state.results.filter(r => !r.hidden)
1442
- state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection)
1443
- state.cursor = 0
1444
- state.scrollOffset = 0
1445
- persistUiSettings()
1649
+ setSortColumnFromCommand(col)
1446
1650
  return
1447
1651
  }
1448
1652
 
1449
1653
  // 📖 F key: toggle favorite on the currently selected row and persist to config.
1450
1654
  if (key.name === 'f') {
1451
- const selected = state.visibleSorted[state.cursor]
1452
- if (!selected) return
1453
- const wasFavorite = selected.isFavorite
1454
- toggleFavoriteModel(state.config, selected.providerKey, selected.modelId)
1455
- syncFavoriteFlags(state.results, state.config)
1456
- applyTierFilter()
1457
- const visible = state.results.filter(r => !r.hidden)
1458
- state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection)
1459
-
1460
- // 📖 UX rule: when unpinning a favorite, jump back to the top of the list.
1461
- if (wasFavorite) {
1462
- state.cursor = 0
1463
- state.scrollOffset = 0
1464
- return
1465
- }
1466
-
1467
- const selectedKey = toFavoriteKey(selected.providerKey, selected.modelId)
1468
- const newCursor = state.visibleSorted.findIndex(r => toFavoriteKey(r.providerKey, r.modelId) === selectedKey)
1469
- if (newCursor >= 0) state.cursor = newCursor
1470
- else if (state.cursor >= state.visibleSorted.length) state.cursor = Math.max(0, state.visibleSorted.length - 1)
1471
- adjustScrollOffset(state)
1655
+ toggleFavoriteOnSelectedRow()
1472
1656
  return
1473
1657
  }
1474
1658
 
1475
1659
  // 📖 I key: open Feedback overlay (anonymous Discord feedback)
1476
1660
  if (key.name === 'i') {
1477
- state.feedbackOpen = true
1478
- state.bugReportBuffer = ''
1479
- state.bugReportStatus = 'idle'
1480
- state.bugReportError = null
1661
+ openFeedbackOverlay()
1481
1662
  return
1482
1663
  }
1483
1664
 
@@ -1552,13 +1733,7 @@ export function createKeyHandler(ctx) {
1552
1733
 
1553
1734
  // 📖 Mode toggle key: Z cycles through the supported tool targets.
1554
1735
  if (key.name === 'z') {
1555
- const modeOrder = getToolModeOrder()
1556
- const currentIndex = modeOrder.indexOf(state.mode)
1557
- const nextIndex = (currentIndex + 1) % modeOrder.length
1558
- state.mode = modeOrder[nextIndex]
1559
- if (!state.config.settings || typeof state.config.settings !== 'object') state.config.settings = {}
1560
- state.config.settings.preferredToolMode = state.mode
1561
- saveConfig(state.config)
1736
+ cycleToolMode()
1562
1737
  return
1563
1738
  }
1564
1739
 
@@ -1586,7 +1761,7 @@ export function createKeyHandler(ctx) {
1586
1761
  }
1587
1762
 
1588
1763
  // 📖 Esc can dismiss the narrow-terminal warning immediately without quitting the app.
1589
- if (key.name === 'escape' && state.terminalCols > 0 && state.terminalCols < 166) {
1764
+ if (key.name === 'escape' && state.terminalCols > 0 && state.terminalCols < WIDTH_WARNING_MIN_COLS) {
1590
1765
  state.widthWarningDismissed = true
1591
1766
  return
1592
1767
  }