Merge pull request #22 from cheikh2shift/windows-fixes

Enhance Windows compatibility and improve tool descriptions
This commit is contained in:
Cheikh Seck authored and GitHub committed 2026-06-30 22:29:59 +00:00
commit 78a341aa9e
4 files changed
+85 -44

No files matched your search

+66 -39
View File
@@ -16,6 +16,7 @@ import (
"os/signal"
"path/filepath"
"runtime"
"sort"
"strconv"
"strings"
"sync"
@@ -406,10 +407,7 @@ func main() {
}
hiveMgr, err = hive.NewManager(hiveCode, baseDir, provider.Model, maxTokens, getServerNames(servers), statusCh, func(ctx context.Context, prompt string) (string, error) {
native := llmProvider != nil && llmProvider.SupportsNativeToolCalls()
toolsDesc := ""
if !native {
toolsDesc = getToolsDescription(servers)
}
toolsDesc := getToolsDescription(servers)
fullPrompt := buildFullPrompt(native, toolsDesc, "", wd, prompt, "", hiveCode)
maxRounds := 10
if provider.MaxToolRounds != nil && *provider.MaxToolRounds > 0 {
@@ -629,10 +627,7 @@ promptLoop:
completionMsg := fmt.Sprintf("Hive worker %s has completed and sent back the following result:\n%s", workerLabel, payload)
sessionContext := buildSessionContext(prevSession, agentsContext, commitContextPath, commitContextRef, wd)
native := llmProvider != nil && llmProvider.SupportsNativeToolCalls()
toolsDesc := ""
if !native {
toolsDesc = getToolsDescription(servers)
}
toolsDesc := getToolsDescription(servers)
var hiveInstanceID string
if hiveMgr != nil {
hiveInstanceID = hiveMgr.Instance().ID
@@ -909,12 +904,7 @@ promptLoop:
}
nativeToolCalls := llmProvider.SupportsNativeToolCalls()
var toolsSection string
if nativeToolCalls {
toolsSection = ""
} else {
toolsSection = fmt.Sprintf("\nYou have access to these tools:\n%s\n", getToolsDescription(servers))
}
toolsSection := getToolsDescription(servers)
sessionContext := buildSessionContext(prevSession, agentsContext, commitContextPath, commitContextRef, wd)
@@ -1258,10 +1248,7 @@ promptLoop:
completionMsg := fmt.Sprintf("Hive worker %s has completed and sent back the following result:\n%s", workerLabel, payload)
sessionContext := buildSessionContext(prevSession, agentsContext, commitContextPath, commitContextRef, wd)
native := llmProvider != nil && llmProvider.SupportsNativeToolCalls()
toolsDesc := ""
if !native {
toolsDesc = getToolsDescription(servers)
}
toolsDesc := getToolsDescription(servers)
var hiveInstanceID string
if hiveMgr != nil {
hiveInstanceID = hiveMgr.Instance().ID
@@ -2062,10 +2049,7 @@ func runSinglePrompt(ctx context.Context, provider *config.Provider, prompt stri
llmProvider, _ := agent.GetProvider(provider)
nativeToolCalls := llmProvider != nil && llmProvider.SupportsNativeToolCalls()
toolsDesc := ""
if !nativeToolCalls {
toolsDesc = getToolsDescription(servers)
}
toolsDesc := getToolsDescription(servers)
fullPrompt := buildFullPrompt(nativeToolCalls, toolsDesc, "", wd, prompt, tree, "")
// Get tool settings
@@ -2099,33 +2083,46 @@ func buildFullPrompt(nativeToolCalls bool, toolsSection, sessionContext, wd, inp
hiveInfo = fmt.Sprintf(`- Hive Instance ID: %s. You can't assign a task to yourself, but you can ask other hive workers to perform tasks for you.
`, hiveInstanceID)
}
osName := runtime.GOOS
base += fmt.Sprintf(`CRITICAL INFORMATION:
- Operating System: %s (%s)
- Current working directory: %s
%sUse this path when the user asks about "this folder", "current directory", or similar.
IMPORTANT: Execute tools FIRST, perform any action asked for by the user, then provide the final answer. Do NOT include any final answer, summary, or "FINAL_ANSWER:" until AFTER you have executed all necessary tools and received their results. If you need to run commands/tests to verify something, run them first before answering.
`, runtime.GOOS, runtime.GOARCH, wd, hiveInfo)
`, osName, runtime.GOARCH, wd, hiveInfo)
if !nativeToolCalls {
base = fmt.Sprintf(`You have access to these tools:
%s
if strings.TrimSpace(toolsSection) != "" {
base += fmt.Sprintf("\nYou have access to these tools:\n%s\n", toolsSection)
}
%s
shellName := "sh"
shellLimits := `- Shell variables like $HOME, $PATH are NOT expanded - use absolute paths instead
- Interactive commands (vim, less, top) will NOT work - use non-interactive alternatives
- Shell aliases are NOT expanded - use full command names`
if osName == "windows" {
shellName = "cmd.exe"
shellLimits = `- Use cmd.exe syntax (e.g., %USERPROFILE% not $HOME, %PATH% not $PATH)
- PowerShell-specific commands may not work; prefer cmd.exe built-ins
- Interactive commands will NOT work - use non-interactive alternatives`
}
base += fmt.Sprintf(`
IMPORTANT: When you need to read files, search, or get directory contents, you MUST call the appropriate tool with the CORRECT path.
Do NOT use example paths like "/path/to/directory" - use the actual path: %s
BASH TOOL LIMITATIONS:
- Shell variables like $HOME, $PATH are NOT expanded - use absolute paths instead
- Interactive commands (vim, less, top) will NOT work - use non-interactive alternatives
- Shell aliases are NOT expanded - use full command names
- Commands run via `+"`"+`%s`+"`"+`
- %s
- NEVER run servers or long-running programs in foreground - they will hang. ALWAYS use background: true
- ALWAYS use background: true for any server, daemon, or program that doesn't exit immediately
- ALWAYS use background: true when starting a webserver or a long-running background task
- After starting a background process, use sleep before making requests to it
- Use kill_command with the PID to stop background processes when done
`, wd, shellName, shellLimits)
if !nativeToolCalls {
base += `
To call tools, respond with one or more JSON objects, each in its own markdown code block.
You can call multiple tools at once to be more efficient. If tools are independent of each other, call them in parallel for faster execution.
When planning parallel tool calls, ensure there is no conflict - tools that modify the same resource (file, directory, process, etc.) should NOT be called in parallel; run them sequentially instead.
@@ -2140,8 +2137,7 @@ Example:
}
`+"```"+`
IMPORTANT: Execute tools FIRST, perform any action asked for by the user, then provide the final answer. Do NOT include any final answer, summary, or "FINAL_ANSWER:" until AFTER you have executed all necessary tools and received their results. If you need to run commands/tests to verify something, run them first before answering.
`, toolsSection, base, wd)
IMPORTANT: Execute tools FIRST, perform any action asked for by the user, then provide the final answer. Do NOT include any final answer, summary, or "FINAL_ANSWER:" until AFTER you have executed all necessary tools and received their results. If you need to run commands/tests to verify something, run them first before answering.`
}
if strings.TrimSpace(tree) != "" {
@@ -2154,10 +2150,7 @@ User request: %s`, strings.TrimSpace(base), input)
func runToolLoop(ctx context.Context, provider *config.Provider, servers []MCPServer, fullPrompt, input string, maxToolRounds int, toolTimeout int, llmProvider providers.Provider, verbose, debug, autoDenyRestrictedPaths bool, nocont bool, onToolCall func(string)) (string, error) {
prevRoundToolCalls := make(map[string]bool)
toolsDesc := ""
if llmProvider == nil || !llmProvider.SupportsNativeToolCalls() {
toolsDesc = getToolsDescription(servers)
}
toolsDesc := getToolsDescription(servers)
prevNoTool := false
for round := 0; round < maxToolRounds; round++ {
@@ -2376,10 +2369,10 @@ func runToolLoop(ctx context.Context, provider *config.Provider, servers []MCPSe
}
func buildContinuePrompt(llmProvider providers.Provider, servers []MCPServer, originalInput, continuePrompt string, currentRound, maxRounds int) string {
if llmProvider != nil && llmProvider.SupportsNativeToolCalls() {
return fmt.Sprintf("Continue from where you left off. %s\n\nProvide your FINAL_ANSWER now.", continuePrompt)
}
toolsDesc := getToolsDescription(servers)
if llmProvider != nil && llmProvider.SupportsNativeToolCalls() {
return fmt.Sprintf("You have access to these tools:%s\n\nContinue from where you left off. %s\n\nProvide your FINAL_ANSWER now.", toolsDesc, continuePrompt)
}
toolCallFormat := "To call tools, respond with JSON in markdown code blocks:\n```json\n{\n \"name\": \"tool_name\",\n \"arguments\": {\n \"arg1\": \"value1\"\n }\n}\n```"
roundInfo := ""
if currentRound > 0 {
@@ -2403,6 +2396,40 @@ func getToolsDescription(servers []MCPServer) string {
desc.WriteString(fmt.Sprintf("\n[%s]\n", serverName))
for _, tool := range tools {
desc.WriteString(fmt.Sprintf(" - %s: %s\n", tool.Name, tool.Description))
var schema map[string]any
if err := json.Unmarshal(tool.InputSchema, &schema); err != nil {
continue
}
props, _ := schema["properties"].(map[string]any)
required, _ := schema["required"].([]any)
reqSet := make(map[string]bool, len(required))
for _, r := range required {
if s, ok := r.(string); ok {
reqSet[s] = true
}
}
if len(props) > 0 {
// Sort for deterministic output
names := make([]string, 0, len(props))
for name := range props {
names = append(names, name)
}
sort.Strings(names)
for _, name := range names {
p, _ := props[name].(map[string]any)
pType, _ := p["type"].(string)
pDesc, _ := p["description"].(string)
req := "optional"
if reqSet[name] {
req = "required"
}
if pDesc != "" {
desc.WriteString(fmt.Sprintf(" %s (%s, %s): %s\n", name, pType, req, pDesc))
} else {
desc.WriteString(fmt.Sprintf(" %s (%s, %s)\n", name, pType, req))
}
}
}
}
}
return desc.String()
+9 -3
View File
@@ -10,6 +10,7 @@ import (
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"sync"
"syscall"
@@ -271,8 +272,8 @@ func NewBashServer(allowedPaths []string, autoConfirm bool) *BashServer {
jobTracker: NewJobTracker(),
tools: []Tool{
{
Name: "run_command",
Description: "Run a shell command and return its output. For servers/services/long-running processes, you MUST set background: true (killable via /kill or /killbg).",
Name: "run_command",
Description: "Run a shell command and return its output. Uses cmd.exe on Windows, sh on Unix. For servers/services/long-running processes, you MUST set background: true (killable via /kill or /killbg).",
InputSchema: json.RawMessage(`{"type":"object","properties":{"command":{"type":"string","description":"Shell command to run"},"timeout":{"type":"number","description":"Timeout in seconds (default 180)"},"background":{"type":"boolean","description":"Run in background as a goroutine (default false)"}},"required":["command"]}`),
},
{
@@ -594,7 +595,12 @@ func (s *BashServer) runSyncCommand(ctx context.Context, command string, timeout
ctx, cancel := context.WithTimeout(ctx, time.Duration(timeout)*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, "sh", "-c", command)
var cmd *exec.Cmd
if runtime.GOOS == "windows" {
cmd = exec.CommandContext(ctx, "cmd", "/c", command)
} else {
cmd = exec.CommandContext(ctx, "sh", "-c", command)
}
cmd.Stdin = nil // Close stdin to prevent interactive prompts from blocking
var stdout, stderr bytes.Buffer
+2 -2
View File
@@ -663,7 +663,7 @@ func (s *Scheduler) executeWithTools(ctx context.Context, task *ScheduledTask) (
}
func (s *Scheduler) buildInitialPrompt(input, toolsDesc string, toolCallFormat string, nativeToolCalls bool) string {
osInfo := "linux"
osInfo := runtime.GOOS
if s.os != "" {
osInfo = s.os
}
@@ -692,7 +692,7 @@ func (s *Scheduler) buildContinuePrompt(continuePrompt, toolsDesc string, toolCa
if currentRound > 0 {
roundInfo = fmt.Sprintf("\n\nNOTE: You are on round %d/%d. Only call more tools if absolutely necessary.", currentRound, maxRounds)
}
osInfo := "linux"
osInfo := runtime.GOOS
if s.os != "" {
osInfo = s.os
}
+8
View File
@@ -479,6 +479,14 @@ func (m multiSelectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, tea.Quit
}
}
// Fallback: on Windows consoles that are not in raw / virtual-terminal
// input mode, Bubble Tea delivers Space as a printable rune rather than
// a KeySpace event. Accept that as a toggle so the multi-select works
// cross-platform (notably fixing the MCP picker on Windows).
if msg.Type == tea.KeyRunes && len(msg.Runes) == 1 && msg.Runes[0] == ' ' {
m.selected[m.cursor] = !m.selected[m.cursor]
return m, nil
}
}
return m, nil
}