Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 5 additions & 54 deletions packages/cli/src/gemini.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 (
<Box>
<Text>
<Spinner /> {message}
</Text>
</Box>
);
};

import { runZedIntegration } from './zed-integration/zedIntegration.js';

export function setupUnhandledRejectionHandler() {
Expand Down Expand Up @@ -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(
<InitializingComponent initialTotal={mcpServersCount} />,
);
}

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);

Expand Down Expand Up @@ -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) {
Expand Down
11 changes: 5 additions & 6 deletions packages/cli/src/ui/AppContainer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<AppContainer
Expand All @@ -430,8 +429,8 @@ describe('AppContainer State Management', () => {
/>,
);
}).not.toThrow();
});
});
},
);
});

describe('Error Handling', () => {
Expand Down
21 changes: 17 additions & 4 deletions packages/cli/src/ui/AppContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ export const AppContainer = (props: AppContainerProps) => {

const [userTier, setUserTier] = useState<UserTierId | undefined>(undefined);

const [isConfigInitialized, setConfigInitialized] = useState(false);

// Auto-accept indicator
const showAutoAcceptIndicator = useAutoAcceptIndicator({
config,
Expand All @@ -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(() => {
Expand Down Expand Up @@ -517,6 +525,7 @@ Logging in with Google... Please restart Gemini CLI to continue.

const { messageQueue, addMessage, clearQueue, getQueuedMessagesText } =
useMessageQueue({
isConfigInitialized,
streamingState,
submitQuery,
});
Expand Down Expand Up @@ -612,6 +621,7 @@ Logging in with Google... Please restart Gemini CLI to continue.
useEffect(() => {
if (
initialPrompt &&
isConfigInitialized &&
!initialPromptSubmitted.current &&
!isAuthenticating &&
!isAuthDialogOpen &&
Expand All @@ -625,6 +635,7 @@ Logging in with Google... Please restart Gemini CLI to continue.
}
}, [
initialPrompt,
isConfigInitialized,
handleFinalSubmit,
isAuthenticating,
isAuthDialogOpen,
Expand Down Expand Up @@ -926,6 +937,7 @@ Logging in with Google... Please restart Gemini CLI to continue.
isThemeDialogOpen,
themeError,
isAuthenticating,
isConfigInitialized,
authError,
isAuthDialogOpen,
editorError,
Expand Down Expand Up @@ -997,6 +1009,7 @@ Logging in with Google... Please restart Gemini CLI to continue.
isThemeDialogOpen,
themeError,
isAuthenticating,
isConfigInitialized,
authError,
isAuthDialogOpen,
editorError,
Expand Down
3 changes: 3 additions & 0 deletions packages/cli/src/ui/components/Composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -72,6 +73,8 @@ export const Composer = () => {
elapsedTime={uiState.elapsedTime}
/>

{!uiState.isConfigInitialized && <ConfigInitDisplay />}

{uiState.messageQueue.length > 0 && (
<Box flexDirection="column" marginTop={1}>
{uiState.messageQueue
Expand Down
46 changes: 46 additions & 0 deletions packages/cli/src/ui/components/ConfigInitDisplay.tsx
Original file line number Diff line number Diff line change
@@ -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<string, McpClient>) => {
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 (
<Box marginTop={1}>
<Text>
<GeminiSpinner /> {message}
</Text>
</Box>
);
};
26 changes: 22 additions & 4 deletions packages/cli/src/ui/components/GeminiRespondingSpinner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,11 @@ export const GeminiRespondingSpinner: React.FC<
const streamingState = useStreamingContext();
const isScreenReaderEnabled = useIsScreenReaderEnabled();
if (streamingState === StreamingState.Responding) {
return isScreenReaderEnabled ? (
<Text>{SCREEN_READER_RESPONDING}</Text>
) : (
<Spinner type={spinnerType} />
return (
<GeminiSpinner
spinnerType={spinnerType}
altText={SCREEN_READER_RESPONDING}
/>
);
} else if (nonRespondingDisplay) {
return isScreenReaderEnabled ? (
Expand All @@ -44,3 +45,20 @@ export const GeminiRespondingSpinner: React.FC<
}
return null;
};

interface GeminiSpinnerProps {
spinnerType?: SpinnerName;
altText?: string;
}

export const GeminiSpinner: React.FC<GeminiSpinnerProps> = ({
spinnerType = 'dots',
altText,
}) => {
const isScreenReaderEnabled = useIsScreenReaderEnabled();
return isScreenReaderEnabled ? (
<Text>{altText}</Text>
) : (
<Spinner type={spinnerType} />
);
};
1 change: 1 addition & 0 deletions packages/cli/src/ui/contexts/UIStateContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export interface UIState {
isThemeDialogOpen: boolean;
themeError: string | null;
isAuthenticating: boolean;
isConfigInitialized: boolean;
authError: string | null;
isAuthDialogOpen: boolean;
editorError: string | null;
Expand Down
9 changes: 9 additions & 0 deletions packages/cli/src/ui/hooks/useMessageQueue.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ describe('useMessageQueue', () => {
it('should initialize with empty queue', () => {
const { result } = renderHook(() =>
useMessageQueue({
isConfigInitialized: true,
streamingState: StreamingState.Idle,
submitQuery: mockSubmitQuery,
}),
Expand All @@ -37,6 +38,7 @@ describe('useMessageQueue', () => {
it('should add messages to queue', () => {
const { result } = renderHook(() =>
useMessageQueue({
isConfigInitialized: true,
streamingState: StreamingState.Responding,
submitQuery: mockSubmitQuery,
}),
Expand All @@ -56,6 +58,7 @@ describe('useMessageQueue', () => {
it('should filter out empty messages', () => {
const { result } = renderHook(() =>
useMessageQueue({
isConfigInitialized: true,
streamingState: StreamingState.Responding,
submitQuery: mockSubmitQuery,
}),
Expand All @@ -77,6 +80,7 @@ describe('useMessageQueue', () => {
it('should clear queue', () => {
const { result } = renderHook(() =>
useMessageQueue({
isConfigInitialized: true,
streamingState: StreamingState.Responding,
submitQuery: mockSubmitQuery,
}),
Expand All @@ -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,
}),
Expand All @@ -118,6 +123,7 @@ describe('useMessageQueue', () => {
const { result, rerender } = renderHook(
({ streamingState }) =>
useMessageQueue({
isConfigInitialized: true,
streamingState,
submitQuery: mockSubmitQuery,
}),
Expand Down Expand Up @@ -145,6 +151,7 @@ describe('useMessageQueue', () => {
const { rerender } = renderHook(
({ streamingState }) =>
useMessageQueue({
isConfigInitialized: true,
streamingState,
submitQuery: mockSubmitQuery,
}),
Expand All @@ -163,6 +170,7 @@ describe('useMessageQueue', () => {
const { result, rerender } = renderHook(
({ streamingState }) =>
useMessageQueue({
isConfigInitialized: true,
streamingState,
submitQuery: mockSubmitQuery,
}),
Expand All @@ -187,6 +195,7 @@ describe('useMessageQueue', () => {
const { result, rerender } = renderHook(
({ streamingState }) =>
useMessageQueue({
isConfigInitialized: true,
streamingState,
submitQuery: mockSubmitQuery,
}),
Expand Down
Loading
Loading