Refactor: Migrate CLI appEvents to Core coreEvents (#15737)

This commit is contained in:
Adib234
2026-01-23 11:45:46 -05:00
committed by GitHub
parent 0b7d26c9e3
commit 488d5fc439
13 changed files with 90 additions and 93 deletions

View File

@@ -36,6 +36,7 @@ import {
type HookDefinition,
type HookEventName,
type OutputFormat,
coreEvents,
GEMINI_MODEL_ALIAS_AUTO,
} from '@google/gemini-cli-core';
import {
@@ -47,7 +48,6 @@ import {
import { loadSandboxConfig } from './sandboxConfig.js';
import { resolvePath } from '../utils/resolvePath.js';
import { appEvents } from '../utils/events.js';
import { RESUME_LATEST } from '../utils/sessionUtils.js';
import { isWorkspaceTrusted } from './trustedFolders.js';
@@ -467,7 +467,7 @@ export async function loadCliConfig(
requestSetting: promptForSetting,
workspaceDir: cwd,
enabledExtensionOverrides: argv.extensions,
eventEmitter: appEvents as EventEmitter<ExtensionEvents>,
eventEmitter: coreEvents as EventEmitter<ExtensionEvents>,
clientVersion: await getVersion(),
});
await extensionManager.loadExtensions();
@@ -772,7 +772,7 @@ export async function loadCliConfig(
truncateToolOutputThreshold: settings.tools?.truncateToolOutputThreshold,
truncateToolOutputLines: settings.tools?.truncateToolOutputLines,
enableToolOutputTruncation: settings.tools?.enableToolOutputTruncation,
eventEmitter: appEvents,
eventEmitter: coreEvents,
useWriteTodos: argv.useWriteTodos ?? settings.useWriteTodos,
output: {
format: (argv.outputFormat ?? settings.output?.format) as OutputFormat,

View File

@@ -85,6 +85,7 @@ vi.mock('./services/CommandService.js', () => ({
vi.mock('./services/FileCommandLoader.js');
vi.mock('./services/McpPromptLoader.js');
vi.mock('./services/BuiltinCommandLoader.js');
describe('runNonInteractive', () => {
let mockConfig: Config;
@@ -1184,7 +1185,9 @@ describe('runNonInteractive', () => {
'./services/FileCommandLoader.js'
);
const { McpPromptLoader } = await import('./services/McpPromptLoader.js');
const { BuiltinCommandLoader } = await import(
'./services/BuiltinCommandLoader.js'
);
mockGetCommands.mockReturnValue([]); // No commands found, so it will fall through
const events: ServerGeminiStreamEvent[] = [
{ type: GeminiEventType.Content, value: 'Acknowledged' },
@@ -1209,13 +1212,17 @@ describe('runNonInteractive', () => {
expect(FileCommandLoader).toHaveBeenCalledWith(mockConfig);
expect(McpPromptLoader).toHaveBeenCalledTimes(1);
expect(McpPromptLoader).toHaveBeenCalledWith(mockConfig);
expect(BuiltinCommandLoader).toHaveBeenCalledWith(mockConfig);
// Check that instances were passed to CommandService.create
expect(mockCommandServiceCreate).toHaveBeenCalledTimes(1);
const loadersArg = mockCommandServiceCreate.mock.calls[0][0];
expect(loadersArg).toHaveLength(2);
expect(loadersArg[0]).toBe(vi.mocked(McpPromptLoader).mock.instances[0]);
expect(loadersArg[1]).toBe(vi.mocked(FileCommandLoader).mock.instances[0]);
expect(loadersArg).toHaveLength(3);
expect(loadersArg[0]).toBe(
vi.mocked(BuiltinCommandLoader).mock.instances[0],
);
expect(loadersArg[1]).toBe(vi.mocked(McpPromptLoader).mock.instances[0]);
expect(loadersArg[2]).toBe(vi.mocked(FileCommandLoader).mock.instances[0]);
});
it('should allow a normally-excluded tool when --allowed-tools is set', async () => {

View File

@@ -13,6 +13,7 @@ import {
type Config,
} from '@google/gemini-cli-core';
import { CommandService } from './services/CommandService.js';
import { BuiltinCommandLoader } from './services/BuiltinCommandLoader.js';
import { FileCommandLoader } from './services/FileCommandLoader.js';
import { McpPromptLoader } from './services/McpPromptLoader.js';
import type { CommandContext } from './ui/commands/types.js';
@@ -40,7 +41,11 @@ export const handleSlashCommand = async (
}
const commandService = await CommandService.create(
[new McpPromptLoader(config), new FileCommandLoader(config)],
[
new BuiltinCommandLoader(config),
new McpPromptLoader(config),
new FileCommandLoader(config),
],
abortController.signal,
);
const commands = commandService.getCommands();

View File

@@ -20,8 +20,10 @@ import {
getErrorMessage,
MCPOAuthTokenStorage,
mcpServerRequiresOAuth,
CoreEvent,
coreEvents,
} from '@google/gemini-cli-core';
import { appEvents, AppEvent } from '../../utils/events.js';
import { MessageType, type HistoryItemMcpStatus } from '../types.js';
import {
McpServerEnablementManager,
@@ -100,8 +102,7 @@ const authCommand: SlashCommand = {
context.ui.addItem({ type: 'info', text: message });
};
appEvents.on(AppEvent.OauthDisplayMessage, displayListener);
coreEvents.on(CoreEvent.OauthDisplayMessage, displayListener);
try {
context.ui.addItem({
type: 'info',
@@ -118,12 +119,7 @@ const authCommand: SlashCommand = {
const mcpServerUrl = server.httpUrl || server.url;
const authProvider = new MCPOAuthProvider(new MCPOAuthTokenStorage());
await authProvider.authenticate(
serverName,
oauthConfig,
mcpServerUrl,
appEvents,
);
await authProvider.authenticate(serverName, oauthConfig, mcpServerUrl);
context.ui.addItem({
type: 'info',
@@ -160,7 +156,7 @@ const authCommand: SlashCommand = {
content: `Failed to authenticate with MCP server '${serverName}': ${getErrorMessage(error)}`,
};
} finally {
appEvents.removeListener(AppEvent.OauthDisplayMessage, displayListener);
coreEvents.removeListener(CoreEvent.OauthDisplayMessage, displayListener);
}
},
completion: async (context: CommandContext, partialArg: string) => {

View File

@@ -5,12 +5,25 @@
*/
import { act } from 'react';
import type { EventEmitter } from 'node:events';
import { render } from '../../test-utils/render.js';
import { waitFor } from '../../test-utils/async.js';
import { ConfigInitDisplay } from './ConfigInitDisplay.js';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { AppEvent } from '../../utils/events.js';
import { MCPServerStatus, type McpClient } from '@google/gemini-cli-core';
import {
describe,
it,
expect,
vi,
beforeEach,
afterEach,
type MockInstance,
} from 'vitest';
import {
CoreEvent,
MCPServerStatus,
type McpClient,
coreEvents,
} from '@google/gemini-cli-core';
import { Text } from 'ink';
// Mock GeminiSpinner
@@ -18,30 +31,11 @@ vi.mock('./GeminiRespondingSpinner.js', () => ({
GeminiSpinner: () => <Text>Spinner</Text>,
}));
// Mock appEvents
const { mockOn, mockOff, mockEmit } = vi.hoisted(() => ({
mockOn: vi.fn(),
mockOff: vi.fn(),
mockEmit: vi.fn(),
}));
vi.mock('../../utils/events.js', async (importOriginal) => {
const actual = await importOriginal<typeof import('../../utils/events.js')>();
return {
...actual,
appEvents: {
on: mockOn,
off: mockOff,
emit: mockEmit,
},
};
});
describe('ConfigInitDisplay', () => {
let onSpy: MockInstance<EventEmitter['on']>;
beforeEach(() => {
mockOn.mockClear();
mockOff.mockClear();
mockEmit.mockClear();
onSpy = vi.spyOn(coreEvents as EventEmitter, 'on');
});
afterEach(() => {
@@ -55,10 +49,11 @@ describe('ConfigInitDisplay', () => {
it('updates message on McpClientUpdate event', async () => {
let listener: ((clients?: Map<string, McpClient>) => void) | undefined;
mockOn.mockImplementation((event, fn) => {
if (event === AppEvent.McpClientUpdate) {
listener = fn;
onSpy.mockImplementation((event: unknown, fn: unknown) => {
if (event === CoreEvent.McpClientUpdate) {
listener = fn as (clients?: Map<string, McpClient>) => void;
}
return coreEvents;
});
const { lastFrame } = render(<ConfigInitDisplay />);
@@ -92,10 +87,11 @@ describe('ConfigInitDisplay', () => {
it('truncates list of waiting servers if too many', async () => {
let listener: ((clients?: Map<string, McpClient>) => void) | undefined;
mockOn.mockImplementation((event, fn) => {
if (event === AppEvent.McpClientUpdate) {
listener = fn;
onSpy.mockImplementation((event: unknown, fn: unknown) => {
if (event === CoreEvent.McpClientUpdate) {
listener = fn as (clients?: Map<string, McpClient>) => void;
}
return coreEvents;
});
const { lastFrame } = render(<ConfigInitDisplay />);
@@ -127,10 +123,11 @@ describe('ConfigInitDisplay', () => {
it('handles empty clients map', async () => {
let listener: ((clients?: Map<string, McpClient>) => void) | undefined;
mockOn.mockImplementation((event, fn) => {
if (event === AppEvent.McpClientUpdate) {
listener = fn;
onSpy.mockImplementation((event: unknown, fn: unknown) => {
if (event === CoreEvent.McpClientUpdate) {
listener = fn as (clients?: Map<string, McpClient>) => void;
}
return coreEvents;
});
const { lastFrame } = render(<ConfigInitDisplay />);

View File

@@ -5,9 +5,13 @@
*/
import { useEffect, useState } from 'react';
import { AppEvent, appEvents } from './../../utils/events.js';
import { Box, Text } from 'ink';
import { type McpClient, MCPServerStatus } from '@google/gemini-cli-core';
import {
CoreEvent,
coreEvents,
type McpClient,
MCPServerStatus,
} from '@google/gemini-cli-core';
import { GeminiSpinner } from './GeminiRespondingSpinner.js';
import { theme } from '../semantic-colors.js';
@@ -45,9 +49,9 @@ export const ConfigInitDisplay = () => {
}
};
appEvents.on(AppEvent.McpClientUpdate, onChange);
coreEvents.on(CoreEvent.McpClientUpdate, onChange);
return () => {
appEvents.off(AppEvent.McpClientUpdate, onChange);
coreEvents.off(CoreEvent.McpClientUpdate, onChange);
};
}, []);

View File

@@ -20,8 +20,8 @@ import {
type GeminiClient,
SlashCommandStatus,
makeFakeConfig,
coreEvents,
} from '@google/gemini-cli-core';
import { appEvents } from '../../utils/events.js';
const {
logSlashCommand,
@@ -1044,7 +1044,7 @@ describe('useSlashCommandProcessor', () => {
// We should not see a change until we fire an event.
await waitFor(() => expect(result.current.slashCommands).toEqual([]));
act(() => {
appEvents.emit('extensionsStarting');
coreEvents.emit('extensionsStarting');
});
await waitFor(() =>
expect(result.current.slashCommands).toEqual([newCommand]),

View File

@@ -30,6 +30,7 @@ import {
ToolConfirmationOutcome,
Storage,
IdeClient,
coreEvents,
addMCPStatusChangeListener,
removeMCPStatusChangeListener,
MCPDiscoveryState,
@@ -55,7 +56,6 @@ import {
type ExtensionUpdateAction,
type ExtensionUpdateStatus,
} from '../state/extensions.js';
import { appEvents } from '../../utils/events.js';
import {
LogoutConfirmationDialog,
LogoutChoice,
@@ -295,8 +295,8 @@ export const useSlashCommandProcessor = (
// starting/stopping
reloadCommands();
};
appEvents.on('extensionsStarting', extensionEventListener);
appEvents.on('extensionsStopping', extensionEventListener);
coreEvents.on('extensionsStarting', extensionEventListener);
coreEvents.on('extensionsStopping', extensionEventListener);
return () => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
@@ -305,8 +305,8 @@ export const useSlashCommandProcessor = (
ideClient.removeStatusChangeListener(listener);
})();
removeMCPStatusChangeListener(listener);
appEvents.off('extensionsStarting', extensionEventListener);
appEvents.off('extensionsStopping', extensionEventListener);
coreEvents.off('extensionsStarting', extensionEventListener);
coreEvents.off('extensionsStopping', extensionEventListener);
};
}, [config, reloadCommands]);

View File

@@ -10,13 +10,13 @@ import { appEvents, AppEvent } from './events.js';
describe('events', () => {
it('should allow registering and emitting events', () => {
const callback = vi.fn();
appEvents.on(AppEvent.OauthDisplayMessage, callback);
appEvents.on(AppEvent.SelectionWarning, callback);
appEvents.emit(AppEvent.OauthDisplayMessage, 'test message');
appEvents.emit(AppEvent.SelectionWarning);
expect(callback).toHaveBeenCalledWith('test message');
expect(callback).toHaveBeenCalled();
appEvents.off(AppEvent.OauthDisplayMessage, callback);
appEvents.off(AppEvent.SelectionWarning, callback);
});
it('should work with events without data', () => {

View File

@@ -4,23 +4,18 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type { ExtensionEvents, McpClient } from '@google/gemini-cli-core';
import { EventEmitter } from 'node:events';
export enum AppEvent {
OpenDebugConsole = 'open-debug-console',
OauthDisplayMessage = 'oauth-display-message',
Flicker = 'flicker',
McpClientUpdate = 'mcp-client-update',
SelectionWarning = 'selection-warning',
PasteTimeout = 'paste-timeout',
}
export interface AppEvents extends ExtensionEvents {
export interface AppEvents {
[AppEvent.OpenDebugConsole]: never[];
[AppEvent.OauthDisplayMessage]: string[];
[AppEvent.Flicker]: never[];
[AppEvent.McpClientUpdate]: Array<Map<string, McpClient> | never>;
[AppEvent.SelectionWarning]: never[];
[AppEvent.PasteTimeout]: never[];
}