mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-31 20:03:37 +00:00
Merge remote-tracking branch 'origin/main' into refactor/folder-pattern-compliance
This commit is contained in:
349
apps/ui/src/components/claude-usage-popover.tsx
Normal file
349
apps/ui/src/components/claude-usage-popover.tsx
Normal file
@@ -0,0 +1,349 @@
|
||||
import { useState, useEffect, useMemo, useCallback } from "react";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
RefreshCw,
|
||||
AlertTriangle,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
Clock,
|
||||
ExternalLink,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { getElectronAPI } from "@/lib/electron";
|
||||
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;
|
||||
};
|
||||
|
||||
// Fixed refresh interval (45 seconds)
|
||||
const REFRESH_INTERVAL_SECONDS = 45;
|
||||
|
||||
export function ClaudeUsagePopover() {
|
||||
const { claudeUsage, claudeUsageLastUpdated, setClaudeUsage } = useAppStore();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<UsageError | null>(null);
|
||||
|
||||
// Check if data is stale (older than 2 minutes) - recalculates when claudeUsageLastUpdated changes
|
||||
const isStale = useMemo(() => {
|
||||
return !claudeUsageLastUpdated || Date.now() - claudeUsageLastUpdated > 2 * 60 * 1000;
|
||||
}, [claudeUsageLastUpdated]);
|
||||
|
||||
const fetchUsage = useCallback(
|
||||
async (isAutoRefresh = false) => {
|
||||
if (!isAutoRefresh) setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
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);
|
||||
}
|
||||
},
|
||||
[setClaudeUsage]
|
||||
);
|
||||
|
||||
// Auto-fetch on mount if data is stale
|
||||
useEffect(() => {
|
||||
if (isStale) {
|
||||
fetchUsage(true);
|
||||
}
|
||||
}, [isStale, fetchUsage]);
|
||||
|
||||
useEffect(() => {
|
||||
// Initial fetch when opened
|
||||
if (open) {
|
||||
if (!claudeUsage || isStale) {
|
||||
fetchUsage();
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-refresh interval (only when open)
|
||||
let intervalId: NodeJS.Timeout | null = null;
|
||||
if (open) {
|
||||
intervalId = setInterval(() => {
|
||||
fetchUsage(true);
|
||||
}, REFRESH_INTERVAL_SECONDS * 1000);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (intervalId) clearInterval(intervalId);
|
||||
};
|
||||
}, [open, claudeUsage, isStale, fetchUsage]);
|
||||
|
||||
// Derived status color/icon helper
|
||||
const getStatusInfo = (percentage: number) => {
|
||||
if (percentage >= 75) return { color: 'text-red-500', icon: XCircle, bg: 'bg-red-500' };
|
||||
if (percentage >= 50)
|
||||
return { color: 'text-orange-500', icon: AlertTriangle, bg: 'bg-orange-500' };
|
||||
return { color: 'text-green-500', icon: CheckCircle, bg: 'bg-green-500' };
|
||||
};
|
||||
|
||||
// Helper component for the progress bar
|
||||
const ProgressBar = ({
|
||||
percentage,
|
||||
colorClass,
|
||||
}: {
|
||||
percentage: number;
|
||||
colorClass: string;
|
||||
}) => (
|
||||
<div className="h-2 w-full bg-secondary/50 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={cn("h-full transition-all duration-500", colorClass)}
|
||||
style={{ width: `${Math.min(percentage, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const UsageCard = ({
|
||||
title,
|
||||
subtitle,
|
||||
percentage,
|
||||
resetText,
|
||||
isPrimary = false,
|
||||
stale = false,
|
||||
}: {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
percentage: number;
|
||||
resetText?: string;
|
||||
isPrimary?: boolean;
|
||||
stale?: boolean;
|
||||
}) => {
|
||||
// Check if percentage is valid (not NaN, not undefined, is a finite number)
|
||||
const isValidPercentage = typeof percentage === "number" && !isNaN(percentage) && isFinite(percentage);
|
||||
const safePercentage = isValidPercentage ? percentage : 0;
|
||||
|
||||
const status = getStatusInfo(safePercentage);
|
||||
const StatusIcon = status.icon;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-xl border bg-card/50 p-4 transition-opacity",
|
||||
isPrimary ? "border-border/60 shadow-sm" : "border-border/40",
|
||||
(stale || !isValidPercentage) && "opacity-50"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div>
|
||||
<h4 className={cn("font-semibold", isPrimary ? "text-sm" : "text-xs")}>
|
||||
{title}
|
||||
</h4>
|
||||
<p className="text-[10px] text-muted-foreground">{subtitle}</p>
|
||||
</div>
|
||||
{isValidPercentage ? (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<StatusIcon className={cn("w-3.5 h-3.5", status.color)} />
|
||||
<span
|
||||
className={cn(
|
||||
"font-mono font-bold",
|
||||
status.color,
|
||||
isPrimary ? "text-base" : "text-sm"
|
||||
)}
|
||||
>
|
||||
{Math.round(safePercentage)}%
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground">N/A</span>
|
||||
)}
|
||||
</div>
|
||||
<ProgressBar percentage={safePercentage} colorClass={isValidPercentage ? status.bg : "bg-muted-foreground/30"} />
|
||||
{resetText && (
|
||||
<div className="mt-2 flex justify-end">
|
||||
<p className="text-xs text-muted-foreground flex items-center gap-1">
|
||||
{title === "Session Usage" && <Clock className="w-3 h-3" />}
|
||||
{resetText}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Header Button
|
||||
const maxPercentage = claudeUsage
|
||||
? Math.max(claudeUsage.sessionPercentage || 0, claudeUsage.weeklyPercentage || 0)
|
||||
: 0;
|
||||
|
||||
const getProgressBarColor = (percentage: number) => {
|
||||
if (percentage >= 80) return 'bg-red-500';
|
||||
if (percentage >= 50) return 'bg-yellow-500';
|
||||
return "bg-green-500";
|
||||
};
|
||||
|
||||
const trigger = (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-9 gap-3 bg-secondary border border-border px-3"
|
||||
>
|
||||
<span className="text-sm font-medium">Usage</span>
|
||||
{claudeUsage && (
|
||||
<div className={cn(
|
||||
"h-1.5 w-16 bg-muted-foreground/20 rounded-full overflow-hidden transition-opacity",
|
||||
isStale && "opacity-60"
|
||||
)}>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full transition-all duration-500",
|
||||
getProgressBarColor(maxPercentage)
|
||||
)}
|
||||
style={{ width: `${Math.min(maxPercentage, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>{trigger}</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="w-80 p-0 overflow-hidden bg-background/95 backdrop-blur-xl border-border shadow-2xl"
|
||||
align="end"
|
||||
sideOffset={8}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-border/50 bg-secondary/10">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-semibold">Claude Usage</span>
|
||||
</div>
|
||||
{error && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn('h-6 w-6', loading && 'opacity-80')}
|
||||
onClick={() => !loading && fetchUsage(false)}
|
||||
>
|
||||
<RefreshCw className="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-4 space-y-4">
|
||||
{error ? (
|
||||
<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" />
|
||||
<div className="space-y-1 flex flex-col items-center">
|
||||
<p className="text-sm font-medium">{error.message}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{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>
|
||||
</div>
|
||||
</div>
|
||||
) : !claudeUsage ? (
|
||||
// Loading state
|
||||
<div className="flex flex-col items-center justify-center py-8 space-y-2">
|
||||
<RefreshCw className="w-6 h-6 animate-spin text-muted-foreground/50" />
|
||||
<p className="text-xs text-muted-foreground">Loading usage data...</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Primary Card */}
|
||||
<UsageCard
|
||||
title="Session Usage"
|
||||
subtitle="5-hour rolling window"
|
||||
percentage={claudeUsage.sessionPercentage}
|
||||
resetText={claudeUsage.sessionResetText}
|
||||
isPrimary={true}
|
||||
stale={isStale}
|
||||
/>
|
||||
|
||||
{/* Secondary Cards Grid */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<UsageCard
|
||||
title="Weekly"
|
||||
subtitle="All models"
|
||||
percentage={claudeUsage.weeklyPercentage}
|
||||
resetText={claudeUsage.weeklyResetText}
|
||||
stale={isStale}
|
||||
/>
|
||||
<UsageCard
|
||||
title="Sonnet"
|
||||
subtitle="Weekly"
|
||||
percentage={claudeUsage.sonnetWeeklyPercentage}
|
||||
resetText={claudeUsage.sonnetResetText}
|
||||
stale={isStale}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Extra Usage / Cost */}
|
||||
{claudeUsage.costLimit && claudeUsage.costLimit > 0 && (
|
||||
<UsageCard
|
||||
title="Extra Usage"
|
||||
subtitle={`${claudeUsage.costUsed ?? 0} / ${claudeUsage.costLimit} ${claudeUsage.costCurrency ?? ''}`}
|
||||
percentage={
|
||||
claudeUsage.costLimit > 0
|
||||
? ((claudeUsage.costUsed ?? 0) / claudeUsage.costLimit) * 100
|
||||
: 0
|
||||
}
|
||||
stale={isStale}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-between px-4 py-2 bg-secondary/10 border-t border-border/50">
|
||||
<a
|
||||
href="https://status.claude.com"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-[10px] text-muted-foreground hover:text-foreground flex items-center gap-1 transition-colors"
|
||||
>
|
||||
Claude Status <ExternalLink className="w-2.5 h-2.5" />
|
||||
</a>
|
||||
|
||||
<span className="text-[10px] text-muted-foreground">Updates every minute</span>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -6,6 +6,8 @@ import { Switch } from "@/components/ui/switch";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Plus, Bot } from "lucide-react";
|
||||
import { KeyboardShortcut } from "@/hooks/use-keyboard-shortcuts";
|
||||
import { ClaudeUsagePopover } from "@/components/claude-usage-popover";
|
||||
import { useAppStore } from "@/store/app-store";
|
||||
|
||||
interface BoardHeaderProps {
|
||||
projectName: string;
|
||||
@@ -30,6 +32,13 @@ export function BoardHeader({
|
||||
addFeatureShortcut,
|
||||
isMounted,
|
||||
}: BoardHeaderProps) {
|
||||
const apiKeys = useAppStore((state) => state.apiKeys);
|
||||
|
||||
// Hide usage tracking when using API key (only show for Claude Code CLI users)
|
||||
// Also hide on Windows for now (CLI usage command not supported)
|
||||
const isWindows = typeof navigator !== 'undefined' && navigator.platform?.toLowerCase().includes('win');
|
||||
const showUsageTracking = !apiKeys.anthropic && !isWindows;
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between p-4 border-b border-border bg-glass backdrop-blur-md">
|
||||
<div>
|
||||
@@ -37,6 +46,9 @@ export function BoardHeader({
|
||||
<p className="text-sm text-muted-foreground">{projectName}</p>
|
||||
</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
{/* Usage Popover - only show for CLI users (not API key users) */}
|
||||
{isMounted && showUsageTracking && <ClaudeUsagePopover />}
|
||||
|
||||
{/* Concurrency Slider - only show after mount to prevent hydration issues */}
|
||||
{isMounted && (
|
||||
<div
|
||||
|
||||
@@ -9,6 +9,7 @@ import { KeyboardMapDialog } from "./settings-view/components/keyboard-map-dialo
|
||||
import { DeleteProjectDialog } from "./settings-view/components/delete-project-dialog";
|
||||
import { SettingsNavigation } from "./settings-view/components/settings-navigation";
|
||||
import { ApiKeysSection } from "./settings-view/api-keys/api-keys-section";
|
||||
import { ClaudeUsageSection } from "./settings-view/api-keys/claude-usage-section";
|
||||
import { ClaudeCliStatus } from "./settings-view/cli-status/claude-cli-status";
|
||||
import { AIEnhancementSection } from "./settings-view/ai-enhancement";
|
||||
import { AppearanceSection } from "./settings-view/appearance/appearance-section";
|
||||
@@ -46,8 +47,14 @@ export function SettingsView() {
|
||||
defaultAIProfileId,
|
||||
setDefaultAIProfileId,
|
||||
aiProfiles,
|
||||
apiKeys,
|
||||
} = useAppStore();
|
||||
|
||||
// Hide usage tracking when using API key (only show for Claude Code CLI users)
|
||||
// Also hide on Windows for now (CLI usage command not supported)
|
||||
const isWindows = typeof navigator !== 'undefined' && navigator.platform?.toLowerCase().includes('win');
|
||||
const showUsageTracking = !apiKeys.anthropic && !isWindows;
|
||||
|
||||
// Convert electron Project to settings-view Project type
|
||||
const convertProject = (
|
||||
project: ElectronProject | null
|
||||
@@ -92,11 +99,14 @@ export function SettingsView() {
|
||||
switch (activeView) {
|
||||
case "claude":
|
||||
return (
|
||||
<ClaudeCliStatus
|
||||
status={claudeCliStatus}
|
||||
isChecking={isCheckingClaudeCli}
|
||||
onRefresh={handleRefreshClaudeCli}
|
||||
/>
|
||||
<div className="space-y-6">
|
||||
<ClaudeCliStatus
|
||||
status={claudeCliStatus}
|
||||
isChecking={isCheckingClaudeCli}
|
||||
onRefresh={handleRefreshClaudeCli}
|
||||
/>
|
||||
{showUsageTracking && <ClaudeUsageSection />}
|
||||
</div>
|
||||
);
|
||||
case "ai-enhancement":
|
||||
return <AIEnhancementSection />;
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function ClaudeUsageSection() {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-2xl overflow-hidden",
|
||||
"border border-border/50",
|
||||
"bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl",
|
||||
"shadow-sm shadow-black/5"
|
||||
)}
|
||||
>
|
||||
<div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-green-500/20 to-green-600/10 flex items-center justify-center border border-green-500/20">
|
||||
<div className="w-5 h-5 rounded-full bg-green-500/50" />
|
||||
</div>
|
||||
<h2 className="text-lg font-semibold text-foreground tracking-tight">Claude Usage Tracking</h2>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground/80 ml-12">
|
||||
Track your Claude Code usage limits. Uses the Claude CLI for data.
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Info about CLI requirement */}
|
||||
<div className="rounded-lg bg-secondary/30 p-3 text-xs text-muted-foreground space-y-2 border border-border/50">
|
||||
<p>Usage tracking requires Claude Code CLI to be installed and authenticated:</p>
|
||||
<ol className="list-decimal list-inside space-y-1 ml-1">
|
||||
<li>Install Claude Code CLI if not already installed</li>
|
||||
<li>Run <code className="font-mono bg-muted px-1 rounded">claude login</code> to authenticate</li>
|
||||
<li>Usage data will be fetched automatically every ~minute</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
// Type definitions for Electron IPC API
|
||||
import type { SessionListItem, Message } from "@/types/electron";
|
||||
import type { ClaudeUsageResponse } from "@/store/app-store";
|
||||
import { getJSON, setJSON, removeItem } from "./storage";
|
||||
|
||||
export interface FileEntry {
|
||||
@@ -482,6 +483,9 @@ export interface ElectronAPI {
|
||||
sessionId: string
|
||||
) => Promise<{ success: boolean; error?: string }>;
|
||||
};
|
||||
claude?: {
|
||||
getUsage: () => Promise<ClaudeUsageResponse>;
|
||||
};
|
||||
}
|
||||
|
||||
// Note: Window interface is declared in @/types/electron.d.ts
|
||||
@@ -879,6 +883,33 @@ const getMockElectronAPI = (): ElectronAPI => {
|
||||
|
||||
// Mock Running Agents API
|
||||
runningAgents: createMockRunningAgentsAPI(),
|
||||
|
||||
// Mock Claude API
|
||||
claude: {
|
||||
getUsage: async () => {
|
||||
console.log("[Mock] Getting Claude usage");
|
||||
return {
|
||||
sessionTokensUsed: 0,
|
||||
sessionLimit: 0,
|
||||
sessionPercentage: 15,
|
||||
sessionResetTime: new Date(Date.now() + 3600000).toISOString(),
|
||||
sessionResetText: "Resets in 1h",
|
||||
weeklyTokensUsed: 0,
|
||||
weeklyLimit: 0,
|
||||
weeklyPercentage: 5,
|
||||
weeklyResetTime: new Date(Date.now() + 86400000 * 2).toISOString(),
|
||||
weeklyResetText: "Resets Dec 23",
|
||||
sonnetWeeklyTokensUsed: 0,
|
||||
sonnetWeeklyPercentage: 1,
|
||||
sonnetResetText: "Resets Dec 27",
|
||||
costUsed: null,
|
||||
costLimit: null,
|
||||
costCurrency: null,
|
||||
lastUpdated: new Date().toISOString(),
|
||||
userTimezone: "UTC"
|
||||
};
|
||||
},
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ import type {
|
||||
SuggestionType,
|
||||
} from "./electron";
|
||||
import type { Message, SessionListItem } from "@/types/electron";
|
||||
import type { Feature } from "@/store/app-store";
|
||||
import type { Feature, ClaudeUsageResponse } from "@/store/app-store";
|
||||
import type {
|
||||
WorktreeAPI,
|
||||
GitAPI,
|
||||
@@ -1016,6 +1016,11 @@ export class HttpApiClient implements ElectronAPI {
|
||||
): Promise<{ success: boolean; error?: string }> =>
|
||||
this.httpDelete(`/api/sessions/${sessionId}`),
|
||||
};
|
||||
|
||||
// Claude API
|
||||
claude = {
|
||||
getUsage: (): Promise<ClaudeUsageResponse> => this.get("/api/claude/usage"),
|
||||
};
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
|
||||
@@ -431,6 +431,75 @@ export interface AppState {
|
||||
planContent: string;
|
||||
planningMode: 'lite' | 'spec' | 'full';
|
||||
} | null;
|
||||
|
||||
// Claude Usage Tracking
|
||||
claudeRefreshInterval: number; // Refresh interval in seconds (default: 60)
|
||||
claudeUsage: ClaudeUsage | null;
|
||||
claudeUsageLastUpdated: number | null;
|
||||
}
|
||||
|
||||
// Claude Usage interface matching the server response
|
||||
export type ClaudeUsage = {
|
||||
sessionTokensUsed: number;
|
||||
sessionLimit: number;
|
||||
sessionPercentage: number;
|
||||
sessionResetTime: string;
|
||||
sessionResetText: string;
|
||||
|
||||
weeklyTokensUsed: number;
|
||||
weeklyLimit: number;
|
||||
weeklyPercentage: number;
|
||||
weeklyResetTime: string;
|
||||
weeklyResetText: string;
|
||||
|
||||
sonnetWeeklyTokensUsed: number;
|
||||
sonnetWeeklyPercentage: number;
|
||||
sonnetResetText: string;
|
||||
|
||||
costUsed: number | null;
|
||||
costLimit: number | null;
|
||||
costCurrency: string | null;
|
||||
|
||||
lastUpdated: string;
|
||||
userTimezone: string;
|
||||
};
|
||||
|
||||
// Response type for Claude usage API (can be success or error)
|
||||
export type ClaudeUsageResponse =
|
||||
| ClaudeUsage
|
||||
| { error: string; message?: string };
|
||||
|
||||
/**
|
||||
* Check if Claude usage is at its limit (any of: session >= 100%, weekly >= 100%, OR cost >= limit)
|
||||
* Returns true if any limit is reached, meaning auto mode should pause feature pickup.
|
||||
*/
|
||||
export function isClaudeUsageAtLimit(claudeUsage: ClaudeUsage | null): boolean {
|
||||
if (!claudeUsage) {
|
||||
// No usage data available - don't block
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check session limit (5-hour window)
|
||||
if (claudeUsage.sessionPercentage >= 100) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check weekly limit
|
||||
if (claudeUsage.weeklyPercentage >= 100) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check cost limit (if configured)
|
||||
if (
|
||||
claudeUsage.costLimit !== null &&
|
||||
claudeUsage.costLimit > 0 &&
|
||||
claudeUsage.costUsed !== null &&
|
||||
claudeUsage.costUsed >= claudeUsage.costLimit
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Default background settings for board backgrounds
|
||||
@@ -665,6 +734,11 @@ export interface AppActions {
|
||||
} | null
|
||||
) => void;
|
||||
|
||||
// Claude Usage Tracking actions
|
||||
setClaudeRefreshInterval: (interval: number) => void;
|
||||
setClaudeUsageLastUpdated: (timestamp: number) => void;
|
||||
setClaudeUsage: (usage: ClaudeUsage | null) => void;
|
||||
|
||||
// Reset
|
||||
reset: () => void;
|
||||
}
|
||||
@@ -756,6 +830,9 @@ const initialState: AppState = {
|
||||
defaultRequirePlanApproval: false,
|
||||
defaultAIProfileId: null,
|
||||
pendingPlanApproval: null,
|
||||
claudeRefreshInterval: 60,
|
||||
claudeUsage: null,
|
||||
claudeUsageLastUpdated: null,
|
||||
};
|
||||
|
||||
export const useAppStore = create<AppState & AppActions>()(
|
||||
@@ -1052,8 +1129,10 @@ export const useAppStore = create<AppState & AppActions>()(
|
||||
},
|
||||
|
||||
addFeature: (feature) => {
|
||||
const id = feature.id || `feature-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const featureWithId = { ...feature, id } as Feature;
|
||||
const id =
|
||||
feature.id ||
|
||||
`feature-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const featureWithId = { ...feature, id } as unknown as Feature;
|
||||
set({ features: [...get().features, featureWithId] });
|
||||
return featureWithId;
|
||||
},
|
||||
@@ -2096,6 +2175,14 @@ export const useAppStore = create<AppState & AppActions>()(
|
||||
// Plan Approval actions
|
||||
setPendingPlanApproval: (approval) => set({ pendingPlanApproval: approval }),
|
||||
|
||||
// Claude Usage Tracking actions
|
||||
setClaudeRefreshInterval: (interval: number) => set({ claudeRefreshInterval: interval }),
|
||||
setClaudeUsageLastUpdated: (timestamp: number) => set({ claudeUsageLastUpdated: timestamp }),
|
||||
setClaudeUsage: (usage: ClaudeUsage | null) => set({
|
||||
claudeUsage: usage,
|
||||
claudeUsageLastUpdated: usage ? Date.now() : null,
|
||||
}),
|
||||
|
||||
// Reset
|
||||
reset: () => set(initialState),
|
||||
}),
|
||||
@@ -2168,6 +2255,10 @@ export const useAppStore = create<AppState & AppActions>()(
|
||||
defaultPlanningMode: state.defaultPlanningMode,
|
||||
defaultRequirePlanApproval: state.defaultRequirePlanApproval,
|
||||
defaultAIProfileId: state.defaultAIProfileId,
|
||||
// Claude usage tracking
|
||||
claudeUsage: state.claudeUsage,
|
||||
claudeUsageLastUpdated: state.claudeUsageLastUpdated,
|
||||
claudeRefreshInterval: state.claudeRefreshInterval,
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user