Render async question prompts as Markdown in the TUI (#55411)

## What changed

Render async question titles with the Markdown renderer so emphasis, fenced code blocks, and labeled links display with their intended formatting. Remove blanket bold styling and wrap overwide tokens while preserving hyperlink destinations and accurate row counts.

## Testing

Update rendering snapshots and extend the question rendering test to cover Markdown formatting, link destinations, selective bold styling, and equivalent LF and CRLF rendering at narrow and wide widths.

GitOrigin-RevId: b3e8a94a04433c2d9e5d11e66f83cbd16959d8ed
This commit is contained in:
Benjamin Carlsson
2026-10-20 00:17:51 +00:00
committed by copyberry
parent 7827cd886d
commit 1a35bc4ccb
7 changed files with 53 additions and 42 deletions
@@ -17,10 +17,6 @@ use crate::keymap::KeymapContext;
use crate::keymap::ListAction;
use crate::keymap::RuntimeKeymap;
use crate::terminal_hyperlinks::HyperlinkLine;
use crate::terminal_hyperlinks::annotate_web_urls_in_line;
use crate::terminal_hyperlinks::remap_source_wrapped_line;
use crate::wrapping::WrappedLine;
use crate::wrapping::wrap_ranges_trim;
use codex_protocol::items::AsyncUserInputQuestion;
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
@@ -156,26 +152,29 @@ impl AsyncQuestions {
}
pub(super) fn wrapped_question_lines(&self, width: u16) -> Vec<HyperlinkLine> {
let width = usize::from(width.max(1));
self.current_question()
.map(|q| {
// Line::from removes line endings, so wrap and annotate each logical line
// separately to keep source byte ranges aligned with the visible text.
q.title
.split('\n')
.flat_map(|text| {
let text = text.strip_suffix('\r').unwrap_or(text);
let source = annotate_web_urls_in_line(text.to_owned().into());
let wrapped = wrap_ranges_trim(text, usize::from(width.max(1)))
.into_iter()
.map(|range| WrappedLine {
line: (&text[range.clone()]).into(),
range,
prefix_bytes: 0,
})
.collect();
remap_source_wrapped_line(&source, wrapped)
})
.collect()
.map(|question| {
crate::markdown_render::render_markdown_lines_with_width_cwd_and_hidden_link_destinations(
&question.title,
Some(width),
/*cwd*/ None,
&crate::markdown_render::hide_web_link_destination,
)
.into_iter()
.flat_map(|line| {
if line.width() <= width {
vec![line]
} else {
// The question layout counts physical rows, so break overwide Markdown
// tokens while preserving their hyperlink destinations.
crate::terminal_hyperlinks::remap_source_wrapped_line(
&line,
crate::wrapping::word_wrap_line_with_source(&line.line, width),
)
}
})
.collect()
})
.unwrap_or_default()
}
@@ -52,7 +52,7 @@ impl Renderable for AsyncQuestions {
Paragraph::new(self.progress_prefix_text().dim()).render(sections.progress_area, buf);
HyperlinkParagraph::new(
&sections.question_lines,
crate::style::accent_style().bold(),
crate::style::accent_style().not_bold(),
)
.render(sections.question_area, buf);
@@ -1,6 +1,7 @@
---
source: tui/src/bottom_pane/async_questions/state_tests.rs
assertion_line: 483
expression: buffer_text(&clipped)
---
Long question Long question Long question
Long question Long question Long question Long
@@ -1,5 +1,6 @@
---
source: tui/src/bottom_pane/async_questions/state_tests.rs
assertion_line: 220
expression: "rendered[0]"
---
@@ -7,11 +8,10 @@ expression: "rendered[0]"
Run this command:
```sh
printf '診 断 '
```
Then review (https://example.com/diagnostics?view=full)?
Then review the diagnostics
(https://example.com/diagnostics?view=full)?
Type your answer
@@ -1,13 +1,13 @@
---
source: tui/src/bottom_pane/async_questions/state_tests.rs
assertion_line: 172
assertion_line: 171
expression: "crate::terminal_hyperlinks::strip_osc8(&buffer_text(&buffer))"
---
2 of 2
Review this change (https://
github.com/openai/codex/pull/12345?
diff=split)?
Review this change
(https://github.com/openai/codex/
pull/12345?diff=split)?
› 1. Named
2. Other
@@ -1,13 +1,14 @@
---
source: tui/src/bottom_pane/async_questions/state_tests.rs
assertion_line: 253
expression: buffer_text(&buffer)
---
Long question Long question Long question
Long question Long question Long question
Long question Long question Long question
Long question Long question Long question
Long question Long question Long question
Long question Long question Long question
Long question Long question Long question
Long question Long question Long question Long
question Long question Long question Long
question Long question Long question Long
question Long question Long question Long
question Long question Long question Long
question Long question Long question Long
question Long question Long question Long
› 2. Other
@@ -181,9 +181,11 @@ fn wrapped_question_url_keeps_its_complete_destination() {
}
#[test]
fn multiline_question_preserves_line_breaks_and_link_destinations() {
fn markdown_question_preserves_formatting_and_link_destinations() {
let url = "https://example.com/diagnostics?view=full";
let title = format!("Run this command:\n\n```sh\nprintf '診断'\n```\nThen review ({url})?\n");
let title = format!(
"Run **this command**:\n\n```sh\nprintf '診断'\n```\nThen review the [diagnostics]({url})?\n"
);
let mut rendered = Vec::new();
for line_ending in ["\n", "\r\n"] {
let mut editor = editor();
@@ -207,9 +209,17 @@ fn multiline_question_preserves_line_breaks_and_link_destinations() {
})
})
.collect::<String>();
assert_eq!(linked_text, url);
assert!(linked_text.contains("diagnostics"));
let text = crate::terminal_hyperlinks::strip_osc8(&buffer_text(&buffer));
if width == 60 {
let bold_text = buffer
.content
.iter()
.filter(|cell| cell.modifier.contains(Modifier::BOLD))
.map(ratatui::buffer::Cell::symbol)
.collect::<String>();
assert!(bold_text.contains("this command"));
assert!(!bold_text.contains("Run "));
rendered.push(text);
}
}