free-coding-models 0.3.19 β†’ 0.3.22

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,7 +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'
34
+ import { buildCommandPaletteTree, flattenCommandTree, filterCommandPaletteEntries } from './command-palette.js'
35
+ import { WIDTH_WARNING_MIN_COLS } from './constants.js'
35
36
 
36
37
  // πŸ“– Some providers need an explicit probe model because the first catalog entry
37
38
  // πŸ“– is not guaranteed to be accepted by their chat endpoint.
@@ -447,38 +448,6 @@ export function createKeyHandler(ctx) {
447
448
  }
448
449
  }
449
450
 
450
- // πŸ“– Keep the width-warning runtime state synced with the persisted Settings toggle
451
- // πŸ“– so the overlay reacts immediately when the user enables or disables it.
452
- function syncWidthsWarningState() {
453
- const widthsWarningDisabled = state.config.settings?.disableWidthsWarning === true
454
- state.disableWidthsWarning = widthsWarningDisabled
455
-
456
- if (widthsWarningDisabled) {
457
- state.widthWarningStartedAt = null
458
- state.widthWarningDismissed = false
459
- return
460
- }
461
-
462
- state.widthWarningShowCount = 0
463
- if ((state.terminalCols || 80) < 166) {
464
- state.widthWarningStartedAt = Date.now()
465
- state.widthWarningDismissed = false
466
- return
467
- }
468
-
469
- state.widthWarningStartedAt = null
470
- state.widthWarningDismissed = false
471
- }
472
-
473
- // πŸ“– Toggle the width-warning setting and apply the effect immediately instead
474
- // πŸ“– of waiting for a resize or restart.
475
- function toggleWidthsWarningSetting() {
476
- if (!state.config.settings) state.config.settings = {}
477
- state.config.settings.disableWidthsWarning = !state.config.settings.disableWidthsWarning
478
- syncWidthsWarningState()
479
- saveConfig(state.config)
480
- }
481
-
482
451
  // πŸ“– Theme switches need to update both persisted preference and the live
483
452
  // πŸ“– semantic palette immediately so every screen redraw adopts the new colors.
