magi-code 0.96.1

Repository-aware CLI coding agent for terminal work
Documentation
use crate::tui::theme::MissionControlTheme;
use ratatui::style::Color;
use ratatui::text::{Line, Text};
use serde_json::Value;
use std::{collections::BTreeMap, path::PathBuf};

const MAX_TREE_ROWS: usize = 8192;
const MAX_TREE_BYTES: usize = 1024 * 1024;
const OMITTED_NOTICE: &str = "Further file paths omitted (display limit).";

/// Confirmed dedicated-tool activity and separately labeled, display-only shell inference.
#[derive(Debug, Default, Clone)]
pub(crate) struct SessionFiles {
    pub(crate) session_id: Option<String>,
    pub(crate) cwd: PathBuf,
    dirty: bool,
    omitted: bool,
    tree_omitted: bool,
    paths: BTreeMap<String, u8>,
    lines: Vec<(String, u8)>,
    pub(crate) scroll: u16,
}

impl SessionFiles {
    pub(crate) fn record(
        &mut self,
        name: &str,
        success: bool,
        metadata: &Value,
        changed: &[PathBuf],
    ) {
        self.dirty = false;
        match name {
            "read" => self.record_read_results(success, metadata),
            "write" => {
                if success
                    || metadata.get("outcome").and_then(Value::as_str)
                        == Some("committed_but_undurable")
                {
                    self.record_metadata_path(metadata.get("path"), 2);
                }
                for path in changed {
                    self.add_path(&path.to_string_lossy(), 2);
                }
            }
            "hash_edit" => {
                for path in changed {
                    self.add_path(&path.to_string_lossy(), 4);
                }
                self.record_edit_results(metadata);
            }
            "bash" => {
                if let Some(activity) = metadata
                    .get("inferred_file_activity")
                    .and_then(Value::as_array)
                {
                    for item in activity.iter().take(32) {
                        let reasons = match item.get("operation").and_then(Value::as_str) {
                            Some("read") => 1,
                            Some("write") => 2,
                            Some("edit") => 4,
                            Some("read_write") => 3,
                            _ => continue,
                        };
                        self.record_metadata_path(item.get("path"), reasons << 3);
                    }
                }
            }
            _ => {}
        }
        if self.dirty {
            self.rebuild_tree();
        }
    }

    fn record_read_results(&mut self, success: bool, metadata: &Value) {
        if let Some(results) = metadata.get("results").and_then(Value::as_array) {
            for item in results {
                if item.get("success").and_then(Value::as_bool) == Some(true) {
                    self.record_metadata_path(item.get("path"), 1);
                }
            }
        } else if success {
            self.record_metadata_path(metadata.get("path"), 1);
        }
    }

    fn record_edit_results(&mut self, metadata: &Value) {
        let Some(files) = metadata.get("files").and_then(Value::as_array) else {
            return;
        };
        for file in files {
            let applied = matches!(
                file.get("status").and_then(Value::as_str),
                Some(
                    "committed"
                        | "committed_but_undurable"
                        | "committed_with_error"
                        | "destination_written_source_retained"
                )
            );
            if applied && file.get("operation").and_then(Value::as_str) != Some("noop") {
                self.record_metadata_path(file.get("path"), 4);
                self.record_metadata_path(file.get("destination"), 4);
            }
        }
    }

    fn record_metadata_path(&mut self, value: Option<&Value>, label: u8) {
        if let Some(path) = value.and_then(Value::as_str) {
            self.add_path(path, label);
        }
    }

    fn add_path(&mut self, path: &str, label: u8) {
        if path.is_empty() || path.contains("://") {
            return;
        }
        if path.len() > 4096 || path.split('/').count() > 128 {
            self.omitted = true;
            return;
        }
        let path = crate::path_utils::lexical_normalize(&self.cwd.join(path));
        let path = path
            .strip_prefix(&self.cwd)
            .unwrap_or(&path)
            .to_string_lossy()
            .into_owned();
        if self.paths.len() >= 4096 && !self.paths.contains_key(&path) {
            self.omitted = true;
            return;
        }
        let labels = self.paths.entry(path).or_default();
        self.dirty |= *labels & label == 0;
        *labels |= label;
    }

