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