diff --git a/packages/cli/src/ui/components/StatusRow.test.tsx b/packages/cli/src/ui/components/StatusRow.test.tsx
index b80dbacabe..7ae1232e73 100644
--- a/packages/cli/src/ui/components/StatusRow.test.tsx
+++ b/packages/cli/src/ui/components/StatusRow.test.tsx
@@ -12,7 +12,7 @@ import { type UIState } from '../contexts/UIStateContext.js';
import { type TextBuffer } from '../components/shared/text-buffer.js';
import { type SessionStatsState } from '../contexts/SessionContext.js';
import { type ThoughtSummary } from '../types.js';
-import { ApprovalMode } from '@google/gemini-cli-core';
+import { ApprovalMode, makeFakeConfig } from '@google/gemini-cli-core';
vi.mock('../hooks/useComposerStatus.js', () => ({
useComposerStatus: vi.fn(),
@@ -142,4 +142,52 @@ describe('', () => {
await waitUntilReady();
expect(lastFrame()).toContain('Tip: Test Tip');
});
+
+ it('renders ActiveTeamIndicator with provider letters', async () => {
+ (useComposerStatus as Mock).mockReturnValue({
+ isInteractiveShellWaiting: false,
+ showLoadingIndicator: false,
+ showTips: false,
+ showWit: false,
+ modeContentObj: null,
+ showMinimalContext: false,
+ });
+
+ const mockTeam = {
+ name: 'poly-team',
+ displayName: 'Polyglot',
+ agents: [
+ { kind: 'local', name: 'g' },
+ { kind: 'external', name: 'c', provider: 'claude-code' },
+ { kind: 'external', name: 'x', provider: 'codex' },
+ ],
+ };
+
+ const config = makeFakeConfig();
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ config.getActiveTeam = () => mockTeam as any;
+
+ const { lastFrame, waitUntilReady } = await renderWithProviders(
+ ,
+ {
+ width: 100,
+ uiState: defaultUiState,
+ config,
+ },
+ );
+
+ await waitUntilReady();
+ const output = lastFrame();
+ expect(output).toContain('[ Team: Polyglot');
+ expect(output).toContain('Claude Cod');
+ expect(output).toContain('Codex');
+ expect(output).toContain('Gemini CLI');
+ });
});
diff --git a/packages/cli/src/ui/components/StatusRow.tsx b/packages/cli/src/ui/components/StatusRow.tsx
index ad168464fe..22280cc86d 100644
--- a/packages/cli/src/ui/components/StatusRow.tsx
+++ b/packages/cli/src/ui/components/StatusRow.tsx
@@ -4,8 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import type React from 'react';
-import { useCallback, useRef, useState } from 'react';
+import React, { useCallback, useRef, useState } from 'react';
import { Box, Text, ResizeObserver, type DOMElement } from 'ink';
import {
isUserVisibleHook,
@@ -147,6 +146,14 @@ export const StatusNode: React.FC<{
);
};
+const PROVIDER_COLORS: Record = {
+ 'claude-code': '#C15F3C', // Claude Orange (Exact)
+ codex: '#FFFFFF', // Codex White
+ gemini: '#A855F7', // Gemini Purple
+ antigravity: '#93C5FD', // Antigravity Light Blue
+ gemma: '#60A5FA', // Gemma Blue
+};
+
/**
* Renders an indicator for the currently active agent team.
*/
@@ -156,9 +163,44 @@ const ActiveTeamIndicator: React.FC = () => {
if (!activeTeam) return null;
+ const providers = new Set();
+ for (const agent of activeTeam.agents) {
+ if (agent.kind === 'external') {
+ providers.add(agent.provider);
+ } else {
+ providers.add('gemini');
+ }
+ }
+
+ const sortedProviders = Array.from(providers).sort();
+
return (
-
- [ Team: {activeTeam.displayName} ]
+
+ [ Team: {activeTeam.displayName} (
+ {sortedProviders.map((p, i) => {
+ const label =
+ p === 'claude-code'
+ ? 'Claude Code'
+ : p === 'codex'
+ ? 'Codex'
+ : p === 'antigravity'
+ ? 'Antigravity'
+ : p === 'gemma'
+ ? 'Gemma'
+ : 'Gemini CLI';
+ const color = PROVIDER_COLORS[p] || theme.text.secondary;
+ return (
+
+
+ {label}
+
+ {i < sortedProviders.length - 1 && (
+ ,
+ )}
+
+ );
+ })}
+ ) ]
);
};
diff --git a/packages/cli/src/ui/components/TeamSelectionDialog.test.tsx b/packages/cli/src/ui/components/TeamSelectionDialog.test.tsx
index f338d731f4..816352beb8 100644
--- a/packages/cli/src/ui/components/TeamSelectionDialog.test.tsx
+++ b/packages/cli/src/ui/components/TeamSelectionDialog.test.tsx
@@ -49,9 +49,10 @@ describe('', () => {
expect(output).toContain('First team description');
expect(output).toContain('Team Two');
expect(output).toContain('Second team description');
+ expect(output).toContain('The Polyglot Team (Curated)');
expect(output).toContain('No Team');
- expect(output).toContain('Browse Marketplace (Coming Soon)');
- expect(output).toContain('Create Team (Coming Soon)');
+ expect(output).toContain('Browse Team Marketplace');
+ expect(output).toContain('Create Team');
unmount();
});
@@ -73,15 +74,13 @@ describe('', () => {
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();
+ // Navigate to "No Team" (index 3: Team 1, Team 2, Polyglot, No Team)
+ for (let i = 0; i < 3; i++) {
+ await act(async () => {
+ stdin.write('\u001B[B'); // Down
+ });
+ await waitUntilReady();
+ }
await act(async () => {
stdin.write('\r'); // Enter
@@ -97,8 +96,8 @@ describe('', () => {
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++) {
+ // Navigate to "Browse Team Marketplace" (index 4)
+ for (let i = 0; i < 4; i++) {
await act(async () => {
stdin.write('\u001B[B'); // Down
});
@@ -113,4 +112,62 @@ describe('', () => {
expect(mockOnSelect).not.toHaveBeenCalled();
unmount();
});
+
+ it('shows marketplace sub-view when selected', async () => {
+ const { stdin, lastFrame, waitUntilReady, unmount } =
+ await renderComponent();
+
+ // Navigate to Marketplace (index 4)
+ for (let i = 0; i < 4; i++) {
+ await act(async () => {
+ stdin.write('\u001B[B'); // Down
+ });
+ await waitUntilReady();
+ }
+
+ await act(async () => {
+ stdin.write('\r'); // Enter
+ });
+ await waitUntilReady();
+
+ expect(lastFrame()).toContain('Agent Team Marketplace');
+ expect(lastFrame()).toContain('under development');
+
+ // Go back
+ await act(async () => {
+ stdin.write('a');
+ });
+ await waitUntilReady();
+ expect(lastFrame()).toContain('Select an Agent Team');
+ unmount();
+ });
+
+ it('shows create team sub-view when selected', async () => {
+ const { stdin, lastFrame, waitUntilReady, unmount } =
+ await renderComponent();
+
+ // Navigate to Create Team (index 5)
+ for (let i = 0; i < 5; i++) {
+ await act(async () => {
+ stdin.write('\u001B[B'); // Down
+ });
+ await waitUntilReady();
+ }
+
+ await act(async () => {
+ stdin.write('\r'); // Enter
+ });
+ await waitUntilReady();
+
+ expect(lastFrame()).toContain('Create New Agent Team');
+ expect(lastFrame()).toContain('Create a directory in .gemini/teams/');
+
+ // Go back
+ await act(async () => {
+ stdin.write('a');
+ });
+ await waitUntilReady();
+ expect(lastFrame()).toContain('Select an Agent Team');
+ unmount();
+ });
});
diff --git a/packages/cli/src/ui/components/TeamSelectionDialog.tsx b/packages/cli/src/ui/components/TeamSelectionDialog.tsx
index 32e7025dc7..40fdb04352 100644
--- a/packages/cli/src/ui/components/TeamSelectionDialog.tsx
+++ b/packages/cli/src/ui/components/TeamSelectionDialog.tsx
@@ -5,60 +5,186 @@
*/
import type React from 'react';
-import { useCallback, useMemo } from 'react';
+import { useCallback, useMemo, useState } from 'react';
import { Box, Text } from 'ink';
-import { type TeamDefinition } from '@google/gemini-cli-core';
+import {
+ type TeamDefinition,
+ type AgentDefinition,
+} from '@google/gemini-cli-core';
import { theme } from '../semantic-colors.js';
import { DescriptiveRadioButtonSelect } from './shared/DescriptiveRadioButtonSelect.js';
+import { useKeypress } from '../hooks/useKeypress.js';
interface TeamSelectionDialogProps {
teams: TeamDefinition[];
onSelect: (teamName: string | undefined) => void;
}
+const PROVIDER_COLORS: Record = {
+ 'claude-code': '#C15F3C', // Claude Orange (Exact)
+ codex: '#FFFFFF', // Codex White
+ gemini: '#A855F7', // Gemini Purple
+ antigravity: '#93C5FD', // Antigravity Light Blue
+ gemma: '#60A5FA', // Gemma Blue
+};
+
+const ProviderTag: React.FC<{ provider: string }> = ({ provider }) => {
+ const label =
+ provider === 'claude-code'
+ ? 'Claude Code'
+ : provider === 'codex'
+ ? 'Codex'
+ : provider === 'antigravity'
+ ? 'Antigravity'
+ : provider === 'gemma'
+ ? 'Gemma'
+ : 'Gemini CLI';
+ const color = PROVIDER_COLORS[provider] || theme.text.secondary;
+
+ return (
+
+ [{label}]
+
+ );
+};
+
+const MultiModelBadge: React.FC = () => (
+
+
+ Multi-Model
+
+
+);
+
+function getProviderTags(agents: AgentDefinition[]): React.ReactNode {
+ const providers = new Set();
+ for (const agent of agents) {
+ if (agent.kind === 'external') {
+ providers.add(agent.provider);
+ } else {
+ providers.add('gemini');
+ }
+ }
+
+ const sortedProviders = Array.from(providers).sort();
+ const isMulti = providers.size > 1;
+
+ return (
+
+
+ {sortedProviders.map((p, i) => (
+
+
+
+ ))}
+
+ {isMulti && }
+
+ );
+}
+
export function TeamSelectionDialog({
- teams,
+ teams: discoveredTeams,
onSelect,
}: TeamSelectionDialogProps): React.JSX.Element {
+ const [view, setView] = useState<'select' | 'marketplace' | 'create'>(
+ 'select',
+ );
+
+ useKeypress(
+ () => {
+ setView('select');
+ return true;
+ },
+ { isActive: view !== 'select' },
+ );
+
const options = useMemo(() => {
- const list = teams.map((team) => ({
+ const list = discoveredTeams.map((team) => ({
value: team.name,
- title: team.displayName,
+ title: team.displayName || team.name,
description: team.description,
key: team.name,
+ titleSuffix: getProviderTags(team.agents),
}));
+ // Curated "Marketplace" Teams (Hardcoded MVP)
+ const polyglotTeam: TeamDefinition = {
+ name: 'curated-polyglot',
+ displayName: 'The Polyglot Team',
+ description:
+ 'Advanced multi-model orchestration with Gemini, Claude Code, and Codex.',
+ instructions: 'Orchestrate across multiple specialized models.',
+ agents: [
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
+ {
+ kind: 'local',
+ name: 'gemini-expert',
+ description: 'Gemini Expert',
+ inputConfig: { type: 'object', properties: {}, required: [] },
+ } as unknown as AgentDefinition,
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
+ {
+ kind: 'external',
+ name: 'claude-coder',
+ provider: 'claude-code',
+ description: 'Claude Coder',
+ inputConfig: { type: 'object', properties: {}, required: [] },
+ } as unknown as AgentDefinition,
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
+ {
+ kind: 'external',
+ name: 'codex-architect',
+ provider: 'codex',
+ description: 'Codex Architect',
+ inputConfig: { type: 'object', properties: {}, required: [] },
+ } as unknown as AgentDefinition,
+ ],
+ };
+
+ list.push({
+ value: polyglotTeam.name,
+ title: `${polyglotTeam.displayName} (Curated)`,
+ description: polyglotTeam.description,
+ key: polyglotTeam.name,
+ titleSuffix: getProviderTags(polyglotTeam.agents),
+ });
+
list.push({
value: 'none',
title: 'No Team',
description: 'Continue with standard Gemini CLI experience',
key: 'none',
+ titleSuffix: undefined,
});
list.push({
value: 'marketplace',
- title: 'Browse Marketplace (Coming Soon)',
+ title: 'Browse Team Marketplace',
description: 'Discover and install teams from the community',
key: 'marketplace',
+ titleSuffix: undefined,
});
list.push({
value: 'create',
- title: 'Create Team (Coming Soon)',
+ title: 'Create Team',
description: 'Define your own agent team and orchestration instructions',
key: 'create',
+ titleSuffix: undefined,
});
return list;
- }, [teams]);
+ }, [discoveredTeams]);
const handleSelect = useCallback(
(value: string) => {
if (value === 'none') {
onSelect(undefined);
- } else if (value === 'marketplace' || value === 'create') {
- // No-op for coming soon features
- return;
+ } else if (value === 'marketplace') {
+ setView('marketplace');
+ } else if (value === 'create') {
+ setView('create');
} else {
onSelect(value);
}
@@ -66,6 +192,91 @@ export function TeamSelectionDialog({
[onSelect],
);
+ if (view === 'marketplace') {
+ return (
+
+
+ Agent Team Marketplace
+
+
+
+ Explore and download community-contributed agent teams.
+
+
+
+ The community marketplace is currently under development. Soon you
+ will be able to browse hundreds of specialized teams.
+
+
+
+
+ Press any key to go back to selection...
+
+
+
+
+ );
+ }
+
+ if (view === 'create') {
+ return (
+
+
+ Create New Agent Team
+
+
+
+ To create a new team, follow these simple steps:
+
+
+ 1. Create a directory in .gemini/teams/ (e.g.,
+ .gemini/teams/my-team/)
+
+
+ 2. Create a TEAM.md file with name, display_name, and description.
+
+
+ 3. (Optional) Add specialized agents to an agents/ sub-directory.
+
+
+
+
+ Tip: You can now specify external agents directly in TEAM.md!
+
+
+
+
+
+ Press any key to go back to selection...
+
+
+
+
+ );
+ }
+
return (
extends SelectionListItem {
title: string;
description?: string;
+ titleSuffix?: React.ReactNode;
}
export interface DescriptiveRadioButtonSelectProps {
@@ -61,7 +62,10 @@ export function DescriptiveRadioButtonSelect({
maxItemsToShow={maxItemsToShow}
renderItem={(item, { titleColor }) => (
- {item.title}
+
+ {item.title}
+ {item.titleSuffix && {item.titleSuffix}}
+
{item.description && (
{item.description}
)}
diff --git a/packages/cli/src/ui/contexts/KeypressContext.tsx b/packages/cli/src/ui/contexts/KeypressContext.tsx
index 3189172792..15b378ba22 100644
--- a/packages/cli/src/ui/contexts/KeypressContext.tsx
+++ b/packages/cli/src/ui/contexts/KeypressContext.tsx
@@ -575,6 +575,11 @@ function* emitKeys(
} else if ((match = /^(\d+)?(?:;(\d+))?([A-Za-z])$/.exec(cmd))) {
code += match[3];
modifier = parseInt(match[2] ?? match[1] ?? '1', 10) - 1;
+
+ // Skip focus-in/out events from useFocus hook
+ if (code === '[I' || code === '[O') {
+ continue;
+ }
} else {
code += cmd;
}
@@ -854,10 +859,9 @@ export function KeypressProvider({
useEffect(() => {
terminalCapabilityManager.enableSupportedModes();
- const wasRaw = stdin.isRaw;
- if (wasRaw === false) {
- setRawMode(true);
- }
+ // Always ensure raw mode is on while we are active.
+ // Ink handles reference counting for setRawMode(true/false) calls.
+ setRawMode(true);
process.stdin.setEncoding('utf8'); // Make data events emit strings
@@ -882,9 +886,7 @@ export function KeypressProvider({
stdin.on('data', dataListener);
return () => {
stdin.removeListener('data', dataListener);
- if (wasRaw === false) {
- setRawMode(false);
- }
+ setRawMode(false);
};
}, [stdin, setRawMode, config, debugKeystrokeLogging, broadcast]);
diff --git a/packages/core/src/agents/agentLoader.ts b/packages/core/src/agents/agentLoader.ts
index ba19f82b22..24a645a4fe 100644
--- a/packages/core/src/agents/agentLoader.ts
+++ b/packages/core/src/agents/agentLoader.ts
@@ -211,7 +211,7 @@ const remoteAgentSchema = z.union([
type FrontmatterRemoteAgentDefinition = z.infer;
-const externalAgentSchema = z
+export const externalAgentSchema = z
.object({
kind: z.literal('external'),
name: nameSchema,
diff --git a/packages/core/src/agents/external-invocation.test.ts b/packages/core/src/agents/external-invocation.test.ts
index 011860f291..8207015f7b 100644
--- a/packages/core/src/agents/external-invocation.test.ts
+++ b/packages/core/src/agents/external-invocation.test.ts
@@ -76,6 +76,30 @@ describe('ExternalAgentInvocation', () => {
);
});
+ it('should support Antigravity provider', () => {
+ const antigravityDef: ExternalAgentDefinition = {
+ ...externalDef,
+ provider: 'antigravity',
+ };
+ const polyfilled = polyfillExternalAgent(antigravityDef);
+
+ expect(polyfilled.promptConfig.systemPrompt).toContain(
+ 'Antigravity Personality Overlay',
+ );
+ });
+
+ it('should support Gemma provider', () => {
+ const gemmaDef: ExternalAgentDefinition = {
+ ...externalDef,
+ provider: 'gemma',
+ };
+ const polyfilled = polyfillExternalAgent(gemmaDef);
+
+ expect(polyfilled.promptConfig.systemPrompt).toContain(
+ 'Gemma Personality Overlay',
+ );
+ });
+
it('should include styleInstructions from providerConfig', () => {
const customDef: ExternalAgentDefinition = {
...externalDef,
diff --git a/packages/core/src/agents/external-invocation.ts b/packages/core/src/agents/external-invocation.ts
index 2291b204b8..cbb4a9a3c0 100644
--- a/packages/core/src/agents/external-invocation.ts
+++ b/packages/core/src/agents/external-invocation.ts
@@ -101,6 +101,24 @@ You are acting as the "Codex" agent, a specialized code generation model.
- Focus on generating high-quality, idiomatic, and correct code for the requested task.
- Prioritize structural integrity and idiomatic patterns for the target language.
- Provide clear, well-documented code snippets.
+${styleInstructions}`.trim();
+
+ case 'antigravity':
+ return `
+# Antigravity Personality Overlay
+You are acting as the "Antigravity" agent.
+- Focus on creative problem solving and thinking "outside the box".
+- Adopt a playful but highly competent engineering persona.
+- Encourage unconventional but effective solutions.
+${styleInstructions}`.trim();
+
+ case 'gemma':
+ return `
+# Gemma Personality Overlay
+You are acting as the "Gemma" agent, an open, lightweight, and capable model.
+- Focus on accessibility and efficiency.
+- Provide helpful, clear, and easy-to-understand explanations.
+- Maintain a friendly and supportive persona.
${styleInstructions}`.trim();
default:
diff --git a/packages/core/src/agents/teamLoader.ts b/packages/core/src/agents/teamLoader.ts
index dfedb720e4..f0cf5b43ba 100644
--- a/packages/core/src/agents/teamLoader.ts
+++ b/packages/core/src/agents/teamLoader.ts
@@ -11,7 +11,12 @@ import * as path from 'node:path';
import * as crypto from 'node:crypto';
import { z } from 'zod';
import { type TeamDefinition } from './types.js';
-import { AgentLoadError, loadAgentsFromDirectory } from './agentLoader.js';
+import {
+ AgentLoadError,
+ loadAgentsFromDirectory,
+ externalAgentSchema,
+ markdownToAgentDefinition,
+} from './agentLoader.js';
import { FRONTMATTER_REGEX } from '../skills/skillLoader.js';
import { getErrorMessage } from '../utils/errors.js';
@@ -32,6 +37,7 @@ const teamSchema = z
name: nameSchema,
display_name: z.string().min(1),
description: z.string().min(1),
+ agents: z.array(externalAgentSchema).optional(),
})
.strict();
@@ -121,18 +127,45 @@ export async function loadTeamsFromDirectory(
);
}
- const { name, display_name, description } = parsedFrontmatter.data;
+ const {
+ name,
+ display_name,
+ description,
+ agents: inlineAgentsRaw,
+ } = parsedFrontmatter.data;
// Load agents from agents/ subfolder
const agentsResult = await loadAgentsFromDirectory(agentsDirPath);
result.errors.push(...agentsResult.errors);
+ const allAgents = [...agentsResult.agents];
+
+ // Add inline agents
+ if (inlineAgentsRaw) {
+ for (const inline of inlineAgentsRaw) {
+ try {
+ const agent = markdownToAgentDefinition(inline, {
+ hash,
+ filePath: teamMdPath,
+ });
+ allAgents.push(agent);
+ } catch (error) {
+ result.errors.push(
+ new AgentLoadError(
+ teamMdPath,
+ `Error loading inline agent "${inline.name}": ${getErrorMessage(error)}`,
+ ),
+ );
+ }
+ }
+ }
+
result.teams.push({
name,
displayName: display_name,
description,
instructions,
- agents: agentsResult.agents,
+ agents: allAgents,
metadata: {
hash,
filePath: teamMdPath,