Codex Handbook
tui/src/bottom_pane/pending_thread_approvals.rs 149 lines
use ratatui::buffer::Buffer;use ratatui::layout::Rect;use ratatui::style::Stylize;use ratatui::text::Line;use ratatui::widgets::Paragraph;use crate::render::renderable::Renderable;use crate::wrapping::RtOptions;use crate::wrapping::adaptive_wrap_lines;/// Widget that lists inactive threads with outstanding approval requests.pub(crate) struct PendingThreadApprovals {    threads: Vec<String>,}impl PendingThreadApprovals {    pub(crate) fn new() -> Self {        Self {            threads: Vec::new(),        }    }    pub(crate) fn set_threads(&mut self, threads: Vec<String>) -> bool {        if self.threads == threads {            return false;        }        self.threads = threads;        true    }    pub(crate) fn is_empty(&self) -> bool {        self.threads.is_empty()    }    #[cfg(test)]    pub(crate) fn threads(&self) -> &[String] {        &self.threads    }    fn as_renderable(&self, width: u16) -> Box<dyn Renderable> {        if self.threads.is_empty() || width < 4 {            return Box::new(());        }        let mut lines = Vec::new();        for thread in self.threads.iter().take(3) {            let wrapped = adaptive_wrap_lines(                std::iter::once(Line::from(format!("Approval needed in {thread}"))),                RtOptions::new(width as usize)                    .initial_indent(Line::from(vec!["  ".into(), "!".red().bold(), " ".into()]))                    .subsequent_indent(Line::from("    ")),            );            lines.extend(wrapped);        }        if self.threads.len() > 3 {            lines.push(Line::from("    ...".dim().italic()));        }        lines.push(            Line::from(vec![                "    ".into(),                "/agent".cyan().bold(),                " to switch threads".dim(),            ])            .dim(),        );        Paragraph::new(lines).into()    }}impl Renderable for PendingThreadApprovals {    fn render(&self, area: Rect, buf: &mut Buffer) {        if area.is_empty() {            return;        }        self.as_renderable(area.width).render(area, buf);    }    fn desired_height(&self, width: u16) -> u16 {        self.as_renderable(width).desired_height(width)    }}#[cfg(test)]mod tests {    use super::*;    use insta::assert_snapshot;    use pretty_assertions::assert_eq;    fn snapshot_rows(widget: &PendingThreadApprovals, width: u16) -> String {        let height = widget.desired_height(width);        let mut buf = Buffer::empty(Rect::new(0, 0, width, height));        widget.render(Rect::new(0, 0, width, height), &mut buf);        (0..height)            .map(|y| {                (0..width)                    .map(|x| buf[(x, y)].symbol().chars().next().unwrap_or(' '))                    .collect::<String>()            })            .collect::<Vec<_>>()            .join("\n")    }    #[test]    fn desired_height_empty() {        let widget = PendingThreadApprovals::new();        assert_eq!(widget.desired_height(/*width*/ 40), 0);    }    #[test]    fn render_single_thread_snapshot() {        let mut widget = PendingThreadApprovals::new();        widget.set_threads(vec!["Robie [explorer]".to_string()]);        assert_snapshot!(            snapshot_rows(&widget, /*width*/ 40).replace(' ', "."),            @r"        ..!.Approval.needed.in.Robie.[explorer].        ..../agent.to.switch.threads............        "        );    }    #[test]    fn render_multiple_threads_snapshot() {        let mut widget = PendingThreadApprovals::new();        widget.set_threads(vec![            "Main [default]".to_string(),            "Robie [explorer]".to_string(),            "Inspector".to_string(),            "Extra agent".to_string(),        ]);        assert_snapshot!(            snapshot_rows(&widget, /*width*/ 44).replace(' ', "."),            @r"        ..!.Approval.needed.in.Main.[default].......        ..!.Approval.needed.in.Robie.[explorer].....        ..!.Approval.needed.in.Inspector............        ............................................        ..../agent.to.switch.threads................        "        );    }}