mirror of
https://github.com/openai/codex.git
synced 2026-10-20 00:18:49 +00:00
Make onboarding login links easier to copy (#48544)
## Why
When browser sign-in does not open automatically, users need to copy the login URL. Onboarding should also allow terminal selection of login URLs and device codes in fullscreen mode.
## What changed
- Add a `c` shortcut to copy the browser sign-in URL, with status messages for pending, successful, unconfirmed, busy, and failed clipboard requests.
- Apply asynchronous clipboard results only to the matching login attempt.
- Disable mouse capture during onboarding so the terminal can select text, and restore the previous input policy afterward.
## Testing
Update the narrow-screen login snapshot to show the copy shortcut and extend mouse-policy coverage to verify onboarding leaves mouse capture disabled.
GitOrigin-RevId: 01676b737efb192d900da306cd2c6b32f791ceb9
(cherry picked from commit 7ed14a27db)
This commit is contained in:
@@ -129,8 +129,8 @@ pub(crate) struct ApiKeyInputState {
|
||||
#[derive(Clone)]
|
||||
/// Used to manage the lifecycle of SpawnedLogin and ensure it gets cleaned up.
|
||||
pub(crate) struct ContinueInBrowserState {
|
||||
login_id: String,
|
||||
auth_url: String,
|
||||
pub(super) login_id: String,
|
||||
pub(super) auth_url: String,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -575,7 +575,14 @@ impl AuthModeWidget {
|
||||
let mut lines = vec![spans.into(), "".into()];
|
||||
|
||||
let auth_url = if !state.auth_url.is_empty() {
|
||||
lines.push(" If the link doesn't open automatically, open the following link to authenticate:".into());
|
||||
lines.push(Line::from(vec![
|
||||
" If the link doesn't open automatically, press ".into(),
|
||||
keys::COPY_LINK[0].into(),
|
||||
" to copy it:".into(),
|
||||
]));
|
||||
if let Some(message) = self.error_message() {
|
||||
lines.push(format!(" {message}").dim().into());
|
||||
}
|
||||
lines.push("".into());
|
||||
lines.push(Line::from(vec![
|
||||
" ".into(),
|
||||
|
||||
@@ -26,6 +26,7 @@ pub(crate) const SELECT_THIRD: [KeyBinding; 1] = [key_hint::plain(KeyCode::Char(
|
||||
pub(crate) const SELECT_FOURTH: [KeyBinding; 1] = [key_hint::plain(KeyCode::Char('4'))];
|
||||
pub(crate) const CONFIRM: [KeyBinding; 1] = [key_hint::plain(KeyCode::Enter)];
|
||||
pub(crate) const CANCEL: [KeyBinding; 1] = [key_hint::plain(KeyCode::Esc)];
|
||||
pub(crate) const COPY_LINK: [KeyBinding; 1] = [key_hint::plain(KeyCode::Char('c'))];
|
||||
pub(crate) const QUIT: [KeyBinding; 3] = [
|
||||
key_hint::plain(KeyCode::Char('q')),
|
||||
key_hint::ctrl(KeyCode::Char('c')),
|
||||
|
||||
@@ -33,6 +33,8 @@ use codex_protocol::config_types::ForcedLoginMethod;
|
||||
|
||||
use crate::LoginStatus;
|
||||
use crate::app_server_session::AppServerSession;
|
||||
use crate::clipboard_copy::CopyFormat;
|
||||
use crate::clipboard_copy::CopyStatus;
|
||||
use crate::config_update::RemoteProjectTrust;
|
||||
use crate::config_update::format_config_error;
|
||||
use crate::config_update::replace_config_value;
|
||||
@@ -48,6 +50,7 @@ use crate::onboarding::trust_directory::TrustDirectorySelection;
|
||||
use crate::onboarding::trust_directory::TrustDirectoryWidget;
|
||||
use crate::onboarding::welcome::WelcomeWidget;
|
||||
use crate::tui::FrameRequester;
|
||||
use crate::tui::OverlayInput;
|
||||
use crate::tui::Tui;
|
||||
use crate::tui::TuiEvent;
|
||||
use color_eyre::eyre::Result;
|
||||
@@ -543,6 +546,30 @@ pub(crate) async fn run_onboarding_app(
|
||||
}
|
||||
|
||||
async fn run_onboarding_screen(
|
||||
onboarding_screen: OnboardingScreen,
|
||||
app_server_request_handle: Option<AppServerRequestHandle>,
|
||||
app_server: Option<&mut AppServerSession>,
|
||||
tui: &mut Tui,
|
||||
) -> Result<OnboardingResult> {
|
||||
// Let the terminal select and copy login URLs and device codes, even in fullscreen mode.
|
||||
let previous_input = tui.overlay_input;
|
||||
let result = match tui.set_overlay_input(OverlayInput::Onboarding) {
|
||||
Ok(()) => {
|
||||
run_onboarding_screen_inner(
|
||||
onboarding_screen,
|
||||
app_server_request_handle,
|
||||
app_server,
|
||||
tui,
|
||||
)
|
||||
.await
|
||||
}
|
||||
Err(error) => Err(error.into()),
|
||||
};
|
||||
let restore = tui.set_overlay_input(previous_input);
|
||||
result.and_then(|result| restore.map(|()| result).map_err(Into::into))
|
||||
}
|
||||
|
||||
async fn run_onboarding_screen_inner(
|
||||
mut onboarding_screen: OnboardingScreen,
|
||||
app_server_request_handle: Option<AppServerRequestHandle>,
|
||||
mut app_server: Option<&mut AppServerSession>,
|
||||
@@ -552,6 +579,7 @@ async fn run_onboarding_screen(
|
||||
let mut directory_trust_persisted = false;
|
||||
// One-time guard to fully clear the screen after ChatGPT login success message is shown
|
||||
let mut did_full_clear_after_success = false;
|
||||
let mut pending_copy: Option<(u64, String)> = None;
|
||||
|
||||
tui.draw(u16::MAX, |frame| {
|
||||
frame.render_widget_ref(&onboarding_screen, frame.area());
|
||||
@@ -569,7 +597,36 @@ async fn run_onboarding_screen(
|
||||
tui.screen_size_for_event(&event)?;
|
||||
match event {
|
||||
TuiEvent::Key(key_event) => {
|
||||
onboarding_screen.handle_key_event(key_event);
|
||||
let copy_link = if key_event.kind == KeyEventKind::Press
|
||||
&& keys::COPY_LINK.is_pressed(key_event)
|
||||
&& let Some(Step::Auth(widget)) = onboarding_screen.current_steps().last()
|
||||
&& let Ok(state) = widget.sign_in_state.read()
|
||||
&& let SignInState::ChatGptContinueInBrowser(state) = &*state
|
||||
&& !state.auth_url.is_empty()
|
||||
{
|
||||
Some((state.login_id.clone(), Arc::<str>::from(state.auth_url.as_str())))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some((login_id, url)) = copy_link {
|
||||
let result = tui.clipboard.copy(url, CopyFormat::PlainText, tui.frame_requester());
|
||||
let message = match result {
|
||||
Ok(CopyStatus::Pending(id)) => {
|
||||
pending_copy = Some((id, login_id));
|
||||
"Copying link…".to_string()
|
||||
}
|
||||
Ok(CopyStatus::Busy) => "Clipboard busy; try again".to_string(),
|
||||
Ok(CopyStatus::Confirmed) => "Copied link to clipboard".to_string(),
|
||||
Ok(CopyStatus::Unconfirmed) => "Copy requested; check your clipboard".to_string(),
|
||||
Err(error) => format!("Could not copy link: {error}"),
|
||||
};
|
||||
if let Some(Step::Auth(widget)) = onboarding_screen.current_steps_mut().into_iter().last() {
|
||||
*widget.error.write().unwrap_or_else(std::sync::PoisonError::into_inner) = Some(message);
|
||||
}
|
||||
tui.frame_requester().schedule_frame();
|
||||
} else {
|
||||
onboarding_screen.handle_key_event(key_event);
|
||||
}
|
||||
if !directory_trust_persisted {
|
||||
directory_trust_persisted = persist_selected_trust(
|
||||
&mut onboarding_screen,
|
||||
@@ -586,6 +643,25 @@ async fn run_onboarding_screen(
|
||||
| TuiEvent::Resize(_)
|
||||
| TuiEvent::FocusGained
|
||||
| TuiEvent::FocusLost => {
|
||||
if let Some((id, result)) = tui.clipboard.poll().cloned()
|
||||
&& let Some((pending_id, login_id)) = &pending_copy
|
||||
&& id == *pending_id
|
||||
{
|
||||
if let Some(Step::Auth(widget)) = onboarding_screen.current_steps_mut().into_iter().last()
|
||||
&& widget.sign_in_state.read().is_ok_and(|state| {
|
||||
matches!(&*state, SignInState::ChatGptContinueInBrowser(state) if state.login_id == *login_id)
|
||||
})
|
||||
{
|
||||
let message = match result {
|
||||
Ok(CopyStatus::Confirmed) => "Copied link to clipboard".to_string(),
|
||||
Ok(CopyStatus::Unconfirmed) => "Copy requested; check your clipboard".to_string(),
|
||||
Ok(CopyStatus::Busy | CopyStatus::Pending(_)) => "Clipboard busy; try again".to_string(),
|
||||
Err(error) => format!("Could not copy link: {error}"),
|
||||
};
|
||||
*widget.error.write().unwrap_or_else(std::sync::PoisonError::into_inner) = Some(message);
|
||||
}
|
||||
pending_copy = None;
|
||||
}
|
||||
for step in &onboarding_screen.steps {
|
||||
if let Step::Welcome(widget) = step {
|
||||
if matches!(&event, TuiEvent::Resume) {
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ expression: contents
|
||||
Finish signing in via your browser
|
||||
|
||||
If the link doesn't open automatically,
|
||||
open the following link to authenticate:
|
||||
press c to copy it:
|
||||
|
||||
https://auth.openai.com/oauth/authorize?re
|
||||
sponse_type=code&client_id=app_EMoamEEZ73f0C
|
||||
|
||||
@@ -604,11 +604,12 @@ pub enum TuiEvent {
|
||||
FocusLost,
|
||||
}
|
||||
|
||||
/// The overlay requesting pointer reports; ordinary pickers retain alternate-scroll input.
|
||||
/// The current screen's pointer policy; ordinary pickers retain alternate-scroll input.
|
||||
#[derive(Clone, Copy, Default, PartialEq, Eq)]
|
||||
pub(crate) enum OverlayInput {
|
||||
#[default]
|
||||
Default,
|
||||
Onboarding,
|
||||
Transcript,
|
||||
StaticPager,
|
||||
Usage,
|
||||
@@ -618,7 +619,7 @@ impl OverlayInput {
|
||||
fn captures_mouse(self, owned_screen: bool) -> bool {
|
||||
match self {
|
||||
Self::Default => owned_screen,
|
||||
Self::StaticPager => false,
|
||||
Self::Onboarding | Self::StaticPager => false,
|
||||
Self::Transcript | Self::Usage => true,
|
||||
}
|
||||
}
|
||||
@@ -651,7 +652,7 @@ pub struct Tui {
|
||||
alt_screen_enabled: bool,
|
||||
// Keep the alternate screen alive when an overlay closes.
|
||||
owned_screen: bool,
|
||||
overlay_input: OverlayInput,
|
||||
pub(crate) overlay_input: OverlayInput,
|
||||
// Copies and native ownership survive closing an overlay or startup picker.
|
||||
pub(crate) clipboard: crate::clipboard_copy::worker::ClipboardWorker,
|
||||
// Keeps unmanaged process stderr writes out of the inline viewport.
|
||||
|
||||
@@ -35,12 +35,18 @@ fn refreshed_mouse_policy_applies_to_fullscreen_and_overlays() {
|
||||
for next in [
|
||||
OverlayInput::Transcript,
|
||||
OverlayInput::Usage,
|
||||
OverlayInput::StaticPager,
|
||||
OverlayInput::Onboarding,
|
||||
OverlayInput::Default,
|
||||
OverlayInput::StaticPager,
|
||||
] {
|
||||
input.apply(&screen, &mut output, next, owned).unwrap();
|
||||
terminal.process(&std::mem::take(&mut output));
|
||||
let expected = if !disabled && next.captures_mouse(owned) {
|
||||
let capture = match next {
|
||||
OverlayInput::Default => owned,
|
||||
OverlayInput::Transcript | OverlayInput::Usage => true,
|
||||
OverlayInput::StaticPager | OverlayInput::Onboarding => false,
|
||||
};
|
||||
let expected = if !disabled && capture {
|
||||
vt100::MouseProtocolMode::AnyMotion
|
||||
} else {
|
||||
vt100::MouseProtocolMode::None
|
||||
|
||||
Reference in New Issue
Block a user