Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
### Added
- Grok: Settings and tray **Add account** flow matching Codex/Claude — isolated `grok login --oauth`, save current CLI login, switch, and remove without logging out the active session.

### Fixed
- Claude: when Hide Personal Info is enabled, keep saved account rows distinguishable with stable localized `Account N` labels and matching redacted tooltips.

---

## [Windows] 0.60.3 - 2026-09-15
Expand Down
23 changes: 21 additions & 2 deletions apps/desktop-tauri/src/components/ClaudeAccountsMenu.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { act, fireEvent, render, screen } from "@testing-library/react";
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { ClaudeAccount } from "../types/bridge";

Expand Down Expand Up @@ -39,12 +39,31 @@ describe("ClaudeAccountsMenu", () => {
expect(mocks.claudeAccountSwitch).toHaveBeenCalledWith(second.id);
});

it("masks emails, including tooltips, when hideEmail is enabled", async () => {
it("uses stable opaque account labels and redacts tooltips when hideEmail is enabled", async () => {
mocks.claudeAccountsList.mockResolvedValue([first, { ...second, organization: `${second.email}'s Organization` }]);
const { container } = render(<ClaudeAccountsMenu hideEmail />);
await screen.findByText("ClaudeAccountsTitle");
const labels = container.querySelectorAll(".codex-menu-accounts__email");
expect(labels[0].firstChild?.textContent).toBe("Account 1");
expect(labels[0].getAttribute("title")).toBe("Account 1");
expect(labels[1].firstChild?.textContent).toBe("Account 2");
expect(labels[1].getAttribute("title")).toBe("Account 2");
expect(container.textContent).not.toContain(first.email);
expect(container.innerHTML).not.toContain(second.email);
expect(container.textContent).not.toContain("Personal");
expect(container.textContent).not.toContain("Organization");
});

it("keeps opaque labels stable when the source reorders accounts", async () => {
const { container } = render(<ClaudeAccountsMenu hideEmail />);
await screen.findByText("ClaudeAccountsTitle");
mocks.claudeAccountsList.mockResolvedValue([second, first]);
await act(async () => window.dispatchEvent(new Event("focus")));
await waitFor(() => {
const labels = container.querySelectorAll(".codex-menu-accounts__email");
expect(labels[0].firstChild?.textContent).toBe("Account 2");
expect(labels[1].firstChild?.textContent).toBe("Account 1");
});
});

it("shows switch failures and leaves the current account marked active", async () => {
Expand Down
18 changes: 14 additions & 4 deletions apps/desktop-tauri/src/components/ClaudeAccountsMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import { listen } from "@tauri-apps/api/event";
import type { ClaudeAccount } from "../types/bridge";
import { claudeAccountsList, claudeAccountSwitch } from "../lib/tauri";
import { useLocale } from "../hooks/useLocale";
import { maskEmail } from "./MenuCard";
import {
buildClaudeAccountOrdinals,
buildPrivateClaudeAccountLabel,
} from "./claudeAccountDisplay";

export default function ClaudeAccountsMenu({ hideEmail, onLayoutChange }: {
hideEmail: boolean;
Expand Down Expand Up @@ -40,6 +43,8 @@ export default function ClaudeAccountsMenu({ hideEmail, onLayoutChange }: {
onLayoutChange?.();
}, [accounts.length, error, switched, onLayoutChange]);

const accountOrdinals = buildClaudeAccountOrdinals(accounts);

const switchAccount = async (id: string) => {
setBusy(true);
setError(null);
Expand Down Expand Up @@ -67,13 +72,18 @@ export default function ClaudeAccountsMenu({ hideEmail, onLayoutChange }: {
{switched && <p role="status">{t("ClaudeAccountsSwitched")}</p>}
<ul className="codex-menu-accounts__list">
{accounts.map(account => {
const email = hideEmail ? maskEmail(account.email) : account.email;
const privateLabel = buildPrivateClaudeAccountLabel(
account,
accountOrdinals[account.id],
hideEmail,
t("Account"),
);
return (
<li key={account.id}>
<div className={`codex-menu-accounts__row${account.isActive ? " codex-menu-accounts__row--active" : ""}`}>
<div className="codex-menu-accounts__meta">
<span className="codex-menu-accounts__email" title={email}>
{email}
<span className="codex-menu-accounts__email" title={privateLabel.tooltip}>
{privateLabel.label}
{account.isActive && <span className="codex-menu-accounts__badge">{t("TokenAccountActive")}</span>}
</span>
{!hideEmail && account.organization && !account.organization.includes(account.email) && (
Expand Down
34 changes: 34 additions & 0 deletions apps/desktop-tauri/src/components/claudeAccountDisplay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { ClaudeAccount } from "../types/bridge";

export interface PrivateClaudeAccountLabel {
label: string;
tooltip: string;
}

/**
* Assign opaque labels from stable account ids rather than the current array
* order. The account list can be reordered when the active login changes, but
* the saved Claude account id remains source-owned and stable.
*/
export function buildClaudeAccountOrdinals(
accounts: readonly ClaudeAccount[],
): Record<string, number> {
const ids = [...new Set(accounts.map((account) => account.id))].sort((left, right) =>
left < right ? -1 : left > right ? 1 : 0,
);
return Object.fromEntries(ids.map((id, index) => [id, index + 1]));
}

export function buildPrivateClaudeAccountLabel(
account: ClaudeAccount,
ordinal: number | undefined,
hidePersonalInfo: boolean,
accountWord: string,
): PrivateClaudeAccountLabel {
if (hidePersonalInfo) {
const label = ordinal === undefined ? "••••" : `${accountWord} ${ordinal}`;
return { label, tooltip: label };
}

return { label: account.email, tooltip: account.email };
}