From 4b2474b403d306b303f60bd9bfef4c9ff935f7db Mon Sep 17 00:00:00 2001 From: Luxferre Date: Sat, 21 Mar 2026 18:32:07 +0200 Subject: [PATCH] released the mock --- cmd/sidekick-tui/main.go | 52 +++++++++++-------- cmd/sidekick-tui/tui_test.go | 17 +++++++ config.toml | 95 +++++++++++++++++++++++++++++++++++ llm.go | 96 +++++++++++++++++++++++++++++++++--- prompts.toml | 73 +++++++++++++++++++++++++++ 5 files changed, 305 insertions(+), 28 deletions(-) create mode 100644 config.toml create mode 100644 prompts.toml diff --git a/cmd/sidekick-tui/main.go b/cmd/sidekick-tui/main.go index 2d7cbac..43e1bd0 100644 --- a/cmd/sidekick-tui/main.go +++ b/cmd/sidekick-tui/main.go @@ -117,6 +117,27 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { ) switch msg := msg.(type) { + case tea.WindowSizeMsg: + m.width = msg.Width + m.height = msg.Height + + headerHeight := 1 + inputHeight := 5 // Textarea height(3) + border/padding + + vpWidth := msg.Width - 4 // Account for viewportStyle padding and border + vpHeight := msg.Height - headerHeight - inputHeight - 2 + + if !m.ready { + m.viewport = viewport.New(vpWidth, vpHeight) + m.viewport.SetContent(strings.Join(m.messages, "\n\n")) + m.ready = true + } else { + m.viewport.Width = vpWidth + m.viewport.Height = vpHeight + } + + m.textarea.SetWidth(msg.Width - 4) + case tea.KeyMsg: switch msg.Type { case tea.KeyCtrlC, tea.KeyEsc: @@ -129,40 +150,29 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if strings.TrimSpace(v) == "" || m.isThinking { return m, nil } - m.messages = append(m.messages, userStyle.Render("You")+"\n"+v) + + wrappedUser := lipgloss.NewStyle().Width(m.viewport.Width).Render(userStyle.Render("You") + "\n" + v) + m.messages = append(m.messages, wrappedUser) m.textarea.Reset() m.viewport.SetContent(strings.Join(m.messages, "\n\n")) m.viewport.GotoBottom() m.isThinking = true return m, m.runAgent(v) } + case agentResponseMsg: m.isThinking = false + var newMsg string if msg.err != nil { - m.messages = append(m.messages, sysStyle.Render(fmt.Sprintf("Error: %v", msg.err))) + newMsg = sysStyle.Render(fmt.Sprintf("Error: %v", msg.err)) } else { - m.messages = append(m.messages, agentStyle.Render("Sidekick")+"\n"+msg.response) + newMsg = agentStyle.Render("Sidekick") + "\n" + msg.response } + wrappedMsg := lipgloss.NewStyle().Width(m.viewport.Width).Render(newMsg) + m.messages = append(m.messages, wrappedMsg) m.viewport.SetContent(strings.Join(m.messages, "\n\n")) m.viewport.GotoBottom() return m, nil - case tea.WindowSizeMsg: - m.width = msg.Width - m.height = msg.Height - - headerHeight := 1 - inputHeight := 5 // Textarea height(3) + border/padding - - if !m.ready { - m.viewport = viewport.New(msg.Width-2, msg.Height-headerHeight-inputHeight-2) - m.viewport.SetContent(strings.Join(m.messages, "\n\n")) - m.ready = true - } else { - m.viewport.Width = msg.Width - 2 - m.viewport.Height = msg.Height - headerHeight - inputHeight - 2 - } - - m.textarea.SetWidth(msg.Width - 4) } m.textarea, tiCmd = m.textarea.Update(msg) @@ -192,7 +202,7 @@ func (m model) View() string { statusStyle.Render(" "+status+" "), ) - vpView := viewportStyle.Width(m.width - 2).Height(m.height - 10).Render(m.viewport.View()) + vpView := viewportStyle.Width(m.width - 2).Render(m.viewport.View()) taStyle := textareaStyle if m.textarea.Focused() { diff --git a/cmd/sidekick-tui/tui_test.go b/cmd/sidekick-tui/tui_test.go index e09d665..374eb01 100644 --- a/cmd/sidekick-tui/tui_test.go +++ b/cmd/sidekick-tui/tui_test.go @@ -1,8 +1,10 @@ package main import ( + "strings" "testing" + "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" ) @@ -54,3 +56,18 @@ func TestModelUpdate(t *testing.T) { t.Error("expected message list to grow after response") } } + +func TestMessageWrapping(t *testing.T) { + m := initialModel() + m.ready = true + m.viewport = viewport.New(10, 5) // Very narrow + + longMsg := "This is a very long message that should be wrapped." + respModel, _ := m.Update(agentResponseMsg{response: longMsg}) + rm := respModel.(model) + + lastMsg := rm.messages[len(rm.messages)-1] + if !strings.Contains(lastMsg, "\n") { + t.Errorf("expected long message to be wrapped, but no newline found") + } +} diff --git a/config.toml b/config.toml new file mode 100644 index 0000000..de45e94 --- /dev/null +++ b/config.toml @@ -0,0 +1,95 @@ +tool_response_threshold = 10000 + +[mcp_listener] +transport = "http" +port = 8080 + +[models] + [models.default] + model_id = "z-ai/glm5" + endpoint = "https://integrate.api.nvidia.com/v1" + api_key = "env:SK_NV_API_KEY" + temperature = 0.0 + max_tokens = 4096 + timeout_secs = 120 + + [models.fast] + model_id = "minimaxai/minimax-m2.5" + endpoint = "https://integrate.api.nvidia.com/v1" + api_key = "env:SK_NV_API_KEY" + temperature = 0.1 + max_tokens = 1024 + timeout_secs = 60 + +[mcp_servers] + [mcp_servers.filesystem] + transport = "stdio" + command = "npx" + args = ["-y", "@modelcontextprotocol/server-filesystem", "."] + env = [] + + [mcp_servers.context7] + transport = "http" + url = "https://mcp.context7.com/mcp" + + [mcp_servers.exa] + transport = "stdio" + command = "npx" + args = ["-y", "@exa/mcp-server"] + +[agents] + [agents.coordinator] + role = "COORDINATOR" + model_id = "default" + enable_shell_exec = true + system_prompt = "You are the Lead Architect. You oversee the software development lifecycle, plan features, and delegate implementation to specialized subagents." + description = "Lead Architect and project coordinator" + max_iterations = 15 + subagents = ["coder", "reviewer", "tester"] + toolsets = ["filesystem", "context7", "exa"] + goals = [ + "Coordinate complex software engineering tasks", + "Ensure high-quality code delivery by delegating to specialized agents", + "Validate the final solution using shell execution and test results" + ] + + [agents.coder] + role = "SPECIALIST" + model_id = "default" + enable_shell_exec = true + system_prompt = "You are a Senior Software Engineer specializing in implementation. Your goal is to write clean, efficient, and well-documented code based on the architect's instructions." + description = "Senior Developer focused on implementation and refactoring" + max_iterations = 10 + toolsets = ["filesystem", "context7"] + goals = [ + "Implement features and bug fixes with precision", + "Adhere to project-specific coding standards and best practices", + "Verify changes by running builds or small code snippets" + ] + + [agents.reviewer] + role = "SPECIALIST" + model_id = "fast" + system_prompt = "You are a Quality Assurance Specialist and Code Reviewer. Your role is to analyze code for potential bugs, security vulnerabilities, and style violations." + description = "Code Reviewer and Quality Assurance specialist" + max_iterations = 8 + toolsets = ["filesystem", "context7"] + goals = [ + "Perform thorough code reviews and identify edge cases", + "Suggest optimizations and improvements", + "Use search tools and context7 for deep library/best practice research" + ] + + [agents.tester] + role = "SPECIALIST" + model_id = "fast" + enable_shell_exec = true + system_prompt = "You are a Test Engineer. Your primary responsibility is to write and execute tests to ensure software reliability and correctness." + description = "Test Engineer focused on automated testing and verification" + max_iterations = 10 + toolsets = ["filesystem"] + goals = [ + "Develop comprehensive test suites (unit, integration, e2e)", + "Run tests and analyze failures to provide actionable feedback", + "Ensure 100% verification of implemented features" + ] diff --git a/llm.go b/llm.go index 426c603..d90f7ca 100644 --- a/llm.go +++ b/llm.go @@ -1,30 +1,112 @@ package sidekick import ( - "context" + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" ) // LLMClient represents an abstraction over the LLM provider type LLMClient interface { - Call(ctx context.Context, model ModelConfig, messages []Message, tools map[string]ToolDefinition) (Message, error) + Call(ctx context.Context, model ModelConfig, messages []Message, tools map[string]ToolDefinition) (Message, error) } -var defaultLLMClient LLMClient = &mockLLMClient{} +// standardLLMClient is an OpenAI-compatible implementation +type standardLLMClient struct{} + +func (s *standardLLMClient) Call(ctx context.Context, model ModelConfig, msgs []Message, ts map[string]ToolDefinition) (Message, error) { + client := &http.Client{ + Timeout: model.Timeout, + } + + reqBody := map[string]interface{}{ + "model": model.ModelID, + "messages": msgs, + "temperature": model.Temperature, + } + if model.MaxTokens > 0 { + reqBody["max_tokens"] = model.MaxTokens + } + + if len(ts) > 0 { + var tools []map[string]interface{} + for _, t := range ts { + tools = append(tools, t.ConvertToOpenAITool()) + } + reqBody["tools"] = tools + } + + jsonBody, err := json.Marshal(reqBody) + if err != nil { + return Message{}, fmt.Errorf("failed to marshal request: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, "POST", model.Endpoint+"/chat/completions", bytes.NewBuffer(jsonBody)) + if err != nil { + return Message{}, fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+model.APIKey) + + resp, err := client.Do(req) + if err != nil { + return Message{}, fmt.Errorf("HTTP request failed: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return Message{}, fmt.Errorf("failed to read response body: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return Message{}, fmt.Errorf("API returned error (%d): %s", resp.StatusCode, string(body)) + } + + var openAIResp struct { + Choices []struct { + Message Message `json:"message"` + } `json:"choices"` + Error *struct { + Message string `json:"message"` + } `json:"error,omitempty"` + } + + if err := json.Unmarshal(body, &openAIResp); err != nil { + return Message{}, fmt.Errorf("failed to unmarshal response: %w", err) + } + + if openAIResp.Error != nil { + return Message{}, fmt.Errorf("API error: %s", openAIResp.Error.Message) + } + + if len(openAIResp.Choices) == 0 { + return Message{}, fmt.Errorf("API returned no choices") + } + + return openAIResp.Choices[0].Message, nil +} + +var defaultLLMClient LLMClient = &standardLLMClient{} // SetLLMClient allows overriding the LLM client (e.g. for testing) func SetLLMClient(client LLMClient) { - defaultLLMClient = client + defaultLLMClient = client } // CallLLM invokes the configured LLM client func CallLLM(ctx context.Context, model ModelConfig, msgs []Message, tools map[string]ToolDefinition) (Message, error) { - return defaultLLMClient.Call(ctx, model, msgs, tools) + return defaultLLMClient.Call(ctx, model, msgs, tools) } // mockLLMClient is used as a fallback if no actual HTTP client is injected. type mockLLMClient struct { - responses []Message - calls int + responses []Message + calls int } func (m *mockLLMClient) Call(ctx context.Context, model ModelConfig, msgs []Message, ts map[string]ToolDefinition) (Message, error) { diff --git a/prompts.toml b/prompts.toml new file mode 100644 index 0000000..3f13e1f --- /dev/null +++ b/prompts.toml @@ -0,0 +1,73 @@ +# Sidekick Multiline System Prompts Example +# Use {{template "name"}} to include other prompts as subprompts. + +base_identity = """ +You are a highly capable AI software engineer operating within the Sidekick framework. +Your role is to assist the user by following instructions precisely and using +the available tools or subagents effectively. Always be professional, concise, +and technical. +""" + +base_formatting = """ +When using tools: +1. State your reasoning for calling the tool. +2. Formulate the JSON action with the correct parameters. +3. If a tool result is buffered to a file, use read_file or grep_file to inspect it. + +When responding to the user: +- Use Markdown for formatting and code blocks with appropriate language tags. +- Be direct, technical, and precise. +""" + +coding_standards = """ +Adhere to these universal coding standards: +- Use clear, descriptive variable and function names. +- Keep functions small and focused on a single responsibility. +- Include meaningful comments for complex logic. +- Ensure proper error handling and logging. +""" + +coordinator = """ +{{template "base_identity"}} +{{template "base_formatting"}} + +You are the COORDINATOR (Lead Architect). You are the primary point of contact for the user. +Your workflow: +1. Analyze the user's request and map out the necessary changes. +2. Use the 'coder' subagent to implement the changes. +3. Use the 'reviewer' subagent to verify the code quality. +4. Use the 'tester' subagent to ensure everything works as expected. +5. If any step fails, iterate with the relevant subagent. + +You have full shell access to run builds and orchestrate the entire process. +""" + +coder = """ +{{template "base_identity"}} +{{template "base_formatting"}} +{{template "coding_standards"}} + +You are the CODER specialist. Your task is to implement specific features or fixes +as directed by the Architect. You should focus on writing the actual code and +ensuring it compiles/runs. Use shell_exec sparingly to verify your changes if necessary. +""" + +reviewer = """ +{{template "base_identity"}} +{{template "base_formatting"}} + +You are the REVIEWER specialist. Your goal is to find flaws. +Do not just look for syntax errors; look for logical bugs, inefficient algorithms, +and potential security risks. Compare the implementation against the requested +requirements and coding standards. +""" + +tester = """ +{{template "base_identity"}} +{{template "base_formatting"}} + +You are the TESTER specialist. You must ensure the software is bulletproof. +Write test cases that cover both happy paths and edge cases. +Use shell_exec to run the project's test suite and report results back +to the Architect. +"""