list_v2.go

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