magi-code 0.77.1

Repository-aware CLI coding agent for terminal work
Documentation
use super::super::*;
use super::{
    ExportWorker, ExportWorkerDelivery, ExportWorkerOutcome, MissionControlApp, PendingExport,
};

const MAX_EXPORT_DIAGNOSTIC_CHARS: usize = 500;

fn bounded_export_text(message: impl std::fmt::Display, limit: usize) -> String {
    let sanitized = crate::output::sanitize_display_text(&message.to_string());
    let normalized = sanitized.split_whitespace().collect::<Vec<_>>().join(" ");
    let mut chars = normalized.chars();
    let mut bounded = chars.by_ref().take(limit).collect::<String>();
    if chars.next().is_some() {
        bounded.pop();
        bounded.push('');
    }
    bounded
}

fn bounded_export_diagnostic(error: impl std::fmt::Display) -> String {
    bounded_export_text(error, MAX_EXPORT_DIAGNOSTIC_CHARS)
}

fn format_export_status(report: &crate::sessions::SessionExportReport) -> String {
    let partial = !report.warnings.is_empty();
    let completeness = if partial { "partial" } else { "complete" };
    let completeness_prefix = if partial { "PARTIAL — " } else { "" };
    let warnings = report
        .warnings
        .iter()
        .take(8)
        .cloned()
        .collect::<Vec<_>>()
        .join("; ");
    let path = report
        .destination
        .display()
        .to_string()
        .chars()
        .take(240)
        .collect::<String>();
    let status = format!(
        "{completeness_prefix}UNENCRYPTED — NOT SAFE TO SHARE; session export {completeness}: {path} ({} members, {} source bytes); raw potentially sensitive session/project/tool data{}",
        report.member_count,
        report.source_bytes,
        if warnings.is_empty() {
            String::new()
        } else {
            format!("; warnings: {warnings}")
        }
    );
    bounded_export_text(status, 600)
}

fn run_export_job(
    paths: crate::config::McPaths,
    manager: crate::sessions::SessionManager,
    session: crate::sessions::Session,
    cancel: Arc<AtomicBool>,
) -> Option<Result<TuiExportWorkerResult, String>> {
    if cancel.load(Ordering::SeqCst) {
        return None;
    }
    let cancellation = crate::cancellation::AgentCancellation::new(Arc::clone(&cancel));
    let destination = match crate::sessions::export_destination(&paths.root, session.id()) {
        Ok(path) => path,
        Err(error) => return Some(Err(bounded_export_diagnostic(error))),
    };
    let result = crate::sessions::export_session_with_cancellation(
        &manager,
        &session,
        &destination,
        &cancellation,
    )
    .map(|report| TuiExportWorkerResult { report })
    .map_err(bounded_export_diagnostic);
    Some(result)
}

impl MissionControlApp {
    pub(super) fn start_export(
        &mut self,
        arg: Option<&str>,
        ui_state: &mut state::MissionControlState,
    ) {
        if arg.is_some() {
            ui_state.status = "usage: /export".to_string();
            return;
        }

        if self.pending_session_switch.is_some() {
            ui_state.status = "session switch loading; export blocked".to_string();
            return;
        }
        let Some(session) = self.state.current_session.as_ref() else {
            ui_state.status = "/export requires an active persisted session".to_string();
            return;
        };
        match std::fs::symlink_metadata(session.path()) {
            Ok(metadata) if metadata.file_type().is_file() => {}
            Ok(_) => {
                ui_state.status = "/export requires an active persisted session".to_string();
                return;
            }
            Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
                ui_state.status = "/export requires an active persisted session".to_string();
                return;
            }
            Err(error) => {
                ui_state.status = format!("export failed: {}", bounded_export_diagnostic(error));
                return;
            }
        }

        if self.pending_export.is_some() {
            ui_state.status = "export request already in progress".to_string();
            return;
        }

        self.next_export_request_id = self.next_export_request_id.saturating_add(1);
        let request_id = self.next_export_request_id;
        let session_id = session.id().to_string();
        let generation = self.session_generation;
        self.pending_export = Some(PendingExport {
            request_id,
            session_id: session_id.clone(),
            generation,
        });
        ui_state.status = "exporting session…".to_string();

