magi-code 0.96.1

Repository-aware CLI coding agent for terminal work
Documentation
//! Local-only image preparation. This module never writes to the terminal.
use crossbeam_channel::{Receiver, Sender, bounded};
use ratatui::layout::Rect;
use std::{collections::VecDeque, path::PathBuf, sync::Arc, thread::JoinHandle};

pub(super) const PREVIEW_ROWS: u16 = 20;
pub(super) const PREVIEW_PADDING: u16 = 1;
const PREVIEW_COLUMNS: u16 = 120;
const PREVIEW_MAX_PIXELS: u32 = 1280;
const CACHE_ENTRIES: usize = 16;

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ImageRow {
    pub path: String,
    pub row: u16,
}

#[derive(Debug)]
pub(super) struct PreparedImage {
    pub width: u32,
    pub height: u32,
    pub encoded: String,
}

#[derive(Debug, Clone)]
pub(super) struct ImagePlacement {
    pub image: Arc<PreparedImage>,
    pub area: Rect,
    pub source_y: u32,
    pub source_height: u32,
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct ImagePolicy {
    cwd: PathBuf,
    absolute: bool,
    max_bytes: u64,
    session: Option<String>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct ImageLayout {
    columns: u16,
    cell_pixels: (u16, u16),
}

#[derive(Debug)]
struct ImageRequest {
    path: String,
    policy: ImagePolicy,
    generation: u64,
    layout: ImageLayout,
}

type ImageResult = (ImageRequest, Option<Arc<PreparedImage>>);

#[derive(Debug)]
struct ImageWorker {
    requests: Sender<ImageRequest>,
    results: Receiver<ImageResult>,
    handle: Option<JoinHandle<()>>,
}

#[derive(Debug, Default)]
pub(crate) struct TranscriptImages {
    worker: Option<ImageWorker>,
    policy: Option<ImagePolicy>,
    generation: u64,
    cache: VecDeque<(String, Option<Arc<PreparedImage>>)>,
    pending: bool,
    visible_paths: Vec<String>,
    pub(super) placements: Vec<ImagePlacement>,
    pub(super) cell_pixels: (u16, u16),
    layout: Option<ImageLayout>,
}

impl TranscriptImages {
    pub(super) fn start(cell_pixels: (u16, u16)) -> Self {
        let (requests, receive) = bounded::<ImageRequest>(1);
        let (send, results) = bounded(1);
        let handle = std::thread::Builder::new()
            .name("transcript-images".into())
            .spawn(move || {
                while let Ok(request) = receive.recv() {
                    let prepared = prepare_image(&request).ok().map(Arc::new);
                    if send.send((request, prepared)).is_err() {
                        break;
                    }
                }
            });
        match handle {
            Ok(handle) => Self {
                worker: Some(ImageWorker {
                    requests,
                    results,
                    handle: Some(handle),
                }),
                cell_pixels,
                ..Self::default()
            },
            Err(_) => Self::default(),
        }
    }

    pub(super) fn enabled(&self) -> bool {
        self.worker.is_some()
    }
    pub(super) fn is_pending(&self) -> bool {
        self.pending
    }

    pub(super) fn clear_frame(&mut self) {
        self.placements.clear();
        self.visible_paths.clear();
    }

    pub(super) fn start_next_request(&mut self) {
        if self.pending {
            return;
        }
        let (Some(worker), Some(policy)) = (&self.worker, &self.policy) else {
            return;
        };
        let Some(layout) = self.layout else {
            return;
        };
        let Some(path) = self
            .visible_paths
            .iter()
            .find(|path| !self.cache.iter().any(|(cached, _)| cached == *path))
        else {
            return;
        };
        let request = ImageRequest {
            path: path.clone(),
            policy: policy.clone(),
            generation: self.generation,
            layout,
        };
        self.pending = worker.requests.try_send(request).is_ok();
    }

    pub(super) fn sync_policy(
        &mut self,
        cwd: &std::path::Path,
        absolute: bool,
        max_bytes: u64,
        session: Option<&str>,
    ) -> bool {
        if self.policy.as_ref().is_some_and(|p| {
            p.cwd == cwd
                && p.absolute == absolute
                && p.max_bytes == max_bytes
                && p.session.as_deref() == session
        }) {
            return false;
        }
        self.policy = Some(ImagePolicy {
            cwd: cwd.to_path_buf(),
            absolute,
            max_bytes,
            session: session.map(str::to_owned),
        });
        self.generation = self.generation.wrapping_add(1);
        self.cache.clear();
        self.clear_frame();
        true
    }

    pub(super) fn poll(&mut self) -> bool {
        let Some(worker) = &self.worker else {
            return false;
        };
        match worker.results.try_recv() {
            Ok((request, image)) => {
                self.pending = false;
                if request.generation == self.generation {
                    self.cache.push_back((request.path, image));
                    while self.cache.len() > CACHE_ENTRIES {
                        self.cache.pop_front();
                    }
                }
                true
            }
            Err(crossbeam_channel::TryRecvError::Disconnected) => std::mem::take(&mut self.pending),
            Err(crossbeam_channel::TryRecvError::Empty) => false,
        }
    }

    pub(super) fn place_row(&mut self, row: &ImageRow, area: Rect) {
        if self.placements.len() >= 64 || area.is_empty() {
            return;
        }
        let layout = ImageLayout {
            columns: area
                .width
                .saturating_sub(2 * PREVIEW_PADDING)
                .min(PREVIEW_COLUMNS),
            cell_pixels: self.cell_pixels,
        };
        if self.layout.is_some_and(|previous| previous != layout) {
            self.generation = self.generation.wrapping_add(1);
            self.cache.clear();
            self.clear_frame();
        }
        self.layout = Some(layout);
        if layout.columns == 0 {
            return;
        }
        if !self.visible_paths.contains(&row.path) {
            if self.visible_paths.len() >= CACHE_ENTRIES {
                return;
            }
            self.visible_paths.push(row.path.clone());
        }
        let Some((_, Some(image))) = self.cache.iter().find(|(path, _)| path == &row.path) else {
            return;
        };
        if let Some(placement) = image_row_placement(image, row.row, area, self.cell_pixels) {
            self.placements.push(placement);
        }
    }
}

fn prepare_image(request: &ImageRequest) -> anyhow::Result<PreparedImage> {
    let image = crate::tools::read_local_preview_image(
        &request.policy.cwd,
        &request.path,
        request.policy.absolute,
        request.policy.max_bytes,
    )?;
    prepare_pixels(image, request.layout)
}

fn prepare_pixels(
    image: image::DynamicImage,
    layout: ImageLayout,
) -> anyhow::Result<PreparedImage> {
    use base64::Engine;
    let (cell_width, cell_height) = layout.cell_pixels;
    let (cell_width, cell_height) = (u32::from(cell_width), u32::from(cell_height));
    anyhow::ensure!(
        (1..=640).contains(&cell_width) && (1..=320).contains(&cell_height),
        "unsupported terminal cell dimensions"
    );
    let max_width = u32::from(layout.columns).min(PREVIEW_MAX_PIXELS / cell_width) * cell_width;
    let max_height = u32::from(PREVIEW_ROWS).min(PREVIEW_MAX_PIXELS / cell_height) * cell_height;
    anyhow::ensure!(max_width > 0, "empty image viewport");
    let resized = image
        .thumbnail(image.width().min(max_width), image.height().min(max_height))
        .to_rgba8();
    // Pad instead of stretching pixels to the cell grid. Each placement then
    // crops exactly one cell-height strip, including transparent edge pixels.
    let mut pixels = image::RgbaImage::new(
        resized.width().div_ceil(cell_width) * cell_width,
        resized.height().div_ceil(cell_height) * cell_height,
    );
    image::imageops::replace(&mut pixels, &resized, 0, 0);
    Ok(PreparedImage {
        width: pixels.width(),
        height: pixels.height(),
        encoded: base64::engine::general_purpose::STANDARD.encode(pixels.as_raw()),
    })
}

fn image_row_placement(
    image: &Arc<PreparedImage>,
    row: u16,
    area: Rect,
    cell_pixels: (u16, u16),
) -> Option<ImagePlacement> {
    if area.is_empty()
        || image.width == 0
        || image.height == 0
        || cell_pixels.0 == 0
        || cell_pixels.1 == 0
    {
        return None;
    }
    let (cell_width, cell_height) = (u32::from(cell_pixels.0), u32::from(cell_pixels.1));
    if !image.width.is_multiple_of(cell_width) || !image.height.is_multiple_of(cell_height) {
        return None;
    }
    let columns = image.width / cell_width;
    let rows = image.height / cell_height;
    if columns
        > u32::from(
            area.width
                .saturating_sub(2 * PREVIEW_PADDING)
                .min(PREVIEW_COLUMNS),
        )
        || rows > u32::from(PREVIEW_ROWS)
        || u32::from(row) >= rows
    {
        return None;
    }
    let source_y = u32::from(row) * cell_height;
    Some(ImagePlacement {
        image: Arc::clone(image),
        area: Rect::new(area.x + PREVIEW_PADDING, area.y, columns as u16, 1),
        source_y,
        source_height: cell_height,
    })
}

impl Drop for ImageWorker {
    fn drop(&mut self) {
        if self.handle.as_ref().is_some_and(JoinHandle::is_finished)
            && let Some(handle) = self.handle.take()
        {
            let _ = handle.join();
        }
        // Otherwise channel disconnection ends this sole bounded worker after
        // its current read/decode. Never delay terminal restoration for I/O.
    }
}