use std::collections::VecDeque;
use std::sync::Arc;
use tokio::task::JoinHandle;
use super::state::{
AppState, AttachmentState, ConversationState, ErrorEntry, ErrorSeverity, GenerationStatus,
InputBuffer, ModelState, OperationState, StatusState, UIState,
};
use crate::constants::UI_ERROR_LOG_MAX_SIZE;
use crate::models::{ChatMessage, MessageRole, Model, ModelConfig, StreamCallback};
use crate::session::{ConversationHistory, ConversationManager};
pub struct App {
pub input: InputBuffer,
pub running: bool,
pub working_dir: String,
pub error_log: VecDeque<ErrorEntry>,
pub app_state: AppState,
pub model_state: ModelState,
pub ui_state: UIState,
pub session_state: ConversationState,
pub operation_state: OperationState,
pub status_state: StatusState,
pub attachment_state: AttachmentState,
pub mcp_tools: Vec<serde_json::Value>,
pub mcp_init_task: Option<JoinHandle<McpInitResult>>,
}
pub struct McpInitResult {
pub tools: Vec<serde_json::Value>,
pub manager: Option<Arc<crate::mcp::McpServerManager>>,
}
impl App {
pub fn new(model: Box<dyn Model>, model_id: String, base_config: ModelConfig) -> Self {
let working_dir = std::env::current_dir()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|_| ".".to_string());
let model_state = ModelState::new(model, model_id, base_config);
let conversation_manager = ConversationManager::new(&working_dir).ok();
let current_conversation = conversation_manager
.as_ref()
.map(|_| ConversationHistory::new(working_dir.clone(), model_state.model_name.clone()));
let input_history: std::collections::VecDeque<String> = current_conversation
.as_ref()
.map(|conv| conv.input_history.clone())
.unwrap_or_default();
let mut input = InputBuffer::new();
input.load_history(input_history);
let ui_state = UIState::new();
let session_state = ConversationState::with_conversation(
conversation_manager,
current_conversation,
);
Self {
input,
running: true,
working_dir,
error_log: VecDeque::new(),
app_state: AppState::Idle,
model_state,
ui_state,
session_state,
operation_state: OperationState::new(),
status_state: StatusState::new(),
attachment_state: AttachmentState::new(),
mcp_tools: Vec::new(),
mcp_init_task: None,
}
}
pub fn build_model_config(&self) -> crate::models::ModelConfig {
let mut config = self.model_state.build_config();
config.mcp_tools = self.mcp_tools.clone();
config
}
pub async fn poll_mcp_init(&mut self) {
let ready = self.mcp_init_task.as_ref().is_some_and(|t| t.is_finished());
if !ready {
return;
}
if let Some(task) = self.mcp_init_task.take()
&& let Ok(result) = task.await
{
if !result.tools.is_empty() {
self.mcp_tools = result.tools;
}
if let Some(manager) = result.manager {
crate::agents::set_mcp_manager(manager);
}
}
crate::agents::mark_mcp_init_complete();
}
pub fn add_message(&mut self, role: MessageRole, content: String) {
self.add_message_with_images(role, content, None);
}
pub fn add_message_with_images(
&mut self,
role: MessageRole,
content: String,
images: Option<Vec<String>>,
) {
let mut message = match role {
MessageRole::User => ChatMessage::user(content),
MessageRole::Assistant => ChatMessage::assistant(content),
MessageRole::System => ChatMessage::system(content),
MessageRole::Tool => ChatMessage::tool("", "", content),
};
let (thinking, answer) = ChatMessage::extract_thinking(&message.content);
message.content = answer;
message.thinking = thinking;
if let Some(imgs) = images {
message = message.with_images(imgs);
}
self.commit_message(message);
}
pub fn add_assistant_message_with_tool_calls(
&mut self,
content: String,
tool_calls: Vec<crate::models::ToolCall>,
) {
let mut message = ChatMessage::assistant(content).with_tool_calls(tool_calls);
let (thinking, answer) = ChatMessage::extract_thinking(&message.content);
message.content = answer;
message.thinking = thinking;
self.commit_message(message);
}
pub fn add_tool_result(&mut self, tool_call_id: String, tool_name: String, content: String) {
let message = ChatMessage::tool(tool_call_id, tool_name, content);
self.commit_message(message);
}
pub fn commit_message(&mut self, message: ChatMessage) {
self.session_state.messages.push(message.clone());
if let Some(ref mut conv) = self.session_state.current_conversation {
conv.add_messages(&[message]);
}
}
pub fn clear_input(&mut self) {
self.input.clear();
}
pub fn set_status(&mut self, message: impl Into<String>) {
self.status_state.set(message);
}
pub fn clear_status(&mut self) {
self.status_state.clear();
}
pub fn display_error(&mut self, summary: impl Into<String>, detail: impl Into<String>) {
let summary = summary.into();
let detail = detail.into();
self.set_status(format!("[Error] {}", summary));
if detail.is_empty() {
self.add_message(MessageRole::System, format!("Error: {}", summary));
} else {
self.add_message(MessageRole::System, detail);
}
}
pub fn display_error_simple(&mut self, message: impl Into<String>) {
let message = message.into();
self.display_error(message.clone(), message);
}
pub fn log_error(&mut self, entry: ErrorEntry) {
self.status_state.set(entry.display());
self.error_log.push_back(entry);
if self.error_log.len() > UI_ERROR_LOG_MAX_SIZE {
self.error_log.pop_front(); }
}
pub fn log_error_msg(&mut self, severity: ErrorSeverity, msg: impl Into<String>) {
self.log_error(ErrorEntry::new(severity, msg.into()));
}
pub fn log_error_with_context(
&mut self,
severity: ErrorSeverity,
msg: impl Into<String>,
context: impl Into<String>,
) {
self.log_error(ErrorEntry::with_context(
severity,
msg.into(),
context.into(),
));
}
pub fn recent_errors(&self, count: usize) -> Vec<&ErrorEntry> {
self.error_log.iter().rev().take(count).collect()
}
pub fn set_terminal_title(&self, title: &str) {
use crossterm::{execute, terminal::SetTitle};
use std::io::stdout;
let _ = execute!(stdout(), SetTitle(title));
}
pub fn spawn_title_generation(&self) -> Option<tokio::task::JoinHandle<Option<String>>> {
if self.session_state.conversation_title.is_some() || self.session_state.messages.len() < 2
{
return None;
}
let mut summary = String::new();
for msg in self
.session_state
.messages
.iter()
.filter(|m| matches!(m.role, MessageRole::User | MessageRole::Assistant))
.take(4)
{
let role = if msg.role == MessageRole::User {
"User"
} else {
"Assistant"
};
summary.push_str(&format!(
"{}: {}\n\n",
role,
msg.content.chars().take(200).collect::<String>()
));
}
let model = self.model_state.model.clone();
let mut config = self.build_model_config();
config.thinking_enabled = Some(false);
Some(tokio::spawn(async move {
let prompt = format!(
"Based on this conversation, generate a short, descriptive title (2-4 words maximum, no quotes):\n\n{}\n\nTitle:",
summary
);
let buf = Arc::new(tokio::sync::Mutex::new(String::new()));
let buf_clone = Arc::clone(&buf);
let callback: StreamCallback = Arc::new(move |chunk: &str| {
if let Ok(mut t) = buf_clone.try_lock() {
t.push_str(chunk);
}
});
let model = model.read().await;
if model
.chat(&[ChatMessage::user(prompt)], &config, Some(callback))
.await
.is_ok()
{
let raw = buf.lock().await;
let title: String = raw
.lines()
.next()
.unwrap_or(&raw)
.trim()
.trim_matches(|c| c == '"' || c == '\'' || c == '.' || c == ',')
.chars()
.take(50)
.collect();
if !title.is_empty() {
return Some(title);
}
}
None
}))
}
pub fn scroll_up(&mut self, amount: u16) {
self.ui_state.chat_state.scroll_up(amount);
}
pub fn scroll_down(&mut self, amount: u16) {
self.ui_state.chat_state.scroll_down(amount);
}
pub fn quit(&mut self) {
self.running = false;
}
fn prepare_api_messages(&self) -> Vec<ChatMessage> {
self.session_state
.messages
.iter()
.filter(|msg| {
msg.role == MessageRole::User
|| msg.role == MessageRole::Assistant
|| msg.role == MessageRole::Tool
})
.map(|msg| {
if msg.role == MessageRole::User {
let ts = msg.timestamp.format("%Y-%m-%d %H:%M:%S %Z").to_string();
let mut m = msg.clone();
m.content = format!("[Sent at: {}]\n{}", ts, m.content);
m
} else {
msg.clone()
}
})
.collect()
}
pub fn build_message_history(&self) -> Vec<ChatMessage> {
self.prepare_api_messages()
}
pub fn build_managed_message_history(
&self,
max_context_tokens: usize,
reserve_tokens: usize,
) -> Vec<ChatMessage> {
use crate::utils::Tokenizer;
let tokenizer = Tokenizer::new(&self.model_state.model_name);
let available_tokens = max_context_tokens.saturating_sub(reserve_tokens);
let all_messages = self.prepare_api_messages();
if all_messages.is_empty() {
return Vec::new();
}
let messages_for_counting: Vec<(String, String)> = all_messages
.iter()
.map(|msg| {
let role = match msg.role {
MessageRole::User => "user",
MessageRole::Assistant => "assistant",
MessageRole::System => "system",
MessageRole::Tool => "tool",
};
(role.to_string(), msg.content.clone())
})
.collect();
let total_tokens = tokenizer
.count_chat_tokens(&messages_for_counting)
.unwrap_or_else(|_| all_messages.iter().map(|m| m.content.len() / 4).sum());
if total_tokens <= available_tokens {
return all_messages;
}
let mut kept_messages = Vec::new();
let mut current_tokens = 0;
for msg in all_messages.iter().rev() {
let msg_text = vec![(
match msg.role {
MessageRole::User => "user",
MessageRole::Assistant => "assistant",
MessageRole::System => "system",
MessageRole::Tool => "tool",
}
.to_string(),
msg.content.clone(),
)];
let msg_tokens = tokenizer
.count_chat_tokens(&msg_text)
.unwrap_or(msg.content.len() / 4);
if current_tokens + msg_tokens <= available_tokens {
kept_messages.push(msg.clone());
current_tokens += msg_tokens;
} else if kept_messages.len() < 2 {
kept_messages.push(msg.clone());
break;
} else {
break;
}
}
kept_messages.reverse();
kept_messages
}
pub fn load_conversation(&mut self, conversation: ConversationHistory) {
self.session_state.cumulative_tokens = conversation.total_tokens.unwrap_or(0);
self.session_state.conversation_title = Some(conversation.title.clone());
self.session_state.messages = conversation.messages.clone();
self.session_state.current_conversation = Some(conversation);
self.set_status("Conversation loaded");
}
pub fn save_conversation(&mut self) -> anyhow::Result<()> {
if let Some(ref manager) = self.session_state.conversation_manager
&& let Some(ref mut conv) = self.session_state.current_conversation
{
conv.messages = self.session_state.messages.clone();
conv.total_tokens = Some(self.session_state.cumulative_tokens);
manager.save_conversation(conv)?;
self.set_status("Conversation saved");
}
Ok(())
}
pub fn auto_save_conversation(&mut self) {
if self.session_state.messages.is_empty() {
return;
}
if let Some(ref manager) = self.session_state.conversation_manager
&& let Some(ref mut conv) = self.session_state.current_conversation
{
conv.messages = self.session_state.messages.clone();
conv.total_tokens = Some(self.session_state.cumulative_tokens);
let conv_clone = conv.clone();
let manager_clone = manager.clone();
tokio::task::spawn_blocking(move || {
if let Err(e) = manager_clone.save_conversation(&conv_clone) {
tracing::warn!("Failed to auto-save conversation: {}", e);
}
});
}
}
pub fn start_generation(&mut self, abort_handle: tokio::task::AbortHandle) {
self.app_state = AppState::Generating {
status: GenerationStatus::Sending,
start_time: std::time::Instant::now(),
tokens_received: 0,
abort_handle: Some(abort_handle),
response_buffer: String::with_capacity(8192),
};
}
pub fn update_abort_handle(&mut self, abort_handle: tokio::task::AbortHandle) {
if let AppState::Generating {
abort_handle: ref mut existing,
..
} = self.app_state
{
*existing = Some(abort_handle);
}
}
pub fn transition_to_sending(&mut self) {
if let AppState::Generating { status, .. } = &mut self.app_state {
*status = GenerationStatus::Sending;
}
}
pub fn transition_to_thinking(&mut self) {
if let AppState::Generating { status, .. } = &mut self.app_state {
*status = GenerationStatus::Thinking;
}
}
pub fn transition_to_streaming(&mut self) {
if let AppState::Generating { status, .. } = &mut self.app_state {
*status = GenerationStatus::Streaming;
}
}
pub fn set_final_tokens(&mut self, count: usize) {
if let AppState::Generating {
tokens_received, ..
} = &mut self.app_state
{
*tokens_received += count;
}
self.session_state.add_tokens(count);
}
pub fn stop_generation(&mut self) {
self.app_state = AppState::Idle;
}
pub fn abort_generation(&mut self) -> (Option<tokio::task::AbortHandle>, String) {
if let AppState::Generating {
abort_handle,
response_buffer,
..
} = &mut self.app_state
{
let handle = abort_handle.take();
let buffer = std::mem::take(response_buffer);
self.app_state = AppState::Idle;
(handle, buffer)
} else {
(None, String::new())
}
}
pub fn push_response(&mut self, text: &str) {
if let AppState::Generating {
response_buffer, ..
} = &mut self.app_state
{
response_buffer.push_str(text);
if response_buffer.len() > crate::constants::MAX_RESPONSE_CHARS {
let end = response_buffer.floor_char_boundary(crate::constants::MAX_RESPONSE_CHARS);
response_buffer.truncate(end);
response_buffer
.push_str("\n\n[TRUNCATED: Response exceeded size limit]\n");
self.set_status("[WARNING] Response truncated (size limit reached)");
}
}
}
pub fn response_len(&self) -> usize {
if let AppState::Generating {
response_buffer, ..
} = &self.app_state
{
response_buffer.len()
} else {
0
}
}
pub fn take_response(&mut self) -> String {
if let AppState::Generating {
response_buffer, ..
} = &mut self.app_state
{
std::mem::take(response_buffer)
} else {
String::new()
}
}
pub fn clear_response(&mut self) {
if let AppState::Generating {
response_buffer, ..
} = &mut self.app_state
{
response_buffer.clear();
}
}
}