Add local project indexing and search guidance
This commit is contained in:
@@ -4,9 +4,14 @@ use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
use ai::index::full_source_code_embedding::manager::{
|
||||
CodebaseIndexManager, CodebaseIndexManagerEvent,
|
||||
};
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
use ai::index::local_project_index::{
|
||||
LocalIndexStatus, LocalProjectIndexEvent, LocalProjectIndexManager,
|
||||
};
|
||||
use futures::channel::oneshot;
|
||||
use futures::future::join_all;
|
||||
use galaxy_cli::agent::Harness;
|
||||
@@ -90,6 +95,9 @@ pub fn prepare_environment(
|
||||
if should_subscribe_to_index_updates && result.is_err() {
|
||||
let _ = spawner
|
||||
.spawn(|_, ctx| {
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
ctx.unsubscribe_from_model(&LocalProjectIndexManager::handle(ctx));
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
ctx.unsubscribe_from_model(&CodebaseIndexManager::handle(ctx));
|
||||
})
|
||||
.await;
|
||||
@@ -201,6 +209,9 @@ async fn prepare_environment_impl(
|
||||
} else if should_index_codebase && source_repos.is_empty() {
|
||||
let _ = spawner
|
||||
.spawn(|_, ctx| {
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
ctx.unsubscribe_from_model(&LocalProjectIndexManager::handle(ctx));
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
ctx.unsubscribe_from_model(&CodebaseIndexManager::handle(ctx));
|
||||
})
|
||||
.await;
|
||||
@@ -235,6 +246,9 @@ fn record_codebase_indexing(
|
||||
setup_events.record_value_detached(SetupStep::EnvironmentCodebaseIndexing, async move {
|
||||
let _ = spawner
|
||||
.spawn(|_, ctx| {
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
ctx.unsubscribe_from_model(&LocalProjectIndexManager::handle(ctx));
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
ctx.unsubscribe_from_model(&CodebaseIndexManager::handle(ctx));
|
||||
})
|
||||
.await;
|
||||
@@ -255,6 +269,9 @@ fn record_codebase_indexing(
|
||||
}
|
||||
let _ = spawner
|
||||
.spawn(|_, ctx| {
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
ctx.unsubscribe_from_model(&LocalProjectIndexManager::handle(ctx));
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
ctx.unsubscribe_from_model(&CodebaseIndexManager::handle(ctx));
|
||||
})
|
||||
.await;
|
||||
@@ -489,6 +506,7 @@ pub(super) async fn register_cloned_repo(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
async fn subscribe_to_codebase_index_events(
|
||||
spawner: &ModelSpawner<TerminalDriver>,
|
||||
repo_channels: Arc<Mutex<HashMap<PathBuf, oneshot::Sender<()>>>>,
|
||||
@@ -496,11 +514,10 @@ async fn subscribe_to_codebase_index_events(
|
||||
spawner
|
||||
.spawn(move |_, ctx| {
|
||||
let repo_channels = Arc::clone(&repo_channels);
|
||||
ctx.subscribe_to_model(&CodebaseIndexManager::handle(ctx), move |_, _, event, ctx| {
|
||||
if !matches!(
|
||||
event,
|
||||
CodebaseIndexManagerEvent::SyncStateUpdated { .. }
|
||||
) {
|
||||
ctx.subscribe_to_model(
|
||||
&CodebaseIndexManager::handle(ctx),
|
||||
move |_, _, event, ctx| {
|
||||
if !matches!(event, CodebaseIndexManagerEvent::SyncStateUpdated { .. }) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -511,22 +528,15 @@ async fn subscribe_to_codebase_index_events(
|
||||
.expect("repo channel map lock should not be poisoned");
|
||||
|
||||
for repo in channels.keys() {
|
||||
let Some(status) =
|
||||
manager.get_codebase_index_status_for_path(repo, ctx)
|
||||
let Some(status) = manager.get_codebase_index_status_for_path(repo, ctx)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if status.has_synced_version() {
|
||||
repos_to_notify.push(repo.clone());
|
||||
continue;
|
||||
}
|
||||
|
||||
if !status.has_pending() && status.last_sync_successful() == Some(false) {
|
||||
safe_warn!(
|
||||
safe: ("Codebase index sync failed for a repo; unblocking environment setup"),
|
||||
full: ("Codebase index sync failed for {repo:?}; unblocking environment setup")
|
||||
);
|
||||
if status.has_synced_version()
|
||||
|| (!status.has_pending()
|
||||
&& status.last_sync_successful() == Some(false))
|
||||
{
|
||||
repos_to_notify.push(repo.clone());
|
||||
}
|
||||
}
|
||||
@@ -536,12 +546,58 @@ async fn subscribe_to_codebase_index_events(
|
||||
let _ = tx.send(());
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
})
|
||||
.await
|
||||
.map_err(|_| PrepareEnvironmentError::InvalidRuntimeState)
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
async fn subscribe_to_codebase_index_events(
|
||||
spawner: &ModelSpawner<TerminalDriver>,
|
||||
repo_channels: Arc<Mutex<HashMap<PathBuf, oneshot::Sender<()>>>>,
|
||||
) -> Result<(), PrepareEnvironmentError> {
|
||||
spawner
|
||||
.spawn(move |_, ctx| {
|
||||
let repo_channels = Arc::clone(&repo_channels);
|
||||
ctx.subscribe_to_model(
|
||||
&LocalProjectIndexManager::handle(ctx),
|
||||
move |_, _, event, _ctx| {
|
||||
if !matches!(event, LocalProjectIndexEvent::StatusChanged { .. }) {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut repos_to_notify = Vec::new();
|
||||
let mut channels = repo_channels
|
||||
.lock()
|
||||
.expect("repo channel map lock should not be poisoned");
|
||||
for repo in channels.keys() {
|
||||
// The manager event identifies the changed root, while the channel map can
|
||||
// contain nested paths. Re-check each path so a root-level completion wakes
|
||||
// the exact environment request that owns it.
|
||||
if LocalProjectIndexManager::as_ref(_ctx).is_ready_for_path(repo)
|
||||
|| matches!(
|
||||
LocalProjectIndexManager::as_ref(_ctx).status_for_path(repo),
|
||||
Some((_, LocalIndexStatus::Failed { .. }))
|
||||
)
|
||||
{
|
||||
repos_to_notify.push(repo.clone());
|
||||
}
|
||||
}
|
||||
for repo in repos_to_notify {
|
||||
if let Some(tx) = channels.remove(&repo) {
|
||||
let _ = tx.send(());
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
})
|
||||
.await
|
||||
.map_err(|_| PrepareEnvironmentError::InvalidRuntimeState)
|
||||
}
|
||||
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
#[tracing::instrument(skip_all, err, fields(tags.cloud_agent = true, repo = %repo_name))]
|
||||
async fn index_repo_codebase(
|
||||
repo_name: &str,
|
||||
@@ -592,6 +648,57 @@ async fn index_repo_codebase(
|
||||
.map_err(|_| PrepareEnvironmentError::InvalidRuntimeState)
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
#[tracing::instrument(skip_all, err, fields(tags.cloud_agent = true, repo = %repo_name))]
|
||||
async fn index_repo_codebase(
|
||||
repo_name: &str,
|
||||
working_dir: &Path,
|
||||
repo_channels: Arc<Mutex<HashMap<PathBuf, oneshot::Sender<()>>>>,
|
||||
spawner: &ModelSpawner<TerminalDriver>,
|
||||
) -> Result<Option<oneshot::Receiver<()>>, PrepareEnvironmentError> {
|
||||
let repo_path = working_dir.join(repo_name);
|
||||
|
||||
safe_info!(
|
||||
safe: ("Trying to index repository for codebase context"),
|
||||
full: ("Trying to index {:?} for codebase context", repo_path)
|
||||
);
|
||||
|
||||
let repo_path_for_spawn = repo_path.clone();
|
||||
spawner
|
||||
.spawn(move |_, ctx| {
|
||||
let (tx, rx) = oneshot::channel::<()>();
|
||||
repo_channels
|
||||
.lock()
|
||||
.expect("repo channel map lock should not be poisoned")
|
||||
.insert(repo_path_for_spawn.clone(), tx);
|
||||
|
||||
let indexing_result = LocalProjectIndexManager::handle(ctx)
|
||||
.update(ctx, |manager, ctx| {
|
||||
manager.index_directory(repo_path_for_spawn.clone(), ctx)
|
||||
});
|
||||
if let Err(error) = indexing_result {
|
||||
log::warn!("Failed to start local project indexing: {error:#}");
|
||||
repo_channels
|
||||
.lock()
|
||||
.expect("repo channel map lock should not be poisoned")
|
||||
.remove(&repo_path_for_spawn);
|
||||
return None;
|
||||
}
|
||||
|
||||
if LocalProjectIndexManager::as_ref(ctx).is_ready_for_path(&repo_path_for_spawn) {
|
||||
repo_channels
|
||||
.lock()
|
||||
.expect("repo channel map lock should not be poisoned")
|
||||
.remove(&repo_path_for_spawn);
|
||||
None
|
||||
} else {
|
||||
Some(rx)
|
||||
}
|
||||
})
|
||||
.await
|
||||
.map_err(|_| PrepareEnvironmentError::InvalidRuntimeState)
|
||||
}
|
||||
|
||||
/// Execute a command in the context of a terminal session.
|
||||
async fn execute_command(
|
||||
command: String,
|
||||
|
||||
@@ -2,7 +2,10 @@ use std::path::Path;
|
||||
use std::sync::LazyLock;
|
||||
use std::time::Duration;
|
||||
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
use ai::index::full_source_code_embedding::manager::CodebaseIndexManager;
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
use ai::index::local_project_index::LocalProjectIndexManager;
|
||||
use markdown_parser::FormattedTextFragment;
|
||||
use rand::seq::SliceRandom;
|
||||
use warpui::keymap::Keystroke;
|
||||
@@ -411,6 +414,9 @@ impl AITip for AgentTip {
|
||||
else {
|
||||
return true;
|
||||
};
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
return !LocalProjectIndexManager::as_ref(app).is_ready_for_path(root);
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
return CodebaseIndexManager::as_ref(app)
|
||||
.get_codebase_index_status_for_path(root, app)
|
||||
.is_none();
|
||||
|
||||
@@ -1558,6 +1558,11 @@ pub fn extract_system_prompt(
|
||||
));
|
||||
}
|
||||
let has_tool = |name: &str| tool_names.iter().any(|tool_name| tool_name == name);
|
||||
if has_tool("search_codebase") {
|
||||
prompt.push_str(
|
||||
"- For source-code discovery, semantic questions, or finding an unfamiliar implementation, MUST use `search_codebase` first. Never use `grep` to discover or search source code when `search_codebase` is available; reserve `grep` for exact known text in non-code files. Use `file_glob` only to locate filenames and `read_files` for focused follow-up context.\n",
|
||||
);
|
||||
}
|
||||
if [
|
||||
"read_files",
|
||||
"apply_file_diffs",
|
||||
@@ -1569,7 +1574,7 @@ pub fn extract_system_prompt(
|
||||
.any(has_tool)
|
||||
{
|
||||
prompt.push_str(
|
||||
"- Use filesystem/search tools to understand the codebase and focused diff tools to edit \
|
||||
"- Use the appropriate filesystem/search tool to understand the codebase and focused diff tools to edit \
|
||||
it. Use paths rooted in the working directory and absolute paths when a schema requires \
|
||||
them.\n",
|
||||
);
|
||||
@@ -1783,7 +1788,7 @@ pub fn default_tool_definitions() -> Vec<ToolDefinition> {
|
||||
},
|
||||
ToolDefinition {
|
||||
name: "grep".to_string(),
|
||||
description: "Search for regex patterns in files. Uses git grep in git repos (respects .gitignore) or ripgrep otherwise. Returns up to 1,000 matching line locations and reports when additional matches were omitted. Use read_files afterward to see context around matches. Pass ALL patterns you need in one call.".to_string(),
|
||||
description: "Search for regex patterns in files. Use only for exact known text in non-code files; never use this to discover or search source code when search_codebase is available. Uses git grep in git repos (respects .gitignore) or ripgrep otherwise. Returns up to 1,000 matching line locations and reports when additional matches were omitted. Use read_files afterward to see context around matches. Pass ALL patterns you need in one call.".to_string(),
|
||||
input_schema: serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -1795,7 +1800,7 @@ pub fn default_tool_definitions() -> Vec<ToolDefinition> {
|
||||
},
|
||||
ToolDefinition {
|
||||
name: "file_glob".to_string(),
|
||||
description: "Find files matching glob patterns. Uses git ls-files in git repos. Returns absolute file paths of matches. Common patterns: '**/*.rs', 'src/**/*.ts', '**/Cargo.toml'. Pass ALL patterns in one call.".to_string(),
|
||||
description: "Find filenames matching glob patterns. Use this to locate files, not to search their contents. Uses git ls-files in git repos. Returns absolute file paths of matches. Common patterns: '**/*.rs', 'src/**/*.ts', '**/Cargo.toml'. Pass ALL patterns in one call.".to_string(),
|
||||
input_schema: serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -1807,7 +1812,7 @@ pub fn default_tool_definitions() -> Vec<ToolDefinition> {
|
||||
},
|
||||
ToolDefinition {
|
||||
name: "search_codebase".to_string(),
|
||||
description: "Semantic code search across the indexed codebase. Use for finding relevant code by meaning rather than exact text match. Better than grep for conceptual queries like 'authentication logic' or 'error handling for database connections'.".to_string(),
|
||||
description: "Primary tool for discovering relevant source code across the indexed codebase. MUST use this first for all source-code discovery and conceptual, semantic, or structural searches such as authentication logic or database error handling. Never use grep to search source code when this tool is available.".to_string(),
|
||||
input_schema: serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -107,6 +107,38 @@ fn advertised_tools_follow_client_capabilities_and_include_local_subagents() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bedrock_prompt_prioritizes_semantic_code_search_over_regex_search() {
|
||||
let request = api::Request {
|
||||
settings: Some(api::request::Settings {
|
||||
supported_tools: vec![
|
||||
api::ToolType::SearchCodebase.into(),
|
||||
api::ToolType::Grep.into(),
|
||||
api::ToolType::FileGlob.into(),
|
||||
api::ToolType::ReadFiles.into(),
|
||||
],
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let prompt = extract_system_prompt(&request, &[]).expect("system prompt");
|
||||
assert!(prompt.contains("MUST use `search_codebase` first"));
|
||||
assert!(prompt.contains("Never use `grep` to discover or search source code"));
|
||||
|
||||
let tools = extract_tools(&request);
|
||||
let search_codebase = tools
|
||||
.iter()
|
||||
.find(|tool| tool.name == "search_codebase")
|
||||
.expect("search_codebase tool");
|
||||
let grep = tools
|
||||
.iter()
|
||||
.find(|tool| tool.name == "grep")
|
||||
.expect("grep tool");
|
||||
assert!(search_codebase.description.contains("MUST use this first"));
|
||||
assert!(grep.description.contains("non-code files"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn direct_provider_advertises_local_tool_history_recall() {
|
||||
let request = api::Request {
|
||||
|
||||
@@ -226,6 +226,7 @@ impl GetFilesExecutor {
|
||||
GetRelevantFilesError::Missing => {
|
||||
"The current directory isn't within a git repository, which is necessary to search for relevant files.".to_owned()
|
||||
}
|
||||
GetRelevantFilesError::LocalIndexUnavailable(message) => message,
|
||||
};
|
||||
ActionExecution::Sync(AIAgentActionResultType::GetFiles(
|
||||
GetFilesResult::Error(error_message),
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
use ai::index::local_project_index::LocalProjectIndexManager;
|
||||
use futures::channel::oneshot;
|
||||
use futures::future::BoxFuture;
|
||||
use futures::FutureExt;
|
||||
@@ -273,6 +275,7 @@ impl SearchCodebaseExecutor {
|
||||
),
|
||||
},
|
||||
Err(e) => {
|
||||
self.active_searches.remove(id);
|
||||
log::warn!("Failed to send remote get_relevant_files request: {e:?}");
|
||||
ActionExecution::Sync(AIAgentActionResultType::SearchCodebase(
|
||||
SearchCodebaseResult::Failed {
|
||||
@@ -385,6 +388,7 @@ impl SearchCodebaseExecutor {
|
||||
),
|
||||
},
|
||||
Err(e) => {
|
||||
self.active_searches.remove(id);
|
||||
log::warn!("Failed to send get_relevant_files request for directory: {e:?}");
|
||||
|
||||
let error_message = match e {
|
||||
@@ -397,6 +401,7 @@ impl SearchCodebaseExecutor {
|
||||
GetRelevantFilesError::Missing => {
|
||||
"The current directory isn't within a git repository, which is necessary to search for relevant files.".to_owned()
|
||||
}
|
||||
GetRelevantFilesError::LocalIndexUnavailable(message) => message,
|
||||
};
|
||||
ActionExecution::Sync(AIAgentActionResultType::SearchCodebase(
|
||||
SearchCodebaseResult::Failed {
|
||||
@@ -465,6 +470,13 @@ impl SearchCodebaseExecutor {
|
||||
pwd
|
||||
};
|
||||
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
if let Some(root) = app
|
||||
.try_get_singleton_model_as_ref::<LocalProjectIndexManager>()
|
||||
.and_then(|manager| manager.root_for_path(&search_dir))
|
||||
{
|
||||
return Some(root);
|
||||
}
|
||||
self.get_relevant_files_controller
|
||||
.as_ref(app)
|
||||
.root_directory_for_search(&search_dir, app)
|
||||
|
||||
@@ -15,7 +15,7 @@ use crate::ui_components::blended_colors;
|
||||
use crate::ui_components::icons::Icon;
|
||||
|
||||
const SPEEDBUMP_HEADER: &str = "Index Codebase?";
|
||||
const SPEEDBUMP_TEXT: &str = "Indexing helps agents quickly understand context and provide targeted solutions. Code is never stored on the server.";
|
||||
const SPEEDBUMP_TEXT: &str = "Galaxy builds and searches this project index on your device. Source files are read from disk when results are returned to the agent.";
|
||||
/// Uniform padding around the banner
|
||||
const PADDING: f32 = 12.;
|
||||
/// Text for the button that allows execution
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use ai::index::full_source_code_embedding::manager::CodebaseIndexManager;
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
use ai::index::local_project_index::LocalProjectIndexManager;
|
||||
use chrono::Local;
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
use galaxy_graphql::generic_string_object::GenericStringObjectFormat as GraphQLFormat;
|
||||
@@ -65,10 +66,12 @@ pub(super) fn input_context_for_request(
|
||||
context.push(AIAgentContext::ExecutionEnvironment(env));
|
||||
}
|
||||
|
||||
if FeatureFlag::FullSourceCodeEmbedding.is_enabled()
|
||||
&& FeatureFlag::CrossRepoContext.is_enabled()
|
||||
{
|
||||
let session_context = SessionContext::from_session(active_session, app);
|
||||
let session_context = SessionContext::from_session(active_session, app);
|
||||
let should_add_codebase_context = FeatureFlag::CrossRepoContext.is_enabled()
|
||||
&& ((session_context.is_remote() && FeatureFlag::FullSourceCodeEmbedding.is_enabled())
|
||||
|| (!session_context.is_remote()
|
||||
&& cfg!(all(feature = "local_fs", not(target_family = "wasm")))));
|
||||
if should_add_codebase_context {
|
||||
if session_context.is_remote() {
|
||||
add_remote_codebase_context(&mut context, &session_context, app);
|
||||
} else {
|
||||
@@ -91,27 +94,28 @@ pub(super) fn input_context_for_request(
|
||||
context.into()
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
fn add_local_codebase_context(context: &mut Vec<AIAgentContext>, app: &AppContext) {
|
||||
for (codebase_path, status) in
|
||||
CodebaseIndexManager::as_ref(app).get_codebase_index_statuses(app)
|
||||
{
|
||||
// TODO(daniel): We should figure out a mechanism for handling stale codebases.
|
||||
if status.has_synced_version() {
|
||||
// For now, we pass the name of the directory as the name of the
|
||||
// codebase.
|
||||
for (codebase_path, status) in LocalProjectIndexManager::as_ref(app).statuses() {
|
||||
if matches!(
|
||||
status,
|
||||
ai::index::local_project_index::LocalIndexStatus::Ready { .. }
|
||||
) {
|
||||
let codebase_name = codebase_path
|
||||
.file_name()
|
||||
.map(|name| name.to_string_lossy())
|
||||
.unwrap_or_default();
|
||||
|
||||
context.push(AIAgentContext::Codebase {
|
||||
name: codebase_name.into(),
|
||||
path: codebase_path.to_string_lossy().into(),
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
fn add_local_codebase_context(_context: &mut Vec<AIAgentContext>, _app: &AppContext) {}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn add_remote_codebase_context(
|
||||
context: &mut Vec<AIAgentContext>,
|
||||
|
||||
@@ -47,9 +47,16 @@ fn codebase_indexing_enabled(
|
||||
surface: CodebaseAutoIndexingSurface,
|
||||
codebase_context_enabled: bool,
|
||||
) -> bool {
|
||||
FeatureFlag::FullSourceCodeEmbedding.is_enabled()
|
||||
&& surface.required_feature_enabled()
|
||||
&& codebase_context_enabled
|
||||
match surface {
|
||||
// Native local indexing is structural/lexical and does not depend on the remote
|
||||
// embedding feature. Remote indexing retains its existing feature gates.
|
||||
CodebaseAutoIndexingSurface::Local => codebase_context_enabled,
|
||||
CodebaseAutoIndexingSurface::Remote => {
|
||||
FeatureFlag::FullSourceCodeEmbedding.is_enabled()
|
||||
&& surface.required_feature_enabled()
|
||||
&& codebase_context_enabled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn codebase_auto_indexing_enabled(
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn local_auto_indexing_requires_full_source_code_embedding_codebase_context_and_auto_indexing() {
|
||||
fn local_auto_indexing_requires_codebase_context_and_auto_indexing() {
|
||||
{
|
||||
let _flag = FeatureFlag::FullSourceCodeEmbedding.override_enabled(false);
|
||||
assert!(!codebase_auto_indexing_enabled(
|
||||
assert!(codebase_auto_indexing_enabled(
|
||||
CodebaseAutoIndexingSurface::Local,
|
||||
true,
|
||||
true,
|
||||
));
|
||||
}
|
||||
{
|
||||
let _flag = FeatureFlag::FullSourceCodeEmbedding.override_enabled(true);
|
||||
let _flag = FeatureFlag::FullSourceCodeEmbedding.override_enabled(false);
|
||||
assert!(codebase_auto_indexing_enabled(
|
||||
CodebaseAutoIndexingSurface::Local,
|
||||
true,
|
||||
|
||||
@@ -2,25 +2,38 @@ use std::collections::HashSet;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
use ai::index::full_source_code_embedding::manager::{
|
||||
CodebaseIndexManager, CodebaseIndexManagerEvent,
|
||||
};
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
use ai::index::full_source_code_embedding::RetrievalID;
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
use ai::index::local_project_index::LocalProjectIndexManager;
|
||||
use ai::index::locations::CodeContextLocation;
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
use anyhow::anyhow;
|
||||
use futures_util::stream::AbortHandle;
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
use galaxyui::{AppContext, Entity, ModelContext, ModelHandle, SingletonEntity};
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
use instant::Instant;
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
use crate::ai::agent::SearchCodebaseFailureReason;
|
||||
use crate::ai::agent::{AIAgentActionId, SearchCodebaseResult};
|
||||
use crate::ai::blocklist::SessionContext;
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
use crate::ai::get_relevant_files::api::{FileContext as FileContextRequest, GetRelevantFiles};
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
use crate::ai::get_relevant_files::controller::GetRelevantFilesError::LocalIndexUnavailable;
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
use crate::ai::outline::{OutlineStatus, RepoOutlines};
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
use crate::report_error;
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
use crate::server::server_api::{AIApiError, ServerApiProvider};
|
||||
use crate::{report_error, send_telemetry_from_ctx, TelemetryEvent};
|
||||
use crate::{send_telemetry_from_ctx, TelemetryEvent};
|
||||
#[cfg_attr(not(target_family = "wasm"), path = "remote_search/native.rs")]
|
||||
#[cfg_attr(target_family = "wasm", path = "remote_search/wasm.rs")]
|
||||
mod remote_search;
|
||||
@@ -60,6 +73,20 @@ pub enum GetRelevantFilesRequestTarget {
|
||||
requested_codebase_path: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Lifecycle state for the two-stage `get_files` relevant-file query.
|
||||
///
|
||||
/// The first stage discovers paths through `GetRelevantFilesController`; the second stage reads
|
||||
/// current file contents in `GetFilesExecutor`. Keeping this state separate from the controller's
|
||||
/// cancellation handles preserves the existing action protocol for both local and legacy search.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum GetRelevantFilesStatus {
|
||||
Pending { root_repo_path: PathBuf },
|
||||
InFlight { root_repo_path: PathBuf },
|
||||
Success { file_paths: Vec<PathBuf> },
|
||||
Failed { message: String },
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum GetRelevantFilesError {
|
||||
#[error("Repo outline is still being computed.")]
|
||||
@@ -68,15 +95,18 @@ pub enum GetRelevantFilesError {
|
||||
CreateFailed,
|
||||
#[error("Failed to create outline.")]
|
||||
Missing,
|
||||
#[error("Local project index is unavailable: {0}")]
|
||||
LocalIndexUnavailable(String),
|
||||
}
|
||||
|
||||
/// This enum allows us to use both the existing structure for outline-based indexing
|
||||
/// and the new full source code indexing manager/model.
|
||||
enum RequestHandle {
|
||||
/// Used with outline-based indexing.
|
||||
/// Used with outline-based indexing and remote search.
|
||||
AbortHandle(AbortHandle),
|
||||
|
||||
/// Used with full source code indexing.
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
RetrievalID {
|
||||
repo_path: PathBuf,
|
||||
retrieval_id: RetrievalID,
|
||||
@@ -88,6 +118,7 @@ impl RequestHandle {
|
||||
fn abort(&mut self, ctx: &mut AppContext) {
|
||||
match self {
|
||||
RequestHandle::AbortHandle(abort_handle) => abort_handle.abort(),
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
RequestHandle::RetrievalID {
|
||||
repo_path,
|
||||
retrieval_id,
|
||||
@@ -112,15 +143,63 @@ pub struct GetRelevantFilesController {
|
||||
/// This allows several SearchCodebase actions to be active at once without newer requests
|
||||
/// cancelling unrelated older ones.
|
||||
pending_requests: std::collections::HashMap<AIAgentActionId, RequestHandle>,
|
||||
/// Status for the two-stage `get_files` relevant-file query protocol.
|
||||
relevant_file_statuses: std::collections::HashMap<AIAgentActionId, GetRelevantFilesStatus>,
|
||||
}
|
||||
|
||||
impl GetRelevantFilesController {
|
||||
pub fn new(ctx: &mut ModelContext<Self>) -> Self {
|
||||
let codebase_manager = CodebaseIndexManager::handle(ctx);
|
||||
ctx.subscribe_to_model(&codebase_manager, Self::handle_codebase_manager_event);
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
{
|
||||
let codebase_manager = CodebaseIndexManager::handle(ctx);
|
||||
ctx.subscribe_to_model(&codebase_manager, Self::handle_codebase_manager_event);
|
||||
}
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn root_for_status(&self, directory: &Path, app: &AppContext) -> PathBuf {
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
{
|
||||
app.try_get_singleton_model_as_ref::<LocalProjectIndexManager>()
|
||||
.and_then(|manager| manager.root_for_path(directory))
|
||||
.unwrap_or_else(|| directory.to_path_buf())
|
||||
}
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
{
|
||||
self.root_directory_for_search(directory, app)
|
||||
.unwrap_or_else(|| directory.to_path_buf())
|
||||
}
|
||||
}
|
||||
|
||||
/// Queues the first stage of a relevant-file query for `GetFilesExecutor`.
|
||||
pub fn queue_request(
|
||||
&mut self,
|
||||
action_id: AIAgentActionId,
|
||||
directory: &Path,
|
||||
app: &AppContext,
|
||||
) {
|
||||
let root_repo_path = self.root_for_status(directory, app);
|
||||
self.relevant_file_statuses.insert(
|
||||
action_id,
|
||||
GetRelevantFilesStatus::Pending { root_repo_path },
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns the current status of a relevant-file query.
|
||||
pub fn status(&self, action_id: &AIAgentActionId) -> Option<&GetRelevantFilesStatus> {
|
||||
self.relevant_file_statuses.get(action_id)
|
||||
}
|
||||
|
||||
fn set_relevant_file_status(
|
||||
&mut self,
|
||||
action_id: &AIAgentActionId,
|
||||
status: GetRelevantFilesStatus,
|
||||
) {
|
||||
self.relevant_file_statuses
|
||||
.insert(action_id.clone(), status);
|
||||
}
|
||||
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
fn pending_request_details_for_retrieval_id(
|
||||
&self,
|
||||
pending_retrieval_id: &RetrievalID,
|
||||
@@ -140,6 +219,7 @@ impl GetRelevantFilesController {
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
fn handle_codebase_manager_event(
|
||||
&mut self,
|
||||
_: ModelHandle<CodebaseIndexManager>,
|
||||
@@ -213,7 +293,38 @@ impl GetRelevantFilesController {
|
||||
self.cancel_request_for_action(&action_id, ctx);
|
||||
match target {
|
||||
GetRelevantFilesRequestTarget::Local { directory } => {
|
||||
self.send_local_request(&directory, query, partial_path_segments, action_id, ctx)
|
||||
let root_repo_path = self
|
||||
.relevant_file_statuses
|
||||
.get(&action_id)
|
||||
.and_then(|status| match status {
|
||||
GetRelevantFilesStatus::Pending { root_repo_path }
|
||||
| GetRelevantFilesStatus::InFlight { root_repo_path } => {
|
||||
Some(root_repo_path.clone())
|
||||
}
|
||||
GetRelevantFilesStatus::Success { .. }
|
||||
| GetRelevantFilesStatus::Failed { .. } => None,
|
||||
})
|
||||
.unwrap_or_else(|| self.root_for_status(&directory, ctx));
|
||||
self.set_relevant_file_status(
|
||||
&action_id,
|
||||
GetRelevantFilesStatus::InFlight { root_repo_path },
|
||||
);
|
||||
let result = self.send_local_request(
|
||||
&directory,
|
||||
query,
|
||||
partial_path_segments,
|
||||
action_id.clone(),
|
||||
ctx,
|
||||
);
|
||||
if let Err(error) = &result {
|
||||
self.set_relevant_file_status(
|
||||
&action_id,
|
||||
GetRelevantFilesStatus::Failed {
|
||||
message: error.to_string(),
|
||||
},
|
||||
);
|
||||
}
|
||||
result
|
||||
}
|
||||
GetRelevantFilesRequestTarget::Remote {
|
||||
session_context,
|
||||
@@ -237,8 +348,45 @@ impl GetRelevantFilesController {
|
||||
action_id: AIAgentActionId,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) -> Result<(), GetRelevantFilesError> {
|
||||
const MINIMUM_FILE_COUNT_FOR_API_CALL: usize = 2;
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
{
|
||||
let Some(local_index) =
|
||||
ctx.try_get_singleton_model_as_ref::<LocalProjectIndexManager>()
|
||||
else {
|
||||
return Err(LocalIndexUnavailable(
|
||||
"The local project index is not available in this session.".to_string(),
|
||||
));
|
||||
};
|
||||
if local_index.is_searchable_for_path(directory) {
|
||||
let locations: HashSet<CodeContextLocation> = local_index
|
||||
.search(directory, &query, partial_path_segments.map(Vec::as_slice))
|
||||
.map_err(|error| LocalIndexUnavailable(error.to_string()))?
|
||||
.into_iter()
|
||||
.map(|hit| CodeContextLocation::WholeFile(hit.path))
|
||||
.collect();
|
||||
let file_paths = locations
|
||||
.iter()
|
||||
.map(|location| location.path().clone())
|
||||
.collect();
|
||||
self.set_relevant_file_status(
|
||||
&action_id,
|
||||
GetRelevantFilesStatus::Success { file_paths },
|
||||
);
|
||||
ctx.emit(GetRelevantFilesControllerEvent::Success {
|
||||
action_id,
|
||||
result: GetRelevantFilesControllerResult::Locations(Arc::new(locations)),
|
||||
});
|
||||
return Ok(());
|
||||
}
|
||||
return Err(LocalIndexUnavailable(
|
||||
"The local project index is not ready. Run /init or /index first.".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
let _ = (directory, query, partial_path_segments, action_id, ctx);
|
||||
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
if FeatureFlag::FullSourceCodeEmbedding.is_enabled() {
|
||||
let codebase_mgr = CodebaseIndexManager::handle(ctx);
|
||||
if let Some(base_path) = codebase_mgr.as_ref(ctx).root_path_for_codebase(directory) {
|
||||
@@ -268,79 +416,93 @@ impl GetRelevantFilesController {
|
||||
}
|
||||
}
|
||||
|
||||
match RepoOutlines::as_ref(ctx).get_outline(directory) {
|
||||
Some((OutlineStatus::Complete(outline), base_path)) => {
|
||||
let server_api = ServerApiProvider::as_ref(ctx).get();
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
{
|
||||
match RepoOutlines::as_ref(ctx).get_outline(directory) {
|
||||
Some((OutlineStatus::Complete(outline), base_path)) => {
|
||||
let server_api = ServerApiProvider::as_ref(ctx).get();
|
||||
|
||||
let file_outlines = outline.to_file_symbols(partial_path_segments);
|
||||
if file_outlines.len() < MINIMUM_FILE_COUNT_FOR_API_CALL {
|
||||
ctx.emit(GetRelevantFilesControllerEvent::Success {
|
||||
action_id,
|
||||
result: GetRelevantFilesControllerResult::Locations(Arc::new(
|
||||
let file_outlines = outline.to_file_symbols(partial_path_segments);
|
||||
if file_outlines.len() < MINIMUM_FILE_COUNT_FOR_API_CALL {
|
||||
let locations: Arc<HashSet<CodeContextLocation>> = Arc::new(
|
||||
file_outlines
|
||||
.into_iter()
|
||||
.map(|file| {
|
||||
CodeContextLocation::WholeFile(PathBuf::from(file.path))
|
||||
})
|
||||
.collect(),
|
||||
)),
|
||||
});
|
||||
} else {
|
||||
let outline_request = GetRelevantFiles {
|
||||
query,
|
||||
files: file_outlines
|
||||
.into_iter()
|
||||
.map(|outline| FileContextRequest {
|
||||
path: outline.path,
|
||||
symbols: outline.symbols,
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
let action_id_clone = action_id.clone();
|
||||
let request_abort_handle = ctx
|
||||
.spawn(
|
||||
async move {
|
||||
let response =
|
||||
server_api.get_relevant_files(&outline_request).await?;
|
||||
Ok(Arc::new(
|
||||
response
|
||||
.relevant_file_paths
|
||||
.into_iter()
|
||||
.filter_map(|path| {
|
||||
let file_path = base_path.join(path);
|
||||
// Validate the returned file paths.
|
||||
if file_path.exists() {
|
||||
Some(CodeContextLocation::WholeFile(file_path))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
))
|
||||
},
|
||||
move |me,
|
||||
relevant_file_paths: Result<
|
||||
Arc<HashSet<CodeContextLocation>>,
|
||||
AIApiError,
|
||||
>,
|
||||
ctx| {
|
||||
me.handle_relevant_file_paths_result(
|
||||
relevant_file_paths.map_err(|e| anyhow!(e)),
|
||||
action_id_clone,
|
||||
ctx,
|
||||
)
|
||||
},
|
||||
)
|
||||
.abort_handle();
|
||||
self.pending_requests
|
||||
.insert(action_id, RequestHandle::AbortHandle(request_abort_handle));
|
||||
);
|
||||
let file_paths = locations
|
||||
.iter()
|
||||
.map(|location| location.path().clone())
|
||||
.collect();
|
||||
self.set_relevant_file_status(
|
||||
&action_id,
|
||||
GetRelevantFilesStatus::Success { file_paths },
|
||||
);
|
||||
ctx.emit(GetRelevantFilesControllerEvent::Success {
|
||||
action_id,
|
||||
result: GetRelevantFilesControllerResult::Locations(locations),
|
||||
});
|
||||
} else {
|
||||
let outline_request = GetRelevantFiles {
|
||||
query,
|
||||
files: file_outlines
|
||||
.into_iter()
|
||||
.map(|outline| FileContextRequest {
|
||||
path: outline.path,
|
||||
symbols: outline.symbols,
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
let action_id_clone = action_id.clone();
|
||||
let request_abort_handle = ctx
|
||||
.spawn(
|
||||
async move {
|
||||
let response =
|
||||
server_api.get_relevant_files(&outline_request).await?;
|
||||
Ok(Arc::new(
|
||||
response
|
||||
.relevant_file_paths
|
||||
.into_iter()
|
||||
.filter_map(|path| {
|
||||
let file_path = base_path.join(path);
|
||||
// Validate the returned file paths.
|
||||
if file_path.exists() {
|
||||
Some(CodeContextLocation::WholeFile(file_path))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
))
|
||||
},
|
||||
move |me,
|
||||
relevant_file_paths: Result<
|
||||
Arc<HashSet<CodeContextLocation>>,
|
||||
AIApiError,
|
||||
>,
|
||||
ctx| {
|
||||
me.handle_relevant_file_paths_result(
|
||||
relevant_file_paths.map_err(|e| anyhow!(e)),
|
||||
action_id_clone,
|
||||
ctx,
|
||||
)
|
||||
},
|
||||
)
|
||||
.abort_handle();
|
||||
self.pending_requests
|
||||
.insert(action_id, RequestHandle::AbortHandle(request_abort_handle));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
Some((OutlineStatus::Pending, _)) => Err(GetRelevantFilesError::Pending),
|
||||
Some((OutlineStatus::Failed, _)) => Err(GetRelevantFilesError::CreateFailed),
|
||||
None => Err(GetRelevantFilesError::Missing),
|
||||
}
|
||||
Some((OutlineStatus::Pending, _)) => Err(GetRelevantFilesError::Pending),
|
||||
Some((OutlineStatus::Failed, _)) => Err(GetRelevantFilesError::CreateFailed),
|
||||
None => Err(GetRelevantFilesError::Missing),
|
||||
}
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
unreachable!("native local search returns before the legacy outline fallback")
|
||||
}
|
||||
|
||||
fn send_remote_request(
|
||||
@@ -375,6 +537,7 @@ impl GetRelevantFilesController {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
fn handle_relevant_file_paths_result(
|
||||
&mut self,
|
||||
relevant_file_locations: anyhow::Result<Arc<HashSet<CodeContextLocation>>>,
|
||||
@@ -386,12 +549,27 @@ impl GetRelevantFilesController {
|
||||
}
|
||||
match relevant_file_locations {
|
||||
Ok(relevant_file_locations) => {
|
||||
let file_paths = relevant_file_locations
|
||||
.iter()
|
||||
.map(|location| location.path().clone())
|
||||
.collect();
|
||||
self.set_relevant_file_status(
|
||||
&action_id,
|
||||
GetRelevantFilesStatus::Success { file_paths },
|
||||
);
|
||||
ctx.emit(GetRelevantFilesControllerEvent::Success {
|
||||
action_id,
|
||||
result: GetRelevantFilesControllerResult::Locations(relevant_file_locations),
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
let message = e.to_string();
|
||||
self.set_relevant_file_status(
|
||||
&action_id,
|
||||
GetRelevantFilesStatus::Failed {
|
||||
message: message.clone(),
|
||||
},
|
||||
);
|
||||
report_error!(anyhow!(e).context("get_relevant_files failed"));
|
||||
ctx.emit(GetRelevantFilesControllerEvent::Error { action_id });
|
||||
}
|
||||
@@ -421,15 +599,24 @@ impl GetRelevantFilesController {
|
||||
|
||||
/// Returns the path to the root directory for a codebase search where pwd is `directory`.
|
||||
pub fn root_directory_for_search(&self, directory: &Path, app: &AppContext) -> Option<PathBuf> {
|
||||
let mut start = None;
|
||||
if FeatureFlag::FullSourceCodeEmbedding.is_enabled() {
|
||||
start = CodebaseIndexManager::as_ref(app).root_path_for_codebase(directory);
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
{
|
||||
app.try_get_singleton_model_as_ref::<LocalProjectIndexManager>()
|
||||
.and_then(|manager| manager.root_for_path(directory))
|
||||
}
|
||||
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
{
|
||||
let mut start = None;
|
||||
if FeatureFlag::FullSourceCodeEmbedding.is_enabled() {
|
||||
start = CodebaseIndexManager::as_ref(app).root_path_for_codebase(directory);
|
||||
}
|
||||
start.or_else(|| {
|
||||
RepoOutlines::as_ref(app)
|
||||
.get_outline(directory)
|
||||
.map(|(_, root)| root)
|
||||
})
|
||||
}
|
||||
start.or_else(|| {
|
||||
RepoOutlines::as_ref(app)
|
||||
.get_outline(directory)
|
||||
.map(|(_, root)| root)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn root_directory_for_remote_search(
|
||||
|
||||
@@ -3,15 +3,21 @@ use std::path::{Path, PathBuf};
|
||||
use std::sync::mpsc::SyncSender;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
use ai::index::full_source_code_embedding::manager::{
|
||||
CodebaseIndexManager, CodebaseIndexManagerEvent,
|
||||
};
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
use ai::index::local_project_index::LocalProjectIndexManager;
|
||||
use ai::project_context::model::{ProjectContextModel, ProjectContextModelEvent};
|
||||
use ai::workspace::{WorkspaceMetadata, WorkspaceMetadataEvent};
|
||||
use anyhow::Context;
|
||||
use chrono::Utc;
|
||||
#[cfg(feature = "local_fs")]
|
||||
use galaxy_core::channel::ChannelState;
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
use galaxy_core::execution_mode::AppExecutionMode;
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
#[cfg(feature = "local_fs")]
|
||||
use galaxy_util::{local_or_remote_path::LocalOrRemotePath, standardized_path::StandardizedPath};
|
||||
@@ -46,6 +52,7 @@ use crate::code::lsp_telemetry::LspTelemetryEvent;
|
||||
use crate::persistence::ModelEvent;
|
||||
#[cfg(feature = "local_fs")]
|
||||
use crate::server::server_api::ServerApiProvider;
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
use crate::settings::CodeSettings;
|
||||
#[cfg(feature = "local_fs")]
|
||||
use crate::terminal::local_shell::LocalShellState;
|
||||
@@ -55,6 +62,12 @@ use crate::{report_if_error, send_telemetry_from_ctx, TelemetryEvent};
|
||||
#[cfg(feature = "local_fs")]
|
||||
use crate::{view_components::DismissibleToast, workspace::ToastStack};
|
||||
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
fn local_project_indexing_enabled(ctx: &AppContext) -> bool {
|
||||
ctx.try_get_singleton_model_as_ref::<AppExecutionMode>()
|
||||
.is_some_and(|mode| mode.local_project_indexing_enabled())
|
||||
}
|
||||
|
||||
/// Represents whether an LSP server is enabled or disabled for a workspace.
|
||||
///
|
||||
/// This is also used in underlying sqlite type persistence. We should be careful
|
||||
@@ -239,6 +252,7 @@ impl PersistedWorkspace {
|
||||
})
|
||||
.collect();
|
||||
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
if FeatureFlag::FullSourceCodeEmbedding.is_enabled() {
|
||||
ctx.subscribe_to_model(&CodebaseIndexManager::handle(ctx), |me, _, event, ctx| {
|
||||
match event {
|
||||
@@ -266,7 +280,7 @@ impl PersistedWorkspace {
|
||||
..
|
||||
} = event
|
||||
{
|
||||
#[cfg(feature = "local_fs")]
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
me.clean_up_deleted_indices(ctx);
|
||||
|
||||
me.trigger_incremental_sync_for_conversation(*terminal_surface_id, ctx);
|
||||
@@ -310,12 +324,68 @@ impl PersistedWorkspace {
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(feature = "local_fs")]
|
||||
if !cfg!(any(
|
||||
test,
|
||||
feature = "fast_dev",
|
||||
feature = "integration_tests"
|
||||
)) && CodebaseIndexManager::as_ref(ctx).is_indexing_enabled()
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
if local_project_indexing_enabled(ctx) {
|
||||
ctx.subscribe_to_model(
|
||||
&UserWorkspaces::handle(ctx),
|
||||
|me, _, user_workspaces_event, ctx| {
|
||||
if let UserWorkspacesEvent::CodebaseContextEnablementChanged =
|
||||
user_workspaces_event
|
||||
{
|
||||
me.on_settings_changed(ctx);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
if local_project_indexing_enabled(ctx) {
|
||||
ctx.subscribe_to_model(
|
||||
&BlocklistAIHistoryModel::handle(ctx),
|
||||
|me, _, event, ctx| {
|
||||
if let BlocklistAIHistoryEvent::StartedNewConversation {
|
||||
terminal_surface_id,
|
||||
..
|
||||
} = event
|
||||
{
|
||||
me.trigger_incremental_sync_for_conversation(*terminal_surface_id, ctx);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
if local_project_indexing_enabled(ctx) {
|
||||
ctx.subscribe_to_model(&ProjectContextModel::handle(ctx), |me, _, event, _ctx| {
|
||||
if let ProjectContextModelEvent::KnownRulesChanged(delta) = event {
|
||||
let mut events = vec![];
|
||||
|
||||
if !delta.discovered_rules.is_empty() {
|
||||
events.push(ModelEvent::UpsertProjectRules {
|
||||
project_rule_paths: delta.discovered_rules.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
if !delta.deleted_rules.is_empty() {
|
||||
events.push(ModelEvent::DeleteProjectRules {
|
||||
path: delta.deleted_rules.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
if !events.is_empty() {
|
||||
me.save_to_db(events);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
if local_project_indexing_enabled(ctx)
|
||||
&& !cfg!(any(
|
||||
test,
|
||||
feature = "fast_dev",
|
||||
feature = "integration_tests"
|
||||
))
|
||||
{
|
||||
ctx.subscribe_to_model(&DetectedRepositories::handle(ctx), |me, _, event, ctx| {
|
||||
let DetectedRepositoriesEvent::DetectedGitRepo { repository, .. } = event;
|
||||
@@ -634,6 +704,33 @@ impl PersistedWorkspace {
|
||||
|
||||
/// Enables or disables codebase indexing according to the setting.
|
||||
fn maybe_enable_codebase_indexing(ctx: &mut ModelContext<Self>) {
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
{
|
||||
if local_project_indexing_enabled(ctx)
|
||||
&& should_auto_index_codebase(CodebaseAutoIndexingSurface::Local, ctx)
|
||||
{
|
||||
for root in auto_index_candidate_roots(
|
||||
all_working_directories(ctx)
|
||||
.into_iter()
|
||||
.filter_map(|directory| {
|
||||
DetectedRepositories::as_ref(ctx)
|
||||
.get_root_for_path(&LocalOrRemotePath::Local(directory))
|
||||
.and_then(|root| root.to_local_path().map(Path::to_path_buf))
|
||||
}),
|
||||
|_| true,
|
||||
) {
|
||||
if let Err(error) = LocalProjectIndexManager::handle(ctx)
|
||||
.update(ctx, |manager, ctx| {
|
||||
manager.index_directory(root.to_path_buf(), ctx)
|
||||
})
|
||||
{
|
||||
log::warn!("Failed to start local project indexing: {error:#}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
CodebaseIndexManager::handle(ctx).update(ctx, |manager, ctx| {
|
||||
if !manager.is_indexing_enabled() {
|
||||
return;
|
||||
@@ -648,6 +745,7 @@ impl PersistedWorkspace {
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
fn enable_codebase_indexing(
|
||||
manager: &mut CodebaseIndexManager,
|
||||
ctx: &mut ModelContext<CodebaseIndexManager>,
|
||||
@@ -661,7 +759,7 @@ impl PersistedWorkspace {
|
||||
ctx,
|
||||
);
|
||||
|
||||
#[cfg(feature = "local_fs")]
|
||||
#[cfg(all(feature = "local_fs", target_family = "wasm"))]
|
||||
if should_auto_index_codebase(CodebaseAutoIndexingSurface::Local, ctx) {
|
||||
let roots = all_working_directories(ctx).into_iter().filter_map(|dir| {
|
||||
DetectedRepositories::as_ref(ctx)
|
||||
@@ -683,6 +781,17 @@ impl PersistedWorkspace {
|
||||
ctx,
|
||||
);
|
||||
});
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
if local_project_indexing_enabled(ctx)
|
||||
&& should_auto_index_codebase(CodebaseAutoIndexingSurface::Local, ctx)
|
||||
{
|
||||
if let Err(error) = LocalProjectIndexManager::handle(ctx).update(ctx, |manager, ctx| {
|
||||
manager.index_directory(directory_path.clone(), ctx)
|
||||
}) {
|
||||
log::warn!("Failed to start local project indexing: {error:#}");
|
||||
}
|
||||
}
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
if FeatureFlag::FullSourceCodeEmbedding.is_enabled()
|
||||
&& UserWorkspaces::as_ref(ctx).is_codebase_context_enabled(ctx)
|
||||
&& *CodeSettings::as_ref(ctx).auto_indexing_enabled
|
||||
@@ -700,6 +809,8 @@ impl PersistedWorkspace {
|
||||
/// scanning, and emits
|
||||
/// [`PersistedWorkspaceEvent::WorkspaceAdded`] so subscribers can refresh their UI.
|
||||
pub fn user_added_workspace(&mut self, path: PathBuf, ctx: &mut ModelContext<Self>) {
|
||||
#[cfg(feature = "local_fs")]
|
||||
let path = dunce::canonicalize(&path).unwrap_or(path);
|
||||
let now = Utc::now();
|
||||
|
||||
match self.workspaces.get_mut(&path) {
|
||||
@@ -850,7 +961,15 @@ impl PersistedWorkspace {
|
||||
if let Some(pwd) = pwd {
|
||||
let directory_path = PathBuf::from(pwd);
|
||||
|
||||
// Trigger an incremental sync through the CodebaseIndexManager
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
if let Err(error) = LocalProjectIndexManager::handle(ctx)
|
||||
.update(ctx, |manager, ctx| {
|
||||
manager.index_directory(directory_path.clone(), ctx)
|
||||
})
|
||||
{
|
||||
log::warn!("Failed to refresh local project index: {error:#}");
|
||||
}
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
CodebaseIndexManager::handle(ctx).update(ctx, |codebase_manager, ctx| {
|
||||
if let Err(e) = codebase_manager
|
||||
.trigger_incremental_sync_for_path(&directory_path, ctx)
|
||||
@@ -907,7 +1026,7 @@ impl PersistedWorkspace {
|
||||
}));
|
||||
}
|
||||
|
||||
#[cfg(feature = "local_fs")]
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
fn clean_up_deleted_indices(&self, ctx: &mut ModelContext<Self>) {
|
||||
CodebaseIndexManager::handle(ctx).update(ctx, |codebase_manager, ctx| {
|
||||
codebase_manager.clean_up_deleted_indices(ctx);
|
||||
@@ -1417,6 +1536,13 @@ impl PersistedWorkspace {
|
||||
}
|
||||
|
||||
fn send_active_indexed_repos_changed_telemetry<T: Entity>(ctx: &mut ModelContext<T>) {
|
||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||
let total = if local_project_indexing_enabled(ctx) {
|
||||
LocalProjectIndexManager::as_ref(ctx).statuses().count()
|
||||
} else {
|
||||
0
|
||||
};
|
||||
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||
let total = CodebaseIndexManager::as_ref(ctx).num_active_indices();
|
||||
let hit_max = AIRequestUsageModel::as_ref(ctx).hit_codebase_index_limit(total);
|
||||
send_telemetry_from_ctx!(
|
||||
|
||||
@@ -34,10 +34,11 @@ const CODE_INSTRUCTIONS: &str = r#"## Instructions
|
||||
|
||||
### Doing Tasks
|
||||
The user will primarily request software engineering tasks: solving bugs, adding features, refactoring, explaining code, and more. For these tasks:
|
||||
1. Use search tools to understand the codebase and the user's query. Search extensively — both in parallel and sequentially.
|
||||
2. Implement the solution using all tools available to you.
|
||||
3. Verify the solution if possible with tests. NEVER assume a specific test framework — check the project first.
|
||||
4. After making changes, run lint/typecheck/build commands if you know them, to ensure correctness.
|
||||
1. For source-code discovery, semantic questions, or finding an unfamiliar implementation, use `search_codebase` first. Never use `grep` to discover or search source code when `search_codebase` is available; reserve it for exact known text in non-code files. Use `file_glob` to locate filenames and `read_files` for focused follow-up context.
|
||||
2. Search extensively — both in parallel and sequentially.
|
||||
3. Implement the solution using all tools available to you.
|
||||
4. Verify the solution if possible with tests. NEVER assume a specific test framework — check the project first.
|
||||
5. After making changes, run lint/typecheck/build commands if you know them, to ensure correctness.
|
||||
|
||||
NEVER commit changes unless the user explicitly asks you to.
|
||||
|
||||
@@ -62,7 +63,7 @@ CRITICAL: You are in READ-ONLY planning mode. You MUST NOT:
|
||||
|
||||
You MAY:
|
||||
- Read files
|
||||
- Search the codebase (grep, glob)
|
||||
- Search the codebase with `search_codebase`; use `file_glob` for filenames and `grep` only for exact known text in non-code files
|
||||
- Run read-only shell commands (ls, cat, git log, git status)
|
||||
- Ask the user clarifying questions
|
||||
|
||||
@@ -116,7 +117,8 @@ const CODE_TOOL_GUIDELINES: &str = r#"## Tool Usage
|
||||
You have been given every tool you need to complete your tasks. Use them to achieve results with as few calls and as little back-and-forth as possible.
|
||||
|
||||
**How to choose tools:**
|
||||
- For reading, writing, searching, and navigating files on the local filesystem, use your filesystem tools (`read_files`, `file_glob`, `grep`, `apply_file_diffs`).
|
||||
- For source-code discovery, use `search_codebase` first. Use `file_glob` to locate filenames and `read_files` to inspect focused files. Never use `grep` to discover or search source code when `search_codebase` is available; reserve it for exact known text in non-code files.
|
||||
- For writing and editing files, use `apply_file_diffs`.
|
||||
- For running commands, installing packages, building, testing, and any shell operation, use `run_shell_command`.
|
||||
- For tasks that require interacting with external services, web UIs, or capabilities not covered by your filesystem and shell tools, use your MCP tools.
|
||||
- For complex multi-step tasks where a single script would replace many tool calls, write code (Python, Node, bash) via `run_shell_command` to reduce round-trips. But never use scripts for simple operations that a single command handles.
|
||||
@@ -140,10 +142,10 @@ If you intend to call multiple tools and there are no dependencies between the c
|
||||
|
||||
const PLAN_TOOL_GUIDELINES: &str = r#"## Tool Usage
|
||||
You have access to read-only tools for exploring the codebase:
|
||||
- `read_files` — Read file contents
|
||||
- `grep` — Search for patterns in files
|
||||
- `search_codebase` — Primary tool for source-code discovery and semantic or structural questions
|
||||
- `file_glob` — Find files by glob pattern
|
||||
- `search_codebase` — Semantic code search
|
||||
- `read_files` — Read focused file contents after discovery
|
||||
- `grep` — Search exact known text in non-code files only
|
||||
- `run_shell_command` — ONLY for read-only commands (ls, git log, git status, etc.)
|
||||
- `ask_user_question` — Ask the user for clarification
|
||||
|
||||
@@ -151,10 +153,10 @@ You MUST NOT use `apply_file_diffs`, `create_documents`, `edit_documents`, or an
|
||||
|
||||
const REVIEW_TOOL_GUIDELINES: &str = r#"## Tool Usage
|
||||
You have access to tools for examining the code under review:
|
||||
- `read_files` — Read file contents to understand context
|
||||
- `grep` — Search for patterns to find related code
|
||||
- `file_glob` — Find related files
|
||||
- `search_codebase` — Semantic search for related implementations
|
||||
- `search_codebase` — Primary tool for discovering relevant source code and understanding implementations semantically
|
||||
- `file_glob` — Find related filenames
|
||||
- `read_files` — Read focused file contents after discovery
|
||||
- `grep` — Search exact known text in non-code files, or use only as a fallback when the semantic index is unavailable
|
||||
- `run_shell_command` — For read-only commands (git diff, git log, etc.)
|
||||
|
||||
Use these tools to gather context needed for a thorough review. You should read the files being changed and their surrounding context before providing feedback."#;
|
||||
|
||||
@@ -16,6 +16,7 @@ mod prompt_builder_tests {
|
||||
assert!(prompt.system_prompt.contains("Galaxy"));
|
||||
assert!(prompt.system_prompt.contains("coding mode"));
|
||||
assert!(prompt.system_prompt.contains("/home/user/project"));
|
||||
assert!(prompt.system_prompt.contains("search_codebase` first"));
|
||||
assert!(!prompt.tools.is_empty());
|
||||
// Code mode should have apply_file_diffs
|
||||
assert!(prompt.tools.iter().any(|t| t.name == "apply_file_diffs"));
|
||||
@@ -31,6 +32,33 @@ mod prompt_builder_tests {
|
||||
assert!(!prompt.tools.iter().any(|t| t.name == "apply_file_diffs"));
|
||||
// But should have read_files
|
||||
assert!(prompt.tools.iter().any(|t| t.name == "read_files"));
|
||||
assert!(prompt.system_prompt.contains("search_codebase` first"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_semantic_search_is_advertised_before_regex_search() {
|
||||
let prompt = PromptBuilder::new(Mode::Code, Provider::Anthropic).build();
|
||||
let search_index = prompt
|
||||
.tools
|
||||
.iter()
|
||||
.position(|tool| tool.name == "search_codebase")
|
||||
.expect("search_codebase tool");
|
||||
let grep_index = prompt
|
||||
.tools
|
||||
.iter()
|
||||
.position(|tool| tool.name == "grep")
|
||||
.expect("grep tool");
|
||||
assert!(search_index < grep_index);
|
||||
assert!(prompt
|
||||
.tools
|
||||
.iter()
|
||||
.find(|tool| tool.name == "search_codebase")
|
||||
.is_some_and(|tool| tool.description.contains("all source-code discovery")));
|
||||
assert!(prompt
|
||||
.tools
|
||||
.iter()
|
||||
.find(|tool| tool.name == "grep")
|
||||
.is_some_and(|tool| tool.description.contains("non-code files")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -23,9 +23,9 @@ fn code_tools() -> Vec<ToolDefinition> {
|
||||
run_shell_command(),
|
||||
read_files(),
|
||||
apply_file_diffs(),
|
||||
grep(),
|
||||
file_glob(),
|
||||
search_codebase(),
|
||||
file_glob(),
|
||||
grep(),
|
||||
write_to_long_running_shell_command(),
|
||||
interrupt_shell_command(),
|
||||
read_shell_command_output(),
|
||||
@@ -48,9 +48,9 @@ fn code_tools() -> Vec<ToolDefinition> {
|
||||
fn plan_tools() -> Vec<ToolDefinition> {
|
||||
vec![
|
||||
read_files(),
|
||||
grep(),
|
||||
file_glob(),
|
||||
search_codebase(),
|
||||
file_glob(),
|
||||
grep(),
|
||||
run_shell_command_readonly(),
|
||||
ask_user_question(),
|
||||
start_agent(),
|
||||
@@ -64,9 +64,9 @@ fn plan_tools() -> Vec<ToolDefinition> {
|
||||
fn review_tools() -> Vec<ToolDefinition> {
|
||||
vec![
|
||||
read_files(),
|
||||
grep(),
|
||||
file_glob(),
|
||||
search_codebase(),
|
||||
file_glob(),
|
||||
grep(),
|
||||
run_shell_command_readonly(),
|
||||
]
|
||||
}
|
||||
@@ -136,7 +136,7 @@ fn apply_file_diffs() -> ToolDefinition {
|
||||
fn grep() -> ToolDefinition {
|
||||
ToolDefinition {
|
||||
name: "grep".to_string(),
|
||||
description: "Search for up to 3 focused regex patterns per call. Uses git grep in git repos (respects .gitignore) or ripgrep otherwise. Returns up to 1,000 matching line locations and reports when additional matches were omitted. Use read_files afterward to see context around matches.".to_string(),
|
||||
description: "Search for up to 3 focused regex patterns per call. Use only for exact known text in non-code files; never use this to discover or search source code when search_codebase is available. Uses git grep in git repos (respects .gitignore) or ripgrep otherwise. Returns up to 1,000 matching line locations and reports when additional matches were omitted. Use read_files afterward to see context around matches.".to_string(),
|
||||
input_schema: serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -151,7 +151,7 @@ fn grep() -> ToolDefinition {
|
||||
fn file_glob() -> ToolDefinition {
|
||||
ToolDefinition {
|
||||
name: "file_glob".to_string(),
|
||||
description: "Find files matching up to 3 focused glob patterns per call. Uses git ls-files in git repos. Returns absolute file paths of matches. Make another call for more patterns.".to_string(),
|
||||
description: "Find filenames matching up to 3 focused glob patterns per call. Use this to locate files, not to search their contents. Uses git ls-files in git repos. Returns absolute file paths of matches. Make another call for more patterns.".to_string(),
|
||||
input_schema: serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -166,7 +166,7 @@ fn file_glob() -> ToolDefinition {
|
||||
fn search_codebase() -> ToolDefinition {
|
||||
ToolDefinition {
|
||||
name: "search_codebase".to_string(),
|
||||
description: "Semantic code search across the indexed codebase. Use for finding relevant code by meaning rather than exact text match. Better than grep for conceptual queries like 'authentication logic' or 'error handling for database connections'.".to_string(),
|
||||
description: "Primary tool for discovering relevant source code in the local project index. Use this first for all source-code discovery and conceptual, semantic, or structural searches before reading files. Never use grep to search source code when this tool is available.".to_string(),
|
||||
input_schema: serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -917,13 +917,19 @@ fn build_system_prompt(
|
||||
.join(", "),
|
||||
);
|
||||
prompt.push_str(".\nNever invent tool names or parameters. Check command exit codes and tool error results before claiming success.\n");
|
||||
let has_search_codebase = tools.iter().any(|tool| tool.name == "search_codebase");
|
||||
if has_search_codebase {
|
||||
prompt.push_str(
|
||||
"For source-code discovery, semantic questions, or finding an unfamiliar implementation, MUST use `search_codebase` first. Never use `grep` to discover or search source code when `search_codebase` is available; reserve `grep` for exact known text in non-code files. Use `file_glob` only to locate filenames and `read_files` for focused follow-up context.\n",
|
||||
);
|
||||
}
|
||||
if tools.iter().any(|tool| tool.name == "run_shell_command") {
|
||||
let has_file_tools = tools
|
||||
.iter()
|
||||
.any(|tool| matches!(tool.name.as_str(), "file_glob" | "grep" | "read_files"));
|
||||
if has_file_tools {
|
||||
prompt.push_str(
|
||||
"Prefer `file_glob`, `grep`, and `read_files` for file discovery, content search, and file reading when they are available. Reserve `run_shell_command` for operations those specialized tools cannot perform; do not use shell `find`, `grep`, `rg`, `cat`, `head`, or `tail` as substitutes.\n",
|
||||
"Prefer `file_glob` for filenames and `read_files` for focused content when they are available. Reserve `run_shell_command` for operations specialized tools cannot perform; do not use shell `find`, `grep`, `rg`, `cat`, `head`, or `tail` as substitutes.\n",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,15 +154,18 @@ fn builds_a_rig_turn_directly_from_galaxy_request_state() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn system_prompt_prefers_specialized_file_tools_over_shell_substitutes() {
|
||||
fn system_prompt_prioritizes_semantic_code_search_over_regex_search() {
|
||||
let mut params = RequestParams::new_for_test();
|
||||
params.input = vec![user_query("Find every Rust file containing ProviderRun")];
|
||||
params.input = vec![user_query(
|
||||
"Find the implementation that handles ProviderRun",
|
||||
)];
|
||||
|
||||
let prepared = prepare_rig_turn(
|
||||
&config(),
|
||||
params,
|
||||
vec![
|
||||
ToolType::RunShellCommand,
|
||||
ToolType::SearchCodebase,
|
||||
ToolType::FileGlob,
|
||||
ToolType::Grep,
|
||||
ToolType::ReadFiles,
|
||||
@@ -171,7 +174,11 @@ fn system_prompt_prefers_specialized_file_tools_over_shell_substitutes() {
|
||||
);
|
||||
let prompt = prepared.request.system_prompt.expect("system prompt");
|
||||
|
||||
assert!(prompt.contains("Prefer `file_glob`, `grep`, and `read_files`"));
|
||||
assert!(prompt.contains("MUST use `search_codebase` first"));
|
||||
assert!(prompt.contains("Never use `grep` to discover or search source code"));
|
||||
assert!(
|
||||
prompt.contains("Prefer `file_glob` for filenames and `read_files` for focused content")
|
||||
);
|
||||
assert!(prompt.contains("do not use shell `find`, `grep`, `rg`, `cat`, `head`, or `tail`"));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user