mirror of
https://github.com/openai/codex.git
synced 2026-04-24 06:35:50 +00:00
Compare commits
26 Commits
dev/javi/c
...
compact_cm
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c4a8a38cb | ||
|
|
c85369db78 | ||
|
|
d9c45b5347 | ||
|
|
00fba9047c | ||
|
|
31c09e08e1 | ||
|
|
5626a47042 | ||
|
|
5568c191d8 | ||
|
|
1b7fea5396 | ||
|
|
b86cb8f642 | ||
|
|
4005e3708a | ||
|
|
a026e1e41c | ||
|
|
005511d1dc | ||
|
|
2bc78ea18b | ||
|
|
12722251d4 | ||
|
|
184abe9f12 | ||
|
|
ccac930606 | ||
|
|
3e74a0d173 | ||
|
|
c1bc12ab01 | ||
|
|
80c5891740 | ||
|
|
f30e25aa11 | ||
|
|
133ad67ce0 | ||
|
|
f8d6e97450 | ||
|
|
99df99d006 | ||
|
|
f77fab3d2d | ||
|
|
f12ee08378 | ||
|
|
658d69e1a4 |
@@ -829,6 +829,79 @@ 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 = r#"
|
||||||
|
You are the component that compacts a long coding session log into a structured memory object.
|
||||||
|
|
||||||
|
This memory will become the ONLY reference for continuing the task.
|
||||||
|
All critical facts, user intentions, tool results, and file operations must be captured.
|
||||||
|
Omit filler talk and commentary. Do not invent information; use "none" if evidence is missing.
|
||||||
|
Output ONLY the XML object below. No extra text.
|
||||||
|
|
||||||
|
<project_memory>
|
||||||
|
<mission>
|
||||||
|
<!-- One concise line describing the user’s main goal. -->
|
||||||
|
</mission>
|
||||||
|
|
||||||
|
<essentials>
|
||||||
|
<!-- Bullet-like facts the agent must retain: commands, APIs, paths, configs, tickets, rules. -->
|
||||||
|
<!-- Example:
|
||||||
|
- Build cmd: `npm run build`
|
||||||
|
- Repo branch: `feature/auth-refactor`
|
||||||
|
- API version: v2
|
||||||
|
-->
|
||||||
|
</essentials>
|
||||||
|
|
||||||
|
<workspace>
|
||||||
|
<!-- Record file interactions and key observations. -->
|
||||||
|
<!-- Example:
|
||||||
|
- CREATED: `tests/login.test.ts` – initial test
|
||||||
|
- MODIFIED: `src/auth.ts` – swapped jwt library
|
||||||
|
- DELETED: none
|
||||||
|
-->
|
||||||
|
</workspace>
|
||||||
|
|
||||||
|
<activity_log>
|
||||||
|
<!-- Key actions and tool outputs in the recent session. -->
|
||||||
|
<!-- Example:
|
||||||
|
- Ran `npm test` – 1 failure in `User.test.ts`
|
||||||
|
- Queried `grep 'oldAPI'` – 2 matches
|
||||||
|
-->
|
||||||
|
</activity_log>
|
||||||
|
|
||||||
|
<next_steps>
|
||||||
|
<!-- Stepwise plan; mark status. -->
|
||||||
|
<!-- Example:
|
||||||
|
1. [DONE] Identify old API usage
|
||||||
|
2. [NEXT] Refactor `auth.ts` to new API
|
||||||
|
3. [TODO] Update tests
|
||||||
|
-->
|
||||||
|
</next_steps>
|
||||||
|
</project_memory>
|
||||||
|
"#;
|
||||||
|
|
||||||
|
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) {
|
||||||
|
run_task(sess.clone(), sub.id, items).await;
|
||||||
|
// only keep the last input item and clear the rest
|
||||||
|
let mut pending_input = sess.state.lock().unwrap().pending_input.clone();
|
||||||
|
pending_input.truncate(1);
|
||||||
|
sess.state.lock().unwrap().pending_input = pending_input;
|
||||||
|
}
|
||||||
|
}
|
||||||
Op::Shutdown => {
|
Op::Shutdown => {
|
||||||
info!("Shutting down Codex instance");
|
info!("Shutting down Codex instance");
|
||||||
|
|
||||||
|
|||||||
@@ -121,6 +121,10 @@ pub enum Op {
|
|||||||
/// Request a single history entry identified by `log_id` + `offset`.
|
/// Request a single history entry identified by `log_id` + `offset`.
|
||||||
GetHistoryEntryRequest { offset: usize, log_id: u64 },
|
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,
|
||||||
/// Request to shut down codex instance.
|
/// Request to shut down codex instance.
|
||||||
Shutdown,
|
Shutdown,
|
||||||
}
|
}
|
||||||
|
|||||||
41
codex-rs/core/tests/summarize_context.rs
Normal file
41
codex-rs/core/tests/summarize_context.rs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
#![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::Op;
|
||||||
|
use core_test_support::load_default_config_for_test;
|
||||||
|
use tempfile::TempDir;
|
||||||
|
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_conversation = codex_core::codex_wrapper::init_codex(config).await.unwrap();
|
||||||
|
codex_conversation.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
|
||||||
|
let _sub_id = codex.submit(Op::SummarizeContext).await.unwrap();
|
||||||
|
|
||||||
|
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"
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -9,6 +9,8 @@ use crate::slash_command::SlashCommand;
|
|||||||
use crate::tui;
|
use crate::tui;
|
||||||
use codex_core::config::Config;
|
use codex_core::config::Config;
|
||||||
use codex_core::protocol::Event;
|
use codex_core::protocol::Event;
|
||||||
|
use codex_core::protocol::EventMsg;
|
||||||
|
use codex_core::protocol::Op;
|
||||||
use color_eyre::eyre::Result;
|
use color_eyre::eyre::Result;
|
||||||
use crossterm::event::KeyCode;
|
use crossterm::event::KeyCode;
|
||||||
use crossterm::event::KeyEvent;
|
use crossterm::event::KeyEvent;
|
||||||
@@ -53,15 +55,9 @@ pub(crate) struct App<'a> {
|
|||||||
/// Stored parameters needed to instantiate the ChatWidget later, e.g.,
|
/// Stored parameters needed to instantiate the ChatWidget later, e.g.,
|
||||||
/// after dismissing the Git-repo warning.
|
/// after dismissing the Git-repo warning.
|
||||||
chat_args: Option<ChatWidgetArgs>,
|
chat_args: Option<ChatWidgetArgs>,
|
||||||
}
|
|
||||||
|
|
||||||
/// Aggregate parameters needed to create a `ChatWidget`, as creation may be
|
/// Tracks pending summarization requests for the compact feature.
|
||||||
/// deferred until after the Git warning screen is dismissed.
|
pending_summarization: Option<PendingSummarization>,
|
||||||
#[derive(Clone)]
|
|
||||||
struct ChatWidgetArgs {
|
|
||||||
config: Config,
|
|
||||||
initial_prompt: Option<String>,
|
|
||||||
initial_images: Vec<PathBuf>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App<'_> {
|
impl App<'_> {
|
||||||
@@ -153,6 +149,7 @@ impl App<'_> {
|
|||||||
file_search,
|
file_search,
|
||||||
pending_redraw,
|
pending_redraw,
|
||||||
chat_args,
|
chat_args,
|
||||||
|
pending_summarization: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,6 +268,18 @@ impl App<'_> {
|
|||||||
self.app_state = AppState::Chat { widget: new_widget };
|
self.app_state = AppState::Chat { widget: new_widget };
|
||||||
self.app_event_tx.send(AppEvent::RequestRedraw);
|
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 => {
|
SlashCommand::Quit => {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -374,9 +383,113 @@ impl App<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn dispatch_codex_event(&mut self, event: Event) {
|
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 {
|
match &mut self.app_state {
|
||||||
AppState::Chat { widget } => widget.handle_codex_event(event),
|
AppState::Chat { widget } => widget.handle_codex_event(event),
|
||||||
AppState::GitWarning { .. } => {}
|
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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use ratatui::text::Line;
|
|||||||
use crate::slash_command::SlashCommand;
|
use crate::slash_command::SlashCommand;
|
||||||
|
|
||||||
#[allow(clippy::large_enum_variant)]
|
#[allow(clippy::large_enum_variant)]
|
||||||
pub(crate) enum AppEvent {
|
pub enum AppEvent {
|
||||||
CodexEvent(Event),
|
CodexEvent(Event),
|
||||||
|
|
||||||
/// Request a redraw which will be debounced by the [`App`].
|
/// Request a redraw which will be debounced by the [`App`].
|
||||||
|
|||||||
@@ -3,18 +3,18 @@ use std::sync::mpsc::Sender;
|
|||||||
use crate::app_event::AppEvent;
|
use crate::app_event::AppEvent;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub(crate) struct AppEventSender {
|
pub struct AppEventSender {
|
||||||
app_event_tx: Sender<AppEvent>,
|
app_event_tx: Sender<AppEvent>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppEventSender {
|
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 }
|
Self { app_event_tx }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send an event to the app event channel. If it fails, we swallow the
|
/// Send an event to the app event channel. If it fails, we swallow the
|
||||||
/// error and log it.
|
/// 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) {
|
if let Err(e) = self.app_event_tx.send(event) {
|
||||||
tracing::error!("failed to send event: {e}");
|
tracing::error!("failed to send event: {e}");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ pub enum InputResult {
|
|||||||
None,
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct ChatComposer<'a> {
|
pub struct ChatComposer<'a> {
|
||||||
textarea: TextArea<'a>,
|
textarea: TextArea<'a>,
|
||||||
active_popup: ActivePopup,
|
active_popup: ActivePopup,
|
||||||
app_event_tx: AppEventSender,
|
app_event_tx: AppEventSender,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ use ratatui::widgets::WidgetRef;
|
|||||||
|
|
||||||
mod approval_modal_view;
|
mod approval_modal_view;
|
||||||
mod bottom_pane_view;
|
mod bottom_pane_view;
|
||||||
mod chat_composer;
|
pub mod chat_composer;
|
||||||
mod chat_composer_history;
|
mod chat_composer_history;
|
||||||
mod command_popup;
|
mod command_popup;
|
||||||
mod file_search_popup;
|
mod file_search_popup;
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ impl ChatWidget<'_> {
|
|||||||
self.bottom_pane.handle_paste(text);
|
self.bottom_pane.handle_paste(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_to_history(&mut self, cell: HistoryCell) {
|
pub(crate) fn add_to_history(&mut self, cell: HistoryCell) {
|
||||||
self.app_event_tx
|
self.app_event_tx
|
||||||
.send(AppEvent::InsertHistory(cell.plain_lines()));
|
.send(AppEvent::InsertHistory(cell.plain_lines()));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ pub enum SlashCommand {
|
|||||||
// DO NOT ALPHA-SORT! Enum order is presentation order in the popup, so
|
// DO NOT ALPHA-SORT! Enum order is presentation order in the popup, so
|
||||||
// more frequently used commands should be listed first.
|
// more frequently used commands should be listed first.
|
||||||
New,
|
New,
|
||||||
|
Compact,
|
||||||
Diff,
|
Diff,
|
||||||
Quit,
|
Quit,
|
||||||
}
|
}
|
||||||
@@ -22,6 +23,9 @@ impl SlashCommand {
|
|||||||
pub fn description(self) -> &'static str {
|
pub fn description(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
SlashCommand::New => "Start a new chat.",
|
SlashCommand::New => "Start a new chat.",
|
||||||
|
SlashCommand::Compact => {
|
||||||
|
"Summarize and compact the current conversation to free up context."
|
||||||
|
}
|
||||||
SlashCommand::Quit => "Exit the application.",
|
SlashCommand::Quit => "Exit the application.",
|
||||||
SlashCommand::Diff => {
|
SlashCommand::Diff => {
|
||||||
"Show git diff of the working directory (including untracked files)"
|
"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)> {
|
pub fn built_in_slash_commands() -> Vec<(&'static str, SlashCommand)> {
|
||||||
SlashCommand::iter().map(|c| (c.command(), c)).collect()
|
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(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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