Allow edit tool to access files outside project (with confirmation) (#35221)
Now the edit tool can access files outside the current project (just like the terminal tool can), but it's behind a prompt (unlike other edit tool actions). Release Notes: - The edit tool can now access files outside the current project, but only if the user grants it permission to.
This commit is contained in:
parent
916eb996bc
commit
e2b863116d
23 changed files with 618 additions and 26 deletions
|
@ -308,7 +308,12 @@ mod tests {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn needs_confirmation(&self, _input: &serde_json::Value, _cx: &App) -> bool {
|
fn needs_confirmation(
|
||||||
|
&self,
|
||||||
|
_input: &serde_json::Value,
|
||||||
|
_project: &Entity<Project>,
|
||||||
|
_cx: &App,
|
||||||
|
) -> bool {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -47,7 +47,7 @@ impl Tool for ContextServerTool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -942,7 +942,7 @@ impl Thread {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tool_uses_for_message(&self, id: MessageId, cx: &App) -> Vec<ToolUse> {
|
pub fn tool_uses_for_message(&self, id: MessageId, cx: &App) -> Vec<ToolUse> {
|
||||||
self.tool_use.tool_uses_for_message(id, cx)
|
self.tool_use.tool_uses_for_message(id, &self.project, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tool_results_for_message(
|
pub fn tool_results_for_message(
|
||||||
|
@ -2557,7 +2557,7 @@ impl Thread {
|
||||||
return self.handle_hallucinated_tool_use(tool_use.id, tool_use.name, window, cx);
|
return self.handle_hallucinated_tool_use(tool_use.id, tool_use.name, window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
if tool.needs_confirmation(&tool_use.input, cx)
|
if tool.needs_confirmation(&tool_use.input, &self.project, cx)
|
||||||
&& !AgentSettings::get_global(cx).always_allow_tool_actions
|
&& !AgentSettings::get_global(cx).always_allow_tool_actions
|
||||||
{
|
{
|
||||||
self.tool_use.confirm_tool_use(
|
self.tool_use.confirm_tool_use(
|
||||||
|
|
|
@ -165,7 +165,12 @@ impl ToolUseState {
|
||||||
self.pending_tool_uses_by_id.values().collect()
|
self.pending_tool_uses_by_id.values().collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tool_uses_for_message(&self, id: MessageId, cx: &App) -> Vec<ToolUse> {
|
pub fn tool_uses_for_message(
|
||||||
|
&self,
|
||||||
|
id: MessageId,
|
||||||
|
project: &Entity<Project>,
|
||||||
|
cx: &App,
|
||||||
|
) -> Vec<ToolUse> {
|
||||||
let Some(tool_uses_for_message) = &self.tool_uses_by_assistant_message.get(&id) else {
|
let Some(tool_uses_for_message) = &self.tool_uses_by_assistant_message.get(&id) else {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
};
|
};
|
||||||
|
@ -211,7 +216,10 @@ impl ToolUseState {
|
||||||
|
|
||||||
let (icon, needs_confirmation) =
|
let (icon, needs_confirmation) =
|
||||||
if let Some(tool) = self.tools.read(cx).tool(&tool_use.name, cx) {
|
if let Some(tool) = self.tools.read(cx).tool(&tool_use.name, cx) {
|
||||||
(tool.icon(), tool.needs_confirmation(&tool_use.input, cx))
|
(
|
||||||
|
tool.icon(),
|
||||||
|
tool.needs_confirmation(&tool_use.input, project, cx),
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
(IconName::Cog, false)
|
(IconName::Cog, false)
|
||||||
};
|
};
|
||||||
|
|
|
@ -216,7 +216,12 @@ pub trait Tool: 'static + Send + Sync {
|
||||||
|
|
||||||
/// Returns true if the tool needs the users's confirmation
|
/// Returns true if the tool needs the users's confirmation
|
||||||
/// before having permission to run.
|
/// before having permission to run.
|
||||||
fn needs_confirmation(&self, input: &serde_json::Value, cx: &App) -> bool;
|
fn needs_confirmation(
|
||||||
|
&self,
|
||||||
|
input: &serde_json::Value,
|
||||||
|
project: &Entity<Project>,
|
||||||
|
cx: &App,
|
||||||
|
) -> bool;
|
||||||
|
|
||||||
/// Returns true if the tool may perform edits.
|
/// Returns true if the tool may perform edits.
|
||||||
fn may_perform_edits(&self) -> bool;
|
fn may_perform_edits(&self) -> bool;
|
||||||
|
|
|
@ -375,7 +375,12 @@ mod tests {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn needs_confirmation(&self, _input: &serde_json::Value, _cx: &App) -> bool {
|
fn needs_confirmation(
|
||||||
|
&self,
|
||||||
|
_input: &serde_json::Value,
|
||||||
|
_project: &Entity<Project>,
|
||||||
|
_cx: &App,
|
||||||
|
) -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -44,7 +44,7 @@ impl Tool for CopyPathTool {
|
||||||
"copy_path".into()
|
"copy_path".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -37,7 +37,7 @@ impl Tool for CreateDirectoryTool {
|
||||||
include_str!("./create_directory_tool/description.md").into()
|
include_str!("./create_directory_tool/description.md").into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -33,7 +33,7 @@ impl Tool for DeletePathTool {
|
||||||
"delete_path".into()
|
"delete_path".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -46,7 +46,7 @@ impl Tool for DiagnosticsTool {
|
||||||
"diagnostics".into()
|
"diagnostics".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -126,7 +126,41 @@ impl Tool for EditFileTool {
|
||||||
"edit_file".into()
|
"edit_file".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
fn needs_confirmation(
|
||||||
|
&self,
|
||||||
|
input: &serde_json::Value,
|
||||||
|
project: &Entity<Project>,
|
||||||
|
cx: &App,
|
||||||
|
) -> bool {
|
||||||
|
if agent_settings::AgentSettings::get_global(cx).always_allow_tool_actions {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Ok(input) = serde_json::from_value::<EditFileToolInput>(input.clone()) else {
|
||||||
|
// If it's not valid JSON, it's going to error and confirming won't do anything.
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
let path = Path::new(&input.path);
|
||||||
|
|
||||||
|
// If any path component is ".zed", then this could affect
|
||||||
|
// the editor in ways beyond the project source, so prompt.
|
||||||
|
if path
|
||||||
|
.components()
|
||||||
|
.any(|component| component.as_os_str() == ".zed")
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the path is outside the project, then prompt.
|
||||||
|
let is_outside_project = project
|
||||||
|
.read(cx)
|
||||||
|
.find_project_path(&input.path, cx)
|
||||||
|
.is_none();
|
||||||
|
if is_outside_project {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -148,7 +182,17 @@ impl Tool for EditFileTool {
|
||||||
|
|
||||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||||
match serde_json::from_value::<EditFileToolInput>(input.clone()) {
|
match serde_json::from_value::<EditFileToolInput>(input.clone()) {
|
||||||
Ok(input) => input.display_description,
|
Ok(input) => {
|
||||||
|
let path = Path::new(&input.path);
|
||||||
|
let mut description = input.display_description.clone();
|
||||||
|
|
||||||
|
// Add context about why confirmation may be needed
|
||||||
|
if path.components().any(|c| c.as_os_str() == ".zed") {
|
||||||
|
description.push_str(" (Zed settings)");
|
||||||
|
}
|
||||||
|
|
||||||
|
description
|
||||||
|
}
|
||||||
Err(_) => "Editing file".to_string(),
|
Err(_) => "Editing file".to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1384,6 +1428,7 @@ mod tests {
|
||||||
cx.set_global(settings_store);
|
cx.set_global(settings_store);
|
||||||
language::init(cx);
|
language::init(cx);
|
||||||
TelemetrySettings::register(cx);
|
TelemetrySettings::register(cx);
|
||||||
|
agent_settings::AgentSettings::register(cx);
|
||||||
Project::init_settings(cx);
|
Project::init_settings(cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1723,4 +1768,528 @@ mod tests {
|
||||||
"Trailing whitespace should remain when remove_trailing_whitespace_on_save is disabled"
|
"Trailing whitespace should remain when remove_trailing_whitespace_on_save is disabled"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_needs_confirmation(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
let tool = Arc::new(EditFileTool);
|
||||||
|
let fs = FakeFs::new(cx.executor());
|
||||||
|
fs.insert_tree("/root", json!({})).await;
|
||||||
|
|
||||||
|
// Test 1: Path with .zed component should require confirmation
|
||||||
|
let input_with_zed = json!({
|
||||||
|
"display_description": "Edit settings",
|
||||||
|
"path": ".zed/settings.json",
|
||||||
|
"mode": "edit"
|
||||||
|
});
|
||||||
|
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||||
|
cx.update(|cx| {
|
||||||
|
assert!(
|
||||||
|
tool.needs_confirmation(&input_with_zed, &project, cx),
|
||||||
|
"Path with .zed component should require confirmation"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 2: Absolute path should require confirmation
|
||||||
|
let input_absolute = json!({
|
||||||
|
"display_description": "Edit file",
|
||||||
|
"path": "/etc/hosts",
|
||||||
|
"mode": "edit"
|
||||||
|
});
|
||||||
|
cx.update(|cx| {
|
||||||
|
assert!(
|
||||||
|
tool.needs_confirmation(&input_absolute, &project, cx),
|
||||||
|
"Absolute path should require confirmation"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 3: Relative path without .zed should not require confirmation
|
||||||
|
let input_relative = json!({
|
||||||
|
"display_description": "Edit file",
|
||||||
|
"path": "root/src/main.rs",
|
||||||
|
"mode": "edit"
|
||||||
|
});
|
||||||
|
cx.update(|cx| {
|
||||||
|
assert!(
|
||||||
|
!tool.needs_confirmation(&input_relative, &project, cx),
|
||||||
|
"Relative path without .zed should not require confirmation"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 4: Path with .zed in the middle should require confirmation
|
||||||
|
let input_zed_middle = json!({
|
||||||
|
"display_description": "Edit settings",
|
||||||
|
"path": "root/.zed/tasks.json",
|
||||||
|
"mode": "edit"
|
||||||
|
});
|
||||||
|
cx.update(|cx| {
|
||||||
|
assert!(
|
||||||
|
tool.needs_confirmation(&input_zed_middle, &project, cx),
|
||||||
|
"Path with .zed in any component should require confirmation"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 5: When always_allow_tool_actions is enabled, no confirmation needed
|
||||||
|
cx.update(|cx| {
|
||||||
|
let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
|
||||||
|
settings.always_allow_tool_actions = true;
|
||||||
|
agent_settings::AgentSettings::override_global(settings, cx);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!tool.needs_confirmation(&input_with_zed, &project, cx),
|
||||||
|
"When always_allow_tool_actions is true, no confirmation should be needed"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!tool.needs_confirmation(&input_absolute, &project, cx),
|
||||||
|
"When always_allow_tool_actions is true, no confirmation should be needed for absolute paths"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn test_ui_text_with_confirmation_context(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
let tool = Arc::new(EditFileTool);
|
||||||
|
|
||||||
|
// Test ui_text shows context for .zed paths
|
||||||
|
let input_zed = json!({
|
||||||
|
"display_description": "Update settings",
|
||||||
|
"path": ".zed/settings.json",
|
||||||
|
"mode": "edit"
|
||||||
|
});
|
||||||
|
cx.update(|_cx| {
|
||||||
|
let ui_text = tool.ui_text(&input_zed);
|
||||||
|
assert_eq!(
|
||||||
|
ui_text, "Update settings (Zed settings)",
|
||||||
|
"UI text should indicate Zed settings file"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test ui_text for normal paths
|
||||||
|
let input_normal = json!({
|
||||||
|
"display_description": "Edit source file",
|
||||||
|
"path": "src/main.rs",
|
||||||
|
"mode": "edit"
|
||||||
|
});
|
||||||
|
cx.update(|_cx| {
|
||||||
|
let ui_text = tool.ui_text(&input_normal);
|
||||||
|
assert_eq!(
|
||||||
|
ui_text, "Edit source file",
|
||||||
|
"UI text should not have additional context for normal paths"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_needs_confirmation_outside_project(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
let tool = Arc::new(EditFileTool);
|
||||||
|
let fs = FakeFs::new(cx.executor());
|
||||||
|
|
||||||
|
// Create a project in /project directory
|
||||||
|
fs.insert_tree("/project", json!({})).await;
|
||||||
|
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
|
||||||
|
|
||||||
|
// Test file outside project requires confirmation
|
||||||
|
let input_outside = json!({
|
||||||
|
"display_description": "Edit file",
|
||||||
|
"path": "/outside/file.txt",
|
||||||
|
"mode": "edit"
|
||||||
|
});
|
||||||
|
cx.update(|cx| {
|
||||||
|
assert!(
|
||||||
|
tool.needs_confirmation(&input_outside, &project, cx),
|
||||||
|
"File outside project should require confirmation"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test file inside project doesn't require confirmation
|
||||||
|
let input_inside = json!({
|
||||||
|
"display_description": "Edit file",
|
||||||
|
"path": "project/file.txt",
|
||||||
|
"mode": "edit"
|
||||||
|
});
|
||||||
|
cx.update(|cx| {
|
||||||
|
assert!(
|
||||||
|
!tool.needs_confirmation(&input_inside, &project, cx),
|
||||||
|
"File inside project should not require confirmation"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_needs_confirmation_zed_paths(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
let tool = Arc::new(EditFileTool);
|
||||||
|
let fs = FakeFs::new(cx.executor());
|
||||||
|
fs.insert_tree("/home/user/myproject", json!({})).await;
|
||||||
|
let project = Project::test(fs.clone(), [path!("/home/user/myproject").as_ref()], cx).await;
|
||||||
|
|
||||||
|
// Test various .zed path patterns
|
||||||
|
let test_cases = vec![
|
||||||
|
(".zed/settings.json", true, "Top-level .zed file"),
|
||||||
|
("myproject/.zed/settings.json", true, ".zed in project path"),
|
||||||
|
("src/.zed/config.toml", true, ".zed in subdirectory"),
|
||||||
|
(
|
||||||
|
".zed.backup/file.txt",
|
||||||
|
true,
|
||||||
|
".zed.backup is outside project (not a .zed component issue)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"my.zed/file.txt",
|
||||||
|
true,
|
||||||
|
"my.zed is outside project (not a .zed component issue)",
|
||||||
|
),
|
||||||
|
("myproject/src/file.zed", false, ".zed as file extension"),
|
||||||
|
(
|
||||||
|
"myproject/normal/path/file.rs",
|
||||||
|
false,
|
||||||
|
"Normal file without .zed",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (path, should_confirm, description) in test_cases {
|
||||||
|
let input = json!({
|
||||||
|
"display_description": "Edit file",
|
||||||
|
"path": path,
|
||||||
|
"mode": "edit"
|
||||||
|
});
|
||||||
|
cx.update(|cx| {
|
||||||
|
assert_eq!(
|
||||||
|
tool.needs_confirmation(&input, &project, cx),
|
||||||
|
should_confirm,
|
||||||
|
"Failed for case: {} - path: {}",
|
||||||
|
description,
|
||||||
|
path
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_needs_confirmation_with_multiple_worktrees(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
let tool = Arc::new(EditFileTool);
|
||||||
|
let fs = FakeFs::new(cx.executor());
|
||||||
|
|
||||||
|
// Create multiple worktree directories
|
||||||
|
fs.insert_tree(
|
||||||
|
"/workspace/frontend",
|
||||||
|
json!({
|
||||||
|
"src": {
|
||||||
|
"main.js": "console.log('frontend');"
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
fs.insert_tree(
|
||||||
|
"/workspace/backend",
|
||||||
|
json!({
|
||||||
|
"src": {
|
||||||
|
"main.rs": "fn main() {}"
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
fs.insert_tree(
|
||||||
|
"/workspace/shared",
|
||||||
|
json!({
|
||||||
|
".zed": {
|
||||||
|
"settings.json": "{}"
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Create project with multiple worktrees
|
||||||
|
let project = Project::test(
|
||||||
|
fs.clone(),
|
||||||
|
[
|
||||||
|
path!("/workspace/frontend").as_ref(),
|
||||||
|
path!("/workspace/backend").as_ref(),
|
||||||
|
path!("/workspace/shared").as_ref(),
|
||||||
|
],
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Test files in different worktrees
|
||||||
|
let test_cases = vec![
|
||||||
|
("frontend/src/main.js", false, "File in first worktree"),
|
||||||
|
("backend/src/main.rs", false, "File in second worktree"),
|
||||||
|
(
|
||||||
|
"shared/.zed/settings.json",
|
||||||
|
true,
|
||||||
|
".zed file in third worktree",
|
||||||
|
),
|
||||||
|
("/etc/hosts", true, "Absolute path outside all worktrees"),
|
||||||
|
(
|
||||||
|
"../outside/file.txt",
|
||||||
|
true,
|
||||||
|
"Relative path outside worktrees",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (path, should_confirm, description) in test_cases {
|
||||||
|
let input = json!({
|
||||||
|
"display_description": "Edit file",
|
||||||
|
"path": path,
|
||||||
|
"mode": "edit"
|
||||||
|
});
|
||||||
|
cx.update(|cx| {
|
||||||
|
assert_eq!(
|
||||||
|
tool.needs_confirmation(&input, &project, cx),
|
||||||
|
should_confirm,
|
||||||
|
"Failed for case: {} - path: {}",
|
||||||
|
description,
|
||||||
|
path
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_needs_confirmation_edge_cases(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
let tool = Arc::new(EditFileTool);
|
||||||
|
let fs = FakeFs::new(cx.executor());
|
||||||
|
fs.insert_tree(
|
||||||
|
"/project",
|
||||||
|
json!({
|
||||||
|
".zed": {
|
||||||
|
"settings.json": "{}"
|
||||||
|
},
|
||||||
|
"src": {
|
||||||
|
".zed": {
|
||||||
|
"local.json": "{}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
|
||||||
|
|
||||||
|
// Test edge cases
|
||||||
|
let test_cases = vec![
|
||||||
|
// Empty path - find_project_path returns Some for empty paths
|
||||||
|
("", false, "Empty path is treated as project root"),
|
||||||
|
// Root directory
|
||||||
|
("/", true, "Root directory should be outside project"),
|
||||||
|
// Parent directory references - find_project_path resolves these
|
||||||
|
(
|
||||||
|
"project/../other",
|
||||||
|
false,
|
||||||
|
"Path with .. is resolved by find_project_path",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"project/./src/file.rs",
|
||||||
|
false,
|
||||||
|
"Path with . should work normally",
|
||||||
|
),
|
||||||
|
// Windows-style paths (if on Windows)
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
("C:\\Windows\\System32\\hosts", true, "Windows system path"),
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
("project\\src\\main.rs", false, "Windows-style project path"),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (path, should_confirm, description) in test_cases {
|
||||||
|
let input = json!({
|
||||||
|
"display_description": "Edit file",
|
||||||
|
"path": path,
|
||||||
|
"mode": "edit"
|
||||||
|
});
|
||||||
|
cx.update(|cx| {
|
||||||
|
assert_eq!(
|
||||||
|
tool.needs_confirmation(&input, &project, cx),
|
||||||
|
should_confirm,
|
||||||
|
"Failed for case: {} - path: {}",
|
||||||
|
description,
|
||||||
|
path
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_ui_text_shows_correct_context(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
let tool = Arc::new(EditFileTool);
|
||||||
|
|
||||||
|
// Test UI text for various scenarios
|
||||||
|
let test_cases = vec![
|
||||||
|
(
|
||||||
|
json!({
|
||||||
|
"display_description": "Update config",
|
||||||
|
"path": ".zed/settings.json",
|
||||||
|
"mode": "edit"
|
||||||
|
}),
|
||||||
|
"Update config (Zed settings)",
|
||||||
|
".zed path should show Zed settings context",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
json!({
|
||||||
|
"display_description": "Fix bug",
|
||||||
|
"path": "src/.zed/local.json",
|
||||||
|
"mode": "edit"
|
||||||
|
}),
|
||||||
|
"Fix bug (Zed settings)",
|
||||||
|
"Nested .zed path should show Zed settings context",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
json!({
|
||||||
|
"display_description": "Update readme",
|
||||||
|
"path": "README.md",
|
||||||
|
"mode": "edit"
|
||||||
|
}),
|
||||||
|
"Update readme",
|
||||||
|
"Normal path should not show additional context",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
json!({
|
||||||
|
"display_description": "Edit config",
|
||||||
|
"path": "config.zed",
|
||||||
|
"mode": "edit"
|
||||||
|
}),
|
||||||
|
"Edit config",
|
||||||
|
".zed as extension should not show context",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (input, expected_text, description) in test_cases {
|
||||||
|
cx.update(|_cx| {
|
||||||
|
let ui_text = tool.ui_text(&input);
|
||||||
|
assert_eq!(ui_text, expected_text, "Failed for case: {}", description);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_needs_confirmation_with_different_modes(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
let tool = Arc::new(EditFileTool);
|
||||||
|
let fs = FakeFs::new(cx.executor());
|
||||||
|
fs.insert_tree(
|
||||||
|
"/project",
|
||||||
|
json!({
|
||||||
|
"existing.txt": "content",
|
||||||
|
".zed": {
|
||||||
|
"settings.json": "{}"
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
|
||||||
|
|
||||||
|
// Test different EditFileMode values
|
||||||
|
let modes = vec![
|
||||||
|
EditFileMode::Edit,
|
||||||
|
EditFileMode::Create,
|
||||||
|
EditFileMode::Overwrite,
|
||||||
|
];
|
||||||
|
|
||||||
|
for mode in modes {
|
||||||
|
// Test .zed path with different modes
|
||||||
|
let input_zed = json!({
|
||||||
|
"display_description": "Edit settings",
|
||||||
|
"path": "project/.zed/settings.json",
|
||||||
|
"mode": mode
|
||||||
|
});
|
||||||
|
cx.update(|cx| {
|
||||||
|
assert!(
|
||||||
|
tool.needs_confirmation(&input_zed, &project, cx),
|
||||||
|
".zed path should require confirmation regardless of mode: {:?}",
|
||||||
|
mode
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test outside path with different modes
|
||||||
|
let input_outside = json!({
|
||||||
|
"display_description": "Edit file",
|
||||||
|
"path": "/outside/file.txt",
|
||||||
|
"mode": mode
|
||||||
|
});
|
||||||
|
cx.update(|cx| {
|
||||||
|
assert!(
|
||||||
|
tool.needs_confirmation(&input_outside, &project, cx),
|
||||||
|
"Outside path should require confirmation regardless of mode: {:?}",
|
||||||
|
mode
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test normal path with different modes
|
||||||
|
let input_normal = json!({
|
||||||
|
"display_description": "Edit file",
|
||||||
|
"path": "project/normal.txt",
|
||||||
|
"mode": mode
|
||||||
|
});
|
||||||
|
cx.update(|cx| {
|
||||||
|
assert!(
|
||||||
|
!tool.needs_confirmation(&input_normal, &project, cx),
|
||||||
|
"Normal path should not require confirmation regardless of mode: {:?}",
|
||||||
|
mode
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_always_allow_tool_actions_bypasses_all_checks(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
let tool = Arc::new(EditFileTool);
|
||||||
|
let fs = FakeFs::new(cx.executor());
|
||||||
|
fs.insert_tree("/project", json!({})).await;
|
||||||
|
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
|
||||||
|
|
||||||
|
// Enable always_allow_tool_actions
|
||||||
|
cx.update(|cx| {
|
||||||
|
let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
|
||||||
|
settings.always_allow_tool_actions = true;
|
||||||
|
agent_settings::AgentSettings::override_global(settings, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test that all paths that normally require confirmation are bypassed
|
||||||
|
let test_cases = vec![
|
||||||
|
".zed/settings.json",
|
||||||
|
"project/.zed/config.toml",
|
||||||
|
"/etc/hosts",
|
||||||
|
"/absolute/path/file.txt",
|
||||||
|
"../outside/project.txt",
|
||||||
|
];
|
||||||
|
|
||||||
|
for path in test_cases {
|
||||||
|
let input = json!({
|
||||||
|
"display_description": "Edit file",
|
||||||
|
"path": path,
|
||||||
|
"mode": "edit"
|
||||||
|
});
|
||||||
|
cx.update(|cx| {
|
||||||
|
assert!(
|
||||||
|
!tool.needs_confirmation(&input, &project, cx),
|
||||||
|
"Path {} should not require confirmation when always_allow_tool_actions is true",
|
||||||
|
path
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable always_allow_tool_actions and verify confirmation is required again
|
||||||
|
cx.update(|cx| {
|
||||||
|
let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
|
||||||
|
settings.always_allow_tool_actions = false;
|
||||||
|
agent_settings::AgentSettings::override_global(settings, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify .zed path requires confirmation again
|
||||||
|
let input = json!({
|
||||||
|
"display_description": "Edit file",
|
||||||
|
"path": ".zed/settings.json",
|
||||||
|
"mode": "edit"
|
||||||
|
});
|
||||||
|
cx.update(|cx| {
|
||||||
|
assert!(
|
||||||
|
tool.needs_confirmation(&input, &project, cx),
|
||||||
|
".zed path should require confirmation when always_allow_tool_actions is false"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -116,7 +116,7 @@ impl Tool for FetchTool {
|
||||||
"fetch".to_string()
|
"fetch".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -55,7 +55,7 @@ impl Tool for FindPathTool {
|
||||||
"find_path".into()
|
"find_path".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -57,7 +57,7 @@ impl Tool for GrepTool {
|
||||||
"grep".into()
|
"grep".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -45,7 +45,7 @@ impl Tool for ListDirectoryTool {
|
||||||
"list_directory".into()
|
"list_directory".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -42,7 +42,7 @@ impl Tool for MovePathTool {
|
||||||
"move_path".into()
|
"move_path".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -33,7 +33,7 @@ impl Tool for NowTool {
|
||||||
"now".into()
|
"now".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,7 @@ impl Tool for OpenTool {
|
||||||
"open".to_string()
|
"open".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
fn may_perform_edits(&self) -> bool {
|
fn may_perform_edits(&self) -> bool {
|
||||||
|
|
|
@ -19,7 +19,7 @@ impl Tool for ProjectNotificationsTool {
|
||||||
"project_notifications".to_string()
|
"project_notifications".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
fn may_perform_edits(&self) -> bool {
|
fn may_perform_edits(&self) -> bool {
|
||||||
|
|
|
@ -54,7 +54,7 @@ impl Tool for ReadFileTool {
|
||||||
"read_file".into()
|
"read_file".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -77,7 +77,7 @@ impl Tool for TerminalTool {
|
||||||
Self::NAME.to_string()
|
Self::NAME.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,7 @@ impl Tool for ThinkingTool {
|
||||||
"thinking".to_string()
|
"thinking".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,7 +32,7 @@ impl Tool for WebSearchTool {
|
||||||
"web_search".into()
|
"web_search".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue