feat: extend quota providers and add usage dropdown (#254) (#284)

* feat: extend quota providers and add usage dropdown

Add new quota providers:
- Claude (Anthropic API)
- Codex (OpenAI/ChatGPT)
- GitHub Copilot (base + add-on)
- Kimi for Coding
- OpenRouter

UI enhancements:
- Add rate limits dropdown in header with timer icon
- Desktop: sticky header with Used/Remaining toggle, refresh button
- Mobile: full-screen dropdown with same controls
- Per-provider sticky headers with provider logos
- Auto-refresh on open if no data exists

Usage settings page:
- Provider icon next to header
- Show in dropdown toggle per provider
- Global display selector (Usage vs Quota remaining)
- Auto-refresh controls in sidebar

Settings persistence:
- Add usageDisplayMode and usageDropdownProviders settings
- Server-side sanitization for new settings

Provider logo aliases:
- codex -> openai, claude -> anthropic

Bug fixes:
- Fix React error #310 by calling hooks before early returns
- Add aria-describedby to SettingsWindow dialog
- Remove horizontal scroll from dropdown

* fix: Remove duplicate github copilot declarations
This commit is contained in:
gsxdsm
2026-02-04 11:13:23 +02:00
committed by GitHub
parent 2785f4d9ad
commit a264393f1b
16 changed files with 1600 additions and 319 deletions
@@ -36,8 +36,10 @@ export const UsageSidebar: React.FC<UsageSidebarProps> = ({ onItemSelect }) => {
const isLoading = useQuotaStore((state) => state.isLoading);
const usageAutoRefresh = useQuotaStore((state) => state.autoRefresh);
const usageRefreshIntervalMs = useQuotaStore((state) => state.refreshIntervalMs);
const usageDisplayMode = useQuotaStore((state) => state.displayMode);
const setUsageAutoRefresh = useQuotaStore((state) => state.setAutoRefresh);
const setUsageRefreshInterval = useQuotaStore((state) => state.setRefreshInterval);
const setUsageDisplayMode = useQuotaStore((state) => state.setDisplayMode);
const loadUsageSettings = useQuotaStore((state) => state.loadSettings);
const { isMobile } = useDeviceInfo();
@@ -57,7 +59,7 @@ export const UsageSidebar: React.FC<UsageSidebarProps> = ({ onItemSelect }) => {
void loadUsageSettings();
}, [loadUsageSettings]);
const persistUsageSettings = React.useCallback(async (changes: { usageAutoRefresh?: boolean; usageRefreshIntervalMs?: number }) => {
const persistUsageSettings = React.useCallback(async (changes: { usageAutoRefresh?: boolean; usageRefreshIntervalMs?: number; usageDisplayMode?: 'usage' | 'remaining'; usageDropdownProviders?: string[] }) => {
try {
await updateDesktopSettings(changes);
} catch (error) {
@@ -79,6 +81,17 @@ export const UsageSidebar: React.FC<UsageSidebarProps> = ({ onItemSelect }) => {
void persistUsageSettings({ usageRefreshIntervalMs: next });
}, [persistUsageSettings, setUsageRefreshInterval]);
const handleUsageDisplayModeChange = React.useCallback((value: string) => {
if (value !== 'usage' && value !== 'remaining') {
return;
}
setUsageDisplayMode(value);
void persistUsageSettings({ usageDisplayMode: value });
}, [persistUsageSettings, setUsageDisplayMode]);
const bgClass = isDesktopRuntime
? 'bg-transparent'
: isVSCode
@@ -134,6 +147,22 @@ export const UsageSidebar: React.FC<UsageSidebarProps> = ({ onItemSelect }) => {
</Button>
</div>
</div>
<div className="mt-2 flex items-center justify-between gap-2">
<span className="typography-micro text-muted-foreground">Display</span>
<Select value={usageDisplayMode} onValueChange={handleUsageDisplayModeChange}>
<SelectTrigger size="sm" className="min-w-[140px]">
<SelectValue placeholder="Display mode" />
</SelectTrigger>
<SelectContent>
<SelectItem value="usage" className="pr-2 [&>span:first-child]:hidden">
Usage
</SelectItem>
<SelectItem value="remaining" className="pr-2 [&>span:first-child]:hidden">
Quota remaining
</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<ScrollableOverlay outerClassName="flex-1 min-h-0" className="space-y-1 px-3 py-2 overflow-x-hidden">
@@ -173,11 +202,11 @@ export const UsageSidebar: React.FC<UsageSidebarProps> = ({ onItemSelect }) => {
<span className="typography-ui-label font-normal truncate flex-1 min-w-0 text-foreground">
{provider.name}
</span>
{!configured && (
<span className="typography-micro text-muted-foreground/60 flex-shrink-0">Not set</span>
)}
</button>
</div>
{!configured && (
<span className="typography-micro text-muted-foreground/60 flex-shrink-0">Not set</span>
)}
</button>
</div>
);
})}
</ScrollableOverlay>