// Package render provides pure rendering functions for message blocks. // These functions are stateless and can be used by both streaming and // historical message rendering paths, eliminating code duplication. package render import ( "fmt" "regexp" "strings" "charm.land/lipgloss/v2" "github.com/indaco/herald" "github.com/mark3labs/kit/internal/ui/style" ) // fileTokenPattern matches @file references in user text. Supports: // - @"path with spaces.txt" (quoted) // - @path/to/file.txt (unquoted, no spaces) var fileTokenPattern = regexp.MustCompile(`@"[^"]+"|@[^\s]+`) // UserBlock-related rendering helpers and herald typography. // HighlightFileTokens wraps @file tokens in the given text with the theme // accent color so they stand out visually in rendered user messages. func HighlightFileTokens(text string, theme style.Theme) string { accentStyle := style.GetCachedStyles().FileTokenAccent return fileTokenPattern.ReplaceAllStringFunc(text, func(token string) string { return accentStyle.Render(token) }) } // AssistantBlock renders an assistant message with markdown styling. func AssistantBlock(content string, width int, theme style.Theme) string { if strings.TrimSpace(content) == "" { return "" } rendered := style.ToMarkdown(content, width-4) return styleMarginBottom(theme, rendered) } // ReasoningBlock renders a reasoning/thinking block with muted italic text. // If duration > 0, shows "Thought for Xs" label. Otherwise shows just "Thought". // The width parameter controls soft-wrapping so long reasoning lines don't get cut off. func ReasoningBlock(content string, duration int64, width int, ty *herald.Typography, theme style.Theme) string { if strings.TrimSpace(content) == "" { return "" } // Match live streaming styling: muted italic text. lines := strings.Split(strings.TrimRight(content, "\n"), "\n") contentStr := strings.TrimLeft(strings.Join(lines, "\n"), " \t\n") if width > 4 { contentStr = wrapText(contentStr, width-4) } cs := style.GetCachedStyles() contentRendered := cs.Muted.Render(ty.Italic(contentStr)) // Build label based on duration if duration > 0 { var durationStr string if duration < 1000 { durationStr = fmt.Sprintf("%dms", duration) } else { durationStr = fmt.Sprintf("%.1fs", float64(duration)/1000) } labelPart := cs.VeryMuted.Render("Thought for ") durationPart := cs.Accent.Render(durationStr) label := labelPart + durationPart rendered := contentRendered + "\n" + label return styleMarginBottom(theme, rendered) } label := cs.VeryMuted.Render("Thought") rendered := contentRendered + "\n" + label return styleMarginBottom(theme, rendered) } // SystemBlock renders a system message with herald Note styling. func SystemBlock(content string, ty *herald.Typography, theme style.Theme) string { if strings.TrimSpace(content) == "" { content = "No content available" } rendered := ty.Note(content) return styleMarginBottom(theme, rendered) } // CustomBlock renders a message with herald Note styling and a custom label. // Content is rendered as markdown before being wrapped in the alert. This // creates a one-off Typography instance with the given label so callers // can use any title (e.g. "Help", "Warning") without changing the shared // typography's default "Info" label. func CustomBlock(content, label string, width int, theme style.Theme) string { if strings.TrimSpace(content) == "" { content = "No content available" } // Render markdown first — subtract 4 for the alert bar prefix ("│ "). mdWidth := max(width-4, 10) rendered := style.ToMarkdown(content, mdWidth) ty := herald.New( herald.WithPalette(herald.ColorPalette{ Primary: theme.Primary, Secondary: theme.Secondary, Tertiary: theme.Info, Accent: theme.Accent, Highlight: theme.Highlight, Muted: theme.Muted, Text: theme.Text, Surface: theme.Background, Base: theme.CodeBg, }), herald.WithAlertPalette(herald.AlertPalette{ Note: theme.Info, Tip: theme.Success, Important: theme.Accent, Warning: theme.Warning, Caution: theme.Error, }), herald.WithAlertLabel(herald.AlertNote, label), ) alertRendered := ty.Note(rendered) return styleMarginBottom(theme, alertRendered) } // ErrorBlock renders an error message with herald Caution styling. func ErrorBlock(errorMsg string, ty *herald.Typography, theme style.Theme) string { rendered := ty.Caution(errorMsg) return styleMarginBottom(theme, rendered) } // styleMarginBottom applies a 1-line margin bottom using the theme. func styleMarginBottom(theme style.Theme, content string) string { return style.GetCachedStyles().MarginBottom1.Render(content) } // wrapText soft-wraps a string to the given width using lipgloss, which is // ANSI-aware and preserves escape sequences across line breaks. func wrapText(s string, width int) string { return lipgloss.NewStyle().Width(width).Render(s) }