diff --git a/src/apps/desktop/src/api/ai_memory_api.rs b/src/apps/desktop/src/api/ai_memory_api.rs deleted file mode 100644 index 0c57aae0e..000000000 --- a/src/apps/desktop/src/api/ai_memory_api.rs +++ /dev/null @@ -1,116 +0,0 @@ -//! AI Memory Points API - -use bitfun_core::infrastructure::PathManager; -use bitfun_core::service::ai_memory::{AIMemory, AIMemoryManager, MemoryType}; -use serde::{Deserialize, Serialize}; -use std::sync::Arc; -use tauri::State; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CreateMemoryRequest { - pub title: String, - pub content: String, - #[serde(rename = "type")] - pub memory_type: MemoryType, - pub importance: u8, - pub tags: Option>, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct UpdateMemoryRequest { - pub id: String, - pub title: String, - pub content: String, - #[serde(rename = "type")] - pub memory_type: MemoryType, - pub importance: u8, - pub tags: Vec, - pub enabled: bool, -} - -#[tauri::command] -pub async fn get_all_memories( - path_manager: State<'_, Arc>, -) -> Result, String> { - let manager = AIMemoryManager::new(path_manager.inner().clone()) - .await - .map_err(|e| e.to_string())?; - - manager.get_all_memories().await.map_err(|e| e.to_string()) -} - -#[tauri::command] -pub async fn add_memory( - path_manager: State<'_, Arc>, - request: CreateMemoryRequest, -) -> Result { - let manager = AIMemoryManager::new(path_manager.inner().clone()) - .await - .map_err(|e| e.to_string())?; - - let mut memory = AIMemory::new( - request.title, - request.content, - request.memory_type, - request.importance, - ); - - if let Some(tags) = request.tags { - memory.tags = tags; - } - - manager.add_memory(memory).await.map_err(|e| e.to_string()) -} - -#[tauri::command] -pub async fn update_memory( - path_manager: State<'_, Arc>, - request: UpdateMemoryRequest, -) -> Result { - let manager = AIMemoryManager::new(path_manager.inner().clone()) - .await - .map_err(|e| e.to_string())?; - - let now = chrono::Utc::now().to_rfc3339(); - let memory = AIMemory { - id: request.id.clone(), - title: request.title, - content: request.content, - memory_type: request.memory_type, - tags: request.tags, - source: "User manual edit".to_string(), - created_at: now.clone(), - updated_at: now, - importance: request.importance.min(5), - enabled: request.enabled, - }; - - manager - .update_memory(memory) - .await - .map_err(|e| e.to_string()) -} - -#[tauri::command] -pub async fn delete_memory( - path_manager: State<'_, Arc>, - id: String, -) -> Result { - let manager = AIMemoryManager::new(path_manager.inner().clone()) - .await - .map_err(|e| e.to_string())?; - - manager.delete_memory(&id).await.map_err(|e| e.to_string()) -} - -#[tauri::command] -pub async fn toggle_memory( - path_manager: State<'_, Arc>, - id: String, -) -> Result { - let manager = AIMemoryManager::new(path_manager.inner().clone()) - .await - .map_err(|e| e.to_string())?; - - manager.toggle_memory(&id).await.map_err(|e| e.to_string()) -} diff --git a/src/apps/desktop/src/api/ai_rules_api.rs b/src/apps/desktop/src/api/ai_rules_api.rs deleted file mode 100644 index bd1a6a301..000000000 --- a/src/apps/desktop/src/api/ai_rules_api.rs +++ /dev/null @@ -1,380 +0,0 @@ -//! AI Rules Management API - -use crate::api::AppState; -use bitfun_core::service::ai_rules::*; -use serde::{Deserialize, Serialize}; -use std::path::PathBuf; -use tauri::State; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum ApiRuleLevel { - User, - Project, - All, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct GetRulesRequest { - pub level: ApiRuleLevel, - pub workspace_path: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct GetRuleRequest { - pub level: ApiRuleLevel, - pub name: String, - pub workspace_path: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CreateRuleApiRequest { - pub level: ApiRuleLevel, - pub rule: CreateRuleRequest, - pub workspace_path: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct UpdateRuleApiRequest { - pub level: ApiRuleLevel, - pub name: String, - pub rule: UpdateRuleRequest, - pub workspace_path: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct DeleteRuleApiRequest { - pub level: ApiRuleLevel, - pub name: String, - pub workspace_path: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct GetRulesStatsRequest { - pub level: ApiRuleLevel, - pub workspace_path: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ReloadRulesRequest { - pub level: ApiRuleLevel, - pub workspace_path: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ToggleRuleApiRequest { - pub level: ApiRuleLevel, - pub name: String, - pub workspace_path: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct BuildSystemPromptRequest { - pub workspace_path: String, -} - -fn workspace_root_from_request(workspace_path: Option<&str>) -> Option { - workspace_path - .filter(|path| !path.is_empty()) - .map(PathBuf::from) -} - -fn require_workspace_root( - level: ApiRuleLevel, - workspace_root: Option, -) -> Result { - match level { - ApiRuleLevel::Project | ApiRuleLevel::All => workspace_root.ok_or_else(|| { - "workspacePath is required when level includes project rules".to_string() - }), - ApiRuleLevel::User => Err("workspacePath is not used for user-only rules".to_string()), - } -} - -#[tauri::command] -pub async fn get_ai_rules( - state: State<'_, AppState>, - request: GetRulesRequest, -) -> Result, String> { - let rules_service = &state.ai_rules_service; - let workspace_root = workspace_root_from_request(request.workspace_path.as_deref()); - - match request.level { - ApiRuleLevel::User => rules_service - .get_user_rules() - .await - .map_err(|e| format!("Failed to get user rules: {}", e)), - ApiRuleLevel::Project => { - let workspace_root = require_workspace_root(request.level, workspace_root)?; - rules_service - .get_project_rules_for_workspace(&workspace_root) - .await - .map_err(|e| format!("Failed to get project rules: {}", e)) - } - ApiRuleLevel::All => { - let workspace_root = require_workspace_root(request.level, workspace_root)?; - let mut all_rules = Vec::new(); - - let user_rules = rules_service - .get_user_rules() - .await - .map_err(|e| format!("Failed to get user rules: {}", e))?; - all_rules.extend(user_rules); - - let project_rules = rules_service - .get_project_rules_for_workspace(&workspace_root) - .await - .map_err(|e| format!("Failed to get project rules: {}", e))?; - all_rules.extend(project_rules); - all_rules.sort_by(|a, b| a.name.cmp(&b.name)); - - Ok(all_rules) - } - } -} - -#[tauri::command] -pub async fn get_ai_rule( - state: State<'_, AppState>, - request: GetRuleRequest, -) -> Result, String> { - let rules_service = &state.ai_rules_service; - let workspace_root = workspace_root_from_request(request.workspace_path.as_deref()); - - match request.level { - ApiRuleLevel::User => rules_service - .get_user_rule(&request.name) - .await - .map_err(|e| format!("Failed to get user rule: {}", e)), - ApiRuleLevel::Project => { - let workspace_root = require_workspace_root(request.level, workspace_root)?; - rules_service - .get_project_rule_for_workspace(&workspace_root, &request.name) - .await - .map_err(|e| format!("Failed to get project rule: {}", e)) - } - ApiRuleLevel::All => { - let workspace_root = require_workspace_root(request.level, workspace_root)?; - if let Some(rule) = rules_service - .get_user_rule(&request.name) - .await - .map_err(|e| format!("Failed to get user rule: {}", e))? - { - Ok(Some(rule)) - } else { - rules_service - .get_project_rule_for_workspace(&workspace_root, &request.name) - .await - .map_err(|e| format!("Failed to get project rule: {}", e)) - } - } - } -} - -#[tauri::command] -pub async fn create_ai_rule( - state: State<'_, AppState>, - request: CreateRuleApiRequest, -) -> Result { - let rules_service = &state.ai_rules_service; - let workspace_root = workspace_root_from_request(request.workspace_path.as_deref()); - - match request.level { - ApiRuleLevel::User => rules_service - .create_user_rule(request.rule) - .await - .map_err(|e| format!("Failed to create user rule: {}", e)), - ApiRuleLevel::Project => { - let workspace_root = require_workspace_root(request.level, workspace_root)?; - rules_service - .create_project_rule_for_workspace(&workspace_root, request.rule) - .await - .map_err(|e| format!("Failed to create project rule: {}", e)) - } - ApiRuleLevel::All => Err( - "Cannot create rule with 'all' level. Please specify 'user' or 'project'.".to_string(), - ), - } -} - -#[tauri::command] -pub async fn update_ai_rule( - state: State<'_, AppState>, - request: UpdateRuleApiRequest, -) -> Result { - let rules_service = &state.ai_rules_service; - let workspace_root = workspace_root_from_request(request.workspace_path.as_deref()); - - match request.level { - ApiRuleLevel::User => rules_service - .update_user_rule(&request.name, request.rule) - .await - .map_err(|e| format!("Failed to update user rule: {}", e)), - ApiRuleLevel::Project => { - let workspace_root = require_workspace_root(request.level, workspace_root)?; - rules_service - .update_project_rule_for_workspace(&workspace_root, &request.name, request.rule) - .await - .map_err(|e| format!("Failed to update project rule: {}", e)) - } - ApiRuleLevel::All => Err( - "Cannot update rule with 'all' level. Please specify 'user' or 'project'.".to_string(), - ), - } -} - -#[tauri::command] -pub async fn delete_ai_rule( - state: State<'_, AppState>, - request: DeleteRuleApiRequest, -) -> Result { - let rules_service = &state.ai_rules_service; - let workspace_root = workspace_root_from_request(request.workspace_path.as_deref()); - - match request.level { - ApiRuleLevel::User => rules_service - .delete_user_rule(&request.name) - .await - .map_err(|e| format!("Failed to delete user rule: {}", e)), - ApiRuleLevel::Project => { - let workspace_root = require_workspace_root(request.level, workspace_root)?; - rules_service - .delete_project_rule_for_workspace(&workspace_root, &request.name) - .await - .map_err(|e| format!("Failed to delete project rule: {}", e)) - } - ApiRuleLevel::All => Err( - "Cannot delete rule with 'all' level. Please specify 'user' or 'project'.".to_string(), - ), - } -} - -#[tauri::command] -pub async fn get_ai_rules_stats( - state: State<'_, AppState>, - request: GetRulesStatsRequest, -) -> Result { - let rules_service = &state.ai_rules_service; - let workspace_root = workspace_root_from_request(request.workspace_path.as_deref()); - - match request.level { - ApiRuleLevel::User => rules_service - .get_user_rules_stats() - .await - .map_err(|e| format!("Failed to get user rules stats: {}", e)), - ApiRuleLevel::Project => { - let workspace_root = require_workspace_root(request.level, workspace_root)?; - rules_service - .get_project_rules_stats_for_workspace(&workspace_root) - .await - .map_err(|e| format!("Failed to get project rules stats: {}", e)) - } - ApiRuleLevel::All => { - let workspace_root = require_workspace_root(request.level, workspace_root)?; - let user_stats = rules_service - .get_user_rules_stats() - .await - .map_err(|e| format!("Failed to get user rules stats: {}", e))?; - let project_stats = rules_service - .get_project_rules_stats_for_workspace(&workspace_root) - .await - .map_err(|e| format!("Failed to get project rules stats: {}", e))?; - - let mut by_apply_type = user_stats.by_apply_type.clone(); - for (key, value) in project_stats.by_apply_type { - *by_apply_type.entry(key).or_insert(0) += value; - } - - Ok(RuleStats { - total_rules: user_stats.total_rules + project_stats.total_rules, - enabled_rules: user_stats.enabled_rules + project_stats.enabled_rules, - disabled_rules: user_stats.disabled_rules + project_stats.disabled_rules, - by_apply_type, - }) - } - } -} - -#[tauri::command] -pub async fn build_ai_rules_system_prompt( - state: State<'_, AppState>, - request: BuildSystemPromptRequest, -) -> Result { - let rules_service = &state.ai_rules_service; - let workspace_root = workspace_root_from_request(Some(request.workspace_path.as_str())) - .ok_or_else(|| "workspacePath is required to build project AI rules prompt".to_string())?; - - rules_service - .build_system_prompt_for(Some(&workspace_root)) - .await - .map_err(|e| format!("Failed to build system prompt: {}", e)) -} - -#[tauri::command] -pub async fn reload_ai_rules( - state: State<'_, AppState>, - request: ReloadRulesRequest, -) -> Result<(), String> { - let rules_service = &state.ai_rules_service; - let workspace_root = workspace_root_from_request(request.workspace_path.as_deref()); - - match request.level { - ApiRuleLevel::User => rules_service - .reload_user_rules() - .await - .map_err(|e| format!("Failed to reload user rules: {}", e)), - ApiRuleLevel::Project => { - let workspace_root = require_workspace_root(request.level, workspace_root)?; - rules_service - .reload_project_rules_for_workspace(&workspace_root) - .await - .map_err(|e| format!("Failed to reload project rules: {}", e)) - } - ApiRuleLevel::All => { - let workspace_root = require_workspace_root(request.level, workspace_root)?; - rules_service - .reload_user_rules() - .await - .map_err(|e| format!("Failed to reload user rules: {}", e))?; - rules_service - .reload_project_rules_for_workspace(&workspace_root) - .await - .map_err(|e| format!("Failed to reload project rules: {}", e)) - } - } -} - -#[tauri::command] -pub async fn toggle_ai_rule( - state: State<'_, AppState>, - request: ToggleRuleApiRequest, -) -> Result { - let rules_service = &state.ai_rules_service; - let workspace_root = workspace_root_from_request(request.workspace_path.as_deref()); - - match request.level { - ApiRuleLevel::User => rules_service - .toggle_user_rule(&request.name) - .await - .map_err(|e| format!("Failed to toggle user rule: {}", e)), - ApiRuleLevel::Project => { - let workspace_root = require_workspace_root(request.level, workspace_root)?; - rules_service - .toggle_project_rule_for_workspace(&workspace_root, &request.name) - .await - .map_err(|e| format!("Failed to toggle project rule: {}", e)) - } - ApiRuleLevel::All => Err( - "Cannot toggle rule with 'all' level. Please specify 'user' or 'project'.".to_string(), - ), - } -} diff --git a/src/apps/desktop/src/api/app_state.rs b/src/apps/desktop/src/api/app_state.rs index 9579b05b5..3bc27cf2f 100644 --- a/src/apps/desktop/src/api/app_state.rs +++ b/src/apps/desktop/src/api/app_state.rs @@ -11,7 +11,7 @@ use bitfun_core::service::remote_ssh::{ init_remote_workspace_manager, RemoteFileService, RemoteTerminalManager, SSHConnectionManager, }; use bitfun_core::service::{ - ai_rules, announcement, config, filesystem, mcp, search, token_usage, workspace, + announcement, config, filesystem, mcp, search, token_usage, workspace, }; use bitfun_core::util::errors::*; @@ -71,7 +71,6 @@ pub struct AppState { pub config_service: Arc, pub filesystem_service: Arc, pub workspace_search_service: Arc, - pub ai_rules_service: Arc, pub agent_registry: Arc, pub mcp_service: Option>, pub acp_client_service: Option>, @@ -121,15 +120,6 @@ impl AppState { let workspace_search_service = Arc::new(search::WorkspaceSearchService::new()); search::set_global_workspace_search_service(workspace_search_service.clone()); - ai_rules::initialize_global_ai_rules_service() - .await - .map_err(|e| { - BitFunError::service(format!("Failed to initialize AI rules service: {}", e)) - })?; - let ai_rules_service = ai_rules::get_global_ai_rules_service() - .await - .map_err(|e| BitFunError::service(format!("Failed to get AI rules service: {}", e)))?; - let agent_registry = agents::get_agent_registry(); let mcp_service = match mcp::MCPService::new(config_service.clone()) { @@ -201,12 +191,6 @@ impl AppState { .as_ref() .map(|workspace| workspace.root_path.clone()); - if let Some(workspace_path) = initial_workspace_path.clone() { - if let Err(e) = ai_rules_service.set_workspace(workspace_path).await { - log::warn!("Failed to restore AI rules workspace on startup: {}", 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(".")) @@ -296,7 +280,6 @@ impl AppState { config_service, filesystem_service, workspace_search_service, - ai_rules_service, agent_registry, mcp_service, acp_client_service, diff --git a/src/apps/desktop/src/api/commands.rs b/src/apps/desktop/src/api/commands.rs index 21dcb9b28..7fd1bbea2 100644 --- a/src/apps/desktop/src/api/commands.rs +++ b/src/apps/desktop/src/api/commands.rs @@ -648,7 +648,6 @@ async fn clear_active_workspace_context(state: &State<'_, AppState>, app: &AppHa pool.stop_all().await; } - state.ai_rules_service.clear_workspace().await; state.agent_registry.clear_custom_subagents(); #[cfg(target_os = "macos")] @@ -680,21 +679,6 @@ async fn apply_active_workspace_context( *state.workspace_path.write().await = Some(workspace_info.root_path.clone()); - // 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. - if let Err(e) = state - .ai_rules_service - .set_workspace(workspace_info.root_path.clone()) - .await - { - warn!( - "Failed to set AI rules workspace: path={}, error={}", - workspace_info.root_path.display(), - e - ); - } - spawn_workspace_background_warmup(&*state, workspace_info.clone()); #[cfg(target_os = "macos")] diff --git a/src/apps/desktop/src/api/mod.rs b/src/apps/desktop/src/api/mod.rs index 5f7f81a83..707305041 100644 --- a/src/apps/desktop/src/api/mod.rs +++ b/src/apps/desktop/src/api/mod.rs @@ -2,8 +2,6 @@ pub mod acp_client_api; pub mod agentic_api; -pub mod ai_memory_api; -pub mod ai_rules_api; pub mod announcement_api; pub mod app_state; pub mod browser_api; diff --git a/src/apps/desktop/src/lib.rs b/src/apps/desktop/src/lib.rs index a88ca8f8e..e862b94ec 100644 --- a/src/apps/desktop/src/lib.rs +++ b/src/apps/desktop/src/lib.rs @@ -29,7 +29,6 @@ use tauri::Manager; pub use api::*; use api::acp_client_api::*; -use api::ai_rules_api::*; use api::clipboard_file_api::*; use api::commands::*; use api::computer_use_api::*; @@ -487,39 +486,41 @@ pub async fn run() { }) .on_window_event({ move |window, event| { - #[cfg(target_os = "macos")] - if let tauri::WindowEvent::CloseRequested { api, .. } = event { + if let tauri::WindowEvent::CloseRequested { api: _api, .. } = event { if window.label() == "main" { - api.prevent_close(); - if !begin_main_window_close_request_on_macos() { - return; - } - - if let Err(error) = window.emit(MAIN_WINDOW_CLOSE_REQUESTED_EVENT, ()) { - log::warn!( - "Failed to emit macOS main window close request event: {}", - error - ); - } + #[cfg(target_os = "macos")] + { + _api.prevent_close(); + if !begin_main_window_close_request_on_macos() { + return; + } - let app_handle = window.app_handle().clone(); - tauri::async_runtime::spawn(async move { - tokio::time::sleep(std::time::Duration::from_millis( - MAIN_WINDOW_CLOSE_FALLBACK_HIDE_MS, - )) - .await; + if let Err(error) = window.emit(MAIN_WINDOW_CLOSE_REQUESTED_EVENT, ()) { + log::warn!( + "Failed to emit macOS main window close request event: {}", + error + ); + } - if take_main_window_close_request_on_macos() { - if let Err(error) = - hide_main_window_on_macos(&app_handle, "frontend_timeout") - { - log::warn!( - "macOS close fallback hide failed after frontend timeout: {}", - error - ); + let app_handle = window.app_handle().clone(); + tauri::async_runtime::spawn(async move { + tokio::time::sleep(std::time::Duration::from_millis( + MAIN_WINDOW_CLOSE_FALLBACK_HIDE_MS, + )) + .await; + + if take_main_window_close_request_on_macos() { + if let Err(error) = + hide_main_window_on_macos(&app_handle, "frontend_timeout") + { + log::warn!( + "macOS close fallback hide failed after frontend timeout: {}", + error + ); + } } - } - }); + }); + } } } @@ -729,15 +730,6 @@ pub async fn run() { cleanup_storage_with_policy, get_storage_statistics, initialize_project_storage, - get_ai_rules, - get_ai_rule, - create_ai_rule, - update_ai_rule, - delete_ai_rule, - get_ai_rules_stats, - build_ai_rules_system_prompt, - reload_ai_rules, - toggle_ai_rule, // Session persistence API list_persisted_sessions, load_session_turns, @@ -749,12 +741,6 @@ pub async fn run() { touch_session_activity, load_persisted_session_metadata, fork_session, - // AI Memory API - api::ai_memory_api::get_all_memories, - api::ai_memory_api::add_memory, - api::ai_memory_api::update_memory, - api::ai_memory_api::delete_memory, - api::ai_memory_api::toggle_memory, api::project_context_api::get_document_statuses, api::project_context_api::toggle_document_enabled, api::project_context_api::create_context_document, @@ -1002,28 +988,23 @@ pub async fn run() { match app { Ok(app) => { - app.run(|app_handle, event| { - #[cfg(not(target_os = "macos"))] - let _ = app_handle; - - match event { - tauri::RunEvent::ExitRequested { .. } | tauri::RunEvent::Exit => { - perform_process_exit_cleanup(); - } - #[cfg(target_os = "macos")] - tauri::RunEvent::Reopen { - has_visible_windows, - .. - } => { - let reason = if has_visible_windows { - "dock_reopen_with_visible_aux_window" - } else { - "dock_reopen_no_visible_windows" - }; - show_main_window_on_macos(app_handle, reason); - } - _ => {} + app.run(|_app_handle, event| match event { + tauri::RunEvent::ExitRequested { .. } | tauri::RunEvent::Exit => { + perform_process_exit_cleanup(); + } + #[cfg(target_os = "macos")] + tauri::RunEvent::Reopen { + has_visible_windows, + .. + } => { + let reason = if has_visible_windows { + "dock_reopen_with_visible_aux_window" + } else { + "dock_reopen_no_visible_windows" + }; + show_main_window_on_macos(_app_handle, reason); } + _ => {} }); } Err(e) => { diff --git a/src/apps/server/src/bootstrap.rs b/src/apps/server/src/bootstrap.rs index 278afe0bf..d7923a606 100644 --- a/src/apps/server/src/bootstrap.rs +++ b/src/apps/server/src/bootstrap.rs @@ -5,7 +5,7 @@ use bitfun_core::agentic::*; use bitfun_core::infrastructure::ai::AIClientFactory; use bitfun_core::infrastructure::try_get_path_manager_arc; -use bitfun_core::service::{ai_rules, config, filesystem, mcp, token_usage, workspace}; +use bitfun_core::service::{config, filesystem, mcp, token_usage, workspace}; use std::sync::Arc; use tokio::sync::RwLock; @@ -16,7 +16,6 @@ pub struct ServerAppState { pub workspace_path: Arc>>, pub config_service: Arc, pub filesystem_service: Arc, - pub ai_rules_service: Arc, pub agent_registry: Arc, pub mcp_service: Option>, pub token_usage_service: Arc, @@ -142,9 +141,6 @@ pub async fn initialize(workspace: Option) -> anyhow::Result) -> anyhow::Result { @@ -213,7 +205,6 @@ pub async fn initialize(workspace: Option) -> anyhow::Result Option { - let path_manager = match try_get_path_manager_arc() { - Ok(pm) => pm, - Err(e) => { - warn!("Failed to create PathManager: {}", e); - return None; - } - }; - - let memory_manager = match AIMemoryManager::new(path_manager).await { - Ok(mm) => mm, - Err(e) => { - warn!("Failed to create AIMemoryManager: {}", e); - return None; - } - }; - - match memory_manager.get_memories_for_prompt().await { - Ok(Some(prompt)) => Some(prompt), - Ok(None) => None, - Err(e) => { - warn!("Failed to load memories: {}", e); - None - } - } - } - pub async fn build_request_context_reminder( &self, policy: &RequestContextPolicy, @@ -249,18 +218,6 @@ impl PromptBuilder { } } - if policy.includes(RequestContextSection::AIRules) { - if let Some(rules_prompt) = self.load_ai_rules().await { - override_sections.push(rules_prompt); - } - } - - if policy.includes(RequestContextSection::AIMemories) { - if let Some(memory_prompt) = self.load_ai_memories().await { - override_sections.push(memory_prompt); - } - } - if policy.includes(RequestContextSection::ProjectLayout) { trailing_sections.push(self.get_project_layout()); } @@ -281,35 +238,6 @@ impl PromptBuilder { } } - /// Load AI rules from disk and format as prompt - pub async fn load_ai_rules(&self) -> Option { - let rules_service = match get_global_ai_rules_service().await { - Ok(service) => service, - Err(e) => { - warn!("Failed to get AIRulesService: {}", e); - return None; - } - }; - - let workspace_pathbuf = std::path::PathBuf::from(&self.context.workspace_path); - match rules_service - .build_system_prompt_for(Some(&workspace_pathbuf)) - .await - { - Ok(prompt) => { - if prompt.is_empty() { - None - } else { - Some(prompt) - } - } - Err(e) => { - warn!("Failed to build AI rules system prompt: {}", e); - None - } - } - } - /// Get visual mode instruction from user config /// /// Reads `app.ai_experience.enable_visual_mode` from global config. diff --git a/src/crates/core/src/agentic/agents/prompt_builder/request_context.rs b/src/crates/core/src/agentic/agents/prompt_builder/request_context.rs index 59dc037ec..4ad1ad792 100644 --- a/src/crates/core/src/agentic/agents/prompt_builder/request_context.rs +++ b/src/crates/core/src/agentic/agents/prompt_builder/request_context.rs @@ -2,8 +2,6 @@ pub enum RequestContextSection { WorkspaceInstructions, WorkspaceMemoryFiles, - AIRules, - AIMemories, ProjectLayout, } @@ -21,8 +19,6 @@ impl RequestContextPolicy { Self::new(vec![ RequestContextSection::WorkspaceInstructions, RequestContextSection::WorkspaceMemoryFiles, - RequestContextSection::AIRules, - RequestContextSection::AIMemories, RequestContextSection::ProjectLayout, ]) } @@ -31,8 +27,6 @@ impl RequestContextPolicy { Self::new(vec![ RequestContextSection::WorkspaceInstructions, RequestContextSection::WorkspaceMemoryFiles, - RequestContextSection::AIRules, - RequestContextSection::AIMemories, ]) } @@ -53,8 +47,6 @@ impl RequestContextPolicy { pub fn has_override_sections(&self) -> bool { self.includes(RequestContextSection::WorkspaceMemoryFiles) - || self.includes(RequestContextSection::AIRules) - || self.includes(RequestContextSection::AIMemories) } } diff --git a/src/crates/core/src/agentic/tools/implementations/file_read_tool.rs b/src/crates/core/src/agentic/tools/implementations/file_read_tool.rs index 7672eaafd..fcd81091c 100644 --- a/src/crates/core/src/agentic/tools/implementations/file_read_tool.rs +++ b/src/crates/core/src/agentic/tools/implementations/file_read_tool.rs @@ -2,10 +2,8 @@ use crate::agentic::tools::framework::{ Tool, ToolRenderOptions, ToolResult, ToolUseContext, ValidationResult, }; use crate::agentic::tools::workspace_paths::is_bitfun_runtime_uri; -use crate::service::ai_rules::get_global_ai_rules_service; use crate::util::errors::{BitFunError, BitFunResult}; use async_trait::async_trait; -use log::debug; use serde_json::{json, Value}; use std::path::Path; use tool_runtime::fs::read_file::read_file; @@ -364,31 +362,6 @@ Usage: .map_err(BitFunError::tool)? }; - let file_rules = if resolved.is_runtime_artifact() { - crate::service::ai_rules::FileRulesResult { - matched_count: 0, - formatted_content: None, - } - } else { - match get_global_ai_rules_service().await { - Ok(rules_service) => { - rules_service - .get_rules_for_file_with_workspace( - &resolved.resolved_path, - context.workspace_root(), - ) - .await - } - Err(e) => { - debug!("Failed to get AIRulesService: {}", e); - crate::service::ai_rules::FileRulesResult { - matched_count: 0, - formatted_content: None, - } - } - } - }; - let mut result_for_assistant = format!( "Read lines {}-{} from {} ({} total lines)\n\n{}\n", read_file_result.start_line, @@ -398,11 +371,6 @@ Usage: read_file_result.content ); - if let Some(rules_content) = &file_rules.formatted_content { - result_for_assistant.push_str("\n\n"); - result_for_assistant.push_str(rules_content); - } - let has_more = read_file_result.end_line < read_file_result.total_lines; if has_more { let next_start = read_file_result.end_line + 1; @@ -432,8 +400,7 @@ Usage: "lines_read": lines_read, "start_line": read_file_result.start_line, "size": read_file_result.content.len(), - "hit_total_char_limit": read_file_result.hit_total_char_limit, - "matched_rules_count": file_rules.matched_count + "hit_total_char_limit": read_file_result.hit_total_char_limit }), result_for_assistant: Some(result_for_assistant), image_attachments: None, diff --git a/src/crates/core/src/infrastructure/app_paths/path_manager.rs b/src/crates/core/src/infrastructure/app_paths/path_manager.rs index c26fcdb57..d6e07631a 100644 --- a/src/crates/core/src/infrastructure/app_paths/path_manager.rs +++ b/src/crates/core/src/infrastructure/app_paths/path_manager.rs @@ -334,12 +334,6 @@ impl PathManager { self.project_runtime_root(workspace_path).join("memory") } - /// Get project AI memories file: ~/.bitfun/projects//ai_memories.json - pub fn project_ai_memories_file(&self, workspace_path: &Path) -> PathBuf { - self.project_runtime_root(workspace_path) - .join("ai_memories.json") - } - fn project_runtime_slug(&self, workspace_path: &Path) -> String { let requested_path = workspace_path.to_path_buf(); if let Some(slug) = self.cached_project_runtime_slug(&requested_path) { diff --git a/src/crates/core/src/service/ai_memory/manager.rs b/src/crates/core/src/service/ai_memory/manager.rs deleted file mode 100644 index b5e5ad604..000000000 --- a/src/crates/core/src/service/ai_memory/manager.rs +++ /dev/null @@ -1,214 +0,0 @@ -//! AI memory point manager - -use super::types::{AIMemory, MemoryStorage, MemoryType}; -use crate::infrastructure::PathManager; -use crate::util::errors::{BitFunError, BitFunResult}; -use log::debug; -use std::path::PathBuf; -use std::sync::Arc; -use tokio::fs; -use tokio::sync::RwLock; - -/// AI memory point manager -pub struct AIMemoryManager { - /// Path manager - #[allow(dead_code)] - path_manager: Arc, - /// In-memory cache - storage: Arc>, - /// Storage file path - storage_path: PathBuf, -} - -impl AIMemoryManager { - /// Creates a new memory manager (user-level). - pub async fn new(path_manager: Arc) -> BitFunResult { - let storage_path = path_manager.user_data_dir().join("ai_memories.json"); - - if let Some(parent) = storage_path.parent() { - fs::create_dir_all(parent).await.map_err(|e| { - BitFunError::io(format!("Failed to create memory storage directory: {}", e)) - })?; - } - - let storage = if storage_path.exists() { - Self::load_storage(&storage_path).await? - } else { - MemoryStorage::new() - }; - - Ok(Self { - path_manager, - storage: Arc::new(RwLock::new(storage)), - storage_path, - }) - } - - /// Creates a new memory manager (project-level). - pub async fn new_project( - path_manager: Arc, - workspace_path: &str, - ) -> BitFunResult { - let workspace_path = PathBuf::from(workspace_path); - let storage_path = path_manager.project_ai_memories_file(&workspace_path); - - if let Some(parent) = storage_path.parent() { - fs::create_dir_all(parent).await.map_err(|e| { - BitFunError::io(format!("Failed to create memory storage directory: {}", e)) - })?; - } - - let storage = if storage_path.exists() { - Self::load_storage(&storage_path).await? - } else { - MemoryStorage::new() - }; - - Ok(Self { - path_manager, - storage: Arc::new(RwLock::new(storage)), - storage_path, - }) - } - - /// Loads storage from disk. - async fn load_storage(path: &PathBuf) -> BitFunResult { - let content = fs::read_to_string(path) - .await - .map_err(|e| BitFunError::io(format!("Failed to read memory storage file: {}", e)))?; - - let storage: MemoryStorage = serde_json::from_str(&content).map_err(|e| { - BitFunError::Deserialization(format!("Failed to deserialize memory storage: {}", e)) - })?; - - debug!("Loaded {} memory points from disk", storage.memories.len()); - Ok(storage) - } - - /// Saves storage to disk. - async fn save_storage(&self) -> BitFunResult<()> { - let storage = self.storage.read().await; - let content = serde_json::to_string_pretty(&*storage).map_err(|e| { - BitFunError::serialization(format!("Failed to serialize memory storage: {}", e)) - })?; - - fs::write(&self.storage_path, content) - .await - .map_err(|e| BitFunError::io(format!("Failed to write memory storage file: {}", e)))?; - - debug!( - "Memory points saved to disk: {}", - self.storage_path.display() - ); - Ok(()) - } - - /// Returns the storage path (for debugging and logging). - pub fn get_storage_path(&self) -> &PathBuf { - &self.storage_path - } - - /// Adds a memory point. - pub async fn add_memory(&self, memory: AIMemory) -> BitFunResult { - let mut storage = self.storage.write().await; - let memory_clone = memory.clone(); - storage.add_memory(memory); - drop(storage); - - self.save_storage().await?; - Ok(memory_clone) - } - - /// Deletes a memory point. - pub async fn delete_memory(&self, id: &str) -> BitFunResult { - let mut storage = self.storage.write().await; - let removed = storage.remove_memory(id); - drop(storage); - - if removed { - self.save_storage().await?; - } - Ok(removed) - } - - /// Updates a memory point. - pub async fn update_memory(&self, memory: AIMemory) -> BitFunResult { - let mut storage = self.storage.write().await; - let updated = storage.update_memory(memory); - drop(storage); - - if updated { - self.save_storage().await?; - } - Ok(updated) - } - - /// Returns all memory points. - pub async fn get_all_memories(&self) -> BitFunResult> { - let storage = self.storage.read().await; - Ok(storage.memories.clone()) - } - - /// Returns enabled memory points. - pub async fn get_enabled_memories(&self) -> BitFunResult> { - let storage = self.storage.read().await; - Ok(storage - .get_enabled_memories() - .into_iter() - .cloned() - .collect()) - } - - /// Gets memory points for prompt assembly. - /// Returns a formatted string that can be appended to the prompt directly. - pub async fn get_memories_for_prompt(&self) -> BitFunResult> { - let memories = self.get_enabled_memories().await?; - - if memories.is_empty() { - return Ok(None); - } - - let mut sorted_memories = memories; - sorted_memories.sort_by(|a, b| b.importance.cmp(&a.importance)); - - let mut prompt = String::from("# Memory Points\n"); - prompt.push_str("The following are important memory points set by the user, consider these information in the conversation\n\n"); - - for memory in sorted_memories.iter() { - let type_label = match memory.memory_type { - MemoryType::TechPreference => "Technology Preference", - MemoryType::ProjectContext => "Project Context", - MemoryType::UserHabit => "User Habit", - MemoryType::CodePattern => "Code Pattern", - MemoryType::Decision => "Architecture Decision", - MemoryType::Other => "Others", - }; - - prompt.push_str(&format!( - "## {} [{}] (Importance: {}/5)\n{}\n", - memory.title, type_label, memory.importance, memory.content - )); - prompt.push('\n'); - } - prompt.push('\n'); - - Ok(Some(prompt)) - } - - /// Toggles whether a memory point is enabled. - pub async fn toggle_memory(&self, id: &str) -> BitFunResult { - let mut storage = self.storage.write().await; - - if let Some(memory) = storage.memories.iter_mut().find(|m| m.id == id) { - memory.enabled = !memory.enabled; - memory.updated_at = chrono::Utc::now().to_rfc3339(); - let new_state = memory.enabled; - drop(storage); - - self.save_storage().await?; - Ok(new_state) - } else { - Ok(false) - } - } -} diff --git a/src/crates/core/src/service/ai_memory/mod.rs b/src/crates/core/src/service/ai_memory/mod.rs deleted file mode 100644 index ce6c831ad..000000000 --- a/src/crates/core/src/service/ai_memory/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -//! AI memory point management module - -pub mod manager; -pub mod types; - -pub use manager::AIMemoryManager; -pub use types::{AIMemory, MemoryStorage, MemoryType}; diff --git a/src/crates/core/src/service/ai_memory/types.rs b/src/crates/core/src/service/ai_memory/types.rs deleted file mode 100644 index d16a392cf..000000000 --- a/src/crates/core/src/service/ai_memory/types.rs +++ /dev/null @@ -1,131 +0,0 @@ -//! AI memory point type definitions - -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; - -/// Memory type -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] -#[serde(rename_all = "snake_case")] -#[derive(Default)] -pub enum MemoryType { - /// Technology preference - TechPreference, - /// Project context - ProjectContext, - /// User habit - UserHabit, - /// Code pattern - CodePattern, - /// Architecture decision - Decision, - /// Other - #[default] - Other, -} - -/// AI memory point -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AIMemory { - /// Unique identifier - pub id: String, - /// Title - pub title: String, - /// Content - pub content: String, - /// Type - #[serde(rename = "type")] - pub memory_type: MemoryType, - /// Tags - pub tags: Vec, - /// Source - pub source: String, - /// Created time (ISO 8601 format) - pub created_at: String, - /// Updated time (ISO 8601 format) - pub updated_at: String, - /// Importance 1-5 - pub importance: u8, - /// Whether enabled - pub enabled: bool, -} - -impl AIMemory { - /// Creates a new memory point. - pub fn new(title: String, content: String, memory_type: MemoryType, importance: u8) -> Self { - let now = chrono::Utc::now().to_rfc3339(); - Self { - id: uuid::Uuid::new_v4().to_string(), - title, - content, - memory_type, - tags: vec![], - source: "User manually added".to_string(), - created_at: now.clone(), - updated_at: now, - importance: importance.min(5), - enabled: true, - } - } -} - -/// Memory storage -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct MemoryStorage { - /// All memory points - pub memories: Vec, - /// Metadata - pub metadata: HashMap, -} - -impl MemoryStorage { - /// Creates a new storage. - pub fn new() -> Self { - Self { - memories: vec![], - metadata: HashMap::new(), - } - } - - /// Adds a memory point. - pub fn add_memory(&mut self, memory: AIMemory) { - self.memories.push(memory); - self.update_metadata(); - } - - /// Removes a memory point. - pub fn remove_memory(&mut self, id: &str) -> bool { - let len_before = self.memories.len(); - self.memories.retain(|m| m.id != id); - let removed = self.memories.len() != len_before; - if removed { - self.update_metadata(); - } - removed - } - - /// Updates a memory point. - pub fn update_memory(&mut self, memory: AIMemory) -> bool { - if let Some(pos) = self.memories.iter().position(|m| m.id == memory.id) { - let mut updated = memory; - updated.updated_at = chrono::Utc::now().to_rfc3339(); - self.memories[pos] = updated; - self.update_metadata(); - true - } else { - false - } - } - - /// Returns enabled memory points. - pub fn get_enabled_memories(&self) -> Vec<&AIMemory> { - self.memories.iter().filter(|m| m.enabled).collect() - } - - /// Updates metadata. - fn update_metadata(&mut self) { - self.metadata - .insert("updated_at".to_string(), chrono::Utc::now().to_rfc3339()); - self.metadata - .insert("count".to_string(), self.memories.len().to_string()); - } -} diff --git a/src/crates/core/src/service/ai_rules/mod.rs b/src/crates/core/src/service/ai_rules/mod.rs deleted file mode 100644 index cbbda6d23..000000000 --- a/src/crates/core/src/service/ai_rules/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ -//! AI rules management service -//! -//! Provides management of project-level and user-level AI rules. -//! Supports creating, reading, updating, and deleting rules. - -pub mod service; -pub mod types; - -pub use service::{ - get_global_ai_rules_service, initialize_global_ai_rules_service, - is_global_ai_rules_service_initialized, AIRulesService, FileRulesResult, -}; -pub use types::*; diff --git a/src/crates/core/src/service/ai_rules/service.rs b/src/crates/core/src/service/ai_rules/service.rs deleted file mode 100644 index e7912a23d..000000000 --- a/src/crates/core/src/service/ai_rules/service.rs +++ /dev/null @@ -1,942 +0,0 @@ -//! AI rules management service implementation -//! -//! Rule management based on the `.mdc` file format. - -use super::types::*; -use crate::infrastructure::{try_get_path_manager_arc, PathManager}; -use crate::util::errors::*; -use globset::{Glob, GlobSetBuilder}; -use log::{debug, info, warn}; -use std::path::{Path, PathBuf}; -use std::sync::Arc; -use std::sync::OnceLock; -use tokio::sync::RwLock; - -/// Global `AIRulesService` singleton container. -static GLOBAL_AI_RULES_SERVICE: OnceLock>>>> = - OnceLock::new(); - -/// Initializes the global `AIRulesService` singleton. -/// Must be called before use, typically during application startup. -pub async fn initialize_global_ai_rules_service() -> BitFunResult<()> { - if is_global_ai_rules_service_initialized() { - debug!("Global AIRulesService already initialized, skipping"); - return Ok(()); - } - - info!("Initializing global AIRulesService"); - - let path_manager = try_get_path_manager_arc() - .map_err(|e| BitFunError::service(format!("Failed to create PathManager: {}", e)))?; - - let service = AIRulesService::new(path_manager).await?; - let wrapper = Arc::new(RwLock::new(Some(Arc::new(service)))); - - GLOBAL_AI_RULES_SERVICE.set(wrapper).map_err(|_| { - BitFunError::service("Failed to initialize global AIRulesService".to_string()) - })?; - - info!("Global AIRulesService initialized successfully"); - Ok(()) -} - -/// Gets the global `AIRulesService` singleton. -pub async fn get_global_ai_rules_service() -> BitFunResult> { - let wrapper = GLOBAL_AI_RULES_SERVICE.get() - .ok_or_else(|| BitFunError::service( - "Global AIRulesService not initialized. Call initialize_global_ai_rules_service() first.".to_string() - ))?; - - let guard = wrapper.read().await; - guard - .as_ref() - .ok_or_else(|| BitFunError::service("Global AIRulesService is None".to_string())) - .map(Arc::clone) -} - -/// Returns whether the global singleton has been initialized. -pub fn is_global_ai_rules_service_initialized() -> bool { - GLOBAL_AI_RULES_SERVICE - .get() - .map(|w| { - if let Ok(guard) = w.try_read() { - guard.is_some() - } else { - false - } - }) - .unwrap_or(false) -} - -/// File rule match result -#[derive(Debug, Clone)] -pub struct FileRulesResult { - /// Number of matched rules - pub matched_count: usize, - /// Formatted rule content (for appending to file read results) - pub formatted_content: Option, -} - -/// AI rules management service -pub struct AIRulesService { - /// Path manager - path_manager: Arc, - - /// User-level rule cache - user_rules: Arc>>, - - /// Project-level rule cache - project_rules: Arc>>, - - /// Current workspace path - workspace_path: Arc>>, -} - -impl AIRulesService { - /// Creates a new rules service. - pub async fn new(path_manager: Arc) -> BitFunResult { - let service = Self { - path_manager, - user_rules: Arc::new(RwLock::new(Vec::new())), - project_rules: Arc::new(RwLock::new(Vec::new())), - workspace_path: Arc::new(RwLock::new(None)), - }; - - service.reload_user_rules().await?; - - Ok(service) - } - - /// Sets the workspace path and loads project-level rules. - pub async fn set_workspace(&self, workspace_path: PathBuf) -> BitFunResult<()> { - *self.workspace_path.write().await = Some(workspace_path); - self.reload_project_rules().await?; - Ok(()) - } - - /// Clears the workspace. - pub async fn clear_workspace(&self) { - *self.workspace_path.write().await = None; - self.project_rules.write().await.clear(); - } - - /// Returns all user-level rules. - pub async fn get_user_rules(&self) -> BitFunResult> { - Ok(self.user_rules.read().await.clone()) - } - - /// Returns a single user-level rule. - pub async fn get_user_rule(&self, name: &str) -> BitFunResult> { - let rules = self.user_rules.read().await; - Ok(rules.iter().find(|r| r.name == name).cloned()) - } - - /// Creates a user-level rule. - pub async fn create_user_rule(&self, request: CreateRuleRequest) -> BitFunResult { - let rules_dir = self.path_manager.user_rules_dir(); - let rule_name = request.name.clone(); - self.create_rule_internal(&rules_dir, RuleLevel::User, request) - .await?; - self.reload_user_rules().await?; - - self.get_user_rule(&rule_name) - .await? - .ok_or_else(|| BitFunError::service("Failed to create rule".to_string())) - } - - /// Updates a user-level rule. - pub async fn update_user_rule( - &self, - name: &str, - request: UpdateRuleRequest, - ) -> BitFunResult { - let rules_dir = self.path_manager.user_rules_dir(); - self.update_rule_internal(&rules_dir, name, request.clone()) - .await?; - self.reload_user_rules().await?; - - let new_name = request.name.as_deref().unwrap_or(name); - self.get_user_rule(new_name) - .await? - .ok_or_else(|| BitFunError::service("Failed to update rule".to_string())) - } - - /// Deletes a user-level rule. - pub async fn delete_user_rule(&self, name: &str) -> BitFunResult { - let rules_dir = self.path_manager.user_rules_dir(); - let result = self.delete_rule_internal(&rules_dir, name).await?; - self.reload_user_rules().await?; - Ok(result) - } - - /// Reloads user-level rules. - pub async fn reload_user_rules(&self) -> BitFunResult<()> { - let rules_dir = self.path_manager.user_rules_dir(); - let rules = self - .load_rules_from_dir(&rules_dir, RuleLevel::User) - .await?; - *self.user_rules.write().await = rules; - Ok(()) - } - - /// Returns user-level rule statistics. - pub async fn get_user_rules_stats(&self) -> BitFunResult { - let rules = self.user_rules.read().await; - Ok(Self::calculate_stats(&rules)) - } - - /// Returns all project-level rules. - pub async fn get_project_rules(&self) -> BitFunResult> { - Ok(self.project_rules.read().await.clone()) - } - - /// Returns all project-level rules for the specified workspace. - pub async fn get_project_rules_for_workspace( - &self, - workspace: &Path, - ) -> BitFunResult> { - self.load_project_rules_for_workspace(workspace).await - } - - /// Returns a single project-level rule. - pub async fn get_project_rule(&self, name: &str) -> BitFunResult> { - let rules = self.project_rules.read().await; - Ok(rules.iter().find(|r| r.name == name).cloned()) - } - - /// Returns a single project-level rule for the specified workspace. - pub async fn get_project_rule_for_workspace( - &self, - workspace: &Path, - name: &str, - ) -> BitFunResult> { - let rules = self.load_project_rules_for_workspace(workspace).await?; - Ok(rules.into_iter().find(|r| r.name == name)) - } - - /// Creates a project-level rule. - pub async fn create_project_rule(&self, request: CreateRuleRequest) -> BitFunResult { - let workspace_path = self.require_workspace_path().await?; - self.create_project_rule_for_workspace(&workspace_path, request) - .await - } - - /// Creates a project-level rule for the specified workspace. - pub async fn create_project_rule_for_workspace( - &self, - workspace: &Path, - request: CreateRuleRequest, - ) -> BitFunResult { - let rules_dir = self.path_manager.project_rules_dir(workspace); - let rule_name = request.name.clone(); - self.create_rule_internal(&rules_dir, RuleLevel::Project, request) - .await?; - let rules = self.load_project_rules_for_workspace(workspace).await?; - self.sync_project_rules_cache_if_current(workspace, &rules) - .await; - - rules - .into_iter() - .find(|rule| rule.name == rule_name) - .ok_or_else(|| BitFunError::service("Failed to create rule".to_string())) - } - - /// Updates a project-level rule. - /// Supports rules from both the BitFun and Cursor directories. - pub async fn update_project_rule( - &self, - name: &str, - request: UpdateRuleRequest, - ) -> BitFunResult { - let workspace_path = self.require_workspace_path().await?; - self.update_project_rule_for_workspace(&workspace_path, name, request) - .await - } - - /// Updates a project-level rule for the specified workspace. - pub async fn update_project_rule_for_workspace( - &self, - workspace: &Path, - name: &str, - request: UpdateRuleRequest, - ) -> BitFunResult { - let rule = self - .get_project_rule_for_workspace(workspace, name) - .await? - .ok_or_else(|| BitFunError::service(format!("Rule '{}' not found", name)))?; - - let rules_dir = rule - .file_path - .parent() - .ok_or_else(|| BitFunError::service("Invalid rule file path".to_string()))?; - - self.update_rule_internal(rules_dir, name, request.clone()) - .await?; - let rules = self.load_project_rules_for_workspace(workspace).await?; - self.sync_project_rules_cache_if_current(workspace, &rules) - .await; - - let new_name = request.name.as_deref().unwrap_or(name); - rules - .into_iter() - .find(|rule| rule.name == new_name) - .ok_or_else(|| BitFunError::service("Failed to update rule".to_string())) - } - - /// Deletes a project-level rule. - /// Supports rules from both the BitFun and Cursor directories. - pub async fn delete_project_rule(&self, name: &str) -> BitFunResult { - let workspace_path = self.require_workspace_path().await?; - self.delete_project_rule_for_workspace(&workspace_path, name) - .await - } - - /// Deletes a project-level rule for the specified workspace. - /// Supports rules from both the BitFun and Cursor directories. - pub async fn delete_project_rule_for_workspace( - &self, - workspace: &Path, - name: &str, - ) -> BitFunResult { - let rule = self - .get_project_rule_for_workspace(workspace, name) - .await? - .ok_or_else(|| BitFunError::service(format!("Rule '{}' not found", name)))?; - - let rules_dir = rule - .file_path - .parent() - .ok_or_else(|| BitFunError::service("Invalid rule file path".to_string()))?; - - let result = self.delete_rule_internal(rules_dir, name).await?; - let rules = self.load_project_rules_for_workspace(workspace).await?; - self.sync_project_rules_cache_if_current(workspace, &rules) - .await; - Ok(result) - } - - /// Reloads project-level rules. - /// Loads BitFun rules first, then Cursor rules; for duplicates, the first loaded wins. - pub async fn reload_project_rules(&self) -> BitFunResult<()> { - let workspace_path = self.workspace_path.read().await.clone(); - - if let Some(workspace) = workspace_path { - let all_rules = self.load_project_rules_for_workspace(&workspace).await?; - *self.project_rules.write().await = all_rules; - } else { - self.project_rules.write().await.clear(); - } - - Ok(()) - } - - /// Reloads project-level rules for the specified workspace. - pub async fn reload_project_rules_for_workspace(&self, workspace: &Path) -> BitFunResult<()> { - let rules = self.load_project_rules_for_workspace(workspace).await?; - self.sync_project_rules_cache_if_current(workspace, &rules) - .await; - Ok(()) - } - - /// Returns project-level rule statistics. - pub async fn get_project_rules_stats(&self) -> BitFunResult { - let rules = self.project_rules.read().await; - Ok(Self::calculate_stats(&rules)) - } - - /// Returns project-level rule statistics for the specified workspace. - pub async fn get_project_rules_stats_for_workspace( - &self, - workspace: &Path, - ) -> BitFunResult { - let rules = self.load_project_rules_for_workspace(workspace).await?; - Ok(Self::calculate_stats(&rules)) - } - - async fn load_project_rules_for_workspace( - &self, - workspace: &Path, - ) -> BitFunResult> { - let mut all_rules = Vec::new(); - let mut loaded_names = std::collections::HashSet::new(); - - let bitfun_rules_dir = self.path_manager.project_rules_dir(workspace); - let bitfun_rules = self - .load_rules_from_dir(&bitfun_rules_dir, RuleLevel::Project) - .await?; - - for rule in bitfun_rules { - loaded_names.insert(rule.name.clone()); - all_rules.push(rule); - } - - let cursor_rules_dir = workspace.join(".cursor").join("rules"); - if cursor_rules_dir.exists() { - let cursor_rules = self - .load_rules_from_dir(&cursor_rules_dir, RuleLevel::Project) - .await?; - - for rule in cursor_rules { - if !loaded_names.contains(&rule.name) { - loaded_names.insert(rule.name.clone()); - all_rules.push(rule); - } else { - debug!( - "Skipping Cursor rule '{}' (already loaded from BitFun)", - rule.name - ); - } - } - } - - all_rules.sort_by(|a, b| a.name.cmp(&b.name)); - Ok(all_rules) - } - - fn format_system_prompt(&self, user_rules: &[AIRule], project_rules: &[AIRule]) -> String { - if user_rules.is_empty() && project_rules.is_empty() { - return String::new(); - } - - let apply_intelligently_rules: Vec<_> = project_rules - .iter() - .filter(|r| r.enabled && r.apply_type == RuleApplyType::ApplyIntelligently) - .collect(); - - let always_apply_rules: Vec<_> = project_rules - .iter() - .filter(|r| r.enabled && r.apply_type == RuleApplyType::AlwaysApply) - .collect(); - - let enabled_user_rules: Vec<_> = user_rules.iter().filter(|r| r.enabled).collect(); - - if always_apply_rules.is_empty() - && apply_intelligently_rules.is_empty() - && enabled_user_rules.is_empty() - { - return String::new(); - } - - let mut prompt = r#"# Rules - -The rules section has a number of possible rules/memories/context that you should consider. In each subsection, we provide instructions about what information the subsection contains and how you should consider/follow the contents of the subsection. - -"#.to_string(); - - if !apply_intelligently_rules.is_empty() { - prompt.push_str(r#" -"#); - for rule in apply_intelligently_rules { - let description = rule.description.as_deref().unwrap_or(&rule.name); - prompt.push_str(&format!( - "- {}: {}\n", - rule.file_path.display().to_string().replace("\\", "/"), - description - )); - } - prompt.push_str("\n"); - } - - if !always_apply_rules.is_empty() { - prompt.push_str(r#" -"#); - for rule in always_apply_rules { - prompt.push_str(&format!("- {}\n", rule.content)); - } - prompt.push_str("\n"); - } - - if !enabled_user_rules.is_empty() { - prompt.push_str(r#" -"#); - for rule in enabled_user_rules { - prompt.push_str(&format!("- {}\n", rule.content)); - } - prompt.push_str("\n"); - } - - prompt.push_str("\n\n"); - prompt - } - - pub async fn build_system_prompt_for( - &self, - workspace_root: Option<&Path>, - ) -> BitFunResult { - let user_rules = self.user_rules.read().await.clone(); - let project_rules = match workspace_root { - Some(workspace_root) => { - self.load_project_rules_for_workspace(workspace_root) - .await? - } - None => Vec::new(), - }; - - Ok(self.format_system_prompt(&user_rules, &project_rules)) - } - - pub async fn get_rules_for_file_with_workspace( - &self, - file_path: &str, - workspace_root: Option<&Path>, - ) -> FileRulesResult { - let workspace_path = match workspace_root { - Some(path) => path, - None => { - debug!("No workspace path set, skipping file-specific rules"); - return FileRulesResult { - matched_count: 0, - formatted_content: None, - }; - } - }; - - let project_rules = match self.load_project_rules_for_workspace(workspace_path).await { - Ok(rules) => rules, - Err(e) => { - warn!( - "Failed to load project rules for file '{}': {}", - file_path, e - ); - return FileRulesResult { - matched_count: 0, - formatted_content: None, - }; - } - }; - - let file_path_obj = Path::new(file_path); - let relative_path = if file_path_obj.is_absolute() { - file_path_obj - .strip_prefix(workspace_path) - .map(|p| p.to_string_lossy().to_string()) - .unwrap_or_else(|_| file_path.to_string()) - } else { - file_path.to_string() - }; - - let relative_path = relative_path.replace("\\", "/"); - let file_name = Path::new(&relative_path) - .file_name() - .map(|n| n.to_string_lossy().to_string()) - .unwrap_or_default(); - - let mut matching_rules: Vec = Vec::new(); - - for rule in &project_rules { - if rule.apply_type != RuleApplyType::ApplyToSpecificFiles || !rule.enabled { - continue; - } - - if let Some(ref globs_str) = rule.globs { - if self.matches_glob_pattern(globs_str, &relative_path, &file_name) { - matching_rules.push(rule.content.clone()); - debug!( - "Rule '{}' matched for file '{}' (glob: {})", - rule.name, relative_path, globs_str - ); - } - } - } - - if matching_rules.is_empty() { - FileRulesResult { - matched_count: 0, - formatted_content: None, - } - } else { - let mut formatted = String::from("Rules relevant to this file:\n"); - for rule_content in &matching_rules { - formatted.push_str(&format!("\n- {}", rule_content)); - } - - FileRulesResult { - matched_count: matching_rules.len(), - formatted_content: Some(formatted), - } - } - } - - /// Builds the system prompt. - pub async fn build_system_prompt(&self) -> BitFunResult { - let user_rules = self.user_rules.read().await.clone(); - let project_rules = self.project_rules.read().await.clone(); - Ok(self.format_system_prompt(&user_rules, &project_rules)) - } - - /// Gets matching "Apply to Specific Files" rules for a given file path. - /// Returns the matched count and formatted content. - pub async fn get_rules_for_file(&self, file_path: &str) -> FileRulesResult { - let workspace_path = self.workspace_path.read().await.clone(); - self.get_rules_for_file_with_workspace(file_path, workspace_path.as_deref()) - .await - } - - /// Checks whether a file matches the given glob patterns. - fn matches_glob_pattern(&self, globs_str: &str, relative_path: &str, file_name: &str) -> bool { - let patterns: Vec<&str> = globs_str.split(',').map(|s| s.trim()).collect(); - - let mut glob_set_builder = GlobSetBuilder::new(); - let mut valid_patterns = false; - - for pattern in patterns { - if pattern.is_empty() { - continue; - } - - let adjusted_pattern = if !pattern.contains('/') && !pattern.contains('\\') { - format!("**/{}", pattern) - } else { - pattern.to_string() - }; - - match Glob::new(&adjusted_pattern) { - Ok(glob) => { - glob_set_builder.add(glob); - valid_patterns = true; - } - Err(e) => { - warn!("Invalid glob pattern '{}': {}", pattern, e); - } - } - } - - if !valid_patterns { - return false; - } - - match glob_set_builder.build() { - Ok(glob_set) => glob_set.is_match(relative_path) || glob_set.is_match(file_name), - Err(e) => { - warn!("Failed to build glob set: {}", e); - false - } - } - } - - /// Loads all rules from a directory. - async fn load_rules_from_dir(&self, dir: &Path, level: RuleLevel) -> BitFunResult> { - let mut rules = Vec::new(); - - if !dir.exists() { - return Ok(rules); - } - - let mut entries = match tokio::fs::read_dir(dir).await { - Ok(entries) => entries, - Err(e) => { - warn!("Failed to read rules directory {:?}: {}", dir, e); - return Ok(rules); - } - }; - - while let Ok(Some(entry)) = entries.next_entry().await { - let path = entry.path(); - - if path.extension().is_some_and(|ext| ext == "mdc") { - match self.load_rule_from_file(&path, level).await { - Ok(rule) => rules.push(rule), - Err(e) => { - warn!("Failed to load rule from {:?}: {}", path, e); - } - } - } - } - - rules.sort_by(|a, b| a.name.cmp(&b.name)); - - Ok(rules) - } - - /// Loads a single rule from a file. - async fn load_rule_from_file(&self, path: &Path, level: RuleLevel) -> BitFunResult { - let content = tokio::fs::read_to_string(path) - .await - .map_err(|e| BitFunError::service(format!("Failed to read file {:?}: {}", path, e)))?; - - let name = path - .file_stem() - .and_then(|s| s.to_str()) - .map(|s| s.to_string()) - .ok_or_else(|| BitFunError::service("Invalid file name".to_string()))?; - - AIRule::from_mdc(name, level, path.to_path_buf(), &content).map_err(BitFunError::service) - } - - /// Creates a rule file. - async fn create_rule_internal( - &self, - dir: &Path, - level: RuleLevel, - request: CreateRuleRequest, - ) -> BitFunResult<()> { - tokio::fs::create_dir_all(dir) - .await - .map_err(|e| BitFunError::service(format!("Failed to create directory: {}", e)))?; - - let file_path = dir.join(filename_from_rule_name(&request.name)); - - if file_path.exists() { - return Err(BitFunError::service(format!( - "Rule '{}' already exists", - request.name - ))); - } - - let mut frontmatter = match request.apply_type { - RuleApplyType::AlwaysApply => RuleMetadata::always_apply(), - RuleApplyType::ApplyIntelligently => { - let desc = request.description.unwrap_or_else(|| request.name.clone()); - RuleMetadata::apply_intelligently(desc) - } - RuleApplyType::ApplyToSpecificFiles => { - let globs = request.globs.unwrap_or_else(|| "*".to_string()); - RuleMetadata::apply_to_specific_files(globs) - } - RuleApplyType::ApplyManually => RuleMetadata::apply_manually(), - }; - - if level == RuleLevel::User { - frontmatter = RuleMetadata::always_apply(); - } - - frontmatter.enabled = request.enabled; - - let mdc_content = format_mdc_content(&frontmatter, &request.content); - - tokio::fs::write(&file_path, mdc_content) - .await - .map_err(|e| BitFunError::service(format!("Failed to write file: {}", e)))?; - - Ok(()) - } - - /// Updates a rule file. - async fn update_rule_internal( - &self, - dir: &Path, - name: &str, - request: UpdateRuleRequest, - ) -> BitFunResult<()> { - let old_file_path = dir.join(filename_from_rule_name(name)); - - if !old_file_path.exists() { - return Err(BitFunError::service(format!("Rule '{}' not found", name))); - } - - let content = tokio::fs::read_to_string(&old_file_path) - .await - .map_err(|e| BitFunError::service(format!("Failed to read file: {}", e)))?; - - let (mut frontmatter, mut body) = - parse_mdc_content(&content).map_err(BitFunError::service)?; - - if let Some(apply_type) = request.apply_type { - match apply_type { - RuleApplyType::AlwaysApply => { - frontmatter.always_apply = true; - frontmatter.description = None; - frontmatter.globs = None; - } - RuleApplyType::ApplyIntelligently => { - frontmatter.always_apply = false; - frontmatter.description = request.description.or(frontmatter.description); - frontmatter.globs = None; - } - RuleApplyType::ApplyToSpecificFiles => { - frontmatter.always_apply = false; - frontmatter.description = None; - frontmatter.globs = request.globs.or(frontmatter.globs); - } - RuleApplyType::ApplyManually => { - frontmatter.always_apply = false; - frontmatter.description = None; - frontmatter.globs = None; - } - } - } else { - if request.description.is_some() { - frontmatter.description = request.description; - } - if request.globs.is_some() { - frontmatter.globs = request.globs; - } - } - - if let Some(new_content) = request.content { - body = new_content; - } - - if let Some(enabled) = request.enabled { - frontmatter.enabled = enabled; - } - - let mdc_content = format_mdc_content(&frontmatter, &body); - - let new_file_path = if let Some(new_name) = &request.name { - if new_name != name { - let new_path = dir.join(filename_from_rule_name(new_name)); - if new_path.exists() { - return Err(BitFunError::service(format!( - "Rule '{}' already exists", - new_name - ))); - } - tokio::fs::remove_file(&old_file_path).await.map_err(|e| { - BitFunError::service(format!("Failed to delete old file: {}", e)) - })?; - new_path - } else { - old_file_path - } - } else { - old_file_path - }; - - tokio::fs::write(&new_file_path, mdc_content) - .await - .map_err(|e| BitFunError::service(format!("Failed to write file: {}", e)))?; - - Ok(()) - } - - /// Deletes a rule file. - async fn delete_rule_internal(&self, dir: &Path, name: &str) -> BitFunResult { - let file_path = dir.join(filename_from_rule_name(name)); - - if !file_path.exists() { - return Ok(false); - } - - tokio::fs::remove_file(&file_path) - .await - .map_err(|e| BitFunError::service(format!("Failed to delete file: {}", e)))?; - - Ok(true) - } - - /// Calculates statistics. - fn calculate_stats(rules: &[AIRule]) -> RuleStats { - let mut by_apply_type = std::collections::HashMap::new(); - let mut enabled_count = 0; - - for rule in rules { - let type_name = format!("{:?}", rule.apply_type).to_lowercase(); - *by_apply_type.entry(type_name).or_insert(0) += 1; - if rule.enabled { - enabled_count += 1; - } - } - - RuleStats { - total_rules: rules.len(), - enabled_rules: enabled_count, - disabled_rules: rules.len() - enabled_count, - by_apply_type, - } - } - - /// Toggles the enabled state of a user-level rule. - pub async fn toggle_user_rule(&self, name: &str) -> BitFunResult { - let rule = self - .get_user_rule(name) - .await? - .ok_or_else(|| BitFunError::service(format!("Rule '{}' not found", name)))?; - - let new_enabled = !rule.enabled; - self.update_user_rule( - name, - UpdateRuleRequest { - name: None, - apply_type: None, - description: None, - globs: None, - content: None, - enabled: Some(new_enabled), - }, - ) - .await - } - - /// Toggles the enabled state of a project-level rule. - pub async fn toggle_project_rule(&self, name: &str) -> BitFunResult { - let workspace_path = self.require_workspace_path().await?; - self.toggle_project_rule_for_workspace(&workspace_path, name) - .await - } - - /// Toggles the enabled state of a project-level rule for the specified workspace. - pub async fn toggle_project_rule_for_workspace( - &self, - workspace: &Path, - name: &str, - ) -> BitFunResult { - let rule = self - .get_project_rule_for_workspace(workspace, name) - .await? - .ok_or_else(|| BitFunError::service(format!("Rule '{}' not found", name)))?; - - let new_enabled = !rule.enabled; - self.update_project_rule_for_workspace( - workspace, - name, - UpdateRuleRequest { - name: None, - apply_type: None, - description: None, - globs: None, - content: None, - enabled: Some(new_enabled), - }, - ) - .await - } - - async fn require_workspace_path(&self) -> BitFunResult { - self.workspace_path - .read() - .await - .clone() - .ok_or_else(|| BitFunError::service("No workspace set".to_string())) - } - - async fn sync_project_rules_cache_if_current(&self, workspace: &Path, rules: &[AIRule]) { - let current_workspace = self.workspace_path.read().await.clone(); - if current_workspace.as_deref() == Some(workspace) { - *self.project_rules.write().await = rules.to_vec(); - } - } -} - -#[cfg(test)] -mod tests { - use super::AIRulesService; - use crate::infrastructure::PathManager; - use std::sync::Arc; - use uuid::Uuid; - - #[tokio::test] - async fn loading_missing_project_rules_does_not_create_rules_dir() { - let test_root = - std::env::temp_dir().join(format!("bitfun-ai-rules-test-{}", Uuid::new_v4())); - let workspace_root = test_root.join("workspace"); - std::fs::create_dir_all(&workspace_root).expect("test workspace should be created"); - - let path_manager = Arc::new(PathManager::with_user_root_for_tests( - test_root.join("user-root"), - )); - let service = AIRulesService::new(path_manager.clone()) - .await - .expect("ai rules service should initialize"); - - let rules = service - .get_project_rules_for_workspace(&workspace_root) - .await - .expect("loading project rules should succeed"); - - assert!(rules.is_empty()); - assert!(!path_manager.project_rules_dir(&workspace_root).exists()); - - let _ = std::fs::remove_dir_all(&test_root); - } -} diff --git a/src/crates/core/src/service/ai_rules/types.rs b/src/crates/core/src/service/ai_rules/types.rs deleted file mode 100644 index 87f5d4707..000000000 --- a/src/crates/core/src/service/ai_rules/types.rs +++ /dev/null @@ -1,364 +0,0 @@ -//! AI rules type definitions -//! -//! Rule type definitions based on the `.mdc` file format. - -use crate::util::front_matter_markdown::FrontMatterMarkdown; -use serde::{Deserialize, Serialize}; -use serde_yaml::Value; -use std::path::PathBuf; - -/// Rule apply type -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum RuleApplyType { - /// Always apply: `alwaysApply=true`, no `description` and no `globs`. - AlwaysApply, - - /// Apply intelligently: `alwaysApply=false`, has `description`, no `globs`. - ApplyIntelligently, - - /// Apply to specific files: `alwaysApply=false`, has `globs`, no `description`. - ApplyToSpecificFiles, - - /// Apply manually: `alwaysApply=false`, no `description` and no `globs`. - ApplyManually, -} - -impl RuleApplyType { - /// Determines the rule apply type from the frontmatter fields. - pub fn from_frontmatter( - always_apply: bool, - description: &Option, - globs: &Option, - ) -> Self { - if always_apply { - RuleApplyType::AlwaysApply - } else if description.is_some() { - RuleApplyType::ApplyIntelligently - } else if globs.is_some() { - RuleApplyType::ApplyToSpecificFiles - } else { - RuleApplyType::ApplyManually - } - } - - /// Returns the display name. - pub fn display_name(&self) -> &'static str { - match self { - RuleApplyType::AlwaysApply => "Always Apply", - RuleApplyType::ApplyIntelligently => "Apply Intelligently", - RuleApplyType::ApplyToSpecificFiles => "Apply to Specific Files", - RuleApplyType::ApplyManually => "Apply Manually", - } - } -} - -/// MDC file frontmatter -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct RuleMetadata { - /// Rule description (used by `ApplyIntelligently`). - #[serde(skip_serializing_if = "Option::is_none")] - pub description: Option, - - /// Glob patterns (used by `ApplyToSpecificFiles`). - #[serde(skip_serializing_if = "Option::is_none")] - pub globs: Option, - - /// Whether to always apply. - #[serde(rename = "alwaysApply")] - pub always_apply: bool, - - /// Whether enabled (defaults to `true`). - #[serde(default = "default_enabled")] - pub enabled: bool, -} - -fn default_enabled() -> bool { - true -} - -impl RuleMetadata { - /// Creates frontmatter for `AlwaysApply`. - pub fn always_apply() -> Self { - Self { - description: None, - globs: None, - always_apply: true, - enabled: true, - } - } - - /// Creates frontmatter for `ApplyIntelligently`. - pub fn apply_intelligently(description: String) -> Self { - Self { - description: Some(description), - globs: None, - always_apply: false, - enabled: true, - } - } - - /// Creates frontmatter for `ApplyToSpecificFiles`. - pub fn apply_to_specific_files(globs: String) -> Self { - Self { - description: None, - globs: Some(globs), - always_apply: false, - enabled: true, - } - } - - /// Creates frontmatter for `ApplyManually`. - pub fn apply_manually() -> Self { - Self { - description: None, - globs: None, - always_apply: false, - enabled: true, - } - } - - /// Returns the rule apply type. - pub fn apply_type(&self) -> RuleApplyType { - RuleApplyType::from_frontmatter(self.always_apply, &self.description, &self.globs) - } - - /// Creates `RuleMetadata` from `serde_yaml::Value`. - pub fn from_value(value: &Value) -> Result { - let description = value - .get("description") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()); - - let globs = value - .get("globs") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()); - - let always_apply = value - .get("alwaysApply") - .and_then(|v| v.as_bool()) - .unwrap_or(false); - - let enabled = value - .get("enabled") - .and_then(|v| v.as_bool()) - .unwrap_or(true); - - Ok(Self { - description, - globs, - always_apply, - enabled, - }) - } - - /// Converts to `serde_yaml::Value`. - pub fn to_value(&self) -> Value { - let mut map = serde_yaml::Mapping::new(); - - if let Some(ref desc) = self.description { - map.insert( - Value::String("description".to_string()), - Value::String(desc.clone()), - ); - } - - if let Some(ref globs) = self.globs { - map.insert( - Value::String("globs".to_string()), - Value::String(globs.clone()), - ); - } - - map.insert( - Value::String("alwaysApply".to_string()), - Value::Bool(self.always_apply), - ); - - if !self.enabled { - map.insert( - Value::String("enabled".to_string()), - Value::Bool(self.enabled), - ); - } - - Value::Mapping(map) - } -} - -/// Rule level -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum RuleLevel { - /// User-level (global) - User, - /// Project-level (workspace) - Project, -} - -/// AI rule definition -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AIRule { - /// Rule name (file name without the `.mdc` extension). - pub name: String, - - /// Rule level - pub level: RuleLevel, - - /// Rule apply type - pub apply_type: RuleApplyType, - - /// Rule description (used by `ApplyIntelligently`). - #[serde(skip_serializing_if = "Option::is_none")] - pub description: Option, - - /// Glob patterns (used by `ApplyToSpecificFiles`). - #[serde(skip_serializing_if = "Option::is_none")] - pub globs: Option, - - /// Rule content - pub content: String, - - /// Full path to the rule file - pub file_path: PathBuf, - - /// Whether enabled - pub enabled: bool, -} - -impl AIRule { - /// Parses a rule from MDC file content. - pub fn from_mdc( - name: String, - level: RuleLevel, - file_path: PathBuf, - mdc_content: &str, - ) -> Result { - let (frontmatter, content) = parse_mdc_content(mdc_content)?; - - Ok(Self { - name, - level, - apply_type: frontmatter.apply_type(), - description: frontmatter.description, - globs: frontmatter.globs, - content, - file_path, - enabled: frontmatter.enabled, - }) - } - - /// Converts to MDC file content. - pub fn to_mdc(&self) -> String { - let frontmatter = RuleMetadata { - description: self.description.clone(), - globs: self.globs.clone(), - always_apply: self.apply_type == RuleApplyType::AlwaysApply, - enabled: self.enabled, - }; - - format_mdc_content(&frontmatter, &self.content) - } -} - -/// Create rule request -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CreateRuleRequest { - /// Rule name (will be used as the file name). - pub name: String, - - /// Rule apply type - pub apply_type: RuleApplyType, - - /// Rule description (used by `ApplyIntelligently`). - #[serde(skip_serializing_if = "Option::is_none")] - pub description: Option, - - /// Glob patterns (used by `ApplyToSpecificFiles`). - #[serde(skip_serializing_if = "Option::is_none")] - pub globs: Option, - - /// Rule content - pub content: String, - - /// Whether enabled (defaults to `true`). - #[serde(default = "default_enabled")] - pub enabled: bool, -} - -/// Update rule request -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct UpdateRuleRequest { - /// New rule name (if renaming is needed). - #[serde(skip_serializing_if = "Option::is_none")] - pub name: Option, - - /// Rule apply type - #[serde(skip_serializing_if = "Option::is_none")] - pub apply_type: Option, - - /// Rule description - #[serde(skip_serializing_if = "Option::is_none")] - pub description: Option, - - /// Glob patterns - #[serde(skip_serializing_if = "Option::is_none")] - pub globs: Option, - - /// Rule content - #[serde(skip_serializing_if = "Option::is_none")] - pub content: Option, - - /// Whether enabled - #[serde(skip_serializing_if = "Option::is_none")] - pub enabled: Option, -} - -/// Rule statistics -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct RuleStats { - /// Total rule count - pub total_rules: usize, - - /// Enabled rule count - pub enabled_rules: usize, - - /// Disabled rule count - pub disabled_rules: usize, - - /// Counts by apply type - pub by_apply_type: std::collections::HashMap, -} - -// ===== MDC parsing helpers ===== - -/// Parses MDC file content and returns the frontmatter and body. -/// Uses `FrontMatterMarkdown` for parsing. -pub fn parse_mdc_content(content: &str) -> Result<(RuleMetadata, String), String> { - let (metadata, body) = FrontMatterMarkdown::load_str(content)?; - - let frontmatter = RuleMetadata::from_value(&metadata)?; - - Ok((frontmatter, body)) -} - -/// Formats MDC file content. -/// Uses the `FrontMatterMarkdown` format. -pub fn format_mdc_content(frontmatter: &RuleMetadata, content: &str) -> String { - let metadata = frontmatter.to_value(); - let yaml_str = - serde_yaml::to_string(&metadata).unwrap_or_else(|_| "alwaysApply: true\n".to_string()); - - format!("---\n{}---\n\n{}", yaml_str, content.trim_start()) -} - -/// Returns the rule name from the file name (strip the `.mdc` extension). -pub fn rule_name_from_filename(filename: &str) -> String { - filename.trim_end_matches(".mdc").to_string() -} - -/// Builds a file name from the rule name. -pub fn filename_from_rule_name(name: &str) -> String { - format!("{}.mdc", name) -} diff --git a/src/crates/core/src/service/announcement/content/tips/en-US/019_ai_rules.md b/src/crates/core/src/service/announcement/content/tips/en-US/019_ai_rules.md deleted file mode 100644 index 847b7e62f..000000000 --- a/src/crates/core/src/service/announcement/content/tips/en-US/019_ai_rules.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -id: ai_rules -nth_open: 30 -auto_dismiss_secs: 10 ---- - -# AI rules - -Add project conventions to AI rules so the assistant always follows your coding style diff --git a/src/crates/core/src/service/announcement/content/tips/zh-CN/019_ai_rules.md b/src/crates/core/src/service/announcement/content/tips/zh-CN/019_ai_rules.md deleted file mode 100644 index 384370c53..000000000 --- a/src/crates/core/src/service/announcement/content/tips/zh-CN/019_ai_rules.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -id: ai_rules -nth_open: 30 -auto_dismiss_secs: 10 ---- - -# AI 规则定制 - -在 AI 规则中添加项目规范,让 AI 始终遵循你的编码风格 diff --git a/src/crates/core/src/service/announcement/content/tips/zh-TW/019_ai_rules.md b/src/crates/core/src/service/announcement/content/tips/zh-TW/019_ai_rules.md deleted file mode 100644 index 46cc9db8c..000000000 --- a/src/crates/core/src/service/announcement/content/tips/zh-TW/019_ai_rules.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -id: ai_rules -nth_open: 30 -auto_dismiss_secs: 10 ---- - -# AI 規則定製 - -在 AI 規則中添加項目規範,讓 AI 始終遵循你的編碼風格 diff --git a/src/crates/core/src/service/mod.rs b/src/crates/core/src/service/mod.rs index 0633fdd36..9777ddbce 100644 --- a/src/crates/core/src/service/mod.rs +++ b/src/crates/core/src/service/mod.rs @@ -1,10 +1,8 @@ //! Service layer module //! -//! Contains core business logic: Workspace, Config, FileSystem, Git, Agentic, AIRules, MCP. +//! Contains core business logic: Workspace, Config, FileSystem, Git, Agentic, MCP. pub(crate) mod agent_memory; // Agent memory prompt helpers -pub mod ai_memory; // AI memory point management -pub mod ai_rules; // AI rules management pub mod announcement; // Announcement / feature-demo / tips system pub(crate) mod bootstrap; // Workspace persona bootstrap helpers pub mod config; // Config management @@ -33,8 +31,6 @@ pub mod workspace_runtime; // Workspace runtime layout / migration / initializat pub use terminal_core as terminal; // Re-export main components. -pub use ai_memory::{AIMemory, AIMemoryManager, MemoryType}; -pub use ai_rules::AIRulesService; pub use announcement::{AnnouncementCard, AnnouncementScheduler, AnnouncementSchedulerRef}; pub use bootstrap::reset_workspace_persona_files_to_default; pub use config::{ConfigManager, ConfigProvider, ConfigService}; diff --git a/src/crates/core/src/service/workspace_runtime/service.rs b/src/crates/core/src/service/workspace_runtime/service.rs index 0c6cb04dd..8f75508b6 100644 --- a/src/crates/core/src/service/workspace_runtime/service.rs +++ b/src/crates/core/src/service/workspace_runtime/service.rs @@ -306,11 +306,6 @@ impl WorkspaceRuntimeService { target: context.snapshots_dir.clone(), strategy: RuntimeMigrationStrategy::MoveIfTargetMissing, }, - RuntimeMigrationSpec { - source: legacy_project_root.join("ai_memories.json"), - target: context.runtime_root.join("ai_memories.json"), - strategy: RuntimeMigrationStrategy::MoveIfTargetMissing, - }, ] } WorkspaceRuntimeTarget::RemoteWorkspaceMirror { diff --git a/src/crates/core/src/util/process_manager.rs b/src/crates/core/src/util/process_manager.rs index 45508cc4b..1e3bd7360 100644 --- a/src/crates/core/src/util/process_manager.rs +++ b/src/crates/core/src/util/process_manager.rs @@ -116,27 +116,21 @@ pub fn create_tokio_command>(program: S) -> TokioComma cmd } +#[cfg(unix)] pub fn configure_process_group(command: &mut TokioCommand) { - #[cfg(unix)] - { - command.process_group(0); - } - #[cfg(not(unix))] - { - let _ = command; - } + command.process_group(0); } +#[cfg(not(unix))] +pub fn configure_process_group(_command: &mut TokioCommand) {} + +#[cfg(unix)] pub async fn terminate_child_process_tree( child: &mut Child, - #[cfg(unix)] graceful_timeout: Duration, - #[cfg(not(unix))] - _graceful_timeout: Duration, ) -> io::Result<()> { let pid = child.id(); - #[cfg(unix)] if let Some(pid) = pid { let process_group = format!("-{}", pid); let _ = create_tokio_command("kill") @@ -158,7 +152,19 @@ pub async fn terminate_child_process_tree( } } - #[cfg(windows)] + child.start_kill()?; + child.wait().await.map(|_| ()) +} + +#[cfg(windows)] +pub async fn terminate_child_process_tree( + child: &mut Child, + graceful_timeout: Duration, +) -> io::Result<()> { + let pid = child.id(); + + let _ = graceful_timeout; + if let Some(pid) = pid { let _ = create_tokio_command("taskkill") .arg("/PID") diff --git a/src/web-ui/src/app/scenes/settings/SettingsScene.tsx b/src/web-ui/src/app/scenes/settings/SettingsScene.tsx index db4698ead..46d1057a2 100644 --- a/src/web-ui/src/app/scenes/settings/SettingsScene.tsx +++ b/src/web-ui/src/app/scenes/settings/SettingsScene.tsx @@ -14,7 +14,6 @@ import { SessionPersonalizationConfig, SessionPermissionsConfig, } from '../../../infrastructure/config/components/SessionConfig'; -import AIRulesMemoryConfig from '../../../infrastructure/config/components/AIRulesMemoryConfig'; import McpToolsConfig from '../../../infrastructure/config/components/McpToolsConfig'; import AcpAgentsConfig from '../../../infrastructure/config/components/AcpAgentsConfig'; import EditorConfig from '../../../infrastructure/config/components/EditorConfig'; @@ -61,7 +60,6 @@ const SettingsScene: React.FC = () => { case 'session-permissions': Content = SessionPermissionsConfig; break; case 'quick-actions': Content = QuickActionsConfig; break; case 'review': Content = ReviewConfig; break; - case 'ai-context': Content = AIRulesMemoryConfig; break; case 'mcp-tools': Content = McpToolsConfig; break; case 'acp-agents': Content = AcpAgentsConfig; break; case 'editor': Content = EditorConfig; break; diff --git a/src/web-ui/src/app/scenes/settings/settingsConfig.ts b/src/web-ui/src/app/scenes/settings/settingsConfig.ts index a0f975323..47736229c 100644 --- a/src/web-ui/src/app/scenes/settings/settingsConfig.ts +++ b/src/web-ui/src/app/scenes/settings/settingsConfig.ts @@ -13,7 +13,6 @@ export type ConfigTab = | 'session-permissions' | 'quick-actions' | 'review' - | 'ai-context' | 'mcp-tools' | 'acp-agents' // | 'lsp' // temporarily hidden from config center @@ -187,12 +186,6 @@ export const SETTINGS_CATEGORIES: ConfigCategoryDef[] = [ '\u4ee3\u7801\u5ba1\u6838', ], }, - { - id: 'ai-context', - labelKey: 'configCenter.tabs.aiContext', - descriptionKey: 'configCenter.tabDescriptions.aiContext', - keywords: ['rules', 'memory', 'context', 'rag', 'knowledge'], - }, { id: 'mcp-tools', labelKey: 'configCenter.tabs.mcpTools', diff --git a/src/web-ui/src/app/scenes/settings/settingsTabSearchContent.ts b/src/web-ui/src/app/scenes/settings/settingsTabSearchContent.ts index ef2e4da15..c1e124788 100644 --- a/src/web-ui/src/app/scenes/settings/settingsTabSearchContent.ts +++ b/src/web-ui/src/app/scenes/settings/settingsTabSearchContent.ts @@ -96,18 +96,6 @@ export const SETTINGS_TAB_SEARCH_CONTENT: Record { - return await invoke('get_all_memories'); -} - - -export async function addMemory(request: CreateMemoryRequest): Promise { - return await invoke('add_memory', { request }); -} - - -export async function updateMemory(request: UpdateMemoryRequest): Promise { - return await invoke('update_memory', { request }); -} - - -export async function deleteMemory(id: string): Promise { - return await invoke('delete_memory', { id }); -} - - -export async function toggleMemory(id: string): Promise { - return await invoke('toggle_memory', { id }); -} - diff --git a/src/web-ui/src/infrastructure/api/service-api/AIRulesAPI.ts b/src/web-ui/src/infrastructure/api/service-api/AIRulesAPI.ts deleted file mode 100644 index 2f058a276..000000000 --- a/src/web-ui/src/infrastructure/api/service-api/AIRulesAPI.ts +++ /dev/null @@ -1,291 +0,0 @@ - - -import { api } from './ApiClient'; -import { i18nService } from '@/infrastructure/i18n'; - - - - -export enum RuleApplyType { - - AlwaysApply = 'always_apply', - - ApplyIntelligently = 'apply_intelligently', - - ApplyToSpecificFiles = 'apply_to_specific_files', - - ApplyManually = 'apply_manually', -} - - -export enum RuleLevel { - - User = 'user', - - Project = 'project', - - All = 'all', -} - - -export interface AIRule { - - name: string; - - level: RuleLevel; - - apply_type: RuleApplyType; - - description?: string; - - globs?: string; - - content: string; - - file_path: string; - - enabled: boolean; -} - - -export interface CreateRuleRequest { - - name: string; - - apply_type: RuleApplyType; - - description?: string; - - globs?: string; - - content: string; - - enabled?: boolean; -} - - -export interface UpdateRuleRequest { - - name?: string; - - apply_type?: RuleApplyType; - - description?: string; - - globs?: string; - - content?: string; - - enabled?: boolean; -} - - -export interface RuleStats { - - total_rules: number; - - enabled_rules: number; - - disabled_rules: number; - - by_apply_type: Record; -} - -function requireWorkspacePathForProjectLevel(level: RuleLevel, workspacePath?: string): string { - if ((level === RuleLevel.Project || level === RuleLevel.All) && !workspacePath) { - throw new Error('workspacePath is required when level includes project rules.'); - } - return workspacePath ?? ''; -} - -function requireWorkspacePath(workspacePath?: string): string { - if (!workspacePath) { - throw new Error('workspacePath is required.'); - } - return workspacePath; -} - - - -export class AIRulesAPI { - - static async getRules(level: RuleLevel, workspacePath?: string): Promise { - const resolvedWorkspacePath = - level === RuleLevel.Project || level === RuleLevel.All - ? requireWorkspacePathForProjectLevel(level, workspacePath) - : workspacePath; - return api.invoke('get_ai_rules', { - request: { - level, - workspacePath: resolvedWorkspacePath, - }, - }); - } - - - static async getRule(level: RuleLevel, name: string, workspacePath?: string): Promise { - const resolvedWorkspacePath = - level === RuleLevel.Project || level === RuleLevel.All - ? requireWorkspacePathForProjectLevel(level, workspacePath) - : workspacePath; - return api.invoke('get_ai_rule', { - request: { - level, - name, - workspacePath: resolvedWorkspacePath, - }, - }); - } - - - static async createRule( - level: RuleLevel, - rule: CreateRuleRequest, - workspacePath?: string, - ): Promise { - if (level === RuleLevel.All) { - throw new Error('Cannot create rule with "all" level. Please specify "user" or "project".'); - } - const resolvedWorkspacePath = - level === RuleLevel.Project ? requireWorkspacePathForProjectLevel(level, workspacePath) : workspacePath; - - return api.invoke('create_ai_rule', { - request: { - level, - rule, - workspacePath: resolvedWorkspacePath, - }, - }); - } - - - static async updateRule( - level: RuleLevel, - name: string, - rule: UpdateRuleRequest, - workspacePath?: string, - ): Promise { - if (level === RuleLevel.All) { - throw new Error('Cannot update rule with "all" level. Please specify "user" or "project".'); - } - const resolvedWorkspacePath = - level === RuleLevel.Project ? requireWorkspacePathForProjectLevel(level, workspacePath) : workspacePath; - - return api.invoke('update_ai_rule', { - request: { - level, - name, - rule, - workspacePath: resolvedWorkspacePath, - }, - }); - } - - - static async deleteRule(level: RuleLevel, name: string, workspacePath?: string): Promise { - if (level === RuleLevel.All) { - throw new Error('Cannot delete rule with "all" level. Please specify "user" or "project".'); - } - const resolvedWorkspacePath = - level === RuleLevel.Project ? requireWorkspacePathForProjectLevel(level, workspacePath) : workspacePath; - - return api.invoke('delete_ai_rule', { - request: { - level, - name, - workspacePath: resolvedWorkspacePath, - }, - }); - } - - - static async getRulesStats(level: RuleLevel, workspacePath?: string): Promise { - const resolvedWorkspacePath = - level === RuleLevel.Project || level === RuleLevel.All - ? requireWorkspacePathForProjectLevel(level, workspacePath) - : workspacePath; - return api.invoke('get_ai_rules_stats', { - request: { - level, - workspacePath: resolvedWorkspacePath, - }, - }); - } - - - static async buildSystemPrompt(workspacePath: string): Promise { - const resolvedWorkspacePath = requireWorkspacePath(workspacePath); - return api.invoke('build_ai_rules_system_prompt', { - request: { - workspacePath: resolvedWorkspacePath, - }, - }); - } - - - static async reloadRules(level: RuleLevel, workspacePath?: string): Promise { - const resolvedWorkspacePath = - level === RuleLevel.Project || level === RuleLevel.All - ? requireWorkspacePathForProjectLevel(level, workspacePath) - : workspacePath; - return api.invoke('reload_ai_rules', { - request: { - level, - workspacePath: resolvedWorkspacePath, - }, - }); - } - - - static async toggleRule(level: RuleLevel, name: string, workspacePath?: string): Promise { - if (level === RuleLevel.All) { - throw new Error('Cannot toggle rule with "all" level. Please specify "user" or "project".'); - } - const resolvedWorkspacePath = - level === RuleLevel.Project ? requireWorkspacePathForProjectLevel(level, workspacePath) : workspacePath; - - return api.invoke('toggle_ai_rule', { - request: { - level, - name, - workspacePath: resolvedWorkspacePath, - }, - }); - } - - - - - static getApplyTypeLabel(type: RuleApplyType): string { - const labels: Record = { - [RuleApplyType.AlwaysApply]: i18nService.t('settings/ai-rules:form.fields.applyTypes.alwaysApply'), - [RuleApplyType.ApplyIntelligently]: i18nService.t('settings/ai-rules:form.fields.applyTypes.applyIntelligently'), - [RuleApplyType.ApplyToSpecificFiles]: i18nService.t('settings/ai-rules:form.fields.applyTypes.applyToSpecificFiles'), - [RuleApplyType.ApplyManually]: i18nService.t('settings/ai-rules:form.fields.applyTypes.applyManually'), - }; - return labels[type] || type; - } - - - static getApplyTypeLabelEn(type: RuleApplyType): string { - const labels: Record = { - [RuleApplyType.AlwaysApply]: 'Always Apply', - [RuleApplyType.ApplyIntelligently]: 'Apply Intelligently', - [RuleApplyType.ApplyToSpecificFiles]: 'Apply to Specific Files', - [RuleApplyType.ApplyManually]: 'Apply Manually', - }; - return labels[type] || type; - } - - - static getRuleLevelLabel(level: RuleLevel): string { - const labels: Record = { - [RuleLevel.User]: i18nService.t('settings/ai-rules:filters.user'), - [RuleLevel.Project]: i18nService.t('settings/ai-rules:filters.project'), - [RuleLevel.All]: i18nService.t('settings/ai-rules:filters.all'), - }; - return labels[level] || level; - } -} - -export default AIRulesAPI; diff --git a/src/web-ui/src/infrastructure/config/components/AIRulesMemoryConfig.scss b/src/web-ui/src/infrastructure/config/components/AIRulesMemoryConfig.scss deleted file mode 100644 index 84f550708..000000000 --- a/src/web-ui/src/infrastructure/config/components/AIRulesMemoryConfig.scss +++ /dev/null @@ -1,282 +0,0 @@ -@use '../../../component-library/styles/tokens' as *; - -// ----- Rules section (from AIRulesConfig) ----- -.bitfun-ai-rules-config { - &__content { - display: flex; - flex-direction: column; - gap: $size-gap-6; - } - - &__rule-row { - cursor: pointer; - user-select: none; - transition: background $motion-base $easing-standard; - - &:hover { - background: var(--element-bg-subtle); - } - - &.is-disabled { - opacity: 0.55; - .bitfun-ai-rules-config__rule-name { - text-decoration: line-through; - } - } - } - - &__rule-label { - display: flex; - align-items: center; - gap: $size-gap-2; - flex-wrap: nowrap; - min-width: 0; - } - - &__rule-name { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - min-width: 0; - flex-shrink: 1; - } - - &__rule-badge { - flex-shrink: 0; - padding: 1px 7px; - border-radius: $size-radius-sm; - font-size: var(--font-size-xs); - font-weight: $font-weight-medium; - white-space: nowrap; - background: var(--element-bg-medium); - color: var(--color-text-muted); - border: 1px solid var(--border-subtle); - } - - &__item-actions { - display: flex; - align-items: center; - gap: $size-gap-2; - } - - &__action-btn { - display: inline-flex; - align-items: center; - justify-content: center; - width: 28px; - height: 28px; - padding: 0; - background: transparent; - border: none; - border-radius: $size-radius-base; - cursor: pointer; - color: var(--color-text-muted); - transition: all $motion-base $easing-standard; - - &:hover { - background: var(--element-bg-medium); - color: var(--color-text-primary); - } - &--danger:hover { - background: rgba($color-error, 0.08); - color: var(--color-error); - } - } - - &__details { - display: flex; - flex-direction: column; - gap: $size-gap-3; - padding: $size-gap-4; - border-top: 1px dashed var(--border-medium); - background: var(--element-bg-subtle); - animation: bitfun-ai-rules-config-slide-down 0.2s $easing-standard; - } - - &__details + .bitfun-config-page-row { - border-top: 1px dashed var(--border-medium); - } - - &__details-field { - font-size: var(--font-size-sm); - color: var(--color-text-secondary); - line-height: $line-height-relaxed; - } - - &__details-label { - font-size: var(--font-size-xs); - font-weight: $font-weight-medium; - color: var(--color-text-muted); - margin-right: $size-gap-2; - } - - &__details-code { - padding: 1px 6px; - background: var(--element-bg-medium); - border-radius: $size-radius-sm; - font-family: $font-family-mono; - font-size: var(--font-size-xs); - color: var(--color-accent-500); - } - - &__details-content { - display: flex; - flex-direction: column; - gap: $size-gap-1; - } - - &__details-pre { - margin: 0; - padding: $size-gap-3; - background: var(--element-bg-medium); - border: 1px solid var(--border-subtle); - border-radius: $size-radius-sm; - font-family: $font-family-mono; - font-size: var(--font-size-xs); - color: var(--color-text-primary); - line-height: $line-height-relaxed; - white-space: pre-wrap; - word-break: break-word; - max-height: 200px; - overflow-y: auto; - } - - &__details-meta { - font-size: var(--font-size-xs); - color: var(--color-text-muted); - padding-top: $size-gap-3; - border-top: 1px solid var(--border-subtle); - } - - &__form { - padding: $size-gap-4; - border-bottom: 1px solid var(--border-subtle); - animation: bitfun-ai-rules-config-slide-down 0.2s $easing-standard; - } - - &__form-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: $size-gap-3; - h3 { - margin: 0; - font-size: var(--font-size-sm); - font-weight: $font-weight-semibold; - color: var(--color-text-primary); - } - } - - &__form-body { - display: flex; - flex-direction: column; - gap: $size-gap-3; - } - - &__form-footer { - display: flex; - justify-content: flex-end; - gap: $size-gap-2; - margin-top: $size-gap-3; - } -} - -@keyframes bitfun-ai-rules-config-slide-down { - from { - opacity: 0; - transform: translateY(-6px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -@container config-panel (max-width: 520px) { - .bitfun-ai-rules-config { - &__details { - padding: $size-gap-3; - gap: $size-gap-2; - } - &__details-pre { - padding: $size-gap-2; - max-height: 160px; - } - &__form { - padding: $size-gap-3; - } - &__form-body { - gap: $size-gap-2; - } - &__action-btn { - width: 24px; - height: 24px; - } - } -} - -// ----- Memory section (from AIMemoryConfig) ----- -.bitfun-ai-memory-config { - &__badge--type { - display: inline-flex; - align-items: center; - gap: 3px; - flex-shrink: 0; - padding: 1px 7px; - border-radius: $size-radius-sm; - font-size: var(--font-size-xs); - font-weight: $font-weight-medium; - white-space: nowrap; - border: 1px solid transparent; - } - - &__dialog-body { - display: flex; - flex-direction: column; - gap: $size-gap-3; - padding: $size-gap-4; - } - - &__form-group { - display: flex; - flex-direction: column; - gap: $size-gap-1; - label { - font-size: var(--font-size-sm); - font-weight: $font-weight-medium; - color: var(--color-text-secondary); - } - input[type='range'] { - width: 100%; - accent-color: var(--color-accent-500); - } - } - - &__dialog-footer { - display: flex; - justify-content: flex-end; - gap: $size-gap-2; - padding: $size-gap-4; - border-top: 1px solid var(--border-subtle); - } -} - -// ----- Merged page layout ----- -.bitfun-ai-rules-memory-config { - &__scope-tabs { - margin-top: $size-gap-2; - .bitfun-tabs__nav { - margin-bottom: $size-gap-2; - } - .bitfun-tab-pane { - min-height: 60px; - } - } - - &__rules-panel, - &__memory-panel { - .bitfun-config-page-section { - margin-bottom: 0; - } - } -} diff --git a/src/web-ui/src/infrastructure/config/components/AIRulesMemoryConfig.tsx b/src/web-ui/src/infrastructure/config/components/AIRulesMemoryConfig.tsx deleted file mode 100644 index 226421b61..000000000 --- a/src/web-ui/src/infrastructure/config/components/AIRulesMemoryConfig.tsx +++ /dev/null @@ -1,611 +0,0 @@ -/* eslint-disable @typescript-eslint/no-use-before-define */ -/** - * AIRulesMemoryConfig — merged Rules & Memory settings page. - * Two sections (Rules / Memory), each with inner tabs: User | Project. - * Rules: full CRUD for user/project. Memory: user-level CRUD; project-level placeholder. - */ - -import React, { useState, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; -import { Plus, Edit2, Trash2, X, Eye, EyeOff } from 'lucide-react'; -import { Select, Input, Textarea, Button, IconButton, Switch, Tooltip, Modal } from '@/component-library'; -import { ConfigPageHeader, ConfigPageLayout, ConfigPageContent, ConfigPageSection, ConfigPageRow, ConfigCollectionItem } from './common'; -import { Tabs, TabPane } from '@/component-library'; -import { useAIRules } from '../../hooks/useAIRules'; -import { useCurrentWorkspace } from '../../contexts/WorkspaceContext'; -import { - AIRulesAPI, - RuleLevel, - RuleApplyType, - type CreateRuleRequest, - type AIRule -} from '../../api/service-api/AIRulesAPI'; -import { - getAllMemories, - addMemory, - updateMemory, - deleteMemory, - toggleMemory, - type AIMemory, - type MemoryType -} from '../../api/aiMemoryApi'; -import { useNotification } from '@/shared/notification-system'; -import { i18nService } from '@/infrastructure/i18n'; -import { createLogger } from '@/shared/utils/logger'; -import './AIRulesMemoryConfig.scss'; - -const log = createLogger('AIRulesMemoryConfig'); - -type ScopeTab = 'user' | 'project'; - -// ----- Rules panel (reused logic from AIRulesConfig) ----- - -function RulesPanel() { - const { t } = useTranslation('settings/ai-rules'); - const { t: tScope } = useTranslation('settings/ai-context'); - const { workspacePath } = useCurrentWorkspace(); - const [showAddForm, setShowAddForm] = useState(false); - const [editingRule, setEditingRule] = useState(null); - const [expandedRuleKeys, setExpandedRuleKeys] = useState>(new Set()); - const [isDeleting, setIsDeleting] = useState(false); - const [scopeTab, setScopeTab] = useState('user'); - const [formLevel, setFormLevel] = useState(RuleLevel.User); - const [formData, setFormData] = useState({ - name: '', - apply_type: RuleApplyType.AlwaysApply, - content: '', - description: '', - globs: '', - }); - - const userRules = useAIRules(RuleLevel.User); - const projectRules = useAIRules(RuleLevel.Project); - - const handleSubmit = async () => { - if (!formData.name?.trim()) { - alert(t('messages.nameRequired')); - return; - } - if (!formData.content?.trim()) { - alert(t('messages.contentRequired')); - return; - } - const finalApplyType = formLevel === RuleLevel.User ? RuleApplyType.AlwaysApply : formData.apply_type; - const ruleData: CreateRuleRequest = { - name: formData.name.trim(), - apply_type: finalApplyType, - content: formData.content, - }; - if (finalApplyType === RuleApplyType.ApplyIntelligently && formData.description) ruleData.description = formData.description; - if (finalApplyType === RuleApplyType.ApplyToSpecificFiles && formData.globs) ruleData.globs = formData.globs; - - try { - if (editingRule) { - await AIRulesAPI.updateRule(formLevel, editingRule.name, { - name: ruleData.name !== editingRule.name ? ruleData.name : undefined, - apply_type: ruleData.apply_type, - content: ruleData.content, - description: ruleData.description, - globs: ruleData.globs, - }, formLevel === RuleLevel.Project ? workspacePath || undefined : undefined); - if (formLevel === RuleLevel.User) await userRules.refresh(); - else await projectRules.refresh(); - } else { - if (formLevel === RuleLevel.User) await userRules.createRule(ruleData); - else await projectRules.createRule(ruleData); - } - resetForm(); - } catch (error) { - log.error('Failed to save rule', error); - alert(t('messages.saveFailed', { error: error instanceof Error ? error.message : String(error) })); - } - }; - - const handleAdd = (level: RuleLevel) => { - resetForm(); - setFormLevel(level); - setShowAddForm(true); - setEditingRule(null); - }; - - const handleEdit = (rule: AIRule) => { - setFormData({ - name: rule.name, - apply_type: rule.apply_type as RuleApplyType, - content: rule.content, - description: rule.description || '', - globs: rule.globs || '', - }); - setFormLevel(rule.level === 'user' ? RuleLevel.User : RuleLevel.Project); - setEditingRule(rule); - setShowAddForm(true); - }; - - const handleDelete = async (rule: AIRule, event: React.MouseEvent) => { - event.preventDefault(); - event.stopPropagation(); - if (isDeleting) return; - if (!(await window.confirm(t('messages.confirmDelete', { name: rule.name })))) return; - try { - setIsDeleting(true); - const level = rule.level === 'user' ? RuleLevel.User : RuleLevel.Project; - await AIRulesAPI.deleteRule( - level, - rule.name, - level === RuleLevel.Project ? workspacePath || undefined : undefined, - ); - if (level === RuleLevel.User) await userRules.refresh(); - else await projectRules.refresh(); - } catch (error) { - log.error('Failed to delete rule', { ruleName: rule.name, level: rule.level, error }); - alert(t('messages.deleteFailed', { error: error instanceof Error ? error.message : String(error) })); - } finally { - setIsDeleting(false); - } - }; - - const resetForm = () => { - setFormData({ - name: '', - apply_type: RuleApplyType.AlwaysApply, - content: '', - description: '', - globs: '', - }); - setFormLevel(RuleLevel.User); - setShowAddForm(false); - setEditingRule(null); - }; - - const handleToggle = async (rule: AIRule, event: React.MouseEvent) => { - event.preventDefault(); - event.stopPropagation(); - try { - const level = rule.level === 'user' ? RuleLevel.User : RuleLevel.Project; - if (level === RuleLevel.User) await userRules.toggleRule(rule.name); - else await projectRules.toggleRule(rule.name); - } catch (error) { - log.error('Failed to toggle rule', { ruleName: rule.name, level: rule.level, error }); - alert(t('messages.toggleFailed', { error: error instanceof Error ? error.message : String(error) })); - } - }; - - const getRuleKey = (rule: AIRule) => `${rule.level}-${rule.name}`; - const toggleRuleExpanded = (ruleKey: string) => { - setExpandedRuleKeys((prev) => { - const next = new Set(prev); - if (next.has(ruleKey)) next.delete(ruleKey); - else next.add(ruleKey); - return next; - }); - }; - - const getApplyTypeOptions = () => [ - { label: t('form.fields.applyTypes.alwaysApply'), value: RuleApplyType.AlwaysApply }, - { label: t('form.fields.applyTypes.applyIntelligently'), value: RuleApplyType.ApplyIntelligently }, - { label: t('form.fields.applyTypes.applyToSpecificFiles'), value: RuleApplyType.ApplyToSpecificFiles }, - { label: t('form.fields.applyTypes.applyManually'), value: RuleApplyType.ApplyManually }, - ]; - - const renderForm = () => { - if (!showAddForm) return null; - const isUserLevel = formLevel === RuleLevel.User; - const showDescription = !isUserLevel && formData.apply_type === RuleApplyType.ApplyIntelligently; - const showGlobs = !isUserLevel && formData.apply_type === RuleApplyType.ApplyToSpecificFiles; - return ( -
-
-

{editingRule ? t('form.titleEdit') : t('form.titleCreate')}

- - - -
-
- setFormData({ ...formData, name: e.target.value })} placeholder={t('form.fields.namePlaceholder')} variant="outlined" size="small" /> - {!isUserLevel && ( - setFormData({ ...formData, description: e.target.value })} placeholder={t('form.fields.descriptionPlaceholder')} variant="outlined" size="small" /> - )} - {showGlobs && ( - setFormData({ ...formData, globs: e.target.value })} placeholder={t('form.fields.globsPlaceholder')} variant="outlined" size="small" /> - )} -