magi-code 0.96.2

Repository-aware CLI coding agent for terminal work
Documentation
use crate::{
    cancellation::{AgentCancellation, is_run_canceled},
    config::{McPaths, SessionTitleConfig, load_effective_provider_selection},
    providers::{
        ChatMessage, Provider, ProviderEvent, ProviderRequest, ProviderSelection,
        provider_from_selection_with_settings_for_workload,
    },
    sessions::{
        SESSION_TITLE_MAX_CHARS, Session, SessionEventKind, record_session_event,
        record_session_title, session_title_metadata,
    },
};
use std::{
    path::PathBuf,
    sync::{Arc, mpsc},
    thread,
    time::Duration,
};

const TITLE_CONTEXT_MAX_CHARS: usize = 2000;
const TITLE_OUTPUT_MAX_CHARS: usize = 512;
const TITLE_OVERALL_TIMEOUT: Duration = Duration::from_secs(60);

#[derive(Debug)]
struct SessionTitleTimeout(Duration);

impl std::fmt::Display for SessionTitleTimeout {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            formatter,
            "session title provider timed out after {:?}",
            self.0
        )
    }
}

impl std::error::Error for SessionTitleTimeout {}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct SessionTitleUpdate {
    pub(crate) session_id: String,
    pub(crate) title: String,
}

pub(crate) type SessionTitleNotifier = Arc<dyn Fn(SessionTitleUpdate) + Send + Sync + 'static>;

#[derive(Clone)]
pub(crate) struct SessionTitleJob {
    pub(crate) paths: McPaths,
    pub(crate) session: Session,
    pub(crate) cwd: PathBuf,
    pub(crate) config: SessionTitleConfig,
    pub(crate) settings: crate::config::Settings,
    pub(crate) first_prompt: String,
    pub(crate) cancellation: AgentCancellation,
    pub(crate) notifier: Option<SessionTitleNotifier>,
}

pub(crate) fn should_start_title_generation_metadata(
    session: &Session,
) -> anyhow::Result<crate::sessions::SessionTitleMetadata> {
    session_title_metadata(session)
}

pub(crate) fn spawn_background(job: SessionTitleJob) -> thread::JoinHandle<()> {
    thread::spawn(move || match generate_and_persist(job.clone()) {
        Ok(Some(update)) => {
            if let Some(notifier) = job.notifier.as_ref() {
                notifier(update);
            }
        }
        Ok(None) => record_title_generation_diagnostic(
            &job,
            "empty_output",
            "provider returned no title text after sanitization",
        ),
        Err(error) if is_run_canceled(&error) => {}
        Err(error) if error.downcast_ref::<SessionTitleTimeout>().is_some() => {
            record_title_generation_diagnostic(&job, "timeout", &safe_error_message(&error))
        }
        Err(error) => {
            record_title_generation_diagnostic(&job, "failed", &safe_error_message(&error))
        }
    })
}

fn generate_and_persist(job: SessionTitleJob) -> anyhow::Result<Option<SessionTitleUpdate>> {
    job.cancellation.check()?;
    let config = load_effective_provider_selection(
        &job.paths,
        job.config.provider.as_str(),
        job.config.model.as_str(),
    )?;
    let selection = ProviderSelection::from_config(&config)?;
    let provider = provider_from_selection_with_settings_for_workload(
        &config,
        &selection,
        &job.cwd,
        &job.settings,
        crate::fast::FastWorkload::SessionTitle,
    )?;
    job.cancellation.check()?;
    let Some(title) = generate_title_cancellable_with_verbosity(
        provider.as_ref(),
        &selection.provider,
        &selection.model,
        title_context(&job),
        job.settings.text_verbosity_for(&selection.provider),
        &job.cancellation,
    )?
    else {
        return Ok(None);
    };
    job.cancellation.check()?;
    record_session_title(
        &job.session,
        &job.cwd,
        &title,
        &selection.provider,
        &selection.model,
    )?;
    Ok(Some(SessionTitleUpdate {
        session_id: job.session.id().to_string(),
        title,
    }))
}

fn title_context(job: &SessionTitleJob) -> &str {
    &job.first_prompt
}

