mirror of
https://github.com/mark3labs/kit.git
synced 2026-06-14 03:30:26 +00:00
615 lines
22 KiB
Go
615 lines
22 KiB
Go
package ui
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
"github.com/charmbracelet/lipgloss"
|
|
"github.com/cloudwego/eino/schema"
|
|
"golang.org/x/term"
|
|
)
|
|
|
|
var (
|
|
promptStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("12"))
|
|
)
|
|
|
|
// CLI manages the command-line interface for MCPHost, providing message rendering,
|
|
// user input handling, and display management. It supports both standard and compact
|
|
// display modes, handles streaming responses, tracks token usage, and manages the
|
|
// overall conversation flow between the user and AI assistants.
|
|
type CLI struct {
|
|
messageRenderer *MessageRenderer
|
|
compactRenderer *CompactRenderer // Add compact renderer
|
|
messageContainer *MessageContainer
|
|
usageTracker *UsageTracker
|
|
width int
|
|
height int
|
|
compactMode bool // Add compact mode flag
|
|
debug bool // Add debug mode flag
|
|
modelName string // Store current model name
|
|
lastStreamHeight int // track how far back we need to move the cursor to overwrite streaming messages
|
|
usageDisplayed bool // track if usage info was displayed after last assistant message
|
|
}
|
|
|
|
// NewCLI creates and initializes a new CLI instance with the specified display modes.
|
|
// The debug parameter enables debug message rendering, while compact enables a more
|
|
// condensed display format. Returns an initialized CLI ready for interaction or an
|
|
// error if initialization fails.
|
|
func NewCLI(debug bool, compact bool) (*CLI, error) {
|
|
cli := &CLI{
|
|
compactMode: compact,
|
|
debug: debug,
|
|
}
|
|
cli.updateSize()
|
|
cli.messageRenderer = NewMessageRenderer(cli.width, debug)
|
|
cli.compactRenderer = NewCompactRenderer(cli.width, debug)
|
|
cli.messageContainer = NewMessageContainer(cli.width, cli.height-4, compact) // Pass compact mode
|
|
|
|
return cli, nil
|
|
}
|
|
|
|
// SetUsageTracker attaches a usage tracker to the CLI for monitoring token
|
|
// consumption and costs. The tracker will be automatically updated with the
|
|
// current display width for proper rendering.
|
|
func (c *CLI) SetUsageTracker(tracker *UsageTracker) {
|
|
c.usageTracker = tracker
|
|
if c.usageTracker != nil {
|
|
c.usageTracker.SetWidth(c.width)
|
|
}
|
|
}
|
|
|
|
// GetDebugLogger returns a CLIDebugLogger instance that routes debug output
|
|
// through the CLI's rendering system for consistent message formatting and display.
|
|
func (c *CLI) GetDebugLogger() *CLIDebugLogger {
|
|
return NewCLIDebugLogger(c)
|
|
}
|
|
|
|
// SetModelName updates the current AI model name being used in the conversation.
|
|
// This name is displayed in message headers to indicate which model is responding.
|
|
func (c *CLI) SetModelName(modelName string) {
|
|
c.modelName = modelName
|
|
if c.messageContainer != nil {
|
|
c.messageContainer.SetModelName(modelName)
|
|
}
|
|
}
|
|
|
|
// GetPrompt displays an interactive prompt and waits for user input. It provides
|
|
// slash command support, multi-line editing, and cancellation handling. Returns
|
|
// the user's input as a string, or an error if the operation was cancelled or
|
|
// failed. Returns io.EOF for clean exit signals.
|
|
func (c *CLI) GetPrompt() (string, error) {
|
|
// Usage info is now displayed immediately after responses via DisplayUsageAfterResponse()
|
|
// No need to display it here to avoid duplication
|
|
|
|
c.messageContainer.messages = nil // clear previous messages (they should have been printed already)
|
|
c.lastStreamHeight = 0 // Reset last stream height for new prompt
|
|
|
|
// No divider needed - removed for cleaner appearance
|
|
|
|
// Create our custom slash command input
|
|
input := NewSlashCommandInput(c.width, "Enter your prompt (Type /help for commands, Ctrl+C to quit, ESC to cancel generation)")
|
|
|
|
// Run as a tea program
|
|
p := tea.NewProgram(input)
|
|
finalModel, err := p.Run()
|
|
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// Get the value from the final model
|
|
if finalInput, ok := finalModel.(*SlashCommandInput); ok {
|
|
// Clear the input field from the display
|
|
linesToClear := finalInput.RenderedLines()
|
|
// We need to clear linesToClear - 1 lines because we're already on the line after the last rendered line
|
|
for i := 0; i < linesToClear-1; i++ {
|
|
fmt.Print("\033[1A\033[2K") // Move up one line and clear it
|
|
}
|
|
|
|
if finalInput.Cancelled() {
|
|
return "", io.EOF // Signal clean exit
|
|
}
|
|
value := strings.TrimSpace(finalInput.Value())
|
|
return value, nil
|
|
}
|
|
|
|
return "", fmt.Errorf("unexpected model type")
|
|
}
|
|
|
|
// ShowSpinner displays an animated spinner with the specified message while
|
|
// executing the provided action function. The spinner automatically stops when
|
|
// the action completes. Returns any error returned by the action function.
|
|
func (c *CLI) ShowSpinner(message string, action func() error) error {
|
|
spinner := NewSpinner(message)
|
|
spinner.Start()
|
|
|
|
err := action()
|
|
|
|
spinner.Stop()
|
|
|
|
return err
|
|
}
|
|
|
|
// DisplayUserMessage renders and displays a user's message with appropriate
|
|
// formatting based on the current display mode (standard or compact). The message
|
|
// is timestamped and styled according to the active theme.
|
|
func (c *CLI) DisplayUserMessage(message string) {
|
|
var msg UIMessage
|
|
if c.compactMode {
|
|
msg = c.compactRenderer.RenderUserMessage(message, time.Now())
|
|
} else {
|
|
msg = c.messageRenderer.RenderUserMessage(message, time.Now())
|
|
}
|
|
c.messageContainer.AddMessage(msg)
|
|
c.displayContainer()
|
|
}
|
|
|
|
// DisplayAssistantMessage renders and displays an AI assistant's response message
|
|
// with appropriate formatting. This method delegates to DisplayAssistantMessageWithModel
|
|
// with an empty model name for backward compatibility.
|
|
func (c *CLI) DisplayAssistantMessage(message string) error {
|
|
return c.DisplayAssistantMessageWithModel(message, "")
|
|
}
|
|
|
|
// DisplayAssistantMessageWithModel renders and displays an AI assistant's response
|
|
// with the specified model name shown in the message header. The message is
|
|
// formatted according to the current display mode and includes timestamp information.
|
|
func (c *CLI) DisplayAssistantMessageWithModel(message, modelName string) error {
|
|
var msg UIMessage
|
|
if c.compactMode {
|
|
msg = c.compactRenderer.RenderAssistantMessage(message, time.Now(), modelName)
|
|
} else {
|
|
msg = c.messageRenderer.RenderAssistantMessage(message, time.Now(), modelName)
|
|
}
|
|
c.messageContainer.AddMessage(msg)
|
|
c.displayContainer()
|
|
return nil
|
|
}
|
|
|
|
// DisplayToolCallMessage renders and displays a message indicating that a tool
|
|
// is being executed. Shows the tool name and its arguments formatted appropriately
|
|
// for the current display mode. This is typically shown while a tool is running.
|
|
func (c *CLI) DisplayToolCallMessage(toolName, toolArgs string) {
|
|
|
|
c.messageContainer.messages = nil // clear previous messages (they should have been printed already)
|
|
c.lastStreamHeight = 0 // Reset last stream height for new prompt
|
|
|
|
var msg UIMessage
|
|
if c.compactMode {
|
|
msg = c.compactRenderer.RenderToolCallMessage(toolName, toolArgs, time.Now())
|
|
} else {
|
|
msg = c.messageRenderer.RenderToolCallMessage(toolName, toolArgs, time.Now())
|
|
}
|
|
|
|
// Always display immediately - spinner management is handled externally
|
|
c.messageContainer.AddMessage(msg)
|
|
c.displayContainer()
|
|
}
|
|
|
|
// DisplayToolMessage renders and displays the complete result of a tool execution,
|
|
// including the tool name, arguments, and result. The isError parameter determines
|
|
// whether the result should be displayed as an error or success message.
|
|
func (c *CLI) DisplayToolMessage(toolName, toolArgs, toolResult string, isError bool) {
|
|
var msg UIMessage
|
|
if c.compactMode {
|
|
msg = c.compactRenderer.RenderToolMessage(toolName, toolArgs, toolResult, isError)
|
|
} else {
|
|
msg = c.messageRenderer.RenderToolMessage(toolName, toolArgs, toolResult, isError)
|
|
}
|
|
|
|
// Always display immediately - spinner management is handled externally
|
|
c.messageContainer.AddMessage(msg)
|
|
c.displayContainer()
|
|
}
|
|
|
|
// StartStreamingMessage initializes a new streaming message display for real-time
|
|
// AI responses. The message will be progressively updated as content arrives.
|
|
// The modelName parameter indicates which AI model is generating the response.
|
|
func (c *CLI) StartStreamingMessage(modelName string) {
|
|
// Add an empty assistant message that we'll update during streaming
|
|
var msg UIMessage
|
|
if c.compactMode {
|
|
msg = c.compactRenderer.RenderAssistantMessage("", time.Now(), modelName)
|
|
} else {
|
|
msg = c.messageRenderer.RenderAssistantMessage("", time.Now(), modelName)
|
|
}
|
|
msg.Streaming = true
|
|
c.lastStreamHeight = 0 // Reset last stream height for new message
|
|
c.messageContainer.AddMessage(msg)
|
|
c.displayContainer()
|
|
}
|
|
|
|
// UpdateStreamingMessage updates the currently streaming message with new content.
|
|
// This method should be called after StartStreamingMessage to progressively display
|
|
// AI responses as they are generated in real-time.
|
|
func (c *CLI) UpdateStreamingMessage(content string) {
|
|
// Update the last message (which should be the streaming assistant message)
|
|
c.messageContainer.UpdateLastMessage(content)
|
|
c.displayContainer()
|
|
}
|
|
|
|
// DisplayError renders and displays an error message with distinctive formatting
|
|
// to ensure visibility. The error is timestamped and styled according to the
|
|
// current display mode's error theme.
|
|
func (c *CLI) DisplayError(err error) {
|
|
var msg UIMessage
|
|
if c.compactMode {
|
|
msg = c.compactRenderer.RenderErrorMessage(err.Error(), time.Now())
|
|
} else {
|
|
msg = c.messageRenderer.RenderErrorMessage(err.Error(), time.Now())
|
|
}
|
|
c.messageContainer.AddMessage(msg)
|
|
c.displayContainer()
|
|
}
|
|
|
|
// DisplayInfo renders and displays an informational system message. These messages
|
|
// are typically used for status updates, notifications, or other non-error system
|
|
// communications to the user.
|
|
func (c *CLI) DisplayInfo(message string) {
|
|
var msg UIMessage
|
|
if c.compactMode {
|
|
msg = c.compactRenderer.RenderSystemMessage(message, time.Now())
|
|
} else {
|
|
msg = c.messageRenderer.RenderSystemMessage(message, time.Now())
|
|
}
|
|
c.messageContainer.AddMessage(msg)
|
|
c.displayContainer()
|
|
}
|
|
|
|
// DisplayCancellation displays a system message indicating that the current
|
|
// AI generation has been cancelled by the user (typically via ESC key).
|
|
func (c *CLI) DisplayCancellation() {
|
|
var msg UIMessage
|
|
if c.compactMode {
|
|
msg = c.compactRenderer.RenderSystemMessage("Generation cancelled by user (ESC pressed)", time.Now())
|
|
} else {
|
|
msg = c.messageRenderer.RenderSystemMessage("Generation cancelled by user (ESC pressed)", time.Now())
|
|
}
|
|
c.messageContainer.AddMessage(msg)
|
|
c.displayContainer()
|
|
}
|
|
|
|
// DisplayDebugMessage renders and displays a debug message if debug mode is enabled.
|
|
// Debug messages are formatted distinctively and only shown when the CLI is
|
|
// initialized with debug=true.
|
|
func (c *CLI) DisplayDebugMessage(message string) {
|
|
if !c.debug {
|
|
return
|
|
}
|
|
var msg UIMessage
|
|
if c.compactMode {
|
|
msg = c.compactRenderer.RenderDebugMessage(message, time.Now())
|
|
} else {
|
|
msg = c.messageRenderer.RenderDebugMessage(message, time.Now())
|
|
}
|
|
c.messageContainer.AddMessage(msg)
|
|
c.displayContainer()
|
|
}
|
|
|
|
// DisplayDebugConfig renders and displays configuration settings in a formatted
|
|
// debug message. The config parameter should contain key-value pairs representing
|
|
// configuration options that will be displayed for debugging purposes.
|
|
func (c *CLI) DisplayDebugConfig(config map[string]any) {
|
|
var msg UIMessage
|
|
if c.compactMode {
|
|
msg = c.compactRenderer.RenderDebugConfigMessage(config, time.Now())
|
|
} else {
|
|
msg = c.messageRenderer.RenderDebugConfigMessage(config, time.Now())
|
|
}
|
|
c.messageContainer.AddMessage(msg)
|
|
c.displayContainer()
|
|
}
|
|
|
|
// DisplayHelp renders and displays comprehensive help information showing all
|
|
// available slash commands, keyboard shortcuts, and usage instructions in a
|
|
// formatted system message block.
|
|
func (c *CLI) DisplayHelp() {
|
|
help := `## Available Commands
|
|
|
|
- ` + "`/help`" + `: Show this help message
|
|
- ` + "`/tools`" + `: List all available tools
|
|
- ` + "`/servers`" + `: List configured MCP servers
|
|
- ` + "`/usage`" + `: Show token usage and cost statistics
|
|
- ` + "`/reset-usage`" + `: Reset usage statistics
|
|
- ` + "`/clear`" + `: Clear message history
|
|
- ` + "`/quit`" + `: Exit the application
|
|
- ` + "`Ctrl+C`" + `: Exit at any time
|
|
- ` + "`ESC`" + `: Cancel ongoing LLM generation
|
|
|
|
You can also just type your message to chat with the AI assistant.`
|
|
|
|
// Display as a system message
|
|
msg := c.messageRenderer.RenderSystemMessage(help, time.Now())
|
|
c.messageContainer.AddMessage(msg)
|
|
c.displayContainer()
|
|
}
|
|
|
|
// DisplayTools renders and displays a formatted list of all available tools
|
|
// that can be used by the AI assistant. Each tool is numbered and shown in
|
|
// a system message block for easy reference.
|
|
func (c *CLI) DisplayTools(tools []string) {
|
|
var content strings.Builder
|
|
content.WriteString("## Available Tools\n\n")
|
|
|
|
if len(tools) == 0 {
|
|
content.WriteString("No tools are currently available.")
|
|
} else {
|
|
for i, tool := range tools {
|
|
content.WriteString(fmt.Sprintf("%d. `%s`\n", i+1, tool))
|
|
}
|
|
}
|
|
|
|
// Display as a system message
|
|
msg := c.messageRenderer.RenderSystemMessage(content.String(), time.Now())
|
|
c.messageContainer.AddMessage(msg)
|
|
c.displayContainer()
|
|
}
|
|
|
|
// DisplayServers renders and displays a formatted list of all configured MCP
|
|
// (Model Context Protocol) servers. Each server is numbered and shown in a
|
|
// system message block for easy reference.
|
|
func (c *CLI) DisplayServers(servers []string) {
|
|
var content strings.Builder
|
|
content.WriteString("## Configured MCP Servers\n\n")
|
|
|
|
if len(servers) == 0 {
|
|
content.WriteString("No MCP servers are currently configured.")
|
|
} else {
|
|
for i, server := range servers {
|
|
content.WriteString(fmt.Sprintf("%d. `%s`\n", i+1, server))
|
|
}
|
|
}
|
|
|
|
// Display as a system message
|
|
msg := c.messageRenderer.RenderSystemMessage(content.String(), time.Now())
|
|
c.messageContainer.AddMessage(msg)
|
|
c.displayContainer()
|
|
}
|
|
|
|
// IsSlashCommand determines whether the provided input string is a slash command
|
|
// by checking if it starts with a forward slash (/). Returns true for commands
|
|
// like "/help", "/tools", etc.
|
|
func (c *CLI) IsSlashCommand(input string) bool {
|
|
return strings.HasPrefix(input, "/")
|
|
}
|
|
|
|
// SlashCommandResult encapsulates the outcome of processing a slash command,
|
|
// indicating whether the command was recognized and handled, and whether the
|
|
// conversation history should be cleared as a result of the command.
|
|
type SlashCommandResult struct {
|
|
Handled bool
|
|
ClearHistory bool
|
|
}
|
|
|
|
// HandleSlashCommand processes and executes slash commands, returning a result
|
|
// that indicates whether the command was handled and any side effects. The servers
|
|
// and tools parameters provide context for commands that display available resources.
|
|
// Supported commands include /help, /tools, /servers, /clear, /usage, /reset-usage, and /quit.
|
|
func (c *CLI) HandleSlashCommand(input string, servers []string, tools []string) SlashCommandResult {
|
|
switch input {
|
|
case "/help":
|
|
c.DisplayHelp()
|
|
return SlashCommandResult{Handled: true}
|
|
case "/tools":
|
|
c.DisplayTools(tools)
|
|
return SlashCommandResult{Handled: true}
|
|
case "/servers":
|
|
c.DisplayServers(servers)
|
|
return SlashCommandResult{Handled: true}
|
|
|
|
case "/clear":
|
|
c.ClearMessages()
|
|
c.DisplayInfo("Conversation cleared. Starting fresh.")
|
|
return SlashCommandResult{Handled: true, ClearHistory: true}
|
|
case "/usage":
|
|
c.DisplayUsageStats()
|
|
return SlashCommandResult{Handled: true}
|
|
case "/reset-usage":
|
|
c.ResetUsageStats()
|
|
return SlashCommandResult{Handled: true}
|
|
case "/quit":
|
|
fmt.Println("\n Goodbye!")
|
|
os.Exit(0)
|
|
return SlashCommandResult{Handled: true}
|
|
default:
|
|
return SlashCommandResult{Handled: false}
|
|
}
|
|
}
|
|
|
|
// ClearMessages removes all messages from the display container and refreshes
|
|
// the screen. This is typically used when starting a new conversation or
|
|
// clearing the chat history.
|
|
func (c *CLI) ClearMessages() {
|
|
c.messageContainer.Clear()
|
|
c.displayContainer()
|
|
}
|
|
|
|
// displayContainer renders and displays the message container
|
|
func (c *CLI) displayContainer() {
|
|
|
|
// Add left padding to the entire container
|
|
content := c.messageContainer.Render()
|
|
|
|
// Check if we're displaying a user message
|
|
// User messages should not have additional left padding since they're right-aligned
|
|
// This only applies in non-compact mode
|
|
paddingLeft := 2
|
|
if !c.compactMode && len(c.messageContainer.messages) > 0 {
|
|
lastMessage := c.messageContainer.messages[len(c.messageContainer.messages)-1]
|
|
if lastMessage.Type == UserMessage {
|
|
paddingLeft = 0
|
|
}
|
|
}
|
|
|
|
paddedContent := lipgloss.NewStyle().
|
|
PaddingLeft(paddingLeft).
|
|
Width(c.width). // overwrite (no content) while agent is streaming
|
|
Render(content)
|
|
|
|
if c.lastStreamHeight > 0 {
|
|
// Move cursor up by the height of the last streamed message
|
|
fmt.Printf("\033[%dF", c.lastStreamHeight)
|
|
} else if c.usageDisplayed {
|
|
// If we're not overwriting a streaming message but usage was displayed,
|
|
// move up to account for the usage info (2 lines: content + padding)
|
|
fmt.Printf("\033[2F")
|
|
c.usageDisplayed = false
|
|
}
|
|
|
|
fmt.Println(paddedContent)
|
|
|
|
// clear message history except the "in-progress" message
|
|
if len(c.messageContainer.messages) > 0 {
|
|
// keep the last message, clear the rest (in case of streaming)
|
|
last := c.messageContainer.messages[len(c.messageContainer.messages)-1]
|
|
c.messageContainer.messages = []UIMessage{}
|
|
if last.Streaming {
|
|
// If the last message is still streaming, we keep it
|
|
c.messageContainer.messages = append(c.messageContainer.messages, last)
|
|
c.lastStreamHeight = lipgloss.Height(paddedContent)
|
|
}
|
|
}
|
|
}
|
|
|
|
// UpdateUsage estimates and records token usage based on input and output text.
|
|
// This method uses text-based estimation when actual token counts are not available
|
|
// from the AI provider's response metadata.
|
|
func (c *CLI) UpdateUsage(inputText, outputText string) {
|
|
if c.usageTracker != nil {
|
|
c.usageTracker.EstimateAndUpdateUsage(inputText, outputText)
|
|
}
|
|
}
|
|
|
|
// UpdateUsageFromResponse records token usage using metadata from the AI provider's
|
|
// response when available. Falls back to text-based estimation if the metadata is
|
|
// missing or appears unreliable. This provides more accurate usage tracking when
|
|
// providers supply token count information.
|
|
func (c *CLI) UpdateUsageFromResponse(response *schema.Message, inputText string) {
|
|
if c.usageTracker == nil {
|
|
return
|
|
}
|
|
|
|
// Try to extract token usage from response metadata
|
|
if response.ResponseMeta != nil && response.ResponseMeta.Usage != nil {
|
|
usage := response.ResponseMeta.Usage
|
|
|
|
// Use actual token counts from the response
|
|
inputTokens := int(usage.PromptTokens)
|
|
outputTokens := int(usage.CompletionTokens)
|
|
|
|
// Validate that the metadata seems reasonable
|
|
// If token counts are 0 or seem unrealistic, fall back to estimation
|
|
if inputTokens > 0 && outputTokens > 0 {
|
|
// Handle cache tokens if available (some providers support this)
|
|
cacheReadTokens := 0
|
|
cacheWriteTokens := 0
|
|
|
|
c.usageTracker.UpdateUsage(inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens)
|
|
} else {
|
|
// Metadata exists but seems incomplete/unreliable, use estimation
|
|
c.usageTracker.EstimateAndUpdateUsage(inputText, response.Content)
|
|
}
|
|
} else {
|
|
// Fallback to estimation if no metadata is available
|
|
c.usageTracker.EstimateAndUpdateUsage(inputText, response.Content)
|
|
}
|
|
}
|
|
|
|
// DisplayUsageStats renders and displays comprehensive token usage statistics
|
|
// including the last request's token counts and costs, as well as session totals.
|
|
// Shows a message if usage tracking is not available for the current model.
|
|
func (c *CLI) DisplayUsageStats() {
|
|
if c.usageTracker == nil {
|
|
c.DisplayInfo("Usage tracking is not available for this model.")
|
|
return
|
|
}
|
|
|
|
sessionStats := c.usageTracker.GetSessionStats()
|
|
lastStats := c.usageTracker.GetLastRequestStats()
|
|
|
|
var content strings.Builder
|
|
content.WriteString("## Usage Statistics\n\n")
|
|
|
|
if lastStats != nil {
|
|
content.WriteString(fmt.Sprintf("**Last Request:** %d input + %d output tokens = $%.6f\n",
|
|
lastStats.InputTokens, lastStats.OutputTokens, lastStats.TotalCost))
|
|
}
|
|
|
|
content.WriteString(fmt.Sprintf("**Session Total:** %d input + %d output tokens = $%.6f (%d requests)\n",
|
|
sessionStats.TotalInputTokens, sessionStats.TotalOutputTokens, sessionStats.TotalCost, sessionStats.RequestCount))
|
|
|
|
var msg UIMessage
|
|
if c.compactMode {
|
|
msg = c.compactRenderer.RenderSystemMessage(content.String(), time.Now())
|
|
} else {
|
|
msg = c.messageRenderer.RenderSystemMessage(content.String(), time.Now())
|
|
}
|
|
c.messageContainer.AddMessage(msg)
|
|
c.displayContainer()
|
|
}
|
|
|
|
// ResetUsageStats clears all accumulated usage statistics, resetting token counts
|
|
// and costs to zero. Displays a confirmation message after resetting or an info
|
|
// message if usage tracking is not available.
|
|
func (c *CLI) ResetUsageStats() {
|
|
if c.usageTracker == nil {
|
|
c.DisplayInfo("Usage tracking is not available for this model.")
|
|
return
|
|
}
|
|
|
|
c.usageTracker.Reset()
|
|
c.DisplayInfo("Usage statistics have been reset.")
|
|
}
|
|
|
|
// DisplayUsageAfterResponse renders and displays token usage information immediately
|
|
// following an AI response. This provides real-time feedback about the cost and
|
|
// token consumption of each interaction.
|
|
func (c *CLI) DisplayUsageAfterResponse() {
|
|
if c.usageTracker == nil {
|
|
return
|
|
}
|
|
|
|
usageInfo := c.usageTracker.RenderUsageInfo()
|
|
if usageInfo != "" {
|
|
paddedUsage := lipgloss.NewStyle().
|
|
PaddingLeft(2).
|
|
PaddingTop(1).
|
|
Render(usageInfo)
|
|
fmt.Print(paddedUsage)
|
|
c.usageDisplayed = true
|
|
}
|
|
}
|
|
|
|
// updateSize updates the CLI size based on terminal dimensions
|
|
func (c *CLI) updateSize() {
|
|
width, height, err := term.GetSize(int(os.Stdout.Fd()))
|
|
if err != nil {
|
|
c.width = 80 // Fallback width
|
|
c.height = 24 // Fallback height
|
|
return
|
|
}
|
|
|
|
// 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
|
|
if c.messageRenderer != nil {
|
|
c.messageRenderer.SetWidth(c.width)
|
|
}
|
|
if c.compactRenderer != nil {
|
|
c.compactRenderer.SetWidth(c.width)
|
|
}
|
|
if c.messageContainer != nil {
|
|
c.messageContainer.SetSize(c.width, c.height-4)
|
|
}
|
|
if c.usageTracker != nil {
|
|
c.usageTracker.SetWidth(c.width)
|
|
}
|
|
}
|