syncable_cli/agent/session/
mod.rs1mod commands;
13mod plan_mode;
14mod providers;
15mod ui;
16
17pub use plan_mode::{IncompletePlan, PlanMode, find_incomplete_plans};
19pub use providers::{get_available_models, get_configured_providers, prompt_api_key};
20
21use crate::agent::commands::TokenUsage;
22use crate::agent::{AgentResult, ProviderType};
23use crate::platform::PlatformSession;
24use colored::Colorize;
25use std::io;
26use std::path::Path;
27
28pub struct ChatSession {
30 pub provider: ProviderType,
31 pub model: String,
32 pub project_path: std::path::PathBuf,
33 pub history: Vec<(String, String)>, pub token_usage: TokenUsage,
35 pub plan_mode: PlanMode,
37 pub pending_resume: Option<crate::agent::persistence::ConversationRecord>,
39 pub platform_session: PlatformSession,
41}
42
43impl ChatSession {
44 pub fn new(project_path: &Path, provider: ProviderType, model: Option<String>) -> Self {
45 let default_model = match provider {
46 ProviderType::OpenAI => "gpt-5.2".to_string(),
47 ProviderType::Anthropic => "claude-sonnet-4-5-20250929".to_string(),
48 ProviderType::Bedrock => "global.anthropic.claude-sonnet-4-20250514-v1:0".to_string(),
49 };
50
51 let platform_session = PlatformSession::load().unwrap_or_default();
53
54 Self {
55 provider,
56 model: model.unwrap_or(default_model),
57 project_path: project_path.to_path_buf(),
58 history: Vec::new(),
59 token_usage: TokenUsage::new(),
60 plan_mode: PlanMode::default(),
61 pending_resume: None,
62 platform_session,
63 }
64 }
65
66 pub fn update_platform_session(&mut self, session: PlatformSession) {
68 self.platform_session = session;
69 if let Err(e) = self.platform_session.save() {
70 eprintln!(
71 "{}",
72 format!("Warning: Failed to save platform session: {}", e).yellow()
73 );
74 }
75 }
76
77 pub fn toggle_plan_mode(&mut self) -> PlanMode {
79 self.plan_mode = self.plan_mode.toggle();
80 self.plan_mode
81 }
82
83 pub fn is_planning(&self) -> bool {
85 self.plan_mode.is_planning()
86 }
87
88 pub fn has_api_key(provider: ProviderType) -> bool {
90 providers::has_api_key(provider)
91 }
92
93 pub fn load_api_key_to_env(provider: ProviderType) {
95 providers::load_api_key_to_env(provider)
96 }
97
98 pub fn prompt_api_key(provider: ProviderType) -> AgentResult<String> {
100 providers::prompt_api_key(provider)
101 }
102
103 pub fn handle_model_command(&mut self) -> AgentResult<()> {
105 commands::handle_model_command(self)
106 }
107
108 pub fn handle_provider_command(&mut self) -> AgentResult<()> {
110 commands::handle_provider_command(self)
111 }
112
113 pub fn handle_reset_command(&mut self) -> AgentResult<()> {
115 commands::handle_reset_command(self)
116 }
117
118 pub fn handle_profile_command(&mut self) -> AgentResult<()> {
120 commands::handle_profile_command(self)
121 }
122
123 pub fn handle_plans_command(&self) -> AgentResult<()> {
125 commands::handle_plans_command(self)
126 }
127
128 pub fn handle_resume_command(&mut self) -> AgentResult<bool> {
131 commands::handle_resume_command(self)
132 }
133
134 pub fn handle_list_sessions_command(&self) {
136 commands::handle_list_sessions_command(self)
137 }
138
139 pub fn print_help() {
141 ui::print_help()
142 }
143
144 pub fn print_logo() {
146 ui::print_logo()
147 }
148
149 pub fn print_banner(&self) {
151 ui::print_banner(self)
152 }
153
154 pub fn process_command(&mut self, input: &str) -> AgentResult<bool> {
156 let cmd = input.trim().to_lowercase();
157
158 if cmd == "/" {
161 Self::print_help();
162 return Ok(true);
163 }
164
165 match cmd.as_str() {
166 "/exit" | "/quit" | "/q" => {
167 println!("\n{}", "👋 Goodbye!".green());
168 return Ok(false);
169 }
170 "/help" | "/h" | "/?" => {
171 Self::print_help();
172 }
173 "/model" | "/m" => {
174 self.handle_model_command()?;
175 }
176 "/provider" | "/p" => {
177 self.handle_provider_command()?;
178 }
179 "/cost" => {
180 self.token_usage.print_report(&self.model);
181 }
182 "/clear" | "/c" => {
183 self.history.clear();
184 println!("{}", "✓ Conversation history cleared".green());
185 }
186 "/reset" | "/r" => {
187 self.handle_reset_command()?;
188 }
189 "/profile" => {
190 self.handle_profile_command()?;
191 }
192 "/plans" => {
193 self.handle_plans_command()?;
194 }
195 "/resume" | "/s" => {
196 let _ = self.handle_resume_command()?;
199 }
200 "/sessions" | "/ls" => {
201 self.handle_list_sessions_command();
202 }
203 _ => {
204 if cmd.starts_with('/') {
205 println!(
207 "{}",
208 format!(
209 "Unknown command: {}. Type /help for available commands.",
210 cmd
211 )
212 .yellow()
213 );
214 }
215 }
216 }
217
218 Ok(true)
219 }
220
221 pub fn is_command(input: &str) -> bool {
223 input.trim().starts_with('/')
224 }
225
226 fn strip_file_references(input: &str) -> String {
230 let mut result = String::with_capacity(input.len());
231 let chars: Vec<char> = input.chars().collect();
232 let mut i = 0;
233
234 while i < chars.len() {
235 if chars[i] == '@' {
236 let is_valid_trigger = i == 0 || chars[i - 1].is_whitespace();
238
239 if is_valid_trigger {
240 let has_path = i + 1 < chars.len() && !chars[i + 1].is_whitespace();
242
243 if has_path {
244 i += 1;
246 continue;
247 }
248 }
249 }
250 result.push(chars[i]);
251 i += 1;
252 }
253
254 result
255 }
256
257 pub fn read_input(&self) -> io::Result<crate::agent::ui::input::InputResult> {
261 use crate::agent::ui::input::read_input_with_file_picker;
262
263 let prompt = if self.platform_session.is_project_selected() {
265 format!(
266 "{} >",
267 self.platform_session.display_context()
268 )
269 } else {
270 ">".to_string()
271 };
272
273 Ok(read_input_with_file_picker(
274 &prompt,
275 &self.project_path,
276 self.plan_mode.is_planning(),
277 ))
278 }
279
280 pub fn process_submitted_text(text: &str) -> String {
282 let trimmed = text.trim();
283 if trimmed.starts_with('/') && trimmed.contains(" ") {
286 if let Some(cmd) = trimmed.split_whitespace().next() {
288 return cmd.to_string();
289 }
290 }
291 Self::strip_file_references(trimmed)
294 }
295}