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// }