magi-code 0.63.2

Repository-aware CLI coding agent for terminal work
Documentation
use serde_json::json;

use super::{
    ToolRuntime,
    args::ListFilesArgs,
    contract::{metadata_key as meta, tool_name},
    result::{ToolResult, ToolResultDisplay},
};

const MAX_FILES: usize = 1000;

impl ToolRuntime {
    pub(super) fn list_files(&self, args: ListFilesArgs) -> anyhow::Result<ToolResult> {
        let path = self.resolve_existing_path(
            &args.path,
            super::fs::ExistingPathPolicy::list_files(self.list_files_absolute_paths),
        )?;
        if !path.is_dir() {
            anyhow::bail!("list_files path must be a directory");
        }

        let listing = self.fs_cache.read_dir_listing(&path)?;
        let mut entries = Vec::new();
        let mut file_count = 0usize;
        let mut directory_count = 0usize;
        let mut truncated = false;
        for entry in listing.entries {
            if entry.file_type.is_file() || (args.include_directories && entry.file_type.is_dir()) {
                if entries.len() == MAX_FILES {
                    truncated = true;
                    break;
                }
                if entry.file_type.is_dir() {
                    directory_count += 1;
                } else {
                    file_count += 1;
                }
                entries.push(entry.name);
            }
        }
        entries.sort();

        Ok(ToolResult {
            tool_name: tool_name::LIST_FILES.to_string(),
            success: true,
            content: entries.join("\n"),
            metadata: json!({
                (meta::PATH): args.path,
                (meta::FILES): file_count,
                (meta::DIRECTORIES): directory_count,
                (meta::TRUNCATED): truncated
            }),
            display: ToolResultDisplay::default(),
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::tools::ToolRuntime;
    use std::fs;

    #[test]
    fn list_files_repeated_call_uses_cache_without_output_change() {
        let cwd = tempfile::TempDir::new().unwrap();
        fs::write(cwd.path().join("b.txt"), "content").unwrap();
        let runtime = ToolRuntime::new(cwd.path()).unwrap();

        let first = runtime
            .list_files(ListFilesArgs {
                path: ".".to_string(),
                include_directories: false,
            })
            .unwrap();
        let second = runtime
            .list_files(ListFilesArgs {
                path: ".".to_string(),
                include_directories: false,
            })
            .unwrap();
        let listing = runtime.fs_cache.read_dir_listing(cwd.path()).unwrap();

        assert_eq!(first.content, "b.txt");
        assert_eq!(first.content, second.content);
        assert_eq!(first.metadata, second.metadata);
        assert!(listing.cache_hit);
    }

    #[test]
    fn list_files_includes_directories_when_enabled() {
        let cwd = tempfile::TempDir::new().unwrap();
        fs::write(cwd.path().join("b.txt"), "content").unwrap();
        fs::create_dir(cwd.path().join("a_dir")).unwrap();
        let runtime = ToolRuntime::new(cwd.path()).unwrap();

        let files_only = runtime
            .list_files(ListFilesArgs {
                path: ".".to_string(),
                include_directories: false,
            })
            .unwrap();
        assert_eq!(files_only.content, "b.txt");
        assert_eq!(files_only.metadata[meta::FILES], 1);
        assert_eq!(files_only.metadata[meta::DIRECTORIES], 0);

        let with_directories = runtime
            .list_files(ListFilesArgs {
                path: ".".to_string(),
                include_directories: true,
            })
            .unwrap();
        assert_eq!(with_directories.content, "a_dir\nb.txt");
        assert_eq!(with_directories.metadata[meta::FILES], 1);
        assert_eq!(with_directories.metadata[meta::DIRECTORIES], 1);
    }

    #[test]
    fn list_files_allows_absolute_outside_cwd_when_setting_true() {
        let cwd = tempfile::TempDir::new().unwrap();
        let outside = tempfile::TempDir::new().unwrap();
        fs::write(outside.path().join("visible.txt"), "content").unwrap();
        fs::create_dir(outside.path().join("hidden_dir")).unwrap();
        let mut runtime = ToolRuntime::new(cwd.path()).unwrap();
        runtime.list_files_absolute_paths = true;

        let result = runtime
            .list_files(ListFilesArgs {
                path: outside.path().to_string_lossy().into_owned(),
                include_directories: false,
            })
            .unwrap();

        assert_eq!(result.content, "visible.txt");
    }

    #[test]
    fn list_files_rejects_absolute_outside_cwd_when_setting_false() {
        let cwd = tempfile::TempDir::new().unwrap();
        let outside = tempfile::TempDir::new().unwrap();
        let mut runtime = ToolRuntime::new(cwd.path()).unwrap();
        runtime.list_files_absolute_paths = false;

        let error = runtime
            .list_files(ListFilesArgs {
                path: outside.path().to_string_lossy().into_owned(),
                include_directories: false,
            })
            .unwrap_err()
            .to_string();

        assert!(error.contains("tools.list_files.absolute_paths"), "{error}");
    }
}