feat(agents): implement first-run experience for project-level sub-agents

This commit is contained in:
Christian Gunderman
2026-01-21 17:58:35 -08:00
parent 27d21f9921
commit 46fc062314
9 changed files with 439 additions and 4 deletions
+35 -1
View File
@@ -62,7 +62,9 @@ import {
SessionStartSource,
SessionEndReason,
generateSummary,
} from '@google/gemini-cli-core';
type AgentDefinition,
type AgentsDiscoveredPayload} from '@google/gemini-cli-core';
import { validateAuthMethod } from '../config/auth.js';
import process from 'node:process';
import { useHistory } from './hooks/useHistoryManager.js';
@@ -130,6 +132,10 @@ import {
QUEUE_ERROR_DISPLAY_DURATION_MS,
} from './constants.js';
import { LoginWithGoogleRestartDialog } from './auth/LoginWithGoogleRestartDialog.js';
import {
NewAgentsNotification,
NewAgentsChoice,
} from './components/NewAgentsNotification.js';
function isToolExecuting(pendingHistoryItems: HistoryItemWithoutId[]) {
return pendingHistoryItems.some((item) => {
@@ -204,6 +210,8 @@ export const AppContainer = (props: AppContainerProps) => {
null,
);
const [newAgents, setNewAgents] = useState<AgentDefinition[] | null>(null);
const [defaultBannerText, setDefaultBannerText] = useState('');
const [warningBannerText, setWarningBannerText] = useState('');
const [bannerVisible, setBannerVisible] = useState(true);
@@ -370,14 +378,20 @@ export const AppContainer = (props: AppContainerProps) => {
setAdminSettingsChanged(true);
};
const handleAgentsDiscovered = (payload: AgentsDiscoveredPayload) => {
setNewAgents(payload.agents);
};
coreEvents.on(CoreEvent.SettingsChanged, handleSettingsChanged);
coreEvents.on(CoreEvent.AdminSettingsChanged, handleAdminSettingsChanged);
coreEvents.on(CoreEvent.AgentsDiscovered, handleAgentsDiscovered);
return () => {
coreEvents.off(CoreEvent.SettingsChanged, handleSettingsChanged);
coreEvents.off(
CoreEvent.AdminSettingsChanged,
handleAdminSettingsChanged,
);
coreEvents.off(CoreEvent.AgentsDiscovered, handleAgentsDiscovered);
};
}, []);
@@ -1836,6 +1850,26 @@ Logging in with Google... Restarting Gemini CLI to continue.
);
}
if (newAgents) {
const handleNewAgentsSelect = (choice: NewAgentsChoice) => {
if (choice === NewAgentsChoice.ACKNOWLEDGE) {
const registry = config.getAgentRegistry();
newAgents.forEach((agent) => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
registry.acknowledgeAgent(agent);
});
}
setNewAgents(null);
};
return (
<NewAgentsNotification
agents={newAgents}
onSelect={handleNewAgentsSelect}
/>
);
}
return (
<UIStateContext.Provider value={uiState}>
<UIActionsContext.Provider value={uiActions}>
@@ -0,0 +1,90 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { Box, Text } from 'ink';
import { type AgentDefinition } from '@google/gemini-cli-core';
import { theme } from '../semantic-colors.js';
import {
RadioButtonSelect,
type RadioSelectItem,
} from './shared/RadioButtonSelect.js';
export enum NewAgentsChoice {
ACKNOWLEDGE = 'acknowledge',
IGNORE = 'ignore',
}
interface NewAgentsNotificationProps {
agents: AgentDefinition[];
onSelect: (choice: NewAgentsChoice) => void;
}
export const NewAgentsNotification = ({
agents,
onSelect,
}: NewAgentsNotificationProps) => {
const options: Array<RadioSelectItem<NewAgentsChoice>> = [
{
label: 'Acknowledge and Enable',
value: NewAgentsChoice.ACKNOWLEDGE,
key: 'acknowledge',
},
{
label: 'Do not enable (Ask again next time)',
value: NewAgentsChoice.IGNORE,
key: 'ignore',
},
];
// Limit display to 5 agents to avoid overflow, show count for rest
const displayAgents = agents.slice(0, 5);
const remaining = agents.length - 5;
return (
<Box flexDirection="column" width="100%">
<Box
flexDirection="column"
borderStyle="round"
borderColor={theme.status.warning}
padding={1}
marginLeft={1}
marginRight={1}
>
<Box flexDirection="column" marginBottom={1}>
<Text bold color={theme.text.primary}>
New Agents Discovered
</Text>
<Text color={theme.text.primary}>
The following agents were found in this project. Please review them:
</Text>
<Box
flexDirection="column"
marginTop={1}
marginBottom={1}
borderStyle="single"
padding={1}
>
{displayAgents.map((agent) => (
<Box key={agent.name} flexDirection="column" marginBottom={1}>
<Text bold>- {agent.name}</Text>
<Text color="gray"> {agent.description}</Text>
</Box>
))}
{remaining > 0 && (
<Text color="gray">... and {remaining} more.</Text>
)}
</Box>
</Box>
<RadioButtonSelect
items={options}
onSelect={onSelect}
isFocused={true}
/>
</Box>
</Box>
);
};