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