484
453
  function applyThemeSetting(nextTheme) {
@@ -640,6 +609,7 @@ export function createKeyHandler(ctx) {
640
609
  function resetViewSettings() {
641
610
  state.tierFilterMode = 0
642
611
  state.originFilterMode = 0
612
+ state.customTextFilter = null // πŸ“– Clear ephemeral text filter on view reset
643
613
  state.sortColumn = 'avg'
644
614
  state.sortDirection = 'asc'
645
615
  if (!state.config.settings || typeof state.config.settings !== 'object') state.config.settings = {}
@@ -684,8 +654,33 @@ export function createKeyHandler(ctx) {
684
654
  }
685
655
 
686
656
  function refreshCommandPaletteResults() {
687
- const commands = buildCommandPaletteEntries()
688
- state.commandPaletteResults = filterCommandPaletteEntries(commands, state.commandPaletteQuery)
657
+ const tree = buildCommandPaletteTree(state.results || [])
658
+ const flat = flattenCommandTree(tree, state.commandPaletteExpandedIds)
659
+ state.commandPaletteResults = filterCommandPaletteEntries(flat, state.commandPaletteQuery)
660
+
661
+ const query = (state.commandPaletteQuery || '').trim()
662
+ if (query.length > 0) {
663
+ state.commandPaletteResults.unshift({
664
+ id: 'filter-custom-text-apply',
665
+ label: `πŸ” Apply text filter: ${query}`,
666
+ type: 'command',
667
+ depth: 0,
668
+ hasChildren: false,
669
+ isExpanded: false,
670
+ filterQuery: query,
671
+ })
672
+ } else if (state.customTextFilter) {
673
+ state.commandPaletteResults.unshift({
674
+ id: 'filter-custom-text-remove',
675
+ label: `❌ Remove custom filter: ${state.customTextFilter}`,
676
+ type: 'command',
677
+ depth: 0,
678
+ hasChildren: false,
679
+ isExpanded: false,
680
+ filterQuery: null,
681
+ })
682
+ }
683
+
689
684
  if (state.commandPaletteCursor >= state.commandPaletteResults.length) {
690
685
  state.commandPaletteCursor = Math.max(0, state.commandPaletteResults.length - 1)
691
686
  }
@@ -713,7 +708,43 @@ export function createKeyHandler(ctx) {
713
708
  if (!entry?.id) return
714
709
 
715
710
  if (entry.id.startsWith('filter-tier-')) {
716
- setTierFilterFromCommand(entry.tierValue ?? null)
711
+ setTierFilterFromCommand(entry.tier ?? null)
712
+ return
713
+ }
714
+
715
+ if (entry.id.startsWith('filter-provider-') && entry.id !== 'filter-provider-cycle') {
716
+ if (entry.providerKey === null || entry.providerKey === undefined) {
717
+ state.originFilterMode = 0 // All
718
+ } else {
719
+ state.originFilterMode = ORIGIN_CYCLE.findIndex(key => key === entry.providerKey) + 1
720
+ if (state.originFilterMode <= 0) state.originFilterMode = 0
721
+ }
722
+ applyTierFilter()
723
+ refreshVisibleSorted({ resetCursor: true })
724
+ persistUiSettings()
725
+ return
726
+ }
727
+
728
+ if (entry.id.startsWith('filter-model-')) {
729
+ if (entry.modelId && entry.providerKey) {
730
+ state.customTextFilter = `${entry.providerKey}/${entry.modelId}`
731
+ applyTierFilter()
732
+ refreshVisibleSorted({ resetCursor: true })
733
+ }
734
+ return
735
+ }
736
+
737
+ // πŸ“– Custom text filter β€” apply or remove the free-text filter from the command palette.
738
+ if (entry.id === 'filter-custom-text-apply') {
739
+ state.customTextFilter = entry.filterQuery || null
740
+ applyTierFilter()
741
+ refreshVisibleSorted({ resetCursor: true })
742
+ return
743
+ }
744
+ if (entry.id === 'filter-custom-text-remove') {
745
+ state.customTextFilter = null
746
+ applyTierFilter()
747
+ refreshVisibleSorted({ resetCursor: true })
717
748
  return
718
749
  }
719
750
 
@@ -789,6 +820,7 @@ export function createKeyHandler(ctx) {
789
820
  if (key.ctrl && key.name === 'c') { exit(0); return }
790
821
 
791
822
  const pageStep = Math.max(1, (state.terminalRows || 1) - 10)
823
+ const selected = state.commandPaletteResults[state.commandPaletteCursor]
792
824
 
793
825
  if (key.name === 'escape') {
794
826
  closeCommandPalette()
@@ -806,6 +838,23 @@ export function createKeyHandler(ctx) {
806
838
  state.commandPaletteCursor = state.commandPaletteCursor < count - 1 ? state.commandPaletteCursor + 1 : 0
807
839
  return
808
840
  }
841
+ if (key.name === 'left') {
842
+ if (selected?.hasChildren && selected.isExpanded) {
843
+ state.commandPaletteExpandedIds.delete(selected.id)
844
+ refreshCommandPaletteResults()
845
+ }
846
+ return
847
+ }
848
+ if (key.name === 'right') {
849
+ if (selected?.hasChildren && !selected.isExpanded) {
850
+ state.commandPaletteExpandedIds.add(selected.id)
851
+ refreshCommandPaletteResults()
852
+ } else if (selected?.type === 'command') {
853
+ closeCommandPalette()
854
+ executeCommandPaletteEntry(selected)
855
+ }
856
+ return
857
+ }
809
858
  if (key.name === 'pageup') {
810
859
  state.commandPaletteCursor = Math.max(0, state.commandPaletteCursor - pageStep)
811
860
  return
@@ -831,9 +880,17 @@ export function createKeyHandler(ctx) {
831
880
  return
832
881
  }
833
882
  if (key.name === 'return') {
834
- const selectedCommand = state.commandPaletteResults[state.commandPaletteCursor]
835
- closeCommandPalette()
836
- executeCommandPaletteEntry(selectedCommand)
883
+ if (selected?.hasChildren) {
884
+ if (selected.isExpanded) {
885
+ state.commandPaletteExpandedIds.delete(selected.id)
886
+ } else {
887
+ state.commandPaletteExpandedIds.add(selected.id)
888
+ }
889
+ refreshCommandPaletteResults()
890
+ } else {
891
+ closeCommandPalette()
892
+ executeCommandPaletteEntry(selected)
893
+ }
837
894
  return
838
895
  }
839
896
  if (str && str.length === 1 && !key.ctrl && !key.meta) {
@@ -1384,8 +1441,7 @@ export function createKeyHandler(ctx) {
1384
1441
  if (state.settingsOpen) {
1385
1442
  const providerKeys = Object.keys(sources)
1386
1443
  const updateRowIdx = providerKeys.length
1387
- const widthWarningRowIdx = updateRowIdx + 1
1388
- const themeRowIdx = widthWarningRowIdx + 1
1444
+ const themeRowIdx = updateRowIdx + 1
1389
1445
  const cleanupLegacyProxyRowIdx = themeRowIdx + 1
1390
1446
  const changelogViewRowIdx = cleanupLegacyProxyRowIdx + 1
1391
1447
  // πŸ“– Profile system removed - API keys now persist permanently across all sessions
@@ -1530,12 +1586,6 @@ export function createKeyHandler(ctx) {
1530
1586
  return
1531
1587
  }
1532
1588
 
1533
- // πŸ“– Widths Warning toggle (Enter to toggle)
1534
- if (state.settingsCursor === widthWarningRowIdx) {
1535
- toggleWidthsWarningSetting()
1536
- return
1537
- }
1538
-
1539
1589
  if (state.settingsCursor === themeRowIdx) {
1540
1590
  cycleGlobalTheme()
1541
1591
  return
@@ -1579,11 +1629,6 @@ export function createKeyHandler(ctx) {
1579
1629
  cycleGlobalTheme()
1580
1630
  return
1581
1631
  }
1582
- // πŸ“– Widths Warning toggle (disable/enable)
1583
- if (state.settingsCursor === widthWarningRowIdx) {
1584
- toggleWidthsWarningSetting()
1585
- return
1586
- }
1587
1632
  // πŸ“– Profile system removed - API keys now persist permanently across all sessions
1588
1633
 
1589
1634
  // πŸ“– Toggle enabled/disabled for selected provider
@@ -1598,7 +1643,6 @@ export function createKeyHandler(ctx) {
1598
1643
  if (key.name === 't') {
1599
1644
  if (
1600
1645
  state.settingsCursor === updateRowIdx
1601
- || state.settingsCursor === widthWarningRowIdx
1602
1646
  || state.settingsCursor === themeRowIdx
1603
1647
  || state.settingsCursor === cleanupLegacyProxyRowIdx
1604
1648
  || state.settingsCursor === changelogViewRowIdx
@@ -1663,11 +1707,7 @@ export function createKeyHandler(ctx) {
1663
1707
  return
1664
1708
  }
1665
1709
 
1666
- // πŸ“– Y key: open Install Endpoints flow for configured providers.
1667
- if (key.name === 'y') {
1668
- openInstallEndpointsOverlay()
1669
- return
1670
- }
1710
+ // πŸ“– Y key freed β€” Install Endpoints is now accessible only via Settings (P) or Command Palette (Ctrl+P).
1671
1711
 
1672
1712
  // πŸ“– Profile system removed - API keys now persist permanently across all sessions
1673
1713
 
@@ -1680,7 +1720,7 @@ export function createKeyHandler(ctx) {
1680
1720
  }
1681
1721
 
1682
1722
  // πŸ“– 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
1683
- // πŸ“– T is reserved for tier filter cycling. Y now opens the install-endpoints flow.
1723
+ // πŸ“– T is reserved for tier filter cycling. Y is now free (Install Endpoints moved to Settings/Palette).
1684
1724
  // πŸ“– D is now reserved for provider filter cycling
1685
1725
  // πŸ“– Shift+R is reserved for reset view settings
1686
1726
  const sortKeys = {
@@ -1805,7 +1845,7 @@ export function createKeyHandler(ctx) {
1805
1845
  }
1806
1846
 
1807
1847
  // πŸ“– Esc can dismiss the narrow-terminal warning immediately without quitting the app.
1808
- if (key.name === 'escape' && state.terminalCols > 0 && state.terminalCols < 166) {
1848
+ if (key.name === 'escape' && state.terminalCols > 0 && state.terminalCols < WIDTH_WARNING_MIN_COLS) {
1809
1849
  state.widthWarningDismissed = true
1810
1850
  return
1811
1851
  }
package/src/overlays.js CHANGED
@@ -93,8 +93,7 @@ export function createOverlayRenderers(state, deps) {
93
93
  function renderSettings() {
94
94
  const providerKeys = Object.keys(sources)
95
95
  const updateRowIdx = providerKeys.length
96
- const widthWarningRowIdx = updateRowIdx + 1
97
- const themeRowIdx = widthWarningRowIdx + 1
96
+ const themeRowIdx = updateRowIdx + 1
98
97
  const cleanupLegacyProxyRowIdx = themeRowIdx + 1
99
98
  const changelogViewRowIdx = cleanupLegacyProxyRowIdx + 1
100
99
  const EL = '\x1b[K'
@@ -220,14 +219,6 @@ export function createOverlayRenderers(state, deps) {
220
219
  const updateRow = `${bullet(updateCursor)}${themeColors.textBold(updateActionLabel).padEnd(44)} ${updateStatus}`
221
220
  cursorLineByRow[updateRowIdx] = lines.length
222
221
  lines.push(updateCursor ? themeColors.bgCursor(updateRow) : updateRow)
223
- // πŸ“– Width warning visibility row for the startup narrow-terminal overlay.
224
- const disableWidthsWarning = Boolean(state.config.settings?.disableWidthsWarning)
225
- const widthWarningStatus = disableWidthsWarning
226
- ? themeColors.errorBold('πŸ™ˆ Disabled')
227
- : themeColors.successBold('πŸ‘ Enabled')
228
- const widthWarningRow = `${bullet(state.settingsCursor === widthWarningRowIdx)}${themeColors.textBold('Small Width Warnings').padEnd(44)} ${widthWarningStatus}`
229
- cursorLineByRow[widthWarningRowIdx] = lines.length
230
- lines.push(state.settingsCursor === widthWarningRowIdx ? themeColors.bgCursor(widthWarningRow) : widthWarningRow)
231
222
  const themeStatus = getThemeStatusLabel(activeThemeSetting())
232
223
  const themeStatusColor = themeStatus.includes('Dark') ? themeColors.warningBold : themeColors.info
233
224
  const themeRow = `${bullet(state.settingsCursor === themeRowIdx)}${themeColors.textBold('Global Theme').padEnd(44)} ${themeStatusColor(themeStatus)}`
@@ -537,18 +528,16 @@ export function createOverlayRenderers(state, deps) {
537
528
 
538
529
  // ─── Command palette renderer ──────────────────────────────────────────────
539
530
  // πŸ“– renderCommandPalette draws a centered floating modal over the live table.
540
- // πŸ“– It returns cursor-positioned ANSI rows instead of replacing the full screen,
541
- // πŸ“– so ping updates continue to animate in the background behind the palette.
531
+ // πŸ“– Supports hierarchical categories with expand/collapse and rich colors.
542
532
  function renderCommandPalette() {
543
533
  const terminalRows = state.terminalRows || 24
544
534
  const terminalCols = state.terminalCols || 80
545
- const panelWidth = Math.max(44, Math.min(96, terminalCols - 8))
546
- const panelInnerWidth = Math.max(28, panelWidth - 4)
535
+ const panelWidth = Math.max(52, Math.min(100, terminalCols - 8))
536
+ const panelInnerWidth = Math.max(32, panelWidth - 4)
547
537
  const panelPad = 2
548
538
  const panelOuterWidth = panelWidth + (panelPad * 2)
549
- const footerRowCount = 2
550
- const headerRowCount = 3
551
- const bodyRows = Math.max(6, Math.min(16, terminalRows - 12))
539
+ const headerRowCount = 4
540
+ const bodyRows = Math.max(8, Math.min(18, terminalRows - 12))
552
541
 
553
542
  const truncatePlain = (text, width) => {
554
543
  if (width <= 1) return ''
@@ -568,30 +557,70 @@ export function createOverlayRenderers(state, deps) {
568
557
  }
569
558
 
570
559
  const allResults = Array.isArray(state.commandPaletteResults) ? state.commandPaletteResults.slice(0, 80) : []
571
- const groupedLines = []
560
+ const panelLines = []
572
561
  const cursorLineByRow = {}
573
- let category = null
574
562
 
575
563
  if (allResults.length === 0) {
576
- groupedLines.push(themeColors.dim(' No command found. Try a broader query.'))
564
+ panelLines.push(themeColors.dim(' No commands found. Try a different search.'))
577
565
  } else {
578
566
  for (let idx = 0; idx < allResults.length; idx++) {
579
567
  const entry = allResults[idx]
580
- if (entry.category !== category) {
581
- category = entry.category
582
- groupedLines.push(themeColors.textBold(` ${category}`))
568
+ const isCursor = idx === state.commandPaletteCursor
569
+
570
+ const indent = ' '.repeat(entry.depth || 0)
571
+ const expandIndicator = entry.hasChildren
572
+ ? (entry.isExpanded ? themeColors.infoBold('β–Ό') : themeColors.dim('β–Ά'))
573
+ : themeColors.dim('β€’')
574
+
575
+ // πŸ“– Only use icon from entry, label should NOT include emoji
576
+ const iconPrefix = entry.icon ? `${entry.icon} ` : ''
577
+ const plainLabel = truncatePlain(entry.label, panelInnerWidth - indent.length - iconPrefix.length - 4)
578
+ const label = entry.matchPositions ? highlightMatch(plainLabel, entry.matchPositions) : plainLabel
579
+
580
+ let rowLine
581
+ if (entry.type === 'category') {
582
+ rowLine = `${indent}${expandIndicator} ${iconPrefix}${themeColors.headerBold(label)}`
583
+ } else if (entry.type === 'subcategory') {
584
+ rowLine = `${indent}${expandIndicator} ${iconPrefix}${themeColors.textBold(label)}`
585
+ } else if (entry.type === 'page') {
586
+ // πŸ“– Pages are at root level with icon + label + shortcut + description
587
+ const shortcut = entry.shortcut ? themeColors.dim(` (${entry.shortcut})`) : ''
588
+ const description = entry.description ? themeColors.dim(` β€” ${entry.description}`) : ''
589
+ rowLine = `${expandIndicator} ${iconPrefix}${themeColors.textBold(label)}${shortcut}${description}`
590
+ } else if (entry.type === 'action') {
591
+ // πŸ“– Actions are at root level with icon + label + shortcut + description
592
+ const shortcut = entry.shortcut ? themeColors.dim(` (${entry.shortcut})`) : ''
593
+ const description = entry.description ? themeColors.dim(` β€” ${entry.description}`) : ''
594
+ rowLine = `${expandIndicator} ${iconPrefix}${themeColors.textBold(label)}${shortcut}${description}`
595
+ } else {
596
+ // πŸ“– Regular commands in submenus
597
+ const shortcut = entry.shortcut ? themeColors.dim(` (${entry.shortcut})`) : ''
598
+ const description = entry.description ? themeColors.dim(` β€” ${entry.description}`) : ''
599
+ // πŸ“– Color tiers and providers
600
+ let coloredLabel = label
601
+ let prefixWithIcon = iconPrefix
602
+
603
+ if (entry.providerKey && !entry.icon) {
604
+ // πŸ“– Model filter: add provider icon
605
+ const providerIcon = '🏒'
606
+ prefixWithIcon = `${providerIcon} `
607
+ coloredLabel = themeColors.provider(entry.providerKey, label, { bold: false })
608
+ } else if (entry.tier) {
609
+ coloredLabel = themeColors.tier(entry.tier, label)
610
+ } else if (entry.providerKey) {
611
+ coloredLabel = themeColors.provider(entry.providerKey, label, { bold: false })
612
+ }
613
+
614
+ rowLine = `${indent} ${expandIndicator} ${prefixWithIcon}${coloredLabel}${shortcut}${description}`
583
615
  }
584
616
 
585
- const isCursor = idx === state.commandPaletteCursor
586
- const pointer = isCursor ? themeColors.accentBold(' ❯ ') : themeColors.dim(' ')
587
- const shortcutText = entry.shortcut ? themeColors.dim(entry.shortcut) : ''
588
- const shortcutWidth = entry.shortcut ? Math.min(16, displayWidth(entry.shortcut)) : 0
589
- const labelMax = Math.max(12, panelInnerWidth - 8 - shortcutWidth)
590
- const plainLabel = truncatePlain(entry.label, labelMax)
591
- const label = highlightMatch(plainLabel, entry.matchPositions)
592
- const row = `${pointer}${padEndDisplay(label, labelMax)}${entry.shortcut ? ` ${shortcutText}` : ''}`
593
- cursorLineByRow[idx] = groupedLines.length
594
- groupedLines.push(isCursor ? themeColors.bgCursor(row) : row)
617
+ cursorLineByRow[idx] = panelLines.length
618
+
619
+ if (isCursor) {
620
+ panelLines.push(themeColors.bgCursor(rowLine))
621
+ } else {
622
+ panelLines.push(rowLine)
623
+ }
595
624
  }
596
625
  }
597
626
 
@@ -599,44 +628,43 @@ export function createOverlayRenderers(state, deps) {
599
628
  state.commandPaletteScrollOffset = keepOverlayTargetVisible(
600
629
  state.commandPaletteScrollOffset,
601
630
  targetLine,
602
- groupedLines.length,
631
+ panelLines.length,
603
632
  bodyRows
604
633
  )
605
- const { visible, offset } = sliceOverlayLines(groupedLines, state.commandPaletteScrollOffset, bodyRows)
634
+ const { visible, offset } = sliceOverlayLines(panelLines, state.commandPaletteScrollOffset, bodyRows)
606
635
  state.commandPaletteScrollOffset = offset
607
636
 
608
637
  const query = state.commandPaletteQuery || ''
609
638
  const queryWithCursor = query.length > 0
610
- ? themeColors.textBold(`${query}▏`)
611
- : themeColors.dim('type a command…') + themeColors.accentBold('▏')
639
+ ? `${query}${themeColors.accentBold('▏')}`
640
+ : themeColors.accentBold('▏') + themeColors.dim(' Search commands…')
612
641
 
613
- const panelLines = []
614
- const title = themeColors.textBold('Command Palette')
642
+ const headerLines = []
643
+ const title = themeColors.headerBold('⚑️ Command Palette')
615
644
  const titleLeft = ` ${title}`
616
- const titleRight = themeColors.dim('Esc close')
617
- const titleWidth = Math.max(1, panelInnerWidth - 1 - displayWidth('Esc close'))
618
- panelLines.push(`${padEndDisplay(titleLeft, titleWidth)} ${titleRight}`)
619
- panelLines.push(` ${padEndDisplay(`> ${queryWithCursor}`, panelInnerWidth)}`)
620
- panelLines.push(themeColors.dim(` ${'-'.repeat(Math.max(1, panelInnerWidth))}`))
621
-
622
- for (const line of visible) {
623
- panelLines.push(` ${padEndDisplay(line, panelInnerWidth)}`)
624
- }
645
+ const titleRight = themeColors.dim('Esc')
646
+ const titleWidth = Math.max(1, panelInnerWidth - 1 - displayWidth('Esc'))
647
+ headerLines.push(`${padEndDisplay(titleLeft, titleWidth)} ${titleRight}`)
648
+ headerLines.push(` ${padEndDisplay(`> ${queryWithCursor}`, panelInnerWidth)}`)
649
+ headerLines.push(themeColors.dim(` ${'─'.repeat(Math.max(1, panelInnerWidth))}`))
650
+
651
+ const footerLines = [
652
+ themeColors.dim(` ${'─'.repeat(Math.max(1, panelInnerWidth))}`),
653
+ ` ${padEndDisplay(themeColors.dim('↡ Select β€’ ← β†’ Expand'), panelInnerWidth)}`,
654
+ ` ${padEndDisplay(themeColors.dim('↑↓ Navigate β€’ Type search'), panelInnerWidth)}`,
655
+ ]
625
656
 
626
- // πŸ“– Keep panel body stable by filling with blank rows when result list is short.
627
- while (panelLines.length < bodyRows + headerRowCount) {
628
- panelLines.push(` ${' '.repeat(panelInnerWidth)}`)
657
+ const allPanelLines = [...headerLines, ...visible, ...footerLines]
658
+
659
+ while (allPanelLines.length < bodyRows + headerRowCount + 3) {
660
+ allPanelLines.splice(headerLines.length + visible.length, 0, ` ${' '.repeat(panelInnerWidth)}`)
629
661
  }
630
662
 
631
- panelLines.push(themeColors.dim(` ${'-'.repeat(Math.max(1, panelInnerWidth))}`))
632
- panelLines.push(` ${padEndDisplay(themeColors.dim('↑↓ navigate β€’ Enter run β€’ Type to search'), panelInnerWidth)}`)
633
- panelLines.push(` ${padEndDisplay(themeColors.dim('PgUp/PgDn β€’ Home/End'), panelInnerWidth)}`)
634
-
635
663
  const blankPaddedLine = ' '.repeat(panelOuterWidth)
636
664
  const paddedPanelLines = [
637
665
  blankPaddedLine,
638
666
  blankPaddedLine,
639
- ...panelLines.map((line) => `${' '.repeat(panelPad)}${padEndDisplay(line, panelWidth)}${' '.repeat(panelPad)}`),
667
+ ...allPanelLines.map((line) => `${' '.repeat(panelPad)}${padEndDisplay(line, panelWidth)}${' '.repeat(panelPad)}`),
640
668
  blankPaddedLine,
641
669
  blankPaddedLine,
642
670
  ]
@@ -650,8 +678,6 @@ export function createOverlayRenderers(state, deps) {
650
678
  return themeColors.overlayBgCommandPalette(padded)
651
679
  })
652
680
 
653
- // πŸ“– Absolute cursor positioning overlays the palette on top of the existing table.
654
- // πŸ“– The next frame starts with ALT_HOME, so this remains stable without manual cleanup.
655
681
  return tintedLines
656
682
  .map((line, idx) => `\x1b[${top + idx};${left}H${line}`)
657
683
  .join('')
@@ -725,11 +751,10 @@ export function createOverlayRenderers(state, deps) {
725
751
  lines.push('')
726
752
  lines.push(` ${heading('Controls')}`)
727
753
  lines.push(` ${key('W')} Toggle ping mode ${hint('(speed 2s β†’ normal 10s β†’ slow 30s β†’ forced 4s)')}`)
728
- lines.push(` ${key('Ctrl+P')} Open command palette ${hint('(search and run actions quickly)')}`)
754
+ lines.push(` ${key('Ctrl+P')} Open ⚑️ command palette ${hint('(search and run actions quickly)')}`)
729
755
  lines.push(` ${key('E')} Toggle configured models only ${hint('(enabled by default)')}`)
730
756
  lines.push(` ${key('Z')} Cycle tool mode ${hint('(OpenCode β†’ Desktop β†’ OpenClaw β†’ Crush β†’ Goose β†’ Pi β†’ Aider β†’ Qwen β†’ OpenHands β†’ Amp)')}`)
731
757
  lines.push(` ${key('F')} Toggle favorite on selected row ${hint('(⭐ pinned at top, persisted)')}`)
732
- lines.push(` ${key('Y')} Install endpoints ${hint('(provider catalog β†’ compatible tools, direct provider only)')}`)
733
758
  lines.push(` ${key('Q')} Smart Recommend ${hint('(🎯 find the best model for your task β€” questionnaire + live analysis)')}`)
734
759
  lines.push(` ${key('G')} Cycle theme ${hint('(auto β†’ dark β†’ light)')}`)
735
760
  lines.push(` ${themeColors.errorBold('I')} Feedback, bugs & requests ${hint('(πŸ“ send anonymous feedback, bug reports, or feature requests)')}`)
@@ -1,14 +1,9 @@
1
1
  /**
2
2
  * @file src/product-flags.js
3
- * @description Product-level copy for temporarily unavailable surfaces.
3
+ * @description Product-level flags and feature gates.
4
4
  *
5
5
  * @details
6
- * πŸ“– The proxy bridge is being rebuilt from scratch. The main TUI still
7
- * πŸ“– shows a clear status line so users know the missing integration is
8
- * πŸ“– intentional instead of silently broken.
9
- *
10
- * @exports PROXY_DISABLED_NOTICE
6
+ * πŸ“– Previously held PROXY_DISABLED_NOTICE for the proxy bridge rebuild.
7
+ * πŸ“– That notice was removed in 0.3.22 after the proxy surface was fully
8
+ * πŸ“– retired. File kept as a home for future product-level flags.
11
9
  */
12
-
13
- // πŸ“– Public note rendered in the main TUI footer and reused in CLI/runtime guards.
14
- export const PROXY_DISABLED_NOTICE = 'ℹ️ Proxy is temporarily disabled while we rebuild it into a much more stable bridge for external tools.'