        let paths = self.config.paths.clone();
        let manager = self.state.session_manager.clone();
        let worker_session = session.clone();
        let worker_session_id = session_id.clone();
        let sender = self.events.clone();
        let cancel = Arc::new(AtomicBool::new(false));
        let worker_cancel = Arc::clone(&cancel);
        let handle = thread::Builder::new()
            .name("magi-export".to_string())
            .spawn(move || {
                let Some(result) =
                    run_export_job(paths, manager, worker_session, worker_cancel.clone())
                else {
                    return ExportWorkerOutcome {
                        result: None,
                        delivery: ExportWorkerDelivery::Canceled,
                    };
                };
                let delivery = match send_tui_event(
                    &sender,
                    TuiEvent::ExportFinished {
                        request_id,
                        session_id: worker_session_id,
                        generation,
                        result: Box::new(result.clone()),
                    },
                ) {
                    Ok(()) => ExportWorkerDelivery::Delivered,
                    Err(_) => ExportWorkerDelivery::Failed,
                };
                ExportWorkerOutcome {
                    result: Some(result),
                    delivery,
                }
            });
        match handle {
            Ok(handle) => self.export_workers.push(ExportWorker {
                request_id,
                cancel,
                handle,
            }),
            Err(error) => {
                self.pending_export = None;
                ui_state.status = format!(
                    "export failed: could not start worker: {}",
                    bounded_export_diagnostic(error)
                );
            }
        }
    }

    pub(in crate::tui) fn handle_export_drain(
        &mut self,
        ui_state: &mut state::MissionControlState,
        drain_result: &DrainResult,
    ) -> bool {
        let mut changed = false;
        for (request_id, session_id, generation, result) in &drain_result.export_finished {
            let Some(pending) = self.pending_export.clone() else {
                continue;
            };
            if pending.request_id != *request_id
                || pending.session_id != *session_id
                || pending.generation != *generation
            {
                continue;
            }
            self.pending_export = None;

            if self.session_generation != *generation
                || self.state.active_session_id() != Some(session_id.as_str())
            {
                ui_state.status = "export result ignored; session changed".to_string();
                changed = true;
                continue;
            }

            match result {
                Ok(worker_result) => {
                    ui_state.status = format_export_status(&worker_result.report);
                }
                Err(error) => {
                    ui_state.status =
                        format!("export failed: {}", bounded_export_diagnostic(error));
                }
            }
            changed = true;
        }
        changed
    }

    pub(in crate::tui) fn reap_export_workers(
        &mut self,
        ui_state: &mut state::MissionControlState,
    ) -> bool {
        let mut changed = false;
        let mut active = Vec::with_capacity(self.export_workers.len());
        for worker in std::mem::take(&mut self.export_workers) {
            if !worker.handle.is_finished() {
                active.push(worker);
                continue;
            }
            match worker.handle.join() {
                Ok(outcome) => match outcome.delivery {
                    ExportWorkerDelivery::Delivered => {}
                    ExportWorkerDelivery::Canceled => {
                        if self
                            .pending_export
                            .as_ref()
                            .is_some_and(|pending| pending.request_id == worker.request_id)
                        {
                            self.pending_export = None;
                            ui_state.status = "export request cancelled".to_string();
                            changed = true;
                        }
                    }
                    ExportWorkerDelivery::Failed => {
                        if let Some(result) = outcome.result {
                            let (session_id, generation) = self
                                .pending_export
                                .as_ref()
                                .filter(|pending| pending.request_id == worker.request_id)
                                .map(|pending| (pending.session_id.clone(), pending.generation))
                                .unwrap_or_default();
                            let mut drain = DrainResult::default();
                            drain.export_finished.push((
                                worker.request_id,
                                session_id,
                                generation,
                                result,
                            ));
                            changed |= self.handle_export_drain(ui_state, &drain);
                        }
                    }
                },
                Err(_) => {
                    if self
                        .pending_export
                        .as_ref()
                        .is_some_and(|pending| pending.request_id == worker.request_id)
                    {
                        self.pending_export = None;
                        ui_state.status = "export worker panicked".to_string();
                        changed = true;
                    }
                }
            }
        }
        self.export_workers = active;
        changed
    }

    pub(super) fn cancel_export_workers(&self) {
        for worker in &self.export_workers {
            worker.cancel.store(true, Ordering::SeqCst);
        }
    }

    pub(in crate::tui) fn join_export_workers_for_cleanup(&mut self) -> Vec<String> {
        let mut errors = Vec::new();
        for worker in std::mem::take(&mut self.export_workers) {
            // An export may already have published the archive when shutdown is
            // requested. Always join this worker; detaching could leave a
            // publication in progress after the application has torn down.
            if worker.handle.join().is_err() {
                errors.push(format!("export worker {} panicked", worker.request_id));
            }
        }
        self.pending_export = None;
        errors
    }
}