mirror of
https://github.com/musistudio/claude-code-router.git
synced 2026-08-30 17:11:12 +08:00
Add account refresh controls to tray views
This commit is contained in:
+2
-1
@@ -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) => {
|
||||
|
||||
@@ -223,6 +223,7 @@ export function runCommand(command, args, options = {}) {
|
||||
const child = spawn(command, args, {
|
||||
cwd: projectRoot,
|
||||
stdio: "inherit",
|
||||
shell: process.platform === "win32",
|
||||
...options
|
||||
});
|
||||
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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": "成功率",
|
||||
|
||||
Reference in New Issue
Block a user