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
This commit is contained in:
Charlie Marsh
2026-10-06 19:12:16 +00:00
committed by copyberry
parent e44b9bb3da
commit 858aea3449
2 changed files with 38 additions and 4 deletions
@@ -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)
@@ -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> {
@@ -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()
@@ -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::<String>();
assert_eq!(
linked,
if waiting {
urls[1].to_owned()
} else {
urls.concat()
}
);
}
}
#[tokio::test]