@@ -1,742 +0,0 @@
1−package chat
2−
3−import (
4− "context"
5− "encoding/json"
6− "fmt"
7− "math"
8− "strings"
9− "time"
10−
11− "github.com/charmbracelet/bubbles/key"
12− "github.com/charmbracelet/bubbles/spinner"
13− "github.com/charmbracelet/bubbles/viewport"
14− tea "github.com/charmbracelet/bubbletea"
15− "github.com/charmbracelet/glamour"
16− "github.com/charmbracelet/lipgloss"
17− "github.com/charmbracelet/x/ansi"
18− "github.com/kujtimiihoxha/opencode/internal/app"
19− "github.com/kujtimiihoxha/opencode/internal/llm/agent"
20− "github.com/kujtimiihoxha/opencode/internal/llm/models"
21− "github.com/kujtimiihoxha/opencode/internal/llm/tools"
22− "github.com/kujtimiihoxha/opencode/internal/logging"
23− "github.com/kujtimiihoxha/opencode/internal/message"
24− "github.com/kujtimiihoxha/opencode/internal/pubsub"
25− "github.com/kujtimiihoxha/opencode/internal/session"
26− "github.com/kujtimiihoxha/opencode/internal/tui/layout"
27− "github.com/kujtimiihoxha/opencode/internal/tui/styles"
28− "github.com/kujtimiihoxha/opencode/internal/tui/util"
29−)
30−
31−type uiMessageType int
32−
33−const (
34− userMessageType uiMessageType = iota
35− assistantMessageType
36− toolMessageType
37−)
38−
39−// messagesTickMsg is a message sent by the timer to refresh messages
40−type messagesTickMsg time.Time
41−
42−type uiMessage struct {
43− ID string
44− messageType uiMessageType
45− position int
46− height int
47− content string
48−}
49−
50−type messagesCmp struct {
51− app *app.App
52− width, height int
53− writingMode bool
54− viewport viewport.Model
55− session session.Session
56− messages []message.Message
57− uiMessages []uiMessage
58− currentMsgID string
59− renderer *glamour.TermRenderer
60− focusRenderer *glamour.TermRenderer
61− cachedContent map[string]string
62− spinner spinner.Model
63− needsRerender bool
64−}
65−
66−func (m *messagesCmp) Init() tea.Cmd {
67− return tea.Batch(m.viewport.Init(), m.spinner.Tick, m.tickMessages())
68−}
69−
70−func (m *messagesCmp) tickMessages() tea.Cmd {
71− return tea.Tick(time.Second, func(t time.Time) tea.Msg {
72− return messagesTickMsg(t)
73− })
74−}
75−
76−func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
77− var cmds []tea.Cmd
78− switch msg := msg.(type) {
79− case messagesTickMsg:
80− // Refresh messages if we have an active session
81− if m.session.ID != "" {
82− messages, err := m.app.Messages.List(context.Background(), m.session.ID)
83− if err == nil {
84− m.messages = messages
85− m.needsRerender = true
86− }
87− }
88− // Continue ticking
89− cmds = append(cmds, m.tickMessages())
90− case EditorFocusMsg:
91− m.writingMode = bool(msg)
92− case SessionSelectedMsg:
93− if msg.ID != m.session.ID {
94− cmd := m.SetSession(msg)
95− m.needsRerender = true
96− return m, cmd
97− }
98− return m, nil
99− case SessionClearedMsg:
100− m.session = session.Session{}
101− m.messages = make([]message.Message, 0)
102− m.currentMsgID = ""
103− m.needsRerender = true
104− m.cachedContent = make(map[string]string)
105− return m, nil
106−
107− case tea.KeyMsg:
108− if m.writingMode {
109− return m, nil
110− }
111− case pubsub.Event[message.Message]:
112− if msg.Type == pubsub.CreatedEvent {
113− if msg.Payload.SessionID == m.session.ID {
114− // check if message exists
115−
116− messageExists := false
117− for _, v := range m.messages {
118− if v.ID == msg.Payload.ID {
119− messageExists = true
120− break
121− }
122− }
123−
124− if !messageExists {
125− // If we have messages, ensure the previous last message is not cached
126− if len(m.messages) > 0 {
127− lastMsgID := m.messages[len(m.messages)-1].ID
128− delete(m.cachedContent, lastMsgID)
129− }
130−
131− m.messages = append(m.messages, msg.Payload)
132− delete(m.cachedContent, m.currentMsgID)
133− m.currentMsgID = msg.Payload.ID
134− m.needsRerender = true
135− }
136− }
137− for _, v := range m.messages {
138− for _, c := range v.ToolCalls() {
139− if c.ID == msg.Payload.SessionID {
140− m.needsRerender = true
141− }
142− }
143− }
144− } else if msg.Type == pubsub.UpdatedEvent && msg.Payload.SessionID == m.session.ID {
145− logging.Debug("Message", "finish", msg.Payload.FinishReason())
146− for i, v := range m.messages {
147− if v.ID == msg.Payload.ID {
148− m.messages[i] = msg.Payload
149− delete(m.cachedContent, msg.Payload.ID)
150−
151− // If this is the last message, ensure it's not cached
152− if i == len(m.messages)-1 {
153− delete(m.cachedContent, msg.Payload.ID)
154− }
155−
156− m.needsRerender = true
157− break
158− }
159− }
160− }
161− }
162−
163− oldPos := m.viewport.YPosition
164− u, cmd := m.viewport.Update(msg)
165− m.viewport = u
166− m.needsRerender = m.needsRerender || m.viewport.YPosition != oldPos
167− cmds = append(cmds, cmd)
168−
169− spinner, cmd := m.spinner.Update(msg)
170− m.spinner = spinner
171− cmds = append(cmds, cmd)
172−
173− if m.needsRerender {
174− m.renderView()
175− if len(m.messages) > 0 {
176− if msg, ok := msg.(pubsub.Event[message.Message]); ok {
177− if (msg.Type == pubsub.CreatedEvent) ||
178− (msg.Type == pubsub.UpdatedEvent && msg.Payload.ID == m.messages[len(m.messages)-1].ID) {
179− m.viewport.GotoBottom()
180− }
181− }
182− }
183− m.needsRerender = false
184− }
185− return m, tea.Batch(cmds...)
186−}
187−
188−func (m *messagesCmp) IsAgentWorking() bool {
189− return m.app.CoderAgent.IsSessionBusy(m.session.ID)
190−}
191−
192−func (m *messagesCmp) renderSimpleMessage(msg message.Message, info ...string) string {
193− // Check if this is the last message in the list
194− isLastMessage := len(m.messages) > 0 && m.messages[len(m.messages)-1].ID == msg.ID
195−
196− // Only use cache for non-last messages
197− if !isLastMessage {
198− if v, ok := m.cachedContent[msg.ID]; ok {
199− return v
200− }
201− }
202−
203− style := styles.BaseStyle.
204− Width(m.width).
205− BorderLeft(true).
206− Foreground(styles.ForgroundDim).
207− BorderForeground(styles.ForgroundDim).
208− BorderStyle(lipgloss.ThickBorder())
209−
210− renderer := m.renderer
211− if msg.ID == m.currentMsgID {
212− style = style.
213− Foreground(styles.Forground).
214− BorderForeground(styles.Blue).
215− BorderStyle(lipgloss.ThickBorder())
216− renderer = m.focusRenderer
217− }
218− c, _ := renderer.Render(msg.Content().String())
219− parts := []string{
220− styles.ForceReplaceBackgroundWithLipgloss(c, styles.Background),
221− }
222− // remove newline at the end
223− parts[0] = strings.TrimSuffix(parts[0], "\n")
224− if len(info) > 0 {
225− parts = append(parts, info...)
226− }
227− rendered := style.Render(
228− lipgloss.JoinVertical(
229− lipgloss.Left,
230− parts...,
231− ),
232− )
233−
234− // Only cache if it's not the last message
235− if !isLastMessage {
236− m.cachedContent[msg.ID] = rendered
237− }
238−
239− return rendered
240−}
241−
242−func formatTimeDifference(unixTime1, unixTime2 int64) string {
243− diffSeconds := float64(math.Abs(float64(unixTime2 - unixTime1)))
244−
245− if diffSeconds < 60 {
246− return fmt.Sprintf("%.1fs", diffSeconds)
247− }
248−
249− minutes := int(diffSeconds / 60)
250− seconds := int(diffSeconds) % 60
251− return fmt.Sprintf("%dm%ds", minutes, seconds)
252−}
253−
254−func (m *messagesCmp) findToolResponse(callID string) *message.ToolResult {
255− for _, v := range m.messages {
256− for _, c := range v.ToolResults() {
257− if c.ToolCallID == callID {
258− return &c
259− }
260− }
261− }
262− return nil
263−}
264−
265−func (m *messagesCmp) renderToolCall(toolCall message.ToolCall, isNested bool) string {
266− key := ""
267− value := ""
268− result := styles.BaseStyle.Foreground(styles.PrimaryColor).Render(m.spinner.View() + " waiting for response...")
269−
270− response := m.findToolResponse(toolCall.ID)
271− if response != nil && response.IsError {
272− // Clean up error message for display by removing newlines
273− // This ensures error messages display properly in the UI
274− errMsg := strings.ReplaceAll(response.Content, "\n", " ")
275− result = styles.BaseStyle.Foreground(styles.Error).Render(ansi.Truncate(errMsg, 40, "..."))
276− } else if response != nil {
277− result = styles.BaseStyle.Foreground(styles.ForgroundMid).Render("Done")
278− }
279− switch toolCall.Name {
280− // TODO: add result data to the tools
281− case agent.AgentToolName:
282− key = "Task"
283− var params agent.AgentParams
284− json.Unmarshal([]byte(toolCall.Input), ¶ms)
285− value = strings.ReplaceAll(params.Prompt, "\n", " ")
286− if response != nil && !response.IsError {
287− firstRow := strings.ReplaceAll(response.Content, "\n", " ")
288− result = styles.BaseStyle.Foreground(styles.ForgroundMid).Render(ansi.Truncate(firstRow, 40, "..."))
289− }
290− case tools.BashToolName:
291− key = "Bash"
292− var params tools.BashParams
293− json.Unmarshal([]byte(toolCall.Input), ¶ms)
294− value = params.Command
295− if response != nil && !response.IsError {
296− metadata := tools.BashResponseMetadata{}
297− json.Unmarshal([]byte(response.Metadata), &metadata)
298− result = styles.BaseStyle.Foreground(styles.ForgroundMid).Render(fmt.Sprintf("Took %s", formatTimeDifference(metadata.StartTime, metadata.EndTime)))
299− }
300−
301− case tools.EditToolName:
302− key = "Edit"
303− var params tools.EditParams
304− json.Unmarshal([]byte(toolCall.Input), ¶ms)
305− value = params.FilePath
306− if response != nil && !response.IsError {
307− metadata := tools.EditResponseMetadata{}
308− json.Unmarshal([]byte(response.Metadata), &metadata)
309− result = styles.BaseStyle.Foreground(styles.ForgroundMid).Render(fmt.Sprintf("%d Additions %d Removals", metadata.Additions, metadata.Removals))
310− }
311− case tools.FetchToolName:
312− key = "Fetch"
313− var params tools.FetchParams
314− json.Unmarshal([]byte(toolCall.Input), ¶ms)
315− value = params.URL
316− if response != nil && !response.IsError {
317− result = styles.BaseStyle.Foreground(styles.Error).Render(response.Content)
318− }
319− case tools.GlobToolName:
320− key = "Glob"
321− var params tools.GlobParams
322− json.Unmarshal([]byte(toolCall.Input), ¶ms)
323− if params.Path == "" {
324− params.Path = "."
325− }
326− value = fmt.Sprintf("%s (%s)", params.Pattern, params.Path)
327− if response != nil && !response.IsError {
328− metadata := tools.GlobResponseMetadata{}
329− json.Unmarshal([]byte(response.Metadata), &metadata)
330− if metadata.Truncated {
331− result = styles.BaseStyle.Foreground(styles.ForgroundMid).Render(fmt.Sprintf("%d files found (truncated)", metadata.NumberOfFiles))
332− } else {
333− result = styles.BaseStyle.Foreground(styles.ForgroundMid).Render(fmt.Sprintf("%d files found", metadata.NumberOfFiles))
334− }
335− }
336− case tools.GrepToolName:
337− key = "Grep"
338− var params tools.GrepParams
339− json.Unmarshal([]byte(toolCall.Input), ¶ms)
340− if params.Path == "" {
341− params.Path = "."
342− }
343− value = fmt.Sprintf("%s (%s)", params.Pattern, params.Path)
344− if response != nil && !response.IsError {
345− metadata := tools.GrepResponseMetadata{}
346− json.Unmarshal([]byte(response.Metadata), &metadata)
347− if metadata.Truncated {
348− result = styles.BaseStyle.Foreground(styles.ForgroundMid).Render(fmt.Sprintf("%d files found (truncated)", metadata.NumberOfMatches))
349− } else {
350− result = styles.BaseStyle.Foreground(styles.ForgroundMid).Render(fmt.Sprintf("%d files found", metadata.NumberOfMatches))
351− }
352− }
353− case tools.LSToolName:
354− key = "ls"
355− var params tools.LSParams
356− json.Unmarshal([]byte(toolCall.Input), ¶ms)
357− if params.Path == "" {
358− params.Path = "."
359− }
360− value = params.Path
361− if response != nil && !response.IsError {
362− metadata := tools.LSResponseMetadata{}
363− json.Unmarshal([]byte(response.Metadata), &metadata)
364− if metadata.Truncated {
365− result = styles.BaseStyle.Foreground(styles.ForgroundMid).Render(fmt.Sprintf("%d files found (truncated)", metadata.NumberOfFiles))
366− } else {
367− result = styles.BaseStyle.Foreground(styles.ForgroundMid).Render(fmt.Sprintf("%d files found", metadata.NumberOfFiles))
368− }
369− }
370− case tools.SourcegraphToolName:
371− key = "Sourcegraph"
372− var params tools.SourcegraphParams
373− json.Unmarshal([]byte(toolCall.Input), ¶ms)
374− value = params.Query
375− if response != nil && !response.IsError {
376− metadata := tools.SourcegraphResponseMetadata{}
377− json.Unmarshal([]byte(response.Metadata), &metadata)
378− if metadata.Truncated {
379− result = styles.BaseStyle.Foreground(styles.ForgroundMid).Render(fmt.Sprintf("%d matches found (truncated)", metadata.NumberOfMatches))
380− } else {
381− result = styles.BaseStyle.Foreground(styles.ForgroundMid).Render(fmt.Sprintf("%d matches found", metadata.NumberOfMatches))
382− }
383− }
384− case tools.ViewToolName:
385− key = "View"
386− var params tools.ViewParams
387− json.Unmarshal([]byte(toolCall.Input), ¶ms)
388− value = params.FilePath
389− case tools.WriteToolName:
390− key = "Write"
391− var params tools.WriteParams
392− json.Unmarshal([]byte(toolCall.Input), ¶ms)
393− value = params.FilePath
394− if response != nil && !response.IsError {
395− metadata := tools.WriteResponseMetadata{}
396− json.Unmarshal([]byte(response.Metadata), &metadata)
397−
398− result = styles.BaseStyle.Foreground(styles.ForgroundMid).Render(fmt.Sprintf("%d Additions %d Removals", metadata.Additions, metadata.Removals))
399− }
400− default:
401− key = toolCall.Name
402− var params map[string]any
403− json.Unmarshal([]byte(toolCall.Input), ¶ms)
404− jsonData, _ := json.Marshal(params)
405− value = string(jsonData)
406− }
407−
408− style := styles.BaseStyle.
409− Width(m.width).
410− BorderLeft(true).
411− BorderStyle(lipgloss.ThickBorder()).
412− PaddingLeft(1).
413− BorderForeground(styles.Yellow)
414−
415− keyStyle := styles.BaseStyle.
416− Foreground(styles.ForgroundDim)
417− valyeStyle := styles.BaseStyle.
418− Foreground(styles.Forground)
419−
420− if isNested {
421− valyeStyle = valyeStyle.Foreground(styles.ForgroundMid)
422− }
423− keyValye := keyStyle.Render(
424− fmt.Sprintf("%s: ", key),
425− )
426− if !isNested {
427− value = valyeStyle.
428− Render(
429− ansi.Truncate(
430− value+" ",
431− m.width-lipgloss.Width(keyValye)-2-lipgloss.Width(result),
432− "...",
433− ),
434− )
435− value += result
436−
437− } else {
438− keyValye = keyStyle.Render(
439− fmt.Sprintf(" └ %s: ", key),
440− )
441− value = valyeStyle.
442− Width(m.width - lipgloss.Width(keyValye) - 2).
443− Render(
444− ansi.Truncate(
445− value,
446− m.width-lipgloss.Width(keyValye)-2,
447− "...",
448− ),
449− )
450− }
451−
452− innerToolCalls := make([]string, 0)
453− if toolCall.Name == agent.AgentToolName {
454− messages, _ := m.app.Messages.List(context.Background(), toolCall.ID)
455− toolCalls := make([]message.ToolCall, 0)
456− for _, v := range messages {
457− toolCalls = append(toolCalls, v.ToolCalls()...)
458− }
459− for _, v := range toolCalls {
460− call := m.renderToolCall(v, true)
461− innerToolCalls = append(innerToolCalls, call)
462− }
463− }
464−
465− if isNested {
466− return lipgloss.JoinHorizontal(
467− lipgloss.Left,
468− keyValye,
469− value,
470− )
471− }
472− callContent := lipgloss.JoinHorizontal(
473− lipgloss.Left,
474− keyValye,
475− value,
476− )
477− callContent = strings.ReplaceAll(callContent, "\n", "")
478− if len(innerToolCalls) > 0 {
479− callContent = lipgloss.JoinVertical(
480− lipgloss.Left,
481− callContent,
482− lipgloss.JoinVertical(
483− lipgloss.Left,
484− innerToolCalls...,
485− ),
486− )
487− }
488− return style.Render(callContent)
489−}
490−
491−func (m *messagesCmp) renderAssistantMessage(msg message.Message) []uiMessage {
492− // find the user message that is before this assistant message
493− var userMsg message.Message
494− for i := len(m.messages) - 1; i >= 0; i-- {
495− if m.messages[i].Role == message.User {
496− userMsg = m.messages[i]
497− break
498− }
499− }
500− messages := make([]uiMessage, 0)
501− if msg.Content().String() != "" {
502− info := make([]string, 0)
503− if msg.IsFinished() && msg.FinishReason() == "end_turn" {
504− finish := msg.FinishPart()
505− took := formatTimeDifference(userMsg.CreatedAt, finish.Time)
506−
507− info = append(info, styles.BaseStyle.Width(m.width-1).Foreground(styles.ForgroundDim).Render(
508− fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, took),
509− ))
510− }
511− content := m.renderSimpleMessage(msg, info...)
512− messages = append(messages, uiMessage{
513− messageType: assistantMessageType,
514− position: 0, // gets updated in renderView
515− height: lipgloss.Height(content),
516− content: content,
517− })
518− }
519− for _, v := range msg.ToolCalls() {
520− content := m.renderToolCall(v, false)
521− messages = append(messages,
522− uiMessage{
523− messageType: toolMessageType,
524− position: 0, // gets updated in renderView
525− height: lipgloss.Height(content),
526− content: content,
527− },
528− )
529− }
530−
531− return messages
532−}
533−
534−func (m *messagesCmp) renderView() {
535− m.uiMessages = make([]uiMessage, 0)
536− pos := 0
537−
538− // If we have messages, ensure the last message is not cached
539− // This ensures we always render the latest content for the most recent message
540− // which may be actively updating (e.g., during generation)
541− if len(m.messages) > 0 {
542− lastMsgID := m.messages[len(m.messages)-1].ID
543− delete(m.cachedContent, lastMsgID)
544− }
545−
546− // Limit cache to 10 messages
547− if len(m.cachedContent) > 15 {
548− // Create a list of keys to delete (oldest messages first)
549− keys := make([]string, 0, len(m.cachedContent))
550− for k := range m.cachedContent {
551− keys = append(keys, k)
552− }
553− // Delete oldest messages until we have 10 or fewer
554− for i := 0; i < len(keys)-15; i++ {
555− delete(m.cachedContent, keys[i])
556− }
557− }
558−
559− for _, v := range m.messages {
560− switch v.Role {
561− case message.User:
562− content := m.renderSimpleMessage(v)
563− m.uiMessages = append(m.uiMessages, uiMessage{
564− messageType: userMessageType,
565− position: pos,
566− height: lipgloss.Height(content),
567− content: content,
568− })
569− pos += lipgloss.Height(content) + 1 // + 1 for spacing
570− case message.Assistant:
571− assistantMessages := m.renderAssistantMessage(v)
572− for _, msg := range assistantMessages {
573− msg.position = pos
574− m.uiMessages = append(m.uiMessages, msg)
575− pos += msg.height + 1 // + 1 for spacing
576− }
577−
578− }
579− }
580−
581− messages := make([]string, 0)
582− for _, v := range m.uiMessages {
583− messages = append(messages, v.content,
584− styles.BaseStyle.
585− Width(m.width).
586− Render(
587− "",
588− ),
589− )
590− }
591− m.viewport.SetContent(
592− styles.BaseStyle.
593− Width(m.width).
594− Render(
595− lipgloss.JoinVertical(
596− lipgloss.Top,
597− messages...,
598− ),
599− ),
600− )
601−}
602−
603−func (m *messagesCmp) View() string {
604− if len(m.messages) == 0 {
605− content := styles.BaseStyle.
606− Width(m.width).
607− Height(m.height - 1).
608− Render(
609− m.initialScreen(),
610− )
611−
612− return styles.BaseStyle.
613− Width(m.width).
614− Render(
615− lipgloss.JoinVertical(
616− lipgloss.Top,
617− content,
618− m.help(),
619− ),
620− )
621− }
622−
623− return styles.BaseStyle.
624− Width(m.width).
625− Render(
626− lipgloss.JoinVertical(
627− lipgloss.Top,
628− m.viewport.View(),
629− m.help(),
630− ),
631− )
632−}
633−
634−func (m *messagesCmp) help() string {
635− text := ""
636−
637− if m.IsAgentWorking() {
638− text += styles.BaseStyle.Foreground(styles.PrimaryColor).Bold(true).Render(
639− fmt.Sprintf("%s %s ", m.spinner.View(), "Generating..."),
640− )
641− }
642− if m.writingMode {
643− text += lipgloss.JoinHorizontal(
644− lipgloss.Left,
645− styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render("press "),
646− styles.BaseStyle.Foreground(styles.Forground).Bold(true).Render("esc"),
647− styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render(" to exit writing mode"),
648− )
649− } else {
650− text += lipgloss.JoinHorizontal(
651− lipgloss.Left,
652− styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render("press "),
653− styles.BaseStyle.Foreground(styles.Forground).Bold(true).Render("i"),
654− styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render(" to start writing"),
655− )
656− }
657−
658− return styles.BaseStyle.
659− Width(m.width).
660− Render(text)
661−}
662−
663−func (m *messagesCmp) initialScreen() string {
664− return styles.BaseStyle.Width(m.width).Render(
665− lipgloss.JoinVertical(
666− lipgloss.Top,
667− header(m.width),
668− "",
669− lspsConfigured(m.width),
670− ),
671− )
672−}
673−
674−func (m *messagesCmp) SetSize(width, height int) {
675− m.width = width
676− m.height = height
677− m.viewport.Width = width
678− m.viewport.Height = height - 1
679− focusRenderer, _ := glamour.NewTermRenderer(
680− glamour.WithStyles(styles.MarkdownTheme(true)),
681− glamour.WithWordWrap(width-1),
682− )
683− renderer, _ := glamour.NewTermRenderer(
684− glamour.WithStyles(styles.MarkdownTheme(false)),
685− glamour.WithWordWrap(width-1),
686− )
687− m.focusRenderer = focusRenderer
688− // clear the cached content
689− for k := range m.cachedContent {
690− delete(m.cachedContent, k)
691− }
692− m.renderer = renderer
693− if len(m.messages) > 0 {
694− m.renderView()
695− m.viewport.GotoBottom()
696− }
697−}
698−
699−func (m *messagesCmp) GetSize() (int, int) {
700− return m.width, m.height
701−}
702−
703−func (m *messagesCmp) SetSession(session session.Session) tea.Cmd {
704− m.session = session
705− messages, err := m.app.Messages.List(context.Background(), session.ID)
706− if err != nil {
707− return util.ReportError(err)
708− }
709− m.messages = messages
710− m.currentMsgID = m.messages[len(m.messages)-1].ID
711− m.needsRerender = true
712− m.cachedContent = make(map[string]string)
713− return nil
714−}
715−
716−func (m *messagesCmp) BindingKeys() []key.Binding {
717− bindings := layout.KeyMapToSlice(m.viewport.KeyMap)
718− return bindings
719−}
720−
721−func NewMessagesCmp(app *app.App) tea.Model {
722− focusRenderer, _ := glamour.NewTermRenderer(
723− glamour.WithStyles(styles.MarkdownTheme(true)),
724− glamour.WithWordWrap(80),
725− )
726− renderer, _ := glamour.NewTermRenderer(
727− glamour.WithStyles(styles.MarkdownTheme(false)),
728− glamour.WithWordWrap(80),
729− )
730−
731− s := spinner.New()
732− s.Spinner = spinner.Pulse
733− return &messagesCmp{
734− app: app,
735− writingMode: true,
736− cachedContent: make(map[string]string),
737− viewport: viewport.New(0, 0),
738− focusRenderer: focusRenderer,
739− renderer: renderer,
740− spinner: s,
741− }
742−}