From a2d353b24020fc8e9f7519518f63fe54d16220a4 Mon Sep 17 00:00:00 2001 From: Rolando Santamaria Maso Date: Sat, 12 Sep 2026 15:15:27 +0200 Subject: [PATCH 1/4] fix(tui): paint every canvas row and re-theme the help card on switch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ember Light showed terminal-black stripes because paintCanvas only re-asserted the canvas after embedded SGR resets — rows with no escapes (plain text, blank rows, bottom padding) fell back to the terminal's own background. Every row now starts on the canvas SGR. The raw /help card was a point-in-time snapshot the theme switch never regenerated, leaving it on the previous palette. switchTheme now rebuilds every help card in place via buildHelpCard, preserving transcript order. Also updates all dependencies (go 1.26.0, x/*, goldmark, regexp2, runewidth, charmbracelet x/exp/slice) and pins both fixes with RED-first regression tests. --- go.mod | 18 ++--- go.sum | 32 ++++---- internal/tui/canvas.go | 7 ++ internal/tui/commands.go | 23 +++++- internal/tui/model.go | 1 + internal/tui/theme_regression_test.go | 102 ++++++++++++++++++++++++++ 6 files changed, 156 insertions(+), 27 deletions(-) create mode 100644 internal/tui/theme_regression_test.go diff --git a/go.mod b/go.mod index 0ad82c0..1c67db4 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/BackendStack21/bodek -go 1.25.0 +go 1.26.0 require ( github.com/charmbracelet/bubbles v1.0.0 @@ -11,8 +11,8 @@ require ( github.com/charmbracelet/x/term v0.2.2 github.com/muesli/cancelreader v0.2.2 github.com/muesli/termenv v0.16.0 - golang.org/x/net v0.58.0 - golang.org/x/sys v0.47.0 + golang.org/x/net v0.59.0 + golang.org/x/sys v0.48.0 ) require ( @@ -22,23 +22,23 @@ require ( github.com/aymerick/douceur v0.2.0 // indirect github.com/charmbracelet/colorprofile v0.4.3 // indirect github.com/charmbracelet/x/cellbuf v0.0.15 // indirect - github.com/charmbracelet/x/exp/slice v0.0.0-20260816001655-68d539dca504 // indirect + github.com/charmbracelet/x/exp/slice v0.0.0-20260906004030-3986e9119cf9 // indirect github.com/clipperhouse/displaywidth v0.11.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect - github.com/dlclark/regexp2/v2 v2.7.1 // indirect + github.com/dlclark/regexp2/v2 v2.8.0 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/gorilla/css v1.0.1 // indirect github.com/lucasb-eyer/go-colorful v1.4.1 // indirect github.com/mattn/go-isatty v0.0.24 // indirect github.com/mattn/go-localereader v0.0.1 // indirect - github.com/mattn/go-runewidth v0.0.28 // indirect + github.com/mattn/go-runewidth v0.0.30 // indirect github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/reflow v0.3.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/xo/terminfo v1.0.0 // indirect - github.com/yuin/goldmark v1.8.5 // indirect + github.com/yuin/goldmark v1.8.6 // indirect github.com/yuin/goldmark-emoji v1.0.6 // indirect - golang.org/x/term v0.45.0 // indirect - golang.org/x/text v0.41.0 // indirect + golang.org/x/term v0.46.0 // indirect + golang.org/x/text v0.42.0 // indirect ) diff --git a/go.sum b/go.sum index 35ae0d2..9795310 100644 --- a/go.sum +++ b/go.sum @@ -30,16 +30,16 @@ github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMx github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q= github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ= github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= -github.com/charmbracelet/x/exp/slice v0.0.0-20260816001655-68d539dca504 h1:Z0hBPQ9hslsfpFRRdMn+4cjnb3LK6FQH58hQkXrXv0A= -github.com/charmbracelet/x/exp/slice v0.0.0-20260816001655-68d539dca504/go.mod h1:vqEfX6xzqW1pKKZUUiFOKg0OQ7bCh54Q2vR/tserrRA= +github.com/charmbracelet/x/exp/slice v0.0.0-20260906004030-3986e9119cf9 h1:zW96vcZ3zDetccerbN7sS/sCydOE/l/9S+C94pozsaU= +github.com/charmbracelet/x/exp/slice v0.0.0-20260906004030-3986e9119cf9/go.mod h1:vqEfX6xzqW1pKKZUUiFOKg0OQ7bCh54Q2vR/tserrRA= github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= -github.com/dlclark/regexp2/v2 v2.7.1 h1:yqDtwI1ptXXvEUNpYTk2lad4jLtAcKqkzepn4savSk4= -github.com/dlclark/regexp2/v2 v2.7.1/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU= +github.com/dlclark/regexp2/v2 v2.8.0 h1:CekDhPLGfm+GAJmPVFIG+5dqMIQPkyHJll7BbdneDfw= +github.com/dlclark/regexp2/v2 v2.8.0/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= @@ -53,8 +53,8 @@ github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -github.com/mattn/go-runewidth v0.0.28 h1:rPyg2ybwEKPebvpzVWe1gKBkH8EQFkxO4Y0hjBeLaBU= -github.com/mattn/go-runewidth v0.0.28/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8= +github.com/mattn/go-runewidth v0.0.30 h1:+KUuiDA4fF0R1p5FeueHefjDm+GIM+kWfFnDjybOPgk= +github.com/mattn/go-runewidth v0.0.30/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= @@ -71,18 +71,18 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/xo/terminfo v1.0.0 h1:2ZpYzqWzyyytjk3TP6aJVDhkMAkc99/1xKQdA3TDTBY= github.com/xo/terminfo v1.0.0/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= -github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA= -github.com/yuin/goldmark v1.8.5/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= +github.com/yuin/goldmark v1.8.6 h1:d0VcaP1sx9GkFVkoW+KtggpGi2KZ965i14b0+bDQST4= +github.com/yuin/goldmark v1.8.6/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs= github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= -golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= -golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= +golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues= +golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= -golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= -golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= -golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= -golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE= +golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc= +golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= +golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= diff --git a/internal/tui/canvas.go b/internal/tui/canvas.go index 2ef1ac0..3b27ec9 100644 --- a/internal/tui/canvas.go +++ b/internal/tui/canvas.go @@ -27,5 +27,12 @@ func (m *Model) paintCanvas(body string) string { base := fg + bg frame = strings.NewReplacer("\x1b[0m", "\x1b[0m"+base, "\x1b[m", "\x1b[m"+base, "\x1b[49m", bg, "\x1b[39m", fg).Replace(frame) + // Reset-at-line-start insurance: a reset at the very end of a row clears + // the background for the whole following row on some terminals, and rows + // that never carried an escape (blank transcript rows, plain text lines, + // bottom padding) were painted by nothing at all — the terminal's own + // background bled through as black stripes on the parchment canvas. + // Every row starts on the canvas. + frame = strings.ReplaceAll(frame, "\n", "\n"+base) return base + frame + "\x1b[0m" } diff --git a/internal/tui/commands.go b/internal/tui/commands.go index 35fc091..07c06d8 100644 --- a/internal/tui/commands.go +++ b/internal/tui/commands.go @@ -254,6 +254,16 @@ func (m *Model) switchTheme(name string) tea.Cmd { m.ta.FocusedStyle.Placeholder = m.th.inputPlaceholder m.ta.BlurredStyle.Placeholder = m.th.inputPlaceholder m.logoCache = "" // the banner gradient is palette-dependent + // Raw cards are point-in-time snapshots and never go through glamour + // (resize skips them) — the /help card must still follow the palette, so + // regenerate it in place, preserving transcript position. + for i := range m.msgs { + if m.msgs[i].help { + card := m.buildHelpCard() + m.msgs[i].content = card + m.msgs[i].rendered = card + } + } m.resize(m.width, m.height) if m.opts.OnThemeChange != nil { if err := m.opts.OnThemeChange(canonical); err != nil { @@ -266,6 +276,16 @@ func (m *Model) switchTheme(name string) tea.Cmd { // showHelp appends a help card listing commands and key bindings. It is // pre-styled to the brand palette (raw), not glamour's stock dark style. func (m *Model) showHelp() { + card := m.buildHelpCard() + m.msgs = append(m.msgs, message{role: roleAsst, content: card, rendered: card, raw: true, help: true}) + m.refresh() +} + +// buildHelpCard renders the /help card against the active theme. It is a +// point-in-time styled snapshot (raw), so switchTheme regenerates every help +// card in the transcript through this builder instead of leaving it on the +// previous palette. +func (m *Model) buildHelpCard() string { th := m.th // Same framed-card contract as the composer, palette, and drawer: the // box spans the terminal and the rule fills the inner text column. @@ -317,8 +337,7 @@ func (m *Model) showHelp() { } card := th.acBox.Width(m.cardWidth()).Render(b.String()) - m.msgs = append(m.msgs, message{role: roleAsst, content: card, rendered: card, raw: true}) - m.refresh() + return card } // runExport saves the current session transcript next to the user — diff --git a/internal/tui/model.go b/internal/tui/model.go index 524b52f..4914424 100644 --- a/internal/tui/model.go +++ b/internal/tui/model.go @@ -117,6 +117,7 @@ type message struct { streaming bool stats *turnStats // finalized-turn telemetry; nil while streaming / for history raw bool // content is pre-styled; render verbatim, never re-render + help bool // raw /help card: regenerated (not glamour) on theme switch sentAt time.Time // user turns: when the prompt was submitted (drives the head's age) collapsed bool // turn card folded to its head + summary line (c) systemWake bool // server-initiated turn (background-job wake): marker on the card diff --git a/internal/tui/theme_regression_test.go b/internal/tui/theme_regression_test.go new file mode 100644 index 0000000..d9d28b1 --- /dev/null +++ b/internal/tui/theme_regression_test.go @@ -0,0 +1,102 @@ +package tui + +import ( + "strings" + "testing" + + "github.com/charmbracelet/glamour" + "github.com/charmbracelet/lipgloss" + "github.com/muesli/termenv" +) + +// TestEmberLightCodeBlocksNotDark reproduces the "blacks still observed after +// enabling Ember Light" report: answerGlamourStyle adopts glamour's stock +// LightStyleConfig wholesale for ember-light, and that preset paints chroma +// code-block backgrounds #373737 — a near-black panel inside every fenced +// code block rendered on the parchment card. The light theme must not emit +// dark background SGRs for code blocks. +func TestEmberLightCodeBlocksNotDark(t *testing.T) { + oldProfile := lipgloss.ColorProfile() + lipgloss.SetColorProfile(termenv.TrueColor) + defer lipgloss.SetColorProfile(oldProfile) + + t.Setenv("BODEK_THEME", "ember-light") + r, err := glamour.NewTermRenderer( + glamour.WithStyles(answerGlamourStyle()), + glamour.WithWordWrap(80), + ) + if err != nil { + t.Fatal(err) + } + out, err := r.Render("```go\nfunc main() {}\n```\n") + if err != nil { + t.Fatal(err) + } + if strings.Contains(out, "48;2;55;55;55") { // #373737 + t.Errorf("ember-light code block paints glamour's near-black #373737 background:\n%q", out) + } +} + +// TestLightCanvasPaintsEveryRow reproduces "blacks are still observed after +// enabling Ember Light": paintCanvas frames the body with an unstyled +// lipgloss Width/Height style and only re-asserts the canvas after embedded +// SGR resets — rows that contain no escapes at all (blank transcript rows, +// plain text lines, bottom padding) carry no background and fall back to the +// terminal's own (dark) background. Every visible row must paint the canvas. +func TestLightCanvasPaintsEveryRow(t *testing.T) { + oldProfile := lipgloss.ColorProfile() + lipgloss.SetColorProfile(termenv.TrueColor) + defer lipgloss.SetColorProfile(oldProfile) + + m := newTestModel() + m.th = themeFrom(emberLight) + m.width, m.height = 20, 5 + // Row 2 is completely escape-free, like a plain transcript line; row 4 + // is empty; row 5 only exists as bottom padding. + out := m.paintCanvas("styled \x1b[0mrow\nplain row no escapes\n\nstyled \x1b[0magain") + bg := "\x1b[48;2;250;248;242m" + for i, line := range strings.Split(out, "\n") { + if lipgloss.Width(plain(line)) == 0 && line == "" { + continue + } + if !strings.Contains(line, bg) && !strings.Contains(line, "\x1b[48") { + t.Errorf("row %d paints no canvas background (terminal black bleeds through): %q", i, line) + } + } +} + +// TestHelpCardRethemedAfterSwitch reproduces "Ember Light misses some +// components": the raw /help card is a point-in-time styled snapshot and +// switchTheme's resize() skips raw messages, so after switching themes the +// help card keeps the previous palette's colors. It must re-render with the +// active theme. +func TestHelpCardRethemedAfterSwitch(t *testing.T) { + oldProfile := lipgloss.ColorProfile() + lipgloss.SetColorProfile(termenv.TrueColor) + defer lipgloss.SetColorProfile(oldProfile) + + old := themeOverride + defer func() { themeOverride = old }() + + m := newTestModel() + themeOverride = "ember-dark" + m.th = themeFrom(emberDark) + m.showHelp() + + rendered, _ := m.renderMessage(m.msgs[len(m.msgs)-1], len(m.msgs)-1, 0) + block := rendered + if !strings.Contains(block, "38;2;168;176;192") { // ember-dark muted #A8B0C0 + t.Fatalf("precondition: help card carries ember-dark muted color:\n%q", block) + } + + m.switchTheme("ember-light") + + rendered, _ = m.renderMessage(m.msgs[len(m.msgs)-1], len(m.msgs)-1, 0) + block = rendered + if strings.Contains(block, "38;2;168;176;192") { + t.Errorf("help card still styled with ember-dark muted after switch to ember-light:\n%q", block) + } + if !strings.Contains(block, "38;2;89;95;109") { // ember-light muted #5A5F6D (lipgloss quantizes g−1) + t.Errorf("help card missing ember-light muted color after switch:\n%q", block) + } +} From 96ded1f895491a2e44bcdbbab886fb5908b8edda Mon Sep 17 00:00:00 2001 From: Rolando Santamaria Maso Date: Sat, 12 Sep 2026 15:21:21 +0200 Subject: [PATCH 2/4] ci: bump golangci-lint to v2.13.2 for go 1.26 module The pinned v2.5.0 binary is built with go1.25 and cannot load the config of a go 1.26.0 module; v2.13.2 supports it. --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 007dbdd..f71f717 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -82,4 +82,4 @@ jobs: - name: golangci-lint uses: golangci/golangci-lint-action@v9 with: - version: v2.5.0 + version: v2.13.2 From 3999325f7adb16e9e34b1cb14b784b1c6e901144 Mon Sep 17 00:00:00 2001 From: Rolando Santamaria Maso Date: Sat, 12 Sep 2026 15:26:47 +0200 Subject: [PATCH 3/4] fix(tui): detect the empty answer-card surface via its SGR lipgloss GetBackground returns NoColor{}, never nil, so the high-contrast skip in answerCardBody was dead code (staticcheck SA4023). surfaceSGR already encodes the no-surface case; use it. --- internal/tui/view.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/tui/view.go b/internal/tui/view.go index d8891fe..5bcc15a 100644 --- a/internal/tui/view.go +++ b/internal/tui/view.go @@ -848,7 +848,9 @@ func (m *Model) renderMessage(msg message, msgIdx, lineOffset int) (string, []st // still fits; high-contrast skips the surface entirely. Returns the styled // card and its line count. func (m *Model) answerCardBody(body string) (string, int) { - if m.th.answerCard.GetBackground() == nil { + // lipgloss GetBackground returns NoColor{}, never nil, so the empty + // surface (high-contrast) is detected through its missing SGR instead. + if surfaceSGR(m.th.answerCard) == "" { return body, lineCount(body) } // Glamour resets styling after each span; without re-asserting the From 9d66c9788551e594551d35f2ae47e56f0f7fc2cc Mon Sep 17 00:00:00 2001 From: Rolando Santamaria Maso Date: Sat, 12 Sep 2026 15:34:13 +0200 Subject: [PATCH 4/4] fix(tui): detect the empty answer-card surface via lipgloss.NoColor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GetBackground never returns nil — the unset case is NoColor{}, so the nil comparison was dead code (staticcheck SA4023). Assert the sentinel by type; unlike the rendered SGR this is profile-independent, so cards keep their width in CI's low-color profile too. --- internal/tui/view.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/tui/view.go b/internal/tui/view.go index 5bcc15a..e8f8d45 100644 --- a/internal/tui/view.go +++ b/internal/tui/view.go @@ -849,8 +849,9 @@ func (m *Model) renderMessage(msg message, msgIdx, lineOffset int) (string, []st // card and its line count. func (m *Model) answerCardBody(body string) (string, int) { // lipgloss GetBackground returns NoColor{}, never nil, so the empty - // surface (high-contrast) is detected through its missing SGR instead. - if surfaceSGR(m.th.answerCard) == "" { + // surface (high-contrast) is detected by value — profile-independent, + // unlike the rendered SGR, which is also empty in low-color profiles. + if _, none := m.th.answerCard.GetBackground().(lipgloss.NoColor); none { return body, lineCount(body) } // Glamour resets styling after each span; without re-asserting the