SLOPSHOPPER

prompt-snippets

Your prompts as snippets: ;name in the prompt expands to the saved text, /snip save keeps the last prompt, /snip lists them all / Ваши промпты как сниппеты…

newpanecommandpromptprocess
★ 1v1.0.0MITupdated 2026-10-26ArtyaEXE/claude-mods/prompt-snippets
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · prompt-snippets
│ ┃ Snippets ✕ › fix the failing auth test and add an audit log call │ ┃ No snippets yet. Send a prompt and keep it: │ ┃ /snip save name — or with the text right ⏺ Read(src/auth.ts) │ ┃ away: /snip save name text. ⎿ 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 │ │ › /snip │ ⎿ prompt-snippets: Opened the snippets pane. │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · Snippets
No snippets yet. Send a prompt and keep it: /snip save name — or with the text right away: /snip save name text.
README

prompt-snippets

Your prompts as snippets: ;name in the prompt expands to the saved text, /snip save keeps the last prompt, /snip lists them all.

English · Русский

English

The same requests come back every day: a review checklist, "fix the failing test and run it again", a project's conventions. Save each once and type it in two keystrokes.

  • Save: /snip save review keeps the last prompt you sent as ;review; /snip save fix Fix the failing test, then run it again. saves the text given right there (lines included).
  • Use: type ; and the first letters: the suggestions list the matching snippets with their first line; taking one puts the whole text in its place. A ;review sent as it is expands too, so ;review for a.ts works without the suggestion.
  • Browse: /snip opens a pane with every snippet, Insert (at the cursor, the draft stays) and Delete. /snip rm review deletes one.

Snippets are yours across every project and session. Names are letters (any script), digits, _ and -, so ;ревью works too.

What it touches

Keeps the snippets and the last prompt you typed in the mod's store (a JSON file under the Claude Code configuration folder). Adds suggestions for words starting with ;; replaces a sent ;name with the snippet's text. Adds the /snip command.

Settings (/config)

  • language: auto (default; follows LANG, else the Windows UI language or regional format), ru or en.

Install

/plugin install prompt-snippets --marketplace ArtyaEXE/claude-mods

Part of claude-mods. MIT license.


Русский

Ваши промпты как сниппеты: ;имя в промпте разворачивается в сохранённый текст, /snip save сохраняет последний промпт, /snip показывает все.

Одни и те же просьбы возвращаются каждый день: чек-лист ревью, «почини упавший тест и прогони заново», соглашения проекта. Сохраните каждую один раз и вставляйте в два нажатия.

  • Сохранить: /snip save ревью сохраняет последний отправленный промпт как ;ревью; /snip save fix Почини упавший тест и прогони заново. сохраняет текст, указанный прямо в команде (с переносами строк).
  • Вставить: наберите ; и первые буквы — в подсказках появятся подходящие сниппеты с первой строкой; выбор подставляет весь текст на место слова. Отправленный как есть ;ревью тоже разворачивается, так что ;ревью для a.ts работает и без подсказки.
  • Посмотреть: /snip открывает панель со всеми сниппетами, кнопками Вставить (у курсора, набранное остаётся) и Удалить. /snip rm ревью удаляет один.

Сниппеты общие для всех проектов и сессий. Имя — буквы (любого алфавита), цифры, _ и -, так что ;ревью тоже работает.

Что мод делает в системе

Хранит сниппеты и последний набранный промпт в хранилище мода (JSON-файл в папке настроек Claude Code). Добавляет подсказки для слов, начинающихся с ;; заменяет отправленное ;имя текстом сниппета. Добавляет команду /snip.

Настройки (/config)

  • language: auto (по умолчанию; по LANG, иначе по языку интерфейса или региональному формату Windows), ru или en.

Установка

/plugin install prompt-snippets --marketplace ArtyaEXE/claude-mods

Часть claude-mods. Лицензия MIT.

