mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-06-13 12:57:12 -07:00
feat(cli,core): expand provider ecosystem with Antigravity and Gemma
- Add Antigravity (Light Blue) and Gemma (Gemini Blue) providers - Update Gemini CLI brand color to Purple - Implement personality overlays for Antigravity and Gemma in core - Ensure brand-accurate labels and colors across Marketplace and Status Bar - Add unit tests for new provider personality overlays
This commit is contained in:
@@ -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('<StatusRow />', () => {
|
||||
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(
|
||||
<StatusRow
|
||||
showUiDetails={true}
|
||||
isNarrow={false}
|
||||
terminalWidth={100}
|
||||
hideContextSummary={false}
|
||||
hideUiDetailsForSuggestions={false}
|
||||
hasPendingActionRequired={false}
|
||||
/>,
|
||||
{
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<string, string> = {
|
||||
'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<string>();
|
||||
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 (
|
||||
<Box marginLeft={LAYOUT.INDICATOR_LEFT_MARGIN}>
|
||||
<Text color={theme.text.accent}>[ Team: {activeTeam.displayName} ]</Text>
|
||||
<Box marginLeft={LAYOUT.INDICATOR_LEFT_MARGIN} flexDirection="row">
|
||||
<Text color={theme.text.accent}>[ Team: {activeTeam.displayName} (</Text>
|
||||
{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 (
|
||||
<React.Fragment key={p}>
|
||||
<Text color={color} bold>
|
||||
{label}
|
||||
</Text>
|
||||
{i < sortedProviders.length - 1 && (
|
||||
<Text color={theme.text.accent}>, </Text>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
<Text color={theme.text.accent}>) ]</Text>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -49,9 +49,10 @@ describe('<TeamSelectionDialog />', () => {
|
||||
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('<TeamSelectionDialog />', () => {
|
||||
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('<TeamSelectionDialog />', () => {
|
||||
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('<TeamSelectionDialog />', () => {
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<string, string> = {
|
||||
'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 (
|
||||
<Text color={color} bold>
|
||||
[{label}]
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
const MultiModelBadge: React.FC = () => (
|
||||
<Box marginLeft={1}>
|
||||
<Text color={theme.text.secondary} italic>
|
||||
Multi-Model
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
|
||||
function getProviderTags(agents: AgentDefinition[]): React.ReactNode {
|
||||
const providers = new Set<string>();
|
||||
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 (
|
||||
<Box flexDirection="row" alignItems="center">
|
||||
<Box flexDirection="row">
|
||||
{sortedProviders.map((p, i) => (
|
||||
<Box key={p} marginLeft={i === 0 ? 0 : 1}>
|
||||
<ProviderTag provider={p} />
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
{isMulti && <MultiModelBadge />}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<Box
|
||||
borderStyle="round"
|
||||
borderColor={theme.border.default}
|
||||
flexDirection="column"
|
||||
padding={1}
|
||||
width="100%"
|
||||
>
|
||||
<Text bold color={theme.text.primary}>
|
||||
Agent Team Marketplace
|
||||
</Text>
|
||||
<Box marginTop={1} flexDirection="column">
|
||||
<Text color={theme.text.secondary}>
|
||||
Explore and download community-contributed agent teams.
|
||||
</Text>
|
||||
<Box
|
||||
marginTop={1}
|
||||
padding={1}
|
||||
borderStyle="single"
|
||||
borderColor={theme.ui.comment}
|
||||
>
|
||||
<Text color={theme.ui.comment}>
|
||||
The community marketplace is currently under development. Soon you
|
||||
will be able to browse hundreds of specialized teams.
|
||||
</Text>
|
||||
</Box>
|
||||
<Box marginTop={1}>
|
||||
<Text color={theme.text.accent}>
|
||||
Press any key to go back to selection...
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (view === 'create') {
|
||||
return (
|
||||
<Box
|
||||
borderStyle="round"
|
||||
borderColor={theme.border.default}
|
||||
flexDirection="column"
|
||||
padding={1}
|
||||
width="100%"
|
||||
>
|
||||
<Text bold color={theme.text.primary}>
|
||||
Create New Agent Team
|
||||
</Text>
|
||||
<Box marginTop={1} flexDirection="column">
|
||||
<Text color={theme.text.secondary}>
|
||||
To create a new team, follow these simple steps:
|
||||
</Text>
|
||||
<Text color={theme.text.primary}>
|
||||
1. Create a directory in .gemini/teams/ (e.g.,
|
||||
.gemini/teams/my-team/)
|
||||
</Text>
|
||||
<Text color={theme.text.primary}>
|
||||
2. Create a TEAM.md file with name, display_name, and description.
|
||||
</Text>
|
||||
<Text color={theme.text.primary}>
|
||||
3. (Optional) Add specialized agents to an agents/ sub-directory.
|
||||
</Text>
|
||||
|
||||
<Box
|
||||
marginTop={1}
|
||||
padding={1}
|
||||
borderStyle="single"
|
||||
borderColor={theme.ui.focus}
|
||||
>
|
||||
<Text color={theme.ui.focus}>
|
||||
Tip: You can now specify external agents directly in TEAM.md!
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text color={theme.text.accent}>
|
||||
Press any key to go back to selection...
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
borderStyle="round"
|
||||
|
||||
@@ -13,6 +13,7 @@ import type { SelectionListItem } from '../../hooks/useSelectionList.js';
|
||||
export interface DescriptiveRadioSelectItem<T> extends SelectionListItem<T> {
|
||||
title: string;
|
||||
description?: string;
|
||||
titleSuffix?: React.ReactNode;
|
||||
}
|
||||
|
||||
export interface DescriptiveRadioButtonSelectProps<T> {
|
||||
@@ -61,7 +62,10 @@ export function DescriptiveRadioButtonSelect<T>({
|
||||
maxItemsToShow={maxItemsToShow}
|
||||
renderItem={(item, { titleColor }) => (
|
||||
<Box flexDirection="column" key={item.key}>
|
||||
<Text color={titleColor}>{item.title}</Text>
|
||||
<Box flexDirection="row">
|
||||
<Text color={titleColor}>{item.title}</Text>
|
||||
{item.titleSuffix && <Box marginLeft={1}>{item.titleSuffix}</Box>}
|
||||
</Box>
|
||||
{item.description && (
|
||||
<Text color={theme.text.secondary}>{item.description}</Text>
|
||||
)}
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -211,7 +211,7 @@ const remoteAgentSchema = z.union([
|
||||
|
||||
type FrontmatterRemoteAgentDefinition = z.infer<typeof remoteAgentSchema>;
|
||||
|
||||
const externalAgentSchema = z
|
||||
export const externalAgentSchema = z
|
||||
.object({
|
||||
kind: z.literal('external'),
|
||||
name: nameSchema,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user