diff --git a/codex-rs/core/src/guardrails.rs b/codex-rs/core/src/guardrails.rs index 8abb787775..2caa82bf35 100644 --- a/codex-rs/core/src/guardrails.rs +++ b/codex-rs/core/src/guardrails.rs @@ -4,7 +4,7 @@ use crate::git_info::get_git_repo_root; use std::path::Path; use std::path::PathBuf; -const AGENTS_TEMPLATE: &str = "# Repository Guidelines\n\n## How to work in this repo\n- Add any key instructions Codex should follow.\n\n## Build and test\n- List the main build and test commands here.\n\n## Coding conventions\n- Note formatting, linting, and naming rules.\n\n## Notes for Codex\n- Capture anything that helps Codex work efficiently.\n"; +const AGENTS_TEMPLATE: &str = "# Repository Guidelines\n\n## How to work in this repo\n- Add any key instructions Codex should follow.\n\n## Build and test\n{build_test_section}\n\n## Coding conventions\n- Note formatting, linting, and naming rules.\n\n## Notes for Codex\n- Capture anything that helps Codex work efficiently.\n"; const PLANS_TEMPLATE: &str = "# Plans\n\nUse this file to record approved plans for complex changes.\n\nTemplate\n- Goal\n- Approach\n- Steps\n- Tests\n- Rollback\n"; @@ -15,11 +15,14 @@ pub struct GuardrailScaffoldOutcome { pub plans_created: bool, } -pub fn scaffold_guardrail_files(cwd: &Path) -> std::io::Result { +pub fn scaffold_guardrail_files( + cwd: &Path, + build_test_commands: Option<&str>, +) -> std::io::Result { let root = get_git_repo_root(cwd).unwrap_or_else(|| cwd.to_path_buf()); let agents_path = root.join("AGENTS.md"); let plans_path = root.join("PLANS.md"); - let agents_created = write_if_missing(&agents_path, AGENTS_TEMPLATE)?; + let agents_created = write_agents_file(&agents_path, build_test_commands)?; let plans_created = write_if_missing(&plans_path, PLANS_TEMPLATE)?; Ok(GuardrailScaffoldOutcome { @@ -37,3 +40,65 @@ fn write_if_missing(path: &Path, contents: &str) -> std::io::Result { std::fs::write(path, contents)?; Ok(true) } + +fn write_agents_file(path: &Path, build_test_commands: Option<&str>) -> std::io::Result { + if path.exists() { + if let Some(commands) = build_test_commands { + append_build_test_section(path, commands)?; + } + return Ok(false); + } + + let section = format_build_test_section(build_test_commands); + let contents = AGENTS_TEMPLATE.replace("{build_test_section}", §ion); + std::fs::write(path, contents)?; + Ok(true) +} + +fn append_build_test_section(path: &Path, commands: &str) -> std::io::Result<()> { + let mut contents = std::fs::read_to_string(path)?; + if !contents.ends_with('\n') { + contents.push('\n'); + } + contents.push('\n'); + contents.push_str("## Build and test (from onboarding)\n"); + contents.push_str(&format_build_test_section(Some(commands))); + std::fs::write(path, contents)?; + Ok(()) +} + +fn format_build_test_section(commands: Option<&str>) -> String { + let Some(commands) = commands else { + return "- List the main build and test commands here.\n".to_string(); + }; + + let mut items: Vec = Vec::new(); + for line in commands.lines() { + let line = line.trim(); + if line.is_empty() { + continue; + } + if line.contains(',') { + for part in line.split(',') { + let trimmed = part.trim(); + if !trimmed.is_empty() { + items.push(trimmed.to_string()); + } + } + } else { + items.push(line.to_string()); + } + } + + if items.is_empty() { + return "- List the main build and test commands here.\n".to_string(); + } + + let mut out = String::new(); + for item in items { + out.push_str("- "); + out.push_str(&item); + out.push('\n'); + } + out +} diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 9135fa2ee8..fd7cb7ba06 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -386,19 +386,15 @@ async fn run_ratatui_app( let should_show_trust_screen = should_show_trust_screen(&initial_config); let should_show_sophistication_screen = should_show_sophistication_screen(&initial_config, cli.force_onboarding_question); - let should_show_onboarding = should_show_onboarding( - login_status, - &initial_config, - should_show_trust_screen, - should_show_sophistication_screen, - ); - - let config = if should_show_onboarding { + let mut show_welcome_screen = true; + if should_show_sophistication_screen { let onboarding_result = run_onboarding_app( OnboardingScreenArgs { - show_sophistication_screen: should_show_sophistication_screen, - show_login_screen: should_show_login_screen(login_status, &initial_config), - show_trust_screen: should_show_trust_screen, + show_welcome_screen: false, + show_sophistication_screen: true, + show_build_test_commands: true, + show_login_screen: false, + show_trust_screen: false, login_status, auth_manager: auth_manager.clone(), config: initial_config.clone(), @@ -424,12 +420,45 @@ async fn run_ratatui_app( { error!("Failed to persist sophistication onboarding flag: {err}"); } + let build_test_commands = onboarding_result.build_test_commands.clone(); if onboarding_result.sophistication_level == Some(SophisticationLevel::Low) && is_first_time_user - && let Err(err) = scaffold_guardrail_files(&initial_config.cwd) + && let Err(err) = + scaffold_guardrail_files(&initial_config.cwd, build_test_commands.as_deref()) { error!("Failed to scaffold guardrail files: {err}"); } + show_welcome_screen = false; + } + + let should_show_onboarding = + should_show_onboarding(login_status, &initial_config, should_show_trust_screen); + + let config = if should_show_onboarding { + let onboarding_result = run_onboarding_app( + OnboardingScreenArgs { + show_welcome_screen, + show_sophistication_screen: false, + show_build_test_commands: false, + show_login_screen: should_show_login_screen(login_status, &initial_config), + show_trust_screen: should_show_trust_screen, + login_status, + auth_manager: auth_manager.clone(), + config: initial_config.clone(), + }, + &mut tui, + ) + .await?; + if onboarding_result.should_exit { + restore(); + session_log::log_session_end(); + let _ = tui.terminal.clear(); + return Ok(AppExitInfo { + token_usage: codex_core::protocol::TokenUsage::default(), + conversation_id: None, + update_action: None, + }); + } // if the user acknowledged windows or made an explicit decision ato trust the directory, reload the config accordingly if onboarding_result .directory_trust_decision @@ -609,13 +638,12 @@ fn should_show_onboarding( login_status: LoginStatus, config: &Config, show_trust_screen: bool, - show_sophistication_screen: bool, ) -> bool { if show_trust_screen { return true; } - show_sophistication_screen || should_show_login_screen(login_status, config) + should_show_login_screen(login_status, config) } fn should_show_login_screen(login_status: LoginStatus, config: &Config) -> bool { diff --git a/codex-rs/tui/src/onboarding/build_test.rs b/codex-rs/tui/src/onboarding/build_test.rs new file mode 100644 index 0000000000..2f0f1ce0f6 --- /dev/null +++ b/codex-rs/tui/src/onboarding/build_test.rs @@ -0,0 +1,143 @@ +use crossterm::event::KeyCode; +use crossterm::event::KeyEvent; +use crossterm::event::KeyEventKind; +use crossterm::event::KeyModifiers; +use ratatui::buffer::Buffer; +use ratatui::layout::Rect; +use ratatui::style::Stylize; +use ratatui::text::Line; +use ratatui::widgets::Paragraph; +use ratatui::widgets::WidgetRef; +use ratatui::widgets::Wrap; + +use crate::key_hint; +use crate::onboarding::onboarding_screen::KeyboardHandler; +use crate::onboarding::onboarding_screen::StepStateProvider; +use crate::render::Insets; +use crate::render::renderable::ColumnRenderable; +use crate::render::renderable::Renderable; +use crate::render::renderable::RenderableExt as _; + +use super::onboarding_screen::StepState; + +pub(crate) struct BuildTestCommandsWidget { + pub value: String, + submitted: bool, +} + +impl BuildTestCommandsWidget { + pub(crate) fn new() -> Self { + Self { + value: String::new(), + submitted: false, + } + } + + pub(crate) fn commands(&self) -> Option { + if !self.submitted { + return None; + } + let trimmed = self.value.trim(); + if trimmed.is_empty() { + None + } else { + Some(trimmed.to_string()) + } + } + + fn append_text(&mut self, text: &str) { + if text.is_empty() { + return; + } + self.value.push_str(text); + } +} + +impl WidgetRef for &BuildTestCommandsWidget { + fn render_ref(&self, area: Rect, buf: &mut Buffer) { + let mut column = ColumnRenderable::new(); + + column.push(Line::from(vec![ + "> ".into(), + "What are the build and test commands?".bold(), + ])); + column.push(""); + column.push( + Paragraph::new( + "Add the commands you'd like Codex to run (comma-separated or a single command)." + .to_string(), + ) + .wrap(Wrap { trim: true }) + .inset(Insets::tlbr(0, 2, 0, 0)), + ); + column.push(""); + + column.push("Build & test commands:".dim()); + let content_line: Line = if self.value.is_empty() { + vec!["e.g. just fmt, just test, cargo test -p codex-tui2".dim()].into() + } else { + Line::from(self.value.clone()) + }; + column.push(content_line.inset(Insets::tlbr(0, 2, 0, 0))); + + column.push(""); + column.push( + Line::from(vec![ + "Press ".dim(), + key_hint::plain(KeyCode::Enter).into(), + " to continue".dim(), + ]) + .inset(Insets::tlbr(0, 2, 0, 0)), + ); + + column.render(area, buf); + } +} + +impl KeyboardHandler for BuildTestCommandsWidget { + fn handle_key_event(&mut self, key_event: KeyEvent) { + if key_event.kind == KeyEventKind::Release { + return; + } + + match key_event.code { + KeyCode::Enter => self.submitted = true, + KeyCode::Backspace => { + self.value.pop(); + } + KeyCode::Char(c) + if key_event.kind == KeyEventKind::Press + && !key_event.modifiers.contains(KeyModifiers::SUPER) + && !key_event.modifiers.contains(KeyModifiers::CONTROL) + && !key_event.modifiers.contains(KeyModifiers::ALT) => + { + self.value.push(c); + } + _ => {} + } + } + + fn handle_paste(&mut self, pasted: String) { + let trimmed = pasted.trim(); + if trimmed.is_empty() { + return; + } + let cleaned = trimmed + .lines() + .map(str::trim) + .filter(|line| !line.is_empty()) + .collect::>() + .join(" "); + self.append_text(&cleaned); + } +} + +impl StepStateProvider for BuildTestCommandsWidget { + fn get_step_state(&self) -> StepState { + if self.submitted { + StepState::Complete + } else { + StepState::InProgress + } + } +} diff --git a/codex-rs/tui/src/onboarding/mod.rs b/codex-rs/tui/src/onboarding/mod.rs index 118786db04..0175e1a082 100644 --- a/codex-rs/tui/src/onboarding/mod.rs +++ b/codex-rs/tui/src/onboarding/mod.rs @@ -1,4 +1,5 @@ mod auth; +mod build_test; pub mod onboarding_screen; mod sophistication; mod trust_directory; diff --git a/codex-rs/tui/src/onboarding/onboarding_screen.rs b/codex-rs/tui/src/onboarding/onboarding_screen.rs index f4a5fbfc63..9115c011bd 100644 --- a/codex-rs/tui/src/onboarding/onboarding_screen.rs +++ b/codex-rs/tui/src/onboarding/onboarding_screen.rs @@ -17,6 +17,7 @@ use codex_protocol::config_types::ForcedLoginMethod; use crate::LoginStatus; use crate::onboarding::auth::AuthModeWidget; use crate::onboarding::auth::SignInState; +use crate::onboarding::build_test::BuildTestCommandsWidget; use crate::onboarding::sophistication::SophisticationWidget; use crate::onboarding::trust_directory::TrustDirectorySelection; use crate::onboarding::trust_directory::TrustDirectoryWidget; @@ -34,6 +35,7 @@ use super::SophisticationLevel; enum Step { Welcome(WelcomeWidget), Sophistication(SophisticationWidget), + BuildTest(BuildTestCommandsWidget), Auth(AuthModeWidget), TrustDirectory(TrustDirectoryWidget), } @@ -62,7 +64,9 @@ pub(crate) struct OnboardingScreen { } pub(crate) struct OnboardingScreenArgs { + pub show_welcome_screen: bool, pub show_sophistication_screen: bool, + pub show_build_test_commands: bool, pub show_trust_screen: bool, pub show_login_screen: bool, pub login_status: LoginStatus, @@ -73,13 +77,16 @@ pub(crate) struct OnboardingScreenArgs { pub(crate) struct OnboardingResult { pub directory_trust_decision: Option, pub sophistication_level: Option, + pub build_test_commands: Option, pub should_exit: bool, } impl OnboardingScreen { pub(crate) fn new(tui: &mut Tui, args: OnboardingScreenArgs) -> Self { let OnboardingScreenArgs { + show_welcome_screen, show_sophistication_screen, + show_build_test_commands, show_trust_screen, show_login_screen, login_status, @@ -92,14 +99,19 @@ impl OnboardingScreen { let codex_home = config.codex_home; let cli_auth_credentials_store_mode = config.cli_auth_credentials_store_mode; let mut steps: Vec = Vec::new(); - steps.push(Step::Welcome(WelcomeWidget::new( - !matches!(login_status, LoginStatus::NotAuthenticated), - tui.frame_requester(), - config.animations, - ))); + if show_welcome_screen { + steps.push(Step::Welcome(WelcomeWidget::new( + !matches!(login_status, LoginStatus::NotAuthenticated), + tui.frame_requester(), + config.animations, + ))); + } if show_sophistication_screen { steps.push(Step::Sophistication(SophisticationWidget::new())); } + if show_build_test_commands { + steps.push(Step::BuildTest(BuildTestCommandsWidget::new())); + } if show_login_screen { let highlighted_mode = match forced_login_method { Some(ForcedLoginMethod::Api) => AuthMode::ApiKey, @@ -219,6 +231,16 @@ impl OnboardingScreen { .flatten() } + pub fn build_test_commands(&self) -> Option { + self.steps.iter().find_map(|step| { + if let Step::BuildTest(widget) = step { + widget.commands() + } else { + None + } + }) + } + fn is_api_key_entry_active(&self) -> bool { self.steps.iter().any(|step| { if let Step::Auth(widget) = step { @@ -357,6 +379,7 @@ impl KeyboardHandler for Step { match self { Step::Welcome(widget) => widget.handle_key_event(key_event), Step::Sophistication(widget) => widget.handle_key_event(key_event), + Step::BuildTest(widget) => widget.handle_key_event(key_event), Step::Auth(widget) => widget.handle_key_event(key_event), Step::TrustDirectory(widget) => widget.handle_key_event(key_event), } @@ -366,6 +389,7 @@ impl KeyboardHandler for Step { match self { Step::Welcome(_) => {} Step::Sophistication(_) => {} + Step::BuildTest(widget) => widget.handle_paste(pasted), Step::Auth(widget) => widget.handle_paste(pasted), Step::TrustDirectory(widget) => widget.handle_paste(pasted), } @@ -377,6 +401,7 @@ impl StepStateProvider for Step { match self { Step::Welcome(w) => w.get_step_state(), Step::Sophistication(w) => w.get_step_state(), + Step::BuildTest(w) => w.get_step_state(), Step::Auth(w) => w.get_step_state(), Step::TrustDirectory(w) => w.get_step_state(), } @@ -392,6 +417,9 @@ impl WidgetRef for Step { Step::Sophistication(widget) => { widget.render_ref(area, buf); } + Step::BuildTest(widget) => { + widget.render_ref(area, buf); + } Step::Auth(widget) => { widget.render_ref(area, buf); } @@ -469,6 +497,7 @@ pub(crate) async fn run_onboarding_app( Ok(OnboardingResult { directory_trust_decision: onboarding_screen.directory_trust_decision(), sophistication_level: onboarding_screen.sophistication_level(), + build_test_commands: onboarding_screen.build_test_commands(), should_exit: onboarding_screen.should_exit(), }) } diff --git a/codex-rs/tui/src/onboarding/sophistication.rs b/codex-rs/tui/src/onboarding/sophistication.rs index 995150db83..ba10314230 100644 --- a/codex-rs/tui/src/onboarding/sophistication.rs +++ b/codex-rs/tui/src/onboarding/sophistication.rs @@ -5,9 +5,7 @@ use ratatui::buffer::Buffer; use ratatui::layout::Rect; use ratatui::style::Stylize; use ratatui::text::Line; -use ratatui::widgets::Paragraph; use ratatui::widgets::WidgetRef; -use ratatui::widgets::Wrap; use crate::key_hint; use crate::onboarding::onboarding_screen::KeyboardHandler; @@ -72,14 +70,6 @@ impl WidgetRef for &SophisticationWidget { ])); column.push(""); - column.push( - Paragraph::new( - "This helps decide whether to auto-create AGENTS.md and PLANS.md on first run." - .to_string(), - ) - .wrap(Wrap { trim: true }) - .inset(Insets::tlbr(0, 2, 0, 0)), - ); column.push(""); let options = [ diff --git a/codex-rs/tui2/src/lib.rs b/codex-rs/tui2/src/lib.rs index 2bd15d562a..b7421bb5f9 100644 --- a/codex-rs/tui2/src/lib.rs +++ b/codex-rs/tui2/src/lib.rs @@ -393,19 +393,15 @@ async fn run_ratatui_app( let should_show_trust_screen = should_show_trust_screen(&initial_config); let should_show_sophistication_screen = should_show_sophistication_screen(&initial_config, cli.force_onboarding_question); - let should_show_onboarding = should_show_onboarding( - login_status, - &initial_config, - should_show_trust_screen, - should_show_sophistication_screen, - ); - - let config = if should_show_onboarding { + let mut show_welcome_screen = true; + if should_show_sophistication_screen { let onboarding_result = run_onboarding_app( OnboardingScreenArgs { - show_sophistication_screen: should_show_sophistication_screen, - show_login_screen: should_show_login_screen(login_status, &initial_config), - show_trust_screen: should_show_trust_screen, + show_welcome_screen: false, + show_sophistication_screen: true, + show_build_test_commands: true, + show_login_screen: false, + show_trust_screen: false, login_status, auth_manager: auth_manager.clone(), config: initial_config.clone(), @@ -432,12 +428,46 @@ async fn run_ratatui_app( { error!("Failed to persist sophistication onboarding flag: {err}"); } + let build_test_commands = onboarding_result.build_test_commands.clone(); if onboarding_result.sophistication_level == Some(SophisticationLevel::Low) && is_first_time_user - && let Err(err) = scaffold_guardrail_files(&initial_config.cwd) + && let Err(err) = + scaffold_guardrail_files(&initial_config.cwd, build_test_commands.as_deref()) { error!("Failed to scaffold guardrail files: {err}"); } + show_welcome_screen = false; + } + + let should_show_onboarding = + should_show_onboarding(login_status, &initial_config, should_show_trust_screen); + + let config = if should_show_onboarding { + let onboarding_result = run_onboarding_app( + OnboardingScreenArgs { + show_welcome_screen, + show_sophistication_screen: false, + show_build_test_commands: false, + show_login_screen: should_show_login_screen(login_status, &initial_config), + show_trust_screen: should_show_trust_screen, + login_status, + auth_manager: auth_manager.clone(), + config: initial_config.clone(), + }, + &mut tui, + ) + .await?; + if onboarding_result.should_exit { + restore(); + session_log::log_session_end(); + let _ = tui.terminal.clear(); + return Ok(AppExitInfo { + token_usage: codex_core::protocol::TokenUsage::default(), + conversation_id: None, + update_action: None, + session_lines: Vec::new(), + }); + } // if the user acknowledged windows or made an explicit decision ato trust the directory, reload the config accordingly if onboarding_result .directory_trust_decision @@ -635,13 +665,12 @@ fn should_show_onboarding( login_status: LoginStatus, config: &Config, show_trust_screen: bool, - show_sophistication_screen: bool, ) -> bool { if show_trust_screen { return true; } - show_sophistication_screen || should_show_login_screen(login_status, config) + should_show_login_screen(login_status, config) } fn should_show_login_screen(login_status: LoginStatus, config: &Config) -> bool { diff --git a/codex-rs/tui2/src/onboarding/build_test.rs b/codex-rs/tui2/src/onboarding/build_test.rs new file mode 100644 index 0000000000..2f0f1ce0f6 --- /dev/null +++ b/codex-rs/tui2/src/onboarding/build_test.rs @@ -0,0 +1,143 @@ +use crossterm::event::KeyCode; +use crossterm::event::KeyEvent; +use crossterm::event::KeyEventKind; +use crossterm::event::KeyModifiers; +use ratatui::buffer::Buffer; +use ratatui::layout::Rect; +use ratatui::style::Stylize; +use ratatui::text::Line; +use ratatui::widgets::Paragraph; +use ratatui::widgets::WidgetRef; +use ratatui::widgets::Wrap; + +use crate::key_hint; +use crate::onboarding::onboarding_screen::KeyboardHandler; +use crate::onboarding::onboarding_screen::StepStateProvider; +use crate::render::Insets; +use crate::render::renderable::ColumnRenderable; +use crate::render::renderable::Renderable; +use crate::render::renderable::RenderableExt as _; + +use super::onboarding_screen::StepState; + +pub(crate) struct BuildTestCommandsWidget { + pub value: String, + submitted: bool, +} + +impl BuildTestCommandsWidget { + pub(crate) fn new() -> Self { + Self { + value: String::new(), + submitted: false, + } + } + + pub(crate) fn commands(&self) -> Option { + if !self.submitted { + return None; + } + let trimmed = self.value.trim(); + if trimmed.is_empty() { + None + } else { + Some(trimmed.to_string()) + } + } + + fn append_text(&mut self, text: &str) { + if text.is_empty() { + return; + } + self.value.push_str(text); + } +} + +impl WidgetRef for &BuildTestCommandsWidget { + fn render_ref(&self, area: Rect, buf: &mut Buffer) { + let mut column = ColumnRenderable::new(); + + column.push(Line::from(vec![ + "> ".into(), + "What are the build and test commands?".bold(), + ])); + column.push(""); + column.push( + Paragraph::new( + "Add the commands you'd like Codex to run (comma-separated or a single command)." + .to_string(), + ) + .wrap(Wrap { trim: true }) + .inset(Insets::tlbr(0, 2, 0, 0)), + ); + column.push(""); + + column.push("Build & test commands:".dim()); + let content_line: Line = if self.value.is_empty() { + vec!["e.g. just fmt, just test, cargo test -p codex-tui2".dim()].into() + } else { + Line::from(self.value.clone()) + }; + column.push(content_line.inset(Insets::tlbr(0, 2, 0, 0))); + + column.push(""); + column.push( + Line::from(vec![ + "Press ".dim(), + key_hint::plain(KeyCode::Enter).into(), + " to continue".dim(), + ]) + .inset(Insets::tlbr(0, 2, 0, 0)), + ); + + column.render(area, buf); + } +} + +impl KeyboardHandler for BuildTestCommandsWidget { + fn handle_key_event(&mut self, key_event: KeyEvent) { + if key_event.kind == KeyEventKind::Release { + return; + } + + match key_event.code { + KeyCode::Enter => self.submitted = true, + KeyCode::Backspace => { + self.value.pop(); + } + KeyCode::Char(c) + if key_event.kind == KeyEventKind::Press + && !key_event.modifiers.contains(KeyModifiers::SUPER) + && !key_event.modifiers.contains(KeyModifiers::CONTROL) + && !key_event.modifiers.contains(KeyModifiers::ALT) => + { + self.value.push(c); + } + _ => {} + } + } + + fn handle_paste(&mut self, pasted: String) { + let trimmed = pasted.trim(); + if trimmed.is_empty() { + return; + } + let cleaned = trimmed + .lines() + .map(str::trim) + .filter(|line| !line.is_empty()) + .collect::>() + .join(" "); + self.append_text(&cleaned); + } +} + +impl StepStateProvider for BuildTestCommandsWidget { + fn get_step_state(&self) -> StepState { + if self.submitted { + StepState::Complete + } else { + StepState::InProgress + } + } +} diff --git a/codex-rs/tui2/src/onboarding/mod.rs b/codex-rs/tui2/src/onboarding/mod.rs index 118786db04..0175e1a082 100644 --- a/codex-rs/tui2/src/onboarding/mod.rs +++ b/codex-rs/tui2/src/onboarding/mod.rs @@ -1,4 +1,5 @@ mod auth; +mod build_test; pub mod onboarding_screen; mod sophistication; mod trust_directory; diff --git a/codex-rs/tui2/src/onboarding/onboarding_screen.rs b/codex-rs/tui2/src/onboarding/onboarding_screen.rs index 108365ef22..d2e52dd1c7 100644 --- a/codex-rs/tui2/src/onboarding/onboarding_screen.rs +++ b/codex-rs/tui2/src/onboarding/onboarding_screen.rs @@ -17,6 +17,7 @@ use codex_protocol::config_types::ForcedLoginMethod; use crate::LoginStatus; use crate::onboarding::auth::AuthModeWidget; use crate::onboarding::auth::SignInState; +use crate::onboarding::build_test::BuildTestCommandsWidget; use crate::onboarding::sophistication::SophisticationWidget; use crate::onboarding::trust_directory::TrustDirectorySelection; use crate::onboarding::trust_directory::TrustDirectoryWidget; @@ -34,6 +35,7 @@ use super::SophisticationLevel; enum Step { Welcome(WelcomeWidget), Sophistication(SophisticationWidget), + BuildTest(BuildTestCommandsWidget), Auth(AuthModeWidget), TrustDirectory(TrustDirectoryWidget), } @@ -62,7 +64,9 @@ pub(crate) struct OnboardingScreen { } pub(crate) struct OnboardingScreenArgs { + pub show_welcome_screen: bool, pub show_sophistication_screen: bool, + pub show_build_test_commands: bool, pub show_trust_screen: bool, pub show_login_screen: bool, pub login_status: LoginStatus, @@ -73,13 +77,16 @@ pub(crate) struct OnboardingScreenArgs { pub(crate) struct OnboardingResult { pub directory_trust_decision: Option, pub sophistication_level: Option, + pub build_test_commands: Option, pub should_exit: bool, } impl OnboardingScreen { pub(crate) fn new(tui: &mut Tui, args: OnboardingScreenArgs) -> Self { let OnboardingScreenArgs { + show_welcome_screen, show_sophistication_screen, + show_build_test_commands, show_trust_screen, show_login_screen, login_status, @@ -92,14 +99,19 @@ impl OnboardingScreen { let codex_home = config.codex_home; let cli_auth_credentials_store_mode = config.cli_auth_credentials_store_mode; let mut steps: Vec = Vec::new(); - steps.push(Step::Welcome(WelcomeWidget::new( - !matches!(login_status, LoginStatus::NotAuthenticated), - tui.frame_requester(), - config.animations, - ))); + if show_welcome_screen { + steps.push(Step::Welcome(WelcomeWidget::new( + !matches!(login_status, LoginStatus::NotAuthenticated), + tui.frame_requester(), + config.animations, + ))); + } if show_sophistication_screen { steps.push(Step::Sophistication(SophisticationWidget::new())); } + if show_build_test_commands { + steps.push(Step::BuildTest(BuildTestCommandsWidget::new())); + } if show_login_screen { let highlighted_mode = match forced_login_method { Some(ForcedLoginMethod::Api) => AuthMode::ApiKey, @@ -219,6 +231,16 @@ impl OnboardingScreen { .flatten() } + pub fn build_test_commands(&self) -> Option { + self.steps.iter().find_map(|step| { + if let Step::BuildTest(widget) = step { + widget.commands() + } else { + None + } + }) + } + fn is_api_key_entry_active(&self) -> bool { self.steps.iter().any(|step| { if let Step::Auth(widget) = step { @@ -357,6 +379,7 @@ impl KeyboardHandler for Step { match self { Step::Welcome(widget) => widget.handle_key_event(key_event), Step::Sophistication(widget) => widget.handle_key_event(key_event), + Step::BuildTest(widget) => widget.handle_key_event(key_event), Step::Auth(widget) => widget.handle_key_event(key_event), Step::TrustDirectory(widget) => widget.handle_key_event(key_event), } @@ -366,6 +389,7 @@ impl KeyboardHandler for Step { match self { Step::Welcome(_) => {} Step::Sophistication(_) => {} + Step::BuildTest(widget) => widget.handle_paste(pasted), Step::Auth(widget) => widget.handle_paste(pasted), Step::TrustDirectory(widget) => widget.handle_paste(pasted), } @@ -377,6 +401,7 @@ impl StepStateProvider for Step { match self { Step::Welcome(w) => w.get_step_state(), Step::Sophistication(w) => w.get_step_state(), + Step::BuildTest(w) => w.get_step_state(), Step::Auth(w) => w.get_step_state(), Step::TrustDirectory(w) => w.get_step_state(), } @@ -392,6 +417,9 @@ impl WidgetRef for Step { Step::Sophistication(widget) => { widget.render_ref(area, buf); } + Step::BuildTest(widget) => { + widget.render_ref(area, buf); + } Step::Auth(widget) => { widget.render_ref(area, buf); } @@ -470,6 +498,7 @@ pub(crate) async fn run_onboarding_app( Ok(OnboardingResult { directory_trust_decision: onboarding_screen.directory_trust_decision(), sophistication_level: onboarding_screen.sophistication_level(), + build_test_commands: onboarding_screen.build_test_commands(), should_exit: onboarding_screen.should_exit(), }) } diff --git a/codex-rs/tui2/src/onboarding/sophistication.rs b/codex-rs/tui2/src/onboarding/sophistication.rs index 995150db83..ba10314230 100644 --- a/codex-rs/tui2/src/onboarding/sophistication.rs +++ b/codex-rs/tui2/src/onboarding/sophistication.rs @@ -5,9 +5,7 @@ use ratatui::buffer::Buffer; use ratatui::layout::Rect; use ratatui::style::Stylize; use ratatui::text::Line; -use ratatui::widgets::Paragraph; use ratatui::widgets::WidgetRef; -use ratatui::widgets::Wrap; use crate::key_hint; use crate::onboarding::onboarding_screen::KeyboardHandler; @@ -72,14 +70,6 @@ impl WidgetRef for &SophisticationWidget { ])); column.push(""); - column.push( - Paragraph::new( - "This helps decide whether to auto-create AGENTS.md and PLANS.md on first run." - .to_string(), - ) - .wrap(Wrap { trim: true }) - .inset(Insets::tlbr(0, 2, 0, 0)), - ); column.push(""); let options = [