on main help sign in
improve status message handling
commitf463ce6694143a8f17953ca029d4c274d0193edd authorKujtim Hoxha <kujtimii.h@gmail.com> dateMon 7 Apr 2025, 19:43 (2 years ago) parent57a2210
10 files changed+140−84
cmd/root.go+10 internal/app/services.go+4 internal/llm/agent/agent.go+18−1 internal/llm/provider/anthropic.go+2−3 internal/llm/provider/provider.go+5 internal/tui/components/core/status.go+18−24 internal/tui/components/dialog/permission.go+48−48 internal/tui/components/repl/editor.go+1−1 internal/tui/tui.go+4−4 internal/tui/util/util.go+30−3
cmd/root.go+10
@@ -107,6 +107,16 @@ func setupSubscriptions(app *app.App) (chan tea.Msg, func()) {
107107 wg.Done()
108108 }()
109109 }
110+ {
111+ sub := app.Status.Subscribe(ctx)
112+ wg.Add(1)
113+ go func() {
114+ for ev := range sub {
115+ ch <- ev
116+ }
117+ wg.Done()
118+ }()
119+ }
110120 return ch, func() {
111121 cancel()
112122 wg.Wait()
internal/app/services.go+4
@@ -11,7 +11,9 @@ import (
1111 "github.com/kujtimiihoxha/termai/internal/lsp/watcher"
1212 "github.com/kujtimiihoxha/termai/internal/message"
1313 "github.com/kujtimiihoxha/termai/internal/permission"
14+ "github.com/kujtimiihoxha/termai/internal/pubsub"
1415 "github.com/kujtimiihoxha/termai/internal/session"
16+ "github.com/kujtimiihoxha/termai/internal/tui/util"
1517)
1618
1719type App struct {
@@ -25,6 +27,7 @@ type App struct {
2527
2628 Logger logging.Interface
2729
30+ Status *pubsub.Broker[util.InfoMsg]
2831 ceanups []func()
2932}
3033
@@ -43,6 +46,7 @@ func New(ctx context.Context, conn *sql.DB) *App {
4346 Messages: messages,
4447 Permissions: permission.Default,
4548 Logger: log,
49+ Status: pubsub.NewBroker[util.InfoMsg](),
4650 LSPClients: make(map[string]*lsp.Client),
4751 }
4852
internal/llm/agent/agent.go+18−1
@@ -15,6 +15,8 @@ import (
1515 "github.com/kujtimiihoxha/termai/internal/llm/provider"
1616 "github.com/kujtimiihoxha/termai/internal/llm/tools"
1717 "github.com/kujtimiihoxha/termai/internal/message"
18+ "github.com/kujtimiihoxha/termai/internal/pubsub"
19+ "github.com/kujtimiihoxha/termai/internal/tui/util"
1820)
1921
2022type Agent interface {
@@ -92,9 +94,24 @@ func (c *agent) processEvent(
9294 assistantMsg.AppendContent(event.Content)
9395 return c.Messages.Update(*assistantMsg)
9496 case provider.EventError:
97+ // TODO: remove when realease
9598 log.Println("error", event.Error)
99+ c.App.Status.Publish(pubsub.UpdatedEvent, util.InfoMsg{
100+ Type: util.InfoTypeError,
101+ Msg: event.Error.Error(),
102+ })
96103 return event.Error
97−
104+ case provider.EventWarning:
105+ c.App.Status.Publish(pubsub.UpdatedEvent, util.InfoMsg{
106+ Type: util.InfoTypeWarn,
107+ Msg: event.Info,
108+ })
109+ return nil
110+ case provider.EventInfo:
111+ c.App.Status.Publish(pubsub.UpdatedEvent, util.InfoMsg{
112+ Type: util.InfoTypeInfo,
113+ Msg: event.Info,
114+ })
98115 case provider.EventComplete:
99116 assistantMsg.SetToolCalls(event.Response.ToolCalls)
100117 assistantMsg.AddFinish(event.Response.FinishReason)
internal/llm/provider/anthropic.go+2−3
@@ -149,8 +149,8 @@ func (a *anthropicProvider) StreamResponse(ctx context.Context, messages []messa
149149
150150 // Inform user we're retrying with attempt number
151151 eventChan <- ProviderEvent{
152− Type: EventContentDelta,
153− Content: fmt.Sprintf("\n\n[Retrying due to rate limit... attempt %d of %d]\n\n", attempts, maxRetries),
152+ Type: EventWarning,
153+ Info: fmt.Sprintf("[Retrying due to rate limit... attempt %d of %d]", attempts, maxRetries),
154154 }
155155
156156 // Calculate backoff with exponential backoff and jitter
@@ -380,4 +380,3 @@ func (a *anthropicProvider) convertToAnthropicMessages(messages []message.Messag
380380
381381 return anthropicMessages
382382}
383−
internal/llm/provider/provider.go+5
@@ -17,6 +17,8 @@ const (
1717 EventContentStop EventType = "content_stop"
1818 EventComplete EventType = "complete"
1919 EventError EventType = "error"
20+ EventWarning EventType = "warning"
21+ EventInfo EventType = "info"
2022)
2123
2224type TokenUsage struct {
@@ -40,6 +42,9 @@ type ProviderEvent struct {
4042 ToolCall *message.ToolCall
4143 Error error
4244 Response *ProviderResponse
45+
46+ // Used for giving users info on e.x retry
47+ Info string
4348}
4449
4550type Provider interface {
internal/tui/components/core/status.go+18−24
@@ -13,10 +13,9 @@ import (
1313)
1414
1515type statusCmp struct {
16− err error
17− info string
18− width int
19− messageTTL time.Duration
16+ info *util.InfoMsg
17+ width int
18+ messageTTL time.Duration
2019}
2120
2221// clearMessageCmd is a command that clears status messages after a timeout
@@ -34,17 +33,12 @@ func (m statusCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
3433 switch msg := msg.(type) {
3534 case tea.WindowSizeMsg:
3635 m.width = msg.Width
37− case util.ErrorMsg:
38− m.err = msg
39− m.info = ""
4036 return m, m.clearMessageCmd()
4137 case util.InfoMsg:
42− m.info = string(msg)
43− m.err = nil
38+ m.info = &msg
4439 return m, m.clearMessageCmd()
4540 case util.ClearStatusMsg:
46− m.info = ""
47− m.err = nil
41+ m.info = nil
4842 }
4943 return m, nil
5044}
@@ -56,25 +50,25 @@ var (
5650
5751func (m statusCmp) View() string {
5852 status := styles.Padded.Background(styles.Grey).Foreground(styles.Text).Render("? help")
59−
60− if m.err != nil {
61− status += styles.Regular.Padding(0, 1).
62− Background(styles.Red).
63− Foreground(styles.Text).
64− Width(m.availableFooterMsgWidth()).
65− Render(m.err.Error())
66− } else if m.info != "" {
67− status += styles.Padded.
53+ if m.info != nil {
54+ infoStyle := styles.Padded.
6855 Foreground(styles.Base).
69− Background(styles.Green).
70− Width(m.availableFooterMsgWidth()).
71− Render(m.info)
56+ Width(m.availableFooterMsgWidth())
57+ switch m.info.Type {
58+ case util.InfoTypeInfo:
59+ infoStyle = infoStyle.Background(styles.Blue)
60+ case util.InfoTypeWarn:
61+ infoStyle = infoStyle.Background(styles.Peach)
62+ case util.InfoTypeError:
63+ infoStyle = infoStyle.Background(styles.Red)
64+ }
65+ status += infoStyle.Render(m.info.Msg)
7266 } else {
7367 status += styles.Padded.
7468 Foreground(styles.Base).
7569 Background(styles.LightGrey).
7670 Width(m.availableFooterMsgWidth()).
77− Render(m.info)
71+ Render("")
7872 }
7973 status += m.model()
8074 status += versionWidget
internal/tui/components/dialog/permission.go+48−48
@@ -69,13 +69,13 @@ type permissionDialogCmp struct {
6969func formatDiff(diffText string) string {
7070 lines := strings.Split(diffText, "\n")
7171 var formattedLines []string
72−
72+
7373 // Define styles for different line types
7474 addStyle := lipgloss.NewStyle().Foreground(styles.Green)
7575 removeStyle := lipgloss.NewStyle().Foreground(styles.Red)
7676 headerStyle := lipgloss.NewStyle().Bold(true).Foreground(styles.Blue)
7777 contextStyle := lipgloss.NewStyle().Foreground(styles.SubText0)
78−
78+
7979 // Process each line
8080 for _, line := range lines {
8181 if strings.HasPrefix(line, "+") {
@@ -90,7 +90,7 @@ func formatDiff(diffText string) string {
9090 formattedLines = append(formattedLines, line)
9191 }
9292 }
93−
93+
9494 // Join all formatted lines
9595 return strings.Join(formattedLines, "\n")
9696}
@@ -112,13 +112,13 @@ func (p *permissionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
112112 p.selectOption.Blur()
113113 // Add a visual indicator for focus change
114114 cmds = append(cmds, tea.Batch(
115− util.CmdHandler(util.InfoMsg("Viewing content - use arrow keys to scroll")),
115+ util.ReportInfo("Viewing content - use arrow keys to scroll"),
116116 ))
117117 } else {
118118 p.selectOption.Focus()
119119 // Add a visual indicator for focus change
120120 cmds = append(cmds, tea.Batch(
121− util.CmdHandler(util.InfoMsg("Select an action")),
121+ util.CmdHandler(util.ReportInfo("Select an action")),
122122 ))
123123 }
124124 return p, tea.Batch(cmds...)
@@ -162,44 +162,44 @@ func (p *permissionDialogCmp) render() string {
162162 lipgloss.JoinHorizontal(lipgloss.Left, keyStyle.Render("Path:"), " ", valueStyle.Render(p.permission.Path)),
163163 " ",
164164 }
165−
165+
166166 // Create the header content first so it can be used in all cases
167167 headerContent := lipgloss.NewStyle().Padding(0, 1).Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...))
168−
168+
169169 r, _ := glamour.NewTermRenderer(
170170 glamour.WithStyles(styles.CatppuccinMarkdownStyle()),
171171 glamour.WithWordWrap(p.width-10),
172172 glamour.WithEmoji(),
173173 )
174−
174+
175175 // Handle different tool types
176176 switch p.permission.ToolName {
177177 case tools.BashToolName:
178178 pr := p.permission.Params.(tools.BashPermissionsParams)
179179 headerParts = append(headerParts, keyStyle.Render("Command:"))
180180 content := fmt.Sprintf("```bash\n%s\n```", pr.Command)
181−
181+
182182 renderedContent, _ := r.Render(content)
183183 p.contentViewPort.Width = p.width - 2 - 2
184−
184+
185185 // Calculate content height dynamically based on content
186186 contentLines := len(strings.Split(renderedContent, "\n"))
187187 // Set a reasonable min/max for the viewport height
188188 minContentHeight := 3
189189 maxContentHeight := p.height - lipgloss.Height(headerContent) - lipgloss.Height(form) - 2 - 2 - 1
190−
190+
191191 // Add some padding to the content lines
192192 contentHeight := contentLines + 2
193193 contentHeight = max(contentHeight, minContentHeight)
194194 contentHeight = min(contentHeight, maxContentHeight)
195195 p.contentViewPort.Height = contentHeight
196−
196+
197197 p.contentViewPort.SetContent(renderedContent)
198−
198+
199199 // Style the viewport
200200 var contentBorder lipgloss.Border
201201 var borderColor lipgloss.TerminalColor
202−
202+
203203 if p.isViewportFocus {
204204 contentBorder = lipgloss.DoubleBorder()
205205 borderColor = styles.Blue
@@ -207,47 +207,47 @@ func (p *permissionDialogCmp) render() string {
207207 contentBorder = lipgloss.RoundedBorder()
208208 borderColor = styles.Flamingo
209209 }
210−
210+
211211 contentStyle := lipgloss.NewStyle().
212212 MarginTop(1).
213213 Padding(0, 1).
214214 Border(contentBorder).
215215 BorderForeground(borderColor)
216−
216+
217217 if p.isViewportFocus {
218218 contentStyle = contentStyle.BorderBackground(styles.Surface0)
219219 }
220−
220+
221221 contentFinal := contentStyle.Render(p.contentViewPort.View())
222−
222+
223223 return lipgloss.JoinVertical(
224224 lipgloss.Top,
225225 headerContent,
226226 contentFinal,
227227 form,
228228 )
229−
229+
230230 case tools.EditToolName:
231231 pr := p.permission.Params.(tools.EditPermissionsParams)
232232 headerParts = append(headerParts, keyStyle.Render("Update"))
233233 // Recreate header content with the updated headerParts
234234 headerContent = lipgloss.NewStyle().Padding(0, 1).Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...))
235−
235+
236236 // Format the diff with colors
237237 formattedDiff := formatDiff(pr.Diff)
238−
238+
239239 // Set up viewport for the diff content
240240 p.contentViewPort.Width = p.width - 2 - 2
241−
241+
242242 // Calculate content height dynamically based on window size
243243 maxContentHeight := p.height - lipgloss.Height(headerContent) - lipgloss.Height(form) - 2 - 2 - 1
244244 p.contentViewPort.Height = maxContentHeight
245245 p.contentViewPort.SetContent(formattedDiff)
246−
246+
247247 // Style the viewport
248248 var contentBorder lipgloss.Border
249249 var borderColor lipgloss.TerminalColor
250−
250+
251251 if p.isViewportFocus {
252252 contentBorder = lipgloss.DoubleBorder()
253253 borderColor = styles.Blue
@@ -255,47 +255,47 @@ func (p *permissionDialogCmp) render() string {
255255 contentBorder = lipgloss.RoundedBorder()
256256 borderColor = styles.Flamingo
257257 }
258−
258+
259259 contentStyle := lipgloss.NewStyle().
260260 MarginTop(1).
261261 Padding(0, 1).
262262 Border(contentBorder).
263263 BorderForeground(borderColor)
264−
264+
265265 if p.isViewportFocus {
266266 contentStyle = contentStyle.BorderBackground(styles.Surface0)
267267 }
268−
268+
269269 contentFinal := contentStyle.Render(p.contentViewPort.View())
270−
270+
271271 return lipgloss.JoinVertical(
272272 lipgloss.Top,
273273 headerContent,
274274 contentFinal,
275275 form,
276276 )
277−
277+
278278 case tools.WriteToolName:
279279 pr := p.permission.Params.(tools.WritePermissionsParams)
280280 headerParts = append(headerParts, keyStyle.Render("Content"))
281281 // Recreate header content with the updated headerParts
282282 headerContent = lipgloss.NewStyle().Padding(0, 1).Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...))
283−
283+
284284 // Format the diff with colors
285285 formattedDiff := formatDiff(pr.Content)
286−
286+
287287 // Set up viewport for the content
288288 p.contentViewPort.Width = p.width - 2 - 2
289−
289+
290290 // Calculate content height dynamically based on window size
291291 maxContentHeight := p.height - lipgloss.Height(headerContent) - lipgloss.Height(form) - 2 - 2 - 1
292292 p.contentViewPort.Height = maxContentHeight
293293 p.contentViewPort.SetContent(formattedDiff)
294−
294+
295295 // Style the viewport
296296 var contentBorder lipgloss.Border
297297 var borderColor lipgloss.TerminalColor
298−
298+
299299 if p.isViewportFocus {
300300 contentBorder = lipgloss.DoubleBorder()
301301 borderColor = styles.Blue
@@ -303,75 +303,75 @@ func (p *permissionDialogCmp) render() string {
303303 contentBorder = lipgloss.RoundedBorder()
304304 borderColor = styles.Flamingo
305305 }
306−
306+
307307 contentStyle := lipgloss.NewStyle().
308308 MarginTop(1).
309309 Padding(0, 1).
310310 Border(contentBorder).
311311 BorderForeground(borderColor)
312−
312+
313313 if p.isViewportFocus {
314314 contentStyle = contentStyle.BorderBackground(styles.Surface0)
315315 }
316−
316+
317317 contentFinal := contentStyle.Render(p.contentViewPort.View())
318−
318+
319319 return lipgloss.JoinVertical(
320320 lipgloss.Top,
321321 headerContent,
322322 contentFinal,
323323 form,
324324 )
325−
325+
326326 case tools.FetchToolName:
327327 pr := p.permission.Params.(tools.FetchPermissionsParams)
328328 headerParts = append(headerParts, keyStyle.Render("URL: "+pr.URL))
329329 content := p.permission.Description
330−
330+
331331 renderedContent, _ := r.Render(content)
332332 p.contentViewPort.Width = p.width - 2 - 2
333333 p.contentViewPort.Height = p.height - lipgloss.Height(headerContent) - lipgloss.Height(form) - 2 - 2 - 1
334334 p.contentViewPort.SetContent(renderedContent)
335−
335+
336336 // Style the viewport
337337 contentStyle := lipgloss.NewStyle().
338338 MarginTop(1).
339339 Padding(0, 1).
340340 Border(lipgloss.RoundedBorder()).
341341 BorderForeground(styles.Flamingo)
342−
342+
343343 contentFinal := contentStyle.Render(p.contentViewPort.View())
344344 if renderedContent == "" {
345345 contentFinal = ""
346346 }
347−
347+
348348 return lipgloss.JoinVertical(
349349 lipgloss.Top,
350350 headerContent,
351351 contentFinal,
352352 form,
353353 )
354−
354+
355355 default:
356356 content := p.permission.Description
357−
357+
358358 renderedContent, _ := r.Render(content)
359359 p.contentViewPort.Width = p.width - 2 - 2
360360 p.contentViewPort.Height = p.height - lipgloss.Height(headerContent) - lipgloss.Height(form) - 2 - 2 - 1
361361 p.contentViewPort.SetContent(renderedContent)
362−
362+
363363 // Style the viewport
364364 contentStyle := lipgloss.NewStyle().
365365 MarginTop(1).
366366 Padding(0, 1).
367367 Border(lipgloss.RoundedBorder()).
368368 BorderForeground(styles.Flamingo)
369−
369+
370370 contentFinal := contentStyle.Render(p.contentViewPort.View())
371371 if renderedContent == "" {
372372 contentFinal = ""
373373 }
374−
374+
375375 return lipgloss.JoinVertical(
376376 lipgloss.Top,
377377 headerContent,
internal/tui/components/repl/editor.go+1−1
@@ -140,7 +140,7 @@ func (m *editorCmp) Send() tea.Cmd {
140140 return func() tea.Msg {
141141 messages, _ := m.app.Messages.List(m.sessionID)
142142 if hasUnfinishedMessages(messages) {
143− return util.InfoMsg("Assistant is still working on the previous message")
143+ return util.ReportWarn("Assistant is still working on the previous message")
144144 }
145145 a, _ := agent.NewCoderAgent(m.app)
146146
internal/tui/tui.go+4−4
@@ -77,6 +77,8 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
7777 switch msg := msg.(type) {
7878 case pubsub.Event[permission.PermissionRequest]:
7979 return a, dialog.NewPermissionDialogCmd(msg.Payload)
80+ case pubsub.Event[util.InfoMsg]:
81+ a.status, _ = a.status.Update(msg)
8082 case dialog.PermissionResponseMsg:
8183 switch msg.Action {
8284 case dialog.PermissionAllow:
@@ -121,8 +123,6 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
121123 return a, a.moveToPage(msg.ID)
122124 case util.InfoMsg:
123125 a.status, _ = a.status.Update(msg)
124− case util.ErrorMsg:
125− a.status, _ = a.status.Update(msg)
126126 case tea.KeyMsg:
127127 if a.editorMode == vimtea.ModeNormal {
128128 switch {
@@ -141,7 +141,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
141141 if a.currentPage == page.ReplPage {
142142 sessions, err := a.app.Sessions.List()
143143 if err != nil {
144− return a, util.CmdHandler(util.ErrorMsg(err))
144+ return a, util.CmdHandler(util.ReportError(err))
145145 }
146146 lastSession := sessions[0]
147147 if lastSession.MessageCount == 0 {
@@ -149,7 +149,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
149149 }
150150 s, err := a.app.Sessions.Create("New Session")
151151 if err != nil {
152− return a, util.CmdHandler(util.ErrorMsg(err))
152+ return a, util.CmdHandler(util.ReportError(err))
153153 }
154154 return a, util.CmdHandler(repl.SelectedSessionMsg{SessionID: s.ID})
155155 }
internal/tui/util/util.go+30−3
@@ -9,12 +9,39 @@ func CmdHandler(msg tea.Msg) tea.Cmd {
99}
1010
1111func ReportError(err error) tea.Cmd {
12− return CmdHandler(ErrorMsg(err))
12+ return CmdHandler(InfoMsg{
13+ Type: InfoTypeError,
14+ Msg: err.Error(),
15+ })
16+}
17+
18+type InfoType int
19+
20+const (
21+ InfoTypeInfo InfoType = iota
22+ InfoTypeWarn
23+ InfoTypeError
24+)
25+
26+func ReportInfo(info string) tea.Cmd {
27+ return CmdHandler(InfoMsg{
28+ Type: InfoTypeInfo,
29+ Msg: info,
30+ })
31+}
32+
33+func ReportWarn(warn string) tea.Cmd {
34+ return CmdHandler(InfoMsg{
35+ Type: InfoTypeWarn,
36+ Msg: warn,
37+ })
1338}
1439
1540type (
16− InfoMsg string
17− ErrorMsg error
41+ InfoMsg struct {
42+ Type InfoType
43+ Msg string
44+ }
1845 ClearStatusMsg struct{}
1946)
2047