From 3493e0492d0bd416fca5786c771e2c4ce2624636 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sat, 20 Jun 2026 20:19:49 +0700 Subject: [PATCH] Polish context window usage card and meter Calm the usage popover and settings card: smaller, muted provider logo and name, more space between the header and the usage bars. The popover divider used the `border` token, which equals the popover background in dark themes and was invisible. Switch it to `borderAccent` (the token the popover outlines itself with) and span it edge to edge. Shrink the context meter ring and, while a session is active but usage hasn't arrived yet, render a track-only ring in the final footprint so the real ring fades in without shifting siblings. Agents that never report usage still render nothing. --- .../src/components/context-window-meter.tsx | 66 +++++++++++++++---- packages/app/src/composer/index.tsx | 10 ++- packages/app/src/provider-usage/card.tsx | 15 ++--- .../src/provider-usage/tooltip-section.tsx | 10 ++- 4 files changed, 74 insertions(+), 27 deletions(-) diff --git a/packages/app/src/components/context-window-meter.tsx b/packages/app/src/components/context-window-meter.tsx index 85bdd6585..b81026630 100644 --- a/packages/app/src/components/context-window-meter.tsx +++ b/packages/app/src/components/context-window-meter.tsx @@ -9,23 +9,25 @@ import { useProviderUsage } from "@/provider-usage/use-provider-usage"; import { formatTokenCount } from "./context-window-meter.utils"; interface ContextWindowMeterProps { - maxTokens: number; - usedTokens: number; + maxTokens: number | null; + usedTokens: number | null; totalCostUsd?: number | null; showPercentage?: boolean; serverId?: string; /** The Paseo provider key, e.g. "claude", "gemini", "codex" */ provider?: string | null; + /** Reserve the meter footprint and show a loading ring while usage is pending. */ + pending?: boolean; } -const SVG_SIZE = 16; -const COMPACT_SVG_SIZE = 14; +const SVG_SIZE = 14; +const COMPACT_SVG_SIZE = 12; const CENTER = SVG_SIZE / 2; const COMPACT_CENTER = COMPACT_SVG_SIZE / 2; -const RADIUS = 7; -const COMPACT_RADIUS = 6; -const STROKE_WIDTH = 2.25; -const COMPACT_STROKE_WIDTH = 2; +const RADIUS = 6; +const COMPACT_RADIUS = 5; +const STROKE_WIDTH = 2; +const COMPACT_STROKE_WIDTH = 1.75; const CIRCUMFERENCE = 2 * Math.PI * RADIUS; const COMPACT_CIRCUMFERENCE = 2 * Math.PI * COMPACT_RADIUS; @@ -100,6 +102,7 @@ export function ContextWindowMeter({ showPercentage = false, serverId, provider, + pending = false, }: ContextWindowMeterProps) { const { theme } = useUnistyles(); const { t } = useTranslation(); @@ -108,7 +111,8 @@ export function ContextWindowMeter({ serverId ?? null, { enabled: isTooltipOpen }, ); - const percentage = getUsagePercentage(maxTokens, usedTokens); + const percentage = + maxTokens !== null && usedTokens !== null ? getUsagePercentage(maxTokens, usedTokens) : null; const handleTooltipOpenChange = useCallback( (nextOpen: boolean) => { setIsTooltipOpen(nextOpen); @@ -119,14 +123,42 @@ export function ContextWindowMeter({ [refreshProviderUsage], ); - if (percentage === null) { - return null; + const geometry = getMeterGeometry(showPercentage); + + // No usage yet: reserve the footprint with a track-only ring while a session is + // active so the real ring fades in without shifting siblings. Render nothing when + // no usage is expected. + if (percentage === null || maxTokens === null || usedTokens === null) { + if (!pending) { + return null; + } + return ( + + + + + {showPercentage ? : null} + + ); } const clampedPercentage = clampPercentage(percentage); const roundedPercentage = Math.round(percentage); - const { svgSize, center, radius, strokeWidth, circumference, containerStyle } = - getMeterGeometry(showPercentage); + const { svgSize, center, radius, strokeWidth, circumference, containerStyle } = geometry; const dashOffset = circumference - (clampedPercentage / 100) * circumference; const colors = getMeterColors(clampedPercentage, theme); const formattedSessionCost = @@ -230,8 +262,14 @@ const styles = StyleSheet.create((theme) => ({ fontSize: theme.fontSize.sm, fontWeight: theme.fontWeight.normal, }, + skeletonLabel: { + width: 22, + height: theme.fontSize.sm, + borderRadius: theme.borderRadius.full, + backgroundColor: theme.colors.surface3, + }, tooltipContent: { - gap: theme.spacing[1], + gap: theme.spacing[1.5], minWidth: 200, }, tooltipTitle: { diff --git a/packages/app/src/composer/index.tsx b/packages/app/src/composer/index.tsx index fc04ae8af..72d5359bd 100644 --- a/packages/app/src/composer/index.tsx +++ b/packages/app/src/composer/index.tsx @@ -209,8 +209,10 @@ function renderContextWindowMeter( showPercentage: boolean, serverId: string, provider: string | null, + pending: boolean, ): ReactElement | null { - if (contextWindowMaxTokens === null || contextWindowUsedTokens === null) { + const hasData = contextWindowMaxTokens !== null && contextWindowUsedTokens !== null; + if (!hasData && !pending) { return null; } return ( @@ -221,6 +223,7 @@ function renderContextWindowMeter( showPercentage={showPercentage} serverId={serverId} provider={provider} + pending={pending} /> ); } @@ -1642,6 +1645,9 @@ export function Composer({ agentState.contextWindowUsedTokens, ); + const contextWindowPending = + agentState.status === "initializing" || agentState.status === "running"; + const contextWindowMeter = useMemo( () => renderContextWindowMeter( @@ -1651,6 +1657,7 @@ export function Composer({ isCompactLayout, serverId, agentState.provider, + contextWindowPending, ), [ contextWindowMaxTokens, @@ -1659,6 +1666,7 @@ export function Composer({ isCompactLayout, serverId, agentState.provider, + contextWindowPending, ], ); const { beforeVoiceContent, footerInlineContent } = useMemo( diff --git a/packages/app/src/provider-usage/card.tsx b/packages/app/src/provider-usage/card.tsx index 0260425fd..d6c47ba1f 100644 --- a/packages/app/src/provider-usage/card.tsx +++ b/packages/app/src/provider-usage/card.tsx @@ -22,7 +22,7 @@ function ProviderUsageIcon({ iconKey, size, color = "" }: ProviderUsageIconProps const ThemedProviderUsageIcon = withUnistyles(ProviderUsageIcon); -const foregroundIconColor = (theme: Theme) => ({ color: theme.colors.foreground }); +const mutedIconColor = (theme: Theme) => ({ color: theme.colors.foregroundMuted }); function statusText(usage: ProviderUsage): string | null { if (usage.status === "available") return null; @@ -65,11 +65,7 @@ export function ProviderUsageCard({ return ( - + {usage.displayName} @@ -129,11 +125,12 @@ const styles = StyleSheet.create((theme) => ({ gap: theme.spacing[3], }, containerPadded: { + gap: theme.spacing[4], paddingVertical: theme.spacing[4], paddingHorizontal: theme.spacing[4], }, containerCompact: { - gap: theme.spacing[2], + gap: theme.spacing[3], }, header: { flexDirection: "row", @@ -143,7 +140,7 @@ const styles = StyleSheet.create((theme) => ({ name: { flexShrink: 1, color: theme.colors.foreground, - fontSize: theme.fontSize.base, + fontSize: theme.fontSize.sm, }, headerSpacer: { flex: 1, @@ -170,7 +167,7 @@ const styles = StyleSheet.create((theme) => ({ fontSize: theme.fontSize.xs, }, bars: { - gap: theme.spacing[2], + gap: theme.spacing[3], }, details: { gap: theme.spacing[1], diff --git a/packages/app/src/provider-usage/tooltip-section.tsx b/packages/app/src/provider-usage/tooltip-section.tsx index a9861b746..2a94a9652 100644 --- a/packages/app/src/provider-usage/tooltip-section.tsx +++ b/packages/app/src/provider-usage/tooltip-section.tsx @@ -54,9 +54,13 @@ export function ProviderUsageTooltipSection({ const styles = StyleSheet.create((theme) => ({ divider: { - height: StyleSheet.hairlineWidth, - backgroundColor: theme.colors.border, - marginVertical: theme.spacing[1] + 1, + height: 1, + // Same token the popover draws its own outline with, so the rule reads as the + // popover's edge. `border` is invisible here (equals the popover background). + backgroundColor: theme.colors.borderAccent, + marginVertical: theme.spacing[2], + // Cancel the tooltip content's horizontal padding so the rule spans edge to edge. + marginHorizontal: -theme.spacing[2], }, detail: { color: theme.colors.foregroundMuted,