on main help sign in
Merge pull request #192 from charmbracelet/revert-176-escape-sequences

Revert "fix(tui): escape ANSI escape sequences and control characters in tool"

commite18502579c40c88a2bd95d239a1adf15058c9413 authorKujtim Hoxha <kujtimii.h@gmail.com> dateTue 15 Jul 2025, 14:56 (1 year ago) parent8122fec6cb766a
3 files changed+23−98
internal/llm/tools/view.go+23−1 internal/tui/components/chat/messages/renderer.go−45 internal/tui/components/chat/messages/renderer_test.go−52
internal/llm/tools/view.go+23−1
@@ -238,7 +238,7 @@ func readTextFile(filePath string, offset, limit int) (string, int, error) {
238238
239239 lineCount := 0
240240
241− scanner := bufio.NewScanner(file)
241+ scanner := NewLineScanner(file)
242242 if offset > 0 {
243243 for lineCount < offset && scanner.Scan() {
244244 lineCount++
@@ -298,3 +298,25 @@ func isImageFile(filePath string) (bool, string) {
298298 return false, ""
299299 }
300300}
301+
302+type LineScanner struct {
303+ scanner *bufio.Scanner
304+}
305+
306+func NewLineScanner(r io.Reader) *LineScanner {
307+ return &LineScanner{
308+ scanner: bufio.NewScanner(r),
309+ }
310+}
311+
312+func (s *LineScanner) Scan() bool {
313+ return s.scanner.Scan()
314+}
315+
316+func (s *LineScanner) Text() string {
317+ return s.scanner.Text()
318+}
319+
320+func (s *LineScanner) Err() error {
321+ return s.scanner.Err()
322+}
internal/tui/components/chat/messages/renderer.go−45
@@ -3,7 +3,6 @@ package messages
33import (
44 "encoding/json"
55 "fmt"
6− "strconv"
76 "strings"
87 "time"
98
@@ -657,7 +656,6 @@ func joinHeaderBody(header, body string) string {
657656func renderPlainContent(v *toolCallCmp, content string) string {
658657 t := styles.CurrentTheme()
659658 content = strings.TrimSpace(content)
660− content = escapeContent(t, content)
661659 lines := strings.Split(content, "\n")
662660
663661 width := v.textWidth() - 2 // -2 for left padding
@@ -696,7 +694,6 @@ func pad(v any, width int) string {
696694
697695func renderCodeContent(v *toolCallCmp, path, content string, offset int) string {
698696 t := styles.CurrentTheme()
699− content = escapeContent(t, content)
700697 truncated := truncateHeight(content, responseContextHeight)
701698
702699 highlighted, _ := highlight.SyntaxHighlight(truncated, path, t.BgBase)
@@ -769,45 +766,3 @@ func prettifyToolName(name string) string {
769766 return name
770767 }
771768}
772−
773−// escapeContent escapes ANSI escape sequences and control characters in the
774−// content and styles it for display in the terminal.
775−func escapeContent(t *styles.Theme, content string) string {
776− content = strings.ReplaceAll(content, "\r\n", "\n")
777− lines := strings.Split(content, "\n")
778− for i, line := range lines {
779− lines[i] = escapeLine(t, line)
780− }
781−
782− content = strings.Join(lines, "\n")
783− return content
784−}
785−
786−// escapeLine escapes ANSI escape sequences and control characters and styles
787−// them for display in the terminal.
788−func escapeLine(t *styles.Theme, text string) string {
789− var (
790− sb strings.Builder
791− state byte
792− seq string
793− n int
794− w int
795− )
796− var faint lipgloss.Style
797− if t != nil {
798− faint = t.S().Muted.Faint(true)
799− }
800− for len(text) > 0 {
801− seq, w, n, state = ansi.DecodeSequence(text, state, nil)
802− if w > 0 {
803− sb.WriteString(seq)
804− } else {
805− quote := strconv.Quote(seq)
806− quote = strings.TrimPrefix(quote, "\"")
807− quote = strings.TrimSuffix(quote, "\"")
808− sb.WriteString(faint.Render(quote))
809− }
810− text = text[n:]
811− }
812− return sb.String()
813−}
internal/tui/components/chat/messages/renderer_test.godeleted−52
@@ -1,52 +0,0 @@
1−package messages
2−
3−import (
4− "testing"
5−)
6−
7−func TestEscapeContent(t *testing.T) {
8− cases := []struct {
9− name string
10− input string
11− expected string
12− }{
13− {
14− name: "nothing to escape",
15− input: "Hello, World!",
16− expected: "Hello, World!",
17− },
18− {
19− name: "escape csi sequences",
20− input: "\x1b[31mRed Text\x1b[0m",
21− expected: "\\x1b[31mRed Text\\x1b[0m",
22− },
23− {
24− name: "escape control characters",
25− input: "Hello\x00World\x7f!",
26− expected: "Hello\\x00World\\x7f!",
27− },
28− {
29− name: "escape csi sequences with control characters",
30− input: "\x1b[31mHello\x00World\x7f!\x1b[0m",
31− expected: "\\x1b[31mHello\\x00World\\x7f!\\x1b[0m",
32− },
33− {
34− name: "just unicode",
35− input: "こんにちは", // "Hello" in Japanese
36− expected: "こんにちは",
37− },
38− {
39− name: "unicode with csi sequences and control characters",
40− input: "\x1b[31mこんにちは\x00World\x7f!\x1b[0m",
41− expected: "\\x1b[31mこんにちは\\x00World\\x7f!\\x1b[0m",
42− },
43− }
44− for i, c := range cases {
45− t.Run(c.name, func(t *testing.T) {
46− result := escapeContent(nil, c.input)
47− if result != c.expected {
48− t.Errorf("case %d, expected %q, got %q", i+1, c.expected, result)
49− }
50− })
51− }
52−}