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
186 changes: 186 additions & 0 deletions assets/RemixIcon-License.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
Remix Icon License v1.0
Version 1.0 – January 2026
Copyright (c) 2017–2026 Remix Design

This License governs the use of the Remix Icon library, including all icons,
SVG files, font files, and related assets (collectively, the "Icons").

1. Grant of License

Subject to the terms and conditions of this License, Remix Design grants you
a worldwide, royalty-free, non-exclusive license to use, copy, modify,
merge, and distribute the Icons, as permitted below.

2. Permitted Uses

You are permitted to:

2.1 Commercial and Non-Commercial Use
Use the Icons in personal, commercial, and client projects, including
but not limited to:
- Websites, web applications, and mobile applications
- Software products and SaaS platforms
- User interface designs, design systems, and UI kits
- Presentations, documentation, and marketing materials
- Physical or digital products where the Icons are decorative or
functional elements

2.2 Modification and Derivative Works
- Modify, adapt, or customize the Icons
- Change colors, sizes, stroke styles, or other visual attributes
to fit your project needs

2.3 Distribution as Part of a Larger Work
Include the Icons (modified or unmodified) as part of a larger product,
provided that:
- The Icons are functional or decorative components of the product, and
- The Icons are not marketed or sold as the primary value of the product

2.4 Attribution (Optional)
Attribution to Remix Icon is appreciated but not required.
Suggested attribution:
"Icons by Remix Icon (https://remixicon.com)"

3. Restrictions

3.1 Prohibited: Standalone Icon Sales
You may NOT:
- Sell, sublicense, or distribute the Icons as a standalone product
- Package the Icons as an independent icon pack, icon library, or
icon font for sale
- Offer the Icons as a downloadable asset collection where the Icons
themselves constitute the primary product value

Clarification:
A product is considered a prohibited standalone icon sale if:
- The Icons are the primary advertised feature, or
- Removing the Icons would eliminate the core value of the product

Examples:
- Prohibited: "Premium Icon Pack – 2,000+ Icons for $29"
- Prohibited: "Extended Remix Icon Library (Paid)"
- Permitted: Website or app templates that include Icons as UI elements
- Permitted: Design systems or UI kits where Icons are a minor component

3.2 Prohibited: Competing Icon Libraries
You may NOT use the Icons to create, distribute, or sell a competing
icon library or icon set, even if the Icons are modified with only
minor or superficial changes (such as color, size, stroke width,
corner radius, or layout).

3.3 Prohibited: Logo, Trademark, and Brand Identity Use
You may NOT use the Icons, whether modified or unmodified, as:
- A logo, trademark, or brand identifier
- The primary visual identity of a company, product, or service

This includes, but is not limited to:
- Creating a logo by applying simple modifications such as color changes,
rotation, combination, or layout
- Using an Icon as a brand mark, app icon, or identity symbol where the
Icon itself represents the brand

Rationale:
Remix Icon is designed for functional and informational use. Using the
Icons as brand identifiers may impair their recognizability and neutrality
in other functional contexts.

4. Brand Icons and Trademarks

Some Icons represent third-party brands or services.

4.1 Permitted Use of Brand Icons
You may use brand icons solely to represent or link to the corresponding
brand, product, or service.
Examples:
- Using a GitHub icon to link to a GitHub repository
- Displaying social media icons in a website footer

4.2 Prohibited Use of Brand Icons
You may NOT:
- Use brand icons as your own logo or trademark
- Modify brand icons to create a new brand identity
- Use brand icons in a way that implies endorsement or affiliation
by the brand owner
- Use brand icons for purposes unrelated to the represented brand

4.3 Trademark Responsibility
All brand icons are subject to the trademark rights of their respective
owners. This License does not grant any trademark rights.
Users are responsible for ensuring that their use of brand icons complies
with applicable trademark laws.

5. Copyright and License Notice

When distributing the complete Icon library or substantial portions thereof:

- You must retain existing copyright notices in source files
- You should include a copy of this License or a link to it

For individual use of Icons within projects (e.g., compiled apps, websites,
presentations), no copyright notice is required.

6. No Trademark Rights

This License does not grant permission to use:

- The "Remix Icon" name, logo, or trademarks
- Any trademarks represented by brand icons

You may refer to Remix Icon by name solely for attribution purposes.

7. Disclaimer of Warranty

THE ICONS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT.

8. Limitation of Liability

IN NO EVENT SHALL REMIX DESIGN BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING
FROM, OUT OF, OR IN CONNECTION WITH THE ICONS OR THEIR USE.

9. License Compatibility

You may integrate the Icons into projects under permissive licenses
(such as MIT, BSD, or Apache 2.0), provided that:

- The Icons themselves remain governed by this License
- The restrictions in Section 3 continue to apply

This License may be incompatible with licenses that require unrestricted
redistribution of assets as standalone products or mandatory application
of strong copyleft or share-alike terms.

10. Termination

Your rights under this License terminate automatically if you breach its
terms.

Upon termination:

