36 KiB
PR #1529: tui supporting /compact operation
- URL: https://github.com/openai/codex/pull/1529
- Author: aibrahim-oai
- Created: 2025-07-11 04:01:08 UTC
- Updated: 2025-07-30 23:35:05 UTC
- Changes: +228/-14, Files changed: 9, Commits: 12
Description
- Supporting compact by sending
Opfor getting summary. - Using the summary to start new Chat with the summary as initial prompt.
- Building on this PR
https://github.com/user-attachments/assets/d8e36e41-b0d5-453f-864b-551314669b22
Full Diff
diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs
index 6823a83a50..4adbdc6388 100644
--- a/codex-rs/tui/src/app.rs
+++ b/codex-rs/tui/src/app.rs
@@ -9,6 +9,8 @@ use crate::slash_command::SlashCommand;
use crate::tui;
use codex_core::config::Config;
use codex_core::protocol::Event;
+use codex_core::protocol::EventMsg;
+use codex_core::protocol::Op;
use color_eyre::eyre::Result;
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
@@ -53,15 +55,9 @@ pub(crate) struct App<'a> {
/// Stored parameters needed to instantiate the ChatWidget later, e.g.,
/// after dismissing the Git-repo warning.
chat_args: Option<ChatWidgetArgs>,
-}
-/// Aggregate parameters needed to create a `ChatWidget`, as creation may be
-/// deferred until after the Git warning screen is dismissed.
-#[derive(Clone)]
-struct ChatWidgetArgs {
- config: Config,
- initial_prompt: Option<String>,
- initial_images: Vec<PathBuf>,
+ /// Tracks pending summarization requests for the compact feature.
+ pending_summarization: Option<PendingSummarization>,
}
impl App<'_> {
@@ -153,6 +149,7 @@ impl App<'_> {
file_search,
pending_redraw,
chat_args,
+ pending_summarization: None,
}
}
@@ -271,6 +268,18 @@ impl App<'_> {
self.app_state = AppState::Chat { widget: new_widget };
self.app_event_tx.send(AppEvent::RequestRedraw);
}
+ SlashCommand::Compact => {
+ if let AppState::Chat { widget } = &mut self.app_state {
+ // Submit the summarization request to the current widget
+ widget.submit_op(Op::SummarizeContext);
+
+ // Set up tracking for the summary response
+ self.pending_summarization = Some(PendingSummarization {
+ summary_buffer: String::new(),
+ started_receiving: false,
+ });
+ }
+ }
SlashCommand::Quit => {
break;
}
@@ -374,9 +383,113 @@ impl App<'_> {
}
fn dispatch_codex_event(&mut self, event: Event) {
+ // First check if we're waiting for a summarization response
+ if self.pending_summarization.is_some() {
+ self.handle_summarization_response(event);
+ return;
+ }
+
+ // Otherwise dispatch to the current app state
match &mut self.app_state {
AppState::Chat { widget } => widget.handle_codex_event(event),
AppState::GitWarning { .. } => {}
}
}
+
+ /// Handles responses during a summarization request.
+ fn handle_summarization_response(&mut self, event: Event) {
+ match &event.msg {
+ EventMsg::AgentMessage(msg) => {
+ // Only collect messages once we've started receiving the summarization
+ if let Some(ref mut pending) = self.pending_summarization {
+ // Start collecting once we see a message that looks like a summary
+ if !pending.started_receiving && msg.message.contains("summarize") {
+ pending.started_receiving = true;
+ }
+
+ if pending.started_receiving {
+ pending.summary_buffer.push_str(&msg.message);
+ pending.summary_buffer.push('\n');
+ }
+ }
+ }
+ EventMsg::TaskComplete(_) => {
+ // Task is complete, now create a new widget with the summary
+ if let Some(pending) = self.pending_summarization.take() {
+ let summary = create_compact_summary_prompt(&pending.summary_buffer);
+
+ // Create new widget with summary as initial prompt
+ let new_widget = Box::new(ChatWidget::new(
+ self.config.clone(),
+ self.app_event_tx.clone(),
+ Some(summary),
+ Vec::new(),
+ ));
+ self.app_state = AppState::Chat { widget: new_widget };
+ self.app_event_tx.send(AppEvent::Redraw);
+ }
+ }
+ _ => {}
+ }
+ }
+}
+
+/// State for tracking a pending summarization request.
+struct PendingSummarization {
+ /// Buffer to collect the summary response.
+ summary_buffer: String,
+ /// Whether we've received the first message of the summarization response.
+ started_receiving: bool,
+}
+
+/// Aggregate parameters needed to create a `ChatWidget`, as creation may be
+/// deferred until after the Git warning screen is dismissed.
+#[derive(Clone)]
+struct ChatWidgetArgs {
+ config: Config,
+ initial_prompt: Option<String>,
+ initial_images: Vec<PathBuf>,
+}
+
+/// Creates the initial prompt for a compacted conversation.
+fn create_compact_summary_prompt(summary_text: &str) -> String {
+ if summary_text.trim().is_empty() {
+ "Previous conversation has been summarized.".to_string()
+ } else {
+ format!(
+ r#"This chat is a continuation of a previous conversation. After providing the summary, acknowledge that /compact command has been applied. Here is the summary of the previous conversation:
+
+{}"#,
+ summary_text.trim()
+ )
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ #![allow(clippy::unwrap_used)]
+ use super::*;
+
+ #[test]
+ fn test_summary_buffer_accumulation() {
+ let mut buffer = String::new();
+
+ // Simulate the way we accumulate messages in pending_summarization
+ buffer.push_str("First message part");
+ buffer.push('\n');
+ buffer.push_str("Second message part");
+ buffer.push('\n');
+ buffer.push_str("Final message part");
+
+ let prompt = create_compact_summary_prompt(&buffer);
+
+ // Should contain all parts
+ assert!(prompt.contains("First message part"));
+ assert!(prompt.contains("Second message part"));
+ assert!(prompt.contains("Final message part"));
+
+ // Should preserve newlines in the content
+ let trimmed_buffer = buffer.trim();
+ assert!(prompt.contains(trimmed_buffer));
+ }
}
diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs
index 77a600d304..15dd82584a 100644
--- a/codex-rs/tui/src/app_event.rs
+++ b/codex-rs/tui/src/app_event.rs
@@ -6,7 +6,7 @@ use ratatui::text::Line;
use crate::slash_command::SlashCommand;
#[allow(clippy::large_enum_variant)]
-pub(crate) enum AppEvent {
+pub enum AppEvent {
CodexEvent(Event),
/// Request a redraw which will be debounced by the [`App`].
diff --git a/codex-rs/tui/src/app_event_sender.rs b/codex-rs/tui/src/app_event_sender.rs
index 9d838273ef..9752a3c415 100644
--- a/codex-rs/tui/src/app_event_sender.rs
+++ b/codex-rs/tui/src/app_event_sender.rs
@@ -3,18 +3,18 @@ use std::sync::mpsc::Sender;
use crate::app_event::AppEvent;
#[derive(Clone, Debug)]
-pub(crate) struct AppEventSender {
+pub struct AppEventSender {
app_event_tx: Sender<AppEvent>,
}
impl AppEventSender {
- pub(crate) fn new(app_event_tx: Sender<AppEvent>) -> Self {
+ pub fn new(app_event_tx: Sender<AppEvent>) -> Self {
Self { app_event_tx }
}
/// Send an event to the app event channel. If it fails, we swallow the
/// error and log it.
- pub(crate) fn send(&self, event: AppEvent) {
+ pub fn send(&self, event: AppEvent) {
if let Err(e) = self.app_event_tx.send(event) {
tracing::error!("failed to send event: {e}");
}
diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs
index b15d81f8f5..03d71afe1d 100644
--- a/codex-rs/tui/src/bottom_pane/chat_composer.rs
+++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs
@@ -33,7 +33,7 @@ pub enum InputResult {
None,
}
-pub(crate) struct ChatComposer<'a> {
+pub struct ChatComposer<'a> {
textarea: TextArea<'a>,
active_popup: ActivePopup,
app_event_tx: AppEventSender,
diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs
index 4ec1ba4b3e..fd1a4a2eb4 100644
--- a/codex-rs/tui/src/bottom_pane/mod.rs
+++ b/codex-rs/tui/src/bottom_pane/mod.rs
@@ -14,7 +14,7 @@ use ratatui::widgets::WidgetRef;
mod approval_modal_view;
mod bottom_pane_view;
-mod chat_composer;
+pub mod chat_composer;
mod chat_composer_history;
mod command_popup;
mod file_search_popup;
diff --git a/codex-rs/tui/src/slash_command.rs b/codex-rs/tui/src/slash_command.rs
index 603eb721cd..ab224ae054 100644
--- a/codex-rs/tui/src/slash_command.rs
+++ b/codex-rs/tui/src/slash_command.rs
@@ -13,6 +13,7 @@ pub enum SlashCommand {
// DO NOT ALPHA-SORT! Enum order is presentation order in the popup, so
// more frequently used commands should be listed first.
New,
+ Compact,
Diff,
Quit,
}
@@ -22,6 +23,9 @@ impl SlashCommand {
pub fn description(self) -> &'static str {
match self {
SlashCommand::New => "Start a new chat.",
+ SlashCommand::Compact => {
+ "Summarize and compact the current conversation to free up context."
+ }
SlashCommand::Quit => "Exit the application.",
SlashCommand::Diff => {
"Show git diff of the working directory (including untracked files)"
@@ -40,3 +44,58 @@ impl SlashCommand {
pub fn built_in_slash_commands() -> Vec<(&'static str, SlashCommand)> {
SlashCommand::iter().map(|c| (c.command(), c)).collect()
}
+
+#[cfg(test)]
+mod tests {
+ use crate::app_event_sender::AppEventSender;
+ use crate::bottom_pane::chat_composer::ChatComposer;
+ use crossterm::event::KeyCode;
+ use insta::assert_snapshot;
+ use ratatui::Terminal;
+ use ratatui::backend::TestBackend;
+ use std::sync::mpsc;
+
+ #[test]
+ fn test_slash_commands() {
+ let (tx, _rx) = mpsc::channel();
+ let sender = AppEventSender::new(tx);
+ let mut composer = ChatComposer::new(true, sender);
+
+ let mut terminal = match Terminal::new(TestBackend::new(100, 10)) {
+ Ok(t) => t,
+ Err(e) => panic!("Failed to create terminal: {e}"),
+ };
+
+ // Initial empty state
+ if let Err(e) = terminal.draw(|f| f.render_widget_ref(&composer, f.area())) {
+ panic!("Failed to draw empty composer: {e}");
+ }
+ assert_snapshot!("empty_slash", terminal.backend());
+
+ // Type slash to show commands
+ let _ = composer.handle_key_event(crossterm::event::KeyEvent::new(
+ KeyCode::Char('/'),
+ crossterm::event::KeyModifiers::empty(),
+ ));
+ if let Err(e) = terminal.draw(|f| f.render_widget_ref(&composer, f.area())) {
+ panic!("Failed to draw slash commands: {e}");
+ }
+ assert_snapshot!("slash_commands", terminal.backend());
+
+ // Type 'c' to filter to compact
+ let _ = composer.handle_key_event(crossterm::event::KeyEvent::new(
+ KeyCode::Char('c'),
+ crossterm::event::KeyModifiers::empty(),
+ ));
+ if let Err(e) = terminal.draw(|f| f.render_widget_ref(&composer, f.area())) {
+ panic!("Failed to draw filtered commands: {e}");
+ }
+ assert_snapshot!("compact_filtered", terminal.backend());
+
+ // Select compact command - we don't check the final state since it's handled by the app layer
+ let _ = composer.handle_key_event(crossterm::event::KeyEvent::new(
+ KeyCode::Enter,
+ crossterm::event::KeyModifiers::empty(),
+ ));
+ }
+}
diff --git a/codex-rs/tui/src/snapshots/codex_tui__slash_command__tests__compact_filtered.snap b/codex-rs/tui/src/snapshots/codex_tui__slash_command__tests__compact_filtered.snap
new file mode 100644
index 0000000000..44207a832a
--- /dev/null
+++ b/codex-rs/tui/src/snapshots/codex_tui__slash_command__tests__compact_filtered.snap
@@ -0,0 +1,14 @@
+---
+source: tui/src/slash_command.rs
+expression: terminal.backend()
+---
+"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮"
+"│/compact Summarize and compact the current conversation to free up context. │"
+"╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
+"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮"
+"│/c │"
+"│ │"
+"│ │"
+"│ │"
+"│ │"
+"╰───────────────────────────────────────────────Enter to send | Ctrl+D to quit | Ctrl+J for newline╯"
diff --git a/codex-rs/tui/src/snapshots/codex_tui__slash_command__tests__empty_slash.snap b/codex-rs/tui/src/snapshots/codex_tui__slash_command__tests__empty_slash.snap
new file mode 100644
index 0000000000..68af93743f
--- /dev/null
+++ b/codex-rs/tui/src/snapshots/codex_tui__slash_command__tests__empty_slash.snap
@@ -0,0 +1,14 @@
+---
+source: tui/src/slash_command.rs
+expression: terminal.backend()
+---
+"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮"
+"│ send a message │"
+"│ │"
+"│ │"
+"│ │"
+"│ │"
+"│ │"
+"│ │"
+"│ │"
+"╰───────────────────────────────────────────────Enter to send | Ctrl+D to quit | Ctrl+J for newline╯"
diff --git a/codex-rs/tui/src/snapshots/codex_tui__slash_command__tests__slash_commands.snap b/codex-rs/tui/src/snapshots/codex_tui__slash_command__tests__slash_commands.snap
new file mode 100644
index 0000000000..257ed9fb46
--- /dev/null
+++ b/codex-rs/tui/src/snapshots/codex_tui__slash_command__tests__slash_commands.snap
@@ -0,0 +1,14 @@
+---
+source: tui/src/slash_command.rs
+expression: terminal.backend()
+---
+"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮"
+"│/new Start a new chat. │"
+"│/compact Summarize and compact the current conversation to free up context. │"
+"│/diff Show git diff of the working directory (including untracked files) │"
+"│/quit Exit the application. │"
+"│/toggle-mouse-mode Toggle mouse mode (enable for scrolling, disable for text selection) │"
+"╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
+"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮"
+"│/ │"
+"╰───────────────────────────────────────────────Enter to send | Ctrl+D to quit | Ctrl+J for newline╯"
Review Comments
codex-rs/tui/src/app.rs
- Created: 2025-07-12 17:09:16 UTC | Link: https://github.com/openai/codex/pull/1529#discussion_r2202825742
@@ -21,6 +22,22 @@ use std::path::PathBuf;
use std::sync::mpsc::Receiver;
use std::sync::mpsc::channel;
+/// Template for compact summary continuation prompt
+const COMPACT_SUMMARY_TEMPLATE: &str = concat!(
+ "This chat is a continuation of a previous conversation. ",
+ "After providing the summary, acknowledge that /compact command has been applied. ",
+ "Here is the summary of the previous conversation:\n\n{}"
Using
{}as a placeholder in this way seems very confusing to me as a Rust person because it's not being used natively byformat!(). Please use something likeSUMMARY_TEXTinstead so it's more obvious that something is meant to be replaced.
- Created: 2025-07-12 17:11:01 UTC | Link: https://github.com/openai/codex/pull/1529#discussion_r2202826750
@@ -21,6 +22,22 @@ use std::path::PathBuf;
use std::sync::mpsc::Receiver;
use std::sync::mpsc::channel;
+/// Template for compact summary continuation prompt
+const COMPACT_SUMMARY_TEMPLATE: &str = concat!(
+ "This chat is a continuation of a previous conversation. ",
+ "After providing the summary, acknowledge that /compact command has been applied. ",
+ "Here is the summary of the previous conversation:\n\n{}"
+);
+
+/// Creates the initial prompt for a compacted conversation
+fn create_compact_summary_prompt(summary_text: &str) -> String {
For small helper functions, particularly ones that are private to the file, please declare them after the functions that use them. I strongly prefer declaring the "most important stuff" at the top of the file and "details" (which includes functions like this) at the top of the file.
- Created: 2025-07-12 17:12:21 UTC | Link: https://github.com/openai/codex/pull/1529#discussion_r2202827235
@@ -21,6 +22,22 @@ use std::path::PathBuf;
use std::sync::mpsc::Receiver;
use std::sync::mpsc::channel;
+/// Template for compact summary continuation prompt
+const COMPACT_SUMMARY_TEMPLATE: &str = concat!(
+ "This chat is a continuation of a previous conversation. ",
+ "After providing the summary, acknowledge that /compact command has been applied. ",
+ "Here is the summary of the previous conversation:\n\n{}"
+);
+
+/// Creates the initial prompt for a compacted conversation
+fn create_compact_summary_prompt(summary_text: &str) -> String {
+ if summary_text.trim().is_empty() {
+ "Previous conversation has been summarized.".to_string()
+ } else {
+ COMPACT_SUMMARY_TEMPLATE.replace("{}", summary_text.trim())
Even better, why not just use the string directly here (again, I would recommend
r#) withformat!(). I see that the other placesCOMPACT_SUMMARY_TEMPLATEis used is in tests, but I'm not sure those tests should be written the way they are, so I don't think we'll need this as its own variable.
- Created: 2025-07-12 17:13:46 UTC | Link: https://github.com/openai/codex/pull/1529#discussion_r2202827674
@@ -36,6 +53,21 @@ enum AppState<'a> {
GitWarning { screen: GitWarningScreen },
}
+/// State for tracking a pending summarization request
Again, the previous order was more appropriate:
Appis the most important thing and should be listed before these structs.
- Created: 2025-07-12 17:14:29 UTC | Link: https://github.com/openai/codex/pull/1529#discussion_r2202827940
@@ -49,15 +81,9 @@ pub(crate) struct App<'a> {
/// Stored parameters needed to instantiate the ChatWidget later, e.g.,
/// after dismissing the Git-repo warning.
chat_args: Option<ChatWidgetArgs>,
-}
-/// Aggregate parameters needed to create a `ChatWidget`, as creation may be
-/// deferred until after the Git warning screen is dismissed.
-#[derive(Clone)]
-struct ChatWidgetArgs {
- config: Config,
- initial_prompt: Option<String>,
- initial_images: Vec<PathBuf>,
+ /// Tracks pending summarization requests for the compact feature
Please match the existing style where comments end with periods.
- Created: 2025-07-12 17:16:52 UTC | Link: https://github.com/openai/codex/pull/1529#discussion_r2202828654
@@ -224,6 +251,40 @@ impl<'a> App<'a> {
self.dispatch_scroll_event(scroll_delta);
}
AppEvent::CodexEvent(event) => {
+ // Check if we're waiting for a summarization response
Is there a reason this logic isn't added to
dispatch_codex_event()instead? Much of the reason to have thedispatch_codex_event()helper is to keep the length ofrun()down. In a new top-level function, there will be less indenting and the code should be easier to read, as well.
- Created: 2025-07-12 17:17:31 UTC | Link: https://github.com/openai/codex/pull/1529#discussion_r2202828894
@@ -224,6 +251,40 @@ impl<'a> App<'a> {
self.dispatch_scroll_event(scroll_delta);
}
AppEvent::CodexEvent(event) => {
+ // Check if we're waiting for a summarization response
+ if let Some(ref mut pending) = self.pending_summarization {
+ if let Event {
+ msg: codex_core::protocol::EventMsg::AgentMessage(ref msg),
I would
use codex_core::protocol::EventMsgso this can just beEventMsg.
- Created: 2025-07-12 17:19:34 UTC | Link: https://github.com/openai/codex/pull/1529#discussion_r2202833564
@@ -357,3 +429,119 @@ impl<'a> App<'a> {
}
}
}
+
+#[cfg(test)]
+mod tests {
+ #![allow(clippy::unwrap_used)]
+ use super::*;
+ use codex_core::protocol::AgentMessageEvent;
+ use codex_core::protocol::EventMsg;
+ use codex_core::protocol::TaskCompleteEvent;
+ use pretty_assertions::assert_eq;
+
+ #[test]
+ fn test_create_compact_summary_prompt_with_content() {
+ let summary_text = "User asked about Rust. I explained ownership and borrowing.";
+ let result = create_compact_summary_prompt(summary_text);
+
+ let expected = COMPACT_SUMMARY_TEMPLATE.replace(
+ "{}",
+ "User asked about Rust. I explained ownership and borrowing.",
+ );
+ assert_eq!(result, expected);
+ }
+
+ #[test]
+ fn test_create_compact_summary_prompt_empty_content() {
+ let result = create_compact_summary_prompt("");
+ assert_eq!(result, "Previous conversation has been summarized.");
+
+ let result_whitespace = create_compact_summary_prompt(" \n\t ");
+ assert_eq!(
+ result_whitespace,
+ "Previous conversation has been summarized."
+ );
+ }
+
+ #[test]
+ fn test_pending_summarization_state_management() {
+ let mut pending = PendingSummarization {
+ summary_buffer: String::new(),
+ };
+
+ // Simulate collecting summary pieces
+ pending.summary_buffer.push_str("First part of summary");
+ pending.summary_buffer.push('\n');
+ pending.summary_buffer.push_str("Second part of summary");
+
+ let expected = "First part of summary\nSecond part of summary";
+ assert_eq!(pending.summary_buffer, expected);
+
+ // Test that create_compact_summary_prompt works with collected buffer
+ let prompt = create_compact_summary_prompt(&pending.summary_buffer);
+ assert!(prompt.contains("First part of summary"));
+ assert!(prompt.contains("Second part of summary"));
+ }
+
+ #[test]
I don't think this test provides any signal. Please remove.
- Created: 2025-07-12 17:20:10 UTC | Link: https://github.com/openai/codex/pull/1529#discussion_r2202834695
@@ -357,3 +429,119 @@ impl<'a> App<'a> {
}
}
}
+
+#[cfg(test)]
+mod tests {
+ #![allow(clippy::unwrap_used)]
+ use super::*;
+ use codex_core::protocol::AgentMessageEvent;
+ use codex_core::protocol::EventMsg;
+ use codex_core::protocol::TaskCompleteEvent;
+ use pretty_assertions::assert_eq;
+
+ #[test]
+ fn test_create_compact_summary_prompt_with_content() {
+ let summary_text = "User asked about Rust. I explained ownership and borrowing.";
+ let result = create_compact_summary_prompt(summary_text);
+
+ let expected = COMPACT_SUMMARY_TEMPLATE.replace(
+ "{}",
+ "User asked about Rust. I explained ownership and borrowing.",
+ );
+ assert_eq!(result, expected);
+ }
+
+ #[test]
+ fn test_create_compact_summary_prompt_empty_content() {
+ let result = create_compact_summary_prompt("");
+ assert_eq!(result, "Previous conversation has been summarized.");
+
+ let result_whitespace = create_compact_summary_prompt(" \n\t ");
+ assert_eq!(
+ result_whitespace,
+ "Previous conversation has been summarized."
+ );
+ }
+
+ #[test]
+ fn test_pending_summarization_state_management() {
+ let mut pending = PendingSummarization {
+ summary_buffer: String::new(),
+ };
+
+ // Simulate collecting summary pieces
+ pending.summary_buffer.push_str("First part of summary");
+ pending.summary_buffer.push('\n');
+ pending.summary_buffer.push_str("Second part of summary");
+
+ let expected = "First part of summary\nSecond part of summary";
+ assert_eq!(pending.summary_buffer, expected);
+
+ // Test that create_compact_summary_prompt works with collected buffer
+ let prompt = create_compact_summary_prompt(&pending.summary_buffer);
+ assert!(prompt.contains("First part of summary"));
+ assert!(prompt.contains("Second part of summary"));
+ }
+
+ #[test]
+ fn test_compact_summary_template_integrity() {
+ // Ensure the template has expected structure and placeholder
+ assert!(COMPACT_SUMMARY_TEMPLATE.contains("{}"));
+ assert!(COMPACT_SUMMARY_TEMPLATE.contains("continuation of a previous conversation"));
+ assert!(COMPACT_SUMMARY_TEMPLATE.contains("/compact command has been applied"));
+ }
+
+ #[test]
+ fn test_agent_message_event_creation() {
+ // Test that we can create the events we expect to handle
+ let msg_event = EventMsg::AgentMessage(AgentMessageEvent {
+ message: "This is a test summary".to_string(),
+ });
+
+ if let EventMsg::AgentMessage(agent_msg) = msg_event {
+ assert_eq!(agent_msg.message, "This is a test summary");
+ } else {
+ panic!("Expected AgentMessage event");
+ }
+
+ let task_complete_event = EventMsg::TaskComplete(TaskCompleteEvent {
+ last_agent_message: Some("Final message".to_string()),
+ });
+
+ matches!(task_complete_event, EventMsg::TaskComplete(_));
+ }
+
+ #[test]
+ fn test_multiline_summary_handling() {
+ let multiline_summary = "Line 1: User question\nLine 2: My response\nLine 3: Follow-up";
+ let result = create_compact_summary_prompt(multiline_summary);
+
+ assert!(result.contains("Line 1: User question"));
+ assert!(result.contains("Line 2: My response"));
+ assert!(result.contains("Line 3: Follow-up"));
+ assert!(result.contains("continuation of a previous conversation"));
+ }
+
+ #[test]
+ fn test_summary_buffer_accumulation() {
What is this test telling us?
- Created: 2025-07-12 17:21:20 UTC | Link: https://github.com/openai/codex/pull/1529#discussion_r2202835602
@@ -357,3 +429,119 @@ impl<'a> App<'a> {
}
}
}
+
+#[cfg(test)]
+mod tests {
+ #![allow(clippy::unwrap_used)]
+ use super::*;
+ use codex_core::protocol::AgentMessageEvent;
+ use codex_core::protocol::EventMsg;
+ use codex_core::protocol::TaskCompleteEvent;
+ use pretty_assertions::assert_eq;
+
+ #[test]
+ fn test_create_compact_summary_prompt_with_content() {
+ let summary_text = "User asked about Rust. I explained ownership and borrowing.";
+ let result = create_compact_summary_prompt(summary_text);
+
+ let expected = COMPACT_SUMMARY_TEMPLATE.replace(
+ "{}",
+ "User asked about Rust. I explained ownership and borrowing.",
+ );
+ assert_eq!(result, expected);
+ }
+
+ #[test]
+ fn test_create_compact_summary_prompt_empty_content() {
+ let result = create_compact_summary_prompt("");
+ assert_eq!(result, "Previous conversation has been summarized.");
+
+ let result_whitespace = create_compact_summary_prompt(" \n\t ");
+ assert_eq!(
+ result_whitespace,
+ "Previous conversation has been summarized."
+ );
+ }
+
+ #[test]
+ fn test_pending_summarization_state_management() {
+ let mut pending = PendingSummarization {
+ summary_buffer: String::new(),
+ };
+
+ // Simulate collecting summary pieces
+ pending.summary_buffer.push_str("First part of summary");
+ pending.summary_buffer.push('\n');
+ pending.summary_buffer.push_str("Second part of summary");
+
+ let expected = "First part of summary\nSecond part of summary";
+ assert_eq!(pending.summary_buffer, expected);
+
+ // Test that create_compact_summary_prompt works with collected buffer
+ let prompt = create_compact_summary_prompt(&pending.summary_buffer);
+ assert!(prompt.contains("First part of summary"));
+ assert!(prompt.contains("Second part of summary"));
+ }
+
+ #[test]
+ fn test_compact_summary_template_integrity() {
+ // Ensure the template has expected structure and placeholder
+ assert!(COMPACT_SUMMARY_TEMPLATE.contains("{}"));
+ assert!(COMPACT_SUMMARY_TEMPLATE.contains("continuation of a previous conversation"));
+ assert!(COMPACT_SUMMARY_TEMPLATE.contains("/compact command has been applied"));
+ }
+
+ #[test]
+ fn test_agent_message_event_creation() {
+ // Test that we can create the events we expect to handle
+ let msg_event = EventMsg::AgentMessage(AgentMessageEvent {
+ message: "This is a test summary".to_string(),
+ });
+
+ if let EventMsg::AgentMessage(agent_msg) = msg_event {
+ assert_eq!(agent_msg.message, "This is a test summary");
+ } else {
+ panic!("Expected AgentMessage event");
+ }
+
+ let task_complete_event = EventMsg::TaskComplete(TaskCompleteEvent {
+ last_agent_message: Some("Final message".to_string()),
+ });
+
+ matches!(task_complete_event, EventMsg::TaskComplete(_));
+ }
+
+ #[test]
What is this test telling us?
A test should help reduce risk. If the test is implemented as
format!()as I suggested, then the risk of it failing in any of the ways this verifies seems low.
- Created: 2025-07-12 17:22:08 UTC | Link: https://github.com/openai/codex/pull/1529#discussion_r2202835856
@@ -357,3 +429,119 @@ impl<'a> App<'a> {
}
}
}
+
+#[cfg(test)]
+mod tests {
+ #![allow(clippy::unwrap_used)]
+ use super::*;
+ use codex_core::protocol::AgentMessageEvent;
+ use codex_core::protocol::EventMsg;
+ use codex_core::protocol::TaskCompleteEvent;
+ use pretty_assertions::assert_eq;
+
+ #[test]
+ fn test_create_compact_summary_prompt_with_content() {
+ let summary_text = "User asked about Rust. I explained ownership and borrowing.";
+ let result = create_compact_summary_prompt(summary_text);
+
+ let expected = COMPACT_SUMMARY_TEMPLATE.replace(
I would just
assert_eq!()using a string literal that, yes, is a copy ofCOMPACT_SUMMARY_TEMPLATE. Again, I would user#.
- Created: 2025-07-12 17:27:22 UTC | Link: https://github.com/openai/codex/pull/1529#discussion_r2202837355
@@ -224,6 +251,40 @@ impl<'a> App<'a> {
self.dispatch_scroll_event(scroll_delta);
}
AppEvent::CodexEvent(event) => {
+ // Check if we're waiting for a summarization response
+ if let Some(ref mut pending) = self.pending_summarization {
+ if let Event {
+ msg: codex_core::protocol::EventMsg::AgentMessage(ref msg),
Why is every
AgentMessageused as thePendingSummarizationin this case?
codex-rs/tui/src/slash_command.rs
- Created: 2025-07-12 17:23:01 UTC | Link: https://github.com/openai/codex/pull/1529#discussion_r2202836083
@@ -16,6 +16,7 @@ pub enum SlashCommand {
Diff,
Quit,
ToggleMouseMode,
+ Compact,
As noted on line 13, enum order should be what we think frequency order is. I would list this second (after
New).
- Created: 2025-07-12 17:23:21 UTC | Link: https://github.com/openai/codex/pull/1529#discussion_r2202836138
@@ -30,6 +31,9 @@ impl SlashCommand {
SlashCommand::Diff => {
"Show git diff of the working directory (including untracked files)"
}
+ SlashCommand::Compact => {
Assuming you reorder the enum, please reorder this, as well.
- Created: 2025-07-12 17:25:09 UTC | Link: https://github.com/openai/codex/pull/1529#discussion_r2202836629
@@ -44,3 +48,28 @@ impl SlashCommand {
pub fn built_in_slash_commands() -> Vec<(&'static str, SlashCommand)> {
SlashCommand::iter().map(|c| (c.command(), c)).collect()
}
+
+#[cfg(test)]
Again, I don't think this test derisks very much. It feels like it would only ever fail if we restructured how commands work, so it is unlikely to catch anything.
An integration test that verifies
/compactwould be a better way to verify this.