list.go

  1package chat
  2
  3import "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{}
 47type MessageKeys struct {
 48	PageDown     key.Binding
 49	PageUp       key.Binding
 50	HalfPageUp   key.Binding
 51	HalfPageDown key.Binding
 52}
 53
 54var messageKeys = MessageKeys{
 55	PageDown: key.NewBinding(
 56		key.WithKeys("pgdown"),
 57		key.WithHelp("f/pgdn", "page down"),
 58	),
 59	PageUp: key.NewBinding(
 60		key.WithKeys("pgup"),
 61		key.WithHelp("b/pgup", "page up"),
 62	),
 63	HalfPageUp: key.NewBinding(
 64		key.WithKeys("ctrl+u"),
 65		key.WithHelp("ctrl+u", "½ page up"),
 66	),
 67	HalfPageDown: key.NewBinding(
 68		key.WithKeys("ctrl+d", "ctrl+d"),
 69		key.WithHelp("ctrl+d", "½ page down"),
 70	),
 71}
 72
 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// }