diff --git a/Cargo.lock b/Cargo.lock index b2955988941ce54f866114368c5f8f54b02f669d..6f9ba18b91989a90748f4d8f6b4f5305860a0220 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -358,6 +358,7 @@ dependencies = [ "futures 0.3.31", "fuzzy", "git", + "git_ui", "gpui", "gpui_tokio", "heapless", diff --git a/crates/agent_ui/Cargo.toml b/crates/agent_ui/Cargo.toml index 6c045d4dd2114834605da278aad111fab174d4c6..875ed31f981e2f320045e18cd67dbfb26f1275d1 100644 --- a/crates/agent_ui/Cargo.toml +++ b/crates/agent_ui/Cargo.toml @@ -59,6 +59,7 @@ file_icons.workspace = true fs.workspace = true futures.workspace = true git.workspace = true +git_ui.workspace = true fuzzy.workspace = true gpui.workspace = true gpui_tokio.workspace = true diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 8d6cac0e647d78f9746b1dcf3d1966ec6b6653af..07bd25ccd914bd95efb54a43c22f1b7badcc7531 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -70,6 +70,7 @@ use gpui::{ }; use language::LanguageRegistry; use language_model::{ConfigurationError, LanguageModelRegistry}; +use notifications::status_toast::{StatusToast, ToastIcon}; use project::project_settings::ProjectSettings; use project::{Project, ProjectPath, Worktree}; use prompt_store::{PromptBuilder, PromptStore, UserPromptId}; @@ -751,6 +752,12 @@ pub struct AgentPanel { _thread_view_subscription: Option, _active_thread_focus_subscription: Option, _worktree_creation_task: Option>, + worktree_creation_tokens: Vec<( + Entity, + PathBuf, + project::git_store::CancellationToken, + )>, + worktree_creation_toast: Option>, show_trust_workspace_message: bool, last_configuration_error_telemetry: Option, on_boarding_upsell_dismissed: AtomicBool, @@ -1084,6 +1091,8 @@ impl AgentPanel { _thread_view_subscription: None, _active_thread_focus_subscription: None, _worktree_creation_task: None, + worktree_creation_tokens: Vec::new(), + worktree_creation_toast: None, show_trust_workspace_message: false, last_configuration_error_telemetry: None, on_boarding_upsell_dismissed: AtomicBool::new(OnboardingUpsell::dismissed(cx)), @@ -1179,6 +1188,12 @@ impl AgentPanel { } pub fn new_thread(&mut self, _action: &NewThread, window: &mut Window, cx: &mut Context) { + if matches!( + self.worktree_creation_status, + Some(WorktreeCreationStatus::Creating) + ) { + self.cancel_worktree_creation(window, cx); + } self.reset_start_thread_in_to_default(cx); self.external_thread(None, None, None, None, None, true, window, cx); } @@ -2632,24 +2647,34 @@ impl AgentPanel { futures::channel::oneshot::Receiver>, )>, Vec<(PathBuf, PathBuf)>, + Vec<( + Entity, + PathBuf, + project::git_store::CancellationToken, + )>, )> { let mut creation_infos = Vec::new(); let mut path_remapping = Vec::new(); + let mut tokens = Vec::new(); for repo in git_repos { - let (work_dir, new_path, receiver) = repo.update(cx, |repo, _cx| { + let (work_dir, new_path, receiver, token) = repo.update(cx, |repo, _cx| { let new_path = repo.path_for_new_linked_worktree(branch_name, worktree_directory_setting)?; - let receiver = - repo.create_worktree(branch_name.to_string(), new_path.clone(), None); + let (receiver, token) = repo.create_cancellable_worktree( + branch_name.to_string(), + new_path.clone(), + None, + ); let work_dir = repo.work_directory_abs_path.clone(); - anyhow::Ok((work_dir, new_path, receiver)) + anyhow::Ok((work_dir, new_path, receiver, token)) })?; path_remapping.push((work_dir.to_path_buf(), new_path.clone())); + tokens.push((repo.clone(), new_path.clone(), token)); creation_infos.push((repo.clone(), new_path, receiver)); } - Ok((creation_infos, path_remapping)) + Ok((creation_infos, path_remapping, tokens)) } /// Waits for every in-flight worktree creation to complete. If any @@ -2730,6 +2755,47 @@ impl AgentPanel { Err(anyhow!(error_message)) } + fn cancel_worktree_creation(&mut self, _window: &mut Window, cx: &mut Context) { + if !matches!( + self.worktree_creation_status, + Some(WorktreeCreationStatus::Creating) + ) { + return; + } + + self._worktree_creation_task = None; + + let tokens = std::mem::take(&mut self.worktree_creation_tokens); + for (_repo, path_for_cleanup, token) in tokens { + drop(token.cancel(move |state, cx| { + cx.background_spawn(async move { + if let project::git_store::RepositoryState::Local(local) = state { + local + .backend + .remove_worktree(path_for_cleanup, true) + .await + .log_err(); + } + }) + })); + } + + self.worktree_creation_status = None; + + if let Some(toast) = self.worktree_creation_toast.take() { + toast.update(cx, |_, cx| cx.emit(DismissEvent)); + } + + cx.notify(); + } + + pub fn is_creating_worktree(&self) -> bool { + matches!( + self.worktree_creation_status, + Some(WorktreeCreationStatus::Creating) + ) + } + fn set_worktree_creation_error( &mut self, message: SharedString, @@ -2737,6 +2803,10 @@ impl AgentPanel { cx: &mut Context, ) { self.worktree_creation_status = Some(WorktreeCreationStatus::Error(message)); + self.worktree_creation_tokens.clear(); + if let Some(toast) = self.worktree_creation_toast.take() { + toast.update(cx, |_, cx| cx.emit(DismissEvent)); + } if matches!(self.active_view, ActiveView::Uninitialized) { let selected_agent_type = self.selected_agent_type.clone(); self.new_agent_thread(selected_agent_type, window, cx); @@ -2760,6 +2830,22 @@ impl AgentPanel { self.worktree_creation_status = Some(WorktreeCreationStatus::Creating); cx.notify(); + if let Some(workspace) = self.workspace.upgrade() { + let toast = StatusToast::new("Creating worktree\u{2026}", cx, |this, _cx| { + this.icon(ToastIcon::new(ui::IconName::LoadCircle).color(ui::Color::Muted)) + .action("Cancel", |window, cx| { + window.dispatch_action( + git_ui::worktree_picker::CancelWorktreeCreation.boxed_clone(), + cx, + ); + }) + }); + self.worktree_creation_toast = Some(toast.clone()); + workspace.update(cx, |workspace, cx| { + workspace.toggle_status_toast(toast, cx); + }); + } + let (git_repos, non_git_paths) = self.classify_worktrees(cx); if git_repos.is_empty() { @@ -2835,27 +2921,33 @@ impl AgentPanel { } }; - let (creation_infos, path_remapping) = match this.update_in(cx, |_this, _window, cx| { - Self::start_worktree_creations( - &git_repos, - &branch_name, - &worktree_directory_setting, - cx, - ) - }) { - Ok(Ok(result)) => result, - Ok(Err(err)) | Err(err) => { - this.update_in(cx, |this, window, cx| { - this.set_worktree_creation_error( - format!("Failed to validate worktree directory: {err}").into(), - window, - cx, - ); - }) - .log_err(); - return anyhow::Ok(()); - } - }; + let (creation_infos, path_remapping, tokens) = + match this.update_in(cx, |_this, _window, cx| { + Self::start_worktree_creations( + &git_repos, + &branch_name, + &worktree_directory_setting, + cx, + ) + }) { + Ok(Ok(result)) => result, + Ok(Err(err)) | Err(err) => { + this.update_in(cx, |this, window, cx| { + this.set_worktree_creation_error( + format!("Failed to validate worktree directory: {err}").into(), + window, + cx, + ); + }) + .log_err(); + return anyhow::Ok(()); + } + }; + + this.update_in(cx, |this, _window, _cx| { + this.worktree_creation_tokens = tokens; + }) + .ok(); let created_paths = match Self::await_and_rollback_on_failure(creation_infos, cx).await { @@ -2886,6 +2978,14 @@ impl AgentPanel { } }; + this.update_in(cx, |this, _window, cx| { + this.worktree_creation_tokens.clear(); + if let Some(toast) = this.worktree_creation_toast.take() { + toast.update(cx, |_, cx| cx.emit(DismissEvent)); + } + }) + .ok(); + let this_for_error = this.clone(); if let Err(err) = Self::setup_new_workspace( this, @@ -4112,6 +4212,24 @@ impl AgentPanel { }), ) }) + .when( + matches!( + self.worktree_creation_status, + Some(WorktreeCreationStatus::Creating) + ), + |this| { + this.child( + IconButton::new("cancel-worktree-creation", IconName::Stop) + .icon_size(IconSize::Small) + .icon_color(Color::Error) + .style(ButtonStyle::Tinted(ui::TintColor::Error)) + .tooltip(Tooltip::text("Cancel worktree creation")) + .on_click(cx.listener(|this, _, window, cx| { + this.cancel_worktree_creation(window, cx); + })), + ) + }, + ) .child(self.render_panel_options_menu(window, cx)), ) .into_any_element() @@ -4178,37 +4296,34 @@ impl AgentPanel { }), ) }) + .when( + matches!( + self.worktree_creation_status, + Some(WorktreeCreationStatus::Creating) + ), + |this| { + this.child( + IconButton::new("cancel-worktree-creation", IconName::Stop) + .icon_size(IconSize::Small) + .icon_color(Color::Error) + .style(ButtonStyle::Tinted(ui::TintColor::Error)) + .tooltip(Tooltip::text("Cancel worktree creation")) + .on_click(cx.listener(|this, _, window, cx| { + this.cancel_worktree_creation(window, cx); + })), + ) + }, + ) .child(self.render_panel_options_menu(window, cx)), ) .into_any_element() } } - fn render_worktree_creation_status(&self, cx: &mut Context) -> Option { + fn render_worktree_creation_status(&self, _cx: &mut Context) -> Option { let status = self.worktree_creation_status.as_ref()?; match status { - WorktreeCreationStatus::Creating => Some( - h_flex() - .absolute() - .bottom_12() - .w_full() - .p_2() - .gap_1() - .justify_center() - .bg(cx.theme().colors().editor_background) - .child( - Icon::new(IconName::LoadCircle) - .size(IconSize::Small) - .color(Color::Muted) - .with_rotate_animation(3), - ) - .child( - Label::new("Creating Worktree…") - .color(Color::Muted) - .size(LabelSize::Small), - ) - .into_any_element(), - ), + WorktreeCreationStatus::Creating => None, WorktreeCreationStatus::Error(message) => Some( Callout::new() .icon(IconName::Warning) @@ -4648,6 +4763,11 @@ impl Render for AgentPanel { .on_action(cx.listener(Self::decrease_font_size)) .on_action(cx.listener(Self::reset_font_size)) .on_action(cx.listener(Self::toggle_zoom)) + .on_action(cx.listener( + |this, _: &git_ui::worktree_picker::CancelWorktreeCreation, window, cx| { + this.cancel_worktree_creation(window, cx); + }, + )) .on_action(cx.listener(|this, _: &ReauthenticateAgent, window, cx| { if let Some(conversation_view) = this.active_conversation_view() { conversation_view.update(cx, |conversation_view, cx| { diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index 036ceeb620e1aa0345b6f9a296c16069c0fa09bf..e804247df6b2a5d90a5e3e3f2e176541a25abd64 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -1660,7 +1660,9 @@ impl GitRepository for RealGitRepository { .spawn(async move { std::fs::create_dir_all(path.parent().unwrap_or(&path))?; let git = git_binary?; - let output = git.build_command(&args).output().await?; + let mut command = git.build_command(&args); + command.kill_on_drop(true); + let output = command.output().await?; if output.status.success() { Ok(()) } else { diff --git a/crates/git_ui/src/worktree_picker.rs b/crates/git_ui/src/worktree_picker.rs index d7488cc1bddb7e9d2825b8d21d3dc6c4c4fdde5a..9600f21372aeca941ebf385316cbc201d68d239f 100644 --- a/crates/git_ui/src/worktree_picker.rs +++ b/crates/git_ui/src/worktree_picker.rs @@ -29,7 +29,9 @@ actions!( [ WorktreeFromDefault, WorktreeFromDefaultOnWindow, - DeleteWorktree + DeleteWorktree, + /// Cancels an in-progress worktree creation. + CancelWorktreeCreation, ] ); diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index f439c5da157cdcdaec813a1fd63ea119af78cb83..8bbdce40b4159006cd7cbb0555063fb98939c1b8 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -298,6 +298,47 @@ pub struct RepositorySnapshot { type JobId = u64; +#[derive(Copy, Clone)] +pub enum CancelOutcome { + /// The job was still queued and was removed before it started. + RemovedFromQueue, + /// The job was running; the process was killed. Cleanup callback has been awaited. + KilledRunning, + /// The job had already finished before the cancel arrived. + AlreadyFinished, +} + +pub struct CancellationToken { + id: JobId, + job_sender: mpsc::UnboundedSender, +} + +impl CancellationToken { + pub fn cancel( + self, + cleanup: impl FnOnce(RepositoryState, &mut AsyncApp) -> Task<()> + 'static, + ) -> oneshot::Receiver { + let (result_tx, result_rx) = oneshot::channel(); + self.job_sender + .unbounded_send(GitWorkerMessage::Cancel { + id: self.id, + cleanup: Some(Box::new(cleanup)), + result_tx, + }) + .ok(); + result_rx + } +} + +enum GitWorkerMessage { + Job(GitJob), + Cancel { + id: JobId, + cleanup: Option Task<()>>>, + result_tx: oneshot::Sender, + }, +} + #[derive(Clone, Debug, PartialEq, Eq)] pub struct JobInfo { pub start: Instant, @@ -336,7 +377,7 @@ pub struct Repository { // For a local repository, holds paths that have had worktree events since the last status scan completed, // and that should be examined during the next status scan. paths_needing_status_update: Vec>, - job_sender: mpsc::UnboundedSender, + job_sender: mpsc::UnboundedSender, active_jobs: HashMap, pending_ops: SumTree, job_id: JobId, @@ -455,6 +496,7 @@ impl EventEmitter for Repository {} impl EventEmitter for GitStore {} pub struct GitJob { + id: JobId, job: Box Task<()>>, key: Option, } @@ -4175,6 +4217,57 @@ impl Repository { self.send_keyed_job(None, status, job) } + pub fn send_cancellable_job( + &mut self, + status: Option, + job: F, + ) -> (oneshot::Receiver, CancellationToken) + where + F: FnOnce(RepositoryState, AsyncApp) -> Fut + 'static, + Fut: Future + 'static, + R: Send + 'static, + { + let (result_tx, result_rx) = futures::channel::oneshot::channel(); + let job_id = post_inc(&mut self.job_id); + let this = self.this.clone(); + let token = CancellationToken { + id: job_id, + job_sender: self.job_sender.clone(), + }; + self.job_sender + .unbounded_send(GitWorkerMessage::Job(GitJob { + id: job_id, + key: None, + job: Box::new(move |state, cx: &mut AsyncApp| { + let job = job(state, cx.clone()); + cx.spawn(async move |cx| { + if let Some(s) = status.clone() { + this.update(cx, |this, cx| { + this.active_jobs.insert( + job_id, + JobInfo { + start: Instant::now(), + message: s.clone(), + }, + ); + cx.notify(); + }) + .ok(); + } + let result = job.await; + this.update(cx, |this, cx| { + this.active_jobs.remove(&job_id); + cx.notify(); + }) + .ok(); + result_tx.send(result).ok(); + }) + }), + })) + .ok(); + (result_rx, token) + } + fn send_keyed_job( &mut self, key: Option, @@ -4190,7 +4283,8 @@ impl Repository { let job_id = post_inc(&mut self.job_id); let this = self.this.clone(); self.job_sender - .unbounded_send(GitJob { + .unbounded_send(GitWorkerMessage::Job(GitJob { + id: job_id, key, job: Box::new(move |state, cx: &mut AsyncApp| { let job = job(state, cx.clone()); @@ -4220,7 +4314,7 @@ impl Repository { result_tx.send(result).ok(); }) }), - }) + })) .ok(); result_rx } @@ -5882,6 +5976,37 @@ impl Repository { ) } + pub fn create_cancellable_worktree( + &mut self, + branch_name: String, + path: PathBuf, + commit: Option, + ) -> (oneshot::Receiver>, CancellationToken) { + let id = self.id; + self.send_cancellable_job( + Some(format!("git worktree add: {}", branch_name).into()), + move |repo, _cx| async move { + match repo { + RepositoryState::Local(LocalRepositoryState { backend, .. }) => { + backend.create_worktree(branch_name, path, commit).await + } + RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => { + client + .request(proto::GitCreateWorktree { + project_id: project_id.0, + repository_id: id.to_proto(), + name: branch_name, + directory: path.to_string_lossy().to_string(), + commit, + }) + .await?; + Ok(()) + } + } + }, + ) + } + pub fn remove_worktree(&mut self, path: PathBuf, force: bool) -> oneshot::Receiver> { let id = self.id; self.send_job( @@ -6405,8 +6530,8 @@ impl Repository { fn spawn_local_git_worker( state: Shared>>, cx: &mut Context, - ) -> mpsc::UnboundedSender { - let (job_tx, mut job_rx) = mpsc::unbounded::(); + ) -> mpsc::UnboundedSender { + let (job_tx, mut job_rx) = mpsc::unbounded::(); cx.spawn(async move |_, cx| { let state = state.await.map_err(|err| anyhow::anyhow!(err))?; @@ -6420,10 +6545,34 @@ impl Repository { .await; } let state = RepositoryState::Local(state); - let mut jobs = VecDeque::new(); + let mut jobs: VecDeque = VecDeque::new(); + let mut completed_ids: HashSet = HashSet::new(); loop { - while let Ok(Some(next_job)) = job_rx.try_next() { - jobs.push_back(next_job); + while let Ok(Some(msg)) = job_rx.try_next() { + match msg { + GitWorkerMessage::Job(job) => jobs.push_back(job), + GitWorkerMessage::Cancel { + id, + cleanup, + result_tx, + } => { + let before = jobs.len(); + jobs.retain(|j| j.id != id); + let outcome = if jobs.len() < before { + CancelOutcome::RemovedFromQueue + } else if completed_ids.contains(&id) { + CancelOutcome::AlreadyFinished + } else { + CancelOutcome::AlreadyFinished + }; + result_tx.send(outcome).ok(); + if let Some(f) = cleanup { + if matches!(outcome, CancelOutcome::RemovedFromQueue) { + f(state.clone(), cx).await; + } + } + } + } } if let Some(job) = jobs.pop_front() { @@ -6434,9 +6583,74 @@ impl Repository { { continue; } - (job.job)(state.clone(), cx).await; - } else if let Some(job) = job_rx.next().await { - jobs.push_back(job); + let running_job_id = job.id; + let task = (job.job)(state.clone(), cx); + let mut task = std::pin::pin!(task); + + loop { + match futures::future::select(task, job_rx.next()).await { + futures::future::Either::Left(((), _)) => { + completed_ids.insert(running_job_id); + if completed_ids.len() > 256 { + let to_remove: Vec<_> = + completed_ids.iter().copied().take(128).collect(); + for id in to_remove { + completed_ids.remove(&id); + } + } + break; + } + futures::future::Either::Right((Some(msg), ongoing_task)) => { + match msg { + GitWorkerMessage::Job(j) => { + jobs.push_back(j); + task = ongoing_task; + } + GitWorkerMessage::Cancel { + id, + cleanup, + result_tx, + } => { + if id == running_job_id { + let _ = ongoing_task; + if let Some(f) = cleanup { + f(state.clone(), cx).await; + } + result_tx.send(CancelOutcome::KilledRunning).ok(); + break; + } else { + let before = jobs.len(); + jobs.retain(|j| j.id != id); + let outcome = if jobs.len() < before { + CancelOutcome::RemovedFromQueue + } else if completed_ids.contains(&id) { + CancelOutcome::AlreadyFinished + } else { + CancelOutcome::AlreadyFinished + }; + result_tx.send(outcome).ok(); + task = ongoing_task; + } + } + } + } + futures::future::Either::Right((None, ongoing_task)) => { + ongoing_task.await; + break; + } + } + } + } else if let Some(msg) = job_rx.next().await { + match msg { + GitWorkerMessage::Job(job) => jobs.push_back(job), + GitWorkerMessage::Cancel { + id: _, + cleanup: _, + result_tx, + } => { + result_tx.send(CancelOutcome::AlreadyFinished).ok(); + } + } } else { break; } @@ -6451,15 +6665,39 @@ impl Repository { fn spawn_remote_git_worker( state: RemoteRepositoryState, cx: &mut Context, - ) -> mpsc::UnboundedSender { - let (job_tx, mut job_rx) = mpsc::unbounded::(); + ) -> mpsc::UnboundedSender { + let (job_tx, mut job_rx) = mpsc::unbounded::(); cx.spawn(async move |_, cx| { let state = RepositoryState::Remote(state); - let mut jobs = VecDeque::new(); + let mut jobs: VecDeque = VecDeque::new(); + let mut completed_ids: HashSet = HashSet::new(); loop { - while let Ok(Some(next_job)) = job_rx.try_next() { - jobs.push_back(next_job); + while let Ok(Some(msg)) = job_rx.try_next() { + match msg { + GitWorkerMessage::Job(job) => jobs.push_back(job), + GitWorkerMessage::Cancel { + id, + cleanup, + result_tx, + } => { + let before = jobs.len(); + jobs.retain(|j| j.id != id); + let outcome = if jobs.len() < before { + CancelOutcome::RemovedFromQueue + } else if completed_ids.contains(&id) { + CancelOutcome::AlreadyFinished + } else { + CancelOutcome::AlreadyFinished + }; + result_tx.send(outcome).ok(); + if let Some(f) = cleanup { + if matches!(outcome, CancelOutcome::RemovedFromQueue) { + f(state.clone(), cx).await; + } + } + } + } } if let Some(job) = jobs.pop_front() { @@ -6470,9 +6708,74 @@ impl Repository { { continue; } - (job.job)(state.clone(), cx).await; - } else if let Some(job) = job_rx.next().await { - jobs.push_back(job); + let running_job_id = job.id; + let task = (job.job)(state.clone(), cx); + let mut task = std::pin::pin!(task); + + loop { + match futures::future::select(task, job_rx.next()).await { + futures::future::Either::Left(((), _)) => { + completed_ids.insert(running_job_id); + if completed_ids.len() > 256 { + let to_remove: Vec<_> = + completed_ids.iter().copied().take(128).collect(); + for id in to_remove { + completed_ids.remove(&id); + } + } + break; + } + futures::future::Either::Right((Some(msg), ongoing_task)) => { + match msg { + GitWorkerMessage::Job(j) => { + jobs.push_back(j); + task = ongoing_task; + } + GitWorkerMessage::Cancel { + id, + cleanup, + result_tx, + } => { + if id == running_job_id { + let _ = ongoing_task; + if let Some(f) = cleanup { + f(state.clone(), cx).await; + } + result_tx.send(CancelOutcome::KilledRunning).ok(); + break; + } else { + let before = jobs.len(); + jobs.retain(|j| j.id != id); + let outcome = if jobs.len() < before { + CancelOutcome::RemovedFromQueue + } else if completed_ids.contains(&id) { + CancelOutcome::AlreadyFinished + } else { + CancelOutcome::AlreadyFinished + }; + result_tx.send(outcome).ok(); + task = ongoing_task; + } + } + } + } + futures::future::Either::Right((None, ongoing_task)) => { + ongoing_task.await; + break; + } + } + } + } else if let Some(msg) = job_rx.next().await { + match msg { + GitWorkerMessage::Job(job) => jobs.push_back(job), + GitWorkerMessage::Cancel { + id: _, + cleanup: _, + result_tx, + } => { + result_tx.send(CancelOutcome::AlreadyFinished).ok(); + } + } } else { break; } diff --git a/crates/sidebar/src/sidebar.rs b/crates/sidebar/src/sidebar.rs index 96a3b4e9c41618a54637bfdd78642e559b975228..19400e0bf9a31e6f280d0eaf4138a48483653951 100644 --- a/crates/sidebar/src/sidebar.rs +++ b/crates/sidebar/src/sidebar.rs @@ -300,6 +300,7 @@ pub struct Sidebar { focused_thread: Option, agent_panel_visible: bool, active_thread_is_draft: bool, + is_creating_worktree: bool, hovered_thread_index: Option, collapsed_groups: HashSet, expanded_groups: HashMap, @@ -399,6 +400,7 @@ impl Sidebar { focused_thread: None, agent_panel_visible: false, active_thread_is_draft: false, + is_creating_worktree: false, hovered_thread_index: None, collapsed_groups: HashSet::new(), expanded_groups: HashMap::new(), @@ -652,6 +654,11 @@ impl Sidebar { .and_then(|ws| ws.read(cx).panel::(cx)) .map_or(false, |panel| panel.read(cx).active_thread_is_draft(cx)); + self.is_creating_worktree = active_workspace + .as_ref() + .and_then(|ws| ws.read(cx).panel::(cx)) + .map_or(false, |panel| panel.read(cx).is_creating_worktree()); + // Derive focused_thread from the active workspace's agent panel. // Only update when the panel gives us a positive signal — if the // panel returns None (e.g. still loading after a thread activation), @@ -2934,6 +2941,9 @@ impl Sidebar { .icon_color(Color::Custom(cx.theme().colors().icon_muted.opacity(0.8))) .selected(is_active) .focused(is_selected) + .when(self.is_creating_worktree && is_active, |this| { + this.status(AgentThreadStatus::Running) + }) .when(!is_active, |this| { this.on_click(cx.listener(move |this, _, window, cx| { this.selection = None;