mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-10 01:50:20 -07:00
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:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user