SLOPSHOPPER

quick-bar

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.

newpanespinnercommandtoast
v1.5.0MITupdated 2026-10-12dricosnyman/claude-favourites
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · quick-bar
│ ┃ Favourites ✕ › fix the failing auth test and add an audit log call │ ┃ ⚡ [ 🧩 Plugins ] 🎓 Skills + Add │ ┃ ⏺ Read(src/auth.ts) │ ┃ No plugins here yet. Press + Add. ⎿ Read 6 lines │ ┃ ⏺ Update(src/auth.ts) │ ┃ ⎿ Added 2 lines, removed 1 line │ ┃ ⏺ Bash(bun test) │ ┃ ⎿ 3 pass, 1 fail │ ┃ │ ● Done. refresh now rejects expired claims and logs an audit event. │ │ ✻ Worked for 42s · done 4:20 PM │ │ › /fav │ ⎿ quick-bar: Favourites opened. │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts auto-accept edits ⚡

Draws

Pane · Favourites
⚡ [ 🧩 Plugins ] 🎓 Skills + Add No plugins here yet. Press + Add.
README

⚡ Favourites for Claude Code

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.

  • Icons only. Hover over one to see its name. Click to use it.
  • Two tabs: 🧩 Plugins and 🎓 Skills. A click on a plugin command (like /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.
  • + Add searches every command and skill you have, live as you type. Pick one, then pick its icon from 150 emoji grouped by area (coding, writing, data, design, learning, plans, everyday life, numbers and more), then say whether it's a plugin or a skill (the best guess is highlighted).
  • ✎ Edit changes an icon, moves a favourite between Plugins and Skills, or removes it.
  • Works while Claude is busy, and your favourites are kept across sessions and projects.
  • Prefer typing? /fav opens the popup too. Esc closes it.

A favourite only shows in projects where its command exists, so one list works everywhere.

Install

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.

What it can and can't touch

A mod runs with your permissions, so here's exactly what this one does:

  • It draws the ⚡ and the popup, lists your commands so you can search them, and runs the one you click.
  • It saves your favourites in Claude Code's own plugin storage.
  • It makes no network calls, reads and writes no files, and starts no programs.

Check for yourself with claude plugin validate <this folder>, which lists every event it hooks and every call it makes.

Turn it off

Disable quick-bar in your plugin settings, or remove it:

claude plugin uninstall quick-bar@quick-bar

Works with

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.

Licence

MIT. See LICENSE.

Made by Drico, built together with Claude Code.

Source 3 files
hooks/register.tsx 345 lines
1import { 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}
345
hooks/labels.ts 78 lines
1// 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}
78
types/index.d.ts 30 lines
1/** 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