mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-10 14:10:37 -07:00
Fix/issue 17070 (#17242)
Co-authored-by: Gal Zahavi <38544478+galz10@users.noreply.github.com>
This commit is contained in:
@@ -560,7 +560,7 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
// Session browser and resume functionality
|
||||
const isGeminiClientInitialized = config.getGeminiClient()?.isInitialized();
|
||||
|
||||
const { loadHistoryForResume } = useSessionResume({
|
||||
const { loadHistoryForResume, isResuming } = useSessionResume({
|
||||
config,
|
||||
historyManager,
|
||||
refreshStatic,
|
||||
@@ -1018,6 +1018,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
isConfigInitialized &&
|
||||
!initError &&
|
||||
!isProcessing &&
|
||||
!isResuming &&
|
||||
!!slashCommands &&
|
||||
(streamingState === StreamingState.Idle ||
|
||||
streamingState === StreamingState.Responding) &&
|
||||
@@ -1670,6 +1671,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
inputWidth,
|
||||
suggestionsWidth,
|
||||
isInputActive,
|
||||
isResuming,
|
||||
shouldShowIdePrompt,
|
||||
isFolderTrustDialogOpen: isFolderTrustDialogOpen ?? false,
|
||||
isTrustedFolder,
|
||||
@@ -1766,6 +1768,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
inputWidth,
|
||||
suggestionsWidth,
|
||||
isInputActive,
|
||||
isResuming,
|
||||
shouldShowIdePrompt,
|
||||
isFolderTrustDialogOpen,
|
||||
isTrustedFolder,
|
||||
|
||||
@@ -71,8 +71,12 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{(!uiState.slashCommands || !uiState.isConfigInitialized) && (
|
||||
<ConfigInitDisplay />
|
||||
{(!uiState.slashCommands ||
|
||||
!uiState.isConfigInitialized ||
|
||||
uiState.isResuming) && (
|
||||
<ConfigInitDisplay
|
||||
message={uiState.isResuming ? 'Resuming session...' : undefined}
|
||||
/>
|
||||
)}
|
||||
|
||||
<QueuedMessageDisplay messageQueue={uiState.messageQueue} />
|
||||
|
||||
@@ -15,13 +15,17 @@ import {
|
||||
import { GeminiSpinner } from './GeminiRespondingSpinner.js';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
|
||||
export const ConfigInitDisplay = () => {
|
||||
const [message, setMessage] = useState('Initializing...');
|
||||
export const ConfigInitDisplay = ({
|
||||
message: initialMessage = 'Initializing...',
|
||||
}: {
|
||||
message?: string;
|
||||
}) => {
|
||||
const [message, setMessage] = useState(initialMessage);
|
||||
|
||||
useEffect(() => {
|
||||
const onChange = (clients?: Map<string, McpClient>) => {
|
||||
if (!clients || clients.size === 0) {
|
||||
setMessage(`Initializing...`);
|
||||
setMessage(initialMessage);
|
||||
return;
|
||||
}
|
||||
let connected = 0;
|
||||
@@ -39,12 +43,18 @@ export const ConfigInitDisplay = () => {
|
||||
const displayedServers = connecting.slice(0, maxDisplay).join(', ');
|
||||
const remaining = connecting.length - maxDisplay;
|
||||
const suffix = remaining > 0 ? `, +${remaining} more` : '';
|
||||
const mcpMessage = `Connecting to MCP servers... (${connected}/${clients.size}) - Waiting for: ${displayedServers}${suffix}`;
|
||||
setMessage(
|
||||
`Connecting to MCP servers... (${connected}/${clients.size}) - Waiting for: ${displayedServers}${suffix}`,
|
||||
initialMessage && initialMessage !== 'Initializing...'
|
||||
? `${initialMessage} (${mcpMessage})`
|
||||
: mcpMessage,
|
||||
);
|
||||
} else {
|
||||
const mcpMessage = `Connecting to MCP servers... (${connected}/${clients.size})`;
|
||||
setMessage(
|
||||
`Connecting to MCP servers... (${connected}/${clients.size})`,
|
||||
initialMessage && initialMessage !== 'Initializing...'
|
||||
? `${initialMessage} (${mcpMessage})`
|
||||
: mcpMessage,
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -53,7 +63,7 @@ export const ConfigInitDisplay = () => {
|
||||
return () => {
|
||||
coreEvents.off(CoreEvent.McpClientUpdate, onChange);
|
||||
};
|
||||
}, []);
|
||||
}, [initialMessage]);
|
||||
|
||||
return (
|
||||
<Box marginTop={1}>
|
||||
|
||||
@@ -94,6 +94,7 @@ export interface UIState {
|
||||
inputWidth: number;
|
||||
suggestionsWidth: number;
|
||||
isInputActive: boolean;
|
||||
isResuming: boolean;
|
||||
shouldShowIdePrompt: boolean;
|
||||
isFolderTrustDialogOpen: boolean;
|
||||
isTrustedFolder: boolean | undefined;
|
||||
|
||||
@@ -24,7 +24,7 @@ export const useSessionBrowser = (
|
||||
uiHistory: HistoryItemWithoutId[],
|
||||
clientHistory: Array<{ role: 'user' | 'model'; parts: Part[] }>,
|
||||
resumedSessionData: ResumedSessionData,
|
||||
) => void,
|
||||
) => Promise<void>,
|
||||
) => {
|
||||
const [isSessionBrowserOpen, setIsSessionBrowserOpen] = useState(false);
|
||||
|
||||
@@ -73,7 +73,7 @@ export const useSessionBrowser = (
|
||||
const historyData = convertSessionToHistoryFormats(
|
||||
conversation.messages,
|
||||
);
|
||||
onLoadHistory(
|
||||
await onLoadHistory(
|
||||
historyData.uiHistory,
|
||||
historyData.clientHistory,
|
||||
resumedSessionData,
|
||||
|
||||
@@ -62,7 +62,7 @@ describe('useSessionResume', () => {
|
||||
expect(result.current.loadHistoryForResume).toBeInstanceOf(Function);
|
||||
});
|
||||
|
||||
it('should clear history and add items when loading history', () => {
|
||||
it('should clear history and add items when loading history', async () => {
|
||||
const { result } = renderHook(() => useSessionResume(getDefaultProps()));
|
||||
|
||||
const uiHistory: HistoryItemWithoutId[] = [
|
||||
@@ -86,8 +86,8 @@ describe('useSessionResume', () => {
|
||||
filePath: '/path/to/session.json',
|
||||
};
|
||||
|
||||
act(() => {
|
||||
result.current.loadHistoryForResume(
|
||||
await act(async () => {
|
||||
await result.current.loadHistoryForResume(
|
||||
uiHistory,
|
||||
clientHistory,
|
||||
resumedData,
|
||||
@@ -116,7 +116,7 @@ describe('useSessionResume', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should not load history if Gemini client is not initialized', () => {
|
||||
it('should not load history if Gemini client is not initialized', async () => {
|
||||
const { result } = renderHook(() =>
|
||||
useSessionResume({
|
||||
...getDefaultProps(),
|
||||
@@ -141,8 +141,8 @@ describe('useSessionResume', () => {
|
||||
filePath: '/path/to/session.json',
|
||||
};
|
||||
|
||||
act(() => {
|
||||
result.current.loadHistoryForResume(
|
||||
await act(async () => {
|
||||
await result.current.loadHistoryForResume(
|
||||
uiHistory,
|
||||
clientHistory,
|
||||
resumedData,
|
||||
@@ -154,7 +154,7 @@ describe('useSessionResume', () => {
|
||||
expect(mockGeminiClient.resumeChat).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle empty history arrays', () => {
|
||||
it('should handle empty history arrays', async () => {
|
||||
const { result } = renderHook(() => useSessionResume(getDefaultProps()));
|
||||
|
||||
const resumedData: ResumedSessionData = {
|
||||
@@ -168,8 +168,8 @@ describe('useSessionResume', () => {
|
||||
filePath: '/path/to/session.json',
|
||||
};
|
||||
|
||||
act(() => {
|
||||
result.current.loadHistoryForResume([], [], resumedData);
|
||||
await act(async () => {
|
||||
await result.current.loadHistoryForResume([], [], resumedData);
|
||||
});
|
||||
|
||||
expect(mockHistoryManager.clearItems).toHaveBeenCalled();
|
||||
@@ -311,15 +311,17 @@ describe('useSessionResume', () => {
|
||||
] as MessageRecord[],
|
||||
};
|
||||
|
||||
renderHook(() =>
|
||||
useSessionResume({
|
||||
...getDefaultProps(),
|
||||
resumedSessionData: {
|
||||
conversation,
|
||||
filePath: '/path/to/session.json',
|
||||
},
|
||||
}),
|
||||
);
|
||||
await act(async () => {
|
||||
renderHook(() =>
|
||||
useSessionResume({
|
||||
...getDefaultProps(),
|
||||
resumedSessionData: {
|
||||
conversation,
|
||||
filePath: '/path/to/session.json',
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockHistoryManager.clearItems).toHaveBeenCalled();
|
||||
@@ -358,20 +360,24 @@ describe('useSessionResume', () => {
|
||||
] as MessageRecord[],
|
||||
};
|
||||
|
||||
const { rerender } = renderHook(
|
||||
({ refreshStatic }: { refreshStatic: () => void }) =>
|
||||
useSessionResume({
|
||||
...getDefaultProps(),
|
||||
refreshStatic,
|
||||
resumedSessionData: {
|
||||
conversation,
|
||||
filePath: '/path/to/session.json',
|
||||
},
|
||||
}),
|
||||
{
|
||||
initialProps: { refreshStatic: mockRefreshStatic },
|
||||
},
|
||||
);
|
||||
let rerenderFunc: (props: { refreshStatic: () => void }) => void;
|
||||
await act(async () => {
|
||||
const { rerender } = renderHook(
|
||||
({ refreshStatic }: { refreshStatic: () => void }) =>
|
||||
useSessionResume({
|
||||
...getDefaultProps(),
|
||||
refreshStatic,
|
||||
resumedSessionData: {
|
||||
conversation,
|
||||
filePath: '/path/to/session.json',
|
||||
},
|
||||
}),
|
||||
{
|
||||
initialProps: { refreshStatic: mockRefreshStatic as () => void },
|
||||
},
|
||||
);
|
||||
rerenderFunc = rerender;
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockHistoryManager.clearItems).toHaveBeenCalled();
|
||||
@@ -383,7 +389,9 @@ describe('useSessionResume', () => {
|
||||
|
||||
// Rerender with different refreshStatic
|
||||
const newRefreshStatic = vi.fn();
|
||||
rerender({ refreshStatic: newRefreshStatic });
|
||||
await act(async () => {
|
||||
rerenderFunc({ refreshStatic: newRefreshStatic });
|
||||
});
|
||||
|
||||
// Should not resume again
|
||||
expect(mockHistoryManager.clearItems).toHaveBeenCalledTimes(
|
||||
@@ -413,15 +421,17 @@ describe('useSessionResume', () => {
|
||||
] as MessageRecord[],
|
||||
};
|
||||
|
||||
renderHook(() =>
|
||||
useSessionResume({
|
||||
...getDefaultProps(),
|
||||
resumedSessionData: {
|
||||
conversation,
|
||||
filePath: '/path/to/session.json',
|
||||
},
|
||||
}),
|
||||
);
|
||||
await act(async () => {
|
||||
renderHook(() =>
|
||||
useSessionResume({
|
||||
...getDefaultProps(),
|
||||
resumedSessionData: {
|
||||
conversation,
|
||||
filePath: '/path/to/session.json',
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockGeminiClient.resumeChat).toHaveBeenCalled();
|
||||
|
||||
@@ -4,8 +4,12 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import type { Config, ResumedSessionData } from '@google/gemini-cli-core';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
coreEvents,
|
||||
type Config,
|
||||
type ResumedSessionData,
|
||||
} from '@google/gemini-cli-core';
|
||||
import type { Part } from '@google/genai';
|
||||
import type { HistoryItemWithoutId } from '../types.js';
|
||||
import type { UseHistoryManagerReturn } from './useHistoryManager.js';
|
||||
@@ -35,6 +39,8 @@ export function useSessionResume({
|
||||
resumedSessionData,
|
||||
isAuthenticating,
|
||||
}: UseSessionResumeParams) {
|
||||
const [isResuming, setIsResuming] = useState(false);
|
||||
|
||||
// Use refs to avoid dependency chain that causes infinite loop
|
||||
const historyManagerRef = useRef(historyManager);
|
||||
const refreshStaticRef = useRef(refreshStatic);
|
||||
@@ -45,7 +51,7 @@ export function useSessionResume({
|
||||
});
|
||||
|
||||
const loadHistoryForResume = useCallback(
|
||||
(
|
||||
async (
|
||||
uiHistory: HistoryItemWithoutId[],
|
||||
clientHistory: Array<{ role: 'user' | 'model'; parts: Part[] }>,
|
||||
resumedData: ResumedSessionData,
|
||||
@@ -55,17 +61,27 @@ export function useSessionResume({
|
||||
return;
|
||||
}
|
||||
|
||||
// Now that we have the client, load the history into the UI and the client.
|
||||
setQuittingMessages(null);
|
||||
historyManagerRef.current.clearItems();
|
||||
uiHistory.forEach((item, index) => {
|
||||
historyManagerRef.current.addItem(item, index, true);
|
||||
});
|
||||
refreshStaticRef.current(); // Force Static component to re-render with the updated history.
|
||||
setIsResuming(true);
|
||||
try {
|
||||
// Now that we have the client, load the history into the UI and the client.
|
||||
setQuittingMessages(null);
|
||||
historyManagerRef.current.clearItems();
|
||||
uiHistory.forEach((item, index) => {
|
||||
historyManagerRef.current.addItem(item, index, true);
|
||||
});
|
||||
refreshStaticRef.current(); // Force Static component to re-render with the updated history.
|
||||
|
||||
// Give the history to the Gemini client.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
config.getGeminiClient()?.resumeChat(clientHistory, resumedData);
|
||||
// Give the history to the Gemini client.
|
||||
await config.getGeminiClient()?.resumeChat(clientHistory, resumedData);
|
||||
} catch (error) {
|
||||
coreEvents.emitFeedback(
|
||||
'error',
|
||||
'Failed to resume session. Please try again.',
|
||||
error,
|
||||
);
|
||||
} finally {
|
||||
setIsResuming(false);
|
||||
}
|
||||
},
|
||||
[config, isGeminiClientInitialized, setQuittingMessages],
|
||||
);
|
||||
@@ -84,7 +100,7 @@ export function useSessionResume({
|
||||
const historyData = convertSessionToHistoryFormats(
|
||||
resumedSessionData.conversation.messages,
|
||||
);
|
||||
loadHistoryForResume(
|
||||
void loadHistoryForResume(
|
||||
historyData.uiHistory,
|
||||
historyData.clientHistory,
|
||||
resumedSessionData,
|
||||
@@ -97,5 +113,5 @@ export function useSessionResume({
|
||||
loadHistoryForResume,
|
||||
]);
|
||||
|
||||
return { loadHistoryForResume };
|
||||
return { loadHistoryForResume, isResuming };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user