Compact mode (#91)

* compact mode

* tweaks

* tweaks

* tweaks

* fix streaming in tool calls
This commit is contained in:
Ed Zynda
2025-06-27 15:56:09 +03:00
committed by GitHub
parent 42af56963f
commit 76dd18030a
6 changed files with 603 additions and 33 deletions
+13 -2
View File
@@ -33,6 +33,7 @@ var (
noExitFlag bool
maxSteps int
streamFlag bool // Enable streaming output
compactMode bool // Enable compact output mode
scriptMCPConfig *config.Config // Used to override config in script mode
// Session management
@@ -166,6 +167,8 @@ func init() {
IntVar(&maxSteps, "max-steps", 0, "maximum number of agent steps (0 for unlimited)")
rootCmd.PersistentFlags().
BoolVar(&streamFlag, "stream", true, "enable streaming output for faster response display")
rootCmd.PersistentFlags().
BoolVar(&compactMode, "compact", false, "enable compact output mode without fancy styling")
// Session management flags
rootCmd.PersistentFlags().
@@ -196,6 +199,7 @@ func init() {
viper.BindPFlag("prompt", rootCmd.PersistentFlags().Lookup("prompt"))
viper.BindPFlag("max-steps", rootCmd.PersistentFlags().Lookup("max-steps"))
viper.BindPFlag("stream", rootCmd.PersistentFlags().Lookup("stream"))
viper.BindPFlag("compact", rootCmd.PersistentFlags().Lookup("compact"))
viper.BindPFlag("provider-url", rootCmd.PersistentFlags().Lookup("provider-url"))
viper.BindPFlag("provider-api-key", rootCmd.PersistentFlags().Lookup("provider-api-key"))
viper.BindPFlag("max-tokens", rootCmd.PersistentFlags().Lookup("max-tokens"))
@@ -301,7 +305,7 @@ func runNormalMode(ctx context.Context) error {
if strings.HasPrefix(viper.GetString("model"), "ollama:") && !quietFlag {
// Create a temporary CLI for the spinner
tempCli, tempErr := ui.NewCLI(viper.GetBool("debug"))
tempCli, tempErr := ui.NewCLI(viper.GetBool("debug"), viper.GetBool("compact"))
if tempErr == nil {
err = tempCli.ShowSpinner("Loading Ollama model...", func() error {
var agentErr error
@@ -336,10 +340,13 @@ func runNormalMode(ctx context.Context) error {
// Create CLI interface (skip if quiet mode)
var cli *ui.CLI
if !quietFlag {
cli, err = ui.NewCLI(viper.GetBool("debug"))
cli, err = ui.NewCLI(viper.GetBool("debug"), viper.GetBool("compact"))
if err != nil {
return fmt.Errorf("failed to create CLI: %v", err)
}
// Set the model name for consistent display
cli.SetModelName(modelName)
// Set up usage tracking for supported providers
if len(parts) == 2 {
@@ -660,6 +667,7 @@ func runAgenticStep(ctx context.Context, mcpAgent *agent.Agent, cli *ui.CLI, mes
if !streamingStarted {
cli.StartStreamingMessage(config.ModelName)
streamingStarted = true
streamingContent.Reset() // Reset content for new streaming session
}
// Accumulate content and update message
@@ -734,6 +742,9 @@ func runAgenticStep(ctx context.Context, mcpAgent *agent.Agent, cli *ui.CLI, mes
}
cli.DisplayToolMessage(toolName, toolArgs, resultContent, isError)
// Reset streaming state for next LLM call
responseWasStreamed = false
streamingStarted = false
// Start spinner again for next LLM call
currentSpinner = ui.NewSpinner("Thinking...")
currentSpinner.Start()
+2 -1
View File
@@ -486,6 +486,7 @@ func runScriptMode(ctx context.Context, mcpConfig *config.Config, prompt string,
}
finalDebug := viper.GetBool("debug") || mcpConfig.Debug
finalCompact := viper.GetBool("compact")
finalMaxSteps := viper.GetInt("max-steps")
if finalMaxSteps == 0 && mcpConfig.MaxSteps != 0 {
finalMaxSteps = mcpConfig.MaxSteps
@@ -582,7 +583,7 @@ func runScriptMode(ctx context.Context, mcpConfig *config.Config, prompt string,
// Create CLI interface (skip if quiet mode)
var cli *ui.CLI
if !quietFlag {
cli, err = ui.NewCLI(finalDebug)
cli, err = ui.NewCLI(finalDebug, finalCompact)
if err != nil {
return fmt.Errorf("failed to create CLI: %v", err)
}
+97 -20
View File
@@ -21,18 +21,24 @@ var (
// CLI handles the command line interface with improved message rendering
type CLI struct {
messageRenderer *MessageRenderer
compactRenderer *CompactRenderer // Add compact renderer
messageContainer *MessageContainer
usageTracker *UsageTracker
width int
height int
compactMode bool // Add compact mode flag
modelName string // Store current model name
}
// NewCLI creates a new CLI instance with message container
func NewCLI(debug bool) (*CLI, error) {
cli := &CLI{}
func NewCLI(debug bool, compact bool) (*CLI, error) {
cli := &CLI{
compactMode: compact,
}
cli.updateSize()
cli.messageRenderer = NewMessageRenderer(cli.width, debug)
cli.messageContainer = NewMessageContainer(cli.width, cli.height-4) // Reserve space for input and help
cli.compactRenderer = NewCompactRenderer(cli.width, debug)
cli.messageContainer = NewMessageContainer(cli.width, cli.height-4, compact) // Pass compact mode
return cli, nil
}
@@ -45,6 +51,14 @@ func (c *CLI) SetUsageTracker(tracker *UsageTracker) {
}
}
// SetModelName sets the current model name for the CLI
func (c *CLI) SetModelName(modelName string) {
c.modelName = modelName
if c.messageContainer != nil {
c.messageContainer.SetModelName(modelName)
}
}
// GetPrompt gets user input using the huh library with divider and padding
func (c *CLI) GetPrompt() (string, error) {
// Usage info is now displayed immediately after responses via DisplayUsageAfterResponse()
@@ -97,9 +111,14 @@ func (c *CLI) ShowSpinner(message string, action func() error) error {
return err
}
// DisplayUserMessage displays the user's message using the new renderer
// DisplayUserMessage displays the user's message using the appropriate renderer
func (c *CLI) DisplayUserMessage(message string) {
msg := c.messageRenderer.RenderUserMessage(message, time.Now())
var msg UIMessage
if c.compactMode {
msg = c.compactRenderer.RenderUserMessage(message, time.Now())
} else {
msg = c.messageRenderer.RenderUserMessage(message, time.Now())
}
c.messageContainer.AddMessage(msg)
c.displayContainer()
}
@@ -111,7 +130,12 @@ func (c *CLI) DisplayAssistantMessage(message string) error {
// DisplayAssistantMessageWithModel displays the assistant's message with model info
func (c *CLI) DisplayAssistantMessageWithModel(message, modelName string) error {
msg := c.messageRenderer.RenderAssistantMessage(message, time.Now(), modelName)
var msg UIMessage
if c.compactMode {
msg = c.compactRenderer.RenderAssistantMessage(message, time.Now(), modelName)
} else {
msg = c.messageRenderer.RenderAssistantMessage(message, time.Now(), modelName)
}
c.messageContainer.AddMessage(msg)
c.displayContainer()
return nil
@@ -119,7 +143,12 @@ func (c *CLI) DisplayAssistantMessageWithModel(message, modelName string) error
// DisplayToolCallMessage displays a tool call in progress
func (c *CLI) DisplayToolCallMessage(toolName, toolArgs string) {
msg := c.messageRenderer.RenderToolCallMessage(toolName, toolArgs, time.Now())
var msg UIMessage
if c.compactMode {
msg = c.compactRenderer.RenderToolCallMessage(toolName, toolArgs, time.Now())
} else {
msg = c.messageRenderer.RenderToolCallMessage(toolName, toolArgs, time.Now())
}
// Always display immediately - spinner management is handled externally
c.messageContainer.AddMessage(msg)
@@ -128,7 +157,12 @@ func (c *CLI) DisplayToolCallMessage(toolName, toolArgs string) {
// DisplayToolMessage displays a tool call message
func (c *CLI) DisplayToolMessage(toolName, toolArgs, toolResult string, isError bool) {
msg := c.messageRenderer.RenderToolMessage(toolName, toolArgs, toolResult, isError)
var msg UIMessage
if c.compactMode {
msg = c.compactRenderer.RenderToolMessage(toolName, toolArgs, toolResult, isError)
} else {
msg = c.messageRenderer.RenderToolMessage(toolName, toolArgs, toolResult, isError)
}
// Always display immediately - spinner management is handled externally
c.messageContainer.AddMessage(msg)
@@ -138,7 +172,12 @@ func (c *CLI) DisplayToolMessage(toolName, toolArgs, toolResult string, isError
// StartStreamingMessage starts a streaming assistant message
func (c *CLI) StartStreamingMessage(modelName string) {
// Add an empty assistant message that we'll update during streaming
msg := c.messageRenderer.RenderAssistantMessage("", time.Now(), modelName)
var msg UIMessage
if c.compactMode {
msg = c.compactRenderer.RenderAssistantMessage("", time.Now(), modelName)
} else {
msg = c.messageRenderer.RenderAssistantMessage("", time.Now(), modelName)
}
c.messageContainer.AddMessage(msg)
c.displayContainer()
}
@@ -150,30 +189,50 @@ func (c *CLI) UpdateStreamingMessage(content string) {
c.displayContainer()
}
// DisplayError displays an error message using the message component
// DisplayError displays an error message using the appropriate renderer
func (c *CLI) DisplayError(err error) {
msg := c.messageRenderer.RenderErrorMessage(err.Error(), time.Now())
var msg UIMessage
if c.compactMode {
msg = c.compactRenderer.RenderErrorMessage(err.Error(), time.Now())
} else {
msg = c.messageRenderer.RenderErrorMessage(err.Error(), time.Now())
}
c.messageContainer.AddMessage(msg)
c.displayContainer()
}
// DisplayInfo displays an informational message using the system message component
// DisplayInfo displays an informational message using the appropriate renderer
func (c *CLI) DisplayInfo(message string) {
msg := c.messageRenderer.RenderSystemMessage(message, time.Now())
var msg UIMessage
if c.compactMode {
msg = c.compactRenderer.RenderSystemMessage(message, time.Now())
} else {
msg = c.messageRenderer.RenderSystemMessage(message, time.Now())
}
c.messageContainer.AddMessage(msg)
c.displayContainer()
}
// DisplayCancellation displays a cancellation message
func (c *CLI) DisplayCancellation() {
msg := c.messageRenderer.RenderSystemMessage("Generation cancelled by user (ESC pressed)", time.Now())
var msg UIMessage
if c.compactMode {
msg = c.compactRenderer.RenderSystemMessage("Generation cancelled by user (ESC pressed)", time.Now())
} else {
msg = c.messageRenderer.RenderSystemMessage("Generation cancelled by user (ESC pressed)", time.Now())
}
c.messageContainer.AddMessage(msg)
c.displayContainer()
}
// DisplayDebugConfig displays configuration settings in debug mode using tool response block styling
// DisplayDebugConfig displays configuration settings using the appropriate renderer
func (c *CLI) DisplayDebugConfig(config map[string]any) {
msg := c.messageRenderer.RenderDebugConfigMessage(config, time.Now())
var msg UIMessage
if c.compactMode {
msg = c.compactRenderer.RenderDebugConfigMessage(config, time.Now())
} else {
msg = c.messageRenderer.RenderDebugConfigMessage(config, time.Now())
}
c.messageContainer.AddMessage(msg)
c.displayContainer()
}
@@ -242,15 +301,25 @@ func (c *CLI) DisplayServers(servers []string) {
// DisplayHistory displays conversation history using the message container
func (c *CLI) DisplayHistory(messages []*schema.Message) {
// Create a temporary container for history
historyContainer := NewMessageContainer(c.width, c.height-4)
historyContainer := NewMessageContainer(c.width, c.height-4, c.compactMode)
for _, msg := range messages {
switch msg.Role {
case schema.User:
uiMsg := c.messageRenderer.RenderUserMessage(msg.Content, time.Now())
var uiMsg UIMessage
if c.compactMode {
uiMsg = c.compactRenderer.RenderUserMessage(msg.Content, time.Now())
} else {
uiMsg = c.messageRenderer.RenderUserMessage(msg.Content, time.Now())
}
historyContainer.AddMessage(uiMsg)
case schema.Assistant:
uiMsg := c.messageRenderer.RenderAssistantMessage(msg.Content, time.Now(), "")
var uiMsg UIMessage
if c.compactMode {
uiMsg = c.compactRenderer.RenderAssistantMessage(msg.Content, time.Now(), c.modelName)
} else {
uiMsg = c.messageRenderer.RenderAssistantMessage(msg.Content, time.Now(), c.modelName)
}
historyContainer.AddMessage(uiMsg)
}
}
@@ -384,7 +453,12 @@ func (c *CLI) DisplayUsageStats() {
content.WriteString(fmt.Sprintf("**Session Total:** %d input + %d output tokens = $%.6f (%d requests)\n",
sessionStats.TotalInputTokens, sessionStats.TotalOutputTokens, sessionStats.TotalCost, sessionStats.RequestCount))
msg := c.messageRenderer.RenderSystemMessage(content.String(), time.Now())
var msg UIMessage
if c.compactMode {
msg = c.compactRenderer.RenderSystemMessage(content.String(), time.Now())
} else {
msg = c.messageRenderer.RenderSystemMessage(content.String(), time.Now())
}
c.messageContainer.AddMessage(msg)
c.displayContainer()
}
@@ -434,6 +508,9 @@ func (c *CLI) updateSize() {
if c.messageRenderer != nil {
c.messageRenderer.SetWidth(c.width)
}
if c.compactRenderer != nil {
c.compactRenderer.SetWidth(c.width)
}
if c.messageContainer != nil {
c.messageContainer.SetSize(c.width, c.height-4)
}
+398
View File
@@ -0,0 +1,398 @@
package ui
import (
"fmt"
"strings"
"time"
"github.com/charmbracelet/lipgloss"
)
// CompactRenderer handles rendering messages in compact format
type CompactRenderer struct {
width int
debug bool
}
// NewCompactRenderer creates a new compact message renderer
func NewCompactRenderer(width int, debug bool) *CompactRenderer {
return &CompactRenderer{
width: width,
debug: debug,
}
}
// SetWidth updates the renderer width
func (r *CompactRenderer) SetWidth(width int) {
r.width = width
}
// RenderUserMessage renders a user message in compact format
func (r *CompactRenderer) RenderUserMessage(content string, timestamp time.Time) UIMessage {
theme := getTheme()
symbol := lipgloss.NewStyle().Foreground(theme.Secondary).Render(">")
label := lipgloss.NewStyle().Foreground(theme.Secondary).Bold(true).Render("User")
// Format content for user messages (preserve formatting, no truncation)
compactContent := r.formatUserAssistantContent(content)
// Handle multi-line content
lines := strings.Split(compactContent, "\n")
var formattedLines []string
for i, line := range lines {
if i == 0 {
// First line includes symbol and label
formattedLines = append(formattedLines, fmt.Sprintf("%s %s %s", symbol, label, line))
} else {
// Subsequent lines without indentation for compact mode
formattedLines = append(formattedLines, line)
}
}
return UIMessage{
Type: UserMessage,
Content: strings.Join(formattedLines, "\n"),
Height: len(formattedLines),
Timestamp: timestamp,
}
}
// RenderAssistantMessage renders an assistant message in compact format
func (r *CompactRenderer) RenderAssistantMessage(content string, timestamp time.Time, modelName string) UIMessage {
theme := getTheme()
symbol := lipgloss.NewStyle().Foreground(theme.Primary).Render("<")
// Use the full model name, fallback to "Assistant" if empty
if modelName == "" {
modelName = "Assistant"
}
label := lipgloss.NewStyle().Foreground(theme.Primary).Bold(true).Render(modelName)
// Format content for assistant messages (preserve formatting, no truncation)
compactContent := r.formatUserAssistantContent(content)
if compactContent == "" {
compactContent = lipgloss.NewStyle().Foreground(theme.Muted).Italic(true).Render("(no output)")
}
// Handle multi-line content
lines := strings.Split(compactContent, "\n")
var formattedLines []string
for i, line := range lines {
if i == 0 {
// First line includes symbol and label
formattedLines = append(formattedLines, fmt.Sprintf("%s %s %s", symbol, label, line))
} else {
// Subsequent lines without indentation for compact mode
formattedLines = append(formattedLines, line)
}
}
return UIMessage{
Type: AssistantMessage,
Content: strings.Join(formattedLines, "\n"),
Height: len(formattedLines),
Timestamp: timestamp,
}
}
// RenderToolCallMessage renders a tool call in progress in compact format
func (r *CompactRenderer) RenderToolCallMessage(toolName, toolArgs string, timestamp time.Time) UIMessage {
theme := getTheme()
symbol := lipgloss.NewStyle().Foreground(theme.Tool).Render("[")
label := lipgloss.NewStyle().Foreground(theme.Tool).Bold(true).Render(toolName)
// Format args for compact display
argsDisplay := r.formatToolArgs(toolArgs)
if argsDisplay != "" {
argsDisplay = lipgloss.NewStyle().Foreground(theme.Muted).Render(argsDisplay)
}
line := fmt.Sprintf("%s %s %s", symbol, label, argsDisplay)
return UIMessage{
Type: ToolCallMessage,
Content: line,
Height: 1,
Timestamp: timestamp,
}
}
// RenderToolMessage renders a tool result in compact format
func (r *CompactRenderer) RenderToolMessage(toolName, toolArgs, toolResult string, isError bool) UIMessage {
theme := getTheme()
symbol := lipgloss.NewStyle().Foreground(theme.Muted).Render("]")
// Determine result type and styling
var label string
var content string
var labelText string
if isError {
labelText = "Error"
label = lipgloss.NewStyle().Foreground(theme.Muted).Bold(true).Render(labelText)
content = lipgloss.NewStyle().Foreground(theme.Muted).Render(r.formatToolResult(toolResult))
} else {
// Determine result type based on tool and content
labelText = r.determineResultType(toolName, toolResult)
label = lipgloss.NewStyle().Foreground(theme.Muted).Bold(true).Render(labelText)
content = lipgloss.NewStyle().Foreground(theme.Muted).Render(r.formatToolResult(toolResult))
if r.formatToolResult(toolResult) == "" {
content = lipgloss.NewStyle().Foreground(theme.Muted).Italic(true).Render("(no output)")
}
}
// Handle multi-line tool results
contentLines := strings.Split(content, "\n")
var formattedLines []string
for i, line := range contentLines {
if i == 0 {
// First line includes symbol and label
formattedLines = append(formattedLines, fmt.Sprintf("%s %s %s", symbol, label, line))
} else {
// Subsequent lines without indentation for compact mode
formattedLines = append(formattedLines, line)
}
}
return UIMessage{
Type: ToolMessage,
Content: strings.Join(formattedLines, "\n"),
Height: len(formattedLines),
}
}
// RenderSystemMessage renders a system message in compact format
func (r *CompactRenderer) RenderSystemMessage(content string, timestamp time.Time) UIMessage {
theme := getTheme()
symbol := lipgloss.NewStyle().Foreground(theme.System).Render("*")
label := lipgloss.NewStyle().Foreground(theme.System).Bold(true).Render("System")
compactContent := r.formatCompactContent(content)
line := fmt.Sprintf("%s %-8s %s", symbol, label, compactContent)
return UIMessage{
Type: SystemMessage,
Content: line,
Height: 1,
Timestamp: timestamp,
}
}
// RenderErrorMessage renders an error message in compact format
func (r *CompactRenderer) RenderErrorMessage(errorMsg string, timestamp time.Time) UIMessage {
theme := getTheme()
symbol := lipgloss.NewStyle().Foreground(theme.Error).Render("!")
label := lipgloss.NewStyle().Foreground(theme.Error).Bold(true).Render("Error")
compactContent := lipgloss.NewStyle().Foreground(theme.Error).Render(r.formatCompactContent(errorMsg))
line := fmt.Sprintf("%s %-8s %s", symbol, label, compactContent)
return UIMessage{
Type: ErrorMessage,
Content: line,
Height: 1,
Timestamp: timestamp,
}
}
// RenderDebugConfigMessage renders debug config in compact format
func (r *CompactRenderer) RenderDebugConfigMessage(config map[string]any, timestamp time.Time) UIMessage {
theme := getTheme()
symbol := lipgloss.NewStyle().Foreground(theme.Tool).Render("*")
label := lipgloss.NewStyle().Foreground(theme.Tool).Bold(true).Render("Debug")
// Format config as compact key=value pairs
var configPairs []string
for key, value := range config {
if value != nil {
configPairs = append(configPairs, fmt.Sprintf("%s=%v", key, value))
}
}
content := strings.Join(configPairs, ", ")
if len(content) > r.width-20 {
content = content[:r.width-23] + "..."
}
line := fmt.Sprintf("%s %-8s %s", symbol, label, content)
return UIMessage{
Type: SystemMessage,
Content: line,
Height: 1,
Timestamp: timestamp,
}
}
// formatCompactContent formats content for compact single-line display
func (r *CompactRenderer) formatCompactContent(content string) string {
if content == "" {
return ""
}
// Remove markdown formatting for compact display
content = strings.ReplaceAll(content, "\n", " ")
content = strings.ReplaceAll(content, "\t", " ")
// Collapse multiple spaces
for strings.Contains(content, " ") {
content = strings.ReplaceAll(content, " ", " ")
}
content = strings.TrimSpace(content)
// Truncate if too long (unless in debug mode)
maxLen := r.width - 28 // Reserve space for symbol and label more conservatively
if maxLen < 40 {
maxLen = 40 // Minimum width for readability
}
if !r.debug && len(content) > maxLen {
content = content[:maxLen-3] + "..."
}
return content
}
// formatUserAssistantContent formats user and assistant content using glamour markdown rendering
func (r *CompactRenderer) formatUserAssistantContent(content string) string {
if content == "" {
return ""
}
// Calculate available width more conservatively
// Account for: symbol (1) + spaces (2) + label (up to 20 chars) + space (1) + margin (4)
availableWidth := r.width - 28
if availableWidth < 40 {
availableWidth = 40 // Minimum width for readability
}
// Use glamour to render markdown content with proper width
rendered := toMarkdown(content, availableWidth)
return strings.TrimSuffix(rendered, "\n")
}
// wrapText wraps text to the specified width, preserving existing line breaks
func (r *CompactRenderer) wrapText(text string, width int) string {
if width <= 0 {
return text
}
lines := strings.Split(text, "\n")
var wrappedLines []string
for _, line := range lines {
if len(line) <= width {
wrappedLines = append(wrappedLines, line)
continue
}
// Wrap long lines
words := strings.Fields(line)
if len(words) == 0 {
wrappedLines = append(wrappedLines, line)
continue
}
currentLine := ""
for _, word := range words {
// If adding this word would exceed the width, start a new line
if len(currentLine)+len(word)+1 > width && currentLine != "" {
wrappedLines = append(wrappedLines, currentLine)
currentLine = word
} else {
if currentLine == "" {
currentLine = word
} else {
currentLine += " " + word
}
}
}
if currentLine != "" {
wrappedLines = append(wrappedLines, currentLine)
}
}
return strings.Join(wrappedLines, "\n")
}
// formatToolArgs formats tool arguments for compact display
func (r *CompactRenderer) formatToolArgs(args string) string {
if args == "" || args == "{}" {
return ""
}
// Remove JSON braces and format compactly
args = strings.TrimSpace(args)
if strings.HasPrefix(args, "{") && strings.HasSuffix(args, "}") {
args = strings.TrimPrefix(args, "{")
args = strings.TrimSuffix(args, "}")
args = strings.TrimSpace(args)
}
// Remove quotes around simple values
args = strings.ReplaceAll(args, `"`, "")
// Remove parameter names (e.g., "command: ls" -> "ls", "path: /home" -> "/home")
// Look for pattern "key: value" and extract just the value
if colonIndex := strings.Index(args, ":"); colonIndex != -1 {
args = strings.TrimSpace(args[colonIndex+1:])
}
return r.formatCompactContent(args)
}
// formatToolResult formats tool results preserving formatting but limiting to 5 lines
func (r *CompactRenderer) formatToolResult(result string) string {
if result == "" {
return ""
}
// Calculate available width more conservatively
availableWidth := r.width - 28
if availableWidth < 40 {
availableWidth = 40 // Minimum width for readability
}
// First wrap the text to prevent long lines (tool results are usually plain text, not markdown)
wrappedResult := r.wrapText(result, availableWidth)
// Then limit to 5 lines
lines := strings.Split(wrappedResult, "\n")
if len(lines) > 5 {
lines = lines[:5]
// Add truncation indicator
if len(lines) == 5 && lines[4] != "" {
lines[4] = lines[4] + "..."
} else {
lines = append(lines, "...")
}
}
return strings.Join(lines, "\n")
}
// determineResultType determines the display type for tool results
func (r *CompactRenderer) determineResultType(toolName, result string) string {
toolName = strings.ToLower(toolName)
switch {
case strings.Contains(toolName, "read"):
return "Text"
case strings.Contains(toolName, "write"):
return "Write"
case strings.Contains(toolName, "bash") || strings.Contains(toolName, "command"):
return "Bash"
case strings.Contains(toolName, "list") || strings.Contains(toolName, "ls"):
return "List"
case strings.Contains(toolName, "search") || strings.Contains(toolName, "grep"):
return "Search"
case strings.Contains(toolName, "fetch") || strings.Contains(toolName, "http"):
return "Fetch"
default:
return "Result"
}
}
+34
View File
@@ -1,6 +1,8 @@
package ui
import (
"fmt"
"github.com/charmbracelet/lipgloss"
)
@@ -210,3 +212,35 @@ func CreateGradientText(text string, startColor, endColor lipgloss.AdaptiveColor
Bold(true).
Render(text)
}
// Compact styling utilities
// StyleCompactSymbol creates a styled symbol for compact mode
func StyleCompactSymbol(symbol string, color lipgloss.AdaptiveColor) lipgloss.Style {
return lipgloss.NewStyle().
Foreground(color).
Bold(true)
}
// StyleCompactLabel creates a styled label for compact mode
func StyleCompactLabel(color lipgloss.AdaptiveColor) lipgloss.Style {
return lipgloss.NewStyle().
Foreground(color).
Bold(true).
Width(8)
}
// StyleCompactContent creates basic content styling for compact mode
func StyleCompactContent(color lipgloss.AdaptiveColor) lipgloss.Style {
return lipgloss.NewStyle().
Foreground(color)
}
// FormatCompactLine formats a complete compact line with consistent spacing
func FormatCompactLine(symbol, label, content string, symbolColor, labelColor, contentColor lipgloss.AdaptiveColor) string {
styledSymbol := StyleCompactSymbol(symbol, symbolColor).Render(symbol)
styledLabel := StyleCompactLabel(labelColor).Render(label)
styledContent := StyleCompactContent(contentColor).Render(content)
return fmt.Sprintf("%s %-8s %s", styledSymbol, styledLabel, styledContent)
}
+59 -10
View File
@@ -516,17 +516,20 @@ func (r *MessageRenderer) renderMarkdown(content string, width int) string {
// MessageContainer wraps multiple messages in a container
type MessageContainer struct {
messages []UIMessage
width int
height int
messages []UIMessage
width int
height int
compactMode bool // Add compact mode flag
modelName string // Store current model name
}
// NewMessageContainer creates a new message container
func NewMessageContainer(width, height int) *MessageContainer {
func NewMessageContainer(width, height int, compact bool) *MessageContainer {
return &MessageContainer{
messages: make([]UIMessage, 0),
width: width,
height: height,
messages: make([]UIMessage, 0),
width: width,
height: height,
compactMode: compact,
}
}
@@ -535,6 +538,11 @@ func (c *MessageContainer) AddMessage(msg UIMessage) {
c.messages = append(c.messages, msg)
}
// SetModelName sets the current model name for the container
func (c *MessageContainer) SetModelName(modelName string) {
c.modelName = modelName
}
// UpdateLastMessage updates the content of the last message efficiently
func (c *MessageContainer) UpdateLastMessage(content string) {
if len(c.messages) == 0 {
@@ -546,9 +554,15 @@ func (c *MessageContainer) UpdateLastMessage(content string) {
// Only re-render if content actually changed and it's an assistant message
if lastMsg.Type == AssistantMessage {
// Create a new renderer to update the message
renderer := NewMessageRenderer(c.width, false)
newMsg := renderer.RenderAssistantMessage(content, lastMsg.Timestamp, "")
// Create appropriate renderer based on compact mode
var newMsg UIMessage
if c.compactMode {
compactRenderer := NewCompactRenderer(c.width, false)
newMsg = compactRenderer.RenderAssistantMessage(content, lastMsg.Timestamp, c.modelName)
} else {
renderer := NewMessageRenderer(c.width, false)
newMsg = renderer.RenderAssistantMessage(content, lastMsg.Timestamp, c.modelName)
}
c.messages[lastIdx] = newMsg
}
}
@@ -567,9 +581,16 @@ func (c *MessageContainer) SetSize(width, height int) {
// Render renders all messages in the container
func (c *MessageContainer) Render() string {
if len(c.messages) == 0 {
if c.compactMode {
return c.renderCompactEmptyState()
}
return c.renderEmptyState()
}
if c.compactMode {
return c.renderCompactMessages()
}
var parts []string
for i, msg := range c.messages {
@@ -665,3 +686,31 @@ func (c *MessageContainer) renderEmptyState() string {
AlignVertical(lipgloss.Center).
Render(welcomeContent)
}
// renderCompactMessages renders messages in compact format
func (c *MessageContainer) renderCompactMessages() string {
var lines []string
for _, msg := range c.messages {
lines = append(lines, msg.Content)
}
return strings.Join(lines, "\n") + "\n"
}
// renderCompactEmptyState renders a simple empty state for compact mode
func (c *MessageContainer) renderCompactEmptyState() string {
theme := getTheme()
// Simple compact welcome
welcome := lipgloss.NewStyle().
Foreground(theme.System).
Bold(true).
Render("MCPHost - AI Assistant with MCP Tools")
help := lipgloss.NewStyle().
Foreground(theme.Muted).
Render("Type your message or /help for commands")
return fmt.Sprintf("%s\n%s\n\n", welcome, help)
}