// This file renders assistant replies as Markdown for interactive terminals. // The line-oriented REPL streams model text token-by-token, but Markdown can // only be laid out once the whole block is known (a table or fenced code span // needs its full extent). So rendering is a turn-end concern: the caller buffers // the streamed text and calls RenderMarkdown once the assistant turn closes. // // Rendering is gated exactly like color (Enabled): only an interactive, // NO_COLOR-unset stdout gets styled output. Pipes, files, CI, and tests receive // the raw Markdown source unchanged, so machine consumers and golden tests are // unaffected. Any renderer failure also falls back to the raw source — pretty // output is never allowed to lose content. package ui import ( "strings" "sync" "github.com/charmbracelet/glamour" ) // mdRenderer is the lazily-built glamour renderer. Building it parses a style // and compiles a chroma lexer set, so it is created once and reused across // turns. A build failure leaves it nil, degrading to raw output. var ( mdOnce sync.Once mdRenderer *glamour.TermRenderer ) // initMarkdown builds the shared renderer on first use. It uses glamour's // auto style, which follows the terminal's dark/light background. // // WithWordWrap(0) disables glamour's hard word-wrap. That matters: with a fixed // wrap width glamour pads every line with trailing-space background cells out to // the full column count, so a three-line reply balloons into kilobytes of ANSI // noise (measured: ~8KB for a short block at width 100 vs. ~0.5KB unwrapped). // Disabling the wrap lets the terminal soft-wrap long lines itself and keeps the // rendered output tight — the REPL doesn't track terminal size anyway. func initMarkdown() { mdOnce.Do(func() { r, err := glamour.NewTermRenderer( glamour.WithAutoStyle(), glamour.WithWordWrap(0), ) if err != nil { return } mdRenderer = r }) } // RenderMarkdown returns src rendered as styled terminal Markdown when output // is an interactive terminal, and src unchanged otherwise. A nil/broken // renderer or a render error also returns src, so content is never dropped in // favor of styling. The returned string carries its own trailing newline from // glamour; callers should not add another. func RenderMarkdown(src string) string { if !Enabled() { return src } if strings.TrimSpace(src) == "" { return src } initMarkdown() if mdRenderer == nil { return src } out, err := mdRenderer.Render(src) if err != nil { return src } return out }