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