From 74f00244bedeb9936beda6884beca3934593552d Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Tue, 21 Apr 2026 20:42:53 +0300 Subject: [PATCH] fix(ui): wrap reasoning blocks to terminal width to prevent clipping - wrap thinking text in StreamComponent and render.ReasoningBlock - plumb width through renderer and streaming item paths - keeps style consistent with user/assistant blocks and avoids cut-off lines --- internal/ui/message_items.go | 2 +- internal/ui/messages.go | 2 +- internal/ui/render/blocks.go | 9 +++++++-- internal/ui/stream.go | 4 ++++ 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/internal/ui/message_items.go b/internal/ui/message_items.go index a7340673..7859b61d 100644 --- a/internal/ui/message_items.go +++ b/internal/ui/message_items.go @@ -156,7 +156,7 @@ func (s *StreamingMessageItem) Render(width int) string { durationMs = time.Since(s.startTime).Milliseconds() } ty := createTypography(style.GetTheme()) - rendered = render.ReasoningBlock(s.content, durationMs, ty, style.GetTheme()) + rendered = render.ReasoningBlock(s.content, durationMs, width, ty, style.GetTheme()) } else { // Render as assistant message rendered = render.AssistantBlock(s.content, width, style.GetTheme()) diff --git a/internal/ui/messages.go b/internal/ui/messages.go index 0b9fe368..6532c68e 100644 --- a/internal/ui/messages.go +++ b/internal/ui/messages.go @@ -178,7 +178,7 @@ func (r *MessageRenderer) RenderAssistantMessage(content string, timestamp time. // as live streaming: muted italic text with margin. This is used when resuming // sessions to display saved reasoning content. func (r *MessageRenderer) RenderReasoningBlock(content string, timestamp time.Time) UIMessage { - rendered := render.ReasoningBlock(content, 0, r.ty, style.GetTheme()) + rendered := render.ReasoningBlock(content, 0, r.width, r.ty, style.GetTheme()) return UIMessage{ Type: AssistantMessage, diff --git a/internal/ui/render/blocks.go b/internal/ui/render/blocks.go index c6cb0b48..280de551 100644 --- a/internal/ui/render/blocks.go +++ b/internal/ui/render/blocks.go @@ -63,14 +63,19 @@ func AssistantBlock(content string, width int, theme style.Theme) string { // ReasoningBlock renders a reasoning/thinking block with muted italic text. // If duration > 0, shows "Thought for Xs" label. Otherwise shows just "Thought". -func ReasoningBlock(content string, duration int64, ty *herald.Typography, theme style.Theme) string { +// 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 + // Match live streaming styling: muted italic text. Wrap before styling so + // ANSI sequences from italics don't interfere with width calculations. lines := strings.Split(strings.TrimRight(content, "\n"), "\n") contentStr := strings.TrimLeft(strings.Join(lines, "\n"), " \t\n") + if width > 4 { // mirror other blocks (User/Assistant) which subtract 4 + contentStr = lipgloss.Wrap(contentStr, width-4, "") + } mutedStyle := lipgloss.NewStyle().Foreground(theme.Muted) contentRendered := mutedStyle.Render(ty.Italic(contentStr)) diff --git a/internal/ui/stream.go b/internal/ui/stream.go index 5b8178fd..575e52e8 100644 --- a/internal/ui/stream.go +++ b/internal/ui/stream.go @@ -472,6 +472,10 @@ func (s *StreamComponent) renderReasoningBlock(reasoning string) string { // Main content using Italic with Muted color for visual distinction. content := strings.TrimLeft(strings.Join(lines, "\n"), " \t\n") + // Soft-wrap to the available width so long lines don't get cut off. + if s.width > 4 { + content = lipgloss.Wrap(content, s.width-4, "") + } theme := GetTheme() mutedStyle := lipgloss.NewStyle().Foreground(theme.Muted) parts = append(parts, mutedStyle.Render(s.ty.Italic(content)))