chore: move ccw-skill-hub to standalone repository

Migrated ccw-skill-hub to D:/ccw-skill-hub as independent git project.
Removed nested git repos (ccw/frontend/ccw-skill-hub, skill-hub-repo, skill-hub-temp).
This commit is contained in:
catlog22
2026-02-24 11:57:26 +08:00
parent 6f0bbe84ea
commit 61e313a0c1
35 changed files with 3189 additions and 362 deletions

View File

@@ -0,0 +1,353 @@
// ========================================
// SkillHubDetailPanel Component
// ========================================
// Right-side slide-out panel for viewing skill hub skill details
import { useEffect, useState } from 'react';
import { useIntl } from 'react-intl';
import {
X,
FileText,
Tag,
User,
Globe,
Folder,
ExternalLink,
Download,
RefreshCw,
Check,
} from 'lucide-react';
import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge';
import { Card } from '@/components/ui/Card';
import { CliModeToggle, type CliMode } from '@/components/mcp/CliModeToggle';
import type { RemoteSkill, LocalSkill, InstalledSkill, CliType, SkillSource } from '@/hooks/useSkillHub';
export interface SkillHubDetailPanelProps {
skill: RemoteSkill | LocalSkill | null;
isOpen: boolean;
onClose: () => void;
source: SkillSource;
installedInfo?: InstalledSkill;
onInstall?: (skill: RemoteSkill | LocalSkill, cliType: CliType) => Promise<void>;
onUninstall?: (skill: RemoteSkill | LocalSkill, cliType: CliType) => Promise<void>;
isInstalling?: boolean;
}
export function SkillHubDetailPanel({
skill,
isOpen,
onClose,
source,
installedInfo,
onInstall,
onUninstall,
isInstalling = false,
}: SkillHubDetailPanelProps) {
const { formatMessage } = useIntl();
const [cliMode, setCliMode] = useState<CliMode>('claude');
const [localInstalling, setLocalInstalling] = useState(false);
// Prevent body scroll when panel is open
useEffect(() => {
if (isOpen) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = '';
}
return () => {
document.body.style.overflow = '';
};
}, [isOpen]);
const isLoading = isInstalling || localInstalling;
const isInstalled = !!installedInfo;
const isRemote = source === 'remote';
const handleInstall = async () => {
if (!skill) return;
setLocalInstalling(true);
try {
await onInstall?.(skill, cliMode);
} finally {
setLocalInstalling(false);
}
};
const handleUninstall = async () => {
if (!skill) return;
await onUninstall?.(skill, installedInfo?.installedTo || cliMode);
};
if (!isOpen || !skill) {
return null;
}
return (
<>
{/* Overlay */}
<div
className="fixed inset-0 bg-black/50 z-50 transition-opacity"
onClick={onClose}
/>
{/* Panel */}
<div className="fixed top-0 right-0 w-full sm:w-[480px] md:w-[560px] lg:w-[640px] h-full bg-background border-l border-border shadow-xl z-50 flex flex-col transition-transform">
{/* Header */}
<div className="flex items-center justify-between px-6 py-4 border-b border-border">
<div className="flex items-center gap-3 min-w-0">
<div className="p-2 rounded-lg flex-shrink-0 bg-primary/10">
<Download className="w-5 h-5 text-primary" />
</div>
<div className="min-w-0">
<h3 className="text-lg font-semibold text-foreground truncate">{skill.name}</h3>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
{skill.version && <span>v{skill.version}</span>}
{skill.author && (
<>
<span>·</span>
<span className="flex items-center gap-1">
<User className="w-3 h-3" />
{skill.author}
</span>
</>
)}
</div>
</div>
</div>
<button
onClick={onClose}
className="w-8 h-8 flex items-center justify-center text-muted-foreground hover:text-foreground hover:bg-muted rounded-lg transition-colors"
>
<X className="w-5 h-5" />
</button>
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto p-6">
<div className="space-y-6">
{/* Status Badges */}
<div className="flex flex-wrap gap-2">
<Badge variant={isRemote ? 'default' : 'secondary'} className="gap-1">
{isRemote ? <Globe className="w-3 h-3" /> : <Folder className="w-3 h-3" />}
{isRemote
? formatMessage({ id: 'skillHub.source.remote' })
: formatMessage({ id: 'skillHub.source.local' })}
</Badge>
{skill.category && (
<Badge variant="outline">{skill.category}</Badge>
)}
{isInstalled && (
installedInfo?.updatesAvailable ? (
<Badge variant="outline" className="gap-1 text-amber-500 border-amber-500">
<RefreshCw className="w-3 h-3" />
{formatMessage({ id: 'skillHub.status.updateAvailable' })}
</Badge>
) : (
<Badge variant="outline" className="gap-1 text-success border-success">
<Check className="w-3 h-3" />
{formatMessage({ id: 'skillHub.status.installed' })}
</Badge>
)
)}
</div>
{/* Description */}
<section>
<h4 className="text-sm font-semibold text-foreground mb-2 flex items-center gap-2">
<FileText className="w-4 h-4 text-muted-foreground" />
{formatMessage({ id: 'skills.card.description' })}
</h4>
<p className="text-sm text-muted-foreground leading-relaxed">
{skill.description || formatMessage({ id: 'skills.noDescription' })}
</p>
</section>
{/* Tags */}
{skill.tags && skill.tags.length > 0 && (
<section>
<h4 className="text-sm font-semibold text-foreground mb-2 flex items-center gap-2">
<Tag className="w-4 h-4 text-muted-foreground" />
{formatMessage({ id: 'skillHub.card.tags' })}
</h4>
<div className="flex flex-wrap gap-2">
{skill.tags.map((tag) => (
<Badge key={tag} variant="secondary" className="text-sm">
{tag}
</Badge>
))}
</div>
</section>
)}
{/* Metadata */}
<section>
<h4 className="text-sm font-semibold text-foreground mb-3">
{formatMessage({ id: 'skills.metadata' })}
</h4>
<div className="grid grid-cols-2 gap-3">
{skill.version && (
<Card className="p-3 bg-muted/50">
<span className="text-xs text-muted-foreground block mb-1">
{formatMessage({ id: 'skills.card.version' })}
</span>
<p className="text-sm font-medium text-foreground">v{skill.version}</p>
</Card>
)}
{skill.author && (
<Card className="p-3 bg-muted/50">
<span className="text-xs text-muted-foreground block mb-1">
{formatMessage({ id: 'skills.card.author' })}
</span>
<p className="text-sm font-medium text-foreground">{skill.author}</p>
</Card>
)}
{skill.category && (
<Card className="p-3 bg-muted/50">
<span className="text-xs text-muted-foreground block mb-1">
{formatMessage({ id: 'skills.card.category' })}
</span>
<p className="text-sm font-medium text-foreground">{skill.category}</p>
</Card>
)}
{isRemote && (skill as RemoteSkill).updatedAt && (
<Card className="p-3 bg-muted/50">
<span className="text-xs text-muted-foreground block mb-1">
{formatMessage({ id: 'skillHub.card.updated' }, { date: '' }).trim()}
</span>
<p className="text-sm font-medium text-foreground">
{new Date((skill as RemoteSkill).updatedAt as string).toLocaleDateString()}
</p>
</Card>
)}
{!isRemote && (skill as LocalSkill).path && (
<Card className="p-3 bg-muted/50 col-span-2">
<span className="text-xs text-muted-foreground block mb-1">
{formatMessage({ id: 'skills.path' })}
</span>
<p className="text-sm font-mono text-foreground break-all">
{(skill as LocalSkill).path}
</p>
</Card>
)}
</div>
</section>
{/* Links (for remote skills) */}
{isRemote && (
(skill as RemoteSkill).readmeUrl ||
(skill as RemoteSkill).homepage ||
(skill as RemoteSkill).license
) && (
<section>
<h4 className="text-sm font-semibold text-foreground mb-3">
{formatMessage({ id: 'skillHub.links' })}
</h4>
<div className="space-y-2">
{(skill as RemoteSkill).readmeUrl && (
<a
href={(skill as RemoteSkill).readmeUrl}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 text-sm text-primary hover:underline"
>
<ExternalLink className="w-4 h-4" />
README
</a>
)}
{(skill as RemoteSkill).homepage && (
<a
href={(skill as RemoteSkill).homepage}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 text-sm text-primary hover:underline"
>
<ExternalLink className="w-4 h-4" />
{formatMessage({ id: 'skillHub.homepage' })}
</a>
)}
{(skill as RemoteSkill).license && (
<div className="text-sm text-muted-foreground">
{formatMessage({ id: 'skillHub.license' })}: {(skill as RemoteSkill).license}
</div>
)}
</div>
</section>
)}
{/* Installation Info */}
{isInstalled && installedInfo && (
<section>
<h4 className="text-sm font-semibold text-foreground mb-3">
{formatMessage({ id: 'skillHub.installationInfo' })}
</h4>
<Card className="p-3 bg-muted/50">
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground">{formatMessage({ id: 'skillHub.installedTo' })}</span>
<span className="font-medium">{installedInfo.installedTo}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">{formatMessage({ id: 'skillHub.installedAt' })}</span>
<span className="font-medium">
{new Date(installedInfo.installedAt).toLocaleDateString()}
</span>
</div>
{installedInfo.updatesAvailable && installedInfo.latestVersion && (
<div className="flex justify-between">
<span className="text-muted-foreground">{formatMessage({ id: 'skillHub.latestVersion' })}</span>
<span className="font-medium text-amber-500">v{installedInfo.latestVersion}</span>
</div>
)}
</div>
</Card>
</section>
)}
</div>
</div>
{/* Footer Actions */}
<div className="px-6 py-4 border-t border-border">
<div className="flex items-center justify-between gap-4">
<CliModeToggle currentMode={cliMode} onModeChange={setCliMode} />
<div className="flex gap-2">
{isInstalled && (
<Button
variant="outline"
onClick={handleUninstall}
className="text-destructive hover:text-destructive"
>
{formatMessage({ id: 'skillHub.actions.uninstall' })}
</Button>
)}
<Button
variant={isInstalled ? 'outline' : 'default'}
onClick={handleInstall}
disabled={isLoading}
>
{isLoading ? (
<>
<RefreshCw className="w-4 h-4 mr-2 animate-spin" />
{formatMessage({ id: 'skillHub.actions.installing' })}
</>
) : isInstalled ? (
<>
<RefreshCw className="w-4 h-4 mr-2" />
{formatMessage({ id: 'skillHub.actions.update' })}
</>
) : (
<>
<Download className="w-4 h-4 mr-2" />
{formatMessage({ id: 'skillHub.actions.install' })}
</>
)}
</Button>
</div>
</div>
</div>
</div>
</>
);
}
export default SkillHubDetailPanel;

