diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx
index 0e337b7c1f..4f10e10645 100644
--- a/packages/cli/src/ui/AppContainer.tsx
+++ b/packages/cli/src/ui/AppContainer.tsx
@@ -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,
diff --git a/packages/cli/src/ui/components/Composer.tsx b/packages/cli/src/ui/components/Composer.tsx
index 9a550a323e..de3ecebd19 100644
--- a/packages/cli/src/ui/components/Composer.tsx
+++ b/packages/cli/src/ui/components/Composer.tsx
@@ -71,8 +71,12 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
/>
)}
- {(!uiState.slashCommands || !uiState.isConfigInitialized) && (
-
+ {(!uiState.slashCommands ||
+ !uiState.isConfigInitialized ||
+ uiState.isResuming) && (
+
)}
diff --git a/packages/cli/src/ui/components/ConfigInitDisplay.tsx b/packages/cli/src/ui/components/ConfigInitDisplay.tsx
index b1dc71ff74..a47e16daff 100644
--- a/packages/cli/src/ui/components/ConfigInitDisplay.tsx
+++ b/packages/cli/src/ui/components/ConfigInitDisplay.tsx
@@ -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) => {
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 (
diff --git a/packages/cli/src/ui/contexts/UIStateContext.tsx b/packages/cli/src/ui/contexts/UIStateContext.tsx
index 893ee80c07..fea13285b1 100644
--- a/packages/cli/src/ui/contexts/UIStateContext.tsx
+++ b/packages/cli/src/ui/contexts/UIStateContext.tsx
@@ -94,6 +94,7 @@ export interface UIState {
inputWidth: number;
suggestionsWidth: number;
isInputActive: boolean;
+ isResuming: boolean;
shouldShowIdePrompt: boolean;
isFolderTrustDialogOpen: boolean;
isTrustedFolder: boolean | undefined;
diff --git a/packages/cli/src/ui/hooks/useSessionBrowser.ts b/packages/cli/src/ui/hooks/useSessionBrowser.ts
index 1dbced887d..3d9619d738 100644
--- a/packages/cli/src/ui/hooks/useSessionBrowser.ts
+++ b/packages/cli/src/ui/hooks/useSessionBrowser.ts
@@ -24,7 +24,7 @@ export const useSessionBrowser = (
uiHistory: HistoryItemWithoutId[],
clientHistory: Array<{ role: 'user' | 'model'; parts: Part[] }>,
resumedSessionData: ResumedSessionData,
- ) => void,
+ ) => Promise,
) => {
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,
diff --git a/packages/cli/src/ui/hooks/useSessionResume.test.ts b/packages/cli/src/ui/hooks/useSessionResume.test.ts
index 029d23d725..071fe5878b 100644
--- a/packages/cli/src/ui/hooks/useSessionResume.test.ts
+++ b/packages/cli/src/ui/hooks/useSessionResume.test.ts
@@ -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();
diff --git a/packages/cli/src/ui/hooks/useSessionResume.ts b/packages/cli/src/ui/hooks/useSessionResume.ts
index 228ca6ac2c..21b9d0884f 100644
--- a/packages/cli/src/ui/hooks/useSessionResume.ts
+++ b/packages/cli/src/ui/hooks/useSessionResume.ts
@@ -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 };
}