fix tools calls

Kujtim Hoxha created

Change summary

internal/tui/components/chat/editor.go    |   4 
internal/tui/components/chat/list.go      | 898 ++++++++++++------------
internal/tui/components/chat/list_v2.go   |   8 
internal/tui/components/chat/message.go   | 629 -----------------
internal/tui/components/core/list/list.go |  15 
5 files changed, 476 insertions(+), 1,078 deletions(-)

Detailed changes

internal/tui/components/chat/editor.go 🔗

@@ -211,7 +211,6 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 				return m, m.send()
 			}
 		}
-
 	}
 	m.textarea, cmd = m.textarea.Update(msg)
 	return m, cmd
@@ -233,7 +232,8 @@ func (m *editorCmp) View() string {
 	return lipgloss.JoinVertical(lipgloss.Top,
 		m.attachmentsContent(),
 		lipgloss.JoinHorizontal(lipgloss.Top, style.Render(">"),
-			m.textarea.View()),
+			m.textarea.View(),
+		),
 	)
 }
 

internal/tui/components/chat/list.go 🔗

@@ -1,44 +1,49 @@
 package chat
 
-import (
-	"context"
-	"fmt"
-	"math"
-
-	"github.com/charmbracelet/bubbles/v2/key"
-	"github.com/charmbracelet/bubbles/v2/spinner"
-	"github.com/charmbracelet/bubbles/v2/viewport"
-	tea "github.com/charmbracelet/bubbletea/v2"
-	"github.com/charmbracelet/lipgloss/v2"
-	"github.com/opencode-ai/opencode/internal/app"
-	"github.com/opencode-ai/opencode/internal/message"
-	"github.com/opencode-ai/opencode/internal/pubsub"
-	"github.com/opencode-ai/opencode/internal/session"
-	"github.com/opencode-ai/opencode/internal/tui/components/dialog"
-	"github.com/opencode-ai/opencode/internal/tui/styles"
-	"github.com/opencode-ai/opencode/internal/tui/theme"
-	"github.com/opencode-ai/opencode/internal/tui/util"
-)
-
-type cacheItem struct {
-	width   int
-	content []uiMessage
-}
-type messagesCmp struct {
-	app           *app.App
-	width, height int
-	viewport      viewport.Model
-	session       session.Session
-	messages      []message.Message
-	uiMessages    []uiMessage
-	currentMsgID  string
-	cachedContent map[string]cacheItem
-	spinner       spinner.Model
-	rendering     bool
-	attachments   viewport.Model
-}
-type renderFinishedMsg struct{}
-
+import "github.com/charmbracelet/bubbles/v2/key"
+
+// import (
+//
+//	"context"
+//	"fmt"
+//	"math"
+//
+//	"github.com/charmbracelet/bubbles/v2/key"
+//	"github.com/charmbracelet/bubbles/v2/spinner"
+//	"github.com/charmbracelet/bubbles/v2/viewport"
+//	tea "github.com/charmbracelet/bubbletea/v2"
+//	"github.com/charmbracelet/lipgloss/v2"
+//	"github.com/opencode-ai/opencode/internal/app"
+//	"github.com/opencode-ai/opencode/internal/message"
+//	"github.com/opencode-ai/opencode/internal/pubsub"
+//	"github.com/opencode-ai/opencode/internal/session"
+//	"github.com/opencode-ai/opencode/internal/tui/components/dialog"
+//	"github.com/opencode-ai/opencode/internal/tui/styles"
+//	"github.com/opencode-ai/opencode/internal/tui/theme"
+//	"github.com/opencode-ai/opencode/internal/tui/util"
+//
+// )
+//
+//	type cacheItem struct {
+//		width   int
+//		content []uiMessage
+//	}
+//
+//	type messagesCmp struct {
+//		app           *app.App
+//		width, height int
+//		viewport      viewport.Model
+//		session       session.Session
+//		messages      []message.Message
+//		uiMessages    []uiMessage
+//		currentMsgID  string
+//		cachedContent map[string]cacheItem
+//		spinner       spinner.Model
+//		rendering     bool
+//		attachments   viewport.Model
+//	}
+//
+// type renderFinishedMsg struct{}
 type MessageKeys struct {
 	PageDown     key.Binding
 	PageUp       key.Binding
@@ -65,410 +70,411 @@ var messageKeys = MessageKeys{
 	),
 }
 
