mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
fix(tui/branch): use BackgroundEvent for all /branch status/error messages (not streaming Answer); retry with unique branch name when branch already exists; propagate effective branch back to callers
This commit is contained in:
@@ -462,7 +462,7 @@ async fn execute_agent(agent_id: String, config: Option<AgentConfig>) {
|
||||
drop(manager);
|
||||
|
||||
match setup_worktree(&git_root, &branch_id).await {
|
||||
Ok(worktree_path) => {
|
||||
Ok((worktree_path, used_branch)) => {
|
||||
let mut manager = AGENT_MANAGER.write().await;
|
||||
manager
|
||||
.add_progress(
|
||||
@@ -474,7 +474,7 @@ async fn execute_agent(agent_id: String, config: Option<AgentConfig>) {
|
||||
.update_worktree_info(
|
||||
&agent_id,
|
||||
worktree_path.display().to_string(),
|
||||
branch_id.clone(),
|
||||
used_branch.clone(),
|
||||
)
|
||||
.await;
|
||||
drop(manager);
|
||||
|
||||
@@ -68,13 +68,14 @@ pub async fn get_git_root_from(cwd: &Path) -> Result<PathBuf, String> {
|
||||
|
||||
/// Create a new worktree for `branch_id` under `<git_root>/.code/branches/<branch_id>`.
|
||||
/// If a previous worktree directory exists, remove it first.
|
||||
pub async fn setup_worktree(git_root: &Path, branch_id: &str) -> Result<PathBuf, String> {
|
||||
pub async fn setup_worktree(git_root: &Path, branch_id: &str) -> Result<(PathBuf, String), String> {
|
||||
let code_dir = git_root.join(".code").join("branches");
|
||||
tokio::fs::create_dir_all(&code_dir)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to create .code/branches directory: {}", e))?;
|
||||
|
||||
let worktree_path = code_dir.join(branch_id);
|
||||
let mut effective_branch = branch_id.to_string();
|
||||
let mut worktree_path = code_dir.join(&effective_branch);
|
||||
if worktree_path.exists() {
|
||||
let _ = Command::new("git")
|
||||
.arg("worktree")
|
||||
@@ -88,17 +89,44 @@ pub async fn setup_worktree(git_root: &Path, branch_id: &str) -> Result<PathBuf,
|
||||
|
||||
let output = Command::new("git")
|
||||
.current_dir(git_root)
|
||||
.args(["worktree", "add", "-b", branch_id, worktree_path.to_str().unwrap()])
|
||||
.args(["worktree", "add", "-b", &effective_branch, worktree_path.to_str().unwrap()])
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to create git worktree: {}", e))?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(format!("Failed to create worktree: {}", stderr));
|
||||
// If the branch already exists, generate a unique name and retry once.
|
||||
if stderr.contains("already exists") {
|
||||
effective_branch = format!("{}-{}", effective_branch, Utc::now().format("%Y%m%d-%H%M%S"));
|
||||
worktree_path = code_dir.join(&effective_branch);
|
||||
// Ensure target path is clean
|
||||
if worktree_path.exists() {
|
||||
let _ = Command::new("git")
|
||||
.arg("worktree")
|
||||
.arg("remove")
|
||||
.arg(worktree_path.to_str().unwrap())
|
||||
.arg("--force")
|
||||
.current_dir(git_root)
|
||||
.output()
|
||||
.await;
|
||||
}
|
||||
let retry = Command::new("git")
|
||||
.current_dir(git_root)
|
||||
.args(["worktree", "add", "-b", &effective_branch, worktree_path.to_str().unwrap()])
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to create git worktree (retry): {}", e))?;
|
||||
if !retry.status.success() {
|
||||
let retry_err = String::from_utf8_lossy(&retry.stderr);
|
||||
return Err(format!("Failed to create worktree: {}", retry_err));
|
||||
}
|
||||
} else {
|
||||
return Err(format!("Failed to create worktree: {}", stderr));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(worktree_path)
|
||||
Ok((worktree_path, effective_branch))
|
||||
}
|
||||
|
||||
/// Copy uncommitted (modified + untracked) files from `src_root` into the `worktree_path`.
|
||||
@@ -160,4 +188,3 @@ pub async fn detect_default_branch(cwd: &Path) -> Option<String> {
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
|
||||
@@ -6081,7 +6081,8 @@ impl ChatWidget<'_> {
|
||||
let git_root = match codex_core::git_worktree::get_git_root_from(&cwd).await {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
tx.send(AppEvent::InsertHistory(vec![ratatui::text::Line::from(format!("`/branch` — not a git repo: {}", e))]));
|
||||
use codex_core::protocol::{BackgroundEventEvent, Event, EventMsg};
|
||||
let _ = tx.send(AppEvent::CodexEvent(Event { id: uuid::Uuid::new_v4().to_string(), event_seq: 0, msg: EventMsg::BackgroundEvent(BackgroundEventEvent { message: format!("`/branch` — not a git repo: {}", e) }), order: None }));
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -6089,10 +6090,11 @@ impl ChatWidget<'_> {
|
||||
let task_opt = if args.trim().is_empty() { None } else { Some(args.trim()) };
|
||||
let branch_name = codex_core::git_worktree::generate_branch_name_from_task(task_opt);
|
||||
// Create worktree
|
||||
let worktree = match codex_core::git_worktree::setup_worktree(&git_root, &branch_name).await {
|
||||
Ok(p) => p,
|
||||
let (worktree, used_branch) = match codex_core::git_worktree::setup_worktree(&git_root, &branch_name).await {
|
||||
Ok((p, b)) => (p, b),
|
||||
Err(e) => {
|
||||
tx.send(AppEvent::InsertHistory(vec![ratatui::text::Line::from(format!("`/branch` — failed to create worktree: {}", e))]));
|
||||
use codex_core::protocol::{BackgroundEventEvent, Event, EventMsg};
|
||||
let _ = tx.send(AppEvent::CodexEvent(Event { id: uuid::Uuid::new_v4().to_string(), event_seq: 0, msg: EventMsg::BackgroundEvent(BackgroundEventEvent { message: format!("`/branch` — failed to create worktree: {}", e) }), order: None }));
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -6100,28 +6102,38 @@ impl ChatWidget<'_> {
|
||||
let copied = match codex_core::git_worktree::copy_uncommitted_to_worktree(&git_root, &worktree).await {
|
||||
Ok(n) => n,
|
||||
Err(e) => {
|
||||
tx.send(AppEvent::InsertHistory(vec![ratatui::text::Line::from(format!("`/branch` — failed to copy changes: {}", e))]));
|
||||
use codex_core::protocol::{BackgroundEventEvent, Event, EventMsg};
|
||||
let _ = tx.send(AppEvent::CodexEvent(Event { id: uuid::Uuid::new_v4().to_string(), event_seq: 0, msg: EventMsg::BackgroundEvent(BackgroundEventEvent { message: format!("`/branch` — failed to copy changes: {}", e) }), order: None }));
|
||||
// Still switch to the branch even if copy fails
|
||||
0
|
||||
}
|
||||
};
|
||||
|
||||
// Build clean multi-line output for improved readability
|
||||
let mut lines: Vec<ratatui::text::Line<'static>> = Vec::new();
|
||||
lines.push(ratatui::text::Line::from(format!("• Created worktree '{}'", branch_name)));
|
||||
lines.push(ratatui::text::Line::from(format!(" Path: {}", worktree.display())));
|
||||
lines.push(ratatui::text::Line::from(format!(" Copied {} changed files", copied)));
|
||||
if let Some(task_text) = task_opt {
|
||||
lines.push(ratatui::text::Line::from(format!(" Task: {}", task_text)));
|
||||
lines.push(ratatui::text::Line::from(" Switching and starting task..."));
|
||||
// Build clean multi-line output as a BackgroundEvent (not streaming Answer)
|
||||
let msg = if let Some(task_text) = task_opt {
|
||||
format!(
|
||||
"• Created worktree '{used}'\n Path: {path}\n Copied {copied} changed files\n Task: {task}\n Switching and starting task...",
|
||||
used = used_branch,
|
||||
path = worktree.display(),
|
||||
copied = copied,
|
||||
task = task_text
|
||||
)
|
||||
} else {
|
||||
lines.push(ratatui::text::Line::from(" Switched to branch. Type your task when ready."));
|
||||
format!(
|
||||
"• Created worktree '{used}'\n Path: {path}\n Copied {copied} changed files\n Switched to branch. Type your task when ready.",
|
||||
used = used_branch,
|
||||
path = worktree.display(),
|
||||
copied = copied
|
||||
)
|
||||
};
|
||||
{
|
||||
use codex_core::protocol::{BackgroundEventEvent, Event, EventMsg};
|
||||
let _ = tx.send(AppEvent::CodexEvent(Event { id: uuid::Uuid::new_v4().to_string(), event_seq: 0, msg: EventMsg::BackgroundEvent(BackgroundEventEvent { message: msg }), order: None }));
|
||||
}
|
||||
tx.send(AppEvent::InsertHistory(lines));
|
||||
|
||||
// Switch cwd and optionally submit the task
|
||||
let initial_prompt = task_opt.map(|s| s.to_string());
|
||||
tx.send(AppEvent::SwitchCwd(worktree, initial_prompt));
|
||||
let _ = tx.send(AppEvent::SwitchCwd(worktree, initial_prompt));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6140,7 +6152,8 @@ impl ChatWidget<'_> {
|
||||
let git_root = match codex_core::git_worktree::get_git_root_from(&work_cwd).await {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
tx.send(AppEvent::InsertHistory(vec![ratatui::text::Line::from(format!("`/branch finalize` — not a git repo: {}", e))]));
|
||||
use codex_core::protocol::{BackgroundEventEvent, Event, EventMsg};
|
||||
let _ = tx.send(AppEvent::CodexEvent(Event { id: uuid::Uuid::new_v4().to_string(), event_seq: 0, msg: EventMsg::BackgroundEvent(BackgroundEventEvent { message: format!("`/branch finalize` — not a git repo: {}", e) }), order: None }));
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -6149,7 +6162,8 @@ impl ChatWidget<'_> {
|
||||
let branch_name = match head {
|
||||
Ok(out) if out.status.success() => String::from_utf8_lossy(&out.stdout).trim().to_string(),
|
||||
_ => {
|
||||
tx.send(AppEvent::InsertHistory(vec![ratatui::text::Line::from("`/branch finalize` — failed to detect branch name") ]));
|
||||
use codex_core::protocol::{BackgroundEventEvent, Event, EventMsg};
|
||||
let _ = tx.send(AppEvent::CodexEvent(Event { id: uuid::Uuid::new_v4().to_string(), event_seq: 0, msg: EventMsg::BackgroundEvent(BackgroundEventEvent { message: "`/branch finalize` — failed to detect branch name".to_string() }), order: None }));
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -6162,20 +6176,22 @@ impl ChatWidget<'_> {
|
||||
.output()
|
||||
.await;
|
||||
if let Ok(o) = &commit_out {
|
||||
if !o.status.success() {
|
||||
// Ignore "nothing to commit" (exit 1); surface others
|
||||
let msg = String::from_utf8_lossy(&o.stderr);
|
||||
if !msg.contains("nothing to commit") {
|
||||
tx.send(AppEvent::InsertHistory(vec![ratatui::text::Line::from(format!("`/branch finalize` — commit failed: {}", msg.trim()))]));
|
||||
if !o.status.success() {
|
||||
// Ignore "nothing to commit" (exit 1); surface others
|
||||
let msg = String::from_utf8_lossy(&o.stderr);
|
||||
if !msg.contains("nothing to commit") {
|
||||
use codex_core::protocol::{BackgroundEventEvent, Event, EventMsg};
|
||||
let _ = tx.send(AppEvent::CodexEvent(Event { id: uuid::Uuid::new_v4().to_string(), event_seq: 0, msg: EventMsg::BackgroundEvent(BackgroundEventEvent { message: format!("`/branch finalize` — commit failed: {}", msg.trim()) }), order: None }));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Determine default branch in main repo
|
||||
let default_branch = match codex_core::git_worktree::detect_default_branch(&git_root).await {
|
||||
Some(b) => b,
|
||||
None => {
|
||||
tx.send(AppEvent::InsertHistory(vec![ratatui::text::Line::from("`/branch finalize` — failed to determine default branch (tried origin/HEAD, main, master)")]));
|
||||
use codex_core::protocol::{BackgroundEventEvent, Event, EventMsg};
|
||||
let _ = tx.send(AppEvent::CodexEvent(Event { id: uuid::Uuid::new_v4().to_string(), event_seq: 0, msg: EventMsg::BackgroundEvent(BackgroundEventEvent { message: "`/branch finalize` — failed to determine default branch (tried origin/HEAD, main, master)".to_string() }), order: None }));
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -6183,13 +6199,15 @@ impl ChatWidget<'_> {
|
||||
// Merge branch into default from the main repo root
|
||||
let co = Command::new("git").current_dir(&git_root).args(["checkout", &default_branch]).output().await;
|
||||
if !matches!(co, Ok(ref o) if o.status.success()) {
|
||||
tx.send(AppEvent::InsertHistory(vec![ratatui::text::Line::from("`/branch finalize` — failed to checkout default branch") ]));
|
||||
use codex_core::protocol::{BackgroundEventEvent, Event, EventMsg};
|
||||
let _ = tx.send(AppEvent::CodexEvent(Event { id: uuid::Uuid::new_v4().to_string(), event_seq: 0, msg: EventMsg::BackgroundEvent(BackgroundEventEvent { message: "`/branch finalize` — failed to checkout default branch".to_string() }), order: None }));
|
||||
return;
|
||||
}
|
||||
let merge = Command::new("git").current_dir(&git_root).args(["merge", "--no-ff", &branch_name]).output().await;
|
||||
if !matches!(merge, Ok(ref o) if o.status.success()) {
|
||||
let err = merge.ok().and_then(|o| String::from_utf8(o.stderr).ok()).unwrap_or_else(|| "unknown error".to_string());
|
||||
tx.send(AppEvent::InsertHistory(vec![ratatui::text::Line::from(format!("`/branch finalize` — merge failed: {}", err.trim()))]));
|
||||
use codex_core::protocol::{BackgroundEventEvent, Event, EventMsg};
|
||||
let _ = tx.send(AppEvent::CodexEvent(Event { id: uuid::Uuid::new_v4().to_string(), event_seq: 0, msg: EventMsg::BackgroundEvent(BackgroundEventEvent { message: format!("`/branch finalize` — merge failed: {}", err.trim()) }), order: None }));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -6199,7 +6217,10 @@ impl ChatWidget<'_> {
|
||||
|
||||
// Inform user and switch back to git root
|
||||
let msg = format!("Merged '{}' into '{}' and cleaned up worktree. Switching back to {}", branch_name, default_branch, git_root.display());
|
||||
tx.send(AppEvent::InsertHistory(vec![ratatui::text::Line::from(msg.clone())]));
|
||||
{
|
||||
use codex_core::protocol::{BackgroundEventEvent, Event, EventMsg};
|
||||
let _ = tx.send(AppEvent::CodexEvent(Event { id: uuid::Uuid::new_v4().to_string(), event_seq: 0, msg: EventMsg::BackgroundEvent(BackgroundEventEvent { message: msg.clone() }), order: None }));
|
||||
}
|
||||
tx.send(AppEvent::SwitchCwd(git_root, None));
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user