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
2 changes: 2 additions & 0 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,7 @@ func (m *home) handleKeyPress(msg tea.KeyMsg) (mod tea.Model, cmd tea.Cmd) {
Title: "",
Path: ".",
Program: m.program,
AutoYes: m.autoYes,
})
if err != nil {
return m, m.handleError(err)
Expand All @@ -647,6 +648,7 @@ func (m *home) handleKeyPress(msg tea.KeyMsg) (mod tea.Model, cmd tea.Cmd) {
Title: "",
Path: ".",
Program: m.program,
AutoYes: m.autoYes,
})
if err != nil {
return m, m.handleError(err)
Expand Down
10 changes: 6 additions & 4 deletions session/instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ func FromInstanceData(data InstanceData) (*Instance, error) {

if instance.Paused() {
instance.started = true
instance.tmuxSession = tmux.NewTmuxSession(instance.Title, instance.Program)
instance.tmuxSession = tmux.NewTmuxSession(instance.Title, tmux.AutoYesProgram(instance.Program, data.AutoYes))
} else {
if err := instance.Start(false); err != nil {
return nil, err
Expand All @@ -153,7 +153,9 @@ type InstanceOptions struct {
Path string
// Program is the program to run in the instance (e.g. "claude", "aider --model ollama_chat/gemma3:1b")
Program string
// If AutoYes is true, then
// AutoYes, when true, runs the instance autonomously: prompts are
// auto-accepted and, for agents that support it, auto-approve flags are
// added to the launch command (see tmux.AutoYesProgram).
AutoYes bool
// Branch is an existing branch name to start the session on (empty = new branch from HEAD)
Branch string
Expand All @@ -177,7 +179,7 @@ func NewInstance(opts InstanceOptions) (*Instance, error) {
Width: 0,
CreatedAt: t,
UpdatedAt: t,
AutoYes: false,
AutoYes: opts.AutoYes,
selectedBranch: opts.Branch,
}, nil
}
Expand Down Expand Up @@ -210,7 +212,7 @@ func (i *Instance) Start(firstTimeSetup bool) error {
tmuxSession = i.tmuxSession
} else {
// Create new tmux session
tmuxSession = tmux.NewTmuxSession(i.Title, i.Program)
tmuxSession = tmux.NewTmuxSession(i.Title, tmux.AutoYesProgram(i.Program, i.AutoYes))
}
i.tmuxSession = tmuxSession

Expand Down
49 changes: 49 additions & 0 deletions session/tmux/tmux.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"io"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"sync"
Expand All @@ -23,6 +24,54 @@ const ProgramClaude = "claude"

const ProgramAider = "aider"
const ProgramGemini = "gemini"
const ProgramCopilot = "copilot"

// GitHub Copilot CLI auto-approve flags. In autonomous (auto-yes) mode these
// stop it from blocking on its trust/permission prompt and from pausing to ask
// the user questions. The broader --allow-all / --yolo flags (allow-all-tools +
// allow-all-paths + allow-all-urls) are treated as already covering tool
// approval when a user has configured them.
const copilotAllowAllToolsFlag = "--allow-all-tools"
const copilotNoAskUserFlag = "--no-ask-user"

// AutoYesProgram returns the program command to launch for an autonomous
// (auto-yes) session. For agents that expose non-interactive auto-approve flags
// it appends them so the agent does not stall on a trust or permission prompt.
// Currently this is needed for GitHub Copilot. For every other program, and
// when autoYes is false, program is returned unchanged. The function is
// idempotent: it never appends a flag that is already present.
func AutoYesProgram(program string, autoYes bool) string {
if !autoYes {
return program
}
fields := strings.Fields(program)
if len(fields) == 0 || filepath.Base(fields[0]) != ProgramCopilot {
return program
}

result := program
// --allow-all and --yolo already imply --allow-all-tools, so only add the
// tool-approval flag when none of them is set.
if !hasArg(fields, copilotAllowAllToolsFlag) &&
!hasArg(fields, "--allow-all") &&
!hasArg(fields, "--yolo") {
result += " " + copilotAllowAllToolsFlag
}
if !hasArg(fields, copilotNoAskUserFlag) {
result += " " + copilotNoAskUserFlag
}
return result
}

// hasArg reports whether args contains the exact flag.
func hasArg(args []string, flag string) bool {
for _, a := range args {
if a == flag {
return true
}
}
return false
}

// TmuxSession represents a managed tmux session
type TmuxSession struct {
Expand Down
50 changes: 50 additions & 0 deletions session/tmux/tmux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,53 @@ func TestStartTmuxSession(t *testing.T) {
_, err = ptyFactory.files[1].Stat()
require.NoError(t, err)
}

func TestAutoYesProgram(t *testing.T) {
cases := []struct {
name string
program string
autoYes bool
want string
}{
{"copilot gets auto-approve flags", "copilot", true, "copilot --allow-all-tools --no-ask-user"},
{"copilot unchanged without auto-yes", "copilot", false, "copilot"},
{"copilot full path", "/opt/homebrew/bin/copilot", true, "/opt/homebrew/bin/copilot --allow-all-tools --no-ask-user"},
{"copilot keeps model passthrough", "copilot --model gpt-5.4", true, "copilot --model gpt-5.4 --allow-all-tools --no-ask-user"},
{"copilot is idempotent", "copilot --allow-all-tools --no-ask-user", true, "copilot --allow-all-tools --no-ask-user"},
{"copilot yolo already implies allow-all-tools", "copilot --yolo", true, "copilot --yolo --no-ask-user"},
{"claude is left untouched", "claude", true, "claude"},
{"empty program", "", true, ""},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
require.Equal(t, tc.want, AutoYesProgram(tc.program, tc.autoYes))
})
}
}

func TestStartTmuxSessionCopilotAutoYes(t *testing.T) {
ptyFactory := NewMockPtyFactory(t)

created := false
cmdExec := cmd_test.MockCmdExec{
RunFunc: func(cmd *exec.Cmd) error {
if strings.Contains(cmd.String(), "has-session") && !created {
created = true
return fmt.Errorf("session already exists")
}
return nil
},
OutputFunc: func(cmd *exec.Cmd) ([]byte, error) {
return []byte("output"), nil
},
}

workdir := t.TempDir()
program := AutoYesProgram("copilot", true)
session := newTmuxSession("test-session", program, ptyFactory, cmdExec)

err := session.Start(workdir)
require.NoError(t, err)
require.Equal(t, fmt.Sprintf("tmux new-session -d -s claudesquad_test-session -c %s copilot --allow-all-tools --no-ask-user", workdir),
cmd2.ToString(ptyFactory.cmds[0]))
}
Loading