-func (m *messagesCmp) Init() tea.Cmd {
-	return tea.Batch(m.viewport.Init(), m.spinner.Tick)
-}
-
-func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
-	var cmds []tea.Cmd
-	switch msg := msg.(type) {
-	case dialog.ThemeChangedMsg:
-		m.rerender()
-		return m, nil
-	case SessionSelectedMsg:
-		if msg.ID != m.session.ID {
-			cmd := m.SetSession(msg)
-			return m, cmd
-		}
-		return m, nil
-	case SessionClearedMsg:
-		m.session = session.Session{}
-		m.messages = make([]message.Message, 0)
-		m.currentMsgID = ""
-		m.rendering = false
-		return m, nil
-
-	case tea.KeyMsg:
-		if key.Matches(msg, messageKeys.PageUp) || key.Matches(msg, messageKeys.PageDown) ||
-			key.Matches(msg, messageKeys.HalfPageUp) || key.Matches(msg, messageKeys.HalfPageDown) {
-			u, cmd := m.viewport.Update(msg)
-			m.viewport = u
-			cmds = append(cmds, cmd)
-		}
-
-	case renderFinishedMsg:
-		m.rendering = false
-		m.viewport.GotoBottom()
-	case pubsub.Event[session.Session]:
-		if msg.Type == pubsub.UpdatedEvent && msg.Payload.ID == m.session.ID {
-			m.session = msg.Payload
-			if m.session.SummaryMessageID == m.currentMsgID {
-				delete(m.cachedContent, m.currentMsgID)
-				m.renderView()
-			}
-		}
-	case pubsub.Event[message.Message]:
-		needsRerender := false
-		if msg.Type == pubsub.CreatedEvent {
-			if msg.Payload.SessionID == m.session.ID {
-
-				messageExists := false
-				for _, v := range m.messages {
-					if v.ID == msg.Payload.ID {
-						messageExists = true
-						break
-					}
-				}
-
-				if !messageExists {
-					if len(m.messages) > 0 {
-						lastMsgID := m.messages[len(m.messages)-1].ID
-						delete(m.cachedContent, lastMsgID)
-					}
-
-					m.messages = append(m.messages, msg.Payload)
-					delete(m.cachedContent, m.currentMsgID)
-					m.currentMsgID = msg.Payload.ID
-					needsRerender = true
-				}
-			}
-			// There are tool calls from the child task
-			for _, v := range m.messages {
-				for _, c := range v.ToolCalls() {
-					if c.ID == msg.Payload.SessionID {
-						delete(m.cachedContent, v.ID)
-						needsRerender = true
-					}
-				}
-			}
-		} else if msg.Type == pubsub.UpdatedEvent && msg.Payload.SessionID == m.session.ID {
-			for i, v := range m.messages {
-				if v.ID == msg.Payload.ID {
-					m.messages[i] = msg.Payload
-					delete(m.cachedContent, msg.Payload.ID)
-					needsRerender = true
-					break
-				}
-			}
-		}
-		if needsRerender {
-			m.renderView()
-			if len(m.messages) > 0 {
-				if (msg.Type == pubsub.CreatedEvent) ||
-					(msg.Type == pubsub.UpdatedEvent && msg.Payload.ID == m.messages[len(m.messages)-1].ID) {
-					m.viewport.GotoBottom()
-				}
-			}
-		}
-	}
-
-	spinner, cmd := m.spinner.Update(msg)
-	m.spinner = spinner
-	cmds = append(cmds, cmd)
-	return m, tea.Batch(cmds...)
-}
-
-func (m *messagesCmp) IsAgentWorking() bool {
-	return m.app.CoderAgent.IsSessionBusy(m.session.ID)
-}
-
-func formatTimeDifference(unixTime1, unixTime2 int64) string {
-	diffSeconds := float64(math.Abs(float64(unixTime2 - unixTime1)))
-
-	if diffSeconds < 60 {
-		return fmt.Sprintf("%.1fs", diffSeconds)
-	}
-
-	minutes := int(diffSeconds / 60)
-	seconds := int(diffSeconds) % 60
-	return fmt.Sprintf("%dm%ds", minutes, seconds)
-}
-
-func (m *messagesCmp) renderView() {
-	m.uiMessages = make([]uiMessage, 0)
-	pos := 0
-	baseStyle := styles.BaseStyle()
-
-	if m.width == 0 {
-		return
-	}
-	for inx, msg := range m.messages {
-		switch msg.Role {
-		case message.User:
-			if cache, ok := m.cachedContent[msg.ID]; ok && cache.width == m.width {
-				m.uiMessages = append(m.uiMessages, cache.content...)
-				continue
-			}
-			userMsg := renderUserMessage(
-				msg,
-				msg.ID == m.currentMsgID,
-				m.width,
-				pos,
-			)
-			m.uiMessages = append(m.uiMessages, userMsg)
-			m.cachedContent[msg.ID] = cacheItem{
-				width:   m.width,
-				content: []uiMessage{userMsg},
-			}
-			pos += userMsg.height + 1 // + 1 for spacing
-		case message.Assistant:
-			if cache, ok := m.cachedContent[msg.ID]; ok && cache.width == m.width {
-				m.uiMessages = append(m.uiMessages, cache.content...)
-				continue
-			}
-			isSummary := m.session.SummaryMessageID == msg.ID
-
-			assistantMessages := renderAssistantMessage(
-				msg,
-				inx,
-				m.messages,
-				m.app.Messages,
-				m.currentMsgID,
-				isSummary,
-				m.width,
-				pos,
-			)
-			for _, msg := range assistantMessages {
-				m.uiMessages = append(m.uiMessages, msg)
-				pos += msg.height + 1 // + 1 for spacing
-			}
-			m.cachedContent[msg.ID] = cacheItem{
-				width:   m.width,
-				content: assistantMessages,
-			}
-		}
-	}
-
-	messages := make([]string, 0)
-	for _, v := range m.uiMessages {
-		messages = append(messages, lipgloss.JoinVertical(lipgloss.Left, v.content),
-			baseStyle.
-				Width(m.width).
-				Render(
-					"",
-				),
-		)
-	}
-
-	m.viewport.SetContent(
-		baseStyle.
-			Width(m.width).
-			Render(
-				lipgloss.JoinVertical(
-					lipgloss.Top,
-					messages...,
-				),
-			),
-	)
-}
-
-func (m *messagesCmp) View() string {
-	baseStyle := styles.BaseStyle()
-
-	if m.rendering {
-		return baseStyle.
-			Width(m.width).
-			Render(
-				lipgloss.JoinVertical(
-					lipgloss.Top,
-					"Loading...",
-					m.working(),
-					m.help(),
-				),
-			)
-	}
-	if len(m.messages) == 0 {
-		content := baseStyle.
-			Width(m.width).
-			Height(m.height - 1).
-			Render(
-				initialScreen(),
-			)
-
-		return baseStyle.
-			Width(m.width).
-			Render(
-				lipgloss.JoinVertical(
-					lipgloss.Top,
-					content,
-					"",
-					m.help(),
-				),
-			)
-	}
-
-	return baseStyle.
-		Width(m.width).
-		Render(
-			lipgloss.JoinVertical(
-				lipgloss.Top,
-				m.viewport.View(),
-				m.working(),
-				m.help(),
-			),
-		)
-}
-
-func hasToolsWithoutResponse(messages []message.Message) bool {
-	toolCalls := make([]message.ToolCall, 0)
-	toolResults := make([]message.ToolResult, 0)
-	for _, m := range messages {
-		toolCalls = append(toolCalls, m.ToolCalls()...)
-		toolResults = append(toolResults, m.ToolResults()...)
-	}
-
-	for _, v := range toolCalls {
-		found := false
-		for _, r := range toolResults {
-			if v.ID == r.ToolCallID {
-				found = true
-				break
-			}
-		}
-		if !found && v.Finished {
-			return true
-		}
-	}
-	return false
-}
-
-func hasUnfinishedToolCalls(messages []message.Message) bool {
-	toolCalls := make([]message.ToolCall, 0)
-	for _, m := range messages {
-		toolCalls = append(toolCalls, m.ToolCalls()...)
-	}
-	for _, v := range toolCalls {
-		if !v.Finished {
-			return true
-		}
-	}
-	return false
-}
-
-func (m *messagesCmp) working() string {
-	text := ""
-	if m.IsAgentWorking() && len(m.messages) > 0 {
-		t := theme.CurrentTheme()
-		baseStyle := styles.BaseStyle()
-
-		task := "Thinking..."
-		lastMessage := m.messages[len(m.messages)-1]
-		if hasToolsWithoutResponse(m.messages) {
-			task = "Waiting for tool response..."
-		} else if hasUnfinishedToolCalls(m.messages) {
-			task = "Building tool call..."
-		} else if !lastMessage.IsFinished() {
-			task = "Generating..."
-		}
-		if task != "" {
-			text += baseStyle.
-				Width(m.width).
-				Foreground(t.Primary()).
-				Bold(true).
-				Render(fmt.Sprintf("%s %s ", m.spinner.View(), task))
-		}
-	}
-	return text
-}
-
-func (m *messagesCmp) help() string {
-	t := theme.CurrentTheme()
-	baseStyle := styles.BaseStyle()
-
-	text := ""
-
-	if m.app.CoderAgent.IsBusy() {
-		text += lipgloss.JoinHorizontal(
-			lipgloss.Left,
-			baseStyle.Foreground(t.TextMuted()).Bold(true).Render("press "),
-			baseStyle.Foreground(t.Text()).Bold(true).Render("esc"),
-			baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" to exit cancel"),
-		)
-	} else {
-		text += lipgloss.JoinHorizontal(
-			lipgloss.Left,
-			baseStyle.Foreground(t.TextMuted()).Bold(true).Render("press "),
-			baseStyle.Foreground(t.Text()).Bold(true).Render("enter"),
-			baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" to send the message,"),
-			baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" write"),
-			baseStyle.Foreground(t.Text()).Bold(true).Render(" \\"),
-			baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" and enter to add a new line"),
-		)
-	}
-	return baseStyle.
-		Width(m.width).
-		Render(text)
-}
-
-func (m *messagesCmp) rerender() {
-	for _, msg := range m.messages {
-		delete(m.cachedContent, msg.ID)
-	}
-	m.renderView()
-}
-
-func (m *messagesCmp) SetSize(width, height int) tea.Cmd {
-	if m.width == width && m.height == height {
-		return nil
-	}
-	m.width = width
-	m.height = height
-	m.viewport.SetWidth(width)
-	m.viewport.SetHeight(height - 2)
-	m.attachments.SetWidth(width + 40)
-	m.attachments.SetHeight(3)
-	m.rerender()
-	return nil
-}
-
-func (m *messagesCmp) GetSize() (int, int) {
-	return m.width, m.height
-}
-
-func (m *messagesCmp) SetSession(session session.Session) tea.Cmd {
-	if m.session.ID == session.ID {
-		return nil
-	}
-	m.session = session
-	messages, err := m.app.Messages.List(context.Background(), session.ID)
-	if err != nil {
-		return util.ReportError(err)
-	}
-	m.messages = messages
-	if len(m.messages) > 0 {
-		m.currentMsgID = m.messages[len(m.messages)-1].ID
-	}
-	delete(m.cachedContent, m.currentMsgID)
-	m.rendering = true
-	return func() tea.Msg {
-		m.renderView()
-		return renderFinishedMsg{}
-	}
-}
-
-func (m *messagesCmp) BindingKeys() []key.Binding {
-	return []key.Binding{
-		m.viewport.KeyMap.PageDown,
-		m.viewport.KeyMap.PageUp,
-		m.viewport.KeyMap.HalfPageUp,
-		m.viewport.KeyMap.HalfPageDown,
-	}
-}
-
-func NewMessagesCmp(app *app.App) util.Model {
-	s := spinner.New()
-	s.Spinner = spinner.Pulse
-	vp := viewport.New()
-	attachmets := viewport.New()
-	vp.KeyMap.PageUp = messageKeys.PageUp
-	vp.KeyMap.PageDown = messageKeys.PageDown
-	vp.KeyMap.HalfPageUp = messageKeys.HalfPageUp
-	vp.KeyMap.HalfPageDown = messageKeys.HalfPageDown
-	return &messagesCmp{
-		app:           app,
-		cachedContent: make(map[string]cacheItem),
-		viewport:      vp,
-		spinner:       s,
-		attachments:   attachmets,
-	}
-}
+//
+// func (m *messagesCmp) Init() tea.Cmd {
+// 	return tea.Batch(m.viewport.Init(), m.spinner.Tick)
+// }
+//
+// func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+// 	var cmds []tea.Cmd
+// 	switch msg := msg.(type) {
+// 	case dialog.ThemeChangedMsg:
+// 		m.rerender()
+// 		return m, nil
+// 	case SessionSelectedMsg:
+// 		if msg.ID != m.session.ID {
+// 			cmd := m.SetSession(msg)
+// 			return m, cmd
+// 		}
+// 		return m, nil
+// 	case SessionClearedMsg:
+// 		m.session = session.Session{}
+// 		m.messages = make([]message.Message, 0)
+// 		m.currentMsgID = ""
+// 		m.rendering = false
+// 		return m, nil
+//
+// 	case tea.KeyMsg:
+// 		if key.Matches(msg, messageKeys.PageUp) || key.Matches(msg, messageKeys.PageDown) ||
+// 			key.Matches(msg, messageKeys.HalfPageUp) || key.Matches(msg, messageKeys.HalfPageDown) {
+// 			u, cmd := m.viewport.Update(msg)
+// 			m.viewport = u
+// 			cmds = append(cmds, cmd)
+// 		}
+//
+// 	case renderFinishedMsg:
+// 		m.rendering = false
+// 		m.viewport.GotoBottom()
+// 	case pubsub.Event[session.Session]:
+// 		if msg.Type == pubsub.UpdatedEvent && msg.Payload.ID == m.session.ID {
+// 			m.session = msg.Payload
+// 			if m.session.SummaryMessageID == m.currentMsgID {
+// 				delete(m.cachedContent, m.currentMsgID)
+// 				m.renderView()
+// 			}
+// 		}
+// 	case pubsub.Event[message.Message]:
+// 		needsRerender := false
+// 		if msg.Type == pubsub.CreatedEvent {
+// 			if msg.Payload.SessionID == m.session.ID {
+//
+// 				messageExists := false
+// 				for _, v := range m.messages {
+// 					if v.ID == msg.Payload.ID {
+// 						messageExists = true
+// 						break
+// 					}
+// 				}
+//
+// 				if !messageExists {
+// 					if len(m.messages) > 0 {
+// 						lastMsgID := m.messages[len(m.messages)-1].ID
+// 						delete(m.cachedContent, lastMsgID)
+// 					}
+//
+// 					m.messages = append(m.messages, msg.Payload)
+// 					delete(m.cachedContent, m.currentMsgID)
+// 					m.currentMsgID = msg.Payload.ID
+// 					needsRerender = true
+// 				}
+// 			}
+// 			// There are tool calls from the child task
+// 			for _, v := range m.messages {
+// 				for _, c := range v.ToolCalls() {
+// 					if c.ID == msg.Payload.SessionID {
+// 						delete(m.cachedContent, v.ID)
+// 						needsRerender = true
+// 					}
+// 				}
+// 			}
+// 		} else if msg.Type == pubsub.UpdatedEvent && msg.Payload.SessionID == m.session.ID {
+// 			for i, v := range m.messages {
+// 				if v.ID == msg.Payload.ID {
+// 					m.messages[i] = msg.Payload
+// 					delete(m.cachedContent, msg.Payload.ID)
+// 					needsRerender = true
+// 					break
+// 				}
+// 			}
+// 		}
+// 		if needsRerender {
+// 			m.renderView()
+// 			if len(m.messages) > 0 {
+// 				if (msg.Type == pubsub.CreatedEvent) ||
+// 					(msg.Type == pubsub.UpdatedEvent && msg.Payload.ID == m.messages[len(m.messages)-1].ID) {
+// 					m.viewport.GotoBottom()
+// 				}
+// 			}
+// 		}
+// 	}
+//
+// 	spinner, cmd := m.spinner.Update(msg)
+// 	m.spinner = spinner
+// 	cmds = append(cmds, cmd)
+// 	return m, tea.Batch(cmds...)
+// }
+//
+// func (m *messagesCmp) IsAgentWorking() bool {
+// 	return m.app.CoderAgent.IsSessionBusy(m.session.ID)
+// }
+//
+// func formatTimeDifference(unixTime1, unixTime2 int64) string {
+// 	diffSeconds := float64(math.Abs(float64(unixTime2 - unixTime1)))
+//
+// 	if diffSeconds < 60 {
+// 		return fmt.Sprintf("%.1fs", diffSeconds)
+// 	}
+//
+// 	minutes := int(diffSeconds / 60)
+// 	seconds := int(diffSeconds) % 60
+// 	return fmt.Sprintf("%dm%ds", minutes, seconds)
+// }
+//
+// func (m *messagesCmp) renderView() {
+// 	m.uiMessages = make([]uiMessage, 0)
+// 	pos := 0
+// 	baseStyle := styles.BaseStyle()
+//
+// 	if m.width == 0 {
+// 		return
+// 	}
+// 	for inx, msg := range m.messages {
+// 		switch msg.Role {
+// 		case message.User:
+// 			if cache, ok := m.cachedContent[msg.ID]; ok && cache.width == m.width {
+// 				m.uiMessages = append(m.uiMessages, cache.content...)
+// 				continue
+// 			}
+// 			userMsg := renderUserMessage(
+// 				msg,
+// 				msg.ID == m.currentMsgID,
+// 				m.width,
+// 				pos,
+// 			)
+// 			m.uiMessages = append(m.uiMessages, userMsg)
+// 			m.cachedContent[msg.ID] = cacheItem{
+// 				width:   m.width,
+// 				content: []uiMessage{userMsg},
+// 			}
+// 			pos += userMsg.height + 1 // + 1 for spacing
+// 		case message.Assistant:
+// 			if cache, ok := m.cachedContent[msg.ID]; ok && cache.width == m.width {
+// 				m.uiMessages = append(m.uiMessages, cache.content...)
+// 				continue
+// 			}
+// 			isSummary := m.session.SummaryMessageID == msg.ID
+//
+// 			assistantMessages := renderAssistantMessage(
+// 				msg,
+// 				inx,
+// 				m.messages,
+// 				m.app.Messages,
+// 				m.currentMsgID,
+// 				isSummary,
+// 				m.width,
+// 				pos,
+// 			)
+// 			for _, msg := range assistantMessages {
+// 				m.uiMessages = append(m.uiMessages, msg)
+// 				pos += msg.height + 1 // + 1 for spacing
+// 			}
+// 			m.cachedContent[msg.ID] = cacheItem{
+// 				width:   m.width,
+// 				content: assistantMessages,
+// 			}
+// 		}
+// 	}
+//
+// 	messages := make([]string, 0)
+// 	for _, v := range m.uiMessages {
+// 		messages = append(messages, lipgloss.JoinVertical(lipgloss.Left, v.content),
+// 			baseStyle.
+// 				Width(m.width).
+// 				Render(
+// 					"",
+// 				),
+// 		)
+// 	}
+//
+// 	m.viewport.SetContent(
+// 		baseStyle.
+// 			Width(m.width).
+// 			Render(
+// 				lipgloss.JoinVertical(
+// 					lipgloss.Top,
+// 					messages...,
+// 				),
+// 			),
+// 	)
+// }
+//
+// func (m *messagesCmp) View() string {
+// 	baseStyle := styles.BaseStyle()
+//
+// 	if m.rendering {
+// 		return baseStyle.
+// 			Width(m.width).
+// 			Render(
+// 				lipgloss.JoinVertical(
+// 					lipgloss.Top,
+// 					"Loading...",
+// 					m.working(),
+// 					m.help(),
+// 				),
+// 			)
+// 	}
+// 	if len(m.messages) == 0 {
+// 		content := baseStyle.
+// 			Width(m.width).
+// 			Height(m.height - 1).
+// 			Render(
+// 				initialScreen(),
+// 			)
+//
+// 		return baseStyle.
+// 			Width(m.width).
+// 			Render(
+// 				lipgloss.JoinVertical(
+// 					lipgloss.Top,
+// 					content,
+// 					"",
+// 					m.help(),
+// 				),
+// 			)
+// 	}
+//
+// 	return baseStyle.
+// 		Width(m.width).
+// 		Render(
+// 			lipgloss.JoinVertical(
+// 				lipgloss.Top,
+// 				m.viewport.View(),
+// 				m.working(),
+// 				m.help(),
+// 			),
+// 		)
+// }
+//
+// func hasToolsWithoutResponse(messages []message.Message) bool {
+// 	toolCalls := make([]message.ToolCall, 0)
+// 	toolResults := make([]message.ToolResult, 0)
+// 	for _, m := range messages {
+// 		toolCalls = append(toolCalls, m.ToolCalls()...)
+// 		toolResults = append(toolResults, m.ToolResults()...)
+// 	}
+//
+// 	for _, v := range toolCalls {
+// 		found := false
+// 		for _, r := range toolResults {
+// 			if v.ID == r.ToolCallID {
+// 				found = true
+// 				break
+// 			}
+// 		}
+// 		if !found && v.Finished {
+// 			return true
+// 		}
+// 	}
+// 	return false
+// }
+//
+// func hasUnfinishedToolCalls(messages []message.Message) bool {
+// 	toolCalls := make([]message.ToolCall, 0)
+// 	for _, m := range messages {
+// 		toolCalls = append(toolCalls, m.ToolCalls()...)
+// 	}
+// 	for _, v := range toolCalls {
+// 		if !v.Finished {
+// 			return true
+// 		}
+// 	}
+// 	return false
+// }
+//
+// func (m *messagesCmp) working() string {
+// 	text := ""
+// 	if m.IsAgentWorking() && len(m.messages) > 0 {
+// 		t := theme.CurrentTheme()
+// 		baseStyle := styles.BaseStyle()
+//
+// 		task := "Thinking..."
+// 		lastMessage := m.messages[len(m.messages)-1]
+// 		if hasToolsWithoutResponse(m.messages) {
+// 			task = "Waiting for tool response..."
+// 		} else if hasUnfinishedToolCalls(m.messages) {
+// 			task = "Building tool call..."
+// 		} else if !lastMessage.IsFinished() {
+// 			task = "Generating..."
+// 		}
+// 		if task != "" {
+// 			text += baseStyle.
+// 				Width(m.width).
+// 				Foreground(t.Primary()).
+// 				Bold(true).
+// 				Render(fmt.Sprintf("%s %s ", m.spinner.View(), task))
+// 		}
+// 	}
+// 	return text
+// }
+//
+// func (m *messagesCmp) help() string {
+// 	t := theme.CurrentTheme()
+// 	baseStyle := styles.BaseStyle()
+//
+// 	text := ""
+//
+// 	if m.app.CoderAgent.IsBusy() {
+// 		text += lipgloss.JoinHorizontal(
+// 			lipgloss.Left,
+// 			baseStyle.Foreground(t.TextMuted()).Bold(true).Render("press "),
+// 			baseStyle.Foreground(t.Text()).Bold(true).Render("esc"),
+// 			baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" to exit cancel"),
+// 		)
+// 	} else {
+// 		text += lipgloss.JoinHorizontal(
+// 			lipgloss.Left,
+// 			baseStyle.Foreground(t.TextMuted()).Bold(true).Render("press "),
+// 			baseStyle.Foreground(t.Text()).Bold(true).Render("enter"),
+// 			baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" to send the message,"),
+// 			baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" write"),
+// 			baseStyle.Foreground(t.Text()).Bold(true).Render(" \\"),
+// 			baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" and enter to add a new line"),
+// 		)
+// 	}
+// 	return baseStyle.
+// 		Width(m.width).
+// 		Render(text)
+// }
+//
+// func (m *messagesCmp) rerender() {
+// 	for _, msg := range m.messages {
+// 		delete(m.cachedContent, msg.ID)
+// 	}
+// 	m.renderView()
+// }
+//
+// func (m *messagesCmp) SetSize(width, height int) tea.Cmd {
+// 	if m.width == width && m.height == height {
+// 		return nil
+// 	}
+// 	m.width = width
+// 	m.height = height
+// 	m.viewport.SetWidth(width)
+// 	m.viewport.SetHeight(height - 2)
+// 	m.attachments.SetWidth(width + 40)
+// 	m.attachments.SetHeight(3)
+// 	m.rerender()
+// 	return nil
+// }
+//
+// func (m *messagesCmp) GetSize() (int, int) {
+// 	return m.width, m.height
+// }
+//
+// func (m *messagesCmp) SetSession(session session.Session) tea.Cmd {
+// 	if m.session.ID == session.ID {
+// 		return nil
+// 	}
+// 	m.session = session
+// 	messages, err := m.app.Messages.List(context.Background(), session.ID)
+// 	if err != nil {
+// 		return util.ReportError(err)
+// 	}
+// 	m.messages = messages
+// 	if len(m.messages) > 0 {
+// 		m.currentMsgID = m.messages[len(m.messages)-1].ID
+// 	}
+// 	delete(m.cachedContent, m.currentMsgID)
+// 	m.rendering = true
+// 	return func() tea.Msg {
+// 		m.renderView()
+// 		return renderFinishedMsg{}
+// 	}
+// }
+//
+// func (m *messagesCmp) BindingKeys() []key.Binding {
+// 	return []key.Binding{
+// 		m.viewport.KeyMap.PageDown,
+// 		m.viewport.KeyMap.PageUp,
+// 		m.viewport.KeyMap.HalfPageUp,
+// 		m.viewport.KeyMap.HalfPageDown,
+// 	}
+// }
+//
+// func NewMessagesCmp(app *app.App) util.Model {
+// 	s := spinner.New()
+// 	s.Spinner = spinner.Pulse
+// 	vp := viewport.New()
+// 	attachmets := viewport.New()
+// 	vp.KeyMap.PageUp = messageKeys.PageUp
+// 	vp.KeyMap.PageDown = messageKeys.PageDown
+// 	vp.KeyMap.HalfPageUp = messageKeys.HalfPageUp
+// 	vp.KeyMap.HalfPageDown = messageKeys.HalfPageDown
+// 	return &messagesCmp{
+// 		app:           app,
+// 		cachedContent: make(map[string]cacheItem),
+// 		viewport:      vp,
+// 		spinner:       s,
+// 		attachments:   attachmets,
+// 	}
+// }

