From 13ede07ea59bfbe65d34338cf688772c77848c66 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Tue, 10 Jun 2025 01:21:17 +0300 Subject: [PATCH] formatting --- cmd/root.go | 60 ++++++++++++++++----------------- internal/agent/agent.go | 40 +++++++++++----------- internal/config/config.go | 58 ++++++++++++++++---------------- internal/models/providers.go | 4 +-- internal/tools/mcp.go | 12 +++---- internal/ui/callbacks.go | 4 +-- internal/ui/cli.go | 35 ++++++++++---------- internal/ui/messages.go | 64 ++++++++++++++++++------------------ internal/ui/spinner.go | 2 +- internal/ui/styles.go | 2 +- 10 files changed, 139 insertions(+), 142 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 5a140009..9ec79aa2 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -139,7 +139,7 @@ func runNormalMode(ctx context.Context) error { // Load configuration var mcpConfig *config.Config var err error - + if scriptMCPConfig != nil { // Use script-provided config mcpConfig = scriptMCPConfig @@ -232,7 +232,7 @@ func runNormalMode(ctx context.Context) error { if agentMaxSteps == 0 { agentMaxSteps = 1000 // Set a high limit for "unlimited" } - + agentConfig := &agent.AgentConfig{ ModelConfig: modelConfig, MCPConfig: mcpConfig, @@ -288,17 +288,17 @@ func runNormalMode(ctx context.Context) error { // Main interaction logic var messages []*schema.Message - + // Check if running in non-interactive mode if promptFlag != "" { return runNonInteractiveMode(ctx, mcpAgent, cli, promptFlag, modelName, messages, quietFlag) } - + // Quiet mode is not allowed in interactive mode if quietFlag { return fmt.Errorf("--quiet flag can only be used with --prompt/-p") } - + return runInteractiveMode(ctx, mcpAgent, cli, serverNames, toolNames, modelName, messages) } @@ -315,13 +315,13 @@ func runNonInteractiveMode(ctx context.Context, mcpAgent *agent.Agent, cli *ui.C // Get agent response with controlled spinner that stops for tool call display var response *schema.Message var currentSpinner *ui.Spinner - + // Start initial spinner (skip if quiet) if !quiet && cli != nil { currentSpinner = ui.NewSpinner("Thinking...") currentSpinner.Start() } - + response, err := mcpAgent.GenerateWithLoop(ctx, messages, // Tool call handler - called when a tool is about to be executed func(toolName, toolArgs string) { @@ -385,7 +385,7 @@ func runNonInteractiveMode(ctx context.Context, mcpAgent *agent.Agent, cli *ui.C } }, ) - + // Make sure spinner is stopped if still running if !quiet && cli != nil && currentSpinner != nil { currentSpinner.Stop() @@ -454,11 +454,11 @@ func runInteractiveMode(ctx context.Context, mcpAgent *agent.Agent, cli *ui.CLI, // Get agent response with controlled spinner that stops for tool call display var response *schema.Message var currentSpinner *ui.Spinner - + // Start initial spinner currentSpinner = ui.NewSpinner("Thinking...") currentSpinner.Start() - + response, err = mcpAgent.GenerateWithLoop(ctx, messages, // Tool call handler - called when a tool is about to be executed func(toolName, toolArgs string) { @@ -511,7 +511,7 @@ func runInteractiveMode(ctx context.Context, mcpAgent *agent.Agent, cli *ui.CLI, currentSpinner.Start() }, ) - + // Make sure spinner is stopped if still running if currentSpinner != nil { currentSpinner.Stop() @@ -531,17 +531,15 @@ func runInteractiveMode(ctx context.Context, mcpAgent *agent.Agent, cli *ui.CLI, } } - - // runScriptMode handles script mode execution func runScriptMode(ctx context.Context) error { var scriptFile string - + // Determine script file from arguments // When called via shebang, the script file is the first non-flag argument // When called with --script flag, we need to find the script file in args args := os.Args[1:] - + // Filter out flags to find the script file for _, arg := range args { if arg == "--script" { @@ -556,17 +554,17 @@ func runScriptMode(ctx context.Context) error { scriptFile = arg break } - + if scriptFile == "" { return fmt.Errorf("script mode requires a script file argument") } - + // Parse the script file scriptConfig, err := parseScriptFile(scriptFile) if err != nil { return fmt.Errorf("failed to parse script file: %v", err) } - + // Override the global configFile and promptFlag with script values originalConfigFile := configFile originalPromptFlag := promptFlag @@ -580,7 +578,7 @@ func runScriptMode(ctx context.Context) error { originalGoogleAPIKey := googleAPIKey originalOpenAIURL := openaiBaseURL originalAnthropicURL := anthropicBaseURL - + // Create config from script or load normal config var mcpConfig *config.Config if len(scriptConfig.MCPServers) > 0 { @@ -627,10 +625,10 @@ func runScriptMode(ctx context.Context) error { mcpConfig.Prompt = scriptConfig.Prompt } } - + // Override the global config for normal mode scriptMCPConfig = mcpConfig - + // Apply script configuration to global flags if mcpConfig.Prompt != "" { promptFlag = mcpConfig.Prompt @@ -665,7 +663,7 @@ func runScriptMode(ctx context.Context) error { if mcpConfig.AnthropicURL != "" { anthropicBaseURL = mcpConfig.AnthropicURL } - + // Restore original values after execution defer func() { configFile = originalConfigFile @@ -682,7 +680,7 @@ func runScriptMode(ctx context.Context) error { anthropicBaseURL = originalAnthropicURL scriptMCPConfig = nil }() - + // Now run the normal execution path which will use our overridden config return runNormalMode(ctx) } @@ -694,9 +692,9 @@ func parseScriptFile(filename string) (*config.Config, error) { return nil, err } defer file.Close() - + scanner := bufio.NewScanner(file) - + // Skip shebang line if present if scanner.Scan() { line := scanner.Text() @@ -705,7 +703,7 @@ func parseScriptFile(filename string) (*config.Config, error) { return parseScriptContent(line + "\n" + readRemainingLines(scanner)) } } - + // Read the rest of the file content := readRemainingLines(scanner) return parseScriptContent(content) @@ -723,20 +721,20 @@ func readRemainingLines(scanner *bufio.Scanner) string { // parseScriptContent parses the content to extract YAML frontmatter func parseScriptContent(content string) (*config.Config, error) { lines := strings.Split(content, "\n") - + // Find YAML frontmatter var yamlLines []string - + for _, line := range lines { yamlLines = append(yamlLines, line) } - + // Parse YAML yamlContent := strings.Join(yamlLines, "\n") var scriptConfig config.Config if err := yaml.Unmarshal([]byte(yamlContent), &scriptConfig); err != nil { return nil, fmt.Errorf("failed to parse YAML: %v", err) } - + return &scriptConfig, nil -} \ No newline at end of file +} diff --git a/internal/agent/agent.go b/internal/agent/agent.go index c9767236..55ef2dcd 100644 --- a/internal/agent/agent.go +++ b/internal/agent/agent.go @@ -31,12 +31,12 @@ type MessageModifier func(ctx context.Context, input []*schema.Message) []*schem // AgentConfig is the config for agent. type AgentConfig struct { - ModelConfig *models.ProviderConfig - MCPConfig *config.Config - SystemPrompt string - MaxSteps int - MessageWindow int - + ModelConfig *models.ProviderConfig + MCPConfig *config.Config + SystemPrompt string + MaxSteps int + MessageWindow int + // MessageModifier. // modify the input messages before the model is called, it's useful when you want to add some system prompt or other messages. MessageModifier MessageModifier @@ -146,7 +146,7 @@ func NewAgent(ctx context.Context, config *AgentConfig) (*Agent, error) { // Only set up tools if we have any hasTools := len(toolsConfig.Tools) > 0 - + if hasTools { if toolInfos, err = genToolInfos(ctx, toolsConfig); err != nil { return nil, err @@ -185,7 +185,7 @@ func NewAgent(ctx context.Context, config *AgentConfig) (*Agent, error) { if len(state.Messages) > 0 && state.Messages[0].Role == schema.System { hasSystemMessage = true } - + if !hasSystemMessage { systemMsg := schema.SystemMessage(config.SystemPrompt) state.Messages = append([]*schema.Message{systemMsg}, state.Messages...) @@ -368,9 +368,9 @@ func (a *Agent) Stream(ctx context.Context, input []*schema.Message, opts ...com } // GenerateWithLoop processes messages with a custom loop that displays tool calls in real-time -func (a *Agent) GenerateWithLoop(ctx context.Context, messages []*schema.Message, +func (a *Agent) GenerateWithLoop(ctx context.Context, messages []*schema.Message, onToolCall ToolCallHandler, onToolExecution ToolExecutionHandler, onToolResult ToolResultHandler, onResponse ResponseHandler, onToolCallContent ToolCallContentHandler) (*schema.Message, error) { - + // Create a copy of messages to avoid modifying the original workingMessages := make([]*schema.Message, len(messages)) copy(workingMessages, messages) @@ -381,7 +381,7 @@ func (a *Agent) GenerateWithLoop(ctx context.Context, messages []*schema.Message if len(workingMessages) > 0 && workingMessages[0].Role == schema.System { hasSystemMessage = true } - + if !hasSystemMessage { systemMsg := schema.SystemMessage(a.systemPrompt) workingMessages = append([]*schema.Message{systemMsg}, workingMessages...) @@ -392,7 +392,7 @@ func (a *Agent) GenerateWithLoop(ctx context.Context, messages []*schema.Message availableTools := a.toolManager.GetTools() var toolInfos []*schema.ToolInfo toolMap := make(map[string]tool.BaseTool) - + for _, t := range availableTools { info, err := t.Info(ctx) if err != nil { @@ -419,7 +419,7 @@ func (a *Agent) GenerateWithLoop(ctx context.Context, messages []*schema.Message if response.Content != "" && onToolCallContent != nil { onToolCallContent(response.Content) } - + // Handle tool calls for _, toolCall := range response.ToolCalls { // Notify about tool call @@ -433,26 +433,26 @@ func (a *Agent) GenerateWithLoop(ctx context.Context, messages []*schema.Message if onToolExecution != nil { onToolExecution(toolCall.Function.Name, true) } - + output, err := selectedTool.(tool.InvokableTool).InvokableRun(ctx, toolCall.Function.Arguments) - + // Notify tool execution end if onToolExecution != nil { onToolExecution(toolCall.Function.Name, false) } - + if err != nil { errorMsg := fmt.Sprintf("Tool execution error: %v", err) toolMessage := schema.ToolMessage(errorMsg, toolCall.ID) workingMessages = append(workingMessages, toolMessage) - + if onToolResult != nil { onToolResult(toolCall.Function.Name, toolCall.Function.Arguments, errorMsg, true) } } else { toolMessage := schema.ToolMessage(output, toolCall.ID) workingMessages = append(workingMessages, toolMessage) - + if onToolResult != nil { onToolResult(toolCall.Function.Name, toolCall.Function.Arguments, output, false) } @@ -461,7 +461,7 @@ func (a *Agent) GenerateWithLoop(ctx context.Context, messages []*schema.Message errorMsg := fmt.Sprintf("Tool not found: %s", toolCall.Function.Name) toolMessage := schema.ToolMessage(errorMsg, toolCall.ID) workingMessages = append(workingMessages, toolMessage) - + if onToolResult != nil { onToolResult(toolCall.Function.Name, toolCall.Function.Arguments, errorMsg, true) } @@ -493,4 +493,4 @@ func (a *Agent) Close() error { // ExportGraph exports the underlying graph from Agent, along with the []compose.GraphAddNodeOpt to be used when adding this graph to another graph. func (a *Agent) ExportGraph() (compose.AnyGraph, []compose.GraphAddNodeOpt) { return a.graph, a.graphAddNodeOpts -} \ No newline at end of file +} diff --git a/internal/config/config.go b/internal/config/config.go index 7e86bb96..768fdb82 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -10,28 +10,28 @@ import ( // MCPServerConfig represents configuration for an MCP server type MCPServerConfig struct { - Command string `json:"command,omitempty"` - Args []string `json:"args,omitempty"` - URL string `json:"url,omitempty"` - Headers []string `json:"headers,omitempty"` - AllowedTools []string `json:"allowedTools,omitempty"` + Command string `json:"command,omitempty"` + Args []string `json:"args,omitempty"` + URL string `json:"url,omitempty"` + Headers []string `json:"headers,omitempty"` + AllowedTools []string `json:"allowedTools,omitempty"` ExcludedTools []string `json:"excludedTools,omitempty"` } // Config represents the application configuration type Config struct { - MCPServers map[string]MCPServerConfig `json:"mcpServers" yaml:"mcpServers"` - Model string `json:"model,omitempty" yaml:"model,omitempty"` - MaxSteps int `json:"max-steps,omitempty" yaml:"max-steps,omitempty"` - MessageWindow int `json:"message-window,omitempty" yaml:"message-window,omitempty"` - Debug bool `json:"debug,omitempty" yaml:"debug,omitempty"` - SystemPrompt string `json:"system-prompt,omitempty" yaml:"system-prompt,omitempty"` - OpenAIAPIKey string `json:"openai-api-key,omitempty" yaml:"openai-api-key,omitempty"` - AnthropicAPIKey string `json:"anthropic-api-key,omitempty" yaml:"anthropic-api-key,omitempty"` - GoogleAPIKey string `json:"google-api-key,omitempty" yaml:"google-api-key,omitempty"` - OpenAIURL string `json:"openai-url,omitempty" yaml:"openai-url,omitempty"` - AnthropicURL string `json:"anthropic-url,omitempty" yaml:"anthropic-url,omitempty"` - Prompt string `json:"prompt,omitempty" yaml:"prompt,omitempty"` + MCPServers map[string]MCPServerConfig `json:"mcpServers" yaml:"mcpServers"` + Model string `json:"model,omitempty" yaml:"model,omitempty"` + MaxSteps int `json:"max-steps,omitempty" yaml:"max-steps,omitempty"` + MessageWindow int `json:"message-window,omitempty" yaml:"message-window,omitempty"` + Debug bool `json:"debug,omitempty" yaml:"debug,omitempty"` + SystemPrompt string `json:"system-prompt,omitempty" yaml:"system-prompt,omitempty"` + OpenAIAPIKey string `json:"openai-api-key,omitempty" yaml:"openai-api-key,omitempty"` + AnthropicAPIKey string `json:"anthropic-api-key,omitempty" yaml:"anthropic-api-key,omitempty"` + GoogleAPIKey string `json:"google-api-key,omitempty" yaml:"google-api-key,omitempty"` + OpenAIURL string `json:"openai-url,omitempty" yaml:"openai-url,omitempty"` + AnthropicURL string `json:"anthropic-url,omitempty" yaml:"anthropic-url,omitempty"` + Prompt string `json:"prompt,omitempty" yaml:"prompt,omitempty"` } // Validate validates the configuration @@ -52,24 +52,24 @@ type SystemPromptConfig struct { // LoadMCPConfig loads MCP configuration from file func LoadMCPConfig(configFile string) (*Config, error) { v := viper.New() - + if configFile == "" { homeDir, err := os.UserHomeDir() if err != nil { return nil, fmt.Errorf("error getting home directory: %v", err) } - + // Try .mcphost files first (new format), then .mcp files (backwards compatibility) configNames := []string{".mcphost", ".mcp"} configTypes := []string{"yaml", "json"} - + var configFound bool for _, configName := range configNames { for _, configType := range configTypes { v.SetConfigName(configName) v.SetConfigType(configType) v.AddConfigPath(homeDir) - + if err := v.ReadInConfig(); err == nil { configFound = true break @@ -79,7 +79,7 @@ func LoadMCPConfig(configFile string) (*Config, error) { break } } - + if !configFound { // Create default config file if err := createDefaultConfig(homeDir); err != nil { @@ -88,7 +88,7 @@ func LoadMCPConfig(configFile string) (*Config, error) { MCPServers: make(map[string]MCPServerConfig), }, nil } - + // Try to load the newly created config v.SetConfigName(".mcphost") v.SetConfigType("yaml") @@ -129,7 +129,7 @@ func LoadSystemPrompt(filePath string) (string, error) { v := viper.New() v.SetConfigFile(filePath) - + if err := v.ReadInConfig(); err != nil { return "", fmt.Errorf("error reading system prompt file: %v", err) } @@ -145,14 +145,14 @@ func LoadSystemPrompt(filePath string) (string, error) { // createDefaultConfig creates a default .mcphost.yml file in the user's home directory func createDefaultConfig(homeDir string) error { configPath := filepath.Join(homeDir, ".mcphost.yml") - + // Create the file file, err := os.Create(configPath) if err != nil { return fmt.Errorf("error creating config file: %v", err) } defer file.Close() - + // Write a clean YAML template content := `# MCPHost Configuration File # All command-line flags can be configured here @@ -184,11 +184,11 @@ mcpServers: # openai-url: "https://api.openai.com/v1" # anthropic-url: "https://api.anthropic.com" ` - + _, err = file.WriteString(content) if err != nil { return fmt.Errorf("error writing config content: %v", err) } - + return nil -} \ No newline at end of file +} diff --git a/internal/models/providers.go b/internal/models/providers.go index 60b5a733..f66afaba 100644 --- a/internal/models/providers.go +++ b/internal/models/providers.go @@ -6,11 +6,11 @@ import ( "os" "strings" - "github.com/cloudwego/eino/components/model" "github.com/cloudwego/eino-ext/components/model/claude" "github.com/cloudwego/eino-ext/components/model/gemini" "github.com/cloudwego/eino-ext/components/model/ollama" "github.com/cloudwego/eino-ext/components/model/openai" + "github.com/cloudwego/eino/components/model" "github.com/google/generative-ai-go/genai" "google.golang.org/api/option" ) @@ -130,4 +130,4 @@ func createOllamaProvider(ctx context.Context, config *ProviderConfig, modelName } return ollama.NewChatModel(ctx, ollamaConfig) -} \ No newline at end of file +} diff --git a/internal/tools/mcp.go b/internal/tools/mcp.go index 5118da37..697d9ecf 100644 --- a/internal/tools/mcp.go +++ b/internal/tools/mcp.go @@ -120,7 +120,7 @@ func (m *MCPToolManager) LoadTools(ctx context.Context, config *config.Config) e if !m.shouldIncludeTool(mcpTool.Name, serverConfig) { continue } - + einoTool := &MCPTool{ client: client, toolInfo: &mcpTool, @@ -159,7 +159,7 @@ func (m *MCPToolManager) shouldIncludeTool(toolName string, serverConfig config. } return false } - + // If excludedTools is specified, exclude tools in the list if len(serverConfig.ExcludedTools) > 0 { for _, excludedTool := range serverConfig.ExcludedTools { @@ -168,7 +168,7 @@ func (m *MCPToolManager) shouldIncludeTool(toolName string, serverConfig config. } } } - + // Include by default return true } @@ -183,12 +183,12 @@ func (m *MCPToolManager) createMCPClient(ctx context.Context, serverName string, if err != nil { return nil, err } - + // Start the SSE client if err := sseClient.Start(ctx); err != nil { return nil, fmt.Errorf("failed to start SSE client: %v", err) } - + return sseClient, nil } @@ -205,4 +205,4 @@ func (m *MCPToolManager) initializeClient(ctx context.Context, client client.MCP _, err := client.Initialize(ctx, initRequest) return err -} \ No newline at end of file +} diff --git a/internal/ui/callbacks.go b/internal/ui/callbacks.go index 9c66a627..92e0b7c5 100644 --- a/internal/ui/callbacks.go +++ b/internal/ui/callbacks.go @@ -30,8 +30,8 @@ func (c *CLI) CreateCallbackHandler() callbacks.Handler { return ctx }, } - + return utilCallbacks.NewHandlerHelper(). Tool(toolHandler). Handler() -} \ No newline at end of file +} diff --git a/internal/ui/cli.go b/internal/ui/cli.go index beeafa9b..9b677e98 100644 --- a/internal/ui/cli.go +++ b/internal/ui/cli.go @@ -32,7 +32,7 @@ func NewCLI() (*CLI, error) { cli.updateSize() cli.messageRenderer = NewMessageRenderer(cli.width) cli.messageContainer = NewMessageContainer(cli.width, cli.height-4) // Reserve space for input and help - + return cli, nil } @@ -46,10 +46,10 @@ func (c *CLI) GetPrompt() (string, error) { BorderForeground(mutedColor). MarginTop(1). MarginBottom(1) - + // Render the divider fmt.Print(dividerStyle.Render("")) - + var prompt string err := huh.NewForm(huh.NewGroup(huh.NewText(). Title("Enter your prompt (Type /help for commands, Ctrl+C to quit)"). @@ -73,11 +73,11 @@ func (c *CLI) GetPrompt() (string, error) { func (c *CLI) ShowSpinner(message string, action func() error) error { spinner := NewSpinner(message) spinner.Start() - + err := action() - + spinner.Stop() - + return err } @@ -104,7 +104,7 @@ func (c *CLI) DisplayAssistantMessageWithModel(message, modelName string) error // DisplayToolCallMessage displays a tool call in progress func (c *CLI) DisplayToolCallMessage(toolName, toolArgs string) { msg := c.messageRenderer.RenderToolCallMessage(toolName, toolArgs, time.Now()) - + // Always display immediately - spinner management is handled externally c.messageContainer.AddMessage(msg) c.displayContainer() @@ -113,7 +113,7 @@ func (c *CLI) DisplayToolCallMessage(toolName, toolArgs string) { // DisplayToolMessage displays a tool call message func (c *CLI) DisplayToolMessage(toolName, toolArgs, toolResult string, isError bool) { msg := c.messageRenderer.RenderToolMessage(toolName, toolArgs, toolResult, isError) - + // Always display immediately - spinner management is handled externally c.messageContainer.AddMessage(msg) c.displayContainer() @@ -123,7 +123,7 @@ func (c *CLI) DisplayToolMessage(toolName, toolArgs, toolResult string, isError func (c *CLI) DisplayStreamingMessage(reader *schema.StreamReader[*schema.Message]) error { // For streaming, we'll collect the content and then display it var content strings.Builder - + for { msg, err := reader.Recv() if err == io.EOF { @@ -175,7 +175,7 @@ You can also just type your message to chat with the AI assistant.` 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 { @@ -183,7 +183,7 @@ func (c *CLI) DisplayTools(tools []string) { 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) @@ -194,7 +194,7 @@ func (c *CLI) DisplayTools(tools []string) { 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 { @@ -202,7 +202,7 @@ func (c *CLI) DisplayServers(servers []string) { 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) @@ -213,7 +213,7 @@ func (c *CLI) DisplayServers(servers []string) { func (c *CLI) DisplayHistory(messages []*schema.Message) { // Create a temporary container for history historyContainer := NewMessageContainer(c.width, c.height-4) - + for _, msg := range messages { switch msg.Role { case schema.User: @@ -224,7 +224,7 @@ func (c *CLI) DisplayHistory(messages []*schema.Message) { historyContainer.AddMessage(uiMsg) } } - + fmt.Println("\nConversation History:") fmt.Println(historyContainer.Render()) } @@ -282,10 +282,10 @@ func (c *CLI) updateSize() { c.height = 24 // Fallback height return } - + c.width = width c.height = height - + // Update renderers if they exist if c.messageRenderer != nil { c.messageRenderer.SetWidth(c.width) @@ -294,4 +294,3 @@ func (c *CLI) updateSize() { c.messageContainer.SetSize(c.width, c.height-4) } } - diff --git a/internal/ui/messages.go b/internal/ui/messages.go index aa0bea66..61321dab 100644 --- a/internal/ui/messages.go +++ b/internal/ui/messages.go @@ -22,12 +22,12 @@ const ( // UIMessage represents a rendered message for display type UIMessage struct { - ID string - Type MessageType - Position int - Height int - Content string - Timestamp time.Time + ID string + Type MessageType + Position int + Height int + Content string + Timestamp time.Time } // Color constants @@ -61,7 +61,7 @@ func (r *MessageRenderer) SetWidth(width int) { // RenderUserMessage renders a user message with proper styling 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). @@ -74,7 +74,7 @@ func (r *MessageRenderer) RenderUserMessage(content string, timestamp time.Time) // Format timestamp timeStr := timestamp.Local().Format("02 Jan 2006 03:04 PM") username := "You" - + // Create info line info := baseStyle. Width(r.width - 1). @@ -83,7 +83,7 @@ func (r *MessageRenderer) RenderUserMessage(content string, timestamp time.Time) // Render the message content messageContent := r.renderMarkdown(content, r.width-2) - + // Combine content and info parts := []string{ strings.TrimSuffix(messageContent, "\n"), @@ -105,7 +105,7 @@ func (r *MessageRenderer) RenderUserMessage(content string, timestamp time.Time) // RenderAssistantMessage renders an assistant message with proper styling 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). @@ -120,7 +120,7 @@ func (r *MessageRenderer) RenderAssistantMessage(content string, timestamp time. if modelName == "" { modelName = "Assistant" } - + // Create info line info := baseStyle. Width(r.width - 1). @@ -129,7 +129,7 @@ func (r *MessageRenderer) RenderAssistantMessage(content string, timestamp time. // Render the message content messageContent := r.renderMarkdown(content, r.width-2) - + // Handle empty content if strings.TrimSpace(content) == "" { messageContent = baseStyle. @@ -137,7 +137,7 @@ func (r *MessageRenderer) RenderAssistantMessage(content string, timestamp time. Foreground(mutedColor). Render("*Finished without output*") } - + // Combine content and info parts := []string{ strings.TrimSuffix(messageContent, "\n"), @@ -159,7 +159,7 @@ func (r *MessageRenderer) RenderAssistantMessage(content string, timestamp time. // RenderSystemMessage renders a system message (help, tools, etc.) with proper styling 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). @@ -171,7 +171,7 @@ func (r *MessageRenderer) RenderSystemMessage(content string, timestamp time.Tim // Format timestamp timeStr := timestamp.Local().Format("02 Jan 2006 03:04 PM") - + // Create info line with MCPHost label info := baseStyle. Width(r.width - 1). @@ -180,7 +180,7 @@ func (r *MessageRenderer) RenderSystemMessage(content string, timestamp time.Tim // Render the message content with markdown messageContent := r.renderMarkdown(content, r.width-2) - + // Handle empty content if strings.TrimSpace(content) == "" { messageContent = baseStyle. @@ -188,7 +188,7 @@ func (r *MessageRenderer) RenderSystemMessage(content string, timestamp time.Tim Foreground(mutedColor). Render("*No content*") } - + // Combine content and info parts := []string{ strings.TrimSuffix(messageContent, "\n"), @@ -210,7 +210,7 @@ func (r *MessageRenderer) RenderSystemMessage(content string, timestamp time.Tim // RenderErrorMessage renders an error message with proper styling 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). @@ -222,7 +222,7 @@ func (r *MessageRenderer) RenderErrorMessage(errorMsg string, timestamp time.Tim // Format timestamp timeStr := timestamp.Local().Format("02 Jan 2006 03:04 PM") - + // Create info line with Error label info := baseStyle. Width(r.width - 1). @@ -234,7 +234,7 @@ func (r *MessageRenderer) RenderErrorMessage(errorMsg string, timestamp time.Tim Foreground(errorColor). Bold(true). Render(fmt.Sprintf("❌ %s", errorMsg)) - + // Combine content and info parts := []string{ errorContent, @@ -256,7 +256,7 @@ func (r *MessageRenderer) RenderErrorMessage(errorMsg string, timestamp time.Tim // RenderToolCallMessage renders a tool call in progress with proper styling 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). @@ -268,7 +268,7 @@ func (r *MessageRenderer) RenderToolCallMessage(toolName, toolArgs string, times // Format timestamp timeStr := timestamp.Local().Format("02 Jan 2006 03:04 PM") - + // Create header with tool icon and name toolIcon := "🔧" header := baseStyle. @@ -313,7 +313,7 @@ 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). @@ -348,7 +348,7 @@ func (r *MessageRenderer) RenderToolMessage(toolName, toolArgs, toolResult strin // Combine parts headerLine := lipgloss.JoinHorizontal(lipgloss.Left, toolNameText, argsText) parts := []string{headerLine} - + if resultContent != "" { parts = append(parts, strings.TrimSuffix(resultContent, "\n")) } @@ -373,25 +373,25 @@ func (r *MessageRenderer) formatToolArgs(args string) string { args = strings.TrimSuffix(args, "}") args = strings.TrimSpace(args) } - + // If it's empty after cleanup, return a placeholder if args == "" { return "(no arguments)" } - + // Truncate if too long maxLen := 100 if len(args) > maxLen { return args[:maxLen] + "..." } - + return args } // formatToolResult formats tool results based on tool type func (r *MessageRenderer) formatToolResult(toolName, result string, width int) string { baseStyle := lipgloss.NewStyle() - + // Truncate very long results maxLines := 10 lines := strings.Split(result, "\n") @@ -416,11 +416,11 @@ func (r *MessageRenderer) formatToolResult(toolName, result string, width int) s func (r *MessageRenderer) truncateText(text string, maxWidth int) string { // Replace newlines with spaces for single-line display text = strings.ReplaceAll(text, "\n", " ") - + if lipgloss.Width(text) <= maxWidth { return text } - + // Simple truncation - could be improved with proper unicode handling for i := len(text) - 1; i >= 0; i-- { truncated := text[:i] + "..." @@ -428,7 +428,7 @@ func (r *MessageRenderer) truncateText(text string, maxWidth int) string { return truncated } } - + return "..." } @@ -524,4 +524,4 @@ func (c *MessageContainer) renderEmptyState() string { "", ), ) -} \ No newline at end of file +} diff --git a/internal/ui/spinner.go b/internal/ui/spinner.go index 91f897e9..e66dd0fa 100644 --- a/internal/ui/spinner.go +++ b/internal/ui/spinner.go @@ -124,4 +124,4 @@ func (s *Spinner) Start() { func (s *Spinner) Stop() { s.cancel() <-s.done -} \ No newline at end of file +} diff --git a/internal/ui/styles.go b/internal/ui/styles.go index e1ae6abf..4cd4ebe5 100644 --- a/internal/ui/styles.go +++ b/internal/ui/styles.go @@ -294,4 +294,4 @@ func toMarkdown(content string, width int) string { r := GetMarkdownRenderer(width) rendered, _ := r.Render(content) return rendered -} \ No newline at end of file +}