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

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.
hooks/register.tsx 138 lines1import { 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}
138types/index.d.ts 17 lines1export 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