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		case message.Tool:
110			return m.handleToolMessage(event.Payload)
111		}
112		// TODO: handle tools
113	case pubsub.UpdatedEvent:
114		return m.handleUpdateAssistantMessage(event.Payload)
115	}
116	return nil
117}
118
119func (m *messageListCmp) handleNewUserMessage(msg message.Message) tea.Cmd {
120	m.lastUserMessageTime = msg.CreatedAt
121	return m.listCmp.AppendItem(messages.NewMessageCmp(msg))
122}
123
124func (m *messageListCmp) handleToolMessage(msg message.Message) tea.Cmd {
125	return nil
126}
127
128func (m *messageListCmp) handleUpdateAssistantMessage(msg message.Message) tea.Cmd {
129	// Simple update the content
130	items := m.listCmp.Items()
131	lastItem := items[len(items)-1].(messages.MessageCmp)
132	// TODO:handle tool calls
133	if lastItem.GetMessage().ID != msg.ID {
134		return nil
135	}
136	// for now just updet the last message
137	if len(msg.ToolCalls()) == 0 || msg.Content().Text != "" || msg.IsThinking() {
138		m.listCmp.UpdateItem(
139			len(items)-1,
140			messages.NewMessageCmp(
141				msg,
142				messages.WithLastUserMessageTime(time.Unix(m.lastUserMessageTime, 0)),
143			),
144		)
145	} else if len(msg.ToolCalls()) > 0 && msg.Content().Text == "" {
146		m.listCmp.DeleteItem(len(items) - 1)
147	}
148	return nil
149}
150
151func (m *messageListCmp) handleNewAssistantMessage(msg message.Message) tea.Cmd {
152	var cmds []tea.Cmd
153	// Only add assistant messages if they don't have tool calls or there is some content
154	if len(msg.ToolCalls()) == 0 || msg.Content().Text != "" || msg.IsThinking() {
155		cmd := m.listCmp.AppendItem(
156			messages.NewMessageCmp(
157				msg,
158				messages.WithLastUserMessageTime(time.Unix(m.lastUserMessageTime, 0)),
159			),
160		)
161		cmds = append(cmds, cmd)
162	}
163	for _, tc := range msg.ToolCalls() {
164		cmd := m.listCmp.AppendItem(messages.NewToolCallCmp(tc))
165		cmds = append(cmds, cmd)
166	}
167	return tea.Batch(cmds...)
168}
169
170func (m *messageListCmp) SetSession(session session.Session) tea.Cmd {
171	if m.session.ID == session.ID {
172		return nil
173	}
174	m.session = session
175	sessionMessages, err := m.app.Messages.List(context.Background(), session.ID)
176	if err != nil {
177		return util.ReportError(err)
178	}
179	uiMessages := make([]util.Model, 0)
180	m.lastUserMessageTime = sessionMessages[0].CreatedAt
181	toolResultMap := make(map[string]message.ToolResult)
182	// first pass to get all tool results
183	for _, msg := range sessionMessages {
184		for _, tr := range msg.ToolResults() {
185			toolResultMap[tr.ToolCallID] = tr
186		}
187	}
188	for _, msg := range sessionMessages {
189		switch msg.Role {
190		case message.User:
191			m.lastUserMessageTime = msg.CreatedAt
192			uiMessages = append(uiMessages, messages.NewMessageCmp(msg))
193		case message.Assistant:
194			// Only add assistant messages if they don't have tool calls or there is some content
195			if len(msg.ToolCalls()) == 0 || msg.Content().Text != "" || msg.IsThinking() {
196				uiMessages = append(
197					uiMessages,
198					messages.NewMessageCmp(
199						msg,
200						messages.WithLastUserMessageTime(time.Unix(m.lastUserMessageTime, 0)),
201					),
202				)
203			}
204			for _, tc := range msg.ToolCalls() {
205				options := []messages.ToolCallOption{}
206				if tr, ok := toolResultMap[tc.ID]; ok {
207					options = append(options, messages.WithToolCallResult(tr))
208				}
209				if msg.FinishPart().Reason == message.FinishReasonCanceled {
210					options = append(options, messages.WithToolCallCancelled())
211				}
212				uiMessages = append(uiMessages, messages.NewToolCallCmp(tc, options...))
213			}
214		}
215	}
216	return m.listCmp.SetItems(uiMessages)
217}
218
219// GetSize implements MessageListCmp.
220func (m *messageListCmp) GetSize() (int, int) {
221	return m.width, m.height
222}
223
224// SetSize implements MessageListCmp.
225func (m *messageListCmp) SetSize(width int, height int) tea.Cmd {
226	m.width = width
227	m.height = height - 1
228	return m.listCmp.SetSize(width, height-1)
229}