internal/tui/components/chat/list_v2.go 🔗

@@ -106,6 +106,8 @@ func (m *messageListCmp) handleMessageEvent(event pubsub.Event[message.Message])
 			return m.handleNewUserMessage(event.Payload)
 		case message.Assistant:
 			return m.handleNewAssistantMessage(event.Payload)
+		case message.Tool:
+			return m.handleToolMessage(event.Payload)
 		}
 		// TODO: handle tools
 	case pubsub.UpdatedEvent:
@@ -119,6 +121,10 @@ func (m *messageListCmp) handleNewUserMessage(msg message.Message) tea.Cmd {
 	return m.listCmp.AppendItem(messages.NewMessageCmp(msg))
 }
 
+func (m *messageListCmp) handleToolMessage(msg message.Message) tea.Cmd {
+	return nil
+}
+
 func (m *messageListCmp) handleUpdateAssistantMessage(msg message.Message) tea.Cmd {
 	// Simple update the content
 	items := m.listCmp.Items()
@@ -136,6 +142,8 @@ func (m *messageListCmp) handleUpdateAssistantMessage(msg message.Message) tea.C
 				messages.WithLastUserMessageTime(time.Unix(m.lastUserMessageTime, 0)),
 			),
 		)
+	} else if len(msg.ToolCalls()) > 0 && msg.Content().Text == "" {
+		m.listCmp.DeleteItem(len(items) - 1)
 	}
 	return nil
 }

