Shows a list of your slash commands and skills in A to Z order as soon as you type / in the prompt, and narrows it as you keep typing.

Shows a list of your slash commands and skills in A to Z order as soon as you type / in the prompt, and narrows it as you keep typing.
You have installed a lot of plugins and cannot remember what a command is called. Type / and the list is right there. Type /brain and it shrinks to everything with "brain" in its name, such as superpowers:brainstorming. Names that start with what you typed come first.
claude plugin marketplace add varunmoka7/skills-az
claude plugin install skills-az@skills-az
Needs Claude Code 2.1.287 or newer with mods enabled. Tested in the terminal on macOS. It has not been tried in the desktop app or on Windows.
/ at the start of an empty prompt. The list opens by itself./word, or when you press Enter./skills-az opens the list by hand. Use it if your terminal was too narrow for the automatic one.claude plugin disable skills-az
claude plugin uninstall skills-az
/skills-az still works./ also opens the list, with no matches.MIT.
hooks/register.tsx 98 lines1import { atom, read, update } from 'claude-code'
2import type { CommandInfo, EngineInterface, Register } from 'claude-code'
3
4import type { SlashFilter } from '../types'
5
6const PANE = 'skills-az'
7const TITLE = 'Skills A-Z'
8// null = prompt is not a slash command; '' = just "/" typed
9const filter = atom({ plugin: 'skills-az', key: 'filter' } as const, null as SlashFilter)
10
11export const slashFilter = (text: string): string | null =>
12 /^\/\S*$/.test(text) ? text.slice(1).toLowerCase() : null
13
14// Substring match so "/brain" finds "superpowers:brainstorming"; prefix hits sort first.
15export const matches = (all: readonly CommandInfo[], f: string): CommandInfo[] =>
16 all
17 .map(c => ({ c, at: c.name.toLowerCase().indexOf(f) }))
18 .filter(m => m.at >= 0)
19 .sort(
20 (a, b) =>
21 Number(a.at > 0) - Number(b.at > 0) ||
22 a.c.name.localeCompare(b.c.name, undefined, { sensitivity: 'base' }),
23 )
24 .map(m => m.c)
25
26let hinted = false
27// The command list, read when the pane first draws and dropped when it closes.
28let listed: CommandInfo[] | null = null
29
30// Open on a fresh "/", close when the prompt stops being a slash command. Driven by the
31// filter's transitions in $.state, so a hand-closed pane reopens next time and a reload
32// does not leave it stuck.
33async function sync($: EngineInterface, text: string) {
34 const f = slashFilter(text)
35 // Check and write in one update, so two quick edits cannot both see the old value.
36 let prev: SlashFilter = f
37 await update($, filter, p => ((prev = p), f))
38 if (f === prev) return
39 if (f !== null && prev === null) {
40 const opened = await $.ui.open({ id: PANE, title: TITLE })
41 if (!opened.isPlaced && !hinted) {
42 hinted = true
43 $.ui.toast('skills-az: terminal too narrow for the pane; run /skills-az once or widen it')
44 }
45 } else if (f === null) {
46 await $.ui.close({ id: PANE })
47 listed = null
48 }
49}
50
51export const register: Register = on => {
52 on('session.start', async ($, e, next) => {
53 await $.command.register({ name: 'skills-az', description: 'Open the Skills A-Z pane' })
54 return next(e)
55 })
56
57 // A slash command never reaches prompt.submit and the box clearing is no prompt.edit,
58 // so close here. Opened at the person's request, the pane seats at any width.
59 on('command.run', async ($, e, next) => {
60 await sync($, '').catch(() => {})
61 if (e.command !== 'skills-az') return next(e)
62 await $.ui.open({ id: PANE, title: TITLE })
63 return { text: 'Skills A-Z pane opened.' }
64 })
65
66 on('prompt.edit', async ($, e, next) => {
67 const box = await next(e)
68 await sync($, box.text).catch(() => {})
69 return box
70 })
71
72 on('prompt.submit', async ($, e, next) => {
73 // Awaited so the close lands before /skills-az's own open.
74 await sync($, '').catch(() => {})
75 return next(e)
76 })
77
78 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
79 const { Box, Text } = $.ui.resolve(e)
80 const f = (await read($, filter)) ?? ''
81 const all = (listed ??= await $.command.list())
82 const rows = matches(all, f)
83 const room = Math.max(1, (e.viewport?.rows ?? 24) - 3)
84
85 return (
86 <Box flexDirection="column">
87 <Text dimColor>{rows.length} of {all.length} match /{f}</Text>
88 {rows.slice(0, room).map(c => (
89 <Text key={c.name} wrap="truncate-end">
90 <Text bold>/{c.name}</Text> <Text dimColor>{c.description}</Text>
91 </Text>
92 ))}
93 {rows.length > room && <Text dimColor>… {rows.length - room} more, keep typing</Text>}
94 </Box>
95 )
96 })
97}
98types/index.d.ts 8 lines1export type SlashFilter = string | null
2
3declare module 'claude-code' {
4 interface PluginState {
5 'skills-az': { filter: SlashFilter }
6 }
7}
8