summaryrefslogtreecommitdiffhomepage
path: root/internal/tui/components/chat
diff options
context:
space:
mode:
authoradamdottv <[email protected]>2025-04-28 08:46:09 -0500
committeradamdottv <[email protected]>2025-04-30 07:46:34 -0500
commit61b605e724eb4cc50ab831534fcdd18e031d68eb (patch)
treeb15b074a8fed1931e179a8d3df08d05b753c7aa3 /internal/tui/components/chat
parent61d9dc95111d2645a49816f6d9d6cc1014be1a22 (diff)
downloadopencode-61b605e724eb4cc50ab831534fcdd18e031d68eb.tar.gz
opencode-61b605e724eb4cc50ab831534fcdd18e031d68eb.zip
feat: themes
Diffstat (limited to 'internal/tui/components/chat')
-rw-r--r--internal/tui/components/chat/chat.go72
-rw-r--r--internal/tui/components/chat/editor.go62
-rw-r--r--internal/tui/components/chat/list.go76
-rw-r--r--internal/tui/components/chat/message.go183
-rw-r--r--internal/tui/components/chat/sidebar.go72
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(