use crate::entities::profile::ToolId;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ToolGate {
Web,
Python,
Fs,
Mcp,
Background,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum ToolGroup {
Introspection,
Memory,
ExternalWorld,
Files,
Utils,
Subagent,
Conversation,
SelfModel,
Plugins,
}
impl ToolGroup {
pub const ALL: [ToolGroup; 9] = [
ToolGroup::Introspection,
ToolGroup::Memory,
ToolGroup::ExternalWorld,
ToolGroup::Files,
ToolGroup::Utils,
ToolGroup::Subagent,
ToolGroup::Conversation,
ToolGroup::SelfModel,
ToolGroup::Plugins,
];
pub fn i18n_key(&self) -> &'static str {
match self {
ToolGroup::Introspection => "ui.tool.group.introspection",
ToolGroup::Memory => "ui.tool.group.memory",
ToolGroup::ExternalWorld => "ui.tool.group.external_world",
ToolGroup::Files => "ui.tool.group.files",
ToolGroup::Utils => "ui.tool.group.utils",
ToolGroup::Subagent => "ui.tool.group.subagent",
ToolGroup::Conversation => "ui.tool.group.conversation",
ToolGroup::SelfModel => "ui.tool.group.self_model",
ToolGroup::Plugins => "ui.tool.group.plugins",
}
}
pub fn title(&self) -> &'static str {
match self {
ToolGroup::Introspection => "Introspection",
ToolGroup::Memory => "Memory and knowledge",
ToolGroup::ExternalWorld => "External world",
ToolGroup::Files => "Files",
ToolGroup::Utils => "Utilities",
ToolGroup::Subagent => "Subagent",
ToolGroup::Conversation => "Conversation control",
ToolGroup::SelfModel => "Self-model",
ToolGroup::Plugins => "Plugins (MCP)",
}
}
}
#[allow(dead_code)]
pub fn group_titles() -> [&'static str; 9] {
ToolGroup::ALL.map(|g| g.title())
}
#[derive(Debug, Clone)]
pub struct ToolInfo {
pub id: ToolId,
pub group: ToolGroup,
pub label: &'static str,
pub gate: Option<ToolGate>,
pub enabled_by_default: bool,
pub concurrent: bool,
pub description: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::features::tools::tool_catalog;
#[test]
fn every_tool_has_group_and_label() {
let titles = group_titles();
for info in tool_catalog() {
assert!(!info.label.is_empty(), "no description for {}", info.id);
assert!(
titles.contains(&info.group.title()),
"foreign group for {}",
info.id
);
}
}
#[test]
fn ui_label_and_group_keys_exist_in_all_bundles() {
use crate::shared::i18n::{Lang, locale};
for &lang in Lang::ALL {
let loc = locale(lang);
for info in tool_catalog() {
let k = format!("ui.tool.label.{}", info.id);
assert!(loc.has_key(&k), "{lang:?}: missing key {k}");
}
for g in ToolGroup::ALL {
assert!(
loc.has_key(g.i18n_key()),
"{lang:?}: missing {}",
g.i18n_key()
);
}
}
}
#[test]
fn gated_tools_map_to_switches() {
let catalog = tool_catalog();
let gate = |id: &str| catalog.iter().find(|i| i.id == id).and_then(|i| i.gate);
assert_eq!(gate("web_search"), Some(ToolGate::Web));
assert_eq!(gate("fetch_url"), Some(ToolGate::Web));
assert_eq!(gate("python_exec"), Some(ToolGate::Python));
assert_eq!(gate("fs_write"), Some(ToolGate::Fs));
assert_eq!(gate("note_save"), None);
}
}