mirror of
https://github.com/openai/codex.git
synced 2026-10-27 18:48:37 +00:00
Show the selected ChatGPT workspace in /status (#57366)
## What changed - Return the selected workspace's display name as `workspaceName` in ChatGPT account responses, caching it alongside workspace routing and updating generated schemas and bindings. - Display a `ChatGPT workspace` row with the name and account ID, falling back to the ID when the name is unavailable or invalid. Refresh workspace details with account identity updates and hide the row for providers that do not use ChatGPT authentication. ## Testing Update account-response tests to cover workspace names and null values, add workspace details to the status snapshot, and verify that switching to a custom API-key provider hides the workspace row. GitOrigin-RevId: 4b30ab8a39e1fa7ad1ba7367c5295544be8f6001
This commit is contained in:
+7
@@ -6805,6 +6805,13 @@
|
||||
],
|
||||
"title": "ChatgptAccountType",
|
||||
"type": "string"
|
||||
},
|
||||
"workspaceName": {
|
||||
"description": "Display name of the selected ChatGPT workspace, when available.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
+7
@@ -40,6 +40,13 @@
|
||||
],
|
||||
"title": "ChatgptAccountType",
|
||||
"type": "string"
|
||||
},
|
||||
"workspaceName": {
|
||||
"description": "Display name of the selected ChatGPT workspace, when available.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
@@ -36,6 +36,13 @@
|
||||
],
|
||||
"title": "ChatgptAccountType",
|
||||
"type": "string"
|
||||
},
|
||||
"workspaceName": {
|
||||
"description": "Display name of the selected ChatGPT workspace, when available.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -3,4 +3,8 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { PlanType } from "../PlanType";
|
||||
|
||||
export type Account = { "type": "apiKey", } | { "type": "chatgpt", email: string | null, planType: PlanType, } | { "type": "amazonBedrock", usesCodexManagedCredentials: boolean, } | { "type": "gcp", };
|
||||
export type Account = { "type": "apiKey", } | { "type": "chatgpt", email: string | null, planType: PlanType,
|
||||
/**
|
||||
* Display name of the selected ChatGPT workspace, when available.
|
||||
*/
|
||||
workspaceName: string | null, } | { "type": "amazonBedrock", usesCodexManagedCredentials: boolean, } | { "type": "gcp", };
|
||||
|
||||
@@ -3654,12 +3654,14 @@ mod tests {
|
||||
let chatgpt = v2::Account::Chatgpt {
|
||||
email: Some("user@example.com".to_string()),
|
||||
plan_type: PlanType::Plus,
|
||||
workspace_name: None,
|
||||
};
|
||||
assert_eq!(
|
||||
json!({
|
||||
"type": "chatgpt",
|
||||
"email": "user@example.com",
|
||||
"planType": "plus",
|
||||
"workspaceName": null,
|
||||
}),
|
||||
serde_json::to_value(&chatgpt)?,
|
||||
);
|
||||
@@ -3667,12 +3669,14 @@ mod tests {
|
||||
let chatgpt_without_email = v2::Account::Chatgpt {
|
||||
email: None,
|
||||
plan_type: PlanType::Pro,
|
||||
workspace_name: None,
|
||||
};
|
||||
assert_eq!(
|
||||
json!({
|
||||
"type": "chatgpt",
|
||||
"email": null,
|
||||
"planType": "pro",
|
||||
"workspaceName": null,
|
||||
}),
|
||||
serde_json::to_value(&chatgpt_without_email)?,
|
||||
);
|
||||
|
||||
@@ -33,6 +33,8 @@ pub enum Account {
|
||||
)]
|
||||
email: Option<String>,
|
||||
plan_type: PlanType,
|
||||
/// Display name of the selected ChatGPT workspace, when available.
|
||||
workspace_name: Option<String>,
|
||||
},
|
||||
|
||||
#[serde(rename = "amazonBedrock", rename_all = "camelCase")]
|
||||
@@ -52,7 +54,11 @@ impl From<ProviderAccount> for Account {
|
||||
fn from(account: ProviderAccount) -> Self {
|
||||
match account {
|
||||
ProviderAccount::ApiKey => Self::ApiKey {},
|
||||
ProviderAccount::Chatgpt { email, plan_type } => Self::Chatgpt { email, plan_type },
|
||||
ProviderAccount::Chatgpt { email, plan_type } => Self::Chatgpt {
|
||||
email,
|
||||
plan_type,
|
||||
workspace_name: None,
|
||||
},
|
||||
ProviderAccount::AmazonBedrock {
|
||||
uses_codex_managed_credentials,
|
||||
} => Self::AmazonBedrock {
|
||||
|
||||
@@ -37,17 +37,20 @@ pub(super) type WorkspaceRoutingFetches = HashMap<WorkspaceRoutingKey, Weak<Work
|
||||
|
||||
pub(super) struct WorkspaceRoutingFetch {
|
||||
discovery: Semaphore,
|
||||
routing: Mutex<Option<WorkspaceRouting>>,
|
||||
routing: Mutex<Option<CachedWorkspaceRouting>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(super) struct CachedWorkspaceRouting {
|
||||
key: WorkspaceRoutingKey,
|
||||
routing: WorkspaceRouting,
|
||||
name: Option<String>,
|
||||
}
|
||||
|
||||
pub(super) struct AccountRead {
|
||||
account_state: ProviderAccountState,
|
||||
pub(super) workspace_routing: Option<WorkspaceRouting>,
|
||||
workspace_name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, thiserror::Error)]
|
||||
@@ -264,9 +267,13 @@ impl AccountRequestProcessor {
|
||||
AccountReadError::InvalidAccount(error) => invalid_request(error.to_string()),
|
||||
AccountReadError::Routing(error) => internal_error(error.to_string()),
|
||||
})?;
|
||||
let mut account = read.account_state.account.map(Account::from);
|
||||
if let Some(Account::Chatgpt { workspace_name, .. }) = &mut account {
|
||||
*workspace_name = read.workspace_name;
|
||||
}
|
||||
Ok(Some(
|
||||
GetAccountResponse {
|
||||
account: read.account_state.account.map(Account::from),
|
||||
account,
|
||||
requires_openai_auth: read.account_state.requires_openai_auth,
|
||||
workspace_routing: read.workspace_routing.map(|routing| {
|
||||
codex_app_server_protocol::WorkspaceRouting {
|
||||
@@ -387,7 +394,7 @@ impl AccountRequestProcessor {
|
||||
.await
|
||||
.as_ref()
|
||||
.filter(|cached| cached.key == key)
|
||||
.map(|cached| cached.routing.clone()),
|
||||
.cloned(),
|
||||
};
|
||||
let routing = if let Some(cached) = cached {
|
||||
cached
|
||||
@@ -450,7 +457,7 @@ impl AccountRequestProcessor {
|
||||
.accounts
|
||||
.into_iter()
|
||||
.filter(|account| account.id == account_id);
|
||||
let entry = accounts
|
||||
let mut entry = accounts
|
||||
.next()
|
||||
.ok_or(WorkspaceRoutingError::MissingWorkspace)?;
|
||||
if accounts.next().is_some() {
|
||||
@@ -471,6 +478,7 @@ impl AccountRequestProcessor {
|
||||
{
|
||||
return Err(WorkspaceRoutingError::ConfigurationChanged.into());
|
||||
}
|
||||
let name = entry.name.take();
|
||||
let routing = resolve_routing(
|
||||
entry,
|
||||
required_chatgpt_base_url.as_deref(),
|
||||
@@ -481,15 +489,17 @@ impl AccountRequestProcessor {
|
||||
{
|
||||
return Err(WorkspaceRoutingError::AccountChanged.into());
|
||||
}
|
||||
let routing = CachedWorkspaceRouting {
|
||||
key: WorkspaceRoutingKey {
|
||||
auth_generation: discovery_generation,
|
||||
..key
|
||||
},
|
||||
routing,
|
||||
name,
|
||||
};
|
||||
// Same-owner refresh can complete a newer discovery first.
|
||||
if current_auth_changes.borrow().generation == discovery_generation {
|
||||
*cached = Some(CachedWorkspaceRouting {
|
||||
key: WorkspaceRoutingKey {
|
||||
auth_generation: discovery_generation,
|
||||
..key
|
||||
},
|
||||
routing: routing.clone(),
|
||||
});
|
||||
*cached = Some(routing.clone());
|
||||
}
|
||||
routing
|
||||
};
|
||||
@@ -509,9 +519,9 @@ impl AccountRequestProcessor {
|
||||
let session_bootstrap_origin = Url::parse(&request.chatgpt_base_url)
|
||||
.map_err(|_| WorkspaceRoutingError::InvalidSessionBootstrapUrl)?
|
||||
.origin();
|
||||
let selected_backend = workspace_routing
|
||||
.as_ref()
|
||||
.is_some_and(|routing| routing.backend_origin == origin.ascii_serialization());
|
||||
let selected_backend = workspace_routing.as_ref().is_some_and(|routing| {
|
||||
routing.routing.backend_origin == origin.ascii_serialization()
|
||||
});
|
||||
let workspace_bound = request.previously_routed
|
||||
|| request.provider_base_url == codex_model_provider::CHATGPT_CODEX_BASE_URL
|
||||
|| origin == session_bootstrap_origin
|
||||
@@ -522,9 +532,13 @@ impl AccountRequestProcessor {
|
||||
return Err(WorkspaceRoutingError::SessionBackendChanged.into());
|
||||
}
|
||||
}
|
||||
let (workspace_routing, workspace_name) = workspace_routing
|
||||
.map(|workspace| (Some(workspace.routing), workspace.name))
|
||||
.unwrap_or_default();
|
||||
Ok(AccountRead {
|
||||
account_state,
|
||||
workspace_routing,
|
||||
workspace_name,
|
||||
})
|
||||
});
|
||||
let result = tokio::select! {
|
||||
|
||||
@@ -229,6 +229,7 @@ async fn personal_access_token_without_email_supports_auth_status_and_account_re
|
||||
account: Some(Account::Chatgpt {
|
||||
email: None,
|
||||
plan_type: AccountPlanType::EnterpriseCbpAutomation,
|
||||
workspace_name: None,
|
||||
}),
|
||||
requires_openai_auth: true,
|
||||
}
|
||||
|
||||
@@ -569,6 +569,7 @@ async fn set_auth_token_updates_account_and_notifies() -> Result<()> {
|
||||
account: Some(Account::Chatgpt {
|
||||
email: Some("embedded@example.com".to_string()),
|
||||
plan_type: AccountPlanType::Pro,
|
||||
workspace_name: None,
|
||||
}),
|
||||
requires_openai_auth: true,
|
||||
}
|
||||
@@ -647,6 +648,7 @@ async fn account_read_refresh_token_is_noop_in_external_mode() -> Result<()> {
|
||||
account: Some(Account::Chatgpt {
|
||||
email: Some("embedded@example.com".to_string()),
|
||||
plan_type: AccountPlanType::Pro,
|
||||
workspace_name: None,
|
||||
}),
|
||||
requires_openai_auth: true,
|
||||
}
|
||||
@@ -2444,6 +2446,7 @@ async fn login_survives_same_owner_token_refresh(
|
||||
account: Some(Account::Chatgpt {
|
||||
email: Some("device@example.com".into()),
|
||||
plan_type: AccountPlanType::Enterprise,
|
||||
workspace_name: None,
|
||||
}),
|
||||
requires_openai_auth: true,
|
||||
workspace_routing: expected_workspace_routing(WORKSPACE_ID_DEVICE),
|
||||
@@ -3485,6 +3488,7 @@ async fn get_account_with_chatgpt() -> Result<()> {
|
||||
account: Some(Account::Chatgpt {
|
||||
email: Some("user@example.com".to_string()),
|
||||
plan_type: AccountPlanType::Pro,
|
||||
workspace_name: None,
|
||||
}),
|
||||
requires_openai_auth: true,
|
||||
};
|
||||
@@ -3542,6 +3546,7 @@ async fn get_account_with_chatgpt_plan_variants_returns_plan_type(
|
||||
account: Some(Account::Chatgpt {
|
||||
email: Some("user@example.com".to_string()),
|
||||
plan_type: expected_plan,
|
||||
workspace_name: None,
|
||||
}),
|
||||
requires_openai_auth: true,
|
||||
}
|
||||
@@ -3589,6 +3594,7 @@ async fn get_account_with_chatgpt_without_email() -> Result<()> {
|
||||
account: Some(Account::Chatgpt {
|
||||
email: None,
|
||||
plan_type: AccountPlanType::Pro,
|
||||
workspace_name: None,
|
||||
}),
|
||||
requires_openai_auth: true,
|
||||
}
|
||||
@@ -3714,6 +3720,7 @@ async fn get_account_with_chatgpt_missing_plan_claim_returns_unknown() -> Result
|
||||
account: Some(Account::Chatgpt {
|
||||
email: Some("user@example.com".to_string()),
|
||||
plan_type: AccountPlanType::Unknown,
|
||||
workspace_name: None,
|
||||
}),
|
||||
requires_openai_auth: true,
|
||||
};
|
||||
|
||||
@@ -80,7 +80,7 @@ async fn gcp_account_tracks_provider_changes_with_saved_chatgpt_credentials() ->
|
||||
),
|
||||
(
|
||||
"openai",
|
||||
json!({"account": {"type": "chatgpt", "email": "user@example.com", "planType": "pro"}, "requiresOpenaiAuth": true, "workspaceRouting": null}),
|
||||
json!({"account": {"type": "chatgpt", "email": "user@example.com", "planType": "pro", "workspaceName": null}, "requiresOpenaiAuth": true, "workspaceRouting": null}),
|
||||
),
|
||||
(
|
||||
"gcp",
|
||||
|
||||
@@ -108,7 +108,7 @@ async fn saved_workspace_is_discovered_once_and_not_the_default_account(
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||
"accounts": [
|
||||
{"id": "other", "workspace_backend_origin": "https://other.example", "account_routing_override": "us"},
|
||||
{"id": "selected", "workspace_backend_origin": "https://gov.chatgpt.com", "account_routing_override": "NO_CONSTRAINT"}
|
||||
{"id": "selected", "name": "Selected workspace", "workspace_backend_origin": "https://gov.chatgpt.com", "account_routing_override": "NO_CONSTRAINT"}
|
||||
], "default_account_id": "other"
|
||||
})).set_delay(Duration::from_secs(accounts_delay_secs)))
|
||||
.expect(if accounts_delay_secs > 5 { 2 } else { 1 })
|
||||
@@ -158,7 +158,7 @@ async fn saved_workspace_is_discovered_once_and_not_the_default_account(
|
||||
Some(json!({"authMode": "chatgpt", "planType": plan_type}))
|
||||
);
|
||||
let expected = json!({
|
||||
"account": {"type": "chatgpt", "email": "user@example.com", "planType": plan_type},
|
||||
"account": {"type": "chatgpt", "email": "user@example.com", "planType": plan_type, "workspaceName": "Selected workspace"},
|
||||
"requiresOpenaiAuth": true, "workspaceRouting": routing("selected", "https://gov.chatgpt.com", "NO_CONSTRAINT")
|
||||
});
|
||||
assert_eq!(read(&mut server).await?, expected);
|
||||
@@ -185,7 +185,7 @@ async fn saved_chatgpt_login_without_selected_workspace_preserves_account() -> R
|
||||
let mut server = start(&home).await?;
|
||||
assert_eq!(
|
||||
read(&mut server).await?,
|
||||
json!({"account": {"type": "chatgpt", "email": null, "planType": "pro"},
|
||||
json!({"account": {"type": "chatgpt", "email": null, "planType": "pro", "workspaceName": null},
|
||||
"requiresOpenaiAuth": true, "workspaceRouting": null})
|
||||
);
|
||||
backend.verify().await;
|
||||
@@ -245,7 +245,7 @@ async fn stable_clients_do_not_treat_failed_workspace_discovery_as_unrestricted(
|
||||
.await;
|
||||
assert_eq!(
|
||||
read(&mut server).await?,
|
||||
json!({"account": {"type": "chatgpt", "email": null, "planType": "pro"},
|
||||
json!({"account": {"type": "chatgpt", "email": null, "planType": "pro", "workspaceName": null},
|
||||
"requiresOpenaiAuth": true, "workspaceRouting": routing("selected", "https://chatgpt.com", "NO_CONSTRAINT")})
|
||||
);
|
||||
Ok(())
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Refreshes the email omitted by account-update notifications without blocking terminal input.
|
||||
//! Refreshes identity details omitted by account-update notifications without blocking terminal input.
|
||||
//! Results carry a request ID so account changes and reconnects reject obsolete identities.
|
||||
|
||||
use super::App;
|
||||
@@ -31,8 +31,20 @@ impl App {
|
||||
.await;
|
||||
match result {
|
||||
Ok(Ok(response)) => {
|
||||
if let Some(Account::Chatgpt { email, .. }) = response.account {
|
||||
app_event_tx.send(AppEvent::AccountEmailLoaded { request_id, email });
|
||||
if let Some(Account::Chatgpt {
|
||||
email,
|
||||
workspace_name,
|
||||
..
|
||||
}) = response.account
|
||||
{
|
||||
app_event_tx.send(AppEvent::AccountEmailLoaded {
|
||||
request_id,
|
||||
email,
|
||||
workspace: crate::app_server_session::workspace_display(
|
||||
workspace_name,
|
||||
response.workspace_routing,
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(Err(_)) | Err(_) => {
|
||||
|
||||
@@ -1333,10 +1333,10 @@ impl App {
|
||||
elicitation_target: None,
|
||||
});
|
||||
}
|
||||
AppEvent::AccountEmailLoaded { request_id, email } => {
|
||||
AppEvent::AccountEmailLoaded { request_id, email, workspace } => {
|
||||
if self.account_email_request_id == Some(request_id) {
|
||||
self.account_email_request_id = None;
|
||||
self.chat_widget.on_account_email_loaded(email);
|
||||
self.chat_widget.on_account_email_loaded(email, workspace);
|
||||
}
|
||||
}
|
||||
AppEvent::SecuritySetupLoaded { request_id, identity, notice } => {
|
||||
|
||||
@@ -172,6 +172,7 @@ async fn security_setup_fetch_with_default_features_uses_authenticated_codex_end
|
||||
AppEvent::AccountEmailLoaded {
|
||||
request_id: uuid::Uuid::new_v4(),
|
||||
email: Some("stale@example.com".into()),
|
||||
workspace: Some("stale-account".into()),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
@@ -180,6 +181,7 @@ async fn security_setup_fetch_with_default_features_uses_authenticated_codex_end
|
||||
Some(&StatusAccountDisplay::ChatGpt {
|
||||
email: Some("user@example.com".into()),
|
||||
plan: None,
|
||||
workspace: Some("account".to_string()),
|
||||
})
|
||||
);
|
||||
app.handle_event(&mut tui, &mut server, event).await?;
|
||||
|
||||
@@ -298,6 +298,7 @@ pub(crate) enum AppEvent {
|
||||
AccountEmailLoaded {
|
||||
request_id: uuid::Uuid,
|
||||
email: Option<String>,
|
||||
workspace: Option<String>,
|
||||
},
|
||||
SecuritySetupLoaded {
|
||||
request_id: uuid::Uuid,
|
||||
|
||||
@@ -845,7 +845,11 @@ impl AppServerSession {
|
||||
FeedbackAudience::External,
|
||||
false,
|
||||
),
|
||||
Some(Account::Chatgpt { email, plan_type }) => {
|
||||
Some(Account::Chatgpt {
|
||||
email,
|
||||
plan_type,
|
||||
workspace_name,
|
||||
}) => {
|
||||
let feedback_audience = if email
|
||||
.as_deref()
|
||||
.is_some_and(|email| email.ends_with("@openai.com"))
|
||||
@@ -860,6 +864,7 @@ impl AppServerSession {
|
||||
Some(StatusAccountDisplay::ChatGpt {
|
||||
email,
|
||||
plan: Some(plan_type_display_name(plan_type)),
|
||||
workspace: workspace_display(workspace_name, account.workspace_routing),
|
||||
}),
|
||||
Some(plan_type),
|
||||
feedback_audience,
|
||||
@@ -1930,6 +1935,20 @@ pub(crate) async fn start_thread_with_request_handle(
|
||||
Ok(started)
|
||||
}
|
||||
|
||||
pub(crate) fn workspace_display(
|
||||
name: Option<String>,
|
||||
routing: Option<codex_app_server_protocol::WorkspaceRouting>,
|
||||
) -> Option<String> {
|
||||
let name = name
|
||||
.map(|name| name.trim().to_owned())
|
||||
.filter(|name| !name.is_empty() && !name.chars().any(char::is_control));
|
||||
let id = routing.map(|routing| routing.chatgpt_account_id);
|
||||
match (name, id) {
|
||||
(Some(name), Some(id)) => Some(format!("{name} ({id})")),
|
||||
(name, id) => name.or(id),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn status_account_display_from_auth_mode(
|
||||
auth_mode: Option<AuthMode>,
|
||||
plan_type: Option<codex_protocol::account::PlanType>,
|
||||
@@ -1942,6 +1961,7 @@ pub(crate) fn status_account_display_from_auth_mode(
|
||||
| Some(AuthMode::PersonalAccessToken) => Some(StatusAccountDisplay::ChatGpt {
|
||||
email: None,
|
||||
plan: plan_type.map(plan_type_display_name),
|
||||
workspace: None,
|
||||
}),
|
||||
Some(AuthMode::Headers)
|
||||
| Some(AuthMode::BedrockApiKey)
|
||||
@@ -2793,6 +2813,7 @@ mod tests {
|
||||
account: Some(Account::Chatgpt {
|
||||
email: Some("teammate@openai.com".to_string()),
|
||||
plan_type: codex_protocol::account::PlanType::Plus,
|
||||
workspace_name: None,
|
||||
}),
|
||||
requires_openai_auth: true,
|
||||
};
|
||||
@@ -2907,6 +2928,7 @@ mod tests {
|
||||
account: Some(Account::Chatgpt {
|
||||
email: Some("teammate@openai.com".to_string()),
|
||||
plan_type: codex_protocol::account::PlanType::Plus,
|
||||
workspace_name: None,
|
||||
}),
|
||||
requires_openai_auth: true,
|
||||
workspace_routing: None,
|
||||
@@ -4811,6 +4833,7 @@ mod tests {
|
||||
Some(StatusAccountDisplay::ChatGpt {
|
||||
email: None,
|
||||
plan: Some(ref plan),
|
||||
..
|
||||
}) if plan == "Enterprise"
|
||||
));
|
||||
|
||||
@@ -4823,6 +4846,7 @@ mod tests {
|
||||
Some(StatusAccountDisplay::ChatGpt {
|
||||
email: None,
|
||||
plan: Some(ref plan),
|
||||
..
|
||||
}) if plan == "Business"
|
||||
));
|
||||
|
||||
@@ -4835,6 +4859,7 @@ mod tests {
|
||||
Some(StatusAccountDisplay::ChatGpt {
|
||||
email: None,
|
||||
plan: Some(ref plan),
|
||||
..
|
||||
}) if plan == "Business Premium"
|
||||
));
|
||||
}
|
||||
|
||||
@@ -213,10 +213,17 @@ impl ChatWidget {
|
||||
self.model_catalog.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn on_account_email_loaded(&mut self, current_email: Option<String>) {
|
||||
if let Some(StatusAccountDisplay::ChatGpt { email, .. }) = &mut self.status_account_display
|
||||
pub(crate) fn on_account_email_loaded(
|
||||
&mut self,
|
||||
current_email: Option<String>,
|
||||
current_workspace: Option<String>,
|
||||
) {
|
||||
if let Some(StatusAccountDisplay::ChatGpt {
|
||||
email, workspace, ..
|
||||
}) = &mut self.status_account_display
|
||||
{
|
||||
*email = current_email;
|
||||
*workspace = current_workspace;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -123,10 +123,21 @@ async fn session_and_settings_sync_server_provider_id() {
|
||||
let first_id = ThreadId::new();
|
||||
let mut first = configured_thread_session(first_id);
|
||||
first.model_provider_id = "server-ollama".to_string();
|
||||
chat.status_account_display = Some(StatusAccountDisplay::ChatGpt {
|
||||
email: None,
|
||||
plan: None,
|
||||
workspace: Some("account-123".to_string()),
|
||||
});
|
||||
let mut provider = chat.config.model_provider.clone();
|
||||
provider.env_key = Some("CUSTOM_API_KEY".to_string());
|
||||
chat.config
|
||||
.model_providers
|
||||
.insert(first.model_provider_id.clone(), provider);
|
||||
chat.handle_thread_session(first.clone());
|
||||
let displayed = status(&mut chat);
|
||||
assert!(displayed.contains("Model provider:") && displayed.contains("server-ollama"));
|
||||
assert!(!displayed.contains("local-provider.example"));
|
||||
assert!(!displayed.contains("ChatGPT workspace:"));
|
||||
|
||||
let mut other = configured_thread_session(ThreadId::new());
|
||||
other.model_provider_id = "server-bedrock".to_string();
|
||||
|
||||
@@ -3917,6 +3917,7 @@ async fn account_update_discards_stale_workspace_headline_results() {
|
||||
Some(StatusAccountDisplay::ChatGpt {
|
||||
email: Some("first@example.com".to_string()),
|
||||
plan: None,
|
||||
workspace: None,
|
||||
}),
|
||||
/*plan_type*/ None,
|
||||
/*has_chatgpt_account*/ true,
|
||||
@@ -3928,6 +3929,7 @@ async fn account_update_discards_stale_workspace_headline_results() {
|
||||
Some(StatusAccountDisplay::ChatGpt {
|
||||
email: Some("second@example.com".to_string()),
|
||||
plan: None,
|
||||
workspace: None,
|
||||
}),
|
||||
/*plan_type*/ None,
|
||||
/*has_chatgpt_account*/ true,
|
||||
|
||||
@@ -3,6 +3,7 @@ pub(crate) enum StatusAccountDisplay {
|
||||
ChatGpt {
|
||||
email: Option<String>,
|
||||
plan: Option<String>,
|
||||
workspace: Option<String>,
|
||||
},
|
||||
ApiKey,
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ use crate::version::CODEX_CLI_VERSION;
|
||||
use chrono::DateTime;
|
||||
use chrono::Local;
|
||||
use codex_app_server_protocol::AskForApproval;
|
||||
use codex_model_provider::provider_uses_first_party_auth_path;
|
||||
use codex_model_provider_info::OPENAI_PROVIDER_ID;
|
||||
use codex_model_provider_info::WireApi;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::account::PlanType;
|
||||
@@ -351,7 +353,27 @@ impl StatusHistoryCell {
|
||||
workspace_root_suffix.as_deref(),
|
||||
);
|
||||
let show_chatgpt_usage_link = requires_openai_auth;
|
||||
let account = compose_account_display(account_display);
|
||||
let mut account = compose_account_display(account_display);
|
||||
let workspace_matches_provider = if remote_connection.is_some() {
|
||||
// A connected server owns the provider definitions; only the built-in ID is certain.
|
||||
model_provider
|
||||
.as_deref()
|
||||
.is_none_or(|provider| provider == OPENAI_PROVIDER_ID)
|
||||
} else {
|
||||
config
|
||||
.model_providers
|
||||
.get(
|
||||
model_provider
|
||||
.as_deref()
|
||||
.unwrap_or(&config.model_provider_id),
|
||||
)
|
||||
.is_some_and(provider_uses_first_party_auth_path)
|
||||
};
|
||||
if !workspace_matches_provider
|
||||
&& let Some(StatusAccountDisplay::ChatGpt { workspace, .. }) = &mut account
|
||||
{
|
||||
*workspace = None;
|
||||
}
|
||||
let session_id = session_id.as_ref().map(std::string::ToString::to_string);
|
||||
let forked_from = forked_from.map(|id| id.to_string());
|
||||
let default_usage = TokenUsage::default();
|
||||
@@ -740,7 +762,7 @@ impl StatusHistoryCell {
|
||||
let mut lines = Vec::new();
|
||||
|
||||
let account_value = self.account.as_ref().map(|account| match account {
|
||||
StatusAccountDisplay::ChatGpt { email, plan } => match (email, plan) {
|
||||
StatusAccountDisplay::ChatGpt { email, plan, .. } => match (email, plan) {
|
||||
(Some(email), Some(plan)) => format!("{email} ({plan})"),
|
||||
(Some(email), None) => email.clone(),
|
||||
(None, Some(plan)) => plan.clone(),
|
||||
@@ -751,6 +773,11 @@ impl StatusHistoryCell {
|
||||
}
|
||||
});
|
||||
|
||||
let workspace = match &self.account {
|
||||
Some(StatusAccountDisplay::ChatGpt { workspace, .. }) => workspace.as_deref(),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let mut labels: Vec<String> = vec!["Model", "Directory", "Permissions", "Agents.md"]
|
||||
.into_iter()
|
||||
.map(str::to_string)
|
||||
@@ -775,6 +802,9 @@ impl StatusHistoryCell {
|
||||
if account_value.is_some() {
|
||||
push_label(&mut labels, &mut seen, "Account");
|
||||
}
|
||||
if workspace.is_some() {
|
||||
push_label(&mut labels, &mut seen, "ChatGPT workspace");
|
||||
}
|
||||
if thread_name.is_some() {
|
||||
push_label(&mut labels, &mut seen, "Thread name");
|
||||
}
|
||||
@@ -837,6 +867,9 @@ impl StatusHistoryCell {
|
||||
if let Some(account_value) = account_value {
|
||||
lines.push(formatter.line("Account", vec![Span::from(account_value)]));
|
||||
}
|
||||
if let Some(workspace) = workspace {
|
||||
lines.push(formatter.line("ChatGPT workspace", vec![Span::from(workspace.to_owned())]));
|
||||
}
|
||||
|
||||
if let Some(thread_name) = thread_name {
|
||||
lines.push(formatter.line("Thread name", vec![Span::from(thread_name.to_string())]));
|
||||
|
||||
+7
-6
@@ -9,10 +9,11 @@ expression: sanitized
|
||||
Visit https://chatgpt.com/settings/usage for up-to-date
|
||||
information on rate limits and credits
|
||||
|
||||
Model: gpt-5.1-codex-max (reasoning none, summaries auto)
|
||||
Directory: [[workspace]]
|
||||
Permissions: Custom (workspace with network access, Ask for approval)
|
||||
Agents.md: <none>
|
||||
Account: Enterprise (Automation)
|
||||
Model: gpt-5.1-codex-max (reasoning none, summaries auto)
|
||||
Directory: [[workspace]]
|
||||
Permissions: Custom (workspace with network access, Ask for approval)
|
||||
Agents.md: <none>
|
||||
Account: Enterprise (Automation)
|
||||
ChatGPT workspace: Example workspace (account-123)
|
||||
|
||||
Limits: data not available yet
|
||||
Limits: data not available yet
|
||||
|
||||
@@ -358,6 +358,19 @@ async fn status_snapshot_shows_chatgpt_plan_without_email() {
|
||||
..LoaderOverrides::without_managed_config_for_tests()
|
||||
};
|
||||
let server = wiremock::MockServer::start().await;
|
||||
wiremock::Mock::given(wiremock::matchers::path("/api/codex/accounts/check"))
|
||||
.respond_with(
|
||||
wiremock::ResponseTemplate::new(200).set_body_json(serde_json::json!({
|
||||
"accounts": [{
|
||||
"id": "account-123",
|
||||
"name": "Example workspace",
|
||||
"workspace_backend_origin": "https://chatgpt.com",
|
||||
"account_routing_override": "NO_CONSTRAINT"
|
||||
}]
|
||||
})),
|
||||
)
|
||||
.mount(&server)
|
||||
.await;
|
||||
wiremock::Mock::given(wiremock::matchers::path("/api/codex/config/bundle"))
|
||||
.respond_with(wiremock::ResponseTemplate::new(/*s*/ 200).set_body_string("{}"))
|
||||
.mount(&server)
|
||||
@@ -368,6 +381,7 @@ async fn status_snapshot_shows_chatgpt_plan_without_email() {
|
||||
)
|
||||
.expect("configure local ChatGPT fixture");
|
||||
let mut config = test_config(&temp_home).await;
|
||||
config.chatgpt_base_url = server.uri();
|
||||
config.model = Some("gpt-5.1-codex-max".to_string());
|
||||
config.model_provider_id = "openai".to_string();
|
||||
config.cli_auth_credentials_store_mode = AuthCredentialsStoreMode::File;
|
||||
@@ -375,7 +389,9 @@ async fn status_snapshot_shows_chatgpt_plan_without_email() {
|
||||
|
||||
write_chatgpt_auth(
|
||||
temp_home.path(),
|
||||
ChatGptAuthFixture::new("access-chatgpt").plan_type("enterprise_cbp_automation"),
|
||||
ChatGptAuthFixture::new("access-chatgpt")
|
||||
.plan_type("enterprise_cbp_automation")
|
||||
.account_id("account-123"),
|
||||
AuthCredentialsStoreMode::File,
|
||||
)
|
||||
.expect("write email-less ChatGPT auth");
|
||||
@@ -406,6 +422,7 @@ async fn status_snapshot_shows_chatgpt_plan_without_email() {
|
||||
StatusAccountDisplay::ChatGpt {
|
||||
email: None,
|
||||
plan: Some("Enterprise (Automation)".to_string()),
|
||||
workspace: Some("Example workspace (account-123)".to_string()),
|
||||
}
|
||||
);
|
||||
let usage = TokenUsage::default();
|
||||
@@ -1618,6 +1635,7 @@ async fn status_snapshot_wraps_halfwidth_kana_in_narrow_terminal() {
|
||||
let account = StatusAccountDisplay::ChatGpt {
|
||||
email: Some("ガパガパガパ@example.com".to_string()),
|
||||
plan: Some("ガパ plan".to_string()),
|
||||
workspace: None,
|
||||
};
|
||||
let usage = TokenUsage::default();
|
||||
let now = chrono::Local
|
||||
@@ -1928,6 +1946,7 @@ async fn status_snapshot_includes_credits_and_limits() {
|
||||
let account_display = Some(StatusAccountDisplay::ChatGpt {
|
||||
email: Some("user@example.com".into()),
|
||||
plan: Some("Pro 200".into()),
|
||||
workspace: None,
|
||||
});
|
||||
let usage = TokenUsage {
|
||||
input_tokens: 1_500,
|
||||
|
||||
@@ -6866,6 +6866,13 @@ class ChatgptAccount(BaseModel):
|
||||
email: str | None
|
||||
plan_type: Annotated[PlanType, Field(alias="planType")]
|
||||
type: Annotated[Literal["chatgpt"], Field(title="ChatgptAccountType")]
|
||||
workspace_name: Annotated[
|
||||
str | None,
|
||||
Field(
|
||||
alias="workspaceName",
|
||||
description="Display name of the selected ChatGPT workspace, when available.",
|
||||
),
|
||||
] = None
|
||||
|
||||
|
||||
class Account(RootModel[ApiKeyAccount | ChatgptAccount | AmazonBedrockAccount | GcpAccount]):
|
||||
|
||||
Reference in New Issue
Block a user