mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-01 17:02:29 -07:00
Agent Skills: Implement Core Skill Infrastructure & Tiered Discovery (#15698)
This commit is contained in:
@@ -28,6 +28,7 @@ import type { SlashCommand } from '../commands/types.js';
|
||||
import { ExtensionsList } from './views/ExtensionsList.js';
|
||||
import { getMCPServerStatus } from '@google/gemini-cli-core';
|
||||
import { ToolsList } from './views/ToolsList.js';
|
||||
import { SkillsList } from './views/SkillsList.js';
|
||||
import { McpStatus } from './views/McpStatus.js';
|
||||
import { ChatList } from './views/ChatList.js';
|
||||
import { HooksList } from './views/HooksList.js';
|
||||
@@ -153,6 +154,12 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
|
||||
showDescriptions={itemForDisplay.showDescriptions}
|
||||
/>
|
||||
)}
|
||||
{itemForDisplay.type === 'skills_list' && (
|
||||
<SkillsList
|
||||
skills={itemForDisplay.skills}
|
||||
showDescriptions={itemForDisplay.showDescriptions}
|
||||
/>
|
||||
)}
|
||||
{itemForDisplay.type === 'mcp_status' && (
|
||||
<McpStatus {...itemForDisplay} serverStatus={getMCPServerStatus} />
|
||||
)}
|
||||
|
||||
90
packages/cli/src/ui/components/views/SkillsList.test.tsx
Normal file
90
packages/cli/src/ui/components/views/SkillsList.test.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from '../../../test-utils/render.js';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { SkillsList } from './SkillsList.js';
|
||||
import { type SkillDefinition } from '../../types.js';
|
||||
|
||||
describe('SkillsList Component', () => {
|
||||
const mockSkills: SkillDefinition[] = [
|
||||
{ name: 'skill1', description: 'description 1', disabled: false },
|
||||
{ name: 'skill2', description: 'description 2', disabled: true },
|
||||
{ name: 'skill3', description: 'description 3', disabled: false },
|
||||
];
|
||||
|
||||
it('should render enabled and disabled skills separately', () => {
|
||||
const { lastFrame, unmount } = render(
|
||||
<SkillsList skills={mockSkills} showDescriptions={true} />,
|
||||
);
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain('Available Agent Skills:');
|
||||
expect(output).toContain('skill1');
|
||||
expect(output).toContain('description 1');
|
||||
expect(output).toContain('skill3');
|
||||
expect(output).toContain('description 3');
|
||||
|
||||
expect(output).toContain('Disabled Skills:');
|
||||
expect(output).toContain('skill2');
|
||||
expect(output).toContain('description 2');
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should not render descriptions when showDescriptions is false', () => {
|
||||
const { lastFrame, unmount } = render(
|
||||
<SkillsList skills={mockSkills} showDescriptions={false} />,
|
||||
);
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain('skill1');
|
||||
expect(output).not.toContain('description 1');
|
||||
expect(output).toContain('skill2');
|
||||
expect(output).not.toContain('description 2');
|
||||
expect(output).toContain('skill3');
|
||||
expect(output).not.toContain('description 3');
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should render "No skills available" when skills list is empty', () => {
|
||||
const { lastFrame, unmount } = render(
|
||||
<SkillsList skills={[]} showDescriptions={true} />,
|
||||
);
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain('No skills available');
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should only render Available Agent Skills section when all skills are enabled', () => {
|
||||
const enabledOnly = mockSkills.filter((s) => !s.disabled);
|
||||
const { lastFrame, unmount } = render(
|
||||
<SkillsList skills={enabledOnly} showDescriptions={true} />,
|
||||
);
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain('Available Agent Skills:');
|
||||
expect(output).not.toContain('Disabled Skills:');
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should only render Disabled Skills section when all skills are disabled', () => {
|
||||
const disabledOnly = mockSkills.filter((s) => s.disabled);
|
||||
const { lastFrame, unmount } = render(
|
||||
<SkillsList skills={disabledOnly} showDescriptions={true} />,
|
||||
);
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).not.toContain('Available Agent Skills:');
|
||||
expect(output).toContain('Disabled Skills:');
|
||||
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
85
packages/cli/src/ui/components/views/SkillsList.tsx
Normal file
85
packages/cli/src/ui/components/views/SkillsList.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import { type SkillDefinition } from '../../types.js';
|
||||
|
||||
interface SkillsListProps {
|
||||
skills: readonly SkillDefinition[];
|
||||
showDescriptions: boolean;
|
||||
}
|
||||
|
||||
export const SkillsList: React.FC<SkillsListProps> = ({
|
||||
skills,
|
||||
showDescriptions,
|
||||
}) => {
|
||||
const enabledSkills = skills
|
||||
.filter((s) => !s.disabled)
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
const disabledSkills = skills
|
||||
.filter((s) => s.disabled)
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
const renderSkill = (skill: SkillDefinition) => (
|
||||
<Box key={skill.name} flexDirection="row">
|
||||
<Text color={theme.text.primary}>{' '}- </Text>
|
||||
<Box flexDirection="column">
|
||||
<Text
|
||||
bold
|
||||
color={skill.disabled ? theme.text.secondary : theme.text.link}
|
||||
>
|
||||
{skill.name}
|
||||
</Text>
|
||||
{showDescriptions && skill.description && (
|
||||
<Box marginLeft={2}>
|
||||
<Text
|
||||
color={skill.disabled ? theme.text.secondary : theme.text.primary}
|
||||
>
|
||||
{skill.description}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
{enabledSkills.length > 0 && (
|
||||
<Box flexDirection="column">
|
||||
<Text bold color={theme.text.primary}>
|
||||
Available Agent Skills:
|
||||
</Text>
|
||||
<Box height={1} />
|
||||
{enabledSkills.map(renderSkill)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{enabledSkills.length > 0 && disabledSkills.length > 0 && (
|
||||
<Box marginY={1}>
|
||||
<Text color={theme.text.secondary}>{'-'.repeat(20)}</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{disabledSkills.length > 0 && (
|
||||
<Box flexDirection="column">
|
||||
<Text bold color={theme.text.secondary}>
|
||||
Disabled Skills:
|
||||
</Text>
|
||||
<Box height={1} />
|
||||
{disabledSkills.map(renderSkill)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{skills.length === 0 && (
|
||||
<Text color={theme.text.primary}> No skills available</Text>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user