magi-code 0.105.1

Repository-aware CLI coding agent for terminal work
Documentation
use super::*;
use crate::output::ActivityId;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ToolOutcome {
    Success,
    Failure,
    Cancelled,
    Interrupted,
    Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ChangeState {
    NotApplicable,
    None,
    Applied,
    Partial,
    Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum CardStatus {
    Running,
    Success,
    Failed,
    Canceled,
    None,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ToolCardRole {
    Bash,
    CodeMode,
    MagiControl,
    Read,
    Grep,
    AstGrep,
    Find,
    ListFiles,
    Web,
    Tool,
    Write,
    HashEdit,
    SubagentBatch,
    ViewImage,
}

/// Inline meaning, not highlighting or color. Renderers choose equivalent styles.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum TextMeaning {
    Plain,
    Metadata,
    Resource,
    Pattern,
    Count,
    Separator,
    OutcomeMark,
    Added,
    Removed,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct TextSpan {
    pub(crate) text: String,
    pub(crate) meaning: TextMeaning,
}
impl TextSpan {
    pub(super) fn new(text: impl Into<String>, meaning: TextMeaning) -> Self {
        Self {
            text: text.into(),
            meaning,
        }
    }
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct TextLine {
    pub(crate) spans: Vec<TextSpan>,
}
impl TextLine {
    pub(super) fn from_span(text: impl Into<String>, meaning: TextMeaning) -> Self {
        Self {
            spans: vec![TextSpan::new(text, meaning)],
        }
    }
    pub(super) fn plain(text: impl Into<String>) -> Self {
        Self::from_span(text, TextMeaning::Plain)
    }
    pub(crate) fn plain_text(&self) -> String {
        self.spans.iter().map(|span| span.text.as_str()).collect()
    }
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum DiffApplication {
    Applied,
    Partial,
    Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ResourceAvailability {
    Available,
    Unavailable,
    Unknown,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ResourceRow {
    pub(crate) line: TextLine,
    pub(crate) location: String,
    pub(crate) availability: ResourceAvailability,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ResultOutcome {
    Success,
    Failure,
    Partial,
    Unknown,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ResultRow {
    pub(crate) label: String,
    pub(crate) value: Option<String>,
    pub(crate) outcome: ResultOutcome,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum ToolBlock {
    Text {
        lines: Vec<TextLine>,
    },
    Command {
        command: Option<String>,
        working_directory: Option<String>,
        exit_code: Option<i64>,
    },
    Resources {
        rows: Vec<ResourceRow>,
    },
    Diff {
        application: DiffApplication,
        lines: Vec<TextLine>,
    },
    Progress {
        label: String,
        completed: Option<u64>,
        total: Option<u64>,
    },
    Results {
        rows: Vec<ResultRow>,
    },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum OmissionSubject {
    Title,
    Metadata,
    Blocks,
    Rows,
    Text,
    DiffLines,
    Children,
    Progress,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) enum OmissionReason {
    Redacted,
    MissingSource,
    NotRecorded,
    UnsupportedSource,
    SizeLimit,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct DisplayOmission {
    pub(crate) subject: OmissionSubject,
    pub(crate) reason: OmissionReason,
    pub(crate) omitted_count: Option<usize>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ToolCard {
    pub(crate) role: ToolCardRole,
    pub(crate) status: CardStatus,
    pub(crate) outcome: Option<ToolOutcome>,
    pub(crate) changes: ChangeState,
    pub(crate) public_protection: PublicProtection,
    pub(crate) title: String,
    pub(crate) metadata: Vec<String>,
    pub(crate) body: String,
    pub(crate) blocks: Vec<ToolBlock>,
    pub(crate) activity_rows: Vec<ToolActivityRow>,
    pub(crate) omissions: Vec<DisplayOmission>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ToolActivityRow {
    pub(crate) label: String,
    pub(crate) status: CardStatus,
    pub(crate) activity_id: ActivityId,
}
/// Already selected child facts. Caller owns relationship lookup and safe source attribution.
pub(crate) struct ChildToolFacts<'a> {
    pub(crate) id: &'a ActivityId,
    pub(crate) status: ActivityStatus,
    pub(crate) tool_name: Option<&'a str>,
    pub(crate) detail: Option<&'a ToolActivityDetail>,
    pub(crate) safe_target: Option<String>,
}

pub(super) struct CardDraft {
    pub(super) role: ToolCardRole,
    pub(super) status: CardStatus,
    pub(super) title: String,
    pub(super) metadata: Vec<String>,
    pub(super) body: String,
    pub(super) body_lines: Vec<TextLine>,
    pub(super) activity_rows: Vec<ToolActivityRow>,
}

pub(super) fn blocks_from_draft(
    draft: &CardDraft,
    input: &ToolCardInput<'_>,
    changes: ChangeState,
) -> Vec<ToolBlock> {
    let mut blocks = Vec::new();
    if draft.role == ToolCardRole::Bash && !input.bash_mode_response {
        if let Some(intent) = input
            .detail
            .and_then(|detail| detail.params["intent"].as_str())
            .map(str::trim)
            .filter(|intent| !intent.is_empty())
        {
            blocks.push(ToolBlock::Text {
                lines: vec![TextLine::plain(sanitized_single_line(intent))],
            });
        }
        blocks.push(ToolBlock::Command {
            command: input.bash_command.map(sanitize_display_text),
            working_directory: None,
            exit_code: input
                .detail
                .and_then(|detail| detail.metadata["exit_code"].as_i64()),
        });
        let rows = super::bash_notices::bash_execution_notices(input.detail);
        if !rows.is_empty() {
            blocks.push(ToolBlock::Results {
                rows: rows
                    .into_iter()
                    .map(|notice| ResultRow {
                        label: notice.label.to_owned(),
                        value: notice.value,
                        outcome: ResultOutcome::Unknown,
                    })
                    .collect(),
            });
        }
    }
    let lines = if draft.body_lines.is_empty() && draft.role != ToolCardRole::Bash {
        draft.body.lines().map(TextLine::plain).collect()
    } else {
        draft.body_lines.clone()
    };
    for mut line in lines {
        for span in &mut line.spans {
            span.text = sanitize_display_text(&span.text);
        }
        let resource = line
            .spans
            .iter()
            .find(|span| span.meaning == TextMeaning::Resource)
            .map(|span| span.text.clone());
        let diff = line
            .spans
            .iter()
            .any(|span| matches!(span.meaning, TextMeaning::Added | TextMeaning::Removed));
        if diff {
            let application = match changes {
                ChangeState::Applied => DiffApplication::Applied,
                ChangeState::Partial => DiffApplication::Partial,
                _ => DiffApplication::Unknown,
            };
            if let Some(ToolBlock::Diff { lines, .. }) = blocks.last_mut() {
                lines.push(line);
            } else {
                blocks.push(ToolBlock::Diff {
                    application,
                    lines: vec![line],
                });
            }
        } else if let Some(location) = resource {
            let row = ResourceRow {
                line,
                location,
                availability: ResourceAvailability::Unknown,
            };
            if let Some(ToolBlock::Resources { rows }) = blocks.last_mut() {
                rows.push(row);
            } else {
                blocks.push(ToolBlock::Resources { rows: vec![row] });
            }
        } else if let Some(ToolBlock::Text { lines }) = blocks.last_mut() {
            lines.push(line);
        } else {
            blocks.push(ToolBlock::Text { lines: vec![line] });
        }
    }
    blocks
}