mirror of
https://github.com/openai/codex.git
synced 2026-04-24 14:45:27 +00:00
progress
review review warnings addressing reviews Reset codex-rs/core/ to match origin/summary_op Reset codex-rs/core/ to match origin/main restore
This commit is contained in:
30
codex-rs/Cargo.lock
generated
30
codex-rs/Cargo.lock
generated
@@ -770,6 +770,7 @@ dependencies = [
|
||||
"color-eyre",
|
||||
"crossterm",
|
||||
"image",
|
||||
"insta",
|
||||
"lazy_static",
|
||||
"mcp-types",
|
||||
"path-clean",
|
||||
@@ -854,6 +855,18 @@ dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "console"
|
||||
version = "0.15.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8"
|
||||
dependencies = [
|
||||
"encode_unicode",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.6.0"
|
||||
@@ -1213,6 +1226,12 @@ dependencies = [
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "encode_unicode"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
version = "0.8.35"
|
||||
@@ -2093,6 +2112,17 @@ version = "2.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd"
|
||||
|
||||
[[package]]
|
||||
name = "insta"
|
||||
version = "1.43.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "154934ea70c58054b556dd430b99a98c2a7ff5309ac9891597e339b5c28f4371"
|
||||
dependencies = [
|
||||
"console",
|
||||
"once_cell",
|
||||
"similar",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "instability"
|
||||
version = "0.3.7"
|
||||
|
||||
@@ -782,36 +782,6 @@ async fn submission_loop(
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Op::SummarizeContext => {
|
||||
let sess = match sess.as_ref() {
|
||||
Some(sess) => sess,
|
||||
None => {
|
||||
send_no_session_event(sub.id).await;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Create a summarization request as user input
|
||||
const SUMMARIZATION_PROMPT: &str = concat!(
|
||||
"Please provide a summary of our conversation so far, highlighting key points, ",
|
||||
"decisions made, and any important context that would be useful for future reference. ",
|
||||
"This summary will be used to replace our conversation history with a more concise ",
|
||||
"version so choose what details you will need to continue your work. ",
|
||||
"Provide the summary directly without main title."
|
||||
);
|
||||
|
||||
let summarization_prompt = vec![InputItem::Text {
|
||||
text: SUMMARIZATION_PROMPT.to_string(),
|
||||
}];
|
||||
|
||||
// Attempt to inject input into current task
|
||||
if let Err(items) = sess.inject_input(summarization_prompt) {
|
||||
// No current task, spawn a new one
|
||||
let task = AgentTask::spawn(Arc::clone(sess), sub.id, items);
|
||||
sess.set_task(task);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
debug!("Agent loop exited");
|
||||
|
||||
@@ -134,6 +134,9 @@ pub struct Config {
|
||||
/// When set to `true`, overrides the default heuristic and forces
|
||||
/// `model_supports_reasoning_summaries()` to return `true`.
|
||||
pub model_supports_reasoning_summaries: bool,
|
||||
|
||||
/// Base URL for requests to ChatGPT (as opposed to the OpenAI API).
|
||||
pub chatgpt_base_url: String,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
@@ -315,6 +318,9 @@ pub struct ConfigToml {
|
||||
|
||||
/// Override to force-enable reasoning summaries for the configured model.
|
||||
pub model_supports_reasoning_summaries: Option<bool>,
|
||||
|
||||
/// Base URL for requests to ChatGPT (as opposed to the OpenAI API).
|
||||
pub chatgpt_base_url: Option<String>,
|
||||
}
|
||||
|
||||
impl ConfigToml {
|
||||
@@ -483,6 +489,11 @@ impl Config {
|
||||
model_supports_reasoning_summaries: cfg
|
||||
.model_supports_reasoning_summaries
|
||||
.unwrap_or(false),
|
||||
|
||||
chatgpt_base_url: config_profile
|
||||
.chatgpt_base_url
|
||||
.or(cfg.chatgpt_base_url)
|
||||
.unwrap_or("https://chatgpt.com/backend-api/".to_string()),
|
||||
};
|
||||
Ok(config)
|
||||
}
|
||||
@@ -788,6 +799,7 @@ disable_response_storage = true
|
||||
model_reasoning_effort: ReasoningEffort::High,
|
||||
model_reasoning_summary: ReasoningSummary::Detailed,
|
||||
model_supports_reasoning_summaries: false,
|
||||
chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(),
|
||||
},
|
||||
o3_profile_config
|
||||
);
|
||||
@@ -833,6 +845,7 @@ disable_response_storage = true
|
||||
model_reasoning_effort: ReasoningEffort::default(),
|
||||
model_reasoning_summary: ReasoningSummary::default(),
|
||||
model_supports_reasoning_summaries: false,
|
||||
chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(),
|
||||
};
|
||||
|
||||
assert_eq!(expected_gpt3_profile_config, gpt3_profile_config);
|
||||
@@ -893,6 +906,7 @@ disable_response_storage = true
|
||||
model_reasoning_effort: ReasoningEffort::default(),
|
||||
model_reasoning_summary: ReasoningSummary::default(),
|
||||
model_supports_reasoning_summaries: false,
|
||||
chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(),
|
||||
};
|
||||
|
||||
assert_eq!(expected_zdr_profile_config, zdr_profile_config);
|
||||
|
||||
@@ -16,4 +16,5 @@ pub struct ConfigProfile {
|
||||
pub disable_response_storage: Option<bool>,
|
||||
pub model_reasoning_effort: Option<ReasoningEffort>,
|
||||
pub model_reasoning_summary: Option<ReasoningSummary>,
|
||||
pub chatgpt_base_url: Option<String>,
|
||||
}
|
||||
|
||||
@@ -108,11 +108,6 @@ pub enum Op {
|
||||
|
||||
/// Request a single history entry identified by `log_id` + `offset`.
|
||||
GetHistoryEntryRequest { offset: usize, log_id: u64 },
|
||||
|
||||
/// Request the agent to summarize the current conversation context.
|
||||
/// The agent will use its existing context (either conversation history or previous response id)
|
||||
/// to generate a summary which will be returned as an AgentMessage event.
|
||||
SummarizeContext,
|
||||
}
|
||||
|
||||
/// Determines the conditions under which the user is consulted to approve
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
#![expect(clippy::unwrap_used, clippy::expect_used)]
|
||||
|
||||
//! Tests for the `Op::SummarizeContext` operation added to verify that
|
||||
//! summarization requests are properly handled and injected as user input.
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use codex_core::Codex;
|
||||
use codex_core::protocol::EventMsg;
|
||||
use codex_core::protocol::InputItem;
|
||||
use codex_core::protocol::Op;
|
||||
mod test_support;
|
||||
use tempfile::TempDir;
|
||||
use test_support::load_default_config_for_test;
|
||||
use tokio::sync::Notify;
|
||||
use tokio::time::timeout;
|
||||
|
||||
/// Helper function to set up a codex session and wait for it to be configured
|
||||
async fn setup_configured_codex_session() -> Codex {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
let config = load_default_config_for_test(&codex_home);
|
||||
let (codex, _init_id) = Codex::spawn(config, std::sync::Arc::new(Notify::new()))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Wait for session configured
|
||||
loop {
|
||||
let event = timeout(Duration::from_secs(5), codex.next_event())
|
||||
.await
|
||||
.expect("timeout waiting for session configured")
|
||||
.expect("codex closed");
|
||||
|
||||
if matches!(event.msg, EventMsg::SessionConfigured(_)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
codex
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_summarize_context_spawns_new_agent_task() {
|
||||
// Test the specific behavior: when there's no current task,
|
||||
// SummarizeContext should spawn a new AgentTask with the summarization prompt
|
||||
let codex = setup_configured_codex_session().await;
|
||||
|
||||
// At this point, there should be no current task running
|
||||
// Submit SummarizeContext operation - this should trigger:
|
||||
// if let Err(items) = sess.inject_input(summarization_prompt) {
|
||||
// let task = AgentTask::spawn(Arc::clone(sess), sub.id, items);
|
||||
// sess.set_task(task);
|
||||
// }
|
||||
let _sub_id = codex.submit(Op::SummarizeContext).await.unwrap();
|
||||
|
||||
// Should receive a TaskStarted event indicating a new AgentTask was spawned
|
||||
let event = timeout(Duration::from_secs(5), codex.next_event())
|
||||
.await
|
||||
.expect("timeout waiting for task started event")
|
||||
.expect("codex closed");
|
||||
|
||||
assert!(
|
||||
matches!(event.msg, EventMsg::TaskStarted),
|
||||
"Expected TaskStarted when no current task exists - should spawn new AgentTask"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_summarize_context_injects_into_running_task() {
|
||||
// Test that when a task IS running, SummarizeContext injects into the existing task
|
||||
let codex = setup_configured_codex_session().await;
|
||||
|
||||
// First, start a task by submitting user input
|
||||
let _input_sub_id = codex
|
||||
.submit(Op::UserInput {
|
||||
items: vec![InputItem::Text {
|
||||
text: "Hello, this should start a task".to_string(),
|
||||
}],
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Wait for the task to start
|
||||
let event = timeout(Duration::from_secs(5), codex.next_event())
|
||||
.await
|
||||
.expect("timeout waiting for task started")
|
||||
.expect("codex closed");
|
||||
|
||||
assert!(
|
||||
matches!(event.msg, EventMsg::TaskStarted),
|
||||
"First task should start"
|
||||
);
|
||||
|
||||
// Now submit SummarizeContext while a task is running
|
||||
// This should test the inject_input SUCCESS path (not the spawn new task path)
|
||||
let _summary_sub_id = codex.submit(Op::SummarizeContext).await.unwrap();
|
||||
|
||||
// The summarization prompt should be injected into the existing task
|
||||
// rather than spawning a new one. We shouldn't get another TaskStarted event
|
||||
let result = timeout(Duration::from_millis(500), codex.next_event()).await;
|
||||
|
||||
// If we get an event, it should NOT be TaskStarted (since we're injecting into existing task)
|
||||
if let Ok(Ok(event)) = result {
|
||||
assert!(
|
||||
!matches!(event.msg, EventMsg::TaskStarted),
|
||||
"Should not spawn new task when one is already running - should inject instead"
|
||||
);
|
||||
}
|
||||
// If we timeout, that's expected - no immediate event for successful injection
|
||||
}
|
||||
@@ -62,3 +62,4 @@ uuid = "1"
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "1"
|
||||
insta = "1.36.1"
|
||||
|
||||
@@ -12,6 +12,7 @@ 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;
|
||||
@@ -22,22 +23,6 @@ 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())
|
||||
}
|
||||
}
|
||||
|
||||
/// Top-level application state: which full-screen view is currently active.
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
enum AppState<'a> {
|
||||
@@ -53,21 +38,6 @@ enum AppState<'a> {
|
||||
GitWarning { screen: GitWarningScreen },
|
||||
}
|
||||
|
||||
/// State for tracking a pending summarization request
|
||||
struct PendingSummarization {
|
||||
/// Buffer to collect the summary response
|
||||
summary_buffer: String,
|
||||
}
|
||||
|
||||
/// 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>,
|
||||
}
|
||||
|
||||
pub(crate) struct App<'a> {
|
||||
app_event_tx: AppEventSender,
|
||||
app_event_rx: Receiver<AppEvent>,
|
||||
@@ -82,7 +52,7 @@ pub(crate) struct App<'a> {
|
||||
/// after dismissing the Git-repo warning.
|
||||
chat_args: Option<ChatWidgetArgs>,
|
||||
|
||||
/// Tracks pending summarization requests for the compact feature
|
||||
/// Tracks pending summarization requests for the compact feature.
|
||||
pending_summarization: Option<PendingSummarization>,
|
||||
}
|
||||
|
||||
@@ -251,40 +221,6 @@ 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),
|
||||
..
|
||||
} = event
|
||||
{
|
||||
// Collect the summary response
|
||||
pending.summary_buffer.push_str(&msg.message);
|
||||
pending.summary_buffer.push('\n');
|
||||
} else if let Event {
|
||||
msg: codex_core::protocol::EventMsg::TaskComplete(_),
|
||||
..
|
||||
} = event
|
||||
{
|
||||
// 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);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.dispatch_codex_event(event);
|
||||
}
|
||||
AppEvent::ExitRequest => {
|
||||
@@ -309,6 +245,18 @@ impl<'a> App<'a> {
|
||||
self.app_state = AppState::Chat { widget: new_widget };
|
||||
self.app_event_tx.send(AppEvent::Redraw);
|
||||
}
|
||||
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::ToggleMouseMode => {
|
||||
if let Err(e) = mouse_capture.toggle() {
|
||||
tracing::error!("Failed to toggle mouse mode: {e}");
|
||||
@@ -338,17 +286,6 @@ impl<'a> App<'a> {
|
||||
widget.add_diff_output(text);
|
||||
}
|
||||
}
|
||||
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(),
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
AppEvent::StartFileSearch(query) => {
|
||||
self.file_search.on_user_query(query);
|
||||
@@ -423,104 +360,92 @@ impl<'a> App<'a> {
|
||||
}
|
||||
|
||||
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::Login { .. } | 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::*;
|
||||
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() {
|
||||
|
||||
@@ -5,7 +5,7 @@ use crossterm::event::KeyEvent;
|
||||
use crate::slash_command::SlashCommand;
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub(crate) enum AppEvent {
|
||||
pub enum AppEvent {
|
||||
CodexEvent(Event),
|
||||
|
||||
Redraw,
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ pub enum InputResult {
|
||||
None,
|
||||
}
|
||||
|
||||
pub(crate) struct ChatComposer<'a> {
|
||||
pub struct ChatComposer<'a> {
|
||||
textarea: TextArea<'a>,
|
||||
active_popup: ActivePopup,
|
||||
app_event_tx: AppEventSender,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -21,8 +21,8 @@ use tracing_subscriber::prelude::*;
|
||||
|
||||
mod app;
|
||||
mod app_event;
|
||||
mod app_event_sender;
|
||||
mod bottom_pane;
|
||||
pub mod app_event_sender;
|
||||
pub mod bottom_pane;
|
||||
mod cell_widget;
|
||||
mod chatwidget;
|
||||
mod citation_regex;
|
||||
@@ -38,7 +38,7 @@ mod login_screen;
|
||||
mod markdown;
|
||||
mod mouse_capture;
|
||||
mod scroll_event_helper;
|
||||
mod slash_command;
|
||||
pub mod slash_command;
|
||||
mod status_indicator_widget;
|
||||
mod text_block;
|
||||
mod text_formatting;
|
||||
|
||||
@@ -13,10 +13,10 @@ 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,
|
||||
ToggleMouseMode,
|
||||
Compact,
|
||||
}
|
||||
|
||||
impl SlashCommand {
|
||||
@@ -24,6 +24,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::ToggleMouseMode => {
|
||||
"Toggle mouse mode (enable for scrolling, disable for text selection)"
|
||||
}
|
||||
@@ -31,9 +34,6 @@ impl SlashCommand {
|
||||
SlashCommand::Diff => {
|
||||
"Show git diff of the working directory (including untracked files)"
|
||||
}
|
||||
SlashCommand::Compact => {
|
||||
"Summarize and compact the current conversation to free up context."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,25 +51,55 @@ pub fn built_in_slash_commands() -> Vec<(&'static str, SlashCommand)> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::unwrap_used)]
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::str::FromStr;
|
||||
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_compact_from_string() {
|
||||
let result = SlashCommand::from_str("compact").unwrap();
|
||||
assert_eq!(result, SlashCommand::Compact);
|
||||
}
|
||||
fn test_slash_commands() {
|
||||
let (tx, _rx) = mpsc::channel();
|
||||
let sender = AppEventSender::new(tx);
|
||||
let mut composer = ChatComposer::new(true, sender);
|
||||
|
||||
#[test]
|
||||
fn test_compact_in_built_in_commands() {
|
||||
let built_in = built_in_slash_commands();
|
||||
let compact_entry = built_in.iter().find(|(cmd, _)| *cmd == "compact");
|
||||
let mut terminal = match Terminal::new(TestBackend::new(100, 10)) {
|
||||
Ok(t) => t,
|
||||
Err(e) => panic!("Failed to create terminal: {e}"),
|
||||
};
|
||||
|
||||
assert!(compact_entry.is_some());
|
||||
let (cmd, slash_cmd) = compact_entry.unwrap();
|
||||
assert_eq!(*cmd, "compact");
|
||||
assert_eq!(*slash_cmd, SlashCommand::Compact);
|
||||
// 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(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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╯"
|
||||
@@ -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╯"
|
||||
@@ -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╯"
|
||||
Reference in New Issue
Block a user