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
+27 -3
View File
@@ -11,6 +11,8 @@ const DEFAULT_REFRESH_INTERVAL_MS = 60000;
interface QuotaSettingsState {
autoRefresh: boolean;
refreshIntervalMs: number;
displayMode: 'usage' | 'remaining';
dropdownProviderIds: QuotaProviderId[];
}
interface QuotaStore extends QuotaSettingsState {
@@ -27,9 +29,12 @@ interface QuotaStore extends QuotaSettingsState {
setSelectedProvider: (providerId: QuotaProviderId | null) => void;
setAutoRefresh: (enabled: boolean) => void;
setRefreshInterval: (intervalMs: number) => void;
setDisplayMode: (mode: 'usage' | 'remaining') => void;
setDropdownProviderIds: (providerIds: QuotaProviderId[]) => void;
}
const parseSettings = (data: Record<string, unknown> | null): QuotaSettingsState => {
const allProviderIds = QUOTA_PROVIDERS.map((provider) => provider.id);
const autoRefresh = typeof data?.usageAutoRefresh === 'boolean'
? data.usageAutoRefresh
: false;
@@ -38,7 +43,17 @@ const parseSettings = (data: Record<string, unknown> | null): QuotaSettingsState
? Math.max(30000, Math.min(300000, Math.round(data.usageRefreshIntervalMs)))
: DEFAULT_REFRESH_INTERVAL_MS;
return { autoRefresh, refreshIntervalMs };
const displayMode = data?.usageDisplayMode === 'remaining' ? 'remaining' : 'usage';
const rawDropdownProviders = Array.isArray(data?.usageDropdownProviders)
? data?.usageDropdownProviders
: null;
const dropdownProviderIds = rawDropdownProviders
? rawDropdownProviders.filter((entry): entry is QuotaProviderId =>
typeof entry === 'string' && allProviderIds.includes(entry as QuotaProviderId)
)
: allProviderIds;
return { autoRefresh, refreshIntervalMs, displayMode, dropdownProviderIds };
};
const loadSettingsFromRuntime = async (): Promise<QuotaSettingsState> => {
@@ -69,7 +84,12 @@ const loadSettingsFromRuntime = async (): Promise<QuotaSettingsState> => {
}
}
return { autoRefresh: false, refreshIntervalMs: DEFAULT_REFRESH_INTERVAL_MS };
return {
autoRefresh: false,
refreshIntervalMs: DEFAULT_REFRESH_INTERVAL_MS,
displayMode: 'usage',
dropdownProviderIds: QUOTA_PROVIDERS.map((provider) => provider.id)
};
};
export const useQuotaStore = create<QuotaStore>()(
@@ -83,6 +103,8 @@ export const useQuotaStore = create<QuotaStore>()(
error: null,
autoRefresh: false,
refreshIntervalMs: DEFAULT_REFRESH_INTERVAL_MS,
displayMode: 'usage',
dropdownProviderIds: QUOTA_PROVIDERS.map((provider) => provider.id),
loadSettings: async () => {
try {
@@ -155,7 +177,9 @@ export const useQuotaStore = create<QuotaStore>()(
setRefreshInterval: (intervalMs) => {
const clamped = Math.max(30000, Math.min(300000, Math.round(intervalMs)));
set({ refreshIntervalMs: clamped });
}
},
setDisplayMode: (mode) => set({ displayMode: mode }),
setDropdownProviderIds: (providerIds) => set({ dropdownProviderIds: providerIds })
}),
{ name: 'quota-store' }
)