99 "github.com/Bigsy/mcpmu/internal/tui/theme"
1010 "github.com/charmbracelet/bubbles/key"
1111 "github.com/charmbracelet/bubbles/list"
12+ "github.com/charmbracelet/bubbles/textinput"
1213 tea "github.com/charmbracelet/bubbletea"
1314 "github.com/charmbracelet/lipgloss"
1415)
@@ -44,6 +45,11 @@ type ToolDenyEditorModel struct {
4445 // Current deny state: toolName -> denied
4546 denied map [string ]bool
4647
48+ // Filter
49+ filterInput textinput.Model
50+ allItems []list.Item // full unfiltered list, set in Show()
51+ filterFocused bool
52+
4753 // Key bindings
4854 escKey key.Binding
4955 enterKey key.Binding
@@ -56,16 +62,19 @@ func NewToolDenyEditor(th theme.Theme) ToolDenyEditorModel {
5662 l := list .New ([]list.Item {}, delegate , 0 , 0 )
5763 l .Title = "Denied Tools"
5864 l .SetShowStatusBar (false )
59- l .SetFilteringEnabled (true )
65+ l .SetFilteringEnabled (false )
6066 l .SetShowHelp (false )
6167 l .Styles .Title = th .Title
62- l .FilterInput .PromptStyle = th .Primary
63- l .FilterInput .Cursor .Style = th .Primary
68+
69+ ti := textinput .New ()
70+ ti .Placeholder = "/ to filter..."
71+ ti .CharLimit = 100
6472
6573 return ToolDenyEditorModel {
66- theme : th ,
67- list : l ,
68- denied : make (map [string ]bool ),
74+ theme : th ,
75+ list : l ,
76+ denied : make (map [string ]bool ),
77+ filterInput : ti ,
6978 escKey : key .NewBinding (
7079 key .WithKeys ("esc" ),
7180 key .WithHelp ("esc" , "cancel" ),
@@ -100,6 +109,10 @@ func (m *ToolDenyEditorModel) Show(serverName string, tools []mcp.Tool, deniedTo
100109 })
101110 }
102111
112+ m .allItems = items
113+ m .filterInput .SetValue ("" )
114+ m .filterInput .Blur ()
115+ m .filterFocused = false
103116 m .list .SetItems (items )
104117 m .list .SetDelegate (newToolDenyDelegate (m .theme , m .denied ))
105118}
@@ -121,7 +134,24 @@ func (m *ToolDenyEditorModel) SetSize(width, height int) {
121134 if height < 30 {
122135 editorHeight = height - 5
123136 }
124- m .list .SetSize (editorWidth - 6 , editorHeight - 6 )
137+ m .list .SetSize (editorWidth - 6 , editorHeight - 8 )
138+ }
139+
140+ // applyFilter filters the list items based on the current filter input value.
141+ func (m * ToolDenyEditorModel ) applyFilter () {
142+ query := strings .ToLower (m .filterInput .Value ())
143+ if query == "" {
144+ m .list .SetItems (m .allItems )
145+ return
146+ }
147+ var filtered []list.Item
148+ for _ , item := range m .allItems {
149+ ti := item .(toolDenyItem )
150+ if strings .Contains (strings .ToLower (ti .toolName ), query ) {
151+ filtered = append (filtered , item )
152+ }
153+ }
154+ m .list .SetItems (filtered )
125155}
126156
127157// Update handles messages.
@@ -130,28 +160,22 @@ func (m *ToolDenyEditorModel) Update(msg tea.Msg) tea.Cmd {
130160 return nil
131161 }
132162
133- // When filtering is active, let the list handle most keys
134- if m . list . FilterState () == list . Filtering {
163+ kmsg , isKey := msg .(tea. KeyMsg )
164+ if ! isKey {
135165 var cmd tea.Cmd
136166 m .list , cmd = m .list .Update (msg )
137167 return cmd
138168 }
139169
140- switch msg := msg .(type ) {
141- case tea.KeyMsg :
170+ if m .filterFocused {
142171 switch {
143- case key .Matches (msg , m .escKey ):
144- if m .list .FilterState () == list .FilterApplied {
145- m .list .ResetFilter ()
146- return nil
147- }
148- m .visible = false
149- return func () tea.Msg {
150- return ToolDenyResult {ServerName : m .serverName , Submitted : false }
151- }
152- case key .Matches (msg , m .enterKey ):
172+ case key .Matches (kmsg , m .escKey ):
173+ // Exit filter mode, keep filter text
174+ m .filterFocused = false
175+ m .filterInput .Blur ()
176+ return nil
177+ case key .Matches (kmsg , m .enterKey ):
153178 m .visible = false
154- // Collect denied tools
155179 var denied []string
156180 for toolName , isDenied := range m .denied {
157181 if isDenied {
@@ -165,16 +189,66 @@ func (m *ToolDenyEditorModel) Update(msg tea.Msg) tea.Cmd {
165189 Submitted : true ,
166190 }
167191 }
168- case key .Matches (msg , m .spaceKey ):
192+ case key .Matches (kmsg , m .spaceKey ):
169193 if item := m .list .SelectedItem (); item != nil {
170194 ti := item .(toolDenyItem )
171195 m .denied [ti .toolName ] = ! m .denied [ti .toolName ]
172196 m .list .SetDelegate (newToolDenyDelegate (m .theme , m .denied ))
173197 }
174198 return nil
199+ case kmsg .Type == tea .KeyUp || kmsg .Type == tea .KeyDown :
200+ var cmd tea.Cmd
201+ m .list , cmd = m .list .Update (msg )
202+ return cmd
203+ default :
204+ // Send to textinput, then apply filter
205+ var cmd tea.Cmd
206+ m .filterInput , cmd = m .filterInput .Update (msg )
207+ m .applyFilter ()
208+ return cmd
175209 }
176210 }
177211
212+ // Action mode
213+ switch {
214+ case kmsg .Type == tea .KeyRunes && string (kmsg .Runes ) == "/" :
215+ m .filterFocused = true
216+ m .filterInput .Focus ()
217+ return nil
218+ case key .Matches (kmsg , m .escKey ):
219+ if m .filterInput .Value () != "" {
220+ m .filterInput .SetValue ("" )
221+ m .applyFilter ()
222+ return nil
223+ }
224+ m .visible = false
225+ return func () tea.Msg {
226+ return ToolDenyResult {ServerName : m .serverName , Submitted : false }
227+ }
228+ case key .Matches (kmsg , m .enterKey ):
229+ m .visible = false
230+ var denied []string
231+ for toolName , isDenied := range m .denied {
232+ if isDenied {
233+ denied = append (denied , toolName )
234+ }
235+ }
236+ return func () tea.Msg {
237+ return ToolDenyResult {
238+ ServerName : m .serverName ,
239+ DeniedTools : denied ,
240+ Submitted : true ,
241+ }
242+ }
243+ case key .Matches (kmsg , m .spaceKey ):
244+ if item := m .list .SelectedItem (); item != nil {
245+ ti := item .(toolDenyItem )
246+ m .denied [ti .toolName ] = ! m .denied [ti .toolName ]
247+ m .list .SetDelegate (newToolDenyDelegate (m .theme , m .denied ))
248+ }
249+ return nil
250+ }
251+
178252 var cmd tea.Cmd
179253 m .list , cmd = m .list .Update (msg )
180254 return cmd
@@ -185,7 +259,17 @@ func (m ToolDenyEditorModel) View() string {
185259 if ! m .visible {
186260 return ""
187261 }
188- return m .list .View ()
262+
263+ filterLabel := m .theme .Faint .Render ("Filter: " )
264+ filterView := m .filterInput .View ()
265+ filterBar := filterLabel + filterView
266+
267+ listView := m .list .View ()
268+ if len (m .list .Items ()) == 0 && m .filterInput .Value () != "" {
269+ listView = "\n " + m .theme .Faint .Render (" No matching tools" ) + "\n "
270+ }
271+
272+ return filterBar + "\n \n " + listView
189273}
190274
191275// RenderOverlay renders the editor as a centered overlay.
0 commit comments