mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-16 00:51:25 -07:00
feat(ui): Add confirmation dialog for disabling loop detection for current session (#8231)
This commit is contained in:
@@ -1789,4 +1789,262 @@ describe('useGeminiStream', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Loop Detection Confirmation', () => {
|
||||
beforeEach(() => {
|
||||
// Add mock for getLoopDetectionService to the config
|
||||
const mockLoopDetectionService = {
|
||||
disableForSession: vi.fn(),
|
||||
};
|
||||
mockConfig.getGeminiClient = vi.fn().mockReturnValue({
|
||||
...new MockedGeminiClientClass(mockConfig),
|
||||
getLoopDetectionService: () => mockLoopDetectionService,
|
||||
});
|
||||
});
|
||||
|
||||
it('should set loopDetectionConfirmationRequest when LoopDetected event is received', async () => {
|
||||
mockSendMessageStream.mockReturnValue(
|
||||
(async function* () {
|
||||
yield {
|
||||
type: ServerGeminiEventType.Content,
|
||||
value: 'Some content',
|
||||
};
|
||||
yield {
|
||||
type: ServerGeminiEventType.LoopDetected,
|
||||
};
|
||||
})(),
|
||||
);
|
||||
|
||||
const { result } = renderTestHook();
|
||||
|
||||
await act(async () => {
|
||||
await result.current.submitQuery('test query');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loopDetectionConfirmationRequest).not.toBeNull();
|
||||
expect(
|
||||
typeof result.current.loopDetectionConfirmationRequest?.onComplete,
|
||||
).toBe('function');
|
||||
});
|
||||
});
|
||||
|
||||
it('should disable loop detection and show message when user selects "disable"', async () => {
|
||||
const mockLoopDetectionService = {
|
||||
disableForSession: vi.fn(),
|
||||
};
|
||||
const mockClient = {
|
||||
...new MockedGeminiClientClass(mockConfig),
|
||||
getLoopDetectionService: () => mockLoopDetectionService,
|
||||
};
|
||||
mockConfig.getGeminiClient = vi.fn().mockReturnValue(mockClient);
|
||||
|
||||
mockSendMessageStream.mockReturnValue(
|
||||
(async function* () {
|
||||
yield {
|
||||
type: ServerGeminiEventType.LoopDetected,
|
||||
};
|
||||
})(),
|
||||
);
|
||||
|
||||
const { result } = renderTestHook();
|
||||
|
||||
await act(async () => {
|
||||
await result.current.submitQuery('test query');
|
||||
});
|
||||
|
||||
// Wait for confirmation request to be set
|
||||
await waitFor(() => {
|
||||
expect(result.current.loopDetectionConfirmationRequest).not.toBeNull();
|
||||
});
|
||||
|
||||
// Simulate user selecting "disable"
|
||||
await act(async () => {
|
||||
result.current.loopDetectionConfirmationRequest?.onComplete({
|
||||
userSelection: 'disable',
|
||||
});
|
||||
});
|
||||
|
||||
// Verify loop detection was disabled
|
||||
expect(mockLoopDetectionService.disableForSession).toHaveBeenCalledTimes(
|
||||
1,
|
||||
);
|
||||
|
||||
// Verify confirmation request was cleared
|
||||
expect(result.current.loopDetectionConfirmationRequest).toBeNull();
|
||||
|
||||
// Verify appropriate message was added
|
||||
expect(mockAddItem).toHaveBeenCalledWith(
|
||||
{
|
||||
type: 'info',
|
||||
text: 'Loop detection has been disabled for this session. Please try your request again.',
|
||||
},
|
||||
expect.any(Number),
|
||||
);
|
||||
});
|
||||
|
||||
it('should keep loop detection enabled and show message when user selects "keep"', async () => {
|
||||
const mockLoopDetectionService = {
|
||||
disableForSession: vi.fn(),
|
||||
};
|
||||
const mockClient = {
|
||||
...new MockedGeminiClientClass(mockConfig),
|
||||
getLoopDetectionService: () => mockLoopDetectionService,
|
||||
};
|
||||
mockConfig.getGeminiClient = vi.fn().mockReturnValue(mockClient);
|
||||
|
||||
mockSendMessageStream.mockReturnValue(
|
||||
(async function* () {
|
||||
yield {
|
||||
type: ServerGeminiEventType.LoopDetected,
|
||||
};
|
||||
})(),
|
||||
);
|
||||
|
||||
const { result } = renderTestHook();
|
||||
|
||||
await act(async () => {
|
||||
await result.current.submitQuery('test query');
|
||||
});
|
||||
|
||||
// Wait for confirmation request to be set
|
||||
await waitFor(() => {
|
||||
expect(result.current.loopDetectionConfirmationRequest).not.toBeNull();
|
||||
});
|
||||
|
||||
// Simulate user selecting "keep"
|
||||
await act(async () => {
|
||||
result.current.loopDetectionConfirmationRequest?.onComplete({
|
||||
userSelection: 'keep',
|
||||
});
|
||||
});
|
||||
|
||||
// Verify loop detection was NOT disabled
|
||||
expect(mockLoopDetectionService.disableForSession).not.toHaveBeenCalled();
|
||||
|
||||
// Verify confirmation request was cleared
|
||||
expect(result.current.loopDetectionConfirmationRequest).toBeNull();
|
||||
|
||||
// Verify appropriate message was added
|
||||
expect(mockAddItem).toHaveBeenCalledWith(
|
||||
{
|
||||
type: 'info',
|
||||
text: 'A potential loop was detected. This can happen due to repetitive tool calls or other model behavior. The request has been halted.',
|
||||
},
|
||||
expect.any(Number),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle multiple loop detection events properly', async () => {
|
||||
const { result } = renderTestHook();
|
||||
|
||||
// First loop detection - set up fresh mock for first call
|
||||
mockSendMessageStream.mockReturnValueOnce(
|
||||
(async function* () {
|
||||
yield {
|
||||
type: ServerGeminiEventType.LoopDetected,
|
||||
};
|
||||
})(),
|
||||
);
|
||||
|
||||
// First loop detection
|
||||
await act(async () => {
|
||||
await result.current.submitQuery('first query');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loopDetectionConfirmationRequest).not.toBeNull();
|
||||
});
|
||||
|
||||
// Simulate user selecting "keep" for first request
|
||||
await act(async () => {
|
||||
result.current.loopDetectionConfirmationRequest?.onComplete({
|
||||
userSelection: 'keep',
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current.loopDetectionConfirmationRequest).toBeNull();
|
||||
|
||||
// Verify first message was added
|
||||
expect(mockAddItem).toHaveBeenCalledWith(
|
||||
{
|
||||
type: 'info',
|
||||
text: 'A potential loop was detected. This can happen due to repetitive tool calls or other model behavior. The request has been halted.',
|
||||
},
|
||||
expect.any(Number),
|
||||
);
|
||||
|
||||
// Second loop detection - set up fresh mock for second call
|
||||
mockSendMessageStream.mockReturnValueOnce(
|
||||
(async function* () {
|
||||
yield {
|
||||
type: ServerGeminiEventType.LoopDetected,
|
||||
};
|
||||
})(),
|
||||
);
|
||||
|
||||
// Second loop detection
|
||||
await act(async () => {
|
||||
await result.current.submitQuery('second query');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loopDetectionConfirmationRequest).not.toBeNull();
|
||||
});
|
||||
|
||||
// Simulate user selecting "disable" for second request
|
||||
await act(async () => {
|
||||
result.current.loopDetectionConfirmationRequest?.onComplete({
|
||||
userSelection: 'disable',
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current.loopDetectionConfirmationRequest).toBeNull();
|
||||
|
||||
// Verify second message was added
|
||||
expect(mockAddItem).toHaveBeenCalledWith(
|
||||
{
|
||||
type: 'info',
|
||||
text: 'Loop detection has been disabled for this session. Please try your request again.',
|
||||
},
|
||||
expect.any(Number),
|
||||
);
|
||||
});
|
||||
|
||||
it('should process LoopDetected event after moving pending history to history', async () => {
|
||||
mockSendMessageStream.mockReturnValue(
|
||||
(async function* () {
|
||||
yield {
|
||||
type: ServerGeminiEventType.Content,
|
||||
value: 'Some response content',
|
||||
};
|
||||
yield {
|
||||
type: ServerGeminiEventType.LoopDetected,
|
||||
};
|
||||
})(),
|
||||
);
|
||||
|
||||
const { result } = renderTestHook();
|
||||
|
||||
await act(async () => {
|
||||
await result.current.submitQuery('test query');
|
||||
});
|
||||
|
||||
// Verify that the content was added to history before the loop detection dialog
|
||||
await waitFor(() => {
|
||||
expect(mockAddItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'gemini',
|
||||
text: 'Some response content',
|
||||
}),
|
||||
expect.any(Number),
|
||||
);
|
||||
});
|
||||
|
||||
// Then verify loop detection confirmation request was set
|
||||
await waitFor(() => {
|
||||
expect(result.current.loopDetectionConfirmationRequest).not.toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -153,6 +153,12 @@ export const useGeminiStream = (
|
||||
);
|
||||
|
||||
const loopDetectedRef = useRef(false);
|
||||
const [
|
||||
loopDetectionConfirmationRequest,
|
||||
setLoopDetectionConfirmationRequest,
|
||||
] = useState<{
|
||||
onComplete: (result: { userSelection: 'disable' | 'keep' }) => void;
|
||||
} | null>(null);
|
||||
|
||||
const onExec = useCallback(async (done: Promise<void>) => {
|
||||
setIsResponding(true);
|
||||
@@ -588,15 +594,38 @@ export const useGeminiStream = (
|
||||
[addItem, config],
|
||||
);
|
||||
|
||||
const handleLoopDetectionConfirmation = useCallback(
|
||||
(result: { userSelection: 'disable' | 'keep' }) => {
|
||||
setLoopDetectionConfirmationRequest(null);
|
||||
|
||||
if (result.userSelection === 'disable') {
|
||||
config.getGeminiClient().getLoopDetectionService().disableForSession();
|
||||
addItem(
|
||||
{
|
||||
type: 'info',
|
||||
text: `Loop detection has been disabled for this session. Please try your request again.`,
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
} else {
|
||||
addItem(
|
||||
{
|
||||
type: 'info',
|
||||
text: `A potential loop was detected. This can happen due to repetitive tool calls or other model behavior. The request has been halted.`,
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
}
|
||||
},
|
||||
[config, addItem],
|
||||
);
|
||||
|
||||
const handleLoopDetectedEvent = useCallback(() => {
|
||||
addItem(
|
||||
{
|
||||
type: 'info',
|
||||
text: `A potential loop was detected. This can happen due to repetitive tool calls or other model behavior. The request has been halted.`,
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
}, [addItem]);
|
||||
// Show the confirmation dialog to choose whether to disable loop detection
|
||||
setLoopDetectionConfirmationRequest({
|
||||
onComplete: handleLoopDetectionConfirmation,
|
||||
});
|
||||
}, [handleLoopDetectionConfirmation]);
|
||||
|
||||
const processGeminiStreamEvents = useCallback(
|
||||
async (
|
||||
@@ -1045,5 +1074,6 @@ export const useGeminiStream = (
|
||||
pendingHistoryItems,
|
||||
thought,
|
||||
cancelOngoingRequest,
|
||||
loopDetectionConfirmationRequest,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user