A pane listing the checkboxes of the folder's TODOS.md, toggled with a press

A pane showing the TODOS.md of the session folder as clickable checkboxes.
TODOS.md in the session folder and lists its ## headings (bold) and checkbox items (- [ ], - [x], * [ ], indented ones included) as ☐ / ☑ buttons.Edit/Write of a file ending in TODOS.md and after any Bash command mentioning TODOS.md.| Command | Effect |
|---|---|
/todos | Open the pane and reload the file. |
claude --plugin-dir /path/to/ModsTools/mods/todo-pane
## headings are shown (not # or ###); text that is not a heading or checkbox is hidden.TODOS.md in the session folder./todos or the next relevant tool call.claude plugin validate mods/todo-pane
claude plugin test mods/todo-pane # 11 testshooks/register.tsx 98 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import { parse, toggle } from './todos'
5
6const PANE = 'todo-pane'
7const TITLE = 'TODOS'
8const FILE = 'TODOS.md'
9const items = atom({ plugin: 'todo-pane', key: 'items' } as const, null)
10
11async function path($: EngineInterface) {
12 return `${await $.session.cwd()}/${FILE}`
13}
14
15async function load($: EngineInterface): Promise<string | null> {
16 try {
17 return await $.fs.read(await path($))
18 } catch {
19 return null
20 }
21}
22
23async function reload($: EngineInterface) {
24 const text = await load($)
25 await update($, items, () => (text === null ? null : parse(text)))
26}
27
28async function toggleLine($: EngineInterface, index: number, expected: string) {
29 const text = await load($)
30 const toggled = text === null ? null : toggle(text, index, expected)
31 if (toggled !== null) await $.fs.write(await path($), toggled)
32 await reload($)
33}
34
35const succeeded = (ran: { deny?: string; isError?: boolean }) => ran.deny === undefined && ran.isError !== true
36
37export const register: Register = on => {
38 on('session.start', async ($, e, next) => {
39 await $.command.register({ name: 'todos', description: `Show the checkboxes of ${FILE} in a pane` })
40 void $.ui.open({ id: PANE, title: TITLE })
41 await reload($)
42
43 return next(e)
44 })
45
46 on('command.run', { command: 'todos' }, async $ => {
47 await $.ui.open({ id: PANE, title: TITLE })
48 await reload($)
49 const list = await read($, items)
50
51 return { text: list === null ? `No ${FILE} in this folder.` : `TODOS pane opened.` }
52 })
53
54 on('tool.call', { tool: 'Edit' }, async ($, e, next) => {
55 const ran = await next(e)
56 if (succeeded(ran) && e.file_path.endsWith(FILE)) await reload($)
57
58 return ran
59 })
60
61 on('tool.call', { tool: 'Write' }, async ($, e, next) => {
62 const ran = await next(e)
63 if (succeeded(ran) && e.file_path.endsWith(FILE)) await reload($)
64
65 return ran
66 })
67
68 on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
69 const ran = await next(e)
70 if (ran.deny === undefined && e.command.includes(FILE)) await reload($)
71
72 return ran
73 })
74
75 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
76 const { Box, Button, Text } = $.ui.resolve(e)
77 const list = await read($, items)
78 if (list === null) return <Text dimColor>{`No ${FILE} in this folder.`}</Text>
79
80 return (
81 <Box flexDirection="column">
82 {list.length === 0 && <Text dimColor>No checkboxes yet.</Text>}
83 {list.map(item =>
84 item.kind === 'heading' ? (
85 <Text bold>{item.text}</Text>
86 ) : (
87 <Button
88 key={`todo:${item.index}`}
89 label={`${item.done ? '☑' : '☐'} ${item.text}`}
90 onPress={() => toggleLine($, item.index, item.text)}
91 />
92 ),
93 )}
94 </Box>
95 )
96 })
97}
98hooks/todos.ts 34 lines1import type { TodoItem } from '../types'
2
3const HEADING = /^##\s+(.*?)\s*$/
4const CHECKBOX = /^(\s*[-*] \[)([ xX])(\]\s+)(.*?)\s*$/
5
6const strip = (line: string) => line.replace(/\r$/, '')
7
8export function parse(text: string): TodoItem[] {
9 const items: TodoItem[] = []
10 text.split('\n').forEach((raw, index) => {
11 const line = strip(raw)
12 const heading = HEADING.exec(line)
13 if (heading) return void items.push({ index, kind: 'heading', text: heading[1] ?? '', done: false })
14 const box = CHECKBOX.exec(line)
15 if (box) items.push({ index, kind: 'todo', text: box[4] ?? '', done: box[2] !== ' ' })
16 })
17
18 return items
19}
20
21// The file with line `index` toggled, or null when that line is no longer the checkbox `expected`.
22export function toggle(text: string, index: number, expected: string): string | null {
23 const lines = text.split('\n')
24 const raw = lines[index]
25 if (raw === undefined) return null
26 const box = CHECKBOX.exec(strip(raw))
27 if (!box || box[4] !== expected) return null
28 const mark = box[2] === ' ' ? 'x' : ' '
29 const prefix = box[1] ?? ''
30 lines[index] = prefix + mark + raw.slice(prefix.length + 1)
31
32 return lines.join('\n')
33}
34types/index.d.ts 8 lines1export type TodoItem = { index: number; kind: 'heading' | 'todo'; text: string; done: boolean }
2
3declare module 'claude-code' {
4 interface PluginState {
5 'todo-pane': { items: TodoItem[] | null }
6 }
7}
8