diff --git a/src/browser/components/ContextUsageIndicatorButton.tsx b/src/browser/components/ContextUsageIndicatorButton.tsx index 65f2f46832..efe283cb0d 100644 --- a/src/browser/components/ContextUsageIndicatorButton.tsx +++ b/src/browser/components/ContextUsageIndicatorButton.tsx @@ -11,6 +11,18 @@ import { Switch } from "./ui/switch"; import { formatTokens, type TokenMeterData } from "@/common/utils/tokens/tokenMeterUtils"; import { cn } from "@/common/lib/utils"; +/** Output reserve indicator (context limit minus max output tokens) */ +const OutputReserveIndicator: React.FC<{ threshold: number }> = (props) => { + const threshold = props.threshold; + if (threshold <= 0 || threshold >= 100) return null; + + return ( +
+ ); +}; /** Compact threshold tick mark for the button view */ const CompactThresholdIndicator: React.FC<{ threshold: number }> = ({ threshold }) => { if (threshold >= 100) return null; @@ -79,6 +91,25 @@ const AutoCompactSettings: React.FC<{ const showUsageSlider = usageConfig && data.maxTokens; const isIdleEnabled = idleConfig?.hours !== null && idleConfig?.hours !== undefined; + const outputReserveThreshold = (() => { + if (!data.maxTokens || !data.maxOutputTokens) return null; + if (data.maxOutputTokens <= 0 || data.maxOutputTokens >= data.maxTokens) return null; + const raw = ((data.maxTokens - data.maxOutputTokens) / data.maxTokens) * 100; + return Math.max(0, Math.min(100, raw)); + })(); + + const outputReserveTokens = + data.maxTokens && data.maxOutputTokens ? data.maxTokens - data.maxOutputTokens : null; + + const showOutputReserveIndicator = Boolean(showUsageSlider && outputReserveThreshold !== null); + const showOutputReserveWarning = Boolean( + showUsageSlider && + usageConfig && + usageConfig.threshold < 100 && + outputReserveThreshold !== null && + usageConfig.threshold > outputReserveThreshold + ); + const handleIdleToggle = (enabled: boolean) => { if (!idleConfig) return; const parsed = parseInt(idleInputValue, 10); @@ -118,9 +149,26 @@ const AutoCompactSettings: React.FC<{
+ {showOutputReserveIndicator && outputReserveThreshold !== null && ( + + )} {showUsageSlider && }
{showUsageSlider && } + {showOutputReserveIndicator && + outputReserveThreshold !== null && + outputReserveTokens !== null && ( +
+ Output reserve starts at {outputReserveThreshold.toFixed(1)}% ( + {formatTokens(outputReserveTokens)} prompt max) +
+ )} + {showOutputReserveWarning && outputReserveThreshold !== null && ( +
+ Auto-compact threshold is above the output reserve ({outputReserveThreshold.toFixed(1)} + %). Requests may hit context_exceeded before auto-compact runs. +
+ )}
{/* Idle-based auto-compact */} diff --git a/src/browser/components/RightSidebar/ContextUsageBar.tsx b/src/browser/components/RightSidebar/ContextUsageBar.tsx index 4d9fefce51..d25d3147b8 100644 --- a/src/browser/components/RightSidebar/ContextUsageBar.tsx +++ b/src/browser/components/RightSidebar/ContextUsageBar.tsx @@ -3,6 +3,18 @@ import { TokenMeter } from "./TokenMeter"; import { HorizontalThresholdSlider, type AutoCompactionConfig } from "./ThresholdSlider"; import { formatTokens, type TokenMeterData } from "@/common/utils/tokens/tokenMeterUtils"; +const OutputReserveIndicator: React.FC<{ threshold: number }> = (props) => { + const threshold = props.threshold; + if (threshold <= 0 || threshold >= 100) return null; + + return ( +
+ ); +}; + interface ContextUsageBarProps { data: TokenMeterData; /** Auto-compaction settings for threshold slider */ @@ -17,8 +29,6 @@ const ContextUsageBarComponent: React.FC = ({ showTitle = true, testId, }) => { - if (data.totalTokens === 0) return null; - const totalDisplay = formatTokens(data.totalTokens); const maxDisplay = data.maxTokens ? ` / ${formatTokens(data.maxTokens)}` : ""; const percentageDisplay = data.maxTokens ? ` (${data.totalPercentage.toFixed(1)}%)` : ""; @@ -26,6 +36,29 @@ const ContextUsageBarComponent: React.FC = ({ const showWarning = !data.maxTokens; const showThresholdSlider = autoCompaction && data.maxTokens; + const outputReserveThreshold = (() => { + if (!data.maxTokens || !data.maxOutputTokens) return null; + if (data.maxOutputTokens <= 0 || data.maxOutputTokens >= data.maxTokens) return null; + const raw = ((data.maxTokens - data.maxOutputTokens) / data.maxTokens) * 100; + return Math.max(0, Math.min(100, raw)); + })(); + + const outputReserveTokens = + data.maxTokens && data.maxOutputTokens ? data.maxTokens - data.maxOutputTokens : null; + + const showOutputReserveIndicator = Boolean( + showThresholdSlider && outputReserveThreshold !== null + ); + const showOutputReserveWarning = Boolean( + showThresholdSlider && + autoCompaction && + autoCompaction.threshold < 100 && + outputReserveThreshold !== null && + autoCompaction.threshold > outputReserveThreshold + ); + + if (data.totalTokens === 0) return null; + return (
@@ -43,9 +76,28 @@ const ContextUsageBarComponent: React.FC = ({
+ {showOutputReserveIndicator && outputReserveThreshold !== null && ( + + )} {showThresholdSlider && }
+ {showOutputReserveIndicator && + outputReserveThreshold !== null && + outputReserveTokens !== null && ( +
+ Output reserve starts at {outputReserveThreshold.toFixed(1)}% ( + {formatTokens(outputReserveTokens)} prompt max) +
+ )} + + {showOutputReserveWarning && outputReserveThreshold !== null && ( +
+ Auto-compact threshold is above the output reserve ({outputReserveThreshold.toFixed(1)}%). + Requests may hit context_exceeded before auto-compact runs. +
+ )} + {showWarning && (
Unknown model limits - showing relative usage only diff --git a/src/common/utils/tokens/tokenMeterUtils.ts b/src/common/utils/tokens/tokenMeterUtils.ts index 5943fa99f7..ca13a61728 100644 --- a/src/common/utils/tokens/tokenMeterUtils.ts +++ b/src/common/utils/tokens/tokenMeterUtils.ts @@ -23,6 +23,7 @@ export interface TokenMeterData { segments: TokenSegment[]; totalTokens: number; maxTokens?: number; + maxOutputTokens?: number; totalPercentage: number; } @@ -65,6 +66,7 @@ export function calculateTokenMeterData( const modelStats = getModelStats(model); const maxTokens = use1M && supports1MContext(model) ? 1_000_000 : modelStats?.max_input_tokens; + const maxOutputTokens = modelStats?.max_output_tokens; // Total tokens used in the request. // For Anthropic prompt caching, cacheCreate tokens are reported separately but still @@ -96,6 +98,7 @@ export function calculateTokenMeterData( segments, totalTokens: totalUsed, maxTokens, + maxOutputTokens, totalPercentage: verticalProportions ? maxTokens ? (totalUsed / maxTokens) * 100