diff --git a/cmd/root.go b/cmd/root.go index 7ba2f504..a5b7ef59 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -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) diff --git a/internal/ui/messages.go b/internal/ui/messages.go index 4b4438cf..25384343 100644 --- a/internal/ui/messages.go +++ b/internal/ui/messages.go @@ -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, "") || strings.Contains(result, "") { return parseBashOutput(result, GetTheme()) } diff --git a/internal/ui/model.go b/internal/ui/model.go index 6297fa99..0abf1b96 100644 --- a/internal/ui/model.go +++ b/internal/ui/model.go @@ -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) } } diff --git a/internal/ui/stream.go b/internal/ui/stream.go index 834bc2f4..928cd970 100644 --- a/internal/ui/stream.go +++ b/internal/ui/stream.go @@ -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. diff --git a/internal/ui/tool_renderers.go b/internal/ui/tool_renderers.go index a47ac5e6..420e86dd 100644 --- a/internal/ui/tool_renderers.go +++ b/internal/ui/tool_renderers.go @@ -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.