From 7db578fca663b3e0026996e9c50104784d420744 Mon Sep 17 00:00:00 2001 From: Felipe Coury Date: Tue, 22 Sep 2026 20:25:00 +0000 Subject: [PATCH] Preserve user prompt URL destinations across terminal widths (#47360) ## Why User prompt URLs only received explicit hyperlink destinations when they required forced wrapping, leaving other URLs without those annotations. ## What changed Annotate web URLs before wrapping user messages so their full destinations survive both adaptive wrapping and forced splitting of oversized URLs. ## Testing Add a regression test and snapshot at widths of 120, 60, and 24 columns, checking URL destinations, line width limits, and matching display and transcript output, including a prompt with an image placeholder and a parenthesized URL. GitOrigin-RevId: fccae7b76a3613776d7a663c41e699ebad7eebc1 --- codex-rs/tui/src/history_cell/messages.rs | 7 ++- .../tui/src/history_cell/messages_tests.rs | 49 +++++++++++++++++++ ..._urls_keep_destinations_across_widths.snap | 31 ++++++++++++ 3 files changed, 83 insertions(+), 4 deletions(-) create mode 100644 codex-rs/tui/src/history_cell/snapshots/codex_tui__history_cell__messages__tests__user_prompt_urls_keep_destinations_across_widths.snap diff --git a/codex-rs/tui/src/history_cell/messages.rs b/codex-rs/tui/src/history_cell/messages.rs index 1bdc477c0ba8..07676e3d21d1 100644 --- a/codex-rs/tui/src/history_cell/messages.rs +++ b/codex-rs/tui/src/history_cell/messages.rs @@ -5,7 +5,7 @@ use super::markdown_render_cache::MarkdownRenderCache; use super::*; use crate::style::accent_color_on; use crate::style::history_prompt_style; -use crate::terminal_hyperlinks::annotate_web_urls_in_line; +use crate::terminal_hyperlinks::annotate_web_urls; use crate::terminal_hyperlinks::lines_with_sources_eq; use crate::terminal_hyperlinks::remap_source_wrapped_line; use crate::wrapping::url_preserving_wrap_options; @@ -289,18 +289,17 @@ fn wrap_user_message( build_user_message_lines_with_elements(message, text_elements, style, element_style) }; let mut wrapped = crate::terminal_hyperlinks::adaptive_wrap_hyperlink_lines( - &plain_hyperlink_lines(logical_lines), + &annotate_web_urls(logical_lines), wrap_options, ) .into_iter() - .flat_map(|mut line| { + .flat_map(|line| { if line.width() <= usize::from(wrap_width) { return vec![line]; } // Terminal autowrap loses the message gutter and background. Explicitly split // oversized URL tokens while retaining their complete OSC-8 destination. - line.hyperlinks = annotate_web_urls_in_line(line.line.clone()).hyperlinks; let forced_lines = word_wrap_line_with_source( &line.line, url_preserving_wrap_options(RtOptions::new(usize::from(wrap_width))) diff --git a/codex-rs/tui/src/history_cell/messages_tests.rs b/codex-rs/tui/src/history_cell/messages_tests.rs index 4ab7aa85fdc9..50d5d0113314 100644 --- a/codex-rs/tui/src/history_cell/messages_tests.rs +++ b/codex-rs/tui/src/history_cell/messages_tests.rs @@ -3,6 +3,55 @@ use crate::history_cell::markdown_render_cache::MarkdownRenderCacheKey; use assert_matches::assert_matches; use pretty_assertions::assert_eq; +#[test] +fn user_prompt_urls_keep_destinations_across_widths() { + let url = "https://example.test/terminal-link-wrapping/solid-octants"; + let placeholder = "[Image #1]"; + let message = format!( + "from this site, write out the ascii for solid octants 18 columns {url}\n{placeholder} See (https://example.test/a)." + ); + let image_start = message.find(placeholder).unwrap(); + let cell = new_user_prompt( + message, + vec![TextElement::new( + (image_start..image_start + placeholder.len()).into(), + Some(placeholder.to_string()), + )], + Vec::new(), + Vec::new(), + ); + let mut snapshots = Vec::new(); + for width in [120, 60, 24] { + let lines = cell.display_hyperlink_lines(width); + assert_eq!(cell.transcript_hyperlink_lines(width), lines); + assert!(lines.iter().all(|line| line.width() <= usize::from(width))); + for destination in [url, "https://example.test/a"] { + let linked_text = lines + .iter() + .flat_map(|line| { + line.hyperlinks + .iter() + .filter(|link| link.destination == destination) + .flat_map(|link| { + line.line + .to_string() + .chars() + .skip(link.columns.start) + .take(link.columns.len()) + .collect::>() + }) + }) + .collect::(); + assert_eq!(linked_text, destination, "width {width}"); + } + snapshots.push(format!( + "width {width}\n{}", + ratatui::text::Text::from(visible_lines(lines)) + )); + } + insta::assert_snapshot!(snapshots.join("\n\n")); +} + #[test] fn local_image_only_user_message_remains_visible() { let cell = new_user_prompt( diff --git a/codex-rs/tui/src/history_cell/snapshots/codex_tui__history_cell__messages__tests__user_prompt_urls_keep_destinations_across_widths.snap b/codex-rs/tui/src/history_cell/snapshots/codex_tui__history_cell__messages__tests__user_prompt_urls_keep_destinations_across_widths.snap new file mode 100644 index 000000000000..c4524aa7fad8 --- /dev/null +++ b/codex-rs/tui/src/history_cell/snapshots/codex_tui__history_cell__messages__tests__user_prompt_urls_keep_destinations_across_widths.snap @@ -0,0 +1,31 @@ +--- +source: tui/src/history_cell/messages_tests.rs +expression: "snapshots.join(\"\\n\\n\")" +--- +width 120 + +› from this site, write out the ascii for solid octants 18 columns + https://example.test/terminal-link-wrapping/solid-octants + [Image #1] See (https://example.test/a). + + +width 60 + +› from this site, write out the ascii for solid octants 18 + columns + https://example.test/terminal-link-wrapping/solid-octants + [Image #1] See (https://example.test/a). + + +width 24 + +› from this site, write + out the ascii for + solid octants 18 + columns + https://example.test/ + terminal-link-wrappin + g/solid-octants + [Image #1] See + (https://example.test + /a).