Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
  • Loading branch information
charliemarsh-oai authored and copyberry committed Oct 6, 2026
commit 858aea34491fec77cb5069548fc842d4a177a84a
6 changes: 4 additions & 2 deletions codex-rs/tui/src/bottom_pane/user_verification.rs
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,9 @@ fn user_verification_options(keymap: &ApprovalKeymap) -> Vec<UserVerificationOpt

pub(crate) fn prompt_header(request: &UserVerificationRequest) -> Box<dyn Renderable> {
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)
Expand All @@ -281,7 +283,7 @@ fn request_details(request: &UserVerificationRequest) -> Box<dyn Renderable> {
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<dyn Renderable> {
Expand Down
36 changes: 34 additions & 2 deletions codex-rs/tui/src/bottom_pane/user_verification_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<String>()
.trim_end()
.to_string()
Expand Down Expand Up @@ -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,
Expand All @@ -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::<String>();
assert_eq!(
linked,
if waiting {
urls[1].to_owned()
} else {
urls.concat()
}
);
}
}

#[tokio::test]
Expand Down
Loading