From f3da73553c45f17e04b1e77cb13eb0fca714d1bd Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Fri, 30 May 2025 20:47:56 -0400 Subject: sync --- internal/tui/components/chat/chat.go | 133 ------ internal/tui/components/chat/editor.go | 406 ------------------ internal/tui/components/chat/message.go | 716 ------------------------------- internal/tui/components/chat/messages.go | 344 --------------- internal/tui/components/chat/sidebar.go | 220 ---------- 5 files changed, 1819 deletions(-) delete mode 100644 internal/tui/components/chat/chat.go delete mode 100644 internal/tui/components/chat/editor.go delete mode 100644 internal/tui/components/chat/message.go delete mode 100644 internal/tui/components/chat/messages.go delete mode 100644 internal/tui/components/chat/sidebar.go (limited to 'internal/tui/components/chat') diff --git a/internal/tui/components/chat/chat.go b/internal/tui/components/chat/chat.go deleted file mode 100644 index 2fabea43d..000000000 --- a/internal/tui/components/chat/chat.go +++ /dev/null @@ -1,133 +0,0 @@ -package chat - -import ( - "fmt" - "sort" - - "github.com/charmbracelet/lipgloss" - "github.com/charmbracelet/x/ansi" - "github.com/sst/opencode/internal/config" - "github.com/sst/opencode/internal/tui/app" - "github.com/sst/opencode/internal/tui/styles" - "github.com/sst/opencode/internal/tui/theme" - "github.com/sst/opencode/internal/version" -) - -type SendMsg struct { - Text string - Attachments []app.Attachment -} - -func header(width int) string { - return lipgloss.JoinVertical( - lipgloss.Top, - logo(width), - repo(width), - "", - cwd(width), - ) -} - -func lspsConfigured(width int) string { - // cfg := config.Get() - title := "LSP Servers" - title = ansi.Truncate(title, width, "…") - - t := theme.CurrentTheme() - baseStyle := styles.BaseStyle() - - lsps := baseStyle. - Width(width). - Foreground(t.Primary()). - Bold(true). - Render(title) - - // Get LSP names and sort them for consistent ordering - var lspNames []string - // for name := range cfg.LSP { - // lspNames = append(lspNames, name) - // } - sort.Strings(lspNames) - - var lspViews []string - // for _, name := range lspNames { - // lsp := cfg.LSP[name] - // lspName := baseStyle. - // Foreground(t.Text()). - // Render(fmt.Sprintf("• %s", name)) - - // cmd := lsp.Command - // cmd = ansi.Truncate(cmd, width-lipgloss.Width(lspName)-3, "…") - - // lspPath := baseStyle. - // Foreground(t.TextMuted()). - // Render(fmt.Sprintf(" (%s)", cmd)) - - // lspViews = append(lspViews, - // baseStyle. - // Width(width). - // Render( - // lipgloss.JoinHorizontal( - // lipgloss.Left, - // lspName, - // lspPath, - // ), - // ), - // ) - // } - - return baseStyle. - Width(width). - Render( - lipgloss.JoinVertical( - lipgloss.Left, - lsps, - lipgloss.JoinVertical( - lipgloss.Left, - lspViews..., - ), - ), - ) -} - -func logo(width int) string { - logo := fmt.Sprintf("%s %s", styles.OpenCodeIcon, "OpenCode") - t := theme.CurrentTheme() - baseStyle := styles.BaseStyle() - - versionText := baseStyle. - Foreground(t.TextMuted()). - Render(version.Version) - - return baseStyle. - Bold(true). - Width(width). - Render( - lipgloss.JoinHorizontal( - lipgloss.Left, - logo, - " ", - versionText, - ), - ) -} - -func repo(width int) string { - repo := "github.com/sst/opencode" - t := theme.CurrentTheme() - - return styles.BaseStyle(). - Foreground(t.TextMuted()). - Width(width). - Render(repo) -} - -func cwd(width int) string { - cwd := fmt.Sprintf("cwd: %s", config.WorkingDirectory()) - t := theme.CurrentTheme() - - return styles.BaseStyle(). - Foreground(t.TextMuted()). - Width(width). - Render(cwd) -} diff --git a/internal/tui/components/chat/editor.go b/internal/tui/components/chat/editor.go deleted file mode 100644 index 6586f2020..000000000 --- a/internal/tui/components/chat/editor.go +++ /dev/null @@ -1,406 +0,0 @@ -package chat - -import ( - "fmt" - "log/slog" - "os" - "os/exec" - "slices" - "strings" - "unicode" - - "github.com/charmbracelet/bubbles/key" - "github.com/charmbracelet/bubbles/textarea" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" - "github.com/sst/opencode/internal/status" - "github.com/sst/opencode/internal/tui/app" - "github.com/sst/opencode/internal/tui/components/dialog" - "github.com/sst/opencode/internal/tui/image" - "github.com/sst/opencode/internal/tui/layout" - "github.com/sst/opencode/internal/tui/styles" - "github.com/sst/opencode/internal/tui/theme" - "github.com/sst/opencode/internal/tui/util" -) - -type editorCmp struct { - width int - height int - app *app.App - textarea textarea.Model - attachments []app.Attachment - deleteMode bool - history []string - historyIndex int - currentMessage string -} - -type EditorKeyMaps struct { - Send key.Binding - OpenEditor key.Binding - Paste key.Binding - HistoryUp key.Binding - HistoryDown key.Binding -} - -type bluredEditorKeyMaps struct { - Send key.Binding - Focus key.Binding - OpenEditor key.Binding -} -type DeleteAttachmentKeyMaps struct { - AttachmentDeleteMode key.Binding - Escape key.Binding - DeleteAllAttachments key.Binding -} - -var editorMaps = EditorKeyMaps{ - Send: key.NewBinding( - key.WithKeys("enter", "ctrl+s"), - key.WithHelp("enter", "send message"), - ), - OpenEditor: key.NewBinding( - key.WithKeys("ctrl+e"), - key.WithHelp("ctrl+e", "open editor"), - ), - Paste: key.NewBinding( - key.WithKeys("ctrl+v"), - key.WithHelp("ctrl+v", "paste content"), - ), - HistoryUp: key.NewBinding( - key.WithKeys("up"), - key.WithHelp("up", "previous message"), - ), - HistoryDown: key.NewBinding( - key.WithKeys("down"), - key.WithHelp("down", "next message"), - ), -} - -var DeleteKeyMaps = DeleteAttachmentKeyMaps{ - AttachmentDeleteMode: key.NewBinding( - key.WithKeys("ctrl+r"), - key.WithHelp("ctrl+r+{i}", "delete attachment at index i"), - ), - Escape: key.NewBinding( - key.WithKeys("esc"), - key.WithHelp("esc", "cancel delete mode"), - ), - DeleteAllAttachments: key.NewBinding( - key.WithKeys("r"), - key.WithHelp("ctrl+r+r", "delete all attachments"), - ), -} - -const ( - maxAttachments = 5 -) - -func (m *editorCmp) openEditor(value string) tea.Cmd { - editor := os.Getenv("EDITOR") - if editor == "" { - editor = "nvim" - } - - tmpfile, err := os.CreateTemp("", "msg_*.md") - tmpfile.WriteString(value) - if err != nil { - status.Error(err.Error()) - return nil - } - tmpfile.Close() - c := exec.Command(editor, tmpfile.Name()) //nolint:gosec - c.Stdin = os.Stdin - c.Stdout = os.Stdout - c.Stderr = os.Stderr - return tea.ExecProcess(c, func(err error) tea.Msg { - if err != nil { - status.Error(err.Error()) - return nil - } - content, err := os.ReadFile(tmpfile.Name()) - if err != nil { - status.Error(err.Error()) - return nil - } - if len(content) == 0 { - status.Warn("Message is empty") - return nil - } - os.Remove(tmpfile.Name()) - attachments := m.attachments - m.attachments = nil - return SendMsg{ - Text: string(content), - Attachments: attachments, - } - }) -} - -func (m *editorCmp) Init() tea.Cmd { - return textarea.Blink -} - -func (m *editorCmp) send() tea.Cmd { - value := m.textarea.Value() - m.textarea.Reset() - attachments := m.attachments - - // Save to history if not empty and not a duplicate of the last entry - if value != "" { - if len(m.history) == 0 || m.history[len(m.history)-1] != value { - m.history = append(m.history, value) - } - m.historyIndex = len(m.history) - m.currentMessage = "" - } - - m.attachments = nil - if value == "" { - return nil - } - return tea.Batch( - util.CmdHandler(SendMsg{ - Text: value, - Attachments: attachments, - }), - ) -} - -func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - var cmd tea.Cmd - switch msg := msg.(type) { - case dialog.ThemeChangedMsg: - m.textarea = CreateTextArea(&m.textarea) - case dialog.CompletionSelectedMsg: - existingValue := m.textarea.Value() - modifiedValue := strings.Replace(existingValue, msg.SearchString, msg.CompletionValue, 1) - - m.textarea.SetValue(modifiedValue) - return m, nil - case dialog.AttachmentAddedMsg: - if len(m.attachments) >= maxAttachments { - status.Error(fmt.Sprintf("cannot add more than %d images", maxAttachments)) - return m, cmd - } - m.attachments = append(m.attachments, msg.Attachment) - case tea.KeyMsg: - if key.Matches(msg, DeleteKeyMaps.AttachmentDeleteMode) { - m.deleteMode = true - return m, nil - } - if key.Matches(msg, DeleteKeyMaps.DeleteAllAttachments) && m.deleteMode { - m.deleteMode = false - m.attachments = nil - return m, nil - } - if m.deleteMode && len(msg.Runes) > 0 && unicode.IsDigit(msg.Runes[0]) { - num := int(msg.Runes[0] - '0') - m.deleteMode = false - if num < 10 && len(m.attachments) > num { - if num == 0 { - m.attachments = m.attachments[num+1:] - } else { - m.attachments = slices.Delete(m.attachments, num, num+1) - } - return m, nil - } - } - if key.Matches(msg, messageKeys.PageUp) || key.Matches(msg, messageKeys.PageDown) || - key.Matches(msg, messageKeys.HalfPageUp) || key.Matches(msg, messageKeys.HalfPageDown) { - return m, nil - } - if key.Matches(msg, editorMaps.OpenEditor) { - // if m.app.PrimaryAgentOLD.IsSessionBusy(m.app.CurrentSessionOLD.ID) { - // status.Warn("Agent is working, please wait...") - // return m, nil - // } - value := m.textarea.Value() - m.textarea.Reset() - return m, m.openEditor(value) - } - if key.Matches(msg, DeleteKeyMaps.Escape) { - m.deleteMode = false - return m, nil - } - - if key.Matches(msg, editorMaps.Paste) { - imageBytes, text, err := image.GetImageFromClipboard() - if err != nil { - slog.Error(err.Error()) - return m, cmd - } - if len(imageBytes) != 0 { - attachmentName := fmt.Sprintf("clipboard-image-%d", len(m.attachments)) - attachment := app.Attachment{FilePath: attachmentName, FileName: attachmentName, Content: imageBytes, MimeType: "image/png"} - m.attachments = append(m.attachments, attachment) - } else { - m.textarea.SetValue(m.textarea.Value() + text) - } - return m, cmd - } - - // Handle history navigation with up/down arrow keys - // Only handle history navigation if the filepicker is not open and completion dialog is not open - if m.textarea.Focused() && key.Matches(msg, editorMaps.HistoryUp) && !m.app.IsFilepickerOpen() && !m.app.IsCompletionDialogOpen() { - // Get the current line number - currentLine := m.textarea.Line() - - // Only navigate history if we're at the first line - if currentLine == 0 && len(m.history) > 0 { - // Save current message if we're just starting to navigate - if m.historyIndex == len(m.history) { - m.currentMessage = m.textarea.Value() - } - - // Go to previous message in history - if m.historyIndex > 0 { - m.historyIndex-- - m.textarea.SetValue(m.history[m.historyIndex]) - } - return m, nil - } - } - - if m.textarea.Focused() && key.Matches(msg, editorMaps.HistoryDown) && !m.app.IsFilepickerOpen() && !m.app.IsCompletionDialogOpen() { - // Get the current line number and total lines - currentLine := m.textarea.Line() - value := m.textarea.Value() - lines := strings.Split(value, "\n") - totalLines := len(lines) - - // Only navigate history if we're at the last line - if currentLine == totalLines-1 { - if m.historyIndex < len(m.history)-1 { - // Go to next message in history - m.historyIndex++ - m.textarea.SetValue(m.history[m.historyIndex]) - } else if m.historyIndex == len(m.history)-1 { - // Return to the current message being composed - m.historyIndex = len(m.history) - m.textarea.SetValue(m.currentMessage) - } - return m, nil - } - } - - // Handle Enter key - if m.textarea.Focused() && key.Matches(msg, editorMaps.Send) { - value := m.textarea.Value() - if len(value) > 0 && value[len(value)-1] == '\\' { - // If the last character is a backslash, remove it and add a newline - m.textarea.SetValue(value[:len(value)-1] + "\n") - return m, nil - } else { - // Otherwise, send the message - return m, m.send() - } - } - - } - m.textarea, cmd = m.textarea.Update(msg) - return m, cmd -} - -func (m *editorCmp) View() string { - t := theme.CurrentTheme() - - // Style the prompt with theme colors - style := lipgloss.NewStyle(). - Padding(0, 0, 0, 1). - Bold(true). - Foreground(t.Primary()) - - if len(m.attachments) == 0 { - return lipgloss.JoinHorizontal(lipgloss.Top, style.Render(">"), m.textarea.View()) - } - m.textarea.SetHeight(m.height - 1) - return lipgloss.JoinVertical(lipgloss.Top, - m.attachmentsContent(), - lipgloss.JoinHorizontal(lipgloss.Top, style.Render(">"), - m.textarea.View()), - ) -} - -func (m *editorCmp) SetSize(width, height int) tea.Cmd { - m.width = width - m.height = height - m.textarea.SetWidth(width - 3) // account for the prompt and padding right - m.textarea.SetHeight(height) - return nil -} - -func (m *editorCmp) GetSize() (int, int) { - return m.textarea.Width(), m.textarea.Height() -} - -func (m *editorCmp) attachmentsContent() string { - var styledAttachments []string - t := theme.CurrentTheme() - attachmentStyles := styles.BaseStyle(). - MarginLeft(1). - Background(t.TextMuted()). - Foreground(t.Text()) - for i, attachment := range m.attachments { - var filename string - if len(attachment.FileName) > 10 { - filename = fmt.Sprintf(" %s %s...", styles.DocumentIcon, attachment.FileName[0:7]) - } else { - filename = fmt.Sprintf(" %s %s", styles.DocumentIcon, attachment.FileName) - } - if m.deleteMode { - filename = fmt.Sprintf("%d%s", i, filename) - } - styledAttachments = append(styledAttachments, attachmentStyles.Render(filename)) - } - content := lipgloss.JoinHorizontal(lipgloss.Left, styledAttachments...) - return content -} - -func (m *editorCmp) BindingKeys() []key.Binding { - bindings := []key.Binding{} - bindings = append(bindings, layout.KeyMapToSlice(editorMaps)...) - bindings = append(bindings, layout.KeyMapToSlice(DeleteKeyMaps)...) - return bindings -} - -func CreateTextArea(existing *textarea.Model) textarea.Model { - t := theme.CurrentTheme() - bgColor := t.Background() - textColor := t.Text() - textMutedColor := t.TextMuted() - - ta := textarea.New() - ta.BlurredStyle.Base = styles.BaseStyle().Background(bgColor).Foreground(textColor) - ta.BlurredStyle.CursorLine = styles.BaseStyle().Background(bgColor) - ta.BlurredStyle.Placeholder = styles.BaseStyle().Background(bgColor).Foreground(textMutedColor) - ta.BlurredStyle.Text = styles.BaseStyle().Background(bgColor).Foreground(textColor) - ta.FocusedStyle.Base = styles.BaseStyle().Background(bgColor).Foreground(textColor) - ta.FocusedStyle.CursorLine = styles.BaseStyle().Background(bgColor) - ta.FocusedStyle.Placeholder = styles.BaseStyle().Background(bgColor).Foreground(textMutedColor) - ta.FocusedStyle.Text = styles.BaseStyle().Background(bgColor).Foreground(textColor) - - ta.Prompt = " " - ta.ShowLineNumbers = false - ta.CharLimit = -1 - - if existing != nil { - ta.SetValue(existing.Value()) - ta.SetWidth(existing.Width()) - ta.SetHeight(existing.Height()) - } - - ta.Focus() - return ta -} - -func NewEditorCmp(app *app.App) tea.Model { - ta := CreateTextArea(nil) - return &editorCmp{ - app: app, - textarea: ta, - history: []string{}, - historyIndex: 0, - currentMessage: "", - } -} diff --git a/internal/tui/components/chat/message.go b/internal/tui/components/chat/message.go deleted file mode 100644 index feed7ec59..000000000 --- a/internal/tui/components/chat/message.go +++ /dev/null @@ -1,716 +0,0 @@ -package chat - -import ( - "fmt" - "path/filepath" - "strings" - "time" - - "github.com/charmbracelet/lipgloss" - "github.com/charmbracelet/x/ansi" - "github.com/sst/opencode/internal/config" - "github.com/sst/opencode/internal/diff" - "github.com/sst/opencode/internal/tui/styles" - "github.com/sst/opencode/internal/tui/theme" - "github.com/sst/opencode/pkg/client" - "golang.org/x/text/cases" - "golang.org/x/text/language" -) - -const ( - maxResultHeight = 10 -) - -func toMarkdown(content string, width int) string { - r := styles.GetMarkdownRenderer(width) - rendered, _ := r.Render(content) - return strings.TrimSuffix(rendered, "\n") -} - -func renderUserMessage(msg client.MessageInfo, width int) string { - t := theme.CurrentTheme() - style := styles.BaseStyle(). - BorderLeft(true). - Foreground(t.TextMuted()). - BorderForeground(t.Secondary()). - BorderStyle(lipgloss.ThickBorder()) - - baseStyle := styles.BaseStyle() - // var styledAttachments []string - // attachmentStyles := baseStyle. - // MarginLeft(1). - // Background(t.TextMuted()). - // Foreground(t.Text()) - // for _, attachment := range msg.BinaryContent() { - // file := filepath.Base(attachment.Path) - // var filename string - // if len(file) > 10 { - // filename = fmt.Sprintf(" %s %s...", styles.DocumentIcon, file[0:7]) - // } else { - // filename = fmt.Sprintf(" %s %s", styles.DocumentIcon, file) - // } - // styledAttachments = append(styledAttachments, attachmentStyles.Render(filename)) - // } - - // Add timestamp info - timestamp := time.UnixMilli(int64(msg.Metadata.Time.Created)).Local().Format("02 Jan 2006 03:04 PM") - username, _ := config.GetUsername() - info := baseStyle. - Foreground(t.TextMuted()). - Render(fmt.Sprintf(" %s (%s)", username, timestamp)) - - content := "" - // if len(styledAttachments) > 0 { - // attachmentContent := baseStyle.Width(width).Render(lipgloss.JoinHorizontal(lipgloss.Left, styledAttachments...)) - // content = renderMessage(msg.Content().String(), true, isFocused, width, append(info, attachmentContent)...) - // } else { - for _, p := range msg.Parts { - part, err := p.ValueByDiscriminator() - if err != nil { - continue //TODO: handle error? - } - - switch part.(type) { - case client.MessagePartText: - textPart := part.(client.MessagePartText) - text := toMarkdown(textPart.Text, width) - content = style.Render(lipgloss.JoinVertical(lipgloss.Left, text, info)) - } - } - - return styles.ForceReplaceBackgroundWithLipgloss(content, t.Background()) -} - -func convertToMap(input *any) (map[string]any, bool) { - if input == nil { - return nil, false // Handle nil pointer - } - value := *input // Dereference the pointer to get the interface value - m, ok := value.(map[string]any) // Type assertion - return m, ok -} - -func renderAssistantMessage( - msg client.MessageInfo, - width int, - showToolMessages bool, -) string { - t := theme.CurrentTheme() - style := styles.BaseStyle(). - BorderLeft(true). - Foreground(t.TextMuted()). - BorderForeground(t.Primary()). - BorderStyle(lipgloss.ThickBorder()) - toolStyle := styles.BaseStyle(). - BorderLeft(true). - Foreground(t.TextMuted()). - BorderForeground(t.TextMuted()). - BorderStyle(lipgloss.ThickBorder()) - - baseStyle := styles.BaseStyle() - messages := []string{} - - // content := strings.TrimSpace(msg.Content().String()) - // thinking := msg.IsThinking() - // thinkingContent := msg.ReasoningContent().Thinking - // finished := msg.IsFinished() - // finishData := msg.FinishPart() - - // Add timestamp info - timestamp := time.UnixMilli(int64(msg.Metadata.Time.Created)).Local().Format("02 Jan 2006 03:04 PM") - modelName := msg.Metadata.Assistant.ModelID - info := baseStyle. - Foreground(t.TextMuted()). - Render(fmt.Sprintf(" %s (%s)", modelName, timestamp)) - - for _, p := range msg.Parts { - part, err := p.ValueByDiscriminator() - if err != nil { - continue //TODO: handle error? - } - - switch part.(type) { - case client.MessagePartText: - textPart := part.(client.MessagePartText) - text := toMarkdown(textPart.Text, width) - content := style.Render(lipgloss.JoinVertical(lipgloss.Left, text, info)) - message := styles.ForceReplaceBackgroundWithLipgloss(content, t.Background()) - messages = append(messages, message) - - case client.MessagePartToolInvocation: - if !showToolMessages { - continue - } - - toolInvocationPart := part.(client.MessagePartToolInvocation) - toolInvocation, _ := toolInvocationPart.ToolInvocation.ValueByDiscriminator() - switch toolInvocation.(type) { - case client.MessageToolInvocationToolCall: - toolCall := toolInvocation.(client.MessageToolInvocationToolCall) - toolName := renderToolName(toolCall.ToolName) - - var toolArgs []string - toolMap, _ := convertToMap(toolCall.Args) - for _, arg := range toolMap { - toolArgs = append(toolArgs, fmt.Sprintf("%v", arg)) - } - params := renderParams(width-lipgloss.Width(toolName)-1, toolArgs...) - title := styles.Padded().Render(fmt.Sprintf("%s: %s", toolName, params)) - - content := toolStyle.Render(lipgloss.JoinVertical(lipgloss.Left, - title, - " In progress...", - )) - message := styles.ForceReplaceBackgroundWithLipgloss(content, t.Background()) - messages = append(messages, message) - - case client.MessageToolInvocationToolResult: - toolInvocationResult := toolInvocation.(client.MessageToolInvocationToolResult) - toolName := renderToolName(toolInvocationResult.ToolName) - var toolArgs []string - toolMap, _ := convertToMap(toolInvocationResult.Args) - for _, arg := range toolMap { - toolArgs = append(toolArgs, fmt.Sprintf("%v", arg)) - } - params := renderParams(width-lipgloss.Width(toolName)-1, toolArgs...) - title := styles.Padded().Render(fmt.Sprintf("%s: %s", toolName, params)) - metadata := msg.Metadata.Tool[toolInvocationResult.ToolCallId].(map[string]any) - - var markdown string - if toolInvocationResult.ToolName == "edit" { - filename := toolMap["filePath"].(string) - title = styles.Padded().Render(fmt.Sprintf("%s: %s", toolName, filename)) - oldString := toolMap["oldString"].(string) - newString := toolMap["newString"].(string) - patch, _, _ := diff.GenerateDiff(oldString, newString, filename) - formattedDiff, _ := diff.FormatDiff(patch, diff.WithTotalWidth(width)) - markdown = strings.TrimSpace(formattedDiff) - message := toolStyle.Render(lipgloss.JoinVertical(lipgloss.Left, - title, - markdown, - )) - messages = append(messages, message) - } else if toolInvocationResult.ToolName == "view" { - result := toolInvocationResult.Result - if metadata["preview"] != nil { - result = metadata["preview"].(string) - } - filename := toolMap["filePath"].(string) - ext := filepath.Ext(filename) - if ext == "" { - ext = "" - } else { - ext = strings.ToLower(ext[1:]) - } - result = fmt.Sprintf("```%s\n%s\n```", ext, truncateHeight(result, 10)) - markdown = toMarkdown(result, width) - content := toolStyle.Render(lipgloss.JoinVertical(lipgloss.Left, - title, - markdown, - )) - message := styles.ForceReplaceBackgroundWithLipgloss(content, t.Background()) - messages = append(messages, message) - } else { - result := truncateHeight(strings.TrimSpace(toolInvocationResult.Result), 10) - markdown = toMarkdown(result, width) - content := toolStyle.Render(lipgloss.JoinVertical(lipgloss.Left, - title, - markdown, - )) - message := styles.ForceReplaceBackgroundWithLipgloss(content, t.Background()) - messages = append(messages, message) - } - } - } - } - - // if finished { - // // Add finish info if available - // switch finishData.Reason { - // case message.FinishReasonCanceled: - // info = append(info, baseStyle. - // Width(width-1). - // Foreground(t.Warning()). - // Render("(canceled)"), - // ) - // case message.FinishReasonError: - // info = append(info, baseStyle. - // Width(width-1). - // Foreground(t.Error()). - // Render("(error)"), - // ) - // case message.FinishReasonPermissionDenied: - // info = append(info, baseStyle. - // Width(width-1). - // Foreground(t.Info()). - // Render("(permission denied)"), - // ) - // } - // } - - // if content != "" || (finished && finishData.Reason == message.FinishReasonEndTurn) { - // if content == "" { - // content = "*Finished without output*" - // } - // - // content = renderMessage(content, false, width, info...) - // messages = append(messages, content) - // // position += messages[0].height - // position++ // for the space - // } else if thinking && thinkingContent != "" { - // // Render the thinking content with timestamp - // content = renderMessage(thinkingContent, false, width, info...) - // messages = append(messages, content) - // position += lipgloss.Height(content) - // position++ // for the space - // } - - // Only render tool messages if they should be shown - if showToolMessages { - // for i, toolCall := range msg.ToolCalls() { - // toolCallContent := renderToolMessage( - // toolCall, - // allMessages, - // messagesService, - // focusedUIMessageId, - // false, - // width, - // i+1, - // ) - // messages = append(messages, toolCallContent) - // } - } - - return strings.Join(messages, "\n\n") -} - -func renderToolName(name string) string { - switch name { - // case agent.AgentToolName: - // return "Task" - case "ls": - return "List" - default: - return cases.Title(language.Und).String(name) - } -} - -func renderToolAction(name string) string { - switch name { - // case agent.AgentToolName: - // return "Preparing prompt..." - case "bash": - return "Building command..." - case "edit": - return "Preparing edit..." - case "fetch": - return "Writing fetch..." - case "glob": - return "Finding files..." - case "grep": - return "Searching content..." - case "ls": - return "Listing directory..." - case "view": - return "Reading file..." - case "write": - return "Preparing write..." - case "patch": - return "Preparing patch..." - case "batch": - return "Running batch operations..." - } - return "Working..." -} - -// renders params, params[0] (params[1]=params[2] ....) -func renderParams(paramsWidth int, params ...string) string { - if len(params) == 0 { - return "" - } - mainParam := params[0] - if len(mainParam) > paramsWidth { - mainParam = mainParam[:paramsWidth-3] + "..." - } - - if len(params) == 1 { - return mainParam - } - otherParams := params[1:] - // create pairs of key/value - // if odd number of params, the last one is a key without value - if len(otherParams)%2 != 0 { - otherParams = append(otherParams, "") - } - parts := make([]string, 0, len(otherParams)/2) - for i := 0; i < len(otherParams); i += 2 { - key := otherParams[i] - value := otherParams[i+1] - if value == "" { - continue - } - parts = append(parts, fmt.Sprintf("%s=%s", key, value)) - } - - partsRendered := strings.Join(parts, ", ") - remainingWidth := paramsWidth - lipgloss.Width(partsRendered) - 5 // for the space - if remainingWidth < 30 { - // No space for the params, just show the main - return mainParam - } - - if len(parts) > 0 { - mainParam = fmt.Sprintf("%s (%s)", mainParam, strings.Join(parts, ", ")) - } - - return ansi.Truncate(mainParam, paramsWidth, "...") -} - -func removeWorkingDirPrefix(path string) string { - wd := config.WorkingDirectory() - if strings.HasPrefix(path, wd) { - path = strings.TrimPrefix(path, wd) - } - if strings.HasPrefix(path, "/") { - path = strings.TrimPrefix(path, "/") - } - if strings.HasPrefix(path, "./") { - path = strings.TrimPrefix(path, "./") - } - if strings.HasPrefix(path, "../") { - path = strings.TrimPrefix(path, "../") - } - return path -} - -func renderToolParams(paramWidth int, toolCall any) string { - params := "" - switch toolCall { - // // case agent.AgentToolName: - // // var params agent.AgentParams - // // json.Unmarshal([]byte(toolCall.Input), ¶ms) - // // prompt := strings.ReplaceAll(params.Prompt, "\n", " ") - // // return renderParams(paramWidth, prompt) - // case "bash": - // var params tools.BashParams - // json.Unmarshal([]byte(toolCall.Input), ¶ms) - // command := strings.ReplaceAll(params.Command, "\n", " ") - // return renderParams(paramWidth, command) - // case "edit": - // var params tools.EditParams - // json.Unmarshal([]byte(toolCall.Input), ¶ms) - // filePath := removeWorkingDirPrefix(params.FilePath) - // return renderParams(paramWidth, filePath) - // case "fetch": - // var params tools.FetchParams - // json.Unmarshal([]byte(toolCall.Input), ¶ms) - // url := params.URL - // toolParams := []string{ - // url, - // } - // if params.Format != "" { - // toolParams = append(toolParams, "format", params.Format) - // } - // if params.Timeout != 0 { - // toolParams = append(toolParams, "timeout", (time.Duration(params.Timeout) * time.Second).String()) - // } - // return renderParams(paramWidth, toolParams...) - // case tools.GlobToolName: - // var params tools.GlobParams - // json.Unmarshal([]byte(toolCall.Input), ¶ms) - // pattern := params.Pattern - // toolParams := []string{ - // pattern, - // } - // if params.Path != "" { - // toolParams = append(toolParams, "path", params.Path) - // } - // return renderParams(paramWidth, toolParams...) - // case tools.GrepToolName: - // var params tools.GrepParams - // json.Unmarshal([]byte(toolCall.Input), ¶ms) - // pattern := params.Pattern - // toolParams := []string{ - // pattern, - // } - // if params.Path != "" { - // toolParams = append(toolParams, "path", params.Path) - // } - // if params.Include != "" { - // toolParams = append(toolParams, "include", params.Include) - // } - // if params.LiteralText { - // toolParams = append(toolParams, "literal", "true") - // } - // return renderParams(paramWidth, toolParams...) - // case tools.LSToolName: - // var params tools.LSParams - // json.Unmarshal([]byte(toolCall.Input), ¶ms) - // path := params.Path - // if path == "" { - // path = "." - // } - // return renderParams(paramWidth, path) - // case tools.ViewToolName: - // var params tools.ViewParams - // json.Unmarshal([]byte(toolCall.Input), ¶ms) - // filePath := removeWorkingDirPrefix(params.FilePath) - // toolParams := []string{ - // filePath, - // } - // if params.Limit != 0 { - // toolParams = append(toolParams, "limit", fmt.Sprintf("%d", params.Limit)) - // } - // if params.Offset != 0 { - // toolParams = append(toolParams, "offset", fmt.Sprintf("%d", params.Offset)) - // } - // return renderParams(paramWidth, toolParams...) - // case tools.WriteToolName: - // var params tools.WriteParams - // json.Unmarshal([]byte(toolCall.Input), ¶ms) - // filePath := removeWorkingDirPrefix(params.FilePath) - // return renderParams(paramWidth, filePath) - // case tools.BatchToolName: - // var params tools.BatchParams - // json.Unmarshal([]byte(toolCall.Input), ¶ms) - // return renderParams(paramWidth, fmt.Sprintf("%d parallel calls", len(params.Calls))) - // default: - // input := strings.ReplaceAll(toolCall, "\n", " ") - // params = renderParams(paramWidth, input) - } - return params -} - -func truncateHeight(content string, height int) string { - lines := strings.Split(content, "\n") - if len(lines) > height { - return strings.Join(lines[:height], "\n") - } - return content -} - -func renderToolResponse(toolCall any, response any, width int) string { - return "" - // t := theme.CurrentTheme() - // baseStyle := styles.BaseStyle() - // - // if response.IsError { - // errContent := fmt.Sprintf("Error: %s", strings.ReplaceAll(response.Content, "\n", " ")) - // errContent = ansi.Truncate(errContent, width-1, "...") - // return baseStyle. - // Width(width). - // Foreground(t.Error()). - // Render(errContent) - // } - // - // resultContent := truncateHeight(response.Content, maxResultHeight) - // switch toolCall.Name { - // case agent.AgentToolName: - // return styles.ForceReplaceBackgroundWithLipgloss( - // toMarkdown(resultContent, false, width), - // t.Background(), - // ) - // case tools.BashToolName: - // resultContent = fmt.Sprintf("```bash\n%s\n```", resultContent) - // return styles.ForceReplaceBackgroundWithLipgloss( - // toMarkdown(resultContent, width), - // t.Background(), - // ) - // case tools.EditToolName: - // metadata := tools.EditResponseMetadata{} - // json.Unmarshal([]byte(response.Metadata), &metadata) - // formattedDiff, _ := diff.FormatDiff(metadata.Diff, diff.WithTotalWidth(width)) - // return formattedDiff - // case tools.FetchToolName: - // var params tools.FetchParams - // json.Unmarshal([]byte(toolCall.Input), ¶ms) - // mdFormat := "markdown" - // switch params.Format { - // case "text": - // mdFormat = "text" - // case "html": - // mdFormat = "html" - // } - // resultContent = fmt.Sprintf("```%s\n%s\n```", mdFormat, resultContent) - // return styles.ForceReplaceBackgroundWithLipgloss( - // toMarkdown(resultContent, width), - // t.Background(), - // ) - // case tools.GlobToolName: - // return baseStyle.Width(width).Foreground(t.TextMuted()).Render(resultContent) - // case tools.GrepToolName: - // return baseStyle.Width(width).Foreground(t.TextMuted()).Render(resultContent) - // case tools.LSToolName: - // return baseStyle.Width(width).Foreground(t.TextMuted()).Render(resultContent) - // case tools.ViewToolName: - // metadata := tools.ViewResponseMetadata{} - // json.Unmarshal([]byte(response.Metadata), &metadata) - // ext := filepath.Ext(metadata.FilePath) - // if ext == "" { - // ext = "" - // } else { - // ext = strings.ToLower(ext[1:]) - // } - // resultContent = fmt.Sprintf("```%s\n%s\n```", ext, truncateHeight(metadata.Content, maxResultHeight)) - // return styles.ForceReplaceBackgroundWithLipgloss( - // toMarkdown(resultContent, width), - // t.Background(), - // ) - // case tools.WriteToolName: - // params := tools.WriteParams{} - // json.Unmarshal([]byte(toolCall.Input), ¶ms) - // metadata := tools.WriteResponseMetadata{} - // json.Unmarshal([]byte(response.Metadata), &metadata) - // ext := filepath.Ext(params.FilePath) - // if ext == "" { - // ext = "" - // } else { - // ext = strings.ToLower(ext[1:]) - // } - // resultContent = fmt.Sprintf("```%s\n%s\n```", ext, truncateHeight(params.Content, maxResultHeight)) - // return styles.ForceReplaceBackgroundWithLipgloss( - // toMarkdown(resultContent, width), - // t.Background(), - // ) - // case tools.BatchToolName: - // var batchResult tools.BatchResult - // if err := json.Unmarshal([]byte(resultContent), &batchResult); err != nil { - // return baseStyle.Width(width).Foreground(t.Error()).Render(fmt.Sprintf("Error parsing batch result: %s", err)) - // } - // - // var toolCalls []string - // for i, result := range batchResult.Results { - // toolName := renderToolName(result.ToolName) - // - // // Format the tool input as a string - // inputStr := string(result.ToolInput) - // - // // Format the result - // var resultStr string - // if result.Error != "" { - // resultStr = fmt.Sprintf("Error: %s", result.Error) - // } else { - // var toolResponse tools.ToolResponse - // if err := json.Unmarshal(result.Result, &toolResponse); err != nil { - // resultStr = "Error parsing tool response" - // } else { - // resultStr = truncateHeight(toolResponse.Content, 3) - // } - // } - // - // // Format the tool call - // toolCall := fmt.Sprintf("%d. %s: %s\n %s", i+1, toolName, inputStr, resultStr) - // toolCalls = append(toolCalls, toolCall) - // } - // - // return baseStyle.Width(width).Foreground(t.TextMuted()).Render(strings.Join(toolCalls, "\n\n")) - // default: - // resultContent = fmt.Sprintf("```text\n%s\n```", resultContent) - // return styles.ForceReplaceBackgroundWithLipgloss( - // toMarkdown(resultContent, width), - // t.Background(), - // ) - // } -} - -// func renderToolMessage( -// toolCall message.ToolCall, -// allMessages []message.Message, -// messagesService message.Service, -// focusedUIMessageId string, -// nested bool, -// width int, -// position int, -// ) string { -// if nested { -// width = width - 3 -// } -// -// t := theme.CurrentTheme() -// baseStyle := styles.BaseStyle() -// -// style := baseStyle. -// Width(width - 1). -// BorderLeft(true). -// BorderStyle(lipgloss.ThickBorder()). -// PaddingLeft(1). -// BorderForeground(t.TextMuted()) -// -// response := findToolResponse(toolCall.ID, allMessages) -// toolNameText := baseStyle.Foreground(t.TextMuted()). -// Render(fmt.Sprintf("%s: ", renderToolName(toolCall.Name))) -// -// if !toolCall.Finished { -// // Get a brief description of what the tool is doing -// toolAction := renderToolAction(toolCall.Name) -// -// progressText := baseStyle. -// Width(width - 2 - lipgloss.Width(toolNameText)). -// Foreground(t.TextMuted()). -// Render(fmt.Sprintf("%s", toolAction)) -// -// content := style.Render(lipgloss.JoinHorizontal(lipgloss.Left, toolNameText, progressText)) -// return content -// } -// -// params := renderToolParams(width-1-lipgloss.Width(toolNameText), toolCall) -// responseContent := "" -// if response != nil { -// responseContent = renderToolResponse(toolCall, *response, width-2) -// responseContent = strings.TrimSuffix(responseContent, "\n") -// } else { -// responseContent = baseStyle. -// Italic(true). -// Width(width - 2). -// Foreground(t.TextMuted()). -// Render("Waiting for response...") -// } -// -// parts := []string{} -// if !nested { -// formattedParams := baseStyle. -// Width(width - 2 - lipgloss.Width(toolNameText)). -// Foreground(t.TextMuted()). -// Render(params) -// -// parts = append(parts, lipgloss.JoinHorizontal(lipgloss.Left, toolNameText, formattedParams)) -// } else { -// prefix := baseStyle. -// Foreground(t.TextMuted()). -// Render(" └ ") -// formattedParams := baseStyle. -// Width(width - 2 - lipgloss.Width(toolNameText)). -// Foreground(t.TextMuted()). -// Render(params) -// parts = append(parts, lipgloss.JoinHorizontal(lipgloss.Left, prefix, toolNameText, formattedParams)) -// } -// -// // if toolCall.Name == agent.AgentToolName { -// // taskMessages, _ := messagesService.List(context.Background(), toolCall.ID) -// // toolCalls := []message.ToolCall{} -// // for _, v := range taskMessages { -// // toolCalls = append(toolCalls, v.ToolCalls()...) -// // } -// // for _, call := range toolCalls { -// // rendered := renderToolMessage(call, []message.Message{}, messagesService, focusedUIMessageId, true, width, 0) -// // parts = append(parts, rendered.content) -// // } -// // } -// if responseContent != "" && !nested { -// parts = append(parts, responseContent) -// } -// -// content := style.Render( -// lipgloss.JoinVertical( -// lipgloss.Left, -// parts..., -// ), -// ) -// if nested { -// content = lipgloss.JoinVertical( -// lipgloss.Left, -// parts..., -// ) -// } -// return content -// } diff --git a/internal/tui/components/chat/messages.go b/internal/tui/components/chat/messages.go deleted file mode 100644 index ae6f2a687..000000000 --- a/internal/tui/components/chat/messages.go +++ /dev/null @@ -1,344 +0,0 @@ -package chat - -import ( - "fmt" - "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/tui/app" - "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" - "github.com/sst/opencode/pkg/client" -) - -type messagesCmp struct { - app *app.App - width, height int - viewport viewport.Model - 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.renderView() - return m, nil - case ToggleToolMessagesMsg: - m.showToolMessages = !m.showToolMessages - m.renderView() - return m, nil - case state.SessionSelectedMsg: - cmd := m.Reload() - return m, cmd - case state.SessionClearedMsg: - cmd := m.Reload() - return m, cmd - 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 state.StateUpdatedMsg: - m.renderView() - m.viewport.GotoBottom() - } - - spinner, cmd := m.spinner.Update(msg) - m.spinner = spinner - cmds = append(cmds, cmd) - return m, tea.Batch(cmds...) -} - -func (m *messagesCmp) renderView() { - if m.width == 0 { - return - } - - messages := make([]string, 0) - for _, msg := range m.app.Messages { - switch msg.Role { - case client.User: - content := renderUserMessage(msg, m.width) - messages = append(messages, content+"\n") - case client.Assistant: - content := renderAssistantMessage(msg, m.width, m.showToolMessages) - messages = append(messages, content+"\n") - } - } - - m.viewport.SetContent( - styles.BaseStyle(). - 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.app.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 len(m.app.Messages) > 0 { - t := theme.CurrentTheme() - baseStyle := styles.BaseStyle() - - task := "" - - lastMessage := m.app.Messages[len(m.app.Messages)-1] - if lastMessage.Metadata.Time.Completed == nil { - task = "Working..." - } - // lastMessage := m.app.Messages[len(m.app.Messages)-1] - // if hasToolsWithoutResponse(m.app.Messages) { - // task = "Waiting for tool response..." - // } else if hasUnfinishedToolCalls(m.app.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.PrimaryAgentOLD.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(" ↑↓"), - baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" for history,"), - 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) 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.renderView() - return nil -} - -func (m *messagesCmp) GetSize() (int, int) { - return m.width, m.height -} - -func (m *messagesCmp) Reload() tea.Cmd { - 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) - attachments := 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, - viewport: vp, - spinner: s, - attachments: attachments, - showToolMessages: true, - } -} diff --git a/internal/tui/components/chat/sidebar.go b/internal/tui/components/chat/sidebar.go deleted file mode 100644 index d6895a164..000000000 --- a/internal/tui/components/chat/sidebar.go +++ /dev/null @@ -1,220 +0,0 @@ -package chat - -import ( - "fmt" - "sort" - "strings" - - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" - "github.com/sst/opencode/internal/config" - "github.com/sst/opencode/internal/tui/app" - "github.com/sst/opencode/internal/tui/state" - "github.com/sst/opencode/internal/tui/styles" - "github.com/sst/opencode/internal/tui/theme" -) - -type sidebarCmp struct { - app *app.App - width, height int - modFiles map[string]struct { - additions int - removals int - } -} - -func (m *sidebarCmp) Init() tea.Cmd { - // TODO: History service not implemented in API yet - // Initialize the modified files map - m.modFiles = make(map[string]struct { - additions int - removals int - }) - return nil -} - -func (m *sidebarCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - switch msg.(type) { - case state.SessionSelectedMsg: - // TODO: History service not implemented in API yet - // ctx := context.Background() - // m.loadModifiedFiles(ctx) - // case pubsub.Event[history.File]: - // TODO: History service not implemented in API yet - // if msg.Payload.SessionID == m.app.CurrentSession.ID { - // // Process the individual file change instead of reloading all files - // ctx := context.Background() - // m.processFileChanges(ctx, msg.Payload) - // } - } - return m, nil -} - -func (m *sidebarCmp) View() string { - t := theme.CurrentTheme() - baseStyle := styles.BaseStyle() - shareUrl := "" - if m.app.Session.Share != nil { - shareUrl = baseStyle.Foreground(t.TextMuted()).Render(m.app.Session.Share.Url) - } - - // qrcode := "" - // if m.app.Session.ShareID != nil { - // url := "https://dev.opencode.ai/share?id=" - // qrcode, _, _ = qr.Generate(url + m.app.Session.Id) - // } - - return baseStyle. - Width(m.width). - PaddingLeft(4). - PaddingRight(1). - Render( - lipgloss.JoinVertical( - lipgloss.Top, - header(m.width), - " ", - m.sessionSection(), - shareUrl, - ), - ) -} - -func (m *sidebarCmp) sessionSection() string { - t := theme.CurrentTheme() - baseStyle := styles.BaseStyle() - - sessionKey := baseStyle. - Foreground(t.Primary()). - Bold(true). - Render("Session") - - sessionValue := baseStyle. - Foreground(t.Text()). - Render(fmt.Sprintf(": %s", m.app.Session.Title)) - - return sessionKey + sessionValue -} - -func (m *sidebarCmp) modifiedFile(filePath string, additions, removals int) string { - t := theme.CurrentTheme() - baseStyle := styles.BaseStyle() - - stats := "" - if additions > 0 && removals > 0 { - additionsStr := baseStyle. - Foreground(t.Success()). - PaddingLeft(1). - Render(fmt.Sprintf("+%d", additions)) - - removalsStr := baseStyle. - Foreground(t.Error()). - PaddingLeft(1). - Render(fmt.Sprintf("-%d", removals)) - - content := lipgloss.JoinHorizontal(lipgloss.Left, additionsStr, removalsStr) - stats = baseStyle.Width(lipgloss.Width(content)).Render(content) - } else if additions > 0 { - additionsStr := fmt.Sprintf(" %s", baseStyle. - PaddingLeft(1). - Foreground(t.Success()). - Render(fmt.Sprintf("+%d", additions))) - stats = baseStyle.Width(lipgloss.Width(additionsStr)).Render(additionsStr) - } else if removals > 0 { - removalsStr := fmt.Sprintf(" %s", baseStyle. - PaddingLeft(1). - Foreground(t.Error()). - Render(fmt.Sprintf("-%d", removals))) - stats = baseStyle.Width(lipgloss.Width(removalsStr)).Render(removalsStr) - } - - filePathStr := baseStyle.Render(filePath) - - return baseStyle. - Width(m.width). - Render( - lipgloss.JoinHorizontal( - lipgloss.Left, - filePathStr, - stats, - ), - ) -} - -func (m *sidebarCmp) modifiedFiles() string { - t := theme.CurrentTheme() - baseStyle := styles.BaseStyle() - - modifiedFiles := baseStyle. - Width(m.width). - Foreground(t.Primary()). - Bold(true). - Render("Modified Files:") - - // If no modified files, show a placeholder message - if m.modFiles == nil || len(m.modFiles) == 0 { - message := "No modified files" - remainingWidth := m.width - lipgloss.Width(message) - if remainingWidth > 0 { - message += strings.Repeat(" ", remainingWidth) - } - return baseStyle. - Width(m.width). - Render( - lipgloss.JoinVertical( - lipgloss.Top, - modifiedFiles, - baseStyle.Foreground(t.TextMuted()).Render(message), - ), - ) - } - - // Sort file paths alphabetically for consistent ordering - var paths []string - for path := range m.modFiles { - paths = append(paths, path) - } - sort.Strings(paths) - - // Create views for each file in sorted order - var fileViews []string - for _, path := range paths { - stats := m.modFiles[path] - fileViews = append(fileViews, m.modifiedFile(path, stats.additions, stats.removals)) - } - - return baseStyle. - Width(m.width). - Render( - lipgloss.JoinVertical( - lipgloss.Top, - modifiedFiles, - lipgloss.JoinVertical( - lipgloss.Left, - fileViews..., - ), - ), - ) -} - -func (m *sidebarCmp) SetSize(width, height int) tea.Cmd { - m.width = width - m.height = height - return nil -} - -func (m *sidebarCmp) GetSize() (int, int) { - return m.width, m.height -} - -func NewSidebarCmp(app *app.App) tea.Model { - return &sidebarCmp{ - app: app, - } -} - -// Helper function to get the display path for a file -func getDisplayPath(path string) string { - workingDir := config.WorkingDirectory() - displayPath := strings.TrimPrefix(path, workingDir) - return strings.TrimPrefix(displayPath, "/") -} -- cgit v1.2.3