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 }