From fbee3bf34f3008fa6da34d335cab443dc2c1965c Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Wed, 8 Apr 2026 21:46:40 -0700 Subject: [PATCH] Hide synthetic timer prompts in chat history --- codex-rs/tui/src/chatwidget.rs | 31 +++++++++++++++++++++++++++-- codex-rs/tui/src/timer_scheduler.rs | 31 +++++++++++++++++++++++++++-- 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 4f92e6ca78..59e76d204c 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -1131,7 +1131,24 @@ fn parse_timer_fired_user_message(message: &str) -> Option Option { + let trimmed = message.trim(); + if !trimmed.starts_with("") + || !trimmed.ends_with("") + { + return None; + } + + let display = extract_xml_tag(trimmed, "display")?; + let display = xml_unescape(display.trim()); + (!display.is_empty()).then_some(display) +} + fn extract_timer_fired_tag(message: &str, tag: &str) -> Option { + extract_xml_tag(message, tag) +} + +fn extract_xml_tag(message: &str, tag: &str) -> Option { let open = format!("<{tag}>"); let close = format!(""); let after_open = message.split_once(&open)?.1; @@ -1139,6 +1156,12 @@ fn extract_timer_fired_tag(message: &str, tag: &str) -> Option { Some(value.to_string()) } +fn xml_unescape(text: &str) -> String { + text.replace("<", "<") + .replace(">", ">") + .replace("&", "&") +} + struct PendingSteer { user_message: UserMessage, compare_key: PendingSteerCompareKey, @@ -5892,6 +5915,8 @@ impl ChatWidget { // Show replayable user content in conversation history. if render_in_history && !text.is_empty() { + let history_text = + parse_synthetic_user_message_display(&text).unwrap_or_else(|| text.clone()); let local_image_paths = local_images .into_iter() .map(|img| img.path) @@ -5904,7 +5929,7 @@ impl ChatWidget { remote_image_urls.clone(), )); self.add_to_history(history_cell::new_user_prompt( - text, + history_text, text_elements, local_image_paths, remote_image_urls, @@ -7257,8 +7282,10 @@ impl ChatWidget { || !event.text_elements.is_empty() || !remote_image_urls.is_empty() { + let message = parse_synthetic_user_message_display(&event.message) + .unwrap_or_else(|| event.message); self.add_to_history(history_cell::new_user_prompt( - event.message, + message, event.text_elements, event.local_images, remote_image_urls, diff --git a/codex-rs/tui/src/timer_scheduler.rs b/codex-rs/tui/src/timer_scheduler.rs index c2f2b31033..48c7769d4b 100644 --- a/codex-rs/tui/src/timer_scheduler.rs +++ b/codex-rs/tui/src/timer_scheduler.rs @@ -1,5 +1,8 @@ pub(crate) fn build_timer_list_prompt() -> String { - "List the thread timers that are currently scheduled. Call the TimerList tool directly, then summarize the pending timers briefly for the user. If there are no pending timers, say that none are scheduled.".to_string() + wrap_synthetic_user_message( + "/loop", + "List the thread timers that are currently scheduled. Call the TimerList tool directly, then summarize the pending timers briefly for the user. If there are no pending timers, say that none are scheduled.", + ) } pub(crate) fn build_loop_timer_prompt(spec: &str) -> Result { @@ -9,7 +12,7 @@ pub(crate) fn build_loop_timer_prompt(spec: &str) -> Result { } let now = chrono::Local::now().format("%Y-%m-%dT%H:%M:%S"); let timezone = chrono::Local::now().offset().to_string(); - Ok(format!( + let prompt = format!( r#"Create a Codex thread timer from this `/loop` request. Call the TimerCreate tool directly; do not only describe the timer. Current local datetime: {now} @@ -30,9 +33,31 @@ Interpretation rules: - For recurring calendar timing, use a schedule trigger with rrule set to an RFC 5545 RRULE string and dtstart set when the user supplies a start datetime; otherwise omit dtstart. - For schedule triggers, use floating local wall-clock datetimes without timezone suffixes. - After TimerCreate succeeds, briefly confirm the schedule and the timer prompt."# + ); + Ok(wrap_synthetic_user_message( + &format!("/loop {spec}"), + &prompt, )) } +fn wrap_synthetic_user_message(display: &str, prompt: &str) -> String { + format!( + r#" +{} + +{prompt} + +"#, + xml_escape(display) + ) +} + +fn xml_escape(text: &str) -> String { + text.replace('&', "&") + .replace('<', "<") + .replace('>', ">") +} + #[cfg(test)] mod tests { use super::*; @@ -45,6 +70,7 @@ mod tests { assert!(prompt.contains("Call the TimerCreate tool directly")); assert!(prompt.contains("every 5 minutes run tests")); assert!(prompt.contains("Current local datetime:")); + assert!(prompt.contains("/loop every 5 minutes run tests")); } #[test] @@ -52,5 +78,6 @@ mod tests { let prompt = build_timer_list_prompt(); assert!(prompt.contains("Call the TimerList tool directly")); + assert!(prompt.contains("/loop")); } }