SLOPSHOPPER

brief

At a new session, a band above the prompt shows the project, git state and where the last session here stopped

newbandpromptprocess
v0.1.0MITupdated 2026-10-22rezaulhreza/claude-code-mods/brief
A shopper browsing a rack in a slop shop
README

brief

At a new session, a band above the prompt shows the project, the git branch, changed files, commits ahead, the last commit, and where the last session in this folder stopped (the first line of Claude's final reply). The band hides at your first prompt. Turn off the last-stop line with the showLast option.

Source 2 files
hooks/register.tsx 138 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Brief } from '../types'
5
6const info = atom({ plugin: 'brief', key: 'info' } as const, null)
7const isHidden = atom({ plugin: 'brief', key: 'isHidden' } as const, false)
8
9const lastKey = (cwd: string) => `last:${cwd}`
10
11export function ago(ms: number): string {
12  const m = Math.floor(ms / 60000)
13
14  if (m < 1) return 'just now'
15  if (m < 60) return `${m}m ago`
16  if (m < 48 * 60) return `${Math.floor(m / 60)}h ago`
17
18  return `${Math.floor(m / (24 * 60))}d ago`
19}
20
21export function firstLine(text: string, max = 90): string {
22  const line = text.split('\n').map(one => one.replace(/^[#>*\-\s]+/, '').trim()).find(Boolean) ?? ''
23
24  return line.length > max ? line.slice(0, max - 1) + '…' : line
25}
26
27async function git($: EngineInterface, cwd: string, args: string[]): Promise<string | null> {
28  try {
29    const ran = await $.process.run(['git', ...args], { cwd, timeoutMs: 3000 })
30
31    return ran.exitCode === 0 ? ran.stdout.trim() : null
32  } catch {
33    return null
34  }
35}
36
37async function gather($: EngineInterface, cwd: string): Promise<Brief> {
38  const now = await $.clock.now()
39  const stored = (await $.store.get(lastKey(cwd))) as { at: number; text: string } | undefined
40  const branch = await git($, cwd, ['rev-parse', '--abbrev-ref', 'HEAD'])
41  const status = branch === null ? null : await git($, cwd, ['status', '--porcelain'])
42  const ahead = branch === null ? null : await git($, cwd, ['rev-list', '--count', '@{upstream}..HEAD'])
43  const log = branch === null ? null : await git($, cwd, ['log', '-1', '--format=%ct%x09%s'])
44  const [stamp, ...subject] = (log ?? '').split('\t')
45
46  return {
47    name: cwd.split('/').filter(Boolean).pop() ?? cwd,
48    path: cwd.replace(/^\/(Users|home)\/[^/]+/, '~'),
49    branch,
50    changed: status ? status.split('\n').filter(Boolean).length : 0,
51    ahead: Number(ahead ?? 0) || 0,
52    commit: log ? { at: Number(stamp) * 1000, subject: subject.join('\t') } : null,
53    last: stored && typeof stored.text === 'string' ? stored : null,
54    now,
55  }
56}
57
58export const register: Register = (on, options) => {
59  const ACCENT = String(options.accent || '#D97757')
60  let cwd = ''
61
62  on('session.start', async ($, e, next) => {
63    const started = await next(e)
64    cwd = e.cwd
65
66    if (e.isInteractive) {
67      await update($, isHidden, () => false)
68      await update($, info, () => null)
69      void gather($, cwd).then(brief => update($, info, () => brief))
70    }
71
72    return started
73  })
74
75  on('prompt.submit', async ($, e, next) => {
76    await update($, isHidden, () => true)
77
78    return next(e)
79  }).catch(($, e, next) => next(e))
80
81  // Keep where this session stopped, for the next session in this folder.
82  on('turn.complete', async ($, e, next) => {
83    const done = await next(e)
84    const text = firstLine(e.answer)
85
86    if (!e.agentId && text && cwd && options.showLast !== false) {
87      await $.store.set(lastKey(cwd), { at: await $.clock.now(), text })
88    }
89
90    return done
91  })
92
93  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
94    const brief = await read($, info)
95
96    if (!brief || e.props.hasSurvey || e.props.isWorking || e.props.view.agentId || (await read($, isHidden))) {
97      return next(e)
98    }
99
100    const { Box, Text } = $.ui.resolve(e)
101    const ahead = brief.ahead ? ` · ${brief.ahead} ahead` : ''
102    const where = brief.branch === null ? ' · no git' : ''
103    const first = brief.last === null ? ' · first session here' : ''
104
105    return (
106      <Box flexDirection="column">
107        <Text wrap="truncate-end">
108          <Text bold color={ACCENT}>
109            {brief.name}
110          </Text>
111          <Text dimColor>{`  ${brief.path}${where}${first}`}</Text>
112        </Text>
113        {brief.branch !== null && (
114          <Text wrap="truncate-end">
115            <Text dimColor>{'branch  '}</Text>
116            {brief.branch}
117            <Text dimColor>{` · ${brief.changed ? `${brief.changed} changed` : 'clean'}${ahead}`}</Text>
118          </Text>
119        )}
120        {brief.commit && (
121          <Text wrap="truncate-end">
122            <Text dimColor>{'commit  '}</Text>
123            <Text dimColor>{`${ago(brief.now - brief.commit.at)}  `}</Text>
124            {brief.commit.subject}
125          </Text>
126        )}
127        {brief.last && options.showLast !== false && (
128          <Text wrap="truncate-end">
129            <Text dimColor>{'last    '}</Text>
130            <Text dimColor>{`${ago(brief.now - brief.last.at)}  `}</Text>
131            {brief.last.text}
132          </Text>
133        )}
134      </Box>
135    )
136  })
137}
138
types/index.d.ts 17 lines
1export type Brief = {
2  name: string
3  path: string
4  branch: string | null
5  changed: number
6  ahead: number
7  commit: { at: number; subject: string } | null
8  last: { at: number; text: string } | null
9  now: number
10}
11
12declare module 'claude-code' {
13  interface PluginState {
14    brief: { info: Brief | null; isHidden: boolean }
15  }
16}
17