feat(cli): implement startup team selection dialog and UI integration

- Add TeamSelectionDialog component for team discovery UX
- Intercept startup flow in AppContainer to show team selection if needed
- Update DialogManager to orchestrate team selection UI
- Add handleTeamSelect and isTeamSelectionActive to UI contexts
- Fix pre-existing StyledLine compilation error in TableRenderer
- Refactor promptProvider-teams.test.ts to resolve type errors
- Add unit tests for TeamSelectionDialog
- Fix ESLint warnings for exhaustive-deps in AppContainer
- Resolve syntax corruptions in AppContainer.tsx and DialogManager.tsx
This commit is contained in:
Taylor Mullen
2026-04-01 15:58:16 -07:00
parent 5aba28c8be
commit 1c5646b68d
15 changed files with 554 additions and 15 deletions
@@ -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 { TeamSelectionDialog } from './TeamSelectionDialog.js';
import { theme } from '../semantic-colors.js';
import { useUIState } from '../contexts/UIStateContext.js';
import { useUIActions } from '../contexts/UIActionsContext.js';
@@ -60,6 +61,14 @@ export const DialogManager = ({
terminalWidth: uiTerminalWidth,
} = uiState;
if (uiState.isTeamSelectionActive) {
return (
<TeamSelectionDialog
teams={config.getTeamRegistry().getAllTeams()}
onSelect={uiActions.handleTeamSelect}
/>
);
}
if (uiState.adminSettingsChanged) {
return <AdminSettingsChangedDialog />;
}
@@ -0,0 +1,115 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi } from 'vitest';
import { act } from 'react';
import { TeamSelectionDialog } from './TeamSelectionDialog.js';
import { renderWithProviders } from '../../test-utils/render.js';
import { waitFor } from '../../test-utils/async.js';
import { type TeamDefinition } from '@google/gemini-cli-core';
describe('<TeamSelectionDialog />', () => {
const mockTeams: TeamDefinition[] = [
{
name: 'team-1',
displayName: 'Team One',
description: 'First team description',
instructions: 'Instructions 1',
agents: [],
},
{
name: 'team-2',
displayName: 'Team Two',
description: 'Second team description',
instructions: 'Instructions 2',
agents: [],
},
];
const mockOnSelect = vi.fn();
beforeEach(() => {
mockOnSelect.mockReset();
});
const renderComponent = async () => renderWithProviders(
<TeamSelectionDialog teams={mockTeams} onSelect={mockOnSelect} />,
);
it('renders all options correctly', async () => {
const { lastFrame, unmount } = await renderComponent();
const output = lastFrame();
expect(output).toContain('Select an Agent Team');
expect(output).toContain('Team One');
expect(output).toContain('First team description');
expect(output).toContain('Team Two');
expect(output).toContain('Second team description');
expect(output).toContain('No Team');
expect(output).toContain('Browse Marketplace (Coming Soon)');
expect(output).toContain('Create Team (Coming Soon)');
unmount();
});
it('calls onSelect with team name when a team is selected', async () => {
const { stdin, waitUntilReady, unmount } = await renderComponent();
// Default selection is index 0 (Team One)
await act(async () => {
stdin.write('\r'); // Enter
});
await waitUntilReady();
await waitFor(() => {
expect(mockOnSelect).toHaveBeenCalledWith('team-1');
});
unmount();
});
it('calls onSelect with undefined when "No Team" is selected', async () => {
const { stdin, waitUntilReady, unmount } = await renderComponent();
// Navigate to "No Team" (index 2)
await act(async () => {
stdin.write('\u001B[B'); // Down
});
await waitUntilReady();
await act(async () => {
stdin.write('\u001B[B'); // Down
});
await waitUntilReady();
await act(async () => {
stdin.write('\r'); // Enter
});
await waitUntilReady();
await waitFor(() => {
expect(mockOnSelect).toHaveBeenCalledWith(undefined);
});
unmount();
});
it('does not call onSelect for placeholder options', async () => {
const { stdin, waitUntilReady, unmount } = await renderComponent();
// Navigate to "Browse Marketplace" (index 3)
for (let i = 0; i < 3; i++) {
await act(async () => {
stdin.write('\u001B[B'); // Down
});
await waitUntilReady();
}
await act(async () => {
stdin.write('\r'); // Enter
});
await waitUntilReady();
expect(mockOnSelect).not.toHaveBeenCalled();
unmount();
});
});
@@ -0,0 +1,100 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
import { useCallback, useMemo } from 'react';
import { Box, Text } from 'ink';
import { type TeamDefinition } from '@google/gemini-cli-core';
import { theme } from '../semantic-colors.js';
import { DescriptiveRadioButtonSelect } from './shared/DescriptiveRadioButtonSelect.js';
interface TeamSelectionDialogProps {
teams: TeamDefinition[];
onSelect: (teamName: string | undefined) => void;
}
export function TeamSelectionDialog({
teams,
onSelect,
}: TeamSelectionDialogProps): React.JSX.Element {
const options = useMemo(() => {
const list = teams.map((team) => ({
value: team.name,
title: team.displayName,
description: team.description,
key: team.name,
}));
list.push({
value: 'none',
title: 'No Team',
description: 'Continue with standard Gemini CLI experience',
key: 'none',
});
list.push({
value: 'marketplace',
title: 'Browse Marketplace (Coming Soon)',
description: 'Discover and install teams from the community',
key: 'marketplace',
});
list.push({
value: 'create',
title: 'Create Team (Coming Soon)',
description: 'Define your own agent team and orchestration instructions',
key: 'create',
});
return list;
}, [teams]);
const handleSelect = useCallback(
(value: string) => {
if (value === 'none') {
onSelect(undefined);
} else if (value === 'marketplace' || value === 'create') {
// No-op for coming soon features
return;
} else {
onSelect(value);
}
},
[onSelect],
);
return (
<Box
borderStyle="round"
borderColor={theme.border.default}
flexDirection="column"
padding={1}
width="100%"
>
<Text bold color={theme.text.primary}>
Select an Agent Team
</Text>
<Text color={theme.text.secondary}>
Choose a specialized team to orchestrate your tasks.
</Text>
<Box marginTop={1}>
<DescriptiveRadioButtonSelect
items={options}
onSelect={handleSelect}
showNumbers={true}
maxItemsToShow={10}
/>
</Box>
<Box marginTop={1}>
<Text color={theme.text.secondary}>
(Use arrow keys to navigate, Enter to select)
</Text>
</Box>
</Box>
);
}