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