diff --git a/internal/agent/tools/bash.go b/internal/agent/tools/bash.go index bfc95888538430a464052b33c6a55adacbed4a4a..3a591c0cfceb9976076deb654c8a710e8747bde2 100644 --- a/internal/agent/tools/bash.go +++ b/internal/agent/tools/bash.go @@ -19,17 +19,17 @@ import ( ) type BashParams struct { - Command string `json:"command" description:"The command to execute"` - Description string `json:"description,omitempty" description:"A brief description of what the command does"` - WorkingDir string `json:"working_dir,omitempty" description:"The working directory to execute the command in (defaults to current directory)"` - Background bool `json:"background,omitempty" description:"Run the command in a background shell. Returns a shell ID for managing the process."` + Command string `json:"command" description:"The command to execute"` + Description string `json:"description,omitempty" description:"A brief description of what the command does"` + WorkingDir string `json:"working_dir,omitempty" description:"The working directory to execute the command in (defaults to current directory)"` + RunInBackground bool `json:"run_in_background,omitempty" description:"Set to true (boolean) to run this command in the background. Use bash_output to read the output later."` } type BashPermissionsParams struct { - Command string `json:"command"` - Description string `json:"description"` - WorkingDir string `json:"working_dir"` - Background bool `json:"background"` + Command string `json:"command"` + Description string `json:"description"` + WorkingDir string `json:"working_dir"` + RunInBackground bool `json:"run_in_background"` } type BashResponseMetadata struct { @@ -231,11 +231,12 @@ func NewBashTool(permissions permission.Service, workingDir string, attribution } } - // If explicitly requested as background, start immediately - if params.Background { + // If explicitly requested as background, start immediately with detached context + if params.RunInBackground { startTime := time.Now() bgManager := shell.GetBackgroundShellManager() - bgShell, err := bgManager.Start(ctx, execWorkingDir, blockFuncs(), params.Command) + // Use background context so it continues after tool returns + bgShell, err := bgManager.Start(context.Background(), execWorkingDir, blockFuncs(), params.Command) if err != nil { return fantasy.ToolResponse{}, fmt.Errorf("error starting background shell: %w", err) } @@ -255,9 +256,9 @@ func NewBashTool(permissions permission.Service, workingDir string, attribution // Start synchronous execution with auto-background support startTime := time.Now() - // Start background shell immediately but wait for threshold before deciding + // Start with detached context so it can survive if moved to background bgManager := shell.GetBackgroundShellManager() - bgShell, err := bgManager.Start(ctx, execWorkingDir, blockFuncs(), params.Command) + bgShell, err := bgManager.Start(context.Background(), execWorkingDir, blockFuncs(), params.Command) if err != nil { return fantasy.ToolResponse{}, fmt.Errorf("error starting shell: %w", err) } @@ -283,7 +284,8 @@ func NewBashTool(permissions permission.Service, workingDir string, attribution stdout, stderr, done, execErr = bgShell.GetOutput() break waitLoop case <-ctx.Done(): - // Context was cancelled, kill the shell and return error + // Incoming context was cancelled before we moved to background + // Kill the shell and return error bgManager.Kill(bgShell.ID) return fantasy.ToolResponse{}, ctx.Err() } diff --git a/internal/agent/tools/bash.tpl b/internal/agent/tools/bash.tpl index 1f3c1878eb7a866e8ab24b69d0c1de89b7ee5115..9ef6c06eaa7ca79df7384078dda2ed1997f039ca 100644 --- a/internal/agent/tools/bash.tpl +++ b/internal/agent/tools/bash.tpl @@ -24,12 +24,22 @@ Common shell builtins and core utils available on Windows. -- Set background=true to run commands in a separate background shell -- Commands taking longer than 1 minute automatically convert to background +- Set run_in_background=true to run commands in a separate background shell - Returns a shell ID for managing the background process - Use bash_output tool to view current output from background shell - Use bash_kill tool to terminate a background shell -- Useful for long-running processes, servers, or monitoring tasks +- IMPORTANT: NEVER use '&' at the end of commands to run in background - use run_in_background parameter instead +- Commands that should run in background: + * Long-running servers (e.g., 'npm start', 'python -m http.server', 'node server.js') + * Watch/monitoring tasks (e.g., 'npm run watch', 'tail -f logfile') + * Continuous processes that don't exit on their own + * Any command expected to run indefinitely +- Commands that should NOT run in background: + * Build commands (e.g., 'npm run build', 'go build') + * Test suites (e.g., 'npm test', 'pytest') + * Git operations + * File operations + * Short-lived scripts diff --git a/internal/tui/components/chat/messages/renderer.go b/internal/tui/components/chat/messages/renderer.go index 00cd36806e2cd55abaf5bd93b708f080f6f7a27a..f5b800243121794925b8974968441d19dccc824d 100644 --- a/internal/tui/components/chat/messages/renderer.go +++ b/internal/tui/components/chat/messages/renderer.go @@ -217,7 +217,7 @@ func (br bashRenderer) Render(v *toolCallCmp) string { cmd = strings.ReplaceAll(cmd, "\t", " ") args := newParamBuilder(). addMain(cmd). - addFlag("background", params.Background). + addFlag("background", params.RunInBackground). build() return br.renderWithParams(v, "Bash", args, func() string {