fix: Update error handling in ClaudeUsagePopover and improve type safety in app-store

This commit is contained in:
Mohamad Yahia
2025-12-21 10:30:06 +04:00
parent 6028889909
commit 5be85a45b1
2 changed files with 64 additions and 30 deletions

View File

@@ -17,37 +17,65 @@ import { cn } from "@/lib/utils";
import { getElectronAPI } from "@/lib/electron"; import { getElectronAPI } from "@/lib/electron";
import { useAppStore } from "@/store/app-store"; import { useAppStore } from "@/store/app-store";
// Error codes for distinguishing failure modes
const ERROR_CODES = {
API_BRIDGE_UNAVAILABLE: "API_BRIDGE_UNAVAILABLE",
AUTH_ERROR: "AUTH_ERROR",
UNKNOWN: "UNKNOWN",
} as const;
type ErrorCode = (typeof ERROR_CODES)[keyof typeof ERROR_CODES];
type UsageError = {
code: ErrorCode;
message: string;
};
export function ClaudeUsagePopover() { export function ClaudeUsagePopover() {
const { claudeRefreshInterval, claudeUsage, claudeUsageLastUpdated, setClaudeUsage } = const { claudeRefreshInterval, claudeUsage, claudeUsageLastUpdated, setClaudeUsage } =
useAppStore(); useAppStore();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<UsageError | null>(null);
// Check if data is stale (older than 2 minutes) - recalculates when claudeUsageLastUpdated changes // Check if data is stale (older than 2 minutes) - recalculates when claudeUsageLastUpdated changes
const isStale = useMemo(() => { const isStale = useMemo(() => {
return !claudeUsageLastUpdated || Date.now() - claudeUsageLastUpdated > 2 * 60 * 1000; return !claudeUsageLastUpdated || Date.now() - claudeUsageLastUpdated > 2 * 60 * 1000;
}, [claudeUsageLastUpdated]); }, [claudeUsageLastUpdated]);
const fetchUsage = useCallback(async (isAutoRefresh = false) => { const fetchUsage = useCallback(
if (!isAutoRefresh) setLoading(true); async (isAutoRefresh = false) => {
setError(null); if (!isAutoRefresh) setLoading(true);
try { setError(null);
const api = getElectronAPI(); try {
if (!api.claude) { const api = getElectronAPI();
throw new Error("Claude API not available"); if (!api.claude) {
setError({
code: ERROR_CODES.API_BRIDGE_UNAVAILABLE,
message: 'Claude API bridge not available',
});
return;
}
const data = await api.claude.getUsage();
if ('error' in data) {
setError({
code: ERROR_CODES.AUTH_ERROR,
message: data.message || data.error,
});
return;
}
setClaudeUsage(data);
} catch (err) {
setError({
code: ERROR_CODES.UNKNOWN,
message: err instanceof Error ? err.message : 'Failed to fetch usage',
});
} finally {
if (!isAutoRefresh) setLoading(false);
} }
const data = await api.claude.getUsage(); },
if ("error" in data) { [setClaudeUsage]
throw new Error(data.message || data.error); );
}
setClaudeUsage(data);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to fetch usage");
} finally {
if (!isAutoRefresh) setLoading(false);
}
}, [setClaudeUsage]);
// Auto-fetch on mount if data is stale // Auto-fetch on mount if data is stale
useEffect(() => { useEffect(() => {
@@ -79,11 +107,10 @@ export function ClaudeUsagePopover() {
// Derived status color/icon helper // Derived status color/icon helper
const getStatusInfo = (percentage: number) => { const getStatusInfo = (percentage: number) => {
if (percentage >= 80) if (percentage >= 75) return { color: 'text-red-500', icon: XCircle, bg: 'bg-red-500' };
return { color: "text-red-500", icon: XCircle, bg: "bg-red-500" };
if (percentage >= 50) if (percentage >= 50)
return { color: "text-orange-500", icon: AlertTriangle, bg: "bg-orange-500" }; return { color: 'text-orange-500', icon: AlertTriangle, bg: 'bg-orange-500' };
return { color: "text-green-500", icon: CheckCircle, bg: "bg-green-500" }; return { color: 'text-green-500', icon: CheckCircle, bg: 'bg-green-500' };
}; };
// Helper component for the progress bar // Helper component for the progress bar
@@ -175,8 +202,8 @@ export function ClaudeUsagePopover() {
: 0; : 0;
const getProgressBarColor = (percentage: number) => { const getProgressBarColor = (percentage: number) => {
if (percentage >= 100) return "bg-red-500"; if (percentage >= 80) return 'bg-red-500';
if (percentage >= 75) return "bg-yellow-500"; if (percentage >= 50) return 'bg-yellow-500';
return "bg-green-500"; return "bg-green-500";
}; };
@@ -220,7 +247,7 @@ export function ClaudeUsagePopover() {
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className={cn("h-6 w-6", loading && "opacity-80")} className={cn('h-6 w-6', loading && 'opacity-80')}
onClick={() => !loading && fetchUsage(false)} onClick={() => !loading && fetchUsage(false)}
> >
<RefreshCw className="w-3.5 h-3.5" /> <RefreshCw className="w-3.5 h-3.5" />
@@ -233,9 +260,16 @@ export function ClaudeUsagePopover() {
<div className="flex flex-col items-center justify-center py-6 text-center space-y-3"> <div className="flex flex-col items-center justify-center py-6 text-center space-y-3">
<AlertTriangle className="w-8 h-8 text-yellow-500/80" /> <AlertTriangle className="w-8 h-8 text-yellow-500/80" />
<div className="space-y-1 flex flex-col items-center"> <div className="space-y-1 flex flex-col items-center">
<p className="text-sm font-medium">{error}</p> <p className="text-sm font-medium">{error.message}</p>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Make sure Claude CLI is installed and authenticated via <code className="font-mono bg-muted px-1 rounded">claude login</code> {error.code === ERROR_CODES.API_BRIDGE_UNAVAILABLE ? (
'Ensure the Electron bridge is running or restart the app'
) : (
<>
Make sure Claude CLI is installed and authenticated via{' '}
<code className="font-mono bg-muted px-1 rounded">claude login</code>
</>
)}
</p> </p>
</div> </div>
</div> </div>
@@ -279,7 +313,7 @@ export function ClaudeUsagePopover() {
{claudeUsage.costLimit && claudeUsage.costLimit > 0 && ( {claudeUsage.costLimit && claudeUsage.costLimit > 0 && (
<UsageCard <UsageCard
title="Extra Usage" title="Extra Usage"
subtitle={`${claudeUsage.costUsed ?? 0} / ${claudeUsage.costLimit} ${claudeUsage.costCurrency ?? ""}`} subtitle={`${claudeUsage.costUsed ?? 0} / ${claudeUsage.costLimit} ${claudeUsage.costCurrency ?? ''}`}
percentage={ percentage={
claudeUsage.costLimit > 0 claudeUsage.costLimit > 0
? ((claudeUsage.costUsed ?? 0) / claudeUsage.costLimit) * 100 ? ((claudeUsage.costUsed ?? 0) / claudeUsage.costLimit) * 100

View File

@@ -1230,7 +1230,7 @@ export const useAppStore = create<AppState & AppActions>()(
const id = const id =
feature.id || feature.id ||
`feature-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; `feature-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
const featureWithId = { ...feature, id } as Feature; const featureWithId = { ...feature, id } as unknown as Feature;
set({ features: [...get().features, featureWithId] }); set({ features: [...get().features, featureWithId] });
return featureWithId; return featureWithId;
}, },