fn record_title_generation_diagnostic(job: &SessionTitleJob, reason: &str, message: &str) {
    let sanitized = sanitize_title_diagnostic(message);
    let _ = record_session_event(
        Some(&job.session),
        &job.cwd,
        SessionEventKind::Diagnostic,
        serde_json::json!({
            "level": "warning",
            "category": "session_title_generation",
            "reason": reason,
            "message": format!("session title generation {reason}: {sanitized}"),
        }),
    );
}

fn safe_error_message(error: &anyhow::Error) -> String {
    error
        .chain()
        .next()
        .map(ToString::to_string)
        .unwrap_or_else(|| "unknown error".to_string())
}

fn sanitize_title_diagnostic(message: &str) -> String {
    let redacted = crate::output::redact_sensitive_text(message);
    let single_line = redacted
        .lines()
        .map(str::trim)
        .filter(|line| !line.is_empty())
        .collect::<Vec<_>>()
        .join(" ");
    let mut chars = single_line.chars();
    let mut bounded = chars.by_ref().take(500).collect::<String>();
    if chars.next().is_some() {
        bounded.push('…');
    }
    if bounded.is_empty() {
        "unknown error".to_string()
    } else {
        bounded
    }
}

fn generate_title_cancellable_with_verbosity(
    provider: &dyn Provider,
    provider_id: &str,
    model: &str,
    first_prompt: &str,
    text_verbosity: Option<crate::config::TextVerbosity>,
    cancellation: &AgentCancellation,
) -> anyhow::Result<Option<String>> {
    generate_title_cancellable_with_timeout(
        provider,
        provider_id,
        model,
        first_prompt,
        text_verbosity,
        cancellation,
        TITLE_OVERALL_TIMEOUT,
    )
}

fn generate_title_cancellable_with_timeout(
    provider: &dyn Provider,
    provider_id: &str,
    model: &str,
    first_prompt: &str,
    text_verbosity: Option<crate::config::TextVerbosity>,
    cancellation: &AgentCancellation,
    overall_timeout: Duration,
) -> anyhow::Result<Option<String>> {
    cancellation.check()?;
    let bounded_context = first_prompt
        .chars()
        .take(TITLE_CONTEXT_MAX_CHARS)
        .collect::<String>();
    let mut request = ProviderRequest::new_without_tools(
        model.to_string(),
        vec![
            ChatMessage::system(format!(
                "Return one concise friendly session title, maximum {SESSION_TITLE_MAX_CHARS} characters. No quotes. No markdown. No explanation."
            )),
            ChatMessage::user(format!(
                "Infer a title from this initial user request:\n\n{bounded_context}"
            )),
        ],
    )
    .with_text_verbosity(text_verbosity);
    if provider_id == crate::providers::OPENAI_CODEX_PROVIDER {
        request = request
            .with_thinking_level(crate::thinking::ThinkingLevel::Low)
            .with_default_reasoning_summary(true);
    }
    let (title_cancellation, timeout_handle) = cancellation.child_token();
    let (done_sender, done_receiver) = mpsc::channel();
    let timeout_thread = thread::spawn(move || {
        if matches!(
            done_receiver.recv_timeout(overall_timeout),
            Err(mpsc::RecvTimeoutError::Timeout)
        ) {
            timeout_handle.cancel();
            true
        } else {
            false
        }
    });
    let mut raw = String::new();
    let result = provider.stream_cancellable(request, &title_cancellation, &mut |event| {
        title_cancellation.check()?;
        if let ProviderEvent::TextDelta(delta) = event {
            let remaining = TITLE_OUTPUT_MAX_CHARS.saturating_sub(raw.chars().count());
            raw.extend(delta.chars().take(remaining));
        }
        title_cancellation.check()?;
        Ok(())
    });
    let _ = done_sender.send(());
    let timed_out = timeout_thread.join().unwrap_or(false);
    if timed_out && !cancellation.is_canceled() {
        return Err(SessionTitleTimeout(overall_timeout).into());
    }
    result?;
    cancellation.check()?;
    Ok(crate::sessions::sanitize_session_title(&raw))
}