on main help sign in
add anim to the messages
25e9651Kujtim Hoxha
internal/tui/components/anim/anim.go+10−8
@@ -216,16 +216,18 @@ func (a anim) View() string {
216 b.WriteRune(c.currentValue)
216 b.WriteRune(c.currentValue)
217 }
217 }
218
218
219+ if len(a.label) > 1 {
219− textStyle := styles.BaseStyle().
220+ textStyle := styles.BaseStyle().
220− Foreground(t.Text())
221+ Foreground(t.Text())
221−
222− for _, c := range a.labelChars {
222+ for _, c := range a.labelChars {
223− b.WriteString(
223+ b.WriteString(
224− textStyle.Render(string(c.currentValue)),
224+ textStyle.Render(string(c.currentValue)),
225− )
225+ )
226+ }
227+ return b.String() + textStyle.Render(a.ellipsis.View())
226 }
228 }
227
229
228− return b.String() + textStyle.Render(a.ellipsis.View())
230+ return b.String()
229}
231}
230
232
231func makeGradientRamp(length int) []color.Color {
233func makeGradientRamp(length int) []color.Color {
internal/tui/components/chat/chat.go+21−17
@@ -5,7 +5,6 @@ import (
5 "sort"
5 "sort"
6
6
7 "github.com/charmbracelet/lipgloss/v2"
7 "github.com/charmbracelet/lipgloss/v2"
8− "github.com/charmbracelet/x/ansi"
9 "github.com/opencode-ai/opencode/internal/config"
8 "github.com/opencode-ai/opencode/internal/config"
10 "github.com/opencode-ai/opencode/internal/message"
9 "github.com/opencode-ai/opencode/internal/message"
11 "github.com/opencode-ai/opencode/internal/session"
10 "github.com/opencode-ai/opencode/internal/session"
@@ -25,26 +24,24 @@ type SessionClearedMsg struct{}
25
24
26type EditorFocusMsg bool
25type EditorFocusMsg bool
27
26
28−func header(width int) string {
27+func header() string {
29 return lipgloss.JoinVertical(
28 return lipgloss.JoinVertical(
30 lipgloss.Top,
29 lipgloss.Top,
31− logo(width),
30+ logo(),
32− repo(width),
31+ repo(),
33 "",
32 "",
34− cwd(width),
33+ cwd(),
35 )
34 )
36}
35}
37
36
38−func lspsConfigured(width int) string {
37+func lspsConfigured() string {
39 cfg := config.Get()
38 cfg := config.Get()
40 title := "LSP Configuration"
39 title := "LSP Configuration"
41− title = ansi.Truncate(title, width, "…")
42
40
43 t := theme.CurrentTheme()
41 t := theme.CurrentTheme()
44 baseStyle := styles.BaseStyle()
42 baseStyle := styles.BaseStyle()
45
43
46 lsps := baseStyle.
44 lsps := baseStyle.
47− Width(width).
48 Foreground(t.Primary()).
45 Foreground(t.Primary()).
49 Bold(true).
46 Bold(true).
50 Render(title)
47 Render(title)
@@ -64,7 +61,6 @@ func lspsConfigured(width int) string {
64 Render(fmt.Sprintf("• %s", name))
61 Render(fmt.Sprintf("• %s", name))
65
62
66 cmd := lsp.Command
63 cmd := lsp.Command
67− cmd = ansi.Truncate(cmd, width-lipgloss.Width(lspName)-3, "…")
68
64
69 lspPath := baseStyle.
65 lspPath := baseStyle.
70 Foreground(t.TextMuted()).
66 Foreground(t.TextMuted()).
@@ -72,7 +68,6 @@ func lspsConfigured(width int) string {
72
68
73 lspViews = append(lspViews,
69 lspViews = append(lspViews,
74 baseStyle.
70 baseStyle.
75− Width(width).
76 Render(
71 Render(
77 lipgloss.JoinHorizontal(
72 lipgloss.JoinHorizontal(
78 lipgloss.Left,
73 lipgloss.Left,
@@ -84,7 +79,6 @@ func lspsConfigured(width int) string {
84 }
79 }
85
80
86 return baseStyle.
81 return baseStyle.
87− Width(width).
88 Render(
82 Render(
89 lipgloss.JoinVertical(
83 lipgloss.JoinVertical(
90 lipgloss.Left,
84 lipgloss.Left,
@@ -97,7 +91,7 @@ func lspsConfigured(width int) string {
97 )
91 )
98}
92}
99
93
100−func logo(width int) string {
94+func logo() string {
101 logo := fmt.Sprintf("%s %s", styles.OpenCodeIcon, "OpenCode")
95 logo := fmt.Sprintf("%s %s", styles.OpenCodeIcon, "OpenCode")
102 t := theme.CurrentTheme()
96 t := theme.CurrentTheme()
103 baseStyle := styles.BaseStyle()
97 baseStyle := styles.BaseStyle()
@@ -108,7 +102,6 @@ func logo(width int) string {
108
102
109 return baseStyle.
103 return baseStyle.
110 Bold(true).
104 Bold(true).
111− Width(width).
112 Render(
105 Render(
113 lipgloss.JoinHorizontal(
106 lipgloss.JoinHorizontal(
114 lipgloss.Left,
107 lipgloss.Left,
@@ -119,22 +112,33 @@ func logo(width int) string {
119 )
112 )
120}
113}
121
114
122−func repo(width int) string {
115+func repo() string {
123 repo := "https://github.com/opencode-ai/opencode"
116 repo := "https://github.com/opencode-ai/opencode"
124 t := theme.CurrentTheme()
117 t := theme.CurrentTheme()
125
118
126 return styles.BaseStyle().
119 return styles.BaseStyle().
127 Foreground(t.TextMuted()).
120 Foreground(t.TextMuted()).
128− Width(width).
129 Render(repo)
121 Render(repo)
130}
122}
131
123
132−func cwd(width int) string {
124+func cwd() string {
133 cwd := fmt.Sprintf("cwd: %s", config.WorkingDirectory())
125 cwd := fmt.Sprintf("cwd: %s", config.WorkingDirectory())
134 t := theme.CurrentTheme()
126 t := theme.CurrentTheme()
135
127
136 return styles.BaseStyle().
128 return styles.BaseStyle().
137 Foreground(t.TextMuted()).
129 Foreground(t.TextMuted()).
138− Width(width).
139 Render(cwd)
130 Render(cwd)
140}
131}
132+
133+func initialScreen() string {
134+ baseStyle := styles.BaseStyle()
135+
136+ return baseStyle.Render(
137+ lipgloss.JoinVertical(
138+ lipgloss.Top,
139+ header(),
140+ "",
141+ lspsConfigured(),
142+ ),
143+ )
144+}
internal/tui/components/chat/list.go+1−14
@@ -282,7 +282,7 @@ func (m *messagesCmp) View() string {
282 Width(m.width).
282 Width(m.width).
283 Height(m.height - 1).
283 Height(m.height - 1).
284 Render(
284 Render(
285− m.initialScreen(),
285+ initialScreen(),
286 )
286 )
287
287
288 return baseStyle.
288 return baseStyle.
@@ -400,19 +400,6 @@ func (m *messagesCmp) help() string {
400 Render(text)
400 Render(text)
401}
401}
402
402
403−func (m *messagesCmp) initialScreen() string {
404− baseStyle := styles.BaseStyle()
405−
406− return baseStyle.Width(m.width).Render(
407− lipgloss.JoinVertical(
408− lipgloss.Top,
409− header(m.width),
410− "",
411− lspsConfigured(m.width),
412− ),
413− )
414−}
415−
416func (m *messagesCmp) rerender() {
403func (m *messagesCmp) rerender() {
417 for _, msg := range m.messages {
404 for _, msg := range m.messages {
418 delete(m.cachedContent, msg.ID)
405 delete(m.cachedContent, msg.ID)
internal/tui/components/chat/list_v2.go+114−17
@@ -8,6 +8,7 @@ import (
8 "github.com/charmbracelet/lipgloss/v2"
8 "github.com/charmbracelet/lipgloss/v2"
9 "github.com/opencode-ai/opencode/internal/app"
9 "github.com/opencode-ai/opencode/internal/app"
10 "github.com/opencode-ai/opencode/internal/message"
10 "github.com/opencode-ai/opencode/internal/message"
11+ "github.com/opencode-ai/opencode/internal/pubsub"
11 "github.com/opencode-ai/opencode/internal/session"
12 "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/chat/messages"
13 "github.com/opencode-ai/opencode/internal/tui/components/core/list"
14 "github.com/opencode-ai/opencode/internal/tui/components/core/list"
@@ -25,8 +26,9 @@ type messageListCmp struct {
25 app *app.App
26 app *app.App
26 width, height int
27 width, height int
27 session session.Session
28 session session.Session
28− messages []util.Model
29 listCmp list.ListModel
29 listCmp list.ListModel
30+
31+ lastUserMessageTime int64
30}
32}
31
33
32func NewMessagesListCmp(app *app.App) MessageListCmp {
34func NewMessagesListCmp(app *app.App) MessageListCmp {
@@ -54,6 +56,12 @@ func (m *messageListCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
54 return m, cmd
56 return m, cmd
55 }
57 }
56 return m, nil
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)
57 default:
65 default:
58 var cmds []tea.Cmd
66 var cmds []tea.Cmd
59 u, cmd := m.listCmp.Update(msg)
67 u, cmd := m.listCmp.Update(msg)
@@ -64,19 +72,91 @@ func (m *messageListCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
64}
72}
65
73
66func (m *messageListCmp) View() string {
74func (m *messageListCmp) View() string {
75+ if len(m.listCmp.Items()) == 0 {
76+ return initialScreen()
77+ }
67 return lipgloss.JoinVertical(lipgloss.Left, m.listCmp.View())
78 return lipgloss.JoinVertical(lipgloss.Left, m.listCmp.View())
68}
79}
69
80
70−// GetSize implements MessageListCmp.
81+func (m *messageListCmp) handleChildSession(event pubsub.Event[message.Message]) {
71−func (m *messageListCmp) GetSize() (int, int) {
82+ // TODO: update the agent tool message with the changes
72− return m.width, m.height
73}
83}
74
84
75−// SetSize implements MessageListCmp.
85+func (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+ }
110+ // TODO: handle tools
111+ case pubsub.UpdatedEvent:
112+ return m.handleUpdateAssistantMessage(event.Payload)
113+ }
114+ return nil
115+}
116+
117+func (m *messageListCmp) handleNewUserMessage(msg message.Message) tea.Cmd {
118+ m.lastUserMessageTime = msg.CreatedAt
119+ return m.listCmp.AppendItem(messages.NewMessageCmp(msg))
120+}
121+
122+func (m *messageListCmp) handleUpdateAssistantMessage(msg message.Message) tea.Cmd {
123+ // Simple update the content
124+ items := m.listCmp.Items()
125+ lastItem := items[len(items)-1].(messages.MessageCmp)
126+ // TODO:handle tool calls
127+ if lastItem.GetMessage().ID != msg.ID {
128+ return nil
129+ }
130+ // for now just updet the last message
131+ if len(msg.ToolCalls()) == 0 || msg.Content().Text != "" || msg.IsThinking() {
132+ m.listCmp.UpdateItem(
133+ len(items)-1,
134+ messages.NewMessageCmp(
135+ msg,
136+ messages.WithLastUserMessageTime(time.Unix(m.lastUserMessageTime, 0)),
137+ ),
138+ )
139+ }
140+ return nil
141+}
142+
76−func (m *messageListCmp) SetSize(width int, height int) tea.Cmd {
143+func (m *messageListCmp) handleNewAssistantMessage(msg message.Message) tea.Cmd {
77− m.width = width
144+ var cmds []tea.Cmd
78− m.height = height - 1
145+ // Only add assistant messages if they don't have tool calls or there is some content
79− return m.listCmp.SetSize(width, height-1)
146+ if len(msg.ToolCalls()) == 0 || msg.Content().Text != "" || msg.IsThinking() {
147+ cmd := m.listCmp.AppendItem(
148+ messages.NewMessageCmp(
149+ msg,
150+ messages.WithLastUserMessageTime(time.Unix(m.lastUserMessageTime, 0)),
151+ ),
152+ )
153+ cmds = append(cmds, cmd)
154+ }
155+ for _, tc := range msg.ToolCalls() {
156+ cmd := m.listCmp.AppendItem(messages.NewToolCallCmp(tc))
157+ cmds = append(cmds, cmd)
158+ }
159+ return tea.Batch(cmds...)
80}
160}
81
161
82func (m *messageListCmp) SetSession(session session.Session) tea.Cmd {
162func (m *messageListCmp) SetSession(session session.Session) tea.Cmd {
@@ -88,8 +168,8 @@ func (m *messageListCmp) SetSession(session session.Session) tea.Cmd {
88 if err != nil {
168 if err != nil {
89 return util.ReportError(err)
169 return util.ReportError(err)
90 }
170 }
91− m.messages = make([]util.Model, 0)
171+ uiMessages := make([]util.Model, 0)
92− lastUserMessageTime := sessionMessages[0].CreatedAt
172+ m.lastUserMessageTime = sessionMessages[0].CreatedAt
93 toolResultMap := make(map[string]message.ToolResult)
173 toolResultMap := make(map[string]message.ToolResult)
94 // first pass to get all tool results
174 // first pass to get all tool results
95 for _, msg := range sessionMessages {
175 for _, msg := range sessionMessages {
@@ -100,12 +180,18 @@ func (m *messageListCmp) SetSession(session session.Session) tea.Cmd {
100 for _, msg := range sessionMessages {
180 for _, msg := range sessionMessages {
101 switch msg.Role {
181 switch msg.Role {
102 case message.User:
182 case message.User:
103− lastUserMessageTime = msg.CreatedAt
183+ m.lastUserMessageTime = msg.CreatedAt
104− m.messages = append(m.messages, messages.NewMessageCmp(msg))
184+ uiMessages = append(uiMessages, messages.NewMessageCmp(msg))
105 case message.Assistant:
185 case message.Assistant:
106 // Only add assistant messages if they don't have tool calls or there is some content
186 // 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() {
187 if len(msg.ToolCalls()) == 0 || msg.Content().Text != "" || msg.IsThinking() {
188+ uiMessages = append(
189+ uiMessages,
190+ messages.NewMessageCmp(
191+ msg,
108− m.messages = append(m.messages, messages.NewMessageCmp(msg, messages.WithLastUserMessageTime(time.Unix(lastUserMessageTime, 0))))
192+ messages.WithLastUserMessageTime(time.Unix(m.lastUserMessageTime, 0)),
193+ ),
194+ )
109 }
195 }
110 for _, tc := range msg.ToolCalls() {
196 for _, tc := range msg.ToolCalls() {
111 options := []messages.ToolCallOption{}
197 options := []messages.ToolCallOption{}
@@ -115,10 +201,21 @@ func (m *messageListCmp) SetSession(session session.Session) tea.Cmd {
115 if msg.FinishPart().Reason == message.FinishReasonCanceled {
201 if msg.FinishPart().Reason == message.FinishReasonCanceled {
116 options = append(options, messages.WithToolCallCancelled())
202 options = append(options, messages.WithToolCallCancelled())
117 }
203 }
118− m.messages = append(m.messages, messages.NewToolCallCmp(tc, options...))
204+ uiMessages = append(uiMessages, messages.NewToolCallCmp(tc, options...))
119 }
205 }
120 }
206 }
121 }
207 }
122− m.listCmp.SetItems(m.messages)
208+ return m.listCmp.SetItems(uiMessages)
123− return nil
209+}
210+
211+// GetSize implements MessageListCmp.
212+func (m *messageListCmp) GetSize() (int, int) {
213+ return m.width, m.height
214+}
215+
216+// SetSize implements MessageListCmp.
217+func (m *messageListCmp) SetSize(width int, height int) tea.Cmd {
218+ m.width = width
219+ m.height = height - 1
220+ return m.listCmp.SetSize(width, height-1)
124}
221}
internal/tui/components/chat/messages/messages.go+41−2
@@ -12,6 +12,7 @@ import (
12 "github.com/opencode-ai/opencode/internal/llm/models"
12 "github.com/opencode-ai/opencode/internal/llm/models"
13
13
14 "github.com/opencode-ai/opencode/internal/message"
14 "github.com/opencode-ai/opencode/internal/message"
15+ "github.com/opencode-ai/opencode/internal/tui/components/anim"
15 "github.com/opencode-ai/opencode/internal/tui/layout"
16 "github.com/opencode-ai/opencode/internal/tui/layout"
16 "github.com/opencode-ai/opencode/internal/tui/styles"
17 "github.com/opencode-ai/opencode/internal/tui/styles"
17 "github.com/opencode-ai/opencode/internal/tui/theme"
18 "github.com/opencode-ai/opencode/internal/tui/theme"
@@ -22,6 +23,8 @@ type MessageCmp interface {
22 util.Model
23 util.Model
23 layout.Sizeable
24 layout.Sizeable
24 layout.Focusable
25 layout.Focusable
26+ GetMessage() message.Message
27+ Spinning() bool
25}
28}
26
29
27type messageCmp struct {
30type messageCmp struct {
@@ -30,9 +33,10 @@ type messageCmp struct {
30
33
31 // Used for agent and user messages
34 // Used for agent and user messages
32 message message.Message
35 message message.Message
36+ spinning bool
37+ anim util.Model
33 lastUserMessageTime time.Time
38 lastUserMessageTime time.Time
34}
39}
35−
36type MessageOption func(*messageCmp)
40type MessageOption func(*messageCmp)
37
41
38func WithLastUserMessageTime(t time.Time) MessageOption {
42func WithLastUserMessageTime(t time.Time) MessageOption {
@@ -44,6 +48,7 @@ func WithLastUserMessageTime(t time.Time) MessageOption {
44func NewMessageCmp(msg message.Message, opts ...MessageOption) MessageCmp {
48func NewMessageCmp(msg message.Message, opts ...MessageOption) MessageCmp {
45 m := &messageCmp{
49 m := &messageCmp{
46 message: msg,
50 message: msg,
51+ anim: anim.New(15, ""),
47 }
52 }
48 for _, opt := range opts {
53 for _, opt := range opts {
49 opt(m)
54 opt(m)
@@ -52,14 +57,23 @@ func NewMessageCmp(msg message.Message, opts ...MessageOption) MessageCmp {
52}
57}
53
58
54func (m *messageCmp) Init() tea.Cmd {
59func (m *messageCmp) Init() tea.Cmd {
60+ m.spinning = m.shouldSpin()
61+ if m.spinning {
62+ return m.anim.Init()
63+ }
55 return nil
64 return nil
56}
65}
57
66
58func (m *messageCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
67func (m *messageCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
68+ u, cmd := m.anim.Update(msg)
69+ m.anim = u.(util.Model)
59− return m, nil
70+ return m, cmd
60}
71}
61
72
62func (m *messageCmp) View() string {
73func (m *messageCmp) View() string {
74+ if m.spinning {
75+ return m.style().PaddingLeft(1).Render(m.anim.View())
76+ }
63 if m.message.ID != "" {
77 if m.message.ID != "" {
64 // this is a user or assistant message
78 // this is a user or assistant message
65 switch m.message.Role {
79 switch m.message.Role {
@@ -72,6 +86,11 @@ func (m *messageCmp) View() string {
72 return "Unknown Message"
86 return "Unknown Message"
73}
87}
74
88
89+// GetMessage implements MessageCmp.
90+func (m *messageCmp) GetMessage() message.Message {
91+ return m.message
92+}
93+
75func (m *messageCmp) textWidth() int {
94func (m *messageCmp) textWidth() int {
76 return m.width - 1 // take into account the border
95 return m.width - 1 // take into account the border
77}
96}
@@ -184,6 +203,21 @@ func (m *messageCmp) markdownContent() string {
184 return m.toMarkdown(content)
203 return m.toMarkdown(content)
185}
204}
186
205
206+func (m *messageCmp) shouldSpin() bool {
207+ if m.message.Role != message.Assistant {
208+ return false
209+ }
210+
211+ if m.message.IsFinished() {
212+ return false
213+ }
214+
215+ if m.message.Content().Text != "" {
216+ return false
217+ }
218+ return true
219+}
220+
187// Blur implements MessageModel.
221// Blur implements MessageModel.
188func (m *messageCmp) Blur() tea.Cmd {
222func (m *messageCmp) Blur() tea.Cmd {
189 m.focused = false
223 m.focused = false
@@ -209,3 +243,8 @@ func (m *messageCmp) SetSize(width int, height int) tea.Cmd {
209 m.width = width
243 m.width = width
210 return nil
244 return nil
211}
245}
246+
247+// Spinning implements MessageCmp.
248+func (m *messageCmp) Spinning() bool {
249+ return m.spinning
250+}
internal/tui/components/chat/messages/tool.go+16−4
@@ -19,6 +19,8 @@ type ToolCallCmp interface {
19 util.Model
19 util.Model
20 layout.Sizeable
20 layout.Sizeable
21 layout.Focusable
21 layout.Focusable
22+ GetToolCall() message.ToolCall
23+ GetToolResult() message.ToolResult
22}
24}
23
25
24type toolCallCmp struct {
26type toolCallCmp struct {
@@ -73,14 +75,24 @@ func (m *toolCallCmp) View() string {
73 return box.PaddingLeft(1).Render(r.Render(m))
75 return box.PaddingLeft(1).Render(r.Render(m))
74}
76}
75
77
76−func (v *toolCallCmp) renderPending() string {
78+// GetToolCall implements ToolCallCmp.
77− return fmt.Sprintf("%s: %s", prettifyToolName(v.call.Name), toolAction(v.call.Name))
79+func (m *toolCallCmp) GetToolCall() message.ToolCall {
80+ return m.call
78}
81}
79
82
83+// GetToolResult implements ToolCallCmp.
84+func (m *toolCallCmp) GetToolResult() message.ToolResult {
85+ return m.result
86+}
87+
88+func (m *toolCallCmp) renderPending() string {
89+ return fmt.Sprintf("%s: %s", prettifyToolName(m.call.Name), toolAction(m.call.Name))
90+}
91+
80−func (msg *toolCallCmp) style() lipgloss.Style {
92+func (m *toolCallCmp) style() lipgloss.Style {
81 t := theme.CurrentTheme()
93 t := theme.CurrentTheme()
82 borderStyle := lipgloss.NormalBorder()
94 borderStyle := lipgloss.NormalBorder()
83− if msg.focused {
95+ if m.focused {
84 borderStyle = lipgloss.DoubleBorder()
96 borderStyle = lipgloss.DoubleBorder()
85 }
97 }
86 return styles.BaseStyle().
98 return styles.BaseStyle().
internal/tui/components/chat/sidebar.go+2−2
@@ -93,11 +93,11 @@ func (m *sidebarCmp) View() string {
93 Render(
93 Render(
94 lipgloss.JoinVertical(
94 lipgloss.JoinVertical(
95 lipgloss.Top,
95 lipgloss.Top,
96− header(m.width),
96+ header(),
97 " ",
97 " ",
98 m.sessionSection(),
98 m.sessionSection(),
99 " ",
99 " ",
100− lspsConfigured(m.width),
100+ lspsConfigured(),
101 " ",
101 " ",
102 m.modifiedFiles(),
102 m.modifiedFiles(),
103 ),
103 ),
internal/tui/components/core/list/list.go+52−6
@@ -9,6 +9,8 @@ import (
9 "github.com/charmbracelet/bubbles/v2/key"
9 "github.com/charmbracelet/bubbles/v2/key"
10 tea "github.com/charmbracelet/bubbletea/v2"
10 tea "github.com/charmbracelet/bubbletea/v2"
11 "github.com/charmbracelet/lipgloss/v2"
11 "github.com/charmbracelet/lipgloss/v2"
12+ "github.com/opencode-ai/opencode/internal/logging"
13+ "github.com/opencode-ai/opencode/internal/tui/components/anim"
12 "github.com/opencode-ai/opencode/internal/tui/layout"
14 "github.com/opencode-ai/opencode/internal/tui/layout"
13 "github.com/opencode-ai/opencode/internal/tui/util"
15 "github.com/opencode-ai/opencode/internal/tui/util"
14)
16)
@@ -17,11 +19,17 @@ type ListModel interface {
17 util.Model
19 util.Model
18 layout.Sizeable
20 layout.Sizeable
19 SetItems([]util.Model) tea.Cmd
21 SetItems([]util.Model) tea.Cmd
20− AppendItem(util.Model)
22+ AppendItem(util.Model) tea.Cmd
21− PrependItem(util.Model)
23+ PrependItem(util.Model) tea.Cmd
22 DeleteItem(int)
24 DeleteItem(int)
23 UpdateItem(int, util.Model)
25 UpdateItem(int, util.Model)
24 ResetView()
26 ResetView()
27+ Items() []util.Model
28+}
29+
30+type HasAnim interface {
31+ util.Model
32+ Spinning() bool
25}
33}
26
34
27type renderedItem struct {
35type renderedItem struct {
@@ -107,6 +115,7 @@ func (m *model) Init() tea.Cmd {
107
115
108// Update implements List.
116// Update implements List.
109func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
117func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
118+ var cmds []tea.Cmd
110 switch msg := msg.(type) {
119 switch msg := msg.(type) {
111 case tea.KeyMsg:
120 case tea.KeyMsg:
112 switch {
121 switch {
@@ -151,11 +160,37 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
151 m.goToBottom()
160 m.goToBottom()
152 return m, nil
161 return m, nil
153 }
162 }
163+ case anim.ColorCycleMsg:
164+ logging.Info("ColorCycleMsg", "msg", msg)
165+ for inx, item := range m.items {
166+ if i, ok := item.(HasAnim); ok {
167+ if i.Spinning() {
168+ updated, cmd := i.Update(msg)
169+ cmds = append(cmds, cmd)
170+ m.UpdateItem(inx, updated.(util.Model))
171+ }
172+ }
173+ }
174+ return m, tea.Batch(cmds...)
175+ case anim.StepCharsMsg:
176+ logging.Info("ColorCycleMsg", "msg", msg)
177+ for inx, item := range m.items {
178+ if i, ok := item.(HasAnim); ok {
179+ if i.Spinning() {
180+ updated, cmd := i.Update(msg)
181+ cmds = append(cmds, cmd)
182+ m.UpdateItem(inx, updated.(util.Model))
183+ }
184+ }
185+ }
186+ return m, tea.Batch(cmds...)
154 }
187 }
155 if m.selectedItemInx > -1 {
188 if m.selectedItemInx > -1 {
156 u, cmd := m.items[m.selectedItemInx].Update(msg)
189 u, cmd := m.items[m.selectedItemInx].Update(msg)
190+ cmds = append(cmds, cmd)
157 m.UpdateItem(m.selectedItemInx, u.(util.Model))
191 m.UpdateItem(m.selectedItemInx, u.(util.Model))
192+ cmds = append(cmds, cmd)
158− return m, cmd
193+ return m, tea.Batch(cmds...)
159 }
194 }
160
195
161 return m, nil
196 return m, nil
@@ -172,6 +207,11 @@ func (m *model) View() string {
172 return lipgloss.NewStyle().Padding(m.padding...).Height(m.height).Render(m.content)
207 return lipgloss.NewStyle().Padding(m.padding...).Height(m.height).Render(m.content)
173}
208}
174
209
210+// Items implements ListModel.
211+func (m *model) Items() []util.Model {
212+ return m.items
213+}
214+
175func (m *model) renderVisibleReverse() {
215func (m *model) renderVisibleReverse() {
176 start := 0
216 start := 0
177 cutoff := m.offset + m.listHeight()
217 cutoff := m.offset + m.listHeight()
@@ -464,7 +504,6 @@ func (m *model) rerenderItem(inx int) {
464 }
504 }
465 // TODO: if hight changed do something
505 // TODO: if hight changed do something
466 if cachedItem.height != len(rerenderedLines) && inx != len(m.items)-1 {
506 if cachedItem.height != len(rerenderedLines) && inx != len(m.items)-1 {
467− panic("not handled")
468 }
507 }
469 m.renderedItems.Store(inx, renderedItem{
508 m.renderedItems.Store(inx, renderedItem{
470 lines: rerenderedLines,
509 lines: rerenderedLines,
@@ -563,10 +602,12 @@ func (m *model) listHeight() int {
563}
602}
564
603
565// AppendItem implements List.
604// AppendItem implements List.
566−func (m *model) AppendItem(item util.Model) {
605+func (m *model) AppendItem(item util.Model) tea.Cmd {
606+ cmd := item.Init()
567 m.items = append(m.items, item)
607 m.items = append(m.items, item)
568 m.goToBottom()
608 m.goToBottom()
569 m.needsRerender = true
609 m.needsRerender = true
610+ return cmd
570}
611}
571
612
572// DeleteItem implements List.
613// DeleteItem implements List.
@@ -577,7 +618,8 @@ func (m *model) DeleteItem(i int) {
577}
618}
578
619
579// PrependItem implements List.
620// PrependItem implements List.
580−func (m *model) PrependItem(item util.Model) {
621+func (m *model) PrependItem(item util.Model) tea.Cmd {
622+ cmd := item.Init()
581 m.items = append([]util.Model{item}, m.items...)
623 m.items = append([]util.Model{item}, m.items...)
582 // update the indices of the rendered items
624 // update the indices of the rendered items
583 newRenderedItems := make(map[int]renderedItem)
625 newRenderedItems := make(map[int]renderedItem)
@@ -594,6 +636,7 @@ func (m *model) PrependItem(item util.Model) {
594 }
636 }
595 m.goToTop()
637 m.goToTop()
596 m.needsRerender = true
638 m.needsRerender = true
639+ return cmd
597}
640}
598
641
599func (m *model) setReverse(reverse bool) {
642func (m *model) setReverse(reverse bool) {
@@ -610,6 +653,9 @@ func (m *model) SetItems(items []util.Model) tea.Cmd {
610 var cmds []tea.Cmd
653 var cmds []tea.Cmd
611 cmd := m.setItemsSize()
654 cmd := m.setItemsSize()
612 cmds = append(cmds, cmd)
655 cmds = append(cmds, cmd)
656+ for _, item := range m.items {
657+ cmds = append(cmds, item.Init())
658+ }
613 if m.reverse {
659 if m.reverse {
614 m.selectedItemInx = len(m.items) - 1
660 m.selectedItemInx = len(m.items) - 1
615 cmd := m.focusSelected()
661 cmd := m.focusSelected()