refactor(ui): improve message spacing and styling consistency

- Add bottom margin to startup header (KVGroup)
- Add bottom margin to thinking/reasoning blocks
- Fix thinking block footer to appear on new line without extra spacing
- Update spawn_subagent tool output to use bash-style formatting
- Add blank line after extension startup messages for visual separation
This commit is contained in:
Ed Zynda
2026-03-27 21:15:41 +03:00
parent d4f27bc912
commit b6ecc36ea1
5 changed files with 86 additions and 41 deletions
+1
View File
@@ -1586,6 +1586,7 @@ func runInteractiveModeBubbleTea(_ context.Context, appInstance *app.App, modelN
for _, msg := range startupExtensionMessages {
fmt.Println(msg)
}
fmt.Println()
}
program := tea.NewProgram(appModel)
+21 -11
View File
@@ -43,14 +43,13 @@ type UIMessage struct {
// toolDisplayNames maps raw tool names to human-friendly display names.
var toolDisplayNames = map[string]string{
"bash": "Bash",
"read": "Read",
"write": "Write",
"edit": "Edit",
"grep": "Grep",
"find": "Find",
"ls": "Ls",
"run_shell_cmd": "Bash",
"bash": "Bash",
"read": "Read",
"write": "Write",
"edit": "Edit",
"grep": "Grep",
"find": "Find",
"ls": "Ls",
}
// getTheme returns the current theme (helper for compact_renderer.go)
@@ -349,16 +348,27 @@ func (r *MessageRenderer) RenderToolMessage(toolName, toolArgs, toolResult strin
}
var icon string
iconColor := GetTheme().Success
if isError {
icon = "×"
iconColor = GetTheme().Error
} else {
icon = "✓"
}
// Style the tool name with color
theme := GetTheme()
nameColor := theme.Info
if isError {
nameColor = theme.Error
}
styledName := lipgloss.NewStyle().Foreground(nameColor).Bold(true).Render(displayName)
styledIcon := lipgloss.NewStyle().Foreground(iconColor).Render(icon)
// Build the content: icon + name + params on first line, then body
headerLine := icon + " " + displayName
headerLine := styledIcon + " " + styledName
if params != "" {
headerLine += " " + params
headerLine += " " + lipgloss.NewStyle().Foreground(theme.Muted).Render(params)
}
// Get body content
@@ -433,7 +443,7 @@ func (r *MessageRenderer) formatToolResult(toolName, result string) string {
}
if strings.Contains(toolName, "bash") || strings.Contains(toolName, "command") ||
strings.Contains(toolName, "shell") || toolName == "run_shell_cmd" {
strings.Contains(toolName, "shell") {
if strings.Contains(result, "<stdout>") || strings.Contains(result, "<stderr>") {
return parseBashOutput(result, GetTheme())
}
+1
View File
@@ -832,6 +832,7 @@ func (m *AppModel) PrintStartupInfo() {
if len(pairs) > 0 {
rendered := ty.KVGroup(pairs)
rendered = lipgloss.NewStyle().MarginBottom(1).Render(rendered)
fmt.Println(rendered)
}
}
+12 -3
View File
@@ -537,7 +537,7 @@ func (s *StreamComponent) renderReasoningBlock(reasoning string) string {
content := strings.Join(lines, "\n")
parts = append(parts, s.ty.Blockquote(content))
// Duration footer.
// Duration footer with indentation.
var duration time.Duration
if s.reasoningDuration > 0 {
duration = s.reasoningDuration
@@ -551,11 +551,20 @@ func (s *StreamComponent) renderReasoningBlock(reasoning string) string {
} else {
durationStr = fmt.Sprintf("%.1fs", duration.Seconds())
}
footer := s.ty.Small(fmt.Sprintf("Thought for %s", durationStr))
footer := lipgloss.NewStyle().PaddingLeft(2).Render(s.ty.Small(fmt.Sprintf("Thought for %s", durationStr)))
parts = append(parts, footer)
}
return s.ty.Compose(parts...)
// Concatenate parts with newline between blockquote and footer
var result string
if len(parts) == 1 {
result = parts[0]
} else if len(parts) == 2 {
result = parts[0] + "\n" + parts[1]
} else {
result = strings.Join(parts, "\n")
}
return lipgloss.NewStyle().MarginBottom(1).Render(result)
}
// SetThinkingVisible sets whether reasoning blocks are shown or collapsed.
+51 -27
View File
@@ -46,7 +46,7 @@ func renderToolBody(toolName, toolArgs, toolResult string, width int) string {
if body := renderWriteBody(toolArgs, toolResult, width); body != "" {
return body
}
case toolName == "bash" || toolName == "run_shell_cmd" ||
case toolName == "bash" || toolName == "grep" || toolName == "find" ||
strings.Contains(toolName, "shell") || strings.Contains(toolName, "command"):
if body := renderBashBody(toolResult, width); body != "" {
return body
@@ -777,7 +777,7 @@ func renderToolBodyCompact(toolName, toolArgs, toolResult string, width int) str
return renderReadCompact(toolResult)
case toolName == "write":
return renderWriteCompact(toolArgs)
case toolName == "bash" || toolName == "run_shell_cmd" ||
case toolName == "bash" || toolName == "grep" || toolName == "find" ||
strings.Contains(toolName, "shell") || strings.Contains(toolName, "command"):
return renderBashCompact(toolResult, width)
case toolName == "spawn_subagent":
@@ -939,8 +939,8 @@ func renderBashCompact(toolResult string, width int) string {
// Subagent tool renderers — show only summary, not full output
// ---------------------------------------------------------------------------
// renderSubagentBody renders a clean summary of subagent results.
// Extracts timing/token info and shows only a brief summary instead of raw output.
// renderSubagentBody renders a clean summary of subagent results with bash-style
// background styling for consistency with other tools.
func renderSubagentBody(toolResult string, width int) string {
theme := getTheme()
result := strings.TrimSpace(toolResult)
@@ -960,9 +960,19 @@ func renderSubagentBody(toolResult string, width int) string {
// First line is always the status summary
statusLine := lines[0]
// Build a clean summary
var summary strings.Builder
summary.WriteString(lipgloss.NewStyle().Foreground(theme.Muted).Render(statusLine))
// Build content lines for display with bash-style background
outputStyle := lipgloss.NewStyle().Background(theme.CodeBg).PaddingLeft(1)
errorStyle := lipgloss.NewStyle().Foreground(theme.Error).Background(theme.CodeBg).PaddingLeft(1)
const lineIndent = " "
lineWidth := max(width-len(lineIndent), 20)
maxLineChars := lineWidth - 1 // account for PaddingLeft(1)
var contentLines []string
// Add status line
styledStatus := outputStyle.Width(lineWidth).Render(truncateLine(statusLine, maxLineChars))
contentLines = append(contentLines, lineIndent+styledStatus)
// For successful results, extract a brief preview of the actual result
if strings.Contains(statusLine, "successfully") {
@@ -970,25 +980,45 @@ func renderSubagentBody(toolResult string, width int) string {
if _, resultContent, found := strings.Cut(result, "Result:\n"); found {
resultContent = strings.TrimSpace(resultContent)
if resultContent != "" {
// Show first 3 meaningful lines as preview
preview := extractSubagentPreview(resultContent, 3, width-4)
if preview != "" {
summary.WriteString("\n\n")
summary.WriteString(lipgloss.NewStyle().
Foreground(theme.Muted).
Italic(true).
Render(preview))
// Show first few meaningful lines as preview
previewLines := extractSubagentPreviewLines(resultContent, 5, maxLineChars)
if len(previewLines) > 0 {
// Add blank separator line
blankLine := outputStyle.Width(lineWidth).Render("")
contentLines = append(contentLines, lineIndent+blankLine)
for _, line := range previewLines {
styled := outputStyle.Width(lineWidth).Render(line)
contentLines = append(contentLines, lineIndent+styled)
}
}
}
}
} else {
// For failed results, show error info
if _, errorContent, found := strings.Cut(result, "Error:\n"); found {
errorContent = strings.TrimSpace(errorContent)
if errorContent != "" {
previewLines := extractSubagentPreviewLines(errorContent, 3, maxLineChars)
if len(previewLines) > 0 {
blankLine := outputStyle.Width(lineWidth).Render("")
contentLines = append(contentLines, lineIndent+blankLine)
for _, line := range previewLines {
styled := errorStyle.Width(lineWidth).Render(line)
contentLines = append(contentLines, lineIndent+styled)
}
}
}
}
}
return summary.String()
return strings.Join(contentLines, "\n")
}
// extractSubagentPreview extracts the first N non-empty lines from content,
// truncating each line to maxWidth.
func extractSubagentPreview(content string, maxLines, maxWidth int) string {
// extractSubagentPreviewLines extracts the first N non-empty lines from content,
// truncating each line to maxWidth. Returns as a slice of strings.
func extractSubagentPreviewLines(content string, maxLines, maxWidth int) []string {
lines := strings.Split(content, "\n")
var preview []string
@@ -1007,12 +1037,6 @@ func extractSubagentPreview(content string, maxLines, maxWidth int) string {
}
}
if len(preview) == 0 {
return ""
}
result := strings.Join(preview, "\n")
// Count remaining lines for "more" indicator
totalLines := 0
for _, line := range lines {
@@ -1021,10 +1045,10 @@ func extractSubagentPreview(content string, maxLines, maxWidth int) string {
}
}
if totalLines > maxLines {
result += fmt.Sprintf("\n...(%d more lines)", totalLines-maxLines)
preview = append(preview, fmt.Sprintf("...(%d more lines)", totalLines-maxLines))
}
return result
return preview
}
// renderSubagentCompact returns a brief one-line summary for subagent results.