File size: 3,015 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
import { Info, Loader2, Minimize } from "lucide-react";
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import { BrandButton } from "#/components/features/settings/brand-button";
import { StyledTooltip } from "#/components/shared/buttons/styled-tooltip";
import { useCompactContextAction } from "#/hooks/use-compact-context-action";
import { CONTEXT_FILL_WARNING_PERCENT } from "./context-meter";

interface CompactContextButtonProps {
  /** Current context fill percentage; past the warning threshold the button becomes a prominent CTA. */
  fillPercent: number;
  /** Optional live per-turn token count used as a fallback snapshot before compaction. */
  perTurnToken?: number;
}

/**
 * "Compact context" action: POSTs `/api/conversations/{id}/condense` with a
 * spinner while in flight and toasts for start, completion (with tokens
 * freed), and failure. Disabled while the agent is actively running (the
 * server would race the active step) and promoted to a primary
 * call-to-action once the context fill passes
 * {@link CONTEXT_FILL_WARNING_PERCENT}.
 */
export function CompactContextButton({
  fillPercent,
  perTurnToken = 0,
}: CompactContextButtonProps) {
  const { t } = useTranslation("openhands");
  const { handleCompact, isCompacting, isDisabled, description } =
    useCompactContextAction(perTurnToken);
  const isHighFill = fillPercent > CONTEXT_FILL_WARNING_PERCENT;

  return (
    <div
      data-testid="compact-context-section"
      className="flex flex-col gap-2 border-t border-[var(--oh-border-subtle)] pt-3"
    >
      {isHighFill && (
        <span className="text-xs text-amber-500">
          {t(I18nKey.CONVERSATION$CONTEXT_FILLING_UP)}
        </span>
      )}
      <div className="flex items-center gap-2">
        <BrandButton
          testId="compact-context-button"
          type="button"
          variant={isHighFill ? "primary" : "secondary"}
          isDisabled={isDisabled}
          aria-busy={isCompacting}
          onClick={handleCompact}
          startContent={
            isCompacting ? (
              <Loader2 className="h-4 w-4 animate-spin" aria-hidden />
            ) : (
              <Minimize className="h-4 w-4" aria-hidden />
            )
          }
          className="w-fit"
        >
          {t(I18nKey.CONVERSATION$COMPACT_CONTEXT)}
        </BrandButton>
        <StyledTooltip
          content={description}
          placement="top"
          tooltipClassName="max-w-[220px] text-xs font-normal leading-relaxed whitespace-normal"
        >
          <button
            type="button"
            data-testid="compact-context-info"
            aria-label={description}
            className="flex size-5 shrink-0 items-center justify-center rounded-full text-[var(--oh-muted)] hover:text-[var(--oh-foreground)] transition-colors cursor-help"
          >
            <Info className="size-3.5" aria-hidden />
          </button>
        </StyledTooltip>
      </div>
    </div>
  );
}