SLOPSHOPPER

done-pill

A pill above the prompt that says whether Claude is working, done, or waiting on you.

newbandguardprompttool
v0.1.0MITupdated 2026-10-16ksenousi/kanigami/mods/done-pill
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · done-pill
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM ● Ended · not confirmed done Claude ended the turn without saying whether anything is pending. ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
● Ended · not confirmed done Claude ended the turn without saying whether anything is pending.
README

done-pill

A Claude Code mod, not part of kanigami: a pill above the prompt that says whether Claude is still working, done with nothing pending, or waiting on you — and, once a turn ends, the reply in one line (TL;DR) and explained simply (ELI5).

  • Working while a turn runs; Done · safe to close, Needs you, Running in the background, Stopped, or Ended · not confirmed done after it.
  • Claude reports through a status tool the mod adds. A turn that tries to end without reporting is refused once, so it reports; one that still does not is shown as unconfirmed, never as done.
  • Background work is checked, not taken on trust: a turn that says done while a background task is still running shows as running in the background, naming it. An artifact's live-update watch is not counted.

Install

At the prompt of a terminal Claude Code session:

/plugin install done-pill --marketplace ksenousi/kanigami

Answer y to add the marketplace, then pick the user scope. Sessions the desktop app starts load it too.

Develop

claude plugin validate mods/done-pill
claude plugin test mods/done-pill
Source 2 files
hooks/register.tsx 163 lines
1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Pill, Verdict } from '../types'
5
6// The pill: whether Claude is still working, finished with nothing pending,
7// or finished but waiting on you — and under it, once a turn has ended, the
8// reply in one line (TL;DR) and explained simply (ELI5). The engine knows when a turn is running;
9// only Claude knows whether it left a question open, so it says so through
10// the status tool as the last thing it does each turn. A turn that ends
11// without saying is shown as unconfirmed, never as done.
12//
13// Enforced, not just asked: when Claude tries to end a turn without having
14// said, the Stop is refused once with a reminder, so it says and then ends.
15// And background work is not left to its word: the Stop carries the
16// session's running tasks, and a turn that called itself done while any are
17// still going is shown as running in the background.
18
19const declared = atom({ plugin: 'done-pill', key: 'declared' } as const, null)
20const pill = atom({ plugin: 'done-pill', key: 'pill' } as const, null)
21const running = atom({ plugin: 'done-pill', key: 'running' } as const, [])
22
23const TOOL = 'mcp__done-pill__status'
24const STATES = ['done', 'needs-you', 'background'] as const
25
26const INSTRUCTION = `# Status pill
27
28The person cannot tell from the transcript alone whether you are finished, so a pill above their prompt shows it. In every turn where you reply to them, call ${TOOL} exactly once, just before your final message, then write that message and end the turn.
29
30- state "done": everything they asked for is finished and checked, nothing you started is still running, and nothing waits on them. They can close the laptop.
31- state "needs-you": you asked a question, offered options, need a decision, an approval or an action from them, or something failed or is left unresolved.
32- state "background": work you started is still running (a background command, a subagent, a deploy you are watching) and you will report back when it ends.
33
34reason: one short plain sentence saying what is done, or exactly what is pending. Be strict: if anything at all is open, it is not "done".
35
36tldr: your final message in one line — the outcome, not the process.
37
38eli5: the same explained simply, as to someone who has not followed the details: a sentence or two, no jargon, no file names.`
39
40const LOOK: Record<Pill['kind'] | 'working', { label: string; color: 'claude' | 'success' | 'warning' | 'suggestion' | 'subtle' | 'inactive' }> = {
41  working: { label: 'Working', color: 'claude' },
42  done: { label: 'Done · safe to close', color: 'success' },
43  'needs-you': { label: 'Needs you', color: 'warning' },
44  background: { label: 'Running in the background', color: 'suggestion' },
45  unconfirmed: { label: 'Ended · not confirmed done', color: 'subtle' },
46  stopped: { label: 'Stopped', color: 'inactive' },
47}
48
49export const register: Register = on => {
50  on('session.start', async ($, e, next) => {
51    await $.tool.register({
52      name: 'status',
53      description:
54        'Report whether this turn leaves anything pending, for the status pill above the prompt. Call once per turn, just before the final message.',
55      inputSchema: {
56        type: 'object',
57        properties: {
58          state: { type: 'string', enum: [...STATES] },
59          reason: { type: 'string', description: 'One short sentence: what is done, or what is pending.' },
60          tldr: { type: 'string', description: 'The final message in one line: the outcome, not the process.' },
61          eli5: { type: 'string', description: 'The same explained simply, in a sentence or two, no jargon.' },
62        },
63        required: ['state', 'reason', 'tldr', 'eli5'],
64      },
65      isDeferred: false,
66    })
67    return next(e)
68  })
69
70  on('prompt.compose', async ($, e, next) => {
71    const composed = await next(e)
72    return { ...composed, sections: [...composed.sections, { id: 'done-pill', text: INSTRUCTION, scope: 'session' }] }
73  })
74
75  on('tool.call', { tool: TOOL }, async ($, e) => {
76    // A subagent's verdict is about its own errand, not the conversation.
77    if (e.agentId) return { result: 'Ignored: only the main conversation sets the pill.' }
78    const state = STATES.find(s => s === e.state)
79    if (!state) return { deny: `state must be one of ${STATES.join(', ')}` }
80    const text = (value: unknown) => (typeof value === 'string' ? value.trim() : '')
81    const verdict: Verdict = { state, reason: text(e.reason), tldr: text(e.tldr), eli5: text(e.eli5) }
82    await update($, declared, () => verdict)
83    return { result: 'Recorded.' }
84  }).catch(() => ({ deny: 'done-pill could not record the status; say plainly in your message whether anything is pending.' }))
85
86  on('turn.start', async ($, e, next) => {
87    await update($, declared, () => null)
88    await update($, running, () => [])
89    return next(e)
90  })
91
92  // The main loop's Stop: refuse it once if nothing was reported, and note
93  // what is still running either way.
94  on('classic.Stop', async ($, e, next) => {
95    // Only what is still going, named as the person would recognise it.
96    const tasks = (e.background_tasks ?? [])
97      .filter(t => t.status === 'running' || t.status === 'pending')
98      // An artifact's live-update watch listens for edits for the rest of the
99      // session and never finishes: it is not work anyone is waiting on.
100      .filter(t => !(t.type === 'monitor' && /^live updates for artifact/i.test(t.description)))
101      .map(t => `${t.type}: ${(t.description || t.command || t.agent_type || t.id).slice(0, 60)}`)
102    await update($, running, () => tasks)
103    if (!(await read($, declared)) && !e.stop_hook_active) {
104      return { block: `Before ending the turn, call ${TOOL} with state and reason (done, needs-you or background), then end it.` }
105    }
106    return next(e)
107  })
108
109  on('turn.complete', async ($, e, next) => {
110    const answered = await next(e)
111    if (e.agentId) return answered
112    const verdict = await read($, declared)
113    let shown: Pill = e.isAborted
114      ? { kind: 'stopped', reason: 'You stopped the turn.' }
115      : e.reason !== 'answer'
116        ? { kind: 'needs-you', reason: 'The turn ended on an error.' }
117        : verdict
118          ? { kind: verdict.state, reason: verdict.reason, tldr: verdict.tldr, eli5: verdict.eli5 }
119          : { kind: 'unconfirmed', reason: 'Claude ended the turn without saying whether anything is pending.' }
120    // Done is not done while something is still running.
121    const tasks = await read($, running)
122    if (shown.kind === 'done' && tasks.length > 0) {
123      shown = { ...shown, kind: 'background', reason: `Said done, but still running: ${tasks.join(' · ')}` }
124    }
125    await update($, pill, () => shown)
126    return answered
127  })
128
129  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
130    if (e.props.hasSurvey) return next(e)
131    const last = await read($, pill)
132    const kind = e.props.isWorking ? 'working' : last?.kind
133    if (!kind) return next(e)
134
135    const { Box, Text } = $.ui.resolve(e)
136    const look = LOOK[kind]
137    // While a turn runs, the last one's summary would describe the wrong reply.
138    const ended = kind === 'working' ? null : last
139    const reason = ended?.reason ?? ''
140
141    return (
142      <Box flexDirection="column" gap={0}>
143        <Text key="status" color={look.color} bold wrap="truncate-end">
144          ● {look.label}
145          {reason ? <Text dimColor bold={false}>{`  ${reason}`}</Text> : null}
146        </Text>
147        {ended?.tldr ? (
148          <Text key="tldr" wrap="wrap">
149            <Text bold>TL;DR </Text>
150            {ended.tldr}
151          </Text>
152        ) : null}
153        {ended?.eli5 ? (
154          <Text key="eli5" wrap="wrap">
155            <Text bold>ELI5 </Text>
156            <Text dimColor>{ended.eli5}</Text>
157          </Text>
158        ) : null}
159      </Box>
160    )
161  })
162}
163
types/index.d.ts 31 lines
1/** What Claude said at the end of its turn, through the status tool. */
2export type Verdict = {
3  state: 'done' | 'needs-you' | 'background'
4  reason: string
5  /** The reply in one line. */
6  tldr: string
7  /** The reply explained simply, in a sentence or two. */
8  eli5: string
9}
10
11/** What the pill shows once a turn has ended. */
12export type Pill = {
13  kind: 'done' | 'needs-you' | 'background' | 'unconfirmed' | 'stopped'
14  reason: string
15  tldr?: string
16  eli5?: string
17}
18
19declare module 'claude-code' {
20  interface PluginState {
21    'done-pill': {
22      /** This turn's verdict so far, cleared when a turn starts. */
23      declared: Verdict | null
24      /** The last finished turn's pill; null before the first turn ends. */
25      pill: Pill | null
26      /** Background work still running when the turn last tried to stop. */
27      running: string[]
28    }
29  }
30}
31