revert(ui): restore original Read tool renderer without herald

The herald-based CodeBlock implementation didn't match the custom
styling we had for line numbers and gutters. Restoring the original
renderReadBody and renderCodeBlock functions with:
- Custom line number gutter styling
- Chroma syntax highlighting
- Truncation handling with footer preservation
This commit is contained in:
Ed Zynda
2026-03-27 20:57:34 +03:00
parent f12e195390
commit d4f27bc912
+105 -104
View File
@@ -4,7 +4,6 @@ import (
"bytes"
"encoding/json"
"fmt"
"path/filepath"
"regexp"
"strconv"
"strings"
@@ -16,89 +15,8 @@ import (
"github.com/alecthomas/chroma/v2/styles"
udiff "github.com/aymanbagabas/go-udiff"
xansi "github.com/charmbracelet/x/ansi"
"github.com/indaco/herald"
)
// detectLanguage extracts the language from a filename for syntax highlighting.
func detectLanguage(fileName string) string {
ext := strings.ToLower(filepath.Ext(fileName))
ext = strings.TrimPrefix(ext, ".")
// Map common extensions to language names
langMap := map[string]string{
"go": "go",
"py": "python",
"js": "javascript",
"ts": "typescript",
"jsx": "jsx",
"tsx": "tsx",
"rs": "rust",
"java": "java",
"cpp": "cpp",
"c": "c",
"h": "c",
"hpp": "cpp",
"cs": "csharp",
"rb": "ruby",
"php": "php",
"swift": "swift",
"kt": "kotlin",
"scala": "scala",
"r": "r",
"sql": "sql",
"sh": "bash",
"bash": "bash",
"zsh": "zsh",
"fish": "fish",
"ps1": "powershell",
"yaml": "yaml",
"yml": "yaml",
"json": "json",
"toml": "toml",
"xml": "xml",
"html": "html",
"htm": "html",
"css": "css",
"scss": "scss",
"sass": "sass",
"less": "less",
"md": "markdown",
"dockerfile": "dockerfile",
"makefile": "makefile",
"vim": "vim",
"lua": "lua",
"perl": "perl",
"pl": "perl",
"haskell": "haskell",
"hs": "haskell",
"erlang": "erlang",
"erl": "erlang",
"elixir": "elixir",
"ex": "elixir",
"exs": "elixir",
"clojure": "clojure",
"clj": "clojure",
"lisp": "lisp",
"scheme": "scheme",
"racket": "racket",
"ocaml": "ocaml",
"ml": "ocaml",
"fsharp": "fsharp",
"fs": "fsharp",
"fsx": "fsharp",
"dart": "dart",
"flutter": "dart",
"julia": "julia",
"groovy": "groovy",
"gradle": "groovy",
}
if lang, ok := langMap[ext]; ok {
return lang
}
return ext
}
// Maximum visible lines per tool type before truncation.
const (
maxDiffLines = 20 // side-by-side rows for Edit
@@ -456,7 +374,8 @@ func renderLsBody(toolResult string, width int) string {
// Read tool — code block with line numbers + syntax highlighting
// ---------------------------------------------------------------------------
// renderReadBody renders Read tool output using herald's CodeBlock with line numbers.
// renderReadBody renders Read tool output with styled line numbers and optional
// syntax highlighting based on file extension.
func renderReadBody(toolArgs, toolResult string, width int) string {
if strings.TrimSpace(toolResult) == "" {
return ""
@@ -471,39 +390,121 @@ func renderReadBody(toolArgs, toolResult string, width int) string {
}
}
// Parse lines and extract just the code content (removing "N: " prefix)
lines := strings.Split(toolResult, "\n")
var codeLines []string
for _, line := range lines {
return renderCodeBlock(toolResult, fileName, width)
}
// codeLine holds a parsed line with optional line number.
type codeLine struct {
lineNum string
code string
}
// renderCodeBlock renders content with a styled gutter (line numbers) and
// optional syntax highlighting.
func renderCodeBlock(content, fileName string, width int) string {
rawLines := strings.Split(content, "\n")
// Parse lines: detect "N: content" format from Read tool
var parsed []codeLine
maxNumWidth := 0
var codeOnly []string
for _, line := range rawLines {
if idx := strings.Index(line, ": "); idx > 0 && idx <= 7 {
numPart := line[:idx]
if _, err := strconv.Atoi(strings.TrimSpace(numPart)); err == nil {
codeLines = append(codeLines, line[idx+2:])
parsed = append(parsed, codeLine{lineNum: numPart, code: line[idx+2:]})
if len(numPart) > maxNumWidth {
maxNumWidth = len(numPart)
}
codeOnly = append(codeOnly, line[idx+2:])
continue
}
}
codeLines = append(codeLines, line)
// No line number — treat as metadata/footer
parsed = append(parsed, codeLine{code: line})
codeOnly = append(codeOnly, line)
}
content := strings.Join(codeLines, "\n")
// Truncate if too long
if len(codeLines) > maxCodeLines {
content = strings.Join(codeLines[:maxCodeLines], "\n")
if len(parsed) == 0 {
return ""
}
// Detect language from filename
lang := detectLanguage(fileName)
// Truncate to maxCodeLines visible lines (preserve footer/metadata lines)
var codeHiddenCount int
totalParsed := len(parsed)
if totalParsed > maxCodeLines {
// Check if last line is a footer (no line number) — keep it
var footerLines []codeLine
for totalParsed > 0 && parsed[totalParsed-1].lineNum == "" {
footerLines = append([]codeLine{parsed[totalParsed-1]}, footerLines...)
totalParsed--
}
if totalParsed > maxCodeLines {
codeHiddenCount = totalParsed - maxCodeLines
parsed = append(parsed[:maxCodeLines], footerLines...)
codeOnly = codeOnly[:maxCodeLines]
for _, fl := range footerLines {
codeOnly = append(codeOnly, fl.code)
}
} else {
// Restore — footer trimming was enough
parsed = parsed[:totalParsed]
parsed = append(parsed, footerLines...)
}
}
// Use herald's CodeBlock with line numbers
ty := herald.New(
herald.WithCodeLineNumbers(true),
herald.WithCodeFormatter(func(code, language string) string {
return syntaxHighlight(code, fileName)
}),
)
// Syntax highlight the code portion
highlighted := syntaxHighlight(strings.Join(codeOnly, "\n"), fileName)
highlightedLines := strings.Split(highlighted, "\n")
return ty.CodeBlock(content, lang)
// Layout
const codeIndent = " "
gutterWidth := max(maxNumWidth+2, 5)
codeWidth := max(width-gutterWidth-len(codeIndent), 20)
theme := getTheme()
gutterStyle := lipgloss.NewStyle().Foreground(theme.Muted).Background(theme.GutterBg).PaddingRight(1)
codeStyle := lipgloss.NewStyle().Background(theme.CodeBg).PaddingLeft(1)
var result []string
for i, p := range parsed {
// If this line has no line number, it's a metadata/footer line (e.g. truncation notice).
if p.lineNum == "" {
// Render footer lines with code background but no gutter
truncatedFooter := truncateLine(p.code, codeWidth-1) // account for PaddingLeft(1)
footer := codeStyle.Width(codeWidth).Render(truncatedFooter)
emptyGutter := gutterStyle.Width(gutterWidth).Render("")
result = append(result, codeIndent+lipgloss.JoinHorizontal(lipgloss.Top, emptyGutter, footer))
continue
}
gutter := gutterStyle.Width(gutterWidth).Render(p.lineNum)
var codePart string
if i < len(highlightedLines) {
codePart = highlightedLines[i]
} else {
codePart = p.code
}
// Truncate the (possibly ANSI-highlighted) line to fit within
// the code column, preventing lipgloss from wrapping it.
codePart = truncateLine(codePart, codeWidth-1) // account for PaddingLeft(1)
styledCode := codeStyle.Width(codeWidth).Render(codePart)
result = append(result, codeIndent+lipgloss.JoinHorizontal(lipgloss.Top, gutter, styledCode))
}
// Truncation hint
if codeHiddenCount > 0 {
hint := fmt.Sprintf("...(%d more lines)", codeHiddenCount)
emptyGutter := gutterStyle.Width(gutterWidth).Render("")
hintContent := codeStyle.Width(codeWidth).
Foreground(theme.Muted).Italic(true).Render(hint)
result = append(result, codeIndent+lipgloss.JoinHorizontal(lipgloss.Top, emptyGutter, hintContent))
}
return strings.Join(result, "\n")
}
// ---------------------------------------------------------------------------