@@ -82,12 +82,20 @@ type errMsg struct {
8282}
8383
8484func Run (ctx context.Context , cfg * config.Config , agents []agent.Agent ) error {
85+ searchInput := textinput .New ()
86+ searchInput .Placeholder = "Search messages..."
87+ searchInput .CharLimit = 100
88+
8589 m := Model {
86- ctx : ctx ,
87- config : cfg ,
88- agents : agents ,
89- messages : make ([]agent.Message , 0 ),
90- running : false ,
90+ ctx : ctx ,
91+ config : cfg ,
92+ agents : agents ,
93+ messages : make ([]agent.Message , 0 ),
94+ running : false ,
95+ searchInput : searchInput ,
96+ searchMode : false ,
97+ searchResults : make ([]int , 0 ),
98+ currentSearchIndex : - 1 ,
9199 }
92100
93101 p := tea .NewProgram (m , tea .WithAltScreen ())
@@ -107,9 +115,63 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
107115
108116 switch msg := msg .(type ) {
109117 case tea.KeyMsg :
118+ // Handle search mode keys
119+ if m .searchMode {
120+ switch msg .Type {
121+ case tea .KeyEsc :
122+ // Exit search mode
123+ m .searchMode = false
124+ m .searchInput .SetValue ("" )
125+ m .searchResults = make ([]int , 0 )
126+ m .currentSearchIndex = - 1
127+ return m , nil
128+ case tea .KeyEnter :
129+ // Perform search
130+ m .performSearch ()
131+ return m , nil
132+ default :
133+ // Handle other keys in search input
134+ switch msg .String () {
135+ case "n" :
136+ // Next search result
137+ if len (m .searchResults ) > 0 {
138+ m .currentSearchIndex = (m .currentSearchIndex + 1 ) % len (m .searchResults )
139+ m .scrollToSearchResult ()
140+ }
141+ return m , nil
142+ case "N" :
143+ // Previous search result
144+ if len (m .searchResults ) > 0 {
145+ m .currentSearchIndex --
146+ if m .currentSearchIndex < 0 {
147+ m .currentSearchIndex = len (m .searchResults ) - 1
148+ }
149+ m .scrollToSearchResult ()
150+ }
151+ return m , nil
152+ default :
153+ // Update search input
154+ var cmd tea.Cmd
155+ m .searchInput , cmd = m .searchInput .Update (msg )
156+ return m , cmd
157+ }
158+ }
159+ }
160+
161+ // Handle normal mode keys
110162 switch msg .Type {
111- case tea .KeyCtrlC , tea . KeyEsc :
163+ case tea .KeyCtrlC :
112164 return m , tea .Quit
165+ case tea .KeyEsc :
166+ return m , tea .Quit
167+ case tea .KeyCtrlF :
168+ // Enter search mode (only if ready)
169+ if m .ready {
170+ m .searchMode = true
171+ // Don't call Focus() to avoid cursor initialization issues in tests
172+ // The searchMode flag will route events to searchInput
173+ return m , nil
174+ }
113175 case tea .KeyCtrlS :
114176 if ! m .running {
115177 m .running = true
@@ -134,6 +196,22 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
134196 ta .SetHeight (3 )
135197 m .textarea = ta
136198
199+ // Initialize search input
200+ searchInput := textinput .New ()
201+ searchInput .Placeholder = "Search messages..."
202+ searchInput .CharLimit = 100
203+ // Initialize the internal cursor by updating with a dummy message
204+ searchInput , _ = searchInput .Update (nil )
205+ m .searchInput = searchInput
206+
207+ // Initialize search state if not already set
208+ if m .searchResults == nil {
209+ m .searchResults = make ([]int , 0 )
210+ }
211+ if m .currentSearchIndex == 0 {
212+ m .currentSearchIndex = - 1
213+ }
214+
137215 m .ready = true
138216 } else {
139217 m .viewport .Width = msg .Width
@@ -188,9 +266,21 @@ func (m Model) View() string {
188266 b .WriteString (statusStyle .Render (status ))
189267 b .WriteString ("\n " )
190268
191- help := helpStyle .Render ("Ctrl+C: Quit | Ctrl+S: Start | Ctrl+P: Pause/Resume | ↑↓: Scroll" )
269+ help := helpStyle .Render ("Ctrl+C: Quit | Ctrl+S: Start | Ctrl+P: Pause/Resume | Ctrl+F: Search | ↑↓: Scroll" )
192270 b .WriteString (help )
193271
272+ // Show search bar when in search mode
273+ if m .searchMode {
274+ b .WriteString ("\n " )
275+ searchBar := searchStyle .Render ("Search: " ) + m .searchInput .View ()
276+ if len (m .searchResults ) > 0 {
277+ searchBar += fmt .Sprintf (" (%d/%d matches, n/N to navigate)" , m .currentSearchIndex + 1 , len (m .searchResults ))
278+ } else if m .searchInput .Value () != "" {
279+ searchBar += " (no matches)"
280+ }
281+ b .WriteString (searchBar )
282+ }
283+
194284 if m .err != nil {
195285 b .WriteString ("\n " )
196286 b .WriteString (lipgloss .NewStyle ().Foreground (lipgloss .Color ("196" )).Render (fmt .Sprintf ("Error: %v" , m .err )))
@@ -225,6 +315,64 @@ func (m Model) renderMessages() string {
225315 return b .String ()
226316}
227317
318+ // performSearch searches through messages for the search term
319+ func (m * Model ) performSearch () {
320+ searchTerm := strings .ToLower (m .searchInput .Value ())
321+ if searchTerm == "" {
322+ m .searchResults = make ([]int , 0 )
323+ m .currentSearchIndex = - 1
324+ return
325+ }
326+
327+ // Clear previous results
328+ m .searchResults = make ([]int , 0 )
329+
330+ // Search through all messages
331+ for i , msg := range m .messages {
332+ // Search in message content and agent name
333+ if strings .Contains (strings .ToLower (msg .Content ), searchTerm ) ||
334+ strings .Contains (strings .ToLower (msg .AgentName ), searchTerm ) {
335+ m .searchResults = append (m .searchResults , i )
336+ }
337+ }
338+
339+ // Set current index to first result if any found
340+ if len (m .searchResults ) > 0 {
341+ m .currentSearchIndex = 0
342+ m .scrollToSearchResult ()
343+ } else {
344+ m .currentSearchIndex = - 1
345+ }
346+ }
347+
348+ // scrollToSearchResult scrolls the viewport to show the current search result
349+ func (m * Model ) scrollToSearchResult () {
350+ if m .currentSearchIndex < 0 || m .currentSearchIndex >= len (m .searchResults ) {
351+ return
352+ }
353+
354+ // Get the message index
355+ msgIndex := m .searchResults [m .currentSearchIndex ]
356+
357+ // Calculate approximate line position
358+ // Each message takes roughly 4 lines (timestamp line + content + blank line + separator)
359+ linePos := msgIndex * 4
360+
361+ // Scroll viewport to show this message
362+ // Try to position it in the middle of the viewport
363+ targetLine := linePos - (m .viewport .Height / 2 )
364+ if targetLine < 0 {
365+ targetLine = 0
366+ }
367+
368+ // Calculate the percentage position
369+ totalLines := len (m .messages ) * 4
370+ if totalLines > 0 {
371+ percent := float64 (targetLine ) / float64 (totalLines )
372+ m .viewport .SetYOffset (int (percent * float64 (m .viewport .TotalLineCount ())))
373+ }
374+ }
375+
228376func (m Model ) startConversation () tea.Cmd {
229377 return func () tea.Msg {
230378 orchConfig := orchestrator.OrchestratorConfig {
0 commit comments