mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-13 23:51:16 -07:00
171 lines
4.7 KiB
TypeScript
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,
|
|
]);
|
|
}
|