Suggests next steps after every prompt and keeps them in a to-do list by category, in a panel you can click (/todo) or drive with commands.

Claude Code mods for every project.
After every prompt, Claude ends its reply with suggested next steps, grouped by category, and adds them to a to-do list for the project.
/todo opens a clickable panel (terminal, desktop app, VS Code): tick, remove and add steps, switch tracking, show or clear finished ones./todo on / /todo off switch tracking; #notodo or #todo in a prompt skips or forces one prompt./todo list, all, add <Category>: <text>, done <id>, undo <id>, clear [all]..claude/next-steps.json in a project, { "board": "<claude.ai artifact url>" }, naming a board page whose database collection items holds the list. Then that project's list lives on the board, shared by every session, and /todo links to it.Without a board the list is kept per project on the machine running the session.
It ticks itself. Claude marks an item done the moment it finishes it, mid-turn, not only at the end. And when Claude works through its own task list (TaskCreate/TaskUpdate, or TodoWrite), completing a task ticks the open item it stands for: one whose #id the task names, or with the same words. On a board, Claude is told to tick it there.
A cloud session's list lives in its container, so it never reaches your computer's list: give the project a board (above) to have cloud and local sessions share one list.
On your computer, for every project (this repo is private, so the machine needs GitHub access to it: gh auth login or an SSH key):
claude plugin marketplace add josh99smith/claude-mods
claude plugin install next-steps@claude-mods --scope user
or inside Claude Code: /plugin install next-steps --marketplace josh99smith/claude-mods, then pick user scope.
An installed plugin runs the copy made at install time; a new version reaches it only through an update. Each change to the mod bumps version in plugins/next-steps/.claude-plugin/plugin.json. To take it:
claude plugin marketplace update claude-mods
claude plugin update next-steps@claude-mods
then /reload-plugins in an open session (or start a new one). To have it happen by itself, turn on auto-update for the marketplace: /plugin → Marketplaces → claude-mods → Enable auto-update (third-party marketplaces start with it off); Claude Code then pulls new versions when it starts.
Cloud containers skip plugin marketplaces (SKIP_PLUGIN_MARKETPLACE) and adopt a plugin folder only from ~/.claude/skills/<name>, so install it in the cloud environment's setup script (environment menu in the session's title bar → Edit → Setup script), which runs before Claude Code starts:
git clone --depth 1 https://github.com/josh99smith/claude-mods /tmp/claude-mods \
&& mkdir -p ~/.claude/skills && cp -r /tmp/claude-mods/plugins/next-steps ~/.claude/skills/ || true
This repo is private: the clone works only where the session can reach it. If the setup log shows it refused, add claude-mods to the session or make the repo public.
Add to its .claude/settings.json (a local Claude Code installs it from the marketplace):
"extraKnownMarketplaces": { "claude-mods": { "source": { "source": "github", "repo": "josh99smith/claude-mods" } } },
"enabledPlugins": { "next-steps@claude-mods": true }
claude plugin test plugins/next-steps runs its tests; claude plugin validate . checks the marketplace and manifests.
hooks/register.tsx 500 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Item, List } from '../types'
5
6// One to-do list per project (keyed by the session's directory), kept in the
7// plugin's store so it outlives the session; the on/off switch is global. The
8// session's copy lives in $.state so the panel redraws whenever it changes.
9
10const TOOL = 'mcp__next-steps__update'
11const PANE = 'next-steps'
12const SKIP = /(^|\s)#notodo\b/i
13const FORCE = /(^|\s)#todo\b/i
14const EMPTY: List = { next: 1, items: [] }
15
16const listAtom = atom({ plugin: 'next-steps', key: 'list' } as const, EMPTY)
17const onAtom = atom({ plugin: 'next-steps', key: 'isOn' } as const, true)
18const showDoneAtom = atom({ plugin: 'next-steps', key: 'showDone' } as const, false)
19const categoryAtom = atom({ plugin: 'next-steps', key: 'category' } as const, 'General')
20/** Claude's own tasks this session (TaskCreate), id → title, so a TaskUpdate that finishes one can be matched. */
21const tasksAtom = atom({ plugin: 'next-steps', key: 'tasks' } as const, {} as Record<string, string>)
22
23const listKey = (cwd: string) => `list:${cwd}`
24const BOARD_FILE = '.claude/next-steps.json'
25
26/**
27 * The project's board, a claude.ai artifact page that holds the list where the
28 * Claude app can show it, named in .claude/next-steps.json ({ "board": url }) so
29 * every session of the project (a fresh cloud container too) finds it. With a
30 * board the list lives there and Claude keeps it through ArtifactData; this
31 * plugin's own store is the list only where there is none.
32 */
33async function boardUrl($: EngineInterface): Promise<string | undefined> {
34 try {
35 const raw = await $.fs.read(`${await $.session.cwd()}/${BOARD_FILE}`)
36 const url = (JSON.parse(String(raw)) as { board?: unknown }).board
37 return typeof url === 'string' && url.startsWith('https://claude.ai/') ? url : undefined
38 } catch {
39 return undefined
40 }
41}
42
43function boardInstruction(url: string): string {
44 return [
45 'The next-steps tracker is on for this prompt. The to-do list lives on a board page,',
46 `${url}, in its database collection \`items\` (doc id = the item's id as a string; fields`,
47 'id (number), category, text, done (boolean), at (ISO time)). When you have finished the request:',
48 '1. Read the board: ArtifactData list on `items` (load ArtifactData with ToolSearch if needed).',
49 ' Items the user ticked there are done; never reopen them.',
50 ' As you finish an open item, even mid-turn, update it {done: true} on the board right away.',
51 '2. Suggest the next steps: end your reply with a short "Suggested next steps" section, one',
52 ' line each, grouped under a short category (reuse the board\'s categories where they fit: e.g.',
53 ' Bugs, Features, Tests, Docs, Refactor, Ops, Questions). Concrete steps the user or a later',
54 ' session could take, not restatements of what was done. Skip steps already open on the board.',
55 '3. One ArtifactData batch: `set` each suggestion as a new doc (ids continue from the highest),',
56 ' and `update` {done: true} (with its if_version) on any open item this work finished.',
57 ' Say nothing about these calls; the board updates by itself.',
58 'If nothing remains and nothing was finished, skip 2 and 3.',
59 ].join('\n')
60}
61
62async function fromStore($: EngineInterface): Promise<List> {
63 const raw = (await $.store.get(listKey(await $.session.cwd()))) as List | undefined
64 return raw && Array.isArray(raw.items) ? raw : EMPTY
65}
66
67async function isOn($: EngineInterface): Promise<boolean> {
68 return (await $.store.get('enabled')) !== false
69}
70
71async function showStatus($: EngineInterface, list: List, on: boolean): Promise<void> {
72 const open = list.items.filter(i => !i.done).length
73 $.ui.status(`todo: ${on ? 'on' : 'off'} · ${open} open`)
74}
75
76/** Changes the list (a copy), then writes the store, the panel's state and the status line. */
77async function edit($: EngineInterface, change: (list: List) => void): Promise<List> {
78 const list = JSON.parse(JSON.stringify(await fromStore($))) as List
79 change(list)
80 // Keep the store small: finished items beyond the newest 200 are dropped.
81 const done = list.items.filter(i => i.done)
82 if (done.length > 200) {
83 const drop = new Set(done.slice(0, done.length - 200).map(i => i.id))
84 list.items = list.items.filter(i => !drop.has(i.id))
85 }
86 await $.store.set(listKey(await $.session.cwd()), list)
87 await update($, listAtom, () => list)
88 await showStatus($, list, await isOn($))
89 return list
90}
91
92async function setOn($: EngineInterface, on: boolean): Promise<void> {
93 await $.store.set('enabled', on)
94 await update($, onAtom, () => on)
95 await showStatus($, await fromStore($), on)
96}
97
98function addItem(list: List, category: string, text: string, at: string): number | undefined {
99 const clean = text.trim()
100 if (!clean) return undefined
101 if (list.items.some(i => !i.done && i.text.toLowerCase() === clean.toLowerCase())) return undefined
102 list.items.push({ id: list.next, category: category.trim() || 'General', text: clean, done: false, at })
103 return list.next++
104}
105
106/** "Bugs: fix it" → category and text; plain text takes the fallback category. */
107function parseEntry(entry: string, fallback: string): { category: string; text: string } {
108 const m = /^([^:]{1,40}):\s*(.+)$/.exec(entry.trim())
109 return m ? { category: m[1]?.trim() ?? fallback, text: m[2]?.trim() ?? '' } : { category: fallback, text: entry.trim() }
110}
111
112function grouped(items: Item[]): [string, Item[]][] {
113 const groups = new Map<string, Item[]>()
114 for (const item of items) groups.set(item.category, [...(groups.get(item.category) ?? []), item])
115 return [...groups].sort(([a], [b]) => a.localeCompare(b))
116}
117
118/** The list grouped by category, as Markdown. */
119export function render(list: List, withDone = false): string {
120 const shown = list.items.filter(i => withDone || !i.done)
121 if (shown.length === 0) return 'No next steps recorded for this project.'
122 const out: string[] = []
123 for (const [category, items] of grouped(shown)) {
124 out.push(`**${category}**`)
125 for (const i of items) out.push(`- [${i.done ? 'x' : ' '}] #${i.id} ${i.text}`)
126 out.push('')
127 }
128 return out.join('\n').trim()
129}
130
131function instruction(list: List): string {
132 const open = list.items.filter(i => !i.done)
133 const current = open.length
134 ? open.slice(-60).map(i => `#${i.id} [${i.category}] ${i.text}`).join('\n')
135 : '(empty)'
136 return [
137 'The next-steps tracker is on for this prompt. When you have finished the request:',
138 '1. Suggest the next steps: end your reply with a short "Suggested next steps" section, one',
139 ' line each, grouped under a short category (reuse an existing one where it fits: e.g. Bugs,',
140 ' Features, Tests, Docs, Refactor, Ops, Questions). Concrete steps the user or a later session',
141 ' could take, not restatements of what was done. Skip steps already open below.',
142 `2. Call ${TOOL} once with exactly those suggestions as \`add\`, and the ids of any open item`,
143 ' this work finished as `done`. Say nothing about the call itself.',
144 'If nothing remains and nothing was finished, skip both.',
145 'Open items now:',
146 current,
147 ].join('\n')
148}
149
150const words = (s: string) =>
151 s.toLowerCase().replace(/#\d+/g, ' ').replace(/[^a-z0-9]+/g, ' ').trim()
152
153/**
154 * The open item a task of Claude's stands for: one whose "#id" its title names,
155 * else one with the same words, or whose words hold the other's whole (the
156 * shorter at least three words, so "fix it" never ticks "fix the kickoff camera").
157 */
158export function matchItem(list: List, title: string): Item | undefined {
159 const open = list.items.filter(i => !i.done)
160 for (const m of title.matchAll(/#(\d+)\b/g)) {
161 const hit = open.find(i => i.id === Number(m[1]))
162 if (hit) return hit
163 }
164 const t = words(title)
165 if (!t) return undefined
166 const exact = open.find(i => words(i.text) === t)
167 if (exact) return exact
168 return open.find(i => {
169 const w = words(i.text)
170 const [short, long] = w.length < t.length ? [w, t] : [t, w]
171 return short.split(' ').length >= 3 && ` ${long} `.includes(` ${short} `)
172 })
173}
174
175/**
176 * Ticks the open items that tasks Claude just completed stand for. Without a
177 * board that is the list itself; with one, the board is Claude's to write, so
178 * Claude is told which finished task to tick there.
179 */
180async function tickFor($: EngineInterface, titles: string[]): Promise<string | undefined> {
181 if (titles.length === 0) return undefined
182 const board = await boardUrl($)
183 if (board) {
184 const names = titles.map(t => `"${t}"`).join(', ')
185 return `next-steps: if ${names} is an open item on the to-do board (${board}), mark it done there now (ArtifactData update {done: true}).`
186 }
187 const list = await fromStore($)
188 const ids = titles.map(t => matchItem(list, t)?.id).filter((n): n is number => n !== undefined)
189 if (ids.length === 0) return undefined
190 await edit($, l => {
191 for (const i of l.items) if (ids.includes(i.id)) i.done = true
192 })
193 return undefined
194}
195
196const now = async ($: EngineInterface) => new Date(await $.clock.now()).toISOString()
197
198export const register: Register = on => {
199 on('session.start', async ($, e, next) => {
200 await $.tool.register({
201 name: 'update',
202 description:
203 "Updates the project's next-steps to-do list: add new items, each with a category, and mark finished ones done by id.",
204 inputSchema: {
205 type: 'object',
206 properties: {
207 add: {
208 type: 'array',
209 items: {
210 type: 'object',
211 properties: {
212 category: { type: 'string', description: 'A short category, e.g. Bugs, Features, Tests, Docs' },
213 text: { type: 'string', description: 'One concrete next step' },
214 },
215 required: ['category', 'text'],
216 },
217 },
218 done: { type: 'array', items: { type: 'integer' }, description: 'Ids of items now finished' },
219 },
220 },
221 })
222 await $.command.register({
223 name: 'todo',
224 description: 'Next-steps panel; or list | all | on | off | done <id> | undo <id> | add <Category>: <text> | clear',
225 argumentHint: '[list|all|on|off|done <id>|undo <id>|add <Category>: <text>|clear [all]]',
226 immediate: true,
227 })
228 const list = await fromStore($)
229 const enabled = await isOn($)
230 await update($, listAtom, () => list)
231 await update($, onAtom, () => enabled)
232 await showStatus($, list, enabled)
233 return next(e)
234 })
235
236 on('prompt.submit', async ($, e, next) => {
237 // Plugins' own prompts and slash commands are left alone.
238 if (e.origin?.kind === 'plugin' || e.text.trimStart().startsWith('/')) return next(e)
239 const skip = SKIP.test(e.text)
240 const force = FORCE.test(e.text)
241 const text = e.text.replace(SKIP, ' ').replace(FORCE, ' ').trim() || e.text
242 const track = !skip && (force || (await isOn($)))
243 if (!track) return next({ ...e, text })
244 const board = await boardUrl($)
245 const note = board ? boardInstruction(board) : instruction(await fromStore($))
246 return next({ ...e, text, context: [...(e.context ?? []), note] })
247 })
248
249 on('tool.call', { tool: TOOL }, async ($, e) => {
250 const input = e as unknown as { add?: { category?: string; text?: string }[]; done?: number[] }
251 const added: number[] = []
252 const finished: number[] = []
253 const at = await now($)
254 await edit($, list => {
255 for (const id of input.done ?? []) {
256 const item = list.items.find(i => i.id === id)
257 if (item && !item.done) {
258 item.done = true
259 finished.push(id)
260 }
261 }
262 for (const a of input.add ?? []) {
263 const id = addItem(list, a.category ?? '', a.text ?? '', at)
264 if (id !== undefined) added.push(id)
265 }
266 })
267 const ids = (ns: number[]) => (ns.length ? ns.map(n => `#${n}`).join(', ') : 'nothing')
268 return { result: `Added ${ids(added)}; marked done ${ids(finished)}.` }
269 })
270
271 // Claude's own task list: completing a task (TaskUpdate, or a todo in
272 // TodoWrite) ticks the open item it stands for, so the checklist follows the work.
273 on('tool.call', { tool: 'TaskCreate' }, async ($, e, next) => {
274 const ran = await next(e)
275 const task = ran.deny === undefined && !ran.isError ? (ran.result as { task?: { id?: string } } | undefined)?.task : undefined
276 if (task?.id) await update($, tasksAtom, t => ({ ...t, [task.id as string]: e.subject }))
277 return ran
278 })
279
280 on('tool.call', { tool: 'TaskUpdate' }, async ($, e, next) => {
281 const ran = await next(e)
282 if (ran.deny !== undefined || ran.isError) return ran
283 const tasks = await read($, tasksAtom)
284 const title = e.subject ?? tasks[e.taskId]
285 if (e.subject) await update($, tasksAtom, t => ({ ...t, [e.taskId]: e.subject as string }))
286 if (e.status !== 'completed' || !title) return ran
287 const note = await tickFor($, [title])
288 return note ? { ...ran, context: [...(ran.context ?? []), note] } : ran
289 })
290
291 on('tool.call', { tool: 'TodoWrite' }, async ($, e, next) => {
292 const ran = await next(e)
293 if (ran.deny !== undefined || ran.isError) return ran
294 const was = new Set(
295 ((ran.result as { oldTodos?: { content: string; status: string }[] } | undefined)?.oldTodos ?? [])
296 .filter(t => t.status === 'completed')
297 .map(t => t.content),
298 )
299 const titles = e.todos.filter(t => t.status === 'completed' && !was.has(t.content)).map(t => t.content)
300 const note = await tickFor($, titles)
301 return note ? { ...ran, context: [...(ran.context ?? []), note] } : ran
302 })
303
304 on('command.run', { command: 'todo' }, async ($, e) => {
305 const [verb = '', ...rest] = e.args.trim().split(/\s+/)
306 const arg = e.args.trim().slice(verb.length).trim()
307 const board = await boardUrl($)
308 if (board && !['on', 'off'].includes(verb.toLowerCase())) {
309 const state = (await isOn($)) ? 'on' : 'off'
310 return { text: `Your to-do list is on its board: ${board}\n\nOpen it to tick, add or remove steps. Tracking is ${state} (/todo on, /todo off).` }
311 }
312 switch (verb.toLowerCase()) {
313 case '': {
314 // A cloud session followed from the Claude app has no screen of its own
315 // that draws a mod's panel (no surface attached, or only a phone's), yet
316 // the open still reports placed: print the list there instead.
317 const surfaces = await $.session.surfaces()
318 const canDraw = surfaces.some(s => s === 'terminal' || s === 'desktop' || s === 'vscode')
319 const opened = canDraw ? await $.ui.open({ id: PANE, title: 'Next steps' }) : undefined
320 if (opened?.isPlaced) return { text: `Next-steps panel opened (${surfaces.join(', ')}).` }
321 const where = canDraw
322 ? 'The panel needs a wider window.'
323 : 'This screen does not draw panels (it works in Claude Code in a terminal, the desktop app or VS Code).'
324 return { text: `${render(await fromStore($))}\n\n_${where} /todo done <id>, add <Category>: <text>, on, off._` }
325 }
326 case 'on':
327 case 'off':
328 await setOn($, verb.toLowerCase() === 'on')
329 return { text: `Next-steps tracking is ${verb.toLowerCase()}. (#todo or #notodo in a prompt overrides it for that prompt.)` }
330 case 'done':
331 case 'undo': {
332 const ids = rest.map(s => Number(s.replace('#', ''))).filter(Number.isFinite)
333 const isDone = verb.toLowerCase() === 'done'
334 let hit: number[] = []
335 await edit($, list => {
336 const items = list.items.filter(i => ids.includes(i.id))
337 for (const i of items) i.done = isDone
338 hit = items.map(i => i.id)
339 })
340 return { text: hit.length ? `${isDone ? 'Done' : 'Reopened'}: ${hit.map(n => `#${n}`).join(', ')}` : 'No item with that id.' }
341 }
342 case 'add': {
343 const { category, text } = parseEntry(arg, 'General')
344 if (!text) return { text: 'Usage: /todo add <Category>: <text>' }
345 const at = await now($)
346 let id: number | undefined
347 await edit($, list => {
348 id = addItem(list, category, text, at)
349 })
350 return { text: id === undefined ? 'That step is already open.' : `Added #${id} to ${category}.` }
351 }
352 case 'clear': {
353 const all = arg.toLowerCase() === 'all'
354 await edit($, list => {
355 list.items = all ? [] : list.items.filter(i => !i.done)
356 })
357 return { text: all ? 'Cleared every item.' : 'Cleared finished items.' }
358 }
359 case 'all':
360 return { text: render(await fromStore($), true) }
361 default: {
362 const state = (await isOn($)) ? 'on' : 'off'
363 return { text: `${render(await fromStore($))}\n\n_Tracking is ${state}: /todo on, /todo off._` }
364 }
365 }
366 })
367
368 // The panel: the list by category, each step a checkbox to tick and an x to
369 // remove, a field to add one, and the tracking switch. Every control writes
370 // through edit(), so the panel, the store and the status line stay together.
371 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
372 const { Box, Text, Button } = $.ui.resolve(e)
373 const list = await read($, listAtom)
374 const tracking = await read($, onAtom)
375 const showDone = await read($, showDoneAtom)
376 const category = await read($, categoryAtom)
377 const open = list.items.filter(i => !i.done)
378 const doneCount = list.items.length - open.length
379 const shown = list.items.filter(i => showDone || !i.done)
380 const width = e.props.bodyColumns ?? e.viewport?.columns ?? 60
381 const textWidth = Math.max(12, width - 12)
382 const clip = (s: string) => (s.length > textWidth * 3 ? `${s.slice(0, textWidth * 3 - 1)}…` : s)
383 const categories = [...new Set(['General', ...list.items.map(i => i.category)])].sort()
384
385 const toggle = (item: Item) => () =>
386 edit($, l => {
387 const it = l.items.find(i => i.id === item.id)
388 if (it) it.done = !it.done
389 })
390 const remove = (item: Item) => () =>
391 edit($, l => {
392 l.items = l.items.filter(i => i.id !== item.id)
393 })
394 const add = async (value: string) => {
395 const { category: c, text } = parseEntry(value, category)
396 const at = await now($)
397 await edit($, l => void addItem(l, c, text, at))
398 await update($, categoryAtom, () => c)
399 }
400
401 const header = (
402 <Box flexDirection="row" justifyContent="space-between">
403 <Text bold>
404 {open.length} open{doneCount ? ` · ${doneCount} done` : ''}
405 </Text>
406 <Box flexDirection="row" gap={1}>
407 <Button
408 key="tracking"
409 hotkey="t"
410 variant={tracking ? 'primary' : 'secondary'}
411 label={tracking ? 'Tracking on' : 'Tracking off'}
412 onPress={() => setOn($, !tracking)}
413 />
414 </Box>
415 </Box>
416 )
417
418 const rows =
419 shown.length === 0 ? (
420 <Text dimColor>
421 {list.items.length === 0
422 ? 'Nothing yet. Steps Claude suggests after each prompt land here.'
423 : 'Everything is done.'}
424 </Text>
425 ) : (
426 <Box flexDirection="column">
427 {grouped(shown).map(([name, items]) => (
428 <Box key={`cat-${name}`} flexDirection="column" marginTop={1}>
429 <Text bold color="cyan">
430 {name} <Text dimColor>({items.filter(i => !i.done).length})</Text>
431 </Text>
432 {items.map(item => (
433 <Box key={`row-${item.id}`} flexDirection="row" gap={1}>
434 <Button key={`done-${item.id}`} plain label={item.done ? '☑' : '☐'} onPress={toggle(item)} />
435 <Box flexGrow={1}>
436 <Text dimColor={item.done} strikethrough={item.done} wrap="wrap">
437 {clip(item.text)}
438 </Text>
439 </Box>
440 <Text dimColor>#{item.id}</Text>
441 <Button key={`del-${item.id}`} plain dimColor label="✕" onPress={remove(item)} />
442 </Box>
443 ))}
444 </Box>
445 ))}
446 </Box>
447 )
448
449 const footer = (
450 <Box flexDirection="row" gap={1} marginTop={1}>
451 <Button
452 key="show-done"
453 hotkey="d"
454 label={showDone ? 'Hide done' : 'Show done'}
455 onPress={() => update($, showDoneAtom, v => !v)}
456 />
457 {doneCount > 0 && (
458 <Button key="clear" hotkey="c" label="Clear done" onPress={() => edit($, l => void (l.items = l.items.filter(i => !i.done)))} />
459 )}
460 </Box>
461 )
462
463 if (e.surface === 'mobile') {
464 return (
465 <Box flexDirection="column">
466 {header}
467 {rows}
468 {footer}
469 </Box>
470 )
471 }
472
473 const { Input, Select } = $.ui.resolve(e)
474 return (
475 <Box flexDirection="column">
476 {header}
477 {rows}
478 <Box flexDirection="column" marginTop={1}>
479 <Select
480 key="category"
481 label="Category "
482 value={category}
483 options={categories.map(c => ({ value: c, label: c }))}
484 onSelect={v => update($, categoryAtom, () => v)}
485 />
486 <Input
487 key="new"
488 label="+ "
489 placeholder={`Add a step to ${category} (or "Bugs: fix it")`}
490 value=""
491 submitLabel="add"
492 onSubmit={value => void add(value)}
493 />
494 </Box>
495 {footer}
496 </Box>
497 )
498 })
499}
500types/index.d.ts 9 lines1export type Item = { id: number; category: string; text: string; done: boolean; at: string }
2export type List = { next: number; items: Item[] }
3
4declare module 'claude-code' {
5 interface PluginState {
6 'next-steps': { list: List; isOn: boolean; showDone: boolean; category: string; tasks: Record<string, string> }
7 }
8}
9