mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-06-25 02:37:53 -07:00
feat(cli): add dynamic agent switcher and /agent subcommand
- Implemented /agent command with manage (opens dialog) and set subcommands. - Created AgentDialog component for choosing between standard and enterprise agents. - Integrated remember agent setting with persist toggle. - Wired --agent cli argument for booting directly into specific agent. - Handled clean session remounting on agent swap via React key-swapping of AppContainer, fully complying with Rules of Hooks. - Added agent-name persistently to the status bar footer items. - Resolved dynamic routing inside non-interactive headless loops to fully support Gemini Enterprise prompts. TAG=agy CONV=81e82460-f8cd-4c7b-a037-2cbedda4d3c0
This commit is contained in:
@@ -51,6 +51,7 @@ import {
|
||||
type Settings,
|
||||
type MergedSettings,
|
||||
saveModelChange,
|
||||
saveAgentChange,
|
||||
loadSettings,
|
||||
isWorktreeEnabled,
|
||||
type LoadedSettings,
|
||||
@@ -77,6 +78,7 @@ import { runExitCleanup } from '../utils/cleanup.js';
|
||||
export interface CliArgs {
|
||||
query: string | undefined;
|
||||
model: string | undefined;
|
||||
agent?: string;
|
||||
sandbox: boolean | string | undefined;
|
||||
debug: boolean | undefined;
|
||||
prompt: string | undefined;
|
||||
@@ -289,6 +291,12 @@ export async function parseArguments(
|
||||
nargs: 1,
|
||||
description: `Model`,
|
||||
})
|
||||
.option('agent', {
|
||||
alias: 'a',
|
||||
type: 'string',
|
||||
nargs: 1,
|
||||
description: `Agent to use: gemini-cli or gemini-enterprise`,
|
||||
})
|
||||
.option('prompt', {
|
||||
alias: 'p',
|
||||
type: 'string',
|
||||
@@ -840,6 +848,18 @@ export async function loadCliConfig(
|
||||
specifiedModel === GEMINI_MODEL_ALIAS_AUTO
|
||||
? defaultModel
|
||||
: specifiedModel || defaultModel;
|
||||
|
||||
const defaultAgent = 'gemini-cli';
|
||||
const rawAgent =
|
||||
argv.agent || process.env['GEMINI_CLI_AGENT'] || settings.agent?.name;
|
||||
|
||||
const specifiedAgent = Array.isArray(rawAgent)
|
||||
? String(rawAgent.at(-1) ?? '').trim() || ''
|
||||
: rawAgent === undefined
|
||||
? undefined
|
||||
: String(rawAgent ?? '').trim() || '';
|
||||
|
||||
const resolvedAgent = specifiedAgent || defaultAgent;
|
||||
const sandboxConfig = await loadSandboxConfig(settings, argv);
|
||||
if (sandboxConfig) {
|
||||
const existingPaths = sandboxConfig.allowedPaths || [];
|
||||
@@ -1014,6 +1034,7 @@ export async function loadCliConfig(
|
||||
fileDiscoveryService: fileService,
|
||||
bugCommand: settings.advanced?.bugCommand,
|
||||
model: resolvedModel,
|
||||
agent: resolvedAgent,
|
||||
maxSessionTurns: settings.model?.maxSessionTurns,
|
||||
|
||||
listExtensions: argv.listExtensions || false,
|
||||
@@ -1093,6 +1114,7 @@ export async function loadCliConfig(
|
||||
disabledHooks: settings.hooksConfig?.disabled || [],
|
||||
projectHooks: projectHooks || {},
|
||||
onModelChange: (model: string) => saveModelChange(loadSettings(cwd), model),
|
||||
onAgentChange: (agent: string) => saveAgentChange(loadSettings(cwd), agent),
|
||||
onReload: async () => {
|
||||
const refreshedSettings = loadSettings(cwd);
|
||||
return {
|
||||
|
||||
@@ -23,6 +23,7 @@ describe('footerItems', () => {
|
||||
'git-branch',
|
||||
'sandbox',
|
||||
'model-name',
|
||||
'agent-name',
|
||||
'quota',
|
||||
]);
|
||||
});
|
||||
@@ -91,6 +92,7 @@ describe('footerItems', () => {
|
||||
'git-branch',
|
||||
'sandbox',
|
||||
'context-used',
|
||||
'agent-name',
|
||||
'memory-usage',
|
||||
]);
|
||||
});
|
||||
@@ -117,6 +119,7 @@ describe('footerItems', () => {
|
||||
'git-branch',
|
||||
'sandbox',
|
||||
'context-used',
|
||||
'agent-name',
|
||||
'quota',
|
||||
'memory-usage',
|
||||
'session-id',
|
||||
|
||||
@@ -27,6 +27,11 @@ export const ALL_ITEMS = [
|
||||
header: '/model',
|
||||
description: 'Current model identifier',
|
||||
},
|
||||
{
|
||||
id: 'agent-name',
|
||||
header: '/agent',
|
||||
description: 'Current active agent name',
|
||||
},
|
||||
{
|
||||
id: 'context-used',
|
||||
header: 'context',
|
||||
@@ -76,6 +81,7 @@ export const DEFAULT_ORDER = [
|
||||
'git-branch',
|
||||
'sandbox',
|
||||
'model-name',
|
||||
'agent-name',
|
||||
'context-used',
|
||||
'quota',
|
||||
'memory-usage',
|
||||
@@ -94,6 +100,7 @@ export function deriveItemsFromLegacySettings(
|
||||
'git-branch',
|
||||
'sandbox',
|
||||
'model-name',
|
||||
'agent-name',
|
||||
'quota',
|
||||
];
|
||||
const items = [...defaults];
|
||||
|
||||
@@ -1197,6 +1197,22 @@ export function saveModelChange(
|
||||
}
|
||||
}
|
||||
|
||||
export function saveAgentChange(
|
||||
loadedSettings: LoadedSettings,
|
||||
agent: string,
|
||||
): void {
|
||||
try {
|
||||
loadedSettings.setValue(SettingScope.User, 'agent.name', agent);
|
||||
} catch (error) {
|
||||
const detailedErrorMessage = getFsErrorMessage(error);
|
||||
coreEvents.emitFeedback(
|
||||
'error',
|
||||
`Failed to save preferred agent: ${detailedErrorMessage}`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function migrateExperimentalSettings(
|
||||
settings: Settings,
|
||||
loadedSettings: LoadedSettings,
|
||||
|
||||
@@ -1043,6 +1043,27 @@ const SETTINGS_SCHEMA = {
|
||||
},
|
||||
},
|
||||
|
||||
agent: {
|
||||
type: 'object',
|
||||
label: 'Agent',
|
||||
category: 'Agent',
|
||||
requiresRestart: false,
|
||||
default: {},
|
||||
description: 'Settings related to the active AI agent.',
|
||||
showInDialog: false,
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
label: 'Agent Name',
|
||||
category: 'Agent',
|
||||
requiresRestart: false,
|
||||
default: 'gemini-cli',
|
||||
description: 'The active AI agent to use (gemini-cli or gemini-enterprise).',
|
||||
showInDialog: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
model: {
|
||||
type: 'object',
|
||||
label: 'Model',
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
type Config,
|
||||
type ResumedSessionData,
|
||||
coreEvents,
|
||||
CoreEvent,
|
||||
createWorkingStdio,
|
||||
disableMouseEvents,
|
||||
enableMouseEvents,
|
||||
@@ -101,6 +102,17 @@ export async function startInteractiveUI(
|
||||
// Create wrapper component to use hooks inside render
|
||||
const AppWrapper = () => {
|
||||
useKittyKeyboardProtocol();
|
||||
const [agentKey, setAgentKey] = React.useState(() => config.getAgent());
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleAgentChanged = (payload: { agent: string }) => {
|
||||
setAgentKey(payload.agent);
|
||||
};
|
||||
coreEvents.on(CoreEvent.AgentChanged, handleAgentChanged);
|
||||
return () => {
|
||||
coreEvents.off(CoreEvent.AgentChanged, handleAgentChanged);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<SettingsContext.Provider value={settings}>
|
||||
@@ -113,6 +125,7 @@ export async function startInteractiveUI(
|
||||
<SessionStatsProvider sessionId={config.getSessionId()}>
|
||||
<VimModeProvider>
|
||||
<AppContainer
|
||||
key={agentKey}
|
||||
config={config}
|
||||
startupWarnings={startupWarnings}
|
||||
version={version}
|
||||
|
||||
@@ -193,6 +193,7 @@ describe('runNonInteractive', () => {
|
||||
getRawOutput: vi.fn().mockReturnValue(false),
|
||||
getAcceptRawOutputRisk: vi.fn().mockReturnValue(false),
|
||||
getAgentSessionNoninteractiveEnabled: vi.fn().mockReturnValue(false),
|
||||
getAgent: vi.fn().mockReturnValue('gemini-cli'),
|
||||
} as unknown as Config;
|
||||
|
||||
mockSettings = {
|
||||
|
||||
@@ -67,6 +67,7 @@ export async function runNonInteractive(
|
||||
params: RunNonInteractiveParams,
|
||||
): Promise<void> {
|
||||
const useAgentSession =
|
||||
params.config.getAgent() === 'gemini-enterprise' ||
|
||||
params.config.getAgentSessionNoninteractiveEnabled() ||
|
||||
process.env['GEMINI_CLI_ENTERPRISE_AGENT'] === 'true';
|
||||
if (useAgentSession) {
|
||||
|
||||
@@ -199,6 +199,7 @@ describe('runNonInteractive', () => {
|
||||
getRawOutput: vi.fn().mockReturnValue(false),
|
||||
getAcceptRawOutputRisk: vi.fn().mockReturnValue(false),
|
||||
getAgentSessionNoninteractiveEnabled: vi.fn().mockReturnValue(false),
|
||||
getAgent: vi.fn().mockReturnValue('gemini-cli'),
|
||||
} as unknown as Config;
|
||||
|
||||
mockSettings = {
|
||||
|
||||
@@ -296,7 +296,9 @@ export async function runNonInteractive({
|
||||
});
|
||||
}
|
||||
|
||||
const useEnterprise = process.env['GEMINI_CLI_ENTERPRISE_AGENT'] === 'true';
|
||||
const useEnterprise =
|
||||
config.getAgent() === 'gemini-enterprise' ||
|
||||
process.env['GEMINI_CLI_ENTERPRISE_AGENT'] === 'true';
|
||||
// Create AgentSession — owns the agentic loop
|
||||
const session = useEnterprise
|
||||
? new EnterpriseAgentSession({ config, promptId: prompt_id })
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
AuthType,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { aboutCommand } from '../ui/commands/aboutCommand.js';
|
||||
import { agentCommand } from '../ui/commands/agentCommand.js';
|
||||
import { agentsCommand } from '../ui/commands/agentsCommand.js';
|
||||
import { authCommand } from '../ui/commands/authCommand.js';
|
||||
import { bugCommand } from '../ui/commands/bugCommand.js';
|
||||
@@ -122,6 +123,7 @@ export class BuiltinCommandLoader implements ICommandLoader {
|
||||
|
||||
const allDefinitions: Array<SlashCommand | null> = [
|
||||
aboutCommand,
|
||||
agentCommand,
|
||||
...(this.config?.isAgentsEnabled() ? [agentsCommand] : []),
|
||||
authCommand,
|
||||
bugCommand,
|
||||
|
||||
@@ -509,6 +509,7 @@ const baseMockUiState = {
|
||||
terminalWidth: 100,
|
||||
terminalHeight: 40,
|
||||
currentModel: 'gemini-pro',
|
||||
activeAgent: 'gemini-cli',
|
||||
terminalBackgroundColor: 'black' as const,
|
||||
cleanUiDetailsVisible: false,
|
||||
allowPlanMode: true,
|
||||
@@ -552,6 +553,8 @@ const mockUIActions: UIActions = {
|
||||
exitPrivacyNotice: vi.fn(),
|
||||
closeSettingsDialog: vi.fn(),
|
||||
closeModelDialog: vi.fn(),
|
||||
closeAgentDialog: vi.fn(),
|
||||
handleAgentSelect: vi.fn(),
|
||||
openVoiceModelDialog: vi.fn(),
|
||||
closeVoiceModelDialog: vi.fn(),
|
||||
openAgentConfigDialog: vi.fn(),
|
||||
|
||||
@@ -103,6 +103,7 @@ import { useQuotaAndFallback } from './hooks/useQuotaAndFallback.js';
|
||||
import { useEditorSettings } from './hooks/useEditorSettings.js';
|
||||
import { useSettingsCommand } from './hooks/useSettingsCommand.js';
|
||||
import { useModelCommand } from './hooks/useModelCommand.js';
|
||||
import { useAgentCommand } from './hooks/useAgentCommand.js';
|
||||
import { useVoiceModelCommand } from './hooks/useVoiceModelCommand.js';
|
||||
import { useSlashCommandProcessor } from './hooks/slashCommandProcessor.js';
|
||||
import { useVimMode } from './contexts/VimModeContext.js';
|
||||
@@ -147,6 +148,7 @@ import { relaunchApp } from '../utils/processUtils.js';
|
||||
import type { SessionInfo } from '../utils/sessionUtils.js';
|
||||
import { useMessageQueue } from './hooks/useMessageQueue.js';
|
||||
import { useMcpStatus } from './hooks/useMcpStatus.js';
|
||||
import { ConsentPrompt } from './components/ConsentPrompt.js';
|
||||
import { useApprovalModeIndicator } from './hooks/useApprovalModeIndicator.js';
|
||||
import { useSessionStats } from './contexts/SessionContext.js';
|
||||
import { useGitBranchName } from './hooks/useGitBranchName.js';
|
||||
@@ -938,6 +940,9 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
|
||||
const { isModelDialogOpen, openModelDialog, closeModelDialog } =
|
||||
useModelCommand();
|
||||
const { isAgentDialogOpen, openAgentDialog, closeAgentDialog } =
|
||||
useAgentCommand();
|
||||
const activeAgent = config?.getAgent() || 'gemini-cli';
|
||||
|
||||
const {
|
||||
isVoiceModelDialogOpen,
|
||||
@@ -968,6 +973,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
openSettingsDialog,
|
||||
openSessionBrowser,
|
||||
openModelDialog,
|
||||
openAgentDialog,
|
||||
openVoiceModelDialog,
|
||||
openAgentConfigDialog,
|
||||
openPermissionsDialog,
|
||||
@@ -1007,6 +1013,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
openSettingsDialog,
|
||||
openSessionBrowser,
|
||||
openModelDialog,
|
||||
openAgentDialog,
|
||||
openVoiceModelDialog,
|
||||
openAgentConfigDialog,
|
||||
setQuittingMessages,
|
||||
@@ -1175,14 +1182,14 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
|
||||
const streamAgent = useMemo(
|
||||
() => {
|
||||
if (process.env['GEMINI_CLI_ENTERPRISE_AGENT'] === 'true') {
|
||||
if (activeAgent === 'gemini-enterprise') {
|
||||
return new EnterpriseAgentProtocol({ config });
|
||||
}
|
||||
return config?.getAgentSessionInteractiveEnabled()
|
||||
? new LegacyAgentProtocol({ config, getPreferredEditor })
|
||||
: undefined;
|
||||
},
|
||||
[config, getPreferredEditor],
|
||||
[config, getPreferredEditor, activeAgent],
|
||||
);
|
||||
|
||||
const activeStream = streamAgent
|
||||
@@ -1238,6 +1245,65 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
retryStatus,
|
||||
} = activeStream;
|
||||
|
||||
const handleAgentSelect = useCallback(
|
||||
(agentName: string) => {
|
||||
const doSwap = () => {
|
||||
config.setAgent(agentName, false);
|
||||
|
||||
// 1. Reset UI history
|
||||
historyManager.clearItems();
|
||||
refreshStatic();
|
||||
setBannerVisible(false);
|
||||
|
||||
// 2. Reset streaming loop state
|
||||
if (activeStream && 'reset' in activeStream) {
|
||||
(activeStream as { reset: () => void }).reset();
|
||||
}
|
||||
};
|
||||
|
||||
const isSessionNonEmpty = historyManager.history.some(
|
||||
(item) => item.type === 'user' || item.type === 'gemini',
|
||||
);
|
||||
|
||||
if (isSessionNonEmpty) {
|
||||
setCustomDialog(
|
||||
<ConsentPrompt
|
||||
prompt={`Switching agents will completely clear your active session history. Do you want to proceed to switch to ${agentName}?`}
|
||||
onConfirm={(confirm: boolean) => {
|
||||
setCustomDialog(null);
|
||||
if (confirm) {
|
||||
doSwap();
|
||||
}
|
||||
}}
|
||||
terminalWidth={terminalWidth}
|
||||
/>,
|
||||
);
|
||||
} else {
|
||||
doSwap();
|
||||
}
|
||||
},
|
||||
[
|
||||
config,
|
||||
historyManager,
|
||||
refreshStatic,
|
||||
setBannerVisible,
|
||||
activeStream,
|
||||
setCustomDialog,
|
||||
terminalWidth,
|
||||
],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const handleAgentChanged = (payload: { agent: string }) => {
|
||||
handleAgentSelect(payload.agent);
|
||||
};
|
||||
|
||||
coreEvents.on(CoreEvent.AgentChanged, handleAgentChanged);
|
||||
return () => {
|
||||
coreEvents.off(CoreEvent.AgentChanged, handleAgentChanged);
|
||||
};
|
||||
}, [handleAgentSelect]);
|
||||
|
||||
const pendingHistoryItems = useMemo(
|
||||
() => [...pendingSlashCommandHistoryItems, ...pendingGeminiHistoryItems],
|
||||
[pendingSlashCommandHistoryItems, pendingGeminiHistoryItems],
|
||||
@@ -2215,6 +2281,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
isThemeDialogOpen ||
|
||||
isSettingsDialogOpen ||
|
||||
isModelDialogOpen ||
|
||||
isAgentDialogOpen ||
|
||||
isVoiceModelDialogOpen ||
|
||||
isAgentConfigDialogOpen ||
|
||||
isPermissionsDialogOpen ||
|
||||
@@ -2476,6 +2543,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
isSettingsDialogOpen,
|
||||
isSessionBrowserOpen,
|
||||
isModelDialogOpen,
|
||||
isAgentDialogOpen,
|
||||
isVoiceModelDialogOpen,
|
||||
isAgentConfigDialogOpen,
|
||||
selectedAgentName,
|
||||
@@ -2526,6 +2594,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
showApprovalModeIndicator,
|
||||
allowPlanMode,
|
||||
currentModel,
|
||||
activeAgent,
|
||||
contextFileNames,
|
||||
errorCount,
|
||||
availableTerminalHeight,
|
||||
@@ -2589,6 +2658,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
isSettingsDialogOpen,
|
||||
isSessionBrowserOpen,
|
||||
isModelDialogOpen,
|
||||
isAgentDialogOpen,
|
||||
isVoiceModelDialogOpen,
|
||||
isAgentConfigDialogOpen,
|
||||
selectedAgentName,
|
||||
@@ -2660,6 +2730,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
ideTrustRestartReason,
|
||||
isRestarting,
|
||||
currentModel,
|
||||
activeAgent,
|
||||
extensionsUpdateState,
|
||||
activePtyId,
|
||||
backgroundTaskCount,
|
||||
@@ -2705,6 +2776,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
exitPrivacyNotice,
|
||||
closeSettingsDialog,
|
||||
closeModelDialog,
|
||||
closeAgentDialog,
|
||||
handleAgentSelect,
|
||||
openVoiceModelDialog,
|
||||
closeVoiceModelDialog,
|
||||
openAgentConfigDialog,
|
||||
@@ -2807,6 +2880,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
exitPrivacyNotice,
|
||||
closeSettingsDialog,
|
||||
closeModelDialog,
|
||||
closeAgentDialog,
|
||||
handleAgentSelect,
|
||||
openVoiceModelDialog,
|
||||
closeVoiceModelDialog,
|
||||
openAgentConfigDialog,
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { agentCommand } from './agentCommand.js';
|
||||
import { type CommandContext } from './types.js';
|
||||
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
import { MessageType } from '../types.js';
|
||||
|
||||
describe('agentCommand', () => {
|
||||
let mockContext: CommandContext;
|
||||
|
||||
beforeEach(() => {
|
||||
mockContext = createMockCommandContext();
|
||||
});
|
||||
|
||||
it('should return a dialog action to open the agent dialog when no args', async () => {
|
||||
if (!agentCommand.action) {
|
||||
throw new Error('The agent command must have an action.');
|
||||
}
|
||||
|
||||
const result = await agentCommand.action(mockContext, '');
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'dialog',
|
||||
dialog: 'agent',
|
||||
});
|
||||
});
|
||||
|
||||
describe('manage subcommand', () => {
|
||||
it('should return a dialog action to open the agent dialog', async () => {
|
||||
const manageCommand = agentCommand.subCommands?.find(
|
||||
(c) => c.name === 'manage',
|
||||
);
|
||||
expect(manageCommand).toBeDefined();
|
||||
|
||||
const result = await manageCommand!.action!(mockContext, '');
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'dialog',
|
||||
dialog: 'agent',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('set subcommand', () => {
|
||||
it('should set the agent and log the command', async () => {
|
||||
const setCommand = agentCommand.subCommands?.find(
|
||||
(c) => c.name === 'set',
|
||||
);
|
||||
expect(setCommand).toBeDefined();
|
||||
|
||||
const mockSetAgent = vi.fn();
|
||||
mockContext.services.agentContext = {
|
||||
setAgent: mockSetAgent,
|
||||
get config() {
|
||||
return this;
|
||||
},
|
||||
} as unknown as Config;
|
||||
|
||||
await setCommand!.action!(mockContext, 'gemini-enterprise');
|
||||
|
||||
expect(mockSetAgent).toHaveBeenCalledWith('gemini-enterprise', true);
|
||||
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: MessageType.INFO,
|
||||
text: expect.stringContaining('Agent set to gemini-enterprise'),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should set the agent with persistence when --persist is used', async () => {
|
||||
const setCommand = agentCommand.subCommands?.find(
|
||||
(c) => c.name === 'set',
|
||||
);
|
||||
const mockSetAgent = vi.fn();
|
||||
mockContext.services.agentContext = {
|
||||
setAgent: mockSetAgent,
|
||||
get config() {
|
||||
return this;
|
||||
},
|
||||
} as unknown as Config;
|
||||
|
||||
await setCommand!.action!(mockContext, 'gemini-cli --persist');
|
||||
|
||||
expect(mockSetAgent).toHaveBeenCalledWith('gemini-cli', false);
|
||||
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: MessageType.INFO,
|
||||
text: expect.stringContaining('Agent set to gemini-cli (persisted)'),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should show error if no agent name is provided', async () => {
|
||||
const setCommand = agentCommand.subCommands?.find(
|
||||
(c) => c.name === 'set',
|
||||
);
|
||||
await setCommand!.action!(mockContext, '');
|
||||
|
||||
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: MessageType.ERROR,
|
||||
text: expect.stringContaining('Usage: /agent set <agent-name>'),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should show error if invalid agent name is provided', async () => {
|
||||
const setCommand = agentCommand.subCommands?.find(
|
||||
(c) => c.name === 'set',
|
||||
);
|
||||
mockContext.services.agentContext = {
|
||||
setAgent: vi.fn(),
|
||||
get config() {
|
||||
return this;
|
||||
},
|
||||
} as unknown as Config;
|
||||
|
||||
await setCommand!.action!(mockContext, 'invalid-agent');
|
||||
|
||||
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: MessageType.ERROR,
|
||||
text: expect.stringContaining('Unknown agent: invalid-agent'),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should have the correct name and description', () => {
|
||||
expect(agentCommand.name).toBe('agent');
|
||||
expect(agentCommand.description).toBe('Manage active AI agent configuration');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {
|
||||
type CommandContext,
|
||||
CommandKind,
|
||||
type SlashCommand,
|
||||
} from './types.js';
|
||||
import { MessageType } from '../types.js';
|
||||
|
||||
const setAgentCommand: SlashCommand = {
|
||||
name: 'set',
|
||||
description:
|
||||
'Set the active agent to use. Usage: /agent set <agent-name> [--persist]',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: false,
|
||||
action: async (context: CommandContext, args: string) => {
|
||||
const parts = args.trim().split(/\s+/).filter(Boolean);
|
||||
if (parts.length === 0) {
|
||||
context.ui.addItem({
|
||||
type: MessageType.ERROR,
|
||||
text: 'Usage: /agent set <agent-name> [--persist]',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const agentName = parts[0];
|
||||
const persist = parts.includes('--persist');
|
||||
|
||||
if (context.services.agentContext?.config) {
|
||||
if (agentName !== 'gemini-cli' && agentName !== 'gemini-enterprise') {
|
||||
context.ui.addItem({
|
||||
type: MessageType.ERROR,
|
||||
text: `Unknown agent: ${agentName}. Valid agents are 'gemini-cli' and 'gemini-enterprise'.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
context.services.agentContext.config.setAgent(agentName, !persist);
|
||||
|
||||
context.ui.addItem({
|
||||
type: MessageType.INFO,
|
||||
text: `Agent set to ${agentName}${persist ? ' (persisted)' : ''}`,
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const manageAgentCommand: SlashCommand = {
|
||||
name: 'manage',
|
||||
description: 'Opens a dialog to select the agent',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: async (context: CommandContext) => {
|
||||
return {
|
||||
type: 'dialog',
|
||||
dialog: 'agent',
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const agentCommand: SlashCommand = {
|
||||
name: 'agent',
|
||||
description: 'Manage active AI agent configuration',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: false,
|
||||
subCommands: [manageAgentCommand, setAgentCommand],
|
||||
action: async (context: CommandContext, args: string) =>
|
||||
manageAgentCommand.action!(context, args),
|
||||
};
|
||||
@@ -128,6 +128,7 @@ export interface OpenDialogActionReturn {
|
||||
| 'settings'
|
||||
| 'sessionBrowser'
|
||||
| 'model'
|
||||
| 'agent'
|
||||
| 'voice-model'
|
||||
| 'agentConfig'
|
||||
| 'permissions';
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { act } from 'react';
|
||||
import { AgentDialog } from './AgentDialog.js';
|
||||
import { renderWithProviders } from '../../test-utils/render.js';
|
||||
import { waitFor } from '../../test-utils/async.js';
|
||||
import { createMockSettings } from '../../test-utils/settings.js';
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
|
||||
describe('<AgentDialog />', () => {
|
||||
const mockSetAgent = vi.fn();
|
||||
const mockGetAgent = vi.fn();
|
||||
const mockOnClose = vi.fn();
|
||||
const mockOnSelect = vi.fn();
|
||||
|
||||
interface MockConfig extends Partial<Config> {
|
||||
setAgent: (agent: string, isTemporary?: boolean) => void;
|
||||
getAgent: () => string;
|
||||
getSessionId: () => string;
|
||||
getIdeMode: () => boolean;
|
||||
}
|
||||
|
||||
const mockConfig: MockConfig = {
|
||||
setAgent: mockSetAgent,
|
||||
getAgent: mockGetAgent,
|
||||
getSessionId: () => 'test-session-id',
|
||||
getIdeMode: () => false,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
mockGetAgent.mockReturnValue('gemini-cli');
|
||||
});
|
||||
|
||||
const renderComponent = async (configValue = mockConfig as Config) => {
|
||||
const settings = createMockSettings({});
|
||||
|
||||
const result = await renderWithProviders(
|
||||
<AgentDialog onClose={mockOnClose} onSelect={mockOnSelect} />,
|
||||
{
|
||||
config: configValue,
|
||||
settings,
|
||||
},
|
||||
);
|
||||
return result;
|
||||
};
|
||||
|
||||
it('renders the agent selection view correctly', async () => {
|
||||
const { lastFrame, unmount } = await renderComponent();
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('Select Agent');
|
||||
expect(output).toContain('Remember agent for future sessions: false');
|
||||
expect(output).toContain('Gemini CLI (Standard)');
|
||||
expect(output).toContain('Gemini Enterprise');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('selects agent and closes when an option is selected', async () => {
|
||||
const { stdin, waitUntilReady, unmount } = await renderComponent();
|
||||
|
||||
// Select Gemini CLI (default selection is first item) by pressing Enter
|
||||
await act(async () => {
|
||||
stdin.write('\r');
|
||||
});
|
||||
await waitUntilReady();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSetAgent).toHaveBeenCalledWith('gemini-cli', true); // Ephemeral by default
|
||||
expect(mockOnSelect).toHaveBeenCalledWith('gemini-cli');
|
||||
expect(mockOnClose).toHaveBeenCalled();
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('selects the second agent when navigating and pressing enter', async () => {
|
||||
const { stdin, waitUntilReady, unmount } = await renderComponent();
|
||||
|
||||
// Press arrow down to move selection to index 1 (gemini-enterprise)
|
||||
await act(async () => {
|
||||
stdin.write('\u001B[B'); // Arrow Down
|
||||
});
|
||||
await waitUntilReady();
|
||||
|
||||
// Press enter to select
|
||||
await act(async () => {
|
||||
stdin.write('\r');
|
||||
});
|
||||
await waitUntilReady();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSetAgent).toHaveBeenCalledWith('gemini-enterprise', true);
|
||||
expect(mockOnSelect).toHaveBeenCalledWith('gemini-enterprise');
|
||||
expect(mockOnClose).toHaveBeenCalled();
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('toggles persist mode with Tab key', async () => {
|
||||
const { lastFrame, stdin, waitUntilReady, unmount } = await renderComponent();
|
||||
|
||||
expect(lastFrame()).toContain('Remember agent for future sessions: false');
|
||||
|
||||
// Press Tab to toggle persist mode
|
||||
await act(async () => {
|
||||
stdin.write('\t');
|
||||
});
|
||||
await waitUntilReady();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(lastFrame()).toContain('Remember agent for future sessions: true');
|
||||
});
|
||||
|
||||
// Select first item (gemini-cli) with persist mode active
|
||||
await act(async () => {
|
||||
stdin.write('\r');
|
||||
});
|
||||
await waitUntilReady();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSetAgent).toHaveBeenCalledWith('gemini-cli', false); // Persist enabled (isTemporary = false)
|
||||
expect(mockOnSelect).toHaveBeenCalledWith('gemini-cli');
|
||||
expect(mockOnClose).toHaveBeenCalled();
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('closes dialog on escape key press', async () => {
|
||||
const { stdin, waitUntilReady, unmount } = await renderComponent();
|
||||
|
||||
await act(async () => {
|
||||
stdin.write('\u001B'); // Escape
|
||||
});
|
||||
await act(async () => {
|
||||
await waitUntilReady();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockOnClose).toHaveBeenCalled();
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { useCallback, useContext, useMemo, useState } from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { DescriptiveRadioButtonSelect } from './shared/DescriptiveRadioButtonSelect.js';
|
||||
import { ConfigContext } from '../contexts/ConfigContext.js';
|
||||
|
||||
interface AgentDialogProps {
|
||||
onClose: () => void;
|
||||
onSelect: (agentName: string) => void;
|
||||
}
|
||||
|
||||
export function AgentDialog({ onClose, onSelect }: AgentDialogProps): React.JSX.Element {
|
||||
const config = useContext(ConfigContext);
|
||||
const [persistMode, setPersistMode] = useState(false);
|
||||
|
||||
const preferredAgent = config?.getAgent() || 'gemini-cli';
|
||||
|
||||
useKeypress(
|
||||
(key) => {
|
||||
if (key.name === 'escape') {
|
||||
onClose();
|
||||
return true;
|
||||
}
|
||||
if (key.name === 'tab') {
|
||||
setPersistMode((prev) => !prev);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
{ isActive: true },
|
||||
);
|
||||
|
||||
const options = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
value: 'gemini-cli',
|
||||
title: 'Gemini CLI (Standard)',
|
||||
description: 'Standard model conversational chat with local tools execution.',
|
||||
key: 'gemini-cli',
|
||||
},
|
||||
{
|
||||
value: 'gemini-enterprise',
|
||||
title: 'Gemini Enterprise',
|
||||
description: 'Connected assistant grounded by your business data.',
|
||||
key: 'gemini-enterprise',
|
||||
},
|
||||
];
|
||||
}, []);
|
||||
|
||||
const initialIndex = useMemo(() => {
|
||||
const idx = options.findIndex((option) => option.value === preferredAgent);
|
||||
return idx !== -1 ? idx : 0;
|
||||
}, [preferredAgent, options]);
|
||||
|
||||
const handleSelect = useCallback(
|
||||
(agentName: string) => {
|
||||
if (config) {
|
||||
config.setAgent(agentName, persistMode ? false : true);
|
||||
}
|
||||
onSelect(agentName);
|
||||
onClose();
|
||||
},
|
||||
[config, onSelect, onClose, persistMode],
|
||||
);
|
||||
|
||||
return (
|
||||
<Box
|
||||
borderStyle="round"
|
||||
borderColor={theme.border.default}
|
||||
flexDirection="column"
|
||||
padding={1}
|
||||
width="100%"
|
||||
>
|
||||
<Text bold>Select Agent</Text>
|
||||
|
||||
<Box marginTop={1}>
|
||||
<DescriptiveRadioButtonSelect
|
||||
items={options}
|
||||
onSelect={handleSelect}
|
||||
initialIndex={initialIndex}
|
||||
showNumbers={true}
|
||||
/>
|
||||
</Box>
|
||||
<Box marginTop={1} flexDirection="column">
|
||||
<Box>
|
||||
<Text bold color={theme.text.primary}>
|
||||
Remember agent for future sessions:{' '}
|
||||
</Text>
|
||||
<Text color={theme.status.success}>
|
||||
{persistMode ? 'true' : 'false'}
|
||||
</Text>
|
||||
<Text color={theme.text.secondary}> (Press Tab to toggle)</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box flexDirection="column">
|
||||
<Text color={theme.text.secondary}>
|
||||
{'> To use a specific agent on startup, use the --agent flag.'}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box marginTop={1} flexDirection="column">
|
||||
<Text color={theme.text.secondary}>(Press Esc to close)</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import { relaunchApp } from '../../utils/processUtils.js';
|
||||
import { SessionBrowser } from './SessionBrowser.js';
|
||||
import { PermissionsModifyTrustDialog } from './PermissionsModifyTrustDialog.js';
|
||||
import { ModelDialog } from './ModelDialog.js';
|
||||
import { AgentDialog } from './AgentDialog.js';
|
||||
import { VoiceModelDialog } from './VoiceModelDialog.js';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { useUIState } from '../contexts/UIStateContext.js';
|
||||
@@ -240,6 +241,14 @@ export const DialogManager = ({
|
||||
if (uiState.isModelDialogOpen) {
|
||||
return <ModelDialog onClose={uiActions.closeModelDialog} />;
|
||||
}
|
||||
if (uiState.isAgentDialogOpen) {
|
||||
return (
|
||||
<AgentDialog
|
||||
onClose={uiActions.closeAgentDialog}
|
||||
onSelect={uiActions.handleAgentSelect}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (uiState.isVoiceModelDialogOpen) {
|
||||
return <VoiceModelDialog onClose={uiActions.closeVoiceModelDialog} />;
|
||||
}
|
||||
|
||||
@@ -198,6 +198,7 @@ export const Footer: React.FC = () => {
|
||||
|
||||
const {
|
||||
model,
|
||||
activeAgent,
|
||||
targetDir,
|
||||
debugMode,
|
||||
branchName,
|
||||
@@ -210,6 +211,7 @@ export const Footer: React.FC = () => {
|
||||
terminalWidth,
|
||||
} = {
|
||||
model: uiState.currentModel,
|
||||
activeAgent: uiState.activeAgent,
|
||||
targetDir: config.getTargetDir(),
|
||||
debugMode: config.getDebugMode(),
|
||||
branchName: uiState.branchName,
|
||||
@@ -332,6 +334,15 @@ export const Footer: React.FC = () => {
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'agent-name': {
|
||||
addCol(
|
||||
id,
|
||||
header,
|
||||
() => <Text color={itemColor}>{activeAgent}</Text>,
|
||||
activeAgent.length,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'context-used': {
|
||||
addCol(
|
||||
id,
|
||||
|
||||
@@ -1,45 +1,49 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`<Footer /> > displays "Limit reached" message when remaining is 0 1`] = `
|
||||
" workspace (/directory) sandbox /model quota
|
||||
~/project/foo/bar/and/some/more/directories/to/make/it/long no sandbox gemini-pro limit reached
|
||||
" workspace (/directory) sandbox /model /agent quota
|
||||
~/project/foo/bar/and/some/more/directories/to/make/it/long no sandbox gemini-pro gemini-cli limit reached
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<Footer /> > displays the usage indicator when usage is low 1`] = `
|
||||
" workspace (/directory) sandbox /model quota
|
||||
~/project/foo/bar/and/some/more/directories/to/make/it/long no sandbox gemini-pro 85% used
|
||||
" workspace (/directory) sandbox /model /agent quota
|
||||
~/project/foo/bar/and/some/more/directories/to/make/it/long no sandbox gemini-pro gemini-cli 85% used
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<Footer /> > footer configuration filtering (golden snapshots) > renders complete footer in narrow terminal (baseline narrow) > complete-footer-narrow 1`] = `
|
||||
" workspace (/directory) sandbox /model context
|
||||
...me/more/directories/to/make/it/long no sandbox gemini-pro 14%
|
||||
" workspace (/directory) sandbox /model context /agent
|
||||
...tories/to/make/it/long no sandbox gemini-pro 14% gemini-cli
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<Footer /> > footer configuration filtering (golden snapshots) > renders complete footer with all sections visible (baseline) > complete-footer-wide 1`] = `
|
||||
" workspace (/directory) sandbox /model context
|
||||
~/project/foo/bar/and/some/more/directories/to/make/it/long no sandbox gemini-pro 14% used
|
||||
" workspace (/directory) sandbox /model context /agent
|
||||
~/project/foo/bar/and/some/more/directories/to/make/it/long no sandbox gemini-pro 14% used gemini-cli
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<Footer /> > footer configuration filtering (golden snapshots) > renders footer with CWD and model info hidden to test alignment (only sandbox visible) > footer-only-sandbox 1`] = `
|
||||
" sandbox
|
||||
no sandbox
|
||||
" sandbox /agent
|
||||
no sandbox gemini-cli
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<Footer /> > footer configuration filtering (golden snapshots) > renders footer with all optional sections hidden (minimal footer) > footer-minimal 1`] = `""`;
|
||||
exports[`<Footer /> > footer configuration filtering (golden snapshots) > renders footer with all optional sections hidden (minimal footer) > footer-minimal 1`] = `
|
||||
" /agent
|
||||
gemini-cli
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<Footer /> > footer configuration filtering (golden snapshots) > renders footer with only model info hidden (partial filtering) > footer-no-model 1`] = `
|
||||
" workspace (/directory) sandbox
|
||||
~/project/foo/bar/and/some/more/directories/to/make/it/long no sandbox
|
||||
" workspace (/directory) sandbox /agent
|
||||
~/project/foo/bar/and/some/more/directories/to/make/it/long no sandbox gemini-cli
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<Footer /> > hides the usage indicator when usage is not near limit 1`] = `
|
||||
" workspace (/directory) sandbox /model quota
|
||||
~/project/foo/bar/and/some/more/directories/to/make/it/long no sandbox gemini-pro 15% used
|
||||
" workspace (/directory) sandbox /model /agent quota
|
||||
~/project/foo/bar/and/some/more/directories/to/make/it/long no sandbox gemini-pro gemini-cli 15% used
|
||||
"
|
||||
`;
|
||||
|
||||
@@ -41,6 +41,8 @@ export interface UIActions {
|
||||
exitPrivacyNotice: () => void;
|
||||
closeSettingsDialog: () => void;
|
||||
closeModelDialog: () => void;
|
||||
closeAgentDialog: () => void;
|
||||
handleAgentSelect: (agentName: string) => void;
|
||||
openVoiceModelDialog: () => void;
|
||||
closeVoiceModelDialog: () => void;
|
||||
openAgentConfigDialog: (
|
||||
|
||||
@@ -114,6 +114,7 @@ export interface UIState {
|
||||
isSettingsDialogOpen: boolean;
|
||||
isSessionBrowserOpen: boolean;
|
||||
isModelDialogOpen: boolean;
|
||||
isAgentDialogOpen: boolean;
|
||||
isVoiceModelDialogOpen: boolean;
|
||||
isAgentConfigDialogOpen: boolean;
|
||||
selectedAgentName?: string;
|
||||
@@ -162,6 +163,7 @@ export interface UIState {
|
||||
showApprovalModeIndicator: ApprovalMode;
|
||||
allowPlanMode: boolean;
|
||||
currentModel: string;
|
||||
activeAgent: string;
|
||||
contextFileNames: string[];
|
||||
errorCount: number;
|
||||
availableTerminalHeight: number | undefined;
|
||||
|
||||
@@ -205,6 +205,7 @@ describe('useSlashCommandProcessor', () => {
|
||||
openSettingsDialog: vi.fn(),
|
||||
openSessionBrowser: vi.fn(),
|
||||
openModelDialog: mockOpenModelDialog,
|
||||
openAgentDialog: vi.fn(),
|
||||
openVoiceModelDialog: vi.fn(),
|
||||
openAgentConfigDialog,
|
||||
openPermissionsDialog: vi.fn(),
|
||||
|
||||
@@ -72,6 +72,7 @@ interface SlashCommandProcessorActions {
|
||||
openSettingsDialog: () => void;
|
||||
openSessionBrowser: () => void;
|
||||
openModelDialog: () => void;
|
||||
openAgentDialog: () => void;
|
||||
openVoiceModelDialog: () => void;
|
||||
openAgentConfigDialog: (
|
||||
name: string,
|
||||
@@ -506,6 +507,9 @@ export const useSlashCommandProcessor = (
|
||||
case 'model':
|
||||
actions.openModelDialog();
|
||||
return { type: 'handled' };
|
||||
case 'agent':
|
||||
actions.openAgentDialog();
|
||||
return { type: 'handled' };
|
||||
case 'voice-model':
|
||||
actions.openVoiceModelDialog();
|
||||
return { type: 'handled' };
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
|
||||
interface UseAgentCommandReturn {
|
||||
isAgentDialogOpen: boolean;
|
||||
openAgentDialog: () => void;
|
||||
closeAgentDialog: () => void;
|
||||
}
|
||||
|
||||
export const useAgentCommand = (): UseAgentCommandReturn => {
|
||||
const [isAgentDialogOpen, setIsAgentDialogOpen] = useState(false);
|
||||
|
||||
const openAgentDialog = useCallback(() => {
|
||||
setIsAgentDialogOpen(true);
|
||||
}, []);
|
||||
|
||||
const closeAgentDialog = useCallback(() => {
|
||||
setIsAgentDialogOpen(false);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
isAgentDialogOpen,
|
||||
openAgentDialog,
|
||||
closeAgentDialog,
|
||||
};
|
||||
};
|
||||
@@ -569,8 +569,31 @@ export const useAgentStream = ({
|
||||
[pendingHistoryItem, pendingToolGroupItems],
|
||||
);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
setStreamingState(StreamingState.Idle);
|
||||
setThought(null);
|
||||
setPendingHistoryItem(null);
|
||||
setTrackedTools([]);
|
||||
setPushedToolCallIds(new Set());
|
||||
setIsFirstToolInGroup(true);
|
||||
setHasEmittedBoxInTurn(false);
|
||||
|
||||
currentStreamIdRef.current = null;
|
||||
userMessageTimestampRef.current = 0;
|
||||
geminiMessageBufferRef.current = '';
|
||||
}, [
|
||||
setStreamingState,
|
||||
setThought,
|
||||
setPendingHistoryItem,
|
||||
setTrackedTools,
|
||||
setPushedToolCallIds,
|
||||
setIsFirstToolInGroup,
|
||||
setHasEmittedBoxInTurn,
|
||||
]);
|
||||
|
||||
return {
|
||||
streamingState,
|
||||
reset,
|
||||
submitQuery,
|
||||
initError,
|
||||
pendingHistoryItems,
|
||||
|
||||
@@ -643,6 +643,7 @@ export interface ConfigParameters {
|
||||
includeDirectories?: string[];
|
||||
bugCommand?: BugCommandSettings;
|
||||
model: string;
|
||||
agent?: string;
|
||||
disableLoopDetection?: boolean;
|
||||
maxSessionTurns?: number;
|
||||
acpMode?: boolean;
|
||||
@@ -734,6 +735,7 @@ export interface ConfigParameters {
|
||||
worktreeSettings?: WorktreeSettings;
|
||||
modelSteering?: boolean;
|
||||
onModelChange?: (model: string) => void;
|
||||
onAgentChange?: (agent: string) => void;
|
||||
mcpEnabled?: boolean;
|
||||
extensionsEnabled?: boolean;
|
||||
agents?: AgentSettings;
|
||||
@@ -831,6 +833,7 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
private readonly cwd: string;
|
||||
private readonly bugCommand: BugCommandSettings | undefined;
|
||||
private model: string;
|
||||
private agent: string;
|
||||
private readonly disableLoopDetection: boolean;
|
||||
// null = unknown (quota not fetched); true = has access; false = definitively no access
|
||||
private hasAccessToPreviewModel: boolean | null = null;
|
||||
@@ -947,6 +950,7 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
private experimentsPromise: Promise<Experiments | undefined> | undefined;
|
||||
private hookSystem?: HookSystem;
|
||||
private readonly onModelChange: ((model: string) => void) | undefined;
|
||||
private readonly onAgentChange: ((agent: string) => void) | undefined;
|
||||
private readonly onReload:
|
||||
| (() => Promise<{
|
||||
disabledSkills?: string[];
|
||||
@@ -1126,6 +1130,7 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
this.fileDiscoveryService = params.fileDiscoveryService ?? null;
|
||||
this.bugCommand = params.bugCommand;
|
||||
this.model = params.model;
|
||||
this.agent = params.agent ?? 'gemini-cli';
|
||||
this.disableLoopDetection = params.disableLoopDetection ?? false;
|
||||
this._activeModel = params.model;
|
||||
this.enableAgents = params.enableAgents ?? true;
|
||||
@@ -1390,6 +1395,7 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
|
||||
this.experiments = params.experiments;
|
||||
this.onModelChange = params.onModelChange;
|
||||
this.onAgentChange = params.onAgentChange;
|
||||
this.onReload = params.onReload;
|
||||
|
||||
this.billing = {
|
||||
@@ -1913,6 +1919,20 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
return this.model;
|
||||
}
|
||||
|
||||
getAgent(): string {
|
||||
return this.agent;
|
||||
}
|
||||
|
||||
setAgent(newAgent: string, isTemporary = true): void {
|
||||
if (this.agent !== newAgent) {
|
||||
this.agent = newAgent;
|
||||
coreEvents.emitAgentChanged(newAgent);
|
||||
}
|
||||
if (this.onAgentChange && !isTemporary) {
|
||||
this.onAgentChange(newAgent);
|
||||
}
|
||||
}
|
||||
|
||||
getDisableLoopDetection(): boolean {
|
||||
return this.disableLoopDetection ?? false;
|
||||
}
|
||||
|
||||
@@ -53,6 +53,16 @@ export interface ModelChangedPayload {
|
||||
model: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Payload for the 'agent-changed' event.
|
||||
*/
|
||||
export interface AgentChangedPayload {
|
||||
/**
|
||||
* The new active agent.
|
||||
*/
|
||||
agent: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Payload for the 'approval-mode-changed' event.
|
||||
*/
|
||||
@@ -196,6 +206,7 @@ export interface QuotaChangedPayload {
|
||||
export enum CoreEvent {
|
||||
UserFeedback = 'user-feedback',
|
||||
ModelChanged = 'model-changed',
|
||||
AgentChanged = 'agent-changed',
|
||||
ApprovalModeChanged = 'approval-mode-changed',
|
||||
ConsoleLog = 'console-log',
|
||||
Output = 'output',
|
||||
@@ -231,6 +242,7 @@ export interface EditorSelectedPayload {
|
||||
export interface CoreEvents extends ExtensionEvents {
|
||||
[CoreEvent.UserFeedback]: [UserFeedbackPayload];
|
||||
[CoreEvent.ModelChanged]: [ModelChangedPayload];
|
||||
[CoreEvent.AgentChanged]: [AgentChangedPayload];
|
||||
[CoreEvent.ApprovalModeChanged]: [ApprovalModeChangedPayload];
|
||||
[CoreEvent.ConsoleLog]: [ConsoleLogPayload];
|
||||
[CoreEvent.Output]: [OutputPayload];
|
||||
@@ -344,6 +356,14 @@ export class CoreEventEmitter extends EventEmitter<CoreEvents> {
|
||||
this.emit(CoreEvent.ModelChanged, payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies subscribers that the active agent has changed.
|
||||
*/
|
||||
emitAgentChanged(agent: string): void {
|
||||
const payload: AgentChangedPayload = { agent };
|
||||
this.emit(CoreEvent.AgentChanged, payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies subscribers that the approval mode has changed.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user