A short to-do list pinned above the prompt, ticked off with one click.

A short to-do list pinned above the prompt, so the few things you must get done today stay in front of you while you work with Claude.
📋 1. call the printer [Done] 📋 2. send the invoice [Done]
/pin <text> pins a to-do at the bottom of the list./pin shows the list with its numbers./pin done 2, or the Done button beside it, ticks one off./pin clear clears the list.When you tick a to-do off, your next message carries one visible line naming it and the time, so Claude knows what you just finished. The list is kept across sessions on your own machine, in Claude Code's plugin store, and nothing is sent anywhere. The band draws in the terminal and in the Code tab of the Claude desktop app.
Nothing goes to any service from the mod itself. Here is each thing, plainly.
What it sends, and where.
items.Which programs it runs, and why.
What it puts in the prompts it submits.
(📋 Ticked off at <time>: "<to-do>"), naming each to-do you ticked off since your last message.What each hook does.
session.start: adds the /pin command and loads your list from the plugin store.command.run for pin: pins, lists, ticks off or clears, and saves the list.prompt.submit: adds the ticked-off line to your message. With nothing ticked off, it passes your message on unchanged.ui.render for AbovePrompt: draws the list, with a Done button on each line, above the prompt.Tested with Claude Code 2.1.287.
See every Rostech mod, live, at rostech.app.
hooks/register.tsx 86 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4// The pinned to-dos, in order. $.store keeps them across sessions; the atom redraws the band.
5const items = atom({ plugin: 'pin', key: 'items' } as const, [] as string[])
6// Items ticked off since the last prompt. The next prompt carries one line naming
7// them, because ticking one off tells Claude what you just finished.
8const done = atom({ plugin: 'pin', key: 'done' } as const, [] as string[])
9
10function stamp(): string {
11 return new Date().toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })
12}
13
14async function save($: EngineInterface, next: string[]) {
15 await $.store.set('items', next)
16 await update($, items, () => next)
17}
18
19async function tick($: EngineInterface, index: number): Promise<string | undefined> {
20 const now = await read($, items)
21 const item = now[index]
22 if (item === undefined) return undefined
23 await save($, now.filter((_, i) => i !== index))
24 await update($, done, d => [...d, item])
25 return item
26}
27
28function listing(now: readonly string[]): string {
29 return now.map((t, i) => `${i + 1}. ${t}`).join('\n')
30}
31
32export const register: Register = on => {
33 on('session.start', async ($, e, next) => {
34 await $.command.register({ name: 'pin', description: 'Pin to-dos above the prompt. /pin <text>, /pin done <number>, /pin clear.' })
35 const kept = await $.store.get('items')
36 await update($, items, () => (Array.isArray(kept) ? (kept as string[]) : []))
37 return next(e)
38 })
39
40 on('command.run', { command: 'pin' }, async ($, e) => {
41 const text = e.args.trim()
42 const [verb, ...rest] = text.split(/\s+/)
43 const now = await read($, items)
44 if (!text) return { text: now.length ? `📋 Pinned:\n${listing(now)}` : 'Nothing pinned. /pin <text> pins a to-do above the prompt.' }
45 if (verb.toLowerCase() === 'clear') {
46 await save($, [])
47 return { text: 'Pins cleared.' }
48 }
49 if (verb.toLowerCase() === 'done') {
50 const n = Number(rest[0])
51 const item = Number.isInteger(n) ? await tick($, n - 1) : undefined
52 return { text: item ? `✓ Done: ${item}` : `No pin number ${rest[0] ?? ''}. ${now.length ? `Pinned:\n${listing(now)}` : 'Nothing is pinned.'}` }
53 }
54 await save($, [...now, text])
55 return { text: `📋 Pinned ${now.length + 1}: ${text}` }
56 })
57
58 on('prompt.submit', async ($, e, next) => {
59 const finished = await read($, done)
60 if (!finished.length) return next(e)
61 await update($, done, () => [])
62 return next({ ...e, text: `${e.text}\n\n(📋 Ticked off at ${stamp()}: ${finished.map(f => `"${f}"`).join(', ')})` })
63 })
64
65 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
66 const now = await read($, items)
67 if (e.props.hasSurvey || !now.length) return next(e)
68
69 const { Box, Button, Text } = $.ui.resolve(e)
70 // The pins sit on their own lines; every other band above the prompt draws beneath them.
71 const below = await next(e)
72 return (
73 <Box flexDirection="column">
74 {now.map((t, i) => (
75 <Box key={`pin-${i}`} flexDirection="row">
76 <Text color="cyan" bold>📋 {i + 1}. </Text>
77 <Text color="cyan">{t} </Text>
78 <Button key={`done-${i}`} label="Done" onPress={async () => { await tick($, i) }} />
79 </Box>
80 ))}
81 {below}
82 </Box>
83 )
84 })
85}
86types/index.d.ts 6 lines1declare module 'claude-code' {
2 interface PluginState {
3 'pin': { items: string[]; done: string[] }
4 }
5}
6