Compare commits

...

4 Commits

Author SHA1 Message Date
YOUR NAME
68f6f55328 Merge remote-tracking branch 'origin/main' into codex/15499-run-20260325-170309-e2e-pr
# Conflicts:
#	codex-rs/core/src/tools/handlers/multi_agents_tests.rs
#	codex-rs/tui/src/chatwidget/snapshots/codex_tui_app_server__chatwidget__tests__chatwidget_percent_encoded_local_link_vt100_snapshot.snap
#	codex-rs/tui/src/chatwidget/tests.rs
#	codex-rs/tui_app_server/src/chatwidget/tests.rs
#	codex-rs/tui_app_server/src/markdown_render.rs
#	codex-rs/tui_app_server/src/markdown_render_tests.rs
2026-04-02 14:47:43 -07:00
YOUR NAME
40ab7dd96d Add transcript snapshots for percent-encoded local links 2026-03-27 10:16:01 -07:00
YOUR NAME
5b199615bd Stabilize multi-agent assign_task test 2026-03-26 09:23:37 -07:00
YOUR NAME
f28c13ee9f Fix percent-encoded local file link display in markdown renderer 2026-03-25 17:20:12 -07:00
4 changed files with 179 additions and 1 deletions

View File

@@ -0,0 +1,23 @@
---
source: tui/src/chatwidget/tests.rs
expression: term.backend().vt100().screen().contents()
---
• See docs/random_ß.md:1.

View File

@@ -1564,6 +1564,77 @@ printf 'fenced within fenced\n'
);
}
#[tokio::test]
async fn chatwidget_percent_encoded_local_link_vt100_snapshot() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
chat.config.cwd = PathBuf::from("/Users/example/code/codex").abs();
chat.handle_codex_event(Event {
id: "t1".into(),
msg: EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-1".to_string(),
model_context_window: None,
collaboration_mode_kind: ModeKind::Default,
}),
});
let width: u16 = 80;
let height: u16 = 20;
let backend = VT100Backend::new(width, height);
let mut term = crate::custom_terminal::Terminal::with_options(backend).expect("terminal");
term.set_viewport_area(Rect::new(0, height - 1, width, 1));
let source = "See [random](/Users/example/code/codex/docs/random_%C3%9F.md:1).";
let mut it = source.chars();
loop {
let mut delta = String::new();
match it.next() {
Some(c) => delta.push(c),
None => break,
}
if let Some(c2) = it.next() {
delta.push(c2);
}
chat.handle_codex_event(Event {
id: "t1".into(),
msg: EventMsg::AgentMessageDelta(AgentMessageDeltaEvent { delta }),
});
loop {
chat.on_commit_tick();
let mut inserted_any = false;
while let Ok(app_ev) = rx.try_recv() {
if let AppEvent::InsertHistoryCell(cell) = app_ev {
let lines = cell.display_lines(width);
crate::insert_history::insert_history_lines(&mut term, lines)
.expect("Failed to insert history lines in test");
inserted_any = true;
}
}
if !inserted_any {
break;
}
}
}
chat.handle_codex_event(Event {
id: "t1".into(),
msg: EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-1".to_string(),
last_agent_message: None,
}),
});
for lines in drain_insert_history(&mut rx) {
crate::insert_history::insert_history_lines(&mut term, lines)
.expect("Failed to insert history lines in test");
}
assert_chatwidget_snapshot!(
"chatwidget_percent_encoded_local_link_vt100_snapshot",
normalize_snapshot_paths(term.backend().vt100().screen().contents())
);
}
#[tokio::test]
async fn chatwidget_tall() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;

View File

@@ -789,7 +789,8 @@ fn parse_local_link_target(dest_url: &str) -> Option<(String, Option<String>)> {
location_suffix = Some(suffix);
}
Some((expand_local_link_path(path_text), location_suffix))
let decoded_path_text = decode_percent_encoded_path_text(path_text);
Some((expand_local_link_path(&decoded_path_text), location_suffix))
}
/// Normalize a hash fragment like `L12` or `L12C3-L14C9` into the display suffix we render.
@@ -830,6 +831,49 @@ fn expand_local_link_path(path_text: &str) -> String {
normalize_local_link_path_text(path_text)
}
/// Decode `%XX` path escapes for non-`file://` local links.
///
/// If any escape is malformed or decoded bytes are not UTF-8, the original text is returned
/// unchanged so rendering stays lossless.
fn decode_percent_encoded_path_text(path_text: &str) -> String {
let bytes = path_text.as_bytes();
let mut decoded = Vec::with_capacity(bytes.len());
let mut i = 0usize;
let mut saw_escape = false;
while i < bytes.len() {
if bytes[i] == b'%' {
if i + 2 >= bytes.len() {
return path_text.to_string();
}
let hi = bytes[i + 1];
let lo = bytes[i + 2];
let Some(hi_val) = (hi as char).to_digit(16) else {
return path_text.to_string();
};
let Some(lo_val) = (lo as char).to_digit(16) else {
return path_text.to_string();
};
let decoded_byte = ((hi_val << 4) | lo_val) as u8;
if decoded_byte.is_ascii() {
return path_text.to_string();
}
decoded.push(decoded_byte);
saw_escape = true;
i += 3;
continue;
}
decoded.push(bytes[i]);
i += 1;
}
if !saw_escape {
return path_text.to_string();
}
String::from_utf8(decoded).unwrap_or_else(|_| path_text.to_string())
}
/// Convert a `file://` URL into the normalized local-path text used for transcript rendering.
///
/// This prefers `Url::to_file_path()` for standard file URLs. When that rejects Windows-oriented

View File

@@ -790,6 +790,46 @@ fn file_link_uses_target_path_for_hash_range() {
assert_eq!(text, expected);
}
#[test]
fn file_link_decodes_percent_encoded_path_text() {
let text = render_markdown_text_for_cwd(
"[random](/Users/example/code/codex/docs/random_%C3%9F.md:1)",
Path::new("/Users/example/code/codex"),
);
let expected = Text::from(Line::from_iter(["docs/random_ß.md:1".cyan()]));
assert_eq!(text, expected);
}
#[test]
fn file_link_preserves_invalid_percent_sequences() {
let text = render_markdown_text_for_cwd(
"[bad](/Users/example/code/codex/docs/%E6%ZZ.md)",
Path::new("/Users/example/code/codex"),
);
let expected = Text::from(Line::from_iter(["docs/%E6%ZZ.md".cyan()]));
assert_eq!(text, expected);
}
#[test]
fn file_link_preserves_percent_encoded_separator_escapes() {
let text = render_markdown_text_for_cwd(
"[encoded](/Users/example/code/codex/docs/%2E%2E%2Fsecret.md)",
Path::new("/Users/example/code/codex"),
);
let expected = Text::from(Line::from_iter(["docs/%2E%2E%2Fsecret.md".cyan()]));
assert_eq!(text, expected);
}
#[test]
fn file_link_preserves_invalid_utf8_percent_sequences() {
let text = render_markdown_text_for_cwd(
"[bad](/Users/example/code/codex/docs/%E2%28.md)",
Path::new("/Users/example/code/codex"),
);
let expected = Text::from(Line::from_iter(["docs/%E2%28.md".cyan()]));
assert_eq!(text, expected);
}
#[test]
fn url_link_shows_destination() {
let text = render_markdown_text("[docs](https://example.com/docs)");