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/pubsub"
 12	"github.com/opencode-ai/opencode/internal/session"
 13	"github.com/opencode-ai/opencode/internal/tui/components/chat/messages"
 14	"github.com/opencode-ai/opencode/internal/tui/components/core/list"
 15	"github.com/opencode-ai/opencode/internal/tui/components/dialog"
 16	"github.com/opencode-ai/opencode/internal/tui/layout"
 17	"github.com/opencode-ai/opencode/internal/tui/util"
 18)
 19
 20type MessageListCmp interface {
 21	util.Model
 22	layout.Sizeable
 23}
 24
 25type messageListCmp struct {
 26	app           *app.App
 27	width, height int
 28	session       session.Session
 29	listCmp       list.ListModel
 30
 31	lastUserMessageTime int64
 32}
 33
 34func NewMessagesListCmp(app *app.App) MessageListCmp {
 35	return &messageListCmp{
 36		app: app,
 37		listCmp: list.New(
 38			list.WithGapSize(1),
 39			list.WithReverse(true),
 40		),
 41	}
 42}
 43
 44func (m *messageListCmp) Init() tea.Cmd {
 45	return nil
 46}
 47
 48func (m *messageListCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 49	switch msg := msg.(type) {
 50	case dialog.ThemeChangedMsg:
 51		m.listCmp.ResetView()
 52		return m, nil
 53	case SessionSelectedMsg:
 54		if msg.ID != m.session.ID {
 55			cmd := m.SetSession(msg)
 56			return m, cmd
 57		}
 58		return m, nil
 59	case SessionClearedMsg:
 60		m.session = session.Session{}
 61		return m, m.listCmp.SetItems([]util.Model{})
 62
 63	case pubsub.Event[message.Message]:
 64		return m, m.handleMessageEvent(msg)
 65	default:
 66		var cmds []tea.Cmd
 67		u, cmd := m.listCmp.Update(msg)
 68		m.listCmp = u.(list.ListModel)
 69		cmds = append(cmds, cmd)
 70		return m, tea.Batch(cmds...)
 71	}
 72}
 73
 74func (m *messageListCmp) View() string {
 75	if len(m.listCmp.Items()) == 0 {
 76		return initialScreen()
 77	}
 78	return lipgloss.JoinVertical(lipgloss.Left, m.listCmp.View())
 79}
 80
 81func (m *messageListCmp) handleChildSession(event pubsub.Event[message.Message]) {
 82	// TODO: update the agent tool message with the changes
 83}
 84
 85func (m *messageListCmp) handleMessageEvent(event pubsub.Event[message.Message]) tea.Cmd {
 86	switch event.Type {
 87	case pubsub.CreatedEvent:
 88		if event.Payload.SessionID != m.session.ID {
 89			m.handleChildSession(event)
 90		}
 91		messageExists := false
 92		// more likely to be at the end of the list
 93		items := m.listCmp.Items()
 94		for i := len(items) - 1; i >= 0; i-- {
 95			msg := items[i].(messages.MessageCmp)
 96			if msg.GetMessage().ID == event.Payload.ID {
 97				messageExists = true
 98				break
 99			}
100		}
101		if messageExists {
102			return nil
103		}
104		switch event.Payload.Role {
105		case message.User:
106			return m.handleNewUserMessage(event.Payload)
107		case message.Assistant:
108			return m.handleNewAssistantMessage(event.Payload)
109		}
110		// TODO: handle tools
111	case pubsub.UpdatedEvent:
112		return m.handleUpdateAssistantMessage(event.Payload)
113	}
114	return nil
115}
116
117func (m *messageListCmp) handleNewUserMessage(msg message.Message) tea.Cmd {
118	m.lastUserMessageTime = msg.CreatedAt
119	return m.listCmp.AppendItem(messages.NewMessageCmp(msg))
120}
121
122func (m *messageListCmp) handleUpdateAssistantMessage(msg message.Message) tea.Cmd {
123	// Simple update the content
124	items := m.listCmp.Items()
125	lastItem := items[len(items)-1].(messages.MessageCmp)
126	// TODO:handle tool calls
127	if lastItem.GetMessage().ID != msg.ID {
128		return nil
129	}
130	// for now just updet the last message
131	if len(msg.ToolCalls()) == 0 || msg.Content().Text != "" || msg.IsThinking() {
132		m.listCmp.UpdateItem(
133			len(items)-1,
134			messages.NewMessageCmp(
135				msg,
136				messages.WithLastUserMessageTime(time.Unix(m.lastUserMessageTime, 0)),
137			),
138		)
139	}
140	return nil
141}
142
143func (m *messageListCmp) handleNewAssistantMessage(msg message.Message) tea.Cmd {
144	var cmds []tea.Cmd
145	// Only add assistant messages if they don't have tool calls or there is some content
146	if len(msg.ToolCalls()) == 0 || msg.Content().Text != "" || msg.IsThinking() {
147		cmd := m.listCmp.AppendItem(
148			messages.NewMessageCmp(
149				msg,
150				messages.WithLastUserMessageTime(time.Unix(m.lastUserMessageTime, 0)),
151			),
152		)
153		cmds = append(cmds, cmd)
154	}
155	for _, tc := range msg.ToolCalls() {
156		cmd := m.listCmp.AppendItem(messages.NewToolCallCmp(tc))
157		cmds = append(cmds, cmd)
158	}
159	return tea.Batch(cmds...)
160}
161
162func (m *messageListCmp) SetSession(session session.Session) tea.Cmd {
163	if m.session.ID == session.ID {
164		return nil
165	}
166	m.session = session
167	sessionMessages, err := m.app.Messages.List(context.Background(), session.ID)
168	if err != nil {
169		return util.ReportError(err)
170	}
171	uiMessages := make([]util.Model, 0)
172	m.lastUserMessageTime = sessionMessages[0].CreatedAt
173	toolResultMap := make(map[string]message.ToolResult)
174	// first pass to get all tool results
175	for _, msg := range sessionMessages {
176		for _, tr := range msg.ToolResults() {
177			toolResultMap[tr.ToolCallID] = tr
178		}
179	}
180	for _, msg := range sessionMessages {
181		switch msg.Role {
182		case message.User:
183			m.lastUserMessageTime = msg.CreatedAt
184			uiMessages = append(uiMessages, messages.NewMessageCmp(msg))
185		case message.Assistant:
186			// Only add assistant messages if they don't have tool calls or there is some content
187			if len(msg.ToolCalls()) == 0 || msg.Content().Text != "" || msg.IsThinking() {
188				uiMessages = append(
189					uiMessages,
190					messages.NewMessageCmp(
191						msg,
192						messages.WithLastUserMessageTime(time.Unix(m.lastUserMessageTime, 0)),
193					),
194				)
195			}
196			for _, tc := range msg.ToolCalls() {
197				options := []messages.ToolCallOption{}
198				if tr, ok := toolResultMap[tc.ID]; ok {
199					options = append(options, messages.WithToolCallResult(tr))
200				}
201				if msg.FinishPart().Reason == message.FinishReasonCanceled {
202					options = append(options, messages.WithToolCallCancelled())
203				}
204				uiMessages = append(uiMessages, messages.NewToolCallCmp(tc, options...))
205			}
206		}
207	}
208	return m.listCmp.SetItems(uiMessages)
209}
210
211// GetSize implements MessageListCmp.
212func (m *messageListCmp) GetSize() (int, int) {
213	return m.width, m.height
214}
215
216// SetSize implements MessageListCmp.
217func (m *messageListCmp) SetSize(width int, height int) tea.Cmd {
218	m.width = width
219	m.height = height - 1
220	return m.listCmp.SetSize(width, height-1)
221}