mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-06-15 22:07:29 -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:
@@ -589,6 +589,7 @@ const mockUIActions: UIActions = {
|
||||
onHintSubmit: vi.fn(),
|
||||
handleRestart: vi.fn(),
|
||||
handleNewAgentsSelect: vi.fn(),
|
||||
handleTeamSelect: vi.fn(),
|
||||
getPreferredEditor: vi.fn(),
|
||||
clearAccountSuspension: vi.fn(),
|
||||
};
|
||||
|
||||
@@ -415,6 +415,7 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
);
|
||||
|
||||
const [isConfigInitialized, setConfigInitialized] = useState(false);
|
||||
const [isTeamSelectionActive, setIsTeamSelectionActive] = useState(false);
|
||||
|
||||
const logger = useLogger(config.storage);
|
||||
const { inputHistory, addInput, initializeFromLogger } =
|
||||
@@ -444,6 +445,16 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
if (!config.isInitialized()) {
|
||||
await config.initialize();
|
||||
}
|
||||
|
||||
// Check if team selection is needed
|
||||
const teamRegistry = config.getTeamRegistry();
|
||||
const availableTeams = teamRegistry.getAllTeams();
|
||||
const activeTeam = config.getActiveTeam();
|
||||
|
||||
if (availableTeams.length > 0 && !activeTeam) {
|
||||
setIsTeamSelectionActive(true);
|
||||
}
|
||||
|
||||
setConfigInitialized(true);
|
||||
startupProfiler.flush(config);
|
||||
|
||||
@@ -1407,6 +1418,15 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
|
||||
const { handleInput: vimHandleInput } = useVim(buffer, handleFinalSubmit);
|
||||
|
||||
const handleTeamSelect = useCallback(
|
||||
(teamName: string | undefined) => {
|
||||
config.setActiveTeam(teamName);
|
||||
setIsTeamSelectionActive(false);
|
||||
refreshStatic();
|
||||
},
|
||||
[config, refreshStatic],
|
||||
);
|
||||
|
||||
/**
|
||||
* Determines if the input prompt should be active and accept user input.
|
||||
* Input is disabled during:
|
||||
@@ -2046,6 +2066,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
const nightly = props.version.includes('nightly');
|
||||
|
||||
const dialogsVisible =
|
||||
isTeamSelectionActive ||
|
||||
shouldShowIdePrompt ||
|
||||
shouldShowIdePrompt ||
|
||||
isFolderTrustDialogOpen ||
|
||||
@@ -2377,6 +2398,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
isBackgroundTaskListOpen,
|
||||
adminSettingsChanged,
|
||||
newAgents,
|
||||
isTeamSelectionActive,
|
||||
showIsExpandableHint,
|
||||
hintMode:
|
||||
config.isModelSteeringEnabled() && isToolExecuting(pendingHistoryItems),
|
||||
@@ -2504,6 +2526,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
adminSettingsChanged,
|
||||
newAgents,
|
||||
showIsExpandableHint,
|
||||
isTeamSelectionActive,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -2600,6 +2623,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
}
|
||||
setNewAgents(null);
|
||||
},
|
||||
handleTeamSelect,
|
||||
getPreferredEditor,
|
||||
clearAccountSuspension: () => {
|
||||
setAccountSuspensionInfo(null);
|
||||
@@ -2661,6 +2685,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
config,
|
||||
historyManager,
|
||||
getPreferredEditor,
|
||||
handleTeamSelect,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -91,7 +91,9 @@ export interface UIActions {
|
||||
onHintSubmit: (hint: string) => void;
|
||||
handleRestart: () => void;
|
||||
handleNewAgentsSelect: (choice: NewAgentsChoice) => Promise<void>;
|
||||
getPreferredEditor: () => EditorType | undefined;
|
||||
handleTeamSelect: (teamName: string | undefined) => void;
|
||||
getPreferredEditor: () => EditorType;
|
||||
|
||||
clearAccountSuspension: () => void;
|
||||
}
|
||||
|
||||
|
||||
@@ -221,6 +221,7 @@ export interface UIState {
|
||||
isBackgroundTaskListOpen: boolean;
|
||||
adminSettingsChanged: boolean;
|
||||
newAgents: AgentDefinition[] | null;
|
||||
isTeamSelectionActive: boolean;
|
||||
showIsExpandableHint: boolean;
|
||||
hintMode: boolean;
|
||||
hintBuffer: string;
|
||||
|
||||
@@ -8,7 +8,6 @@ import React, { useMemo } from 'react';
|
||||
import {
|
||||
Text,
|
||||
Box,
|
||||
StyledLine,
|
||||
toStyledCharacters,
|
||||
wordBreakStyledChars,
|
||||
wrapStyledChars,
|
||||
@@ -16,6 +15,12 @@ import {
|
||||
styledCharsWidth,
|
||||
styledCharsToString,
|
||||
} from 'ink';
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||
type StyledLine = any;
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { parseMarkdownToANSI } from './markdownParsingUtils.js';
|
||||
import { stripUnsafeCharacters } from './textUtils.js';
|
||||
@@ -100,12 +105,12 @@ export const TableRenderer: React.FC<TableRendererProps> = ({
|
||||
// --- Define Constraints per Column ---
|
||||
const constraints = Array.from({ length: numColumns }).map(
|
||||
(_, colIndex) => {
|
||||
const headerStyledLine = styledHeaders[colIndex] || StyledLine.empty(0);
|
||||
const headerStyledLine = styledHeaders[colIndex] || [];
|
||||
let { contentWidth: maxContentWidth, maxWordWidth } =
|
||||
calculateWidths(headerStyledLine);
|
||||
|
||||
styledRows.forEach((row) => {
|
||||
const cellStyledLine = row[colIndex] || StyledLine.empty(0);
|
||||
const cellStyledLine = row[colIndex] || [];
|
||||
const { contentWidth: cellWidth, maxWordWidth: cellWordWidth } =
|
||||
calculateWidths(cellStyledLine);
|
||||
|
||||
@@ -180,7 +185,7 @@ export const TableRenderer: React.FC<TableRendererProps> = ({
|
||||
const rowResult: ProcessedLine[][] = [];
|
||||
// Ensure we iterate up to numColumns, filling with empty cells if needed
|
||||
for (let colIndex = 0; colIndex < numColumns; colIndex++) {
|
||||
const cellStyledLine = row[colIndex] || StyledLine.empty(0);
|
||||
const cellStyledLine = row[colIndex] || [];
|
||||
const allocatedWidth = finalContentWidths[colIndex];
|
||||
const contentWidth = Math.max(1, allocatedWidth);
|
||||
|
||||
@@ -210,7 +215,7 @@ export const TableRenderer: React.FC<TableRendererProps> = ({
|
||||
// Use the TIGHTEST widths that fit the wrapped content + padding
|
||||
const adjustedWidths = actualColumnWidths.map(
|
||||
(w) =>
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
|
||||
w + COLUMN_PADDING,
|
||||
);
|
||||
|
||||
@@ -263,7 +268,7 @@ export const TableRenderer: React.FC<TableRendererProps> = ({
|
||||
isHeader = false,
|
||||
): React.ReactNode => {
|
||||
const renderedCells = cells.map((cell, index) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
|
||||
const width = adjustedWidths[index] || 0;
|
||||
return renderCell(cell, width, isHeader);
|
||||
});
|
||||
|
||||
@@ -12,9 +12,11 @@ import { type TeamDefinition } from '../agents/types.js';
|
||||
describe('PromptProvider with Agent Teams', () => {
|
||||
let promptProvider: PromptProvider;
|
||||
let mockContext: AgentLoopContext;
|
||||
const mockGetActiveTeam = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
promptProvider = new PromptProvider();
|
||||
mockGetActiveTeam.mockReturnValue(undefined);
|
||||
mockContext = {
|
||||
config: {
|
||||
isInteractive: vi.fn().mockReturnValue(true),
|
||||
@@ -23,7 +25,7 @@ describe('PromptProvider with Agent Teams', () => {
|
||||
.fn()
|
||||
.mockReturnValue({ getAllDefinitions: () => [] }),
|
||||
getActiveModel: vi.fn().mockReturnValue('gemini-3.1-pro-preview'),
|
||||
getActiveTeam: vi.fn().mockReturnValue(undefined),
|
||||
getActiveTeam: mockGetActiveTeam,
|
||||
getApprovedPlanPath: vi.fn().mockReturnValue(undefined),
|
||||
getApprovalMode: vi.fn().mockReturnValue('default'),
|
||||
getGemini31LaunchedSync: vi.fn().mockReturnValue(true),
|
||||
@@ -50,9 +52,7 @@ describe('PromptProvider with Agent Teams', () => {
|
||||
});
|
||||
|
||||
it('should not include team section when no team is active', () => {
|
||||
const prompt = promptProvider.getCoreSystemPrompt(
|
||||
mockContext as unknown as AgentLoopContext,
|
||||
);
|
||||
const prompt = promptProvider.getCoreSystemPrompt(mockContext);
|
||||
expect(prompt).not.toContain('# Active Agent Team');
|
||||
});
|
||||
|
||||
@@ -64,11 +64,9 @@ describe('PromptProvider with Agent Teams', () => {
|
||||
instructions: 'These are the team instructions.',
|
||||
agents: [],
|
||||
};
|
||||
mockContext.config.getActiveTeam.mockReturnValue(mockTeam);
|
||||
mockGetActiveTeam.mockReturnValue(mockTeam);
|
||||
|
||||
const prompt = promptProvider.getCoreSystemPrompt(
|
||||
mockContext as unknown as AgentLoopContext,
|
||||
);
|
||||
const prompt = promptProvider.getCoreSystemPrompt(mockContext);
|
||||
expect(prompt).toContain('# Active Agent Team: Test Team');
|
||||
expect(prompt).toContain('These are the team instructions.');
|
||||
expect(prompt).toContain(
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
# TASK-01: Agent Team Definitions, Loader, and Registry
|
||||
|
||||
## Objective
|
||||
|
||||
Define the fundamental data models and services for discovering, loading, and
|
||||
managing Agent Teams within the `packages/core` module.
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### 1. Types (`packages/core/src/agents/types.ts`)
|
||||
|
||||
- [ ] Define the `TeamDefinition` interface:
|
||||
```typescript
|
||||
export interface TeamDefinition {
|
||||
name: string; // The directory name (slug)
|
||||
displayName: string; // From frontmatter 'display_name'
|
||||
description: string; // From frontmatter 'description'
|
||||
instructions: string; // The body text of TEAM.md
|
||||
agents: AgentDefinition[]; // Loaded from the 'agents/' subfolder
|
||||
metadata?: {
|
||||
hash?: string;
|
||||
filePath?: string;
|
||||
};
|
||||
}
|
||||
```
|
||||
- [ ] Extend existing schemas (e.g., using `zod`) to support the metadata
|
||||
validation for teams.
|
||||
|
||||
### 2. Team Loader (`packages/core/src/agents/teamLoader.ts`)
|
||||
|
||||
- [ ] Create a new service to scan directories for teams.
|
||||
- [ ] `loadTeamsFromDirectory(dir: string)`:
|
||||
- [ ] Reads all subdirectories in `dir`.
|
||||
- [ ] For each subdirectory, looks for `TEAM.md`.
|
||||
- [ ] Uses `parseAgentMarkdown` (from `agentLoader.ts`) or similar logic to
|
||||
extract frontmatter and instructions.
|
||||
- [ ] Looks for an `agents/` subfolder within the team directory.
|
||||
- [ ] Calls `loadAgentsFromDirectory` (from `agentLoader.ts`) on that
|
||||
subfolder.
|
||||
- [ ] Returns an array of `TeamDefinition` and any loading errors.
|
||||
|
||||
### 3. Team Registry (`packages/core/src/agents/teamRegistry.ts`)
|
||||
|
||||
- [ ] Create a new `TeamRegistry` class to manage loaded teams.
|
||||
- [ ] Methods:
|
||||
- [ ] `initialize()`: Triggers team discovery.
|
||||
- [ ] `getAllTeams()`: Returns all loaded teams.
|
||||
- [ ] `setActiveTeam(name: string)`: Sets the current active team.
|
||||
- [ ] `getActiveTeam()`: Returns the currently active `TeamDefinition`.
|
||||
- [ ] Ensure that agents loaded as part of a team are also registered in the
|
||||
global `AgentRegistry` (so they can be surfaced as `SubagentTool`s).
|
||||
|
||||
## Verification
|
||||
|
||||
- Unit tests in `packages/core/src/agents/teamLoader.test.ts`.
|
||||
- Unit tests in `packages/core/src/agents/teamRegistry.test.ts`.
|
||||
- Verify that a mocked team directory structure is correctly parsed into a
|
||||
`TeamDefinition` list.
|
||||
@@ -0,0 +1,40 @@
|
||||
# TASK-02: Integrating Team Registry and Discovery into Config
|
||||
|
||||
## Objective
|
||||
|
||||
Embed the `TeamRegistry` into the `Config` service and manage the team discovery
|
||||
lifecycle during application startup and configuration reloads.
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### 1. Config Modifications (`packages/core/src/config/config.ts`)
|
||||
|
||||
- [ ] Add a private `teamRegistry` member:
|
||||
`private teamRegistry!: TeamRegistry;`.
|
||||
- [ ] In the `Config` constructor or initializer:
|
||||
- [ ] Initialize `teamRegistry = new TeamRegistry(this);`.
|
||||
- [ ] Call `await this.teamRegistry.initialize();`.
|
||||
- [ ] Update `reloadAgents()` to also reload the `TeamRegistry`.
|
||||
- [ ] Provide a public accessor: `getTeamRegistry(): TeamRegistry`.
|
||||
- [ ] Implement `getActiveTeam()` and `setActiveTeam(name: string | undefined)`
|
||||
delegates to the registry.
|
||||
|
||||
### 2. Discovery Path
|
||||
|
||||
- [ ] Ensure `TeamRegistry` scans the correct paths:
|
||||
- [ ] User-level: `~/.gemini/teams/`
|
||||
- [ ] Project-level: `.gemini/teams/`
|
||||
|
||||
### 3. State Persistence
|
||||
|
||||
- [ ] While the MVP can use memory, consider if the active team should be saved
|
||||
in `settings.json` via the `SettingsService` so it persists across
|
||||
sessions.
|
||||
|
||||
## Verification
|
||||
|
||||
- Unit tests for `Config.getTeamRegistry()`.
|
||||
- Integration tests ensuring that a team in `.gemini/teams/` is discovered and
|
||||
loaded during `Config` initialization.
|
||||
- Test that reloading the configuration correctly refreshes the list of
|
||||
available teams.
|
||||
@@ -0,0 +1,40 @@
|
||||
# TASK-03: Team-aware Orchestration: Prompting and Tooling
|
||||
|
||||
## Objective
|
||||
|
||||
Enable the top-level Gemini CLI agent to use the instructions and agents of an
|
||||
active team to fulfill user requests via delegation.
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### 1. System Prompt Engineering (`packages/core/src/prompts/promptProvider.ts`)
|
||||
|
||||
- [ ] Add `activeTeam?: TeamDefinition` to the `SystemPromptOptions` interface.
|
||||
- [ ] In `PromptProvider.getCoreSystemPrompt(context, ...)`:
|
||||
- [ ] If an active team exists, retrieve its `TeamDefinition`.
|
||||
- [ ] Pass the active team to the prompt composition logic.
|
||||
- [ ] Update `snippets.ts`:
|
||||
- [ ] Add `renderActiveTeam(team?: TeamDefinition)`:
|
||||
- [ ] If a team is active, render a section:
|
||||
```
|
||||
# Active Agent Team: ${team.displayName}
|
||||
${team.instructions}
|
||||
You should prioritize delegating tasks to this team's agents whenever appropriate.
|
||||
```
|
||||
- [ ] Update `getCoreSystemPrompt` to call `renderActiveTeam`.
|
||||
|
||||
### 2. Tool Prioritization (`packages/core/src/config/config.ts`)
|
||||
|
||||
- [ ] In `registerSubAgentTools(registry: ToolRegistry)`:
|
||||
- [ ] Identify which sub-agents are part of the active team.
|
||||
- [ ] Enhance the descriptions of `SubagentTool`s that belong to the team to
|
||||
indicate they are part of the active team.
|
||||
- [ ] Consider adding a priority flag to team tools so they appear higher in
|
||||
the model's tool list.
|
||||
|
||||
## Verification
|
||||
|
||||
- Unit tests for system prompt generation with and without an active team.
|
||||
- Manual inspection of the prompt context in debug logs to verify team
|
||||
instructions injection.
|
||||
- Verify that team agents are listed as available tools for the top-level agent.
|
||||
@@ -0,0 +1,39 @@
|
||||
# TASK-04: CLI UX: Startup Team Selection Dialog
|
||||
|
||||
## Objective
|
||||
|
||||
Introduce a selection dialog during the CLI startup process to allow users to
|
||||
choose an agent team, browse existing teams, or create new ones.
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### 1. New UI Component (`packages/cli/src/ui/components/TeamSelectionDialog.tsx`)
|
||||
|
||||
- [ ] Create a new React/Ink component that displays a list of available teams.
|
||||
- [ ] Options should include:
|
||||
- [ ] Specific discovered teams (name + description).
|
||||
- [ ] "No Team" (continue as individual agent).
|
||||
- [ ] "Browse Team Marketplace" (placeholder for MVP).
|
||||
- [ ] "Create New Team" (placeholder for MVP).
|
||||
- [ ] Use `SelectInput` from `@inkjs/select-input` for selection.
|
||||
|
||||
### 2. Startup Flow (`packages/cli/src/ui/AppContainer.tsx`)
|
||||
|
||||
- [ ] Add state: `isTeamSelectionActive: boolean`.
|
||||
- [ ] In `useEffect` or `useLayoutEffect` that runs once after initialization:
|
||||
- [ ] If multiple teams are available and no active team is set in the
|
||||
session/config:
|
||||
- [ ] Set `isTeamSelectionActive(true)`.
|
||||
- [ ] Intercept the render flow:
|
||||
- [ ] If `isTeamSelectionActive`, render the `TeamSelectionDialog`.
|
||||
- [ ] Implement `handleTeamSelect(teamName: string | undefined)`:
|
||||
- [ ] Call `config.setActiveTeam(teamName)`.
|
||||
- [ ] Set `isTeamSelectionActive(false)`.
|
||||
|
||||
## Verification
|
||||
|
||||
- Manual verification of the startup screen.
|
||||
- Test selection of a team and confirm the CLI proceeds to the main chat.
|
||||
- Test the "No Team" option.
|
||||
- Verify that if only one team exists, the dialog can be bypassed (optional UX
|
||||
polish).
|
||||
@@ -0,0 +1,44 @@
|
||||
# TASK-05: Active Team Visual Indicator and Sample Coding Team
|
||||
|
||||
## Objective
|
||||
|
||||
Provide visual feedback to the user when a team is active and create a sample
|
||||
"Coding Team" to verify the end-to-end orchestration flow.
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### 1. Status Indicator (`packages/cli/src/ui/components/StatusRow.tsx`)
|
||||
|
||||
- [ ] Add an `ActiveTeamIndicator` component.
|
||||
- [ ] Render the indicator within the `StatusRow` (next to Shell or Approval
|
||||
mode indicators).
|
||||
- [ ] If a team is active, display: `[ Team: ${displayName} ]` in a distinctive
|
||||
color.
|
||||
- [ ] Ensure that it is clearly visible during idle states.
|
||||
|
||||
### 2. Sample Coding Team
|
||||
|
||||
- [ ] Create `.gemini/teams/coding-team/`.
|
||||
- [ ] Create `TEAM.md`:
|
||||
- [ ] Frontmatter: `name: coding-team`, `display_name: Coding Team`,
|
||||
`description: A team of coder, reviewer, and tester agents.`
|
||||
- [ ] Body:
|
||||
```
|
||||
You are managing a software development task.
|
||||
Use the 'coder' agent for implementing new code or major refactors.
|
||||
Use the 'tester' agent to write or run tests for the changes.
|
||||
Use the 'reviewer' agent to ensure the code and tests meet quality standards.
|
||||
Prioritize delegating to these specialists rather than performing the implementation yourself.
|
||||
```
|
||||
- [ ] Create `.gemini/teams/coding-team/agents/`:
|
||||
- [ ] `coder.md`: Agent with code editing tools.
|
||||
- [ ] `reviewer.md`: Agent with code review instructions.
|
||||
- [ ] `tester.md`: Agent with test running tools.
|
||||
|
||||
## Verification
|
||||
|
||||
- Manual verification of the active team indicator.
|
||||
- End-to-end verification of the Coding Team by asking a complex task (e.g.,
|
||||
"Implement feature X with tests and review").
|
||||
- Verify that the top-level agent correctly delegates to the team agents as
|
||||
instructed.
|
||||
@@ -0,0 +1,62 @@
|
||||
# Agent Teams: Research & Implementation Specification
|
||||
|
||||
This document serves as a detailed design specification for the "Agent Teams"
|
||||
feature. It is intended to be used as a blueprint for implementation by an
|
||||
automated agent.
|
||||
|
||||
## Core Concept
|
||||
|
||||
Agent Teams are specialized collections of sub-agents orchestrated by the
|
||||
top-level Gemini CLI agent. Each team provides a set of tools (via its agents)
|
||||
and a set of instructions (via `TEAM.md`) that guide the top-level agent on how
|
||||
to delegate work effectively.
|
||||
|
||||
## Architecture & Data Flow
|
||||
|
||||
### 1. Storage & Discovery
|
||||
|
||||
Teams are stored in `.gemini/teams/`. Each team is a directory containing:
|
||||
|
||||
- `TEAM.md`: A Markdown file with YAML frontmatter for metadata (`name`,
|
||||
`display_name`, `description`) and a body containing orchestration
|
||||
instructions.
|
||||
- `agents/`: A sub-directory containing standard agent definitions (`.md` files)
|
||||
that comprise the team.
|
||||
|
||||
### 2. Core Components
|
||||
|
||||
- **`TeamDefinition`**: The internal representation of a team, including its
|
||||
instructions and associated `AgentDefinition`s.
|
||||
- **`TeamLoader`**: Responsible for scanning the filesystem, parsing `TEAM.md`,
|
||||
and loading team agents.
|
||||
- **`TeamRegistry`**: Central manager for all discovered teams and the active
|
||||
team session state.
|
||||
- **`SubagentTool` Prioritization**: Logic in the `ToolRegistry` or `Config` to
|
||||
surface team-specific agents as preferred tools.
|
||||
|
||||
### 3. Orchestration
|
||||
|
||||
The top-level Gemini CLI agent is modified to be "team-aware":
|
||||
|
||||
- Its **System Prompt** is dynamically updated to include the active team's
|
||||
instructions.
|
||||
- It is instructed to **prioritize delegation** to team agents for tasks
|
||||
matching the team's purpose.
|
||||
|
||||
### 4. User Experience
|
||||
|
||||
- **Startup Selection**: Users are prompted to select a team on launch if
|
||||
multiple teams are available and no default is set.
|
||||
- **Active Team Indicator**: A clear visual status in the CLI showing the
|
||||
currently active team.
|
||||
|
||||
## Execution Order
|
||||
|
||||
The implementation should follow this strict sequence to ensure a solid
|
||||
foundation before moving to UI:
|
||||
|
||||
1. **TASK-01**: Core Types, Loader, and Registry.
|
||||
2. **TASK-02**: Integration into `Config` and lifecycle management.
|
||||
3. **TASK-03**: Prompt Engineering and Tool Prioritization logic.
|
||||
4. **TASK-04**: CLI Startup UX (Selection Dialog).
|
||||
5. **TASK-05**: CLI Main UI (Status Indicator) and Sample Team creation.
|
||||
Reference in New Issue
Block a user