8.9 KiB
Skills UI Feature Design
Overview
Port the CLI /skills listing to the Desktop Web APP, adding a full Skills browser with file tree navigation and content preview in the Settings page.
Architecture
Desktop UI (React) Server (Bun.serve) Filesystem
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ Settings Page │ │ GET /api/skills │ │ ~/.claude/skills/│
│ └─ Skills Tab │──────▶│ → list meta │──────▶│ ./.claude/skills/│
│ ├─ SkillList │ │ │ │ plugins cache/ │
│ └─ SkillDetail │ GET /api/skills/ │ │ bundled skills/ │
│ ├─ FileTree│──────▶│ detail?s=&n= │──────▶│ │
│ └─ Viewer │ │ → tree + files │ │ │
└──────────────────┘ └──────────────────┘ └──────────────────┘
Data Models
SkillMeta (list item)
type SkillSource = 'userSettings' | 'projectSettings' | 'policySettings'
| 'plugin' | 'mcp' | 'bundled'
type SkillMeta = {
name: string // unique name (plugin format: "plugin:skill")
displayName?: string // custom display name from frontmatter
description: string
source: SkillSource
userInvocable: boolean
version?: string
contentLength: number // char count for token estimation
pluginName?: string // plugin name when source is 'plugin'
hasDirectory: boolean // whether the skill has a browsable directory
}
FileTreeNode
type FileTreeNode = {
name: string // file/dir name
path: string // relative to skill root
type: 'file' | 'directory'
children?: FileTreeNode[] // present for directories
}
SkillFile
type SkillFile = {
path: string // relative path
content: string // file content
language: string // language id (md, ts, json, yaml, sh, etc.)
}
SkillDetail (detail view)
type SkillDetail = {
meta: SkillMeta
tree: FileTreeNode[] // directory tree
files: SkillFile[] // all file contents, loaded at once
skillRoot: string // absolute path (display only)
}
Server API
GET /api/skills
Returns metadata list of all installed skills.
Response:
{
"skills": [
{
"name": "my-skill",
"displayName": "My Skill",
"description": "Does something useful",
"source": "userSettings",
"userInvocable": true,
"contentLength": 1234,
"hasDirectory": true
}
]
}
Implementation: Reuse loadAllCommands() from src/commands.ts, filter for type === 'prompt' commands with skill-related loadedFrom values. Extract metadata without loading full content.
GET /api/skills/detail?source={source}&name={name}
Returns full skill data including file tree and all file contents.
Response:
{
"detail": {
"meta": { ... },
"tree": [
{ "name": "SKILL.md", "path": "SKILL.md", "type": "file" },
{
"name": "examples",
"path": "examples",
"type": "directory",
"children": [
{ "name": "demo.ts", "path": "examples/demo.ts", "type": "file" }
]
}
],
"files": [
{ "path": "SKILL.md", "content": "---\nname: ...\n---\n# ...", "language": "md" },
{ "path": "examples/demo.ts", "content": "...", "language": "ts" }
],
"skillRoot": "/Users/x/.claude/skills/my-skill"
}
}
Implementation:
- Find matching skill by source + name from loaded commands
- Get
skillRootfrom the command object - Recursively walk the directory, skip
node_modules,.git, hidden dirs - Read all files (max 100KB per file, max 50 files total as safety limit)
- Detect language from file extension
- For MCP skills (no directory): return empty tree, single virtual file with the skill's prompt content
File: src/server/api/skills.ts
New file following the pattern of src/server/api/status.ts.
Router registration: src/server/router.ts
Add case 'skills': return handleSkillsApi(req, url, segments).
Desktop UI
Settings Tab Addition
Add a 5th tab "Skills" (icon: auto_awesome) to desktop/src/pages/Settings.tsx.
type SettingsTab = 'providers' | 'permissions' | 'general' | 'adapters' | 'skills'
Skill List View (SkillList.tsx)
- Grouped by source (User, Project, Plugin, MCP, Bundled)
- Each group: collapsible header with count
- Each item: name, description (truncated), source badge
- Click → navigate to detail view
- Loading state + empty state
Skill Detail View (SkillDetail.tsx)
Two-panel layout:
- Left panel (w-[220px]): File tree with expand/collapse
- Right panel (flex-1): File content viewer
- Header: Back button, skill name, description, source badge, token estimate
- Default selected file: SKILL.md
File Tree Component (FileTree.tsx)
- Recursive tree with indent
- Directory expand/collapse (default: all expanded)
- File icons based on extension
- Click file → show in right panel
- Active file highlight
File Viewer (FileViewer.tsx)
- For
.mdfiles: Use existingMarkdownRenderer - For code files: Use existing
CodeViewerwith language detection - File path header with copy button
- Scrollable content area
Language Detection
Map file extension to language:
const LANG_MAP: Record<string, string> = {
md: 'markdown', ts: 'typescript', tsx: 'typescript',
js: 'javascript', jsx: 'javascript', json: 'json',
yaml: 'yaml', yml: 'yaml', sh: 'bash', bash: 'bash',
py: 'python', toml: 'toml', css: 'css', html: 'html',
}
State Management
desktop/src/stores/skillStore.ts
type SkillStoreState = {
skills: SkillMeta[]
selectedSkill: SkillDetail | null
isLoading: boolean
isDetailLoading: boolean
error: string | null
fetchSkills: () => Promise<void>
fetchSkillDetail: (source: string, name: string) => Promise<void>
clearSelection: () => void
}
desktop/src/api/skills.ts
export const skillsApi = {
list: () => api.get<{ skills: SkillMeta[] }>('/api/skills'),
detail: (source: string, name: string) =>
api.get<{ detail: SkillDetail }>(`/api/skills/detail?source=${encodeURIComponent(source)}&name=${encodeURIComponent(name)}`),
}
desktop/src/types/skill.ts
All type definitions from Data Models section.
i18n
Add to both en.ts and zh.ts:
'settings.tab.skills': 'Skills' / '技能',
'settings.skills.title': 'Installed Skills' / '已安装技能',
'settings.skills.description': 'Skills extend Claude...' / '技能扩展 Claude...',
'settings.skills.empty': 'No skills installed' / '暂无已安装技能',
'settings.skills.back': 'Back to list' / '返回列表',
'settings.skills.tokens': '~{count} tokens',
'settings.skills.files': '{count} files' / '{count} 个文件',
'settings.skills.source.userSettings': 'User' / '用户',
'settings.skills.source.projectSettings': 'Project' / '项目',
'settings.skills.source.plugin': 'Plugin' / '插件',
'settings.skills.source.mcp': 'MCP',
'settings.skills.source.bundled': 'Built-in' / '内置',
Files to Create/Modify
New files (7):
src/server/api/skills.ts— Server API handlerdesktop/src/types/skill.ts— Type definitionsdesktop/src/api/skills.ts— API clientdesktop/src/stores/skillStore.ts— Zustand storedesktop/src/components/skills/SkillList.tsx— Skill list componentdesktop/src/components/skills/SkillDetail.tsx— Detail view (FileTree + FileViewer inline)
Modified files (4):
src/server/router.ts— Add skills routedesktop/src/pages/Settings.tsx— Add Skills tabdesktop/src/i18n/locales/en.ts— English translationsdesktop/src/i18n/locales/zh.ts— Chinese translations
Design Decisions
- One detail request loads all files — Single skill directories are small (typically <50KB total), so loading all at once avoids per-file request overhead.
- Reuse existing components —
MarkdownRendererfor .md files,CodeViewerfor code. No new rendering dependencies. - source + name as identifier — Skills can have duplicate names across sources, so both are needed.
- Server-side skill loading — Reuse
loadAllCommands()which already handles all 6 sources with deduplication and caching. - Safety limits — Max 50 files, 100KB per file to prevent issues with massive plugin directories.
- FileTree + FileViewer inline in SkillDetail — No need for separate components given the simplicity; keeps code collocated.