diff --git a/codex-rs/tui/src/bottom_pane/status_line_setup.rs b/codex-rs/tui/src/bottom_pane/status_line_setup.rs index cdda2d7ca4f1..ed4f76c1d079 100644 --- a/codex-rs/tui/src/bottom_pane/status_line_setup.rs +++ b/codex-rs/tui/src/bottom_pane/status_line_setup.rs @@ -14,7 +14,7 @@ //! - Git information (branch name) //! - Context usage (meter, window size) //! - Usage limits (5-hour, weekly) -//! - Session info (ID, tokens used) +//! - Session info (thread title, ID, tokens used) //! - Application version use ratatui::buffer::Buffer; @@ -98,6 +98,9 @@ pub(crate) enum StatusLineItem { /// Whether Fast mode is currently active. FastMode, + + /// Current thread title (if set by user). + ThreadTitle, } impl StatusLineItem { @@ -129,6 +132,7 @@ impl StatusLineItem { "Current session identifier (omitted until session starts)" } StatusLineItem::FastMode => "Whether Fast mode is currently active", + StatusLineItem::ThreadTitle => "Current thread title (omitted unless changed by user)", } } } @@ -149,6 +153,7 @@ const SELECTABLE_STATUS_LINE_ITEMS: &[StatusLineItem] = &[ StatusLineItem::TotalOutputTokens, StatusLineItem::SessionId, StatusLineItem::FastMode, + StatusLineItem::ThreadTitle, ]; /// Runtime values used to preview the current status-line selection. @@ -380,6 +385,33 @@ mod tests { ); } + #[test] + fn preview_includes_thread_title() { + let preview_data = StatusLinePreviewData::from_iter([ + (StatusLineItem::ModelName, "gpt-5".to_string()), + (StatusLineItem::ThreadTitle, "Roadmap cleanup".to_string()), + ]); + let items = vec![ + MultiSelectItem { + id: StatusLineItem::ModelName.to_string(), + name: String::new(), + description: None, + enabled: true, + }, + MultiSelectItem { + id: StatusLineItem::ThreadTitle.to_string(), + name: String::new(), + description: None, + enabled: true, + }, + ]; + + assert_eq!( + preview_data.line_for_items(&items), + Some(Line::from("gpt-5 · Roadmap cleanup")) + ); + } + #[test] fn setup_view_snapshot_uses_runtime_preview_values() { let (tx_raw, _rx) = unbounded_channel::(); diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 74b203289239..67bf8331dd96 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -2151,6 +2151,7 @@ impl ChatWidget { } self.thread_name = event.thread_name; self.refresh_terminal_title(); + self.refresh_status_surfaces(); self.request_redraw(); } } diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__renamed_thread_footer_title.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__renamed_thread_footer_title.snap new file mode 100644 index 000000000000..e024f534aee9 --- /dev/null +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__renamed_thread_footer_title.snap @@ -0,0 +1,9 @@ +--- +source: tui/src/chatwidget/tests.rs +expression: terminal.backend() +--- +" " +" " +"› Ask Codex to do anything " +" " +" gpt-5.3-codex high · Roadmap cleanup " diff --git a/codex-rs/tui/src/chatwidget/status_surfaces.rs b/codex-rs/tui/src/chatwidget/status_surfaces.rs index bd58fd4e8fc6..078d31617515 100644 --- a/codex-rs/tui/src/chatwidget/status_surfaces.rs +++ b/codex-rs/tui/src/chatwidget/status_surfaces.rs @@ -495,6 +495,10 @@ impl ChatWidget { "Fast off".to_string() }, ), + StatusLineItem::ThreadTitle => self.thread_name.as_ref().and_then(|name| { + let trimmed = name.trim(); + (!trimmed.is_empty()).then(|| trimmed.to_string()) + }), } } diff --git a/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs b/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs index 50de7064cc5c..20c0427f8f0e 100644 --- a/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs +++ b/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs @@ -1198,6 +1198,42 @@ async fn status_line_model_with_reasoning_plan_mode_footer_snapshot() { ); } +#[tokio::test] +async fn renamed_thread_footer_title_snapshot() { + use ratatui::Terminal; + use ratatui::backend::TestBackend; + + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.3-codex")).await; + chat.show_welcome_banner = false; + chat.config.tui_status_line = Some(vec![ + "model-with-reasoning".to_string(), + "thread-title".to_string(), + ]); + chat.set_reasoning_effort(Some(ReasoningEffortConfig::High)); + chat.refresh_status_line(); + + let thread_id = ThreadId::new(); + chat.thread_id = Some(thread_id); + chat.handle_codex_event(Event { + id: "rename".to_string(), + msg: EventMsg::ThreadNameUpdated(codex_protocol::protocol::ThreadNameUpdatedEvent { + thread_id, + thread_name: Some("Roadmap cleanup".to_string()), + }), + }); + + let width = 80; + let height = chat.desired_height(width); + let mut terminal = Terminal::new(TestBackend::new(width, height)).expect("create terminal"); + terminal + .draw(|f| chat.render(f.area(), f.buffer_mut())) + .expect("draw renamed-thread footer"); + assert_chatwidget_snapshot!( + "renamed_thread_footer_title", + normalized_backend_snapshot(terminal.backend()) + ); +} + #[tokio::test] async fn status_line_model_with_reasoning_fast_footer_snapshot() { use ratatui::Terminal;