- You must cease any distribution that violates this License
- You may continue to use the Icons in projects already distributed
prior to termination
- End users who received such projects are not affected

11. License Versioning

Remix Design may publish revised versions of this License.

You may choose to use the Icons under:

- The version of this License under which you received them, or
- Any later version published by Remix Design

Once chosen, that version may be used indefinitely.

How to Apply This License

Include the following notice in your documentation when appropriate:

"This project uses icons from Remix Icon (https://remixicon.com),
licensed under the Remix Icon License v1.0."

When distributing the complete Icon library or substantial portions of it,
include a copy of this License.
Binary file modified assets/readme-demo-zh-CN.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/readme-demo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions assets/remix-earth-line.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 11 additions & 8 deletions docs/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,14 @@ wide desktop with enough card width, then collapse to two and one card without i
Focused dialogs may handle add-account, explicit confirmation, progress/results,
or recovery; they do not create a second navigation system.

The toolbar contains the current Codex account, Refresh, Wake all, Add account,
and a compact language menu. It is a command bar, not a dashboard header
or a custom window chrome. Its status dot always remains visible; only a long
account label may truncate. The status is also written in text so current, unknown,
signed-out, setup, and recovery states do not rely on color.
The toolbar contains the vertically centered GSwitch mark and name, Refresh,
Wake all, Add account, and a compact language menu. The current account is
identified on its card rather than repeated beside the brand. The language
button uses Remix Icon's `earth-line` at 18px, inheriting the current theme
color. It is a command bar, not a dashboard header or custom window chrome.
Unknown, setup, and recovery states use the existing actionable notice. With
saved accounts, a signed-out Codex uses one quiet text notice without another
button; without saved accounts, the empty state provides the next step.

An account card should show only the state needed for a decision:

Expand Down Expand Up @@ -90,7 +93,7 @@ points to the official install guide. A failed check remains quiet; no update
is installed without a click.

- Do not optimistically display a switch or redemption as complete.
- After a verified switch, update the toolbar identity and active card directly.
- After a verified switch, update the active card directly.
That state is the visible confirmation; do not insert a success banner above
the account grid or reload the whole workspace before showing it. Other brief
success and information messages use one compact floating notice without
Expand Down Expand Up @@ -147,8 +150,8 @@ is installed without a click.
**Sign in again** its primary action. The focused dialog shows the saved
email and workspace, offers Copy email, and updates the original record only
after verifying the returning identity. A saved login for the current account
that could not be applied shows **Apply**. The toolbar identifies the selected
local Codex account without claiming that its remote sign-in is valid.
that could not be applied shows **Apply**. The current badge identifies the
selected local Codex account without claiming that its remote sign-in is valid.
- After Wake, sign-in, or refresh, update affected cards in place. Preserve
order and last known quota while a new provider projection is pending; old
results cannot replace a newer login or clear the entire account grid.
Expand Down
5 changes: 3 additions & 2 deletions docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,8 +247,9 @@ as applicable:
and aggregate-only command output;
- Add Account import-first hierarchy, account selection controls, selected Wake,
unencrypted-export confirmation, and result feedback without credential data.
- the toolbar's visible ready indicator beside a separately truncatable account
label, plus the canonical SVG and generated native/installer icon dimensions.
- the toolbar's centered brand without a duplicate current-account label,
current-card identification, signed-out and recovery notices, theme-aware
language icon, plus the canonical SVG and generated native/installer icon dimensions.
- system-language selection, unsupported-locale fallback, immediate manual
language selection, language preference persistence, locale-aware reset-time
formatting, and the main account flow in Simplified Chinese.
Expand Down
28 changes: 23 additions & 5 deletions src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -392,18 +392,36 @@ describe("GSwitch account workspace", () => {
await waitFor(() => expect(dialog).not.toBeInTheDocument());
});

