Simplify New Worktree to only restore active buffer or fullscreen agent panel (#51700)

Richard Feldman created

Dramatically simplifies what "New Worktree" does when forking a
workspace.

Previously we captured the entire dock structure and all open file
paths, then restored them all in the new worktree workspace. Now the
behavior is:

- If you have an active buffer open, we reopen just that one file (no
other buffer state is carried over).
- If you do not have an active buffer open, we instead fullscreen (zoom)
the agent panel (equivalent to cmd-esc).

No other workspace state is carried over.

Implementation details:
- Replace `capture_dock_state` / `set_dock_structure` /
`open_item_abs_paths` with a single `active_file_path` lookup.
- Pass `None` for the `init` closure instead of restoring dock layout.
- Only reopen the single active file via `open_paths`, with proper error
logging (`detach_and_log_err`) instead of silent `.detach()`.
- Emit `PanelEvent::ZoomIn` on the agent panel when no active buffer
exists, after `focus_panel` to ensure the panel is properly activated
first.
- Warn when an active file exists but cannot be remapped to the new
worktree.

Closes AI-87

Release Notes:

- N/A

Change summary

crates/agent_ui/src/agent_panel.rs | 113 ++++++++++++++++++-------------
1 file changed, 64 insertions(+), 49 deletions(-)

Detailed changes

crates/agent_ui/src/agent_panel.rs 🔗

@@ -2894,15 +2894,15 @@ 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
@@ -3004,8 +3004,7 @@ impl AgentPanel {
                 all_paths,
                 app_state,
                 window_handle,
-                dock_structure,
-                open_file_paths,
+                active_file_path,
                 path_remapping,
                 non_git_paths,
                 has_non_git,
@@ -3038,8 +3037,7 @@ impl AgentPanel {
         all_paths: Vec<PathBuf>,
         app_state: Arc<workspace::AppState>,
         window_handle: Option<gpui::WindowHandle<workspace::MultiWorkspace>>,
-        dock_structure: workspace::DockStructure,
-        open_file_paths: Vec<PathBuf>,
+        active_file_path: Option<PathBuf>,
         path_remapping: Vec<(PathBuf, PathBuf)>,
         non_git_paths: Vec<PathBuf>,
         has_non_git: bool,
@@ -3047,19 +3045,13 @@ impl AgentPanel {
         selected_agent: Option<Agent>,
         cx: &mut AsyncWindowContext,
     ) -> Result<()> {
-        let init: Option<
-            Box<dyn FnOnce(&mut Workspace, &mut Window, &mut gpui::Context<Workspace>) + 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?;
 
@@ -3089,44 +3081,67 @@ impl AgentPanel {
                     );
                 }
 
-                let remapped_paths: Vec<PathBuf> = 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_panel::<AgentPanel>(window, cx);
+
+                // If no active buffer was open, zoom the agent panel
+                // (equivalent to cmd-esc fullscreen behavior).
+                // This must happen after focus_panel, which activates
+                // and opens the panel in the dock.
+                if should_zoom_agent_panel {
+                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
+                        panel.update(cx, |_panel, cx| {
+                            cx.emit(PanelEvent::ZoomIn);
+                        });
+                    }
+                }
                 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
                     panel.update(cx, |panel, cx| {
                         panel.external_thread(