From b9f68379135afa653f835cd36fef8a6ad515bad9 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Tue, 23 Jun 2026 23:10:15 -0400 Subject: [PATCH] fix(bar): lead carousel with default account, label it as the base command Address UX review: the bare login (e.g. ~/.codex) is the default way of running a surface, not a profile. Rename it from 'personal' to 'default' and present it as the base command ('ccsx') with a 'default' badge, while named profiles show their name ('ck') plus the owning surface tag ('ccsx'). Order each provider carousel so the default account leads, then by tightest quota window. --- macos-bar/Sources/CCSBarApp/BarMenuView.swift | 9 ++++--- .../CCSBarApp/BarSubscriptionCard.swift | 10 ++++--- macos-bar/Sources/CCSBarCheck/main.swift | 26 +++++++++++++++++++ .../Sources/CCSBarCore/BarFormatting.swift | 26 ++++++++++++++++++- .../usage/native-quota-collector.ts | 26 ++++++++++++------- 5 files changed, 80 insertions(+), 17 deletions(-) diff --git a/macos-bar/Sources/CCSBarApp/BarMenuView.swift b/macos-bar/Sources/CCSBarApp/BarMenuView.swift index 91a60b71..4c14e1fd 100644 --- a/macos-bar/Sources/CCSBarApp/BarMenuView.swift +++ b/macos-bar/Sources/CCSBarApp/BarMenuView.swift @@ -340,11 +340,14 @@ struct BarMenuView: View { } } - /// Order subscription cards by tightest binding window ascending (closest to - /// empty on top). Rows with no binding window (error/reauth) sink to the bottom - /// so the actionable quota always leads. + /// Order subscription cards so the default/base account leads its provider + /// carousel (it is the account the user runs by default), then by tightest + /// binding window ascending (closest to empty next). Rows with no binding + /// window (error/reauth) sink to the bottom so actionable quota leads. private func orderedSubscriptions(_ subs: [BarSummaryRow]) -> [BarSummaryRow] { subs.sorted { a, b in + // Default account first within its provider group. + if a.isDefault != b.isDefault { return a.isDefault } let ra = BarQuotaGauge.selectBindingWindow(a.quotaWindows ?? [])?.remainingPercent let rb = BarQuotaGauge.selectBindingWindow(b.quotaWindows ?? [])?.remainingPercent switch (ra, rb) { diff --git a/macos-bar/Sources/CCSBarApp/BarSubscriptionCard.swift b/macos-bar/Sources/CCSBarApp/BarSubscriptionCard.swift index f1f77d55..9363fb2a 100644 --- a/macos-bar/Sources/CCSBarApp/BarSubscriptionCard.swift +++ b/macos-bar/Sources/CCSBarApp/BarSubscriptionCard.swift @@ -61,12 +61,14 @@ struct BarSubscriptionCard: View { Circle() .fill(healthColor) .frame(width: 8, height: 8) - // Multi-profile: show profile name when available; fall back to provider label. - Text(row.profile ?? BarFormatting.providerLabel(row.provider)) + // Default account shows the bare command ("ccsx"); a named profile shows its + // name ("ck"). Falls back to the provider label for legacy rows. + Text(BarFormatting.accountTitle(row)) .font(.system(.body, design: .default).weight(.semibold)) .lineLimit(1) - // Surface tag chip ("ccs · work", "ccsx · personal") for multi-profile rows. - if let tag = BarFormatting.surfaceProfileLabel(row) { + // Tag: "default" for the base account, else the owning surface ("ccsx") so a + // named profile reads as e.g. "ck · ccsx". + if let tag = BarFormatting.accountTag(row) { Chip(tag, tint: theme.subscription.opacity(isParked ? 0.5 : 1)) } if row.needsReauth { diff --git a/macos-bar/Sources/CCSBarCheck/main.swift b/macos-bar/Sources/CCSBarCheck/main.swift index 36ac3e67..72612c59 100644 --- a/macos-bar/Sources/CCSBarCheck/main.swift +++ b/macos-bar/Sources/CCSBarCheck/main.swift @@ -1832,6 +1832,32 @@ do { check( BarFormatting.surfaceProfileLabel(noSurfaceRow) == "codex · personal", "mp: surfaceProfileLabel falls back to provider when surface is nil") + + // (MP9) base/default account vs named profile presentation + let defaultCodexRow = BarSummaryRow( + accountId: "ccsx:default", provider: "codex", + surface: "ccsx", profile: "default", isSubscription: true) + check( + BarFormatting.isBaseAccount(defaultCodexRow), + "mp: 'default' profile is the base account") + check( + !BarFormatting.isBaseAccount(codexRow), + "mp: named profile 'ck' is not the base account") + check( + BarFormatting.accountTitle(defaultCodexRow) == "ccsx", + "mp: accountTitle for default account -> 'ccsx' (bare command)") + check( + BarFormatting.accountTitle(codexRow) == "ck", + "mp: accountTitle for named profile -> 'ck'") + check( + BarFormatting.accountTag(defaultCodexRow) == "default", + "mp: accountTag for default account -> 'default'") + check( + BarFormatting.accountTag(codexRow) == "ccsx", + "mp: accountTag for named profile -> 'ccsx' (surface)") + check( + BarFormatting.accountTag(legacyRow) == nil, + "mp: accountTag for CLIProxy pool row -> nil") } catch { check(false, "mp: multi-profile JSON decode failed: \(error)") } diff --git a/macos-bar/Sources/CCSBarCore/BarFormatting.swift b/macos-bar/Sources/CCSBarCore/BarFormatting.swift index 4bea7a92..f08530fc 100644 --- a/macos-bar/Sources/CCSBarCore/BarFormatting.swift +++ b/macos-bar/Sources/CCSBarCore/BarFormatting.swift @@ -177,13 +177,37 @@ public enum BarFormatting { } /// Surface + profile chip label for multi-profile cards, e.g. "ccs · work" or - /// "ccsx · personal". Returns nil for CLIProxy pool rows that have no profile. + /// "ccsx · ck". Returns nil for CLIProxy pool rows that have no profile. public static func surfaceProfileLabel(_ row: BarSummaryRow) -> String? { guard let p = row.profile else { return nil } let s = row.surface ?? row.provider return "\(s) · \(p)" } + /// True when this row is the surface's default/base account — the bare login + /// (e.g. `ccsx` => ~/.codex), as opposed to a named `ccsx ` profile. + /// The server names this account "default". + public static func isBaseAccount(_ row: BarSummaryRow) -> Bool { + row.profile == "default" + } + + /// Primary card title. For the default/base account it is the bare command the + /// user actually runs ("ccsx" / "ccs"); for a named profile it is the profile + /// name ("ck"). Falls back to the provider product label for legacy rows. + public static func accountTitle(_ row: BarSummaryRow) -> String { + if isBaseAccount(row) { return row.surface ?? providerLabel(row.provider) } + return row.profile ?? providerLabel(row.provider) + } + + /// Secondary chip beside the title. The default/base account is tagged + /// "default"; a named profile is tagged with the surface that owns it + /// ("ccs"/"ccsx") so "ck" reads as a ccsx profile. Nil for pool rows. + public static func accountTag(_ row: BarSummaryRow) -> String? { + guard row.profile != nil else { return nil } + if isBaseAccount(row) { return "default" } + return row.surface ?? row.provider + } + /// Friendly product label for a provider key. Native subscription keys read as /// products ("Claude Code", "Codex"); any other provider passes through verbatim /// (so "agy"/"ghcp"/"kiro" keep their established short chip text). diff --git a/src/web-server/usage/native-quota-collector.ts b/src/web-server/usage/native-quota-collector.ts index 0d89b3c4..5e32cb05 100644 --- a/src/web-server/usage/native-quota-collector.ts +++ b/src/web-server/usage/native-quota-collector.ts @@ -80,6 +80,11 @@ const CB_COOLDOWN_MS = 900_000; // 15 minutes const SURFACE_CLAUDE = 'ccs'; const SURFACE_CODEX = 'ccsx'; +// The "default way of running" a surface — the bare login (e.g. ~/.codex for +// ccsx), as opposed to a named `ccsx ` profile. Rendered in the Bar as +// the base command ("ccsx") with a "default" badge, not as a named profile. +const DEFAULT_PROFILE = 'default'; + // Provider values on the wire (unchanged from before) const CLAUDE_NATIVE_PROVIDER = 'claude-code'; const CODEX_NATIVE_PROVIDER = 'codex'; @@ -117,13 +122,13 @@ export interface NativeQuotaDeps { getCodexQuota?: () => Promise; /** * Read the native Codex auth for a profile (file-only, no keychain). - * 'personal' reads ~/.codex/auth.json; other names read codex-instances//auth.json. + * DEFAULT_PROFILE ('default') reads ~/.codex/auth.json; other names read codex-instances//auth.json. * Returns null when absent/unparseable. */ readCodexNativeAuth?: (profile: string) => { accessToken: string; accountId: string } | null; /** Enumerate Claude profile names. Injected so tests never touch real fs. */ listClaudeProfiles?: () => string[]; - /** Enumerate Codex profile names (including 'personal' for bare ~/.codex). */ + /** Enumerate Codex profile names (including DEFAULT_PROFILE for bare ~/.codex). */ listCodexProfiles?: () => string[]; /** Resolve the default Claude profile name. */ defaultClaudeProfile?: () => string | null; @@ -513,7 +518,7 @@ function readCodexNativeAuthFromDisk( ): { accessToken: string; accountId: string } | null { try { let authPath: string; - if (profile === 'personal') { + if (profile === DEFAULT_PROFILE) { authPath = path.join(os.homedir(), '.codex', 'auth.json'); } else { // resolveCodexProfileDir would validate, but we do it inline to avoid the @@ -576,7 +581,7 @@ function getDefaultClaudeProfileFromDisk(): string | null { } /** - * List all Codex profiles from the registry, plus 'personal' when the bare + * List all Codex profiles from the registry, plus DEFAULT_PROFILE when the bare * ~/.codex/auth.json exists. Returns [] on any read error. */ function listCodexProfilesFromDisk(): string[] { @@ -588,9 +593,11 @@ function listCodexProfilesFromDisk(): string[] { }; const registry = new CodexProfileRegistry(); const profiles = registry.listProfiles(); - // Add 'personal' for the bare ~/.codex/auth.json if it exists + // Add the bare ~/.codex/auth.json (the default `ccsx` invocation) as the + // DEFAULT_PROFILE account when it exists. It is the "default way of running", + // distinct from named `ccsx ` profiles. if (fs.existsSync(path.join(os.homedir(), '.codex', 'auth.json'))) { - if (!profiles.includes('personal')) profiles.push('personal'); + if (!profiles.includes(DEFAULT_PROFILE)) profiles.push(DEFAULT_PROFILE); } return profiles; } catch { @@ -599,7 +606,7 @@ function listCodexProfilesFromDisk(): string[] { } /** - * Resolve the default Codex profile. Falls back to 'personal' when the bare + * Resolve the default Codex profile. Falls back to DEFAULT_PROFILE when the bare * ~/.codex/auth.json exists and no registry default is set. */ function getDefaultCodexProfileFromDisk(): string | null { @@ -612,8 +619,9 @@ function getDefaultCodexProfileFromDisk(): string | null { const registry = new CodexProfileRegistry(); const def = registry.getDefault(); if (def) return def; - // Fall back to 'personal' if the bare auth.json exists - if (fs.existsSync(path.join(os.homedir(), '.codex', 'auth.json'))) return 'personal'; + // Fall back to the bare ~/.codex account (DEFAULT_PROFILE) when no registry + // default is set — it is the default `ccsx` invocation. + if (fs.existsSync(path.join(os.homedir(), '.codex', 'auth.json'))) return DEFAULT_PROFILE; return null; } catch { return null;