Compare commits

...

1 Commits

Author SHA1 Message Date
Your Name 63bcec12cc feat(cli): Migrate to AgentLoopContext. 2026-03-17 22:16:20 +00:00
172 changed files with 1303 additions and 1168 deletions
+5 -3
View File
@@ -381,9 +381,11 @@ describe('Task', () => {
describe('currentPromptId and promptCount', () => {
it('should correctly initialize and update promptId and promptCount', async () => {
const mockConfig = createMockConfig();
mockConfig.getGeminiClient = vi.fn().mockReturnValue({
sendMessageStream: vi.fn().mockReturnValue((async function* () {})()),
});
mockConfig.createAgentLoopContext = vi.fn().mockReturnValue({
geminiClient: {
sendMessageStream: vi.fn().mockReturnValue((async function* () {})()),
},
} as any);
mockConfig.getSessionId = () => 'test-session-id';
const mockEventBus: ExecutionEventBus = {
+3 -1
View File
@@ -81,11 +81,13 @@ vi.mock('../config/config.js', async () => {
...actual,
loadConfig: vi.fn().mockImplementation(async () => {
const mockConfig = createMockConfig({
getToolRegistry: getToolRegistrySpy,
getApprovalMode: getApprovalModeSpy,
getShellExecutionConfig: getShellExecutionConfigSpy,
getExtensions: getExtensionsSpy,
});
mockConfig.createAgentLoopContext = vi
.fn()
.mockReturnValue({ toolRegistry: getToolRegistrySpy() } as any);
config = mockConfig as Config;
return config;
}),
+10 -27
View File
@@ -14,9 +14,8 @@ import {
ApprovalMode,
DEFAULT_GEMINI_MODEL,
DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
GeminiClient,
type GeminiClient,
HookSystem,
type MessageBus,
PolicyDecision,
tmpdir,
type Config,
@@ -38,30 +37,19 @@ export function createMockConfig(
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return this as unknown as Config;
},
get toolRegistry() {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const config = this as unknown as Config;
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return config.getToolRegistry?.() as unknown as ToolRegistry;
},
get messageBus() {
return (
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
(this as unknown as Config).getMessageBus?.() as unknown as MessageBus
);
},
get geminiClient() {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const config = this as unknown as Config;
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return config.getGeminiClient?.() as unknown as GeminiClient;
},
getToolRegistry: vi.fn().mockReturnValue({
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
toolRegistry: {
getTool: vi.fn(),
getAllToolNames: vi.fn().mockReturnValue([]),
getAllTools: vi.fn().mockReturnValue([]),
getToolsByServer: vi.fn().mockReturnValue([]),
}),
} as unknown as ToolRegistry,
messageBus: createMockMessageBus(),
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
geminiClient: {
getChat: vi.fn(),
} as unknown as GeminiClient,
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
getIdeMode: vi.fn().mockReturnValue(false),
isInteractive: () => true,
@@ -115,15 +103,10 @@ export function createMockConfig(
(mockConfig as unknown as { config: Config; promptId: string }).promptId =
'test-prompt-id';
mockConfig.getMessageBus = vi.fn().mockReturnValue(createMockMessageBus());
mockConfig.getHookSystem = vi
.fn()
.mockReturnValue(new HookSystem(mockConfig));
mockConfig.getGeminiClient = vi
.fn()
.mockReturnValue(new GeminiClient(mockConfig));
mockConfig.getPolicyEngine = vi.fn().mockReturnValue({
check: async () => {
const mode = mockConfig.getApprovalMode();
+5 -5
View File
@@ -163,10 +163,10 @@ describe('GeminiAgent', () => {
getContentGeneratorConfig: vi.fn(),
getActiveModel: vi.fn().mockReturnValue('gemini-pro'),
getModel: vi.fn().mockReturnValue('gemini-pro'),
getGeminiClient: vi.fn().mockReturnValue({
geminiClient: vi.fn().mockReturnValue({
startChat: vi.fn().mockResolvedValue({}),
}),
getMessageBus: vi.fn().mockReturnValue({
messageBus: vi.fn().mockReturnValue({
publish: vi.fn(),
subscribe: vi.fn(),
unsubscribe: vi.fn(),
@@ -321,7 +321,7 @@ describe('GeminiAgent', () => {
expect(response.sessionId).toBe('test-session-id');
expect(loadCliConfig).toHaveBeenCalled();
expect(mockConfig.initialize).toHaveBeenCalled();
expect(mockConfig.getGeminiClient).toHaveBeenCalled();
expect(mockConfig.createAgentLoopContext().geminiClient).toBeDefined();
// Verify deferred call
await vi.runAllTimersAsync();
@@ -639,7 +639,7 @@ describe('Session', () => {
mockConfig = {
getModel: vi.fn().mockReturnValue('gemini-pro'),
getActiveModel: vi.fn().mockReturnValue('gemini-pro'),
getToolRegistry: vi.fn().mockReturnValue(mockToolRegistry),
toolRegistry: vi.fn().mockReturnValue(mockToolRegistry),
getMcpServers: vi.fn(),
getFileService: vi.fn().mockReturnValue({
shouldIgnoreFile: vi.fn().mockReturnValue(false),
@@ -648,7 +648,7 @@ describe('Session', () => {
getTargetDir: vi.fn().mockReturnValue('/tmp'),
getEnableRecursiveFileSearch: vi.fn().mockReturnValue(false),
getDebugMode: vi.fn().mockReturnValue(false),
getMessageBus: vi.fn().mockReturnValue(mockMessageBus),
messageBus: vi.fn().mockReturnValue(mockMessageBus),
setApprovalMode: vi.fn(),
setModel: vi.fn(),
isPlanEnabled: vi.fn().mockReturnValue(true),
+49 -39
View File
@@ -47,6 +47,7 @@ import {
DEFAULT_GEMINI_MODEL_AUTO,
PREVIEW_GEMINI_MODEL_AUTO,
getDisplayString,
type AgentLoopContext,
} from '@google/gemini-cli-core';
import * as acp from '@agentclientprotocol/sdk';
import { AcpFileSystemService } from './fileSystemService.js';
@@ -73,10 +74,11 @@ import { SessionSelector } from '../utils/sessionUtils.js';
import { CommandHandler } from './commandHandler.js';
export async function runAcpClient(
config: Config,
context: AgentLoopContext,
settings: LoadedSettings,
argv: CliArgs,
) {
const config = context.config;
// ... (skip unchanged lines) ...
const { stdout: workingStdout } = createWorkingStdio();
@@ -104,7 +106,7 @@ export class GeminiAgent {
private customHeaders: Record<string, string> | undefined;
constructor(
private config: Config,
private context: AgentLoopContext,
private settings: LoadedSettings,
private argv: CliArgs,
private connection: acp.AgentSideConnection,
@@ -148,7 +150,7 @@ export class GeminiAgent {
},
];
await this.config.initialize();
await this.context.config.initialize();
const version = await getVersion();
return {
protocolVersion: acp.PROTOCOL_VERSION,
@@ -220,7 +222,7 @@ export class GeminiAgent {
this.baseUrl = baseUrl;
this.customHeaders = headers;
await this.config.refreshAuth(
await this.context.config.refreshAuth(
method,
apiKey ?? this.apiKey,
baseUrl,
@@ -300,12 +302,12 @@ export class GeminiAgent {
await config.initialize();
startupProfiler.flush(config);
const geminiClient = config.getGeminiClient();
const geminiClient = this.context.geminiClient;
const chat = await geminiClient.startChat();
const session = new Session(
sessionId,
chat,
config,
this.context,
this.connection,
this.settings,
);
@@ -362,7 +364,7 @@ export class GeminiAgent {
const clientHistory = convertSessionToClientHistory(sessionData.messages);
const geminiClient = config.getGeminiClient();
const geminiClient = this.context.geminiClient;
await geminiClient.initialize();
await geminiClient.resumeChat(clientHistory, {
conversation: sessionData,
@@ -372,7 +374,7 @@ export class GeminiAgent {
const session = new Session(
sessionId,
geminiClient.getChat(),
config,
this.context,
this.connection,
this.settings,
);
@@ -537,7 +539,7 @@ export class Session {
constructor(
private readonly id: string,
private readonly chat: GeminiChat,
private readonly config: Config,
private readonly context: AgentLoopContext,
private readonly connection: acp.AgentSideConnection,
private readonly settings: LoadedSettings,
) {}
@@ -552,13 +554,15 @@ export class Session {
}
setMode(modeId: acp.SessionModeId): acp.SetSessionModeResponse {
const availableModes = buildAvailableModes(this.config.isPlanEnabled());
const availableModes = buildAvailableModes(
this.context.config.isPlanEnabled(),
);
const mode = availableModes.find((m) => m.id === modeId);
if (!mode) {
throw new Error(`Invalid or unavailable mode: ${modeId}`);
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
this.config.setApprovalMode(mode.id as ApprovalMode);
this.context.config.setApprovalMode(mode.id as ApprovalMode);
return {};
}
@@ -579,7 +583,7 @@ export class Session {
}
setModel(modelId: acp.ModelId): acp.SetSessionModelResponse {
this.config.setModel(modelId);
this.context.config.setModel(modelId);
return {};
}
@@ -634,7 +638,7 @@ export class Session {
}
}
const tool = this.config.getToolRegistry().getTool(toolCall.name);
const tool = this.context.toolRegistry.getTool(toolCall.name);
await this.sendUpdate({
sessionUpdate: 'tool_call',
@@ -658,7 +662,7 @@ export class Session {
const pendingSend = new AbortController();
this.pendingPrompt = pendingSend;
await this.config.waitForMcpInit();
await this.context.config.waitForMcpInit();
const promptId = Math.random().toString(16).slice(2);
const chat = this.chat;
@@ -712,8 +716,8 @@ export class Session {
try {
const model = resolveModel(
this.config.getModel(),
(await this.config.getGemini31Launched?.()) ?? false,
this.context.config.getModel(),
(await this.context.config.getGemini31Launched?.()) ?? false,
);
const responseStream = await chat.sendMessageStream(
{ model },
@@ -804,9 +808,9 @@ export class Session {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
parts: Part[],
): Promise<boolean> {
const gitService = await this.config.getGitService();
const gitService = await this.context.config.getGitService();
const commandContext = {
config: this.config,
agentContext: this.context,
settings: this.settings,
git: gitService,
sendMessage: async (text: string) => {
@@ -842,7 +846,7 @@ export class Session {
const errorResponse = (error: Error) => {
const durationMs = Date.now() - startTime;
logToolCall(
this.config,
this.context.config,
new ToolCallEvent(
undefined,
fc.name ?? '',
@@ -872,7 +876,7 @@ export class Session {
return errorResponse(new Error('Missing function name'));
}
const toolRegistry = this.config.getToolRegistry();
const toolRegistry = this.context.toolRegistry;
const tool = toolRegistry.getTool(fc.name);
if (!tool) {
@@ -908,7 +912,10 @@ export class Session {
const params: acp.RequestPermissionRequest = {
sessionId: this.id,
options: toPermissionOptions(confirmationDetails, this.config),
options: toPermissionOptions(
confirmationDetails,
this.context.config,
),
toolCall: {
toolCallId: callId,
status: 'pending',
@@ -971,7 +978,7 @@ export class Session {
const durationMs = Date.now() - startTime;
logToolCall(
this.config,
this.context.config,
new ToolCallEvent(
undefined,
fc.name ?? '',
@@ -985,7 +992,7 @@ export class Session {
),
);
this.chat.recordCompletedToolCalls(this.config.getActiveModel(), [
this.chat.recordCompletedToolCalls(this.context.config.getActiveModel(), [
{
status: CoreToolCallStatus.Success,
request: {
@@ -1003,8 +1010,8 @@ export class Session {
fc.name,
callId,
toolResult.llmContent,
this.config.getActiveModel(),
this.config,
this.context.config.getActiveModel(),
this.context.config,
),
resultDisplay: toolResult.returnDisplay,
error: undefined,
@@ -1017,8 +1024,8 @@ export class Session {
fc.name,
callId,
toolResult.llmContent,
this.config.getActiveModel(),
this.config,
this.context.config.getActiveModel(),
this.context.config,
);
} catch (e) {
const error = e instanceof Error ? e : new Error(String(e));
@@ -1032,7 +1039,7 @@ export class Session {
],
});
this.chat.recordCompletedToolCalls(this.config.getActiveModel(), [
this.chat.recordCompletedToolCalls(this.context.config.getActiveModel(), [
{
status: CoreToolCallStatus.Error,
request: {
@@ -1118,18 +1125,18 @@ export class Session {
const atPathToResolvedSpecMap = new Map<string, string>();
// Get centralized file discovery service
const fileDiscovery = this.config.getFileService();
const fileDiscovery = this.context.config.getFileService();
const fileFilteringOptions: FilterFilesOptions =
this.config.getFileFilteringOptions();
this.context.config.getFileFilteringOptions();
const pathSpecsToRead: string[] = [];
const contentLabelsForDisplay: string[] = [];
const ignoredPaths: string[] = [];
const toolRegistry = this.config.getToolRegistry();
const toolRegistry = this.context.toolRegistry;
const readManyFilesTool = new ReadManyFilesTool(
this.config,
this.config.getMessageBus(),
this.context.config,
this.context.messageBus,
);
const globTool = toolRegistry.getTool('glob');
@@ -1148,8 +1155,11 @@ export class Session {
let currentPathSpec = pathName;
let resolvedSuccessfully = false;
try {
const absolutePath = path.resolve(this.config.getTargetDir(), pathName);
if (isWithinRoot(absolutePath, this.config.getTargetDir())) {
const absolutePath = path.resolve(
this.context.config.getTargetDir(),
pathName,
);
if (isWithinRoot(absolutePath, this.context.config.getTargetDir())) {
const stats = await fs.stat(absolutePath);
if (stats.isDirectory()) {
currentPathSpec = pathName.endsWith('/')
@@ -1169,7 +1179,7 @@ export class Session {
}
} catch (error) {
if (isNodeError(error) && error.code === 'ENOENT') {
if (this.config.getEnableRecursiveFileSearch() && globTool) {
if (this.context.config.getEnableRecursiveFileSearch() && globTool) {
this.debug(
`Path ${pathName} not found directly, attempting glob search.`,
);
@@ -1177,7 +1187,7 @@ export class Session {
const globResult = await globTool.buildAndExecute(
{
pattern: `**/*${pathName}*`,
path: this.config.getTargetDir(),
path: this.context.config.getTargetDir(),
},
abortSignal,
);
@@ -1191,7 +1201,7 @@ export class Session {
if (lines.length > 1 && lines[1]) {
const firstMatchAbsolute = lines[1].trim();
currentPathSpec = path.relative(
this.config.getTargetDir(),
this.context.config.getTargetDir(),
firstMatchAbsolute,
);
this.debug(
@@ -1402,7 +1412,7 @@ export class Session {
}
debug(msg: string) {
if (this.config.getDebugMode()) {
if (this.context.config.getDebugMode()) {
debugLogger.warn(msg);
}
}
+6 -4
View File
@@ -83,7 +83,7 @@ describe('GeminiAgent Session Resume', () => {
initialize: vi.fn().mockResolvedValue(undefined),
getFileSystemService: vi.fn(),
setFileSystemService: vi.fn(),
getGeminiClient: vi.fn().mockReturnValue({
geminiClient: vi.fn().mockReturnValue({
initialize: vi.fn().mockResolvedValue(undefined),
resumeChat: vi.fn().mockResolvedValue(undefined),
getChat: vi.fn().mockReturnValue({}),
@@ -158,9 +158,9 @@ describe('GeminiAgent Session Resume', () => {
],
};
mockConfig.getToolRegistry = vi.fn().mockReturnValue({
(mockConfig as any).toolRegistry = {
getTool: vi.fn().mockReturnValue({ kind: 'read' }),
});
} as any;
(SessionSelector as unknown as Mock).mockImplementation(() => ({
resolveSession: vi.fn().mockResolvedValue({
@@ -219,7 +219,9 @@ describe('GeminiAgent Session Resume', () => {
});
// Verify resumeChat received the correct arguments
expect(mockConfig.getGeminiClient().resumeChat).toHaveBeenCalledWith(
expect(
mockConfig.createAgentLoopContext().geminiClient.resumeChat,
).toHaveBeenCalledWith(
mockClientHistory,
expect.objectContaining({
conversation: sessionData,
+10 -9
View File
@@ -53,7 +53,7 @@ export class ListExtensionsCommand implements Command {
context: CommandContext,
_: string[],
): Promise<CommandExecutionResponse> {
const extensions = listExtensions(context.config);
const extensions = listExtensions(context.agentContext.config);
const data = extensions.length ? extensions : 'No extensions installed.';
return { name: this.name, data };
@@ -134,7 +134,7 @@ export class EnableExtensionCommand implements Command {
args: string[],
): Promise<CommandExecutionResponse> {
const enableContext = getEnableDisableContext(
context.config,
context.agentContext.config,
args,
'enable',
);
@@ -156,7 +156,8 @@ export class EnableExtensionCommand implements Command {
if (extension?.mcpServers) {
const mcpEnablementManager = McpServerEnablementManager.getInstance();
const mcpClientManager = context.config.getMcpClientManager();
const mcpClientManager =
context.agentContext.config.getMcpClientManager();
const enabledServers = await mcpEnablementManager.autoEnableServers(
Object.keys(extension.mcpServers),
);
@@ -191,7 +192,7 @@ export class DisableExtensionCommand implements Command {
args: string[],
): Promise<CommandExecutionResponse> {
const enableContext = getEnableDisableContext(
context.config,
context.agentContext.config,
args,
'disable',
);
@@ -223,7 +224,7 @@ export class InstallExtensionCommand implements Command {
context: CommandContext,
args: string[],
): Promise<CommandExecutionResponse> {
const extensionLoader = context.config.getExtensionLoader();
const extensionLoader = context.agentContext.config.getExtensionLoader();
if (!(extensionLoader instanceof ExtensionManager)) {
return {
name: this.name,
@@ -268,7 +269,7 @@ export class LinkExtensionCommand implements Command {
context: CommandContext,
args: string[],
): Promise<CommandExecutionResponse> {
const extensionLoader = context.config.getExtensionLoader();
const extensionLoader = context.agentContext.config.getExtensionLoader();
if (!(extensionLoader instanceof ExtensionManager)) {
return {
name: this.name,
@@ -313,7 +314,7 @@ export class UninstallExtensionCommand implements Command {
context: CommandContext,
args: string[],
): Promise<CommandExecutionResponse> {
const extensionLoader = context.config.getExtensionLoader();
const extensionLoader = context.agentContext.config.getExtensionLoader();
if (!(extensionLoader instanceof ExtensionManager)) {
return {
name: this.name,
@@ -369,7 +370,7 @@ export class RestartExtensionCommand implements Command {
context: CommandContext,
args: string[],
): Promise<CommandExecutionResponse> {
const extensionLoader = context.config.getExtensionLoader();
const extensionLoader = context.agentContext.config.getExtensionLoader();
if (!(extensionLoader instanceof ExtensionManager)) {
return { name: this.name, data: 'Cannot restart extensions.' };
}
@@ -424,7 +425,7 @@ export class UpdateExtensionCommand implements Command {
context: CommandContext,
args: string[],
): Promise<CommandExecutionResponse> {
const extensionLoader = context.config.getExtensionLoader();
const extensionLoader = context.agentContext.config.getExtensionLoader();
if (!(extensionLoader instanceof ExtensionManager)) {
return { name: this.name, data: 'Cannot update extensions.' };
}
+1 -1
View File
@@ -22,7 +22,7 @@ export class InitCommand implements Command {
context: CommandContext,
_args: string[] = [],
): Promise<CommandExecutionResponse> {
const targetDir = context.config.getTargetDir();
const targetDir = context.agentContext.config.getTargetDir();
if (!targetDir) {
throw new Error('Command requires a workspace.');
}
+6 -6
View File
@@ -49,7 +49,7 @@ export class ShowMemoryCommand implements Command {
context: CommandContext,
_: string[],
): Promise<CommandExecutionResponse> {
const result = showMemory(context.config);
const result = showMemory(context.agentContext.config);
return { name: this.name, data: result.content };
}
}
@@ -63,7 +63,7 @@ export class RefreshMemoryCommand implements Command {
context: CommandContext,
_: string[],
): Promise<CommandExecutionResponse> {
const result = await refreshMemory(context.config);
const result = await refreshMemory(context.agentContext.config);
return { name: this.name, data: result.content };
}
}
@@ -76,7 +76,7 @@ export class ListMemoryCommand implements Command {
context: CommandContext,
_: string[],
): Promise<CommandExecutionResponse> {
const result = listMemoryFiles(context.config);
const result = listMemoryFiles(context.agentContext.config);
return { name: this.name, data: result.content };
}
}
@@ -95,7 +95,7 @@ export class AddMemoryCommand implements Command {
return { name: this.name, data: result.content };
}
const toolRegistry = context.config.getToolRegistry();
const toolRegistry = context.agentContext.toolRegistry;
const tool = toolRegistry.getTool(result.toolName);
if (tool) {
const abortController = new AbortController();
@@ -105,9 +105,9 @@ export class AddMemoryCommand implements Command {
await tool.buildAndExecute(result.toolArgs, signal, undefined, {
sanitizationConfig: DEFAULT_SANITIZATION_CONFIG,
sandboxManager: context.config.sandboxManager,
sandboxManager: context.agentContext.config.sandboxManager,
});
await refreshMemory(context.config);
await refreshMemory(context.agentContext.config);
return {
name: this.name,
data: `Added memory: "${textToAdd}"`,
+3 -2
View File
@@ -29,7 +29,8 @@ export class RestoreCommand implements Command {
context: CommandContext,
args: string[],
): Promise<CommandExecutionResponse> {
const { config, git: gitService } = context;
const { agentContext: agentContext, git: gitService } = context;
const { config } = agentContext;
const argsStr = args.join(' ');
try {
@@ -116,7 +117,7 @@ export class ListCheckpointsCommand implements Command {
readonly description = 'Lists all available checkpoints.';
async execute(context: CommandContext): Promise<CommandExecutionResponse> {
const { config } = context;
const { config } = context.agentContext;
try {
if (!config.getCheckpointingEnabled()) {
+2 -2
View File
@@ -4,11 +4,11 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type { Config, GitService } from '@google/gemini-cli-core';
import type { AgentLoopContext, GitService } from '@google/gemini-cli-core';
import type { LoadedSettings } from '../../config/settings.js';
export interface CommandContext {
config: Config;
agentContext: AgentLoopContext;
settings: LoadedSettings;
git?: GitService;
sendMessage: (text: string) => Promise<void>;
@@ -97,7 +97,7 @@ describe('ExtensionManager theme loading', () => {
getMcpClientManager: () => ({
startExtension: vi.fn().mockResolvedValue(undefined),
}),
getGeminiClient: () => ({
geminiClient: () => ({
isInitialized: () => false,
updateSystemInstruction: vi.fn(),
setTools: vi.fn(),
@@ -129,7 +129,7 @@ describe('ExtensionManager theme loading', () => {
enableEnvironmentVariableRedaction: false,
},
}),
getToolRegistry: () => ({
toolRegistry: () => ({
getTools: () => [],
}),
getProxy: () => undefined,
@@ -208,7 +208,7 @@ describe('ExtensionManager theme loading', () => {
setGeminiMdFileCount: vi.fn(),
setGeminiMdFilePaths: vi.fn(),
getEnableExtensionReloading: () => true,
getGeminiClient: () => ({
geminiClient: () => ({
isInitialized: () => false,
updateSystemInstruction: vi.fn(),
setTools: vi.fn(),
+22 -7
View File
@@ -41,7 +41,7 @@ vi.mock('./theme.js', () => ({
describe('initializer', () => {
let mockConfig: {
getToolRegistry: ReturnType<typeof vi.fn>;
toolRegistry: ReturnType<typeof vi.fn>;
getIdeMode: ReturnType<typeof vi.fn>;
getGeminiMdFileCount: ReturnType<typeof vi.fn>;
};
@@ -53,7 +53,7 @@ describe('initializer', () => {
beforeEach(() => {
vi.clearAllMocks();
mockConfig = {
getToolRegistry: vi.fn(),
toolRegistry: vi.fn(),
getIdeMode: vi.fn().mockReturnValue(false),
getGeminiMdFileCount: vi.fn().mockReturnValue(5),
};
@@ -81,7 +81,10 @@ describe('initializer', () => {
it('should initialize correctly in non-IDE mode', async () => {
const result = await initializeApp(
mockConfig as unknown as Config,
{
config: mockConfig as unknown as Config,
toolRegistry: mockConfig.toolRegistry,
} as any,
mockSettings,
);
@@ -101,7 +104,10 @@ describe('initializer', () => {
it('should initialize correctly in IDE mode', async () => {
mockConfig.getIdeMode.mockReturnValue(true);
const result = await initializeApp(
mockConfig as unknown as Config,
{
config: mockConfig as unknown as Config,
toolRegistry: mockConfig.toolRegistry,
} as any,
mockSettings,
);
@@ -126,7 +132,10 @@ describe('initializer', () => {
accountSuspensionInfo: null,
});
const result = await initializeApp(
mockConfig as unknown as Config,
{
config: mockConfig as unknown as Config,
toolRegistry: mockConfig.toolRegistry,
} as any,
mockSettings,
);
@@ -137,7 +146,10 @@ describe('initializer', () => {
it('should handle undefined auth type', async () => {
mockSettings.merged.security.auth.selectedType = undefined;
const result = await initializeApp(
mockConfig as unknown as Config,
{
config: mockConfig as unknown as Config,
toolRegistry: mockConfig.toolRegistry,
} as any,
mockSettings,
);
@@ -147,7 +159,10 @@ describe('initializer', () => {
it('should handle theme error', async () => {
vi.mocked(validateTheme).mockReturnValue('Theme not found');
const result = await initializeApp(
mockConfig as unknown as Config,
{
config: mockConfig as unknown as Config,
toolRegistry: mockConfig.toolRegistry,
} as any,
mockSettings,
);
+4 -3
View File
@@ -9,10 +9,10 @@ import {
IdeConnectionEvent,
IdeConnectionType,
logIdeConnection,
type Config,
StartSessionEvent,
logCliConfiguration,
startupProfiler,
type AgentLoopContext,
} from '@google/gemini-cli-core';
import { type LoadedSettings } from '../config/settings.js';
import { performInitialAuth } from './auth.js';
@@ -35,9 +35,10 @@ export interface InitializationResult {
* @returns The results of the initialization.
*/
export async function initializeApp(
config: Config,
context: AgentLoopContext,
settings: LoadedSettings,
): Promise<InitializationResult> {
const config = context.config;
const authHandle = startupProfiler.start('authenticate');
const { authError, accountSuspensionInfo } = await performInitialAuth(
config,
@@ -51,7 +52,7 @@ export async function initializeApp(
logCliConfiguration(
config,
new StartSessionEvent(config, config.getToolRegistry()),
new StartSessionEvent(config, context.toolRegistry),
);
if (config.getIdeMode()) {
+4 -3
View File
@@ -448,7 +448,8 @@ export async function main() {
registerTelemetryConfig(config);
const policyEngine = config.getPolicyEngine();
const messageBus = config.getMessageBus();
const context = config.createAgentLoopContext();
const messageBus = context.messageBus;
createPolicyUpdater(policyEngine, messageBus, config.storage);
// Register SessionEnd hook to fire on graceful exit
@@ -517,7 +518,7 @@ export async function main() {
await setupTerminalAndTheme(config, settings);
const initAppHandle = startupProfiler.start('initialize_app');
const initializationResult = await initializeApp(config, settings);
const initializationResult = await initializeApp(context, settings);
initAppHandle?.end();
if (
@@ -673,7 +674,7 @@ export async function main() {
initializeOutputListenersAndFlush();
await runNonInteractive({
config,
context,
settings,
input,
prompt_id,
+2 -2
View File
@@ -206,7 +206,7 @@ describe('gemini.tsx main function cleanup', () => {
getSandbox: vi.fn(() => false),
getDebugMode: vi.fn(() => false),
getPolicyEngine: vi.fn(),
getMessageBus: () => ({ subscribe: vi.fn() }),
messageBus: () => ({ subscribe: vi.fn() }),
getEnableHooks: vi.fn(() => false),
getHookSystem: () => undefined,
initialize: vi.fn(),
@@ -222,7 +222,7 @@ describe('gemini.tsx main function cleanup', () => {
getListExtensions: vi.fn(() => false),
getListSessions: vi.fn(() => false),
getDeleteSession: vi.fn(() => undefined),
getToolRegistry: vi.fn(),
toolRegistry: vi.fn(),
getExtensions: vi.fn(() => []),
getModel: vi.fn(() => 'gemini-pro'),
getEmbeddingModel: vi.fn(() => 'embedding-001'),
+4 -3
View File
@@ -12,7 +12,7 @@ import { ConsolePatcher } from './ui/utils/ConsolePatcher.js';
import { registerCleanup, setupTtyCheck } from './utils/cleanup.js';
import {
type StartupWarning,
type Config,
type AgentLoopContext,
type ResumedSessionData,
coreEvents,
createWorkingStdio,
@@ -50,13 +50,14 @@ import { profiler } from './ui/components/DebugProfiler.js';
const SLOW_RENDER_MS = 200;
export async function startInteractiveUI(
config: Config,
context: AgentLoopContext,
settings: LoadedSettings,
startupWarnings: StartupWarning[],
workspaceRoot: string = process.cwd(),
resumedSessionData: ResumedSessionData | undefined,
initializationResult: InitializationResult,
) {
const config = context.config;
// Never enter Ink alternate buffer mode when screen reader mode is enabled
// as there is no benefit of alternate buffer mode when using a screen reader
// and the Ink alternate buffer mode requires line wrapping harmful to
@@ -119,7 +120,7 @@ export async function startInteractiveUI(
<SessionStatsProvider>
<VimModeProvider>
<AppContainer
config={config}
context={context}
startupWarnings={startupWarnings}
version={version}
resumedSessionData={resumedSessionData}
+50 -50
View File
@@ -167,13 +167,13 @@ describe('runNonInteractive', () => {
mockConfig = {
initialize: vi.fn().mockResolvedValue(undefined),
getMessageBus: vi.fn().mockReturnValue({
messageBus: {
subscribe: vi.fn(),
unsubscribe: vi.fn(),
publish: vi.fn(),
}),
getGeminiClient: vi.fn().mockReturnValue(mockGeminiClient),
getToolRegistry: vi.fn().mockReturnValue(mockToolRegistry),
} as any,
geminiClient: mockGeminiClient as any,
toolRegistry: mockToolRegistry as any,
getMaxSessionTurns: vi.fn().mockReturnValue(10),
getSessionId: vi.fn().mockReturnValue('test-session-id'),
getProjectRoot: vi.fn().mockReturnValue('/test/project'),
@@ -249,7 +249,7 @@ describe('runNonInteractive', () => {
);
await runNonInteractive({
config: mockConfig,
context: mockConfig.createAgentLoopContext() as any,
settings: mockSettings,
input: 'Test input',
prompt_id: 'prompt-id-1',
@@ -281,7 +281,7 @@ describe('runNonInteractive', () => {
);
await runNonInteractive({
config: mockConfig,
context: mockConfig.createAgentLoopContext() as any,
settings: mockSettings,
input: 'test',
prompt_id: 'prompt-id-activity-logger',
@@ -304,7 +304,7 @@ describe('runNonInteractive', () => {
);
await runNonInteractive({
config: mockConfig,
context: mockConfig.createAgentLoopContext() as any,
settings: mockSettings,
input: 'test',
prompt_id: 'prompt-id-activity-logger-off',
@@ -362,7 +362,7 @@ describe('runNonInteractive', () => {
.mockReturnValueOnce(createStreamFromEvents(secondCallEvents));
await runNonInteractive({
config: mockConfig,
context: mockConfig.createAgentLoopContext() as any,
settings: mockSettings,
input: 'Use a tool',
prompt_id: 'prompt-id-2',
@@ -442,7 +442,7 @@ describe('runNonInteractive', () => {
// 4. Run the command.
await runNonInteractive({
config: mockConfig,
context: mockConfig.createAgentLoopContext() as any,
settings: mockSettings,
input: 'Use mock tool multiple times',
prompt_id: 'prompt-id-multi',
@@ -513,7 +513,7 @@ describe('runNonInteractive', () => {
.mockReturnValueOnce(createStreamFromEvents(finalResponse));
await runNonInteractive({
config: mockConfig,
context: mockConfig.createAgentLoopContext() as any,
settings: mockSettings,
input: 'Trigger tool error',
prompt_id: 'prompt-id-3',
@@ -553,7 +553,7 @@ describe('runNonInteractive', () => {
await expect(
runNonInteractive({
config: mockConfig,
context: mockConfig.createAgentLoopContext() as any,
settings: mockSettings,
input: 'Initial fail',
prompt_id: 'prompt-id-4',
@@ -608,7 +608,7 @@ describe('runNonInteractive', () => {
.mockReturnValueOnce(createStreamFromEvents(finalResponse));
await runNonInteractive({
config: mockConfig,
context: mockConfig.createAgentLoopContext() as any,
settings: mockSettings,
input: 'Trigger tool not found',
prompt_id: 'prompt-id-5',
@@ -626,7 +626,7 @@ describe('runNonInteractive', () => {
vi.mocked(mockConfig.getMaxSessionTurns).mockReturnValue(0);
await expect(
runNonInteractive({
config: mockConfig,
context: mockConfig.createAgentLoopContext() as any,
settings: mockSettings,
input: 'Trigger loop',
prompt_id: 'prompt-id-6',
@@ -669,7 +669,7 @@ describe('runNonInteractive', () => {
// 4. Run the non-interactive mode with the raw input
await runNonInteractive({
config: mockConfig,
context: mockConfig.createAgentLoopContext() as any,
settings: mockSettings,
input: rawInput,
prompt_id: 'prompt-id-7',
@@ -706,7 +706,7 @@ describe('runNonInteractive', () => {
);
await runNonInteractive({
config: mockConfig,
context: mockConfig.createAgentLoopContext() as any,
settings: mockSettings,
input: 'Test input',
prompt_id: 'prompt-id-1',
@@ -796,7 +796,7 @@ describe('runNonInteractive', () => {
);
await runNonInteractive({
config: mockConfig,
context: mockConfig.createAgentLoopContext() as any,
settings: mockSettings,
input: 'Execute tool only',
prompt_id: 'prompt-id-tool-only',
@@ -839,7 +839,7 @@ describe('runNonInteractive', () => {
);
await runNonInteractive({
config: mockConfig,
context: mockConfig.createAgentLoopContext() as any,
settings: mockSettings,
input: 'Empty response test',
prompt_id: 'prompt-id-empty',
@@ -879,7 +879,7 @@ describe('runNonInteractive', () => {
let thrownError: Error | null = null;
try {
await runNonInteractive({
config: mockConfig,
context: mockConfig.createAgentLoopContext() as any,
settings: mockSettings,
input: 'Test input',
prompt_id: 'prompt-id-error',
@@ -921,7 +921,7 @@ describe('runNonInteractive', () => {
let thrownError: Error | null = null;
try {
await runNonInteractive({
config: mockConfig,
context: mockConfig.createAgentLoopContext() as any,
settings: mockSettings,
input: 'Invalid syntax',
prompt_id: 'prompt-id-fatal',
@@ -975,7 +975,7 @@ describe('runNonInteractive', () => {
);
await runNonInteractive({
config: mockConfig,
context: mockConfig.createAgentLoopContext() as any,
settings: mockSettings,
input: '/testcommand',
prompt_id: 'prompt-id-slash',
@@ -1016,7 +1016,7 @@ describe('runNonInteractive', () => {
);
await runNonInteractive({
config: mockConfig,
context: mockConfig.createAgentLoopContext() as any,
settings: mockSettings,
input: '/help',
prompt_id: 'prompt-id-slash',
@@ -1095,7 +1095,7 @@ describe('runNonInteractive', () => {
);
const runPromise = runNonInteractive({
config: mockConfig,
context: mockConfig.createAgentLoopContext() as any,
settings: mockSettings,
input: 'Long running query',
prompt_id: 'prompt-id-cancel',
@@ -1172,7 +1172,7 @@ describe('runNonInteractive', () => {
await expect(
runNonInteractive({
config: mockConfig,
context: mockConfig.createAgentLoopContext() as any,
settings: mockSettings,
input: '/confirm',
prompt_id: 'prompt-id-confirm',
@@ -1198,7 +1198,7 @@ describe('runNonInteractive', () => {
);
await runNonInteractive({
config: mockConfig,
context: mockConfig.createAgentLoopContext() as any,
settings: mockSettings,
input: '/unknowncommand',
prompt_id: 'prompt-id-unknown',
@@ -1229,7 +1229,7 @@ describe('runNonInteractive', () => {
await expect(
runNonInteractive({
config: mockConfig,
context: mockConfig.createAgentLoopContext() as any,
settings: mockSettings,
input: '/noaction',
prompt_id: 'prompt-id-unhandled',
@@ -1263,7 +1263,7 @@ describe('runNonInteractive', () => {
);
await runNonInteractive({
config: mockConfig,
context: mockConfig.createAgentLoopContext() as any,
settings: mockSettings,
input: '/testargs arg1 arg2',
prompt_id: 'prompt-id-args',
@@ -1296,7 +1296,7 @@ describe('runNonInteractive', () => {
);
await runNonInteractive({
config: mockConfig,
context: mockConfig.createAgentLoopContext() as any,
settings: mockSettings,
input: '/mycommand',
prompt_id: 'prompt-id-loaders',
@@ -1323,14 +1323,14 @@ describe('runNonInteractive', () => {
it('should allow a normally-excluded tool when --allowed-tools is set', async () => {
// By default, ShellTool is excluded in non-interactive mode.
// This test ensures that --allowed-tools overrides this exclusion.
vi.mocked(mockConfig.getToolRegistry).mockReturnValue({
(mockConfig as any).toolRegistry = {
getTool: vi.fn().mockReturnValue({
name: 'ShellTool',
description: 'A shell tool',
run: vi.fn(),
}),
getFunctionDeclarations: vi.fn().mockReturnValue([{ name: 'ShellTool' }]),
} as unknown as ToolRegistry);
} as unknown as ToolRegistry;
const toolCallEvent: ServerGeminiStreamEvent = {
type: GeminiEventType.ToolCallRequest,
@@ -1379,7 +1379,7 @@ describe('runNonInteractive', () => {
.mockReturnValueOnce(createStreamFromEvents(secondCallEvents));
await runNonInteractive({
config: mockConfig,
context: mockConfig.createAgentLoopContext() as any,
settings: mockSettings,
input: 'List the files',
prompt_id: 'prompt-id-allowed',
@@ -1405,7 +1405,7 @@ describe('runNonInteractive', () => {
);
await runNonInteractive({
config: mockConfig,
context: mockConfig.createAgentLoopContext() as any,
settings: mockSettings,
input: 'test',
prompt_id: 'prompt-id-events',
@@ -1430,7 +1430,7 @@ describe('runNonInteractive', () => {
);
await runNonInteractive({
config: mockConfig,
context: mockConfig.createAgentLoopContext() as any,
settings: mockSettings,
input: 'test',
prompt_id: 'prompt-id-events',
@@ -1454,7 +1454,7 @@ describe('runNonInteractive', () => {
);
await runNonInteractive({
config: mockConfig,
context: mockConfig.createAgentLoopContext() as any,
settings: mockSettings,
input: 'test',
prompt_id: 'prompt-id-events',
@@ -1491,7 +1491,7 @@ describe('runNonInteractive', () => {
);
await runNonInteractive({
config: mockConfig,
context: mockConfig.createAgentLoopContext() as any,
settings: mockSettings,
input: 'test',
prompt_id: 'prompt-id-events',
@@ -1576,7 +1576,7 @@ describe('runNonInteractive', () => {
.mockReturnValueOnce(createStreamFromEvents(secondCallEvents));
await runNonInteractive({
config: mockConfig,
context: mockConfig.createAgentLoopContext() as any,
settings: mockSettings,
input: 'Stream test',
prompt_id: 'prompt-id-stream',
@@ -1611,7 +1611,7 @@ describe('runNonInteractive', () => {
}
await runNonInteractive({
config: mockConfig,
context: mockConfig.createAgentLoopContext() as any,
settings: mockSettings,
input: 'EPIPE test',
prompt_id: 'prompt-id-epipe',
@@ -1652,7 +1652,7 @@ describe('runNonInteractive', () => {
};
await runNonInteractive({
config: mockConfig,
context: mockConfig.createAgentLoopContext() as any,
settings: mockSettings,
input: 'Continue',
prompt_id: 'prompt-id-resume',
@@ -1706,7 +1706,7 @@ describe('runNonInteractive', () => {
try {
await runNonInteractive({
config: mockConfig,
context: mockConfig.createAgentLoopContext() as any,
settings: mockSettings,
input,
prompt_id: promptId,
@@ -1790,7 +1790,7 @@ describe('runNonInteractive', () => {
.mockImplementation(() => {});
await runNonInteractive({
config: mockConfig,
context: mockConfig.createAgentLoopContext() as any,
settings: mockSettings,
input: 'Tool recording error test',
prompt_id: 'prompt-id-tool-error',
@@ -1846,7 +1846,7 @@ describe('runNonInteractive', () => {
.mockReturnValueOnce(createStreamFromEvents([]));
await runNonInteractive({
config: mockConfig,
context: mockConfig.createAgentLoopContext() as any,
settings: mockSettings,
input: 'Run stop tool',
prompt_id: 'prompt-id-stop',
@@ -1905,7 +1905,7 @@ describe('runNonInteractive', () => {
);
await runNonInteractive({
config: mockConfig,
context: mockConfig.createAgentLoopContext() as any,
settings: mockSettings,
input: 'Run stop tool',
prompt_id: 'prompt-id-stop-json',
@@ -1966,7 +1966,7 @@ describe('runNonInteractive', () => {
);
await runNonInteractive({
config: mockConfig,
context: mockConfig.createAgentLoopContext() as any,
settings: mockSettings,
input: 'Run stop tool',
prompt_id: 'prompt-id-stop-stream',
@@ -1990,7 +1990,7 @@ describe('runNonInteractive', () => {
);
await runNonInteractive({
config: mockConfig,
context: mockConfig.createAgentLoopContext() as any,
settings: mockSettings,
input: 'test stop',
prompt_id: 'prompt-id-stop',
@@ -2021,7 +2021,7 @@ describe('runNonInteractive', () => {
);
await runNonInteractive({
config: mockConfig,
context: mockConfig.createAgentLoopContext() as any,
settings: mockSettings,
input: 'test block',
prompt_id: 'prompt-id-block',
@@ -2060,7 +2060,7 @@ describe('runNonInteractive', () => {
vi.mocked(mockConfig.getRawOutput).mockReturnValue(false);
await runNonInteractive({
config: mockConfig,
context: mockConfig.createAgentLoopContext() as any,
settings: mockSettings,
input: 'Test input',
prompt_id: 'prompt-id-sanitization',
@@ -2087,7 +2087,7 @@ describe('runNonInteractive', () => {
vi.mocked(mockConfig.getAcceptRawOutputRisk).mockReturnValue(true);
await runNonInteractive({
config: mockConfig,
context: mockConfig.createAgentLoopContext() as any,
settings: mockSettings,
input: 'Test input',
prompt_id: 'prompt-id-raw',
@@ -2112,7 +2112,7 @@ describe('runNonInteractive', () => {
vi.mocked(mockConfig.getAcceptRawOutputRisk).mockReturnValue(true);
await runNonInteractive({
config: mockConfig,
context: mockConfig.createAgentLoopContext() as any,
settings: mockSettings,
input: 'Test input',
prompt_id: 'prompt-id-accept-only',
@@ -2136,7 +2136,7 @@ describe('runNonInteractive', () => {
vi.mocked(mockConfig.getAcceptRawOutputRisk).mockReturnValue(false);
await runNonInteractive({
config: mockConfig,
context: mockConfig.createAgentLoopContext() as any,
settings: mockSettings,
input: 'Test input',
prompt_id: 'prompt-id-warn',
@@ -2162,7 +2162,7 @@ describe('runNonInteractive', () => {
vi.mocked(mockConfig.getAcceptRawOutputRisk).mockReturnValue(true);
await runNonInteractive({
config: mockConfig,
context: mockConfig.createAgentLoopContext() as any,
settings: mockSettings,
input: 'Test input',
prompt_id: 'prompt-id-no-warn',
@@ -2220,7 +2220,7 @@ describe('runNonInteractive', () => {
);
await runNonInteractive({
config: mockConfig,
context: mockConfig.createAgentLoopContext() as any,
settings: mockSettings,
input: 'Test input',
prompt_id: 'prompt-id-cancel',
+9 -8
View File
@@ -5,7 +5,7 @@
*/
import type {
Config,
AgentLoopContext,
ToolCallRequestInfo,
ResumedSessionData,
UserFeedbackPayload,
@@ -48,7 +48,7 @@ import {
import { TextOutput } from './ui/utils/textOutput.js';
interface RunNonInteractiveParams {
config: Config;
context: AgentLoopContext;
settings: LoadedSettings;
input: string;
prompt_id: string;
@@ -56,12 +56,13 @@ interface RunNonInteractiveParams {
}
export async function runNonInteractive({
config,
context,
settings,
input,
prompt_id,
resumedSessionData,
}: RunNonInteractiveParams): Promise<void> {
const config = context.config;
return promptIdContext.run(prompt_id, async () => {
const consolePatcher = new ConsolePatcher({
stderr: true,
@@ -209,10 +210,10 @@ export async function runNonInteractive({
}
});
const geminiClient = config.getGeminiClient();
const geminiClient = context.geminiClient;
const scheduler = new Scheduler({
context: config,
messageBus: config.getMessageBus(),
context,
messageBus: context.messageBus,
getPreferredEditor: () => undefined,
schedulerId: ROOT_SCHEDULER_ID,
});
@@ -243,7 +244,7 @@ export async function runNonInteractive({
const slashCommandResult = await handleSlashCommand(
input,
abortController,
config,
context,
settings,
);
// If a slash command is found and returns a prompt, use it.
@@ -258,7 +259,7 @@ export async function runNonInteractive({
if (!query) {
const { processedQuery, error } = await handleAtCommand({
query: input,
config,
context: config,
addItem: (_item, _timestamp) => 0,
onDebugMessage: () => {},
messageId: Date.now(),
@@ -10,7 +10,7 @@ import {
FatalInputError,
Logger,
uiTelemetryService,
type Config,
type AgentLoopContext,
} from '@google/gemini-cli-core';
import { CommandService } from './services/CommandService.js';
import { BuiltinCommandLoader } from './services/BuiltinCommandLoader.js';
@@ -32,9 +32,10 @@ import type { SessionStatsState } from './ui/contexts/SessionContext.js';
export const handleSlashCommand = async (
rawQuery: string,
abortController: AbortController,
config: Config,
context: AgentLoopContext,
settings: LoadedSettings,
): Promise<PartListUnion | undefined> => {
const config = context.config;
const trimmed = rawQuery.trim();
if (!trimmed.startsWith('/')) {
return;
@@ -65,9 +66,9 @@ export const handleSlashCommand = async (
const logger = new Logger(config?.getSessionId() || '', config?.storage);
const context: CommandContext = {
const commandContext: CommandContext = {
services: {
config,
agentContext: context,
settings,
git: undefined,
logger,
@@ -84,7 +85,7 @@ export const handleSlashCommand = async (
},
};
const result = await commandToExecute.action(context, args);
const result = await commandToExecute.action(commandContext, args);
if (result) {
switch (result.type) {
@@ -35,7 +35,7 @@ describe('AtFileProcessor', () => {
context = createMockCommandContext({
services: {
config: mockConfig,
agentContext: mockConfig as any,
},
});
@@ -60,7 +60,7 @@ describe('AtFileProcessor', () => {
const prompt: PartUnion[] = [{ text: 'Analyze @{file.txt}' }];
const contextWithoutConfig = createMockCommandContext({
services: {
config: null,
agentContext: null as any,
},
});
const result = await processor.process(prompt, contextWithoutConfig);
@@ -25,7 +25,7 @@ export class AtFileProcessor implements IPromptProcessor {
input: PromptPipelineContent,
context: CommandContext,
): Promise<PromptPipelineContent> {
const config = context.services.config;
const config = context.services.agentContext?.config;
if (!config) {
return input;
}
@@ -98,7 +98,7 @@ describe('ShellProcessor', () => {
args: 'default args',
},
services: {
config: mockConfig as Config,
agentContext: mockConfig as any as Config,
},
session: {
sessionShellAllowlist: new Set(),
@@ -120,7 +120,7 @@ describe('ShellProcessor', () => {
const prompt: PromptPipelineContent = createPromptPipelineContent('!{ls}');
const contextWithoutConfig = createMockCommandContext({
services: {
config: null,
agentContext: null as any,
},
});
@@ -74,7 +74,7 @@ export class ShellProcessor implements IPromptProcessor {
];
}
const config = context.services.config;
const config = context.services.agentContext?.config || null;
if (!config) {
throw new Error(
`Security configuration not loaded. Cannot verify shell command permissions for '${this.commandName}'. Aborting.`,
+7 -6
View File
@@ -31,6 +31,7 @@ import {
debugLogger,
CoreToolCallStatus,
IntegrityDataStatus,
type AgentLoopContext,
} from '@google/gemini-cli-core';
import {
type MockShellCommand,
@@ -157,6 +158,7 @@ export interface PendingConfirmation {
export class AppRig {
private renderResult: ReturnType<typeof renderWithProviders> | undefined;
private config: Config | undefined;
context: AgentLoopContext | undefined;
private settings: LoadedSettings | undefined;
private testDir: string;
private sessionId: string;
@@ -312,7 +314,7 @@ export class AppRig {
private setupMessageBusListeners() {
if (!this.config) return;
const messageBus = this.config.getMessageBus();
const messageBus = this.context!.messageBus;
messageBus.subscribe(
MessageBusType.TOOL_CALLS_UPDATE,
@@ -396,7 +398,7 @@ export class AppRig {
act(() => {
this.renderResult = renderWithProviders(
<AppContainer
config={this.config!}
context={this.config as any}
version="test-version"
initializationResult={{
authError: null,
@@ -578,7 +580,7 @@ export class AppRig {
outcome: ToolConfirmationOutcome = ToolConfirmationOutcome.ProceedOnce,
): Promise<void> {
if (!this.config) throw new Error('AppRig not initialized');
const messageBus = this.config.getMessageBus();
const messageBus = this.context!.messageBus;
let pending: PendingConfirmation;
if (
@@ -725,9 +727,8 @@ export class AppRig {
// Poison the chat recording service to prevent late writes to the test directory
if (this.config) {
const recordingService = this.config
.getGeminiClient()
?.getChatRecordingService();
const recordingService =
this.context!.geminiClient?.getChatRecordingService();
if (recordingService) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-type-assertion
(recordingService as any).conversationFile = null;
@@ -46,15 +46,19 @@ describe('createMockCommandContext', () => {
const overrides = {
services: {
config: mockConfig,
agentContext: mockConfig as any,
},
};
const context = createMockCommandContext(overrides);
expect(context.services.config).toBeDefined();
expect(context.services.config?.getModel()).toBe('gemini-pro');
expect(context.services.config?.getProjectRoot()).toBe('/test/project');
expect(context.services.agentContext?.config).toBeDefined();
expect(context.services.agentContext?.config?.getModel()).toBe(
'gemini-pro',
);
expect(context.services.agentContext?.config?.getProjectRoot()).toBe(
'/test/project',
);
// Verify a default property on the same nested object is still there
expect(context.services.logger).toBeDefined();
@@ -36,7 +36,7 @@ export const createMockCommandContext = (
args: '',
},
services: {
config: null,
agentContext: null,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
settings: {
merged: defaultMergedSettings,
+2 -3
View File
@@ -34,7 +34,6 @@ export const createMockConfig = (overrides: Partial<Config> = {}): Config =>
getRemoteAdminSettings: vi.fn(() => undefined),
initialize: vi.fn().mockResolvedValue(undefined),
getPolicyEngine: vi.fn(() => ({})),
getMessageBus: vi.fn(() => ({ subscribe: vi.fn() })),
getHookSystem: vi.fn(() => ({
fireSessionEndEvent: vi.fn().mockResolvedValue(undefined),
fireSessionStartEvent: vi.fn().mockResolvedValue(undefined),
@@ -66,7 +65,7 @@ export const createMockConfig = (overrides: Partial<Config> = {}): Config =>
clientVersion: '1.0.0',
getModel: vi.fn().mockReturnValue('gemini-pro'),
getWorkingDir: vi.fn().mockReturnValue('/mock/cwd'),
getToolRegistry: vi.fn().mockReturnValue({
toolRegistry: vi.fn().mockReturnValue({
getTools: vi.fn().mockReturnValue([]),
getAllTools: vi.fn().mockReturnValue([]),
}),
@@ -90,7 +89,7 @@ export const createMockConfig = (overrides: Partial<Config> = {}): Config =>
getTelemetryOutfile: vi.fn().mockReturnValue(undefined),
getTelemetryUseCollector: vi.fn().mockReturnValue(false),
getTelemetryUseCliAuth: vi.fn().mockReturnValue(false),
getGeminiClient: vi.fn().mockReturnValue({
geminiClient: vi.fn().mockReturnValue({
isInitialized: vi.fn().mockReturnValue(true),
}),
updateSystemInstructionIfInitialized: vi.fn().mockResolvedValue(undefined),
+1 -1
View File
@@ -782,7 +782,7 @@ export const renderWithProviders = (
<UIActionsContext.Provider value={finalUIActions}>
<OverflowProvider>
<ToolActionsProvider
config={finalConfig}
context={finalConfig.createAgentLoopContext()}
toolCalls={allToolCalls}
>
<AskUserActionsProvider
+16 -22
View File
@@ -269,7 +269,7 @@ describe('AppContainer State Management', () => {
<KeypressProvider config={config}>
<OverflowProvider>
<AppContainer
config={config}
context={config.createAgentLoopContext()}
version={version}
initializationResult={initResult}
startupWarnings={startupWarnings}
@@ -1179,9 +1179,8 @@ describe('AppContainer State Management', () => {
};
const configWithRecording = makeFakeConfig();
vi.spyOn(configWithRecording, 'getGeminiClient').mockReturnValue(
mockGeminiClient as unknown as ReturnType<Config['getGeminiClient']>,
);
(configWithRecording as any).geminiClient =
mockGeminiClient as unknown as Config['geminiClient'];
expect(() => {
renderAppContainer({
@@ -1213,9 +1212,8 @@ describe('AppContainer State Management', () => {
};
const configWithRecording = makeFakeConfig();
vi.spyOn(configWithRecording, 'getGeminiClient').mockReturnValue(
mockGeminiClient as unknown as ReturnType<Config['getGeminiClient']>,
);
(configWithRecording as any).geminiClient =
mockGeminiClient as unknown as Config['geminiClient'];
vi.spyOn(configWithRecording, 'getSessionId').mockReturnValue(
'test-session-123',
);
@@ -1230,7 +1228,7 @@ describe('AppContainer State Management', () => {
}).not.toThrow();
// Verify the recording service structure is correct
expect(configWithRecording.getGeminiClient).toBeDefined();
expect(configWithRecording.geminiClient).toBeDefined();
expect(mockGeminiClient.getChatRecordingService).toBeDefined();
expect(mockChatRecordingService.initialize).toBeDefined();
expect(mockChatRecordingService.recordMessage).toBeDefined();
@@ -1255,9 +1253,8 @@ describe('AppContainer State Management', () => {
};
const configWithRecording = makeFakeConfig();
vi.spyOn(configWithRecording, 'getGeminiClient').mockReturnValue(
mockGeminiClient as unknown as ReturnType<Config['getGeminiClient']>,
);
(configWithRecording as any).geminiClient =
mockGeminiClient as unknown as Config['geminiClient'];
renderAppContainer({
config: configWithRecording,
@@ -1289,9 +1286,8 @@ describe('AppContainer State Management', () => {
};
const configWithClient = makeFakeConfig();
vi.spyOn(configWithClient, 'getGeminiClient').mockReturnValue(
mockGeminiClient as unknown as ReturnType<Config['getGeminiClient']>,
);
(configWithClient as any).geminiClient =
mockGeminiClient as unknown as Config['geminiClient'];
const resumedData = {
conversation: {
@@ -1345,9 +1341,8 @@ describe('AppContainer State Management', () => {
};
const configWithClient = makeFakeConfig();
vi.spyOn(configWithClient, 'getGeminiClient').mockReturnValue(
mockGeminiClient as unknown as ReturnType<Config['getGeminiClient']>,
);
(configWithClient as any).geminiClient =
mockGeminiClient as unknown as Config['geminiClient'];
const resumedData = {
conversation: {
@@ -1398,9 +1393,8 @@ describe('AppContainer State Management', () => {
};
const configWithRecording = makeFakeConfig();
vi.spyOn(configWithRecording, 'getGeminiClient').mockReturnValue(
mockGeminiClient as unknown as ReturnType<Config['getGeminiClient']>,
);
(configWithRecording as any).geminiClient =
mockGeminiClient as unknown as Config['geminiClient'];
renderAppContainer({
config: configWithRecording,
@@ -2686,10 +2680,10 @@ describe('AppContainer State Management', () => {
const getTree = (settings: LoadedSettings) => (
<SettingsContext.Provider value={settings}>
<KeypressProvider config={mockConfig}>
<KeypressProvider config={mockConfig as any}>
<OverflowProvider>
<AppContainer
config={mockConfig}
context={(mockConfig as any).createAgentLoopContext()}
version="1.0.0"
initializationResult={mockInitResult}
/>
+12 -11
View File
@@ -43,7 +43,6 @@ import { ToolActionsProvider } from './contexts/ToolActionsContext.js';
import {
type StartupWarning,
type EditorType,
type Config,
type IdeInfo,
type IdeContext,
type UserTierId,
@@ -85,6 +84,7 @@ import {
buildUserSteeringHintPrompt,
logBillingEvent,
ApiKeyUpdatedEvent,
type AgentLoopContext,
} from '@google/gemini-cli-core';
import { validateAuthMethod } from '../config/auth.js';
import process from 'node:process';
@@ -194,7 +194,7 @@ function isToolAwaitingConfirmation(
}
interface AppContainerProps {
config: Config;
context: AgentLoopContext;
startupWarnings?: StartupWarning[];
version: string;
initializationResult: InitializationResult;
@@ -223,13 +223,14 @@ const SHELL_HEIGHT_PADDING = 10;
export const AppContainer = (props: AppContainerProps) => {
const isHelpDismissKey = useIsHelpDismissKey();
const keyMatchers = useKeyMatchers();
const { config, initializationResult, resumedSessionData } = props;
const { context, initializationResult, resumedSessionData } = props;
const config = context.config;
const settings = useSettings();
const { reset } = useOverflowActions()!;
const notificationsEnabled = isNotificationsEnabled(settings);
const historyManager = useHistory({
chatRecordingService: config.getGeminiClient()?.getChatRecordingService(),
chatRecordingService: context.geminiClient?.getChatRecordingService(),
});
useMemoryMonitor(historyManager);
@@ -453,7 +454,7 @@ export const AppContainer = (props: AppContainerProps) => {
}
const additionalContext = result.getAdditionalContext();
const geminiClient = config.getGeminiClient();
const geminiClient = context.geminiClient;
if (additionalContext && geminiClient) {
await geminiClient.addHistory({
role: 'user',
@@ -724,10 +725,10 @@ export const AppContainer = (props: AppContainerProps) => {
const isAuthenticating = authState === AuthState.Unauthenticated;
// Session browser and resume functionality
const isGeminiClientInitialized = config.getGeminiClient()?.isInitialized();
const isGeminiClientInitialized = context.geminiClient?.isInitialized();
const { loadHistoryForResume, isResuming } = useSessionResume({
config,
context,
historyManager,
refreshStatic,
isGeminiClientInitialized,
@@ -1119,10 +1120,10 @@ Logging in with Google... Restarting Gemini CLI to continue.
dismissBackgroundShell,
retryStatus,
} = useGeminiStream(
config.getGeminiClient(),
context.geminiClient,
historyManager.history,
historyManager.addItem,
config,
context,
settings,
setDebugMessage,
handleSlashCommand,
@@ -1442,7 +1443,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
// Initial prompt handling
const initialPrompt = useMemo(() => config.getQuestion(), [config]);
const initialPromptSubmitted = useRef(false);
const geminiClient = config.getGeminiClient();
const geminiClient = context.geminiClient;
useEffect(() => {
if (
@@ -2610,7 +2611,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
startupWarnings: props.startupWarnings || [],
}}
>
<ToolActionsProvider config={config} toolCalls={allToolCalls}>
<ToolActionsProvider context={context} toolCalls={allToolCalls}>
<ShellFocusContext.Provider value={isFocused}>
<App key={`app-${forceRerenderKey}`} />
</ShellFocusContext.Provider>
@@ -36,10 +36,12 @@ describe('aboutCommand', () => {
beforeEach(() => {
mockContext = createMockCommandContext({
services: {
config: {
getModel: vi.fn(),
getIdeMode: vi.fn().mockReturnValue(true),
getUserTierName: vi.fn().mockReturnValue(undefined),
context: {
config: {
getModel: vi.fn(),
getIdeMode: vi.fn().mockReturnValue(true),
getUserTierName: vi.fn().mockReturnValue(undefined),
},
},
settings: {
merged: {
@@ -57,9 +59,10 @@ describe('aboutCommand', () => {
} as unknown as CommandContext);
vi.mocked(getVersion).mockResolvedValue('test-version');
vi.spyOn(mockContext.services.config!, 'getModel').mockReturnValue(
'test-model',
);
vi.spyOn(
mockContext.services.agentContext!.config!,
'getModel',
).mockReturnValue('test-model');
process.env['GOOGLE_CLOUD_PROJECT'] = 'test-gcp-project';
Object.defineProperty(process, 'platform', {
value: 'test-os',
@@ -160,9 +163,9 @@ describe('aboutCommand', () => {
});
it('should display the tier when getUserTierName returns a value', async () => {
vi.mocked(mockContext.services.config!.getUserTierName).mockReturnValue(
'Enterprise Tier',
);
vi.mocked(
mockContext.services.agentContext!.config.getUserTierName,
).mockReturnValue('Enterprise Tier');
if (!aboutCommand.action) {
throw new Error('The about command must have an action.');
}
+4 -3
View File
@@ -34,7 +34,8 @@ export const aboutCommand: SlashCommand = {
process.env['SEATBELT_PROFILE'] || 'unknown'
})`;
}
const modelVersion = context.services.config?.getModel() || 'Unknown';
const modelVersion =
context.services.agentContext?.config?.getModel() || 'Unknown';
const cliVersion = await getVersion();
const selectedAuthType =
context.services.settings.merged.security.auth.selectedType || '';
@@ -48,7 +49,7 @@ export const aboutCommand: SlashCommand = {
});
const userEmail = cachedAccount ?? undefined;
const tier = context.services.config?.getUserTierName();
const tier = context.services.agentContext?.config?.getUserTierName();
const aboutItem: Omit<HistoryItemAbout, 'id'> = {
type: MessageType.ABOUT,
@@ -68,7 +69,7 @@ export const aboutCommand: SlashCommand = {
};
async function getIdeClientName(context: CommandContext) {
if (!context.services.config?.getIdeMode()) {
if (!context.services.agentContext?.config?.getIdeMode()) {
return '';
}
const ideClient = await IdeClient.getInstance();
@@ -41,7 +41,7 @@ describe('agentsCommand', () => {
mockContext = createMockCommandContext({
services: {
config: mockConfig as unknown as Config,
agentContext: mockConfig as any as unknown as Config,
settings: {
workspace: { path: '/mock/path' },
merged: { agents: { overrides: {} } },
@@ -53,7 +53,7 @@ describe('agentsCommand', () => {
it('should show an error if config is not available', async () => {
const contextWithoutConfig = createMockCommandContext({
services: {
config: null,
agentContext: null as any,
},
});
@@ -226,7 +226,7 @@ describe('agentsCommand', () => {
it('should show an error if config is not available for enable', async () => {
const contextWithoutConfig = createMockCommandContext({
services: { config: null },
services: { agentContext: null },
});
const enableCommand = agentsCommand.subCommands?.find(
(cmd) => cmd.name === 'enable',
@@ -332,7 +332,7 @@ describe('agentsCommand', () => {
it('should show an error if config is not available for disable', async () => {
const contextWithoutConfig = createMockCommandContext({
services: { config: null },
services: { agentContext: null },
});
const disableCommand = agentsCommand.subCommands?.find(
(cmd) => cmd.name === 'disable',
@@ -433,7 +433,7 @@ describe('agentsCommand', () => {
it('should show an error if config is not available', async () => {
const contextWithoutConfig = createMockCommandContext({
services: { config: null },
services: { agentContext: null as any },
});
const configCommand = agentsCommand.subCommands?.find(
(cmd) => cmd.name === 'config',
+11 -8
View File
@@ -21,7 +21,7 @@ const agentsListCommand: SlashCommand = {
kind: CommandKind.BUILT_IN,
autoExecute: true,
action: async (context: CommandContext) => {
const { config } = context.services;
const config = context.services.agentContext?.config;
if (!config) {
return {
type: 'message',
@@ -61,7 +61,8 @@ async function enableAction(
context: CommandContext,
args: string,
): Promise<SlashCommandActionReturn | void> {
const { config, settings } = context.services;
const config = context.services.agentContext?.config;
const { settings } = context.services;
if (!config) {
return {
type: 'message',
@@ -137,7 +138,8 @@ async function disableAction(
context: CommandContext,
args: string,
): Promise<SlashCommandActionReturn | void> {
const { config, settings } = context.services;
const config = context.services.agentContext?.config;
const { settings } = context.services;
if (!config) {
return {
type: 'message',
@@ -216,7 +218,7 @@ async function configAction(
context: CommandContext,
args: string,
): Promise<SlashCommandActionReturn | void> {
const { config } = context.services;
const config = context.services.agentContext?.config;
if (!config) {
return {
type: 'message',
@@ -266,7 +268,8 @@ async function configAction(
}
function completeAgentsToEnable(context: CommandContext, partialArg: string) {
const { config, settings } = context.services;
const config = context.services.agentContext?.config;
const { settings } = context.services;
if (!config) return [];
const overrides = settings.merged.agents.overrides;
@@ -278,7 +281,7 @@ function completeAgentsToEnable(context: CommandContext, partialArg: string) {
}
function completeAgentsToDisable(context: CommandContext, partialArg: string) {
const { config } = context.services;
const config = context.services.agentContext?.config;
if (!config) return [];
const agentRegistry = config.getAgentRegistry();
@@ -287,7 +290,7 @@ function completeAgentsToDisable(context: CommandContext, partialArg: string) {
}
function completeAllAgents(context: CommandContext, partialArg: string) {
const { config } = context.services;
const config = context.services.agentContext?.config;
if (!config) return [];
const agentRegistry = config.getAgentRegistry();
@@ -328,7 +331,7 @@ const agentsReloadCommand: SlashCommand = {
description: 'Reload the agent registry',
kind: CommandKind.BUILT_IN,
action: async (context: CommandContext) => {
const { config } = context.services;
const config = context.services.agentContext?.config;
const agentRegistry = config?.getAgentRegistry();
if (!agentRegistry) {
return {
@@ -24,8 +24,8 @@ describe('authCommand', () => {
beforeEach(() => {
mockContext = createMockCommandContext({
services: {
config: {
getGeminiClient: vi.fn(),
agentContext: {
geminiClient: {} as any,
},
},
});
@@ -101,12 +101,13 @@ describe('authCommand', () => {
const mockStripThoughts = vi.fn();
const mockClient = {
stripThoughtsFromHistory: mockStripThoughts,
} as unknown as ReturnType<
NonNullable<typeof mockContext.services.config>['getGeminiClient']
>;
} as unknown as NonNullable<
NonNullable<typeof mockContext.services.agentContext>['config']
>['geminiClient'];
if (mockContext.services.config) {
mockContext.services.config.getGeminiClient = vi.fn(() => mockClient);
if (mockContext.services.agentContext?.config) {
(mockContext.services.agentContext?.config as any).geminiClient =
mockClient as any;
}
await logoutCommand!.action!(mockContext, '');
@@ -123,7 +124,7 @@ describe('authCommand', () => {
it('should handle missing config gracefully', async () => {
const logoutCommand = authCommand.subCommands?.[1];
mockContext.services.config = null;
mockContext.services.agentContext = null;
const result = await logoutCommand!.action!(mockContext, '');
+1 -1
View File
@@ -39,7 +39,7 @@ const authLogoutCommand: SlashCommand = {
undefined,
);
// Strip thoughts from history instead of clearing completely
context.services.config?.getGeminiClient()?.stripThoughtsFromHistory();
context.services.agentContext?.geminiClient?.stripThoughtsFromHistory();
// Return logout action to signal explicit state change
return {
type: 'logout',
+38 -32
View File
@@ -83,16 +83,18 @@ describe('bugCommand', () => {
it('should generate the default GitHub issue URL', async () => {
const mockContext = createMockCommandContext({
services: {
config: {
getModel: () => 'gemini-pro',
getBugCommand: () => undefined,
getIdeMode: () => true,
getGeminiClient: () => ({
getChat: () => ({
getHistory: () => [],
}),
}),
getContentGeneratorConfig: () => ({ authType: 'oauth-personal' }),
agentContext: {
config: {
getModel: () => 'gemini-pro',
getBugCommand: () => undefined,
getIdeMode: () => true,
geminiClient: {
getChat: () => ({
getHistory: () => [],
}),
} as any,
getContentGeneratorConfig: () => ({ authType: 'oauth-personal' }),
},
},
},
});
@@ -126,18 +128,20 @@ describe('bugCommand', () => {
];
const mockContext = createMockCommandContext({
services: {
config: {
getModel: () => 'gemini-pro',
getBugCommand: () => undefined,
getIdeMode: () => true,
getGeminiClient: () => ({
getChat: () => ({
getHistory: () => history,
}),
}),
getContentGeneratorConfig: () => ({ authType: 'vertex-ai' }),
storage: {
getProjectTempDir: () => '/tmp/gemini',
agentContext: {
config: {
getModel: () => 'gemini-pro',
getBugCommand: () => undefined,
getIdeMode: () => true,
geminiClient: {
getChat: () => ({
getHistory: () => history,
}),
} as any,
getContentGeneratorConfig: () => ({ authType: 'vertex-ai' }),
storage: {
getProjectTempDir: () => '/tmp/gemini',
},
},
},
},
@@ -172,16 +176,18 @@ describe('bugCommand', () => {
'https://internal.bug-tracker.com/new?desc={title}&details={info}';
const mockContext = createMockCommandContext({
services: {
config: {
getModel: () => 'gemini-pro',
getBugCommand: () => ({ urlTemplate: customTemplate }),
getIdeMode: () => true,
getGeminiClient: () => ({
getChat: () => ({
getHistory: () => [],
}),
}),
getContentGeneratorConfig: () => ({ authType: 'vertex-ai' }),
agentContext: {
config: {
getModel: () => 'gemini-pro',
getBugCommand: () => ({ urlTemplate: customTemplate }),
getIdeMode: () => true,
geminiClient: {
getChat: () => ({
getHistory: () => [],
}),
} as any,
getContentGeneratorConfig: () => ({ authType: 'vertex-ai' }),
},
},
},
});
+3 -3
View File
@@ -32,7 +32,7 @@ export const bugCommand: SlashCommand = {
autoExecute: false,
action: async (context: CommandContext, args?: string): Promise<void> => {
const bugDescription = (args || '').trim();
const { config } = context.services;
const config = context.services.agentContext?.config;
const osVersion = `${process.platform} ${process.version}`;
let sandboxEnv = 'no sandbox';
@@ -73,7 +73,7 @@ export const bugCommand: SlashCommand = {
info += `* **IDE Client:** ${ideClient}\n`;
}
const chat = config?.getGeminiClient()?.getChat();
const chat = context.services.agentContext?.geminiClient?.getChat();
const history = chat?.getHistory() || [];
let historyFileMessage = '';
let problemValue = bugDescription;
@@ -134,7 +134,7 @@ export const bugCommand: SlashCommand = {
};
async function getIdeClientName(context: CommandContext) {
if (!context.services.config?.getIdeMode()) {
if (!context.services.agentContext?.config?.getIdeMode()) {
return '';
}
const ideClient = await IdeClient.getInstance();
@@ -70,18 +70,19 @@ describe('chatCommand', () => {
mockContext = createMockCommandContext({
services: {
config: {
getProjectRoot: () => '/project/root',
getGeminiClient: () =>
({
agentContext: {
config: {
getProjectRoot: () => '/project/root',
geminiClient: {
getChat: mockGetChat,
}) as unknown as GeminiClient,
storage: {
getProjectTempDir: () => '/project/root/.gemini/tmp/mockhash',
} as unknown as GeminiClient,
storage: {
getProjectTempDir: () => '/project/root/.gemini/tmp/mockhash',
},
getContentGeneratorConfig: () => ({
authType: AuthType.LOGIN_WITH_GOOGLE,
}),
},
getContentGeneratorConfig: () => ({
authType: AuthType.LOGIN_WITH_GOOGLE,
}),
},
logger: {
saveCheckpoint: mockSaveCheckpoint,
@@ -698,8 +699,8 @@ Hi there!`;
beforeEach(() => {
mockGetLatestApiRequest = vi.fn();
mockContext.services.config!.getLatestApiRequest =
mockGetLatestApiRequest;
const config = mockContext.services.agentContext!.config;
config.getLatestApiRequest = mockGetLatestApiRequest;
vi.spyOn(process, 'cwd').mockReturnValue('/project/root');
vi.spyOn(Date, 'now').mockReturnValue(1234567890);
mockFs.writeFile.mockClear();
+8 -6
View File
@@ -35,7 +35,7 @@ const getSavedChatTags = async (
context: CommandContext,
mtSortDesc: boolean,
): Promise<ChatDetail[]> => {
const cfg = context.services.config;
const cfg = context.services.agentContext?.config;
const geminiDir = cfg?.storage?.getProjectTempDir();
if (!geminiDir) {
return [];
@@ -103,7 +103,8 @@ const saveCommand: SlashCommand = {
};
}
const { logger, config } = context.services;
const { logger } = context.services;
const config = context.services.agentContext?.config;
await logger.initialize();
if (!context.overwriteConfirmed) {
@@ -125,7 +126,7 @@ const saveCommand: SlashCommand = {
}
}
const chat = config?.getGeminiClient()?.getChat();
const chat = context.services.agentContext?.geminiClient?.getChat();
if (!chat) {
return {
type: 'message',
@@ -172,7 +173,8 @@ const resumeCheckpointCommand: SlashCommand = {
};
}
const { logger, config } = context.services;
const { logger } = context.services;
const config = context.services.agentContext?.config;
await logger.initialize();
const checkpoint = await logger.loadCheckpoint(tag);
const conversation = checkpoint.history;
@@ -298,7 +300,7 @@ const shareCommand: SlashCommand = {
};
}
const chat = context.services.config?.getGeminiClient()?.getChat();
const chat = context.services.agentContext?.geminiClient?.getChat();
if (!chat) {
return {
type: 'message',
@@ -344,7 +346,7 @@ export const debugCommand: SlashCommand = {
kind: CommandKind.BUILT_IN,
autoExecute: true,
action: async (context): Promise<MessageActionReturn> => {
const req = context.services.config?.getLatestApiRequest();
const req = context.services.agentContext?.config?.getLatestApiRequest();
if (!req) {
return {
type: 'message',
@@ -36,23 +36,24 @@ describe('clearCommand', () => {
mockContext = createMockCommandContext({
services: {
config: {
getGeminiClient: () =>
({
resetChat: mockResetChat,
getChat: () => ({
getChatRecordingService: mockGetChatRecordingService,
}),
}) as unknown as GeminiClient,
setSessionId: vi.fn(),
getEnableHooks: vi.fn().mockReturnValue(false),
getMessageBus: vi.fn().mockReturnValue(undefined),
getHookSystem: vi.fn().mockReturnValue({
fireSessionEndEvent: vi.fn().mockResolvedValue(undefined),
fireSessionStartEvent: vi.fn().mockResolvedValue(undefined),
}),
userHintService: {
clear: mockHintClear,
agentContext: {
geminiClient: {
resetChat: mockResetChat,
getChat: () => ({
getChatRecordingService: mockGetChatRecordingService,
}),
} as unknown as GeminiClient,
config: {
setSessionId: vi.fn(),
getEnableHooks: vi.fn().mockReturnValue(false),
messageBus: undefined as any,
getHookSystem: vi.fn().mockReturnValue({
fireSessionEndEvent: vi.fn().mockResolvedValue(undefined),
fireSessionStartEvent: vi.fn().mockResolvedValue(undefined),
}),
userHintService: {
clear: mockHintClear,
},
},
},
},
@@ -98,7 +99,7 @@ describe('clearCommand', () => {
const nullConfigContext = createMockCommandContext({
services: {
config: null,
agentContext: null as any,
},
});
+2 -2
View File
@@ -20,8 +20,8 @@ export const clearCommand: SlashCommand = {
kind: CommandKind.BUILT_IN,
autoExecute: true,
action: async (context, _args) => {
const geminiClient = context.services.config?.getGeminiClient();
const config = context.services.config;
const geminiClient = context.services.agentContext?.geminiClient;
const config = context.services.agentContext?.config;
// Fire SessionEnd hook before clearing
const hookSystem = config?.getHookSystem();
@@ -22,11 +22,10 @@ describe('compressCommand', () => {
mockTryCompressChat = vi.fn();
context = createMockCommandContext({
services: {
config: {
getGeminiClient: () =>
({
tryCompressChat: mockTryCompressChat,
}) as unknown as GeminiClient,
agentContext: {
geminiClient: {
tryCompressChat: mockTryCompressChat,
} as unknown as GeminiClient,
},
},
});
@@ -39,9 +39,11 @@ export const compressCommand: SlashCommand = {
try {
ui.setPendingItem(pendingMessage);
const promptId = `compress-${Date.now()}`;
const compressed = await context.services.config
?.getGeminiClient()
?.tryCompressChat(promptId, true);
const compressed =
await context.services.agentContext?.geminiClient?.tryCompressChat(
promptId,
true,
);
if (compressed) {
ui.addItem(
{
@@ -29,10 +29,10 @@ describe('copyCommand', () => {
mockContext = createMockCommandContext({
services: {
config: {
getGeminiClient: () => ({
agentContext: {
geminiClient: {
getChat: mockGetChat,
}),
} as any,
},
},
});
@@ -301,7 +301,7 @@ describe('copyCommand', () => {
if (!copyCommand.action) throw new Error('Command has no action');
const nullConfigContext = createMockCommandContext({
services: { config: null },
services: { agentContext: null as any },
});
const result = await copyCommand.action(nullConfigContext, '');
+1 -1
View File
@@ -18,7 +18,7 @@ export const copyCommand: SlashCommand = {
kind: CommandKind.BUILT_IN,
autoExecute: true,
action: async (context, _args): Promise<SlashCommandActionReturn | void> => {
const chat = context.services.config?.getGeminiClient()?.getChat();
const chat = context.services.agentContext?.geminiClient?.getChat();
const history = chat?.getHistory();
// Get the last message from the AI (model role)
@@ -72,7 +72,7 @@ describe('directoryCommand', () => {
mockConfig = {
getWorkspaceContext: () => mockWorkspaceContext,
isRestrictiveSandbox: vi.fn().mockReturnValue(false),
getGeminiClient: vi.fn().mockReturnValue({
geminiClient: vi.fn().mockReturnValue({
addDirectoryContext: vi.fn(),
getChatRecordingService: vi.fn().mockReturnValue({
recordDirectories: vi.fn(),
@@ -89,7 +89,7 @@ describe('directoryCommand', () => {
mockContext = {
services: {
config: mockConfig,
context: mockConfig as any,
settings: {
merged: {
memoryDiscoveryMaxDirs: 1000,
@@ -17,7 +17,7 @@ import {
import { MessageType, type HistoryItem } from '../types.js';
import {
refreshServerHierarchicalMemory,
type Config,
type AgentLoopContext,
} from '@google/gemini-cli-core';
import {
expandHomeDir,
@@ -28,7 +28,7 @@ import * as path from 'node:path';
import * as fs from 'node:fs';
async function finishAddingDirectories(
config: Config,
context: AgentLoopContext,
addItem: (
itemData: Omit<HistoryItem, 'id'>,
baseTimestamp?: number,
@@ -36,7 +36,7 @@ async function finishAddingDirectories(
added: string[],
errors: string[],
) {
if (!config) {
if (!context) {
addItem({
type: MessageType.ERROR,
text: 'Configuration is not available.',
@@ -46,8 +46,8 @@ async function finishAddingDirectories(
if (added.length > 0) {
try {
if (config.shouldLoadMemoryFromIncludeDirectories()) {
await refreshServerHierarchicalMemory(config);
if (context.config.shouldLoadMemoryFromIncludeDirectories()) {
await refreshServerHierarchicalMemory(context.config);
}
addItem({
type: MessageType.INFO,
@@ -60,13 +60,13 @@ async function finishAddingDirectories(
}
if (added.length > 0) {
const gemini = config.getGeminiClient();
const gemini = context.geminiClient;
if (gemini) {
await gemini.addDirectoryContext();
// Persist directories to session file for resume support
const chatRecordingService = gemini.getChatRecordingService();
const workspaceContext = config.getWorkspaceContext();
const workspaceContext = context.config.getWorkspaceContext();
chatRecordingService?.recordDirectories(
workspaceContext.getDirectories(),
);
@@ -110,9 +110,9 @@ export const directoryCommand: SlashCommand = {
// Filter out existing directories
let filteredSuggestions = suggestions;
if (context.services.config) {
if (context.services.agentContext?.config) {
const workspaceContext =
context.services.config.getWorkspaceContext();
context.services.agentContext?.config.getWorkspaceContext();
const existingDirs = new Set(
workspaceContext.getDirectories().map((dir) => path.resolve(dir)),
);
@@ -144,8 +144,9 @@ export const directoryCommand: SlashCommand = {
action: async (context: CommandContext, args: string) => {
const {
ui: { addItem },
services: { config, settings },
services: { agentContext: agentContext, settings },
} = context;
const config = agentContext?.config;
const [...rest] = args.split(' ');
if (!config) {
@@ -252,7 +253,7 @@ export const directoryCommand: SlashCommand = {
trustedDirs={added}
errors={errors}
finishAddingDirectories={finishAddingDirectories}
config={config}
context={agentContext!}
addItem={addItem}
/>
),
@@ -275,8 +276,9 @@ export const directoryCommand: SlashCommand = {
action: async (context: CommandContext) => {
const {
ui: { addItem },
services: { config },
services: { agentContext: agentContext },
} = context;
const config = agentContext?.config;
if (!config) {
addItem({
type: MessageType.ERROR,
@@ -161,14 +161,16 @@ describe('extensionsCommand', () => {
mockContext = createMockCommandContext({
services: {
config: {
getExtensions: mockGetExtensions,
getExtensionLoader: vi.fn().mockReturnValue(mockExtensionLoader),
getWorkingDir: () => '/test/dir',
reloadSkills: mockReloadSkills,
getAgentRegistry: vi.fn().mockReturnValue({
reload: mockReloadAgents,
}),
agentContext: {
config: {
getExtensions: mockGetExtensions,
getExtensionLoader: vi.fn().mockReturnValue(mockExtensionLoader),
getWorkingDir: () => '/test/dir',
reloadSkills: mockReloadSkills,
getAgentRegistry: vi.fn().mockReturnValue({
reload: mockReloadAgents,
}),
},
},
},
ui: {
@@ -917,7 +919,7 @@ describe('extensionsCommand', () => {
expect(restartAction).not.toBeNull();
mockRestartExtension = vi.fn();
mockContext.services.config!.getExtensionLoader = vi
mockContext.services.agentContext!.config.getExtensionLoader = vi
.fn()
.mockImplementation(() => ({
getExtensions: mockGetExtensions,
@@ -927,7 +929,7 @@ describe('extensionsCommand', () => {
});
it('should show a message if no extensions are installed', async () => {
mockContext.services.config!.getExtensionLoader = vi
mockContext.services.agentContext!.config.getExtensionLoader = vi
.fn()
.mockImplementation(() => ({
getExtensions: () => [],
@@ -1017,7 +1019,7 @@ describe('extensionsCommand', () => {
});
it('shows an error if no extension loader is available', async () => {
mockContext.services.config!.getExtensionLoader = vi.fn();
mockContext.services.agentContext!.config.getExtensionLoader = vi.fn();
await restartAction!(mockContext, '--all');
@@ -54,8 +54,8 @@ function showMessageIfNoExtensions(
}
async function listAction(context: CommandContext) {
const extensions = context.services.config
? listExtensions(context.services.config)
const extensions = context.services.agentContext?.config
? listExtensions(context.services.agentContext?.config)
: [];
if (showMessageIfNoExtensions(context, extensions)) {
@@ -88,8 +88,8 @@ function updateAction(context: CommandContext, args: string): Promise<void> {
(resolve) => (resolveUpdateComplete = resolve),
);
const extensions = context.services.config
? listExtensions(context.services.config)
const extensions = context.services.agentContext?.config
? listExtensions(context.services.agentContext?.config)
: [];
if (showMessageIfNoExtensions(context, extensions)) {
@@ -128,7 +128,7 @@ function updateAction(context: CommandContext, args: string): Promise<void> {
},
});
if (names?.length) {
const extensions = listExtensions(context.services.config!);
const extensions = listExtensions(context.services.agentContext?.config!);
for (const name of names) {
const extension = extensions.find(
(extension) => extension.name === name,
@@ -156,7 +156,8 @@ async function restartAction(
context: CommandContext,
args: string,
): Promise<void> {
const extensionLoader = context.services.config?.getExtensionLoader();
const extensionLoader =
context.services.agentContext?.config?.getExtensionLoader();
if (!extensionLoader) {
context.ui.addItem({
type: MessageType.ERROR,
@@ -235,8 +236,8 @@ async function restartAction(
if (failures.length < extensionsToRestart.length) {
try {
await context.services.config?.reloadSkills();
await context.services.config?.getAgentRegistry()?.reload();
await context.services.agentContext?.config?.reloadSkills();
await context.services.agentContext?.config?.getAgentRegistry()?.reload();
} catch (error) {
context.ui.addItem({
type: MessageType.ERROR,
@@ -274,7 +275,8 @@ async function exploreAction(
const useRegistryUI = settings.experimental?.extensionRegistry;
if (useRegistryUI) {
const extensionManager = context.services.config?.getExtensionLoader();
const extensionManager =
context.services.agentContext?.config?.getExtensionLoader();
if (extensionManager instanceof ExtensionManager) {
return {
type: 'custom_dialog' as const,
@@ -331,7 +333,8 @@ function getEnableDisableContext(
names: string[];
scope: SettingScope;
} | null {
const extensionLoader = context.services.config?.getExtensionLoader();
const extensionLoader =
context.services.agentContext?.config?.getExtensionLoader();
if (!(extensionLoader instanceof ExtensionManager)) {
debugLogger.error(
`Cannot ${context.invocation?.name} extensions in this environment`,
@@ -431,7 +434,8 @@ async function enableAction(context: CommandContext, args: string) {
if (extension?.mcpServers) {
const mcpEnablementManager = McpServerEnablementManager.getInstance();
const mcpClientManager = context.services.config?.getMcpClientManager();
const mcpClientManager =
context.services.agentContext?.config?.getMcpClientManager();
const enabledServers = await mcpEnablementManager.autoEnableServers(
Object.keys(extension.mcpServers ?? {}),
);
@@ -463,7 +467,8 @@ async function installAction(
args: string,
requestConsentOverride?: (consent: string) => Promise<boolean>,
) {
const extensionLoader = context.services.config?.getExtensionLoader();
const extensionLoader =
context.services.agentContext?.config?.getExtensionLoader();
if (!(extensionLoader instanceof ExtensionManager)) {
debugLogger.error(
`Cannot ${context.invocation?.name} extensions in this environment`,
@@ -529,7 +534,8 @@ async function installAction(
}
async function linkAction(context: CommandContext, args: string) {
const extensionLoader = context.services.config?.getExtensionLoader();
const extensionLoader =
context.services.agentContext?.config?.getExtensionLoader();
if (!(extensionLoader instanceof ExtensionManager)) {
debugLogger.error(
`Cannot ${context.invocation?.name} extensions in this environment`,
@@ -593,7 +599,8 @@ async function linkAction(context: CommandContext, args: string) {
}
async function uninstallAction(context: CommandContext, args: string) {
const extensionLoader = context.services.config?.getExtensionLoader();
const extensionLoader =
context.services.agentContext?.config?.getExtensionLoader();
if (!(extensionLoader instanceof ExtensionManager)) {
debugLogger.error(
`Cannot ${context.invocation?.name} extensions in this environment`,
@@ -692,7 +699,8 @@ async function configAction(context: CommandContext, args: string) {
}
}
const extensionManager = context.services.config?.getExtensionLoader();
const extensionManager =
context.services.agentContext?.config?.getExtensionLoader();
if (!(extensionManager instanceof ExtensionManager)) {
debugLogger.error(
`Cannot ${context.invocation?.name} extensions in this environment`,
@@ -729,7 +737,7 @@ export function completeExtensions(
context: CommandContext,
partialArg: string,
) {
let extensions = context.services.config?.getExtensions() ?? [];
let extensions = context.services.agentContext?.config?.getExtensions() ?? [];
if (context.invocation?.name === 'enable') {
extensions = extensions.filter((ext) => !ext.isActive);
@@ -93,7 +93,7 @@ describe('hooksCommand', () => {
// Create mock context with config and settings
mockContext = createMockCommandContext({
services: {
config: mockConfig,
agentContext: mockConfig as any,
settings: mockSettings,
},
});
@@ -141,7 +141,7 @@ describe('hooksCommand', () => {
it('should return error when config is not loaded', async () => {
const contextWithoutConfig = createMockCommandContext({
services: {
config: null,
agentContext: null as any,
},
});
@@ -225,7 +225,7 @@ describe('hooksCommand', () => {
it('should return error when config is not loaded', async () => {
const contextWithoutConfig = createMockCommandContext({
services: {
config: null,
agentContext: null as any,
},
});
@@ -338,7 +338,7 @@ describe('hooksCommand', () => {
it('should return error when config is not loaded', async () => {
const contextWithoutConfig = createMockCommandContext({
services: {
config: null,
agentContext: null as any,
},
});
@@ -470,7 +470,7 @@ describe('hooksCommand', () => {
it('should return empty array when config is not available', () => {
const contextWithoutConfig = createMockCommandContext({
services: {
config: null,
agentContext: null as any,
},
});
@@ -567,7 +567,7 @@ describe('hooksCommand', () => {
it('should return error when config is not loaded', async () => {
const contextWithoutConfig = createMockCommandContext({
services: {
config: null,
agentContext: null as any,
},
});
@@ -691,7 +691,7 @@ describe('hooksCommand', () => {
it('should return error when config is not loaded', async () => {
const contextWithoutConfig = createMockCommandContext({
services: {
config: null,
agentContext: null as any,
},
});
+7 -7
View File
@@ -27,7 +27,7 @@ import { HooksDialog } from '../components/HooksDialog.js';
function panelAction(
context: CommandContext,
): MessageActionReturn | OpenCustomDialogActionReturn {
const { config } = context.services;
const config = context.services.agentContext?.config;
if (!config) {
return {
type: 'message',
@@ -55,7 +55,7 @@ async function enableAction(
context: CommandContext,
args: string,
): Promise<void | MessageActionReturn> {
const { config } = context.services;
const config = context.services.agentContext?.config;
if (!config) {
return {
type: 'message',
@@ -108,7 +108,7 @@ async function disableAction(
context: CommandContext,
args: string,
): Promise<void | MessageActionReturn> {
const { config } = context.services;
const config = context.services.agentContext?.config;
if (!config) {
return {
type: 'message',
@@ -163,7 +163,7 @@ function completeEnabledHookNames(
context: CommandContext,
partialArg: string,
): string[] {
const { config } = context.services;
const config = context.services.agentContext?.config;
if (!config) return [];
const hookSystem = config.getHookSystem();
@@ -183,7 +183,7 @@ function completeDisabledHookNames(
context: CommandContext,
partialArg: string,
): string[] {
const { config } = context.services;
const config = context.services.agentContext?.config;
if (!config) return [];
const hookSystem = config.getHookSystem();
@@ -209,7 +209,7 @@ function getHookDisplayName(hook: HookRegistryEntry): string {
async function enableAllAction(
context: CommandContext,
): Promise<void | MessageActionReturn> {
const { config } = context.services;
const config = context.services.agentContext?.config;
if (!config) {
return {
type: 'message',
@@ -280,7 +280,7 @@ async function enableAllAction(
async function disableAllAction(
context: CommandContext,
): Promise<void | MessageActionReturn> {
const { config } = context.services;
const config = context.services.agentContext?.config;
if (!config) {
return {
type: 'message',
@@ -60,7 +60,7 @@ describe('ideCommand', () => {
settings: {
setValue: vi.fn(),
},
config: {
context: {
getIdeMode: vi.fn(),
setIdeMode: vi.fn(),
getUsageStatisticsEnabled: vi.fn().mockReturnValue(false),
+15 -5
View File
@@ -217,9 +217,13 @@ export const ideCommand = async (): Promise<SlashCommand> => {
);
// Poll for up to 5 seconds for the extension to activate.
for (let i = 0; i < 10; i++) {
await setIdeModeAndSyncConnection(context.services.config!, true, {
logToConsole: false,
});
await setIdeModeAndSyncConnection(
context.services.agentContext!.config,
true,
{
logToConsole: false,
},
);
if (
ideClient.getConnectionStatus().status ===
IDEConnectionStatus.Connected
@@ -262,7 +266,10 @@ export const ideCommand = async (): Promise<SlashCommand> => {
'ide.enabled',
true,
);
await setIdeModeAndSyncConnection(context.services.config!, true);
await setIdeModeAndSyncConnection(
context.services.agentContext!.config,
true,
);
const { messageType, content } = getIdeStatusMessage(ideClient);
context.ui.addItem(
{
@@ -285,7 +292,10 @@ export const ideCommand = async (): Promise<SlashCommand> => {
'ide.enabled',
false,
);
await setIdeModeAndSyncConnection(context.services.config!, false);
await setIdeModeAndSyncConnection(
context.services.agentContext!.config,
false,
);
const { messageType, content } = getIdeStatusMessage(ideClient);
context.ui.addItem(
{
@@ -31,8 +31,10 @@ describe('initCommand', () => {
// Create a fresh mock context for each test
mockContext = createMockCommandContext({
services: {
config: {
getTargetDir: () => targetDir,
agentContext: {
config: {
getTargetDir: () => targetDir,
},
},
},
});
@@ -94,7 +96,7 @@ describe('initCommand', () => {
// Arrange: Create a context without config
const noConfigContext = createMockCommandContext();
if (noConfigContext.services) {
noConfigContext.services.config = null;
noConfigContext.services.agentContext = null;
}
// Act: Run the command's action
+2 -2
View File
@@ -23,14 +23,14 @@ export const initCommand: SlashCommand = {
context: CommandContext,
_args: string,
): Promise<SlashCommandActionReturn> => {
if (!context.services.config) {
if (!context.services.agentContext?.config) {
return {
type: 'message',
messageType: 'error',
content: 'Configuration not available.',
};
}
const targetDir = context.services.config.getTargetDir();
const targetDir = context.services.agentContext?.config.getTargetDir();
const geminiMdPath = path.join(targetDir, 'GEMINI.md');
const result = performInit(fs.existsSync(geminiMdPath));
+10 -10
View File
@@ -70,11 +70,11 @@ const createMockMCPTool = (
describe('mcpCommand', () => {
let mockContext: ReturnType<typeof createMockCommandContext>;
let mockConfig: {
getToolRegistry: ReturnType<typeof vi.fn>;
toolRegistry: any;
getMcpServers: ReturnType<typeof vi.fn>;
getBlockedMcpServers: ReturnType<typeof vi.fn>;
getPromptRegistry: ReturnType<typeof vi.fn>;
getGeminiClient: ReturnType<typeof vi.fn>;
geminiClient: any;
getMcpClientManager: ReturnType<typeof vi.fn>;
getResourceRegistry: ReturnType<typeof vi.fn>;
setUserInteractedWithMcp: ReturnType<typeof vi.fn>;
@@ -95,16 +95,16 @@ describe('mcpCommand', () => {
// Create mock config with all necessary methods
mockConfig = {
getToolRegistry: vi.fn().mockReturnValue({
toolRegistry: {
getAllTools: vi.fn().mockReturnValue([]),
}),
} as any,
getMcpServers: vi.fn().mockReturnValue({}),
getBlockedMcpServers: vi.fn().mockReturnValue([]),
getPromptRegistry: vi.fn().mockReturnValue({
getAllPrompts: vi.fn().mockReturnValue([]),
getPromptsByServer: vi.fn().mockReturnValue([]),
}),
getGeminiClient: vi.fn(),
geminiClient: {} as any,
getMcpClientManager: vi.fn().mockImplementation(() => ({
getBlockedMcpServers: vi.fn().mockReturnValue([]),
getMcpServers: vi.fn().mockReturnValue({}),
@@ -119,7 +119,7 @@ describe('mcpCommand', () => {
mockContext = createMockCommandContext({
services: {
config: mockConfig,
agentContext: mockConfig as any,
},
});
});
@@ -132,7 +132,7 @@ describe('mcpCommand', () => {
it('should show an error if config is not available', async () => {
const contextWithoutConfig = createMockCommandContext({
services: {
config: null,
agentContext: null as any,
},
});
@@ -146,7 +146,7 @@ describe('mcpCommand', () => {
});
it('should show an error if tool registry is not available', async () => {
mockConfig.getToolRegistry = vi.fn().mockReturnValue(undefined);
(mockConfig as any).toolRegistry = undefined;
const result = await mcpCommand.action!(mockContext, '');
@@ -196,9 +196,9 @@ describe('mcpCommand', () => {
...mockServer3Tools,
];
mockConfig.getToolRegistry = vi.fn().mockReturnValue({
(mockConfig as any).toolRegistry = {
getAllTools: vi.fn().mockReturnValue(allTools),
});
};
const resourcesByServer: Record<
string,
+17 -11
View File
@@ -42,7 +42,8 @@ const authCommand: SlashCommand = {
args: string,
): Promise<MessageActionReturn> => {
const serverName = args.trim();
const { config } = context.services;
const agentContext = context.services.agentContext;
const config = agentContext?.config;
if (!config) {
return {
@@ -138,7 +139,7 @@ const authCommand: SlashCommand = {
await mcpClientManager.restartServer(serverName);
}
// Update the client with the new tools
const geminiClient = config.getGeminiClient();
const geminiClient = agentContext?.geminiClient;
if (geminiClient?.isInitialized()) {
await geminiClient.setTools();
}
@@ -162,7 +163,8 @@ const authCommand: SlashCommand = {
}
},
completion: async (context: CommandContext, partialArg: string) => {
const { config } = context.services;
const agentContext = context.services.agentContext;
const config = agentContext?.config;
if (!config) return [];
const mcpServers = config.getMcpClientManager()?.getMcpServers() || {};
@@ -177,7 +179,8 @@ const listAction = async (
showDescriptions = false,
showSchema = false,
): Promise<void | MessageActionReturn> => {
const { config } = context.services;
const agentContext = context.services.agentContext;
const config = agentContext?.config;
if (!config) {
return {
type: 'message',
@@ -188,7 +191,7 @@ const listAction = async (
config.setUserInteractedWithMcp();
const toolRegistry = config.getToolRegistry();
const toolRegistry = agentContext?.toolRegistry;
if (!toolRegistry) {
return {
type: 'message',
@@ -334,7 +337,8 @@ const reloadCommand: SlashCommand = {
action: async (
context: CommandContext,
): Promise<void | SlashCommandActionReturn> => {
const { config } = context.services;
const agentContext = context.services.agentContext;
const config = agentContext?.config;
if (!config) {
return {
type: 'message',
@@ -360,7 +364,7 @@ const reloadCommand: SlashCommand = {
await mcpClientManager.restart();
// Update the client with the new tools
const geminiClient = config.getGeminiClient();
const geminiClient = agentContext?.geminiClient;
if (geminiClient?.isInitialized()) {
await geminiClient.setTools();
}
@@ -377,7 +381,8 @@ async function handleEnableDisable(
args: string,
enable: boolean,
): Promise<MessageActionReturn> {
const { config } = context.services;
const agentContext = context.services.agentContext;
const config = agentContext?.config;
if (!config) {
return {
type: 'message',
@@ -465,8 +470,8 @@ async function handleEnableDisable(
);
await mcpClientManager.restart();
}
if (config.getGeminiClient()?.isInitialized())
await config.getGeminiClient().setTools();
if (agentContext?.geminiClient?.isInitialized())
await agentContext?.geminiClient?.setTools();
context.ui.reloadCommands();
return { type: 'message', messageType: 'info', content: msg };
@@ -477,7 +482,8 @@ async function getEnablementCompletion(
partialArg: string,
showEnabled: boolean,
): Promise<string[]> {
const { config } = context.services;
const agentContext = context.services.agentContext;
const config = agentContext?.config;
if (!config) return [];
const servers = Object.keys(
config.getMcpClientManager()?.getMcpServers() || {},
@@ -102,10 +102,12 @@ describe('memoryCommand', () => {
mockContext = createMockCommandContext({
services: {
config: {
getUserMemory: mockGetUserMemory,
getGeminiMdFileCount: mockGetGeminiMdFileCount,
getExtensionLoader: () => new SimpleExtensionLoader([]),
agentContext: {
config: {
getUserMemory: mockGetUserMemory,
getGeminiMdFileCount: mockGetGeminiMdFileCount,
getExtensionLoader: () => new SimpleExtensionLoader([]),
},
},
},
});
@@ -250,7 +252,7 @@ describe('memoryCommand', () => {
mockContext = createMockCommandContext({
services: {
config: mockConfig,
agentContext: mockConfig as any,
settings: {
merged: {
memoryDiscoveryMaxDirs: 1000,
@@ -268,7 +270,7 @@ describe('memoryCommand', () => {
if (!reloadCommand.action) throw new Error('Command has no action');
// Enable JIT in mock config
const config = mockContext.services.config;
const config = mockContext.services.agentContext!.config;
if (!config) throw new Error('Config is undefined');
vi.mocked(config.isJitContextEnabled).mockReturnValue(true);
@@ -370,7 +372,7 @@ describe('memoryCommand', () => {
if (!reloadCommand.action) throw new Error('Command has no action');
const nullConfigContext = createMockCommandContext({
services: { config: null },
services: { agentContext: null as any },
});
await expect(
@@ -413,8 +415,10 @@ describe('memoryCommand', () => {
});
mockContext = createMockCommandContext({
services: {
config: {
getGeminiMdFilePaths: mockGetGeminiMdfilePaths,
agentContext: {
config: {
getGeminiMdFilePaths: mockGetGeminiMdfilePaths,
},
},
},
});
@@ -29,7 +29,7 @@ export const memoryCommand: SlashCommand = {
kind: CommandKind.BUILT_IN,
autoExecute: true,
action: async (context) => {
const config = context.services.config;
const config = context.services.agentContext?.config;
if (!config) return;
const result = showMemory(config);
@@ -81,7 +81,7 @@ export const memoryCommand: SlashCommand = {
);
try {
const config = context.services.config;
const config = context.services.agentContext?.config;
if (config) {
const result = await refreshMemory(config);
@@ -111,7 +111,7 @@ export const memoryCommand: SlashCommand = {
kind: CommandKind.BUILT_IN,
autoExecute: true,
action: async (context) => {
const config = context.services.config;
const config = context.services.agentContext?.config;
if (!config) return;
const result = listMemoryFiles(config);
@@ -37,8 +37,10 @@ describe('modelCommand', () => {
}
const mockRefreshUserQuota = vi.fn();
mockContext.services.config = {
refreshUserQuota: mockRefreshUserQuota,
mockContext.services.agentContext = {
config: {
refreshUserQuota: mockRefreshUserQuota,
},
} as unknown as Config;
await modelCommand.action(mockContext, '');
@@ -66,8 +68,10 @@ describe('modelCommand', () => {
(c) => c.name === 'manage',
);
const mockRefreshUserQuota = vi.fn();
mockContext.services.config = {
refreshUserQuota: mockRefreshUserQuota,
mockContext.services.agentContext = {
config: {
refreshUserQuota: mockRefreshUserQuota,
},
} as unknown as Config;
await manageCommand!.action!(mockContext, '');
@@ -84,20 +88,22 @@ describe('modelCommand', () => {
expect(setCommand).toBeDefined();
const mockSetModel = vi.fn();
mockContext.services.config = {
setModel: mockSetModel,
getHasAccessToPreviewModel: vi.fn().mockReturnValue(true),
getUserId: vi.fn().mockReturnValue('test-user'),
getUsageStatisticsEnabled: vi.fn().mockReturnValue(true),
getSessionId: vi.fn().mockReturnValue('test-session'),
getContentGeneratorConfig: vi
.fn()
.mockReturnValue({ authType: 'test-auth' }),
isInteractive: vi.fn().mockReturnValue(true),
getExperiments: vi.fn().mockReturnValue({ experimentIds: [] }),
getPolicyEngine: vi.fn().mockReturnValue({
getApprovalMode: vi.fn().mockReturnValue('auto'),
}),
mockContext.services.agentContext = {
config: {
setModel: mockSetModel,
getHasAccessToPreviewModel: vi.fn().mockReturnValue(true),
getUserId: vi.fn().mockReturnValue('test-user'),
getUsageStatisticsEnabled: vi.fn().mockReturnValue(true),
getSessionId: vi.fn().mockReturnValue('test-session'),
getContentGeneratorConfig: vi
.fn()
.mockReturnValue({ authType: 'test-auth' }),
isInteractive: vi.fn().mockReturnValue(true),
getExperiments: vi.fn().mockReturnValue({ experimentIds: [] }),
getPolicyEngine: vi.fn().mockReturnValue({
getApprovalMode: vi.fn().mockReturnValue('auto'),
}),
},
} as unknown as Config;
await setCommand!.action!(mockContext, 'gemini-pro');
@@ -116,20 +122,22 @@ describe('modelCommand', () => {
(c) => c.name === 'set',
);
const mockSetModel = vi.fn();
mockContext.services.config = {
setModel: mockSetModel,
getHasAccessToPreviewModel: vi.fn().mockReturnValue(true),
getUserId: vi.fn().mockReturnValue('test-user'),
getUsageStatisticsEnabled: vi.fn().mockReturnValue(true),
getSessionId: vi.fn().mockReturnValue('test-session'),
getContentGeneratorConfig: vi
.fn()
.mockReturnValue({ authType: 'test-auth' }),
isInteractive: vi.fn().mockReturnValue(true),
getExperiments: vi.fn().mockReturnValue({ experimentIds: [] }),
getPolicyEngine: vi.fn().mockReturnValue({
getApprovalMode: vi.fn().mockReturnValue('auto'),
}),
mockContext.services.agentContext = {
config: {
setModel: mockSetModel,
getHasAccessToPreviewModel: vi.fn().mockReturnValue(true),
getUserId: vi.fn().mockReturnValue('test-user'),
getUsageStatisticsEnabled: vi.fn().mockReturnValue(true),
getSessionId: vi.fn().mockReturnValue('test-session'),
getContentGeneratorConfig: vi
.fn()
.mockReturnValue({ authType: 'test-auth' }),
isInteractive: vi.fn().mockReturnValue(true),
getExperiments: vi.fn().mockReturnValue({ experimentIds: [] }),
getPolicyEngine: vi.fn().mockReturnValue({
getApprovalMode: vi.fn().mockReturnValue('auto'),
}),
},
} as unknown as Config;
await setCommand!.action!(mockContext, 'gemini-pro --persist');
+5 -5
View File
@@ -34,10 +34,10 @@ const setModelCommand: SlashCommand = {
const modelName = parts[0];
const persist = parts.includes('--persist');
if (context.services.config) {
context.services.config.setModel(modelName, !persist);
if (context.services.agentContext?.config) {
context.services.agentContext?.config.setModel(modelName, !persist);
const event = new ModelSlashCommandEvent(modelName);
logModelSlashCommand(context.services.config, event);
logModelSlashCommand(context.services.agentContext?.config, event);
context.ui.addItem({
type: MessageType.INFO,
@@ -53,8 +53,8 @@ const manageModelCommand: SlashCommand = {
kind: CommandKind.BUILT_IN,
autoExecute: true,
action: async (context: CommandContext) => {
if (context.services.config) {
await context.services.config.refreshUserQuota();
if (context.services.agentContext?.config) {
await context.services.agentContext?.config.refreshUserQuota();
}
return {
type: 'dialog',
@@ -24,7 +24,7 @@ export const oncallCommand: SlashCommand = {
kind: CommandKind.BUILT_IN,
autoExecute: true,
action: async (context, args): Promise<OpenCustomDialogActionReturn> => {
const { config } = context.services;
const config = context.services.agentContext?.config;
if (!config) {
throw new Error('Config not available');
}
@@ -56,7 +56,7 @@ export const oncallCommand: SlashCommand = {
kind: CommandKind.BUILT_IN,
autoExecute: true,
action: async (context, args): Promise<OpenCustomDialogActionReturn> => {
const { config } = context.services;
const config = context.services.agentContext?.config;
if (!config) {
throw new Error('Config not available');
}
@@ -52,7 +52,7 @@ describe('planCommand', () => {
beforeEach(() => {
mockContext = createMockCommandContext({
services: {
config: {
context: {
isPlanEnabled: vi.fn(),
setApprovalMode: vi.fn(),
getApprovedPlanPath: vi.fn(),
@@ -83,17 +83,19 @@ describe('planCommand', () => {
});
it('should switch to plan mode if enabled', async () => {
vi.mocked(mockContext.services.config!.isPlanEnabled).mockReturnValue(true);
vi.mocked(mockContext.services.config!.getApprovedPlanPath).mockReturnValue(
undefined,
);
vi.mocked(
mockContext.services.agentContext!.config!.isPlanEnabled,
).mockReturnValue(true);
vi.mocked(
mockContext.services.agentContext!.config!.getApprovedPlanPath,
).mockReturnValue(undefined);
if (!planCommand.action) throw new Error('Action missing');
await planCommand.action(mockContext, '');
expect(mockContext.services.config!.setApprovalMode).toHaveBeenCalledWith(
ApprovalMode.PLAN,
);
expect(
mockContext.services.agentContext!.config!.setApprovalMode,
).toHaveBeenCalledWith(ApprovalMode.PLAN);
expect(coreEvents.emitFeedback).toHaveBeenCalledWith(
'info',
'Switched to Plan Mode.',
@@ -102,10 +104,12 @@ describe('planCommand', () => {
it('should display the approved plan from config', async () => {
const mockPlanPath = '/mock/plans/dir/approved-plan.md';
vi.mocked(mockContext.services.config!.isPlanEnabled).mockReturnValue(true);
vi.mocked(mockContext.services.config!.getApprovedPlanPath).mockReturnValue(
mockPlanPath,
);
vi.mocked(
mockContext.services.agentContext!.config!.isPlanEnabled,
).mockReturnValue(true);
vi.mocked(
mockContext.services.agentContext!.config!.getApprovedPlanPath,
).mockReturnValue(mockPlanPath);
vi.mocked(processSingleFileContent).mockResolvedValue({
llmContent: '# Approved Plan Content',
returnDisplay: '# Approved Plan Content',
@@ -128,7 +132,7 @@ describe('planCommand', () => {
it('should copy the approved plan to clipboard', async () => {
const mockPlanPath = '/mock/plans/dir/approved-plan.md';
vi.mocked(
mockContext.services.config!.getApprovedPlanPath,
mockContext.services.agentContext!.config!.getApprovedPlanPath,
).mockReturnValue(mockPlanPath);
vi.mocked(readFileWithEncoding).mockResolvedValue('# Plan Content');
@@ -149,7 +153,7 @@ describe('planCommand', () => {
it('should warn if no approved plan is found', async () => {
vi.mocked(
mockContext.services.config!.getApprovedPlanPath,
mockContext.services.agentContext!.config!.getApprovedPlanPath,
).mockReturnValue(undefined);
const copySubCommand = planCommand.subCommands?.find(
+2 -2
View File
@@ -22,7 +22,7 @@ import * as path from 'node:path';
import { copyToClipboard } from '../utils/commandUtils.js';
async function copyAction(context: CommandContext) {
const config = context.services.config;
const config = context.services.agentContext?.config;
if (!config) {
debugLogger.debug('Plan copy command: config is not available in context');
return;
@@ -53,7 +53,7 @@ export const planCommand: SlashCommand = {
kind: CommandKind.BUILT_IN,
autoExecute: false,
action: async (context) => {
const config = context.services.config;
const config = context.services.agentContext?.config;
if (!config) {
debugLogger.debug('Plan command: config is not available in context');
return;
@@ -32,7 +32,7 @@ describe('policiesCommand', () => {
describe('list subcommand', () => {
it('should show error if config is missing', async () => {
mockContext.services.config = null;
mockContext.services.agentContext = null;
const listCommand = policiesCommand.subCommands![0];
await listCommand.action!(mockContext, '');
@@ -50,8 +50,10 @@ describe('policiesCommand', () => {
const mockPolicyEngine = {
getRules: vi.fn().mockReturnValue([]),
};
mockContext.services.config = {
getPolicyEngine: vi.fn().mockReturnValue(mockPolicyEngine),
mockContext.services.agentContext = {
config: {
getPolicyEngine: vi.fn().mockReturnValue(mockPolicyEngine),
},
} as unknown as Config;
const listCommand = policiesCommand.subCommands![0];
@@ -85,8 +87,10 @@ describe('policiesCommand', () => {
const mockPolicyEngine = {
getRules: vi.fn().mockReturnValue(mockRules),
};
mockContext.services.config = {
getPolicyEngine: vi.fn().mockReturnValue(mockPolicyEngine),
mockContext.services.agentContext = {
config: {
getPolicyEngine: vi.fn().mockReturnValue(mockPolicyEngine),
},
} as unknown as Config;
const listCommand = policiesCommand.subCommands![0];
@@ -142,8 +146,10 @@ describe('policiesCommand', () => {
const mockPolicyEngine = {
getRules: vi.fn().mockReturnValue(mockRules),
};
mockContext.services.config = {
getPolicyEngine: vi.fn().mockReturnValue(mockPolicyEngine),
mockContext.services.agentContext = {
config: {
getPolicyEngine: vi.fn().mockReturnValue(mockPolicyEngine),
},
} as unknown as Config;
const listCommand = policiesCommand.subCommands![0];
@@ -51,7 +51,7 @@ const listPoliciesCommand: SlashCommand = {
kind: CommandKind.BUILT_IN,
autoExecute: true,
action: async (context) => {
const { config } = context.services;
const config = context.services.agentContext?.config;
if (!config) {
context.ui.addItem(
{
@@ -47,14 +47,14 @@ describe('restoreCommand', () => {
getProjectTempCheckpointsDir: vi.fn().mockReturnValue(checkpointsDir),
getProjectTempDir: vi.fn().mockReturnValue(geminiTempDir),
},
getGeminiClient: vi.fn().mockReturnValue({
geminiClient: vi.fn().mockReturnValue({
setHistory: mockSetHistory,
}),
} as unknown as Config;
mockContext = createMockCommandContext({
services: {
config: mockConfig,
agentContext: mockConfig as any,
git: mockGitService,
},
});
@@ -37,7 +37,8 @@ async function restoreAction(
args: string,
): Promise<void | SlashCommandActionReturn> {
const { services, ui } = context;
const { config, git: gitService } = services;
const { agentContext: agentContext, git: gitService } = services;
const config = agentContext?.config;
const { addItem, loadHistory } = ui;
const checkpointDir = config?.storage.getProjectTempCheckpointsDir();
@@ -116,7 +117,9 @@ async function restoreAction(
} else if (action.type === 'load_history' && loadHistory) {
loadHistory(action.history);
if (action.clientHistory) {
config?.getGeminiClient()?.setHistory(action.clientHistory);
context.services.agentContext?.geminiClient?.setHistory(
action.clientHistory,
);
}
}
}
@@ -140,7 +143,8 @@ async function completion(
_partialArg: string,
): Promise<string[]> {
const { services } = context;
const { config } = services;
const { agentContext: agentContext } = services;
const config = agentContext?.config;
const checkpointDir = config?.storage.getProjectTempCheckpointsDir();
if (!checkpointDir) {
return [];
@@ -97,15 +97,17 @@ describe('rewindCommand', () => {
mockContext = createMockCommandContext({
services: {
config: {
getGeminiClient: () => ({
agentContext: {
geminiClient: {
getChatRecordingService: mockGetChatRecordingService,
setHistory: mockSetHistory,
sendMessageStream: mockSendMessageStream,
}),
getSessionId: () => 'test-session-id',
getContextManager: () => ({ refresh: mockResetContext }),
getProjectRoot: mockGetProjectRoot,
} as any,
config: {
getSessionId: () => 'test-session-id',
getContextManager: () => ({ refresh: mockResetContext }),
getProjectRoot: mockGetProjectRoot,
},
},
},
ui: {
@@ -293,7 +295,7 @@ describe('rewindCommand', () => {
it('should fail if client is not initialized', () => {
const context = createMockCommandContext({
services: {
config: { getGeminiClient: () => undefined },
agentContext: { geminiClient: undefined as any },
},
}) as unknown as CommandContext;
@@ -309,8 +311,8 @@ describe('rewindCommand', () => {
it('should fail if recording service is unavailable', () => {
const context = createMockCommandContext({
services: {
config: {
getGeminiClient: () => ({ getChatRecordingService: () => undefined }),
agentContext: {
geminiClient: { getChatRecordingService: () => undefined } as any,
},
},
}) as unknown as CommandContext;
@@ -61,7 +61,7 @@ async function rewindConversation(
client.setHistory(clientHistory as Content[]);
// Reset context manager as we are rewinding history
await context.services.config?.getContextManager()?.refresh();
await context.services.agentContext?.config?.getContextManager()?.refresh();
// Update UI History
// We generate IDs based on index for the rewind history
@@ -94,7 +94,8 @@ export const rewindCommand: SlashCommand = {
description: 'Jump back to a specific message and restart the conversation',
kind: CommandKind.BUILT_IN,
action: (context) => {
const config = context.services.config;
const agentContext = context.services.agentContext;
const config = agentContext?.config;
if (!config)
return {
type: 'message',
@@ -102,7 +103,7 @@ export const rewindCommand: SlashCommand = {
content: 'Config not found',
};
const client = config.getGeminiClient();
const client = agentContext!.geminiClient;
if (!client)
return {
type: 'message',
@@ -230,7 +230,7 @@ export const setupGithubCommand: SlashCommand = {
}
// Get the latest release tag from GitHub
const proxy = context?.services?.config?.getProxy();
const proxy = context?.services?.agentContext?.config?.getProxy();
const releaseTag = await getLatestGitHubRelease(proxy);
const readmeUrl = `https://github.com/google-github-actions/run-gemini-cli/blob/${releaseTag}/README.md#quick-start`;
@@ -68,7 +68,7 @@ describe('skillsCommand', () => {
];
context = createMockCommandContext({
services: {
config: {
agentContext: {
getSkillManager: vi.fn().mockReturnValue({
getAllSkills: vi.fn().mockReturnValue(skills),
getSkills: vi.fn().mockReturnValue(skills),
@@ -162,7 +162,8 @@ describe('skillsCommand', () => {
});
it('should filter built-in skills by default and show them with "all"', async () => {
const skillManager = context.services.config!.getSkillManager();
const skillManager =
context.services.agentContext!.config!.getSkillManager();
const mockSkills = [
{
name: 'regular',
@@ -452,7 +453,8 @@ describe('skillsCommand', () => {
});
it('should show error if skills are disabled by admin during disable', async () => {
const skillManager = context.services.config!.getSkillManager();
const skillManager =
context.services.agentContext!.config!.getSkillManager();
vi.mocked(skillManager.isAdminEnabled).mockReturnValue(false);
const disableCmd = skillsCommand.subCommands!.find(
@@ -470,7 +472,8 @@ describe('skillsCommand', () => {
});
it('should show error if skills are disabled by admin during enable', async () => {
const skillManager = context.services.config!.getSkillManager();
const skillManager =
context.services.agentContext!.config!.getSkillManager();
vi.mocked(skillManager.isAdminEnabled).mockReturnValue(false);
const enableCmd = skillsCommand.subCommands!.find(
@@ -537,7 +540,8 @@ describe('skillsCommand', () => {
(s) => s.name === 'reload',
)!;
const reloadSkillsMock = vi.fn().mockImplementation(async () => {
const skillManager = context.services.config!.getSkillManager();
const skillManager =
context.services.agentContext!.config!.getSkillManager();
vi.mocked(skillManager.getSkills).mockReturnValue([
{ name: 'skill1' },
{ name: 'skill2' },
@@ -562,7 +566,8 @@ describe('skillsCommand', () => {
(s) => s.name === 'reload',
)!;
const reloadSkillsMock = vi.fn().mockImplementation(async () => {
const skillManager = context.services.config!.getSkillManager();
const skillManager =
context.services.agentContext!.config!.getSkillManager();
vi.mocked(skillManager.getSkills).mockReturnValue([
{ name: 'skill1' },
] as SkillDefinition[]);
@@ -585,7 +590,8 @@ describe('skillsCommand', () => {
(s) => s.name === 'reload',
)!;
const reloadSkillsMock = vi.fn().mockImplementation(async () => {
const skillManager = context.services.config!.getSkillManager();
const skillManager =
context.services.agentContext!.config!.getSkillManager();
vi.mocked(skillManager.getSkills).mockReturnValue([
{ name: 'skill2' }, // skill1 removed, skill3 added
{ name: 'skill3' },
@@ -608,7 +614,7 @@ describe('skillsCommand', () => {
const reloadCmd = skillsCommand.subCommands!.find(
(s) => s.name === 'reload',
)!;
context.services.config = null;
context.services.agentContext = null;
await reloadCmd.action!(context, '');
@@ -651,7 +657,8 @@ describe('skillsCommand', () => {
const disableCmd = skillsCommand.subCommands!.find(
(s) => s.name === 'disable',
)!;
const skillManager = context.services.config!.getSkillManager();
const skillManager =
context.services.agentContext!.config!.getSkillManager();
const mockSkills = [
{
name: 'skill1',
@@ -681,7 +688,8 @@ describe('skillsCommand', () => {
const enableCmd = skillsCommand.subCommands!.find(
(s) => s.name === 'enable',
)!;
const skillManager = context.services.config!.getSkillManager();
const skillManager =
context.services.agentContext!.config!.getSkillManager();
const mockSkills = [
{
name: 'skill1',
+10 -10
View File
@@ -46,7 +46,7 @@ async function listAction(
}
}
const skillManager = context.services.config?.getSkillManager();
const skillManager = context.services.agentContext?.config?.getSkillManager();
if (!skillManager) {
context.ui.addItem({
type: MessageType.ERROR,
@@ -127,8 +127,8 @@ async function linkAction(
text: `Successfully linked skills from "${sourcePath}" (${scope}).`,
});
if (context.services.config) {
await context.services.config.reloadSkills();
if (context.services.agentContext?.config) {
await context.services.agentContext?.config.reloadSkills();
}
} catch (error) {
context.ui.addItem({
@@ -150,14 +150,14 @@ async function disableAction(
});
return;
}
const skillManager = context.services.config?.getSkillManager();
const skillManager = context.services.agentContext?.config?.getSkillManager();
if (skillManager?.isAdminEnabled() === false) {
context.ui.addItem(
{
type: MessageType.ERROR,
text: getAdminErrorMessage(
'Agent skills',
context.services.config ?? undefined,
context.services.agentContext?.config ?? undefined,
),
},
Date.now(),
@@ -211,14 +211,14 @@ async function enableAction(
return;
}
const skillManager = context.services.config?.getSkillManager();
const skillManager = context.services.agentContext?.config?.getSkillManager();
if (skillManager?.isAdminEnabled() === false) {
context.ui.addItem(
{
type: MessageType.ERROR,
text: getAdminErrorMessage(
'Agent skills',
context.services.config ?? undefined,
context.services.agentContext?.config ?? undefined,
),
},
Date.now(),
@@ -246,7 +246,7 @@ async function enableAction(
async function reloadAction(
context: CommandContext,
): Promise<void | SlashCommandActionReturn> {
const config = context.services.config;
const config = context.services.agentContext?.config;
if (!config) {
context.ui.addItem({
type: MessageType.ERROR,
@@ -333,7 +333,7 @@ function disableCompletion(
context: CommandContext,
partialArg: string,
): string[] {
const skillManager = context.services.config?.getSkillManager();
const skillManager = context.services.agentContext?.config?.getSkillManager();
if (!skillManager) {
return [];
}
@@ -347,7 +347,7 @@ function enableCompletion(
context: CommandContext,
partialArg: string,
): string[] {
const skillManager = context.services.config?.getSkillManager();
const skillManager = context.services.agentContext?.config?.getSkillManager();
if (!skillManager) {
return [];
}
@@ -10,7 +10,7 @@ import { type CommandContext } from './types.js';
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
import { MessageType } from '../types.js';
import { formatDuration } from '../utils/formatters.js';
import type { Config } from '@google/gemini-cli-core';
import type { AgentLoopContext } from '@google/gemini-cli-core';
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
@@ -43,14 +43,15 @@ describe('statsCommand', () => {
it('should display general session stats when run with no subcommand', async () => {
if (!statsCommand.action) throw new Error('Command has no action');
mockContext.services.config = {
refreshUserQuota: vi.fn(),
refreshAvailableCredits: vi.fn(),
getUserTierName: vi.fn(),
getUserPaidTier: vi.fn(),
getModel: vi.fn(),
} as unknown as Config;
mockContext.services.agentContext = {
config: {
refreshUserQuota: vi.fn(),
refreshAvailableCredits: vi.fn(),
getUserTierName: vi.fn(),
getUserPaidTier: vi.fn(),
getModel: vi.fn(),
},
} as unknown as AgentLoopContext;
await statsCommand.action(mockContext, '');
const expectedDuration = formatDuration(
@@ -80,16 +81,18 @@ describe('statsCommand', () => {
.fn()
.mockReturnValue('2025-01-01T12:00:00Z');
mockContext.services.config = {
refreshUserQuota: mockRefreshUserQuota,
getUserTierName: mockGetUserTierName,
getModel: mockGetModel,
getQuotaRemaining: mockGetQuotaRemaining,
getQuotaLimit: mockGetQuotaLimit,
getQuotaResetTime: mockGetQuotaResetTime,
getUserPaidTier: vi.fn(),
refreshAvailableCredits: vi.fn(),
} as unknown as Config;
mockContext.services.agentContext = {
config: {
refreshUserQuota: mockRefreshUserQuota,
getUserTierName: mockGetUserTierName,
getModel: mockGetModel,
getQuotaRemaining: mockGetQuotaRemaining,
getQuotaLimit: mockGetQuotaLimit,
getQuotaResetTime: mockGetQuotaResetTime,
getUserPaidTier: vi.fn(),
refreshAvailableCredits: vi.fn(),
},
} as unknown as AgentLoopContext;
await statsCommand.action(mockContext, '');
+19 -13
View File
@@ -29,8 +29,8 @@ function getUserIdentity(context: CommandContext) {
const cachedAccount = userAccountManager.getCachedGoogleAccount();
const userEmail = cachedAccount ?? undefined;
const tier = context.services.config?.getUserTierName();
const paidTier = context.services.config?.getUserPaidTier();
const tier = context.services.agentContext?.config?.getUserTierName();
const paidTier = context.services.agentContext?.config?.getUserPaidTier();
const creditBalance = getG1CreditBalance(paidTier) ?? undefined;
return { selectedAuthType, userEmail, tier, creditBalance };
@@ -50,7 +50,7 @@ async function defaultSessionView(context: CommandContext) {
const { selectedAuthType, userEmail, tier, creditBalance } =
getUserIdentity(context);
const currentModel = context.services.config?.getModel();
const currentModel = context.services.agentContext?.config?.getModel();
const statsItem: HistoryItemStats = {
type: MessageType.STATS,
@@ -62,16 +62,19 @@ async function defaultSessionView(context: CommandContext) {
creditBalance,
};
if (context.services.config) {
if (context.services.agentContext?.config) {
const [quota] = await Promise.all([
context.services.config.refreshUserQuota(),
context.services.config.refreshAvailableCredits(),
context.services.agentContext?.config.refreshUserQuota(),
context.services.agentContext?.config.refreshAvailableCredits(),
]);
if (quota) {
statsItem.quotas = quota;
statsItem.pooledRemaining = context.services.config.getQuotaRemaining();
statsItem.pooledLimit = context.services.config.getQuotaLimit();
statsItem.pooledResetTime = context.services.config.getQuotaResetTime();
statsItem.pooledRemaining =
context.services.agentContext?.config.getQuotaRemaining();
statsItem.pooledLimit =
context.services.agentContext?.config.getQuotaLimit();
statsItem.pooledResetTime =
context.services.agentContext?.config.getQuotaResetTime();
}
}
@@ -107,10 +110,13 @@ export const statsCommand: SlashCommand = {
isSafeConcurrent: true,
action: (context: CommandContext) => {
const { selectedAuthType, userEmail, tier } = getUserIdentity(context);
const currentModel = context.services.config?.getModel();
const pooledRemaining = context.services.config?.getQuotaRemaining();
const pooledLimit = context.services.config?.getQuotaLimit();
const pooledResetTime = context.services.config?.getQuotaResetTime();
const currentModel = context.services.agentContext?.config?.getModel();
const pooledRemaining =
context.services.agentContext?.config?.getQuotaRemaining();
const pooledLimit =
context.services.agentContext?.config?.getQuotaLimit();
const pooledResetTime =
context.services.agentContext?.config?.getQuotaResetTime();
context.ui.addItem({
type: MessageType.MODEL_STATS,
selectedAuthType,
@@ -30,8 +30,8 @@ describe('toolsCommand', () => {
it('should display an error if the tool registry is unavailable', async () => {
const mockContext = createMockCommandContext({
services: {
config: {
getToolRegistry: () => undefined,
agentContext: {
toolRegistry: undefined as any,
},
},
});
@@ -48,10 +48,10 @@ describe('toolsCommand', () => {
it('should display "No tools available" when none are found', async () => {
const mockContext = createMockCommandContext({
services: {
config: {
getToolRegistry: () => ({
agentContext: {
toolRegistry: {
getAllTools: () => [] as Array<ToolBuilder<object, ToolResult>>,
}),
} as any,
},
},
});
@@ -69,8 +69,8 @@ describe('toolsCommand', () => {
it('should list tools without descriptions by default (no args)', async () => {
const mockContext = createMockCommandContext({
services: {
config: {
getToolRegistry: () => ({ getAllTools: () => mockTools }),
agentContext: {
toolRegistry: { getAllTools: () => mockTools } as any,
},
},
});
@@ -90,8 +90,8 @@ describe('toolsCommand', () => {
it('should list tools without descriptions when "list" arg is passed', async () => {
const mockContext = createMockCommandContext({
services: {
config: {
getToolRegistry: () => ({ getAllTools: () => mockTools }),
agentContext: {
toolRegistry: { getAllTools: () => mockTools } as any,
},
},
});
@@ -111,8 +111,8 @@ describe('toolsCommand', () => {
it('should list tools with descriptions when "desc" arg is passed', async () => {
const mockContext = createMockCommandContext({
services: {
config: {
getToolRegistry: () => ({ getAllTools: () => mockTools }),
agentContext: {
toolRegistry: { getAllTools: () => mockTools } as any,
},
},
});
@@ -144,8 +144,8 @@ describe('toolsCommand', () => {
it('subcommand "list" should display tools without descriptions', async () => {
const mockContext = createMockCommandContext({
services: {
config: {
getToolRegistry: () => ({ getAllTools: () => mockTools }),
agentContext: {
toolRegistry: { getAllTools: () => mockTools } as any,
},
},
});
@@ -165,8 +165,8 @@ describe('toolsCommand', () => {
it('subcommand "desc" should display tools with descriptions', async () => {
const mockContext = createMockCommandContext({
services: {
config: {
getToolRegistry: () => ({ getAllTools: () => mockTools }),
agentContext: {
toolRegistry: { getAllTools: () => mockTools } as any,
},
},
});
@@ -196,8 +196,8 @@ describe('toolsCommand', () => {
const mockContext = createMockCommandContext({
services: {
config: {
getToolRegistry: () => ({ getAllTools: () => mockTools }),
agentContext: {
toolRegistry: { getAllTools: () => mockTools } as any,
},
},
});
+1 -1
View File
@@ -15,7 +15,7 @@ async function listTools(
context: CommandContext,
showDescriptions: boolean,
): Promise<void> {
const toolRegistry = context.services.config?.getToolRegistry();
const toolRegistry = context.services.agentContext?.toolRegistry;
if (!toolRegistry) {
context.ui.addItem({
type: MessageType.ERROR,
+3 -3
View File
@@ -11,11 +11,11 @@ import type {
ConfirmationRequest,
} from '../types.js';
import type {
Config,
GitService,
Logger,
CommandActionReturn,
AgentDefinition,
AgentLoopContext,
} from '@google/gemini-cli-core';
import type { LoadedSettings } from '../../config/settings.js';
import type { UseHistoryManagerReturn } from '../hooks/useHistoryManager.js';
@@ -38,8 +38,8 @@ export interface CommandContext {
};
// Core services and configuration
services: {
// TODO(abhipatel12): Ensure that config is never null.
config: Config | null;
// TODO(abhipatel12): Ensure that context is never null.
agentContext: AgentLoopContext | null;
settings: LoadedSettings;
git: GitService | undefined;
logger: Logger;
@@ -33,7 +33,7 @@ describe('upgradeCommand', () => {
vi.clearAllMocks();
mockContext = createMockCommandContext({
services: {
config: {
context: {
getContentGeneratorConfig: vi.fn().mockReturnValue({
authType: AuthType.LOGIN_WITH_GOOGLE,
}),
@@ -62,7 +62,7 @@ describe('upgradeCommand', () => {
it('should return an error message when NOT logged in with Google', async () => {
vi.mocked(
mockContext.services.config!.getContentGeneratorConfig,
mockContext.services.agentContext!.config!.getContentGeneratorConfig,
).mockReturnValue({
authType: AuthType.USE_GEMINI,
});
@@ -118,9 +118,9 @@ describe('upgradeCommand', () => {
});
it('should return info message for ultra tiers', async () => {
vi.mocked(mockContext.services.config!.getUserTierName).mockReturnValue(
'Advanced Ultra',
);
vi.mocked(
mockContext.services.agentContext!.config!.getUserTierName,
).mockReturnValue('Advanced Ultra');
if (!upgradeCommand.action) {
throw new Error('The upgrade command must have an action.');
@@ -24,7 +24,8 @@ export const upgradeCommand: SlashCommand = {
autoExecute: true,
action: async (context) => {
const authType =
context.services.config?.getContentGeneratorConfig()?.authType;
context.services.agentContext?.config?.getContentGeneratorConfig()
?.authType;
if (authType !== AuthType.LOGIN_WITH_GOOGLE) {
// This command should ideally be hidden if not logged in with Google,
// but we add a safety check here just in case.
@@ -36,7 +37,7 @@ export const upgradeCommand: SlashCommand = {
};
}
const tierName = context.services.config?.getUserTierName();
const tierName = context.services.agentContext?.config?.getUserTierName();
if (isUltraTier(tierName)) {
return {
type: 'message',
+2 -2
View File
@@ -110,7 +110,7 @@ export const AppHeader = ({ version, showDetails = true }: AppHeaderProps) => {
{/* Lines 3 & 4: User Identity info (Email /auth and Plan /upgrade) */}
{settings.merged.ui.showUserIdentity !== false && (
<UserIdentity config={config} />
<UserIdentity context={config} />
)}
</Box>
</Box>
@@ -125,7 +125,7 @@ export const AppHeader = ({ version, showDetails = true }: AppHeaderProps) => {
)}
{!(settings.merged.ui.hideTips || config.getScreenReader()) &&
showTips && <Tips config={config} />}
showTips && <Tips context={config} />}
</Box>
);
};
@@ -232,7 +232,7 @@ const createMockConfig = (overrides = {}): Config =>
getAccessibility: vi.fn(() => ({})),
getMcpServers: vi.fn(() => ({})),
isPlanEnabled: vi.fn(() => true),
getToolRegistry: () => ({
toolRegistry: () => ({
getTool: vi.fn(),
}),
getSkillManager: () => ({
+1 -1
View File
@@ -443,7 +443,7 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
userMessages={uiState.userMessages}
setBannerVisible={uiActions.setBannerVisible}
onClearScreen={uiActions.handleClearScreen}
config={config}
context={config}
slashCommands={uiState.slashCommands || []}
commandContext={uiState.commandContext}
shellModeActive={uiState.shellModeActive}
@@ -146,7 +146,7 @@ export const DialogManager = ({
if (uiState.isPolicyUpdateDialogOpen) {
return (
<PolicyUpdateDialog
config={config}
context={config}
request={uiState.policyUpdateConfirmationRequest!}
onClose={() => uiActions.setIsPolicyUpdateDialogOpen(false)}
/>
@@ -345,7 +345,7 @@ export const DialogManager = ({
if (uiState.isSessionBrowserOpen) {
return (
<SessionBrowser
config={config}
context={config}
onResumeSession={uiActions.handleResumeSession}
onDeleteSession={uiActions.handleDeleteSession}
onExit={uiActions.closeSessionBrowser}
@@ -12,10 +12,10 @@ import {
validatePlanPath,
validatePlanContent,
QuestionType,
type Config,
type EditorType,
processSingleFileContent,
debugLogger,
type AgentLoopContext,
} from '@google/gemini-cli-core';
import { theme } from '../semantic-colors.js';
import { useConfig } from '../contexts/ConfigContext.js';
@@ -61,7 +61,10 @@ const StatusMessage: React.FC<{
children: React.ReactNode;
}> = ({ children }) => <Box paddingX={1}>{children}</Box>;
function usePlanContent(planPath: string, config: Config): PlanContentState {
function usePlanContent(
planPath: string,
context: AgentLoopContext,
): PlanContentState {
const [version, setVersion] = useState(0);
const [state, setState] = useState<Omit<PlanContentState, 'refresh'>>({
status: PlanStatus.Loading,
@@ -79,8 +82,8 @@ function usePlanContent(planPath: string, config: Config): PlanContentState {
try {
const pathError = await validatePlanPath(
planPath,
config.storage.getPlansDir(),
config.getTargetDir(),
context.config.storage.getPlansDir(),
context.config.getTargetDir(),
);
if (ignore) return;
if (pathError) {
@@ -97,8 +100,8 @@ function usePlanContent(planPath: string, config: Config): PlanContentState {
const result = await processSingleFileContent(
planPath,
config.storage.getPlansDir(),
config.getFileSystemService(),
context.config.storage.getPlansDir(),
context.config.getFileSystemService(),
);
if (ignore) return;
@@ -134,7 +137,7 @@ function usePlanContent(planPath: string, config: Config): PlanContentState {
return () => {
ignore = true;
};
}, [planPath, config, version]);
}, [planPath, context, version]);
return { ...state, refresh };
}
@@ -343,7 +343,7 @@ describe('InputPrompt', () => {
onSubmit: vi.fn(),
userMessages: [],
onClearScreen: vi.fn(),
config: {
context: {
getProjectRoot: () => path.join('test', 'project'),
getTargetDir: () => path.join('test', 'project', 'src'),
getVimMode: () => false,
@@ -773,10 +773,10 @@ describe('InputPrompt', () => {
await waitFor(() => {
expect(clipboardUtils.clipboardHasImage).toHaveBeenCalled();
expect(clipboardUtils.saveClipboardImage).toHaveBeenCalledWith(
props.config.getTargetDir(),
props.context.config.getTargetDir(),
);
expect(clipboardUtils.cleanupOldClipboardImages).toHaveBeenCalledWith(
props.config.getTargetDir(),
props.context.config.getTargetDir(),
);
expect(mockBuffer.replaceRangeByOffset).toHaveBeenCalled();
});
@@ -1819,7 +1819,7 @@ describe('InputPrompt', () => {
});
it('should render with plain borders when useBackgroundColor is false', async () => {
props.config.getUseBackgroundColor = () => false;
props.context.config.getUseBackgroundColor = () => false;
const { stdout, unmount } = renderWithProviders(
<InputPrompt {...props} />,
);
@@ -2092,7 +2092,7 @@ describe('InputPrompt', () => {
mockBuffer.lines = [text];
mockBuffer.viewportVisualLines = [text];
mockBuffer.visualCursor = visualCursor as [number, number];
props.config.getUseBackgroundColor = () => false;
props.context.config.getUseBackgroundColor = () => false;
const { stdout, unmount } = renderWithProviders(
<InputPrompt {...props} />,
@@ -2155,7 +2155,7 @@ describe('InputPrompt', () => {
mockBuffer.visualToLogicalMap = visualToLogicalMap as Array<
[number, number]
>;
props.config.getUseBackgroundColor = () => false;
props.context.config.getUseBackgroundColor = () => false;
const { stdout, unmount } = renderWithProviders(
<InputPrompt {...props} />,
@@ -2185,7 +2185,7 @@ describe('InputPrompt', () => {
[1, 0],
[2, 0],
];
props.config.getUseBackgroundColor = () => false;
props.context.config.getUseBackgroundColor = () => false;
const { stdout, unmount } = renderWithProviders(
<InputPrompt {...props} />,
@@ -2217,7 +2217,7 @@ describe('InputPrompt', () => {
[1, 0],
[2, 0],
];
props.config.getUseBackgroundColor = () => false;
props.context.config.getUseBackgroundColor = () => false;
const { stdout, unmount } = renderWithProviders(
<InputPrompt {...props} />,
@@ -3627,7 +3627,7 @@ describe('InputPrompt', () => {
});
it('should move cursor on mouse click with plain borders', async () => {
props.config.getUseBackgroundColor = () => false;
props.context.config.getUseBackgroundColor = () => false;
props.buffer.text = 'hello world';
props.buffer.lines = ['hello world'];
props.buffer.viewportVisualLines = ['hello world'];
+19 -11
View File
@@ -43,7 +43,7 @@ import {
ApprovalMode,
coreEvents,
debugLogger,
type Config,
type AgentLoopContext,
} from '@google/gemini-cli-core';
import {
parseInputForHighlighting,
@@ -100,7 +100,7 @@ export interface InputPromptProps {
onSubmit: (value: string) => void;
userMessages: readonly string[];
onClearScreen: () => void;
config: Config;
context: AgentLoopContext;
slashCommands: readonly SlashCommand[];
commandContext: CommandContext;
placeholder?: string;
@@ -193,7 +193,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
onSubmit,
userMessages,
onClearScreen,
config,
context,
slashCommands,
commandContext,
placeholder = ' Type your message or @path/to/file',
@@ -273,17 +273,20 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
]);
const [expandedSuggestionIndex, setExpandedSuggestionIndex] =
useState<number>(-1);
const shellHistory = useShellHistory(config.getProjectRoot(), config.storage);
const shellHistory = useShellHistory(
context.config.getProjectRoot(),
context.config.storage,
);
const shellHistoryData = shellHistory.history;
const completion = useCommandCompletion({
buffer,
cwd: config.getTargetDir(),
cwd: context.config.getTargetDir(),
slashCommands,
commandContext,
reverseSearchActive,
shellModeActive,
config,
context: context,
active: !suppressCompletion,
});
@@ -479,15 +482,20 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
}
try {
if (await clipboardHasImage()) {
const imagePath = await saveClipboardImage(config.getTargetDir());
const imagePath = await saveClipboardImage(
context.config.getTargetDir(),
);
if (imagePath) {
// Clean up old images
cleanupOldClipboardImages(config.getTargetDir()).catch(() => {
cleanupOldClipboardImages(context.config.getTargetDir()).catch(() => {
// Ignore cleanup errors
});
// Get relative path from current directory
const relativePath = path.relative(config.getTargetDir(), imagePath);
const relativePath = path.relative(
context.config.getTargetDir(),
imagePath,
);
// Insert @path reference at cursor position
const insertText = `@${relativePath}`;
@@ -533,7 +541,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
}
}, [
buffer,
config,
context,
stdout,
settings,
shortcutsHelpVisible,
@@ -1430,7 +1438,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
const { inlineGhost, additionalLines } = getGhostTextLines();
const useBackgroundColor = config.getUseBackgroundColor();
const useBackgroundColor = context.config.getUseBackgroundColor();
const isLowColor = isLowColorDepth();
const terminalBg = theme.background.primary || 'black';
@@ -23,7 +23,11 @@ import {
AuthType,
UserTierId,
} from '@google/gemini-cli-core';
import type { Config, ModelSlashCommandEvent } from '@google/gemini-cli-core';
import type {
Config,
ModelSlashCommandEvent,
AgentLoopContext,
} from '@google/gemini-cli-core';
// Mock dependencies
const mockGetDisplayString = vi.fn();
@@ -36,8 +40,10 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
return {
...actual,
getDisplayString: (val: string) => mockGetDisplayString(val),
logModelSlashCommand: (config: Config, event: ModelSlashCommandEvent) =>
mockLogModelSlashCommand(config, event),
logModelSlashCommand: (
context: AgentLoopContext,
event: ModelSlashCommandEvent,
) => mockLogModelSlashCommand(context, event),
ModelSlashCommandEvent: class {
constructor(model: string) {
mockModelSlashCommandEvent(model);
@@ -55,7 +55,7 @@ const defaultProps: MultiFolderTrustDialogProps = {
trustedDirs: [],
errors: [],
finishAddingDirectories: mockFinishAddingDirectories,
config: mockConfig,
context: mockConfig,
addItem: mockAddItem,
};
@@ -234,7 +234,7 @@ describe('MultiFolderTrustDialog', () => {
<MultiFolderTrustDialog
{...defaultProps}
folders={folders}
config={null as unknown as Config}
context={null as unknown as Config}
/>,
);
await waitUntilReady();
@@ -17,7 +17,7 @@ import { loadTrustedFolders, TrustLevel } from '../../config/trustedFolders.js';
import { expandHomeDir } from '../utils/directoryUtils.js';
import * as path from 'node:path';
import { MessageType, type HistoryItem } from '../types.js';
import { type Config } from '@google/gemini-cli-core';
import { type AgentLoopContext } from '@google/gemini-cli-core';
export enum MultiFolderTrustChoice {
YES,
@@ -31,7 +31,7 @@ export interface MultiFolderTrustDialogProps {
trustedDirs: string[];
errors: string[];
finishAddingDirectories: (
config: Config,
context: AgentLoopContext,
addItem: (
itemData: Omit<HistoryItem, 'id'>,
baseTimestamp?: number,
@@ -39,7 +39,7 @@ export interface MultiFolderTrustDialogProps {
added: string[],
errors: string[],
) => Promise<void>;
config: Config;
context: AgentLoopContext;
addItem: (
itemData: Omit<HistoryItem, 'id'>,
baseTimestamp?: number,
@@ -52,7 +52,7 @@ export const MultiFolderTrustDialog: React.FC<MultiFolderTrustDialogProps> = ({
trustedDirs,
errors: initialErrors,
finishAddingDirectories,
config,
context,
addItem,
}) => {
const [submitted, setSubmitted] = useState(false);
@@ -65,7 +65,7 @@ export const MultiFolderTrustDialog: React.FC<MultiFolderTrustDialogProps> = ({
'\n- ',
)}`,
);
await finishAddingDirectories(config, addItem, trustedDirs, errors);
await finishAddingDirectories(context, addItem, trustedDirs, errors);
onComplete();
};
@@ -102,7 +102,7 @@ export const MultiFolderTrustDialog: React.FC<MultiFolderTrustDialogProps> = ({
const handleSelect = async (choice: MultiFolderTrustChoice) => {
setSubmitted(true);
if (!config) {
if (!context) {
addItem({
type: MessageType.ERROR,
text: 'Configuration is not available.',
@@ -111,7 +111,7 @@ export const MultiFolderTrustDialog: React.FC<MultiFolderTrustDialogProps> = ({
return;
}
const workspaceContext = config.getWorkspaceContext();
const workspaceContext = context.config.getWorkspaceContext();
const trustedFolders = loadTrustedFolders();
const errors = [...initialErrors];
const added = [...trustedDirs];
@@ -142,7 +142,7 @@ export const MultiFolderTrustDialog: React.FC<MultiFolderTrustDialogProps> = ({
}
}
await finishAddingDirectories(config, addItem, added, errors);
await finishAddingDirectories(context, addItem, added, errors);
onComplete();
};
@@ -59,7 +59,7 @@ describe('PolicyUpdateDialog', () => {
it('renders correctly and matches snapshot', async () => {
const { lastFrame, waitUntilReady } = renderWithProviders(
<PolicyUpdateDialog
config={mockConfig}
context={mockConfig}
request={mockRequest}
onClose={onClose}
/>,
@@ -77,7 +77,7 @@ describe('PolicyUpdateDialog', () => {
it('handles ACCEPT correctly', async () => {
const { stdin } = renderWithProviders(
<PolicyUpdateDialog
config={mockConfig}
context={mockConfig}
request={mockRequest}
onClose={onClose}
/>,
@@ -100,7 +100,7 @@ describe('PolicyUpdateDialog', () => {
it('handles IGNORE correctly', async () => {
const { stdin } = renderWithProviders(
<PolicyUpdateDialog
config={mockConfig}
context={mockConfig}
request={mockRequest}
onClose={onClose}
/>,
@@ -124,7 +124,7 @@ describe('PolicyUpdateDialog', () => {
it('calls onClose when Escape key is pressed', async () => {
const { stdin } = renderWithProviders(
<PolicyUpdateDialog
config={mockConfig}
context={mockConfig}
request={mockRequest}
onClose={onClose}
/>,
@@ -9,8 +9,8 @@ import type React from 'react';
import { useCallback, useRef } from 'react';
import {
PolicyIntegrityManager,
type Config,
type PolicyUpdateConfirmationRequest,
type AgentLoopContext,
} from '@google/gemini-cli-core';
import { theme } from '../semantic-colors.js';
import {
@@ -27,13 +27,13 @@ export enum PolicyUpdateChoice {
}
interface PolicyUpdateDialogProps {
config: Config;
context: AgentLoopContext;
request: PolicyUpdateConfirmationRequest;
onClose: () => void;
}
export const PolicyUpdateDialog: React.FC<PolicyUpdateDialogProps> = ({
config,
context,
request,
onClose,
}) => {
@@ -55,14 +55,14 @@ export const PolicyUpdateDialog: React.FC<PolicyUpdateDialogProps> = ({
request.identifier,
request.newHash,
);
await config.loadWorkspacePolicies(request.policyDir);
await context.config.loadWorkspacePolicies(request.policyDir);
}
onClose();
} finally {
isProcessing.current = false;
}
},
[config, request, onClose],
[context, request, onClose],
);
useKeypress(
@@ -156,7 +156,7 @@ describe('SessionBrowser component', () => {
const { lastFrame, waitUntilReady } = render(
<TestSessionBrowser
config={config}
context={config}
onResumeSession={onResumeSession}
onDeleteSession={onDeleteSession}
onExit={onExit}
@@ -194,7 +194,7 @@ describe('SessionBrowser component', () => {
const { lastFrame, waitUntilReady } = render(
<TestSessionBrowser
config={config}
context={config}
onResumeSession={onResumeSession}
onDeleteSession={onDeleteSession}
onExit={onExit}
@@ -247,7 +247,7 @@ describe('SessionBrowser component', () => {
const { lastFrame, waitUntilReady } = render(
<TestSessionBrowser
config={config}
context={config}
onResumeSession={onResumeSession}
onDeleteSession={onDeleteSession}
onExit={onExit}
@@ -307,7 +307,7 @@ describe('SessionBrowser component', () => {
const { lastFrame, waitUntilReady } = render(
<TestSessionBrowser
config={config}
context={config}
onResumeSession={onResumeSession}
onDeleteSession={onDeleteSession}
onExit={onExit}
@@ -356,7 +356,7 @@ describe('SessionBrowser component', () => {
const { waitUntilReady } = render(
<TestSessionBrowser
config={config}
context={config}
onResumeSession={onResumeSession}
onDeleteSession={onDeleteSession}
onExit={onExit}
@@ -384,7 +384,7 @@ describe('SessionBrowser component', () => {
const { lastFrame, waitUntilReady } = render(
<TestSessionBrowser
config={config}
context={config}
onResumeSession={onResumeSession}
onDeleteSession={onDeleteSession}
onExit={onExit}
@@ -12,7 +12,7 @@ import { Colors } from '../colors.js';
import { useTerminalSize } from '../hooks/useTerminalSize.js';
import { useKeypress } from '../hooks/useKeypress.js';
import path from 'node:path';
import type { Config } from '@google/gemini-cli-core';
import type { AgentLoopContext } from '@google/gemini-cli-core';
import type { SessionInfo } from '../../utils/sessionUtils.js';
import {
formatRelativeTime,
@@ -24,7 +24,7 @@ import {
*/
export interface SessionBrowserProps {
/** Application configuration object */
config: Config;
context: AgentLoopContext;
/** Callback when user selects a session to resume */
onResumeSession: (session: SessionInfo) => void;
/** Callback when user deletes a session */
@@ -494,7 +494,10 @@ export const useSessionBrowserState = (
/**
* Hook to load sessions on mount.
*/
const useLoadSessions = (config: Config, state: SessionBrowserState) => {
const useLoadSessions = (
context: AgentLoopContext,
state: SessionBrowserState,
) => {
const {
setSessions,
setLoading,
@@ -507,10 +510,13 @@ const useLoadSessions = (config: Config, state: SessionBrowserState) => {
useEffect(() => {
const loadSessions = async () => {
try {
const chatsDir = path.join(config.storage.getProjectTempDir(), 'chats');
const chatsDir = path.join(
context.config.storage.getProjectTempDir(),
'chats',
);
const sessionData = await getSessionFiles(
chatsDir,
config.getSessionId(),
context.config.getSessionId(),
);
setSessions(sessionData);
setLoading(false);
@@ -524,19 +530,19 @@ const useLoadSessions = (config: Config, state: SessionBrowserState) => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
loadSessions();
}, [config, setSessions, setLoading, setError]);
}, [context, setSessions, setLoading, setError]);
useEffect(() => {
const loadFullContent = async () => {
if (isSearchMode && !hasLoadedFullContent) {
try {
const chatsDir = path.join(
config.storage.getProjectTempDir(),
context.config.storage.getProjectTempDir(),
'chats',
);
const sessionData = await getSessionFiles(
chatsDir,
config.getSessionId(),
context.config.getSessionId(),
{ includeFullContent: true },
);
setSessions(sessionData);
@@ -556,7 +562,7 @@ const useLoadSessions = (config: Config, state: SessionBrowserState) => {
}, [
isSearchMode,
hasLoadedFullContent,
config,
context,
setSessions,
setHasLoadedFullContent,
setError,
@@ -787,14 +793,14 @@ export function SessionBrowserView({
}
export function SessionBrowser({
config,
context,
onResumeSession,
onDeleteSession,
onExit,
}: SessionBrowserProps): React.JSX.Element {
// Use all our custom hooks
const state = useSessionBrowserState();
useLoadSessions(config, state);
useLoadSessions(context, state);
const moveSelection = useMoveSelection(state);
const cycleSortOrder = useCycleSortOrder(state);
useSessionBrowserInput(
@@ -33,6 +33,7 @@ import {
getDisplayString,
isAutoModel,
AuthType,
type AgentLoopContext,
} from '@google/gemini-cli-core';
import { useSettings } from '../contexts/SettingsContext.js';
import { useConfig } from '../contexts/ConfigContext.js';
@@ -89,7 +90,7 @@ const Section: React.FC<SectionProps> = ({ title, children }) => (
// Logic for building the unified list of table rows
const buildModelRows = (
models: Record<string, ModelMetrics>,
config: Config,
context: AgentLoopContext,
quotas?: RetrieveUserQuotaResponse,
useGemini3_1 = false,
useCustomToolModel = false,
@@ -98,7 +99,7 @@ const buildModelRows = (
const usedModelNames = new Set(
Object.keys(models)
.map(getBaseModelName)
.map((name) => getDisplayString(name, config)),
.map((name) => getDisplayString(name, context.config)),
);
// 1. Models with active usage
@@ -108,7 +109,7 @@ const buildModelRows = (
const inputTokens = metrics.tokens.input;
return {
key: name,
modelName: getDisplayString(modelName, config),
modelName: getDisplayString(modelName, context.config),
requests: metrics.api.totalRequests,
cachedTokens: cachedTokens.toLocaleString(),
inputTokens: inputTokens.toLocaleString(),
@@ -125,11 +126,11 @@ const buildModelRows = (
(b) =>
b.modelId &&
isActiveModel(b.modelId, useGemini3_1, useCustomToolModel) &&
!usedModelNames.has(getDisplayString(b.modelId, config)),
!usedModelNames.has(getDisplayString(b.modelId, context.config)),
)
.map((bucket) => ({
key: bucket.modelId!,
modelName: getDisplayString(bucket.modelId!, config),
modelName: getDisplayString(bucket.modelId!, context.config),
requests: '-',
cachedTokens: '-',
inputTokens: '-',
+1 -1
View File
@@ -19,7 +19,7 @@ describe('Tips', () => {
} as unknown as Config;
const { lastFrame, waitUntilReady, unmount } = render(
<Tips config={config} />,
<Tips context={config} />,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
+4 -4
View File
@@ -7,14 +7,14 @@
import type React from 'react';
import { Box, Text } from 'ink';
import { theme } from '../semantic-colors.js';
import { type Config } from '@google/gemini-cli-core';
import { type AgentLoopContext } from '@google/gemini-cli-core';
interface TipsProps {
config: Config;
context: AgentLoopContext;
}
export const Tips: React.FC<TipsProps> = ({ config }) => {
const geminiMdFileCount = config.getGeminiMdFileCount();
export const Tips: React.FC<TipsProps> = ({ context }) => {
const geminiMdFileCount = context.config.getGeminiMdFileCount();
return (
<Box flexDirection="column" marginTop={1}>
@@ -133,7 +133,7 @@ export const ToolConfirmationQueue: React.FC<ToolConfirmationQueueProps> = ({
<ToolConfirmationMessage
callId={tool.callId}
confirmationDetails={tool.confirmationDetails}
config={config}
context={config}
getPreferredEditor={getPreferredEditor}
terminalWidth={mainAreaWidth - 4} // Adjust for parent border/padding
availableTerminalHeight={availableContentHeight}

Some files were not shown because too many files have changed in this diff Show More