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, 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
153fn 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}