Merge branch 'main' of github.com:mark3labs/mcphost

This commit is contained in:
Ed Zynda
2025-06-25 18:18:44 +03:00
11 changed files with 798 additions and 295 deletions
+1 -1
View File
@@ -318,7 +318,7 @@ func runNormalMode(ctx context.Context) error {
isOAuth = true
}
}
usageTracker := ui.NewUsageTracker(modelInfo, provider, 80, isOAuth) // Will be updated with actual width
cli.SetUsageTracker(usageTracker)
}
+9 -9
View File
@@ -29,22 +29,22 @@ const (
// resolveModelAlias resolves model aliases to their full names using the registry
func resolveModelAlias(provider, modelName string) string {
registry := GetGlobalRegistry()
// Common alias patterns for Anthropic models - using Claude 4 as the latest/default
aliasMap := map[string]string{
// Claude 4 models (latest and most capable)
"claude-opus-latest": "claude-opus-4-20250514",
"claude-sonnet-latest": "claude-sonnet-4-20250514",
"claude-4-opus-latest": "claude-opus-4-20250514",
"claude-4-sonnet-latest": "claude-sonnet-4-20250514",
"claude-opus-latest": "claude-opus-4-20250514",
"claude-sonnet-latest": "claude-sonnet-4-20250514",
"claude-4-opus-latest": "claude-opus-4-20250514",
"claude-4-sonnet-latest": "claude-sonnet-4-20250514",
// Claude 3.x models for backward compatibility
"claude-3-5-haiku-latest": "claude-3-5-haiku-20241022",
"claude-3-5-sonnet-latest": "claude-3-5-sonnet-20241022",
"claude-3-5-sonnet-latest": "claude-3-5-sonnet-20241022",
"claude-3-7-sonnet-latest": "claude-3-7-sonnet-20250219",
"claude-3-opus-latest": "claude-3-opus-20240229",
}
// Check if it's a known alias
if resolved, exists := aliasMap[modelName]; exists {
// Verify the resolved model exists in the registry
@@ -52,7 +52,7 @@ func resolveModelAlias(provider, modelName string) string {
return resolved
}
}
// Return original if no alias found or resolved model doesn't exist
return modelName
}
+178
View File
@@ -0,0 +1,178 @@
package ui
import (
"github.com/charmbracelet/lipgloss"
)
// blockRenderer handles rendering of content blocks with configurable options
type blockRenderer struct {
align *lipgloss.Position
borderColor *lipgloss.AdaptiveColor
fullWidth bool
paddingTop int
paddingBottom int
paddingLeft int
paddingRight int
marginTop int
marginBottom int
width int
}
// renderingOption configures block rendering
type renderingOption func(*blockRenderer)
// WithFullWidth makes the block take full available width
func WithFullWidth() renderingOption {
return func(c *blockRenderer) {
c.fullWidth = true
}
}
// WithAlign sets the horizontal alignment of the block
func WithAlign(align lipgloss.Position) renderingOption {
return func(c *blockRenderer) {
c.align = &align
}
}
// WithBorderColor sets the border color
func WithBorderColor(color lipgloss.AdaptiveColor) renderingOption {
return func(c *blockRenderer) {
c.borderColor = &color
}
}
// WithMarginTop sets the top margin
func WithMarginTop(margin int) renderingOption {
return func(c *blockRenderer) {
c.marginTop = margin
}
}
// WithMarginBottom sets the bottom margin
func WithMarginBottom(margin int) renderingOption {
return func(c *blockRenderer) {
c.marginBottom = margin
}
}
// WithPaddingLeft sets the left padding
func WithPaddingLeft(padding int) renderingOption {
return func(c *blockRenderer) {
c.paddingLeft = padding
}
}
// WithPaddingRight sets the right padding
func WithPaddingRight(padding int) renderingOption {
return func(c *blockRenderer) {
c.paddingRight = padding
}
}
// WithPaddingTop sets the top padding
func WithPaddingTop(padding int) renderingOption {
return func(c *blockRenderer) {
c.paddingTop = padding
}
}
// WithPaddingBottom sets the bottom padding
func WithPaddingBottom(padding int) renderingOption {
return func(c *blockRenderer) {
c.paddingBottom = padding
}
}
// WithWidth sets a specific width for the block
func WithWidth(width int) renderingOption {
return func(c *blockRenderer) {
c.width = width
}
}
// renderContentBlock renders content with configurable styling options
func renderContentBlock(content string, containerWidth int, options ...renderingOption) string {
renderer := &blockRenderer{
fullWidth: false,
paddingTop: 1,
paddingBottom: 1,
paddingLeft: 2,
paddingRight: 2,
width: containerWidth,
}
for _, option := range options {
option(renderer)
}
theme := GetTheme()
style := lipgloss.NewStyle().
PaddingTop(renderer.paddingTop).
PaddingBottom(renderer.paddingBottom).
PaddingLeft(renderer.paddingLeft).
PaddingRight(renderer.paddingRight).
Foreground(theme.Text).
BorderStyle(lipgloss.ThickBorder())
align := lipgloss.Left
if renderer.align != nil {
align = *renderer.align
}
// Default to transparent/no border color
borderColor := lipgloss.AdaptiveColor{Light: "", Dark: ""}
if renderer.borderColor != nil {
borderColor = *renderer.borderColor
}
// Very muted color for the opposite border
mutedOppositeBorder := lipgloss.AdaptiveColor{
Light: "#F3F4F6", // Very light gray, barely visible
Dark: "#1F2937", // Very dark gray, barely visible
}
switch align {
case lipgloss.Left:
style = style.
BorderLeft(true).
BorderRight(true).
AlignHorizontal(align).
BorderLeftForeground(borderColor).
BorderRightForeground(mutedOppositeBorder)
case lipgloss.Right:
style = style.
BorderRight(true).
BorderLeft(true).
AlignHorizontal(align).
BorderRightForeground(borderColor).
BorderLeftForeground(mutedOppositeBorder)
}
if renderer.fullWidth {
style = style.Width(renderer.width)
}
content = style.Render(content)
// Place the content horizontally with proper background
content = lipgloss.PlaceHorizontal(
renderer.width,
align,
content,
)
// Add margins
if renderer.marginTop > 0 {
for range renderer.marginTop {
content = "\n" + content
}
}
if renderer.marginBottom > 0 {
for range renderer.marginBottom {
content = content + "\n"
}
}
return content
}
+24 -8
View File
@@ -51,20 +51,27 @@ func (c *CLI) GetPrompt() (string, error) {
if c.usageTracker != nil {
usageInfo := c.usageTracker.RenderUsageInfo()
if usageInfo != "" {
fmt.Print(usageInfo)
paddedUsage := lipgloss.NewStyle().
PaddingLeft(2).
Render(usageInfo)
fmt.Print(paddedUsage)
}
}
// Create a divider before the input
// Create an enhanced divider with gradient effect
theme := GetTheme()
dividerStyle := lipgloss.NewStyle().
Width(c.width).
BorderTop(true).
BorderStyle(lipgloss.NormalBorder()).
BorderForeground(mutedColor).
BorderStyle(lipgloss.Border{
Top: "━",
}).
BorderForeground(theme.Border).
MarginTop(1).
MarginBottom(1)
MarginBottom(1).
PaddingLeft(2)
// Render the divider
// Render the enhanced input section
fmt.Print(dividerStyle.Render(""))
var prompt string
@@ -312,7 +319,14 @@ func (c *CLI) ClearMessages() {
func (c *CLI) displayContainer() {
// Clear screen and display messages
fmt.Print("\033[2J\033[H") // Clear screen and move cursor to top
fmt.Print(c.messageContainer.Render())
// Add left padding to the entire container
content := c.messageContainer.Render()
paddedContent := lipgloss.NewStyle().
PaddingLeft(2).
Render(content)
fmt.Print(paddedContent)
}
// UpdateUsage updates the usage tracker with token counts and costs
@@ -393,7 +407,9 @@ func (c *CLI) updateSize() {
return
}
c.width = width
// Add left and right padding (4 characters total: 2 on each side)
paddingTotal := 4
c.width = width - paddingTotal
c.height = height
// Update renderers if they exist
+212
View File
@@ -0,0 +1,212 @@
package ui
import (
"github.com/charmbracelet/lipgloss"
)
// Enhanced styling utilities and theme definitions
// Global theme instance
var currentTheme = DefaultTheme()
// GetTheme returns the current theme
func GetTheme() Theme {
return currentTheme
}
// SetTheme sets the current theme
func SetTheme(theme Theme) {
currentTheme = theme
}
// Theme represents a complete UI theme
type Theme struct {
Primary lipgloss.AdaptiveColor
Secondary lipgloss.AdaptiveColor
Success lipgloss.AdaptiveColor
Warning lipgloss.AdaptiveColor
Error lipgloss.AdaptiveColor
Info lipgloss.AdaptiveColor
Text lipgloss.AdaptiveColor
Muted lipgloss.AdaptiveColor
VeryMuted lipgloss.AdaptiveColor
Background lipgloss.AdaptiveColor
Border lipgloss.AdaptiveColor
MutedBorder lipgloss.AdaptiveColor
System lipgloss.AdaptiveColor
Tool lipgloss.AdaptiveColor
Accent lipgloss.AdaptiveColor
Highlight lipgloss.AdaptiveColor
}
// DefaultTheme returns the default MCPHost theme (Catppuccin Mocha)
func DefaultTheme() Theme {
return Theme{
Primary: lipgloss.AdaptiveColor{
Light: "#8839ef", // Latte Mauve
Dark: "#cba6f7", // Mocha Mauve
},
Secondary: lipgloss.AdaptiveColor{
Light: "#04a5e5", // Latte Sky
Dark: "#89dceb", // Mocha Sky
},
Success: lipgloss.AdaptiveColor{
Light: "#40a02b", // Latte Green
Dark: "#a6e3a1", // Mocha Green
},
Warning: lipgloss.AdaptiveColor{
Light: "#df8e1d", // Latte Yellow
Dark: "#f9e2af", // Mocha Yellow
},
Error: lipgloss.AdaptiveColor{
Light: "#d20f39", // Latte Red
Dark: "#f38ba8", // Mocha Red
},
Info: lipgloss.AdaptiveColor{
Light: "#1e66f5", // Latte Blue
Dark: "#89b4fa", // Mocha Blue
},
Text: lipgloss.AdaptiveColor{
Light: "#4c4f69", // Latte Text
Dark: "#cdd6f4", // Mocha Text
},
Muted: lipgloss.AdaptiveColor{
Light: "#6c6f85", // Latte Subtext 0
Dark: "#a6adc8", // Mocha Subtext 0
},
VeryMuted: lipgloss.AdaptiveColor{
Light: "#9ca0b0", // Latte Overlay 0
Dark: "#6c7086", // Mocha Overlay 0
},
Background: lipgloss.AdaptiveColor{
Light: "#eff1f5", // Latte Base
Dark: "#1e1e2e", // Mocha Base
},
Border: lipgloss.AdaptiveColor{
Light: "#acb0be", // Latte Surface 2
Dark: "#585b70", // Mocha Surface 2
},
MutedBorder: lipgloss.AdaptiveColor{
Light: "#ccd0da", // Latte Surface 0
Dark: "#313244", // Mocha Surface 0
},
System: lipgloss.AdaptiveColor{
Light: "#179299", // Latte Teal
Dark: "#94e2d5", // Mocha Teal
},
Tool: lipgloss.AdaptiveColor{
Light: "#fe640b", // Latte Peach
Dark: "#fab387", // Mocha Peach
},
Accent: lipgloss.AdaptiveColor{
Light: "#ea76cb", // Latte Pink
Dark: "#f5c2e7", // Mocha Pink
},
Highlight: lipgloss.AdaptiveColor{
Light: "#df8e1d", // Latte Yellow (for highlights)
Dark: "#45475a", // Mocha Surface 1 (subtle highlight)
},
}
}
// StyleCard creates a styled card container
func StyleCard(width int, theme Theme) lipgloss.Style {
return lipgloss.NewStyle().
Width(width).
Border(lipgloss.RoundedBorder()).
BorderForeground(theme.Border).
Padding(1, 2).
MarginBottom(1)
}
// StyleHeader creates a styled header
func StyleHeader(theme Theme) lipgloss.Style {
return lipgloss.NewStyle().
Foreground(theme.Primary).
Bold(true)
}
// StyleSubheader creates a styled subheader
func StyleSubheader(theme Theme) lipgloss.Style {
return lipgloss.NewStyle().
Foreground(theme.Secondary).
Bold(true)
}
// StyleMuted creates muted text styling
func StyleMuted(theme Theme) lipgloss.Style {
return lipgloss.NewStyle().
Foreground(theme.Muted).
Italic(true)
}
// StyleSuccess creates success text styling
func StyleSuccess(theme Theme) lipgloss.Style {
return lipgloss.NewStyle().
Foreground(theme.Success).
Bold(true)
}
// StyleError creates error text styling
func StyleError(theme Theme) lipgloss.Style {
return lipgloss.NewStyle().
Foreground(theme.Error).
Bold(true)
}
// StyleWarning creates warning text styling
func StyleWarning(theme Theme) lipgloss.Style {
return lipgloss.NewStyle().
Foreground(theme.Warning).
Bold(true)
}
// StyleInfo creates info text styling
func StyleInfo(theme Theme) lipgloss.Style {
return lipgloss.NewStyle().
Foreground(theme.Info).
Bold(true)
}
// CreateSeparator creates a styled separator line
func CreateSeparator(width int, char string, color lipgloss.AdaptiveColor) string {
return lipgloss.NewStyle().
Foreground(color).
Width(width).
Render(lipgloss.PlaceHorizontal(width, lipgloss.Center, char))
}
// CreateProgressBar creates a simple progress bar
func CreateProgressBar(width int, percentage float64, theme Theme) string {
filled := int(float64(width) * percentage / 100)
empty := width - filled
filledBar := lipgloss.NewStyle().
Foreground(theme.Success).
Render(lipgloss.PlaceHorizontal(filled, lipgloss.Left, "█"))
emptyBar := lipgloss.NewStyle().
Foreground(theme.Muted).
Render(lipgloss.PlaceHorizontal(empty, lipgloss.Left, "░"))
return filledBar + emptyBar
}
// CreateBadge creates a styled badge
func CreateBadge(text string, color lipgloss.AdaptiveColor) string {
return lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "#FFFFFF", Dark: "#000000"}).
Background(color).
Padding(0, 1).
Bold(true).
Render(text)
}
// CreateGradientText creates text with gradient-like effect using different shades
func CreateGradientText(text string, startColor, endColor lipgloss.AdaptiveColor) string {
// For now, just use the start color - true gradients would require more complex implementation
return lipgloss.NewStyle().
Foreground(startColor).
Bold(true).
Render(text)
}
+229 -226
View File
@@ -2,6 +2,8 @@ package ui
import (
"fmt"
"os"
"os/user"
"strings"
"time"
@@ -30,16 +32,10 @@ type UIMessage struct {
Timestamp time.Time
}
// Color constants
var (
primaryColor = lipgloss.Color("#7C3AED") // Purple
secondaryColor = lipgloss.Color("#06B6D4") // Cyan
systemColor = lipgloss.Color("#10B981") // Green for MCPHost system messages
textColor = lipgloss.Color("#FFFFFF") // White
mutedColor = lipgloss.Color("#6B7280") // Gray
errorColor = lipgloss.Color("#EF4444") // Red
toolColor = lipgloss.Color("#F59E0B") // Orange/Amber for tool calls
)
// Helper functions to get theme colors
func getTheme() Theme {
return GetTheme()
}
// MessageRenderer handles rendering of messages with proper styling
type MessageRenderer struct {
@@ -47,6 +43,21 @@ type MessageRenderer struct {
debug bool
}
// getSystemUsername returns the current system username, fallback to "User"
func getSystemUsername() string {
if currentUser, err := user.Current(); err == nil && currentUser.Username != "" {
return currentUser.Username
}
// Fallback to environment variable
if username := os.Getenv("USER"); username != "" {
return username
}
if username := os.Getenv("USERNAME"); username != "" {
return username
}
return "User"
}
// NewMessageRenderer creates a new message renderer
func NewMessageRenderer(width int, debug bool) *MessageRenderer {
return &MessageRenderer{
@@ -60,40 +71,30 @@ func (r *MessageRenderer) SetWidth(width int) {
r.width = width
}
// RenderUserMessage renders a user message with proper styling
// RenderUserMessage renders a user message with right border and background header
func (r *MessageRenderer) RenderUserMessage(content string, timestamp time.Time) UIMessage {
baseStyle := lipgloss.NewStyle()
// Create the main message style with border
style := baseStyle.
Width(r.width - 1).
BorderLeft(true).
Foreground(mutedColor).
BorderForeground(secondaryColor).
BorderStyle(lipgloss.ThickBorder()).
PaddingLeft(1)
// Format timestamp
timeStr := timestamp.Local().Format("02 Jan 2006 03:04 PM")
username := "You"
// Create info line
info := baseStyle.
Width(r.width - 1).
Foreground(mutedColor).
Render(fmt.Sprintf(" %s (%s)", username, timeStr))
// Format timestamp and username
timeStr := timestamp.Local().Format("15:04")
username := getSystemUsername()
// Render the message content
messageContent := r.renderMarkdown(content, r.width-2)
messageContent := r.renderMarkdown(content, r.width-8) // Account for padding and borders
// Create info line
info := fmt.Sprintf(" %s (%s)", username, timeStr)
// Combine content and info
parts := []string{
strings.TrimSuffix(messageContent, "\n"),
info,
}
theme := getTheme()
fullContent := strings.TrimSuffix(messageContent, "\n") + "\n" +
lipgloss.NewStyle().Foreground(theme.VeryMuted).Render(info)
rendered := style.Render(
lipgloss.JoinVertical(lipgloss.Left, parts...),
// Use the new block renderer
rendered := renderContentBlock(
fullContent,
r.width,
WithAlign(lipgloss.Right),
WithBorderColor(theme.Secondary),
WithMarginBottom(1),
)
return UIMessage{
@@ -104,50 +105,41 @@ func (r *MessageRenderer) RenderUserMessage(content string, timestamp time.Time)
}
}
// RenderAssistantMessage renders an assistant message with proper styling
// RenderAssistantMessage renders an assistant message with left border and background header
func (r *MessageRenderer) RenderAssistantMessage(content string, timestamp time.Time, modelName string) UIMessage {
baseStyle := lipgloss.NewStyle()
// Create the main message style with border
style := baseStyle.
Width(r.width - 1).
BorderLeft(true).
Foreground(mutedColor).
BorderForeground(primaryColor).
BorderStyle(lipgloss.ThickBorder()).
PaddingLeft(1)
// Format timestamp and model info
timeStr := timestamp.Local().Format("02 Jan 2006 03:04 PM")
// Format timestamp and model info with better defaults
timeStr := timestamp.Local().Format("15:04")
if modelName == "" {
modelName = "Assistant"
}
// Create info line
info := baseStyle.
Width(r.width - 1).
Foreground(mutedColor).
Render(fmt.Sprintf(" %s (%s)", modelName, timeStr))
// Render the message content
messageContent := r.renderMarkdown(content, r.width-2)
// Handle empty content
// Handle empty content with better styling
theme := getTheme()
var messageContent string
if strings.TrimSpace(content) == "" {
messageContent = baseStyle.
messageContent = lipgloss.NewStyle().
Italic(true).
Foreground(mutedColor).
Render("*Finished without output*")
Foreground(theme.Muted).
Align(lipgloss.Center).
Render("Finished without output")
} else {
messageContent = r.renderMarkdown(content, r.width-8) // Account for padding and borders
}
// Create info line
info := fmt.Sprintf(" %s (%s)", modelName, timeStr)
// Combine content and info
parts := []string{
strings.TrimSuffix(messageContent, "\n"),
info,
}
fullContent := strings.TrimSuffix(messageContent, "\n") + "\n" +
lipgloss.NewStyle().Foreground(theme.VeryMuted).Render(info)
rendered := style.Render(
lipgloss.JoinVertical(lipgloss.Left, parts...),
// Use the new block renderer
rendered := renderContentBlock(
fullContent,
r.width,
WithAlign(lipgloss.Left),
WithBorderColor(theme.Primary),
WithMarginBottom(1),
)
return UIMessage{
@@ -158,47 +150,38 @@ func (r *MessageRenderer) RenderAssistantMessage(content string, timestamp time.
}
}
// RenderSystemMessage renders a system message (help, tools, etc.) with proper styling
// RenderSystemMessage renders a system message with left border and background header
func (r *MessageRenderer) RenderSystemMessage(content string, timestamp time.Time) UIMessage {
baseStyle := lipgloss.NewStyle()
// Create the main message style with border
style := baseStyle.
Width(r.width - 1).
BorderLeft(true).
Foreground(mutedColor).
BorderForeground(systemColor).
BorderStyle(lipgloss.ThickBorder()).
PaddingLeft(1)
// Format timestamp
timeStr := timestamp.Local().Format("02 Jan 2006 03:04 PM")
timeStr := timestamp.Local().Format("15:04")
// Create info line with MCPHost label
info := baseStyle.
Width(r.width - 1).
Foreground(mutedColor).
Render(fmt.Sprintf(" MCPHost (%s)", timeStr))
// Render the message content with markdown
messageContent := r.renderMarkdown(content, r.width-2)
// Handle empty content
// Handle empty content with better styling
theme := getTheme()
var messageContent string
if strings.TrimSpace(content) == "" {
messageContent = baseStyle.
messageContent = lipgloss.NewStyle().
Italic(true).
Foreground(mutedColor).
Render("*No content*")
Foreground(theme.Muted).
Align(lipgloss.Center).
Render("No content available")
} else {
messageContent = r.renderMarkdown(content, r.width-8) // Account for padding and borders
}
// Create info line
info := fmt.Sprintf(" MCPHost System (%s)", timeStr)
// Combine content and info
parts := []string{
strings.TrimSuffix(messageContent, "\n"),
info,
}
fullContent := strings.TrimSuffix(messageContent, "\n") + "\n" +
lipgloss.NewStyle().Foreground(theme.VeryMuted).Render(info)
rendered := style.Render(
lipgloss.JoinVertical(lipgloss.Left, parts...),
// Use the new block renderer
rendered := renderContentBlock(
fullContent,
r.width,
WithAlign(lipgloss.Left),
WithBorderColor(theme.System),
WithMarginBottom(1),
)
return UIMessage{
@@ -214,11 +197,12 @@ func (r *MessageRenderer) RenderDebugConfigMessage(config map[string]any, timest
baseStyle := lipgloss.NewStyle()
// Create the main message style with border using tool color
theme := getTheme()
style := baseStyle.
Width(r.width - 1).
BorderLeft(true).
Foreground(mutedColor).
BorderForeground(toolColor).
Foreground(theme.Muted).
BorderForeground(theme.Tool).
BorderStyle(lipgloss.ThickBorder()).
PaddingLeft(1)
@@ -227,7 +211,7 @@ func (r *MessageRenderer) RenderDebugConfigMessage(config map[string]any, timest
// Create header with debug icon
header := baseStyle.
Foreground(toolColor).
Foreground(theme.Tool).
Bold(true).
Render("🔧 Debug Configuration")
@@ -240,13 +224,13 @@ func (r *MessageRenderer) RenderDebugConfigMessage(config map[string]any, timest
}
configContent := baseStyle.
Foreground(mutedColor).
Foreground(theme.Muted).
Render(strings.Join(configLines, "\n"))
// Create info line
info := baseStyle.
Width(r.width - 1).
Foreground(mutedColor).
Foreground(theme.Muted).
Render(fmt.Sprintf(" MCPHost (%s)", timeStr))
// Combine parts
@@ -268,42 +252,32 @@ func (r *MessageRenderer) RenderDebugConfigMessage(config map[string]any, timest
}
}
// RenderErrorMessage renders an error message with proper styling
// RenderErrorMessage renders an error message with left border and background header
func (r *MessageRenderer) RenderErrorMessage(errorMsg string, timestamp time.Time) UIMessage {
baseStyle := lipgloss.NewStyle()
// Create the main message style with border
style := baseStyle.
Width(r.width - 1).
BorderLeft(true).
Foreground(mutedColor).
BorderForeground(errorColor).
BorderStyle(lipgloss.ThickBorder()).
PaddingLeft(1)
// Format timestamp
timeStr := timestamp.Local().Format("02 Jan 2006 03:04 PM")
timeStr := timestamp.Local().Format("15:04")
// Create info line with Error label
info := baseStyle.
Width(r.width - 1).
Foreground(mutedColor).
Render(fmt.Sprintf(" Error (%s)", timeStr))
// Format error content with error styling
errorContent := baseStyle.
Foreground(errorColor).
// Format error content
theme := getTheme()
errorContent := lipgloss.NewStyle().
Foreground(theme.Error).
Bold(true).
Render(fmt.Sprintf("❌ %s", errorMsg))
Render(errorMsg)
// Create info line
info := fmt.Sprintf(" Error (%s)", timeStr)
// Combine content and info
parts := []string{
errorContent,
info,
}
fullContent := errorContent + "\n" +
lipgloss.NewStyle().Foreground(theme.VeryMuted).Render(info)
rendered := style.Render(
lipgloss.JoinVertical(lipgloss.Left, parts...),
// Use the new block renderer
rendered := renderContentBlock(
fullContent,
r.width,
WithAlign(lipgloss.Left),
WithBorderColor(theme.Error),
WithMarginBottom(1),
)
return UIMessage{
@@ -314,53 +288,40 @@ func (r *MessageRenderer) RenderErrorMessage(errorMsg string, timestamp time.Tim
}
}
// RenderToolCallMessage renders a tool call in progress with proper styling
// RenderToolCallMessage renders a tool call in progress with left border and background header
func (r *MessageRenderer) RenderToolCallMessage(toolName, toolArgs string, timestamp time.Time) UIMessage {
baseStyle := lipgloss.NewStyle()
// Create the main message style with border
style := baseStyle.
Width(r.width - 1).
BorderLeft(true).
Foreground(mutedColor).
BorderForeground(toolColor).
BorderStyle(lipgloss.ThickBorder()).
PaddingLeft(1)
// Format timestamp
timeStr := timestamp.Local().Format("02 Jan 2006 03:04 PM")
timeStr := timestamp.Local().Format("15:04")
// Create header with tool icon and name
toolIcon := "🔧"
header := baseStyle.
Foreground(toolColor).
Bold(true).
Render(fmt.Sprintf("%s Calling %s", toolIcon, toolName))
// Format arguments in a more readable way
// Format arguments with better presentation
theme := getTheme()
var argsContent string
if toolArgs != "" && toolArgs != "{}" {
// Try to format JSON args nicely
argsContent = baseStyle.
Foreground(mutedColor).
argsContent = lipgloss.NewStyle().
Foreground(theme.Muted).
Italic(true).
Render(fmt.Sprintf("Arguments: %s", r.formatToolArgs(toolArgs)))
}
// Create info line
info := baseStyle.
Width(r.width - 1).
Foreground(mutedColor).
Render(fmt.Sprintf(" Tool Call (%s)", timeStr))
info := fmt.Sprintf(" Executing %s (%s)", toolName, timeStr)
// Combine parts
parts := []string{header}
var fullContent string
if argsContent != "" {
parts = append(parts, argsContent)
fullContent = argsContent + "\n" +
lipgloss.NewStyle().Foreground(theme.VeryMuted).Render(info)
} else {
fullContent = lipgloss.NewStyle().Foreground(theme.VeryMuted).Render(info)
}
parts = append(parts, info)
rendered := style.Render(
lipgloss.JoinVertical(lipgloss.Left, parts...),
// Use the new block renderer
rendered := renderContentBlock(
fullContent,
r.width,
WithAlign(lipgloss.Left),
WithBorderColor(theme.Tool),
WithMarginBottom(1),
)
return UIMessage{
@@ -373,49 +334,44 @@ func (r *MessageRenderer) RenderToolCallMessage(toolName, toolArgs string, times
// RenderToolMessage renders a tool call message with proper styling
func (r *MessageRenderer) RenderToolMessage(toolName, toolArgs, toolResult string, isError bool) UIMessage {
baseStyle := lipgloss.NewStyle()
// Create the main message style with border
style := baseStyle.
Width(r.width - 1).
BorderLeft(true).
BorderStyle(lipgloss.ThickBorder()).
PaddingLeft(1).
BorderForeground(mutedColor)
// Tool name styling
toolNameText := baseStyle.
Foreground(mutedColor).
// Tool name and arguments header
theme := getTheme()
toolNameText := lipgloss.NewStyle().
Foreground(theme.Muted).
Render(fmt.Sprintf("%s: ", toolName))
// Tool arguments styling
argsText := baseStyle.
Width(r.width - 2 - lipgloss.Width(toolNameText)).
Foreground(mutedColor).
Render(r.truncateText(toolArgs, r.width-2-lipgloss.Width(toolNameText)))
argsText := lipgloss.NewStyle().
Foreground(theme.Muted).
Render(r.truncateText(toolArgs, r.width-8-lipgloss.Width(toolNameText)))
headerLine := lipgloss.JoinHorizontal(lipgloss.Left, toolNameText, argsText)
// Tool result styling
var resultContent string
if isError {
resultContent = baseStyle.
Width(r.width - 2).
Foreground(errorColor).
resultContent = lipgloss.NewStyle().
Foreground(theme.Error).
Render(fmt.Sprintf("Error: %s", toolResult))
} else {
// Format result based on tool type
resultContent = r.formatToolResult(toolName, toolResult, r.width-2)
resultContent = r.formatToolResult(toolName, toolResult, r.width-8)
}
// Combine parts
headerLine := lipgloss.JoinHorizontal(lipgloss.Left, toolNameText, argsText)
parts := []string{headerLine}
var fullContent string
if resultContent != "" {
parts = append(parts, strings.TrimSuffix(resultContent, "\n"))
fullContent = headerLine + "\n" + strings.TrimSuffix(resultContent, "\n")
} else {
fullContent = headerLine
}
rendered := style.Render(
lipgloss.JoinVertical(lipgloss.Left, parts...),
// Use the new block renderer
rendered := renderContentBlock(
fullContent,
r.width,
WithAlign(lipgloss.Left),
WithBorderColor(theme.Muted),
WithMarginBottom(1),
)
return UIMessage{
@@ -471,9 +427,10 @@ func (r *MessageRenderer) formatToolResult(toolName, result string, width int) s
}
// For other tools, render as muted text
theme := getTheme()
return baseStyle.
Width(width).
Foreground(mutedColor).
Foreground(theme.Muted).
Render(result)
}
@@ -546,16 +503,24 @@ func (c *MessageContainer) Render() string {
return c.renderEmptyState()
}
baseStyle := lipgloss.NewStyle()
var parts []string
for _, msg := range c.messages {
parts = append(parts, msg.Content)
// Add spacing between messages
parts = append(parts, baseStyle.Width(c.width).Render(""))
for i, msg := range c.messages {
// Center each message horizontally
centeredMsg := lipgloss.PlaceHorizontal(
c.width,
lipgloss.Center,
msg.Content,
)
parts = append(parts, centeredMsg)
// Add spacing between messages (except after the last one)
if i < len(c.messages)-1 {
parts = append(parts, "")
}
}
return baseStyle.
return lipgloss.NewStyle().
Width(c.width).
PaddingBottom(1).
Render(
@@ -563,35 +528,73 @@ func (c *MessageContainer) Render() string {
)
}
// renderEmptyState renders the initial empty state
// renderEmptyState renders an enhanced initial empty state
func (c *MessageContainer) renderEmptyState() string {
baseStyle := lipgloss.NewStyle()
header := baseStyle.
Width(c.width).
Align(lipgloss.Center).
Foreground(systemColor).
// Create a welcome box with border
theme := getTheme()
welcomeBox := baseStyle.
Width(c.width-4).
Border(lipgloss.RoundedBorder()).
BorderForeground(theme.System).
Padding(2, 4).
Align(lipgloss.Center)
// Main title
title := baseStyle.
Foreground(theme.System).
Bold(true).
Render("MCPHost - AI Assistant with MCP Tools")
Render("MCPHost")
// Subtitle with better typography
subtitle := baseStyle.
Width(c.width).
Align(lipgloss.Center).
Foreground(mutedColor).
Render("Start a conversation by typing your message below")
Foreground(theme.Primary).
Bold(true).
MarginTop(1).
Render("AI Assistant with MCP Tools")
// Feature highlights
features := []string{
"Natural language conversations",
"Powerful tool integrations",
"Multi-provider LLM support",
"Usage tracking & analytics",
}
var featureList []string
for _, feature := range features {
featureList = append(featureList, baseStyle.
Foreground(theme.Muted).
MarginLeft(2).
Render("• "+feature))
}
// Getting started prompt
prompt := baseStyle.
Foreground(theme.Accent).
Italic(true).
MarginTop(2).
Render("Start by typing your message below or use /help for commands")
// Combine all elements
content := lipgloss.JoinVertical(
lipgloss.Center,
title,
subtitle,
"",
lipgloss.JoinVertical(lipgloss.Left, featureList...),
"",
prompt,
)
welcomeContent := welcomeBox.Render(content)
// Center the welcome box vertically
return baseStyle.
Width(c.width).
Height(c.height).
PaddingBottom(1).
Render(
lipgloss.JoinVertical(
lipgloss.Center,
"",
header,
"",
subtitle,
"",
),
)
Align(lipgloss.Center).
AlignVertical(lipgloss.Center).
Render(welcomeContent)
}
+20 -4
View File
@@ -51,17 +51,33 @@ func (m spinnerModel) View() string {
if m.quitting {
return ""
}
return fmt.Sprintf("%s %s", m.spinner.View(), m.message)
// Enhanced spinner display with better styling
baseStyle := lipgloss.NewStyle()
theme := GetTheme()
spinnerStyle := baseStyle.
Foreground(theme.Primary).
Bold(true)
messageStyle := baseStyle.
Foreground(theme.Text).
Italic(true)
return fmt.Sprintf("%s %s",
spinnerStyle.Render(m.spinner.View()),
messageStyle.Render(m.message))
}
// quitMsg is sent when we want to quit the spinner
type quitMsg struct{}
// NewSpinner creates a new spinner with the given message
// NewSpinner creates a new spinner with enhanced styling
func NewSpinner(message string) *Spinner {
s := spinner.New()
s.Spinner = spinner.Dot
s.Style = s.Style.Foreground(lipgloss.Color("205")) // Purple color
s.Spinner = spinner.Points // More modern spinner style
theme := GetTheme()
s.Style = s.Style.Foreground(theme.Primary)
ctx, cancel := context.WithCancel(context.Background())
+60 -27
View File
@@ -29,19 +29,44 @@ func GetMarkdownRenderer(width int) *glamour.TermRenderer {
// generateMarkdownStyleConfig creates an ansi.StyleConfig for markdown rendering
func generateMarkdownStyleConfig() ansi.StyleConfig {
// Define colors - using simple colors since we're not implementing theming
textColor := "#ffffff"
mutedColor := "#888888"
headingColor := "#00d7ff"
emphColor := "#ffff87"
strongColor := "#ffffff"
linkColor := "#5fd7ff"
codeColor := "#d7d7af"
errorColor := "#ff5f5f"
keywordColor := "#ff87d7"
stringColor := "#87ff87"
numberColor := "#ffaf87"
commentColor := "#5f5f87"
// Define adaptive colors based on terminal background
var textColor, mutedColor string
if lipgloss.HasDarkBackground() {
textColor = "#F9FAFB" // Light text for dark backgrounds
mutedColor = "#9CA3AF" // Light muted for dark backgrounds
} else {
textColor = "#1F2937" // Dark text for light backgrounds
mutedColor = "#6B7280" // Dark muted for light backgrounds
}
var headingColor, emphColor, strongColor, linkColor, codeColor, errorColor, keywordColor, stringColor, numberColor, commentColor string
if lipgloss.HasDarkBackground() {
// Dark background colors
headingColor = "#22D3EE" // Cyan
emphColor = "#FDE047" // Yellow
strongColor = "#F9FAFB" // Light gray
linkColor = "#60A5FA" // Blue
codeColor = "#D1D5DB" // Light gray
errorColor = "#F87171" // Red
keywordColor = "#C084FC" // Purple
stringColor = "#34D399" // Green
numberColor = "#FBBF24" // Orange
commentColor = "#9CA3AF" // Muted gray
} else {
// Light background colors
headingColor = "#0891B2" // Dark cyan
emphColor = "#D97706" // Orange
strongColor = "#1F2937" // Dark gray
linkColor = "#2563EB" // Blue
codeColor = "#374151" // Dark gray
errorColor = "#DC2626" // Red
keywordColor = "#7C3AED" // Purple
stringColor = "#059669" // Green
numberColor = "#D97706" // Orange
commentColor = "#6B7280" // Muted gray
}
// Don't apply background in markdown - let the block renderer handle it
bgColor := ""
return ansi.StyleConfig{
Document: ansi.StyleBlock{
@@ -50,7 +75,7 @@ func generateMarkdownStyleConfig() ansi.StyleConfig {
BlockSuffix: "",
Color: stringPtr(textColor),
},
Margin: uintPtr(defaultMargin),
Margin: uintPtr(0), // Remove margin to prevent spacing
},
BlockQuote: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{
@@ -59,12 +84,12 @@ func generateMarkdownStyleConfig() ansi.StyleConfig {
Prefix: "┃ ",
},
Indent: uintPtr(1),
IndentToken: stringPtr(BaseStyle().Render(" ")),
IndentToken: stringPtr(lipgloss.NewStyle().Background(lipgloss.AdaptiveColor{Light: bgColor, Dark: bgColor}).Render(" ")),
},
List: ansi.StyleList{
LevelIndent: defaultMargin,
LevelIndent: 0, // Remove list indentation
StyleBlock: ansi.StyleBlock{
IndentToken: stringPtr(BaseStyle().Render(" ")),
IndentToken: stringPtr(lipgloss.NewStyle().Background(lipgloss.AdaptiveColor{Light: bgColor, Dark: bgColor}).Render(" ")),
StylePrimitive: ansi.StylePrimitive{
Color: stringPtr(textColor),
},
@@ -124,7 +149,8 @@ func generateMarkdownStyleConfig() ansi.StyleConfig {
Color: stringPtr(mutedColor),
},
Emph: ansi.StylePrimitive{
Color: stringPtr(emphColor),
Color: stringPtr(emphColor),
Italic: boolPtr(true),
},
Strong: ansi.StylePrimitive{
@@ -149,25 +175,30 @@ func generateMarkdownStyleConfig() ansi.StyleConfig {
Unticked: "[ ] ",
},
Link: ansi.StylePrimitive{
Color: stringPtr(linkColor),
Color: stringPtr(linkColor),
Underline: boolPtr(true),
},
LinkText: ansi.StylePrimitive{
Color: stringPtr(linkColor),
Bold: boolPtr(true),
Bold: boolPtr(true),
},
Image: ansi.StylePrimitive{
Color: stringPtr(linkColor),
Color: stringPtr(linkColor),
Underline: boolPtr(true),
Format: "🖼 {{.text}}",
},
ImageText: ansi.StylePrimitive{
Color: stringPtr(linkColor),
Color: stringPtr(linkColor),
Format: "{{.text}}",
},
Code: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{
Color: stringPtr(codeColor),
Color: stringPtr(codeColor),
Prefix: "",
Suffix: "",
},
@@ -175,10 +206,10 @@ func generateMarkdownStyleConfig() ansi.StyleConfig {
CodeBlock: ansi.StyleCodeBlock{
StyleBlock: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{
Prefix: " ",
Prefix: "",
Color: stringPtr(codeColor),
},
Margin: uintPtr(defaultMargin),
Margin: uintPtr(0), // Remove margin
},
Chroma: &ansi.Chroma{
Text: ansi.StylePrimitive{
@@ -248,7 +279,8 @@ func generateMarkdownStyleConfig() ansi.StyleConfig {
Color: stringPtr(errorColor),
},
GenericEmph: ansi.StylePrimitive{
Color: stringPtr(emphColor),
Color: stringPtr(emphColor),
Italic: boolPtr(true),
},
GenericInserted: ansi.StylePrimitive{
@@ -256,7 +288,8 @@ func generateMarkdownStyleConfig() ansi.StyleConfig {
},
GenericStrong: ansi.StylePrimitive{
Color: stringPtr(strongColor),
Bold: boolPtr(true),
Bold: boolPtr(true),
},
GenericSubheading: ansi.StylePrimitive{
Color: stringPtr(headingColor),
+55 -10
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"sync"
"github.com/charmbracelet/lipgloss"
"github.com/mark3labs/mcphost/internal/models"
"github.com/mark3labs/mcphost/internal/tokens"
)
@@ -68,7 +69,7 @@ func (ut *UsageTracker) UpdateUsage(inputTokens, outputTokens, cacheReadTokens,
// Calculate costs based on model pricing
// For OAuth credentials, costs are $0 for usage tracking purposes
var inputCost, outputCost, cacheReadCost, cacheWriteCost, totalCost float64
if !ut.isOAuth {
inputCost = float64(inputTokens) * ut.modelInfo.Cost.Input / 1000000 // Cost is per million tokens
outputCost = float64(outputTokens) * ut.modelInfo.Cost.Output / 1000000
@@ -120,7 +121,7 @@ func (ut *UsageTracker) EstimateAndUpdateUsageFromText(inputText, outputText str
ut.UpdateUsage(inputTokens, outputTokens, 0, 0)
}
// RenderUsageInfo renders the current usage information in a single line format
// RenderUsageInfo renders enhanced usage information with better styling
func (ut *UsageTracker) RenderUsageInfo() string {
ut.mu.RLock()
defer ut.mu.RUnlock()
@@ -129,29 +130,73 @@ func (ut *UsageTracker) RenderUsageInfo() string {
return ""
}
// Import lipgloss for styling
baseStyle := lipgloss.NewStyle()
// Calculate total tokens
totalTokens := ut.sessionStats.TotalInputTokens + ut.sessionStats.TotalOutputTokens
// Format tokens with K suffix if >= 1000
// Format tokens with K/M suffix for better readability
var tokenStr string
if totalTokens >= 1000 {
if totalTokens >= 1000000 {
tokenStr = fmt.Sprintf("%.1fM", float64(totalTokens)/1000000)
} else if totalTokens >= 1000 {
tokenStr = fmt.Sprintf("%.1fK", float64(totalTokens)/1000)
} else {
tokenStr = fmt.Sprintf("%d", totalTokens)
}
// Calculate percentage based on context limit (if available)
// Calculate percentage based on context limit with color coding
var percentageStr string
var percentageColor lipgloss.AdaptiveColor
if ut.modelInfo.Limit.Context > 0 {
percentage := float64(totalTokens) / float64(ut.modelInfo.Limit.Context) * 100
percentageStr = fmt.Sprintf(" (%.0f%%)", percentage)
// Color code based on usage percentage
theme := GetTheme()
if percentage >= 80 {
percentageColor = theme.Error // Red
} else if percentage >= 60 {
percentageColor = theme.Warning // Orange
} else {
percentageColor = theme.Success // Green
}
percentageStr = baseStyle.
Foreground(percentageColor).
Render(fmt.Sprintf(" (%.0f%%)", percentage))
}
// Format cost
costStr := fmt.Sprintf("$%.2f", ut.sessionStats.TotalCost)
// Format cost with appropriate styling
theme := GetTheme()
var costStr string
if ut.isOAuth {
costStr = baseStyle.
Foreground(theme.Primary).
Render("$0.00")
} else {
costStr = baseStyle.
Foreground(theme.Primary).
Render(fmt.Sprintf("$%.4f", ut.sessionStats.TotalCost))
}
// Build the single line display
return fmt.Sprintf("Tokens: %s%s, Cost: %s", tokenStr, percentageStr, costStr)
// Create styled components
tokensLabel := baseStyle.
Foreground(theme.Muted).
Render("Tokens: ")
tokensValue := baseStyle.
Foreground(theme.Text).
Bold(true).
Render(tokenStr)
costLabel := baseStyle.
Foreground(theme.Muted).
Render(" | Cost: ")
// Build the enhanced display
return fmt.Sprintf("%s%s%s%s%s\n",
tokensLabel, tokensValue, percentageStr, costLabel, costStr)
}
// GetSessionStats returns a copy of the current session statistics
+6 -6
View File
@@ -27,8 +27,8 @@ func TestUsageTracker_RenderUsageInfo_OAuth(t *testing.T) {
oauthTracker.UpdateUsage(1500, 500, 0, 0) // 2000 total tokens
rendered := oauthTracker.RenderUsageInfo()
// Should show tokens and percentage, but cost should be $0.00
// Should show tokens and percentage, but cost should show "$0.00"
if !strings.Contains(rendered, "Tokens: 2.0K") {
t.Errorf("Expected rendered output to contain 'Tokens: 2.0K', got: %s", rendered)
}
@@ -44,16 +44,16 @@ func TestUsageTracker_RenderUsageInfo_OAuth(t *testing.T) {
regularTracker.UpdateUsage(1500, 500, 0, 0) // Same token usage
regularRendered := regularTracker.RenderUsageInfo()
// Should show tokens and actual cost
if !strings.Contains(regularRendered, "Tokens: 2.0K") {
t.Errorf("Expected regular rendered output to contain 'Tokens: 2.0K', got: %s", regularRendered)
}
if strings.Contains(regularRendered, "Cost: $0.00") {
t.Errorf("Expected regular rendered output to NOT show $0.00 cost, got: %s", regularRendered)
t.Errorf("Expected regular rendered output to NOT show $0.00, got: %s", regularRendered)
}
// Should show actual calculated cost (1500*3 + 500*15)/1000000 = 0.0120
if !strings.Contains(regularRendered, "Cost: $0.01") { // Rounded to 2 decimal places
if !strings.Contains(regularRendered, "Cost: $0.0120") { // Now showing 4 decimal places
t.Errorf("Expected regular rendered output to show actual cost, got: %s", regularRendered)
}
}
}
+4 -4
View File
@@ -27,8 +27,8 @@ func TestUsageTracker_OAuthCosts(t *testing.T) {
}
// Check that costs are calculated for regular API key
expectedInputCost := float64(1000) * 3.0 / 1000000 // $0.003
expectedOutputCost := float64(500) * 15.0 / 1000000 // $0.0075
expectedInputCost := float64(1000) * 3.0 / 1000000 // $0.003
expectedOutputCost := float64(500) * 15.0 / 1000000 // $0.0075
expectedTotalCost := expectedInputCost + expectedOutputCost // $0.0105
if stats.InputCost != expectedInputCost {
@@ -83,7 +83,7 @@ func TestUsageTracker_OAuthSessionStats(t *testing.T) {
// Test OAuth session stats accumulation
oauthTracker := NewUsageTracker(modelInfo, "anthropic", 80, true)
// Make multiple requests
oauthTracker.UpdateUsage(1000, 500, 0, 0)
oauthTracker.UpdateUsage(2000, 1000, 0, 0)
@@ -107,4 +107,4 @@ func TestUsageTracker_OAuthSessionStats(t *testing.T) {
if sessionStats.RequestCount != 2 {
t.Errorf("Expected request count to be 2, got %d", sessionStats.RequestCount)
}
}
}