File size: 4,155 Bytes
63522a5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
import React from "react";
import { useQueryClient } from "@tanstack/react-query";
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import { useActiveConversation } from "#/hooks/query/use-active-conversation";
import { useAgentState } from "#/hooks/use-agent-state";
import { useCondenseConversation } from "#/hooks/mutation/use-condense-conversation";
import {
  type ContextCompactionResult,
  useAwaitContextCompaction,
} from "#/hooks/use-await-context-compaction";
import { AgentState } from "#/types/agent-state";
import useMetricsStore from "#/stores/metrics-store";
import { useEventStore } from "#/stores/use-event-store";
import {
  displayErrorToast,
  displaySuccessToast,
} from "#/utils/custom-toast-handlers";
import { formatCompactTokenCount } from "#/utils/format-token-count";
import { retrieveAxiosErrorMessage } from "#/utils/retrieve-axios-error-message";

/**
 * Shared compact/condense action used by the Usage panel CTA and the
 * composer context-window popover.
 */
export function useCompactContextAction(perTurnToken: number = 0) {
  const { t } = useTranslation("openhands");
  const queryClient = useQueryClient();
  const { data: conversation } = useActiveConversation();
  const { curAgentState } = useAgentState();
  const { mutate: condense, isPending } = useCondenseConversation();
  const [beforeToken, setBeforeToken] = React.useState<number | null>(null);
  const baselineEventIdsRef = React.useRef<Set<string | number> | null>(null);

  const isAgentBusy =
    curAgentState === AgentState.RUNNING ||
    curAgentState === AgentState.LOADING;
  const isCompacting = isPending || beforeToken !== null;
  const isDisabled = !conversation?.id || isAgentBusy || isCompacting;
  const description = t(I18nKey.CONVERSATION$COMPACT_CONTEXT_DESCRIPTION);

  const handleCompactionComplete = React.useEffectEvent(
    (result: ContextCompactionResult) => {
      setBeforeToken(null);
      baselineEventIdsRef.current = null;
      if (conversation?.id) {
        queryClient.invalidateQueries({
          queryKey: ["conversation-metrics", conversation.id],
        });
      }

      if (result.outcome === "timeout") {
        displayErrorToast(t(I18nKey.CONVERSATION$COMPACT_CONTEXT_FAILED));
        return;
      }

      if (result.savedToken > 0) {
        displaySuccessToast(
          t(I18nKey.CONVERSATION$COMPACT_CONTEXT_COMPLETE, {
            saved: formatCompactTokenCount(result.savedToken),
            before: formatCompactTokenCount(result.beforeToken),
            after: formatCompactTokenCount(result.afterToken),
          }),
        );
        return;
      }

      displaySuccessToast(
        t(I18nKey.CONVERSATION$COMPACT_CONTEXT_COMPLETE_NO_CHANGE),
      );
    },
  );

  useAwaitContextCompaction({
    beforeToken,
    baselineEventIds: beforeToken !== null ? baselineEventIdsRef.current : null,
    onComplete: handleCompactionComplete,
  });

  const handleCompact = () => {
    if (!conversation?.id || isCompacting) return;

    const snapshot =
      useMetricsStore.getState().usage?.per_turn_token ?? perTurnToken;
    // The condense POST can return only after the server already emitted its
    // Condensation event, so the await baseline must be captured *before* the
    // request fires, not when the post-ack effect starts.
    baselineEventIdsRef.current = new Set(useEventStore.getState().eventIds);

    condense(
      {
        conversationId: conversation.id,
        conversationUrl: conversation.conversation_url,
        sessionApiKey: conversation.session_api_key,
      },
      {
        onSuccess: () => {
          setBeforeToken(snapshot);
          displaySuccessToast(t(I18nKey.CONVERSATION$COMPACT_CONTEXT_STARTED));
        },
        onError: (error) => {
          setBeforeToken(null);
          baselineEventIdsRef.current = null;
          displayErrorToast(
            retrieveAxiosErrorMessage(error) ||
              t(I18nKey.CONVERSATION$COMPACT_CONTEXT_FAILED),
          );
        },
      },
    );
  };

  return {
    handleCompact,
    isCompacting,
    isDisabled,
    description,
  };
}