chore: v2.8.73 — Artifacts folder bar restyled; hidden live profile gets a stand-in

- Artifacts: folders are rounded chips with a count badge; rename/delete are
  icons beside the picked folder (double-click renames); New folder is a
  dashed chip; the empty-folder hint mentions drag and drop.
- GET /api/pricing/quota: every Claude row carries its accountUuid.
- Settings → Profiles and the status bar's Usage panel: a profile hidden in
  this browser is left out of the Usage panel too; when the live login is
  hidden, the first visible profile is shown in its place (Logged in now,
  "in use", the % used chip with that profile's own usage). Display only.
This commit is contained in:
2026-11-04 16:14:27 +07:00
parent e21120d51d
commit 59533b997b
16 changed files with 305 additions and 117 deletions
+44 -2
View File
@@ -29,7 +29,7 @@
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
import { Fragment, useCallback, useEffect, useRef, useState } from "react";
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { useTranslation } from "react-i18next";
import { Link, useLocation } from "react-router-dom";
@@ -38,6 +38,12 @@ import { api, type RunHandle, type UserQuotaStatus } from "../lib/api";
import type { BudgetStatus, CostResult, ToolQuota } from "../lib/types";
import { fmt, fmtCostFull, formatDateTime, formatModelName, timeAgo } from "../lib/format";
import { AiToolIcon } from "./AiToolIcon";
import {
HIDDEN_PROFILES_EVENT,
isHiddenProfilesKey,
readHiddenProfiles,
standIn,
} from "../lib/hiddenProfilesStorage";
import { useLiveRefresh } from "../hooks/useLiveRefresh";
import { useSpin } from "../hooks/useSpin";
import { sessionChangeMatcher } from "../lib/sessionChange";
@@ -276,10 +282,44 @@ function QuotaPanel({
);
}
/** The profiles hidden in Settings → AI tools → Profiles, kept current. */
function useHiddenProfiles(): string[] {
const [hidden, setHidden] = useState<string[]>(readHiddenProfiles);
useEffect(() => {
const again = () => setHidden(readHiddenProfiles());
const onStorage = (e: StorageEvent) => isHiddenProfilesKey(e.key) && again();
window.addEventListener(HIDDEN_PROFILES_EVENT, again);
window.addEventListener("storage", onStorage);
return () => {
window.removeEventListener(HIDDEN_PROFILES_EVENT, again);
window.removeEventListener("storage", onStorage);
};
}, []);
return hidden;
}
/**
* Claude rows without the hidden profiles. When the live login is one of
* them, the first visible profile stands in as the live row — its own usage
* on the chip and the panel — so the panel never names the hidden account.
*/
export function withoutHidden(tools: ToolQuota[], hidden: string[]): ToolQuota[] {
if (!hidden.length) return tools;
const claude = tools.filter((q) => q.id === "claude");
const isLive = (q: ToolQuota) => q.active !== false;
const decoy = standIn(claude, isLive, hidden);
const keep = (q: ToolQuota) =>
q.id !== "claude" || !q.accountUuid || !hidden.includes(q.accountUuid);
const shown = tools.filter(keep);
if (!decoy) return shown;
// The stand-in leads the Claude rows, as the live login does.
return [{ ...decoy, active: true }, ...shown.filter((q) => q !== decoy)];
}
/** The right-end quota chips; nothing at all until a tool reports one. */
function QuotaChips() {
const { t } = useTranslation("nav");
const [tools, setTools] = useState<ToolQuota[]>([]);
const [fetched, setTools] = useState<ToolQuota[]>([]);
const [open, setOpen] = useState(false);
// Leaving the chips closes the panel only after a beat, and entering the
// panel cancels that: the pointer crosses the gap to read it.
@@ -303,6 +343,8 @@ function QuotaChips() {
);
const [refreshing, refresh] = useSpin(() => load(true));
useVisiblePoll(load, QUOTA_POLL_MS, [load]);
const hidden = useHiddenProfiles();
const tools = useMemo(() => withoutHidden(fetched, hidden), [fetched, hidden]);
if (!tools.length) return null;
return (
<>