// ======================================== // SkillDetailPanel Component // ======================================== // Right-side slide-out panel for viewing skill details import { useEffect, useState, useMemo, useCallback } from 'react'; import { useIntl } from 'react-intl'; import { X, FileText, Edit, Trash2, Folder, Lock, Tag, MapPin, Code, ChevronRight, ChevronDown, Eye, Loader2, Maximize2, } from 'lucide-react'; import { cn } from '@/lib/utils'; import { Button } from '@/components/ui/Button'; import { Badge } from '@/components/ui/Badge'; import { Card } from '@/components/ui/Card'; import type { Skill } from '@/lib/api'; import { buildSkillFileTree, getDefaultExpandedPaths } from '@/utils/skill-files'; import type { FileSystemNode } from '@/types/file-explorer'; import { readSkillFile } from '@/lib/api'; import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore'; export interface SkillDetailPanelProps { skill: Skill | null; isOpen: boolean; onClose: () => void; onEdit?: (skill: Skill) => void; onDelete?: (skill: Skill) => void; onEditFile?: (skillName: string, fileName: string, location: 'project' | 'user') => void; isLoading?: boolean; } export function SkillDetailPanel({ skill, isOpen, onClose, onEdit, onDelete, onEditFile, isLoading = false, }: SkillDetailPanelProps) { const { formatMessage } = useIntl(); const projectPath = useWorkflowStore(selectProjectPath); const [cliMode] = useState<'claude' | 'codex'>('claude'); // Tree view state const [expandedPaths, setExpandedPaths] = useState>(new Set()); const [selectedFile, setSelectedFile] = useState(null); const [previewContent, setPreviewContent] = useState(null); const [isPreviewLoading, setIsPreviewLoading] = useState(false); const [showPreviewPanel, setShowPreviewPanel] = 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]); // Build file tree from supportingFiles const fileTree = useMemo(() => { if (!skill?.supportingFiles) return []; return buildSkillFileTree(skill.supportingFiles); }, [skill?.supportingFiles]); // Initialize expanded paths when skill changes useEffect(() => { if (fileTree.length > 0) { setExpandedPaths(getDefaultExpandedPaths(fileTree)); } }, [fileTree]); // Load file content for preview const loadFilePreview = useCallback(async (filePath: string) => { if (!skill) return; setIsPreviewLoading(true); setShowPreviewPanel(true); setSelectedFile(filePath); setPreviewContent(null); try { const data = await readSkillFile({ skillName: skill.folderName || skill.name, fileName: filePath, location: skill.location || 'project', projectPath: projectPath, cliType: cliMode, }); setPreviewContent(data.content); } catch (error) { console.error('Failed to load file preview:', error); setPreviewContent(`Error loading preview: ${error instanceof Error ? error.message : String(error)}`); } finally { setIsPreviewLoading(false); } }, [skill, projectPath, cliMode]); const handleClosePreview = () => { setShowPreviewPanel(false); }; // Close preview on panel close useEffect(() => { if (!isOpen) { handleClosePreview(); } }, [isOpen]); if (!isOpen || !skill) { return null; } const hasAllowedTools = skill.allowedTools && skill.allowedTools.length > 0; const hasSupportingFiles = skill.supportingFiles && skill.supportingFiles.length > 0; const folderName = skill.folderName || skill.name; const handleEditFile = (fileName: string) => { onEditFile?.(folderName, fileName, skill.location || 'project'); }; // Toggle directory expanded state const togglePath = (path: string) => { setExpandedPaths(prev => { const next = new Set(prev); if (next.has(path)) { next.delete(path); } else { next.add(path); } return next; }); }; return ( <> {/* Overlay */}
{/* Panel */}
{/* Header */}

{skill.name}

