mirror of
https://github.com/openai/codex.git
synced 2026-02-01 22:47:52 +00:00
Add /compact command to Rust CLI
This commit is contained in:
@@ -59,6 +59,8 @@ tui-markdown = "0.3.3"
|
||||
tui-textarea = "0.7.0"
|
||||
unicode-segmentation = "1.12.0"
|
||||
uuid = "1"
|
||||
reqwest = { version = "0.12", features = ["json"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "1"
|
||||
|
||||
@@ -277,6 +277,11 @@ impl<'a> App<'a> {
|
||||
widget.add_diff_output(text);
|
||||
}
|
||||
}
|
||||
SlashCommand::Compact => {
|
||||
if let AppState::Chat { widget } = &mut self.app_state {
|
||||
widget.compact().await;
|
||||
}
|
||||
}
|
||||
},
|
||||
AppEvent::StartFileSearch(query) => {
|
||||
self.file_search.on_user_query(query);
|
||||
|
||||
@@ -39,6 +39,7 @@ use crate::conversation_history_widget::ConversationHistoryWidget;
|
||||
use crate::history_cell::PatchEventType;
|
||||
use crate::user_approval_widget::ApprovalRequest;
|
||||
use codex_file_search::FileMatch;
|
||||
use crate::compact::{generate_compact_summary, Role, TranscriptEntry};
|
||||
|
||||
pub(crate) struct ChatWidget<'a> {
|
||||
app_event_tx: AppEventSender,
|
||||
@@ -49,6 +50,7 @@ pub(crate) struct ChatWidget<'a> {
|
||||
config: Config,
|
||||
initial_user_message: Option<UserMessage>,
|
||||
token_usage: TokenUsage,
|
||||
transcript: Vec<TranscriptEntry>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Eq, PartialEq)]
|
||||
@@ -135,6 +137,7 @@ impl ChatWidget<'_> {
|
||||
initial_images,
|
||||
),
|
||||
token_usage: TokenUsage::default(),
|
||||
transcript: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,6 +211,7 @@ impl ChatWidget<'_> {
|
||||
// Only show text portion in conversation history for now.
|
||||
if !text.is_empty() {
|
||||
self.conversation_history.add_user_message(text);
|
||||
self.transcript.push(TranscriptEntry { role: Role::User, text });
|
||||
}
|
||||
self.conversation_history.scroll_to_bottom();
|
||||
}
|
||||
@@ -236,6 +240,7 @@ impl ChatWidget<'_> {
|
||||
EventMsg::AgentMessage(AgentMessageEvent { message }) => {
|
||||
self.conversation_history
|
||||
.add_agent_message(&self.config, message);
|
||||
self.transcript.push(TranscriptEntry { role: Role::Assistant, text: message });
|
||||
self.request_redraw();
|
||||
}
|
||||
EventMsg::AgentReasoning(AgentReasoningEvent { text }) => {
|
||||
@@ -410,6 +415,19 @@ impl ChatWidget<'_> {
|
||||
self.bottom_pane.on_file_search_result(query, matches);
|
||||
}
|
||||
|
||||
pub(crate) async fn compact(&mut self) {
|
||||
let Ok(summary) = generate_compact_summary(&self.transcript, &self.config.model, &self.config).await else {
|
||||
self.conversation_history.add_error("Failed to compact context".to_string());
|
||||
self.request_redraw();
|
||||
return;
|
||||
};
|
||||
|
||||
self.conversation_history = ConversationHistoryWidget::new();
|
||||
self.conversation_history.add_agent_message(&self.config, summary.clone());
|
||||
self.transcript = vec![TranscriptEntry { role: Role::Assistant, text: summary }];
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
/// Handle Ctrl-C key press.
|
||||
/// Returns true if the key press was handled, false if it was not.
|
||||
/// If the key press was not handled, the caller should handle it (likely by exiting the process).
|
||||
|
||||
90
codex-rs/tui/src/compact.rs
Normal file
90
codex-rs/tui/src/compact.rs
Normal file
@@ -0,0 +1,90 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use serde::Serialize;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::openai_api_key::get_openai_api_key;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum Role {
|
||||
User,
|
||||
Assistant,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TranscriptEntry {
|
||||
pub role: Role,
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
impl TranscriptEntry {
|
||||
fn role_str(&self) -> &'static str {
|
||||
match self.role {
|
||||
Role::User => "user",
|
||||
Role::Assistant => "assistant",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Message<'a> {
|
||||
role: &'a str,
|
||||
content: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Payload<'a> {
|
||||
model: &'a str,
|
||||
messages: Vec<Message<'a>>,
|
||||
}
|
||||
|
||||
/// Generate a concise summary of the provided transcript using the OpenAI chat
|
||||
/// completions API.
|
||||
pub async fn generate_compact_summary(
|
||||
transcript: &[TranscriptEntry],
|
||||
model: &str,
|
||||
config: &Config,
|
||||
) -> Result<String> {
|
||||
let conversation_text = transcript
|
||||
.iter()
|
||||
.map(|e| format!("{}: {}", e.role_str(), e.text))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
|
||||
let messages = vec![
|
||||
Message {
|
||||
role: "assistant",
|
||||
content: "You are an expert coding assistant. Your goal is to generate a concise, structured summary of the conversation below that captures all essential information needed to continue development after context replacement. Include tasks performed, code areas modified or reviewed, key decisions or assumptions, test results or errors, and outstanding tasks or next steps.".to_string(),
|
||||
},
|
||||
Message {
|
||||
role: "user",
|
||||
content: format!(
|
||||
"Here is the conversation so far:\n{conversation_text}\n\nPlease summarize this conversation, covering:\n1. Tasks performed and outcomes\n2. Code files, modules, or functions modified or examined\n3. Important decisions or assumptions made\n4. Errors encountered and test or build results\n5. Remaining tasks, open questions, or next steps\nProvide the summary in a clear, concise format."
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
let api_key = get_openai_api_key().ok_or_else(|| anyhow!("OpenAI API key not set"))?;
|
||||
let client = reqwest::Client::new();
|
||||
let base = config.model_provider.base_url.trim_end_matches('/');
|
||||
let url = format!("{}/chat/completions", base);
|
||||
|
||||
let payload = Payload { model, messages };
|
||||
let res = client
|
||||
.post(url)
|
||||
.bearer_auth(api_key)
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let body: serde_json::Value = res.json().await?;
|
||||
if let Some(summary) = body
|
||||
.get("choices")
|
||||
.and_then(|c| c.get(0))
|
||||
.and_then(|c| c.get("message"))
|
||||
.and_then(|m| m.get("content"))
|
||||
.and_then(|v| v.as_str())
|
||||
{
|
||||
Ok(summary.to_string())
|
||||
} else {
|
||||
Ok("Unable to generate summary.".to_string())
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ mod bottom_pane;
|
||||
mod cell_widget;
|
||||
mod chatwidget;
|
||||
mod citation_regex;
|
||||
mod compact;
|
||||
mod cli;
|
||||
mod conversation_history_widget;
|
||||
mod exec_command;
|
||||
|
||||
@@ -14,6 +14,7 @@ pub enum SlashCommand {
|
||||
// more frequently used commands should be listed first.
|
||||
New,
|
||||
Diff,
|
||||
Compact,
|
||||
Quit,
|
||||
ToggleMouseMode,
|
||||
}
|
||||
@@ -30,6 +31,7 @@ impl SlashCommand {
|
||||
SlashCommand::Diff => {
|
||||
"Show git diff of the working directory (including untracked files)"
|
||||
}
|
||||
SlashCommand::Compact => "Condense context into a summary.",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user