From 59533b997bd1399314afd461b3bf6f15cdc68d76 Mon Sep 17 00:00:00 2001
From: nntrivi2001
Date: Wed, 4 Nov 2026 16:14:27 +0700
Subject: [PATCH] =?UTF-8?q?chore:=20v2.8.73=20=E2=80=94=20Artifacts=20fold?=
=?UTF-8?q?er=20bar=20restyled;=20hidden=20live=20profile=20gets=20a=20sta?=
=?UTF-8?q?nd-in?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 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.
---
CLAUDE.md | 2 +-
client/src/components/StatusBar.tsx | 46 +++++-
.../components/__tests__/StatusBar.test.tsx | 34 +++-
.../settings/ToolProfilesSettings.tsx | 149 +++++++++++-------
.../__tests__/ToolProfilesSettings.test.tsx | 13 ++
client/src/i18n/locales/en/artifacts.json | 2 +-
client/src/i18n/locales/vi/artifacts.json | 2 +-
client/src/lib/hiddenProfilesStorage.ts | 39 ++++-
client/src/lib/types.ts | 2 +
client/src/pages/Artifacts.tsx | 103 +++++++-----
.../screens.snapshot.test.tsx.snap | 2 +-
docs/API.md | 2 +-
package-lock.json | 4 +-
package.json | 2 +-
server/__tests__/quota.test.js | 2 +
server/lib/quota.js | 18 ++-
16 files changed, 305 insertions(+), 117 deletions(-)
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")}