mirror of
https://github.com/openai/codex.git
synced 2026-06-02 19:31:59 +00:00
Compare commits
3 Commits
dev/winsto
...
fcoury/hea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce870fad20 | ||
|
|
055482eac7 | ||
|
|
bb76d3ed69 |
@@ -56,6 +56,7 @@ use crate::bottom_pane::StatusSurfacePreviewData;
|
||||
use crate::bottom_pane::StatusSurfacePreviewItem;
|
||||
use crate::bottom_pane::TerminalTitleItem;
|
||||
use crate::bottom_pane::TerminalTitleSetupView;
|
||||
use crate::codex_logo;
|
||||
use crate::diff_model::FileChange;
|
||||
use crate::git_action_directives::parse_assistant_markdown;
|
||||
use crate::legacy_core::DEFAULT_AGENTS_MD_FILENAME;
|
||||
@@ -632,6 +633,9 @@ pub(crate) struct ChatWidget {
|
||||
forked_from: Option<ThreadId>,
|
||||
interrupted_turn_notice_mode: InterruptedTurnNoticeMode,
|
||||
frame_requester: FrameRequester,
|
||||
// Shared by the placeholder and configured headers so startup motion does not restart while
|
||||
// session metadata arrives.
|
||||
startup_logo_animation: Option<codex_logo::StartupAnimation>,
|
||||
// Whether to include the initial welcome banner on session configured
|
||||
show_welcome_banner: bool,
|
||||
// One-shot tooltip override for the primary startup session.
|
||||
@@ -1157,6 +1161,31 @@ impl ChatWidget {
|
||||
if let Some(pet) = self.ambient_pet.as_ref() {
|
||||
pet.schedule_next_frame();
|
||||
}
|
||||
// Session-header cells expose a tick only while the one-shot mascot motion is active.
|
||||
// Once it expires, settle configured session info into stable transcript history.
|
||||
if self.startup_logo_animation.is_some() {
|
||||
if self
|
||||
.transcript
|
||||
.active_cell
|
||||
.as_ref()
|
||||
.and_then(|cell| cell.transcript_animation_tick())
|
||||
.is_some()
|
||||
{
|
||||
self.frame_requester
|
||||
.schedule_frame_in(codex_logo::animation_frame_interval());
|
||||
} else {
|
||||
self.startup_logo_animation = None;
|
||||
if self
|
||||
.transcript
|
||||
.active_cell
|
||||
.as_ref()
|
||||
.is_some_and(|cell| cell.as_any().is::<history_cell::SessionInfoCell>())
|
||||
{
|
||||
self.flush_active_cell();
|
||||
}
|
||||
self.request_redraw();
|
||||
}
|
||||
}
|
||||
self.refresh_plan_mode_nudge();
|
||||
self.refresh_goal_status_indicator_for_time_tick();
|
||||
if self.terminal_title_shows_action_required() != self.last_terminal_title_requires_action {
|
||||
@@ -1170,7 +1199,8 @@ impl ChatWidget {
|
||||
}
|
||||
|
||||
fn flush_active_cell(&mut self) {
|
||||
if let Some(active) = self.transcript.active_cell.take() {
|
||||
if let Some(mut active) = self.transcript.active_cell.take() {
|
||||
active.finalize_for_history();
|
||||
self.transcript.needs_final_message_separator = true;
|
||||
self.app_event_tx.send(AppEvent::InsertHistoryCell(active));
|
||||
}
|
||||
@@ -1449,22 +1479,30 @@ impl ChatWidget {
|
||||
}
|
||||
|
||||
/// Build a placeholder header cell while the session is configuring.
|
||||
fn placeholder_session_header_cell(config: &Config) -> Box<dyn HistoryCell> {
|
||||
fn placeholder_session_header_cell(
|
||||
config: &Config,
|
||||
startup_logo_animation: Option<codex_logo::StartupAnimation>,
|
||||
) -> Box<dyn HistoryCell> {
|
||||
let placeholder_style = Style::default().add_modifier(Modifier::DIM | Modifier::ITALIC);
|
||||
Box::new(
|
||||
history_cell::SessionHeaderHistoryCell::new_with_style(
|
||||
DEFAULT_MODEL_DISPLAY_NAME.to_string(),
|
||||
placeholder_style,
|
||||
/*reasoning_effort*/ None,
|
||||
/*show_fast_status*/ false,
|
||||
config.cwd.to_path_buf(),
|
||||
CODEX_CLI_VERSION,
|
||||
)
|
||||
.with_yolo_mode(history_cell::is_yolo_mode(config)),
|
||||
let mut header = history_cell::SessionHeaderHistoryCell::new_with_style(
|
||||
DEFAULT_MODEL_DISPLAY_NAME.to_string(),
|
||||
placeholder_style,
|
||||
/*reasoning_effort*/ None,
|
||||
/*show_fast_status*/ false,
|
||||
config.cwd.to_path_buf(),
|
||||
CODEX_CLI_VERSION,
|
||||
)
|
||||
.with_yolo_mode(history_cell::is_yolo_mode(config));
|
||||
if let Some(animation) = startup_logo_animation {
|
||||
header = header.with_startup_logo_animation(animation);
|
||||
}
|
||||
Box::new(header)
|
||||
}
|
||||
|
||||
/// Merge the real session info cell with any placeholder header to avoid double boxes.
|
||||
/// Merge configured session info with the placeholder while preserving startup mascot motion.
|
||||
///
|
||||
/// The configured header remains active until the shared startup animation settles, avoiding a
|
||||
/// second header box and keeping transcript history static after the eventual flush.
|
||||
fn apply_session_info_cell(&mut self, cell: history_cell::SessionInfoCell) {
|
||||
let mut session_info_cell = Some(Box::new(cell) as Box<dyn HistoryCell>);
|
||||
let merged_header = if let Some(active) = self.transcript.active_cell.take() {
|
||||
@@ -1485,6 +1523,21 @@ impl ChatWidget {
|
||||
false
|
||||
};
|
||||
|
||||
if self.startup_logo_animation.is_some()
|
||||
&& self
|
||||
.transcript
|
||||
.active_cell
|
||||
.as_ref()
|
||||
.and_then(|cell| cell.transcript_animation_tick())
|
||||
.is_some()
|
||||
{
|
||||
self.frame_requester
|
||||
.schedule_frame_in(codex_logo::animation_frame_interval());
|
||||
self.request_redraw();
|
||||
return;
|
||||
}
|
||||
|
||||
self.startup_logo_animation = None;
|
||||
self.flush_active_cell();
|
||||
|
||||
if !merged_header && let Some(cell) = session_info_cell {
|
||||
|
||||
@@ -61,7 +61,12 @@ impl ChatWidget {
|
||||
settings: fallback_default,
|
||||
};
|
||||
|
||||
let active_cell = Some(Self::placeholder_session_header_cell(&config));
|
||||
// Select startup motion once so the placeholder and configured headers share one clock.
|
||||
let startup_logo_animation = config.animations.then(codex_logo::startup_animation);
|
||||
let active_cell = Some(Self::placeholder_session_header_cell(
|
||||
&config,
|
||||
startup_logo_animation,
|
||||
));
|
||||
|
||||
let current_cwd = Some(config.cwd.to_path_buf());
|
||||
let effective_service_tier = crate::service_tier_resolution::effective_service_tier(
|
||||
@@ -181,6 +186,7 @@ impl ChatWidget {
|
||||
side_placeholder_text: side_placeholder,
|
||||
forked_from: None,
|
||||
interrupted_turn_notice_mode: InterruptedTurnNoticeMode::Default,
|
||||
startup_logo_animation,
|
||||
input_queue: InputQueueState::default(),
|
||||
chat_keymap,
|
||||
queued_message_edit_hint_binding,
|
||||
|
||||
@@ -111,7 +111,7 @@ impl ChatWidget {
|
||||
let startup_tooltip_override = self.startup_tooltip_override.take();
|
||||
let show_fast_status = self
|
||||
.should_show_fast_status(&model_for_header, self.effective_service_tier.as_deref());
|
||||
let session_info_cell = history_cell::new_session_info(
|
||||
let mut session_info_cell = history_cell::new_session_info(
|
||||
&self.config,
|
||||
&model_for_header,
|
||||
&session,
|
||||
@@ -120,6 +120,10 @@ impl ChatWidget {
|
||||
self.plan_type,
|
||||
show_fast_status,
|
||||
);
|
||||
if let Some(animation) = self.startup_logo_animation {
|
||||
// Continue the placeholder's original motion instead of restarting at handoff.
|
||||
session_info_cell = session_info_cell.with_startup_logo_animation(animation);
|
||||
}
|
||||
self.apply_session_info_cell(session_info_cell);
|
||||
} else if self
|
||||
.transcript
|
||||
|
||||
@@ -449,6 +449,19 @@ async fn configured_pet_load_is_deferred_until_after_construction() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn placeholder_session_header_animates_while_session_is_configuring() {
|
||||
let mut cfg = test_config().await;
|
||||
cfg.animations = true;
|
||||
|
||||
let cell = ChatWidget::placeholder_session_header_cell(
|
||||
&cfg,
|
||||
/*startup_logo_animation*/ Some(codex_logo::startup_animation()),
|
||||
);
|
||||
|
||||
assert!(cell.transcript_animation_tick().is_some());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn prefetch_rate_limits_is_gated_on_chatgpt_auth_provider() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
232
codex-rs/tui/src/codex_logo.rs
Normal file
232
codex-rs/tui/src/codex_logo.rs
Normal file
@@ -0,0 +1,232 @@
|
||||
//! Unicode mascot rendering primitives for the startup session header.
|
||||
//!
|
||||
//! This module owns the small amount of runtime state needed to animate the generated frame
|
||||
//! tables in `codex_logo_frames`. It does not decide where the mascot is rendered or schedule
|
||||
//! redraws; the session-header cell renders the selected frame and `ChatWidget` drives the
|
||||
//! lifecycle while the header remains active.
|
||||
//!
|
||||
//! A startup animation is intentionally copyable. The placeholder header created before session
|
||||
//! configuration and the configured session header must share the same selected motion and
|
||||
//! `Instant`, otherwise the mascot can restart or switch motions when session metadata arrives.
|
||||
//! Once the fixed startup window expires, callers render `STATIC_FRAME` and stop scheduling
|
||||
//! mascot redraws.
|
||||
|
||||
use crate::codex_logo_frames::BLINK_FRAMES;
|
||||
use crate::codex_logo_frames::READ_BELOW_FRAMES;
|
||||
use crate::codex_logo_frames::THINKING_FRAMES;
|
||||
use crate::codex_logo_frames::WORKING_FRAMES;
|
||||
use crate::color;
|
||||
use rand::Rng;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
pub(crate) use crate::codex_logo_frames::HEIGHT;
|
||||
pub(crate) use crate::codex_logo_frames::LogoFrame;
|
||||
pub(crate) use crate::codex_logo_frames::WIDTH;
|
||||
|
||||
/// Number of columns reserved between the mascot and session text.
|
||||
pub(crate) const GAP_WIDTH: usize = 1;
|
||||
|
||||
const ANIMATION_FRAME_MILLIS: u64 = 200;
|
||||
const STARTUP_ANIMATION_LOOPS: u64 = 2;
|
||||
const FRAME_COUNT: usize = 8;
|
||||
const HIGHLIGHT_PERIOD_FRAMES: u64 = HEIGHT as u64 + 4;
|
||||
|
||||
const BRIGHT_GRADIENT: [(u8, u8, u8); HEIGHT] = [
|
||||
(153, 161, 255),
|
||||
(136, 157, 255),
|
||||
(119, 148, 255),
|
||||
(100, 130, 255),
|
||||
(72, 92, 253),
|
||||
(61, 78, 249),
|
||||
];
|
||||
|
||||
const DARK_GRADIENT: [(u8, u8, u8); HEIGHT] = [
|
||||
(70, 84, 202),
|
||||
(58, 96, 214),
|
||||
(47, 108, 222),
|
||||
(40, 101, 218),
|
||||
(30, 72, 194),
|
||||
(27, 57, 176),
|
||||
];
|
||||
|
||||
/// Motion sequences eligible for the one-shot startup animation.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum StartupAnimationKind {
|
||||
ReadBelow,
|
||||
Thinking,
|
||||
Working,
|
||||
}
|
||||
|
||||
/// Selected startup motion and the clock origin shared across header handoffs.
|
||||
///
|
||||
/// Keep this value intact when replacing the placeholder header with configured session
|
||||
/// information. Constructing another value during that handoff would restart the animation and
|
||||
/// could choose a different motion.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) struct StartupAnimation {
|
||||
kind: StartupAnimationKind,
|
||||
start: Instant,
|
||||
}
|
||||
|
||||
const STARTUP_ANIMATION_KINDS: [StartupAnimationKind; 3] = [
|
||||
StartupAnimationKind::Working,
|
||||
StartupAnimationKind::Thinking,
|
||||
StartupAnimationKind::ReadBelow,
|
||||
];
|
||||
|
||||
/// Reference frame rendered after startup motion settles or when animations are disabled.
|
||||
pub(crate) const STATIC_FRAME: LogoFrame = BLINK_FRAMES[0];
|
||||
|
||||
/// Starts one randomly selected mascot motion at the current instant.
|
||||
pub(crate) fn startup_animation() -> StartupAnimation {
|
||||
let mut rng = rand::rng();
|
||||
StartupAnimation {
|
||||
kind: STARTUP_ANIMATION_KINDS[rng.random_range(0..STARTUP_ANIMATION_KINDS.len())],
|
||||
start: Instant::now(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the total lifetime of the one-shot startup motion.
|
||||
pub(crate) fn startup_animation_duration() -> Duration {
|
||||
Duration::from_millis(ANIMATION_FRAME_MILLIS * FRAME_COUNT as u64 * STARTUP_ANIMATION_LOOPS)
|
||||
}
|
||||
|
||||
/// Returns the redraw cadence expected while startup motion is active.
|
||||
pub(crate) fn animation_frame_interval() -> Duration {
|
||||
Duration::from_millis(ANIMATION_FRAME_MILLIS)
|
||||
}
|
||||
|
||||
/// Returns the current frame tick while startup motion remains active.
|
||||
///
|
||||
/// `None` means the caller should settle the header to `STATIC_FRAME` and stop scheduling redraws.
|
||||
pub(crate) fn animation_tick(animation: StartupAnimation) -> Option<u64> {
|
||||
let elapsed = animation.start.elapsed();
|
||||
if elapsed >= startup_animation_duration() {
|
||||
None
|
||||
} else {
|
||||
Some(elapsed.as_millis() as u64 / ANIMATION_FRAME_MILLIS)
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolves the generated mascot frame for an active animation tick.
|
||||
pub(crate) fn frame_for_tick(animation: StartupAnimation, tick: u64) -> &'static LogoFrame {
|
||||
&animation_frames(animation.kind)[tick as usize % FRAME_COUNT]
|
||||
}
|
||||
|
||||
/// Selects a readable static mascot gradient for the terminal background.
|
||||
pub(crate) fn gradient_for_bg(bg: Option<(u8, u8, u8)>) -> [(u8, u8, u8); HEIGHT] {
|
||||
if bg.is_some_and(color::is_light) {
|
||||
DARK_GRADIENT
|
||||
} else {
|
||||
BRIGHT_GRADIENT
|
||||
}
|
||||
}
|
||||
|
||||
/// Selects the mascot gradient for an active tick, including the moving highlight row.
|
||||
pub(crate) fn gradient_for_animation_tick(
|
||||
bg: Option<(u8, u8, u8)>,
|
||||
tick: u64,
|
||||
) -> [(u8, u8, u8); HEIGHT] {
|
||||
let mut gradient = gradient_for_bg(bg);
|
||||
let highlight_row = tick % HIGHLIGHT_PERIOD_FRAMES;
|
||||
for (row, rgb) in gradient.iter_mut().enumerate() {
|
||||
let distance = row.abs_diff(highlight_row as usize);
|
||||
if distance <= 1 {
|
||||
*rgb = brighten(
|
||||
*rgb,
|
||||
if distance == 0 {
|
||||
/*amount*/
|
||||
36
|
||||
} else {
|
||||
/*amount*/
|
||||
16
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
gradient
|
||||
}
|
||||
|
||||
fn animation_frames(kind: StartupAnimationKind) -> &'static [LogoFrame; FRAME_COUNT] {
|
||||
match kind {
|
||||
StartupAnimationKind::ReadBelow => &READ_BELOW_FRAMES,
|
||||
StartupAnimationKind::Thinking => &THINKING_FRAMES,
|
||||
StartupAnimationKind::Working => &WORKING_FRAMES,
|
||||
}
|
||||
}
|
||||
|
||||
fn brighten((r, g, b): (u8, u8, u8), amount: u8) -> (u8, u8, u8) {
|
||||
(
|
||||
r.saturating_add(amount),
|
||||
g.saturating_add(amount),
|
||||
b.saturating_add(amount),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
#[test]
|
||||
fn frames_keep_14_by_6_geometry() {
|
||||
for kind in STARTUP_ANIMATION_KINDS {
|
||||
for frame in animation_frames(kind) {
|
||||
assert_eq!(frame.len(), HEIGHT);
|
||||
for line in frame {
|
||||
assert_eq!(UnicodeWidthStr::width(*line), WIDTH);
|
||||
}
|
||||
}
|
||||
}
|
||||
for line in STATIC_FRAME {
|
||||
assert_eq!(UnicodeWidthStr::width(line), WIDTH);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_animations_have_distinct_frames() {
|
||||
for kind in STARTUP_ANIMATION_KINDS {
|
||||
let frames = animation_frames(kind);
|
||||
assert!(frames.windows(2).any(|frames| frames[0] != frames[1]));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn animation_wraps_and_settles() {
|
||||
let animation = StartupAnimation {
|
||||
kind: StartupAnimationKind::Working,
|
||||
start: Instant::now(),
|
||||
};
|
||||
assert_eq!(
|
||||
frame_for_tick(animation, /*tick*/ 0),
|
||||
frame_for_tick(animation, FRAME_COUNT as u64)
|
||||
);
|
||||
let completed_animation = StartupAnimation {
|
||||
kind: StartupAnimationKind::Working,
|
||||
start: Instant::now()
|
||||
.checked_sub(startup_animation_duration())
|
||||
.expect("duration should fit"),
|
||||
};
|
||||
assert_eq!(animation_tick(completed_animation), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copied_animation_preserves_kind_and_origin() {
|
||||
let animation = StartupAnimation {
|
||||
kind: StartupAnimationKind::Thinking,
|
||||
start: Instant::now(),
|
||||
};
|
||||
let copied_animation = animation;
|
||||
|
||||
assert_eq!(copied_animation, animation);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn animation_tick_changes_gradient() {
|
||||
assert_ne!(
|
||||
gradient_for_animation_tick(/*bg*/ None, /*tick*/ 0),
|
||||
gradient_for_animation_tick(/*bg*/ None, /*tick*/ 1)
|
||||
);
|
||||
}
|
||||
}
|
||||
273
codex-rs/tui/src/codex_logo_frames.rs
Normal file
273
codex-rs/tui/src/codex_logo_frames.rs
Normal file
@@ -0,0 +1,273 @@
|
||||
// @generated by scripts/export-ansi-mascot-frames.mjs; do not edit.
|
||||
|
||||
pub(crate) const HEIGHT: usize = 6;
|
||||
pub(crate) const WIDTH: usize = 14;
|
||||
pub(crate) type LogoFrame = [&'static str; HEIGHT];
|
||||
|
||||
pub(crate) const BLINK_FRAMES: [LogoFrame; 8] = [
|
||||
[
|
||||
" ⢀⣴⣶⣶⣶⣤⣤⣄⡀ ",
|
||||
" ⢀⣴⣾⣿⣿⣿⣿⣿⣿⣿⣿⡆ ",
|
||||
" ⣿⣿⣿⣇⠙⣿⣿⣿⣿⣿⣿⣷⡀",
|
||||
" ⢹⣿⣿⡏⣠⣿⣟⠛⠛⣿⣿⣿⡇",
|
||||
" ⢿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠋ ",
|
||||
" ⠉⠛⠛⠻⠿⠿⠟⠋ ",
|
||||
],
|
||||
[
|
||||
" ⢀⣴⣶⣶⣶⣤⣤⣄⡀ ",
|
||||
" ⢀⣴⣾⣿⣿⣿⣿⣿⣿⣿⣿⡆ ",
|
||||
" ⣿⣿⣿⣿⠈⣿⣿⣿⣿⣿⣿⣷⡀",
|
||||
" ⢹⣿⣿⣿⢀⣿⣟⠛⠛⣿⣿⣿⡇",
|
||||
" ⢿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠋ ",
|
||||
" ⠉⠛⠛⠻⠿⠿⠟⠋ ",
|
||||
],
|
||||
[
|
||||
" ⢀⣴⣶⣶⣶⣤⣤⣄⡀ ",
|
||||
" ⢀⣴⣾⣿⣿⣿⣿⣿⣿⣿⣿⡆ ",
|
||||
" ⣿⣿⣿⣿⠁⣿⣿⣿⣿⣿⣿⣷⡀",
|
||||
" ⢹⣿⣿⣿⡀⣿⣟⠛⠛⣿⣿⣿⡇",
|
||||
" ⢿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠋ ",
|
||||
" ⠉⠛⠛⠻⠿⠿⠟⠋ ",
|
||||
],
|
||||
[
|
||||
" ⢀⣴⣶⣶⣶⣤⣤⣄⡀ ",
|
||||
" ⢀⣴⣾⣿⣿⣿⣿⣿⣿⣿⣿⡆ ",
|
||||
" ⣿⣿⣿⣿⡇⣿⣿⣿⣿⣿⣿⣷⡀",
|
||||
" ⢹⣿⣿⣿⡇⣿⣟⠛⠛⣿⣿⣿⡇",
|
||||
" ⢿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠋ ",
|
||||
" ⠉⠛⠛⠻⠿⠿⠟⠋ ",
|
||||
],
|
||||
[
|
||||
" ⢀⣴⣶⣶⣶⣤⣤⣄⡀ ",
|
||||
" ⢀⣴⣾⣿⣿⣿⣿⣿⣿⣿⣿⡆ ",
|
||||
" ⣿⣿⣿⣿⠃⣿⣿⣿⣿⣿⣿⣷⡀",
|
||||
" ⢹⣿⣿⣿⡄⣿⣟⠛⠛⣿⣿⣿⡇",
|
||||
" ⢿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠋ ",
|
||||
" ⠉⠛⠛⠻⠿⠿⠟⠋ ",
|
||||
],
|
||||
[
|
||||
" ⢀⣴⣶⣶⣶⣤⣤⣄⡀ ",
|
||||
" ⢀⣴⣾⣿⣿⣿⣿⣿⣿⣿⣿⡆ ",
|
||||
" ⣿⣿⣿⣿⠈⣿⣿⣿⣿⣿⣿⣷⡀",
|
||||
" ⢹⣿⣿⣿⢀⣿⣟⠛⠛⣿⣿⣿⡇",
|
||||
" ⢿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠋ ",
|
||||
" ⠉⠛⠛⠻⠿⠿⠟⠋ ",
|
||||
],
|
||||
[
|
||||
" ⢀⣴⣶⣶⣶⣤⣤⣄⡀ ",
|
||||
" ⢀⣴⣾⣿⣿⣿⣿⣿⣿⣿⣿⡆ ",
|
||||
" ⣿⣿⣿⣧⠙⣿⣿⣿⣿⣿⣿⣷⡀",
|
||||
" ⢹⣿⣿⡟⣠⣿⣟⠛⠛⣿⣿⣿⡇",
|
||||
" ⢿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠋ ",
|
||||
" ⠉⠛⠛⠻⠿⠿⠟⠋ ",
|
||||
],
|
||||
[
|
||||
" ⢀⣴⣶⣶⣶⣤⣤⣄⡀ ",
|
||||
" ⢀⣴⣾⣿⣿⣿⣿⣿⣿⣿⣿⡆ ",
|
||||
" ⣿⣿⣿⣇⠙⣿⣿⣿⣿⣿⣿⣷⡀",
|
||||
" ⢹⣿⣿⡏⣠⣿⣟⠛⠛⣿⣿⣿⡇",
|
||||
" ⢿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠋ ",
|
||||
" ⠉⠛⠛⠻⠿⠿⠟⠋ ",
|
||||
],
|
||||
];
|
||||
|
||||
pub(crate) const READ_BELOW_FRAMES: [LogoFrame; 8] = [
|
||||
[
|
||||
" ⢀⣴⣶⣶⣶⣤⣤⣄⡀ ",
|
||||
" ⢀⣴⣾⣿⣿⣿⣿⣿⣿⣿⣿⡆ ",
|
||||
" ⣿⣿⣿⣿⡇⢸⣿ ⣿⣿⣿⣷⡀",
|
||||
" ⢹⣿⣿⣿⣧⣼⣿⣴⣿⣿⣿⣿⡇",
|
||||
" ⢿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠋ ",
|
||||
" ⠉⠛⠛⠻⠿⠿⠟⠋ ",
|
||||
],
|
||||
[
|
||||
" ⣠⣴⣶⣦⣄⣀⣀ ",
|
||||
" ⢀⣤⣾⣿⣿⣿⣿⣿⣿⣿⣿⡄ ",
|
||||
" ⣾⣿⣿⣿⡏⢹⣿⠉⣿⣿⣿⣧ ",
|
||||
" ⢻⣿⣿⣿⣧⣼⣿⣤⣿⣿⣿⣿⡇",
|
||||
" ⠈⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠟ ",
|
||||
" ⠈⠛⠻⠿⢿⣿⣿⡿⠋ ",
|
||||
],
|
||||
[
|
||||
" ⣠⣤⣶⣦⣄⣀⣀ ",
|
||||
" ⣠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣆ ",
|
||||
" ⣾⣿⣿⣿⣿⡟⢻⣿⠛⣿⣿⣿⡀",
|
||||
" ⠹⣿⣿⣿⣿⣇⣸⣿⣀⣿⣿⣿⡇",
|
||||
" ⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠟⠁",
|
||||
" ⠈⠙⠻⠿⢿⣿⣿⡿⠟ ",
|
||||
],
|
||||
[
|
||||
" ⣠⣤⣶⣦⣄⣀⣀ ",
|
||||
" ⣠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣆ ",
|
||||
" ⣾⣿⣿⣿⣿⡟⢻⣿⠛⣿⣿⣿⡀",
|
||||
" ⠹⣿⣿⣿⣿⣇⣸⣿⣀⣿⣿⣿⡇",
|
||||
" ⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠟⠁",
|
||||
" ⠈⠙⠻⠿⢿⣿⣿⡿⠟ ",
|
||||
],
|
||||
[
|
||||
" ⣠⣤⣶⣦⣄⣀⣀ ",
|
||||
" ⢀⣠⣾⣿⣿⣿⣿⣿⣿⣿⣿⡄ ",
|
||||
" ⣾⣿⣿⣿⣿⠛⣿⡟⢻⣿⣿⣿⡀",
|
||||
" ⠻⣿⣿⣿⣿⣀⣿⣇⣸⣿⣿⣿⡇",
|
||||
" ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠟⠁",
|
||||
" ⠈⠛⠻⠿⢿⣿⣿⡿⠟ ",
|
||||
],
|
||||
[
|
||||
" ⣠⣴⣶⣤⣄⣀⣀ ",
|
||||
" ⢀⣤⣾⣿⣿⣿⣿⣿⣿⣿⣷⡄ ",
|
||||
" ⣿⣿⣿⡟⢻⣿⠛⣿⣿⣿⣿⣧ ",
|
||||
" ⢻⣿⣿⣇⣸⣿⣀⣿⣿⣿⣿⣿⡇",
|
||||
" ⠘⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠟ ",
|
||||
" ⠈⠛⠻⠿⢿⣿⣿⡿⠋ ",
|
||||
],
|
||||
[
|
||||
" ⣠⣴⣶⣤⣄⣀⣀ ",
|
||||
" ⢀⣤⣾⣿⣿⣿⣿⣿⣿⣿⣷⡄ ",
|
||||
" ⣿⣿⣿⡟⢻⣿⠛⣿⣿⣿⣿⣧ ",
|
||||
" ⢻⣿⣿⣇⣸⣿⣀⣿⣿⣿⣿⣿⡇",
|
||||
" ⠘⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠟ ",
|
||||
" ⠈⠛⠻⠿⢿⣿⣿⡿⠋ ",
|
||||
],
|
||||
[
|
||||
" ⣠⣴⣶⣦⣄⣀⣀ ",
|
||||
" ⢀⣤⣾⣿⣿⣿⣿⣿⣿⣿⣷⡄ ",
|
||||
" ⣾⣿⣿⣿⠋⣿⡏⢹⣿⣿⣿⣧ ",
|
||||
" ⢻⣿⣿⣿⣄⣿⣧⣸⣿⣿⣿⣿⡇",
|
||||
" ⠘⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠟ ",
|
||||
" ⠈⠛⠛⠿⢿⣿⣿⠿⠋ ",
|
||||
],
|
||||
];
|
||||
|
||||
pub(crate) const THINKING_FRAMES: [LogoFrame; 8] = [
|
||||
[
|
||||
" ⢀⣴⣾⣿⣷⣦⣤⣤⡀ ",
|
||||
" ⢠⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⡆ ",
|
||||
" ⣿⣿⣿⣿⠿⣿⢿⡿⢿⣿⣿⣷⡀",
|
||||
" ⢹⣿⣿⣿⣷⣿⣾⣷⣿⣿⣿⣿⠇",
|
||||
" ⢿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠋ ",
|
||||
" ⠉⠙⠛⠻⠿⠿⠟⠋ ",
|
||||
],
|
||||
[
|
||||
" ⣀⣠⣶⣿⣿⣷⣦⣀⡀ ",
|
||||
" ⢠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦ ",
|
||||
" ⢻⣿⣿⣿⠿⣿⢿⡿⣿⣿⣿⣿ ",
|
||||
" ⢸⣿⣿⣿⣷⣿⣾⣷⣿⣿⣿⣿ ",
|
||||
" ⠘⢿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠏ ",
|
||||
" ⠉⠻⠿⠿⠿⠋⠁ ",
|
||||
],
|
||||
[
|
||||
" ⢀⣠⣤⣴⣾⣿⣿⣶⣄ ",
|
||||
" ⢠⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣦ ",
|
||||
" ⣸⣿⣿⣿⠿⡿⢿⡿⣿⣿⣿⣿⠆",
|
||||
" ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡟ ",
|
||||
" ⠈⠻⢿⣿⣿⣿⣿⣿⣿⣿⣿⠃ ",
|
||||
" ⠈⠛⠿⠿⠿⠛⠋⠉ ",
|
||||
],
|
||||
[
|
||||
" ⣤⣶⣶⣶⣶⣿⣶⣦⡀ ",
|
||||
" ⣼⣿⣿⣿⣿⣿⣿⣿⣿⣷⣄ ",
|
||||
" ⣾⣿⣿⣿⠿⣿⢿⡿⣿⣿⣿⣿⡆",
|
||||
" ⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠃",
|
||||
" ⠹⣿⣿⣿⣿⣿⣿⣿⣿⡿ ",
|
||||
" ⠈⠛⠿⠟⠛⠛⠛⠋ ",
|
||||
],
|
||||
[
|
||||
" ⢠⣶⣿⣿⣿⣶⣶⣤⣄ ",
|
||||
" ⢠⣶⣿⣿⣿⣿⣿⣿⣿⣿⣿⡆ ",
|
||||
" ⣿⣿⣿⣿⠿⣿⢿⡿⢿⣿⣿⣷⡄",
|
||||
" ⠘⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠇",
|
||||
" ⢻⣿⣿⣿⣿⣿⣿⣿⣿⠟⠋ ",
|
||||
" ⠈⠙⠛⠛⠿⠿⠟⠋ ",
|
||||
],
|
||||
[
|
||||
" ⢀⣠⣤⣴⣾⣿⣿⣷⣄ ",
|
||||
" ⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦ ",
|
||||
" ⣰⣿⣿⣿⡿⣿⢿⡿⢿⣿⣿⣿⡇",
|
||||
" ⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿ ",
|
||||
" ⠈⠛⢿⣿⣿⣿⣿⣿⣿⣿⣿⠏ ",
|
||||
" ⠙⠿⠿⠿⠛⠛⠉⠁ ",
|
||||
],
|
||||
[
|
||||
" ⢀⣴⣾⣿⣿⣷⣤⣤⣀ ",
|
||||
" ⢠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧ ",
|
||||
" ⢿⣿⣿⣿⡿⣿⢿⡿⢿⣿⣿⣿ ",
|
||||
" ⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇",
|
||||
" ⠈⢿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠋ ",
|
||||
" ⠈⠉⠙⠿⠿⠿⠛⠁ ",
|
||||
],
|
||||
[
|
||||
" ⢀⣴⣾⣿⣷⣦⣤⣤⡀ ",
|
||||
" ⢠⣶⣿⣿⣿⣿⣿⣿⣿⣿⣿⣆ ",
|
||||
" ⣿⣿⣿⣿⠿⣿⢿⡿⢿⣿⣿⣿⡀",
|
||||
" ⢹⣿⣿⣿⣿⣿⣾⣷⣿⣿⣿⣿⡇",
|
||||
" ⢿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠋ ",
|
||||
" ⠉⠙⠛⠻⠿⠿⠟⠋ ",
|
||||
],
|
||||
];
|
||||
|
||||
pub(crate) const WORKING_FRAMES: [LogoFrame; 8] = [
|
||||
[
|
||||
" ⢀⣴⣶⣿⣶⣦⣤⣤⡀ ",
|
||||
" ⢀⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⡆ ",
|
||||
" ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡀",
|
||||
" ⢹⣿⡟⣁⡙⣿⡏⣁⣙⣿⣿⣿⡇",
|
||||
" ⢿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠋ ",
|
||||
" ⠉⠛⠛⠻⠿⠿⠟⠋ ",
|
||||
],
|
||||
[
|
||||
" ⢀⣤⣶⣶⣶⣦⣤⣄⡀ ",
|
||||
" ⢀⣴⣾⣿⣿⣿⣿⣿⣿⣿⣿⣆ ",
|
||||
" ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡀",
|
||||
" ⢹⣿⣟⣁⡙⣿⡟⣁⡙⣿⣿⣿⡇",
|
||||
" ⠈⢿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠋ ",
|
||||
" ⠉⠛⠛⠻⠿⠿⠟⠋ ",
|
||||
],
|
||||
[
|
||||
" ⢀⣤⣶⣶⣶⣦⣤⣄⡀ ",
|
||||
" ⢀⣴⣾⣿⣿⣿⣿⣿⣿⣿⣿⣦ ",
|
||||
" ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡄",
|
||||
" ⢹⣿⣟⣁⡉⣿⡟⢉⡙⣿⣿⣿⡇",
|
||||
" ⠈⢿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠛ ",
|
||||
" ⠉⠙⠛⠻⠿⠿⠟⠋ ",
|
||||
],
|
||||
[
|
||||
" ⢀⣤⣶⣶⣶⣦⣤⣄⡀ ",
|
||||
" ⢀⣴⣾⣿⣿⣿⣿⣿⣿⣿⣿⣆ ",
|
||||
" ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡀",
|
||||
" ⢹⣿⣿⡟⢉⡙⣿⡟⢉⠙⣿⣿⡇",
|
||||
" ⠈⢿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠋ ",
|
||||
" ⠉⠛⠛⠻⠿⠿⠟⠋ ",
|
||||
],
|
||||
[
|
||||
" ⢀⣴⣶⣿⣶⣦⣤⣤⡀ ",
|
||||
" ⢀⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⡆ ",
|
||||
" ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡀",
|
||||
" ⢹⣿⣿⡟⢉⠙⣿⡟⢉⠙⣿⣿⡇",
|
||||
" ⢿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠋ ",
|
||||
" ⠉⠛⠛⠻⠿⠿⠟⠋ ",
|
||||
],
|
||||
[
|
||||
" ⢀⣴⣶⣷⣶⣤⣤⣤⡀ ",
|
||||
" ⢠⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⡆ ",
|
||||
" ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡀",
|
||||
" ⢹⣿⣿⡟⢉⡙⣿⡟⢉⡙⣿⣿⠇",
|
||||
" ⠈⢿⣿⣿⣿⣿⣿⣿⣿⣿⠟⠋ ",
|
||||
" ⠉⠛⠛⠻⠿⠿⠟⠋ ",
|
||||
],
|
||||
[
|
||||
" ⢀⣴⣶⣶⣶⣤⣤⣤⡀ ",
|
||||
" ⣠⣶⣿⣿⣿⣿⣿⣿⣿⣿⣿⡆ ",
|
||||
" ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡀",
|
||||
" ⢻⣿⣿⣁⣌⣻⣿⣡⣌⣻⣿⣿⡇",
|
||||
" ⠈⢿⣿⣿⣿⣿⣿⣿⣿⣿⠟⠋ ",
|
||||
" ⠉⠛⠛⠻⠿⠿⠟⠉ ",
|
||||
],
|
||||
[
|
||||
" ⢀⣴⣶⣷⣶⣤⣤⣤⡀ ",
|
||||
" ⢠⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⡆ ",
|
||||
" ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡀",
|
||||
" ⢹⣿⣿⣁⣌⣻⣟⣡⣌⣻⣿⣿⠇",
|
||||
" ⠈⢿⣿⣿⣿⣿⣿⣿⣿⣿⠟⠋ ",
|
||||
" ⠉⠛⠛⠻⠿⠿⠟⠋ ",
|
||||
],
|
||||
];
|
||||
@@ -162,4 +162,16 @@ impl HistoryCell for CompositeHistoryCell {
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn transcript_animation_tick(&self) -> Option<u64> {
|
||||
self.parts
|
||||
.iter()
|
||||
.find_map(|part| part.transcript_animation_tick())
|
||||
}
|
||||
|
||||
fn finalize_for_history(&mut self) {
|
||||
for part in &mut self.parts {
|
||||
part.finalize_for_history();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,6 +295,14 @@ pub(crate) trait HistoryCell: std::fmt::Debug + Send + Sync + Any {
|
||||
fn transcript_animation_tick(&self) -> Option<u64> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Settles transient visuals before an active cell is committed to transcript history.
|
||||
///
|
||||
/// Implementations with time-dependent rendering should discard that state here so committed
|
||||
/// history remains stable and does not require redraw scheduling. `ChatWidget` calls this when
|
||||
/// flushing an active cell; forgetting to forward it through wrapper cells can leave transcript
|
||||
/// history dependent on an expired animation clock.
|
||||
fn finalize_for_history(&mut self) {}
|
||||
}
|
||||
|
||||
impl Renderable for Box<dyn HistoryCell> {
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
//! Session headers, onboarding guidance, and transcript cards.
|
||||
|
||||
use super::*;
|
||||
use crate::codex_logo;
|
||||
use crate::terminal_palette;
|
||||
|
||||
pub(crate) const SESSION_HEADER_MAX_INNER_WIDTH: usize = 56; // Just an eyeballed value
|
||||
const SESSION_HEADER_TEXT_MAX_INNER_WIDTH: usize = 56; // Just an eyeballed value
|
||||
pub(crate) const SESSION_HEADER_MAX_INNER_WIDTH: usize =
|
||||
codex_logo::WIDTH + codex_logo::GAP_WIDTH + SESSION_HEADER_TEXT_MAX_INNER_WIDTH;
|
||||
|
||||
pub(crate) fn card_inner_width(width: u16, max_inner_width: usize) -> Option<usize> {
|
||||
if width < 4 {
|
||||
@@ -116,9 +120,33 @@ impl HistoryCell for TooltipHistoryCell {
|
||||
}
|
||||
}
|
||||
|
||||
/// Session header plus any startup guidance or tooltip cells rendered with it.
|
||||
#[derive(Debug)]
|
||||
pub struct SessionInfoCell(CompositeHistoryCell);
|
||||
|
||||
impl SessionInfoCell {
|
||||
/// Preserves a startup mascot animation when configured session information replaces a placeholder.
|
||||
///
|
||||
/// `new_session_info` always includes one `SessionHeaderHistoryCell`. This method locates that
|
||||
/// nested header so the placeholder and configured header can share the same selected motion
|
||||
/// and clock origin. If the session-info composition changes, keep this propagation path
|
||||
/// intact or the startup mascot will stop or restart during the handoff.
|
||||
pub(crate) fn with_startup_logo_animation(
|
||||
mut self,
|
||||
animation: codex_logo::StartupAnimation,
|
||||
) -> Self {
|
||||
if let Some(header) = self
|
||||
.0
|
||||
.parts
|
||||
.iter_mut()
|
||||
.find_map(|part| part.as_any_mut().downcast_mut::<SessionHeaderHistoryCell>())
|
||||
{
|
||||
header.logo_animation = Some(animation);
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl HistoryCell for SessionInfoCell {
|
||||
fn display_lines(&self, width: u16) -> Vec<Line<'static>> {
|
||||
self.0.display_lines(width)
|
||||
@@ -135,6 +163,14 @@ impl HistoryCell for SessionInfoCell {
|
||||
fn raw_lines(&self) -> Vec<Line<'static>> {
|
||||
self.0.raw_lines()
|
||||
}
|
||||
|
||||
fn transcript_animation_tick(&self) -> Option<u64> {
|
||||
self.0.transcript_animation_tick()
|
||||
}
|
||||
|
||||
fn finalize_for_history(&mut self) {
|
||||
self.0.finalize_for_history();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn new_session_info(
|
||||
@@ -237,6 +273,11 @@ pub(crate) fn has_yolo_permissions(
|
||||
}
|
||||
)
|
||||
}
|
||||
/// Header card rendered while a session is configuring and after its metadata arrives.
|
||||
///
|
||||
/// The same type is used for the dim placeholder and configured header so both layouts follow the
|
||||
/// same mascot width and fallback rules. `logo_animation` is transient: active cells may render
|
||||
/// time-dependent frames, while committed history must be finalized to the static reference frame.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct SessionHeaderHistoryCell {
|
||||
version: &'static str,
|
||||
@@ -246,6 +287,7 @@ pub(crate) struct SessionHeaderHistoryCell {
|
||||
show_fast_status: bool,
|
||||
directory: PathBuf,
|
||||
yolo_mode: bool,
|
||||
logo_animation: Option<codex_logo::StartupAnimation>,
|
||||
}
|
||||
|
||||
impl SessionHeaderHistoryCell {
|
||||
@@ -282,6 +324,7 @@ impl SessionHeaderHistoryCell {
|
||||
show_fast_status,
|
||||
directory,
|
||||
yolo_mode: false,
|
||||
logo_animation: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,6 +333,27 @@ impl SessionHeaderHistoryCell {
|
||||
self
|
||||
}
|
||||
|
||||
/// Attaches transient startup mascot state to this header.
|
||||
///
|
||||
/// Pass through the original `StartupAnimation` when replacing a placeholder header. Creating
|
||||
/// a new value here would restart the clock and may switch the selected motion mid-startup.
|
||||
pub(crate) fn with_startup_logo_animation(
|
||||
mut self,
|
||||
animation: codex_logo::StartupAnimation,
|
||||
) -> Self {
|
||||
self.logo_animation = Some(animation);
|
||||
self
|
||||
}
|
||||
|
||||
fn finalize_logo_animation(&mut self) {
|
||||
self.logo_animation = None;
|
||||
}
|
||||
|
||||
fn logo_animation_tick(&self) -> Option<(codex_logo::StartupAnimation, u64)> {
|
||||
let animation = self.logo_animation?;
|
||||
codex_logo::animation_tick(animation).map(|tick| (animation, tick))
|
||||
}
|
||||
|
||||
fn format_directory(&self, max_width: Option<usize>) -> String {
|
||||
Self::format_directory_inner(&self.directory, max_width)
|
||||
}
|
||||
@@ -334,6 +398,12 @@ impl HistoryCell for SessionHeaderHistoryCell {
|
||||
let Some(inner_width) = card_inner_width(width, SESSION_HEADER_MAX_INNER_WIDTH) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let show_logo = inner_width > codex_logo::WIDTH + codex_logo::GAP_WIDTH;
|
||||
let text_width = if show_logo {
|
||||
inner_width.saturating_sub(codex_logo::WIDTH + codex_logo::GAP_WIDTH)
|
||||
} else {
|
||||
inner_width
|
||||
};
|
||||
|
||||
let make_row = |spans: Vec<Span<'static>>| Line::from(spans);
|
||||
|
||||
@@ -383,26 +453,69 @@ impl HistoryCell for SessionHeaderHistoryCell {
|
||||
let dir_label = format!("{DIR_LABEL:<label_width$}");
|
||||
let dir_prefix = format!("{dir_label} ");
|
||||
let dir_prefix_width = UnicodeWidthStr::width(dir_prefix.as_str());
|
||||
let dir_max_width = inner_width.saturating_sub(dir_prefix_width);
|
||||
let dir = self.format_directory(Some(dir_max_width));
|
||||
let dir_spans = vec![Span::from(dir_prefix).dim(), Span::from(dir)];
|
||||
let build_text_lines = |width: usize| {
|
||||
let dir_max_width = width.saturating_sub(dir_prefix_width);
|
||||
let dir = self.format_directory(/*max_width*/ Some(dir_max_width));
|
||||
let dir_spans = vec![Span::from(dir_prefix.clone()).dim(), Span::from(dir)];
|
||||
let mut lines = vec![
|
||||
make_row(title_spans.clone()),
|
||||
make_row(Vec::new()),
|
||||
make_row(model_spans.clone()),
|
||||
make_row(dir_spans),
|
||||
];
|
||||
|
||||
let mut lines = vec![
|
||||
make_row(title_spans),
|
||||
make_row(Vec::new()),
|
||||
make_row(model_spans),
|
||||
make_row(dir_spans),
|
||||
];
|
||||
if self.yolo_mode {
|
||||
let permissions_label = format!("{PERMISSIONS_LABEL:<label_width$}");
|
||||
lines.push(make_row(vec![
|
||||
Span::from(format!("{permissions_label} ")).dim(),
|
||||
"YOLO mode".magenta().bold(),
|
||||
]));
|
||||
}
|
||||
lines
|
||||
};
|
||||
|
||||
if self.yolo_mode {
|
||||
let permissions_label = format!("{PERMISSIONS_LABEL:<label_width$}");
|
||||
lines.push(make_row(vec![
|
||||
Span::from(format!("{permissions_label} ")).dim(),
|
||||
"YOLO mode".magenta().bold(),
|
||||
]));
|
||||
let text_lines = build_text_lines(text_width);
|
||||
// Rebuild text against the full inner width when the side-by-side layout cannot fit.
|
||||
// Reusing `text_lines` would keep the directory truncated for mascot space after the
|
||||
// mascot itself has been removed.
|
||||
if !show_logo || text_lines.iter().any(|line| line_width(line) > text_width) {
|
||||
return with_border(build_text_lines(inner_width));
|
||||
}
|
||||
|
||||
with_border(lines)
|
||||
let logo_animation_tick = self.logo_animation_tick();
|
||||
let frame = logo_animation_tick
|
||||
.map(|(animation, tick)| codex_logo::frame_for_tick(animation, tick))
|
||||
.unwrap_or(&codex_logo::STATIC_FRAME);
|
||||
let terminal_bg = terminal_palette::default_bg();
|
||||
let gradient = logo_animation_tick
|
||||
.map(|(_, tick)| codex_logo::gradient_for_animation_tick(terminal_bg, tick))
|
||||
.unwrap_or_else(|| codex_logo::gradient_for_bg(terminal_bg));
|
||||
let text_top_padding = (codex_logo::HEIGHT - text_lines.len()) / 2;
|
||||
let mut lines = Vec::with_capacity(codex_logo::HEIGHT);
|
||||
|
||||
let mut push_logo_row = |row: usize, logo_line: &str, style: Style| {
|
||||
let mut spans = vec![
|
||||
Span::styled(padded_logo_line(logo_line), style),
|
||||
Span::from(" ".repeat(codex_logo::GAP_WIDTH)),
|
||||
];
|
||||
if let Some(text_line) = row
|
||||
.checked_sub(text_top_padding)
|
||||
.and_then(|text_row| text_lines.get(text_row))
|
||||
{
|
||||
spans.extend(text_line.spans.clone());
|
||||
}
|
||||
lines.push(Line::from(spans));
|
||||
};
|
||||
|
||||
for (row, color) in gradient.into_iter().enumerate() {
|
||||
push_logo_row(
|
||||
row,
|
||||
frame[row],
|
||||
Style::default().fg(terminal_palette::best_color(color)),
|
||||
);
|
||||
}
|
||||
|
||||
with_border_with_inner_width(lines, inner_width)
|
||||
}
|
||||
|
||||
fn raw_lines(&self) -> Vec<Line<'static>> {
|
||||
@@ -425,4 +538,26 @@ impl HistoryCell for SessionHeaderHistoryCell {
|
||||
}
|
||||
lines
|
||||
}
|
||||
|
||||
fn transcript_animation_tick(&self) -> Option<u64> {
|
||||
self.logo_animation_tick().map(|(_, tick)| tick)
|
||||
}
|
||||
|
||||
fn finalize_for_history(&mut self) {
|
||||
self.finalize_logo_animation();
|
||||
}
|
||||
}
|
||||
|
||||
fn line_width(line: &Line<'_>) -> usize {
|
||||
line.iter()
|
||||
.map(|span| UnicodeWidthStr::width(span.content.as_ref()))
|
||||
.sum()
|
||||
}
|
||||
|
||||
fn padded_logo_line(line: &str) -> String {
|
||||
let width = UnicodeWidthStr::width(line);
|
||||
format!(
|
||||
"{line}{}",
|
||||
" ".repeat(codex_logo::WIDTH.saturating_sub(width))
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
---
|
||||
source: tui/src/history_cell.rs
|
||||
source: tui/src/history_cell/tests.rs
|
||||
expression: rendered
|
||||
---
|
||||
╭───────────────────────────────────────╮
|
||||
│ >_ OpenAI Codex (vtest) │
|
||||
│ │
|
||||
│ model: gpt-5 /model to change │
|
||||
│ directory: /tmp/project │
|
||||
│ permissions: YOLO mode │
|
||||
╰───────────────────────────────────────╯
|
||||
╭─────────────────────────────────────────────────────────────────────────╮
|
||||
│ ⢀⣴⣶⣶⣶⣤⣤⣄⡀ >_ OpenAI Codex (vtest) │
|
||||
│ ⢀⣴⣾⣿⣿⣿⣿⣿⣿⣿⣿⡆ │
|
||||
│ ⣿⣿⣿⣇⠙⣿⣿⣿⣿⣿⣿⣷⡀ model: gpt-5 /model to change │
|
||||
│ ⢹⣿⣿⡏⣠⣿⣟⠛⠛⣿⣿⣿⡇ directory: /tmp/project │
|
||||
│ ⢿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠋ permissions: YOLO mode │
|
||||
│ ⠉⠛⠛⠻⠿⠿⠟⠋ │
|
||||
╰─────────────────────────────────────────────────────────────────────────╯
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
---
|
||||
source: tui/src/history_cell.rs
|
||||
source: tui/src/history_cell/tests.rs
|
||||
expression: rendered
|
||||
---
|
||||
╭─────────────────────────────────────╮
|
||||
│ >_ OpenAI Codex (v0.0.0) │
|
||||
│ │
|
||||
│ model: gpt-5 /model to change │
|
||||
│ directory: /tmp/project │
|
||||
╰─────────────────────────────────────╯
|
||||
╭─────────────────────────────────────────────────────────────────────────╮
|
||||
│ ⢀⣴⣶⣶⣶⣤⣤⣄⡀ │
|
||||
│ ⢀⣴⣾⣿⣿⣿⣿⣿⣿⣿⣿⡆ >_ OpenAI Codex (v0.0.0) │
|
||||
│ ⣿⣿⣿⣇⠙⣿⣿⣿⣿⣿⣿⣷⡀ │
|
||||
│ ⢹⣿⣿⡏⣠⣿⣟⠛⠛⣿⣿⣿⡇ model: gpt-5 /model to change │
|
||||
│ ⢿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠋ directory: /tmp/project │
|
||||
│ ⠉⠛⠛⠻⠿⠿⠟⠋ │
|
||||
╰─────────────────────────────────────────────────────────────────────────╯
|
||||
|
||||
Tip: Model just became available
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Coverage for history-cell rendering, wrapping, and transcript behavior.
|
||||
|
||||
use super::*;
|
||||
use crate::codex_logo;
|
||||
use crate::exec_cell::CommandOutput;
|
||||
use crate::exec_cell::ExecCall;
|
||||
use crate::exec_cell::ExecCell;
|
||||
@@ -1473,6 +1474,7 @@ fn session_header_includes_reasoning_level_when_present() {
|
||||
|
||||
assert!(model_line.contains("gpt-4o high fast"));
|
||||
assert!(model_line.contains("/model to change"));
|
||||
assert!(lines.iter().any(|line| line.contains("⣿")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1495,6 +1497,66 @@ fn session_header_hides_fast_status_when_disabled() {
|
||||
assert!(!model_line.contains("fast"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_header_hides_logo_when_text_would_not_fit() {
|
||||
let directory = test_path_buf("/tmp/project/with/a/long/directory/name")
|
||||
.abs()
|
||||
.to_path_buf();
|
||||
let cell = SessionHeaderHistoryCell::new(
|
||||
"gpt-4o".to_string(),
|
||||
Some(ReasoningEffortConfig::High),
|
||||
/*show_fast_status*/ true,
|
||||
directory.clone(),
|
||||
"test",
|
||||
);
|
||||
|
||||
let rendered = render_lines(&cell.display_lines(/*width*/ 48)).join("\n");
|
||||
let inner_width = card_inner_width(/*width*/ 48, SESSION_HEADER_MAX_INNER_WIDTH)
|
||||
.expect("session header should fit");
|
||||
let full_width_directory = SessionHeaderHistoryCell::format_directory_inner(
|
||||
&directory,
|
||||
/*max_width*/ Some(inner_width.saturating_sub("directory: ".len())),
|
||||
);
|
||||
let logo_width_directory = SessionHeaderHistoryCell::format_directory_inner(
|
||||
&directory,
|
||||
/*max_width*/
|
||||
Some(
|
||||
inner_width
|
||||
.saturating_sub(codex_logo::WIDTH + codex_logo::GAP_WIDTH)
|
||||
.saturating_sub("directory: ".len()),
|
||||
),
|
||||
);
|
||||
|
||||
assert!(rendered.contains("OpenAI Codex"));
|
||||
assert!(!rendered.contains("⣿"));
|
||||
assert!(rendered.contains(&full_width_directory));
|
||||
assert_ne!(full_width_directory, logo_width_directory);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn session_info_logo_animation_is_independent_from_welcome_banner() {
|
||||
let mut config = test_config().await;
|
||||
config.animations = true;
|
||||
let mut cell = new_session_info(
|
||||
&config,
|
||||
"gpt-5",
|
||||
&session_configured_event("gpt-5"),
|
||||
/*is_first_event*/ false,
|
||||
/*tooltip_override*/ None,
|
||||
/*auth_plan*/ None,
|
||||
/*show_fast_status*/ false,
|
||||
)
|
||||
.with_startup_logo_animation(codex_logo::startup_animation());
|
||||
|
||||
assert!(cell.transcript_animation_tick().is_some());
|
||||
let rendered = render_transcript(&cell).join("\n");
|
||||
assert!(rendered.contains("⣿"));
|
||||
assert!(!rendered.contains("To get started"));
|
||||
|
||||
cell.finalize_for_history();
|
||||
assert!(cell.transcript_animation_tick().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(
|
||||
target_os = "windows",
|
||||
|
||||
@@ -116,6 +116,8 @@ mod chatwidget;
|
||||
mod cli;
|
||||
mod clipboard_copy;
|
||||
mod clipboard_paste;
|
||||
mod codex_logo;
|
||||
mod codex_logo_frames;
|
||||
mod collaboration_modes;
|
||||
mod color;
|
||||
mod config_update;
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
---
|
||||
source: tui/src/app.rs
|
||||
source: tui/src/app/tests.rs
|
||||
expression: rendered
|
||||
---
|
||||
╭─────────────────────────────────────────────╮
|
||||
│ >_ OpenAI Codex (v<VERSION>) │
|
||||
│ │
|
||||
│ model: gpt-test high /model to change │
|
||||
│ directory: /tmp/project │
|
||||
╰─────────────────────────────────────────────╯
|
||||
╭─────────────────────────────────────────────────────────────────────────╮
|
||||
│ ⢀⣴⣶⣶⣶⣤⣤⣄⡀ │
|
||||
│ ⢀⣴⣾⣿⣿⣿⣿⣿⣿⣿⣿⡆ >_ OpenAI Codex (v<VERSION>) │
|
||||
│ ⣿⣿⣿⣇⠙⣿⣿⣿⣿⣿⣿⣷⡀ │
|
||||
│ ⢹⣿⣿⡏⣠⣿⣟⠛⠛⣿⣿⣿⡇ model: gpt-test high /model to change │
|
||||
│ ⢿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠋ directory: /tmp/project │
|
||||
│ ⠉⠛⠛⠻⠿⠿⠟⠋ │
|
||||
╰─────────────────────────────────────────────────────────────────────────╯
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
---
|
||||
source: tui/src/app.rs
|
||||
source: tui/src/app/tests.rs
|
||||
expression: rendered
|
||||
---
|
||||
╭────────────────────────────────────────────────────╮
|
||||
│ >_ OpenAI Codex (v<VERSION>) │
|
||||
│ │
|
||||
│ model: gpt-5.4 xhigh fast /model to change │
|
||||
│ directory: /tmp/project │
|
||||
╰────────────────────────────────────────────────────╯
|
||||
╭─────────────────────────────────────────────────────────────────────────╮
|
||||
│ ⢀⣴⣶⣶⣶⣤⣤⣄⡀ │
|
||||
│ ⢀⣴⣾⣿⣿⣿⣿⣿⣿⣿⣿⡆ >_ OpenAI Codex (v<VERSION>) │
|
||||
│ ⣿⣿⣿⣇⠙⣿⣿⣿⣿⣿⣿⣷⡀ │
|
||||
│ ⢹⣿⣿⡏⣠⣿⣟⠛⠛⣿⣿⣿⡇ model: gpt-5.4 xhigh fast /model to change │
|
||||
│ ⢿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠋ directory: /tmp/project │
|
||||
│ ⠉⠛⠛⠻⠿⠿⠟⠋ │
|
||||
╰─────────────────────────────────────────────────────────────────────────╯
|
||||
|
||||
Reference in New Issue
Block a user