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
+2
View File
@@ -57,6 +57,8 @@ export type DesktopSettings = {
notifyOnSubtasks?: boolean;
usageAutoRefresh?: boolean;
usageRefreshIntervalMs?: number;
usageDisplayMode?: 'usage' | 'remaining';
usageDropdownProviders?: string[];
autoDeleteEnabled?: boolean;
autoDeleteAfterDays?: number;
defaultModel?: string; // format: "provider/model"
+8
View File
@@ -357,6 +357,14 @@ const sanitizeWebSettings = (payload: unknown): DesktopSettings | null => {
if (typeof candidate.usageRefreshIntervalMs === 'number' && Number.isFinite(candidate.usageRefreshIntervalMs)) {
result.usageRefreshIntervalMs = candidate.usageRefreshIntervalMs;
}
if (candidate.usageDisplayMode === 'usage' || candidate.usageDisplayMode === 'remaining') {
result.usageDisplayMode = candidate.usageDisplayMode;
}
if (Array.isArray(candidate.usageDropdownProviders)) {
result.usageDropdownProviders = candidate.usageDropdownProviders.filter(
(entry): entry is string => typeof entry === 'string' && entry.length > 0
);
}
if (
typeof candidate.toolCallExpansion === 'string'
&& (candidate.toolCallExpansion === 'collapsed'
+5 -2
View File
@@ -6,10 +6,13 @@ export interface QuotaProviderMeta {
}
export const QUOTA_PROVIDERS: QuotaProviderMeta[] = [
{ id: 'openai', name: 'OpenAI' },
{ id: 'claude', name: 'Claude' },
{ id: 'codex', name: 'Codex' },
{ id: 'github-copilot', name: 'GitHub Copilot' },
{ id: 'google', name: 'Google' },
{ id: 'kimi-for-coding', name: 'Kimi for Coding' },
{ id: 'openrouter', name: 'OpenRouter' },
{ id: 'zai-coding-plan', name: 'z.ai' },
{ id: 'github-copilot', name: 'GitHub Copilot' }
];
export const QUOTA_PROVIDER_MAP = QUOTA_PROVIDERS.reduce<Record<string, QuotaProviderMeta>>(
+8
View File
@@ -28,7 +28,15 @@ export const resolveUsageTone = (percent: number | null): 'safe' | 'warn' | 'cri
export const formatWindowLabel = (label: string): string => {
if (label === '5h') return '5-Hour Limit';
if (label === '7d') return '7-Day Limit';
if (label === '7d-sonnet') return '7-Day Sonnet Limit';
if (label === '7d-opus') return '7-Day Opus Limit';
if (label === 'weekly') return 'Weekly Limit';
if (label === 'monthly') return 'Monthly Limit';
if (label === 'credits') return 'Credits';
if (label === 'premium') return 'Premium Interactions';
if (label === 'chat') return 'Chat Requests';
if (label === 'completions') return 'Completions';
if (label === 'premium_interactions') return 'Premium interactions';
return label;
};