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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum GenerationStatus {
16 Idle,
18 Sending,
20 Initializing,
22 Thinking,
24 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum ErrorSeverity {
43 Info,
45 Warning,
47 Error,
49 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#[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#[derive(Debug, Clone)]
117pub enum AppState {
118 Idle,
120
121 Generating {
123 status: GenerationStatus,
124 start_time: Instant,
125 tokens_received: usize,
126 abort_handle: Option<tokio::task::AbortHandle>,
127 },
128
129 AwaitingActionConfirmation {
131 action: AgentAction,
132 executor: ModeAwareExecutor,
133 },
134
135 ExecutingAction {
137 action: AgentAction,
138 start_time: Instant,
139 },
140
141 AwaitingPlanApproval {
143 plan: Plan,
144 explanation: Option<String>,
145 },
146
147 ExecutingPlan {
149 plan: Plan,
150 current_step: usize,
151 start_time: Instant,
152 },
153
154 ReadingFileFeedback {
156 intent: Option<String>,
157 started_at: Instant,
158 },
159}
160
161impl AppState {
162 pub fn generation_status(&self) -> Option<GenerationStatus> {
164 match self {
165 AppState::Generating { status, .. } => Some(*status),
166 _ => None,
167 }
168 }
169
170 pub fn is_generating(&self) -> bool {
172 matches!(self, AppState::Generating { .. })
173 }
174
175 pub fn is_idle(&self) -> bool {
177 matches!(self, AppState::Idle)
178 }
179
180 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 pub fn tokens_received(&self) -> Option<usize> {
190 match self {
191 AppState::Generating { tokens_received, .. } => Some(*tokens_received),
192 _ => None,
193 }
194 }
195
196 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 pub fn pending_action(&self) -> Option<&AgentAction> {
206 match self {
207 AppState::AwaitingActionConfirmation { action, .. } => Some(action),
208 _ => None,
209 }
210 }
211
212 pub fn pending_executor(&self) -> Option<&ModeAwareExecutor> {
214 match self {
215 AppState::AwaitingActionConfirmation { executor, .. } => Some(executor),
216 _ => None,
217 }
218 }
219
220 pub fn is_awaiting_plan_approval(&self) -> bool {
222 matches!(self, AppState::AwaitingPlanApproval { .. })
223 }
224
225 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 pub fn is_reading_file_feedback(&self) -> bool {
236 matches!(self, AppState::ReadingFileFeedback { .. })
237 }
238
239 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 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
256pub struct UIState {
258 pub chat_state: ChatState,
260 pub input_state: InputState,
262 pub theme: Theme,
264 pub selected_message: Option<usize>,
266}
267
268impl UIState {
269 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
280pub struct SessionState {
282 pub messages: Vec<ChatMessage>,
284 pub conversation_manager: Option<ConversationManager>,
286 pub current_conversation: Option<ConversationHistory>,
288 pub input_history: Vec<String>,
290 pub history_index: Option<usize>,
292 pub history_buffer: String,
294 pub context_manager: Option<ContextManager>,
296 pub cumulative_tokens: usize,
298 pub conversation_title: Option<String>,
300}
301
302impl SessionState {
303 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 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
338pub struct OperationState {
340 pub operation_mode: OperationMode,
342 pub bypass_confirmed: bool,
344 pub pending_file_read: bool,
346 pub plan_execution_index: Option<usize>,
348 pub plan_mode_active_for_generation: bool,
350 pub reading_file_status: Option<String>,
352 pub confirmation_state: Option<ConfirmationState>,
354}
355
356impl OperationState {
357 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
371pub 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#[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
407pub struct App {
409 pub input: String,
411 pub cursor_position: usize,
413 pub running: bool,
415 pub context: ProjectContext,
417 pub current_response: String,
419 pub working_dir: String,
421 pub error_log: Vec<ErrorEntry>,
423 pub app_state: AppState,
425
426 pub model_state: ModelState,
428 pub ui_state: UIState,
430 pub session_state: SessionState,
432 pub operation_state: OperationState,
434 pub status_state: StatusState,
436}
437
438impl App {
439 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 let model_state = ModelState::new(model, model_id);
447
448 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 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 let ui_state = UIState {
463 chat_state: ChatState::new(),
464 input_state: InputState::new(),
465 theme: Theme::dark(), selected_message: None,
467 };
468
469 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 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 pub fn set_context_manager(&mut self, manager: ContextManager) {
504 self.session_state.context_manager = Some(manager);
505 }
506
507 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 self.context = manager.build_context();
513 }
514 }
515 Ok(())
516 }
517
518 pub fn add_message(&mut self, role: MessageRole, content: String) {
520 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 if let Some(ref mut conv) = self.session_state.current_conversation {
536 conv.add_messages(&[message]);
537 }
538
539 }
543
544 pub fn clear_input(&mut self) {
546 self.input.clear();
547 self.cursor_position = 0;
548 }
549
550 pub fn set_status(&mut self, message: impl Into<String>) {
552 self.status_state.status_message = Some(message.into());
553 }
554
555 pub fn clear_status(&mut self) {
557 self.status_state.status_message = None;
558 }
559
560 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 pub async fn generate_conversation_title(&mut self) {
569 if self.session_state.conversation_title.is_some() || self.session_state.messages.len() < 2 {
571 return;
572 }
573
574 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, };
582 conversation_summary.push_str(&format!("{}: {}\n\n", role, msg.content.chars().take(200).collect::<String>()));
583 if i >= 3 { break; } }
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 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 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 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 pub fn log_error(&mut self, entry: ErrorEntry) {
634 self.status_state.status_message = Some(entry.display());
636
637 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 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 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 pub fn recent_errors(&self, count: usize) -> Vec<&ErrorEntry> {
661 self.error_log.iter().rev().take(count).collect()
662 }
663
664 pub fn scroll_up(&mut self, amount: u16) {
666 self.ui_state.chat_state.scroll_up(amount);
667 }
668
669 pub fn scroll_down(&mut self, amount: u16) {
671 self.ui_state.chat_state.scroll_down(amount);
672 }
673
674 pub fn quit(&mut self) {
676 self.running = false;
677 }
678
679 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; self.set_status(format!("Mode: {}", self.operation_state.operation_mode.display_name()));
684 }
685
686 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 pub fn set_mode(&mut self, mode: OperationMode) {
695 if self.operation_state.operation_mode != mode {
696 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 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 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 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 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 all_messages.is_empty() {
753 return Vec::new();
754 }
755
756 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 all_messages.iter().map(|m| m.content.len() / 4).sum()
774 });
775
776 if total_tokens <= available_tokens {
778 return all_messages;
779 }
780
781 let mut kept_messages = Vec::new();
784 let mut current_tokens = 0;
785
786 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 kept_messages.push(msg.clone());
808 break;
809 } else {
810 break;
811 }
812 }
813
814 kept_messages.reverse();
816 kept_messages
817 }
818
819 pub fn load_conversation(&mut self, conversation: ConversationHistory) {
821 self.session_state.messages = conversation.messages.clone();
823 self.session_state.current_conversation = Some(conversation);
824 self.set_status("Conversation loaded");
825 }
826
827 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 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 pub fn auto_save_conversation(&mut self) {
842 if self.session_state.messages.is_empty() {
843 return; }
845
846 if let Err(e) = self.save_conversation() {
847 eprintln!("Failed to auto-save conversation: {}", e);
848 }
849 }
850
851 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 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 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 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 self.session_state.cumulative_tokens += count;
898 }
899 }
900
901 pub fn stop_generation(&mut self) {
903 self.app_state = AppState::Idle;
904 }
905
906 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 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 pub fn clear_pending_action(&mut self) {
929 self.app_state = AppState::Idle;
930 }
931
932 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 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 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 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 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 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 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 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 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 pub fn get_plan_stats(&self) -> Option<crate::agents::PlanStats> {
1028 self.app_state.active_plan().map(|plan| plan.stats())
1029 }
1030}
1031
1032#[derive(Debug, Clone)]
1036pub struct ConfirmationState {
1037 pub action: AgentAction,
1038 pub action_description: String,
1039 pub preview_lines: Vec<String>, pub file_info: Option<FileInfo>, pub allow_always: bool, }
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}