diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 3fa98439c28e7d32d3663824827adea2399a2f01..09a8750ff92c42bf599b06f1b751442e99f746cf 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -2814,21 +2814,23 @@ impl AgentPanel { .worktree_directory .clone(); - let (dock_structure, open_file_paths) = self - .workspace - .upgrade() - .map(|workspace| { - let dock_structure = workspace.read(cx).capture_dock_state(window, cx); - let open_file_paths = workspace.read(cx).open_item_abs_paths(cx); - (dock_structure, open_file_paths) - }) - .unwrap_or_default(); + let active_file_path = self.workspace.upgrade().and_then(|workspace| { + let workspace = workspace.read(cx); + let active_item = workspace.active_item(cx)?; + let project_path = active_item.project_path(cx)?; + workspace + .project() + .read(cx) + .absolute_path(&project_path, cx) + }); let workspace = self.workspace.clone(); let window_handle = window .window_handle() .downcast::(); + let selected_agent = self.selected_agent(); + let task = cx.spawn_in(window, async move |this, cx| { // Await the branch listings we kicked off earlier. let mut existing_branches = Vec::new(); @@ -2922,12 +2924,12 @@ impl AgentPanel { all_paths, app_state, window_handle, - dock_structure, - open_file_paths, + active_file_path, path_remapping, non_git_paths, has_non_git, content, + selected_agent, cx, ) .await @@ -2955,27 +2957,21 @@ impl AgentPanel { all_paths: Vec, app_state: Arc, window_handle: Option>, - dock_structure: workspace::DockStructure, - open_file_paths: Vec, + active_file_path: Option, path_remapping: Vec<(PathBuf, PathBuf)>, non_git_paths: Vec, has_non_git: bool, content: Vec, + selected_agent: Option, cx: &mut AsyncWindowContext, ) -> Result<()> { - let init: Option< - Box) + Send>, - > = Some(Box::new(move |workspace, window, cx| { - workspace.set_dock_structure(dock_structure, window, cx); - })); - let OpenResult { window: new_window_handle, workspace: new_workspace, .. } = cx .update(|_window, cx| { - Workspace::new_local(all_paths, app_state, window_handle, None, init, false, cx) + Workspace::new_local(all_paths, app_state, window_handle, None, None, false, cx) })? .await?; @@ -3005,48 +3001,71 @@ impl AgentPanel { ); } - let remapped_paths: Vec = open_file_paths - .iter() - .filter_map(|original_path| { - let best_match = path_remapping - .iter() - .filter_map(|(old_root, new_root)| { - original_path.strip_prefix(old_root).ok().map(|relative| { - (old_root.components().count(), new_root.join(relative)) - }) + // If we had an active buffer, remap its path and reopen it. + let should_zoom_agent_panel = active_file_path.is_none(); + + let remapped_active_path = active_file_path.and_then(|original_path| { + let best_match = path_remapping + .iter() + .filter_map(|(old_root, new_root)| { + original_path.strip_prefix(old_root).ok().map(|relative| { + (old_root.components().count(), new_root.join(relative)) }) - .max_by_key(|(depth, _)| *depth); + }) + .max_by_key(|(depth, _)| *depth); + + if let Some((_, remapped_path)) = best_match { + return Some(remapped_path); + } - if let Some((_, remapped_path)) = best_match { - return Some(remapped_path); + for non_git in &non_git_paths { + if original_path.starts_with(non_git) { + return Some(original_path); } + } + None + }); - for non_git in &non_git_paths { - if original_path.starts_with(non_git) { - return Some(original_path.clone()); - } + if !should_zoom_agent_panel && remapped_active_path.is_none() { + log::warn!( + "Active file could not be remapped to the new worktree; it will not be reopened" + ); + } + + if let Some(path) = remapped_active_path { + let open_task = workspace.open_paths( + vec![path], + workspace::OpenOptions::default(), + None, + window, + cx, + ); + cx.spawn(async move |_, _| -> anyhow::Result<()> { + for item in open_task.await.into_iter().flatten() { + item?; } - None + Ok(()) }) - .collect(); - - if !remapped_paths.is_empty() { - workspace - .open_paths( - remapped_paths, - workspace::OpenOptions::default(), - None, - window, - cx, - ) - .detach(); + .detach_and_log_err(cx); } workspace.focus_drawer::(window, cx); + + // If no active buffer was open, zoom the agent panel + // (equivalent to cmd-esc fullscreen behavior). + // This must happen after focus_drawer, which activates + // and opens the panel in the drawer. + if should_zoom_agent_panel { + if let Some(panel) = workspace.drawer::() { + panel.update(cx, |_panel, cx| { + cx.emit(PanelEvent::ZoomIn); + }); + } + } if let Some(panel) = workspace.drawer::() { panel.update(cx, |panel, cx| { panel.external_thread( - None, + selected_agent, None, None, None, @@ -5962,6 +5981,160 @@ mod tests { ); } + #[gpui::test] + async fn test_worktree_creation_preserves_selected_agent(cx: &mut TestAppContext) { + init_test(cx); + + let app_state = cx.update(|cx| { + cx.update_flags(true, vec!["agent-v2".to_string()]); + agent::ThreadStore::init_global(cx); + language_model::LanguageModelRegistry::test(cx); + + let app_state = workspace::AppState::test(cx); + workspace::init(app_state.clone(), cx); + app_state + }); + + let fs = app_state.fs.as_fake(); + fs.insert_tree( + "/project", + json!({ + ".git": {}, + "src": { + "main.rs": "fn main() {}" + } + }), + ) + .await; + fs.set_branch_name(Path::new("/project/.git"), Some("main")); + + let project = Project::test(app_state.fs.clone(), [Path::new("/project")], cx).await; + + let multi_workspace = + cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + + let workspace = multi_workspace + .read_with(cx, |multi_workspace, _cx| { + multi_workspace.workspace().clone() + }) + .unwrap(); + + workspace.update(cx, |workspace, _cx| { + workspace.set_random_database_id(); + }); + + // Register a callback so new workspaces also get an AgentPanel. + cx.update(|cx| { + cx.observe_new( + |workspace: &mut Workspace, + window: Option<&mut Window>, + cx: &mut Context| { + if let Some(window) = window { + let project = workspace.project().clone(); + let text_thread_store = + cx.new(|cx| TextThreadStore::fake(project.clone(), cx)); + let panel = cx.new(|cx| { + AgentPanel::new(workspace, text_thread_store, None, window, cx) + }); + workspace.add_panel(panel, window, cx); + } + }, + ) + .detach(); + }); + + let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx); + + // Wait for the project to discover the git repository. + cx.run_until_parked(); + + let panel = workspace.update_in(cx, |workspace, window, cx| { + let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx)); + let panel = + cx.new(|cx| AgentPanel::new(workspace, text_thread_store, None, window, cx)); + workspace.add_panel(panel.clone(), window, cx); + panel + }); + + cx.run_until_parked(); + + // Open a thread (needed so there's an active thread view). + panel.update_in(cx, |panel, window, cx| { + panel.open_external_thread_with_server( + Rc::new(StubAgentServer::default_response()), + window, + cx, + ); + }); + + cx.run_until_parked(); + + // Set the selected agent to Codex (a custom agent) and start_thread_in + // to NewWorktree. We do this AFTER opening the thread because + // open_external_thread_with_server overrides selected_agent_type. + panel.update(cx, |panel, cx| { + panel.selected_agent_type = AgentType::Custom { + name: CODEX_NAME.into(), + }; + panel.set_start_thread_in(&StartThreadIn::NewWorktree, cx); + }); + + // Verify the panel has the Codex agent selected. + panel.read_with(cx, |panel, _cx| { + assert_eq!( + panel.selected_agent_type, + AgentType::Custom { + name: CODEX_NAME.into() + }, + ); + }); + + // Directly call handle_worktree_creation_requested, which is what + // handle_first_send_requested does when start_thread_in == NewWorktree. + let content = vec![acp::ContentBlock::Text(acp::TextContent::new( + "Hello from test", + ))]; + panel.update_in(cx, |panel, window, cx| { + panel.handle_worktree_creation_requested(content, window, cx); + }); + + // Let the async worktree creation + workspace setup complete. + cx.run_until_parked(); + + // Find the new workspace's AgentPanel and verify it used the Codex agent. + let found_codex = multi_workspace + .read_with(cx, |multi_workspace, cx| { + // There should be more than one workspace now (the original + the new worktree). + assert!( + multi_workspace.workspaces().len() > 1, + "expected a new workspace to have been created, found {}", + multi_workspace.workspaces().len(), + ); + + // Check the newest workspace's panel for the correct agent. + let new_workspace = multi_workspace + .workspaces() + .iter() + .find(|ws| ws.entity_id() != workspace.entity_id()) + .expect("should find the new workspace"); + let new_panel = new_workspace + .read(cx) + .panel::(cx) + .expect("new workspace should have an AgentPanel"); + + new_panel.read(cx).selected_agent_type.clone() + }) + .unwrap(); + + assert_eq!( + found_codex, + AgentType::Custom { + name: CODEX_NAME.into() + }, + "the new worktree workspace should use the same agent (Codex) that was selected in the original panel", + ); + } + #[test] fn test_deserialize_legacy_serialized_panel() { let json = serde_json::json!({