From 2227540fadb1e6f6a18e090d70c7f0d19a244ac2 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sun, 31 May 2026 22:38:46 +0700 Subject: [PATCH] Show context usage beside compact mode selector --- .../src/components/context-window-meter.tsx | 57 ++++++++++++++----- packages/app/src/composer/index.tsx | 40 +++++++------ 2 files changed, 62 insertions(+), 35 deletions(-) diff --git a/packages/app/src/components/context-window-meter.tsx b/packages/app/src/components/context-window-meter.tsx index 0b8da5ef5..a1bb92c1b 100644 --- a/packages/app/src/components/context-window-meter.tsx +++ b/packages/app/src/components/context-window-meter.tsx @@ -7,13 +7,19 @@ interface ContextWindowMeterProps { maxTokens: number; usedTokens: number; totalCostUsd?: number | null; + showPercentage?: boolean; } const SVG_SIZE = 16; +const COMPACT_SVG_SIZE = 14; 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 CIRCUMFERENCE = 2 * Math.PI * RADIUS; +const COMPACT_CIRCUMFERENCE = 2 * Math.PI * COMPACT_RADIUS; function isValidMaxTokens(value: number): boolean { return Number.isFinite(value) && value > 0; @@ -72,6 +78,7 @@ export function ContextWindowMeter({ maxTokens, usedTokens, totalCostUsd, + showPercentage = false, }: ContextWindowMeterProps) { const { theme } = useUnistyles(); const percentage = getUsagePercentage(maxTokens, usedTokens); @@ -82,47 +89,56 @@ export function ContextWindowMeter({ const clampedPercentage = clampPercentage(percentage); const roundedPercentage = Math.round(percentage); - const dashOffset = CIRCUMFERENCE - (clampedPercentage / 100) * CIRCUMFERENCE; + const svgSize = showPercentage ? COMPACT_SVG_SIZE : SVG_SIZE; + const center = showPercentage ? COMPACT_CENTER : CENTER; + const radius = showPercentage ? COMPACT_RADIUS : RADIUS; + const strokeWidth = showPercentage ? COMPACT_STROKE_WIDTH : STROKE_WIDTH; + const circumference = showPercentage ? COMPACT_CIRCUMFERENCE : CIRCUMFERENCE; + const dashOffset = circumference - (clampedPercentage / 100) * circumference; const colors = getMeterColors(clampedPercentage, theme); const formattedSessionCost = typeof totalCostUsd === "number" ? formatSessionCost(totalCostUsd) : null; + const containerStyle = showPercentage ? styles.containerWithLabel : styles.container; return ( + {showPercentage ? ( + {`${roundedPercentage}%`} + ) : null} @@ -149,9 +165,22 @@ const styles = StyleSheet.create((theme) => ({ alignItems: "center", justifyContent: "center", }, + containerWithLabel: { + height: 28, + flexDirection: "row", + alignItems: "center", + justifyContent: "center", + gap: theme.spacing[1], + borderRadius: theme.borderRadius.full, + }, svg: { transform: [{ rotate: "-90deg" }], }, + percentageLabel: { + color: theme.colors.foregroundMuted, + fontSize: theme.fontSize.sm, + fontWeight: theme.fontWeight.normal, + }, tooltipContent: { gap: theme.spacing[1], }, diff --git a/packages/app/src/composer/index.tsx b/packages/app/src/composer/index.tsx index 2d10d7f70..954d90f24 100644 --- a/packages/app/src/composer/index.tsx +++ b/packages/app/src/composer/index.tsx @@ -190,6 +190,7 @@ function renderContextWindowMeter( contextWindowMaxTokens: number | null, contextWindowUsedTokens: number | null, totalCostUsd: number | null, + showPercentage: boolean, ): ReactElement | null { if (contextWindowMaxTokens === null || contextWindowUsedTokens === null) { return null; @@ -199,6 +200,7 @@ function renderContextWindowMeter( maxTokens={contextWindowMaxTokens} usedTokens={contextWindowUsedTokens} totalCostUsd={totalCostUsd} + showPercentage={showPercentage} /> ); } @@ -206,13 +208,13 @@ function renderContextWindowMeter( function resolveContextWindowPlacement( meter: ReactElement | null, isMobile: boolean, -): { beforeVoiceContent: ReactNode; footerRight: ReactNode } { +): { beforeVoiceContent: ReactNode; footerInlineContent: ReactNode } { if (isMobile) { - return { beforeVoiceContent: null, footerRight: meter }; + return { beforeVoiceContent: null, footerInlineContent: meter }; } return { beforeVoiceContent: {meter}, - footerRight: null, + footerInlineContent: null, }; } @@ -238,13 +240,18 @@ interface RenderAttachmentTrayArgs { handleRemoveAttachment: (index: number) => void; } -function renderComposerFooter(footer: ReactNode, footerRight: ReactNode): ReactElement | null { - if (!footer && !footerRight) return null; +function renderComposerFooter( + footer: ReactNode, + footerInlineContent: ReactNode, +): ReactElement | null { + if (!footer && !footerInlineContent) return null; return ( - {footer} - {footerRight} + + {footer} + {footerInlineContent} + ); @@ -1468,10 +1475,11 @@ export function Composer({ contextWindowMaxTokens, contextWindowUsedTokens, agentState.totalCostUsd, + isMobile, ), - [contextWindowMaxTokens, contextWindowUsedTokens, agentState.totalCostUsd], + [contextWindowMaxTokens, contextWindowUsedTokens, agentState.totalCostUsd, isMobile], ); - const { beforeVoiceContent, footerRight } = useMemo( + const { beforeVoiceContent, footerInlineContent } = useMemo( () => resolveContextWindowPlacement(contextWindowMeter, isMobile), [contextWindowMeter, isMobile], ); @@ -1718,7 +1726,7 @@ export function Composer({ - {renderComposerFooter(footer, footerRight)} + {renderComposerFooter(footer, footerInlineContent)} ); } @@ -1782,6 +1790,7 @@ const styles = StyleSheet.create((theme: Theme) => ({ flexShrink: 1, flexDirection: "row", alignItems: "center", + gap: theme.spacing[1], // On mobile, cancel the leading glyph's internal padding (chip paddingHorizontal) // so its icon aligns to the composer border before the footer inset is applied. marginLeft: { @@ -1789,17 +1798,6 @@ const styles = StyleSheet.create((theme: Theme) => ({ md: 0, }, }, - footerRight: { - flexShrink: 0, - flexDirection: "row", - alignItems: "center", - // On mobile, cancel the trailing glyph's internal inset (28px box around a 16px - // ring) so its right edge aligns to the composer border before the footer inset. - marginRight: { - xs: -6, - md: 0, - }, - }, messageInputContainer: { position: "relative", width: "100%",