mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-11 06:31:01 -07:00
Co-authored-by: Mervap <megavaprold@gmail.com>
This commit is contained in:
@@ -16,6 +16,7 @@ import {
|
||||
import { GeminiAgent } from './zedIntegration.js';
|
||||
import * as acp from '@agentclientprotocol/sdk';
|
||||
import {
|
||||
ApprovalMode,
|
||||
AuthType,
|
||||
type Config,
|
||||
CoreToolCallStatus,
|
||||
@@ -62,6 +63,8 @@ describe('GeminiAgent Session Resume', () => {
|
||||
storage: {
|
||||
getProjectTempDir: vi.fn().mockReturnValue('/tmp/project'),
|
||||
},
|
||||
getApprovalMode: vi.fn().mockReturnValue('default'),
|
||||
isPlanEnabled: vi.fn().mockReturnValue(false),
|
||||
} as unknown as Mocked<Config>;
|
||||
mockSettings = {
|
||||
merged: {
|
||||
@@ -149,7 +152,28 @@ describe('GeminiAgent Session Resume', () => {
|
||||
mcpServers: [],
|
||||
});
|
||||
|
||||
expect(response).toEqual({});
|
||||
expect(response).toEqual({
|
||||
modes: {
|
||||
availableModes: [
|
||||
{
|
||||
id: ApprovalMode.DEFAULT,
|
||||
name: 'Default',
|
||||
description: 'Prompts for approval',
|
||||
},
|
||||
{
|
||||
id: ApprovalMode.AUTO_EDIT,
|
||||
name: 'Auto Edit',
|
||||
description: 'Auto-approves edit tools',
|
||||
},
|
||||
{
|
||||
id: ApprovalMode.YOLO,
|
||||
name: 'YOLO',
|
||||
description: 'Auto-approves all tools',
|
||||
},
|
||||
],
|
||||
currentModeId: ApprovalMode.DEFAULT,
|
||||
},
|
||||
});
|
||||
|
||||
// Verify resumeChat received the correct arguments
|
||||
expect(mockConfig.getGeminiClient().resumeChat).toHaveBeenCalledWith(
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
import { loadCliConfig, type CliArgs } from '../config/config.js';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import * as path from 'node:path';
|
||||
import { ApprovalMode } from '@google/gemini-cli-core/src/policy/types.js';
|
||||
|
||||
vi.mock('../config/config.js', () => ({
|
||||
loadCliConfig: vi.fn(),
|
||||
@@ -119,6 +120,8 @@ describe('GeminiAgent', () => {
|
||||
subscribe: vi.fn(),
|
||||
unsubscribe: vi.fn(),
|
||||
}),
|
||||
getApprovalMode: vi.fn().mockReturnValue('default'),
|
||||
isPlanEnabled: vi.fn().mockReturnValue(false),
|
||||
} as unknown as Mocked<Awaited<ReturnType<typeof loadCliConfig>>>;
|
||||
mockSettings = {
|
||||
merged: {
|
||||
@@ -185,6 +188,59 @@ describe('GeminiAgent', () => {
|
||||
expect(mockConfig.getGeminiClient).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return modes without plan mode when plan is disabled', async () => {
|
||||
mockConfig.getContentGeneratorConfig = vi.fn().mockReturnValue({
|
||||
apiKey: 'test-key',
|
||||
});
|
||||
mockConfig.isPlanEnabled = vi.fn().mockReturnValue(false);
|
||||
mockConfig.getApprovalMode = vi.fn().mockReturnValue('default');
|
||||
|
||||
const response = await agent.newSession({
|
||||
cwd: '/tmp',
|
||||
mcpServers: [],
|
||||
});
|
||||
|
||||
expect(response.modes).toEqual({
|
||||
availableModes: [
|
||||
{ id: 'default', name: 'Default', description: 'Prompts for approval' },
|
||||
{
|
||||
id: 'autoEdit',
|
||||
name: 'Auto Edit',
|
||||
description: 'Auto-approves edit tools',
|
||||
},
|
||||
{ id: 'yolo', name: 'YOLO', description: 'Auto-approves all tools' },
|
||||
],
|
||||
currentModeId: 'default',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return modes with plan mode when plan is enabled', async () => {
|
||||
mockConfig.getContentGeneratorConfig = vi.fn().mockReturnValue({
|
||||
apiKey: 'test-key',
|
||||
});
|
||||
mockConfig.isPlanEnabled = vi.fn().mockReturnValue(true);
|
||||
mockConfig.getApprovalMode = vi.fn().mockReturnValue('plan');
|
||||
|
||||
const response = await agent.newSession({
|
||||
cwd: '/tmp',
|
||||
mcpServers: [],
|
||||
});
|
||||
|
||||
expect(response.modes).toEqual({
|
||||
availableModes: [
|
||||
{ id: 'default', name: 'Default', description: 'Prompts for approval' },
|
||||
{
|
||||
id: 'autoEdit',
|
||||
name: 'Auto Edit',
|
||||
description: 'Auto-approves edit tools',
|
||||
},
|
||||
{ id: 'yolo', name: 'YOLO', description: 'Auto-approves all tools' },
|
||||
{ id: 'plan', name: 'Plan', description: 'Read-only mode' },
|
||||
],
|
||||
currentModeId: 'plan',
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail session creation if Gemini API key is missing', async () => {
|
||||
(loadSettings as unknown as Mock).mockImplementation(() => ({
|
||||
merged: {
|
||||
@@ -306,6 +362,32 @@ describe('GeminiAgent', () => {
|
||||
expect(session.prompt).toHaveBeenCalled();
|
||||
expect(result).toEqual({ stopReason: 'end_turn' });
|
||||
});
|
||||
|
||||
it('should delegate setMode to session', async () => {
|
||||
await agent.newSession({ cwd: '/tmp', mcpServers: [] });
|
||||
const session = (
|
||||
agent as unknown as { sessions: Map<string, Session> }
|
||||
).sessions.get('test-session-id');
|
||||
if (!session) throw new Error('Session not found');
|
||||
session.setMode = vi.fn().mockReturnValue({});
|
||||
|
||||
const result = await agent.setSessionMode({
|
||||
sessionId: 'test-session-id',
|
||||
modeId: 'plan',
|
||||
});
|
||||
|
||||
expect(session.setMode).toHaveBeenCalledWith('plan');
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it('should throw error when setting mode on non-existent session', async () => {
|
||||
await expect(
|
||||
agent.setSessionMode({
|
||||
sessionId: 'unknown',
|
||||
modeId: 'plan',
|
||||
}),
|
||||
).rejects.toThrow('Session not found: unknown');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Session', () => {
|
||||
@@ -352,6 +434,8 @@ describe('Session', () => {
|
||||
getEnableRecursiveFileSearch: vi.fn().mockReturnValue(false),
|
||||
getDebugMode: vi.fn().mockReturnValue(false),
|
||||
getMessageBus: vi.fn().mockReturnValue(mockMessageBus),
|
||||
setApprovalMode: vi.fn(),
|
||||
isPlanEnabled: vi.fn().mockReturnValue(false),
|
||||
} as unknown as Mocked<Config>;
|
||||
mockConnection = {
|
||||
sessionUpdate: vi.fn(),
|
||||
@@ -822,4 +906,17 @@ describe('Session', () => {
|
||||
].value;
|
||||
expect(mockInstance.build).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should set mode on config', () => {
|
||||
session.setMode(ApprovalMode.AUTO_EDIT);
|
||||
expect(mockConfig.setApprovalMode).toHaveBeenCalledWith(
|
||||
ApprovalMode.AUTO_EDIT,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error for invalid mode', () => {
|
||||
expect(() => session.setMode('invalid-mode')).toThrow(
|
||||
'Invalid or unavailable mode: invalid-mode',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
Kind,
|
||||
partListUnionToString,
|
||||
LlmRole,
|
||||
ApprovalMode,
|
||||
} from '@google/gemini-cli-core';
|
||||
import * as acp from '@agentclientprotocol/sdk';
|
||||
import { AcpFileSystemService } from './fileSystemService.js';
|
||||
@@ -225,6 +226,10 @@ export class GeminiAgent {
|
||||
|
||||
return {
|
||||
sessionId,
|
||||
modes: {
|
||||
availableModes: buildAvailableModes(config.isPlanEnabled()),
|
||||
currentModeId: config.getApprovalMode(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -276,7 +281,12 @@ export class GeminiAgent {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
session.streamHistory(sessionData.messages);
|
||||
|
||||
return {};
|
||||
return {
|
||||
modes: {
|
||||
availableModes: buildAvailableModes(config.isPlanEnabled()),
|
||||
currentModeId: config.getApprovalMode(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async initializeSessionConfig(
|
||||
@@ -377,6 +387,16 @@ export class GeminiAgent {
|
||||
}
|
||||
return session.prompt(params);
|
||||
}
|
||||
|
||||
async setSessionMode(
|
||||
params: acp.SetSessionModeRequest,
|
||||
): Promise<acp.SetSessionModeResponse> {
|
||||
const session = this.sessions.get(params.sessionId);
|
||||
if (!session) {
|
||||
throw new Error(`Session not found: ${params.sessionId}`);
|
||||
}
|
||||
return session.setMode(params.modeId);
|
||||
}
|
||||
}
|
||||
|
||||
export class Session {
|
||||
@@ -398,6 +418,17 @@ export class Session {
|
||||
this.pendingPrompt = null;
|
||||
}
|
||||
|
||||
setMode(modeId: acp.SessionModeId): acp.SetSessionModeResponse {
|
||||
const availableModes = buildAvailableModes(this.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);
|
||||
return {};
|
||||
}
|
||||
|
||||
async streamHistory(messages: ConversationRecord['messages']): Promise<void> {
|
||||
for (const msg of messages) {
|
||||
const contentString = partListUnionToString(msg.content);
|
||||
@@ -1273,3 +1304,33 @@ function toAcpToolKind(kind: Kind): acp.ToolKind {
|
||||
return 'other';
|
||||
}
|
||||
}
|
||||
|
||||
function buildAvailableModes(isPlanEnabled: boolean): acp.SessionMode[] {
|
||||
const modes: acp.SessionMode[] = [
|
||||
{
|
||||
id: ApprovalMode.DEFAULT,
|
||||
name: 'Default',
|
||||
description: 'Prompts for approval',
|
||||
},
|
||||
{
|
||||
id: ApprovalMode.AUTO_EDIT,
|
||||
name: 'Auto Edit',
|
||||
description: 'Auto-approves edit tools',
|
||||
},
|
||||
{
|
||||
id: ApprovalMode.YOLO,
|
||||
name: 'YOLO',
|
||||
description: 'Auto-approves all tools',
|
||||
},
|
||||
];
|
||||
|
||||
if (isPlanEnabled) {
|
||||
modes.push({
|
||||
id: ApprovalMode.PLAN,
|
||||
name: 'Plan',
|
||||
description: 'Read-only mode',
|
||||
});
|
||||
}
|
||||
|
||||
return modes;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user