From 7e65e4f3303881ee8b042128996f159fa5b3f128 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 14 Aug 2026 20:08:48 +0000 Subject: [PATCH] Render the initial TUI session header before input (#38639) ## What changed - Drain queued app events until the initial session header is installed before handling terminal input. - Render the active session header without a leading separator so it starts at the top of the viewport. ## Testing - Add a rendering snapshot that verifies the initial header begins on the first viewport row. GitOrigin-RevId: 070417776b629c681b3f94e93ba619dfefe764b1 --- codex-rs/tui/src/app.rs | 18 ++++++++-- codex-rs/tui/src/chatwidget/rendering.rs | 7 +++- .../tui/src/chatwidget/rendering_tests.rs | 35 +++++++++++++++++++ 3 files changed, 57 insertions(+), 3 deletions(-) diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 55724a8c17..3f70f0bf01 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -1181,6 +1181,7 @@ See the Codex keymap documentation for supported actions and examples." let mut listen_for_app_server_events = true; let mut waiting_for_initial_session_configured = wait_for_initial_session_configured; + let mut waiting_for_initial_session_header = true; #[cfg(not(debug_assertions))] let pre_loop_exit_reason = if let Some(latest_version) = upgrade_version { @@ -1207,10 +1208,23 @@ See the Codex keymap documentation for supported actions and examples." Ok(exit_reason) } else { loop { + let initial_session_header_pending = waiting_for_initial_session_header + && app.primary_session_configured.is_some() + && !app_event_rx.is_empty(); let control = select! { Some(event) = app_event_rx.recv() => { + let is_initial_session_header = matches!( + &event, + AppEvent::InsertHistoryCell(cell) + if cell.as_any().is::() + ); match Box::pin(app.handle_event(tui, &mut app_server, event)).await { - Ok(control) => control, + Ok(control) => { + if is_initial_session_header { + waiting_for_initial_session_header = false; + } + control + } Err(err) => break Err(err), } } @@ -1233,7 +1247,7 @@ See the Codex keymap documentation for supported actions and examples." } AppRunControl::Continue } - event = tui_events.next() => { + event = tui_events.next(), if !initial_session_header_pending => { if let Some(event) = event { match app.handle_tui_event(tui, &mut app_server, event).await { Ok(control) => control, diff --git a/codex-rs/tui/src/chatwidget/rendering.rs b/codex-rs/tui/src/chatwidget/rendering.rs index 8b35a26c1a..37685eed6a 100644 --- a/codex-rs/tui/src/chatwidget/rendering.rs +++ b/codex-rs/tui/src/chatwidget/rendering.rs @@ -11,7 +11,12 @@ impl ChatWidget { let active_cell_renderable = match &self.transcript.active_cell { Some(cell) => RenderableItem::Owned(Box::new(TranscriptAreaRenderable { child: cell.as_ref(), - top: 1, + // The initial header becomes the first history cell, which has no leading separator. + top: if cell.as_any().is::() { + 0 + } else { + 1 + }, right: active_cell_right_reserve, // Externally backed transcript cells can also change viewport height without an // active-cell revision. Spinner cells remain safe because their indicator width diff --git a/codex-rs/tui/src/chatwidget/rendering_tests.rs b/codex-rs/tui/src/chatwidget/rendering_tests.rs index 34926b09de..e3cf4abc28 100644 --- a/codex-rs/tui/src/chatwidget/rendering_tests.rs +++ b/codex-rs/tui/src/chatwidget/rendering_tests.rs @@ -75,6 +75,41 @@ fn contains_text(buffer: &Buffer, text: &str) -> bool { }) } +#[tokio::test] +async fn initial_session_header_starts_at_the_top_of_the_viewport() { + let (mut widget, _sender, _events, _operations) = make_chatwidget_manual_with_sender().await; + widget.transcript.active_cell = + Some(ChatWidget::placeholder_session_header_cell(&widget.config)); + + let frame = render_frame(&widget, /*width*/ 48); + let header = frame + .content + .chunks(usize::from(frame.area.width)) + .take(/*n*/ 6) + .map(|row| { + row.iter() + .map(ratatui::buffer::Cell::symbol) + .collect::() + .trim_end() + .to_string() + }) + .collect::>() + .join("\n") + .replace(crate::version::CODEX_CLI_VERSION, ""); + + let cwd = widget.config.cwd.as_path().display().to_string(); + let normalized_cwd = format!("{:_ OpenAI Codex (v) │ + │ │ + │ model: loading /model to change │ + │ directory: /tmp/project │ + ╰───────────────────────────────────────╯ + "); +} + #[tokio::test] async fn active_cell_layout_reuses_heights_without_freezing_animation() { let (widget, desired_height_calls, display_lines_calls) = widget_with_counting_cell(