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:
Eric Traut
2026-10-27 05:22:23 +00:00
committed by copyberry
parent 6c0db39b94
commit 155dde7ff6
26 changed files with 216 additions and 38 deletions
@@ -6805,6 +6805,13 @@
],
"title": "ChatgptAccountType",
"type": "string"
},
"workspaceName": {
"description": "Display name of the selected ChatGPT workspace, when available.",
"type": [
"string",
"null"
]
}
},
"required": [
@@ -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": [
@@ -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! {
+1
View File
@@ -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(())
+15 -3
View File
@@ -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(_) => {
+2 -2
View File
@@ -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?;
+1
View File
@@ -298,6 +298,7 @@ pub(crate) enum AppEvent {
AccountEmailLoaded {
request_id: uuid::Uuid,
email: Option<String>,
workspace: Option<String>,
},
SecuritySetupLoaded {
request_id: uuid::Uuid,
+26 -1
View File
@@ -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"
));
}
+9 -2
View File
@@ -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,
+1
View File
@@ -3,6 +3,7 @@ pub(crate) enum StatusAccountDisplay {
ChatGpt {
email: Option<String>,
plan: Option<String>,
workspace: Option<String>,
},
ApiKey,
}
+35 -2
View File
@@ -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())]));
@@ -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
+20 -1
View File
@@ -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]):