Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions src/browser/components/ContextUsageIndicatorButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div
className="border-dashed-warning pointer-events-none absolute top-0 z-40 h-full w-0 border-l"
style={{ left: `${threshold}%` }}
/>
);
};
/** Compact threshold tick mark for the button view */
const CompactThresholdIndicator: React.FC<{ threshold: number }> = ({ threshold }) => {
if (threshold >= 100) return null;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -118,9 +149,26 @@ const AutoCompactSettings: React.FC<{
<div>
<div className="relative w-full py-1.5">
<TokenMeter segments={data.segments} orientation="horizontal" />
{showOutputReserveIndicator && outputReserveThreshold !== null && (
<OutputReserveIndicator threshold={outputReserveThreshold} />
)}
{showUsageSlider && <HorizontalThresholdSlider config={usageConfig} />}
</div>
{showUsageSlider && <PercentTickMarks />}
{showOutputReserveIndicator &&
outputReserveThreshold !== null &&
outputReserveTokens !== null && (
<div className="text-muted mt-1 text-[10px]">
Output reserve starts at {outputReserveThreshold.toFixed(1)}% (
{formatTokens(outputReserveTokens)} prompt max)
</div>
)}
{showOutputReserveWarning && outputReserveThreshold !== null && (
<div className="text-warning mt-1 text-[10px]">
Auto-compact threshold is above the output reserve ({outputReserveThreshold.toFixed(1)}
%). Requests may hit context_exceeded before auto-compact runs.
</div>
)}
</div>

{/* Idle-based auto-compact */}
Expand Down
56 changes: 54 additions & 2 deletions src/browser/components/RightSidebar/ContextUsageBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div
className="border-dashed-warning pointer-events-none absolute top-0 z-40 h-full w-0 border-l"
style={{ left: `${threshold}%` }}
/>
);
};

interface ContextUsageBarProps {
data: TokenMeterData;
/** Auto-compaction settings for threshold slider */
Expand All @@ -17,15 +29,36 @@ const ContextUsageBarComponent: React.FC<ContextUsageBarProps> = ({
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)}%)` : "";

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 (
<div data-testid={testId} className="relative flex flex-col gap-1">
<div className="flex items-baseline justify-between">
Expand All @@ -43,9 +76,28 @@ const ContextUsageBarComponent: React.FC<ContextUsageBarProps> = ({

<div className="relative w-full overflow-hidden py-2">
<TokenMeter segments={data.segments} orientation="horizontal" />
{showOutputReserveIndicator && outputReserveThreshold !== null && (
<OutputReserveIndicator threshold={outputReserveThreshold} />
)}
{showThresholdSlider && <HorizontalThresholdSlider config={autoCompaction} />}
</div>

{showOutputReserveIndicator &&
outputReserveThreshold !== null &&
outputReserveTokens !== null && (
<div className="text-muted mt-1 text-[11px]">
Output reserve starts at {outputReserveThreshold.toFixed(1)}% (
{formatTokens(outputReserveTokens)} prompt max)
</div>
)}

{showOutputReserveWarning && outputReserveThreshold !== null && (
<div className="text-warning mt-1 text-[11px]">
Auto-compact threshold is above the output reserve ({outputReserveThreshold.toFixed(1)}%).
Requests may hit context_exceeded before auto-compact runs.
</div>
)}

{showWarning && (
<div className="text-subtle mt-2 text-[11px] italic">
Unknown model limits - showing relative usage only
Expand Down
3 changes: 3 additions & 0 deletions src/common/utils/tokens/tokenMeterUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export interface TokenMeterData {
segments: TokenSegment[];
totalTokens: number;
maxTokens?: number;
maxOutputTokens?: number;
totalPercentage: number;
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -96,6 +98,7 @@ export function calculateTokenMeterData(
segments,
totalTokens: totalUsed,
maxTokens,
maxOutputTokens,
totalPercentage: verticalProportions
? maxTokens
? (totalUsed / maxTokens) * 100
Expand Down
Loading