Skip to content

Commit 5da229e

Browse files
charliemarsh-oaicopyberry
authored andcommitted
Make URLs in the TUI warnings viewer clickable (#51451)
## What changed Render web URLs in warning details as terminal hyperlinks, preserving the full destination when a URL wraps across lines or the viewer scrolls. ## Testing Add a regression test for hyperlink destinations after wrapping and scrolling, plus a snapshot of a warning containing a wrapped URL. GitOrigin-RevId: 6500e6129a95b2dea8e8f53296ca056e63196a57
1 parent eb2a33a commit 5da229e

3 files changed

Lines changed: 81 additions & 13 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
source: tui/src/bottom_pane/warnings_view_tests.rs
3+
expression: "draw(&view, 32, 9)"
4+
---
5+
Warnings · 1 of 2 · MCP · ex
6+
7+
Review https://github.com/
8+
openai/codex/pull/12345?
9+
diff=split before
10+
continuing.
11+
12+
13+
k keep & next · ⌃o copy

‎codex-rs/tui/src/bottom_pane/warnings_view_render.rs‎

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -46,24 +46,31 @@ impl Renderable for WarningsView {
4646
}
4747
let lines: Vec<_> = details
4848
.lines()
49-
.flat_map(|line| textwrap::wrap(line, usize::from(body.width.max(/*other*/ 1))))
49+
.flat_map(|text| {
50+
let source =
51+
crate::terminal_hyperlinks::annotate_web_urls_in_line(text.to_owned().into());
52+
let wrapped =
53+
crate::wrapping::wrap_ranges_trim(text, usize::from(body.width.max(1)))
54+
.into_iter()
55+
.map(|range| crate::wrapping::WrappedLine {
56+
line: (&text[range.clone()]).into(),
57+
range,
58+
prefix_bytes: 0,
59+
})
60+
.collect();
61+
crate::terminal_hyperlinks::remap_source_wrapped_line(&source, wrapped)
62+
})
5063
.collect();
5164
let page_size = usize::from(body.height.max(/*other*/ 1));
5265
self.page_size.set(page_size);
5366
self.max_offset.set(lines.len().saturating_sub(page_size));
5467
let offset = self.offset.get().min(self.max_offset.get());
5568
self.offset.set(offset);
56-
for (row, line) in lines
57-
.iter()
58-
.skip(offset)
59-
.take(usize::from(body.height))
60-
.enumerate()
61-
{
62-
Line::from(line.as_ref()).render(
63-
Rect::new(body.x, body.y + row as u16, body.width, /*height*/ 1),
64-
buf,
65-
);
66-
}
69+
crate::terminal_hyperlinks::HyperlinkParagraph::new(
70+
&lines[offset..],
71+
ratatui::style::Style::default(),
72+
)
73+
.render(body, buf);
6774
// Plain k belongs to this viewer, including when configured as a chord prefix.
6875
let hint = |context, action| {
6976
self.keymap.primary_hint(context, action).filter(|hint| {

‎codex-rs/tui/src/bottom_pane/warnings_view_tests.rs‎

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ fn draw(view: &WarningsView, width: u16, height: u16) -> String {
3939
.chunks(usize::from(width))
4040
.map(|row| {
4141
row.iter()
42-
.map(ratatui::buffer::Cell::symbol)
42+
.map(|cell| crate::terminal_hyperlinks::strip_osc8(cell.symbol()))
4343
.collect::<String>()
4444
.trim_end()
4545
.to_string()
@@ -169,3 +169,51 @@ fn warnings_keep_current_and_next_without_dismissing_unvisited_pages() {
169169
};
170170
assert_eq!((dismissed, kept), (expected, vec![entries()[0].clone()]));
171171
}
172+
173+
#[test]
174+
fn warning_links_preserve_destinations_when_wrapped_and_scrolled() {
175+
let url = "https://github.com/openai/codex/pull/12345?diff=split";
176+
let (tx, _) = unbounded_channel();
177+
let mut data = entries();
178+
data[0].details = format!("Review {url} before continuing.");
179+
let mut view = WarningsView::new(
180+
data,
181+
Arc::default(),
182+
RuntimeKeymap::defaults(),
183+
AppEventSender::new(tx),
184+
);
185+
let area = Rect::new(0, 0, 32, 9);
186+
let mut buffer = Buffer::empty(area);
187+
view.render(area, &mut buffer);
188+
let linked = buffer
189+
.content
190+
.iter()
191+
.filter(|cell| cell.symbol().contains("\x1b]8;;"))
192+
.map(|cell| {
193+
assert!(cell.symbol().starts_with(&format!("\x1b]8;;{url}\x07")));
194+
crate::terminal_hyperlinks::strip_osc8(cell.symbol())
195+
})
196+
.collect::<String>();
197+
assert_eq!(linked, url);
198+
insta::assert_snapshot!(
199+
"warnings_wrapped_url",
200+
draw(&view, /*width*/ 32, /*height*/ 9)
201+
);
202+
key(&mut view, KeyCode::Down);
203+
let area = Rect::new(0, 0, 32, 5);
204+
let mut buffer = Buffer::empty(area);
205+
view.render(area, &mut buffer);
206+
key(&mut view, KeyCode::Down);
207+
view.render(area, &mut buffer);
208+
let linked = buffer
209+
.content
210+
.iter()
211+
.filter(|cell| cell.symbol().contains("\x1b]8;;"))
212+
.collect::<Vec<_>>();
213+
assert!(!linked.is_empty());
214+
assert!(
215+
linked
216+
.iter()
217+
.all(|cell| cell.symbol().starts_with(&format!("\x1b]8;;{url}\x07")))
218+
);
219+
}

0 commit comments

Comments
 (0)