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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
185 changes: 36 additions & 149 deletions src/apps/desktop/src/api/config_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -243,127 +243,24 @@ pub async fn get_runtime_logging_info(
}

#[tauri::command]
pub async fn get_mode_configs(state: State<'_, AppState>) -> Result<Value, String> {
use bitfun_core::service::config::types::ModeConfig;
use std::collections::HashMap;

let config_service = &state.config_service;
let mut mode_configs: HashMap<String, ModeConfig> = config_service
.get_config(Some("ai.mode_configs"))
.await
.unwrap_or_default();

let all_modes = state.agent_registry.get_modes_info().await;
let mut needs_save = false;

for mode in all_modes {
let mode_id = mode.id;
let default_tools = mode.default_tools;

if !mode_configs.contains_key(&mode_id) {
let new_config = ModeConfig {
mode_id: mode_id.clone(),
available_tools: default_tools.clone(),
enabled: true,
default_tools: default_tools,
};
mode_configs.insert(mode_id.clone(), new_config);
needs_save = true;
} else if let Some(config) = mode_configs.get_mut(&mode_id) {
config.default_tools = default_tools.clone();
// Migration: add ComputerUse to available_tools when the mode default includes it.
if default_tools.iter().any(|t| t == "ComputerUse")
&& !config.available_tools.iter().any(|t| t == "ComputerUse")
{
config.available_tools.push("ComputerUse".to_string());
needs_save = true;
}
// Migrate older Claw sessions that only allowlisted "ComputerUse" before split mouse tools existed;
// All desktop automation is now consolidated into ComputerUse.
// Remove any stale split tool names from available_tools.
if mode_id == "Claw" {
let stale = ["ComputerUseMousePrecise", "ComputerUseMouseStep", "ComputerUseMouseClick"];
let before = config.available_tools.len();
config.available_tools.retain(|t| !stale.contains(&t.as_str()));
if config.available_tools.len() != before {
needs_save = true;
}
}
}
}

if needs_save {
match to_json_value(&mode_configs, "mode configs") {
Ok(mode_configs_value) => {
if let Err(e) = config_service
.set_config("ai.mode_configs", mode_configs_value)
.await
{
warn!("Failed to save initialized mode configs: {}", e);
}
}
Err(e) => {
warn!("Failed to serialize initialized mode configs: {}", e);
}
}
}
pub async fn get_mode_configs(_state: State<'_, AppState>) -> Result<Value, String> {
let mode_configs =
bitfun_core::service::config::mode_config_canonicalizer::get_mode_config_views()
.await
.map_err(|e| format!("Failed to get mode configs: {}", e))?;

Ok(to_json_value(mode_configs, "mode configs")?)
}

#[tauri::command]
pub async fn get_mode_config(state: State<'_, AppState>, mode_id: String) -> Result<Value, String> {
use bitfun_core::service::config::types::ModeConfig;

let config_service = &state.config_service;
let agent_registry = &state.agent_registry;
let path = format!("ai.mode_configs.{}", mode_id);
let config_result = config_service.get_config::<ModeConfig>(Some(&path)).await;

let config = match config_result {
Ok(existing_config) => {
let mut cfg = existing_config;
if let Some(mode) = agent_registry.get_mode_agent(&mode_id) {
cfg.default_tools = mode.default_tools();
}
cfg
}
Err(_) => {
if let Some(mode) = agent_registry.get_mode_agent(&mode_id) {
let default_tools = mode.default_tools();
let new_config = ModeConfig {
mode_id: mode_id.clone(),
available_tools: default_tools.clone(),
enabled: true,
default_tools: default_tools,
};
match to_json_value(&new_config, "initial mode config") {
Ok(new_config_value) => {
if let Err(e) = config_service.set_config(&path, new_config_value).await {
warn!(
"Failed to save initial mode config: mode_id={}, error={}",
mode_id, e
);
}
}
Err(e) => {
warn!(
"Failed to serialize initial mode config: mode_id={}, error={}",
mode_id, e
);
}
}
new_config
} else {
ModeConfig {
mode_id: mode_id.clone(),
available_tools: vec![],
enabled: true,
default_tools: vec![],
}
}
}
};
pub async fn get_mode_config(
_state: State<'_, AppState>,
mode_id: String,
) -> Result<Value, String> {
let config =
bitfun_core::service::config::mode_config_canonicalizer::get_mode_config_view(&mode_id)
.await
.map_err(|e| format!("Failed to get mode config: {}", e))?;

Ok(to_json_value(config, "mode config")?)
}
Expand All @@ -374,10 +271,13 @@ pub async fn set_mode_config(
mode_id: String,
config: Value,
) -> Result<String, String> {
let config_service = &state.config_service;
let path = format!("ai.mode_configs.{}", mode_id);
let _ = state;

match config_service.set_config(&path, config).await {
match bitfun_core::service::config::mode_config_canonicalizer::persist_mode_config_from_value(
&mode_id, config,
)
.await
{
Ok(_) => {
if let Err(e) = bitfun_core::service::config::reload_global_config().await {
warn!(
Expand Down Expand Up @@ -405,30 +305,14 @@ pub async fn set_mode_config(

#[tauri::command]
pub async fn reset_mode_config(
state: State<'_, AppState>,
_state: State<'_, AppState>,
mode_id: String,
) -> Result<String, String> {
use bitfun_core::service::config::types::ModeConfig;

let agent_registry = &state.agent_registry;
let default_tools = if let Some(mode) = agent_registry.get_mode_agent(&mode_id) {
mode.default_tools()
} else {
return Err(format!("Mode does not exist: {}", mode_id));
};

let default_config = ModeConfig {
mode_id: mode_id.clone(),
available_tools: default_tools.clone(),
enabled: true,
default_tools: default_tools,
};

let config_service = &state.config_service;
let path = format!("ai.mode_configs.{}", mode_id);
let default_config_value = to_json_value(&default_config, "default mode config")?;

match config_service.set_config(&path, default_config_value).await {
match bitfun_core::service::config::mode_config_canonicalizer::reset_mode_config_to_default(
&mode_id,
)
.await
{
Ok(_) => {
if let Err(e) = bitfun_core::service::config::reload_global_config().await {
warn!(
Expand Down Expand Up @@ -539,20 +423,23 @@ pub async fn set_subagent_config(
}

#[tauri::command]
pub async fn sync_tool_configs(_state: State<'_, AppState>) -> Result<Value, String> {
match bitfun_core::service::config::tool_config_sync::sync_tool_configs().await {
pub async fn canonicalize_mode_configs(_state: State<'_, AppState>) -> Result<Value, String> {
match bitfun_core::service::config::mode_config_canonicalizer::canonicalize_mode_configs().await
{
Ok(report) => {
info!(
"Tool configs synced: new_tools={}, deleted_tools={}, updated_modes={}",
report.new_tools.len(),
report.deleted_tools.len(),
"Mode configs canonicalized: removed_modes={}, updated_modes={}",
report.removed_mode_configs.len(),
report.updated_modes.len()
);
Ok(to_json_value(report, "tool config sync report")?)
Ok(to_json_value(
report,
"mode config canonicalization report",
)?)
}
Err(e) => {
error!("Failed to sync tool configs: {}", e);
Err(format!("Failed to sync tool configs: {}", e))
error!("Failed to canonicalize mode configs: {}", e);
Err(format!("Failed to canonicalize mode configs: {}", e))
}
}
}
2 changes: 1 addition & 1 deletion src/apps/desktop/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -600,7 +600,7 @@ pub async fn run() {
create_cron_job,
update_cron_job,
delete_cron_job,
api::config_api::sync_tool_configs,
api::config_api::canonicalize_mode_configs,
api::terminal_api::terminal_get_shells,
api::terminal_api::terminal_create,
api::terminal_api::terminal_get,
Expand Down
66 changes: 30 additions & 36 deletions src/crates/core/src/agentic/agents/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ use crate::agentic::agents::custom_subagents::{
};
use crate::agentic::tools::get_all_registered_tool_names;
use crate::service::config::global::GlobalConfigManager;
use crate::service::config::mode_config_canonicalizer::resolve_effective_tools;
use crate::service::config::types::{ModeConfig, SubAgentConfig};
use crate::service::config::GlobalConfig;
use crate::util::errors::{BitFunError, BitFunResult};
use log::{debug, error, warn};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use std::sync::RwLock;
use std::sync::{Arc, OnceLock};
Expand Down Expand Up @@ -372,7 +373,7 @@ impl AgentRegistry {

/// get agent tools from config
/// if not set, return default tools
/// tool configuration synchronization is implemented through tool_config_sync, here only read configuration
/// mode config canonicalization is handled separately; this only reads resolved configuration
pub async fn get_agent_tools(
&self,
agent_type: &str,
Expand All @@ -385,18 +386,13 @@ impl AgentRegistry {
match entry.category {
AgentCategory::Mode => {
let mode_configs = get_mode_configs().await;
let mut tools = mode_configs
.get(agent_type)
.map(|config| config.available_tools.clone())
.unwrap_or_else(|| entry.agent.default_tools());
let defaults = entry.agent.default_tools();
const COMPUTER_USE: &str = "ComputerUse";
if defaults.iter().any(|t| t == COMPUTER_USE)
&& !tools.iter().any(|t| t == COMPUTER_USE)
{
tools.push(COMPUTER_USE.to_string());
}
tools
let valid_tools: HashSet<String> =
get_all_registered_tool_names().await.into_iter().collect();
resolve_effective_tools(
&entry.agent.default_tools(),
mode_configs.get(agent_type),
&valid_tools,
)
}
AgentCategory::SubAgent | AgentCategory::Hidden => entry.agent.default_tools(),
}
Expand Down Expand Up @@ -716,9 +712,9 @@ impl AgentRegistry {
agent_id: &str,
workspace_root: Option<&Path>,
) -> BitFunResult<CustomSubagentDetail> {
let entry = self.find_agent_entry(agent_id, workspace_root).ok_or_else(|| {
BitFunError::agent(format!("Subagent not found: {}", agent_id))
})?;
let entry = self
.find_agent_entry(agent_id, workspace_root)
.ok_or_else(|| BitFunError::agent(format!("Subagent not found: {}", agent_id)))?;
if entry.category != AgentCategory::SubAgent {
return Err(BitFunError::agent(format!(
"Agent '{}' is not a subagent",
Expand Down Expand Up @@ -775,9 +771,9 @@ impl AgentRegistry {
if let Some(root) = workspace_root {
self.load_custom_subagents(root).await;
}
let entry = self.find_agent_entry(agent_id, workspace_root).ok_or_else(|| {
BitFunError::agent(format!("Subagent not found: {}", agent_id))
})?;
let entry = self
.find_agent_entry(agent_id, workspace_root)
.ok_or_else(|| BitFunError::agent(format!("Subagent not found: {}", agent_id)))?;
if entry.category != AgentCategory::SubAgent {
return Err(BitFunError::agent(format!(
"Agent '{}' is not a subagent",
Expand All @@ -799,16 +795,14 @@ impl AgentRegistry {
agent_id
))
})?;
let tools = tools
.filter(|t| !t.is_empty())
.unwrap_or_else(|| {
vec![
"LS".to_string(),
"Read".to_string(),
"Glob".to_string(),
"Grep".to_string(),
]
});
let tools = tools.filter(|t| !t.is_empty()).unwrap_or_else(|| {
vec![
"LS".to_string(),
"Read".to_string(),
"Glob".to_string(),
"Grep".to_string(),
]
});
let mut new_subagent = CustomSubagent::new(
old.name.clone(),
description,
Expand Down Expand Up @@ -838,9 +832,9 @@ impl AgentRegistry {
) -> BitFunResult<()> {
let mut map = self.write_agents();
if map.contains_key(agent_id) {
let old_entry = map.get(agent_id).ok_or_else(|| {
BitFunError::agent(format!("Subagent not found: {}", agent_id))
})?;
let old_entry = map
.get(agent_id)
.ok_or_else(|| BitFunError::agent(format!("Subagent not found: {}", agent_id)))?;
if old_entry.category != AgentCategory::SubAgent {
return Err(BitFunError::agent(format!(
"Agent '{}' is not a subagent",
Expand Down Expand Up @@ -877,9 +871,9 @@ impl AgentRegistry {
let entries = pm.get_mut(root).ok_or_else(|| {
BitFunError::agent("Project subagent cache not loaded for this workspace".to_string())
})?;
let old_entry = entries.get(agent_id).ok_or_else(|| {
BitFunError::agent(format!("Subagent not found: {}", agent_id))
})?;
let old_entry = entries
.get(agent_id)
.ok_or_else(|| BitFunError::agent(format!("Subagent not found: {}", agent_id)))?;
if old_entry.category != AgentCategory::SubAgent {
return Err(BitFunError::agent(format!(
"Agent '{}' is not a subagent",
Expand Down
Loading
Loading