Merge branch 'feat/project-indexing'
This commit is contained in:
Generated
+1
@@ -275,6 +275,7 @@ dependencies = [
|
|||||||
"uuid",
|
"uuid",
|
||||||
"virtual-fs",
|
"virtual-fs",
|
||||||
"warp_multi_agent_api",
|
"warp_multi_agent_api",
|
||||||
|
"warp_search_core",
|
||||||
"watcher",
|
"watcher",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,14 @@ use std::path::{Path, PathBuf};
|
|||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||||
use ai::index::full_source_code_embedding::manager::{
|
use ai::index::full_source_code_embedding::manager::{
|
||||||
CodebaseIndexManager, CodebaseIndexManagerEvent,
|
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::channel::oneshot;
|
||||||
use futures::future::join_all;
|
use futures::future::join_all;
|
||||||
use galaxy_cli::agent::Harness;
|
use galaxy_cli::agent::Harness;
|
||||||
@@ -90,6 +95,9 @@ pub fn prepare_environment(
|
|||||||
if should_subscribe_to_index_updates && result.is_err() {
|
if should_subscribe_to_index_updates && result.is_err() {
|
||||||
let _ = spawner
|
let _ = spawner
|
||||||
.spawn(|_, ctx| {
|
.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));
|
ctx.unsubscribe_from_model(&CodebaseIndexManager::handle(ctx));
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
@@ -201,6 +209,9 @@ async fn prepare_environment_impl(
|
|||||||
} else if should_index_codebase && source_repos.is_empty() {
|
} else if should_index_codebase && source_repos.is_empty() {
|
||||||
let _ = spawner
|
let _ = spawner
|
||||||
.spawn(|_, ctx| {
|
.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));
|
ctx.unsubscribe_from_model(&CodebaseIndexManager::handle(ctx));
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
@@ -235,6 +246,9 @@ fn record_codebase_indexing(
|
|||||||
setup_events.record_value_detached(SetupStep::EnvironmentCodebaseIndexing, async move {
|
setup_events.record_value_detached(SetupStep::EnvironmentCodebaseIndexing, async move {
|
||||||
let _ = spawner
|
let _ = spawner
|
||||||
.spawn(|_, ctx| {
|
.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));
|
ctx.unsubscribe_from_model(&CodebaseIndexManager::handle(ctx));
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
@@ -255,6 +269,9 @@ fn record_codebase_indexing(
|
|||||||
}
|
}
|
||||||
let _ = spawner
|
let _ = spawner
|
||||||
.spawn(|_, ctx| {
|
.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));
|
ctx.unsubscribe_from_model(&CodebaseIndexManager::handle(ctx));
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
@@ -489,6 +506,7 @@ pub(super) async fn register_cloned_repo(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||||
async fn subscribe_to_codebase_index_events(
|
async fn subscribe_to_codebase_index_events(
|
||||||
spawner: &ModelSpawner<TerminalDriver>,
|
spawner: &ModelSpawner<TerminalDriver>,
|
||||||
repo_channels: Arc<Mutex<HashMap<PathBuf, oneshot::Sender<()>>>>,
|
repo_channels: Arc<Mutex<HashMap<PathBuf, oneshot::Sender<()>>>>,
|
||||||
@@ -496,11 +514,10 @@ async fn subscribe_to_codebase_index_events(
|
|||||||
spawner
|
spawner
|
||||||
.spawn(move |_, ctx| {
|
.spawn(move |_, ctx| {
|
||||||
let repo_channels = Arc::clone(&repo_channels);
|
let repo_channels = Arc::clone(&repo_channels);
|
||||||
ctx.subscribe_to_model(&CodebaseIndexManager::handle(ctx), move |_, _, event, ctx| {
|
ctx.subscribe_to_model(
|
||||||
if !matches!(
|
&CodebaseIndexManager::handle(ctx),
|
||||||
event,
|
move |_, _, event, ctx| {
|
||||||
CodebaseIndexManagerEvent::SyncStateUpdated { .. }
|
if !matches!(event, CodebaseIndexManagerEvent::SyncStateUpdated { .. }) {
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -511,22 +528,15 @@ async fn subscribe_to_codebase_index_events(
|
|||||||
.expect("repo channel map lock should not be poisoned");
|
.expect("repo channel map lock should not be poisoned");
|
||||||
|
|
||||||
for repo in channels.keys() {
|
for repo in channels.keys() {
|
||||||
let Some(status) =
|
let Some(status) = manager.get_codebase_index_status_for_path(repo, ctx)
|
||||||
manager.get_codebase_index_status_for_path(repo, ctx)
|
|
||||||
else {
|
else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
if status.has_synced_version() {
|
if status.has_synced_version()
|
||||||
repos_to_notify.push(repo.clone());
|
|| (!status.has_pending()
|
||||||
continue;
|
&& status.last_sync_successful() == Some(false))
|
||||||
}
|
{
|
||||||
|
|
||||||
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")
|
|
||||||
);
|
|
||||||
repos_to_notify.push(repo.clone());
|
repos_to_notify.push(repo.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -536,12 +546,58 @@ async fn subscribe_to_codebase_index_events(
|
|||||||
let _ = tx.send(());
|
let _ = tx.send(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|_| PrepareEnvironmentError::InvalidRuntimeState)
|
.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))]
|
#[tracing::instrument(skip_all, err, fields(tags.cloud_agent = true, repo = %repo_name))]
|
||||||
async fn index_repo_codebase(
|
async fn index_repo_codebase(
|
||||||
repo_name: &str,
|
repo_name: &str,
|
||||||
@@ -592,6 +648,57 @@ async fn index_repo_codebase(
|
|||||||
.map_err(|_| PrepareEnvironmentError::InvalidRuntimeState)
|
.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.
|
/// Execute a command in the context of a terminal session.
|
||||||
async fn execute_command(
|
async fn execute_command(
|
||||||
command: String,
|
command: String,
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ use std::path::Path;
|
|||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||||
use ai::index::full_source_code_embedding::manager::CodebaseIndexManager;
|
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 markdown_parser::FormattedTextFragment;
|
||||||
use rand::seq::SliceRandom;
|
use rand::seq::SliceRandom;
|
||||||
use warpui::keymap::Keystroke;
|
use warpui::keymap::Keystroke;
|
||||||
@@ -411,6 +414,9 @@ impl AITip for AgentTip {
|
|||||||
else {
|
else {
|
||||||
return true;
|
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)
|
return CodebaseIndexManager::as_ref(app)
|
||||||
.get_codebase_index_status_for_path(root, app)
|
.get_codebase_index_status_for_path(root, app)
|
||||||
.is_none();
|
.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);
|
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 [
|
if [
|
||||||
"read_files",
|
"read_files",
|
||||||
"apply_file_diffs",
|
"apply_file_diffs",
|
||||||
@@ -1569,7 +1574,7 @@ pub fn extract_system_prompt(
|
|||||||
.any(has_tool)
|
.any(has_tool)
|
||||||
{
|
{
|
||||||
prompt.push_str(
|
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 \
|
it. Use paths rooted in the working directory and absolute paths when a schema requires \
|
||||||
them.\n",
|
them.\n",
|
||||||
);
|
);
|
||||||
@@ -1783,7 +1788,7 @@ pub fn default_tool_definitions() -> Vec<ToolDefinition> {
|
|||||||
},
|
},
|
||||||
ToolDefinition {
|
ToolDefinition {
|
||||||
name: "grep".to_string(),
|
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!({
|
input_schema: serde_json::json!({
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -1795,7 +1800,7 @@ pub fn default_tool_definitions() -> Vec<ToolDefinition> {
|
|||||||
},
|
},
|
||||||
ToolDefinition {
|
ToolDefinition {
|
||||||
name: "file_glob".to_string(),
|
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!({
|
input_schema: serde_json::json!({
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -1807,7 +1812,7 @@ pub fn default_tool_definitions() -> Vec<ToolDefinition> {
|
|||||||
},
|
},
|
||||||
ToolDefinition {
|
ToolDefinition {
|
||||||
name: "search_codebase".to_string(),
|
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!({
|
input_schema: serde_json::json!({
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"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]
|
#[test]
|
||||||
fn direct_provider_advertises_local_tool_history_recall() {
|
fn direct_provider_advertises_local_tool_history_recall() {
|
||||||
let request = api::Request {
|
let request = api::Request {
|
||||||
|
|||||||
@@ -226,6 +226,7 @@ impl GetFilesExecutor {
|
|||||||
GetRelevantFilesError::Missing => {
|
GetRelevantFilesError::Missing => {
|
||||||
"The current directory isn't within a git repository, which is necessary to search for relevant files.".to_owned()
|
"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(
|
ActionExecution::Sync(AIAgentActionResultType::GetFiles(
|
||||||
GetFilesResult::Error(error_message),
|
GetFilesResult::Error(error_message),
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::{Path, PathBuf};
|
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::channel::oneshot;
|
||||||
use futures::future::BoxFuture;
|
use futures::future::BoxFuture;
|
||||||
use futures::FutureExt;
|
use futures::FutureExt;
|
||||||
@@ -273,6 +275,7 @@ impl SearchCodebaseExecutor {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
self.active_searches.remove(id);
|
||||||
log::warn!("Failed to send remote get_relevant_files request: {e:?}");
|
log::warn!("Failed to send remote get_relevant_files request: {e:?}");
|
||||||
ActionExecution::Sync(AIAgentActionResultType::SearchCodebase(
|
ActionExecution::Sync(AIAgentActionResultType::SearchCodebase(
|
||||||
SearchCodebaseResult::Failed {
|
SearchCodebaseResult::Failed {
|
||||||
@@ -385,6 +388,7 @@ impl SearchCodebaseExecutor {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
self.active_searches.remove(id);
|
||||||
log::warn!("Failed to send get_relevant_files request for directory: {e:?}");
|
log::warn!("Failed to send get_relevant_files request for directory: {e:?}");
|
||||||
|
|
||||||
let error_message = match e {
|
let error_message = match e {
|
||||||
@@ -397,6 +401,7 @@ impl SearchCodebaseExecutor {
|
|||||||
GetRelevantFilesError::Missing => {
|
GetRelevantFilesError::Missing => {
|
||||||
"The current directory isn't within a git repository, which is necessary to search for relevant files.".to_owned()
|
"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(
|
ActionExecution::Sync(AIAgentActionResultType::SearchCodebase(
|
||||||
SearchCodebaseResult::Failed {
|
SearchCodebaseResult::Failed {
|
||||||
@@ -465,6 +470,13 @@ impl SearchCodebaseExecutor {
|
|||||||
pwd
|
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
|
self.get_relevant_files_controller
|
||||||
.as_ref(app)
|
.as_ref(app)
|
||||||
.root_directory_for_search(&search_dir, app)
|
.root_directory_for_search(&search_dir, app)
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ use crate::ui_components::blended_colors;
|
|||||||
use crate::ui_components::icons::Icon;
|
use crate::ui_components::icons::Icon;
|
||||||
|
|
||||||
const SPEEDBUMP_HEADER: &str = "Index Codebase?";
|
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
|
/// Uniform padding around the banner
|
||||||
const PADDING: f32 = 12.;
|
const PADDING: f32 = 12.;
|
||||||
/// Text for the button that allows execution
|
/// Text for the button that allows execution
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
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 chrono::Local;
|
||||||
use galaxy_core::features::FeatureFlag;
|
use galaxy_core::features::FeatureFlag;
|
||||||
use galaxy_graphql::generic_string_object::GenericStringObjectFormat as GraphQLFormat;
|
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));
|
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() {
|
if session_context.is_remote() {
|
||||||
add_remote_codebase_context(&mut context, &session_context, app);
|
add_remote_codebase_context(&mut context, &session_context, app);
|
||||||
} else {
|
} else {
|
||||||
@@ -91,27 +94,28 @@ pub(super) fn input_context_for_request(
|
|||||||
context.into()
|
context.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||||
fn add_local_codebase_context(context: &mut Vec<AIAgentContext>, app: &AppContext) {
|
fn add_local_codebase_context(context: &mut Vec<AIAgentContext>, app: &AppContext) {
|
||||||
for (codebase_path, status) in
|
for (codebase_path, status) in LocalProjectIndexManager::as_ref(app).statuses() {
|
||||||
CodebaseIndexManager::as_ref(app).get_codebase_index_statuses(app)
|
if matches!(
|
||||||
{
|
status,
|
||||||
// TODO(daniel): We should figure out a mechanism for handling stale codebases.
|
ai::index::local_project_index::LocalIndexStatus::Ready { .. }
|
||||||
if status.has_synced_version() {
|
) {
|
||||||
// For now, we pass the name of the directory as the name of the
|
|
||||||
// codebase.
|
|
||||||
let codebase_name = codebase_path
|
let codebase_name = codebase_path
|
||||||
.file_name()
|
.file_name()
|
||||||
.map(|name| name.to_string_lossy())
|
.map(|name| name.to_string_lossy())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
context.push(AIAgentContext::Codebase {
|
context.push(AIAgentContext::Codebase {
|
||||||
name: codebase_name.into(),
|
name: codebase_name.into(),
|
||||||
path: codebase_path.to_string_lossy().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"))]
|
#[cfg(not(target_family = "wasm"))]
|
||||||
fn add_remote_codebase_context(
|
fn add_remote_codebase_context(
|
||||||
context: &mut Vec<AIAgentContext>,
|
context: &mut Vec<AIAgentContext>,
|
||||||
|
|||||||
@@ -47,10 +47,17 @@ fn codebase_indexing_enabled(
|
|||||||
surface: CodebaseAutoIndexingSurface,
|
surface: CodebaseAutoIndexingSurface,
|
||||||
codebase_context_enabled: bool,
|
codebase_context_enabled: bool,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
|
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()
|
FeatureFlag::FullSourceCodeEmbedding.is_enabled()
|
||||||
&& surface.required_feature_enabled()
|
&& surface.required_feature_enabled()
|
||||||
&& codebase_context_enabled
|
&& codebase_context_enabled
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn codebase_auto_indexing_enabled(
|
pub(crate) fn codebase_auto_indexing_enabled(
|
||||||
surface: CodebaseAutoIndexingSurface,
|
surface: CodebaseAutoIndexingSurface,
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[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);
|
let _flag = FeatureFlag::FullSourceCodeEmbedding.override_enabled(false);
|
||||||
assert!(!codebase_auto_indexing_enabled(
|
assert!(codebase_auto_indexing_enabled(
|
||||||
CodebaseAutoIndexingSurface::Local,
|
CodebaseAutoIndexingSurface::Local,
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
let _flag = FeatureFlag::FullSourceCodeEmbedding.override_enabled(true);
|
let _flag = FeatureFlag::FullSourceCodeEmbedding.override_enabled(false);
|
||||||
assert!(codebase_auto_indexing_enabled(
|
assert!(codebase_auto_indexing_enabled(
|
||||||
CodebaseAutoIndexingSurface::Local,
|
CodebaseAutoIndexingSurface::Local,
|
||||||
true,
|
true,
|
||||||
|
|||||||
@@ -2,25 +2,38 @@ use std::collections::HashSet;
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||||
use ai::index::full_source_code_embedding::manager::{
|
use ai::index::full_source_code_embedding::manager::{
|
||||||
CodebaseIndexManager, CodebaseIndexManagerEvent,
|
CodebaseIndexManager, CodebaseIndexManagerEvent,
|
||||||
};
|
};
|
||||||
|
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||||
use ai::index::full_source_code_embedding::RetrievalID;
|
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;
|
use ai::index::locations::CodeContextLocation;
|
||||||
|
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use futures_util::stream::AbortHandle;
|
use futures_util::stream::AbortHandle;
|
||||||
use galaxy_core::features::FeatureFlag;
|
use galaxy_core::features::FeatureFlag;
|
||||||
use galaxyui::{AppContext, Entity, ModelContext, ModelHandle, SingletonEntity};
|
use galaxyui::{AppContext, Entity, ModelContext, ModelHandle, SingletonEntity};
|
||||||
|
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||||
use instant::Instant;
|
use instant::Instant;
|
||||||
|
|
||||||
#[cfg(not(target_family = "wasm"))]
|
#[cfg(not(target_family = "wasm"))]
|
||||||
use crate::ai::agent::SearchCodebaseFailureReason;
|
use crate::ai::agent::SearchCodebaseFailureReason;
|
||||||
use crate::ai::agent::{AIAgentActionId, SearchCodebaseResult};
|
use crate::ai::agent::{AIAgentActionId, SearchCodebaseResult};
|
||||||
use crate::ai::blocklist::SessionContext;
|
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};
|
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};
|
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::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(not(target_family = "wasm"), path = "remote_search/native.rs")]
|
||||||
#[cfg_attr(target_family = "wasm", path = "remote_search/wasm.rs")]
|
#[cfg_attr(target_family = "wasm", path = "remote_search/wasm.rs")]
|
||||||
mod remote_search;
|
mod remote_search;
|
||||||
@@ -60,6 +73,20 @@ pub enum GetRelevantFilesRequestTarget {
|
|||||||
requested_codebase_path: Option<String>,
|
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)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum GetRelevantFilesError {
|
pub enum GetRelevantFilesError {
|
||||||
#[error("Repo outline is still being computed.")]
|
#[error("Repo outline is still being computed.")]
|
||||||
@@ -68,15 +95,18 @@ pub enum GetRelevantFilesError {
|
|||||||
CreateFailed,
|
CreateFailed,
|
||||||
#[error("Failed to create outline.")]
|
#[error("Failed to create outline.")]
|
||||||
Missing,
|
Missing,
|
||||||
|
#[error("Local project index is unavailable: {0}")]
|
||||||
|
LocalIndexUnavailable(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This enum allows us to use both the existing structure for outline-based indexing
|
/// This enum allows us to use both the existing structure for outline-based indexing
|
||||||
/// and the new full source code indexing manager/model.
|
/// and the new full source code indexing manager/model.
|
||||||
enum RequestHandle {
|
enum RequestHandle {
|
||||||
/// Used with outline-based indexing.
|
/// Used with outline-based indexing and remote search.
|
||||||
AbortHandle(AbortHandle),
|
AbortHandle(AbortHandle),
|
||||||
|
|
||||||
/// Used with full source code indexing.
|
/// Used with full source code indexing.
|
||||||
|
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||||
RetrievalID {
|
RetrievalID {
|
||||||
repo_path: PathBuf,
|
repo_path: PathBuf,
|
||||||
retrieval_id: RetrievalID,
|
retrieval_id: RetrievalID,
|
||||||
@@ -88,6 +118,7 @@ impl RequestHandle {
|
|||||||
fn abort(&mut self, ctx: &mut AppContext) {
|
fn abort(&mut self, ctx: &mut AppContext) {
|
||||||
match self {
|
match self {
|
||||||
RequestHandle::AbortHandle(abort_handle) => abort_handle.abort(),
|
RequestHandle::AbortHandle(abort_handle) => abort_handle.abort(),
|
||||||
|
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||||
RequestHandle::RetrievalID {
|
RequestHandle::RetrievalID {
|
||||||
repo_path,
|
repo_path,
|
||||||
retrieval_id,
|
retrieval_id,
|
||||||
@@ -112,15 +143,63 @@ pub struct GetRelevantFilesController {
|
|||||||
/// This allows several SearchCodebase actions to be active at once without newer requests
|
/// This allows several SearchCodebase actions to be active at once without newer requests
|
||||||
/// cancelling unrelated older ones.
|
/// cancelling unrelated older ones.
|
||||||
pending_requests: std::collections::HashMap<AIAgentActionId, RequestHandle>,
|
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 {
|
impl GetRelevantFilesController {
|
||||||
pub fn new(ctx: &mut ModelContext<Self>) -> Self {
|
pub fn new(ctx: &mut ModelContext<Self>) -> Self {
|
||||||
|
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||||
|
{
|
||||||
let codebase_manager = CodebaseIndexManager::handle(ctx);
|
let codebase_manager = CodebaseIndexManager::handle(ctx);
|
||||||
ctx.subscribe_to_model(&codebase_manager, Self::handle_codebase_manager_event);
|
ctx.subscribe_to_model(&codebase_manager, Self::handle_codebase_manager_event);
|
||||||
|
}
|
||||||
Self::default()
|
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(
|
fn pending_request_details_for_retrieval_id(
|
||||||
&self,
|
&self,
|
||||||
pending_retrieval_id: &RetrievalID,
|
pending_retrieval_id: &RetrievalID,
|
||||||
@@ -140,6 +219,7 @@ impl GetRelevantFilesController {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||||
fn handle_codebase_manager_event(
|
fn handle_codebase_manager_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: ModelHandle<CodebaseIndexManager>,
|
_: ModelHandle<CodebaseIndexManager>,
|
||||||
@@ -213,7 +293,38 @@ impl GetRelevantFilesController {
|
|||||||
self.cancel_request_for_action(&action_id, ctx);
|
self.cancel_request_for_action(&action_id, ctx);
|
||||||
match target {
|
match target {
|
||||||
GetRelevantFilesRequestTarget::Local { directory } => {
|
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 {
|
GetRelevantFilesRequestTarget::Remote {
|
||||||
session_context,
|
session_context,
|
||||||
@@ -237,8 +348,45 @@ impl GetRelevantFilesController {
|
|||||||
action_id: AIAgentActionId,
|
action_id: AIAgentActionId,
|
||||||
ctx: &mut ModelContext<Self>,
|
ctx: &mut ModelContext<Self>,
|
||||||
) -> Result<(), GetRelevantFilesError> {
|
) -> 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() {
|
if FeatureFlag::FullSourceCodeEmbedding.is_enabled() {
|
||||||
let codebase_mgr = CodebaseIndexManager::handle(ctx);
|
let codebase_mgr = CodebaseIndexManager::handle(ctx);
|
||||||
if let Some(base_path) = codebase_mgr.as_ref(ctx).root_path_for_codebase(directory) {
|
if let Some(base_path) = codebase_mgr.as_ref(ctx).root_path_for_codebase(directory) {
|
||||||
@@ -268,22 +416,33 @@ impl GetRelevantFilesController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||||
|
{
|
||||||
match RepoOutlines::as_ref(ctx).get_outline(directory) {
|
match RepoOutlines::as_ref(ctx).get_outline(directory) {
|
||||||
Some((OutlineStatus::Complete(outline), base_path)) => {
|
Some((OutlineStatus::Complete(outline), base_path)) => {
|
||||||
let server_api = ServerApiProvider::as_ref(ctx).get();
|
let server_api = ServerApiProvider::as_ref(ctx).get();
|
||||||
|
|
||||||
let file_outlines = outline.to_file_symbols(partial_path_segments);
|
let file_outlines = outline.to_file_symbols(partial_path_segments);
|
||||||
if file_outlines.len() < MINIMUM_FILE_COUNT_FOR_API_CALL {
|
if file_outlines.len() < MINIMUM_FILE_COUNT_FOR_API_CALL {
|
||||||
ctx.emit(GetRelevantFilesControllerEvent::Success {
|
let locations: Arc<HashSet<CodeContextLocation>> = Arc::new(
|
||||||
action_id,
|
|
||||||
result: GetRelevantFilesControllerResult::Locations(Arc::new(
|
|
||||||
file_outlines
|
file_outlines
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|file| {
|
.map(|file| {
|
||||||
CodeContextLocation::WholeFile(PathBuf::from(file.path))
|
CodeContextLocation::WholeFile(PathBuf::from(file.path))
|
||||||
})
|
})
|
||||||
.collect(),
|
.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(locations),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
let outline_request = GetRelevantFiles {
|
let outline_request = GetRelevantFiles {
|
||||||
@@ -342,6 +501,9 @@ impl GetRelevantFilesController {
|
|||||||
None => Err(GetRelevantFilesError::Missing),
|
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(
|
fn send_remote_request(
|
||||||
&mut self,
|
&mut self,
|
||||||
@@ -375,6 +537,7 @@ impl GetRelevantFilesController {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||||
fn handle_relevant_file_paths_result(
|
fn handle_relevant_file_paths_result(
|
||||||
&mut self,
|
&mut self,
|
||||||
relevant_file_locations: anyhow::Result<Arc<HashSet<CodeContextLocation>>>,
|
relevant_file_locations: anyhow::Result<Arc<HashSet<CodeContextLocation>>>,
|
||||||
@@ -386,12 +549,27 @@ impl GetRelevantFilesController {
|
|||||||
}
|
}
|
||||||
match relevant_file_locations {
|
match relevant_file_locations {
|
||||||
Ok(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 {
|
ctx.emit(GetRelevantFilesControllerEvent::Success {
|
||||||
action_id,
|
action_id,
|
||||||
result: GetRelevantFilesControllerResult::Locations(relevant_file_locations),
|
result: GetRelevantFilesControllerResult::Locations(relevant_file_locations),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Err(e) => {
|
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"));
|
report_error!(anyhow!(e).context("get_relevant_files failed"));
|
||||||
ctx.emit(GetRelevantFilesControllerEvent::Error { action_id });
|
ctx.emit(GetRelevantFilesControllerEvent::Error { action_id });
|
||||||
}
|
}
|
||||||
@@ -421,6 +599,14 @@ impl GetRelevantFilesController {
|
|||||||
|
|
||||||
/// Returns the path to the root directory for a codebase search where pwd is `directory`.
|
/// 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> {
|
pub fn root_directory_for_search(&self, directory: &Path, app: &AppContext) -> Option<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))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||||
|
{
|
||||||
let mut start = None;
|
let mut start = None;
|
||||||
if FeatureFlag::FullSourceCodeEmbedding.is_enabled() {
|
if FeatureFlag::FullSourceCodeEmbedding.is_enabled() {
|
||||||
start = CodebaseIndexManager::as_ref(app).root_path_for_codebase(directory);
|
start = CodebaseIndexManager::as_ref(app).root_path_for_codebase(directory);
|
||||||
@@ -431,6 +617,7 @@ impl GetRelevantFilesController {
|
|||||||
.map(|(_, root)| root)
|
.map(|(_, root)| root)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn root_directory_for_remote_search(
|
pub fn root_directory_for_remote_search(
|
||||||
&self,
|
&self,
|
||||||
|
|||||||
+9
-1
@@ -2742,7 +2742,15 @@ fn openai_model_context_window(
|
|||||||
is_configurable: max_context_size > default_context_size,
|
is_configurable: max_context_size > default_context_size,
|
||||||
min: default_context_size,
|
min: default_context_size,
|
||||||
max: max_context_size,
|
max: max_context_size,
|
||||||
default_max: default_context_size,
|
// ChatGPT's catalog exposes `context_window` as the effective window
|
||||||
|
// and `max_context_window` as the model's actual maximum. Use the
|
||||||
|
// latter as the default so model refreshes do not reset the profile
|
||||||
|
// to the smaller effective window (for example, 272k).
|
||||||
|
default_max: if provider_kind == OpenAIProviderKind::ChatGPTSubscription {
|
||||||
|
max_context_size
|
||||||
|
} else {
|
||||||
|
default_context_size
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -813,7 +813,7 @@ fn chatgpt_codex_models_parse_visible_catalog_entries() {
|
|||||||
);
|
);
|
||||||
assert!(configurable.is_configurable);
|
assert!(configurable.is_configurable);
|
||||||
assert_eq!(configurable.min, 258_400);
|
assert_eq!(configurable.min, 258_400);
|
||||||
assert_eq!(configurable.default_max, 258_400);
|
assert_eq!(configurable.default_max, 828_400);
|
||||||
assert_eq!(configurable.max, 828_400);
|
assert_eq!(configurable.max, 828_400);
|
||||||
|
|
||||||
let fixed =
|
let fixed =
|
||||||
@@ -829,6 +829,8 @@ fn chatgpt_catalog_merge_drops_stale_models_but_preserves_overrides() {
|
|||||||
let mut existing = openai_model("gpt-5.6-sol");
|
let mut existing = openai_model("gpt-5.6-sol");
|
||||||
existing.enabled = false;
|
existing.enabled = false;
|
||||||
existing.use_rig = false;
|
existing.use_rig = false;
|
||||||
|
existing.context_size = 272_000;
|
||||||
|
existing.max_input_tokens = Some(272_000);
|
||||||
existing.capability_overrides.insert(
|
existing.capability_overrides.insert(
|
||||||
"vision".to_string(),
|
"vision".to_string(),
|
||||||
crate::settings::ModelCapabilityOverride::Unsupported,
|
crate::settings::ModelCapabilityOverride::Unsupported,
|
||||||
@@ -839,11 +841,15 @@ fn chatgpt_catalog_merge_drops_stale_models_but_preserves_overrides() {
|
|||||||
discovered.display_name = "GPT-5.6-Sol".to_string();
|
discovered.display_name = "GPT-5.6-Sol".to_string();
|
||||||
discovered.vision_supported = true;
|
discovered.vision_supported = true;
|
||||||
discovered.use_rig = true;
|
discovered.use_rig = true;
|
||||||
|
discovered.context_size = 828_400;
|
||||||
|
discovered.max_input_tokens = Some(258_400);
|
||||||
|
|
||||||
let merged = merge_discovered_chatgpt_subscription_models(&[existing, stale], vec![discovered]);
|
let merged = merge_discovered_chatgpt_subscription_models(&[existing, stale], vec![discovered]);
|
||||||
|
|
||||||
assert_eq!(merged.len(), 1);
|
assert_eq!(merged.len(), 1);
|
||||||
assert_eq!(merged[0].model_id, "gpt-5.6-sol");
|
assert_eq!(merged[0].model_id, "gpt-5.6-sol");
|
||||||
|
assert_eq!(merged[0].context_size, 828_400);
|
||||||
|
assert_eq!(merged[0].max_input_tokens, Some(258_400));
|
||||||
assert!(!merged[0].enabled);
|
assert!(!merged[0].enabled);
|
||||||
assert!(!merged[0].use_rig);
|
assert!(!merged[0].use_rig);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
@@ -3,15 +3,21 @@ use std::path::{Path, PathBuf};
|
|||||||
use std::sync::mpsc::SyncSender;
|
use std::sync::mpsc::SyncSender;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||||
use ai::index::full_source_code_embedding::manager::{
|
use ai::index::full_source_code_embedding::manager::{
|
||||||
CodebaseIndexManager, CodebaseIndexManagerEvent,
|
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::project_context::model::{ProjectContextModel, ProjectContextModelEvent};
|
||||||
use ai::workspace::{WorkspaceMetadata, WorkspaceMetadataEvent};
|
use ai::workspace::{WorkspaceMetadata, WorkspaceMetadataEvent};
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
#[cfg(feature = "local_fs")]
|
#[cfg(feature = "local_fs")]
|
||||||
use galaxy_core::channel::ChannelState;
|
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;
|
use galaxy_core::features::FeatureFlag;
|
||||||
#[cfg(feature = "local_fs")]
|
#[cfg(feature = "local_fs")]
|
||||||
use galaxy_util::{local_or_remote_path::LocalOrRemotePath, standardized_path::StandardizedPath};
|
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;
|
use crate::persistence::ModelEvent;
|
||||||
#[cfg(feature = "local_fs")]
|
#[cfg(feature = "local_fs")]
|
||||||
use crate::server::server_api::ServerApiProvider;
|
use crate::server::server_api::ServerApiProvider;
|
||||||
|
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||||
use crate::settings::CodeSettings;
|
use crate::settings::CodeSettings;
|
||||||
#[cfg(feature = "local_fs")]
|
#[cfg(feature = "local_fs")]
|
||||||
use crate::terminal::local_shell::LocalShellState;
|
use crate::terminal::local_shell::LocalShellState;
|
||||||
@@ -55,6 +62,12 @@ use crate::{report_if_error, send_telemetry_from_ctx, TelemetryEvent};
|
|||||||
#[cfg(feature = "local_fs")]
|
#[cfg(feature = "local_fs")]
|
||||||
use crate::{view_components::DismissibleToast, workspace::ToastStack};
|
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.
|
/// 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
|
/// This is also used in underlying sqlite type persistence. We should be careful
|
||||||
@@ -239,6 +252,7 @@ impl PersistedWorkspace {
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||||
if FeatureFlag::FullSourceCodeEmbedding.is_enabled() {
|
if FeatureFlag::FullSourceCodeEmbedding.is_enabled() {
|
||||||
ctx.subscribe_to_model(&CodebaseIndexManager::handle(ctx), |me, _, event, ctx| {
|
ctx.subscribe_to_model(&CodebaseIndexManager::handle(ctx), |me, _, event, ctx| {
|
||||||
match event {
|
match event {
|
||||||
@@ -266,7 +280,7 @@ impl PersistedWorkspace {
|
|||||||
..
|
..
|
||||||
} = event
|
} = event
|
||||||
{
|
{
|
||||||
#[cfg(feature = "local_fs")]
|
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||||
me.clean_up_deleted_indices(ctx);
|
me.clean_up_deleted_indices(ctx);
|
||||||
|
|
||||||
me.trigger_incremental_sync_for_conversation(*terminal_surface_id, ctx);
|
me.trigger_incremental_sync_for_conversation(*terminal_surface_id, ctx);
|
||||||
@@ -310,12 +324,68 @@ impl PersistedWorkspace {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "local_fs")]
|
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||||
if !cfg!(any(
|
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,
|
test,
|
||||||
feature = "fast_dev",
|
feature = "fast_dev",
|
||||||
feature = "integration_tests"
|
feature = "integration_tests"
|
||||||
)) && CodebaseIndexManager::as_ref(ctx).is_indexing_enabled()
|
))
|
||||||
{
|
{
|
||||||
ctx.subscribe_to_model(&DetectedRepositories::handle(ctx), |me, _, event, ctx| {
|
ctx.subscribe_to_model(&DetectedRepositories::handle(ctx), |me, _, event, ctx| {
|
||||||
let DetectedRepositoriesEvent::DetectedGitRepo { repository, .. } = event;
|
let DetectedRepositoriesEvent::DetectedGitRepo { repository, .. } = event;
|
||||||
@@ -634,6 +704,33 @@ impl PersistedWorkspace {
|
|||||||
|
|
||||||
/// Enables or disables codebase indexing according to the setting.
|
/// Enables or disables codebase indexing according to the setting.
|
||||||
fn maybe_enable_codebase_indexing(ctx: &mut ModelContext<Self>) {
|
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| {
|
CodebaseIndexManager::handle(ctx).update(ctx, |manager, ctx| {
|
||||||
if !manager.is_indexing_enabled() {
|
if !manager.is_indexing_enabled() {
|
||||||
return;
|
return;
|
||||||
@@ -648,6 +745,7 @@ impl PersistedWorkspace {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||||
fn enable_codebase_indexing(
|
fn enable_codebase_indexing(
|
||||||
manager: &mut CodebaseIndexManager,
|
manager: &mut CodebaseIndexManager,
|
||||||
ctx: &mut ModelContext<CodebaseIndexManager>,
|
ctx: &mut ModelContext<CodebaseIndexManager>,
|
||||||
@@ -661,7 +759,7 @@ impl PersistedWorkspace {
|
|||||||
ctx,
|
ctx,
|
||||||
);
|
);
|
||||||
|
|
||||||
#[cfg(feature = "local_fs")]
|
#[cfg(all(feature = "local_fs", target_family = "wasm"))]
|
||||||
if should_auto_index_codebase(CodebaseAutoIndexingSurface::Local, ctx) {
|
if should_auto_index_codebase(CodebaseAutoIndexingSurface::Local, ctx) {
|
||||||
let roots = all_working_directories(ctx).into_iter().filter_map(|dir| {
|
let roots = all_working_directories(ctx).into_iter().filter_map(|dir| {
|
||||||
DetectedRepositories::as_ref(ctx)
|
DetectedRepositories::as_ref(ctx)
|
||||||
@@ -683,6 +781,17 @@ impl PersistedWorkspace {
|
|||||||
ctx,
|
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()
|
if FeatureFlag::FullSourceCodeEmbedding.is_enabled()
|
||||||
&& UserWorkspaces::as_ref(ctx).is_codebase_context_enabled(ctx)
|
&& UserWorkspaces::as_ref(ctx).is_codebase_context_enabled(ctx)
|
||||||
&& *CodeSettings::as_ref(ctx).auto_indexing_enabled
|
&& *CodeSettings::as_ref(ctx).auto_indexing_enabled
|
||||||
@@ -700,6 +809,8 @@ impl PersistedWorkspace {
|
|||||||
/// scanning, and emits
|
/// scanning, and emits
|
||||||
/// [`PersistedWorkspaceEvent::WorkspaceAdded`] so subscribers can refresh their UI.
|
/// [`PersistedWorkspaceEvent::WorkspaceAdded`] so subscribers can refresh their UI.
|
||||||
pub fn user_added_workspace(&mut self, path: PathBuf, ctx: &mut ModelContext<Self>) {
|
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();
|
let now = Utc::now();
|
||||||
|
|
||||||
match self.workspaces.get_mut(&path) {
|
match self.workspaces.get_mut(&path) {
|
||||||
@@ -850,7 +961,15 @@ impl PersistedWorkspace {
|
|||||||
if let Some(pwd) = pwd {
|
if let Some(pwd) = pwd {
|
||||||
let directory_path = PathBuf::from(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| {
|
CodebaseIndexManager::handle(ctx).update(ctx, |codebase_manager, ctx| {
|
||||||
if let Err(e) = codebase_manager
|
if let Err(e) = codebase_manager
|
||||||
.trigger_incremental_sync_for_path(&directory_path, ctx)
|
.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>) {
|
fn clean_up_deleted_indices(&self, ctx: &mut ModelContext<Self>) {
|
||||||
CodebaseIndexManager::handle(ctx).update(ctx, |codebase_manager, ctx| {
|
CodebaseIndexManager::handle(ctx).update(ctx, |codebase_manager, ctx| {
|
||||||
codebase_manager.clean_up_deleted_indices(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>) {
|
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 total = CodebaseIndexManager::as_ref(ctx).num_active_indices();
|
||||||
let hit_max = AIRequestUsageModel::as_ref(ctx).hit_codebase_index_limit(total);
|
let hit_max = AIRequestUsageModel::as_ref(ctx).hit_codebase_index_limit(total);
|
||||||
send_telemetry_from_ctx!(
|
send_telemetry_from_ctx!(
|
||||||
|
|||||||
@@ -34,10 +34,11 @@ const CODE_INSTRUCTIONS: &str = r#"## Instructions
|
|||||||
|
|
||||||
### Doing Tasks
|
### Doing Tasks
|
||||||
The user will primarily request software engineering tasks: solving bugs, adding features, refactoring, explaining code, and more. For these 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.
|
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. Implement the solution using all tools available to you.
|
2. Search extensively — both in parallel and sequentially.
|
||||||
3. Verify the solution if possible with tests. NEVER assume a specific test framework — check the project first.
|
3. Implement the solution using all tools available to you.
|
||||||
4. After making changes, run lint/typecheck/build commands if you know them, to ensure correctness.
|
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.
|
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:
|
You MAY:
|
||||||
- Read files
|
- 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)
|
- Run read-only shell commands (ls, cat, git log, git status)
|
||||||
- Ask the user clarifying questions
|
- 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.
|
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:**
|
**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 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 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.
|
- 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
|
const PLAN_TOOL_GUIDELINES: &str = r#"## Tool Usage
|
||||||
You have access to read-only tools for exploring the codebase:
|
You have access to read-only tools for exploring the codebase:
|
||||||
- `read_files` — Read file contents
|
- `search_codebase` — Primary tool for source-code discovery and semantic or structural questions
|
||||||
- `grep` — Search for patterns in files
|
|
||||||
- `file_glob` — Find files by glob pattern
|
- `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.)
|
- `run_shell_command` — ONLY for read-only commands (ls, git log, git status, etc.)
|
||||||
- `ask_user_question` — Ask the user for clarification
|
- `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
|
const REVIEW_TOOL_GUIDELINES: &str = r#"## Tool Usage
|
||||||
You have access to tools for examining the code under review:
|
You have access to tools for examining the code under review:
|
||||||
- `read_files` — Read file contents to understand context
|
- `search_codebase` — Primary tool for discovering relevant source code and understanding implementations semantically
|
||||||
- `grep` — Search for patterns to find related code
|
- `file_glob` — Find related filenames
|
||||||
- `file_glob` — Find related files
|
- `read_files` — Read focused file contents after discovery
|
||||||
- `search_codebase` — Semantic search for related implementations
|
- `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.)
|
- `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."#;
|
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("Galaxy"));
|
||||||
assert!(prompt.system_prompt.contains("coding mode"));
|
assert!(prompt.system_prompt.contains("coding mode"));
|
||||||
assert!(prompt.system_prompt.contains("/home/user/project"));
|
assert!(prompt.system_prompt.contains("/home/user/project"));
|
||||||
|
assert!(prompt.system_prompt.contains("search_codebase` first"));
|
||||||
assert!(!prompt.tools.is_empty());
|
assert!(!prompt.tools.is_empty());
|
||||||
// Code mode should have apply_file_diffs
|
// Code mode should have apply_file_diffs
|
||||||
assert!(prompt.tools.iter().any(|t| t.name == "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"));
|
assert!(!prompt.tools.iter().any(|t| t.name == "apply_file_diffs"));
|
||||||
// But should have read_files
|
// But should have read_files
|
||||||
assert!(prompt.tools.iter().any(|t| t.name == "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]
|
#[test]
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ fn code_tools() -> Vec<ToolDefinition> {
|
|||||||
run_shell_command(),
|
run_shell_command(),
|
||||||
read_files(),
|
read_files(),
|
||||||
apply_file_diffs(),
|
apply_file_diffs(),
|
||||||
grep(),
|
|
||||||
file_glob(),
|
|
||||||
search_codebase(),
|
search_codebase(),
|
||||||
|
file_glob(),
|
||||||
|
grep(),
|
||||||
write_to_long_running_shell_command(),
|
write_to_long_running_shell_command(),
|
||||||
interrupt_shell_command(),
|
interrupt_shell_command(),
|
||||||
read_shell_command_output(),
|
read_shell_command_output(),
|
||||||
@@ -48,9 +48,9 @@ fn code_tools() -> Vec<ToolDefinition> {
|
|||||||
fn plan_tools() -> Vec<ToolDefinition> {
|
fn plan_tools() -> Vec<ToolDefinition> {
|
||||||
vec![
|
vec![
|
||||||
read_files(),
|
read_files(),
|
||||||
grep(),
|
|
||||||
file_glob(),
|
|
||||||
search_codebase(),
|
search_codebase(),
|
||||||
|
file_glob(),
|
||||||
|
grep(),
|
||||||
run_shell_command_readonly(),
|
run_shell_command_readonly(),
|
||||||
ask_user_question(),
|
ask_user_question(),
|
||||||
start_agent(),
|
start_agent(),
|
||||||
@@ -64,9 +64,9 @@ fn plan_tools() -> Vec<ToolDefinition> {
|
|||||||
fn review_tools() -> Vec<ToolDefinition> {
|
fn review_tools() -> Vec<ToolDefinition> {
|
||||||
vec![
|
vec![
|
||||||
read_files(),
|
read_files(),
|
||||||
grep(),
|
|
||||||
file_glob(),
|
|
||||||
search_codebase(),
|
search_codebase(),
|
||||||
|
file_glob(),
|
||||||
|
grep(),
|
||||||
run_shell_command_readonly(),
|
run_shell_command_readonly(),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -136,7 +136,7 @@ fn apply_file_diffs() -> ToolDefinition {
|
|||||||
fn grep() -> ToolDefinition {
|
fn grep() -> ToolDefinition {
|
||||||
ToolDefinition {
|
ToolDefinition {
|
||||||
name: "grep".to_string(),
|
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!({
|
input_schema: serde_json::json!({
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -151,7 +151,7 @@ fn grep() -> ToolDefinition {
|
|||||||
fn file_glob() -> ToolDefinition {
|
fn file_glob() -> ToolDefinition {
|
||||||
ToolDefinition {
|
ToolDefinition {
|
||||||
name: "file_glob".to_string(),
|
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!({
|
input_schema: serde_json::json!({
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -166,7 +166,7 @@ fn file_glob() -> ToolDefinition {
|
|||||||
fn search_codebase() -> ToolDefinition {
|
fn search_codebase() -> ToolDefinition {
|
||||||
ToolDefinition {
|
ToolDefinition {
|
||||||
name: "search_codebase".to_string(),
|
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!({
|
input_schema: serde_json::json!({
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -917,13 +917,19 @@ fn build_system_prompt(
|
|||||||
.join(", "),
|
.join(", "),
|
||||||
);
|
);
|
||||||
prompt.push_str(".\nNever invent tool names or parameters. Check command exit codes and tool error results before claiming success.\n");
|
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") {
|
if tools.iter().any(|tool| tool.name == "run_shell_command") {
|
||||||
let has_file_tools = tools
|
let has_file_tools = tools
|
||||||
.iter()
|
.iter()
|
||||||
.any(|tool| matches!(tool.name.as_str(), "file_glob" | "grep" | "read_files"));
|
.any(|tool| matches!(tool.name.as_str(), "file_glob" | "grep" | "read_files"));
|
||||||
if has_file_tools {
|
if has_file_tools {
|
||||||
prompt.push_str(
|
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]
|
#[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();
|
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(
|
let prepared = prepare_rig_turn(
|
||||||
&config(),
|
&config(),
|
||||||
params,
|
params,
|
||||||
vec![
|
vec![
|
||||||
ToolType::RunShellCommand,
|
ToolType::RunShellCommand,
|
||||||
|
ToolType::SearchCodebase,
|
||||||
ToolType::FileGlob,
|
ToolType::FileGlob,
|
||||||
ToolType::Grep,
|
ToolType::Grep,
|
||||||
ToolType::ReadFiles,
|
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");
|
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`"));
|
assert!(prompt.contains("do not use shell `find`, `grep`, `rg`, `cat`, `head`, or `tail`"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||||
use ai::index::full_source_code_embedding::manager::CodebaseIndexManager;
|
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::integration::{AssertionOutcome, StepData, TestStep};
|
||||||
use galaxyui::{async_assert, App, ReadModel, SingletonEntity, UpdateModel, WindowId};
|
use galaxyui::{async_assert, App, ReadModel, SingletonEntity, UpdateModel, WindowId};
|
||||||
use settings::Setting;
|
use settings::Setting;
|
||||||
@@ -51,6 +56,11 @@ pub fn sync_current_codebase_index() -> TestStep {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Kick off codebase indexing at the current directory.
|
// 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| {
|
app.update_model(&CodebaseIndexManager::handle(app), |manager, ctx| {
|
||||||
manager.index_directory(canonicalized_path.clone(), 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())
|
.get::<String, PathBuf>(CWD_DATA_KEY.into())
|
||||||
.expect("No cwd");
|
.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| {
|
let status = app.read_model(&CodebaseIndexManager::handle(app), |manager, ctx| {
|
||||||
manager.get_codebase_index_status_for_path(cwd, ctx)
|
manager.get_codebase_index_status_for_path(cwd, ctx)
|
||||||
});
|
});
|
||||||
match status {
|
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) => {
|
Some(status) => {
|
||||||
async_assert!(
|
async_assert!(
|
||||||
status.has_synced_version()
|
status.has_synced_version()
|
||||||
|
|||||||
+19
-3
@@ -1155,10 +1155,15 @@ fn run_internal(mut launch_mode: LaunchMode) -> Result<()> {
|
|||||||
.spawn(galaxy_logging::rotate_log_files())
|
.spawn(galaxy_logging::rotate_log_files())
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
|
let local_project_indexing_enabled = matches!(
|
||||||
|
launch_mode,
|
||||||
|
LaunchMode::App { .. } | LaunchMode::CommandLine { .. } | LaunchMode::Test { .. }
|
||||||
|
);
|
||||||
ctx.add_singleton_model(|ctx| {
|
ctx.add_singleton_model(|ctx| {
|
||||||
AppExecutionMode::new(
|
AppExecutionMode::new_with_local_project_indexing(
|
||||||
launch_mode.execution_mode(),
|
launch_mode.execution_mode(),
|
||||||
launch_mode.is_sandboxed(),
|
launch_mode.is_sandboxed(),
|
||||||
|
local_project_indexing_enabled,
|
||||||
ctx,
|
ctx,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
@@ -1988,6 +1993,14 @@ pub(crate) fn initialize_app(
|
|||||||
} else {
|
} else {
|
||||||
ctx.add_singleton_model(|ctx| RepoOutlines::new_with_indexing_enabled(false, ctx));
|
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| {
|
ctx.add_singleton_model(|ctx| {
|
||||||
galaxy_core::sync_queue::SyncQueue::<SyncTask>::new_with_rate_limit(
|
galaxy_core::sync_queue::SyncQueue::<SyncTask>::new_with_rate_limit(
|
||||||
&ctx.background_executor(),
|
&ctx.background_executor(),
|
||||||
@@ -2215,7 +2228,10 @@ pub(crate) fn initialize_app(
|
|||||||
ctx.add_singleton_model(DefaultTerminal::new);
|
ctx.add_singleton_model(DefaultTerminal::new);
|
||||||
|
|
||||||
ctx.add_singleton_model(|ctx| {
|
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 { .. })
|
&& (matches!(launch_mode, LaunchMode::RemoteServerDaemon { .. })
|
||||||
|| UserWorkspaces::as_ref(ctx).is_codebase_context_enabled(ctx));
|
|| UserWorkspaces::as_ref(ctx).is_codebase_context_enabled(ctx));
|
||||||
let indices_to_restore = if should_restore_indices {
|
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.max_files_per_repo,
|
||||||
codebase_limits.embedding_generation_batch_size,
|
codebase_limits.embedding_generation_batch_size,
|
||||||
server_api_provider.as_ref(ctx).get(),
|
server_api_provider.as_ref(ctx).get(),
|
||||||
launch_mode.supports_indexing(),
|
launch_mode.supports_indexing() && legacy_indexing_enabled,
|
||||||
);
|
);
|
||||||
if matches!(launch_mode, LaunchMode::RemoteServerDaemon { .. }) {
|
if matches!(launch_mode, LaunchMode::RemoteServerDaemon { .. }) {
|
||||||
codebase_index_config = codebase_index_config.defer_persisted_index_restore();
|
codebase_index_config = codebase_index_config.defer_persisted_index_restore();
|
||||||
|
|||||||
@@ -196,6 +196,8 @@ fn initialize_app(app: &mut App) {
|
|||||||
app.add_singleton_model(voice_input::VoiceInput::new);
|
app.add_singleton_model(voice_input::VoiceInput::new);
|
||||||
#[cfg(feature = "local_fs")]
|
#[cfg(feature = "local_fs")]
|
||||||
app.add_singleton_model(RepoMetadataModel::new);
|
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(SkillManager::new);
|
||||||
app.add_singleton_model(FileSearchModel::new);
|
app.add_singleton_model(FileSearchModel::new);
|
||||||
app.add_singleton_model(|_| crate::code_review::git_repo_model::GitRepoModels::new());
|
app.add_singleton_model(|_| crate::code_review::git_repo_model::GitRepoModels::new());
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ use cfg_if::cfg_if;
|
|||||||
use galaxy_core::context_flag::ContextFlag;
|
use galaxy_core::context_flag::ContextFlag;
|
||||||
use galaxy_core::safe_error;
|
use galaxy_core::safe_error;
|
||||||
use galaxy_core::user_preferences::GetUserPreferences as _;
|
use galaxy_core::user_preferences::GetUserPreferences as _;
|
||||||
|
#[cfg(feature = "voice_input")]
|
||||||
|
use galaxyui_core::event::KeyState;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use onboarding::{
|
use onboarding::{
|
||||||
|
|||||||
@@ -209,9 +209,9 @@ pub const OPEN_CODE_REVIEW: StaticCommand = StaticCommand {
|
|||||||
|
|
||||||
pub const INDEX: StaticCommand = StaticCommand {
|
pub const INDEX: StaticCommand = StaticCommand {
|
||||||
name: "/index",
|
name: "/index",
|
||||||
description: "Index this codebase",
|
description: "Build or refresh the local project index",
|
||||||
icon_path: "bundled/svg/find-all.svg",
|
icon_path: "bundled/svg/find-all.svg",
|
||||||
availability: Availability::REPOSITORY
|
availability: Availability::LOCAL
|
||||||
.union(Availability::CODEBASE_CONTEXT)
|
.union(Availability::CODEBASE_CONTEXT)
|
||||||
.union(Availability::AI_ENABLED),
|
.union(Availability::AI_ENABLED),
|
||||||
auto_enter_ai_mode: false,
|
auto_enter_ai_mode: false,
|
||||||
@@ -220,11 +220,9 @@ pub const INDEX: StaticCommand = StaticCommand {
|
|||||||
|
|
||||||
pub const INIT: StaticCommand = StaticCommand {
|
pub const INIT: StaticCommand = StaticCommand {
|
||||||
name: "/init",
|
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",
|
icon_path: "bundled/svg/warp-2.svg",
|
||||||
availability: Availability::REPOSITORY
|
availability: Availability::LOCAL.union(Availability::AI_ENABLED),
|
||||||
.union(Availability::AGENT_VIEW)
|
|
||||||
.union(Availability::AI_ENABLED),
|
|
||||||
auto_enter_ai_mode: true,
|
auto_enter_ai_mode: true,
|
||||||
argument: None,
|
argument: None,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,13 +5,9 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||||
use ai::index::full_source_code_embedding::manager::CodebaseIndexManager;
|
use ai::index::local_project_index::LocalProjectIndexEvent;
|
||||||
#[cfg(all(
|
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||||
feature = "local_fs",
|
use ai::index::local_project_index::{LocalIndexStatus, LocalProjectIndexManager};
|
||||||
not(target_family = "wasm"),
|
|
||||||
not(any(test, feature = "integration_tests"))
|
|
||||||
))]
|
|
||||||
use ai::index::full_source_code_embedding::manager::CodebaseIndexManagerEvent;
|
|
||||||
use galaxy_core::features::FeatureFlag;
|
use galaxy_core::features::FeatureFlag;
|
||||||
use galaxy_core::paths::home_relative_path;
|
use galaxy_core::paths::home_relative_path;
|
||||||
use galaxy_core::ui::theme::color::internal_colors;
|
use galaxy_core::ui::theme::color::internal_colors;
|
||||||
@@ -129,23 +125,17 @@ impl AgentAssistedEnvironmentModal {
|
|||||||
create_button,
|
create_button,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(all(
|
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||||
feature = "local_fs",
|
|
||||||
not(target_family = "wasm"),
|
|
||||||
not(any(test, feature = "integration_tests"))
|
|
||||||
))]
|
|
||||||
{
|
{
|
||||||
let index_manager = CodebaseIndexManager::handle(ctx);
|
let index_manager = LocalProjectIndexManager::handle(ctx);
|
||||||
ctx.subscribe_to_model(&index_manager, |me, _, event, ctx| {
|
ctx.subscribe_to_model(&index_manager, |me, _, event, ctx| {
|
||||||
if !me.visible {
|
if !me.visible {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
match event {
|
match event {
|
||||||
CodebaseIndexManagerEvent::SyncStateUpdated { .. }
|
LocalProjectIndexEvent::StatusChanged { .. }
|
||||||
| CodebaseIndexManagerEvent::NewIndexCreated { .. }
|
| LocalProjectIndexEvent::IndexRemoved { .. } => {
|
||||||
| CodebaseIndexManagerEvent::RemoveExpiredIndexMetadata { .. }
|
|
||||||
| CodebaseIndexManagerEvent::IndexMetadataUpdated { .. } => {
|
|
||||||
me.refresh_available_repos(ctx);
|
me.refresh_available_repos(ctx);
|
||||||
if me.available_repos.is_empty() {
|
if me.available_repos.is_empty() {
|
||||||
me.maybe_start_available_repos_loading(ctx);
|
me.maybe_start_available_repos_loading(ctx);
|
||||||
@@ -734,10 +724,10 @@ impl View for AgentAssistedEnvironmentModal {
|
|||||||
fn available_indexed_repos(app: &AppContext) -> Vec<RepoEntry> {
|
fn available_indexed_repos(app: &AppContext) -> Vec<RepoEntry> {
|
||||||
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||||
{
|
{
|
||||||
let mut repos: Vec<RepoEntry> = CodebaseIndexManager::as_ref(app)
|
let mut repos: Vec<RepoEntry> = LocalProjectIndexManager::as_ref(app)
|
||||||
.get_codebase_index_statuses(app)
|
.statuses()
|
||||||
.filter_map(|(root, status)| {
|
.filter(|(_, status)| matches!(status, LocalIndexStatus::Ready { .. }))
|
||||||
status.has_synced_version().then(|| {
|
.map(|(root, _)| {
|
||||||
let name = root
|
let name = root
|
||||||
.file_name()
|
.file_name()
|
||||||
.and_then(|s| s.to_str())
|
.and_then(|s| s.to_str())
|
||||||
@@ -748,7 +738,6 @@ fn available_indexed_repos(app: &AppContext) -> Vec<RepoEntry> {
|
|||||||
path: root.clone(),
|
path: root.clone(),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
repos.sort_by_key(|a| a.name.to_lowercase());
|
repos.sort_by_key(|a| a.name.to_lowercase());
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||||
use ai::index::full_source_code_embedding::manager::CodebaseIndexManager;
|
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 galaxy_core::ui::appearance::Appearance;
|
||||||
use galaxyui::elements::{ChildView, Empty};
|
use galaxyui::elements::{ChildView, Empty};
|
||||||
use galaxyui::platform::WindowStyle;
|
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(|_| Appearance::mock());
|
||||||
app.add_singleton_model(|_| ToastStack);
|
app.add_singleton_model(|_| ToastStack);
|
||||||
|
|
||||||
// The modal queries CodebaseIndexManager for locally indexed repos.
|
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||||
// Register a test instance so `available_indexed_repos(...)` doesn't panic.
|
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| {
|
app.add_singleton_model(|ctx| {
|
||||||
CodebaseIndexManager::new_for_test(ServerApiProvider::as_ref(ctx).get(), ctx)
|
CodebaseIndexManager::new_for_test(ServerApiProvider::as_ref(ctx).get(), ctx)
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,11 +2,17 @@ use std::borrow::Cow;
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||||
use ai::index::full_source_code_embedding::manager::{
|
use ai::index::full_source_code_embedding::manager::{
|
||||||
CodebaseIndexFinishedStatus, CodebaseIndexManager, CodebaseIndexManagerEvent,
|
CodebaseIndexFinishedStatus, CodebaseIndexManager, CodebaseIndexManagerEvent,
|
||||||
CodebaseIndexStatus, CodebaseIndexingError,
|
CodebaseIndexStatus, CodebaseIndexingError,
|
||||||
};
|
};
|
||||||
|
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||||
use ai::index::full_source_code_embedding::SyncProgress;
|
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::project_context::model::{ProjectContextModel, ProjectContextModelEvent};
|
||||||
use ai::workspace::WorkspaceMetadata;
|
use ai::workspace::WorkspaceMetadata;
|
||||||
use galaxy_core::features::FeatureFlag;
|
use galaxy_core::features::FeatureFlag;
|
||||||
@@ -170,6 +176,44 @@ enum IndexingRefreshAction {
|
|||||||
RequestRemote,
|
RequestRemote,
|
||||||
Resync,
|
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 {
|
pub struct CodeSettingsPageView {
|
||||||
page: PageType<Self>,
|
page: PageType<Self>,
|
||||||
active_subpage: Option<CodeSubpage>,
|
active_subpage: Option<CodeSubpage>,
|
||||||
@@ -196,34 +240,52 @@ pub struct CodeSettingsPageView {
|
|||||||
|
|
||||||
impl CodeSettingsPageView {
|
impl CodeSettingsPageView {
|
||||||
pub fn new(ctx: &mut ViewContext<CodeSettingsPageView>) -> Self {
|
pub fn new(ctx: &mut ViewContext<CodeSettingsPageView>) -> Self {
|
||||||
|
#[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(count, Default::default);
|
||||||
|
me.codebase_delete_mouse_states
|
||||||
|
.resize_with(count, Default::default);
|
||||||
|
me.resize_workspace_mouse_states(ctx);
|
||||||
|
ctx.notify();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
count
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||||
|
let codebase_count = {
|
||||||
let index_manager = CodebaseIndexManager::handle(ctx);
|
let index_manager = CodebaseIndexManager::handle(ctx);
|
||||||
let codebase_count = index_manager
|
let count = index_manager
|
||||||
.as_ref(ctx)
|
.as_ref(ctx)
|
||||||
.get_codebase_index_statuses(ctx)
|
.get_codebase_index_statuses(ctx)
|
||||||
.count();
|
.count();
|
||||||
|
|
||||||
ctx.subscribe_to_model(&index_manager, |me, index, event, ctx| {
|
ctx.subscribe_to_model(&index_manager, |me, index, event, ctx| {
|
||||||
if matches!(
|
if matches!(
|
||||||
event,
|
event,
|
||||||
CodebaseIndexManagerEvent::SyncStateUpdated { .. }
|
CodebaseIndexManagerEvent::SyncStateUpdated { .. }
|
||||||
| CodebaseIndexManagerEvent::NewIndexCreated { .. }
|
| CodebaseIndexManagerEvent::NewIndexCreated { .. }
|
||||||
) {
|
) {
|
||||||
let codebase_count = index.as_ref(ctx).get_codebase_index_statuses(ctx).count();
|
let 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
|
|
||||||
me.codebase_manual_resync_mouse_states
|
me.codebase_manual_resync_mouse_states
|
||||||
.resize_with(codebase_count, Default::default);
|
.resize_with(count, Default::default);
|
||||||
me.codebase_delete_mouse_states
|
me.codebase_delete_mouse_states
|
||||||
.resize_with(codebase_count, Default::default);
|
.resize_with(count, Default::default);
|
||||||
}
|
|
||||||
|
|
||||||
me.resize_workspace_mouse_states(ctx);
|
me.resize_workspace_mouse_states(ctx);
|
||||||
|
|
||||||
ctx.notify();
|
ctx.notify();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
count
|
||||||
|
};
|
||||||
|
|
||||||
#[cfg(not(target_family = "wasm"))]
|
#[cfg(not(target_family = "wasm"))]
|
||||||
let remote_codebase_count = {
|
let remote_codebase_count = {
|
||||||
@@ -552,6 +614,18 @@ impl CodeSettingsPageView {
|
|||||||
if let Some(directory_path) = paths.first() {
|
if let Some(directory_path) = paths.first() {
|
||||||
let path = PathBuf::from(directory_path);
|
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| {
|
CodebaseIndexManager::handle(ctx).update(ctx, |manager, ctx| {
|
||||||
manager.index_directory(path, ctx);
|
manager.index_directory(path, ctx);
|
||||||
});
|
});
|
||||||
@@ -697,11 +771,25 @@ impl TypedActionView for CodeSettingsPageView {
|
|||||||
ctx.notify();
|
ctx.notify();
|
||||||
}
|
}
|
||||||
CodeSettingsPageAction::ManualResync(repo_path) => {
|
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| {
|
CodebaseIndexManager::handle(ctx).update(ctx, |manager, ctx| {
|
||||||
manager.try_manual_resync_codebase(repo_path, ctx);
|
manager.try_manual_resync_codebase(repo_path, ctx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
CodeSettingsPageAction::DeleteIndex(repo_path) => {
|
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| {
|
CodebaseIndexManager::handle(ctx).update(ctx, |manager, ctx| {
|
||||||
manager.drop_index(repo_path.clone(), 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()
|
if codebase_indexing_enabled && !CodebaseIndexManager::as_ref(app).can_create_new_indices()
|
||||||
{
|
{
|
||||||
rows.push(self.render_settings_subtext(
|
rows.push(self.render_settings_subtext(
|
||||||
@@ -1372,7 +1461,10 @@ impl CodePageWidget {
|
|||||||
Vec::new()
|
Vec::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||||
let codebase_manager = CodebaseIndexManager::as_ref(app);
|
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 lsp_manager = LspManagerModel::as_ref(app);
|
||||||
let persisted_workspace = PersistedWorkspace::as_ref(app);
|
let persisted_workspace = PersistedWorkspace::as_ref(app);
|
||||||
|
|
||||||
@@ -1383,6 +1475,11 @@ impl CodePageWidget {
|
|||||||
let workspace_path = &workspace.path;
|
let workspace_path = &workspace.path;
|
||||||
|
|
||||||
// Get codebase index status if it exists
|
// 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 =
|
let index_status =
|
||||||
codebase_manager.get_codebase_index_status_for_path(workspace_path, app);
|
codebase_manager.get_codebase_index_status_for_path(workspace_path, app);
|
||||||
|
|
||||||
@@ -1403,7 +1500,17 @@ impl CodePageWidget {
|
|||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
// Skip workspaces that have neither an index nor any LSP servers
|
// 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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1431,7 +1538,7 @@ impl CodePageWidget {
|
|||||||
|
|
||||||
content.add_child(self.render_workspace_row(
|
content.add_child(self.render_workspace_row(
|
||||||
workspace_path,
|
workspace_path,
|
||||||
index_status.as_ref(),
|
index_presentation,
|
||||||
&all_servers,
|
&all_servers,
|
||||||
lsp_manager,
|
lsp_manager,
|
||||||
resync_mouse,
|
resync_mouse,
|
||||||
@@ -1485,7 +1592,7 @@ impl CodePageWidget {
|
|||||||
fn render_workspace_row(
|
fn render_workspace_row(
|
||||||
&self,
|
&self,
|
||||||
workspace_path: &Path,
|
workspace_path: &Path,
|
||||||
index_status: Option<&CodebaseIndexStatus>,
|
index_presentation: IndexingStatusPresentation,
|
||||||
all_servers: &[(LSPServerType, EnablementState)],
|
all_servers: &[(LSPServerType, EnablementState)],
|
||||||
lsp_manager: &LspManagerModel,
|
lsp_manager: &LspManagerModel,
|
||||||
resync_mouse: MouseStateHandle,
|
resync_mouse: MouseStateHandle,
|
||||||
@@ -1568,7 +1675,7 @@ impl CodePageWidget {
|
|||||||
// Indexing section (always rendered per design)
|
// Indexing section (always rendered per design)
|
||||||
workspace_content.add_child(self.render_indexing_subsection(
|
workspace_content.add_child(self.render_indexing_subsection(
|
||||||
workspace_path,
|
workspace_path,
|
||||||
index_status,
|
index_presentation,
|
||||||
resync_mouse,
|
resync_mouse,
|
||||||
delete_mouse,
|
delete_mouse,
|
||||||
appearance,
|
appearance,
|
||||||
@@ -1671,13 +1778,13 @@ impl CodePageWidget {
|
|||||||
fn render_indexing_subsection(
|
fn render_indexing_subsection(
|
||||||
&self,
|
&self,
|
||||||
workspace_path: &Path,
|
workspace_path: &Path,
|
||||||
index_status: Option<&CodebaseIndexStatus>,
|
presentation: IndexingStatusPresentation,
|
||||||
resync_mouse: MouseStateHandle,
|
resync_mouse: MouseStateHandle,
|
||||||
delete_mouse: MouseStateHandle,
|
delete_mouse: MouseStateHandle,
|
||||||
appearance: &Appearance,
|
appearance: &Appearance,
|
||||||
) -> Box<dyn Element> {
|
) -> Box<dyn Element> {
|
||||||
self.render_indexing_subsection_for_target(
|
self.render_indexing_subsection_for_target(
|
||||||
self.local_indexing_status_presentation(index_status, appearance),
|
presentation,
|
||||||
Some(LocalOrRemotePath::Local(workspace_path.to_path_buf())),
|
Some(LocalOrRemotePath::Local(workspace_path.to_path_buf())),
|
||||||
resync_mouse,
|
resync_mouse,
|
||||||
delete_mouse,
|
delete_mouse,
|
||||||
@@ -1730,6 +1837,7 @@ impl CodePageWidget {
|
|||||||
column.finish()
|
column.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||||
fn local_indexing_status_presentation(
|
fn local_indexing_status_presentation(
|
||||||
&self,
|
&self,
|
||||||
index_state: Option<&CodebaseIndexStatus>,
|
index_state: Option<&CodebaseIndexStatus>,
|
||||||
@@ -2569,6 +2677,7 @@ impl SettingsWidget for CodebaseIndexingCategorizedWidget {
|
|||||||
Some(AUTO_INDEX_DESCRIPTION.into()),
|
Some(AUTO_INDEX_DESCRIPTION.into()),
|
||||||
));
|
));
|
||||||
|
|
||||||
|
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||||
if !CodebaseIndexManager::as_ref(app).can_create_new_indices() {
|
if !CodebaseIndexManager::as_ref(app).can_create_new_indices() {
|
||||||
content.add_child(
|
content.add_child(
|
||||||
ui_builder
|
ui_builder
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||||
use ai::index::full_source_code_embedding::manager::{
|
use ai::index::full_source_code_embedding::manager::{
|
||||||
CodebaseIndexManager, CodebaseIndexManagerEvent,
|
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 galaxy_util::path::user_friendly_path;
|
||||||
use galaxyui::elements::{
|
use galaxyui::elements::{
|
||||||
Border, ChildView, ConstrainedBox, Container, CrossAxisAlignment, Flex, Hoverable,
|
Border, ChildView, ConstrainedBox, Container, CrossAxisAlignment, Flex, Hoverable,
|
||||||
@@ -76,6 +79,19 @@ pub(super) enum DirectoryColorAddPickerEvent {
|
|||||||
|
|
||||||
impl DirectoryColorAddPicker {
|
impl DirectoryColorAddPicker {
|
||||||
pub(super) fn new(ctx: &mut ViewContext<Self>) -> Self {
|
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| {
|
ctx.subscribe_to_model(&CodebaseIndexManager::handle(ctx), |me, _, event, ctx| {
|
||||||
// Refresh for any event that may change the set of indexed codebase paths or
|
// Refresh for any event that may change the set of indexed codebase paths or
|
||||||
// persisted workspaces: new index created, sync state updated (which covers
|
// 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>) {
|
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)
|
let indexed_paths: HashSet<PathBuf> = CodebaseIndexManager::as_ref(ctx)
|
||||||
.get_codebase_paths()
|
.get_codebase_paths()
|
||||||
.cloned()
|
.cloned()
|
||||||
|
|||||||
@@ -276,6 +276,8 @@ pub fn initialize_app(app: &mut App) {
|
|||||||
app.add_singleton_model(RepoMetadataModel::new);
|
app.add_singleton_model(RepoMetadataModel::new);
|
||||||
app.add_singleton_model(FileSearchModel::new);
|
app.add_singleton_model(FileSearchModel::new);
|
||||||
app.add_singleton_model(RepoOutlines::new_for_test);
|
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")]
|
#[cfg(feature = "voice_input")]
|
||||||
app.add_singleton_model(voice_input::VoiceInput::new);
|
app.add_singleton_model(voice_input::VoiceInput::new);
|
||||||
app.add_singleton_model(|ctx| {
|
app.add_singleton_model(|ctx| {
|
||||||
|
|||||||
+75
-20
@@ -71,6 +71,8 @@ use action::RememberForWormholing;
|
|||||||
pub use action::{AgentOnboardingVersion, OnboardingIntention, OnboardingVersion, TerminalAction};
|
pub use action::{AgentOnboardingVersion, OnboardingIntention, OnboardingVersion, TerminalAction};
|
||||||
use ai::api_keys::{ApiKeyManager, AwsCredentialsState};
|
use ai::api_keys::{ApiKeyManager, AwsCredentialsState};
|
||||||
use ai::index::full_source_code_embedding::manager::{BuildSource, CodebaseIndexManager};
|
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 async_channel::{Receiver, Sender};
|
||||||
use base64::Engine as _;
|
use base64::Engine as _;
|
||||||
use block_banner::{render_wormholing_banner, WormholeBannerState};
|
use block_banner::{render_wormholing_banner, WormholeBannerState};
|
||||||
@@ -7362,6 +7364,15 @@ impl TerminalView {
|
|||||||
BlocklistAIActionEvent::InitProject(_) => {
|
BlocklistAIActionEvent::InitProject(_) => {
|
||||||
self.on_next_conversation_finished(|me, _reason, ctx| {
|
self.on_next_conversation_finished(|me, _reason, ctx| {
|
||||||
if let Some(path) = me.pwd() {
|
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| {
|
CodebaseIndexManager::handle(ctx).update(ctx, |manager, ctx| {
|
||||||
manager.index_directory(PathBuf::from(path), 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| {
|
CodebaseIndexManager::handle(ctx).update(ctx, |manager, ctx| {
|
||||||
manager.index_directory(banner_state.repo_path.clone(), ctx);
|
manager.index_directory(banner_state.repo_path.clone(), ctx);
|
||||||
});
|
});
|
||||||
@@ -13835,6 +13855,13 @@ impl TerminalView {
|
|||||||
self.update_focused_terminal_info(ctx);
|
self.update_focused_terminal_info(ctx);
|
||||||
|
|
||||||
if let Some(working_directory) = self.active_session_path_if_local(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| {
|
CodebaseIndexManager::handle(ctx).update(ctx, |manager, _ctx| {
|
||||||
manager.handle_session_bootstrapped(&working_directory);
|
manager.handle_session_bootstrapped(&working_directory);
|
||||||
});
|
});
|
||||||
@@ -14200,10 +14227,7 @@ impl TerminalView {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(pwd_path) = self
|
let Some(pwd_path) = self.active_session_path_if_local(ctx) else {
|
||||||
.pwd()
|
|
||||||
.and_then(|pwd| Path::new(&pwd).canonicalize().ok())
|
|
||||||
else {
|
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -14246,6 +14270,18 @@ impl TerminalView {
|
|||||||
me.redetermine_terminal_focus(ctx);
|
me.redetermine_terminal_focus(ctx);
|
||||||
}
|
}
|
||||||
InitProjectModelEvent::StepCompleted(_) => {}
|
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 => {
|
InitProjectModelEvent::Cancelled => {
|
||||||
me.active_init_project_model = None;
|
me.active_init_project_model = None;
|
||||||
// Mark conversation as cancelled
|
// Mark conversation as cancelled
|
||||||
@@ -26002,6 +26038,13 @@ impl TerminalView {
|
|||||||
return;
|
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| {
|
CodebaseIndexManager::handle(ctx).update(ctx, |manager, ctx| {
|
||||||
manager.build_and_sync_codebase_index(
|
manager.build_and_sync_codebase_index(
|
||||||
BuildSource::FromPath(active_session_path.as_path()),
|
BuildSource::FromPath(active_session_path.as_path()),
|
||||||
@@ -26011,7 +26054,7 @@ impl TerminalView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn write_codebase_index(&self, _ctx: &mut ViewContext<Self>) {
|
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 {
|
let Some(working_directory_str) = self.pwd() else {
|
||||||
log::error!("No working directory found for terminal session");
|
log::error!("No working directory found for terminal session");
|
||||||
@@ -27431,26 +27474,38 @@ impl TypedActionView for TerminalView {
|
|||||||
}
|
}
|
||||||
SummarizeConversation => self.summarize_conversation(ctx),
|
SummarizeConversation => self.summarize_conversation(ctx),
|
||||||
IndexProjectSpeedbump => {
|
IndexProjectSpeedbump => {
|
||||||
let codebase_context_enabled =
|
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||||
UserWorkspaces::as_ref(ctx).is_codebase_context_enabled(ctx);
|
if let Some(repo_path) = self
|
||||||
|
.active_session_path_if_local(ctx)
|
||||||
if FeatureFlag::FullSourceCodeEmbedding.is_enabled() && codebase_context_enabled {
|
.and_then(|path| dunce::canonicalize(path).ok())
|
||||||
#[cfg(feature = "local_fs")]
|
{
|
||||||
if let Some(current_dir) = self.pwd() {
|
if let Err(error) = LocalProjectIndexManager::handle(ctx)
|
||||||
let directory = PathBuf::from(¤t_dir);
|
.update(ctx, |manager, ctx| {
|
||||||
|
manager.index_directory(repo_path.clone(), ctx)
|
||||||
if let Ok(repo_path) = directory.canonicalize() {
|
})
|
||||||
// Start indexing the codebase
|
{
|
||||||
CodebaseIndexManager::handle(ctx).update(ctx, |manager, ctx| {
|
log::warn!("Failed to start local project indexing: {error:#}");
|
||||||
manager.index_directory(repo_path.clone(), ctx);
|
}
|
||||||
});
|
|
||||||
|
|
||||||
self.remove_codebase_index_speedbump_banner(ctx);
|
self.remove_codebase_index_speedbump_banner(ctx);
|
||||||
self.insert_codebase_index_speedbump_banner(
|
self.insert_codebase_index_speedbump_banner(
|
||||||
repo_path, true, /* show_is_indexing */
|
repo_path, true, /* show_is_indexing */
|
||||||
ctx,
|
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(¤t_dir);
|
||||||
|
if let Ok(repo_path) = directory.canonicalize() {
|
||||||
|
CodebaseIndexManager::handle(ctx).update(ctx, |manager, ctx| {
|
||||||
|
manager.index_directory(repo_path.clone(), ctx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ pub mod model;
|
|||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||||
use ai::index::full_source_code_embedding::manager::CodebaseIndexManager;
|
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 galaxy_core::ui::theme::Fill;
|
||||||
use lsp::supported_servers::LSPServerType;
|
use lsp::supported_servers::LSPServerType;
|
||||||
use lsp_server_selector::{create_lsp_server_selector, LSPServerInfo};
|
use lsp_server_selector::{create_lsp_server_selector, LSPServerInfo};
|
||||||
@@ -40,7 +43,7 @@ use crate::view_components::DismissibleToast;
|
|||||||
use crate::workspace::ToastStack;
|
use crate::workspace::ToastStack;
|
||||||
use crate::{send_telemetry_from_ctx, TelemetryEvent};
|
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.";
|
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.
|
// Native Galaxy rules file format.
|
||||||
pub const FILES_TO_CHECK: [&str; 4] = ["GALAXY.md", "AGENTS.md", "WARP.md", "CLAUDE.md"];
|
pub const FILES_TO_CHECK: [&str; 4] = ["GALAXY.md", "AGENTS.md", "WARP.md", "CLAUDE.md"];
|
||||||
@@ -644,10 +647,11 @@ impl InitStepBlock {
|
|||||||
.render(app)
|
.render(app)
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
InitStepStatus::Running => {
|
InitStepStatus::Running => RenderableAction::new("Building local project index", app)
|
||||||
// Codebase context doesn't have a "running" state
|
.with_icon(in_progress_icon(Appearance::as_ref(app)).finish())
|
||||||
Empty::new().finish()
|
.with_content_item_spacing()
|
||||||
}
|
.render(app)
|
||||||
|
.finish(),
|
||||||
InitStepStatus::Completed(result) => {
|
InitStepStatus::Completed(result) => {
|
||||||
self.render_completed_codebase_context(result, app)
|
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>) {
|
fn handle_action(&mut self, action: &Self::Action, ctx: &mut ViewContext<Self>) {
|
||||||
match action {
|
match action {
|
||||||
InitProjectBlockAction::IndexCodebase(directory) => {
|
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!(
|
send_telemetry_from_ctx!(
|
||||||
TelemetryEvent::AgentModeSetupCodebaseContextAction {
|
TelemetryEvent::AgentModeSetupCodebaseContextAction {
|
||||||
action: AgentModeSetupCodebaseContextActionType::IndexCodebase,
|
action: AgentModeSetupCodebaseContextActionType::IndexCodebase,
|
||||||
},
|
},
|
||||||
ctx
|
ctx
|
||||||
);
|
);
|
||||||
CodebaseIndexManager::handle(ctx).update(ctx, |manager, ctx| {
|
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||||
manager.index_directory(directory.clone(), ctx);
|
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| {
|
self.model.update(ctx, |model, ctx| {
|
||||||
model.mark_step_completed(
|
model.mark_step_completed(
|
||||||
InitStepKind::CodebaseContext,
|
InitStepKind::CodebaseContext,
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
|
||||||
use ai::index::full_source_code_embedding::manager::CodebaseIndexManager;
|
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 ai::project_context::model::ProjectContextModel;
|
||||||
use enum_iterator::Sequence;
|
use enum_iterator::Sequence;
|
||||||
use galaxy_util::local_or_remote_path::LocalOrRemotePath;
|
use galaxy_util::local_or_remote_path::LocalOrRemotePath;
|
||||||
@@ -10,7 +15,6 @@ use lsp::supported_servers::LSPServerType;
|
|||||||
use repo_metadata::repositories::DetectedRepositories;
|
use repo_metadata::repositories::DetectedRepositories;
|
||||||
|
|
||||||
use crate::ai::persisted_workspace::PersistedWorkspace;
|
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::lsp_server_selector::LSPServerInfo;
|
||||||
use crate::terminal::view::init_project::{
|
use crate::terminal::view::init_project::{
|
||||||
CodebaseIndexingResult, CreateEnvironmentResult, InitActionResult, LanguageServersResult,
|
CodebaseIndexingResult, CreateEnvironmentResult, InitActionResult, LanguageServersResult,
|
||||||
@@ -124,7 +128,11 @@ impl InitProjectModel {
|
|||||||
path_env_var: Option<String>,
|
path_env_var: Option<String>,
|
||||||
ctx: &mut ModelContext<Self>,
|
ctx: &mut ModelContext<Self>,
|
||||||
) -> 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 {
|
Self {
|
||||||
steps: [None, None, None, None, None],
|
steps: [None, None, None, None, None],
|
||||||
@@ -132,7 +140,7 @@ impl InitProjectModel {
|
|||||||
is_cancelled: false,
|
is_cancelled: false,
|
||||||
is_already_setup,
|
is_already_setup,
|
||||||
#[cfg(feature = "local_fs")]
|
#[cfg(feature = "local_fs")]
|
||||||
root_path: pwd_path,
|
root_path,
|
||||||
path_env_var,
|
path_env_var,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -154,6 +162,17 @@ impl InitProjectModel {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Start async computations for subsequent steps
|
// 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);
|
self.compute_codebase_context_step(&pwd_path, ctx);
|
||||||
if self.path_env_var.is_some() {
|
if self.path_env_var.is_some() {
|
||||||
self.compute_language_servers_step(&pwd_path, ctx);
|
self.compute_language_servers_step(&pwd_path, ctx);
|
||||||
@@ -176,14 +195,12 @@ impl InitProjectModel {
|
|||||||
|
|
||||||
/// Check if there are any steps that need user action
|
/// Check if there are any steps that need user action
|
||||||
pub fn should_have_available_steps(path: &Path, ctx: &galaxyui::AppContext) -> bool {
|
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.
|
// /init is an explicit user request, so its indexing step must remain available even
|
||||||
// This avoids the potential race condition with the banner showing just when we start auto-indexing.
|
// 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)
|
let has_pending_codebase_context = UserWorkspaces::as_ref(ctx)
|
||||||
.is_codebase_context_enabled(ctx)
|
.is_codebase_context_enabled(ctx)
|
||||||
&& CodebaseIndexManager::as_ref(ctx)
|
&& !Self::codebase_index_ready(path, ctx);
|
||||||
.get_codebase_index_status_for_path(path, ctx)
|
|
||||||
.is_none()
|
|
||||||
&& !*CodeSettings::as_ref(ctx).auto_indexing_enabled;
|
|
||||||
|
|
||||||
let has_pending_project_scoped_rules = ProjectContextModel::as_ref(ctx)
|
let has_pending_project_scoped_rules = ProjectContextModel::as_ref(ctx)
|
||||||
.find_applicable_project_rules(&LocalOrRemotePath::Local(path.to_path_buf()))
|
.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
|
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> {
|
pub fn get_step(&self, kind: InitStepKind) -> Option<&InitStep> {
|
||||||
self.steps
|
self.steps
|
||||||
.get(kind as usize)
|
.get(kind as usize)
|
||||||
@@ -368,9 +397,12 @@ impl InitProjectModel {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let codebase_index_manager = CodebaseIndexManager::handle(ctx);
|
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||||
let is_indexed = codebase_index_manager
|
let is_indexed = LocalProjectIndexManager::as_ref(ctx)
|
||||||
.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)
|
.get_codebase_index_status_for_path(pwd_path, ctx)
|
||||||
.is_some();
|
.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>) {
|
fn compute_language_servers_step(&mut self, pwd_path: &Path, ctx: &mut ModelContext<Self>) {
|
||||||
// Start as Pending
|
// Start as Pending
|
||||||
self.set_step(
|
self.set_step(
|
||||||
@@ -584,6 +657,8 @@ pub enum InitProjectModelEvent {
|
|||||||
InitCompleted,
|
InitCompleted,
|
||||||
/// Trigger AGENTS.md generation slash command
|
/// Trigger AGENTS.md generation slash command
|
||||||
GenerateProjectRules,
|
GenerateProjectRules,
|
||||||
|
/// The local index failed and can be retried from the same step.
|
||||||
|
CodebaseIndexFailed(String),
|
||||||
/// Trigger AGENTS.md regeneration
|
/// Trigger AGENTS.md regeneration
|
||||||
RegenerateProjectRules,
|
RegenerateProjectRules,
|
||||||
/// View codebase context status
|
/// View codebase context status
|
||||||
|
|||||||
@@ -156,6 +156,8 @@ pub fn initialize_app_for_terminal_view(app: &mut App) {
|
|||||||
model
|
model
|
||||||
});
|
});
|
||||||
app.add_singleton_model(FileSearchModel::new);
|
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(|_| GitRepoModels::new());
|
||||||
app.add_singleton_model(RepoOutlines::new_for_test);
|
app.add_singleton_model(RepoOutlines::new_for_test);
|
||||||
app.add_singleton_model(HomeDirectoryWatcher::new_for_test);
|
app.add_singleton_model(HomeDirectoryWatcher::new_for_test);
|
||||||
|
|||||||
@@ -231,6 +231,8 @@ pub(crate) fn initialize_app(app: &mut App) {
|
|||||||
|
|
||||||
#[cfg(feature = "local_fs")]
|
#[cfg(feature = "local_fs")]
|
||||||
app.add_singleton_model(RepoMetadataModel::new);
|
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);
|
app.add_singleton_model(search::files::model::FileSearchModel::new);
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ priority-queue = "2.3.1"
|
|||||||
repo_metadata.workspace = true
|
repo_metadata.workspace = true
|
||||||
uuid.workspace = true
|
uuid.workspace = true
|
||||||
unicode-width.workspace = true
|
unicode-width.workspace = true
|
||||||
|
warp_search_core.workspace = true
|
||||||
|
|
||||||
[target.'cfg(unix)'.dependencies]
|
[target.'cfg(unix)'.dependencies]
|
||||||
nix.workspace = true
|
nix.workspace = true
|
||||||
|
|||||||
@@ -0,0 +1,695 @@
|
|||||||
|
//! Persistent local-only structural and lexical project search.
|
||||||
|
//!
|
||||||
|
//! This index stores only bounded search metadata. Search callers read the current file contents
|
||||||
|
//! after applying their normal permission checks; no source is sent to a server by this module.
|
||||||
|
|
||||||
|
#![cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||||
|
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
use std::fs;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use galaxy_core::paths::state_dir;
|
||||||
|
use galaxy_util::standardized_path::StandardizedPath;
|
||||||
|
use galaxyui_core::{Entity, ModelContext, SingletonEntity};
|
||||||
|
use itertools::Itertools;
|
||||||
|
use repo_metadata::{RepoMetadataEvent, RepositoryIdentifier};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use warp_search_core::define_search_schema;
|
||||||
|
use warp_search_core::searcher::{SimpleFullTextSearcher, DEFAULT_MEMORY_BUDGET};
|
||||||
|
|
||||||
|
use crate::index::build_outline;
|
||||||
|
|
||||||
|
const INDEX_SCHEMA_VERSION: u32 = 1;
|
||||||
|
const MAX_INDEX_FILES: usize = 5_000;
|
||||||
|
const MAX_INDEXED_FILE_BYTES: usize = 3 * 1_000_000;
|
||||||
|
const MAX_INDEXED_BODY_BYTES: usize = 256_000;
|
||||||
|
const INDEX_DIRECTORY_NAME: &str = "local_project_indices";
|
||||||
|
const CURRENT_FILE_NAME: &str = "CURRENT";
|
||||||
|
const METADATA_FILE_NAME: &str = "metadata.json";
|
||||||
|
|
||||||
|
// Field weights intentionally prioritize symbols and paths over implementation text.
|
||||||
|
define_search_schema!(
|
||||||
|
schema_name: LOCAL_PROJECT_INDEX_SCHEMA,
|
||||||
|
config_name: LocalProjectIndexSchema,
|
||||||
|
search_doc: LocalProjectSearchDocument,
|
||||||
|
identifying_doc: LocalProjectIndexId,
|
||||||
|
search_fields: [symbol: 8.0, path: 5.0, metadata: 4.0, body: 1.0],
|
||||||
|
id_fields: [file_path: String]
|
||||||
|
);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum LocalIndexStatus {
|
||||||
|
Indexing,
|
||||||
|
Ready { file_count: usize },
|
||||||
|
Failed { message: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct LocalSearchHit {
|
||||||
|
pub path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum LocalProjectIndexEvent {
|
||||||
|
StatusChanged {
|
||||||
|
root_path: PathBuf,
|
||||||
|
status: LocalIndexStatus,
|
||||||
|
},
|
||||||
|
IndexRemoved {
|
||||||
|
root_path: PathBuf,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
struct LocalIndexMetadata {
|
||||||
|
root_path: String,
|
||||||
|
schema_version: u32,
|
||||||
|
generation: String,
|
||||||
|
file_count: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LocalProjectIndex {
|
||||||
|
root_path: PathBuf,
|
||||||
|
searcher: SimpleFullTextSearcher<LocalProjectIndexSchema>,
|
||||||
|
file_count: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LocalProjectIndex {
|
||||||
|
fn search(
|
||||||
|
&self,
|
||||||
|
query: &str,
|
||||||
|
partial_path_segments: Option<&[String]>,
|
||||||
|
) -> Result<Vec<LocalSearchHit>> {
|
||||||
|
let matches = self.searcher.search_id(query)?;
|
||||||
|
|
||||||
|
Ok(matches
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|matched| {
|
||||||
|
let path = PathBuf::from(matched.values.file_path);
|
||||||
|
let relative_path = path.strip_prefix(&self.root_path).ok()?;
|
||||||
|
if partial_path_segments.is_some_and(|segments| {
|
||||||
|
!segments.is_empty()
|
||||||
|
&& !segments
|
||||||
|
.iter()
|
||||||
|
.any(|segment| relative_path.to_string_lossy().contains(segment))
|
||||||
|
}) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(LocalSearchHit { path })
|
||||||
|
})
|
||||||
|
.unique_by(|hit| hit.path.clone())
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct LocalProjectIndexManager {
|
||||||
|
indices: HashMap<PathBuf, LocalProjectIndex>,
|
||||||
|
statuses: HashMap<PathBuf, LocalIndexStatus>,
|
||||||
|
pending_rebuilds: HashSet<PathBuf>,
|
||||||
|
rebuild_epochs: HashMap<PathBuf, u64>,
|
||||||
|
storage_root: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Entity for LocalProjectIndexManager {
|
||||||
|
type Event = LocalProjectIndexEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SingletonEntity for LocalProjectIndexManager {}
|
||||||
|
|
||||||
|
impl LocalProjectIndexManager {
|
||||||
|
pub fn new(ctx: &mut ModelContext<Self>) -> Self {
|
||||||
|
let repo_metadata = repo_metadata::RepoMetadataModel::handle(ctx);
|
||||||
|
let manager = Self::new_at(state_dir().join(INDEX_DIRECTORY_NAME), ctx);
|
||||||
|
ctx.subscribe_to_model(&repo_metadata, |manager, _, event, ctx| {
|
||||||
|
manager.handle_repo_metadata_event(event, ctx);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Re-register restored roots with repository metadata so filesystem updates continue to
|
||||||
|
// refresh the local index after an app restart. RepositoryUpdated is intentionally ignored
|
||||||
|
// below; the restored generation is already usable and only file-tree updates need a
|
||||||
|
// rebuild.
|
||||||
|
let restored_roots: Vec<PathBuf> = manager.statuses.keys().cloned().collect();
|
||||||
|
for root_path in restored_roots {
|
||||||
|
let Ok(standardized_root) = StandardizedPath::from_local_canonicalized(&root_path)
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let _ = repo_metadata.update(ctx, |model, ctx| {
|
||||||
|
model.index_local_directory_path(&standardized_root, ctx)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
manager
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Constructs a manager with an explicit storage root for hermetic tests.
|
||||||
|
pub fn new_at(storage_root: PathBuf, _ctx: &mut ModelContext<Self>) -> Self {
|
||||||
|
let mut manager = Self {
|
||||||
|
indices: HashMap::new(),
|
||||||
|
statuses: HashMap::new(),
|
||||||
|
pending_rebuilds: HashSet::new(),
|
||||||
|
rebuild_epochs: HashMap::new(),
|
||||||
|
storage_root,
|
||||||
|
};
|
||||||
|
manager.restore_persisted_indices();
|
||||||
|
manager
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Starts a background rebuild for an explicit local project root.
|
||||||
|
pub fn index_directory(
|
||||||
|
&mut self,
|
||||||
|
root_path: PathBuf,
|
||||||
|
ctx: &mut ModelContext<Self>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let root_path = dunce::canonicalize(&root_path).with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Failed to canonicalize project root {}",
|
||||||
|
root_path.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
if !root_path.is_dir() {
|
||||||
|
anyhow::bail!("Project root is not a directory: {}", root_path.display());
|
||||||
|
}
|
||||||
|
if matches!(
|
||||||
|
self.statuses.get(&root_path),
|
||||||
|
Some(LocalIndexStatus::Indexing)
|
||||||
|
) {
|
||||||
|
self.pending_rebuilds.insert(root_path);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let standardized_root = StandardizedPath::from_local_canonicalized(&root_path)
|
||||||
|
.map_err(|error| anyhow::anyhow!("Failed to standardize project root: {error}"))?;
|
||||||
|
repo_metadata::RepoMetadataModel::handle(ctx)
|
||||||
|
.update(ctx, |model, ctx| {
|
||||||
|
model.index_local_directory_path(&standardized_root, ctx)
|
||||||
|
})
|
||||||
|
.map_err(|error| anyhow::anyhow!("Failed to register project root: {error}"))?;
|
||||||
|
|
||||||
|
self.start_rebuild(root_path, ctx);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_rebuild(&mut self, root_path: PathBuf, ctx: &mut ModelContext<Self>) {
|
||||||
|
let rebuild_epoch = {
|
||||||
|
let epoch = self.rebuild_epochs.entry(root_path.clone()).or_default();
|
||||||
|
*epoch += 1;
|
||||||
|
*epoch
|
||||||
|
};
|
||||||
|
self.statuses
|
||||||
|
.insert(root_path.clone(), LocalIndexStatus::Indexing);
|
||||||
|
ctx.emit(LocalProjectIndexEvent::StatusChanged {
|
||||||
|
root_path: root_path.clone(),
|
||||||
|
status: LocalIndexStatus::Indexing,
|
||||||
|
});
|
||||||
|
|
||||||
|
let storage_root = self.storage_root.clone();
|
||||||
|
ctx.spawn(
|
||||||
|
async move { build_persisted_index(root_path, storage_root).await },
|
||||||
|
move |manager, result, ctx| match result {
|
||||||
|
Ok((root_path, built_index, file_count)) => {
|
||||||
|
if manager.rebuild_epochs.get(&root_path).copied() != Some(rebuild_epoch) {
|
||||||
|
drop(built_index.searcher);
|
||||||
|
let _ = fs::remove_dir_all(&built_index.generation_directory);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Err(error) = publish_generation(
|
||||||
|
&manager.storage_root,
|
||||||
|
&root_path,
|
||||||
|
&built_index.generation,
|
||||||
|
) {
|
||||||
|
drop(built_index.searcher);
|
||||||
|
manager.handle_rebuild_failure(root_path, error, ctx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let status = LocalIndexStatus::Ready { file_count };
|
||||||
|
manager.indices.insert(
|
||||||
|
root_path.clone(),
|
||||||
|
LocalProjectIndex {
|
||||||
|
root_path: root_path.clone(),
|
||||||
|
searcher: built_index.searcher,
|
||||||
|
file_count,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
manager.statuses.insert(root_path.clone(), status.clone());
|
||||||
|
ctx.emit(LocalProjectIndexEvent::StatusChanged {
|
||||||
|
root_path: root_path.clone(),
|
||||||
|
status,
|
||||||
|
});
|
||||||
|
let should_rebuild = manager.pending_rebuilds.remove(&root_path);
|
||||||
|
cleanup_old_generations(&manager.storage_root, &root_path);
|
||||||
|
if should_rebuild {
|
||||||
|
manager.start_rebuild(root_path, ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err((root_path, error)) => {
|
||||||
|
if manager.rebuild_epochs.get(&root_path).copied() != Some(rebuild_epoch) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
manager.handle_rebuild_failure(root_path, error, ctx);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_rebuild_failure(
|
||||||
|
&mut self,
|
||||||
|
root_path: PathBuf,
|
||||||
|
error: anyhow::Error,
|
||||||
|
ctx: &mut ModelContext<Self>,
|
||||||
|
) {
|
||||||
|
log::warn!(
|
||||||
|
"Failed to refresh local project index for {}: {error:#}",
|
||||||
|
root_path.display()
|
||||||
|
);
|
||||||
|
// Keep a previously committed generation searchable. Only a root with no
|
||||||
|
// usable generation transitions to Failed.
|
||||||
|
if let Some(previous_index) = self.indices.get(&root_path) {
|
||||||
|
let status = LocalIndexStatus::Ready {
|
||||||
|
file_count: previous_index.file_count,
|
||||||
|
};
|
||||||
|
self.statuses.insert(root_path.clone(), status.clone());
|
||||||
|
ctx.emit(LocalProjectIndexEvent::StatusChanged {
|
||||||
|
root_path: root_path.clone(),
|
||||||
|
status,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
let status = LocalIndexStatus::Failed {
|
||||||
|
message: error.to_string(),
|
||||||
|
};
|
||||||
|
self.statuses.insert(root_path.clone(), status.clone());
|
||||||
|
ctx.emit(LocalProjectIndexEvent::StatusChanged {
|
||||||
|
root_path: root_path.clone(),
|
||||||
|
status,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let should_rebuild = self.pending_rebuilds.remove(&root_path);
|
||||||
|
cleanup_old_generations(&self.storage_root, &root_path);
|
||||||
|
if should_rebuild {
|
||||||
|
self.start_rebuild(root_path, ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the most specific indexed root containing `path`.
|
||||||
|
pub fn status_for_path(&self, path: &Path) -> Option<(&Path, &LocalIndexStatus)> {
|
||||||
|
let canonical_path = dunce::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
|
||||||
|
self.statuses
|
||||||
|
.iter()
|
||||||
|
.filter(|(root, _)| canonical_path.starts_with(root))
|
||||||
|
.max_by_key(|(root, _)| root.components().count())
|
||||||
|
.map(|(root, status)| (root.as_path(), status))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_ready_for_path(&self, path: &Path) -> bool {
|
||||||
|
self.status_for_path(path)
|
||||||
|
.is_some_and(|(_, status)| matches!(status, LocalIndexStatus::Ready { .. }))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_searchable_for_path(&self, path: &Path) -> bool {
|
||||||
|
self.status_for_path(path).is_some_and(|(root, status)| {
|
||||||
|
matches!(
|
||||||
|
status,
|
||||||
|
LocalIndexStatus::Ready { .. } | LocalIndexStatus::Indexing
|
||||||
|
) && self.indices.contains_key(root)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn root_for_path(&self, path: &Path) -> Option<PathBuf> {
|
||||||
|
self.status_for_path(path)
|
||||||
|
.map(|(root, _)| root.to_path_buf())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn search(
|
||||||
|
&self,
|
||||||
|
path: &Path,
|
||||||
|
query: &str,
|
||||||
|
partial_path_segments: Option<&[String]>,
|
||||||
|
) -> Result<Vec<LocalSearchHit>> {
|
||||||
|
let Some((root, status)) = self.status_for_path(path) else {
|
||||||
|
anyhow::bail!("No local project index is available for {}", path.display());
|
||||||
|
};
|
||||||
|
// A previous generation remains searchable while a refresh is being built.
|
||||||
|
if !matches!(
|
||||||
|
status,
|
||||||
|
LocalIndexStatus::Ready { .. } | LocalIndexStatus::Indexing
|
||||||
|
) {
|
||||||
|
anyhow::bail!("Local project index is not ready for {}", root.display());
|
||||||
|
}
|
||||||
|
self.indices
|
||||||
|
.get(root)
|
||||||
|
.context("Local project index status has no loaded generation")?
|
||||||
|
.search(query, partial_path_segments)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn statuses(&self) -> impl Iterator<Item = (&PathBuf, &LocalIndexStatus)> {
|
||||||
|
self.statuses.iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes a local project index and its persisted generations.
|
||||||
|
pub fn remove_index_for_path(&mut self, root_path: PathBuf, ctx: &mut ModelContext<Self>) {
|
||||||
|
let root_path = dunce::canonicalize(&root_path).unwrap_or(root_path);
|
||||||
|
self.remove_index(&root_path);
|
||||||
|
ctx.emit(LocalProjectIndexEvent::IndexRemoved { root_path });
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_repo_metadata_event(
|
||||||
|
&mut self,
|
||||||
|
event: &RepoMetadataEvent,
|
||||||
|
ctx: &mut ModelContext<Self>,
|
||||||
|
) {
|
||||||
|
let local_root = match event {
|
||||||
|
RepoMetadataEvent::FileTreeEntryUpdated {
|
||||||
|
id: RepositoryIdentifier::Local(path),
|
||||||
|
..
|
||||||
|
} => path.to_local_path(),
|
||||||
|
// FileTreeUpdated is emitted before repository mutations are applied. The
|
||||||
|
// post-application FileTreeEntryUpdated event below is the rebuild trigger; using
|
||||||
|
// both would start duplicate full rebuilds for every watcher batch.
|
||||||
|
RepoMetadataEvent::FileTreeUpdated { .. } => None,
|
||||||
|
RepoMetadataEvent::RepositoryRemoved {
|
||||||
|
id: RepositoryIdentifier::Local(path),
|
||||||
|
} => {
|
||||||
|
let local_path = path.to_local_path_lossy();
|
||||||
|
let root_path = dunce::canonicalize(&local_path).unwrap_or(local_path);
|
||||||
|
if self.statuses.contains_key(&root_path) {
|
||||||
|
self.remove_index(&root_path);
|
||||||
|
ctx.emit(LocalProjectIndexEvent::IndexRemoved { root_path });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
RepoMetadataEvent::RepositoryUpdated { .. }
|
||||||
|
| RepoMetadataEvent::RepositoryRemoved {
|
||||||
|
id: RepositoryIdentifier::Remote(_),
|
||||||
|
}
|
||||||
|
| RepoMetadataEvent::FileTreeEntryUpdated {
|
||||||
|
id: RepositoryIdentifier::Remote(_),
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| RepoMetadataEvent::UpdatingRepositoryFailed { .. }
|
||||||
|
| RepoMetadataEvent::StandingQueryResultsUpdated { .. }
|
||||||
|
| RepoMetadataEvent::IncrementalUpdateReady { .. } => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(local_root) = local_root else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let canonical_root = dunce::canonicalize(&local_root).unwrap_or(local_root);
|
||||||
|
if self.statuses.contains_key(&canonical_root) {
|
||||||
|
if matches!(
|
||||||
|
self.statuses.get(&canonical_root),
|
||||||
|
Some(LocalIndexStatus::Indexing)
|
||||||
|
) {
|
||||||
|
self.pending_rebuilds.insert(canonical_root);
|
||||||
|
} else {
|
||||||
|
self.start_rebuild(canonical_root, ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_index(&mut self, root_path: &Path) {
|
||||||
|
let root_path = dunce::canonicalize(root_path).unwrap_or_else(|_| root_path.to_path_buf());
|
||||||
|
self.indices.remove(&root_path);
|
||||||
|
self.statuses.remove(&root_path);
|
||||||
|
self.pending_rebuilds.remove(&root_path);
|
||||||
|
let epoch = self.rebuild_epochs.entry(root_path.clone()).or_default();
|
||||||
|
*epoch += 1;
|
||||||
|
let directory = repository_storage_directory(&self.storage_root, &root_path);
|
||||||
|
if let Err(error) = fs::remove_dir_all(directory) {
|
||||||
|
if error.kind() != std::io::ErrorKind::NotFound {
|
||||||
|
log::warn!("Failed to remove local project index: {error}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn restore_persisted_indices(&mut self) {
|
||||||
|
let Ok(entries) = fs::read_dir(&self.storage_root) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let directory = entry.path();
|
||||||
|
if !directory.is_dir() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let metadata = match read_metadata(&directory) {
|
||||||
|
Ok(metadata) => metadata,
|
||||||
|
Err(error) => {
|
||||||
|
log::warn!(
|
||||||
|
"Discarding invalid local project index at {}: {error:#}",
|
||||||
|
directory.display()
|
||||||
|
);
|
||||||
|
let _ = fs::remove_dir_all(&directory);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if metadata.schema_version != INDEX_SCHEMA_VERSION {
|
||||||
|
let _ = fs::remove_dir_all(&directory);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let raw_root_path = PathBuf::from(&metadata.root_path);
|
||||||
|
let Ok(root_path) = dunce::canonicalize(&raw_root_path) else {
|
||||||
|
log::debug!(
|
||||||
|
"Discarding local project index for missing root {}",
|
||||||
|
raw_root_path.display()
|
||||||
|
);
|
||||||
|
let _ = fs::remove_dir_all(&directory);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if !root_path.is_dir()
|
||||||
|
|| directory.file_name().and_then(|name| name.to_str())
|
||||||
|
!= Some(&format_storage_directory_name(&root_path))
|
||||||
|
{
|
||||||
|
let _ = fs::remove_dir_all(&directory);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let generation_path = directory.join("generations").join(&metadata.generation);
|
||||||
|
match SimpleFullTextSearcher::open_in_dir(
|
||||||
|
&LOCAL_PROJECT_INDEX_SCHEMA,
|
||||||
|
DEFAULT_MEMORY_BUDGET,
|
||||||
|
&generation_path,
|
||||||
|
) {
|
||||||
|
Ok(searcher) => {
|
||||||
|
self.indices.insert(
|
||||||
|
root_path.clone(),
|
||||||
|
LocalProjectIndex {
|
||||||
|
root_path: root_path.clone(),
|
||||||
|
searcher,
|
||||||
|
file_count: metadata.file_count,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
self.statuses.insert(
|
||||||
|
root_path,
|
||||||
|
LocalIndexStatus::Ready {
|
||||||
|
file_count: metadata.file_count,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
log::warn!(
|
||||||
|
"Discarding unreadable local project index at {}: {error:#}",
|
||||||
|
generation_path.display()
|
||||||
|
);
|
||||||
|
let _ = fs::remove_dir_all(&directory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct BuiltLocalProjectIndex {
|
||||||
|
searcher: SimpleFullTextSearcher<LocalProjectIndexSchema>,
|
||||||
|
generation: String,
|
||||||
|
generation_directory: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn build_persisted_index(
|
||||||
|
root_path: PathBuf,
|
||||||
|
storage_root: PathBuf,
|
||||||
|
) -> std::result::Result<(PathBuf, BuiltLocalProjectIndex, usize), (PathBuf, anyhow::Error)> {
|
||||||
|
let error_root_path = root_path.clone();
|
||||||
|
let result = async {
|
||||||
|
let documents = build_documents(&root_path).await?;
|
||||||
|
let index_directory = repository_storage_directory(&storage_root, &root_path);
|
||||||
|
fs::create_dir_all(index_directory.join("generations"))?;
|
||||||
|
let generation = format!("generation-{}", uuid::Uuid::new_v4());
|
||||||
|
let temporary_directory = index_directory
|
||||||
|
.join("generations")
|
||||||
|
.join(format!(".tmp-{generation}"));
|
||||||
|
let generation_directory = index_directory.join("generations").join(&generation);
|
||||||
|
let _ = fs::remove_dir_all(&temporary_directory);
|
||||||
|
fs::create_dir_all(&temporary_directory)?;
|
||||||
|
|
||||||
|
let searcher = match SimpleFullTextSearcher::create_in_dir(
|
||||||
|
&LOCAL_PROJECT_INDEX_SCHEMA,
|
||||||
|
DEFAULT_MEMORY_BUDGET,
|
||||||
|
&temporary_directory,
|
||||||
|
) {
|
||||||
|
Ok(searcher) => searcher,
|
||||||
|
Err(error) => {
|
||||||
|
let _ = fs::remove_dir_all(&temporary_directory);
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Err(error) = searcher.build_index(build_search_documents(&documents)) {
|
||||||
|
let _ = fs::remove_dir_all(&temporary_directory);
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
// Close the writer before moving the directory so the committed generation can be
|
||||||
|
// reopened consistently on all supported platforms.
|
||||||
|
drop(searcher);
|
||||||
|
// The generation is complete and validated before it becomes visible through CURRENT.
|
||||||
|
fs::rename(&temporary_directory, &generation_directory)?;
|
||||||
|
let searcher = SimpleFullTextSearcher::open_in_dir(
|
||||||
|
&LOCAL_PROJECT_INDEX_SCHEMA,
|
||||||
|
DEFAULT_MEMORY_BUDGET,
|
||||||
|
&generation_directory,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let metadata = LocalIndexMetadata {
|
||||||
|
root_path: root_path.to_string_lossy().into_owned(),
|
||||||
|
schema_version: INDEX_SCHEMA_VERSION,
|
||||||
|
generation: generation.clone(),
|
||||||
|
file_count: documents.len(),
|
||||||
|
};
|
||||||
|
let metadata_json = serde_json::to_vec_pretty(&metadata)?;
|
||||||
|
// Store metadata inside the generation so an interrupted refresh cannot replace the
|
||||||
|
// metadata belonging to the generation still referenced by CURRENT.
|
||||||
|
fs::write(generation_directory.join(METADATA_FILE_NAME), metadata_json)?;
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
root_path,
|
||||||
|
BuiltLocalProjectIndex {
|
||||||
|
searcher,
|
||||||
|
generation,
|
||||||
|
generation_directory,
|
||||||
|
},
|
||||||
|
documents.len(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
.await;
|
||||||
|
result.map_err(|error| (error_root_path, error))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct LocalProjectDocument {
|
||||||
|
file_path: String,
|
||||||
|
symbol: String,
|
||||||
|
metadata: String,
|
||||||
|
body: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_search_documents(
|
||||||
|
documents: &[LocalProjectDocument],
|
||||||
|
) -> impl Iterator<Item = LocalProjectSearchDocument> + '_ {
|
||||||
|
documents
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.map(|document| LocalProjectSearchDocument {
|
||||||
|
symbol: document.symbol,
|
||||||
|
path: document.file_path.clone(),
|
||||||
|
metadata: document.metadata,
|
||||||
|
body: document.body,
|
||||||
|
file_path: document.file_path,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn build_documents(root_path: &Path) -> Result<Vec<LocalProjectDocument>> {
|
||||||
|
let root_path = dunce::canonicalize(root_path).with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Failed to canonicalize project root {}",
|
||||||
|
root_path.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let outline = build_outline(&root_path, Some(MAX_INDEX_FILES)).await?;
|
||||||
|
let mut documents = Vec::new();
|
||||||
|
for file in outline.to_file_symbols(None) {
|
||||||
|
let relative_path = PathBuf::from(&file.path);
|
||||||
|
let absolute_path = root_path.join(&relative_path);
|
||||||
|
let Ok(bytes) = fs::read(&absolute_path) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if bytes.is_empty() || bytes.len() > MAX_INDEXED_FILE_BYTES {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Ok(content) = String::from_utf8(bytes) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let body = content.chars().take(MAX_INDEXED_BODY_BYTES).collect();
|
||||||
|
let extension = absolute_path
|
||||||
|
.extension()
|
||||||
|
.and_then(|extension| extension.to_str())
|
||||||
|
.unwrap_or_default();
|
||||||
|
let metadata = format!("{extension} {}", file.symbols);
|
||||||
|
documents.push(LocalProjectDocument {
|
||||||
|
file_path: absolute_path.to_string_lossy().into_owned(),
|
||||||
|
symbol: file.symbols,
|
||||||
|
metadata,
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(documents)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_storage_directory_name(root_path: &Path) -> String {
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(root_path.to_string_lossy().as_bytes());
|
||||||
|
format!("{:x}", hasher.finalize())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn repository_storage_directory(storage_root: &Path, root_path: &Path) -> PathBuf {
|
||||||
|
storage_root.join(format_storage_directory_name(root_path))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn publish_generation(storage_root: &Path, root_path: &Path, generation: &str) -> Result<()> {
|
||||||
|
let index_directory = repository_storage_directory(storage_root, root_path);
|
||||||
|
let temporary_current = index_directory.join(format!(".{CURRENT_FILE_NAME}.tmp"));
|
||||||
|
fs::write(&temporary_current, generation.as_bytes())?;
|
||||||
|
fs::rename(&temporary_current, index_directory.join(CURRENT_FILE_NAME))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cleanup_old_generations(storage_root: &Path, root_path: &Path) {
|
||||||
|
let index_directory = repository_storage_directory(storage_root, root_path);
|
||||||
|
let Ok(current) = fs::read_to_string(index_directory.join(CURRENT_FILE_NAME)) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let current = current.trim();
|
||||||
|
let generations_directory = index_directory.join("generations");
|
||||||
|
let Ok(entries) = fs::read_dir(&generations_directory) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let generation = entry.file_name();
|
||||||
|
if generation.to_string_lossy() != current {
|
||||||
|
let _ = fs::remove_dir_all(entry.path());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_metadata(directory: &Path) -> Result<LocalIndexMetadata> {
|
||||||
|
let current = fs::read_to_string(directory.join(CURRENT_FILE_NAME))?;
|
||||||
|
let current = current.trim();
|
||||||
|
let mut components = Path::new(current).components();
|
||||||
|
if current.is_empty()
|
||||||
|
|| !matches!(components.next(), Some(std::path::Component::Normal(_)))
|
||||||
|
|| components.next().is_some()
|
||||||
|
{
|
||||||
|
anyhow::bail!("Invalid local index generation name");
|
||||||
|
}
|
||||||
|
let metadata_path = directory
|
||||||
|
.join("generations")
|
||||||
|
.join(current)
|
||||||
|
.join(METADATA_FILE_NAME);
|
||||||
|
let metadata: LocalIndexMetadata = serde_json::from_slice(&fs::read(metadata_path)?)?;
|
||||||
|
if metadata.generation != current {
|
||||||
|
anyhow::bail!("Local index generation pointer does not match metadata");
|
||||||
|
}
|
||||||
|
Ok(metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "tests.rs"]
|
||||||
|
mod tests;
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
use std::fs;
|
||||||
|
|
||||||
|
use tempfile::TempDir;
|
||||||
|
use warp_search_core::searcher::SimpleFullTextSearcher;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn local_index_documents_include_symbols_paths_and_body_text() {
|
||||||
|
let root = TempDir::new().unwrap();
|
||||||
|
fs::create_dir(root.path().join("src")).unwrap();
|
||||||
|
fs::write(
|
||||||
|
root.path().join("src/lib.rs"),
|
||||||
|
"/// Authenticate a request\npub fn authenticate_request() {}\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let documents = futures::executor::block_on(build_documents(root.path())).unwrap();
|
||||||
|
assert_eq!(documents.len(), 1);
|
||||||
|
assert!(documents[0].file_path.ends_with("src/lib.rs"));
|
||||||
|
assert!(documents[0].symbol.contains("authenticate_request"));
|
||||||
|
assert!(documents[0].body.contains("Authenticate a request"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn persistent_index_can_be_reopened_without_network_access() {
|
||||||
|
let root = TempDir::new().unwrap();
|
||||||
|
let storage = TempDir::new().unwrap();
|
||||||
|
fs::write(
|
||||||
|
root.path().join("main.rs"),
|
||||||
|
"fn local_authentication() {}\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let canonical_root = dunce::canonicalize(root.path()).unwrap();
|
||||||
|
let (root_path, built, _) = futures::executor::block_on(build_persisted_index(
|
||||||
|
canonical_root,
|
||||||
|
storage.path().to_path_buf(),
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
built
|
||||||
|
.searcher
|
||||||
|
.search_id("local_authentication")
|
||||||
|
.unwrap()
|
||||||
|
.len(),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
|
||||||
|
let directory = repository_storage_directory(storage.path(), &root_path);
|
||||||
|
publish_generation(storage.path(), &root_path, &built.generation).unwrap();
|
||||||
|
let metadata = read_metadata(&directory).unwrap();
|
||||||
|
let reopened = SimpleFullTextSearcher::open_in_dir(
|
||||||
|
&LOCAL_PROJECT_INDEX_SCHEMA,
|
||||||
|
DEFAULT_MEMORY_BUDGET,
|
||||||
|
&directory.join("generations").join(metadata.generation),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(reopened.search_id("local_authentication").unwrap().len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn partial_paths_filter_local_search_results() {
|
||||||
|
let root = TempDir::new().unwrap();
|
||||||
|
fs::create_dir(root.path().join("src")).unwrap();
|
||||||
|
fs::create_dir(root.path().join("tests")).unwrap();
|
||||||
|
fs::write(root.path().join("src/lib.rs"), "fn authentication() {}\n").unwrap();
|
||||||
|
fs::write(root.path().join("tests/lib.rs"), "fn authentication() {}\n").unwrap();
|
||||||
|
|
||||||
|
let storage = TempDir::new().unwrap();
|
||||||
|
let canonical_root = dunce::canonicalize(root.path()).unwrap();
|
||||||
|
let (_, built, file_count) = futures::executor::block_on(build_persisted_index(
|
||||||
|
canonical_root.clone(),
|
||||||
|
storage.path().to_path_buf(),
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
let manager = LocalProjectIndex {
|
||||||
|
root_path: canonical_root,
|
||||||
|
searcher: built.searcher,
|
||||||
|
file_count,
|
||||||
|
};
|
||||||
|
|
||||||
|
let matches = manager
|
||||||
|
.search("authentication", Some(&["src".to_string()]))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(matches.len(), 1);
|
||||||
|
assert!(matches[0].path.ends_with("src/lib.rs"));
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
mod file_outline;
|
mod file_outline;
|
||||||
|
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
||||||
|
pub mod local_project_index;
|
||||||
pub mod locations;
|
pub mod locations;
|
||||||
pub const DEFAULT_SYNC_REQUESTS_PER_MIN: u32 = 600;
|
pub const DEFAULT_SYNC_REQUESTS_PER_MIN: u32 = 600;
|
||||||
|
|
||||||
|
|||||||
@@ -35,13 +35,33 @@ impl ExecutionMode {
|
|||||||
pub struct AppExecutionMode {
|
pub struct AppExecutionMode {
|
||||||
mode: ExecutionMode,
|
mode: ExecutionMode,
|
||||||
is_sandboxed: bool,
|
is_sandboxed: bool,
|
||||||
|
local_project_indexing_enabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppExecutionMode {
|
impl AppExecutionMode {
|
||||||
/// Create an `AppExecutionMode` model with the execution mode set.
|
/// Create an `AppExecutionMode` model with the execution mode set.
|
||||||
pub fn new(mode: ExecutionMode, is_sandboxed: bool, _ctx: &mut ModelContext<Self>) -> Self {
|
pub fn new(mode: ExecutionMode, is_sandboxed: bool, ctx: &mut ModelContext<Self>) -> Self {
|
||||||
|
Self::new_with_local_project_indexing(
|
||||||
|
mode,
|
||||||
|
is_sandboxed,
|
||||||
|
matches!(mode, ExecutionMode::App | ExecutionMode::Sdk),
|
||||||
|
ctx,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create an execution-mode model with an explicit local project-index capability.
|
||||||
|
pub fn new_with_local_project_indexing(
|
||||||
|
mode: ExecutionMode,
|
||||||
|
is_sandboxed: bool,
|
||||||
|
local_project_indexing_enabled: bool,
|
||||||
|
_ctx: &mut ModelContext<Self>,
|
||||||
|
) -> Self {
|
||||||
let _ = GLOBAL_EXECUTION_MODE.set(mode);
|
let _ = GLOBAL_EXECUTION_MODE.set(mode);
|
||||||
Self { mode, is_sandboxed }
|
Self {
|
||||||
|
mode,
|
||||||
|
is_sandboxed,
|
||||||
|
local_project_indexing_enabled,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// True if running as the full desktop app.
|
/// True if running as the full desktop app.
|
||||||
@@ -121,6 +141,18 @@ impl AppExecutionMode {
|
|||||||
pub fn is_sandboxed(&self) -> bool {
|
pub fn is_sandboxed(&self) -> bool {
|
||||||
self.is_sandboxed
|
self.is_sandboxed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns whether this process is the remote-server daemon. Native local app and SDK
|
||||||
|
/// sessions use the local project index, while the daemon must retain its remote indexing
|
||||||
|
/// pipeline.
|
||||||
|
pub fn is_remote_server_daemon(&self) -> bool {
|
||||||
|
matches!(self.mode, ExecutionMode::RemoteServerDaemon)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns whether local project indexing is registered and may be used in this process.
|
||||||
|
pub fn local_project_indexing_enabled(&self) -> bool {
|
||||||
|
self.local_project_indexing_enabled
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Entity for AppExecutionMode {
|
impl Entity for AppExecutionMode {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::iter::Peekable;
|
use std::iter::Peekable;
|
||||||
|
use std::path::Path;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::thread::available_parallelism;
|
use std::thread::available_parallelism;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
@@ -474,7 +475,7 @@ impl SearcherWriterWrapper {
|
|||||||
MAX_THREADS_PER_INDEX_WRITER,
|
MAX_THREADS_PER_INDEX_WRITER,
|
||||||
);
|
);
|
||||||
let writer = search_index
|
let writer = search_index
|
||||||
.writer_with_num_threads(memory_budget, num_threads)
|
.writer_with_num_threads(num_threads, memory_budget)
|
||||||
.ok();
|
.ok();
|
||||||
|
|
||||||
SearcherWriterWrapper {
|
SearcherWriterWrapper {
|
||||||
@@ -653,40 +654,99 @@ pub struct SimpleFullTextSearcher<C: SearchSchemaConfig> {
|
|||||||
|
|
||||||
impl<C: SearchSchemaConfig> SimpleFullTextSearcher<C> {
|
impl<C: SearchSchemaConfig> SimpleFullTextSearcher<C> {
|
||||||
pub fn new(schema: &FullTextSearchSchema<C>, memory_budget: usize) -> Self {
|
pub fn new(schema: &FullTextSearchSchema<C>, memory_budget: usize) -> Self {
|
||||||
let mut schema_builder = Schema::builder();
|
let (
|
||||||
|
tantivy_schema,
|
||||||
// Add composite key field for efficient term querying
|
composite_key_field,
|
||||||
let composite_key_field = schema_builder.add_bytes_field(
|
weighted_search_fields,
|
||||||
COMPOSITE_KEY_FIELD,
|
id_fields,
|
||||||
BytesOptions::default().set_indexed().set_stored(),
|
normalizing_factor,
|
||||||
);
|
) = build_tantivy_schema(schema);
|
||||||
|
Self::new_with_index(
|
||||||
let mut weighted_search_fields = HashMap::new();
|
Arc::new(Index::create_in_ram(tantivy_schema)),
|
||||||
let mut normalizing_factor = 0.0;
|
composite_key_field,
|
||||||
for (field_name, weight) in schema.weighted_search_fields.iter() {
|
weighted_search_fields,
|
||||||
let text_indexing = TEXT
|
id_fields,
|
||||||
.get_indexing_options()
|
normalizing_factor,
|
||||||
.cloned()
|
memory_budget,
|
||||||
.unwrap_or(
|
|
||||||
TextFieldIndexing::default()
|
|
||||||
.set_index_option(IndexRecordOption::WithFreqsAndPositions),
|
|
||||||
)
|
)
|
||||||
.set_tokenizer("custom");
|
|
||||||
let text_option = TEXT.clone().set_indexing_options(text_indexing) | STORED;
|
|
||||||
|
|
||||||
let field = schema_builder.add_text_field(field_name, text_option);
|
|
||||||
weighted_search_fields.insert(field_name.clone(), (field, *weight));
|
|
||||||
normalizing_factor += weight;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut id_fields = HashMap::new();
|
/// Opens an existing Tantivy index in `directory` and validates its schema.
|
||||||
for (field_name, field_type) in schema.id_fields.iter() {
|
pub fn open_in_dir(
|
||||||
let field =
|
schema: &FullTextSearchSchema<C>,
|
||||||
schema_builder.add_field(field_type.field_entry_from_name(field_name.clone()));
|
memory_budget: usize,
|
||||||
id_fields.insert(field_name.clone(), (field, *field_type));
|
directory: &Path,
|
||||||
|
) -> anyhow::Result<Self> {
|
||||||
|
let (
|
||||||
|
expected_schema,
|
||||||
|
composite_key_field,
|
||||||
|
weighted_search_fields,
|
||||||
|
id_fields,
|
||||||
|
normalizing_factor,
|
||||||
|
) = build_tantivy_schema(schema);
|
||||||
|
let search_index = Index::open_in_dir(directory)?;
|
||||||
|
if search_index.schema() != expected_schema {
|
||||||
|
anyhow::bail!("Persistent Tantivy index schema does not match the requested schema");
|
||||||
|
}
|
||||||
|
Ok(Self::new_with_index(
|
||||||
|
Arc::new(search_index),
|
||||||
|
composite_key_field,
|
||||||
|
weighted_search_fields,
|
||||||
|
id_fields,
|
||||||
|
normalizing_factor,
|
||||||
|
memory_budget,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
let search_index = Arc::new(Index::create_in_ram(schema_builder.build()));
|
/// Creates a new Tantivy index in `directory` and validates its schema.
|
||||||
|
pub fn create_in_dir(
|
||||||
|
schema: &FullTextSearchSchema<C>,
|
||||||
|
memory_budget: usize,
|
||||||
|
directory: &Path,
|
||||||
|
) -> anyhow::Result<Self> {
|
||||||
|
std::fs::create_dir_all(directory)?;
|
||||||
|
let (
|
||||||
|
tantivy_schema,
|
||||||
|
composite_key_field,
|
||||||
|
weighted_search_fields,
|
||||||
|
id_fields,
|
||||||
|
normalizing_factor,
|
||||||
|
) = build_tantivy_schema(schema);
|
||||||
|
let search_index = Index::create_in_dir(directory, tantivy_schema)?;
|
||||||
|
Ok(Self::new_with_index(
|
||||||
|
Arc::new(search_index),
|
||||||
|
composite_key_field,
|
||||||
|
weighted_search_fields,
|
||||||
|
id_fields,
|
||||||
|
normalizing_factor,
|
||||||
|
memory_budget,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Opens or creates a Tantivy index in `directory`.
|
||||||
|
///
|
||||||
|
/// This compatibility constructor is intended for callers that explicitly accept the
|
||||||
|
/// open-or-create behavior. Persistent committed generations should use [`Self::open_in_dir`]
|
||||||
|
/// and new temporary generations should use [`Self::create_in_dir`].
|
||||||
|
pub fn new_in_dir(
|
||||||
|
schema: &FullTextSearchSchema<C>,
|
||||||
|
memory_budget: usize,
|
||||||
|
directory: &Path,
|
||||||
|
) -> anyhow::Result<Self> {
|
||||||
|
match Self::open_in_dir(schema, memory_budget, directory) {
|
||||||
|
Ok(searcher) => Ok(searcher),
|
||||||
|
Err(_) => Self::create_in_dir(schema, memory_budget, directory),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_with_index(
|
||||||
|
search_index: Arc<Index>,
|
||||||
|
composite_key_field: Field,
|
||||||
|
weighted_search_fields: HashMap<String, (Field, f32)>,
|
||||||
|
id_fields: HashMap<String, (Field, FullTextSearchFieldTypes)>,
|
||||||
|
normalizing_factor: f32,
|
||||||
|
memory_budget: usize,
|
||||||
|
) -> Self {
|
||||||
search_index
|
search_index
|
||||||
.tokenizers()
|
.tokenizers()
|
||||||
.register("custom", CustomTokenizer::default());
|
.register("custom", CustomTokenizer::default());
|
||||||
@@ -695,10 +755,8 @@ impl<C: SearchSchemaConfig> SimpleFullTextSearcher<C> {
|
|||||||
search_index,
|
search_index,
|
||||||
weighted_search_fields,
|
weighted_search_fields,
|
||||||
id_fields,
|
id_fields,
|
||||||
// The normalization is done via division, so in order to boost the score, we divide by the boost factor.
|
normalizing_factor,
|
||||||
normalizing_factor / schema.boost_factor,
|
|
||||||
)));
|
)));
|
||||||
|
|
||||||
let writer = Arc::new(Mutex::new(SearcherWriterWrapper::new(
|
let writer = Arc::new(Mutex::new(SearcherWriterWrapper::new(
|
||||||
reader.clone(),
|
reader.clone(),
|
||||||
composite_key_field,
|
composite_key_field,
|
||||||
@@ -841,6 +899,59 @@ impl<C: SearchSchemaConfig> SimpleFullTextSearcher<C> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TantivySchemaParts = (
|
||||||
|
Schema,
|
||||||
|
Field,
|
||||||
|
HashMap<String, (Field, f32)>,
|
||||||
|
HashMap<String, (Field, FullTextSearchFieldTypes)>,
|
||||||
|
f32,
|
||||||
|
);
|
||||||
|
|
||||||
|
fn build_tantivy_schema<C: SearchSchemaConfig>(
|
||||||
|
schema: &FullTextSearchSchema<C>,
|
||||||
|
) -> TantivySchemaParts {
|
||||||
|
let mut schema_builder = Schema::builder();
|
||||||
|
let composite_key_field = schema_builder.add_bytes_field(
|
||||||
|
COMPOSITE_KEY_FIELD,
|
||||||
|
BytesOptions::default().set_indexed().set_stored(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut weighted_search_fields = HashMap::new();
|
||||||
|
let mut normalizing_factor = 0.0;
|
||||||
|
let mut weighted_fields = schema.weighted_search_fields.iter().collect_vec();
|
||||||
|
weighted_fields.sort_by_key(|(left, _)| *left);
|
||||||
|
for (field_name, weight) in weighted_fields {
|
||||||
|
let text_indexing = TEXT
|
||||||
|
.get_indexing_options()
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or(
|
||||||
|
TextFieldIndexing::default()
|
||||||
|
.set_index_option(IndexRecordOption::WithFreqsAndPositions),
|
||||||
|
)
|
||||||
|
.set_tokenizer("custom");
|
||||||
|
let text_option = TEXT.clone().set_indexing_options(text_indexing) | STORED;
|
||||||
|
let field = schema_builder.add_text_field(field_name, text_option);
|
||||||
|
weighted_search_fields.insert(field_name.clone(), (field, *weight));
|
||||||
|
normalizing_factor += weight;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut id_fields = HashMap::new();
|
||||||
|
let mut id_field_entries = schema.id_fields.iter().collect_vec();
|
||||||
|
id_field_entries.sort_by_key(|(left, _)| *left);
|
||||||
|
for (field_name, field_type) in id_field_entries {
|
||||||
|
let field = schema_builder.add_field(field_type.field_entry_from_name(field_name.clone()));
|
||||||
|
id_fields.insert(field_name.clone(), (field, *field_type));
|
||||||
|
}
|
||||||
|
|
||||||
|
(
|
||||||
|
schema_builder.build(),
|
||||||
|
composite_key_field,
|
||||||
|
weighted_search_fields,
|
||||||
|
id_fields,
|
||||||
|
normalizing_factor / schema.boost_factor,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn build_term_query(term: Term) -> Box<BooleanQuery> {
|
fn build_term_query(term: Term) -> Box<BooleanQuery> {
|
||||||
let term_query = Box::new(TermQuery::new(
|
let term_query = Box::new(TermQuery::new(
|
||||||
term.clone(),
|
term.clone(),
|
||||||
|
|||||||
Reference in New Issue
Block a user