list_v2.go

  1package chat
  2
  3import (
  4	"context"
  5	"time"
  6
  7	tea "github.com/charmbracelet/bubbletea/v2"
  8	"github.com/charmbracelet/lipgloss/v2"
  9	"github.com/opencode-ai/opencode/internal/app"
 10	"github.com/opencode-ai/opencode/internal/message"
 11	"github.com/opencode-ai/opencode/internal/session"
 12	"github.com/opencode-ai/opencode/internal/tui/components/chat/messages"
 13	"github.com/opencode-ai/opencode/internal/tui/components/core/list"
 14	"github.com/opencode-ai/opencode/internal/tui/components/dialog"
 15	"github.com/opencode-ai/opencode/internal/tui/layout"
 16	"github.com/opencode-ai/opencode/internal/tui/util"
 17)
 18
 19type MessageListCmp interface {
 20	util.Model
 21	layout.Sizeable
 22}
 23
 24type messageListCmp struct {
 25	app           *app.App
 26	width, height int
 27	session       session.Session
 28	messages      []util.Model
 29	listCmp       list.ListModel
 30}
 31
 32func NewMessagesListCmp(app *app.App) MessageListCmp {
 33	return &messageListCmp{
 34		app: app,
 35		listCmp: list.New(
 36			list.WithGapSize(1),
 37			list.WithReverse(true),
 38		),
 39	}
 40}
 41
 42func (m *messageListCmp) Init() tea.Cmd {
 43	return nil
 44}
 45
 46func (m *messageListCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 47	switch msg := msg.(type) {
 48	case dialog.ThemeChangedMsg:
 49		m.listCmp.ResetView()
 50		return m, nil
 51	case SessionSelectedMsg:
 52		if msg.ID != m.session.ID {
 53			cmd := m.SetSession(msg)
 54			return m, cmd
 55		}
 56		return m, nil
 57	default:
 58		var cmds []tea.Cmd
 59		u, cmd := m.listCmp.Update(msg)
 60		m.listCmp = u.(list.ListModel)
 61		cmds = append(cmds, cmd)
 62		return m, tea.Batch(cmds...)
 63	}
 64}
 65
 66func (m *messageListCmp) View() string {
 67	return lipgloss.JoinVertical(lipgloss.Left, m.listCmp.View())
 68}
 69
 70// GetSize implements MessageListCmp.
 71func (m *messageListCmp) GetSize() (int, int) {
 72	return m.width, m.height
 73}
 74
 75// SetSize implements MessageListCmp.
 76func (m *messageListCmp) SetSize(width int, height int) tea.Cmd {
 77	m.width = width
 78	m.height = height - 1
 79	return m.listCmp.SetSize(width, height-1)
 80}
 81
 82func (m *messageListCmp) SetSession(session session.Session) tea.Cmd {
 83	if m.session.ID == session.ID {
 84		return nil
 85	}
 86	m.session = session
 87	sessionMessages, err := m.app.Messages.List(context.Background(), session.ID)
 88	if err != nil {
 89		return util.ReportError(err)
 90	}
 91	m.messages = make([]util.Model, 0)
 92	lastUserMessageTime := sessionMessages[0].CreatedAt
 93	toolResultMap := make(map[string]message.ToolResult)
 94	// first pass to get all tool results
 95	for _, msg := range sessionMessages {
 96		for _, tr := range msg.ToolResults() {
 97			toolResultMap[tr.ToolCallID] = tr
 98		}
 99	}
100	for _, msg := range sessionMessages {
101		switch msg.Role {
102		case message.User:
103			lastUserMessageTime = msg.CreatedAt
104			m.messages = append(m.messages, messages.NewMessageCmp(msg))
105		case message.Assistant:
106			// Only add assistant messages if they don't have tool calls or there is some content
107			if len(msg.ToolCalls()) == 0 || msg.Content().Text != "" || msg.IsThinking() {
108				m.messages = append(m.messages, messages.NewMessageCmp(msg, messages.WithLastUserMessageTime(time.Unix(lastUserMessageTime, 0))))
109			}
110			for _, tc := range msg.ToolCalls() {
111				options := []messages.ToolCallOption{}
112				if tr, ok := toolResultMap[tc.ID]; ok {
113					options = append(options, messages.WithToolCallResult(tr))
114				}
115				if msg.FinishPart().Reason == message.FinishReasonCanceled {
116					options = append(options, messages.WithToolCallCancelled())
117				}
118				m.messages = append(m.messages, messages.NewToolCallCmp(tc, options...))
119			}
120		}
121	}
122	m.listCmp.SetItems(m.messages)
123	return nil
124}