Allow dragging files and tabs into the agent panel (#29959)

Max Brunsfeld and Michael Sloan created

Release Notes:

- Added the ability to drag files and tabs onto the new agent panel.

---------

Co-authored-by: Michael Sloan <mgsloan@gmail.com>

Change summary

crates/agent/src/active_thread.rs                     |   4 
crates/agent/src/assistant_panel.rs                   | 136 +++++++++++-
crates/assistant/src/assistant_panel.rs               |  19 -
crates/assistant_context_editor/src/context_editor.rs |  90 ++++---
4 files changed, 181 insertions(+), 68 deletions(-)

Detailed changes

crates/agent/src/active_thread.rs 🔗

@@ -858,6 +858,10 @@ impl ActiveThread {
             .map(|(id, state)| (*id, state.last_estimated_token_count.unwrap_or(0)))
     }
 
+    pub fn context_store(&self) -> &Entity<ContextStore> {
+        &self.context_store
+    }
+
     pub fn thread_store(&self) -> &Entity<ThreadStore> {
         &self.thread_store
     }

crates/agent/src/assistant_panel.rs 🔗

@@ -22,18 +22,18 @@ use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
 use fs::Fs;
 use gpui::{
     Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, ClipboardItem,
-    Corner, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, KeyContext,
-    Pixels, Subscription, Task, UpdateGlobal, WeakEntity, linear_color_stop, linear_gradient,
-    prelude::*, pulsating_between,
+    Corner, DismissEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, FontWeight,
+    KeyContext, Pixels, Subscription, Task, UpdateGlobal, WeakEntity, linear_color_stop,
+    linear_gradient, prelude::*, pulsating_between,
 };
 use language::LanguageRegistry;
 use language_model::{LanguageModelProviderTosView, LanguageModelRegistry, RequestUsage};
 use language_model_selector::ToggleModelSelector;
-use project::Project;
+use project::{Project, ProjectPath, Worktree};
 use prompt_store::{PromptBuilder, PromptStore, UserPromptId};
 use proto::Plan;
 use rules_library::{RulesLibrary, open_rules_library};
-use search::{BufferSearchBar, buffer_search::DivRegistrar};
+use search::{BufferSearchBar, buffer_search};
 use settings::{Settings, update_settings_file};
 use theme::ThemeSettings;
 use time::UtcOffset;
@@ -43,7 +43,7 @@ use ui::{
 };
 use util::{ResultExt as _, maybe};
 use workspace::dock::{DockPosition, Panel, PanelEvent};
-use workspace::{CollaboratorId, ToolbarItemView, Workspace};
+use workspace::{CollaboratorId, DraggedSelection, DraggedTab, ToolbarItemView, Workspace};
 use zed_actions::agent::OpenConfiguration;
 use zed_actions::assistant::{OpenRulesLibrary, ToggleFocus};
 use zed_actions::{DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize};
@@ -2570,6 +2570,108 @@ impl AssistantPanel {
             .into_any()
     }
 
+    fn render_drag_target(&self, cx: &Context<Self>) -> Div {
+        let is_local = self.project.read(cx).is_local();
+        div()
+            .invisible()
+            .absolute()
+            .top_0()
+            .right_0()
+            .bottom_0()
+            .left_0()
+            .bg(cx.theme().colors().drop_target_background)
+            .drag_over::<DraggedTab>(|this, _, _, _| this.visible())
+            .drag_over::<DraggedSelection>(|this, _, _, _| this.visible())
+            .when(is_local, |this| {
+                this.drag_over::<ExternalPaths>(|this, _, _, _| this.visible())
+            })
+            .on_drop(cx.listener(move |this, tab: &DraggedTab, window, cx| {
+                let item = tab.pane.read(cx).item_for_index(tab.ix);
+                let project_paths = item
+                    .and_then(|item| item.project_path(cx))
+                    .into_iter()
+                    .collect::<Vec<_>>();
+                this.handle_drop(project_paths, vec![], window, cx);
+            }))
+            .on_drop(
+                cx.listener(move |this, selection: &DraggedSelection, window, cx| {
+                    let project_paths = selection
+                        .items()
+                        .filter_map(|item| this.project.read(cx).path_for_entry(item.entry_id, cx))
+                        .collect::<Vec<_>>();
+                    this.handle_drop(project_paths, vec![], window, cx);
+                }),
+            )
+            .on_drop(cx.listener(move |this, paths: &ExternalPaths, window, cx| {
+                let tasks = paths
+                    .paths()
+                    .into_iter()
+                    .map(|path| {
+                        Workspace::project_path_for_path(this.project.clone(), &path, false, cx)
+                    })
+                    .collect::<Vec<_>>();
+                cx.spawn_in(window, async move |this, cx| {
+                    let mut paths = vec![];
+                    let mut added_worktrees = vec![];
+                    let opened_paths = futures::future::join_all(tasks).await;
+                    for entry in opened_paths {
+                        if let Some((worktree, project_path)) = entry.log_err() {
+                            added_worktrees.push(worktree);
+                            paths.push(project_path);
+                        }
+                    }
+                    this.update_in(cx, |this, window, cx| {
+                        this.handle_drop(paths, added_worktrees, window, cx);
+                    })
+                    .ok();
+                })
+                .detach();
+            }))
+    }
+
+    fn handle_drop(
+        &mut self,
+        paths: Vec<ProjectPath>,
+        added_worktrees: Vec<Entity<Worktree>>,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        match &self.active_view {
+            ActiveView::Thread { .. } => {
+                let context_store = self.thread.read(cx).context_store().clone();
+                context_store.update(cx, move |context_store, cx| {
+                    let mut tasks = Vec::new();
+                    for project_path in &paths {
+                        tasks.push(context_store.add_file_from_path(
+                            project_path.clone(),
+                            false,
+                            cx,
+                        ));
+                    }
+                    cx.background_spawn(async move {
+                        futures::future::join_all(tasks).await;
+                        // Need to hold onto the worktrees until they have already been used when
+                        // opening the buffers.
+                        drop(added_worktrees);
+                    })
+                    .detach();
+                });
+            }
+            ActiveView::PromptEditor { context_editor, .. } => {
+                context_editor.update(cx, |context_editor, cx| {
+                    ContextEditor::insert_dragged_files(
+                        context_editor,
+                        paths,
+                        added_worktrees,
+                        window,
+                        cx,
+                    );
+                });
+            }
+            ActiveView::History | ActiveView::Configuration => {}
+        }
+    }
+
     fn create_copy_button(&self, message: impl Into<String>) -> impl IntoElement {
         let message = message.into();
         IconButton::new("copy", IconName::Copy)
@@ -2617,18 +2719,24 @@ impl Render for AssistantPanel {
             .child(self.render_toolbar(window, cx))
             .children(self.render_trial_upsell(window, cx))
             .map(|parent| match &self.active_view {
-                ActiveView::Thread { .. } => parent
-                    .child(self.render_active_thread_or_empty_state(window, cx))
-                    .children(self.render_tool_use_limit_reached(cx))
-                    .child(h_flex().child(self.message_editor.clone()))
-                    .children(self.render_last_error(cx)),
+                ActiveView::Thread { .. } => parent.child(
+                    v_flex()
+                        .relative()
+                        .justify_between()
+                        .size_full()
+                        .child(self.render_active_thread_or_empty_state(window, cx))
+                        .children(self.render_tool_use_limit_reached(cx))
+                        .child(h_flex().child(self.message_editor.clone()))
+                        .children(self.render_last_error(cx))
+                        .child(self.render_drag_target(cx)),
+                ),
                 ActiveView::History => parent.child(self.history.clone()),
                 ActiveView::PromptEditor {
                     context_editor,
                     buffer_search_bar,
                     ..
                 } => {
-                    let mut registrar = DivRegistrar::new(
+                    let mut registrar = buffer_search::DivRegistrar::new(
                         |this, _, _cx| match &this.active_view {
                             ActiveView::PromptEditor {
                                 buffer_search_bar, ..
@@ -2642,6 +2750,7 @@ impl Render for AssistantPanel {
                         registrar
                             .into_div()
                             .size_full()
+                            .relative()
                             .map(|parent| {
                                 buffer_search_bar.update(cx, |buffer_search_bar, cx| {
                                     if buffer_search_bar.is_dismissed() {
@@ -2657,7 +2766,8 @@ impl Render for AssistantPanel {
                                     )
                                 })
                             })
-                            .child(context_editor.clone()),
+                            .child(context_editor.clone())
+                            .child(self.render_drag_target(cx)),
                     )
                 }
                 ActiveView::Configuration => parent.children(self.configuration.clone()),

crates/assistant/src/assistant_panel.rs 🔗

@@ -34,7 +34,7 @@ use smol::stream::StreamExt;
 
 use std::ops::Range;
 use std::path::Path;
-use std::{ops::ControlFlow, path::PathBuf, sync::Arc};
+use std::{ops::ControlFlow, sync::Arc};
 use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
 use ui::{ContextMenu, PopoverMenu, Tooltip, prelude::*};
 use util::{ResultExt, maybe};
@@ -54,7 +54,7 @@ pub fn init(cx: &mut App) {
                 .register_action(ContextEditor::quote_selection)
                 .register_action(ContextEditor::insert_selection)
                 .register_action(ContextEditor::copy_code)
-                .register_action(ContextEditor::insert_dragged_files)
+                .register_action(ContextEditor::handle_insert_dragged_files)
                 .register_action(AssistantPanel::show_configuration)
                 .register_action(AssistantPanel::create_new_context)
                 .register_action(AssistantPanel::restart_context_servers)
@@ -182,20 +182,7 @@ impl AssistantPanel {
                         None
                     }?;
 
-                    let paths = project_paths
-                        .into_iter()
-                        .filter_map(|project_path| {
-                            let worktree = project
-                                .read(cx)
-                                .worktree_for_id(project_path.worktree_id, cx)?;
-
-                            let mut full_path = PathBuf::from(worktree.read(cx).root_name());
-                            full_path.push(&project_path.path);
-                            Some(full_path)
-                        })
-                        .collect::<Vec<_>>();
-
-                    Some(InsertDraggedFiles::ProjectPaths(paths))
+                    Some(InsertDraggedFiles::ProjectPaths(project_paths))
                 });
 
                 if let Some(action) = action {

crates/assistant_context_editor/src/context_editor.rs 🔗

@@ -43,8 +43,8 @@ use language_model_selector::{
 };
 use multi_buffer::MultiBufferRow;
 use picker::Picker;
-use project::lsp_store::LocalLspAdapterDelegate;
 use project::{Project, Worktree};
+use project::{ProjectPath, lsp_store::LocalLspAdapterDelegate};
 use rope::Point;
 use serde::{Deserialize, Serialize};
 use settings::{Settings, SettingsStore, update_settings_file};
@@ -100,7 +100,7 @@ actions!(
 
 #[derive(PartialEq, Clone)]
 pub enum InsertDraggedFiles {
-    ProjectPaths(Vec<PathBuf>),
+    ProjectPaths(Vec<ProjectPath>),
     ExternalFiles(Vec<PathBuf>),
 }
 
@@ -1725,7 +1725,7 @@ impl ContextEditor {
         );
     }
 
-    pub fn insert_dragged_files(
+    pub fn handle_insert_dragged_files(
         workspace: &mut Workspace,
         action: &InsertDraggedFiles,
         window: &mut Window,
@@ -1740,7 +1740,7 @@ impl ContextEditor {
             return;
         };
 
-        let project = workspace.project().clone();
+        let project = context_editor_view.read(cx).project.clone();
 
         let paths = match action {
             InsertDraggedFiles::ProjectPaths(paths) => Task::ready((paths.clone(), vec![])),
@@ -1751,22 +1751,17 @@ impl ContextEditor {
                     .map(|path| Workspace::project_path_for_path(project.clone(), &path, false, cx))
                     .collect::<Vec<_>>();
 
-                cx.spawn(async move |_, cx| {
+                cx.background_spawn(async move {
                     let mut paths = vec![];
                     let mut worktrees = vec![];
 
                     let opened_paths = futures::future::join_all(tasks).await;
-                    for (worktree, project_path) in opened_paths.into_iter().flatten() {
-                        let Ok(worktree_root_name) =
-                            worktree.read_with(cx, |worktree, _| worktree.root_name().to_string())
-                        else {
-                            continue;
-                        };
 
-                        let mut full_path = PathBuf::from(worktree_root_name.clone());
-                        full_path.push(&project_path.path);
-                        paths.push(full_path);
-                        worktrees.push(worktree);
+                    for entry in opened_paths {
+                        if let Some((worktree, project_path)) = entry.log_err() {
+                            worktrees.push(worktree);
+                            paths.push(project_path);
+                        }
                     }
 
                     (paths, worktrees)
@@ -1774,33 +1769,50 @@ impl ContextEditor {
             }
         };
 
-        window
-            .spawn(cx, async move |cx| {
+        context_editor_view.update(cx, |_, cx| {
+            cx.spawn_in(window, async move |this, cx| {
                 let (paths, dragged_file_worktrees) = paths.await;
-                let cmd_name = FileSlashCommand.name();
-
-                context_editor_view
-                    .update_in(cx, |context_editor, window, cx| {
-                        let file_argument = paths
-                            .into_iter()
-                            .map(|path| path.to_string_lossy().to_string())
-                            .collect::<Vec<_>>()
-                            .join(" ");
-
-                        context_editor.editor.update(cx, |editor, cx| {
-                            editor.insert("\n", window, cx);
-                            editor.insert(&format!("/{} {}", cmd_name, file_argument), window, cx);
-                        });
-
-                        context_editor.confirm_command(&ConfirmCommand, window, cx);
-
-                        context_editor
-                            .dragged_file_worktrees
-                            .extend(dragged_file_worktrees);
-                    })
-                    .log_err();
+                this.update_in(cx, |this, window, cx| {
+                    this.insert_dragged_files(paths, dragged_file_worktrees, window, cx);
+                })
+                .ok();
             })
             .detach();
+        })
+    }
+
+    pub fn insert_dragged_files(
+        &mut self,
+        opened_paths: Vec<ProjectPath>,
+        added_worktrees: Vec<Entity<Worktree>>,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let mut file_slash_command_args = vec![];
+        for project_path in opened_paths.into_iter() {
+            let Some(worktree) = self
+                .project
+                .read(cx)
+                .worktree_for_id(project_path.worktree_id, cx)
+            else {
+                continue;
+            };
+            let worktree_root_name = worktree.read(cx).root_name().to_string();
+            let mut full_path = PathBuf::from(worktree_root_name.clone());
+            full_path.push(&project_path.path);
+            file_slash_command_args.push(full_path.to_string_lossy().to_string());
+        }
+
+        let cmd_name = FileSlashCommand.name();
+
+        let file_argument = file_slash_command_args.join(" ");
+
+        self.editor.update(cx, |editor, cx| {
+            editor.insert("\n", window, cx);
+            editor.insert(&format!("/{} {}", cmd_name, file_argument), window, cx);
+        });
+        self.confirm_command(&ConfirmCommand, window, cx);
+        self.dragged_file_worktrees.extend(added_worktrees);
     }
 
     pub fn quote_selection(