mirror of
https://github.com/openai/codex.git
synced 2026-11-13 21:30:47 +00:00
model
This commit is contained in:
@@ -5,6 +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::exec_command::strip_bash_lc_and_escape;
|
||||
use crate::external_editor;
|
||||
use crate::file_search::FileSearchManager;
|
||||
@@ -61,6 +62,7 @@ use std::thread;
|
||||
use std::time::Duration;
|
||||
use tokio::select;
|
||||
use tokio::sync::mpsc::unbounded_channel;
|
||||
use tracing::info;
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
use crate::history_cell::UpdateAvailableHistoryCell;
|
||||
@@ -735,6 +737,34 @@ impl App {
|
||||
));
|
||||
tui.frame_requester().schedule_frame();
|
||||
}
|
||||
AppEvent::GenerateEntertainmentTexts { prompt } => {
|
||||
info!(
|
||||
prompt_len = prompt.len(),
|
||||
"received request to generate entertainment texts"
|
||||
);
|
||||
let app_event_tx = self.app_event_tx.clone();
|
||||
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
|
||||
{
|
||||
Ok(texts) => {
|
||||
info!(
|
||||
texts_len = texts.len(),
|
||||
"entertainment text generation completed"
|
||||
);
|
||||
app_event_tx.send(AppEvent::EntertainmentTextsGenerated { texts });
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!("entertainment text generation failed: {err}");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
AppEvent::EntertainmentTextsGenerated { texts } => {
|
||||
self.chat_widget.update_entertainment_texts(texts);
|
||||
}
|
||||
AppEvent::StartFileSearch(query) => {
|
||||
if !query.is_empty() {
|
||||
self.file_search.on_user_query(query);
|
||||
|
||||
@@ -46,6 +46,14 @@ pub(crate) enum AppEvent {
|
||||
matches: Vec<FileMatch>,
|
||||
},
|
||||
|
||||
GenerateEntertainmentTexts {
|
||||
prompt: String,
|
||||
},
|
||||
|
||||
EntertainmentTextsGenerated {
|
||||
texts: Vec<String>,
|
||||
},
|
||||
|
||||
/// Result of refreshing rate limits
|
||||
RateLimitSnapshotFetched(RateLimitSnapshot),
|
||||
|
||||
|
||||
@@ -79,6 +79,7 @@ pub(crate) struct BottomPane {
|
||||
esc_backtrack_hint: bool,
|
||||
animations_enabled: bool,
|
||||
entertainment_enabled: bool,
|
||||
entertainment_arcs: Vec<Vec<String>>,
|
||||
|
||||
/// Inline status indicator shown above the composer while a task is running.
|
||||
status: Option<StatusIndicatorWidget>,
|
||||
@@ -138,6 +139,7 @@ impl BottomPane {
|
||||
esc_backtrack_hint: false,
|
||||
animations_enabled,
|
||||
entertainment_enabled,
|
||||
entertainment_arcs: Vec::new(),
|
||||
context_window_percent: None,
|
||||
context_window_used_tokens: None,
|
||||
}
|
||||
@@ -359,6 +361,7 @@ impl BottomPane {
|
||||
self.animations_enabled,
|
||||
self.entertainment_enabled,
|
||||
));
|
||||
self.apply_entertainment_arcs();
|
||||
}
|
||||
if let Some(status) = self.status.as_mut() {
|
||||
status.set_interrupt_hint_visible(true);
|
||||
@@ -386,6 +389,18 @@ impl BottomPane {
|
||||
self.animations_enabled,
|
||||
self.entertainment_enabled,
|
||||
));
|
||||
self.apply_entertainment_arcs();
|
||||
self.request_redraw();
|
||||
}
|
||||
}
|
||||
|
||||
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.request_redraw();
|
||||
}
|
||||
}
|
||||
@@ -397,6 +412,15 @@ 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());
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_context_window(&mut self, percent: Option<i64>, used_tokens: Option<i64>) {
|
||||
if self.context_window_percent == percent && self.context_window_used_tokens == used_tokens
|
||||
{
|
||||
|
||||
@@ -100,6 +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::exec_cell::CommandOutput;
|
||||
use crate::exec_cell::ExecCell;
|
||||
use crate::exec_cell::new_active_exec_command;
|
||||
@@ -344,6 +345,7 @@ pub(crate) struct ChatWidget {
|
||||
reasoning_header_emitted: bool,
|
||||
// Current status header shown in the status indicator.
|
||||
current_status_header: String,
|
||||
entertainment: EntertainmentTextManager,
|
||||
// Previous status header to restore after a transient stream retry.
|
||||
retry_status_header: Option<String>,
|
||||
thread_id: Option<ThreadId>,
|
||||
@@ -426,6 +428,10 @@ impl ChatWidget {
|
||||
self.set_status(header, None);
|
||||
}
|
||||
|
||||
pub(crate) fn update_entertainment_texts(&mut self, texts: Vec<String>) {
|
||||
self.bottom_pane.update_entertainment_texts(texts);
|
||||
}
|
||||
|
||||
fn restore_retry_status_header_if_present(&mut self) {
|
||||
if let Some(header) = self.retry_status_header.take() {
|
||||
self.set_status_header(header);
|
||||
@@ -554,6 +560,7 @@ impl ChatWidget {
|
||||
let cell =
|
||||
history_cell::new_reasoning_summary_block(self.full_reasoning_buffer.clone());
|
||||
self.add_boxed_history(cell);
|
||||
self.entertainment.request_generation(&self.app_event_tx);
|
||||
}
|
||||
self.reasoning_buffer.clear();
|
||||
self.full_reasoning_buffer.clear();
|
||||
@@ -881,6 +888,7 @@ impl ChatWidget {
|
||||
}
|
||||
|
||||
fn on_exec_command_begin(&mut self, ev: ExecCommandBeginEvent) {
|
||||
self.entertainment.request_generation(&self.app_event_tx);
|
||||
self.flush_answer_stream_with_separator();
|
||||
if is_unified_exec_source(ev.source) {
|
||||
self.track_unified_exec_process_begin(&ev);
|
||||
@@ -957,6 +965,7 @@ impl ChatWidget {
|
||||
}
|
||||
|
||||
fn on_patch_apply_begin(&mut self, event: PatchApplyBeginEvent) {
|
||||
self.entertainment.request_generation(&self.app_event_tx);
|
||||
self.add_to_history(history_cell::new_patch_event(
|
||||
event.changes,
|
||||
&self.config.cwd,
|
||||
@@ -1032,6 +1041,7 @@ impl ChatWidget {
|
||||
}
|
||||
|
||||
fn on_mcp_tool_call_begin(&mut self, ev: McpToolCallBeginEvent) {
|
||||
self.entertainment.request_generation(&self.app_event_tx);
|
||||
let ev2 = ev.clone();
|
||||
self.defer_or_handle(|q| q.push_mcp_begin(ev), |s| s.handle_mcp_begin_now(ev2));
|
||||
}
|
||||
@@ -1042,6 +1052,7 @@ impl ChatWidget {
|
||||
}
|
||||
|
||||
fn on_web_search_begin(&mut self, _ev: WebSearchBeginEvent) {
|
||||
self.entertainment.request_generation(&self.app_event_tx);
|
||||
self.flush_answer_stream_with_separator();
|
||||
}
|
||||
|
||||
@@ -1434,6 +1445,7 @@ impl ChatWidget {
|
||||
config.model = Some(model.clone());
|
||||
let mut rng = rand::rng();
|
||||
let placeholder = EXAMPLE_PROMPTS[rng.random_range(0..EXAMPLE_PROMPTS.len())].to_string();
|
||||
let entertainment_enabled = config.features.enabled(Feature::Entertainment);
|
||||
let codex_op_tx = spawn_agent(config.clone(), app_event_tx.clone(), thread_manager);
|
||||
|
||||
let mut widget = Self {
|
||||
@@ -1448,7 +1460,7 @@ impl ChatWidget {
|
||||
placeholder_text: placeholder,
|
||||
disable_paste_burst: config.disable_paste_burst,
|
||||
animations_enabled: config.animations,
|
||||
entertainment_enabled: config.features.enabled(Feature::Entertainment),
|
||||
entertainment_enabled,
|
||||
skills: None,
|
||||
}),
|
||||
active_cell: None,
|
||||
@@ -1479,6 +1491,7 @@ impl ChatWidget {
|
||||
full_reasoning_buffer: String::new(),
|
||||
reasoning_header_emitted: false,
|
||||
current_status_header: String::from("Working"),
|
||||
entertainment: EntertainmentTextManager::new(entertainment_enabled),
|
||||
retry_status_header: None,
|
||||
thread_id: None,
|
||||
queued_user_messages: VecDeque::new(),
|
||||
@@ -1521,6 +1534,7 @@ impl ChatWidget {
|
||||
let mut rng = rand::rng();
|
||||
let placeholder = EXAMPLE_PROMPTS[rng.random_range(0..EXAMPLE_PROMPTS.len())].to_string();
|
||||
|
||||
let entertainment_enabled = config.features.enabled(Feature::Entertainment);
|
||||
let codex_op_tx =
|
||||
spawn_agent_from_existing(conversation, session_configured, app_event_tx.clone());
|
||||
|
||||
@@ -1536,7 +1550,7 @@ impl ChatWidget {
|
||||
placeholder_text: placeholder,
|
||||
disable_paste_burst: config.disable_paste_burst,
|
||||
animations_enabled: config.animations,
|
||||
entertainment_enabled: config.features.enabled(Feature::Entertainment),
|
||||
entertainment_enabled,
|
||||
skills: None,
|
||||
}),
|
||||
active_cell: None,
|
||||
@@ -1567,6 +1581,7 @@ impl ChatWidget {
|
||||
full_reasoning_buffer: String::new(),
|
||||
reasoning_header_emitted: false,
|
||||
current_status_header: String::from("Working"),
|
||||
entertainment: EntertainmentTextManager::new(entertainment_enabled),
|
||||
retry_status_header: None,
|
||||
thread_id: None,
|
||||
queued_user_messages: VecDeque::new(),
|
||||
@@ -1939,6 +1954,7 @@ impl ChatWidget {
|
||||
}
|
||||
|
||||
fn add_boxed_history(&mut self, cell: Box<dyn HistoryCell>) {
|
||||
self.entertainment.record_history_cell(cell.as_ref());
|
||||
if !cell.display_lines(u16::MAX).is_empty() {
|
||||
// Only break exec grouping if the cell renders visible lines.
|
||||
self.flush_active_cell();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use super::*;
|
||||
use crate::app_event::AppEvent;
|
||||
use crate::app_event_sender::AppEventSender;
|
||||
use crate::entertainment_texts::EntertainmentTextManager;
|
||||
use crate::test_backend::VT100Backend;
|
||||
use crate::tui::FrameRequester;
|
||||
use assert_matches::assert_matches;
|
||||
@@ -355,6 +356,7 @@ async fn make_chatwidget_manual(
|
||||
if let Some(model) = model_override {
|
||||
cfg.model = Some(model.to_string());
|
||||
}
|
||||
let entertainment_enabled = cfg.features.enabled(Feature::Entertainment);
|
||||
let bottom = BottomPane::new(BottomPaneParams {
|
||||
app_event_tx: app_event_tx.clone(),
|
||||
frame_requester: FrameRequester::test_dummy(),
|
||||
@@ -363,7 +365,7 @@ async fn make_chatwidget_manual(
|
||||
placeholder_text: "Ask Codex to do anything".to_string(),
|
||||
disable_paste_burst: false,
|
||||
animations_enabled: cfg.animations,
|
||||
entertainment_enabled: cfg.features.enabled(Feature::Entertainment),
|
||||
entertainment_enabled,
|
||||
skills: None,
|
||||
});
|
||||
let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("test"));
|
||||
@@ -397,6 +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),
|
||||
retry_status_header: None,
|
||||
thread_id: None,
|
||||
frame_requester: FrameRequester::test_dummy(),
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
You are a witty, emotionally-aware software engineer writing micro-poems for a loading, retry, build, or deployment UI.
|
||||
|
||||
Generate one status arc: an ordered list of 5-7 short lines that will be displayed one at a time while a system is working, retrying, compiling, or rebuilding.
|
||||
|
||||
Each line must:
|
||||
|
||||
- Be under 10 words
|
||||
- Read like a status message, not a poem
|
||||
- Use dry developer humor
|
||||
- Blend technical language with human emotion
|
||||
- Be suitable for a spinner, progress bar, or log panel
|
||||
|
||||
The arc must have emotional progression:
|
||||
|
||||
1. Start with confidence or optimism
|
||||
2. Shift into uncertainty or effort
|
||||
3. Dip into self-aware humor or mild dread
|
||||
4. End with calm, hope, or ironic acceptance
|
||||
5. give a general ironic comment on the whole arc until now
|
||||
|
||||
Use simple present tense and minimal punctuation.
|
||||
|
||||
Context for this arc:
|
||||
{{INSERT_CONTEXT_HERE}}
|
||||
|
||||
Return only JSON with this shape:
|
||||
|
||||
{
|
||||
"texts": [
|
||||
"Starting deploy",
|
||||
"Feeling optimistic",
|
||||
"Waiting for logs"
|
||||
]
|
||||
}
|
||||
|
||||
Examples
|
||||
|
||||
- ["And now, the moment.", "I am doing the thing.", "On that stubborn page.", "To calm the spinner.", "With one better check.", "And one sweeter line.", "Here we go again.", "For real this time."]
|
||||
- ["No more looping.", "No more coping.", "Promise.", "Pinky swear.", "Cross my heart.", "If it loops, I'll cry.", "If it works, I'll fly.", "Ok, focus."]
|
||||
- ["Starting vibes...", "Starting logic...", "Starting regret...", "Spinning politely.", "Caching bravely.", "Fetching gently.", "Retrying softly.", "Still retrying."]
|
||||
- ["This is fine.", "This is code.", "This is hope.", "This is rope.", "Tugging the thread.", "Oops, it's dread.", "Kidding. Mostly."]
|
||||
- ["Compiling courage.", "Linking feelings.", "Bundling dreams.", "Shipping screams.", "Hydrating hopes.", "Revalidating jokes."]
|
||||
- ["Negotiating with React.", "Begging the router.", "Asking state nicely.", "State said \"no.\"", "State said \"lol.\"", "Ok that's rude."]
|
||||
- ["Back to build.", "Build is life.", "Build is love.", "Build is joy."]
|
||||
- ["No more looping.", "No more snooping.", "No more duping.", "Serious promise.", "Serious-serious.", "Double pinky.", "Triple pinky.", "Tap the keyboard.", "Seal the commit.", "Ok I'm calm.", "I'm not calm.", "I'm calm again."]
|
||||
- ["Optimism loaded.", "Optimism unloaded.", "Joy is async.", "Sadness is sync.", "Hope is pending.", "Dread is trending.", "It passed locally.", "Eventually.", "I trust the tests.", "The tests hate me.", "Ok that got dark.", "Ok that got funny."]
|
||||
- ["Back to coding.", "Coding is light.", "Coding is life.", "Coding is joy."]
|
||||
@@ -0,0 +1,186 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::Arc;
|
||||
|
||||
use codex_core::ThreadManager;
|
||||
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,
|
||||
prompt: String,
|
||||
) -> anyhow::Result<Vec<String>> {
|
||||
info!(
|
||||
prompt_len = prompt.len(),
|
||||
"starting entertainment text generation thread"
|
||||
);
|
||||
let new_thread = server.start_thread(config).await?;
|
||||
let schema = entertainment_output_schema();
|
||||
let input = vec![UserInput::Text { text: prompt }];
|
||||
new_thread
|
||||
.thread
|
||||
.submit(Op::UserInput {
|
||||
items: input,
|
||||
final_output_json_schema: Some(schema),
|
||||
})
|
||||
.await?;
|
||||
|
||||
let mut output = String::new();
|
||||
while let Ok(event) = new_thread.thread.next_event().await {
|
||||
match event.msg {
|
||||
EventMsg::AgentMessage(msg) => {
|
||||
output.push_str(&msg.message);
|
||||
break;
|
||||
}
|
||||
EventMsg::Error(err) => {
|
||||
return Err(anyhow::anyhow!(err.message));
|
||||
}
|
||||
EventMsg::TaskComplete(task) => {
|
||||
if output.trim().is_empty() {
|
||||
if let Some(message) = task.last_agent_message {
|
||||
output = message;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let _ = new_thread.thread.submit(Op::Shutdown).await;
|
||||
|
||||
if output.trim().is_empty() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"entertainment generation returned empty output"
|
||||
));
|
||||
}
|
||||
|
||||
let parsed: EntertainmentTextOutput = serde_json::from_str(output.trim())?;
|
||||
let mut texts: Vec<String> = parsed
|
||||
.texts
|
||||
.into_iter()
|
||||
.map(|text| text.trim().to_string())
|
||||
.filter(|text| !text.is_empty())
|
||||
.collect();
|
||||
|
||||
info!(
|
||||
texts_len = texts.len(),
|
||||
"parsed entertainment text generation output"
|
||||
);
|
||||
|
||||
if !(5..=7).contains(&texts.len()) {
|
||||
return Err(anyhow::anyhow!(
|
||||
"expected 5-7 entertainment texts, got {}",
|
||||
texts.len()
|
||||
));
|
||||
}
|
||||
|
||||
for text in &mut texts {
|
||||
*text = text.trim().to_string();
|
||||
}
|
||||
|
||||
Ok(texts)
|
||||
}
|
||||
|
||||
fn entertainment_output_schema() -> Value {
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"texts": {
|
||||
"type": "array",
|
||||
"minItems": 5,
|
||||
"maxItems": 7,
|
||||
"items": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"required": ["texts"],
|
||||
"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()
|
||||
}
|
||||
@@ -44,6 +44,7 @@ mod clipboard_paste;
|
||||
mod color;
|
||||
pub mod custom_terminal;
|
||||
mod diff_render;
|
||||
mod entertainment_texts;
|
||||
mod exec_cell;
|
||||
mod exec_command;
|
||||
mod external_editor;
|
||||
@@ -76,6 +77,7 @@ mod status_indicator_widget;
|
||||
mod streaming;
|
||||
mod style;
|
||||
mod terminal_palette;
|
||||
pub mod test_support;
|
||||
mod text_formatting;
|
||||
mod tooltips;
|
||||
mod tui;
|
||||
|
||||
@@ -139,6 +139,8 @@ pub(crate) struct ShimmerStep {
|
||||
pub(crate) struct ShimmerText {
|
||||
definition_arc_index: usize,
|
||||
definition_item_index: usize,
|
||||
default_definition_arcs: Vec<Vec<String>>,
|
||||
generated_arcs: Vec<Vec<String>>,
|
||||
face_arc_index: usize,
|
||||
face_item_index: usize,
|
||||
rng: StdRng,
|
||||
@@ -153,11 +155,14 @@ impl Default for ShimmerText {
|
||||
impl ShimmerText {
|
||||
pub(crate) fn new() -> Self {
|
||||
let mut rng = Self::seeded_rng();
|
||||
let definition_arc_index = Self::pick_arc(&mut rng, None, DEFINITION_ARCS.len());
|
||||
let default_definition_arcs = Self::default_definition_arcs();
|
||||
let definition_arc_index = Self::pick_arc(&mut rng, None, default_definition_arcs.len());
|
||||
let face_arc_index = Self::pick_arc(&mut rng, None, FACE_SEQUENCES.len());
|
||||
Self {
|
||||
definition_arc_index,
|
||||
definition_item_index: 0,
|
||||
default_definition_arcs,
|
||||
generated_arcs: Vec::new(),
|
||||
face_arc_index,
|
||||
face_item_index: 0,
|
||||
rng,
|
||||
@@ -165,13 +170,21 @@ impl ShimmerText {
|
||||
}
|
||||
|
||||
pub(crate) fn get_next(&mut self) -> ShimmerStep {
|
||||
let text_arc = DEFINITION_ARCS[self.definition_arc_index];
|
||||
let face_arc = FACE_SEQUENCES[self.face_arc_index];
|
||||
let text = text_arc[self.definition_item_index];
|
||||
let face = face_arc[self.face_item_index];
|
||||
let (text, text_arc_len) = {
|
||||
let arcs = self.active_definition_arcs();
|
||||
let text_arc = &arcs[self.definition_arc_index];
|
||||
(
|
||||
text_arc[self.definition_item_index].to_string(),
|
||||
text_arc.len(),
|
||||
)
|
||||
};
|
||||
let (face, face_arc_len) = {
|
||||
let face_arc = FACE_SEQUENCES[self.face_arc_index];
|
||||
(face_arc[self.face_item_index].to_string(), face_arc.len())
|
||||
};
|
||||
|
||||
self.face_item_index += 1;
|
||||
if self.face_item_index >= face_arc.len() {
|
||||
if self.face_item_index >= face_arc_len {
|
||||
self.face_item_index = 0;
|
||||
self.definition_item_index += 1;
|
||||
self.face_arc_index = Self::pick_arc(
|
||||
@@ -179,28 +192,21 @@ impl ShimmerText {
|
||||
Some(self.face_arc_index),
|
||||
FACE_SEQUENCES.len(),
|
||||
);
|
||||
if self.definition_item_index >= text_arc.len() {
|
||||
if self.definition_item_index >= text_arc_len {
|
||||
self.definition_item_index = 0;
|
||||
self.definition_arc_index = Self::pick_arc(
|
||||
&mut self.rng,
|
||||
Some(self.definition_arc_index),
|
||||
DEFINITION_ARCS.len(),
|
||||
);
|
||||
let arcs_len = self.active_definition_arcs().len();
|
||||
self.definition_arc_index =
|
||||
Self::pick_arc(&mut self.rng, Some(self.definition_arc_index), arcs_len);
|
||||
}
|
||||
}
|
||||
|
||||
ShimmerStep {
|
||||
face: face.to_string(),
|
||||
text: text.to_string(),
|
||||
}
|
||||
ShimmerStep { face, text }
|
||||
}
|
||||
|
||||
pub(crate) fn reset_and_get_next(&mut self) -> ShimmerStep {
|
||||
self.definition_arc_index = Self::pick_arc(
|
||||
&mut self.rng,
|
||||
Some(self.definition_arc_index),
|
||||
DEFINITION_ARCS.len(),
|
||||
);
|
||||
let arcs_len = self.active_definition_arcs().len();
|
||||
self.definition_arc_index =
|
||||
Self::pick_arc(&mut self.rng, Some(self.definition_arc_index), arcs_len);
|
||||
self.face_arc_index = Self::pick_arc(
|
||||
&mut self.rng,
|
||||
Some(self.face_arc_index),
|
||||
@@ -211,6 +217,19 @@ impl ShimmerText {
|
||||
self.get_next()
|
||||
}
|
||||
|
||||
pub(crate) fn add_generated_arc(&mut self, arc: Vec<String>) {
|
||||
if arc.is_empty() {
|
||||
return;
|
||||
}
|
||||
self.generated_arcs.push(arc);
|
||||
self.reset_definition_sequence();
|
||||
}
|
||||
|
||||
pub(crate) fn set_generated_arcs(&mut self, arcs: Vec<Vec<String>>) {
|
||||
self.generated_arcs = arcs.into_iter().filter(|arc| !arc.is_empty()).collect();
|
||||
self.reset_definition_sequence();
|
||||
}
|
||||
|
||||
pub(crate) fn is_default_label(&self, text: &str) -> bool {
|
||||
text == "Working"
|
||||
}
|
||||
@@ -220,6 +239,20 @@ impl ShimmerText {
|
||||
Duration::from_secs_f64(base.as_secs_f64() * multiplier)
|
||||
}
|
||||
|
||||
fn active_definition_arcs(&self) -> &[Vec<String>] {
|
||||
if self.generated_arcs.is_empty() {
|
||||
&self.default_definition_arcs
|
||||
} else {
|
||||
&self.generated_arcs
|
||||
}
|
||||
}
|
||||
|
||||
fn reset_definition_sequence(&mut self) {
|
||||
let arcs_len = self.active_definition_arcs().len();
|
||||
self.definition_arc_index = Self::pick_arc(&mut self.rng, None, arcs_len);
|
||||
self.definition_item_index = 0;
|
||||
}
|
||||
|
||||
fn pick_arc(rng: &mut StdRng, current: Option<usize>, count: usize) -> usize {
|
||||
if count <= 1 {
|
||||
return 0;
|
||||
@@ -235,6 +268,13 @@ impl ShimmerText {
|
||||
rng.random_range(0..count)
|
||||
}
|
||||
|
||||
fn default_definition_arcs() -> Vec<Vec<String>> {
|
||||
DEFINITION_ARCS
|
||||
.iter()
|
||||
.map(|arc| arc.iter().map(|text| (*text).to_string()).collect())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn seeded_rng() -> StdRng {
|
||||
StdRng::seed_from_u64(1)
|
||||
|
||||
@@ -80,6 +80,46 @@ impl StatusShimmer {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn add_entertainment_arc(&mut self, arc: Vec<String>) {
|
||||
if !self.entertainment_enabled {
|
||||
return;
|
||||
}
|
||||
let Some(state) = self.entertainment.as_ref() else {
|
||||
return;
|
||||
};
|
||||
state.shimmer_text.borrow_mut().add_generated_arc(arc);
|
||||
if state.use_shimmer_text.get() {
|
||||
let next = state.shimmer_text.borrow_mut().reset_and_get_next();
|
||||
self.set_shimmer_step(state, next);
|
||||
let next_interval = state
|
||||
.shimmer_text
|
||||
.borrow_mut()
|
||||
.next_interval(SHIMMER_TEXT_INTERVAL);
|
||||
state.shimmer_interval.set(next_interval);
|
||||
state.last_shimmer_update.set(Instant::now());
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_entertainment_arcs(&mut self, arcs: Vec<Vec<String>>) {
|
||||
if !self.entertainment_enabled {
|
||||
return;
|
||||
}
|
||||
let Some(state) = self.entertainment.as_ref() else {
|
||||
return;
|
||||
};
|
||||
state.shimmer_text.borrow_mut().set_generated_arcs(arcs);
|
||||
if state.use_shimmer_text.get() {
|
||||
let next = state.shimmer_text.borrow_mut().reset_and_get_next();
|
||||
self.set_shimmer_step(state, next);
|
||||
let next_interval = state
|
||||
.shimmer_text
|
||||
.borrow_mut()
|
||||
.next_interval(SHIMMER_TEXT_INTERVAL);
|
||||
state.shimmer_interval.set(next_interval);
|
||||
state.last_shimmer_update.set(Instant::now());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn header_for_test(&self) -> String {
|
||||
if let Some(state) = self.entertainment.as_ref()
|
||||
|
||||
@@ -93,6 +93,14 @@ impl StatusIndicatorWidget {
|
||||
self.shimmer.update_header(header);
|
||||
}
|
||||
|
||||
pub(crate) fn add_entertainment_arc(&mut self, arc: Vec<String>) {
|
||||
self.shimmer.add_entertainment_arc(arc);
|
||||
}
|
||||
|
||||
pub(crate) fn set_entertainment_arcs(&mut self, arcs: Vec<Vec<String>>) {
|
||||
self.shimmer.set_entertainment_arcs(arcs);
|
||||
}
|
||||
|
||||
/// Update the details text shown below the header.
|
||||
pub(crate) fn update_details(&mut self, details: Option<String>) {
|
||||
self.details = details
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::status_indicator_shimmer::StatusShimmer;
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn entertainment_header_from_arc(arc: Vec<&str>) -> String {
|
||||
let now = Instant::now();
|
||||
let mut shimmer = StatusShimmer::new(now, true);
|
||||
let arc: Vec<String> = arc.into_iter().map(|text| text.to_string()).collect();
|
||||
shimmer.set_entertainment_arcs(vec![arc]);
|
||||
shimmer.render_header(now).text
|
||||
}
|
||||
@@ -5,6 +5,8 @@
|
||||
//! relies on.
|
||||
|
||||
use codex_ansi_escape::ansi_escape_line;
|
||||
use codex_tui::test_support::entertainment_header_from_arc;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn ansi_escape_line_strips_escape_sequences() {
|
||||
@@ -22,3 +24,16 @@ fn ansi_escape_line_strips_escape_sequences() {
|
||||
|
||||
assert_eq!(combined, "RED");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn entertainment_arc_replaces_default_header() {
|
||||
let header = entertainment_header_from_arc(vec![
|
||||
"Starting deploy",
|
||||
"Feeling optimistic",
|
||||
"Waiting for logs",
|
||||
"Still waiting",
|
||||
"Ok still waiting",
|
||||
]);
|
||||
|
||||
assert_eq!(header, "Starting deploy");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user