refactor(skills): add skill-adapter and refactor SkillMarket

- Add skill-adapter.ts to bridge configStore and UI skill formats
- Refactor SkillMarket to use new skill-adapter instead of skill-discovery
- Add health check state to connectionStore
- Update multiple components with improved typing
- Clean up test artifacts and add new test results
- Update README and add skill-market-mvp plan

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
iven
2026-03-21 00:28:03 +08:00
parent 54ccc0a7b0
commit 48a430fc97
50 changed files with 1523 additions and 360 deletions

View File

@@ -11,33 +11,30 @@
*/
import { useState, useEffect, useMemo, useCallback } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
Search,
Package,
Check,
Plus,
Minus,
Sparkles,
Tag,
Layers,
ChevronDown,
ChevronRight,
RefreshCw,
Info,
} from 'lucide-react';
import { useConfigStore, type SkillInfo } from '../store/configStore';
import {
SkillDiscoveryEngine,
type SkillInfo,
type SkillSuggestion,
} from '../lib/skill-discovery';
adaptSkillsCatalog,
type SkillDisplay,
} from '../lib/skill-adapter';
// === Types ===
interface SkillMarketProps {
className?: string;
onSkillInstall?: (skill: SkillInfo) => void;
onSkillUninstall?: (skill: SkillInfo) => void;
onSkillInstall?: (skill: SkillDisplay) => void;
onSkillUninstall?: (skill: SkillDisplay) => void;
}
type CategoryFilter = 'all' | 'development' | 'security' | 'analytics' | 'content' | 'ops' | 'management' | 'testing' | 'business' | 'marketing';
@@ -80,7 +77,7 @@ function SkillCard({
onInstall,
onUninstall,
}: {
skill: SkillInfo;
skill: SkillDisplay;
isExpanded: boolean;
onToggle: () => void;
onInstall: () => void;
@@ -240,35 +237,6 @@ function SkillCard({
);
}
function SuggestionCard({ suggestion }: { suggestion: SkillSuggestion }) {
const confidencePercent = Math.round(suggestion.confidence * 100);
return (
<div className="p-3 bg-gradient-to-r from-blue-50 to-purple-50 dark:from-blue-900/20 dark:to-purple-900/20 rounded-lg border border-blue-200 dark:border-blue-800">
<div className="flex items-center gap-2 mb-2">
<Sparkles className="w-4 h-4 text-blue-500" />
<span className="text-sm font-medium text-gray-900 dark:text-gray-100">
{suggestion.skill.name}
</span>
<span className="text-xs text-blue-600 dark:text-blue-400 ml-auto">
{confidencePercent}%
</span>
</div>
<p className="text-xs text-gray-600 dark:text-gray-300 mb-2">{suggestion.reason}</p>
<div className="flex flex-wrap gap-1">
{suggestion.matchedPatterns.map((pattern) => (
<span
key={pattern}
className="px-1.5 py-0.5 text-xs bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400 rounded"
>
{pattern}
</span>
))}
</div>
</div>
);
}
// === Main Component ===
export function SkillMarket({
@@ -276,19 +244,23 @@ export function SkillMarket({
onSkillInstall,
onSkillUninstall,
}: SkillMarketProps) {
const [engine] = useState(() => new SkillDiscoveryEngine());
const [skills, setSkills] = useState<SkillInfo[]>([]);
// Use configStore instead of SkillDiscoveryEngine
const skillsCatalog = useConfigStore((s) => s.skillsCatalog);
const loadSkillsCatalog = useConfigStore((s) => s.loadSkillsCatalog);
const updateSkill = useConfigStore((s) => s.updateSkill);
const [searchQuery, setSearchQuery] = useState('');
const [categoryFilter, setCategoryFilter] = useState<CategoryFilter>('all');
const [expandedSkillId, setExpandedSkillId] = useState<string | null>(null);
const [suggestions, setSuggestions] = useState<SkillSuggestion[]>([]);
const [isRefreshing, setIsRefreshing] = useState(false);
// Load skills
// Adapt skills to display format
const skills = useMemo(() => adaptSkillsCatalog(skillsCatalog), [skillsCatalog]);
// Load skills on mount
useEffect(() => {
const allSkills = engine.getAllSkills();
setSkills(allSkills);
}, [engine]);
loadSkillsCatalog();
}, [loadSkillsCatalog]);
// Filter skills
const filteredSkills = useMemo(() => {
@@ -301,13 +273,17 @@ export function SkillMarket({
// Search filter
if (searchQuery.trim()) {
const searchResult = engine.searchSkills(searchQuery);
const matchingIds = new Set(searchResult.results.map((s) => s.id));
result = result.filter((s) => matchingIds.has(s.id));
const queryLower = searchQuery.toLowerCase();
result = result.filter((s) =>
s.name.toLowerCase().includes(queryLower) ||
s.description.toLowerCase().includes(queryLower) ||
s.triggers.some((t) => t.toLowerCase().includes(queryLower)) ||
s.capabilities.some((c) => c.toLowerCase().includes(queryLower))
);
}
return result;
}, [skills, categoryFilter, searchQuery, engine]);
}, [skills, categoryFilter, searchQuery]);
// Get categories from skills
const categories = useMemo(() => {
@@ -323,44 +299,31 @@ export function SkillMarket({
const handleRefresh = useCallback(async () => {
setIsRefreshing(true);
await new Promise((resolve) => setTimeout(resolve, 500));
// engine.refreshIndex doesn't exist - skip
setSkills(engine.getAllSkills());
await loadSkillsCatalog();
setIsRefreshing(false);
}, [engine]);
}, [loadSkillsCatalog]);
const handleInstall = useCallback(
(skill: SkillInfo) => {
// Install skill - update local state
setSkills((prev) => prev.map(s => ({ ...s, installed: true })));
onSkillInstall?.(skill);
},
[onSkillInstall]
async (skill: SkillDisplay) => {
// Update skill via configStore (persists to backend)
await updateSkill(skill.id, { enabled: true });
onSkillInstall?.(skill);
},
[updateSkill, onSkillInstall]
);
const handleUninstall = useCallback(
(skill: SkillInfo) => {
// Uninstall skill - update local state
setSkills((prev) => prev.map(s => ({ ...s, installed: false })));
onSkillUninstall?.(skill);
async (skill: SkillDisplay) => {
// Update skill via configStore (persists to backend)
await updateSkill(skill.id, { enabled: false });
onSkillUninstall?.(skill);
},
[onSkillUninstall]
[updateSkill, onSkillUninstall]
);
const handleSearch = useCallback(
async (query: string) => {
setSearchQuery(query);
if (query.trim()) {
// Get suggestions based on search
const mockConversation = [{ role: 'user' as const, content: query }];
const newSuggestions = await engine.suggestSkills(mockConversation, 'default', 3);
setSuggestions(newSuggestions.slice(0, 3));
} else {
setSuggestions([]);
}
},
[engine]
);
const handleSearch = useCallback((query: string) => {
setSearchQuery(query);
}, []);
return (
<div className={`flex flex-col h-full ${className}`}>
@@ -405,25 +368,8 @@ export function SkillMarket({
/>
</div>
{/* Suggestions */}
<AnimatePresence>
{suggestions.length > 0 && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
className="mt-3 space-y-2"
>
<h4 className="text-xs font-medium text-gray-500 dark:text-gray-400 flex items-center gap-1">
<Info className="w-3 h-3" />
</h4>
{suggestions.map((suggestion) => (
<SuggestionCard key={suggestion.skill.id} suggestion={suggestion} />
))}
</motion.div>
)}
</AnimatePresence>
{/* Suggestions - placeholder for future AI-powered recommendations */}
</div>
{/* Category Filter */}