supercode-frontend-tui 0.5.53

Attachable terminal frontend primitives for Volter Harness SDK runtimes.
Documentation
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::Modifier;
use ratatui::style::Style;
use ratatui::text::Line;
use ratatui::text::Span;
use ratatui::widgets::Paragraph;
use ratatui::widgets::Widget;
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;

use crate::foundation::wrapping::{adaptive_wrap_lines, RtOptions};
use crate::human::{bounded_terminal_metadata, bounded_terminal_text, sanitize_terminal_text};
use crate::terminal::palette::ColorCapabilities;

use super::ComposerModel;
use super::ComposerOverlayKind;
use super::ComposerTurnState;

/// Pure Ratatui projection of composer, request overlay, commands, and footer.
pub struct ComposerRenderer<'a> {
    model: &'a ComposerModel,
    colors: ColorCapabilities,
}

impl<'a> ComposerRenderer<'a> {
    pub fn new(model: &'a ComposerModel, colors: ColorCapabilities) -> Self {
        Self { model, colors }
    }

    pub fn lines(&self, width: u16) -> Vec<Line<'static>> {
        let width = usize::from(width.max(1));
        let mut lines = Vec::new();
        if let Some(overlay) = self.model.overlay() {
            lines.extend(self.overlay_lines(overlay, width));
            lines.push(Line::default());
        }
        lines.push(self.status_line());
        lines.extend(self.input_lines(width));
        lines.extend(self.command_lines());
        lines.push(self.footer_line(width));
        lines
    }

    pub fn desired_height(&self, width: u16) -> u16 {
        self.lines(width).len().min(usize::from(u16::MAX)) as u16
    }

    pub fn render_buffer(&self, area: Rect, buffer: &mut Buffer) {
        Paragraph::new(self.lines(area.width)).render(area, buffer);
    }

    /// Cursor row and display column within the rendered input body. This is
    /// computed by the exact same word/grapheme layout used by `input_lines`.
    pub fn cursor_position(&self, width: u16) -> (usize, usize) {
        let body_width = usize::from(width.saturating_sub(2).max(1));
        input_layout(self.model.input(), self.model.cursor(), body_width).cursor
    }

    fn status_line(&self) -> Line<'static> {
        let (dot, label, color) = match self.model.turn_state() {
            ComposerTurnState::Idle => ("●", "Ready", (80, 200, 120)),
            ComposerTurnState::Working { .. } => ("●", "Working", (225, 180, 80)),
        };
        let mut spans = vec![
            Span::styled(dot, Style::default().fg(self.colors.best_color(color))),
            Span::raw(" "),
            Span::styled(
                label,
                Style::default()
                    .fg(self.colors.best_color(color))
                    .add_modifier(Modifier::BOLD),
            ),
        ];
        if !self.model.queued_steering().is_empty() {
            spans.push(Span::styled(
                format!(" · {} queued", self.model.queued_steering().len()),
                Style::default().fg(self.colors.best_color((145, 150, 160))),
            ));
        }
        if let Some(failure) = self.model.last_failure() {
            spans.push(Span::styled(
                format!(" · {}", bounded_terminal_metadata(failure, 128)),
                Style::default().fg(self.colors.best_color((235, 100, 100))),
            ));
        }
        Line::from(spans)
    }

    fn input_lines(&self, width: usize) -> Vec<Line<'static>> {
        let body_width = width.saturating_sub(2).max(1);
        let layout = input_layout(self.model.input(), self.model.cursor(), body_width);
        let raw = if layout.text_is_empty {
            vec![Line::from(Span::styled(
                "Ask anything…",
                Style::default().fg(self.colors.best_color((120, 125, 135))),
            ))]
        } else {
            layout
                .lines
                .into_iter()
                .map(|line| Line::from(line.text))
                .collect()
        };
        raw.into_iter()
            .enumerate()
            .map(|(index, line)| {
                let mut spans = Vec::with_capacity(line.spans.len() + 1);
                spans.push(Span::styled(
                    if index == 0 { "› " } else { "  " },
                    Style::default()
                        .fg(self.colors.best_color((120, 190, 255)))
                        .add_modifier(Modifier::BOLD),
                ));
                spans.extend(line.spans);
                Line::from(spans)
            })
            .collect()
    }

    fn command_lines(&self) -> Vec<Line<'static>> {
        let Some(prefix) = self
            .model
            .input()
            .strip_prefix('/')
            .filter(|input| !input.contains(char::is_whitespace))
        else {
            return Vec::new();
        };
        self.model
            .capabilities()
            .operations()
            .iter()
            .filter_map(|operation| operation.command.as_ref())
            .filter(|command| command.name.starts_with(prefix))
            .take(6)
            .map(|command| {
                let mut spans = vec![Span::styled(
                    format!("  /{}", bounded_terminal_metadata(&command.name, 128)),
                    Style::default()
                        .fg(self.colors.best_color((175, 140, 245)))
                        .add_modifier(Modifier::BOLD),
                )];
                if let Some(description) = &command.description {
                    spans.push(Span::styled(
                        format!("  {}", bounded_terminal_metadata(description, 256)),
                        Style::default().fg(self.colors.best_color((145, 150, 160))),
                    ));
                }
                if let Some(argument_hint) = &command.argument_hint {
                    spans.push(Span::styled(
                        format!("  {}", bounded_terminal_metadata(argument_hint, 128)),
                        Style::default().fg(self.colors.best_color((120, 125, 135))),
                    ));
                }
                Line::from(spans)
            })
            .collect()
    }

    fn footer_line(&self, width: usize) -> Line<'static> {
        let capabilities = self.model.capabilities();
        let mut hints = vec!["Enter send", "Shift+Enter newline"];
        if capabilities.can_interrupt {
            hints.push("Ctrl+C interrupt");
        }
        let mut text = hints.join(" · ");
        if text.chars().count() > width {
            text = text.chars().take(width.saturating_sub(1)).collect();
            text.push('…');
        }
        Line::from(Span::styled(
            text,
            Style::default().fg(self.colors.best_color((120, 125, 135))),
        ))
    }

    fn overlay_lines(&self, overlay: &super::ComposerOverlay, width: usize) -> Vec<Line<'static>> {
        let (title, hint) = match overlay.kind {
            ComposerOverlayKind::Approval => ("Approval required", "y allow · a session · n deny"),
            ComposerOverlayKind::ChildApproval => {
                ("Child-agent approval", "y allow · a session · n deny")
            }
            ComposerOverlayKind::Elicitation => (
                "MCP input requested",
                "Enter accept · Ctrl+C decline · Esc cancel",
            ),
            ComposerOverlayKind::Other => (
                "Runtime input requested",
                "Enter accept · Ctrl+C decline · Esc cancel",
            ),
        };
        let title_style = Style::default()
            .fg(self.colors.best_color((235, 175, 80)))
            .add_modifier(Modifier::BOLD);
        let mut lines = vec![Line::from(vec![
            Span::styled("◆ ", title_style),
            Span::styled(title, title_style),
        ])];
        let summary = bounded_terminal_text(&overlay.summary(), 512);
        lines.extend(adaptive_wrap_lines(
            [Line::from(format!("  {summary}"))],
            RtOptions::new(width.max(1)),
        ));
        if matches!(
            overlay.kind,
            ComposerOverlayKind::Elicitation | ComposerOverlayKind::Other
        ) {
            lines.push(Line::from(format!(
                "  › {}",
                bounded_terminal_text(&overlay.input, 512)
            )));
        }
        lines.push(Line::from(Span::styled(
            format!("  {hint}"),
            Style::default().fg(self.colors.best_color((145, 150, 160))),
        )));
        lines
    }
}

