프롬프트 위 띠(AbovePrompt)에 세션 지표를 오른쪽 끝에 붙여 표시: 지금은 마지막 응답의 프롬프트 캐시 적중률. 다른 플러그인의 띠와 위아래로 쌓인다

개인용 Claude Code 플러그인 마켓플레이스.
Claude Code 프롬프트에서:
/plugin install statusbar --marketplace DevooKim/my-claude
/plugin install prompt-band --marketplace DevooKim/my-claude
마켓플레이스 추가 질문에는 y, 범위는 user를 고르면 됩니다.
프롬프트 입력창 아래 힌트 줄 밑에 git 줄과 사용량 줄을 그립니다.
my-claude/fix-bar · worktree-fix-bar · !3+2?1⇡1 · PR #12
✻ Opus 5.5 ●●●○○ high · C ━━━━━━━─── 71% 715k/1M · S ━───────── 13% ↻3h35m W ━───────── 4% ↻5d
git 줄은 starship 프롬프트와 같은 기호와 색으로 그려 셸 프롬프트와 똑같이 보입니다.
사용량 줄
/effort 선택 창과 같은 색, xhigh는 반짝임, max는 흐르는 무지개!)git 줄 (저장소 안에서만)
프로젝트[/워크트리] · 브랜치 · 상태를 ·로 나눠 이어 씀. 경로는 굵은 하늘색(#57C7FF), 브랜치는 굵은 회색, 상태는 노랑git_status와 같음): = 충돌, $ 스태시, × 삭제, » 이름 바꿈, ! 수정, + 스테이징, ? 추적 안 함, ⇡ 앞섬, ⇣ 뒤처짐. 깨끗하고 upstream과 같으면 아무것도 없음(커밋), 리베이스·병합·체리픽·revert·bisect 중이면 브랜치 뒤에 (REBASING)처럼 표시gh 로그인 필요): 누르면 기본 브라우저로 PR 페이지가 열림(전체 화면 모드에서 클릭). 색이 상태: draft 회색, 머지할 수 있지만 승인 전(리뷰 전·CI 진행 중 포함) 주황, 승인까지 끝나 머지 가능 초록, 충돌·CI 실패·변경 요청으로 머지 불가 빨강, 병합됨 보라. Claude Code의 PR 표시와 겹치면 /config의 Show PR status footer를 끄면 됨서브에이전트와 할 일(Task)은 Claude Code가 직접 보여 주므로 따로 그리지 않습니다.
테스트: claude plugin test plugins/statusbar
프롬프트 입력창 바로 위 띠(AbovePrompt)의 오른쪽 끝, 엔진의 접기 버튼 [-] 옆에 세션 지표를 그립니다.
Cache ◕ 93% 160k/172k [-]
읽은 양/전체. 서브에이전트 응답은 세지 않음○ ◔ ◑ ◕ ●, 꽉 찬 원은 100%만), 숫자는 1% 단위next(e)를 품어야 함)테스트: claude plugin test plugins/prompt-band
hooks/register.tsx 102 lines1import { atom, read, update } from 'claude-code'
2import type { Register, TurnUsage } from 'claude-code'
3
4import type { PromptBandCache } from '../types'
5
6const cacheAtom = atom({ plugin: 'prompt-band', key: 'cache' } as const, null)
7
8// 글자와 장식은 사용자의 테마(라이트·다크)를 따르는 테마 키로 쓴다.
9const MUTED = 'inactive'
10// 상태 색: statusbar와 같은, 라이트·다크 양쪽에서 검증된 고정 색이다.
11const STATUS_COLORS = {
12 good: '#0ca30c',
13 warning: '#fab219',
14 serious: '#ec835a',
15 critical: '#d03b3b',
16} as const
17// 캐시 적중률을 25% 단위로 내려 원 조각으로 보인다: 꽉 찬 원은 100%일 때만. 숫자는 1% 단위 그대로.
18const PIE = ['○', '◔', '◑', '◕', '●']
19
20export type SpanStyle = { color?: string; bold?: boolean }
21export type Span = { text: string; style?: SpanStyle }
22
23export const register: Register = on => {
24 // 메인 스레드의 응답 스트림은 그대로 흘려보내고, 응답이 끝나는 stop 조각에서 캐시 사용량만 읽는다.
25 on('turn.step', async function* ($, e, next) {
26 const stream = next(e)
27 let step = await stream.next()
28 while (!step.done) {
29 if (e.agentId === undefined && step.value.kind === 'stop') {
30 const cache = cacheOf(step.value.usage)
31 if (cache !== undefined) await update($, cacheAtom, () => cache)
32 }
33 yield step.value
34 step = await stream.next()
35 }
36
37 return step.value
38 })
39
40 // 띠 자리는 하나라서, 아래 플러그인(또는 엔진)이 그린 것을 받아 그 위에 이 줄들을 쌓는다.
41 // 설문이 떠 있거나 보일 값이 없으면 손대지 않고 넘긴다.
42 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
43 if (e.props.hasSurvey) return next(e)
44 const cache = await read($, cacheAtom)
45 const rows = cache === null ? [] : [cacheRow(cache)]
46 if (rows.length === 0) return next(e)
47 const below = await next(e)
48 const { Box, Text } = $.ui.resolve(e)
49 const draw = (part: Span) =>
50 part.style === undefined ? part.text : <Text {...part.style}>{part.text}</Text>
51
52 // 줄마다 띠의 본문 너비(엔진의 [-] 자리를 뺀 칸)만큼 넓혀 오른쪽 끝에 붙인다.
53 return (
54 <Box flexDirection="column">
55 {rows.map(row => (
56 <Box flexDirection="row" justifyContent="flex-end" width={e.props.bodyColumns}>
57 <Text wrap="truncate-end">{row.map(draw)}</Text>
58 </Box>
59 ))}
60 {below}
61 </Box>
62 )
63 })
64}
65
66export function cacheOf(usage: TurnUsage | null): PromptBandCache | undefined {
67 if (usage === null) return undefined
68 const read = usage.cache_read_input_tokens
69 const total = usage.input_tokens + read + usage.cache_creation_input_tokens
70
71 return total > 0 ? { read, total } : undefined
72}
73
74export function cacheRow({ read, total }: PromptBandCache): Span[] {
75 const hit = Math.round((read / total) * 100)
76 const color = STATUS_COLORS[cacheStatusOf(hit)]
77 const pie = PIE[Math.floor((hit / 100) * (PIE.length - 1))] ?? PIE[0]
78
79 return [
80 { text: 'Cache ', style: { color: MUTED } },
81 { text: pie, style: { color } },
82 { text: ` ${hit}%`, style: { color, bold: true } },
83 { text: ` ${formatTokens(read)}/${formatTokens(total)}`, style: { color: MUTED } },
84 ]
85}
86
87// 캐시 적중률의 상태 단계. 이어지는 대화는 보통 90%를 넘으므로 높게 끊는다.
88export function cacheStatusOf(hit: number): keyof typeof STATUS_COLORS {
89 if (hit >= 90) return 'good'
90 if (hit >= 70) return 'warning'
91 if (hit >= 40) return 'serious'
92
93 return 'critical'
94}
95
96export function formatTokens(count: number): string {
97 if (count >= 999_500) return `${(count / 1_000_000).toFixed(1).replace(/\.0$/, '')}M`
98 if (count >= 1000) return `${Math.round(count / 1000)}k`
99
100 return String(count)
101}
102types/index.d.ts 9 lines1// 마지막 응답의 입력 토큰 가운데 프롬프트 캐시가 읽어 준 몫: read가 total 중 얼마.
2export type PromptBandCache = { read: number; total: number }
3
4declare module 'claude-code' {
5 interface PluginState {
6 'prompt-band': { cache: PromptBandCache | null }
7 }
8}
9