Files
gemini-cli/packages/cli/src/ui/hooks/useRunEventNotifications.ts
2026-02-18 20:28:17 +00:00

171 lines
4.7 KiB
TypeScript

/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { useEffect, useMemo, useRef } from 'react';
import {
StreamingState,
type ConfirmationRequest,
type HistoryItemWithoutId,
type PermissionConfirmationRequest,
} from '../types.js';
import { getPendingAttentionNotification } from '../utils/pendingAttentionNotification.js';
import {
buildRunEventNotificationContent,
notifyViaTerminal,
} from '../../utils/terminalNotifications.js';
const ATTENTION_NOTIFICATION_COOLDOWN_MS = 20_000;
interface RunEventNotificationParams {
notificationsEnabled: boolean;
isFocused: boolean;
hasReceivedFocusEvent: boolean;
streamingState: StreamingState;
hasPendingActionRequired: boolean;
pendingHistoryItems: HistoryItemWithoutId[];
commandConfirmationRequest: ConfirmationRequest | null;
authConsentRequest: ConfirmationRequest | null;
permissionConfirmationRequest: PermissionConfirmationRequest | null;
hasConfirmUpdateExtensionRequests: boolean;
hasLoopDetectionConfirmationRequest: boolean;
terminalName?: string;
}
export function useRunEventNotifications({
notificationsEnabled,
isFocused,
hasReceivedFocusEvent,
streamingState,
hasPendingActionRequired,
pendingHistoryItems,
commandConfirmationRequest,
authConsentRequest,
permissionConfirmationRequest,
hasConfirmUpdateExtensionRequests,
hasLoopDetectionConfirmationRequest,
}: RunEventNotificationParams): void {
const pendingAttentionNotification = useMemo(
() =>
getPendingAttentionNotification(
pendingHistoryItems,
commandConfirmationRequest,
authConsentRequest,
permissionConfirmationRequest,
hasConfirmUpdateExtensionRequests,
hasLoopDetectionConfirmationRequest,
),
[
pendingHistoryItems,
commandConfirmationRequest,
authConsentRequest,
permissionConfirmationRequest,
hasConfirmUpdateExtensionRequests,
hasLoopDetectionConfirmationRequest,
],
);
const hadPendingAttentionRef = useRef(false);
const previousFocusedRef = useRef(isFocused);
const previousStreamingStateRef = useRef(streamingState);
const lastSentAttentionNotificationRef = useRef<{
key: string;
sentAt: number;
} | null>(null);
useEffect(() => {
if (!notificationsEnabled) {
return;
}
const wasFocused = previousFocusedRef.current;
previousFocusedRef.current = isFocused;
const hasPendingAttention = pendingAttentionNotification !== null;
const hadPendingAttention = hadPendingAttentionRef.current;
hadPendingAttentionRef.current = hasPendingAttention;
if (!hasPendingAttention) {
lastSentAttentionNotificationRef.current = null;
return;
}
const shouldSuppressForFocus = hasReceivedFocusEvent && isFocused;
if (shouldSuppressForFocus) {
return;
}
const justEnteredAttentionState = !hadPendingAttention;
const justLostFocus = wasFocused && !isFocused;
const now = Date.now();
const currentKey = pendingAttentionNotification.key;
const lastSent = lastSentAttentionNotificationRef.current;
const keyChanged = !lastSent || lastSent.key !== currentKey;
const onCooldown =
!!lastSent &&
lastSent.key === currentKey &&
now - lastSent.sentAt < ATTENTION_NOTIFICATION_COOLDOWN_MS;
const shouldNotifyByStateChange = hasReceivedFocusEvent
? justEnteredAttentionState || justLostFocus || keyChanged
: justEnteredAttentionState || keyChanged;
if (!shouldNotifyByStateChange || onCooldown) {
return;
}
lastSentAttentionNotificationRef.current = {
key: currentKey,
sentAt: now,
};
void notifyViaTerminal(
notificationsEnabled,
buildRunEventNotificationContent(pendingAttentionNotification.event),
);
}, [
isFocused,
hasReceivedFocusEvent,
notificationsEnabled,
pendingAttentionNotification,
]);
useEffect(() => {
if (!notificationsEnabled) {
return;
}
const previousStreamingState = previousStreamingStateRef.current;
previousStreamingStateRef.current = streamingState;
const justCompletedTurn =
previousStreamingState === StreamingState.Responding &&
streamingState === StreamingState.Idle;
const shouldSuppressForFocus = hasReceivedFocusEvent && isFocused;
if (
!justCompletedTurn ||
shouldSuppressForFocus ||
hasPendingActionRequired
) {
return;
}
void notifyViaTerminal(
notificationsEnabled,
buildRunEventNotificationContent({
type: 'session_complete',
detail: 'Gemini CLI finished responding.',
}),
);
}, [
streamingState,
isFocused,
hasReceivedFocusEvent,
notificationsEnabled,
hasPendingActionRequired,
]);
}