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}