magi-code 0.77.1

Repository-aware CLI coding agent for terminal work
Documentation
use super::{MissionControlApp, state};
use crate::config::ProviderCredential;

impl MissionControlApp {
    /// Called after terminal restoration and worker cleanup, while still owning
    /// the writer. Like service session.create, materialize an empty session
    /// with a lifecycle diagnostic so --resume can reopen its exact ID.
    pub(crate) fn finish_update_handoff(
        &mut self,
    ) -> anyhow::Result<Option<crate::updates::RestartContext>> {
        if self.update_restart.is_some()
            && let Some(session) = &self.state.current_session
            && !session.path().try_exists()?
        {
            session.append_with_outcome(&crate::sessions::SessionEvent::new_kind(
                crate::sessions::SessionEventKind::Diagnostic,
                session.id().to_owned(),
                self.state.cwd.clone(),
                serde_json::json!({"lifecycle": "created"}),
            ))?;
        }
        Ok(self.update_restart.take())
    }

    pub(super) fn request_update(
        &mut self,
        argument: Option<&str>,
        ui_state: &mut state::MissionControlState,
    ) -> bool {
        if argument.is_some() {
            ui_state.status = "usage: /update (no arguments)".into();
            return false;
        }
        if self.active_run
            || self.worker.is_some()
            || self.pending_session_switch.is_some()
            || self.pending_rewind.is_some()
            || self.pending_export.is_some()
            || self.pending_model_selection.is_some()
            || self.pending_theme_save.is_some()
            || self.settings_persistence.is_pending()
            || self.fast_mode_persistence.is_pending()
            || self.session_maintenance.is_pruning()
            || self.pending_startup_prompt.is_some()
        {
            ui_state.status = "Wait for the current work to finish before /update.".into();
            return false;
        }
        let mut command = std::process::Command::new("magi-code");
        command
            .current_dir(&self.state.cwd)
            .env("MC_HOME", &self.config.paths.root);
        if let Some(session_id) = self.state.active_session_id() {
            command.arg("--resume").arg(session_id);
        } else {
            command.arg("--no-session");
        }
        if let Some(provider) = &self.config.provider {
            command.arg("--provider").arg(provider);
        }
        if let Some(model) = &self.config.model {
            command.arg("--model").arg(model);
        }
        if self.theme_cli_override {
            command.arg("--theme").arg(&self.appearance.theme.id);
        }
        // Credentials are never placed in argv or a restart file. OAuth remains
        // provider-keyed in its normal store; API keys use the provider's own env.
        if let Some(ProviderCredential::ApiKey { key }) = &self.config.auth {
            let provider = self.config.provider.as_deref().unwrap_or_default();
            let variable = if provider == crate::providers::ANTHROPIC_PROVIDER {
                Some("ANTHROPIC_API_KEY")
            } else {
                self.config
                    .custom_providers
                    .get(provider)
                    .and_then(|provider| provider.api_key_env_var.as_deref())
            };
            let Some(variable) = variable else {
                ui_state.status =
                    "Cannot safely preserve this credential; use --update outside Mission Control."
                        .into();
                return false;
            };
            command.env(variable, key);
        }
        let session = self
            .state
            .active_session_id()
            .map(|id| (self.config.paths.sessions.clone(), id.to_owned()));
        self.update_restart = Some(crate::updates::RestartContext { command, session });
        true
    }
}

#[cfg(test)]
mod tests {
    use super::super::{StartupReadiness, tests::test_app};
    use super::*;

    #[test]
    fn update_handoff_resumes_exact_session_without_replaying_initial_prompt() {
        let temp = tempfile::tempdir().unwrap();
        let (sender, receiver) = crossbeam_channel::bounded(4);
        let mut app = test_app(&temp, sender);
        app.startup_readiness = StartupReadiness::Ready;
        app.state.current_session = Some(
            app.state
                .session_manager
                .create()
                .unwrap()
                .admit_standalone_writer()
                .unwrap(),
        );
        let id = app.state.active_session_id().unwrap().to_owned();
        app.config.provider = Some("anthropic".into());
        app.config.model = Some("selected-model".into());
        app.config.auth = Some(ProviderCredential::ApiKey {
            key: "unused-test-credential".into(),
        });
        let mut state = state::MissionControlState::default();
        // Test the same submit route used by keyboard input, not an updater on the host.
        assert!(app.submit(
            "/update".into(),
            &mut state,
            &receiver,
            ratatui::layout::Rect::new(0, 0, 80, 24)
        ));
        app.cleanup_after_run().unwrap();
        let restart = app.finish_update_handoff().unwrap().unwrap();
        let reopened = app.state.session_manager.open_existing(&id).unwrap();
        let events = reopened.read_events().unwrap();
        assert_eq!(events.len(), 1);
        assert_eq!(
            events[0].payload,
            serde_json::json!({"lifecycle": "created"})
        );
        let args: Vec<_> = restart.command.get_args().collect();
        assert_eq!(args[0], "--resume");
        assert_eq!(args[1], std::ffi::OsStr::new(&id));
        assert!(
            !args
                .iter()
                .any(|arg| *arg == "--prompt" || *arg == "--continue" || *arg == "--api-key")
        );
        assert_eq!(
            restart.command.get_current_dir(),
            Some(app.state.cwd.as_path())
        );
        assert!(
            restart
                .command
                .get_envs()
                .any(|(key, value)| key == "ANTHROPIC_API_KEY"
                    && value == Some(std::ffi::OsStr::new("unused-test-credential")))
        );
        drop(app);
        assert!(reopened.try_frontend_writer().unwrap().is_some());
    }

    #[test]
    fn update_rejects_arguments_and_active_work_without_requesting_exit() {
        let temp = tempfile::tempdir().unwrap();
        let (sender, receiver) = crossbeam_channel::bounded(4);
        let mut app = test_app(&temp, sender);
        app.startup_readiness = StartupReadiness::Ready;
        let mut state = state::MissionControlState::default();
        let area = ratatui::layout::Rect::new(0, 0, 80, 24);
        assert!(!app.submit("/update now".into(), &mut state, &receiver, area));
        assert_eq!(state.status, "usage: /update (no arguments)");
        app.active_run = true;
        assert!(!app.submit("/update".into(), &mut state, &receiver, area));
        assert!(app.update_restart.is_none());
    }
}