mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-08-02 21:21:09 -07:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c22187d731 | |||
| fc8928c089 |
@@ -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 || [];
|
||||
@@ -974,6 +994,7 @@ export async function loadCliConfig(
|
||||
mcpEnabled,
|
||||
extensionsEnabled,
|
||||
agents: settings.agents,
|
||||
enterprise: settings.enterprise,
|
||||
adminSkillsEnabled,
|
||||
allowedMcpServers: mcpEnabled
|
||||
? (argv.allowedMcpServerNames ?? settings.mcp?.allowed)
|
||||
@@ -1013,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,
|
||||
@@ -1092,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',
|
||||
@@ -1366,6 +1387,44 @@ const SETTINGS_SCHEMA = {
|
||||
},
|
||||
},
|
||||
},
|
||||
enterprise: {
|
||||
type: 'object',
|
||||
label: 'Gemini Enterprise',
|
||||
category: 'Advanced',
|
||||
requiresRestart: true,
|
||||
default: {},
|
||||
description: 'Settings for Gemini Enterprise integration.',
|
||||
showInDialog: false,
|
||||
properties: {
|
||||
projectId: {
|
||||
type: 'string',
|
||||
label: 'Project ID',
|
||||
category: 'Advanced',
|
||||
requiresRestart: true,
|
||||
default: undefined as string | undefined,
|
||||
description: 'Google Cloud Project ID for Gemini Enterprise.',
|
||||
showInDialog: true,
|
||||
},
|
||||
engineId: {
|
||||
type: 'string',
|
||||
label: 'Engine ID',
|
||||
category: 'Advanced',
|
||||
requiresRestart: true,
|
||||
default: undefined as string | undefined,
|
||||
description: 'Discovery Engine ID for Gemini Enterprise.',
|
||||
showInDialog: true,
|
||||
},
|
||||
location: {
|
||||
type: 'string',
|
||||
label: 'Location',
|
||||
category: 'Advanced',
|
||||
requiresRestart: true,
|
||||
default: 'global',
|
||||
description: 'Google Cloud Location for Gemini Enterprise.',
|
||||
showInDialog: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
context: {
|
||||
type: 'object',
|
||||
|
||||
@@ -507,7 +507,8 @@ export async function main() {
|
||||
// the sandbox because the sandbox will interfere with the Oauth2 web
|
||||
// redirect.
|
||||
let initialAuthFailed = false;
|
||||
if (!settings.merged.security.auth.useExternal && !argv.isCommand) {
|
||||
const useEnterprise = process.env['GEMINI_CLI_ENTERPRISE_AGENT'] === 'true';
|
||||
if (!settings.merged.security.auth.useExternal && !argv.isCommand && !useEnterprise) {
|
||||
try {
|
||||
if (
|
||||
partialConfig.isInteractive() &&
|
||||
@@ -858,13 +859,16 @@ export async function main() {
|
||||
),
|
||||
);
|
||||
|
||||
const authType = await validateNonInteractiveAuth(
|
||||
settings.merged.security.auth.selectedType,
|
||||
settings.merged.security.auth.useExternal,
|
||||
config,
|
||||
settings,
|
||||
);
|
||||
await config.refreshAuth(authType);
|
||||
const useEnterprise = process.env['GEMINI_CLI_ENTERPRISE_AGENT'] === 'true';
|
||||
if (!useEnterprise) {
|
||||
const authType = await validateNonInteractiveAuth(
|
||||
settings.merged.security.auth.selectedType,
|
||||
settings.merged.security.auth.useExternal,
|
||||
config,
|
||||
settings,
|
||||
);
|
||||
await config.refreshAuth(authType);
|
||||
}
|
||||
|
||||
if (config.getDebugMode()) {
|
||||
debugLogger.log('Session ID: %s', sessionId);
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -66,7 +66,10 @@ interface RunNonInteractiveParams {
|
||||
export async function runNonInteractive(
|
||||
params: RunNonInteractiveParams,
|
||||
): Promise<void> {
|
||||
const useAgentSession = params.config.getAgentSessionNoninteractiveEnabled();
|
||||
const useAgentSession =
|
||||
params.config.getAgent() === 'gemini-enterprise' ||
|
||||
params.config.getAgentSessionNoninteractiveEnabled() ||
|
||||
process.env['GEMINI_CLI_ENTERPRISE_AGENT'] === 'true';
|
||||
if (useAgentSession) {
|
||||
debugLogger.debug(
|
||||
'[ADK] Running non-interactive mode with ADK agent session',
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
Scheduler,
|
||||
ROOT_SCHEDULER_ID,
|
||||
LegacyAgentSession,
|
||||
EnterpriseAgentSession,
|
||||
ToolErrorType,
|
||||
geminiPartsToContentParts,
|
||||
displayContentToString,
|
||||
@@ -295,13 +296,18 @@ export async function runNonInteractive({
|
||||
});
|
||||
}
|
||||
|
||||
// Create LegacyAgentSession — owns the agentic loop
|
||||
const session = new LegacyAgentSession({
|
||||
client: geminiClient,
|
||||
scheduler,
|
||||
config,
|
||||
promptId: prompt_id,
|
||||
});
|
||||
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 })
|
||||
: new LegacyAgentSession({
|
||||
client: geminiClient,
|
||||
scheduler,
|
||||
config,
|
||||
promptId: prompt_id,
|
||||
});
|
||||
|
||||
// Wire Ctrl+C to session abort
|
||||
abortSession = () => {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -90,6 +90,7 @@ import {
|
||||
logBillingEvent,
|
||||
ApiKeyUpdatedEvent,
|
||||
LegacyAgentProtocol,
|
||||
EnterpriseAgentProtocol,
|
||||
type InjectionSource,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { validateAuthMethod } from '../config/auth.js';
|
||||
@@ -102,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';
|
||||
@@ -146,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';
|
||||
@@ -937,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,
|
||||
@@ -967,6 +973,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
openSettingsDialog,
|
||||
openSessionBrowser,
|
||||
openModelDialog,
|
||||
openAgentDialog,
|
||||
openVoiceModelDialog,
|
||||
openAgentConfigDialog,
|
||||
openPermissionsDialog,
|
||||
@@ -1006,6 +1013,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
openSettingsDialog,
|
||||
openSessionBrowser,
|
||||
openModelDialog,
|
||||
openAgentDialog,
|
||||
openVoiceModelDialog,
|
||||
openAgentConfigDialog,
|
||||
setQuittingMessages,
|
||||
@@ -1173,11 +1181,15 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
}, [config]);
|
||||
|
||||
const streamAgent = useMemo(
|
||||
() =>
|
||||
config?.getAgentSessionInteractiveEnabled()
|
||||
() => {
|
||||
if (activeAgent === 'gemini-enterprise') {
|
||||
return new EnterpriseAgentProtocol({ config });
|
||||
}
|
||||
return config?.getAgentSessionInteractiveEnabled()
|
||||
? new LegacyAgentProtocol({ config, getPreferredEditor })
|
||||
: undefined,
|
||||
[config, getPreferredEditor],
|
||||
: undefined;
|
||||
},
|
||||
[config, getPreferredEditor, activeAgent],
|
||||
);
|
||||
|
||||
const activeStream = streamAgent
|
||||
@@ -1233,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],
|
||||
@@ -2210,6 +2281,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
isThemeDialogOpen ||
|
||||
isSettingsDialogOpen ||
|
||||
isModelDialogOpen ||
|
||||
isAgentDialogOpen ||
|
||||
isVoiceModelDialogOpen ||
|
||||
isAgentConfigDialogOpen ||
|
||||
isPermissionsDialogOpen ||
|
||||
@@ -2471,6 +2543,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
isSettingsDialogOpen,
|
||||
isSessionBrowserOpen,
|
||||
isModelDialogOpen,
|
||||
isAgentDialogOpen,
|
||||
isVoiceModelDialogOpen,
|
||||
isAgentConfigDialogOpen,
|
||||
selectedAgentName,
|
||||
@@ -2521,6 +2594,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
showApprovalModeIndicator,
|
||||
allowPlanMode,
|
||||
currentModel,
|
||||
activeAgent,
|
||||
contextFileNames,
|
||||
errorCount,
|
||||
availableTerminalHeight,
|
||||
@@ -2584,6 +2658,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
isSettingsDialogOpen,
|
||||
isSessionBrowserOpen,
|
||||
isModelDialogOpen,
|
||||
isAgentDialogOpen,
|
||||
isVoiceModelDialogOpen,
|
||||
isAgentConfigDialogOpen,
|
||||
selectedAgentName,
|
||||
@@ -2655,6 +2730,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
ideTrustRestartReason,
|
||||
isRestarting,
|
||||
currentModel,
|
||||
activeAgent,
|
||||
extensionsUpdateState,
|
||||
activePtyId,
|
||||
backgroundTaskCount,
|
||||
@@ -2700,6 +2776,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
exitPrivacyNotice,
|
||||
closeSettingsDialog,
|
||||
closeModelDialog,
|
||||
closeAgentDialog,
|
||||
handleAgentSelect,
|
||||
openVoiceModelDialog,
|
||||
closeVoiceModelDialog,
|
||||
openAgentConfigDialog,
|
||||
@@ -2802,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,
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, expect, it, vi, beforeEach, afterEach, type Mock } from 'vitest';
|
||||
import { EnterpriseAgentSession } from './enterprise-agent-session.js';
|
||||
import type { Config } from '../config/config.js';
|
||||
import type { AgentEvent } from './types.js';
|
||||
import { GoogleAuth } from 'google-auth-library';
|
||||
|
||||
// Mock google-auth-library
|
||||
vi.mock('google-auth-library', () => ({
|
||||
GoogleAuth: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('EnterpriseAgentSession', () => {
|
||||
let mockConfig: Config;
|
||||
let globalFetch: typeof fetch;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
(GoogleAuth as unknown as Mock).mockImplementation(() => ({
|
||||
getClient: vi.fn().mockResolvedValue({
|
||||
getAccessToken: vi.fn().mockResolvedValue({ token: 'fake-token' }),
|
||||
}),
|
||||
}));
|
||||
|
||||
mockConfig = {
|
||||
getSessionId: vi.fn().mockReturnValue('test-session'),
|
||||
getEnterpriseConfig: vi.fn().mockReturnValue({
|
||||
projectId: 'test-project',
|
||||
engineId: 'test-engine',
|
||||
location: 'global',
|
||||
}),
|
||||
} as unknown as Config;
|
||||
|
||||
globalFetch = global.fetch;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
global.fetch = globalFetch;
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
const mockFetchResponse = (chunks: string[]) => {
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
for (const chunk of chunks) {
|
||||
controller.enqueue(new TextEncoder().encode(chunk));
|
||||
}
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
body: stream,
|
||||
headers: new Headers(),
|
||||
} as Response);
|
||||
};
|
||||
|
||||
it('should successfully call Enterprise API and stream responses', async () => {
|
||||
const chunk1 = JSON.stringify({
|
||||
sessionInfo: { session: 'projects/test-project/locations/global/collections/default_collection/engines/test-engine/sessions/s1' },
|
||||
answer: {
|
||||
replies: [
|
||||
{
|
||||
groundedContent: {
|
||||
content: { text: 'Hello' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}) + '\n';
|
||||
|
||||
const chunk2 = JSON.stringify({
|
||||
answer: {
|
||||
replies: [
|
||||
{
|
||||
groundedContent: {
|
||||
content: { text: ' World' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}) + '\n';
|
||||
|
||||
mockFetchResponse([chunk1, chunk2]);
|
||||
|
||||
const session = new EnterpriseAgentSession({ config: mockConfig });
|
||||
const { streamId } = await session.send({
|
||||
message: { content: [{ type: 'text', text: 'hi' }] },
|
||||
});
|
||||
|
||||
expect(streamId).toBe('enterprise-stream-1');
|
||||
|
||||
const events: AgentEvent[] = [];
|
||||
for await (const event of session.stream({ streamId: streamId! })) {
|
||||
events.push(event);
|
||||
}
|
||||
|
||||
expect(events.map(e => e.type)).toEqual([
|
||||
'agent_start',
|
||||
'message', // Hello
|
||||
'message', // World
|
||||
'agent_end',
|
||||
]);
|
||||
|
||||
const messages = events.filter((e): e is AgentEvent<'message'> => e.type === 'message' && e.role === 'agent');
|
||||
expect(messages[0].content).toEqual([{ type: 'text', text: 'Hello' }]);
|
||||
expect(messages[1].content).toEqual([{ type: 'text', text: ' World' }]);
|
||||
});
|
||||
|
||||
it('should handle thoughts', async () => {
|
||||
const chunk = JSON.stringify({
|
||||
answer: {
|
||||
replies: [
|
||||
{
|
||||
groundedContent: {
|
||||
content: { text: 'Thinking...', thought: true },
|
||||
},
|
||||
},
|
||||
{
|
||||
groundedContent: {
|
||||
content: { text: 'Final answer' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}) + '\n';
|
||||
|
||||
mockFetchResponse([chunk]);
|
||||
|
||||
const session = new EnterpriseAgentSession({ config: mockConfig });
|
||||
const { streamId } = await session.send({
|
||||
message: { content: [{ type: 'text', text: 'hi' }] },
|
||||
});
|
||||
|
||||
const events: AgentEvent[] = [];
|
||||
for await (const event of session.stream({ streamId: streamId! })) {
|
||||
events.push(event);
|
||||
}
|
||||
|
||||
|
||||
|
||||
const thoughts = events.filter((e): e is AgentEvent<'message'> => e.type === 'message' && e.content[0]?.type === 'thought');
|
||||
expect(thoughts).toHaveLength(1);
|
||||
expect(thoughts[0].content).toEqual([{ type: 'thought', thought: 'Thinking...' }]);
|
||||
|
||||
const texts = events.filter((e): e is AgentEvent<'message'> => e.type === 'message' && e.content[0]?.type === 'text' && e.role === 'agent');
|
||||
expect(texts).toHaveLength(1);
|
||||
expect(texts[0].content).toEqual([{ type: 'text', text: 'Final answer' }]);
|
||||
});
|
||||
|
||||
it('should handle tool requests and responses (executable code)', async () => {
|
||||
const chunk1 = JSON.stringify({
|
||||
answer: {
|
||||
replies: [
|
||||
{
|
||||
groundedContent: {
|
||||
content: {
|
||||
executableCode: { code: 'print("hello")' },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}) + '\n';
|
||||
|
||||
const chunk2 = JSON.stringify({
|
||||
answer: {
|
||||
replies: [
|
||||
{
|
||||
groundedContent: {
|
||||
content: {
|
||||
codeExecutionResult: { outcome: 'OUTCOME_OK', output: 'hello\n' },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}) + '\n';
|
||||
|
||||
mockFetchResponse([chunk1, chunk2]);
|
||||
|
||||
const session = new EnterpriseAgentSession({ config: mockConfig });
|
||||
const { streamId } = await session.send({
|
||||
message: { content: [{ type: 'text', text: 'run code' }] },
|
||||
});
|
||||
|
||||
const events: AgentEvent[] = [];
|
||||
for await (const event of session.stream({ streamId: streamId! })) {
|
||||
events.push(event);
|
||||
}
|
||||
|
||||
|
||||
|
||||
expect(events.map(e => e.type)).toEqual([
|
||||
'agent_start',
|
||||
'tool_request',
|
||||
'tool_response',
|
||||
'agent_end',
|
||||
]);
|
||||
|
||||
const toolReq = events.find(e => e.type === 'tool_request') as AgentEvent<'tool_request'>;
|
||||
expect(toolReq.name).toBe('python_interpreter');
|
||||
expect(toolReq.args).toEqual({ code: 'print("hello")' });
|
||||
|
||||
const toolResp = events.find(e => e.type === 'tool_response') as AgentEvent<'tool_response'>;
|
||||
expect(toolResp.name).toBe('python_interpreter');
|
||||
expect(toolResp.content).toEqual([{ type: 'text', text: 'hello\n' }]);
|
||||
expect(toolResp.isError).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle immersive artifacts (tables/docs)', async () => {
|
||||
const chunk = JSON.stringify({
|
||||
answer: {
|
||||
replies: [
|
||||
{
|
||||
groundedContent: {
|
||||
content: { text: 'Here is the table:\n' },
|
||||
},
|
||||
immersiveArtifact: [
|
||||
{
|
||||
docArtifact: { text: '| Col 1 | Col 2 |\n|---|---|\n| Val 1 | Val 2 |' },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
}) + '\n';
|
||||
|
||||
mockFetchResponse([chunk]);
|
||||
|
||||
const session = new EnterpriseAgentSession({ config: mockConfig });
|
||||
const { streamId } = await session.send({
|
||||
message: { content: [{ type: 'text', text: 'show table' }] },
|
||||
});
|
||||
|
||||
const events: AgentEvent[] = [];
|
||||
for await (const event of session.stream({ streamId: streamId! })) {
|
||||
events.push(event);
|
||||
}
|
||||
|
||||
const texts = events.filter((e): e is AgentEvent<'message'> => e.type === 'message' && e.role === 'agent');
|
||||
expect(texts).toHaveLength(2);
|
||||
expect(texts[0].content).toEqual([{ type: 'text', text: 'Here is the table:\n' }]);
|
||||
expect(texts[1].content).toEqual([{ type: 'text', text: '| Col 1 | Col 2 |\n|---|---|\n| Val 1 | Val 2 |' }]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,420 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { GoogleAuth } from 'google-auth-library';
|
||||
import type { Config } from '../config/config.js';
|
||||
import { AgentSession } from './agent-session.js';
|
||||
import type {
|
||||
AgentEvent,
|
||||
AgentProtocol,
|
||||
AgentSend,
|
||||
ContentPart,
|
||||
Unsubscribe,
|
||||
} from './types.js';
|
||||
import { fetchWithTimeout } from '../utils/fetch.js';
|
||||
import { debugLogger } from '../utils/debugLogger.js';
|
||||
|
||||
export interface EnterpriseAgentSessionDeps {
|
||||
config: Config;
|
||||
promptId?: string;
|
||||
streamId?: string;
|
||||
}
|
||||
|
||||
export class EnterpriseAgentProtocol implements AgentProtocol {
|
||||
private _events: AgentEvent[] = [];
|
||||
private _subscribers = new Set<(event: AgentEvent) => void>();
|
||||
private _activeStreamId?: string;
|
||||
private _abortController = new AbortController();
|
||||
private _streamCounter = 0;
|
||||
private _eventCounter = 0;
|
||||
|
||||
private readonly _config: Config;
|
||||
private _sessionResourceName?: string;
|
||||
|
||||
constructor(deps: EnterpriseAgentSessionDeps) {
|
||||
this._config = deps.config;
|
||||
this._sessionResourceName = deps.streamId;
|
||||
}
|
||||
|
||||
get events(): readonly AgentEvent[] {
|
||||
return this._events;
|
||||
}
|
||||
|
||||
subscribe(callback: (event: AgentEvent) => void): Unsubscribe {
|
||||
this._subscribers.add(callback);
|
||||
return () => {
|
||||
this._subscribers.delete(callback);
|
||||
};
|
||||
}
|
||||
|
||||
async abort(): Promise<void> {
|
||||
this._abortController.abort();
|
||||
}
|
||||
|
||||
async send(payload: AgentSend): Promise<{ streamId: string }> {
|
||||
const message = 'message' in payload ? payload.message : undefined;
|
||||
if (!message) {
|
||||
throw new Error(
|
||||
'EnterpriseAgentSession.send() only supports message sends for the moment.',
|
||||
);
|
||||
}
|
||||
|
||||
if (this._activeStreamId) {
|
||||
throw new Error(
|
||||
'EnterpriseAgentSession.send() cannot be called while a stream is active.',
|
||||
);
|
||||
}
|
||||
|
||||
this._beginNewStream();
|
||||
const streamId = this._activeStreamId!;
|
||||
|
||||
const userMessage = this._makeUserMessageEvent(
|
||||
message.content,
|
||||
message.displayContent,
|
||||
payload._meta,
|
||||
);
|
||||
this._emit([userMessage]);
|
||||
|
||||
this._scheduleRunLoop(message.content.map(p => p.type === 'text' ? p.text : '').join(' '));
|
||||
|
||||
return { streamId };
|
||||
}
|
||||
|
||||
private _beginNewStream(): void {
|
||||
this._streamCounter++;
|
||||
this._eventCounter = 0;
|
||||
this._abortController = new AbortController();
|
||||
this._activeStreamId = `enterprise-stream-${this._streamCounter}`;
|
||||
}
|
||||
|
||||
private _scheduleRunLoop(queryText: string): void {
|
||||
setTimeout(() => {
|
||||
void this._runLoopInBackground(queryText);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
private async _runLoopInBackground(queryText: string): Promise<void> {
|
||||
this._ensureAgentStart();
|
||||
|
||||
try {
|
||||
const enterpriseConfig = this._config.getEnterpriseConfig();
|
||||
if (!enterpriseConfig?.projectId || !enterpriseConfig?.engineId) {
|
||||
throw new Error('Gemini Enterprise is not fully configured. projectId and engineId are required in ~/.gemini/settings.json.');
|
||||
}
|
||||
|
||||
const projectId = enterpriseConfig.projectId;
|
||||
const engineId = enterpriseConfig.engineId;
|
||||
const location = enterpriseConfig.location ?? 'global';
|
||||
|
||||
// Get Auth Token
|
||||
const auth = new GoogleAuth({
|
||||
scopes: ['https://www.googleapis.com/auth/cloud-platform'],
|
||||
});
|
||||
const client = await auth.getClient();
|
||||
const tokenResponse = await client.getAccessToken();
|
||||
const token = tokenResponse.token;
|
||||
|
||||
if (!token) {
|
||||
throw new Error('Failed to retrieve ADC access token.');
|
||||
}
|
||||
|
||||
const endpoint = `https://discoveryengine.googleapis.com/v1alpha/projects/${projectId}/locations/${location}/collections/default_collection/engines/${engineId}/assistants/default_assistant:streamAssist`;
|
||||
|
||||
const requestBody = {
|
||||
query: {
|
||||
text: queryText,
|
||||
},
|
||||
session: this._sessionResourceName || '-',
|
||||
assistSkippingMode: 'REQUEST_ASSIST',
|
||||
toolsSpec: {
|
||||
vertexAiSearchSpec: {},
|
||||
webGroundingSpec: {},
|
||||
},
|
||||
};
|
||||
|
||||
debugLogger.debug(`Calling Enterprise API: ${endpoint}`);
|
||||
debugLogger.debug(`Request Body: ${JSON.stringify(requestBody)}`);
|
||||
|
||||
const response = await fetchWithTimeout(endpoint, 60000, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
signal: this._abortController.signal,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Enterprise API call failed with status ${response.status}: ${errorText}`);
|
||||
}
|
||||
|
||||
if (!response.body) {
|
||||
throw new Error('Response body is empty.');
|
||||
}
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
let braceCount = 0;
|
||||
let insideString = false;
|
||||
let escapeNext = false;
|
||||
let objectStart = -1;
|
||||
let lastScannedIndex = 0;
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
|
||||
for (let i = lastScannedIndex; i < buffer.length; i++) {
|
||||
const char = buffer[i];
|
||||
|
||||
if (escapeNext) {
|
||||
escapeNext = false;
|
||||
lastScannedIndex = i + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === '\\') {
|
||||
escapeNext = true;
|
||||
lastScannedIndex = i + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === '"') {
|
||||
insideString = !insideString;
|
||||
lastScannedIndex = i + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!insideString) {
|
||||
if (char === '{') {
|
||||
if (braceCount === 0) {
|
||||
objectStart = i;
|
||||
}
|
||||
braceCount++;
|
||||
} else if (char === '}') {
|
||||
braceCount--;
|
||||
if (braceCount === 0 && objectStart !== -1) {
|
||||
const objectStr = buffer.substring(objectStart, i + 1);
|
||||
await this._parseAndEmitChunk(objectStr);
|
||||
// Remove parsed object from buffer and reset index
|
||||
buffer = buffer.substring(i + 1);
|
||||
i = -1;
|
||||
lastScannedIndex = 0;
|
||||
objectStart = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
lastScannedIndex = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
this._emit([this._makeAgentEndEvent('completed')]);
|
||||
|
||||
} catch (err: unknown) {
|
||||
if (this._abortController.signal.aborted) {
|
||||
this._emit([this._makeAgentEndEvent('aborted')]);
|
||||
} else {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
this._emit([
|
||||
this._makeErrorEvent({
|
||||
status: 'INTERNAL',
|
||||
message,
|
||||
fatal: true,
|
||||
}),
|
||||
]);
|
||||
this._emit([this._makeAgentEndEvent('failed')]);
|
||||
}
|
||||
} finally {
|
||||
this._activeStreamId = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _ensureAgentStart(): void {
|
||||
// We can emit agent_start early or wait until we get the first chunk.
|
||||
// For now, emit it at the start of background run.
|
||||
this._emit([this._makeAgentStartEvent()]);
|
||||
}
|
||||
|
||||
private async _parseAndEmitChunk(line: string): Promise<void> {
|
||||
try {
|
||||
const response = JSON.parse(line);
|
||||
debugLogger.debug(`Received Enterprise Chunk: ${JSON.stringify(response)}`);
|
||||
|
||||
if (response.sessionInfo?.session) {
|
||||
this._sessionResourceName = response.sessionInfo.session;
|
||||
debugLogger.debug(`Session updated: ${this._sessionResourceName}`);
|
||||
}
|
||||
|
||||
const answer = response.answer;
|
||||
if (answer) {
|
||||
if (answer.replies) {
|
||||
for (const reply of answer.replies) {
|
||||
if (reply.groundedContent?.content) {
|
||||
const content = reply.groundedContent.content;
|
||||
|
||||
// Handle Thought
|
||||
if (content.thought) {
|
||||
if (content.text) {
|
||||
this._emit([
|
||||
this._makeMessageEvent('agent', [
|
||||
{ type: 'thought', thought: content.text }
|
||||
])
|
||||
]);
|
||||
}
|
||||
}
|
||||
// Handle Tool Use (Executable Code)
|
||||
else if (content.executableCode) {
|
||||
this._emit([
|
||||
this._makeToolRequestEvent({
|
||||
requestId: `ent-tool-${Date.now()}`,
|
||||
name: 'python_interpreter',
|
||||
args: { code: content.executableCode.code },
|
||||
display: {
|
||||
name: 'Python Interpreter',
|
||||
description: 'Executing code server-side',
|
||||
}
|
||||
})
|
||||
]);
|
||||
}
|
||||
// Handle Tool Response (Code Execution Result)
|
||||
else if (content.codeExecutionResult) {
|
||||
this._emit([
|
||||
this._makeToolResponseEvent({
|
||||
requestId: `ent-tool-${Date.now()}`,
|
||||
name: 'python_interpreter',
|
||||
content: [{ type: 'text', text: content.codeExecutionResult.output || '' }],
|
||||
isError: content.codeExecutionResult.outcome === 'OUTCOME_FAILED',
|
||||
})
|
||||
]);
|
||||
}
|
||||
// Handle standard Text
|
||||
else if (content.text) {
|
||||
this._emit([
|
||||
this._makeMessageEvent('agent', [
|
||||
{ type: 'text', text: content.text }
|
||||
])
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if (reply.immersiveArtifact) {
|
||||
for (const artifact of reply.immersiveArtifact) {
|
||||
if (artifact.docArtifact?.text) {
|
||||
this._emit([
|
||||
this._makeMessageEvent('agent', [
|
||||
{ type: 'text', text: artifact.docArtifact.text }
|
||||
])
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugLogger.error(`Failed to parse line: ${line}`, e);
|
||||
}
|
||||
}
|
||||
|
||||
private _emit(events: AgentEvent[]): void {
|
||||
if (events.length === 0) return;
|
||||
|
||||
const subscribers = [...this._subscribers];
|
||||
for (const event of events) {
|
||||
this._events.push(event);
|
||||
for (const subscriber of subscribers) {
|
||||
subscriber(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _nextEventFields() {
|
||||
return {
|
||||
id: `${this._activeStreamId}-${this._eventCounter++}`,
|
||||
timestamp: new Date().toISOString(),
|
||||
streamId: this._activeStreamId!,
|
||||
};
|
||||
}
|
||||
|
||||
private _makeUserMessageEvent(
|
||||
content: ContentPart[],
|
||||
displayContent?: string,
|
||||
meta?: Record<string, unknown>,
|
||||
): AgentEvent<'message'> {
|
||||
const eventContent: ContentPart[] = displayContent
|
||||
? [{ type: 'text', text: displayContent }]
|
||||
: content;
|
||||
return {
|
||||
...this._nextEventFields(),
|
||||
type: 'message',
|
||||
role: 'user',
|
||||
content: eventContent,
|
||||
...(meta ? { _meta: meta } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
private _makeMessageEvent(
|
||||
role: 'agent' | 'user' | 'developer',
|
||||
content: ContentPart[],
|
||||
): AgentEvent<'message'> {
|
||||
return {
|
||||
...this._nextEventFields(),
|
||||
type: 'message',
|
||||
role,
|
||||
content,
|
||||
};
|
||||
}
|
||||
|
||||
private _makeAgentStartEvent(): AgentEvent<'agent_start'> {
|
||||
return {
|
||||
...this._nextEventFields(),
|
||||
type: 'agent_start',
|
||||
};
|
||||
}
|
||||
|
||||
private _makeAgentEndEvent(reason: string): AgentEvent<'agent_end'> {
|
||||
return {
|
||||
...this._nextEventFields(),
|
||||
type: 'agent_end',
|
||||
reason,
|
||||
};
|
||||
}
|
||||
|
||||
private _makeErrorEvent(payload: Omit<AgentEvent<'error'>, 'id' | 'timestamp' | 'streamId' | 'type'>): AgentEvent<'error'> {
|
||||
return {
|
||||
...this._nextEventFields(),
|
||||
type: 'error',
|
||||
...payload,
|
||||
};
|
||||
}
|
||||
|
||||
private _makeToolRequestEvent(payload: Omit<AgentEvent<'tool_request'>, 'id' | 'timestamp' | 'streamId' | 'type'>): AgentEvent<'tool_request'> {
|
||||
return {
|
||||
...this._nextEventFields(),
|
||||
type: 'tool_request',
|
||||
...payload,
|
||||
};
|
||||
}
|
||||
|
||||
private _makeToolResponseEvent(payload: Omit<AgentEvent<'tool_response'>, 'id' | 'timestamp' | 'streamId' | 'type'>): AgentEvent<'tool_response'> {
|
||||
return {
|
||||
...this._nextEventFields(),
|
||||
type: 'tool_response',
|
||||
...payload,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class EnterpriseAgentSession extends AgentSession {
|
||||
constructor(deps: EnterpriseAgentSessionDeps) {
|
||||
super(new EnterpriseAgentProtocol(deps));
|
||||
}
|
||||
}
|
||||
@@ -208,6 +208,12 @@ export interface PlanSettings {
|
||||
modelRouting?: boolean;
|
||||
}
|
||||
|
||||
export interface EnterpriseSettings {
|
||||
projectId?: string;
|
||||
engineId?: string;
|
||||
location?: string;
|
||||
}
|
||||
|
||||
export interface TelemetrySettings {
|
||||
enabled?: boolean;
|
||||
traces?: boolean;
|
||||
@@ -637,6 +643,7 @@ export interface ConfigParameters {
|
||||
includeDirectories?: string[];
|
||||
bugCommand?: BugCommandSettings;
|
||||
model: string;
|
||||
agent?: string;
|
||||
disableLoopDetection?: boolean;
|
||||
maxSessionTurns?: number;
|
||||
acpMode?: boolean;
|
||||
@@ -728,6 +735,7 @@ export interface ConfigParameters {
|
||||
worktreeSettings?: WorktreeSettings;
|
||||
modelSteering?: boolean;
|
||||
onModelChange?: (model: string) => void;
|
||||
onAgentChange?: (agent: string) => void;
|
||||
mcpEnabled?: boolean;
|
||||
extensionsEnabled?: boolean;
|
||||
agents?: AgentSettings;
|
||||
@@ -742,6 +750,7 @@ export interface ConfigParameters {
|
||||
};
|
||||
vertexAiRouting?: VertexAiRoutingConfig;
|
||||
logRagSnippets?: boolean;
|
||||
enterprise?: EnterpriseSettings;
|
||||
}
|
||||
|
||||
export class Config implements McpContext, AgentLoopContext {
|
||||
@@ -824,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;
|
||||
@@ -940,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[];
|
||||
@@ -978,12 +989,14 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
private lastModeSwitchTime: number = performance.now();
|
||||
readonly injectionService: InjectionService;
|
||||
private approvedPlanPath: string | undefined;
|
||||
private readonly enterprise?: EnterpriseSettings;
|
||||
|
||||
constructor(params: ConfigParameters) {
|
||||
this._sessionId = params.sessionId;
|
||||
this.clientName = params.clientName;
|
||||
this._clientVersion = params.clientVersion ?? 'unknown';
|
||||
this.approvedPlanPath = undefined;
|
||||
this.enterprise = params.enterprise;
|
||||
|
||||
this.embeddingModel =
|
||||
params.embeddingModel ?? DEFAULT_GEMINI_EMBEDDING_MODEL;
|
||||
@@ -1117,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;
|
||||
@@ -1381,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 = {
|
||||
@@ -1817,6 +1832,10 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
return this.clientName;
|
||||
}
|
||||
|
||||
getEnterpriseConfig(): EnterpriseSettings | undefined {
|
||||
return this.enterprise;
|
||||
}
|
||||
|
||||
setSessionId(sessionId: string): void {
|
||||
const previousPlansDir = this.storage.isInitialized()
|
||||
? this.storage.getPlansDir()
|
||||
@@ -1900,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;
|
||||
}
|
||||
|
||||
@@ -196,6 +196,7 @@ export { resetBrowserSession } from './agents/browser/browserAgentFactory.js';
|
||||
// Export agent session interface
|
||||
export * from './agent/agent-session.js';
|
||||
export * from './agent/legacy-agent-session.js';
|
||||
export * from './agent/enterprise-agent-session.js';
|
||||
export * from './agent/event-translator.js';
|
||||
export * from './agent/content-utils.js';
|
||||
export * from './agent/tool-display-utils.js';
|
||||
|
||||
@@ -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