mod commands;
mod plan_mode;
mod providers;
mod ui;
pub use plan_mode::{IncompletePlan, PlanMode, find_incomplete_plans};
pub use providers::{get_available_models, get_configured_providers, prompt_api_key};
use crate::agent::commands::TokenUsage;
use crate::agent::{AgentResult, ProviderType};
use crate::platform::PlatformSession;
use colored::Colorize;
use std::io;
use std::path::Path;
pub struct ChatSession {
pub provider: ProviderType,
pub model: String,
pub project_path: std::path::PathBuf,
pub history: Vec<(String, String)>, pub token_usage: TokenUsage,
pub plan_mode: PlanMode,
pub last_was_generation: bool,
pub pending_resume: Option<crate::agent::persistence::ConversationRecord>,
pub platform_session: PlatformSession,
}
impl ChatSession {
pub fn new(project_path: &Path, provider: ProviderType, model: Option<String>) -> Self {
let default_model = match provider {
ProviderType::OpenAI => "gpt-5.2".to_string(),
ProviderType::Anthropic => "claude-sonnet-4-5-20250929".to_string(),
ProviderType::Bedrock => "global.anthropic.claude-sonnet-4-20250514-v1:0".to_string(),
};
let platform_session = PlatformSession::load().unwrap_or_default();
Self {
provider,
model: model.unwrap_or(default_model),
project_path: project_path.to_path_buf(),
history: Vec::new(),
token_usage: TokenUsage::new(),
plan_mode: PlanMode::default(),
last_was_generation: false,
pending_resume: None,
platform_session,
}
}
pub fn update_platform_session(&mut self, session: PlatformSession) {
self.platform_session = session;
if let Err(e) = self.platform_session.save() {
eprintln!(
"{}",
format!("Warning: Failed to save platform session: {}", e).yellow()
);
}
}
pub fn toggle_plan_mode(&mut self) -> PlanMode {
self.plan_mode = self.plan_mode.toggle();
self.plan_mode
}
pub fn is_planning(&self) -> bool {
self.plan_mode.is_planning()
}
pub fn has_api_key(provider: ProviderType) -> bool {
providers::has_api_key(provider)
}
pub fn load_api_key_to_env(provider: ProviderType) {
providers::load_api_key_to_env(provider)
}
pub fn prompt_api_key(provider: ProviderType) -> AgentResult<String> {
providers::prompt_api_key(provider)
}
pub fn handle_model_command(&mut self) -> AgentResult<()> {
commands::handle_model_command(self)
}
pub fn handle_provider_command(&mut self) -> AgentResult<()> {
commands::handle_provider_command(self)
}
pub fn handle_reset_command(&mut self) -> AgentResult<()> {
commands::handle_reset_command(self)
}
pub fn handle_profile_command(&mut self) -> AgentResult<()> {
commands::handle_profile_command(self)
}
pub fn handle_plans_command(&self) -> AgentResult<()> {
commands::handle_plans_command(self)
}
pub fn handle_resume_command(&mut self) -> AgentResult<bool> {
commands::handle_resume_command(self)
}
pub fn handle_list_sessions_command(&self) {
commands::handle_list_sessions_command(self)
}
pub fn print_help() {
ui::print_help()
}
pub fn print_logo() {
ui::print_logo()
}
pub fn print_banner(&self) {
ui::print_banner(self)
}
pub fn process_command(&mut self, input: &str) -> AgentResult<bool> {
let cmd = input.trim().to_lowercase();
if cmd == "/" {
Self::print_help();
return Ok(true);
}
match cmd.as_str() {
"/exit" | "/quit" | "/q" => {
println!("\n{}", "👋 Goodbye!".green());
return Ok(false);
}
"/help" | "/h" | "/?" => {
Self::print_help();
}
"/model" | "/m" => {
self.handle_model_command()?;
}
"/provider" | "/p" => {
self.handle_provider_command()?;
}
"/cost" => {
self.token_usage.print_report(&self.model);
}
"/clear" | "/c" => {
self.history.clear();
println!("{}", "✓ Conversation history cleared".green());
}
"/reset" | "/r" => {
self.handle_reset_command()?;
}
"/profile" => {
self.handle_profile_command()?;
}
"/plans" => {
self.handle_plans_command()?;
}
"/resume" | "/s" => {
let _ = self.handle_resume_command()?;
}
"/sessions" | "/ls" => {
self.handle_list_sessions_command();
}
_ => {
if cmd.starts_with('/') {
println!(
"{}",
format!(
"Unknown command: {}. Type /help for available commands.",
cmd
)
.yellow()
);
}
}
}
Ok(true)
}
pub fn is_command(input: &str) -> bool {
input.trim().starts_with('/')
}
fn strip_file_references(input: &str) -> String {
let mut result = String::with_capacity(input.len());
let chars: Vec<char> = input.chars().collect();
let mut i = 0;
while i < chars.len() {
if chars[i] == '@' {
let is_valid_trigger = i == 0 || chars[i - 1].is_whitespace();
if is_valid_trigger {
let has_path = i + 1 < chars.len() && !chars[i + 1].is_whitespace();
if has_path {
i += 1;
continue;
}
}
}
result.push(chars[i]);
i += 1;
}
result
}
pub fn read_input(&self) -> io::Result<crate::agent::ui::input::InputResult> {
use crate::agent::ui::input::read_input_with_file_picker;
let prompt = if self.platform_session.is_project_selected() {
format!("{} >", self.platform_session.display_context())
} else {
">".to_string()
};
Ok(read_input_with_file_picker(
&prompt,
&self.project_path,
self.plan_mode.is_planning(),
))
}
pub fn process_submitted_text(text: &str) -> String {
let trimmed = text.trim();
if trimmed.starts_with('/') && trimmed.contains(" ") {
if let Some(cmd) = trimmed.split_whitespace().next() {
return cmd.to_string();
}
}
Self::strip_file_references(trimmed)
}
}