diff --git a/packages/cli/src/config/extensions/github.test.ts b/packages/cli/src/config/extensions/github.test.ts
index 117d65a0c7..0c53155ec7 100644
--- a/packages/cli/src/config/extensions/github.test.ts
+++ b/packages/cli/src/config/extensions/github.test.ts
@@ -12,6 +12,7 @@ import type { ExtensionManager } from '../extension-manager.js';
import {
fetchReleaseFromGithub,
type GeminiCLIExtension,
+ type GithubReleaseData,
} from '@google/gemini-cli-core';
import type { ExtensionConfig } from '../extension.js';
@@ -150,7 +151,10 @@ describe('github.ts (CLI specific)', () => {
});
it('should return UPDATE_AVAILABLE if github release tag differs', async () => {
- vi.mocked(fetchReleaseFromGithub).mockResolvedValue({ tag_name: 'v2.0.0' } as any);
+ vi.mocked(fetchReleaseFromGithub).mockResolvedValue({
+ tag_name: 'v2.0.0',
+ assets: [],
+ } as unknown as GithubReleaseData);
const ext = {
installMetadata: {
diff --git a/packages/cli/src/test-utils/render.tsx b/packages/cli/src/test-utils/render.tsx
index 47974b194e..e8a4e7f249 100644
--- a/packages/cli/src/test-utils/render.tsx
+++ b/packages/cli/src/test-utils/render.tsx
@@ -595,6 +595,7 @@ export const mockUIActions: UIActions = {
handleRestart: vi.fn(),
handleNewAgentsSelect: vi.fn(),
handleTeamSelect: vi.fn(),
+ handleRefreshTeams: vi.fn(),
setIsTeamCreatorActive: vi.fn(),
getPreferredEditor: vi.fn(),
clearAccountSuspension: vi.fn(),
diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx
index 02ed67b230..f908b149c1 100644
--- a/packages/cli/src/ui/AppContainer.tsx
+++ b/packages/cli/src/ui/AppContainer.tsx
@@ -1431,6 +1431,11 @@ Logging in with Google... Restarting Gemini CLI to continue.
},
[config, refreshStatic],
);
+
+ const handleRefreshTeams = useCallback(async () => {
+ await config.getTeamRegistry().reload();
+ setForceRerenderKey((prev) => prev + 1);
+ }, [config]);
/**
* Determines if the input prompt should be active and accept user input.
* Input is disabled during:
@@ -2631,6 +2636,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
setNewAgents(null);
},
handleTeamSelect,
+ handleRefreshTeams,
setIsTeamCreatorActive,
getPreferredEditor,
clearAccountSuspension: () => {
@@ -2694,6 +2700,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
historyManager,
getPreferredEditor,
handleTeamSelect,
+ handleRefreshTeams,
setIsTeamCreatorActive,
],
);
diff --git a/packages/cli/src/ui/components/DialogManager.tsx b/packages/cli/src/ui/components/DialogManager.tsx
index 604b835bc5..fd560effef 100644
--- a/packages/cli/src/ui/components/DialogManager.tsx
+++ b/packages/cli/src/ui/components/DialogManager.tsx
@@ -67,6 +67,7 @@ export const DialogManager = ({
);
}
diff --git a/packages/cli/src/ui/components/StatusRow.tsx b/packages/cli/src/ui/components/StatusRow.tsx
index 27f0bcc908..2c3d924b54 100644
--- a/packages/cli/src/ui/components/StatusRow.tsx
+++ b/packages/cli/src/ui/components/StatusRow.tsx
@@ -25,6 +25,7 @@ import { HorizontalLine } from './shared/HorizontalLine.js';
import { ApprovalModeIndicator } from './ApprovalModeIndicator.js';
import { ShellModeIndicator } from './ShellModeIndicator.js';
import { RawMarkdownIndicator } from './RawMarkdownIndicator.js';
+import { ProviderTag } from './shared/ProviderTag.js';
import { useComposerStatus } from '../hooks/useComposerStatus.js';
/**
@@ -146,14 +147,6 @@ 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.
*/
@@ -180,29 +173,14 @@ const ActiveTeamIndicator: React.FC = () => {
return (
[ 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 && (
- ,
- )}
-
- );
- })}
+ {sortedProviders.map((p, i) => (
+
+
+ {i < sortedProviders.length - 1 && (
+ ,
+ )}
+
+ ))}
) ]
);
diff --git a/packages/cli/src/ui/components/TeamCreatorWizard.tsx b/packages/cli/src/ui/components/TeamCreatorWizard.tsx
index 4c34b6da35..ed419e4f03 100644
--- a/packages/cli/src/ui/components/TeamCreatorWizard.tsx
+++ b/packages/cli/src/ui/components/TeamCreatorWizard.tsx
@@ -10,7 +10,6 @@ import { Box, Text, useStdin } from 'ink';
import {
type ScaffoldTeamAgent,
scaffoldTeam,
- type EditorType,
} from '@google/gemini-cli-core';
import { theme } from '../semantic-colors.js';
import { TextInput } from './shared/TextInput.js';
@@ -18,13 +17,12 @@ import { useTextBuffer } from './shared/text-buffer.js';
import { BaseSelectionList } from './shared/BaseSelectionList.js';
import { DialogFooter } from './shared/DialogFooter.js';
import { useConfig } from '../contexts/ConfigContext.js';
-import { useUIActions } from '../contexts/UIActionsContext.js';
-import { useKeypress, type Key } from '../hooks/useKeypress.js';
+import { useKeypress } from '../hooks/useKeypress.js';
import { Command } from '../key/keyMatchers.js';
import { useKeyMatchers } from '../hooks/useKeyMatchers.js';
import { type SelectionListItem } from '../hooks/useSelectionList.js';
-import { useSettingsStore } from '../contexts/SettingsContext.js';
import { formatCommand } from '../key/keybindingUtils.js';
+import { ProviderTag, PROVIDER_COLORS } from './shared/ProviderTag.js';
interface TeamCreatorWizardProps {
onComplete: () => void;
@@ -38,14 +36,6 @@ type WizardStep =
| 'confirmation'
| 'success';
-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 EXTERNAL_TEMPLATES: RosterListItem[] = [
{
name: 'claude-coder',
@@ -79,7 +69,7 @@ const EXTERNAL_TEMPLATES: RosterListItem[] = [
interface RosterListItem {
name: string;
- kind: 'local' | 'external' | 'meta';
+ kind: 'local' | 'external';
provider?: string;
description?: string;
sourcePath?: string;
@@ -87,31 +77,11 @@ interface RosterListItem {
logoColor?: string;
}
-function ProviderTag({ provider }: { provider: string }) {
- const label =
- provider === 'claude-code'
- ? 'Claude Code'
- : provider === 'codex'
- ? 'Codex'
- : provider === 'antigravity'
- ? 'Antigravity'
- : provider === 'gemma'
- ? 'Gemma'
- : 'Gemini CLI';
- const color = PROVIDER_COLORS[provider] || theme.ui.comment;
- return (
-
- [{label}]
-
- );
-}
-
export function TeamCreatorWizard({
onComplete,
onCancel,
}: TeamCreatorWizardProps): React.JSX.Element {
const config = useConfig();
- const { handleTeamSelect } = useUIActions();
const keyMatchers = useKeyMatchers();
const [step, setStep] = useState('identity');
const [teamName, setTeamName] = useState('');
@@ -122,7 +92,7 @@ export function TeamCreatorWizard({
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState(null);
const [createdPath, setCreatedPath] = useState(null);
- const [rosterMode, setRosterMode] = useState<'list' | 'action'>('list');
+ const [rosterMode] = useState<'list' | 'action'>('list');
const localAgents = useMemo(
() =>
@@ -152,270 +122,136 @@ export function TeamCreatorWizard({
name: a.name,
kind: 'local' as const,
description: a.description,
- sourcePath: a.metadata?.filePath,
- logo: !a.metadata?.filePath ? logo : undefined, // Only for built-ins
- logoColor: !a.metadata?.filePath ? logoColor : undefined,
+ sourcePath: (a as any).sourcePath,
+ logo,
+ logoColor,
};
});
- const external: RosterListItem[] = EXTERNAL_TEMPLATES.map((a) => ({
- name: a.name,
- kind: 'external' as const,
- provider: a.provider,
- description: a.description,
- logo: a.logo,
- logoColor: a.logoColor,
- }));
+
+ const external: RosterListItem[] = EXTERNAL_TEMPLATES;
+
return [...local, ...external];
}, [localAgents]);
- const { settings } = useSettingsStore();
- const { stdin, setRawMode } = useStdin();
- const getPreferredEditor = useCallback(
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
- () => settings.merged.general.preferredEditor as EditorType,
- [settings.merged.general.preferredEditor],
- );
+ const teamProviders = useMemo(() => {
+ const providers = new Set();
+ for (const name of selectedAgents) {
+ const agent = allAvailableAgents.find((a) => a.name === name);
+ if (agent?.provider) {
+ providers.add(agent.provider);
+ } else {
+ providers.add('gemini');
+ }
+ }
+ return Array.from(providers).sort();
+ }, [selectedAgents, allAvailableAgents]);
const handleNext = useCallback(() => {
if (step === 'identity') {
- if (!teamName.trim()) {
- setError('Slug is required');
+ if (!teamName || !displayName) {
+ setError('Team ID and Display Name are required.');
return;
}
- if (!displayName.trim()) {
- setError('Display Name is required');
- return;
- }
- setError(null);
setStep('roster');
} else if (step === 'roster') {
setStep('objective');
} else if (step === 'objective') {
- if (!instructions.trim()) {
- setError('Objective/Instructions are required');
+ if (!instructions) {
+ setError('Team mission instructions are required.');
return;
}
- setError(null);
setStep('confirmation');
+ } else if (step === 'confirmation') {
+ void handleSubmit();
}
}, [step, teamName, displayName, instructions]);
- const handleBack = useCallback(() => {
- if (step === 'roster') {
- setStep('identity');
- setRosterMode('list'); // Reset roster mode when going back
- } else if (step === 'objective') {
- setStep('roster');
- } else if (step === 'confirmation') {
- setStep('objective');
- } else if (step === 'identity') {
- onCancel();
- } else if (step === 'success') {
- onComplete();
- }
- }, [step, onCancel, onComplete]);
-
- const handleToggleAgent = (name: string) => {
- const next = new Set(selectedAgents);
- if (next.has(name)) next.delete(name);
- else next.add(name);
- setSelectedAgents(next);
- };
-
- const handleCreate = useCallback(async () => {
+ const handleSubmit = async () => {
setIsSubmitting(true);
setError(null);
try {
- const agentsToScaffold: ScaffoldTeamAgent[] = allAvailableAgents
- .filter((a) => selectedAgents.has(a.name) && a.kind !== 'meta')
- .map((a) => ({
- name: a.name,
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
- kind: a.kind as 'local' | 'external',
- provider: a.provider,
- description: a.description,
- sourcePath: a.sourcePath,
- }));
+ const agents: ScaffoldTeamAgent[] = Array.from(selectedAgents).map(
+ (name) => {
+ const agent = allAvailableAgents.find((a) => a.name === name)!;
+ return {
+ name,
+ kind: agent.kind,
+ provider: agent.provider,
+ description: agent.description,
+ sourcePath: agent.sourcePath,
+ };
+ },
+ );
+
const path = await scaffoldTeam({
name: teamName,
displayName,
description,
instructions,
- agents: agentsToScaffold,
+ agents,
targetDir: config.storage.getProjectTeamsDir(),
});
+
setCreatedPath(path);
- await config.getTeamRegistry().reload();
setStep('success');
- } catch (e) {
- setError(e instanceof Error ? e.message : String(e));
+ } catch (e: any) {
+ setError(e.message || 'Failed to create team.');
+ } finally {
setIsSubmitting(false);
}
- }, [
- allAvailableAgents,
- selectedAgents,
- teamName,
- displayName,
- description,
- instructions,
- config,
- ]);
+ };
+
+ const handleBack = useCallback(() => {
+ if (step === 'roster') setStep('identity');
+ else if (step === 'objective') setStep('roster');
+ else if (step === 'confirmation') setStep('objective');
+ }, [step]);
- // Identity Step Buffers
const nameBuffer = useTextBuffer({
initialText: teamName,
- viewport: { width: 40, height: 1 },
+ viewport: { width: 100, height: 100 },
onChange: setTeamName,
- singleLine: true,
});
const displayNameBuffer = useTextBuffer({
initialText: displayName,
- viewport: { width: 40, height: 1 },
+ viewport: { width: 100, height: 100 },
onChange: setDisplayName,
- singleLine: true,
});
- const descBuffer = useTextBuffer({
+ const descriptionBuffer = useTextBuffer({
initialText: description,
- viewport: { width: 80, height: 1 },
+ viewport: { width: 100, height: 100 },
onChange: setDescription,
- singleLine: true,
});
-
- // Objective Step Buffer
const instructionsBuffer = useTextBuffer({
initialText: instructions,
- viewport: { width: 80, height: 5 },
+ viewport: { width: 100, height: 100 },
onChange: setInstructions,
- stdin,
- setRawMode,
- getPreferredEditor,
});
- const [activeIdentityField, setActiveIdentityField] = useState<0 | 1 | 2>(0);
-
- const handleIdentityKeyPress = useCallback(
- (key: Key) => {
- const isNext =
- keyMatchers[Command.DIALOG_NAVIGATION_DOWN](key) ||
- (key.name === 'tab' && !key.shift);
- const isPrev =
- keyMatchers[Command.DIALOG_NAVIGATION_UP](key) ||
- (key.name === 'tab' && key.shift);
-
- if (isNext) {
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
- setActiveIdentityField(((activeIdentityField + 1) % 3) as 0 | 1 | 2);
- return true;
- }
- if (isPrev) {
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
- setActiveIdentityField(((activeIdentityField + 2) % 3) as 0 | 1 | 2);
- return true;
- }
+ const { stdin } = useStdin();
+ useKeypress(
+ (key) => {
if (keyMatchers[Command.ESCAPE](key)) {
onCancel();
return true;
}
- return false;
- },
- [activeIdentityField, keyMatchers, onCancel],
- );
-
- // Handle Tab for "Back" and any key for success exit
- const handleGlobalKeys = useCallback(
- (key: Key) => {
- if (step === 'success') {
- if (key.sequence?.toLowerCase() === 'y') {
- void handleTeamSelect(teamName);
- }
- onComplete();
- return true;
- }
- if (key.name === 'tab') {
- if (step === 'roster') {
- setRosterMode((prev) => (prev === 'list' ? 'action' : 'list'));
- return true;
- }
+ if (key.name === 'b' && !stdin.isPaused()) {
handleBack();
return true;
}
return false;
},
- [step, handleBack, onComplete, handleTeamSelect, teamName],
+ { isActive: step !== 'identity' && step !== 'success' },
);
- useKeypress(handleGlobalKeys, { isActive: true, priority: true });
-
- useKeypress(handleIdentityKeyPress, {
- isActive: step === 'identity',
- priority: true,
- });
-
- const handleRosterKeyPress = useCallback(
- (key: Key) => {
- if (key.sequence?.toLowerCase() === 'b') {
- handleBack();
- return true;
- }
- if (rosterMode === 'action') {
- if (keyMatchers[Command.RETURN](key)) {
- handleNext();
- return true;
- }
- }
- if (keyMatchers[Command.ESCAPE](key)) {
- onCancel();
- return true;
- }
- return false;
- },
- [handleNext, handleBack, onCancel, rosterMode, keyMatchers],
- );
-
- useKeypress(handleRosterKeyPress, {
- isActive: step === 'roster',
- priority: true,
- });
-
- const handleObjectiveKeyPress = useCallback(
- (key: Key) => {
- if (keyMatchers[Command.OPEN_EXTERNAL_EDITOR](key)) {
- void instructionsBuffer.openInExternalEditor();
- return true;
- }
- if (keyMatchers[Command.ESCAPE](key)) {
- onCancel();
- return true;
- }
- return false;
- },
- [instructionsBuffer, keyMatchers, onCancel],
- );
-
- useKeypress(handleObjectiveKeyPress, {
- isActive: step === 'objective',
- priority: true,
- });
-
- const handleConfirmationKeyPress = useCallback(
- (key: Key) => {
- if (keyMatchers[Command.RETURN](key)) {
- void handleCreate();
- return true;
- }
- if (keyMatchers[Command.ESCAPE](key)) {
- onCancel();
- return true;
- }
- return false;
- },
- [handleCreate, keyMatchers, onCancel],
- );
-
- useKeypress(handleConfirmationKeyPress, {
- isActive: step === 'confirmation' && !isSubmitting,
- priority: true,
- });
+ const handleToggleAgent = useCallback((name: string) => {
+ setSelectedAgents((prev) => {
+ const next = new Set(prev);
+ if (next.has(name)) next.delete(name);
+ else next.add(name);
+ return next;
+ });
+ }, []);
if (step === 'identity') {
return (
@@ -430,38 +266,41 @@ export function TeamCreatorWizard({
Step 1: Team Identity
-
- Slug Name (e.g., coding-team):
-
- setActiveIdentityField(1)}
- />
+ Unique Team ID (slug):
+
+
+
-
-
- Display Name (e.g., The Coding Team):
-
+
+ Display Name:
+
+
setActiveIdentityField(2)}
+ placeholder="e.g. Frontend Experts"
/>
-
- Short Description:
+
+ Description (Optional):
+
+
+
+ {error && (
+
+ Error: {error}
+
+ )}
+
@@ -469,11 +308,13 @@ export function TeamCreatorWizard({
}
if (step === 'roster') {
- const rosterItems: Array> =
- allAvailableAgents.map((a) => ({
+ const items: SelectionListItem[] = allAvailableAgents.map(
+ (a) => ({
key: a.name,
+ label: a.name,
value: a,
- }));
+ }),
+ );
return (
-
-
- Step 2: Team Roster
-
-
-
- Selected: {selectedAgents.size}
-
-
-
-
-
+
+ Step 2: Team Roster
+
+
- {rosterMode === 'list'
- ? 'Select agents to include in your team:'
- : 'Switch back to the list if you need to select more agents:'}
+ Select the agents that will participate in this team.
-
+
- items={rosterItems}
- onSelect={(item) => handleToggleAgent(item.name)}
- maxItemsToShow={5}
- showScrollArrows={true}
- isFocused={rosterMode === 'list'}
+ items={items}
+ onSelect={(value) => handleToggleAgent(value.name)}
renderItem={(item, context) => {
const value = item.value;
- const isChecked = selectedAgents.has(value.name);
return (
-
-
-
- [{isChecked ? 'x' : ' '}]
-
-
- {' '}
- {value.logo && (
-
- {value.logo}{' '}
-
- )}
- {value.name}
-
- {(() => {
- const p = value.provider || 'gemini';
- const label =
- p === 'claude-code'
- ? 'Claude Code'
- : p === 'codex'
- ? 'Codex'
- : p === 'antigravity'
- ? 'Antigravity'
- : p === 'gemma'
- ? 'Gemma'
- : 'Gemini CLI';
- const color = PROVIDER_COLORS[p] || theme.ui.comment;
- return (
-
- {' '}
- [{label}]
-
- );
- })()}
+
+
+
+
+ {selectedAgents.has(value.name) ? '☑' : '☐'}
+
+
+
+
+ {' '}
+ {value.logo && (
+
+ {value.logo}{' '}
+
+ )}
+ {value.name}
+
+
+
+
+
+ {value.description && (
+
+ {' '}
+ {value.description}
+
+ )}
- {value.description && (
-
- {' '}
- {value.description}
-
- )}
);
}}
+ isFocused={rosterMode === 'list'}
/>
- {rosterMode === 'action' ? '> ' : ' '}[ Done - Continue to
- Objective ]
+ {rosterMode === 'action' ? '> ' : ' '}
+ [ Done - Continue to Objective ]
0 && (
+ Providers:
+ {teamProviders.map((p, i) => (
+
+
+
+ ))}
+
+
Referencing agents:
{Array.from(selectedAgents).map((name, i) => {
const agent = allAvailableAgents.find((a) => a.name === name);
@@ -634,7 +445,9 @@ export function TeamCreatorWizard({
{agent.logo}{' '}
)}
- @{name}
+
+ @{name}{' '}
+
{i < selectedAgents.size - 1 && (
,
@@ -680,7 +493,7 @@ export function TeamCreatorWizard({
width="100%"
>
- Step 4: Confirm Team Creation
+ Step 4: Confirm Team Configuration
@@ -707,7 +520,9 @@ export function TeamCreatorWizard({
{agent.logo}{' '}
)}
- {name}
+
+ {name}{' '}
+
);
@@ -719,23 +534,23 @@ export function TeamCreatorWizard({
)}
+ {isSubmitting && (
+
+ Creating team...
+
+ )}
+
{error && (
Error: {error}
)}
-
- {isSubmitting ? (
- Creating team...
- ) : (
-
- )}
-
+
);
}
@@ -750,28 +565,21 @@ export function TeamCreatorWizard({
width="100%"
>
- Team Created Successfully!
+ Success! Team Created
- Your new team "{displayName}" is now ready to use.
+ Team {displayName} has been saved to:
- {createdPath && (
-
- Created at:
- {createdPath}
-
- )}
+ {createdPath}
+
+
+ You can now select this team from the main team selection menu.
+
+
-
- Would you like to switch to this team now? (y/N)
-
-
-
-
- Press any other key to finish...
-
+ [Enter] Finish
);
diff --git a/packages/cli/src/ui/components/TeamSelectionDialog.tsx b/packages/cli/src/ui/components/TeamSelectionDialog.tsx
index 39dd83e462..5c88fd02fd 100644
--- a/packages/cli/src/ui/components/TeamSelectionDialog.tsx
+++ b/packages/cli/src/ui/components/TeamSelectionDialog.tsx
@@ -10,45 +10,22 @@ import { Box, Text } from 'ink';
import {
type TeamDefinition,
type AgentDefinition,
+ installRegistryTeam,
} from '@google/gemini-cli-core';
import { theme } from '../semantic-colors.js';
import { DescriptiveRadioButtonSelect } from './shared/DescriptiveRadioButtonSelect.js';
import { useKeypress } from '../hooks/useKeypress.js';
import { useUIActions } from '../contexts/UIActionsContext.js';
+import { TeamRegistryView } from './views/TeamRegistryView.js';
+import { useConfig } from '../contexts/ConfigContext.js';
+import { ProviderTag } from './shared/ProviderTag.js';
interface TeamSelectionDialogProps {
teams: TeamDefinition[];
onSelect: (teamName: string | undefined) => void;
+ onRefreshTeams?: () => void | Promise;
}
-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 = () => (
@@ -87,8 +64,10 @@ function getProviderTags(agents: AgentDefinition[]): React.ReactNode {
export function TeamSelectionDialog({
teams: discoveredTeams,
onSelect,
+ onRefreshTeams,
}: TeamSelectionDialogProps): React.JSX.Element {
const uiActions = useUIActions();
+ const config = useConfig();
const [view, setView] = useState<'select' | 'marketplace'>('select');
useKeypress(
@@ -160,7 +139,7 @@ export function TeamSelectionDialog({
list.push({
value: 'marketplace',
- title: 'Browse Team Marketplace',
+ title: 'Browse Agent Teams',
description: 'Discover and install teams from the community',
key: 'marketplace',
titleSuffix: undefined,
@@ -193,39 +172,36 @@ export function TeamSelectionDialog({
[onSelect, uiActions],
);
+ const handleInstallTeam = useCallback(
+ async (registryTeam: any) => {
+ try {
+ const targetDir = config.storage.getProjectTeamsDir();
+ await installRegistryTeam(registryTeam, targetDir);
+ await onRefreshTeams?.();
+ // After installation, we go back to selection screen
+ setView('select');
+ } catch (error) {
+ // Errors are currently handled by the UI feedback system via coreEvents if they bubble up
+ }
+ },
+ [config, onRefreshTeams],
+ );
+
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...
-
-
-
+ setView('select')}
+ />
);
}
diff --git a/packages/cli/src/ui/components/shared/ProviderTag.tsx b/packages/cli/src/ui/components/shared/ProviderTag.tsx
new file mode 100644
index 0000000000..b22abf94a5
--- /dev/null
+++ b/packages/cli/src/ui/components/shared/ProviderTag.tsx
@@ -0,0 +1,43 @@
+/**
+ * @license
+ * Copyright 2026 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { Text } from 'ink';
+import { theme } from '../../semantic-colors.js';
+
+export const PROVIDER_COLORS: Record = {
+ 'claude-code': '#C15F3C', // Claude Orange
+ codex: '#FFFFFF', // Codex White
+ gemini: '#A855F7', // Gemini Purple
+ antigravity: '#93C5FD', // Antigravity Light Blue
+ gemma: '#60A5FA', // Gemma Blue
+};
+
+export function getProviderLabel(provider: string): string {
+ switch (provider) {
+ case 'claude-code':
+ return 'Claude Code';
+ case 'codex':
+ return 'Codex';
+ case 'antigravity':
+ return 'Antigravity';
+ case 'gemma':
+ return 'Gemma';
+ default:
+ return 'Gemini CLI';
+ }
+}
+
+export const ProviderTag: React.FC<{ provider: string }> = ({ provider }) => {
+ const label = getProviderLabel(provider);
+ const color = PROVIDER_COLORS[provider] || theme.text.secondary;
+
+ return (
+
+ [{label}]
+
+ );
+};
diff --git a/packages/cli/src/ui/components/views/TeamDetailsView.tsx b/packages/cli/src/ui/components/views/TeamDetailsView.tsx
new file mode 100644
index 0000000000..2c8c96c8f2
--- /dev/null
+++ b/packages/cli/src/ui/components/views/TeamDetailsView.tsx
@@ -0,0 +1,183 @@
+/**
+ * @license
+ * Copyright 2026 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type React from 'react';
+import { useState } from 'react';
+import { Box, Text } from 'ink';
+import {
+ type RegistryTeam,
+} from '@google/gemini-cli-core';
+import { useKeypress } from '../../hooks/useKeypress.js';
+import { Command } from '../../key/keyMatchers.js';
+import { useKeyMatchers } from '../../hooks/useKeyMatchers.js';
+import { theme } from '../../semantic-colors.js';
+import { ProviderTag } from '../shared/ProviderTag.js';
+
+export interface TeamDetailsViewProps {
+ team: RegistryTeam;
+ onBack: () => void;
+ onInstall: () => void | Promise;
+ isInstalled: boolean;
+}
+
+export function TeamDetailsView({
+ team,
+ onBack,
+ onInstall,
+ isInstalled,
+}: TeamDetailsViewProps): React.JSX.Element {
+ const keyMatchers = useKeyMatchers();
+ const [isInstalling, setIsInstalling] = useState(false);
+
+ useKeypress(
+ (key) => {
+ if (keyMatchers[Command.ESCAPE](key)) {
+ onBack();
+ return true;
+ }
+
+ if (keyMatchers[Command.RETURN](key) && !isInstalled && !isInstalling) {
+ setIsInstalling(true);
+ void (async () => {
+ await onInstall();
+ setIsInstalling(false);
+ })();
+ return true;
+ }
+ return false;
+ },
+ { isActive: true, priority: true },
+ );
+
+ if (isInstalling) {
+ return (
+
+
+ Installing Team {team.displayName}...
+
+
+ );
+ }
+
+ return (
+
+ {/* Header Row */}
+
+
+
+ {'>'} Agent Teams {'>'}{' '}
+
+
+ {team.displayName}
+
+
+
+
+ {team.version ? `v${team.version}` : ''} |{' '}
+
+ ⭐
+
+ {String(team.stars || 0)} |{' '}
+
+ @{team.name}
+
+
+
+ {/* Description */}
+
+ {team.description}
+
+
+ {/* Author */}
+
+ Author:
+ {team.author || 'Community'}
+
+
+ {/* Agents Roster */}
+
+
+ Team Roster:
+
+ {team.agents.map((agent) => (
+
+
+ @{agent.name}
+
+
+
+
+
+
+ {agent.description}
+
+
+
+ ))}
+
+
+ {/* Instructions Summary */}
+
+
+ Team Mission:
+
+
+
+ {team.instructions}
+
+
+
+
+ {/* Spacer to push installation UI to bottom */}
+
+
+ {/* Installation UI */}
+ {!isInstalled ? (
+
+
+ This team will be installed to your local .gemini/teams/ directory.
+ Agent teams may contain custom instructions and configurations.
+
+
+ [{'Enter'}] Install Team
+
+ [Esc] Back
+
+
+
+ ) : (
+
+ Team Already Installed
+
+ [Esc] Back
+
+
+ )}
+
+ );
+}
diff --git a/packages/cli/src/ui/components/views/TeamRegistryView.tsx b/packages/cli/src/ui/components/views/TeamRegistryView.tsx
new file mode 100644
index 0000000000..f90be1266d
--- /dev/null
+++ b/packages/cli/src/ui/components/views/TeamRegistryView.tsx
@@ -0,0 +1,244 @@
+/**
+ * @license
+ * Copyright 2026 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type React from 'react';
+import { useMemo, useCallback, useState } from 'react';
+import { Box, Text } from 'ink';
+import {
+ type RegistryTeam,
+ type TeamDefinition,
+} from '@google/gemini-cli-core';
+import {
+ SearchableList,
+ type GenericListItem,
+} from '../shared/SearchableList.js';
+import { theme } from '../../semantic-colors.js';
+
+import { useTeamRegistry } from '../../hooks/useTeamRegistry.js';
+import { useConfig } from '../../contexts/ConfigContext.js';
+import { useRegistrySearch } from '../../hooks/useRegistrySearch.js';
+import { useUIState } from '../../contexts/UIStateContext.js';
+import { TeamDetailsView } from './TeamDetailsView.js';
+import { ProviderTag } from '../shared/ProviderTag.js';
+
+export interface TeamRegistryViewProps {
+ onInstall?: (team: RegistryTeam) => void | Promise;
+ onClose?: () => void;
+ installedTeams: TeamDefinition[];
+}
+
+interface TeamListItem extends GenericListItem {
+ team: RegistryTeam;
+}
+
+export function TeamRegistryView({
+ onInstall,
+ onClose,
+ installedTeams,
+}: TeamRegistryViewProps): React.JSX.Element {
+ const config = useConfig();
+ const { teams, loading, error, search } = useTeamRegistry(
+ '',
+ config.getTeamRegistryURI(),
+ );
+ const { terminalHeight, staticExtraHeight } = useUIState();
+ const [selectedTeam, setSelectedTeam] = useState(null);
+
+ const items: TeamListItem[] = useMemo(
+ () =>
+ teams.map((team) => ({
+ key: team.id,
+ label: team.displayName,
+ description: team.description,
+ team,
+ })),
+ [teams],
+ );
+
+ const handleSelect = useCallback((item: TeamListItem) => {
+ setSelectedTeam(item.team);
+ }, []);
+
+ const handleBack = useCallback(() => {
+ setSelectedTeam(null);
+ }, []);
+
+ const handleInstallAction = useCallback(
+ async (team: RegistryTeam) => {
+ await onInstall?.(team);
+ // Go back to list view after install
+ setSelectedTeam(null);
+ },
+ [onInstall],
+ );
+
+ const renderItem = useCallback(
+ (item: TeamListItem, isActive: boolean, _labelWidth: number) => {
+ const isInstalled = installedTeams.some((t) => t.name === item.team.name);
+ const providers = Array.from(
+ new Set(item.team.agents.map((a) => a.provider || 'gemini')),
+ ).sort();
+
+ return (
+
+
+
+
+
+ {isActive ? '● ' : ' '}
+
+
+
+
+ {item.label}
+
+
+
+ |
+
+ {isInstalled && (
+
+ [Installed]
+
+ )}
+
+
+ {item.description}
+
+
+
+
+ ⭐
+
+ {' '}
+ {item.team.stars || 0}
+
+
+
+
+
+ {item.team.agents.length} agent
+ {item.team.agents.length === 1 ? '' : 's'}{' '}
+
+
+ {providers.map((p) => (
+
+
+
+ ))}
+
+
+
+ );
+ },
+ [installedTeams],
+ );
+
+ const header = useMemo(
+ () => (
+
+
+
+ Browse and search Agent Teams from the registry.
+
+
+
+
+ {installedTeams.length > 0 &&
+ `${installedTeams.length} teams installed`}
+
+
+
+ ),
+ [installedTeams.length],
+ );
+
+ const footer = useCallback(
+ ({
+ startIndex,
+ endIndex,
+ totalVisible,
+ }: {
+ startIndex: number;
+ endIndex: number;
+ totalVisible: number;
+ }) => (
+
+ ({startIndex + 1}-{endIndex}) / {totalVisible}
+
+ ),
+ [],
+ );
+
+ const maxItemsToShow = useMemo(() => {
+ const staticHeight = 10;
+ const availableTerminalHeight = terminalHeight - staticExtraHeight;
+ const remainingHeight = Math.max(0, availableTerminalHeight - staticHeight);
+ const itemHeight = 3; // Increased due to multi-line rendering
+ return Math.max(4, Math.floor(remainingHeight / itemHeight));
+ }, [terminalHeight, staticExtraHeight]);
+
+ if (loading && items.length === 0) {
+ return (
+
+ Loading Agent Teams...
+
+ );
+ }
+
+ if (error) {
+ return (
+
+ Error loading teams:
+ {error}
+
+ );
+ }
+
+ return (
+ <>
+
+
+ title="Browse Agent Teams"
+ items={items}
+ onSelect={handleSelect}
+ onClose={onClose || (() => {})}
+ searchPlaceholder="Search agent teams"
+ renderItem={renderItem}
+ header={header}
+ footer={footer}
+ maxItemsToShow={maxItemsToShow}
+ useSearch={useRegistrySearch}
+ onSearch={search}
+ resetSelectionOnItemsChange={true}
+ isFocused={!selectedTeam}
+ />
+
+ {selectedTeam && (
+ {
+ await handleInstallAction(selectedTeam);
+ }}
+ isInstalled={installedTeams.some((t) => t.name === selectedTeam.name)}
+ />
+ )}
+ >
+ );
+}
diff --git a/packages/cli/src/ui/contexts/UIActionsContext.tsx b/packages/cli/src/ui/contexts/UIActionsContext.tsx
index 92501691fd..271b40a563 100644
--- a/packages/cli/src/ui/contexts/UIActionsContext.tsx
+++ b/packages/cli/src/ui/contexts/UIActionsContext.tsx
@@ -92,6 +92,7 @@ export interface UIActions {
handleRestart: () => void;
handleNewAgentsSelect: (choice: NewAgentsChoice) => Promise;
handleTeamSelect: (teamName: string | undefined) => void;
+ handleRefreshTeams: () => Promise;
setIsTeamCreatorActive: (active: boolean) => void;
getPreferredEditor: () => EditorType;
diff --git a/packages/cli/src/ui/hooks/useTeamRegistry.ts b/packages/cli/src/ui/hooks/useTeamRegistry.ts
new file mode 100644
index 0000000000..0d872747f4
--- /dev/null
+++ b/packages/cli/src/ui/hooks/useTeamRegistry.ts
@@ -0,0 +1,105 @@
+/**
+ * @license
+ * Copyright 2026 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { useState, useEffect, useMemo, useCallback, useRef } from 'react';
+import {
+ TeamRegistryClient,
+ type RegistryTeam,
+} from '@google/gemini-cli-core';
+
+export interface UseTeamRegistryResult {
+ teams: RegistryTeam[];
+ loading: boolean;
+ error: string | null;
+ search: (query: string) => void;
+}
+
+export function useTeamRegistry(
+ initialQuery = '',
+ registryURI?: string,
+): UseTeamRegistryResult {
+ const [teams, setTeams] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ const client = useMemo(
+ () => new TeamRegistryClient(registryURI),
+ [registryURI],
+ );
+
+ // Ref to track the latest query to avoid race conditions
+ const latestQueryRef = useRef(initialQuery);
+
+ // Ref for debounce timeout
+ const debounceTimeoutRef = useRef(undefined);
+
+ const searchTeams = useCallback(
+ async (query: string) => {
+ try {
+ setLoading(true);
+ const results = await client.searchTeams(query);
+
+ // Only update if this is still the latest query
+ if (query === latestQueryRef.current) {
+ // Check if results are different from current teams
+ setTeams((prev) => {
+ if (
+ prev.length === results.length &&
+ prev.every((team, i) => team.id === results[i].id)
+ ) {
+ return prev;
+ }
+ return results;
+ });
+ setError(null);
+ setLoading(false);
+ }
+ } catch (err) {
+ if (query === latestQueryRef.current) {
+ setError(err instanceof Error ? err.message : String(err));
+ setTeams([]);
+ setLoading(false);
+ }
+ }
+ },
+ [client],
+ );
+
+ const search = useCallback(
+ (query: string) => {
+ latestQueryRef.current = query;
+
+ // Clear existing timeout
+ if (debounceTimeoutRef.current) {
+ clearTimeout(debounceTimeoutRef.current);
+ }
+
+ // Debounce
+ debounceTimeoutRef.current = setTimeout(() => {
+ void searchTeams(query);
+ }, 300);
+ },
+ [searchTeams],
+ );
+
+ // Initial load
+ useEffect(() => {
+ void searchTeams(initialQuery);
+
+ return () => {
+ if (debounceTimeoutRef.current) {
+ clearTimeout(debounceTimeoutRef.current);
+ }
+ };
+ }, [initialQuery, searchTeams]);
+
+ return {
+ teams,
+ loading,
+ error,
+ search,
+ };
+}
diff --git a/packages/core/src/agents/teamRegistryClient.ts b/packages/core/src/agents/teamRegistryClient.ts
index b42e869a06..66eb27d1f7 100644
--- a/packages/core/src/agents/teamRegistryClient.ts
+++ b/packages/core/src/agents/teamRegistryClient.ts
@@ -69,7 +69,10 @@ export class TeamRegistryClient {
}
} catch (error) {
TeamRegistryClient.fetchPromise = null;
- throw error;
+ if (error instanceof Error) {
+ throw error;
+ }
+ throw new Error(String(error));
}
})();
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index c2b42bf3a1..4f8ff58f30 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -186,6 +186,8 @@ export * from './agents/types.js';
export * from './agents/agentLoader.js';
export * from './agents/local-executor.js';
export * from './agents/agent-scheduler.js';
+export * from './agents/teamRegistry.js';
+export * from './agents/teamRegistryClient.js';
export * from './agents/teamScaffolder.js';
// Export browser session management