SLOPSHOPPER

session-recap

Band above the prompt: open questions Claude asked you plus bullets of what the last prompt did

newbandguardpromptmodel
A shopper browsing a rack in a slop shop
README

session-recap

A band above the prompt that keeps the important bits from scrolling away: a short list of what the last prompt did, and the questions Claude asked you that still need an answer.

What it does

After each prompt finishes, the band shows:

Done last prompt
- Added a status line that shows each subagent's model
- Kept the line visible after agents finish
- Validated the mod and ran a headless check
Open questions
? Should I add the new mod to your default load?
? Want a README for each mod?
[Clear questions]
  • Done last prompt: 2 to 5 plain-English bullets for your most recent prompt only.
  • Open questions: every question Claude has asked you this session that needed an answer. It can't tell when you've answered one, so use Clear questions when you're through them.
  • The band is empty until the first prompt completes, and steps aside for any survey prompt.

Why

When a run produces a lot of output, a question or a result in the middle gets buried as the conversation scrolls. This puts both in a fixed spot.

Install and run

claude --plugin-dir /path/to/claude-code-mods/session-recap

To load it in every session, see Install.

Try it:

List the files in this folder, read the README, and tell me in two sentences what it's for. Then ask me which thing to work on next, and whether you should add tests.

Cost

One small Claude Haiku 4.5 call per finished prompt (a few hundred tokens). It reads your prompt, a one-line list of the tool calls and Claude's final message. Interrupted prompts and subagent turns are skipped.

Notes

  • The band is capped at about half the terminal height; a taller one scrolls.
  • The summary is only as good as Haiku's reading of the turn. Tune the instructions in hooks/register.tsx if the bullets are too long or too vague.

How it works

prompt.submit and tool.call collect what the prompt did, turn.complete makes the Haiku call through $.model.complete and stores the result, and a ui.render hook on the AbovePrompt band draws it.

Source 2 files
hooks/register.tsx 120 lines
1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Recap } from '../types'
5
6const turns = atom({ plugin: 'session-recap', key: 'turns' } as const, [] as Recap[])
7const questions = atom({ plugin: 'session-recap', key: 'questions' } as const, [] as string[])
8
9// What the current prompt has done so far; reset on each prompt.
10let prompt = ''
11let steps: string[] = []
12
13const clip = (text: string, max: number) =>
14  text.length > max ? `${text.slice(0, max)}...` : text
15
16// One short line per tool call, for the summarizer to read.
17function describe(call: Record<string, unknown>) {
18  const target = call.description ?? call.file_path ?? call.command ?? call.pattern ?? call.url ?? ''
19
20  return clip(`${String(call.tool)} ${String(target)}`.trim(), 120)
21}
22
23function parseRecap(text: string) {
24  try {
25    const json = text.slice(text.indexOf('{'), text.lastIndexOf('}') + 1)
26    const parsed = JSON.parse(json) as { done?: unknown; asked?: unknown }
27    const strings = (v: unknown) =>
28      Array.isArray(v) ? v.filter((s): s is string => typeof s === 'string' && s.trim() !== '') : []
29
30    return { done: strings(parsed.done), asked: strings(parsed.asked) }
31  } catch {
32    return null
33  }
34}
35
36export const register: Register = on => {
37  on('prompt.submit', ($, e, next) => {
38    prompt = e.text
39    steps = []
40
41    return next(e)
42  })
43
44  on('tool.call', ($, e, next) => {
45    if (!e.agentId) {
46      steps = [...steps, describe(e as unknown as Record<string, unknown>)].slice(-40)
47    }
48
49    return next(e)
50  })
51
52  on('turn.complete', async ($, e, next) => {
53    const done = await next(e)
54
55    if (e.agentId || e.reason !== 'answer') {
56      return done
57    }
58
59    const result = await $.model.complete({
60      model: 'haiku',
61      maxTokens: 400,
62      system: 'You write terse recaps for a busy developer. Reply with JSON only.',
63      prompt: [
64        `The user asked: ${clip(prompt, 500)}`,
65        `Tool calls made:\n${steps.join('\n') || '(none)'}`,
66        `The assistant's final message:\n${clip(e.answer, 3000)}`,
67        '',
68        'Reply with JSON: {"done": [...], "asked": [...]}',
69        '"done": 2-5 plain-English bullets of what was actually done or found, each under 12 words, no file paths or jargon unless essential.',
70        '"asked": every question the assistant put to the user in its final message that still needs an answer (a choice, a confirmation, an offer like "want me to...?"), each rewritten under 20 words. Empty list if none.',
71      ].join('\n'),
72    })
73
74    if (!result.isAnswered) {
75      return done
76    }
77
78    const recap = parseRecap(result.text)
79
80    if (recap && recap.done.length > 0) {
81      await update($, turns, list =>
82        [...list, { prompt: clip(prompt, 80), bullets: recap.done }].slice(-30),
83      )
84      await update($, questions, list =>
85        [...list, ...recap.asked.filter(q => !list.includes(q))].slice(-30),
86      )
87    }
88
89    return done
90  })
91
92  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
93    const asked = await read($, questions)
94    const past = await read($, turns)
95    const latest = past[past.length - 1]
96
97    if (e.props.hasSurvey || (asked.length === 0 && !latest)) {
98      return next(e)
99    }
100
101    const { Box, Button, Text } = $.ui.resolve(e)
102
103    return (
104      <Box flexDirection="column">
105        {latest && <Text bold>Done last prompt</Text>}
106        {latest?.bullets.map((b, j) => (
107          <Text key={`b${j}`} dimColor>- {b}</Text>
108        ))}
109        {asked.length > 0 && <Text bold>Open questions</Text>}
110        {asked.map((q, i) => (
111          <Text key={`q${i}`}>? {q}</Text>
112        ))}
113        {asked.length > 0 && (
114          <Button key="clear" label="Clear questions" onPress={() => update($, questions, () => [])} />
115        )}
116      </Box>
117    )
118  })
119}
120
types/index.d.ts 8 lines
1export type Recap = { prompt: string; bullets: string[] }
2
3declare module 'claude-code' {
4  interface PluginState {
5    'session-recap': { turns: Recap[]; questions: string[] }
6  }
7}
8