impl Widget for ComposerRenderer<'_> {
    fn render(self, area: Rect, buffer: &mut Buffer) {
        self.render_buffer(area, buffer);
    }
}

#[derive(Debug)]
struct InputLayout {
    lines: Vec<WrappedInputLine>,
    cursor: (usize, usize),
    text_is_empty: bool,
}

#[derive(Debug)]
struct WrappedInputLine {
    text: String,
    start: usize,
    end: usize,
}

fn input_layout(input: &str, cursor: usize, width: usize) -> InputLayout {
    let text = sanitize_terminal_text(input);
    let cursor = sanitize_terminal_text(&input[..cursor]).len();
    let mut lines = Vec::new();
    let mut offset = 0usize;
    for logical in text.split('\n') {
        lines.extend(wrap_logical_line(logical, offset, width.max(1)));
        offset += logical.len() + 1;
    }

    let mut cursor_position = (0usize, 0usize);
    for (row, line) in lines.iter().enumerate() {
        if cursor >= line.start && cursor <= line.end {
            let byte = cursor.min(line.end).saturating_sub(line.start);
            let column = UnicodeWidthStr::width(&line.text[..byte]);
            if column >= width && row + 1 < lines.len() {
                cursor_position = (row + 1, 0);
            } else {
                cursor_position = (row, column);
            }
            break;
        }
    }

    InputLayout {
        lines,
        cursor: cursor_position,
        text_is_empty: text.is_empty(),
    }
}

fn wrap_logical_line(text: &str, offset: usize, width: usize) -> Vec<WrappedInputLine> {
    if text.is_empty() {
        return vec![WrappedInputLine {
            text: String::new(),
            start: offset,
            end: offset,
        }];
    }

    let graphemes = text.grapheme_indices(true).collect::<Vec<_>>();
    let mut lines = Vec::new();
    let mut start = 0usize;
    while start < graphemes.len() {
        let mut used = 0usize;
        let mut fit_end = start;
        let mut word_break = None;
        for (index, (_, grapheme)) in graphemes.iter().enumerate().skip(start) {
            let grapheme_width = UnicodeWidthStr::width(*grapheme);
            if fit_end > start && used.saturating_add(grapheme_width) > width {
                break;
            }
            used = used.saturating_add(grapheme_width);
            fit_end = index + 1;
            if grapheme.chars().all(char::is_whitespace) {
                word_break = Some(fit_end);
            }
            if used >= width {
                break;
            }
        }
        if fit_end == start {
            fit_end += 1;
        }
        let end = if fit_end < graphemes.len() {
            word_break.filter(|end| *end > start).unwrap_or(fit_end)
        } else {
            fit_end
        };
        let start_byte = graphemes[start].0;
        let end_byte = graphemes.get(end).map_or(text.len(), |(byte, _)| *byte);
        lines.push(WrappedInputLine {
            text: text[start_byte..end_byte].to_string(),
            start: offset + start_byte,
            end: offset + end_byte,
        });
        start = end;
    }
    lines
}