magi-code 0.77.1

Repository-aware CLI coding agent for terminal work
Documentation
use super::super::*;
use super::display_reducer;
use super::{
    MissionControlApp, PendingSessionSwitch,
    actions::{ActionContext, AppControlFlow},
};

impl MissionControlApp {
    pub(super) fn start_session_switch_worker(
        &mut self,
        session_id: String,
        ui_state: &mut state::MissionControlState,
    ) {
        self.start_session_switch_worker_with_admission(session_id, None, ui_state);
    }

    pub(super) fn start_session_switch_worker_with_admission(
        &mut self,
        session_id: String,
        admitted_session: Option<Session>,
        ui_state: &mut state::MissionControlState,
    ) {
        if self.session_maintenance.is_pruning() {
            ui_state.status = "session pruning in progress; please wait".into();
            return;
        }
        if self.pending_export.is_some() {
            ui_state.status = "session export is in progress; please wait".to_string();
            return;
        }
        if self.pending_session_switch.is_some() {
            ui_state.status = "session switch already loading".to_string();
            return;
        }
        if self.active_run || self.worker.is_some() {
            ui_state.status = "cannot switch sessions while a prompt is running".to_string();
            return;
        }
        if self.state.active_session_id() == Some(session_id.as_str()) {
            ui_state.close_session_picker();
            ui_state.status = format!("session unchanged: {}", short_session_id(&session_id));
            return;
        }
        self.next_session_switch_request_id = self.next_session_switch_request_id.saturating_add(1);
        let request_id = self.next_session_switch_request_id;
        self.pending_session_switch = Some(PendingSessionSwitch {
            request_id,
            session_id: session_id.clone(),
            generation: self.session_generation,
        });
        ui_state.status = "loading session…".to_string();
        let manager = self.state.session_manager.clone();
        let config = self.state.config.clone();
        let sender = self.events.clone();
        thread::spawn(move || {
            let result = match admitted_session {
                Some(session) => crate::tui::sessions::commands::load_admitted_session_switch(
                    session,
                    config.as_ref(),
                    None,
                )
                .map_err(|error| error.to_string()),
                None => start_session_switch_load(&manager, config.as_ref(), &session_id),
            };
            let _ = send_tui_event(
                &sender,
                TuiEvent::SessionSwitchLoaded {
                    request_id,
                    session_id,
                    result: Box::new(result),
                },
            );
        });
    }

    fn apply_copy_request<C: input::clipboard::ClipboardWriter + ?Sized>(
        text: &str,
        state: &mut state::MissionControlState,
        clipboard: &mut C,
        now: Instant,
    ) -> bool {
        if text.is_empty() {
            return false;
        }
        if clipboard.copy(text).is_ok() {
            state.show_copy_success_toast(now);
        } else {
            state.show_copy_failure_toast(now);
        }
        true
    }

    pub(super) fn reduce_input_command(
        &mut self,
        command: input::InputCommand,
        ui_state: &mut state::MissionControlState,
        context: &mut ActionContext<'_>,
        prompt_revision_before: u64,
    ) -> (AppControlFlow, display_reducer::DisplayApplyResult) {
        let (action, display) = command.into_parts();
        // Check exclusivity before applying destructive display commands such as
        // TakeForSubmit, otherwise a rejected submit would lose the draft.
        if self.reject_exclusive_operation(&action, ui_state) {
            return (
                AppControlFlow::Continue,
                display_reducer::DisplayApplyResult::default(),
            );
        }
        let mut display_result =
            display.map_or_else(display_reducer::DisplayApplyResult::default, |display| {
                let mut result = display_reducer::apply_display_command(
                    display,
                    ui_state,
                    context.autocomplete_candidates,
                );
                if let Some(text) = result.copy.take() {
                    Self::apply_copy_request(&text, ui_state, context.clipboard, Instant::now());
                }
                result
            });
        self.mark_initial_prompt_input(
            ui_state.prompt_content_revision() != prompt_revision_before,
            Some(&action),
            ui_state,
        );
        let flow = self.apply_input_action(action, ui_state, context);
        display_result.copy = None;
        (flow, display_result)
    }

