From 5f372ea348a195769275b3157c59aa17e2da4373 Mon Sep 17 00:00:00 2001 From: wsp1911 Date: Wed, 8 Apr 2026 21:01:01 +0800 Subject: [PATCH 1/2] feat(skills): add mode-based builtin skill defaults and batch selection save - add explicit builtin skill profiles per mode and mark builtin skills in the registry - support default-off builtin skills via `enabled_user_skills` alongside existing disable overrides - update skill resolution and desktop APIs to compute final mode skill state from defaults plus overrides - batch-save Agents scene skill selection in a single request instead of per-skill serial toggles - hide skill sections in agent cards when the `Skill` tool is not enabled --- src/apps/desktop/src/api/skill_api.rs | 266 +++++++++++++++++- src/apps/desktop/src/lib.rs | 1 + .../tools/implementations/skills/builtin.rs | 27 ++ .../skills/default_profiles.rs | 158 +++++++++++ .../tools/implementations/skills/mod.rs | 1 + .../implementations/skills/mode_overrides.rs | 116 +++++++- .../tools/implementations/skills/registry.rs | 41 +-- .../tools/implementations/skills/types.rs | 6 +- .../config/mode_config_canonicalizer.rs | 100 ++++++- src/crates/core/src/service/config/types.rs | 8 + .../src/app/scenes/agents/AgentsScene.tsx | 19 +- .../app/scenes/agents/hooks/useAgentsList.ts | 22 +- .../api/service-api/ConfigAPI.ts | 24 ++ .../src/infrastructure/config/types/index.ts | 214 +++++--------- 14 files changed, 791 insertions(+), 212 deletions(-) create mode 100644 src/crates/core/src/agentic/tools/implementations/skills/default_profiles.rs diff --git a/src/apps/desktop/src/api/skill_api.rs b/src/apps/desktop/src/api/skill_api.rs index af6858bc..e9b3eda7 100644 --- a/src/apps/desktop/src/api/skill_api.rs +++ b/src/apps/desktop/src/api/skill_api.rs @@ -17,12 +17,13 @@ use tokio::time::{timeout, Duration}; use crate::api::app_state::AppState; use bitfun_core::agentic::tools::implementations::skills::mode_overrides::{ - get_disabled_mode_skills_from_document, load_disabled_user_mode_skills, - load_project_mode_skills_document_local, project_mode_skills_path_for_remote, - save_project_mode_skills_document_local, set_mode_skill_disabled_in_document, - set_user_mode_skill_disabled, + get_disabled_mode_skills_from_document, load_project_mode_skills_document_local, + load_user_mode_skill_overrides, project_mode_skills_path_for_remote, + save_project_mode_skills_document_local, set_disabled_mode_skills_in_document, + set_mode_skill_disabled_in_document, set_user_mode_skill_state, }; use bitfun_core::agentic::tools::implementations::skills::{ + default_profiles::{is_enabled_by_default_for_mode, is_skill_enabled_for_mode}, ModeSkillInfo, SkillData, SkillInfo, SkillLocation, SkillRegistry, }; use bitfun_core::agentic::workspace::RemoteWorkspaceFs; @@ -82,6 +83,14 @@ pub struct SkillMarketDownloadResponse { pub output: String, } +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ReplaceModeSkillSelectionRequest { + pub mode_id: String, + pub enabled_skill_keys: Vec, + pub workspace_path: Option, +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SkillMarketItem { @@ -183,11 +192,9 @@ async fn get_mode_skill_infos_for_workspace_input( workspace_path: Option<&str>, ) -> Result, String> { let all_skills = get_all_skills_for_workspace_input(state, registry, workspace_path).await?; - let disabled_user: HashSet = load_disabled_user_mode_skills(mode_id) + let user_overrides = load_user_mode_skill_overrides(mode_id) .await - .map_err(|e| format!("Failed to load user skill overrides: {}", e))? - .into_iter() - .collect(); + .map_err(|e| format!("Failed to load user skill overrides: {}", e))?; let (disabled_project, resolved_skills): (HashSet, Vec) = if let Some(( remote_root, @@ -254,10 +261,8 @@ async fn get_mode_skill_infos_for_workspace_input( Ok(all_skills .into_iter() .map(|skill| { - let disabled_by_mode = match skill.level { - SkillLocation::User => disabled_user.contains(&skill.key), - SkillLocation::Project => disabled_project.contains(&skill.key), - }; + let disabled_by_mode = + !is_skill_enabled_for_mode(&skill, mode_id, &user_overrides, &disabled_project); let selected_for_runtime = resolved_keys.contains(&skill.key); ModeSkillInfo { @@ -269,6 +274,145 @@ async fn get_mode_skill_infos_for_workspace_input( .collect()) } +fn normalize_skill_key_list(keys: Vec) -> Vec { + let mut seen = HashSet::new(); + let mut normalized = Vec::new(); + + for key in keys { + let trimmed = key.trim(); + if trimmed.is_empty() { + continue; + } + + let owned = trimmed.to_string(); + if seen.insert(owned.clone()) { + normalized.push(owned); + } + } + + normalized +} + +async fn persist_user_mode_skill_selection( + mode_id: &str, + all_skills: &[SkillInfo], + enabled_keys: &HashSet, +) -> Result<(), String> { + let mut disabled_user_skills = Vec::new(); + let mut enabled_user_skills = Vec::new(); + + for skill in all_skills + .iter() + .filter(|skill| skill.level == SkillLocation::User) + { + let should_enable = enabled_keys.contains(&skill.key); + let default_enabled = is_enabled_by_default_for_mode(skill, mode_id); + + if default_enabled && !should_enable { + disabled_user_skills.push(skill.key.clone()); + } else if !default_enabled && should_enable { + enabled_user_skills.push(skill.key.clone()); + } + } + + bitfun_core::service::config::mode_config_canonicalizer::persist_mode_config_from_value( + mode_id, + serde_json::json!({ + "disabled_user_skills": normalize_skill_key_list(disabled_user_skills), + "enabled_user_skills": normalize_skill_key_list(enabled_user_skills), + }), + ) + .await + .map_err(|e| format!("Failed to update user skill overrides: {}", e)) +} + +fn build_disabled_project_skill_keys( + all_skills: &[SkillInfo], + enabled_keys: &HashSet, +) -> Vec { + all_skills + .iter() + .filter(|skill| skill.level == SkillLocation::Project) + .filter(|skill| !enabled_keys.contains(&skill.key)) + .map(|skill| skill.key.clone()) + .collect() +} + +async fn persist_project_mode_skill_selection_local( + mode_id: &str, + workspace_root: &Path, + disabled_project_skills: Vec, +) -> Result<(), String> { + let mut document = load_project_mode_skills_document_local(workspace_root) + .await + .map_err(|e| format!("Failed to load project mode skills: {}", e))?; + set_disabled_mode_skills_in_document(&mut document, mode_id, disabled_project_skills) + .map_err(|e| format!("Failed to update project skill overrides: {}", e))?; + save_project_mode_skills_document_local(workspace_root, &document) + .await + .map_err(|e| format!("Failed to save project mode skills: {}", e)) +} + +async fn persist_project_mode_skill_selection_remote( + state: &State<'_, AppState>, + remote_root: &str, + entry: &RemoteWorkspaceEntry, + mode_id: &str, + disabled_project_skills: Vec, +) -> Result<(), String> { + let remote_fs = state + .get_remote_file_service_async() + .await + .map_err(|e| format!("Remote file service not available: {}", e))?; + let config_path = project_mode_skills_path_for_remote(remote_root); + let mut document = if remote_fs + .exists(&entry.connection_id, &config_path) + .await + .map_err(|e| format!("Failed to check remote project skill overrides: {}", e))? + { + let content = remote_fs + .read_file(&entry.connection_id, &config_path) + .await + .map_err(|e| format!("Failed to read remote project skill overrides: {}", e))?; + let content = String::from_utf8(content) + .map_err(|e| format!("Remote project skill overrides are not valid UTF-8: {}", e))?; + serde_json::from_str::(&content) + .map_err(|e| format!("Invalid remote project skill overrides JSON: {}", e))? + } else { + serde_json::json!({}) + }; + + set_disabled_mode_skills_in_document(&mut document, mode_id, disabled_project_skills) + .map_err(|e| format!("Failed to update remote project skill overrides: {}", e))?; + + let config_dir = config_path + .rsplit_once('/') + .map(|(dir, _)| dir.to_string()) + .ok_or_else(|| format!("Invalid remote project config path '{}'", config_path))?; + + remote_fs + .create_dir_all(&entry.connection_id, &config_dir) + .await + .map_err(|e| { + format!( + "Failed to create remote project skill overrides directory: {}", + e + ) + })?; + remote_fs + .write_file( + &entry.connection_id, + &config_path, + serde_json::to_vec_pretty(&document) + .map_err(|e| format!("Failed to serialize remote project skill overrides: {}", e))? + .as_slice(), + ) + .await + .map_err(|e| format!("Failed to write remote project skill overrides: {}", e))?; + + Ok(()) +} + #[tauri::command] pub async fn get_skill_configs( state: State<'_, AppState>, @@ -322,7 +466,34 @@ pub async fn set_mode_skill_disabled( workspace_path: Option, ) -> Result { if skill_key.starts_with("user::") { - set_user_mode_skill_disabled(&mode_id, &skill_key, disabled) + let registry = SkillRegistry::global(); + let skill_info = if let Some((remote_root, entry)) = + resolve_remote_workspace(&state, workspace_path.as_deref()).await? + { + let remote_fs = state + .get_remote_file_service_async() + .await + .map_err(|e| format!("Remote file service not available: {}", e))?; + let remote_workspace_fs = RemoteWorkspaceFs::new(entry.connection_id, remote_fs); + registry + .find_skill_by_key_for_remote_workspace( + &remote_workspace_fs, + &remote_root, + &skill_key, + ) + .await + } else { + registry + .find_skill_by_key_for_workspace( + &skill_key, + workspace_root_from_input(workspace_path.as_deref()).as_deref(), + ) + .await + } + .ok_or_else(|| format!("Skill '{}' not found", skill_key))?; + + let default_enabled = is_enabled_by_default_for_mode(&skill_info, &mode_id); + set_user_mode_skill_state(&mode_id, &skill_key, !disabled, default_enabled) .await .map_err(|e| format!("Failed to update user skill override: {}", e))?; if let Err(e) = bitfun_core::service::config::reload_global_config().await { @@ -417,6 +588,75 @@ pub async fn set_mode_skill_disabled( )) } +#[tauri::command] +pub async fn replace_mode_skill_selection( + state: State<'_, AppState>, + request: ReplaceModeSkillSelectionRequest, +) -> Result { + let registry = SkillRegistry::global(); + let all_skills = + get_all_skills_for_workspace_input(&state, registry, request.workspace_path.as_deref()) + .await?; + + let enabled_skill_keys = normalize_skill_key_list(request.enabled_skill_keys); + let enabled_keys: HashSet = enabled_skill_keys.iter().cloned().collect(); + let known_keys: HashSet = all_skills.iter().map(|skill| skill.key.clone()).collect(); + let unknown_keys: Vec = enabled_skill_keys + .iter() + .filter(|key| !known_keys.contains(*key)) + .cloned() + .collect(); + if !unknown_keys.is_empty() { + return Err(format!( + "Unknown skill keys for mode '{}': {}", + request.mode_id, + unknown_keys.join(", ") + )); + } + + persist_user_mode_skill_selection(&request.mode_id, &all_skills, &enabled_keys).await?; + + let disabled_project_skills = normalize_skill_key_list(build_disabled_project_skill_keys( + &all_skills, + &enabled_keys, + )); + + if let Some((remote_root, entry)) = + resolve_remote_workspace(&state, request.workspace_path.as_deref()).await? + { + persist_project_mode_skill_selection_remote( + &state, + &remote_root, + &entry, + &request.mode_id, + disabled_project_skills, + ) + .await?; + } else if let Some(workspace_root) = + workspace_root_from_input(request.workspace_path.as_deref()) + { + persist_project_mode_skill_selection_local( + &request.mode_id, + &workspace_root, + disabled_project_skills, + ) + .await?; + } + + if let Err(e) = bitfun_core::service::config::reload_global_config().await { + log::warn!( + "Failed to reload global config after batch skill update: mode_id={}, error={}", + request.mode_id, + e + ); + } + + Ok(format!( + "Mode '{}' skill selection updated successfully", + request.mode_id + )) +} + #[tauri::command] pub async fn validate_skill_path(path: String) -> Result { use std::path::Path; diff --git a/src/apps/desktop/src/lib.rs b/src/apps/desktop/src/lib.rs index 4617005a..c1104233 100644 --- a/src/apps/desktop/src/lib.rs +++ b/src/apps/desktop/src/lib.rs @@ -420,6 +420,7 @@ pub async fn run() { search_skill_market, download_skill_market, set_mode_skill_disabled, + replace_mode_skill_selection, validate_skill_path, add_skill, delete_skill, diff --git a/src/crates/core/src/agentic/tools/implementations/skills/builtin.rs b/src/crates/core/src/agentic/tools/implementations/skills/builtin.rs index 11e32672..3a61d86c 100644 --- a/src/crates/core/src/agentic/tools/implementations/skills/builtin.rs +++ b/src/crates/core/src/agentic/tools/implementations/skills/builtin.rs @@ -7,10 +7,37 @@ use crate::infrastructure::get_path_manager_arc; use crate::util::errors::BitFunResult; use include_dir::{include_dir, Dir}; use log::{debug, error}; +use std::collections::HashSet; use std::path::{Path, PathBuf}; +use std::sync::OnceLock; use tokio::fs; static BUILTIN_SKILLS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/builtin_skills"); +static BUILTIN_SKILL_DIR_NAMES: OnceLock> = OnceLock::new(); + +fn collect_builtin_skill_dir_names() -> HashSet { + BUILTIN_SKILLS_DIR + .dirs() + .filter_map(|dir| { + let rel = dir.path(); + if rel.components().count() != 1 { + return None; + } + + rel.file_name() + .and_then(|name| name.to_str()) + .map(|name| name.to_string()) + }) + .collect() +} + +pub fn builtin_skill_dir_names() -> &'static HashSet { + BUILTIN_SKILL_DIR_NAMES.get_or_init(collect_builtin_skill_dir_names) +} + +pub fn is_builtin_skill_dir_name(dir_name: &str) -> bool { + builtin_skill_dir_names().contains(dir_name) +} pub async fn ensure_builtin_skills_installed() -> BitFunResult<()> { let pm = get_path_manager_arc(); diff --git a/src/crates/core/src/agentic/tools/implementations/skills/default_profiles.rs b/src/crates/core/src/agentic/tools/implementations/skills/default_profiles.rs new file mode 100644 index 00000000..e3f83146 --- /dev/null +++ b/src/crates/core/src/agentic/tools/implementations/skills/default_profiles.rs @@ -0,0 +1,158 @@ +//! Default built-in skill profiles per mode. + +use super::mode_overrides::UserModeSkillOverrides; +use super::types::{SkillInfo, SkillLocation}; +use std::collections::HashSet; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct BuiltinSkillProfile { + /// Baseline state for built-in skills in this mode. + default_enabled: bool, + /// Built-in skill directory names whose state differs from `default_enabled`. + overridden_skills: &'static [&'static str], +} + +const ENABLE_ALL_BUILTINS: BuiltinSkillProfile = BuiltinSkillProfile { + default_enabled: true, + overridden_skills: &[], +}; + +const DISABLE_ALL_BUILTINS: BuiltinSkillProfile = BuiltinSkillProfile { + default_enabled: false, + overridden_skills: &[], +}; + +const AGENTIC_PROFILE: BuiltinSkillProfile = BuiltinSkillProfile { + default_enabled: true, + overridden_skills: &["docx", "pdf", "pptx", "xlsx"], +}; + +const COWORK_PROFILE: BuiltinSkillProfile = BuiltinSkillProfile { + default_enabled: false, + overridden_skills: &[ + "docx", + "pdf", + "pptx", + "xlsx", + "find-skills", + "writing-skills", + ], +}; + +fn builtin_profile_for_mode(mode_id: &str) -> BuiltinSkillProfile { + match mode_id { + "Plan" | "debug" => DISABLE_ALL_BUILTINS, + "agentic" => AGENTIC_PROFILE, + "Cowork" => COWORK_PROFILE, + _ => ENABLE_ALL_BUILTINS, + } +} + +pub fn is_enabled_by_default_for_mode(skill: &SkillInfo, mode_id: &str) -> bool { + if skill.level != SkillLocation::User || !skill.is_builtin { + return true; + } + + let profile = builtin_profile_for_mode(mode_id); + if profile.overridden_skills.contains(&skill.dir_name.as_str()) { + !profile.default_enabled + } else { + profile.default_enabled + } +} + +pub fn is_skill_enabled_for_mode( + skill: &SkillInfo, + mode_id: &str, + user_overrides: &UserModeSkillOverrides, + disabled_project_skills: &HashSet, +) -> bool { + match skill.level { + SkillLocation::Project => !disabled_project_skills.contains(&skill.key), + SkillLocation::User => { + let default_enabled = is_enabled_by_default_for_mode(skill, mode_id); + + if default_enabled { + !user_overrides.disabled_skills.contains(&skill.key) + } else { + user_overrides.enabled_skills.contains(&skill.key) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::{is_enabled_by_default_for_mode, is_skill_enabled_for_mode}; + use crate::agentic::tools::implementations::skills::mode_overrides::UserModeSkillOverrides; + use crate::agentic::tools::implementations::skills::types::{SkillInfo, SkillLocation}; + use std::collections::HashSet; + + fn builtin_skill(dir_name: &str) -> SkillInfo { + SkillInfo { + key: format!("user::bitfun::{}", dir_name), + name: dir_name.to_string(), + description: String::new(), + path: format!("/tmp/{}", dir_name), + level: SkillLocation::User, + source_slot: "bitfun".to_string(), + dir_name: dir_name.to_string(), + is_builtin: true, + } + } + + fn custom_user_skill(dir_name: &str) -> SkillInfo { + SkillInfo { + key: format!("user::bitfun::{}", dir_name), + name: dir_name.to_string(), + description: String::new(), + path: format!("/tmp/{}", dir_name), + level: SkillLocation::User, + source_slot: "bitfun".to_string(), + dir_name: dir_name.to_string(), + is_builtin: false, + } + } + + #[test] + fn builtin_defaults_follow_mode_profiles() { + let pdf = builtin_skill("pdf"); + let tdd = builtin_skill("test-driven-development"); + + assert!(!is_enabled_by_default_for_mode(&pdf, "agentic")); + assert!(is_enabled_by_default_for_mode(&tdd, "agentic")); + assert!(is_enabled_by_default_for_mode(&pdf, "Cowork")); + assert!(!is_enabled_by_default_for_mode(&tdd, "Cowork")); + assert!(!is_enabled_by_default_for_mode(&pdf, "Plan")); + assert!(!is_enabled_by_default_for_mode(&tdd, "debug")); + } + + #[test] + fn non_builtin_user_skills_remain_enabled_by_default() { + let custom = custom_user_skill("my-custom-skill"); + assert!(is_enabled_by_default_for_mode(&custom, "agentic")); + assert!(is_enabled_by_default_for_mode(&custom, "Plan")); + } + + #[test] + fn user_overrides_apply_on_top_of_defaults() { + let pdf = builtin_skill("pdf"); + let mut overrides = UserModeSkillOverrides::default(); + let disabled_project = HashSet::new(); + + assert!(!is_skill_enabled_for_mode( + &pdf, + "agentic", + &overrides, + &disabled_project, + )); + + overrides.enabled_skills.push(pdf.key.clone()); + assert!(is_skill_enabled_for_mode( + &pdf, + "agentic", + &overrides, + &disabled_project, + )); + } +} diff --git a/src/crates/core/src/agentic/tools/implementations/skills/mod.rs b/src/crates/core/src/agentic/tools/implementations/skills/mod.rs index 53988260..c65852e6 100644 --- a/src/crates/core/src/agentic/tools/implementations/skills/mod.rs +++ b/src/crates/core/src/agentic/tools/implementations/skills/mod.rs @@ -3,6 +3,7 @@ //! Provides Skill registry, loading, and configuration management functionality pub mod builtin; +pub mod default_profiles; pub mod mode_overrides; pub mod registry; pub mod types; diff --git a/src/crates/core/src/agentic/tools/implementations/skills/mode_overrides.rs b/src/crates/core/src/agentic/tools/implementations/skills/mode_overrides.rs index 660bb450..1575d6e0 100644 --- a/src/crates/core/src/agentic/tools/implementations/skills/mode_overrides.rs +++ b/src/crates/core/src/agentic/tools/implementations/skills/mode_overrides.rs @@ -13,6 +13,12 @@ use std::path::Path; const PROJECT_MODE_SKILLS_FILE_NAME: &str = "mode_skills.json"; const DISABLED_SKILLS_KEY: &str = "disabled_skills"; +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct UserModeSkillOverrides { + pub disabled_skills: Vec, + pub enabled_skills: Vec, +} + fn dedupe_skill_keys(keys: Vec) -> Vec { let mut seen = HashSet::new(); let mut normalized = Vec::new(); @@ -31,36 +37,71 @@ fn dedupe_skill_keys(keys: Vec) -> Vec { normalized } -pub async fn load_disabled_user_mode_skills(mode_id: &str) -> BitFunResult> { +fn normalize_user_overrides( + disabled_skills: Vec, + enabled_skills: Vec, +) -> UserModeSkillOverrides { + let disabled_skills = dedupe_skill_keys(disabled_skills); + let disabled_set: HashSet = disabled_skills.iter().cloned().collect(); + let mut enabled_skills = dedupe_skill_keys(enabled_skills); + enabled_skills.retain(|key| !disabled_set.contains(key)); + + UserModeSkillOverrides { + disabled_skills, + enabled_skills, + } +} + +pub async fn load_user_mode_skill_overrides(mode_id: &str) -> BitFunResult { let config_service = GlobalConfigManager::get_service().await?; let stored_configs: HashMap = config_service .get_config(Some("ai.mode_configs")) .await .unwrap_or_default(); - Ok(dedupe_skill_keys( - stored_configs - .get(mode_id) - .map(|config| config.disabled_user_skills.clone()) + let config = stored_configs.get(mode_id); + Ok(normalize_user_overrides( + config + .map(|item| item.disabled_user_skills.clone()) + .unwrap_or_default(), + config + .map(|item| item.enabled_user_skills.clone()) .unwrap_or_default(), )) } -pub async fn set_user_mode_skill_disabled( +pub async fn set_user_mode_skill_state( mode_id: &str, skill_key: &str, - disabled: bool, -) -> BitFunResult> { - let mut next = load_disabled_user_mode_skills(mode_id).await?; - if disabled { - next.push(skill_key.to_string()); - next = dedupe_skill_keys(next); + enabled: bool, + default_enabled: bool, +) -> BitFunResult { + let mut overrides = load_user_mode_skill_overrides(mode_id).await?; + overrides.disabled_skills.retain(|value| value != skill_key); + overrides.enabled_skills.retain(|value| value != skill_key); + + if default_enabled { + if !enabled { + overrides.disabled_skills.push(skill_key.to_string()); + } } else { - next.retain(|value| value != skill_key); + if enabled { + overrides.enabled_skills.push(skill_key.to_string()); + } } - persist_mode_config_from_value(mode_id, json!({ "disabled_user_skills": next })).await?; - load_disabled_user_mode_skills(mode_id).await + let overrides = normalize_user_overrides(overrides.disabled_skills, overrides.enabled_skills); + + persist_mode_config_from_value( + mode_id, + json!({ + "disabled_user_skills": overrides.disabled_skills, + "enabled_user_skills": overrides.enabled_skills, + }), + ) + .await?; + + load_user_mode_skill_overrides(mode_id).await } pub fn project_mode_skills_path_for_remote(remote_root: &str) -> String { @@ -158,6 +199,51 @@ pub fn set_mode_skill_disabled_in_document( Ok(next) } +pub fn set_disabled_mode_skills_in_document( + document: &mut Value, + mode_id: &str, + skill_keys: Vec, +) -> BitFunResult> { + let mode_skills = mode_skills_object_mut(document)?; + let next = dedupe_skill_keys(skill_keys); + + if next.is_empty() { + if let Some(mode_entry) = mode_skills.get_mut(mode_id) { + if !mode_entry.is_object() { + *mode_entry = Value::Object(Map::new()); + } + + if let Some(mode_object) = mode_entry.as_object_mut() { + mode_object.remove(DISABLED_SKILLS_KEY); + if mode_object.is_empty() { + mode_skills.remove(mode_id); + } + } + } + + return Ok(Vec::new()); + } + + let mode_entry = mode_skills + .entry(mode_id.to_string()) + .or_insert_with(|| Value::Object(Map::new())); + + if !mode_entry.is_object() { + *mode_entry = Value::Object(Map::new()); + } + + let mode_object = mode_entry.as_object_mut().ok_or_else(|| { + BitFunError::config("Mode skills entry must be a JSON object".to_string()) + })?; + + mode_object.insert( + DISABLED_SKILLS_KEY.to_string(), + serde_json::to_value(&next)?, + ); + + Ok(next) +} + pub async fn load_project_mode_skills_document_local(workspace_root: &Path) -> BitFunResult { let path = get_path_manager_arc().project_mode_skills_file(workspace_root); match tokio::fs::read_to_string(&path).await { diff --git a/src/crates/core/src/agentic/tools/implementations/skills/registry.rs b/src/crates/core/src/agentic/tools/implementations/skills/registry.rs index c540951b..db44bf8f 100644 --- a/src/crates/core/src/agentic/tools/implementations/skills/registry.rs +++ b/src/crates/core/src/agentic/tools/implementations/skills/registry.rs @@ -2,10 +2,11 @@ //! //! Manages skill discovery, mode-specific filtering, and loading. -use super::builtin::ensure_builtin_skills_installed; +use super::builtin::{ensure_builtin_skills_installed, is_builtin_skill_dir_name}; +use super::default_profiles::is_skill_enabled_for_mode; use super::mode_overrides::{ load_disabled_mode_skills_local, load_disabled_mode_skills_remote, - load_disabled_user_mode_skills, + load_user_mode_skill_overrides, UserModeSkillOverrides, }; use super::types::{SkillData, SkillInfo, SkillLocation}; use crate::agentic::workspace::WorkspaceFileSystem; @@ -73,6 +74,9 @@ impl SkillCandidate { fn from_data(mut data: SkillData, slot: &str, key_prefix: &str, priority: usize) -> Self { data.source_slot = slot.to_string(); data.key = build_skill_key(key_prefix, slot, &data.dir_name); + let is_builtin = data.location == SkillLocation::User + && slot == "bitfun" + && is_builtin_skill_dir_name(&data.dir_name); Self { info: SkillInfo { @@ -83,6 +87,7 @@ impl SkillCandidate { level: data.location, source_slot: data.source_slot, dir_name: data.dir_name, + is_builtin, }, priority, } @@ -411,9 +416,9 @@ impl SkillRegistry { return candidates; }; - let disabled_user = load_disabled_user_mode_skills(mode_id) + let user_overrides = load_user_mode_skill_overrides(mode_id) .await - .unwrap_or_default(); + .unwrap_or_else(|_| UserModeSkillOverrides::default()); let disabled_project = match workspace_root { Some(root) => load_disabled_mode_skills_local(root, mode_id) .await @@ -421,17 +426,19 @@ impl SkillRegistry { None => Vec::new(), }; - let disabled_user: HashSet = - dedupe_preserving_order(disabled_user).into_iter().collect(); let disabled_project: HashSet = dedupe_preserving_order(disabled_project) .into_iter() .collect(); candidates .into_iter() - .filter(|candidate| match candidate.info.level { - SkillLocation::User => !disabled_user.contains(&candidate.info.key), - SkillLocation::Project => !disabled_project.contains(&candidate.info.key), + .filter(|candidate| { + is_skill_enabled_for_mode( + &candidate.info, + mode_id, + &user_overrides, + &disabled_project, + ) }) .collect() } @@ -447,24 +454,26 @@ impl SkillRegistry { return candidates; }; - let disabled_user = load_disabled_user_mode_skills(mode_id) + let user_overrides = load_user_mode_skill_overrides(mode_id) .await - .unwrap_or_default(); + .unwrap_or_else(|_| UserModeSkillOverrides::default()); let disabled_project = load_disabled_mode_skills_remote(fs, remote_root, mode_id) .await .unwrap_or_default(); - let disabled_user: HashSet = - dedupe_preserving_order(disabled_user).into_iter().collect(); let disabled_project: HashSet = dedupe_preserving_order(disabled_project) .into_iter() .collect(); candidates .into_iter() - .filter(|candidate| match candidate.info.level { - SkillLocation::User => !disabled_user.contains(&candidate.info.key), - SkillLocation::Project => !disabled_project.contains(&candidate.info.key), + .filter(|candidate| { + is_skill_enabled_for_mode( + &candidate.info, + mode_id, + &user_overrides, + &disabled_project, + ) }) .collect() } diff --git a/src/crates/core/src/agentic/tools/implementations/skills/types.rs b/src/crates/core/src/agentic/tools/implementations/skills/types.rs index 5aadeb5c..5c548702 100644 --- a/src/crates/core/src/agentic/tools/implementations/skills/types.rs +++ b/src/crates/core/src/agentic/tools/implementations/skills/types.rs @@ -41,6 +41,9 @@ pub struct SkillInfo { pub source_slot: String, /// Directory name under the slot's `skills/` root. pub dir_name: String, + /// Whether this skill is bundled with BitFun as a built-in skill. + #[serde(default)] + pub is_builtin: bool, } impl SkillInfo { @@ -70,7 +73,8 @@ impl SkillInfo { pub struct ModeSkillInfo { #[serde(flatten)] pub skill: SkillInfo, - /// True when this skill key is explicitly disabled in the current mode's config. + /// True when this skill is currently disabled for the mode after applying + /// defaults plus user/project overrides. pub disabled_by_mode: bool, /// True when this skill is the one actually selected for runtime after applying /// mode disables and same-name priority resolution. diff --git a/src/crates/core/src/service/config/mode_config_canonicalizer.rs b/src/crates/core/src/service/config/mode_config_canonicalizer.rs index 3f26ae3a..4570c86b 100644 --- a/src/crates/core/src/service/config/mode_config_canonicalizer.rs +++ b/src/crates/core/src/service/config/mode_config_canonicalizer.rs @@ -57,6 +57,17 @@ fn normalize_skill_keys(keys: Vec) -> Vec { dedupe_preserving_order(keys) } +fn normalize_skill_override_lists( + disabled_user_skills: Vec, + enabled_user_skills: Vec, +) -> (Vec, Vec) { + let disabled_user_skills = normalize_skill_keys(disabled_user_skills); + let disabled_set: HashSet = disabled_user_skills.iter().cloned().collect(); + let mut enabled_user_skills = normalize_skill_keys(enabled_user_skills); + enabled_user_skills.retain(|key| !disabled_set.contains(key)); + (disabled_user_skills, enabled_user_skills) +} + pub fn resolve_effective_tools( default_tools: &[String], mode_config: Option<&ModeConfig>, @@ -96,6 +107,7 @@ fn stored_mode_from_enabled_tools( enabled: bool, enabled_tools: Vec, disabled_user_skills: Vec, + enabled_user_skills: Vec, default_tools: &[String], valid_tools: &HashSet, ) -> Option { @@ -124,6 +136,7 @@ fn stored_mode_from_enabled_tools( added_tools, removed_tools, disabled_user_skills, + enabled_user_skills, &default_tools, valid_tools, ) @@ -135,13 +148,15 @@ fn stored_mode_from_overrides( added_tools: Vec, removed_tools: Vec, disabled_user_skills: Vec, + enabled_user_skills: Vec, default_tools: &[String], valid_tools: &HashSet, ) -> Option { let default_set: HashSet = default_tools.iter().cloned().collect(); let mut added_tools = normalize_tools(added_tools, valid_tools); let mut removed_tools = normalize_tools(removed_tools, valid_tools); - let disabled_user_skills = normalize_skill_keys(disabled_user_skills); + let (disabled_user_skills, enabled_user_skills) = + normalize_skill_override_lists(disabled_user_skills, enabled_user_skills); added_tools.retain(|tool| !default_set.contains(tool)); removed_tools.retain(|tool| default_set.contains(tool)); @@ -153,6 +168,7 @@ fn stored_mode_from_overrides( && added_tools.is_empty() && removed_tools.is_empty() && disabled_user_skills.is_empty() + && enabled_user_skills.is_empty() { return None; } @@ -163,6 +179,7 @@ fn stored_mode_from_overrides( removed_tools, enabled, disabled_user_skills, + enabled_user_skills, }) } @@ -175,9 +192,14 @@ fn build_mode_view( let default_tools = normalize_tools(default_tools, valid_tools); let enabled_tools = resolve_effective_tools(&default_tools, mode_config, valid_tools); let enabled = mode_config.map(|config| config.enabled).unwrap_or(true); - let disabled_user_skills = mode_config - .map(|config| normalize_skill_keys(config.disabled_user_skills.clone())) - .unwrap_or_default(); + let (disabled_user_skills, enabled_user_skills) = mode_config + .map(|config| { + normalize_skill_override_lists( + config.disabled_user_skills.clone(), + config.enabled_user_skills.clone(), + ) + }) + .unwrap_or_else(|| (Vec::new(), Vec::new())); ModeConfigView { mode_id: mode_id.to_string(), @@ -185,6 +207,7 @@ fn build_mode_view( default_tools, enabled, disabled_user_skills, + enabled_user_skills, } } @@ -214,6 +237,7 @@ fn canonicalize_mode_config( stored.added_tools, stored.removed_tools, stored.disabled_user_skills, + stored.enabled_user_skills, default_tools, valid_tools, )) @@ -316,12 +340,34 @@ pub async fn persist_mode_config_from_value(mode_id: &str, config: Value) -> Bit .map(|item| item.disabled_user_skills.clone()) .unwrap_or_default() }; + let enabled_user_skills = if config + .as_object() + .map(|obj| obj.contains_key("enabled_user_skills")) + .unwrap_or(false) + { + match config.get("enabled_user_skills") { + Some(Value::Null) | None => Vec::new(), + Some(value) => { + serde_json::from_value::>(value.clone()).map_err(|error| { + BitFunError::config(format!( + "Invalid enabled_user_skills for mode '{}': {}", + mode_id, error + )) + })? + } + } + } else { + current + .map(|item| item.enabled_user_skills.clone()) + .unwrap_or_default() + }; if let Some(canonical) = stored_mode_from_enabled_tools( mode_id, enabled, enabled_tools, disabled_user_skills, + enabled_user_skills, default_tools, &valid_tools, ) { @@ -405,3 +451,49 @@ pub async fn canonicalize_mode_configs() -> BitFunResult, + + /// User-level built-in skills explicitly enabled even though the mode default disables them. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub enabled_user_skills: Vec, } /// API view of a mode configuration. @@ -506,6 +510,8 @@ pub struct ModeConfigView { pub enabled: bool, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub disabled_user_skills: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub enabled_user_skills: Vec, } fn default_true() -> bool { @@ -534,6 +540,7 @@ impl Default for ModeConfig { removed_tools: Vec::new(), enabled: true, disabled_user_skills: Vec::new(), + enabled_user_skills: Vec::new(), } } } @@ -546,6 +553,7 @@ impl Default for ModeConfigView { default_tools: Vec::new(), enabled: true, disabled_user_skills: Vec::new(), + enabled_user_skills: Vec::new(), } } } diff --git a/src/web-ui/src/app/scenes/agents/AgentsScene.tsx b/src/web-ui/src/app/scenes/agents/AgentsScene.tsx index 41adef9a..c83fe3aa 100644 --- a/src/web-ui/src/app/scenes/agents/AgentsScene.tsx +++ b/src/web-ui/src/app/scenes/agents/AgentsScene.tsx @@ -44,6 +44,10 @@ function getConfiguredEnabledSkillKeys(skills: ModeSkillInfo[]): string[] { return skills.filter((skill) => !skill.disabledByMode).map((skill) => skill.key); } +function modeHasSkillTool(enabledTools: string[]): boolean { + return enabledTools.includes('Skill'); +} + function buildDuplicateSkillNameSet(skills: ModeSkillInfo[]): Set { const counts = new Map(); for (const skill of skills) { @@ -169,6 +173,9 @@ const AgentsHomeView: React.FC = () => { const selectedAgentTools = selectedAgent?.agentKind === 'mode' ? (selectedAgentModeConfig?.enabled_tools ?? selectedAgent.defaultTools ?? []) : (selectedAgent?.defaultTools ?? []); + const selectedAgentHasSkillTool = selectedAgent?.agentKind === 'mode' + ? modeHasSkillTool(selectedAgentTools) + : false; const selectedAgentSkills = useMemo( () => getConfiguredEnabledSkillKeys(selectedAgentModeSkills), [selectedAgentModeSkills], @@ -317,7 +324,9 @@ const AgentsHomeView: React.FC = () => { index={index} meta={coreAgentMeta[agent.id] ?? { role: agent.name, accentColor: '#6366f1', accentBg: 'rgba(99,102,241,0.10)' }} toolCount={getDisplayedToolCount(agent)} - skillCount={agent.agentKind === 'mode' ? getConfiguredEnabledSkillKeys(getModeSkills(agent.id)).length : 0} + skillCount={agent.agentKind === 'mode' && modeHasSkillTool(getModeConfig(agent.id)?.enabled_tools ?? agent.defaultTools ?? []) + ? getConfiguredEnabledSkillKeys(getModeSkills(agent.id)).length + : 0} onOpenDetails={openAgentDetails} /> ))} @@ -401,7 +410,9 @@ const AgentsHomeView: React.FC = () => { index={index} soloEnabled={agentSoloEnabled[agent.id] ?? agent.enabled} toolCount={getDisplayedToolCount(agent)} - skillCount={agent.agentKind === 'mode' ? getConfiguredEnabledSkillKeys(getModeSkills(agent.id)).length : 0} + skillCount={agent.agentKind === 'mode' && modeHasSkillTool(getModeConfig(agent.id)?.enabled_tools ?? agent.defaultTools ?? []) + ? getConfiguredEnabledSkillKeys(getModeSkills(agent.id)).length + : 0} onToggleSolo={setAgentSoloEnabled} onOpenDetails={openAgentDetails} /> @@ -434,7 +445,7 @@ const AgentsHomeView: React.FC = () => { meta={selectedAgent ? ( <> {t('agentCard.meta.tools', { count: selectedAgentToolCount })} - {selectedAgent.agentKind === 'mode' ? ( + {selectedAgent.agentKind === 'mode' && selectedAgentHasSkillTool ? ( {t('agentCard.meta.skills', { count: selectedAgentSkills.length })} ) : null} @@ -587,7 +598,7 @@ const AgentsHomeView: React.FC = () => { ) : null} - {selectedAgent.agentKind === 'mode' && selectedAgentModeSkills.length > 0 ? ( + {selectedAgent.agentKind === 'mode' && selectedAgentHasSkillTool && selectedAgentModeSkills.length > 0 ? (
diff --git a/src/web-ui/src/app/scenes/agents/hooks/useAgentsList.ts b/src/web-ui/src/app/scenes/agents/hooks/useAgentsList.ts index 09ca22a9..51072848 100644 --- a/src/web-ui/src/app/scenes/agents/hooks/useAgentsList.ts +++ b/src/web-ui/src/app/scenes/agents/hooks/useAgentsList.ts @@ -187,23 +187,11 @@ export function useAgentsList({ const handleSetSkills = useCallback(async (agentId: string, enabledSkillKeys: string[]) => { try { - const currentSkills = getModeSkills(agentId); - const nextEnabled = new Set(enabledSkillKeys); - - for (const skill of currentSkills) { - const shouldBeEnabled = nextEnabled.has(skill.key); - const isEnabled = !skill.disabledByMode; - if (shouldBeEnabled === isEnabled) { - continue; - } - - await configAPI.setModeSkillDisabled({ - modeId: agentId, - skillKey: skill.key, - disabled: !shouldBeEnabled, - workspacePath: workspacePath || undefined, - }); - } + await configAPI.replaceModeSkillSelection({ + modeId: agentId, + enabledSkillKeys, + workspacePath: workspacePath || undefined, + }); const updatedSkills = await configAPI.getModeSkillConfigs({ modeId: agentId, diff --git a/src/web-ui/src/infrastructure/api/service-api/ConfigAPI.ts b/src/web-ui/src/infrastructure/api/service-api/ConfigAPI.ts index 26d1e888..2065b964 100644 --- a/src/web-ui/src/infrastructure/api/service-api/ConfigAPI.ts +++ b/src/web-ui/src/infrastructure/api/service-api/ConfigAPI.ts @@ -31,6 +31,12 @@ export interface SetModeSkillDisabledParams { workspacePath?: string; } +export interface ReplaceModeSkillSelectionParams { + modeId: string; + enabledSkillKeys: string[]; + workspacePath?: string; +} + export interface AddSkillParams { sourcePath: string; level: SkillLevel; @@ -279,6 +285,24 @@ export class ConfigAPI { } } + async replaceModeSkillSelection({ + modeId, + enabledSkillKeys, + workspacePath, + }: ReplaceModeSkillSelectionParams): Promise { + try { + return await api.invoke('replace_mode_skill_selection', { + request: { modeId, enabledSkillKeys, workspacePath }, + }); + } catch (error) { + throw createTauriCommandError('replace_mode_skill_selection', error, { + modeId, + enabledSkillKeys, + workspacePath, + }); + } + } + async validateSkillPath(path: string): Promise { try { diff --git a/src/web-ui/src/infrastructure/config/types/index.ts b/src/web-ui/src/infrastructure/config/types/index.ts index 71da3dc4..664ce92a 100644 --- a/src/web-ui/src/infrastructure/config/types/index.ts +++ b/src/web-ui/src/infrastructure/config/types/index.ts @@ -1,4 +1,3 @@ - import { i18nService } from '@/infrastructure/i18n'; const t = (key: string, options?: Record) => i18nService.t(key, options); @@ -9,7 +8,7 @@ export interface GlobalConfig { workspace: WorkspaceConfig; ai: AIConfig; version: string; - last_modified: number; + last_modified: number; } export interface AppConfig { @@ -65,13 +64,11 @@ export interface AIExperienceConfig { enable_agent_companion: boolean; } - - export type ModelCapability = | 'text_chat' | 'function_calling'; -export type ModelCategory = +export type ModelCategory = | 'general_chat' | 'multimodal'; @@ -82,61 +79,49 @@ export interface ModelMetadata { strengths?: string[]; } - export const CATEGORY_LABELS: Record = { general_chat: t('settings/ai-model:category.general_chat'), multimodal: t('settings/ai-model:category.multimodal') }; - export const CATEGORY_ICONS: Record = { general_chat: t('settings/ai-model:categoryIcons.general_chat'), multimodal: t('settings/ai-model:categoryIcons.multimodal') }; - export type CustomHeadersMode = 'replace' | 'merge'; - export interface AIModelConfig { id?: string; name: string; provider: string; - api_key?: string; + api_key?: string; base_url: string; /** Computed actual request URL, derived from base_url + provider format. Stored on save. */ request_url?: string; - model_name: string; - description?: string; - context_window?: number; - max_tokens?: number; + model_name: string; + description?: string; + context_window?: number; + max_tokens?: number; temperature?: number; - top_p?: number; - frequency_penalty?: number; - presence_penalty?: number; + top_p?: number; + frequency_penalty?: number; + presence_penalty?: number; enabled: boolean; - is_default?: boolean; - custom_headers?: Record; - custom_headers_mode?: CustomHeadersMode; - skip_ssl_verify?: boolean; - custom_request_body?: string; + is_default?: boolean; + custom_headers?: Record; + custom_headers_mode?: CustomHeadersMode; + skip_ssl_verify?: boolean; + custom_request_body?: string; timeout?: number; - - category: ModelCategory; capabilities: ModelCapability[]; recommended_for?: string[]; metadata?: Record; - - enable_thinking_process?: boolean; - - support_preserved_thinking?: boolean; - /** Parse `...` text chunks into streaming reasoning content. */ inline_think_in_text?: boolean; - /** Reasoning effort for OpenAI Responses API ("low" | "medium" | "high" | "xhigh") */ reasoning_effort?: string; } @@ -148,23 +133,20 @@ export interface ProxyConfig { password?: string; } - export interface DefaultModelsConfig { - primary?: string | null; - fast?: string | null; } export interface AIConfig { - models: AIModelConfig[]; - default_models: DefaultModelsConfig; - agent_models: Record; - func_agent_models: Record; - mode_configs: Record; - subagent_configs: Record; - proxy: ProxyConfig; - debug_mode_config: DebugModeConfig; + models: AIModelConfig[]; + default_models: DefaultModelsConfig; + agent_models: Record; + func_agent_models: Record; + mode_configs: Record; + subagent_configs: Record; + proxy: ProxyConfig; + debug_mode_config: DebugModeConfig; request_timeout: number; max_retries: number; temperature: number; @@ -178,43 +160,39 @@ export interface AIConfig { computer_use_enabled?: boolean; } - - - export interface StoredModeConfigItem { mode_id: string; added_tools: string[]; removed_tools: string[]; enabled: boolean; disabled_user_skills?: string[]; + enabled_user_skills?: string[]; } export interface ModeConfigItem { - mode_id: string; - enabled_tools: string[]; - enabled: boolean; - default_tools: string[]; + mode_id: string; + enabled_tools: string[]; + enabled: boolean; + default_tools: string[]; disabled_user_skills?: string[]; + enabled_user_skills?: string[]; } - export interface SubAgentConfigItem { - enabled: boolean; + enabled: boolean; } - - export type SkillLevel = 'user' | 'project'; - export interface SkillInfo { key: string; - name: string; - description: string; - path: string; + name: string; + description: string; + path: string; level: SkillLevel; sourceSlot: string; dirName: string; + isBuiltin: boolean; } export interface ModeSkillInfo extends SkillInfo { @@ -241,39 +219,24 @@ export interface SkillMarketDownloadResult { output: string; } - - - export interface DebugModeConfig { - log_path: string; - ingest_port: number; - enabled_languages: string[]; - language_templates: Record; } - + export interface LanguageDebugTemplate { - language: string; - display_name: string; - enabled: boolean; - instrumentation_template: string; - region_start: string; - region_end: string; - notes: string[]; } - export const DEFAULT_DEBUG_MODE_CONFIG: DebugModeConfig = { log_path: '.bitfun/debug.log', ingest_port: 7242, @@ -281,7 +244,6 @@ export const DEFAULT_DEBUG_MODE_CONFIG: DebugModeConfig = { language_templates: {} }; - export const LANGUAGE_TEMPLATE_LABELS: Record = { javascript: t('settings/debug:languageLabels.javascript'), python: t('settings/debug:languageLabels.python'), @@ -290,15 +252,13 @@ export const LANGUAGE_TEMPLATE_LABELS: Record = { java: t('settings/debug:languageLabels.java') }; - export const ALL_LANGUAGES = ['javascript', 'python', 'rust', 'go', 'java'] as const; - export const DEFAULT_LANGUAGE_TEMPLATES: Record = { javascript: { language: 'javascript', display_name: t('settings/debug:languageLabels.javascript'), - enabled: false, + enabled: false, instrumentation_template: `fetch('http://127.0.0.1:{PORT}/ingest/{SESSION_ID}',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'{LOCATION}',message:'{MESSAGE}',data:{DATA},timestamp:Date.now(),sessionId:'{SESSION_ID}',hypothesisId:'{HYPOTHESIS_ID}',runId:'{RUN_ID}'})}).catch(()=>{});`, region_start: '// #region agent log', region_end: '// #endregion', @@ -388,7 +348,6 @@ with open(os.path.join(os.getcwd(), '{LOG_PATH}'), 'a', encoding='utf-8') as _f: }, }; - export interface SkillValidationResult { valid: boolean; name?: string; @@ -396,34 +355,30 @@ export interface SkillValidationResult { error?: string; } - export interface EditorConfig { - font_size: number; - font_family: string; - font_weight?: 'normal' | 'bold'; - line_height: number; - tab_size: number; - insert_spaces: boolean; - word_wrap: string; - line_numbers: string; + font_size: number; + font_family: string; + font_weight?: 'normal' | 'bold'; + line_height: number; + tab_size: number; + insert_spaces: boolean; + word_wrap: string; + line_numbers: string; minimap: MinimapConfig; theme: string; - auto_save: string; - auto_save_delay: number; - format_on_save: boolean; - format_on_paste: boolean; - trim_auto_whitespace: boolean; - - cursor_style?: string; - cursor_blinking?: string; - render_whitespace?: string; - render_line_highlight?: string; - - smooth_scrolling?: boolean; - scroll_beyond_last_line?: boolean; - - semantic_highlighting?: boolean; - bracket_pair_colorization?: boolean; + auto_save: string; + auto_save_delay: number; + format_on_save: boolean; + format_on_paste: boolean; + trim_auto_whitespace: boolean; + cursor_style?: string; + cursor_blinking?: string; + render_whitespace?: string; + render_line_highlight?: string; + smooth_scrolling?: boolean; + scroll_beyond_last_line?: boolean; + semantic_highlighting?: boolean; + bracket_pair_colorization?: boolean; } export interface MinimapConfig { @@ -432,25 +387,23 @@ export interface MinimapConfig { size?: string; } - export interface TerminalConfig { - default_shell: string; - font_size: number; - font_family: string; - cursor_style: string; - cursor_blink: boolean; - scrollback_lines: number; + default_shell: string; + font_size: number; + font_family: string; + cursor_style: string; + cursor_blink: boolean; + scrollback_lines: number; theme: string; transparency: number; - bell_style: string; - copy_on_select: boolean; - paste_on_right_click: boolean; - confirm_on_exit: boolean; - startup_command: string; - env_vars: Record; + bell_style: string; + copy_on_select: boolean; + paste_on_right_click: boolean; + confirm_on_exit: boolean; + startup_command: string; + env_vars: Record; } - export interface WorkspaceConfig { recent_workspaces: string[]; max_recent_workspaces: number; @@ -462,23 +415,14 @@ export interface WorkspaceConfig { search_exclude_patterns: string[]; } - - export interface IConfigManager { - getConfig(path?: string): Promise; setConfig(path: string, value: T): Promise; resetConfig(path?: string): Promise; - - validateConfig(): Promise; exportConfig(): Promise; importConfig(config: ConfigExport): Promise; - - onConfigChange(callback: (path: string, oldValue: any, newValue: any) => void): () => void; - - refreshCache(): Promise; clearCache(): void; } @@ -510,8 +454,6 @@ export interface ConfigExport { }; } - - export interface ConfigChangeEvent { path: string; old_value: any; @@ -519,8 +461,6 @@ export interface ConfigChangeEvent { timestamp: number; } - - export interface UseConfigReturn { data: T | null; loading: boolean; @@ -530,9 +470,7 @@ export interface UseConfigReturn { refreshConfig: () => Promise; } - - -export type ConfigPath = +export type ConfigPath = | 'app' | 'app.language' | 'app.auto_update' @@ -551,9 +489,7 @@ export type ConfigPath = | 'ai.default_model' | 'ai.models' | 'agents' - | string; - - + | string; export interface ConfigPanelProps { section?: keyof GlobalConfig; @@ -570,15 +506,9 @@ export interface RuntimeLoggingInfo { webviewLogPath: string; } - - - export interface DefaultModels { - primary: string | null; - fast: string | null; } - -export type OptionalCapabilityModels = Record; +export type OptionalCapabilityModels = Record; \ No newline at end of file From eb9aa1180688b53ad6048a4a5e9ba6baffba95d9 Mon Sep 17 00:00:00 2001 From: wsp1911 Date: Wed, 8 Apr 2026 21:01:01 +0800 Subject: [PATCH 2/2] feat(skills): add builtin skill grouping and grouped agent controls - add builtin skill group metadata for office, meta, computer-use, and superpowers - render agent skills by group and support group-level enable/disable in Agents scene - add localized group labels and update skill grouping tests - fix MCP prompt block tests and normalize path assertions for cross-platform lib tests --- .../tools/implementations/skills/builtin.rs | 29 ++ .../skills/default_profiles.rs | 2 + .../tools/implementations/skills/registry.rs | 10 +- .../tools/implementations/skills/types.rs | 3 + .../core/src/agentic/tools/workspace_paths.rs | 2 +- .../service/mcp/protocol/transport_remote.rs | 19 +- .../src/service/remote_connect/bot/mod.rs | 4 +- .../src/service/remote_ssh/workspace_state.rs | 17 +- .../src/app/scenes/agents/AgentsScene.tsx | 262 ++++++++++++++---- .../scenes/agents/components/AgentCard.scss | 60 ++++ .../app/scenes/agents/hooks/useAgentsList.ts | 2 +- .../src/infrastructure/config/types/index.ts | 3 +- .../src/locales/en-US/scenes/agents.json | 11 + .../src/locales/zh-CN/scenes/agents.json | 11 + .../components/FileSearchResults.tsx | 2 +- 15 files changed, 370 insertions(+), 67 deletions(-) diff --git a/src/crates/core/src/agentic/tools/implementations/skills/builtin.rs b/src/crates/core/src/agentic/tools/implementations/skills/builtin.rs index 3a61d86c..d4b2ce46 100644 --- a/src/crates/core/src/agentic/tools/implementations/skills/builtin.rs +++ b/src/crates/core/src/agentic/tools/implementations/skills/builtin.rs @@ -39,6 +39,15 @@ pub fn is_builtin_skill_dir_name(dir_name: &str) -> bool { builtin_skill_dir_names().contains(dir_name) } +pub fn builtin_skill_group_key(dir_name: &str) -> Option<&'static str> { + match dir_name { + "docx" | "pdf" | "pptx" | "xlsx" => Some("office"), + "find-skills" | "writing-skills" => Some("meta"), + "agent-browser" => Some("computer-use"), + _ => Some("superpowers"), + } +} + pub async fn ensure_builtin_skills_installed() -> BitFunResult<()> { let pm = get_path_manager_arc(); let dest_root = pm.user_skills_dir(); @@ -151,3 +160,23 @@ async fn desired_file_content( ) -> BitFunResult> { Ok(file.contents().to_vec()) } + +#[cfg(test)] +mod tests { + use super::builtin_skill_group_key; + + #[test] + fn builtin_skill_groups_match_expected_sets() { + assert_eq!(builtin_skill_group_key("docx"), Some("office")); + assert_eq!(builtin_skill_group_key("pdf"), Some("office")); + assert_eq!(builtin_skill_group_key("pptx"), Some("office")); + assert_eq!(builtin_skill_group_key("xlsx"), Some("office")); + assert_eq!(builtin_skill_group_key("find-skills"), Some("meta")); + assert_eq!(builtin_skill_group_key("writing-skills"), Some("meta")); + assert_eq!(builtin_skill_group_key("agent-browser"), Some("computer-use")); + assert_eq!( + builtin_skill_group_key("test-driven-development"), + Some("superpowers") + ); + } +} diff --git a/src/crates/core/src/agentic/tools/implementations/skills/default_profiles.rs b/src/crates/core/src/agentic/tools/implementations/skills/default_profiles.rs index e3f83146..112fc7ba 100644 --- a/src/crates/core/src/agentic/tools/implementations/skills/default_profiles.rs +++ b/src/crates/core/src/agentic/tools/implementations/skills/default_profiles.rs @@ -98,6 +98,7 @@ mod tests { source_slot: "bitfun".to_string(), dir_name: dir_name.to_string(), is_builtin: true, + group_key: None, } } @@ -111,6 +112,7 @@ mod tests { source_slot: "bitfun".to_string(), dir_name: dir_name.to_string(), is_builtin: false, + group_key: None, } } diff --git a/src/crates/core/src/agentic/tools/implementations/skills/registry.rs b/src/crates/core/src/agentic/tools/implementations/skills/registry.rs index db44bf8f..ac5c6a7a 100644 --- a/src/crates/core/src/agentic/tools/implementations/skills/registry.rs +++ b/src/crates/core/src/agentic/tools/implementations/skills/registry.rs @@ -2,7 +2,9 @@ //! //! Manages skill discovery, mode-specific filtering, and loading. -use super::builtin::{ensure_builtin_skills_installed, is_builtin_skill_dir_name}; +use super::builtin::{ + builtin_skill_group_key, ensure_builtin_skills_installed, is_builtin_skill_dir_name, +}; use super::default_profiles::is_skill_enabled_for_mode; use super::mode_overrides::{ load_disabled_mode_skills_local, load_disabled_mode_skills_remote, @@ -77,6 +79,11 @@ impl SkillCandidate { let is_builtin = data.location == SkillLocation::User && slot == "bitfun" && is_builtin_skill_dir_name(&data.dir_name); + let group_key = if is_builtin { + builtin_skill_group_key(&data.dir_name).map(str::to_string) + } else { + None + }; Self { info: SkillInfo { @@ -88,6 +95,7 @@ impl SkillCandidate { source_slot: data.source_slot, dir_name: data.dir_name, is_builtin, + group_key, }, priority, } diff --git a/src/crates/core/src/agentic/tools/implementations/skills/types.rs b/src/crates/core/src/agentic/tools/implementations/skills/types.rs index 5c548702..3a33ec41 100644 --- a/src/crates/core/src/agentic/tools/implementations/skills/types.rs +++ b/src/crates/core/src/agentic/tools/implementations/skills/types.rs @@ -44,6 +44,9 @@ pub struct SkillInfo { /// Whether this skill is bundled with BitFun as a built-in skill. #[serde(default)] pub is_builtin: bool, + /// Optional logical group for built-in skills. + #[serde(default)] + pub group_key: Option, } impl SkillInfo { diff --git a/src/crates/core/src/agentic/tools/workspace_paths.rs b/src/crates/core/src/agentic/tools/workspace_paths.rs index 524b7b36..63fc93a7 100644 --- a/src/crates/core/src/agentic/tools/workspace_paths.rs +++ b/src/crates/core/src/agentic/tools/workspace_paths.rs @@ -133,7 +133,7 @@ mod tests { let resolved = resolve_path_with_workspace("src/main.rs", Some(Path::new("/repo"))) .expect("path should resolve"); - assert_eq!(resolved, "/repo/src/main.rs"); + assert_eq!(PathBuf::from(resolved), Path::new("/repo").join("src/main.rs")); } #[test] diff --git a/src/crates/core/src/service/mcp/protocol/transport_remote.rs b/src/crates/core/src/service/mcp/protocol/transport_remote.rs index fcabdc1e..b5d569b7 100644 --- a/src/crates/core/src/service/mcp/protocol/transport_remote.rs +++ b/src/crates/core/src/service/mcp/protocol/transport_remote.rs @@ -1144,7 +1144,8 @@ mod tests { let mapped = map_prompt_message(prompt_message); assert!(matches!( mapped.content, - MCPPromptMessageContent::Block(MCPPromptMessageContentBlock::Text { ref text }) if text == "hello" + MCPPromptMessageContent::Block(ref block) + if matches!(block.as_ref(), MCPPromptMessageContentBlock::Text { text } if text == "hello") )); let resource_link = RawResource { @@ -1167,8 +1168,12 @@ mod tests { let mapped = map_prompt_message(prompt_message); assert!(matches!( mapped.content, - MCPPromptMessageContent::Block(MCPPromptMessageContentBlock::ResourceLink { ref uri, .. }) - if uri == "file:///tmp/input.md" + MCPPromptMessageContent::Block(ref block) + if matches!( + block.as_ref(), + MCPPromptMessageContentBlock::ResourceLink { uri, .. } + if uri == "file:///tmp/input.md" + ) )); let embedded = rmcp::model::RawEmbeddedResource { @@ -1188,8 +1193,12 @@ mod tests { let mapped = map_prompt_message(prompt_message); assert!(matches!( mapped.content, - MCPPromptMessageContent::Block(MCPPromptMessageContentBlock::Resource { ref resource }) - if resource.uri == "file:///tmp/embedded.txt" + MCPPromptMessageContent::Block(ref block) + if matches!( + block.as_ref(), + MCPPromptMessageContentBlock::Resource { resource } + if resource.uri == "file:///tmp/embedded.txt" + ) )); } } diff --git a/src/crates/core/src/service/remote_connect/bot/mod.rs b/src/crates/core/src/service/remote_connect/bot/mod.rs index fa11ac7d..2ee11f23 100644 --- a/src/crates/core/src/service/remote_connect/bot/mod.rs +++ b/src/crates/core/src/service/remote_connect/bot/mod.rs @@ -646,7 +646,9 @@ mod tests { assert_eq!(paths.len(), 1); assert!(std::path::Path::new(&paths[0]).is_absolute()); - assert!(paths[0].ends_with("artifacts/report.pptx")); + assert!(std::path::Path::new(&paths[0]).ends_with( + std::path::Path::new("artifacts").join("report.pptx") + )); assert!(std::path::Path::new(&paths[0]).exists()); let _ = std::fs::remove_dir_all(base); } diff --git a/src/crates/core/src/service/remote_ssh/workspace_state.rs b/src/crates/core/src/service/remote_ssh/workspace_state.rs index 93885c53..c3838567 100644 --- a/src/crates/core/src/service/remote_ssh/workspace_state.rs +++ b/src/crates/core/src/service/remote_ssh/workspace_state.rs @@ -422,10 +422,19 @@ impl RemoteWorkspaceStateManager { // Assistant sessions use client-local paths under ~/.bitfun/personal_assistant. // A registered remote root of `/` matches every absolute path; without an explicit // `remote_connection_id`, those paths must not be treated as SSH workspaces. - if preferred_connection_id.is_none() - && get_path_manager_arc().is_local_assistant_workspace_path(path) - { - return None; + let is_local_assistant_path = get_path_manager_arc().is_local_assistant_workspace_path(path); + if is_local_assistant_path { + let preferred_connection_id = preferred_connection_id?; + let guard = self.registrations.read().await; + let registration = guard + .iter() + .find(|r| r.connection_id == preferred_connection_id)?; + return Some(RemoteWorkspaceEntry { + connection_id: registration.connection_id.clone(), + connection_name: registration.connection_name.clone(), + ssh_host: registration.ssh_host.clone(), + remote_root: registration.remote_root.clone(), + }); } let path_norm = normalize_remote_workspace_path(path); diff --git a/src/web-ui/src/app/scenes/agents/AgentsScene.tsx b/src/web-ui/src/app/scenes/agents/AgentsScene.tsx index c83fe3aa..b4b3c909 100644 --- a/src/web-ui/src/app/scenes/agents/AgentsScene.tsx +++ b/src/web-ui/src/app/scenes/agents/AgentsScene.tsx @@ -1,4 +1,5 @@ import React, { useCallback, useMemo, useState } from 'react'; +import type { TFunction } from 'i18next'; import { Bot, Cpu, @@ -40,6 +41,22 @@ import { SubagentAPI } from '@/infrastructure/api/service-api/SubagentAPI'; import type { ModeSkillInfo } from '@/infrastructure/config/types'; import { useNotification } from '@/shared/notification-system'; +const UNGROUPED_SKILL_GROUP = '__ungrouped__'; + +const SKILL_GROUP_ORDER: Record = { + office: 0, + meta: 1, + [UNGROUPED_SKILL_GROUP]: 99, +}; + +interface SkillGroup { + key: string; + label: string; + skills: ModeSkillInfo[]; + enabledCount: number; + totalCount: number; +} + function getConfiguredEnabledSkillKeys(skills: ModeSkillInfo[]): string[] { return skills.filter((skill) => !skill.disabledByMode).map((skill) => skill.key); } @@ -71,6 +88,76 @@ function formatSkillDisplayName(skill: ModeSkillInfo, duplicateNames: Set): string { + switch (groupKey) { + case 'office': + return t('agentsOverview.skillGroups.office'); + case 'computer-use': + return t('agentsOverview.skillGroups.computerUse'); + case 'meta': + return t('agentsOverview.skillGroups.meta'); + case 'superpowers': + return t('agentsOverview.skillGroups.superpowers'); + default: + return t('agentsOverview.skillGroups.other'); + } +} + +function getSkillTitle(skill: ModeSkillInfo, t: TFunction<'scenes/agents'>): string { + return [ + skill.description || skill.name, + `key: ${skill.key}`, + !skill.disabledByMode && !skill.selectedForRuntime + ? t('agentsOverview.skillShadowed') + : null, + ].filter(Boolean).join('\n'); +} + +function buildSkillGroups( + skills: ModeSkillInfo[], + enabledSkillKeys: string[], + t: TFunction<'scenes/agents'>, +): SkillGroup[] { + const enabledSkillKeySet = new Set(enabledSkillKeys); + const groups = new Map(); + + for (const skill of skills) { + const groupKey = getSkillGroupKey(skill); + const items = groups.get(groupKey); + if (items) { + items.push(skill); + } else { + groups.set(groupKey, [skill]); + } + } + + return [...groups.entries()] + .map(([groupKey, groupSkills]) => ({ + key: groupKey, + label: getSkillGroupLabel(groupKey, t), + skills: [...groupSkills].sort((a, b) => { + const aEnabled = enabledSkillKeySet.has(a.key); + const bEnabled = enabledSkillKeySet.has(b.key); + if (aEnabled && !bEnabled) return -1; + if (!aEnabled && bEnabled) return 1; + return a.name.localeCompare(b.name) || a.key.localeCompare(b.key); + }), + enabledCount: groupSkills.filter((skill) => enabledSkillKeySet.has(skill.key)).length, + totalCount: groupSkills.length, + })) + .sort((a, b) => { + const orderDiff = (SKILL_GROUP_ORDER[a.key] ?? 50) - (SKILL_GROUP_ORDER[b.key] ?? 50); + if (orderDiff !== 0) { + return orderDiff; + } + return a.label.localeCompare(b.label); + }); +} + const AgentsHomeView: React.FC = () => { const { t } = useTranslation('scenes/agents'); const notification = useNotification(); @@ -184,6 +271,14 @@ const AgentsHomeView: React.FC = () => { () => selectedAgentModeSkills.filter((skill) => !skill.disabledByMode), [selectedAgentModeSkills], ); + const selectedAgentSkillGroups = useMemo( + () => buildSkillGroups(selectedAgentModeSkills, selectedAgentSkills, t), + [selectedAgentModeSkills, selectedAgentSkills, t], + ); + const editableSkillGroups = useMemo( + () => buildSkillGroups(selectedAgentModeSkills, pendingSkills ?? selectedAgentSkills, t), + [pendingSkills, selectedAgentModeSkills, selectedAgentSkills, t], + ); const selectedAgentDuplicateSkillNames = useMemo( () => buildDuplicateSkillNameSet(selectedAgentModeSkills), [selectedAgentModeSkills], @@ -207,6 +302,34 @@ const AgentsHomeView: React.FC = () => { setSavingSkills(false); }, []); + const togglePendingSkill = useCallback((skillKey: string) => { + setPendingSkills((prev) => { + const current = prev ?? selectedAgentSkills; + return current.includes(skillKey) + ? current.filter((key) => key !== skillKey) + : [...current, skillKey]; + }); + }, [selectedAgentSkills]); + + const setPendingSkillGroupEnabled = useCallback((skills: ModeSkillInfo[], enabled: boolean) => { + setPendingSkills((prev) => { + const current = prev ?? selectedAgentSkills; + const groupKeys = new Set(skills.map((skill) => skill.key)); + + if (!enabled) { + return current.filter((key) => !groupKeys.has(key)); + } + + const next = [...current]; + for (const skill of skills) { + if (!next.includes(skill.key)) { + next.push(skill.key); + } + } + return next; + }); + }, [selectedAgentSkills]); + const openAgentDetails = useCallback((agent: AgentWithCapabilities) => { setSelectedAgentId(agent.id); resetEditState(); @@ -659,65 +782,100 @@ const AgentsHomeView: React.FC = () => {
{skillsEditing ? ( -
- {[...selectedAgentModeSkills] - .sort((a, b) => { - const draft = pendingSkills ?? selectedAgentSkills; - const aOn = draft.includes(a.key); - const bOn = draft.includes(b.key); - if (aOn && !bOn) return -1; - if (!aOn && bOn) return 1; - return 0; - }) - .map((skill) => { - const draft = pendingSkills ?? selectedAgentSkills; - const isOn = draft.includes(skill.key); - const displayName = formatSkillDisplayName(skill, selectedAgentDuplicateSkillNames); - const title = [ - skill.description || skill.name, - `key: ${skill.key}`, - !skill.disabledByMode && !skill.selectedForRuntime ? 'shadowed by a higher-priority skill with the same name' : null, - ].filter(Boolean).join('\n'); - return ( - - ); - })} +
+ {editableSkillGroups.map((group) => { + const allEnabled = group.enabledCount === group.totalCount; + const someEnabled = group.enabledCount > 0; + + return ( +
+
+
+ {group.label} + + {`${group.enabledCount}/${group.totalCount}`} + +
+
+ + {someEnabled && !allEnabled ? ( + + ) : null} +
+
+
+ {group.skills.map((skill) => { + const isOn = (pendingSkills ?? selectedAgentSkills).includes(skill.key); + const displayName = formatSkillDisplayName( + skill, + selectedAgentDuplicateSkillNames, + ); + + return ( + + ); + })} +
+
+ ); + })}
) : ( -
+
{selectedAgentSkillItems.length === 0 ? ( {t('agentsOverview.noSkills')} ) : ( - selectedAgentSkillItems.map((skill) => ( - - {formatSkillDisplayName(skill, selectedAgentDuplicateSkillNames)} - - )) + selectedAgentSkillGroups + .filter((group) => group.enabledCount > 0) + .map((group) => ( +
+
+
+ {group.label} + + {group.enabledCount} + +
+
+
+ {group.skills + .filter((skill) => !skill.disabledByMode) + .map((skill) => ( + + {formatSkillDisplayName(skill, selectedAgentDuplicateSkillNames)} + + ))} +
+
+ )) )}
)} diff --git a/src/web-ui/src/app/scenes/agents/components/AgentCard.scss b/src/web-ui/src/app/scenes/agents/components/AgentCard.scss index 324be254..1b7d012d 100644 --- a/src/web-ui/src/app/scenes/agents/components/AgentCard.scss +++ b/src/web-ui/src/app/scenes/agents/components/AgentCard.scss @@ -324,6 +324,66 @@ flex-shrink: 0; } + &__skill-groups { + display: flex; + flex-direction: column; + gap: $size-gap-2; + } + + &__skill-group { + display: flex; + flex-direction: column; + gap: $size-gap-1; + padding: $size-gap-2; + border: 1px solid var(--border-subtle); + border-radius: $size-radius-base; + background: var(--element-bg-subtle); + } + + &__skill-group-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: $size-gap-2; + min-height: 26px; + } + + &__skill-group-title-wrap { + display: inline-flex; + align-items: center; + gap: $size-gap-2; + min-width: 0; + } + + &__skill-group-title { + font-size: 11px; + font-weight: $font-weight-semibold; + color: var(--color-text-secondary); + text-transform: uppercase; + letter-spacing: 0.4px; + } + + &__skill-group-count { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 22px; + padding: 1px 6px; + border-radius: $size-radius-full; + background: var(--element-bg-medium); + font-size: 10px; + color: var(--color-text-muted); + font-variant-numeric: tabular-nums; + } + + &__skill-group-actions { + display: flex; + align-items: center; + gap: $size-gap-1; + flex-wrap: wrap; + justify-content: flex-end; + } + // ── Read-only chip grid ────────────────────────────────────────────────── &__chip-grid { display: flex; diff --git a/src/web-ui/src/app/scenes/agents/hooks/useAgentsList.ts b/src/web-ui/src/app/scenes/agents/hooks/useAgentsList.ts index 51072848..95ef4669 100644 --- a/src/web-ui/src/app/scenes/agents/hooks/useAgentsList.ts +++ b/src/web-ui/src/app/scenes/agents/hooks/useAgentsList.ts @@ -208,7 +208,7 @@ export function useAgentsList({ } catch { notification.error(t('agentsOverview.skillToggleFailed', 'Skill 切换失败')); } - }, [getModeSkills, notification, t, workspacePath]); + }, [notification, t, workspacePath]); const filteredAgents = useMemo(() => allAgents.filter((agent) => { if (searchQuery) { diff --git a/src/web-ui/src/infrastructure/config/types/index.ts b/src/web-ui/src/infrastructure/config/types/index.ts index 664ce92a..005a315d 100644 --- a/src/web-ui/src/infrastructure/config/types/index.ts +++ b/src/web-ui/src/infrastructure/config/types/index.ts @@ -193,6 +193,7 @@ export interface SkillInfo { sourceSlot: string; dirName: string; isBuiltin: boolean; + groupKey?: string | null; } export interface ModeSkillInfo extends SkillInfo { @@ -511,4 +512,4 @@ export interface DefaultModels { fast: string | null; } -export type OptionalCapabilityModels = Record; \ No newline at end of file +export type OptionalCapabilityModels = Record; diff --git a/src/web-ui/src/locales/en-US/scenes/agents.json b/src/web-ui/src/locales/en-US/scenes/agents.json index 4d14d5f7..f2e051c9 100644 --- a/src/web-ui/src/locales/en-US/scenes/agents.json +++ b/src/web-ui/src/locales/en-US/scenes/agents.json @@ -75,8 +75,19 @@ "skillsCancel": "Cancel editing", "skillsSave": "Save", "skillsEdit": "Manage skills", + "enableGroup": "Enable group", + "disableGroup": "Disable group", + "clearGroup": "Clear", "skillToggleFailed": "Failed to toggle skill", + "skillShadowed": "shadowed by a higher-priority skill with the same name", "noSkills": "No skills enabled", + "skillGroups": { + "computerUse": "Computer Use", + "office": "Office", + "meta": "Meta", + "superpowers": "Superpowers", + "other": "Other" + }, "customActions": "Custom agent", "editAgent": "Edit", "deleteAgent": "Delete", diff --git a/src/web-ui/src/locales/zh-CN/scenes/agents.json b/src/web-ui/src/locales/zh-CN/scenes/agents.json index 4d71207c..3b4f12b9 100644 --- a/src/web-ui/src/locales/zh-CN/scenes/agents.json +++ b/src/web-ui/src/locales/zh-CN/scenes/agents.json @@ -75,8 +75,19 @@ "skillsCancel": "取消编辑", "skillsSave": "保存", "skillsEdit": "管理 Skills", + "enableGroup": "启用分组", + "disableGroup": "禁用分组", + "clearGroup": "清空", "skillToggleFailed": "Skill 切换失败", + "skillShadowed": "该 Skill 被同名且优先级更高的 Skill 遮蔽,运行时不会生效", "noSkills": "未启用任何 Skill", + "skillGroups": { + "computerUse": "Computer Use", + "office": "Office", + "meta": "Meta", + "superpowers": "Superpowers", + "other": "其他" + }, "customActions": "自定义 Agent", "editAgent": "编辑", "deleteAgent": "删除", diff --git a/src/web-ui/src/tools/file-system/components/FileSearchResults.tsx b/src/web-ui/src/tools/file-system/components/FileSearchResults.tsx index cf612af2..bc2fa1c8 100644 --- a/src/web-ui/src/tools/file-system/components/FileSearchResults.tsx +++ b/src/web-ui/src/tools/file-system/components/FileSearchResults.tsx @@ -25,7 +25,7 @@ interface FileSearchResultsProps { className?: string; } -interface SearchResultTarget extends FileMentionTarget {} +type SearchResultTarget = FileMentionTarget; interface MatchPreviewSegments { before: string;