magi-code 0.96.1

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

impl MissionControlState {
    pub(crate) fn activity_is_descendant_of(&self, id: &ActivityId, ancestor: &ActivityId) -> bool {
        // Visual parent links are authoritative when present. Path-shaped ids
        // are only a fallback for nodes whose creator did not provide parent
        // information or for ids not yet present in the graph. Explicit roots
        // and unresolved declared parents must not inherit path ancestry.
        if id == ancestor {
            return false;
        }
        let Some(_) = self.nodes.get(id) else {
            return id.is_path_descendant_of(ancestor);
        };

        let mut current = id;
        let mut visited = HashSet::new();
        let mut has_visual_parent = false;
        loop {
            if !visited.insert(current.clone()) {
                return false;
            }
            let Some(node) = self.nodes.get(current) else {
                return false;
            };
            let Some(parent_id) = node.parent_id.as_ref() else {
                return !has_visual_parent
                    && matches!(
                        &node.parent_provenance,
                        ActivityParentProvenance::Unavailable
                    )
                    && id.is_path_descendant_of(ancestor);
            };
            has_visual_parent = true;
            if parent_id == ancestor {
                return true;
            }
            if !self.nodes.contains_key(parent_id) {
                return false;
            }
            current = parent_id;
        }
    }

    pub(super) fn parent_assignment_would_cycle(
        &self,
        id: &ActivityId,
        parent: &ActivityId,
    ) -> bool {
        let mut current = parent;
        let mut visited = HashSet::new();
        loop {
            if current == id || !visited.insert(current.clone()) {
                return true;
            }
            let Some(node) = self.nodes.get(current) else {
                return false;
            };
            let Some(next) = node.parent_id.as_ref() else {
                return false;
            };
            current = next;
        }
    }

    pub(super) fn reattach_waiting_children(&mut self, parent_id: &ActivityId) -> bool {
        let waiting_children = self
            .nodes
            .iter()
            .filter(|(_, node)| {
                node.parent_id.is_none()
                    && matches!(
                        &node.parent_provenance,
                        ActivityParentProvenance::Declared(declared_parent)
                            if declared_parent == parent_id
                    )
            })
            .map(|(child_id, _)| child_id.clone())
            .collect::<Vec<_>>();
        let mut changed = false;
        for child_id in waiting_children {
            if self.parent_assignment_would_cycle(&child_id, parent_id) {
                continue;
            }
            self.attach(&child_id, Some(parent_id));
            changed |= self
                .nodes
                .get(&child_id)
                .is_some_and(|node| node.parent_id.as_ref() == Some(parent_id));
        }
        changed
    }

    pub(in crate::tui::state) fn attach(&mut self, id: &ActivityId, parent: Option<&ActivityId>) {
        let parent = parent
            .filter(|parent| self.nodes.contains_key(*parent))
            .cloned();
        if let Some(parent) = parent.as_ref()
            && self.parent_assignment_would_cycle(id, parent)
        {
            return;
        }
        match parent.as_ref() {
            Some(parent) => {
                self.roots.retain(|root| root != id);
                let children = self.children.entry(parent.clone()).or_default();
                if !children.contains(id) {
                    children.push(id.clone());
                }
            }
            None => {
                if !self.roots.contains(id) {
                    self.roots.push(id.clone());
                }
            }
        }
        if let Some(node) = self.nodes.get_mut(id) {
            node.parent_id = parent;
        }
    }

    pub(super) fn detach(&mut self, id: &ActivityId, parent: Option<&ActivityId>) {
        // Invalidate the old ancestry before removing the link. The event's
        // final update will mark the new ancestry after reparenting.
        self.mark_activity_updated(id);
        self.roots.retain(|root| root != id);
        if let Some(parent) = parent
            && let Some(children) = self.children.get_mut(parent)
        {
            children.retain(|child| child != id);
        }
        if let Some(node) = self.nodes.get_mut(id) {
            node.parent_id = None;
        }
    }

    pub(super) fn aggregate_parent_status(&mut self, id: &ActivityId) {
        let mut parent = self.nodes.get(id).and_then(|node| node.parent_id.clone());
        let mut visited = HashSet::new();
        while let Some(parent_id) = parent {
            if !visited.insert(parent_id.clone()) {
                break;
            }
            let statuses = self.children.get(&parent_id).cloned().unwrap_or_default();
            let failed = statuses.iter().any(|child| {
                self.nodes.get(child).is_some_and(|node| {
                    !matches!(
                        node.kind,
                        ActivityKind::Hook | ActivityKind::ProviderContextInjection
                    ) && node.status == ActivityStatus::Failed
                })
            });
            if failed
                && let Some(node) = self.nodes.get_mut(&parent_id)
                && !is_active_status(node.status)
            {
                node.status = ActivityStatus::Failed;
            }
            parent = self
                .nodes
                .get(&parent_id)
                .and_then(|node| node.parent_id.clone());
        }
    }
}