use crate::store::settings::{Settings, ThinkingLevel};
use std::str::FromStr;
fn thinking_level_to_str(v: ThinkingLevel) -> &'static str {
match v {
ThinkingLevel::Off => "off",
ThinkingLevel::Minimal => "minimal",
ThinkingLevel::Low => "low",
ThinkingLevel::Medium => "medium",
ThinkingLevel::High => "high",
ThinkingLevel::XHigh => "xhigh",
}
}
fn edit_format_to_str(v: crate::store::settings::EditFormat) -> &'static str {
use crate::store::settings::EditFormat;
match v {
EditFormat::Hashline => "hashline",
EditFormat::StrReplace => "str_replace",
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub enum SettingKey {
ThinkingLevel,
AutoCompaction,
GlyphSet,
EditFormat,
ExtensionsEnabled,
SessionHistorySize,
ToolTimeoutSecs,
AskTimeoutSecs,
DisabledTools,
CommitToolEnabled,
TodoPanelEnabled,
AgentHubEnabled,
MermaidRenderEnabled,
SnapcompactEnabled,
MemoryEnabled,
TtsrEnabled,
TtsrInterruptMode,
AdvisorEnabled,
AdvisorSyncBacklog,
AdvisorImmuneTurns,
DefaultTemperature,
MaxResponseTokens,
ModelRoles,
Keybindings,
Theme,
Model,
CustomProviders,
Hooks,
ExtensionPaths,
SkillPaths,
PromptPaths,
ThemePaths,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub enum SettingsTab {
General,
Model,
Interaction,
Tools,
Ui,
AdvisorMemory,
Keybindings,
Advanced,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum SettingWidget {
Toggle,
Cycle,
SubmenuSelect(&'static [&'static str]),
Text,
Multiselect,
MapEditor,
Pointer,
}
pub struct SettingDef {
pub key: SettingKey,
pub tab: SettingsTab,
pub group: &'static str,
pub label: &'static str,
pub description: &'static str,
pub widget: SettingWidget,
pub condition: Option<fn(&Settings) -> bool>,
}
pub const SETTING_DEFS: &[SettingDef] = &[
SettingDef {
key: SettingKey::ExtensionsEnabled,
tab: SettingsTab::General,
group: "Behavior",
label: "Extensions",
description: "Load WASM/native extensions",
widget: SettingWidget::Toggle,
condition: None,
},
SettingDef {
key: SettingKey::SessionHistorySize,
tab: SettingsTab::General,
group: "Behavior",
label: "Session history size",
description: "Entries kept in memory",
widget: SettingWidget::Text,
condition: None,
},
SettingDef {
key: SettingKey::EditFormat,
tab: SettingsTab::General,
group: "Behavior",
label: "Edit format",
description: "str_replace or hashline",
widget: SettingWidget::Cycle,
condition: None,
},
SettingDef {
key: SettingKey::ThinkingLevel,
tab: SettingsTab::Model,
group: "Defaults",
label: "Thinking level",
description: "Reasoning effort",
widget: SettingWidget::Cycle,
condition: None,
},
SettingDef {
key: SettingKey::DefaultTemperature,
tab: SettingsTab::Model,
group: "Defaults",
label: "Temperature",
description: "0.0-2.0 (empty = model default)",
widget: SettingWidget::Text,
condition: None,
},
SettingDef {
key: SettingKey::MaxResponseTokens,
tab: SettingsTab::Model,
group: "Defaults",
label: "Max response tokens",
description: "Per-response cap (empty = model default)",
widget: SettingWidget::Text,
condition: None,
},
SettingDef {
key: SettingKey::ModelRoles,
tab: SettingsTab::Model,
group: "Defaults",
label: "Model roles",
description: "role -> model pattern assignments",
widget: SettingWidget::MapEditor,
condition: None,
},
SettingDef {
key: SettingKey::Theme,
tab: SettingsTab::Model,
group: "Pointers",
label: "Theme",
description: "Use /theme to change",
widget: SettingWidget::Pointer,
condition: None,
},
SettingDef {
key: SettingKey::Model,
tab: SettingsTab::Model,
group: "Pointers",
label: "Model",
description: "Use /model to change",
widget: SettingWidget::Pointer,
condition: None,
},
SettingDef {
key: SettingKey::AutoCompaction,
tab: SettingsTab::Interaction,
group: "Compaction",
label: "Auto-compaction",
description: "Compact when context exceeds window",
widget: SettingWidget::Toggle,
condition: None,
},
SettingDef {
key: SettingKey::ToolTimeoutSecs,
tab: SettingsTab::Interaction,
group: "Timeouts",
label: "Tool timeout (s)",
description: "Tool execution timeout",
widget: SettingWidget::Text,
condition: None,
},
SettingDef {
key: SettingKey::AskTimeoutSecs,
tab: SettingsTab::Interaction,
group: "Timeouts",
label: "Ask timeout (s)",
description: "Ask overlay timeout",
widget: SettingWidget::Text,
condition: None,
},
SettingDef {
key: SettingKey::DisabledTools,
tab: SettingsTab::Tools,
group: "Tools",
label: "Disabled tools",
description: "Tools turned off for the agent",
widget: SettingWidget::Multiselect,
condition: None,
},
SettingDef {
key: SettingKey::CommitToolEnabled,
tab: SettingsTab::Tools,
group: "Tools",
label: "Commit tool",
description: "Enable the Commit tool",
widget: SettingWidget::Toggle,
condition: None,
},
SettingDef {
key: SettingKey::CustomProviders,
tab: SettingsTab::Tools,
group: "Pointers",
label: "Custom providers",
description: "Use /providers to manage",
widget: SettingWidget::Pointer,
condition: None,
},
SettingDef {
key: SettingKey::Hooks,
tab: SettingsTab::Tools,
group: "Pointers",
label: "Hooks",
description: "Use /hooks to view",
widget: SettingWidget::Pointer,
condition: None,
},
SettingDef {
key: SettingKey::GlyphSet,
tab: SettingsTab::Ui,
group: "Appearance",
label: "Icons",
description: "unicode / ascii / nerd glyph set",
widget: SettingWidget::Cycle,
condition: None,
},
SettingDef {
key: SettingKey::TodoPanelEnabled,
tab: SettingsTab::Ui,
group: "Panels",
label: "Todo panel",
description: "Sticky todo panel",
widget: SettingWidget::Toggle,
condition: None,
},
SettingDef {
key: SettingKey::AgentHubEnabled,
tab: SettingsTab::Ui,
group: "Panels",
label: "Agent hub",
description: "Ctrl+h /agents overlay",
widget: SettingWidget::Toggle,
condition: None,
},
SettingDef {
key: SettingKey::MermaidRenderEnabled,
tab: SettingsTab::Ui,
group: "Panels",
label: "Mermaid",
description: "Render mermaid diagrams",
widget: SettingWidget::Toggle,
condition: None,
},
SettingDef {
key: SettingKey::SnapcompactEnabled,
tab: SettingsTab::Ui,
group: "Panels",
label: "Snapcompact",
description: "PNG-frame compactor",
widget: SettingWidget::Toggle,
condition: None,
},
SettingDef {
key: SettingKey::AdvisorEnabled,
tab: SettingsTab::AdvisorMemory,
group: "Advisor",
label: "Advisor",
description: "Read-only reviewer shadowing the agent",
widget: SettingWidget::Toggle,
condition: None,
},
SettingDef {
key: SettingKey::AdvisorSyncBacklog,
tab: SettingsTab::AdvisorMemory,
group: "Advisor",
label: "Sync backlog",
description: "off / sync / async",
widget: SettingWidget::SubmenuSelect(&["off", "sync", "async"]),
condition: Some(|s| s.advisor.enabled),
},
SettingDef {
key: SettingKey::AdvisorImmuneTurns,
tab: SettingsTab::AdvisorMemory,
group: "Advisor",
label: "Immune turns",
description: "Turns the advisor skips",
widget: SettingWidget::Text,
condition: Some(|s| s.advisor.enabled),
},
SettingDef {
key: SettingKey::MemoryEnabled,
tab: SettingsTab::AdvisorMemory,
group: "Memory",
label: "Memory",
description: "Oxibrain durable memory",
widget: SettingWidget::Toggle,
condition: None,
},
SettingDef {
key: SettingKey::TtsrEnabled,
tab: SettingsTab::AdvisorMemory,
group: "Memory",
label: "TTSR",
description: "Time-traveling stream rules",
widget: SettingWidget::Toggle,
condition: None,
},
SettingDef {
key: SettingKey::TtsrInterruptMode,
tab: SettingsTab::AdvisorMemory,
group: "Memory",
label: "TTSR mode",
description: "prose_only or rules",
widget: SettingWidget::SubmenuSelect(&["prose_only", "rules"]),
condition: Some(|s| s.ttsr_enabled),
},
SettingDef {
key: SettingKey::Keybindings,
tab: SettingsTab::Keybindings,
group: "Keybindings",
label: "Keybindings",
description: "Global shortcuts",
widget: SettingWidget::MapEditor,
condition: None,
},
SettingDef {
key: SettingKey::ExtensionPaths,
tab: SettingsTab::Advanced,
group: "Resources",
label: "Extensions",
description: "Use `oxicode config` to manage",
widget: SettingWidget::Pointer,
condition: None,
},
SettingDef {
key: SettingKey::SkillPaths,
tab: SettingsTab::Advanced,
group: "Resources",
label: "Skills",
description: "Use `oxicode config` to manage",
widget: SettingWidget::Pointer,
condition: None,
},
SettingDef {
key: SettingKey::PromptPaths,
tab: SettingsTab::Advanced,
group: "Resources",
label: "Prompts",
description: "Use `oxicode config` to manage",
widget: SettingWidget::Pointer,
condition: None,
},
SettingDef {
key: SettingKey::ThemePaths,
tab: SettingsTab::Advanced,
group: "Resources",
label: "Themes",
description: "Use `oxicode config` to manage",
widget: SettingWidget::Pointer,
condition: None,
},
];
pub fn defs_for_tab(tab: SettingsTab, s: &Settings) -> Vec<&'static SettingDef> {
SETTING_DEFS
.iter()
.filter(|d| d.tab == tab && d.condition.is_none_or(|c| c(s)))
.collect()
}
pub fn get_display_value(key: SettingKey, s: &Settings) -> String {
use SettingKey::*;
match key {
ThinkingLevel => thinking_level_to_str(s.thinking_level).to_string(),
AutoCompaction => s.auto_compaction.to_string(),
GlyphSet => s.glyph_set.to_string(),
EditFormat => edit_format_to_str(s.edit_format).to_string(),
ExtensionsEnabled => s.extensions_enabled.to_string(),
SessionHistorySize => s.session_history_size.to_string(),
ToolTimeoutSecs => s.tool_timeout_seconds.to_string(),
AskTimeoutSecs => s.ask_timeout_secs.to_string(),
DisabledTools => format!("{}", s.disabled_tools.len()),
CommitToolEnabled => s.commit_tool_enabled.to_string(),
TodoPanelEnabled => s.todo_panel_enabled.to_string(),
AgentHubEnabled => s.agent_hub_enabled.to_string(),
MermaidRenderEnabled => s.mermaid_render_enabled.to_string(),
SnapcompactEnabled => s.snapcompact_enabled.to_string(),
MemoryEnabled => s.memory_enabled.to_string(),
TtsrEnabled => s.ttsr_enabled.to_string(),
TtsrInterruptMode => s.ttsr_interrupt_mode.clone(),
AdvisorEnabled => s.advisor.enabled.to_string(),
AdvisorSyncBacklog => s.advisor.sync_backlog.clone(),
AdvisorImmuneTurns => s.advisor.immune_turns.to_string(),
DefaultTemperature => s
.default_temperature
.map(|v| format!("{v}"))
.unwrap_or_else(|| "default".to_string()),
MaxResponseTokens => s
.max_response_tokens
.map(|v| v.to_string())
.unwrap_or_else(|| "default".to_string()),
ModelRoles => format!("{}", s.model_roles.len()),
Keybindings => format!("{}", s.keybindings.len()),
Theme => s.theme.clone(),
Model => s.last_used_model.clone().unwrap_or_else(|| "unset".into()),
CustomProviders => format!("{}", s.custom_providers.len()),
Hooks => format!("{}", s.hooks.len()),
ExtensionPaths => format!("{}", s.extensions.len()),
SkillPaths => format!("{}", s.skills.len()),
PromptPaths => format!("{}", s.prompts.len()),
ThemePaths => format!("{}", s.themes.len()),
}
}
pub fn apply_change(key: SettingKey, s: &mut Settings, new: String) -> anyhow::Result<()> {
use SettingKey::*;
match key {
AutoCompaction => s.auto_compaction = new == "true",
ExtensionsEnabled => s.extensions_enabled = new == "true",
CommitToolEnabled => s.commit_tool_enabled = new == "true",
TodoPanelEnabled => s.todo_panel_enabled = new == "true",
AgentHubEnabled => s.agent_hub_enabled = new == "true",
MermaidRenderEnabled => s.mermaid_render_enabled = new == "true",
SnapcompactEnabled => s.snapcompact_enabled = new == "true",
MemoryEnabled => s.memory_enabled = new == "true",
TtsrEnabled => s.ttsr_enabled = new == "true",
AdvisorEnabled => s.advisor.enabled = new == "true",
ThinkingLevel => {
s.thinking_level = crate::store::settings::parse_thinking_level(&new)
.ok_or_else(|| anyhow::anyhow!("invalid thinking level: {new}"))?;
}
GlyphSet => {
s.glyph_set = crate::symbols::GlyphSet::from_str(&new)
.map_err(|e| anyhow::anyhow!("invalid glyph set: {e}"))?
}
EditFormat => {
use crate::store::settings::EditFormat as Ef;
s.edit_format = match new.as_str() {
"hashline" => Ef::Hashline,
"str_replace" => Ef::StrReplace,
_ => anyhow::bail!("invalid edit format: {new} (expected hashline|str_replace)"),
};
}
SessionHistorySize => s.session_history_size = new.parse()?,
ToolTimeoutSecs => s.tool_timeout_seconds = new.parse()?,
AskTimeoutSecs => s.ask_timeout_secs = new.parse()?,
AdvisorImmuneTurns => s.advisor.immune_turns = new.parse()?,
TtsrInterruptMode => s.ttsr_interrupt_mode = new,
AdvisorSyncBacklog => s.advisor.sync_backlog = new,
DefaultTemperature => {
let trimmed = new.trim();
if trimmed.is_empty() {
s.default_temperature = None;
} else {
let v: f64 = trimmed.parse().map_err(|e| {
anyhow::anyhow!("invalid temperature '{trimmed}': {e} (expected 0.0–2.0)")
})?;
if !(0.0..=2.0).contains(&v) {
anyhow::bail!("temperature {v} out of range 0.0–2.0");
}
s.default_temperature = Some(v);
}
}
MaxResponseTokens => {
let trimmed = new.trim();
if trimmed.is_empty() {
s.max_response_tokens = None;
} else {
let v: usize = trimmed
.parse()
.map_err(|e| anyhow::anyhow!("invalid max response tokens '{trimmed}': {e}"))?;
if v == 0 {
anyhow::bail!("max_response_tokens must be > 0 (empty to clear)");
}
s.max_response_tokens = Some(v);
}
}
DisabledTools => anyhow::bail!("disabled_tools edited via its multiselect"),
ModelRoles => anyhow::bail!("model_roles edited via its map-editor"),
Keybindings => anyhow::bail!("keybindings edited via its map-editor"),
Theme | Model | CustomProviders | Hooks | ExtensionPaths | SkillPaths | PromptPaths
| ThemePaths => anyhow::bail!("read-only"),
}
Ok(())
}
pub fn toggle_disabled_tool(s: &mut Settings, tool: &str, enabled: bool) {
if enabled {
s.disabled_tools.retain(|t| t != tool);
} else if !s.disabled_tools.iter().any(|t| t == tool) {
s.disabled_tools.push(tool.to_string());
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SettingsMapRow {
KeybindingAction(crate::tui_vt::keymap::GlobalAction),
KeybindingCombo(crate::tui_vt::keymap::GlobalAction, String),
ModelRole(String),
}
pub fn set_action_combos(
s: &mut Settings,
action: crate::tui_vt::keymap::GlobalAction,
combos: Vec<String>,
) {
let defaults: Vec<String> = crate::tui_vt::keymap::DEFAULT_KEYBINDINGS
.iter()
.filter(|(a, _)| *a == action)
.map(|(_, combo)| (*combo).to_string())
.collect();
if combos == defaults {
s.keybindings.remove(action.name());
} else {
s.keybindings.insert(action.name().to_string(), combos);
}
}
pub fn set_model_role(s: &mut Settings, role: &str, model: String) {
s.model_roles.insert(role.to_string(), model);
}
pub fn remove_model_role(s: &mut Settings, role: &str) -> bool {
s.model_roles.remove(role).is_some()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn groups_are_contiguous_and_nonempty() {
for def in SETTING_DEFS {
assert!(
!def.group.is_empty(),
"SettingDef for {:?} has empty group",
def.key,
);
}
let mut completed: std::collections::HashMap<
SettingsTab,
std::collections::HashSet<&'static str>,
> = std::collections::HashMap::new();
let mut current_group_per_tab: std::collections::HashMap<SettingsTab, &'static str> =
std::collections::HashMap::new();
for def in SETTING_DEFS {
if let Some(prev) = current_group_per_tab.get(&def.tab)
&& *prev != def.group
{
completed.entry(def.tab).or_default().insert(*prev);
}
let done = completed
.get(&def.tab)
.is_some_and(|c| c.contains(def.group));
assert!(
!done,
"group {:?} reappears non-contiguously in tab {:?}",
def.group, def.tab,
);
current_group_per_tab.insert(def.tab, def.group);
}
}
#[test]
fn thinking_level_round_trips() {
let mut s = Settings::default();
apply_change(SettingKey::ThinkingLevel, &mut s, "high".into()).unwrap();
assert_eq!(s.thinking_level, ThinkingLevel::High);
assert_eq!(get_display_value(SettingKey::ThinkingLevel, &s), "high");
assert!(apply_change(SettingKey::ThinkingLevel, &mut s, "yikes".into()).is_err());
}
#[test]
fn auto_compaction_round_trips() {
let mut s = Settings::default();
apply_change(SettingKey::AutoCompaction, &mut s, "false".into()).unwrap();
assert!(!s.auto_compaction);
assert_eq!(get_display_value(SettingKey::AutoCompaction, &s), "false");
apply_change(SettingKey::AutoCompaction, &mut s, "true".into()).unwrap();
assert!(s.auto_compaction);
}
#[test]
fn tool_timeout_secs_round_trips() {
let mut s = Settings::default();
apply_change(SettingKey::ToolTimeoutSecs, &mut s, "42".into()).unwrap();
assert_eq!(s.tool_timeout_seconds, 42);
assert_eq!(get_display_value(SettingKey::ToolTimeoutSecs, &s), "42");
assert!(apply_change(SettingKey::ToolTimeoutSecs, &mut s, "abc".into()).is_err());
}
#[test]
fn defs_for_tab_respects_condition() {
let mut s = Settings::default();
let keys_disabled: Vec<SettingKey> = defs_for_tab(SettingsTab::AdvisorMemory, &s)
.iter()
.map(|d| d.key)
.collect();
assert!(!keys_disabled.contains(&SettingKey::AdvisorSyncBacklog));
assert!(!keys_disabled.contains(&SettingKey::AdvisorImmuneTurns));
s.advisor.enabled = true;
let keys_enabled: Vec<SettingKey> = defs_for_tab(SettingsTab::AdvisorMemory, &s)
.iter()
.map(|d| d.key)
.collect();
assert!(keys_enabled.contains(&SettingKey::AdvisorSyncBacklog));
assert!(keys_enabled.contains(&SettingKey::AdvisorImmuneTurns));
assert!(!keys_enabled.contains(&SettingKey::TtsrInterruptMode));
s.ttsr_enabled = true;
let keys_ttsr: Vec<SettingKey> = defs_for_tab(SettingsTab::AdvisorMemory, &s)
.iter()
.map(|d| d.key)
.collect();
assert!(keys_ttsr.contains(&SettingKey::TtsrInterruptMode));
}
#[test]
fn structured_editor_rows_reject_scalar_edits() {
let mut s = Settings::default();
let err = apply_change(SettingKey::DisabledTools, &mut s, "anything".into()).unwrap_err();
assert!(format!("{err}").contains("multiselect"));
let err = apply_change(SettingKey::ModelRoles, &mut s, "anything".into()).unwrap_err();
assert!(format!("{err}").contains("map-editor"));
let err = apply_change(SettingKey::Keybindings, &mut s, "anything".into()).unwrap_err();
assert!(format!("{err}").contains("map-editor"));
}
#[test]
fn pointer_rows_are_read_only() {
let mut s = Settings::default();
for key in [
SettingKey::Theme,
SettingKey::Model,
SettingKey::CustomProviders,
SettingKey::Hooks,
SettingKey::ExtensionPaths,
SettingKey::SkillPaths,
SettingKey::PromptPaths,
SettingKey::ThemePaths,
] {
assert!(
apply_change(key, &mut s, "anything".into()).is_err(),
"{key:?} should reject scalar edits",
);
}
}
#[test]
fn default_temperature_round_trips() {
let mut s = Settings::default();
assert_eq!(
get_display_value(SettingKey::DefaultTemperature, &s),
"default",
"None renders as 'default'"
);
apply_change(SettingKey::DefaultTemperature, &mut s, "0.7".into()).unwrap();
assert_eq!(s.default_temperature, Some(0.7));
assert_eq!(get_display_value(SettingKey::DefaultTemperature, &s), "0.7");
apply_change(SettingKey::DefaultTemperature, &mut s, " ".into()).unwrap();
assert_eq!(s.default_temperature, None);
assert_eq!(
get_display_value(SettingKey::DefaultTemperature, &s),
"default"
);
assert!(
apply_change(SettingKey::DefaultTemperature, &mut s, "2.5".into()).is_err(),
"above 2.0 must be rejected"
);
assert!(
apply_change(SettingKey::DefaultTemperature, &mut s, "-0.1".into()).is_err(),
"below 0.0 must be rejected"
);
assert!(
apply_change(SettingKey::DefaultTemperature, &mut s, "warm".into()).is_err(),
"non-numeric must be rejected"
);
apply_change(SettingKey::DefaultTemperature, &mut s, "0".into()).unwrap();
assert_eq!(s.default_temperature, Some(0.0));
apply_change(SettingKey::DefaultTemperature, &mut s, "2.0".into()).unwrap();
assert_eq!(s.default_temperature, Some(2.0));
}
#[test]
fn max_response_tokens_round_trips() {
let mut s = Settings::default();
assert_eq!(
get_display_value(SettingKey::MaxResponseTokens, &s),
"default"
);
apply_change(SettingKey::MaxResponseTokens, &mut s, "8192".into()).unwrap();
assert_eq!(s.max_response_tokens, Some(8192));
assert_eq!(get_display_value(SettingKey::MaxResponseTokens, &s), "8192");
apply_change(SettingKey::MaxResponseTokens, &mut s, "".into()).unwrap();
assert_eq!(s.max_response_tokens, None);
assert!(
apply_change(SettingKey::MaxResponseTokens, &mut s, "0".into()).is_err(),
"zero must be rejected (empty clears, zero caps everything)"
);
assert!(
apply_change(SettingKey::MaxResponseTokens, &mut s, "many".into()).is_err(),
"non-numeric must be rejected"
);
}
#[test]
fn model_defaults_defs_exist_as_text() {
let s = Settings::default();
let model_defs: Vec<&SettingDef> = defs_for_tab(SettingsTab::Model, &s);
let temp = model_defs
.iter()
.find(|d| d.key == SettingKey::DefaultTemperature)
.expect("DefaultTemperature def on Model tab");
assert_eq!(temp.group, "Defaults");
assert!(matches!(temp.widget, SettingWidget::Text));
let tokens = model_defs
.iter()
.find(|d| d.key == SettingKey::MaxResponseTokens)
.expect("MaxResponseTokens def on Model tab");
assert_eq!(tokens.group, "Defaults");
assert!(matches!(tokens.widget, SettingWidget::Text));
}
#[test]
fn toggle_disabled_tool_idempotent() {
let mut s = Settings::default();
assert!(s.disabled_tools.is_empty());
toggle_disabled_tool(&mut s, "bash", false);
assert_eq!(s.disabled_tools, vec!["bash".to_string()]);
toggle_disabled_tool(&mut s, "bash", false);
assert_eq!(s.disabled_tools, vec!["bash".to_string()]);
toggle_disabled_tool(&mut s, "bash", true);
assert!(s.disabled_tools.is_empty());
}
}