on main help sign in
improve status message handling
f463ce6Kujtim Hoxha
cmd/root.go+10
@@ -107,6 +107,16 @@ func setupSubscriptions(app *app.App) (chan tea.Msg, func()) {
107 wg.Done()
107 wg.Done()
108 }()
108 }()
109 }
109 }
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+ }
110 return ch, func() {
120 return ch, func() {
111 cancel()
121 cancel()
112 wg.Wait()
122 wg.Wait()
internal/app/services.go+4
@@ -11,7 +11,9 @@ import (
11 "github.com/kujtimiihoxha/termai/internal/lsp/watcher"
11 "github.com/kujtimiihoxha/termai/internal/lsp/watcher"
12 "github.com/kujtimiihoxha/termai/internal/message"
12 "github.com/kujtimiihoxha/termai/internal/message"
13 "github.com/kujtimiihoxha/termai/internal/permission"
13 "github.com/kujtimiihoxha/termai/internal/permission"
14+ "github.com/kujtimiihoxha/termai/internal/pubsub"
14 "github.com/kujtimiihoxha/termai/internal/session"
15 "github.com/kujtimiihoxha/termai/internal/session"
16+ "github.com/kujtimiihoxha/termai/internal/tui/util"
15)
17)
16
18
17type App struct {
19type App struct {
@@ -25,6 +27,7 @@ type App struct {
25
27
26 Logger logging.Interface
28 Logger logging.Interface
27
29
30+ Status *pubsub.Broker[util.InfoMsg]
28 ceanups []func()
31 ceanups []func()
29}
32}
30
33
@@ -43,6 +46,7 @@ func New(ctx context.Context, conn *sql.DB) *App {
43 Messages: messages,
46 Messages: messages,
44 Permissions: permission.Default,
47 Permissions: permission.Default,
45 Logger: log,
48 Logger: log,
49+ Status: pubsub.NewBroker[util.InfoMsg](),
46 LSPClients: make(map[string]*lsp.Client),
50 LSPClients: make(map[string]*lsp.Client),
47 }
51 }
48
52
internal/llm/agent/agent.go+18−1
@@ -15,6 +15,8 @@ import (
15 "github.com/kujtimiihoxha/termai/internal/llm/provider"
15 "github.com/kujtimiihoxha/termai/internal/llm/provider"
16 "github.com/kujtimiihoxha/termai/internal/llm/tools"
16 "github.com/kujtimiihoxha/termai/internal/llm/tools"
17 "github.com/kujtimiihoxha/termai/internal/message"
17 "github.com/kujtimiihoxha/termai/internal/message"
18+ "github.com/kujtimiihoxha/termai/internal/pubsub"
19+ "github.com/kujtimiihoxha/termai/internal/tui/util"
18)
20)
19
21
20type Agent interface {
22type Agent interface {
@@ -92,9 +94,24 @@ func (c *agent) processEvent(
92 assistantMsg.AppendContent(event.Content)
94 assistantMsg.AppendContent(event.Content)
93 return c.Messages.Update(*assistantMsg)
95 return c.Messages.Update(*assistantMsg)
94 case provider.EventError:
96 case provider.EventError:
97+ // TODO: remove when realease
95 log.Println("error", event.Error)
98 log.Println("error", event.Error)
99+ c.App.Status.Publish(pubsub.UpdatedEvent, util.InfoMsg{
100+ Type: util.InfoTypeError,
101+ Msg: event.Error.Error(),
102+ })
96 return event.Error
103 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+ })
98 case provider.EventComplete:
115 case provider.EventComplete:
99 assistantMsg.SetToolCalls(event.Response.ToolCalls)
116 assistantMsg.SetToolCalls(event.Response.ToolCalls)
100 assistantMsg.AddFinish(event.Response.FinishReason)
117 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
149
149
150 // Inform user we're retrying with attempt number
150 // Inform user we're retrying with attempt number
151 eventChan <- ProviderEvent{
151 eventChan <- ProviderEvent{
152− Type: EventContentDelta,
152+ Type: EventWarning,
153− Content: fmt.Sprintf("\n\n[Retrying due to rate limit... attempt %d of %d]\n\n", attempts, maxRetries),
153+ Info: fmt.Sprintf("[Retrying due to rate limit... attempt %d of %d]", attempts, maxRetries),
154 }
154 }
155
155
156 // Calculate backoff with exponential backoff and jitter
156 // Calculate backoff with exponential backoff and jitter
@@ -380,4 +380,3 @@ func (a *anthropicProvider) convertToAnthropicMessages(messages []message.Messag
380
380
381 return anthropicMessages
381 return anthropicMessages
382}
382}
383−
internal/llm/provider/provider.go+5
@@ -17,6 +17,8 @@ const (
17 EventContentStop EventType = "content_stop"
17 EventContentStop EventType = "content_stop"
18 EventComplete EventType = "complete"
18 EventComplete EventType = "complete"
19 EventError EventType = "error"
19 EventError EventType = "error"
20+ EventWarning EventType = "warning"
21+ EventInfo EventType = "info"
20)
22)
21
23
22type TokenUsage struct {
24type TokenUsage struct {
@@ -40,6 +42,9 @@ type ProviderEvent struct {
40 ToolCall *message.ToolCall
42 ToolCall *message.ToolCall
41 Error error
43 Error error
42 Response *ProviderResponse
44 Response *ProviderResponse
45+
46+ // Used for giving users info on e.x retry
47+ Info string
43}
48}
44
49
45type Provider interface {
50type Provider interface {
internal/tui/components/core/status.go+18−24
@@ -13,10 +13,9 @@ import (
13)
13)
14
14
15type statusCmp struct {
15type statusCmp struct {
16− err error
16+ info *util.InfoMsg
17− info string
18− width int
17+ width int
19− messageTTL time.Duration
18+ messageTTL time.Duration
20}
19}
21
20
22// clearMessageCmd is a command that clears status messages after a timeout
21// 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) {
34 switch msg := msg.(type) {
33 switch msg := msg.(type) {
35 case tea.WindowSizeMsg:
34 case tea.WindowSizeMsg:
36 m.width = msg.Width
35 m.width = msg.Width
37− case util.ErrorMsg:
38− m.err = msg
39− m.info = ""
40 return m, m.clearMessageCmd()
36 return m, m.clearMessageCmd()
41 case util.InfoMsg:
37 case util.InfoMsg:
42− m.info = string(msg)
38+ m.info = &msg
43− m.err = nil
44 return m, m.clearMessageCmd()
39 return m, m.clearMessageCmd()
45 case util.ClearStatusMsg:
40 case util.ClearStatusMsg:
46− m.info = ""
41+ m.info = nil
47− m.err = nil
48 }
42 }
49 return m, nil
43 return m, nil
50}
44}
@@ -56,25 +50,25 @@ var (
56
50
57func (m statusCmp) View() string {
51func (m statusCmp) View() string {
58 status := styles.Padded.Background(styles.Grey).Foreground(styles.Text).Render("? help")
52 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 != "" {
53+ if m.info != nil {
67− status += styles.Padded.
54+ infoStyle := styles.Padded.
68 Foreground(styles.Base).
55 Foreground(styles.Base).
69− Background(styles.Green).
70− Width(m.availableFooterMsgWidth()).
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+ }
71− Render(m.info)
65+ status += infoStyle.Render(m.info.Msg)
72 } else {
66 } else {
73 status += styles.Padded.
67 status += styles.Padded.
74 Foreground(styles.Base).
68 Foreground(styles.Base).
75 Background(styles.LightGrey).
69 Background(styles.LightGrey).
76 Width(m.availableFooterMsgWidth()).
70 Width(m.availableFooterMsgWidth()).
77− Render(m.info)
71+ Render("")
78 }
72 }
79 status += m.model()
73 status += m.model()
80 status += versionWidget
74 status += versionWidget
internal/tui/components/dialog/permission.go+48−48
@@ -69,13 +69,13 @@ type permissionDialogCmp struct {
69func formatDiff(diffText string) string {
69func formatDiff(diffText string) string {
70 lines := strings.Split(diffText, "\n")
70 lines := strings.Split(diffText, "\n")
71 var formattedLines []string
71 var formattedLines []string
72−
72+
73 // Define styles for different line types
73 // Define styles for different line types
74 addStyle := lipgloss.NewStyle().Foreground(styles.Green)
74 addStyle := lipgloss.NewStyle().Foreground(styles.Green)
75 removeStyle := lipgloss.NewStyle().Foreground(styles.Red)
75 removeStyle := lipgloss.NewStyle().Foreground(styles.Red)
76 headerStyle := lipgloss.NewStyle().Bold(true).Foreground(styles.Blue)
76 headerStyle := lipgloss.NewStyle().Bold(true).Foreground(styles.Blue)
77 contextStyle := lipgloss.NewStyle().Foreground(styles.SubText0)
77 contextStyle := lipgloss.NewStyle().Foreground(styles.SubText0)
78−
78+
79 // Process each line
79 // Process each line
80 for _, line := range lines {
80 for _, line := range lines {
81 if strings.HasPrefix(line, "+") {
81 if strings.HasPrefix(line, "+") {
@@ -90,7 +90,7 @@ func formatDiff(diffText string) string {
90 formattedLines = append(formattedLines, line)
90 formattedLines = append(formattedLines, line)
91 }
91 }
92 }
92 }
93−
93+
94 // Join all formatted lines
94 // Join all formatted lines
95 return strings.Join(formattedLines, "\n")
95 return strings.Join(formattedLines, "\n")
96}
96}
@@ -112,13 +112,13 @@ func (p *permissionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
112 p.selectOption.Blur()
112 p.selectOption.Blur()
113 // Add a visual indicator for focus change
113 // Add a visual indicator for focus change
114 cmds = append(cmds, tea.Batch(
114 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"),
116 ))
116 ))
117 } else {
117 } else {
118 p.selectOption.Focus()
118 p.selectOption.Focus()
119 // Add a visual indicator for focus change
119 // Add a visual indicator for focus change
120 cmds = append(cmds, tea.Batch(
120 cmds = append(cmds, tea.Batch(
121− util.CmdHandler(util.InfoMsg("Select an action")),
121+ util.CmdHandler(util.ReportInfo("Select an action")),
122 ))
122 ))
123 }
123 }
124 return p, tea.Batch(cmds...)
124 return p, tea.Batch(cmds...)
@@ -162,44 +162,44 @@ func (p *permissionDialogCmp) render() string {
162 lipgloss.JoinHorizontal(lipgloss.Left, keyStyle.Render("Path:"), " ", valueStyle.Render(p.permission.Path)),
162 lipgloss.JoinHorizontal(lipgloss.Left, keyStyle.Render("Path:"), " ", valueStyle.Render(p.permission.Path)),
163 " ",
163 " ",
164 }
164 }
165−
165+
166 // Create the header content first so it can be used in all cases
166 // Create the header content first so it can be used in all cases
167 headerContent := lipgloss.NewStyle().Padding(0, 1).Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...))
167 headerContent := lipgloss.NewStyle().Padding(0, 1).Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...))
168−
168+
169 r, _ := glamour.NewTermRenderer(
169 r, _ := glamour.NewTermRenderer(
170 glamour.WithStyles(styles.CatppuccinMarkdownStyle()),
170 glamour.WithStyles(styles.CatppuccinMarkdownStyle()),
171 glamour.WithWordWrap(p.width-10),
171 glamour.WithWordWrap(p.width-10),
172 glamour.WithEmoji(),
172 glamour.WithEmoji(),
173 )
173 )
174−
174+
175 // Handle different tool types
175 // Handle different tool types
176 switch p.permission.ToolName {
176 switch p.permission.ToolName {
177 case tools.BashToolName:
177 case tools.BashToolName:
178 pr := p.permission.Params.(tools.BashPermissionsParams)
178 pr := p.permission.Params.(tools.BashPermissionsParams)
179 headerParts = append(headerParts, keyStyle.Render("Command:"))
179 headerParts = append(headerParts, keyStyle.Render("Command:"))
180 content := fmt.Sprintf("```bash\n%s\n```", pr.Command)
180 content := fmt.Sprintf("```bash\n%s\n```", pr.Command)
181−
181+
182 renderedContent, _ := r.Render(content)
182 renderedContent, _ := r.Render(content)
183 p.contentViewPort.Width = p.width - 2 - 2
183 p.contentViewPort.Width = p.width - 2 - 2
184−
184+
185 // Calculate content height dynamically based on content
185 // Calculate content height dynamically based on content
186 contentLines := len(strings.Split(renderedContent, "\n"))
186 contentLines := len(strings.Split(renderedContent, "\n"))
187 // Set a reasonable min/max for the viewport height
187 // Set a reasonable min/max for the viewport height
188 minContentHeight := 3
188 minContentHeight := 3
189 maxContentHeight := p.height - lipgloss.Height(headerContent) - lipgloss.Height(form) - 2 - 2 - 1
189 maxContentHeight := p.height - lipgloss.Height(headerContent) - lipgloss.Height(form) - 2 - 2 - 1
190−
190+
191 // Add some padding to the content lines
191 // Add some padding to the content lines
192 contentHeight := contentLines + 2
192 contentHeight := contentLines + 2
193 contentHeight = max(contentHeight, minContentHeight)
193 contentHeight = max(contentHeight, minContentHeight)
194 contentHeight = min(contentHeight, maxContentHeight)
194 contentHeight = min(contentHeight, maxContentHeight)
195 p.contentViewPort.Height = contentHeight
195 p.contentViewPort.Height = contentHeight
196−
196+
197 p.contentViewPort.SetContent(renderedContent)
197 p.contentViewPort.SetContent(renderedContent)
198−
198+
199 // Style the viewport
199 // Style the viewport
200 var contentBorder lipgloss.Border
200 var contentBorder lipgloss.Border
201 var borderColor lipgloss.TerminalColor
201 var borderColor lipgloss.TerminalColor
202−
202+
203 if p.isViewportFocus {
203 if p.isViewportFocus {
204 contentBorder = lipgloss.DoubleBorder()
204 contentBorder = lipgloss.DoubleBorder()
205 borderColor = styles.Blue
205 borderColor = styles.Blue
@@ -207,47 +207,47 @@ func (p *permissionDialogCmp) render() string {
207 contentBorder = lipgloss.RoundedBorder()
207 contentBorder = lipgloss.RoundedBorder()
208 borderColor = styles.Flamingo
208 borderColor = styles.Flamingo
209 }
209 }
210−
210+
211 contentStyle := lipgloss.NewStyle().
211 contentStyle := lipgloss.NewStyle().
212 MarginTop(1).
212 MarginTop(1).
213 Padding(0, 1).
213 Padding(0, 1).
214 Border(contentBorder).
214 Border(contentBorder).
215 BorderForeground(borderColor)
215 BorderForeground(borderColor)
216−
216+
217 if p.isViewportFocus {
217 if p.isViewportFocus {
218 contentStyle = contentStyle.BorderBackground(styles.Surface0)
218 contentStyle = contentStyle.BorderBackground(styles.Surface0)
219 }
219 }
220−
220+
221 contentFinal := contentStyle.Render(p.contentViewPort.View())
221 contentFinal := contentStyle.Render(p.contentViewPort.View())
222−
222+
223 return lipgloss.JoinVertical(
223 return lipgloss.JoinVertical(
224 lipgloss.Top,
224 lipgloss.Top,
225 headerContent,
225 headerContent,
226 contentFinal,
226 contentFinal,
227 form,
227 form,
228 )
228 )
229−
229+
230 case tools.EditToolName:
230 case tools.EditToolName:
231 pr := p.permission.Params.(tools.EditPermissionsParams)
231 pr := p.permission.Params.(tools.EditPermissionsParams)
232 headerParts = append(headerParts, keyStyle.Render("Update"))
232 headerParts = append(headerParts, keyStyle.Render("Update"))
233 // Recreate header content with the updated headerParts
233 // Recreate header content with the updated headerParts
234 headerContent = lipgloss.NewStyle().Padding(0, 1).Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...))
234 headerContent = lipgloss.NewStyle().Padding(0, 1).Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...))
235−
235+
236 // Format the diff with colors
236 // Format the diff with colors
237 formattedDiff := formatDiff(pr.Diff)
237 formattedDiff := formatDiff(pr.Diff)
238−
238+
239 // Set up viewport for the diff content
239 // Set up viewport for the diff content
240 p.contentViewPort.Width = p.width - 2 - 2
240 p.contentViewPort.Width = p.width - 2 - 2
241−
241+
242 // Calculate content height dynamically based on window size
242 // Calculate content height dynamically based on window size
243 maxContentHeight := p.height - lipgloss.Height(headerContent) - lipgloss.Height(form) - 2 - 2 - 1
243 maxContentHeight := p.height - lipgloss.Height(headerContent) - lipgloss.Height(form) - 2 - 2 - 1
244 p.contentViewPort.Height = maxContentHeight
244 p.contentViewPort.Height = maxContentHeight
245 p.contentViewPort.SetContent(formattedDiff)
245 p.contentViewPort.SetContent(formattedDiff)
246−
246+
247 // Style the viewport
247 // Style the viewport
248 var contentBorder lipgloss.Border
248 var contentBorder lipgloss.Border
249 var borderColor lipgloss.TerminalColor
249 var borderColor lipgloss.TerminalColor
250−
250+
251 if p.isViewportFocus {
251 if p.isViewportFocus {
252 contentBorder = lipgloss.DoubleBorder()
252 contentBorder = lipgloss.DoubleBorder()
253 borderColor = styles.Blue
253 borderColor = styles.Blue
@@ -255,47 +255,47 @@ func (p *permissionDialogCmp) render() string {
255 contentBorder = lipgloss.RoundedBorder()
255 contentBorder = lipgloss.RoundedBorder()
256 borderColor = styles.Flamingo
256 borderColor = styles.Flamingo
257 }
257 }
258−
258+
259 contentStyle := lipgloss.NewStyle().
259 contentStyle := lipgloss.NewStyle().
260 MarginTop(1).
260 MarginTop(1).
261 Padding(0, 1).
261 Padding(0, 1).
262 Border(contentBorder).
262 Border(contentBorder).
263 BorderForeground(borderColor)
263 BorderForeground(borderColor)
264−
264+
265 if p.isViewportFocus {
265 if p.isViewportFocus {
266 contentStyle = contentStyle.BorderBackground(styles.Surface0)
266 contentStyle = contentStyle.BorderBackground(styles.Surface0)
267 }
267 }
268−
268+
269 contentFinal := contentStyle.Render(p.contentViewPort.View())
269 contentFinal := contentStyle.Render(p.contentViewPort.View())
270−
270+
271 return lipgloss.JoinVertical(
271 return lipgloss.JoinVertical(
272 lipgloss.Top,
272 lipgloss.Top,
273 headerContent,
273 headerContent,
274 contentFinal,
274 contentFinal,
275 form,
275 form,
276 )
276 )
277−
277+
278 case tools.WriteToolName:
278 case tools.WriteToolName:
279 pr := p.permission.Params.(tools.WritePermissionsParams)
279 pr := p.permission.Params.(tools.WritePermissionsParams)
280 headerParts = append(headerParts, keyStyle.Render("Content"))
280 headerParts = append(headerParts, keyStyle.Render("Content"))
281 // Recreate header content with the updated headerParts
281 // Recreate header content with the updated headerParts
282 headerContent = lipgloss.NewStyle().Padding(0, 1).Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...))
282 headerContent = lipgloss.NewStyle().Padding(0, 1).Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...))
283−
283+
284 // Format the diff with colors
284 // Format the diff with colors
285 formattedDiff := formatDiff(pr.Content)
285 formattedDiff := formatDiff(pr.Content)
286−
286+
287 // Set up viewport for the content
287 // Set up viewport for the content
288 p.contentViewPort.Width = p.width - 2 - 2
288 p.contentViewPort.Width = p.width - 2 - 2
289−
289+
290 // Calculate content height dynamically based on window size
290 // Calculate content height dynamically based on window size
291 maxContentHeight := p.height - lipgloss.Height(headerContent) - lipgloss.Height(form) - 2 - 2 - 1
291 maxContentHeight := p.height - lipgloss.Height(headerContent) - lipgloss.Height(form) - 2 - 2 - 1
292 p.contentViewPort.Height = maxContentHeight
292 p.contentViewPort.Height = maxContentHeight
293 p.contentViewPort.SetContent(formattedDiff)
293 p.contentViewPort.SetContent(formattedDiff)
294−
294+
295 // Style the viewport
295 // Style the viewport
296 var contentBorder lipgloss.Border
296 var contentBorder lipgloss.Border
297 var borderColor lipgloss.TerminalColor
297 var borderColor lipgloss.TerminalColor
298−
298+
299 if p.isViewportFocus {
299 if p.isViewportFocus {
300 contentBorder = lipgloss.DoubleBorder()
300 contentBorder = lipgloss.DoubleBorder()
301 borderColor = styles.Blue
301 borderColor = styles.Blue
@@ -303,75 +303,75 @@ func (p *permissionDialogCmp) render() string {
303 contentBorder = lipgloss.RoundedBorder()
303 contentBorder = lipgloss.RoundedBorder()
304 borderColor = styles.Flamingo
304 borderColor = styles.Flamingo
305 }
305 }
306−
306+
307 contentStyle := lipgloss.NewStyle().
307 contentStyle := lipgloss.NewStyle().
308 MarginTop(1).
308 MarginTop(1).
309 Padding(0, 1).
309 Padding(0, 1).
310 Border(contentBorder).
310 Border(contentBorder).
311 BorderForeground(borderColor)
311 BorderForeground(borderColor)
312−
312+
313 if p.isViewportFocus {
313 if p.isViewportFocus {
314 contentStyle = contentStyle.BorderBackground(styles.Surface0)
314 contentStyle = contentStyle.BorderBackground(styles.Surface0)
315 }
315 }
316−
316+
317 contentFinal := contentStyle.Render(p.contentViewPort.View())
317 contentFinal := contentStyle.Render(p.contentViewPort.View())
318−
318+
319 return lipgloss.JoinVertical(
319 return lipgloss.JoinVertical(
320 lipgloss.Top,
320 lipgloss.Top,
321 headerContent,
321 headerContent,
322 contentFinal,
322 contentFinal,
323 form,
323 form,
324 )
324 )
325−
325+
326 case tools.FetchToolName:
326 case tools.FetchToolName:
327 pr := p.permission.Params.(tools.FetchPermissionsParams)
327 pr := p.permission.Params.(tools.FetchPermissionsParams)
328 headerParts = append(headerParts, keyStyle.Render("URL: "+pr.URL))
328 headerParts = append(headerParts, keyStyle.Render("URL: "+pr.URL))
329 content := p.permission.Description
329 content := p.permission.Description
330−
330+
331 renderedContent, _ := r.Render(content)
331 renderedContent, _ := r.Render(content)
332 p.contentViewPort.Width = p.width - 2 - 2
332 p.contentViewPort.Width = p.width - 2 - 2
333 p.contentViewPort.Height = p.height - lipgloss.Height(headerContent) - lipgloss.Height(form) - 2 - 2 - 1
333 p.contentViewPort.Height = p.height - lipgloss.Height(headerContent) - lipgloss.Height(form) - 2 - 2 - 1
334 p.contentViewPort.SetContent(renderedContent)
334 p.contentViewPort.SetContent(renderedContent)
335−
335+
336 // Style the viewport
336 // Style the viewport
337 contentStyle := lipgloss.NewStyle().
337 contentStyle := lipgloss.NewStyle().
338 MarginTop(1).
338 MarginTop(1).
339 Padding(0, 1).
339 Padding(0, 1).
340 Border(lipgloss.RoundedBorder()).
340 Border(lipgloss.RoundedBorder()).
341 BorderForeground(styles.Flamingo)
341 BorderForeground(styles.Flamingo)
342−
342+
343 contentFinal := contentStyle.Render(p.contentViewPort.View())
343 contentFinal := contentStyle.Render(p.contentViewPort.View())
344 if renderedContent == "" {
344 if renderedContent == "" {
345 contentFinal = ""
345 contentFinal = ""
346 }
346 }
347−
347+
348 return lipgloss.JoinVertical(
348 return lipgloss.JoinVertical(
349 lipgloss.Top,
349 lipgloss.Top,
350 headerContent,
350 headerContent,
351 contentFinal,
351 contentFinal,
352 form,
352 form,
353 )
353 )
354−
354+
355 default:
355 default:
356 content := p.permission.Description
356 content := p.permission.Description
357−
357+
358 renderedContent, _ := r.Render(content)
358 renderedContent, _ := r.Render(content)
359 p.contentViewPort.Width = p.width - 2 - 2
359 p.contentViewPort.Width = p.width - 2 - 2
360 p.contentViewPort.Height = p.height - lipgloss.Height(headerContent) - lipgloss.Height(form) - 2 - 2 - 1
360 p.contentViewPort.Height = p.height - lipgloss.Height(headerContent) - lipgloss.Height(form) - 2 - 2 - 1
361 p.contentViewPort.SetContent(renderedContent)
361 p.contentViewPort.SetContent(renderedContent)
362−
362+
363 // Style the viewport
363 // Style the viewport
364 contentStyle := lipgloss.NewStyle().
364 contentStyle := lipgloss.NewStyle().
365 MarginTop(1).
365 MarginTop(1).
366 Padding(0, 1).
366 Padding(0, 1).
367 Border(lipgloss.RoundedBorder()).
367 Border(lipgloss.RoundedBorder()).
368 BorderForeground(styles.Flamingo)
368 BorderForeground(styles.Flamingo)
369−
369+
370 contentFinal := contentStyle.Render(p.contentViewPort.View())
370 contentFinal := contentStyle.Render(p.contentViewPort.View())
371 if renderedContent == "" {
371 if renderedContent == "" {
372 contentFinal = ""
372 contentFinal = ""
373 }
373 }
374−
374+
375 return lipgloss.JoinVertical(
375 return lipgloss.JoinVertical(
376 lipgloss.Top,
376 lipgloss.Top,
377 headerContent,
377 headerContent,
internal/tui/components/repl/editor.go+1−1
@@ -140,7 +140,7 @@ func (m *editorCmp) Send() tea.Cmd {
140 return func() tea.Msg {
140 return func() tea.Msg {
141 messages, _ := m.app.Messages.List(m.sessionID)
141 messages, _ := m.app.Messages.List(m.sessionID)
142 if hasUnfinishedMessages(messages) {
142 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")
144 }
144 }
145 a, _ := agent.NewCoderAgent(m.app)
145 a, _ := agent.NewCoderAgent(m.app)
146
146
internal/tui/tui.go+4−4
@@ -77,6 +77,8 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
77 switch msg := msg.(type) {
77 switch msg := msg.(type) {
78 case pubsub.Event[permission.PermissionRequest]:
78 case pubsub.Event[permission.PermissionRequest]:
79 return a, dialog.NewPermissionDialogCmd(msg.Payload)
79 return a, dialog.NewPermissionDialogCmd(msg.Payload)
80+ case pubsub.Event[util.InfoMsg]:
81+ a.status, _ = a.status.Update(msg)
80 case dialog.PermissionResponseMsg:
82 case dialog.PermissionResponseMsg:
81 switch msg.Action {
83 switch msg.Action {
82 case dialog.PermissionAllow:
84 case dialog.PermissionAllow:
@@ -121,8 +123,6 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
121 return a, a.moveToPage(msg.ID)
123 return a, a.moveToPage(msg.ID)
122 case util.InfoMsg:
124 case util.InfoMsg:
123 a.status, _ = a.status.Update(msg)
125 a.status, _ = a.status.Update(msg)
124− case util.ErrorMsg:
125− a.status, _ = a.status.Update(msg)
126 case tea.KeyMsg:
126 case tea.KeyMsg:
127 if a.editorMode == vimtea.ModeNormal {
127 if a.editorMode == vimtea.ModeNormal {
128 switch {
128 switch {
@@ -141,7 +141,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
141 if a.currentPage == page.ReplPage {
141 if a.currentPage == page.ReplPage {
142 sessions, err := a.app.Sessions.List()
142 sessions, err := a.app.Sessions.List()
143 if err != nil {
143 if err != nil {
144− return a, util.CmdHandler(util.ErrorMsg(err))
144+ return a, util.CmdHandler(util.ReportError(err))
145 }
145 }
146 lastSession := sessions[0]
146 lastSession := sessions[0]
147 if lastSession.MessageCount == 0 {
147 if lastSession.MessageCount == 0 {
@@ -149,7 +149,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
149 }
149 }
150 s, err := a.app.Sessions.Create("New Session")
150 s, err := a.app.Sessions.Create("New Session")
151 if err != nil {
151 if err != nil {
152− return a, util.CmdHandler(util.ErrorMsg(err))
152+ return a, util.CmdHandler(util.ReportError(err))
153 }
153 }
154 return a, util.CmdHandler(repl.SelectedSessionMsg{SessionID: s.ID})
154 return a, util.CmdHandler(repl.SelectedSessionMsg{SessionID: s.ID})
155 }
155 }
internal/tui/util/util.go+30−3
@@ -9,12 +9,39 @@ func CmdHandler(msg tea.Msg) tea.Cmd {
9}
9}
10
10
11func ReportError(err error) tea.Cmd {
11func 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+ })
13}
38}
14
39
15type (
40type (
41+ InfoMsg struct {
42+ Type InfoType
16− InfoMsg string
43+ Msg string
17− ErrorMsg error
44+ }
18 ClearStatusMsg struct{}
45 ClearStatusMsg struct{}
19)
46)
20
47