internal/tui/components/chat/message.go 🔗

@@ -1,629 +0,0 @@
-package chat
-
-import (
-	"context"
-	"encoding/json"
-	"fmt"
-	"path/filepath"
-	"strings"
-	"time"
-
-	"github.com/charmbracelet/lipgloss/v2"
-	"github.com/charmbracelet/x/ansi"
-	"github.com/opencode-ai/opencode/internal/config"
-	"github.com/opencode-ai/opencode/internal/diff"
-	"github.com/opencode-ai/opencode/internal/llm/agent"
-	"github.com/opencode-ai/opencode/internal/llm/models"
-	"github.com/opencode-ai/opencode/internal/llm/tools"
-	"github.com/opencode-ai/opencode/internal/message"
-	"github.com/opencode-ai/opencode/internal/tui/styles"
-	"github.com/opencode-ai/opencode/internal/tui/theme"
-)
-
-type uiMessageType int
-
-const (
-	userMessageType uiMessageType = iota
-	assistantMessageType
-	toolMessageType
-
-	maxResultHeight = 10
-)
-
-type uiMessage struct {
-	ID          string
-	messageType uiMessageType
-	position    int
-	height      int
-	content     string
-}
-
-func toMarkdown(content string, focused bool, width int) string {
-	r := styles.GetMarkdownRenderer(width)
-	rendered, _ := r.Render(content)
-	return rendered
-}
-
-func renderMessage(msg string, isUser bool, isFocused bool, width int, info ...string) string {
-	t := theme.CurrentTheme()
-
-	style := styles.BaseStyle().
-		Width(width - 1).
-		BorderLeft(true).
-		Foreground(t.TextMuted()).
-		BorderForeground(t.Primary()).
-		BorderStyle(lipgloss.ThickBorder())
-
-	if isUser {
-		style = style.BorderForeground(t.Secondary())
-	}
-
-	// Apply markdown formatting and handle background color
-	parts := []string{
-		toMarkdown(msg, isFocused, width),
-	}
-
-	// Remove newline at the end
-	parts[0] = strings.TrimSuffix(parts[0], "\n")
-	if len(info) > 0 {
-		parts = append(parts, info...)
-	}
-
-	rendered := style.Render(
-		lipgloss.JoinVertical(
-			lipgloss.Left,
-			parts...,
-		),
-	)
-
-	return rendered
-}
-
-func renderUserMessage(msg message.Message, isFocused bool, width int, position int) uiMessage {
-	var styledAttachments []string
-	t := theme.CurrentTheme()
-	attachmentStyles := styles.BaseStyle().
-		MarginLeft(1).
-		Background(t.TextMuted()).
-		Foreground(t.Text())
-	for _, attachment := range msg.BinaryContent() {
-		file := filepath.Base(attachment.Path)
-		var filename string
-		if len(file) > 10 {
-			filename = fmt.Sprintf(" %s %s...", styles.DocumentIcon, file[0:7])
-		} else {
-			filename = fmt.Sprintf(" %s %s", styles.DocumentIcon, file)
-		}
-		styledAttachments = append(styledAttachments, attachmentStyles.Render(filename))
-	}
-	content := ""
-	if len(styledAttachments) > 0 {
-		attachmentContent := styles.BaseStyle().Width(width).Render(lipgloss.JoinHorizontal(lipgloss.Left, styledAttachments...))
-		content = renderMessage(msg.Content().String(), true, isFocused, width, attachmentContent)
-	} else {
-		content = renderMessage(msg.Content().String(), true, isFocused, width)
-	}
-	userMsg := uiMessage{
-		ID:          msg.ID,
-		messageType: userMessageType,
-		position:    position,
-		height:      lipgloss.Height(content),
-		content:     content,
-	}
-	return userMsg
-}
-
-// Returns multiple uiMessages because of the tool calls
-func renderAssistantMessage(
-	msg message.Message,
-	msgIndex int,
-	allMessages []message.Message, // we need this to get tool results and the user message
-	messagesService message.Service, // We need this to get the task tool messages
-	focusedUIMessageId string,
-	isSummary bool,
-	width int,
-	position int,
-) []uiMessage {
-	messages := []uiMessage{}
-	content := msg.Content().String()
-	thinking := msg.IsThinking()
-	thinkingContent := msg.ReasoningContent().Thinking
-	finished := msg.IsFinished()
-	finishData := msg.FinishPart()
-	info := []string{}
-
-	t := theme.CurrentTheme()
-	baseStyle := styles.BaseStyle()
-
-	// Add finish info if available
-	if finished {
-		switch finishData.Reason {
-		case message.FinishReasonEndTurn:
-			took := formatTimestampDiff(msg.CreatedAt, finishData.Time)
-			info = append(info, baseStyle.
-				Width(width-1).
-				Foreground(t.TextMuted()).
-				Render(fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, took)),
-			)
-		case message.FinishReasonCanceled:
-			info = append(info, baseStyle.
-				Width(width-1).
-				Foreground(t.TextMuted()).
-				Render(fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, "canceled")),
-			)
-		case message.FinishReasonError:
-			info = append(info, baseStyle.
-				Width(width-1).
-				Foreground(t.TextMuted()).
-				Render(fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, "error")),
-			)
-		case message.FinishReasonPermissionDenied:
-			info = append(info, baseStyle.
-				Width(width-1).
-				Foreground(t.TextMuted()).
-				Render(fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, "permission denied")),
-			)
-		}
-	}
-	if content != "" || (finished && finishData.Reason == message.FinishReasonEndTurn) {
-		if content == "" {
-			content = "*Finished without output*"
-		}
-		if isSummary {
-			info = append(info, baseStyle.Width(width-1).Foreground(t.TextMuted()).Render(" (summary)"))
-		}
-
-		content = renderMessage(content, false, true, width, info...)
-		messages = append(messages, uiMessage{
-			ID:          msg.ID,
-			messageType: assistantMessageType,
-			position:    position,
-			height:      lipgloss.Height(content),
-			content:     content,
-		})
-		position += messages[0].height
-		position++ // for the space
-	} else if thinking && thinkingContent != "" {
-		// Render the thinking content
-		content = renderMessage(thinkingContent, false, msg.ID == focusedUIMessageId, width)
-	}
-
-	for i, toolCall := range msg.ToolCalls() {
-		toolCallContent := renderToolMessage(
-			toolCall,
-			allMessages,
-			messagesService,
-			focusedUIMessageId,
-			false,
-			width,
-			i+1,
-		)
-		messages = append(messages, toolCallContent)
-		position += toolCallContent.height
-		position++ // for the space
-	}
-	return messages
-}
-
-func findToolResponse(toolCallID string, futureMessages []message.Message) *message.ToolResult {
-	for _, msg := range futureMessages {
-		for _, result := range msg.ToolResults() {
-			if result.ToolCallID == toolCallID {
-				return &result
-			}
-		}
-	}
-	return nil
-}
-
-func toolName(name string) string {
-	switch name {
-	case agent.AgentToolName:
-		return "Task"
-	case tools.BashToolName:
-		return "Bash"
-	case tools.EditToolName:
-		return "Edit"
-	case tools.FetchToolName:
-		return "Fetch"
-	case tools.GlobToolName:
-		return "Glob"
-	case tools.GrepToolName:
-		return "Grep"
-	case tools.LSToolName:
-		return "List"
-	case tools.SourcegraphToolName:
-		return "Sourcegraph"
-	case tools.ViewToolName:
-		return "View"
-	case tools.WriteToolName:
-		return "Write"
-	case tools.PatchToolName:
-		return "Patch"
-	}
-	return name
-}
-
-func getToolAction(name string) string {
-	switch name {
-	case agent.AgentToolName:
-		return "Preparing prompt..."
-	case tools.BashToolName:
-		return "Building command..."
-	case tools.EditToolName:
-		return "Preparing edit..."
-	case tools.FetchToolName:
-		return "Writing fetch..."
-	case tools.GlobToolName:
-		return "Finding files..."
-	case tools.GrepToolName:
-		return "Searching content..."
-	case tools.LSToolName:
-		return "Listing directory..."
-	case tools.SourcegraphToolName:
-		return "Searching code..."
-	case tools.ViewToolName:
-		return "Reading file..."
-	case tools.WriteToolName:
-		return "Preparing write..."
-	case tools.PatchToolName:
-		return "Preparing patch..."
-	}
-	return "Working..."
-}
-
-func renderToolParams(paramWidth int, toolCall message.ToolCall) string {
-	params := ""
-	switch toolCall.Name {
-	case agent.AgentToolName:
-		var params agent.AgentParams
-		json.Unmarshal([]byte(toolCall.Input), &params)
-		prompt := strings.ReplaceAll(params.Prompt, "\n", " ")
-		return renderParams(paramWidth, prompt)
-	case tools.BashToolName:
-		var params tools.BashParams
-		json.Unmarshal([]byte(toolCall.Input), &params)
-		command := strings.ReplaceAll(params.Command, "\n", " ")
-		return renderParams(paramWidth, command)
-	case tools.EditToolName:
-		var params tools.EditParams
-		json.Unmarshal([]byte(toolCall.Input), &params)
-		filePath := removeWorkingDirPrefix(params.FilePath)
-		return renderParams(paramWidth, filePath)
-	case tools.FetchToolName:
-		var params tools.FetchParams
-		json.Unmarshal([]byte(toolCall.Input), &params)
-		url := params.URL
-		toolParams := []string{
-			url,
-		}
-		if params.Format != "" {
-			toolParams = append(toolParams, "format", params.Format)
-		}
-		if params.Timeout != 0 {
-			toolParams = append(toolParams, "timeout", (time.Duration(params.Timeout) * time.Second).String())
-		}
-		return renderParams(paramWidth, toolParams...)
-	case tools.GlobToolName:
-		var params tools.GlobParams
-		json.Unmarshal([]byte(toolCall.Input), &params)
-		pattern := params.Pattern
-		toolParams := []string{
-			pattern,
-		}
-		if params.Path != "" {
-			toolParams = append(toolParams, "path", params.Path)
-		}
-		return renderParams(paramWidth, toolParams...)
-	case tools.GrepToolName:
-		var params tools.GrepParams
-		json.Unmarshal([]byte(toolCall.Input), &params)
-		pattern := params.Pattern
-		toolParams := []string{
-			pattern,
-		}
-		if params.Path != "" {
-			toolParams = append(toolParams, "path", params.Path)
-		}
-		if params.Include != "" {
-			toolParams = append(toolParams, "include", params.Include)
-		}
-		if params.LiteralText {
-			toolParams = append(toolParams, "literal", "true")
-		}
-		return renderParams(paramWidth, toolParams...)
-	case tools.LSToolName:
-		var params tools.LSParams
-		json.Unmarshal([]byte(toolCall.Input), &params)
-		path := params.Path
-		if path == "" {
-			path = "."
-		}
-		return renderParams(paramWidth, path)
-	case tools.SourcegraphToolName:
-		var params tools.SourcegraphParams
-		json.Unmarshal([]byte(toolCall.Input), &params)
-		return renderParams(paramWidth, params.Query)
-	case tools.ViewToolName:
-		var params tools.ViewParams
-		json.Unmarshal([]byte(toolCall.Input), &params)
-		filePath := removeWorkingDirPrefix(params.FilePath)
-		toolParams := []string{
-			filePath,
-		}
-		if params.Limit != 0 {
-			toolParams = append(toolParams, "limit", fmt.Sprintf("%d", params.Limit))
-		}
-		if params.Offset != 0 {
-			toolParams = append(toolParams, "offset", fmt.Sprintf("%d", params.Offset))
-		}
-		return renderParams(paramWidth, toolParams...)
-	case tools.WriteToolName:
-		var params tools.WriteParams
-		json.Unmarshal([]byte(toolCall.Input), &params)
-		filePath := removeWorkingDirPrefix(params.FilePath)
-		return renderParams(paramWidth, filePath)
-	default:
-		input := strings.ReplaceAll(toolCall.Input, "\n", " ")
-		params = renderParams(paramWidth, input)
-	}
-	return params
-}
-
-func renderToolResponse(toolCall message.ToolCall, response message.ToolResult, width int) string {
-	t := theme.CurrentTheme()
-	baseStyle := styles.BaseStyle()
-
-	if response.IsError {
-		errContent := fmt.Sprintf("Error: %s", strings.ReplaceAll(response.Content, "\n", " "))
-		errContent = ansi.Truncate(errContent, width-1, "...")
-		return baseStyle.
-			Width(width).
-			Foreground(t.Error()).
-			Render(errContent)
-	}
-
-	resultContent := truncateHeight(response.Content, maxResultHeight)
-	switch toolCall.Name {
-	case agent.AgentToolName:
-		return toMarkdown(resultContent, false, width)
-	case tools.BashToolName:
-		resultContent = fmt.Sprintf("```bash\n%s\n```", resultContent)
-		return toMarkdown(resultContent, true, width)
-	case tools.EditToolName:
-		metadata := tools.EditResponseMetadata{}
-		json.Unmarshal([]byte(response.Metadata), &metadata)
-		truncDiff := truncateHeight(metadata.Diff, maxResultHeight)
-		formattedDiff, _ := diff.FormatDiff(truncDiff, diff.WithTotalWidth(width))
-		return formattedDiff
-	case tools.FetchToolName:
-		var params tools.FetchParams
-		json.Unmarshal([]byte(toolCall.Input), &params)
-		mdFormat := "markdown"
-		switch params.Format {
-		case "text":
-			mdFormat = "text"
-		case "html":
-			mdFormat = "html"
-		}
-		resultContent = fmt.Sprintf("```%s\n%s\n```", mdFormat, resultContent)
-		return toMarkdown(resultContent, true, width)
-	case tools.GlobToolName:
-		return baseStyle.Width(width).Foreground(t.TextMuted()).Render(resultContent)
-	case tools.GrepToolName:
-		return baseStyle.Width(width).Foreground(t.TextMuted()).Render(resultContent)
-	case tools.LSToolName:
-		return baseStyle.Width(width).Foreground(t.TextMuted()).Render(resultContent)
-	case tools.SourcegraphToolName:
-		return baseStyle.Width(width).Foreground(t.TextMuted()).Render(resultContent)
-	case tools.ViewToolName:
-		metadata := tools.ViewResponseMetadata{}
-		json.Unmarshal([]byte(response.Metadata), &metadata)
-		ext := filepath.Ext(metadata.FilePath)
-		if ext == "" {
-			ext = ""
-		} else {
-			ext = strings.ToLower(ext[1:])
-		}
-		resultContent = fmt.Sprintf("```%s\n%s\n```", ext, truncateHeight(metadata.Content, maxResultHeight))
-		return toMarkdown(resultContent, true, width)
-	case tools.WriteToolName:
-		params := tools.WriteParams{}
-		json.Unmarshal([]byte(toolCall.Input), &params)
-		metadata := tools.WriteResponseMetadata{}
-		json.Unmarshal([]byte(response.Metadata), &metadata)
-		ext := filepath.Ext(params.FilePath)
-		if ext == "" {
-			ext = ""
-		} else {
-			ext = strings.ToLower(ext[1:])
-		}
-		resultContent = fmt.Sprintf("```%s\n%s\n```", ext, truncateHeight(params.Content, maxResultHeight))
-		return toMarkdown(resultContent, true, width)
-	default:
-		resultContent = fmt.Sprintf("```text\n%s\n```", resultContent)
-		return toMarkdown(resultContent, true, width)
-	}
-}
-
-func renderToolMessage(
-	toolCall message.ToolCall,
-	allMessages []message.Message,
-	messagesService message.Service,
-	focusedUIMessageId string,
-	nested bool,
-	width int,
-	position int,
-) uiMessage {
-	if nested {
-		width = width - 3
-	}
-
-	t := theme.CurrentTheme()
-	baseStyle := styles.BaseStyle()
-
-	style := baseStyle.
-		Width(width - 1).
-		BorderLeft(true).
-		BorderStyle(lipgloss.ThickBorder()).
-		PaddingLeft(1).
-		BorderForeground(t.TextMuted())
-
-	response := findToolResponse(toolCall.ID, allMessages)
-	toolNameText := baseStyle.Foreground(t.TextMuted()).
-		Render(fmt.Sprintf("%s: ", toolName(toolCall.Name)))
-
-	if !toolCall.Finished {
-		// Get a brief description of what the tool is doing
-		toolAction := getToolAction(toolCall.Name)
-
-		progressText := baseStyle.
-			Width(width - 2 - lipgloss.Width(toolNameText)).
-			Foreground(t.TextMuted()).
-			Render(fmt.Sprintf("%s", toolAction))
-
-		content := style.Render(lipgloss.JoinHorizontal(lipgloss.Left, toolNameText, progressText))
-		toolMsg := uiMessage{
-			messageType: toolMessageType,
-			position:    position,
-			height:      lipgloss.Height(content),
-			content:     content,
-		}
-		return toolMsg
-	}
-
-	params := renderToolParams(width-2-lipgloss.Width(toolNameText), toolCall)
-	responseContent := ""
-	if response != nil {
-		responseContent = renderToolResponse(toolCall, *response, width-2)
-		responseContent = strings.TrimSuffix(responseContent, "\n")
-	} else {
-		responseContent = baseStyle.
-			Italic(true).
-			Width(width - 2).
-			Foreground(t.TextMuted()).
-			Render("Waiting for response...")
-	}
-
-	parts := []string{}
-	if !nested {
-		formattedParams := baseStyle.
-			Width(width - 2 - lipgloss.Width(toolNameText)).
-			Foreground(t.TextMuted()).
-			Render(params)
-
-		parts = append(parts, lipgloss.JoinHorizontal(lipgloss.Left, toolNameText, formattedParams))
-	} else {
-		prefix := baseStyle.
-			Foreground(t.TextMuted()).
-			Render(" └ ")
-		formattedParams := baseStyle.
-			Width(width - 2 - lipgloss.Width(toolNameText)).
-			Foreground(t.TextMuted()).
-			Render(params)
-		parts = append(parts, lipgloss.JoinHorizontal(lipgloss.Left, prefix, toolNameText, formattedParams))
-	}
-
-	if toolCall.Name == agent.AgentToolName {
-		taskMessages, _ := messagesService.List(context.Background(), toolCall.ID)
-		toolCalls := []message.ToolCall{}
-		for _, v := range taskMessages {
-			toolCalls = append(toolCalls, v.ToolCalls()...)
-		}
-		for _, call := range toolCalls {
-			rendered := renderToolMessage(call, []message.Message{}, messagesService, focusedUIMessageId, true, width, 0)
-			parts = append(parts, rendered.content)
-		}
-	}
-	if responseContent != "" && !nested {
-		parts = append(parts, responseContent)
-	}
-
-	content := style.Render(
-		lipgloss.JoinVertical(
-			lipgloss.Left,
-			parts...,
-		),
-	)
-	if nested {
-		content = lipgloss.JoinVertical(
-			lipgloss.Left,
-			parts...,
-		)
-	}
-	toolMsg := uiMessage{
-		messageType: toolMessageType,
-		position:    position,
-		height:      lipgloss.Height(content),
-		content:     content,
-	}
-	return toolMsg
-}
-
-func removeWorkingDirPrefix(path string) string {
-	wd := config.WorkingDirectory()
-	path = strings.TrimPrefix(path, wd)
-	return path
-}
-
-func truncateHeight(content string, height int) string {
-	lines := strings.Split(content, "\n")
-	if len(lines) > height {
-		return strings.Join(lines[:height], "\n")
-	}
-	return content
-}
-
-func renderParams(paramsWidth int, params ...string) string {
-	if len(params) == 0 {
-		return ""
-	}
-	mainParam := params[0]
-	if len(mainParam) > paramsWidth {
-		mainParam = mainParam[:paramsWidth-3] + "..."
-	}
-
-	if len(params) == 1 {
-		return mainParam
-	}
-	otherParams := params[1:]
-	// create pairs of key/value
-	// if odd number of params, the last one is a key without value
-	if len(otherParams)%2 != 0 {
-		otherParams = append(otherParams, "")
-	}
-	parts := make([]string, 0, len(otherParams)/2)
-	for i := 0; i < len(otherParams); i += 2 {
-		key := otherParams[i]
-		value := otherParams[i+1]
-		if value == "" {
-			continue
-		}
-		parts = append(parts, fmt.Sprintf("%s=%s", key, value))
-	}
-
-	partsRendered := strings.Join(parts, ", ")
-	remainingWidth := paramsWidth - lipgloss.Width(partsRendered) - 3 // count for " ()"
-	if remainingWidth < 30 {
-		// No space for the params, just show the main
-		return mainParam
-	}
-
-	if len(parts) > 0 {
-		mainParam = fmt.Sprintf("%s (%s)", mainParam, strings.Join(parts, ", "))
-	}
-
-	return ansi.Truncate(mainParam, paramsWidth, "...")
-}
-
-// Helper function to format the time difference between two Unix timestamps
-func formatTimestampDiff(start, end int64) string {
-	diffSeconds := float64(end-start) / 1000.0 // Convert to seconds
-	if diffSeconds < 1 {
-		return fmt.Sprintf("%dms", int(diffSeconds*1000))
-	}
-	if diffSeconds < 60 {
-		return fmt.Sprintf("%.1fs", diffSeconds)
-	}
-	return fmt.Sprintf("%.1fm", diffSeconds/60)
-}

