mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-12 19:10:55 -07:00
a40af4e03e
- Add interactive 5-step TeamCreatorWizard with agent selection and objective editor - Implement /team slash command for team selection and creation - Add colorized Nerd Font logos and scroll support for agent roster selection - Refactor teamScaffolder to generate standalone .md files for all agents - Implement persistent activeTeam setting in workspace configuration - Add ActiveTeamChanged event to force immediate system instruction refresh - Support external editor (Ctrl+X) for team objective instructions - Enhance keyboard navigation with Tab/Shift+Tab field switching in wizard - Fix team selection logic to correctly pivot to creation wizard - Update various UI components and tests for team state management
156 lines
4.7 KiB
TypeScript
156 lines
4.7 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2026 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import { type TeamDefinition } from './types.js';
|
|
import { type Config } from '../config/config.js';
|
|
import { Storage } from '../config/storage.js';
|
|
import { type AgentRegistry } from './registry.js';
|
|
import { loadTeamsFromDirectory, type TeamLoadResult } from './teamLoader.js';
|
|
import { coreEvents } from '../utils/events.js';
|
|
import { debugLogger } from '../utils/debugLogger.js';
|
|
|
|
/**
|
|
* Manages the discovery, loading, and active state of Agent Teams.
|
|
*/
|
|
export class TeamRegistry {
|
|
private readonly teams = new Map<string, TeamDefinition>();
|
|
private activeTeamName?: string;
|
|
|
|
constructor(
|
|
private readonly config: Config,
|
|
private readonly agentRegistry: AgentRegistry,
|
|
) {}
|
|
|
|
/**
|
|
* Discovers and loads teams from the global and project-level directories.
|
|
*/
|
|
async initialize(): Promise<void> {
|
|
await this.loadTeams();
|
|
}
|
|
|
|
/**
|
|
* Clears the current registry and re-scans for teams.
|
|
*/
|
|
async reload(): Promise<void> {
|
|
await this.loadTeams();
|
|
}
|
|
|
|
private async loadTeams(): Promise<void> {
|
|
this.teams.clear();
|
|
|
|
if (!this.config.isAgentsEnabled()) {
|
|
return;
|
|
}
|
|
|
|
// Load user-level teams first
|
|
const userTeamsDir = Storage.getUserTeamsDir();
|
|
const userLoadResult = await loadTeamsFromDirectory(userTeamsDir);
|
|
await this.processLoadResult(userLoadResult);
|
|
|
|
const folderTrustEnabled = this.config.getFolderTrust();
|
|
const isTrustedFolder = this.config.isTrustedFolder();
|
|
|
|
if (folderTrustEnabled && !isTrustedFolder) {
|
|
debugLogger.log(
|
|
'[TeamRegistry] Skipping project teams due to untrusted folder.',
|
|
);
|
|
coreEvents.emitFeedback(
|
|
'info',
|
|
'Skipping project teams due to untrusted folder. To enable, ensure that the project root is trusted.',
|
|
);
|
|
} else {
|
|
// Load project-level teams (takes precedence over user-level if names collide)
|
|
const projectTeamsDir = this.config.storage.getProjectTeamsDir();
|
|
const projectLoadResult = await loadTeamsFromDirectory(projectTeamsDir);
|
|
await this.processLoadResult(projectLoadResult);
|
|
}
|
|
|
|
if (this.config.getDebugMode()) {
|
|
debugLogger.log(`[TeamRegistry] Loaded with ${this.teams.size} teams.`);
|
|
}
|
|
}
|
|
|
|
private async processLoadResult(result: TeamLoadResult): Promise<void> {
|
|
for (const error of result.errors) {
|
|
debugLogger.warn(`[TeamRegistry] Error loading team: ${error.message}`);
|
|
coreEvents.emitFeedback('error', `Team loading error: ${error.message}`);
|
|
}
|
|
|
|
const registrationPromises: Array<Promise<void>> = [];
|
|
|
|
for (const team of result.teams) {
|
|
this.teams.set(team.name, team);
|
|
|
|
// Register team agents in the global AgentRegistry so they are available as SubagentTools
|
|
for (const agent of team.agents) {
|
|
const descriptionOverride = `MANDATORY for ${agent.displayName} tasks: ${agent.description} (Team Agent: ${team.displayName}). You MUST delegate all ${agent.displayName} tasks to this agent.`;
|
|
|
|
// We wrap the agent definition to provide the description override
|
|
const wrappedAgent = {
|
|
...agent,
|
|
description: descriptionOverride,
|
|
};
|
|
|
|
registrationPromises.push(
|
|
this.agentRegistry.registerAgent(wrappedAgent).catch((e) => {
|
|
debugLogger.warn(
|
|
`[TeamRegistry] Error registering agent "${agent.name}" from team "${team.name}":`,
|
|
e,
|
|
);
|
|
coreEvents.emitFeedback(
|
|
'error',
|
|
`Error registering agent "${agent.name}" from team "${team.name}": ${e instanceof Error ? e.message : String(e)}`,
|
|
);
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
|
|
await Promise.allSettled(registrationPromises);
|
|
}
|
|
|
|
/**
|
|
* Returns all loaded teams.
|
|
*/
|
|
getAllTeams(): TeamDefinition[] {
|
|
return Array.from(this.teams.values());
|
|
}
|
|
|
|
/**
|
|
* Sets the current active team.
|
|
* @param name The slug (name) of the team to activate, or undefined to clear.
|
|
* @throws Error if the team is not found and name is provided.
|
|
*/
|
|
setActiveTeam(name: string | undefined): void {
|
|
if (name === undefined) {
|
|
this.activeTeamName = undefined;
|
|
return;
|
|
}
|
|
|
|
if (this.teams.has(name)) {
|
|
this.activeTeamName = name;
|
|
} else {
|
|
throw new Error(`Team not found: ${name}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the currently active team definition, if any.
|
|
*/
|
|
getActiveTeam(): TeamDefinition | undefined {
|
|
return this.activeTeamName
|
|
? this.teams.get(this.activeTeamName)
|
|
: undefined;
|
|
}
|
|
|
|
/**
|
|
* Returns a team definition by name.
|
|
*/
|
|
getTeam(name: string): TeamDefinition | undefined {
|
|
return this.teams.get(name);
|
|
}
|
|
}
|