feat(cli): Add state management and plumbing for agent configuration dialog (#17259)

This commit is contained in:
Sandy Tao
2026-01-22 10:30:44 -08:00
committed by GitHub
parent ba8c64459b
commit 902e5d6dae
9 changed files with 246 additions and 1 deletions

View File

@@ -151,6 +151,8 @@ const mockUIActions: UIActions = {
exitPrivacyNotice: vi.fn(),
closeSettingsDialog: vi.fn(),
closeModelDialog: vi.fn(),
openAgentConfigDialog: vi.fn(),
closeAgentConfigDialog: vi.fn(),
openPermissionsDialog: vi.fn(),
openSessionBrowser: vi.fn(),
closeSessionBrowser: vi.fn(),

View File

@@ -28,6 +28,7 @@ import {
type UserFeedbackPayload,
type ResumedSessionData,
AuthType,
type AgentDefinition,
} from '@google/gemini-cli-core';
// Mock coreEvents
@@ -2147,6 +2148,77 @@ describe('AppContainer State Management', () => {
});
});
describe('Agent Configuration Dialog Integration', () => {
it('should initialize with dialog closed and no agent selected', async () => {
let unmount: () => void;
await act(async () => {
const result = renderAppContainer();
unmount = result.unmount;
});
await waitFor(() => expect(capturedUIState).toBeTruthy());
expect(capturedUIState.isAgentConfigDialogOpen).toBe(false);
expect(capturedUIState.selectedAgentName).toBeUndefined();
expect(capturedUIState.selectedAgentDisplayName).toBeUndefined();
expect(capturedUIState.selectedAgentDefinition).toBeUndefined();
unmount!();
});
it('should update state when openAgentConfigDialog is called', async () => {
let unmount: () => void;
await act(async () => {
const result = renderAppContainer();
unmount = result.unmount;
});
await waitFor(() => expect(capturedUIState).toBeTruthy());
const agentDefinition = { name: 'test-agent' };
act(() => {
capturedUIActions.openAgentConfigDialog(
'test-agent',
'Test Agent',
agentDefinition as unknown as AgentDefinition,
);
});
expect(capturedUIState.isAgentConfigDialogOpen).toBe(true);
expect(capturedUIState.selectedAgentName).toBe('test-agent');
expect(capturedUIState.selectedAgentDisplayName).toBe('Test Agent');
expect(capturedUIState.selectedAgentDefinition).toEqual(agentDefinition);
unmount!();
});
it('should clear state when closeAgentConfigDialog is called', async () => {
let unmount: () => void;
await act(async () => {
const result = renderAppContainer();
unmount = result.unmount;
});
await waitFor(() => expect(capturedUIState).toBeTruthy());
const agentDefinition = { name: 'test-agent' };
act(() => {
capturedUIActions.openAgentConfigDialog(
'test-agent',
'Test Agent',
agentDefinition as unknown as AgentDefinition,
);
});
expect(capturedUIState.isAgentConfigDialogOpen).toBe(true);
act(() => {
capturedUIActions.closeAgentConfigDialog();
});
expect(capturedUIState.isAgentConfigDialogOpen).toBe(false);
expect(capturedUIState.selectedAgentName).toBeUndefined();
expect(capturedUIState.selectedAgentDisplayName).toBeUndefined();
expect(capturedUIState.selectedAgentDefinition).toBeUndefined();
unmount!();
});
});
describe('CoreEvents Integration', () => {
it('subscribes to UserFeedback and drains backlog on mount', async () => {
let unmount: () => void;

View File

@@ -37,6 +37,7 @@ import {
type IdeContext,
type UserTierId,
type UserFeedbackPayload,
type AgentDefinition,
IdeClient,
ideContextStore,
getErrorMessage,
@@ -253,6 +254,34 @@ export const AppContainer = (props: AppContainerProps) => {
setPermissionsDialogProps(null);
}, []);
const [isAgentConfigDialogOpen, setIsAgentConfigDialogOpen] = useState(false);
const [selectedAgentName, setSelectedAgentName] = useState<
string | undefined
>();
const [selectedAgentDisplayName, setSelectedAgentDisplayName] = useState<
string | undefined
>();
const [selectedAgentDefinition, setSelectedAgentDefinition] = useState<
AgentDefinition | undefined
>();
const openAgentConfigDialog = useCallback(
(name: string, displayName: string, definition: AgentDefinition) => {
setSelectedAgentName(name);
setSelectedAgentDisplayName(displayName);
setSelectedAgentDefinition(definition);
setIsAgentConfigDialogOpen(true);
},
[],
);
const closeAgentConfigDialog = useCallback(() => {
setIsAgentConfigDialogOpen(false);
setSelectedAgentName(undefined);
setSelectedAgentDisplayName(undefined);
setSelectedAgentDefinition(undefined);
}, []);
const toggleDebugProfiler = useCallback(
() => setShowDebugProfiler((prev) => !prev),
[],
@@ -679,6 +708,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
openSettingsDialog,
openSessionBrowser,
openModelDialog,
openAgentConfigDialog,
openPermissionsDialog,
quit: (messages: HistoryItem[]) => {
setQuittingMessages(messages);
@@ -701,6 +731,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
openSettingsDialog,
openSessionBrowser,
openModelDialog,
openAgentConfigDialog,
setQuittingMessages,
setDebugMessage,
setShowPrivacyNotice,
@@ -1477,6 +1508,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
isThemeDialogOpen ||
isSettingsDialogOpen ||
isModelDialogOpen ||
isAgentConfigDialogOpen ||
isPermissionsDialogOpen ||
isAuthenticating ||
isAuthDialogOpen ||
@@ -1570,6 +1602,10 @@ Logging in with Google... Restarting Gemini CLI to continue.
isSettingsDialogOpen,
isSessionBrowserOpen,
isModelDialogOpen,
isAgentConfigDialogOpen,
selectedAgentName,
selectedAgentDisplayName,
selectedAgentDefinition,
isPermissionsDialogOpen,
permissionsDialogProps,
slashCommands,
@@ -1662,6 +1698,10 @@ Logging in with Google... Restarting Gemini CLI to continue.
isSettingsDialogOpen,
isSessionBrowserOpen,
isModelDialogOpen,
isAgentConfigDialogOpen,
selectedAgentName,
selectedAgentDisplayName,
selectedAgentDefinition,
isPermissionsDialogOpen,
permissionsDialogProps,
slashCommands,
@@ -1761,6 +1801,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
exitPrivacyNotice,
closeSettingsDialog,
closeModelDialog,
openAgentConfigDialog,
closeAgentConfigDialog,
openPermissionsDialog,
closePermissionsDialog,
setShellModeActive,
@@ -1802,6 +1844,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
exitPrivacyNotice,
closeSettingsDialog,
closeModelDialog,
openAgentConfigDialog,
closeAgentConfigDialog,
openPermissionsDialog,
closePermissionsDialog,
setShellModeActive,

View File

@@ -15,6 +15,7 @@ import type {
GitService,
Logger,
CommandActionReturn,
AgentDefinition,
} from '@google/gemini-cli-core';
import type { LoadedSettings } from '../../config/settings.js';
import type { UseHistoryManagerReturn } from '../hooks/useHistoryManager.js';
@@ -74,6 +75,11 @@ export interface CommandContext {
toggleDebugProfiler: () => void;
toggleVimEnabled: () => Promise<boolean>;
reloadCommands: () => void;
openAgentConfigDialog: (
name: string,
displayName: string,
definition: AgentDefinition,
) => void;
extensionsUpdateState: Map<string, ExtensionUpdateStatus>;
dispatchExtensionStateUpdate: (action: ExtensionUpdateAction) => void;
addConfirmUpdateExtensionRequest: (value: ConfirmationRequest) => void;
@@ -111,6 +117,7 @@ export interface OpenDialogActionReturn {
| 'settings'
| 'sessionBrowser'
| 'model'
| 'agentConfig'
| 'permissions';
}

View File

@@ -8,7 +8,11 @@ import { createContext, useContext } from 'react';
import { type Key } from '../hooks/useKeypress.js';
import { type IdeIntegrationNudgeResult } from '../IdeIntegrationNudge.js';
import { type FolderTrustChoice } from '../components/FolderTrustDialog.js';
import { type AuthType, type EditorType } from '@google/gemini-cli-core';
import {
type AuthType,
type EditorType,
type AgentDefinition,
} from '@google/gemini-cli-core';
import { type LoadableSettingScope } from '../../config/settings.js';
import type { AuthState } from '../types.js';
import { type PermissionsDialogProps } from '../components/PermissionsModifyTrustDialog.js';
@@ -32,6 +36,12 @@ export interface UIActions {
exitPrivacyNotice: () => void;
closeSettingsDialog: () => void;
closeModelDialog: () => void;
openAgentConfigDialog: (
name: string,
displayName: string,
definition: AgentDefinition,
) => void;
closeAgentConfigDialog: () => void;
openPermissionsDialog: (props?: PermissionsDialogProps) => void;
closePermissionsDialog: () => void;
setShellModeActive: (value: boolean) => void;

View File

@@ -24,6 +24,7 @@ import type {
IdeInfo,
FallbackIntent,
ValidationIntent,
AgentDefinition,
} from '@google/gemini-cli-core';
import type { DOMElement } from 'ink';
import type { SessionStatsState } from '../contexts/SessionContext.js';
@@ -70,6 +71,10 @@ export interface UIState {
isSettingsDialogOpen: boolean;
isSessionBrowserOpen: boolean;
isModelDialogOpen: boolean;
isAgentConfigDialogOpen: boolean;
selectedAgentName?: string;
selectedAgentDisplayName?: string;
selectedAgentDefinition?: AgentDefinition;
isPermissionsDialogOpen: boolean;
permissionsDialogProps: { targetDirectory?: string } | null;
slashCommands: readonly SlashCommand[] | undefined;

View File

@@ -188,6 +188,7 @@ describe('useSlashCommandProcessor', () => {
openSettingsDialog: vi.fn(),
openSessionBrowser: vi.fn(),
openModelDialog: mockOpenModelDialog,
openAgentConfigDialog: vi.fn(),
openPermissionsDialog: vi.fn(),
quit: mockSetQuittingMessages,
setDebugMessage: vi.fn(),
@@ -520,6 +521,82 @@ describe('useSlashCommandProcessor', () => {
expect(mockFn).toHaveBeenCalled();
},
);
it('should handle "dialog: agentConfig" action with props', async () => {
const mockOpenAgentConfigDialog = vi.fn();
const agentDefinition = { name: 'test-agent' };
const commandName = 'agentconfigcmd';
const command = createTestCommand({
name: commandName,
action: vi.fn().mockResolvedValue({
type: 'dialog',
dialog: 'agentConfig',
props: {
name: 'test-agent',
displayName: 'Test Agent',
definition: agentDefinition,
},
}),
});
// Re-setup the hook with the mock action that we can inspect
mockBuiltinLoadCommands.mockResolvedValue(Object.freeze([command]));
mockFileLoadCommands.mockResolvedValue(Object.freeze([]));
mockMcpLoadCommands.mockResolvedValue(Object.freeze([]));
let result!: { current: ReturnType<typeof useSlashCommandProcessor> };
await act(async () => {
const hook = renderHook(() =>
useSlashCommandProcessor(
mockConfig,
mockSettings,
mockAddItem,
mockClearItems,
mockLoadHistory,
vi.fn(),
vi.fn(),
vi.fn(),
{
openAuthDialog: vi.fn(),
openThemeDialog: vi.fn(),
openEditorDialog: vi.fn(),
openPrivacyNotice: vi.fn(),
openSettingsDialog: vi.fn(),
openSessionBrowser: vi.fn(),
openModelDialog: vi.fn(),
openAgentConfigDialog: mockOpenAgentConfigDialog,
openPermissionsDialog: vi.fn(),
quit: vi.fn(),
setDebugMessage: vi.fn(),
toggleCorgiMode: vi.fn(),
toggleDebugProfiler: vi.fn(),
dispatchExtensionStateUpdate: vi.fn(),
addConfirmUpdateExtensionRequest: vi.fn(),
setText: vi.fn(),
},
new Map(),
true,
vi.fn(),
vi.fn(),
),
);
result = hook.result;
});
await waitFor(() =>
expect(result.current.slashCommands).toHaveLength(1),
);
await act(async () => {
await result.current.handleSlashCommand(`/${commandName}`);
});
expect(mockOpenAgentConfigDialog).toHaveBeenCalledWith(
'test-agent',
'Test Agent',
agentDefinition,
);
});
});
it('should handle "load_history" action', async () => {

View File

@@ -19,6 +19,7 @@ import type {
ExtensionsStartingEvent,
ExtensionsStoppingEvent,
ToolCallConfirmationDetails,
AgentDefinition,
} from '@google/gemini-cli-core';
import {
GitService,
@@ -69,6 +70,11 @@ interface SlashCommandProcessorActions {
openSettingsDialog: () => void;
openSessionBrowser: () => void;
openModelDialog: () => void;
openAgentConfigDialog: (
name: string,
displayName: string,
definition: AgentDefinition,
) => void;
openPermissionsDialog: (props?: { targetDirectory?: string }) => void;
quit: (messages: HistoryItem[]) => void;
setDebugMessage: (message: string) => void;
@@ -224,6 +230,7 @@ export const useSlashCommandProcessor = (
toggleDebugProfiler: actions.toggleDebugProfiler,
toggleVimEnabled,
reloadCommands,
openAgentConfigDialog: actions.openAgentConfigDialog,
extensionsUpdateState,
dispatchExtensionStateUpdate: actions.dispatchExtensionStateUpdate,
addConfirmUpdateExtensionRequest:
@@ -452,6 +459,26 @@ export const useSlashCommandProcessor = (
case 'model':
actions.openModelDialog();
return { type: 'handled' };
case 'agentConfig': {
const props = result.props as Record<string, unknown>;
if (
!props ||
typeof props['name'] !== 'string' ||
typeof props['displayName'] !== 'string' ||
!props['definition']
) {
throw new Error(
'Received invalid properties for agentConfig dialog action.',
);
}
actions.openAgentConfigDialog(
props['name'],
props['displayName'],
props['definition'] as AgentDefinition,
);
return { type: 'handled' };
}
case 'permissions':
actions.openPermissionsDialog(
result.props as { targetDirectory?: string },

View File

@@ -24,6 +24,7 @@ export function createNonInteractiveUI(): CommandContext['ui'] {
toggleDebugProfiler: () => {},
toggleVimEnabled: async () => false,
reloadCommands: () => {},
openAgentConfigDialog: () => {},
extensionsUpdateState: new Map(),
dispatchExtensionStateUpdate: (_action: ExtensionUpdateAction) => {},
addConfirmUpdateExtensionRequest: (_request) => {},