Просмотр исходного кода

feat: default system theme (#419)

Co-authored-by: adamdottv <[email protected]>
Adam 8 месяцев назад
Родитель
Сommit
7d13baadc8
33 измененных файлов с 1221 добавлено и 436 удалено
  1. 1 0
      packages/tui/cmd/opencode/main.go
  2. 7 0
      packages/tui/internal/app/app.go
  3. 2 1
      packages/tui/internal/completions/commands.go
  4. 41 24
      packages/tui/internal/components/chat/editor.go
  5. 16 15
      packages/tui/internal/components/chat/message.go
  6. 13 13
      packages/tui/internal/components/chat/messages.go
  7. 7 12
      packages/tui/internal/components/commands/commands.go
  8. 2 2
      packages/tui/internal/components/dialog/complete.go
  9. 1 1
      packages/tui/internal/components/dialog/init.go
  10. 1 1
      packages/tui/internal/components/dialog/models.go
  11. 2 3
      packages/tui/internal/components/dialog/permission.go
  12. 5 6
      packages/tui/internal/components/dialog/session.go
  13. 1 1
      packages/tui/internal/components/dialog/theme.go
  14. 139 110
      packages/tui/internal/components/diff/diff.go
  15. 3 3
      packages/tui/internal/components/list/list.go
  16. 1 5
      packages/tui/internal/components/modal/modal.go
  17. 2 4
      packages/tui/internal/components/qr/qr.go
  18. 12 9
      packages/tui/internal/components/status/status.go
  19. 4 5
      packages/tui/internal/components/toast/toast.go
  20. 1 1
      packages/tui/internal/config/config.go
  21. 2 3
      packages/tui/internal/layout/container.go
  22. 3 2
      packages/tui/internal/layout/flex.go
  23. 4 0
      packages/tui/internal/styles/background.go
  24. 62 55
      packages/tui/internal/styles/markdown.go
  25. 2 149
      packages/tui/internal/styles/styles.go
  26. 295 0
      packages/tui/internal/styles/utilities.go
  27. 11 2
      packages/tui/internal/theme/loader.go
  28. 129 5
      packages/tui/internal/theme/manager.go
  29. 299 0
      packages/tui/internal/theme/system.go
  30. 15 1
      packages/tui/internal/tui/tui.go
  31. 93 0
      packages/tui/internal/util/color.go
  32. 20 0
      packages/web/public/theme.json
  33. 25 3
      packages/web/src/content/docs/docs/themes.mdx

+ 1 - 0
packages/tui/cmd/opencode/main.go

@@ -66,6 +66,7 @@ func main() {
 
 	program := tea.NewProgram(
 		tui.NewModel(app_),
+		// tea.WithColorProfile(colorprofile.ANSI),
 		tea.WithAltScreen(),
 		tea.WithKeyboardEnhancements(),
 		tea.WithMouseCellMotion(),

+ 7 - 0
packages/tui/internal/app/app.go

@@ -14,6 +14,7 @@ import (
 	"github.com/sst/opencode/internal/commands"
 	"github.com/sst/opencode/internal/components/toast"
 	"github.com/sst/opencode/internal/config"
+	"github.com/sst/opencode/internal/styles"
 	"github.com/sst/opencode/internal/theme"
 	"github.com/sst/opencode/internal/util"
 	"github.com/sst/opencode/pkg/client"
@@ -103,6 +104,12 @@ func New(
 	}
 
 	if appState.Theme != "" {
+		if appState.Theme == "system" && styles.Terminal != nil {
+			theme.UpdateSystemTheme(
+				styles.Terminal.Background,
+				styles.Terminal.BackgroundIsDark,
+			)
+		}
 		theme.SetTheme(appState.Theme)
 	}
 

+ 2 - 1
packages/tui/internal/completions/commands.go

@@ -9,6 +9,7 @@ import (
 	"github.com/sst/opencode/internal/app"
 	"github.com/sst/opencode/internal/commands"
 	"github.com/sst/opencode/internal/components/dialog"
+	"github.com/sst/opencode/internal/styles"
 	"github.com/sst/opencode/internal/theme"
 )
 
@@ -37,7 +38,7 @@ func (c *CommandCompletionProvider) GetEmptyMessage() string {
 
 func getCommandCompletionItem(cmd commands.Command, space int, t theme.Theme) dialog.CompletionItemI {
 	spacer := strings.Repeat(" ", space)
-	title := "  /" + cmd.Trigger + lipgloss.NewStyle().Foreground(t.TextMuted()).Render(spacer+cmd.Description)
+	title := "  /" + cmd.Trigger + styles.NewStyle().Foreground(t.TextMuted()).Render(spacer+cmd.Description)
 	value := string(cmd.Name)
 	return dialog.NewCompletionItem(dialog.CompletionItem{
 		Title: title,

+ 41 - 24
packages/tui/internal/components/chat/editor.go

@@ -26,6 +26,9 @@ type EditorComponent interface {
 	Content() string
 	Lines() int
 	Value() string
+	Focused() bool
+	Focus() (tea.Model, tea.Cmd)
+	Blur()
 	Submit() (tea.Model, tea.Cmd)
 	Clear() (tea.Model, tea.Cmd)
 	Paste() (tea.Model, tea.Cmd)
@@ -48,7 +51,7 @@ type editorComponent struct {
 }
 
 func (m *editorComponent) Init() tea.Cmd {
-	return tea.Batch(textarea.Blink, m.spinner.Tick, tea.EnableReportFocus)
+	return tea.Batch(m.textarea.Focus(), m.spinner.Tick, tea.EnableReportFocus)
 }
 
 func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
@@ -69,7 +72,7 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	case dialog.ThemeSelectedMsg:
 		m.textarea = createTextArea(&m.textarea)
 		m.spinner = createSpinner()
-		return m, tea.Batch(m.spinner.Tick, textarea.Blink)
+		return m, tea.Batch(m.spinner.Tick, m.textarea.Focus())
 	case dialog.CompletionSelectedMsg:
 		if msg.IsCommand {
 			commandName := strings.TrimPrefix(msg.CompletionValue, "/")
@@ -104,12 +107,11 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 
 func (m *editorComponent) Content() string {
 	t := theme.CurrentTheme()
-	base := styles.BaseStyle().Background(t.Background()).Render
-	muted := styles.Muted().Background(t.Background()).Render
-	promptStyle := lipgloss.NewStyle().
+	base := styles.NewStyle().Foreground(t.Text()).Background(t.Background()).Render
+	muted := styles.NewStyle().Foreground(t.TextMuted()).Background(t.Background()).Render
+	promptStyle := styles.NewStyle().Foreground(t.Primary()).
 		Padding(0, 0, 0, 1).
-		Bold(true).
-		Foreground(t.Primary())
+		Bold(true)
 	prompt := promptStyle.Render(">")
 
 	textarea := lipgloss.JoinHorizontal(
@@ -117,11 +119,11 @@ func (m *editorComponent) Content() string {
 		prompt,
 		m.textarea.View(),
 	)
-	textarea = styles.BaseStyle().
+	textarea = styles.NewStyle().
+		Background(t.BackgroundElement()).
 		Width(m.width).
 		PaddingTop(1).
 		PaddingBottom(1).
-		Background(t.BackgroundElement()).
 		Render(textarea)
 
 	hint := base(m.getSubmitKeyText()) + muted(" send   ")
@@ -140,10 +142,10 @@ func (m *editorComponent) Content() string {
 	}
 
 	space := m.width - 2 - lipgloss.Width(model) - lipgloss.Width(hint)
-	spacer := lipgloss.NewStyle().Background(t.Background()).Width(space).Render("")
+	spacer := styles.NewStyle().Background(t.Background()).Width(space).Render("")
 
 	info := hint + spacer + model
-	info = styles.Padded().Background(t.Background()).Render(info)
+	info = styles.NewStyle().Background(t.Background()).Padding(0, 1).Render(info)
 
 	content := strings.Join([]string{"", textarea, info}, "\n")
 	return content
@@ -156,6 +158,18 @@ func (m *editorComponent) View() string {
 	return m.Content()
 }
 
+func (m *editorComponent) Focused() bool {
+	return m.textarea.Focused()
+}
+
+func (m *editorComponent) Focus() (tea.Model, tea.Cmd) {
+	return m, m.textarea.Focus()
+}
+
+func (m *editorComponent) Blur() {
+	m.textarea.Blur()
+}
+
 func (m *editorComponent) GetSize() (width, height int) {
 	return m.width, m.height
 }
@@ -297,14 +311,14 @@ func createTextArea(existing *textarea.Model) textarea.Model {
 
 	ta := textarea.New()
 
-	ta.Styles.Blurred.Base = lipgloss.NewStyle().Background(bgColor).Foreground(textColor)
-	ta.Styles.Blurred.CursorLine = lipgloss.NewStyle().Background(bgColor)
-	ta.Styles.Blurred.Placeholder = lipgloss.NewStyle().Background(bgColor).Foreground(textMutedColor)
-	ta.Styles.Blurred.Text = lipgloss.NewStyle().Background(bgColor).Foreground(textColor)
-	ta.Styles.Focused.Base = lipgloss.NewStyle().Background(bgColor).Foreground(textColor)
-	ta.Styles.Focused.CursorLine = lipgloss.NewStyle().Background(bgColor)
-	ta.Styles.Focused.Placeholder = lipgloss.NewStyle().Background(bgColor).Foreground(textMutedColor)
-	ta.Styles.Focused.Text = lipgloss.NewStyle().Background(bgColor).Foreground(textColor)
+	ta.Styles.Blurred.Base = styles.NewStyle().Foreground(textColor).Background(bgColor).Lipgloss()
+	ta.Styles.Blurred.CursorLine = styles.NewStyle().Background(bgColor).Lipgloss()
+	ta.Styles.Blurred.Placeholder = styles.NewStyle().Foreground(textMutedColor).Background(bgColor).Lipgloss()
+	ta.Styles.Blurred.Text = styles.NewStyle().Foreground(textColor).Background(bgColor).Lipgloss()
+	ta.Styles.Focused.Base = styles.NewStyle().Foreground(textColor).Background(bgColor).Lipgloss()
+	ta.Styles.Focused.CursorLine = styles.NewStyle().Background(bgColor).Lipgloss()
+	ta.Styles.Focused.Placeholder = styles.NewStyle().Foreground(textMutedColor).Background(bgColor).Lipgloss()
+	ta.Styles.Focused.Text = styles.NewStyle().Foreground(textColor).Background(bgColor).Lipgloss()
 	ta.Styles.Cursor.Color = t.Primary()
 
 	ta.Prompt = " "
@@ -317,18 +331,21 @@ func createTextArea(existing *textarea.Model) textarea.Model {
 		ta.SetHeight(existing.Height())
 	}
 
-	ta.Focus()
+	// ta.Focus()
 	return ta
 }
 
 func createSpinner() spinner.Model {
+	t := theme.CurrentTheme()
 	return spinner.New(
 		spinner.WithSpinner(spinner.Ellipsis),
 		spinner.WithStyle(
-			styles.
-				Muted().
-				Background(theme.CurrentTheme().Background()).
-				Width(3)),
+			styles.NewStyle().
+				Foreground(t.Background()).
+				Foreground(t.TextMuted()).
+				Width(3).
+				Lipgloss(),
+		),
 	)
 }
 

+ 16 - 15
packages/tui/internal/components/chat/message.go

@@ -129,15 +129,13 @@ func renderContentBlock(content string, options ...renderingOption) string {
 		option(renderer)
 	}
 
-	style := styles.BaseStyle().
+	style := styles.NewStyle().Foreground(t.TextMuted()).Background(t.BackgroundPanel()).
 		// MarginTop(renderer.marginTop).
 		// MarginBottom(renderer.marginBottom).
 		PaddingTop(renderer.paddingTop).
 		PaddingBottom(renderer.paddingBottom).
 		PaddingLeft(renderer.paddingLeft).
 		PaddingRight(renderer.paddingRight).
-		Background(t.BackgroundPanel()).
-		Foreground(t.TextMuted()).
 		BorderStyle(lipgloss.ThickBorder())
 
 	align := lipgloss.Left
@@ -179,13 +177,13 @@ func renderContentBlock(content string, options ...renderingOption) string {
 		layout.Current.Container.Width,
 		align,
 		content,
-		lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
+		styles.WhitespaceStyle(t.Background()),
 	)
 	content = lipgloss.PlaceHorizontal(
 		layout.Current.Viewport.Width,
 		lipgloss.Center,
 		content,
-		lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
+		styles.WhitespaceStyle(t.Background()),
 	)
 	if renderer.marginTop > 0 {
 		for range renderer.marginTop {
@@ -226,7 +224,7 @@ func renderText(message client.MessageInfo, text string, author string) string {
 	textWidth := max(lipgloss.Width(text), lipgloss.Width(info))
 	markdownWidth := min(textWidth, width-padding-4) // -4 for the border and padding
 	if message.Role == client.Assistant {
-		markdownWidth = width - padding - 4 - 2
+		markdownWidth = width - padding - 4 - 3
 	}
 	if message.Role == client.User {
 		text = strings.ReplaceAll(text, "<", "\\<")
@@ -275,9 +273,10 @@ func renderToolInvocation(
 	}
 
 	t := theme.CurrentTheme()
-	style := styles.Muted().
-		Width(outerWidth).
+	style := styles.NewStyle().
+		Foreground(t.TextMuted()).
 		Background(t.BackgroundPanel()).
+		Width(outerWidth).
 		PaddingTop(paddingTop).
 		PaddingBottom(paddingBottom).
 		PaddingLeft(2).
@@ -293,7 +292,9 @@ func renderToolInvocation(
 		if !showDetails {
 			title = "∟ " + title
 			padding := calculatePadding()
-			style := lipgloss.NewStyle().Width(outerWidth - padding - 4).Background(t.BackgroundPanel())
+			style := styles.NewStyle().
+				Background(t.BackgroundPanel()).
+				Width(outerWidth - padding - 4 - 3)
 			return renderContentBlock(style.Render(title),
 				WithAlign(lipgloss.Left),
 				WithBorderColor(t.Accent()),
@@ -334,9 +335,9 @@ func renderToolInvocation(
 	if e, ok := metadata.Get("error"); ok && e.(bool) == true {
 		if m, ok := metadata.Get("message"); ok {
 			style = style.BorderLeftForeground(t.Error())
-			error = styles.BaseStyle().
-				Background(t.BackgroundPanel()).
+			error = styles.NewStyle().
 				Foreground(t.Error()).
+				Background(t.BackgroundPanel()).
 				Render(m.(string))
 			error = renderContentBlock(
 				error,
@@ -374,7 +375,7 @@ func renderToolInvocation(
 					formattedDiff, _ = diff.FormatDiff(filename, patch, diff.WithTotalWidth(diffWidth))
 				}
 				formattedDiff = strings.TrimSpace(formattedDiff)
-				formattedDiff = lipgloss.NewStyle().
+				formattedDiff = styles.NewStyle().
 					BorderStyle(lipgloss.ThickBorder()).
 					BorderBackground(t.Background()).
 					BorderForeground(t.BackgroundPanel()).
@@ -394,7 +395,7 @@ func renderToolInvocation(
 					lipgloss.Center,
 					lipgloss.Top,
 					body,
-					lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
+					styles.WhitespaceStyle(t.Background()),
 				)
 			}
 		}
@@ -506,7 +507,7 @@ func renderToolInvocation(
 	if !showDetails {
 		title = "∟ " + title
 		padding := calculatePadding()
-		style := lipgloss.NewStyle().Width(outerWidth - padding - 4).Background(t.BackgroundPanel())
+		style := styles.NewStyle().Background(t.BackgroundPanel()).Width(outerWidth - padding - 4 - 3)
 		paddingBottom := 0
 		if isLast {
 			paddingBottom = 1
@@ -530,7 +531,7 @@ func renderToolInvocation(
 		layout.Current.Viewport.Width,
 		lipgloss.Center,
 		content,
-		lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
+		styles.WhitespaceStyle(t.Background()),
 	)
 	if showDetails && body != "" && error == "" {
 		content += "\n" + body

+ 13 - 13
packages/tui/internal/components/chat/messages.go

@@ -245,7 +245,7 @@ func (m *messagesComponent) renderView() {
 			m.width,
 			lipgloss.Center,
 			block,
-			lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
+			styles.WhitespaceStyle(t.Background()),
 		))
 	}
 
@@ -260,8 +260,8 @@ func (m *messagesComponent) header() string {
 
 	t := theme.CurrentTheme()
 	width := layout.Current.Container.Width
-	base := styles.BaseStyle().Background(t.Background()).Render
-	muted := styles.Muted().Background(t.Background()).Render
+	base := styles.NewStyle().Foreground(t.Text()).Background(t.Background()).Render
+	muted := styles.NewStyle().Foreground(t.TextMuted()).Background(t.Background()).Render
 	headerLines := []string{}
 	headerLines = append(headerLines, toMarkdown("# "+m.app.Session.Title, width-6, t.Background()))
 	if m.app.Session.Share != nil && m.app.Session.Share.Url != "" {
@@ -271,11 +271,11 @@ func (m *messagesComponent) header() string {
 	}
 	header := strings.Join(headerLines, "\n")
 
-	header = styles.BaseStyle().
+	header = styles.NewStyle().
+		Background(t.Background()).
 		Width(width).
 		PaddingLeft(2).
 		PaddingRight(2).
-		Background(t.Background()).
 		BorderLeft(true).
 		BorderRight(true).
 		BorderBackground(t.Background()).
@@ -306,7 +306,7 @@ func (m *messagesComponent) View() string {
 			m.width,
 			lipgloss.Center,
 			m.header(),
-			lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
+			styles.WhitespaceStyle(t.Background()),
 		),
 		m.viewport.View(),
 	)
@@ -314,9 +314,9 @@ func (m *messagesComponent) View() string {
 
 func (m *messagesComponent) home() string {
 	t := theme.CurrentTheme()
-	baseStyle := styles.BaseStyle().Background(t.Background())
+	baseStyle := styles.NewStyle().Background(t.Background())
 	base := baseStyle.Render
-	muted := styles.Muted().Background(t.Background()).Render
+	muted := styles.NewStyle().Foreground(t.TextMuted()).Background(t.Background()).Render
 
 	open := `
 █▀▀█ █▀▀█ █▀▀ █▀▀▄ 
@@ -335,9 +335,9 @@ func (m *messagesComponent) home() string {
 	// cwd := app.Info.Path.Cwd
 	// config := app.Info.Path.Config
 
-	versionStyle := lipgloss.NewStyle().
-		Background(t.Background()).
+	versionStyle := styles.NewStyle().
 		Foreground(t.TextMuted()).
+		Background(t.Background()).
 		Width(lipgloss.Width(logo)).
 		Align(lipgloss.Right)
 	version := versionStyle.Render(m.app.Version)
@@ -347,14 +347,14 @@ func (m *messagesComponent) home() string {
 		m.width,
 		lipgloss.Center,
 		logoAndVersion,
-		lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
+		styles.WhitespaceStyle(t.Background()),
 	)
 	m.commands.SetBackgroundColor(t.Background())
 	commands := lipgloss.PlaceHorizontal(
 		m.width,
 		lipgloss.Center,
 		m.commands.View(),
-		lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
+		styles.WhitespaceStyle(t.Background()),
 	)
 
 	lines := []string{}
@@ -372,7 +372,7 @@ func (m *messagesComponent) home() string {
 		lipgloss.Center,
 		lipgloss.Center,
 		baseStyle.Render(strings.Join(lines, "\n")),
-		lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
+		styles.WhitespaceStyle(t.Background()),
 	)
 }
 

+ 7 - 12
packages/tui/internal/components/commands/commands.go

@@ -60,15 +60,9 @@ func (c *commandsComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 func (c *commandsComponent) View() string {
 	t := theme.CurrentTheme()
 
-	triggerStyle := lipgloss.NewStyle().
-		Foreground(t.Primary()).
-		Bold(true)
-
-	descriptionStyle := lipgloss.NewStyle().
-		Foreground(t.Text())
-
-	keybindStyle := lipgloss.NewStyle().
-		Foreground(t.TextMuted())
+	triggerStyle := styles.NewStyle().Foreground(t.Primary()).Bold(true)
+	descriptionStyle := styles.NewStyle().Foreground(t.Text())
+	keybindStyle := styles.NewStyle().Foreground(t.TextMuted())
 
 	if c.background != nil {
 		triggerStyle = triggerStyle.Background(*c.background)
@@ -99,10 +93,11 @@ func (c *commandsComponent) View() string {
 	}
 
 	if len(commandsToShow) == 0 {
+		muted := styles.NewStyle().Foreground(theme.CurrentTheme().TextMuted())
 		if c.showAll {
-			return styles.Muted().Render("No commands available")
+			return muted.Render("No commands available")
 		}
-		return styles.Muted().Render("No commands with triggers available")
+		return muted.Render("No commands with triggers available")
 	}
 
 	// Calculate column widths
@@ -188,7 +183,7 @@ func (c *commandsComponent) View() string {
 	// Remove trailing newline
 	result := strings.TrimSuffix(output.String(), "\n")
 	if c.background != nil {
-		result = lipgloss.NewStyle().Background(c.background).Width(maxWidth).Render(result)
+		result = styles.NewStyle().Background(*c.background).Width(maxWidth).Render(result)
 	}
 
 	return result

+ 2 - 2
packages/tui/internal/components/dialog/complete.go

@@ -26,7 +26,7 @@ type CompletionItemI interface {
 
 func (ci *CompletionItem) Render(selected bool, width int) string {
 	t := theme.CurrentTheme()
-	baseStyle := styles.BaseStyle()
+	baseStyle := styles.NewStyle().Foreground(t.Text())
 
 	itemStyle := baseStyle.
 		Background(t.BackgroundElement()).
@@ -185,7 +185,7 @@ func (c *completionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 
 func (c *completionDialogComponent) View() string {
 	t := theme.CurrentTheme()
-	baseStyle := styles.BaseStyle()
+	baseStyle := styles.NewStyle().Foreground(t.Text())
 
 	maxWidth := 40
 	completions := c.list.GetItems()

+ 1 - 1
packages/tui/internal/components/dialog/init.go

@@ -94,7 +94,7 @@ func (m InitDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 // View implements tea.Model.
 func (m InitDialogCmp) View() string {
 	t := theme.CurrentTheme()
-	baseStyle := styles.BaseStyle()
+	baseStyle := styles.NewStyle().Foreground(t.Text())
 
 	// Calculate width needed for content
 	maxWidth := 60 // Width for explanation text

+ 1 - 1
packages/tui/internal/components/dialog/models.go

@@ -158,7 +158,7 @@ func (m *modelDialog) getScrollIndicators(maxWidth int) string {
 	}
 
 	t := theme.CurrentTheme()
-	return styles.BaseStyle().
+	return styles.NewStyle().
 		Foreground(t.TextMuted()).
 		Width(maxWidth).
 		Align(lipgloss.Right).

+ 2 - 3
packages/tui/internal/components/dialog/permission.go

@@ -145,7 +145,7 @@ func (p *permissionDialogComponent) selectCurrentOption() tea.Cmd {
 
 func (p *permissionDialogComponent) renderButtons() string {
 	t := theme.CurrentTheme()
-	baseStyle := styles.BaseStyle()
+	baseStyle := styles.NewStyle().Foreground(t.Text())
 
 	allowStyle := baseStyle
 	allowSessionStyle := baseStyle
@@ -355,8 +355,7 @@ func (p *permissionDialogComponent) renderDefaultContent() string {
 
 func (p *permissionDialogComponent) styleViewport() string {
 	t := theme.CurrentTheme()
-	contentStyle := lipgloss.NewStyle().
-		Background(t.Background())
+	contentStyle := styles.NewStyle().Background(t.Background())
 
 	return contentStyle.Render(p.contentViewPort.View())
 }

+ 5 - 6
packages/tui/internal/components/dialog/session.go

@@ -7,7 +7,6 @@ import (
 	"slices"
 
 	tea "github.com/charmbracelet/bubbletea/v2"
-	"github.com/charmbracelet/lipgloss/v2"
 	"github.com/muesli/reflow/truncate"
 	"github.com/sst/opencode/internal/app"
 	"github.com/sst/opencode/internal/components/list"
@@ -33,7 +32,7 @@ type sessionItem struct {
 
 func (s sessionItem) Render(selected bool, width int) string {
 	t := theme.CurrentTheme()
-	baseStyle := styles.BaseStyle()
+	baseStyle := styles.NewStyle()
 
 	var text string
 	if s.isDeleteConfirming {
@@ -44,7 +43,7 @@ func (s sessionItem) Render(selected bool, width int) string {
 
 	truncatedStr := truncate.StringWithTail(text, uint(width-1), "...")
 
-	var itemStyle lipgloss.Style
+	var itemStyle styles.Style
 	if selected {
 		if s.isDeleteConfirming {
 			// Red background for delete confirmation
@@ -151,9 +150,9 @@ func (s *sessionDialog) Render(background string) string {
 	listView := s.list.View()
 
 	t := theme.CurrentTheme()
-	helpStyle := styles.BaseStyle().PaddingLeft(1).PaddingTop(1)
-	helpText := styles.BaseStyle().Foreground(t.Text()).Render("x/del")
-	helpText = helpText + styles.BaseStyle().Background(t.BackgroundElement()).Foreground(t.TextMuted()).Render(" delete session")
+	helpStyle := styles.NewStyle().PaddingLeft(1).PaddingTop(1)
+	helpText := styles.NewStyle().Foreground(t.Text()).Render("x/del")
+	helpText = helpText + styles.NewStyle().Background(t.BackgroundElement()).Foreground(t.TextMuted()).Render(" delete session")
 	helpText = helpStyle.Render(helpText)
 
 	content := strings.Join([]string{listView, helpText}, "\n")

+ 1 - 1
packages/tui/internal/components/dialog/theme.go

@@ -103,7 +103,7 @@ func NewThemeDialog() ThemeDialog {
 
 	// Set the initial selection to the current theme
 	list.SetSelectedIndex(selectedIdx)
-	
+
 	// Set the max width for the list to match the modal width
 	list.SetMaxWidth(36) // 40 (modal max width) - 4 (modal padding)
 

+ 139 - 110
packages/tui/internal/components/diff/diff.go

@@ -441,84 +441,84 @@ func SyntaxHighlight(w io.Writer, source, fileName, formatter string, bg color.C
 	<entry type="TextWhitespace" style="%s"/>
 </style>
 `,
-		getColor(t.BackgroundPanel()), // Background
-		getColor(t.Text()),            // Text
-		getColor(t.Text()),            // Other
-		getColor(t.Error()),           // Error
-
-		getColor(t.SyntaxKeyword()), // Keyword
-		getColor(t.SyntaxKeyword()), // KeywordConstant
-		getColor(t.SyntaxKeyword()), // KeywordDeclaration
-		getColor(t.SyntaxKeyword()), // KeywordNamespace
-		getColor(t.SyntaxKeyword()), // KeywordPseudo
-		getColor(t.SyntaxKeyword()), // KeywordReserved
-		getColor(t.SyntaxType()),    // KeywordType
-
-		getColor(t.Text()),           // Name
-		getColor(t.SyntaxVariable()), // NameAttribute
-		getColor(t.SyntaxType()),     // NameBuiltin
-		getColor(t.SyntaxVariable()), // NameBuiltinPseudo
-		getColor(t.SyntaxType()),     // NameClass
-		getColor(t.SyntaxVariable()), // NameConstant
-		getColor(t.SyntaxFunction()), // NameDecorator
-		getColor(t.SyntaxVariable()), // NameEntity
-		getColor(t.SyntaxType()),     // NameException
-		getColor(t.SyntaxFunction()), // NameFunction
-		getColor(t.Text()),           // NameLabel
-		getColor(t.SyntaxType()),     // NameNamespace
-		getColor(t.SyntaxVariable()), // NameOther
-		getColor(t.SyntaxKeyword()),  // NameTag
-		getColor(t.SyntaxVariable()), // NameVariable
-		getColor(t.SyntaxVariable()), // NameVariableClass
-		getColor(t.SyntaxVariable()), // NameVariableGlobal
-		getColor(t.SyntaxVariable()), // NameVariableInstance
-
-		getColor(t.SyntaxString()), // Literal
-		getColor(t.SyntaxString()), // LiteralDate
-		getColor(t.SyntaxString()), // LiteralString
-		getColor(t.SyntaxString()), // LiteralStringBacktick
-		getColor(t.SyntaxString()), // LiteralStringChar
-		getColor(t.SyntaxString()), // LiteralStringDoc
-		getColor(t.SyntaxString()), // LiteralStringDouble
-		getColor(t.SyntaxString()), // LiteralStringEscape
-		getColor(t.SyntaxString()), // LiteralStringHeredoc
-		getColor(t.SyntaxString()), // LiteralStringInterpol
-		getColor(t.SyntaxString()), // LiteralStringOther
-		getColor(t.SyntaxString()), // LiteralStringRegex
-		getColor(t.SyntaxString()), // LiteralStringSingle
-		getColor(t.SyntaxString()), // LiteralStringSymbol
-
-		getColor(t.SyntaxNumber()), // LiteralNumber
-		getColor(t.SyntaxNumber()), // LiteralNumberBin
-		getColor(t.SyntaxNumber()), // LiteralNumberFloat
-		getColor(t.SyntaxNumber()), // LiteralNumberHex
-		getColor(t.SyntaxNumber()), // LiteralNumberInteger
-		getColor(t.SyntaxNumber()), // LiteralNumberIntegerLong
-		getColor(t.SyntaxNumber()), // LiteralNumberOct
-
-		getColor(t.SyntaxOperator()),    // Operator
-		getColor(t.SyntaxKeyword()),     // OperatorWord
-		getColor(t.SyntaxPunctuation()), // Punctuation
-
-		getColor(t.SyntaxComment()), // Comment
-		getColor(t.SyntaxComment()), // CommentHashbang
-		getColor(t.SyntaxComment()), // CommentMultiline
-		getColor(t.SyntaxComment()), // CommentSingle
-		getColor(t.SyntaxComment()), // CommentSpecial
-		getColor(t.SyntaxKeyword()), // CommentPreproc
-
-		getColor(t.Text()),      // Generic
-		getColor(t.Error()),     // GenericDeleted
-		getColor(t.Text()),      // GenericEmph
-		getColor(t.Error()),     // GenericError
-		getColor(t.Text()),      // GenericHeading
-		getColor(t.Success()),   // GenericInserted
-		getColor(t.TextMuted()), // GenericOutput
-		getColor(t.Text()),      // GenericPrompt
-		getColor(t.Text()),      // GenericStrong
-		getColor(t.Text()),      // GenericSubheading
-		getColor(t.Error()),     // GenericTraceback
-		getColor(t.Text()),      // TextWhitespace
+		getChromaColor(t.BackgroundPanel()), // Background
+		getChromaColor(t.Text()),            // Text
+		getChromaColor(t.Text()),            // Other
+		getChromaColor(t.Error()),           // Error
+
+		getChromaColor(t.SyntaxKeyword()), // Keyword
+		getChromaColor(t.SyntaxKeyword()), // KeywordConstant
+		getChromaColor(t.SyntaxKeyword()), // KeywordDeclaration
+		getChromaColor(t.SyntaxKeyword()), // KeywordNamespace
+		getChromaColor(t.SyntaxKeyword()), // KeywordPseudo
+		getChromaColor(t.SyntaxKeyword()), // KeywordReserved
+		getChromaColor(t.SyntaxType()),    // KeywordType
+
+		getChromaColor(t.Text()),           // Name
+		getChromaColor(t.SyntaxVariable()), // NameAttribute
+		getChromaColor(t.SyntaxType()),     // NameBuiltin
+		getChromaColor(t.SyntaxVariable()), // NameBuiltinPseudo
+		getChromaColor(t.SyntaxType()),     // NameClass
+		getChromaColor(t.SyntaxVariable()), // NameConstant
+		getChromaColor(t.SyntaxFunction()), // NameDecorator
+		getChromaColor(t.SyntaxVariable()), // NameEntity
+		getChromaColor(t.SyntaxType()),     // NameException
+		getChromaColor(t.SyntaxFunction()), // NameFunction
+		getChromaColor(t.Text()),           // NameLabel
+		getChromaColor(t.SyntaxType()),     // NameNamespace
+		getChromaColor(t.SyntaxVariable()), // NameOther
+		getChromaColor(t.SyntaxKeyword()),  // NameTag
+		getChromaColor(t.SyntaxVariable()), // NameVariable
+		getChromaColor(t.SyntaxVariable()), // NameVariableClass
+		getChromaColor(t.SyntaxVariable()), // NameVariableGlobal
+		getChromaColor(t.SyntaxVariable()), // NameVariableInstance
+
+		getChromaColor(t.SyntaxString()), // Literal
+		getChromaColor(t.SyntaxString()), // LiteralDate
+		getChromaColor(t.SyntaxString()), // LiteralString
+		getChromaColor(t.SyntaxString()), // LiteralStringBacktick
+		getChromaColor(t.SyntaxString()), // LiteralStringChar
+		getChromaColor(t.SyntaxString()), // LiteralStringDoc
+		getChromaColor(t.SyntaxString()), // LiteralStringDouble
+		getChromaColor(t.SyntaxString()), // LiteralStringEscape
+		getChromaColor(t.SyntaxString()), // LiteralStringHeredoc
+		getChromaColor(t.SyntaxString()), // LiteralStringInterpol
+		getChromaColor(t.SyntaxString()), // LiteralStringOther
+		getChromaColor(t.SyntaxString()), // LiteralStringRegex
+		getChromaColor(t.SyntaxString()), // LiteralStringSingle
+		getChromaColor(t.SyntaxString()), // LiteralStringSymbol
+
+		getChromaColor(t.SyntaxNumber()), // LiteralNumber
+		getChromaColor(t.SyntaxNumber()), // LiteralNumberBin
+		getChromaColor(t.SyntaxNumber()), // LiteralNumberFloat
+		getChromaColor(t.SyntaxNumber()), // LiteralNumberHex
+		getChromaColor(t.SyntaxNumber()), // LiteralNumberInteger
+		getChromaColor(t.SyntaxNumber()), // LiteralNumberIntegerLong
+		getChromaColor(t.SyntaxNumber()), // LiteralNumberOct
+
+		getChromaColor(t.SyntaxOperator()),    // Operator
+		getChromaColor(t.SyntaxKeyword()),     // OperatorWord
+		getChromaColor(t.SyntaxPunctuation()), // Punctuation
+
+		getChromaColor(t.SyntaxComment()), // Comment
+		getChromaColor(t.SyntaxComment()), // CommentHashbang
+		getChromaColor(t.SyntaxComment()), // CommentMultiline
+		getChromaColor(t.SyntaxComment()), // CommentSingle
+		getChromaColor(t.SyntaxComment()), // CommentSpecial
+		getChromaColor(t.SyntaxKeyword()), // CommentPreproc
+
+		getChromaColor(t.Text()),      // Generic
+		getChromaColor(t.Error()),     // GenericDeleted
+		getChromaColor(t.Text()),      // GenericEmph
+		getChromaColor(t.Error()),     // GenericError
+		getChromaColor(t.Text()),      // GenericHeading
+		getChromaColor(t.Success()),   // GenericInserted
+		getChromaColor(t.TextMuted()), // GenericOutput
+		getChromaColor(t.Text()),      // GenericPrompt
+		getChromaColor(t.Text()),      // GenericStrong
+		getChromaColor(t.Text()),      // GenericSubheading
+		getChromaColor(t.Error()),     // GenericTraceback
+		getChromaColor(t.Text()),      // TextWhitespace
 	)
 
 	r := strings.NewReader(syntaxThemeXml)
@@ -527,6 +527,9 @@ func SyntaxHighlight(w io.Writer, source, fileName, formatter string, bg color.C
 	// Modify the style to use the provided background
 	s, err := style.Builder().Transform(
 		func(t chroma.StyleEntry) chroma.StyleEntry {
+			if _, ok := bg.(lipgloss.NoColor); ok {
+				return t
+			}
 			r, g, b, _ := bg.RGBA()
 			t.Background = chroma.NewColour(uint8(r>>8), uint8(g>>8), uint8(b>>8))
 			return t
@@ -546,10 +549,18 @@ func SyntaxHighlight(w io.Writer, source, fileName, formatter string, bg color.C
 }
 
 // getColor returns the appropriate hex color string based on terminal background
-func getColor(adaptiveColor compat.AdaptiveColor) string {
+func getColor(adaptiveColor compat.AdaptiveColor) *string {
 	return stylesi.AdaptiveColorToString(adaptiveColor)
 }
 
+func getChromaColor(adaptiveColor compat.AdaptiveColor) string {
+	color := stylesi.AdaptiveColorToString(adaptiveColor)
+	if color == nil {
+		return ""
+	}
+	return *color
+}
+
 // highlightLine applies syntax highlighting to a single line
 func highlightLine(fileName string, line string, bg color.Color) string {
 	var buf bytes.Buffer
@@ -561,11 +572,11 @@ func highlightLine(fileName string, line string, bg color.Color) string {
 }
 
 // createStyles generates the lipgloss styles needed for rendering diffs
-func createStyles(t theme.Theme) (removedLineStyle, addedLineStyle, contextLineStyle, lineNumberStyle lipgloss.Style) {
-	removedLineStyle = lipgloss.NewStyle().Background(t.DiffRemovedBg())
-	addedLineStyle = lipgloss.NewStyle().Background(t.DiffAddedBg())
-	contextLineStyle = lipgloss.NewStyle().Background(t.DiffContextBg())
-	lineNumberStyle = lipgloss.NewStyle().Background(t.DiffLineNumber()).Foreground(t.TextMuted())
+func createStyles(t theme.Theme) (removedLineStyle, addedLineStyle, contextLineStyle, lineNumberStyle stylesi.Style) {
+	removedLineStyle = stylesi.NewStyle().Background(t.DiffRemovedBg())
+	addedLineStyle = stylesi.NewStyle().Background(t.DiffAddedBg())
+	contextLineStyle = stylesi.NewStyle().Background(t.DiffContextBg())
+	lineNumberStyle = stylesi.NewStyle().Foreground(t.TextMuted()).Background(t.DiffLineNumber())
 	return
 }
 
@@ -613,9 +624,17 @@ func applyHighlighting(content string, segments []Segment, segmentType LineType,
 	currentPos := 0
 
 	// Get the appropriate color based on terminal background
-	bgColor := lipgloss.Color(getColor(highlightBg))
-	fgColor := lipgloss.Color(getColor(theme.CurrentTheme().BackgroundPanel()))
+	bg := getColor(highlightBg)
+	fg := getColor(theme.CurrentTheme().BackgroundPanel())
+	var bgColor color.Color
+	var fgColor color.Color
 
+	if bg != nil {
+		bgColor = lipgloss.Color(*bg)
+	}
+	if fg != nil {
+		fgColor = lipgloss.Color(*fg)
+	}
 	for i := 0; i < len(content); {
 		// Check if we're at an ANSI sequence
 		isAnsi := false
@@ -651,12 +670,20 @@ func applyHighlighting(content string, segments []Segment, segmentType LineType,
 			currentStyle := ansiSequences[currentPos]
 
 			// Apply foreground and background highlight
-			sb.WriteString("\x1b[38;2;")
-			r, g, b, _ := fgColor.RGBA()
-			sb.WriteString(fmt.Sprintf("%d;%d;%dm", r>>8, g>>8, b>>8))
-			sb.WriteString("\x1b[48;2;")
-			r, g, b, _ = bgColor.RGBA()
-			sb.WriteString(fmt.Sprintf("%d;%d;%dm", r>>8, g>>8, b>>8))
+			if fgColor != nil {
+				sb.WriteString("\x1b[38;2;")
+				r, g, b, _ := fgColor.RGBA()
+				sb.WriteString(fmt.Sprintf("%d;%d;%dm", r>>8, g>>8, b>>8))
+			} else {
+				sb.WriteString("\x1b[49m")
+			}
+			if bgColor != nil {
+				sb.WriteString("\x1b[48;2;")
+				r, g, b, _ := bgColor.RGBA()
+				sb.WriteString(fmt.Sprintf("%d;%d;%dm", r>>8, g>>8, b>>8))
+			} else {
+				sb.WriteString("\x1b[39m")
+			}
 			sb.WriteString(char)
 
 			// Full reset of all attributes to ensure clean state
@@ -677,16 +704,16 @@ func applyHighlighting(content string, segments []Segment, segmentType LineType,
 }
 
 // renderLinePrefix renders the line number and marker prefix for a diff line
-func renderLinePrefix(dl DiffLine, lineNum string, marker string, lineNumberStyle lipgloss.Style, t theme.Theme) string {
+func renderLinePrefix(dl DiffLine, lineNum string, marker string, lineNumberStyle stylesi.Style, t theme.Theme) string {
 	// Style the marker based on line type
 	var styledMarker string
 	switch dl.Kind {
 	case LineRemoved:
-		styledMarker = lipgloss.NewStyle().Background(t.DiffRemovedBg()).Foreground(t.DiffRemoved()).Render(marker)
+		styledMarker = stylesi.NewStyle().Foreground(t.DiffRemoved()).Background(t.DiffRemovedBg()).Render(marker)
 	case LineAdded:
-		styledMarker = lipgloss.NewStyle().Background(t.DiffAddedBg()).Foreground(t.DiffAdded()).Render(marker)
+		styledMarker = stylesi.NewStyle().Foreground(t.DiffAdded()).Background(t.DiffAddedBg()).Render(marker)
 	case LineContext:
-		styledMarker = lipgloss.NewStyle().Background(t.DiffContextBg()).Foreground(t.TextMuted()).Render(marker)
+		styledMarker = stylesi.NewStyle().Foreground(t.TextMuted()).Background(t.DiffContextBg()).Render(marker)
 	default:
 		styledMarker = marker
 	}
@@ -695,7 +722,7 @@ func renderLinePrefix(dl DiffLine, lineNum string, marker string, lineNumberStyl
 }
 
 // renderLineContent renders the content of a diff line with syntax and intra-line highlighting
-func renderLineContent(fileName string, dl DiffLine, bgStyle lipgloss.Style, highlightColor compat.AdaptiveColor, width int, t theme.Theme) string {
+func renderLineContent(fileName string, dl DiffLine, bgStyle stylesi.Style, highlightColor compat.AdaptiveColor, width int) string {
 	// Apply syntax highlighting
 	content := highlightLine(fileName, dl.Content, bgStyle.GetBackground())
 
@@ -714,7 +741,9 @@ func renderLineContent(fileName string, dl DiffLine, bgStyle lipgloss.Style, hig
 		ansi.Truncate(
 			content,
 			width,
-			lipgloss.NewStyle().Background(bgStyle.GetBackground()).Foreground(t.TextMuted()).Render("..."),
+			"...",
+			// stylesi.NewStyleWithColors(t.TextMuted(), bgStyle.GetBackground()).Render("..."),
+			// stylesi.WithForeground(stylesi.NewStyle().Background(bgStyle.GetBackground()), t.TextMuted()).Render("..."),
 		),
 	)
 }
@@ -725,7 +754,7 @@ func renderUnifiedLine(fileName string, dl DiffLine, width int, t theme.Theme) s
 
 	// Determine line style and marker based on line type
 	var marker string
-	var bgStyle lipgloss.Style
+	var bgStyle stylesi.Style
 	var lineNum string
 	var highlightColor compat.AdaptiveColor
 
@@ -733,8 +762,8 @@ func renderUnifiedLine(fileName string, dl DiffLine, width int, t theme.Theme) s
 	case LineRemoved:
 		marker = "-"
 		bgStyle = removedLineStyle
-		lineNumberStyle = lineNumberStyle.Foreground(t.DiffRemoved()).Background(t.DiffRemovedLineNumberBg())
-		highlightColor = t.DiffHighlightRemoved()
+		lineNumberStyle = lineNumberStyle.Background(t.DiffRemovedLineNumberBg()).Foreground(t.DiffRemoved())
+		highlightColor = t.DiffHighlightRemoved() // TODO: handle "none"
 		if dl.OldLineNo > 0 {
 			lineNum = fmt.Sprintf("%6d       ", dl.OldLineNo)
 		} else {
@@ -743,8 +772,8 @@ func renderUnifiedLine(fileName string, dl DiffLine, width int, t theme.Theme) s
 	case LineAdded:
 		marker = "+"
 		bgStyle = addedLineStyle
-		lineNumberStyle = lineNumberStyle.Foreground(t.DiffAdded()).Background(t.DiffAddedLineNumberBg())
-		highlightColor = t.DiffHighlightAdded()
+		lineNumberStyle = lineNumberStyle.Background(t.DiffAddedLineNumberBg()).Foreground(t.DiffAdded())
+		highlightColor = t.DiffHighlightAdded() // TODO: handle "none"
 		if dl.NewLineNo > 0 {
 			lineNum = fmt.Sprintf("      %7d", dl.NewLineNo)
 		} else {
@@ -766,7 +795,7 @@ func renderUnifiedLine(fileName string, dl DiffLine, width int, t theme.Theme) s
 	// Render the content
 	prefixWidth := ansi.StringWidth(prefix)
 	contentWidth := width - prefixWidth
-	content := renderLineContent(fileName, dl, bgStyle, highlightColor, contentWidth, t)
+	content := renderLineContent(fileName, dl, bgStyle, highlightColor, contentWidth)
 
 	return prefix + content
 }
@@ -780,7 +809,7 @@ func renderDiffColumnLine(
 	t theme.Theme,
 ) string {
 	if dl == nil {
-		contextLineStyle := lipgloss.NewStyle().Background(t.DiffContextBg())
+		contextLineStyle := stylesi.NewStyle().Background(t.DiffContextBg())
 		return contextLineStyle.Width(colWidth).Render("")
 	}
 
@@ -788,7 +817,7 @@ func renderDiffColumnLine(
 
 	// Determine line style based on line type and column
 	var marker string
-	var bgStyle lipgloss.Style
+	var bgStyle stylesi.Style
 	var lineNum string
 	var highlightColor compat.AdaptiveColor
 
@@ -798,8 +827,8 @@ func renderDiffColumnLine(
 		case LineRemoved:
 			marker = "-"
 			bgStyle = removedLineStyle
-			lineNumberStyle = lineNumberStyle.Foreground(t.DiffRemoved()).Background(t.DiffRemovedLineNumberBg())
-			highlightColor = t.DiffHighlightRemoved()
+			lineNumberStyle = lineNumberStyle.Background(t.DiffRemovedLineNumberBg()).Foreground(t.DiffRemoved())
+			highlightColor = t.DiffHighlightRemoved() // TODO: handle "none"
 		case LineAdded:
 			marker = "?"
 			bgStyle = contextLineStyle
@@ -818,7 +847,7 @@ func renderDiffColumnLine(
 		case LineAdded:
 			marker = "+"
 			bgStyle = addedLineStyle
-			lineNumberStyle = lineNumberStyle.Foreground(t.DiffAdded()).Background(t.DiffAddedLineNumberBg())
+			lineNumberStyle = lineNumberStyle.Background(t.DiffAddedLineNumberBg()).Foreground(t.DiffAdded())
 			highlightColor = t.DiffHighlightAdded()
 		case LineRemoved:
 			marker = "?"
@@ -849,7 +878,7 @@ func renderDiffColumnLine(
 	// Render the content
 	prefixWidth := ansi.StringWidth(prefix)
 	contentWidth := colWidth - prefixWidth
-	content := renderLineContent(fileName, *dl, bgStyle, highlightColor, contentWidth, t)
+	content := renderLineContent(fileName, *dl, bgStyle, highlightColor, contentWidth)
 
 	return prefix + content
 }

+ 3 - 3
packages/tui/internal/components/list/list.go

@@ -5,7 +5,6 @@ import (
 
 	"github.com/charmbracelet/bubbles/v2/key"
 	tea "github.com/charmbracelet/bubbletea/v2"
-	"github.com/charmbracelet/lipgloss/v2"
 	"github.com/muesli/reflow/truncate"
 	"github.com/sst/opencode/internal/styles"
 	"github.com/sst/opencode/internal/theme"
@@ -174,11 +173,11 @@ type StringItem string
 
 func (s StringItem) Render(selected bool, width int) string {
 	t := theme.CurrentTheme()
-	baseStyle := styles.BaseStyle()
+	baseStyle := styles.NewStyle()
 
 	truncatedStr := truncate.StringWithTail(string(s), uint(width-1), "...")
 
-	var itemStyle lipgloss.Style
+	var itemStyle styles.Style
 	if selected {
 		itemStyle = baseStyle.
 			Background(t.Primary()).
@@ -187,6 +186,7 @@ func (s StringItem) Render(selected bool, width int) string {
 			PaddingLeft(1)
 	} else {
 		itemStyle = baseStyle.
+			Foreground(t.TextMuted()).
 			PaddingLeft(1)
 	}
 

+ 1 - 5
packages/tui/internal/components/modal/modal.go

@@ -90,12 +90,8 @@ func (m *Modal) Render(contentView string, background string) string {
 
 	innerWidth := outerWidth - 4
 
-	// Base style for the modal
-	baseStyle := styles.BaseStyle().
-		Background(t.BackgroundElement()).
-		Foreground(t.TextMuted())
+	baseStyle := styles.NewStyle().Foreground(t.TextMuted()).Background(t.BackgroundElement())
 
-	// Add title if provided
 	var finalContent string
 	if m.title != "" {
 		titleStyle := baseStyle.

+ 2 - 4
packages/tui/internal/components/qr/qr.go

@@ -3,7 +3,7 @@ package qr
 import (
 	"strings"
 
-	"github.com/charmbracelet/lipgloss/v2"
+	"github.com/sst/opencode/internal/styles"
 	"github.com/sst/opencode/internal/theme"
 	"rsc.io/qr"
 )
@@ -23,9 +23,7 @@ func Generate(text string) (string, int, error) {
 	}
 
 	// Create lipgloss style for QR code with theme colors
-	qrStyle := lipgloss.NewStyle().
-		Foreground(t.Text()).
-		Background(t.Background())
+	qrStyle := styles.NewStyleWithColors(t.Text(), t.Background())
 
 	var result strings.Builder
 

+ 12 - 9
packages/tui/internal/components/status/status.go

@@ -36,14 +36,15 @@ func (m statusComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 
 func (m statusComponent) logo() string {
 	t := theme.CurrentTheme()
-	base := lipgloss.NewStyle().Background(t.BackgroundElement()).Foreground(t.TextMuted()).Render
-	emphasis := lipgloss.NewStyle().Bold(true).Background(t.BackgroundElement()).Foreground(t.Text()).Render
+	base := styles.NewStyle().Foreground(t.TextMuted()).Background(t.BackgroundElement()).Render
+	emphasis := styles.NewStyle().Foreground(t.Text()).Background(t.BackgroundElement()).Bold(true).Render
 
 	open := base("open")
 	code := emphasis("code ")
 	version := base(m.app.Version)
-	return styles.Padded().
+	return styles.NewStyle().
 		Background(t.BackgroundElement()).
+		Padding(0, 1).
 		Render(open + code + version)
 }
 
@@ -77,7 +78,7 @@ func formatTokensAndCost(tokens float32, contextWindow float32, cost float32) st
 func (m statusComponent) View() string {
 	t := theme.CurrentTheme()
 	if m.app.Session.Id == "" {
-		return styles.BaseStyle().
+		return styles.NewStyle().
 			Background(t.Background()).
 			Width(m.width).
 			Height(2).
@@ -86,9 +87,10 @@ func (m statusComponent) View() string {
 
 	logo := m.logo()
 
-	cwd := styles.Padded().
+	cwd := styles.NewStyle().
 		Foreground(t.TextMuted()).
 		Background(t.BackgroundPanel()).
+		Padding(0, 1).
 		Render(m.app.Info.Path.Cwd)
 
 	sessionInfo := ""
@@ -111,9 +113,10 @@ func (m statusComponent) View() string {
 			}
 		}
 
-		sessionInfo = styles.Padded().
-			Background(t.BackgroundElement()).
+		sessionInfo = styles.NewStyle().
 			Foreground(t.TextMuted()).
+			Background(t.BackgroundElement()).
+			Padding(0, 1).
 			Render(formatTokensAndCost(tokens, contextWindow, cost))
 	}
 
@@ -123,11 +126,11 @@ func (m statusComponent) View() string {
 		0,
 		m.width-lipgloss.Width(logo)-lipgloss.Width(cwd)-lipgloss.Width(sessionInfo),
 	)
-	spacer := lipgloss.NewStyle().Background(t.BackgroundPanel()).Width(space).Render("")
+	spacer := styles.NewStyle().Background(t.BackgroundPanel()).Width(space).Render("")
 
 	status := logo + cwd + spacer + sessionInfo
 
-	blank := styles.BaseStyle().Background(t.Background()).Width(m.width).Render("")
+	blank := styles.NewStyle().Background(t.Background()).Width(m.width).Render("")
 	return blank + "\n" + status
 }
 

+ 4 - 5
packages/tui/internal/components/toast/toast.go

@@ -90,9 +90,9 @@ func (tm *ToastManager) Update(msg tea.Msg) (*ToastManager, tea.Cmd) {
 func (tm *ToastManager) renderSingleToast(toast Toast) string {
 	t := theme.CurrentTheme()
 
-	baseStyle := styles.BaseStyle().
-		Background(t.BackgroundElement()).
+	baseStyle := styles.NewStyle().
 		Foreground(t.Text()).
+		Background(t.BackgroundElement()).
 		Padding(1, 2)
 
 	maxWidth := max(40, layout.Current.Viewport.Width/3)
@@ -101,15 +101,14 @@ func (tm *ToastManager) renderSingleToast(toast Toast) string {
 	// Build content with wrapping
 	var content strings.Builder
 	if toast.Title != nil {
-		titleStyle := lipgloss.NewStyle().
-			Foreground(toast.Color).
+		titleStyle := styles.NewStyle().Foreground(toast.Color).
 			Bold(true)
 		content.WriteString(titleStyle.Render(*toast.Title))
 		content.WriteString("\n")
 	}
 
 	// Wrap message text
-	messageStyle := lipgloss.NewStyle()
+	messageStyle := styles.NewStyle()
 	contentWidth := lipgloss.Width(toast.Message)
 	if contentWidth > contentMaxWidth {
 		messageStyle = messageStyle.Width(contentMaxWidth)

+ 1 - 1
packages/tui/internal/config/config.go

@@ -18,7 +18,7 @@ type State struct {
 
 func NewState() *State {
 	return &State{
-		Theme: "opencode",
+		Theme: "system",
 	}
 }
 

+ 2 - 3
packages/tui/internal/layout/container.go

@@ -3,6 +3,7 @@ package layout
 import (
 	tea "github.com/charmbracelet/bubbletea/v2"
 	"github.com/charmbracelet/lipgloss/v2"
+	"github.com/sst/opencode/internal/styles"
 	"github.com/sst/opencode/internal/theme"
 )
 
@@ -57,7 +58,7 @@ func (c *container) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 
 func (c *container) View() string {
 	t := theme.CurrentTheme()
-	style := lipgloss.NewStyle()
+	style := styles.NewStyle().Background(t.Background())
 	width := c.width
 	height := c.height
 
@@ -66,8 +67,6 @@ func (c *container) View() string {
 		width = c.maxWidth
 	}
 
-	style = style.Background(t.Background())
-
 	// Apply border if any side is enabled
 	if c.borderTop || c.borderRight || c.borderBottom || c.borderLeft {
 		// Adjust width and height for borders

+ 3 - 2
packages/tui/internal/layout/flex.go

@@ -3,6 +3,7 @@ package layout
 import (
 	tea "github.com/charmbracelet/bubbletea/v2"
 	"github.com/charmbracelet/lipgloss/v2"
+	"github.com/sst/opencode/internal/styles"
 	"github.com/sst/opencode/internal/theme"
 )
 
@@ -66,7 +67,7 @@ func (f *flexLayout) View() string {
 				alignment,
 				child.View(),
 				// TODO: make configurable WithBackgroundStyle
-				lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
+				lipgloss.WithWhitespaceStyle(styles.NewStyle().Background(t.Background()).Lipgloss()),
 			)
 			views = append(views, view)
 		} else {
@@ -78,7 +79,7 @@ func (f *flexLayout) View() string {
 				alignment,
 				child.View(),
 				// TODO: make configurable WithBackgroundStyle
-				lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
+				lipgloss.WithWhitespaceStyle(styles.NewStyle().Background(t.Background()).Lipgloss()),
 			)
 			views = append(views, view)
 		}

+ 4 - 0
packages/tui/internal/styles/background.go

@@ -1,6 +1,9 @@
 package styles
 
+import "image/color"
+
 type TerminalInfo struct {
+	Background       color.Color
 	BackgroundIsDark bool
 }
 
@@ -8,6 +11,7 @@ var Terminal *TerminalInfo
 
 func init() {
 	Terminal = &TerminalInfo{
+		Background:       color.Black,
 		BackgroundIsDark: true,
 	}
 }

+ 62 - 55
packages/tui/internal/styles/markdown.go

@@ -3,6 +3,7 @@ package styles
 import (
 	"github.com/charmbracelet/glamour"
 	"github.com/charmbracelet/glamour/ansi"
+	"github.com/charmbracelet/lipgloss/v2"
 	"github.com/charmbracelet/lipgloss/v2/compat"
 	"github.com/lucasb-eyer/go-colorful"
 	"github.com/sst/opencode/internal/theme"
@@ -29,7 +30,7 @@ func GetMarkdownRenderer(width int, backgroundColor compat.AdaptiveColor) *glamo
 // using adaptive colors from the provided theme.
 func generateMarkdownStyleConfig(backgroundColor compat.AdaptiveColor) ansi.StyleConfig {
 	t := theme.CurrentTheme()
-	background := stringPtr(AdaptiveColorToString(backgroundColor))
+	background := AdaptiveColorToString(backgroundColor)
 
 	return ansi.StyleConfig{
 		Document: ansi.StyleBlock{
@@ -37,12 +38,12 @@ func generateMarkdownStyleConfig(backgroundColor compat.AdaptiveColor) ansi.Styl
 				BlockPrefix:     "",
 				BlockSuffix:     "",
 				BackgroundColor: background,
-				Color:           stringPtr(AdaptiveColorToString(t.MarkdownText())),
+				Color:           AdaptiveColorToString(t.MarkdownText()),
 			},
 		},
 		BlockQuote: ansi.StyleBlock{
 			StylePrimitive: ansi.StylePrimitive{
-				Color:  stringPtr(AdaptiveColorToString(t.MarkdownBlockQuote())),
+				Color:  AdaptiveColorToString(t.MarkdownBlockQuote()),
 				Italic: boolPtr(true),
 				Prefix: "┃ ",
 			},
@@ -54,108 +55,108 @@ func generateMarkdownStyleConfig(backgroundColor compat.AdaptiveColor) ansi.Styl
 			StyleBlock: ansi.StyleBlock{
 				IndentToken: stringPtr(" "),
 				StylePrimitive: ansi.StylePrimitive{
-					Color: stringPtr(AdaptiveColorToString(t.MarkdownText())),
+					Color: AdaptiveColorToString(t.MarkdownText()),
 				},
 			},
 		},
 		Heading: ansi.StyleBlock{
 			StylePrimitive: ansi.StylePrimitive{
 				BlockSuffix: "\n",
-				Color:       stringPtr(AdaptiveColorToString(t.MarkdownHeading())),
+				Color:       AdaptiveColorToString(t.MarkdownHeading()),
 				Bold:        boolPtr(true),
 			},
 		},
 		H1: ansi.StyleBlock{
 			StylePrimitive: ansi.StylePrimitive{
 				Prefix: "# ",
-				Color:  stringPtr(AdaptiveColorToString(t.MarkdownHeading())),
+				Color:  AdaptiveColorToString(t.MarkdownHeading()),
 				Bold:   boolPtr(true),
 			},
 		},
 		H2: ansi.StyleBlock{
 			StylePrimitive: ansi.StylePrimitive{
 				Prefix: "## ",
-				Color:  stringPtr(AdaptiveColorToString(t.MarkdownHeading())),
+				Color:  AdaptiveColorToString(t.MarkdownHeading()),
 				Bold:   boolPtr(true),
 			},
 		},
 		H3: ansi.StyleBlock{
 			StylePrimitive: ansi.StylePrimitive{
 				Prefix: "### ",
-				Color:  stringPtr(AdaptiveColorToString(t.MarkdownHeading())),
+				Color:  AdaptiveColorToString(t.MarkdownHeading()),
 				Bold:   boolPtr(true),
 			},
 		},
 		H4: ansi.StyleBlock{
 			StylePrimitive: ansi.StylePrimitive{
 				Prefix: "#### ",
-				Color:  stringPtr(AdaptiveColorToString(t.MarkdownHeading())),
+				Color:  AdaptiveColorToString(t.MarkdownHeading()),
 				Bold:   boolPtr(true),
 			},
 		},
 		H5: ansi.StyleBlock{
 			StylePrimitive: ansi.StylePrimitive{
 				Prefix: "##### ",
-				Color:  stringPtr(AdaptiveColorToString(t.MarkdownHeading())),
+				Color:  AdaptiveColorToString(t.MarkdownHeading()),
 				Bold:   boolPtr(true),
 			},
 		},
 		H6: ansi.StyleBlock{
 			StylePrimitive: ansi.StylePrimitive{
 				Prefix: "###### ",
-				Color:  stringPtr(AdaptiveColorToString(t.MarkdownHeading())),
+				Color:  AdaptiveColorToString(t.MarkdownHeading()),
 				Bold:   boolPtr(true),
 			},
 		},
 		Strikethrough: ansi.StylePrimitive{
 			CrossedOut: boolPtr(true),
-			Color:      stringPtr(AdaptiveColorToString(t.TextMuted())),
+			Color:      AdaptiveColorToString(t.TextMuted()),
 		},
 		Emph: ansi.StylePrimitive{
-			Color:  stringPtr(AdaptiveColorToString(t.MarkdownEmph())),
+			Color:  AdaptiveColorToString(t.MarkdownEmph()),
 			Italic: boolPtr(true),
 		},
 		Strong: ansi.StylePrimitive{
 			Bold:  boolPtr(true),
-			Color: stringPtr(AdaptiveColorToString(t.MarkdownStrong())),
+			Color: AdaptiveColorToString(t.MarkdownStrong()),
 		},
 		HorizontalRule: ansi.StylePrimitive{
-			Color:  stringPtr(AdaptiveColorToString(t.MarkdownHorizontalRule())),
+			Color:  AdaptiveColorToString(t.MarkdownHorizontalRule()),
 			Format: "\n─────────────────────────────────────────\n",
 		},
 		Item: ansi.StylePrimitive{
 			BlockPrefix: "• ",
-			Color:       stringPtr(AdaptiveColorToString(t.MarkdownListItem())),
+			Color:       AdaptiveColorToString(t.MarkdownListItem()),
 		},
 		Enumeration: ansi.StylePrimitive{
 			BlockPrefix: ". ",
-			Color:       stringPtr(AdaptiveColorToString(t.MarkdownListEnumeration())),
+			Color:       AdaptiveColorToString(t.MarkdownListEnumeration()),
 		},
 		Task: ansi.StyleTask{
 			Ticked:   "[✓] ",
 			Unticked: "[ ] ",
 		},
 		Link: ansi.StylePrimitive{
-			Color:     stringPtr(AdaptiveColorToString(t.MarkdownLink())),
+			Color:     AdaptiveColorToString(t.MarkdownLink()),
 			Underline: boolPtr(true),
 		},
 		LinkText: ansi.StylePrimitive{
-			Color: stringPtr(AdaptiveColorToString(t.MarkdownLinkText())),
+			Color: AdaptiveColorToString(t.MarkdownLinkText()),
 			Bold:  boolPtr(true),
 		},
 		Image: ansi.StylePrimitive{
-			Color:     stringPtr(AdaptiveColorToString(t.MarkdownImage())),
+			Color:     AdaptiveColorToString(t.MarkdownImage()),
 			Underline: boolPtr(true),
 			Format:    "🖼 {{.text}}",
 		},
 		ImageText: ansi.StylePrimitive{
-			Color:  stringPtr(AdaptiveColorToString(t.MarkdownImageText())),
+			Color:  AdaptiveColorToString(t.MarkdownImageText()),
 			Format: "{{.text}}",
 		},
 		Code: ansi.StyleBlock{
 			StylePrimitive: ansi.StylePrimitive{
 				BackgroundColor: background,
-				Color:           stringPtr(AdaptiveColorToString(t.MarkdownCode())),
+				Color:           AdaptiveColorToString(t.MarkdownCode()),
 				Prefix:          "",
 				Suffix:          "",
 			},
@@ -165,7 +166,7 @@ func generateMarkdownStyleConfig(backgroundColor compat.AdaptiveColor) ansi.Styl
 				StylePrimitive: ansi.StylePrimitive{
 					BackgroundColor: background,
 					Prefix:          " ",
-					Color:           stringPtr(AdaptiveColorToString(t.MarkdownCodeBlock())),
+					Color:           AdaptiveColorToString(t.MarkdownCodeBlock()),
 				},
 			},
 			Chroma: &ansi.Chroma{
@@ -174,109 +175,109 @@ func generateMarkdownStyleConfig(backgroundColor compat.AdaptiveColor) ansi.Styl
 				},
 				Text: ansi.StylePrimitive{
 					BackgroundColor: background,
-					Color:           stringPtr(AdaptiveColorToString(t.MarkdownText())),
+					Color:           AdaptiveColorToString(t.MarkdownText()),
 				},
 				Error: ansi.StylePrimitive{
 					BackgroundColor: background,
-					Color:           stringPtr(AdaptiveColorToString(t.Error())),
+					Color:           AdaptiveColorToString(t.Error()),
 				},
 				Comment: ansi.StylePrimitive{
 					BackgroundColor: background,
-					Color:           stringPtr(AdaptiveColorToString(t.SyntaxComment())),
+					Color:           AdaptiveColorToString(t.SyntaxComment()),
 				},
 				CommentPreproc: ansi.StylePrimitive{
 					BackgroundColor: background,
-					Color:           stringPtr(AdaptiveColorToString(t.SyntaxKeyword())),
+					Color:           AdaptiveColorToString(t.SyntaxKeyword()),
 				},
 				Keyword: ansi.StylePrimitive{
 					BackgroundColor: background,
-					Color:           stringPtr(AdaptiveColorToString(t.SyntaxKeyword())),
+					Color:           AdaptiveColorToString(t.SyntaxKeyword()),
 				},
 				KeywordReserved: ansi.StylePrimitive{
 					BackgroundColor: background,
-					Color:           stringPtr(AdaptiveColorToString(t.SyntaxKeyword())),
+					Color:           AdaptiveColorToString(t.SyntaxKeyword()),
 				},
 				KeywordNamespace: ansi.StylePrimitive{
 					BackgroundColor: background,
-					Color:           stringPtr(AdaptiveColorToString(t.SyntaxKeyword())),
+					Color:           AdaptiveColorToString(t.SyntaxKeyword()),
 				},
 				KeywordType: ansi.StylePrimitive{
 					BackgroundColor: background,
-					Color:           stringPtr(AdaptiveColorToString(t.SyntaxType())),
+					Color:           AdaptiveColorToString(t.SyntaxType()),
 				},
 				Operator: ansi.StylePrimitive{
 					BackgroundColor: background,
-					Color:           stringPtr(AdaptiveColorToString(t.SyntaxOperator())),
+					Color:           AdaptiveColorToString(t.SyntaxOperator()),
 				},
 				Punctuation: ansi.StylePrimitive{
 					BackgroundColor: background,
-					Color:           stringPtr(AdaptiveColorToString(t.SyntaxPunctuation())),
+					Color:           AdaptiveColorToString(t.SyntaxPunctuation()),
 				},
 				Name: ansi.StylePrimitive{
 					BackgroundColor: background,
-					Color:           stringPtr(AdaptiveColorToString(t.SyntaxVariable())),
+					Color:           AdaptiveColorToString(t.SyntaxVariable()),
 				},
 				NameBuiltin: ansi.StylePrimitive{
 					BackgroundColor: background,
-					Color:           stringPtr(AdaptiveColorToString(t.SyntaxVariable())),
+					Color:           AdaptiveColorToString(t.SyntaxVariable()),
 				},
 				NameTag: ansi.StylePrimitive{
 					BackgroundColor: background,
-					Color:           stringPtr(AdaptiveColorToString(t.SyntaxKeyword())),
+					Color:           AdaptiveColorToString(t.SyntaxKeyword()),
 				},
 				NameAttribute: ansi.StylePrimitive{
 					BackgroundColor: background,
-					Color:           stringPtr(AdaptiveColorToString(t.SyntaxFunction())),
+					Color:           AdaptiveColorToString(t.SyntaxFunction()),
 				},
 				NameClass: ansi.StylePrimitive{
 					BackgroundColor: background,
-					Color:           stringPtr(AdaptiveColorToString(t.SyntaxType())),
+					Color:           AdaptiveColorToString(t.SyntaxType()),
 				},
 				NameConstant: ansi.StylePrimitive{
 					BackgroundColor: background,
-					Color:           stringPtr(AdaptiveColorToString(t.SyntaxVariable())),
+					Color:           AdaptiveColorToString(t.SyntaxVariable()),
 				},
 				NameDecorator: ansi.StylePrimitive{
 					BackgroundColor: background,
-					Color:           stringPtr(AdaptiveColorToString(t.SyntaxFunction())),
+					Color:           AdaptiveColorToString(t.SyntaxFunction()),
 				},
 				NameFunction: ansi.StylePrimitive{
 					BackgroundColor: background,
-					Color:           stringPtr(AdaptiveColorToString(t.SyntaxFunction())),
+					Color:           AdaptiveColorToString(t.SyntaxFunction()),
 				},
 				LiteralNumber: ansi.StylePrimitive{
 					BackgroundColor: background,
-					Color:           stringPtr(AdaptiveColorToString(t.SyntaxNumber())),
+					Color:           AdaptiveColorToString(t.SyntaxNumber()),
 				},
 				LiteralString: ansi.StylePrimitive{
 					BackgroundColor: background,
-					Color:           stringPtr(AdaptiveColorToString(t.SyntaxString())),
+					Color:           AdaptiveColorToString(t.SyntaxString()),
 				},
 				LiteralStringEscape: ansi.StylePrimitive{
 					BackgroundColor: background,
-					Color:           stringPtr(AdaptiveColorToString(t.SyntaxKeyword())),
+					Color:           AdaptiveColorToString(t.SyntaxKeyword()),
 				},
 				GenericDeleted: ansi.StylePrimitive{
 					BackgroundColor: background,
-					Color:           stringPtr(AdaptiveColorToString(t.DiffRemoved())),
+					Color:           AdaptiveColorToString(t.DiffRemoved()),
 				},
 				GenericEmph: ansi.StylePrimitive{
 					BackgroundColor: background,
-					Color:           stringPtr(AdaptiveColorToString(t.MarkdownEmph())),
+					Color:           AdaptiveColorToString(t.MarkdownEmph()),
 					Italic:          boolPtr(true),
 				},
 				GenericInserted: ansi.StylePrimitive{
 					BackgroundColor: background,
-					Color:           stringPtr(AdaptiveColorToString(t.DiffAdded())),
+					Color:           AdaptiveColorToString(t.DiffAdded()),
 				},
 				GenericStrong: ansi.StylePrimitive{
 					BackgroundColor: background,
-					Color:           stringPtr(AdaptiveColorToString(t.MarkdownStrong())),
+					Color:           AdaptiveColorToString(t.MarkdownStrong()),
 					Bold:            boolPtr(true),
 				},
 				GenericSubheading: ansi.StylePrimitive{
 					BackgroundColor: background,
-					Color:           stringPtr(AdaptiveColorToString(t.MarkdownHeading())),
+					Color:           AdaptiveColorToString(t.MarkdownHeading()),
 				},
 			},
 		},
@@ -293,14 +294,14 @@ func generateMarkdownStyleConfig(backgroundColor compat.AdaptiveColor) ansi.Styl
 		},
 		DefinitionDescription: ansi.StylePrimitive{
 			BlockPrefix: "\n ❯ ",
-			Color:       stringPtr(AdaptiveColorToString(t.MarkdownLinkText())),
+			Color:       AdaptiveColorToString(t.MarkdownLinkText()),
 		},
 		Text: ansi.StylePrimitive{
-			Color: stringPtr(AdaptiveColorToString(t.MarkdownText())),
+			Color: AdaptiveColorToString(t.MarkdownText()),
 		},
 		Paragraph: ansi.StyleBlock{
 			StylePrimitive: ansi.StylePrimitive{
-				Color: stringPtr(AdaptiveColorToString(t.MarkdownText())),
+				Color: AdaptiveColorToString(t.MarkdownText()),
 			},
 		},
 	}
@@ -308,11 +309,17 @@ func generateMarkdownStyleConfig(backgroundColor compat.AdaptiveColor) ansi.Styl
 
 // AdaptiveColorToString converts a compat.AdaptiveColor to the appropriate
 // hex color string based on the current terminal background
-func AdaptiveColorToString(color compat.AdaptiveColor) string {
+func AdaptiveColorToString(color compat.AdaptiveColor) *string {
 	if Terminal.BackgroundIsDark {
+		if _, ok := color.Dark.(lipgloss.NoColor); ok {
+			return nil
+		}
 		c1, _ := colorful.MakeColor(color.Dark)
-		return c1.Hex()
+		return stringPtr(c1.Hex())
+	}
+	if _, ok := color.Light.(lipgloss.NoColor); ok {
+		return nil
 	}
 	c1, _ := colorful.MakeColor(color.Light)
-	return c1.Hex()
+	return stringPtr(c1.Hex())
 }

+ 2 - 149
packages/tui/internal/styles/styles.go

@@ -3,155 +3,8 @@ package styles
 import (
 	"github.com/charmbracelet/lipgloss/v2"
 	"github.com/charmbracelet/lipgloss/v2/compat"
-	"github.com/sst/opencode/internal/theme"
 )
 
-// BaseStyle returns the base style with background and foreground colors
-func BaseStyle() lipgloss.Style {
-	t := theme.CurrentTheme()
-	return lipgloss.NewStyle().Foreground(t.Text())
-}
-
-func Panel() lipgloss.Style {
-	t := theme.CurrentTheme()
-	return lipgloss.NewStyle().
-		Background(t.BackgroundPanel()).
-		Border(lipgloss.NormalBorder(), true, false, true, false).
-		BorderForeground(t.BorderSubtle()).
-		Foreground(t.Text())
-}
-
-// Regular returns a basic unstyled lipgloss.Style
-func Regular() lipgloss.Style {
-	return lipgloss.NewStyle()
-}
-
-func Muted() lipgloss.Style {
-	t := theme.CurrentTheme()
-	return lipgloss.NewStyle().Foreground(t.TextMuted())
-}
-
-// Bold returns a bold style
-func Bold() lipgloss.Style {
-	return BaseStyle().Bold(true)
-}
-
-// Padded returns a style with horizontal padding
-func Padded() lipgloss.Style {
-	return BaseStyle().Padding(0, 1)
-}
-
-// Border returns a style with a normal border
-func Border() lipgloss.Style {
-	t := theme.CurrentTheme()
-	return Regular().
-		Border(lipgloss.NormalBorder()).
-		BorderForeground(t.Border())
-}
-
-// ThickBorder returns a style with a thick border
-func ThickBorder() lipgloss.Style {
-	t := theme.CurrentTheme()
-	return Regular().
-		Border(lipgloss.ThickBorder()).
-		BorderForeground(t.Border())
-}
-
-// DoubleBorder returns a style with a double border
-func DoubleBorder() lipgloss.Style {
-	t := theme.CurrentTheme()
-	return Regular().
-		Border(lipgloss.DoubleBorder()).
-		BorderForeground(t.Border())
-}
-
-// FocusedBorder returns a style with a border using the focused border color
-func FocusedBorder() lipgloss.Style {
-	t := theme.CurrentTheme()
-	return Regular().
-		Border(lipgloss.NormalBorder()).
-		BorderForeground(t.BorderActive())
-}
-
-// DimBorder returns a style with a border using the dim border color
-func DimBorder() lipgloss.Style {
-	t := theme.CurrentTheme()
-	return Regular().
-		Border(lipgloss.NormalBorder()).
-		BorderForeground(t.BorderSubtle())
-}
-
-// PrimaryColor returns the primary color from the current theme
-func PrimaryColor() compat.AdaptiveColor {
-	return theme.CurrentTheme().Primary()
-}
-
-// SecondaryColor returns the secondary color from the current theme
-func SecondaryColor() compat.AdaptiveColor {
-	return theme.CurrentTheme().Secondary()
-}
-
-// AccentColor returns the accent color from the current theme
-func AccentColor() compat.AdaptiveColor {
-	return theme.CurrentTheme().Accent()
-}
-
-// ErrorColor returns the error color from the current theme
-func ErrorColor() compat.AdaptiveColor {
-	return theme.CurrentTheme().Error()
-}
-
-// WarningColor returns the warning color from the current theme
-func WarningColor() compat.AdaptiveColor {
-	return theme.CurrentTheme().Warning()
-}
-
-// SuccessColor returns the success color from the current theme
-func SuccessColor() compat.AdaptiveColor {
-	return theme.CurrentTheme().Success()
-}
-
-// InfoColor returns the info color from the current theme
-func InfoColor() compat.AdaptiveColor {
-	return theme.CurrentTheme().Info()
-}
-
-// TextColor returns the text color from the current theme
-func TextColor() compat.AdaptiveColor {
-	return theme.CurrentTheme().Text()
-}
-
-// TextMutedColor returns the muted text color from the current theme
-func TextMutedColor() compat.AdaptiveColor {
-	return theme.CurrentTheme().TextMuted()
-}
-
-// BackgroundColor returns the background color from the current theme
-func BackgroundColor() compat.AdaptiveColor {
-	return theme.CurrentTheme().Background()
-}
-
-// BackgroundPanelColor returns the subtle background color from the current theme
-func BackgroundPanelColor() compat.AdaptiveColor {
-	return theme.CurrentTheme().BackgroundPanel()
-}
-
-// BackgroundElementColor returns the darker background color from the current theme
-func BackgroundElementColor() compat.AdaptiveColor {
-	return theme.CurrentTheme().BackgroundElement()
-}
-
-// BorderColor returns the border color from the current theme
-func BorderColor() compat.AdaptiveColor {
-	return theme.CurrentTheme().Border()
-}
-
-// BorderActiveColor returns the active border color from the current theme
-func BorderActiveColor() compat.AdaptiveColor {
-	return theme.CurrentTheme().BorderActive()
-}
-
-// BorderSubtleColor returns the subtle border color from the current theme
-func BorderSubtleColor() compat.AdaptiveColor {
-	return theme.CurrentTheme().BorderSubtle()
+func WhitespaceStyle(bg compat.AdaptiveColor) lipgloss.WhitespaceOption {
+	return lipgloss.WithWhitespaceStyle(NewStyle().Background(bg).Lipgloss())
 }

+ 295 - 0
packages/tui/internal/styles/utilities.go

@@ -0,0 +1,295 @@
+package styles
+
+import (
+	"image/color"
+
+	"github.com/charmbracelet/lipgloss/v2"
+	"github.com/charmbracelet/lipgloss/v2/compat"
+)
+
+// IsNoColor checks if a color is the special NoColor type
+func IsNoColor(c color.Color) bool {
+	_, ok := c.(lipgloss.NoColor)
+	return ok
+}
+
+// Style wraps lipgloss.Style to provide a fluent API for handling "none" colors
+type Style struct {
+	lipgloss.Style
+}
+
+// NewStyle creates a new Style with proper handling of "none" colors
+func NewStyle() Style {
+	return Style{lipgloss.NewStyle()}
+}
+
+func (s Style) Lipgloss() lipgloss.Style {
+	return s.Style
+}
+
+// Foreground sets the foreground color, handling "none" appropriately
+func (s Style) Foreground(c compat.AdaptiveColor) Style {
+	if IsNoColor(c.Dark) && IsNoColor(c.Light) {
+		return Style{s.Style.UnsetForeground()}
+	}
+	return Style{s.Style.Foreground(c)}
+}
+
+// Background sets the background color, handling "none" appropriately
+func (s Style) Background(c compat.AdaptiveColor) Style {
+	if IsNoColor(c.Dark) && IsNoColor(c.Light) {
+		return Style{s.Style.UnsetBackground()}
+	}
+	return Style{s.Style.Background(c)}
+}
+
+// BorderForeground sets the border foreground color, handling "none" appropriately
+func (s Style) BorderForeground(c compat.AdaptiveColor) Style {
+	if IsNoColor(c.Dark) && IsNoColor(c.Light) {
+		return Style{s.Style.UnsetBorderForeground()}
+	}
+	return Style{s.Style.BorderForeground(c)}
+}
+
+// BorderBackground sets the border background color, handling "none" appropriately
+func (s Style) BorderBackground(c compat.AdaptiveColor) Style {
+	if IsNoColor(c.Dark) && IsNoColor(c.Light) {
+		return Style{s.Style.UnsetBorderBackground()}
+	}
+	return Style{s.Style.BorderBackground(c)}
+}
+
+// BorderTopForeground sets the border top foreground color, handling "none" appropriately
+func (s Style) BorderTopForeground(c compat.AdaptiveColor) Style {
+	if IsNoColor(c.Dark) && IsNoColor(c.Light) {
+		return Style{s.Style.UnsetBorderTopForeground()}
+	}
+	return Style{s.Style.BorderTopForeground(c)}
+}
+
+// BorderTopBackground sets the border top background color, handling "none" appropriately
+func (s Style) BorderTopBackground(c compat.AdaptiveColor) Style {
+	if IsNoColor(c.Dark) && IsNoColor(c.Light) {
+		return Style{s.Style.UnsetBorderTopBackground()}
+	}
+	return Style{s.Style.BorderTopBackground(c)}
+}
+
+// BorderBottomForeground sets the border bottom foreground color, handling "none" appropriately
+func (s Style) BorderBottomForeground(c compat.AdaptiveColor) Style {
+	if IsNoColor(c.Dark) && IsNoColor(c.Light) {
+		return Style{s.Style.UnsetBorderBottomForeground()}
+	}
+	return Style{s.Style.BorderBottomForeground(c)}
+}
+
+// BorderBottomBackground sets the border bottom background color, handling "none" appropriately
+func (s Style) BorderBottomBackground(c compat.AdaptiveColor) Style {
+	if IsNoColor(c.Dark) && IsNoColor(c.Light) {
+		return Style{s.Style.UnsetBorderBottomBackground()}
+	}
+	return Style{s.Style.BorderBottomBackground(c)}
+}
+
+// BorderLeftForeground sets the border left foreground color, handling "none" appropriately
+func (s Style) BorderLeftForeground(c compat.AdaptiveColor) Style {
+	if IsNoColor(c.Dark) && IsNoColor(c.Light) {
+		return Style{s.Style.UnsetBorderLeftForeground()}
+	}
+	return Style{s.Style.BorderLeftForeground(c)}
+}
+
+// BorderLeftBackground sets the border left background color, handling "none" appropriately
+func (s Style) BorderLeftBackground(c compat.AdaptiveColor) Style {
+	if IsNoColor(c.Dark) && IsNoColor(c.Light) {
+		return Style{s.Style.UnsetBorderLeftBackground()}
+	}
+	return Style{s.Style.BorderLeftBackground(c)}
+}
+
+// BorderRightForeground sets the border right foreground color, handling "none" appropriately
+func (s Style) BorderRightForeground(c compat.AdaptiveColor) Style {
+	if IsNoColor(c.Dark) && IsNoColor(c.Light) {
+		return Style{s.Style.UnsetBorderRightForeground()}
+	}
+	return Style{s.Style.BorderRightForeground(c)}
+}
+
+// BorderRightBackground sets the border right background color, handling "none" appropriately
+func (s Style) BorderRightBackground(c compat.AdaptiveColor) Style {
+	if IsNoColor(c.Dark) && IsNoColor(c.Light) {
+		return Style{s.Style.UnsetBorderRightBackground()}
+	}
+	return Style{s.Style.BorderRightBackground(c)}
+}
+
+// Render applies the style to a string
+func (s Style) Render(str string) string {
+	return s.Style.Render(str)
+}
+
+// Common lipgloss.Style method delegations for seamless usage
+
+func (s Style) Bold(v bool) Style {
+	return Style{s.Style.Bold(v)}
+}
+
+func (s Style) Italic(v bool) Style {
+	return Style{s.Style.Italic(v)}
+}
+
+func (s Style) Underline(v bool) Style {
+	return Style{s.Style.Underline(v)}
+}
+
+func (s Style) Strikethrough(v bool) Style {
+	return Style{s.Style.Strikethrough(v)}
+}
+
+func (s Style) Blink(v bool) Style {
+	return Style{s.Style.Blink(v)}
+}
+
+func (s Style) Faint(v bool) Style {
+	return Style{s.Style.Faint(v)}
+}
+
+func (s Style) Reverse(v bool) Style {
+	return Style{s.Style.Reverse(v)}
+}
+
+func (s Style) Width(i int) Style {
+	return Style{s.Style.Width(i)}
+}
+
+func (s Style) Height(i int) Style {
+	return Style{s.Style.Height(i)}
+}
+
+func (s Style) Padding(i ...int) Style {
+	return Style{s.Style.Padding(i...)}
+}
+
+func (s Style) PaddingTop(i int) Style {
+	return Style{s.Style.PaddingTop(i)}
+}
+
+func (s Style) PaddingBottom(i int) Style {
+	return Style{s.Style.PaddingBottom(i)}
+}
+
+func (s Style) PaddingLeft(i int) Style {
+	return Style{s.Style.PaddingLeft(i)}
+}
+
+func (s Style) PaddingRight(i int) Style {
+	return Style{s.Style.PaddingRight(i)}
+}
+
+func (s Style) Margin(i ...int) Style {
+	return Style{s.Style.Margin(i...)}
+}
+
+func (s Style) MarginTop(i int) Style {
+	return Style{s.Style.MarginTop(i)}
+}
+
+func (s Style) MarginBottom(i int) Style {
+	return Style{s.Style.MarginBottom(i)}
+}
+
+func (s Style) MarginLeft(i int) Style {
+	return Style{s.Style.MarginLeft(i)}
+}
+
+func (s Style) MarginRight(i int) Style {
+	return Style{s.Style.MarginRight(i)}
+}
+
+func (s Style) Border(b lipgloss.Border, sides ...bool) Style {
+	return Style{s.Style.Border(b, sides...)}
+}
+
+func (s Style) BorderStyle(b lipgloss.Border) Style {
+	return Style{s.Style.BorderStyle(b)}
+}
+
+func (s Style) BorderTop(v bool) Style {
+	return Style{s.Style.BorderTop(v)}
+}
+
+func (s Style) BorderBottom(v bool) Style {
+	return Style{s.Style.BorderBottom(v)}
+}
+
+func (s Style) BorderLeft(v bool) Style {
+	return Style{s.Style.BorderLeft(v)}
+}
+
+func (s Style) BorderRight(v bool) Style {
+	return Style{s.Style.BorderRight(v)}
+}
+
+func (s Style) Align(p ...lipgloss.Position) Style {
+	return Style{s.Style.Align(p...)}
+}
+
+func (s Style) AlignHorizontal(p lipgloss.Position) Style {
+	return Style{s.Style.AlignHorizontal(p)}
+}
+
+func (s Style) AlignVertical(p lipgloss.Position) Style {
+	return Style{s.Style.AlignVertical(p)}
+}
+
+func (s Style) Inline(v bool) Style {
+	return Style{s.Style.Inline(v)}
+}
+
+func (s Style) MaxWidth(n int) Style {
+	return Style{s.Style.MaxWidth(n)}
+}
+
+func (s Style) MaxHeight(n int) Style {
+	return Style{s.Style.MaxHeight(n)}
+}
+
+func (s Style) TabWidth(n int) Style {
+	return Style{s.Style.TabWidth(n)}
+}
+
+func (s Style) UnsetBold() Style {
+	return Style{s.Style.UnsetBold()}
+}
+
+func (s Style) UnsetItalic() Style {
+	return Style{s.Style.UnsetItalic()}
+}
+
+func (s Style) UnsetUnderline() Style {
+	return Style{s.Style.UnsetUnderline()}
+}
+
+func (s Style) UnsetStrikethrough() Style {
+	return Style{s.Style.UnsetStrikethrough()}
+}
+
+func (s Style) UnsetBlink() Style {
+	return Style{s.Style.UnsetBlink()}
+}
+
+func (s Style) UnsetFaint() Style {
+	return Style{s.Style.UnsetFaint()}
+}
+
+func (s Style) UnsetReverse() Style {
+	return Style{s.Style.UnsetReverse()}
+}
+
+func (s Style) Copy() Style {
+	return Style{s.Style}
+}
+
+func (s Style) Inherit(i Style) Style {
+	return Style{s.Style.Inherit(i.Style)}
+}

+ 11 - 2
packages/tui/internal/theme/loader.go

@@ -171,7 +171,7 @@ func (r *colorResolver) resolveColor(key string, value any) (any, error) {
 
 	switch v := value.(type) {
 	case string:
-		if strings.HasPrefix(v, "#") {
+		if strings.HasPrefix(v, "#") || v == "none" {
 			return v, nil
 		}
 		return r.resolveReference(v)
@@ -205,7 +205,7 @@ func (r *colorResolver) resolveColor(key string, value any) (any, error) {
 func (r *colorResolver) resolveColorValue(value any) (any, error) {
 	switch v := value.(type) {
 	case string:
-		if strings.HasPrefix(v, "#") {
+		if strings.HasPrefix(v, "#") || v == "none" {
 			return v, nil
 		}
 		return r.resolveReference(v)
@@ -240,6 +240,12 @@ func (r *colorResolver) resolveReference(ref string) (any, error) {
 func parseResolvedColor(value any) (compat.AdaptiveColor, error) {
 	switch v := value.(type) {
 	case string:
+		if v == "none" {
+			return compat.AdaptiveColor{
+				Dark:  lipgloss.NoColor{},
+				Light: lipgloss.NoColor{},
+			}, nil
+		}
 		return compat.AdaptiveColor{
 			Dark:  lipgloss.Color(v),
 			Light: lipgloss.Color(v),
@@ -277,6 +283,9 @@ func parseResolvedColor(value any) (compat.AdaptiveColor, error) {
 func parseColorValue(value any) (color.Color, error) {
 	switch v := value.(type) {
 	case string:
+		if v == "none" {
+			return lipgloss.NoColor{}, nil
+		}
 		return lipgloss.Color(v), nil
 	case float64:
 		return lipgloss.Color(fmt.Sprintf("%d", int(v))), nil

+ 129 - 5
packages/tui/internal/theme/manager.go

@@ -2,19 +2,25 @@ package theme
 
 import (
 	"fmt"
+	"image/color"
 	"slices"
+	"strconv"
 	"strings"
 	"sync"
 
 	"github.com/alecthomas/chroma/v2/styles"
+	"github.com/charmbracelet/lipgloss/v2"
+	"github.com/charmbracelet/lipgloss/v2/compat"
+	"github.com/charmbracelet/x/ansi"
 )
 
 // Manager handles theme registration, selection, and retrieval.
 // It maintains a registry of available themes and tracks the currently active theme.
 type Manager struct {
-	themes      map[string]Theme
-	currentName string
-	mu          sync.RWMutex
+	themes               map[string]Theme
+	currentName          string
+	currentUsesAnsiCache bool // Cache whether current theme uses ANSI colors
+	mu                   sync.RWMutex
 }
 
 // Global instance of the theme manager
@@ -34,6 +40,7 @@ func RegisterTheme(name string, theme Theme) {
 	// If this is the first theme, make it the default
 	if globalManager.currentName == "" {
 		globalManager.currentName = name
+		globalManager.currentUsesAnsiCache = themeUsesAnsiColors(theme)
 	}
 }
 
@@ -44,11 +51,13 @@ func SetTheme(name string) error {
 	defer globalManager.mu.Unlock()
 	delete(styles.Registry, "charm")
 
-	if _, exists := globalManager.themes[name]; !exists {
+	theme, exists := globalManager.themes[name]
+	if !exists {
 		return fmt.Errorf("theme '%s' not found", name)
 	}
 
 	globalManager.currentName = name
+	globalManager.currentUsesAnsiCache = themeUsesAnsiColors(theme)
 
 	return nil
 }
@@ -84,7 +93,11 @@ func AvailableThemes() []string {
 		names = append(names, name)
 	}
 	slices.SortFunc(names, func(a, b string) int {
-		// list system theme first
+		if a == "system" {
+			return -1
+		} else if b == "system" {
+			return 1
+		}
 		if a == "opencode" {
 			return -1
 		} else if b == "opencode" {
@@ -103,3 +116,114 @@ func GetTheme(name string) Theme {
 
 	return globalManager.themes[name]
 }
+
+// UpdateSystemTheme updates the system theme with terminal background info
+func UpdateSystemTheme(terminalBg color.Color, isDark bool) {
+	globalManager.mu.Lock()
+	defer globalManager.mu.Unlock()
+
+	dynamicTheme := NewSystemTheme(terminalBg, isDark)
+	globalManager.themes["system"] = dynamicTheme
+	if globalManager.currentName == "system" {
+		globalManager.currentUsesAnsiCache = themeUsesAnsiColors(dynamicTheme)
+	}
+}
+
+// CurrentThemeUsesAnsiColors returns true if the current theme uses ANSI 0-16 colors
+func CurrentThemeUsesAnsiColors() bool {
+	// globalManager.mu.RLock()
+	// defer globalManager.mu.RUnlock()
+
+	return globalManager.currentUsesAnsiCache
+}
+
+// isAnsiColor checks if a color represents an ANSI 0-16 color
+func isAnsiColor(c color.Color) bool {
+	if _, ok := c.(lipgloss.NoColor); ok {
+		return false
+	}
+	if _, ok := c.(ansi.BasicColor); ok {
+		return true
+	}
+
+	// For other color types, check if they represent ANSI colors
+	// by examining their string representation
+	if stringer, ok := c.(fmt.Stringer); ok {
+		str := stringer.String()
+		// Check if it's a numeric ANSI color (0-15)
+		if num, err := strconv.Atoi(str); err == nil && num >= 0 && num <= 15 {
+			return true
+		}
+	}
+
+	return false
+}
+
+// adaptiveColorUsesAnsi checks if an AdaptiveColor uses ANSI colors
+func adaptiveColorUsesAnsi(ac compat.AdaptiveColor) bool {
+	if isAnsiColor(ac.Dark) {
+		return true
+	}
+	if isAnsiColor(ac.Light) {
+		return true
+	}
+	return false
+}
+
+// themeUsesAnsiColors checks if a theme uses any ANSI 0-16 colors
+func themeUsesAnsiColors(theme Theme) bool {
+	if theme == nil {
+		return false
+	}
+
+	return adaptiveColorUsesAnsi(theme.Primary()) ||
+		adaptiveColorUsesAnsi(theme.Secondary()) ||
+		adaptiveColorUsesAnsi(theme.Accent()) ||
+		adaptiveColorUsesAnsi(theme.Error()) ||
+		adaptiveColorUsesAnsi(theme.Warning()) ||
+		adaptiveColorUsesAnsi(theme.Success()) ||
+		adaptiveColorUsesAnsi(theme.Info()) ||
+		adaptiveColorUsesAnsi(theme.Text()) ||
+		adaptiveColorUsesAnsi(theme.TextMuted()) ||
+		adaptiveColorUsesAnsi(theme.Background()) ||
+		adaptiveColorUsesAnsi(theme.BackgroundPanel()) ||
+		adaptiveColorUsesAnsi(theme.BackgroundElement()) ||
+		adaptiveColorUsesAnsi(theme.Border()) ||
+		adaptiveColorUsesAnsi(theme.BorderActive()) ||
+		adaptiveColorUsesAnsi(theme.BorderSubtle()) ||
+		adaptiveColorUsesAnsi(theme.DiffAdded()) ||
+		adaptiveColorUsesAnsi(theme.DiffRemoved()) ||
+		adaptiveColorUsesAnsi(theme.DiffContext()) ||
+		adaptiveColorUsesAnsi(theme.DiffHunkHeader()) ||
+		adaptiveColorUsesAnsi(theme.DiffHighlightAdded()) ||
+		adaptiveColorUsesAnsi(theme.DiffHighlightRemoved()) ||
+		adaptiveColorUsesAnsi(theme.DiffAddedBg()) ||
+		adaptiveColorUsesAnsi(theme.DiffRemovedBg()) ||
+		adaptiveColorUsesAnsi(theme.DiffContextBg()) ||
+		adaptiveColorUsesAnsi(theme.DiffLineNumber()) ||
+		adaptiveColorUsesAnsi(theme.DiffAddedLineNumberBg()) ||
+		adaptiveColorUsesAnsi(theme.DiffRemovedLineNumberBg()) ||
+		adaptiveColorUsesAnsi(theme.MarkdownText()) ||
+		adaptiveColorUsesAnsi(theme.MarkdownHeading()) ||
+		adaptiveColorUsesAnsi(theme.MarkdownLink()) ||
+		adaptiveColorUsesAnsi(theme.MarkdownLinkText()) ||
+		adaptiveColorUsesAnsi(theme.MarkdownCode()) ||
+		adaptiveColorUsesAnsi(theme.MarkdownBlockQuote()) ||
+		adaptiveColorUsesAnsi(theme.MarkdownEmph()) ||
+		adaptiveColorUsesAnsi(theme.MarkdownStrong()) ||
+		adaptiveColorUsesAnsi(theme.MarkdownHorizontalRule()) ||
+		adaptiveColorUsesAnsi(theme.MarkdownListItem()) ||
+		adaptiveColorUsesAnsi(theme.MarkdownListEnumeration()) ||
+		adaptiveColorUsesAnsi(theme.MarkdownImage()) ||
+		adaptiveColorUsesAnsi(theme.MarkdownImageText()) ||
+		adaptiveColorUsesAnsi(theme.MarkdownCodeBlock()) ||
+		adaptiveColorUsesAnsi(theme.SyntaxComment()) ||
+		adaptiveColorUsesAnsi(theme.SyntaxKeyword()) ||
+		adaptiveColorUsesAnsi(theme.SyntaxFunction()) ||
+		adaptiveColorUsesAnsi(theme.SyntaxVariable()) ||
+		adaptiveColorUsesAnsi(theme.SyntaxString()) ||
+		adaptiveColorUsesAnsi(theme.SyntaxNumber()) ||
+		adaptiveColorUsesAnsi(theme.SyntaxType()) ||
+		adaptiveColorUsesAnsi(theme.SyntaxOperator()) ||
+		adaptiveColorUsesAnsi(theme.SyntaxPunctuation())
+}

+ 299 - 0
packages/tui/internal/theme/system.go

@@ -0,0 +1,299 @@
+package theme
+
+import (
+	"fmt"
+	"image/color"
+	"math"
+
+	"github.com/charmbracelet/lipgloss/v2"
+	"github.com/charmbracelet/lipgloss/v2/compat"
+)
+
+// SystemTheme is a dynamic theme that derives its gray scale colors
+// from the terminal's background color at runtime
+type SystemTheme struct {
+	BaseTheme
+	terminalBg       color.Color
+	terminalBgIsDark bool
+}
+
+// NewSystemTheme creates a new instance of the dynamic system theme
+func NewSystemTheme(terminalBg color.Color, isDark bool) *SystemTheme {
+	theme := &SystemTheme{
+		terminalBg:       terminalBg,
+		terminalBgIsDark: isDark,
+	}
+	theme.initializeColors()
+	return theme
+}
+
+// initializeColors sets up all theme colors
+func (t *SystemTheme) initializeColors() {
+	// Generate gray scale based on terminal background
+	grays := t.generateGrayScale()
+
+	// Set ANSI colors for primary colors
+	t.PrimaryColor = compat.AdaptiveColor{
+		Dark:  lipgloss.Cyan,
+		Light: lipgloss.Cyan,
+	}
+	t.SecondaryColor = compat.AdaptiveColor{
+		Dark:  lipgloss.Magenta,
+		Light: lipgloss.Magenta,
+	}
+	t.AccentColor = compat.AdaptiveColor{
+		Dark:  lipgloss.Cyan,
+		Light: lipgloss.Cyan,
+	}
+
+	// Status colors using ANSI
+	t.ErrorColor = compat.AdaptiveColor{
+		Dark:  lipgloss.Red,
+		Light: lipgloss.Red,
+	}
+	t.WarningColor = compat.AdaptiveColor{
+		Dark:  lipgloss.Yellow,
+		Light: lipgloss.Yellow,
+	}
+	t.SuccessColor = compat.AdaptiveColor{
+		Dark:  lipgloss.Green,
+		Light: lipgloss.Green,
+	}
+	t.InfoColor = compat.AdaptiveColor{
+		Dark:  lipgloss.Cyan,
+		Light: lipgloss.Cyan,
+	}
+
+	// Text colors
+	t.TextColor = compat.AdaptiveColor{
+		Dark:  lipgloss.NoColor{},
+		Light: lipgloss.NoColor{},
+	}
+	// Derive muted text color from terminal foreground
+	t.TextMutedColor = t.generateMutedTextColor()
+
+	// Background colors
+	t.BackgroundColor = compat.AdaptiveColor{
+		Dark:  lipgloss.NoColor{},
+		Light: lipgloss.NoColor{},
+	}
+	t.BackgroundPanelColor = grays[2]
+	t.BackgroundElementColor = grays[3]
+
+	// Border colors
+	t.BorderSubtleColor = grays[6]
+	t.BorderColor = grays[7]
+	t.BorderActiveColor = grays[8]
+
+	// Diff colors using ANSI colors
+	t.DiffAddedColor = compat.AdaptiveColor{
+		Dark:  lipgloss.Color("2"), // green
+		Light: lipgloss.Color("2"),
+	}
+	t.DiffRemovedColor = compat.AdaptiveColor{
+		Dark:  lipgloss.Color("1"), // red
+		Light: lipgloss.Color("1"),
+	}
+	t.DiffContextColor = grays[7] // Use gray for context
+	t.DiffHunkHeaderColor = grays[7]
+	t.DiffHighlightAddedColor = compat.AdaptiveColor{
+		Dark:  lipgloss.Color("2"), // green
+		Light: lipgloss.Color("2"),
+	}
+	t.DiffHighlightRemovedColor = compat.AdaptiveColor{
+		Dark:  lipgloss.Color("1"), // red
+		Light: lipgloss.Color("1"),
+	}
+	// Use subtle gray backgrounds for diff
+	t.DiffAddedBgColor = grays[2]
+	t.DiffRemovedBgColor = grays[2]
+	t.DiffContextBgColor = grays[1]
+	t.DiffLineNumberColor = grays[6]
+	t.DiffAddedLineNumberBgColor = grays[3]
+	t.DiffRemovedLineNumberBgColor = grays[3]
+
+	// Markdown colors using ANSI
+	t.MarkdownTextColor = compat.AdaptiveColor{
+		Dark:  lipgloss.NoColor{},
+		Light: lipgloss.NoColor{},
+	}
+	t.MarkdownHeadingColor = compat.AdaptiveColor{
+		Dark:  lipgloss.NoColor{},
+		Light: lipgloss.NoColor{},
+	}
+	t.MarkdownLinkColor = compat.AdaptiveColor{
+		Dark:  lipgloss.Color("4"), // blue
+		Light: lipgloss.Color("4"),
+	}
+	t.MarkdownLinkTextColor = compat.AdaptiveColor{
+		Dark:  lipgloss.Color("6"), // cyan
+		Light: lipgloss.Color("6"),
+	}
+	t.MarkdownCodeColor = compat.AdaptiveColor{
+		Dark:  lipgloss.Color("2"), // green
+		Light: lipgloss.Color("2"),
+	}
+	t.MarkdownBlockQuoteColor = compat.AdaptiveColor{
+		Dark:  lipgloss.Color("3"), // yellow
+		Light: lipgloss.Color("3"),
+	}
+	t.MarkdownEmphColor = compat.AdaptiveColor{
+		Dark:  lipgloss.Color("3"), // yellow
+		Light: lipgloss.Color("3"),
+	}
+	t.MarkdownStrongColor = compat.AdaptiveColor{
+		Dark:  lipgloss.NoColor{},
+		Light: lipgloss.NoColor{},
+	}
+	t.MarkdownHorizontalRuleColor = t.BorderColor
+	t.MarkdownListItemColor = compat.AdaptiveColor{
+		Dark:  lipgloss.Color("4"), // blue
+		Light: lipgloss.Color("4"),
+	}
+	t.MarkdownListEnumerationColor = compat.AdaptiveColor{
+		Dark:  lipgloss.Color("6"), // cyan
+		Light: lipgloss.Color("6"),
+	}
+	t.MarkdownImageColor = compat.AdaptiveColor{
+		Dark:  lipgloss.Color("4"), // blue
+		Light: lipgloss.Color("4"),
+	}
+	t.MarkdownImageTextColor = compat.AdaptiveColor{
+		Dark:  lipgloss.Color("6"), // cyan
+		Light: lipgloss.Color("6"),
+	}
+	t.MarkdownCodeBlockColor = compat.AdaptiveColor{
+		Dark:  lipgloss.NoColor{},
+		Light: lipgloss.NoColor{},
+	}
+
+	// Syntax colors
+	t.SyntaxCommentColor = t.TextMutedColor // Use same as muted text
+	t.SyntaxKeywordColor = compat.AdaptiveColor{
+		Dark:  lipgloss.Color("5"), // magenta
+		Light: lipgloss.Color("5"),
+	}
+	t.SyntaxFunctionColor = compat.AdaptiveColor{
+		Dark:  lipgloss.Color("4"), // blue
+		Light: lipgloss.Color("4"),
+	}
+	t.SyntaxVariableColor = compat.AdaptiveColor{
+		Dark:  lipgloss.NoColor{},
+		Light: lipgloss.NoColor{},
+	}
+	t.SyntaxStringColor = compat.AdaptiveColor{
+		Dark:  lipgloss.Color("2"), // green
+		Light: lipgloss.Color("2"),
+	}
+	t.SyntaxNumberColor = compat.AdaptiveColor{
+		Dark:  lipgloss.Color("3"), // yellow
+		Light: lipgloss.Color("3"),
+	}
+	t.SyntaxTypeColor = compat.AdaptiveColor{
+		Dark:  lipgloss.Color("6"), // cyan
+		Light: lipgloss.Color("6"),
+	}
+	t.SyntaxOperatorColor = compat.AdaptiveColor{
+		Dark:  lipgloss.Color("6"), // cyan
+		Light: lipgloss.Color("6"),
+	}
+	t.SyntaxPunctuationColor = compat.AdaptiveColor{
+		Dark:  lipgloss.NoColor{},
+		Light: lipgloss.NoColor{},
+	}
+}
+
+// generateGrayScale creates a gray scale based on the terminal background
+func (t *SystemTheme) generateGrayScale() map[int]compat.AdaptiveColor {
+	grays := make(map[int]compat.AdaptiveColor)
+
+	r, g, b, _ := t.terminalBg.RGBA()
+	bgR := float64(r >> 8)
+	bgG := float64(g >> 8)
+	bgB := float64(b >> 8)
+
+	luminance := 0.299*bgR + 0.587*bgG + 0.114*bgB
+
+	for i := 1; i <= 12; i++ {
+		var stepColor string
+		factor := float64(i) / 12.0
+
+		if t.terminalBgIsDark {
+			if luminance < 10 {
+				grayValue := int(factor * 0.4 * 255)
+				stepColor = fmt.Sprintf("#%02x%02x%02x", grayValue, grayValue, grayValue)
+			} else {
+				newLum := luminance + (255-luminance)*factor*0.4
+
+				ratio := newLum / luminance
+				newR := math.Min(bgR*ratio, 255)
+				newG := math.Min(bgG*ratio, 255)
+				newB := math.Min(bgB*ratio, 255)
+
+				stepColor = fmt.Sprintf("#%02x%02x%02x", int(newR), int(newG), int(newB))
+			}
+		} else {
+			if luminance > 245 {
+				grayValue := int(255 - factor*0.4*255)
+				stepColor = fmt.Sprintf("#%02x%02x%02x", grayValue, grayValue, grayValue)
+			} else {
+				newLum := luminance * (1 - factor*0.4)
+
+				ratio := newLum / luminance
+				newR := math.Max(bgR*ratio, 0)
+				newG := math.Max(bgG*ratio, 0)
+				newB := math.Max(bgB*ratio, 0)
+
+				stepColor = fmt.Sprintf("#%02x%02x%02x", int(newR), int(newG), int(newB))
+			}
+		}
+
+		grays[i] = compat.AdaptiveColor{
+			Dark:  lipgloss.Color(stepColor),
+			Light: lipgloss.Color(stepColor),
+		}
+	}
+
+	return grays
+}
+
+// generateMutedTextColor creates a muted gray color based on the terminal background
+func (t *SystemTheme) generateMutedTextColor() compat.AdaptiveColor {
+	bgR, bgG, bgB, _ := t.terminalBg.RGBA()
+
+	bgRf := float64(bgR >> 8)
+	bgGf := float64(bgG >> 8)
+	bgBf := float64(bgB >> 8)
+
+	bgLum := 0.299*bgRf + 0.587*bgGf + 0.114*bgBf
+
+	var grayValue int
+	if t.terminalBgIsDark {
+		if bgLum < 10 {
+			// Very dark/black background
+			// grays[3] would be around #2e (46), so we need much lighter
+			grayValue = 180 // #b4b4b4
+		} else {
+			// Scale up for lighter dark backgrounds
+			// Ensure we're always significantly brighter than BackgroundElement
+			grayValue = min(int(160+(bgLum*0.3)), 200)
+		}
+	} else {
+		if bgLum > 245 {
+			// Very light/white background
+			// grays[3] would be around #f5 (245), so we need much darker
+			grayValue = 75 // #4b4b4b
+		} else {
+			// Scale down for darker light backgrounds
+			// Ensure we're always significantly darker than BackgroundElement
+			grayValue = max(int(100-((255-bgLum)*0.2)), 60)
+		}
+	}
+
+	mutedColor := fmt.Sprintf("#%02x%02x%02x", grayValue, grayValue, grayValue)
+
+	return compat.AdaptiveColor{
+		Dark:  lipgloss.Color(mutedColor),
+		Light: lipgloss.Color(mutedColor),
+	}
+}

+ 15 - 1
packages/tui/internal/tui/tui.go

@@ -22,6 +22,7 @@ import (
 	"github.com/sst/opencode/internal/components/toast"
 	"github.com/sst/opencode/internal/layout"
 	"github.com/sst/opencode/internal/styles"
+	"github.com/sst/opencode/internal/theme"
 	"github.com/sst/opencode/internal/util"
 	"github.com/sst/opencode/pkg/client"
 )
@@ -230,9 +231,19 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		return a, tea.Batch(cmds...)
 	case tea.BackgroundColorMsg:
 		styles.Terminal = &styles.TerminalInfo{
+			Background:       msg.Color,
 			BackgroundIsDark: msg.IsDark(),
 		}
-		slog.Debug("Background color", "isDark", msg.IsDark())
+		slog.Debug("Background color", "color", msg.String(), "isDark", msg.IsDark())
+		return a, func() tea.Msg {
+			theme.UpdateSystemTheme(
+				styles.Terminal.Background,
+				styles.Terminal.BackgroundIsDark,
+			)
+			return dialog.ThemeSelectedMsg{
+				ThemeName: theme.CurrentThemeName(),
+			}
+		}
 	case modal.CloseModalMsg:
 		var cmd tea.Cmd
 		if a.modal != nil {
@@ -424,6 +435,9 @@ func (a appModel) View() string {
 
 	appView = a.toastManager.RenderOverlay(appView)
 
+	if theme.CurrentThemeUsesAnsiColors() {
+		appView = util.ConvertRGBToAnsi16Colors(appView)
+	}
 	return appView
 }
 

+ 93 - 0
packages/tui/internal/util/color.go

@@ -0,0 +1,93 @@
+package util
+
+import (
+	"regexp"
+	"strings"
+)
+
+var csiRE *regexp.Regexp
+
+func init() {
+	csiRE = regexp.MustCompile(`\x1b\[([0-9;]+)m`)
+}
+
+var targetFGMap = map[string]string{
+	"0;0;0":       "\x1b[30m", // Black
+	"128;0;0":     "\x1b[31m", // Red
+	"0;128;0":     "\x1b[32m", // Green
+	"128;128;0":   "\x1b[33m", // Yellow
+	"0;0;128":     "\x1b[34m", // Blue
+	"128;0;128":   "\x1b[35m", // Magenta
+	"0;128;128":   "\x1b[36m", // Cyan
+	"192;192;192": "\x1b[37m", // White (light grey)
+	"128;128;128": "\x1b[90m", // Bright Black (dark grey)
+	"255;0;0":     "\x1b[91m", // Bright Red
+	"0;255;0":     "\x1b[92m", // Bright Green
+	"255;255;0":   "\x1b[93m", // Bright Yellow
+	"0;0;255":     "\x1b[94m", // Bright Blue
+	"255;0;255":   "\x1b[95m", // Bright Magenta
+	"0;255;255":   "\x1b[96m", // Bright Cyan
+	"255;255;255": "\x1b[97m", // Bright White
+}
+
+var targetBGMap = map[string]string{
+	"0;0;0":       "\x1b[40m",
+	"128;0;0":     "\x1b[41m",
+	"0;128;0":     "\x1b[42m",
+	"128;128;0":   "\x1b[43m",
+	"0;0;128":     "\x1b[44m",
+	"128;0;128":   "\x1b[45m",
+	"0;128;128":   "\x1b[46m",
+	"192;192;192": "\x1b[47m",
+	"128;128;128": "\x1b[100m",
+	"255;0;0":     "\x1b[101m",
+	"0;255;0":     "\x1b[102m",
+	"255;255;0":   "\x1b[103m",
+	"0;0;255":     "\x1b[104m",
+	"255;0;255":   "\x1b[105m",
+	"0;255;255":   "\x1b[106m",
+	"255;255;255": "\x1b[107m",
+}
+
+func ConvertRGBToAnsi16Colors(s string) string {
+	return csiRE.ReplaceAllStringFunc(s, func(seq string) string {
+		params := strings.Split(csiRE.FindStringSubmatch(seq)[1], ";")
+		out := make([]string, 0, len(params))
+
+		for i := 0; i < len(params); {
+			// Detect “38 | 48 ; 2 ; r ; g ; b ( ; alpha? )”
+			if (params[i] == "38" || params[i] == "48") &&
+				i+4 < len(params) &&
+				params[i+1] == "2" {
+
+				key := strings.Join(params[i+2:i+5], ";")
+				var repl string
+				if params[i] == "38" {
+					repl = targetFGMap[key]
+				} else {
+					repl = targetBGMap[key]
+				}
+
+				if repl != "" { // exact RGB hit
+					out = append(out, repl[2:len(repl)-1])
+					i += 5 // skip 38/48;2;r;g;b
+
+					// if i == len(params)-1 && looksLikeByte(params[i]) {
+					// 	i++ // swallow the alpha byte
+					// }
+					continue
+				}
+			}
+			// Normal token — keep verbatim.
+			out = append(out, params[i])
+			i++
+		}
+
+		return "\x1b[" + strings.Join(out, ";") + "m"
+	})
+}
+
+// func looksLikeByte(tok string) bool {
+// 	v, err := strconv.Atoi(tok)
+// 	return err == nil && v >= 0 && v <= 255
+// }

+ 20 - 0
packages/web/public/theme.json

@@ -22,6 +22,11 @@
               "minimum": 0,
               "maximum": 255,
               "description": "ANSI color code (0-255)"
+            },
+            {
+              "type": "string",
+              "enum": ["none"],
+              "description": "No color (uses terminal default)"
             }
           ]
         }
@@ -110,6 +115,11 @@
           "maximum": 255,
           "description": "ANSI color code (0-255, same for dark and light)"
         },
+        {
+          "type": "string",
+          "enum": ["none"],
+          "description": "No color (uses terminal default)"
+        },
         {
           "type": "string",
           "pattern": "^[a-zA-Z][a-zA-Z0-9_]*$",
@@ -131,6 +141,11 @@
                   "maximum": 255,
                   "description": "ANSI color code for dark mode"
                 },
+                {
+                  "type": "string",
+                  "enum": ["none"],
+                  "description": "No color (uses terminal default)"
+                },
                 {
                   "type": "string",
                   "pattern": "^[a-zA-Z][a-zA-Z0-9_]*$",
@@ -151,6 +166,11 @@
                   "maximum": 255,
                   "description": "ANSI color code for light mode"
                 },
+                {
+                  "type": "string",
+                  "enum": ["none"],
+                  "description": "No color (uses terminal default)"
+                },
                 {
                   "type": "string",
                   "pattern": "^[a-zA-Z][a-zA-Z0-9_]*$",

+ 25 - 3
packages/web/src/content/docs/docs/themes.mdx

@@ -2,7 +2,7 @@
 title: Themes
 ---
 
-opencode supports a flexible JSON-based theme system that allows users to create and customize themes easily.
+opencode supports a flexible JSON-based theme system that allows users to create and customize themes easily. 
 
 ## Theme Loading Hierarchy
 
@@ -37,6 +37,7 @@ Themes use a flexible JSON format with support for:
 - **ANSI colors**: `3` (0-255)
 - **Color references**: `"primary"` or custom definitions
 - **Dark/light variants**: `{"dark": "#000", "light": "#fff"}`
+- **No color**: `"none"` - Uses the terminal's default color (transparent)
 
 ### Example Theme
 
@@ -270,10 +271,30 @@ Themes use a flexible JSON format with support for:
 
 The `defs` section (optional) allows you to define reusable colors that can be referenced in the theme.
 
+### Using "none" for Terminal Defaults
+
+The special value `\"none\"` can be used for any color to inherit the terminal's default color. This is particularly useful for creating themes that blend seamlessly with your terminal's color scheme:
+
+- `\"text\": \"none\"` - Uses terminal's default foreground color
+- `\"background\": \"none\"` - Uses terminal's default background color
+
+## The System Theme
+
+The `system` theme is opencode's default theme, designed to automatically adapt to your terminal's color scheme. Unlike traditional themes that use fixed colors, the system theme:
+
+- **Generates gray scale**: Creates a custom gray scale based on your terminal's background color, ensuring optimal contrast
+- **Uses ANSI colors**: Leverages standard ANSI colors (0-15) for syntax highlighting and UI elements, which respect your terminal's color palette
+- **Preserves terminal defaults**: Uses `none` for text and background colors to maintain your terminal's native appearance
+
+The system theme is ideal for users who:
+- Want opencode to match their terminal's appearance
+- Use custom terminal color schemes
+- Prefer a consistent look across all terminal applications
+
 ## Built-in Themes
 
 opencode comes with several built-in themes:
-- `opencode` - Default opencode theme
+- `system` - Default theme that dynamically adapts to your terminal's background color
 - `tokyonight` - Tokyonight theme
 - `everforest` - Everforest theme
 - `ayu` - Ayu dark theme
@@ -281,7 +302,8 @@ opencode comes with several built-in themes:
 - `gruvbox` - Gruvbox theme
 - `kanagawa` - Kanagawa theme
 - `nord` - Nord theme
-- and more (see ./packages/tui/internal/theme/themes)
+- `matrix` - Hacker-style green on black theme
+- `one-dark` - Atom One Dark inspired theme
 
 ## Using a Theme