mirror of
https://github.com/openai/codex.git
synced 2026-04-29 08:56:38 +00:00
873 lines
36 KiB
Markdown
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. |