Files
codex/codex-rs/tui/src/bottom_pane/async_questions/render.rs
T
Ian MacLeod 6ae8dcf6e1 Add TUI building blocks for inline async question editing (#42889)
## What changed

- Introduce an async-question editor component with per-question drafts, navigation, replay deduplication, and submit or queue handling using the shared composer.
- Add bounded `AnsweredQuestion` framing that truncates question text at a UTF-8 boundary and flattens line breaks before prepending it to an answer.
- Add `prompt_stack_back` and `skip_question` keybinding actions and the `tui.question_esc_back` setting. Normalize `Ctrl+]` and `Ctrl+5` for key matching and conflict detection, and let default question shortcuts yield to explicit bindings.
- Flush buffered typing in both the main composer and the covering view so background input cannot keep the draw loop waiting.

The async-question component is not yet connected to the TUI event flow.

## Testing

Add regression coverage for bounded Unicode question framing, question-shortcut conflicts, and paste-buffer flushing in background and covering editors.

GitOrigin-RevId: 4098043cf588ddafc05c27505645495edd690cb9
2026-09-04 23:56:25 +00:00

80 lines
2.9 KiB
Rust

//! Render freeform questions inline using the existing modal editing engine.
use super::AsyncQuestions;
use crate::bottom_pane::selection_popup_common::menu_surface_inset;
use crate::bottom_pane::selection_popup_common::menu_surface_padding_height;
use crate::bottom_pane::selection_popup_common::render_menu_surface;
use crate::render::renderable::Renderable;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::Stylize;
use ratatui::text::Line;
use ratatui::widgets::Paragraph;
use ratatui::widgets::Widget;
impl AsyncQuestions {
fn question_lines(&self, width: u16) -> Vec<Line<'_>> {
self.current_question()
.map(|q| {
textwrap::wrap(&q.title, usize::from(width.max(1)))
.into_iter()
.map(Line::from)
.collect()
})
.unwrap_or_default()
}
fn input_area(&self, area: Rect) -> Rect {
let area = menu_surface_inset(area);
let input_height = self
.composer
.inline_input_height(area.width)
.min(area.height);
Rect::new(
area.x,
area.bottom().saturating_sub(input_height),
area.width,
input_height,
)
}
}
impl Renderable for AsyncQuestions {
fn cursor_style(&self, area: Rect) -> crossterm::cursor::SetCursorStyle {
self.composer.cursor_style(area)
}
fn desired_height(&self, width: u16) -> u16 {
let width = menu_surface_inset(Rect::new(/*x*/ 0, /*y*/ 0, width, u16::MAX)).width;
self.question_lines(width).len() as u16
+ self.composer.inline_input_height(width)
+ 1
+ u16::from(self.unanswered_count() > 1)
+ u16::from(self.unanswered_count() > 1)
+ menu_surface_padding_height()
}
fn render(&self, area: Rect, buf: &mut Buffer) {
let input = self.input_area(area);
let mut prompt = render_menu_surface(area, buf);
prompt.height = input.y.saturating_sub(prompt.y);
if self.unanswered_count() > 1 && prompt.height > 0 {
Paragraph::new(self.progress_prefix_text().dim()).render(
Rect::new(prompt.x, prompt.y, prompt.width, /*height*/ 1),
buf,
);
prompt.y += 1;
prompt.height -= 1;
}
if self.unanswered_count() > 1
&& prompt.height > self.question_lines(prompt.width).len() as u16
{
prompt.y += 1;
prompt.height -= 1;
}
Paragraph::new(self.question_lines(prompt.width))
.style(crate::style::accent_style())
.bold()
.render(prompt, buf);
self.composer.render_inline_input(input, buf);
}
fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)> {
self.composer.inline_cursor_pos(self.input_area(area))
}
}