mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-13 05:12:55 -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 { GeminiAgent } from './zedIntegration.js';
|
||||||
import * as acp from '@agentclientprotocol/sdk';
|
import * as acp from '@agentclientprotocol/sdk';
|
||||||
import {
|
import {
|
||||||
|
ApprovalMode,
|
||||||
AuthType,
|
AuthType,
|
||||||
type Config,
|
type Config,
|
||||||
CoreToolCallStatus,
|
CoreToolCallStatus,
|
||||||
@@ -62,6 +63,8 @@ describe('GeminiAgent Session Resume', () => {
|
|||||||
storage: {
|
storage: {
|
||||||
getProjectTempDir: vi.fn().mockReturnValue('/tmp/project'),
|
getProjectTempDir: vi.fn().mockReturnValue('/tmp/project'),
|
||||||
},
|
},
|
||||||
|
getApprovalMode: vi.fn().mockReturnValue('default'),
|
||||||
|
isPlanEnabled: vi.fn().mockReturnValue(false),
|
||||||
} as unknown as Mocked<Config>;
|
} as unknown as Mocked<Config>;
|
||||||
mockSettings = {
|
mockSettings = {
|
||||||
merged: {
|
merged: {
|
||||||
@@ -149,7 +152,28 @@ describe('GeminiAgent Session Resume', () => {
|
|||||||
mcpServers: [],
|
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
|
// Verify resumeChat received the correct arguments
|
||||||
expect(mockConfig.getGeminiClient().resumeChat).toHaveBeenCalledWith(
|
expect(mockConfig.getGeminiClient().resumeChat).toHaveBeenCalledWith(
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import {
|
|||||||
import { loadCliConfig, type CliArgs } from '../config/config.js';
|
import { loadCliConfig, type CliArgs } from '../config/config.js';
|
||||||
import * as fs from 'node:fs/promises';
|
import * as fs from 'node:fs/promises';
|
||||||
import * as path from 'node:path';
|
import * as path from 'node:path';
|
||||||
|
import { ApprovalMode } from '@google/gemini-cli-core/src/policy/types.js';
|
||||||
|
|
||||||
vi.mock('../config/config.js', () => ({
|
vi.mock('../config/config.js', () => ({
|
||||||
loadCliConfig: vi.fn(),
|
loadCliConfig: vi.fn(),
|
||||||
@@ -119,6 +120,8 @@ describe('GeminiAgent', () => {
|
|||||||
subscribe: vi.fn(),
|
subscribe: vi.fn(),
|
||||||
unsubscribe: vi.fn(),
|
unsubscribe: vi.fn(),
|
||||||
}),
|
}),
|
||||||
|
getApprovalMode: vi.fn().mockReturnValue('default'),
|
||||||
|
isPlanEnabled: vi.fn().mockReturnValue(false),
|
||||||
} as unknown as Mocked<Awaited<ReturnType<typeof loadCliConfig>>>;
|
} as unknown as Mocked<Awaited<ReturnType<typeof loadCliConfig>>>;
|
||||||
mockSettings = {
|
mockSettings = {
|
||||||
merged: {
|
merged: {
|
||||||
@@ -185,6 +188,59 @@ describe('GeminiAgent', () => {
|
|||||||
expect(mockConfig.getGeminiClient).toHaveBeenCalled();
|
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 () => {
|
it('should fail session creation if Gemini API key is missing', async () => {
|
||||||
(loadSettings as unknown as Mock).mockImplementation(() => ({
|
(loadSettings as unknown as Mock).mockImplementation(() => ({
|
||||||
merged: {
|
merged: {
|
||||||
@@ -306,6 +362,32 @@ describe('GeminiAgent', () => {
|
|||||||
expect(session.prompt).toHaveBeenCalled();
|
expect(session.prompt).toHaveBeenCalled();
|
||||||
expect(result).toEqual({ stopReason: 'end_turn' });
|
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', () => {
|
describe('Session', () => {
|
||||||
@@ -352,6 +434,8 @@ describe('Session', () => {
|
|||||||
getEnableRecursiveFileSearch: vi.fn().mockReturnValue(false),
|
getEnableRecursiveFileSearch: vi.fn().mockReturnValue(false),
|
||||||
getDebugMode: vi.fn().mockReturnValue(false),
|
getDebugMode: vi.fn().mockReturnValue(false),
|
||||||
getMessageBus: vi.fn().mockReturnValue(mockMessageBus),
|
getMessageBus: vi.fn().mockReturnValue(mockMessageBus),
|
||||||
|
setApprovalMode: vi.fn(),
|
||||||
|
isPlanEnabled: vi.fn().mockReturnValue(false),
|
||||||
} as unknown as Mocked<Config>;
|
} as unknown as Mocked<Config>;
|
||||||
mockConnection = {
|
mockConnection = {
|
||||||
sessionUpdate: vi.fn(),
|
sessionUpdate: vi.fn(),
|
||||||
@@ -822,4 +906,17 @@ describe('Session', () => {
|
|||||||
].value;
|
].value;
|
||||||
expect(mockInstance.build).toHaveBeenCalled();
|
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,
|
Kind,
|
||||||
partListUnionToString,
|
partListUnionToString,
|
||||||
LlmRole,
|
LlmRole,
|
||||||
|
ApprovalMode,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import * as acp from '@agentclientprotocol/sdk';
|
import * as acp from '@agentclientprotocol/sdk';
|
||||||
import { AcpFileSystemService } from './fileSystemService.js';
|
import { AcpFileSystemService } from './fileSystemService.js';
|
||||||
@@ -225,6 +226,10 @@ export class GeminiAgent {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
sessionId,
|
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
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
session.streamHistory(sessionData.messages);
|
session.streamHistory(sessionData.messages);
|
||||||
|
|
||||||
return {};
|
return {
|
||||||
|
modes: {
|
||||||
|
availableModes: buildAvailableModes(config.isPlanEnabled()),
|
||||||
|
currentModeId: config.getApprovalMode(),
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async initializeSessionConfig(
|
private async initializeSessionConfig(
|
||||||
@@ -377,6 +387,16 @@ export class GeminiAgent {
|
|||||||
}
|
}
|
||||||
return session.prompt(params);
|
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 {
|
export class Session {
|
||||||
@@ -398,6 +418,17 @@ export class Session {
|
|||||||
this.pendingPrompt = null;
|
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> {
|
async streamHistory(messages: ConversationRecord['messages']): Promise<void> {
|
||||||
for (const msg of messages) {
|
for (const msg of messages) {
|
||||||
const contentString = partListUnionToString(msg.content);
|
const contentString = partListUnionToString(msg.content);
|
||||||
@@ -1273,3 +1304,33 @@ function toAcpToolKind(kind: Kind): acp.ToolKind {
|
|||||||
return 'other';
|
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