Skip to main content

claude_code_cli_acp/config/
session.rs

1use agent_client_protocol::schema::{
2    ConfigOptionUpdate, CurrentModeUpdate, ModelInfo, SessionConfigOption,
3    SessionConfigOptionCategory, SessionConfigSelectOption, SessionConfigValueId, SessionMode,
4    SessionModeState, SessionModelState, SessionUpdate,
5};
6
7use crate::config::settings::ClaudeSettings;
8
9const DEFAULT_MODEL_ID: &str = "default";
10const DEFAULT_EFFORT: &str = "high";
11
12#[derive(Clone, Debug, PartialEq, Eq)]
13pub struct SessionConfigState {
14    mode: String,
15    model: String,
16    effort: String,
17    models: Vec<ModelChoice>,
18    allow_bypass: bool,
19}
20
21#[derive(Clone, Debug, PartialEq, Eq)]
22pub struct ModelChoice {
23    pub id: String,
24    pub name: String,
25    pub description: Option<String>,
26}
27
28impl SessionConfigState {
29    pub fn from_settings(settings: &ClaudeSettings) -> Self {
30        let allow_bypass = allow_bypass_permissions();
31        let mut models = configured_models(settings.available_models.as_deref());
32        let model = resolve_model(
33            &models,
34            std::env::var("ANTHROPIC_MODEL")
35                .ok()
36                .as_deref()
37                .or(settings.model.as_deref()),
38        )
39        .unwrap_or_else(|| DEFAULT_MODEL_ID.to_string());
40        if !models.iter().any(|choice| choice.id == model) {
41            models.push(ModelChoice {
42                id: model.clone(),
43                name: model.clone(),
44                description: None,
45            });
46        }
47        let mode =
48            resolve_permission_mode(settings.permissions.default_mode.as_deref(), allow_bypass);
49        let effort = normalize_effort(settings.effort_level.as_deref())
50            .unwrap_or_else(|| DEFAULT_EFFORT.to_string());
51
52        Self {
53            mode,
54            model,
55            effort,
56            models,
57            allow_bypass,
58        }
59    }
60
61    pub fn mode(&self) -> &str {
62        &self.mode
63    }
64
65    pub fn model(&self) -> &str {
66        &self.model
67    }
68
69    pub fn effort(&self) -> &str {
70        &self.effort
71    }
72
73    pub fn modes(&self) -> SessionModeState {
74        SessionModeState::new(self.mode.clone(), available_modes(self.allow_bypass))
75    }
76
77    pub fn models(&self) -> SessionModelState {
78        SessionModelState::new(
79            self.model.clone(),
80            self.models
81                .iter()
82                .map(|choice| {
83                    ModelInfo::new(choice.id.clone(), choice.name.clone())
84                        .description(choice.description.clone())
85                })
86                .collect(),
87        )
88    }
89
90    pub fn config_options(&self) -> Vec<SessionConfigOption> {
91        vec![
92            SessionConfigOption::select(
93                "mode",
94                "Mode",
95                self.mode.clone(),
96                available_modes(self.allow_bypass)
97                    .into_iter()
98                    .map(|mode| {
99                        SessionConfigSelectOption::new(mode.id.0.to_string(), mode.name)
100                            .description(mode.description)
101                    })
102                    .collect::<Vec<_>>(),
103            )
104            .description("Session permission mode")
105            .category(SessionConfigOptionCategory::Mode),
106            SessionConfigOption::select(
107                "model",
108                "Model",
109                self.model.clone(),
110                self.models
111                    .iter()
112                    .map(|choice| {
113                        SessionConfigSelectOption::new(choice.id.clone(), choice.name.clone())
114                            .description(choice.description.clone())
115                    })
116                    .collect::<Vec<_>>(),
117            )
118            .description("Claude model to use")
119            .category(SessionConfigOptionCategory::Model),
120            SessionConfigOption::select(
121                "effort",
122                "Effort",
123                self.effort.clone(),
124                ["low", "medium", "high", "xhigh", "max"]
125                    .into_iter()
126                    .map(|level| SessionConfigSelectOption::new(level, effort_label(level)))
127                    .collect::<Vec<_>>(),
128            )
129            .description("Claude reasoning effort level")
130            .category(SessionConfigOptionCategory::ThoughtLevel),
131        ]
132    }
133
134    pub fn set_mode(&mut self, mode: &str) -> anyhow::Result<()> {
135        let resolved = resolve_permission_mode(Some(mode), self.allow_bypass);
136        if resolved == "default" && !mode.trim().eq_ignore_ascii_case("default") {
137            anyhow::bail!("invalid permission mode: {mode}");
138        }
139        self.mode = resolved;
140        Ok(())
141    }
142
143    pub fn set_model(&mut self, model: &str) -> anyhow::Result<String> {
144        let Some(resolved) = resolve_model(&self.models, Some(model)) else {
145            anyhow::bail!("invalid model: {model}");
146        };
147        self.model = resolved.clone();
148        Ok(resolved)
149    }
150
151    pub fn set_effort(&mut self, effort: &str) -> anyhow::Result<()> {
152        let Some(resolved) = normalize_effort(Some(effort)) else {
153            anyhow::bail!("invalid effort: {effort}");
154        };
155        self.effort = resolved;
156        Ok(())
157    }
158
159    pub fn set_option(
160        &mut self,
161        config_id: &str,
162        value: &SessionConfigValueId,
163    ) -> anyhow::Result<Option<SessionUpdate>> {
164        match config_id {
165            "mode" => {
166                self.set_mode(value.0.as_ref())?;
167                Ok(Some(SessionUpdate::CurrentModeUpdate(
168                    CurrentModeUpdate::new(self.mode.clone()),
169                )))
170            }
171            "model" => {
172                self.set_model(value.0.as_ref())?;
173                Ok(Some(SessionUpdate::ConfigOptionUpdate(
174                    ConfigOptionUpdate::new(self.config_options()),
175                )))
176            }
177            "effort" => {
178                self.set_effort(value.0.as_ref())?;
179                Ok(Some(SessionUpdate::ConfigOptionUpdate(
180                    ConfigOptionUpdate::new(self.config_options()),
181                )))
182            }
183            _ => anyhow::bail!("unknown config option: {config_id}"),
184        }
185    }
186}
187
188pub fn resolve_permission_mode(default_mode: Option<&str>, allow_bypass: bool) -> String {
189    let Some(default_mode) = default_mode else {
190        return "default".to_string();
191    };
192    let normalized = default_mode.trim().to_ascii_lowercase();
193    match normalized.as_str() {
194        "auto" => "auto".to_string(),
195        "default" => "default".to_string(),
196        "acceptedits" => "acceptEdits".to_string(),
197        "dontask" => "dontAsk".to_string(),
198        "plan" => "plan".to_string(),
199        "bypasspermissions" | "bypass" if allow_bypass => "bypassPermissions".to_string(),
200        _ => "default".to_string(),
201    }
202}
203
204pub fn available_modes(allow_bypass: bool) -> Vec<SessionMode> {
205    let mut modes = vec![
206        SessionMode::new("auto", "Auto")
207            .description("Use Claude's auto mode for permission prompts"),
208        SessionMode::new("default", "Default")
209            .description("Standard behavior, prompts for dangerous operations"),
210        SessionMode::new("acceptEdits", "Accept Edits")
211            .description("Auto-accept file edit operations"),
212        SessionMode::new("plan", "Plan Mode").description("Planning mode, no tool execution"),
213        SessionMode::new("dontAsk", "Don't Ask")
214            .description("Deny unapproved permission prompts instead of asking"),
215    ];
216    if allow_bypass {
217        modes.push(
218            SessionMode::new("bypassPermissions", "Bypass Permissions")
219                .description("Bypass all permission checks"),
220        );
221    }
222    modes
223}
224
225pub fn allow_bypass_permissions() -> bool {
226    !running_as_root() || std::env::var_os("IS_SANDBOX").is_some()
227}
228
229fn configured_models(allowlist: Option<&[String]>) -> Vec<ModelChoice> {
230    let mut models = vec![ModelChoice {
231        id: DEFAULT_MODEL_ID.to_string(),
232        name: "Default".to_string(),
233        description: Some("Claude Code default model".to_string()),
234    }];
235
236    let configured = allowlist
237        .map(|models| models.to_vec())
238        .unwrap_or_else(|| vec!["sonnet".to_string(), "opus".to_string()]);
239    for model in configured {
240        let trimmed = model.trim();
241        if trimmed.is_empty() || models.iter().any(|choice| choice.id == trimmed) {
242            continue;
243        }
244        models.push(ModelChoice {
245            id: trimmed.to_string(),
246            name: model_label(trimmed),
247            description: None,
248        });
249    }
250    models
251}
252
253fn resolve_model(models: &[ModelChoice], preference: Option<&str>) -> Option<String> {
254    let preference = preference?.trim();
255    if preference.is_empty() {
256        return None;
257    }
258    let lower = preference.to_ascii_lowercase();
259    models
260        .iter()
261        .find(|model| {
262            let id = model.id.to_ascii_lowercase();
263            let name = model.name.to_ascii_lowercase();
264            id == lower || name == lower || id.contains(&lower) || lower.contains(&id)
265        })
266        .map(|model| model.id.clone())
267}
268
269fn normalize_effort(effort: Option<&str>) -> Option<String> {
270    match effort?.trim().to_ascii_lowercase().as_str() {
271        "low" => Some("low".to_string()),
272        "medium" => Some("medium".to_string()),
273        "high" => Some("high".to_string()),
274        "xhigh" | "extra-high" | "extra_high" => Some("xhigh".to_string()),
275        "max" => Some("max".to_string()),
276        _ => None,
277    }
278}
279
280fn model_label(model: &str) -> String {
281    if model == DEFAULT_MODEL_ID {
282        return "Default".to_string();
283    }
284    let mut chars = model.chars();
285    match chars.next() {
286        Some(first) => first.to_uppercase().chain(chars).collect(),
287        None => model.to_string(),
288    }
289}
290
291fn effort_label(level: &str) -> String {
292    match level {
293        "xhigh" => "XHigh".to_string(),
294        _ => model_label(level),
295    }
296}
297
298#[cfg(unix)]
299fn running_as_root() -> bool {
300    unsafe extern "C" {
301        fn geteuid() -> u32;
302    }
303    unsafe { geteuid() == 0 }
304}
305
306#[cfg(not(unix))]
307fn running_as_root() -> bool {
308    false
309}