Favourites: a ⚡ at the bottom-right of the text box opens a popup of icons for your chosen commands and skills; hover for names, search to add.

Stop typing /commands. Click a small ⚡ at the bottom-right of the text box and a popup shows your favourite commands and skills as icons. One click runs it.
/chat) runs it. A click on a skill puts it in your text box (/find-skills ▌), so you can add what you want and press Enter./fav opens the popup too. Esc closes it.A favourite only shows in projects where its command exists, so one list works everywhere.
You need Claude Code 2.1.287 or newer (that's when mods arrived).
From GitHub, at the prompt of a terminal session:
/plugin install quick-bar --marketplace dricosnyman/claude-favourites
Answer y to add the marketplace, then pick the user scope so it loads in every project.
From a folder (a copy on a USB stick or OneDrive), in a normal terminal:
claude plugin marketplace add "C:\path\to\quick-bar"
claude plugin install quick-bar@quick-bar --scope user
Start a new session (or run /reload-plugins) and the ⚡ appears.
A mod runs with your permissions, so here's exactly what this one does:
Check for yourself with claude plugin validate <this folder>, which lists every event it hooks and every call it makes.
Disable quick-bar in your plugin settings, or remove it:
claude plugin uninstall quick-bar@quick-bar
The Claude Code desktop app (Code tab) and the terminal, on Windows, Mac and Linux. In the VS Code extension a mod can't draw, so the ⚡ won't show there.
MIT. See LICENSE.
Made by Drico, built together with Claude Code.
hooks/register.tsx 345 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { ClickAction, Favourite, Kind, Mode } from '../types'
5import { actionFor, ICON_GROUPS, kindOf, loadFavourites, search, setIcon, setKind } from './labels'
6
7const POPUP = 'quick-bar-popup'
8const SEARCH_SHOWN = 25
9
10// The favourites live in the store across sessions; this is the session's copy the drawings read.
11const favourites = atom({ plugin: 'quick-bar', key: 'favourites' } as const, [] as Favourite[])
12const mode = atom({ plugin: 'quick-bar', key: 'mode' } as const, 'grid' as Mode)
13const query = atom({ plugin: 'quick-bar', key: 'query' } as const, '')
14const pending = atom({ plugin: 'quick-bar', key: 'pending' } as const, null as string | null)
15const showBuiltins = atom({ plugin: 'quick-bar', key: 'showBuiltins' } as const, false)
16const immediates = atom({ plugin: 'quick-bar', key: 'immediates' } as const, [] as string[])
17const tab = atom({ plugin: 'quick-bar', key: 'tab' } as const, 'plugin' as Kind)
18
19async function setFavourites($: EngineInterface, change: (list: Favourite[]) => Favourite[]) {
20 const next = await update($, favourites, change)
21 await $.store.set('favourites', next)
22}
23
24async function setTab($: EngineInterface, to: Kind) {
25 await update($, tab, (): Kind => to)
26 await $.store.set('tab', to)
27}
28
29async function goTo($: EngineInterface, to: Mode) {
30 await update($, mode, (): Mode => to)
31 if (to === 'add') await update($, query, () => '')
32}
33
34// Opens the popup as a dialog: it takes the keys, Esc closes it, popups wait behind it.
35async function openPopup($: EngineInterface) {
36 await goTo($, 'grid')
37 await $.ui.open({ id: POPUP, title: 'Favourites', focus: true, closeOnEscape: true, holdToasts: true })
38}
39
40// Closes the popup, then either puts the command in the text box (to add an instruction) or runs it at once.
41async function useFavourite($: EngineInterface, command: string, action: ClickAction) {
42 await $.ui.close({ id: POPUP })
43 if (action === 'fill') {
44 const filled = await $.prompt.fill({ text: `/${command} ` })
45 if (!filled.isFilled) $.ui.toast(`Type /${command} to use it here.`)
46 return
47 }
48 try {
49 const result = await $.command.run({ command })
50 const text = result.text?.trim()
51 if (text) $.ui.toast(text.length > 160 ? `${text.slice(0, 157)}...` : text)
52 } catch {
53 $.ui.toast(`Could not run /${command} from favourites. Try typing it instead.`)
54 }
55}
56
57export const register: Register = on => {
58 on('session.start', async ($, e, next) => {
59 const saved = loadFavourites(await $.store.get('favourites'))
60 await update($, favourites, () => saved)
61 const savedTab = await $.store.get('tab')
62 await update($, tab, (): Kind => (savedTab === 'skill' ? 'skill' : 'plugin'))
63 await $.command.register({ name: 'fav', description: 'Open your favourites: one-click commands and skills', immediate: true })
64 return next(e)
65 })
66
67 // Claude Code says whether each command runs at once mid-turn when it describes it; remember the ones that do.
68 on('command.describe', async ($, e, next) => {
69 if (e.immediate) {
70 const name = e.command
71 if (!(await read($, immediates)).includes(name)) await update($, immediates, l => [...l, name])
72 }
73 return next(e)
74 })
75
76 on('command.run', { command: 'fav' }, async $ => {
77 await openPopup($)
78 return { text: 'Favourites opened.' }
79 })
80
81 // In the mode-label slot at the prompt footer's right: a small ⚡ beside any labels Claude shows there.
82 on('ui.render', { component: 'SessionMode' }, async ($, e) => {
83 const { Box, Text, Button } = $.ui.resolve(e)
84 return (
85 <Box flexDirection="row" gap={1} alignItems="center">
86 {e.props.modes.length > 0 ? <Text dimColor>{e.props.modes.join(' & ')}</Text> : null}
87 <Button key="fav-open" plain label="⚡" onPress={() => openPopup($)} />
88 </Box>
89 )
90 })
91
92 on('ui.render', { component: 'Pane', requestId: POPUP }, async ($, e) => {
93 const t = $.ui.resolve(e)
94 const { Box, Text, Button } = t
95 const Input = 'Input' in t ? t.Input : null
96 const all = await $.command.list()
97 const byName = new Map(all.map(c => [c.name, c]))
98 const favs = await read($, favourites)
99 const view = await read($, mode)
100 const quick = await read($, immediates)
101 // Which tab a favourite lives in, and so what a click does.
102 const kindOfFav = (f: Favourite): Kind => kindOf(f, byName.get(f.command), quick.includes(f.command))
103 const actionOf = (f: Favourite): ClickAction => actionFor(kindOfFav(f))
104
105 // ---- The icons: hover for the name, click to run ----
106 if (view === 'grid') {
107 const open = await read($, tab)
108 const here = favs.filter(f => byName.has(f.command) && kindOfFav(f) === open)
109 const tabs: [Kind, string][] = [
110 ['plugin', '🧩 Plugins'],
111 ['skill', '🎓 Skills'],
112 ]
113 return (
114 <Box flexDirection="column" gap={1} paddingX={1}>
115 <Box flexDirection="row" gap={2} alignItems="center" flexWrap="wrap">
116 <Text bold color="claude">
117 ⚡
118 </Text>
119 {tabs.map(([kind, label]) => (
120 <Button
121 key={`tab-${kind}`}
122 variant={kind === open ? 'primary' : undefined}
123 plain={kind === open ? undefined : true}
124 dimColor={kind === open ? undefined : true}
125 label={label}
126 onPress={() => setTab($, kind)}
127 />
128 ))}
129 <Button key="go-add" plain dimColor label="+ Add" onPress={() => goTo($, 'add')} />
130 {favs.length > 0 ? <Button key="go-edit" plain dimColor label="✎ Edit" onPress={() => goTo($, 'edit')} /> : null}
131 </Box>
132 {here.length === 0 ? (
133 <Text dimColor>{open === 'skill' ? 'No skills here yet. Press + Add.' : 'No plugins here yet. Press + Add.'}</Text>
134 ) : null}
135 <Box flexDirection="row" flexWrap="wrap" gap={2}>
136 {here.map(f => (
137 <Box key={`fav-${f.command}`} flexDirection="column">
138 <Button key={`run-${f.command}`} plain label={f.icon} onPress={() => useFavourite($, f.command, actionOf(f))} />
139 <Box
140 position="absolute"
141 top={1}
142 left={0}
143 display="none"
144 hover={{ display: 'flex' }}
145 borderStyle="bold"
146 borderColor="claude"
147 paddingX={1}
148 >
149 <Text bold inverse color="claude" wrap="truncate-end">
150 {` /${f.command} `}
151 </Text>
152 </Box>
153 </Box>
154 ))}
155 </Box>
156 {/* Empty room under the icons, so the hover label has space to appear and covers nothing. */}
157 <Box height={3} />
158 </Box>
159 )
160 }
161
162 // ---- Add: search every command and skill, click one to pick its icon ----
163 if (view === 'add') {
164 const q = await read($, query)
165 const builtins = await read($, showBuiltins)
166 const pool = all.filter(c => c.name !== 'fav' && (builtins || c.source !== 'builtin'))
167 const found = search(pool, q)
168 return (
169 <Box flexDirection="column" gap={1} paddingX={1}>
170 <Box flexDirection="row" gap={2} alignItems="center">
171 <Text bold color="claude">
172 + Add a favourite
173 </Text>
174 <Button key="back" plain dimColor label="← Back" onPress={() => goTo($, 'grid')} />
175 </Box>
176 {Input ? (
177 <Input
178 key="fav-search"
179 placeholder="Search commands and skills"
180 autoFocus
181 onInput={(value: string) => update($, query, () => value)}
182 onSubmit={async () => {
183 const first = found[0]
184 if (!first) return
185 await update($, pending, () => first.name)
186 await goTo($, 'icon')
187 }}
188 />
189 ) : null}
190 <Box flexDirection="row">
191 <Button
192 key="toggle-builtins"
193 plain
194 dimColor
195 label={builtins ? '☑ Include Claude Code commands' : '☐ Include Claude Code commands'}
196 onPress={() => update($, showBuiltins, v => !v)}
197 />
198 </Box>
199 {found.length === 0 ? <Text dimColor>Nothing matches "{q}".</Text> : null}
200 <Box flexDirection="column">
201 {found.slice(0, SEARCH_SHOWN).map(c => {
202 const already = favs.find(f => f.command === c.name)
203 return (
204 <Box key={`found-${c.name}`} flexDirection="column">
205 <Button
206 key={`pick-${c.name}`}
207 plain
208 label={`${already ? `${already.icon} ` : ''}/${c.name}`}
209 onPress={async () => {
210 await update($, pending, () => c.name)
211 await goTo($, 'icon')
212 }}
213 />
214 <Text dimColor wrap="truncate-end">
215 {' '}
216 {c.description}
217 </Text>
218 </Box>
219 )
220 })}
221 </Box>
222 {found.length > SEARCH_SHOWN ? <Text dimColor>{found.length - SEARCH_SHOWN} more. Keep typing to narrow it down.</Text> : null}
223 </Box>
224 )
225 }
226
227 // ---- Icon: pick the emoji for the chosen command ----
228 if (view === 'icon') {
229 const command = await read($, pending)
230 const current = favs.find(f => f.command === command)?.icon
231 return (
232 <Box flexDirection="column" gap={1} paddingX={1}>
233 <Box flexDirection="row" gap={2} alignItems="center">
234 <Text bold color="claude">
235 Pick an icon for /{command}
236 </Text>
237 <Button key="back" plain dimColor label="← Back" onPress={() => goTo($, current ? 'edit' : 'add')} />
238 </Box>
239 {ICON_GROUPS.map(group => (
240 <Box key={`group-${group.area}`} flexDirection="column">
241 <Text dimColor>{group.area}</Text>
242 <Box flexDirection="row" flexWrap="wrap" gap={2}>
243 {group.icons.map(icon => (
244 <Button
245 key={`icon-${icon}`}
246 plain
247 label={icon === current ? `[${icon}]` : icon}
248 onPress={async () => {
249 if (!command) return
250 const isNew = !favs.some(f => f.command === command)
251 await setFavourites($, list => setIcon(list, command, icon))
252 await goTo($, isNew ? 'kind' : 'grid')
253 }}
254 />
255 ))}
256 </Box>
257 </Box>
258 ))}
259 </Box>
260 )
261 }
262
263 // ---- Kind: a plugin (runs) or a skill (waits in the text box) ----
264 if (view === 'kind') {
265 const command = await read($, pending)
266 const fav = favs.find(f => f.command === command)
267 const guess = fav ? kindOfFav(fav) : 'plugin'
268 const choose = async (kind: Kind) => {
269 if (command) await setFavourites($, list => setKind(list, command, kind))
270 await setTab($, kind)
271 await goTo($, 'grid')
272 }
273 return (
274 <Box flexDirection="column" gap={1} paddingX={1}>
275 <Text bold color="claude">
276 {fav?.icon} Is /{command} a plugin or a skill?
277 </Text>
278 <Box flexDirection="column" gap={1}>
279 <Button
280 key="choose-plugin"
281 variant={guess === 'plugin' ? 'primary' : undefined}
282 label="🧩 Plugin: a click runs it at once"
283 onPress={() => choose('plugin')}
284 />
285 <Button
286 key="choose-skill"
287 variant={guess === 'skill' ? 'primary' : undefined}
288 label="🎓 Skill: a click puts it in my text box, for an instruction"
289 onPress={() => choose('skill')}
290 />
291 </Box>
292 <Text dimColor>The highlighted one is the best guess. You can move it later in ✎ Edit.</Text>
293 </Box>
294 )
295 }
296
297 // ---- Edit: change an icon, move between Plugins and Skills, or remove a favourite ----
298 return (
299 <Box flexDirection="column" gap={1} paddingX={1}>
300 <Box flexDirection="row" gap={2} alignItems="center">
301 <Text bold color="claude">
302 ✎ Edit favourites
303 </Text>
304 <Button key="back" plain dimColor label="← Done" onPress={() => goTo($, 'grid')} />
305 </Box>
306 <Box flexDirection="column">
307 {favs.map(f => (
308 <Box key={`edit-${f.command}`} flexDirection="row" gap={2} alignItems="center">
309 <Text>{f.icon}</Text>
310 <Text dimColor={!byName.has(f.command)}>
311 /{f.command}
312 {byName.has(f.command) ? '' : ' (not in this project)'}
313 </Text>
314 <Button
315 key={`change-${f.command}`}
316 plain
317 dimColor
318 label="Change icon"
319 onPress={async () => {
320 await update($, pending, () => f.command)
321 await goTo($, 'icon')
322 }}
323 />
324 <Button
325 key={`kind-${f.command}`}
326 plain
327 dimColor
328 label={kindOfFav(f) === 'skill' ? '🎓 Skill' : '🧩 Plugin'}
329 onPress={() => setFavourites($, list => setKind(list, f.command, kindOfFav(f) === 'skill' ? 'plugin' : 'skill'))}
330 />
331 <Button
332 key={`remove-${f.command}`}
333 plain
334 dimColor
335 label="✕ Remove"
336 onPress={() => setFavourites($, list => list.filter(x => x.command !== f.command))}
337 />
338 </Box>
339 ))}
340 </Box>
341 </Box>
342 )
343 })
344}
345hooks/labels.ts 78 lines1// Icons and search for the favourites popup. Pure: no `$`.
2
3import type { ClickAction, Favourite, Kind } from '../types'
4
5// The icons to pick from, grouped by the kind of skill they suit: emoji, so they draw in colour everywhere
6// with nothing to download. Single-codepoint emoji where possible, so every terminal and the desktop app draw them alike.
7export const ICON_GROUPS: { area: string; icons: string[] }[] = [
8 { area: 'Coding', icons: ['💻', '⌨️', '🖥️', '🖱️', '💾', '🛠️', '🔧', '⚙️', '🧰', '🐞', '🧱', '🌐', '🔗', '🗄️', '🔌', '📡', '📟', '🐙', '🧩', '📦'] },
9 { area: 'Writing & docs', icons: ['📝', '✏️', '✍️', '🖋️', '📄', '📑', '📋', '📜', '📖', '📚', '🗒️', '📰', '🗞️', '💭', '🔤'] },
10 { area: 'Data & maths', icons: ['📊', '📈', '📉', '💹', '🔢', '➗', '♾️', '🧮', '📐', '📏', '🗃️', '🧾', '💰'] },
11 { area: 'Talk & people', icons: ['💬', '📧', '📨', '💌', '📞', '📣', '🗣️', '👋', '🙋', '👥', '🤝'] },
12 { area: 'Design & media', icons: ['🎨', '🖌️', '🖍️', '🖼️', '📷', '🎬', '🎞️', '📺', '🎭', '🎙️', '🎤', '🎧', '🎵', '🎹', '🎸', '🎮'] },
13 { area: 'Learning & science', icons: ['🎓', '🏫', '🏛️', '🧠', '💡', '🔬', '🧪', '🧫', '🦠', '🧬', '⚛️', '🧲', '🌡️', '🔭', '⚗️', '🌱'] },
14 { area: 'Plans & time', icons: ['📅', '🗓️', '📆', '⏰', '⏳', '⌛', '🎯', '📌', '📍', '🗺️', '🧭', '📁', '🗂️', '💼', '🛒', '🏷️'] },
15 { area: 'Safety & upkeep', icons: ['🔒', '🔐', '🔑', '🛡️', '🚨', '⚠️', '🚦', '🩹', '🧹', '🔄', '🗜️', '✅'] },
16 { area: 'Fun & AI', icons: ['🤖', '✨', '🪄', '🔮', '👾', '⚡', '🚀', '🔥', '⭐', '🌈', '🎉', '🏆', '🎲', '🔍'] },
17 { area: 'Life & world', icons: ['🌍', '🏠', '❤️', '🍳', '☕', '⚽', '🌙'] },
18 // Keycap numbers are three characters each (digit, emoji style, keycap); the desktop app draws them as tiles.
19 { area: 'Numbers', icons: ['0️⃣', '1️⃣', '2️⃣', '3️⃣', '4️⃣', '5️⃣', '6️⃣', '7️⃣', '8️⃣', '9️⃣'] },
20]
21
22export const ICONS = ICON_GROUPS.flatMap(g => g.icons)
23
24// A new user starts with no favourites and adds their own.
25export const STARTERS: Favourite[] = []
26
27// Reads saved favourites: plain names (an early version) or { command, icon }.
28export function loadFavourites(saved: unknown): Favourite[] {
29 if (!Array.isArray(saved)) return STARTERS
30 return saved
31 .map(x => (typeof x === 'string' ? { command: x, icon: '⭐' } : x))
32 .filter((x): x is Favourite => !!x && typeof x.command === 'string' && typeof x.icon === 'string')
33}
34
35// Commands whose name or description holds every word of the search, best matches (name hits) first.
36export function search<T extends { name: string; description: string }>(all: T[], query: string): T[] {
37 const words = query.toLowerCase().split(/\s+/).filter(Boolean)
38 if (words.length === 0) return all
39 const hits = all.filter(c => words.every(w => `${c.name} ${c.description}`.toLowerCase().includes(w)))
40 const inName = (c: T) => words.every(w => c.name.toLowerCase().includes(w))
41 return [...hits.filter(inName), ...hits.filter(c => !inName(c))]
42}
43
44// What a click should do when nobody chose: immediate commands run at once; skills (from a skills folder,
45// or named plugin:skill) wait in the text box for an instruction; other commands run at once.
46export function defaultAction(command: { name: string; source: string }, isImmediate = false): ClickAction {
47 if (isImmediate) return 'run'
48 return command.source === 'user' || command.name.includes(':') ? 'fill' : 'run'
49}
50
51// A skill fills the text box; a plugin command runs.
52export function actionFor(kind: Kind): ClickAction {
53 return kind === 'skill' ? 'fill' : 'run'
54}
55
56// A favourite's kind: as chosen, else from an older click choice, else the usual guess for the command.
57export function kindOf(f: Favourite, command?: { name: string; source: string }, isImmediate = false): Kind {
58 if (f.kind) return f.kind
59 if (f.action) return f.action === 'fill' ? 'skill' : 'plugin'
60 return command && defaultAction(command, isImmediate) === 'fill' ? 'skill' : 'plugin'
61}
62
63// Sets one favourite's kind, dropping any older click choice.
64export function setKind(list: Favourite[], command: string, kind: Kind): Favourite[] {
65 return list.map(f => {
66 if (f.command !== command) return f
67 const { action: _, ...rest } = f
68 return { ...rest, kind }
69 })
70}
71
72// Adds a favourite, or changes the icon of one already there.
73export function setIcon(list: Favourite[], command: string, icon: string): Favourite[] {
74 return list.some(f => f.command === command)
75 ? list.map(f => (f.command === command ? { ...f, icon } : f))
76 : [...list, { command, icon }]
77}
78types/index.d.ts 30 lines1/** What a click does: run the command at once, or put it in the text box to add an instruction. */
2export type ClickAction = 'run' | 'fill'
3
4/** Which tab a favourite lives in: a plugin command (a click runs it) or a skill (a click fills the text box). */
5export type Kind = 'plugin' | 'skill'
6
7/** A favourite: a command or skill (its name, without the slash), its icon, and its kind.
8 * `action` is how version 1.2 to 1.4 stored the click choice; it is read as the kind when `kind` is missing. */
9export type Favourite = { command: string; icon: string; kind?: Kind; action?: ClickAction }
10
11/** What the popup shows: the icons, the search to add one, picking an icon or a kind, or editing. */
12export type Mode = 'grid' | 'add' | 'icon' | 'kind' | 'edit'
13
14declare module 'claude-code' {
15 interface PluginState {
16 'quick-bar': {
17 favourites: Favourite[]
18 mode: Mode
19 query: string
20 /** The command waiting for its icon, while picking one. */
21 pending: string | null
22 showBuiltins: boolean
23 /** The popup's open tab, remembered across sessions. */
24 tab: Kind
25 /** Commands Claude Code described as immediate (they run at once even mid-turn), by name. */
26 immediates: string[]
27 }
28 }
29}
30