mirror of
https://github.com/openai/codex.git
synced 2026-10-20 00:18:49 +00:00
Preserve clickable URLs in TUI approval headers (#51439)
## What changed Render command, permission, patch, and MCP elicitation approval headers with `HyperlinkText`, which annotates web URLs and preserves their full destinations across wrapping and clipping. ## Testing Add a regression test covering wrapped URL destinations in all four header types, plus a snapshot of the wrapped command approval header. GitOrigin-RevId: 7f6b79484c9297cd03514da2d6805b0d239fee63
This commit is contained in:
@@ -3,11 +3,10 @@
|
||||
use super::approval_overlay::ApplyPatchApprovalRequest;
|
||||
use crate::diff_model::FileChange;
|
||||
use crate::render::renderable::Renderable;
|
||||
use crate::terminal_hyperlinks::HyperlinkText;
|
||||
use codex_utils_path_uri::LegacyAppPathString;
|
||||
use ratatui::style::Stylize;
|
||||
use ratatui::text::Line;
|
||||
use ratatui::widgets::Paragraph;
|
||||
use ratatui::widgets::Wrap;
|
||||
|
||||
pub(super) fn build_header(request: &ApplyPatchApprovalRequest) -> Box<dyn Renderable> {
|
||||
let mut header = Vec::new();
|
||||
@@ -70,5 +69,5 @@ pub(super) fn build_header(request: &ApplyPatchApprovalRequest) -> Box<dyn Rende
|
||||
destination.bold(),
|
||||
]));
|
||||
}
|
||||
Box::new(Paragraph::new(header).wrap(Wrap { trim: false }))
|
||||
Box::new(HyperlinkText::new(header))
|
||||
}
|
||||
|
||||
@@ -735,7 +735,7 @@ fn build_header(request: &ApprovalRequest) -> Box<dyn Renderable> {
|
||||
header.extend(full_cmd_lines);
|
||||
}
|
||||
}
|
||||
Box::new(Paragraph::new(header).wrap(Wrap { trim: false }))
|
||||
Box::new(crate::terminal_hyperlinks::HyperlinkText::new(header))
|
||||
}
|
||||
ApprovalRequest::Permissions(request) => {
|
||||
let mut header: Vec<Line<'static>> = Vec::new();
|
||||
@@ -763,7 +763,7 @@ fn build_header(request: &ApprovalRequest) -> Box<dyn Renderable> {
|
||||
rule_line.fg(crate::style::accent_color()),
|
||||
]));
|
||||
}
|
||||
Box::new(Paragraph::new(header).wrap(Wrap { trim: false }))
|
||||
Box::new(crate::terminal_hyperlinks::HyperlinkText::new(header))
|
||||
}
|
||||
ApprovalRequest::ApplyPatch(request) => super::apply_patch_header::build_header(request),
|
||||
ApprovalRequest::McpElicitation(request) => {
|
||||
@@ -780,7 +780,7 @@ fn build_header(request: &ApprovalRequest) -> Box<dyn Renderable> {
|
||||
Line::from(""),
|
||||
Line::from(request.message.clone()),
|
||||
]);
|
||||
Box::new(Paragraph::new(lines).wrap(Wrap { trim: false }))
|
||||
Box::new(crate::terminal_hyperlinks::HyperlinkText::new(lines))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1438,6 +1438,60 @@ mod tests {
|
||||
assert!(saw_denied, "expected deny shortcut to emit denied decision");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn approval_headers_preserve_wrapped_url_destinations() {
|
||||
let url = "https://github.com/openai/codex/pull/12345?diff=split";
|
||||
for mut request in [
|
||||
make_exec_request(),
|
||||
make_permissions_request(),
|
||||
make_elicitation_request(),
|
||||
ApprovalRequest::ApplyPatch(ApplyPatchApprovalRequest {
|
||||
thread_id: ThreadId::new(),
|
||||
thread_label: None,
|
||||
id: "patch".into(),
|
||||
reason: None,
|
||||
cwd: absolute_path("/tmp"),
|
||||
changes: HashMap::new(),
|
||||
}),
|
||||
] {
|
||||
match &mut request {
|
||||
ApprovalRequest::Exec(request) => request.reason = Some(url.into()),
|
||||
ApprovalRequest::Permissions(request) => request.reason = Some(url.into()),
|
||||
ApprovalRequest::McpElicitation(request) => request.message = url.into(),
|
||||
ApprovalRequest::ApplyPatch(request) => request.reason = Some(url.into()),
|
||||
}
|
||||
let header = build_header(&request);
|
||||
let area = Rect::new(0, 0, 32, header.desired_height(/*width*/ 32));
|
||||
let mut buf = Buffer::empty(area);
|
||||
header.render(area, &mut buf);
|
||||
let linked = buf
|
||||
.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::<String>();
|
||||
assert_eq!(linked, url);
|
||||
if matches!(request, ApprovalRequest::Exec(_)) {
|
||||
let visible = buf
|
||||
.content
|
||||
.chunks(32)
|
||||
.map(|row| {
|
||||
row.iter()
|
||||
.map(|cell| crate::terminal_hyperlinks::strip_osc8(cell.symbol()))
|
||||
.collect::<String>()
|
||||
.trim_end()
|
||||
.to_owned()
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
insta::assert_snapshot!("approval_header_wrapped_url", visible);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn network_deny_shortcut_submits_policy_deny_decision() {
|
||||
let (tx, mut rx) = unbounded_channel::<AppEvent>();
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/approval_overlay.rs
|
||||
assertion_line: 1462
|
||||
expression: visible
|
||||
---
|
||||
Reason:
|
||||
https://github.com/openai/codex/
|
||||
pull/12345?diff=split
|
||||
|
||||
$ echo hi
|
||||
@@ -7,6 +7,7 @@ mod paragraph;
|
||||
mod source;
|
||||
|
||||
pub(crate) use paragraph::HyperlinkParagraph;
|
||||
pub(crate) use paragraph::HyperlinkText;
|
||||
pub(crate) use source::LineWrapPolicy;
|
||||
pub(crate) use source::LogicalLineSource;
|
||||
|
||||
|
||||
@@ -1,16 +1,38 @@
|
||||
//! Paragraph rendering that keeps visible text and hyperlink annotations aligned.
|
||||
|
||||
use super::HyperlinkLine;
|
||||
use super::annotate_web_urls;
|
||||
use super::mark_buffer_hyperlinks;
|
||||
use super::visible_lines_ref;
|
||||
use crate::render::renderable::Renderable;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::Style;
|
||||
use ratatui::text::Line;
|
||||
use ratatui::text::Text;
|
||||
use ratatui::widgets::Paragraph;
|
||||
use ratatui::widgets::Widget;
|
||||
use ratatui::widgets::Wrap;
|
||||
|
||||
/// Owns a word-wrapped paragraph whose web destinations survive clipping and wrapping.
|
||||
pub(crate) struct HyperlinkText(Vec<HyperlinkLine>);
|
||||
|
||||
impl HyperlinkText {
|
||||
pub(crate) fn new(lines: Vec<Line<'static>>) -> Self {
|
||||
Self(annotate_web_urls(lines))
|
||||
}
|
||||
}
|
||||
|
||||
impl Renderable for HyperlinkText {
|
||||
fn render(&self, area: Rect, buf: &mut Buffer) {
|
||||
HyperlinkParagraph::new(&self.0, Style::default()).render(area, buf);
|
||||
}
|
||||
|
||||
fn desired_height(&self, width: u16) -> u16 {
|
||||
HyperlinkParagraph::new(&self.0, Style::default()).line_count(width) as u16
|
||||
}
|
||||
}
|
||||
|
||||
/// Word-wraps without trimming and applies the same vertical scroll to text and links.
|
||||
pub(crate) struct HyperlinkParagraph<'a> {
|
||||
lines: &'a [HyperlinkLine],
|
||||
|
||||
Reference in New Issue
Block a user