mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-22 20:14:58 -07:00
Refactor: Migrate CLI appEvents to Core coreEvents (#15737)
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 />);
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -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]),
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user