released the mock

This commit is contained in:
Luxferre
2026-03-21 18:32:07 +02:00
parent 6db2d20652
commit 4b2474b403
5 changed files with 305 additions and 28 deletions
+31 -21
View File
@@ -117,6 +117,27 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
) )
switch msg := msg.(type) { 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: case tea.KeyMsg:
switch msg.Type { switch msg.Type {
case tea.KeyCtrlC, tea.KeyEsc: 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 { if strings.TrimSpace(v) == "" || m.isThinking {
return m, nil 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.textarea.Reset()
m.viewport.SetContent(strings.Join(m.messages, "\n\n")) m.viewport.SetContent(strings.Join(m.messages, "\n\n"))
m.viewport.GotoBottom() m.viewport.GotoBottom()
m.isThinking = true m.isThinking = true
return m, m.runAgent(v) return m, m.runAgent(v)
} }
case agentResponseMsg: case agentResponseMsg:
m.isThinking = false m.isThinking = false
var newMsg string
if msg.err != nil { 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 { } 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.SetContent(strings.Join(m.messages, "\n\n"))
m.viewport.GotoBottom() m.viewport.GotoBottom()
return m, nil 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) m.textarea, tiCmd = m.textarea.Update(msg)
@@ -192,7 +202,7 @@ func (m model) View() string {
statusStyle.Render(" "+status+" "), 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 taStyle := textareaStyle
if m.textarea.Focused() { if m.textarea.Focused() {
+17
View File
@@ -1,8 +1,10 @@
package main package main
import ( import (
"strings"
"testing" "testing"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
) )
@@ -54,3 +56,18 @@ func TestModelUpdate(t *testing.T) {
t.Error("expected message list to grow after response") 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")
}
}
+95
View File
@@ -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"
]
+89 -7
View File
@@ -1,30 +1,112 @@
package sidekick package sidekick
import ( import (
"context" "bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
) )
// LLMClient represents an abstraction over the LLM provider // LLMClient represents an abstraction over the LLM provider
type LLMClient interface { 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) // SetLLMClient allows overriding the LLM client (e.g. for testing)
func SetLLMClient(client LLMClient) { func SetLLMClient(client LLMClient) {
defaultLLMClient = client defaultLLMClient = client
} }
// CallLLM invokes the configured LLM client // CallLLM invokes the configured LLM client
func CallLLM(ctx context.Context, model ModelConfig, msgs []Message, tools map[string]ToolDefinition) (Message, error) { 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. // mockLLMClient is used as a fallback if no actual HTTP client is injected.
type mockLLMClient struct { type mockLLMClient struct {
responses []Message responses []Message
calls int calls int
} }
func (m *mockLLMClient) Call(ctx context.Context, model ModelConfig, msgs []Message, ts map[string]ToolDefinition) (Message, error) { func (m *mockLLMClient) Call(ctx context.Context, model ModelConfig, msgs []Message, ts map[string]ToolDefinition) (Message, error) {
+73
View File
@@ -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.
"""