Skip to content

Commit cebe386

Browse files
gemini-cli-robotAdib234SandyTao520
authored
fix(patch): cherry-pick 12a5490 to release/v0.26.0-preview.3-pr-17395 [CONFLICTS] (#17424)
Co-authored-by: Adib234 <30782825+Adib234@users.noreply.github.com> Co-authored-by: Sandy Tao <sandytao520@icloud.com> Co-authored-by: A.K.M. Adib <adibakm@google.com>
1 parent 1c207e2 commit cebe386

10 files changed

Lines changed: 242 additions & 159 deletions

File tree

packages/cli/src/config/config.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {
3737
type HookEventName,
3838
type OutputFormat,
3939
GEMINI_MODEL_ALIAS_AUTO,
40+
coreEvents,
4041
} from '@google/gemini-cli-core';
4142
import {
4243
type Settings,
@@ -47,7 +48,6 @@ import {
4748

4849
import { loadSandboxConfig } from './sandboxConfig.js';
4950
import { resolvePath } from '../utils/resolvePath.js';
50-
import { appEvents } from '../utils/events.js';
5151
import { RESUME_LATEST } from '../utils/sessionUtils.js';
5252

5353
import { isWorkspaceTrusted } from './trustedFolders.js';
@@ -450,7 +450,7 @@ export async function loadCliConfig(
450450
requestSetting: promptForSetting,
451451
workspaceDir: cwd,
452452
enabledExtensionOverrides: argv.extensions,
453-
eventEmitter: appEvents as EventEmitter<ExtensionEvents>,
453+
eventEmitter: coreEvents as EventEmitter<ExtensionEvents>,
454454
clientVersion: await getVersion(),
455455
});
456456
await extensionManager.loadExtensions();
@@ -746,7 +746,7 @@ export async function loadCliConfig(
746746
truncateToolOutputThreshold: settings.tools?.truncateToolOutputThreshold,
747747
truncateToolOutputLines: settings.tools?.truncateToolOutputLines,
748748
enableToolOutputTruncation: settings.tools?.enableToolOutputTruncation,
749-
eventEmitter: appEvents,
749+
eventEmitter: coreEvents,
750750
useWriteTodos: argv.useWriteTodos ?? settings.useWriteTodos,
751751
output: {
752752
format: (argv.outputFormat ?? settings.output?.format) as OutputFormat,

packages/cli/src/ui/AppContainer.tsx

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ import { RELAUNCH_EXIT_CODE } from '../utils/processUtils.js';
103103
import type { SessionInfo } from '../utils/sessionUtils.js';
104104
import { useMessageQueue } from './hooks/useMessageQueue.js';
105105
import { useAutoAcceptIndicator } from './hooks/useAutoAcceptIndicator.js';
106+
import { useMcpStatus } from './hooks/useMcpStatus.js';
106107
import { useSessionStats } from './contexts/SessionContext.js';
107108
import { useGitBranchName } from './hooks/useGitBranchName.js';
108109
import {
@@ -129,6 +130,7 @@ import {
129130
} from './constants.js';
130131
import { LoginWithGoogleRestartDialog } from './auth/LoginWithGoogleRestartDialog.js';
131132
import { useInactivityTimer } from './hooks/useInactivityTimer.js';
133+
import { isSlashCommand } from './utils/commandUtils.js';
132134

133135
function isToolExecuting(pendingHistoryItems: HistoryItemWithoutId[]) {
134136
return pendingHistoryItems.some((item) => {
@@ -861,6 +863,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
861863
isActive: !embeddedShellFocused,
862864
});
863865

866+
const { isMcpReady } = useMcpStatus(config);
867+
864868
const {
865869
messageQueue,
866870
addMessage,
@@ -871,6 +875,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
871875
isConfigInitialized,
872876
streamingState,
873877
submitQuery,
878+
isMcpReady,
874879
});
875880

876881
cancelHandlerRef.current = useCallback(
@@ -909,10 +914,31 @@ Logging in with Google... Restarting Gemini CLI to continue.
909914

910915
const handleFinalSubmit = useCallback(
911916
(submittedValue: string) => {
912-
addMessage(submittedValue);
917+
const isSlash = isSlashCommand(submittedValue.trim());
918+
const isIdle = streamingState === StreamingState.Idle;
919+
920+
if (isSlash || (isIdle && isMcpReady)) {
921+
void submitQuery(submittedValue);
922+
} else {
923+
// Check messageQueue.length === 0 to only notify on the first queued item
924+
if (isIdle && !isMcpReady && messageQueue.length === 0) {
925+
coreEvents.emitFeedback(
926+
'info',
927+
'Waiting for MCP servers to initialize... Slash commands are still available and prompts will be queued.',
928+
);
929+
}
930+
addMessage(submittedValue);
931+
}
913932
addInput(submittedValue); // Track input for up-arrow history
914933
},
915-
[addMessage, addInput],
934+
[
935+
addMessage,
936+
addInput,
937+
submitQuery,
938+
isMcpReady,
939+
streamingState,
940+
messageQueue.length,
941+
],
916942
);
917943

918944
const handleClearScreen = useCallback(() => {

packages/cli/src/ui/hooks/useGeminiStream.test.tsx

Lines changed: 0 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -1962,73 +1962,6 @@ describe('useGeminiStream', () => {
19621962
});
19631963
});
19641964

1965-
describe('MCP Discovery State', () => {
1966-
it('should block non-slash command queries when discovery is in progress and servers exist', async () => {
1967-
const mockMcpClientManager = {
1968-
getDiscoveryState: vi
1969-
.fn()
1970-
.mockReturnValue(MCPDiscoveryState.IN_PROGRESS),
1971-
getMcpServerCount: vi.fn().mockReturnValue(1),
1972-
};
1973-
mockConfig.getMcpClientManager = () => mockMcpClientManager as any;
1974-
1975-
const { result } = renderTestHook();
1976-
1977-
await act(async () => {
1978-
await result.current.submitQuery('test query');
1979-
});
1980-
1981-
expect(coreEvents.emitFeedback).toHaveBeenCalledWith(
1982-
'info',
1983-
'Waiting for MCP servers to initialize... Slash commands are still available.',
1984-
);
1985-
expect(mockSendMessageStream).not.toHaveBeenCalled();
1986-
});
1987-
1988-
it('should NOT block queries when discovery is NOT_STARTED but there are no servers', async () => {
1989-
const mockMcpClientManager = {
1990-
getDiscoveryState: vi
1991-
.fn()
1992-
.mockReturnValue(MCPDiscoveryState.NOT_STARTED),
1993-
getMcpServerCount: vi.fn().mockReturnValue(0),
1994-
};
1995-
mockConfig.getMcpClientManager = () => mockMcpClientManager as any;
1996-
1997-
const { result } = renderTestHook();
1998-
1999-
await act(async () => {
2000-
await result.current.submitQuery('test query');
2001-
});
2002-
2003-
expect(coreEvents.emitFeedback).not.toHaveBeenCalledWith(
2004-
'info',
2005-
'Waiting for MCP servers to initialize... Slash commands are still available.',
2006-
);
2007-
expect(mockSendMessageStream).toHaveBeenCalled();
2008-
});
2009-
2010-
it('should NOT block slash commands even when discovery is in progress', async () => {
2011-
const mockMcpClientManager = {
2012-
getDiscoveryState: vi
2013-
.fn()
2014-
.mockReturnValue(MCPDiscoveryState.IN_PROGRESS),
2015-
getMcpServerCount: vi.fn().mockReturnValue(1),
2016-
};
2017-
mockConfig.getMcpClientManager = () => mockMcpClientManager as any;
2018-
2019-
const { result } = renderTestHook();
2020-
2021-
await act(async () => {
2022-
await result.current.submitQuery('/help');
2023-
});
2024-
2025-
expect(coreEvents.emitFeedback).not.toHaveBeenCalledWith(
2026-
'info',
2027-
'Waiting for MCP servers to initialize... Slash commands are still available.',
2028-
);
2029-
});
2030-
});
2031-
20321965
describe('handleFinishedEvent', () => {
20331966
it('should add info message for MAX_TOKENS finish reason', async () => {
20341967
// Setup mock to return a stream with MAX_TOKENS finish reason
@@ -3182,68 +3115,4 @@ describe('useGeminiStream', () => {
31823115
});
31833116
});
31843117
});
3185-
3186-
describe('MCP Server Initialization', () => {
3187-
it('should allow slash commands to run while MCP servers are initializing', async () => {
3188-
const mockMcpClientManager = {
3189-
getDiscoveryState: vi
3190-
.fn()
3191-
.mockReturnValue(MCPDiscoveryState.IN_PROGRESS),
3192-
getMcpServerCount: vi.fn().mockReturnValue(1),
3193-
};
3194-
mockConfig.getMcpClientManager = () => mockMcpClientManager as any;
3195-
3196-
const { result } = renderTestHook();
3197-
3198-
await act(async () => {
3199-
await result.current.submitQuery('/help');
3200-
});
3201-
3202-
// Slash command should be handled, and no Gemini call should be made.
3203-
expect(mockHandleSlashCommand).toHaveBeenCalledWith('/help');
3204-
expect(coreEvents.emitFeedback).not.toHaveBeenCalled();
3205-
});
3206-
3207-
it('should block normal prompts and provide feedback while MCP servers are initializing', async () => {
3208-
const mockMcpClientManager = {
3209-
getDiscoveryState: vi
3210-
.fn()
3211-
.mockReturnValue(MCPDiscoveryState.IN_PROGRESS),
3212-
getMcpServerCount: vi.fn().mockReturnValue(1),
3213-
};
3214-
mockConfig.getMcpClientManager = () => mockMcpClientManager as any;
3215-
const { result } = renderTestHook();
3216-
3217-
await act(async () => {
3218-
await result.current.submitQuery('a normal prompt');
3219-
});
3220-
3221-
// No slash command, no Gemini call, but feedback should be emitted.
3222-
expect(mockHandleSlashCommand).not.toHaveBeenCalled();
3223-
expect(mockSendMessageStream).not.toHaveBeenCalled();
3224-
expect(coreEvents.emitFeedback).toHaveBeenCalledWith(
3225-
'info',
3226-
'Waiting for MCP servers to initialize... Slash commands are still available.',
3227-
);
3228-
});
3229-
3230-
it('should allow normal prompts to run when MCP servers are finished initializing', async () => {
3231-
const mockMcpClientManager = {
3232-
getDiscoveryState: vi.fn().mockReturnValue(MCPDiscoveryState.COMPLETED),
3233-
getMcpServerCount: vi.fn().mockReturnValue(1),
3234-
};
3235-
mockConfig.getMcpClientManager = () => mockMcpClientManager as any;
3236-
3237-
const { result } = renderTestHook();
3238-
3239-
await act(async () => {
3240-
await result.current.submitQuery('a normal prompt');
3241-
});
3242-
3243-
// Prompt should be sent to Gemini.
3244-
expect(mockHandleSlashCommand).not.toHaveBeenCalled();
3245-
expect(mockSendMessageStream).toHaveBeenCalled();
3246-
expect(coreEvents.emitFeedback).not.toHaveBeenCalled();
3247-
});
3248-
});
32493118
});

packages/cli/src/ui/hooks/useGeminiStream.ts

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ import {
3131
ValidationRequiredError,
3232
coreEvents,
3333
CoreEvent,
34-
MCPDiscoveryState,
3534
} from '@google/gemini-cli-core';
3635
import type {
3736
Config,
@@ -961,25 +960,6 @@ export const useGeminiStream = (
961960
async ({ metadata: spanMetadata }) => {
962961
spanMetadata.input = query;
963962

964-
const discoveryState = config
965-
.getMcpClientManager()
966-
?.getDiscoveryState();
967-
const mcpServerCount =
968-
config.getMcpClientManager()?.getMcpServerCount() ?? 0;
969-
if (
970-
!options?.isContinuation &&
971-
typeof query === 'string' &&
972-
!isSlashCommand(query.trim()) &&
973-
mcpServerCount > 0 &&
974-
discoveryState !== MCPDiscoveryState.COMPLETED
975-
) {
976-
coreEvents.emitFeedback(
977-
'info',
978-
'Waiting for MCP servers to initialize... Slash commands are still available.',
979-
);
980-
return;
981-
}
982-
983963
const queryId = `${Date.now()}-${Math.random()}`;
984964
activeQueryIdRef.current = queryId;
985965
if (
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
8+
import { act } from 'react';
9+
import { render } from '../../test-utils/render.js';
10+
import { useMcpStatus } from './useMcpStatus.js';
11+
import {
12+
MCPDiscoveryState,
13+
type Config,
14+
CoreEvent,
15+
coreEvents,
16+
} from '@google/gemini-cli-core';
17+
18+
describe('useMcpStatus', () => {
19+
let mockConfig: Config;
20+
let mockMcpClientManager: {
21+
getDiscoveryState: Mock<() => MCPDiscoveryState>;
22+
getMcpServerCount: Mock<() => number>;
23+
};
24+
25+
beforeEach(() => {
26+
mockMcpClientManager = {
27+
getDiscoveryState: vi.fn().mockReturnValue(MCPDiscoveryState.NOT_STARTED),
28+
getMcpServerCount: vi.fn().mockReturnValue(0),
29+
};
30+
31+
mockConfig = {
32+
getMcpClientManager: vi.fn().mockReturnValue(mockMcpClientManager),
33+
} as unknown as Config;
34+
});
35+
36+
const renderMcpStatusHook = (config: Config) => {
37+
let hookResult: ReturnType<typeof useMcpStatus>;
38+
function TestComponent({ config }: { config: Config }) {
39+
hookResult = useMcpStatus(config);
40+
return null;
41+
}
42+
render(<TestComponent config={config} />);
43+
return {
44+
result: {
45+
get current() {
46+
return hookResult;
47+
},
48+
},
49+
};
50+
};
51+
52+
it('should initialize with correct values (no servers)', () => {
53+
const { result } = renderMcpStatusHook(mockConfig);
54+
55+
expect(result.current.discoveryState).toBe(MCPDiscoveryState.NOT_STARTED);
56+
expect(result.current.mcpServerCount).toBe(0);
57+
expect(result.current.isMcpReady).toBe(true);
58+
});
59+
60+
it('should initialize with correct values (with servers, not started)', () => {
61+
mockMcpClientManager.getMcpServerCount.mockReturnValue(1);
62+
const { result } = renderMcpStatusHook(mockConfig);
63+
64+
expect(result.current.isMcpReady).toBe(false);
65+
});
66+
67+
it('should not be ready while in progress', () => {
68+
mockMcpClientManager.getDiscoveryState.mockReturnValue(
69+
MCPDiscoveryState.IN_PROGRESS,
70+
);
71+
mockMcpClientManager.getMcpServerCount.mockReturnValue(1);
72+
const { result } = renderMcpStatusHook(mockConfig);
73+
74+
expect(result.current.isMcpReady).toBe(false);
75+
});
76+
77+
it('should update state when McpClientUpdate is emitted', () => {
78+
mockMcpClientManager.getMcpServerCount.mockReturnValue(1);
79+
mockMcpClientManager.getDiscoveryState.mockReturnValue(
80+
MCPDiscoveryState.IN_PROGRESS,
81+
);
82+
const { result } = renderMcpStatusHook(mockConfig);
83+
84+
expect(result.current.isMcpReady).toBe(false);
85+
86+
mockMcpClientManager.getDiscoveryState.mockReturnValue(
87+
MCPDiscoveryState.COMPLETED,
88+
);
89+
90+
act(() => {
91+
coreEvents.emit(CoreEvent.McpClientUpdate, new Map());
92+
});
93+
94+
expect(result.current.discoveryState).toBe(MCPDiscoveryState.COMPLETED);
95+
expect(result.current.isMcpReady).toBe(true);
96+
});
97+
});

0 commit comments

Comments
 (0)