mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-10-25 17:22:27 +08:00
Give each visual signal one meaning across pages.
Modes: only the mode being viewed gets the raised fill; the active mode
keeps its colour and dot. The Stack preview starts Stack from a row
("Start Stack with this as default…"), like the routing preview, so the
dialog no longer picks a default. The mode help moves next to the tabs,
the routing status drops the local address, the two-step explanation and
the "advanced feature" checkbox leave the dialogs, and added cards in
additive apps are white; grey now only marks the default provider.
Navigation: page-level switches use underline tabs (new PageTabs, also
used by Usage), with a smaller second level for OpenClaw config, Hermes
memory and Skills. Segmented controls stay for modes and in-page filters.
Hermes memory keeps unsaved edits when switching files.
Danger: irreversible confirmations use the red button; recoverable ones
(back to direct, remove from app, uninstall Skills, rebuild Codex usage,
delete env vars) no longer use red.
Pages:
- MCP/Skills matrix: one square checkbox shape for every state, and the
column header highlights and shows the app name on hover or focus.
- Skills: drop the update banner; "N updatable" in the header filters
the table and offers "Update all"; the unmanaged banner keeps only
Ignore; the installed count lives on the tab only.
- Add provider: "All" is split by category, names sorted within each;
versions split into plan and region, shown as two segmented rows when
they form a grid and as a dropdown otherwise.
- Usage: the trend chart shows the selected metric only; today's log
rows show the time only and the provider column gets the space.
- Sessions: the reader merges into the page header.
- Accounts: rows say how many providers use them, and removing an
account confirms first and names the affected providers.
- Apps: small neutral switches for "Show"; Settings says "Check for new
versions of your apps at launch".
Also add the provider empty state, a one-time "new layout" notice for
existing users, and a 1040px max width for card lists on wide windows.
128 lines
3.5 KiB
TypeScript
128 lines
3.5 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import type { Provider } from "@/types";
|
|
import {
|
|
findManagedAccountUsers,
|
|
groupUsersByApp,
|
|
} from "@/lib/managedAccountUsage";
|
|
|
|
const provider = (
|
|
id: string,
|
|
extra: Partial<Provider> = {},
|
|
settingsConfig: Record<string, unknown> = {},
|
|
): Provider =>
|
|
({
|
|
id,
|
|
name: id,
|
|
settingsConfig,
|
|
...extra,
|
|
}) as Provider;
|
|
|
|
const byId = (...list: Provider[]) =>
|
|
Object.fromEntries(list.map((p) => [p.id, p]));
|
|
|
|
describe("findManagedAccountUsers", () => {
|
|
const claude = byId(
|
|
// 指定了 a1
|
|
provider("bound-a1", {
|
|
meta: {
|
|
providerType: "github_copilot",
|
|
authBinding: {
|
|
source: "managed_account",
|
|
authProvider: "github_copilot",
|
|
accountId: "a1",
|
|
},
|
|
},
|
|
}),
|
|
// 旧字段 githubAccountId 指定了 a2
|
|
provider("legacy-a2", {
|
|
meta: { providerType: "github_copilot", githubAccountId: "a2" },
|
|
}),
|
|
// 没指定:跟着默认账号
|
|
provider("follows-default", { meta: { providerType: "github_copilot" } }),
|
|
// 没写 providerType 的旧卡按地址认
|
|
provider(
|
|
"by-url",
|
|
{},
|
|
{ env: { ANTHROPIC_BASE_URL: "https://api.githubcopilot.com" } },
|
|
),
|
|
// 别的服务
|
|
provider("xai", { meta: { providerType: "xai_oauth" } }),
|
|
// 普通供应商
|
|
provider("plain", {}, { env: { ANTHROPIC_BASE_URL: "https://x.test" } }),
|
|
);
|
|
|
|
it("counts bound providers and default followers for the default account", () => {
|
|
const users = findManagedAccountUsers("github_copilot", ["a1"], "a1", {
|
|
claude,
|
|
});
|
|
expect(users.map((u) => [u.providerId, u.viaDefault])).toEqual([
|
|
["bound-a1", false],
|
|
["follows-default", true],
|
|
["by-url", true],
|
|
]);
|
|
});
|
|
|
|
it("counts only bound providers for a non-default account", () => {
|
|
const users = findManagedAccountUsers("github_copilot", ["a2"], "a1", {
|
|
claude,
|
|
});
|
|
expect(users.map((u) => u.providerId)).toEqual(["legacy-a2"]);
|
|
});
|
|
|
|
it("collects every provider when removing all accounts", () => {
|
|
const users = findManagedAccountUsers(
|
|
"github_copilot",
|
|
["a1", "a2"],
|
|
"a1",
|
|
{ claude },
|
|
);
|
|
expect(users.map((u) => u.providerId)).toEqual([
|
|
"bound-a1",
|
|
"legacy-a2",
|
|
"follows-default",
|
|
"by-url",
|
|
]);
|
|
});
|
|
|
|
it("does not count an unbound Codex official card (it uses Codex's own login)", () => {
|
|
const codex = byId(
|
|
provider(
|
|
"codex-official",
|
|
{ category: "official", meta: { providerType: "codex_oauth" } },
|
|
{ auth: {}, config: "" },
|
|
),
|
|
provider(
|
|
"codex-managed",
|
|
{
|
|
category: "official",
|
|
meta: {
|
|
providerType: "codex_oauth",
|
|
authBinding: {
|
|
source: "managed_account",
|
|
authProvider: "codex_oauth",
|
|
accountId: "c1",
|
|
},
|
|
},
|
|
},
|
|
{ auth: {}, config: "" },
|
|
),
|
|
);
|
|
const desktop = byId(
|
|
provider("desktop-chatgpt", { meta: { providerType: "codex_oauth" } }),
|
|
);
|
|
|
|
const users = findManagedAccountUsers("codex_oauth", ["c1"], "c1", {
|
|
codex,
|
|
"claude-desktop": desktop,
|
|
});
|
|
expect(users.map((u) => [u.appId, u.providerId])).toEqual([
|
|
["claude-desktop", "desktop-chatgpt"],
|
|
["codex", "codex-managed"],
|
|
]);
|
|
expect(groupUsersByApp(users)).toEqual([
|
|
{ appId: "claude-desktop", names: ["desktop-chatgpt"] },
|
|
{ appId: "codex", names: ["codex-managed"] },
|
|
]);
|
|
});
|
|
});
|