Skip to content
Merged
2 changes: 1 addition & 1 deletion codex-rs/tui/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ impl App {
self.on_update_reasoning_effort(effort);
}
AppEvent::UpdateModel(model) => {
self.chat_widget.set_model(model.clone());
self.chat_widget.set_model(&model);
self.config.model = model.clone();
if let Some(family) = find_family_for_model(&model) {
self.config.model_family = family;
Expand Down
39 changes: 28 additions & 11 deletions codex-rs/tui/src/chatwidget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ use self::interrupts::InterruptManager;
mod agent;
use self::agent::spawn_agent;
use self::agent::spawn_agent_from_existing;
mod session_header;
use self::session_header::SessionHeader;
use crate::streaming::controller::AppEventHistorySink;
use crate::streaming::controller::StreamController;
use codex_common::approval_presets::ApprovalPreset;
Expand Down Expand Up @@ -109,6 +111,7 @@ pub(crate) struct ChatWidget {
bottom_pane: BottomPane,
active_exec_cell: Option<ExecCell>,
config: Config,
session_header: SessionHeader,
initial_user_message: Option<UserMessage>,
token_info: Option<TokenUsageInfo>,
// Stream lifecycle controller
Expand Down Expand Up @@ -165,9 +168,11 @@ impl ChatWidget {
.set_history_metadata(event.history_log_id, event.history_entry_count);
self.conversation_id = Some(event.session_id);
let initial_messages = event.initial_messages.clone();
let model_for_header = event.model.clone();
if let Some(messages) = initial_messages {
self.replay_initial_messages(messages);
}
self.session_header.set_model(&model_for_header);
self.add_to_history(history_cell::new_session_info(
&self.config,
event,
Expand Down Expand Up @@ -609,14 +614,23 @@ impl ChatWidget {
));
}

fn layout_areas(&self, area: Rect) -> [Rect; 2] {
fn layout_areas(&self, area: Rect) -> [Rect; 3] {
let bottom_min = self.bottom_pane.desired_height(area.width).min(area.height);
let remaining = area.height.saturating_sub(bottom_min);

let active_desired = self
.active_exec_cell
.as_ref()
.map_or(0, |c| c.desired_height(area.width) + 1);
let active_height = active_desired.min(remaining);
// Note: no header area; remaining is not used beyond computing active height.

let header_height = 0u16;

Layout::vertical([
Constraint::Max(
self.active_exec_cell
.as_ref()
.map_or(0, |c| c.desired_height(area.width) + 1),
),
Constraint::Min(self.bottom_pane.desired_height(area.width)),
Constraint::Length(header_height),
Constraint::Length(active_height),
Constraint::Min(bottom_min),
])
.areas(area)
}
Expand Down Expand Up @@ -651,6 +665,7 @@ impl ChatWidget {
}),
active_exec_cell: None,
config: config.clone(),
session_header: SessionHeader::new(config.model.clone()),
initial_user_message: create_initial_user_message(
initial_prompt.unwrap_or_default(),
initial_images,
Expand Down Expand Up @@ -703,6 +718,7 @@ impl ChatWidget {
}),
active_exec_cell: None,
config: config.clone(),
session_header: SessionHeader::new(config.model.clone()),
initial_user_message: create_initial_user_message(
initial_prompt.unwrap_or_default(),
initial_images,
Expand Down Expand Up @@ -1281,8 +1297,9 @@ impl ChatWidget {
}

/// Set the model in the widget's config copy.
pub(crate) fn set_model(&mut self, model: String) {
self.config.model = model;
pub(crate) fn set_model(&mut self, model: &str) {
self.session_header.set_model(model);
self.config.model = model.to_string();
}

pub(crate) fn add_info_message(&mut self, message: String, hint: Option<String>) {
Expand Down Expand Up @@ -1403,14 +1420,14 @@ impl ChatWidget {
}

pub fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)> {
let [_, bottom_pane_area] = self.layout_areas(area);
let [_, _, bottom_pane_area] = self.layout_areas(area);
self.bottom_pane.cursor_pos(bottom_pane_area)
}
}

impl WidgetRef for &ChatWidget {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
let [active_cell_area, bottom_pane_area] = self.layout_areas(area);
let [_, active_cell_area, bottom_pane_area] = self.layout_areas(area);
(&self.bottom_pane).render(bottom_pane_area, buf);
if !active_cell_area.is_empty()
&& let Some(cell) = &self.active_exec_cell
Expand Down
16 changes: 16 additions & 0 deletions codex-rs/tui/src/chatwidget/session_header.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
pub(crate) struct SessionHeader {
model: String,
}

impl SessionHeader {
pub(crate) fn new(model: String) -> Self {
Self { model }
}

/// Updates the header's model text.
pub(crate) fn set_model(&mut self, model: &str) {
if self.model != model {
self.model = model.to_string();
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
---
source: tui/src/chatwidget/tests.rs
assertion_line: 728
expression: terminal.backend()
---
" "
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
---
source: tui/src/chatwidget/tests.rs
assertion_line: 794
expression: terminal.backend()
---
" "
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
---
source: tui/src/chatwidget/tests.rs
assertion_line: 921
expression: terminal.backend()
---
" "
Expand Down
10 changes: 7 additions & 3 deletions codex-rs/tui/src/chatwidget/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,9 @@ fn final_answer_without_newline_is_flushed_immediately() {

// Set up a VT100 test terminal to capture ANSI visual output
let width: u16 = 80;
let height: u16 = 2000;
// Increased height to keep the initial banner/help lines in view even if
// the session renders an extra header line or minor layout changes occur.
let height: u16 = 2500;
let viewport = Rect::new(0, height - 1, width, 1);
let backend = ratatui::backend::TestBackend::new(width, height);
let mut terminal = crate::custom_terminal::Terminal::with_options(backend)
Expand Down Expand Up @@ -230,6 +232,7 @@ fn make_chatwidget_manual() -> (
bottom_pane: bottom,
active_exec_cell: None,
config: cfg.clone(),
session_header: SessionHeader::new(cfg.model.clone()),
initial_user_message: None,
token_info: None,
stream: StreamController::new(cfg),
Expand Down Expand Up @@ -773,10 +776,11 @@ async fn binary_size_transcript_snapshot() {
// Consider content only after the last session banner marker. Skip the transient
// 'thinking' header if present, and start from the first non-empty content line
// that follows. This keeps the snapshot stable across sessions.
const MARKER_PREFIX: &str = ">_ You are using OpenAI Codex in ";
const MARKER_PREFIX: &str =
"Describe a task to get started or try one of the following commands:";
let last_marker_line_idx = lines
.iter()
.rposition(|l| l.starts_with(MARKER_PREFIX))
.rposition(|l| l.trim_start().starts_with(MARKER_PREFIX))
.expect("marker not found in visible output");
// Prefer the first assistant content line (blockquote '>' prefix) after the marker;
// fallback to the first non-empty, non-'thinking' line.
Expand Down
Loading
Loading