internal/tui/components/core/list/list.go 🔗

@@ -492,6 +492,7 @@ func (m *model) rerenderItem(inx int) {
 	}
 	// check if the item is in the content
 	start := cachedItem.start
+	logging.Info("rerenderItem", "inx", inx, "start", start, "cachedItem.start", cachedItem.start, "cachedItem.height", cachedItem.height)
 	end := start + cachedItem.height
 	totalLines := len(m.renderedLines)
 	if m.reverse {
@@ -504,6 +505,9 @@ func (m *model) rerenderItem(inx int) {
 	}
 	// TODO: if hight changed do something
 	if cachedItem.height != len(rerenderedLines) && inx != len(m.items)-1 {
+		if inx == len(m.items)-1 {
+			m.finalHight = max(0, start+len(rerenderedLines)-m.listHeight())
+		}
 	}
 	m.renderedItems.Store(inx, renderedItem{
 		lines:  rerenderedLines,
@@ -541,7 +545,12 @@ func (m *model) decreaseOffset(n int) {
 // UpdateItem implements List.
 func (m *model) UpdateItem(inx int, item util.Model) {
 	m.items[inx] = item
-	m.rerenderItem(inx)
+	if m.selectedItemInx == inx {
+		if i, ok := m.items[m.selectedItemInx].(layout.Focusable); ok {
+			i.Focus()
+		}
+	}
+	m.ResetView()
 	m.needsRerender = true
 }
 
@@ -614,6 +623,10 @@ func (m *model) AppendItem(item util.Model) tea.Cmd {
 func (m *model) DeleteItem(i int) {
 	m.items = slices.Delete(m.items, i, i+1)
 	m.renderedItems.Delete(i)
+	if m.selectedItemInx == i {
+		m.selectedItemInx--
+	}
+	m.ResetView()
 	m.needsRerender = true
 }