@@ -104,6 +104,9 @@ type Model struct {
104104 workflowList []workflow.Workflow
105105 workflowRun * workflow.WorkflowRunResult
106106 workflowCursor int
107+ // Projects tab search
108+ searchActive bool
109+ searchQuery string
107110}
108111
109112// projectDeletedMsg is sent after deleting a project.
@@ -212,7 +215,7 @@ func NewModel(version string) Model {
212215 pl .SetShowTitle (false )
213216 pl .SetShowHelp (false )
214217 pl .SetShowStatusBar (false )
215- pl .SetFilteringEnabled (true )
218+ pl .SetFilteringEnabled (false ) // Custom search implemented below
216219
217220 // Load profiles
218221 profileItems , _ := loadProfiles ()
@@ -335,15 +338,22 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
335338 return m .updatePartialRollback (msg )
336339 }
337340
341+ // Handle custom search mode for Projects tab
342+ if m .state == viewProjects && m .searchActive && msg .String () != "tab" {
343+ return m .updateProjectSearch (msg )
344+ }
345+ if m .state == viewProjects && m .searchActive { // tab pressed during search
346+ m .searchActive = false
347+ m .searchQuery = ""
348+ m .projectList .SetItems (loadProjects ())
349+ }
350+
338351 // Right panel focused — handle session selection
339352 if m .focus == focusRight && m .state == viewProjects {
340353 return m .updateRightPanel (msg )
341354 }
342355
343356 // Don't intercept keys when the current list is filtering
344- if m .state == viewProjects && m .projectList .FilterState () == list .Filtering {
345- return m .updateList (msg )
346- }
347357 if m .state == viewConfig {
348358 if m .configSubTab == configProfiles && m .profileList .FilterState () == list .Filtering {
349359 return m .updateList (msg )
@@ -446,6 +456,11 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
446456 }
447457 }
448458
459+ case msg .String () == "/" && m .state == viewProjects && m .focus == focusLeft :
460+ m .searchActive = true
461+ m .searchQuery = ""
462+ return m , nil
463+
449464 case msg .String () == "l" && m .state == viewProjects :
450465 // Open link management for selected project
451466 if item , ok := m .projectList .SelectedItem ().(projectItem ); ok {
@@ -1360,7 +1375,12 @@ func (m Model) View() string {
13601375 }
13611376
13621377 // Status/Error display
1363- if m .err != "" {
1378+ if m .state == viewProjects && m .searchActive {
1379+ b .WriteString ("\n " )
1380+ searchStyle := lipgloss .NewStyle ().Foreground (primaryColor ).Bold (true )
1381+ cursor := lipgloss .NewStyle ().Background (primaryColor ).Foreground (lipgloss .Color ("#FFFFFF" )).Render (" " )
1382+ b .WriteString (" " + searchStyle .Render ("/" ) + " " + m .searchQuery + cursor )
1383+ } else if m .err != "" {
13641384 b .WriteString ("\n " )
13651385 b .WriteString (statusErrorStyle .Render (" Error: " + m .err ))
13661386 } else if m .statusMsg != "" {
@@ -1476,8 +1496,10 @@ func (m Model) renderAgentSubHeader(width int) string {
14761496}
14771497
14781498func (m Model ) renderHelp () string {
1479- if m .state == viewAddForm {
1480- return formHintStyle .Render ("Tab: switch fields Enter: add Esc: cancel" )
1499+ if m .state == viewProjects && m .searchActive {
1500+ return formHintStyle .Render ("type to filter Backspace: delete Enter: confirm Esc: clear" )
1501+ }
1502+ if m .state == viewAddForm { return formHintStyle .Render ("Tab: switch fields Enter: add Esc: cancel" )
14811503 }
14821504 if m .state == viewAddProfile {
14831505 return formHintStyle .Render ("Tab: switch fields Space: toggle Enter: add Esc: cancel" )
@@ -1692,3 +1714,58 @@ func openBrowser(url string) error {
16921714 return fmt .Errorf ("unsupported platform" )
16931715 }
16941716}
1717+
1718+ // updateProjectSearch handles key events when search mode is active on the Projects tab.
1719+ func (m Model ) updateProjectSearch (msg tea.KeyMsg ) (tea.Model , tea.Cmd ) {
1720+ switch msg .String () {
1721+ case "esc" :
1722+ m .searchActive = false
1723+ m .searchQuery = ""
1724+ m .projectList .SetItems (loadProjects ())
1725+ case "enter" :
1726+ // Confirm filter, exit search mode (filtered items remain)
1727+ m .searchActive = false
1728+ case "backspace" , "ctrl+h" :
1729+ if len (m .searchQuery ) > 0 {
1730+ runes := []rune (m .searchQuery )
1731+ m .searchQuery = string (runes [:len (runes )- 1 ])
1732+ m = m .applyProjectSearch ()
1733+ }
1734+ case "ctrl+u" :
1735+ // Clear entire query
1736+ m .searchQuery = ""
1737+ m .projectList .SetItems (loadProjects ())
1738+ case "q" , "ctrl+c" :
1739+ return m , tea .Quit
1740+ default :
1741+ if len (msg .Runes ) > 0 {
1742+ m .searchQuery += string (msg .Runes )
1743+ m = m .applyProjectSearch ()
1744+ }
1745+ }
1746+ return m , nil
1747+ }
1748+
1749+ // applyProjectSearch filters the project list by the current searchQuery.
1750+ func (m Model ) applyProjectSearch () Model {
1751+ if m .searchQuery == "" {
1752+ m .projectList .SetItems (loadProjects ())
1753+ return m
1754+ }
1755+ query := strings .ToLower (m .searchQuery )
1756+ all := loadProjects ()
1757+ var filtered []list.Item
1758+ for _ , item := range all {
1759+ if proj , ok := item .(projectItem ); ok {
1760+ if strings .Contains (strings .ToLower (proj .info .Name ), query ) {
1761+ filtered = append (filtered , item )
1762+ }
1763+ }
1764+ }
1765+ if len (filtered ) == 0 {
1766+ m .projectList .SetItems ([]list.Item {})
1767+ } else {
1768+ m .projectList .SetItems (filtered )
1769+ }
1770+ return m
1771+ }
0 commit comments