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.
- package/CHANGELOG.md +27 -0
- package/README.md +3 -1
- package/package.json +1 -1
- package/src/app.js +96 -32
- package/src/cli-help.js +1 -2
- package/src/command-palette.js +170 -0
- package/src/config.js +0 -3
- package/src/constants.js +5 -0
- package/src/key-handler.js +318 -143
- package/src/overlays.js +127 -11
- package/src/render-table.js +120 -50
- package/src/testfcm.js +1 -1
- package/src/theme.js +3 -0
- package/src/utils.js +0 -2
package/src/key-handler.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 <
|
|
1764
|
+
if (key.name === 'escape' && state.terminalCols > 0 && state.terminalCols < WIDTH_WARNING_MIN_COLS) {
|
|
1590
1765
|
state.widthWarningDismissed = true
|
|
1591
1766
|
return
|
|
1592
1767
|
}
|