Story 33: Copy-paste diff commands for agent worktrees
- Add base_branch detection to WorktreeInfo (from project root HEAD)
- Expose base_branch in AgentInfo API response
- Add {{base_branch}} template variable to agent config rendering
- Show git difftool command with copy-to-clipboard in AgentPanel UI
- Add diff command instruction to coder agent prompts
- Add AgentPanel tests for diff command rendering and clipboard
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -76,6 +76,7 @@ pub struct AgentInfo {
|
||||
pub status: AgentStatus,
|
||||
pub session_id: Option<String>,
|
||||
pub worktree_path: Option<String>,
|
||||
pub base_branch: Option<String>,
|
||||
}
|
||||
|
||||
struct StoryAgent {
|
||||
@@ -182,7 +183,7 @@ impl AgentPool {
|
||||
// Spawn the agent process
|
||||
let wt_path_str = wt_info.path.to_string_lossy().to_string();
|
||||
let (command, args, prompt) =
|
||||
config.render_agent_args(&wt_path_str, story_id, Some(&resolved_name))?;
|
||||
config.render_agent_args(&wt_path_str, story_id, Some(&resolved_name), Some(&wt_info.base_branch))?;
|
||||
|
||||
let sid = story_id.to_string();
|
||||
let aname = resolved_name.clone();
|
||||
@@ -247,6 +248,7 @@ impl AgentPool {
|
||||
status: AgentStatus::Running,
|
||||
session_id: None,
|
||||
worktree_path: Some(wt_path_str),
|
||||
base_branch: Some(wt_info.base_branch.clone()),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -321,6 +323,10 @@ impl AgentPool {
|
||||
.worktree_info
|
||||
.as_ref()
|
||||
.map(|wt| wt.path.to_string_lossy().to_string()),
|
||||
base_branch: agent
|
||||
.worktree_info
|
||||
.as_ref()
|
||||
.map(|wt| wt.base_branch.clone()),
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
|
||||
@@ -175,6 +175,7 @@ impl ProjectConfig {
|
||||
worktree_path: &str,
|
||||
story_id: &str,
|
||||
agent_name: Option<&str>,
|
||||
base_branch: Option<&str>,
|
||||
) -> Result<(String, Vec<String>, String), String> {
|
||||
let agent = match agent_name {
|
||||
Some(name) => self
|
||||
@@ -185,9 +186,11 @@ impl ProjectConfig {
|
||||
.ok_or_else(|| "No agents configured".to_string())?,
|
||||
};
|
||||
|
||||
let bb = base_branch.unwrap_or("master");
|
||||
let render = |s: &str| {
|
||||
s.replace("{{worktree_path}}", worktree_path)
|
||||
.replace("{{story_id}}", story_id)
|
||||
.replace("{{base_branch}}", bb)
|
||||
};
|
||||
|
||||
let command = render(&agent.command);
|
||||
@@ -378,7 +381,7 @@ max_turns = 0
|
||||
fn render_agent_args_default() {
|
||||
let config = ProjectConfig::default();
|
||||
let (cmd, args, prompt) = config
|
||||
.render_agent_args("/tmp/wt", "42_foo", None)
|
||||
.render_agent_args("/tmp/wt", "42_foo", None, None)
|
||||
.unwrap();
|
||||
assert_eq!(cmd, "claude");
|
||||
assert!(args.is_empty());
|
||||
@@ -404,7 +407,7 @@ max_turns = 30
|
||||
|
||||
let config = ProjectConfig::parse(toml_str).unwrap();
|
||||
let (cmd, args, prompt) = config
|
||||
.render_agent_args("/tmp/wt", "42_foo", Some("supervisor"))
|
||||
.render_agent_args("/tmp/wt", "42_foo", Some("supervisor"), Some("master"))
|
||||
.unwrap();
|
||||
assert_eq!(cmd, "claude");
|
||||
assert!(args.contains(&"--model".to_string()));
|
||||
@@ -422,7 +425,7 @@ max_turns = 30
|
||||
|
||||
// Render for coder
|
||||
let (_, coder_args, _) = config
|
||||
.render_agent_args("/tmp/wt", "42_foo", Some("coder"))
|
||||
.render_agent_args("/tmp/wt", "42_foo", Some("coder"), Some("master"))
|
||||
.unwrap();
|
||||
assert!(coder_args.contains(&"sonnet".to_string()));
|
||||
assert!(coder_args.contains(&"30".to_string()));
|
||||
@@ -433,7 +436,7 @@ max_turns = 30
|
||||
#[test]
|
||||
fn render_agent_args_not_found() {
|
||||
let config = ProjectConfig::default();
|
||||
let result = config.render_agent_args("/tmp/wt", "42_foo", Some("nonexistent"));
|
||||
let result = config.render_agent_args("/tmp/wt", "42_foo", Some("nonexistent"), None);
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("No agent named 'nonexistent'"));
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use std::process::Command;
|
||||
pub struct WorktreeInfo {
|
||||
pub path: PathBuf,
|
||||
pub branch: String,
|
||||
pub base_branch: String,
|
||||
}
|
||||
|
||||
/// Worktree path as a sibling of the project root: `{project_root}-story-{id}`.
|
||||
@@ -24,6 +25,23 @@ fn branch_name(story_id: &str) -> String {
|
||||
format!("feature/story-{story_id}")
|
||||
}
|
||||
|
||||
/// Detect the current branch of the project root (the base branch worktrees fork from).
|
||||
fn detect_base_branch(project_root: &Path) -> String {
|
||||
Command::new("git")
|
||||
.args(["rev-parse", "--abbrev-ref", "HEAD"])
|
||||
.current_dir(project_root)
|
||||
.output()
|
||||
.ok()
|
||||
.and_then(|o| {
|
||||
if o.status.success() {
|
||||
Some(String::from_utf8_lossy(&o.stdout).trim().to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| "master".to_string())
|
||||
}
|
||||
|
||||
/// Create a git worktree for the given story.
|
||||
///
|
||||
/// - Creates the worktree at `{project_root}-story-{story_id}` (sibling directory)
|
||||
@@ -37,6 +55,7 @@ pub async fn create_worktree(
|
||||
) -> Result<WorktreeInfo, String> {
|
||||
let wt_path = worktree_path(project_root, story_id);
|
||||
let branch = branch_name(story_id);
|
||||
let base_branch = detect_base_branch(project_root);
|
||||
let root = project_root.to_path_buf();
|
||||
|
||||
// Already exists — reuse
|
||||
@@ -45,6 +64,7 @@ pub async fn create_worktree(
|
||||
return Ok(WorktreeInfo {
|
||||
path: wt_path,
|
||||
branch,
|
||||
base_branch,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -60,6 +80,7 @@ pub async fn create_worktree(
|
||||
Ok(WorktreeInfo {
|
||||
path: wt_path,
|
||||
branch,
|
||||
base_branch,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user