diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 1cee518b5a..a1deb1479b 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -581,7 +581,6 @@ pub(crate) struct App { pub(crate) file_search: FileSearchManager, pub(crate) transcript_cells: Vec>, - composer_tips: composer_hints::ComposerTips, native_history: native_history::NativeHistory, pub(crate) transcript_view: crate::transcript_view::TranscriptView, last_rendered_history_tail: Option, diff --git a/codex-rs/tui/src/app/composer_hints.rs b/codex-rs/tui/src/app/composer_hints.rs index 9d58336ba1..19fd54ad43 100644 --- a/codex-rs/tui/src/app/composer_hints.rs +++ b/codex-rs/tui/src/app/composer_hints.rs @@ -1,75 +1,10 @@ -//! Usage warnings take priority over startup announcements and stable catalog tips in fullscreen. -//! Only complete tips that fit one row are shown; tips require idle input and show_tooltips. +//! Usage warnings appear above the fullscreen composer when no interaction takes priority. use super::*; -use crate::history_cell::SessionInfoCell; -use crate::history_cell::UserHistoryCell; -use crate::style::secondary_text_style; use crate::terminal_hyperlinks::HyperlinkLine; -use rand::SeedableRng; -use rand::rngs::StdRng; -use rand::seq::SliceRandom; - -/// Keep announcement eligibility and random tip selection stable across redraws. -pub(super) struct ComposerTips { - seed: u64, - boot_turn_count: Option, - announcement_dismissed: bool, -} - -impl Default for ComposerTips { - fn default() -> Self { - Self::new(rand::random()) - } -} - -impl ComposerTips { - pub(super) fn new(seed: u64) -> Self { - Self { - seed, - boot_turn_count: None, - announcement_dismissed: false, - } - } - - fn select( - &mut self, - turn_count: usize, - announcement: impl FnOnce() -> Option, - keymap: &RuntimeKeymap, - width: usize, - cwd: &Path, - ) -> Option { - // Remember leaving startup even if /clear or backtracking later removes user cells. - self.announcement_dismissed |= - *self.boot_turn_count.get_or_insert(turn_count) != turn_count; - let render = |tip: String| { - let lines = crate::tooltips::render_tooltip_lines(&tip, width, cwd); - match lines.as_slice() { - [line] if line.width() <= width => Some(line.clone().style(secondary_text_style())), - _ => None, - } - }; - if !self.announcement_dismissed - && let Some(line) = announcement().and_then(&render) - { - return Some(line); - } - - let mut rng = StdRng::seed_from_u64(self.seed.wrapping_add(turn_count as u64)); - // Randomize before fitting so short tips are not favored by catalog order. - let mut tips = crate::tooltips::resolved_tooltips(Some(keymap)).collect::>(); - tips.shuffle(&mut rng); - tips.into_iter().find_map(render) - } -} impl App { - pub(super) fn composer_hint(&mut self, width: u16) -> Option { - let running = self.chat_widget.is_user_turn_pending_or_running(); - if running { - self.composer_tips.announcement_dismissed = true; - } + pub(super) fn composer_hint(&self, width: u16) -> Option { if !self.chat_widget.no_modal_or_popup_active() || !self.transcript_view.is_following() || self.transcript_view.has_active_interaction() @@ -82,34 +17,7 @@ impl App { if width == 0 { return None; } - if let Some(notice) = self.chat_widget.usage_notice(width) { - return Some(HyperlinkLine::new(notice)); - } - if !self.local_settings.tui.show_tooltips - || !self.chat_widget.composer_is_empty() - || running - { - return None; - } - // Cosmetic tip rotation needs cell types, not a scan of every prompt's text. - let count = self - .transcript_cells - .iter() - .rev() - .take_while(|cell| !cell.as_any().is::()) - .filter(|cell| cell.as_any().is::()) - .count(); - self.composer_tips.select( - count, - || { - crate::tooltips::announcement::fetch_announcement_tip( - self.chat_widget.current_plan_type(), - ) - }, - &self.keymap, - usize::from(width), - self.config.cwd.as_path(), - ) + self.chat_widget.usage_notice(width).map(HyperlinkLine::new) } } diff --git a/codex-rs/tui/src/app/composer_hints_tests.rs b/codex-rs/tui/src/app/composer_hints_tests.rs index 637d30b83e..75e44b906c 100644 --- a/codex-rs/tui/src/app/composer_hints_tests.rs +++ b/codex-rs/tui/src/app/composer_hints_tests.rs @@ -1,19 +1,12 @@ -//! Shared tips remain stable and linked while usage warnings take precedence in the composer gap. +//! Usage warnings preserve composer geometry and yield to active interactions. use super::*; -use crate::app::test_support::select_catalog_tip; -use crate::terminal_hyperlinks::strip_osc8; -use crate::transcript_view::ViewAction; use codex_app_server_protocol::RateLimitSnapshot; use codex_app_server_protocol::ServerNotification; use codex_app_server_protocol::Turn; use codex_app_server_protocol::TurnItemsView; use codex_app_server_protocol::TurnStartedNotification; use codex_app_server_protocol::TurnStatus; -use codex_protocol::account::PlanType; -use crossterm::event::MouseButton; -use crossterm::event::MouseEvent; -use crossterm::event::MouseEventKind; use pretty_assertions::assert_eq; fn quota(used_percent: i32) -> RateLimitSnapshot { @@ -24,7 +17,7 @@ fn quota(used_percent: i32) -> RateLimitSnapshot { } #[tokio::test] -async fn usage_notice_preserves_composer_geometry_and_restores_tip_on_recovery() -> Result<()> { +async fn usage_notice_preserves_composer_geometry_on_recovery() -> Result<()> { let mut snapshots = Vec::new(); for (width, height, running, queued, used_percent) in [ (80, 10, false, false, 92), @@ -91,7 +84,7 @@ async fn usage_notice_preserves_composer_geometry_and_restores_tip_on_recovery() tui.terminal.resize(size)?; let before = app.render_owned_transcript(&mut tui, size)?; let cursor = tui.terminal.last_known_cursor_pos; - let tip = app.composer_hint(width); + let hint = app.composer_hint(width); app.chat_widget .on_rate_limit_snapshot(Some(quota(used_percent))); while let Ok(event) = events.try_recv() { @@ -119,7 +112,7 @@ async fn usage_notice_preserves_composer_geometry_and_restores_tip_on_recovery() )); app.chat_widget .on_rate_limit_snapshot(Some(quota(/*used_percent*/ 10))); - assert_eq!(app.composer_hint(width), tip); + assert_eq!(app.composer_hint(width), hint); assert_eq!(app.render_owned_transcript(&mut tui, size)?, before); assert_eq!(tui.terminal.last_known_cursor_pos, cursor); tui.set_owned_screen(/*owned*/ false)?; @@ -135,12 +128,12 @@ async fn usage_notice_preserves_composer_geometry_and_restores_tip_on_recovery() async fn usage_notice_yields_to_interactions_and_blocking_banners() { let mut app = crate::app::test_support::make_test_app().await; app.local_settings.tui.show_tooltips = true; - let tip = app.composer_hint(/*width*/ 80); + let hint = app.composer_hint(/*width*/ 80); app.chat_widget .on_rate_limit_snapshot(Some(quota(/*used_percent*/ 92))); let notice = app.composer_hint(/*width*/ 80); assert!(notice.is_some()); - assert_ne!(notice, tip); + assert_ne!(notice, hint); let notice_line = notice.as_ref().map(|notice| notice.line.clone()); app.transcript_view.begin_search(); assert_eq!(app.composer_hint(/*width*/ 80), None); @@ -182,293 +175,6 @@ async fn usage_notice_yields_to_interactions_and_blocking_banners() { assert_eq!(app.chat_widget.usage_notice(/*width*/ 78), notice_line); } -#[tokio::test] -async fn hints_respect_settings_drafts_and_custom_shortcuts() -> Result<()> { - let mut app = crate::app::test_support::make_test_app().await; - app.local_settings.tui.show_tooltips = true; - app.chat_widget.update_account_state( - /*status_account_display*/ None, - Some(PlanType::Free), - /*has_chatgpt_account*/ true, - /*has_codex_backend_auth*/ true, - ); - app.transcript_cells - .push(Arc::new(crate::history_cell::new_user_prompt( - "question".into(), - Vec::new(), - Vec::new(), - Vec::new(), - ))); - app.keymap.app.find_transcript = vec![crate::key_hint::plain(KeyCode::F(12))]; - select_catalog_tip( - &mut app, - /*width*/ 80, - "Tip: Press f12 to search this conversation.", - ); - let tip = app.composer_hint(/*width*/ 80).unwrap(); - assert_eq!( - tip.line.to_string(), - "Tip: Press f12 to search this conversation." - ); - assert_eq!(app.composer_hint(/*width*/ 80), Some(tip)); - let mut tui = crate::tui::test_support::make_test_tui()?; - tui.set_owned_screen(/*owned*/ true)?; - let mut snapshots = Vec::new(); - for width in [80, 32] { - let size = Size::new(width, /*height*/ 8); - tui.screen_size_for_event(&TuiEvent::Resize(size))?; - app.render_owned_transcript(&mut tui, size)?; - let buffer = crate::custom_terminal::test_support::last_rendered_buffer(&tui.terminal); - let rendered = (buffer.area.y..buffer.area.bottom()) - .map(|y| { - (buffer.area.x..buffer.area.right()) - .map(|x| buffer[(x, y)].symbol()) - .collect::() - .trim_end() - .to_string() - }) - .collect::>() - .join("\n"); - snapshots.push(format!("{width} columns\n{rendered}")); - } - insta::assert_snapshot!( - "composer_tip_layout", - crate::chatwidget::tests::helpers::normalize_snapshot_paths(snapshots.join("\n\n")), - ); - for width in [0, 1, 2, 32] { - assert_eq!(app.composer_hint(width), None); - } - app.keymap.app.find_transcript = vec![crate::key_hint::plain(KeyCode::F(6))]; - assert_eq!( - app.composer_hint(/*width*/ 80).unwrap().line.to_string(), - "Tip: Press f6 to search this conversation." - ); - app.keymap.app.find_transcript.clear(); - let tip = app.composer_hint(/*width*/ 80).unwrap(); - assert!( - !tip.line - .to_string() - .contains(" to search this conversation.") - ); - app.chat_widget.apply_external_edit("draft".into()); - assert_eq!(app.composer_hint(/*width*/ 80), None); - app.chat_widget.apply_external_edit(String::new()); - app.local_settings.tui.show_tooltips = false; - assert_eq!(app.composer_hint(/*width*/ 120), None); - tui.set_owned_screen(/*owned*/ false)?; - Ok(()) -} - -#[test] -fn announcement_is_only_used_at_startup_and_catalog_tips_are_stable() { - let mut tips = ComposerTips::new(/*seed*/ 42); - let keymap = RuntimeKeymap::defaults(); - let cwd = Path::new("."); - let before_fetch = tips - .select( - /*turn_count*/ 0, - || None, - &keymap, - /*width*/ 78, - cwd, - ) - .unwrap(); - let announcement = || Some("A startup announcement.".to_string()); - let boot = tips - .select( - /*turn_count*/ 0, - announcement, - &keymap, - /*width*/ 78, - cwd, - ) - .unwrap(); - assert_eq!(boot.line.to_string(), "Tip: A startup announcement."); - assert_eq!( - tips.select( - /*turn_count*/ 0, - announcement, - &keymap, - /*width*/ 78, - cwd - ), - Some(boot.clone()), - ); - let after_turn = tips - .select( - /*turn_count*/ 1, - || panic!("announcements must not be fetched after a user turn"), - &keymap, - /*width*/ 78, - cwd, - ) - .unwrap(); - assert_eq!( - tips.select( - /*turn_count*/ 1, - announcement, - &keymap, - /*width*/ 78, - cwd - ), - Some(after_turn), - ); - let after_clear = tips - .select( - /*turn_count*/ 0, - || panic!("clearing history must not restore the startup announcement"), - &keymap, - /*width*/ 78, - cwd, - ) - .unwrap(); - assert_eq!(after_clear, before_fetch); - insta::assert_snapshot!("composer_tip_startup_announcement", boot.line.to_string()); -} - -#[test] -fn catalog_selection_varies_across_sessions_and_turns_and_fits_the_row() { - let keymap = RuntimeKeymap::defaults(); - let cwd = Path::new("."); - let mut starts = std::collections::BTreeSet::new(); - for seed in 0..16 { - let mut turns = std::collections::BTreeSet::new(); - let mut tips = ComposerTips::new(seed); - // A long announcement falls back to a complete catalog tip that fits. - let initial = tips - .select( - /*turn_count*/ 0, - || Some("A long announcement. ".repeat(/*n*/ 10)), - &keymap, - /*width*/ 78, - cwd, - ) - .unwrap(); - assert!(initial.width() <= 78); - starts.insert(initial.line.to_string()); - for count in 1..8 { - let tip = tips - .select(count, || None, &keymap, /*width*/ 78, cwd) - .unwrap(); - assert!(tip.width() <= 78); - turns.insert(tip.line.to_string()); - } - assert!(turns.len() > 1); - assert_eq!( - tips.select( - /*turn_count*/ 8, - || None, - &keymap, - /*width*/ 1, - cwd - ), - None, - ); - } - assert!(starts.len() > 1); -} - -#[tokio::test] -async fn running_turn_dismisses_announcement_across_thread_resets() { - let mut app = crate::app::test_support::make_test_app().await; - app.chat_widget.handle_server_notification( - ServerNotification::TurnStarted(codex_app_server_protocol::TurnStartedNotification { - thread_id: ThreadId::new().to_string(), - turn: codex_app_server_protocol::Turn { - id: "active-turn".to_string(), - items_view: codex_app_server_protocol::TurnItemsView::Full, - items: Vec::new(), - status: codex_app_server_protocol::TurnStatus::InProgress, - error: None, - started_at: None, - completed_at: None, - duration_ms: None, - }, - }), - /*replay_kind*/ None, - ); - assert_eq!(app.composer_hint(/*width*/ 80), None); - app.reset_thread_event_state().await; - let tip = app.composer_tips.select( - /*turn_count*/ 0, - || panic!("an already running turn must end the startup announcement"), - &app.keymap, - /*width*/ 78, - app.config.cwd.as_path(), - ); - assert!(tip.is_some()); -} - -#[tokio::test] -async fn hints_render_clickable_links_and_release_them_when_disabled() -> Result<()> { - let mut app = crate::app::test_support::make_test_app().await; - app.local_settings.tui.show_tooltips = true; - app.chat_widget.update_account_state( - /*status_account_display*/ None, - Some(PlanType::Free), - /*has_chatgpt_account*/ true, - /*has_codex_backend_auth*/ true, - ); - let destination = "https://community.openai.com/c/codex/37"; - select_catalog_tip( - &mut app, - /*width*/ 85, - "Tip: Visit the Codex community forum", - ); - let mut tui = crate::tui::test_support::make_test_tui()?; - tui.set_owned_screen(/*owned*/ true)?; - let size = Size::new(/*width*/ 85, /*height*/ 8); - tui.screen_size_for_event(&TuiEvent::Resize(size))?; - let bottom = app.render_owned_transcript(&mut tui, size)?; - let buffer = crate::custom_terminal::test_support::last_rendered_buffer(&tui.terminal); - let row = bottom.y; - let rendered = (0..size.width) - .map(|column| strip_osc8(buffer[(column, row)].symbol())) - .collect::(); - insta::assert_snapshot!("composer_tip_link", rendered.trim_end()); - let columns = (0..size.width) - .filter(|column| buffer[(*column, row)].symbol().contains(destination)) - .collect::>(); - assert!(!columns.is_empty()); - for column in columns { - for modifiers in [KeyModifiers::CONTROL, KeyModifiers::SUPER] { - let event = MouseEvent { - kind: MouseEventKind::Down(MouseButton::Left), - column, - row, - modifiers, - }; - let Some(ViewAction::OpenLink(url)) = app - .transcript_view - .handle_mouse(event, &app.transcript_cells) - else { - panic!("linked tip cell should open its destination"); - }; - assert_eq!(url, destination); - } - } - app.local_settings.tui.show_tooltips = false; - tui.screen_size_for_event(&TuiEvent::Resize(size))?; - app.render_owned_transcript(&mut tui, size)?; - for column in 0..size.width { - assert!( - app.transcript_view - .handle_mouse( - MouseEvent { - kind: MouseEventKind::Down(MouseButton::Left), - column, - row, - modifiers: KeyModifiers::CONTROL, - }, - &app.transcript_cells - ) - .is_none() - ); - } - tui.set_owned_screen(/*owned*/ false)?; - Ok(()) -} - #[tokio::test] async fn escape_closes_shortcut_help_before_transcript_backtracking() -> Result<()> { let mut app = crate::app::test_support::make_test_app().await; diff --git a/codex-rs/tui/src/app/empty_state_animation_tests.rs b/codex-rs/tui/src/app/empty_state_animation_tests.rs index d4760ecab1..a7b53d490f 100644 --- a/codex-rs/tui/src/app/empty_state_animation_tests.rs +++ b/codex-rs/tui/src/app/empty_state_animation_tests.rs @@ -195,11 +195,6 @@ async fn empty_state_animation_preserves_header_cursor_and_footer() -> Result<() ["example".to_string()], /*failure_reason*/ None, ))); - crate::app::test_support::select_catalog_tip( - &mut app, - size.width, - "Tip: Start a fresh idea with /new; the previous session stays in history.", - ); let before_bottom = draw(&mut app, &mut tui, size)?; let history_len = app.transcript_cells.len(); let before = crate::custom_terminal::test_support::last_rendered_buffer(&tui.terminal).clone(); diff --git a/codex-rs/tui/src/app/owned_transcript_tests.rs b/codex-rs/tui/src/app/owned_transcript_tests.rs index 6d05952861..d860430304 100644 --- a/codex-rs/tui/src/app/owned_transcript_tests.rs +++ b/codex-rs/tui/src/app/owned_transcript_tests.rs @@ -285,11 +285,6 @@ async fn recap_spacing_belongs_to_the_transcript_tail() -> Result<()> { .with_next_action(next_action.map(str::to_owned)), ), ]; - crate::app::test_support::select_catalog_tip( - &mut app, - /*width*/ 80, - "Tip: Use /mcp to list configured MCP tools.", - ); let mut tui = crate::tui::test_support::make_test_tui()?; tui.set_owned_screen(/*owned*/ true)?; let size = Size::new(width, height); diff --git a/codex-rs/tui/src/app/snapshots/codex_tui__app__composer_hints__tests__composer_tip_layout.snap b/codex-rs/tui/src/app/snapshots/codex_tui__app__composer_hints__tests__composer_tip_layout.snap deleted file mode 100644 index 219056e97f..0000000000 --- a/codex-rs/tui/src/app/snapshots/codex_tui__app__composer_hints__tests__composer_tip_layout.snap +++ /dev/null @@ -1,23 +0,0 @@ ---- -source: tui/src/app/composer_hints_tests.rs -expression: "crate::chatwidget::tests::helpers::normalize_snapshot_paths(snapshots.join(\"\\n\\n\"))" ---- -80 columns -› question - - Tip: Press f12 to search this conversation. - -› Ask Codex to do anything - - GPT-5.6-Sol default · /tmp/project - ? for shortcuts - -32 columns -› question - - - -› Ask Codex to do anything - - GPT-5.6-Sol default · /tmp/pr… - ? for shortcuts diff --git a/codex-rs/tui/src/app/snapshots/codex_tui__app__composer_hints__tests__composer_tip_link.snap b/codex-rs/tui/src/app/snapshots/codex_tui__app__composer_hints__tests__composer_tip_link.snap deleted file mode 100644 index db789b639f..0000000000 --- a/codex-rs/tui/src/app/snapshots/codex_tui__app__composer_hints__tests__composer_tip_link.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: tui/src/app/composer_hints_tests.rs -expression: rendered.trim_end() ---- - Tip: Visit the Codex community forum (https://community.openai.com/c/codex/37). diff --git a/codex-rs/tui/src/app/snapshots/codex_tui__app__composer_hints__tests__composer_tip_startup_announcement.snap b/codex-rs/tui/src/app/snapshots/codex_tui__app__composer_hints__tests__composer_tip_startup_announcement.snap deleted file mode 100644 index fe1a29afed..0000000000 --- a/codex-rs/tui/src/app/snapshots/codex_tui__app__composer_hints__tests__composer_tip_startup_announcement.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: tui/src/app/composer_hints_tests.rs -expression: boot.line.to_string() ---- -Tip: A startup announcement. diff --git a/codex-rs/tui/src/app/snapshots/codex_tui__app__owned_transcript__empty_state_animation_tests__fresh_thread_header.snap b/codex-rs/tui/src/app/snapshots/codex_tui__app__owned_transcript__empty_state_animation_tests__fresh_thread_header.snap index 91094ce9a9..03bca75b48 100644 --- a/codex-rs/tui/src/app/snapshots/codex_tui__app__owned_transcript__empty_state_animation_tests__fresh_thread_header.snap +++ b/codex-rs/tui/src/app/snapshots/codex_tui__app__owned_transcript__empty_state_animation_tests__fresh_thread_header.snap @@ -40,7 +40,7 @@ expression: text(after) - Tip: Start a fresh idea with /new; the previous session stays in history. + › Ask Codex to do anything diff --git a/codex-rs/tui/src/app/snapshots/codex_tui__app__owned_transcript__tests__recap_tail_spacing.snap b/codex-rs/tui/src/app/snapshots/codex_tui__app__owned_transcript__tests__recap_tail_spacing.snap index fdde278b9b..34bbf54b6e 100644 --- a/codex-rs/tui/src/app/snapshots/codex_tui__app__owned_transcript__tests__recap_tail_spacing.snap +++ b/codex-rs/tui/src/app/snapshots/codex_tui__app__owned_transcript__tests__recap_tail_spacing.snap @@ -7,7 +7,7 @@ History 20 ↳ Recap: The draft is ready. - Tip: Use /mcp to list configured MCP tools. + › Ask Codex to do anything @@ -19,7 +19,7 @@ History 20 ↳ Recap: The draft is ready. • Follow-up response. - Tip: Use /mcp to list configured MCP tools. + › Ask Codex to do anything @@ -31,7 +31,7 @@ History 20 ↳ Recap: The draft is ready. • Streaming response. - Tip: Use /mcp to list configured MCP tools. + › Ask Codex to do anything @@ -45,7 +45,7 @@ History 20 ↳ Recap: The draft is ready. Next: Review the changes. - Tip: Use /mcp to list configured MCP tools. + › Ask Codex to do anything @@ -59,7 +59,7 @@ History 20 Next: Review the changes. • Follow-up response. - Tip: Use /mcp to list configured MCP tools. + › Ask Codex to do anything @@ -73,7 +73,7 @@ History 20 Next: Review the changes. • Streaming response. - Tip: Use /mcp to list configured MCP tools. + › Ask Codex to do anything @@ -124,7 +124,7 @@ History 20 80x7, Recap at tail Next: Review the changes. - Tip: Use /mcp to list configured MCP tools. + › Ask Codex to do anything @@ -133,7 +133,7 @@ History 20 80x7, Message after recap • Follow-up response. - Tip: Use /mcp to list configured MCP tools. + › Ask Codex to do anything @@ -142,7 +142,7 @@ History 20 80x7, Live message after recap • Streaming response. - Tip: Use /mcp to list configured MCP tools. + › Ask Codex to do anything diff --git a/codex-rs/tui/src/app/startup.rs b/codex-rs/tui/src/app/startup.rs index 9ff5f6ad62..89bd252a62 100644 --- a/codex-rs/tui/src/app/startup.rs +++ b/codex-rs/tui/src/app/startup.rs @@ -764,7 +764,6 @@ See the Codex keymap documentation for supported actions and examples." keymap: runtime_keymap, key_chord_matcher: KeyChordMatcher::default(), transcript_cells: Vec::new(), - composer_tips: Default::default(), native_history: Default::default(), transcript_view: Default::default(), last_rendered_history_tail: None, diff --git a/codex-rs/tui/src/app/test_support.rs b/codex-rs/tui/src/app/test_support.rs index c5c714e369..f1bd6589c4 100644 --- a/codex-rs/tui/src/app/test_support.rs +++ b/codex-rs/tui/src/app/test_support.rs @@ -9,19 +9,6 @@ use crate::chatwidget::tests::make_chatwidget_manual_with_sender; use codex_models_manager::test_support::construct_model_info_offline_for_tests; use codex_models_manager::test_support::get_model_offline_for_tests; -pub(super) fn select_catalog_tip(app: &mut App, width: u16, expected: &str) { - for seed in 0..1024 { - app.composer_tips = super::composer_hints::ComposerTips::new(seed); - if app - .composer_hint(width) - .is_some_and(|tip| tip.line.to_string().starts_with(expected)) - { - return; - } - } - panic!("catalog tip was never selected: {expected}"); -} - pub(crate) async fn make_test_app() -> App { let (mut chat_widget, app_event_tx, _rx, _op_rx) = make_chatwidget_manual_with_sender().await; let test_codex_home = chat_widget.test_codex_home.take(); @@ -51,7 +38,6 @@ pub(crate) async fn make_test_app() -> App { pending_server_profiles: HashMap::new(), file_search, transcript_cells: Vec::new(), - composer_tips: super::composer_hints::ComposerTips::new(/*seed*/ 0), native_history: Default::default(), transcript_view: Default::default(), last_rendered_history_tail: None, diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs index 3fd815daeb..268ca41008 100644 --- a/codex-rs/tui/src/app/tests.rs +++ b/codex-rs/tui/src/app/tests.rs @@ -6011,7 +6011,6 @@ async fn make_test_app() -> Box { runtime_permission_profile_override: None, file_search, transcript_cells: Vec::new(), - composer_tips: super::composer_hints::ComposerTips::new(/*seed*/ 0), native_history: Default::default(), transcript_view: Default::default(), last_rendered_history_tail: None, @@ -6120,7 +6119,6 @@ pub(super) async fn make_test_app_with_channels() -> ( runtime_permission_profile_override: None, file_search, transcript_cells: Vec::new(), - composer_tips: super::composer_hints::ComposerTips::new(/*seed*/ 0), native_history: Default::default(), transcript_view: Default::default(), last_rendered_history_tail: None, diff --git a/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__startup_frame_tests__startup_tip_in_transcript.snap b/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__startup_frame_tests__startup_tip_in_transcript.snap new file mode 100644 index 0000000000..5a78a900ac --- /dev/null +++ b/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__startup_frame_tests__startup_tip_in_transcript.snap @@ -0,0 +1,47 @@ +--- +source: tui/src/app/tests/startup_frame_tests.rs +expression: "normalize_snapshot_paths(normalize_agent_center_snapshot(snapshots.join(\"\\n\\n\")))" +--- +80 columns +╭────────────────────────────────────────╮ +│ >_ OpenAI Codex (v0.0.0) │ +│ │ +│ model: gpt-test /model to change │ +│ directory: /tmp/project │ +╰────────────────────────────────────────╯ + + Tip: Use /mcp to list configured MCP tools. + +• Conversation continues here. + + + + + + +› Ask Codex to do anything + + GPT-5.6-Sol default · /tmp/project + ? for shortcuts + +32 columns +╭──────────────────────────────╮ +│ >_ OpenAI Codex (v0.0.0) │ +│ │ +│ model: gpt-test /mode… │ +│ directory: /tmp/project │ +╰──────────────────────────────╯ + + Tip: Use /mcp to list + configured MCP tools. + +• Conversation continues here. + + + + + +› Ask Codex to do anything + + GPT-5.6-Sol default · /tmp/pr… + ? for shortcuts diff --git a/codex-rs/tui/src/app/tests/startup_frame_tests.rs b/codex-rs/tui/src/app/tests/startup_frame_tests.rs index a2028e6495..e9f983be54 100644 --- a/codex-rs/tui/src/app/tests/startup_frame_tests.rs +++ b/codex-rs/tui/src/app/tests/startup_frame_tests.rs @@ -1,6 +1,8 @@ //! Startup handoff preserves the loading frame until replayed cells can replace it. use super::*; +use crate::chatwidget::tests::helpers::normalize_agent_center_snapshot; +use crate::chatwidget::tests::helpers::normalize_snapshot_paths; use crate::custom_terminal::test_support::last_rendered_buffer; use crate::startup_draft::tests::quiet_startup_test_pump; use pretty_assertions::assert_eq; @@ -13,6 +15,59 @@ fn frame_text(tui: &tui::Tui) -> String { .collect() } +#[tokio::test] +async fn owned_startup_tip_follows_splash_in_transcript() -> Result<()> { + let mut app = crate::app::test_support::make_test_app().await; + app.local_settings.tui.show_tooltips = true; + app.local_settings.tui.animations = false; + let session = test_thread_session(ThreadId::new(), app.config.cwd.to_path_buf()); + app.transcript_cells = vec![ + Arc::new(new_session_info( + &app.config, + &app.local_settings, + &session.model, + &session.model, + &session, + /*is_first_event*/ false, + Some("Use /mcp to list configured MCP tools.".into()), + /*auth_plan*/ None, + /*show_fast_status*/ false, + )), + Arc::new(AgentMessageCell::new( + vec!["Conversation continues here.".into()], + /*is_first_line*/ true, + )), + ]; + let mut tui = crate::tui::test_support::make_test_tui()?; + tui.set_owned_screen(/*owned*/ true)?; + let mut snapshots = Vec::new(); + for width in [80, 32] { + let size = Size::new(width, /*height*/ 20); + tui.screen_size_for_event(&TuiEvent::Resize(size))?; + app.render_owned_transcript(&mut tui, size)?; + let buffer = last_rendered_buffer(&tui.terminal); + let screen = buffer + .content() + .chunks(usize::from(buffer.area.width)) + .map(|row| { + row.iter() + .map(ratatui::buffer::Cell::symbol) + .collect::() + .trim_end() + .to_owned() + }) + .collect::>() + .join("\n"); + snapshots.push(format!("{width} columns\n{screen}")); + } + insta::assert_snapshot!( + "startup_tip_in_transcript", + normalize_snapshot_paths(normalize_agent_center_snapshot(snapshots.join("\n\n"))), + ); + tui.set_owned_screen(/*owned*/ false)?; + Ok(()) +} + #[tokio::test] async fn owned_startup_preserves_loading_until_resume_replay_is_applied() -> Result<()> { for items in [ diff --git a/codex-rs/tui/src/history_cell/session.rs b/codex-rs/tui/src/history_cell/session.rs index a6d5eed20e..3d9c174856 100644 --- a/codex-rs/tui/src/history_cell/session.rs +++ b/codex-rs/tui/src/history_cell/session.rs @@ -80,11 +80,6 @@ impl TooltipHistoryCell { } impl HistoryCell for TooltipHistoryCell { - fn compact_hyperlink_lines(&self, _width: u16) -> Vec { - // Optional tips stay available in detailed history without occupying the conversation. - Vec::new() - } - fn display_lines(&self, width: u16) -> Vec> { visible_lines(self.display_hyperlink_lines(width)) } diff --git a/codex-rs/tui/src/history_cell/tests.rs b/codex-rs/tui/src/history_cell/tests.rs index 5ce4b76560..4d22db3310 100644 --- a/codex-rs/tui/src/history_cell/tests.rs +++ b/codex-rs/tui/src/history_cell/tests.rs @@ -798,6 +798,7 @@ async fn session_info_preserves_styled_tooltip_links() { let lines = cell.transcript_hyperlink_lines(/*width*/ 30); assert_eq!(lines, cell.display_hyperlink_lines(/*width*/ 30)); + assert_eq!(lines, cell.compact_hyperlink_lines(/*width*/ 30)); assert_eq!( visible_lines(lines.clone()), cell.transcript_lines(/*width*/ 30)