Source 2 files
hooks/register.tsx 184 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Snippet } from '../types'
5
6type Lang = 'ru' | 'en'
7
8// `ru` / `en` as set; `auto`: the locale variables first, then Windows' UI language or regional format.
9async function resolveLang($: EngineInterface, setting: unknown): Promise<Lang> {
10  if (setting === 'ru' || setting === 'en') return setting
11  const env = (await $.env.get('LC_ALL')) || (await $.env.get('LC_MESSAGES')) || (await $.env.get('LANG'))
12  if (env) return env.toLowerCase().startsWith('ru') ? 'ru' : 'en'
13  try {
14    const ran = await $.process.run(
15      ['powershell.exe', '-NoProfile', '-NonInteractive', '-Command', '(Get-UICulture).Name; (Get-Culture).Name'],
16      { timeoutMs: 10_000 },
17    )
18    if (ran.exitCode === 0 && /^ru\b/im.test(ran.stdout)) return 'ru'
19  } catch {
20    // Not Windows: English.
21  }
22
23  return 'en'
24}
25
26const PANE = 'prompt-snippets'
27const list = atom({ plugin: 'prompt-snippets', key: 'list' } as const, [] as Snippet[], { shape: 'list-1' })
28
29// A snippet's name: letters (any script), digits, `_` and `-`.
30const NAME = /^[\p{L}\p{N}_-]+$/u
31// `;name` standing alone in a prompt: at the start or after whitespace, up to whitespace or the end.
32const MENTION = /(^|\s);([\p{L}\p{N}_-]+)(?=\s|$)/gu
33
34const STRINGS = {
35  ru: {
36    command: 'Сниппеты промптов: ;имя в промпте; /snip save имя [текст] | rm имя',
37    paneTitle: 'Сниппеты',
38    opened: 'Открыл панель сниппетов.',
39    empty: 'Сниппетов нет. Отправьте промпт и сохраните его: /snip save имя — или сразу с текстом: /snip save имя текст.',
40    hint: 'В промпте: ;имя — подсказка подставит текст',
41    insert: 'Вставить',
42    remove: 'Удалить',
43    saved: (name: string, n: number) => `Сохранил ;${name} (${n} симв.). В промпте: ;${name}`,
44    replaced: (name: string) => `Перезаписал ;${name}.`,
45    removed: (name: string) => `Удалил ;${name}.`,
46    noSuch: (name: string) => `Сниппета ;${name} нет.`,
47    noLast: 'Пока нечего сохранять: отправьте промпт, затем /snip save имя. Или: /snip save имя текст.',
48    badName: (name: string) => `Имя «${name}» не подходит: только буквы, цифры, _ и -.`,
49    usage: 'Использование: /snip, /snip save имя [текст], /snip rm имя',
50  },
51  en: {
52    command: 'Prompt snippets: ;name in the prompt; /snip save name [text] | rm name',
53    paneTitle: 'Snippets',
54    opened: 'Opened the snippets pane.',
55    empty: 'No snippets yet. Send a prompt and keep it: /snip save name — or with the text right away: /snip save name text.',
56    hint: 'In a prompt: ;name — the suggestion puts the text in',
57    insert: 'Insert',
58    remove: 'Delete',
59    saved: (name: string, n: number) => `Saved ;${name} (${n} chars). In a prompt: ;${name}`,
60    replaced: (name: string) => `Replaced ;${name}.`,
61    removed: (name: string) => `Deleted ;${name}.`,
62    noSuch: (name: string) => `There is no ;${name}.`,
63    noLast: 'Nothing to save yet: send a prompt, then /snip save name. Or: /snip save name text.',
64    badName: (name: string) => `"${name}" will not do as a name: letters, digits, _ and - only.`,
65    usage: 'Usage: /snip, /snip save name [text], /snip rm name',
66  },
67}
68
69let lang: Lang = 'en'
70
71type Saved = Record<string, string>
72
73async function load($: EngineInterface): Promise<Saved> {
74  const raw = await $.store.get('snippets')
75
76  return raw !== null && typeof raw === 'object' ? (raw as Saved) : {}
77}
78
79async function store($: EngineInterface, saved: Saved) {
80  await $.store.set('snippets', saved)
81  await update($, list, () =>
82    Object.entries(saved)
83      .map(([name, text]) => ({ name, text }))
84      .sort((a, b) => a.name.localeCompare(b.name)),
85  )
86}
87
88const firstLine = (text: string, n = 70) => {
89  const line = text.split('\n').find(l => l.trim() !== '')?.trim() ?? ''
90
91  return line.length > n ? `${line.slice(0, n - 1)}…` : line
92}
93
94async function remove($: EngineInterface, name: string) {
95  const saved = await load($)
96  delete saved[name]
97  await store($, saved)
98}
99
100export const register: Register = (on, options) => {
101  on('session.start', async ($, e, next) => {
102    const started = await next(e)
103    lang = await resolveLang($, (options as { language?: unknown }).language)
104    await $.command.register({ name: 'snip', description: STRINGS[lang].command, argumentHint: 'save <name> [text] | rm <name>' })
105    await store($, await load($))
106
107    return started
108  })
109
110  on('command.run', { command: 'snip' }, async ($, e) => {
111    const L = STRINGS[lang]
112    const [verb = '', name = '', ...rest] = e.args.trim().split(/\s+/)
113    if (verb === '') {
114      await $.ui.open({ id: PANE, title: L.paneTitle, focus: true, closeOnEscape: true })
115
116      return { text: L.opened }
117    }
118    if (verb !== 'save' && verb !== 'rm') return { text: L.usage }
119    if (!NAME.test(name)) return { text: name === '' ? L.usage : L.badName(name) }
120
121    const saved = await load($)
122    if (verb === 'rm') {
123      if (saved[name] === undefined) return { text: L.noSuch(name) }
124      await remove($, name)
125
126      return { text: L.removed(name) }
127    }
128    // The text after the name as typed (spacing and lines kept), else the last prompt sent.
129    const given = rest.length > 0 ? e.args.trim().replace(/^\S+\s+\S+\s*/, '') : ''
130    const last = await $.store.get('lastPrompt')
131    const text = given !== '' ? given : typeof last === 'string' ? last : ''
132    if (text === '') return { text: L.noLast }
133    const existed = saved[name] !== undefined
134    await store($, { ...saved, [name]: text })
135
136    return { text: existed ? L.replaced(name) : L.saved(name, text.length) }
137  })
138
139  // `;na…` while typing: the snippets whose names start so, each writing its whole text in place of the token.
140  on('prompt.autocomplete', { token: /^;/ }, async ($, e, next) => {
141    const below = await next(e)
142    const typed = e.token.slice(1).toLowerCase()
143    const mine = (await read($, list))
144      .filter(s => s.name.toLowerCase().startsWith(typed))
145      .map(s => ({ text: s.text, label: `;${s.name}`, description: firstLine(s.text) }))
146
147    return { suggestions: [...mine, ...below.suggestions] }
148  })
149
150  // A `;name` sent as it is expands to its text; a prompt typed by the person is kept as the last one, for /snip save.
151  on('prompt.submit', async ($, e, next) => {
152    // The person's own words: typed here, or sent from the phone through Remote Control.
153    if ((e.origin.kind !== 'composer' && e.origin.kind !== 'bridge') || e.text.trimStart().startsWith('/')) return next(e)
154    const saved = await load($)
155    const text = e.text.replace(MENTION, (whole, lead: string, name: string) => (saved[name] === undefined ? whole : `${lead}${saved[name]}`))
156    await $.store.set('lastPrompt', e.text)
157
158    return next(text === e.text ? e : { ...e, text })
159  })
160
161  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
162    const { Box, Button, Text } = $.ui.resolve(e)
163    const L = STRINGS[lang]
164    const snippets = await read($, list)
165    const width = Math.max(20, e.props.bodyColumns - 30)
166
167    return (
168      <Box flexDirection="column">
169        {snippets.length === 0 ? <Text dimColor>{L.empty}</Text> : <Text dimColor>{L.hint}</Text>}
170        {snippets.map(s => (
171          <Box flexDirection="row" gap={1}>
172            <Text color="cyan">;{s.name}</Text>
173            <Text dimColor wrap="truncate">
174              {firstLine(s.text, width)}
175            </Text>
176            <Button key={`insert:${s.name}`} plain label={L.insert} onPress={() => void $.prompt.fill({ text: s.text, mode: 'insert' })} />
177            <Button key={`remove:${s.name}`} plain dimColor label={L.remove} onPress={() => remove($, s.name)} />
178          </Box>
179        ))}
180      </Box>
181    )
182  })
183}
184
types/index.d.ts 11 lines
1export type Snippet = { name: string; text: string }
2
3declare module 'claude-code' {
4  interface PluginState {
5    'prompt-snippets': {
6      // The saved snippets as the pane draws them, by name.
7      list: Shaped<Snippet[]>
8    }
9  }
10}
11