Files
zero/internal/tui/transcript_view.go
Vasanth T 49bec0274f feat(tui): interactive theme picker + color theme catalog (#354)
* feat(tui): interactive theme picker + color theme catalog

Bare `/theme` now opens a popup that live-previews each palette as the cursor
moves and applies on select (Esc reverts), matching /model and /effort. Adds ten
built-in color themes alongside the dark/light built-ins — dracula, nord, gruvbox,
tokyo-night, catppuccin, one-dark, solarized-dark, rose-pine, everforest, and
solarized-light — plus a reworked light palette. Every palette is contrast-audited
to WCAG AA; the contrast tests now loop the whole registry.

Palettes and an ordered registry live in a new internal/tui/theme_palettes.go (now
the sole home of hex literals); selection, the picker, /theme <name>, --theme, and
ZERO_THEME all resolve through it. The whole frame is painted with the active
theme's surface (tea.View.BackgroundColor) so every theme is legible on any
terminal — a light theme's dark text no longer lands invisibly on a dark terminal,
and color themes show their real surface.

The chosen theme persists to user config (preferences.theme) and is reapplied at
startup, below the --theme flag and ZERO_THEME.

* fix(tui): address CodeRabbit review on the theme catalog

- CHANGELOG: reconcile the --theme bullet with the named themes (was still
  documenting {auto|dark|light}).
- /theme usage string advertises the list and auto forms (/theme list is still
  accepted) so /help matches the command surface.
- Theme-persistence test parses the written config and asserts preferences.theme
  directly instead of a raw substring, so a wrong-key regression fails.
2026-07-01 15:46:15 +05:30

73 lines
1.9 KiB
Go

package tui
import "strings"
func (m model) toggleDetailedTranscript() model {
m.transcriptDetailed = !m.transcriptDetailed
m.clearSuggestions()
if m.picker != nil && m.picker.kind == pickerTheme {
// Dropping a theme picker mid-preview must undo the live palette, or the
// previewed theme would stick while m.themeMode still holds the old one.
m.restoreCommittedTheme()
}
m.picker = nil
return m
}
func (m model) detailedTranscriptView() string {
width := chatWidth(m.width)
rc := buildRowContext(m.transcript)
renderer := m
renderer.pending = false
renderer.activeRunID = 0
var builder strings.Builder
builder.WriteString(detailedTranscriptHeader(width))
builder.WriteString("\n")
builder.WriteString(zeroTheme.line.Render(strings.Repeat("-", width)))
builder.WriteString("\n")
shownAny := false
var previousKind rowKind
for _, row := range m.transcript {
if rc.skip(row) {
continue
}
rendered := ""
if row.kind == rowWelcome {
rendered = fitStyledLine(zeroTheme.faint.Render(row.text), width)
} else {
rendered = renderer.renderRowDetailed(row, width, rc)
}
if rendered == "" {
continue
}
if shownAny && startsTurn(row.kind) {
builder.WriteString("\n")
}
if shownAny && previousKind == rowUser && row.kind == rowReasoning {
builder.WriteString("\n")
}
builder.WriteString(rendered)
builder.WriteString("\n")
shownAny = true
previousKind = row.kind
}
if !shownAny {
builder.WriteString(zeroTheme.faint.Render("No transcript rows."))
builder.WriteString("\n")
}
builder.WriteString(zeroTheme.line.Render(strings.Repeat("-", width)))
builder.WriteString("\n")
builder.WriteString(fitStyledLine(zeroTheme.faint.Render("Esc close | Ctrl+O toggle"), width))
return builder.String()
}
func detailedTranscriptHeader(width int) string {
title := zeroTheme.ink.Bold(true).Render("Transcript")
hint := zeroTheme.faint.Render("detailed")
return fitStyledLine(joinHeaderLine(title, hint, width), width)
}