    fn rebuild_tree(&mut self) {
        let mut tree = FileTree::default();
        let mut nodes = 0;
        let mut bytes = 0;
        self.tree_omitted = false;
        for (path, labels) in &self.paths {
            if !tree.insert(path, *labels, &mut nodes, &mut bytes) {
                self.tree_omitted = true;
                break;
            }
        }
        self.lines.clear();
        let mut bytes = OMITTED_NOTICE.len() + 1;
        self.tree_omitted |= !tree.append_lines("", &mut self.lines, &mut bytes);
    }

    pub(crate) fn visible_text(
        &self,
        offset: u16,
        height: u16,
        theme: MissionControlTheme,
    ) -> Text<'_> {
        let count = self.lines.len().max(1) + usize::from(self.has_omissions());
        let lines = (usize::from(offset)..count)
            .take(usize::from(height))
            .map(|index| {
                if let Some((line, reasons)) = self.lines.get(index) {
                    Line::styled(line.as_str(), file_reason_color(theme, *reasons))
                } else if index == 0 && self.lines.is_empty() {
                    Line::raw("No recorded file activity.")
                } else {
                    Line::raw(OMITTED_NOTICE)
                }
            })
            .collect::<Vec<_>>();
        Text::from(lines)
    }

    fn has_omissions(&self) -> bool {
        self.omitted || self.tree_omitted
    }

    pub(crate) fn max_scroll(&self, height: u16) -> u16 {
        self.lines
            .len()
            .max(1)
            .saturating_add(usize::from(self.has_omissions()))
            .saturating_sub(usize::from(height))
            .min(usize::from(u16::MAX)) as u16
    }
}

/// Edits take precedence over writes, then reads; all reason letters remain visible.
pub(crate) fn file_reason_color(theme: MissionControlTheme, reasons: u8) -> Color {
    let reasons = reasons | (reasons >> 3);
    if reasons & 4 != 0 {
        theme.diff_changed_color()
    } else if reasons & 2 != 0 {
        theme.diff_inserted_color()
    } else if reasons & 1 != 0 {
        theme.text_accent()
    } else {
        theme.text_primary()
    }
}

#[derive(Default)]
struct FileTree {
    children: BTreeMap<String, FileTree>,
    labels: u8,
}

impl FileTree {
    fn insert(&mut self, path: &str, labels: u8, nodes: &mut usize, bytes: &mut usize) -> bool {
        let mut node = self;
        let components = path
            .starts_with('/')
            .then_some("/")
            .into_iter()
            .chain(path.split('/').filter(|part| !part.is_empty()));
        for component in components {
            if !node.children.contains_key(component) {
                if *nodes >= MAX_TREE_ROWS - 1 || *bytes + component.len() > MAX_TREE_BYTES {
                    return false;
                }
                *nodes += 1;
                *bytes += component.len();
            }
            node = node.children.entry(component.to_string()).or_default();
        }
        node.labels |= labels;
        true
    }

    fn append_lines(&self, prefix: &str, lines: &mut Vec<(String, u8)>, bytes: &mut usize) -> bool {
        for (index, (name, node)) in self.children.iter().enumerate() {
            let last = index + 1 == self.children.len();
            let branch = if last { "└── " } else { "├── " };
            let labels: String = [
                (1, " R", " R (inferred)"),
                (2, " W", " W (inferred)"),
                (4, " E", " E (inferred)"),
            ]
            .into_iter()
            .filter_map(|(bit, confirmed, inferred)| {
                if node.labels & bit != 0 {
                    Some(confirmed)
                } else if node.labels & (bit << 3) != 0 {
                    Some(inferred)
                } else {
                    None
                }
            })
            .collect();
            let name = crate::tui::activity::sanitize_and_bound_preview(name).replace('\n', " ");
            let folder = if (node.children.is_empty() && node.labels != 0) || name == "/" {
                ""
            } else {
                "/"
            };
            let line = format!("{prefix}{branch}{name}{folder}{labels}");
            if lines.len() >= MAX_TREE_ROWS - 1 || *bytes + line.len() + 1 > MAX_TREE_BYTES {
                return false;
            }
            *bytes += line.len() + 1;
            lines.push((line, node.labels));
            if !node.append_lines(
                &format!("{prefix}{}", if last { "    " } else { "│   " }),
                lines,
                bytes,
            ) {
                return false;
            }
        }
        true
    }
}