From cfc11eb483bb3adc63c8bfc74ea17b732ff7a4e9 Mon Sep 17 00:00:00 2001 From: Joe Savona Date: Sat, 7 Nov 2026 01:18:48 +0000 Subject: [PATCH] Allow extension status content to use multiple footer rows (#60032) ## Why Extension status content was limited to one row, clipping multiline text and content that wraps at narrow terminal widths. ## What changed Measure extension status content at the available width and reserve up to four rows in both shared and separate status layouts. Account for mode indicators and warnings, and remeasure when content or available width changes. Extra status rows yield to the draft and other composer surfaces when vertical space is limited. An unfocused composer keeps a single dim status row. ## Testing Add regression coverage for resizing, wrapping beside mode indicators and warnings, hyperlink preservation, limited vertical space, cursor placement, footer overrides, focus changes, theme updates, and reasoning effort changes. GitOrigin-RevId: 6a637b5cf6f34684639a92d920da4795c90abb6f --- codex-rs/tui/src/bottom_pane/chat_composer.rs | 13 +- .../chat_composer/composer_layout.rs | 67 +++-- .../bottom_pane/chat_composer/footer_state.rs | 15 +- ...ft_by_the_mode_indicator_after_resize.snap | 10 +- ..._composer_and_shared_footer_overrides.snap | 13 + .../chat_composer/status_surface.rs | 44 +++- .../chat_composer/status_surface_tests.rs | 248 ++++++++++++++++-- .../chat_composer/warning_notice.rs | 9 +- codex-rs/tui/src/ui_tree/render.rs | 1 - 9 files changed, 357 insertions(+), 63 deletions(-) create mode 100644 codex-rs/tui/src/bottom_pane/chat_composer/snapshots/codex_tui__bottom_pane__chat_composer__status_surface__tests__status_rows_yield_to_the_composer_and_shared_footer_overrides.snap diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 407e17955e..8074f3319c 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -789,6 +789,7 @@ impl ChatComposer { goal_status_indicator: None, ide_context_active: false, status_line_value: None, + measured_status_line: Default::default(), status_line_enabled: false, side_conversation_context_label: None, active_agent_label: None, @@ -4679,17 +4680,7 @@ impl ChatComposer { .render(transcript_hint_area, buf); } None => { - let hint_rect = if let Some(warning_area) = warning_area { - Rect { - width: warning_area - .x - .saturating_sub(footer_rect.x) - .saturating_sub(/*rhs*/ 2), - ..footer_rect - } - } else { - footer_rect - }; + let hint_rect = warning_notice::before_warning(footer_rect, warning_area); if let Some(input) = self.draft.textarea.vim_query() { input.render(inset_footer_hint_area(hint_rect), buf); } else if let Some(line) = self.history_search_footer_line() { diff --git a/codex-rs/tui/src/bottom_pane/chat_composer/composer_layout.rs b/codex-rs/tui/src/bottom_pane/chat_composer/composer_layout.rs index d687ed06a8..bf5cffc9cb 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer/composer_layout.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer/composer_layout.rs @@ -41,9 +41,23 @@ impl ChatComposer { area: Rect, options: ComposerRenderOptions<'_>, ) -> ComposerLayout { - let footer_hint_height = self.footer_hint_height(area.width, options); - // Preserve input and hints before allocating the persistent status row. - let status_height = self.status_surface_height(options).min( + let mut footer_hint_height = self.footer_hint_height(area.width, options); + let mut status_height = self.status_surface_height(area.width, options); + let shared = footer_hint_height > 1 && self.shows_shared_status(options); + if shared || status_height > 1 { + // Extra status rows yield to the draft, voice strip and other composer surfaces. + let desired = + self.height_with_rows(area.width, options, footer_hint_height, status_height); + let rows = if shared { + &mut footer_hint_height + } else { + &mut status_height + }; + *rows = rows + .saturating_sub(desired.saturating_sub(area.height)) + .max(1); + } + let status_height = status_height.min( area.height .saturating_sub(footer_hint_height.saturating_add(/*rhs*/ 3)), ); @@ -195,7 +209,21 @@ impl ChatComposer { width: u16, options: ComposerRenderOptions<'_>, ) -> u16 { - let footer_hint_height = self.footer_hint_height(width, options); + self.height_with_rows( + width, + options, + self.footer_hint_height(width, options), + self.status_surface_height(width, options), + ) + } + + fn height_with_rows( + &self, + width: u16, + options: ComposerRenderOptions<'_>, + footer_hint_height: u16, + status_height: u16, + ) -> u16 { const COLS_WITH_MARGIN: u16 = LIVE_PREFIX_COLS + 1; let inner_width = width.saturating_sub(COLS_WITH_MARGIN.saturating_add(options.textarea_right_reserve)); @@ -206,22 +234,22 @@ impl ChatComposer { .try_into() .unwrap_or(u16::MAX); let remote_images_separator = u16::from(remote_images_height > 0); - self.draft.textarea.desired_height(inner_width) - + remote_images_height - + self.thumbnail_height(inner_width, options) - + remote_images_separator - + self.status_surface_height(options) - + if self.shortcuts_above_composer(options) { + [ + self.draft.textarea.desired_height(inner_width), + remote_images_height, + self.thumbnail_height(inner_width, options), + remote_images_separator, + status_height, + if self.shortcuts_above_composer(options) { footer_height(&self.hint_footer_props(options), width) } else { 0 - } - + 2 - + self - .voice_strip - .as_ref() - .map_or(0, |strip| strip.desired_height(width) + 1) - + if self.popups.active.is_above_composer() + }, + 2, + self.voice_strip.as_ref().map_or(/*default*/ 0, |strip| { + strip.desired_height(width).saturating_add(1) + }), + if self.popups.active.is_above_composer() && options.command_popup_placement != CommandPopupPlacement::AboveComposer { footer_hint_height @@ -229,6 +257,9 @@ impl ChatComposer { self.popups .active .required_height(width, footer_hint_height) - } + }, + ] + .into_iter() + .fold(0, u16::saturating_add) } } diff --git a/codex-rs/tui/src/bottom_pane/chat_composer/footer_state.rs b/codex-rs/tui/src/bottom_pane/chat_composer/footer_state.rs index a0d7e70d2f..b70e740c3c 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer/footer_state.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer/footer_state.rs @@ -17,6 +17,7 @@ use std::time::Instant; use super::super::footer::footer_height; use super::super::footer::reset_mode_after_activity; use super::ActivePopup; +use super::warning_notice; use crate::bottom_pane::footer::FooterKeyHints; use crate::bottom_pane::footer::FooterProps; use crate::bottom_pane::footer_content::FooterContent; @@ -283,6 +284,9 @@ impl super::ChatComposer { } pub(super) fn footer_hint_height(&self, width: u16, options: ComposerRenderOptions<'_>) -> u16 { + if self.shows_shared_status(options) { + return self.status_line_height(width, options); + } if self.show_warning_notice(options) || self.shortcuts_above_composer(options) { return 1; } @@ -300,11 +304,12 @@ impl super::ChatComposer { pub(super) fn render_transcript_footer( &self, - mut hint_area: Rect, + hint_area: Rect, buf: &mut Buffer, footer: &TranscriptFooter, warning_area: Option, ) { + let hint_area = warning_notice::before_warning(hint_area, warning_area); let mut text = footer.text.clone(); if footer.cursor_column.is_none() && self.footer.flash_visible() @@ -313,12 +318,6 @@ impl super::ChatComposer { { *line = flash.line.clone(); } - if let Some(warning_area) = warning_area { - hint_area.width = warning_area - .x - .saturating_sub(hint_area.x) - .saturating_sub(/*rhs*/ 2); - } if !footer.is_interactive && !self.footer.flash_visible() && self.footer_mode() == FooterMode::ComposerEmpty @@ -366,6 +365,8 @@ pub(super) struct FooterState { pub(super) goal_status_indicator: Option, pub(super) ide_context_active: bool, pub(super) status_line_value: Option, + // Available width and occupied height, shared by layout and cursor placement. + pub(super) measured_status_line: std::cell::Cell>, pub(super) status_line_enabled: bool, pub(super) side_conversation_context_label: Option, pub(super) active_agent_label: Option, diff --git a/codex-rs/tui/src/bottom_pane/chat_composer/snapshots/codex_tui__bottom_pane__chat_composer__status_surface__tests__extension_layout_uses_the_width_left_by_the_mode_indicator_after_resize.snap b/codex-rs/tui/src/bottom_pane/chat_composer/snapshots/codex_tui__bottom_pane__chat_composer__status_surface__tests__extension_layout_uses_the_width_left_by_the_mode_indicator_after_resize.snap index 49953ae4bf..b71c9c9963 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer/snapshots/codex_tui__bottom_pane__chat_composer__status_surface__tests__extension_layout_uses_the_width_left_by_the_mode_indicator_after_resize.snap +++ b/codex-rs/tui/src/bottom_pane/chat_composer/snapshots/codex_tui__bottom_pane__chat_composer__status_surface__tests__extension_layout_uses_the_width_left_by_the_mode_indicator_after_resize.snap @@ -1,6 +1,10 @@ --- source: tui/src/bottom_pane/chat_composer/status_surface_tests.rs -expression: "format!(\"{wide}\\n{narrow}\")" +expression: shared --- -branch PR #42 Plan mode (⇧tab to cycle) -branch… Plan mode +16 | Ready PR 12345 +15 | Ready +15 | PR 12345 +16 | Ready PR 12345 +25 | Ready +25 | PR 12345 Plan mode diff --git a/codex-rs/tui/src/bottom_pane/chat_composer/snapshots/codex_tui__bottom_pane__chat_composer__status_surface__tests__status_rows_yield_to_the_composer_and_shared_footer_overrides.snap b/codex-rs/tui/src/bottom_pane/chat_composer/snapshots/codex_tui__bottom_pane__chat_composer__status_surface__tests__status_rows_yield_to_the_composer_and_shared_footer_overrides.snap new file mode 100644 index 0000000000..b407541488 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/chat_composer/snapshots/codex_tui__bottom_pane__chat_composer__status_surface__tests__status_rows_yield_to_the_composer_and_shared_footer_overrides.snap @@ -0,0 +1,13 @@ +--- +source: tui/src/bottom_pane/chat_composer/status_surface_tests.rs +expression: rows +--- +[ + " ", + "› a ", + " b ", + " c ", + " ", + " one… ", + " ", +] diff --git a/codex-rs/tui/src/bottom_pane/chat_composer/status_surface.rs b/codex-rs/tui/src/bottom_pane/chat_composer/status_surface.rs index 0b772629e2..e735f0fa2c 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer/status_surface.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer/status_surface.rs @@ -3,6 +3,9 @@ //! Shortcut help grows above the input, preserving status and close rows below it even when clipped. use super::*; +use ratatui::layout::Size; + +const MAX_FOOTER_ROWS: u16 = 4; impl ChatComposer { pub(crate) fn set_status_line(&mut self, status_line: Option) -> bool { @@ -12,6 +15,7 @@ impl ChatComposer { if matches!(status_line, Some(FooterContent::Extension { .. })) { self.effort_status_line_transition = None; } + self.footer.measured_status_line.set(None); self.footer.status_line_value = status_line; true } @@ -44,8 +48,44 @@ impl ChatComposer { && self.footer_mode().shows_passive_line(self.is_task_running) } - pub(super) fn status_surface_height(&self, options: ComposerRenderOptions<'_>) -> u16 { - u16::from(self.has_separate_status(options)) + pub(super) fn status_surface_height( + &self, + width: u16, + options: ComposerRenderOptions<'_>, + ) -> u16 { + if self.has_separate_status(options) { + self.status_line_height(width, options) + } else { + 0 + } + } + + pub(super) fn status_line_height(&self, width: u16, options: ComposerRenderOptions<'_>) -> u16 { + // The unfocused composer paints one dim status line. + if options.unfocused { + return 1; + } + let Some(FooterContent::Extension { content }) = &self.footer.status_line_value else { + return 1; + }; + let mut area = Rect::new(/*x*/ 0, /*y*/ 0, width, /*height*/ 1); + if !options.separate_status_line { + let warning = self.warning_notice_layout(inset_footer_hint_area(area), options); + area = warning_notice::before_warning(area, warning.map(|(rect, _)| rect)); + } + let (_, width) = self.status_line_layout(area, options, content.measure_natural().width); + let cache = &self.footer.measured_status_line; + if let Some((previous_width, height)) = cache.get() + && previous_width == width + { + return height; + } + let height = content + .measure(Size::new(width, MAX_FOOTER_ROWS)) + .height + .max(1); + cache.set(Some((width, height))); + height } pub(super) fn hint_footer_props(&self, options: ComposerRenderOptions<'_>) -> FooterProps { 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 805f31490e..acce3a30f1 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 @@ -2,6 +2,7 @@ use super::*; use crate::terminal_hyperlinks::HyperlinkLine; +use crate::terminal_hyperlinks::osc8_hyperlink; use crate::terminal_hyperlinks::strip_osc8; use pretty_assertions::assert_eq; use tokio::sync::mpsc::unbounded_channel; @@ -59,7 +60,15 @@ fn extension_status_line_clips_links_to_footer_layout() { .iter() .filter(|cell| cell.symbol().contains("https://example.com/pr/42")) .collect::>(); - assert_eq!(linked.len(), if width == 12 { 1 } else { 6 }, "{buffer:?}"); + let linked_text: String = linked + .iter() + .map(|cell| strip_osc8(cell.symbol())) + .collect(); + assert_eq!( + linked_text, + if width == 12 { "PR#" } else { "PR #42" }, + "{buffer:?}" + ); assert!(linked.iter().all(|cell| !cell.symbol().contains('…'))); assert!(buffer.content.iter().any(|cell| cell.symbol() == "p")); } @@ -71,38 +80,237 @@ fn extension_status_line_clips_links_to_footer_layout() { assert!(!output.contains("bot"), "{output}"); } +fn status_rows( + composer: &ChatComposer, + area: Rect, + options: ComposerRenderOptions<'_>, +) -> Vec { + let layout = composer.layout_with_options(area, options); + let status = if options.separate_status_line { + layout.status + } else { + layout.footer + }; + let mut buf = Buffer::empty(area); + composer.render_with_options(area, &mut buf, /*mask_char*/ None, options); + (status.y..status.bottom()) + .map(|y| { + (status.x..status.right()) + .map(|x| buf[(x, y)].symbol()) + .collect::() + .trim() + .to_owned() + }) + .collect() +} + #[test] fn extension_layout_uses_the_width_left_by_the_mode_indicator_after_resize() { let mut composer = composer(); - composer.set_collaboration_modes_enabled(/*enabled*/ true); - composer.set_collaboration_mode_indicator(Some(CollaborationModeIndicator::Plan)); + composer.footer.show_warnings_key = None; let tree: crate::ui_tree::UiTree = - r#"{"type":"row","props":{"gap":1,"wrap":"wrap"},"children":[ - "branch", {"type":"link","props":{"href":"https://example.com/pr/42"},"children":["PR #42"]} - ]}"# - .parse() - .unwrap(); + r#"{"type":"row","props":{"wrap":"wrap","gap":1},"children":[ + "Ready", {"type":"row","props":{"gap":1},"children":["PR", + {"type":"link","props":{"href":"https://example.com/pr/12345"},"children":["12345"]}]}]}"# + .parse() + .unwrap(); composer.set_status_line(Some(FooterContent::from(tree))); + composer.set_collaboration_modes_enabled(/*enabled*/ true); + let linked_number: String = "12345" + .chars() + .map(|digit| osc8_hyperlink("https://example.com/pr/12345", &digit.to_string())) + .collect(); + let [shared, separate] = [false, true].map(|separate_status_line| { + let mut frames = Vec::new(); + for (width, mode, warning_count) in [ + (16, None, 0), + (15, None, 0), + (16, None, 0), + (25, Some(CollaborationModeIndicator::Plan), 0), + (19, None, 1), + ] { + composer.set_collaboration_mode_indicator(mode); + let options = ComposerRenderOptions { + separate_status_line, + warning_count, + ..Default::default() + }; + let height = composer.desired_height_with_options(width, options); + let area = Rect::new(/*x*/ 0, /*y*/ 0, width, height); + let rows = status_rows(&composer, area, options); + for row in &rows { + let text = strip_osc8(row); + assert_eq!(*row, text.replace("12345", &linked_number)); + if warning_count == 0 { + frames.push(format!("{width} | {text}")); + } + } + if warning_count > 0 { + assert_eq!( + composer.footer.warning_notice_area.get().unwrap().bottom(), + height + ); + assert_eq!(rows.len(), if separate_status_line { 1 } else { 2 }); + let last = strip_osc8(rows.last().unwrap()); + assert!(last.contains("PR 12345")); + assert_eq!(last.contains("⚠ 1"), !separate_status_line); + } + } + frames.join("\n") + }); + assert_eq!(shared, separate); + insta::assert_snapshot!(shared); +} - let hosts = [false, true].map(|separate_status_line| { +#[test] +fn status_rows_yield_to_the_composer_and_shared_footer_overrides() { + let mut composer = composer(); + let tree: crate::ui_tree::UiTree = r#""one\ntwo\nthree\nfour\nfive""#.parse().unwrap(); + composer.set_status_line(Some(FooterContent::from(tree))); + composer.set_text_content("a\nb\nc".into(), Vec::new(), Vec::new()); + for separate_status_line in [false, true] { let options = ComposerRenderOptions { separate_status_line, ..Default::default() }; - [55, 24, 55].map(|width| { - let height = composer.desired_height_with_options(width, options); - let area = Rect::new(0, 0, width, height); + let short = if separate_status_line { + "one…" + } else { + "one\ntwo…" + }; + for (height, expected) in [(12, "one\ntwo\nthree\nfour…"), (7, short)] { + let area = Rect::new(/*x*/ 0, /*y*/ 0, /*width*/ 7, height); + assert_eq!(status_rows(&composer, area, options).join("\n"), expected); + let input = composer.layout_with_options(area, options).textarea; let mut buffer = Buffer::empty(area); composer.render_with_options(area, &mut buffer, /*mask_char*/ None, options); - let y = height - 1 - u16::from(separate_status_line); - let row: String = (0..width).map(|x| buffer[(x, y)].symbol()).collect(); - strip_osc8(&row).trim().to_owned() - }) + assert!(input.height >= 3); + let draft = [0, 1, 2].map(|dy| buffer[(input.x, input.y + dy)].symbol()); + assert_eq!(draft, ["a", "b", "c"]); + let (x, y) = composer + .cursor_pos_with_options(area, options) + .expect("visible draft cursor"); + assert!(input.contains((x, y).into())); + if separate_status_line && height == 7 { + let rows: Vec = buffer + .content + .chunks(usize::from(area.width)) + .map(|row| row.iter().map(ratatui::buffer::Cell::symbol).collect()) + .collect(); + insta::assert_debug_snapshot!(rows); + } + } + } + let tree: crate::ui_tree::UiTree = r#""x\ny""#.parse().unwrap(); + composer.set_status_line(Some(FooterContent::from(tree))); + assert_eq!( + composer.footer_hint_height(/*width*/ 7, ComposerRenderOptions::default()), + 2 + ); + composer.show_footer_flash("Copied".into(), Duration::from_secs(/*secs*/ 3)); + assert_eq!( + composer.footer_hint_height(/*width*/ 7, ComposerRenderOptions::default()), + 1 + ); +} + +#[test] +fn sidebar_focus_collapses_extension_status_to_its_dim_line() { + let mut composer = composer(); + composer.insert_str("draft"); + let tree: crate::ui_tree::UiTree = r#""one\ntwo\nthree\nfour""#.parse().unwrap(); + composer.set_status_line(Some(tree.into())); + let frames = [false, true, false].map(|unfocused| { + let options = ComposerRenderOptions { + separate_status_line: true, + unfocused, + ..Default::default() + }; + let height = composer.desired_height_with_options(/*width*/ 40, options); + status_rows(&composer, Rect::new(0, 0, 40, height), options).join("\n") }); - let [wide, narrow, restored] = &hosts[0]; - assert_eq!(hosts[0], hosts[1]); - assert_eq!(wide, restored); - insta::assert_snapshot!(format!("{wide}\n{narrow}")); + insta::assert_snapshot!(frames.join("\n---\n"), @" + one + two + three + four + --- + one… + --- + one + two + three + four + "); +} + +#[test] +fn extension_status_paints_with_the_current_theme_at_the_same_width() { + use crate::render::highlight::current_syntax_theme; + use crate::render::highlight::set_syntax_theme; + use ratatui::style::Color; + use syntect::highlighting::Color as ThemeColor; + + let mut composer = composer(); + let content: crate::ui_tree::UiTree = + r#"{"type":"text","props":{"color":"primary"},"children":["λ"]}"# + .parse() + .unwrap(); + composer.set_status_line(Some(FooterContent::from(content))); + let previous = current_syntax_theme(); + let colors = [1, 2].map(|index| { + let mut theme = previous.clone(); + theme.settings.foreground = Some(ThemeColor { + r: index, + g: 0, + b: 0, + a: 0, + }); + set_syntax_theme(theme); + let options = ComposerRenderOptions::default(); + let width = 24; + let area = Rect::new( + 0, + 0, + width, + composer.desired_height_with_options(width, options), + ); + let mut buffer = Buffer::empty(area); + composer.render_with_options(area, &mut buffer, /*mask_char*/ None, options); + buffer + .content + .iter() + .find(|cell| cell.symbol() == "λ") + .unwrap() + .fg + }); + set_syntax_theme(previous); + assert_eq!(colors, [Color::Red, Color::Green]); +} + +#[test] +fn extension_status_keeps_its_rows_when_effort_changes() { + let mut composer = composer(); + let tree: crate::ui_tree::UiTree = r#""one\ntwo\nthree""#.parse().unwrap(); + composer.set_status_line(Some(FooterContent::from(tree))); + composer.set_active_reasoning_effort_baseline(/*effort*/ None); + composer.set_active_reasoning_effort( + Some(&ReasoningEffort::Max), + /*animations_enabled*/ true, + ); + for separate_status_line in [false, true] { + let options = ComposerRenderOptions { + separate_status_line, + ..Default::default() + }; + let area = Rect::new( + /*x*/ 0, /*y*/ 0, /*width*/ 7, /*height*/ 12, + ); + assert_eq!( + status_rows(&composer, area, options), + ["one", "two", "three"] + ); + } } fn render( diff --git a/codex-rs/tui/src/bottom_pane/chat_composer/warning_notice.rs b/codex-rs/tui/src/bottom_pane/chat_composer/warning_notice.rs index f3a768f278..e9a98b71d5 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer/warning_notice.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer/warning_notice.rs @@ -1,10 +1,17 @@ //! Fit warning counts and the configured warnings shortcut into the existing passive hint row. -//! Interactive hints retain priority; warning delivery never changes composer geometry. +//! Interactive hints retain priority. Shared status content wraps beside the warning. //! Only the count and warning label receive an amber accent; surrounding hints stay muted. use super::*; use crate::line_truncation::truncate_line_with_ellipsis_if_overflow; +pub(super) fn before_warning(mut area: Rect, warning: Option) -> Rect { + if let Some(warning) = warning { + area.width = warning.x.saturating_sub(area.x).saturating_sub(/*rhs*/ 2); + } + area +} + impl ChatComposer { pub(crate) fn warning_notice_contains(&self, position: ratatui::layout::Position) -> bool { self.footer diff --git a/codex-rs/tui/src/ui_tree/render.rs b/codex-rs/tui/src/ui_tree/render.rs index d81af44f46..939b251375 100644 --- a/codex-rs/tui/src/ui_tree/render.rs +++ b/codex-rs/tui/src/ui_tree/render.rs @@ -48,7 +48,6 @@ impl PreparedUiTree { }) } - #[cfg_attr(not(test), expect(dead_code))] // Used by composer layout in the next change. pub(crate) fn measure(&self, bounds: Size) -> Size { layout(self.0.as_ref(), bounds, /*draw*/ None) }