magi-code 0.62.0

Repository-aware CLI coding agent for terminal work
Documentation
use crate::{agent::cancellation::AgentCancellation, providers::ToolCall, tools::ToolResult};
use std::path::PathBuf;

use super::redact_sensitive_text;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ContextUsageSource {
    FallbackEstimate,
    TokenizerEstimate,
    FallbackProjection,
    TokenizerProjection,
    LastProviderUsage,
    ProviderExact,
    ProviderPartial,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ProviderContextInjectionDisplay {
    pub(crate) phase: crate::hooks::HookPhase,
    pub(crate) label: String,
    pub(crate) target: String,
    pub(crate) status: String,
    pub(crate) item_count: usize,
    pub(crate) byte_count: usize,
    pub(crate) content: Option<String>,
}

impl ProviderContextInjectionDisplay {
    pub(crate) fn new(
        phase: crate::hooks::HookPhase,
        label: impl Into<String>,
        target: impl Into<String>,
        status: impl Into<String>,
        item_count: usize,
        byte_count: usize,
        content: Option<String>,
    ) -> Self {
        Self {
            phase,
            label: redact_sensitive_text(&label.into()),
            target: redact_sensitive_text(&target.into()),
            status: redact_sensitive_text(&status.into()),
            item_count,
            byte_count,
            content: content.map(|text| redact_sensitive_text(&text)),
        }
    }
}

#[derive(Debug, Clone, PartialEq)]
pub(crate) enum OutputEvent {
    SessionHeader {
        session_id: Option<String>,
        model: String,
        cwd: PathBuf,
    },
    UserPrompt {
        text: String,
    },
    BashCommand {
        command: String,
    },
    ContextUsage {
        current_tokens: usize,
        max_tokens: usize,
        reasoning_tokens: Option<usize>,
        source: ContextUsageSource,
        request_sequence: u64,
    },
    ThinkingSummaryDelta {
        text: String,
    },
    ThinkingSummaryComplete {
        text: String,
    },
    ThinkingSummaryCompleteIdentified {
        text: String,
        item_id: Option<String>,
        turn_id: Option<String>,
    },
    AssistantDelta {
        text: String,
    },
    /// Complete assistant text for display-only reconciliation when best-effort
    /// deltas were dropped by a bounded TUI channel. Classic renderers ignore it
    /// so streamed CLI output is not duplicated.
    AssistantComplete {
        text: String,
    },
    ToolStarted {
        call: Box<ToolCall>,
        label: String,
    },
    Diagnostic {
        level: String,
        message: String,
    },
    HookDiagnostic {
        diagnostic: Box<crate::hooks::HookDiagnostic>,
    },
    ProviderContextInjection {
        metadata: ProviderContextInjectionDisplay,
    },
    SubdirInstructionInjection {
        path: PathBuf,
        bytes: usize,
        parent_activity_id: Option<String>,
    },
    ToolResult {
        call: Box<ToolCall>,
        result: Box<ToolResult>,
        summary: Box<ToolDisplaySummary>,
    },
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ToolDisplaySummary {
    pub tool_name: String,
    pub status: ToolStatus,
    pub unicode_mark: &'static str,
    pub ascii_mark: &'static str,
    pub label: String,
    pub metadata: Vec<(String, String)>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ToolStatus {
    Running,
    Writing,
    Success,
    Failure,
}

#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub(crate) struct ActivityId(pub(crate) String);

impl ActivityId {
    pub(crate) fn new(id: impl Into<String>) -> Self {
        Self(id.into())
    }

    pub(crate) fn as_str(&self) -> &str {
        &self.0
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum ActivityKind {
    Tool,
    Hook,
    ProviderContextInjection,
    SubagentBatch,
    SubagentTask,
    Assistant,
    Diagnostic,
    Compaction,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ActivityStatus {
    Queued,
    Running,
    Writing,
    Success,
    Failed,
    Canceled,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ActivityMetadata {
    pub(crate) label: String,
    pub(crate) detail: Option<String>,
    pub(crate) fields: Vec<(String, String)>,
}

impl ActivityMetadata {
    pub(crate) fn new(label: impl Into<String>) -> Self {
        Self {
            label: label.into(),
            detail: None,
            fields: Vec::new(),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum ActivityEvent {
    Started {
        id: ActivityId,
        parent_id: Option<ActivityId>,
        kind: ActivityKind,
        status: ActivityStatus,
        metadata: ActivityMetadata,
    },
    Delta {
        id: ActivityId,
        preview: String,
    },
    UsageUpdate {
        id: ActivityId,
        current_tokens: usize,
        max_tokens: usize,
        reasoning_tokens: Option<usize>,
        source: ContextUsageSource,
        request_sequence: u64,
    },
    FinalPreview {
        id: ActivityId,
        preview: String,
        metadata: Option<ActivityMetadata>,
        status: Option<ActivityStatus>,
    },
    Finished {
        id: ActivityId,
        status: ActivityStatus,
        metadata: Option<ActivityMetadata>,
    },
}

pub(crate) type ActivitySender = std::sync::Arc<dyn Fn(ActivityEvent) + Send + Sync>;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum InvocationMode {
    Print,
    Shell,
    MissionControl,
    Subagent,
}

impl InvocationMode {
    pub(crate) fn as_str(self) -> &'static str {
        match self {
            Self::Print => "print",
            Self::Shell => "shell",
            Self::MissionControl => "mission_control",
            Self::Subagent => "subagent",
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct HookContextMetadata {
    pub(crate) session_id: Option<String>,
    pub(crate) session_path: Option<PathBuf>,
    pub(crate) provider_id: Option<String>,
    pub(crate) model_id: Option<String>,
    pub(crate) agent_id: Option<String>,
    pub(crate) invocation_mode: InvocationMode,
    pub(crate) turn_id: Option<String>,
    pub(crate) message_id: Option<String>,
    pub(crate) subagent: bool,
}

impl HookContextMetadata {
    pub(crate) fn new(invocation_mode: InvocationMode) -> Self {
        Self {
            session_id: None,
            session_path: None,
            provider_id: None,
            model_id: None,
            agent_id: None,
            invocation_mode,
            turn_id: None,
            message_id: None,
            subagent: matches!(invocation_mode, InvocationMode::Subagent),
        }
    }

    pub(crate) fn for_tool_call(
        &self,
        turn_id: impl Into<String>,
        message_id: impl Into<String>,
    ) -> Self {
        let mut cloned = self.clone();
        cloned.turn_id = Some(turn_id.into());
        cloned.message_id = Some(message_id.into());
        cloned
    }
}

#[derive(Clone)]
pub(crate) struct ToolDispatchContext {
    pub(crate) parent_activity_id: Option<ActivityId>,
    pub(crate) activity_sender: Option<ActivitySender>,
    pub(crate) hook_context: HookContextMetadata,
    pub(crate) cancellation: AgentCancellation,
}

impl ToolDispatchContext {
    pub(crate) fn new(
        parent_activity_id: Option<ActivityId>,
        activity_sender: Option<ActivitySender>,
    ) -> Self {
        Self::new_with_hook_context(
            parent_activity_id,
            activity_sender,
            HookContextMetadata::new(InvocationMode::Print),
        )
    }

    pub(crate) fn new_with_hook_context(
        parent_activity_id: Option<ActivityId>,
        activity_sender: Option<ActivitySender>,
        hook_context: HookContextMetadata,
    ) -> Self {
        Self::new_with_hook_context_and_cancellation(
            parent_activity_id,
            activity_sender,
            hook_context,
            AgentCancellation::default(),
        )
    }

    pub(crate) fn new_with_hook_context_and_cancellation(
        parent_activity_id: Option<ActivityId>,
        activity_sender: Option<ActivitySender>,
        hook_context: HookContextMetadata,
        cancellation: AgentCancellation,
    ) -> Self {
        Self {
            parent_activity_id,
            activity_sender,
            hook_context,
            cancellation,
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum OutputMode {
    Plain,
    Rich(RichStyle),
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct RichStyle {
    pub color: bool,
    pub unicode: bool,
}

pub(crate) fn output_mode(is_tty: bool, color_enabled: bool, unicode_enabled: bool) -> OutputMode {
    if is_tty || color_enabled {
        OutputMode::Rich(RichStyle {
            color: color_enabled,
            unicode: unicode_enabled,
        })
    } else {
        OutputMode::Plain
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn output_mode_uses_rich_for_tty_or_forced_color() {
        assert_eq!(output_mode(false, false, false), OutputMode::Plain);
        assert_eq!(
            output_mode(false, true, false),
            OutputMode::Rich(RichStyle {
                color: true,
                unicode: false
            })
        );
        assert_eq!(
            output_mode(true, true, true),
            OutputMode::Rich(RichStyle {
                color: true,
                unicode: true
            })
        );
        assert_eq!(
            output_mode(true, false, true),
            OutputMode::Rich(RichStyle {
                color: false,
                unicode: true
            })
        );
    }
}