    fn reject_exclusive_operation(
        &self,
        action: &input::InputAction,
        ui_state: &mut state::MissionControlState,
    ) -> bool {
        let reason = if self.pending_export.is_some() {
            "session export is in progress; please wait"
        } else if self
            .pending_rewind
            .as_ref()
            .is_some_and(|pending| pending.kind == super::RewindRequestKind::Execute)
        {
            "rewind execution is still in progress; please wait"
        } else if self.pending_model_selection.is_some() {
            "model selection is still in progress; please wait"
        } else {
            return false;
        };
        if matches!(
            action,
            input::InputAction::Submit(_)
                | input::InputAction::SelectModel(_)
                | input::InputAction::SelectSession(_)
                | input::InputAction::PreviewTheme(_)
                | input::InputAction::ConfirmTheme
                | input::InputAction::SetSkillEnabled { .. }
                | input::InputAction::SetMcpServerEnabled { .. }
                | input::InputAction::SetToolEnabled { .. }
                | input::InputAction::SetSubagentProfileEnabled { .. }
                | input::InputAction::SetModelEnabled { .. }
                | input::InputAction::PrimaryAgentSelectionChanged(_)
                | input::InputAction::ThinkingLevelSelectionChanged(_)
                | input::InputAction::SelectConnectProvider(_)
                | input::InputAction::SelectLogoutProvider(_)
                | input::InputAction::ConfirmLogout(_)
                | input::InputAction::ConfirmConnectProviderReplacement
                | input::InputAction::SubmitConnectProviderStep
                | input::InputAction::SubmitOAuthFallback
                | input::InputAction::ConfirmRewind
                | input::InputAction::OpenSetModel
        ) {
            ui_state.status = reason.to_string();
            return true;
        }
        false
    }
    pub(super) fn apply_input_action(
        &mut self,
        action: input::InputAction,
        ui_state: &mut state::MissionControlState,
        context: &mut ActionContext<'_>,
    ) -> AppControlFlow {
        if self.reject_exclusive_operation(&action, ui_state) {
            return AppControlFlow::Continue;
        }
        match action {
            input::InputAction::Submit(prompt) => {
                self.submit(prompt, ui_state, context.receiver, context.last_area);
            }
            input::InputAction::PreviewTheme(id) => self.preview_theme(&id, ui_state),
            input::InputAction::ConfirmTheme => self.save_theme(ui_state),
            input::InputAction::CancelTheme => self.cancel_theme(ui_state),
            input::InputAction::SelectModel(model_id) => {
                self.select_model(&model_id, ui_state);
            }
            input::InputAction::SelectSession(session_id) => {
                self.start_session_switch_worker(session_id, ui_state);
            }
            input::InputAction::SetSkillEnabled { name, enabled } => {
                self.set_skill_enabled(&name, enabled, ui_state);
            }
            input::InputAction::ToggleSkillsSettingsScope => {
                self.toggle_skills_settings_scope(ui_state, context.last_area);
            }
            input::InputAction::SetMcpServerEnabled { name, enabled } => {
                self.set_mcp_server_enabled(&name, enabled, ui_state);
            }
            input::InputAction::SetToolEnabled { name, enabled } => {
                self.set_tool_enabled(&name, enabled, ui_state);
            }
            input::InputAction::ToggleToolsSettingsScope => {
                self.toggle_tools_settings_scope(ui_state, context.last_area);
            }
            input::InputAction::SetSubagentProfileEnabled { id, enabled } => {
                self.set_subagent_profile_enabled(&id, enabled, ui_state);
            }
            input::InputAction::ToggleSubagentsSettingsScope => {
                self.toggle_subagents_settings_scope(ui_state, context.last_area);
            }
            input::InputAction::SetModelEnabled { id, enabled } => {
                self.set_model_enabled(&id, enabled, ui_state);
            }
            input::InputAction::ToggleModelsSettingsScope => {
                self.toggle_models_settings_scope(ui_state, context.last_area);
            }
            input::InputAction::PrimaryAgentSelectionChanged(selected_id) => {
                self.persist_primary_agent_selection(selected_id.as_deref(), ui_state);
            }
            input::InputAction::ThinkingLevelSelectionChanged(level) => {
                self.persist_thinking_level(level, ui_state);
            }
            input::InputAction::SelectConnectProvider(provider_id) => {
                self.start_connect_provider(&provider_id, ui_state);
            }
            input::InputAction::SelectLogoutProvider(provider_id) => {
                self.logout_provider(&provider_id, ui_state);
            }
            input::InputAction::MoveConnectProviderUp(visible_rows) => {
                ui_state.connect_provider_up(visible_rows);
            }
            input::InputAction::MoveConnectProviderDown(visible_rows) => {
                ui_state.connect_provider_down(visible_rows);
            }
            input::InputAction::FocusNextConnectProviderField => {
                ui_state.connect_provider_next_field();
            }
            input::InputAction::FocusPreviousConnectProviderField => {
                ui_state.connect_provider_previous_field();
            }
            input::InputAction::InsertConnectProviderChar(ch) => {
                ui_state.insert_modal_field_char(ch);
            }
            input::InputAction::InsertConnectProviderText(text) => {
                ui_state.insert_modal_field_text(&text.into_inner());
            }
            input::InputAction::BackspaceConnectProviderField => {
                ui_state.backspace_modal_field();
            }
            input::InputAction::DeleteConnectProviderField => {
                ui_state.delete_modal_field_forward();
            }
            input::InputAction::MoveConnectProviderFieldLeft => {
                ui_state.move_modal_field_left();
            }
            input::InputAction::MoveConnectProviderFieldRight => {
                ui_state.move_modal_field_right();
            }
            input::InputAction::MoveConnectProviderFieldHome => {
                ui_state.move_modal_field_home();
            }
            input::InputAction::MoveConnectProviderFieldEnd => {
                ui_state.move_modal_field_end();
            }
            input::InputAction::SubmitOAuthFallback => {
                self.submit_connect_provider_fallback(ui_state);
            }
            input::InputAction::SubmitConnectProviderStep => {
                self.submit_connect_provider_setup_step(ui_state);
            }
            input::InputAction::CopyOAuthUrl => {
                let url = ui_state
                    .modals
                    .connect_provider
                    .as_ref()
                    .map(|connect| connect.url.clone());
                match url.as_deref() {
                    Some(url) if !url.is_empty() => {
                        Self::apply_copy_request(url, ui_state, context.clipboard, Instant::now());
                    }
                    _ => ui_state.status = "OAuth URL is not ready yet".to_string(),
                }
            }
            input::InputAction::CopySelectedPrompt(text) => {
                Self::apply_copy_request(&text, ui_state, context.clipboard, Instant::now());
            }
            input::InputAction::OpenOAuthUrl => {
                let url = ui_state
                    .modals
                    .connect_provider
                    .as_ref()
                    .map(|connect| connect.url.as_str());
                ui_state.status = match url {
                    Some(url) if !url.is_empty() => match open_url_in_browser(url) {
                        Ok(()) => "opened OAuth URL in browser".to_string(),
                        Err(error) => {
                            format!("could not open browser: {error}; press [Ctrl-Y] to copy URL")
                        }
                    },
                    _ => "OAuth URL is not ready yet".to_string(),
                };
            }
            input::InputAction::CancelConnectProvider => {
                self.cancel_connect_provider(ui_state);
            }
            input::InputAction::RetryConnectProvider => {
                self.retry_connect_provider(ui_state);
            }
            input::InputAction::BackConnectProvider => {
                if ui_state.back_connect_provider() {
                    ui_state.status = "select provider".to_string();
                }
            }
            input::InputAction::CloseConnectProvider => {
                self.cancel_connect_provider(ui_state);
            }
            input::InputAction::ConfirmLogout(provider_id) => {
                self.remove_logout_provider(&provider_id, ui_state);
            }
            input::InputAction::CancelLogout => {
                ui_state.status = "logout cancelled; credentials unchanged".to_string();
            }
            input::InputAction::ConfirmConnectProviderReplacement => {
                self.confirm_connect_provider_replacement(ui_state);
            }
            input::InputAction::CancelConnectProviderReplacement => {
                if ui_state.modals.connect_provider.is_some() {
                    ui_state.back_connect_provider();
                    ui_state.status =
                        "custom provider replacement cancelled; edit the form or cancel"
                            .to_string();
                }
            }
            input::InputAction::ConfirmRewind => {
                if let Some((plan, mode)) = ui_state.take_rewind_plan() {
                    self.start_rewind_execution(plan, mode, ui_state);
                }
            }
            input::InputAction::CancelRewind => {
                ui_state.status = "rewind cancelled".to_string();
            }
            input::InputAction::CancelRunningPrompt => {
                self.cancel_running_prompt(ui_state);
            }
            input::InputAction::CancelStartupPrompt => {
                let was_queued = self.pending_startup_prompt.is_some();
                self.clear_pending_startup_prompt();
                ui_state.startup_prompt_queued = false;
                if was_queued {
                    ui_state.focus_prompt();
                    ui_state.status = "startup prompt cancelled".to_string();
                }
            }
            input::InputAction::OpenSetModel => {
                if self.active_run || self.worker.is_some() {
                    ui_state.status = "cannot change model while a prompt is running".to_string();
                } else {
                    self.open_model_picker(ui_state, context.last_area);
                }
            }
            input::InputAction::Exit => {
                self.clear_pending_startup_prompt();
                ui_state.startup_prompt_queued = false;
                if let Some(status) = self.request_cancel_active_worker(ui_state) {
                    ui_state.status = status;
                }
                // The post-terminal cleanup seam owns the final blocking join;
                // only request cancellation while the terminal is active.
                return AppControlFlow::Exit;
            }
            input::InputAction::None => {}
        }
        AppControlFlow::Continue
    }
}