0f17efe3b4
- Skill snapshot/upload: a key file's name (.env, *.pem, id_rsa, .netrc, .credentials.json…) is always refused, but a word like token/secret/ password/credential only in a data file's name — design-tokens.md or count_tokens.py is skill content, not a key. - System Health: off Linux the History, CPU threads and Busiest processes cards (all read from /proc) are left out instead of shown empty; the CPU bar without a sample and Windows' always-zero Load row go too. - Users pick their own pool account per tool (/api/me/pool-accounts, Settings → AI tools), within their groups, refused while pinned. - Admin user detail, 9router update, session replay and Antigravity catalog changes from parallel sessions.
121 lines
4.3 KiB
TypeScript
121 lines
4.3 KiB
TypeScript
/**
|
|
* @file PoolAccountSettings — Settings → AI tools → Pool account: in server
|
|
* mode, every user picks which account of the shared account pool their own
|
|
* new sessions run on, per tool, among the accounts their pool group may use
|
|
* (`GET/PUT /api/me/pool-accounts`, server/lib/account-pins.js `setChoice`).
|
|
* "Automatic" leaves it to the balancer; an admin's pin locks the choice. A
|
|
* Claude login goes into the user's home at once, so running panes follow it;
|
|
* Codex and Cursor take it at the next session. Renders nothing in local mode
|
|
* or when the pool holds no account the user may use.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { Lock } from "lucide-react";
|
|
import { api, type MyPoolTool, type PoolTool } from "../../lib/api";
|
|
import { toast } from "../../lib/toast";
|
|
import { usePermissions } from "../AuthGate";
|
|
import { AiToolIcon } from "../AiToolIcon";
|
|
import { SettingsRow, SettingsSection } from "./SettingsRow";
|
|
|
|
const LABEL: Record<PoolTool, string> = { claude: "Claude Code", codex: "Codex", cursor: "Cursor" };
|
|
|
|
export function PoolAccountSettings() {
|
|
const { t } = useTranslation("settings");
|
|
const { local } = usePermissions();
|
|
const [tools, setTools] = useState<MyPoolTool[] | null>(null);
|
|
const [busy, setBusy] = useState<PoolTool | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (local) return;
|
|
api.me
|
|
.poolAccounts()
|
|
.then((r) => setTools(r.tools))
|
|
.catch(() => setTools(null));
|
|
}, [local]);
|
|
|
|
if (local || !tools?.length) return null;
|
|
|
|
const choose = async (tool: PoolTool, account: string | null) => {
|
|
setBusy(tool);
|
|
try {
|
|
const r = await api.me.choosePoolAccount(tool, account);
|
|
setTools((list) =>
|
|
(list ?? []).map((x) => (x.tool === tool ? { ...x, choice: r.choice } : x))
|
|
);
|
|
toast.success(
|
|
!r.choice
|
|
? t("poolAccount.automaticSaved")
|
|
: r.switched === "switched" || r.switched === "kept"
|
|
? t("poolAccount.switchedNow", { account: r.choice })
|
|
: t("poolAccount.nextSession", { account: r.choice })
|
|
);
|
|
} catch (err) {
|
|
toast.error(err);
|
|
} finally {
|
|
setBusy(null);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<SettingsSection
|
|
id="pool-account"
|
|
title={t("poolAccount.title")}
|
|
description={t("poolAccount.description")}
|
|
>
|
|
{tools.map((x) => (
|
|
<SettingsRow
|
|
key={x.tool}
|
|
id={`pool-account-${x.tool}`}
|
|
label={
|
|
<span className="inline-flex items-center gap-1.5">
|
|
<AiToolIcon tool={{ id: x.tool, label: LABEL[x.tool] }} className="h-4 w-4" />
|
|
{LABEL[x.tool]}
|
|
</span>
|
|
}
|
|
description={
|
|
x.pin ? (
|
|
<span className="inline-flex items-center gap-1">
|
|
<Lock className="h-3 w-3" />
|
|
{t("poolAccount.pinned", { account: x.pin })}
|
|
</span>
|
|
) : (
|
|
t("poolAccount.count", { count: x.accounts.length })
|
|
)
|
|
}
|
|
disabled={!!x.pin}
|
|
control={
|
|
<select
|
|
aria-label={t("poolAccount.select", { tool: LABEL[x.tool] })}
|
|
value={x.pin ?? x.choice ?? ""}
|
|
disabled={!!x.pin || busy === x.tool}
|
|
onChange={(e) => void choose(x.tool, e.target.value || null)}
|
|
className="select !py-1 !text-xs max-w-[16rem]"
|
|
>
|
|
{!x.pin && <option value="">{t("poolAccount.automatic")}</option>}
|
|
{x.accounts.map((a) => (
|
|
<option key={a.name} value={a.name} disabled={a.expired && a.name !== x.choice}>
|
|
{[
|
|
a.name,
|
|
a.email && a.email !== a.name ? `(${a.email})` : null,
|
|
a.expired
|
|
? `— ${t("poolAccount.expired")}`
|
|
: a.full
|
|
? `— ${t("poolAccount.full")}`
|
|
: a.quotaPercent != null
|
|
? `— ${Math.round(a.quotaPercent)}%`
|
|
: null,
|
|
]
|
|
.filter(Boolean)
|
|
.join(" ")}
|
|
</option>
|
|
))}
|
|
</select>
|
|
}
|
|
/>
|
|
))}
|
|
</SettingsSection>
|
|
);
|
|
}
|