Codex Handbook
tui/src/bottom_pane/pending_input_preview.rs 389 lines
use crossterm::event::KeyCode;use ratatui::buffer::Buffer;use ratatui::layout::Rect;use ratatui::style::Stylize;use ratatui::text::Line;use ratatui::widgets::Paragraph;use crate::key_hint;use crate::render::renderable::Renderable;use crate::wrapping::RtOptions;use crate::wrapping::adaptive_wrap_lines;/// Widget that displays pending steers plus follow-up inputs held while a turn is in progress.////// The widget renders pending steers first, then rejected steers that will be/// resubmitted at end of turn, then ordinary queued user messages. Pending/// steers explain that they will be submitted after the next tool/result/// boundary unless the user invokes the interrupt binding to send them/// immediately. The edit hint at the bottom only appears when there are actual/// queued user inputs to pop back into the composer. Because some terminals/// intercept certain modifier-key combinations, the displayed binding is/// configurable via [`set_edit_binding`](Self::set_edit_binding).pub(crate) struct PendingInputPreview {    pub pending_steers: Vec<String>,    pub rejected_steers: Vec<String>,    pub queued_messages: Vec<String>,    /// Key combination rendered in the hint line.  Defaults to Alt+Up but may    /// be overridden for terminals where that chord is unavailable.    edit_binding: Option<key_hint::KeyBinding>,    /// Key combination rendered for immediately interrupting and sending steers.    interrupt_binding: Option<key_hint::KeyBinding>,}const PREVIEW_LINE_LIMIT: usize = 3;impl PendingInputPreview {    pub(crate) fn new() -> Self {        Self {            pending_steers: Vec::new(),            rejected_steers: Vec::new(),            queued_messages: Vec::new(),            edit_binding: Some(key_hint::alt(KeyCode::Up)),            interrupt_binding: Some(key_hint::plain(KeyCode::Esc)),        }    }    /// Replace the keybinding shown in the hint line at the bottom of the    /// queued-messages list.  The caller is responsible for also wiring the    /// corresponding key event handler.    pub(crate) fn set_edit_binding(&mut self, binding: Option<key_hint::KeyBinding>) {        self.edit_binding = binding;    }    pub(crate) fn set_interrupt_binding(&mut self, binding: Option<key_hint::KeyBinding>) {        self.interrupt_binding = binding;    }    fn push_truncated_preview_lines(        lines: &mut Vec<Line<'static>>,        wrapped: Vec<Line<'static>>,        overflow_line: Line<'static>,    ) {        let wrapped_len = wrapped.len();        lines.extend(wrapped.into_iter().take(PREVIEW_LINE_LIMIT));        if wrapped_len > PREVIEW_LINE_LIMIT {            lines.push(overflow_line);        }    }    fn push_section_header(lines: &mut Vec<Line<'static>>, width: u16, header: Line<'static>) {        let mut spans = vec!["• ".dim()];        spans.extend(header.spans);        lines.extend(adaptive_wrap_lines(            std::iter::once(Line::from(spans)),            RtOptions::new(width as usize).subsequent_indent(Line::from("  ".dim())),        ));    }    fn as_renderable(&self, width: u16) -> Box<dyn Renderable> {        if (self.pending_steers.is_empty()            && self.rejected_steers.is_empty()            && self.queued_messages.is_empty())            || width < 4        {            return Box::new(());        }        let mut lines = vec![];        if !self.pending_steers.is_empty() {            let mut header = vec!["Messages to be submitted after next tool call".into()];            if let Some(interrupt_binding) = self.interrupt_binding {                header.extend(vec![                    " (press ".dim(),                    interrupt_binding.into(),                    " to interrupt and send immediately)".dim(),                ]);            }            Self::push_section_header(&mut lines, width, Line::from(header));            for steer in &self.pending_steers {                let wrapped = adaptive_wrap_lines(                    steer.lines().map(|line| Line::from(line.dim())),                    RtOptions::new(width as usize)                        .initial_indent(Line::from("  ↳ ".dim()))                        .subsequent_indent(Line::from("    ")),                );                Self::push_truncated_preview_lines(&mut lines, wrapped, Line::from("    …".dim()));            }        }        if !self.rejected_steers.is_empty() {            if !lines.is_empty() {                lines.push(Line::from(""));            }            Self::push_section_header(                &mut lines,                width,                "Messages to be submitted at end of turn".into(),            );            for steer in &self.rejected_steers {                let wrapped = adaptive_wrap_lines(                    steer.lines().map(|line| Line::from(line.dim())),                    RtOptions::new(width as usize)                        .initial_indent(Line::from("  ↳ ".dim()))                        .subsequent_indent(Line::from("    ")),                );                Self::push_truncated_preview_lines(&mut lines, wrapped, Line::from("    …".dim()));            }        }        if !self.queued_messages.is_empty() {            if !lines.is_empty() {                lines.push(Line::from(""));            }            Self::push_section_header(&mut lines, width, "Queued follow-up inputs".into());            for message in &self.queued_messages {                let wrapped = adaptive_wrap_lines(                    message.lines().map(|line| Line::from(line.dim().italic())),                    RtOptions::new(width as usize)                        .initial_indent(Line::from("  ↳ ".dim()))                        .subsequent_indent(Line::from("    ")),                );                Self::push_truncated_preview_lines(                    &mut lines,                    wrapped,                    Line::from("    …".dim().italic()),                );            }        }        if !self.queued_messages.is_empty()            && let Some(edit_binding) = self.edit_binding        {            lines.push(                Line::from(vec![                    "    ".into(),                    edit_binding.into(),                    " edit last queued message".into(),                ])                .dim(),            );        }        Paragraph::new(lines).into()    }}impl Renderable for PendingInputPreview {    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;    #[test]    fn desired_height_empty() {        let queue = PendingInputPreview::new();        assert_eq!(queue.desired_height(/*width*/ 40), 0);    }    #[test]    fn desired_height_one_message() {        let mut queue = PendingInputPreview::new();        queue.queued_messages.push("Hello, world!".to_string());        assert_eq!(queue.desired_height(/*width*/ 40), 3);    }    #[test]    fn render_one_message() {        let mut queue = PendingInputPreview::new();        queue.queued_messages.push("Hello, world!".to_string());        let width = 40;        let height = queue.desired_height(width);        let mut buf = Buffer::empty(Rect::new(0, 0, width, height));        queue.render(Rect::new(0, 0, width, height), &mut buf);        assert_snapshot!("render_one_message", format!("{buf:?}"));    }    #[test]    fn render_one_message_with_shift_left_binding() {        let mut queue = PendingInputPreview::new();        queue.queued_messages.push("Hello, world!".to_string());        queue.set_edit_binding(Some(key_hint::shift(KeyCode::Left)));        let width = 40;        let height = queue.desired_height(width);        let mut buf = Buffer::empty(Rect::new(0, 0, width, height));        queue.render(Rect::new(0, 0, width, height), &mut buf);        assert_snapshot!(            "render_one_message_with_shift_left_binding",            format!("{buf:?}")        );    }    #[test]    fn render_two_messages() {        let mut queue = PendingInputPreview::new();        queue.queued_messages.push("Hello, world!".to_string());        queue            .queued_messages            .push("This is another message".to_string());        let width = 40;        let height = queue.desired_height(width);        let mut buf = Buffer::empty(Rect::new(0, 0, width, height));        queue.render(Rect::new(0, 0, width, height), &mut buf);        assert_snapshot!("render_two_messages", format!("{buf:?}"));    }    #[test]    fn render_more_than_three_messages() {        let mut queue = PendingInputPreview::new();        queue.queued_messages.push("Hello, world!".to_string());        queue            .queued_messages            .push("This is another message".to_string());        queue            .queued_messages            .push("This is a third message".to_string());        queue            .queued_messages            .push("This is a fourth message".to_string());        let width = 40;        let height = queue.desired_height(width);        let mut buf = Buffer::empty(Rect::new(0, 0, width, height));        queue.render(Rect::new(0, 0, width, height), &mut buf);        assert_snapshot!("render_more_than_three_messages", format!("{buf:?}"));    }    #[test]    fn render_wrapped_message() {        let mut queue = PendingInputPreview::new();        queue            .queued_messages            .push("This is a longer message that should be wrapped".to_string());        queue            .queued_messages            .push("This is another message".to_string());        let width = 40;        let height = queue.desired_height(width);        let mut buf = Buffer::empty(Rect::new(0, 0, width, height));        queue.render(Rect::new(0, 0, width, height), &mut buf);        assert_snapshot!("render_wrapped_message", format!("{buf:?}"));    }    #[test]    fn render_many_line_message() {        let mut queue = PendingInputPreview::new();        queue            .queued_messages            .push("This is\na message\nwith many\nlines".to_string());        let width = 40;        let height = queue.desired_height(width);        let mut buf = Buffer::empty(Rect::new(0, 0, width, height));        queue.render(Rect::new(0, 0, width, height), &mut buf);        assert_snapshot!("render_many_line_message", format!("{buf:?}"));    }    #[test]    fn long_url_like_message_does_not_expand_into_wrapped_ellipsis_rows() {        let mut queue = PendingInputPreview::new();        queue.queued_messages.push(            "example.test/api/v1/projects/alpha-team/releases/2026-02-17/builds/1234567890/artifacts/reports/performance/summary/detail/session_id=abc123def456ghi789"                .to_string(),        );        let width = 36;        let height = queue.desired_height(width);        assert_eq!(            height, 3,            "expected header, one message row, and hint row for URL-like token"        );        let mut buf = Buffer::empty(Rect::new(0, 0, width, height));        queue.render(Rect::new(0, 0, width, height), &mut buf);        let rendered_rows = (0..height)            .map(|y| {                (0..width)                    .map(|x| buf[(x, y)].symbol().chars().next().unwrap_or(' '))                    .collect::<String>()            })            .collect::<Vec<_>>();        assert!(            !rendered_rows.iter().any(|row| row.contains('…')),            "expected no wrapped-ellipsis row for URL-like token, got rows: {rendered_rows:?}"        );    }    #[test]    fn render_one_pending_steer() {        let mut queue = PendingInputPreview::new();        queue.pending_steers.push("Please continue.".to_string());        let width = 48;        let height = queue.desired_height(width);        let mut buf = Buffer::empty(Rect::new(0, 0, width, height));        queue.render(Rect::new(0, 0, width, height), &mut buf);        assert_snapshot!("render_one_pending_steer", format!("{buf:?}"));    }    #[test]    fn render_one_pending_steer_with_remapped_interrupt_binding() {        let mut queue = PendingInputPreview::new();        queue.pending_steers.push("Please continue.".to_string());        queue.set_interrupt_binding(Some(key_hint::plain(KeyCode::F(12))));        let width = 48;        let height = queue.desired_height(width);        let mut buf = Buffer::empty(Rect::new(0, 0, width, height));        queue.render(Rect::new(0, 0, width, height), &mut buf);        assert_snapshot!(            "render_one_pending_steer_with_remapped_interrupt_binding",            format!("{buf:?}")        );    }    #[test]    fn render_pending_steers_above_queued_messages() {        let mut queue = PendingInputPreview::new();        queue.pending_steers.push("Please continue.".to_string());        queue            .pending_steers            .push("Check the last command output.".to_string());        queue            .rejected_steers            .push("Rejected steer that will be retried.".to_string());        queue            .queued_messages            .push("Queued follow-up question".to_string());        let width = 52;        let height = queue.desired_height(width);        let mut buf = Buffer::empty(Rect::new(0, 0, width, height));        queue.render(Rect::new(0, 0, width, height), &mut buf);        assert_snapshot!(            "render_pending_steers_above_queued_messages",            format!("{buf:?}")        );    }    #[test]    fn render_multiline_pending_steer_uses_single_prefix_and_truncates() {        let mut queue = PendingInputPreview::new();        queue            .pending_steers            .push("First line\nSecond line\nThird line\nFourth line".to_string());        let width = 48;        let height = queue.desired_height(width);        let mut buf = Buffer::empty(Rect::new(0, 0, width, height));        queue.render(Rect::new(0, 0, width, height), &mut buf);        assert_snapshot!(            "render_multiline_pending_steer_uses_single_prefix_and_truncates",            format!("{buf:?}")        );    }}