The previous config approach had a few issues: 1. It is part of the config but not designed to be used externally 2. It had to be wired through many places (look at the +/- on this PR 3. It wasn't guaranteed to be set consistently everywhere because we don't have a super well defined way that configs stack. For example, the extension would configure during newConversation but anything that happened outside of that (like login) wouldn't get it. This env var approach is cleaner and also creates one less thing we have to deal with when coming up with a better holistic story around configs. One downside is that I removed the unit test testing for the override because I don't want to deal with setting the global env or spawning child processes and figuring out how to introspect their originator header. The new code is sufficiently simple and I tested it e2e that I feel as if this is still worth it.
326 lines
9.7 KiB
Rust
326 lines
9.7 KiB
Rust
use codex_core::AuthManager;
|
|
use codex_core::config::Config;
|
|
use codex_core::git_info::get_git_repo_root;
|
|
use crossterm::event::KeyCode;
|
|
use crossterm::event::KeyEvent;
|
|
use crossterm::event::KeyEventKind;
|
|
use ratatui::buffer::Buffer;
|
|
use ratatui::layout::Rect;
|
|
use ratatui::prelude::Widget;
|
|
use ratatui::widgets::Clear;
|
|
use ratatui::widgets::WidgetRef;
|
|
|
|
use codex_protocol::mcp_protocol::AuthMode;
|
|
|
|
use crate::LoginStatus;
|
|
use crate::onboarding::auth::AuthModeWidget;
|
|
use crate::onboarding::auth::SignInState;
|
|
use crate::onboarding::trust_directory::TrustDirectorySelection;
|
|
use crate::onboarding::trust_directory::TrustDirectoryWidget;
|
|
use crate::onboarding::welcome::WelcomeWidget;
|
|
use crate::tui::FrameRequester;
|
|
use crate::tui::Tui;
|
|
use crate::tui::TuiEvent;
|
|
use color_eyre::eyre::Result;
|
|
use std::sync::Arc;
|
|
use std::sync::RwLock;
|
|
|
|
#[allow(clippy::large_enum_variant)]
|
|
enum Step {
|
|
Welcome(WelcomeWidget),
|
|
Auth(AuthModeWidget),
|
|
TrustDirectory(TrustDirectoryWidget),
|
|
}
|
|
|
|
pub(crate) trait KeyboardHandler {
|
|
fn handle_key_event(&mut self, key_event: KeyEvent);
|
|
}
|
|
|
|
pub(crate) enum StepState {
|
|
Hidden,
|
|
InProgress,
|
|
Complete,
|
|
}
|
|
|
|
pub(crate) trait StepStateProvider {
|
|
fn get_step_state(&self) -> StepState;
|
|
}
|
|
|
|
pub(crate) struct OnboardingScreen {
|
|
request_frame: FrameRequester,
|
|
steps: Vec<Step>,
|
|
is_done: bool,
|
|
}
|
|
|
|
pub(crate) struct OnboardingScreenArgs {
|
|
pub show_trust_screen: bool,
|
|
pub show_login_screen: bool,
|
|
pub login_status: LoginStatus,
|
|
pub auth_manager: Arc<AuthManager>,
|
|
pub config: Config,
|
|
}
|
|
|
|
impl OnboardingScreen {
|
|
pub(crate) fn new(tui: &mut Tui, args: OnboardingScreenArgs) -> Self {
|
|
let OnboardingScreenArgs {
|
|
show_trust_screen,
|
|
show_login_screen,
|
|
login_status,
|
|
auth_manager,
|
|
config,
|
|
} = args;
|
|
let preferred_auth_method = config.preferred_auth_method;
|
|
let cwd = config.cwd.clone();
|
|
let codex_home = config.codex_home.clone();
|
|
let mut steps: Vec<Step> = vec![Step::Welcome(WelcomeWidget {
|
|
is_logged_in: !matches!(login_status, LoginStatus::NotAuthenticated),
|
|
})];
|
|
if show_login_screen {
|
|
steps.push(Step::Auth(AuthModeWidget {
|
|
request_frame: tui.frame_requester(),
|
|
highlighted_mode: AuthMode::ChatGPT,
|
|
error: None,
|
|
sign_in_state: Arc::new(RwLock::new(SignInState::PickMode)),
|
|
codex_home: codex_home.clone(),
|
|
login_status,
|
|
auth_manager,
|
|
preferred_auth_method,
|
|
}))
|
|
}
|
|
let is_git_repo = get_git_repo_root(&cwd).is_some();
|
|
let highlighted = if is_git_repo {
|
|
TrustDirectorySelection::Trust
|
|
} else {
|
|
// Default to not trusting the directory if it's not a git repo.
|
|
TrustDirectorySelection::DontTrust
|
|
};
|
|
if show_trust_screen {
|
|
steps.push(Step::TrustDirectory(TrustDirectoryWidget {
|
|
cwd,
|
|
codex_home,
|
|
is_git_repo,
|
|
selection: None,
|
|
highlighted,
|
|
error: None,
|
|
}))
|
|
}
|
|
// TODO: add git warning.
|
|
Self {
|
|
request_frame: tui.frame_requester(),
|
|
steps,
|
|
is_done: false,
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
pub(crate) fn is_done(&self) -> bool {
|
|
self.is_done
|
|
|| !self
|
|
.steps
|
|
.iter()
|
|
.any(|step| matches!(step.get_step_state(), StepState::InProgress))
|
|
}
|
|
|
|
pub fn directory_trust_decision(&self) -> Option<TrustDirectorySelection> {
|
|
self.steps
|
|
.iter()
|
|
.find_map(|step| {
|
|
if let Step::TrustDirectory(TrustDirectoryWidget { selection, .. }) = step {
|
|
Some(*selection)
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.flatten()
|
|
}
|
|
}
|
|
|
|
impl KeyboardHandler for OnboardingScreen {
|
|
fn handle_key_event(&mut self, key_event: KeyEvent) {
|
|
match key_event {
|
|
KeyEvent {
|
|
code: KeyCode::Char('d'),
|
|
modifiers: crossterm::event::KeyModifiers::CONTROL,
|
|
kind: KeyEventKind::Press,
|
|
..
|
|
}
|
|
| KeyEvent {
|
|
code: KeyCode::Char('c'),
|
|
modifiers: crossterm::event::KeyModifiers::CONTROL,
|
|
kind: KeyEventKind::Press,
|
|
..
|
|
}
|
|
| KeyEvent {
|
|
code: KeyCode::Char('q'),
|
|
kind: KeyEventKind::Press,
|
|
..
|
|
} => {
|
|
self.is_done = true;
|
|
}
|
|
_ => {
|
|
if let Some(active_step) = self.current_steps_mut().into_iter().last() {
|
|
active_step.handle_key_event(key_event);
|
|
}
|
|
}
|
|
};
|
|
self.request_frame.schedule_frame();
|
|
}
|
|
}
|
|
|
|
impl WidgetRef for &OnboardingScreen {
|
|
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
|
Clear.render(area, buf);
|
|
// Render steps top-to-bottom, measuring each step's height dynamically.
|
|
let mut y = area.y;
|
|
let bottom = area.y.saturating_add(area.height);
|
|
let width = area.width;
|
|
|
|
// Helper to scan a temporary buffer and return number of used rows.
|
|
fn used_rows(tmp: &Buffer, width: u16, height: u16) -> u16 {
|
|
if width == 0 || height == 0 {
|
|
return 0;
|
|
}
|
|
let mut last_non_empty: Option<u16> = None;
|
|
for yy in 0..height {
|
|
let mut any = false;
|
|
for xx in 0..width {
|
|
let sym = tmp[(xx, yy)].symbol();
|
|
if !sym.trim().is_empty() {
|
|
any = true;
|
|
break;
|
|
}
|
|
}
|
|
if any {
|
|
last_non_empty = Some(yy);
|
|
}
|
|
}
|
|
last_non_empty.map(|v| v + 2).unwrap_or(0)
|
|
}
|
|
|
|
let mut i = 0usize;
|
|
let current_steps = self.current_steps();
|
|
|
|
while i < current_steps.len() && y < bottom {
|
|
let step = ¤t_steps[i];
|
|
let max_h = bottom.saturating_sub(y);
|
|
if max_h == 0 || width == 0 {
|
|
break;
|
|
}
|
|
let scratch_area = Rect::new(0, 0, width, max_h);
|
|
let mut scratch = Buffer::empty(scratch_area);
|
|
step.render_ref(scratch_area, &mut scratch);
|
|
let h = used_rows(&scratch, width, max_h).min(max_h);
|
|
if h > 0 {
|
|
let target = Rect {
|
|
x: area.x,
|
|
y,
|
|
width,
|
|
height: h,
|
|
};
|
|
Clear.render(target, buf);
|
|
step.render_ref(target, buf);
|
|
y = y.saturating_add(h);
|
|
}
|
|
i += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
impl KeyboardHandler for Step {
|
|
fn handle_key_event(&mut self, key_event: KeyEvent) {
|
|
match self {
|
|
Step::Welcome(_) => (),
|
|
Step::Auth(widget) => widget.handle_key_event(key_event),
|
|
Step::TrustDirectory(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::TrustDirectory(w) => w.get_step_state(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl WidgetRef for Step {
|
|
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
|
match self {
|
|
Step::Welcome(widget) => {
|
|
widget.render_ref(area, buf);
|
|
}
|
|
Step::Auth(widget) => {
|
|
widget.render_ref(area, buf);
|
|
}
|
|
Step::TrustDirectory(widget) => {
|
|
widget.render_ref(area, buf);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) async fn run_onboarding_app(
|
|
args: OnboardingScreenArgs,
|
|
tui: &mut Tui,
|
|
) -> Result<Option<crate::onboarding::TrustDirectorySelection>> {
|
|
use tokio_stream::StreamExt;
|
|
|
|
let mut onboarding_screen = OnboardingScreen::new(tui, args);
|
|
|
|
tui.draw(u16::MAX, |frame| {
|
|
frame.render_widget_ref(&onboarding_screen, frame.area());
|
|
})?;
|
|
|
|
let tui_events = tui.event_stream();
|
|
tokio::pin!(tui_events);
|
|
|
|
while !onboarding_screen.is_done() {
|
|
if let Some(event) = tui_events.next().await {
|
|
match event {
|
|
TuiEvent::Key(key_event) => {
|
|
onboarding_screen.handle_key_event(key_event);
|
|
}
|
|
TuiEvent::Draw => {
|
|
let _ = tui.draw(u16::MAX, |frame| {
|
|
frame.render_widget_ref(&onboarding_screen, frame.area());
|
|
});
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
Ok(onboarding_screen.directory_trust_decision())
|
|
}
|