mod docs;
mod items;
mod mutations;
mod path;
mod render;
use std::borrow::Cow;
use std::path::{Path, PathBuf};
use super::config_section_headings::{heading_for_path, humanize_identifier};
use anyhow::{Context, Result, anyhow, bail};
use path::get_node;
use render::summarize_value;
use toml::Value as TomlValue;
use vtcode_core::config::loader::VTCodeConfig;
use vtcode_core::utils::ansi::AnsiRenderer;
use vtcode_ui::tui::app::{InlineListSearchConfig, InlineListSelection};
#[cfg(test)]
use docs::FIELD_DOCS;
use items::build_settings_items;
use mutations::{
ScalarOperation, add_array_item, apply_scalar_operation, mutate_draft_and_persist, no_config_source_label,
pop_array_item, reload_state_from_disk,
};
#[cfg(test)]
use mutations::{mutate_draft, render_commented_config};
pub(crate) use path::parent_view_path;
#[cfg(test)]
use path::{PathToken, parse_path_tokens};
const SETTINGS_TITLE: &str = "VT Code Settings";
const SETTINGS_HINT: &str = "Enter open/apply • ←/→ adjust • Esc back";
const SETTINGS_SEARCH_PLACEHOLDER: &str = "section, setting, or value";
const ACTION_RELOAD: &str = "settings:reload";
const ACTION_OPEN_ROOT: &str = "settings:open_root";
const ACTION_PREFIX_OPEN: &str = "settings:open:";
const ACTION_PREFIX_ARRAY_ADD: &str = "settings:array_add:";
const ACTION_PREFIX_ARRAY_POP: &str = "settings:array_pop:";
const ACTION_PREFIX_SET: &str = "settings:set:";
const OPTIONAL_DOC_FIELDS: &[&str] = &["provider.anthropic.thinking_display", "provider.openai.service_tier"];
pub(crate) const SETTINGS_MODEL_CONFIG_PATH: &str = "model_config";
pub(crate) const SETTINGS_MODEL_CONFIG_MAIN_PATH: &str = "model_config.main";
pub(crate) const SETTINGS_MODEL_CONFIG_LIGHTWEIGHT_PATH: &str = "model_config.lightweight";
pub(crate) const ACTION_PICK_MAIN_MODEL: &str = "settings:pick_main_model";
pub(crate) const ACTION_PICK_LIGHTWEIGHT_MODEL: &str = "settings:pick_lightweight_model";
pub(crate) const ACTION_CONFIGURE_EDITOR: &str = "settings:configure_editor";
#[derive(Clone)]
pub(crate) struct SettingsPaletteState {
pub(crate) workspace: PathBuf,
pub(crate) source_path: PathBuf,
pub(crate) source_label: String,
pub(crate) draft: VTCodeConfig,
pub(crate) view_path: Option<String>,
}
#[derive(Debug, Default)]
pub(crate) struct SettingsApplyOutcome {
pub(crate) message: Option<String>,
pub(crate) saved: bool,
}
pub(crate) fn create_settings_palette_state(
workspace: &Path,
vt_snapshot: &Option<VTCodeConfig>,
) -> Result<SettingsPaletteState> {
let manager = crate::main_helpers::load_workspace_config(workspace)?;
let has_config_file = manager.config_path().is_some();
let source_path = manager
.config_path()
.map(Path::to_path_buf)
.unwrap_or_else(|| workspace.join("vtcode.toml"));
let draft = if has_config_file {
manager.config().clone()
} else {
vt_snapshot.clone().unwrap_or_else(|| manager.config().clone())
};
let source_label = if has_config_file {
format!("Configuration source: {}", source_path.display())
} else {
no_config_source_label(workspace)
};
Ok(SettingsPaletteState {
workspace: workspace.to_path_buf(),
source_path,
source_label,
draft,
view_path: None,
})
}
pub(crate) fn show_settings_palette(
renderer: &mut AnsiRenderer,
state: &SettingsPaletteState,
selected: Option<InlineListSelection>,
) -> Result<bool> {
let draft_value = TomlValue::try_from(state.draft.clone()).context("Failed to serialize draft configuration")?;
let mut lines = Vec::new();
lines.push(state.source_label.clone());
if let Some(view_path) = state.view_path.as_deref() {
let heading = heading_for_path(view_path);
lines.push(format!("{} ({})", heading.title, display_settings_view_path(view_path)));
if !heading.summary.is_empty() {
lines.push(heading.summary.into_owned());
}
if view_path == "permissions" {
lines.push(format_permission_summary(&state.draft));
}
} else {
lines.push("Choose a section to edit.".to_string());
}
lines.push(SETTINGS_HINT.to_string());
let items = build_settings_items(state, &draft_value)?;
if items.is_empty() {
return Ok(false);
}
renderer.show_list_modal(
SETTINGS_TITLE,
lines,
items,
selected,
Some(InlineListSearchConfig {
label: String::new(),
placeholder: Some(SETTINGS_SEARCH_PLACEHOLDER.to_string()),
}),
);
Ok(true)
}
fn format_permission_summary(config: &VTCodeConfig) -> String {
format!(
"Rules: deny: {} | ask: {} | allow: {}",
config.permissions.deny.len(),
config.permissions.ask.len(),
config.permissions.allow.len()
)
}
pub(crate) fn apply_settings_action(state: &mut SettingsPaletteState, action: &str) -> Result<SettingsApplyOutcome> {
let mut outcome = SettingsApplyOutcome::default();
if matches!(action, ACTION_PICK_MAIN_MODEL | ACTION_PICK_LIGHTWEIGHT_MODEL | ACTION_CONFIGURE_EDITOR) {
return Ok(outcome);
}
match action {
ACTION_RELOAD => {
reload_state_from_disk(state)?;
outcome.message = Some("Reloaded settings from disk.".to_string());
return Ok(outcome);
}
ACTION_OPEN_ROOT => {
state.view_path = None;
return Ok(outcome);
}
_ => {}
}
if let Some(path) = action.strip_prefix(ACTION_PREFIX_OPEN) {
if path.trim().is_empty() {
state.view_path = None;
} else {
state.view_path = Some(path.to_string());
}
return Ok(outcome);
}
if let Some(path) = action.strip_prefix(ACTION_PREFIX_ARRAY_ADD) {
mutate_draft_and_persist(state, |draft| add_array_item(draft, path))?;
outcome.saved = true;
outcome.message = Some(describe_array_change(path, true));
return Ok(outcome);
}
if let Some(path) = action.strip_prefix(ACTION_PREFIX_ARRAY_POP) {
mutate_draft_and_persist(state, |draft| pop_array_item(draft, path))?;
outcome.saved = true;
outcome.message = Some(describe_array_change(path, false));
return Ok(outcome);
}
if let Some(rest) = action.strip_prefix(ACTION_PREFIX_SET) {
let (path, op) = rest
.rsplit_once(':')
.ok_or_else(|| anyhow!("Invalid settings action: {action}"))?;
let operation = match op {
"toggle" => ScalarOperation::Toggle,
"inc" => ScalarOperation::Increment,
"dec" => ScalarOperation::Decrement,
"cycle" => ScalarOperation::CycleNext,
"cycle_prev" => ScalarOperation::CyclePrev,
_ => bail!("Unsupported settings operation: {op}"),
};
mutate_draft_and_persist(state, |draft| apply_scalar_operation(draft, path, operation))?;
outcome.saved = true;
outcome.message = Some(describe_scalar_change(state, path, operation));
return Ok(outcome);
}
bail!("Unknown settings action: {action}")
}
pub(crate) fn resolve_settings_view_path(path: &str) -> String {
match path.trim() {
"model" => SETTINGS_MODEL_CONFIG_PATH.to_string(),
"model.main" => SETTINGS_MODEL_CONFIG_MAIN_PATH.to_string(),
"model.lightweight" => SETTINGS_MODEL_CONFIG_LIGHTWEIGHT_PATH.to_string(),
"codex" | "codex_app_server" | "codex.app_server" | "app_server" => "agent.codex_app_server".to_string(),
other => other.to_string(),
}
}
pub(crate) fn display_settings_view_path(path: &str) -> Cow<'_, str> {
match path {
SETTINGS_MODEL_CONFIG_PATH => Cow::Borrowed("model"),
SETTINGS_MODEL_CONFIG_MAIN_PATH => Cow::Borrowed("model.main"),
SETTINGS_MODEL_CONFIG_LIGHTWEIGHT_PATH => Cow::Borrowed("model.lightweight"),
other => Cow::Borrowed(other),
}
}
fn change_title(path: &str) -> String {
let last_segment = path
.rsplit('.')
.next()
.unwrap_or(path)
.trim_end_matches(|c: char| c.is_numeric() || c == ']' || c == '[');
let leaf = humanize_identifier(last_segment);
const GENERIC_BOOL_LEAVES: &[&str] = &["Enabled", "Disabled", "Show", "Hide", "On", "Off"];
if GENERIC_BOOL_LEAVES.contains(&leaf.as_str()) {
if let Some(parent) = path.rsplit_once('.') {
let parent_leaf = parent
.0
.rsplit('.')
.next()
.unwrap_or(parent.0)
.trim_end_matches(|c: char| c.is_numeric() || c == ']' || c == '[');
return humanize_identifier(parent_leaf);
}
}
leaf
}
fn describe_scalar_change(state: &SettingsPaletteState, path: &str, operation: ScalarOperation) -> String {
let title = change_title(path);
let draft_value = TomlValue::try_from(state.draft.clone()).ok();
let value = draft_value.as_ref().and_then(|value| get_node(value, path));
match (operation, value) {
(ScalarOperation::Toggle, Some(TomlValue::Boolean(enabled))) => {
format!("{} {}", if *enabled { "Enabled" } else { "Disabled" }, title)
}
(_, Some(value)) => format!("{} → {}", title, summarize_value(value)),
_ => format!("Updated {}", title),
}
}
fn describe_array_change(path: &str, added: bool) -> String {
let title = change_title(path);
if added {
format!("Added item to {}", title)
} else {
format!("Removed last item from {}", title)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::agent::runloop::unified::config_section_headings::normalize_config_path;
use vtcode_commons::canonicalize;
#[test]
fn parse_path_handles_arrays() {
let tokens = parse_path_tokens("commands.allow_list[1]").expect("tokens");
assert_eq!(tokens.len(), 3);
matches!(tokens[0], PathToken::Key(_));
matches!(tokens[1], PathToken::Key(_));
matches!(tokens[2], PathToken::Index(1));
}
#[test]
fn normalize_field_path_replaces_indexes() {
assert_eq!(normalize_config_path("commands.allow_list[12]"), "commands.allow_list[]");
}
#[test]
fn parent_view_path_handles_nested_segments() {
assert_eq!(parent_view_path("agent"), None);
assert_eq!(parent_view_path("agent.vibe_coding"), Some("agent".to_string()));
assert_eq!(
parent_view_path("hooks.lifecycle.pre_tool_use[0].hooks[2]"),
Some("hooks.lifecycle.pre_tool_use[0].hooks".to_string())
);
}
#[test]
fn parse_field_docs_has_known_entry() {
assert!(FIELD_DOCS.lookup("agent.provider").is_some());
}
#[test]
fn root_settings_items_include_nested_keys_for_global_search() {
let state = SettingsPaletteState {
workspace: PathBuf::from("."),
source_path: PathBuf::from("vtcode.toml"),
source_label: "test".to_string(),
draft: VTCodeConfig::default(),
view_path: None,
};
let draft: TomlValue = toml::from_str(
r#"
[tools.editor]
preferred_editor = "code --wait"
[agent]
quiet = true
"#,
)
.expect("valid draft value");
let items = build_settings_items(&state, &draft).expect("settings items");
let tools_entry = items
.iter()
.find(|item| item.title == "Tool Defaults")
.expect("root should show section heading");
let search_value = tools_entry.search_value.as_deref().expect("search value");
assert!(search_value.contains("tools.editor.preferred_editor"));
assert!(search_value.contains("code --wait"));
}
#[test]
fn root_settings_hide_nested_items_until_section_is_opened() {
let state = SettingsPaletteState {
workspace: PathBuf::from("."),
source_path: PathBuf::from("vtcode.toml"),
source_label: "test".to_string(),
draft: VTCodeConfig::default(),
view_path: None,
};
let draft: TomlValue = toml::from_str(
r#"
[tools.editor]
preferred_editor = "code --wait"
"#,
)
.expect("valid draft value");
let items = build_settings_items(&state, &draft).expect("settings items");
assert!(items.iter().any(|item| item.title == "Tool Defaults"));
assert!(!items.iter().any(|item| item.title == "Preferred Editor"));
}
#[test]
fn root_settings_do_not_show_missing_nested_optional_fields() {
let state = SettingsPaletteState {
workspace: PathBuf::from("."),
source_path: PathBuf::from("vtcode.toml"),
source_label: "test".to_string(),
draft: VTCodeConfig::default(),
view_path: None,
};
let draft = TomlValue::try_from(VTCodeConfig::default()).expect("default config should serialize");
let items = build_settings_items(&state, &draft).expect("settings items");
assert!(!items.iter().any(|item| item.title == "Service Tier"));
}
#[test]
fn nested_settings_titles_are_humanized() {
let state = SettingsPaletteState {
workspace: PathBuf::from("."),
source_path: PathBuf::from("vtcode.toml"),
source_label: "test".to_string(),
draft: VTCodeConfig::default(),
view_path: Some("agent".to_string()),
};
let draft: TomlValue = toml::from_str(
r#"
[agent]
default_model = "gpt-5.4"
[agent.circuit_breaker]
enabled = true
"#,
)
.expect("valid draft value");
let items = build_settings_items(&state, &draft).expect("settings items");
assert!(items.iter().any(|item| item.title == "Default Model"));
assert!(items.iter().any(|item| item.title == "Circuit Breaker"));
}
#[test]
fn agent_view_hides_deprecated_auto_permissions_field() {
let state = SettingsPaletteState {
workspace: PathBuf::from("."),
source_path: PathBuf::from("vtcode.toml"),
source_label: "test".to_string(),
draft: VTCodeConfig::default(),
view_path: Some("agent".to_string()),
};
let draft = TomlValue::try_from(VTCodeConfig::default()).expect("default config should serialize");
let items = build_settings_items(&state, &draft).expect("settings items");
assert!(!items.iter().any(|item| item.title == "Autonomous Execution"));
}
#[test]
fn provider_openai_view_includes_missing_service_tier_doc_field() {
let state = SettingsPaletteState {
workspace: PathBuf::from("."),
source_path: PathBuf::from("vtcode.toml"),
source_label: "test".to_string(),
draft: VTCodeConfig::default(),
view_path: Some("provider.openai".to_string()),
};
let draft = TomlValue::try_from(VTCodeConfig::default()).expect("default config should serialize");
let items = build_settings_items(&state, &draft).expect("settings items");
assert!(items.iter().any(|item| item.title == "Service Tier"));
}
#[test]
fn render_commented_config_includes_section_heading() {
let mut config = VTCodeConfig::default();
config.agent.default_model = "gpt-5.4".to_string();
let rendered = render_commented_config(&config).expect("config should render");
assert!(rendered.contains("# Agent Defaults"));
assert!(rendered.contains("[agent]"));
}
#[test]
fn missing_service_tier_cycle_creates_value() {
let mut state = SettingsPaletteState {
workspace: PathBuf::from("."),
source_path: PathBuf::from("vtcode.toml"),
source_label: "test".to_string(),
draft: VTCodeConfig::default(),
view_path: Some("provider.openai".to_string()),
};
mutate_draft(&mut state, |draft| {
apply_scalar_operation(draft, "provider.openai.service_tier", ScalarOperation::CycleNext)
})
.expect("service tier should be inserted");
assert_eq!(state.draft.provider.openai.service_tier, Some(vtcode_config::OpenAIServiceTier::Flex));
}
#[test]
fn service_tier_cycle_advances_from_flex_to_priority() {
let mut state = SettingsPaletteState {
workspace: PathBuf::from("."),
source_path: PathBuf::from("vtcode.toml"),
source_label: "test".to_string(),
draft: VTCodeConfig::default(),
view_path: Some("provider.openai".to_string()),
};
state.draft.provider.openai.service_tier = Some(vtcode_config::OpenAIServiceTier::Flex);
mutate_draft(&mut state, |draft| {
apply_scalar_operation(draft, "provider.openai.service_tier", ScalarOperation::CycleNext)
})
.expect("service tier should advance");
assert_eq!(state.draft.provider.openai.service_tier, Some(vtcode_config::OpenAIServiceTier::Priority));
}
#[test]
fn root_settings_include_ide_context_section() {
let state = SettingsPaletteState {
workspace: PathBuf::from("."),
source_path: PathBuf::from("vtcode.toml"),
source_label: "test".to_string(),
draft: VTCodeConfig::default(),
view_path: None,
};
let draft = TomlValue::try_from(VTCodeConfig::default()).expect("default config should serialize");
let items = build_settings_items(&state, &draft).expect("settings items");
assert!(items.iter().any(|item| item.title == "IDE Context"));
assert!(items.iter().any(|item| item.title == "Custom Providers"));
}
#[test]
fn resolve_settings_view_path_maps_model_aliases() {
assert_eq!(resolve_settings_view_path("model"), SETTINGS_MODEL_CONFIG_PATH);
assert_eq!(resolve_settings_view_path("model.main"), SETTINGS_MODEL_CONFIG_MAIN_PATH);
assert_eq!(resolve_settings_view_path("model.lightweight"), SETTINGS_MODEL_CONFIG_LIGHTWEIGHT_PATH);
assert_eq!(resolve_settings_view_path("codex"), "agent.codex_app_server");
assert_eq!(resolve_settings_view_path("codex_app_server"), "agent.codex_app_server");
}
#[test]
fn root_settings_include_model_config_quick_access() {
let state = SettingsPaletteState {
workspace: PathBuf::from("."),
source_path: PathBuf::from("vtcode.toml"),
source_label: "test".to_string(),
draft: VTCodeConfig::default(),
view_path: None,
};
let draft = TomlValue::try_from(VTCodeConfig::default()).expect("default config should serialize");
let items = build_settings_items(&state, &draft).expect("settings items");
let entry = items
.iter()
.find(|item| item.title == "Model Config")
.expect("model config quick access");
assert_eq!(
entry.selection,
Some(InlineListSelection::ConfigAction(format!("{ACTION_PREFIX_OPEN}{SETTINGS_MODEL_CONFIG_PATH}")))
);
}
#[test]
fn root_settings_include_external_editor_quick_access() {
let state = SettingsPaletteState {
workspace: PathBuf::from("."),
source_path: PathBuf::from("vtcode.toml"),
source_label: "test".to_string(),
draft: VTCodeConfig::default(),
view_path: None,
};
let draft = TomlValue::try_from(VTCodeConfig::default()).expect("default config should serialize");
let items = build_settings_items(&state, &draft).expect("settings items");
let entry = items
.iter()
.find(|item| item.title == "External Editor")
.expect("external editor quick access");
assert_eq!(entry.selection, Some(InlineListSelection::ConfigAction(ACTION_CONFIGURE_EDITOR.to_string())));
}
#[test]
fn root_settings_include_codex_app_server_quick_access() {
let state = SettingsPaletteState {
workspace: PathBuf::from("."),
source_path: PathBuf::from("vtcode.toml"),
source_label: "test".to_string(),
draft: VTCodeConfig::default(),
view_path: None,
};
let draft = TomlValue::try_from(VTCodeConfig::default()).expect("default config should serialize");
let items = build_settings_items(&state, &draft).expect("settings items");
let entry = items
.iter()
.find(|item| item.title == "Codex App Server")
.expect("codex app server quick access");
assert_eq!(
entry.selection,
Some(InlineListSelection::ConfigAction("settings:open:agent.codex_app_server".to_string()))
);
}
#[test]
fn codex_app_server_custom_command_entry_is_cycleable() {
let state = SettingsPaletteState {
workspace: PathBuf::from("."),
source_path: PathBuf::from("vtcode.toml"),
source_label: "test".to_string(),
draft: VTCodeConfig::default(),
view_path: Some("agent.codex_app_server".to_string()),
};
let draft: TomlValue = toml::from_str(
r#"
[agent.codex_app_server]
command = "/usr/local/bin/codex"
args = ["app-server"]
startup_timeout_secs = 10
experimental_features = false
"#,
)
.expect("valid draft value");
let items = build_settings_items(&state, &draft).expect("settings items");
let entry = items.iter().find(|item| item.title == "Command").expect("command entry");
assert_eq!(
entry.selection,
Some(InlineListSelection::ConfigAction("settings:set:agent.codex_app_server.command:cycle".to_string()))
);
}
#[test]
fn model_config_root_shows_main_and_lightweight_sections() {
let state = SettingsPaletteState {
workspace: PathBuf::from("."),
source_path: PathBuf::from("vtcode.toml"),
source_label: "test".to_string(),
draft: VTCodeConfig::default(),
view_path: Some(SETTINGS_MODEL_CONFIG_PATH.to_string()),
};
let draft = TomlValue::try_from(VTCodeConfig::default()).expect("default config should serialize");
let items = build_settings_items(&state, &draft).expect("settings items");
assert!(items.iter().any(|item| item.title == "Main Model"));
assert!(items.iter().any(|item| item.title == "Lightweight Model"));
}
#[test]
fn model_config_main_uses_picker_backed_default_model() {
let state = SettingsPaletteState {
workspace: PathBuf::from("."),
source_path: PathBuf::from("vtcode.toml"),
source_label: "test".to_string(),
draft: VTCodeConfig::default(),
view_path: Some(SETTINGS_MODEL_CONFIG_MAIN_PATH.to_string()),
};
let draft = TomlValue::try_from(VTCodeConfig::default()).expect("default config should serialize");
let items = build_settings_items(&state, &draft).expect("settings items");
assert!(items.iter().any(|item| item.title == "Provider"));
let default_model = items
.iter()
.find(|item| item.title == "Default Model")
.expect("default model entry");
assert_eq!(
default_model.selection,
Some(InlineListSelection::ConfigAction(ACTION_PICK_MAIN_MODEL.to_string()))
);
}
#[test]
fn model_config_lightweight_shows_full_small_model_block() {
let state = SettingsPaletteState {
workspace: PathBuf::from("."),
source_path: PathBuf::from("vtcode.toml"),
source_label: "test".to_string(),
draft: VTCodeConfig::default(),
view_path: Some(SETTINGS_MODEL_CONFIG_LIGHTWEIGHT_PATH.to_string()),
};
let draft = TomlValue::try_from(VTCodeConfig::default()).expect("default config should serialize");
let items = build_settings_items(&state, &draft).expect("settings items");
for title in [
"Enabled",
"Model",
"Temperature",
"Use For Git History",
"Use For Large Reads",
"Use For Web Summary",
"Use For Memory",
] {
assert!(items.iter().any(|item| item.title == title), "missing {title}");
}
let model = items
.iter()
.find(|item| item.title == "Model")
.expect("lightweight model entry");
assert_eq!(model.selection, Some(InlineListSelection::ConfigAction(ACTION_PICK_LIGHTWEIGHT_MODEL.to_string())));
}
#[test]
fn ide_context_view_includes_provider_section_navigation() {
let state = SettingsPaletteState {
workspace: PathBuf::from("."),
source_path: PathBuf::from("vtcode.toml"),
source_label: "test".to_string(),
draft: VTCodeConfig::default(),
view_path: Some("ide_context.providers".to_string()),
};
let draft = TomlValue::try_from(VTCodeConfig::default()).expect("default config should serialize");
let items = build_settings_items(&state, &draft).expect("settings items");
assert!(items.iter().any(|item| item.title == "VS Code Family"));
assert!(items.iter().any(|item| item.title == "Zed Family"));
assert!(items.iter().any(|item| item.title == "Generic Bridge"));
}
#[test]
fn tools_view_routes_external_editor_to_configure_action() {
let state = SettingsPaletteState {
workspace: PathBuf::from("."),
source_path: PathBuf::from("vtcode.toml"),
source_label: "test".to_string(),
draft: VTCodeConfig::default(),
view_path: Some("tools".to_string()),
};
let draft = TomlValue::try_from(VTCodeConfig::default()).expect("default config should serialize");
let items = build_settings_items(&state, &draft).expect("settings items");
let entry = items
.iter()
.find(|item| item.title == "External Editor")
.expect("tools.external editor entry");
assert_eq!(entry.selection, Some(InlineListSelection::ConfigAction(ACTION_CONFIGURE_EDITOR.to_string())));
}
#[test]
fn agent_view_uses_picker_backed_default_model() {
let state = SettingsPaletteState {
workspace: PathBuf::from("."),
source_path: PathBuf::from("vtcode.toml"),
source_label: "test".to_string(),
draft: VTCodeConfig::default(),
view_path: Some("agent".to_string()),
};
let draft = TomlValue::try_from(VTCodeConfig::default()).expect("default config should serialize");
let items = build_settings_items(&state, &draft).expect("settings items");
let default_model = items
.iter()
.find(|item| item.title == "Default Model")
.expect("default model entry");
assert_eq!(
default_model.selection,
Some(InlineListSelection::ConfigAction(ACTION_PICK_MAIN_MODEL.to_string()))
);
}
#[test]
fn lightweight_model_view_uses_picker_backed_model_entry() {
let state = SettingsPaletteState {
workspace: PathBuf::from("."),
source_path: PathBuf::from("vtcode.toml"),
source_label: "test".to_string(),
draft: VTCodeConfig::default(),
view_path: Some("agent.small_model".to_string()),
};
let draft = TomlValue::try_from(VTCodeConfig::default()).expect("default config should serialize");
let items = build_settings_items(&state, &draft).expect("settings items");
let model = items.iter().find(|item| item.title == "Model").expect("model entry");
assert_eq!(model.selection, Some(InlineListSelection::ConfigAction(ACTION_PICK_LIGHTWEIGHT_MODEL.to_string())));
assert!(model.subtitle.as_deref().is_some_and(|subtitle| subtitle.contains("Automatic")));
}
#[test]
fn ide_context_toggle_action_persists_to_disk() {
let temp = tempfile::tempdir().expect("temp dir");
let source_path = temp.path().join("vtcode.toml");
let mut state = SettingsPaletteState {
workspace: temp.path().to_path_buf(),
source_path: source_path.clone(),
source_label: "test".to_string(),
draft: VTCodeConfig::default(),
view_path: Some("ide_context".to_string()),
};
apply_settings_action(&mut state, "settings:set:ide_context.enabled:toggle").expect("toggle ide context");
assert!(!state.draft.ide_context.enabled);
let persisted = std::fs::read_to_string(&source_path).expect("persisted config");
assert!(persisted.contains("[ide_context]"));
assert!(persisted.contains("enabled = false"));
}
#[test]
fn custom_providers_array_add_uses_valid_template() {
let temp = tempfile::tempdir().expect("temp dir");
let source_path = temp.path().join("vtcode.toml");
let mut state = SettingsPaletteState {
workspace: temp.path().to_path_buf(),
source_path: source_path.clone(),
source_label: "test".to_string(),
draft: VTCodeConfig::default(),
view_path: Some("custom_providers".to_string()),
};
let outcome = apply_settings_action(&mut state, "settings:array_add:custom_providers")
.expect("add custom provider template");
assert_eq!(outcome.message.as_deref(), Some("Added item to Custom Providers"));
assert!(outcome.saved);
assert_eq!(state.draft.custom_providers.len(), 1);
let provider = &state.draft.custom_providers[0];
assert_eq!(provider.name, "custom-provider-1");
assert_eq!(provider.display_name, "Custom Provider 1");
assert_eq!(provider.base_url, "https://llm.example/v1");
assert_eq!(provider.api_key_env, "");
assert_eq!(provider.model, "");
let persisted = std::fs::read_to_string(&source_path).expect("persisted config");
assert!(persisted.contains("custom_providers"));
}
#[test]
fn toggle_action_produces_change_feedback_message() {
let temp = tempfile::tempdir().expect("temp dir");
let source_path = temp.path().join("vtcode.toml");
let mut state = SettingsPaletteState {
workspace: temp.path().to_path_buf(),
source_path: source_path.clone(),
source_label: "test".to_string(),
draft: VTCodeConfig::default(),
view_path: Some("ide_context".to_string()),
};
let outcome =
apply_settings_action(&mut state, "settings:set:ide_context.enabled:toggle").expect("toggle ide context");
assert_eq!(outcome.message.as_deref(), Some("Disabled Ide Context"));
assert!(outcome.saved);
assert!(!state.draft.ide_context.enabled);
}
#[test]
fn settings_palette_state_loads_workspace_config_directly() {
let temp = tempfile::tempdir().expect("temp dir");
let source_path = temp.path().join("vtcode.toml");
let mut config = VTCodeConfig::default();
config.agent.theme = "ansi".to_string();
std::fs::write(&source_path, toml::to_string(&config).expect("config should serialize"))
.expect("workspace config should be written");
let state = create_settings_palette_state(temp.path(), &None).expect("settings state should load");
assert_eq!(
canonicalize(&state.source_path).expect("canonical state source path"),
canonicalize(&source_path).expect("canonical expected source path")
);
assert_eq!(state.draft.agent.theme, "ansi");
}
#[test]
fn permission_view_summary_includes_mode_and_rule_counts() {
let mut config = VTCodeConfig::default();
config.permissions.allow = vec!["Read".to_string()];
config.permissions.ask = vec!["Bash".to_string(), "Write".to_string()];
config.permissions.deny = vec!["Edit".to_string()];
let summary = format_permission_summary(&config);
assert!(summary.contains("Rules:"));
assert!(summary.contains("deny: 1"));
assert!(summary.contains("ask: 2"));
assert!(summary.contains("allow: 1"));
}
}