on main help sign in
chore(chat): remove empty assistant messages
commit34741602aab75afb82e3eba797efe6ec6d97aefc authorKujtim Hoxha <kujtimii.h@gmail.com> dateFri 19 Dec 2025, 12:01 (1 year ago) parent9899424
5 files changed+112−17
internal/ui/AGENTS.md+52−10 internal/ui/chat/messages.go+3−3 internal/ui/list/list.go+25 internal/ui/model/chat.go+24 internal/ui/model/ui.go+8−4
internal/ui/AGENTS.md+52−10
@@ -1,17 +1,59 @@
11# UI Development Instructions
22
3−## General guideline
4−- Never use commands to send messages when you can directly mutate children or state
5−- Keep things simple do not overcomplicated
6−- Create files if needed to separate logic do not nest models
3+## General Guidelines
4+- Never use commands to send messages when you can directly mutate children or state.
5+- Keep things simple; do not overcomplicate.
6+- Create files if needed to separate logic; do not nest models.
77
8−## Big model
9−Keep most of the logic and state in the main model `internal/ui/model/ui.go`.
8+## Architecture
109
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
1116
12−## When working on components
13−Whenever you work on components make them dumb they should not handle bubble tea messages they should have methods.
17+### Components Should Be Dumb
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`
1422
15−## When adding logic that has to do with the chat
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.
23+### Chat Logic (`model/chat.go`)
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`).
1725
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
161161 return []MessageItem{NewUserMessageItem(sty, msg)}
162162 case message.Assistant:
163163 var items []MessageItem
164− if shouldRenderAssistantMessage(msg) {
164+ if ShouldRenderAssistantMessage(msg) {
165165 items = append(items, NewAssistantMessageItem(sty, msg))
166166 }
167167 for _, tc := range msg.ToolCalls() {
@@ -181,11 +181,11 @@ func ExtractMessageItems(sty *styles.Styles, msg *message.Message, toolResults m
181181 return []MessageItem{}
182182}
183183
184−// shouldRenderAssistantMessage determines if an assistant message should be rendered
184+// ShouldRenderAssistantMessage determines if an assistant message should be rendered
185185//
186186// In some cases the assistant message only has tools so we do not want to render an
187187// empty message.
188−func shouldRenderAssistantMessage(msg *message.Message) bool {
188+func ShouldRenderAssistantMessage(msg *message.Message) bool {
189189 content := strings.TrimSpace(msg.Content().Text)
190190 thinking := strings.TrimSpace(msg.ReasoningContent().Thinking)
191191 isError := msg.FinishReason() == message.FinishReasonError
internal/ui/list/list.go+25
@@ -304,6 +304,31 @@ func (l *List) AppendItems(items ...Item) {
304304 l.items = append(l.items, items...)
305305}
306306
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+
307332// Focus sets the focus state of the list.
308333func (l *List) Focus() {
309334 l.focused = true
internal/ui/model/chat.go+24
@@ -245,6 +245,30 @@ func (m *Chat) ClearMessages() {
245245 m.ClearMouse()
246246}
247247
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+
248272// MessageItem returns the message item with the given ID, or nil if not found.
249273func (m *Chat) MessageItem(id string) chat.MessageItem {
250274 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 {
431431func (m *UI) updateSessionMessage(msg message.Message) tea.Cmd {
432432 var cmds []tea.Cmd
433433 existingItem := m.chat.MessageItem(msg.ID)
434− if existingItem == nil || msg.Role != message.Assistant {
435− return nil
434+
435+ if existingItem != nil {
436+ if assistantItem, ok := existingItem.(*chat.AssistantMessageItem); ok {
437+ assistantItem.SetMessage(&msg)
438+ }
436439 }
437440
438− if assistantItem, ok := existingItem.(*chat.AssistantMessageItem); ok {
439− assistantItem.SetMessage(&msg)
441+ // if the message of the assistant does not have any response just tool calls we need to remove it
442+ if !chat.ShouldRenderAssistantMessage(&msg) && len(msg.ToolCalls()) > 0 && existingItem != nil {
443+ m.chat.RemoveMessage(msg.ID)
440444 }
441445
442446 var items []chat.MessageItem