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:
Ahmed Ibrahim
2025-07-11 16:19:55 -07:00
parent c1bc12ab01
commit 3e74a0d173
17 changed files with 236 additions and 337 deletions

30
codex-rs/Cargo.lock generated
View File

@@ -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"

View File

@@ -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");

View File

@@ -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);

View File

@@ -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>,
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -62,3 +62,4 @@ uuid = "1"
[dev-dependencies]
pretty_assertions = "1"
insta = "1.36.1"

View File

@@ -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() {

View File

@@ -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,

View File

@@ -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}");
}

View File

@@ -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,

View File

@@ -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;

View File

@@ -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;

View File

@@ -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(),
));
}
}

View File

@@ -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╯"

View File

@@ -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╯"

View File

@@ -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╯"