File size: 3,609 Bytes
3201ca6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 | import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import { cn } from "#/utils/utils";
import { getContextWindowUsagePercentage } from "#/utils/format-token-count";
/** Context fill percentage above which the meter warns and the compact CTA becomes prominent. */
export const CONTEXT_FILL_WARNING_PERCENT = 70;
/** Context fill percentage above which the meter signals danger. */
export const CONTEXT_FILL_DANGER_PERCENT = 90;
export type ContextFillTone = "neutral" | "warning" | "danger";
export function getContextFillTone(usagePercentage: number): ContextFillTone {
if (usagePercentage > CONTEXT_FILL_DANGER_PERCENT) {
return "danger";
}
if (usagePercentage > CONTEXT_FILL_WARNING_PERCENT) {
return "warning";
}
return "neutral";
}
interface ContextMeterProps {
/** Tokens currently held in the agent's context (`per_turn_token`). */
perTurnToken: number;
/** Model context window size in tokens. */
contextWindow: number;
}
/**
* Context-fill progress bar: neutral below
* {@link CONTEXT_FILL_WARNING_PERCENT}, amber above it, red above
* {@link CONTEXT_FILL_DANGER_PERCENT}. Shows the raw token numbers underneath.
*/
export function ContextMeter({
perTurnToken,
contextWindow,
}: ContextMeterProps) {
const { t } = useTranslation("openhands");
// Some models (e.g. via OpenRouter) report no context window size; show the
// raw token count without a misleading "x / 0" or "0.0% used" readout.
const isWindowUnknown = contextWindow <= 0;
// Capped at 100 by getContextWindowUsagePercentage, so the bar width and
// the label never read above a full window.
const usagePercentage = getContextWindowUsagePercentage(
perTurnToken,
contextWindow,
);
const tone = getContextFillTone(usagePercentage);
const isWarning = tone === "warning";
const isDanger = tone === "danger";
const roundedPercentage = Math.round(usagePercentage);
const remainingPercentage = Math.max(0, 100 - roundedPercentage);
const usagePercentLabel = isWindowUnknown
? t(I18nKey.CONVERSATION$CONTEXT_WINDOW_UNKNOWN)
: `${roundedPercentage}% ${t(I18nKey.CONVERSATION$USED)} (${remainingPercentage}% ${t(I18nKey.CONVERSATION$LEFT)})`;
return (
<div data-testid="context-meter" className="flex flex-col gap-2">
<div className="flex items-center justify-between gap-2">
<span className="font-semibold">
{t(I18nKey.CONVERSATION$CONTEXT_WINDOW)}
</span>
<span
className={cn(
"shrink-0 text-xs",
isDanger
? "text-red-500"
: isWarning
? "text-amber-500"
: "text-[var(--oh-muted)]",
)}
>
{usagePercentLabel}
</span>
</div>
<div className="relative h-1.5 w-full rounded-full bg-tertiary">
<div
data-testid="context-meter-bar"
className={cn(
"absolute inset-y-0 left-0 rounded-full transition-all duration-300",
isDanger
? "bg-red-500"
: isWarning
? "bg-amber-500"
: "bg-foreground",
)}
// runtime usage-percentage width
style={{ width: `${usagePercentage}%` }}
/>
</div>
<div className="flex justify-end">
<span className="text-xs text-[var(--oh-muted)]">
{isWindowUnknown
? perTurnToken.toLocaleString()
: `${perTurnToken.toLocaleString()} / ${contextWindow.toLocaleString()}`}
</span>
</div>
</div>
);
}
|