diff --git a/README.md b/README.md index 1a7ef855..de7a77c6 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ export PATH="$PATH:$HOME/go/bin" - **Git**: For version control and worktree management - **Tmux**: For terminal session management - **Go**: For installing -- **Your AI tool of choice**: Such as `claude`, `codex`, etc. +- **Your AI tool of choice**: Such as `claude`, `codex`, `copilot`, etc. ## Configuration @@ -194,6 +194,15 @@ uzi reset uzi prompt --agents=claude:2,aider:2,cursor:1 "Refactor the authentication system" ``` +**Using GitHub Copilot CLI:** + +The `copilot` agent is launched non-interactively with `copilot -s --allow-all-tools --no-ask-user -p ""`, so it runs autonomously without stopping for tool or trust prompts. Set `COPILOT_MODEL` to pick a model (passed through as `--model`). + +```bash +uzi prompt --agents=copilot:3 "Add input validation to the signup form" +COPILOT_MODEL=claude-sonnet-4.5 uzi prompt --agents=copilot:1 "Fix the failing tests" +``` + **Using random agent names:** ```bash diff --git a/cmd/prompt/prompt.go b/cmd/prompt/prompt.go index e364c25f..91ff973f 100644 --- a/cmd/prompt/prompt.go +++ b/cmd/prompt/prompt.go @@ -74,6 +74,24 @@ func parseAgents(agentsStr string) (map[string]AgentConfig, error) { return agentConfigs, nil } +// buildAgentCommand returns the command prefix used to invoke an agent. Most +// agents are launched as ` ""`, but GitHub Copilot needs its +// non-interactive flags so it never blocks on a permission or trust prompt. The +// trailing prompt is still appended by the tmux send-keys template, so the +// returned command ends with -p for copilot. An optional model is read from +// COPILOT_MODEL and passed through with --model. +func buildAgentCommand(commandToUse string) string { + if commandToUse != "copilot" { + return commandToUse + } + + cmd := "copilot -s --allow-all-tools --no-ask-user" + if model := strings.TrimSpace(os.Getenv("COPILOT_MODEL")); model != "" { + cmd += " --model " + model + } + return cmd + " -p" +} + // isPortAvailable checks if a port is available for use func isPortAvailable(port int) bool { address := fmt.Sprintf(":%d", port) @@ -237,7 +255,7 @@ func executePrompt(ctx context.Context, args []string) error { } // Always run send-keys command to the agent pane - tmuxCmd := fmt.Sprintf("tmux send-keys -t %s:agent '%s \"%%s\"' C-m", sessionName, commandToUse) + tmuxCmd := fmt.Sprintf("tmux send-keys -t %s:agent '%s \"%%s\"' C-m", sessionName, buildAgentCommand(commandToUse)) tmuxCmdExec := exec.CommandContext(ctx, "sh", "-c", fmt.Sprintf(tmuxCmd, promptText)) tmuxCmdExec.Dir = worktreePath if err := tmuxCmdExec.Run(); err != nil { @@ -302,7 +320,7 @@ func executePrompt(ctx context.Context, args []string) error { assignedPorts = append(assignedPorts, selectedPort) // Always run send-keys command to the agent pane - tmuxCmd := fmt.Sprintf("tmux send-keys -t %s:agent '%s \"%%s\"' C-m", sessionName, commandToUse) + tmuxCmd := fmt.Sprintf("tmux send-keys -t %s:agent '%s \"%%s\"' C-m", sessionName, buildAgentCommand(commandToUse)) tmuxCmdExec := exec.CommandContext(ctx, "sh", "-c", fmt.Sprintf(tmuxCmd, promptText)) tmuxCmdExec.Dir = worktreePath if err := tmuxCmdExec.Run(); err != nil { diff --git a/cmd/prompt/prompt_test.go b/cmd/prompt/prompt_test.go new file mode 100644 index 00000000..684d0bb5 --- /dev/null +++ b/cmd/prompt/prompt_test.go @@ -0,0 +1,53 @@ +package prompt + +import "testing" + +func TestBuildAgentCommand(t *testing.T) { + t.Setenv("COPILOT_MODEL", "") + + cases := []struct { + name string + command string + want string + }{ + {"claude passthrough", "claude", "claude"}, + {"codex passthrough", "codex", "codex"}, + {"aider passthrough", "aider", "aider"}, + {"copilot non-interactive", "copilot", "copilot -s --allow-all-tools --no-ask-user -p"}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if got := buildAgentCommand(tc.command); got != tc.want { + t.Errorf("buildAgentCommand(%q) = %q, want %q", tc.command, got, tc.want) + } + }) + } +} + +func TestBuildAgentCommandWithModel(t *testing.T) { + t.Setenv("COPILOT_MODEL", "claude-sonnet-4.5") + + want := "copilot -s --allow-all-tools --no-ask-user --model claude-sonnet-4.5 -p" + if got := buildAgentCommand("copilot"); got != want { + t.Errorf("buildAgentCommand(\"copilot\") = %q, want %q", got, want) + } + + if got := buildAgentCommand("claude"); got != "claude" { + t.Errorf("model should not affect non-copilot agents, got %q", got) + } +} + +func TestParseAgents(t *testing.T) { + configs, err := parseAgents("copilot:2,claude:1") + if err != nil { + t.Fatalf("parseAgents returned error: %v", err) + } + + if got := configs["copilot"]; got.Command != "copilot" || got.Count != 2 { + t.Errorf("copilot config = %+v, want {copilot 2}", got) + } + if got := configs["claude"]; got.Command != "claude" || got.Count != 1 { + t.Errorf("claude config = %+v, want {claude 1}", got) + } +} diff --git a/cmd/watch/auto.go b/cmd/watch/auto.go index 0ac40485..585979ee 100644 --- a/cmd/watch/auto.go +++ b/cmd/watch/auto.go @@ -75,8 +75,9 @@ func (aw *AgentWatcher) hasUpdated(sessionName string) (bool, bool, error) { // Check for specific prompts that need auto-enter hasPrompt := false - // Check for Claude trust prompt - if strings.Contains(content, "Do you trust the files in this folder?") { + // Check for Claude trust prompt (GitHub Copilot uses the same wording) + if strings.Contains(content, "Do you trust the files in this folder?") || + strings.Contains(content, "Confirm folder trust") { hasPrompt = true }