feat: expose skills and $ autocomplete#92
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds support for autocompleting Codex skills using the $ prefix in the chat input. Skills are loaded from the ~/.codex/skills directory (including .system subdirectories) by reading SKILL.md files with YAML frontmatter containing skill names and descriptions. The autocomplete UI has been updated to better display multi-line descriptions, and recently used skills are tracked in localStorage for improved suggestion ordering.
Changes:
- Added backend API endpoint to list available skills from the filesystem
- Implemented frontend autocomplete for skills triggered by
$prefix - Updated autocomplete UI to support longer descriptions with line clamping
Reviewed changes
Copilot reviewed 15 out of 17 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
web/src/types/api.ts |
Added SkillSummary and SkillsResponse types |
web/src/router.tsx |
Integrated skill autocomplete alongside slash commands |
web/src/lib/recent-skills.ts |
New utility for tracking recently used skills in localStorage |
web/src/lib/query-keys.ts |
Added query key for skills data caching |
web/src/hooks/queries/useSkills.ts |
New hook for fetching and filtering skill suggestions |
web/src/components/ChatInput/Autocomplete.tsx |
Updated layout to display descriptions on separate lines with clamping |
web/src/components/AssistantChat/HappyComposer.tsx |
Added $ to autocomplete prefixes and skill usage tracking |
web/src/api/client.ts |
Added getSkills API method |
server/src/web/routes/sessions.ts |
Added GET /sessions/:id/skills endpoint |
server/src/sync/syncEngine.ts |
Added listSkills method |
server/src/sync/rpcGateway.ts |
Added RPC gateway method for listing skills |
cli/src/modules/common/skills.ts |
Core logic for reading skills from filesystem with frontmatter parsing |
cli/src/modules/common/skills.test.ts |
Comprehensive tests for skill listing functionality |
cli/src/modules/common/handlers/skills.ts |
RPC handler for listSkills requests |
cli/src/modules/common/registerCommonHandlers.ts |
Registered skills handler |
bun.lock |
Added Windows x64 binary dependency |
.gitignore |
Added cli/npm/main/ to ignore list |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| <span className="w-full font-medium">{suggestion.label}</span> | ||
| {suggestion.description && ( | ||
| <span className={`truncate text-xs ${ | ||
| <span className={`w-full min-h-[2.25rem] text-xs leading-snug line-clamp-2 ${ |
There was a problem hiding this comment.
The min-h-[2.25rem] class sets a minimum height of 2.25rem (36px) for description text with line-clamp-2. This creates excessive whitespace when descriptions are short (single line), as the element will still occupy 36px minimum. Consider removing min-h-[2.25rem] to allow the description to naturally size based on its content, or use a smaller minimum height if needed.
| <span className={`w-full min-h-[2.25rem] text-xs leading-snug line-clamp-2 ${ | |
| <span className={`w-full text-xs leading-snug line-clamp-2 ${ |
| const getSuggestions = useCallback(async (queryText: string): Promise<Suggestion[]> => { | ||
| const recent = getRecentSkills() | ||
| const getRecency = (name: string) => recent[name] ?? 0 | ||
| const searchTerm = queryText.startsWith('$') | ||
| ? queryText.slice(1).toLowerCase() | ||
| : queryText.toLowerCase() | ||
|
|
||
| if (!searchTerm) { | ||
| return [...skills] | ||
| .sort((a, b) => getRecency(b.name) - getRecency(a.name) || a.name.localeCompare(b.name)) | ||
| .map((skill) => ({ | ||
| key: `$${skill.name}`, | ||
| text: `$${skill.name}`, | ||
| label: `$${skill.name}`, | ||
| description: skill.description, | ||
| source: 'builtin' | ||
| })) | ||
| } | ||
|
|
||
| const maxDistance = Math.max(2, Math.floor(searchTerm.length / 2)) | ||
| return skills | ||
| .map(skill => { | ||
| const name = skill.name.toLowerCase() | ||
| let score: number | ||
| if (name === searchTerm) score = 0 | ||
| else if (name.startsWith(searchTerm)) score = 1 | ||
| else if (name.includes(searchTerm)) score = 2 | ||
| else { | ||
| const dist = levenshteinDistance(searchTerm, name) | ||
| score = dist <= maxDistance ? 3 + dist : Infinity | ||
| } | ||
| return { skill, score, recency: getRecency(skill.name) } | ||
| }) | ||
| .filter(item => item.score < Infinity) | ||
| .sort((a, b) => a.score - b.score || b.recency - a.recency || a.skill.name.localeCompare(b.skill.name)) | ||
| .map(({ skill }) => ({ | ||
| key: `$${skill.name}`, | ||
| text: `$${skill.name}`, | ||
| label: `$${skill.name}`, | ||
| description: skill.description, | ||
| source: 'builtin' | ||
| })) | ||
| }, [skills]) |
There was a problem hiding this comment.
The getSuggestions callback uses the skills array but doesn't include it in the useCallback dependency array. This means the callback will continue to use a stale version of the skills array from when the callback was first created. Add skills to the dependency array to ensure the callback always uses the latest skills data.
增加codex 支持 $ 显示技能