One band above the prompt: rate limits, context, PR checks and Datadog auth

The setting files for my development PC.
Work with bash and setup_*.sh scripts.
$ mkdir -p ~/dev/src/github/com/chroju
$ cd ~/dev/src/github/com/chroju
$ git clone https://github.com/chroju/dotfiles
$ cd dotfiles
# must execute setup_default.sh with sudo
$ sudo ./setup_default.sh
$ ./setup_symlinks.sh
$ ./setup_homebrew.sh
$ ./setup_gh_extensions.sh
Other repositories can reuse this dotfiles setup via the devcontainer dotfiles feature.
Run setup_symlinks.sh on the host first. This installs devcontainer-init to /usr/local/bin/.
Copy .devcontainer/devcontainer.json from this repository to your project. Change name and add project-specific features as needed. Add "dotfiles.repository": "chroju/dotfiles" to enable the dotfiles feature.
devcontainer-init (host, initializeCommand): Sets up SSH agent forwarding via Podman VM and writes GH_TOKEN / SSH_AUTH_SOCK to .devcontainer/.env.devcontainer.install.sh (container, dotfiles feature): Creates symlinks from dotfiles/HOME/ to $HOME, installs Claude Code and mise, configures SSH agent socket.hooks/register.tsx 191 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register, SessionRateLimit } from 'claude-code'
3
4import type { Checks, HudView, Limit, Pr } from '../types'
5
6type CheckItem = { status?: string; conclusion?: string; state?: string }
7
8const PR_POLL_MS = 60_000
9const DATADOG_CHECK_MS = 5 * 60_000
10
11const view = atom({ plugin: 'session-hud', key: 'view' } as const, { limits: [], isDatadogOut: false } as HudView)
12
13const pad = (n: number) => String(n).padStart(2, '0')
14
15export const pie = (percent: number): string =>
16 percent < 12.5 ? '○' : percent < 37.5 ? '◔' : percent < 62.5 ? '◑' : percent < 87.5 ? '◕' : '●'
17
18export const colorOf = (percent: number) => (percent >= 90 ? 'red' : percent >= 70 ? 'yellow' : 'green')
19
20export const summarizeChecks = (items: readonly CheckItem[]): Checks => {
21 const checks = { pass: 0, fail: 0, pending: 0 }
22 for (const item of items) {
23 const verdict = item.conclusion || item.state || ''
24 if (item.status !== undefined && item.status !== 'COMPLETED') checks.pending += 1
25 else if (['SUCCESS', 'NEUTRAL', 'SKIPPED'].includes(verdict)) checks.pass += 1
26 else if (['PENDING', 'EXPECTED', ''].includes(verdict)) checks.pending += 1
27 else checks.fail += 1
28 }
29 return checks
30}
31
32export const describePr = ({ number, checks }: Pr): { text: string; color?: string } => {
33 const total = checks.pass + checks.fail + checks.pending
34 if (total === 0) return { text: `PR #${number}` }
35 if (checks.fail > 0) return { text: `PR #${number} ✗ ${checks.fail} failed`, color: 'red' }
36 if (checks.pending > 0) return { text: `PR #${number} ⏳ ${checks.pass}/${total}`, color: 'yellow' }
37 return { text: `PR #${number} ✓`, color: 'green' }
38}
39
40export const resetTime = (l: Limit): string | undefined => {
41 if (l.resetsAt === undefined) return undefined
42 const d = new Date(l.resetsAt)
43 if (Number.isNaN(d.getTime())) return undefined
44 const time = `${pad(d.getHours())}:${pad(d.getMinutes())}`
45 return l.kind === 'five_hour' ? time : `${d.getMonth() + 1}/${d.getDate()} ${time}`
46}
47
48const SVG_COLORS: Record<string, string> = { green: '#3fb950', yellow: '#d29922', red: '#f85149' }
49
50// A pie drawn as SVG, for surfaces whose fonts draw ○◔◑◕● at different sizes
51export const pieSvg = (percent: number): string => {
52 const p = Math.min(Math.max(percent, 0), 100) / 100
53 const fill = SVG_COLORS[colorOf(percent)]
54 const r = 6
55 const c = 7
56 const track = `<circle cx="${c}" cy="${c}" r="${r}" fill="none" stroke="${fill}" stroke-width="1.5"/>`
57 let slice = ''
58 if (p >= 1) {
59 slice = `<circle cx="${c}" cy="${c}" r="${r}" fill="${fill}"/>`
60 } else if (p > 0) {
61 const angle = 2 * Math.PI * p
62 const x = (c + r * Math.sin(angle)).toFixed(2)
63 const y = (c - r * Math.cos(angle)).toFixed(2)
64 slice = `<path d="M${c} ${c} L${c} ${c - r} A${r} ${r} 0 ${p > 0.5 ? 1 : 0} 1 ${x} ${y} Z" fill="${fill}"/>`
65 }
66 return `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14">${track}${slice}</svg>`
67}
68
69const label = (kind: string) => (kind === 'five_hour' ? '5h' : kind === 'seven_day' ? '7d' : kind)
70
71// Work that is not drawn: kept in the module, started over on a reload
72type Runtime = { poll?: { cancel: () => void }; datadogCheckedAt?: number }
73
74async function setUsage($: EngineInterface, limits: readonly SessionRateLimit[], percent?: number) {
75 await update($, view, v => ({ ...v, limits: [...limits], contextPercent: percent }))
76}
77
78async function refreshPr($: EngineInterface, rt: Runtime) {
79 const before = (await read($, view)).pr
80 let pr: Pr | undefined
81 try {
82 const r = await $.process.run(['gh', 'pr', 'view', '--json', 'number,url,statusCheckRollup'], { timeoutMs: 15_000 })
83 if (r.exitCode === 0) {
84 const json = JSON.parse(r.stdout) as { number: number; url: string; statusCheckRollup?: CheckItem[] }
85 pr = { number: json.number, url: json.url, checks: summarizeChecks(json.statusCheckRollup ?? []) }
86 }
87 } catch {
88 pr = undefined
89 }
90 await update($, view, v => ({ ...v, pr }))
91
92 const wasPending = before !== undefined && before.number === pr?.number && before.checks.pending > 0
93 if (wasPending && pr !== undefined && pr.checks.pending === 0) {
94 $.ui.toast(pr.checks.fail > 0 ? `PR #${pr.number}: CI failed (${pr.checks.fail})` : `PR #${pr.number}: CI passed`)
95 }
96
97 const shouldPoll = pr !== undefined && pr.checks.pending > 0
98 if (shouldPoll && rt.poll === undefined) {
99 rt.poll = $.clock.every(PR_POLL_MS, () => refreshPr($, rt))
100 } else if (!shouldPoll && rt.poll !== undefined) {
101 rt.poll.cancel()
102 rt.poll = undefined
103 }
104}
105
106async function checkDatadog($: EngineInterface, rt: Runtime) {
107 const now = await $.clock.now()
108 const isOut = (await read($, view)).isDatadogOut
109 if (rt.datadogCheckedAt !== undefined && now - rt.datadogCheckedAt < DATADOG_CHECK_MS && !isOut) return
110 rt.datadogCheckedAt = now
111 try {
112 const r = await $.process.run(['pup', 'auth', 'status', '--site', 'datadoghq.com'], { timeoutMs: 15_000 })
113 const json = JSON.parse(r.stdout.slice(r.stdout.indexOf('{'))) as { authenticated?: boolean }
114 const isNowOut = json.authenticated !== true
115 if (isNowOut && !isOut) $.ui.toast('Datadog: not authenticated (pup auth login)')
116 await update($, view, v => ({ ...v, isDatadogOut: isNowOut }))
117 } catch {
118 // pup missing or unreadable output: say nothing rather than guess
119 }
120}
121
122export const register: Register = on => {
123 const rt: Runtime = {}
124
125 on('session.start', async ($, e, next) => {
126 const result = await next(e)
127 const usage = await $.session.usage()
128 await setUsage($, usage.rateLimits, usage.context.percent)
129 void refreshPr($, rt)
130 return result
131 })
132
133 on('session.measure', async ($, e, next) => {
134 await setUsage($, e.rateLimits, e.context.percent)
135 return next(e)
136 })
137
138 on('turn.complete', async ($, e, next) => {
139 const result = await next(e)
140 void refreshPr($, rt)
141 return result
142 })
143
144 on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
145 if (/^\s*pup\s/.test(e.command) && !/^\s*pup\s+auth\b/.test(e.command)) await checkDatadog($, rt)
146 return next(e)
147 }).catch(($, e, next) => next(e))
148
149 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
150 const v = await read($, view)
151 const hasAny = v.limits.length > 0 || v.contextPercent !== undefined || v.pr || v.isDatadogOut
152 if (e.props.hasSurvey || !hasAny) return next(e)
153
154 const elements = $.ui.resolve(e)
155 const { Box, Text, Link } = elements
156 const Svg = e.surface !== 'terminal' && 'Svg' in elements ? elements.Svg : undefined
157 const meter = (key: string, name: string, percent: number, extra?: string) => (
158 <Box key={key} flexDirection="row" columnGap={1} alignItems="center">
159 <Text bold>{name}</Text>
160 {Svg ? (
161 <Svg source={pieSvg(percent)} alt={`${Math.round(percent)}%`} width={14} height={14} />
162 ) : (
163 <Text color={colorOf(percent)}>{pie(percent)}</Text>
164 )}
165 <Text color={colorOf(percent)}>{`${Math.round(percent)}%`}</Text>
166 {extra ? <Text dimColor>{extra}</Text> : null}
167 </Box>
168 )
169 const pr = v.pr ? describePr(v.pr) : undefined
170
171 return (
172 <Box key="session-hud" flexDirection="row" flexWrap="wrap" columnGap={3}>
173 {v.limits.map(l => meter(`limit-${l.kind}`, label(l.kind), l.percentUsed, resetTime(l)))}
174 {v.contextPercent !== undefined ? meter('context', 'ctx', v.contextPercent) : null}
175 {v.pr && pr ? (
176 <Box key="pr">
177 <Link href={v.pr.url}>
178 <Text color={pr.color}>{pr.text}</Text>
179 </Link>
180 </Box>
181 ) : null}
182 {v.isDatadogOut ? (
183 <Box key="datadog">
184 <Text color="red">dd ✗ pup auth login</Text>
185 </Box>
186 ) : null}
187 </Box>
188 )
189 })
190}
191types/index.d.ts 16 lines1export type Checks = { pass: number; fail: number; pending: number }
2export type Pr = { number: number; url: string; checks: Checks }
3export type Limit = { kind: string; percentUsed: number; resetsAt?: string }
4export type HudView = {
5 limits: Limit[]
6 contextPercent?: number
7 pr?: Pr
8 isDatadogOut: boolean
9}
10
11declare module 'claude-code' {
12 interface PluginState {
13 'session-hud': { view: HudView }
14 }
15}
16