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

Release Notes:

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

---------

Co-authored-by: Michael Sloan <mgsloan@gmail.com>
This commit is contained in:
Max Brunsfeld 2025-05-05 17:21:22 -07:00 committed by GitHub
parent b214c9e4a8
commit 275c808b03
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 181 additions and 68 deletions

View file

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

View file

@ -22,18 +22,18 @@ use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
use fs::Fs; use fs::Fs;
use gpui::{ use gpui::{
Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, ClipboardItem, Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, ClipboardItem,
Corner, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, KeyContext, Corner, DismissEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, FontWeight,
Pixels, Subscription, Task, UpdateGlobal, WeakEntity, linear_color_stop, linear_gradient, KeyContext, Pixels, Subscription, Task, UpdateGlobal, WeakEntity, linear_color_stop,
prelude::*, pulsating_between, linear_gradient, prelude::*, pulsating_between,
}; };
use language::LanguageRegistry; use language::LanguageRegistry;
use language_model::{LanguageModelProviderTosView, LanguageModelRegistry, RequestUsage}; use language_model::{LanguageModelProviderTosView, LanguageModelRegistry, RequestUsage};
use language_model_selector::ToggleModelSelector; use language_model_selector::ToggleModelSelector;
use project::Project; use project::{Project, ProjectPath, Worktree};
use prompt_store::{PromptBuilder, PromptStore, UserPromptId}; use prompt_store::{PromptBuilder, PromptStore, UserPromptId};
use proto::Plan; use proto::Plan;
use rules_library::{RulesLibrary, open_rules_library}; 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 settings::{Settings, update_settings_file};
use theme::ThemeSettings; use theme::ThemeSettings;
use time::UtcOffset; use time::UtcOffset;
@ -43,7 +43,7 @@ use ui::{
}; };
use util::{ResultExt as _, maybe}; use util::{ResultExt as _, maybe};
use workspace::dock::{DockPosition, Panel, PanelEvent}; 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::agent::OpenConfiguration;
use zed_actions::assistant::{OpenRulesLibrary, ToggleFocus}; use zed_actions::assistant::{OpenRulesLibrary, ToggleFocus};
use zed_actions::{DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize}; use zed_actions::{DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize};
@ -2570,6 +2570,108 @@ impl AssistantPanel {
.into_any() .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 { fn create_copy_button(&self, message: impl Into<String>) -> impl IntoElement {
let message = message.into(); let message = message.into();
IconButton::new("copy", IconName::Copy) IconButton::new("copy", IconName::Copy)
@ -2617,18 +2719,24 @@ impl Render for AssistantPanel {
.child(self.render_toolbar(window, cx)) .child(self.render_toolbar(window, cx))
.children(self.render_trial_upsell(window, cx)) .children(self.render_trial_upsell(window, cx))
.map(|parent| match &self.active_view { .map(|parent| match &self.active_view {
ActiveView::Thread { .. } => parent ActiveView::Thread { .. } => parent.child(
.child(self.render_active_thread_or_empty_state(window, cx)) v_flex()
.children(self.render_tool_use_limit_reached(cx)) .relative()
.child(h_flex().child(self.message_editor.clone())) .justify_between()
.children(self.render_last_error(cx)), .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::History => parent.child(self.history.clone()),
ActiveView::PromptEditor { ActiveView::PromptEditor {
context_editor, context_editor,
buffer_search_bar, buffer_search_bar,
.. ..
} => { } => {
let mut registrar = DivRegistrar::new( let mut registrar = buffer_search::DivRegistrar::new(
|this, _, _cx| match &this.active_view { |this, _, _cx| match &this.active_view {
ActiveView::PromptEditor { ActiveView::PromptEditor {
buffer_search_bar, .. buffer_search_bar, ..
@ -2642,6 +2750,7 @@ impl Render for AssistantPanel {
registrar registrar
.into_div() .into_div()
.size_full() .size_full()
.relative()
.map(|parent| { .map(|parent| {
buffer_search_bar.update(cx, |buffer_search_bar, cx| { buffer_search_bar.update(cx, |buffer_search_bar, cx| {
if buffer_search_bar.is_dismissed() { 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()), ActiveView::Configuration => parent.children(self.configuration.clone()),

View file

@ -34,7 +34,7 @@ use smol::stream::StreamExt;
use std::ops::Range; use std::ops::Range;
use std::path::Path; 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 terminal_view::{TerminalView, terminal_panel::TerminalPanel};
use ui::{ContextMenu, PopoverMenu, Tooltip, prelude::*}; use ui::{ContextMenu, PopoverMenu, Tooltip, prelude::*};
use util::{ResultExt, maybe}; use util::{ResultExt, maybe};
@ -54,7 +54,7 @@ pub fn init(cx: &mut App) {
.register_action(ContextEditor::quote_selection) .register_action(ContextEditor::quote_selection)
.register_action(ContextEditor::insert_selection) .register_action(ContextEditor::insert_selection)
.register_action(ContextEditor::copy_code) .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::show_configuration)
.register_action(AssistantPanel::create_new_context) .register_action(AssistantPanel::create_new_context)
.register_action(AssistantPanel::restart_context_servers) .register_action(AssistantPanel::restart_context_servers)
@ -182,20 +182,7 @@ impl AssistantPanel {
None None
}?; }?;
let paths = project_paths Some(InsertDraggedFiles::ProjectPaths(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))
}); });
if let Some(action) = action { if let Some(action) = action {

View file

@ -43,8 +43,8 @@ use language_model_selector::{
}; };
use multi_buffer::MultiBufferRow; use multi_buffer::MultiBufferRow;
use picker::Picker; use picker::Picker;
use project::lsp_store::LocalLspAdapterDelegate;
use project::{Project, Worktree}; use project::{Project, Worktree};
use project::{ProjectPath, lsp_store::LocalLspAdapterDelegate};
use rope::Point; use rope::Point;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsStore, update_settings_file}; use settings::{Settings, SettingsStore, update_settings_file};
@ -100,7 +100,7 @@ actions!(
#[derive(PartialEq, Clone)] #[derive(PartialEq, Clone)]
pub enum InsertDraggedFiles { pub enum InsertDraggedFiles {
ProjectPaths(Vec<PathBuf>), ProjectPaths(Vec<ProjectPath>),
ExternalFiles(Vec<PathBuf>), ExternalFiles(Vec<PathBuf>),
} }
@ -1725,7 +1725,7 @@ impl ContextEditor {
); );
} }
pub fn insert_dragged_files( pub fn handle_insert_dragged_files(
workspace: &mut Workspace, workspace: &mut Workspace,
action: &InsertDraggedFiles, action: &InsertDraggedFiles,
window: &mut Window, window: &mut Window,
@ -1740,7 +1740,7 @@ impl ContextEditor {
return; return;
}; };
let project = workspace.project().clone(); let project = context_editor_view.read(cx).project.clone();
let paths = match action { let paths = match action {
InsertDraggedFiles::ProjectPaths(paths) => Task::ready((paths.clone(), vec![])), 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)) .map(|path| Workspace::project_path_for_path(project.clone(), &path, false, cx))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
cx.spawn(async move |_, cx| { cx.background_spawn(async move {
let mut paths = vec![]; let mut paths = vec![];
let mut worktrees = vec![]; let mut worktrees = vec![];
let opened_paths = futures::future::join_all(tasks).await; 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()); for entry in opened_paths {
full_path.push(&project_path.path); if let Some((worktree, project_path)) = entry.log_err() {
paths.push(full_path); worktrees.push(worktree);
worktrees.push(worktree); paths.push(project_path);
}
} }
(paths, worktrees) (paths, worktrees)
@ -1774,33 +1769,50 @@ impl ContextEditor {
} }
}; };
window context_editor_view.update(cx, |_, cx| {
.spawn(cx, async move |cx| { cx.spawn_in(window, async move |this, cx| {
let (paths, dragged_file_worktrees) = paths.await; let (paths, dragged_file_worktrees) = paths.await;
let cmd_name = FileSlashCommand.name(); this.update_in(cx, |this, window, cx| {
this.insert_dragged_files(paths, dragged_file_worktrees, window, cx);
context_editor_view })
.update_in(cx, |context_editor, window, cx| { .ok();
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();
}) })
.detach(); .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( pub fn quote_selection(