feat: show installed skills in UI (#7910)

Signed-off-by: Vincenzo Palazzo <vincenzopalazzodev@gmail.com>
Signed-off-by: Douwe Osinga <douwe@squareup.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Douwe Osinga <douwe@squareup.com>
This commit is contained in:
Vincenzo Palazzo
2026-03-26 21:57:12 +01:00
committed by GitHub
parent f6fdbbdd7e
commit 9feb3f3bca
8 changed files with 547 additions and 18 deletions
+14 -1
View File
@@ -1569,6 +1569,18 @@
"super::routes::config_management"
],
"operationId": "get_slash_commands",
"parameters": [
{
"name": "working_dir",
"in": "query",
"description": "Optional working directory to discover local skills from",
"required": false,
"schema": {
"type": "string",
"nullable": true
}
}
],
"responses": {
"200": {
"description": "Slash commands retrieved successfully",
@@ -4139,7 +4151,8 @@
"type": "string",
"enum": [
"Builtin",
"Recipe"
"Recipe",
"Skill"
]
},
"ConfigKey": {
+7 -2
View File
@@ -80,7 +80,7 @@ export type CheckProviderRequest = {
provider: string;
};
export type CommandType = 'Builtin' | 'Recipe';
export type CommandType = 'Builtin' | 'Recipe' | 'Skill';
/**
* Configuration key metadata for provider setup
@@ -2837,7 +2837,12 @@ export type SetConfigProviderData = {
export type GetSlashCommandsData = {
body?: never;
path?: never;
query?: never;
query?: {
/**
* Optional working directory to discover local skills from
*/
working_dir?: string | null;
};
url: '/config/slash_commands';
};
+3
View File
@@ -15,6 +15,7 @@ import {
Zap,
BookOpen,
Wrench,
Sparkles,
} from 'lucide-react';
import { DisplayItem } from './MentionPopover';
@@ -33,6 +34,8 @@ export const getItemIcon = (item: DisplayItem): IconInfo => {
return { Icon: Zap, color: '#3b82f6' }; // Blue
case 'Recipe':
return { Icon: BookOpen, color: '#10b981' }; // Green
case 'Skill':
return { Icon: Sparkles, color: '#8b5cf6' }; // Purple
case 'Directory':
return { Icon: Folder, color: '#f59e0b' }; // Amber
default: {
+20 -11
View File
@@ -17,7 +17,8 @@ const typeOrder: Record<DisplayItemType, number> = {
Directory: 0,
File: 1,
Builtin: 2,
Recipe: 3,
Skill: 3,
Recipe: 4,
};
export interface DisplayItem {
@@ -441,6 +442,16 @@ const MentionPopover = forwardRef<
});
}, [items, query, currentWorkingDir]);
const getSelectionText = (item: DisplayItem): string => {
if (item.itemType === 'Skill') {
return `Use the ${item.name} skill to `;
}
if (['Builtin', 'Recipe'].includes(item.itemType)) {
return '/' + item.name;
}
return item.extra;
};
// Expose methods to parent component
useImperativeHandle(
ref,
@@ -448,7 +459,7 @@ const MentionPopover = forwardRef<
getDisplayFiles: () => displayItems,
selectFile: (index: number) => {
if (displayItems[index]) {
onSelect(displayItems[index].extra);
onSelect(getSelectionText(displayItems[index]));
onClose();
}
},
@@ -459,7 +470,10 @@ const MentionPopover = forwardRef<
useEffect(() => {
const loadData = async () => {
if (isSlashCommand) {
const response = await getSlashCommands({ throwOnError: true });
const response = await getSlashCommands({
query: { working_dir: currentWorkingDir },
throwOnError: true,
});
const commandItems: DisplayItem[] = (response.data?.commands || []).map((cmd) => ({
name: cmd.command,
extra: cmd.help,
@@ -475,7 +489,7 @@ const MentionPopover = forwardRef<
if (isOpen) {
loadData();
}
}, [isOpen, isSlashCommand, scanFilesFromRoot]);
}, [isOpen, isSlashCommand, scanFilesFromRoot, currentWorkingDir]);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
@@ -509,12 +523,7 @@ const MentionPopover = forwardRef<
const handleItemClick = (index: number) => {
if (index >= 0 && index < displayItems.length) {
onSelectedIndexChange(index);
const displayItem = displayItems[index];
onSelect(
['Builtin', 'Recipe'].includes(displayItem.itemType)
? '/' + displayItem.name
: displayItem.extra
);
onSelect(getSelectionText(displayItems[index]));
onClose();
}
};
@@ -551,7 +560,7 @@ const MentionPopover = forwardRef<
>
{displayItems.map((item, index) => (
<div
key={item.extra}
key={`${item.itemType}-${item.name}`}
onClick={() => handleItemClick(index)}
data-selected={index === selectedIndex}
className={`flex items-center gap-3 p-2 rounded-md cursor-pointer transition-colors ${