vtcode-tui 0.98.6

Reusable TUI primitives and session API for VT Code-style terminal interfaces
use super::{Session, message::TranscriptLine};
use ratatui::prelude::*;
use unicode_width::UnicodeWidthStr;
use vtcode_commons::preview;

pub(crate) struct QueueOverlay {
    pub(crate) width: u16,
    pub(crate) version: u64,
    pub(crate) lines: Vec<Line<'static>>,
}

impl Session {
    pub(crate) fn set_queued_inputs_entries(&mut self, entries: Vec<String>) {
        self.queued_inputs = entries;
        self.invalidate_queue_overlay();
    }

    pub(crate) fn push_queued_input(&mut self, entry: String) {
        self.queued_inputs.push(entry);
        self.invalidate_queue_overlay();
    }

    pub(crate) fn pop_latest_queued_input(&mut self) -> Option<String> {
        let result = self.queued_inputs.pop();
        if result.is_some() {
            self.invalidate_queue_overlay();
        }
        result
    }

    pub(crate) fn queue_input_lines(&self, width: u16) -> Vec<Line<'static>> {
        if width == 0 || self.queued_inputs.is_empty() {
            return Vec::new();
        }

        let max_width = width as usize;
        let mut lines = Vec::new();
        let mut prefix_style = self.styles.accent_style();
        prefix_style = prefix_style.add_modifier(Modifier::BOLD);
        let message_style = self.styles.default_style();

        let prefix = "";
        let prefix_width = UnicodeWidthStr::width(prefix);
        let available = max_width.saturating_sub(prefix_width);

        for entry in self.queued_inputs.iter().rev().take(2) {
            let trimmed = truncate_to_width(entry, available);
            let spans = vec![
                Span::styled(prefix.to_owned(), prefix_style),
                Span::styled(trimmed, message_style),
            ];
            lines.push(Line::from(spans));
        }

        let muted_style = self.styles.default_style().add_modifier(Modifier::DIM);
        lines.push(Line::from(vec![Span::styled(
            super::terminal_capabilities::queued_input_edit_hint().to_string(),
            muted_style,
        )]));

        lines
    }

    pub(crate) fn invalidate_queue_overlay(&mut self) {
        self.queue_overlay_version = self.queue_overlay_version.wrapping_add(1);
        self.queue_overlay_cache = None;
        self.request_transcript_clear();
    }

    pub(crate) fn queue_overlay_lines(&mut self, width: u16) -> Option<&[Line<'static>]> {
        if width == 0 || self.queued_inputs.is_empty() {
            self.queue_overlay_cache = None;
            return None;
        }

        let version = self.queue_overlay_version;
        let needs_rebuild = match &self.queue_overlay_cache {
            Some(cache) => cache.width != width || cache.version != version,
            None => true,
        };

        if needs_rebuild {
            let lines = self.reflow_queue_lines(width);
            self.queue_overlay_cache = Some(QueueOverlay {
                width,
                version,
                lines,
            });
        }

        self.queue_overlay_cache.as_ref().and_then(|cache| {
            if cache.lines.is_empty() {
                None
            } else {
                Some(cache.lines.as_slice())
            }
        })
    }

    pub(crate) fn overlay_queue_lines(
        &mut self,
        visible_lines: &mut [TranscriptLine],
        content_width: u16,
    ) {
        if visible_lines.is_empty() || content_width == 0 {
            return;
        }

        if let Some(queue_lines) = self.queue_overlay_lines(content_width) {
            let queue_visible = queue_lines.len().min(visible_lines.len());
            let start = visible_lines.len().saturating_sub(queue_visible);
            let slice_start = queue_lines.len().saturating_sub(queue_visible);
            let overlay = &queue_lines[slice_start..];
            for (target, source) in visible_lines[start..].iter_mut().zip(overlay.iter()) {
                *target = TranscriptLine {
                    line: source.clone(),
                    explicit_links: Vec::new(),
                };
            }
        }
    }

    fn reflow_queue_lines(&self, width: u16) -> Vec<Line<'static>> {
        self.queue_input_lines(width)
    }
}

fn truncate_to_width(text: &str, max_width: usize) -> String {
    preview::truncate_with_ellipsis(text, max_width, "")
}