From 99a4a3c28f2868216db61b045131abbacf35be66 Mon Sep 17 00:00:00 2001 From: Ukang'a Dickson Date: Thu, 7 May 2026 13:18:24 +0300 Subject: [PATCH 1/5] test(tmux): set git identity in test repo helper setup_test_repo now configures user.email and user.name so the initial empty commit succeeds in CI environments without a global git identity. --- tests/tmux_test.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/tmux_test.rs b/tests/tmux_test.rs index 5657eb7..27e511f 100644 --- a/tests/tmux_test.rs +++ b/tests/tmux_test.rs @@ -16,6 +16,18 @@ fn setup_test_repo() -> (TempDir, PathBuf) { .output() .expect("Failed to init git repo"); + Command::new("git") + .args(["config", "user.email", "test@example.com"]) + .current_dir(&repo_path) + .output() + .expect("Failed to set git user.email"); + + Command::new("git") + .args(["config", "user.name", "Test User"]) + .current_dir(&repo_path) + .output() + .expect("Failed to set git user.name"); + Command::new("git") .args(["commit", "--allow-empty", "-m", "init"]) .current_dir(&repo_path) From ebdc76bb5f237032a7f0f810a8eba434999e4394 Mon Sep 17 00:00:00 2001 From: Ukang'a Dickson Date: Thu, 7 May 2026 13:18:33 +0300 Subject: [PATCH 2/5] feat(config): add one-off agent_cmd override helper SessionConfig::with_agent_cmd_override returns a clone with agent_cmd swapped when an override is provided, leaving the loaded config untouched. This is the foundation for a per-invocation --agent-cmd flag that does not mutate persisted config. Tests cover: - override applied / preserved when absent - original SessionConfig is not mutated - local .wt.toml agent_cmd overrides global ~/.wt/config.toml - full precedence chain: default < global file < local file < override --- src/config.rs | 102 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/src/config.rs b/src/config.rs index 8464cca..4a35abd 100644 --- a/src/config.rs +++ b/src/config.rs @@ -66,6 +66,16 @@ impl SessionConfig { pub fn session_name_for(&self, worktree: &str) -> String { format!("{}{}", self.session_prefix, worktree) } + + /// Return a copy of the session config with a one-off agent command + /// override applied. + pub fn with_agent_cmd_override(&self, agent_cmd_override: Option<&str>) -> Self { + let mut config = self.clone(); + if let Some(agent_cmd) = agent_cmd_override { + config.agent_cmd = agent_cmd.to_string(); + } + config + } } impl Config { @@ -212,6 +222,39 @@ panes = 3 assert_eq!(config.session.editor_cmd, "nvim"); } + #[test] + fn test_with_agent_cmd_override_uses_override_when_present() { + let config = SessionConfig::default(); + + let effective = config.with_agent_cmd_override(Some("aider --fast")); + + assert_eq!(effective.agent_cmd, "aider --fast"); + assert_eq!(effective.editor_cmd, config.editor_cmd); + assert_eq!(effective.session_prefix, config.session_prefix); + } + + #[test] + fn test_with_agent_cmd_override_trims_nothing_and_does_not_mutate_original() { + let mut config = SessionConfig::default(); + config.agent_cmd = "claude --resume".to_string(); + + let effective = config.with_agent_cmd_override(Some("aider --fast --model sonnet")); + + assert_eq!(effective.agent_cmd, "aider --fast --model sonnet"); + assert_eq!(config.agent_cmd, "claude --resume"); + } + + #[test] + fn test_with_agent_cmd_override_preserves_config_when_absent() { + let config = SessionConfig::default(); + + let effective = config.with_agent_cmd_override(None); + + assert_eq!(effective.agent_cmd, config.agent_cmd); + assert_eq!(effective.editor_cmd, config.editor_cmd); + assert_eq!(effective.session_prefix, config.session_prefix); + } + #[test] fn test_default_mode_is_panes() { let config = Config::default(); @@ -392,6 +435,65 @@ panes = 3 assert_eq!(config.session.agent_cmd, "aider"); } + #[test] + fn test_load_layered_local_agent_cmd_overrides_global_agent_cmd() { + use std::io::Write; + + let dir = tempfile::tempdir().unwrap(); + let global = dir.path().join("global.toml"); + let local = dir.path().join("local.toml"); + + writeln!( + std::fs::File::create(&global).unwrap(), + "[session]\nagent_cmd = \"claude --resume\"\npanes = 2\n" + ) + .unwrap(); + writeln!( + std::fs::File::create(&local).unwrap(), + "[session]\nagent_cmd = \"aider --fast\"\n" + ) + .unwrap(); + + let config = Config::load_layered(Some(&global), Some(&local)); + assert_eq!(config.session.agent_cmd, "aider --fast"); + assert_eq!(config.session.panes, 2); + } + + #[test] + fn test_full_precedence_chain_cli_beats_local_beats_global_beats_default() { + use std::io::Write; + + let dir = tempfile::tempdir().unwrap(); + let global = dir.path().join("global.toml"); + let local = dir.path().join("local.toml"); + + writeln!( + std::fs::File::create(&global).unwrap(), + "[session]\nagent_cmd = \"opencode\"\n" + ) + .unwrap(); + writeln!( + std::fs::File::create(&local).unwrap(), + "[session]\nagent_cmd = \"claude --resume\"\n" + ) + .unwrap(); + + let loaded = Config::load_layered(Some(&global), Some(&local)); + assert_eq!(loaded.session.agent_cmd, "claude --resume"); + + let with_override = loaded.session.with_agent_cmd_override(Some("aider --fast")); + assert_eq!(with_override.agent_cmd, "aider --fast"); + + let without_override = loaded.session.with_agent_cmd_override(None); + assert_eq!(without_override.agent_cmd, "claude --resume"); + + let global_only = Config::load_layered(Some(&global), None); + assert_eq!(global_only.session.agent_cmd, "opencode"); + + let no_files = Config::load_layered(None, None); + assert_eq!(no_files.session.agent_cmd, default_agent_cmd()); + } + #[test] fn test_load_layered_returns_default_when_both_missing() { let config = Config::load_layered(None, None); From 41014b170305a142622f23106ba7af66169ff635 Mon Sep 17 00:00:00 2001 From: Ukang'a Dickson Date: Thu, 7 May 2026 13:18:43 +0300 Subject: [PATCH 3/5] feat(session): support --agent-cmd flag on wt session add Adds a one-off CLI override for session.agent_cmd that applies for the current invocation only and does not mutate persisted config. Threaded through both panes mode and windows mode so a new agent pane/window runs the overridden command. Precedence (highest wins): --agent-cmd > .wt.toml > ~/.wt/config.toml > default. When wt session add targets an existing tmux window or session, the override is not retroactively applied; an explanatory note is printed so the user knows the running command is unchanged. Tests cover: - CLI parsing with and without --agent-cmd - effective_session_config applies the override - override does not mutate context.config - no override falls back to the loaded value --- src/main.rs | 58 +++++++++++++++++++++++ src/session_cmd.rs | 114 ++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 166 insertions(+), 6 deletions(-) diff --git a/src/main.rs b/src/main.rs index 31628ea..af8289b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -424,3 +424,61 @@ fn cmd_use(config: &RepoConfig, name: Option) -> Result<()> { spawn_wt_shell(&wt_info.path, &wt_info.task_id, &wt_info.branch)?; Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_cli_session_add_without_agent_cmd_leaves_override_unset() { + let cli = Cli::try_parse_from(["wt", "session", "add", "demo"]) + .expect("session add should parse without --agent-cmd"); + + match cli.command { + Commands::Session { + action: Some(SessionAction::Add { agent_cmd, .. }), + .. + } => { + assert_eq!(agent_cmd, None); + } + _ => panic!("expected session add command"), + } + } + + #[test] + fn test_cli_parses_session_add_agent_cmd() { + let cli = Cli::try_parse_from([ + "wt", + "session", + "--mode", + "windows", + "add", + "demo", + "--agent-cmd", + "aider --fast", + ]) + .expect("session add should accept --agent-cmd"); + + match cli.command { + Commands::Session { + mode, + action: + Some(SessionAction::Add { + name, + base, + panes, + agent_cmd, + watch, + }), + } => { + assert_eq!(mode, Some(SessionMode::Windows)); + assert_eq!(name, "demo"); + assert_eq!(base, "main"); + assert_eq!(panes, None); + assert_eq!(agent_cmd.as_deref(), Some("aider --fast")); + assert!(!watch); + } + _ => panic!("expected session add command"), + } + } +} diff --git a/src/session_cmd.rs b/src/session_cmd.rs index dc2cfad..b314868 100644 --- a/src/session_cmd.rs +++ b/src/session_cmd.rs @@ -28,6 +28,9 @@ pub(crate) enum SessionAction { /// Override pane count (2 or 3) #[arg(long)] panes: Option, + /// Override agent command for this invocation when creating the agent pane/window + #[arg(long, value_name = "CMD")] + agent_cmd: Option, /// Create status window with live agent status #[arg(long)] watch: bool, @@ -71,6 +74,15 @@ impl<'a> SessionCmdContext<'a> { fn effective_panes(&self, panes_override: Option) -> u8 { self.config.effective_panes(panes_override) } + + fn effective_session_config( + &self, + agent_cmd_override: Option<&str>, + ) -> wt::config::SessionConfig { + self.config + .session + .with_agent_cmd_override(agent_cmd_override) + } } pub(crate) fn run_session( @@ -104,10 +116,15 @@ pub(crate) fn run_session( name, base, panes, + agent_cmd, watch, }) => match context.mode { - SessionMode::Panes => cmd_session_add_panes(&context, &name, &base, panes, watch), - SessionMode::Windows => cmd_session_add_windows(&context, &name, &base, panes, watch), + SessionMode::Panes => { + cmd_session_add_panes(&context, &name, &base, panes, agent_cmd.as_deref(), watch) + } + SessionMode::Windows => { + cmd_session_add_windows(&context, &name, &base, panes, agent_cmd.as_deref(), watch) + } }, Some(SessionAction::Rm { name }) => match context.mode { SessionMode::Panes => cmd_session_rm_panes(&context, &name), @@ -242,13 +259,19 @@ fn cmd_session_add_panes( name: &str, base: &str, panes_override: Option, + agent_cmd_override: Option<&str>, watch: bool, ) -> Result<()> { let tmux = panes_tmux(); let worktree_path = ensure_worktree_path(context, name, base)?; let panes = context.effective_panes(panes_override); + let session_config = context.effective_session_config(agent_cmd_override); let inside_session = tmux.is_inside_session(); + if let Some(agent_cmd) = agent_cmd_override { + eprintln!("Using one-off agent command override: {}", agent_cmd); + } + if !tmux.session_exists()? { eprintln!("Creating tmux session: {}", SESSION_NAME); if watch { @@ -257,7 +280,7 @@ fn cmd_session_add_panes( } else { tmux.create_session(name, &worktree_path)?; } - tmux.setup_worktree_layout(name, &worktree_path, panes, &context.config.session)?; + tmux.setup_worktree_layout(name, &worktree_path, panes, &session_config)?; } else { if watch { ensure_status_window(&tmux, &context.repo.root)?; @@ -267,13 +290,18 @@ fn cmd_session_add_panes( if windows.iter().any(|window| window.name == name) { eprintln!("Window '{}' already exists in session.", name); + if agent_cmd_override.is_some() { + eprintln!( + "Note: --agent-cmd only applies when creating a new agent pane; existing windows keep their current command." + ); + } if inside_session { tmux.select_window(name)?; } } else { eprintln!("Adding window: {} ({} panes)", name, panes); tmux.create_window(name, &worktree_path)?; - tmux.setup_worktree_layout(name, &worktree_path, panes, &context.config.session)?; + tmux.setup_worktree_layout(name, &worktree_path, panes, &session_config)?; } } @@ -339,6 +367,7 @@ fn cmd_session_add_windows( name: &str, base: &str, panes_override: Option, + agent_cmd_override: Option<&str>, watch: bool, ) -> Result<()> { if watch { @@ -347,18 +376,28 @@ fn cmd_session_add_windows( let worktree_path = ensure_worktree_path(context, name, base)?; let panes = context.effective_panes(panes_override); - let session_name = context.config.session.session_name_for(name); + let session_config = context.effective_session_config(agent_cmd_override); + let session_name = session_config.session_name_for(name); let tmux = TmuxManager::new(&session_name); + if let Some(agent_cmd) = agent_cmd_override { + eprintln!("Using one-off agent command override: {}", agent_cmd); + } + if tmux.session_exists()? { eprintln!("Using existing session: {}", session_name); + if agent_cmd_override.is_some() { + eprintln!( + "Note: --agent-cmd only applies when creating a new agent window; existing sessions keep their current command." + ); + } } else { eprintln!( "Creating tmux session: {} ({} windows)", session_name, panes ); tmux.create_session("agent", &worktree_path)?; - tmux.setup_worktree_windows(&worktree_path, panes, &context.config.session)?; + tmux.setup_worktree_windows(&worktree_path, panes, &session_config)?; } persist_windows_session(name, &session_name, &worktree_path, panes)?; @@ -671,6 +710,7 @@ fn print_rm_hint(mode: SessionMode, name: &str, probe: &SessionRmProbe) { #[cfg(test)] mod tests { use super::*; + use wt::config::Config; fn probe() -> SessionRmProbe { SessionRmProbe { @@ -742,6 +782,68 @@ mod tests { assert_eq!(windows_rm_hint("demo", &probe()), None); } + #[test] + fn test_effective_session_config_uses_agent_cmd_override() { + let mut config = Config::default(); + config.session.agent_cmd = "claude --resume".to_string(); + config.session.editor_cmd = "hx".to_string(); + + let context = SessionCmdContext { + repo: &crate::RepoConfig { + root: std::path::PathBuf::from("/tmp/repo"), + worktree_dir: std::path::PathBuf::from("/tmp/repo/.worktrees"), + }, + config, + mode: SessionMode::Panes, + }; + + let effective = context.effective_session_config(Some("aider --fast")); + + assert_eq!(effective.agent_cmd, "aider --fast"); + assert_eq!(effective.editor_cmd, "hx"); + } + + #[test] + fn test_effective_session_config_override_is_one_off() { + let mut config = Config::default(); + config.session.agent_cmd = "claude --resume".to_string(); + config.session.editor_cmd = "hx".to_string(); + + let original_agent_cmd = config.session.agent_cmd.clone(); + let context = SessionCmdContext { + repo: &crate::RepoConfig { + root: std::path::PathBuf::from("/tmp/repo"), + worktree_dir: std::path::PathBuf::from("/tmp/repo/.worktrees"), + }, + config, + mode: SessionMode::Windows, + }; + + let effective = context.effective_session_config(Some("aider --fast")); + + assert_eq!(effective.agent_cmd, "aider --fast"); + assert_eq!(context.config.session.agent_cmd, original_agent_cmd); + } + + #[test] + fn test_effective_session_config_uses_loaded_value_when_no_override() { + let mut config = Config::default(); + config.session.agent_cmd = "opencode".to_string(); + + let context = SessionCmdContext { + repo: &crate::RepoConfig { + root: std::path::PathBuf::from("/tmp/repo"), + worktree_dir: std::path::PathBuf::from("/tmp/repo/.worktrees"), + }, + config, + mode: SessionMode::Windows, + }; + + let effective = context.effective_session_config(None); + + assert_eq!(effective.agent_cmd, "opencode"); + } + #[test] fn test_windows_layout_names_match_pane_count() { assert_eq!( From 2d934f8a364adaee0cfb6c9cc387d1d3e22ffbf2 Mon Sep 17 00:00:00 2001 From: Ukang'a Dickson Date: Thu, 7 May 2026 13:18:51 +0300 Subject: [PATCH 4/5] docs(readme): document --agent-cmd flag - Add --agent-cmd to both CLI reference tables. - Add an example invocation in the Session Mode section. - Add a paragraph explaining one-off semantics and that the override only applies when creating a new pane/window. - List --agent-cmd in the precedence rule alongside --mode and --panes. --- README.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f3fbf0c..aff64c8 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,7 @@ wt session [--mode M] ls List workspaces in session wt session [--mode M] add [-b base] base: defaults to main [--panes 2|3] override pane count (panes mode) / window count (windows mode) + [--agent-cmd ] override agent command for this invocation [--watch] add status window with live agent status (panes mode only) wt session [--mode M] rm wt session [--mode M] watch [-i N] @@ -146,6 +147,7 @@ wt session [--mode M] ls List workspaces in session wt session [--mode M] add Add a named session [-b ] Defaults to main [--panes 2|3] Override pane count (panes mode) / window count (windows mode) + [--agent-cmd ] Override agent command for this invocation [--watch] Add status window with live agent status (panes mode only) wt session [--mode M] rm Remove a named session wt session [--mode M] watch [-i N] Watch all the sessions @@ -162,6 +164,7 @@ Manage multiple workspaces in tmux with dedicated agent, terminal, and optional # Add workspaces to session $ wt session add feature/auth $ wt session add feature/payments +$ wt session add feature/review --agent-cmd "aider --fast" # List workspaces with agent status $ wt session ls @@ -249,6 +252,12 @@ does not cause `wt` to pick up unrelated tmux sessions. `wt session watch` and `--watch` are currently panes-mode only. +`--agent-cmd` is a one-off override for `session.agent_cmd` and only affects the +`wt session add` invocation it is passed to. It does not update config, and it +only applies when creating a new panes-mode window or windows-mode session. +Re-running `wt session add` against an existing tmux target keeps that target's +current command. + ### Configuration Create `~/.wt/config.toml` for global settings or `.wt.toml` in repo root for per-repo settings: @@ -262,7 +271,7 @@ agent_cmd = "claude" # command for agent pane/window editor_cmd = "nvim" # command for editor pane/window (when panes=3) ``` -Precedence: `--mode` / `--panes` flags > `.wt.toml` > `~/.wt/config.toml` > defaults +Precedence: `--mode` / `--panes` / `--agent-cmd` flags > `.wt.toml` > `~/.wt/config.toml` > defaults ### Navigation From 890a7c3f720f14a8d4bb046fbd6f88b14907558f Mon Sep 17 00:00:00 2001 From: Ukang'a Dickson Date: Fri, 8 May 2026 07:26:17 +0300 Subject: [PATCH 5/5] refactor(session): dedupe agent_cmd override messaging Extract `note_agent_cmd_override` and `note_agent_cmd_ignored` helpers so the panes-mode and windows-mode add paths share the same emission logic for the "Using one-off agent command override" line and the "--agent-cmd only applies when creating..." note. Addresses review feedback on PR #12 (duplicates 293-297). --- src/session_cmd.rs | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/src/session_cmd.rs b/src/session_cmd.rs index b314868..d765847 100644 --- a/src/session_cmd.rs +++ b/src/session_cmd.rs @@ -254,6 +254,21 @@ fn cmd_session_ls(tmux: &TmuxManager) -> Result<()> { Ok(()) } +fn note_agent_cmd_override(agent_cmd: Option<&str>) { + if let Some(cmd) = agent_cmd { + eprintln!("Using one-off agent command override: {}", cmd); + } +} + +fn note_agent_cmd_ignored(agent_cmd: Option<&str>, creating: &str, existing: &str) { + if agent_cmd.is_some() { + eprintln!( + "Note: --agent-cmd only applies when creating a new {}; existing {} keep their current command.", + creating, existing + ); + } +} + fn cmd_session_add_panes( context: &SessionCmdContext<'_>, name: &str, @@ -268,9 +283,7 @@ fn cmd_session_add_panes( let session_config = context.effective_session_config(agent_cmd_override); let inside_session = tmux.is_inside_session(); - if let Some(agent_cmd) = agent_cmd_override { - eprintln!("Using one-off agent command override: {}", agent_cmd); - } + note_agent_cmd_override(agent_cmd_override); if !tmux.session_exists()? { eprintln!("Creating tmux session: {}", SESSION_NAME); @@ -290,11 +303,7 @@ fn cmd_session_add_panes( if windows.iter().any(|window| window.name == name) { eprintln!("Window '{}' already exists in session.", name); - if agent_cmd_override.is_some() { - eprintln!( - "Note: --agent-cmd only applies when creating a new agent pane; existing windows keep their current command." - ); - } + note_agent_cmd_ignored(agent_cmd_override, "agent pane", "windows"); if inside_session { tmux.select_window(name)?; } @@ -380,17 +389,11 @@ fn cmd_session_add_windows( let session_name = session_config.session_name_for(name); let tmux = TmuxManager::new(&session_name); - if let Some(agent_cmd) = agent_cmd_override { - eprintln!("Using one-off agent command override: {}", agent_cmd); - } + note_agent_cmd_override(agent_cmd_override); if tmux.session_exists()? { eprintln!("Using existing session: {}", session_name); - if agent_cmd_override.is_some() { - eprintln!( - "Note: --agent-cmd only applies when creating a new agent window; existing sessions keep their current command." - ); - } + note_agent_cmd_ignored(agent_cmd_override, "agent window", "sessions"); } else { eprintln!( "Creating tmux session: {} ({} windows)",