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.
This commit is contained in:
Mohamed Boudra
2026-06-20 20:19:49 +07:00
parent 8bd6c617a3
commit 3493e0492d
4 changed files with 74 additions and 27 deletions
@@ -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 (
<View style={geometry.containerStyle}>
<Svg
width={geometry.svgSize}
height={geometry.svgSize}
viewBox={`0 0 ${geometry.svgSize} ${geometry.svgSize}`}
style={styles.svg}
accessibilityElementsHidden
importantForAccessibility="no-hide-descendants"
>
<Circle
cx={geometry.center}
cy={geometry.center}
r={geometry.radius}
fill="none"
stroke={theme.colors.surface3}
strokeWidth={geometry.strokeWidth}
/>
</Svg>
{showPercentage ? <View style={styles.skeletonLabel} /> : null}
</View>
);
}
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: {
+9 -1
View File
@@ -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(
+6 -9
View File
@@ -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 (
<View style={containerStyle}>
<View style={styles.header}>
<ThemedProviderUsageIcon
iconKey={usage.providerId}
size={16}
uniProps={foregroundIconColor}
/>
<ThemedProviderUsageIcon iconKey={usage.providerId} size={14} uniProps={mutedIconColor} />
<Text style={styles.name} numberOfLines={1}>
{usage.displayName}
</Text>
@@ -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],
@@ -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,