Add local project indexing and search guidance

This commit is contained in:
2026-08-30 22:00:27 -05:00
parent 88c1ef9716
commit 1c7d3c175d
39 changed files with 2094 additions and 306 deletions
+125 -18
View File
@@ -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,
+6
View File
@@ -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();
+9 -4
View File
@@ -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>,
+10 -3
View File
@@ -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(
+3 -3
View File
@@ -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,
+264 -77
View File
@@ -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(
+136 -10
View File
@@ -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!(
+15 -13
View File
@@ -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."#;
+28
View File
@@ -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]
+9 -9
View File
@@ -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": {
+7 -1
View File
@@ -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",
);
}
}
+10 -3
View File
@@ -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`"));
}
@@ -1,7 +1,12 @@
use std::path::PathBuf;
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::LocalIndexStatus;
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
use ai::index::local_project_index::LocalProjectIndexManager;
use galaxyui::integration::{AssertionOutcome, StepData, TestStep};
use galaxyui::{async_assert, App, ReadModel, SingletonEntity, UpdateModel, WindowId};
use settings::Setting;
@@ -51,6 +56,11 @@ pub fn sync_current_codebase_index() -> TestStep {
};
// Kick off codebase indexing at the current directory.
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
app.update_model(&LocalProjectIndexManager::handle(app), |manager, ctx| {
let _ = manager.index_directory(canonicalized_path.clone(), ctx);
});
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
app.update_model(&CodebaseIndexManager::handle(app), |manager, ctx| {
manager.index_directory(canonicalized_path.clone(), ctx);
});
@@ -67,10 +77,29 @@ pub fn sync_current_codebase_index() -> TestStep {
.get::<String, PathBuf>(CWD_DATA_KEY.into())
.expect("No cwd");
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
let status =
app.read_model(&LocalProjectIndexManager::handle(app), |manager, _ctx| {
manager
.status_for_path(cwd)
.map(|(_, status)| status.clone())
});
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
let status = app.read_model(&CodebaseIndexManager::handle(app), |manager, ctx| {
manager.get_codebase_index_status_for_path(cwd, ctx)
});
match status {
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
Some(LocalIndexStatus::Ready { .. }) => AssertionOutcome::Success,
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
Some(status) => {
async_assert!(
matches!(status, LocalIndexStatus::Ready { .. }),
"Codebase index for {} should be ready",
cwd.display()
)
}
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
Some(status) => {
async_assert!(
status.has_synced_version()
+19 -3
View File
@@ -1155,10 +1155,15 @@ fn run_internal(mut launch_mode: LaunchMode) -> Result<()> {
.spawn(galaxy_logging::rotate_log_files())
.detach();
let local_project_indexing_enabled = matches!(
launch_mode,
LaunchMode::App { .. } | LaunchMode::CommandLine { .. } | LaunchMode::Test { .. }
);
ctx.add_singleton_model(|ctx| {
AppExecutionMode::new(
AppExecutionMode::new_with_local_project_indexing(
launch_mode.execution_mode(),
launch_mode.is_sandboxed(),
local_project_indexing_enabled,
ctx,
)
});
@@ -1988,6 +1993,14 @@ pub(crate) fn initialize_app(
} else {
ctx.add_singleton_model(|ctx| RepoOutlines::new_with_indexing_enabled(false, ctx));
}
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
if matches!(
launch_mode,
LaunchMode::App { .. } | LaunchMode::CommandLine { .. } | LaunchMode::Test { .. }
) {
ctx.add_singleton_model(::ai::index::local_project_index::LocalProjectIndexManager::new);
}
ctx.add_singleton_model(|ctx| {
galaxy_core::sync_queue::SyncQueue::<SyncTask>::new_with_rate_limit(
&ctx.background_executor(),
@@ -2215,7 +2228,10 @@ pub(crate) fn initialize_app(
ctx.add_singleton_model(DefaultTerminal::new);
ctx.add_singleton_model(|ctx| {
let should_restore_indices = launch_mode.supports_indexing()
let legacy_indexing_enabled = matches!(launch_mode, LaunchMode::RemoteServerDaemon { .. })
|| !cfg!(all(feature = "local_fs", not(target_family = "wasm")));
let should_restore_indices = legacy_indexing_enabled
&& launch_mode.supports_indexing()
&& (matches!(launch_mode, LaunchMode::RemoteServerDaemon { .. })
|| UserWorkspaces::as_ref(ctx).is_codebase_context_enabled(ctx));
let indices_to_restore = if should_restore_indices {
@@ -2231,7 +2247,7 @@ pub(crate) fn initialize_app(
codebase_limits.max_files_per_repo,
codebase_limits.embedding_generation_batch_size,
server_api_provider.as_ref(ctx).get(),
launch_mode.supports_indexing(),
launch_mode.supports_indexing() && legacy_indexing_enabled,
);
if matches!(launch_mode, LaunchMode::RemoteServerDaemon { .. }) {
codebase_index_config = codebase_index_config.defer_persisted_index_restore();
+2
View File
@@ -196,6 +196,8 @@ fn initialize_app(app: &mut App) {
app.add_singleton_model(voice_input::VoiceInput::new);
#[cfg(feature = "local_fs")]
app.add_singleton_model(RepoMetadataModel::new);
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
app.add_singleton_model(ai::index::local_project_index::LocalProjectIndexManager::new);
app.add_singleton_model(SkillManager::new);
app.add_singleton_model(FileSearchModel::new);
app.add_singleton_model(|_| crate::code_review::git_repo_model::GitRepoModels::new());
+2
View File
@@ -8,6 +8,8 @@ use cfg_if::cfg_if;
use galaxy_core::context_flag::ContextFlag;
use galaxy_core::safe_error;
use galaxy_core::user_preferences::GetUserPreferences as _;
#[cfg(feature = "voice_input")]
use galaxyui_core::event::KeyState;
use itertools::Itertools;
use lazy_static::lazy_static;
use onboarding::{
@@ -209,9 +209,9 @@ pub const OPEN_CODE_REVIEW: StaticCommand = StaticCommand {
pub const INDEX: StaticCommand = StaticCommand {
name: "/index",
description: "Index this codebase",
description: "Build or refresh the local project index",
icon_path: "bundled/svg/find-all.svg",
availability: Availability::REPOSITORY
availability: Availability::LOCAL
.union(Availability::CODEBASE_CONTEXT)
.union(Availability::AI_ENABLED),
auto_enter_ai_mode: false,
@@ -220,11 +220,9 @@ pub const INDEX: StaticCommand = StaticCommand {
pub const INIT: StaticCommand = StaticCommand {
name: "/init",
description: "Index this codebase and generate an AGENTS.md file",
description: "Build a local project index and generate an AGENTS.md file",
icon_path: "bundled/svg/warp-2.svg",
availability: Availability::REPOSITORY
.union(Availability::AGENT_VIEW)
.union(Availability::AI_ENABLED),
availability: Availability::LOCAL.union(Availability::AI_ENABLED),
auto_enter_ai_mode: true,
argument: None,
};
@@ -5,13 +5,9 @@ use std::{
};
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
use ai::index::full_source_code_embedding::manager::CodebaseIndexManager;
#[cfg(all(
feature = "local_fs",
not(target_family = "wasm"),
not(any(test, feature = "integration_tests"))
))]
use ai::index::full_source_code_embedding::manager::CodebaseIndexManagerEvent;
use ai::index::local_project_index::LocalProjectIndexEvent;
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
use ai::index::local_project_index::{LocalIndexStatus, LocalProjectIndexManager};
use galaxy_core::features::FeatureFlag;
use galaxy_core::paths::home_relative_path;
use galaxy_core::ui::theme::color::internal_colors;
@@ -129,23 +125,17 @@ impl AgentAssistedEnvironmentModal {
create_button,
};
#[cfg(all(
feature = "local_fs",
not(target_family = "wasm"),
not(any(test, feature = "integration_tests"))
))]
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
{
let index_manager = CodebaseIndexManager::handle(ctx);
let index_manager = LocalProjectIndexManager::handle(ctx);
ctx.subscribe_to_model(&index_manager, |me, _, event, ctx| {
if !me.visible {
return;
}
match event {
CodebaseIndexManagerEvent::SyncStateUpdated { .. }
| CodebaseIndexManagerEvent::NewIndexCreated { .. }
| CodebaseIndexManagerEvent::RemoveExpiredIndexMetadata { .. }
| CodebaseIndexManagerEvent::IndexMetadataUpdated { .. } => {
LocalProjectIndexEvent::StatusChanged { .. }
| LocalProjectIndexEvent::IndexRemoved { .. } => {
me.refresh_available_repos(ctx);
if me.available_repos.is_empty() {
me.maybe_start_available_repos_loading(ctx);
@@ -734,20 +724,19 @@ impl View for AgentAssistedEnvironmentModal {
fn available_indexed_repos(app: &AppContext) -> Vec<RepoEntry> {
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
{
let mut repos: Vec<RepoEntry> = CodebaseIndexManager::as_ref(app)
.get_codebase_index_statuses(app)
.filter_map(|(root, status)| {
status.has_synced_version().then(|| {
let name = root
.file_name()
.and_then(|s| s.to_str())
.map(ToOwned::to_owned)
.unwrap_or_else(|| root.to_string_lossy().into_owned());
RepoEntry {
name,
path: root.clone(),
}
})
let mut repos: Vec<RepoEntry> = LocalProjectIndexManager::as_ref(app)
.statuses()
.filter(|(_, status)| matches!(status, LocalIndexStatus::Ready { .. }))
.map(|(root, _)| {
let name = root
.file_name()
.and_then(|s| s.to_str())
.map(ToOwned::to_owned)
.unwrap_or_else(|| root.to_string_lossy().into_owned());
RepoEntry {
name,
path: root.clone(),
}
})
.collect();
@@ -1,6 +1,9 @@
use std::path::PathBuf;
#[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 galaxy_core::ui::appearance::Appearance;
use galaxyui::elements::{ChildView, Empty};
use galaxyui::platform::WindowStyle;
@@ -17,8 +20,11 @@ fn init_modal_test_models(app: &mut App) {
app.add_singleton_model(|_| Appearance::mock());
app.add_singleton_model(|_| ToastStack);
// The modal queries CodebaseIndexManager for locally indexed repos.
// Register a test instance so `available_indexed_repos(...)` doesn't panic.
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
app.add_singleton_model(|ctx| {
LocalProjectIndexManager::new_at(tempfile::tempdir().unwrap().keep(), ctx)
});
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
app.add_singleton_model(|ctx| {
CodebaseIndexManager::new_for_test(ServerApiProvider::as_ref(ctx).get(), ctx)
});
+139 -30
View File
@@ -2,11 +2,17 @@ use std::borrow::Cow;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
use ai::index::full_source_code_embedding::manager::{
CodebaseIndexFinishedStatus, CodebaseIndexManager, CodebaseIndexManagerEvent,
CodebaseIndexStatus, CodebaseIndexingError,
};
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
use ai::index::full_source_code_embedding::SyncProgress;
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
use ai::index::local_project_index::{
LocalIndexStatus, LocalProjectIndexEvent, LocalProjectIndexManager,
};
use ai::project_context::model::{ProjectContextModel, ProjectContextModelEvent};
use ai::workspace::WorkspaceMetadata;
use galaxy_core::features::FeatureFlag;
@@ -170,6 +176,44 @@ enum IndexingRefreshAction {
RequestRemote,
Resync,
}
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
fn local_status_presentation(
status: Option<&LocalIndexStatus>,
appearance: &Appearance,
) -> IndexingStatusPresentation {
let theme = appearance.theme();
match status {
None => IndexingStatusPresentation {
text: Cow::from("No index created"),
color: theme.disabled_ui_text_color().into_solid(),
icon: Some(Icon::SlashCircle),
refresh_action: None,
show_delete: false,
},
Some(LocalIndexStatus::Indexing) => IndexingStatusPresentation {
text: Cow::from("Indexing..."),
color: theme.disabled_ui_text_color().into_solid(),
icon: None,
refresh_action: None,
show_delete: true,
},
Some(LocalIndexStatus::Ready { file_count }) => IndexingStatusPresentation {
text: Cow::from(format!("Ready ({file_count} files)")),
color: theme.ansi_fg_green(),
icon: Some(Icon::Check),
refresh_action: Some(IndexingRefreshAction::Resync),
show_delete: true,
},
Some(LocalIndexStatus::Failed { .. }) => IndexingStatusPresentation {
text: Cow::from("Failed"),
color: theme.ui_error_color(),
icon: Some(Icon::AlertTriangle),
refresh_action: Some(IndexingRefreshAction::Resync),
show_delete: true,
},
}
}
pub struct CodeSettingsPageView {
page: PageType<Self>,
active_subpage: Option<CodeSubpage>,
@@ -196,34 +240,52 @@ pub struct CodeSettingsPageView {
impl CodeSettingsPageView {
pub fn new(ctx: &mut ViewContext<CodeSettingsPageView>) -> Self {
let index_manager = CodebaseIndexManager::handle(ctx);
let codebase_count = index_manager
.as_ref(ctx)
.get_codebase_index_statuses(ctx)
.count();
ctx.subscribe_to_model(&index_manager, |me, index, event, ctx| {
if matches!(
event,
CodebaseIndexManagerEvent::SyncStateUpdated { .. }
| CodebaseIndexManagerEvent::NewIndexCreated { .. }
) {
let codebase_count = index.as_ref(ctx).get_codebase_index_statuses(ctx).count();
// Only update mouse states if the number of codebases changed
if me.codebase_manual_resync_mouse_states.len() != codebase_count {
// Resize the vector to match the new codebase count, but preserve the existing mouse states
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
let codebase_count = {
let index_manager = LocalProjectIndexManager::handle(ctx);
let count = index_manager.as_ref(ctx).statuses().count();
ctx.subscribe_to_model(&index_manager, |me, index, event, ctx| {
if matches!(
event,
LocalProjectIndexEvent::StatusChanged { .. }
| LocalProjectIndexEvent::IndexRemoved { .. }
) {
let count = index.as_ref(ctx).statuses().count();
me.codebase_manual_resync_mouse_states
.resize_with(codebase_count, Default::default);
.resize_with(count, Default::default);
me.codebase_delete_mouse_states
.resize_with(codebase_count, Default::default);
.resize_with(count, Default::default);
me.resize_workspace_mouse_states(ctx);
ctx.notify();
}
});
count
};
me.resize_workspace_mouse_states(ctx);
ctx.notify();
}
});
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
let codebase_count = {
let index_manager = CodebaseIndexManager::handle(ctx);
let count = index_manager
.as_ref(ctx)
.get_codebase_index_statuses(ctx)
.count();
ctx.subscribe_to_model(&index_manager, |me, index, event, ctx| {
if matches!(
event,
CodebaseIndexManagerEvent::SyncStateUpdated { .. }
| CodebaseIndexManagerEvent::NewIndexCreated { .. }
) {
let count = index.as_ref(ctx).get_codebase_index_statuses(ctx).count();
me.codebase_manual_resync_mouse_states
.resize_with(count, Default::default);
me.codebase_delete_mouse_states
.resize_with(count, Default::default);
me.resize_workspace_mouse_states(ctx);
ctx.notify();
}
});
count
};
#[cfg(not(target_family = "wasm"))]
let remote_codebase_count = {
@@ -552,6 +614,18 @@ impl CodeSettingsPageView {
if let Some(directory_path) = paths.first() {
let path = PathBuf::from(directory_path);
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
PersistedWorkspace::handle(ctx).update(ctx, |workspace, ctx| {
workspace.user_added_workspace(path.clone(), ctx);
if let Err(error) = LocalProjectIndexManager::handle(ctx)
.update(ctx, |manager, ctx| {
manager.index_directory(path.clone(), 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| {
manager.index_directory(path, ctx);
});
@@ -697,11 +771,25 @@ impl TypedActionView for CodeSettingsPageView {
ctx.notify();
}
CodeSettingsPageAction::ManualResync(repo_path) => {
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
if let Err(error) = LocalProjectIndexManager::handle(ctx)
.update(ctx, |manager, ctx| {
manager.index_directory(repo_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, |manager, ctx| {
manager.try_manual_resync_codebase(repo_path, ctx);
});
}
CodeSettingsPageAction::DeleteIndex(repo_path) => {
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
LocalProjectIndexManager::handle(ctx).update(ctx, |manager, ctx| {
manager.remove_index_for_path(repo_path.clone(), ctx);
});
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
CodebaseIndexManager::handle(ctx).update(ctx, |manager, ctx| {
manager.drop_index(repo_path.clone(), ctx);
});
@@ -1112,6 +1200,7 @@ impl CodePageWidget {
),
];
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
if codebase_indexing_enabled && !CodebaseIndexManager::as_ref(app).can_create_new_indices()
{
rows.push(self.render_settings_subtext(
@@ -1372,7 +1461,10 @@ impl CodePageWidget {
Vec::new()
};
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
let codebase_manager = CodebaseIndexManager::as_ref(app);
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
let local_index_manager = LocalProjectIndexManager::as_ref(app);
let lsp_manager = LspManagerModel::as_ref(app);
let persisted_workspace = PersistedWorkspace::as_ref(app);
@@ -1383,6 +1475,11 @@ impl CodePageWidget {
let workspace_path = &workspace.path;
// Get codebase index status if it exists
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
let local_status = local_index_manager
.status_for_path(workspace_path)
.map(|(_, status)| status);
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
let index_status =
codebase_manager.get_codebase_index_status_for_path(workspace_path, app);
@@ -1403,7 +1500,17 @@ impl CodePageWidget {
.unwrap_or_default();
// Skip workspaces that have neither an index nor any LSP servers
if index_status.is_none() && all_servers.is_empty() {
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
let (has_index, index_presentation) = (
local_status.is_some(),
local_status_presentation(local_status, appearance),
);
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
let (has_index, index_presentation) = (
index_status.is_some(),
self.local_indexing_status_presentation(index_status.as_ref(), appearance),
);
if !has_index && all_servers.is_empty() {
continue;
}
@@ -1431,7 +1538,7 @@ impl CodePageWidget {
content.add_child(self.render_workspace_row(
workspace_path,
index_status.as_ref(),
index_presentation,
&all_servers,
lsp_manager,
resync_mouse,
@@ -1485,7 +1592,7 @@ impl CodePageWidget {
fn render_workspace_row(
&self,
workspace_path: &Path,
index_status: Option<&CodebaseIndexStatus>,
index_presentation: IndexingStatusPresentation,
all_servers: &[(LSPServerType, EnablementState)],
lsp_manager: &LspManagerModel,
resync_mouse: MouseStateHandle,
@@ -1568,7 +1675,7 @@ impl CodePageWidget {
// Indexing section (always rendered per design)
workspace_content.add_child(self.render_indexing_subsection(
workspace_path,
index_status,
index_presentation,
resync_mouse,
delete_mouse,
appearance,
@@ -1671,13 +1778,13 @@ impl CodePageWidget {
fn render_indexing_subsection(
&self,
workspace_path: &Path,
index_status: Option<&CodebaseIndexStatus>,
presentation: IndexingStatusPresentation,
resync_mouse: MouseStateHandle,
delete_mouse: MouseStateHandle,
appearance: &Appearance,
) -> Box<dyn Element> {
self.render_indexing_subsection_for_target(
self.local_indexing_status_presentation(index_status, appearance),
presentation,
Some(LocalOrRemotePath::Local(workspace_path.to_path_buf())),
resync_mouse,
delete_mouse,
@@ -1730,6 +1837,7 @@ impl CodePageWidget {
column.finish()
}
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
fn local_indexing_status_presentation(
&self,
index_state: Option<&CodebaseIndexStatus>,
@@ -2569,6 +2677,7 @@ impl SettingsWidget for CodebaseIndexingCategorizedWidget {
Some(AUTO_INDEX_DESCRIPTION.into()),
));
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
if !CodebaseIndexManager::as_ref(app).can_create_new_indices() {
content.add_child(
ui_builder
@@ -1,9 +1,12 @@
use std::collections::HashSet;
use std::path::{Path, PathBuf};
#[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::{LocalProjectIndexEvent, LocalProjectIndexManager};
use galaxy_util::path::user_friendly_path;
use galaxyui::elements::{
Border, ChildView, ConstrainedBox, Container, CrossAxisAlignment, Flex, Hoverable,
@@ -76,6 +79,19 @@ pub(super) enum DirectoryColorAddPickerEvent {
impl DirectoryColorAddPicker {
pub(super) fn new(ctx: &mut ViewContext<Self>) -> Self {
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
let local_index_manager = LocalProjectIndexManager::handle(ctx);
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
ctx.subscribe_to_model(&local_index_manager, |me, _, event, ctx| {
if matches!(
event,
LocalProjectIndexEvent::StatusChanged { .. }
| LocalProjectIndexEvent::IndexRemoved { .. }
) {
me.refresh_items(ctx);
}
});
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
ctx.subscribe_to_model(&CodebaseIndexManager::handle(ctx), |me, _, event, ctx| {
// Refresh for any event that may change the set of indexed codebase paths or
// persisted workspaces: new index created, sync state updated (which covers
@@ -190,6 +206,12 @@ impl DirectoryColorAddPicker {
}
fn refresh_items(&mut self, ctx: &mut ViewContext<Self>) {
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
let indexed_paths: HashSet<PathBuf> = LocalProjectIndexManager::as_ref(ctx)
.statuses()
.map(|(path, _)| path.clone())
.collect();
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
let indexed_paths: HashSet<PathBuf> = CodebaseIndexManager::as_ref(ctx)
.get_codebase_paths()
.cloned()
+2
View File
@@ -276,6 +276,8 @@ pub fn initialize_app(app: &mut App) {
app.add_singleton_model(RepoMetadataModel::new);
app.add_singleton_model(FileSearchModel::new);
app.add_singleton_model(RepoOutlines::new_for_test);
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
app.add_singleton_model(ai::index::local_project_index::LocalProjectIndexManager::new);
#[cfg(feature = "voice_input")]
app.add_singleton_model(voice_input::VoiceInput::new);
app.add_singleton_model(|ctx| {
+79 -24
View File
@@ -71,6 +71,8 @@ use action::RememberForWormholing;
pub use action::{AgentOnboardingVersion, OnboardingIntention, OnboardingVersion, TerminalAction};
use ai::api_keys::{ApiKeyManager, AwsCredentialsState};
use ai::index::full_source_code_embedding::manager::{BuildSource, CodebaseIndexManager};
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
use ai::index::local_project_index::LocalProjectIndexManager;
use async_channel::{Receiver, Sender};
use base64::Engine as _;
use block_banner::{render_wormholing_banner, WormholeBannerState};
@@ -7362,6 +7364,15 @@ impl TerminalView {
BlocklistAIActionEvent::InitProject(_) => {
self.on_next_conversation_finished(|me, _reason, ctx| {
if let Some(path) = me.pwd() {
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
if let Err(error) = LocalProjectIndexManager::handle(ctx)
.update(ctx, |manager, ctx| {
manager.index_directory(PathBuf::from(path), 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| {
manager.index_directory(PathBuf::from(path), ctx);
});
@@ -10619,7 +10630,16 @@ impl TerminalView {
});
}
// Index the codebase
// Index the project.
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
if let Err(error) = LocalProjectIndexManager::handle(ctx)
.update(ctx, |manager, ctx| {
manager.index_directory(banner_state.repo_path.clone(), 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| {
manager.index_directory(banner_state.repo_path.clone(), ctx);
});
@@ -13835,6 +13855,13 @@ impl TerminalView {
self.update_focused_terminal_info(ctx);
if let Some(working_directory) = self.active_session_path_if_local(ctx) {
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
if let Err(error) = LocalProjectIndexManager::handle(ctx).update(ctx, |manager, ctx| {
manager.index_directory(working_directory.clone(), 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| {
manager.handle_session_bootstrapped(&working_directory);
});
@@ -14200,10 +14227,7 @@ impl TerminalView {
return;
}
let Some(pwd_path) = self
.pwd()
.and_then(|pwd| Path::new(&pwd).canonicalize().ok())
else {
let Some(pwd_path) = self.active_session_path_if_local(ctx) else {
return;
};
@@ -14246,6 +14270,18 @@ impl TerminalView {
me.redetermine_terminal_focus(ctx);
}
InitProjectModelEvent::StepCompleted(_) => {}
InitProjectModelEvent::CodebaseIndexFailed(message) => {
let window_id = ctx.window_id();
ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| {
toast_stack.add_ephemeral_toast(
DismissibleToast::error(format!(
"Local project indexing failed: {message}"
)),
window_id,
ctx,
);
});
}
InitProjectModelEvent::Cancelled => {
me.active_init_project_model = None;
// Mark conversation as cancelled
@@ -26002,6 +26038,13 @@ impl TerminalView {
return;
};
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
if let Err(error) = LocalProjectIndexManager::handle(ctx).update(ctx, |manager, ctx| {
manager.index_directory(active_session_path.clone(), 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| {
manager.build_and_sync_codebase_index(
BuildSource::FromPath(active_session_path.as_path()),
@@ -26011,7 +26054,7 @@ impl TerminalView {
}
fn write_codebase_index(&self, _ctx: &mut ViewContext<Self>) {
#[cfg(feature = "local_fs")]
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
{
let Some(working_directory_str) = self.pwd() else {
log::error!("No working directory found for terminal session");
@@ -27431,25 +27474,37 @@ impl TypedActionView for TerminalView {
}
SummarizeConversation => self.summarize_conversation(ctx),
IndexProjectSpeedbump => {
let codebase_context_enabled =
UserWorkspaces::as_ref(ctx).is_codebase_context_enabled(ctx);
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
if let Some(repo_path) = self
.active_session_path_if_local(ctx)
.and_then(|path| dunce::canonicalize(path).ok())
{
if let Err(error) = LocalProjectIndexManager::handle(ctx)
.update(ctx, |manager, ctx| {
manager.index_directory(repo_path.clone(), ctx)
})
{
log::warn!("Failed to start local project indexing: {error:#}");
}
self.remove_codebase_index_speedbump_banner(ctx);
self.insert_codebase_index_speedbump_banner(
repo_path, true, /* show_is_indexing */
ctx,
);
}
if FeatureFlag::FullSourceCodeEmbedding.is_enabled() && codebase_context_enabled {
#[cfg(feature = "local_fs")]
if let Some(current_dir) = self.pwd() {
let directory = PathBuf::from(&current_dir);
if let Ok(repo_path) = directory.canonicalize() {
// Start indexing the codebase
CodebaseIndexManager::handle(ctx).update(ctx, |manager, ctx| {
manager.index_directory(repo_path.clone(), ctx);
});
self.remove_codebase_index_speedbump_banner(ctx);
self.insert_codebase_index_speedbump_banner(
repo_path, true, /* show_is_indexing */
ctx,
);
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
{
let codebase_context_enabled =
UserWorkspaces::as_ref(ctx).is_codebase_context_enabled(ctx);
if codebase_context_enabled {
if let Some(current_dir) = self.pwd() {
let directory = PathBuf::from(&current_dir);
if let Ok(repo_path) = directory.canonicalize() {
CodebaseIndexManager::handle(ctx).update(ctx, |manager, ctx| {
manager.index_directory(repo_path.clone(), ctx);
});
}
}
}
}
+26 -7
View File
@@ -3,7 +3,10 @@ pub mod model;
use std::path::{Path, PathBuf};
#[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 galaxy_core::ui::theme::Fill;
use lsp::supported_servers::LSPServerType;
use lsp_server_selector::{create_lsp_server_selector, LSPServerInfo};
@@ -40,7 +43,7 @@ use crate::view_components::DismissibleToast;
use crate::workspace::ToastStack;
use crate::{send_telemetry_from_ctx, TelemetryEvent};
const ONBOARDING_TEXT: &str = "Great - let's begin setting up this project! Would you like to give me permission to index this codebase? It allows me to quickly understand context and provide more targeted solutions when working in this codebase. No code is stored on Galaxy servers.";
const ONBOARDING_TEXT: &str = "Great - let's begin setting up this project! Would you like to let Galaxy build a local project index? It allows me to quickly understand context and provide more targeted solutions while keeping the index on your device.";
const ALREADY_SETUP_TEXT: &str = "It looks like this project has already been initialized. You can re-generate the GALAXY.md for this codebase by clicking the button below.";
// Native Galaxy rules file format.
pub const FILES_TO_CHECK: [&str; 4] = ["GALAXY.md", "AGENTS.md", "WARP.md", "CLAUDE.md"];
@@ -644,10 +647,11 @@ impl InitStepBlock {
.render(app)
.finish()
}
InitStepStatus::Running => {
// Codebase context doesn't have a "running" state
Empty::new().finish()
}
InitStepStatus::Running => RenderableAction::new("Building local project index", app)
.with_icon(in_progress_icon(Appearance::as_ref(app)).finish())
.with_content_item_spacing()
.render(app)
.finish(),
InitStepStatus::Completed(result) => {
self.render_completed_codebase_context(result, app)
}
@@ -1053,15 +1057,30 @@ impl TypedActionView for InitStepBlock {
fn handle_action(&mut self, action: &Self::Action, ctx: &mut ViewContext<Self>) {
match action {
InitProjectBlockAction::IndexCodebase(directory) => {
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
if let Err(error) = LocalProjectIndexManager::handle(ctx)
.update(ctx, |manager, ctx| {
manager.index_directory(directory.clone(), ctx)
})
{
log::warn!("Failed to start local project indexing: {error:#}");
return;
}
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
CodebaseIndexManager::handle(ctx).update(ctx, |manager, ctx| {
manager.index_directory(directory.clone(), ctx);
});
send_telemetry_from_ctx!(
TelemetryEvent::AgentModeSetupCodebaseContextAction {
action: AgentModeSetupCodebaseContextActionType::IndexCodebase,
},
ctx
);
CodebaseIndexManager::handle(ctx).update(ctx, |manager, ctx| {
manager.index_directory(directory.clone(), ctx);
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
self.model.update(ctx, |model, ctx| {
model.mark_step_running(InitStepKind::CodebaseContext, ctx);
});
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
self.model.update(ctx, |model, ctx| {
model.mark_step_completed(
InitStepKind::CodebaseContext,
+87 -12
View File
@@ -1,6 +1,11 @@
use std::path::{Path, PathBuf};
#[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::{
LocalIndexStatus, LocalProjectIndexEvent, LocalProjectIndexManager,
};
use ai::project_context::model::ProjectContextModel;
use enum_iterator::Sequence;
use galaxy_util::local_or_remote_path::LocalOrRemotePath;
@@ -10,7 +15,6 @@ use lsp::supported_servers::LSPServerType;
use repo_metadata::repositories::DetectedRepositories;
use crate::ai::persisted_workspace::PersistedWorkspace;
use crate::settings::CodeSettings;
use crate::terminal::view::init_project::lsp_server_selector::LSPServerInfo;
use crate::terminal::view::init_project::{
CodebaseIndexingResult, CreateEnvironmentResult, InitActionResult, LanguageServersResult,
@@ -124,7 +128,11 @@ impl InitProjectModel {
path_env_var: Option<String>,
ctx: &mut ModelContext<Self>,
) -> Self {
let is_already_setup = !Self::should_have_available_steps(&pwd_path, ctx);
#[cfg(feature = "local_fs")]
let root_path = dunce::canonicalize(&pwd_path).unwrap_or(pwd_path);
#[cfg(not(feature = "local_fs"))]
let root_path = pwd_path;
let is_already_setup = !Self::should_have_available_steps(&root_path, ctx);
Self {
steps: [None, None, None, None, None],
@@ -132,7 +140,7 @@ impl InitProjectModel {
is_cancelled: false,
is_already_setup,
#[cfg(feature = "local_fs")]
root_path: pwd_path,
root_path,
path_env_var,
}
}
@@ -154,6 +162,17 @@ impl InitProjectModel {
);
// Start async computations for subsequent steps
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
{
let local_index_manager = LocalProjectIndexManager::handle(ctx);
ctx.subscribe_to_model(&local_index_manager, |model, _, event, ctx| {
if let LocalProjectIndexEvent::StatusChanged { root_path, status } = event {
if root_path == &model.root_path {
model.handle_local_index_status(status, ctx);
}
}
});
}
self.compute_codebase_context_step(&pwd_path, ctx);
if self.path_env_var.is_some() {
self.compute_language_servers_step(&pwd_path, ctx);
@@ -176,14 +195,12 @@ impl InitProjectModel {
/// Check if there are any steps that need user action
pub fn should_have_available_steps(path: &Path, ctx: &galaxyui::AppContext) -> bool {
// Note that we consider auto-indexing setting to true to satisfy the codebase context step.
// This avoids the potential race condition with the banner showing just when we start auto-indexing.
// /init is an explicit user request, so its indexing step must remain available even
// when background auto-indexing is enabled. The auto-index setting only controls whether
// navigation/startup starts indexing without this explicit action.
let has_pending_codebase_context = UserWorkspaces::as_ref(ctx)
.is_codebase_context_enabled(ctx)
&& CodebaseIndexManager::as_ref(ctx)
.get_codebase_index_status_for_path(path, ctx)
.is_none()
&& !*CodeSettings::as_ref(ctx).auto_indexing_enabled;
&& !Self::codebase_index_ready(path, ctx);
let has_pending_project_scoped_rules = ProjectContextModel::as_ref(ctx)
.find_applicable_project_rules(&LocalOrRemotePath::Local(path.to_path_buf()))
@@ -192,6 +209,18 @@ impl InitProjectModel {
has_pending_codebase_context || has_pending_project_scoped_rules
}
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
fn codebase_index_ready(path: &Path, ctx: &galaxyui::AppContext) -> bool {
LocalProjectIndexManager::as_ref(ctx).is_ready_for_path(path)
}
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
fn codebase_index_ready(path: &Path, ctx: &galaxyui::AppContext) -> bool {
CodebaseIndexManager::as_ref(ctx)
.get_codebase_index_status_for_path(path, ctx)
.is_some()
}
pub fn get_step(&self, kind: InitStepKind) -> Option<&InitStep> {
self.steps
.get(kind as usize)
@@ -368,9 +397,12 @@ impl InitProjectModel {
return;
}
let codebase_index_manager = CodebaseIndexManager::handle(ctx);
let is_indexed = codebase_index_manager
.as_ref(ctx)
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
let is_indexed = LocalProjectIndexManager::as_ref(ctx)
.status_for_path(pwd_path)
.is_some_and(|(_, status)| matches!(status, LocalIndexStatus::Ready { .. }));
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
let is_indexed = CodebaseIndexManager::as_ref(ctx)
.get_codebase_index_status_for_path(pwd_path, ctx)
.is_some();
@@ -397,6 +429,47 @@ impl InitProjectModel {
}
}
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
fn handle_local_index_status(
&mut self,
status: &LocalIndexStatus,
ctx: &mut ModelContext<Self>,
) {
if !matches!(
self.get_step(InitStepKind::CodebaseContext)
.map(|step| &step.status),
Some(InitStepStatus::Running)
) {
return;
}
let root_path = self.root_path.clone();
let Some(step) = self.get_step_mut(InitStepKind::CodebaseContext) else {
return;
};
match status {
LocalIndexStatus::Indexing => {
step.status = InitStepStatus::Running;
ctx.notify();
}
LocalIndexStatus::Ready { .. } => {
step.status = InitStepStatus::Completed(InitActionResult::CodebaseContext(
CodebaseIndexingResult::Accepted,
));
ctx.emit(InitProjectModelEvent::StepCompleted(
InitStepKind::CodebaseContext,
));
self.maybe_emit_next_step(ctx);
}
LocalIndexStatus::Failed { message } => {
step.status = InitStepStatus::Ready(InitStepData::CodebaseContext {
pwd_path: root_path,
});
ctx.notify();
ctx.emit(InitProjectModelEvent::CodebaseIndexFailed(message.clone()));
}
}
}
fn compute_language_servers_step(&mut self, pwd_path: &Path, ctx: &mut ModelContext<Self>) {
// Start as Pending
self.set_step(
@@ -584,6 +657,8 @@ pub enum InitProjectModelEvent {
InitCompleted,
/// Trigger AGENTS.md generation slash command
GenerateProjectRules,
/// The local index failed and can be retried from the same step.
CodebaseIndexFailed(String),
/// Trigger AGENTS.md regeneration
RegenerateProjectRules,
/// View codebase context status
+2
View File
@@ -156,6 +156,8 @@ pub fn initialize_app_for_terminal_view(app: &mut App) {
model
});
app.add_singleton_model(FileSearchModel::new);
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
app.add_singleton_model(ai::index::local_project_index::LocalProjectIndexManager::new);
app.add_singleton_model(|_| GitRepoModels::new());
app.add_singleton_model(RepoOutlines::new_for_test);
app.add_singleton_model(HomeDirectoryWatcher::new_for_test);
+2
View File
@@ -231,6 +231,8 @@ pub(crate) fn initialize_app(app: &mut App) {
#[cfg(feature = "local_fs")]
app.add_singleton_model(RepoMetadataModel::new);
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
app.add_singleton_model(ai::index::local_project_index::LocalProjectIndexManager::new);
app.add_singleton_model(search::files::model::FileSearchModel::new);
#[cfg(windows)]