mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:23:33 +00:00
feat(tui/accounts): prioritize chatgpt accounts in login/limits
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
use codex_core::auth_accounts::StoredAccount;
|
||||
use codex_protocol::mcp_protocol::AuthMode;
|
||||
|
||||
const KEY_SUFFIX_LEN: usize = 8;
|
||||
|
||||
/// Returns a user-facing label for the given account.
|
||||
/// Prefers stored labels when present, otherwise formats a sensible default.
|
||||
pub(crate) fn account_display_label(account: &StoredAccount) -> String {
|
||||
if let Some(label) = account
|
||||
.label
|
||||
.as_ref()
|
||||
.and_then(|value| (!value.trim().is_empty()).then(|| value.trim().to_string()))
|
||||
{
|
||||
return label;
|
||||
}
|
||||
|
||||
match account.mode {
|
||||
AuthMode::ChatGPT => account
|
||||
.tokens
|
||||
.as_ref()
|
||||
.and_then(|tokens| tokens.id_token.email.clone())
|
||||
.map(|email| format!("ChatGPT ({email})"))
|
||||
.unwrap_or_else(|| "ChatGPT".to_string()),
|
||||
AuthMode::ApiKey => account
|
||||
.openai_api_key
|
||||
.as_ref()
|
||||
.map(|key| format!("API key (…{})", key_suffix(key)))
|
||||
.unwrap_or_else(|| "API key".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the fixed-length suffix used when displaying sensitive tokens.
|
||||
pub(crate) fn key_suffix(text: &str) -> String {
|
||||
let tail: Vec<char> = text.chars().rev().take(KEY_SUFFIX_LEN).collect();
|
||||
tail.into_iter().rev().collect()
|
||||
}
|
||||
|
||||
/// Returns an ordering priority for accounts. ChatGPT accounts should appear first.
|
||||
pub(crate) fn account_mode_priority(mode: AuthMode) -> u8 {
|
||||
match mode {
|
||||
AuthMode::ChatGPT => 0,
|
||||
AuthMode::ApiKey => 1,
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ use ratatui::style::{Modifier, Style};
|
||||
use ratatui::text::{Line, Span};
|
||||
use ratatui::widgets::{Block, Borders, Clear, Paragraph, Widget};
|
||||
|
||||
use crate::account_label::{account_display_label, account_mode_priority};
|
||||
use crate::app_event::AppEvent;
|
||||
use crate::app_event_sender::AppEventSender;
|
||||
|
||||
@@ -117,6 +118,11 @@ impl LoginAccountsState {
|
||||
}
|
||||
|
||||
fn reload_accounts(&mut self) {
|
||||
let previously_selected_id = self
|
||||
.accounts
|
||||
.get(self.selected)
|
||||
.map(|row| row.id.clone());
|
||||
|
||||
match auth_accounts::list_accounts(&self.codex_home) {
|
||||
Ok(raw_accounts) => {
|
||||
let active_id = auth_accounts::get_active_account_id(&self.codex_home).ok().flatten();
|
||||
@@ -125,10 +131,33 @@ impl LoginAccountsState {
|
||||
.into_iter()
|
||||
.map(|account| AccountRow::from(account, active_id.as_deref()))
|
||||
.collect();
|
||||
|
||||
self.accounts.sort_by(|a, b| {
|
||||
let priority = account_mode_priority;
|
||||
let a_priority = priority(a.mode);
|
||||
let b_priority = priority(b.mode);
|
||||
a_priority
|
||||
.cmp(&b_priority)
|
||||
.then_with(|| a.label.to_ascii_lowercase().cmp(&b.label.to_ascii_lowercase()))
|
||||
.then_with(|| a.label.cmp(&b.label))
|
||||
.then_with(|| a.id.cmp(&b.id))
|
||||
});
|
||||
|
||||
let mut selected_idx = previously_selected_id
|
||||
.and_then(|id| self.accounts.iter().position(|row| row.id == id))
|
||||
.or_else(|| {
|
||||
active_id
|
||||
.as_ref()
|
||||
.and_then(|id| self.accounts.iter().position(|row| &row.id == id))
|
||||
});
|
||||
|
||||
if self.accounts.is_empty() {
|
||||
self.selected = 0;
|
||||
} else if self.selected >= self.accounts.len() {
|
||||
self.selected = self.accounts.len().saturating_sub(1);
|
||||
} else {
|
||||
if selected_idx.is_none() {
|
||||
selected_idx = Some(0);
|
||||
}
|
||||
self.selected = selected_idx.unwrap_or(0).min(self.accounts.len() - 1);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
@@ -832,23 +861,12 @@ impl LoginAddAccountState {
|
||||
|
||||
impl AccountRow {
|
||||
fn from(account: StoredAccount, active_id: Option<&str>) -> Self {
|
||||
let label = match account.mode {
|
||||
AuthMode::ApiKey => account
|
||||
.openai_api_key
|
||||
.as_ref()
|
||||
.map(|key| format!("API key (…{})", suffix(key)))
|
||||
.unwrap_or_else(|| "API key".to_string()),
|
||||
AuthMode::ChatGPT => account
|
||||
.tokens
|
||||
.as_ref()
|
||||
.and_then(|t| t.id_token.email.clone())
|
||||
.map(|email| format!("ChatGPT ({email})"))
|
||||
.unwrap_or_else(|| "ChatGPT".to_string()),
|
||||
};
|
||||
|
||||
let id = account.id.clone();
|
||||
let label = account_display_label(&account);
|
||||
let mode = account.mode;
|
||||
let mut detail_parts: Vec<String> = Vec::new();
|
||||
|
||||
if let AuthMode::ChatGPT = account.mode {
|
||||
if let AuthMode::ChatGPT = mode {
|
||||
if let Some(plan) = account
|
||||
.tokens
|
||||
.as_ref()
|
||||
@@ -868,26 +886,18 @@ impl AccountRow {
|
||||
Some(detail_parts.join(" • "))
|
||||
};
|
||||
|
||||
let is_active = active_id.is_some_and(|candidate| candidate == id);
|
||||
|
||||
Self {
|
||||
id: account.id.clone(),
|
||||
id,
|
||||
label,
|
||||
detail,
|
||||
mode: account.mode,
|
||||
is_active: active_id.is_some_and(|id| id == account.id),
|
||||
mode,
|
||||
is_active,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn suffix(text: &str) -> String {
|
||||
let tail = text
|
||||
.char_indices()
|
||||
.rev()
|
||||
.take(4)
|
||||
.map(|(_, c)| c)
|
||||
.collect::<Vec<_>>();
|
||||
tail.into_iter().rev().collect()
|
||||
}
|
||||
|
||||
fn format_timestamp(ts: DateTime<Utc>) -> String {
|
||||
ts.with_timezone(&chrono::Local)
|
||||
.format("%Y-%m-%d %H:%M")
|
||||
|
||||
@@ -173,6 +173,7 @@ fn status_tokens_prefix() -> &'static str {
|
||||
STATUS_TOKENS_PREFIX
|
||||
}
|
||||
|
||||
use crate::account_label::{account_display_label, account_mode_priority};
|
||||
use crate::app_event::{
|
||||
AppEvent,
|
||||
BackgroundPlacement,
|
||||
@@ -3604,7 +3605,7 @@ impl ChatWidget<'_> {
|
||||
.and_then(|id| usage_summary_map.get(id));
|
||||
|
||||
let title = account_ref
|
||||
.map(Self::account_label)
|
||||
.map(account_display_label)
|
||||
.or_else(|| active_id.clone())
|
||||
.unwrap_or_else(|| "Current session".to_string());
|
||||
let header = Self::account_header_lines(account_ref, snapshot_ref, summary_ref);
|
||||
@@ -3626,23 +3627,28 @@ impl ChatWidget<'_> {
|
||||
.filter(|id| !seen_ids.contains(*id))
|
||||
.cloned()
|
||||
.collect();
|
||||
remaining_ids.sort_by(|a, b| {
|
||||
let a_label = account_map
|
||||
.get(a)
|
||||
.map(Self::account_label)
|
||||
.unwrap_or_else(|| a.clone());
|
||||
let b_label = account_map
|
||||
.get(b)
|
||||
.map(Self::account_label)
|
||||
.unwrap_or_else(|| b.clone());
|
||||
let a_cmp = a_label.to_ascii_lowercase();
|
||||
let b_cmp = b_label.to_ascii_lowercase();
|
||||
let order = a_cmp.cmp(&b_cmp);
|
||||
if order == std::cmp::Ordering::Equal {
|
||||
a_label.cmp(&b_label)
|
||||
|
||||
let account_sort_key = |id: &String| {
|
||||
if let Some(account) = account_map.get(id) {
|
||||
let label = account_display_label(account);
|
||||
(
|
||||
account_mode_priority(account.mode),
|
||||
label.to_ascii_lowercase(),
|
||||
label,
|
||||
)
|
||||
} else {
|
||||
order
|
||||
(u8::MAX, id.to_ascii_lowercase(), id.clone())
|
||||
}
|
||||
};
|
||||
|
||||
remaining_ids.sort_by(|a, b| {
|
||||
let (a_priority, a_lower, a_label) = account_sort_key(a);
|
||||
let (b_priority, b_lower, b_label) = account_sort_key(b);
|
||||
a_priority
|
||||
.cmp(&b_priority)
|
||||
.then_with(|| a_lower.cmp(&b_lower))
|
||||
.then_with(|| a_label.cmp(&b_label))
|
||||
.then_with(|| a.cmp(b))
|
||||
});
|
||||
|
||||
for id in remaining_ids {
|
||||
@@ -3650,7 +3656,7 @@ impl ChatWidget<'_> {
|
||||
let record = snapshot_map.remove(&id);
|
||||
let usage_summary = usage_summary_map.remove(&id);
|
||||
let title = account
|
||||
.map(Self::account_label)
|
||||
.map(account_display_label)
|
||||
.unwrap_or_else(|| id.clone());
|
||||
match record {
|
||||
Some(record) => {
|
||||
@@ -3712,14 +3718,6 @@ impl ChatWidget<'_> {
|
||||
tabs
|
||||
}
|
||||
|
||||
fn account_label(account: &StoredAccount) -> String {
|
||||
account
|
||||
.label
|
||||
.clone()
|
||||
.filter(|label| !label.trim().is_empty())
|
||||
.unwrap_or_else(|| account.id.clone())
|
||||
}
|
||||
|
||||
fn usage_cost_usd_from_totals(totals: &TokenTotals) -> f64 {
|
||||
let non_cached_input = totals
|
||||
.input_tokens
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::account_label::key_suffix;
|
||||
use crate::diff_render::create_diff_summary_with_width;
|
||||
use crate::exec_command::strip_bash_lc_and_escape;
|
||||
use crate::sanitize::Mode as SanitizeMode;
|
||||
@@ -5632,10 +5633,7 @@ pub(crate) fn new_status_output(
|
||||
.ok()
|
||||
.and_then(|a| a.openai_api_key)
|
||||
.or_else(|| std::env::var(OPENAI_API_KEY_ENV_VAR).ok())
|
||||
.map(|k| {
|
||||
let n = k.len().saturating_sub(4);
|
||||
k[n..].to_string()
|
||||
})
|
||||
.map(|k| key_suffix(&k))
|
||||
.unwrap_or_else(|| "????".to_string());
|
||||
lines.push(Line::from(format!(" • Method: API key (…{suffix})")));
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ mod app;
|
||||
mod app_event;
|
||||
mod app_event_sender;
|
||||
mod backtrack_helpers;
|
||||
mod account_label;
|
||||
mod bottom_pane;
|
||||
mod chatwidget;
|
||||
mod citation_regex;
|
||||
|
||||
Reference in New Issue
Block a user