@@ -1,44 +1,49 @@
11package chat
22
3−import (
4− "context"
5− "fmt"
6− "math"
7−
8− "github.com/charmbracelet/bubbles/v2/key"
9− "github.com/charmbracelet/bubbles/v2/spinner"
10− "github.com/charmbracelet/bubbles/v2/viewport"
11− tea "github.com/charmbracelet/bubbletea/v2"
12− "github.com/charmbracelet/lipgloss/v2"
13− "github.com/opencode-ai/opencode/internal/app"
14− "github.com/opencode-ai/opencode/internal/message"
15− "github.com/opencode-ai/opencode/internal/pubsub"
16− "github.com/opencode-ai/opencode/internal/session"
17− "github.com/opencode-ai/opencode/internal/tui/components/dialog"
18− "github.com/opencode-ai/opencode/internal/tui/styles"
19− "github.com/opencode-ai/opencode/internal/tui/theme"
20− "github.com/opencode-ai/opencode/internal/tui/util"
21−)
22−
23−type cacheItem struct {
24− width int
25− content []uiMessage
26−}
27−type messagesCmp struct {
28− app *app.App
29− width, height int
30− viewport viewport.Model
31− session session.Session
32− messages []message.Message
33− uiMessages []uiMessage
34− currentMsgID string
35− cachedContent map[string]cacheItem
36− spinner spinner.Model
37− rendering bool
38− attachments viewport.Model
39−}
40−type renderFinishedMsg struct{}
41−
3+import "github.com/charmbracelet/bubbles/v2/key"
4+
5+// import (
6+//
7+// "context"
8+// "fmt"
9+// "math"
10+//
11+// "github.com/charmbracelet/bubbles/v2/key"
12+// "github.com/charmbracelet/bubbles/v2/spinner"
13+// "github.com/charmbracelet/bubbles/v2/viewport"
14+// tea "github.com/charmbracelet/bubbletea/v2"
15+// "github.com/charmbracelet/lipgloss/v2"
16+// "github.com/opencode-ai/opencode/internal/app"
17+// "github.com/opencode-ai/opencode/internal/message"
18+// "github.com/opencode-ai/opencode/internal/pubsub"
19+// "github.com/opencode-ai/opencode/internal/session"
20+// "github.com/opencode-ai/opencode/internal/tui/components/dialog"
21+// "github.com/opencode-ai/opencode/internal/tui/styles"
22+// "github.com/opencode-ai/opencode/internal/tui/theme"
23+// "github.com/opencode-ai/opencode/internal/tui/util"
24+//
25+// )
26+//
27+// type cacheItem struct {
28+// width int
29+// content []uiMessage
30+// }
31+//
32+// type messagesCmp struct {
33+// app *app.App
34+// width, height int
35+// viewport viewport.Model
36+// session session.Session
37+// messages []message.Message
38+// uiMessages []uiMessage
39+// currentMsgID string
40+// cachedContent map[string]cacheItem
41+// spinner spinner.Model
42+// rendering bool
43+// attachments viewport.Model
44+// }
45+//
46+// type renderFinishedMsg struct{}
4247type MessageKeys struct {
4348 PageDown key.Binding
4449 PageUp key.Binding
@@ -65,410 +70,411 @@ var messageKeys = MessageKeys{
6570 ),
6671}
6772
68−func (m *messagesCmp) Init() tea.Cmd {
69− return tea.Batch(m.viewport.Init(), m.spinner.Tick)
70−}
71−
72−func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
73− var cmds []tea.Cmd
74− switch msg := msg.(type) {
75− case dialog.ThemeChangedMsg:
76− m.rerender()
77− return m, nil
78− case SessionSelectedMsg:
79− if msg.ID != m.session.ID {
80− cmd := m.SetSession(msg)
81− return m, cmd
82− }
83− return m, nil
84− case SessionClearedMsg:
85− m.session = session.Session{}
86− m.messages = make([]message.Message, 0)
87− m.currentMsgID = ""
88− m.rendering = false
89− return m, nil
90−
91− case tea.KeyMsg:
92− if key.Matches(msg, messageKeys.PageUp) || key.Matches(msg, messageKeys.PageDown) ||
93− key.Matches(msg, messageKeys.HalfPageUp) || key.Matches(msg, messageKeys.HalfPageDown) {
94− u, cmd := m.viewport.Update(msg)
95− m.viewport = u
96− cmds = append(cmds, cmd)
97− }
98−
99− case renderFinishedMsg:
100− m.rendering = false
101− m.viewport.GotoBottom()
102− case pubsub.Event[session.Session]:
103− if msg.Type == pubsub.UpdatedEvent && msg.Payload.ID == m.session.ID {
104− m.session = msg.Payload
105− if m.session.SummaryMessageID == m.currentMsgID {
106− delete(m.cachedContent, m.currentMsgID)
107− m.renderView()
108− }
109− }
110− case pubsub.Event[message.Message]:
111− needsRerender := false
112− if msg.Type == pubsub.CreatedEvent {
113− if msg.Payload.SessionID == m.session.ID {
114−
115− messageExists := false
116− for _, v := range m.messages {
117− if v.ID == msg.Payload.ID {
118− messageExists = true
119− break
120− }
121− }
122−
123− if !messageExists {
124− if len(m.messages) > 0 {
125− lastMsgID := m.messages[len(m.messages)-1].ID
126− delete(m.cachedContent, lastMsgID)
127− }
128−
129− m.messages = append(m.messages, msg.Payload)
130− delete(m.cachedContent, m.currentMsgID)
131− m.currentMsgID = msg.Payload.ID
132− needsRerender = true
133− }
134− }
135− // There are tool calls from the child task
136− for _, v := range m.messages {
137− for _, c := range v.ToolCalls() {
138− if c.ID == msg.Payload.SessionID {
139− delete(m.cachedContent, v.ID)
140− needsRerender = true
141− }
142− }
143− }
144− } else if msg.Type == pubsub.UpdatedEvent && msg.Payload.SessionID == m.session.ID {
145− for i, v := range m.messages {
146− if v.ID == msg.Payload.ID {
147− m.messages[i] = msg.Payload
148− delete(m.cachedContent, msg.Payload.ID)
149− needsRerender = true
150− break
151− }
152− }
153− }
154− if needsRerender {
155− m.renderView()
156− if len(m.messages) > 0 {
157− if (msg.Type == pubsub.CreatedEvent) ||
158− (msg.Type == pubsub.UpdatedEvent && msg.Payload.ID == m.messages[len(m.messages)-1].ID) {
159− m.viewport.GotoBottom()
160− }
161− }
162− }
163− }
164−
165− spinner, cmd := m.spinner.Update(msg)
166− m.spinner = spinner
167− cmds = append(cmds, cmd)
168− return m, tea.Batch(cmds...)
169−}
170−
171−func (m *messagesCmp) IsAgentWorking() bool {
172− return m.app.CoderAgent.IsSessionBusy(m.session.ID)
173−}
174−
175−func formatTimeDifference(unixTime1, unixTime2 int64) string {
176− diffSeconds := float64(math.Abs(float64(unixTime2 - unixTime1)))
177−
178− if diffSeconds < 60 {
179− return fmt.Sprintf("%.1fs", diffSeconds)
180− }
181−
182− minutes := int(diffSeconds / 60)
183− seconds := int(diffSeconds) % 60
184− return fmt.Sprintf("%dm%ds", minutes, seconds)
185−}
186−
187−func (m *messagesCmp) renderView() {
188− m.uiMessages = make([]uiMessage, 0)
189− pos := 0
190− baseStyle := styles.BaseStyle()
191−
192− if m.width == 0 {
193− return
194− }
195− for inx, msg := range m.messages {
196− switch msg.Role {
197− case message.User:
198− if cache, ok := m.cachedContent[msg.ID]; ok && cache.width == m.width {
199− m.uiMessages = append(m.uiMessages, cache.content...)
200− continue
201− }
202− userMsg := renderUserMessage(
203− msg,
204− msg.ID == m.currentMsgID,
205− m.width,
206− pos,
207− )
208− m.uiMessages = append(m.uiMessages, userMsg)
209− m.cachedContent[msg.ID] = cacheItem{
210− width: m.width,
211− content: []uiMessage{userMsg},
212− }
213− pos += userMsg.height + 1 // + 1 for spacing
214− case message.Assistant:
215− if cache, ok := m.cachedContent[msg.ID]; ok && cache.width == m.width {
216− m.uiMessages = append(m.uiMessages, cache.content...)
217− continue
218− }
219− isSummary := m.session.SummaryMessageID == msg.ID
220−
221− assistantMessages := renderAssistantMessage(
222− msg,
223− inx,
224− m.messages,
225− m.app.Messages,
226− m.currentMsgID,
227− isSummary,
228− m.width,
229− pos,
230− )
231− for _, msg := range assistantMessages {
232− m.uiMessages = append(m.uiMessages, msg)
233− pos += msg.height + 1 // + 1 for spacing
234− }
235− m.cachedContent[msg.ID] = cacheItem{
236− width: m.width,
237− content: assistantMessages,
238− }
239− }
240− }
241−
242− messages := make([]string, 0)
243− for _, v := range m.uiMessages {
244− messages = append(messages, lipgloss.JoinVertical(lipgloss.Left, v.content),
245− baseStyle.
246− Width(m.width).
247− Render(
248− "",
249− ),
250− )
251− }
252−
253− m.viewport.SetContent(
254− baseStyle.
255− Width(m.width).
256− Render(
257− lipgloss.JoinVertical(
258− lipgloss.Top,
259− messages...,
260− ),
261− ),
262− )
263−}
264−
265−func (m *messagesCmp) View() string {
266− baseStyle := styles.BaseStyle()
267−
268− if m.rendering {
269− return baseStyle.
270− Width(m.width).
271− Render(
272− lipgloss.JoinVertical(
273− lipgloss.Top,
274− "Loading...",
275− m.working(),
276− m.help(),
277− ),
278− )
279− }
280− if len(m.messages) == 0 {
281− content := baseStyle.
282− Width(m.width).
283− Height(m.height - 1).
284− Render(
285− initialScreen(),
286− )
287−
288− return baseStyle.
289− Width(m.width).
290− Render(
291− lipgloss.JoinVertical(
292− lipgloss.Top,
293− content,
294− "",
295− m.help(),
296− ),
297− )
298− }
299−
300− return baseStyle.
301− Width(m.width).
302− Render(
303− lipgloss.JoinVertical(
304− lipgloss.Top,
305− m.viewport.View(),
306− m.working(),
307− m.help(),
308− ),
309− )
310−}
311−
312−func hasToolsWithoutResponse(messages []message.Message) bool {
313− toolCalls := make([]message.ToolCall, 0)
314− toolResults := make([]message.ToolResult, 0)
315− for _, m := range messages {
316− toolCalls = append(toolCalls, m.ToolCalls()...)
317− toolResults = append(toolResults, m.ToolResults()...)
318− }
319−
320− for _, v := range toolCalls {
321− found := false
322− for _, r := range toolResults {
323− if v.ID == r.ToolCallID {
324− found = true
325− break
326− }
327− }
328− if !found && v.Finished {
329− return true
330− }
331− }
332− return false
333−}
334−
335−func hasUnfinishedToolCalls(messages []message.Message) bool {
336− toolCalls := make([]message.ToolCall, 0)
337− for _, m := range messages {
338− toolCalls = append(toolCalls, m.ToolCalls()...)
339− }
340− for _, v := range toolCalls {
341− if !v.Finished {
342− return true
343− }
344− }
345− return false
346−}
347−
348−func (m *messagesCmp) working() string {
349− text := ""
350− if m.IsAgentWorking() && len(m.messages) > 0 {
351− t := theme.CurrentTheme()
352− baseStyle := styles.BaseStyle()
353−
354− task := "Thinking..."
355− lastMessage := m.messages[len(m.messages)-1]
356− if hasToolsWithoutResponse(m.messages) {
357− task = "Waiting for tool response..."
358− } else if hasUnfinishedToolCalls(m.messages) {
359− task = "Building tool call..."
360− } else if !lastMessage.IsFinished() {
361− task = "Generating..."
362− }
363− if task != "" {
364− text += baseStyle.
365− Width(m.width).
366− Foreground(t.Primary()).
367− Bold(true).
368− Render(fmt.Sprintf("%s %s ", m.spinner.View(), task))
369− }
370− }
371− return text
372−}
373−
374−func (m *messagesCmp) help() string {
375− t := theme.CurrentTheme()
376− baseStyle := styles.BaseStyle()
377−
378− text := ""
379−
380− if m.app.CoderAgent.IsBusy() {
381− text += lipgloss.JoinHorizontal(
382− lipgloss.Left,
383− baseStyle.Foreground(t.TextMuted()).Bold(true).Render("press "),
384− baseStyle.Foreground(t.Text()).Bold(true).Render("esc"),
385− baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" to exit cancel"),
386− )
387− } else {
388− text += lipgloss.JoinHorizontal(
389− lipgloss.Left,
390− baseStyle.Foreground(t.TextMuted()).Bold(true).Render("press "),
391− baseStyle.Foreground(t.Text()).Bold(true).Render("enter"),
392− baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" to send the message,"),
393− baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" write"),
394− baseStyle.Foreground(t.Text()).Bold(true).Render(" \\"),
395− baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" and enter to add a new line"),
396− )
397− }
398− return baseStyle.
399− Width(m.width).
400− Render(text)
401−}
402−
403−func (m *messagesCmp) rerender() {
404− for _, msg := range m.messages {
405− delete(m.cachedContent, msg.ID)
406− }
407− m.renderView()
408−}
409−
410−func (m *messagesCmp) SetSize(width, height int) tea.Cmd {
411− if m.width == width && m.height == height {
412− return nil
413− }
414− m.width = width
415− m.height = height
416− m.viewport.SetWidth(width)
417− m.viewport.SetHeight(height - 2)
418− m.attachments.SetWidth(width + 40)
419− m.attachments.SetHeight(3)
420− m.rerender()
421− return nil
422−}
423−
424−func (m *messagesCmp) GetSize() (int, int) {
425− return m.width, m.height
426−}
427−
428−func (m *messagesCmp) SetSession(session session.Session) tea.Cmd {
429− if m.session.ID == session.ID {
430− return nil
431− }
432− m.session = session
433− messages, err := m.app.Messages.List(context.Background(), session.ID)
434− if err != nil {
435− return util.ReportError(err)
436− }
437− m.messages = messages
438− if len(m.messages) > 0 {
439− m.currentMsgID = m.messages[len(m.messages)-1].ID
440− }
441− delete(m.cachedContent, m.currentMsgID)
442− m.rendering = true
443− return func() tea.Msg {
444− m.renderView()
445− return renderFinishedMsg{}
446− }
447−}
448−
449−func (m *messagesCmp) BindingKeys() []key.Binding {
450− return []key.Binding{
451− m.viewport.KeyMap.PageDown,
452− m.viewport.KeyMap.PageUp,
453− m.viewport.KeyMap.HalfPageUp,
454− m.viewport.KeyMap.HalfPageDown,
455− }
456−}
457−
458−func NewMessagesCmp(app *app.App) util.Model {
459− s := spinner.New()
460− s.Spinner = spinner.Pulse
461− vp := viewport.New()
462− attachmets := viewport.New()
463− vp.KeyMap.PageUp = messageKeys.PageUp
464− vp.KeyMap.PageDown = messageKeys.PageDown
465− vp.KeyMap.HalfPageUp = messageKeys.HalfPageUp
466− vp.KeyMap.HalfPageDown = messageKeys.HalfPageDown
467− return &messagesCmp{
468− app: app,
469− cachedContent: make(map[string]cacheItem),
470− viewport: vp,
471− spinner: s,
472− attachments: attachmets,
473− }
474−}
73+//
74+// func (m *messagesCmp) Init() tea.Cmd {
75+// return tea.Batch(m.viewport.Init(), m.spinner.Tick)
76+// }
77+//
78+// func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
79+// var cmds []tea.Cmd
80+// switch msg := msg.(type) {
81+// case dialog.ThemeChangedMsg:
82+// m.rerender()
83+// return m, nil
84+// case SessionSelectedMsg:
85+// if msg.ID != m.session.ID {
86+// cmd := m.SetSession(msg)
87+// return m, cmd
88+// }
89+// return m, nil
90+// case SessionClearedMsg:
91+// m.session = session.Session{}
92+// m.messages = make([]message.Message, 0)
93+// m.currentMsgID = ""
94+// m.rendering = false
95+// return m, nil
96+//
97+// case tea.KeyMsg:
98+// if key.Matches(msg, messageKeys.PageUp) || key.Matches(msg, messageKeys.PageDown) ||
99+// key.Matches(msg, messageKeys.HalfPageUp) || key.Matches(msg, messageKeys.HalfPageDown) {
100+// u, cmd := m.viewport.Update(msg)
101+// m.viewport = u
102+// cmds = append(cmds, cmd)
103+// }
104+//
105+// case renderFinishedMsg:
106+// m.rendering = false
107+// m.viewport.GotoBottom()
108+// case pubsub.Event[session.Session]:
109+// if msg.Type == pubsub.UpdatedEvent && msg.Payload.ID == m.session.ID {
110+// m.session = msg.Payload
111+// if m.session.SummaryMessageID == m.currentMsgID {
112+// delete(m.cachedContent, m.currentMsgID)
113+// m.renderView()
114+// }
115+// }
116+// case pubsub.Event[message.Message]:
117+// needsRerender := false
118+// if msg.Type == pubsub.CreatedEvent {
119+// if msg.Payload.SessionID == m.session.ID {
120+//
121+// messageExists := false
122+// for _, v := range m.messages {
123+// if v.ID == msg.Payload.ID {
124+// messageExists = true
125+// break
126+// }
127+// }
128+//
129+// if !messageExists {
130+// if len(m.messages) > 0 {
131+// lastMsgID := m.messages[len(m.messages)-1].ID
132+// delete(m.cachedContent, lastMsgID)
133+// }
134+//
135+// m.messages = append(m.messages, msg.Payload)
136+// delete(m.cachedContent, m.currentMsgID)
137+// m.currentMsgID = msg.Payload.ID
138+// needsRerender = true
139+// }
140+// }
141+// // There are tool calls from the child task
142+// for _, v := range m.messages {
143+// for _, c := range v.ToolCalls() {
144+// if c.ID == msg.Payload.SessionID {
145+// delete(m.cachedContent, v.ID)
146+// needsRerender = true
147+// }
148+// }
149+// }
150+// } else if msg.Type == pubsub.UpdatedEvent && msg.Payload.SessionID == m.session.ID {
151+// for i, v := range m.messages {
152+// if v.ID == msg.Payload.ID {
153+// m.messages[i] = msg.Payload
154+// delete(m.cachedContent, msg.Payload.ID)
155+// needsRerender = true
156+// break
157+// }
158+// }
159+// }
160+// if needsRerender {
161+// m.renderView()
162+// if len(m.messages) > 0 {
163+// if (msg.Type == pubsub.CreatedEvent) ||
164+// (msg.Type == pubsub.UpdatedEvent && msg.Payload.ID == m.messages[len(m.messages)-1].ID) {
165+// m.viewport.GotoBottom()
166+// }
167+// }
168+// }
169+// }
170+//
171+// spinner, cmd := m.spinner.Update(msg)
172+// m.spinner = spinner
173+// cmds = append(cmds, cmd)
174+// return m, tea.Batch(cmds...)
175+// }
176+//
177+// func (m *messagesCmp) IsAgentWorking() bool {
178+// return m.app.CoderAgent.IsSessionBusy(m.session.ID)
179+// }
180+//
181+// func formatTimeDifference(unixTime1, unixTime2 int64) string {
182+// diffSeconds := float64(math.Abs(float64(unixTime2 - unixTime1)))
183+//
184+// if diffSeconds < 60 {
185+// return fmt.Sprintf("%.1fs", diffSeconds)
186+// }
187+//
188+// minutes := int(diffSeconds / 60)
189+// seconds := int(diffSeconds) % 60
190+// return fmt.Sprintf("%dm%ds", minutes, seconds)
191+// }
192+//
193+// func (m *messagesCmp) renderView() {
194+// m.uiMessages = make([]uiMessage, 0)
195+// pos := 0
196+// baseStyle := styles.BaseStyle()
197+//
198+// if m.width == 0 {
199+// return
200+// }
201+// for inx, msg := range m.messages {
202+// switch msg.Role {
203+// case message.User:
204+// if cache, ok := m.cachedContent[msg.ID]; ok && cache.width == m.width {
205+// m.uiMessages = append(m.uiMessages, cache.content...)
206+// continue
207+// }
208+// userMsg := renderUserMessage(
209+// msg,
210+// msg.ID == m.currentMsgID,
211+// m.width,
212+// pos,
213+// )
214+// m.uiMessages = append(m.uiMessages, userMsg)
215+// m.cachedContent[msg.ID] = cacheItem{
216+// width: m.width,
217+// content: []uiMessage{userMsg},
218+// }
219+// pos += userMsg.height + 1 // + 1 for spacing
220+// case message.Assistant:
221+// if cache, ok := m.cachedContent[msg.ID]; ok && cache.width == m.width {
222+// m.uiMessages = append(m.uiMessages, cache.content...)
223+// continue
224+// }
225+// isSummary := m.session.SummaryMessageID == msg.ID
226+//
227+// assistantMessages := renderAssistantMessage(
228+// msg,
229+// inx,
230+// m.messages,
231+// m.app.Messages,
232+// m.currentMsgID,
233+// isSummary,
234+// m.width,
235+// pos,
236+// )
237+// for _, msg := range assistantMessages {
238+// m.uiMessages = append(m.uiMessages, msg)
239+// pos += msg.height + 1 // + 1 for spacing
240+// }
241+// m.cachedContent[msg.ID] = cacheItem{
242+// width: m.width,
243+// content: assistantMessages,
244+// }
245+// }
246+// }
247+//
248+// messages := make([]string, 0)
249+// for _, v := range m.uiMessages {
250+// messages = append(messages, lipgloss.JoinVertical(lipgloss.Left, v.content),
251+// baseStyle.
252+// Width(m.width).
253+// Render(
254+// "",
255+// ),
256+// )
257+// }
258+//
259+// m.viewport.SetContent(
260+// baseStyle.
261+// Width(m.width).
262+// Render(
263+// lipgloss.JoinVertical(
264+// lipgloss.Top,
265+// messages...,
266+// ),
267+// ),
268+// )
269+// }
270+//
271+// func (m *messagesCmp) View() string {
272+// baseStyle := styles.BaseStyle()
273+//
274+// if m.rendering {
275+// return baseStyle.
276+// Width(m.width).
277+// Render(
278+// lipgloss.JoinVertical(
279+// lipgloss.Top,
280+// "Loading...",
281+// m.working(),
282+// m.help(),
283+// ),
284+// )
285+// }
286+// if len(m.messages) == 0 {
287+// content := baseStyle.
288+// Width(m.width).
289+// Height(m.height - 1).
290+// Render(
291+// initialScreen(),
292+// )
293+//
294+// return baseStyle.
295+// Width(m.width).
296+// Render(
297+// lipgloss.JoinVertical(
298+// lipgloss.Top,
299+// content,
300+// "",
301+// m.help(),
302+// ),
303+// )
304+// }
305+//
306+// return baseStyle.
307+// Width(m.width).
308+// Render(
309+// lipgloss.JoinVertical(
310+// lipgloss.Top,
311+// m.viewport.View(),
312+// m.working(),
313+// m.help(),
314+// ),
315+// )
316+// }
317+//
318+// func hasToolsWithoutResponse(messages []message.Message) bool {
319+// toolCalls := make([]message.ToolCall, 0)
320+// toolResults := make([]message.ToolResult, 0)
321+// for _, m := range messages {
322+// toolCalls = append(toolCalls, m.ToolCalls()...)
323+// toolResults = append(toolResults, m.ToolResults()...)
324+// }
325+//
326+// for _, v := range toolCalls {
327+// found := false
328+// for _, r := range toolResults {
329+// if v.ID == r.ToolCallID {
330+// found = true
331+// break
332+// }
333+// }
334+// if !found && v.Finished {
335+// return true
336+// }
337+// }
338+// return false
339+// }
340+//
341+// func hasUnfinishedToolCalls(messages []message.Message) bool {
342+// toolCalls := make([]message.ToolCall, 0)
343+// for _, m := range messages {
344+// toolCalls = append(toolCalls, m.ToolCalls()...)
345+// }
346+// for _, v := range toolCalls {
347+// if !v.Finished {
348+// return true
349+// }
350+// }
351+// return false
352+// }
353+//
354+// func (m *messagesCmp) working() string {
355+// text := ""
356+// if m.IsAgentWorking() && len(m.messages) > 0 {
357+// t := theme.CurrentTheme()
358+// baseStyle := styles.BaseStyle()
359+//
360+// task := "Thinking..."
361+// lastMessage := m.messages[len(m.messages)-1]
362+// if hasToolsWithoutResponse(m.messages) {
363+// task = "Waiting for tool response..."
364+// } else if hasUnfinishedToolCalls(m.messages) {
365+// task = "Building tool call..."
366+// } else if !lastMessage.IsFinished() {
367+// task = "Generating..."
368+// }
369+// if task != "" {
370+// text += baseStyle.
371+// Width(m.width).
372+// Foreground(t.Primary()).
373+// Bold(true).
374+// Render(fmt.Sprintf("%s %s ", m.spinner.View(), task))
375+// }
376+// }
377+// return text
378+// }
379+//
380+// func (m *messagesCmp) help() string {
381+// t := theme.CurrentTheme()
382+// baseStyle := styles.BaseStyle()
383+//
384+// text := ""
385+//
386+// if m.app.CoderAgent.IsBusy() {
387+// text += lipgloss.JoinHorizontal(
388+// lipgloss.Left,
389+// baseStyle.Foreground(t.TextMuted()).Bold(true).Render("press "),
390+// baseStyle.Foreground(t.Text()).Bold(true).Render("esc"),
391+// baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" to exit cancel"),
392+// )
393+// } else {
394+// text += lipgloss.JoinHorizontal(
395+// lipgloss.Left,
396+// baseStyle.Foreground(t.TextMuted()).Bold(true).Render("press "),
397+// baseStyle.Foreground(t.Text()).Bold(true).Render("enter"),
398+// baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" to send the message,"),
399+// baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" write"),
400+// baseStyle.Foreground(t.Text()).Bold(true).Render(" \\"),
401+// baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" and enter to add a new line"),
402+// )
403+// }
404+// return baseStyle.
405+// Width(m.width).
406+// Render(text)
407+// }
408+//
409+// func (m *messagesCmp) rerender() {
410+// for _, msg := range m.messages {
411+// delete(m.cachedContent, msg.ID)
412+// }
413+// m.renderView()
414+// }
415+//
416+// func (m *messagesCmp) SetSize(width, height int) tea.Cmd {
417+// if m.width == width && m.height == height {
418+// return nil
419+// }
420+// m.width = width
421+// m.height = height
422+// m.viewport.SetWidth(width)
423+// m.viewport.SetHeight(height - 2)
424+// m.attachments.SetWidth(width + 40)
425+// m.attachments.SetHeight(3)
426+// m.rerender()
427+// return nil
428+// }
429+//
430+// func (m *messagesCmp) GetSize() (int, int) {
431+// return m.width, m.height
432+// }
433+//
434+// func (m *messagesCmp) SetSession(session session.Session) tea.Cmd {
435+// if m.session.ID == session.ID {
436+// return nil
437+// }
438+// m.session = session
439+// messages, err := m.app.Messages.List(context.Background(), session.ID)
440+// if err != nil {
441+// return util.ReportError(err)
442+// }
443+// m.messages = messages
444+// if len(m.messages) > 0 {
445+// m.currentMsgID = m.messages[len(m.messages)-1].ID
446+// }
447+// delete(m.cachedContent, m.currentMsgID)
448+// m.rendering = true
449+// return func() tea.Msg {
450+// m.renderView()
451+// return renderFinishedMsg{}
452+// }
453+// }
454+//
455+// func (m *messagesCmp) BindingKeys() []key.Binding {
456+// return []key.Binding{
457+// m.viewport.KeyMap.PageDown,
458+// m.viewport.KeyMap.PageUp,
459+// m.viewport.KeyMap.HalfPageUp,
460+// m.viewport.KeyMap.HalfPageDown,
461+// }
462+// }
463+//
464+// func NewMessagesCmp(app *app.App) util.Model {
465+// s := spinner.New()
466+// s.Spinner = spinner.Pulse
467+// vp := viewport.New()
468+// attachmets := viewport.New()
469+// vp.KeyMap.PageUp = messageKeys.PageUp
470+// vp.KeyMap.PageDown = messageKeys.PageDown
471+// vp.KeyMap.HalfPageUp = messageKeys.HalfPageUp
472+// vp.KeyMap.HalfPageDown = messageKeys.HalfPageDown
473+// return &messagesCmp{
474+// app: app,
475+// cachedContent: make(map[string]cacheItem),
476+// viewport: vp,
477+// spinner: s,
478+// attachments: attachmets,
479+// }
480+// }