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

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.
| Comando | O que faz |
|---|---|
/ideas | Abre o painel Ideas Shelf do projeto. |
/idea <texto> | Guarda uma ideia direto, sem abrir o painel. |
No painel:
▰▰▰▱▱▱▱▱▱▱ 3/10 feitas mostra o progresso.Os textos do painel estão em português.
# 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.mdno.gitignoredo projeto.
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.
IDEAS.md da raiz do projeto da sessão.claude plugin validate .
claude plugin test .
Testado com o Claude Code 2.1.296.
MIT. Feito por Chiara Costa.
hooks/register.tsx 313 lines1import { 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 <texto> 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}
313types/index.d.ts 8 lines1export type Shelf = { path: string; lines: string[] }
2
3declare module 'claude-code' {
4 interface PluginState {
5 'ideas-shelf': { shelf: Shelf; added: number }
6 }
7}
8