first pass of merging in warp (doesn't build)
This commit is contained in:
@@ -1,38 +1,35 @@
|
||||
use ai::index::{
|
||||
full_source_code_embedding::{
|
||||
manager::{CodebaseIndexManager, CodebaseIndexManagerEvent},
|
||||
RetrievalID,
|
||||
},
|
||||
locations::CodeContextLocation,
|
||||
use std::collections::HashSet;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use ai::index::full_source_code_embedding::manager::{
|
||||
CodebaseIndexManager, CodebaseIndexManagerEvent,
|
||||
};
|
||||
use ai::index::full_source_code_embedding::RetrievalID;
|
||||
use ai::index::locations::CodeContextLocation;
|
||||
use anyhow::anyhow;
|
||||
use futures_util::stream::AbortHandle;
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
use instant::Instant;
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use galaxyui::{AppContext, Entity, ModelContext, ModelHandle, SingletonEntity};
|
||||
|
||||
use galaxyui::{AppContext, Entity, ModelContext, SingletonEntity};
|
||||
|
||||
use crate::{
|
||||
ai::{
|
||||
agent::AIAgentActionId,
|
||||
get_relevant_files::api::{FileContext, GetRelevantFiles},
|
||||
outline::{OutlineStatus, RepoOutlines},
|
||||
},
|
||||
report_error, send_telemetry_from_ctx,
|
||||
server::server_api::{AIApiError, ServerApiProvider},
|
||||
TelemetryEvent,
|
||||
};
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
use crate::ai::agent::SearchCodebaseFailureReason;
|
||||
use crate::ai::agent::{AIAgentActionId, SearchCodebaseResult};
|
||||
use crate::ai::blocklist::SessionContext;
|
||||
use crate::ai::get_relevant_files::api::{FileContext as FileContextRequest, GetRelevantFiles};
|
||||
use crate::ai::outline::{OutlineStatus, RepoOutlines};
|
||||
use crate::server::server_api::{AIApiError, ServerApiProvider};
|
||||
use crate::{report_error, send_telemetry_from_ctx, TelemetryEvent};
|
||||
#[cfg_attr(not(target_family = "wasm"), path = "remote_search/native.rs")]
|
||||
#[cfg_attr(target_family = "wasm", path = "remote_search/wasm.rs")]
|
||||
mod remote_search;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum GetRelevantFilesControllerEvent {
|
||||
Success {
|
||||
action_id: AIAgentActionId,
|
||||
fragments: Arc<HashSet<CodeContextLocation>>,
|
||||
result: GetRelevantFilesControllerResult,
|
||||
},
|
||||
Error {
|
||||
action_id: AIAgentActionId,
|
||||
@@ -48,6 +45,21 @@ impl GetRelevantFilesControllerEvent {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum GetRelevantFilesControllerResult {
|
||||
Locations(Arc<HashSet<CodeContextLocation>>),
|
||||
SearchResult(SearchCodebaseResult),
|
||||
}
|
||||
|
||||
pub enum GetRelevantFilesRequestTarget {
|
||||
Local {
|
||||
directory: PathBuf,
|
||||
},
|
||||
Remote {
|
||||
session_context: SessionContext,
|
||||
requested_codebase_path: Option<String>,
|
||||
},
|
||||
}
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum GetRelevantFilesError {
|
||||
#[error("Repo outline is still being computed.")]
|
||||
@@ -130,6 +142,7 @@ impl GetRelevantFilesController {
|
||||
|
||||
fn handle_codebase_manager_event(
|
||||
&mut self,
|
||||
_: ModelHandle<CodebaseIndexManager>,
|
||||
codebase_manager_event: &CodebaseIndexManagerEvent,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) {
|
||||
@@ -188,6 +201,35 @@ impl GetRelevantFilesController {
|
||||
|
||||
/// Start a new search query based on the repo outline.
|
||||
pub fn send_request(
|
||||
&mut self,
|
||||
target: GetRelevantFilesRequestTarget,
|
||||
query: String,
|
||||
partial_path_segments: Option<&Vec<String>>,
|
||||
action_id: AIAgentActionId,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) -> Result<(), GetRelevantFilesError> {
|
||||
// Cancel any previous request for this action before dispatching to either the local or
|
||||
// remote implementation.
|
||||
self.cancel_request_for_action(&action_id, ctx);
|
||||
match target {
|
||||
GetRelevantFilesRequestTarget::Local { directory } => {
|
||||
self.send_local_request(&directory, query, partial_path_segments, action_id, ctx)
|
||||
}
|
||||
GetRelevantFilesRequestTarget::Remote {
|
||||
session_context,
|
||||
requested_codebase_path,
|
||||
} => self.send_remote_request(
|
||||
session_context,
|
||||
requested_codebase_path,
|
||||
query,
|
||||
partial_path_segments.cloned(),
|
||||
action_id,
|
||||
ctx,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn send_local_request(
|
||||
&mut self,
|
||||
directory: &Path,
|
||||
query: String,
|
||||
@@ -196,7 +238,6 @@ impl GetRelevantFilesController {
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) -> Result<(), GetRelevantFilesError> {
|
||||
const MINIMUM_FILE_COUNT_FOR_API_CALL: usize = 2;
|
||||
self.cancel_request_for_action(&action_id, ctx);
|
||||
|
||||
if FeatureFlag::FullSourceCodeEmbedding.is_enabled() {
|
||||
let codebase_mgr = CodebaseIndexManager::handle(ctx);
|
||||
@@ -235,21 +276,21 @@ impl GetRelevantFilesController {
|
||||
if file_outlines.len() < MINIMUM_FILE_COUNT_FOR_API_CALL {
|
||||
ctx.emit(GetRelevantFilesControllerEvent::Success {
|
||||
action_id,
|
||||
fragments: Arc::new(
|
||||
result: GetRelevantFilesControllerResult::Locations(Arc::new(
|
||||
file_outlines
|
||||
.into_iter()
|
||||
.map(|file| {
|
||||
CodeContextLocation::WholeFile(PathBuf::from(file.path))
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
)),
|
||||
});
|
||||
} else {
|
||||
let outline_request = GetRelevantFiles {
|
||||
query,
|
||||
files: file_outlines
|
||||
.into_iter()
|
||||
.map(|outline| FileContext {
|
||||
.map(|outline| FileContextRequest {
|
||||
path: outline.path,
|
||||
symbols: outline.symbols,
|
||||
})
|
||||
@@ -302,6 +343,38 @@ impl GetRelevantFilesController {
|
||||
}
|
||||
}
|
||||
|
||||
fn send_remote_request(
|
||||
&mut self,
|
||||
session_context: SessionContext,
|
||||
requested_codebase_path: Option<String>,
|
||||
query: String,
|
||||
partial_path_segments: Option<Vec<String>>,
|
||||
action_id: AIAgentActionId,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) -> Result<(), GetRelevantFilesError> {
|
||||
match remote_search::send_request(
|
||||
query,
|
||||
partial_path_segments,
|
||||
session_context,
|
||||
requested_codebase_path,
|
||||
action_id.clone(),
|
||||
ctx,
|
||||
) {
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
remote_search::RemoteSearchRequest::Pending(abort_handle) => {
|
||||
self.pending_requests
|
||||
.insert(action_id, RequestHandle::AbortHandle(abort_handle));
|
||||
}
|
||||
remote_search::RemoteSearchRequest::Ready(result) => {
|
||||
ctx.emit(GetRelevantFilesControllerEvent::Success {
|
||||
action_id,
|
||||
result: GetRelevantFilesControllerResult::SearchResult(result),
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_relevant_file_paths_result(
|
||||
&mut self,
|
||||
relevant_file_locations: anyhow::Result<Arc<HashSet<CodeContextLocation>>>,
|
||||
@@ -315,7 +388,7 @@ impl GetRelevantFilesController {
|
||||
Ok(relevant_file_locations) => {
|
||||
ctx.emit(GetRelevantFilesControllerEvent::Success {
|
||||
action_id,
|
||||
fragments: relevant_file_locations,
|
||||
result: GetRelevantFilesControllerResult::Locations(relevant_file_locations),
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -325,6 +398,27 @@ impl GetRelevantFilesController {
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn handle_remote_search_result(
|
||||
&mut self,
|
||||
search_result: anyhow::Result<SearchCodebaseResult>,
|
||||
action_id: AIAgentActionId,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) {
|
||||
if self.pending_requests.remove(&action_id).is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
let result = search_result.unwrap_or_else(|e| SearchCodebaseResult::Failed {
|
||||
reason: SearchCodebaseFailureReason::ClientError,
|
||||
message: e.to_string(),
|
||||
});
|
||||
ctx.emit(GetRelevantFilesControllerEvent::Success {
|
||||
action_id,
|
||||
result: GetRelevantFilesControllerResult::SearchResult(result),
|
||||
});
|
||||
}
|
||||
|
||||
/// Returns the path to the root directory for a codebase search where pwd is `directory`.
|
||||
pub fn root_directory_for_search(&self, directory: &Path, app: &AppContext) -> Option<PathBuf> {
|
||||
let mut start = None;
|
||||
@@ -338,6 +432,15 @@ impl GetRelevantFilesController {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn root_directory_for_remote_search(
|
||||
&self,
|
||||
session_context: &SessionContext,
|
||||
requested_codebase_path: Option<&str>,
|
||||
app: &AppContext,
|
||||
) -> Option<PathBuf> {
|
||||
remote_search::root_directory_for_search(session_context, requested_codebase_path, app)
|
||||
}
|
||||
|
||||
pub fn cancel_request_for_action(
|
||||
&mut self,
|
||||
action_id: &AIAgentActionId,
|
||||
|
||||
@@ -0,0 +1,455 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use ::ai::index::full_source_code_embedding::search_shaping::{
|
||||
build_fragments_from_file_contents, fragments_to_context_locations,
|
||||
};
|
||||
use ::ai::index::full_source_code_embedding::store_client::StoreClient;
|
||||
use ::ai::index::full_source_code_embedding::{
|
||||
ContentHash, FragmentMetadata as AiFragmentMetadata, FragmentMetadataLocation, RepoMetadata,
|
||||
};
|
||||
use ::ai::index::locations::CodeContextLocation;
|
||||
use itertools::Itertools;
|
||||
use remote_server::proto::{
|
||||
file_context_proto, FragmentMetadata as ProtoFragmentMetadata, LineRange, ReadFileContextFile,
|
||||
ReadFileContextRequest, ReadFileContextResponse,
|
||||
};
|
||||
use string_offset::ByteOffset;
|
||||
use warpui::{AppContext, ModelContext, SingletonEntity};
|
||||
|
||||
use crate::ai::agent::{
|
||||
AnyFileContent, FileContext, SearchCodebaseFailureReason, SearchCodebaseResult,
|
||||
};
|
||||
use crate::ai::blocklist::SessionContext;
|
||||
use crate::ai::codebase_auto_indexing::{
|
||||
should_use_codebase_indexing, CodebaseAutoIndexingSurface,
|
||||
};
|
||||
use crate::ai::get_relevant_files::controller::GetRelevantFilesController;
|
||||
use crate::remote_server::codebase_index_model::{
|
||||
RemoteCodebaseIndexModel, RemoteCodebaseSearchAvailability, RemoteCodebaseSearchContext,
|
||||
};
|
||||
use crate::server::server_api::{ServerApi, ServerApiProvider};
|
||||
|
||||
pub(super) enum RemoteSearchRequest {
|
||||
Pending(futures_util::stream::AbortHandle),
|
||||
Ready(SearchCodebaseResult),
|
||||
}
|
||||
|
||||
pub(super) fn root_directory_for_search(
|
||||
session_context: &SessionContext,
|
||||
requested_codebase_path: Option<&str>,
|
||||
app: &AppContext,
|
||||
) -> Option<PathBuf> {
|
||||
RemoteCodebaseIndexModel::as_ref(app)
|
||||
.active_repo_path(session_context, requested_codebase_path)
|
||||
.or_else(|| {
|
||||
requested_codebase_path
|
||||
.is_none()
|
||||
.then(|| session_context.current_working_directory().clone())
|
||||
.flatten()
|
||||
})
|
||||
.map(PathBuf::from)
|
||||
}
|
||||
|
||||
pub(super) fn send_request(
|
||||
query: String,
|
||||
partial_paths: Option<Vec<String>>,
|
||||
session_context: SessionContext,
|
||||
requested_codebase_path: Option<String>,
|
||||
action_id: crate::ai::agent::AIAgentActionId,
|
||||
ctx: &mut ModelContext<GetRelevantFilesController>,
|
||||
) -> RemoteSearchRequest {
|
||||
if !should_use_codebase_indexing(CodebaseAutoIndexingSurface::Remote, ctx) {
|
||||
return RemoteSearchRequest::Ready(SearchCodebaseResult::Failed {
|
||||
reason: SearchCodebaseFailureReason::CodebaseNotIndexed,
|
||||
message: "Remote codebase search is not enabled.".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let availability = RemoteCodebaseIndexModel::as_ref(ctx)
|
||||
.active_repo_availability(&session_context, requested_codebase_path.as_deref());
|
||||
match availability {
|
||||
RemoteCodebaseSearchAvailability::Ready(search_context) => {
|
||||
let handle = remote_server::manager::RemoteServerManager::as_ref(ctx)
|
||||
.host_request_handle(&search_context.remote_path.host_id);
|
||||
if search_context.is_stale {
|
||||
let remote_path = search_context.remote_path.clone();
|
||||
let sync_requested = remote_server::manager::RemoteServerManager::handle(ctx)
|
||||
.update(ctx, |manager, ctx| {
|
||||
manager.trigger_codebase_incremental_sync(remote_path, ctx)
|
||||
});
|
||||
if !sync_requested {
|
||||
log::warn!(
|
||||
"Remote codebase search is using a stale index because incremental sync could not be requested"
|
||||
);
|
||||
}
|
||||
}
|
||||
let store_client = ServerApiProvider::as_ref(ctx).get();
|
||||
let abort_handle = ctx
|
||||
.spawn(
|
||||
async move {
|
||||
execute_remote_codebase_search(
|
||||
query,
|
||||
partial_paths,
|
||||
search_context,
|
||||
handle,
|
||||
store_client,
|
||||
)
|
||||
.await
|
||||
},
|
||||
move |me, result, ctx| {
|
||||
me.handle_remote_search_result(result, action_id, ctx);
|
||||
},
|
||||
)
|
||||
.abort_handle();
|
||||
RemoteSearchRequest::Pending(abort_handle)
|
||||
}
|
||||
availability @ RemoteCodebaseSearchAvailability::NotIndexed { .. } => {
|
||||
RemoteSearchRequest::Ready(remote_availability_failure(availability))
|
||||
}
|
||||
RemoteCodebaseSearchAvailability::NoConnectedHost
|
||||
| RemoteCodebaseSearchAvailability::NoActiveRepo
|
||||
| RemoteCodebaseSearchAvailability::Indexing { .. }
|
||||
| RemoteCodebaseSearchAvailability::Unavailable { .. } => {
|
||||
RemoteSearchRequest::Ready(remote_availability_failure(availability))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The controller owns request lifecycle concerns like cancellation, pending request tracking, and
|
||||
// result emission. This function only contains the remote-specific pipeline: store content hashes
|
||||
// -> daemon fragment metadata -> remote file reads -> fragment reranking.
|
||||
async fn execute_remote_codebase_search(
|
||||
query: String,
|
||||
partial_paths: Option<Vec<String>>,
|
||||
search_context: RemoteCodebaseSearchContext,
|
||||
handle: remote_server::manager::HostRequestHandle,
|
||||
store_client: Arc<ServerApi>,
|
||||
) -> Result<SearchCodebaseResult, anyhow::Error> {
|
||||
let root_hash = search_context.root_hash;
|
||||
let root_hash_string = root_hash.to_string();
|
||||
let repo_path = search_context.remote_path.path.as_str().to_string();
|
||||
let embedding_config = store_client
|
||||
.codebase_context_config()
|
||||
.await?
|
||||
.embedding_config;
|
||||
log::debug!(
|
||||
"[Remote codebase indexing] Remote codebase search using embedding config: repo_path={repo_path} embedding_config={embedding_config:?}"
|
||||
);
|
||||
let candidate_hashes = store_client
|
||||
.get_relevant_fragments(
|
||||
embedding_config,
|
||||
query.clone(),
|
||||
root_hash,
|
||||
RepoMetadata {
|
||||
path: Some(repo_path.clone()),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
if candidate_hashes.is_empty() {
|
||||
return Ok(SearchCodebaseResult::Success { files: vec![] });
|
||||
}
|
||||
|
||||
let candidate_hash_strings = candidate_hashes
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect_vec();
|
||||
let metadata_response = handle
|
||||
.get_fragment_metadata_from_hash(
|
||||
repo_path.clone(),
|
||||
root_hash_string,
|
||||
candidate_hash_strings,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Fragment metadata lookup failed: {e}"))?;
|
||||
if !metadata_response.missing_hashes.is_empty() {
|
||||
log::warn!(
|
||||
"Remote codebase search metadata lookup missed {} hashes for repo {}",
|
||||
metadata_response.missing_hashes.len(),
|
||||
repo_path
|
||||
);
|
||||
}
|
||||
let mut remote_fragments = metadata_response.fragments;
|
||||
if let Some(partial_paths) = partial_paths {
|
||||
remote_fragments.retain(|fragment| {
|
||||
partial_paths
|
||||
.iter()
|
||||
.any(|partial_path| fragment.path.contains(partial_path))
|
||||
});
|
||||
}
|
||||
if remote_fragments.is_empty() {
|
||||
return Ok(SearchCodebaseResult::Success { files: vec![] });
|
||||
}
|
||||
|
||||
// Convert daemon protobuf metadata into the shared search-shaping metadata format so
|
||||
// remote search reuses the same fragment reconstruction and context expansion path as
|
||||
// local search.
|
||||
let parsed_fragment_metadata = remote_fragments
|
||||
.into_iter()
|
||||
.filter_map(|fragment| match remote_fragment_metadata(fragment) {
|
||||
Ok(metadata) => Some(metadata),
|
||||
Err(err) => {
|
||||
log::warn!("Failed to parse remote codebase fragment metadata: {err:?}");
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect_vec();
|
||||
if parsed_fragment_metadata.is_empty() {
|
||||
return Ok(SearchCodebaseResult::Success { files: vec![] });
|
||||
}
|
||||
|
||||
let response = handle
|
||||
.read_file_context(read_full_fragment_files_request(&parsed_fragment_metadata))
|
||||
.await?;
|
||||
if !response.failed_files.is_empty() && response.file_contexts.is_empty() {
|
||||
let failed = response
|
||||
.failed_files
|
||||
.iter()
|
||||
.map(|file| {
|
||||
let reason = file
|
||||
.error
|
||||
.as_ref()
|
||||
.map(|error| error.message.as_str())
|
||||
.unwrap_or("unknown error");
|
||||
format!("{}: {reason}", file.path)
|
||||
})
|
||||
.join(", ");
|
||||
return Ok(SearchCodebaseResult::Failed {
|
||||
reason: SearchCodebaseFailureReason::InvalidFilePaths,
|
||||
message: format!("Failed to read remote search result files: {failed}"),
|
||||
});
|
||||
}
|
||||
let file_contents = file_contents_from_response(response);
|
||||
let read_fragment_result = build_fragments_from_file_contents(
|
||||
parsed_fragment_metadata.iter().cloned(),
|
||||
&file_contents,
|
||||
);
|
||||
if !read_fragment_result.fail_to_read_path.is_empty() {
|
||||
log::warn!(
|
||||
"Remote codebase search failed to read {} fragment file(s)",
|
||||
read_fragment_result.fail_to_read_path.len()
|
||||
);
|
||||
}
|
||||
let fragments = read_fragment_result.successfully_read;
|
||||
if fragments.is_empty() {
|
||||
return Ok(SearchCodebaseResult::Success { files: vec![] });
|
||||
}
|
||||
|
||||
let reranked_fragments = store_client.rerank_fragments(query, fragments).await?;
|
||||
let metadata_by_hash = fragment_metadata_by_hash(&parsed_fragment_metadata);
|
||||
let locations = fragments_to_context_locations(
|
||||
reranked_fragments,
|
||||
|hash| metadata_by_hash.get(hash).map(Vec::as_slice),
|
||||
RETRIEVE_FRAGMENT_CONTEXT_LENGTH,
|
||||
);
|
||||
if locations.is_empty() {
|
||||
return Ok(SearchCodebaseResult::Success { files: vec![] });
|
||||
}
|
||||
|
||||
let response = handle
|
||||
.read_file_context(read_context_locations_request(&locations))
|
||||
.await?;
|
||||
if !response.failed_files.is_empty() && response.file_contexts.is_empty() {
|
||||
let failed = response
|
||||
.failed_files
|
||||
.iter()
|
||||
.map(|file| {
|
||||
let reason = file
|
||||
.error
|
||||
.as_ref()
|
||||
.map(|error| error.message.as_str())
|
||||
.unwrap_or("unknown error");
|
||||
format!("{}: {reason}", file.path)
|
||||
})
|
||||
.join(", ");
|
||||
return Ok(SearchCodebaseResult::Failed {
|
||||
reason: SearchCodebaseFailureReason::InvalidFilePaths,
|
||||
message: format!("Failed to read remote search result files: {failed}"),
|
||||
});
|
||||
}
|
||||
let files = response
|
||||
.file_contexts
|
||||
.into_iter()
|
||||
.filter_map(proto_file_context_to_file_context)
|
||||
.collect_vec();
|
||||
|
||||
Ok(SearchCodebaseResult::Success { files })
|
||||
}
|
||||
|
||||
const RETRIEVE_FRAGMENT_CONTEXT_LENGTH: usize = 0;
|
||||
|
||||
fn remote_fragment_metadata(
|
||||
fragment: ProtoFragmentMetadata,
|
||||
) -> anyhow::Result<(ContentHash, AiFragmentMetadata)> {
|
||||
let content_hash = ContentHash::from_str(&fragment.content_hash)?;
|
||||
Ok((
|
||||
content_hash,
|
||||
AiFragmentMetadata {
|
||||
absolute_path: PathBuf::from(fragment.path),
|
||||
location: FragmentMetadataLocation {
|
||||
start_line: fragment.start_line as usize,
|
||||
end_line: fragment.end_line as usize,
|
||||
byte_range: ByteOffset::from(fragment.byte_start as usize)
|
||||
..ByteOffset::from(fragment.byte_end as usize),
|
||||
},
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
fn read_full_fragment_files_request(
|
||||
metadata: &[(ContentHash, AiFragmentMetadata)],
|
||||
) -> ReadFileContextRequest {
|
||||
let mut seen_paths = HashSet::new();
|
||||
ReadFileContextRequest {
|
||||
files: metadata
|
||||
.iter()
|
||||
.filter_map(|(_, fragment)| {
|
||||
let path = fragment.absolute_path.to_string_lossy().to_string();
|
||||
seen_paths
|
||||
.insert(path.clone())
|
||||
.then_some(ReadFileContextFile {
|
||||
path,
|
||||
line_ranges: vec![],
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
max_file_bytes: None,
|
||||
max_batch_bytes: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn file_contents_from_response(response: ReadFileContextResponse) -> HashMap<PathBuf, String> {
|
||||
let mut file_contents = HashMap::new();
|
||||
for file_context in response.file_contexts {
|
||||
if file_context.line_range_start.is_some() || file_context.line_range_end.is_some() {
|
||||
continue;
|
||||
}
|
||||
if let Some(file_context_proto::Content::TextContent(content)) = file_context.content {
|
||||
file_contents.insert(PathBuf::from(file_context.file_name), content);
|
||||
}
|
||||
}
|
||||
file_contents
|
||||
}
|
||||
|
||||
fn fragment_metadata_by_hash(
|
||||
metadata: &[(ContentHash, AiFragmentMetadata)],
|
||||
) -> HashMap<ContentHash, Vec<AiFragmentMetadata>> {
|
||||
let mut metadata_by_hash: HashMap<ContentHash, Vec<AiFragmentMetadata>> = HashMap::new();
|
||||
for (content_hash, metadata) in metadata {
|
||||
metadata_by_hash
|
||||
.entry(content_hash.clone())
|
||||
.or_default()
|
||||
.push(metadata.clone());
|
||||
}
|
||||
metadata_by_hash
|
||||
}
|
||||
|
||||
fn read_context_locations_request(
|
||||
locations: &HashSet<CodeContextLocation>,
|
||||
) -> ReadFileContextRequest {
|
||||
ReadFileContextRequest {
|
||||
files: locations
|
||||
.iter()
|
||||
.map(|location| match location {
|
||||
CodeContextLocation::WholeFile(path) => ReadFileContextFile {
|
||||
path: path.to_string_lossy().to_string(),
|
||||
line_ranges: vec![],
|
||||
},
|
||||
CodeContextLocation::Fragment(fragment) => ReadFileContextFile {
|
||||
path: fragment.path.to_string_lossy().to_string(),
|
||||
line_ranges: fragment
|
||||
.line_ranges
|
||||
.iter()
|
||||
.map(|range| LineRange {
|
||||
start: range.start as u32,
|
||||
end: range.end as u32,
|
||||
})
|
||||
.collect(),
|
||||
},
|
||||
})
|
||||
.collect(),
|
||||
max_file_bytes: None,
|
||||
max_batch_bytes: None,
|
||||
}
|
||||
}
|
||||
|
||||
// Keep this conversion at the AI boundary: `FileContext` lives in the `ai` crate, so the
|
||||
// `remote_server` protocol/client crate should not depend on it just to return typed agent
|
||||
// file contexts.
|
||||
fn proto_file_context_to_file_context(
|
||||
file_context: remote_server::proto::FileContextProto,
|
||||
) -> Option<FileContext> {
|
||||
let content = match file_context.content? {
|
||||
file_context_proto::Content::TextContent(text) => AnyFileContent::StringContent(text),
|
||||
file_context_proto::Content::BinaryContent(bytes) => AnyFileContent::BinaryContent(bytes),
|
||||
};
|
||||
let line_range = match (file_context.line_range_start, file_context.line_range_end) {
|
||||
(Some(start), Some(end)) => Some(start as usize..end as usize),
|
||||
(Some(_), None) | (None, Some(_)) | (None, None) => None,
|
||||
};
|
||||
let last_modified = file_context
|
||||
.last_modified_epoch_millis
|
||||
.map(|ms| std::time::UNIX_EPOCH + std::time::Duration::from_millis(ms));
|
||||
Some(FileContext {
|
||||
file_name: file_context.file_name,
|
||||
content,
|
||||
line_range,
|
||||
last_modified,
|
||||
line_count: file_context.line_count as usize,
|
||||
})
|
||||
}
|
||||
|
||||
fn remote_availability_failure(
|
||||
availability: RemoteCodebaseSearchAvailability,
|
||||
) -> SearchCodebaseResult {
|
||||
match availability {
|
||||
RemoteCodebaseSearchAvailability::NoConnectedHost => SearchCodebaseResult::Failed {
|
||||
reason: SearchCodebaseFailureReason::ClientError,
|
||||
message:
|
||||
"Remote codebase search is unavailable because the remote host is not connected."
|
||||
.to_string(),
|
||||
},
|
||||
RemoteCodebaseSearchAvailability::NoActiveRepo => SearchCodebaseResult::Failed {
|
||||
reason: SearchCodebaseFailureReason::CodebaseNotIndexed,
|
||||
message: "The current remote directory is not in a known codebase.".to_string(),
|
||||
},
|
||||
RemoteCodebaseSearchAvailability::NotIndexed { remote_path } => {
|
||||
SearchCodebaseResult::Failed {
|
||||
reason: SearchCodebaseFailureReason::CodebaseNotIndexed,
|
||||
message: format!(
|
||||
"The remote codebase at {} is not indexed yet.",
|
||||
remote_path.path.as_str()
|
||||
),
|
||||
}
|
||||
}
|
||||
RemoteCodebaseSearchAvailability::Indexing { remote_path } => {
|
||||
SearchCodebaseResult::Failed {
|
||||
reason: SearchCodebaseFailureReason::CodebaseNotIndexed,
|
||||
message: format!(
|
||||
"The remote codebase at {} is still being indexed. Try again later.",
|
||||
remote_path.path.as_str()
|
||||
),
|
||||
}
|
||||
}
|
||||
RemoteCodebaseSearchAvailability::Unavailable {
|
||||
remote_path,
|
||||
message,
|
||||
} => SearchCodebaseResult::Failed {
|
||||
reason: SearchCodebaseFailureReason::CodebaseNotIndexed,
|
||||
message: format!(
|
||||
"Remote codebase search is unavailable for {}: {message}",
|
||||
remote_path.path.as_str()
|
||||
),
|
||||
},
|
||||
RemoteCodebaseSearchAvailability::Ready(_) => SearchCodebaseResult::Failed {
|
||||
reason: SearchCodebaseFailureReason::ClientError,
|
||||
message: "Remote codebase search was unexpectedly unavailable.".to_string(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "native_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,42 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use remote_server::proto::{file_context_proto, FileContextProto, ReadFileContextResponse};
|
||||
|
||||
use super::file_contents_from_response;
|
||||
|
||||
#[test]
|
||||
fn file_contents_from_response_keeps_only_whole_text_files() {
|
||||
let response = ReadFileContextResponse {
|
||||
file_contexts: vec![
|
||||
FileContextProto {
|
||||
file_name: "/repo/src/lib.rs".to_string(),
|
||||
content: Some(file_context_proto::Content::TextContent(
|
||||
"content".to_string(),
|
||||
)),
|
||||
line_range_start: None,
|
||||
line_range_end: None,
|
||||
last_modified_epoch_millis: None,
|
||||
line_count: 1,
|
||||
},
|
||||
FileContextProto {
|
||||
file_name: "/repo/src/fragment.rs".to_string(),
|
||||
content: Some(file_context_proto::Content::TextContent(
|
||||
"fragment".to_string(),
|
||||
)),
|
||||
line_range_start: Some(1),
|
||||
line_range_end: Some(2),
|
||||
last_modified_epoch_millis: None,
|
||||
line_count: 1,
|
||||
},
|
||||
],
|
||||
failed_files: vec![],
|
||||
};
|
||||
|
||||
let file_contents = file_contents_from_response(response);
|
||||
|
||||
assert_eq!(file_contents.len(), 1);
|
||||
assert_eq!(
|
||||
file_contents.get(&PathBuf::from("/repo/src/lib.rs")),
|
||||
Some(&"content".to_string())
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use warpui::{AppContext, ModelContext};
|
||||
|
||||
use crate::ai::agent::{AIAgentActionId, SearchCodebaseFailureReason, SearchCodebaseResult};
|
||||
use crate::ai::blocklist::SessionContext;
|
||||
use crate::ai::get_relevant_files::controller::GetRelevantFilesController;
|
||||
|
||||
pub(super) enum RemoteSearchRequest {
|
||||
Ready(SearchCodebaseResult),
|
||||
}
|
||||
|
||||
pub(super) fn root_directory_for_search(
|
||||
_session_context: &SessionContext,
|
||||
_requested_codebase_path: Option<&str>,
|
||||
_app: &AppContext,
|
||||
) -> Option<PathBuf> {
|
||||
None
|
||||
}
|
||||
|
||||
pub(super) fn send_request(
|
||||
_query: String,
|
||||
_partial_paths: Option<Vec<String>>,
|
||||
_session_context: SessionContext,
|
||||
_requested_codebase_path: Option<String>,
|
||||
_action_id: AIAgentActionId,
|
||||
_ctx: &mut ModelContext<GetRelevantFilesController>,
|
||||
) -> RemoteSearchRequest {
|
||||
RemoteSearchRequest::Ready(SearchCodebaseResult::Failed {
|
||||
reason: SearchCodebaseFailureReason::CodebaseNotIndexed,
|
||||
message: "Remote codebase search is not available in this environment.".to_string(),
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user