agent-file-tools 0.43.0

Agent File Tools — tree-sitter powered code analysis for AI agents
Documentation
use std::panic::{catch_unwind, AssertUnwindSafe};

const FALLBACK_TAIL_BYTES: usize = 16 * 1024;

pub fn render_screen(raw: &[u8], rows: u16, cols: u16) -> String {
    catch_unwind(AssertUnwindSafe(|| render_screen_inner(raw, rows, cols)))
        .unwrap_or_else(|_| render_raw_fallback(raw))
}

fn render_screen_inner(raw: &[u8], rows: u16, cols: u16) -> String {
    let mut parser = vt100::Parser::new(rows, cols, 0);
    parser.process(raw);
    let screen = parser.screen();
    let mut lines: Vec<String> = Vec::new();
    for y in 0..rows {
        let mut text = String::new();
        for x in 0..cols {
            let c = screen
                .cell(y, x)
                .map(|cell| cell.contents())
                .unwrap_or_default();
            if c.is_empty() {
                text.push(' ');
            } else {
                text.push_str(c);
            }
        }
        lines.push(text.trim_end().to_string());
    }
    while lines.last().is_some_and(|line| line.is_empty()) {
        lines.pop();
    }
    lines.join("\n")
}

pub fn render_raw_fallback(raw: &[u8]) -> String {
    let tail = if raw.len() > FALLBACK_TAIL_BYTES {
        &raw[raw.len() - FALLBACK_TAIL_BYTES..]
    } else {
        raw
    };
    let note = if raw.len() > FALLBACK_TAIL_BYTES {
        format!("[PTY screen render panicked; showing last {FALLBACK_TAIL_BYTES} raw bytes]\n")
    } else {
        "[PTY screen render panicked; showing raw PTY bytes]\n".to_string()
    };
    format!("{note}{}", String::from_utf8_lossy(tail))
}

#[cfg(test)]
mod tests {
    use super::*;

    const CASES: &[(&str, &[u8], &str)] = &[
        (
            "cap1_ls",
            include_bytes!("../tests/fixtures/pty_render/cap1_ls.raw"),
            include_str!("../tests/fixtures/pty_render/cap1_ls.xterm.txt"),
        ),
        (
            "cap2_sgr",
            include_bytes!("../tests/fixtures/pty_render/cap2_sgr.raw"),
            include_str!("../tests/fixtures/pty_render/cap2_sgr.xterm.txt"),
        ),
        (
            "cap3_box",
            include_bytes!("../tests/fixtures/pty_render/cap3_box.raw"),
            include_str!("../tests/fixtures/pty_render/cap3_box.xterm.txt"),
        ),
        (
            "cap4_cr",
            include_bytes!("../tests/fixtures/pty_render/cap4_cr.raw"),
            include_str!("../tests/fixtures/pty_render/cap4_cr.xterm.txt"),
        ),
        (
            "cap5_alt",
            include_bytes!("../tests/fixtures/pty_render/cap5_alt.raw"),
            include_str!("../tests/fixtures/pty_render/cap5_alt.xterm.txt"),
        ),
        (
            "cap6_vim",
            include_bytes!("../tests/fixtures/pty_render/cap6_vim.raw"),
            include_str!("../tests/fixtures/pty_render/cap6_vim.xterm.txt"),
        ),
    ];

    #[test]
    fn matches_xterm_headless_golden_corpus() {
        for (name, raw, expected) in CASES {
            assert_eq!(render_screen(raw, 24, 80), *expected, "{name}");
        }
    }

    #[test]
    fn panic_fallback_returns_raw_tail() {
        let raw = b"before panic";
        let rendered = catch_unwind(AssertUnwindSafe(|| {
            render_screen_catching(raw, || panic!("forced vt100 panic"))
        }))
        .expect("render_screen must catch renderer panics");
        assert!(rendered.starts_with("[PTY screen render panicked; showing raw PTY bytes]\n"));
        assert!(rendered.ends_with("before panic"));
    }

    #[test]
    fn fallback_trims_large_raw_payload_to_tail() {
        let raw = vec![b'x'; FALLBACK_TAIL_BYTES + 10];
        let rendered = render_raw_fallback(&raw);
        assert!(rendered.starts_with(&format!(
            "[PTY screen render panicked; showing last {FALLBACK_TAIL_BYTES} raw bytes]\n"
        )));
        assert_eq!(
            rendered.len(),
            format!("[PTY screen render panicked; showing last {FALLBACK_TAIL_BYTES} raw bytes]\n")
                .len()
                + FALLBACK_TAIL_BYTES
        );
    }

    fn render_screen_catching(raw: &[u8], render: impl FnOnce() -> String) -> String {
        catch_unwind(AssertUnwindSafe(render)).unwrap_or_else(|_| render_raw_fallback(raw))
    }
}