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
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ wt session [--mode M] ls List workspaces in session
wt session [--mode M] add <name>
[-b base] base: defaults to main
[--panes 2|3] override pane count (panes mode) / window count (windows mode)
[--agent-cmd <cmd>] override agent command for this invocation
[--watch] add status window with live agent status (panes mode only)
wt session [--mode M] rm <name>
wt session [--mode M] watch [-i N]
Expand All @@ -146,6 +147,7 @@ wt session [--mode M] ls List workspaces in session
wt session [--mode M] add <name> Add a named session
[-b <base>] Defaults to main
[--panes 2|3] Override pane count (panes mode) / window count (windows mode)
[--agent-cmd <cmd>] Override agent command for this invocation
[--watch] Add status window with live agent status (panes mode only)
wt session [--mode M] rm <name> Remove a named session
wt session [--mode M] watch [-i N] Watch all the sessions
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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

Expand Down
102 changes: 102 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand Down
58 changes: 58 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -424,3 +424,61 @@ fn cmd_use(config: &RepoConfig, name: Option<String>) -> 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"),
}
}
}
Loading
Loading