Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions crates/agent/src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5545,6 +5545,106 @@ async fn test_max_subagent_depth_prevents_tool_registration(cx: &mut TestAppCont
});
}

#[gpui::test]
async fn test_lsp_tools_gated_by_feature_flag(cx: &mut TestAppContext) {
init_test(cx);

let fs = FakeFs::new(cx.executor());
fs.insert_tree(path!("/test"), json!({})).await;
let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
let project_context = cx.new(|_cx| ProjectContext::default());
let context_server_store = project.read_with(cx, |project, _| project.context_server_store());
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(context_server_store.clone(), cx));
let model = Arc::new(FakeLanguageModel::default());
let environment = Rc::new(cx.update(|cx| {
FakeThreadEnvironment::default().with_terminal(FakeTerminalHandle::new_never_exits(cx))
}));

let thread = cx.new(|cx| {
let mut thread = Thread::new(
project,
project_context,
context_server_registry,
Templates::new(),
Some(model.clone() as Arc<dyn LanguageModel>),
cx,
);
thread.add_default_tools(environment, cx);
thread
});

let lsp_tool_names = [
DiagnosticsTool::NAME,
FindReferencesTool::NAME,
GetCodeActionsTool::NAME,
ApplyCodeActionTool::NAME,
GoToDefinitionTool::NAME,
RenameTool::NAME,
];

// All LSP tools should be registered on the thread regardless of the flag,
// since the feature flag now only controls exposure to the model rather
// than registration.
thread.read_with(cx, |thread, _| {
for name in &lsp_tool_names {
assert!(
thread.has_registered_tool(name),
"expected LSP tool {name} to be registered"
);
}
});

// Without the `lsp-tool` flag, sending a message should produce a
// completion request whose tool list excludes the LSP tools.
thread
.update(cx, |thread, cx| {
thread.send(UserMessageId::new(), ["hello"], cx)
})
.unwrap();
cx.run_until_parked();

let completion = model.pending_completions().pop().unwrap();
let tool_names = tool_names_for_completion(&completion);
for name in &lsp_tool_names {
assert!(
!tool_names.iter().any(|t| t == name),
"expected LSP tool {name} to be hidden without the lsp-tool flag, \
but completion tools were: {tool_names:?}"
);
}
// Sanity check: a non-LSP default tool should still be exposed.
assert!(
tool_names.iter().any(|t| t == ReadFileTool::NAME),
"expected non-LSP tools to still be exposed, got: {tool_names:?}"
);
model.end_last_completion_stream();
cx.run_until_parked();

// Enable the `lsp-tool` flag and send another message; the LSP tools
// should now appear in the completion request.
cx.update(|cx| {
cx.update_flags(false, vec!["lsp-tool".to_string()]);
});

thread
.update(cx, |thread, cx| {
thread.send(UserMessageId::new(), ["hello again"], cx)
})
.unwrap();
cx.run_until_parked();

let completion = model.pending_completions().pop().unwrap();
let tool_names = tool_names_for_completion(&completion);
for name in &lsp_tool_names {
assert!(
tool_names.iter().any(|t| t == name),
"expected LSP tool {name} to be exposed when lsp-tool flag is on, \
but completion tools were: {tool_names:?}"
);
}
}

#[gpui::test]
async fn test_parent_cancel_stops_subagent(cx: &mut TestAppContext) {
init_test(cx);
Expand Down
43 changes: 25 additions & 18 deletions crates/agent/src/thread.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1577,20 +1577,19 @@ impl Thread {
self.add_tool(WebSearchTool);

self.add_tool(DiagnosticsTool::new(self.project.clone()));
if cx.has_flag::<LspToolFeatureFlag>() {
let code_action_store: CodeActionStore = cx.new(|_cx| None);
self.add_tool(FindReferencesTool::new(self.project.clone()));
self.add_tool(GetCodeActionsTool::new(
self.project.clone(),
code_action_store.clone(),
));
self.add_tool(ApplyCodeActionTool::new(
self.project.clone(),
code_action_store,
));
self.add_tool(GoToDefinitionTool::new(self.project.clone()));
self.add_tool(RenameTool::new(self.project.clone()));
}

let code_action_store: CodeActionStore = cx.new(|_cx| None);
self.add_tool(FindReferencesTool::new(self.project.clone()));
self.add_tool(GetCodeActionsTool::new(
self.project.clone(),
code_action_store.clone(),
));
self.add_tool(ApplyCodeActionTool::new(
self.project.clone(),
code_action_store,
));
self.add_tool(GoToDefinitionTool::new(self.project.clone()));
self.add_tool(RenameTool::new(self.project.clone()));

if self.depth() < MAX_SUBAGENT_DEPTH {
self.add_tool(SpawnAgentTool::new(environment));
Expand Down Expand Up @@ -2894,6 +2893,18 @@ impl Thread {
None
}
})
.filter(|(tool_name, _)| {
cx.has_flag::<LspToolFeatureFlag>()
|| !matches!(
tool_name.as_ref(),
DiagnosticsTool::NAME
Comment thread
bennetbo marked this conversation as resolved.
Resolved
| FindReferencesTool::NAME
| GetCodeActionsTool::NAME
| ApplyCodeActionTool::NAME
| GoToDefinitionTool::NAME
| RenameTool::NAME
)
})
.collect::<BTreeMap<_, _>>();

let mut context_server_tools = Vec::new();
Expand Down Expand Up @@ -2957,10 +2968,6 @@ impl Thread {
self.tools.contains_key(name)
}

pub fn registered_tool_names(&self) -> Vec<SharedString> {
self.tools.keys().cloned().collect()
}

pub(crate) fn register_running_subagent(&mut self, subagent: WeakEntity<Thread>) {
self.running_subagents.push(subagent);
}
Expand Down
Loading