diff --git a/packages/cli/src/gemini.tsx b/packages/cli/src/gemini.tsx
index d7b77ef19a..bb58022d22 100644
--- a/packages/cli/src/gemini.tsx
+++ b/packages/cli/src/gemini.tsx
@@ -4,9 +4,8 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import React, { useState, useEffect } from 'react';
-import { render, Box, Text } from 'ink';
-import Spinner from 'ink-spinner';
+import React from 'react';
+import { render } from 'ink';
import { AppContainer } from './ui/AppContainer.js';
import { loadCliConfig, parseArguments } from './config/config.js';
import { readStdin } from './utils/readStdin.js';
@@ -117,38 +116,6 @@ async function relaunchWithAdditionalArgs(additionalArgs: string[]) {
process.exit(0);
}
-const InitializingComponent = ({ initialTotal }: { initialTotal: number }) => {
- const [total, setTotal] = useState(initialTotal);
- const [connected, setConnected] = useState(0);
-
- useEffect(() => {
- const onStart = ({ count }: { count: number }) => setTotal(count);
- const onChange = () => {
- setConnected((val) => val + 1);
- };
-
- appEvents.on('mcp-servers-discovery-start', onStart);
- appEvents.on('mcp-server-connected', onChange);
- appEvents.on('mcp-server-error', onChange);
-
- return () => {
- appEvents.off('mcp-servers-discovery-start', onStart);
- appEvents.off('mcp-server-connected', onChange);
- appEvents.off('mcp-server-error', onChange);
- };
- }, []);
-
- const message = `Connecting to MCP servers... (${connected}/${total})`;
-
- return (
-
-
- {message}
-
-
- );
-};
-
import { runZedIntegration } from './zed-integration/zedIntegration.js';
export function setupUnhandledRejectionHandler() {
@@ -315,25 +282,6 @@ export async function main() {
setMaxSizedBoxDebugging(config.getDebugMode());
- const mcpServers = config.getMcpServers();
- const mcpServersCount = mcpServers ? Object.keys(mcpServers).length : 0;
-
- let spinnerInstance;
- if (config.isInteractive() && mcpServersCount > 0) {
- spinnerInstance = render(
- ,
- );
- }
-
- await config.initialize();
-
- if (spinnerInstance) {
- // Small UX detail to show the completion message for a bit before unmounting.
- await new Promise((f) => setTimeout(f, 100));
- spinnerInstance.clear();
- spinnerInstance.unmount();
- }
-
// Load custom themes from settings
themeManager.loadCustomThemes(settings.merged.ui?.customThemes);
@@ -446,6 +394,9 @@ export async function main() {
);
return;
}
+
+ await config.initialize();
+
// If not a TTY, read from stdin
// This is for cases where the user pipes input directly into the command
if (!process.stdin.isTTY) {
diff --git a/packages/cli/src/ui/AppContainer.test.tsx b/packages/cli/src/ui/AppContainer.test.tsx
index a5d83ca383..621e16c7df 100644
--- a/packages/cli/src/ui/AppContainer.test.tsx
+++ b/packages/cli/src/ui/AppContainer.test.tsx
@@ -416,10 +416,9 @@ describe('AppContainer State Management', () => {
});
describe('Version Handling', () => {
- it('handles different version formats', () => {
- const versions = ['1.0.0', '2.1.3-beta', '3.0.0-nightly'];
-
- versions.forEach((version) => {
+ it.each(['1.0.0', '2.1.3-beta', '3.0.0-nightly'])(
+ 'handles version format: %s',
+ (version) => {
expect(() => {
render(
{
/>,
);
}).not.toThrow();
- });
- });
+ },
+ );
});
describe('Error Handling', () => {
diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx
index 17e08b54ea..a5977bb615 100644
--- a/packages/cli/src/ui/AppContainer.tsx
+++ b/packages/cli/src/ui/AppContainer.tsx
@@ -134,6 +134,8 @@ export const AppContainer = (props: AppContainerProps) => {
const [userTier, setUserTier] = useState(undefined);
+ const [isConfigInitialized, setConfigInitialized] = useState(false);
+
// Auto-accept indicator
const showAutoAcceptIndicator = useAutoAcceptIndicator({
config,
@@ -157,16 +159,22 @@ export const AppContainer = (props: AppContainerProps) => {
const staticExtraHeight = 3;
useEffect(() => {
+ (async () => {
+ // Note: the program will not work if this fails so let errors be
+ // handled by the global catch.
+ await config.initialize();
+ setConfigInitialized(true);
+ })();
registerCleanup(async () => {
const ideClient = await IdeClient.getInstance();
await ideClient.disconnect();
});
}, [config]);
- useEffect(() => {
- const cleanup = setUpdateHandler(historyManager.addItem, setUpdateInfo);
- return cleanup;
- }, [historyManager.addItem]);
+ useEffect(
+ () => setUpdateHandler(historyManager.addItem, setUpdateInfo),
+ [historyManager.addItem],
+ );
// Watch for model changes (e.g., from Flash fallback)
useEffect(() => {
@@ -517,6 +525,7 @@ Logging in with Google... Please restart Gemini CLI to continue.
const { messageQueue, addMessage, clearQueue, getQueuedMessagesText } =
useMessageQueue({
+ isConfigInitialized,
streamingState,
submitQuery,
});
@@ -612,6 +621,7 @@ Logging in with Google... Please restart Gemini CLI to continue.
useEffect(() => {
if (
initialPrompt &&
+ isConfigInitialized &&
!initialPromptSubmitted.current &&
!isAuthenticating &&
!isAuthDialogOpen &&
@@ -625,6 +635,7 @@ Logging in with Google... Please restart Gemini CLI to continue.
}
}, [
initialPrompt,
+ isConfigInitialized,
handleFinalSubmit,
isAuthenticating,
isAuthDialogOpen,
@@ -926,6 +937,7 @@ Logging in with Google... Please restart Gemini CLI to continue.
isThemeDialogOpen,
themeError,
isAuthenticating,
+ isConfigInitialized,
authError,
isAuthDialogOpen,
editorError,
@@ -997,6 +1009,7 @@ Logging in with Google... Please restart Gemini CLI to continue.
isThemeDialogOpen,
themeError,
isAuthenticating,
+ isConfigInitialized,
authError,
isAuthDialogOpen,
editorError,
diff --git a/packages/cli/src/ui/components/Composer.tsx b/packages/cli/src/ui/components/Composer.tsx
index d84dc602bd..fa5322e4ae 100644
--- a/packages/cli/src/ui/components/Composer.tsx
+++ b/packages/cli/src/ui/components/Composer.tsx
@@ -23,6 +23,7 @@ import { useConfig } from '../contexts/ConfigContext.js';
import { useSettings } from '../contexts/SettingsContext.js';
import { ApprovalMode } from '@google/gemini-cli-core';
import { StreamingState } from '../types.js';
+import { ConfigInitDisplay } from '../components/ConfigInitDisplay.js';
const MAX_DISPLAYED_QUEUED_MESSAGES = 3;
@@ -72,6 +73,8 @@ export const Composer = () => {
elapsedTime={uiState.elapsedTime}
/>
+ {!uiState.isConfigInitialized && }
+
{uiState.messageQueue.length > 0 && (
{uiState.messageQueue
diff --git a/packages/cli/src/ui/components/ConfigInitDisplay.tsx b/packages/cli/src/ui/components/ConfigInitDisplay.tsx
new file mode 100644
index 0000000000..f4ca44ad74
--- /dev/null
+++ b/packages/cli/src/ui/components/ConfigInitDisplay.tsx
@@ -0,0 +1,46 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { useEffect, useState } from 'react';
+import { appEvents } from './../../utils/events.js';
+import { Box, Text } from 'ink';
+import { useConfig } from '../contexts/ConfigContext.js';
+import { type McpClient, MCPServerStatus } from '@google/gemini-cli-core';
+import { GeminiSpinner } from './GeminiRespondingSpinner.js';
+
+export const ConfigInitDisplay = () => {
+ const config = useConfig();
+ const [message, setMessage] = useState('Initializing...');
+
+ useEffect(() => {
+ const onChange = (clients?: Map) => {
+ if (!clients || clients.size === 0) {
+ setMessage(`Initializing...`);
+ return;
+ }
+ let connected = 0;
+ for (const client of clients.values()) {
+ if (client.getStatus() === MCPServerStatus.CONNECTED) {
+ connected++;
+ }
+ }
+ setMessage(`Connecting to MCP servers... (${connected}/${clients.size})`);
+ };
+
+ appEvents.on('mcp-client-update', onChange);
+ return () => {
+ appEvents.off('mcp-client-update', onChange);
+ };
+ }, [config]);
+
+ return (
+
+
+ {message}
+
+
+ );
+};
diff --git a/packages/cli/src/ui/components/GeminiRespondingSpinner.tsx b/packages/cli/src/ui/components/GeminiRespondingSpinner.tsx
index caf774e2a8..057faaffc3 100644
--- a/packages/cli/src/ui/components/GeminiRespondingSpinner.tsx
+++ b/packages/cli/src/ui/components/GeminiRespondingSpinner.tsx
@@ -30,10 +30,11 @@ export const GeminiRespondingSpinner: React.FC<
const streamingState = useStreamingContext();
const isScreenReaderEnabled = useIsScreenReaderEnabled();
if (streamingState === StreamingState.Responding) {
- return isScreenReaderEnabled ? (
- {SCREEN_READER_RESPONDING}
- ) : (
-
+ return (
+
);
} else if (nonRespondingDisplay) {
return isScreenReaderEnabled ? (
@@ -44,3 +45,20 @@ export const GeminiRespondingSpinner: React.FC<
}
return null;
};
+
+interface GeminiSpinnerProps {
+ spinnerType?: SpinnerName;
+ altText?: string;
+}
+
+export const GeminiSpinner: React.FC = ({
+ spinnerType = 'dots',
+ altText,
+}) => {
+ const isScreenReaderEnabled = useIsScreenReaderEnabled();
+ return isScreenReaderEnabled ? (
+ {altText}
+ ) : (
+
+ );
+};
diff --git a/packages/cli/src/ui/contexts/UIStateContext.tsx b/packages/cli/src/ui/contexts/UIStateContext.tsx
index f09d34d63f..0c62ebaf57 100644
--- a/packages/cli/src/ui/contexts/UIStateContext.tsx
+++ b/packages/cli/src/ui/contexts/UIStateContext.tsx
@@ -38,6 +38,7 @@ export interface UIState {
isThemeDialogOpen: boolean;
themeError: string | null;
isAuthenticating: boolean;
+ isConfigInitialized: boolean;
authError: string | null;
isAuthDialogOpen: boolean;
editorError: string | null;
diff --git a/packages/cli/src/ui/hooks/useMessageQueue.test.ts b/packages/cli/src/ui/hooks/useMessageQueue.test.ts
index 01e49afe5f..33dbf3211c 100644
--- a/packages/cli/src/ui/hooks/useMessageQueue.test.ts
+++ b/packages/cli/src/ui/hooks/useMessageQueue.test.ts
@@ -25,6 +25,7 @@ describe('useMessageQueue', () => {
it('should initialize with empty queue', () => {
const { result } = renderHook(() =>
useMessageQueue({
+ isConfigInitialized: true,
streamingState: StreamingState.Idle,
submitQuery: mockSubmitQuery,
}),
@@ -37,6 +38,7 @@ describe('useMessageQueue', () => {
it('should add messages to queue', () => {
const { result } = renderHook(() =>
useMessageQueue({
+ isConfigInitialized: true,
streamingState: StreamingState.Responding,
submitQuery: mockSubmitQuery,
}),
@@ -56,6 +58,7 @@ describe('useMessageQueue', () => {
it('should filter out empty messages', () => {
const { result } = renderHook(() =>
useMessageQueue({
+ isConfigInitialized: true,
streamingState: StreamingState.Responding,
submitQuery: mockSubmitQuery,
}),
@@ -77,6 +80,7 @@ describe('useMessageQueue', () => {
it('should clear queue', () => {
const { result } = renderHook(() =>
useMessageQueue({
+ isConfigInitialized: true,
streamingState: StreamingState.Responding,
submitQuery: mockSubmitQuery,
}),
@@ -98,6 +102,7 @@ describe('useMessageQueue', () => {
it('should return queued messages as text with double newlines', () => {
const { result } = renderHook(() =>
useMessageQueue({
+ isConfigInitialized: true,
streamingState: StreamingState.Responding,
submitQuery: mockSubmitQuery,
}),
@@ -118,6 +123,7 @@ describe('useMessageQueue', () => {
const { result, rerender } = renderHook(
({ streamingState }) =>
useMessageQueue({
+ isConfigInitialized: true,
streamingState,
submitQuery: mockSubmitQuery,
}),
@@ -145,6 +151,7 @@ describe('useMessageQueue', () => {
const { rerender } = renderHook(
({ streamingState }) =>
useMessageQueue({
+ isConfigInitialized: true,
streamingState,
submitQuery: mockSubmitQuery,
}),
@@ -163,6 +170,7 @@ describe('useMessageQueue', () => {
const { result, rerender } = renderHook(
({ streamingState }) =>
useMessageQueue({
+ isConfigInitialized: true,
streamingState,
submitQuery: mockSubmitQuery,
}),
@@ -187,6 +195,7 @@ describe('useMessageQueue', () => {
const { result, rerender } = renderHook(
({ streamingState }) =>
useMessageQueue({
+ isConfigInitialized: true,
streamingState,
submitQuery: mockSubmitQuery,
}),
diff --git a/packages/cli/src/ui/hooks/useMessageQueue.ts b/packages/cli/src/ui/hooks/useMessageQueue.ts
index f7bbe1ebe7..517040fecf 100644
--- a/packages/cli/src/ui/hooks/useMessageQueue.ts
+++ b/packages/cli/src/ui/hooks/useMessageQueue.ts
@@ -8,6 +8,7 @@ import { useCallback, useEffect, useState } from 'react';
import { StreamingState } from '../types.js';
export interface UseMessageQueueOptions {
+ isConfigInitialized: boolean;
streamingState: StreamingState;
submitQuery: (query: string) => void;
}
@@ -25,6 +26,7 @@ export interface UseMessageQueueReturn {
* sends them when streaming completes.
*/
export function useMessageQueue({
+ isConfigInitialized,
streamingState,
submitQuery,
}: UseMessageQueueOptions): UseMessageQueueReturn {
@@ -51,14 +53,18 @@ export function useMessageQueue({
// Process queued messages when streaming becomes idle
useEffect(() => {
- if (streamingState === StreamingState.Idle && messageQueue.length > 0) {
+ if (
+ isConfigInitialized &&
+ streamingState === StreamingState.Idle &&
+ messageQueue.length > 0
+ ) {
// Combine all messages with double newlines for clarity
const combinedMessage = messageQueue.join('\n\n');
// Clear the queue and submit
setMessageQueue([]);
submitQuery(combinedMessage);
}
- }, [streamingState, messageQueue, submitQuery]);
+ }, [isConfigInitialized, streamingState, messageQueue, submitQuery]);
return {
messageQueue,
diff --git a/packages/core/src/tools/mcp-client-manager.ts b/packages/core/src/tools/mcp-client-manager.ts
index dcb4589106..93e25ea8b2 100644
--- a/packages/core/src/tools/mcp-client-manager.ts
+++ b/packages/core/src/tools/mcp-client-manager.ts
@@ -66,22 +66,11 @@ export class McpClientManager {
this.mcpServerCommand,
);
- const serverEntries = Object.entries(servers);
- const total = serverEntries.length;
-
- this.eventEmitter?.emit('mcp-servers-discovery-start', { count: total });
-
this.discoveryState = MCPDiscoveryState.IN_PROGRESS;
- const discoveryPromises = serverEntries.map(
- async ([name, config], index) => {
- const current = index + 1;
- this.eventEmitter?.emit('mcp-server-connecting', {
- name,
- current,
- total,
- });
-
+ this.eventEmitter?.emit('mcp-client-update', this.clients);
+ const discoveryPromises = Object.entries(servers).map(
+ async ([name, config]) => {
const client = new McpClient(
name,
config,
@@ -92,21 +81,13 @@ export class McpClientManager {
);
this.clients.set(name, client);
+ this.eventEmitter?.emit('mcp-client-update', this.clients);
try {
await client.connect();
await client.discover(cliConfig);
- this.eventEmitter?.emit('mcp-server-connected', {
- name,
- current,
- total,
- });
+ this.eventEmitter?.emit('mcp-client-update', this.clients);
} catch (error) {
- this.eventEmitter?.emit('mcp-server-error', {
- name,
- current,
- total,
- error,
- });
+ this.eventEmitter?.emit('mcp-client-update', this.clients);
// Log the error but don't let a single failed server stop the others
console.error(
`Error during discovery for server '${name}': ${getErrorMessage(