1use std::collections::VecDeque;
7use std::sync::Arc;
8use tracing::warn;
9
10use super::state::{
11 AppState, AttachmentState, ConversationState, ErrorEntry, ErrorSeverity, GenerationStatus,
12 InputBuffer, ModelState, OperationState, StatusState, UIState,
13};
14use super::theme::Theme;
15use super::widgets::{ChatState, InputState};
16use crate::constants::UI_ERROR_LOG_MAX_SIZE;
17use crate::models::{ChatMessage, MessageRole, Model, ModelConfig, StreamCallback};
18use crate::session::{ConversationHistory, ConversationManager};
19
20pub struct App {
22 pub input: InputBuffer,
24 pub running: bool,
26 pub current_response: String,
28 pub working_dir: String,
30 pub error_log: VecDeque<ErrorEntry>,
32 pub app_state: AppState,
34
35 pub model_state: ModelState,
37 pub ui_state: UIState,
39 pub session_state: ConversationState,
41 pub operation_state: OperationState,
43 pub status_state: StatusState,
45 pub attachment_state: AttachmentState,
47}
48
49impl App {
50 pub fn new(model: Box<dyn Model>, model_id: String) -> Self {
52 let working_dir = std::env::current_dir()
53 .map(|p| p.to_string_lossy().to_string())
54 .unwrap_or_else(|_| ".".to_string());
55
56 let model_state = ModelState::new(model, model_id);
58
59 let conversation_manager = ConversationManager::new(&working_dir).ok();
61 let current_conversation = conversation_manager
62 .as_ref()
63 .map(|_| ConversationHistory::new(working_dir.clone(), model_state.model_name.clone()));
64
65 let input_history: std::collections::VecDeque<String> = conversation_manager
67 .as_ref()
68 .and_then(|_| current_conversation.as_ref())
69 .map(|conv| conv.input_history.clone())
70 .unwrap_or_default();
71
72 let ui_state = UIState {
74 chat_state: ChatState::new(),
75 input_state: InputState::new(),
76 theme: Theme::dark(),
77 selected_message: None,
78 attachment_focused: false,
79 selected_attachment: 0,
80 attachment_area_y: None,
81 };
82
83 let session_state = ConversationState::with_conversation(
85 conversation_manager,
86 current_conversation,
87 input_history,
88 );
89
90 Self {
91 input: InputBuffer::new(),
92 running: true,
93 current_response: String::with_capacity(8192),
94 working_dir,
95 error_log: VecDeque::new(),
96 app_state: AppState::Idle,
97 model_state,
98 ui_state,
99 session_state,
100 operation_state: OperationState::new(),
101 status_state: StatusState::new(),
102 attachment_state: AttachmentState::new(),
103 }
104 }
105
106 pub fn cursor_position(&self) -> usize {
111 self.input.cursor_position
112 }
113
114 pub fn set_cursor_position(&mut self, pos: usize) {
116 self.input.cursor_position = pos;
117 }
118
119 pub fn add_message(&mut self, role: MessageRole, content: String) {
123 let (thinking, answer_content) = ChatMessage::extract_thinking(&content);
124
125 let message = ChatMessage {
126 role,
127 content: answer_content,
128 timestamp: chrono::Local::now(),
129 actions: Vec::new(),
130 thinking,
131 images: None,
132 tool_calls: None,
133 tool_call_id: None,
134 tool_name: None,
135 };
136 self.session_state.messages.push(message.clone());
137
138 if let Some(ref mut conv) = self.session_state.current_conversation {
139 conv.add_messages(&[message]);
140 }
141 }
142
143 pub fn add_message_with_images(&mut self, role: MessageRole, content: String, images: Option<Vec<String>>) {
145 let (thinking, answer_content) = ChatMessage::extract_thinking(&content);
146
147 let message = ChatMessage {
148 role,
149 content: answer_content,
150 timestamp: chrono::Local::now(),
151 actions: Vec::new(),
152 thinking,
153 images,
154 tool_calls: None,
155 tool_call_id: None,
156 tool_name: None,
157 };
158 self.session_state.messages.push(message.clone());
159
160 if let Some(ref mut conv) = self.session_state.current_conversation {
161 conv.add_messages(&[message]);
162 }
163 }
164
165 pub fn add_assistant_message_with_tool_calls(
169 &mut self,
170 content: String,
171 tool_calls: Vec<crate::models::ToolCall>,
172 ) {
173 let (thinking, answer_content) = ChatMessage::extract_thinking(&content);
174
175 let message = ChatMessage {
176 role: MessageRole::Assistant,
177 content: answer_content,
178 timestamp: chrono::Local::now(),
179 actions: Vec::new(),
180 thinking,
181 images: None,
182 tool_calls: if tool_calls.is_empty() { None } else { Some(tool_calls) },
183 tool_call_id: None,
184 tool_name: None,
185 };
186 self.session_state.messages.push(message.clone());
187
188 if let Some(ref mut conv) = self.session_state.current_conversation {
189 conv.add_messages(&[message]);
190 }
191 }
192
193 pub fn add_tool_result(
200 &mut self,
201 tool_call_id: String,
202 tool_name: String,
203 content: String,
204 ) {
205 let message = ChatMessage {
206 role: MessageRole::Tool,
207 content,
208 timestamp: chrono::Local::now(),
209 actions: Vec::new(),
210 thinking: None,
211 images: None,
212 tool_calls: None,
213 tool_call_id: Some(tool_call_id),
214 tool_name: Some(tool_name),
215 };
216 self.session_state.messages.push(message.clone());
217
218 if let Some(ref mut conv) = self.session_state.current_conversation {
219 conv.add_messages(&[message]);
220 }
221 }
222
223 pub fn clear_input(&mut self) {
225 self.input.clear();
226 }
227
228 pub fn set_status(&mut self, message: impl Into<String>) {
232 self.status_state.set(message);
233 }
234
235 pub fn clear_status(&mut self) {
237 self.status_state.clear();
238 }
239
240 pub fn display_error(&mut self, summary: impl Into<String>, detail: impl Into<String>) {
244 let summary = summary.into();
245 let detail = detail.into();
246
247 self.set_status(format!("[Error] {}", summary));
248
249 if detail.is_empty() {
250 self.add_message(MessageRole::System, format!("Error: {}", summary));
251 } else {
252 self.add_message(MessageRole::System, detail);
253 }
254 }
255
256 pub fn display_error_simple(&mut self, message: impl Into<String>) {
258 let message = message.into();
259 self.display_error(message.clone(), message);
260 }
261
262 pub fn log_error(&mut self, entry: ErrorEntry) {
264 self.status_state.set(entry.display());
265 self.error_log.push_back(entry);
266 if self.error_log.len() > UI_ERROR_LOG_MAX_SIZE {
267 self.error_log.pop_front(); }
269 }
270
271 pub fn log_error_msg(&mut self, severity: ErrorSeverity, msg: impl Into<String>) {
273 self.log_error(ErrorEntry::new(severity, msg.into()));
274 }
275
276 pub fn log_error_with_context(
278 &mut self,
279 severity: ErrorSeverity,
280 msg: impl Into<String>,
281 context: impl Into<String>,
282 ) {
283 self.log_error(ErrorEntry::with_context(severity, msg.into(), context.into()));
284 }
285
286 pub fn recent_errors(&self, count: usize) -> Vec<&ErrorEntry> {
288 self.error_log.iter().rev().take(count).collect()
289 }
290
291 pub fn set_terminal_title(&self, title: &str) {
295 use crossterm::{execute, terminal::SetTitle};
296 use std::io::stdout;
297 let _ = execute!(stdout(), SetTitle(title));
298 }
299
300 pub async fn generate_conversation_title(&mut self) {
304 if self.session_state.conversation_title.is_some() || self.session_state.messages.len() < 2 {
305 return;
306 }
307
308 let mut conversation_summary = String::new();
309 for (i, msg) in self.session_state.messages.iter().take(4).enumerate() {
310 let role = match msg.role {
311 MessageRole::User => "User",
312 MessageRole::Assistant => "Assistant",
313 MessageRole::System | MessageRole::Tool => continue,
314 };
315 conversation_summary.push_str(&format!(
316 "{}: {}\n\n",
317 role,
318 msg.content.chars().take(200).collect::<String>()
319 ));
320 if i >= 3 { break; }
321 }
322
323 let title_prompt = format!(
324 "Based on this conversation, generate a short, descriptive title (2-4 words maximum, no quotes):\n\n{}\n\nTitle:",
325 conversation_summary
326 );
327
328 let messages = vec![ChatMessage {
329 role: MessageRole::User,
330 content: title_prompt,
331 timestamp: chrono::Local::now(),
332 actions: Vec::new(),
333 thinking: None,
334 images: None,
335 tool_calls: None,
336 tool_call_id: None,
337 tool_name: None,
338 }];
339
340 let title_string = Arc::new(tokio::sync::Mutex::new(String::new()));
341 let title_clone = Arc::clone(&title_string);
342
343 let callback: StreamCallback = Arc::new(move |chunk: &str| {
344 if let Ok(mut title) = title_clone.try_lock() {
345 title.push_str(chunk);
346 }
347 });
348
349 let model = self.model_state.model.write().await;
350 let mut config = ModelConfig::default();
351 config.model = self.model_state.model_id.clone();
352
353 if model.chat(&messages, &config, Some(callback)).await.is_ok() {
354 let final_title = title_string.lock().await;
355 let title = final_title.lines().next().unwrap_or(&final_title)
356 .trim()
357 .trim_matches(|c| c == '"' || c == '\'' || c == '.' || c == ',')
358 .chars()
359 .take(50)
360 .collect::<String>();
361
362 if !title.is_empty() {
363 self.session_state.conversation_title = Some(title);
364 }
365 }
366 }
367
368 pub fn scroll_up(&mut self, amount: u16) {
371 self.ui_state.chat_state.scroll_up(amount);
372 }
373
374 pub fn scroll_down(&mut self, amount: u16) {
375 self.ui_state.chat_state.scroll_down(amount);
376 }
377
378 pub fn quit(&mut self) {
381 self.running = false;
382 }
383
384 pub fn build_message_history(&self) -> Vec<ChatMessage> {
389 self.session_state.messages
390 .iter()
391 .filter(|msg| {
392 msg.role == MessageRole::User
393 || msg.role == MessageRole::Assistant
394 || msg.role == MessageRole::Tool
395 })
396 .cloned()
397 .collect()
398 }
399
400 pub fn build_managed_message_history(
401 &self,
402 max_context_tokens: usize,
403 reserve_tokens: usize,
404 ) -> Vec<ChatMessage> {
405 use crate::utils::Tokenizer;
406
407 let tokenizer = Tokenizer::new(&self.model_state.model_name);
408 let available_tokens = max_context_tokens.saturating_sub(reserve_tokens);
409
410 let all_messages: Vec<ChatMessage> = self
412 .session_state
413 .messages
414 .iter()
415 .filter(|msg| {
416 msg.role == MessageRole::User
417 || msg.role == MessageRole::Assistant
418 || msg.role == MessageRole::Tool
419 })
420 .cloned()
421 .collect();
422
423 if all_messages.is_empty() {
424 return Vec::new();
425 }
426
427 let messages_for_counting: Vec<(String, String)> = all_messages
428 .iter()
429 .map(|msg| {
430 let role = match msg.role {
431 MessageRole::User => "user",
432 MessageRole::Assistant => "assistant",
433 MessageRole::System => "system",
434 MessageRole::Tool => "tool",
435 };
436 (role.to_string(), msg.content.clone())
437 })
438 .collect();
439
440 let total_tokens = tokenizer
441 .count_chat_tokens(&messages_for_counting)
442 .unwrap_or_else(|_| all_messages.iter().map(|m| m.content.len() / 4).sum());
443
444 if total_tokens <= available_tokens {
445 return all_messages;
446 }
447
448 let mut kept_messages = Vec::new();
449 let mut current_tokens = 0;
450
451 for msg in all_messages.iter().rev() {
452 let msg_text = vec![(
453 match msg.role {
454 MessageRole::User => "user",
455 MessageRole::Assistant => "assistant",
456 MessageRole::System => "system",
457 MessageRole::Tool => "tool",
458 }
459 .to_string(),
460 msg.content.clone(),
461 )];
462
463 let msg_tokens = tokenizer
464 .count_chat_tokens(&msg_text)
465 .unwrap_or(msg.content.len() / 4);
466
467 if current_tokens + msg_tokens <= available_tokens {
468 kept_messages.push(msg.clone());
469 current_tokens += msg_tokens;
470 } else if kept_messages.len() < 2 {
471 kept_messages.push(msg.clone());
472 break;
473 } else {
474 break;
475 }
476 }
477
478 kept_messages.reverse();
479 kept_messages
480 }
481
482 pub fn load_conversation(&mut self, conversation: ConversationHistory) {
485 self.session_state.messages = conversation.messages.clone();
486 self.session_state.current_conversation = Some(conversation);
487 self.set_status("Conversation loaded");
488 }
489
490 pub fn save_conversation(&mut self) -> anyhow::Result<()> {
491 if let Some(ref manager) = self.session_state.conversation_manager {
492 if let Some(ref mut conv) = self.session_state.current_conversation {
493 conv.messages = self.session_state.messages.clone();
494 manager.save_conversation(conv)?;
495 self.set_status("Conversation saved");
496 }
497 }
498 Ok(())
499 }
500
501 pub fn auto_save_conversation(&mut self) {
502 if self.session_state.messages.is_empty() {
503 return;
504 }
505 if let Err(e) = self.save_conversation() {
506 warn!("Failed to auto-save conversation: {}", e);
507 }
508 }
509
510 pub fn start_generation(&mut self, abort_handle: tokio::task::AbortHandle) {
513 self.operation_state.accumulated_tool_calls.clear();
515
516 self.app_state = AppState::Generating {
517 status: GenerationStatus::Sending,
518 start_time: std::time::Instant::now(),
519 tokens_received: 0,
520 abort_handle: Some(abort_handle),
521 };
522 }
523
524 pub fn transition_to_thinking(&mut self) {
525 if let AppState::Generating { start_time, tokens_received, ref abort_handle, .. } = self.app_state {
526 self.app_state = AppState::Generating {
527 status: GenerationStatus::Thinking,
528 start_time,
529 tokens_received,
530 abort_handle: abort_handle.clone(),
531 };
532 }
533 }
534
535 pub fn transition_to_streaming(&mut self) {
536 if let AppState::Generating { start_time, tokens_received, ref abort_handle, .. } = self.app_state {
537 self.app_state = AppState::Generating {
538 status: GenerationStatus::Streaming,
539 start_time,
540 tokens_received,
541 abort_handle: abort_handle.clone(),
542 };
543 }
544 }
545
546 pub fn set_final_tokens(&mut self, count: usize) {
548 if let AppState::Generating { status, start_time, ref abort_handle, .. } = self.app_state {
549 self.app_state = AppState::Generating {
550 status,
551 start_time,
552 tokens_received: count,
553 abort_handle: abort_handle.clone(),
554 };
555 self.session_state.add_tokens(count);
556 }
557 }
558
559 pub fn stop_generation(&mut self) {
560 self.app_state = AppState::Idle;
561 }
562
563 pub fn abort_generation(&mut self) -> Option<tokio::task::AbortHandle> {
564 if let AppState::Generating { abort_handle, .. } = &mut self.app_state {
565 let handle = abort_handle.take();
566 self.app_state = AppState::Idle;
567 handle
568 } else {
569 None
570 }
571 }
572
573}