diff --git a/codex-rs/features/src/lib.rs b/codex-rs/features/src/lib.rs index 6101738b54..4493d45953 100644 --- a/codex-rs/features/src/lib.rs +++ b/codex-rs/features/src/lib.rs @@ -128,7 +128,7 @@ pub enum Feature { TuiMultiBackend, /// Enable workspace behavior for side conversations in the owned TUI. TuiWorkspace, - /// Render the configured status line through the experimental TUI Extensions renderer. + /// Enable experimental MCP status line providers. TuiExtensionsStatusLine, /// Enable negotiated zstd compression for app-server WebSocket and UDS connections. AppServerWebSocketCompression, diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs index 25300e230d..17c64be8eb 100644 --- a/codex-rs/tui/src/app/tests.rs +++ b/codex-rs/tui/src/app/tests.rs @@ -2246,9 +2246,8 @@ async fn token_usage_update_refreshes_status_line_with_runtime_context_window() token_usage_notification(thread_id, "turn-1", Some(window)), ))); assert!(matches!( - (extensions, app.chat_widget.status_line_content()), - (false, Some(FooterContent::HyperlinkLine(_))) - | (true, Some(FooterContent::Extension { .. })) + app.chat_widget.status_line_content(), + Some(FooterContent::HyperlinkLine(_)) )); let rendered = render_bottom_popup(&app.chat_widget, /*width*/ 60); assert!( diff --git a/codex-rs/tui/src/bottom_pane/chat_composer/status_surface_tests.rs b/codex-rs/tui/src/bottom_pane/chat_composer/status_surface_tests.rs index 30543e24ed..e4f2b2c28f 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer/status_surface_tests.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer/status_surface_tests.rs @@ -25,7 +25,7 @@ fn composer() -> ChatComposer { #[test] fn extension_status_line_clips_links_to_footer_layout() { let mut composer = composer(); - let tree = r#"{ + let tree: crate::ui_tree::UiTree = r#"{ "type": "row", "props": {"gap": 1}, "children": [ "plain", { "type": "row", "props": {"gap": 1}, "children": [ @@ -38,7 +38,7 @@ fn extension_status_line_clips_links_to_footer_layout() { }"# .parse() .unwrap(); - composer.set_status_line(Some(FooterContent::Extension { content: tree })); + composer.set_status_line(Some(FooterContent::from(tree))); for separate_status_line in [false, true] { for width in [12, 40] { diff --git a/codex-rs/tui/src/bottom_pane/footer/footer_content.rs b/codex-rs/tui/src/bottom_pane/footer/footer_content.rs index 5fff31b0bd..2ba57376fe 100644 --- a/codex-rs/tui/src/bottom_pane/footer/footer_content.rs +++ b/codex-rs/tui/src/bottom_pane/footer/footer_content.rs @@ -19,6 +19,12 @@ pub(crate) enum FooterContent { Extension { content: UiTree }, } +impl From for FooterContent { + fn from(content: UiTree) -> Self { + Self::Extension { content } + } +} + impl FooterContent { /// Natural display width, before the footer allocates space for the right indicator. pub(crate) fn width(&self) -> u16 { diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index a6f2e71312..7c906db0a5 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -93,7 +93,6 @@ pub(crate) use multi_select_picker::MultiSelectPicker; mod question_tests; mod questions; mod request_user_input; -pub(crate) mod status_line_content; mod status_line_setup; mod status_line_style; mod status_surface_preview; @@ -124,7 +123,6 @@ pub(crate) use footer::footer_content; pub(crate) use mcp_server_elicitation::McpServerElicitationFormRequest; pub(crate) use mcp_server_elicitation::McpServerElicitationOverlay; pub(crate) use request_user_input::RequestUserInputOverlay; -pub(crate) use status_line_style::STATUS_LINE_SEPARATOR; pub(crate) use status_line_style::StatusLineAccent; pub(crate) use status_line_style::status_line_from_segments; pub(crate) use voice_strip::VoiceStripPhase; diff --git a/codex-rs/tui/src/bottom_pane/status_line_content.rs b/codex-rs/tui/src/bottom_pane/status_line_content.rs deleted file mode 100644 index 1c16876eae..0000000000 --- a/codex-rs/tui/src/bottom_pane/status_line_content.rs +++ /dev/null @@ -1,65 +0,0 @@ -//! Builds the configured status line for both the footer and its live picker preview. - -use super::STATUS_LINE_SEPARATOR; -use super::footer_content::FooterContent; -use super::status_line_from_segments; -use super::status_line_setup::StatusLineItem; -use crate::ui_tree::LinkHref; -use crate::ui_tree::LinkProps; -use crate::ui_tree::RowProps; -use crate::ui_tree::UiNode; -use crate::ui_tree::UiTree; -use codex_protocol::ThreadId; - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub(crate) enum StatusLineRenderer { - Native, - Extension, -} - -impl StatusLineRenderer { - pub(crate) fn content( - self, - segments: impl IntoIterator, - use_theme_colors: bool, - thread_id: Option, - pull_request_url: Option<&str>, - ) -> Option { - if self == Self::Native { - return status_line_from_segments( - segments, - use_theme_colors, - thread_id, - pull_request_url, - ) - .map(FooterContent::HyperlinkLine); - } - - let mut children = Vec::new(); - for (item, text) in segments { - if !children.is_empty() { - children.push(Some(UiNode::String( - STATUS_LINE_SEPARATOR.trim().to_owned(), - ))); - } - let text = UiNode::String(text); - let node = if item == StatusLineItem::PullRequestNumber - && let Some(href) = pull_request_url.and_then(|url| LinkHref::try_from(url).ok()) - { - UiNode::Link { - props: LinkProps { href, title: None }, - children: vec![Some(text)], - } - } else { - text - }; - children.push(Some(node)); - } - let mut props = RowProps::default(); - props.gap = 1; - let tree = UiNode::Row { props, children }; - Some(FooterContent::Extension { - content: UiTree(Some(tree)), - }) - } -} 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 f99e49868c..0d7985f6ea 100644 --- a/codex-rs/tui/src/bottom_pane/status_line_setup.rs +++ b/codex-rs/tui/src/bottom_pane/status_line_setup.rs @@ -306,19 +306,14 @@ impl StatusLineSetupView { list_keymap: ListKeymap, ) -> Self { let mut used_ids = HashSet::new(); - let show_theme_colors = - preview_data.renderer == super::status_line_content::StatusLineRenderer::Native; - let mut items = Vec::new(); - if show_theme_colors { - items.push(MultiSelectItem { - id: STATUS_LINE_USE_THEME_COLORS_ITEM_ID.to_string(), - name: "Use theme colors".to_string(), - description: Some("Apply colors from the active /theme".to_string()), - enabled: use_theme_colors, - orderable: false, - section_break_after: true, - }); - } + let mut items = vec![MultiSelectItem { + id: STATUS_LINE_USE_THEME_COLORS_ITEM_ID.to_string(), + name: "Use theme colors".to_string(), + description: Some("Apply colors from the active /theme".to_string()), + enabled: use_theme_colors, + orderable: false, + section_break_after: true, + }]; if let Some(selected_items) = status_line_items.as_ref() { for id in *selected_items { @@ -352,14 +347,7 @@ impl StatusLineSetupView { Self { picker: MultiSelectPicker::builder( "Configure Status Line".to_string(), - Some( - if show_theme_colors { - "Select which items to display in the status line." - } else { - "Select items to display. The extension renderer uses plain text." - } - .to_string(), - ), + Some("Select which items to display in the status line.".to_string()), app_event_tx, ) .list_keymap(list_keymap) @@ -368,9 +356,7 @@ impl StatusLineSetupView { .on_preview_content(move |items| { let use_theme_colors = items .iter() - .find(|item| item.id == STATUS_LINE_USE_THEME_COLORS_ITEM_ID) - .map(|item| item.enabled) - .unwrap_or(use_theme_colors); + .any(|item| item.id == STATUS_LINE_USE_THEME_COLORS_ITEM_ID && item.enabled); preview_data.status_line_for_items( items .iter() @@ -379,13 +365,10 @@ impl StatusLineSetupView { use_theme_colors, ) }) - .on_confirm(move |ids, app_event| { - let use_theme_colors = if show_theme_colors { - ids.iter() - .any(|id| id == STATUS_LINE_USE_THEME_COLORS_ITEM_ID) - } else { - use_theme_colors - }; + .on_confirm(|ids, app_event| { + let use_theme_colors = ids + .iter() + .any(|id| id == STATUS_LINE_USE_THEME_COLORS_ITEM_ID); let items = ids .iter() .filter_map(|id| id.parse::().ok()) @@ -715,7 +698,7 @@ mod tests { } #[test] - fn extension_picker_previews_the_pr_link_and_preserves_the_color_preference() { + fn picker_previews_the_pr_link_and_preserves_the_color_preference() { let url = "https://example.com/pr/42"; for use_theme_colors in [false, true] { let (tx, mut rx) = unbounded_channel(); @@ -723,8 +706,6 @@ mod tests { StatusLineItem::PullRequestNumber.preview_item(), "PR #42", )]); - preview.renderer = - crate::bottom_pane::status_line_content::StatusLineRenderer::Extension; preview.pull_request_url = Some(url.to_owned()); let mut view = StatusLineSetupView::new( Some(&[StatusLineItem::PullRequestNumber.to_string()]), @@ -735,7 +716,6 @@ mod tests { ); let output = render_lines(&view, /*width*/ 80); assert!(output.contains(&crate::terminal_hyperlinks::osc8_hyperlink(url, "P"))); - assert!(!output.contains("Use theme colors")); view.handle_key_event(crossterm::event::KeyCode::Enter.into()); assert!(matches!( rx.try_recv(), diff --git a/codex-rs/tui/src/bottom_pane/status_surface_preview.rs b/codex-rs/tui/src/bottom_pane/status_surface_preview.rs index 3e56a1d49d..3018d12749 100644 --- a/codex-rs/tui/src/bottom_pane/status_surface_preview.rs +++ b/codex-rs/tui/src/bottom_pane/status_surface_preview.rs @@ -3,7 +3,7 @@ use std::collections::BTreeMap; use super::footer_content::FooterContent; use codex_protocol::ThreadId; -use super::status_line_content::StatusLineRenderer; +use super::status_line_from_segments; use super::status_line_setup::StatusLineItem; #[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)] @@ -137,7 +137,6 @@ struct PreviewValue { #[derive(Clone, Debug, Eq, PartialEq)] pub(crate) struct StatusSurfacePreviewData { pub(crate) thread_id: Option, - pub(crate) renderer: StatusLineRenderer, pub(crate) pull_request_url: Option, values: BTreeMap, } @@ -146,7 +145,6 @@ impl Default for StatusSurfacePreviewData { fn default() -> Self { let mut data = Self { thread_id: None, - renderer: StatusLineRenderer::Native, pull_request_url: None, values: BTreeMap::new(), }; @@ -254,21 +252,18 @@ impl StatusSurfacePreviewData { where I: IntoIterator, { - let mut segments = items - .into_iter() - .filter_map(|item| { - self.value_for(item.preview_item()) - .map(|value| (item, value.to_string())) - }) - .peekable(); - segments.peek()?; - self.renderer.content( + let segments = items.into_iter().filter_map(|item| { + self.value_for(item.preview_item()) + .map(|value| (item, value.to_string())) + }); + status_line_from_segments( segments, use_theme_colors, self.thread_id, self.live_value_for(StatusSurfacePreviewItem::PullRequestNumber) .and(self.pull_request_url.as_deref()), ) + .map(FooterContent::HyperlinkLine) } } diff --git a/codex-rs/tui/src/chatwidget/status_controls.rs b/codex-rs/tui/src/chatwidget/status_controls.rs index d08632d79e..4d947a952b 100644 --- a/codex-rs/tui/src/chatwidget/status_controls.rs +++ b/codex-rs/tui/src/chatwidget/status_controls.rs @@ -368,7 +368,6 @@ impl ChatWidget { }), ); preview_data.thread_id = self.thread_id; - preview_data.renderer = self.status_line_renderer(); preview_data.pull_request_url = self.status_line_pull_request_url().map(str::to_owned); if self.rate_limit_snapshots_by_limit_id.contains_key("codex") { diff --git a/codex-rs/tui/src/chatwidget/status_surfaces.rs b/codex-rs/tui/src/chatwidget/status_surfaces.rs index 9e6baf1412..00822bd494 100644 --- a/codex-rs/tui/src/chatwidget/status_surfaces.rs +++ b/codex-rs/tui/src/chatwidget/status_surfaces.rs @@ -4,7 +4,7 @@ //! behavior easier to review without paging through the rest of `chatwidget.rs`. use super::*; -use crate::bottom_pane::status_line_content::StatusLineRenderer; +use crate::bottom_pane::status_line_from_segments; use crate::branch_summary; use crate::chatwidget::limit_label_for_window; use crate::chatwidget::rate_limits::get_limits_duration; @@ -216,25 +216,13 @@ impl ChatWidget { } } - let content = self.status_line_renderer().content( + let line = status_line_from_segments( segments, self.local_settings.tui.status_line_use_colors, self.thread_id, self.status_line_pull_request_url(), ); - self.set_status_line(content); - } - - pub(super) fn status_line_renderer(&self) -> StatusLineRenderer { - if self - .config - .features - .enabled(Feature::TuiExtensionsStatusLine) - { - StatusLineRenderer::Extension - } else { - StatusLineRenderer::Native - } + self.set_status_line(line.map(Into::into)); } /// Clears the terminal title Codex most recently wrote, if any. diff --git a/codex-rs/tui/src/chatwidget/tests/status_surface_previews.rs b/codex-rs/tui/src/chatwidget/tests/status_surface_previews.rs index 64245f3f3e..70d000f94b 100644 --- a/codex-rs/tui/src/chatwidget/tests/status_surface_previews.rs +++ b/codex-rs/tui/src/chatwidget/tests/status_surface_previews.rs @@ -163,6 +163,7 @@ async fn status_surface_preview_lines_live_only_snapshot() { #[tokio::test] async fn status_line_setup_popup_live_only_snapshot() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + chat.set_feature_enabled(Feature::TuiExtensionsStatusLine, /*enabled*/ true); cache_project_root(&mut chat, "preview-live-root"); chat.status_line_branch = Some("feature/live-preview-branch".to_string()); chat.thread_name = Some("Live preview thread".to_string());