summaryrefslogtreecommitdiffhomepage
path: root/internal/tui/components/chat
diff options
context:
space:
mode:
authorDax Raad <[email protected]>2025-05-30 20:47:56 -0400
committerDax Raad <[email protected]>2025-05-30 20:48:36 -0400
commitf3da73553c45f17e04b1e77cb13eb0fca714d1bd (patch)
treea24317a19e1ab2a89da50db669dc6894f15d00d1 /internal/tui/components/chat
parent9a26b3058ffc1023e5c7e54b6d571c903d15888e (diff)
downloadopencode-f3da73553c45f17e04b1e77cb13eb0fca714d1bd.tar.gz
opencode-f3da73553c45f17e04b1e77cb13eb0fca714d1bd.zip
sync
Diffstat (limited to 'internal/tui/components/chat')
-rw-r--r--internal/tui/components/chat/chat.go133
-rw-r--r--internal/tui/components/chat/editor.go406
-rw-r--r--internal/tui/components/chat/message.go716
-rw-r--r--internal/tui/components/chat/messages.go344
-rw-r--r--internal/tui/components/chat/sidebar.go220
5 files changed, 0 insertions, 1819 deletions
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), &params)
- // // prompt := strings.ReplaceAll(params.Prompt, "\n", " ")
- // // return renderParams(paramWidth, prompt)
- // case "bash":
- // var params tools.BashParams
- // json.Unmarshal([]byte(toolCall.Input), &params)
- // command := strings.ReplaceAll(params.Command, "\n", " ")
- // return renderParams(paramWidth, command)
- // case "edit":
- // var params tools.EditParams
- // json.Unmarshal([]byte(toolCall.Input), &params)
- // filePath := removeWorkingDirPrefix(params.FilePath)
- // return renderParams(paramWidth, filePath)
- // case "fetch":
- // var params tools.FetchParams
- // json.Unmarshal([]byte(toolCall.Input), &params)
- // 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), &params)
- // 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), &params)
- // 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), &params)
- // path := params.Path
- // if path == "" {
- // path = "."
- // }
- // return renderParams(paramWidth, path)
- // case tools.ViewToolName:
- // var params tools.ViewParams
- // json.Unmarshal([]byte(toolCall.Input), &params)
- // 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), &params)
- // filePath := removeWorkingDirPrefix(params.FilePath)
- // return renderParams(paramWidth, filePath)
- // case tools.BatchToolName:
- // var params tools.BatchParams
- // json.Unmarshal([]byte(toolCall.Input), &params)
- // 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), &params)
- // 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), &params)
- // 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, "/")
-}