magi-code 0.96.1

Repository-aware CLI coding agent for terminal work
Documentation
use crate::{commands::CommandRegistry, skills::SkillDiscovery, tui::state};
use std::path::{Component, Path};

pub(crate) fn tui_autocomplete_candidates(
    commands: &CommandRegistry,
) -> Vec<state::AutocompleteCandidate> {
    let mut candidates = commands
        .list()
        .into_iter()
        .map(|command| {
            state::AutocompleteCandidate::slash_command(
                command.name.clone(),
                command.description.clone(),
            )
        })
        .collect::<Vec<_>>();
    candidates.extend(tui_context_injection_autocomplete_candidates());
    candidates
}

fn tui_context_injection_autocomplete_candidates() -> Vec<state::AutocompleteCandidate> {
    crate::agent::prompt_injections::definitions()
        .iter()
        .map(|definition| {
            state::AutocompleteCandidate::context_injection(definition.name, definition.description)
        })
        .collect()
}

pub(crate) fn tui_skill_autocomplete_candidates(
    skills: &SkillDiscovery,
) -> Vec<state::AutocompleteCandidate> {
    skills
        .skills
        .iter()
        .map(|(name, skill)| {
            state::AutocompleteCandidate::skill_tag_with_argument_hint(
                name.clone(),
                skill.frontmatter.get("argument-hint").map(String::as_str),
            )
        })
        .collect()
}

pub(crate) fn refresh_skill_autocomplete_candidates(
    candidates: &mut Vec<state::AutocompleteCandidate>,
    skills: &SkillDiscovery,
) {
    candidates.retain(|candidate| candidate.kind != state::AutocompleteKind::SkillTag);
    candidates.extend(tui_skill_autocomplete_candidates(skills));
}

pub(crate) fn refresh_file_autocomplete_candidates(
    candidates: &mut Vec<state::AutocompleteCandidate>,
    cwd: &Path,
    respect_gitignore: bool,
) {
    candidates.retain(|candidate| candidate.kind != state::AutocompleteKind::FileTag);
    candidates.extend(tui_file_autocomplete_candidates(cwd, respect_gitignore));
}

const MAX_FILE_AUTOCOMPLETE_CANDIDATES: usize = 2000;
const MAX_FILE_AUTOCOMPLETE_VISITED_ENTRIES: usize = 10_000;

pub(crate) fn tui_file_autocomplete_candidates(
    cwd: &Path,
    respect_gitignore: bool,
) -> Vec<state::AutocompleteCandidate> {
    tui_file_autocomplete_candidates_with_limits(
        cwd,
        respect_gitignore,
        MAX_FILE_AUTOCOMPLETE_CANDIDATES,
        MAX_FILE_AUTOCOMPLETE_VISITED_ENTRIES,
    )
}

fn tui_file_autocomplete_candidates_with_limits(
    cwd: &Path,
    respect_gitignore: bool,
    max_candidates: usize,
    max_visited_entries: usize,
) -> Vec<state::AutocompleteCandidate> {
    if max_candidates == 0 || max_visited_entries == 0 {
        return Vec::new();
    }
    let Ok(root) = cwd.canonicalize() else {
        return Vec::new();
    };
    let mut paths = crate::tools::autocomplete_file_paths(
        &root,
        respect_gitignore,
        max_candidates,
        max_visited_entries,
    )
    .unwrap_or_default()
    .iter()
    .filter_map(|path| path.strip_prefix(&root).ok())
    .map(slash_normalized_relative_path)
    .collect::<Vec<_>>();
    paths.sort();
    paths.dedup();
    paths
        .into_iter()
        .map(state::AutocompleteCandidate::file_tag)
        .collect()
}

fn slash_normalized_relative_path(path: &Path) -> String {
    path.components()
        .filter_map(|component| match component {
            Component::Normal(value) => Some(value.to_string_lossy().into_owned()),
            _ => None,
        })
        .collect::<Vec<_>>()
        .join("/")
}