free-coding-models 0.3.21 β 0.3.23
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 +43 -1
- package/README.md +20 -9
- package/package.json +1 -1
- package/src/app.js +40 -14
- package/src/command-palette.js +327 -59
- package/src/config.js +4 -2
- package/src/endpoint-installer.js +1 -1
- package/src/key-handler.js +205 -31
- package/src/overlays.js +105 -57
- package/src/product-flags.js +4 -9
- package/src/render-helpers.js +15 -2
- package/src/render-table.js +51 -25
- package/src/theme.js +2 -0
- package/src/utils.js +1 -1
package/src/key-handler.js
CHANGED
|
@@ -31,7 +31,7 @@ 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 {
|
|
34
|
+
import { buildCommandPaletteTree, flattenCommandTree, filterCommandPaletteEntries } from './command-palette.js'
|
|
35
35
|
import { WIDTH_WARNING_MIN_COLS } from './constants.js'
|
|
36
36
|
|
|
37
37
|
// π Some providers need an explicit probe model because the first catalog entry
|
|
@@ -519,7 +519,9 @@ export function createKeyHandler(ctx) {
|
|
|
519
519
|
// π Shared table refresh helper so command-palette and hotkeys keep identical behavior.
|
|
520
520
|
function refreshVisibleSorted({ resetCursor = true } = {}) {
|
|
521
521
|
const visible = state.results.filter(r => !r.hidden)
|
|
522
|
-
state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection
|
|
522
|
+
state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection, {
|
|
523
|
+
pinFavorites: state.favoritesPinnedAndSticky,
|
|
524
|
+
})
|
|
523
525
|
if (resetCursor) {
|
|
524
526
|
state.cursor = 0
|
|
525
527
|
state.scrollOffset = 0
|
|
@@ -600,15 +602,52 @@ export function createKeyHandler(ctx) {
|
|
|
600
602
|
const modeOrder = getToolModeOrder()
|
|
601
603
|
const currentIndex = modeOrder.indexOf(state.mode)
|
|
602
604
|
const nextIndex = (currentIndex + 1) % modeOrder.length
|
|
603
|
-
|
|
605
|
+
setToolMode(modeOrder[nextIndex])
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// π Keep tool-mode changes centralized so keyboard shortcuts and command palette
|
|
609
|
+
// π both persist to config exactly the same way.
|
|
610
|
+
function setToolMode(nextMode) {
|
|
611
|
+
if (!getToolModeOrder().includes(nextMode)) return
|
|
612
|
+
state.mode = nextMode
|
|
604
613
|
if (!state.config.settings || typeof state.config.settings !== 'object') state.config.settings = {}
|
|
605
614
|
state.config.settings.preferredToolMode = state.mode
|
|
606
615
|
saveConfig(state.config)
|
|
607
616
|
}
|
|
608
617
|
|
|
618
|
+
// π Favorites display mode:
|
|
619
|
+
// π - true => favorites stay pinned + always visible (legacy behavior)
|
|
620
|
+
// π - false => favorites are just starred rows and obey normal sort/filter rules
|
|
621
|
+
function setFavoritesDisplayMode(nextPinned, { preserveSelection = true } = {}) {
|
|
622
|
+
const normalizedNextPinned = nextPinned !== false
|
|
623
|
+
if (state.favoritesPinnedAndSticky === normalizedNextPinned) return
|
|
624
|
+
|
|
625
|
+
const selected = preserveSelection ? state.visibleSorted[state.cursor] : null
|
|
626
|
+
const selectedKey = selected ? toFavoriteKey(selected.providerKey, selected.modelId) : null
|
|
627
|
+
|
|
628
|
+
state.favoritesPinnedAndSticky = normalizedNextPinned
|
|
629
|
+
if (!state.config.settings || typeof state.config.settings !== 'object') state.config.settings = {}
|
|
630
|
+
state.config.settings.favoritesPinnedAndSticky = state.favoritesPinnedAndSticky
|
|
631
|
+
saveConfig(state.config)
|
|
632
|
+
|
|
633
|
+
applyTierFilter()
|
|
634
|
+
refreshVisibleSorted({ resetCursor: false })
|
|
635
|
+
|
|
636
|
+
if (selectedKey) {
|
|
637
|
+
const selectedIdx = state.visibleSorted.findIndex((row) => toFavoriteKey(row.providerKey, row.modelId) === selectedKey)
|
|
638
|
+
if (selectedIdx >= 0) state.cursor = selectedIdx
|
|
639
|
+
adjustScrollOffset(state)
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function toggleFavoritesDisplayMode() {
|
|
644
|
+
setFavoritesDisplayMode(!state.favoritesPinnedAndSticky)
|
|
645
|
+
}
|
|
646
|
+
|
|
609
647
|
function resetViewSettings() {
|
|
610
648
|
state.tierFilterMode = 0
|
|
611
649
|
state.originFilterMode = 0
|
|
650
|
+
state.customTextFilter = null // π Clear ephemeral text filter on view reset
|
|
612
651
|
state.sortColumn = 'avg'
|
|
613
652
|
state.sortDirection = 'asc'
|
|
614
653
|
if (!state.config.settings || typeof state.config.settings !== 'object') state.config.settings = {}
|
|
@@ -630,7 +669,7 @@ export function createKeyHandler(ctx) {
|
|
|
630
669
|
applyTierFilter()
|
|
631
670
|
refreshVisibleSorted({ resetCursor: false })
|
|
632
671
|
|
|
633
|
-
if (wasFavorite) {
|
|
672
|
+
if (wasFavorite && state.favoritesPinnedAndSticky) {
|
|
634
673
|
state.cursor = 0
|
|
635
674
|
state.scrollOffset = 0
|
|
636
675
|
return
|
|
@@ -653,8 +692,50 @@ export function createKeyHandler(ctx) {
|
|
|
653
692
|
}
|
|
654
693
|
|
|
655
694
|
function refreshCommandPaletteResults() {
|
|
656
|
-
const
|
|
657
|
-
|
|
695
|
+
const query = (state.commandPaletteQuery || '').trim()
|
|
696
|
+
const tree = buildCommandPaletteTree(state.results || [])
|
|
697
|
+
// π Keep collapsed view clean when query is empty, but search across the
|
|
698
|
+
// π full tree when users type so hidden submenu commands still appear.
|
|
699
|
+
let flat
|
|
700
|
+
if (query.length > 0) {
|
|
701
|
+
const expandedIds = new Set()
|
|
702
|
+
const collectExpandedIds = (nodes) => {
|
|
703
|
+
for (const node of nodes || []) {
|
|
704
|
+
if (Array.isArray(node.children) && node.children.length > 0) {
|
|
705
|
+
expandedIds.add(node.id)
|
|
706
|
+
collectExpandedIds(node.children)
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
collectExpandedIds(tree)
|
|
711
|
+
flat = flattenCommandTree(tree, expandedIds)
|
|
712
|
+
} else {
|
|
713
|
+
flat = flattenCommandTree(tree, state.commandPaletteExpandedIds)
|
|
714
|
+
}
|
|
715
|
+
state.commandPaletteResults = filterCommandPaletteEntries(flat, query)
|
|
716
|
+
|
|
717
|
+
if (query.length > 0) {
|
|
718
|
+
state.commandPaletteResults.unshift({
|
|
719
|
+
id: 'filter-custom-text-apply',
|
|
720
|
+
label: `π Apply text filter: ${query}`,
|
|
721
|
+
type: 'command',
|
|
722
|
+
depth: 0,
|
|
723
|
+
hasChildren: false,
|
|
724
|
+
isExpanded: false,
|
|
725
|
+
filterQuery: query,
|
|
726
|
+
})
|
|
727
|
+
} else if (state.customTextFilter) {
|
|
728
|
+
state.commandPaletteResults.unshift({
|
|
729
|
+
id: 'filter-custom-text-remove',
|
|
730
|
+
label: `β Remove custom filter: ${state.customTextFilter}`,
|
|
731
|
+
type: 'command',
|
|
732
|
+
depth: 0,
|
|
733
|
+
hasChildren: false,
|
|
734
|
+
isExpanded: false,
|
|
735
|
+
filterQuery: null,
|
|
736
|
+
})
|
|
737
|
+
}
|
|
738
|
+
|
|
658
739
|
if (state.commandPaletteCursor >= state.commandPaletteResults.length) {
|
|
659
740
|
state.commandPaletteCursor = Math.max(0, state.commandPaletteResults.length - 1)
|
|
660
741
|
}
|
|
@@ -681,8 +762,59 @@ export function createKeyHandler(ctx) {
|
|
|
681
762
|
function executeCommandPaletteEntry(entry) {
|
|
682
763
|
if (!entry?.id) return
|
|
683
764
|
|
|
765
|
+
if (entry.id.startsWith('action-set-ping-') && entry.pingMode) {
|
|
766
|
+
setPingMode(entry.pingMode, 'manual')
|
|
767
|
+
return
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
if (entry.id.startsWith('action-set-tool-') && entry.toolMode) {
|
|
771
|
+
setToolMode(entry.toolMode)
|
|
772
|
+
return
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
if (entry.id.startsWith('action-favorites-mode-') && typeof entry.favoritesPinned === 'boolean') {
|
|
776
|
+
setFavoritesDisplayMode(entry.favoritesPinned)
|
|
777
|
+
return
|
|
778
|
+
}
|
|
779
|
+
|
|
684
780
|
if (entry.id.startsWith('filter-tier-')) {
|
|
685
|
-
setTierFilterFromCommand(entry.
|
|
781
|
+
setTierFilterFromCommand(entry.tier ?? null)
|
|
782
|
+
return
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
if (entry.id.startsWith('filter-provider-') && entry.id !== 'filter-provider-cycle') {
|
|
786
|
+
if (entry.providerKey === null || entry.providerKey === undefined) {
|
|
787
|
+
state.originFilterMode = 0 // All
|
|
788
|
+
} else {
|
|
789
|
+
state.originFilterMode = ORIGIN_CYCLE.findIndex(key => key === entry.providerKey) + 1
|
|
790
|
+
if (state.originFilterMode <= 0) state.originFilterMode = 0
|
|
791
|
+
}
|
|
792
|
+
applyTierFilter()
|
|
793
|
+
refreshVisibleSorted({ resetCursor: true })
|
|
794
|
+
persistUiSettings()
|
|
795
|
+
return
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
if (entry.id.startsWith('filter-model-')) {
|
|
799
|
+
if (entry.modelId && entry.providerKey) {
|
|
800
|
+
state.customTextFilter = `${entry.providerKey}/${entry.modelId}`
|
|
801
|
+
applyTierFilter()
|
|
802
|
+
refreshVisibleSorted({ resetCursor: true })
|
|
803
|
+
}
|
|
804
|
+
return
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// π Custom text filter β apply or remove the free-text filter from the command palette.
|
|
808
|
+
if (entry.id === 'filter-custom-text-apply') {
|
|
809
|
+
state.customTextFilter = entry.filterQuery || null
|
|
810
|
+
applyTierFilter()
|
|
811
|
+
refreshVisibleSorted({ resetCursor: true })
|
|
812
|
+
return
|
|
813
|
+
}
|
|
814
|
+
if (entry.id === 'filter-custom-text-remove') {
|
|
815
|
+
state.customTextFilter = null
|
|
816
|
+
applyTierFilter()
|
|
817
|
+
refreshVisibleSorted({ resetCursor: true })
|
|
686
818
|
return
|
|
687
819
|
}
|
|
688
820
|
|
|
@@ -731,6 +863,7 @@ export function createKeyHandler(ctx) {
|
|
|
731
863
|
return
|
|
732
864
|
}
|
|
733
865
|
case 'action-toggle-favorite': return toggleFavoriteOnSelectedRow()
|
|
866
|
+
case 'action-toggle-favorite-mode': return toggleFavoritesDisplayMode()
|
|
734
867
|
case 'action-reset-view': return resetViewSettings()
|
|
735
868
|
default:
|
|
736
869
|
return
|
|
@@ -758,6 +891,7 @@ export function createKeyHandler(ctx) {
|
|
|
758
891
|
if (key.ctrl && key.name === 'c') { exit(0); return }
|
|
759
892
|
|
|
760
893
|
const pageStep = Math.max(1, (state.terminalRows || 1) - 10)
|
|
894
|
+
const selected = state.commandPaletteResults[state.commandPaletteCursor]
|
|
761
895
|
|
|
762
896
|
if (key.name === 'escape') {
|
|
763
897
|
closeCommandPalette()
|
|
@@ -775,6 +909,23 @@ export function createKeyHandler(ctx) {
|
|
|
775
909
|
state.commandPaletteCursor = state.commandPaletteCursor < count - 1 ? state.commandPaletteCursor + 1 : 0
|
|
776
910
|
return
|
|
777
911
|
}
|
|
912
|
+
if (key.name === 'left') {
|
|
913
|
+
if (selected?.hasChildren && selected.isExpanded) {
|
|
914
|
+
state.commandPaletteExpandedIds.delete(selected.id)
|
|
915
|
+
refreshCommandPaletteResults()
|
|
916
|
+
}
|
|
917
|
+
return
|
|
918
|
+
}
|
|
919
|
+
if (key.name === 'right') {
|
|
920
|
+
if (selected?.hasChildren && !selected.isExpanded) {
|
|
921
|
+
state.commandPaletteExpandedIds.add(selected.id)
|
|
922
|
+
refreshCommandPaletteResults()
|
|
923
|
+
} else if (selected?.type === 'command') {
|
|
924
|
+
closeCommandPalette()
|
|
925
|
+
executeCommandPaletteEntry(selected)
|
|
926
|
+
}
|
|
927
|
+
return
|
|
928
|
+
}
|
|
778
929
|
if (key.name === 'pageup') {
|
|
779
930
|
state.commandPaletteCursor = Math.max(0, state.commandPaletteCursor - pageStep)
|
|
780
931
|
return
|
|
@@ -800,9 +951,17 @@ export function createKeyHandler(ctx) {
|
|
|
800
951
|
return
|
|
801
952
|
}
|
|
802
953
|
if (key.name === 'return') {
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
954
|
+
if (selected?.hasChildren) {
|
|
955
|
+
if (selected.isExpanded) {
|
|
956
|
+
state.commandPaletteExpandedIds.delete(selected.id)
|
|
957
|
+
} else {
|
|
958
|
+
state.commandPaletteExpandedIds.add(selected.id)
|
|
959
|
+
}
|
|
960
|
+
refreshCommandPaletteResults()
|
|
961
|
+
} else {
|
|
962
|
+
closeCommandPalette()
|
|
963
|
+
executeCommandPaletteEntry(selected)
|
|
964
|
+
}
|
|
806
965
|
return
|
|
807
966
|
}
|
|
808
967
|
if (str && str.length === 1 && !key.ctrl && !key.meta) {
|
|
@@ -1354,7 +1513,8 @@ export function createKeyHandler(ctx) {
|
|
|
1354
1513
|
const providerKeys = Object.keys(sources)
|
|
1355
1514
|
const updateRowIdx = providerKeys.length
|
|
1356
1515
|
const themeRowIdx = updateRowIdx + 1
|
|
1357
|
-
const
|
|
1516
|
+
const favoritesModeRowIdx = themeRowIdx + 1
|
|
1517
|
+
const cleanupLegacyProxyRowIdx = favoritesModeRowIdx + 1
|
|
1358
1518
|
const changelogViewRowIdx = cleanupLegacyProxyRowIdx + 1
|
|
1359
1519
|
// π Profile system removed - API keys now persist permanently across all sessions
|
|
1360
1520
|
const maxRowIdx = changelogViewRowIdx
|
|
@@ -1435,10 +1595,7 @@ export function createKeyHandler(ctx) {
|
|
|
1435
1595
|
setResults(nextResults)
|
|
1436
1596
|
syncFavoriteFlags(state.results, state.config)
|
|
1437
1597
|
applyTierFilter()
|
|
1438
|
-
|
|
1439
|
-
state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection)
|
|
1440
|
-
if (state.cursor >= state.visibleSorted.length) state.cursor = Math.max(0, state.visibleSorted.length - 1)
|
|
1441
|
-
adjustScrollOffset(state)
|
|
1598
|
+
refreshVisibleSorted({ resetCursor: false })
|
|
1442
1599
|
// π Re-ping all models that were 'noauth' (got 401 without key) but now have a key
|
|
1443
1600
|
// π This makes the TUI react immediately when a user adds an API key in settings
|
|
1444
1601
|
const pingModel = getPingModel?.()
|
|
@@ -1503,6 +1660,11 @@ export function createKeyHandler(ctx) {
|
|
|
1503
1660
|
return
|
|
1504
1661
|
}
|
|
1505
1662
|
|
|
1663
|
+
if (state.settingsCursor === favoritesModeRowIdx) {
|
|
1664
|
+
toggleFavoritesDisplayMode()
|
|
1665
|
+
return
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1506
1668
|
if (state.settingsCursor === cleanupLegacyProxyRowIdx) {
|
|
1507
1669
|
runLegacyProxyCleanup()
|
|
1508
1670
|
return
|
|
@@ -1541,6 +1703,10 @@ export function createKeyHandler(ctx) {
|
|
|
1541
1703
|
cycleGlobalTheme()
|
|
1542
1704
|
return
|
|
1543
1705
|
}
|
|
1706
|
+
if (state.settingsCursor === favoritesModeRowIdx) {
|
|
1707
|
+
toggleFavoritesDisplayMode()
|
|
1708
|
+
return
|
|
1709
|
+
}
|
|
1544
1710
|
// π Profile system removed - API keys now persist permanently across all sessions
|
|
1545
1711
|
|
|
1546
1712
|
// π Toggle enabled/disabled for selected provider
|
|
@@ -1556,6 +1722,7 @@ export function createKeyHandler(ctx) {
|
|
|
1556
1722
|
if (
|
|
1557
1723
|
state.settingsCursor === updateRowIdx
|
|
1558
1724
|
|| state.settingsCursor === themeRowIdx
|
|
1725
|
+
|| state.settingsCursor === favoritesModeRowIdx
|
|
1559
1726
|
|| state.settingsCursor === cleanupLegacyProxyRowIdx
|
|
1560
1727
|
|| state.settingsCursor === changelogViewRowIdx
|
|
1561
1728
|
) return
|
|
@@ -1573,6 +1740,12 @@ export function createKeyHandler(ctx) {
|
|
|
1573
1740
|
return
|
|
1574
1741
|
}
|
|
1575
1742
|
|
|
1743
|
+
// π Y toggles favorites display mode directly from Settings.
|
|
1744
|
+
if (key.name === 'y') {
|
|
1745
|
+
toggleFavoritesDisplayMode()
|
|
1746
|
+
return
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1576
1749
|
// π Profile system removed - API keys now persist permanently across all sessions
|
|
1577
1750
|
|
|
1578
1751
|
if (key.ctrl && key.name === 'c') { exit(0); return }
|
|
@@ -1619,9 +1792,18 @@ export function createKeyHandler(ctx) {
|
|
|
1619
1792
|
return
|
|
1620
1793
|
}
|
|
1621
1794
|
|
|
1622
|
-
// π Y key
|
|
1623
|
-
if (key.name === 'y') {
|
|
1624
|
-
|
|
1795
|
+
// π Y key toggles favorites display mode (pinned+sticky vs normal rows).
|
|
1796
|
+
if (key.name === 'y' && !key.ctrl && !key.meta) {
|
|
1797
|
+
toggleFavoritesDisplayMode()
|
|
1798
|
+
return
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
// π X clears the active free-text filter set from the command palette.
|
|
1802
|
+
if (key.name === 'x' && !key.ctrl && !key.meta) {
|
|
1803
|
+
if (!state.customTextFilter) return
|
|
1804
|
+
state.customTextFilter = null
|
|
1805
|
+
applyTierFilter()
|
|
1806
|
+
refreshVisibleSorted({ resetCursor: true })
|
|
1625
1807
|
return
|
|
1626
1808
|
}
|
|
1627
1809
|
|
|
@@ -1636,7 +1818,8 @@ export function createKeyHandler(ctx) {
|
|
|
1636
1818
|
}
|
|
1637
1819
|
|
|
1638
1820
|
// π Sorting keys: R=rank, O=origin, M=model, L=latest ping, A=avg ping, S=SWE-bench, C=context, H=health, V=verdict, B=stability, U=uptime, G=usage
|
|
1639
|
-
// π T is reserved for tier filter cycling. Y
|
|
1821
|
+
// π T is reserved for tier filter cycling. Y toggles favorites display mode.
|
|
1822
|
+
// π X clears the active custom text filter.
|
|
1640
1823
|
// π D is now reserved for provider filter cycling
|
|
1641
1824
|
// π Shift+R is reserved for reset view settings
|
|
1642
1825
|
const sortKeys = {
|
|
@@ -1679,10 +1862,7 @@ export function createKeyHandler(ctx) {
|
|
|
1679
1862
|
state.config.settings.hideUnconfiguredModels = state.hideUnconfiguredModels
|
|
1680
1863
|
saveConfig(state.config)
|
|
1681
1864
|
applyTierFilter()
|
|
1682
|
-
|
|
1683
|
-
state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection)
|
|
1684
|
-
state.cursor = 0
|
|
1685
|
-
state.scrollOffset = 0
|
|
1865
|
+
refreshVisibleSorted({ resetCursor: true })
|
|
1686
1866
|
return
|
|
1687
1867
|
}
|
|
1688
1868
|
|
|
@@ -1691,10 +1871,7 @@ export function createKeyHandler(ctx) {
|
|
|
1691
1871
|
state.tierFilterMode = (state.tierFilterMode + 1) % TIER_CYCLE.length
|
|
1692
1872
|
applyTierFilter()
|
|
1693
1873
|
// π Recompute visible sorted list and reset cursor to avoid stale index into new filtered set
|
|
1694
|
-
|
|
1695
|
-
state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection)
|
|
1696
|
-
state.cursor = 0
|
|
1697
|
-
state.scrollOffset = 0
|
|
1874
|
+
refreshVisibleSorted({ resetCursor: true })
|
|
1698
1875
|
persistUiSettings()
|
|
1699
1876
|
return
|
|
1700
1877
|
}
|
|
@@ -1704,10 +1881,7 @@ export function createKeyHandler(ctx) {
|
|
|
1704
1881
|
state.originFilterMode = (state.originFilterMode + 1) % ORIGIN_CYCLE.length
|
|
1705
1882
|
applyTierFilter()
|
|
1706
1883
|
// π Recompute visible sorted list and reset cursor to avoid stale index into new filtered set
|
|
1707
|
-
|
|
1708
|
-
state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection)
|
|
1709
|
-
state.cursor = 0
|
|
1710
|
-
state.scrollOffset = 0
|
|
1884
|
+
refreshVisibleSorted({ resetCursor: true })
|
|
1711
1885
|
persistUiSettings()
|
|
1712
1886
|
return
|
|
1713
1887
|
}
|
package/src/overlays.js
CHANGED
|
@@ -94,7 +94,8 @@ export function createOverlayRenderers(state, deps) {
|
|
|
94
94
|
const providerKeys = Object.keys(sources)
|
|
95
95
|
const updateRowIdx = providerKeys.length
|
|
96
96
|
const themeRowIdx = updateRowIdx + 1
|
|
97
|
-
const
|
|
97
|
+
const favoritesModeRowIdx = themeRowIdx + 1
|
|
98
|
+
const cleanupLegacyProxyRowIdx = favoritesModeRowIdx + 1
|
|
98
99
|
const changelogViewRowIdx = cleanupLegacyProxyRowIdx + 1
|
|
99
100
|
const EL = '\x1b[K'
|
|
100
101
|
const lines = []
|
|
@@ -224,6 +225,16 @@ export function createOverlayRenderers(state, deps) {
|
|
|
224
225
|
const themeRow = `${bullet(state.settingsCursor === themeRowIdx)}${themeColors.textBold('Global Theme').padEnd(44)} ${themeStatusColor(themeStatus)}`
|
|
225
226
|
cursorLineByRow[themeRowIdx] = lines.length
|
|
226
227
|
lines.push(state.settingsCursor === themeRowIdx ? themeColors.bgCursor(themeRow) : themeRow)
|
|
228
|
+
|
|
229
|
+
// π Favorites mode row mirrors Y-key behavior from the main table.
|
|
230
|
+
const favoritesModeEnabled = state.favoritesPinnedAndSticky === true
|
|
231
|
+
const favoritesModeStatus = favoritesModeEnabled
|
|
232
|
+
? themeColors.warningBold('Pinned + always visible')
|
|
233
|
+
: themeColors.info('Normal rows (filter/sort)')
|
|
234
|
+
const favoritesModeRow = `${bullet(state.settingsCursor === favoritesModeRowIdx)}${themeColors.textBold('Favorites Display Mode').padEnd(44)} ${favoritesModeStatus}`
|
|
235
|
+
cursorLineByRow[favoritesModeRowIdx] = lines.length
|
|
236
|
+
lines.push(state.settingsCursor === favoritesModeRowIdx ? themeColors.bgCursorSettingsList(favoritesModeRow) : favoritesModeRow)
|
|
237
|
+
|
|
227
238
|
if (updateState === 'error' && state.settingsUpdateError) {
|
|
228
239
|
lines.push(themeColors.error(` ${state.settingsUpdateError}`))
|
|
229
240
|
}
|
|
@@ -244,7 +255,7 @@ export function createOverlayRenderers(state, deps) {
|
|
|
244
255
|
if (state.settingsEditMode) {
|
|
245
256
|
lines.push(themeColors.dim(' Type API key β’ Enter Save β’ Esc Cancel'))
|
|
246
257
|
} else {
|
|
247
|
-
lines.push(themeColors.dim(' ββ Navigate β’ Enter Edit/Run/Cycle β’ + Add key β’ - Remove key β’ Space Toggle/Cycle β’ T Test key β’ U Updates β’ G Global theme β’ Esc Close'))
|
|
258
|
+
lines.push(themeColors.dim(' ββ Navigate β’ Enter Edit/Run/Cycle β’ + Add key β’ - Remove key β’ Space Toggle/Cycle β’ T Test key β’ U Updates β’ G Global theme β’ Y Favorites mode β’ Esc Close'))
|
|
248
259
|
}
|
|
249
260
|
// π Show sync/restore status message if set
|
|
250
261
|
if (state.settingsSyncStatus) {
|
|
@@ -283,7 +294,7 @@ export function createOverlayRenderers(state, deps) {
|
|
|
283
294
|
|
|
284
295
|
// βββ Install Endpoints overlay renderer βββββββββββββββββββββββββββββββββββ
|
|
285
296
|
// π renderInstallEndpoints drives the provider β tool β scope β model flow
|
|
286
|
-
// π
|
|
297
|
+
// π opened from Settings/Command Palette. It deliberately reuses the same overlay viewport
|
|
287
298
|
// π helpers as Settings so long provider/model lists stay navigable.
|
|
288
299
|
function renderInstallEndpoints() {
|
|
289
300
|
const EL = '\x1b[K'
|
|
@@ -528,18 +539,16 @@ export function createOverlayRenderers(state, deps) {
|
|
|
528
539
|
|
|
529
540
|
// βββ Command palette renderer ββββββββββββββββββββββββββββββββββββββββββββββ
|
|
530
541
|
// π renderCommandPalette draws a centered floating modal over the live table.
|
|
531
|
-
// π
|
|
532
|
-
// π so ping updates continue to animate in the background behind the palette.
|
|
542
|
+
// π Supports hierarchical categories with expand/collapse and rich colors.
|
|
533
543
|
function renderCommandPalette() {
|
|
534
544
|
const terminalRows = state.terminalRows || 24
|
|
535
545
|
const terminalCols = state.terminalCols || 80
|
|
536
|
-
const panelWidth = Math.max(
|
|
537
|
-
const panelInnerWidth = Math.max(
|
|
546
|
+
const panelWidth = Math.max(52, Math.min(100, terminalCols - 8))
|
|
547
|
+
const panelInnerWidth = Math.max(32, panelWidth - 4)
|
|
538
548
|
const panelPad = 2
|
|
539
549
|
const panelOuterWidth = panelWidth + (panelPad * 2)
|
|
540
|
-
const
|
|
541
|
-
const
|
|
542
|
-
const bodyRows = Math.max(6, Math.min(16, terminalRows - 12))
|
|
550
|
+
const headerRowCount = 4
|
|
551
|
+
const bodyRows = Math.max(8, Math.min(18, terminalRows - 12))
|
|
543
552
|
|
|
544
553
|
const truncatePlain = (text, width) => {
|
|
545
554
|
if (width <= 1) return ''
|
|
@@ -559,30 +568,70 @@ export function createOverlayRenderers(state, deps) {
|
|
|
559
568
|
}
|
|
560
569
|
|
|
561
570
|
const allResults = Array.isArray(state.commandPaletteResults) ? state.commandPaletteResults.slice(0, 80) : []
|
|
562
|
-
const
|
|
571
|
+
const panelLines = []
|
|
563
572
|
const cursorLineByRow = {}
|
|
564
|
-
let category = null
|
|
565
573
|
|
|
566
574
|
if (allResults.length === 0) {
|
|
567
|
-
|
|
575
|
+
panelLines.push(themeColors.dim(' No commands found. Try a different search.'))
|
|
568
576
|
} else {
|
|
569
577
|
for (let idx = 0; idx < allResults.length; idx++) {
|
|
570
578
|
const entry = allResults[idx]
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
579
|
+
const isCursor = idx === state.commandPaletteCursor
|
|
580
|
+
|
|
581
|
+
const indent = ' '.repeat(entry.depth || 0)
|
|
582
|
+
const expandIndicator = entry.hasChildren
|
|
583
|
+
? (entry.isExpanded ? themeColors.infoBold('βΌ') : themeColors.dim('βΆ'))
|
|
584
|
+
: themeColors.dim('β’')
|
|
585
|
+
|
|
586
|
+
// π Only use icon from entry, label should NOT include emoji
|
|
587
|
+
const iconPrefix = entry.icon ? `${entry.icon} ` : ''
|
|
588
|
+
const plainLabel = truncatePlain(entry.label, panelInnerWidth - indent.length - iconPrefix.length - 4)
|
|
589
|
+
const label = entry.matchPositions ? highlightMatch(plainLabel, entry.matchPositions) : plainLabel
|
|
590
|
+
|
|
591
|
+
let rowLine
|
|
592
|
+
if (entry.type === 'category') {
|
|
593
|
+
rowLine = `${indent}${expandIndicator} ${iconPrefix}${themeColors.headerBold(label)}`
|
|
594
|
+
} else if (entry.type === 'subcategory') {
|
|
595
|
+
rowLine = `${indent}${expandIndicator} ${iconPrefix}${themeColors.textBold(label)}`
|
|
596
|
+
} else if (entry.type === 'page') {
|
|
597
|
+
// π Pages are at root level with icon + label + shortcut + description
|
|
598
|
+
const shortcut = entry.shortcut ? themeColors.dim(` (${entry.shortcut})`) : ''
|
|
599
|
+
const description = entry.description ? themeColors.dim(` β ${entry.description}`) : ''
|
|
600
|
+
rowLine = `${expandIndicator} ${iconPrefix}${themeColors.textBold(label)}${shortcut}${description}`
|
|
601
|
+
} else if (entry.type === 'action') {
|
|
602
|
+
// π Actions are at root level with icon + label + shortcut + description
|
|
603
|
+
const shortcut = entry.shortcut ? themeColors.dim(` (${entry.shortcut})`) : ''
|
|
604
|
+
const description = entry.description ? themeColors.dim(` β ${entry.description}`) : ''
|
|
605
|
+
rowLine = `${expandIndicator} ${iconPrefix}${themeColors.textBold(label)}${shortcut}${description}`
|
|
606
|
+
} else {
|
|
607
|
+
// π Regular commands in submenus
|
|
608
|
+
const shortcut = entry.shortcut ? themeColors.dim(` (${entry.shortcut})`) : ''
|
|
609
|
+
const description = entry.description ? themeColors.dim(` β ${entry.description}`) : ''
|
|
610
|
+
// π Color tiers and providers
|
|
611
|
+
let coloredLabel = label
|
|
612
|
+
let prefixWithIcon = iconPrefix
|
|
613
|
+
|
|
614
|
+
if (entry.providerKey && !entry.icon) {
|
|
615
|
+
// π Model filter: add provider icon
|
|
616
|
+
const providerIcon = 'π’'
|
|
617
|
+
prefixWithIcon = `${providerIcon} `
|
|
618
|
+
coloredLabel = themeColors.provider(entry.providerKey, label, { bold: false })
|
|
619
|
+
} else if (entry.tier) {
|
|
620
|
+
coloredLabel = themeColors.tier(entry.tier, label)
|
|
621
|
+
} else if (entry.providerKey) {
|
|
622
|
+
coloredLabel = themeColors.provider(entry.providerKey, label, { bold: false })
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
rowLine = `${indent} ${expandIndicator} ${prefixWithIcon}${coloredLabel}${shortcut}${description}`
|
|
574
626
|
}
|
|
575
627
|
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
const row = `${pointer}${padEndDisplay(label, labelMax)}${entry.shortcut ? ` ${shortcutText}` : ''}`
|
|
584
|
-
cursorLineByRow[idx] = groupedLines.length
|
|
585
|
-
groupedLines.push(isCursor ? themeColors.bgCursor(row) : row)
|
|
628
|
+
cursorLineByRow[idx] = panelLines.length
|
|
629
|
+
|
|
630
|
+
if (isCursor) {
|
|
631
|
+
panelLines.push(themeColors.bgCursor(rowLine))
|
|
632
|
+
} else {
|
|
633
|
+
panelLines.push(rowLine)
|
|
634
|
+
}
|
|
586
635
|
}
|
|
587
636
|
}
|
|
588
637
|
|
|
@@ -590,44 +639,43 @@ export function createOverlayRenderers(state, deps) {
|
|
|
590
639
|
state.commandPaletteScrollOffset = keepOverlayTargetVisible(
|
|
591
640
|
state.commandPaletteScrollOffset,
|
|
592
641
|
targetLine,
|
|
593
|
-
|
|
642
|
+
panelLines.length,
|
|
594
643
|
bodyRows
|
|
595
644
|
)
|
|
596
|
-
const { visible, offset } = sliceOverlayLines(
|
|
645
|
+
const { visible, offset } = sliceOverlayLines(panelLines, state.commandPaletteScrollOffset, bodyRows)
|
|
597
646
|
state.commandPaletteScrollOffset = offset
|
|
598
647
|
|
|
599
648
|
const query = state.commandPaletteQuery || ''
|
|
600
649
|
const queryWithCursor = query.length > 0
|
|
601
|
-
?
|
|
602
|
-
: themeColors.
|
|
650
|
+
? `${query}${themeColors.accentBold('β')}`
|
|
651
|
+
: themeColors.accentBold('β') + themeColors.dim(' Search commandsβ¦')
|
|
603
652
|
|
|
604
|
-
const
|
|
605
|
-
const title = themeColors.
|
|
653
|
+
const headerLines = []
|
|
654
|
+
const title = themeColors.headerBold('β‘οΈ Command Palette')
|
|
606
655
|
const titleLeft = ` ${title}`
|
|
607
|
-
const titleRight = themeColors.dim('Esc
|
|
608
|
-
const titleWidth = Math.max(1, panelInnerWidth - 1 - displayWidth('Esc
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
656
|
+
const titleRight = themeColors.dim('Esc')
|
|
657
|
+
const titleWidth = Math.max(1, panelInnerWidth - 1 - displayWidth('Esc'))
|
|
658
|
+
headerLines.push(`${padEndDisplay(titleLeft, titleWidth)} ${titleRight}`)
|
|
659
|
+
headerLines.push(` ${padEndDisplay(`> ${queryWithCursor}`, panelInnerWidth)}`)
|
|
660
|
+
headerLines.push(themeColors.dim(` ${'β'.repeat(Math.max(1, panelInnerWidth))}`))
|
|
661
|
+
|
|
662
|
+
const footerLines = [
|
|
663
|
+
themeColors.dim(` ${'β'.repeat(Math.max(1, panelInnerWidth))}`),
|
|
664
|
+
` ${padEndDisplay(themeColors.dim('β΅ Select β’ β β Expand'), panelInnerWidth)}`,
|
|
665
|
+
` ${padEndDisplay(themeColors.dim('ββ Navigate β’ Type search'), panelInnerWidth)}`,
|
|
666
|
+
]
|
|
616
667
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
668
|
+
const allPanelLines = [...headerLines, ...visible, ...footerLines]
|
|
669
|
+
|
|
670
|
+
while (allPanelLines.length < bodyRows + headerRowCount + 3) {
|
|
671
|
+
allPanelLines.splice(headerLines.length + visible.length, 0, ` ${' '.repeat(panelInnerWidth)}`)
|
|
620
672
|
}
|
|
621
673
|
|
|
622
|
-
panelLines.push(themeColors.dim(` ${'-'.repeat(Math.max(1, panelInnerWidth))}`))
|
|
623
|
-
panelLines.push(` ${padEndDisplay(themeColors.dim('ββ navigate β’ Enter run β’ Type to search'), panelInnerWidth)}`)
|
|
624
|
-
panelLines.push(` ${padEndDisplay(themeColors.dim('PgUp/PgDn β’ Home/End'), panelInnerWidth)}`)
|
|
625
|
-
|
|
626
674
|
const blankPaddedLine = ' '.repeat(panelOuterWidth)
|
|
627
675
|
const paddedPanelLines = [
|
|
628
676
|
blankPaddedLine,
|
|
629
677
|
blankPaddedLine,
|
|
630
|
-
...
|
|
678
|
+
...allPanelLines.map((line) => `${' '.repeat(panelPad)}${padEndDisplay(line, panelWidth)}${' '.repeat(panelPad)}`),
|
|
631
679
|
blankPaddedLine,
|
|
632
680
|
blankPaddedLine,
|
|
633
681
|
]
|
|
@@ -641,8 +689,6 @@ export function createOverlayRenderers(state, deps) {
|
|
|
641
689
|
return themeColors.overlayBgCommandPalette(padded)
|
|
642
690
|
})
|
|
643
691
|
|
|
644
|
-
// π Absolute cursor positioning overlays the palette on top of the existing table.
|
|
645
|
-
// π The next frame starts with ALT_HOME, so this remains stable without manual cleanup.
|
|
646
692
|
return tintedLines
|
|
647
693
|
.map((line, idx) => `\x1b[${top + idx};${left}H${line}`)
|
|
648
694
|
.join('')
|
|
@@ -678,8 +724,8 @@ export function createOverlayRenderers(state, deps) {
|
|
|
678
724
|
lines.push(` ${label('CTX')} Context window size (128k, 200k, 256k, 1m, etc.) ${hint('Sort:')} ${key('C')}`)
|
|
679
725
|
lines.push(` ${hint('Bigger context = the model can read more of your codebase at once without forgetting.')}`)
|
|
680
726
|
lines.push('')
|
|
681
|
-
lines.push(` ${label('Model')} Model name (β = favorited
|
|
682
|
-
lines.push(` ${hint('Star the ones you like
|
|
727
|
+
lines.push(` ${label('Model')} Model name (β = favorited) ${hint('Sort:')} ${key('M')} ${hint('Favorite:')} ${key('F')}`)
|
|
728
|
+
lines.push(` ${hint('Star the ones you like. Press Y to switch between pinned mode and normal filter/sort mode.')}`)
|
|
683
729
|
lines.push('')
|
|
684
730
|
lines.push(` ${label('Provider')} Provider source (NIM, Groq, Cerebras, etc.) ${hint('Sort:')} ${key('O')} ${hint('Cycle:')} ${key('D')}`)
|
|
685
731
|
lines.push(` ${hint('Same model on different providers can have very different speed and uptime.')}`)
|
|
@@ -716,11 +762,12 @@ export function createOverlayRenderers(state, deps) {
|
|
|
716
762
|
lines.push('')
|
|
717
763
|
lines.push(` ${heading('Controls')}`)
|
|
718
764
|
lines.push(` ${key('W')} Toggle ping mode ${hint('(speed 2s β normal 10s β slow 30s β forced 4s)')}`)
|
|
719
|
-
lines.push(` ${key('Ctrl+P')} Open command palette ${hint('(search and run actions quickly)')}`)
|
|
765
|
+
lines.push(` ${key('Ctrl+P')} Open β‘οΈ command palette ${hint('(search and run actions quickly)')}`)
|
|
720
766
|
lines.push(` ${key('E')} Toggle configured models only ${hint('(enabled by default)')}`)
|
|
721
767
|
lines.push(` ${key('Z')} Cycle tool mode ${hint('(OpenCode β Desktop β OpenClaw β Crush β Goose β Pi β Aider β Qwen β OpenHands β Amp)')}`)
|
|
722
|
-
lines.push(` ${key('F')} Toggle favorite on selected row ${hint('(β
|
|
723
|
-
lines.push(` ${key('Y')}
|
|
768
|
+
lines.push(` ${key('F')} Toggle favorite on selected row ${hint('(β persisted across sessions)')}`)
|
|
769
|
+
lines.push(` ${key('Y')} Toggle favorites mode ${hint('(Pinned + always visible β Normal filter/sort behavior)')}`)
|
|
770
|
+
lines.push(` ${key('X')} Clear active text filter ${hint('(remove custom query applied from β‘οΈ Command Palette)')}`)
|
|
724
771
|
lines.push(` ${key('Q')} Smart Recommend ${hint('(π― find the best model for your task β questionnaire + live analysis)')}`)
|
|
725
772
|
lines.push(` ${key('G')} Cycle theme ${hint('(auto β dark β light)')}`)
|
|
726
773
|
lines.push(` ${themeColors.errorBold('I')} Feedback, bugs & requests ${hint('(π send anonymous feedback, bug reports, or feature requests)')}`)
|
|
@@ -736,7 +783,8 @@ export function createOverlayRenderers(state, deps) {
|
|
|
736
783
|
lines.push(` ${key('PgUp/PgDn')} Jump by page`)
|
|
737
784
|
lines.push(` ${key('Home/End')} Jump first/last row`)
|
|
738
785
|
lines.push(` ${key('Enter')} Edit key / run selected maintenance action`)
|
|
739
|
-
lines.push(` ${key('Space')} Toggle provider
|
|
786
|
+
lines.push(` ${key('Space')} Toggle selected row option (provider/theme/favorites)`)
|
|
787
|
+
lines.push(` ${key('Y')} Toggle favorites mode (global)`)
|
|
740
788
|
lines.push(` ${key('T')} Test selected provider key`)
|
|
741
789
|
lines.push(` ${key('U')} Check updates manually`)
|
|
742
790
|
lines.push(` ${key('G')} Cycle theme globally`)
|