diff options
| author | adamdottv <[email protected]> | 2025-04-28 08:46:09 -0500 |
|---|---|---|
| committer | adamdottv <[email protected]> | 2025-04-30 07:46:34 -0500 |
| commit | 61b605e724eb4cc50ab831534fcdd18e031d68eb (patch) | |
| tree | b15b074a8fed1931e179a8d3df08d05b753c7aa3 /internal/tui/components/chat | |
| parent | 61d9dc95111d2645a49816f6d9d6cc1014be1a22 (diff) | |
| download | opencode-61b605e724eb4cc50ab831534fcdd18e031d68eb.tar.gz opencode-61b605e724eb4cc50ab831534fcdd18e031d68eb.zip | |
feat: themes
Diffstat (limited to 'internal/tui/components/chat')
| -rw-r--r-- | internal/tui/components/chat/chat.go | 72 | ||||
| -rw-r--r-- | internal/tui/components/chat/editor.go | 62 | ||||
| -rw-r--r-- | internal/tui/components/chat/list.go | 76 | ||||
| -rw-r--r-- | internal/tui/components/chat/message.go | 183 | ||||
| -rw-r--r-- | internal/tui/components/chat/sidebar.go | 72 |
5 files changed, 292 insertions, 173 deletions
diff --git a/internal/tui/components/chat/chat.go b/internal/tui/components/chat/chat.go index f4c055903..ca094ca7c 100644 --- a/internal/tui/components/chat/chat.go +++ b/internal/tui/components/chat/chat.go @@ -9,6 +9,7 @@ import ( "github.com/opencode-ai/opencode/internal/config" "github.com/opencode-ai/opencode/internal/session" "github.com/opencode-ai/opencode/internal/tui/styles" + "github.com/opencode-ai/opencode/internal/tui/theme" "github.com/opencode-ai/opencode/internal/version" ) @@ -22,12 +23,29 @@ type SessionClearedMsg struct{} type EditorFocusMsg bool +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 Configuration" title = ansi.Truncate(title, width, "…") - lsps := styles.BaseStyle.Width(width).Foreground(styles.PrimaryColor).Bold(true).Render(title) + 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 @@ -39,16 +57,19 @@ func lspsConfigured(width int) string { var lspViews []string for _, name := range lspNames { lsp := cfg.LSP[name] - lspName := styles.BaseStyle.Foreground(styles.Forground).Render( - fmt.Sprintf("• %s", name), - ) + lspName := baseStyle. + Foreground(t.Text()). + Render(fmt.Sprintf("• %s", name)) + cmd := lsp.Command cmd = ansi.Truncate(cmd, width-lipgloss.Width(lspName)-3, "…") - lspPath := styles.BaseStyle.Foreground(styles.ForgroundDim).Render( - fmt.Sprintf(" (%s)", cmd), - ) + + lspPath := baseStyle. + Foreground(t.TextMuted()). + Render(fmt.Sprintf(" (%s)", cmd)) + lspViews = append(lspViews, - styles.BaseStyle. + baseStyle. Width(width). Render( lipgloss.JoinHorizontal( @@ -59,7 +80,8 @@ func lspsConfigured(width int) string { ), ) } - return styles.BaseStyle. + + return baseStyle. Width(width). Render( lipgloss.JoinVertical( @@ -75,10 +97,14 @@ func lspsConfigured(width int) string { func logo(width int) string { logo := fmt.Sprintf("%s %s", styles.OpenCodeIcon, "OpenCode") + t := theme.CurrentTheme() + baseStyle := styles.BaseStyle() - version := styles.BaseStyle.Foreground(styles.ForgroundDim).Render(version.Version) + versionText := baseStyle. + Foreground(t.TextMuted()). + Render(version.Version) - return styles.BaseStyle. + return baseStyle. Bold(true). Width(width). Render( @@ -86,34 +112,28 @@ func logo(width int) string { lipgloss.Left, logo, " ", - version, + versionText, ), ) } func repo(width int) string { repo := "https://github.com/opencode-ai/opencode" - return styles.BaseStyle. - Foreground(styles.ForgroundDim). + 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()) - return styles.BaseStyle. - Foreground(styles.ForgroundDim). + t := theme.CurrentTheme() + + return styles.BaseStyle(). + Foreground(t.TextMuted()). Width(width). Render(cwd) } -func header(width int) string { - header := lipgloss.JoinVertical( - lipgloss.Top, - logo(width), - repo(width), - "", - cwd(width), - ) - return header -} diff --git a/internal/tui/components/chat/editor.go b/internal/tui/components/chat/editor.go index 88ac3e759..3548cbb0b 100644 --- a/internal/tui/components/chat/editor.go +++ b/internal/tui/components/chat/editor.go @@ -10,8 +10,10 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/opencode-ai/opencode/internal/app" "github.com/opencode-ai/opencode/internal/session" + "github.com/opencode-ai/opencode/internal/tui/components/dialog" "github.com/opencode-ai/opencode/internal/tui/layout" "github.com/opencode-ai/opencode/internal/tui/styles" + "github.com/opencode-ai/opencode/internal/tui/theme" "github.com/opencode-ai/opencode/internal/tui/util" ) @@ -100,6 +102,9 @@ func (m *editorCmp) send() tea.Cmd { 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) + return m, nil case SessionSelectedMsg: if msg.ID != m.session.ID { m.session = msg @@ -134,7 +139,13 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (m *editorCmp) View() string { - style := lipgloss.NewStyle().Padding(0, 0, 0, 1).Bold(true) + t := theme.CurrentTheme() + + // Style the prompt with theme colors + style := lipgloss.NewStyle(). + Padding(0, 0, 0, 1). + Bold(true). + Foreground(t.Primary()) return lipgloss.JoinHorizontal(lipgloss.Top, style.Render(">"), m.textarea.View()) } @@ -155,23 +166,42 @@ func (m *editorCmp) BindingKeys() []key.Binding { 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 { - ti := textarea.New() - ti.Prompt = " " - ti.ShowLineNumbers = false - ti.BlurredStyle.Base = ti.BlurredStyle.Base.Background(styles.Background) - ti.BlurredStyle.CursorLine = ti.BlurredStyle.CursorLine.Background(styles.Background) - ti.BlurredStyle.Placeholder = ti.BlurredStyle.Placeholder.Background(styles.Background) - ti.BlurredStyle.Text = ti.BlurredStyle.Text.Background(styles.Background) - - ti.FocusedStyle.Base = ti.FocusedStyle.Base.Background(styles.Background) - ti.FocusedStyle.CursorLine = ti.FocusedStyle.CursorLine.Background(styles.Background) - ti.FocusedStyle.Placeholder = ti.FocusedStyle.Placeholder.Background(styles.Background) - ti.FocusedStyle.Text = ti.BlurredStyle.Text.Background(styles.Background) - ti.CharLimit = -1 - ti.Focus() + ta := CreateTextArea(nil) + return &editorCmp{ app: app, - textarea: ti, + textarea: ta, } } + diff --git a/internal/tui/components/chat/list.go b/internal/tui/components/chat/list.go index fa7332d5f..12f1681fa 100644 --- a/internal/tui/components/chat/list.go +++ b/internal/tui/components/chat/list.go @@ -14,7 +14,9 @@ import ( "github.com/opencode-ai/opencode/internal/message" "github.com/opencode-ai/opencode/internal/pubsub" "github.com/opencode-ai/opencode/internal/session" + "github.com/opencode-ai/opencode/internal/tui/components/dialog" "github.com/opencode-ai/opencode/internal/tui/styles" + "github.com/opencode-ai/opencode/internal/tui/theme" "github.com/opencode-ai/opencode/internal/tui/util" ) @@ -69,7 +71,9 @@ func (m *messagesCmp) Init() tea.Cmd { func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmds []tea.Cmd switch msg := msg.(type) { - + case dialog.ThemeChangedMsg: + m.rerender() + return m, nil case SessionSelectedMsg: if msg.ID != m.session.ID { cmd := m.SetSession(msg) @@ -174,6 +178,7 @@ func formatTimeDifference(unixTime1, unixTime2 int64) string { func (m *messagesCmp) renderView() { m.uiMessages = make([]uiMessage, 0) pos := 0 + baseStyle := styles.BaseStyle() if m.width == 0 { return @@ -225,15 +230,13 @@ func (m *messagesCmp) renderView() { messages := make([]string, 0) for _, v := range m.uiMessages { messages = append(messages, v.content, - styles.BaseStyle. + baseStyle. Width(m.width). - Render( - "", - ), + Render(""), ) } m.viewport.SetContent( - styles.BaseStyle. + baseStyle. Width(m.width). Render( lipgloss.JoinVertical( @@ -245,8 +248,10 @@ func (m *messagesCmp) renderView() { } func (m *messagesCmp) View() string { + baseStyle := styles.BaseStyle() + if m.rendering { - return styles.BaseStyle. + return baseStyle. Width(m.width). Render( lipgloss.JoinVertical( @@ -258,14 +263,14 @@ func (m *messagesCmp) View() string { ) } if len(m.messages) == 0 { - content := styles.BaseStyle. + content := baseStyle. Width(m.width). Height(m.height - 1). Render( m.initialScreen(), ) - return styles.BaseStyle. + return baseStyle. Width(m.width). Render( lipgloss.JoinVertical( @@ -277,7 +282,7 @@ func (m *messagesCmp) View() string { ) } - return styles.BaseStyle. + return baseStyle. Width(m.width). Render( lipgloss.JoinVertical( @@ -328,6 +333,9 @@ func hasUnfinishedToolCalls(messages []message.Message) bool { func (m *messagesCmp) working() string { text := "" if m.IsAgentWorking() && len(m.messages) > 0 { + t := theme.CurrentTheme() + baseStyle := styles.BaseStyle() + task := "Thinking..." lastMessage := m.messages[len(m.messages)-1] if hasToolsWithoutResponse(m.messages) { @@ -338,42 +346,49 @@ func (m *messagesCmp) working() string { task = "Generating..." } if task != "" { - text += styles.BaseStyle.Width(m.width).Foreground(styles.PrimaryColor).Bold(true).Render( - fmt.Sprintf("%s %s ", m.spinner.View(), 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.CoderAgent.IsBusy() { text += lipgloss.JoinHorizontal( lipgloss.Left, - styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render("press "), - styles.BaseStyle.Foreground(styles.Forground).Bold(true).Render("esc"), - styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render(" to exit cancel"), + baseStyle.Foreground(t.TextMuted()).Bold(true).Render("press "), + baseStyle.Foreground(t.Text()).Bold(true).Render("esc"), + baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" to exit cancel"), ) } else { text += lipgloss.JoinHorizontal( lipgloss.Left, - styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render("press "), - styles.BaseStyle.Foreground(styles.Forground).Bold(true).Render("enter"), - styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render(" to send the message,"), - styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render(" write"), - styles.BaseStyle.Foreground(styles.Forground).Bold(true).Render(" \\"), - styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render(" and enter to add a new line"), + baseStyle.Foreground(t.TextMuted()).Bold(true).Render("press "), + baseStyle.Foreground(t.Text()).Bold(true).Render("enter"), + baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" to send the message,"), + baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" write"), + baseStyle.Foreground(t.Text()).Bold(true).Render(" \\"), + baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" and enter to add a new line"), ) } - return styles.BaseStyle. + return baseStyle. Width(m.width). Render(text) } func (m *messagesCmp) initialScreen() string { - return styles.BaseStyle.Width(m.width).Render( + baseStyle := styles.BaseStyle() + + return baseStyle.Width(m.width).Render( lipgloss.JoinVertical( lipgloss.Top, header(m.width), @@ -383,6 +398,13 @@ func (m *messagesCmp) initialScreen() string { ) } +func (m *messagesCmp) rerender() { + for _, msg := range m.messages { + delete(m.cachedContent, msg.ID) + } + m.renderView() +} + func (m *messagesCmp) SetSize(width, height int) tea.Cmd { if m.width == width && m.height == height { return nil @@ -391,11 +413,7 @@ func (m *messagesCmp) SetSize(width, height int) tea.Cmd { m.height = height m.viewport.Width = width m.viewport.Height = height - 2 - for _, msg := range m.messages { - delete(m.cachedContent, msg.ID) - } - m.uiMessages = make([]uiMessage, 0) - m.renderView() + m.rerender() return nil } diff --git a/internal/tui/components/chat/message.go b/internal/tui/components/chat/message.go index 53ec7ea3d..715e00d66 100644 --- a/internal/tui/components/chat/message.go +++ b/internal/tui/components/chat/message.go @@ -6,10 +6,8 @@ import ( "fmt" "path/filepath" "strings" - "sync" "time" - "github.com/charmbracelet/glamour" "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/x/ansi" "github.com/opencode-ai/opencode/internal/config" @@ -19,6 +17,7 @@ import ( "github.com/opencode-ai/opencode/internal/llm/tools" "github.com/opencode-ai/opencode/internal/message" "github.com/opencode-ai/opencode/internal/tui/styles" + "github.com/opencode-ai/opencode/internal/tui/theme" ) type uiMessageType int @@ -31,7 +30,10 @@ const ( maxResultHeight = 10 ) -var diffStyle = diff.NewStyleConfig(diff.WithShowHeader(false), diff.WithShowHunkHeader(false)) +// getDiffWidth returns the width for the diff formatting +func getDiffWidth(width int) int { + return width +} type uiMessage struct { ID string @@ -41,46 +43,37 @@ type uiMessage struct { content string } -type renderCache struct { - mutex sync.Mutex - cache map[string][]uiMessage -} - func toMarkdown(content string, focused bool, width int) string { - r, _ := glamour.NewTermRenderer( - glamour.WithStyles(styles.MarkdownTheme(false)), - glamour.WithWordWrap(width), - ) - if focused { - r, _ = glamour.NewTermRenderer( - glamour.WithStyles(styles.MarkdownTheme(true)), - glamour.WithWordWrap(width), - ) - } + r := styles.GetMarkdownRenderer(width) rendered, _ := r.Render(content) return rendered } func renderMessage(msg string, isUser bool, isFocused bool, width int, info ...string) string { - style := styles.BaseStyle. + t := theme.CurrentTheme() + + style := styles.BaseStyle(). Width(width - 1). BorderLeft(true). - Foreground(styles.ForgroundDim). - BorderForeground(styles.PrimaryColor). + Foreground(t.TextMuted()). + BorderForeground(t.Primary()). BorderStyle(lipgloss.ThickBorder()) + if isUser { - style = style. - BorderForeground(styles.Blue) + style = style.BorderForeground(t.Secondary()) } + + // Apply markdown formatting and handle background color parts := []string{ - styles.ForceReplaceBackgroundWithLipgloss(toMarkdown(msg, isFocused, width), styles.Background), + styles.ForceReplaceBackgroundWithLipgloss(toMarkdown(msg, isFocused, width), t.Background()), } - // remove newline at the end + // Remove newline at the end parts[0] = strings.TrimSuffix(parts[0], "\n") if len(info) > 0 { parts = append(parts, info...) } + rendered := style.Render( lipgloss.JoinVertical( lipgloss.Left, @@ -121,26 +114,37 @@ func renderAssistantMessage( finishData := msg.FinishPart() info := []string{} + t := theme.CurrentTheme() + baseStyle := styles.BaseStyle() + // Add finish info if available if finished { switch finishData.Reason { case message.FinishReasonEndTurn: - took := formatTimeDifference(msg.CreatedAt, finishData.Time) - info = append(info, styles.BaseStyle.Width(width-1).Foreground(styles.ForgroundDim).Render( - fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, took), - )) + took := formatTimestampDiff(msg.CreatedAt, finishData.Time) + info = append(info, baseStyle. + Width(width-1). + Foreground(t.TextMuted()). + Render(fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, took)), + ) case message.FinishReasonCanceled: - info = append(info, styles.BaseStyle.Width(width-1).Foreground(styles.ForgroundDim).Render( - fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, "canceled"), - )) + info = append(info, baseStyle. + Width(width-1). + Foreground(t.TextMuted()). + Render(fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, "canceled")), + ) case message.FinishReasonError: - info = append(info, styles.BaseStyle.Width(width-1).Foreground(styles.ForgroundDim).Render( - fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, "error"), - )) + info = append(info, baseStyle. + Width(width-1). + Foreground(t.TextMuted()). + Render(fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, "error")), + ) case message.FinishReasonPermissionDenied: - info = append(info, styles.BaseStyle.Width(width-1).Foreground(styles.ForgroundDim).Render( - fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, "permission denied"), - )) + info = append(info, baseStyle. + Width(width-1). + Foreground(t.TextMuted()). + Render(fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, "permission denied")), + ) } } if content != "" || (finished && finishData.Reason == message.FinishReasonEndTurn) { @@ -414,32 +418,36 @@ func truncateHeight(content string, height int) string { } func renderToolResponse(toolCall message.ToolCall, response message.ToolResult, width int) string { + 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 styles.BaseStyle. + return baseStyle. Width(width). - Foreground(styles.Error). + Foreground(t.Error()). Render(errContent) } + resultContent := truncateHeight(response.Content, maxResultHeight) switch toolCall.Name { case agent.AgentToolName: return styles.ForceReplaceBackgroundWithLipgloss( toMarkdown(resultContent, false, width), - styles.Background, + t.Background(), ) case tools.BashToolName: resultContent = fmt.Sprintf("```bash\n%s\n```", resultContent) return styles.ForceReplaceBackgroundWithLipgloss( toMarkdown(resultContent, true, width), - styles.Background, + t.Background(), ) case tools.EditToolName: metadata := tools.EditResponseMetadata{} json.Unmarshal([]byte(response.Metadata), &metadata) truncDiff := truncateHeight(metadata.Diff, maxResultHeight) - formattedDiff, _ := diff.FormatDiff(truncDiff, diff.WithTotalWidth(width), diff.WithStyle(diffStyle)) + formattedDiff, _ := diff.FormatDiff(truncDiff, diff.WithTotalWidth(width)) return formattedDiff case tools.FetchToolName: var params tools.FetchParams @@ -454,16 +462,16 @@ func renderToolResponse(toolCall message.ToolCall, response message.ToolResult, resultContent = fmt.Sprintf("```%s\n%s\n```", mdFormat, resultContent) return styles.ForceReplaceBackgroundWithLipgloss( toMarkdown(resultContent, true, width), - styles.Background, + t.Background(), ) case tools.GlobToolName: - return styles.BaseStyle.Width(width).Foreground(styles.ForgroundMid).Render(resultContent) + return baseStyle.Width(width).Foreground(t.TextMuted()).Render(resultContent) case tools.GrepToolName: - return styles.BaseStyle.Width(width).Foreground(styles.ForgroundMid).Render(resultContent) + return baseStyle.Width(width).Foreground(t.TextMuted()).Render(resultContent) case tools.LSToolName: - return styles.BaseStyle.Width(width).Foreground(styles.ForgroundMid).Render(resultContent) + return baseStyle.Width(width).Foreground(t.TextMuted()).Render(resultContent) case tools.SourcegraphToolName: - return styles.BaseStyle.Width(width).Foreground(styles.ForgroundMid).Render(resultContent) + return baseStyle.Width(width).Foreground(t.TextMuted()).Render(resultContent) case tools.ViewToolName: metadata := tools.ViewResponseMetadata{} json.Unmarshal([]byte(response.Metadata), &metadata) @@ -476,7 +484,7 @@ func renderToolResponse(toolCall message.ToolCall, response message.ToolResult, resultContent = fmt.Sprintf("```%s\n%s\n```", ext, truncateHeight(metadata.Content, maxResultHeight)) return styles.ForceReplaceBackgroundWithLipgloss( toMarkdown(resultContent, true, width), - styles.Background, + t.Background(), ) case tools.WriteToolName: params := tools.WriteParams{} @@ -492,13 +500,13 @@ func renderToolResponse(toolCall message.ToolCall, response message.ToolResult, resultContent = fmt.Sprintf("```%s\n%s\n```", ext, truncateHeight(params.Content, maxResultHeight)) return styles.ForceReplaceBackgroundWithLipgloss( toMarkdown(resultContent, true, width), - styles.Background, + t.Background(), ) default: resultContent = fmt.Sprintf("```text\n%s\n```", resultContent) return styles.ForceReplaceBackgroundWithLipgloss( toMarkdown(resultContent, true, width), - styles.Background, + t.Background(), ) } } @@ -515,39 +523,31 @@ func renderToolMessage( if nested { width = width - 3 } - style := styles.BaseStyle. + + t := theme.CurrentTheme() + baseStyle := styles.BaseStyle() + + style := baseStyle. Width(width - 1). BorderLeft(true). BorderStyle(lipgloss.ThickBorder()). PaddingLeft(1). - BorderForeground(styles.ForgroundDim) + BorderForeground(t.TextMuted()) response := findToolResponse(toolCall.ID, allMessages) - toolName := styles.BaseStyle.Foreground(styles.ForgroundDim).Render(fmt.Sprintf("%s: ", toolName(toolCall.Name))) + toolNameText := baseStyle.Foreground(t.TextMuted()). + Render(fmt.Sprintf("%s: ", toolName(toolCall.Name))) if !toolCall.Finished { // Get a brief description of what the tool is doing toolAction := getToolAction(toolCall.Name) - // toolInput := strings.ReplaceAll(toolCall.Input, "\n", " ") - // truncatedInput := toolInput - // if len(truncatedInput) > 10 { - // truncatedInput = truncatedInput[len(truncatedInput)-10:] - // } - // - // truncatedInput = styles.BaseStyle. - // Italic(true). - // Width(width - 2 - lipgloss.Width(toolName)). - // Background(styles.BackgroundDim). - // Foreground(styles.ForgroundMid). - // Render(truncatedInput) - - progressText := styles.BaseStyle. - Width(width - 2 - lipgloss.Width(toolName)). - Foreground(styles.ForgroundDim). + progressText := baseStyle. + Width(width - 2 - lipgloss.Width(toolNameText)). + Foreground(t.TextMuted()). Render(fmt.Sprintf("%s", toolAction)) - content := style.Render(lipgloss.JoinHorizontal(lipgloss.Left, toolName, progressText)) + content := style.Render(lipgloss.JoinHorizontal(lipgloss.Left, toolNameText, progressText)) toolMsg := uiMessage{ messageType: toolMessageType, position: position, @@ -556,37 +556,39 @@ func renderToolMessage( } return toolMsg } - params := renderToolParams(width-2-lipgloss.Width(toolName), toolCall) + + params := renderToolParams(width-2-lipgloss.Width(toolNameText), toolCall) responseContent := "" if response != nil { responseContent = renderToolResponse(toolCall, *response, width-2) responseContent = strings.TrimSuffix(responseContent, "\n") } else { - responseContent = styles.BaseStyle. + responseContent = baseStyle. Italic(true). Width(width - 2). - Foreground(styles.ForgroundDim). + Foreground(t.TextMuted()). Render("Waiting for response...") } parts := []string{} if !nested { - params := styles.BaseStyle. - Width(width - 2 - lipgloss.Width(toolName)). - Foreground(styles.ForgroundDim). + formattedParams := baseStyle. + Width(width - 2 - lipgloss.Width(toolNameText)). + Foreground(t.TextMuted()). Render(params) - parts = append(parts, lipgloss.JoinHorizontal(lipgloss.Left, toolName, params)) + parts = append(parts, lipgloss.JoinHorizontal(lipgloss.Left, toolNameText, formattedParams)) } else { - prefix := styles.BaseStyle. - Foreground(styles.ForgroundDim). + prefix := baseStyle. + Foreground(t.TextMuted()). Render(" └ ") - params := styles.BaseStyle. - Width(width - 2 - lipgloss.Width(toolName)). - Foreground(styles.ForgroundMid). + formattedParams := baseStyle. + Width(width - 2 - lipgloss.Width(toolNameText)). + Foreground(t.TextMuted()). Render(params) - parts = append(parts, lipgloss.JoinHorizontal(lipgloss.Left, prefix, toolName, 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{} @@ -622,3 +624,16 @@ func renderToolMessage( } return toolMsg } + +// Helper function to format the time difference between two Unix timestamps +func formatTimestampDiff(start, end int64) string { + diffSeconds := float64(end-start) / 1000.0 // Convert to seconds + if diffSeconds < 1 { + return fmt.Sprintf("%dms", int(diffSeconds*1000)) + } + if diffSeconds < 60 { + return fmt.Sprintf("%.1fs", diffSeconds) + } + return fmt.Sprintf("%.1fm", diffSeconds/60) +} + diff --git a/internal/tui/components/chat/sidebar.go b/internal/tui/components/chat/sidebar.go index 5baac3cd4..a66249b36 100644 --- a/internal/tui/components/chat/sidebar.go +++ b/internal/tui/components/chat/sidebar.go @@ -14,6 +14,7 @@ import ( "github.com/opencode-ai/opencode/internal/pubsub" "github.com/opencode-ai/opencode/internal/session" "github.com/opencode-ai/opencode/internal/tui/styles" + "github.com/opencode-ai/opencode/internal/tui/theme" ) type sidebarCmp struct { @@ -81,7 +82,9 @@ func (m *sidebarCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (m *sidebarCmp) View() string { - return styles.BaseStyle. + baseStyle := styles.BaseStyle() + + return baseStyle. Width(m.width). PaddingLeft(4). PaddingRight(2). @@ -101,11 +104,19 @@ func (m *sidebarCmp) View() string { } func (m *sidebarCmp) sessionSection() string { - sessionKey := styles.BaseStyle.Foreground(styles.PrimaryColor).Bold(true).Render("Session") - sessionValue := styles.BaseStyle. - Foreground(styles.Forground). + t := theme.CurrentTheme() + baseStyle := styles.BaseStyle() + + sessionKey := baseStyle. + Foreground(t.Primary()). + Bold(true). + Render("Session") + + sessionValue := baseStyle. + Foreground(t.Text()). Width(m.width - lipgloss.Width(sessionKey)). Render(fmt.Sprintf(": %s", m.session.Title)) + return lipgloss.JoinHorizontal( lipgloss.Left, sessionKey, @@ -114,22 +125,40 @@ func (m *sidebarCmp) sessionSection() string { } func (m *sidebarCmp) modifiedFile(filePath string, additions, removals int) string { + t := theme.CurrentTheme() + baseStyle := styles.BaseStyle() + stats := "" if additions > 0 && removals > 0 { - additions := styles.BaseStyle.Foreground(styles.Green).PaddingLeft(1).Render(fmt.Sprintf("+%d", additions)) - removals := styles.BaseStyle.Foreground(styles.Red).PaddingLeft(1).Render(fmt.Sprintf("-%d", removals)) - content := lipgloss.JoinHorizontal(lipgloss.Left, additions, removals) - stats = styles.BaseStyle.Width(lipgloss.Width(content)).Render(content) + 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 { - additions := fmt.Sprintf(" %s", styles.BaseStyle.PaddingLeft(1).Foreground(styles.Green).Render(fmt.Sprintf("+%d", additions))) - stats = styles.BaseStyle.Width(lipgloss.Width(additions)).Render(additions) + 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 { - removals := fmt.Sprintf(" %s", styles.BaseStyle.PaddingLeft(1).Foreground(styles.Red).Render(fmt.Sprintf("-%d", removals))) - stats = styles.BaseStyle.Width(lipgloss.Width(removals)).Render(removals) + removalsStr := fmt.Sprintf(" %s", baseStyle. + PaddingLeft(1). + Foreground(t.Error()). + Render(fmt.Sprintf("-%d", removals))) + stats = baseStyle.Width(lipgloss.Width(removalsStr)).Render(removalsStr) } - filePathStr := styles.BaseStyle.Render(filePath) - return styles.BaseStyle. + filePathStr := baseStyle.Render(filePath) + + return baseStyle. Width(m.width). Render( lipgloss.JoinHorizontal( @@ -141,7 +170,14 @@ func (m *sidebarCmp) modifiedFile(filePath string, additions, removals int) stri } func (m *sidebarCmp) modifiedFiles() string { - modifiedFiles := styles.BaseStyle.Width(m.width).Foreground(styles.PrimaryColor).Bold(true).Render("Modified Files:") + 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 { @@ -150,13 +186,13 @@ func (m *sidebarCmp) modifiedFiles() string { if remainingWidth > 0 { message += strings.Repeat(" ", remainingWidth) } - return styles.BaseStyle. + return baseStyle. Width(m.width). Render( lipgloss.JoinVertical( lipgloss.Top, modifiedFiles, - styles.BaseStyle.Foreground(styles.ForgroundDim).Render(message), + baseStyle.Foreground(t.TextMuted()).Render(message), ), ) } @@ -175,7 +211,7 @@ func (m *sidebarCmp) modifiedFiles() string { fileViews = append(fileViews, m.modifiedFile(path, stats.additions, stats.removals)) } - return styles.BaseStyle. + return baseStyle. Width(m.width). Render( lipgloss.JoinVertical( |
