mirror of
https://github.com/mark3labs/kit.git
synced 2026-06-14 03:30:26 +00:00
Merge branch 'main' of github.com:mark3labs/mcphost
This commit is contained in:
+1
-1
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user