From 858aea34491fec77cb5069548fc842d4a177a84a Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 6 Oct 2026 17:59:18 +0000 Subject: [PATCH] Make URLs clickable in user verification prompts (#51458) ## What changed Render user verification titles and request details with `HyperlinkText` so URLs carry terminal hyperlinks, including in the waiting-for-verification view. ## Testing Extend the narrow-prompt regression test to verify that wrapped URLs retain their full hyperlink targets before and during verification. Strip OSC 8 annotations when comparing visible text in snapshots. GitOrigin-RevId: 9b2fba963c028eaa2b177220b0487364b44d04bb --- .../tui/src/bottom_pane/user_verification.rs | 6 ++-- .../bottom_pane/user_verification_tests.rs | 36 +++++++++++++++++-- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/codex-rs/tui/src/bottom_pane/user_verification.rs b/codex-rs/tui/src/bottom_pane/user_verification.rs index 436613a939..612e1c4b4f 100644 --- a/codex-rs/tui/src/bottom_pane/user_verification.rs +++ b/codex-rs/tui/src/bottom_pane/user_verification.rs @@ -261,7 +261,9 @@ fn user_verification_options(keymap: &ApprovalKeymap) -> Vec Box { let mut header = ColumnRenderable::new(); - header.push(Paragraph::new(request.title.clone().bold()).wrap(Wrap { trim: false })); + header.push(crate::terminal_hyperlinks::HyperlinkText::new(vec![ + request.title.clone().bold().into(), + ])); header.push(Line::from("")); header.push(request_details(request)); Box::new(header) @@ -281,7 +283,7 @@ fn request_details(request: &UserVerificationRequest) -> Box { Line::from(""), Line::from(request.description.clone()), ]); - Box::new(Paragraph::new(lines).wrap(Wrap { trim: false })) + Box::new(crate::terminal_hyperlinks::HyperlinkText::new(lines)) } fn waiting_view(request: &UserVerificationRequest, keymap: &ListKeymap) -> Box { diff --git a/codex-rs/tui/src/bottom_pane/user_verification_tests.rs b/codex-rs/tui/src/bottom_pane/user_verification_tests.rs index 601c067fc3..351f909d55 100644 --- a/codex-rs/tui/src/bottom_pane/user_verification_tests.rs +++ b/codex-rs/tui/src/bottom_pane/user_verification_tests.rs @@ -11,7 +11,7 @@ fn render_view_lines(view: &UserVerificationView, width: u16, height: u16) -> St (0..buf.area.height) .map(|row| { (0..buf.area.width) - .map(|col| buf[(col, row)].symbol().to_string()) + .map(|col| crate::terminal_hyperlinks::strip_osc8(buf[(col, row)].symbol())) .collect::() .trim_end() .to_string() @@ -157,7 +157,7 @@ fn full_screen_request_is_available_before_and_during_verification() { #[test] fn long_url_tokens_remain_visible_in_a_narrow_verification_prompt() { - let (view, _) = make_view_for_request(UserVerificationRequest { + let (mut view, _) = make_view_for_request(UserVerificationRequest { title: "Approve https://example.com/production/release/candidate?".to_string(), description: "Deploy https://example.com/production/critical-release-candidate".to_string(), thread_label: None, @@ -168,6 +168,38 @@ fn long_url_tokens_remain_visible_in_a_narrow_verification_prompt() { "user_verification_long_urls", render_view_lines(&view, /*width*/ 30, view.desired_height(/*width*/ 30)) ); + let urls = [ + "https://example.com/production/release/candidate?", + "https://example.com/production/critical-release-candidate", + ]; + for waiting in [false, true] { + if waiting { + view.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); + } + let area = Rect::new(0, 0, 30, view.desired_height(/*width*/ 30)); + let mut buf = Buffer::empty(area); + view.render(area, &mut buf); + let linked = buf + .content + .iter() + .filter(|cell| cell.symbol().contains("\x1b]8;;")) + .map(|cell| { + assert!( + urls.iter() + .any(|url| cell.symbol().starts_with(&format!("\x1b]8;;{url}\x07"))) + ); + crate::terminal_hyperlinks::strip_osc8(cell.symbol()) + }) + .collect::(); + assert_eq!( + linked, + if waiting { + urls[1].to_owned() + } else { + urls.concat() + } + ); + } } #[tokio::test]