From 8f135dffd3df23d74c8d44a0afef312fe5318181 Mon Sep 17 00:00:00 2001 From: afgonullu <47690646+afgonullu@users.noreply.github.com> Date: Sat, 7 Nov 2026 14:59:20 +0100 Subject: [PATCH 1/5] fix(gui): name the active Claude account in the provider Overview The Overview of a Claude pool showed only "OAuth" and "Logged in", because the login email is hidden for Anthropic pools (#8055). Show the active account with its Accounts row label instead: alias, then the server-masked identity, then the ordinal. The email stays hidden, and other OAuth providers keep their current Overview text. Refs #8286 --- .../provider-workspace/ProviderDetails.tsx | 7 +- .../provider-workspace/ProviderOverview.tsx | 18 ++- .../provider-overview-account-label.test.tsx | 152 ++++++++++++++++++ 3 files changed, 171 insertions(+), 6 deletions(-) create mode 100644 gui/tests/provider-overview-account-label.test.tsx diff --git a/gui/src/components/provider-workspace/ProviderDetails.tsx b/gui/src/components/provider-workspace/ProviderDetails.tsx index ab45351a1f4..6d0c8d94992 100644 --- a/gui/src/components/provider-workspace/ProviderDetails.tsx +++ b/gui/src/components/provider-workspace/ProviderDetails.tsx @@ -8,7 +8,7 @@ import type { WorkspaceItem } from "../../provider-workspace/catalog"; import { formatProviderDisplayName } from "../../provider-icons"; import { isAccountProvider, isFreeProvider } from "../../provider-workspace/catalog"; import { isLocalProvider } from "../../provider-workspace/kind"; -import { providerAuthSurface } from "../../provider-workspace/auth"; +import { oauthAccountDisplayLabel, providerAuthSurface } from "../../provider-workspace/auth"; import { ProviderIcon } from "./ProviderRail"; import { Switch } from "../../ui"; import { IconChevron, IconTrash } from "../../icons"; @@ -139,6 +139,10 @@ export default function ProviderDetails({ const currentQuotaReading = authSurface === "oauth-accounts" ? accounts?.find(account => account.active) : authSurface === "api-keys" ? keys?.find(entry => entry.active) : undefined; + const activeOAuthAccount = authSurface === "oauth-accounts" ? accounts?.find(account => account.active) : undefined; + const activeAccountLabel = activeOAuthAccount && accounts + ? oauthAccountDisplayLabel(accounts, activeOAuthAccount, t) + : undefined; // Global counter from Providers — only honor it for the reveal target. const scopedAccountsFocusToken = accountsFocusProvider === item.name ? accountsFocusToken : 0; const usesCodexPool = isAccountProvider(item.name, item); @@ -372,6 +376,7 @@ export default function ProviderDetails({ currentQuotaReading={currentQuotaReading} onRefreshQuota={onRefreshQuota} oauthEmail={oauthEmail} + activeAccountLabel={activeAccountLabel} oauth={oauth} onEditSettings={() => switchTab("settings")} onViewUsage={() => switchTab("usage")} diff --git a/gui/src/components/provider-workspace/ProviderOverview.tsx b/gui/src/components/provider-workspace/ProviderOverview.tsx index 4912b2ed86a..21e225882f5 100644 --- a/gui/src/components/provider-workspace/ProviderOverview.tsx +++ b/gui/src/components/provider-workspace/ProviderOverview.tsx @@ -37,7 +37,7 @@ type ConnectionTestState = { }; export default function ProviderOverview({ - item, preset, usageTotals, quotaReport, currentQuotaReading, onRefreshQuota, oauthEmail, oauth, + item, preset, usageTotals, quotaReport, currentQuotaReading, onRefreshQuota, oauthEmail, activeAccountLabel, oauth, apiBase, connectionIdentity, probeController, availableModels = [], selectedModels, modelRows, modelRowsReady = false, modelsLoading = false, modelsLoadFailed = false, onRetryModels, onOpenAccounts, onOpenModelsTab, onOpenModels, onOpenConnect, onEditSettings, onViewUsage, onUpdateProvider, @@ -51,6 +51,12 @@ export default function ProviderOverview({ currentQuotaReading?: AccountQuotaReading; onRefreshQuota?: () => Promise; oauthEmail?: string; + /** + * Label of the active OAuth pool account, as its Accounts row shows it: alias, then the + * server-masked identity, then the ordinal. Claude pools name the account with this, because + * their login email stays hidden. + */ + activeAccountLabel?: string; /** Login state for OAuth summaries that carry no email (e.g. Cursor/Kimi). */ oauth?: { loggedIn?: boolean; needsReauth?: boolean }; apiBase?: string; @@ -245,7 +251,9 @@ export default function ProviderOverview({ : reverifyPending ? t("pws.firstResponse.probeReverify", { model: reverifyModel }) : t("pws.firstResponse.probeUntested"); - const displayEmail = isAnthropicInstanceId(item.name) && anthropicInstanceRowShapeMatches(item.name, item) ? undefined : oauthEmail; + const displayIdentity = isAnthropicInstanceId(item.name) && anthropicInstanceRowShapeMatches(item.name, item) + ? activeAccountLabel + : oauthEmail; const openModelsAction = onOpenModelsTab ?? onOpenModels; const fixCredentialsAction = onOpenAccounts ?? onEditSettings; @@ -272,7 +280,7 @@ export default function ProviderOverview({
{t("pws.cell.auth")}
-
{displayEmail ? `${authModeLabel(item, t)} · ${displayEmail}` : authModeLabel(item, t)}
+
{displayIdentity ? `${authModeLabel(item, t)} · ${displayIdentity}` : authModeLabel(item, t)}
{t("modal.defaultModel")}
@@ -446,8 +454,8 @@ export default function ProviderOverview({ {item.authMode === "forward" ? t("pws.passthrough") : item.authMode === "oauth" - ? (displayEmail - ? t("pws.loggedInAs", { email: displayEmail }) + ? (displayIdentity + ? t("pws.loggedInAs", { email: displayIdentity }) : oauth?.loggedIn ? t("pws.loggedInTitle") : t("pws.notLoggedIn")) diff --git a/gui/tests/provider-overview-account-label.test.tsx b/gui/tests/provider-overview-account-label.test.tsx new file mode 100644 index 00000000000..19302ee772f --- /dev/null +++ b/gui/tests/provider-overview-account-label.test.tsx @@ -0,0 +1,152 @@ +import { afterEach, beforeEach, expect, test } from "bun:test"; +import { Window } from "happy-dom"; +import { act } from "react"; +import type { Root } from "react-dom/client"; +import ProviderDetails from "../src/components/provider-workspace/ProviderDetails"; +import type { OAuthAccountRow } from "../src/components/provider-workspace/types"; +import { LanguageProvider } from "../src/i18n/provider"; +import type { WorkspaceItem } from "../src/provider-workspace/catalog"; + +/** + * #8286: the Overview of a Claude pool named no account at all, while its Accounts rows show + * the alias. The Overview now names the active account with the row label (alias, then the + * server-masked identity, then the ordinal) and still never renders the login email. + */ + +const globals = ["document", "window", "navigator", "localStorage", "IS_REACT_ACT_ENVIRONMENT"] as const; +const WINDOW_EVENT_STUB: { event: undefined } = { event: undefined }; +let previous: Record<(typeof globals)[number], unknown>; +let win: Window; +let host: HTMLElement; +let root: Root | null = null; +let originalFetch: typeof globalThis.fetch; + +const POOL_A = { name: "anthropic", adapter: "anthropic", baseUrl: "https://api.anthropic.com", authMode: "oauth" } as unknown as WorkspaceItem; +const POOL_B = { ...POOL_A, name: "anthropic2", anthropicOAuthInstance: "anthropic2" } as unknown as WorkspaceItem; +const OTHER = { name: "xai", adapter: "openai-chat", baseUrl: "https://xai.invalid/v1", authMode: "oauth", hasApiKey: false } as unknown as WorkspaceItem; + +function rows(pool: string, active: Partial): OAuthAccountRow[] { + return [ + { id: "first", email: "first@" + pool + ".test", active: false, paused: false, alias: "Spare" }, + { id: "second", email: "second@" + pool + ".test", active: true, paused: false, ...active }, + ] as OAuthAccountRow[]; +} + +beforeEach(() => { + previous = Object.fromEntries(globals.map((k) => [k, Reflect.get(globalThis, k)])) as typeof previous; + originalFetch = globalThis.fetch; + win = new Window({ url: "http://localhost/" }); + Object.defineProperty(win.navigator, "language", { configurable: true, value: "en-US" }); + Object.defineProperty(win, "event", { configurable: true, writable: true, value: undefined }); + Object.defineProperties(globalThis, { + document: { configurable: true, value: win.document }, + window: { configurable: true, value: win }, + navigator: { configurable: true, value: win.navigator }, + localStorage: { configurable: true, value: win.localStorage }, + }); + (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; + Object.defineProperty(globalThis, "fetch", { + configurable: true, + value: async () => ({ ok: true, json: async () => ({}) }) as unknown as Response, + }); + host = win.document.createElement("div") as unknown as HTMLElement; + win.document.body.appendChild(host as never); +}); + +afterEach(async () => { + if (root) { + const current = root; + await act(async () => { current.unmount(); }); + root = null; + } + await act(async () => { + for (let i = 0; i < 5; i++) { + await new Promise((r) => setTimeout(r, 0)); + await Promise.resolve(); + } + }); + for (const key of globals) { + let value = previous[key]; + if (key === "window") { + if (value == null || typeof value !== "object") { + value = WINDOW_EVENT_STUB; + } else if (!Object.prototype.hasOwnProperty.call(value, "event")) { + try { + Object.defineProperty(value, "event", { configurable: true, writable: true, value: undefined }); + } catch { + value = WINDOW_EVENT_STUB; + } + } + } + Object.defineProperty(globalThis, key, { configurable: true, value }); + } + Object.defineProperty(globalThis, "fetch", { configurable: true, value: originalFetch }); +}); + +async function overviewText(item: WorkspaceItem, accounts: OAuthAccountRow[], oauthEmail?: string): Promise { + const { createRoot } = await import("react-dom/client"); + await act(async () => { + root = createRoot(host); + root.render( + + {}} + onDeselect={() => {}} + apiBase="" + accounts={accounts} + accountLoadState="ready" + oauth={{ loggedIn: true }} + oauthEmail={oauthEmail} + /> + , + ); + }); + await act(async () => { await new Promise((r) => setTimeout(r, 20)); }); + expect(host.querySelector('[role="tab"][aria-selected="true"]')?.id).toBe("pws-tab-overview"); + return host.textContent ?? ""; +} + +for (const item of [POOL_A, POOL_B]) { + const email = "second@" + item.name + ".test"; + + test(`${item.name} Overview names the active account by its alias`, async () => { + const text = await overviewText(item, rows(item.name, { alias: "Work", maskedLabel: "s***d@example.test" }), email); + expect(text).toContain("OAuth · Work"); + expect(text).toContain("Logged in as Work"); + expect(text).not.toContain("Spare"); + expect(text).not.toContain(email); + }); + + test(`${item.name} Overview falls back to the masked label, then the ordinal, never the email`, async () => { + const masked = await overviewText(item, rows(item.name, { maskedLabel: "s***d@example.test" }), email); + expect(masked).toContain("Logged in as s***d@example.test"); + expect(masked).not.toContain(email); + await act(async () => { root!.unmount(); }); + root = null; + + const ordinal = await overviewText(item, rows(item.name, {}), email); + expect(ordinal).toContain("Logged in as Account 2"); + expect(ordinal).not.toContain(email); + }); + + test(`${item.name} Overview without an active account keeps the generic status`, async () => { + const text = await overviewText(item, rows(item.name, { active: false }), email); + expect(text).toContain("Logged in"); + expect(text).not.toContain("Logged in as"); + expect(text).not.toContain(email); + }); +} + +test("other OAuth providers keep the login identity in the Overview", async () => { + const text = await overviewText(OTHER, rows("xai", { alias: "Work" }), "s***d@xai.test"); + expect(text).toContain("Logged in as s***d@xai.test"); +}); + From c71352a44a3e2abed224c1a2df126b3f6bf9f0f6 Mon Sep 17 00:00:00 2001 From: afgonullu <47690646+afgonullu@users.noreply.github.com> Date: Sat, 7 Nov 2026 15:00:24 +0100 Subject: [PATCH 2/5] docs(claude-code): note that the Overview names the active account Refs #8286 --- docs-site/src/content/docs/guides/claude-code.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs-site/src/content/docs/guides/claude-code.md b/docs-site/src/content/docs/guides/claude-code.md index d3e46fbbd37..d38c711abe3 100644 --- a/docs-site/src/content/docs/guides/claude-code.md +++ b/docs-site/src/content/docs/guides/claude-code.md @@ -79,7 +79,8 @@ The dashboard shows Pro, Max 5x, or Ultra beside Claude account quotas, using su metadata from the authenticated profile. Both `ultra` and `claude_max_20x` display as Ultra (the existing `ultra` and `max_20x` tier aliases are retained). Unfamiliar validated tiers retain their original name without failing the usage read. Claude account rows prefer your alias, then a masked email, and -fall back to a numbered label when no safe identity is available. +fall back to a numbered label when no safe identity is available. The provider Overview +names the active account with the same label. You can log in multiple Claude accounts via the Providers dashboard (`ocx login anthropic` / add-account). By default every request uses the **active** account only. From 40dc256f8057bf15ff7c79a6048a141be5ae04ab Mon Sep 17 00:00:00 2001 From: afgonullu <47690646+afgonullu@users.noreply.github.com> Date: Sat, 7 Nov 2026 15:08:02 +0100 Subject: [PATCH 3/5] test(gui): cover a blank alias in the Overview account label Refs #8286 --- gui/tests/provider-overview-account-label.test.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/gui/tests/provider-overview-account-label.test.tsx b/gui/tests/provider-overview-account-label.test.tsx index 19302ee772f..93509b23e85 100644 --- a/gui/tests/provider-overview-account-label.test.tsx +++ b/gui/tests/provider-overview-account-label.test.tsx @@ -25,6 +25,7 @@ const POOL_A = { name: "anthropic", adapter: "anthropic", baseUrl: "https://api. const POOL_B = { ...POOL_A, name: "anthropic2", anthropicOAuthInstance: "anthropic2" } as unknown as WorkspaceItem; const OTHER = { name: "xai", adapter: "openai-chat", baseUrl: "https://xai.invalid/v1", authMode: "oauth", hasApiKey: false } as unknown as WorkspaceItem; +/** Two-account roster for `pool`: an inactive "Spare" row, then the active row with `active` applied. */ function rows(pool: string, active: Partial): OAuthAccountRow[] { return [ { id: "first", email: "first@" + pool + ".test", active: false, paused: false, alias: "Spare" }, @@ -83,6 +84,7 @@ afterEach(async () => { Object.defineProperty(globalThis, "fetch", { configurable: true, value: originalFetch }); }); +/** Mounts ProviderDetails on its default Overview tab and returns the rendered text. */ async function overviewText(item: WorkspaceItem, accounts: OAuthAccountRow[], oauthEmail?: string): Promise { const { createRoot } = await import("react-dom/client"); await act(async () => { @@ -137,6 +139,12 @@ for (const item of [POOL_A, POOL_B]) { expect(ordinal).not.toContain(email); }); + test(`${item.name} Overview treats a blank alias as missing`, async () => { + const text = await overviewText(item, rows(item.name, { alias: " ", maskedLabel: "s***d@example.test" }), email); + expect(text).toContain("Logged in as s***d@example.test"); + expect(text).not.toContain(email); + }); + test(`${item.name} Overview without an active account keeps the generic status`, async () => { const text = await overviewText(item, rows(item.name, { active: false }), email); expect(text).toContain("Logged in"); From f2875ceaaebf34cbb39a2ca7f9ac39ee278111b8 Mon Sep 17 00:00:00 2001 From: afgonullu <47690646+afgonullu@users.noreply.github.com> Date: Sat, 7 Nov 2026 15:23:29 +0100 Subject: [PATCH 4/5] docs(gui): document ProviderDetails and ProviderOverview Refs #8286 --- gui/src/components/provider-workspace/ProviderDetails.tsx | 4 ++++ gui/src/components/provider-workspace/ProviderOverview.tsx | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/gui/src/components/provider-workspace/ProviderDetails.tsx b/gui/src/components/provider-workspace/ProviderDetails.tsx index 6d0c8d94992..01fe26e8c78 100644 --- a/gui/src/components/provider-workspace/ProviderDetails.tsx +++ b/gui/src/components/provider-workspace/ProviderDetails.tsx @@ -27,6 +27,10 @@ import type { AccountLoadState, ProviderModelUsageRow, ProviderUsageTotals, OAut type Tab = "overview" | "models" | "usage" | "accounts" | "settings"; +/** + * Detail view for one provider. Owns the tab state and derives what the tabs share: the auth + * surface, the active account or key, and the active OAuth account's display label for the Overview. + */ export default function ProviderDetails({ item, preset, diff --git a/gui/src/components/provider-workspace/ProviderOverview.tsx b/gui/src/components/provider-workspace/ProviderOverview.tsx index 21e225882f5..5b611602fe1 100644 --- a/gui/src/components/provider-workspace/ProviderOverview.tsx +++ b/gui/src/components/provider-workspace/ProviderOverview.tsx @@ -36,6 +36,10 @@ type ConnectionTestState = { result: ConnectionTestResult | null; }; +/** + * Overview tab: connection, authentication, notes, usage statistics and the current account's quota. + * A Claude pool names its active account with `activeAccountLabel` and never with the login email. + */ export default function ProviderOverview({ item, preset, usageTotals, quotaReport, currentQuotaReading, onRefreshQuota, oauthEmail, activeAccountLabel, oauth, apiBase, connectionIdentity, probeController, availableModels = [], selectedModels, modelRows, modelRowsReady = false, From b7db176a280f02984c75b03b802eca4cbfcb7c7e Mon Sep 17 00:00:00 2001 From: JUN Date: Sun, 8 Nov 2026 18:14:38 +0900 Subject: [PATCH 5/5] test(gui): reconcile Claude account overview coverage Co-authored-by: afgonullu <47690646+afgonullu@users.noreply.github.com> --- .../provider-overview-account-label.test.tsx | 63 +++++++++++++++---- 1 file changed, 50 insertions(+), 13 deletions(-) diff --git a/gui/tests/provider-overview-account-label.test.tsx b/gui/tests/provider-overview-account-label.test.tsx index 93509b23e85..57e5f3392df 100644 --- a/gui/tests/provider-overview-account-label.test.tsx +++ b/gui/tests/provider-overview-account-label.test.tsx @@ -3,14 +3,14 @@ import { Window } from "happy-dom"; import { act } from "react"; import type { Root } from "react-dom/client"; import ProviderDetails from "../src/components/provider-workspace/ProviderDetails"; -import type { OAuthAccountRow } from "../src/components/provider-workspace/types"; +import type { AccountLoadState, OAuthAccountRow } from "../src/components/provider-workspace/types"; import { LanguageProvider } from "../src/i18n/provider"; import type { WorkspaceItem } from "../src/provider-workspace/catalog"; /** * #8286: the Overview of a Claude pool named no account at all, while its Accounts rows show - * the alias. The Overview now names the active account with the row label (alias, then the - * server-masked identity, then the ordinal) and still never renders the login email. + * the alias. A verified roster supplies a separate current-account field using the row label + * (alias, then masked identity, then ordinal), without exposing the login email. */ const globals = ["document", "window", "navigator", "localStorage", "IS_REACT_ACT_ENVIRONMENT"] as const; @@ -85,7 +85,13 @@ afterEach(async () => { }); /** Mounts ProviderDetails on its default Overview tab and returns the rendered text. */ -async function overviewText(item: WorkspaceItem, accounts: OAuthAccountRow[], oauthEmail?: string): Promise { +async function overviewText( + item: WorkspaceItem, + accounts: OAuthAccountRow[], + oauthEmail?: string, + accountLoadState: AccountLoadState = "ready", + loggedIn: boolean | null = true, +): Promise { const { createRoot } = await import("react-dom/client"); await act(async () => { root = createRoot(host); @@ -104,8 +110,8 @@ async function overviewText(item: WorkspaceItem, accounts: OAuthAccountRow[], oa onDeselect={() => {}} apiBase="" accounts={accounts} - accountLoadState="ready" - oauth={{ loggedIn: true }} + accountLoadState={accountLoadState} + oauth={{ loggedIn: loggedIn ?? undefined }} oauthEmail={oauthEmail} /> , @@ -116,38 +122,69 @@ async function overviewText(item: WorkspaceItem, accounts: OAuthAccountRow[], oa return host.textContent ?? ""; } +function currentAccount(): string | undefined { + const row = Array.from(host.querySelectorAll(".pws-kv-row")) + .find((entry) => entry.querySelector("dt")?.textContent === "Current account"); + return row?.querySelector("dd")?.textContent ?? undefined; +} + for (const item of [POOL_A, POOL_B]) { const email = "second@" + item.name + ".test"; test(`${item.name} Overview names the active account by its alias`, async () => { const text = await overviewText(item, rows(item.name, { alias: "Work", maskedLabel: "s***d@example.test" }), email); - expect(text).toContain("OAuth · Work"); - expect(text).toContain("Logged in as Work"); + expect(currentAccount()).toBe("Work"); + expect(text).not.toContain("Logged in as Work"); expect(text).not.toContain("Spare"); expect(text).not.toContain(email); }); test(`${item.name} Overview falls back to the masked label, then the ordinal, never the email`, async () => { const masked = await overviewText(item, rows(item.name, { maskedLabel: "s***d@example.test" }), email); - expect(masked).toContain("Logged in as s***d@example.test"); + expect(currentAccount()).toBe("s***d@example.test"); expect(masked).not.toContain(email); await act(async () => { root!.unmount(); }); root = null; const ordinal = await overviewText(item, rows(item.name, {}), email); - expect(ordinal).toContain("Logged in as Account 2"); + expect(currentAccount()).toBe("Account 2"); expect(ordinal).not.toContain(email); }); test(`${item.name} Overview treats a blank alias as missing`, async () => { const text = await overviewText(item, rows(item.name, { alias: " ", maskedLabel: "s***d@example.test" }), email); - expect(text).toContain("Logged in as s***d@example.test"); + expect(currentAccount()).toBe("s***d@example.test"); expect(text).not.toContain(email); }); test(`${item.name} Overview without an active account keeps the generic status`, async () => { const text = await overviewText(item, rows(item.name, { active: false }), email); - expect(text).toContain("Logged in"); + expect(currentAccount()).toBeUndefined(); + expect(text).not.toContain("Logged in as"); + expect(text).not.toContain(email); + }); + + for (const state of ["idle", "loading", "error"] as const) { + test(`${item.name} Overview hides stale account labels while roster is ${state}`, async () => { + const text = await overviewText(item, rows(item.name, { alias: "Stale" }), email, state); + expect(currentAccount()).toBeUndefined(); + expect(text).not.toContain("Stale"); + expect(text).not.toContain(email); + }); + } + + test(`${item.name} Overview reports logged-out status without naming an account`, async () => { + const text = await overviewText(item, [], email, "ready", false); + expect(currentAccount()).toBeUndefined(); + expect(text).toContain("Not logged in"); + expect(text).not.toContain("Logged in as"); + expect(text).not.toContain(email); + }); + + test(`${item.name} Overview reports unknown login status as unverified`, async () => { + const text = await overviewText(item, [], email, "ready", null); + expect(currentAccount()).toBeUndefined(); + expect(text).toContain("Not verified"); expect(text).not.toContain("Logged in as"); expect(text).not.toContain(email); }); @@ -156,5 +193,5 @@ for (const item of [POOL_A, POOL_B]) { test("other OAuth providers keep the login identity in the Overview", async () => { const text = await overviewText(OTHER, rows("xai", { alias: "Work" }), "s***d@xai.test"); expect(text).toContain("Logged in as s***d@xai.test"); + expect(currentAccount()).toBeUndefined(); }); -