mirror of
https://github.com/zed-industries/zed.git
synced 2026-05-31 19:05:00 +07:00
agent_ui: Paste dropped paths into agent terminals (#56686)
Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Release Notes: - N/A
This commit is contained in:
parent
23231879cd
commit
43585afc29
3 changed files with 269 additions and 27 deletions
|
|
@ -145,3 +145,4 @@ tempfile.workspace = true
|
|||
vim.workspace = true
|
||||
tree-sitter-md.workspace = true
|
||||
unindent.workspace = true
|
||||
terminal = { workspace = true, features = ["test-support"] }
|
||||
|
|
|
|||
|
|
@ -4905,32 +4905,79 @@ impl AgentPanel {
|
|||
}),
|
||||
)
|
||||
.on_drop(cx.listener(move |this, paths: &ExternalPaths, window, cx| {
|
||||
let tasks = paths
|
||||
.paths()
|
||||
.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();
|
||||
this.handle_external_paths_drop(paths, window, cx);
|
||||
}))
|
||||
}
|
||||
|
||||
fn handle_external_paths_drop(
|
||||
&mut self,
|
||||
paths: &ExternalPaths,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if matches!(&self.base_view, BaseView::Terminal { .. }) {
|
||||
// Terminal drops should match normal terminal views by pasting raw OS paths.
|
||||
// The agent-thread path below converts paths to project paths, which can add
|
||||
// worktrees and is only needed when attaching files to a conversation.
|
||||
self.paste_external_paths_into_active_terminal(paths, window, cx);
|
||||
return;
|
||||
}
|
||||
|
||||
let BaseView::AgentThread { conversation_view } = &self.base_view else {
|
||||
return;
|
||||
};
|
||||
let conversation_view = conversation_view.clone();
|
||||
let tasks = paths
|
||||
.paths()
|
||||
.iter()
|
||||
.map(|path| Workspace::project_path_for_path(self.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);
|
||||
}
|
||||
}
|
||||
conversation_view
|
||||
.update_in(cx, |conversation_view, window, cx| {
|
||||
conversation_view.insert_dragged_files(paths, added_worktrees, window, cx);
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn paste_external_paths_into_active_terminal(
|
||||
&mut self,
|
||||
paths: &ExternalPaths,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let BaseView::Terminal { terminal_id } = &self.base_view else {
|
||||
return;
|
||||
};
|
||||
|
||||
if !self.project.read(cx).is_local() {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(terminal_view) = self
|
||||
.terminals
|
||||
.get(terminal_id)
|
||||
.map(|terminal| terminal.view.clone())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
terminal_view.update(cx, |terminal_view, cx| {
|
||||
terminal_view.add_paths_to_terminal(paths.paths(), window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_drop(
|
||||
&mut self,
|
||||
paths: Vec<ProjectPath>,
|
||||
|
|
@ -4944,7 +4991,30 @@ impl AgentPanel {
|
|||
conversation_view.insert_dragged_files(paths, added_worktrees, window, cx);
|
||||
});
|
||||
}
|
||||
BaseView::Terminal { .. } | BaseView::Uninitialized => {}
|
||||
BaseView::Terminal { terminal_id } => {
|
||||
let paths = {
|
||||
let project = self.project.read(cx);
|
||||
paths
|
||||
.iter()
|
||||
.filter_map(|project_path| project.absolute_path(project_path, cx))
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
if paths.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(terminal_view) = self
|
||||
.terminals
|
||||
.get(terminal_id)
|
||||
.map(|terminal| terminal.view.clone())
|
||||
{
|
||||
terminal_view.update(cx, |terminal_view, cx| {
|
||||
terminal_view.add_paths_to_terminal(&paths, window, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
BaseView::Uninitialized => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -5003,7 +5073,9 @@ impl Render for AgentPanel {
|
|||
VisibleSurface::AgentThread(conversation_view) => parent
|
||||
.child(conversation_view.clone())
|
||||
.child(self.render_drag_target(cx)),
|
||||
VisibleSurface::Terminal(terminal_view) => parent.child(terminal_view.clone()),
|
||||
VisibleSurface::Terminal(terminal_view) => parent
|
||||
.child(terminal_view.clone())
|
||||
.child(self.render_drag_target(cx)),
|
||||
VisibleSurface::Configuration(configuration) => {
|
||||
parent.children(configuration.cloned())
|
||||
}
|
||||
|
|
@ -5279,7 +5351,7 @@ mod tests {
|
|||
use std::any::Any;
|
||||
|
||||
use serde_json::json;
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
|
||||
|
|
@ -6669,6 +6741,175 @@ mod tests {
|
|||
(panel, cx)
|
||||
}
|
||||
|
||||
fn expected_terminal_drop_text(paths: &[PathBuf]) -> String {
|
||||
let mut text = String::new();
|
||||
for path in paths {
|
||||
text.push(' ');
|
||||
text.push_str(&format!("{path:?}"));
|
||||
}
|
||||
text.push(' ');
|
||||
text
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_terminal_external_image_drop_writes_path(cx: &mut TestAppContext) {
|
||||
let (panel, mut cx) = setup_panel(cx).await;
|
||||
cx.update(|_, cx| {
|
||||
cx.update_flags(true, vec!["agent-panel-terminal".to_string()]);
|
||||
});
|
||||
|
||||
let terminal_id = panel
|
||||
.update_in(&mut cx, |panel, window, cx| {
|
||||
panel.insert_test_terminal("Image Upload", true, window, cx)
|
||||
})
|
||||
.expect("test terminal should be inserted");
|
||||
cx.run_until_parked();
|
||||
|
||||
let terminal = panel.read_with(&cx, |panel, cx| {
|
||||
panel
|
||||
.terminals
|
||||
.get(&terminal_id)
|
||||
.expect("terminal should remain in the panel")
|
||||
.view
|
||||
.read(cx)
|
||||
.terminal()
|
||||
.clone()
|
||||
});
|
||||
terminal.update(&mut cx, |terminal, _cx| terminal.take_input_log());
|
||||
|
||||
let image_path = PathBuf::from("/tmp/dropped-image.png");
|
||||
panel.update_in(&mut cx, |panel, window, cx| {
|
||||
let external_paths = ExternalPaths(vec![image_path.clone()].into());
|
||||
panel.paste_external_paths_into_active_terminal(&external_paths, window, cx);
|
||||
});
|
||||
|
||||
let mut input_log = terminal.update(&mut cx, |terminal, _cx| terminal.take_input_log());
|
||||
assert_eq!(input_log.len(), 1, "expected one write to the terminal");
|
||||
let written =
|
||||
String::from_utf8(input_log.remove(0)).expect("terminal write should be valid UTF-8");
|
||||
assert_eq!(
|
||||
written,
|
||||
expected_terminal_drop_text(std::slice::from_ref(&image_path))
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_terminal_external_paths_drop_handler_writes_image_path(cx: &mut TestAppContext) {
|
||||
let (panel, mut cx) = setup_panel(cx).await;
|
||||
cx.update(|_, cx| {
|
||||
cx.update_flags(true, vec!["agent-panel-terminal".to_string()]);
|
||||
});
|
||||
|
||||
let terminal_id = panel
|
||||
.update_in(&mut cx, |panel, window, cx| {
|
||||
panel.insert_test_terminal("Image Upload", true, window, cx)
|
||||
})
|
||||
.expect("test terminal should be inserted");
|
||||
cx.run_until_parked();
|
||||
|
||||
let terminal = panel.read_with(&cx, |panel, cx| {
|
||||
panel
|
||||
.terminals
|
||||
.get(&terminal_id)
|
||||
.expect("terminal should remain in the panel")
|
||||
.view
|
||||
.read(cx)
|
||||
.terminal()
|
||||
.clone()
|
||||
});
|
||||
terminal.update(&mut cx, |terminal, _cx| terminal.take_input_log());
|
||||
|
||||
let image_path = PathBuf::from("/tmp/dropped-image.png");
|
||||
panel.update_in(&mut cx, |panel, window, cx| {
|
||||
let external_paths = ExternalPaths(vec![image_path.clone()].into());
|
||||
panel.handle_external_paths_drop(&external_paths, window, cx);
|
||||
});
|
||||
|
||||
let mut input_log = terminal.update(&mut cx, |terminal, _cx| terminal.take_input_log());
|
||||
assert_eq!(input_log.len(), 1, "expected one write to the terminal");
|
||||
let written =
|
||||
String::from_utf8(input_log.remove(0)).expect("terminal write should be valid UTF-8");
|
||||
assert_eq!(
|
||||
written,
|
||||
expected_terminal_drop_text(std::slice::from_ref(&image_path))
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_external_file_drop_on_thread_does_not_paste_into_later_terminal(
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
init_test(cx);
|
||||
cx.update(|cx| {
|
||||
agent::ThreadStore::init_global(cx);
|
||||
language_model::LanguageModelRegistry::test(cx);
|
||||
cx.update_flags(true, vec!["agent-panel-terminal".to_string()]);
|
||||
});
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
cx.update(|cx| <dyn fs::Fs>::set_global(fs.clone(), cx));
|
||||
fs.insert_tree("/project", json!({ "file.txt": "content" }))
|
||||
.await;
|
||||
let project = Project::test(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();
|
||||
let mut cx = VisualTestContext::from_window(multi_workspace.into(), cx);
|
||||
|
||||
let panel = workspace.update_in(&mut cx, |workspace, window, cx| {
|
||||
let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
|
||||
workspace.add_panel(panel.clone(), window, cx);
|
||||
panel
|
||||
});
|
||||
open_thread_with_connection(&panel, StubAgentConnection::new(), &mut cx);
|
||||
let thread_id = active_thread_id(&panel, &cx);
|
||||
|
||||
let file_path = PathBuf::from("/project/file.txt");
|
||||
panel.update_in(&mut cx, |panel, window, cx| {
|
||||
let external_paths = ExternalPaths(vec![file_path.clone()].into());
|
||||
panel.handle_external_paths_drop(&external_paths, window, cx);
|
||||
});
|
||||
|
||||
let terminal_id = panel
|
||||
.update_in(&mut cx, |panel, window, cx| {
|
||||
panel.insert_test_terminal("Drop Target", true, window, cx)
|
||||
})
|
||||
.expect("test terminal should be inserted");
|
||||
let terminal = panel.read_with(&cx, |panel, cx| {
|
||||
panel
|
||||
.terminals
|
||||
.get(&terminal_id)
|
||||
.expect("terminal should remain in the panel")
|
||||
.view
|
||||
.read(cx)
|
||||
.terminal()
|
||||
.clone()
|
||||
});
|
||||
terminal.update(&mut cx, |terminal, _cx| terminal.take_input_log());
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
let input_log = terminal.update(&mut cx, |terminal, _cx| terminal.take_input_log());
|
||||
assert!(
|
||||
input_log.is_empty(),
|
||||
"thread drop completion should not write to the active terminal"
|
||||
);
|
||||
|
||||
let expected_uri = MentionUri::File {
|
||||
abs_path: file_path,
|
||||
}
|
||||
.to_uri()
|
||||
.to_string();
|
||||
let expected_text = format!("[@file.txt]({expected_uri}) ");
|
||||
let actual_text = panel.read_with(&cx, |panel, cx| panel.editor_text(thread_id, cx));
|
||||
assert_eq!(actual_text.as_deref(), Some(expected_text.as_str()));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_terminal_entry_kind_controls_new_entry(cx: &mut TestAppContext) {
|
||||
let (panel, mut cx) = setup_panel(cx).await;
|
||||
|
|
|
|||
|
|
@ -855,7 +855,7 @@ impl TerminalView {
|
|||
});
|
||||
}
|
||||
|
||||
fn add_paths_to_terminal(&self, paths: &[PathBuf], window: &mut Window, cx: &mut App) {
|
||||
pub fn add_paths_to_terminal(&self, paths: &[PathBuf], window: &mut Window, cx: &mut App) {
|
||||
let mut text = paths.iter().map(|path| format!(" {path:?}")).join("");
|
||||
text.push(' ');
|
||||
window.focus(&self.focus_handle(cx), cx);
|
||||
|
|
|
|||
Loading…
Reference in a new issue