Кнопки со следующими шагами над полем ввода после каждого ответа

Пять модов - маленьких плагинов, которые меняют интерфейс Claude Code: показывают кэш и лимиты, прячут личное на записи экрана, рисуют прогресс цели и не дают двум чатам править один файл вслепую.
Из ролика на канале @amir_aktay.
В Claude Code:
/plugin marketplace add pewdredd/claude-code-mods
/plugin install cache-keeper@aktay-mods
Остальные ставятся так же, меняется только имя: collision-guard, goal-meter, next-steps, recording-mode. Можно поставить любые по отдельности.
| Мод | Что делает | Как пользоваться |
|---|---|---|
| cache-keeper | Над полем ввода: сколько осталось жить кэшу промпта (60 минут), заполнение контекста, лимиты 5 часов и недели, цена сессии. За 5 минут до остывания кэша - уведомление. Кнопка «перенести сессию»: Claude пишет сводку сессии, потом кнопка «Очистить и продолжить» сама делает /clear и отправляет сводку в чистый чат. Пока включён режим записи, здесь же горит красный «● REC» | работает сам |
| next-steps | После ответа - три кнопки «что дальше», одним нажатием отправляют следующий запрос | работает сам, /next-steps - вкл/выкл |
| recording-mode | Пока пишешь экран, прячет в транскрипте почты, ключи, телефоны и суммы. Claude при этом видит всё как есть. Метку «● REC» рисует cache-keeper - ставь их вместе | /record on, /record off |
| goal-meter | Разбивает цель на шаги, отдаёт агенту и показывает полоску прогресса. После каждого ответа отмечает готовые шаги | /goal-meter <цель>, /goal-meter off |
| collision-guard | Если файл в последние 30 минут правил другой чат Claude Code - останавливает правку и просит агента спросить тебя. Повторная правка проходит | работает сам |
CACHE_TTL_MS в cache-keeper/hooks/register.tsx.Опиши словами, что мод должен показывать и когда, и попроси Claude Code его собрать. Любой мод отсюда - хороший образец: три файла в папке плагина и один register.tsx.
hooks/register.tsx 100 lines1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4// Next Steps: после каждого ответа спрашиваем модель (через fork, из кэша)
5// о трёх следующих шагах и рисуем их кнопками над полем ввода.
6const steps = atom({ plugin: 'next-steps', key: 'steps' } as const, [])
7const isHidden = atom({ plugin: 'next-steps', key: 'isHidden' } as const, false)
8
9const ASK =
10 'Based on this conversation, suggest exactly 3 short next steps the user would most likely ask you to do next. ' +
11 'Answer in the language of the conversation. Each step is an imperative phrase under 60 characters. ' +
12 'Reply with a JSON array of 3 strings and nothing else.'
13
14function parse(text: string): string[] {
15 const match = text.match(/\[[\s\S]*\]/)
16 if (!match) return []
17 try {
18 const list = JSON.parse(match[0])
19 return Array.isArray(list) ? list.filter(s => typeof s === 'string').slice(0, 3) : []
20 } catch {
21 return []
22 }
23}
24
25export const register: Register = on => {
26 on('session.start', async ($, e, next) => {
27 await $.command.register({ name: 'next-steps', description: 'Показать или спрятать кнопки следующих шагов' })
28 return next(e)
29 })
30
31 on('command.run', { command: 'next-steps' }, async $ => {
32 const hidden = await update($, isHidden, v => !v)
33 return { text: hidden ? 'Next Steps выключен.' : 'Next Steps включён.' }
34 })
35
36 on('prompt.submit', async ($, e, next) => {
37 await update($, steps, () => [])
38 return next(e)
39 })
40
41 on('turn.complete', async ($, e, next) => {
42 const result = await next(e)
43 if (e.agentId || e.reason !== 'answer' || (await read($, isHidden))) return result
44
45 // fork живёт дольше хука, поэтому запускаем его таймером
46 $.clock.after(0, async () => {
47 try {
48 const answer = await $.model.fork({ prompt: ASK })
49 if (!answer.isAnswered) {
50 $.ui.log(`next-steps: модель не ответила (${answer.reason})`)
51 return
52 }
53 const list = parse(answer.text)
54 if (list.length === 0) $.ui.log(`next-steps: не разобрал ответ: ${answer.text.slice(0, 120)}`)
55 await update($, steps, () => list)
56 } catch (err) {
57 $.ui.log(`next-steps: ошибка ${String(err).slice(0, 160)}`)
58 }
59 })
60 return result
61 })
62
63 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
64 const list = await read($, steps)
65 if (e.props.hasSurvey || e.props.isWorking || list.length === 0 || (await read($, isHidden))) {
66 return next(e)
67 }
68
69 const { Box, Button, Text } = $.ui.resolve(e)
70 const mine = (
71 <Box flexDirection="column" marginTop={1}>
72 <Text dimColor>дальше:</Text>
73 <Box flexDirection="column" paddingLeft={2}>
74 {list.map((step, i) => (
75 <Button
76 key={`step-${i}`}
77 plain
78 hotkey={String(i + 1)}
79 label={step}
80 onPress={async () => {
81 await update($, steps, () => [])
82 await $.prompt.submit({ text: step })
83 }}
84 />
85 ))}
86 <Button key="dismiss" plain dimColor hotkey="0" label="скрыть" onPress={() => update($, steps, () => [])} />
87 </Box>
88 </Box>
89 )
90
91 // место над полем ввода общее: дорисовываем под собой строки других модов
92 return (
93 <Box flexDirection="column">
94 {mine}
95 {await next(e)}
96 </Box>
97 )
98 })
99}
100types/index.d.ts 8 lines1export type NextStep = string
2
3declare module 'claude-code' {
4 interface PluginState {
5 'next-steps': { steps: NextStep[]; isHidden: boolean }
6 }
7}
8