mirror of
https://github.com/openai/codex.git
synced 2026-11-02 19:43:24 +00:00
shimmer
This commit is contained in:
@@ -87,6 +87,8 @@ pub enum Feature {
|
||||
ShellSnapshot,
|
||||
/// Experimental TUI v2 (viewport) implementation.
|
||||
Tui2,
|
||||
/// Experimental entertainment mode for the status shimmer.
|
||||
Entertainment,
|
||||
/// Enforce UTF8 output in Powershell.
|
||||
PowershellUtf8,
|
||||
}
|
||||
@@ -380,4 +382,10 @@ pub const FEATURES: &[FeatureSpec] = &[
|
||||
stage: Stage::Experimental,
|
||||
default_enabled: false,
|
||||
},
|
||||
FeatureSpec {
|
||||
id: Feature::Entertainment,
|
||||
key: "entertainment",
|
||||
stage: Stage::Experimental,
|
||||
default_enabled: false,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -17,8 +17,6 @@ path = "src/lib.rs"
|
||||
vt100-tests = []
|
||||
# Gate verbose debug logging inside the TUI implementation.
|
||||
debug-logs = []
|
||||
# Enable entertainment-only shimmer behavior in the status indicator.
|
||||
entertainment = []
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -78,6 +78,7 @@ pub(crate) struct BottomPane {
|
||||
ctrl_c_quit_hint: bool,
|
||||
esc_backtrack_hint: bool,
|
||||
animations_enabled: bool,
|
||||
entertainment_enabled: bool,
|
||||
|
||||
/// Inline status indicator shown above the composer while a task is running.
|
||||
status: Option<StatusIndicatorWidget>,
|
||||
@@ -97,6 +98,7 @@ pub(crate) struct BottomPaneParams {
|
||||
pub(crate) placeholder_text: String,
|
||||
pub(crate) disable_paste_burst: bool,
|
||||
pub(crate) animations_enabled: bool,
|
||||
pub(crate) entertainment_enabled: bool,
|
||||
pub(crate) skills: Option<Vec<SkillMetadata>>,
|
||||
}
|
||||
|
||||
@@ -110,6 +112,7 @@ impl BottomPane {
|
||||
placeholder_text,
|
||||
disable_paste_burst,
|
||||
animations_enabled,
|
||||
entertainment_enabled,
|
||||
skills,
|
||||
} = params;
|
||||
let mut composer = ChatComposer::new(
|
||||
@@ -134,6 +137,7 @@ impl BottomPane {
|
||||
queued_user_messages: QueuedUserMessages::new(),
|
||||
esc_backtrack_hint: false,
|
||||
animations_enabled,
|
||||
entertainment_enabled,
|
||||
context_window_percent: None,
|
||||
context_window_used_tokens: None,
|
||||
}
|
||||
@@ -353,6 +357,7 @@ impl BottomPane {
|
||||
self.app_event_tx.clone(),
|
||||
self.frame_requester.clone(),
|
||||
self.animations_enabled,
|
||||
self.entertainment_enabled,
|
||||
));
|
||||
}
|
||||
if let Some(status) = self.status.as_mut() {
|
||||
@@ -379,6 +384,7 @@ impl BottomPane {
|
||||
self.app_event_tx.clone(),
|
||||
self.frame_requester.clone(),
|
||||
self.animations_enabled,
|
||||
self.entertainment_enabled,
|
||||
));
|
||||
self.request_redraw();
|
||||
}
|
||||
@@ -636,6 +642,7 @@ mod tests {
|
||||
placeholder_text: "Ask Codex to do anything".to_string(),
|
||||
disable_paste_burst: false,
|
||||
animations_enabled: true,
|
||||
entertainment_enabled: false,
|
||||
skills: Some(Vec::new()),
|
||||
});
|
||||
pane.push_approval_request(exec_request(), &features);
|
||||
@@ -659,6 +666,7 @@ mod tests {
|
||||
placeholder_text: "Ask Codex to do anything".to_string(),
|
||||
disable_paste_burst: false,
|
||||
animations_enabled: true,
|
||||
entertainment_enabled: false,
|
||||
skills: Some(Vec::new()),
|
||||
});
|
||||
|
||||
@@ -693,6 +701,7 @@ mod tests {
|
||||
placeholder_text: "Ask Codex to do anything".to_string(),
|
||||
disable_paste_burst: false,
|
||||
animations_enabled: true,
|
||||
entertainment_enabled: false,
|
||||
skills: Some(Vec::new()),
|
||||
});
|
||||
|
||||
@@ -760,6 +769,7 @@ mod tests {
|
||||
placeholder_text: "Ask Codex to do anything".to_string(),
|
||||
disable_paste_burst: false,
|
||||
animations_enabled: true,
|
||||
entertainment_enabled: false,
|
||||
skills: Some(Vec::new()),
|
||||
});
|
||||
|
||||
@@ -787,6 +797,7 @@ mod tests {
|
||||
placeholder_text: "Ask Codex to do anything".to_string(),
|
||||
disable_paste_burst: false,
|
||||
animations_enabled: true,
|
||||
entertainment_enabled: false,
|
||||
skills: Some(Vec::new()),
|
||||
});
|
||||
|
||||
@@ -818,6 +829,7 @@ mod tests {
|
||||
placeholder_text: "Ask Codex to do anything".to_string(),
|
||||
disable_paste_burst: false,
|
||||
animations_enabled: true,
|
||||
entertainment_enabled: false,
|
||||
skills: Some(Vec::new()),
|
||||
});
|
||||
|
||||
@@ -846,6 +858,7 @@ mod tests {
|
||||
placeholder_text: "Ask Codex to do anything".to_string(),
|
||||
disable_paste_burst: false,
|
||||
animations_enabled: true,
|
||||
entertainment_enabled: false,
|
||||
skills: Some(Vec::new()),
|
||||
});
|
||||
|
||||
|
||||
@@ -1448,6 +1448,7 @@ impl ChatWidget {
|
||||
placeholder_text: placeholder,
|
||||
disable_paste_burst: config.disable_paste_burst,
|
||||
animations_enabled: config.animations,
|
||||
entertainment_enabled: config.features.enabled(Feature::Entertainment),
|
||||
skills: None,
|
||||
}),
|
||||
active_cell: None,
|
||||
@@ -1535,6 +1536,7 @@ impl ChatWidget {
|
||||
placeholder_text: placeholder,
|
||||
disable_paste_burst: config.disable_paste_burst,
|
||||
animations_enabled: config.animations,
|
||||
entertainment_enabled: config.features.enabled(Feature::Entertainment),
|
||||
skills: None,
|
||||
}),
|
||||
active_cell: None,
|
||||
|
||||
@@ -363,6 +363,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),
|
||||
skills: None,
|
||||
});
|
||||
let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("test"));
|
||||
|
||||
@@ -68,7 +68,6 @@ mod resume_picker;
|
||||
mod selection_list;
|
||||
mod session_log;
|
||||
mod shimmer;
|
||||
#[cfg(feature = "entertainment")]
|
||||
mod shimmer_text;
|
||||
mod slash_command;
|
||||
mod status;
|
||||
|
||||
@@ -216,7 +216,7 @@ impl ShimmerText {
|
||||
}
|
||||
|
||||
pub(crate) fn next_interval(&mut self, base: Duration) -> Duration {
|
||||
let multiplier = self.rng.random_range(0.5..=1.25);
|
||||
let multiplier = self.rng.random_range(0.4..=1.0);
|
||||
Duration::from_secs_f64(base.as_secs_f64() * multiplier)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
#[cfg(feature = "entertainment")]
|
||||
use std::cell::Cell;
|
||||
use std::cell::RefCell;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
#[cfg(feature = "entertainment")]
|
||||
use std::cell::Cell;
|
||||
#[cfg(feature = "entertainment")]
|
||||
use std::cell::RefCell;
|
||||
|
||||
#[cfg(feature = "entertainment")]
|
||||
use crate::shimmer_text::ShimmerText;
|
||||
|
||||
#[cfg(feature = "entertainment")]
|
||||
const SHIMMER_TEXT_INTERVAL: Duration = Duration::from_secs(1);
|
||||
|
||||
pub(crate) struct RenderHeader {
|
||||
@@ -18,10 +12,14 @@ pub(crate) struct RenderHeader {
|
||||
pub(crate) text: String,
|
||||
}
|
||||
|
||||
#[cfg(feature = "entertainment")]
|
||||
pub(crate) struct StatusShimmer {
|
||||
entertainment_enabled: bool,
|
||||
header: String,
|
||||
use_shimmer_text: bool,
|
||||
entertainment: Option<EntertainmentState>,
|
||||
}
|
||||
|
||||
struct EntertainmentState {
|
||||
use_shimmer_text: Cell<bool>,
|
||||
shimmer_text: RefCell<ShimmerText>,
|
||||
shimmer_face_cache: RefCell<String>,
|
||||
shimmer_text_cache: RefCell<String>,
|
||||
@@ -29,111 +27,103 @@ pub(crate) struct StatusShimmer {
|
||||
shimmer_interval: Cell<Duration>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "entertainment")]
|
||||
impl StatusShimmer {
|
||||
pub(crate) fn new(now: Instant) -> Self {
|
||||
let mut shimmer_text = ShimmerText::new();
|
||||
let shimmer_step = shimmer_text.get_next();
|
||||
let shimmer_interval = shimmer_text.next_interval(SHIMMER_TEXT_INTERVAL);
|
||||
Self {
|
||||
header: shimmer_step.text.clone(),
|
||||
use_shimmer_text: true,
|
||||
shimmer_text: RefCell::new(shimmer_text),
|
||||
shimmer_face_cache: RefCell::new(shimmer_step.face),
|
||||
shimmer_text_cache: RefCell::new(shimmer_step.text),
|
||||
last_shimmer_update: Cell::new(now),
|
||||
shimmer_interval: Cell::new(shimmer_interval),
|
||||
pub(crate) fn new(now: Instant, entertainment_enabled: bool) -> Self {
|
||||
if entertainment_enabled {
|
||||
let mut shimmer_text = ShimmerText::new();
|
||||
let shimmer_step = shimmer_text.get_next();
|
||||
let shimmer_interval = shimmer_text.next_interval(SHIMMER_TEXT_INTERVAL);
|
||||
let entertainment = EntertainmentState {
|
||||
use_shimmer_text: Cell::new(true),
|
||||
shimmer_text: RefCell::new(shimmer_text),
|
||||
shimmer_face_cache: RefCell::new(shimmer_step.face),
|
||||
shimmer_text_cache: RefCell::new(shimmer_step.text.clone()),
|
||||
last_shimmer_update: Cell::new(now),
|
||||
shimmer_interval: Cell::new(shimmer_interval),
|
||||
};
|
||||
Self {
|
||||
entertainment_enabled,
|
||||
header: shimmer_step.text,
|
||||
entertainment: Some(entertainment),
|
||||
}
|
||||
} else {
|
||||
Self {
|
||||
entertainment_enabled,
|
||||
header: String::from("Working"),
|
||||
entertainment: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn update_header(&mut self, header: String) {
|
||||
let was_shimmer = self.use_shimmer_text;
|
||||
let use_shimmer = self.shimmer_text.borrow().is_default_label(&header);
|
||||
self.use_shimmer_text = use_shimmer;
|
||||
self.header = header.clone();
|
||||
if !self.entertainment_enabled {
|
||||
return;
|
||||
}
|
||||
let Some(state) = self.entertainment.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let was_shimmer = state.use_shimmer_text.get();
|
||||
let use_shimmer = state.shimmer_text.borrow().is_default_label(&header);
|
||||
state.use_shimmer_text.set(use_shimmer);
|
||||
if use_shimmer {
|
||||
self.header = header.clone();
|
||||
if !was_shimmer {
|
||||
let next = self.shimmer_text.borrow_mut().reset_and_get_next();
|
||||
self.set_shimmer_step(next);
|
||||
let next_interval = self
|
||||
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);
|
||||
self.shimmer_interval.set(next_interval);
|
||||
state.shimmer_interval.set(next_interval);
|
||||
}
|
||||
self.last_shimmer_update.set(Instant::now());
|
||||
} else {
|
||||
self.header = header;
|
||||
state.last_shimmer_update.set(Instant::now());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn header_for_test(&self) -> String {
|
||||
if self.use_shimmer_text {
|
||||
self.shimmer_text_cache.borrow().clone()
|
||||
} else {
|
||||
self.header.clone()
|
||||
if let Some(state) = self.entertainment.as_ref()
|
||||
&& state.use_shimmer_text.get()
|
||||
{
|
||||
return state.shimmer_text_cache.borrow().clone();
|
||||
}
|
||||
self.header.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn render_header(&self, now: Instant) -> RenderHeader {
|
||||
if !self.use_shimmer_text {
|
||||
let Some(state) = self.entertainment.as_ref() else {
|
||||
return RenderHeader {
|
||||
face: Some(self.shimmer_face_cache.borrow().clone()),
|
||||
face: None,
|
||||
text: self.header.clone(),
|
||||
};
|
||||
};
|
||||
if !state.use_shimmer_text.get() {
|
||||
return RenderHeader {
|
||||
face: Some(state.shimmer_face_cache.borrow().clone()),
|
||||
text: self.header.clone(),
|
||||
};
|
||||
}
|
||||
|
||||
let elapsed = now.saturating_duration_since(self.last_shimmer_update.get());
|
||||
if elapsed >= self.shimmer_interval.get() {
|
||||
let next = self.shimmer_text.borrow_mut().get_next();
|
||||
self.set_shimmer_step(next);
|
||||
self.last_shimmer_update.set(now);
|
||||
let next_interval = self
|
||||
let elapsed = now.saturating_duration_since(state.last_shimmer_update.get());
|
||||
if elapsed >= state.shimmer_interval.get() {
|
||||
let next = state.shimmer_text.borrow_mut().get_next();
|
||||
self.set_shimmer_step(state, next);
|
||||
state.last_shimmer_update.set(now);
|
||||
let next_interval = state
|
||||
.shimmer_text
|
||||
.borrow_mut()
|
||||
.next_interval(SHIMMER_TEXT_INTERVAL);
|
||||
self.shimmer_interval.set(next_interval);
|
||||
state.shimmer_interval.set(next_interval);
|
||||
}
|
||||
|
||||
RenderHeader {
|
||||
face: Some(self.shimmer_face_cache.borrow().clone()),
|
||||
text: self.shimmer_text_cache.borrow().clone(),
|
||||
face: Some(state.shimmer_face_cache.borrow().clone()),
|
||||
text: state.shimmer_text_cache.borrow().clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_shimmer_step(&self, step: crate::shimmer_text::ShimmerStep) {
|
||||
*self.shimmer_face_cache.borrow_mut() = step.face;
|
||||
*self.shimmer_text_cache.borrow_mut() = step.text;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "entertainment"))]
|
||||
pub(crate) struct StatusShimmer {
|
||||
header: String,
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "entertainment"))]
|
||||
impl StatusShimmer {
|
||||
pub(crate) fn new(_now: Instant) -> Self {
|
||||
Self {
|
||||
header: String::from("Working"),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn update_header(&mut self, header: String) {
|
||||
self.header = header;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn header_for_test(&self) -> String {
|
||||
self.header.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn render_header(&self, _now: Instant) -> RenderHeader {
|
||||
RenderHeader {
|
||||
face: None,
|
||||
text: self.header.clone(),
|
||||
}
|
||||
fn set_shimmer_step(&self, state: &EntertainmentState, step: crate::shimmer_text::ShimmerStep) {
|
||||
*state.shimmer_face_cache.borrow_mut() = step.face;
|
||||
*state.shimmer_text_cache.borrow_mut() = step.text;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,10 +67,11 @@ impl StatusIndicatorWidget {
|
||||
app_event_tx: AppEventSender,
|
||||
frame_requester: FrameRequester,
|
||||
animations_enabled: bool,
|
||||
entertainment_enabled: bool,
|
||||
) -> Self {
|
||||
let now = Instant::now();
|
||||
Self {
|
||||
shimmer: StatusShimmer::new(now),
|
||||
shimmer: StatusShimmer::new(now, entertainment_enabled),
|
||||
details: None,
|
||||
show_interrupt_hint: true,
|
||||
elapsed_running: Duration::ZERO,
|
||||
@@ -280,7 +281,8 @@ mod tests {
|
||||
fn renders_with_working_header() {
|
||||
let (tx_raw, _rx) = unbounded_channel::<AppEvent>();
|
||||
let tx = AppEventSender::new(tx_raw);
|
||||
let w = StatusIndicatorWidget::new(tx, crate::tui::FrameRequester::test_dummy(), true);
|
||||
let w =
|
||||
StatusIndicatorWidget::new(tx, crate::tui::FrameRequester::test_dummy(), true, false);
|
||||
|
||||
// Render into a fixed-size test terminal and snapshot the backend.
|
||||
let mut terminal = Terminal::new(TestBackend::new(80, 2)).expect("terminal");
|
||||
@@ -294,7 +296,8 @@ mod tests {
|
||||
fn renders_truncated() {
|
||||
let (tx_raw, _rx) = unbounded_channel::<AppEvent>();
|
||||
let tx = AppEventSender::new(tx_raw);
|
||||
let w = StatusIndicatorWidget::new(tx, crate::tui::FrameRequester::test_dummy(), true);
|
||||
let w =
|
||||
StatusIndicatorWidget::new(tx, crate::tui::FrameRequester::test_dummy(), true, false);
|
||||
|
||||
// Render into a fixed-size test terminal and snapshot the backend.
|
||||
let mut terminal = Terminal::new(TestBackend::new(20, 2)).expect("terminal");
|
||||
@@ -308,7 +311,8 @@ mod tests {
|
||||
fn renders_wrapped_details_panama_two_lines() {
|
||||
let (tx_raw, _rx) = unbounded_channel::<AppEvent>();
|
||||
let tx = AppEventSender::new(tx_raw);
|
||||
let mut w = StatusIndicatorWidget::new(tx, crate::tui::FrameRequester::test_dummy(), false);
|
||||
let mut w =
|
||||
StatusIndicatorWidget::new(tx, crate::tui::FrameRequester::test_dummy(), false, false);
|
||||
w.update_details(Some("A man a plan a canal panama".to_string()));
|
||||
w.set_interrupt_hint_visible(false);
|
||||
|
||||
@@ -330,7 +334,7 @@ mod tests {
|
||||
let (tx_raw, _rx) = unbounded_channel::<AppEvent>();
|
||||
let tx = AppEventSender::new(tx_raw);
|
||||
let mut widget =
|
||||
StatusIndicatorWidget::new(tx, crate::tui::FrameRequester::test_dummy(), true);
|
||||
StatusIndicatorWidget::new(tx, crate::tui::FrameRequester::test_dummy(), true, false);
|
||||
|
||||
let baseline = Instant::now();
|
||||
widget.last_resume_at = baseline;
|
||||
@@ -351,7 +355,8 @@ mod tests {
|
||||
fn details_overflow_adds_ellipsis() {
|
||||
let (tx_raw, _rx) = unbounded_channel::<AppEvent>();
|
||||
let tx = AppEventSender::new(tx_raw);
|
||||
let mut w = StatusIndicatorWidget::new(tx, crate::tui::FrameRequester::test_dummy(), true);
|
||||
let mut w =
|
||||
StatusIndicatorWidget::new(tx, crate::tui::FrameRequester::test_dummy(), true, false);
|
||||
w.update_details(Some("abcd abcd abcd abcd".to_string()));
|
||||
|
||||
let lines = w.wrapped_details_lines(6);
|
||||
|
||||
Reference in New Issue
Block a user