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 colored::Colorize;
24use std::io;
25use std::path::Path;
26
27pub struct ChatSession {
29 pub provider: ProviderType,
30 pub model: String,
31 pub project_path: std::path::PathBuf,
32 pub history: Vec<(String, String)>, pub token_usage: TokenUsage,
34 pub plan_mode: PlanMode,
36 pub pending_resume: Option<crate::agent::persistence::ConversationRecord>,
38}
39
40impl ChatSession {
41 pub fn new(project_path: &Path, provider: ProviderType, model: Option<String>) -> Self {
42 let default_model = match provider {
43 ProviderType::OpenAI => "gpt-5.2".to_string(),
44 ProviderType::Anthropic => "claude-sonnet-4-5-20250929".to_string(),
45 ProviderType::Bedrock => "global.anthropic.claude-sonnet-4-20250514-v1:0".to_string(),
46 };
47
48 Self {
49 provider,
50 model: model.unwrap_or(default_model),
51 project_path: project_path.to_path_buf(),
52 history: Vec::new(),
53 token_usage: TokenUsage::new(),
54 plan_mode: PlanMode::default(),
55 pending_resume: None,
56 }
57 }
58
59 pub fn toggle_plan_mode(&mut self) -> PlanMode {
61 self.plan_mode = self.plan_mode.toggle();
62 self.plan_mode
63 }
64
65 pub fn is_planning(&self) -> bool {
67 self.plan_mode.is_planning()
68 }
69
70 pub fn has_api_key(provider: ProviderType) -> bool {
72 providers::has_api_key(provider)
73 }
74
75 pub fn load_api_key_to_env(provider: ProviderType) {
77 providers::load_api_key_to_env(provider)
78 }
79
80 pub fn prompt_api_key(provider: ProviderType) -> AgentResult<String> {
82 providers::prompt_api_key(provider)
83 }
84
85 pub fn handle_model_command(&mut self) -> AgentResult<()> {
87 commands::handle_model_command(self)
88 }
89
90 pub fn handle_provider_command(&mut self) -> AgentResult<()> {
92 commands::handle_provider_command(self)
93 }
94
95 pub fn handle_reset_command(&mut self) -> AgentResult<()> {
97 commands::handle_reset_command(self)
98 }
99
100 pub fn handle_profile_command(&mut self) -> AgentResult<()> {
102 commands::handle_profile_command(self)
103 }
104
105 pub fn handle_plans_command(&self) -> AgentResult<()> {
107 commands::handle_plans_command(self)
108 }
109
110 pub fn handle_resume_command(&mut self) -> AgentResult<bool> {
113 commands::handle_resume_command(self)
114 }
115
116 pub fn handle_list_sessions_command(&self) {
118 commands::handle_list_sessions_command(self)
119 }
120
121 pub fn print_help() {
123 ui::print_help()
124 }
125
126 pub fn print_logo() {
128 ui::print_logo()
129 }
130
131 pub fn print_banner(&self) {
133 ui::print_banner(self)
134 }
135
136 pub fn process_command(&mut self, input: &str) -> AgentResult<bool> {
138 let cmd = input.trim().to_lowercase();
139
140 if cmd == "/" {
143 Self::print_help();
144 return Ok(true);
145 }
146
147 match cmd.as_str() {
148 "/exit" | "/quit" | "/q" => {
149 println!("\n{}", "👋 Goodbye!".green());
150 return Ok(false);
151 }
152 "/help" | "/h" | "/?" => {
153 Self::print_help();
154 }
155 "/model" | "/m" => {
156 self.handle_model_command()?;
157 }
158 "/provider" | "/p" => {
159 self.handle_provider_command()?;
160 }
161 "/cost" => {
162 self.token_usage.print_report(&self.model);
163 }
164 "/clear" | "/c" => {
165 self.history.clear();
166 println!("{}", "✓ Conversation history cleared".green());
167 }
168 "/reset" | "/r" => {
169 self.handle_reset_command()?;
170 }
171 "/profile" => {
172 self.handle_profile_command()?;
173 }
174 "/plans" => {
175 self.handle_plans_command()?;
176 }
177 "/resume" | "/s" => {
178 let _ = self.handle_resume_command()?;
181 }
182 "/sessions" | "/ls" => {
183 self.handle_list_sessions_command();
184 }
185 _ => {
186 if cmd.starts_with('/') {
187 println!(
189 "{}",
190 format!(
191 "Unknown command: {}. Type /help for available commands.",
192 cmd
193 )
194 .yellow()
195 );
196 }
197 }
198 }
199
200 Ok(true)
201 }
202
203 pub fn is_command(input: &str) -> bool {
205 input.trim().starts_with('/')
206 }
207
208 fn strip_file_references(input: &str) -> String {
212 let mut result = String::with_capacity(input.len());
213 let chars: Vec<char> = input.chars().collect();
214 let mut i = 0;
215
216 while i < chars.len() {
217 if chars[i] == '@' {
218 let is_valid_trigger = i == 0 || chars[i - 1].is_whitespace();
220
221 if is_valid_trigger {
222 let has_path = i + 1 < chars.len() && !chars[i + 1].is_whitespace();
224
225 if has_path {
226 i += 1;
228 continue;
229 }
230 }
231 }
232 result.push(chars[i]);
233 i += 1;
234 }
235
236 result
237 }
238
239 pub fn read_input(&self) -> io::Result<crate::agent::ui::input::InputResult> {
243 use crate::agent::ui::input::read_input_with_file_picker;
244
245 Ok(read_input_with_file_picker(
246 ">",
247 &self.project_path,
248 self.plan_mode.is_planning(),
249 ))
250 }
251
252 pub fn process_submitted_text(text: &str) -> String {
254 let trimmed = text.trim();
255 if trimmed.starts_with('/') && trimmed.contains(" ") {
258 if let Some(cmd) = trimmed.split_whitespace().next() {
260 return cmd.to_string();
261 }
262 }
263 Self::strip_file_references(trimmed)
266 }
267}