From 3944930fc04a57c3da9c80d9d7377effd1277004 Mon Sep 17 00:00:00 2001 From: adamdottv <2363879+adamdottv@users.noreply.github.com> Date: Thu, 15 May 2025 15:45:22 -0500 Subject: chore: cleanup --- internal/tui/components/chat/editor.go | 1 - internal/tui/components/chat/list.go | 483 ------------------------------- internal/tui/components/chat/messages.go | 483 +++++++++++++++++++++++++++++++ internal/tui/layout/container.go | 11 +- 4 files changed, 487 insertions(+), 491 deletions(-) delete mode 100644 internal/tui/components/chat/list.go create mode 100644 internal/tui/components/chat/messages.go diff --git a/internal/tui/components/chat/editor.go b/internal/tui/components/chat/editor.go index 0b2c9abb8..4d5ba0128 100644 --- a/internal/tui/components/chat/editor.go +++ b/internal/tui/components/chat/editor.go @@ -243,7 +243,6 @@ func (m *editorCmp) SetSize(width, height int) tea.Cmd { m.height = height m.textarea.SetWidth(width - 3) // account for the prompt and padding right m.textarea.SetHeight(height) - m.textarea.SetWidth(width) return nil } diff --git a/internal/tui/components/chat/list.go b/internal/tui/components/chat/list.go deleted file mode 100644 index baa7c7e6d..000000000 --- a/internal/tui/components/chat/list.go +++ /dev/null @@ -1,483 +0,0 @@ -package chat - -import ( - "context" - "fmt" - "math" - "time" - - "github.com/charmbracelet/bubbles/key" - "github.com/charmbracelet/bubbles/spinner" - "github.com/charmbracelet/bubbles/viewport" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" - "github.com/sst/opencode/internal/app" - "github.com/sst/opencode/internal/message" - "github.com/sst/opencode/internal/pubsub" - "github.com/sst/opencode/internal/session" - "github.com/sst/opencode/internal/status" - "github.com/sst/opencode/internal/tui/components/dialog" - "github.com/sst/opencode/internal/tui/state" - "github.com/sst/opencode/internal/tui/styles" - "github.com/sst/opencode/internal/tui/theme" -) - -type cacheItem struct { - width int - content []uiMessage -} - -type messagesCmp struct { - app *app.App - width, height int - viewport viewport.Model - messages []message.Message - uiMessages []uiMessage - currentMsgID string - cachedContent map[string]cacheItem - spinner spinner.Model - rendering bool - attachments viewport.Model - showToolMessages bool -} -type renderFinishedMsg struct{} -type ToggleToolMessagesMsg struct{} - -type MessageKeys struct { - PageDown key.Binding - PageUp key.Binding - HalfPageUp key.Binding - HalfPageDown key.Binding -} - -var messageKeys = MessageKeys{ - PageDown: key.NewBinding( - key.WithKeys("pgdown"), - key.WithHelp("f/pgdn", "page down"), - ), - PageUp: key.NewBinding( - key.WithKeys("pgup"), - key.WithHelp("b/pgup", "page up"), - ), - HalfPageUp: key.NewBinding( - key.WithKeys("ctrl+u"), - key.WithHelp("ctrl+u", "½ page up"), - ), - HalfPageDown: key.NewBinding( - key.WithKeys("ctrl+d", "ctrl+d"), - key.WithHelp("ctrl+d", "½ page down"), - ), -} - -func (m *messagesCmp) Init() tea.Cmd { - return tea.Batch(m.viewport.Init(), m.spinner.Tick) -} - -func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - var cmds []tea.Cmd - switch msg := msg.(type) { - case dialog.ThemeChangedMsg: - m.rerender() - return m, nil - case ToggleToolMessagesMsg: - m.showToolMessages = !m.showToolMessages - // Clear the cache to force re-rendering of all messages - m.cachedContent = make(map[string]cacheItem) - m.renderView() - return m, nil - case state.SessionSelectedMsg: - cmd := m.Reload(msg) - return m, cmd - case state.SessionClearedMsg: - m.messages = make([]message.Message, 0) - m.currentMsgID = "" - m.rendering = false - return m, nil - case tea.KeyMsg: - if key.Matches(msg, messageKeys.PageUp) || key.Matches(msg, messageKeys.PageDown) || - key.Matches(msg, messageKeys.HalfPageUp) || key.Matches(msg, messageKeys.HalfPageDown) { - u, cmd := m.viewport.Update(msg) - m.viewport = u - cmds = append(cmds, cmd) - } - case renderFinishedMsg: - m.rendering = false - m.viewport.GotoBottom() - case pubsub.Event[message.Message]: - needsRerender := false - if msg.Type == message.EventMessageCreated { - if msg.Payload.SessionID == m.app.CurrentSession.ID { - messageExists := false - for _, v := range m.messages { - if v.ID == msg.Payload.ID { - messageExists = true - break - } - } - - if !messageExists { - if len(m.messages) > 0 { - lastMsgID := m.messages[len(m.messages)-1].ID - delete(m.cachedContent, lastMsgID) - } - - m.messages = append(m.messages, msg.Payload) - delete(m.cachedContent, m.currentMsgID) - m.currentMsgID = msg.Payload.ID - needsRerender = true - } - } - // There are tool calls from the child task - for _, v := range m.messages { - for _, c := range v.ToolCalls() { - if c.ID == msg.Payload.SessionID { - delete(m.cachedContent, v.ID) - needsRerender = true - } - } - } - } else if msg.Type == message.EventMessageUpdated && msg.Payload.SessionID == m.app.CurrentSession.ID { - for i, v := range m.messages { - if v.ID == msg.Payload.ID { - m.messages[i] = msg.Payload - delete(m.cachedContent, msg.Payload.ID) - needsRerender = true - break - } - } - } - if needsRerender { - m.renderView() - if len(m.messages) > 0 { - if (msg.Type == message.EventMessageCreated) || - (msg.Type == message.EventMessageUpdated && msg.Payload.ID == m.messages[len(m.messages)-1].ID) { - m.viewport.GotoBottom() - } - } - } - } - - spinner, cmd := m.spinner.Update(msg) - m.spinner = spinner - cmds = append(cmds, cmd) - return m, tea.Batch(cmds...) -} - -func (m *messagesCmp) IsAgentWorking() bool { - return m.app.PrimaryAgent.IsSessionBusy(m.app.CurrentSession.ID) -} - -func formatTimeDifference(unixTime1, unixTime2 int64) string { - diffSeconds := float64(math.Abs(float64(unixTime2 - unixTime1))) - - if diffSeconds < 60 { - return fmt.Sprintf("%.1fs", diffSeconds) - } - - minutes := int(diffSeconds / 60) - seconds := int(diffSeconds) % 60 - return fmt.Sprintf("%dm%ds", minutes, seconds) -} - -func (m *messagesCmp) renderView() { - m.uiMessages = make([]uiMessage, 0) - pos := 0 - baseStyle := styles.BaseStyle() - - if m.width == 0 { - return - } - for inx, msg := range m.messages { - switch msg.Role { - case message.User: - if cache, ok := m.cachedContent[msg.ID]; ok && cache.width == m.width { - m.uiMessages = append(m.uiMessages, cache.content...) - continue - } - userMsg := renderUserMessage( - msg, - msg.ID == m.currentMsgID, - m.width, - pos, - ) - m.uiMessages = append(m.uiMessages, userMsg) - m.cachedContent[msg.ID] = cacheItem{ - width: m.width, - content: []uiMessage{userMsg}, - } - pos += userMsg.height + 1 // + 1 for spacing - case message.Assistant: - if cache, ok := m.cachedContent[msg.ID]; ok && cache.width == m.width { - m.uiMessages = append(m.uiMessages, cache.content...) - continue - } - assistantMessages := renderAssistantMessage( - msg, - inx, - m.messages, - m.app.Messages, - m.currentMsgID, - m.width, - pos, - m.showToolMessages, - ) - for _, msg := range assistantMessages { - m.uiMessages = append(m.uiMessages, msg) - pos += msg.height + 1 // + 1 for spacing - } - m.cachedContent[msg.ID] = cacheItem{ - width: m.width, - content: assistantMessages, - } - } - } - - messages := make([]string, 0) - for _, v := range m.uiMessages { - messages = append(messages, lipgloss.JoinVertical(lipgloss.Left, v.content), - baseStyle. - Width(m.width). - Render( - "", - ), - ) - } - - m.viewport.SetContent( - baseStyle. - Width(m.width). - Render( - lipgloss.JoinVertical( - lipgloss.Top, - messages..., - ), - ), - ) -} - -func (m *messagesCmp) View() string { - baseStyle := styles.BaseStyle() - - if m.rendering { - return baseStyle. - Width(m.width). - Render( - lipgloss.JoinVertical( - lipgloss.Top, - "Loading...", - m.working(), - m.help(), - ), - ) - } - if len(m.messages) == 0 { - content := baseStyle. - Width(m.width). - Height(m.height - 1). - Render( - m.initialScreen(), - ) - - return baseStyle. - Width(m.width). - Render( - lipgloss.JoinVertical( - lipgloss.Top, - content, - "", - m.help(), - ), - ) - } - - return baseStyle. - Width(m.width). - Render( - lipgloss.JoinVertical( - lipgloss.Top, - m.viewport.View(), - m.working(), - m.help(), - ), - ) -} - -func hasToolsWithoutResponse(messages []message.Message) bool { - toolCalls := make([]message.ToolCall, 0) - toolResults := make([]message.ToolResult, 0) - for _, m := range messages { - toolCalls = append(toolCalls, m.ToolCalls()...) - toolResults = append(toolResults, m.ToolResults()...) - } - - for _, v := range toolCalls { - found := false - for _, r := range toolResults { - if v.ID == r.ToolCallID { - found = true - break - } - } - if !found && v.Finished { - return true - } - } - return false -} - -func hasUnfinishedToolCalls(messages []message.Message) bool { - toolCalls := make([]message.ToolCall, 0) - for _, m := range messages { - toolCalls = append(toolCalls, m.ToolCalls()...) - } - for _, v := range toolCalls { - if !v.Finished { - return true - } - } - return false -} - -func (m *messagesCmp) working() string { - text := "" - if m.IsAgentWorking() && len(m.messages) > 0 { - t := theme.CurrentTheme() - baseStyle := styles.BaseStyle() - - task := "Thinking..." - lastMessage := m.messages[len(m.messages)-1] - if hasToolsWithoutResponse(m.messages) { - task = "Waiting for tool response..." - } else if hasUnfinishedToolCalls(m.messages) { - task = "Building tool call..." - } else if !lastMessage.IsFinished() { - task = "Generating..." - } - if task != "" { - text += baseStyle. - Width(m.width). - Foreground(t.Primary()). - Bold(true). - Render(fmt.Sprintf("%s %s ", m.spinner.View(), task)) - } - } - return text -} - -func (m *messagesCmp) help() string { - t := theme.CurrentTheme() - baseStyle := styles.BaseStyle() - - text := "" - - if m.app.PrimaryAgent.IsBusy() { - text += lipgloss.JoinHorizontal( - lipgloss.Left, - baseStyle.Foreground(t.TextMuted()).Bold(true).Render("press "), - baseStyle.Foreground(t.Text()).Bold(true).Render("esc"), - baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" to interrupt"), - ) - } else { - text += lipgloss.JoinHorizontal( - lipgloss.Left, - baseStyle.Foreground(t.Text()).Bold(true).Render("enter"), - baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" to send,"), - baseStyle.Foreground(t.Text()).Bold(true).Render(" \\"), - baseStyle.Foreground(t.TextMuted()).Bold(true).Render("+"), - baseStyle.Foreground(t.Text()).Bold(true).Render("enter"), - baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" for newline,"), - baseStyle.Foreground(t.Text()).Bold(true).Render(" ctrl+h"), - baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" to toggle tool messages"), - ) - } - return baseStyle. - Width(m.width). - Render(text) -} - -func (m *messagesCmp) initialScreen() string { - baseStyle := styles.BaseStyle() - - return baseStyle.Width(m.width).Render( - lipgloss.JoinVertical( - lipgloss.Top, - header(m.width), - "", - lspsConfigured(m.width), - ), - ) -} - -func (m *messagesCmp) rerender() { - for _, msg := range m.messages { - delete(m.cachedContent, msg.ID) - } - m.renderView() -} - -func (m *messagesCmp) SetSize(width, height int) tea.Cmd { - if m.width == width && m.height == height { - return nil - } - m.width = width - m.height = height - m.viewport.Width = width - m.viewport.Height = height - 2 - m.attachments.Width = width + 40 - m.attachments.Height = 3 - m.rerender() - return nil -} - -func (m *messagesCmp) GetSize() (int, int) { - return m.width, m.height -} - -func (m *messagesCmp) Reload(session *session.Session) tea.Cmd { - messages, err := m.app.Messages.List(context.Background(), session.ID) - if err != nil { - status.Error(err.Error()) - return nil - } - m.messages = messages - if len(m.messages) > 0 { - m.currentMsgID = m.messages[len(m.messages)-1].ID - } - delete(m.cachedContent, m.currentMsgID) - m.rendering = true - return func() tea.Msg { - m.renderView() - return renderFinishedMsg{} - } -} - -func (m *messagesCmp) BindingKeys() []key.Binding { - return []key.Binding{ - m.viewport.KeyMap.PageDown, - m.viewport.KeyMap.PageUp, - m.viewport.KeyMap.HalfPageUp, - m.viewport.KeyMap.HalfPageDown, - } -} - -func NewMessagesCmp(app *app.App) tea.Model { - customSpinner := spinner.Spinner{ - Frames: []string{" ", "┃", "┃"}, - FPS: time.Second / 3, - } - s := spinner.New(spinner.WithSpinner(customSpinner)) - vp := viewport.New(0, 0) - attachmets := viewport.New(0, 0) - vp.KeyMap.PageUp = messageKeys.PageUp - vp.KeyMap.PageDown = messageKeys.PageDown - vp.KeyMap.HalfPageUp = messageKeys.HalfPageUp - vp.KeyMap.HalfPageDown = messageKeys.HalfPageDown - return &messagesCmp{ - app: app, - cachedContent: make(map[string]cacheItem), - viewport: vp, - spinner: s, - attachments: attachmets, - showToolMessages: true, - } -} diff --git a/internal/tui/components/chat/messages.go b/internal/tui/components/chat/messages.go new file mode 100644 index 000000000..baa7c7e6d --- /dev/null +++ b/internal/tui/components/chat/messages.go @@ -0,0 +1,483 @@ +package chat + +import ( + "context" + "fmt" + "math" + "time" + + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/spinner" + "github.com/charmbracelet/bubbles/viewport" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/sst/opencode/internal/app" + "github.com/sst/opencode/internal/message" + "github.com/sst/opencode/internal/pubsub" + "github.com/sst/opencode/internal/session" + "github.com/sst/opencode/internal/status" + "github.com/sst/opencode/internal/tui/components/dialog" + "github.com/sst/opencode/internal/tui/state" + "github.com/sst/opencode/internal/tui/styles" + "github.com/sst/opencode/internal/tui/theme" +) + +type cacheItem struct { + width int + content []uiMessage +} + +type messagesCmp struct { + app *app.App + width, height int + viewport viewport.Model + messages []message.Message + uiMessages []uiMessage + currentMsgID string + cachedContent map[string]cacheItem + spinner spinner.Model + rendering bool + attachments viewport.Model + showToolMessages bool +} +type renderFinishedMsg struct{} +type ToggleToolMessagesMsg struct{} + +type MessageKeys struct { + PageDown key.Binding + PageUp key.Binding + HalfPageUp key.Binding + HalfPageDown key.Binding +} + +var messageKeys = MessageKeys{ + PageDown: key.NewBinding( + key.WithKeys("pgdown"), + key.WithHelp("f/pgdn", "page down"), + ), + PageUp: key.NewBinding( + key.WithKeys("pgup"), + key.WithHelp("b/pgup", "page up"), + ), + HalfPageUp: key.NewBinding( + key.WithKeys("ctrl+u"), + key.WithHelp("ctrl+u", "½ page up"), + ), + HalfPageDown: key.NewBinding( + key.WithKeys("ctrl+d", "ctrl+d"), + key.WithHelp("ctrl+d", "½ page down"), + ), +} + +func (m *messagesCmp) Init() tea.Cmd { + return tea.Batch(m.viewport.Init(), m.spinner.Tick) +} + +func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmds []tea.Cmd + switch msg := msg.(type) { + case dialog.ThemeChangedMsg: + m.rerender() + return m, nil + case ToggleToolMessagesMsg: + m.showToolMessages = !m.showToolMessages + // Clear the cache to force re-rendering of all messages + m.cachedContent = make(map[string]cacheItem) + m.renderView() + return m, nil + case state.SessionSelectedMsg: + cmd := m.Reload(msg) + return m, cmd + case state.SessionClearedMsg: + m.messages = make([]message.Message, 0) + m.currentMsgID = "" + m.rendering = false + return m, nil + case tea.KeyMsg: + if key.Matches(msg, messageKeys.PageUp) || key.Matches(msg, messageKeys.PageDown) || + key.Matches(msg, messageKeys.HalfPageUp) || key.Matches(msg, messageKeys.HalfPageDown) { + u, cmd := m.viewport.Update(msg) + m.viewport = u + cmds = append(cmds, cmd) + } + case renderFinishedMsg: + m.rendering = false + m.viewport.GotoBottom() + case pubsub.Event[message.Message]: + needsRerender := false + if msg.Type == message.EventMessageCreated { + if msg.Payload.SessionID == m.app.CurrentSession.ID { + messageExists := false + for _, v := range m.messages { + if v.ID == msg.Payload.ID { + messageExists = true + break + } + } + + if !messageExists { + if len(m.messages) > 0 { + lastMsgID := m.messages[len(m.messages)-1].ID + delete(m.cachedContent, lastMsgID) + } + + m.messages = append(m.messages, msg.Payload) + delete(m.cachedContent, m.currentMsgID) + m.currentMsgID = msg.Payload.ID + needsRerender = true + } + } + // There are tool calls from the child task + for _, v := range m.messages { + for _, c := range v.ToolCalls() { + if c.ID == msg.Payload.SessionID { + delete(m.cachedContent, v.ID) + needsRerender = true + } + } + } + } else if msg.Type == message.EventMessageUpdated && msg.Payload.SessionID == m.app.CurrentSession.ID { + for i, v := range m.messages { + if v.ID == msg.Payload.ID { + m.messages[i] = msg.Payload + delete(m.cachedContent, msg.Payload.ID) + needsRerender = true + break + } + } + } + if needsRerender { + m.renderView() + if len(m.messages) > 0 { + if (msg.Type == message.EventMessageCreated) || + (msg.Type == message.EventMessageUpdated && msg.Payload.ID == m.messages[len(m.messages)-1].ID) { + m.viewport.GotoBottom() + } + } + } + } + + spinner, cmd := m.spinner.Update(msg) + m.spinner = spinner + cmds = append(cmds, cmd) + return m, tea.Batch(cmds...) +} + +func (m *messagesCmp) IsAgentWorking() bool { + return m.app.PrimaryAgent.IsSessionBusy(m.app.CurrentSession.ID) +} + +func formatTimeDifference(unixTime1, unixTime2 int64) string { + diffSeconds := float64(math.Abs(float64(unixTime2 - unixTime1))) + + if diffSeconds < 60 { + return fmt.Sprintf("%.1fs", diffSeconds) + } + + minutes := int(diffSeconds / 60) + seconds := int(diffSeconds) % 60 + return fmt.Sprintf("%dm%ds", minutes, seconds) +} + +func (m *messagesCmp) renderView() { + m.uiMessages = make([]uiMessage, 0) + pos := 0 + baseStyle := styles.BaseStyle() + + if m.width == 0 { + return + } + for inx, msg := range m.messages { + switch msg.Role { + case message.User: + if cache, ok := m.cachedContent[msg.ID]; ok && cache.width == m.width { + m.uiMessages = append(m.uiMessages, cache.content...) + continue + } + userMsg := renderUserMessage( + msg, + msg.ID == m.currentMsgID, + m.width, + pos, + ) + m.uiMessages = append(m.uiMessages, userMsg) + m.cachedContent[msg.ID] = cacheItem{ + width: m.width, + content: []uiMessage{userMsg}, + } + pos += userMsg.height + 1 // + 1 for spacing + case message.Assistant: + if cache, ok := m.cachedContent[msg.ID]; ok && cache.width == m.width { + m.uiMessages = append(m.uiMessages, cache.content...) + continue + } + assistantMessages := renderAssistantMessage( + msg, + inx, + m.messages, + m.app.Messages, + m.currentMsgID, + m.width, + pos, + m.showToolMessages, + ) + for _, msg := range assistantMessages { + m.uiMessages = append(m.uiMessages, msg) + pos += msg.height + 1 // + 1 for spacing + } + m.cachedContent[msg.ID] = cacheItem{ + width: m.width, + content: assistantMessages, + } + } + } + + messages := make([]string, 0) + for _, v := range m.uiMessages { + messages = append(messages, lipgloss.JoinVertical(lipgloss.Left, v.content), + baseStyle. + Width(m.width). + Render( + "", + ), + ) + } + + m.viewport.SetContent( + baseStyle. + Width(m.width). + Render( + lipgloss.JoinVertical( + lipgloss.Top, + messages..., + ), + ), + ) +} + +func (m *messagesCmp) View() string { + baseStyle := styles.BaseStyle() + + if m.rendering { + return baseStyle. + Width(m.width). + Render( + lipgloss.JoinVertical( + lipgloss.Top, + "Loading...", + m.working(), + m.help(), + ), + ) + } + if len(m.messages) == 0 { + content := baseStyle. + Width(m.width). + Height(m.height - 1). + Render( + m.initialScreen(), + ) + + return baseStyle. + Width(m.width). + Render( + lipgloss.JoinVertical( + lipgloss.Top, + content, + "", + m.help(), + ), + ) + } + + return baseStyle. + Width(m.width). + Render( + lipgloss.JoinVertical( + lipgloss.Top, + m.viewport.View(), + m.working(), + m.help(), + ), + ) +} + +func hasToolsWithoutResponse(messages []message.Message) bool { + toolCalls := make([]message.ToolCall, 0) + toolResults := make([]message.ToolResult, 0) + for _, m := range messages { + toolCalls = append(toolCalls, m.ToolCalls()...) + toolResults = append(toolResults, m.ToolResults()...) + } + + for _, v := range toolCalls { + found := false + for _, r := range toolResults { + if v.ID == r.ToolCallID { + found = true + break + } + } + if !found && v.Finished { + return true + } + } + return false +} + +func hasUnfinishedToolCalls(messages []message.Message) bool { + toolCalls := make([]message.ToolCall, 0) + for _, m := range messages { + toolCalls = append(toolCalls, m.ToolCalls()...) + } + for _, v := range toolCalls { + if !v.Finished { + return true + } + } + return false +} + +func (m *messagesCmp) working() string { + text := "" + if m.IsAgentWorking() && len(m.messages) > 0 { + t := theme.CurrentTheme() + baseStyle := styles.BaseStyle() + + task := "Thinking..." + lastMessage := m.messages[len(m.messages)-1] + if hasToolsWithoutResponse(m.messages) { + task = "Waiting for tool response..." + } else if hasUnfinishedToolCalls(m.messages) { + task = "Building tool call..." + } else if !lastMessage.IsFinished() { + task = "Generating..." + } + if task != "" { + text += baseStyle. + Width(m.width). + Foreground(t.Primary()). + Bold(true). + Render(fmt.Sprintf("%s %s ", m.spinner.View(), task)) + } + } + return text +} + +func (m *messagesCmp) help() string { + t := theme.CurrentTheme() + baseStyle := styles.BaseStyle() + + text := "" + + if m.app.PrimaryAgent.IsBusy() { + text += lipgloss.JoinHorizontal( + lipgloss.Left, + baseStyle.Foreground(t.TextMuted()).Bold(true).Render("press "), + baseStyle.Foreground(t.Text()).Bold(true).Render("esc"), + baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" to interrupt"), + ) + } else { + text += lipgloss.JoinHorizontal( + lipgloss.Left, + baseStyle.Foreground(t.Text()).Bold(true).Render("enter"), + baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" to send,"), + baseStyle.Foreground(t.Text()).Bold(true).Render(" \\"), + baseStyle.Foreground(t.TextMuted()).Bold(true).Render("+"), + baseStyle.Foreground(t.Text()).Bold(true).Render("enter"), + baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" for newline,"), + baseStyle.Foreground(t.Text()).Bold(true).Render(" ctrl+h"), + baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" to toggle tool messages"), + ) + } + return baseStyle. + Width(m.width). + Render(text) +} + +func (m *messagesCmp) initialScreen() string { + baseStyle := styles.BaseStyle() + + return baseStyle.Width(m.width).Render( + lipgloss.JoinVertical( + lipgloss.Top, + header(m.width), + "", + lspsConfigured(m.width), + ), + ) +} + +func (m *messagesCmp) rerender() { + for _, msg := range m.messages { + delete(m.cachedContent, msg.ID) + } + m.renderView() +} + +func (m *messagesCmp) SetSize(width, height int) tea.Cmd { + if m.width == width && m.height == height { + return nil + } + m.width = width + m.height = height + m.viewport.Width = width + m.viewport.Height = height - 2 + m.attachments.Width = width + 40 + m.attachments.Height = 3 + m.rerender() + return nil +} + +func (m *messagesCmp) GetSize() (int, int) { + return m.width, m.height +} + +func (m *messagesCmp) Reload(session *session.Session) tea.Cmd { + messages, err := m.app.Messages.List(context.Background(), session.ID) + if err != nil { + status.Error(err.Error()) + return nil + } + m.messages = messages + if len(m.messages) > 0 { + m.currentMsgID = m.messages[len(m.messages)-1].ID + } + delete(m.cachedContent, m.currentMsgID) + m.rendering = true + return func() tea.Msg { + m.renderView() + return renderFinishedMsg{} + } +} + +func (m *messagesCmp) BindingKeys() []key.Binding { + return []key.Binding{ + m.viewport.KeyMap.PageDown, + m.viewport.KeyMap.PageUp, + m.viewport.KeyMap.HalfPageUp, + m.viewport.KeyMap.HalfPageDown, + } +} + +func NewMessagesCmp(app *app.App) tea.Model { + customSpinner := spinner.Spinner{ + Frames: []string{" ", "┃", "┃"}, + FPS: time.Second / 3, + } + s := spinner.New(spinner.WithSpinner(customSpinner)) + vp := viewport.New(0, 0) + attachmets := viewport.New(0, 0) + vp.KeyMap.PageUp = messageKeys.PageUp + vp.KeyMap.PageDown = messageKeys.PageDown + vp.KeyMap.HalfPageUp = messageKeys.HalfPageUp + vp.KeyMap.HalfPageDown = messageKeys.HalfPageDown + return &messagesCmp{ + app: app, + cachedContent: make(map[string]cacheItem), + viewport: vp, + spinner: s, + attachments: attachmets, + showToolMessages: true, + } +} diff --git a/internal/tui/layout/container.go b/internal/tui/layout/container.go index 08b10fdd6..b5bdca20a 100644 --- a/internal/tui/layout/container.go +++ b/internal/tui/layout/container.go @@ -11,16 +11,16 @@ type Container interface { tea.Model Sizeable Bindings - Focus() // Add focus method - Blur() // Add blur method + Focus() + Blur() } + type container struct { width int height int content tea.Model - // Style options paddingTop int paddingRight int paddingBottom int @@ -32,7 +32,7 @@ type container struct { borderLeft bool borderStyle lipgloss.Border - focused bool // Track focus state + focused bool } func (c *container) Init() tea.Cmd { @@ -152,16 +152,13 @@ func (c *container) Blur() { type ContainerOption func(*container) func NewContainer(content tea.Model, options ...ContainerOption) Container { - c := &container{ content: content, borderStyle: lipgloss.NormalBorder(), } - for _, option := range options { option(c) } - return c } -- cgit v1.2.3