Compare commits

...

4 Commits

Author SHA1 Message Date
Felipe Coury
2ccf93e177 feat(tui): shrink session header logo 2026-04-27 13:16:22 -03:00
Felipe Coury
b5b3d6d856 fix(tui): gate session logo on header width 2026-04-25 22:14:15 -03:00
Felipe Coury
a166d6d03f fix(tui): darken codex logo on light backgrounds
Adjust the light-theme logo palette to use deeper indigo and cobalt
tones so the braille mark keeps enough contrast on white terminals.
2026-04-25 16:49:28 -03:00
Felipe Coury
1bb45a8bb7 feat(tui): add codex logo to session header
Render the startup session header with the braille Codex logo when
the terminal is wide enough, while keeping the existing text-only
layout as the fallback for narrower terminals.

Choose the blue gradient from the queried terminal background so the
mark stays legible on both light and dark themes.
2026-04-25 13:30:11 -03:00
5 changed files with 199 additions and 30 deletions

View File

@@ -21,6 +21,7 @@ use crate::exec_command::relativize_to_home;
use crate::exec_command::strip_bash_lc_and_escape;
use crate::legacy_core::config::Config;
use crate::legacy_core::web_search_detail;
use crate::line_truncation::line_width;
use crate::live_wrap::take_prefix_by_width;
use crate::markdown::append_markdown;
use crate::render::line_utils::line_to_static;
@@ -29,6 +30,7 @@ use crate::render::line_utils::push_owned_lines;
use crate::render::renderable::Renderable;
use crate::style::proposed_plan_style;
use crate::style::user_message_style;
use crate::terminal_palette;
#[cfg(test)]
use crate::test_support::PathBufExt;
#[cfg(test)]
@@ -1049,7 +1051,51 @@ impl HistoryCell for CompletedMcpToolCallWithImageOutput {
}
}
pub(crate) const SESSION_HEADER_MAX_INNER_WIDTH: usize = 56; // Just an eyeballed value
const CODEX_LOGO_LINES: [&str; 6] = [
" ⢀⣴⣶⣶⣦⣤⣤⣀",
"⢀⣴⣾⣿⣿⣿⣿⣿⣿⣿⣷",
"⣾⣿⣷⡀⢻⣿⣿⣿⣿⣿⣿⡄",
"⠘⣿⣿⠁⣼⣿⠛⠛⠛⣿⣿⣿",
" ⣿⣿⣿⣿⣿⣿⣿⣿⣿⠟⠁",
" ⠈⠛⠛⠻⢿⣿⣿⠿⠃",
];
const CODEX_LOGO_WIDTH: usize = 12;
const CODEX_LOGO_GAP_WIDTH: usize = 2;
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;
const CODEX_LOGO_BRIGHT_GRADIENT: [(u8, u8, u8); 6] = [
(169, 162, 255),
(143, 161, 255),
(116, 146, 255),
(85, 114, 255),
(68, 85, 252),
(49, 66, 245),
];
const CODEX_LOGO_DARK_GRADIENT: [(u8, u8, u8); 6] = [
(82, 72, 190),
(63, 91, 210),
(45, 110, 224),
(34, 92, 213),
(29, 66, 188),
(24, 43, 155),
];
fn codex_logo_gradient_for_bg(terminal_bg: Option<(u8, u8, u8)>) -> [(u8, u8, u8); 6] {
if terminal_bg.is_some_and(crate::color::is_light) {
CODEX_LOGO_DARK_GRADIENT
} else {
CODEX_LOGO_BRIGHT_GRADIENT
}
}
fn padded_logo_line(line: &str) -> String {
let width = UnicodeWidthStr::width(line);
format!(
"{line}{}",
" ".repeat(CODEX_LOGO_WIDTH.saturating_sub(width))
)
}
pub(crate) fn card_inner_width(width: u16, max_inner_width: usize) -> Option<usize> {
if width < 4 {
@@ -1383,9 +1429,20 @@ impl SessionHeaderHistoryCell {
impl HistoryCell for SessionHeaderHistoryCell {
fn display_lines(&self, width: u16) -> Vec<Line<'static>> {
let Some(inner_width) = card_inner_width(width, SESSION_HEADER_MAX_INNER_WIDTH) else {
let show_logo = usize::from(width) >= SESSION_HEADER_MAX_INNER_WIDTH + 4;
let max_inner_width = if show_logo {
SESSION_HEADER_MAX_INNER_WIDTH
} else {
SESSION_HEADER_TEXT_MAX_INNER_WIDTH
};
let Some(inner_width) = card_inner_width(width, max_inner_width) else {
return Vec::new();
};
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);
@@ -1435,11 +1492,11 @@ 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_max_width = text_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 mut lines = vec![
let mut text_lines = vec![
make_row(title_spans),
make_row(Vec::new()),
make_row(model_spans),
@@ -1448,12 +1505,35 @@ impl HistoryCell for SessionHeaderHistoryCell {
if self.yolo_mode {
let permissions_label = format!("{PERMISSIONS_LABEL:<label_width$}");
lines.push(make_row(vec![
text_lines.push(make_row(vec![
Span::from(format!("{permissions_label} ")).dim(),
"YOLO mode".magenta().bold(),
]));
}
if !show_logo || text_lines.iter().any(|line| line_width(line) > text_width) {
return with_border(text_lines);
}
let logo_gradient = codex_logo_gradient_for_bg(terminal_palette::default_bg());
let text_top_padding = CODEX_LOGO_LINES.len().saturating_sub(text_lines.len()) / 2;
let mut lines = Vec::with_capacity(CODEX_LOGO_LINES.len());
for (idx, logo) in CODEX_LOGO_LINES.iter().enumerate() {
let mut spans = vec![
Span::styled(
padded_logo_line(logo),
Style::default().fg(terminal_palette::best_color(logo_gradient[idx])),
),
Span::from(" ".repeat(CODEX_LOGO_GAP_WIDTH)).dim(),
];
if let Some(text_idx) = idx.checked_sub(text_top_padding)
&& let Some(text_line) = text_lines.get(text_idx)
{
spans.extend(text_line.spans.clone());
}
lines.push(Line::from(spans));
}
with_border(lines)
}
}
@@ -4059,6 +4139,84 @@ mod tests {
assert!(!model_line.contains("fast"));
}
#[test]
fn session_header_places_logo_to_left_of_text() {
let cell = SessionHeaderHistoryCell::new(
"gpt-5".to_string(),
Some(ReasoningEffortConfig::High),
/*show_fast_status*/ false,
test_path_buf("/tmp/project").abs().to_path_buf(),
"test",
);
let lines = render_lines(&cell.display_lines(/*width*/ 100));
let title_line = lines
.iter()
.find(|line| line.contains("OpenAI Codex"))
.expect("title line");
let logo_column = title_line.find("").expect("logo");
let title_column = title_line.find("OpenAI Codex").expect("title");
assert!(logo_column < title_column);
assert_eq!(lines.len(), 8);
assert_eq!(lines[2], title_line.as_str());
}
#[test]
fn session_header_logo_gradient_uses_terminal_background() {
assert_eq!(
codex_logo_gradient_for_bg(Some((8, 8, 8))),
CODEX_LOGO_BRIGHT_GRADIENT
);
assert_eq!(
codex_logo_gradient_for_bg(Some((250, 250, 250))),
CODEX_LOGO_DARK_GRADIENT
);
assert_eq!(
codex_logo_gradient_for_bg(/*terminal_bg*/ None),
CODEX_LOGO_BRIGHT_GRADIENT
);
}
#[test]
fn session_header_falls_back_to_text_only_when_width_is_narrow() {
let cell = SessionHeaderHistoryCell::new(
"gpt-5".to_string(),
Some(ReasoningEffortConfig::High),
/*show_fast_status*/ false,
test_path_buf("/tmp/project").abs().to_path_buf(),
"test",
);
let rendered = render_lines(&cell.display_lines(/*width*/ 60)).join("\n");
assert!(rendered.contains(">_ OpenAI Codex (vtest)"));
assert!(!rendered.contains(""));
assert_eq!(rendered.lines().count(), 6);
}
#[test]
fn session_header_omits_logo_when_text_would_overflow_logo_layout() {
let cell = SessionHeaderHistoryCell::new(
"custom-model-name-with-long-label".to_string(),
Some(ReasoningEffortConfig::High),
/*show_fast_status*/ false,
test_path_buf("/tmp/project").abs().to_path_buf(),
"test",
);
let rendered = render_lines(&cell.display_lines(/*width*/ 80));
let rendered_text = rendered.join("\n");
assert!(rendered_text.contains(">_ OpenAI Codex (vtest)"));
assert!(!rendered_text.contains(""));
assert!(
rendered
.iter()
.all(|line| UnicodeWidthStr::width(line.as_str()) <= 80)
);
}
#[test]
#[cfg_attr(
target_os = "windows",

View File

@@ -1,10 +1,13 @@
---
source: tui/src/app.rs
assertion_line: 10814
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 │
│ ⠈⠛⠛⠻⢿⣿⣿⠿⠃ │
╰───────────────────────────────────────────────────────────╯

View File

@@ -1,10 +1,13 @@
---
source: tui/src/app.rs
assertion_line: 10856
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 │
│ ⠈⠛⠛⠻⢿⣿⣿⠿⠃ │
╰──────────────────────────────────────────────────────────────────╯

View File

@@ -1,11 +1,13 @@
---
source: tui/src/history_cell.rs
assertion_line: 4236
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 │
│ ⠈⠛⠛⠻⢿⣿⣿⠿⠃ │
╰─────────────────────────────────────────────────────╯

View File

@@ -1,12 +1,15 @@
---
source: tui/src/history_cell.rs
assertion_line: 3288
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