diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 625c614920..72722559fc 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -4617,7 +4617,7 @@ impl ChatComposer { popup: popup_rect, footer: footer_rect, } = self.layout_with_options(area, options); - self.render_status_surface(status, buf); + self.render_status_surface(status, buf, options); if self.popups.active.is_above_composer() && options.command_popup_placement != CommandPopupPlacement::Hidden { diff --git a/codex-rs/tui/src/bottom_pane/chat_composer/snapshots/codex_tui__bottom_pane__chat_composer__status_surface__tests__fullscreen_plan_cycle_hint.snap b/codex-rs/tui/src/bottom_pane/chat_composer/snapshots/codex_tui__bottom_pane__chat_composer__status_surface__tests__fullscreen_plan_cycle_hint.snap new file mode 100644 index 0000000000..f083a97c97 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/chat_composer/snapshots/codex_tui__bottom_pane__chat_composer__status_surface__tests__fullscreen_plan_cycle_hint.snap @@ -0,0 +1,18 @@ +--- +source: tui/src/bottom_pane/chat_composer/status_surface_tests.rs +assertion_line: 102 +expression: wide +--- + + + + + + + + + +› Ask Codex + + MODEL · ~/project · Context 20% used Plan mode (shift+tab to cycle) + ? for shortcuts 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 14fe0a9dbc..60ee2e0fa9 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 @@ -19,16 +19,43 @@ impl ChatComposer { props } - pub(super) fn render_status_surface(&self, area: Rect, buf: &mut Buffer) { + pub(super) fn render_status_surface( + &self, + area: Rect, + buf: &mut Buffer, + options: ComposerRenderOptions<'_>, + ) { if area.is_empty() { return; } let mut props = self.footer_props(); + let show_cycle_hint = !props.is_task_running + && !self.blocks_direct_input + && self.draft.input_enabled + && !self.popup_active() + && matches!( + props.mode, + FooterMode::ComposerEmpty | FooterMode::ComposerHasDraft + ) + && options.footer.is_none_or(|footer| !footer.is_interactive); // Persistent context is independent of help, queue, search, and quit modes. props.mode = FooterMode::ComposerHasDraft; props.is_task_running = false; let line = passive_footer_status_line(&props); - let right = self.mode_indicator_line(/*show_cycle_hint*/ false); + let full = self.mode_indicator_line(show_cycle_hint); + let left_width = line + .as_ref() + .map_or(/*default*/ 0, |line| line.width() as u16); + let full_width = full + .as_ref() + .map_or(/*default*/ 0, |line| line.width() as u16); + let right = if full_width <= inset_footer_hint_area(area).width + && can_show_left_with_context(area, left_width, full_width) + { + full + } else { + self.mode_indicator_line(/*show_cycle_hint*/ false) + }; let right_width = right .as_ref() .map_or(/*default*/ 0, |line| line.width() as u16); 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 4a7bc01f8a..369bca1726 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 @@ -89,6 +89,65 @@ fn passive_activity_keeps_shortcuts_only_when_the_complete_hint_fits() { ); } +#[test] +fn fullscreen_plan_indicator_keeps_the_cycle_hint_when_it_fits() { + let mut composer = composer(); + composer.set_collaboration_modes_enabled(/*enabled*/ true); + composer.set_collaboration_mode_indicator(Some(CollaborationModeIndicator::Plan)); + let (wide, _) = render(&composer, /*width*/ 100, /*footer*/ None); + assert!( + wide.contains("Plan mode (shift+tab to cycle)"), + "idle fullscreen must explain how to leave Plan mode: {wide}" + ); + insta::assert_snapshot!("fullscreen_plan_cycle_hint", wide); + + let (narrow, _) = render(&composer, /*width*/ 44, /*footer*/ None); + assert!(narrow.contains("Plan mode"), "{narrow}"); + assert!(!narrow.contains("shift+tab"), "{narrow}"); + + composer.set_task_running(/*running*/ true); + let (running, _) = render(&composer, /*width*/ 100, /*footer*/ None); + assert!(running.contains("Plan mode"), "{running}"); + assert!(!running.contains("shift+tab"), "{running}"); + composer.set_task_running(/*running*/ false); + + let footer = TranscriptFooter { + text: "Search: verify".into(), + cursor_column: Some(8), + is_interactive: true, + }; + let (search, _) = render(&composer, /*width*/ 100, Some(&footer)); + assert!(search.contains("Plan mode"), "{search}"); + assert!(!search.contains("shift+tab"), "{search}"); + + // Configured items such as git-branch can have no value outside a repository. + composer.set_status_line(/*status_line*/ None); + let (empty_narrow, _) = render(&composer, /*width*/ 24, /*footer*/ None); + assert_eq!( + empty_narrow.lines().rev().nth(1).map(str::trim), + Some("Plan mode") + ); + let (empty_wide, _) = render(&composer, /*width*/ 100, /*footer*/ None); + assert!( + empty_wide.contains("Plan mode (shift+tab to cycle)"), + "{empty_wide}" + ); + + composer.set_input_enabled(/*enabled*/ false, /*placeholder*/ None); + let (read_only, _) = render(&composer, /*width*/ 100, /*footer*/ None); + assert_eq!( + read_only.lines().rev().nth(1).map(str::trim), + Some("Plan mode") + ); + composer.set_input_enabled(/*enabled*/ true, /*placeholder*/ None); + composer.set_parent_owned_thread(); + let (parent_owned, _) = render(&composer, /*width*/ 100, /*footer*/ None); + assert_eq!( + parent_owned.lines().rev().nth(1).map(str::trim), + Some("Plan mode") + ); +} + #[test] fn shortcut_help_stays_above_the_prompt_and_bottom_status() { let mut composer = composer();