Files
agentu/internal/tui/theme.go
T
loveuer 7b8bad5e62 feat: crush-inspired TUI improvements, activity bar polish, startup fresh session
TUI improvements:
- semantic icon constants (✓ ◆ ▣ ✕ ⌘ ◌ ◔ ◉)
- pill-style metadata bar with model/provider/session/context
- fuzzy command palette with deterministic subsequence scoring
- input history navigation (ctrl+p / ctrl+n)
- structured tool log cards (▣ tool_name + args preview)
- no-background activity indicator and meta bar for cleaner rendering

Startup behavior:
- default startup creates a fresh session
- explicit --resume <id> restores saved context
- added main_test.go for initializeSession coverage
2026-06-24 18:16:53 -07:00

271 lines
6.2 KiB
Go

package tui
import (
"fmt"
"strings"
"github.com/charmbracelet/bubbles/textarea"
"github.com/charmbracelet/lipgloss"
)
type ThemeMode string
const (
ThemeLight ThemeMode = "light"
ThemeDark ThemeMode = "dark"
)
const (
iconReady = "✓"
iconWorking = "◆"
iconTool = "▣"
iconError = "✕"
iconCommand = "⌘"
iconSession = "◌"
iconContext = "◔"
iconModel = "◉"
)
func ParseThemeMode(value string) (ThemeMode, error) {
switch ThemeMode(strings.ToLower(strings.TrimSpace(value))) {
case "", ThemeLight:
return ThemeLight, nil
case ThemeDark:
return ThemeDark, nil
default:
return "", fmt.Errorf("unsupported theme %q; expected light or dark", value)
}
}
type theme struct {
Mode ThemeMode
Background string
Surface string
SurfaceAlt string
Border string
Text string
Muted string
Title string
User string
Assistant string
Tool string
Error string
System string
}
type styles struct {
App lipgloss.Style
Muted lipgloss.Style
Activity lipgloss.Style
Header lipgloss.Style
ModelMeta lipgloss.Style
Footer lipgloss.Style
Viewport lipgloss.Style
InputBox lipgloss.Style
CommandHint lipgloss.Style
StatusReady lipgloss.Style
StatusWarn lipgloss.Style
StatusError lipgloss.Style
StatusInfo lipgloss.Style
Pill lipgloss.Style
PillAccent lipgloss.Style
Palette lipgloss.Style
PaletteMatch lipgloss.Style
ToolName lipgloss.Style
ToolKey lipgloss.Style
ToolValue lipgloss.Style
UserLabel lipgloss.Style
ToolLabel lipgloss.Style
ErrorLabel lipgloss.Style
SystemLabel lipgloss.Style
UserMessage lipgloss.Style
AssistantMsg lipgloss.Style
ToolMessage lipgloss.Style
ErrorMessage lipgloss.Style
SystemMessage lipgloss.Style
}
func themeForMode(mode ThemeMode) theme {
if mode == ThemeDark {
return darkTheme()
}
return lightTheme()
}
func lightTheme() theme {
return theme{
Mode: ThemeLight,
Background: "#F8FAFC",
Surface: "#FFFFFF",
SurfaceAlt: "#EEF2F7",
Border: "#CBD5E1",
Text: "#111827",
Muted: "#64748B",
Title: "#0F766E",
User: "#2563EB",
Assistant: "#0F766E",
Tool: "#B45309",
Error: "#DC2626",
System: "#64748B",
}
}
func darkTheme() theme {
return theme{
Mode: ThemeDark,
Background: "#111827",
Surface: "#1F2937",
SurfaceAlt: "#0F172A",
Border: "#475569",
Text: "#E5E7EB",
Muted: "#94A3B8",
Title: "#5EEAD4",
User: "#93C5FD",
Assistant: "#5EEAD4",
Tool: "#FBBF24",
Error: "#FCA5A5",
System: "#94A3B8",
}
}
func (t theme) styles() styles {
return styles{
App: lipgloss.NewStyle().
Foreground(lipgloss.Color(t.Text)),
Muted: lipgloss.NewStyle().
Foreground(lipgloss.Color(t.Muted)),
Activity: lipgloss.NewStyle().
Foreground(lipgloss.Color(t.Muted)).
Padding(0, 1),
Header: lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color(t.Title)),
ModelMeta: lipgloss.NewStyle().
Foreground(lipgloss.Color(t.Muted)).
Padding(0, 1),
Footer: lipgloss.NewStyle().
Foreground(lipgloss.Color(t.Muted)).
Padding(0, 1),
Viewport: lipgloss.NewStyle().
Foreground(lipgloss.Color(t.Text)),
InputBox: lipgloss.NewStyle().
Foreground(lipgloss.Color(t.Text)).
Border(lipgloss.ThickBorder()).
BorderForeground(lipgloss.Color(t.Border)).
Padding(0, 2),
CommandHint: lipgloss.NewStyle().
Foreground(lipgloss.Color(t.Muted)).
Padding(0, 1),
StatusReady: lipgloss.NewStyle().
Foreground(lipgloss.Color(t.Assistant)),
StatusWarn: lipgloss.NewStyle().
Foreground(lipgloss.Color(t.Tool)),
StatusError: lipgloss.NewStyle().
Foreground(lipgloss.Color(t.Error)),
StatusInfo: lipgloss.NewStyle().
Foreground(lipgloss.Color(t.User)),
Pill: lipgloss.NewStyle().
Foreground(lipgloss.Color(t.Text)).
Background(lipgloss.Color(t.SurfaceAlt)).
Padding(0, 1),
PillAccent: lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color(t.Title)).
Background(lipgloss.Color(t.SurfaceAlt)).
Padding(0, 1),
Palette: lipgloss.NewStyle().
Foreground(lipgloss.Color(t.Text)).
Border(lipgloss.ThickBorder()).
BorderForeground(lipgloss.Color(t.Border)).
Padding(0, 2),
PaletteMatch: lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color(t.User)),
ToolName: lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color(t.Tool)),
ToolKey: lipgloss.NewStyle().
Foreground(lipgloss.Color(t.Muted)),
ToolValue: lipgloss.NewStyle().
Foreground(lipgloss.Color(t.Text)),
UserLabel: labelStyle(t.User),
ToolLabel: labelStyle(t.Tool),
ErrorLabel: labelStyle(t.Error),
SystemLabel: labelStyle(t.System),
UserMessage: bubbleMessageStyle(t.Text, t.SurfaceAlt),
AssistantMsg: assistantMessageStyle(t.Text),
ToolMessage: messageStyle(t.Text),
ErrorMessage: messageStyle(t.Error),
SystemMessage: lipgloss.NewStyle().Foreground(lipgloss.Color(t.System)).Padding(0, 1),
}
}
func labelStyle(color string) lipgloss.Style {
return lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color(color))
}
func messageStyle(textColor string) lipgloss.Style {
return lipgloss.NewStyle().
Foreground(lipgloss.Color(textColor)).
Padding(0, 1)
}
func assistantMessageStyle(textColor string) lipgloss.Style {
return lipgloss.NewStyle().
Foreground(lipgloss.Color(textColor)).
Padding(1, 2)
}
func bubbleMessageStyle(textColor, backgroundColor string) lipgloss.Style {
return lipgloss.NewStyle().
Foreground(lipgloss.Color(textColor)).
Background(lipgloss.Color(backgroundColor)).
Padding(1, 2)
}
func applyTextareaTheme(input *textarea.Model, t theme) {
focused := textarea.Style{
Base: lipgloss.NewStyle().
Foreground(lipgloss.Color(t.Text)),
CursorLine: lipgloss.NewStyle(),
Placeholder: lipgloss.NewStyle().
Foreground(lipgloss.Color(t.Muted)),
Prompt: lipgloss.NewStyle().
Foreground(lipgloss.Color(t.Muted)),
Text: lipgloss.NewStyle().
Foreground(lipgloss.Color(t.Text)),
EndOfBuffer: lipgloss.NewStyle().
Foreground(lipgloss.Color(t.Surface)),
}
blurred := focused
blurred.CursorLine = lipgloss.NewStyle()
input.FocusedStyle = focused
input.BlurredStyle = blurred
}