on main help sign in
chore(chat): remove empty assistant messages
3474160Kujtim Hoxha
internal/ui/AGENTS.md+52−10
@@ -1,17 +1,59 @@
1# UI Development Instructions
1# UI Development Instructions
2
2
3−## General guideline
3+## General Guidelines
4−- Never use commands to send messages when you can directly mutate children or state
4+- Never use commands to send messages when you can directly mutate children or state.
5−- Keep things simple do not overcomplicated
5+- Keep things simple; do not overcomplicate.
6−- Create files if needed to separate logic do not nest models
6+- Create files if needed to separate logic; do not nest models.
7
7
8−## Big model
8+## Architecture
9−Keep most of the logic and state in the main model `internal/ui/model/ui.go`.
10
9
10+### Main Model (`model/ui.go`)
11+Keep most of the logic and state in the main model. This is where:
12+- Message routing happens
13+- Focus and UI state is managed
14+- Layout calculations are performed
15+- Dialogs are orchestrated
11
16
12−## When working on components
17+### Components Should Be Dumb
13−Whenever you work on components make them dumb they should not handle bubble tea messages they should have methods.
18+Components should not handle bubbletea messages directly. Instead:
19+- Expose methods for state changes
20+- Return `tea.Cmd` from methods when side effects are needed
21+- Handle their own rendering via `Render(width int) string`
14
22
15−## When adding logic that has to do with the chat
23+### Chat Logic (`model/chat.go`)
16−Most of the logic with the chat should be in the chat component `internal/ui/model/chat.go`, keep individual items dumb and handle logic in this component.
24+Most chat-related logic belongs here. Individual chat items in `chat/` should be simple renderers that cache their output and invalidate when data changes (see `cachedMessageItem` in `chat/messages.go`).
17
25
26+## Key Patterns
27+
28+### Composition Over Inheritance
29+Use struct embedding for shared behaviors. See `chat/messages.go` for examples of reusable embedded structs for highlighting, caching, and focus.
30+
31+### Interfaces
32+- List item interfaces are in `list/item.go`
33+- Chat message interfaces are in `chat/messages.go`
34+- Dialog interface is in `dialog/dialog.go`
35+
36+### Styling
37+- All styles are defined in `styles/styles.go`
38+- Access styles via `*common.Common` passed to components
39+- Use semantic color fields rather than hardcoded colors
40+
41+### Dialogs
42+- Implement the dialog interface in `dialog/dialog.go`
43+- Return message types from `Update()` to signal actions to the main model
44+- Use the overlay system for managing dialog lifecycle
45+
46+## File Organization
47+- `model/` - Main UI model and major components (chat, sidebar, etc.)
48+- `chat/` - Chat message item types and renderers
49+- `dialog/` - Dialog implementations
50+- `list/` - Generic list component with lazy rendering
51+- `common/` - Shared utilities and the Common struct
52+- `styles/` - All style definitions
53+- `anim/` - Animation system
54+- `logo/` - Logo rendering
55+
56+## Common Gotchas
57+- Always account for padding/borders in width calculations
58+- Use `tea.Batch()` when returning multiple commands
59+- Pass `*common.Common` to components that need styles or app access
internal/ui/chat/messages.go+3−3
@@ -161,7 +161,7 @@ func ExtractMessageItems(sty *styles.Styles, msg *message.Message, toolResults m
161 return []MessageItem{NewUserMessageItem(sty, msg)}
161 return []MessageItem{NewUserMessageItem(sty, msg)}
162 case message.Assistant:
162 case message.Assistant:
163 var items []MessageItem
163 var items []MessageItem
164− if shouldRenderAssistantMessage(msg) {
164+ if ShouldRenderAssistantMessage(msg) {
165 items = append(items, NewAssistantMessageItem(sty, msg))
165 items = append(items, NewAssistantMessageItem(sty, msg))
166 }
166 }
167 for _, tc := range msg.ToolCalls() {
167 for _, tc := range msg.ToolCalls() {
@@ -181,11 +181,11 @@ func ExtractMessageItems(sty *styles.Styles, msg *message.Message, toolResults m
181 return []MessageItem{}
181 return []MessageItem{}
182}
182}
183
183
184−// shouldRenderAssistantMessage determines if an assistant message should be rendered
184+// ShouldRenderAssistantMessage determines if an assistant message should be rendered
185//
185//
186// In some cases the assistant message only has tools so we do not want to render an
186// In some cases the assistant message only has tools so we do not want to render an
187// empty message.
187// empty message.
188−func shouldRenderAssistantMessage(msg *message.Message) bool {
188+func ShouldRenderAssistantMessage(msg *message.Message) bool {
189 content := strings.TrimSpace(msg.Content().Text)
189 content := strings.TrimSpace(msg.Content().Text)
190 thinking := strings.TrimSpace(msg.ReasoningContent().Thinking)
190 thinking := strings.TrimSpace(msg.ReasoningContent().Thinking)
191 isError := msg.FinishReason() == message.FinishReasonError
191 isError := msg.FinishReason() == message.FinishReasonError
internal/ui/list/list.go+25
@@ -304,6 +304,31 @@ func (l *List) AppendItems(items ...Item) {
304 l.items = append(l.items, items...)
304 l.items = append(l.items, items...)
305}
305}
306
306
307+// RemoveItem removes the item at the given index from the list.
308+func (l *List) RemoveItem(idx int) {
309+ if idx < 0 || idx >= len(l.items) {
310+ return
311+ }
312+
313+ // Remove the item
314+ l.items = append(l.items[:idx], l.items[idx+1:]...)
315+
316+ // Adjust selection if needed
317+ if l.selectedIdx == idx {
318+ l.selectedIdx = -1
319+ } else if l.selectedIdx > idx {
320+ l.selectedIdx--
321+ }
322+
323+ // Adjust offset if needed
324+ if l.offsetIdx > idx {
325+ l.offsetIdx--
326+ } else if l.offsetIdx == idx && l.offsetIdx >= len(l.items) {
327+ l.offsetIdx = max(0, len(l.items)-1)
328+ l.offsetLine = 0
329+ }
330+}
331+
307// Focus sets the focus state of the list.
332// Focus sets the focus state of the list.
308func (l *List) Focus() {
333func (l *List) Focus() {
309 l.focused = true
334 l.focused = true
internal/ui/model/chat.go+24
@@ -245,6 +245,30 @@ func (m *Chat) ClearMessages() {
245 m.ClearMouse()
245 m.ClearMouse()
246}
246}
247
247
248+// RemoveMessage removes a message from the chat list by its ID.
249+func (m *Chat) RemoveMessage(id string) {
250+ idx, ok := m.idInxMap[id]
251+ if !ok {
252+ return
253+ }
254+
255+ // Remove from list
256+ m.list.RemoveItem(idx)
257+
258+ // Remove from index map
259+ delete(m.idInxMap, id)
260+
261+ // Rebuild index map for all items after the removed one
262+ for i := idx; i < m.list.Len(); i++ {
263+ if item, ok := m.list.ItemAt(i).(chat.MessageItem); ok {
264+ m.idInxMap[item.ID()] = i
265+ }
266+ }
267+
268+ // Clean up any paused animations for this message
269+ delete(m.pausedAnimations, id)
270+}
271+
248// MessageItem returns the message item with the given ID, or nil if not found.
272// MessageItem returns the message item with the given ID, or nil if not found.
249func (m *Chat) MessageItem(id string) chat.MessageItem {
273func (m *Chat) MessageItem(id string) chat.MessageItem {
250 idx, ok := m.idInxMap[id]
274 idx, ok := m.idInxMap[id]
internal/ui/model/ui.go+8−4
@@ -431,12 +431,16 @@ func (m *UI) appendSessionMessage(msg message.Message) tea.Cmd {
431func (m *UI) updateSessionMessage(msg message.Message) tea.Cmd {
431func (m *UI) updateSessionMessage(msg message.Message) tea.Cmd {
432 var cmds []tea.Cmd
432 var cmds []tea.Cmd
433 existingItem := m.chat.MessageItem(msg.ID)
433 existingItem := m.chat.MessageItem(msg.ID)
434+
434− if existingItem == nil || msg.Role != message.Assistant {
435+ if existingItem != nil {
435− return nil
436+ if assistantItem, ok := existingItem.(*chat.AssistantMessageItem); ok {
437+ assistantItem.SetMessage(&msg)
438+ }
436 }
439 }
437
440
438− if assistantItem, ok := existingItem.(*chat.AssistantMessageItem); ok {
441+ // if the message of the assistant does not have any response just tool calls we need to remove it
439− assistantItem.SetMessage(&msg)
442+ if !chat.ShouldRenderAssistantMessage(&msg) && len(msg.ToolCalls()) > 0 && existingItem != nil {
443+ m.chat.RemoveMessage(msg.ID)
440 }
444 }
441
445
442 var items []chat.MessageItem
446 var items []chat.MessageItem