2025-08-20 13:47:24 -07:00
|
|
|
|
#![allow(clippy::unwrap_used)]
|
|
|
|
|
|
|
2025-09-02 18:36:19 -07:00
|
|
|
|
use codex_core::AuthManager;
|
2025-10-27 19:41:49 -07:00
|
|
|
|
use codex_core::auth::AuthCredentialsStoreMode;
|
2025-09-02 18:36:19 -07:00
|
|
|
|
use codex_core::auth::CLIENT_ID;
|
2025-09-11 09:16:34 -07:00
|
|
|
|
use codex_core::auth::login_with_api_key;
|
|
|
|
|
|
use codex_core::auth::read_openai_api_key_from_env;
|
2025-08-14 19:42:14 -07:00
|
|
|
|
use codex_login::ServerOptions;
|
2025-08-18 17:32:03 -07:00
|
|
|
|
use codex_login::ShutdownHandle;
|
2025-08-14 19:42:14 -07:00
|
|
|
|
use codex_login::run_login_server;
|
2025-08-06 15:22:14 -07:00
|
|
|
|
use crossterm::event::KeyCode;
|
|
|
|
|
|
use crossterm::event::KeyEvent;
|
2025-09-11 09:16:34 -07:00
|
|
|
|
use crossterm::event::KeyModifiers;
|
2025-08-06 15:22:14 -07:00
|
|
|
|
use ratatui::buffer::Buffer;
|
2025-09-11 09:16:34 -07:00
|
|
|
|
use ratatui::layout::Constraint;
|
|
|
|
|
|
use ratatui::layout::Layout;
|
2025-08-06 15:22:14 -07:00
|
|
|
|
use ratatui::layout::Rect;
|
|
|
|
|
|
use ratatui::prelude::Widget;
|
|
|
|
|
|
use ratatui::style::Color;
|
|
|
|
|
|
use ratatui::style::Modifier;
|
|
|
|
|
|
use ratatui::style::Style;
|
fix: clean up styles & colors and define in styles.md (#2401)
New style guide:
# Headers, primary, and secondary text
- **Headers:** Use `bold`. For markdown with various header levels,
leave in the `#` signs.
- **Primary text:** Default.
- **Secondary text:** Use `dim`.
# Foreground colors
- **Default:** Most of the time, just use the default foreground color.
`reset` can help get it back.
- **Selection:** Use ANSI `blue`. (Ed & AE want to make this cyan too,
but we'll do that in a followup since it's riskier in different themes.)
- **User input tips and status indicators:** Use ANSI `cyan`.
- **Success and additions:** Use ANSI `green`.
- **Errors, failures and deletions:** Use ANSI `red`.
- **Codex:** Use ANSI `magenta`.
# Avoid
- Avoid custom colors because there's no guarantee that they'll contrast
well or look good on various terminal color themes.
- Avoid ANSI `black`, `white`, `yellow` as foreground colors because the
terminal theme will do a better job. (Use `reset` if you need to in
order to get those.) The exception is if you need contrast rendering
over a manually colored background.
(There are some rules to try to catch this in `clippy.toml`.)
# Testing
Tested in a variety of light and dark color themes in Terminal, iTerm2, and Ghostty.
2025-08-18 08:26:29 -07:00
|
|
|
|
use ratatui::style::Stylize;
|
2025-08-06 15:22:14 -07:00
|
|
|
|
use ratatui::text::Line;
|
2025-09-11 09:16:34 -07:00
|
|
|
|
use ratatui::widgets::Block;
|
|
|
|
|
|
use ratatui::widgets::BorderType;
|
|
|
|
|
|
use ratatui::widgets::Borders;
|
2025-08-06 15:22:14 -07:00
|
|
|
|
use ratatui::widgets::Paragraph;
|
|
|
|
|
|
use ratatui::widgets::WidgetRef;
|
|
|
|
|
|
use ratatui::widgets::Wrap;
|
|
|
|
|
|
|
fix: remove mcp-types from app server protocol (#4537)
We continue the separation between `codex app-server` and `codex
mcp-server`.
In particular, we introduce a new crate, `codex-app-server-protocol`,
and migrate `codex-rs/protocol/src/mcp_protocol.rs` into it, renaming it
`codex-rs/app-server-protocol/src/protocol.rs`.
Because `ConversationId` was defined in `mcp_protocol.rs`, we move it
into its own file, `codex-rs/protocol/src/conversation_id.rs`, and
because it is referenced in a ton of places, we have to touch a lot of
files as part of this PR.
We also decide to get away from proper JSON-RPC 2.0 semantics, so we
also introduce `codex-rs/app-server-protocol/src/jsonrpc_lite.rs`, which
is basically the same `JSONRPCMessage` type defined in `mcp-types`
except with all of the `"jsonrpc": "2.0"` removed.
Getting rid of `"jsonrpc": "2.0"` makes our serialization logic
considerably simpler, as we can lean heavier on serde to serialize
directly into the wire format that we use now.
2025-09-30 19:16:26 -07:00
|
|
|
|
use codex_app_server_protocol::AuthMode;
|
2025-10-20 08:50:54 -07:00
|
|
|
|
use codex_protocol::config_types::ForcedLoginMethod;
|
2025-08-20 13:47:24 -07:00
|
|
|
|
use std::sync::RwLock;
|
2025-08-06 15:22:14 -07:00
|
|
|
|
|
2025-08-18 20:22:48 -07:00
|
|
|
|
use crate::LoginStatus;
|
2025-08-06 15:22:14 -07:00
|
|
|
|
use crate::onboarding::onboarding_screen::KeyboardHandler;
|
2025-08-06 19:39:07 -07:00
|
|
|
|
use crate::onboarding::onboarding_screen::StepStateProvider;
|
2025-08-06 15:22:14 -07:00
|
|
|
|
use crate::shimmer::shimmer_spans;
|
2025-08-20 13:47:24 -07:00
|
|
|
|
use crate::tui::FrameRequester;
|
2025-08-06 15:22:14 -07:00
|
|
|
|
use std::path::PathBuf;
|
2025-08-20 13:47:24 -07:00
|
|
|
|
use std::sync::Arc;
|
2025-08-06 19:39:07 -07:00
|
|
|
|
|
|
|
|
|
|
use super::onboarding_screen::StepState;
|
2025-08-06 15:22:14 -07:00
|
|
|
|
|
2025-08-20 13:47:24 -07:00
|
|
|
|
#[derive(Clone)]
|
2025-08-06 15:22:14 -07:00
|
|
|
|
pub(crate) enum SignInState {
|
|
|
|
|
|
PickMode,
|
2025-08-08 18:30:34 -07:00
|
|
|
|
ChatGptContinueInBrowser(ContinueInBrowserState),
|
2025-08-06 19:39:07 -07:00
|
|
|
|
ChatGptSuccessMessage,
|
2025-08-06 15:22:14 -07:00
|
|
|
|
ChatGptSuccess,
|
2025-09-11 09:16:34 -07:00
|
|
|
|
ApiKeyEntry(ApiKeyInputState),
|
|
|
|
|
|
ApiKeyConfigured,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-20 08:50:54 -07:00
|
|
|
|
const API_KEY_DISABLED_MESSAGE: &str = "API key login is disabled.";
|
|
|
|
|
|
|
2025-09-11 09:16:34 -07:00
|
|
|
|
#[derive(Clone, Default)]
|
|
|
|
|
|
pub(crate) struct ApiKeyInputState {
|
|
|
|
|
|
value: String,
|
|
|
|
|
|
prepopulated_from_env: bool,
|
2025-08-06 15:22:14 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-20 13:47:24 -07:00
|
|
|
|
#[derive(Clone)]
|
2025-08-14 16:59:47 -04:00
|
|
|
|
/// Used to manage the lifecycle of SpawnedLogin and ensure it gets cleaned up.
|
2025-08-06 15:22:14 -07:00
|
|
|
|
pub(crate) struct ContinueInBrowserState {
|
2025-08-14 19:42:14 -07:00
|
|
|
|
auth_url: String,
|
2025-08-20 13:47:24 -07:00
|
|
|
|
shutdown_flag: Option<ShutdownHandle>,
|
2025-08-06 15:22:14 -07:00
|
|
|
|
}
|
2025-08-18 17:32:03 -07:00
|
|
|
|
|
2025-08-06 15:22:14 -07:00
|
|
|
|
impl Drop for ContinueInBrowserState {
|
|
|
|
|
|
fn drop(&mut self) {
|
2025-08-20 13:47:24 -07:00
|
|
|
|
if let Some(handle) = &self.shutdown_flag {
|
|
|
|
|
|
handle.shutdown();
|
2025-08-06 15:22:14 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl KeyboardHandler for AuthModeWidget {
|
2025-08-06 19:39:07 -07:00
|
|
|
|
fn handle_key_event(&mut self, key_event: KeyEvent) {
|
2025-09-11 09:16:34 -07:00
|
|
|
|
if self.handle_api_key_entry_key_event(&key_event) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-06 15:22:14 -07:00
|
|
|
|
match key_event.code {
|
|
|
|
|
|
KeyCode::Up | KeyCode::Char('k') => {
|
2025-10-20 08:50:54 -07:00
|
|
|
|
if self.is_chatgpt_login_allowed() {
|
|
|
|
|
|
self.highlighted_mode = AuthMode::ChatGPT;
|
|
|
|
|
|
}
|
2025-08-06 15:22:14 -07:00
|
|
|
|
}
|
|
|
|
|
|
KeyCode::Down | KeyCode::Char('j') => {
|
2025-10-20 08:50:54 -07:00
|
|
|
|
if self.is_api_login_allowed() {
|
|
|
|
|
|
self.highlighted_mode = AuthMode::ApiKey;
|
|
|
|
|
|
}
|
2025-08-06 15:22:14 -07:00
|
|
|
|
}
|
|
|
|
|
|
KeyCode::Char('1') => {
|
2025-10-20 08:50:54 -07:00
|
|
|
|
if self.is_chatgpt_login_allowed() {
|
|
|
|
|
|
self.start_chatgpt_login();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
KeyCode::Char('2') => {
|
|
|
|
|
|
if self.is_api_login_allowed() {
|
|
|
|
|
|
self.start_api_key_entry();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.disallow_api_login();
|
|
|
|
|
|
}
|
2025-08-06 15:22:14 -07:00
|
|
|
|
}
|
2025-08-20 13:47:24 -07:00
|
|
|
|
KeyCode::Enter => {
|
|
|
|
|
|
let sign_in_state = { (*self.sign_in_state.read().unwrap()).clone() };
|
|
|
|
|
|
match sign_in_state {
|
|
|
|
|
|
SignInState::PickMode => match self.highlighted_mode {
|
2025-10-20 08:50:54 -07:00
|
|
|
|
AuthMode::ChatGPT if self.is_chatgpt_login_allowed() => {
|
2025-08-20 13:47:24 -07:00
|
|
|
|
self.start_chatgpt_login();
|
|
|
|
|
|
}
|
2025-10-20 08:50:54 -07:00
|
|
|
|
AuthMode::ApiKey if self.is_api_login_allowed() => {
|
2025-09-11 09:16:34 -07:00
|
|
|
|
self.start_api_key_entry();
|
2025-08-20 13:47:24 -07:00
|
|
|
|
}
|
2025-10-20 08:50:54 -07:00
|
|
|
|
AuthMode::ChatGPT => {}
|
|
|
|
|
|
AuthMode::ApiKey => {
|
|
|
|
|
|
self.disallow_api_login();
|
|
|
|
|
|
}
|
2025-08-20 13:47:24 -07:00
|
|
|
|
},
|
|
|
|
|
|
SignInState::ChatGptSuccessMessage => {
|
|
|
|
|
|
*self.sign_in_state.write().unwrap() = SignInState::ChatGptSuccess;
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => {}
|
2025-08-06 19:39:07 -07:00
|
|
|
|
}
|
2025-08-20 13:47:24 -07:00
|
|
|
|
}
|
2025-08-06 15:22:14 -07:00
|
|
|
|
KeyCode::Esc => {
|
2025-08-20 13:47:24 -07:00
|
|
|
|
tracing::info!("Esc pressed");
|
|
|
|
|
|
let sign_in_state = { (*self.sign_in_state.read().unwrap()).clone() };
|
|
|
|
|
|
if matches!(sign_in_state, SignInState::ChatGptContinueInBrowser(_)) {
|
|
|
|
|
|
*self.sign_in_state.write().unwrap() = SignInState::PickMode;
|
|
|
|
|
|
self.request_frame.schedule_frame();
|
2025-08-06 15:22:14 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-08-06 19:39:07 -07:00
|
|
|
|
_ => {}
|
2025-08-06 15:22:14 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-09-11 09:16:34 -07:00
|
|
|
|
|
|
|
|
|
|
fn handle_paste(&mut self, pasted: String) {
|
|
|
|
|
|
let _ = self.handle_api_key_entry_paste(pasted);
|
|
|
|
|
|
}
|
2025-08-06 15:22:14 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-20 13:47:24 -07:00
|
|
|
|
#[derive(Clone)]
|
2025-08-06 15:22:14 -07:00
|
|
|
|
pub(crate) struct AuthModeWidget {
|
2025-08-20 13:47:24 -07:00
|
|
|
|
pub request_frame: FrameRequester,
|
2025-08-06 19:39:07 -07:00
|
|
|
|
pub highlighted_mode: AuthMode,
|
2025-08-06 15:22:14 -07:00
|
|
|
|
pub error: Option<String>,
|
2025-08-20 13:47:24 -07:00
|
|
|
|
pub sign_in_state: Arc<RwLock<SignInState>>,
|
2025-08-06 15:22:14 -07:00
|
|
|
|
pub codex_home: PathBuf,
|
2025-10-27 19:41:49 -07:00
|
|
|
|
pub cli_auth_credentials_store_mode: AuthCredentialsStoreMode,
|
2025-08-18 20:22:48 -07:00
|
|
|
|
pub login_status: LoginStatus,
|
2025-08-22 13:10:11 -07:00
|
|
|
|
pub auth_manager: Arc<AuthManager>,
|
2025-10-20 08:50:54 -07:00
|
|
|
|
pub forced_chatgpt_workspace_id: Option<String>,
|
|
|
|
|
|
pub forced_login_method: Option<ForcedLoginMethod>,
|
2025-08-06 15:22:14 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl AuthModeWidget {
|
2025-10-20 08:50:54 -07:00
|
|
|
|
fn is_api_login_allowed(&self) -> bool {
|
|
|
|
|
|
!matches!(self.forced_login_method, Some(ForcedLoginMethod::Chatgpt))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn is_chatgpt_login_allowed(&self) -> bool {
|
|
|
|
|
|
!matches!(self.forced_login_method, Some(ForcedLoginMethod::Api))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn disallow_api_login(&mut self) {
|
|
|
|
|
|
self.highlighted_mode = AuthMode::ChatGPT;
|
|
|
|
|
|
self.error = Some(API_KEY_DISABLED_MESSAGE.to_string());
|
|
|
|
|
|
*self.sign_in_state.write().unwrap() = SignInState::PickMode;
|
|
|
|
|
|
self.request_frame.schedule_frame();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-06 15:22:14 -07:00
|
|
|
|
fn render_pick_mode(&self, area: Rect, buf: &mut Buffer) {
|
|
|
|
|
|
let mut lines: Vec<Line> = vec![
|
|
|
|
|
|
Line::from(vec![
|
2025-09-15 00:42:53 -07:00
|
|
|
|
" ".into(),
|
|
|
|
|
|
"Sign in with ChatGPT to use Codex as part of your paid plan".into(),
|
2025-08-07 03:29:33 -07:00
|
|
|
|
]),
|
|
|
|
|
|
Line::from(vec![
|
2025-09-02 16:19:54 -07:00
|
|
|
|
" ".into(),
|
2025-09-15 00:42:53 -07:00
|
|
|
|
"or connect an API key for usage-based billing".into(),
|
2025-08-06 15:22:14 -07:00
|
|
|
|
]),
|
2025-09-02 16:19:54 -07:00
|
|
|
|
"".into(),
|
2025-08-06 15:22:14 -07:00
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
let create_mode_item = |idx: usize,
|
|
|
|
|
|
selected_mode: AuthMode,
|
|
|
|
|
|
text: &str,
|
|
|
|
|
|
description: &str|
|
|
|
|
|
|
-> Vec<Line<'static>> {
|
2025-08-06 19:39:07 -07:00
|
|
|
|
let is_selected = self.highlighted_mode == selected_mode;
|
2025-08-06 15:22:14 -07:00
|
|
|
|
let caret = if is_selected { ">" } else { " " };
|
|
|
|
|
|
|
|
|
|
|
|
let line1 = if is_selected {
|
|
|
|
|
|
Line::from(vec![
|
2025-08-18 09:02:25 -07:00
|
|
|
|
format!("{} {}. ", caret, idx + 1).cyan().dim(),
|
|
|
|
|
|
text.to_string().cyan(),
|
2025-08-06 15:22:14 -07:00
|
|
|
|
])
|
|
|
|
|
|
} else {
|
2025-09-02 16:19:54 -07:00
|
|
|
|
format!(" {}. {text}", idx + 1).into()
|
2025-08-06 15:22:14 -07:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let line2 = if is_selected {
|
|
|
|
|
|
Line::from(format!(" {description}"))
|
2025-08-18 09:02:25 -07:00
|
|
|
|
.fg(Color::Cyan)
|
fix: clean up styles & colors and define in styles.md (#2401)
New style guide:
# Headers, primary, and secondary text
- **Headers:** Use `bold`. For markdown with various header levels,
leave in the `#` signs.
- **Primary text:** Default.
- **Secondary text:** Use `dim`.
# Foreground colors
- **Default:** Most of the time, just use the default foreground color.
`reset` can help get it back.
- **Selection:** Use ANSI `blue`. (Ed & AE want to make this cyan too,
but we'll do that in a followup since it's riskier in different themes.)
- **User input tips and status indicators:** Use ANSI `cyan`.
- **Success and additions:** Use ANSI `green`.
- **Errors, failures and deletions:** Use ANSI `red`.
- **Codex:** Use ANSI `magenta`.
# Avoid
- Avoid custom colors because there's no guarantee that they'll contrast
well or look good on various terminal color themes.
- Avoid ANSI `black`, `white`, `yellow` as foreground colors because the
terminal theme will do a better job. (Use `reset` if you need to in
order to get those.) The exception is if you need contrast rendering
over a manually colored background.
(There are some rules to try to catch this in `clippy.toml`.)
# Testing
Tested in a variety of light and dark color themes in Terminal, iTerm2, and Ghostty.
2025-08-18 08:26:29 -07:00
|
|
|
|
.add_modifier(Modifier::DIM)
|
2025-08-06 15:22:14 -07:00
|
|
|
|
} else {
|
|
|
|
|
|
Line::from(format!(" {description}"))
|
|
|
|
|
|
.style(Style::default().add_modifier(Modifier::DIM))
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
vec![line1, line2]
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-10-20 08:50:54 -07:00
|
|
|
|
let chatgpt_description = if self.is_chatgpt_login_allowed() {
|
|
|
|
|
|
"Usage included with Plus, Pro, and Team plans"
|
|
|
|
|
|
} else {
|
|
|
|
|
|
"ChatGPT login is disabled"
|
|
|
|
|
|
};
|
2025-08-06 15:22:14 -07:00
|
|
|
|
lines.extend(create_mode_item(
|
|
|
|
|
|
0,
|
|
|
|
|
|
AuthMode::ChatGPT,
|
2025-09-11 09:16:34 -07:00
|
|
|
|
"Sign in with ChatGPT",
|
2025-10-20 08:50:54 -07:00
|
|
|
|
chatgpt_description,
|
2025-08-06 15:22:14 -07:00
|
|
|
|
));
|
2025-09-02 16:19:54 -07:00
|
|
|
|
lines.push("".into());
|
2025-10-20 08:50:54 -07:00
|
|
|
|
if self.is_api_login_allowed() {
|
|
|
|
|
|
lines.extend(create_mode_item(
|
|
|
|
|
|
1,
|
|
|
|
|
|
AuthMode::ApiKey,
|
|
|
|
|
|
"Provide your own API key",
|
|
|
|
|
|
"Pay for what you use",
|
|
|
|
|
|
));
|
|
|
|
|
|
lines.push("".into());
|
|
|
|
|
|
} else {
|
|
|
|
|
|
lines.push(
|
|
|
|
|
|
" API key login is disabled by this workspace. Sign in with ChatGPT to continue."
|
|
|
|
|
|
.dim()
|
|
|
|
|
|
.into(),
|
|
|
|
|
|
);
|
|
|
|
|
|
lines.push("".into());
|
|
|
|
|
|
}
|
2025-08-06 15:22:14 -07:00
|
|
|
|
lines.push(
|
2025-08-18 09:02:25 -07:00
|
|
|
|
// AE: Following styles.md, this should probably be Cyan because it's a user input tip.
|
|
|
|
|
|
// But leaving this for a future cleanup.
|
2025-09-02 16:19:54 -07:00
|
|
|
|
" Press Enter to continue".dim().into(),
|
2025-08-06 15:22:14 -07:00
|
|
|
|
);
|
|
|
|
|
|
if let Some(err) = &self.error {
|
2025-09-02 16:19:54 -07:00
|
|
|
|
lines.push("".into());
|
|
|
|
|
|
lines.push(err.as_str().red().into());
|
2025-08-06 15:22:14 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Paragraph::new(lines)
|
|
|
|
|
|
.wrap(Wrap { trim: false })
|
|
|
|
|
|
.render(area, buf);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn render_continue_in_browser(&self, area: Rect, buf: &mut Buffer) {
|
2025-09-15 00:42:53 -07:00
|
|
|
|
let mut spans = vec![" ".into()];
|
2025-08-14 16:59:47 -04:00
|
|
|
|
// Schedule a follow-up frame to keep the shimmer animation going.
|
2025-08-20 13:47:24 -07:00
|
|
|
|
self.request_frame
|
|
|
|
|
|
.schedule_frame_in(std::time::Duration::from_millis(100));
|
2025-08-14 16:59:47 -04:00
|
|
|
|
spans.extend(shimmer_spans("Finish signing in via your browser"));
|
2025-09-02 16:19:54 -07:00
|
|
|
|
let mut lines = vec![spans.into(), "".into()];
|
2025-08-08 18:30:34 -07:00
|
|
|
|
|
2025-08-20 13:47:24 -07:00
|
|
|
|
let sign_in_state = self.sign_in_state.read().unwrap();
|
|
|
|
|
|
if let SignInState::ChatGptContinueInBrowser(state) = &*sign_in_state
|
2025-08-19 13:22:02 -07:00
|
|
|
|
&& !state.auth_url.is_empty()
|
|
|
|
|
|
{
|
2025-09-02 16:19:54 -07:00
|
|
|
|
lines.push(" If the link doesn't open automatically, open the following link to authenticate:".into());
|
2025-09-15 00:42:53 -07:00
|
|
|
|
lines.push("".into());
|
|
|
|
|
|
lines.push(Line::from(state.auth_url.as_str().cyan().underlined()));
|
2025-09-02 16:19:54 -07:00
|
|
|
|
lines.push("".into());
|
2025-08-08 18:30:34 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-02 16:19:54 -07:00
|
|
|
|
lines.push(" Press Esc to cancel".dim().into());
|
2025-08-06 15:22:14 -07:00
|
|
|
|
Paragraph::new(lines)
|
|
|
|
|
|
.wrap(Wrap { trim: false })
|
|
|
|
|
|
.render(area, buf);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-06 19:39:07 -07:00
|
|
|
|
fn render_chatgpt_success_message(&self, area: Rect, buf: &mut Buffer) {
|
2025-08-06 15:22:14 -07:00
|
|
|
|
let lines = vec![
|
2025-09-02 16:19:54 -07:00
|
|
|
|
"✓ Signed in with your ChatGPT account".fg(Color::Green).into(),
|
|
|
|
|
|
"".into(),
|
2025-09-15 00:42:53 -07:00
|
|
|
|
" Before you start:".into(),
|
2025-09-02 16:19:54 -07:00
|
|
|
|
"".into(),
|
|
|
|
|
|
" Decide how much autonomy you want to grant Codex".into(),
|
2025-08-07 03:29:33 -07:00
|
|
|
|
Line::from(vec![
|
2025-09-02 16:19:54 -07:00
|
|
|
|
" For more details see the ".into(),
|
|
|
|
|
|
"\u{1b}]8;;https://github.com/openai/codex\u{7}Codex docs\u{1b}]8;;\u{7}".underlined(),
|
2025-08-07 03:29:33 -07:00
|
|
|
|
])
|
2025-09-02 16:19:54 -07:00
|
|
|
|
.dim(),
|
|
|
|
|
|
"".into(),
|
|
|
|
|
|
" Codex can make mistakes".into(),
|
|
|
|
|
|
" Review the code it writes and commands it runs".dim().into(),
|
|
|
|
|
|
"".into(),
|
|
|
|
|
|
" Powered by your ChatGPT account".into(),
|
2025-08-07 03:29:33 -07:00
|
|
|
|
Line::from(vec![
|
2025-09-02 16:19:54 -07:00
|
|
|
|
" Uses your plan's rate limits and ".into(),
|
|
|
|
|
|
"\u{1b}]8;;https://chatgpt.com/#settings\u{7}training data preferences\u{1b}]8;;\u{7}".underlined(),
|
2025-08-07 03:29:33 -07:00
|
|
|
|
])
|
2025-09-02 16:19:54 -07:00
|
|
|
|
.dim(),
|
|
|
|
|
|
"".into(),
|
|
|
|
|
|
" Press Enter to continue".fg(Color::Cyan).into(),
|
2025-08-06 15:22:14 -07:00
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
Paragraph::new(lines)
|
|
|
|
|
|
.wrap(Wrap { trim: false })
|
|
|
|
|
|
.render(area, buf);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-06 19:39:07 -07:00
|
|
|
|
fn render_chatgpt_success(&self, area: Rect, buf: &mut Buffer) {
|
2025-09-02 16:19:54 -07:00
|
|
|
|
let lines = vec![
|
|
|
|
|
|
"✓ Signed in with your ChatGPT account"
|
|
|
|
|
|
.fg(Color::Green)
|
|
|
|
|
|
.into(),
|
|
|
|
|
|
];
|
2025-08-06 19:39:07 -07:00
|
|
|
|
|
|
|
|
|
|
Paragraph::new(lines)
|
|
|
|
|
|
.wrap(Wrap { trim: false })
|
|
|
|
|
|
.render(area, buf);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-11 09:16:34 -07:00
|
|
|
|
fn render_api_key_configured(&self, area: Rect, buf: &mut Buffer) {
|
|
|
|
|
|
let lines = vec![
|
|
|
|
|
|
"✓ API key configured".fg(Color::Green).into(),
|
|
|
|
|
|
"".into(),
|
|
|
|
|
|
" Codex will use usage-based billing with your API key.".into(),
|
|
|
|
|
|
];
|
2025-08-06 19:39:07 -07:00
|
|
|
|
|
|
|
|
|
|
Paragraph::new(lines)
|
|
|
|
|
|
.wrap(Wrap { trim: false })
|
|
|
|
|
|
.render(area, buf);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-11 09:16:34 -07:00
|
|
|
|
fn render_api_key_entry(&self, area: Rect, buf: &mut Buffer, state: &ApiKeyInputState) {
|
|
|
|
|
|
let [intro_area, input_area, footer_area] = Layout::vertical([
|
|
|
|
|
|
Constraint::Min(4),
|
|
|
|
|
|
Constraint::Length(3),
|
|
|
|
|
|
Constraint::Min(2),
|
|
|
|
|
|
])
|
|
|
|
|
|
.areas(area);
|
|
|
|
|
|
|
|
|
|
|
|
let mut intro_lines: Vec<Line> = vec![
|
|
|
|
|
|
Line::from(vec![
|
|
|
|
|
|
"> ".into(),
|
|
|
|
|
|
"Use your own OpenAI API key for usage-based billing".bold(),
|
|
|
|
|
|
]),
|
|
|
|
|
|
"".into(),
|
|
|
|
|
|
" Paste or type your API key below. It will be stored locally in auth.json.".into(),
|
2025-09-02 16:19:54 -07:00
|
|
|
|
"".into(),
|
2025-08-06 19:39:07 -07:00
|
|
|
|
];
|
2025-09-11 09:16:34 -07:00
|
|
|
|
if state.prepopulated_from_env {
|
|
|
|
|
|
intro_lines.push(" Detected OPENAI_API_KEY environment variable.".into());
|
|
|
|
|
|
intro_lines.push(
|
|
|
|
|
|
" Paste a different key if you prefer to use another account."
|
|
|
|
|
|
.dim()
|
|
|
|
|
|
.into(),
|
|
|
|
|
|
);
|
|
|
|
|
|
intro_lines.push("".into());
|
|
|
|
|
|
}
|
|
|
|
|
|
Paragraph::new(intro_lines)
|
|
|
|
|
|
.wrap(Wrap { trim: false })
|
|
|
|
|
|
.render(intro_area, buf);
|
2025-08-06 19:39:07 -07:00
|
|
|
|
|
2025-09-11 09:16:34 -07:00
|
|
|
|
let content_line: Line = if state.value.is_empty() {
|
|
|
|
|
|
vec!["Paste or type your API key".dim()].into()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Line::from(state.value.clone())
|
|
|
|
|
|
};
|
|
|
|
|
|
Paragraph::new(content_line)
|
2025-08-06 19:39:07 -07:00
|
|
|
|
.wrap(Wrap { trim: false })
|
2025-09-11 09:16:34 -07:00
|
|
|
|
.block(
|
|
|
|
|
|
Block::default()
|
|
|
|
|
|
.title("API key")
|
|
|
|
|
|
.borders(Borders::ALL)
|
|
|
|
|
|
.border_type(BorderType::Rounded)
|
|
|
|
|
|
.border_style(Style::default().fg(Color::Cyan)),
|
|
|
|
|
|
)
|
|
|
|
|
|
.render(input_area, buf);
|
|
|
|
|
|
|
|
|
|
|
|
let mut footer_lines: Vec<Line> = vec![
|
|
|
|
|
|
" Press Enter to save".dim().into(),
|
|
|
|
|
|
" Press Esc to go back".dim().into(),
|
|
|
|
|
|
];
|
|
|
|
|
|
if let Some(error) = &self.error {
|
|
|
|
|
|
footer_lines.push("".into());
|
|
|
|
|
|
footer_lines.push(error.as_str().red().into());
|
|
|
|
|
|
}
|
|
|
|
|
|
Paragraph::new(footer_lines)
|
|
|
|
|
|
.wrap(Wrap { trim: false })
|
|
|
|
|
|
.render(footer_area, buf);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn handle_api_key_entry_key_event(&mut self, key_event: &KeyEvent) -> bool {
|
|
|
|
|
|
let mut should_save: Option<String> = None;
|
|
|
|
|
|
let mut should_request_frame = false;
|
|
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
let mut guard = self.sign_in_state.write().unwrap();
|
|
|
|
|
|
if let SignInState::ApiKeyEntry(state) = &mut *guard {
|
|
|
|
|
|
match key_event.code {
|
|
|
|
|
|
KeyCode::Esc => {
|
|
|
|
|
|
*guard = SignInState::PickMode;
|
|
|
|
|
|
self.error = None;
|
|
|
|
|
|
should_request_frame = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
KeyCode::Enter => {
|
|
|
|
|
|
let trimmed = state.value.trim().to_string();
|
|
|
|
|
|
if trimmed.is_empty() {
|
|
|
|
|
|
self.error = Some("API key cannot be empty".to_string());
|
|
|
|
|
|
should_request_frame = true;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
should_save = Some(trimmed);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
KeyCode::Backspace => {
|
|
|
|
|
|
if state.prepopulated_from_env {
|
|
|
|
|
|
state.value.clear();
|
|
|
|
|
|
state.prepopulated_from_env = false;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
state.value.pop();
|
|
|
|
|
|
}
|
|
|
|
|
|
self.error = None;
|
|
|
|
|
|
should_request_frame = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
KeyCode::Char(c)
|
|
|
|
|
|
if !key_event.modifiers.contains(KeyModifiers::CONTROL)
|
|
|
|
|
|
&& !key_event.modifiers.contains(KeyModifiers::ALT) =>
|
|
|
|
|
|
{
|
|
|
|
|
|
if state.prepopulated_from_env {
|
|
|
|
|
|
state.value.clear();
|
|
|
|
|
|
state.prepopulated_from_env = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
state.value.push(c);
|
|
|
|
|
|
self.error = None;
|
|
|
|
|
|
should_request_frame = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => {}
|
|
|
|
|
|
}
|
|
|
|
|
|
// handled; let guard drop before potential save
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if let Some(api_key) = should_save {
|
|
|
|
|
|
self.save_api_key(api_key);
|
|
|
|
|
|
} else if should_request_frame {
|
|
|
|
|
|
self.request_frame.schedule_frame();
|
|
|
|
|
|
}
|
|
|
|
|
|
true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn handle_api_key_entry_paste(&mut self, pasted: String) -> bool {
|
|
|
|
|
|
let trimmed = pasted.trim();
|
|
|
|
|
|
if trimmed.is_empty() {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let mut guard = self.sign_in_state.write().unwrap();
|
|
|
|
|
|
if let SignInState::ApiKeyEntry(state) = &mut *guard {
|
|
|
|
|
|
if state.prepopulated_from_env {
|
|
|
|
|
|
state.value = trimmed.to_string();
|
|
|
|
|
|
state.prepopulated_from_env = false;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
state.value.push_str(trimmed);
|
|
|
|
|
|
}
|
|
|
|
|
|
self.error = None;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
drop(guard);
|
|
|
|
|
|
self.request_frame.schedule_frame();
|
|
|
|
|
|
true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn start_api_key_entry(&mut self) {
|
2025-10-20 08:50:54 -07:00
|
|
|
|
if !self.is_api_login_allowed() {
|
|
|
|
|
|
self.disallow_api_login();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-09-11 09:16:34 -07:00
|
|
|
|
self.error = None;
|
|
|
|
|
|
let prefill_from_env = read_openai_api_key_from_env();
|
|
|
|
|
|
let mut guard = self.sign_in_state.write().unwrap();
|
|
|
|
|
|
match &mut *guard {
|
|
|
|
|
|
SignInState::ApiKeyEntry(state) => {
|
|
|
|
|
|
if state.value.is_empty() {
|
2025-09-11 11:59:37 -07:00
|
|
|
|
if let Some(prefill) = prefill_from_env {
|
2025-09-11 09:16:34 -07:00
|
|
|
|
state.value = prefill;
|
|
|
|
|
|
state.prepopulated_from_env = true;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
state.prepopulated_from_env = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => {
|
|
|
|
|
|
*guard = SignInState::ApiKeyEntry(ApiKeyInputState {
|
|
|
|
|
|
value: prefill_from_env.clone().unwrap_or_default(),
|
|
|
|
|
|
prepopulated_from_env: prefill_from_env.is_some(),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
drop(guard);
|
|
|
|
|
|
self.request_frame.schedule_frame();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn save_api_key(&mut self, api_key: String) {
|
2025-10-20 08:50:54 -07:00
|
|
|
|
if !self.is_api_login_allowed() {
|
|
|
|
|
|
self.disallow_api_login();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-10-27 19:41:49 -07:00
|
|
|
|
match login_with_api_key(
|
|
|
|
|
|
&self.codex_home,
|
|
|
|
|
|
&api_key,
|
|
|
|
|
|
self.cli_auth_credentials_store_mode,
|
|
|
|
|
|
) {
|
2025-09-11 09:16:34 -07:00
|
|
|
|
Ok(()) => {
|
|
|
|
|
|
self.error = None;
|
|
|
|
|
|
self.login_status = LoginStatus::AuthMode(AuthMode::ApiKey);
|
|
|
|
|
|
self.auth_manager.reload();
|
|
|
|
|
|
*self.sign_in_state.write().unwrap() = SignInState::ApiKeyConfigured;
|
|
|
|
|
|
}
|
|
|
|
|
|
Err(err) => {
|
|
|
|
|
|
self.error = Some(format!("Failed to save API key: {err}"));
|
|
|
|
|
|
let mut guard = self.sign_in_state.write().unwrap();
|
|
|
|
|
|
if let SignInState::ApiKeyEntry(existing) = &mut *guard {
|
|
|
|
|
|
if existing.value.is_empty() {
|
|
|
|
|
|
existing.value.push_str(&api_key);
|
|
|
|
|
|
}
|
|
|
|
|
|
existing.prepopulated_from_env = false;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
*guard = SignInState::ApiKeyEntry(ApiKeyInputState {
|
|
|
|
|
|
value: api_key,
|
|
|
|
|
|
prepopulated_from_env: false,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
self.request_frame.schedule_frame();
|
2025-08-06 19:39:07 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn start_chatgpt_login(&mut self) {
|
2025-08-18 20:22:48 -07:00
|
|
|
|
// If we're already authenticated with ChatGPT, don't start a new login –
|
|
|
|
|
|
// just proceed to the success message flow.
|
|
|
|
|
|
if matches!(self.login_status, LoginStatus::AuthMode(AuthMode::ChatGPT)) {
|
2025-08-20 13:47:24 -07:00
|
|
|
|
*self.sign_in_state.write().unwrap() = SignInState::ChatGptSuccess;
|
|
|
|
|
|
self.request_frame.schedule_frame();
|
2025-08-18 20:22:48 -07:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-06 15:22:14 -07:00
|
|
|
|
self.error = None;
|
2025-10-20 08:50:54 -07:00
|
|
|
|
let opts = ServerOptions::new(
|
|
|
|
|
|
self.codex_home.clone(),
|
|
|
|
|
|
CLIENT_ID.to_string(),
|
|
|
|
|
|
self.forced_chatgpt_workspace_id.clone(),
|
2025-10-27 19:41:49 -07:00
|
|
|
|
self.cli_auth_credentials_store_mode,
|
2025-10-20 08:50:54 -07:00
|
|
|
|
);
|
2025-08-20 13:47:24 -07:00
|
|
|
|
match run_login_server(opts) {
|
2025-08-06 15:22:14 -07:00
|
|
|
|
Ok(child) => {
|
2025-08-20 13:47:24 -07:00
|
|
|
|
let sign_in_state = self.sign_in_state.clone();
|
|
|
|
|
|
let request_frame = self.request_frame.clone();
|
2025-08-22 13:10:11 -07:00
|
|
|
|
let auth_manager = self.auth_manager.clone();
|
2025-08-20 13:47:24 -07:00
|
|
|
|
tokio::spawn(async move {
|
|
|
|
|
|
let auth_url = child.auth_url.clone();
|
|
|
|
|
|
{
|
|
|
|
|
|
*sign_in_state.write().unwrap() =
|
|
|
|
|
|
SignInState::ChatGptContinueInBrowser(ContinueInBrowserState {
|
|
|
|
|
|
auth_url,
|
|
|
|
|
|
shutdown_flag: Some(child.cancel_handle()),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
request_frame.schedule_frame();
|
|
|
|
|
|
let r = child.block_until_done().await;
|
|
|
|
|
|
match r {
|
|
|
|
|
|
Ok(()) => {
|
2025-08-22 13:10:11 -07:00
|
|
|
|
// Force the auth manager to reload the new auth information.
|
|
|
|
|
|
auth_manager.reload();
|
|
|
|
|
|
|
2025-08-20 13:47:24 -07:00
|
|
|
|
*sign_in_state.write().unwrap() = SignInState::ChatGptSuccessMessage;
|
|
|
|
|
|
request_frame.schedule_frame();
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => {
|
|
|
|
|
|
*sign_in_state.write().unwrap() = SignInState::PickMode;
|
|
|
|
|
|
// self.error = Some(e.to_string());
|
|
|
|
|
|
request_frame.schedule_frame();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-08-18 17:23:40 -07:00
|
|
|
|
});
|
2025-08-06 15:22:14 -07:00
|
|
|
|
}
|
|
|
|
|
|
Err(e) => {
|
2025-08-20 13:47:24 -07:00
|
|
|
|
*self.sign_in_state.write().unwrap() = SignInState::PickMode;
|
2025-08-06 15:22:14 -07:00
|
|
|
|
self.error = Some(e.to_string());
|
2025-08-20 13:47:24 -07:00
|
|
|
|
self.request_frame.schedule_frame();
|
2025-08-06 15:22:14 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-08-18 17:23:40 -07:00
|
|
|
|
}
|
2025-08-06 15:22:14 -07:00
|
|
|
|
|
2025-08-06 19:39:07 -07:00
|
|
|
|
impl StepStateProvider for AuthModeWidget {
|
|
|
|
|
|
fn get_step_state(&self) -> StepState {
|
2025-08-20 13:47:24 -07:00
|
|
|
|
let sign_in_state = self.sign_in_state.read().unwrap();
|
|
|
|
|
|
match &*sign_in_state {
|
2025-08-06 19:39:07 -07:00
|
|
|
|
SignInState::PickMode
|
2025-09-11 09:16:34 -07:00
|
|
|
|
| SignInState::ApiKeyEntry(_)
|
2025-08-06 19:39:07 -07:00
|
|
|
|
| SignInState::ChatGptContinueInBrowser(_)
|
|
|
|
|
|
| SignInState::ChatGptSuccessMessage => StepState::InProgress,
|
2025-09-11 09:16:34 -07:00
|
|
|
|
SignInState::ChatGptSuccess | SignInState::ApiKeyConfigured => StepState::Complete,
|
2025-08-06 19:39:07 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-06 15:22:14 -07:00
|
|
|
|
impl WidgetRef for AuthModeWidget {
|
|
|
|
|
|
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
2025-08-20 13:47:24 -07:00
|
|
|
|
let sign_in_state = self.sign_in_state.read().unwrap();
|
|
|
|
|
|
match &*sign_in_state {
|
2025-08-06 15:22:14 -07:00
|
|
|
|
SignInState::PickMode => {
|
|
|
|
|
|
self.render_pick_mode(area, buf);
|
|
|
|
|
|
}
|
|
|
|
|
|
SignInState::ChatGptContinueInBrowser(_) => {
|
|
|
|
|
|
self.render_continue_in_browser(area, buf);
|
|
|
|
|
|
}
|
2025-08-06 19:39:07 -07:00
|
|
|
|
SignInState::ChatGptSuccessMessage => {
|
|
|
|
|
|
self.render_chatgpt_success_message(area, buf);
|
|
|
|
|
|
}
|
2025-08-06 15:22:14 -07:00
|
|
|
|
SignInState::ChatGptSuccess => {
|
|
|
|
|
|
self.render_chatgpt_success(area, buf);
|
|
|
|
|
|
}
|
2025-09-11 09:16:34 -07:00
|
|
|
|
SignInState::ApiKeyEntry(state) => {
|
|
|
|
|
|
self.render_api_key_entry(area, buf, state);
|
2025-08-06 19:39:07 -07:00
|
|
|
|
}
|
2025-09-11 09:16:34 -07:00
|
|
|
|
SignInState::ApiKeyConfigured => {
|
|
|
|
|
|
self.render_api_key_configured(area, buf);
|
2025-08-06 19:39:07 -07:00
|
|
|
|
}
|
2025-08-06 15:22:14 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-10-20 08:50:54 -07:00
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
|
mod tests {
|
|
|
|
|
|
use super::*;
|
|
|
|
|
|
use pretty_assertions::assert_eq;
|
|
|
|
|
|
use tempfile::TempDir;
|
|
|
|
|
|
|
2025-10-27 19:41:49 -07:00
|
|
|
|
use codex_core::auth::AuthCredentialsStoreMode;
|
|
|
|
|
|
|
2025-10-20 08:50:54 -07:00
|
|
|
|
fn widget_forced_chatgpt() -> (AuthModeWidget, TempDir) {
|
|
|
|
|
|
let codex_home = TempDir::new().unwrap();
|
|
|
|
|
|
let codex_home_path = codex_home.path().to_path_buf();
|
|
|
|
|
|
let widget = AuthModeWidget {
|
|
|
|
|
|
request_frame: FrameRequester::test_dummy(),
|
|
|
|
|
|
highlighted_mode: AuthMode::ChatGPT,
|
|
|
|
|
|
error: None,
|
|
|
|
|
|
sign_in_state: Arc::new(RwLock::new(SignInState::PickMode)),
|
|
|
|
|
|
codex_home: codex_home_path.clone(),
|
2025-10-27 19:41:49 -07:00
|
|
|
|
cli_auth_credentials_store_mode: AuthCredentialsStoreMode::File,
|
2025-10-20 08:50:54 -07:00
|
|
|
|
login_status: LoginStatus::NotAuthenticated,
|
2025-10-27 19:41:49 -07:00
|
|
|
|
auth_manager: AuthManager::shared(
|
|
|
|
|
|
codex_home_path,
|
|
|
|
|
|
false,
|
|
|
|
|
|
AuthCredentialsStoreMode::File,
|
|
|
|
|
|
),
|
2025-10-20 08:50:54 -07:00
|
|
|
|
forced_chatgpt_workspace_id: None,
|
|
|
|
|
|
forced_login_method: Some(ForcedLoginMethod::Chatgpt),
|
|
|
|
|
|
};
|
|
|
|
|
|
(widget, codex_home)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn api_key_flow_disabled_when_chatgpt_forced() {
|
|
|
|
|
|
let (mut widget, _tmp) = widget_forced_chatgpt();
|
|
|
|
|
|
|
|
|
|
|
|
widget.start_api_key_entry();
|
|
|
|
|
|
|
|
|
|
|
|
assert_eq!(widget.error.as_deref(), Some(API_KEY_DISABLED_MESSAGE));
|
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
|
&*widget.sign_in_state.read().unwrap(),
|
|
|
|
|
|
SignInState::PickMode
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn saving_api_key_is_blocked_when_chatgpt_forced() {
|
|
|
|
|
|
let (mut widget, _tmp) = widget_forced_chatgpt();
|
|
|
|
|
|
|
|
|
|
|
|
widget.save_api_key("sk-test".to_string());
|
|
|
|
|
|
|
|
|
|
|
|
assert_eq!(widget.error.as_deref(), Some(API_KEY_DISABLED_MESSAGE));
|
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
|
&*widget.sign_in_state.read().unwrap(),
|
|
|
|
|
|
SignInState::PickMode
|
|
|
|
|
|
));
|
|
|
|
|
|
assert_eq!(widget.login_status, LoginStatus::NotAuthenticated);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|