[config] Onboarding flow with persistence (#1929)
## Summary In collaboration with @gpeal: upgrade the onboarding flow, and persist user settings. --------- Co-authored-by: Gabriel Peal <gabriel@openai.com>
This commit is contained in:
@@ -8,12 +8,14 @@ use crate::app_event_sender::AppEventSender;
|
||||
use crate::onboarding::onboarding_screen::StepStateProvider;
|
||||
|
||||
use super::onboarding_screen::StepState;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
|
||||
/// 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,
|
||||
pub chat_widget_args: Arc<Mutex<ChatWidgetArgs>>,
|
||||
}
|
||||
|
||||
impl StepStateProvider for ContinueToChatWidget {
|
||||
@@ -24,7 +26,9 @@ impl StepStateProvider for ContinueToChatWidget {
|
||||
|
||||
impl WidgetRef for &ContinueToChatWidget {
|
||||
fn render_ref(&self, _area: Rect, _buf: &mut Buffer) {
|
||||
self.event_tx
|
||||
.send(AppEvent::OnboardingComplete(self.chat_widget_args.clone()));
|
||||
if let Ok(args) = self.chat_widget_args.lock() {
|
||||
self.event_tx
|
||||
.send(AppEvent::OnboardingComplete(args.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
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,5 +1,5 @@
|
||||
mod auth;
|
||||
mod continue_to_chat;
|
||||
mod git_warning;
|
||||
pub mod onboarding_screen;
|
||||
mod trust_directory;
|
||||
mod welcome;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use codex_core::util::is_inside_git_repo;
|
||||
use crossterm::event::KeyEvent;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
@@ -11,16 +12,18 @@ use crate::app_event_sender::AppEventSender;
|
||||
use crate::onboarding::auth::AuthModeWidget;
|
||||
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::trust_directory::TrustDirectorySelection;
|
||||
use crate::onboarding::trust_directory::TrustDirectoryWidget;
|
||||
use crate::onboarding::welcome::WelcomeWidget;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
enum Step {
|
||||
Welcome(WelcomeWidget),
|
||||
Auth(AuthModeWidget),
|
||||
GitWarning(GitWarningWidget),
|
||||
TrustDirectory(TrustDirectoryWidget),
|
||||
ContinueToChat(ContinueToChatWidget),
|
||||
}
|
||||
|
||||
@@ -49,7 +52,7 @@ pub(crate) struct OnboardingScreenArgs {
|
||||
pub codex_home: PathBuf,
|
||||
pub cwd: PathBuf,
|
||||
pub show_login_screen: bool,
|
||||
pub show_git_warning: bool,
|
||||
pub show_trust_screen: bool,
|
||||
}
|
||||
|
||||
impl OnboardingScreen {
|
||||
@@ -60,7 +63,7 @@ impl OnboardingScreen {
|
||||
codex_home,
|
||||
cwd,
|
||||
show_login_screen,
|
||||
show_git_warning,
|
||||
show_trust_screen,
|
||||
} = args;
|
||||
let mut steps: Vec<Step> = vec![Step::Welcome(WelcomeWidget {
|
||||
is_logged_in: !show_login_screen,
|
||||
@@ -71,20 +74,33 @@ impl OnboardingScreen {
|
||||
highlighted_mode: AuthMode::ChatGPT,
|
||||
error: None,
|
||||
sign_in_state: SignInState::PickMode,
|
||||
codex_home,
|
||||
codex_home: codex_home.clone(),
|
||||
}))
|
||||
}
|
||||
if show_git_warning {
|
||||
steps.push(Step::GitWarning(GitWarningWidget {
|
||||
event_tx: event_tx.clone(),
|
||||
let is_git_repo = is_inside_git_repo(&cwd);
|
||||
let highlighted = if is_git_repo {
|
||||
TrustDirectorySelection::Trust
|
||||
} else {
|
||||
// Default to not trusting the directory if it's not a git repo.
|
||||
TrustDirectorySelection::DontTrust
|
||||
};
|
||||
// Share ChatWidgetArgs between steps so changes in the TrustDirectory step
|
||||
// are reflected when continuing to chat.
|
||||
let shared_chat_args = Arc::new(Mutex::new(chat_widget_args));
|
||||
if show_trust_screen {
|
||||
steps.push(Step::TrustDirectory(TrustDirectoryWidget {
|
||||
cwd,
|
||||
codex_home,
|
||||
is_git_repo,
|
||||
selection: None,
|
||||
highlighted: GitWarningSelection::Continue,
|
||||
highlighted,
|
||||
error: None,
|
||||
chat_widget_args: shared_chat_args.clone(),
|
||||
}))
|
||||
}
|
||||
steps.push(Step::ContinueToChat(ContinueToChatWidget {
|
||||
event_tx: event_tx.clone(),
|
||||
chat_widget_args,
|
||||
chat_widget_args: shared_chat_args,
|
||||
}));
|
||||
// TODO: add git warning.
|
||||
Self { event_tx, steps }
|
||||
@@ -215,7 +231,7 @@ impl KeyboardHandler for Step {
|
||||
match self {
|
||||
Step::Welcome(_) | Step::ContinueToChat(_) => (),
|
||||
Step::Auth(widget) => widget.handle_key_event(key_event),
|
||||
Step::GitWarning(widget) => widget.handle_key_event(key_event),
|
||||
Step::TrustDirectory(widget) => widget.handle_key_event(key_event),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -225,7 +241,7 @@ impl StepStateProvider for Step {
|
||||
match self {
|
||||
Step::Welcome(w) => w.get_step_state(),
|
||||
Step::Auth(w) => w.get_step_state(),
|
||||
Step::GitWarning(w) => w.get_step_state(),
|
||||
Step::TrustDirectory(w) => w.get_step_state(),
|
||||
Step::ContinueToChat(w) => w.get_step_state(),
|
||||
}
|
||||
}
|
||||
@@ -240,7 +256,7 @@ impl WidgetRef for Step {
|
||||
Step::Auth(widget) => {
|
||||
widget.render_ref(area, buf);
|
||||
}
|
||||
Step::GitWarning(widget) => {
|
||||
Step::TrustDirectory(widget) => {
|
||||
widget.render_ref(area, buf);
|
||||
}
|
||||
Step::ContinueToChat(widget) => {
|
||||
|
||||
179
codex-rs/tui/src/onboarding/trust_directory.rs
Normal file
179
codex-rs/tui/src/onboarding/trust_directory.rs
Normal file
@@ -0,0 +1,179 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use codex_core::config::set_project_trusted;
|
||||
use codex_core::protocol::AskForApproval;
|
||||
use codex_core::protocol::SandboxPolicy;
|
||||
use crossterm::event::KeyCode;
|
||||
use crossterm::event::KeyEvent;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::prelude::Widget;
|
||||
use ratatui::style::Color;
|
||||
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::colors::LIGHT_BLUE;
|
||||
|
||||
use crate::onboarding::onboarding_screen::KeyboardHandler;
|
||||
use crate::onboarding::onboarding_screen::StepStateProvider;
|
||||
|
||||
use super::onboarding_screen::StepState;
|
||||
use crate::app::ChatWidgetArgs;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
|
||||
pub(crate) struct TrustDirectoryWidget {
|
||||
pub codex_home: PathBuf,
|
||||
pub cwd: PathBuf,
|
||||
pub is_git_repo: bool,
|
||||
pub selection: Option<TrustDirectorySelection>,
|
||||
pub highlighted: TrustDirectorySelection,
|
||||
pub error: Option<String>,
|
||||
pub chat_widget_args: Arc<Mutex<ChatWidgetArgs>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum TrustDirectorySelection {
|
||||
Trust,
|
||||
DontTrust,
|
||||
}
|
||||
|
||||
impl WidgetRef for &TrustDirectoryWidget {
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
let mut lines: Vec<Line> = vec![
|
||||
Line::from(vec![
|
||||
Span::raw("> "),
|
||||
Span::styled(
|
||||
"You are running Codex in ",
|
||||
Style::default().add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(self.cwd.to_string_lossy().to_string()),
|
||||
]),
|
||||
Line::from(""),
|
||||
];
|
||||
|
||||
if self.is_git_repo {
|
||||
lines.push(Line::from(
|
||||
" Since this folder is version controlled, you may wish to allow Codex",
|
||||
));
|
||||
lines.push(Line::from(
|
||||
" to work in this folder without asking for approval.",
|
||||
));
|
||||
} else {
|
||||
lines.push(Line::from(
|
||||
" Since this folder is not version controlled, we recommend requiring",
|
||||
));
|
||||
lines.push(Line::from(" approval of all edits and commands."));
|
||||
}
|
||||
lines.push(Line::from(""));
|
||||
|
||||
let create_option =
|
||||
|idx: usize, option: TrustDirectorySelection, 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))
|
||||
}
|
||||
};
|
||||
|
||||
if self.is_git_repo {
|
||||
lines.push(create_option(
|
||||
0,
|
||||
TrustDirectorySelection::Trust,
|
||||
"Yes, allow Codex to work in this folder without asking for approval",
|
||||
));
|
||||
lines.push(create_option(
|
||||
1,
|
||||
TrustDirectorySelection::DontTrust,
|
||||
"No, ask me to approve edits and commands",
|
||||
));
|
||||
} else {
|
||||
lines.push(create_option(
|
||||
0,
|
||||
TrustDirectorySelection::Trust,
|
||||
"Allow Codex to work in this folder without asking for approval",
|
||||
));
|
||||
lines.push(create_option(
|
||||
1,
|
||||
TrustDirectorySelection::DontTrust,
|
||||
"Require approval of edits and commands",
|
||||
));
|
||||
}
|
||||
lines.push(Line::from(""));
|
||||
if let Some(error) = &self.error {
|
||||
lines.push(Line::from(format!(" {error}")).fg(Color::Red));
|
||||
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 TrustDirectoryWidget {
|
||||
fn handle_key_event(&mut self, key_event: KeyEvent) {
|
||||
match key_event.code {
|
||||
KeyCode::Up | KeyCode::Char('k') => {
|
||||
self.highlighted = TrustDirectorySelection::Trust;
|
||||
}
|
||||
KeyCode::Down | KeyCode::Char('j') => {
|
||||
self.highlighted = TrustDirectorySelection::DontTrust;
|
||||
}
|
||||
KeyCode::Char('1') => self.handle_trust(),
|
||||
KeyCode::Char('2') => self.handle_dont_trust(),
|
||||
KeyCode::Enter => match self.highlighted {
|
||||
TrustDirectorySelection::Trust => self.handle_trust(),
|
||||
TrustDirectorySelection::DontTrust => self.handle_dont_trust(),
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StepStateProvider for TrustDirectoryWidget {
|
||||
fn get_step_state(&self) -> StepState {
|
||||
match self.selection {
|
||||
Some(_) => StepState::Complete,
|
||||
None => StepState::InProgress,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TrustDirectoryWidget {
|
||||
fn handle_trust(&mut self) {
|
||||
if let Err(e) = set_project_trusted(&self.codex_home, &self.cwd) {
|
||||
tracing::error!("Failed to set project trusted: {e:?}");
|
||||
self.error = Some(e.to_string());
|
||||
// self.error = Some("Failed to set project trusted".to_string());
|
||||
}
|
||||
|
||||
// Update the in-memory chat config for this session to a more permissive
|
||||
// policy suitable for a trusted workspace.
|
||||
if let Ok(mut args) = self.chat_widget_args.lock() {
|
||||
args.config.approval_policy = AskForApproval::OnRequest;
|
||||
args.config.sandbox_policy = SandboxPolicy::new_workspace_write_policy();
|
||||
}
|
||||
|
||||
self.selection = Some(TrustDirectorySelection::Trust);
|
||||
}
|
||||
|
||||
fn handle_dont_trust(&mut self) {
|
||||
self.highlighted = TrustDirectorySelection::DontTrust;
|
||||
self.selection = Some(TrustDirectorySelection::DontTrust);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user