{skill.version && (

v{skill.version}

)}
{/* Content */}
{isLoading ? (
) : (
{/* Description */}

{formatMessage({ id: 'skills.card.description' })}

{skill.description || formatMessage({ id: 'skills.noDescription' })}

{/* Metadata */}

{formatMessage({ id: 'skills.metadata' })}

{formatMessage({ id: 'skills.location' })}

{skill.location === 'project' ? formatMessage({ id: 'skills.projectSkills' }) : formatMessage({ id: 'skills.userSkills' })}

{skill.version && ( {formatMessage({ id: 'skills.card.version' })}

v{skill.version}

)} {skill.author && ( {formatMessage({ id: 'skills.card.author' })}

{skill.author}

)} {skill.source && ( {formatMessage({ id: 'skills.card.source' })}

{formatMessage({ id: `skills.source.${skill.source}` })}

)}
{/* Triggers */} {skill.triggers && skill.triggers.length > 0 && (

{formatMessage({ id: 'skills.card.triggers' })}

{skill.triggers.map((trigger) => ( {trigger} ))}
)} {/* Allowed Tools */} {hasAllowedTools && (

{formatMessage({ id: 'skills.allowedTools' })}

{skill.allowedTools!.map((tool) => ( {tool} ))}
)} {/* Files */}

{formatMessage({ id: 'skills.files' })}

{/* SKILL.md (main file) */}
SKILL.md
{onEditFile && ( )}
{/* Supporting Files Tree */} {hasSupportingFiles && fileTree.length > 0 && (
)} {/* Empty state */} {hasSupportingFiles && fileTree.length === 0 && (
{formatMessage({ id: 'skills.files.empty' })}
)}
{/* File Preview Modal */} {/* Path */} {skill.path && (

{formatMessage({ id: 'skills.path' })}

{skill.path}
)}
)}
{/* Footer Actions */}
{onDelete && ( )}
{onEdit && ( )}
); } // ======================================== // SkillFileTree Component // ======================================== // Recursive tree view for skill files interface SkillFileTreeNodeProps { node: FileSystemNode; depth: number; expandedPaths: Set; onTogglePath: (path: string) => void; onEditFile: (fileName: string) => void; onPreviewFile: (fileName: string) => void; } function SkillFileTreeNode({ node, depth, expandedPaths, onTogglePath, onEditFile, onPreviewFile, }: SkillFileTreeNodeProps) { const isDirectory = node.type === 'directory'; const isExpanded = expandedPaths.has(node.path); const hasChildren = node.children && node.children.length > 0; const handleClick = (e: React.MouseEvent) => { e.stopPropagation(); if (isDirectory) { onTogglePath(node.path); } else { // Preview file on click onPreviewFile(node.path); } }; return (
{/* Chevron for directories */} {isDirectory && ( {hasChildren && ( isExpanded ? ( ) : ( ) )} )} {/* Icon */} {isDirectory ? ( ) : ( )} {/* Name */} {node.name} {/* Preview button for files */} {!isDirectory && (
)}
{/* Children */} {isDirectory && isExpanded && hasChildren && (
{node.children!.map((child) => ( ))}
)}
); } interface SkillFileTreeProps { nodes: FileSystemNode[]; expandedPaths: Set; onTogglePath: (path: string) => void; onEditFile: (fileName: string) => void; onPreviewFile: (fileName: string) => void; depth: number; } function SkillFileTree({ nodes, expandedPaths, onTogglePath, onEditFile, onPreviewFile, depth, }: SkillFileTreeProps) { return (
{nodes.map((node) => ( ))}
); } // ======================================== // File Preview Modal Component // ======================================== interface FilePreviewModalProps { fileName: string | null; content: string | null; isLoading: boolean; isOpen: boolean; onClose: () => void; } function FilePreviewModal({ fileName, content, isLoading, isOpen, onClose }: FilePreviewModalProps) { const { formatMessage } = useIntl(); if (!isOpen) return null; return ( <> {/* Overlay */}
{/* Modal */}
{/* Header */}
{fileName}
{/* Content */}
{isLoading ? (
) : content ? (
                  {content}
                
) : (
{formatMessage({ id: 'skills.files.preview.empty' })}
)}
{/* Footer */}
); } export default SkillDetailPanel;