Skip to main content

saya_cli/interactive/
session_commands.rs

1use super::session_state::SessionState;
2use crate::agent::runtime::PromptOverrides;
3use crate::slash::SlashCommand;
4use saya_agent::AgentOutput;
5use saya_agent::ApprovalPolicy;
6
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub enum SessionAction {
9    Message(String),
10    Agent(AgentOutput),
11    Cancelled,
12    NotImplemented(String),
13    Error(String),
14    History,
15    Resume(String),
16    Schema(bool),
17    Sql(String),
18    Exit,
19}
20
21impl SessionState {
22    pub fn apply(&mut self, command: SlashCommand, available: &[String]) -> SessionAction {
23        match command {
24            SlashCommand::Connect(name) => {
25                if !available.iter().any(|profile| profile == &name) {
26                    return SessionAction::Error(format!("Unknown configured profile: {name}"));
27                }
28                self.profile = Some(name.clone());
29                SessionAction::Message(format!("Selected profile: {name}"))
30            }
31            SlashCommand::Connections => SessionAction::Message(if available.is_empty() {
32                "No configured connection profiles.".into()
33            } else {
34                format!("Profiles: {}", available.join(", "))
35            }),
36            SlashCommand::Include(name) => {
37                if !available.iter().any(|profile| profile == &name) {
38                    return SessionAction::Error(format!("Unknown configured profile: {name}"));
39                }
40                if !self.included_profiles.contains(&name) {
41                    self.included_profiles.push(name.clone());
42                }
43                SessionAction::Message(format!("Included profile: {name}"))
44            }
45            SlashCommand::Exclude(name) => {
46                self.included_profiles.retain(|item| item != &name);
47                SessionAction::Message(format!("Excluded profile: {name}"))
48            }
49            SlashCommand::Provider(value) => {
50                if let Some(value) = value {
51                    if saya_config::AiProvider::parse(&value).is_none() {
52                        return SessionAction::Error(format!(
53                            "Unsupported provider: {value}. Use ollama, openai, openai_compatible, anthropic, or gemini."
54                        ));
55                    }
56                    self.provider = value;
57                    SessionAction::Message(format!("Provider: {}", self.provider))
58                } else {
59                    SessionAction::Message(format!(
60                        "Provider: {} (available: {})",
61                        self.provider,
62                        available_providers().join(", ")
63                    ))
64                }
65            }
66            SlashCommand::Model(value) => {
67                if let Some(value) = value {
68                    self.model = value;
69                    SessionAction::Message(format!("Model: {}", self.model))
70                } else {
71                    let models = known_models(&self.provider);
72                    if models.is_empty() {
73                        SessionAction::Message(format!(
74                            "Model: {} (no suggestions for provider {})",
75                            self.model, self.provider
76                        ))
77                    } else {
78                        SessionAction::Message(format!(
79                            "Model: {}\nKnown models for {}: {}",
80                            self.model,
81                            self.provider,
82                            models.join(", ")
83                        ))
84                    }
85                }
86            }
87            SlashCommand::Privacy(value) => {
88                if let Some(value) = value {
89                    self.allow_data_sharing = value;
90                }
91                SessionAction::Message(format!(
92                    "Cloud data sharing: {}",
93                    if self.allow_data_sharing {
94                        "enabled"
95                    } else {
96                        "disabled"
97                    }
98                ))
99            }
100            SlashCommand::Approvals(value) => {
101                if let Some(value) = value {
102                    self.approval_mode = approval_name(value);
103                }
104                SessionAction::Message(format!("Approval mode: {}", self.approval_mode))
105            }
106            SlashCommand::Schema(refresh) => SessionAction::Schema(refresh),
107            SlashCommand::Sql(query) => SessionAction::Sql(query),
108            SlashCommand::Clear => {
109                self.messages.clear();
110                self.turns.clear();
111                SessionAction::Message("Conversation context cleared.".into())
112            }
113            SlashCommand::History => SessionAction::History,
114            SlashCommand::Sessions => SessionAction::History,
115            SlashCommand::Resume(id) => SessionAction::Resume(id),
116            SlashCommand::Help(topic) => {
117                SessionAction::Message(crate::slash::help_for(topic.as_deref()))
118            }
119            SlashCommand::Exit => SessionAction::Exit,
120        }
121    }
122
123    pub(crate) fn prompt_overrides(&self) -> PromptOverrides {
124        PromptOverrides {
125            provider: saya_config::AiProvider::parse(&self.provider),
126            model: Some(self.model.clone()),
127            allow_data_sharing: Some(self.allow_data_sharing),
128            profile: self.profile.clone(),
129            included_profiles: self.included_profiles.clone(),
130        }
131    }
132}
133
134fn approval_name(policy: ApprovalPolicy) -> String {
135    match policy {
136        ApprovalPolicy::Ask => "ask",
137        ApprovalPolicy::ReadOnly => "read-only",
138        ApprovalPolicy::Never => "never",
139    }
140    .into()
141}
142
143fn available_providers() -> &'static [&'static str] {
144    &[
145        "ollama",
146        "openai",
147        "openai_compatible",
148        "anthropic",
149        "gemini",
150    ]
151}
152
153/// Curated suggestions for models per provider (convenience suggestions, not an exhaustive or validated list).
154fn known_models(provider: &str) -> &'static [&'static str] {
155    match provider.to_ascii_lowercase().as_str() {
156        "ollama" => &["qwen2.5-coder:14b", "llama3.1", "mistral"],
157        "openai" => &["gpt-4o", "gpt-4o-mini", "o3-mini"],
158        "anthropic" => &["claude-sonnet-4", "claude-opus-4", "claude-3-5-haiku"],
159        "gemini" => &["gemini-2.0-flash", "gemini-1.5-pro"],
160        "openai_compatible" => &[],
161        _ => &[],
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168
169    #[test]
170    fn test_provider_none_lists_available() {
171        let mut state = SessionState::new("test", None, "gpt-4o");
172        let action = state.apply(SlashCommand::Provider(None), &[]);
173        if let SessionAction::Message(msg) = action {
174            assert!(msg.contains("available:"));
175            assert!(msg.contains("anthropic"));
176        } else {
177            panic!("Expected SessionAction::Message");
178        }
179    }
180
181    #[test]
182    fn test_model_none_lists_known_models_for_ollama() {
183        let mut state = SessionState::new("test", None, "qwen2.5-coder:14b");
184        state.provider = "ollama".into();
185        let action = state.apply(SlashCommand::Model(None), &[]);
186        if let SessionAction::Message(msg) = action {
187            assert!(msg.contains("qwen2.5-coder:14b"));
188        } else {
189            panic!("Expected SessionAction::Message");
190        }
191    }
192
193    #[test]
194    fn test_provider_some_sets_provider_without_available() {
195        let mut state = SessionState::new("test", None, "gpt-4o");
196        let action = state.apply(SlashCommand::Provider(Some("openai".into())), &[]);
197        assert_eq!(state.provider, "openai");
198        if let SessionAction::Message(msg) = action {
199            assert_eq!(msg, "Provider: openai");
200            assert!(!msg.contains("available:"));
201        } else {
202            panic!("Expected SessionAction::Message");
203        }
204    }
205}