mermaid_cli/tui/
app.rs

1use super::mode::OperationMode;
2use super::theme::Theme;
3use super::widgets::{ChatState, InputState};
4use crate::agents::{AgentAction, ModeAwareExecutor, Plan};
5use crate::constants::UI_ERROR_LOG_MAX_SIZE;
6use crate::context::ContextManager;
7use crate::models::{ChatMessage, MessageRole, Model, ModelConfig, ProjectContext, StreamCallback};
8use crate::session::{ConversationHistory, ConversationManager};
9use std::sync::Arc;
10use std::time::Instant;
11use tokio::sync::RwLock;
12
13/// Generation status for the status line
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum GenerationStatus {
16    /// Not currently generating
17    Idle,
18    /// Message sent upstream, waiting for model to start responding
19    Sending,
20    /// Model is loading/initializing (before first token)
21    Initializing,
22    /// Waiting for first token from model (thinking/reasoning)
23    Thinking,
24    /// Actively receiving and displaying tokens
25    Streaming,
26}
27
28impl GenerationStatus {
29    pub fn display_text(&self) -> &str {
30        match self {
31            GenerationStatus::Idle => "Idle",
32            GenerationStatus::Sending => "Sending",
33            GenerationStatus::Initializing => "Initializing",
34            GenerationStatus::Thinking => "Thinking",
35            GenerationStatus::Streaming => "Streaming",
36        }
37    }
38}
39
40/// Error severity level
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum ErrorSeverity {
43    /// Informational (not really an error)
44    Info,
45    /// Warning - operation completed but with issues
46    Warning,
47    /// Error - operation failed
48    Error,
49    /// Security error - action was rejected
50    Security,
51}
52
53impl ErrorSeverity {
54    pub fn display(&self) -> &str {
55        match self {
56            ErrorSeverity::Info => "INFO",
57            ErrorSeverity::Warning => "WARN",
58            ErrorSeverity::Error => "ERROR",
59            ErrorSeverity::Security => "SECURITY",
60        }
61    }
62
63    pub fn color(&self) -> &str {
64        match self {
65            ErrorSeverity::Info => "cyan",
66            ErrorSeverity::Warning => "yellow",
67            ErrorSeverity::Error => "red",
68            ErrorSeverity::Security => "magenta",
69        }
70    }
71}
72
73/// An error entry in the error log
74#[derive(Debug, Clone)]
75pub struct ErrorEntry {
76    pub timestamp: Instant,
77    pub severity: ErrorSeverity,
78    pub message: String,
79    pub context: Option<String>,
80}
81
82impl ErrorEntry {
83    pub fn new(severity: ErrorSeverity, message: String) -> Self {
84        Self {
85            timestamp: Instant::now(),
86            severity,
87            message,
88            context: None,
89        }
90    }
91
92    pub fn with_context(severity: ErrorSeverity, message: String, context: String) -> Self {
93        Self {
94            timestamp: Instant::now(),
95            severity,
96            message,
97            context: Some(context),
98        }
99    }
100
101    pub fn display(&self) -> String {
102        match &self.context {
103            Some(ctx) => format!(
104                "[{}] {} - {}",
105                self.severity.display(),
106                self.message,
107                ctx
108            ),
109            None => format!("[{}] {}", self.severity.display(), self.message),
110        }
111    }
112}
113
114/// Comprehensive state machine for the application lifecycle
115/// Impossible states become impossible to represent
116#[derive(Debug, Clone)]
117pub enum AppState {
118    /// Idle - not doing anything, ready for input
119    Idle,
120
121    /// Currently generating a response from the model
122    Generating {
123        status: GenerationStatus,
124        start_time: Instant,
125        tokens_received: usize,
126        abort_handle: Option<tokio::task::AbortHandle>,
127    },
128
129    /// Generation complete, waiting for action confirmation
130    AwaitingActionConfirmation {
131        action: AgentAction,
132        executor: ModeAwareExecutor,
133    },
134
135    /// Executing an action
136    ExecutingAction {
137        action: AgentAction,
138        start_time: Instant,
139    },
140
141    /// Waiting for user to approve a plan
142    AwaitingPlanApproval {
143        plan: Plan,
144        explanation: Option<String>,
145    },
146
147    /// Executing a plan step-by-step
148    ExecutingPlan {
149        plan: Plan,
150        current_step: usize,
151        start_time: Instant,
152    },
153
154    /// Waiting for file read feedback from user
155    ReadingFileFeedback {
156        intent: Option<String>,
157        started_at: Instant,
158    },
159}
160
161impl AppState {
162    /// Get generation status if we're generating
163    pub fn generation_status(&self) -> Option<GenerationStatus> {
164        match self {
165            AppState::Generating { status, .. } => Some(*status),
166            _ => None,
167        }
168    }
169
170    /// Check if we're currently generating
171    pub fn is_generating(&self) -> bool {
172        matches!(self, AppState::Generating { .. })
173    }
174
175    /// Check if we're idle
176    pub fn is_idle(&self) -> bool {
177        matches!(self, AppState::Idle)
178    }
179
180    /// Get generation start time if we're generating
181    pub fn generation_start_time(&self) -> Option<Instant> {
182        match self {
183            AppState::Generating { start_time, .. } => Some(*start_time),
184            _ => None,
185        }
186    }
187
188    /// Get tokens received if we're generating
189    pub fn tokens_received(&self) -> Option<usize> {
190        match self {
191            AppState::Generating { tokens_received, .. } => Some(*tokens_received),
192            _ => None,
193        }
194    }
195
196    /// Get abort handle if we're generating
197    pub fn abort_handle(&self) -> Option<&tokio::task::AbortHandle> {
198        match self {
199            AppState::Generating { abort_handle, .. } => abort_handle.as_ref(),
200            _ => None,
201        }
202    }
203
204    /// Get pending action if awaiting confirmation
205    pub fn pending_action(&self) -> Option<&AgentAction> {
206        match self {
207            AppState::AwaitingActionConfirmation { action, .. } => Some(action),
208            _ => None,
209        }
210    }
211
212    /// Get executor if awaiting confirmation
213    pub fn pending_executor(&self) -> Option<&ModeAwareExecutor> {
214        match self {
215            AppState::AwaitingActionConfirmation { executor, .. } => Some(executor),
216            _ => None,
217        }
218    }
219
220    /// Check if awaiting plan approval
221    pub fn is_awaiting_plan_approval(&self) -> bool {
222        matches!(self, AppState::AwaitingPlanApproval { .. })
223    }
224
225    /// Get active plan if in plan-related state
226    pub fn active_plan(&self) -> Option<&Plan> {
227        match self {
228            AppState::AwaitingPlanApproval { plan, .. } => Some(plan),
229            AppState::ExecutingPlan { plan, .. } => Some(plan),
230            _ => None,
231        }
232    }
233
234    /// Check if currently reading file feedback
235    pub fn is_reading_file_feedback(&self) -> bool {
236        matches!(self, AppState::ReadingFileFeedback { .. })
237    }
238
239    /// Get current plan step if executing plan
240    pub fn plan_current_step(&self) -> Option<usize> {
241        match self {
242            AppState::ExecutingPlan { current_step, .. } => Some(*current_step),
243            _ => None,
244        }
245    }
246
247    /// Get action start time if executing
248    pub fn action_start_time(&self) -> Option<Instant> {
249        match self {
250            AppState::ExecutingAction { start_time, .. } => Some(*start_time),
251            _ => None,
252        }
253    }
254}
255
256/// UI state - visual presentation and widget states
257pub struct UIState {
258    /// Chat widget state (scroll, scrolling flag)
259    pub chat_state: ChatState,
260    /// Input widget state (cursor position for display)
261    pub input_state: InputState,
262    /// UI theme
263    pub theme: Theme,
264    /// Selected message index (for navigation)
265    pub selected_message: Option<usize>,
266}
267
268impl UIState {
269    /// Create a new UIState with default values
270    pub fn new() -> Self {
271        Self {
272            chat_state: ChatState::default(),
273            input_state: InputState::default(),
274            theme: Theme::dark(),
275            selected_message: None,
276        }
277    }
278}
279
280/// Session state - conversation history and persistence
281pub struct SessionState {
282    /// Current chat messages
283    pub messages: Vec<ChatMessage>,
284    /// Conversation manager for persistence
285    pub conversation_manager: Option<ConversationManager>,
286    /// Current conversation being tracked
287    pub current_conversation: Option<ConversationHistory>,
288    /// Input history for arrow key navigation (loaded from session)
289    pub input_history: Vec<String>,
290    /// Current position in history (None = editing current input, Some(i) = viewing history[i])
291    pub history_index: Option<usize>,
292    /// Saved input when navigating away from current draft
293    pub history_buffer: String,
294    /// Context manager for dynamic file tree reloading
295    pub context_manager: Option<ContextManager>,
296    /// Cumulative token count for the entire conversation
297    pub cumulative_tokens: usize,
298    /// Auto-generated conversation title (like Claude Code)
299    pub conversation_title: Option<String>,
300}
301
302impl SessionState {
303    /// Create a new SessionState with default values
304    pub fn new() -> Self {
305        Self {
306            messages: Vec::new(),
307            conversation_manager: None,
308            current_conversation: None,
309            input_history: Vec::new(),
310            history_index: None,
311            history_buffer: String::new(),
312            context_manager: None,
313            cumulative_tokens: 0,
314            conversation_title: None,
315        }
316    }
317
318    /// Create SessionState with conversation management
319    pub fn with_conversation(
320        conversation_manager: Option<ConversationManager>,
321        current_conversation: Option<ConversationHistory>,
322        input_history: Vec<String>,
323    ) -> Self {
324        Self {
325            messages: Vec::new(),
326            conversation_manager,
327            current_conversation,
328            input_history,
329            history_index: None,
330            history_buffer: String::new(),
331            context_manager: None,
332            cumulative_tokens: 0,
333            conversation_title: None,
334        }
335    }
336}
337
338/// Operation state - mode, confirmations, and plan execution
339pub struct OperationState {
340    /// Current operation mode (Normal, AcceptEdits, PlanMode, BypassAll)
341    pub operation_mode: OperationMode,
342    /// Flag for confirming destructive operations in BypassAll mode
343    pub bypass_confirmed: bool,
344    /// Track if FILE_READ feedback is pending
345    pub pending_file_read: bool,
346    /// Current step index during plan execution (0-based)
347    pub plan_execution_index: Option<usize>,
348    /// Track if plan mode was active when generation started
349    pub plan_mode_active_for_generation: bool,
350    /// Status text to show during file reading
351    pub reading_file_status: Option<String>,
352    /// Current confirmation state
353    pub confirmation_state: Option<ConfirmationState>,
354}
355
356impl OperationState {
357    /// Create a new OperationState with default values
358    pub fn new() -> Self {
359        Self {
360            operation_mode: OperationMode::default(),
361            bypass_confirmed: false,
362            pending_file_read: false,
363            plan_execution_index: None,
364            plan_mode_active_for_generation: false,
365            reading_file_status: None,
366            confirmation_state: None,
367        }
368    }
369}
370
371/// Model state - LLM configuration and identity
372pub struct ModelState {
373    pub model: Arc<RwLock<Box<dyn Model>>>,
374    pub model_id: String,
375    pub model_name: String,
376}
377
378impl ModelState {
379    pub fn new(model: Box<dyn Model>, model_id: String) -> Self {
380        let model_name = model.name().to_string();
381        Self {
382            model: Arc::new(RwLock::new(model)),
383            model_id,
384            model_name,
385        }
386    }
387}
388
389/// Status state - UI status messages and timing
390#[derive(Debug)]
391pub struct StatusState {
392    pub status_message: Option<String>,
393    pub status_timestamp: Option<Instant>,
394    pub custom_status: Option<String>,
395}
396
397impl StatusState {
398    pub fn new() -> Self {
399        Self {
400            status_message: None,
401            status_timestamp: None,
402            custom_status: None,
403        }
404    }
405}
406
407/// Application state
408pub struct App {
409    /// User input buffer
410    pub input: String,
411    /// Cursor position in the input string
412    pub cursor_position: usize,
413    /// Is the app running?
414    pub running: bool,
415    /// Project context
416    pub context: ProjectContext,
417    /// Current model response (for streaming)
418    pub current_response: String,
419    /// Current working directory
420    pub working_dir: String,
421    /// Error log - keeps last N errors for visibility (no more silent failures!)
422    pub error_log: Vec<ErrorEntry>,
423    /// State machine for application lifecycle
424    pub app_state: AppState,
425
426    /// Model state - LLM configuration
427    pub model_state: ModelState,
428    /// UI state - visual presentation and widget states
429    pub ui_state: UIState,
430    /// Session state - conversation history and persistence
431    pub session_state: SessionState,
432    /// Operation state - mode, confirmations, and plan execution
433    pub operation_state: OperationState,
434    /// Status state - UI status messages
435    pub status_state: StatusState,
436}
437
438impl App {
439    /// Create a new app instance
440    pub fn new(model: Box<dyn Model>, context: ProjectContext, model_id: String) -> Self {
441        let working_dir = std::env::current_dir()
442            .map(|p| p.to_string_lossy().to_string())
443            .unwrap_or_else(|_| ".".to_string());
444
445        // Initialize model state
446        let model_state = ModelState::new(model, model_id);
447
448        // Initialize conversation manager for the current directory
449        let conversation_manager = ConversationManager::new(&working_dir).ok();
450        let current_conversation = conversation_manager
451            .as_ref()
452            .map(|_| ConversationHistory::new(working_dir.clone(), model_state.model_name.clone()));
453
454        // Load input history from conversation if available
455        let input_history = conversation_manager
456            .as_ref()
457            .and_then(|_| current_conversation.as_ref())
458            .map(|conv| conv.input_history.clone())
459            .unwrap_or_default();
460
461        // Initialize UIState
462        let ui_state = UIState {
463            chat_state: ChatState::new(),
464            input_state: InputState::new(),
465            theme: Theme::dark(), // Default to dark theme
466            selected_message: None,
467        };
468
469        // Initialize SessionState with conversation management
470        let session_state = SessionState {
471            messages: Vec::new(),
472            conversation_manager,
473            current_conversation,
474            input_history,
475            history_index: None,
476            history_buffer: String::new(),
477            context_manager: None,
478            cumulative_tokens: 0,
479            conversation_title: None,
480        };
481
482        // Initialize OperationState
483        let operation_state = OperationState::new();
484
485        Self {
486            input: String::new(),
487            cursor_position: 0,
488            running: true,
489            context,
490            current_response: String::new(),
491            working_dir,
492            error_log: Vec::new(),
493            app_state: AppState::Idle,
494            model_state,
495            ui_state,
496            session_state,
497            operation_state,
498            status_state: StatusState::new(),
499        }
500    }
501
502    /// Set the context manager for dynamic context reloading
503    pub fn set_context_manager(&mut self, manager: ContextManager) {
504        self.session_state.context_manager = Some(manager);
505    }
506
507    /// Update context if file tree has changed since last message
508    pub async fn reload_context_if_needed(&mut self) -> anyhow::Result<()> {
509        if let Some(ref mut manager) = self.session_state.context_manager {
510            if manager.reload_if_needed().await? {
511                // Context changed, rebuild it
512                self.context = manager.build_context();
513            }
514        }
515        Ok(())
516    }
517
518    /// Add a message to the chat
519    pub fn add_message(&mut self, role: MessageRole, content: String) {
520        // Extract thinking blocks if present
521        let (thinking, answer_content) = ChatMessage::extract_thinking(&content);
522
523        let message = ChatMessage {
524            role,
525            content: answer_content,
526            timestamp: chrono::Local::now(),
527            actions: Vec::new(),
528            thinking,
529            images: None,
530            tool_calls: None,
531        };
532        self.session_state.messages.push(message.clone());
533
534        // Update current conversation
535        if let Some(ref mut conv) = self.session_state.current_conversation {
536            conv.add_messages(&[message]);
537        }
538
539        // Auto-scroll to bottom when adding messages if not manually scrolling
540        // Note: Proper scrolling now happens in the main loop with viewport height
541        // This is just a placeholder for compatibility
542    }
543
544    /// Clear the input buffer
545    pub fn clear_input(&mut self) {
546        self.input.clear();
547        self.cursor_position = 0;
548    }
549
550    /// Set status message
551    pub fn set_status(&mut self, message: impl Into<String>) {
552        self.status_state.status_message = Some(message.into());
553    }
554
555    /// Clear status message
556    pub fn clear_status(&mut self) {
557        self.status_state.status_message = None;
558    }
559
560    /// Set terminal window title
561    pub fn set_terminal_title(&self, title: &str) {
562        use crossterm::{execute, terminal::SetTitle};
563        use std::io::stdout;
564        let _ = execute!(stdout(), SetTitle(title));
565    }
566
567    /// Generate conversation title from current messages
568    pub async fn generate_conversation_title(&mut self) {
569        // Don't generate if already have a title or less than 2 messages
570        if self.session_state.conversation_title.is_some() || self.session_state.messages.len() < 2 {
571            return;
572        }
573
574        // Build prompt for title generation
575        let mut conversation_summary = String::new();
576        for (i, msg) in self.session_state.messages.iter().take(4).enumerate() {
577            let role = match msg.role {
578                MessageRole::User => "User",
579                MessageRole::Assistant => "Assistant",
580                MessageRole::System => continue, // Skip system messages
581            };
582            conversation_summary.push_str(&format!("{}: {}\n\n", role, msg.content.chars().take(200).collect::<String>()));
583            if i >= 3 { break; } // Only use first 4 messages
584        }
585
586        let title_prompt = format!(
587            "Based on this conversation, generate a short, descriptive title (2-4 words maximum, no quotes):\n\n{}\n\nTitle:",
588            conversation_summary
589        );
590
591        // Send title generation request to model
592        let messages = vec![ChatMessage {
593            role: MessageRole::User,
594            content: title_prompt,
595            timestamp: chrono::Local::now(),
596            actions: Vec::new(),
597            thinking: None,
598            images: None,
599            tool_calls: None,
600        }];
601
602        // Use the model to generate title
603        let title_string = Arc::new(tokio::sync::Mutex::new(String::new()));
604        let title_clone = Arc::clone(&title_string);
605
606        let callback: StreamCallback = Arc::new(move |chunk: &str| {
607            if let Ok(mut title) = title_clone.try_lock() {
608                title.push_str(chunk);
609            }
610        });
611
612        let mut model = self.model_state.model.write().await;
613        let mut config = ModelConfig::default();
614        config.model = self.model_state.model_id.clone();
615
616        if let Ok(_) = model.chat(&messages, &self.context, &config, Some(callback)).await {
617            let final_title = title_string.lock().await;
618            // Clean up title (remove quotes, trim whitespace, take first line)
619            let title = final_title.lines().next().unwrap_or(&final_title)
620                .trim()
621                .trim_matches(|c| c == '"' || c == '\'' || c == '.' || c == ',')
622                .chars()
623                .take(50)
624                .collect::<String>();
625
626            if !title.is_empty() {
627                self.session_state.conversation_title = Some(title);
628            }
629        }
630    }
631
632    /// Log an error to the error log (no more silent failures!)
633    pub fn log_error(&mut self, entry: ErrorEntry) {
634        // Also show in status bar
635        self.status_state.status_message = Some(entry.display());
636
637        // Keep last N errors in log
638        self.error_log.push(entry);
639        if self.error_log.len() > UI_ERROR_LOG_MAX_SIZE {
640            self.error_log.remove(0);
641        }
642    }
643
644    /// Convenience: log a simple error message
645    pub fn log_error_msg(&mut self, severity: ErrorSeverity, msg: impl Into<String>) {
646        self.log_error(ErrorEntry::new(severity, msg.into()));
647    }
648
649    /// Convenience: log error with context
650    pub fn log_error_with_context(
651        &mut self,
652        severity: ErrorSeverity,
653        msg: impl Into<String>,
654        context: impl Into<String>,
655    ) {
656        self.log_error(ErrorEntry::with_context(severity, msg.into(), context.into()));
657    }
658
659    /// Get recent errors (last N)
660    pub fn recent_errors(&self, count: usize) -> Vec<&ErrorEntry> {
661        self.error_log.iter().rev().take(count).collect()
662    }
663
664    /// Scroll chat view up
665    pub fn scroll_up(&mut self, amount: u16) {
666        self.ui_state.chat_state.scroll_up(amount);
667    }
668
669    /// Scroll chat view down
670    pub fn scroll_down(&mut self, amount: u16) {
671        self.ui_state.chat_state.scroll_down(amount);
672    }
673
674    /// Quit the application
675    pub fn quit(&mut self) {
676        self.running = false;
677    }
678
679    /// Cycle to the next operation mode
680    pub fn cycle_mode(&mut self) {
681        self.operation_state.operation_mode = self.operation_state.operation_mode.cycle();
682        self.operation_state.bypass_confirmed = false; // Reset confirmation flag when changing modes
683        self.set_status(format!("Mode: {}", self.operation_state.operation_mode.display_name()));
684    }
685
686    /// Cycle to the previous operation mode
687    pub fn cycle_mode_reverse(&mut self) {
688        self.operation_state.operation_mode = self.operation_state.operation_mode.cycle_reverse();
689        self.operation_state.bypass_confirmed = false;
690        self.set_status(format!("Mode: {}", self.operation_state.operation_mode.display_name()));
691    }
692
693    /// Set a specific operation mode
694    pub fn set_mode(&mut self, mode: OperationMode) {
695        if self.operation_state.operation_mode != mode {
696            // If switching away from PlanMode and a plan is awaiting approval, cancel it
697            if self.operation_state.operation_mode == OperationMode::PlanMode && self.app_state.is_awaiting_plan_approval() {
698                self.cancel_plan();
699                self.set_status(format!(
700                    "Plan cancelled - switched to {}",
701                    mode.display_name()
702                ));
703            }
704
705            self.operation_state.operation_mode = mode;
706            self.operation_state.bypass_confirmed = false;
707            self.set_status(format!("Mode: {}", mode.display_name()));
708        }
709    }
710
711    /// Toggle bypass mode (Ctrl+Y shortcut)
712    pub fn toggle_bypass_mode(&mut self) {
713        if self.operation_state.operation_mode == OperationMode::BypassAll {
714            self.set_mode(OperationMode::Normal);
715        } else {
716            self.set_mode(OperationMode::BypassAll);
717        }
718    }
719
720    /// Build message history for sending to the model
721    /// Includes only user and assistant messages (not system messages from the UI)
722    pub fn build_message_history(&self) -> Vec<ChatMessage> {
723        self.session_state.messages
724            .iter()
725            .filter(|msg| msg.role == MessageRole::User || msg.role == MessageRole::Assistant)
726            .cloned()
727            .collect()
728    }
729
730    /// Build message history with token management
731    /// Ensures the conversation doesn't exceed the model's context window
732    pub fn build_managed_message_history(
733        &self,
734        max_context_tokens: usize,
735        reserve_tokens: usize,
736    ) -> Vec<ChatMessage> {
737        use crate::utils::Tokenizer;
738
739        let tokenizer = Tokenizer::new(&self.model_state.model_name);
740        let available_tokens = max_context_tokens.saturating_sub(reserve_tokens);
741
742        // Get all relevant messages
743        let all_messages: Vec<ChatMessage> = self
744            .session_state
745            .messages
746            .iter()
747            .filter(|msg| msg.role == MessageRole::User || msg.role == MessageRole::Assistant)
748            .cloned()
749            .collect();
750
751        // If no messages, return empty
752        if all_messages.is_empty() {
753            return Vec::new();
754        }
755
756        // Try to keep all messages first
757        let messages_for_counting: Vec<(String, String)> = all_messages
758            .iter()
759            .map(|msg| {
760                let role = match msg.role {
761                    MessageRole::User => "user",
762                    MessageRole::Assistant => "assistant",
763                    MessageRole::System => "system",
764                };
765                (role.to_string(), msg.content.clone())
766            })
767            .collect();
768
769        let total_tokens = tokenizer
770            .count_chat_tokens(&messages_for_counting)
771            .unwrap_or_else(|_| {
772                // Fallback: estimate 4 chars per token
773                all_messages.iter().map(|m| m.content.len() / 4).sum()
774            });
775
776        // If we're within budget, return all messages
777        if total_tokens <= available_tokens {
778            return all_messages;
779        }
780
781        // Otherwise, trim from the beginning, keeping the most recent messages
782        // Always keep at least the last message pair (user + assistant)
783        let mut kept_messages = Vec::new();
784        let mut current_tokens = 0;
785
786        // Start from the most recent and work backwards
787        for msg in all_messages.iter().rev() {
788            let msg_text = vec![(
789                match msg.role {
790                    MessageRole::User => "user",
791                    MessageRole::Assistant => "assistant",
792                    MessageRole::System => "system",
793                }
794                .to_string(),
795                msg.content.clone(),
796            )];
797
798            let msg_tokens = tokenizer
799                .count_chat_tokens(&msg_text)
800                .unwrap_or(msg.content.len() / 4);
801
802            if current_tokens + msg_tokens <= available_tokens {
803                kept_messages.push(msg.clone());
804                current_tokens += msg_tokens;
805            } else if kept_messages.len() < 2 {
806                // Always keep at least one message pair
807                kept_messages.push(msg.clone());
808                break;
809            } else {
810                break;
811            }
812        }
813
814        // Reverse to restore chronological order
815        kept_messages.reverse();
816        kept_messages
817    }
818
819    /// Load a conversation history
820    pub fn load_conversation(&mut self, conversation: ConversationHistory) {
821        // Load messages from the conversation
822        self.session_state.messages = conversation.messages.clone();
823        self.session_state.current_conversation = Some(conversation);
824        self.set_status("Conversation loaded");
825    }
826
827    /// Save the current conversation
828    pub fn save_conversation(&mut self) -> anyhow::Result<()> {
829        if let Some(ref manager) = self.session_state.conversation_manager {
830            if let Some(ref mut conv) = self.session_state.current_conversation {
831                // Update messages in conversation
832                conv.messages = self.session_state.messages.clone();
833                manager.save_conversation(conv)?;
834                self.set_status("Conversation saved");
835            }
836        }
837        Ok(())
838    }
839
840    /// Auto-save the conversation (called on exit)
841    pub fn auto_save_conversation(&mut self) {
842        if self.session_state.messages.is_empty() {
843            return; // Don't save empty conversations
844        }
845
846        if let Err(e) = self.save_conversation() {
847            eprintln!("Failed to auto-save conversation: {}", e);
848        }
849    }
850
851    // ===== Generation State Transitions =====
852
853    /// Start generation - transitions to Generating state with Sending status
854    pub fn start_generation(&mut self, abort_handle: tokio::task::AbortHandle) {
855        self.app_state = AppState::Generating {
856            status: GenerationStatus::Sending,
857            start_time: std::time::Instant::now(),
858            tokens_received: 0,
859            abort_handle: Some(abort_handle),
860        };
861    }
862
863    /// Transition from Sending to Thinking status
864    pub fn transition_to_thinking(&mut self) {
865        if let AppState::Generating { start_time, tokens_received, ref abort_handle, .. } = self.app_state {
866            self.app_state = AppState::Generating {
867                status: GenerationStatus::Thinking,
868                start_time,
869                tokens_received,
870                abort_handle: abort_handle.clone(),
871            };
872        }
873    }
874
875    /// Transition from Thinking/Sending to Streaming status
876    pub fn transition_to_streaming(&mut self) {
877        if let AppState::Generating { start_time, tokens_received, ref abort_handle, .. } = self.app_state {
878            self.app_state = AppState::Generating {
879                status: GenerationStatus::Streaming,
880                start_time,
881                tokens_received,
882                abort_handle: abort_handle.clone(),
883            };
884        }
885    }
886
887    /// Increment tokens received during generation
888    pub fn increment_tokens(&mut self, count: usize) {
889        if let AppState::Generating { status, start_time, tokens_received, ref abort_handle } = self.app_state {
890            self.app_state = AppState::Generating {
891                status,
892                start_time,
893                tokens_received: tokens_received + count,
894                abort_handle: abort_handle.clone(),
895            };
896            // Also add to cumulative tokens for the entire conversation
897            self.session_state.cumulative_tokens += count;
898        }
899    }
900
901    /// Stop generation - transitions back to Idle state
902    pub fn stop_generation(&mut self) {
903        self.app_state = AppState::Idle;
904    }
905
906    /// Abort generation and return the abort handle if present
907    pub fn abort_generation(&mut self) -> Option<tokio::task::AbortHandle> {
908        if let AppState::Generating { abort_handle, .. } = &mut self.app_state {
909            let handle = abort_handle.take();
910            self.app_state = AppState::Idle;
911            handle
912        } else {
913            None
914        }
915    }
916
917    // ===== Action Confirmation State Transitions =====
918
919    /// Set pending action - transitions to AwaitingActionConfirmation state
920    pub fn set_pending_action(&mut self, action: crate::agents::AgentAction, executor: crate::agents::ModeAwareExecutor) {
921        self.app_state = AppState::AwaitingActionConfirmation {
922            action,
923            executor,
924        };
925    }
926
927    /// Clear pending action - transitions back to Idle state
928    pub fn clear_pending_action(&mut self) {
929        self.app_state = AppState::Idle;
930    }
931
932    // ===== Plan State Transitions =====
933
934    /// Set active plan and enter approval state
935    pub fn set_plan(&mut self, plan: Plan) {
936        self.app_state = AppState::AwaitingPlanApproval {
937            plan: plan.clone(),
938            explanation: plan.explanation.clone(),
939        };
940        self.operation_state.plan_execution_index = None;
941        self.set_status("Plan ready - Ctrl+Y to approve, Ctrl+N to cancel");
942    }
943
944    /// Cancel the active plan
945    pub fn cancel_plan(&mut self) {
946        self.app_state = AppState::Idle;
947        self.operation_state.plan_execution_index = None;
948        self.operation_state.plan_mode_active_for_generation = false;
949        self.set_status("Plan cancelled");
950    }
951
952    /// Start executing the active plan
953    pub fn start_plan_execution(&mut self) {
954        if let AppState::AwaitingPlanApproval { plan, .. } = &self.app_state {
955            let plan_clone = plan.clone();
956            self.app_state = AppState::ExecutingPlan {
957                plan: plan_clone,
958                current_step: 0,
959                start_time: std::time::Instant::now(),
960            };
961            self.operation_state.plan_execution_index = Some(0);
962            self.set_status("Executing plan...");
963        }
964    }
965
966    /// Get the next pending action from the plan
967    pub fn plan_next_action(&self) -> Option<&crate::agents::PlannedAction> {
968        self.app_state
969            .active_plan()
970            .and_then(|plan| plan.next_pending_action().map(|(_, action)| action))
971    }
972
973    /// Mark current plan action as completed
974    pub fn mark_plan_action_completed(&mut self, result: Option<crate::agents::ActionResult>) {
975        if let AppState::ExecutingPlan { plan, current_step, start_time } = &mut self.app_state {
976            let mut plan_clone = plan.clone();
977            if let Some(index) = self.operation_state.plan_execution_index {
978                plan_clone.update_action_status(
979                    index,
980                    crate::agents::ActionStatus::Completed,
981                    result,
982                    None,
983                );
984                self.operation_state.plan_execution_index = Some(index + 1);
985                // Update the plan in the state
986                self.app_state = AppState::ExecutingPlan {
987                    plan: plan_clone,
988                    current_step: *current_step + 1,
989                    start_time: *start_time,
990                };
991            }
992        }
993    }
994
995    /// Mark current plan action as failed
996    pub fn mark_plan_action_failed(&mut self, error: String) {
997        if let AppState::ExecutingPlan { plan, current_step, start_time } = &mut self.app_state {
998            let mut plan_clone = plan.clone();
999            if let Some(index) = self.operation_state.plan_execution_index {
1000                plan_clone.update_action_status(
1001                    index,
1002                    crate::agents::ActionStatus::Failed,
1003                    None,
1004                    Some(error),
1005                );
1006                self.operation_state.plan_execution_index = Some(index + 1);
1007                // Update the plan in the state
1008                self.app_state = AppState::ExecutingPlan {
1009                    plan: plan_clone,
1010                    current_step: *current_step + 1,
1011                    start_time: *start_time,
1012                };
1013            }
1014        }
1015    }
1016
1017    /// Check if plan execution is complete
1018    pub fn is_plan_complete(&self) -> bool {
1019        if let Some(plan) = self.app_state.active_plan() {
1020            plan.stats().is_complete()
1021        } else {
1022            false
1023        }
1024    }
1025
1026    /// Get plan statistics
1027    pub fn get_plan_stats(&self) -> Option<crate::agents::PlanStats> {
1028        self.app_state.active_plan().map(|plan| plan.stats())
1029    }
1030}
1031
1032// AppState removed - we're always in "chat" mode now
1033
1034/// State for action confirmation
1035#[derive(Debug, Clone)]
1036pub struct ConfirmationState {
1037    pub action: AgentAction,
1038    pub action_description: String,
1039    pub preview_lines: Vec<String>,  // First few lines for preview
1040    pub file_info: Option<FileInfo>, // Size, path, overwrite status
1041    pub allow_always: bool,          // Can user select "always approve"?
1042}
1043
1044#[derive(Debug, Clone)]
1045pub struct FileInfo {
1046    pub path: String,
1047    pub size: usize,
1048    pub exists: bool,
1049    pub language: Option<String>,
1050}