Skip to content
Merged
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 internal/session/tooloptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ func NewClaudeOptions(config *UserConfig) *ClaudeOptions {
opts.SkipPermissions = config.Claude.GetDangerousMode()
opts.AutoMode = config.Claude.AutoMode
opts.AllowSkipPermissions = config.Claude.AllowDangerousMode
opts.UseChrome = config.Claude.UseChrome
opts.UseTeammateMode = config.Claude.UseTeammateMode
}
return opts
}
Expand Down
10 changes: 9 additions & 1 deletion internal/session/tooloptions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,9 @@ func TestNewClaudeOptions_WithConfig(t *testing.T) {
dangerousModeBool := true
config := &UserConfig{
Claude: ClaudeSettings{
DangerousMode: &dangerousModeBool,
DangerousMode: &dangerousModeBool,
UseChrome: true,
UseTeammateMode: true,
},
}

Expand All @@ -241,6 +243,12 @@ func TestNewClaudeOptions_WithConfig(t *testing.T) {
if !opts.SkipPermissions {
t.Error("expected SkipPermissions=true when config.DangerousMode=true")
}
if !opts.UseChrome {
t.Error("expected UseChrome=true")
}
if !opts.UseTeammateMode {
t.Error("expected UseTeammateMode=true")
}
}

func TestNewClaudeOptions_AutoMode(t *testing.T) {
Expand Down
16 changes: 16 additions & 0 deletions internal/session/userconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -671,6 +671,17 @@ type ClaudeSettings struct {
// Default: false
AutoMode bool `toml:"auto_mode"`

// ExtraArgs are user-supplied Claude CLI flags used as the New Session
// dialog default. They are persisted as discrete TOML array entries and
// copied to Instance.ExtraArgs when a Claude session is created.
ExtraArgs []string `toml:"extra_args"`

// UseChrome enables --chrome by default for Claude sessions.
UseChrome bool `toml:"use_chrome"`

// UseTeammateMode enables --teammate-mode tmux by default for Claude sessions.
UseTeammateMode bool `toml:"use_teammate_mode"`

// EnvFile is a .env file specific to Claude sessions
// Sourced AFTER global [shell].env_files
// Path can be absolute, ~ for home, $HOME/${VAR} for env vars, or relative to session working directory
Expand Down Expand Up @@ -2350,6 +2361,11 @@ func CreateExampleConfig() error {
# config_dir = "~/.claude-work"
# Enable --dangerously-skip-permissions by default (default: false)
# dangerous_mode = true
# Extra Claude CLI flags remembered from the New Session dialog
# extra_args = ["--agent", "reviewer"]
# Enable Chrome / teammate mode by default
# use_chrome = false
# use_teammate_mode = false

# Gemini CLI integration
# [gemini]
Expand Down
40 changes: 40 additions & 0 deletions internal/session/userconfig_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,46 @@ func TestSaveUserConfig(t *testing.T) {
}
}

func TestClaudeExtraArgsConfigRoundTrip(t *testing.T) {
tempDir := t.TempDir()
originalHome := os.Getenv("HOME")
os.Setenv("HOME", tempDir)
defer os.Setenv("HOME", originalHome)
ClearUserConfigCache()
defer ClearUserConfigCache()

config := &UserConfig{
Claude: ClaudeSettings{
ExtraArgs: []string{"--agent", "reviewer", "--model", "opus"},
UseChrome: true,
UseTeammateMode: true,
},
}
if err := SaveUserConfig(config); err != nil {
t.Fatalf("SaveUserConfig failed: %v", err)
}

loaded, err := LoadUserConfig()
if err != nil {
t.Fatalf("LoadUserConfig failed: %v", err)
}
want := []string{"--agent", "reviewer", "--model", "opus"}
if len(loaded.Claude.ExtraArgs) != len(want) {
t.Fatalf("Claude.ExtraArgs = %v, want %v", loaded.Claude.ExtraArgs, want)
}
for i := range want {
if loaded.Claude.ExtraArgs[i] != want[i] {
t.Fatalf("Claude.ExtraArgs[%d] = %q, want %q", i, loaded.Claude.ExtraArgs[i], want[i])
}
}
if !loaded.Claude.UseChrome {
t.Fatal("Claude.UseChrome = false, want true")
}
if !loaded.Claude.UseTeammateMode {
t.Fatal("Claude.UseTeammateMode = false, want true")
}
}

func TestGetTheme_Default(t *testing.T) {
// Setup: use temp directory with no config
tempDir := t.TempDir()
Expand Down
3 changes: 3 additions & 0 deletions internal/ui/claudeoptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ func (p *ClaudeOptionsPanel) SetDefaults(config *session.UserConfig) {
p.skipPermissions = config.Claude.GetDangerousMode()
p.allowSkipPermissions = config.Claude.AllowDangerousMode
p.autoMode = config.Claude.AutoMode
p.SetExtraArgs(config.Claude.ExtraArgs)
p.useChrome = config.Claude.UseChrome
p.useTeammateMode = config.Claude.UseTeammateMode
}
}

Expand Down
25 changes: 25 additions & 0 deletions internal/ui/home.go
Original file line number Diff line number Diff line change
Expand Up @@ -5192,6 +5192,7 @@ func (h *Home) handleNewDialogKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
if command == "claude" && claudeOpts != nil {
toolOptionsJSON, _ = session.MarshalToolOptions(claudeOpts)
claudeExtraArgs = h.newDialog.GetClaudeExtraArgs()
persistClaudeDialogDefaults(claudeOpts, claudeExtraArgs)
claudeStartQuery = h.newDialog.GetClaudeStartQuery()
} else if command == "codex" {
yolo := h.newDialog.GetCodexYoloMode()
Expand Down Expand Up @@ -5277,6 +5278,30 @@ func (h *Home) handleNewDialogKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
return h, cmd
}

func persistClaudeDialogDefaults(opts *session.ClaudeOptions, args []string) {
cfg, err := session.LoadUserConfig()
if err != nil || cfg == nil || opts == nil {
return
}
cleaned := make([]string, 0, len(args))
for _, arg := range args {
if tok := strings.TrimSpace(arg); tok != "" {
cleaned = append(cleaned, tok)
}
}
if len(cleaned) == 0 {
cfg.Claude.ExtraArgs = nil
} else {
cfg.Claude.ExtraArgs = cleaned
}
cfg.Claude.DangerousMode = &opts.SkipPermissions
cfg.Claude.AllowDangerousMode = opts.AllowSkipPermissions
cfg.Claude.AutoMode = opts.AutoMode
cfg.Claude.UseChrome = opts.UseChrome
cfg.Claude.UseTeammateMode = opts.UseTeammateMode
_ = session.SaveUserConfig(cfg)
}

func (h *Home) beginNotesEditing(inst *session.Instance) {
if inst == nil {
return
Expand Down
56 changes: 56 additions & 0 deletions internal/ui/home_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,62 @@ func TestApplyCreateSessionToolOverrides_NonGeminiNoop(t *testing.T) {
}
}

func TestPersistClaudeDialogDefaults(t *testing.T) {
origHome := os.Getenv("HOME")
tmpHome := t.TempDir()
os.Setenv("HOME", tmpHome)
session.ClearUserConfigCache()
defer func() {
os.Setenv("HOME", origHome)
session.ClearUserConfigCache()
}()

persistClaudeDialogDefaults(&session.ClaudeOptions{
SkipPermissions: false,
AllowSkipPermissions: true,
AutoMode: true,
UseChrome: true,
UseTeammateMode: true,
}, []string{"--agent", "reviewer", "", " --model "})
cfg, err := session.LoadUserConfig()
if err != nil {
t.Fatalf("LoadUserConfig: %v", err)
}
want := []string{"--agent", "reviewer", "--model"}
if len(cfg.Claude.ExtraArgs) != len(want) {
t.Fatalf("Claude.ExtraArgs = %v, want %v", cfg.Claude.ExtraArgs, want)
}
for i := range want {
if cfg.Claude.ExtraArgs[i] != want[i] {
t.Fatalf("Claude.ExtraArgs[%d] = %q, want %q", i, cfg.Claude.ExtraArgs[i], want[i])
}
}
if cfg.Claude.DangerousMode == nil || *cfg.Claude.DangerousMode {
t.Fatalf("Claude.DangerousMode = %v, want explicit false", cfg.Claude.DangerousMode)
}
if !cfg.Claude.AllowDangerousMode {
t.Fatal("Claude.AllowDangerousMode = false, want true")
}
if !cfg.Claude.AutoMode {
t.Fatal("Claude.AutoMode = false, want true")
}
if !cfg.Claude.UseChrome {
t.Fatal("Claude.UseChrome = false, want true")
}
if !cfg.Claude.UseTeammateMode {
t.Fatal("Claude.UseTeammateMode = false, want true")
}

persistClaudeDialogDefaults(&session.ClaudeOptions{}, nil)
cfg, err = session.LoadUserConfig()
if err != nil {
t.Fatalf("LoadUserConfig after clear: %v", err)
}
if cfg.Claude.ExtraArgs != nil {
t.Fatalf("Claude.ExtraArgs should clear to nil, got %v", cfg.Claude.ExtraArgs)
}
}

// Co-credit @masta-g3 (PR #674): TUI session creation must produce
// Tool="pi" rather than Tool="shell" with Command="pi", matching the
// tmux/userconfig wiring already present.
Expand Down
46 changes: 46 additions & 0 deletions internal/ui/newdialog_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1577,6 +1577,52 @@ func TestNewDialog_GetClaudeStartQuery_ReturnsInputValue(t *testing.T) {
}
}

func TestNewDialog_ShowInGroup_LoadsConfiguredClaudeExtraArgs(t *testing.T) {
origHome := os.Getenv("HOME")
tmpHome := t.TempDir()
os.Setenv("HOME", tmpHome)
session.ClearUserConfigCache()
defer func() {
os.Setenv("HOME", origHome)
session.ClearUserConfigCache()
}()

if err := session.SaveUserConfig(&session.UserConfig{
Claude: session.ClaudeSettings{
ExtraArgs: []string{"--agent", "reviewer", "--model", "opus"},
UseChrome: true,
UseTeammateMode: true,
},
}); err != nil {
t.Fatalf("SaveUserConfig: %v", err)
}

dialog := NewNewDialog()
dialog.SetDefaultTool("claude")
dialog.ShowInGroup("default", "default", "", nil, "")

got := dialog.GetClaudeExtraArgs()
want := []string{"--agent", "reviewer", "--model", "opus"}
if len(got) != len(want) {
t.Fatalf("GetClaudeExtraArgs() = %v, want %v", got, want)
}
for i := range want {
if got[i] != want[i] {
t.Fatalf("GetClaudeExtraArgs()[%d] = %q, want %q", i, got[i], want[i])
}
}
opts := dialog.GetClaudeOptions()
if opts == nil {
t.Fatal("GetClaudeOptions() = nil")
}
if !opts.UseChrome {
t.Fatal("GetClaudeOptions().UseChrome = false, want true")
}
if !opts.UseTeammateMode {
t.Fatal("GetClaudeOptions().UseTeammateMode = false, want true")
}
}

// TestNewDialog_StartQuery_ClearsBetweenOpenings is the RED regression for
// #741 (@Clindbergh). Filed against v1.7.67 after #725 shipped the dedicated
// "Start query" field: opening the new-session dialog a second time showed
Expand Down
3 changes: 3 additions & 0 deletions skills/agent-deck/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,9 @@ See `agent-deck watcher --help` for the full command surface and per-adapter exa
[claude]
config_dir = "~/.claude-work" # Custom Claude profile
dangerous_mode = true # --dangerously-skip-permissions
use_chrome = false # --chrome
use_teammate_mode = false # --teammate-mode tmux
extra_args = ["--agent", "reviewer"]

[logs]
max_size_mb = 10 # Max before truncation
Expand Down
6 changes: 6 additions & 0 deletions skills/agent-deck/references/config-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ config_dir = "~/.claude" # Path to Claude config directory
dangerous_mode = true # Enable --dangerously-skip-permissions
auto_mode = false # Enable --permission-mode auto (classifier-based)
allow_dangerous_mode = false # Enable --allow-dangerously-skip-permissions
use_chrome = false # Enable --chrome
use_teammate_mode = false # Enable --teammate-mode tmux
extra_args = ["--agent", "reviewer"] # Extra Claude CLI flags
env_file = "~/.claude.env" # .env file specific to Claude sessions

[profiles.work.claude]
Expand All @@ -74,6 +77,9 @@ config_dir = "~/.claude-work" # Optional override for profile "work"
| `dangerous_mode` | bool | `false` | Adds `--dangerously-skip-permissions`. Forces bypass on. Takes precedence over `auto_mode` and `allow_dangerous_mode`. |
| `auto_mode` | bool | `false` | Adds `--permission-mode auto`. A classifier model auto-approves safe operations while blocking risky ones. Ignored when `dangerous_mode` is true. |
| `allow_dangerous_mode` | bool | `false` | Adds `--allow-dangerously-skip-permissions`. Unlocks bypass as an option without activating it. Ignored when `dangerous_mode` or `auto_mode` is true. |
| `use_chrome` | bool | `false` | Adds `--chrome` to Claude sessions and is remembered from the New Session dialog. |
| `use_teammate_mode` | bool | `false` | Adds `--teammate-mode tmux` to Claude sessions and is remembered from the New Session dialog. |
| `extra_args` | array of strings | `[]` | Extra Claude CLI flags remembered from the New Session dialog and appended to new/restarted Claude sessions. Do not store secrets here. |
| `env_file` | string | `""` | A .env file sourced for Claude sessions only. Sourced after global `[shell].env_files`. See [Path Resolution](#path-resolution). |

Config resolution order for Claude config dir:
Expand Down
3 changes: 3 additions & 0 deletions skills/agent-deck/references/tui-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,12 @@ Complete reference for agent-deck Terminal UI features.
- Project path (required, supports `~/`)
- Command (claude/gemini/opencode/codex/custom)
- Parent group (auto-selected)
- Claude options (when Claude is selected): permission mode, Chrome, teammate mode, extra args, and start query

**Controls:** `Tab` move fields | `Enter` create | `Esc` cancel

Claude New Session defaults are remembered in `~/.agent-deck/config.toml` under `[claude]`, except start query and resume IDs, which are per-launch values.

### MCP Manager (`m`)

**Layout:**
Expand Down