on main help sign in
fix tools calls
commit9c4a47b895c5022d75696fb66658bbfd74de3b6e authorKujtim Hoxha <kujtimii.h@gmail.com> dateThu 22 May 2025, 17:43 (1 year ago) parent25e9651
5 files changed+476−1078
internal/tui/components/chat/editor.go+2−2 internal/tui/components/chat/list.go+452−446 internal/tui/components/chat/list_v2.go+8 internal/tui/components/chat/message.go−629 internal/tui/components/core/list/list.go+14−1
internal/tui/components/chat/editor.go+2−2
@@ -211,7 +211,6 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
211211 return m, m.send()
212212 }
213213 }
214−
215214 }
216215 m.textarea, cmd = m.textarea.Update(msg)
217216 return m, cmd
@@ -233,7 +232,8 @@ func (m *editorCmp) View() string {
233232 return lipgloss.JoinVertical(lipgloss.Top,
234233 m.attachmentsContent(),
235234 lipgloss.JoinHorizontal(lipgloss.Top, style.Render(">"),
236− m.textarea.View()),
235+ m.textarea.View(),
236+ ),
237237 )
238238}
239239
internal/tui/components/chat/list.go+452−446
@@ -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+// }
internal/tui/components/chat/list_v2.go+8
@@ -106,6 +106,8 @@ func (m *messageListCmp) handleMessageEvent(event pubsub.Event[message.Message])
106106 return m.handleNewUserMessage(event.Payload)
107107 case message.Assistant:
108108 return m.handleNewAssistantMessage(event.Payload)
109+ case message.Tool:
110+ return m.handleToolMessage(event.Payload)
109111 }
110112 // TODO: handle tools
111113 case pubsub.UpdatedEvent:
@@ -119,6 +121,10 @@ func (m *messageListCmp) handleNewUserMessage(msg message.Message) tea.Cmd {
119121 return m.listCmp.AppendItem(messages.NewMessageCmp(msg))
120122}
121123
124+func (m *messageListCmp) handleToolMessage(msg message.Message) tea.Cmd {
125+ return nil
126+}
127+
122128func (m *messageListCmp) handleUpdateAssistantMessage(msg message.Message) tea.Cmd {
123129 // Simple update the content
124130 items := m.listCmp.Items()
@@ -136,6 +142,8 @@ func (m *messageListCmp) handleUpdateAssistantMessage(msg message.Message) tea.C
136142 messages.WithLastUserMessageTime(time.Unix(m.lastUserMessageTime, 0)),
137143 ),
138144 )
145+ } else if len(msg.ToolCalls()) > 0 && msg.Content().Text == "" {
146+ m.listCmp.DeleteItem(len(items) - 1)
139147 }
140148 return nil
141149}
internal/tui/components/chat/message.godeleted−629
@@ -1,629 +0,0 @@
1−package chat
2−
3−import (
4− "context"
5− "encoding/json"
6− "fmt"
7− "path/filepath"
8− "strings"
9− "time"
10−
11− "github.com/charmbracelet/lipgloss/v2"
12− "github.com/charmbracelet/x/ansi"
13− "github.com/opencode-ai/opencode/internal/config"
14− "github.com/opencode-ai/opencode/internal/diff"
15− "github.com/opencode-ai/opencode/internal/llm/agent"
16− "github.com/opencode-ai/opencode/internal/llm/models"
17− "github.com/opencode-ai/opencode/internal/llm/tools"
18− "github.com/opencode-ai/opencode/internal/message"
19− "github.com/opencode-ai/opencode/internal/tui/styles"
20− "github.com/opencode-ai/opencode/internal/tui/theme"
21−)
22−
23−type uiMessageType int
24−
25−const (
26− userMessageType uiMessageType = iota
27− assistantMessageType
28− toolMessageType
29−
30− maxResultHeight = 10
31−)
32−
33−type uiMessage struct {
34− ID string
35− messageType uiMessageType
36− position int
37− height int
38− content string
39−}
40−
41−func toMarkdown(content string, focused bool, width int) string {
42− r := styles.GetMarkdownRenderer(width)
43− rendered, _ := r.Render(content)
44− return rendered
45−}
46−
47−func renderMessage(msg string, isUser bool, isFocused bool, width int, info ...string) string {
48− t := theme.CurrentTheme()
49−
50− style := styles.BaseStyle().
51− Width(width - 1).
52− BorderLeft(true).
53− Foreground(t.TextMuted()).
54− BorderForeground(t.Primary()).
55− BorderStyle(lipgloss.ThickBorder())
56−
57− if isUser {
58− style = style.BorderForeground(t.Secondary())
59− }
60−
61− // Apply markdown formatting and handle background color
62− parts := []string{
63− toMarkdown(msg, isFocused, width),
64− }
65−
66− // Remove newline at the end
67− parts[0] = strings.TrimSuffix(parts[0], "\n")
68− if len(info) > 0 {
69− parts = append(parts, info...)
70− }
71−
72− rendered := style.Render(
73− lipgloss.JoinVertical(
74− lipgloss.Left,
75− parts...,
76− ),
77− )
78−
79− return rendered
80−}
81−
82−func renderUserMessage(msg message.Message, isFocused bool, width int, position int) uiMessage {
83− var styledAttachments []string
84− t := theme.CurrentTheme()
85− attachmentStyles := styles.BaseStyle().
86− MarginLeft(1).
87− Background(t.TextMuted()).
88− Foreground(t.Text())
89− for _, attachment := range msg.BinaryContent() {
90− file := filepath.Base(attachment.Path)
91− var filename string
92− if len(file) > 10 {
93− filename = fmt.Sprintf(" %s %s...", styles.DocumentIcon, file[0:7])
94− } else {
95− filename = fmt.Sprintf(" %s %s", styles.DocumentIcon, file)
96− }
97− styledAttachments = append(styledAttachments, attachmentStyles.Render(filename))
98− }
99− content := ""
100− if len(styledAttachments) > 0 {
101− attachmentContent := styles.BaseStyle().Width(width).Render(lipgloss.JoinHorizontal(lipgloss.Left, styledAttachments...))
102− content = renderMessage(msg.Content().String(), true, isFocused, width, attachmentContent)
103− } else {
104− content = renderMessage(msg.Content().String(), true, isFocused, width)
105− }
106− userMsg := uiMessage{
107− ID: msg.ID,
108− messageType: userMessageType,
109− position: position,
110− height: lipgloss.Height(content),
111− content: content,
112− }
113− return userMsg
114−}
115−
116−// Returns multiple uiMessages because of the tool calls
117−func renderAssistantMessage(
118− msg message.Message,
119− msgIndex int,
120− allMessages []message.Message, // we need this to get tool results and the user message
121− messagesService message.Service, // We need this to get the task tool messages
122− focusedUIMessageId string,
123− isSummary bool,
124− width int,
125− position int,
126−) []uiMessage {
127− messages := []uiMessage{}
128− content := msg.Content().String()
129− thinking := msg.IsThinking()
130− thinkingContent := msg.ReasoningContent().Thinking
131− finished := msg.IsFinished()
132− finishData := msg.FinishPart()
133− info := []string{}
134−
135− t := theme.CurrentTheme()
136− baseStyle := styles.BaseStyle()
137−
138− // Add finish info if available
139− if finished {
140− switch finishData.Reason {
141− case message.FinishReasonEndTurn:
142− took := formatTimestampDiff(msg.CreatedAt, finishData.Time)
143− info = append(info, baseStyle.
144− Width(width-1).
145− Foreground(t.TextMuted()).
146− Render(fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, took)),
147− )
148− case message.FinishReasonCanceled:
149− info = append(info, baseStyle.
150− Width(width-1).
151− Foreground(t.TextMuted()).
152− Render(fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, "canceled")),
153− )
154− case message.FinishReasonError:
155− info = append(info, baseStyle.
156− Width(width-1).
157− Foreground(t.TextMuted()).
158− Render(fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, "error")),
159− )
160− case message.FinishReasonPermissionDenied:
161− info = append(info, baseStyle.
162− Width(width-1).
163− Foreground(t.TextMuted()).
164− Render(fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, "permission denied")),
165− )
166− }
167− }
168− if content != "" || (finished && finishData.Reason == message.FinishReasonEndTurn) {
169− if content == "" {
170− content = "*Finished without output*"
171− }
172− if isSummary {
173− info = append(info, baseStyle.Width(width-1).Foreground(t.TextMuted()).Render(" (summary)"))
174− }
175−
176− content = renderMessage(content, false, true, width, info...)
177− messages = append(messages, uiMessage{
178− ID: msg.ID,
179− messageType: assistantMessageType,
180− position: position,
181− height: lipgloss.Height(content),
182− content: content,
183− })
184− position += messages[0].height
185− position++ // for the space
186− } else if thinking && thinkingContent != "" {
187− // Render the thinking content
188− content = renderMessage(thinkingContent, false, msg.ID == focusedUIMessageId, width)
189− }
190−
191− for i, toolCall := range msg.ToolCalls() {
192− toolCallContent := renderToolMessage(
193− toolCall,
194− allMessages,
195− messagesService,
196− focusedUIMessageId,
197− false,
198− width,
199− i+1,
200− )
201− messages = append(messages, toolCallContent)
202− position += toolCallContent.height
203− position++ // for the space
204− }
205− return messages
206−}
207−
208−func findToolResponse(toolCallID string, futureMessages []message.Message) *message.ToolResult {
209− for _, msg := range futureMessages {
210− for _, result := range msg.ToolResults() {
211− if result.ToolCallID == toolCallID {
212− return &result
213− }
214− }
215− }
216− return nil
217−}
218−
219−func toolName(name string) string {
220− switch name {
221− case agent.AgentToolName:
222− return "Task"
223− case tools.BashToolName:
224− return "Bash"
225− case tools.EditToolName:
226− return "Edit"
227− case tools.FetchToolName:
228− return "Fetch"
229− case tools.GlobToolName:
230− return "Glob"
231− case tools.GrepToolName:
232− return "Grep"
233− case tools.LSToolName:
234− return "List"
235− case tools.SourcegraphToolName:
236− return "Sourcegraph"
237− case tools.ViewToolName:
238− return "View"
239− case tools.WriteToolName:
240− return "Write"
241− case tools.PatchToolName:
242− return "Patch"
243− }
244− return name
245−}
246−
247−func getToolAction(name string) string {
248− switch name {
249− case agent.AgentToolName:
250− return "Preparing prompt..."
251− case tools.BashToolName:
252− return "Building command..."
253− case tools.EditToolName:
254− return "Preparing edit..."
255− case tools.FetchToolName:
256− return "Writing fetch..."
257− case tools.GlobToolName:
258− return "Finding files..."
259− case tools.GrepToolName:
260− return "Searching content..."
261− case tools.LSToolName:
262− return "Listing directory..."
263− case tools.SourcegraphToolName:
264− return "Searching code..."
265− case tools.ViewToolName:
266− return "Reading file..."
267− case tools.WriteToolName:
268− return "Preparing write..."
269− case tools.PatchToolName:
270− return "Preparing patch..."
271− }
272− return "Working..."
273−}
274−
275−func renderToolParams(paramWidth int, toolCall message.ToolCall) string {
276− params := ""
277− switch toolCall.Name {
278− case agent.AgentToolName:
279− var params agent.AgentParams
280− json.Unmarshal([]byte(toolCall.Input), &params)
281− prompt := strings.ReplaceAll(params.Prompt, "\n", " ")
282− return renderParams(paramWidth, prompt)
283− case tools.BashToolName:
284− var params tools.BashParams
285− json.Unmarshal([]byte(toolCall.Input), &params)
286− command := strings.ReplaceAll(params.Command, "\n", " ")
287− return renderParams(paramWidth, command)
288− case tools.EditToolName:
289− var params tools.EditParams
290− json.Unmarshal([]byte(toolCall.Input), &params)
291− filePath := removeWorkingDirPrefix(params.FilePath)
292− return renderParams(paramWidth, filePath)
293− case tools.FetchToolName:
294− var params tools.FetchParams
295− json.Unmarshal([]byte(toolCall.Input), &params)
296− url := params.URL
297− toolParams := []string{
298− url,
299− }
300− if params.Format != "" {
301− toolParams = append(toolParams, "format", params.Format)
302− }
303− if params.Timeout != 0 {
304− toolParams = append(toolParams, "timeout", (time.Duration(params.Timeout) * time.Second).String())
305− }
306− return renderParams(paramWidth, toolParams...)
307− case tools.GlobToolName:
308− var params tools.GlobParams
309− json.Unmarshal([]byte(toolCall.Input), &params)
310− pattern := params.Pattern
311− toolParams := []string{
312− pattern,
313− }
314− if params.Path != "" {
315− toolParams = append(toolParams, "path", params.Path)
316− }
317− return renderParams(paramWidth, toolParams...)
318− case tools.GrepToolName:
319− var params tools.GrepParams
320− json.Unmarshal([]byte(toolCall.Input), &params)
321− pattern := params.Pattern
322− toolParams := []string{
323− pattern,
324− }
325− if params.Path != "" {
326− toolParams = append(toolParams, "path", params.Path)
327− }
328− if params.Include != "" {
329− toolParams = append(toolParams, "include", params.Include)
330− }
331− if params.LiteralText {
332− toolParams = append(toolParams, "literal", "true")
333− }
334− return renderParams(paramWidth, toolParams...)
335− case tools.LSToolName:
336− var params tools.LSParams
337− json.Unmarshal([]byte(toolCall.Input), &params)
338− path := params.Path
339− if path == "" {
340− path = "."
341− }
342− return renderParams(paramWidth, path)
343− case tools.SourcegraphToolName:
344− var params tools.SourcegraphParams
345− json.Unmarshal([]byte(toolCall.Input), &params)
346− return renderParams(paramWidth, params.Query)
347− case tools.ViewToolName:
348− var params tools.ViewParams
349− json.Unmarshal([]byte(toolCall.Input), &params)
350− filePath := removeWorkingDirPrefix(params.FilePath)
351− toolParams := []string{
352− filePath,
353− }
354− if params.Limit != 0 {
355− toolParams = append(toolParams, "limit", fmt.Sprintf("%d", params.Limit))
356− }
357− if params.Offset != 0 {
358− toolParams = append(toolParams, "offset", fmt.Sprintf("%d", params.Offset))
359− }
360− return renderParams(paramWidth, toolParams...)
361− case tools.WriteToolName:
362− var params tools.WriteParams
363− json.Unmarshal([]byte(toolCall.Input), &params)
364− filePath := removeWorkingDirPrefix(params.FilePath)
365− return renderParams(paramWidth, filePath)
366− default:
367− input := strings.ReplaceAll(toolCall.Input, "\n", " ")
368− params = renderParams(paramWidth, input)
369− }
370− return params
371−}
372−
373−func renderToolResponse(toolCall message.ToolCall, response message.ToolResult, width int) string {
374− t := theme.CurrentTheme()
375− baseStyle := styles.BaseStyle()
376−
377− if response.IsError {
378− errContent := fmt.Sprintf("Error: %s", strings.ReplaceAll(response.Content, "\n", " "))
379− errContent = ansi.Truncate(errContent, width-1, "...")
380− return baseStyle.
381− Width(width).
382− Foreground(t.Error()).
383− Render(errContent)
384− }
385−
386− resultContent := truncateHeight(response.Content, maxResultHeight)
387− switch toolCall.Name {
388− case agent.AgentToolName:
389− return toMarkdown(resultContent, false, width)
390− case tools.BashToolName:
391− resultContent = fmt.Sprintf("```bash\n%s\n```", resultContent)
392− return toMarkdown(resultContent, true, width)
393− case tools.EditToolName:
394− metadata := tools.EditResponseMetadata{}
395− json.Unmarshal([]byte(response.Metadata), &metadata)
396− truncDiff := truncateHeight(metadata.Diff, maxResultHeight)
397− formattedDiff, _ := diff.FormatDiff(truncDiff, diff.WithTotalWidth(width))
398− return formattedDiff
399− case tools.FetchToolName:
400− var params tools.FetchParams
401− json.Unmarshal([]byte(toolCall.Input), &params)
402− mdFormat := "markdown"
403− switch params.Format {
404− case "text":
405− mdFormat = "text"
406− case "html":
407− mdFormat = "html"
408− }
409− resultContent = fmt.Sprintf("```%s\n%s\n```", mdFormat, resultContent)
410− return toMarkdown(resultContent, true, width)
411− case tools.GlobToolName:
412− return baseStyle.Width(width).Foreground(t.TextMuted()).Render(resultContent)
413− case tools.GrepToolName:
414− return baseStyle.Width(width).Foreground(t.TextMuted()).Render(resultContent)
415− case tools.LSToolName:
416− return baseStyle.Width(width).Foreground(t.TextMuted()).Render(resultContent)
417− case tools.SourcegraphToolName:
418− return baseStyle.Width(width).Foreground(t.TextMuted()).Render(resultContent)
419− case tools.ViewToolName:
420− metadata := tools.ViewResponseMetadata{}
421− json.Unmarshal([]byte(response.Metadata), &metadata)
422− ext := filepath.Ext(metadata.FilePath)
423− if ext == "" {
424− ext = ""
425− } else {
426− ext = strings.ToLower(ext[1:])
427− }
428− resultContent = fmt.Sprintf("```%s\n%s\n```", ext, truncateHeight(metadata.Content, maxResultHeight))
429− return toMarkdown(resultContent, true, width)
430− case tools.WriteToolName:
431− params := tools.WriteParams{}
432− json.Unmarshal([]byte(toolCall.Input), &params)
433− metadata := tools.WriteResponseMetadata{}
434− json.Unmarshal([]byte(response.Metadata), &metadata)
435− ext := filepath.Ext(params.FilePath)
436− if ext == "" {
437− ext = ""
438− } else {
439− ext = strings.ToLower(ext[1:])
440− }
441− resultContent = fmt.Sprintf("```%s\n%s\n```", ext, truncateHeight(params.Content, maxResultHeight))
442− return toMarkdown(resultContent, true, width)
443− default:
444− resultContent = fmt.Sprintf("```text\n%s\n```", resultContent)
445− return toMarkdown(resultContent, true, width)
446− }
447−}
448−
449−func renderToolMessage(
450− toolCall message.ToolCall,
451− allMessages []message.Message,
452− messagesService message.Service,
453− focusedUIMessageId string,
454− nested bool,
455− width int,
456− position int,
457−) uiMessage {
458− if nested {
459− width = width - 3
460− }
461−
462− t := theme.CurrentTheme()
463− baseStyle := styles.BaseStyle()
464−
465− style := baseStyle.
466− Width(width - 1).
467− BorderLeft(true).
468− BorderStyle(lipgloss.ThickBorder()).
469− PaddingLeft(1).
470− BorderForeground(t.TextMuted())
471−
472− response := findToolResponse(toolCall.ID, allMessages)
473− toolNameText := baseStyle.Foreground(t.TextMuted()).
474− Render(fmt.Sprintf("%s: ", toolName(toolCall.Name)))
475−
476− if !toolCall.Finished {
477− // Get a brief description of what the tool is doing
478− toolAction := getToolAction(toolCall.Name)
479−
480− progressText := baseStyle.
481− Width(width - 2 - lipgloss.Width(toolNameText)).
482− Foreground(t.TextMuted()).
483− Render(fmt.Sprintf("%s", toolAction))
484−
485− content := style.Render(lipgloss.JoinHorizontal(lipgloss.Left, toolNameText, progressText))
486− toolMsg := uiMessage{
487− messageType: toolMessageType,
488− position: position,
489− height: lipgloss.Height(content),
490− content: content,
491− }
492− return toolMsg
493− }
494−
495− params := renderToolParams(width-2-lipgloss.Width(toolNameText), toolCall)
496− responseContent := ""
497− if response != nil {
498− responseContent = renderToolResponse(toolCall, *response, width-2)
499− responseContent = strings.TrimSuffix(responseContent, "\n")
500− } else {
501− responseContent = baseStyle.
502− Italic(true).
503− Width(width - 2).
504− Foreground(t.TextMuted()).
505− Render("Waiting for response...")
506− }
507−
508− parts := []string{}
509− if !nested {
510− formattedParams := baseStyle.
511− Width(width - 2 - lipgloss.Width(toolNameText)).
512− Foreground(t.TextMuted()).
513− Render(params)
514−
515− parts = append(parts, lipgloss.JoinHorizontal(lipgloss.Left, toolNameText, formattedParams))
516− } else {
517− prefix := baseStyle.
518− Foreground(t.TextMuted()).
519− Render(" └ ")
520− formattedParams := baseStyle.
521− Width(width - 2 - lipgloss.Width(toolNameText)).
522− Foreground(t.TextMuted()).
523− Render(params)
524− parts = append(parts, lipgloss.JoinHorizontal(lipgloss.Left, prefix, toolNameText, formattedParams))
525− }
526−
527− if toolCall.Name == agent.AgentToolName {
528− taskMessages, _ := messagesService.List(context.Background(), toolCall.ID)
529− toolCalls := []message.ToolCall{}
530− for _, v := range taskMessages {
531− toolCalls = append(toolCalls, v.ToolCalls()...)
532− }
533− for _, call := range toolCalls {
534− rendered := renderToolMessage(call, []message.Message{}, messagesService, focusedUIMessageId, true, width, 0)
535− parts = append(parts, rendered.content)
536− }
537− }
538− if responseContent != "" && !nested {
539− parts = append(parts, responseContent)
540− }
541−
542− content := style.Render(
543− lipgloss.JoinVertical(
544− lipgloss.Left,
545− parts...,
546− ),
547− )
548− if nested {
549− content = lipgloss.JoinVertical(
550− lipgloss.Left,
551− parts...,
552− )
553− }
554− toolMsg := uiMessage{
555− messageType: toolMessageType,
556− position: position,
557− height: lipgloss.Height(content),
558− content: content,
559− }
560− return toolMsg
561−}
562−
563−func removeWorkingDirPrefix(path string) string {
564− wd := config.WorkingDirectory()
565− path = strings.TrimPrefix(path, wd)
566− return path
567−}
568−
569−func truncateHeight(content string, height int) string {
570− lines := strings.Split(content, "\n")
571− if len(lines) > height {
572− return strings.Join(lines[:height], "\n")
573− }
574− return content
575−}
576−
577−func renderParams(paramsWidth int, params ...string) string {
578− if len(params) == 0 {
579− return ""
580− }
581− mainParam := params[0]
582− if len(mainParam) > paramsWidth {
583− mainParam = mainParam[:paramsWidth-3] + "..."
584− }
585−
586− if len(params) == 1 {
587− return mainParam
588− }
589− otherParams := params[1:]
590− // create pairs of key/value
591− // if odd number of params, the last one is a key without value
592− if len(otherParams)%2 != 0 {
593− otherParams = append(otherParams, "")
594− }
595− parts := make([]string, 0, len(otherParams)/2)
596− for i := 0; i < len(otherParams); i += 2 {
597− key := otherParams[i]
598− value := otherParams[i+1]
599− if value == "" {
600− continue
601− }
602− parts = append(parts, fmt.Sprintf("%s=%s", key, value))
603− }
604−
605− partsRendered := strings.Join(parts, ", ")
606− remainingWidth := paramsWidth - lipgloss.Width(partsRendered) - 3 // count for " ()"
607− if remainingWidth < 30 {
608− // No space for the params, just show the main
609− return mainParam
610− }
611−
612− if len(parts) > 0 {
613− mainParam = fmt.Sprintf("%s (%s)", mainParam, strings.Join(parts, ", "))
614− }
615−
616− return ansi.Truncate(mainParam, paramsWidth, "...")
617−}
618−
619−// Helper function to format the time difference between two Unix timestamps
620−func formatTimestampDiff(start, end int64) string {
621− diffSeconds := float64(end-start) / 1000.0 // Convert to seconds
622− if diffSeconds < 1 {
623− return fmt.Sprintf("%dms", int(diffSeconds*1000))
624− }
625− if diffSeconds < 60 {
626− return fmt.Sprintf("%.1fs", diffSeconds)
627− }
628− return fmt.Sprintf("%.1fm", diffSeconds/60)
629−}
internal/tui/components/core/list/list.go+14−1
@@ -492,6 +492,7 @@ func (m *model) rerenderItem(inx int) {
492492 }
493493 // check if the item is in the content
494494 start := cachedItem.start
495+ logging.Info("rerenderItem", "inx", inx, "start", start, "cachedItem.start", cachedItem.start, "cachedItem.height", cachedItem.height)
495496 end := start + cachedItem.height
496497 totalLines := len(m.renderedLines)
497498 if m.reverse {
@@ -504,6 +505,9 @@ func (m *model) rerenderItem(inx int) {
504505 }
505506 // TODO: if hight changed do something
506507 if cachedItem.height != len(rerenderedLines) && inx != len(m.items)-1 {
508+ if inx == len(m.items)-1 {
509+ m.finalHight = max(0, start+len(rerenderedLines)-m.listHeight())
510+ }
507511 }
508512 m.renderedItems.Store(inx, renderedItem{
509513 lines: rerenderedLines,
@@ -541,7 +545,12 @@ func (m *model) decreaseOffset(n int) {
541545// UpdateItem implements List.
542546func (m *model) UpdateItem(inx int, item util.Model) {
543547 m.items[inx] = item
544− m.rerenderItem(inx)
548+ if m.selectedItemInx == inx {
549+ if i, ok := m.items[m.selectedItemInx].(layout.Focusable); ok {
550+ i.Focus()
551+ }
552+ }
553+ m.ResetView()
545554 m.needsRerender = true
546555}
547556
@@ -614,6 +623,10 @@ func (m *model) AppendItem(item util.Model) tea.Cmd {
614623func (m *model) DeleteItem(i int) {
615624 m.items = slices.Delete(m.items, i, i+1)
616625 m.renderedItems.Delete(i)
626+ if m.selectedItemInx == i {
627+ m.selectedItemInx--
628+ }
629+ m.ResetView()
617630 m.needsRerender = true
618631}
619632