Migrate GitWarning to OnboardingScreen (#1915)
This paves the way to do per-directory approval settings (https://github.com/openai/codex/pull/1912). This also lets us pass in a Config/ChatWidgetArgs into onboarding which can then mutate it and emit the ChatWidgetArgs it wants at the end which may be modified by the said approval settings. <img width="1180" height="428" alt="CleanShot 2025-08-06 at 19 30 55" src="https://github.com/user-attachments/assets/4dcfda42-0f5e-4b6d-a16d-2597109cc31c" />
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
use std::path::Path;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@@ -5,8 +6,6 @@ use rand::Rng;
|
|||||||
use tokio::sync::Notify;
|
use tokio::sync::Notify;
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
||||||
use crate::config::Config;
|
|
||||||
|
|
||||||
const INITIAL_DELAY_MS: u64 = 200;
|
const INITIAL_DELAY_MS: u64 = 200;
|
||||||
const BACKOFF_FACTOR: f64 = 1.3;
|
const BACKOFF_FACTOR: f64 = 1.3;
|
||||||
|
|
||||||
@@ -47,8 +46,8 @@ pub(crate) fn backoff(attempt: u64) -> Duration {
|
|||||||
/// `git worktree add` where the checkout lives outside the main repository
|
/// `git worktree add` where the checkout lives outside the main repository
|
||||||
/// directory. If you need Codex to work from such a checkout simply pass the
|
/// directory. If you need Codex to work from such a checkout simply pass the
|
||||||
/// `--allow-no-git-exec` CLI flag that disables the repo requirement.
|
/// `--allow-no-git-exec` CLI flag that disables the repo requirement.
|
||||||
pub fn is_inside_git_repo(config: &Config) -> bool {
|
pub fn is_inside_git_repo(base_dir: &Path) -> bool {
|
||||||
let mut dir = config.cwd.to_path_buf();
|
let mut dir = base_dir.to_path_buf();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
if dir.join(".git").exists() {
|
if dir.join(".git").exists() {
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
|
|||||||
// is using.
|
// is using.
|
||||||
event_processor.print_config_summary(&config, &prompt);
|
event_processor.print_config_summary(&config, &prompt);
|
||||||
|
|
||||||
if !skip_git_repo_check && !is_inside_git_repo(&config) {
|
if !skip_git_repo_check && !is_inside_git_repo(&config.cwd.to_path_buf()) {
|
||||||
eprintln!("Not inside a Git repo and --skip-git-repo-check was not specified.");
|
eprintln!("Not inside a Git repo and --skip-git-repo-check was not specified.");
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,9 @@ use crate::app_event_sender::AppEventSender;
|
|||||||
use crate::chatwidget::ChatWidget;
|
use crate::chatwidget::ChatWidget;
|
||||||
use crate::file_search::FileSearchManager;
|
use crate::file_search::FileSearchManager;
|
||||||
use crate::get_git_diff::get_git_diff;
|
use crate::get_git_diff::get_git_diff;
|
||||||
use crate::git_warning_screen::GitWarningOutcome;
|
|
||||||
use crate::git_warning_screen::GitWarningScreen;
|
|
||||||
use crate::onboarding::onboarding_screen::KeyEventResult;
|
|
||||||
use crate::onboarding::onboarding_screen::KeyboardHandler;
|
use crate::onboarding::onboarding_screen::KeyboardHandler;
|
||||||
use crate::onboarding::onboarding_screen::OnboardingScreen;
|
use crate::onboarding::onboarding_screen::OnboardingScreen;
|
||||||
|
use crate::onboarding::onboarding_screen::OnboardingScreenArgs;
|
||||||
use crate::should_show_login_screen;
|
use crate::should_show_login_screen;
|
||||||
use crate::slash_command::SlashCommand;
|
use crate::slash_command::SlashCommand;
|
||||||
use crate::tui;
|
use crate::tui;
|
||||||
@@ -15,6 +13,7 @@ use codex_core::config::Config;
|
|||||||
use codex_core::protocol::Event;
|
use codex_core::protocol::Event;
|
||||||
use codex_core::protocol::EventMsg;
|
use codex_core::protocol::EventMsg;
|
||||||
use codex_core::protocol::Op;
|
use codex_core::protocol::Op;
|
||||||
|
use codex_core::util::is_inside_git_repo;
|
||||||
use color_eyre::eyre::Result;
|
use color_eyre::eyre::Result;
|
||||||
use crossterm::SynchronizedUpdate;
|
use crossterm::SynchronizedUpdate;
|
||||||
use crossterm::event::KeyCode;
|
use crossterm::event::KeyCode;
|
||||||
@@ -48,10 +47,6 @@ enum AppState<'a> {
|
|||||||
/// `AppState`.
|
/// `AppState`.
|
||||||
widget: Box<ChatWidget<'a>>,
|
widget: Box<ChatWidget<'a>>,
|
||||||
},
|
},
|
||||||
/// The start-up warning that recommends running codex inside a Git repo.
|
|
||||||
GitWarning {
|
|
||||||
screen: GitWarningScreen,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct App<'a> {
|
pub(crate) struct App<'a> {
|
||||||
@@ -69,17 +64,13 @@ pub(crate) struct App<'a> {
|
|||||||
|
|
||||||
pending_history_lines: Vec<Line<'static>>,
|
pending_history_lines: Vec<Line<'static>>,
|
||||||
|
|
||||||
/// Stored parameters needed to instantiate the ChatWidget later, e.g.,
|
|
||||||
/// after dismissing the Git-repo warning.
|
|
||||||
chat_args: Option<ChatWidgetArgs>,
|
|
||||||
|
|
||||||
enhanced_keys_supported: bool,
|
enhanced_keys_supported: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Aggregate parameters needed to create a `ChatWidget`, as creation may be
|
/// Aggregate parameters needed to create a `ChatWidget`, as creation may be
|
||||||
/// deferred until after the Git warning screen is dismissed.
|
/// deferred until after the Git warning screen is dismissed.
|
||||||
#[derive(Clone)]
|
#[derive(Clone, Debug)]
|
||||||
struct ChatWidgetArgs {
|
pub(crate) struct ChatWidgetArgs {
|
||||||
config: Config,
|
config: Config,
|
||||||
initial_prompt: Option<String>,
|
initial_prompt: Option<String>,
|
||||||
initial_images: Vec<PathBuf>,
|
initial_images: Vec<PathBuf>,
|
||||||
@@ -90,7 +81,7 @@ impl App<'_> {
|
|||||||
pub(crate) fn new(
|
pub(crate) fn new(
|
||||||
config: Config,
|
config: Config,
|
||||||
initial_prompt: Option<String>,
|
initial_prompt: Option<String>,
|
||||||
show_git_warning: bool,
|
skip_git_repo_check: bool,
|
||||||
initial_images: Vec<std::path::PathBuf>,
|
initial_images: Vec<std::path::PathBuf>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let (app_event_tx, app_event_rx) = channel();
|
let (app_event_tx, app_event_rx) = channel();
|
||||||
@@ -143,30 +134,25 @@ impl App<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let show_login_screen = should_show_login_screen(&config);
|
let show_login_screen = should_show_login_screen(&config);
|
||||||
let (app_state, chat_args) = if show_login_screen {
|
let show_git_warning =
|
||||||
(
|
!skip_git_repo_check && !is_inside_git_repo(&config.cwd.to_path_buf());
|
||||||
AppState::Onboarding {
|
let app_state = if show_login_screen || show_git_warning {
|
||||||
screen: OnboardingScreen::new(app_event_tx.clone(), config.codex_home.clone()),
|
let chat_widget_args = ChatWidgetArgs {
|
||||||
},
|
config: config.clone(),
|
||||||
Some(ChatWidgetArgs {
|
initial_prompt,
|
||||||
config: config.clone(),
|
initial_images,
|
||||||
initial_prompt,
|
enhanced_keys_supported,
|
||||||
initial_images,
|
};
|
||||||
enhanced_keys_supported,
|
AppState::Onboarding {
|
||||||
|
screen: OnboardingScreen::new(OnboardingScreenArgs {
|
||||||
|
event_tx: app_event_tx.clone(),
|
||||||
|
codex_home: config.codex_home.clone(),
|
||||||
|
cwd: config.cwd.clone(),
|
||||||
|
show_login_screen,
|
||||||
|
show_git_warning,
|
||||||
|
chat_widget_args,
|
||||||
}),
|
}),
|
||||||
)
|
}
|
||||||
} else if show_git_warning {
|
|
||||||
(
|
|
||||||
AppState::GitWarning {
|
|
||||||
screen: GitWarningScreen::new(),
|
|
||||||
},
|
|
||||||
Some(ChatWidgetArgs {
|
|
||||||
config: config.clone(),
|
|
||||||
initial_prompt,
|
|
||||||
initial_images,
|
|
||||||
enhanced_keys_supported,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
let chat_widget = ChatWidget::new(
|
let chat_widget = ChatWidget::new(
|
||||||
config.clone(),
|
config.clone(),
|
||||||
@@ -175,12 +161,9 @@ impl App<'_> {
|
|||||||
initial_images,
|
initial_images,
|
||||||
enhanced_keys_supported,
|
enhanced_keys_supported,
|
||||||
);
|
);
|
||||||
(
|
AppState::Chat {
|
||||||
AppState::Chat {
|
widget: Box::new(chat_widget),
|
||||||
widget: Box::new(chat_widget),
|
}
|
||||||
},
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let file_search = FileSearchManager::new(config.cwd.clone(), app_event_tx.clone());
|
let file_search = FileSearchManager::new(config.cwd.clone(), app_event_tx.clone());
|
||||||
@@ -192,7 +175,6 @@ impl App<'_> {
|
|||||||
config,
|
config,
|
||||||
file_search,
|
file_search,
|
||||||
pending_redraw,
|
pending_redraw,
|
||||||
chat_args,
|
|
||||||
enhanced_keys_supported,
|
enhanced_keys_supported,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -249,20 +231,14 @@ impl App<'_> {
|
|||||||
modifiers: crossterm::event::KeyModifiers::CONTROL,
|
modifiers: crossterm::event::KeyModifiers::CONTROL,
|
||||||
kind: KeyEventKind::Press,
|
kind: KeyEventKind::Press,
|
||||||
..
|
..
|
||||||
} => {
|
} => match &mut self.app_state {
|
||||||
match &mut self.app_state {
|
AppState::Chat { widget } => {
|
||||||
AppState::Chat { widget } => {
|
widget.on_ctrl_c();
|
||||||
widget.on_ctrl_c();
|
|
||||||
}
|
|
||||||
AppState::Onboarding { .. } => {
|
|
||||||
self.app_event_tx.send(AppEvent::ExitRequest);
|
|
||||||
}
|
|
||||||
AppState::GitWarning { .. } => {
|
|
||||||
// Allow exiting the app with Ctrl+C from the warning screen.
|
|
||||||
self.app_event_tx.send(AppEvent::ExitRequest);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
AppState::Onboarding { .. } => {
|
||||||
|
self.app_event_tx.send(AppEvent::ExitRequest);
|
||||||
|
}
|
||||||
|
},
|
||||||
KeyEvent {
|
KeyEvent {
|
||||||
code: KeyCode::Char('z'),
|
code: KeyCode::Char('z'),
|
||||||
modifiers: crossterm::event::KeyModifiers::CONTROL,
|
modifiers: crossterm::event::KeyModifiers::CONTROL,
|
||||||
@@ -293,9 +269,6 @@ impl App<'_> {
|
|||||||
AppState::Onboarding { .. } => {
|
AppState::Onboarding { .. } => {
|
||||||
self.app_event_tx.send(AppEvent::ExitRequest);
|
self.app_event_tx.send(AppEvent::ExitRequest);
|
||||||
}
|
}
|
||||||
AppState::GitWarning { .. } => {
|
|
||||||
self.app_event_tx.send(AppEvent::ExitRequest);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
KeyEvent {
|
KeyEvent {
|
||||||
@@ -321,15 +294,14 @@ impl App<'_> {
|
|||||||
AppEvent::CodexOp(op) => match &mut self.app_state {
|
AppEvent::CodexOp(op) => match &mut self.app_state {
|
||||||
AppState::Chat { widget } => widget.submit_op(op),
|
AppState::Chat { widget } => widget.submit_op(op),
|
||||||
AppState::Onboarding { .. } => {}
|
AppState::Onboarding { .. } => {}
|
||||||
AppState::GitWarning { .. } => {}
|
|
||||||
},
|
},
|
||||||
AppEvent::LatestLog(line) => match &mut self.app_state {
|
AppEvent::LatestLog(line) => match &mut self.app_state {
|
||||||
AppState::Chat { widget } => widget.update_latest_log(line),
|
AppState::Chat { widget } => widget.update_latest_log(line),
|
||||||
AppState::Onboarding { .. } => {}
|
AppState::Onboarding { .. } => {}
|
||||||
AppState::GitWarning { .. } => {}
|
|
||||||
},
|
},
|
||||||
AppEvent::DispatchCommand(command) => match command {
|
AppEvent::DispatchCommand(command) => match command {
|
||||||
SlashCommand::New => {
|
SlashCommand::New => {
|
||||||
|
// User accepted – switch to chat view.
|
||||||
let new_widget = Box::new(ChatWidget::new(
|
let new_widget = Box::new(ChatWidget::new(
|
||||||
self.config.clone(),
|
self.config.clone(),
|
||||||
self.app_event_tx.clone(),
|
self.app_event_tx.clone(),
|
||||||
@@ -424,8 +396,23 @@ impl App<'_> {
|
|||||||
},
|
},
|
||||||
AppEvent::OnboardingAuthComplete(result) => {
|
AppEvent::OnboardingAuthComplete(result) => {
|
||||||
if let AppState::Onboarding { screen } = &mut self.app_state {
|
if let AppState::Onboarding { screen } = &mut self.app_state {
|
||||||
// Let the onboarding screen handle success/failure and emit follow-up events.
|
screen.on_auth_complete(result);
|
||||||
let _ = screen.on_auth_complete(result);
|
}
|
||||||
|
}
|
||||||
|
AppEvent::OnboardingComplete(ChatWidgetArgs {
|
||||||
|
config,
|
||||||
|
enhanced_keys_supported,
|
||||||
|
initial_images,
|
||||||
|
initial_prompt,
|
||||||
|
}) => {
|
||||||
|
self.app_state = AppState::Chat {
|
||||||
|
widget: Box::new(ChatWidget::new(
|
||||||
|
config,
|
||||||
|
app_event_tx.clone(),
|
||||||
|
initial_prompt,
|
||||||
|
initial_images,
|
||||||
|
enhanced_keys_supported,
|
||||||
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AppEvent::StartFileSearch(query) => {
|
AppEvent::StartFileSearch(query) => {
|
||||||
@@ -447,7 +434,6 @@ impl App<'_> {
|
|||||||
match &self.app_state {
|
match &self.app_state {
|
||||||
AppState::Chat { widget } => widget.token_usage().clone(),
|
AppState::Chat { widget } => widget.token_usage().clone(),
|
||||||
AppState::Onboarding { .. } => codex_core::protocol::TokenUsage::default(),
|
AppState::Onboarding { .. } => codex_core::protocol::TokenUsage::default(),
|
||||||
AppState::GitWarning { .. } => codex_core::protocol::TokenUsage::default(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -476,7 +462,6 @@ impl App<'_> {
|
|||||||
let desired_height = match &self.app_state {
|
let desired_height = match &self.app_state {
|
||||||
AppState::Chat { widget } => widget.desired_height(size.width),
|
AppState::Chat { widget } => widget.desired_height(size.width),
|
||||||
AppState::Onboarding { .. } => size.height,
|
AppState::Onboarding { .. } => size.height,
|
||||||
AppState::GitWarning { .. } => size.height,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut area = terminal.viewport_area;
|
let mut area = terminal.viewport_area;
|
||||||
@@ -507,7 +492,6 @@ impl App<'_> {
|
|||||||
frame.render_widget_ref(&**widget, frame.area())
|
frame.render_widget_ref(&**widget, frame.area())
|
||||||
}
|
}
|
||||||
AppState::Onboarding { screen } => frame.render_widget_ref(&*screen, frame.area()),
|
AppState::Onboarding { screen } => frame.render_widget_ref(&*screen, frame.area()),
|
||||||
AppState::GitWarning { screen } => frame.render_widget_ref(&*screen, frame.area()),
|
|
||||||
})?;
|
})?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -519,49 +503,11 @@ impl App<'_> {
|
|||||||
AppState::Chat { widget } => {
|
AppState::Chat { widget } => {
|
||||||
widget.handle_key_event(key_event);
|
widget.handle_key_event(key_event);
|
||||||
}
|
}
|
||||||
AppState::Onboarding { screen } => match screen.handle_key_event(key_event) {
|
AppState::Onboarding { screen } => match key_event.code {
|
||||||
KeyEventResult::Continue => {
|
KeyCode::Char('q') => {
|
||||||
self.app_state = AppState::Chat {
|
|
||||||
widget: Box::new(ChatWidget::new(
|
|
||||||
self.config.clone(),
|
|
||||||
self.app_event_tx.clone(),
|
|
||||||
None,
|
|
||||||
Vec::new(),
|
|
||||||
self.enhanced_keys_supported,
|
|
||||||
)),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
KeyEventResult::Quit => {
|
|
||||||
self.app_event_tx.send(AppEvent::ExitRequest);
|
self.app_event_tx.send(AppEvent::ExitRequest);
|
||||||
}
|
}
|
||||||
KeyEventResult::None => {
|
_ => screen.handle_key_event(key_event),
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
},
|
|
||||||
AppState::GitWarning { screen } => match screen.handle_key_event(key_event) {
|
|
||||||
GitWarningOutcome::Continue => {
|
|
||||||
// User accepted – switch to chat view.
|
|
||||||
let args = match self.chat_args.take() {
|
|
||||||
Some(args) => args,
|
|
||||||
None => panic!("ChatWidgetArgs already consumed"),
|
|
||||||
};
|
|
||||||
|
|
||||||
let widget = Box::new(ChatWidget::new(
|
|
||||||
args.config,
|
|
||||||
self.app_event_tx.clone(),
|
|
||||||
args.initial_prompt,
|
|
||||||
args.initial_images,
|
|
||||||
args.enhanced_keys_supported,
|
|
||||||
));
|
|
||||||
self.app_state = AppState::Chat { widget };
|
|
||||||
self.app_event_tx.send(AppEvent::RequestRedraw);
|
|
||||||
}
|
|
||||||
GitWarningOutcome::Quit => {
|
|
||||||
self.app_event_tx.send(AppEvent::ExitRequest);
|
|
||||||
}
|
|
||||||
GitWarningOutcome::None => {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -570,7 +516,6 @@ impl App<'_> {
|
|||||||
match &mut self.app_state {
|
match &mut self.app_state {
|
||||||
AppState::Chat { widget } => widget.handle_paste(pasted),
|
AppState::Chat { widget } => widget.handle_paste(pasted),
|
||||||
AppState::Onboarding { .. } => {}
|
AppState::Onboarding { .. } => {}
|
||||||
AppState::GitWarning { .. } => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -578,7 +523,6 @@ impl App<'_> {
|
|||||||
match &mut self.app_state {
|
match &mut self.app_state {
|
||||||
AppState::Chat { widget } => widget.handle_codex_event(event),
|
AppState::Chat { widget } => widget.handle_codex_event(event),
|
||||||
AppState::Onboarding { .. } => {}
|
AppState::Onboarding { .. } => {}
|
||||||
AppState::GitWarning { .. } => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use codex_file_search::FileMatch;
|
|||||||
use crossterm::event::KeyEvent;
|
use crossterm::event::KeyEvent;
|
||||||
use ratatui::text::Line;
|
use ratatui::text::Line;
|
||||||
|
|
||||||
|
use crate::app::ChatWidgetArgs;
|
||||||
use crate::slash_command::SlashCommand;
|
use crate::slash_command::SlashCommand;
|
||||||
|
|
||||||
#[allow(clippy::large_enum_variant)]
|
#[allow(clippy::large_enum_variant)]
|
||||||
@@ -51,4 +52,5 @@ pub(crate) enum AppEvent {
|
|||||||
|
|
||||||
/// Onboarding: result of login_with_chatgpt.
|
/// Onboarding: result of login_with_chatgpt.
|
||||||
OnboardingAuthComplete(Result<(), String>),
|
OnboardingAuthComplete(Result<(), String>),
|
||||||
|
OnboardingComplete(ChatWidgetArgs),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use crate::app_event::AppEvent;
|
|||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub(crate) struct AppEventSender {
|
pub(crate) struct AppEventSender {
|
||||||
app_event_tx: Sender<AppEvent>,
|
pub app_event_tx: Sender<AppEvent>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppEventSender {
|
impl AppEventSender {
|
||||||
|
|||||||
@@ -1,122 +0,0 @@
|
|||||||
//! Full‑screen warning displayed when Codex is started outside a Git
|
|
||||||
//! repository (unless the user passed `--allow-no-git-exec`). The screen
|
|
||||||
//! blocks all input until the user explicitly decides whether to continue or
|
|
||||||
//! quit.
|
|
||||||
|
|
||||||
use crossterm::event::KeyCode;
|
|
||||||
use crossterm::event::KeyEvent;
|
|
||||||
use ratatui::buffer::Buffer;
|
|
||||||
use ratatui::layout::Alignment;
|
|
||||||
use ratatui::layout::Constraint;
|
|
||||||
use ratatui::layout::Direction;
|
|
||||||
use ratatui::layout::Layout;
|
|
||||||
use ratatui::layout::Rect;
|
|
||||||
use ratatui::style::Color;
|
|
||||||
use ratatui::style::Modifier;
|
|
||||||
use ratatui::style::Style;
|
|
||||||
use ratatui::text::Span;
|
|
||||||
use ratatui::widgets::Block;
|
|
||||||
use ratatui::widgets::BorderType;
|
|
||||||
use ratatui::widgets::Borders;
|
|
||||||
use ratatui::widgets::Paragraph;
|
|
||||||
use ratatui::widgets::Widget;
|
|
||||||
use ratatui::widgets::WidgetRef;
|
|
||||||
use ratatui::widgets::Wrap;
|
|
||||||
|
|
||||||
const NO_GIT_ERROR: &str = "We recommend running codex inside a git repository. \
|
|
||||||
This helps ensure that changes can be tracked and easily rolled back if necessary. \
|
|
||||||
Do you wish to proceed?";
|
|
||||||
|
|
||||||
/// Result of handling a key event while the warning screen is active.
|
|
||||||
pub(crate) enum GitWarningOutcome {
|
|
||||||
/// User chose to proceed – switch to the main Chat UI.
|
|
||||||
Continue,
|
|
||||||
/// User opted to quit the application.
|
|
||||||
Quit,
|
|
||||||
/// No actionable key was pressed – stay on the warning screen.
|
|
||||||
None,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) struct GitWarningScreen;
|
|
||||||
|
|
||||||
impl GitWarningScreen {
|
|
||||||
pub(crate) fn new() -> Self {
|
|
||||||
Self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle a key event, returning an outcome indicating whether the user
|
|
||||||
/// chose to continue, quit, or neither.
|
|
||||||
pub(crate) fn handle_key_event(&self, key_event: KeyEvent) -> GitWarningOutcome {
|
|
||||||
match key_event.code {
|
|
||||||
KeyCode::Char('y') | KeyCode::Char('Y') => GitWarningOutcome::Continue,
|
|
||||||
KeyCode::Char('n') | KeyCode::Char('q') | KeyCode::Esc => GitWarningOutcome::Quit,
|
|
||||||
_ => GitWarningOutcome::None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WidgetRef for &GitWarningScreen {
|
|
||||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
|
||||||
const MIN_WIDTH: u16 = 35;
|
|
||||||
const MIN_HEIGHT: u16 = 15;
|
|
||||||
// Check if the available area is too small for our popup.
|
|
||||||
if area.width < MIN_WIDTH || area.height < MIN_HEIGHT {
|
|
||||||
// Fallback rendering: a simple abbreviated message that fits the available area.
|
|
||||||
let fallback_message = Paragraph::new(NO_GIT_ERROR)
|
|
||||||
.wrap(Wrap { trim: true })
|
|
||||||
.alignment(Alignment::Center);
|
|
||||||
fallback_message.render(area, buf);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine the popup (modal) size – aim for 60 % width, 30 % height
|
|
||||||
// but keep a sensible minimum so the content is always readable.
|
|
||||||
let popup_width = std::cmp::max(MIN_WIDTH, (area.width as f32 * 0.6) as u16);
|
|
||||||
let popup_height = std::cmp::max(MIN_HEIGHT, (area.height as f32 * 0.3) as u16);
|
|
||||||
|
|
||||||
// Center the popup in the available area.
|
|
||||||
let popup_x = area.x + (area.width.saturating_sub(popup_width)) / 2;
|
|
||||||
let popup_y = area.y + (area.height.saturating_sub(popup_height)) / 2;
|
|
||||||
let popup_area = Rect::new(popup_x, popup_y, popup_width, popup_height);
|
|
||||||
|
|
||||||
// The modal block that contains everything.
|
|
||||||
let popup_block = Block::default()
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.border_type(BorderType::Plain)
|
|
||||||
.title(Span::styled(
|
|
||||||
"Warning: Not a Git repository", // bold warning title
|
|
||||||
Style::default().add_modifier(Modifier::BOLD).fg(Color::Red),
|
|
||||||
));
|
|
||||||
|
|
||||||
// Obtain the inner area before rendering (render consumes the block).
|
|
||||||
let inner = popup_block.inner(popup_area);
|
|
||||||
popup_block.render(popup_area, buf);
|
|
||||||
|
|
||||||
// Split the inner area vertically into two boxes: one for the warning
|
|
||||||
// explanation, one for the user action instructions.
|
|
||||||
let chunks = Layout::default()
|
|
||||||
.direction(Direction::Vertical)
|
|
||||||
.constraints([Constraint::Min(3), Constraint::Length(3)])
|
|
||||||
.split(inner);
|
|
||||||
|
|
||||||
// ----- First box: detailed warning text --------------------------------
|
|
||||||
let text_block = Block::default().borders(Borders::ALL);
|
|
||||||
let text_inner = text_block.inner(chunks[0]);
|
|
||||||
text_block.render(chunks[0], buf);
|
|
||||||
|
|
||||||
let warning_paragraph = Paragraph::new(NO_GIT_ERROR)
|
|
||||||
.wrap(Wrap { trim: true })
|
|
||||||
.alignment(Alignment::Left);
|
|
||||||
warning_paragraph.render(text_inner, buf);
|
|
||||||
|
|
||||||
// ----- Second box: "proceed? y/n" instructions --------------------------
|
|
||||||
let action_block = Block::default().borders(Borders::ALL);
|
|
||||||
let action_inner = action_block.inner(chunks[1]);
|
|
||||||
action_block.render(chunks[1], buf);
|
|
||||||
|
|
||||||
let action_text = Paragraph::new("press 'y' to continue, 'n' to quit")
|
|
||||||
.alignment(Alignment::Center)
|
|
||||||
.style(Style::default().add_modifier(Modifier::BOLD));
|
|
||||||
action_text.render(action_inner, buf);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,6 @@ use codex_core::config::Config;
|
|||||||
use codex_core::config::ConfigOverrides;
|
use codex_core::config::ConfigOverrides;
|
||||||
use codex_core::config_types::SandboxMode;
|
use codex_core::config_types::SandboxMode;
|
||||||
use codex_core::protocol::AskForApproval;
|
use codex_core::protocol::AskForApproval;
|
||||||
use codex_core::util::is_inside_git_repo;
|
|
||||||
use codex_login::load_auth;
|
use codex_login::load_auth;
|
||||||
use codex_ollama::DEFAULT_OSS_MODEL;
|
use codex_ollama::DEFAULT_OSS_MODEL;
|
||||||
use log_layer::TuiLogLayer;
|
use log_layer::TuiLogLayer;
|
||||||
@@ -31,7 +30,6 @@ pub mod custom_terminal;
|
|||||||
mod exec_command;
|
mod exec_command;
|
||||||
mod file_search;
|
mod file_search;
|
||||||
mod get_git_diff;
|
mod get_git_diff;
|
||||||
mod git_warning_screen;
|
|
||||||
mod history_cell;
|
mod history_cell;
|
||||||
pub mod insert_history;
|
pub mod insert_history;
|
||||||
pub mod live_wrap;
|
pub mod live_wrap;
|
||||||
@@ -206,20 +204,12 @@ pub async fn run_main(
|
|||||||
eprintln!("");
|
eprintln!("");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine whether we need to display the "not a git repo" warning
|
run_ratatui_app(cli, config, log_rx).map_err(|err| std::io::Error::other(err.to_string()))
|
||||||
// modal. The flag is shown when the current working directory is *not*
|
|
||||||
// inside a Git repository **and** the user did *not* pass the
|
|
||||||
// `--allow-no-git-exec` flag.
|
|
||||||
let show_git_warning = !cli.skip_git_repo_check && !is_inside_git_repo(&config);
|
|
||||||
|
|
||||||
run_ratatui_app(cli, config, show_git_warning, log_rx)
|
|
||||||
.map_err(|err| std::io::Error::other(err.to_string()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_ratatui_app(
|
fn run_ratatui_app(
|
||||||
cli: Cli,
|
cli: Cli,
|
||||||
config: Config,
|
config: Config,
|
||||||
show_git_warning: bool,
|
|
||||||
mut log_rx: tokio::sync::mpsc::UnboundedReceiver<String>,
|
mut log_rx: tokio::sync::mpsc::UnboundedReceiver<String>,
|
||||||
) -> color_eyre::Result<codex_core::protocol::TokenUsage> {
|
) -> color_eyre::Result<codex_core::protocol::TokenUsage> {
|
||||||
color_eyre::install()?;
|
color_eyre::install()?;
|
||||||
@@ -237,7 +227,7 @@ fn run_ratatui_app(
|
|||||||
terminal.clear()?;
|
terminal.clear()?;
|
||||||
|
|
||||||
let Cli { prompt, images, .. } = cli;
|
let Cli { prompt, images, .. } = cli;
|
||||||
let mut app = App::new(config.clone(), prompt, show_git_warning, images);
|
let mut app = App::new(config.clone(), prompt, cli.skip_git_repo_check, images);
|
||||||
|
|
||||||
// Bridge log receiver into the AppEvent channel so latest log lines update the UI.
|
// Bridge log receiver into the AppEvent channel so latest log lines update the UI.
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -18,18 +18,23 @@ use crate::app_event::AppEvent;
|
|||||||
use crate::app_event_sender::AppEventSender;
|
use crate::app_event_sender::AppEventSender;
|
||||||
use crate::colors::LIGHT_BLUE;
|
use crate::colors::LIGHT_BLUE;
|
||||||
use crate::colors::SUCCESS_GREEN;
|
use crate::colors::SUCCESS_GREEN;
|
||||||
use crate::onboarding::onboarding_screen::KeyEventResult;
|
|
||||||
use crate::onboarding::onboarding_screen::KeyboardHandler;
|
use crate::onboarding::onboarding_screen::KeyboardHandler;
|
||||||
|
use crate::onboarding::onboarding_screen::StepStateProvider;
|
||||||
use crate::shimmer::FrameTicker;
|
use crate::shimmer::FrameTicker;
|
||||||
use crate::shimmer::shimmer_spans;
|
use crate::shimmer::shimmer_spans;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use super::onboarding_screen::StepState;
|
||||||
// no additional imports
|
// no additional imports
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(crate) enum SignInState {
|
pub(crate) enum SignInState {
|
||||||
PickMode,
|
PickMode,
|
||||||
ChatGptContinueInBrowser(#[allow(dead_code)] ContinueInBrowserState),
|
ChatGptContinueInBrowser(#[allow(dead_code)] ContinueInBrowserState),
|
||||||
|
ChatGptSuccessMessage,
|
||||||
ChatGptSuccess,
|
ChatGptSuccess,
|
||||||
|
EnvVarMissing,
|
||||||
|
EnvVarFound,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -38,7 +43,6 @@ pub(crate) struct ContinueInBrowserState {
|
|||||||
_login_child: Option<codex_login::SpawnedLogin>,
|
_login_child: Option<codex_login::SpawnedLogin>,
|
||||||
_frame_ticker: Option<FrameTicker>,
|
_frame_ticker: Option<FrameTicker>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for ContinueInBrowserState {
|
impl Drop for ContinueInBrowserState {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
if let Some(child) = &self._login_child {
|
if let Some(child) = &self._login_child {
|
||||||
@@ -52,54 +56,45 @@ impl Drop for ContinueInBrowserState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl KeyboardHandler for AuthModeWidget {
|
impl KeyboardHandler for AuthModeWidget {
|
||||||
fn handle_key_event(&mut self, key_event: KeyEvent) -> KeyEventResult {
|
fn handle_key_event(&mut self, key_event: KeyEvent) {
|
||||||
match key_event.code {
|
match key_event.code {
|
||||||
KeyCode::Up | KeyCode::Char('k') => {
|
KeyCode::Up | KeyCode::Char('k') => {
|
||||||
self.mode = AuthMode::ChatGPT;
|
self.highlighted_mode = AuthMode::ChatGPT;
|
||||||
KeyEventResult::None
|
|
||||||
}
|
}
|
||||||
KeyCode::Down | KeyCode::Char('j') => {
|
KeyCode::Down | KeyCode::Char('j') => {
|
||||||
self.mode = AuthMode::ApiKey;
|
self.highlighted_mode = AuthMode::ApiKey;
|
||||||
KeyEventResult::None
|
|
||||||
}
|
}
|
||||||
KeyCode::Char('1') => {
|
KeyCode::Char('1') => {
|
||||||
self.mode = AuthMode::ChatGPT;
|
|
||||||
self.start_chatgpt_login();
|
self.start_chatgpt_login();
|
||||||
KeyEventResult::None
|
|
||||||
}
|
}
|
||||||
KeyCode::Char('2') => {
|
KeyCode::Char('2') => self.verify_api_key(),
|
||||||
self.mode = AuthMode::ApiKey;
|
KeyCode::Enter => match self.sign_in_state {
|
||||||
self.verify_api_key()
|
SignInState::PickMode => match self.highlighted_mode {
|
||||||
}
|
AuthMode::ChatGPT => self.start_chatgpt_login(),
|
||||||
KeyCode::Enter => match self.mode {
|
AuthMode::ApiKey => self.verify_api_key(),
|
||||||
AuthMode::ChatGPT => match &self.sign_in_state {
|
|
||||||
SignInState::PickMode => self.start_chatgpt_login(),
|
|
||||||
SignInState::ChatGptContinueInBrowser(_) => KeyEventResult::None,
|
|
||||||
SignInState::ChatGptSuccess => KeyEventResult::Continue,
|
|
||||||
},
|
},
|
||||||
AuthMode::ApiKey => self.verify_api_key(),
|
SignInState::EnvVarMissing => self.sign_in_state = SignInState::PickMode,
|
||||||
|
SignInState::ChatGptSuccessMessage => {
|
||||||
|
self.sign_in_state = SignInState::ChatGptSuccess
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
},
|
},
|
||||||
KeyCode::Esc => {
|
KeyCode::Esc => {
|
||||||
if matches!(self.sign_in_state, SignInState::ChatGptContinueInBrowser(_)) {
|
if matches!(self.sign_in_state, SignInState::ChatGptContinueInBrowser(_)) {
|
||||||
self.sign_in_state = SignInState::PickMode;
|
self.sign_in_state = SignInState::PickMode;
|
||||||
self.event_tx.send(AppEvent::RequestRedraw);
|
|
||||||
KeyEventResult::None
|
|
||||||
} else {
|
|
||||||
KeyEventResult::Quit
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
KeyCode::Char('q') => KeyEventResult::Quit,
|
_ => {}
|
||||||
_ => KeyEventResult::None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(crate) struct AuthModeWidget {
|
pub(crate) struct AuthModeWidget {
|
||||||
pub mode: AuthMode,
|
pub event_tx: AppEventSender,
|
||||||
|
pub highlighted_mode: AuthMode,
|
||||||
pub error: Option<String>,
|
pub error: Option<String>,
|
||||||
pub sign_in_state: SignInState,
|
pub sign_in_state: SignInState,
|
||||||
pub event_tx: AppEventSender,
|
|
||||||
pub codex_home: PathBuf,
|
pub codex_home: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,7 +116,7 @@ impl AuthModeWidget {
|
|||||||
text: &str,
|
text: &str,
|
||||||
description: &str|
|
description: &str|
|
||||||
-> Vec<Line<'static>> {
|
-> Vec<Line<'static>> {
|
||||||
let is_selected = self.mode == selected_mode;
|
let is_selected = self.highlighted_mode == selected_mode;
|
||||||
let caret = if is_selected { ">" } else { " " };
|
let caret = if is_selected { ">" } else { " " };
|
||||||
|
|
||||||
let line1 = if is_selected {
|
let line1 = if is_selected {
|
||||||
@@ -192,7 +187,7 @@ impl AuthModeWidget {
|
|||||||
.render(area, buf);
|
.render(area, buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_chatgpt_success(&self, area: Rect, buf: &mut Buffer) {
|
fn render_chatgpt_success_message(&self, area: Rect, buf: &mut Buffer) {
|
||||||
let lines = vec![
|
let lines = vec![
|
||||||
Line::from("✓ Signed in with your ChatGPT account")
|
Line::from("✓ Signed in with your ChatGPT account")
|
||||||
.style(Style::default().fg(SUCCESS_GREEN)),
|
.style(Style::default().fg(SUCCESS_GREEN)),
|
||||||
@@ -219,7 +214,40 @@ impl AuthModeWidget {
|
|||||||
.render(area, buf);
|
.render(area, buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn start_chatgpt_login(&mut self) -> KeyEventResult {
|
fn render_chatgpt_success(&self, area: Rect, buf: &mut Buffer) {
|
||||||
|
let lines = vec![
|
||||||
|
Line::from("✓ Signed in with your ChatGPT account")
|
||||||
|
.style(Style::default().fg(SUCCESS_GREEN)),
|
||||||
|
];
|
||||||
|
|
||||||
|
Paragraph::new(lines)
|
||||||
|
.wrap(Wrap { trim: false })
|
||||||
|
.render(area, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_env_var_found(&self, area: Rect, buf: &mut Buffer) {
|
||||||
|
let lines =
|
||||||
|
vec![Line::from("✓ Using OPENAI_API_KEY").style(Style::default().fg(SUCCESS_GREEN))];
|
||||||
|
|
||||||
|
Paragraph::new(lines)
|
||||||
|
.wrap(Wrap { trim: false })
|
||||||
|
.render(area, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_env_var_missing(&self, area: Rect, buf: &mut Buffer) {
|
||||||
|
let lines = vec![
|
||||||
|
Line::from("✘ OPENAI_API_KEY not found").style(Style::default().fg(Color::Red)),
|
||||||
|
Line::from(""),
|
||||||
|
Line::from(" Press Enter to return")
|
||||||
|
.style(Style::default().add_modifier(Modifier::DIM)),
|
||||||
|
];
|
||||||
|
|
||||||
|
Paragraph::new(lines)
|
||||||
|
.wrap(Wrap { trim: false })
|
||||||
|
.render(area, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_chatgpt_login(&mut self) {
|
||||||
self.error = None;
|
self.error = None;
|
||||||
match codex_login::spawn_login_with_chatgpt(&self.codex_home) {
|
match codex_login::spawn_login_with_chatgpt(&self.codex_home) {
|
||||||
Ok(child) => {
|
Ok(child) => {
|
||||||
@@ -230,27 +258,23 @@ impl AuthModeWidget {
|
|||||||
_frame_ticker: Some(FrameTicker::new(self.event_tx.clone())),
|
_frame_ticker: Some(FrameTicker::new(self.event_tx.clone())),
|
||||||
});
|
});
|
||||||
self.event_tx.send(AppEvent::RequestRedraw);
|
self.event_tx.send(AppEvent::RequestRedraw);
|
||||||
KeyEventResult::None
|
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
self.sign_in_state = SignInState::PickMode;
|
self.sign_in_state = SignInState::PickMode;
|
||||||
self.error = Some(e.to_string());
|
self.error = Some(e.to_string());
|
||||||
self.event_tx.send(AppEvent::RequestRedraw);
|
self.event_tx.send(AppEvent::RequestRedraw);
|
||||||
KeyEventResult::None
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// TODO: Read/write from the correct hierarchy config overrides + auth json + OPENAI_API_KEY.
|
/// TODO: Read/write from the correct hierarchy config overrides + auth json + OPENAI_API_KEY.
|
||||||
fn verify_api_key(&mut self) -> KeyEventResult {
|
fn verify_api_key(&mut self) {
|
||||||
if std::env::var("OPENAI_API_KEY").is_err() {
|
if std::env::var("OPENAI_API_KEY").is_err() {
|
||||||
self.error =
|
self.sign_in_state = SignInState::EnvVarMissing;
|
||||||
Some("Set OPENAI_API_KEY in your environment. Learn more: https://platform.openai.com/docs/libraries".to_string());
|
|
||||||
self.event_tx.send(AppEvent::RequestRedraw);
|
|
||||||
KeyEventResult::None
|
|
||||||
} else {
|
} else {
|
||||||
KeyEventResult::Continue
|
self.sign_in_state = SignInState::EnvVarFound;
|
||||||
}
|
}
|
||||||
|
self.event_tx.send(AppEvent::RequestRedraw);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn spawn_completion_poller(&self, child: codex_login::SpawnedLogin) {
|
fn spawn_completion_poller(&self, child: codex_login::SpawnedLogin) {
|
||||||
@@ -299,6 +323,18 @@ impl AuthModeWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl StepStateProvider for AuthModeWidget {
|
||||||
|
fn get_step_state(&self) -> StepState {
|
||||||
|
match &self.sign_in_state {
|
||||||
|
SignInState::PickMode
|
||||||
|
| SignInState::EnvVarMissing
|
||||||
|
| SignInState::ChatGptContinueInBrowser(_)
|
||||||
|
| SignInState::ChatGptSuccessMessage => StepState::InProgress,
|
||||||
|
SignInState::ChatGptSuccess | SignInState::EnvVarFound => StepState::Complete,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl WidgetRef for AuthModeWidget {
|
impl WidgetRef for AuthModeWidget {
|
||||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||||
match self.sign_in_state {
|
match self.sign_in_state {
|
||||||
@@ -308,9 +344,18 @@ impl WidgetRef for AuthModeWidget {
|
|||||||
SignInState::ChatGptContinueInBrowser(_) => {
|
SignInState::ChatGptContinueInBrowser(_) => {
|
||||||
self.render_continue_in_browser(area, buf);
|
self.render_continue_in_browser(area, buf);
|
||||||
}
|
}
|
||||||
|
SignInState::ChatGptSuccessMessage => {
|
||||||
|
self.render_chatgpt_success_message(area, buf);
|
||||||
|
}
|
||||||
SignInState::ChatGptSuccess => {
|
SignInState::ChatGptSuccess => {
|
||||||
self.render_chatgpt_success(area, buf);
|
self.render_chatgpt_success(area, buf);
|
||||||
}
|
}
|
||||||
|
SignInState::EnvVarMissing => {
|
||||||
|
self.render_env_var_missing(area, buf);
|
||||||
|
}
|
||||||
|
SignInState::EnvVarFound => {
|
||||||
|
self.render_env_var_found(area, buf);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
30
codex-rs/tui/src/onboarding/continue_to_chat.rs
Normal file
30
codex-rs/tui/src/onboarding/continue_to_chat.rs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
use ratatui::buffer::Buffer;
|
||||||
|
use ratatui::layout::Rect;
|
||||||
|
use ratatui::widgets::WidgetRef;
|
||||||
|
|
||||||
|
use crate::app::ChatWidgetArgs;
|
||||||
|
use crate::app_event::AppEvent;
|
||||||
|
use crate::app_event_sender::AppEventSender;
|
||||||
|
use crate::onboarding::onboarding_screen::StepStateProvider;
|
||||||
|
|
||||||
|
use super::onboarding_screen::StepState;
|
||||||
|
|
||||||
|
/// This doesn't render anything explicitly but serves as a signal that we made it to the end and
|
||||||
|
/// we should continue to the chat.
|
||||||
|
pub(crate) struct ContinueToChatWidget {
|
||||||
|
pub event_tx: AppEventSender,
|
||||||
|
pub chat_widget_args: ChatWidgetArgs,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StepStateProvider for ContinueToChatWidget {
|
||||||
|
fn get_step_state(&self) -> StepState {
|
||||||
|
StepState::Complete
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WidgetRef for &ContinueToChatWidget {
|
||||||
|
fn render_ref(&self, _area: Rect, _buf: &mut Buffer) {
|
||||||
|
self.event_tx
|
||||||
|
.send(AppEvent::OnboardingComplete(self.chat_widget_args.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
126
codex-rs/tui/src/onboarding/git_warning.rs
Normal file
126
codex-rs/tui/src/onboarding/git_warning.rs
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use codex_core::util::is_inside_git_repo;
|
||||||
|
use crossterm::event::KeyCode;
|
||||||
|
use crossterm::event::KeyEvent;
|
||||||
|
use ratatui::buffer::Buffer;
|
||||||
|
use ratatui::layout::Rect;
|
||||||
|
use ratatui::prelude::Widget;
|
||||||
|
use ratatui::style::Modifier;
|
||||||
|
use ratatui::style::Style;
|
||||||
|
use ratatui::style::Stylize;
|
||||||
|
use ratatui::text::Line;
|
||||||
|
use ratatui::text::Span;
|
||||||
|
use ratatui::widgets::Paragraph;
|
||||||
|
use ratatui::widgets::WidgetRef;
|
||||||
|
use ratatui::widgets::Wrap;
|
||||||
|
|
||||||
|
use crate::app_event::AppEvent;
|
||||||
|
use crate::app_event_sender::AppEventSender;
|
||||||
|
use crate::colors::LIGHT_BLUE;
|
||||||
|
|
||||||
|
use crate::onboarding::onboarding_screen::KeyboardHandler;
|
||||||
|
use crate::onboarding::onboarding_screen::StepStateProvider;
|
||||||
|
|
||||||
|
use super::onboarding_screen::StepState;
|
||||||
|
|
||||||
|
pub(crate) struct GitWarningWidget {
|
||||||
|
pub event_tx: AppEventSender,
|
||||||
|
pub cwd: PathBuf,
|
||||||
|
pub selection: Option<GitWarningSelection>,
|
||||||
|
pub highlighted: GitWarningSelection,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub(crate) enum GitWarningSelection {
|
||||||
|
Continue,
|
||||||
|
Exit,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WidgetRef for &GitWarningWidget {
|
||||||
|
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||||
|
let mut lines: Vec<Line> = vec![
|
||||||
|
Line::from(vec![
|
||||||
|
Span::raw("> "),
|
||||||
|
Span::raw("You are running Codex in "),
|
||||||
|
Span::styled(
|
||||||
|
self.cwd.to_string_lossy().to_string(),
|
||||||
|
Style::default().add_modifier(Modifier::BOLD),
|
||||||
|
),
|
||||||
|
Span::raw(". This folder is not version controlled."),
|
||||||
|
]),
|
||||||
|
Line::from(""),
|
||||||
|
Line::from(" Do you want to continue?"),
|
||||||
|
Line::from(""),
|
||||||
|
];
|
||||||
|
|
||||||
|
let create_option =
|
||||||
|
|idx: usize, option: GitWarningSelection, text: &str| -> Line<'static> {
|
||||||
|
let is_selected = self.highlighted == option;
|
||||||
|
if is_selected {
|
||||||
|
Line::from(vec![
|
||||||
|
Span::styled(
|
||||||
|
format!("> {}. ", idx + 1),
|
||||||
|
Style::default().fg(LIGHT_BLUE).add_modifier(Modifier::DIM),
|
||||||
|
),
|
||||||
|
Span::styled(text.to_owned(), Style::default().fg(LIGHT_BLUE)),
|
||||||
|
])
|
||||||
|
} else {
|
||||||
|
Line::from(format!(" {}. {}", idx + 1, text))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
lines.push(create_option(0, GitWarningSelection::Continue, "Yes"));
|
||||||
|
lines.push(create_option(1, GitWarningSelection::Exit, "No"));
|
||||||
|
lines.push(Line::from(""));
|
||||||
|
lines.push(Line::from(" Press Enter to continue").add_modifier(Modifier::DIM));
|
||||||
|
|
||||||
|
Paragraph::new(lines)
|
||||||
|
.wrap(Wrap { trim: false })
|
||||||
|
.render(area, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KeyboardHandler for GitWarningWidget {
|
||||||
|
fn handle_key_event(&mut self, key_event: KeyEvent) {
|
||||||
|
match key_event.code {
|
||||||
|
KeyCode::Up | KeyCode::Char('k') => {
|
||||||
|
self.highlighted = GitWarningSelection::Continue;
|
||||||
|
}
|
||||||
|
KeyCode::Down | KeyCode::Char('j') => {
|
||||||
|
self.highlighted = GitWarningSelection::Exit;
|
||||||
|
}
|
||||||
|
KeyCode::Char('1') => self.handle_continue(),
|
||||||
|
KeyCode::Char('2') => self.handle_quit(),
|
||||||
|
KeyCode::Enter => match self.highlighted {
|
||||||
|
GitWarningSelection::Continue => self.handle_continue(),
|
||||||
|
GitWarningSelection::Exit => self.handle_quit(),
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StepStateProvider for GitWarningWidget {
|
||||||
|
fn get_step_state(&self) -> StepState {
|
||||||
|
let is_git_repo = is_inside_git_repo(&self.cwd);
|
||||||
|
match is_git_repo {
|
||||||
|
true => StepState::Hidden,
|
||||||
|
false => match self.selection {
|
||||||
|
Some(_) => StepState::Complete,
|
||||||
|
None => StepState::InProgress,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GitWarningWidget {
|
||||||
|
fn handle_continue(&mut self) {
|
||||||
|
self.selection = Some(GitWarningSelection::Continue);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_quit(&mut self) {
|
||||||
|
self.highlighted = GitWarningSelection::Exit;
|
||||||
|
self.event_tx.send(AppEvent::ExitRequest);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
mod auth;
|
mod auth;
|
||||||
|
mod continue_to_chat;
|
||||||
|
mod git_warning;
|
||||||
pub mod onboarding_screen;
|
pub mod onboarding_screen;
|
||||||
mod welcome;
|
mod welcome;
|
||||||
|
|||||||
@@ -5,26 +5,37 @@ use ratatui::widgets::WidgetRef;
|
|||||||
|
|
||||||
use codex_login::AuthMode;
|
use codex_login::AuthMode;
|
||||||
|
|
||||||
|
use crate::app::ChatWidgetArgs;
|
||||||
use crate::app_event::AppEvent;
|
use crate::app_event::AppEvent;
|
||||||
use crate::app_event_sender::AppEventSender;
|
use crate::app_event_sender::AppEventSender;
|
||||||
use crate::onboarding::auth::AuthModeWidget;
|
use crate::onboarding::auth::AuthModeWidget;
|
||||||
use crate::onboarding::auth::SignInState;
|
use crate::onboarding::auth::SignInState;
|
||||||
|
use crate::onboarding::continue_to_chat::ContinueToChatWidget;
|
||||||
|
use crate::onboarding::git_warning::GitWarningSelection;
|
||||||
|
use crate::onboarding::git_warning::GitWarningWidget;
|
||||||
use crate::onboarding::welcome::WelcomeWidget;
|
use crate::onboarding::welcome::WelcomeWidget;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[allow(clippy::large_enum_variant)]
|
||||||
enum Step {
|
enum Step {
|
||||||
Welcome(WelcomeWidget),
|
Welcome(WelcomeWidget),
|
||||||
Auth(AuthModeWidget),
|
Auth(AuthModeWidget),
|
||||||
|
GitWarning(GitWarningWidget),
|
||||||
|
ContinueToChat(ContinueToChatWidget),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) trait KeyboardHandler {
|
pub(crate) trait KeyboardHandler {
|
||||||
fn handle_key_event(&mut self, key_event: KeyEvent) -> KeyEventResult;
|
fn handle_key_event(&mut self, key_event: KeyEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) enum KeyEventResult {
|
pub(crate) enum StepState {
|
||||||
Continue,
|
Hidden,
|
||||||
Quit,
|
InProgress,
|
||||||
None,
|
Complete,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) trait StepStateProvider {
|
||||||
|
fn get_step_state(&self) -> StepState;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct OnboardingScreen {
|
pub(crate) struct OnboardingScreen {
|
||||||
@@ -32,50 +43,113 @@ pub(crate) struct OnboardingScreen {
|
|||||||
steps: Vec<Step>,
|
steps: Vec<Step>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) struct OnboardingScreenArgs {
|
||||||
|
pub event_tx: AppEventSender,
|
||||||
|
pub chat_widget_args: ChatWidgetArgs,
|
||||||
|
pub codex_home: PathBuf,
|
||||||
|
pub cwd: PathBuf,
|
||||||
|
pub show_login_screen: bool,
|
||||||
|
pub show_git_warning: bool,
|
||||||
|
}
|
||||||
|
|
||||||
impl OnboardingScreen {
|
impl OnboardingScreen {
|
||||||
pub(crate) fn new(event_tx: AppEventSender, codex_home: PathBuf) -> Self {
|
pub(crate) fn new(args: OnboardingScreenArgs) -> Self {
|
||||||
let steps: Vec<Step> = vec![
|
let OnboardingScreenArgs {
|
||||||
Step::Welcome(WelcomeWidget {}),
|
event_tx,
|
||||||
Step::Auth(AuthModeWidget {
|
chat_widget_args,
|
||||||
|
codex_home,
|
||||||
|
cwd,
|
||||||
|
show_login_screen,
|
||||||
|
show_git_warning,
|
||||||
|
} = args;
|
||||||
|
let mut steps: Vec<Step> = vec![Step::Welcome(WelcomeWidget {
|
||||||
|
is_logged_in: !show_login_screen,
|
||||||
|
})];
|
||||||
|
if show_login_screen {
|
||||||
|
steps.push(Step::Auth(AuthModeWidget {
|
||||||
event_tx: event_tx.clone(),
|
event_tx: event_tx.clone(),
|
||||||
mode: AuthMode::ChatGPT,
|
highlighted_mode: AuthMode::ChatGPT,
|
||||||
error: None,
|
error: None,
|
||||||
sign_in_state: SignInState::PickMode,
|
sign_in_state: SignInState::PickMode,
|
||||||
codex_home,
|
codex_home,
|
||||||
}),
|
}))
|
||||||
];
|
}
|
||||||
|
if show_git_warning {
|
||||||
|
steps.push(Step::GitWarning(GitWarningWidget {
|
||||||
|
event_tx: event_tx.clone(),
|
||||||
|
cwd,
|
||||||
|
selection: None,
|
||||||
|
highlighted: GitWarningSelection::Continue,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
steps.push(Step::ContinueToChat(ContinueToChatWidget {
|
||||||
|
event_tx: event_tx.clone(),
|
||||||
|
chat_widget_args,
|
||||||
|
}));
|
||||||
|
// TODO: add git warning.
|
||||||
Self { event_tx, steps }
|
Self { event_tx, steps }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn on_auth_complete(&mut self, result: Result<(), String>) -> KeyEventResult {
|
pub(crate) fn on_auth_complete(&mut self, result: Result<(), String>) {
|
||||||
if let Some(Step::Auth(state)) = self.steps.last_mut() {
|
let current_step = self.current_step_mut();
|
||||||
|
if let Some(Step::Auth(state)) = current_step {
|
||||||
match result {
|
match result {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
state.sign_in_state = SignInState::ChatGptSuccess;
|
state.sign_in_state = SignInState::ChatGptSuccessMessage;
|
||||||
self.event_tx.send(AppEvent::RequestRedraw);
|
self.event_tx.send(AppEvent::RequestRedraw);
|
||||||
KeyEventResult::None
|
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
state.sign_in_state = SignInState::PickMode;
|
state.sign_in_state = SignInState::PickMode;
|
||||||
state.error = Some(e);
|
state.error = Some(e);
|
||||||
self.event_tx.send(AppEvent::RequestRedraw);
|
self.event_tx.send(AppEvent::RequestRedraw);
|
||||||
KeyEventResult::None
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
KeyEventResult::None
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn current_steps_mut(&mut self) -> Vec<&mut Step> {
|
||||||
|
let mut out: Vec<&mut Step> = Vec::new();
|
||||||
|
for step in self.steps.iter_mut() {
|
||||||
|
match step.get_step_state() {
|
||||||
|
StepState::Hidden => continue,
|
||||||
|
StepState::Complete => out.push(step),
|
||||||
|
StepState::InProgress => {
|
||||||
|
out.push(step);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_steps(&self) -> Vec<&Step> {
|
||||||
|
let mut out: Vec<&Step> = Vec::new();
|
||||||
|
for step in self.steps.iter() {
|
||||||
|
match step.get_step_state() {
|
||||||
|
StepState::Hidden => continue,
|
||||||
|
StepState::Complete => out.push(step),
|
||||||
|
StepState::InProgress => {
|
||||||
|
out.push(step);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_step_mut(&mut self) -> Option<&mut Step> {
|
||||||
|
self.steps
|
||||||
|
.iter_mut()
|
||||||
|
.find(|step| matches!(step.get_step_state(), StepState::InProgress))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl KeyboardHandler for OnboardingScreen {
|
impl KeyboardHandler for OnboardingScreen {
|
||||||
fn handle_key_event(&mut self, key_event: KeyEvent) -> KeyEventResult {
|
fn handle_key_event(&mut self, key_event: KeyEvent) {
|
||||||
if let Some(last_step) = self.steps.last_mut() {
|
if let Some(active_step) = self.current_steps_mut().into_iter().last() {
|
||||||
self.event_tx.send(AppEvent::RequestRedraw);
|
active_step.handle_key_event(key_event);
|
||||||
last_step.handle_key_event(key_event)
|
|
||||||
} else {
|
|
||||||
KeyEventResult::None
|
|
||||||
}
|
}
|
||||||
|
self.event_tx.send(AppEvent::RequestRedraw);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,8 +183,10 @@ impl WidgetRef for &OnboardingScreen {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut i = 0usize;
|
let mut i = 0usize;
|
||||||
while i < self.steps.len() && y < bottom {
|
let current_steps = self.current_steps();
|
||||||
let step = &self.steps[i];
|
|
||||||
|
while i < current_steps.len() && y < bottom {
|
||||||
|
let step = ¤t_steps[i];
|
||||||
let max_h = bottom.saturating_sub(y);
|
let max_h = bottom.saturating_sub(y);
|
||||||
if max_h == 0 || width == 0 {
|
if max_h == 0 || width == 0 {
|
||||||
break;
|
break;
|
||||||
@@ -135,10 +211,22 @@ impl WidgetRef for &OnboardingScreen {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl KeyboardHandler for Step {
|
impl KeyboardHandler for Step {
|
||||||
fn handle_key_event(&mut self, key_event: KeyEvent) -> KeyEventResult {
|
fn handle_key_event(&mut self, key_event: KeyEvent) {
|
||||||
match self {
|
match self {
|
||||||
Step::Welcome(_) => KeyEventResult::None,
|
Step::Welcome(_) | Step::ContinueToChat(_) => (),
|
||||||
Step::Auth(widget) => widget.handle_key_event(key_event),
|
Step::Auth(widget) => widget.handle_key_event(key_event),
|
||||||
|
Step::GitWarning(widget) => widget.handle_key_event(key_event),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StepStateProvider for Step {
|
||||||
|
fn get_step_state(&self) -> StepState {
|
||||||
|
match self {
|
||||||
|
Step::Welcome(w) => w.get_step_state(),
|
||||||
|
Step::Auth(w) => w.get_step_state(),
|
||||||
|
Step::GitWarning(w) => w.get_step_state(),
|
||||||
|
Step::ContinueToChat(w) => w.get_step_state(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -152,6 +240,12 @@ impl WidgetRef for Step {
|
|||||||
Step::Auth(widget) => {
|
Step::Auth(widget) => {
|
||||||
widget.render_ref(area, buf);
|
widget.render_ref(area, buf);
|
||||||
}
|
}
|
||||||
|
Step::GitWarning(widget) => {
|
||||||
|
widget.render_ref(area, buf);
|
||||||
|
}
|
||||||
|
Step::ContinueToChat(widget) => {
|
||||||
|
widget.render_ref(area, buf);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,13 @@ use ratatui::text::Line;
|
|||||||
use ratatui::text::Span;
|
use ratatui::text::Span;
|
||||||
use ratatui::widgets::WidgetRef;
|
use ratatui::widgets::WidgetRef;
|
||||||
|
|
||||||
pub(crate) struct WelcomeWidget {}
|
use crate::onboarding::onboarding_screen::StepStateProvider;
|
||||||
|
|
||||||
|
use super::onboarding_screen::StepState;
|
||||||
|
|
||||||
|
pub(crate) struct WelcomeWidget {
|
||||||
|
pub is_logged_in: bool,
|
||||||
|
}
|
||||||
|
|
||||||
impl WidgetRef for &WelcomeWidget {
|
impl WidgetRef for &WelcomeWidget {
|
||||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||||
@@ -21,3 +27,12 @@ impl WidgetRef for &WelcomeWidget {
|
|||||||
line.render(area, buf);
|
line.render(area, buf);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl StepStateProvider for WelcomeWidget {
|
||||||
|
fn get_step_state(&self) -> StepState {
|
||||||
|
match self.is_logged_in {
|
||||||
|
true => StepState::Hidden,
|
||||||
|
false => StepState::Complete,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user