Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified resources/flashgrep/flashgrep.exe
Binary file not shown.
49 changes: 5 additions & 44 deletions src/apps/desktop/src/api/app_state.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! Application state management

use crate::api::workspace_activation::spawn_workspace_background_warmup;
use bitfun_core::agentic::side_question::SideQuestionRuntime;
use bitfun_core::agentic::{agents, tools};
use bitfun_core::infrastructure::ai::{AIClient, AIClientFactory};
Expand Down Expand Up @@ -201,55 +202,11 @@ impl AppState {
.map(|workspace| workspace.root_path.clone());

if let Some(workspace_path) = initial_workspace_path.clone() {
let skip_startup_snapshot_restore = initial_workspace
.as_ref()
.map(|workspace| {
matches!(
workspace.workspace_kind,
bitfun_core::service::workspace::WorkspaceKind::Remote
)
})
.unwrap_or(false);
if skip_startup_snapshot_restore {
log::debug!(
"Skipping snapshot restore on startup for remote workspace: path={}",
workspace_path.display()
);
} else {
if let Err(e) =
bitfun_core::service::snapshot::initialize_snapshot_manager_for_workspace(
workspace_path.clone(),
None,
)
.await
{
log::warn!(
"Failed to restore snapshot system on startup: path={}, error={}",
workspace_path.display(),
e
);
}
}
if let Err(e) = ai_rules_service.set_workspace(workspace_path).await {
log::warn!("Failed to restore AI rules workspace on startup: {}", e);
}
}

if let Some(workspace_info) = initial_workspace {
if workspace_info.workspace_kind != workspace::WorkspaceKind::Remote {
if let Err(e) = workspace_search_service
.open_repo(&workspace_info.root_path)
.await
{
log::warn!(
"Failed to restore workspace search repository session on startup: path={}, error={}",
workspace_info.root_path.display(),
e
);
}
}
}

// Initialize SSH Remote services synchronously so they're ready before app starts
let ssh_data_dir = dirs::data_local_dir()
.unwrap_or_else(|| std::path::PathBuf::from("."))
Expand Down Expand Up @@ -358,6 +315,10 @@ impl AppState {
announcement_scheduler,
};

if let Some(workspace_info) = initial_workspace {
spawn_workspace_background_warmup(&app_state, workspace_info);
}

log::info!("AppState initialized successfully");
Ok(app_state)
}
Expand Down
53 changes: 12 additions & 41 deletions src/apps/desktop/src/api/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use crate::api::search_api::{
group_search_results, search_file_contents_via_workspace_search,
search_metadata_from_content_result, should_use_workspace_search, SearchMetadataResponse,
};
use crate::api::workspace_activation::spawn_workspace_background_warmup;
use bitfun_core::infrastructure::{
BatchedFileSearchProgressSink, FileSearchResult, FileSearchResultGroup, FileTreeNode,
SearchMatchType,
Expand Down Expand Up @@ -608,8 +609,18 @@ async fn clear_active_workspace_context(state: &State<'_, AppState>, app: &AppHa
#[cfg(not(target_os = "macos"))]
let _ = app;

let previous_workspace_path = state.workspace_path.read().await.clone();
*state.workspace_path.write().await = None;

if let Some(previous_workspace_path) = previous_workspace_path {
let root_str = previous_workspace_path.to_string_lossy().to_string();
if !is_remote_path(root_str.trim()).await {
state
.workspace_search_service
.schedule_repo_release(previous_workspace_path);
}
}

if let Some(ref pool) = state.js_worker_pool {
pool.stop_all().await;
}
Expand Down Expand Up @@ -649,34 +660,6 @@ async fn apply_active_workspace_context(
// Remote workspace roots are POSIX paths on the SSH host — not writable local directories on
// Windows. Snapshot hooks already skip file tracking for registered remote paths; avoid
// creating `/.bitfun` (or drive root) here which fails with access denied.
let root_str = workspace_info.root_path.to_string_lossy().to_string();
let skip_local_snapshot = workspace_info.workspace_kind == WorkspaceKind::Remote
|| is_remote_path(root_str.trim()).await;
if !skip_local_snapshot {
if let Err(e) = bitfun_core::service::snapshot::initialize_snapshot_manager_for_workspace(
workspace_info.root_path.clone(),
None,
)
.await
{
warn!(
"Failed to initialize snapshot system: path={}, error={}",
workspace_info.root_path.display(),
e
);
}
} else {
debug!(
"Skipping local snapshot manager init for remote/non-local workspace root_path={}",
workspace_info.root_path.display()
);
}

state
.agent_registry
.load_custom_subagents(&workspace_info.root_path)
.await;

if let Err(e) = state
.ai_rules_service
.set_workspace(workspace_info.root_path.clone())
Expand All @@ -689,19 +672,7 @@ async fn apply_active_workspace_context(
);
}

if workspace_info.workspace_kind != WorkspaceKind::Remote {
if let Err(e) = state
.workspace_search_service
.open_repo(&workspace_info.root_path)
.await
{
warn!(
"Failed to open workspace search repository session: path={}, error={}",
workspace_info.root_path.display(),
e
);
}
}
spawn_workspace_background_warmup(&*state, workspace_info.clone());

#[cfg(target_os = "macos")]
{
Expand Down
1 change: 1 addition & 0 deletions src/apps/desktop/src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,6 @@ pub mod subagent_api;
pub mod system_api;
pub mod terminal_api;
pub mod tool_api;
pub mod workspace_activation;

pub use app_state::{AppState, AppStatistics, HealthStatus, RemoteWorkspace};
118 changes: 118 additions & 0 deletions src/apps/desktop/src/api/workspace_activation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
use crate::api::app_state::AppState;
use bitfun_core::service::remote_ssh::workspace_state::is_remote_path;
use bitfun_core::service::workspace::{WorkspaceInfo, WorkspaceKind};
use log::{debug, info, warn};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Instant;
use tokio::sync::RwLock;

pub fn spawn_workspace_background_warmup(state: &AppState, workspace_info: WorkspaceInfo) {
let workspace_path = state.workspace_path.clone();
let agent_registry = state.agent_registry.clone();
let workspace_search_service = state.workspace_search_service.clone();

tokio::spawn(async move {
warm_workspace_background_services(
workspace_path,
agent_registry,
workspace_search_service,
workspace_info,
)
.await;
});
}

async fn warm_workspace_background_services(
workspace_path: Arc<RwLock<Option<PathBuf>>>,
agent_registry: Arc<bitfun_core::agentic::agents::AgentRegistry>,
workspace_search_service: Arc<bitfun_core::service::search::WorkspaceSearchService>,
workspace_info: WorkspaceInfo,
) {
let started_at = Instant::now();
let target_path = workspace_info.root_path.clone();
let root_str = target_path.to_string_lossy().to_string();
let skip_local_snapshot = workspace_info.workspace_kind == WorkspaceKind::Remote
|| is_remote_path(root_str.trim()).await;

if !skip_local_snapshot && is_workspace_active(&workspace_path, &target_path).await {
let snapshot_started_at = Instant::now();
if let Err(error) =
bitfun_core::service::snapshot::initialize_snapshot_manager_for_workspace(
target_path.clone(),
None,
)
.await
{
warn!(
"Failed to initialize snapshot system during workspace warmup: path={}, error={}",
target_path.display(),
error
);
} else {
debug!(
"Workspace snapshot warmup completed: path={}, elapsed_ms={}",
target_path.display(),
snapshot_started_at.elapsed().as_millis()
);
}
}

if is_workspace_active(&workspace_path, &target_path).await {
let subagents_started_at = Instant::now();
agent_registry.load_custom_subagents(&target_path).await;
debug!(
"Workspace custom subagent warmup completed: path={}, elapsed_ms={}",
target_path.display(),
subagents_started_at.elapsed().as_millis()
);
}

if workspace_info.workspace_kind != WorkspaceKind::Remote
&& is_workspace_active(&workspace_path, &target_path).await
{
let search_started_at = Instant::now();
match workspace_search_service.open_repo(&target_path).await {
Ok(_) => {
let still_active = is_workspace_active(&workspace_path, &target_path).await;
if !still_active {
workspace_search_service.schedule_repo_release(target_path.clone());
debug!(
"Released flashgrep warmup session for inactive workspace: path={}",
target_path.display()
);
}
info!(
"Workspace search warmup completed: path={}, elapsed_ms={}, active_after_open={}",
target_path.display(),
search_started_at.elapsed().as_millis(),
still_active
);
}
Err(error) => {
warn!(
"Failed to open workspace search repository session during warmup: path={}, error={}",
target_path.display(),
error
);
}
}
}

debug!(
"Workspace background warmup completed: path={}, total_elapsed_ms={}",
target_path.display(),
started_at.elapsed().as_millis()
);
}

async fn is_workspace_active(
workspace_path: &Arc<RwLock<Option<PathBuf>>>,
target_path: &Path,
) -> bool {
workspace_path
.read()
.await
.as_ref()
.is_some_and(|current| current == target_path)
}
17 changes: 5 additions & 12 deletions src/apps/desktop/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1017,18 +1017,11 @@ fn perform_process_exit_cleanup() -> bool {
}
});

match shutdown_thread {
Ok(handle) => {
if handle.join().is_err() {
log::warn!("Workspace search shutdown thread panicked");
}
}
Err(error) => {
log::warn!(
"Failed to spawn workspace search shutdown thread: {}",
error
);
}
if let Err(error) = shutdown_thread {
log::warn!(
"Failed to spawn workspace search shutdown thread: {}",
error
);
}
}
bitfun_core::util::process_manager::cleanup_all_processes();
Expand Down
Loading
Loading