Add account refresh controls to tray views

This commit is contained in:
musi
2026-06-25 09:20:36 +08:00
parent 502c59b7fb
commit e08b6ac601
8 changed files with 84 additions and 12 deletions
+2 -1
View File
@@ -229,7 +229,8 @@ await buildStyles({ minify: false });
const tailwindProcess = spawn(binPath("tailwindcss"), ["-i", cssInput, "-o", cssOutput, "--watch"], {
cwd: projectRoot,
stdio: "inherit"
stdio: "inherit",
shell: process.platform === "win32"
});
logDev(`Tailwind watcher started pid=${tailwindProcess.pid ?? "unknown"} input=${relativePath(cssInput)} output=${relativePath(cssOutput)}`);
tailwindProcess.on("exit", (code, signal) => {
+1
View File
@@ -223,6 +223,7 @@ export function runCommand(command, args, options = {}) {
const child = spawn(command, args, {
cwd: projectRoot,
stdio: "inherit",
shell: process.platform === "win32",
...options
});
+26 -1
View File
@@ -23,6 +23,7 @@ export function TrayApp() {
const [selectedProvider, setSelectedProvider] = useState<string>();
const [snapshots, setSnapshots] = useState<SnapshotMap>(emptySnapshots);
const [accountSnapshots, setAccountSnapshots] = useState<ProviderAccountSnapshot[]>([]);
const [accountRefreshing, setAccountRefreshing] = useState(false);
const [trayWidgets, setTrayWidgets] = useState<TrayWidgetConfig[]>(DEFAULT_TRAY_WIDGETS);
const [selectedRange, setSelectedRange] = useState<TrayHeaderRange>("30d");
@@ -60,6 +61,24 @@ export function TrayApp() {
}
}, [formatError, selectedProvider]);
const refreshAccountSnapshots = useCallback(async () => {
if (!window.ccr) {
setAccountSnapshots([]);
return;
}
setAccountRefreshing(true);
setError("");
try {
const accounts = await window.ccr.getProviderAccountSnapshots(selectedProvider, { forceRefresh: true });
setAccountSnapshots(accounts);
} catch (nextError) {
setError(formatError(nextError));
} finally {
setAccountRefreshing(false);
}
}, [formatError, selectedProvider]);
useEffect(() => {
document.body.classList.add("tray-window");
const closeOnEscape = (event: KeyboardEvent) => {
@@ -117,6 +136,7 @@ export function TrayApp() {
{trayWidgets.map((widget, index) => (
<TrayRuntimeWidget
accountSnapshots={accountSnapshots}
accountRefreshing={accountRefreshing}
activeStats={activeStats}
activeTotals={activeTotals}
index={index}
@@ -127,6 +147,7 @@ export function TrayApp() {
topModel={topModel}
widget={widget}
onChangeRange={setSelectedRange}
onRefreshAccount={refreshAccountSnapshots}
onSelectProvider={setSelectedProvider}
/>
))}
@@ -148,6 +169,7 @@ export function TrayApp() {
function TrayRuntimeWidget({
accountSnapshots,
accountRefreshing,
activeStats,
activeTotals,
index,
@@ -157,9 +179,11 @@ function TrayRuntimeWidget({
topModel,
widget,
onChangeRange,
onRefreshAccount,
onSelectProvider
}: {
accountSnapshots: ProviderAccountSnapshot[];
accountRefreshing: boolean;
activeStats: SnapshotMap["30d"];
activeTotals: UsageTotals;
index: number;
@@ -169,6 +193,7 @@ function TrayRuntimeWidget({
topModel?: UsageComparisonRow;
widget: TrayWidgetConfig;
onChangeRange: (range: TrayHeaderRange) => void;
onRefreshAccount: () => void | Promise<void>;
onSelectProvider: (provider?: string) => void;
}) {
const t = useTrayText();
@@ -190,7 +215,7 @@ function TrayRuntimeWidget({
}
if (widget.type === "account") {
return <AccountSummaryPanel snapshots={accountSnapshots} variant={(widget.variant ?? defaultTrayWidgetVariant("account")) as TrayComponentVariants["account"]} />;
return <AccountSummaryPanel refreshing={accountRefreshing} snapshots={accountSnapshots} variant={(widget.variant ?? defaultTrayWidgetVariant("account")) as TrayComponentVariants["account"]} onRefresh={onRefreshAccount} />;
}
if (widget.type === "token-flow") {
+20 -1
View File
@@ -15,6 +15,7 @@ export function TrayDetailApp({ provider }: { provider?: string }) {
const [range, setRange] = useState<UsageStatsRange>("30d");
const [snapshots, setSnapshots] = useState<SnapshotMap>(emptySnapshots);
const [accountSnapshots, setAccountSnapshots] = useState<ProviderAccountSnapshot[]>([]);
const [accountRefreshing, setAccountRefreshing] = useState(false);
const [trayWidgets, setTrayWidgets] = useState<TrayWidgetConfig[]>(DEFAULT_TRAY_WIDGETS);
const refresh = useCallback(async () => {
@@ -46,6 +47,24 @@ export function TrayDetailApp({ provider }: { provider?: string }) {
}
}, [formatError, provider]);
const refreshAccountSnapshots = useCallback(async () => {
if (!window.ccr) {
setAccountSnapshots([]);
return;
}
setAccountRefreshing(true);
setError("");
try {
const accounts = await window.ccr.getProviderAccountSnapshots(provider, { forceRefresh: true });
setAccountSnapshots(accounts);
} catch (nextError) {
setError(formatError(nextError));
} finally {
setAccountRefreshing(false);
}
}, [formatError, provider]);
useEffect(() => {
document.body.classList.add("tray-window");
const closeOnEscape = (event: KeyboardEvent) => {
@@ -75,7 +94,7 @@ export function TrayDetailApp({ provider }: { provider?: string }) {
className="h-screen w-screen overflow-y-auto rounded-[14px] border border-slate-950/15 bg-slate-950 p-3 text-slate-100 shadow-[0_18px_42px_rgba(15,23,42,.28)]"
>
<TrayStatusStrip totalTokens={snapshots[range].totals.totalTokens} />
<UsageDetailPanel activeStats={snapshots[range]} accountSnapshots={accountSnapshots} provider={provider} range={range} widgets={trayWidgets} onRangeChange={setRange} />
<UsageDetailPanel activeStats={snapshots[range]} accountRefreshing={accountRefreshing} accountSnapshots={accountSnapshots} provider={provider} range={range} widgets={trayWidgets} onRefreshAccount={refreshAccountSnapshots} onRangeChange={setRange} />
{loading ? <div className="mt-2 text-[11px] font-medium text-slate-200/60">{t("Syncing usage...")}</div> : null}
{error ? <div className="mt-3 rounded-lg border border-rose-400/24 bg-rose-500/18 px-3 py-2 text-[12px] font-medium text-rose-100">{error}</div> : null}
</main>
@@ -1,14 +1,18 @@
import {
accountMetersForDisplay, accountProgressClass, accountProgressColor, accountSnapshotLabel, accountStatusClass, compareAccountSnapshots, formatAccountMeterTitle, formatAccountMeterValue,
meterProgress, meterRemainingRatio, ProviderAccountMeter, ProviderAccountSnapshot, TrayComponentVariants,
LoaderCircle, meterProgress, meterRemainingRatio, ProviderAccountMeter, ProviderAccountSnapshot, RefreshCw, TrayComponentVariants,
useTrayText
} from "../shared";
import { RadialMetric } from "./widgets";
export function AccountSummaryPanel({
onRefresh,
refreshing = false,
snapshots,
variant
}: {
onRefresh?: () => void | Promise<void>;
refreshing?: boolean;
snapshots: ProviderAccountSnapshot[];
variant: TrayComponentVariants["account"];
}) {
@@ -32,9 +36,18 @@ export function AccountSummaryPanel({
<div className="rounded-[8px] border border-white/10 bg-white/[.04] p-2">
<div className="mb-2 flex min-w-0 items-center justify-between gap-2">
<h3 className="truncate text-[11px] font-bold text-slate-100">{accountSnapshotLabel(snapshot)}</h3>
<span className={`shrink-0 rounded-full px-1.5 py-0.5 text-[9px] font-bold ${accountStatusClass(snapshot.status)}`}>
{t(snapshot.status)}
</span>
<button
aria-label={t("Refresh")}
className={`flex h-5 w-5 shrink-0 items-center justify-center rounded-full border transition disabled:cursor-not-allowed disabled:opacity-50 ${accountStatusButtonClass(snapshot.status)}`}
disabled={refreshing || !onRefresh}
onClick={() => {
void onRefresh?.();
}}
title={t("Refresh")}
type="button"
>
{refreshing ? <LoaderCircle className="h-3 w-3 animate-spin" /> : <RefreshCw className="h-3 w-3" />}
</button>
</div>
{meters.length > 0 ? (
<AccountMeters meters={meters} status={snapshot.status} variant={variant} />
@@ -45,6 +58,10 @@ export function AccountSummaryPanel({
);
}
function accountStatusButtonClass(status: ProviderAccountSnapshot["status"]): string {
return `${accountStatusClass(status)} hover:border-white/20 hover:bg-white/[.08] hover:text-slate-50`;
}
function AccountMeters({
meters,
status,
@@ -8,16 +8,19 @@ import { AnimatedUsageChart, ChartShell, ModelShareChart, RingMetrics, StatsGrid
export function UsageOverviewPanel({
activeStats,
accountSnapshots,
accountRefreshing,
componentVariants,
loading,
modules,
monthTotals,
todayTotals,
topModel,
weekTotals
weekTotals,
onRefreshAccount
}: {
activeStats: UsageStatsSnapshot;
accountSnapshots: ProviderAccountSnapshot[];
accountRefreshing?: boolean;
componentVariants: TrayComponentVariants;
loading: boolean;
modules: ReadonlySet<TrayWindowModuleId>;
@@ -25,6 +28,7 @@ export function UsageOverviewPanel({
todayTotals: UsageTotals;
topModel?: UsageComparisonRow;
weekTotals: UsageTotals;
onRefreshAccount?: () => void | Promise<void>;
}) {
const t = useTrayText();
const showTokenMix = modules.has("token-mix");
@@ -32,7 +36,7 @@ export function UsageOverviewPanel({
return (
<section className="space-y-2">
{modules.has("account") ? <AccountSummaryPanel snapshots={accountSnapshots} variant={componentVariants.account} /> : null}
{modules.has("account") ? <AccountSummaryPanel refreshing={accountRefreshing} snapshots={accountSnapshots} variant={componentVariants.account} onRefresh={onRefreshAccount} /> : null}
{modules.has("token-flow") ? (
<ChartShell
@@ -8,16 +8,20 @@ import { AnimatedUsageChart, ChartShell, ModelShareChart, RangeSwitch, RingMetri
export function UsageDetailPanel({
activeStats,
accountSnapshots,
accountRefreshing,
provider,
range,
widgets,
onRefreshAccount,
onRangeChange
}: {
activeStats: UsageStatsSnapshot;
accountSnapshots: ProviderAccountSnapshot[];
accountRefreshing?: boolean;
provider?: string;
range: UsageStatsRange;
widgets: TrayWidgetConfig[];
onRefreshAccount?: () => void | Promise<void>;
onRangeChange: (range: UsageStatsRange) => void;
}) {
const t = useTrayText();
@@ -57,7 +61,7 @@ export function UsageDetailPanel({
);
}
if (widget.type === "account") {
return <AccountSummaryPanel key={`${widget.id}-${index}`} snapshots={accountSnapshots} variant={(widget.variant ?? defaultTrayWidgetVariant("account")) as TrayComponentVariants["account"]} />;
return <AccountSummaryPanel key={`${widget.id}-${index}`} refreshing={accountRefreshing} snapshots={accountSnapshots} variant={(widget.variant ?? defaultTrayWidgetVariant("account")) as TrayComponentVariants["account"]} onRefresh={onRefreshAccount} />;
}
if (widget.type === "token-flow") {
return (
+3 -2
View File
@@ -1,6 +1,6 @@
import { createContext, useCallback, useContext, useEffect, useMemo, useState, type ReactNode } from "react";
import { createRoot } from "react-dom/client";
import { Power } from "lucide-react";
import { LoaderCircle, Power, RefreshCw } from "lucide-react";
import appLogoUrl from "../../../../assets/logo.png";
import trayCyanIconUrl from "../../../../assets/tray-cyan.png";
import trayOrangeIconUrl from "../../../../assets/tray-orange.png";
@@ -26,7 +26,7 @@ import type {
export {
createContext, useCallback, useContext, useEffect, useMemo, useState, createRoot,
Power, appLogoUrl, trayCyanIconUrl, trayOrangeIconUrl, trayVioletIconUrl, DEFAULT_TRAY_COMPONENT_VARIANTS, DEFAULT_TRAY_WIDGETS, DEFAULT_TRAY_WINDOW_MODULES, TRAY_SINGLETON_WIDGET_TYPES, TRAY_TOP_WIDGET_TYPES, TRAY_WINDOW_MODULE_IDS
LoaderCircle, Power, RefreshCw, appLogoUrl, trayCyanIconUrl, trayOrangeIconUrl, trayVioletIconUrl, DEFAULT_TRAY_COMPONENT_VARIANTS, DEFAULT_TRAY_WIDGETS, DEFAULT_TRAY_WINDOW_MODULES, TRAY_SINGLETON_WIDGET_TYPES, TRAY_TOP_WIDGET_TYPES, TRAY_WINDOW_MODULE_IDS
};
export type {
ReactNode, AppConfig, ProviderAccountMeter, ProviderAccountSnapshot, TrayBalanceProgressConfig, TrayComponentVariants, TrayWidgetConfig, TrayWidgetType, TrayWidgetVariant, TrayWindowModuleId, UsageComparisonRow,
@@ -85,6 +85,7 @@ export const trayText: Record<ResolvedLanguage, Record<string, string>> = {
"Overview": "概览",
"Open CCR": "打开 CCR",
"Quit": "退出",
"Refresh": "刷新",
"Subscription": "订阅",
"Success": "成功",
"Success rate": "成功率",