update.go
  1  package main
  2  
  3  import (
  4  	"fmt"
  5  	"strings"
  6  
  7  	"github.com/charmbracelet/bubbles/textarea"
  8  	tea "github.com/charmbracelet/bubbletea"
  9  )
 10  
 11  // Init is called once when the program starts
 12  func (m model) Init() tea.Cmd {
 13  	return tea.Batch(
 14  		textarea.Blink,
 15  		pollStatus(),          // Get initial status
 16  		startStatusPolling(),  // Start periodic updates
 17  	)
 18  }
 19  
 20  // Update handles all events (keyboard, messages, etc.)
 21  func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 22  	var (
 23  		tiCmd tea.Cmd
 24  		vpCmd tea.Cmd
 25  	)
 26  
 27  	// Handle the message type
 28  	switch msg := msg.(type) {
 29  
 30  	case tea.WindowSizeMsg:
 31  		// Update window dimensions
 32  		m.width = msg.Width
 33  		m.height = msg.Height
 34  
 35  		if !m.ready {
 36  			// Initialize viewport with correct size
 37  			headerHeight := 1
 38  			footerHeight := 3 + 3 // status bars + input area
 39  			m.viewport.Width = msg.Width
 40  			m.viewport.Height = msg.Height - headerHeight - footerHeight
 41  
 42  			m.textarea.SetWidth(msg.Width)
 43  			m.ready = true
 44  		} else {
 45  			// Update sizes
 46  			headerHeight := 1
 47  			footerHeight := 3 + 3
 48  			m.viewport.Width = msg.Width
 49  			m.viewport.Height = msg.Height - headerHeight - footerHeight
 50  
 51  			m.textarea.SetWidth(msg.Width)
 52  		}
 53  
 54  	case tea.KeyMsg:
 55  		switch msg.Type {
 56  		case tea.KeyCtrlC, tea.KeyEsc:
 57  			// Quit the program
 58  			return m, tea.Quit
 59  		
 60  		case tea.KeyCtrlL:
 61  			// Clear screen
 62  			m.messages = []Message{}
 63  			m.viewport.SetContent(m.renderMessages())
 64  			m.viewport.GotoTop()
 65  			return m, nil
 66  
 67  		case tea.KeyEnter:
 68  			// Send the message
 69  			if !m.loading && m.textarea.Value() != "" {
 70  				userMessage := strings.TrimSpace(m.textarea.Value())
 71  
 72  				// Add user message to history
 73  				m.messages = append(m.messages, Message{
 74  					Role:    "user",
 75  					Content: userMessage,
 76  				})
 77  
 78  				// Update viewport with user message
 79  				m.viewport.SetContent(m.renderMessages())
 80  				m.viewport.GotoBottom()
 81  
 82  				// Clear input
 83  				m.textarea.Reset()
 84  
 85  				// Mark as loading
 86  				m.loading = true
 87  				m.spinnerFrame = 0
 88  
 89  				// TODO: Send to backend
 90  				// For now, just echo back
 91  				return m, tea.Batch(sendToPythonBackend(userMessage), tick())
 92  			}
 93  
 94  		case tea.KeyPgUp:
 95  			// Scroll up
 96  			m.viewport, vpCmd = m.viewport.Update(msg)
 97  			return m, vpCmd
 98  
 99  		case tea.KeyPgDown:
100  			// Scroll down
101  			m.viewport, vpCmd = m.viewport.Update(msg)
102  			return m, vpCmd
103  		}
104  
105  	case backendResponseMsg:
106  		// Received response from backend
107  		m.loading = false
108  		m.messages = append(m.messages, Message{
109  			Role:    "assistant",
110  			Content: string(msg),
111  		})
112  		m.viewport.SetContent(m.renderMessages())
113  		m.viewport.GotoBottom()
114  		return m, nil
115  
116  	case errMsg:
117  		// Error occurred - display styled error
118  		m.err = msg
119  		m.loading = false
120  		m.messages = append(m.messages, Message{
121  			Role:    "system",
122  			Content: StyledErrorMessage(msg),
123  		})
124  		m.viewport.SetContent(m.renderMessages())
125  		m.viewport.GotoBottom()
126  		return m, nil
127  
128  	case tickMsg:
129  		// Animation tick for spinner
130  		if m.loading {
131  			m.spinnerFrame++
132  			return m, tick()
133  		}
134  		return m, nil
135  
136  	case statusUpdateMsg:
137  		// Status update received - update model
138  		m.provider = msg.Provider
139  		m.model = msg.Model
140  		m.shellCwd = msg.ShellCwd
141  		// Continue polling
142  		return m, startStatusPolling()
143  	}
144  
145  	// Update textarea
146  	m.textarea, tiCmd = m.textarea.Update(msg)
147  
148  	return m, tea.Batch(tiCmd, vpCmd)
149  }
150  
151  // renderMessages converts message history to a string for display
152  func (m model) renderMessages() string {
153  	var sb strings.Builder
154  
155  	// Add welcome message
156  	sb.WriteString(welcomeMessage())
157  	sb.WriteString("\n\n")
158  
159  	// Add all messages with styled rendering
160  	for _, msg := range m.messages {
161  		switch msg.Role {
162  		case "user":
163  			sb.WriteString(StyledUserMessage(msg.Content))
164  			sb.WriteString("\n")
165  
166  		case "assistant":
167  			sb.WriteString(StyledAssistantMessage(msg.Content))
168  			sb.WriteString("\n")
169  
170  		case "thinking":
171  			sb.WriteString(StyledThinkingPanel(msg.Content))
172  
173  		case "tool":
174  			// Show tool execution with timing
175  			sb.WriteString(StyledToolPanel(msg.ToolName, msg.Content, msg.Timing))
176  			
177  		case "system":
178  			// System messages (errors, etc)
179  			sb.WriteString(msg.Content)
180  			sb.WriteString("\n")
181  		}
182  	}
183  
184  	return sb.String()
185  }
186  
187  // formatDuration formats a duration in seconds to a human-readable string
188  func formatDuration(seconds float64) string {
189  	if seconds < 1 {
190  		return "< 1s"
191  	}
192  	return fmt.Sprintf("%.2fs", seconds)
193  }
194  
195  // handleAgentEvents processes agent events and adds them as messages
196  func (m *model) handleAgentEvents(events []AgentEvent) {
197  	for _, event := range events {
198  		var message Message
199  		
200  		switch event.Type {
201  		case "thinking":
202  			message = Message{
203  				Role:    "thinking",
204  				Content: event.Content,
205  				Timing:  event.Timing,
206  			}
207  		case "action":
208  			message = Message{
209  				Role:     "tool",
210  				ToolName: event.Tool,
211  				Content:  "Starting: " + event.Input,
212  			}
213  		case "observation":
214  			message = Message{
215  				Role:     "tool",
216  				ToolName: event.Tool,
217  				Content:  event.Content,
218  				Timing:   event.Timing,
219  			}
220  		case "final":
221  			message = Message{
222  				Role:    "assistant",
223  				Content: event.Content,
224  			}
225  		}
226  		
227  		m.messages = append(m.messages, message)
228  	}
229  }