From 40e13e6281e4725b0e81c8488ada69747be89cb7 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Thu, 24 Sep 2026 22:32:49 +0000 Subject: [PATCH] Move fullscreen startup tips into the transcript (#47954) ## What changed Show the startup tip beneath the session header in the fullscreen transcript, with wrapping on narrow terminals. Remove rotating tips and startup announcements from above the composer, leaving that area for usage warnings. ## Testing Add startup transcript snapshots at 80 and 32 columns and assert that compact tooltip rendering preserves styled links. Update usage-warning tests to verify composer geometry and recovery without rotating tips. GitOrigin-RevId: b5e79a46147d112cd50802b0702537deb9b87001 --- codex-rs/tui/src/app.rs | 1 - codex-rs/tui/src/app/composer_hints.rs | 98 +----- codex-rs/tui/src/app/composer_hints_tests.rs | 306 +----------------- .../src/app/empty_state_animation_tests.rs | 5 - .../tui/src/app/owned_transcript_tests.rs | 5 - ...ser_hints__tests__composer_tip_layout.snap | 23 -- ...poser_hints__tests__composer_tip_link.snap | 5 - ...ts__composer_tip_startup_announcement.snap | 5 - ..._animation_tests__fresh_thread_header.snap | 2 +- ...transcript__tests__recap_tail_spacing.snap | 18 +- codex-rs/tui/src/app/startup.rs | 1 - codex-rs/tui/src/app/test_support.rs | 14 - codex-rs/tui/src/app/tests.rs | 2 - ...rame_tests__startup_tip_in_transcript.snap | 47 +++ .../tui/src/app/tests/startup_frame_tests.rs | 55 ++++ codex-rs/tui/src/history_cell/session.rs | 5 - codex-rs/tui/src/history_cell/tests.rs | 1 + 17 files changed, 122 insertions(+), 471 deletions(-) delete mode 100644 codex-rs/tui/src/app/snapshots/codex_tui__app__composer_hints__tests__composer_tip_layout.snap delete mode 100644 codex-rs/tui/src/app/snapshots/codex_tui__app__composer_hints__tests__composer_tip_link.snap delete mode 100644 codex-rs/tui/src/app/snapshots/codex_tui__app__composer_hints__tests__composer_tip_startup_announcement.snap create mode 100644 codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__startup_frame_tests__startup_tip_in_transcript.snap 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)