mirror of
https://github.com/mark3labs/kit.git
synced 2026-06-14 03:30:26 +00:00
Compact mode (#91)
* compact mode * tweaks * tweaks * tweaks * fix streaming in tool calls
This commit is contained in:
+13
-2
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user