Add local project indexing and search guidance

This commit is contained in:
2026-08-30 22:00:27 -05:00
parent 88c1ef9716
commit 1c7d3c175d
39 changed files with 2094 additions and 306 deletions
@@ -5,13 +5,9 @@ use std::{
};
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
use ai::index::full_source_code_embedding::manager::CodebaseIndexManager;
#[cfg(all(
feature = "local_fs",
not(target_family = "wasm"),
not(any(test, feature = "integration_tests"))
))]
use ai::index::full_source_code_embedding::manager::CodebaseIndexManagerEvent;
use ai::index::local_project_index::LocalProjectIndexEvent;
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
use ai::index::local_project_index::{LocalIndexStatus, LocalProjectIndexManager};
use galaxy_core::features::FeatureFlag;
use galaxy_core::paths::home_relative_path;
use galaxy_core::ui::theme::color::internal_colors;
@@ -129,23 +125,17 @@ impl AgentAssistedEnvironmentModal {
create_button,
};
#[cfg(all(
feature = "local_fs",
not(target_family = "wasm"),
not(any(test, feature = "integration_tests"))
))]
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
{
let index_manager = CodebaseIndexManager::handle(ctx);
let index_manager = LocalProjectIndexManager::handle(ctx);
ctx.subscribe_to_model(&index_manager, |me, _, event, ctx| {
if !me.visible {
return;
}
match event {
CodebaseIndexManagerEvent::SyncStateUpdated { .. }
| CodebaseIndexManagerEvent::NewIndexCreated { .. }
| CodebaseIndexManagerEvent::RemoveExpiredIndexMetadata { .. }
| CodebaseIndexManagerEvent::IndexMetadataUpdated { .. } => {
LocalProjectIndexEvent::StatusChanged { .. }
| LocalProjectIndexEvent::IndexRemoved { .. } => {
me.refresh_available_repos(ctx);
if me.available_repos.is_empty() {
me.maybe_start_available_repos_loading(ctx);
@@ -734,20 +724,19 @@ impl View for AgentAssistedEnvironmentModal {
fn available_indexed_repos(app: &AppContext) -> Vec<RepoEntry> {
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
{
let mut repos: Vec<RepoEntry> = CodebaseIndexManager::as_ref(app)
.get_codebase_index_statuses(app)
.filter_map(|(root, status)| {
status.has_synced_version().then(|| {
let name = root
.file_name()
.and_then(|s| s.to_str())
.map(ToOwned::to_owned)
.unwrap_or_else(|| root.to_string_lossy().into_owned());
RepoEntry {
name,
path: root.clone(),
}
})
let mut repos: Vec<RepoEntry> = LocalProjectIndexManager::as_ref(app)
.statuses()
.filter(|(_, status)| matches!(status, LocalIndexStatus::Ready { .. }))
.map(|(root, _)| {
let name = root
.file_name()
.and_then(|s| s.to_str())
.map(ToOwned::to_owned)
.unwrap_or_else(|| root.to_string_lossy().into_owned());
RepoEntry {
name,
path: root.clone(),
}
})
.collect();
@@ -1,6 +1,9 @@
use std::path::PathBuf;
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
use ai::index::full_source_code_embedding::manager::CodebaseIndexManager;
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
use ai::index::local_project_index::LocalProjectIndexManager;
use galaxy_core::ui::appearance::Appearance;
use galaxyui::elements::{ChildView, Empty};
use galaxyui::platform::WindowStyle;
@@ -17,8 +20,11 @@ fn init_modal_test_models(app: &mut App) {
app.add_singleton_model(|_| Appearance::mock());
app.add_singleton_model(|_| ToastStack);
// The modal queries CodebaseIndexManager for locally indexed repos.
// Register a test instance so `available_indexed_repos(...)` doesn't panic.
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
app.add_singleton_model(|ctx| {
LocalProjectIndexManager::new_at(tempfile::tempdir().unwrap().keep(), ctx)
});
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
app.add_singleton_model(|ctx| {
CodebaseIndexManager::new_for_test(ServerApiProvider::as_ref(ctx).get(), ctx)
});
+139 -30
View File
@@ -2,11 +2,17 @@ use std::borrow::Cow;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
use ai::index::full_source_code_embedding::manager::{
CodebaseIndexFinishedStatus, CodebaseIndexManager, CodebaseIndexManagerEvent,
CodebaseIndexStatus, CodebaseIndexingError,
};
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
use ai::index::full_source_code_embedding::SyncProgress;
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
use ai::index::local_project_index::{
LocalIndexStatus, LocalProjectIndexEvent, LocalProjectIndexManager,
};
use ai::project_context::model::{ProjectContextModel, ProjectContextModelEvent};
use ai::workspace::WorkspaceMetadata;
use galaxy_core::features::FeatureFlag;
@@ -170,6 +176,44 @@ enum IndexingRefreshAction {
RequestRemote,
Resync,
}
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
fn local_status_presentation(
status: Option<&LocalIndexStatus>,
appearance: &Appearance,
) -> IndexingStatusPresentation {
let theme = appearance.theme();
match status {
None => IndexingStatusPresentation {
text: Cow::from("No index created"),
color: theme.disabled_ui_text_color().into_solid(),
icon: Some(Icon::SlashCircle),
refresh_action: None,
show_delete: false,
},
Some(LocalIndexStatus::Indexing) => IndexingStatusPresentation {
text: Cow::from("Indexing..."),
color: theme.disabled_ui_text_color().into_solid(),
icon: None,
refresh_action: None,
show_delete: true,
},
Some(LocalIndexStatus::Ready { file_count }) => IndexingStatusPresentation {
text: Cow::from(format!("Ready ({file_count} files)")),
color: theme.ansi_fg_green(),
icon: Some(Icon::Check),
refresh_action: Some(IndexingRefreshAction::Resync),
show_delete: true,
},
Some(LocalIndexStatus::Failed { .. }) => IndexingStatusPresentation {
text: Cow::from("Failed"),
color: theme.ui_error_color(),
icon: Some(Icon::AlertTriangle),
refresh_action: Some(IndexingRefreshAction::Resync),
show_delete: true,
},
}
}
pub struct CodeSettingsPageView {
page: PageType<Self>,
active_subpage: Option<CodeSubpage>,
@@ -196,34 +240,52 @@ pub struct CodeSettingsPageView {
impl CodeSettingsPageView {
pub fn new(ctx: &mut ViewContext<CodeSettingsPageView>) -> Self {
let index_manager = CodebaseIndexManager::handle(ctx);
let codebase_count = index_manager
.as_ref(ctx)
.get_codebase_index_statuses(ctx)
.count();
ctx.subscribe_to_model(&index_manager, |me, index, event, ctx| {
if matches!(
event,
CodebaseIndexManagerEvent::SyncStateUpdated { .. }
| CodebaseIndexManagerEvent::NewIndexCreated { .. }
) {
let codebase_count = index.as_ref(ctx).get_codebase_index_statuses(ctx).count();
// Only update mouse states if the number of codebases changed
if me.codebase_manual_resync_mouse_states.len() != codebase_count {
// Resize the vector to match the new codebase count, but preserve the existing mouse states
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
let codebase_count = {
let index_manager = LocalProjectIndexManager::handle(ctx);
let count = index_manager.as_ref(ctx).statuses().count();
ctx.subscribe_to_model(&index_manager, |me, index, event, ctx| {
if matches!(
event,
LocalProjectIndexEvent::StatusChanged { .. }
| LocalProjectIndexEvent::IndexRemoved { .. }
) {
let count = index.as_ref(ctx).statuses().count();
me.codebase_manual_resync_mouse_states
.resize_with(codebase_count, Default::default);
.resize_with(count, Default::default);
me.codebase_delete_mouse_states
.resize_with(codebase_count, Default::default);
.resize_with(count, Default::default);
me.resize_workspace_mouse_states(ctx);
ctx.notify();
}
});
count
};
me.resize_workspace_mouse_states(ctx);
ctx.notify();
}
});
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
let codebase_count = {
let index_manager = CodebaseIndexManager::handle(ctx);
let count = index_manager
.as_ref(ctx)
.get_codebase_index_statuses(ctx)
.count();
ctx.subscribe_to_model(&index_manager, |me, index, event, ctx| {
if matches!(
event,
CodebaseIndexManagerEvent::SyncStateUpdated { .. }
| CodebaseIndexManagerEvent::NewIndexCreated { .. }
) {
let count = index.as_ref(ctx).get_codebase_index_statuses(ctx).count();
me.codebase_manual_resync_mouse_states
.resize_with(count, Default::default);
me.codebase_delete_mouse_states
.resize_with(count, Default::default);
me.resize_workspace_mouse_states(ctx);
ctx.notify();
}
});
count
};
#[cfg(not(target_family = "wasm"))]
let remote_codebase_count = {
@@ -552,6 +614,18 @@ impl CodeSettingsPageView {
if let Some(directory_path) = paths.first() {
let path = PathBuf::from(directory_path);
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
PersistedWorkspace::handle(ctx).update(ctx, |workspace, ctx| {
workspace.user_added_workspace(path.clone(), ctx);
if let Err(error) = LocalProjectIndexManager::handle(ctx)
.update(ctx, |manager, ctx| {
manager.index_directory(path.clone(), ctx)
})
{
log::warn!("Failed to start local project indexing: {error:#}");
}
});
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
CodebaseIndexManager::handle(ctx).update(ctx, |manager, ctx| {
manager.index_directory(path, ctx);
});
@@ -697,11 +771,25 @@ impl TypedActionView for CodeSettingsPageView {
ctx.notify();
}
CodeSettingsPageAction::ManualResync(repo_path) => {
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
if let Err(error) = LocalProjectIndexManager::handle(ctx)
.update(ctx, |manager, ctx| {
manager.index_directory(repo_path.clone(), ctx)
})
{
log::warn!("Failed to refresh local project index: {error:#}");
}
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
CodebaseIndexManager::handle(ctx).update(ctx, |manager, ctx| {
manager.try_manual_resync_codebase(repo_path, ctx);
});
}
CodeSettingsPageAction::DeleteIndex(repo_path) => {
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
LocalProjectIndexManager::handle(ctx).update(ctx, |manager, ctx| {
manager.remove_index_for_path(repo_path.clone(), ctx);
});
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
CodebaseIndexManager::handle(ctx).update(ctx, |manager, ctx| {
manager.drop_index(repo_path.clone(), ctx);
});
@@ -1112,6 +1200,7 @@ impl CodePageWidget {
),
];
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
if codebase_indexing_enabled && !CodebaseIndexManager::as_ref(app).can_create_new_indices()
{
rows.push(self.render_settings_subtext(
@@ -1372,7 +1461,10 @@ impl CodePageWidget {
Vec::new()
};
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
let codebase_manager = CodebaseIndexManager::as_ref(app);
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
let local_index_manager = LocalProjectIndexManager::as_ref(app);
let lsp_manager = LspManagerModel::as_ref(app);
let persisted_workspace = PersistedWorkspace::as_ref(app);
@@ -1383,6 +1475,11 @@ impl CodePageWidget {
let workspace_path = &workspace.path;
// Get codebase index status if it exists
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
let local_status = local_index_manager
.status_for_path(workspace_path)
.map(|(_, status)| status);
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
let index_status =
codebase_manager.get_codebase_index_status_for_path(workspace_path, app);
@@ -1403,7 +1500,17 @@ impl CodePageWidget {
.unwrap_or_default();
// Skip workspaces that have neither an index nor any LSP servers
if index_status.is_none() && all_servers.is_empty() {
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
let (has_index, index_presentation) = (
local_status.is_some(),
local_status_presentation(local_status, appearance),
);
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
let (has_index, index_presentation) = (
index_status.is_some(),
self.local_indexing_status_presentation(index_status.as_ref(), appearance),
);
if !has_index && all_servers.is_empty() {
continue;
}
@@ -1431,7 +1538,7 @@ impl CodePageWidget {
content.add_child(self.render_workspace_row(
workspace_path,
index_status.as_ref(),
index_presentation,
&all_servers,
lsp_manager,
resync_mouse,
@@ -1485,7 +1592,7 @@ impl CodePageWidget {
fn render_workspace_row(
&self,
workspace_path: &Path,
index_status: Option<&CodebaseIndexStatus>,
index_presentation: IndexingStatusPresentation,
all_servers: &[(LSPServerType, EnablementState)],
lsp_manager: &LspManagerModel,
resync_mouse: MouseStateHandle,
@@ -1568,7 +1675,7 @@ impl CodePageWidget {
// Indexing section (always rendered per design)
workspace_content.add_child(self.render_indexing_subsection(
workspace_path,
index_status,
index_presentation,
resync_mouse,
delete_mouse,
appearance,
@@ -1671,13 +1778,13 @@ impl CodePageWidget {
fn render_indexing_subsection(
&self,
workspace_path: &Path,
index_status: Option<&CodebaseIndexStatus>,
presentation: IndexingStatusPresentation,
resync_mouse: MouseStateHandle,
delete_mouse: MouseStateHandle,
appearance: &Appearance,
) -> Box<dyn Element> {
self.render_indexing_subsection_for_target(
self.local_indexing_status_presentation(index_status, appearance),
presentation,
Some(LocalOrRemotePath::Local(workspace_path.to_path_buf())),
resync_mouse,
delete_mouse,
@@ -1730,6 +1837,7 @@ impl CodePageWidget {
column.finish()
}
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
fn local_indexing_status_presentation(
&self,
index_state: Option<&CodebaseIndexStatus>,
@@ -2569,6 +2677,7 @@ impl SettingsWidget for CodebaseIndexingCategorizedWidget {
Some(AUTO_INDEX_DESCRIPTION.into()),
));
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
if !CodebaseIndexManager::as_ref(app).can_create_new_indices() {
content.add_child(
ui_builder
@@ -1,9 +1,12 @@
use std::collections::HashSet;
use std::path::{Path, PathBuf};
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
use ai::index::full_source_code_embedding::manager::{
CodebaseIndexManager, CodebaseIndexManagerEvent,
};
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
use ai::index::local_project_index::{LocalProjectIndexEvent, LocalProjectIndexManager};
use galaxy_util::path::user_friendly_path;
use galaxyui::elements::{
Border, ChildView, ConstrainedBox, Container, CrossAxisAlignment, Flex, Hoverable,
@@ -76,6 +79,19 @@ pub(super) enum DirectoryColorAddPickerEvent {
impl DirectoryColorAddPicker {
pub(super) fn new(ctx: &mut ViewContext<Self>) -> Self {
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
let local_index_manager = LocalProjectIndexManager::handle(ctx);
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
ctx.subscribe_to_model(&local_index_manager, |me, _, event, ctx| {
if matches!(
event,
LocalProjectIndexEvent::StatusChanged { .. }
| LocalProjectIndexEvent::IndexRemoved { .. }
) {
me.refresh_items(ctx);
}
});
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
ctx.subscribe_to_model(&CodebaseIndexManager::handle(ctx), |me, _, event, ctx| {
// Refresh for any event that may change the set of indexed codebase paths or
// persisted workspaces: new index created, sync state updated (which covers
@@ -190,6 +206,12 @@ impl DirectoryColorAddPicker {
}
fn refresh_items(&mut self, ctx: &mut ViewContext<Self>) {
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
let indexed_paths: HashSet<PathBuf> = LocalProjectIndexManager::as_ref(ctx)
.statuses()
.map(|(path, _)| path.clone())
.collect();
#[cfg(any(not(feature = "local_fs"), target_family = "wasm"))]
let indexed_paths: HashSet<PathBuf> = CodebaseIndexManager::as_ref(ctx)
.get_codebase_paths()
.cloned()