mirror of
https://github.com/openai/codex.git
synced 2026-10-20 00:18:49 +00:00
Preserve terminal hyperlinks in agents overview previews (#50431)
## What changed Keep Markdown link destinations attached to prompt and last-message previews in the agents overview details pane as text wraps and is clipped. Render visible links as terminal hyperlinks, while keeping truncation ellipses unlinked. ## Testing Add a regression test and snapshot covering links that span multiple rows, prompt links, and clipped previews with unlinked ellipses. GitOrigin-RevId: d121e062b197790ce45a70d6d64aadcb181e4dde
This commit is contained in:
committed by
copyberry
parent
bee28e8a06
commit
b6903c0669
@@ -959,6 +959,56 @@ async fn agents_overview_details_render_markdown() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn agents_overview_preview_links_survive_wrapping_and_clipping() {
|
||||
let mut app = make_test_app().await;
|
||||
let thread_id = ThreadId::new();
|
||||
let mut thread = overview_thread(
|
||||
thread_id,
|
||||
/*parent_thread_id*/ None,
|
||||
"Review links",
|
||||
ThreadStatus::Idle,
|
||||
);
|
||||
let destination = "https://example.com/a/long/path/to/the/pull/request";
|
||||
thread.preview = "[Prompt](https://example.com/prompt)".into();
|
||||
app.agents_overview.last_messages.insert(
|
||||
thread_id,
|
||||
format!("Opened [a pull request with a label that wraps across rows]({destination}).\n\nMore context that should be clipped in a short preview.\n\nAdditional details."),
|
||||
);
|
||||
let view = app.agents_overview_view(vec![thread], Some(thread_id));
|
||||
for height in [40, 22] {
|
||||
let area =
|
||||
ratatui::layout::Rect::new(/*x*/ 0, /*y*/ 0, /*width*/ 96, height);
|
||||
let mut buffer = ratatui::buffer::Buffer::empty(area);
|
||||
view.render(area, &mut buffer);
|
||||
let mut linked_rows = Vec::new();
|
||||
for y in 0..height {
|
||||
let linked = (0..area.width)
|
||||
.map(|x| buffer[(x, y)].symbol())
|
||||
.filter(|symbol| symbol.starts_with(&format!("\x1b]8;;{destination}\x07")))
|
||||
.map(crate::terminal_hyperlinks::strip_osc8)
|
||||
.collect::<String>();
|
||||
if !linked.is_empty() {
|
||||
linked_rows.push(linked);
|
||||
}
|
||||
}
|
||||
assert!(linked_rows.len() >= 2, "link must span multiple rows");
|
||||
assert!(buffer.content.iter().all(|cell| {
|
||||
crate::terminal_hyperlinks::strip_osc8(cell.symbol()) != "…"
|
||||
|| !cell.symbol().contains("\x1b]8;;")
|
||||
}));
|
||||
if height == 40 {
|
||||
insta::assert_snapshot!("agents_overview_link_rows", linked_rows.join("\n"));
|
||||
assert!(buffer.content.iter().any(|cell| {
|
||||
cell.symbol()
|
||||
.starts_with("\x1b]8;;https://example.com/prompt\x07")
|
||||
}));
|
||||
} else {
|
||||
assert!(buffer.content.iter().any(|cell| cell.symbol() == "…"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agents_overview_markdown_preview_preserves_layout_and_bounds() {
|
||||
let text = format!("a\r\n\t\u{1b}{}", "界".repeat(600));
|
||||
|
||||
@@ -29,6 +29,11 @@ use crate::keymap::ListAction;
|
||||
use crate::keymap::ListKeymap;
|
||||
use crate::keymap::RuntimeKeymap;
|
||||
use crate::render::renderable::Renderable;
|
||||
use crate::terminal_hyperlinks::HyperlinkLine;
|
||||
use crate::terminal_hyperlinks::mark_buffer_hyperlinks;
|
||||
use crate::terminal_hyperlinks::plain_hyperlink_lines;
|
||||
use crate::terminal_hyperlinks::remap_wrapped_line;
|
||||
use crate::terminal_hyperlinks::visible_lines;
|
||||
use codex_app_server_protocol::Thread;
|
||||
use codex_app_server_protocol::ThreadActiveFlag;
|
||||
use codex_app_server_protocol::ThreadStatus;
|
||||
@@ -442,38 +447,51 @@ impl AgentsOverviewView {
|
||||
lines.push("Branch".dim().into());
|
||||
lines.push(branch.clone().into());
|
||||
}
|
||||
// Keep destinations attached to their text through wrapping and clipping.
|
||||
let wrap = |lines: Vec<HyperlinkLine>| {
|
||||
lines
|
||||
.into_iter()
|
||||
.flat_map(|line| {
|
||||
let wrapped = crate::wrapping::word_wrap_lines([line.line.clone()], width);
|
||||
remap_wrapped_line(&line, wrapped)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
let preview = super::agents_overview_details::preview_markdown(&row.thread.preview);
|
||||
let prompt_start = crate::wrapping::word_wrap_lines(lines.clone(), width).len();
|
||||
lines.extend([Line::default(), Line::from("Prompt".dim())]);
|
||||
let prompt = crate::markdown_render::render_markdown_text_with_width_and_cwd(
|
||||
let prompt = crate::markdown_render::render_markdown_lines_with_width_and_cwd(
|
||||
match preview.as_str() {
|
||||
"" => "No prompt available.",
|
||||
preview => preview,
|
||||
},
|
||||
Some(width),
|
||||
Some(row.thread.cwd.as_path()),
|
||||
)
|
||||
.lines;
|
||||
let mut prompt = crate::wrapping::word_wrap_lines(prompt, width);
|
||||
);
|
||||
let mut prompt = wrap(prompt);
|
||||
if prompt.len() > 2 {
|
||||
prompt.truncate(2);
|
||||
prompt[1] = "…".dim().into();
|
||||
prompt[1] = HyperlinkLine::new("…".dim().into());
|
||||
}
|
||||
lines.extend(prompt);
|
||||
let details_start = crate::wrapping::word_wrap_lines(lines[..4].to_vec(), width).len();
|
||||
let mut lines = crate::wrapping::word_wrap_lines(lines, width);
|
||||
let mut lines = wrap(plain_hyperlink_lines(lines));
|
||||
lines.extend(prompt);
|
||||
if self.state().connection_notice.is_none() {
|
||||
let mut details = row.details.lines.clone();
|
||||
let mut details = plain_hyperlink_lines(row.details.lines.clone());
|
||||
if let Some((message, cwd)) = &row.details.last_message {
|
||||
details.extend([Line::default(), "Last message".dim().into()]);
|
||||
crate::markdown::append_markdown(
|
||||
&crate::markdown::normalize_markdown_for_rendering(message),
|
||||
Some(width),
|
||||
Some(cwd.as_path()),
|
||||
&mut details,
|
||||
details.extend(plain_hyperlink_lines(vec![
|
||||
Line::default(),
|
||||
"Last message".dim().into(),
|
||||
]));
|
||||
details.extend(
|
||||
crate::markdown_render::render_markdown_lines_with_width_and_cwd(
|
||||
&crate::markdown::normalize_markdown_for_rendering(message),
|
||||
Some(width),
|
||||
Some(cwd.as_path()),
|
||||
),
|
||||
);
|
||||
}
|
||||
let mut details = crate::wrapping::word_wrap_lines(details, width);
|
||||
let mut details = wrap(details);
|
||||
if !row.details.usage_lines.is_empty()
|
||||
&& details.len() > usize::from(area.height).saturating_sub(lines.len())
|
||||
{
|
||||
@@ -484,12 +502,13 @@ impl AgentsOverviewView {
|
||||
if details.len() > available {
|
||||
details.truncate(available);
|
||||
if let Some(last) = details.last_mut() {
|
||||
*last = "…".dim().into();
|
||||
*last = HyperlinkLine::new("…".dim().into());
|
||||
}
|
||||
}
|
||||
lines.splice(details_start..details_start, details);
|
||||
}
|
||||
Paragraph::new(lines).render(area, buf);
|
||||
Paragraph::new(visible_lines(lines.clone())).render(area, buf);
|
||||
mark_buffer_hyperlinks(buf, area, &lines, /*scroll_rows*/ 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
---
|
||||
source: tui/src/app/agents_overview_tests.rs
|
||||
assertion_line: 1271
|
||||
expression: "linked_rows.join(\"\\n\")"
|
||||
---
|
||||
a pull request with a label
|
||||
that wraps across rows
|
||||
https://example.com/a/long/path/to/
|
||||
the/pull/request
|
||||
Reference in New Issue
Block a user