diff --git a/CLAUDE.md b/CLAUDE.md
index 3d6e6c45..0305472c 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1,6 +1,6 @@
# Claude Code Working Guide
-_Last synced: v2.8.72 (2026-11-04) — an update puts back npm-rewritten lockfiles before its `--ff-only` merge; artifacts drag into folders._ A change that makes any statement in this file, `AGENTS.md`, or a README wrong — a moved path, a new or changed extension point, a retired rule — updates that file in the same change-set and bumps this marker. **Bump = replace the version/date and a ≤ 1-line summary; never append a changelog here** — details belong in the commit message and `docs/`, because this whole file is loaded into every session.
+_Last synced: v2.8.73 (2026-11-04) — Claude quota rows carry `accountUuid`; the Artifacts folder bar is restyled._ A change that makes any statement in this file, `AGENTS.md`, or a README wrong — a moved path, a new or changed extension point, a retired rule — updates that file in the same change-set and bumps this marker. **Bump = replace the version/date and a ≤ 1-line summary; never append a changelog here** — details belong in the commit message and `docs/`, because this whole file is loaded into every session.
## Project mission
- Maintain a reliable local-first dashboard for Claude Code session monitoring.
diff --git a/client/src/components/StatusBar.tsx b/client/src/components/StatusBar.tsx
index 9a5144b2..cf3b2895 100644
--- a/client/src/components/StatusBar.tsx
+++ b/client/src/components/StatusBar.tsx
@@ -29,7 +29,7 @@
* @author Nguyễn Ngọc Trí Vĩ
*/
-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(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([]);
+ const [fetched, setTools] = useState([]);
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 (
<>
diff --git a/client/src/components/__tests__/StatusBar.test.tsx b/client/src/components/__tests__/StatusBar.test.tsx
index 4401911b..71689013 100644
--- a/client/src/components/__tests__/StatusBar.test.tsx
+++ b/client/src/components/__tests__/StatusBar.test.tsx
@@ -162,7 +162,7 @@ vi.mock("../../lib/api", () => ({
}));
import { api } from "../../lib/api";
-import { StatusBar } from "../StatusBar";
+import { StatusBar, withoutHidden } from "../StatusBar";
import { LIVE_RUNS_EVENT } from "../WorkspaceTree";
const at = (url: string, ui: React.JSX.Element) =>
@@ -307,3 +307,35 @@ describe("StatusBar", () => {
expect(api.pricing.quota).toHaveBeenLastCalledWith(true);
});
});
+
+describe("withoutHidden", () => {
+ const row = (accountUuid: string, active: boolean, pct: number) => ({
+ id: "claude" as const,
+ active,
+ accountUuid,
+ account: `${accountUuid}@x.y`,
+ windows: [{ label: "5h", usedPercent: pct, resetsAt: null }],
+ });
+ const codex = { id: "codex" as const, active: true, windows: [] };
+
+ it("leaves the list alone when nothing is hidden", () => {
+ const list = [row("u1", true, 82), row("u2", false, 19), codex];
+ expect(withoutHidden(list, [])).toBe(list);
+ });
+
+ it("drops a hidden saved profile", () => {
+ const out = withoutHidden([row("u1", true, 82), row("u2", false, 19), codex], ["u2"]);
+ expect(out.map((q) => q.accountUuid ?? q.id)).toEqual(["u1", "codex"]);
+ });
+
+ it("puts the first visible profile, with its own usage, in place of a hidden live login", () => {
+ const out = withoutHidden(
+ [row("u1", true, 82), row("u2", false, 19), row("u3", false, 0), codex],
+ ["u1"]
+ );
+ expect(out.map((q) => q.accountUuid ?? q.id)).toEqual(["u2", "u3", "codex"]);
+ expect(out[0]?.active).toBe(true);
+ expect(out[0]?.windows[0]?.usedPercent).toBe(19);
+ expect(out[1]?.active).toBe(false);
+ });
+});
diff --git a/client/src/components/settings/ToolProfilesSettings.tsx b/client/src/components/settings/ToolProfilesSettings.tsx
index 4fce077f..b89f6d10 100644
--- a/client/src/components/settings/ToolProfilesSettings.tsx
+++ b/client/src/components/settings/ToolProfilesSettings.tsx
@@ -41,6 +41,7 @@ import { toast } from "../../lib/toast";
import {
profileKey,
readHiddenProfiles,
+ standIn,
toggleHidden,
writeHiddenProfiles,
} from "../../lib/hiddenProfilesStorage";
@@ -526,6 +527,17 @@ function ClaudeProfiles({ pooled, local }: { pooled: boolean; local: boolean })
writeHiddenProfiles(next);
};
const activeHidden = !!active && !reveal && isHidden(active);
+ /** Shown as the live login while the real one is hidden: the first visible
+ * profile, with its own usage — a "—" there would only draw the eye. */
+ const decoy =
+ activeHidden && items
+ ? standIn(
+ items.map((p) => ({ ...p, accountUuid: p.accountUuid ?? null })),
+ (p) => p.active || (!!active?.accountUuid && p.accountUuid === active.accountUuid),
+ hidden
+ )
+ : null;
+ const shownActive = (p: CcProfile) => (decoy ? p.name === decoy.name : p.active);
const load = useCallback(async () => {
try {
@@ -622,7 +634,9 @@ function ClaudeProfiles({ pooled, local }: { pooled: boolean; local: boolean })
{t("profiles.mismatch", {
config: mismatchEmail,
- owner: activeHidden ? "?" : active?.email || active?.accountUuid || "?",
+ owner: activeHidden
+ ? decoy?.email || "?"
+ : active?.email || active?.accountUuid || "?",
})}
@@ -639,7 +653,16 @@ function ClaudeProfiles({ pooled, local }: { pooled: boolean; local: boolean })
{t("profiles.activeLabel")}
{active && activeHidden ? (
- —
+ decoy ? (
+ <>
+
+ {decoy.email || decoy.displayName || decoy.accountUuid}
+
+
+ >
+ ) : (
+ {t("profiles.noActive")}
+ )
) : active ? (
<>
@@ -650,7 +673,7 @@ function ClaudeProfiles({ pooled, local }: { pooled: boolean; local: boolean })
) : (
{t("profiles.noActive")}
)}
- {active && !activeSaved && (
+ {active && !activeSaved && !activeHidden && (
{t("profiles.unsavedActive")}