Agent Skills: Implement Core Skill Infrastructure & Tiered Discovery (#15698)

This commit is contained in:
N. Taylor Mullen
2025-12-30 13:35:52 -08:00
committed by GitHub
parent ec11b8afbf
commit de1233b8ca
19 changed files with 1209 additions and 3 deletions

View File

@@ -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} />
)}

View 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();
});
});

View 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>
);
};