diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 9b41aeb902..dde05d3bd6 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -5,7 +5,7 @@ use crate::bottom_pane::ApprovalRequest; use crate::chatwidget::ChatWidget; use crate::chatwidget::ExternalEditorState; use crate::diff_render::DiffSummary; -use crate::entertainment_texts; +use crate::entertainment; use crate::exec_command::strip_bash_lc_and_escape; use crate::external_editor; use crate::file_search::FileSearchManager; @@ -746,8 +746,7 @@ impl App { let server = Arc::clone(&self.server); let config = self.config.clone(); tokio::spawn(async move { - match entertainment_texts::generate_entertainment_texts(server, config, prompt) - .await + match entertainment::generate_entertainment_texts(server, config, prompt).await { Ok(texts) => { info!( diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index 7bacf01d43..a300167e7f 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -4,6 +4,7 @@ use std::path::PathBuf; use crate::app_event_sender::AppEventSender; use crate::bottom_pane::queued_user_messages::QueuedUserMessages; use crate::bottom_pane::unified_exec_footer::UnifiedExecFooter; +use crate::entertainment::EntertainmentArcStore; use crate::render::renderable::FlexRenderable; use crate::render::renderable::Renderable; use crate::render::renderable::RenderableItem; @@ -79,7 +80,7 @@ pub(crate) struct BottomPane { esc_backtrack_hint: bool, animations_enabled: bool, entertainment_enabled: bool, - entertainment_arcs: Vec>, + entertainment_arcs: EntertainmentArcStore, /// Inline status indicator shown above the composer while a task is running. status: Option, @@ -139,7 +140,7 @@ impl BottomPane { esc_backtrack_hint: false, animations_enabled, entertainment_enabled, - entertainment_arcs: Vec::new(), + entertainment_arcs: EntertainmentArcStore::new(entertainment_enabled), context_window_percent: None, context_window_used_tokens: None, } @@ -395,12 +396,8 @@ impl BottomPane { } pub(crate) fn update_entertainment_texts(&mut self, texts: Vec) { - if texts.is_empty() { - return; - } - self.entertainment_arcs.push(texts.clone()); - if let Some(status) = self.status.as_mut() { - status.add_entertainment_arc(texts); + self.entertainment_arcs.push(texts, self.status.as_mut()); + if self.status.is_some() { self.request_redraw(); } } @@ -413,11 +410,8 @@ impl BottomPane { } fn apply_entertainment_arcs(&mut self) { - if self.entertainment_arcs.is_empty() { - return; - } if let Some(status) = self.status.as_mut() { - status.set_entertainment_arcs(self.entertainment_arcs.clone()); + self.entertainment_arcs.apply_to(status); } } diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 33995e301e..beba2a96b5 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -100,7 +100,7 @@ use crate::bottom_pane::custom_prompt_view::CustomPromptView; use crate::bottom_pane::popup_consts::standard_popup_hint_line; use crate::clipboard_paste::paste_image_to_temp_png; use crate::diff_render::display_path_for; -use crate::entertainment_texts::EntertainmentTextManager; +use crate::entertainment::EntertainmentController; use crate::exec_cell::CommandOutput; use crate::exec_cell::ExecCell; use crate::exec_cell::new_active_exec_command; @@ -345,7 +345,7 @@ pub(crate) struct ChatWidget { reasoning_header_emitted: bool, // Current status header shown in the status indicator. current_status_header: String, - entertainment: EntertainmentTextManager, + entertainment: EntertainmentController, // Previous status header to restore after a transient stream retry. retry_status_header: Option, thread_id: Option, @@ -1491,7 +1491,7 @@ impl ChatWidget { full_reasoning_buffer: String::new(), reasoning_header_emitted: false, current_status_header: String::from("Working"), - entertainment: EntertainmentTextManager::new(entertainment_enabled), + entertainment: EntertainmentController::new(entertainment_enabled), retry_status_header: None, thread_id: None, queued_user_messages: VecDeque::new(), @@ -1581,7 +1581,7 @@ impl ChatWidget { full_reasoning_buffer: String::new(), reasoning_header_emitted: false, current_status_header: String::from("Working"), - entertainment: EntertainmentTextManager::new(entertainment_enabled), + entertainment: EntertainmentController::new(entertainment_enabled), retry_status_header: None, thread_id: None, queued_user_messages: VecDeque::new(), diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index b130218878..14328441f6 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -1,7 +1,7 @@ use super::*; use crate::app_event::AppEvent; use crate::app_event_sender::AppEventSender; -use crate::entertainment_texts::EntertainmentTextManager; +use crate::entertainment::EntertainmentController; use crate::test_backend::VT100Backend; use crate::tui::FrameRequester; use assert_matches::assert_matches; @@ -399,7 +399,7 @@ async fn make_chatwidget_manual( full_reasoning_buffer: String::new(), reasoning_header_emitted: false, current_status_header: String::from("Working"), - entertainment: EntertainmentTextManager::new(entertainment_enabled), + entertainment: EntertainmentController::new(entertainment_enabled), retry_status_header: None, thread_id: None, frame_requester: FrameRequester::test_dummy(), diff --git a/codex-rs/tui/src/entertainment/arc_store.rs b/codex-rs/tui/src/entertainment/arc_store.rs new file mode 100644 index 0000000000..4083c61e62 --- /dev/null +++ b/codex-rs/tui/src/entertainment/arc_store.rs @@ -0,0 +1,33 @@ +use crate::status_indicator_widget::StatusIndicatorWidget; + +#[derive(Debug)] +pub(crate) struct EntertainmentArcStore { + enabled: bool, + arcs: Vec>, +} + +impl EntertainmentArcStore { + pub(crate) fn new(enabled: bool) -> Self { + Self { + enabled, + arcs: Vec::new(), + } + } + + pub(crate) fn push(&mut self, texts: Vec, status: Option<&mut StatusIndicatorWidget>) { + if !self.enabled || texts.is_empty() { + return; + } + self.arcs.push(texts.clone()); + if let Some(status) = status { + status.add_entertainment_arc(texts); + } + } + + pub(crate) fn apply_to(&self, status: &mut StatusIndicatorWidget) { + if !self.enabled || self.arcs.is_empty() { + return; + } + status.set_entertainment_arcs(self.arcs.clone()); + } +} diff --git a/codex-rs/tui/src/entertainment/controller.rs b/codex-rs/tui/src/entertainment/controller.rs new file mode 100644 index 0000000000..e7536ecd12 --- /dev/null +++ b/codex-rs/tui/src/entertainment/controller.rs @@ -0,0 +1,80 @@ +use std::collections::VecDeque; + +use ratatui::text::Line; +use tracing::info; + +use crate::app_event::AppEvent; +use crate::app_event_sender::AppEventSender; +use crate::history_cell::HistoryCell; + +const PROMPT_TEMPLATE: &str = include_str!("prompt.md"); +const HISTORY_LIMIT: usize = 10; + +#[derive(Debug)] +pub(crate) struct EntertainmentController { + enabled: bool, + history: VecDeque, +} + +impl EntertainmentController { + pub(crate) fn new(enabled: bool) -> Self { + Self { + enabled, + history: VecDeque::new(), + } + } + + pub(crate) fn record_history_cell(&mut self, cell: &dyn HistoryCell) { + let lines = cell.transcript_lines(u16::MAX); + let text = render_lines(&lines).join("\n"); + let text = text.trim(); + if text.is_empty() { + return; + } + self.history.push_back(text.to_string()); + while self.history.len() > HISTORY_LIMIT { + self.history.pop_front(); + } + } + + pub(crate) fn request_generation(&self, app_event_tx: &AppEventSender) { + if !self.enabled { + return; + } + let prompt = self.build_prompt(); + info!( + history_len = self.history.len(), + prompt_len = prompt.len(), + "requesting entertainment text generation" + ); + app_event_tx.send(AppEvent::GenerateEntertainmentTexts { prompt }); + } + + fn build_prompt(&self) -> String { + let history = if self.history.is_empty() { + "- (no recent history)".to_string() + } else { + let mut out = String::new(); + for entry in &self.history { + out.push_str("- "); + out.push_str(entry); + out.push('\n'); + } + out.trim_end().to_string() + }; + + PROMPT_TEMPLATE.replace("{{INSERT_CONTEXT_HERE}}", &history) + } +} + +fn render_lines(lines: &[Line<'static>]) -> Vec { + lines + .iter() + .map(|line| { + line.spans + .iter() + .map(|span| span.content.as_ref()) + .collect::() + }) + .collect() +} diff --git a/codex-rs/tui/src/entertainment_texts.rs b/codex-rs/tui/src/entertainment/generator.rs similarity index 56% rename from codex-rs/tui/src/entertainment_texts.rs rename to codex-rs/tui/src/entertainment/generator.rs index 278e81de39..0694e2c607 100644 --- a/codex-rs/tui/src/entertainment_texts.rs +++ b/codex-rs/tui/src/entertainment/generator.rs @@ -1,4 +1,3 @@ -use std::collections::VecDeque; use std::sync::Arc; use codex_core::ThreadManager; @@ -6,80 +5,15 @@ use codex_core::config::Config; use codex_core::protocol::EventMsg; use codex_core::protocol::Op; use codex_protocol::user_input::UserInput; -use ratatui::text::Line; use serde::Deserialize; use serde_json::Value; use tracing::info; -use crate::app_event::AppEvent; -use crate::app_event_sender::AppEventSender; -use crate::history_cell::HistoryCell; - -const PROMPT_TEMPLATE: &str = include_str!("entertainment_prompt.md"); -const HISTORY_LIMIT: usize = 10; - -#[derive(Debug)] -pub(crate) struct EntertainmentTextManager { - enabled: bool, - history: VecDeque, -} - #[derive(Debug, Deserialize)] struct EntertainmentTextOutput { texts: Vec, } -impl EntertainmentTextManager { - pub(crate) fn new(enabled: bool) -> Self { - Self { - enabled, - history: VecDeque::new(), - } - } - - pub(crate) fn record_history_cell(&mut self, cell: &dyn HistoryCell) { - let lines = cell.transcript_lines(u16::MAX); - let text = render_lines(&lines).join("\n"); - let text = text.trim(); - if text.is_empty() { - return; - } - self.history.push_back(text.to_string()); - while self.history.len() > HISTORY_LIMIT { - self.history.pop_front(); - } - } - - pub(crate) fn request_generation(&self, app_event_tx: &AppEventSender) { - if !self.enabled { - return; - } - let prompt = self.build_prompt(); - info!( - history_len = self.history.len(), - prompt_len = prompt.len(), - "requesting entertainment text generation" - ); - app_event_tx.send(AppEvent::GenerateEntertainmentTexts { prompt }); - } - - fn build_prompt(&self) -> String { - let history = if self.history.is_empty() { - "- (no recent history)".to_string() - } else { - let mut out = String::new(); - for entry in &self.history { - out.push_str("- "); - out.push_str(entry); - out.push('\n'); - } - out.trim_end().to_string() - }; - - PROMPT_TEMPLATE.replace("{{INSERT_CONTEXT_HERE}}", &history) - } -} - pub(crate) async fn generate_entertainment_texts( server: Arc, config: Config, @@ -90,7 +24,7 @@ pub(crate) async fn generate_entertainment_texts( "starting entertainment text generation thread" ); let mut config = config; - config.model = Some("gpt-4.1-nano".to_string()); + config.model = Some("gpt-5-nano".to_string()); let new_thread = server.start_thread(config).await?; let schema = entertainment_output_schema(); let input = vec![UserInput::Text { text: prompt }]; @@ -174,15 +108,3 @@ fn entertainment_output_schema() -> Value { "additionalProperties": false }) } - -fn render_lines(lines: &[Line<'static>]) -> Vec { - lines - .iter() - .map(|line| { - line.spans - .iter() - .map(|span| span.content.as_ref()) - .collect::() - }) - .collect() -} diff --git a/codex-rs/tui/src/entertainment/mod.rs b/codex-rs/tui/src/entertainment/mod.rs new file mode 100644 index 0000000000..46d85960fb --- /dev/null +++ b/codex-rs/tui/src/entertainment/mod.rs @@ -0,0 +1,11 @@ +pub(crate) mod arc_store; +pub(crate) mod controller; +pub(crate) mod generator; +pub(crate) mod shimmer_text; +pub mod test_support; + +pub(crate) use arc_store::EntertainmentArcStore; +pub(crate) use controller::EntertainmentController; +pub(crate) use generator::generate_entertainment_texts; +pub(crate) use shimmer_text::ShimmerStep; +pub(crate) use shimmer_text::ShimmerText; diff --git a/codex-rs/tui/src/entertainment_prompt.md b/codex-rs/tui/src/entertainment/prompt.md similarity index 100% rename from codex-rs/tui/src/entertainment_prompt.md rename to codex-rs/tui/src/entertainment/prompt.md diff --git a/codex-rs/tui/src/shimmer_text.rs b/codex-rs/tui/src/entertainment/shimmer_text.rs similarity index 100% rename from codex-rs/tui/src/shimmer_text.rs rename to codex-rs/tui/src/entertainment/shimmer_text.rs diff --git a/codex-rs/tui/src/test_support.rs b/codex-rs/tui/src/entertainment/test_support.rs similarity index 100% rename from codex-rs/tui/src/test_support.rs rename to codex-rs/tui/src/entertainment/test_support.rs diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index f8c1496e20..8277923d0f 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -44,7 +44,7 @@ mod clipboard_paste; mod color; pub mod custom_terminal; mod diff_render; -mod entertainment_texts; +mod entertainment; mod exec_cell; mod exec_command; mod external_editor; @@ -69,7 +69,6 @@ mod resume_picker; mod selection_list; mod session_log; mod shimmer; -mod shimmer_text; mod slash_command; mod status; mod status_indicator_shimmer; @@ -77,7 +76,7 @@ mod status_indicator_widget; mod streaming; mod style; mod terminal_palette; -pub mod test_support; +pub use entertainment::test_support; mod text_formatting; mod tooltips; mod tui; diff --git a/codex-rs/tui/src/status_indicator_shimmer.rs b/codex-rs/tui/src/status_indicator_shimmer.rs index 18499f0859..9a041f6711 100644 --- a/codex-rs/tui/src/status_indicator_shimmer.rs +++ b/codex-rs/tui/src/status_indicator_shimmer.rs @@ -3,7 +3,7 @@ use std::cell::RefCell; use std::time::Duration; use std::time::Instant; -use crate::shimmer_text::ShimmerText; +use crate::entertainment::ShimmerText; const SHIMMER_TEXT_INTERVAL: Duration = Duration::from_secs(1); @@ -162,7 +162,11 @@ impl StatusShimmer { } } - fn set_shimmer_step(&self, state: &EntertainmentState, step: crate::shimmer_text::ShimmerStep) { + fn set_shimmer_step( + &self, + state: &EntertainmentState, + step: crate::entertainment::ShimmerStep, + ) { *state.shimmer_face_cache.borrow_mut() = step.face; *state.shimmer_text_cache.borrow_mut() = step.text; }