mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-19 01:30:42 -07:00
feat(cli): add macOS run-event notifications (interactive only) (#19056)
Co-authored-by: Tommaso Sciortino <sciortino@gmail.com>
This commit is contained in:
170
packages/cli/src/ui/hooks/useRunEventNotifications.ts
Normal file
170
packages/cli/src/ui/hooks/useRunEventNotifications.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
/**
|
||||
* @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,
|
||||
]);
|
||||
}
|
||||
Reference in New Issue
Block a user