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>
  );
}