mirror of
https://github.com/openai/codex.git
synced 2026-10-19 17:28:46 +00:00
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
This commit is contained in:
@@ -581,7 +581,6 @@ pub(crate) struct App {
|
||||
pub(crate) file_search: FileSearchManager,
|
||||
|
||||
pub(crate) transcript_cells: Vec<Arc<dyn HistoryCell>>,
|
||||
composer_tips: composer_hints::ComposerTips,
|
||||
native_history: native_history::NativeHistory,
|
||||
pub(crate) transcript_view: crate::transcript_view::TranscriptView,
|
||||
last_rendered_history_tail: Option<history_ui::RenderedHistoryTail>,
|
||||
|
||||
@@ -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<usize>,
|
||||
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<String>,
|
||||
keymap: &RuntimeKeymap,
|
||||
width: usize,
|
||||
cwd: &Path,
|
||||
) -> Option<HyperlinkLine> {
|
||||
// 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::<Vec<_>>();
|
||||
tips.shuffle(&mut rng);
|
||||
tips.into_iter().find_map(render)
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub(super) fn composer_hint(&mut self, width: u16) -> Option<HyperlinkLine> {
|
||||
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<HyperlinkLine> {
|
||||
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::<SessionInfoCell>())
|
||||
.filter(|cell| cell.as_any().is::<UserHistoryCell>())
|
||||
.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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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::<String>()
|
||||
.trim_end()
|
||||
.to_string()
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.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::<String>();
|
||||
insta::assert_snapshot!("composer_tip_link", rendered.trim_end());
|
||||
let columns = (0..size.width)
|
||||
.filter(|column| buffer[(*column, row)].symbol().contains(destination))
|
||||
.collect::<Vec<_>>();
|
||||
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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
-23
@@ -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
|
||||
-5
@@ -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).
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
---
|
||||
source: tui/src/app/composer_hints_tests.rs
|
||||
expression: boot.line.to_string()
|
||||
---
|
||||
Tip: A startup announcement.
|
||||
+1
-1
@@ -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
|
||||
|
||||
|
||||
+9
-9
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -6011,7 +6011,6 @@ async fn make_test_app() -> Box<App> {
|
||||
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,
|
||||
|
||||
+47
@@ -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
|
||||
@@ -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::<String>()
|
||||
.trim_end()
|
||||
.to_owned()
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.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 [
|
||||
|
||||
@@ -80,11 +80,6 @@ impl TooltipHistoryCell {
|
||||
}
|
||||
|
||||
impl HistoryCell for TooltipHistoryCell {
|
||||
fn compact_hyperlink_lines(&self, _width: u16) -> Vec<HyperlinkLine> {
|
||||
// Optional tips stay available in detailed history without occupying the conversation.
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
fn display_lines(&self, width: u16) -> Vec<Line<'static>> {
|
||||
visible_lines(self.display_hyperlink_lines(width))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user