mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-07-02 18:43:08 +08:00
feat(skills): add provider skill management
Users need one settings surface to discover and install skills without manually navigating provider-specific directories. Add provider-backed global skill installation for Claude, Codex, Gemini, and Cursor, while keeping OpenCode read-only because it reuses other providers' skill locations. Add a responsive Skills settings tab with scoped discovery, search, refresh controls, markdown and folder uploads, upload feedback, and overflow-safe layouts. Validate bundled skill files and paths before writing them, preserve scripts and assets, and cover provider discovery and installation behavior with tests.
This commit is contained in:
@@ -5,7 +5,7 @@ import type { ProviderAuthStatus } from '../../provider-auth/types';
|
||||
|
||||
export type SettingsMainTab = 'agents' | 'appearance' | 'git' | 'api' | 'tasks' | 'browser' | 'notifications' | 'plugins' | 'about';
|
||||
export type AgentProvider = LLMProvider;
|
||||
export type AgentCategory = 'account' | 'permissions' | 'mcp';
|
||||
export type AgentCategory = 'account' | 'permissions' | 'mcp' | 'skills';
|
||||
export type ProjectSortOrder = 'name' | 'date';
|
||||
export type SaveStatus = 'success' | 'error' | null;
|
||||
export type CodexPermissionMode = 'default' | 'acceptEdits' | 'bypassPermissions';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { X } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import ProviderLoginModal from '../../provider-auth/view/ProviderLoginModal';
|
||||
import { Button } from '../../../shared/view/ui';
|
||||
import SettingsSidebar from '../view/SettingsSidebar';
|
||||
@@ -101,12 +102,12 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }: Set
|
||||
</div>
|
||||
|
||||
{/* Body: sidebar + content */}
|
||||
<div className="flex min-h-0 flex-1 flex-col md:flex-row">
|
||||
<div className="flex min-h-0 min-w-0 flex-1 flex-col md:flex-row">
|
||||
<SettingsSidebar activeTab={activeTab} onChange={setActiveTab} />
|
||||
|
||||
{/* Content */}
|
||||
<main className="flex-1 overflow-y-auto">
|
||||
<div key={activeTab} className="settings-content-enter space-y-6 p-4 pb-safe-area-inset-bottom md:space-y-8 md:p-6">
|
||||
<main className="min-w-0 flex-1 overflow-y-auto overflow-x-hidden">
|
||||
<div key={activeTab} className="settings-content-enter min-w-0 space-y-6 overflow-x-hidden p-4 pb-safe-area-inset-bottom md:space-y-8 md:p-6">
|
||||
{activeTab === 'appearance' && (
|
||||
<AppearanceSettingsTab
|
||||
projectSortOrder={projectSortOrder}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import type { AgentCategory, AgentProvider } from '../../../types/types';
|
||||
|
||||
@@ -22,6 +22,11 @@ export default function AgentsSettingsTab({
|
||||
}: AgentsSettingsTabProps) {
|
||||
const [selectedAgent, setSelectedAgent] = useState<AgentProvider>('claude');
|
||||
const [selectedCategory, setSelectedCategory] = useState<AgentCategory>('account');
|
||||
const visibleCategories = useMemo<AgentCategory[]>(() => (
|
||||
selectedAgent === 'opencode'
|
||||
? ['account', 'permissions', 'mcp']
|
||||
: ['account', 'permissions', 'mcp', 'skills']
|
||||
), [selectedAgent]);
|
||||
|
||||
const visibleAgents = useMemo<AgentProvider[]>(() => {
|
||||
return ['claude', 'cursor', 'codex', 'gemini', 'opencode'];
|
||||
@@ -57,8 +62,14 @@ export default function AgentsSettingsTab({
|
||||
providerAuthStatus.opencode,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visibleCategories.includes(selectedCategory)) {
|
||||
setSelectedCategory(visibleCategories[0] ?? 'account');
|
||||
}
|
||||
}, [selectedCategory, visibleCategories]);
|
||||
|
||||
return (
|
||||
<div className="-mx-4 -mb-4 -mt-2 flex min-h-[300px] flex-col overflow-hidden md:-mx-6 md:-mb-6 md:-mt-2 md:min-h-[500px]">
|
||||
<div className="-mx-4 -mb-4 -mt-2 flex min-h-[300px] min-w-0 flex-col overflow-hidden md:-mx-6 md:-mb-6 md:-mt-2 md:min-h-[500px]">
|
||||
<AgentSelectorSection
|
||||
agents={visibleAgents}
|
||||
selectedAgent={selectedAgent}
|
||||
@@ -66,8 +77,10 @@ export default function AgentsSettingsTab({
|
||||
agentContextById={agentContextById}
|
||||
/>
|
||||
|
||||
<div className="flex flex-1 flex-col overflow-hidden">
|
||||
<div className="flex min-w-0 flex-1 flex-col overflow-hidden">
|
||||
<AgentCategoryTabsSection
|
||||
categories={visibleCategories}
|
||||
selectedAgent={selectedAgent}
|
||||
selectedCategory={selectedCategory}
|
||||
onSelectCategory={setSelectedCategory}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { AgentCategoryContentSectionProps } from '../types';
|
||||
import type { McpProject } from '../../../../../mcp/types';
|
||||
import { McpServers } from '../../../../../mcp';
|
||||
import type { SkillsProject } from '../../../../../skills/types';
|
||||
import { ProviderSkills } from '../../../../../skills';
|
||||
|
||||
import AccountContent from './content/AccountContent';
|
||||
import PermissionsContent from './content/PermissionsContent';
|
||||
@@ -18,7 +20,7 @@ export default function AgentCategoryContentSection({
|
||||
projects,
|
||||
}: AgentCategoryContentSectionProps) {
|
||||
return (
|
||||
<div className="flex-1 overflow-y-auto p-3 md:p-4">
|
||||
<div className="min-w-0 flex-1 overflow-y-auto overflow-x-hidden p-3 md:p-4">
|
||||
{selectedCategory === 'account' && (
|
||||
<AccountContent
|
||||
agent={selectedAgent}
|
||||
@@ -84,6 +86,18 @@ export default function AgentCategoryContentSection({
|
||||
}))}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedCategory === 'skills' && selectedAgent !== 'opencode' && (
|
||||
<ProviderSkills
|
||||
selectedProvider={selectedAgent}
|
||||
currentProjects={projects.map<SkillsProject>((project) => ({
|
||||
projectId: project.name,
|
||||
displayName: project.displayName,
|
||||
fullPath: project.fullPath,
|
||||
path: project.path,
|
||||
}))}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { cn } from '../../../../../../lib/utils';
|
||||
import type { AgentCategory } from '../../../../types/types';
|
||||
import type { AgentCategoryTabsSectionProps } from '../types';
|
||||
|
||||
const AGENT_CATEGORIES: AgentCategory[] = ['account', 'permissions', 'mcp'];
|
||||
|
||||
export default function AgentCategoryTabsSection({
|
||||
categories,
|
||||
selectedAgent,
|
||||
selectedCategory,
|
||||
onSelectCategory,
|
||||
}: AgentCategoryTabsSectionProps) {
|
||||
@@ -14,7 +14,7 @@ export default function AgentCategoryTabsSection({
|
||||
return (
|
||||
<div className="flex-shrink-0 border-b border-border">
|
||||
<div role="tablist" className="flex overflow-x-auto px-2 md:px-4">
|
||||
{AGENT_CATEGORIES.map((category) => (
|
||||
{categories.map((category) => (
|
||||
<button
|
||||
key={category}
|
||||
role="tab"
|
||||
@@ -30,6 +30,9 @@ export default function AgentCategoryTabsSection({
|
||||
{category === 'account' && t('tabs.account')}
|
||||
{category === 'permissions' && t('tabs.permissions')}
|
||||
{category === 'mcp' && t('tabs.mcpServers')}
|
||||
{category === 'skills' && t('tabs.skills', {
|
||||
defaultValue: selectedAgent === 'opencode' ? 'Shared Skills' : 'Skills',
|
||||
})}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -32,6 +32,8 @@ export type AgentsSettingsTabProps = {
|
||||
};
|
||||
|
||||
export type AgentCategoryTabsSectionProps = {
|
||||
categories: AgentCategory[];
|
||||
selectedAgent: AgentProvider;
|
||||
selectedCategory: AgentCategory;
|
||||
onSelectCategory: (category: AgentCategory) => void;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user