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:
@@ -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: {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user