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
This commit is contained in:
Felipe Coury
2026-09-22 20:34:21 +00:00
committed by copyberry
parent 722dae7afd
commit 7db578fca6
3 changed files with 83 additions and 4 deletions
+3 -4
View File
@@ -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)))
@@ -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::<Vec<_>>()
})
})
.collect::<String>();
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(
@@ -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).