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:
+14
-1
@@ -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": {
|
||||
|
||||
@@ -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';
|
||||
};
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 ${
|
||||
|
||||
Reference in New Issue
Block a user