This commit is contained in:
Ahmed Ibrahim
2026-01-08 12:34:40 -08:00
parent 7b3263e9a6
commit ed7723607d
13 changed files with 147 additions and 105 deletions

View File

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

View File

@@ -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<Vec<String>>,
entertainment_arcs: EntertainmentArcStore,
/// Inline status indicator shown above the composer while a task is running.
status: Option<StatusIndicatorWidget>,
@@ -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<String>) {
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);
}
}

View File

@@ -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<String>,
thread_id: Option<ThreadId>,
@@ -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(),

View File

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

View File

@@ -0,0 +1,33 @@
use crate::status_indicator_widget::StatusIndicatorWidget;
#[derive(Debug)]
pub(crate) struct EntertainmentArcStore {
enabled: bool,
arcs: Vec<Vec<String>>,
}
impl EntertainmentArcStore {
pub(crate) fn new(enabled: bool) -> Self {
Self {
enabled,
arcs: Vec::new(),
}
}
pub(crate) fn push(&mut self, texts: Vec<String>, 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());
}
}

View File

@@ -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<String>,
}
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<String> {
lines
.iter()
.map(|line| {
line.spans
.iter()
.map(|span| span.content.as_ref())
.collect::<String>()
})
.collect()
}

View File

@@ -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<String>,
}
#[derive(Debug, Deserialize)]
struct EntertainmentTextOutput {
texts: Vec<String>,
}
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<ThreadManager>,
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<String> {
lines
.iter()
.map(|line| {
line.spans
.iter()
.map(|span| span.content.as_ref())
.collect::<String>()
})
.collect()
}

View File

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

View File

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

View File

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