SLOPSHOPPER

ideas-shelf

Prateleira de ideias por projeto: anote ideias num painel e marque como feitas; fica salvo em IDEAS.md na raiz do projeto.

newpanecommandtoast
v0.1.0MITupdated 2026-10-09ptpmediabr/ideas-shelf
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · ideas-shelf
│ ┃ Ideas Shelf ✕ › fix the failing auth test and add an audit log call │ ┃ Ideas Shelf │ ┃ ⏺ Read(src/auth.ts) │ ┃ +: Anote uma ideia e pressione Enter ⏎ gua ⎿ Read 6 lines │ ┃ ⏺ Update(src/auth.ts) │ ┃ ⎿ Added 2 lines, removed 1 line │ ┃ A prateleira está vazia. ⏺ Bash(bun test) │ ┃ Anote acima ou use /idea <texto> no chat. ⎿ 3 pass, 1 fail │ ┃ │ ┃ ● Done. refresh now rejects expired claims and logs an audit event. │ ┃ ○ marca como feita · ➤ envia ao chat · ✕ │ ┃ remove · IDEAS.md ✻ Worked for 42s · done 4:20 PM │ │ › /ideas │ ⎿ ideas-shelf: Ideas Shelf aberto. │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · Ideas Shelf
Ideas Shelf +: Anote uma ideia e pressione Enter ⏎ guardar A prateleira está vazia. Anote acima ou use /idea <texto> no chat. ○ marca como feita · ➤ envia ao chat · ✕ remove · IDEAS.md
README

ideas-shelf

Uma prateleira de ideias para cada projeto, dentro do Claude Code. Anote ideias num painel, marque as que já fez e mande as pendentes para o chat com um clique. Tudo fica num arquivo IDEAS.md na raiz do projeto: fácil de ler, editar e versionar.

Como usar

ComandoO que faz
/ideasAbre o painel Ideas Shelf do projeto.
/idea <texto>Guarda uma ideia direto, sem abrir o painel.

No painel:

  • Digite no campo e pressione Enter para guardar.
  • ○ marca a ideia como feita e ✓ desmarca.
  • ➤ envia a ideia ao chat, como se você tivesse digitado (só nas pendentes).
  • ✕ remove a ideia.
  • A barra ▰▰▰▱▱▱▱▱▱▱ 3/10 feitas mostra o progresso.

Os textos do painel estão em português.

O arquivo IDEAS.md

# Ideias

- [ ] ideia pendente
- [x] ideia feita

Cada ideia é uma linha de tarefa em markdown. O arquivo é a fonte da verdade: o painel o relê antes de mostrar, antes de cada mudança e ao fim de cada turno, então edições feitas à mão ou pelo próprio Claude aparecem na hora. Se o arquivo ainda não existe, ele é criado na primeira ideia.

Dica: se não quer versionar suas ideias, coloque IDEAS.md no .gitignore do projeto.

Instalação

Numa sessão do Claude Code no terminal (claude), digite:

/plugin install ideas-shelf --marketplace ptpmediabr/ideas-shelf

O Claude Code pergunta se pode adicionar o marketplace (responda y) e depois o escopo (escolha user para valer em todos os projetos). Quando aparecer Installed ideas-shelf. Plugin is now active., o mod já está rodando, sem reiniciar: use /ideas.

O comando de instalação só funciona no terminal. Instalado no escopo user, o mod também carrega nas sessões do app desktop do Claude Code.

Privacidade e o que o mod faz

  • Lê e escreve só o IDEAS.md da raiz do projeto da sessão.
  • Não usa a rede nem chama modelos por conta própria: o ➤ apenas envia o texto da ideia como uma mensagem sua, e o Claude responde como a qualquer outra.

Desenvolvimento

claude plugin validate .
claude plugin test .

Testado com o Claude Code 2.1.296.

Licença

MIT. Feito por Chiara Costa.