View File

@@ -25,6 +25,9 @@ export type { SkillCreateDialogProps } from './SkillCreateDialog';
export { SkillHubCard } from './SkillHubCard';
export type { SkillHubCardProps } from './SkillHubCard';
export { SkillHubDetailPanel } from './SkillHubDetailPanel';
export type { SkillHubDetailPanelProps } from './SkillHubDetailPanel';
export { StatCard, StatCardSkeleton } from './StatCard';
export type { StatCardProps } from './StatCard';

View File

@@ -547,7 +547,7 @@ export function useUpdateCodexLensConfig(): UseUpdateCodexLensConfigReturn {
mutationFn: updateCodexLensConfig,
onMutate: () => {
info(
formatMessage({ id: 'status.inProgress' }),
formatMessage({ id: 'common.status.inProgress' }),
formatMessage({ id: 'common.feedback.codexLensConfigUpdate.success' })
);
},
@@ -780,7 +780,7 @@ export function useDeleteModel(): UseDeleteModelReturn {
},
onMutate: () => {
info(
formatMessage({ id: 'status.deleting' }),
formatMessage({ id: 'common.actions.deleting' }),
formatMessage({ id: 'common.feedback.codexLensDeleteModel.success' })
);
},
@@ -825,7 +825,7 @@ export function useUpdateCodexLensEnv(): UseUpdateCodexLensEnvReturn {
mutationFn: (request: CodexLensUpdateEnvRequest) => updateCodexLensEnv(request),
onMutate: () => {
info(
formatMessage({ id: 'status.inProgress' }),
formatMessage({ id: 'common.status.inProgress' }),
formatMessage({ id: 'common.feedback.codexLensUpdateEnv.success' })
);
},
@@ -872,7 +872,7 @@ export function useSelectGpu(): UseSelectGpuReturn {
mutationFn: (deviceId: string | number) => selectCodexLensGpu(deviceId),
onMutate: () => {
info(
formatMessage({ id: 'status.inProgress' }),
formatMessage({ id: 'common.status.inProgress' }),
formatMessage({ id: 'common.feedback.codexLensSelectGpu.success' })
);
},
@@ -895,7 +895,7 @@ export function useSelectGpu(): UseSelectGpuReturn {
mutationFn: () => resetCodexLensGpu(),
onMutate: () => {
info(
formatMessage({ id: 'status.inProgress' }),
formatMessage({ id: 'common.status.inProgress' }),
formatMessage({ id: 'common.feedback.codexLensResetGpu.success' })
);
},
@@ -941,7 +941,7 @@ export function useUpdateIgnorePatterns(): UseUpdateIgnorePatternsReturn {
mutationFn: updateCodexLensIgnorePatterns,
onMutate: () => {
info(
formatMessage({ id: 'status.inProgress' }),
formatMessage({ id: 'common.status.inProgress' }),
formatMessage({ id: 'common.feedback.codexLensUpdatePatterns.success' })
);
},
@@ -1070,7 +1070,7 @@ export function useRebuildIndex(): UseRebuildIndexReturn {
}) => rebuildCodexLensIndex(projectPath, options),
onMutate: () => {
info(
formatMessage({ id: 'status.inProgress' }),
formatMessage({ id: 'common.status.inProgress' }),
formatMessage({ id: 'common.feedback.codexLensRebuildIndex.success' })
);
},
@@ -1132,7 +1132,7 @@ export function useUpdateIndex(): UseUpdateIndexReturn {
}) => updateCodexLensIndex(projectPath, options),
onMutate: () => {
info(
formatMessage({ id: 'status.inProgress' }),
formatMessage({ id: 'common.status.inProgress' }),
formatMessage({ id: 'common.feedback.codexLensUpdateIndex.success' })
);
},
@@ -1178,7 +1178,7 @@ export function useCancelIndexing(): UseCancelIndexingReturn {
mutationFn: cancelCodexLensIndexing,
onMutate: () => {
info(
formatMessage({ id: 'status.inProgress' }),
formatMessage({ id: 'common.status.inProgress' }),
formatMessage({ id: 'common.feedback.codexLensCancelIndexing.success' })
);
},

View File

@@ -177,6 +177,9 @@
"helpQuotes": "Values with spaces should use quotes",
"helpRestart": "Restart service after changes to take effect"
},
"downloadedModels": "Downloaded Models",
"noConfiguredModels": "No models configured",
"noLocalModels": "No models downloaded",
"models": {
"title": "Model Management",
"searchPlaceholder": "Search models...",

View File

@@ -1,42 +1,83 @@
{
"skillHub.title": "Skill Hub",
"skillHub.description": "Discover and install shared skills from the community",
"skillHub.source.remote": "Remote",
"skillHub.source.local": "Local",
"skillHub.status.installed": "Installed",
"skillHub.status.updateAvailable": "Update Available",
"skillHub.tabs.remote": "Remote",
"skillHub.tabs.local": "Local",
"skillHub.tabs.installed": "Installed",
"skillHub.stats.remote": "Remote Skills",
"skillHub.stats.remoteDesc": "Available from community",
"skillHub.stats.local": "Local Skills",
"skillHub.stats.localDesc": "Shared locally",
"skillHub.stats.installed": "Installed",
"skillHub.stats.installedDesc": "Skills in use",
"skillHub.stats.updates": "Updates",
"skillHub.stats.updatesDesc": "New versions available",
"skillHub.search.placeholder": "Search skills...",
"skillHub.filter.allCategories": "All Categories",
"skillHub.actions.refresh": "Refresh",
"skillHub.actions.install": "Install",
"skillHub.actions.installing": "Installing...",
"skillHub.actions.update": "Update",
"skillHub.actions.uninstall": "Uninstall",
"skillHub.actions.viewDetails": "View Details",
"skillHub.card.tags": "Tags",
"skillHub.card.updated": "Updated: {date}",
"skillHub.install.success": "Skill '{name}' installed successfully",
"skillHub.install.error": "Failed to install skill: {error}",
"skillHub.uninstall.success": "Skill '{name}' uninstalled",
"skillHub.uninstall.error": "Failed to uninstall skill: {error}",
"skillHub.refresh.success": "Skill list refreshed",
"skillHub.details.comingSoon": "Details view coming soon",
"skillHub.error.loadFailed": "Failed to load skills. Check network connection.",
"skillHub.empty.remote.title": "No Remote Skills",
"skillHub.empty.remote.description": "Remote skill repository is empty or unreachable.",
"skillHub.empty.local.title": "No Local Skills",
"skillHub.empty.local.description": "Add skills to ~/.ccw/skill-hub/local/ to share them.",
"skillHub.empty.installed.title": "No Installed Skills",
"skillHub.empty.installed.description": "Install skills from Remote or Local tabs to use them."
"title": "Skill Hub",
"description": "Discover and install shared skills from the community",
"links": "Links",
"homepage": "Homepage",
"license": "License",
"source": {
"remote": "Remote",
"local": "Local"
},
"status": {
"installed": "Installed",
"updateAvailable": "Update Available"
},
"tabs": {
"remote": "Remote",
"local": "Local",
"installed": "Installed"
},
"stats": {
"remote": "Remote Skills",
"remoteDesc": "Available from community",
"local": "Local Skills",
"localDesc": "Shared locally",
"installed": "Installed",
"installedDesc": "Skills in use",
"updates": "Updates",
"updatesDesc": "New versions available"
},
"search": {
"placeholder": "Search skills..."
},
"filter": {
"allCategories": "All Categories"
},
"actions": {
"refresh": "Refresh",
"install": "Install",
"installing": "Installing...",
"update": "Update",
"uninstall": "Uninstall",
"viewDetails": "View Details"
},
"card": {
"tags": "Tags",
"updated": "Updated: {date}"
},
"install": {
"success": "Skill '{name}' installed successfully",
"error": "Failed to install skill: {error}"
},
"uninstall": {
"success": "Skill '{name}' uninstalled",
"error": "Failed to uninstall skill: {error}"
},
"refresh": {
"success": "Skill list refreshed"
},
"details": {
"comingSoon": "Details view coming soon"
},
"error": {
"loadFailed": "Failed to load skills. Check network connection."
},
"empty": {
"remote": {
"title": "No Remote Skills",
"description": "Remote skill repository is empty or unreachable."
},
"local": {
"title": "No Local Skills",
"description": "Add skills to ~/.ccw/skill-hub/local/ to share them."
},
"installed": {
"title": "No Installed Skills",
"description": "Install skills from Remote or Local tabs to use them."
}
},
"installationInfo": "Installation Info",
"installedTo": "Installed To",
"installedAt": "Installed At",
"latestVersion": "Latest Version"
}

View File

@@ -177,6 +177,9 @@
"helpQuotes": "包含空格的值建议使用引号",
"helpRestart": "修改后需要重启服务才能生效"
},
"downloadedModels": "已下载模型",
"noConfiguredModels": "无已配置模型",
"noLocalModels": "无已下载模型",
"models": {
"title": "模型管理",
"searchPlaceholder": "搜索模型...",

View File

@@ -1,42 +1,83 @@
{
"skillHub.title": "技能中心",
"skillHub.description": "发现并安装社区共享的技能",
"skillHub.source.remote": "远程",
"skillHub.source.local": "本地",
"skillHub.status.installed": "已安装",
"skillHub.status.updateAvailable": "有更新",
"skillHub.tabs.remote": "远程",
"skillHub.tabs.local": "本地",
"skillHub.tabs.installed": "已安装",
"skillHub.stats.remote": "远程技能",
"skillHub.stats.remoteDesc": "来自社区",
"skillHub.stats.local": "本地技能",
"skillHub.stats.localDesc": "本地共享",
"skillHub.stats.installed": "已安装",
"skillHub.stats.installedDesc": "使用中的技能",
"skillHub.stats.updates": "更新",
"skillHub.stats.updatesDesc": "有新版本可用",
"skillHub.search.placeholder": "搜索技能...",
"skillHub.filter.allCategories": "全部分类",
"skillHub.actions.refresh": "刷新",
"skillHub.actions.install": "安装",
"skillHub.actions.installing": "安装中...",
"skillHub.actions.update": "更新",
"skillHub.actions.uninstall": "卸载",
"skillHub.actions.viewDetails": "查看详情",
"skillHub.card.tags": "标签",
"skillHub.card.updated": "更新于: {date}",
"skillHub.install.success": "技能 '{name}' 安装成功",
"skillHub.install.error": "安装技能失败: {error}",
"skillHub.uninstall.success": "技能 '{name}' 已卸载",
"skillHub.uninstall.error": "卸载技能失败: {error}",
"skillHub.refresh.success": "技能列表已刷新",
"skillHub.details.comingSoon": "详情视图即将推出",
"skillHub.error.loadFailed": "加载技能失败。请检查网络连接。",
"skillHub.empty.remote.title": "暂无远程技能",
"skillHub.empty.remote.description": "远程技能仓库为空或无法访问。",
"skillHub.empty.local.title": "暂无本地技能",
"skillHub.empty.local.description": "将技能添加到 ~/.ccw/skill-hub/local/ 即可共享。",
"skillHub.empty.installed.title": "暂无已安装技能",
"skillHub.empty.installed.description": "从远程或本地标签页安装技能以使用它们。"
"title": "技能中心",
"description": "发现并安装社区共享的技能",
"links": "链接",
"homepage": "主页",
"license": "许可证",
"source": {
"remote": "远程",
"local": "本地"
},
"status": {
"installed": "已安装",
"updateAvailable": "有更新"
},
"tabs": {
"remote": "远程",
"local": "本地",
"installed": "已安装"
},
"stats": {
"remote": "远程技能",
"remoteDesc": "来自社区",
"local": "本地技能",
"localDesc": "本地共享",
"installed": "已安装",
"installedDesc": "使用中的技能",
"updates": "更新",
"updatesDesc": "有新版本可用"
},
"search": {
"placeholder": "搜索技能..."
},
"filter": {
"allCategories": "全部分类"
},
"actions": {
"refresh": "刷新",
"install": "安装",
"installing": "安装中...",
"update": "更新",
"uninstall": "卸载",
"viewDetails": "查看详情"
},
"card": {
"tags": "标签",
"updated": "更新于: {date}"
},
"install": {
"success": "技能 '{name}' 安装成功",
"error": "安装技能失败: {error}"
},
"uninstall": {
"success": "技能 '{name}' 已卸载",
"error": "卸载技能失败: {error}"
},
"refresh": {
"success": "技能列表已刷新"
},
"details": {
"comingSoon": "详情视图即将推出"
},
"error": {
"loadFailed": "加载技能失败。请检查网络连接。"
},
"empty": {
"remote": {
"title": "暂无远程技能",
"description": "远程技能仓库为空或无法访问。"
},
"local": {
"title": "暂无本地技能",
"description": "将技能添加到 ~/.ccw/skill-hub/local/ 即可共享。"
},
"installed": {
"title": "暂无已安装技能",
"description": "从远程或本地标签页安装技能以使用它们。"
}
},
"installationInfo": "安装信息",
"installedTo": "安装到",
"installedAt": "安装时间",
"latestVersion": "最新版本"
}

View File

@@ -20,7 +20,7 @@ import { Input } from '@/components/ui/Input';
import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge';
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/Tabs';
import { StatCard } from '@/components/shared';
import { StatCard, SkillHubDetailPanel } from '@/components/shared';
import { SkillHubCard } from '@/components/shared/SkillHubCard';
import {
useSkillHub,
@@ -136,6 +136,8 @@ export function SkillHubPage() {
const [activeTab, setActiveTab] = useState<TabValue>('remote');
const [searchQuery, setSearchQuery] = useState('');
const [categoryFilter, setCategoryFilter] = useState<string | null>(null);
const [selectedSkill, setSelectedSkill] = useState<{ skill: RemoteSkill | LocalSkill; source: SkillSource } | null>(null);
const [isDetailPanelOpen, setIsDetailPanelOpen] = useState(false);
// Fetch data
const {
@@ -221,8 +223,15 @@ export function SkillHubPage() {
}
};
const handleViewDetails = () => {
toast.info(formatMessage({ id: 'skillHub.details.comingSoon' }));
const handleViewDetails = (skill: RemoteSkill | LocalSkill) => {
const source: SkillSource = 'downloadUrl' in skill ? 'remote' : 'local';
setSelectedSkill({ skill, source });
setIsDetailPanelOpen(true);
};
const handleCloseDetailPanel = () => {
setIsDetailPanelOpen(false);
setSelectedSkill(null);
};
const handleRefresh = () => {
@@ -378,7 +387,7 @@ export function SkillHubPage() {
source={skillSource}
onInstall={handleInstall}
onUninstall={handleUninstall}
onViewDetails={handleViewDetails}
onViewDetails={() => handleViewDetails(skill as RemoteSkill | LocalSkill)}
isInstalling={installMutation.isPending}
/>
);
@@ -386,6 +395,20 @@ export function SkillHubPage() {
</div>
)}
</div>
{/* Detail Panel */}
{selectedSkill && (
<SkillHubDetailPanel
skill={selectedSkill.skill}
isOpen={isDetailPanelOpen}
onClose={handleCloseDetailPanel}
source={selectedSkill.source}
installedInfo={installedMap.get(selectedSkill.skill.id)}
onInstall={handleInstall}
onUninstall={handleUninstall}
isInstalling={installMutation.isPending}
/>
)}
</div>
);
}

View File

@@ -1,4 +1,4 @@
import { existsSync, mkdirSync, readdirSync, statSync, copyFileSync, readFileSync, writeFileSync, unlinkSync, rmdirSync, appendFileSync, renameSync } from 'fs';
import { existsSync, mkdirSync, readdirSync, statSync, copyFileSync, readFileSync, writeFileSync, unlinkSync, rmdirSync, appendFileSync, renameSync, cpSync } from 'fs';
import { join, dirname, basename } from 'path';
import { homedir, platform } from 'os';
import { fileURLToPath } from 'url';

View File

@@ -565,7 +565,7 @@ export async function handleCodexLensConfigRoutes(ctx: RouteContext): Promise<bo
return true;
}
// API: CodexLens Model List (list available embedding models)
// API: CodexLens Model List (list available embedding AND reranker models)
if (pathname === '/api/codexlens/models' && req.method === 'GET') {
try {
// Check if CodexLens is installed first (without auto-installing)
@@ -575,20 +575,46 @@ export async function handleCodexLensConfigRoutes(ctx: RouteContext): Promise<bo
res.end(JSON.stringify({ success: false, error: 'CodexLens not installed' }));
return true;
}
const result = await executeCodexLens(['model-list', '--json']);
if (result.success) {
// Fetch both embedding and reranker models in parallel
const [embeddingResult, rerankerResult] = await Promise.all([
executeCodexLens(['model-list', '--json']),
executeCodexLens(['reranker-model-list', '--json'])
]);
const allModels: any[] = [];
// Parse embedding models and add type field
if (embeddingResult.success) {
try {
const parsed = extractJSON(result.output ?? '');
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(parsed));
const parsed = extractJSON(embeddingResult.output ?? '');
const models = parsed?.result?.models ?? parsed?.models ?? [];
for (const model of models) {
allModels.push({ ...model, type: 'embedding' });
}
} catch {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: true, result: { models: [] }, output: result.output }));
// Ignore parsing errors for embedding models
}
} else {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: false, error: result.error }));
}
// Parse reranker models and add type field
if (rerankerResult.success) {
try {
const parsed = extractJSON(rerankerResult.output ?? '');
const models = parsed?.result?.models ?? parsed?.models ?? [];
for (const model of models) {
allModels.push({ ...model, type: 'reranker' });
}
} catch {
// Ignore parsing errors for reranker models
}
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
success: true,
result: { models: allModels }
}));
} catch (err: unknown) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: false, error: err instanceof Error ? err.message : String(err) }));
@@ -596,27 +622,65 @@ export async function handleCodexLensConfigRoutes(ctx: RouteContext): Promise<bo
return true;
}
// API: CodexLens Model Download (download embedding model by profile)
// API: CodexLens Model Download (download embedding or reranker model by profile)
if (pathname === '/api/codexlens/models/download' && req.method === 'POST') {
handlePostRequest(req, res, async (body) => {
const { profile } = body as { profile?: unknown };
const { profile, model_type } = body as { profile?: unknown; model_type?: unknown };
const resolvedProfile = typeof profile === 'string' && profile.trim().length > 0 ? profile.trim() : undefined;
const resolvedModelType = typeof model_type === 'string' ? model_type.trim() : undefined;
if (!resolvedProfile) {
return { success: false, error: 'profile is required', status: 400 };
}
try {
const result = await executeCodexLens(['model-download', resolvedProfile, '--json'], { timeout: 600000 }); // 10 min for download
if (result.success) {
// If model_type is specified, use it directly
if (resolvedModelType === 'reranker') {
const result = await executeCodexLens(['reranker-model-download', resolvedProfile, '--json'], { timeout: 600000 });
if (result.success) {
try {
const parsed = extractJSON(result.output ?? '');
return { success: true, ...parsed };
} catch {
return { success: true, output: result.output };
}
} else {
return { success: false, error: result.error, status: 500 };
}
}
// Try embedding model first, then reranker if profile not found
const embeddingResult = await executeCodexLens(['model-download', resolvedProfile, '--json'], { timeout: 600000 });
// Check if the error indicates unknown profile
const outputStr = embeddingResult.output ?? '';
const isUnknownProfile = !embeddingResult.success &&
(outputStr.includes('Unknown profile') || outputStr.includes('unknown profile'));
if (isUnknownProfile) {
// Try reranker model
const rerankerResult = await executeCodexLens(['reranker-model-download', resolvedProfile, '--json'], { timeout: 600000 });
if (rerankerResult.success) {
try {
const parsed = extractJSON(rerankerResult.output ?? '');
return { success: true, ...parsed };
} catch {
return { success: true, output: rerankerResult.output };
}
} else {
return { success: false, error: rerankerResult.error, status: 500 };
}
}
if (embeddingResult.success) {
try {
const parsed = extractJSON(result.output ?? '');
const parsed = extractJSON(embeddingResult.output ?? '');
return { success: true, ...parsed };
} catch {
return { success: true, output: result.output };
return { success: true, output: embeddingResult.output };
}
} else {
return { success: false, error: result.error, status: 500 };
return { success: false, error: embeddingResult.error, status: 500 };
}
} catch (err: unknown) {
return { success: false, error: err instanceof Error ? err.message : String(err), status: 500 };
@@ -665,27 +729,65 @@ export async function handleCodexLensConfigRoutes(ctx: RouteContext): Promise<bo
return true;
}
// API: CodexLens Model Delete (delete embedding model by profile)
// API: CodexLens Model Delete (delete embedding or reranker model by profile)
if (pathname === '/api/codexlens/models/delete' && req.method === 'POST') {
handlePostRequest(req, res, async (body) => {
const { profile } = body as { profile?: unknown };
const { profile, model_type } = body as { profile?: unknown; model_type?: unknown };
const resolvedProfile = typeof profile === 'string' && profile.trim().length > 0 ? profile.trim() : undefined;
const resolvedModelType = typeof model_type === 'string' ? model_type.trim() : undefined;
if (!resolvedProfile) {
return { success: false, error: 'profile is required', status: 400 };
}
try {
const result = await executeCodexLens(['model-delete', resolvedProfile, '--json']);
if (result.success) {
// If model_type is specified, use it directly
if (resolvedModelType === 'reranker') {
const result = await executeCodexLens(['reranker-model-delete', resolvedProfile, '--json']);
if (result.success) {
try {
const parsed = extractJSON(result.output ?? '');
return { success: true, ...parsed };
} catch {
return { success: true, output: result.output };
}
} else {
return { success: false, error: result.error, status: 500 };
}
}
// Try embedding model first, then reranker if profile not found
const embeddingResult = await executeCodexLens(['model-delete', resolvedProfile, '--json']);
// Check if the error indicates unknown profile
const outputStr = embeddingResult.output ?? '';
const isUnknownProfile = !embeddingResult.success &&
(outputStr.includes('Unknown profile') || outputStr.includes('unknown profile'));
if (isUnknownProfile) {
// Try reranker model
const rerankerResult = await executeCodexLens(['reranker-model-delete', resolvedProfile, '--json']);
if (rerankerResult.success) {
try {
const parsed = extractJSON(rerankerResult.output ?? '');
return { success: true, ...parsed };
} catch {
return { success: true, output: rerankerResult.output };
}
} else {
return { success: false, error: rerankerResult.error, status: 500 };
}
}
if (embeddingResult.success) {
try {
const parsed = extractJSON(result.output ?? '');
const parsed = extractJSON(embeddingResult.output ?? '');
return { success: true, ...parsed };
} catch {
return { success: true, output: result.output };
return { success: true, output: embeddingResult.output };
}
} else {
return { success: false, error: result.error, status: 500 };
return { success: false, error: embeddingResult.error, status: 500 };
}
} catch (err: unknown) {
return { success: false, error: err instanceof Error ? err.message : String(err), status: 500 };

View File

@@ -171,10 +171,10 @@ interface SkillCacheRequest {
* GitHub repository configuration for remote skills
*/
const GITHUB_CONFIG = {
owner: 'anthropics',
repo: 'claude-code-workflow',
owner: 'catlog22',
repo: 'skill-hub',
branch: 'main',
skillIndexPath: 'skill-hub/index.json'
skillIndexPath: 'index.json'
};
/**