diff --git a/apps/staged/src-tauri/src/branches.rs b/apps/staged/src-tauri/src/branches.rs index 94ef5cca..6afd3a9e 100644 --- a/apps/staged/src-tauri/src/branches.rs +++ b/apps/staged/src-tauri/src/branches.rs @@ -337,10 +337,31 @@ async fn clone_repo_into_workspace( "Failed to fetch base branch '{base_ref}' for '{repo_slug}' in workspace '{ws_name}': {e}" ))?; + // If the branch already exists on the remote (e.g. from an existing PR), + // fetch it and start from there so we pick up its commits. Otherwise fall + // back to starting from the base branch. + let has_remote_branch = if branch_name != base_ref { + run_workspace_git_async( + ws_name, + Some(repo_subpath), + &["fetch", "origin", branch_name], + ) + .await + .is_ok() + } else { + false + }; + + let start_point = if has_remote_branch { + format!("origin/{branch_name}") + } else { + format!("origin/{base_ref}") + }; + run_workspace_git_async( ws_name, Some(repo_subpath), - &["checkout", "-B", branch_name, &format!("origin/{base_ref}")], + &["checkout", "-B", branch_name, &start_point], ) .await .map_err(|e| { @@ -1026,6 +1047,7 @@ pub async fn create_remote_branch( #[tauri::command(rename_all = "camelCase")] pub async fn start_workspace( store: tauri::State<'_, Mutex>>>, + app_handle: AppHandle, branch_id: String, ) -> Result<(), String> { let store = get_store(&store)?; @@ -1035,6 +1057,12 @@ pub async fn start_workspace( .map_err(|e| e.to_string())? .ok_or_else(|| format!("Branch not found: {branch_id}"))?; + // Track whether this is the first workspace start (Starting → Running) + // vs a restart (Stopped → Running). We only trigger auto-review on the + // first start so that existing branches don't get a spurious review + // every time the workspace restarts. + let is_first_start = branch.workspace_status == Some(store::WorkspaceStatus::Starting); + let project = store .get_project(&branch.project_id) .map_err(|e| e.to_string())? @@ -1088,6 +1116,24 @@ pub async fn start_workspace( store .update_branch_workspace_status(&branch_id, &store::WorkspaceStatus::Running) .map_err(|e| e.to_string())?; + + // Trigger auto-review for the newly cloned secondary repo + // if this is the first start for this branch. + if is_first_start { + let store_bg = Arc::clone(&store); + let app_handle_bg = app_handle.clone(); + let branch_id_bg = branch_id.clone(); + tauri::async_runtime::spawn(async move { + crate::maybe_trigger_auto_review_for_new_repo( + &store_bg, + &app_handle_bg, + &branch_id_bg, + None, + ) + .await; + }); + } + return Ok(()); } } @@ -1124,21 +1170,60 @@ pub async fn start_workspace( ws_name, ws_start_started_at.elapsed().as_millis() ); - // Create the feature branch inside the workspace so work happens - // on `branch_name` rather than the detached base ref. - if let Err(e) = run_workspace_git_async( + // If the branch already exists on the remote (e.g. from an + // existing PR), fetch it and start from there so we pick up its + // commits. Otherwise create a fresh branch from the base ref. + let has_remote_branch = run_workspace_git_async( ws_name, repo_subpath.as_deref(), - &["checkout", "-b", &branch.branch_name], + &["fetch", "origin", &branch.branch_name], ) .await - { + .is_ok(); + + let remote_ref = format!("origin/{}", branch.branch_name); + let checkout_result = if has_remote_branch { + run_workspace_git_async( + ws_name, + repo_subpath.as_deref(), + &["checkout", "-B", &branch.branch_name, &remote_ref], + ) + .await + } else { + run_workspace_git_async( + ws_name, + repo_subpath.as_deref(), + &["checkout", "-b", &branch.branch_name], + ) + .await + }; + + if let Err(e) = checkout_result { log::warn!( "failed to create branch '{}' in workspace '{}': {e}", branch.branch_name, ws_name ); } + + // If this is the first workspace start for a new branch, check + // whether the branch already has commits (e.g. from an existing + // PR) and kick off an automatic code review. + if is_first_start { + let store_bg = Arc::clone(&store); + let app_handle_bg = app_handle.clone(); + let branch_id_bg = branch_id.clone(); + tauri::async_runtime::spawn(async move { + crate::maybe_trigger_auto_review_for_new_repo( + &store_bg, + &app_handle_bg, + &branch_id_bg, + None, + ) + .await; + }); + } + Ok(()) } Err(blox::BloxError::NotAuthenticated) => { diff --git a/apps/staged/src-tauri/src/lib.rs b/apps/staged/src-tauri/src/lib.rs index 338d165d..5540b8ee 100644 --- a/apps/staged/src-tauri/src/lib.rs +++ b/apps/staged/src-tauri/src/lib.rs @@ -209,6 +209,69 @@ fn confirm_reset_store( // Project commands // ============================================================================= +/// Check whether a newly added repo already has commits on its branch and, if +/// so, kick off an automatic code review. +/// +/// For **local** branches (`worktree_path: Some`) the check is done by +/// inspecting the worktree on disk before triggering. +/// For **remote** branches (`worktree_path: None`) the review is triggered +/// unconditionally — the caller is responsible for ensuring the workspace is +/// running before calling this (e.g. after `start_workspace` or +/// `setup_remote_repo_clone` completes). +/// +/// This is fire-and-forget — errors are logged but never propagated. +pub(crate) async fn maybe_trigger_auto_review_for_new_repo( + store: &Arc, + app_handle: &tauri::AppHandle, + branch_id: &str, + worktree_path: Option<&str>, +) { + // For local branches, check whether there are any commits on the branch + // relative to its base before spinning up a review session. + if let Some(path) = worktree_path { + let branch = match store.get_branch(branch_id) { + Ok(Some(b)) => b, + _ => return, + }; + let worktree = std::path::PathBuf::from(path); + match git::get_commits_since_base(&worktree, &branch.base_branch) { + Ok(commits) if commits.is_empty() => { + log::info!( + "[auto_review] branch {branch_id} has no commits yet — skipping auto review" + ); + return; + } + Err(e) => { + log::warn!("[auto_review] failed to check commits for branch {branch_id}: {e}"); + return; + } + Ok(_) => { /* has commits — fall through to trigger */ } + } + } + + let registry = app_handle.state::>(); + match session_commands::trigger_auto_review( + Arc::clone(store), + Arc::clone(®istry), + app_handle.clone(), + branch_id.to_string(), + None, + ) + .await + { + Ok(resp) => { + log::info!( + "[auto_review] triggered for new repo on branch {branch_id}: session={}, review={}", + resp.session_id, + resp.artifact_id, + ); + } + Err(e) => { + log::warn!("[auto_review] failed to trigger for branch {branch_id}: {e}"); + } + } +} + #[tauri::command] fn list_projects( store: tauri::State<'_, Mutex>>>, @@ -298,7 +361,7 @@ fn create_project( format!("origin/{detected_base}") }; - let branch_id = match project.location { + let (branch_id, is_local) = match project.location { store::ProjectLocation::Local => { let mut branch = store::Branch::new(&project.id, &inferred_branch_name, &effective_base) @@ -307,7 +370,7 @@ fn create_project( branch = branch.with_pr(pr); } store.create_branch(&branch).map_err(|e| e.to_string())?; - Some(branch.id) + (branch.id, true) } store::ProjectLocation::Remote => { let workspace_name = branches::infer_workspace_name(&inferred_branch_name); @@ -328,12 +391,12 @@ fn create_project( workspace_name, project.id ); - None // remote branches don't need worktree setup + (branch.id, false) } }; - // Spawn background worktree + prerun-actions setup for local branches. - if let Some(branch_id) = branch_id { + if is_local { + // Spawn background worktree + prerun-actions setup for local branches. let project_id = project.id.clone(); let store_bg = Arc::clone(&store); tauri::async_runtime::spawn(async move { @@ -346,10 +409,11 @@ fn create_project( }) .await; - match worktree_result { + let worktree_path = match worktree_result { Ok(Ok(path)) => { log::info!("[create_project] worktree ready at {path}"); let _ = app_handle.emit("project-setup-progress", project_id.clone()); + path } Ok(Err(e)) => { log::warn!("[create_project] worktree setup failed: {e}"); @@ -359,7 +423,7 @@ fn create_project( log::warn!("[create_project] worktree task panicked: {e}"); return; } - } + }; let executor = app_handle.state::>(); let act_registry = app_handle.state::>(); @@ -380,7 +444,21 @@ fn create_project( log::warn!("[create_project] prerun actions failed: {e}"); } } + + // If the repo already has commits on this branch, kick off + // an automatic code review so the user gets immediate feedback. + maybe_trigger_auto_review_for_new_repo( + &store_bg, + &app_handle, + &branch_id, + Some(&worktree_path), + ) + .await; }); + } else { + // Remote branches: auto-review is deferred until `start_workspace` + // completes — the workspace isn't running yet at this point, so + // attempting to trigger a review here would fail. } } else if project.location == store::ProjectLocation::Remote { log::info!( @@ -472,10 +550,11 @@ async fn add_project_repo( }) .await; - match worktree_result { + let worktree_path = match worktree_result { Ok(Ok(path)) => { log::info!("[add_project_repo] worktree ready at {path}"); let _ = app_handle.emit("project-setup-progress", project_id.clone()); + path } Ok(Err(e)) => { log::warn!("[add_project_repo] worktree setup failed: {e}"); @@ -485,7 +564,7 @@ async fn add_project_repo( log::warn!("[add_project_repo] worktree task panicked: {e}"); return; } - } + }; let executor = app_handle.state::>(); let act_registry = app_handle.state::>(); @@ -506,6 +585,16 @@ async fn add_project_repo( log::warn!("[add_project_repo] prerun actions failed: {e}"); } } + + // If the repo already has commits on this branch, kick off + // an automatic code review so the user gets immediate feedback. + maybe_trigger_auto_review_for_new_repo( + &store, + &app_handle, + &branch.id, + Some(&worktree_path), + ) + .await; } else { // Remote branch: clone the repo into the running workspace, // fetch the base branch, and create the feature branch. @@ -521,9 +610,21 @@ async fn add_project_repo( "[add_project_repo] remote repo clone failed for branch '{}': {e}", branch.branch_name ); + let _ = app_handle.emit("project-setup-progress", project_id); + return; } } let _ = app_handle.emit("project-setup-progress", project_id); + + // If the repo already has commits on this branch, kick off + // an automatic code review so the user gets immediate feedback. + maybe_trigger_auto_review_for_new_repo( + &store, + &app_handle, + &branch.id, + None, // remote — trigger_auto_review resolves HEAD via workspace + ) + .await; } } }); diff --git a/apps/staged/src-tauri/src/project_mcp.rs b/apps/staged/src-tauri/src/project_mcp.rs index a82b0d1d..fb4297f3 100644 --- a/apps/staged/src-tauri/src/project_mcp.rs +++ b/apps/staged/src-tauri/src/project_mcp.rs @@ -637,16 +637,14 @@ impl ProjectToolsHandler { }) .await; - match worktree_result { - Ok(Ok(worktree_path)) => { - log::debug!( - "[project_mcp] add_project_repo: worktree ready at {}", - worktree_path - ); + let worktree_path = match worktree_result { + Ok(Ok(path)) => { + log::debug!("[project_mcp] add_project_repo: worktree ready at {}", path); // Notify UI that the worktree is ready so branch state updates let _ = self .app_handle .emit("project-setup-progress", self.project_id.clone()); + path } Ok(Err(e)) => { log::warn!( @@ -665,7 +663,7 @@ impl ProjectToolsHandler { "Added repository {github_repo} to project (worktree task error: {e})" ); } - } + }; // Run detect_actions + prerun actions if we have an executor if let (Some(executor), Some(act_registry)) = @@ -705,6 +703,16 @@ impl ProjectToolsHandler { "[project_mcp] add_project_repo: no action executor available, skipping prerun actions" ); } + + // If the repo already has commits on this branch, kick off + // an automatic code review so the user gets immediate feedback. + crate::maybe_trigger_auto_review_for_new_repo( + &self.store, + &self.app_handle, + &branch.id, + Some(&worktree_path), + ) + .await; } format!("Added repository {github_repo} to project")