Source 2 files
hooks/register.tsx 313 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4const PANE = 'ideas-shelf'
5const TITLE = 'Ideas Shelf'
6const FILE = 'IDEAS.md'
7const HEADER = ['# Ideias', '']
8const BAR = 10
9// One idea is one markdown task line: "- [ ] text" or "- [x] text".
10const TASK = /^(\s*[-*] \[)([ xX])(\] )(.*)$/
11
12const shelf = atom({ plugin: 'ideas-shelf', key: 'shelf' } as const, {
13  path: '',
14  lines: [],
15})
16
17// Counts the ideas added from the field: the field's key carries it, so each
18// add draws a fresh, empty field.
19const added = atom({ plugin: 'ideas-shelf', key: 'added' } as const, 0)
20
21const readLines = async ($: EngineInterface, path: string) => {
22  if (!(await $.fs.exists(path))) {
23    return []
24  }
25  const lines = (await $.fs.read(path)).split(/\r?\n/)
26  while (lines.length > 0 && lines[lines.length - 1] === '') {
27    lines.pop()
28  }
29
30  return lines
31}
32
33// The project's file is the truth: the shelf is re-read before it is shown
34// and before every change, so edits made by hand or by Claude are kept.
35const load = async ($: EngineInterface) => {
36  const path = `${await $.session.root()}/${FILE}`
37  const lines = await readLines($, path)
38  await update($, shelf, () => ({ path, lines }))
39
40  return { path, lines }
41}
42
43const change = async (
44  $: EngineInterface,
45  edit: (lines: string[]) => string[] | undefined,
46) => {
47  const { path, lines } = await load($)
48  const edited = edit(lines.length === 0 ? [...HEADER] : [...lines])
49  if (edited === undefined) {
50    $.ui.toast('Essa ideia mudou no arquivo; a lista foi atualizada.')
51
52    return false
53  }
54  await $.fs.write(path, `${edited.join('\n')}\n`)
55  await update($, shelf, () => ({ path, lines: edited }))
56
57  return true
58}
59
60const add = ($: EngineInterface, text: string) => {
61  const idea = text.replace(/\s+/g, ' ').trim()
62  if (idea === '') {
63    return Promise.resolve(false)
64  }
65
66  return change($, lines => [...lines, `- [ ] ${idea}`])
67}
68
69// A row is addressed by the line it was drawn from; if the file moved under
70// it, the same line is looked up again before anything is touched.
71const locate = (lines: string[], index: number, line: string) =>
72  lines[index] === line ? index : lines.indexOf(line)
73
74const toggle = ($: EngineInterface, index: number, line: string) =>
75  change($, lines => {
76    const at = locate(lines, index, line)
77    if (at < 0) {
78      return undefined
79    }
80    lines[at] = line.replace(
81      TASK,
82      (_, head: string, mark: string, tail: string, text: string) =>
83        `${head}${mark === ' ' ? 'x' : ' '}${tail}${text}`,
84    )
85
86    return lines
87  })
88
89const remove = ($: EngineInterface, index: number, line: string) =>
90  change($, lines => {
91    const at = locate(lines, index, line)
92    if (at < 0) {
93      return undefined
94    }
95    lines.splice(at, 1)
96
97    return lines
98  })
99
100// Sent as the person's own words; the turn starts once the session is idle.
101const send = ($: EngineInterface, text: string) => {
102  void $.prompt.submit({ text, asUser: true })
103  $.ui.toast('Ideia enviada ao chat.')
104}
105
106export const register: Register = on => {
107  on('session.start', async ($, e, next) => {
108    await $.command.register({
109      name: 'ideas',
110      description: 'Abre a prateleira de ideias deste projeto',
111    })
112    await $.command.register({
113      name: 'idea',
114      description: 'Guarda uma ideia na prateleira deste projeto',
115      argumentHint: '<texto da ideia>',
116    })
117    await load($)
118
119    return next(e)
120  })
121
122  on('command.run', { command: 'ideas' }, async $ => {
123    await load($)
124    await $.ui.open({ id: PANE, title: TITLE, focus: true })
125
126    return { text: 'Ideas Shelf aberto.' }
127  })
128
129  on('command.run', { command: 'idea' }, async ($, e) => {
130    if (e.args.trim() === '') {
131      await load($)
132      await $.ui.open({ id: PANE, title: TITLE, focus: true })
133
134      return { text: 'Ideas Shelf aberto. Use /idea <texto> para guardar direto.' }
135    }
136    await add($, e.args)
137
138    return { text: `Ideia guardada em ${FILE}: ${e.args.trim()}` }
139  })
140
141  // Claude may have edited IDEAS.md, or the session may have moved project.
142  on('turn.complete', async ($, e, next) => {
143    await load($)
144
145    return next(e)
146  })
147
148  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
149    const { Box, Button, Text } = $.ui.resolve(e)
150    const Input = e.surface === 'mobile' ? undefined : $.ui.resolve(e).Input
151    const { lines } = await read($, shelf)
152    const field = await read($, added)
153    const ideas = lines.flatMap((line, index) => {
154      const task = TASK.exec(line)
155
156      return task === null
157        ? []
158        : [{ index, line, isDone: task[2] !== ' ', text: task[4] ?? '' }]
159    })
160    const open = ideas.filter(idea => !idea.isDone)
161    const done = ideas.filter(idea => idea.isDone)
162    const filled =
163      ideas.length === 0 ? 0 : Math.round((done.length / ideas.length) * BAR)
164    const actions = (idea: (typeof ideas)[number]) => (
165      <Box flexShrink={0} gap={1}>
166        {!idea.isDone && (
167          <Button
168            key={`send:${idea.index}`}
169            plain
170            dimColor
171            hover={{ dimColor: false, color: 'suggestion' }}
172            onPress={() => {
173              send($, idea.text)
174            }}
175          >
176            ➤
177          </Button>
178        )}
179        <Button
180          key={`remove:${idea.index}`}
181          plain
182          dimColor
183          hover={{ dimColor: false, color: 'error' }}
184          onPress={() => {
185            void remove($, idea.index, idea.line)
186          }}
187        >
188          ✕
189        </Button>
190      </Box>
191    )
192
193    return (
194      <Box flexDirection="column" paddingX={1} gap={1}>
195        <Box justifyContent="space-between">
196          <Text bold>{TITLE}</Text>
197          {ideas.length > 0 && (
198            <Text>
199              <Text color="success">{'▰'.repeat(filled)}</Text>
200              <Text dimColor>{'▱'.repeat(BAR - filled)}</Text>
201              <Text dimColor>
202                {' '}
203                {done.length}/{ideas.length} feitas
204              </Text>
205            </Text>
206          )}
207        </Box>
208        {Input !== undefined && (
209          <Input
210            key={`new:${field}`}
211            label="+"
212            placeholder="Anote uma ideia e pressione Enter"
213            value=""
214            submitLabel="guardar"
215            autoFocus
216            onSubmit={value => {
217              void add($, value).then(isAdded =>
218                isAdded ? update($, added, count => count + 1) : undefined,
219              )
220            }}
221          />
222        )}
223        {ideas.length === 0 && (
224          <Box flexDirection="column" alignItems="center" paddingY={1}>
225            <Text dimColor>A prateleira está vazia.</Text>
226            <Text dimColor>Anote acima ou use /idea &lt;texto&gt; no chat.</Text>
227          </Box>
228        )}
229        {ideas.length > 0 && (
230          <Box flexDirection="column">
231            <Text bold color="suggestion">
232              PENDENTES <Text dimColor>· {open.length}</Text>
233            </Text>
234            {open.length === 0 && (
235              <Text dimColor>Tudo feito por aqui.</Text>
236            )}
237            {open.map(idea => (
238              <Box
239                key={`row:${idea.index}`}
240                justifyContent="space-between"
241                gap={2}
242                paddingX={1}
243                borderStyle="round"
244                borderDimColor
245                hover={{ borderColor: 'suggestion' }}
246              >
247                <Box flexGrow={1} flexShrink={1} gap={1}>
248                  <Box flexShrink={0}>
249                    <Button
250                      key={`toggle:${idea.index}`}
251                      plain
252                      onPress={() => {
253                        void toggle($, idea.index, idea.line)
254                      }}
255                    >
256                      <Text bold color="claude">
257                        ○
258                      </Text>
259                    </Button>
260                  </Box>
261                  <Text>{idea.text}</Text>
262                </Box>
263                {actions(idea)}
264              </Box>
265            ))}
266          </Box>
267        )}
268        {done.length > 0 && (
269          <Box flexDirection="column">
270            <Text bold color="success">
271              FEITAS <Text dimColor>· {done.length}</Text>
272            </Text>
273            {done.map(idea => (
274              <Box
275                key={`row:${idea.index}`}
276                justifyContent="space-between"
277                gap={2}
278                paddingX={1}
279                borderStyle="round"
280                borderDimColor
281                hover={{ borderColor: 'success' }}
282              >
283                <Box flexGrow={1} flexShrink={1} gap={1}>
284                  <Box flexShrink={0}>
285                    <Button
286                      key={`toggle:${idea.index}`}
287                      plain
288                      onPress={() => {
289                        void toggle($, idea.index, idea.line)
290                      }}
291                    >
292                      <Text bold color="success">
293                        ✓
294                      </Text>
295                    </Button>
296                  </Box>
297                  <Text dimColor strikethrough>
298                    {idea.text}
299                  </Text>
300                </Box>
301                {actions(idea)}
302              </Box>
303            ))}
304          </Box>
305        )}
306        <Text dimColor>
307          ○ marca como feita · ➤ envia ao chat · ✕ remove · {FILE}
308        </Text>
309      </Box>
310    )
311  })
312}
313
types/index.d.ts 8 lines
1export type Shelf = { path: string; lines: string[] }
2
3declare module 'claude-code' {
4  interface PluginState {
5    'ideas-shelf': { shelf: Shelf; added: number }
6  }
7}
8