mirror of
https://github.com/usestrix/strix.git
synced 2026-08-24 20:02:39 +02:00
fix(tui): keep color across every wrapped row of a tool result line
A long blocked-safety reason showed its amber only on the first row. wrapBlock wraps an over-width line with ansi.Wrap, which emits the opening SGR on the first row and the reset on the last, leaving the continuation rows unstyled — so a wrapped colored line (a safety block reason, a long error) lost its color after the first row. carryStyle re-opens the active style on each continuation row and terminates each with a reset. Any colored line wider than the chat width was affected; block reasons are just the first routinely-long one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -159,11 +160,37 @@ func wrapBlock(value string, width int) string {
|
||||
out = append(out, line)
|
||||
continue
|
||||
}
|
||||
out = append(out, strings.Split(ansi.Wrap(line, width, " -"), "\n")...)
|
||||
out = append(out, carryStyle(strings.Split(ansi.Wrap(line, width, " -"), "\n"))...)
|
||||
}
|
||||
return strings.Join(out, "\n")
|
||||
}
|
||||
|
||||
var sgrPattern = regexp.MustCompile(`\x1b\[[0-9;]*m`)
|
||||
|
||||
// carryStyle re-opens the active foreground/attribute style on each continuation
|
||||
// line of a wrapped logical line. ansi.Wrap emits the opening SGR only on the first
|
||||
// line and the reset only on the last, so a wrapped colored line (a blocked-safety
|
||||
// reason, a long error) would otherwise show color on its first row alone.
|
||||
func carryStyle(lines []string) []string {
|
||||
active := ""
|
||||
for i, line := range lines {
|
||||
if active != "" {
|
||||
lines[i] = active + line
|
||||
}
|
||||
for _, seq := range sgrPattern.FindAllString(line, -1) {
|
||||
if seq == "\x1b[0m" || seq == "\x1b[m" {
|
||||
active = ""
|
||||
} else {
|
||||
active = seq
|
||||
}
|
||||
}
|
||||
if active != "" && i < len(lines)-1 {
|
||||
lines[i] += "\x1b[0m"
|
||||
}
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
// scrollbarThumb brightens the bar being dragged so the grab reads as taking
|
||||
// hold of it.
|
||||
func (m Model) scrollbarThumb(target scrollbarTarget) lipgloss.Color {
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
"github.com/muesli/termenv"
|
||||
)
|
||||
|
||||
// A colored line wider than the wrap width must stay colored on every row, not
|
||||
// only the first: ansi.Wrap emits the opening SGR once and the reset once, so
|
||||
// wrapBlock re-opens the active style on each continuation line.
|
||||
func TestWrapBlockCarriesColorAcrossContinuationLines(t *testing.T) {
|
||||
lipgloss.SetColorProfile(termenv.TrueColor)
|
||||
amber := "\x1b[38;2;245;158;11m"
|
||||
line := lipgloss.NewStyle().Foreground(lipgloss.Color("#f59e0b")).
|
||||
Render("Blocked: " + strings.Repeat("a reason long enough to wrap ", 4))
|
||||
|
||||
rows := strings.Split(wrapBlock(line, 30), "\n")
|
||||
if len(rows) < 3 {
|
||||
t.Fatalf("expected the reason to wrap to several rows, got %d", len(rows))
|
||||
}
|
||||
for i, row := range rows {
|
||||
if strings.TrimSpace(ansi.Strip(row)) == "" {
|
||||
continue
|
||||
}
|
||||
if !strings.Contains(row, amber) {
|
||||
t.Errorf("row %d lost its color after wrapping: %q", i, row)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrapBlockLeavesShortColoredLineUnchanged(t *testing.T) {
|
||||
lipgloss.SetColorProfile(termenv.TrueColor)
|
||||
line := lipgloss.NewStyle().Foreground(lipgloss.Color("#f59e0b")).Render("Blocked: short")
|
||||
if got := wrapBlock(line, 80); got != line {
|
||||
t.Errorf("a line within width was rewritten:\n got %q\nwant %q", got, line)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user