One line above the prompt: repository and branch highlighted, context, 5h limit and weekly limit

Marketplace local de mods do Claude Code: status-sessao, sub-agents e replay-theater. O replay-theater é uma cópia do mod de anthropics/claude-code-playground, alterada para não esconder a banda do status-sessao.
Também uso dois mods de terceiros, que não estão neste repo: Blast Radius (de anthropics/claude-code-playground) e Cache Tax (de karanb192/claude-code-mods). Os passos abaixo instalam tudo.
Clone este repo e o playground para a pasta de marketplaces locais. O Cache Tax não precisa de clone, porque o marketplace dele é lido direto do GitHub.
Linux:
git clone https://github.com/joaodecarv/joao-mods.git ~/.claude/local-marketplaces/joao-mods
git clone https://github.com/anthropics/claude-code-playground.git ~/.claude/local-marketplaces/claude-code-playground
Windows (PowerShell):
git clone https://github.com/joaodecarv/joao-mods.git "$env:USERPROFILE\.claude\local-marketplaces\joao-mods"
git clone https://github.com/anthropics/claude-code-playground.git "$env:USERPROFILE\.claude\local-marketplaces\claude-code-playground"
Depois junte isto ao ~/.claude/settings.json. Troque <HOME> pelo seu diretório: /home/joao no Linux, ou C:/Users/<você> no Windows (com barras /).
"extraKnownMarketplaces": {
"joao-mods": {
"source": { "source": "directory", "path": "<HOME>/.claude/local-marketplaces/joao-mods" }
},
"claude-code-playground-mods": {
"source": { "source": "directory", "path": "<HOME>/.claude/local-marketplaces/claude-code-playground/claude-code/mods" }
},
"claude-code-mods": {
"source": { "source": "github", "repo": "karanb192/claude-code-mods" }
}
},
"enabledPlugins": {
"sub-agents@joao-mods": true,
"status-sessao@joao-mods": true,
"replay-theater@joao-mods": true,
"blast-radius@claude-code-playground-mods": true,
"cache-tax@claude-code-mods": true
}
Reinicie o Claude Code. Se algum plugin não aparecer, instale pelo /plugin em um terminal claude.
Neste repo: git pull antes de mexer, git commit + git push depois. Para atualizar o playground, rode git pull no clone dele.
hooks/register.tsx 118 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Git, Usage } from '../types'
5import { bar, basename, isEmpty, isProtected, level, parent, parseGitFile, parseHead, resetIn, tokens, usage } from './format'
6
7const usageAtom = atom({ plugin: 'status-sessao', key: 'usage' } as const, null as Usage | null)
8const gitAtom = atom({ plugin: 'status-sessao', key: 'git' } as const, null as Git | null)
9
10// Reads .git/HEAD walking up from the session folder; runs no process.
11async function readGit($: EngineInterface): Promise<Git | null> {
12 try {
13 let next: string | null = await $.session.cwd()
14 for (let depth = 0; next !== null && depth < 40; depth++) {
15 const dir: string = next
16 next = parent(dir)
17 const dotGit = `${dir === '/' ? '' : dir}/.git`
18 if (!(await $.fs.exists(dotGit))) continue
19 const stat = await $.fs.stat(dotGit)
20 const gitDir = stat.kind === 'file' ? parseGitFile(await $.fs.read(dotGit), dir) : dotGit
21 if (gitDir === null) return null
22 const head = parseHead(await $.fs.read(`${gitDir}/HEAD`))
23 return head ? { repo: basename(dir), ...head } : null
24 }
25 return null
26 } catch {
27 return null
28 }
29}
30
31async function refreshGit($: EngineInterface) {
32 const git = await readGit($)
33 await update($, gitAtom, () => git)
34}
35
36export const register: Register = on => {
37 on('session.start', async ($, e, next) => {
38 const result = await next(e)
39 const now = await $.session.usage()
40 await update($, usageAtom, () => usage(now.context, now.rateLimits))
41 await refreshGit($)
42 return result
43 })
44
45 // After each turn, and whenever a rate-limit window moves a point.
46 on('session.measure', async ($, e, next) => {
47 await update($, usageAtom, () => usage(e.context, e.rateLimits))
48 await refreshGit($)
49 return next(e)
50 })
51
52 // A checkout or switch mid-turn shows before the next commit, not at the turn's end.
53 on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
54 const result = await next(e)
55 if (/\bgit\b/.test(e.command)) await refreshGit($)
56 return result
57 })
58
59 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
60 const data = await read($, usageAtom)
61 const git = await read($, gitAtom)
62 if (e.props.hasSurvey || (data === null && git === null)) return next(e)
63
64 const now = await $.clock.now()
65 const { Box, Text } = $.ui.resolve(e)
66
67 const meter = (key: string, name: string, percent: number | null, detail: string | null) => {
68 if (percent === null) {
69 return (
70 <Box key={key} flexDirection="row" columnGap={1}>
71 <Text dimColor>{name}</Text>
72 <Text dimColor>—</Text>
73 </Box>
74 )
75 }
76 const [filled, rest] = bar(percent)
77 return (
78 <Box key={key} flexDirection="row" columnGap={1}>
79 <Text dimColor>{name}</Text>
80 <Text>
81 <Text color={level(percent)}>{filled}</Text>
82 <Text color="subtle">{rest}</Text>
83 </Text>
84 <Text color={level(percent)} bold={percent >= 85}>
85 {Math.round(percent)}%
86 </Text>
87 {detail !== null && <Text dimColor>{detail}</Text>}
88 </Box>
89 )
90 }
91
92 const danger = git !== null && (isProtected(git.branch) || git.isDetached)
93 const badge = git && (
94 <Text key="git" bold color="#ffffff" backgroundColor={danger ? '#c62828' : '#1f6feb'}>
95 {` ⎇ ${git.repo} · ${git.isDetached ? `HEAD ${git.branch}` : git.branch} `}
96 </Text>
97 )
98
99 // The band has one instance for every plugin: draw what is beneath (another
100 // mod's band) under this row instead of replacing it.
101 const beneath = await next(e)
102 const hasBeneath = !isEmpty(beneath)
103
104 return (
105 <Box flexDirection="column" rowGap={hasBeneath ? 1 : 0}>
106 <Box flexDirection="row" flexWrap="wrap" columnGap={3}>
107 {badge}
108 {data &&
109 meter('ctx', 'context', data.percent, data.tokens === null ? null : `${tokens(data.tokens)}/${tokens(data.window)}`)}
110 {data && meter('5h', '5h', data.session?.percent ?? null, data.session ? resetIn(data.session.resetsAt, now) : null)}
111 {data && meter('week', 'week', data.weekly?.percent ?? null, data.weekly ? resetIn(data.weekly.resetsAt, now) : null)}
112 </Box>
113 {hasBeneath && beneath}
114 </Box>
115 )
116 })
117}
118hooks/format.ts 85 lines1import type { RenderElement, SessionContextUsage, SessionRateLimit } from 'claude-code'
2
3import type { Limit, Usage } from '../types'
4
5const PROTECTED = /^(main|master|prod|production|release|develop|trunk)$|^release\//
6
7export function isProtected(branch: string): boolean {
8 return PROTECTED.test(branch)
9}
10
11export function level(percent: number): 'success' | 'warning' | 'error' {
12 if (percent >= 85) return 'error'
13 if (percent >= 60) return 'warning'
14 return 'success'
15}
16
17export function bar(percent: number, width = 8): [string, string] {
18 const filled = Math.round((Math.min(Math.max(percent, 0), 100) / 100) * width)
19 return ['━'.repeat(filled), '━'.repeat(width - filled)]
20}
21
22export function tokens(n: number): string {
23 if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`
24 if (n >= 1_000) return `${Math.round(n / 1_000)}k`
25 return String(n)
26}
27
28export function resetIn(resetsAt: string | null, now: number): string | null {
29 if (resetsAt === null) return null
30 const ms = Date.parse(resetsAt) - now
31 if (!Number.isFinite(ms) || ms <= 0) return null
32 const minutes = Math.floor(ms / 60_000)
33 const days = Math.floor(minutes / 1440)
34 const hours = Math.floor((minutes % 1440) / 60)
35 if (days > 0) return `${days}d${hours}h`
36 if (hours > 0) return `${hours}h${String(minutes % 60).padStart(2, '0')}`
37 return `${minutes}m`
38}
39
40function limit(rateLimits: readonly SessionRateLimit[], kind: string): Limit | null {
41 const found = rateLimits.find(r => r.kind === kind)
42 return found ? { percent: found.percentUsed, resetsAt: found.resetsAt ?? null } : null
43}
44
45export function usage(context: SessionContextUsage, rateLimits: readonly SessionRateLimit[]): Usage {
46 return {
47 tokens: context.tokens ?? null,
48 window: context.window,
49 percent: context.percent ?? null,
50 session: limit(rateLimits, 'five_hour'),
51 weekly: limit(rateLimits, 'seven_day'),
52 }
53}
54
55/** `.git/HEAD`: a symbolic ref names the branch; a bare hash means a detached HEAD. */
56export function parseHead(text: string): { branch: string; isDetached: boolean } | null {
57 const line = text.trim()
58 const ref = /^ref:\s*refs\/heads\/(.+)$/.exec(line)
59 if (ref?.[1]) return { branch: ref[1], isDetached: false }
60 if (/^[0-9a-f]{7,64}$/.test(line)) return { branch: line.slice(0, 7), isDetached: true }
61 return null
62}
63
64/** A `.git` file (worktree or submodule) points at the real git dir, maybe relatively. */
65export function parseGitFile(text: string, dir: string): string | null {
66 const m = /^gitdir:\s*(.+)$/m.exec(text)
67 if (!m?.[1]) return null
68 const target = m[1].trim()
69 return target.startsWith('/') ? target : `${dir}/${target}`
70}
71
72export function parent(dir: string): string | null {
73 const i = dir.replace(/\/+$/, '').lastIndexOf('/')
74 return i <= 0 ? (dir === '/' ? null : '/') : dir.slice(0, i)
75}
76
77export function basename(dir: string): string {
78 return dir.replace(/\/+$/, '').split('/').pop() || dir
79}
80
81/** What the engine draws when no other mod has anything for the band: a bare Box. */
82export function isEmpty(el: RenderElement): boolean {
83 return el.type === 'Box' && !el.children?.length
84}
85types/index.d.ts 18 lines1export type Limit = { percent: number; resetsAt: string | null }
2
3export type Usage = {
4 tokens: number | null
5 window: number
6 percent: number | null
7 session: Limit | null
8 weekly: Limit | null
9}
10
11export type Git = { repo: string; branch: string; isDetached: boolean }
12
13declare module 'claude-code' {
14 interface PluginState {
15 'status-sessao': { usage: Usage | null; git: Git | null }
16 }
17}
18