it("keeps the current account label visible without claiming sign-in readiness", async () => {
it("shows the current account on its card without repeating it in the toolbar", async () => {
const account = { ...chatAccount, email: "a-very-long-account-name-that-must-truncate@example.com", active: true };
mocks.listAccounts.mockResolvedValue([account]);
mocks.liveAccount.mockResolvedValue({
status: "ready",
credential_store: "file",
account: { label: "a-very-long-account-name-that-must-truncate@example.com" },
account,
});
const { container } = render(<App />);

const card = (await screen.findByRole("heading", { name: account.email })).closest("article")!;
expect(within(card).getByText("Current", { selector: ".badge-active" })).toBeInTheDocument();
expect(container.querySelector("header")).not.toHaveTextContent(account.email);
expect(within(container.querySelector("header")!).getByRole("heading", { name: "GSwitch" })).toBeInTheDocument();
});

it("shows a signed-out notice with saved accounts and no extra action", async () => {
mocks.listAccounts.mockResolvedValue([chatAccount]);
render(<App />);
const notice = await screen.findByText("Codex is signed out");
expect(notice).toHaveAttribute("role", "status");
expect(notice.querySelector("button")).toBeNull();
expect(screen.getByRole("button", { name: "Switch to person@example.com" })).toBeInTheDocument();
});

it("uses the empty state without a duplicate signed-out notice", async () => {
render(<App />);
await screen.findByRole("heading", { name: "0 saved accounts" });
expect(container.querySelector(".brand-status > .status-dot")).toBeInTheDocument();
expect(container.querySelector(".brand-status > .status-ready")).not.toBeInTheDocument();
expect(container.querySelector(".brand-status-label")).toHaveTextContent("a-very-long-account-name-that-must-truncate@example.com");
expect(screen.getByRole("heading", { name: "Add your Codex accounts" })).toBeInTheDocument();
expect(screen.queryByText("Codex is signed out")).not.toBeInTheDocument();
});

it("does not scan local account sources at startup or when the method row opens", async () => {
Expand Down
24 changes: 6 additions & 18 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2018,14 +2018,6 @@ export default function App() {
const cliUpdateAvailable = cliInfo?.update_status === "available" && cliInfo.supports_update;
const currentActiveId = live?.account?.id;
refreshInputs.current = { accounts, quotas, quotaFailures, activeId: currentActiveId };
const liveAccountLabel = live?.account ? accountPrimaryName(live.account) : t("toolbar.currentAccount");
const liveStatusLabel = !live ? t("toolbar.statusChecking") : t(({
ready: "toolbar.statusReady",
not_signed_in: "toolbar.statusSignedOut",
unknown_account: "toolbar.statusUnknown",
file_store_required: "toolbar.statusFileStore",
recovery_required: "toolbar.statusRecovery",
} as const)[live.status]);
const chatGptAccounts = accounts.filter((account) => account.kind === "chat_gpt");
const selectedChatGptCount = accounts.filter(
(account) => account.kind === "chat_gpt" && selectedAccountIds.includes(account.id),
Expand Down Expand Up @@ -2113,15 +2105,7 @@ export default function App() {
<header className="toolbar">
<div className="brand-lockup">
<img alt="" aria-hidden="true" className="brand-mark" src="/gswitch-icon.svg" />
<div className="brand-copy">
<h1>GSwitch</h1>
<p aria-atomic="true" aria-live="polite" className="brand-status">
<span aria-hidden="true" className="status-dot" />
<span className="brand-status-label">{liveAccountLabel}</span>
<span aria-hidden="true">·</span>
<span className="brand-status-state">{liveStatusLabel}</span>
</p>
</div>
<h1>GSwitch</h1>
</div>
<div className="toolbar-actions">
<button className="button button-quiet" disabled={loading || busy !== null || refreshProgress !== null || storageRecovery} onClick={() => void refreshAll()} type="button">
Expand Down Expand Up @@ -2159,7 +2143,7 @@ export default function App() {
) : null}
<PopoverMenu
className="language-menu"
icon={<Globe2 size={18} />}
icon={<span aria-hidden="true" className="language-menu-icon" />}
label={t("toolbar.language")}
popoverLabel={t("settings.language")}
title={t("toolbar.language")}
Expand Down Expand Up @@ -2213,6 +2197,10 @@ export default function App() {
</section>
) : null}

{!safetyNotice && live?.status === "not_signed_in" && accounts.length > 0 ? (
<p className="signed-out-notice" role="status">{t("safety.signedOut")}</p>
) : null}

<section className="accounts-section" aria-labelledby="accounts-heading">
<div className="section-heading">
<div className="section-heading-main">
Expand Down
16 changes: 2 additions & 14 deletions src/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,13 +94,7 @@ const en = {
"toolbar.language": "Language",
"toolbar.codexCli": "Codex CLI",
"toolbar.wakeAll": "Wake all",
"toolbar.currentAccount": "Current Codex account",
"toolbar.statusChecking": "Checking",
"toolbar.statusReady": "Current",
"toolbar.statusSignedOut": "Signed out",
"toolbar.statusUnknown": "Unknown account",
"toolbar.statusFileStore": "Setup required",
"toolbar.statusRecovery": "Recovery required",
"safety.signedOut": "Codex is signed out",
"cli.checking": "Checking Codex CLI…",
"cli.version": "Codex CLI {version}",
"cli.upToDate": "Codex CLI {version} · up to date",
Expand Down Expand Up @@ -469,13 +463,7 @@ const zhCN: Record<keyof typeof en, string> = {
"toolbar.language": "语言",
"toolbar.codexCli": "Codex CLI",
"toolbar.wakeAll": "全部唤醒",
"toolbar.currentAccount": "当前 Codex 账户",
"toolbar.statusChecking": "检查中",
"toolbar.statusReady": "当前账户",
"toolbar.statusSignedOut": "未登录",
"toolbar.statusUnknown": "未知账户",
"toolbar.statusFileStore": "需要设置",
"toolbar.statusRecovery": "需要恢复",
"safety.signedOut": "Codex 未登录",
"cli.checking": "正在检查 Codex CLI…",
"cli.version": "Codex CLI {version}",
"cli.upToDate": "Codex CLI {version} · 已是最新",
Expand Down
Loading
Loading