diff --git a/build/dev.mjs b/build/dev.mjs index bfb50dee..05115924 100644 --- a/build/dev.mjs +++ b/build/dev.mjs @@ -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) => { diff --git a/build/esbuild.config.mjs b/build/esbuild.config.mjs index 1cbdd748..3507935a 100644 --- a/build/esbuild.config.mjs +++ b/build/esbuild.config.mjs @@ -223,6 +223,7 @@ export function runCommand(command, args, options = {}) { const child = spawn(command, args, { cwd: projectRoot, stdio: "inherit", + shell: process.platform === "win32", ...options }); diff --git a/src/renderer/pages/tray/TrayApp.tsx b/src/renderer/pages/tray/TrayApp.tsx index e3e405c1..45922060 100644 --- a/src/renderer/pages/tray/TrayApp.tsx +++ b/src/renderer/pages/tray/TrayApp.tsx @@ -23,6 +23,7 @@ export function TrayApp() { const [selectedProvider, setSelectedProvider] = useState(); const [snapshots, setSnapshots] = useState(emptySnapshots); const [accountSnapshots, setAccountSnapshots] = useState([]); + const [accountRefreshing, setAccountRefreshing] = useState(false); const [trayWidgets, setTrayWidgets] = useState(DEFAULT_TRAY_WIDGETS); const [selectedRange, setSelectedRange] = useState("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) => ( ))} @@ -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; onSelectProvider: (provider?: string) => void; }) { const t = useTrayText(); @@ -190,7 +215,7 @@ function TrayRuntimeWidget({ } if (widget.type === "account") { - return ; + return ; } if (widget.type === "token-flow") { diff --git a/src/renderer/pages/tray/TrayDetailApp.tsx b/src/renderer/pages/tray/TrayDetailApp.tsx index 2155273f..af0627e2 100644 --- a/src/renderer/pages/tray/TrayDetailApp.tsx +++ b/src/renderer/pages/tray/TrayDetailApp.tsx @@ -15,6 +15,7 @@ export function TrayDetailApp({ provider }: { provider?: string }) { const [range, setRange] = useState("30d"); const [snapshots, setSnapshots] = useState(emptySnapshots); const [accountSnapshots, setAccountSnapshots] = useState([]); + const [accountRefreshing, setAccountRefreshing] = useState(false); const [trayWidgets, setTrayWidgets] = useState(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)]" > - + {loading ?
{t("Syncing usage...")}
: null} {error ?
{error}
: null} diff --git a/src/renderer/pages/tray/components/account-panel.tsx b/src/renderer/pages/tray/components/account-panel.tsx index 789302de..75b17f41 100644 --- a/src/renderer/pages/tray/components/account-panel.tsx +++ b/src/renderer/pages/tray/components/account-panel.tsx @@ -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; + refreshing?: boolean; snapshots: ProviderAccountSnapshot[]; variant: TrayComponentVariants["account"]; }) { @@ -32,9 +36,18 @@ export function AccountSummaryPanel({

{accountSnapshotLabel(snapshot)}

- - {t(snapshot.status)} - +
{meters.length > 0 ? ( @@ -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, diff --git a/src/renderer/pages/tray/components/overview-panel.tsx b/src/renderer/pages/tray/components/overview-panel.tsx index f0877e8c..c026f306 100644 --- a/src/renderer/pages/tray/components/overview-panel.tsx +++ b/src/renderer/pages/tray/components/overview-panel.tsx @@ -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; @@ -25,6 +28,7 @@ export function UsageOverviewPanel({ todayTotals: UsageTotals; topModel?: UsageComparisonRow; weekTotals: UsageTotals; + onRefreshAccount?: () => void | Promise; }) { const t = useTrayText(); const showTokenMix = modules.has("token-mix"); @@ -32,7 +36,7 @@ export function UsageOverviewPanel({ return (
- {modules.has("account") ? : null} + {modules.has("account") ? : null} {modules.has("token-flow") ? ( void | Promise; onRangeChange: (range: UsageStatsRange) => void; }) { const t = useTrayText(); @@ -57,7 +61,7 @@ export function UsageDetailPanel({ ); } if (widget.type === "account") { - return ; + return ; } if (widget.type === "token-flow") { return ( diff --git a/src/renderer/pages/tray/shared.tsx b/src/renderer/pages/tray/shared.tsx index 54fd6920..7b98b62f 100644 --- a/src/renderer/pages/tray/shared.tsx +++ b/src/renderer/pages/tray/shared.tsx @@ -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> = { "Overview": "概览", "Open CCR": "打开 CCR", "Quit": "退出", + "Refresh": "刷新", "Subscription": "订阅", "Success": "成功", "Success rate": "成功率",