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