mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-16 09:01:17 -07:00
feat(core/ui): enhance retry mechanism and UX (#16489)
This commit is contained in:
@@ -802,6 +802,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
activePtyId,
|
||||
loopDetectionConfirmationRequest,
|
||||
lastOutputTime,
|
||||
retryStatus,
|
||||
} = useGeminiStream(
|
||||
config.getGeminiClient(),
|
||||
historyManager.history,
|
||||
@@ -1223,6 +1224,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
settings.merged.ui?.customWittyPhrases,
|
||||
!!activePtyId && !embeddedShellFocused,
|
||||
lastOutputTime,
|
||||
retryStatus,
|
||||
);
|
||||
|
||||
const handleGlobalKeypress = useCallback(
|
||||
|
||||
@@ -34,6 +34,8 @@ import {
|
||||
ToolConfirmationOutcome,
|
||||
tokenLimit,
|
||||
debugLogger,
|
||||
coreEvents,
|
||||
CoreEvent,
|
||||
} from '@google/gemini-cli-core';
|
||||
import type { Part, PartListUnion } from '@google/genai';
|
||||
import type { UseHistoryManagerReturn } from './useHistoryManager.js';
|
||||
@@ -1333,6 +1335,66 @@ describe('useGeminiStream', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Retry Handling', () => {
|
||||
it('should update retryStatus when CoreEvent.RetryAttempt is emitted', async () => {
|
||||
const { result } = renderHookWithDefaults();
|
||||
|
||||
const retryPayload = {
|
||||
model: 'gemini-2.5-pro',
|
||||
attempt: 2,
|
||||
maxAttempts: 3,
|
||||
delayMs: 1000,
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
coreEvents.emit(CoreEvent.RetryAttempt, retryPayload);
|
||||
});
|
||||
|
||||
expect(result.current.retryStatus).toEqual(retryPayload);
|
||||
});
|
||||
|
||||
it('should reset retryStatus when isResponding becomes false', async () => {
|
||||
const { result } = renderTestHook();
|
||||
|
||||
const retryPayload = {
|
||||
model: 'gemini-2.5-pro',
|
||||
attempt: 2,
|
||||
maxAttempts: 3,
|
||||
delayMs: 1000,
|
||||
};
|
||||
|
||||
// Start a query to make isResponding true
|
||||
const mockStream = (async function* () {
|
||||
yield { type: ServerGeminiEventType.Content, value: 'Part 1' };
|
||||
await new Promise(() => {}); // Keep stream open
|
||||
})();
|
||||
mockSendMessageStream.mockReturnValue(mockStream);
|
||||
|
||||
await act(async () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
result.current.submitQuery('test query');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.streamingState).toBe(StreamingState.Responding);
|
||||
});
|
||||
|
||||
// Emit retry event
|
||||
await act(async () => {
|
||||
coreEvents.emit(CoreEvent.RetryAttempt, retryPayload);
|
||||
});
|
||||
|
||||
expect(result.current.retryStatus).toEqual(retryPayload);
|
||||
|
||||
// Cancel to make isResponding false
|
||||
await act(async () => {
|
||||
result.current.cancelOngoingRequest();
|
||||
});
|
||||
|
||||
expect(result.current.retryStatus).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Slash Command Handling', () => {
|
||||
it('should schedule a tool call when the command processor returns a schedule_tool action', async () => {
|
||||
const clientToolRequest: SlashCommandProcessorResult = {
|
||||
|
||||
@@ -5,18 +5,6 @@
|
||||
*/
|
||||
|
||||
import { useState, useRef, useCallback, useEffect, useMemo } from 'react';
|
||||
import type {
|
||||
Config,
|
||||
EditorType,
|
||||
GeminiClient,
|
||||
ServerGeminiChatCompressedEvent,
|
||||
ServerGeminiContentEvent as ContentEvent,
|
||||
ServerGeminiFinishedEvent,
|
||||
ServerGeminiStreamEvent as GeminiEvent,
|
||||
ThoughtSummary,
|
||||
ToolCallRequestInfo,
|
||||
GeminiErrorEventValue,
|
||||
} from '@google/gemini-cli-core';
|
||||
import {
|
||||
GeminiEventType as ServerGeminiEventType,
|
||||
getErrorMessage,
|
||||
@@ -40,6 +28,21 @@ import {
|
||||
processRestorableToolCalls,
|
||||
recordToolCallInteractions,
|
||||
ToolErrorType,
|
||||
coreEvents,
|
||||
CoreEvent,
|
||||
} from '@google/gemini-cli-core';
|
||||
import type {
|
||||
Config,
|
||||
EditorType,
|
||||
GeminiClient,
|
||||
ServerGeminiChatCompressedEvent,
|
||||
ServerGeminiContentEvent as ContentEvent,
|
||||
ServerGeminiFinishedEvent,
|
||||
ServerGeminiStreamEvent as GeminiEvent,
|
||||
ThoughtSummary,
|
||||
ToolCallRequestInfo,
|
||||
GeminiErrorEventValue,
|
||||
RetryAttemptPayload,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { type Part, type PartListUnion, FinishReason } from '@google/genai';
|
||||
import type {
|
||||
@@ -113,6 +116,9 @@ export const useGeminiStream = (
|
||||
isShellFocused?: boolean,
|
||||
) => {
|
||||
const [initError, setInitError] = useState<string | null>(null);
|
||||
const [retryStatus, setRetryStatus] = useState<RetryAttemptPayload | null>(
|
||||
null,
|
||||
);
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
const turnCancelledRef = useRef(false);
|
||||
const activeQueryIdRef = useRef<string | null>(null);
|
||||
@@ -133,6 +139,16 @@ export const useGeminiStream = (
|
||||
return new GitService(config.getProjectRoot(), storage);
|
||||
}, [config, storage]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleRetryAttempt = (payload: RetryAttemptPayload) => {
|
||||
setRetryStatus(payload);
|
||||
};
|
||||
coreEvents.on(CoreEvent.RetryAttempt, handleRetryAttempt);
|
||||
return () => {
|
||||
coreEvents.off(CoreEvent.RetryAttempt, handleRetryAttempt);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const [
|
||||
toolCalls,
|
||||
scheduleToolCalls,
|
||||
@@ -297,6 +313,12 @@ export const useGeminiStream = (
|
||||
}
|
||||
}, [streamingState, config, history]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isResponding) {
|
||||
setRetryStatus(null);
|
||||
}
|
||||
}, [isResponding]);
|
||||
|
||||
const cancelOngoingRequest = useCallback(() => {
|
||||
if (
|
||||
streamingState !== StreamingState.Responding &&
|
||||
@@ -527,6 +549,7 @@ export const useGeminiStream = (
|
||||
currentGeminiMessageBuffer: string,
|
||||
userMessageTimestamp: number,
|
||||
): string => {
|
||||
setRetryStatus(null);
|
||||
if (turnCancelledRef.current) {
|
||||
// Prevents additional output after a user initiated cancel.
|
||||
return '';
|
||||
@@ -1362,5 +1385,6 @@ export const useGeminiStream = (
|
||||
activePtyId,
|
||||
loopDetectionConfirmationRequest,
|
||||
lastOutputTime,
|
||||
retryStatus,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
} from './usePhraseCycler.js';
|
||||
import { WITTY_LOADING_PHRASES } from '../constants/wittyPhrases.js';
|
||||
import { INFORMATIVE_TIPS } from '../constants/tips.js';
|
||||
import type { RetryAttemptPayload } from '@google/gemini-cli-core';
|
||||
|
||||
describe('useLoadingIndicator', () => {
|
||||
beforeEach(() => {
|
||||
@@ -32,22 +33,26 @@ describe('useLoadingIndicator', () => {
|
||||
initialStreamingState: StreamingState,
|
||||
initialIsInteractiveShellWaiting: boolean = false,
|
||||
initialLastOutputTime: number = 0,
|
||||
initialRetryStatus: RetryAttemptPayload | null = null,
|
||||
) => {
|
||||
let hookResult: ReturnType<typeof useLoadingIndicator>;
|
||||
function TestComponent({
|
||||
streamingState,
|
||||
isInteractiveShellWaiting,
|
||||
lastOutputTime,
|
||||
retryStatus,
|
||||
}: {
|
||||
streamingState: StreamingState;
|
||||
isInteractiveShellWaiting?: boolean;
|
||||
lastOutputTime?: number;
|
||||
retryStatus?: RetryAttemptPayload | null;
|
||||
}) {
|
||||
hookResult = useLoadingIndicator(
|
||||
streamingState,
|
||||
undefined,
|
||||
isInteractiveShellWaiting,
|
||||
lastOutputTime,
|
||||
retryStatus,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
@@ -56,6 +61,7 @@ describe('useLoadingIndicator', () => {
|
||||
streamingState={initialStreamingState}
|
||||
isInteractiveShellWaiting={initialIsInteractiveShellWaiting}
|
||||
lastOutputTime={initialLastOutputTime}
|
||||
retryStatus={initialRetryStatus}
|
||||
/>,
|
||||
);
|
||||
return {
|
||||
@@ -68,6 +74,7 @@ describe('useLoadingIndicator', () => {
|
||||
streamingState: StreamingState;
|
||||
isInteractiveShellWaiting?: boolean;
|
||||
lastOutputTime?: number;
|
||||
retryStatus?: RetryAttemptPayload | null;
|
||||
}) => rerender(<TestComponent {...newProps} />),
|
||||
};
|
||||
};
|
||||
@@ -206,4 +213,23 @@ describe('useLoadingIndicator', () => {
|
||||
});
|
||||
expect(result.current.elapsedTime).toBe(0);
|
||||
});
|
||||
|
||||
it('should reflect retry status in currentLoadingPhrase when provided', () => {
|
||||
const retryStatus = {
|
||||
model: 'gemini-pro',
|
||||
attempt: 2,
|
||||
maxAttempts: 3,
|
||||
delayMs: 1000,
|
||||
};
|
||||
const { result } = renderLoadingIndicatorHook(
|
||||
StreamingState.Responding,
|
||||
false,
|
||||
0,
|
||||
retryStatus,
|
||||
);
|
||||
|
||||
expect(result.current.currentLoadingPhrase).toBe(
|
||||
'Trying to reach gemini-pro (Attempt 2/3)',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,13 +7,18 @@
|
||||
import { StreamingState } from '../types.js';
|
||||
import { useTimer } from './useTimer.js';
|
||||
import { usePhraseCycler } from './usePhraseCycler.js';
|
||||
import { useState, useEffect, useRef } from 'react'; // Added useRef
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import {
|
||||
getDisplayString,
|
||||
type RetryAttemptPayload,
|
||||
} from '@google/gemini-cli-core';
|
||||
|
||||
export const useLoadingIndicator = (
|
||||
streamingState: StreamingState,
|
||||
customWittyPhrases?: string[],
|
||||
isInteractiveShellWaiting: boolean = false,
|
||||
lastOutputTime: number = 0,
|
||||
retryStatus: RetryAttemptPayload | null = null,
|
||||
) => {
|
||||
const [timerResetKey, setTimerResetKey] = useState(0);
|
||||
const isTimerActive = streamingState === StreamingState.Responding;
|
||||
@@ -55,11 +60,15 @@ export const useLoadingIndicator = (
|
||||
prevStreamingStateRef.current = streamingState;
|
||||
}, [streamingState, elapsedTimeFromTimer]);
|
||||
|
||||
const retryPhrase = retryStatus
|
||||
? `Trying to reach ${getDisplayString(retryStatus.model)} (Attempt ${retryStatus.attempt}/${retryStatus.maxAttempts})`
|
||||
: null;
|
||||
|
||||
return {
|
||||
elapsedTime:
|
||||
streamingState === StreamingState.WaitingForConfirmation
|
||||
? retainedElapsedTime
|
||||
: elapsedTimeFromTimer,
|
||||
currentLoadingPhrase,
|
||||
currentLoadingPhrase: retryPhrase || currentLoadingPhrase,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user