diff --git a/resources/flashgrep/README.md b/resources/flashgrep/README.md index 92270b960..0f0e876c7 100644 --- a/resources/flashgrep/README.md +++ b/resources/flashgrep/README.md @@ -2,7 +2,11 @@ Place the prebuilt `flashgrep` daemon binary in this directory. Expected filenames: -- macOS/Linux: `flashgrep` -- Windows: `flashgrep.exe` +- macOS x86_64: `flashgrep-x86_64-apple-darwin` +- macOS arm64: `flashgrep-aarch64-apple-darwin` +- Linux x86_64: `flashgrep-x86_64-unknown-linux-gnu` +- Linux arm64: `flashgrep-aarch64-unknown-linux-gnu` +- Windows x86_64: `flashgrep-x86_64-pc-windows-msvc.exe` +- Windows arm64: `flashgrep-aarch64-pc-windows-msvc.exe` BitFun dev/build scripts load the daemon from this repository-relative path. diff --git a/resources/flashgrep/flashgrep b/resources/flashgrep/flashgrep-aarch64-apple-darwin similarity index 50% rename from resources/flashgrep/flashgrep rename to resources/flashgrep/flashgrep-aarch64-apple-darwin index b3bef668e..fc6b2e52d 100755 Binary files a/resources/flashgrep/flashgrep and b/resources/flashgrep/flashgrep-aarch64-apple-darwin differ diff --git a/resources/flashgrep/flashgrep-aarch64-pc-windows-msvc.exe b/resources/flashgrep/flashgrep-aarch64-pc-windows-msvc.exe new file mode 100644 index 000000000..3c4771c66 Binary files /dev/null and b/resources/flashgrep/flashgrep-aarch64-pc-windows-msvc.exe differ diff --git a/resources/flashgrep/flashgrep-aarch64-unknown-linux-gnu b/resources/flashgrep/flashgrep-aarch64-unknown-linux-gnu new file mode 100755 index 000000000..0a6c14ee5 Binary files /dev/null and b/resources/flashgrep/flashgrep-aarch64-unknown-linux-gnu differ diff --git a/resources/flashgrep/flashgrep-x86_64-apple-darwin b/resources/flashgrep/flashgrep-x86_64-apple-darwin new file mode 100755 index 000000000..28baf781c Binary files /dev/null and b/resources/flashgrep/flashgrep-x86_64-apple-darwin differ diff --git a/resources/flashgrep/flashgrep-x86_64-pc-windows-msvc.exe b/resources/flashgrep/flashgrep-x86_64-pc-windows-msvc.exe new file mode 100644 index 000000000..0143f13fa Binary files /dev/null and b/resources/flashgrep/flashgrep-x86_64-pc-windows-msvc.exe differ diff --git a/resources/flashgrep/flashgrep-x86_64-unknown-linux-gnu b/resources/flashgrep/flashgrep-x86_64-unknown-linux-gnu new file mode 100755 index 000000000..ba206a6fb Binary files /dev/null and b/resources/flashgrep/flashgrep-x86_64-unknown-linux-gnu differ diff --git a/resources/flashgrep/flashgrep.exe b/resources/flashgrep/flashgrep.exe deleted file mode 100644 index 9b1078a40..000000000 Binary files a/resources/flashgrep/flashgrep.exe and /dev/null differ diff --git a/scripts/dev.cjs b/scripts/dev.cjs index 3e0325f40..6237dfd3b 100644 --- a/scripts/dev.cjs +++ b/scripts/dev.cjs @@ -544,26 +544,49 @@ async function startDesktopPreview() { await new Promise(() => {}); } -function flashgrepBinaryName() { - return process.platform === 'win32' ? 'flashgrep.exe' : 'flashgrep'; +function flashgrepBinaryNames() { + if (process.platform === 'win32' && process.arch === 'x64') { + return ['flashgrep-x86_64-pc-windows-msvc.exe']; + } + if (process.platform === 'win32' && process.arch === 'arm64') { + return ['flashgrep-aarch64-pc-windows-msvc.exe']; + } + if (process.platform === 'darwin' && process.arch === 'x64') { + return ['flashgrep-x86_64-apple-darwin']; + } + if (process.platform === 'darwin' && process.arch === 'arm64') { + return ['flashgrep-aarch64-apple-darwin']; + } + if (process.platform === 'linux' && process.arch === 'x64') { + return ['flashgrep-x86_64-unknown-linux-gnu']; + } + if (process.platform === 'linux' && process.arch === 'arm64') { + return ['flashgrep-aarch64-unknown-linux-gnu']; + } + return [process.platform === 'win32' ? 'flashgrep.exe' : 'flashgrep']; } -function flashgrepBinaryPath() { - return path.join(ROOT_DIR, 'resources', 'flashgrep', flashgrepBinaryName()); +function flashgrepBinaryName() { + return flashgrepBinaryNames()[0]; } function ensureFlashgrepBinary() { - const binaryPath = flashgrepBinaryPath(); - if (!fs.existsSync(binaryPath)) { - return { - ok: false, - error: new Error( - `flashgrep binary not found: ${binaryPath}. Put the prebuilt daemon binary at resources/flashgrep/${flashgrepBinaryName()}` - ), - }; + for (const binaryName of flashgrepBinaryNames()) { + const binaryPath = path.join(ROOT_DIR, 'resources', 'flashgrep', binaryName); + if (!fs.existsSync(binaryPath)) { + continue; + } + return { ok: true, binaryPath }; } - return { ok: true, binaryPath }; + return { + ok: false, + error: new Error( + `flashgrep binary not found for ${process.platform}/${process.arch}. Expected one of: ${flashgrepBinaryNames() + .map((name) => `resources/flashgrep/${name}`) + .join(', ')}` + ), + }; } async function ensureFlashgrepBundleResource() { @@ -685,7 +708,7 @@ async function main() { if (mode === 'desktop') { await ensureDesktopOpenSslIfNeeded(); const desktopDir = path.join(ROOT_DIR, 'src/apps/desktop'); - const tauriConfig = path.join(desktopDir, 'tauri.conf.json'); + const tauriConfig = path.join(desktopDir, 'tauri.dev.conf.json'); if (process.platform === 'win32') { // Running the generated .cmd shim directly via spawn is flaky on Windows. // Use cmd.exe with an explicit args array so the desktop app directory diff --git a/scripts/prepare-flashgrep-resource.mjs b/scripts/prepare-flashgrep-resource.mjs index acb29a803..0d8876585 100644 --- a/scripts/prepare-flashgrep-resource.mjs +++ b/scripts/prepare-flashgrep-resource.mjs @@ -6,8 +6,30 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); const ROOT = join(__dirname, '..'); const RESOURCE_DIR = join(ROOT, 'resources', 'flashgrep'); +export function flashgrepBinaryNames() { + if (process.platform === 'win32' && process.arch === 'x64') { + return ['flashgrep-x86_64-pc-windows-msvc.exe']; + } + if (process.platform === 'win32' && process.arch === 'arm64') { + return ['flashgrep-aarch64-pc-windows-msvc.exe']; + } + if (process.platform === 'darwin' && process.arch === 'x64') { + return ['flashgrep-x86_64-apple-darwin']; + } + if (process.platform === 'darwin' && process.arch === 'arm64') { + return ['flashgrep-aarch64-apple-darwin']; + } + if (process.platform === 'linux' && process.arch === 'x64') { + return ['flashgrep-x86_64-unknown-linux-gnu']; + } + if (process.platform === 'linux' && process.arch === 'arm64') { + return ['flashgrep-aarch64-unknown-linux-gnu']; + } + return [process.platform === 'win32' ? 'flashgrep.exe' : 'flashgrep']; +} + export function flashgrepBinaryName() { - return process.platform === 'win32' ? 'flashgrep.exe' : 'flashgrep'; + return flashgrepBinaryNames()[0]; } export function flashgrepBinaryPath() { @@ -15,15 +37,21 @@ export function flashgrepBinaryPath() { } export function ensureFlashgrepBinary() { - const binaryPath = flashgrepBinaryPath(); - if (!existsSync(binaryPath)) { - throw new Error( - `flashgrep binary not found: ${binaryPath}. Put the prebuilt daemon binary at resources/flashgrep/${flashgrepBinaryName()}` - ); - } + for (const binaryName of flashgrepBinaryNames()) { + const binaryPath = join(RESOURCE_DIR, binaryName); + if (!existsSync(binaryPath)) { + continue; + } - if (process.platform !== 'win32') { - chmodSync(binaryPath, statSync(binaryPath).mode | 0o111); + if (process.platform !== 'win32') { + chmodSync(binaryPath, statSync(binaryPath).mode | 0o111); + } + return binaryPath; } - return binaryPath; + + throw new Error( + `flashgrep binary not found for ${process.platform}/${process.arch}. Expected one of: ${flashgrepBinaryNames() + .map((name) => `resources/flashgrep/${name}`) + .join(', ')}` + ); } diff --git a/src/apps/desktop/src/api/search_api.rs b/src/apps/desktop/src/api/search_api.rs index 4a8363a5d..3659ec5c1 100644 --- a/src/apps/desktop/src/api/search_api.rs +++ b/src/apps/desktop/src/api/search_api.rs @@ -2,7 +2,8 @@ use crate::api::app_state::AppState; use bitfun_core::infrastructure::{FileSearchResult, FileSearchResultGroup, SearchMatchType}; use bitfun_core::service::remote_ssh::workspace_state::is_remote_path; use bitfun_core::service::search::{ - ContentSearchResult, WorkspaceSearchBackend, WorkspaceSearchRepoPhase, + workspace_search_daemon_available, workspace_search_feature_enabled, ContentSearchResult, + WorkspaceSearchBackend, WorkspaceSearchRepoPhase, }; use serde::{Deserialize, Serialize}; use tauri::State; @@ -24,8 +25,31 @@ pub struct SearchMetadataResponse { pub matched_occurrences: usize, } +async fn workspace_search_unavailable_message(root_path: &str) -> Option { + if is_remote_path(root_path.trim()).await { + return Some( + "Remote workspace search status is not managed by BitFun workspace search".to_string(), + ); + } + + if !workspace_search_feature_enabled().await { + return Some( + "Workspace search is disabled. Enable it in Settings > Session Config to use accelerated workspace search.".to_string(), + ); + } + + if !workspace_search_daemon_available() { + return Some( + "Workspace search daemon is unavailable. BitFun will continue using legacy search." + .to_string(), + ); + } + + None +} + pub(crate) async fn should_use_workspace_search(root_path: &str) -> bool { - !is_remote_path(root_path.trim()).await + workspace_search_unavailable_message(root_path).await.is_none() } pub(crate) async fn search_file_contents_via_workspace_search( @@ -113,10 +137,8 @@ pub async fn search_get_repo_status( state: State<'_, AppState>, request: SearchRepoIndexRequest, ) -> Result { - if !should_use_workspace_search(&request.root_path).await { - return Err( - "Remote workspace search status is not managed by BitFun workspace search".to_string(), - ); + if let Some(message) = workspace_search_unavailable_message(&request.root_path).await { + return Err(message); } state @@ -132,11 +154,8 @@ pub async fn search_build_index( state: State<'_, AppState>, request: SearchRepoIndexRequest, ) -> Result { - if !should_use_workspace_search(&request.root_path).await { - return Err( - "Remote workspace search indexing is not managed by BitFun workspace search" - .to_string(), - ); + if let Some(message) = workspace_search_unavailable_message(&request.root_path).await { + return Err(message); } state @@ -152,11 +171,8 @@ pub async fn search_rebuild_index( state: State<'_, AppState>, request: SearchRepoIndexRequest, ) -> Result { - if !should_use_workspace_search(&request.root_path).await { - return Err( - "Remote workspace search indexing is not managed by BitFun workspace search" - .to_string(), - ); + if let Some(message) = workspace_search_unavailable_message(&request.root_path).await { + return Err(message); } state diff --git a/src/apps/desktop/src/api/workspace_activation.rs b/src/apps/desktop/src/api/workspace_activation.rs index 03682c2dc..43d24cae8 100644 --- a/src/apps/desktop/src/api/workspace_activation.rs +++ b/src/apps/desktop/src/api/workspace_activation.rs @@ -1,4 +1,5 @@ use crate::api::app_state::AppState; +use bitfun_core::service::search::workspace_search_runtime_available; use bitfun_core::service::remote_ssh::workspace_state::is_remote_path; use bitfun_core::service::workspace::{WorkspaceInfo, WorkspaceKind}; use log::{debug, info, warn}; @@ -70,6 +71,7 @@ async fn warm_workspace_background_services( if workspace_info.workspace_kind != WorkspaceKind::Remote && is_workspace_active(&workspace_path, &target_path).await + && workspace_search_runtime_available().await { let search_started_at = Instant::now(); match workspace_search_service.open_repo(&target_path).await { diff --git a/src/apps/desktop/src/lib.rs b/src/apps/desktop/src/lib.rs index beb6da287..5077f6526 100644 --- a/src/apps/desktop/src/lib.rs +++ b/src/apps/desktop/src/lib.rs @@ -141,6 +141,9 @@ pub async fn run() { } startup_timings.record_elapsed("init_function_agents", step_started); + let workspace_search_enabled = bitfun_core::service::search::workspace_search_feature_enabled().await; + let startup_flashgrep_path = configure_workspace_search_daemon_env(); + let step_started = Instant::now(); let app_state = match AppState::new_async(token_usage_service).await { Ok(state) => state, @@ -206,6 +209,52 @@ pub async fn run() { ); } + if workspace_search_enabled { + let flashgrep_path = startup_flashgrep_path.clone().or_else(|| { + let binary_names = + bitfun_core::service::search::workspace_search_daemon_binary_names(); + for binary_name in binary_names { + let primary = format!("flashgrep/{}", binary_name); + if let Ok(path) = app + .path() + .resolve(&primary, tauri::path::BaseDirectory::Resource) + { + if path.exists() { + return Some(path); + } + } + } + + if let Ok(resource_dir) = app.path().resource_dir() { + for binary_name in binary_names { + for candidate in [ + resource_dir.join("flashgrep").join(binary_name), + resource_dir.join("resources").join("flashgrep").join(binary_name), + resource_dir.join(binary_name), + ] { + if candidate.exists() { + return Some(candidate); + } + } + } + } + + None + }); + if let Some(path) = flashgrep_path { + std::env::set_var("FLASHGREP_DAEMON_BIN", &path); + log::info!( + "Workspace search daemon startup check passed: path={}", + path.display() + ); + } else { + log::warn!( + "Workspace search daemon startup check failed: {}", + bitfun_core::service::search::workspace_search_daemon_missing_hint() + ); + } + } + // Register bundled mobile-web resource path for remote connect. // tauri.conf.json maps "../../mobile-web/dist" -> "mobile-web/dist", // so the primary candidate is "mobile-web/dist". Additional fallbacks @@ -1051,6 +1100,14 @@ fn perform_process_exit_cleanup() -> bool { true } +fn configure_workspace_search_daemon_env() -> Option { + let path = bitfun_core::service::search::resolve_workspace_search_daemon_program_path(); + if let Some(path) = path.as_ref() { + std::env::set_var("FLASHGREP_DAEMON_BIN", path); + } + path +} + fn start_event_loop_with_transport( event_queue: Arc, event_router: Arc, @@ -1086,6 +1143,7 @@ fn init_services(app_handle: tauri::AppHandle, default_log_level: log::LevelFilt spawn_ingest_server_with_config_listener(); spawn_runtime_log_level_listener(default_log_level); + spawn_workspace_search_feature_listener(app_handle.clone()); tokio::spawn(async move { let transport = Arc::new(TauriTransportAdapter::new(app_handle.clone())); @@ -1184,6 +1242,93 @@ fn create_event_emitter( Arc::new(TransportEmitter::new(transport)) } +fn spawn_workspace_search_feature_listener(app_handle: tauri::AppHandle) { + use bitfun_core::service::config::{subscribe_config_updates, ConfigUpdateEvent}; + + let app_state: tauri::State<'_, api::AppState> = app_handle.state(); + let workspace_search_service = app_state.workspace_search_service.clone(); + let workspace_path = app_state.workspace_path.clone(); + + tokio::spawn(async move { + let mut feature_enabled = + bitfun_core::service::search::workspace_search_feature_enabled().await; + + let Some(mut receiver) = subscribe_config_updates() else { + log::warn!("Config update subscription unavailable for workspace search listener"); + return; + }; + + loop { + match receiver.recv().await { + Ok(ConfigUpdateEvent::AppUpdated) | Ok(ConfigUpdateEvent::ConfigReloaded) => { + let next_enabled = + bitfun_core::service::search::workspace_search_feature_enabled().await; + + if next_enabled == feature_enabled { + continue; + } + + if !next_enabled { + workspace_search_service.stop_all_daemons().await; + log::info!( + "Workspace search feature disabled; stopped flashgrep daemon and cleared sessions" + ); + feature_enabled = false; + continue; + } + + let resolved_path = configure_workspace_search_daemon_env(); + if !bitfun_core::service::search::workspace_search_daemon_available() { + log::warn!( + "Workspace search feature enabled but daemon is unavailable: path={:?}, hint={}", + resolved_path.as_ref().map(|path| path.display().to_string()), + bitfun_core::service::search::workspace_search_daemon_missing_hint() + ); + feature_enabled = true; + continue; + } + + let current_workspace = workspace_path.read().await.clone(); + if let Some(current_workspace) = current_workspace { + let workspace_str = current_workspace.to_string_lossy().to_string(); + if !bitfun_core::service::remote_ssh::workspace_state::is_remote_path( + workspace_str.trim(), + ) + .await + { + match workspace_search_service.open_repo(¤t_workspace).await { + Ok(_) => { + log::info!( + "Workspace search feature enabled; warmed current workspace: path={}", + current_workspace.display() + ); + } + Err(error) => { + log::warn!( + "Workspace search feature enabled but failed to warm current workspace: path={}, error={}", + current_workspace.display(), + error + ); + } + } + } + } + + feature_enabled = true; + } + Ok(_) => {} + Err(tokio::sync::broadcast::error::RecvError::Closed) => { + log::warn!("Workspace search feature listener channel closed"); + break; + } + Err(tokio::sync::broadcast::error::RecvError::Lagged(n)) => { + log::warn!("Workspace search feature listener lagged by {} messages", n); + } + } + } + }); +} + fn spawn_ingest_server_with_config_listener() { use bitfun_core::infrastructure::debug_log::IngestServerManager; use bitfun_core::service::config::{ diff --git a/src/apps/desktop/tauri.dev.conf.json b/src/apps/desktop/tauri.dev.conf.json new file mode 100644 index 000000000..b0f92544d --- /dev/null +++ b/src/apps/desktop/tauri.dev.conf.json @@ -0,0 +1,54 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "BitFun", + "identifier": "com.bitfun.desktop", + "build": { + "beforeDevCommand": "pnpm run dev:web", + "devUrl": "http://localhost:1422", + "beforeBuildCommand": "pnpm run build:web && pnpm run prepare:mobile-web", + "frontendDist": "../../../dist" + }, + "bundle": { + "active": true, + "targets": "all", + "icon": [ + "icons/icon.icns", + "icons/icon.ico", + "icons/icon.png" + ], + "resources": { + "../../mobile-web/dist": "mobile-web/dist", + "resources/worker_host.js": "resources/worker_host.js" + }, + "linux": { + "deb": { + "depends": [ + "libwebkit2gtk-4.1-0", + "libgtk-3-0" + ], + "files": { + "/usr/share/icons/hicolor/16x16/apps/bitfun-desktop.png": "icons/hicolor/16x16/apps/bitfun-desktop.png", + "/usr/share/icons/hicolor/32x32/apps/bitfun-desktop.png": "icons/hicolor/32x32/apps/bitfun-desktop.png", + "/usr/share/icons/hicolor/48x48/apps/bitfun-desktop.png": "icons/hicolor/48x48/apps/bitfun-desktop.png", + "/usr/share/icons/hicolor/64x64/apps/bitfun-desktop.png": "icons/hicolor/64x64/apps/bitfun-desktop.png", + "/usr/share/icons/hicolor/96x96/apps/bitfun-desktop.png": "icons/hicolor/96x96/apps/bitfun-desktop.png", + "/usr/share/icons/hicolor/128x128/apps/bitfun-desktop.png": "icons/hicolor/128x128/apps/bitfun-desktop.png", + "/usr/share/icons/hicolor/256x256/apps/bitfun-desktop.png": "icons/hicolor/256x256/apps/bitfun-desktop.png", + "/usr/share/icons/hicolor/512x512/apps/bitfun-desktop.png": "icons/hicolor/512x512/apps/bitfun-desktop.png" + }, + "postInstallScript": "scripts/post-install-icons.sh" + }, + "appimage": { + "bundleMediaFramework": false + } + } + }, + "app": { + "windows": [], + "security": { + "csp": null + }, + "macOSPrivateApi": true, + "withGlobalTauri": true + } +} diff --git a/src/crates/core/src/agentic/tools/implementations/glob_tool.rs b/src/crates/core/src/agentic/tools/implementations/glob_tool.rs index 0ef309436..f30f19202 100644 --- a/src/crates/core/src/agentic/tools/implementations/glob_tool.rs +++ b/src/crates/core/src/agentic/tools/implementations/glob_tool.rs @@ -1,5 +1,7 @@ use crate::agentic::tools::framework::{Tool, ToolResult, ToolUseContext}; -use crate::service::search::{get_global_workspace_search_service, GlobSearchRequest}; +use crate::service::search::{ + get_global_workspace_search_service, workspace_search_runtime_available, GlobSearchRequest, +}; use crate::util::errors::{BitFunError, BitFunResult}; use async_trait::async_trait; use globset::{GlobBuilder, GlobMatcher}; @@ -567,44 +569,46 @@ impl Tool for GlobTool { let resolved_str = resolved.resolved_path.clone(); - if let Some(search_service) = get_global_workspace_search_service() { - let workspace_root = context - .workspace - .as_ref() - .map(|workspace| PathBuf::from(workspace.root_path_string())) - .ok_or_else(|| { - BitFunError::tool( - "workspace_path is required when Glob path is omitted".to_string(), - ) - })?; - let resolved_path = PathBuf::from(&resolved_str); - let glob_result = search_service - .glob(GlobSearchRequest { - repo_root: workspace_root.clone(), - search_path: (resolved_path != workspace_root).then_some(resolved_path), - pattern: pattern.to_string(), - limit, - }) - .await?; - - let result_text = if glob_result.paths.is_empty() { - format!("No files found matching pattern '{}'", pattern) - } else { - glob_result.paths.join("\n") - }; - - return Ok(vec![ToolResult::Result { - data: json!({ - "pattern": pattern, - "path": resolved_str, - "matches": glob_result.paths, - "match_count": glob_result.paths.len(), - "repo_phase": glob_result.repo_status.phase, - "rebuild_recommended": glob_result.repo_status.rebuild_recommended - }), - result_for_assistant: Some(result_text), - image_attachments: None, - }]); + if workspace_search_runtime_available().await { + if let Some(search_service) = get_global_workspace_search_service() { + let workspace_root = context + .workspace + .as_ref() + .map(|workspace| PathBuf::from(workspace.root_path_string())) + .ok_or_else(|| { + BitFunError::tool( + "workspace_path is required when Glob path is omitted".to_string(), + ) + })?; + let resolved_path = PathBuf::from(&resolved_str); + let glob_result = search_service + .glob(GlobSearchRequest { + repo_root: workspace_root.clone(), + search_path: (resolved_path != workspace_root).then_some(resolved_path), + pattern: pattern.to_string(), + limit, + }) + .await?; + + let result_text = if glob_result.paths.is_empty() { + format!("No files found matching pattern '{}'", pattern) + } else { + glob_result.paths.join("\n") + }; + + return Ok(vec![ToolResult::Result { + data: json!({ + "pattern": pattern, + "path": resolved_str, + "matches": glob_result.paths, + "match_count": glob_result.paths.len(), + "repo_phase": glob_result.repo_status.phase, + "rebuild_recommended": glob_result.repo_status.rebuild_recommended + }), + result_for_assistant: Some(result_text), + image_attachments: None, + }]); + } } let resolved_str_for_rg = resolved_str.clone(); let pattern_for_rg = pattern.to_string(); diff --git a/src/crates/core/src/agentic/tools/implementations/grep_tool.rs b/src/crates/core/src/agentic/tools/implementations/grep_tool.rs index 373ef204e..37d2e905d 100644 --- a/src/crates/core/src/agentic/tools/implementations/grep_tool.rs +++ b/src/crates/core/src/agentic/tools/implementations/grep_tool.rs @@ -1,7 +1,7 @@ use crate::agentic::tools::framework::{Tool, ToolResult, ToolUseContext}; use crate::service::search::{ - get_global_workspace_search_service, ContentSearchOutputMode, ContentSearchRequest, - WorkspaceSearchHit, WorkspaceSearchLine, + get_global_workspace_search_service, workspace_search_runtime_available, + ContentSearchOutputMode, ContentSearchRequest, WorkspaceSearchHit, WorkspaceSearchLine, }; use crate::util::errors::{BitFunError, BitFunResult}; use async_trait::async_trait; @@ -830,66 +830,68 @@ Usage: return self.call_remote(input, context).await; } - if let Some(search_service) = get_global_workspace_search_service() { - let (request, output_mode, show_line_numbers, offset, head_limit) = - self.build_workspace_search_request(input, context)?; - let pattern = request.pattern.clone(); - let search_mode = request.output_mode.search_mode(); - let path = request - .search_path - .as_ref() - .map(|path| path.to_string_lossy().to_string()) - .unwrap_or_else(|| request.repo_root.to_string_lossy().to_string()); - let search_started_at = Instant::now(); - let search_result = search_service.search_content(request).await?; - let display_base = Self::display_base(context); - let (result_text, file_count, total_matches) = self.format_workspace_search_output( - &output_mode, - show_line_numbers, - offset, - head_limit, - &search_result, - display_base.as_deref(), - ); - let workspace_search_elapsed_ms = search_started_at.elapsed().as_millis(); - - log::info!( - "Grep tool workspace-search result: pattern={}, path={}, output_mode={}, search_mode={:?}, file_count={}, total_matches={}, backend={:?}, repo_phase={:?}, rebuild_recommended={}, dirty_modified={}, dirty_deleted={}, dirty_new={}, candidate_docs={}, matched_lines={}, matched_occurrences={}, workspace_search_ms={}", - pattern, - path, - output_mode, - search_mode, - file_count, - total_matches, - search_result.backend, - search_result.repo_status.phase, - search_result.repo_status.rebuild_recommended, - search_result.repo_status.dirty_files.modified, - search_result.repo_status.dirty_files.deleted, - search_result.repo_status.dirty_files.new, - search_result.candidate_docs, - search_result.matched_lines, - search_result.matched_occurrences, - workspace_search_elapsed_ms, - ); - - return Ok(vec![ToolResult::Result { - data: json!({ - "pattern": pattern, - "path": path, - "output_mode": output_mode, - "file_count": file_count, - "total_matches": total_matches, - "backend": search_result.backend, - "repo_phase": search_result.repo_status.phase, - "rebuild_recommended": search_result.repo_status.rebuild_recommended, - "applied_limit": head_limit, - "applied_offset": if offset > 0 { Some(offset) } else { None:: }, - "result": result_text, - }), - result_for_assistant: Some(result_text), - image_attachments: None, - }]); + if workspace_search_runtime_available().await { + if let Some(search_service) = get_global_workspace_search_service() { + let (request, output_mode, show_line_numbers, offset, head_limit) = + self.build_workspace_search_request(input, context)?; + let pattern = request.pattern.clone(); + let search_mode = request.output_mode.search_mode(); + let path = request + .search_path + .as_ref() + .map(|path| path.to_string_lossy().to_string()) + .unwrap_or_else(|| request.repo_root.to_string_lossy().to_string()); + let search_started_at = Instant::now(); + let search_result = search_service.search_content(request).await?; + let display_base = Self::display_base(context); + let (result_text, file_count, total_matches) = self.format_workspace_search_output( + &output_mode, + show_line_numbers, + offset, + head_limit, + &search_result, + display_base.as_deref(), + ); + let workspace_search_elapsed_ms = search_started_at.elapsed().as_millis(); + + log::info!( + "Grep tool workspace-search result: pattern={}, path={}, output_mode={}, search_mode={:?}, file_count={}, total_matches={}, backend={:?}, repo_phase={:?}, rebuild_recommended={}, dirty_modified={}, dirty_deleted={}, dirty_new={}, candidate_docs={}, matched_lines={}, matched_occurrences={}, workspace_search_ms={}", + pattern, + path, + output_mode, + search_mode, + file_count, + total_matches, + search_result.backend, + search_result.repo_status.phase, + search_result.repo_status.rebuild_recommended, + search_result.repo_status.dirty_files.modified, + search_result.repo_status.dirty_files.deleted, + search_result.repo_status.dirty_files.new, + search_result.candidate_docs, + search_result.matched_lines, + search_result.matched_occurrences, + workspace_search_elapsed_ms, + ); + + return Ok(vec![ToolResult::Result { + data: json!({ + "pattern": pattern, + "path": path, + "output_mode": output_mode, + "file_count": file_count, + "total_matches": total_matches, + "backend": search_result.backend, + "repo_phase": search_result.repo_status.phase, + "rebuild_recommended": search_result.repo_status.rebuild_recommended, + "applied_limit": head_limit, + "applied_offset": if offset > 0 { Some(offset) } else { None:: }, + "result": result_text, + }), + result_for_assistant: Some(result_text), + image_attachments: None, + }]); + } } let grep_options = self.build_grep_options(input, context)?; diff --git a/src/crates/core/src/service/config/manager.rs b/src/crates/core/src/service/config/manager.rs index 846e4aa4b..677847227 100644 --- a/src/crates/core/src/service/config/manager.rs +++ b/src/crates/core/src/service/config/manager.rs @@ -506,6 +506,7 @@ impl ConfigManager { path: &str, old_config: &GlobalConfig, ) -> BitFunResult<()> { + self.check_and_broadcast_app_change(path).await; self.check_and_broadcast_debug_mode_change(old_config).await; self.check_and_broadcast_log_level_change(old_config).await; @@ -514,6 +515,14 @@ impl ConfigManager { .await } + /// Detects and broadcasts app-scope configuration changes. + async fn check_and_broadcast_app_change(&self, path: &str) { + if path == "app" || path.starts_with("app.") { + use super::global::{ConfigUpdateEvent, GlobalConfigManager}; + GlobalConfigManager::broadcast_update(ConfigUpdateEvent::AppUpdated).await; + } + } + /// Detects and broadcasts debug-mode configuration changes. async fn check_and_broadcast_debug_mode_change(&self, old_config: &GlobalConfig) { let old_debug = &old_config.ai.debug_mode_config; diff --git a/src/crates/core/src/service/config/types.rs b/src/crates/core/src/service/config/types.rs index 34068f668..a2c05ebbe 100644 --- a/src/crates/core/src/service/config/types.rs +++ b/src/crates/core/src/service/config/types.rs @@ -119,6 +119,8 @@ pub struct AIExperienceConfig { /// Optional Petdex-compatible companion package selected by the user. #[serde(default, skip_serializing_if = "Option::is_none")] pub agent_companion_pet: Option, + /// Whether to enable flashgrep-backed accelerated workspace search. + pub enable_workspace_search: bool, } /// User-selected Agent companion pet package. @@ -1275,6 +1277,7 @@ impl Default for AIExperienceConfig { enable_agent_companion: true, agent_companion_display_mode: "desktop".to_string(), agent_companion_pet: None, + enable_workspace_search: false, } } } diff --git a/src/crates/core/src/service/search/flashgrep/client.rs b/src/crates/core/src/service/search/flashgrep/client.rs index 2eab5592d..724a43b08 100644 --- a/src/crates/core/src/service/search/flashgrep/client.rs +++ b/src/crates/core/src/service/search/flashgrep/client.rs @@ -130,6 +130,14 @@ impl ManagedClient { Ok(()) } + pub(crate) async fn stop_daemon(&self) -> Result<()> { + let daemon = self.state.lock().await.daemon.take(); + if let Some(daemon) = daemon { + daemon.shutdown().await?; + } + Ok(()) + } + async fn send_request_with_restart(&self, request: Request) -> Result { self.send_request_with_restart_timeout(request, None).await } diff --git a/src/crates/core/src/service/search/flashgrep/protocol.rs b/src/crates/core/src/service/search/flashgrep/protocol.rs index 667831bca..8aa398b8b 100644 --- a/src/crates/core/src/service/search/flashgrep/protocol.rs +++ b/src/crates/core/src/service/search/flashgrep/protocol.rs @@ -323,7 +323,8 @@ pub(crate) enum Response { SearchCompleted { repo_id: String, backend: SearchBackend, - consistency_applied: ConsistencyMode, + #[serde(default, skip_serializing_if = "Option::is_none")] + consistency_applied: Option, status: RepoStatus, results: SearchResults, }, @@ -362,6 +363,7 @@ pub(crate) struct ServerCapabilities { #[derive(Debug, Clone, Serialize, Deserialize)] pub(crate) struct SearchProtocolCapabilities { + #[serde(default)] pub consistency_modes: Vec, pub search_modes: Vec, } diff --git a/src/crates/core/src/service/search/mod.rs b/src/crates/core/src/service/search/mod.rs index 8ae47f547..a467f21a8 100644 --- a/src/crates/core/src/service/search/mod.rs +++ b/src/crates/core/src/service/search/mod.rs @@ -3,8 +3,11 @@ pub mod service; pub mod types; pub use service::{ - get_global_workspace_search_service, set_global_workspace_search_service, - WorkspaceSearchService, + get_global_workspace_search_service, resolve_workspace_search_daemon_program_path, + set_global_workspace_search_service, workspace_search_daemon_available, + workspace_search_daemon_binary_name, workspace_search_daemon_binary_names, + workspace_search_daemon_missing_hint, workspace_search_feature_enabled, + workspace_search_runtime_available, WorkspaceSearchService, }; pub use types::{ ContentSearchOutputMode, ContentSearchRequest, ContentSearchResult, GlobSearchRequest, diff --git a/src/crates/core/src/service/search/service.rs b/src/crates/core/src/service/search/service.rs index 1e1c7e624..d5d45c880 100644 --- a/src/crates/core/src/service/search/service.rs +++ b/src/crates/core/src/service/search/service.rs @@ -279,6 +279,20 @@ impl WorkspaceSearchService { } } + pub async fn stop_all_daemons(&self) { + let released_sessions = self.sessions.write().await.drain().count(); + self.open_guards.lock().await.clear(); + if released_sessions > 0 { + log::info!( + "Workspace search stop releasing sessions via daemon stop: count={}", + released_sessions + ); + } + if let Err(error) = self.client.stop_daemon().await { + log::debug!("Workspace search daemon stop skipped: {}", error); + } + } + async fn get_or_open_session(&self, repo_root: &Path) -> BitFunResult> { let repo_root = normalize_repo_root(repo_root)?; let repo_guard = { @@ -413,34 +427,92 @@ pub fn get_global_workspace_search_service() -> Option Option { +pub fn workspace_search_daemon_binary_names() -> &'static [&'static str] { + if cfg!(all(target_os = "windows", target_arch = "x86_64")) { + &["flashgrep-x86_64-pc-windows-msvc.exe"] + } else if cfg!(all(target_os = "windows", target_arch = "aarch64")) { + &["flashgrep-aarch64-pc-windows-msvc.exe"] + } else if cfg!(all(target_os = "macos", target_arch = "x86_64")) { + &["flashgrep-x86_64-apple-darwin"] + } else if cfg!(all(target_os = "macos", target_arch = "aarch64")) { + &["flashgrep-aarch64-apple-darwin"] + } else if cfg!(all(target_os = "linux", target_arch = "x86_64")) { + &["flashgrep-x86_64-unknown-linux-gnu"] + } else if cfg!(all(target_os = "linux", target_arch = "aarch64")) { + &["flashgrep-aarch64-unknown-linux-gnu"] + } else if cfg!(windows) { + &["flashgrep.exe"] + } else { + &["flashgrep"] + } +} + +pub fn workspace_search_daemon_binary_name() -> &'static str { + workspace_search_daemon_binary_names() + .first() + .copied() + .unwrap_or("flashgrep") +} + +pub fn workspace_search_daemon_missing_hint() -> String { + let bundled_paths = workspace_search_daemon_binary_names() + .iter() + .map(|name| format!("flashgrep/{name}")) + .collect::>() + .join(", "); + format!( + "workspace search daemon binary is missing; expected one of bundled resources [{}] or a valid FLASHGREP_DAEMON_BIN override", + bundled_paths + ) +} + +pub fn workspace_search_daemon_available() -> bool { + resolve_workspace_search_daemon_program_path().is_some() +} + +pub async fn workspace_search_feature_enabled() -> bool { + match get_global_config_service().await { + Ok(config_service) => config_service + .get_config::(Some("app.ai_experience.enable_workspace_search")) + .await + .unwrap_or(false), + Err(_) => false, + } +} + +pub async fn workspace_search_runtime_available() -> bool { + workspace_search_feature_enabled().await && workspace_search_daemon_available() +} + +pub fn resolve_workspace_search_daemon_program_path() -> Option { if let Some(program) = std::env::var_os("FLASHGREP_DAEMON_BIN") { - return Some(program); + let path = PathBuf::from(program); + if path.exists() { + return Some(path); + } } let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); let workspace_root = manifest_dir.join("../../.."); - let binary_name = if cfg!(windows) { - "flashgrep.exe" - } else { - "flashgrep" - }; + let binary_names = workspace_search_daemon_binary_names(); let profile = std::env::var("PROFILE").ok(); - for candidate in daemon_binary_candidates(&workspace_root, binary_name, profile.as_deref()) { + for candidate in daemon_binary_candidates(&workspace_root, binary_names, profile.as_deref()) { if candidate.exists() { - return Some(candidate.into_os_string()); + return Some(candidate); } } - which::which("flashgrep") - .ok() - .map(|path| path.into_os_string()) + which::which("flashgrep").ok() +} + +fn resolve_daemon_program() -> Option { + resolve_workspace_search_daemon_program_path().map(PathBuf::into_os_string) } fn daemon_binary_candidates( workspace_root: &Path, - binary_name: &str, + binary_names: &[&str], current_profile: Option<&str>, ) -> Vec { let mut candidates = Vec::new(); @@ -454,8 +526,10 @@ fn daemon_binary_candidates( if let Ok(current_exe) = std::env::current_exe() { if let Some(parent) = current_exe.parent() { - push_candidate(parent.join(binary_name)); - push_exe_relative_bundle_candidates(&mut push_candidate, parent, binary_name); + for binary_name in binary_names { + push_candidate(parent.join(binary_name)); + } + push_exe_relative_bundle_candidates(&mut push_candidate, parent, binary_names); } } @@ -463,12 +537,14 @@ fn daemon_binary_candidates( .into_iter() .chain(["debug", "release", "release-fast"]) { - push_candidate( - workspace_root - .join("target") - .join(profile) - .join(binary_name), - ); + for binary_name in binary_names { + push_candidate( + workspace_root + .join("target") + .join(profile) + .join(binary_name), + ); + } } candidates @@ -477,23 +553,29 @@ fn daemon_binary_candidates( fn push_exe_relative_bundle_candidates( push_candidate: &mut impl FnMut(PathBuf), exe_dir: &Path, - binary_name: &str, + binary_names: &[&str], ) { if cfg!(target_os = "macos") { - push_candidate(exe_dir.join("../Resources/flashgrep").join(binary_name)); + for binary_name in binary_names { + push_candidate(exe_dir.join("../Resources/flashgrep").join(binary_name)); + } } - push_candidate(exe_dir.join("flashgrep").join(binary_name)); - push_candidate(exe_dir.join("resources/flashgrep").join(binary_name)); + for binary_name in binary_names { + push_candidate(exe_dir.join("flashgrep").join(binary_name)); + push_candidate(exe_dir.join("resources/flashgrep").join(binary_name)); + } if cfg!(target_os = "linux") { - push_candidate(exe_dir.join("../lib/bitfun/flashgrep").join(binary_name)); - push_candidate(exe_dir.join("../share/bitfun/flashgrep").join(binary_name)); - push_candidate( - exe_dir - .join("../share/com.bitfun.desktop/flashgrep") - .join(binary_name), - ); + for binary_name in binary_names { + push_candidate(exe_dir.join("../lib/bitfun/flashgrep").join(binary_name)); + push_candidate(exe_dir.join("../share/bitfun/flashgrep").join(binary_name)); + push_candidate( + exe_dir + .join("../share/com.bitfun.desktop/flashgrep") + .join(binary_name), + ); + } } } @@ -732,5 +814,15 @@ fn split_preview( fn map_flashgrep_error( prefix: &'static str, ) -> impl Fn(crate::service::search::flashgrep::error::AppError) -> BitFunError { - move |error| BitFunError::service(format!("{prefix}: {error}")) + move |error| { + let detail = match &error { + crate::service::search::flashgrep::error::AppError::Io(io_error) + if io_error.kind() == std::io::ErrorKind::NotFound => + { + format!("{error}. {}", workspace_search_daemon_missing_hint()) + } + _ => error.to_string(), + }; + BitFunError::service(format!("{prefix}: {detail}")) + } } diff --git a/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceItem.tsx b/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceItem.tsx index a04adb9d3..91a3c8146 100644 --- a/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceItem.tsx +++ b/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceItem.tsx @@ -6,6 +6,7 @@ import { DotMatrixArrowRightIcon } from './DotMatrixArrowRightIcon'; import { Button, ConfirmDialog, Modal, Tooltip } from '@/component-library'; import { useI18n } from '@/infrastructure/i18n'; import { i18nService } from '@/infrastructure/i18n'; +import { aiExperienceConfigService } from '@/infrastructure/config/services/AIExperienceConfigService'; import { useWorkspaceContext } from '@/infrastructure/contexts/WorkspaceContext'; import { createWorktreeWorkspace, @@ -135,6 +136,9 @@ const WorkspaceItem: React.FC = ({ const [isResettingWorkspace, setIsResettingWorkspace] = useState(false); const [sessionsCollapsed, setSessionsCollapsed] = useState(false); const [searchIndexModalOpen, setSearchIndexModalOpen] = useState(false); + const [workspaceSearchEnabled, setWorkspaceSearchEnabled] = useState( + () => aiExperienceConfigService.getSettings().enable_workspace_search, + ); const [acpClients, setAcpClients] = useState([]); const menuRef = useRef(null); const menuAnchorRef = useRef(null); @@ -155,6 +159,7 @@ const WorkspaceItem: React.FC = ({ const isLinkedWorktree = isLinkedWorktreeWorkspace(workspace); const canShowSearchIndex = isActive + && workspaceSearchEnabled && workspace.workspaceKind === WorkspaceKind.Normal && !isRemoteWorkspace(workspace); const workspaceSearchIndex = useWorkspaceSearchIndex({ @@ -162,6 +167,13 @@ const WorkspaceItem: React.FC = ({ enabled: canShowSearchIndex, }); + useEffect(() => { + setWorkspaceSearchEnabled(aiExperienceConfigService.getSettings().enable_workspace_search); + return aiExperienceConfigService.addChangeListener(settings => { + setWorkspaceSearchEnabled(settings.enable_workspace_search); + }); + }, []); + // Remote connection status — optional: safe if not inside SSHRemoteProvider const sshContext = useContext(SSHContext); const remoteConnStatus = workspace.connectionId && sshContext diff --git a/src/web-ui/src/app/scenes/settings/settingsTabSearchContent.ts b/src/web-ui/src/app/scenes/settings/settingsTabSearchContent.ts index 1164d52a9..4d26411d0 100644 --- a/src/web-ui/src/app/scenes/settings/settingsTabSearchContent.ts +++ b/src/web-ui/src/app/scenes/settings/settingsTabSearchContent.ts @@ -41,6 +41,9 @@ export const SETTINGS_TAB_SEARCH_CONTENT: Record { + flowChatStore.setState((): FlowChatState => ({ + sessions: new Map(), + activeSessionId: null, + })); +}; + +const createWorkspace = (): WorkspaceInfo => ({ + id: 'workspace-1', + name: 'BitFun', + rootPath: '/workspace/BitFun', + workspaceKind: WorkspaceKind.Normal, +}); + +const createSession = (overrides: Partial = {}): Session => ({ + sessionId: 'session-1', + title: 'Session 1', + dialogTurns: [], + status: 'idle', + config: { agentType: 'agentic' }, + createdAt: 1, + lastActiveAt: 1, + error: null, + isHistorical: false, + maxContextTokens: 128128, + mode: 'agentic', + workspacePath: '/workspace/BitFun', + workspaceId: 'workspace-1', + sessionKind: 'normal', + btwThreads: [], + isTransient: false, + ...overrides, +}); + +describe('findReusableEmptySessionId', () => { + afterEach(() => { + resetStore(); + }); + + it('does not reuse an empty ACP session for a new code session', () => { + const workspace = createWorkspace(); + const acpSession = createSession({ + sessionId: 'acp-session', + config: { agentType: 'acp:codex' }, + mode: 'acp:codex', + lastActiveAt: 10, + }); + + flowChatStore.setState(() => ({ + sessions: new Map([[acpSession.sessionId, acpSession]]), + activeSessionId: acpSession.sessionId, + })); + + expect(findReusableEmptySessionId(workspace, 'agentic')).toBeNull(); + }); + + it('still reuses a matching empty code session when ACP sessions also exist', () => { + const workspace = createWorkspace(); + const codeSession = createSession({ + sessionId: 'code-session', + lastActiveAt: 5, + }); + const acpSession = createSession({ + sessionId: 'acp-session', + config: { agentType: 'acp:codex' }, + mode: 'acp:codex', + lastActiveAt: 20, + }); + + flowChatStore.setState(() => ({ + sessions: new Map([ + [codeSession.sessionId, codeSession], + [acpSession.sessionId, acpSession], + ]), + activeSessionId: acpSession.sessionId, + })); + + expect(findReusableEmptySessionId(workspace, 'agentic')).toBe(codeSession.sessionId); + }); +}); diff --git a/src/web-ui/src/app/utils/projectSessionWorkspace.ts b/src/web-ui/src/app/utils/projectSessionWorkspace.ts index 9aa6a9535..d0193d8b9 100644 --- a/src/web-ui/src/app/utils/projectSessionWorkspace.ts +++ b/src/web-ui/src/app/utils/projectSessionWorkspace.ts @@ -1,5 +1,6 @@ import { flowChatStore } from '@/flow_chat/store/FlowChatStore'; import type { Session } from '@/flow_chat/types/flow-chat'; +import { isAcpFlowSession } from '@/flow_chat/utils/acpSession'; import { WorkspaceKind, isRemoteWorkspace, type WorkspaceInfo } from '@/shared/types'; type SessionDisplayBucket = 'code' | 'cowork' | 'claw'; @@ -53,6 +54,9 @@ function isEmptyReusableSession(session: Session, workspace: WorkspaceInfo, buck if (session.sessionKind !== 'normal') { return false; } + if (isAcpFlowSession(session)) { + return false; + } if (session.isHistorical) { return false; } diff --git a/src/web-ui/src/infrastructure/config/components/SessionConfig.tsx b/src/web-ui/src/infrastructure/config/components/SessionConfig.tsx index 8896f8fbb..e264dfe51 100644 --- a/src/web-ui/src/infrastructure/config/components/SessionConfig.tsx +++ b/src/web-ui/src/infrastructure/config/components/SessionConfig.tsx @@ -863,6 +863,22 @@ const SessionConfig: React.FC = () => { + {/* ── Accelerated workspace search ───────────────────────── */} + + +
+ updateSetting('enable_workspace_search', e.target.checked)} + size="small" + /> +
+
+
+ {/* ── Tool execution behavior ────────────────────────────── */} (null); const [loading, setLoading] = useState(false); const [refreshing, setRefreshing] = useState(false); const [actionRunning, setActionRunning] = useState(false); const [error, setError] = useState(null); + const [backendSupported, setBackendSupported] = useState(true); + const supported = Boolean(workspacePath && enabled && backendSupported); const mountedRef = useRef(true); const pollTimerRef = useRef | null>(null); @@ -86,6 +93,12 @@ export function useWorkspaceSearchIndex( return null; } const message = err instanceof Error ? err.message : 'Failed to load search index status'; + if (isWorkspaceSearchUnavailableError(message)) { + setBackendSupported(false); + setIndexStatus(null); + setError(null); + return null; + } log.warn('Failed to refresh workspace search index status', { workspacePath, error: err, @@ -127,7 +140,13 @@ export function useWorkspaceSearchIndex( } catch (err) { if (mountedRef.current) { const message = err instanceof Error ? err.message : `Failed to ${action} search index`; - setError(message); + if (isWorkspaceSearchUnavailableError(message)) { + setBackendSupported(false); + setIndexStatus(null); + setError(null); + } else { + setError(message); + } } return null; } finally { @@ -150,6 +169,10 @@ export function useWorkspaceSearchIndex( }; }, [clearPollTimer]); + useEffect(() => { + setBackendSupported(true); + }, [enabled, workspacePath]); + useEffect(() => { clearPollTimer();