Files
codex/prs/bolinfest/PR-1529.md
2025-09-02 15:17:45 -07:00

873 lines
36 KiB
Markdown

# 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 `Op` for getting summary.
- Using the summary to start new Chat with the summary as initial prompt.
- Building on this [PR](https://github.com/openai/codex/pull/1527)
https://github.com/user-attachments/assets/d8e36e41-b0d5-453f-864b-551314669b22
## Full Diff
```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
```diff
@@ -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 by `format!()`. Please use something like `SUMMARY_TEXT` instead 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
```diff
@@ -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
```diff
@@ -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#`) with `format!()`. I see that the other places `COMPACT_SUMMARY_TEMPLATE` is 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
```diff
@@ -36,6 +53,21 @@ enum AppState<'a> {
GitWarning { screen: GitWarningScreen },
}
+/// State for tracking a pending summarization request
```
> Again, the previous order was more appropriate: `App` is 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
```diff
@@ -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
```diff
@@ -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 the `dispatch_codex_event()` helper is to keep the length of `run()` 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
```diff
@@ -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::EventMsg` so this can just be `EventMsg`.
- Created: 2025-07-12 17:19:34 UTC | Link: https://github.com/openai/codex/pull/1529#discussion_r2202833564
```diff
@@ -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
```diff
@@ -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
```diff
@@ -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
```diff
@@ -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 of `COMPACT_SUMMARY_TEMPLATE`. Again, I would use `r#`.
- Created: 2025-07-12 17:27:22 UTC | Link: https://github.com/openai/codex/pull/1529#discussion_r2202837355
```diff
@@ -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 `AgentMessage` used as the `PendingSummarization` in 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
```diff
@@ -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
```diff
@@ -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
```diff
@@ -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 `/compact` would be a better way to verify this.