diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__warnings_view__tests__warnings_wrapped_url.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__warnings_view__tests__warnings_wrapped_url.snap new file mode 100644 index 0000000000..63de9bf0b3 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__warnings_view__tests__warnings_wrapped_url.snap @@ -0,0 +1,13 @@ +--- +source: tui/src/bottom_pane/warnings_view_tests.rs +expression: "draw(&view, 32, 9)" +--- + Warnings · 1 of 2 · MCP · ex + + Review https://github.com/ + openai/codex/pull/12345? + diff=split before + continuing. + + + k keep & next · ⌃o copy diff --git a/codex-rs/tui/src/bottom_pane/warnings_view_render.rs b/codex-rs/tui/src/bottom_pane/warnings_view_render.rs index 295f80c6b1..98557c86a5 100644 --- a/codex-rs/tui/src/bottom_pane/warnings_view_render.rs +++ b/codex-rs/tui/src/bottom_pane/warnings_view_render.rs @@ -46,24 +46,31 @@ impl Renderable for WarningsView { } let lines: Vec<_> = details .lines() - .flat_map(|line| textwrap::wrap(line, usize::from(body.width.max(/*other*/ 1)))) + .flat_map(|text| { + let source = + crate::terminal_hyperlinks::annotate_web_urls_in_line(text.to_owned().into()); + let wrapped = + crate::wrapping::wrap_ranges_trim(text, usize::from(body.width.max(1))) + .into_iter() + .map(|range| crate::wrapping::WrappedLine { + line: (&text[range.clone()]).into(), + range, + prefix_bytes: 0, + }) + .collect(); + crate::terminal_hyperlinks::remap_source_wrapped_line(&source, wrapped) + }) .collect(); let page_size = usize::from(body.height.max(/*other*/ 1)); self.page_size.set(page_size); self.max_offset.set(lines.len().saturating_sub(page_size)); let offset = self.offset.get().min(self.max_offset.get()); self.offset.set(offset); - for (row, line) in lines - .iter() - .skip(offset) - .take(usize::from(body.height)) - .enumerate() - { - Line::from(line.as_ref()).render( - Rect::new(body.x, body.y + row as u16, body.width, /*height*/ 1), - buf, - ); - } + crate::terminal_hyperlinks::HyperlinkParagraph::new( + &lines[offset..], + ratatui::style::Style::default(), + ) + .render(body, buf); // Plain k belongs to this viewer, including when configured as a chord prefix. let hint = |context, action| { self.keymap.primary_hint(context, action).filter(|hint| { diff --git a/codex-rs/tui/src/bottom_pane/warnings_view_tests.rs b/codex-rs/tui/src/bottom_pane/warnings_view_tests.rs index a2fb2581dc..3b864d7a49 100644 --- a/codex-rs/tui/src/bottom_pane/warnings_view_tests.rs +++ b/codex-rs/tui/src/bottom_pane/warnings_view_tests.rs @@ -39,7 +39,7 @@ fn draw(view: &WarningsView, width: u16, height: u16) -> String { .chunks(usize::from(width)) .map(|row| { row.iter() - .map(ratatui::buffer::Cell::symbol) + .map(|cell| crate::terminal_hyperlinks::strip_osc8(cell.symbol())) .collect::() .trim_end() .to_string() @@ -169,3 +169,51 @@ fn warnings_keep_current_and_next_without_dismissing_unvisited_pages() { }; assert_eq!((dismissed, kept), (expected, vec![entries()[0].clone()])); } + +#[test] +fn warning_links_preserve_destinations_when_wrapped_and_scrolled() { + let url = "https://github.com/openai/codex/pull/12345?diff=split"; + let (tx, _) = unbounded_channel(); + let mut data = entries(); + data[0].details = format!("Review {url} before continuing."); + let mut view = WarningsView::new( + data, + Arc::default(), + RuntimeKeymap::defaults(), + AppEventSender::new(tx), + ); + let area = Rect::new(0, 0, 32, 9); + let mut buffer = Buffer::empty(area); + view.render(area, &mut buffer); + let linked = buffer + .content + .iter() + .filter(|cell| cell.symbol().contains("\x1b]8;;")) + .map(|cell| { + assert!(cell.symbol().starts_with(&format!("\x1b]8;;{url}\x07"))); + crate::terminal_hyperlinks::strip_osc8(cell.symbol()) + }) + .collect::(); + assert_eq!(linked, url); + insta::assert_snapshot!( + "warnings_wrapped_url", + draw(&view, /*width*/ 32, /*height*/ 9) + ); + key(&mut view, KeyCode::Down); + let area = Rect::new(0, 0, 32, 5); + let mut buffer = Buffer::empty(area); + view.render(area, &mut buffer); + key(&mut view, KeyCode::Down); + view.render(area, &mut buffer); + let linked = buffer + .content + .iter() + .filter(|cell| cell.symbol().contains("\x1b]8;;")) + .collect::>(); + assert!(!linked.is_empty()); + assert!( + linked + .iter() + .all(|cell| cell.symbol().starts_with(&format!("\x1b]8;;{url}\x07"))) + ); +}