fix(usage): refresh work status quotas automatically

This commit is contained in:
Bohdan Triapitsyn
2026-08-11 12:58:36 +03:00
parent 99e254acf3
commit b55152db6f
31 changed files with 130 additions and 625 deletions
@@ -82,7 +82,8 @@ and therefore displaces nothing.
## Data sources
Everything is read from already-warm caches. The panel adds no aggregated
endpoint and no polling of its own.
endpoint; quota data refreshes through the shared fixed three-minute quota timer,
which requests only providers enabled for this panel.
| Block | Source | Notes |
|---|---|---|
@@ -320,8 +321,8 @@ Two readouts had no loader of their own and appeared only after the user opened
the matching header dropdown:
- **MCP** — `McpDropdown` was the only mount-time caller of `refresh()`.
- **Usage** — `useQuotaAutoRefresh` merely schedules an interval; the *first*
fetch was performed by the dropdown's open handler.
- **Usage** — `useQuotaAutoRefresh` schedules the shared fixed three-minute
refresh; the *first* fetch was performed by the dropdown's open handler.
- **Skills** — `loadSkills()` ran only when the composer's slash autocomplete
opened, so the context-sources count was whatever happened to be cached. The
section loads them itself, keyed on the directory, since skills are
@@ -330,7 +331,8 @@ the matching header dropdown:
The panel now performs these itself, silently and through the
background-network gate, so it cannot compete with chat bootstrap traffic for
sockets. A panel that reports a subsystem's state cannot depend on an unrelated
sockets. Usage additionally provides an explicit refresh action in its section
header. A panel that reports a subsystem's state cannot depend on an unrelated
component having been mounted or opened.
The repository section follows the same ownership rule. It subscribes directly
@@ -63,9 +63,11 @@ export const WorkStatusCollapsibleSection: React.FC<{
iconColor?: string;
/** Shown on the header while collapsed and expanded alike. */
summary?: React.ReactNode;
/** An independent header action, such as refreshing this section's data. */
action?: React.ReactNode;
defaultExpanded?: boolean;
children: React.ReactNode;
}> = ({ id, title, icon, iconNode, iconColor, summary, defaultExpanded = false, children }) => {
}> = ({ id, title, icon, iconNode, iconColor, summary, action, defaultExpanded = false, children }) => {
const stored = useUIStore(
React.useCallback((state) => state.workStatusExpandedSections[id], [id]),
);
@@ -73,35 +75,38 @@ export const WorkStatusCollapsibleSection: React.FC<{
const expanded = stored ?? defaultExpanded;
return (
<section className={SECTION_CLASS}>
<button
type="button"
aria-expanded={expanded}
onClick={() => setExpandedInStore(id, !expanded)}
className={cn(
'group/section mb-0.5 flex h-6 items-center gap-1.5 rounded-md px-1 text-left',
// No hover fill anywhere in the panel: at this row density the blocks
// of colour read as selection, not as affordance. Interactivity shows
// through the text instead.
'transition-colors hover:text-foreground',
)}
>
{iconNode ?? (icon ? (
<div className="mb-0.5 flex h-6 items-center gap-1">
<button
type="button"
aria-expanded={expanded}
onClick={() => setExpandedInStore(id, !expanded)}
className={cn(
'group/section flex min-w-0 flex-1 items-center gap-1.5 rounded-md px-1 text-left',
// No hover fill anywhere in the panel: at this row density the blocks
// of colour read as selection, not as affordance. Interactivity shows
// through the text instead.
'transition-colors hover:text-foreground',
)}
>
{iconNode ?? (icon ? (
<Icon
name={icon}
className={cn('size-4 shrink-0', !iconColor && 'text-muted-foreground')}
style={iconColor ? { color: iconColor } : undefined}
/>
) : null)}
<span className={cn(HEADING_CLASS, 'min-w-0 truncate')}>{title}</span>
<Icon
name={icon}
className={cn('size-4 shrink-0', !iconColor && 'text-muted-foreground')}
style={iconColor ? { color: iconColor } : undefined}
name={expanded ? 'arrow-down-s' : 'arrow-right-s'}
className="size-3.5 shrink-0 text-muted-foreground"
/>
) : null)}
<span className={cn(HEADING_CLASS, 'min-w-0 truncate')}>{title}</span>
<Icon
name={expanded ? 'arrow-down-s' : 'arrow-right-s'}
className="size-3.5 shrink-0 text-muted-foreground"
/>
<span className="flex-1" />
{summary !== undefined && summary !== null ? (
<span className="shrink-0 text-xs text-muted-foreground tabular-nums">{summary}</span>
) : null}
</button>
<span className="flex-1" />
{summary !== undefined && summary !== null ? (
<span className="shrink-0 text-xs text-muted-foreground tabular-nums">{summary}</span>
) : null}
</button>
{action}
</div>
{expanded ? children : null}
</section>
);
@@ -1,5 +1,7 @@
import React from 'react';
import { Icon } from '@/components/icon/Icon';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { useI18n } from '@/lib/i18n';
import { ProviderLogo } from '@/components/ui/ProviderLogo';
import { preloadProviderLogos } from '@/hooks/useProviderLogo';
@@ -43,7 +45,7 @@ export const WorkStatusUsageSection: React.FC = () => {
const isLoading = useQuotaStore((state) => state.isLoading);
const quotaResults = useQuotaStore((state) => state.results);
const dropdownProviderIds = useQuotaStore((state) => state.dropdownProviderIds);
const fetchAllQuotas = useQuotaStore((state) => state.fetchAllQuotas);
const fetchQuotas = useQuotaStore((state) => state.fetchQuotas);
const timeFormatPreference = useUIStore((state) => state.timeFormatPreference);
const currentProviderId = useConfigStore((state) => state.currentProviderId);
@@ -61,8 +63,8 @@ export const WorkStatusUsageSection: React.FC = () => {
(providerId) => !quotaResults.some((result) => result.providerId === providerId),
);
if (!missingProvider) return;
void runBackgroundNetworkTask(() => fetchAllQuotas());
}, [dropdownProviderIds, fetchAllQuotas, isLoading, quotaResults]);
void runBackgroundNetworkTask(() => fetchQuotas(dropdownProviderIds));
}, [dropdownProviderIds, fetchQuotas, isLoading, quotaResults]);
React.useEffect(() => {
if (groups.length === 0) return;
@@ -96,7 +98,6 @@ export const WorkStatusUsageSection: React.FC = () => {
icon="timer"
summary={(
<span className="inline-flex items-center gap-1.5">
{isLoading ? <Icon name="refresh" className="size-3 animate-spin" /> : null}
{headline && headlineMetric && headlineMetric !== '-' ? (
<>
<span className="truncate">{headline.row.label}</span>
@@ -105,6 +106,19 @@ export const WorkStatusUsageSection: React.FC = () => {
) : modeLabel}
</span>
)}
action={(
<Button
size="icon"
variant="ghost"
className="size-6 shrink-0 text-muted-foreground"
onClick={() => void fetchQuotas(dropdownProviderIds)}
aria-label={t('settings.usage.sidebar.actions.refreshAria')}
title={t('settings.usage.sidebar.actions.refreshTitle')}
disabled={isLoading}
>
<Icon name="refresh" className={cn('size-3.5', isLoading && 'animate-spin')} />
</Button>
)}
>
{groups.map((group) => (
<React.Fragment key={group.providerId}>