Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 "<prompt>"`, 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
Expand Down
22 changes: 20 additions & 2 deletions cmd/prompt/prompt.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<agent> "<prompt>"`, 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)
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
53 changes: 53 additions & 0 deletions cmd/prompt/prompt_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
5 changes: 3 additions & 2 deletions cmd/watch/auto.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down