fix(usage): refresh work status quotas automatically
This commit is contained in:
@@ -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}>
|
||||
|
||||
Reference in New Issue
Block a user