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
+1
View File
@@ -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(),
};
+25
View File
@@ -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;
+11 -6
View File
@@ -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(
+58
View File
@@ -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.
+40
View File
@@ -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.
+40
View File
@@ -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.
+39
View File
@@ -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).
+44
View File
@@ -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.
+62
View File
@@ -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.