statusline-usage.sh 상태 줄을 mod로 다시 그린다

Claude Code mod 플러그인 두 개와, 이를 설치하는 마켓플레이스 local-mods를 담은 저장소입니다.
usage-line: 입력창 바로 위에 작업 상태와 사용량을 두 줄 띠로 보여 줍니다.korean-ui: 화면에 보이는 영문을 한국어로 바꿉니다. 아래 절 참고.~/.claude/statusline-usage.sh 상태 줄에 나오던 정보를 mod API로 다시 그립니다. 스크립트를 실행하지는 않으며, 스크립트에 있던 vim 모드 표시는 mod API로 읽을 수 없어 뺐습니다.
mod(function hooks) API는 early access 기능이라 Claude Code 릴리스에 따라 바뀔 수 있습니다.
proj on main [+1 ?3] │ Opus 5.5 ⚡high [status] [context] [diff] [compact]
ctx 42% 84k/200k · cache 87% │ $1.23 │ 5h 30% →14:00 · 7d 12% · Fable 5%
1줄: 어디서, 무엇으로
+), 수정(!), 미추적(?), ahead(⇡), behind(⇣) 개수를 0이 아닌 것만 표시합니다. detached 상태면 커밋 해시 앞 7자리를 보여 줍니다.claude-opus-5-5 → Opus 5.5)과 effort. effort는 마지막 모델 요청 값을 쓰고, 아직 요청이 없으면 settings의 modelSettings[모델 ID].effortLevel, 그것도 없으면 effortLevel을 씁니다.[status], [context] 버튼은 /status, /context를, [compact] 버튼은 이어서 작업할 수 있게 요약하라는 지시문을 붙여 /compact를 실행합니다. 응답 중에 누르면 응답이 끝난 뒤 실행됩니다.[diff](/diff), 5h나 7d 사용률이 70% 이상이면 [usage](/usage)가 [compact] 앞에 나타납니다. 미추적 파일만 있을 때는 [diff]를 보이지 않습니다.…로 잘립니다. 버튼을 [ label ]로 그리면 폴더·모델에 40칸이 남지 않을 만큼 좁을 때는 [compact]를 뺀 버튼을 테두리 없는 흐린 라벨로 그립니다. 마우스를 올리거나 포커스가 가면 진하게 보입니다.2줄: 얼마나
ctx: 컨텍스트 사용률과 토큰 수(사용/창 크기)cache: 프롬프트 캐시 적중률. 이 대화의 메인 스레드 요청에서 누적한 cache_read 토큰을 전체 입력 토큰으로 나눈 값입니다. /clear로 세션 id가 바뀌거나 Claude Code를 새로 시작하면(resume 포함) 0부터 다시 셉니다.5h(초기화 시각 포함), 7d 사용 한도와 모델별 주간 한도. 모델별 한도는 상태 줄 입력에 없어서 Claude Code가 ~/.claude.json에 캐시해 둔 값을 읽으며, 다른 계정의 캐시이거나 24시간 넘게 갱신되지 않았으면 표시하지 않습니다.라벨과 부가 정보는 흐리게, 값은 기본 글자색으로 그립니다. 사용률은 한도에 가까울 때만 색을 바꿉니다.
| 항목 | 노랑 | 굵은 빨강 |
|---|---|---|
ctx | 60% 이상 | 80% 이상 |
5h, 7d, 모델별 한도 | 70% 이상 | 90% 이상 |
띠에 두 줄을 그릴 자리가 없으면 한 줄로 합칩니다.
session.measure)git 정보는 git status --porcelain=v2 --branch 한 번으로 읽으므로 git이 PATH에 있어야 합니다. git 저장소가 아닌 폴더에서는 git 구간을 표시하지 않습니다.
플러그인 API가 바꿀 수 있게 열어 둔 화면 요소만 한국어로 바꿉니다.
| 위치 | 원래 | 바뀐 뒤 |
|---|---|---|
| Claude 답변·질문·작업 목록 | 영어로 답할 때가 있음 | 시스템 프롬프트에 지시를 붙여 한국어로 답하게 함 |
| 작업 중 표시 | Sauteing… | 궁리하는 중… |
| 턴 종료 줄 | ✻ Baked for 3s | ✻ 3초 동안 궁리함 |
| 입력창 아래 힌트 | ? for shortcuts, esc to interrupt | ? 단축키, esc 중단 |
| 모드 표시 | plan mode on | 계획 모드 켜짐 |
| 접힌 도구 묶음 | Read 3 files, ran 2 shell commands | 파일 3개 읽음, 명령 2개 실행 (ctrl+o 펼치기) |
/config 메뉴 항목 | Auto-compact, Show tips | 자동 압축, 팁 표시, 사전에 없는 항목은 모델 번역 |
/ 자동완성 명령 설명 | Start a new session … | 빈 컨텍스트로 새 세션 시작 … |
| 대화 중 엔진 안내 줄 | Conversation compacted, Session resumed without branch: main | 대화를 압축함, 그 밖의 문장은 모델 번역 |
| 백그라운드 작업 알림 줄 | Task … completed 같은 요약 | 모델 번역 |
| 스킬·플러그인 명령 설명, 사전에 없는 기본 명령 설명 | / 메뉴의 영문 설명 | 모델 번역 |
번역은 두 단계로 합니다.
/config 항목, 기본 명령 설명처럼 문구가 정해진 곳은 register.tsx·dict.ts의 사전으로 바로 바꿉니다. 사전 문구는 Claude Code 2.1.296 실행 파일에서 확인했습니다. 키 이름 뒤에 동작이 붙는 힌트(esc to close, / to search)는 키 + 동작 규칙으로 바꿉니다./config 항목 이름처럼 종류가 많거나 값이 섞여 드는 문장은 haiku 모델로 번역합니다. 결과는 $.store에 최근 500개까지 남겨 다시 쓰므로 같은 문장은 한 번만 번역합니다./config 항목 이름은 세션이 시작되고 3초 뒤 한 번에 번역합니다(10개씩 묶어 요청). 처음 설치한 세션이나 새 스킬·새 버전의 문구가 생겼을 때만 토큰이 듭니다.사전에도 없고 모델 번역 대상도 아닌 문장은 섞인 문장이 되지 않도록 원문 그대로 둡니다. 코드, 명령어, 경로, 식별자는 번역하지 않습니다.
바꾸지 못하는 것: 시작 로고, 권한 확인 창, 질문 창의 고정 버튼(Other, Submit), /status·/usage 창, /config의 탭 이름과 설정값, 도구 실행 줄(Bash(...))과 결과 요약(Read 120 lines). 이 화면들은 플러그인 API에 바꿀 수 있는 지점이 없습니다.
/config 항목 이름과 기본 명령 설명은 Claude Code 2.1.296에서 뽑은 영문을 키로 씁니다. 버전이 바뀌어 사전에 없는 문구가 생기면 모델 번역으로 채우고, 번역을 고정하고 싶을 때만 korean-ui/hooks/dict.ts에 추가합니다. 입력창 힌트·스피너·모드 표시는 자주 다시 그려지는 짧은 문구라 사전으로만 바꾸므로, 새 문구는 영문으로 남습니다.
스피너 아래 Tip: 줄은 플러그인 훅이 닿지 않아, Claude Code의 spinnerTipsOverride 설정으로 바꿉니다. korean-ui/tips.json에 2.1.291·2.1.296의 기본 팁을 옮긴 54개가 있습니다. 맥 전용, 기업·외부 클라우드 전용, 그때그때 값이 바뀌는 팁은 뺐습니다. ~/.claude/settings.json에 다음을 넣으면 기본 영어 팁 대신 이 목록이 팁: 머리말로 나옵니다.
"spinnerTipsOverride": {
"tipsFile": "~/.claude/mods/korean-ui/tips.json",
"excludeDefault": true,
"label": "팁"
}
팁은 파일에서 바로 읽으므로 저장소를 git pull하면 갱신됩니다. Claude Code에 새 팁이 생기면 이 파일에 직접 추가해야 합니다(최대 200개, 각 500자).
저장소를 내려받아 로컬 마켓플레이스로 추가한 뒤 플러그인을 설치합니다.
git clone https://github.com/jaeseongs95/claude-code-mods.git ~/.claude/mods
claude plugin marketplace add ~/.claude/mods
claude plugin install usage-line@local-mods
claude plugin install korean-ui@local-mods
필요한 플러그인만 설치해도 됩니다.
.claude-plugin/marketplace.json 마켓플레이스 local-mods 정의
usage-line/
.claude-plugin/plugin.json 플러그인 매니페스트
hooks/hooks.json 훅 모듈 목록
hooks/register.tsx 띠 그리기와 갱신 로직
hooks/usage-line.test.ts 테스트
types/index.d.ts state 타입 선언
korean-ui/
.claude-plugin/plugin.json 플러그인 매니페스트
hooks/hooks.json 훅 모듈 목록
hooks/register.tsx 화면 요소별 한글화 훅
hooks/dict.ts /config 항목 이름·명령 설명 사전
hooks/translate.ts 모델 번역 프롬프트와 응답 해석
hooks/register.test.ts 테스트
tips.json spinnerTipsOverride용 한국어 팁 목록
각 플러그인의 .claude-plugin/types/는 Claude Code가 mod를 불러올 때 생성하는 API 타입 선언이라 저장소에 포함하지 않습니다.
claude plugin validate usage-line
claude plugin test usage-line
claude plugin validate korean-ui
claude plugin test korean-uihooks/register.tsx 310 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Cache, Seg } from '../types'
5
6const band = atom({ plugin: 'usage-line', key: 'rows' } as const, [] as Seg[][])
7// 상황에 따라 [status] [context] 와 [compact] 사이에 끼우는 버튼 이름.
8const extra = atom({ plugin: 'usage-line', key: 'buttons' } as const, [] as string[])
9
10// ~/.claude/statusline-usage.sh 의 정보를 두 줄 띠로 그린다. 스크립트에 있던 vim 모드는 mod API로 읽을 수 없어 뺐다.
11// 1줄 "어디서·무엇으로": 폴더 · git · 모델 ⚡effort (+ [status] [context] [diff?] [usage?] [compact] 버튼)
12// 2줄 "얼마나": 컨텍스트 · 캐시 | 비용 | 사용 한도(5h · 7d · 모델별)
13// 색 규칙: 라벨과 부가 정보는 흐리게, 값은 기본 글자색. 한도에 가까워질 때만 노랑·빨강으로 눈에 띄게 한다.
14// 정체성(폴더·브랜치·모델·effort)만 고유 색을 쓴다.
15
16export type Git = { branch: string; staged: number; modified: number; untracked: number; ahead: number; behind: number }
17export type Line = {
18 dir: string
19 git?: Git
20 ctxPct?: number
21 ctxTokens?: number
22 ctxWindow?: number
23 cost?: number
24 cacheHit?: number
25 five?: { pct: number; reset?: string }
26 seven?: number
27 scoped: { name: string; pct: number }[]
28 model?: string
29 effort?: string
30}
31
32const LAVENDER = '#af87ff', ORANGE = '#ff8700'
33const s = (text: string, color?: string, bold?: boolean): Seg => ({ text, color, bold })
34const dim = (text: string): Seg => ({ text, dim: true })
35
36// pct 가 warn 이상이면 노랑, crit 이상이면 굵은 빨강.
37const pct = (label: string, v: number, warn: number, crit: number, note?: string): Seg[] => [
38 dim(`${label} `),
39 { text: `${Math.round(v)}%`, color: v >= crit ? 'red' : v >= warn ? 'yellow' : undefined, bold: v >= crit },
40 ...(note ? [dim(` ${note}`)] : []),
41]
42const join = (items: Seg[][], sep: Seg) => items.filter(i => i.length).flatMap((g, i) => (i ? [sep, ...g] : g))
43const DOT = dim(' · '), BAR = dim(' │ ')
44
45export function humanTokens(n: number): string {
46 if (n >= 1e6) {
47 const m = Math.floor(n / 1e6), f = Math.floor((n % 1e6) / 1e5)
48 return f ? `${m}.${f}M` : `${m}M`
49 }
50 return n >= 1000 ? `${Math.floor(n / 1000)}k` : String(n)
51}
52
53// $.session.model() 은 ID(claude-opus-5-5)를 준다. 스크립트처럼 표시 이름(Opus 5.5)으로 바꾸고, 모르는 형식이면 그대로 둔다.
54export function modelName(id: string): string {
55 const m = /^claude-([a-z]+)-(\d+)(?:-(\d{1,2}))?(?:-\d{8})?(\[.+\])?$/.exec(id)
56 if (!m) return id
57 const [, family = '', major, minor, suffix] = m
58 return `${family[0]?.toUpperCase()}${family.slice(1)} ${major}${minor ? `.${minor}` : ''}${suffix ? ` ${suffix}` : ''}`
59}
60
61const EFFORT: Record<string, Omit<Seg, 'text'>> = {
62 low: { color: 'gray' },
63 medium: { color: 'cyan' },
64 high: { color: 'yellow' },
65 xhigh: { color: ORANGE },
66 max: { color: 'red', bold: true },
67}
68
69export function format(d: Line): Seg[][] {
70 const loc = [s(d.dir, 'cyan', true)]
71 if (d.git) {
72 const g = d.git
73 loc.push(dim(' on '), s(` ${g.branch}`, 'magenta', true))
74 const parts = [
75 g.staged && s(`+${g.staged}`, 'green'),
76 g.modified && s(`!${g.modified}`, 'yellow'),
77 g.untracked && s(`?${g.untracked}`, 'red'),
78 g.ahead && s(`⇡${g.ahead}`, 'cyan'),
79 g.behind && s(`⇣${g.behind}`, 'magenta'),
80 ].filter((p): p is Seg => !!p)
81 if (parts.length) loc.push(dim(' ['), ...parts.flatMap((p, i) => (i ? [s(' '), p] : [p])), dim(']'))
82 }
83
84 const model = d.model ? [s(d.model, LAVENDER)] : []
85 if (d.effort) model.push(...(model.length ? [s(' ')] : []), { ...(EFFORT[d.effort] ?? {}), text: `⚡${d.effort}` })
86
87 const tokens = d.ctxTokens !== undefined && d.ctxWindow !== undefined ? `${humanTokens(d.ctxTokens)}/${humanTokens(d.ctxWindow)}` : undefined
88 const context = join([
89 pct('ctx', d.ctxPct ?? 0, 60, 80, tokens),
90 d.cacheHit !== undefined ? [dim('cache '), s(`${d.cacheHit}%`)] : [],
91 ], DOT)
92 const limits = join([
93 d.five ? pct('5h', d.five.pct, 70, 90, d.five.reset && `→${d.five.reset}`) : [],
94 d.seven !== undefined ? pct('7d', d.seven, 70, 90) : [],
95 ...d.scoped.map(x => pct(x.name, x.pct, 70, 90)),
96 ], DOT)
97
98 return [
99 join([loc, model], BAR),
100 join([context, [s(`$${(d.cost ?? 0).toFixed(2)}`)], limits], BAR),
101 ]
102}
103
104// 필요할 때만 보이는 버튼: 커밋 안 한 변경이 있으면 [diff], 5h·7d 한도가 노랑(70%) 이상이면 [usage].
105export function extraButtons(d: Line): string[] {
106 const near = Math.max(d.five?.pct ?? 0, d.seven ?? 0) >= 70
107 return [...(d.git && d.git.staged + d.git.modified ? ['diff'] : []), ...(near ? ['usage'] : [])]
108}
109
110// git status --porcelain=v2 --branch 한 번으로 브랜치·스테이징·수정·미추적·ahead/behind를 모두 읽는다.
111export function parseGit(out: string): Git | undefined {
112 const g: Git = { branch: '', staged: 0, modified: 0, untracked: 0, ahead: 0, behind: 0 }
113 let oid = ''
114 for (const l of out.split('\n')) {
115 if (l.startsWith('# branch.head ')) g.branch = l.slice(14)
116 else if (l.startsWith('# branch.oid ')) oid = l.slice(13)
117 else if (l.startsWith('# branch.ab ')) {
118 const m = /\+(\d+) -(\d+)/.exec(l)
119 if (m) { g.ahead = Number(m[1]); g.behind = Number(m[2]) }
120 } else if (/^[12u] /.test(l)) {
121 if (l[2] !== '.') g.staged++
122 if (l[3] !== '.') g.modified++
123 } else if (l.startsWith('? ')) g.untracked++
124 }
125 if (!g.branch) return undefined
126 if (g.branch === '(detached)') g.branch = oid.slice(0, 7)
127 return g
128}
129
130const hhmm = (iso?: string) => {
131 if (!iso) return undefined
132 const t = new Date(iso)
133 return isNaN(+t) ? undefined : `${String(t.getHours()).padStart(2, '0')}:${String(t.getMinutes()).padStart(2, '0')}`
134}
135
136// 모델별 주간 창: 상태 줄 입력에 없어서 Claude Code가 ~/.claude.json 에 캐시해 둔 값을 읽는다.
137// 다른 계정 캐시거나 24시간 넘게 갱신이 없으면 버린다(스크립트와 같은 규칙).
138async function readScoped($: EngineInterface) {
139 const home = (await $.env.get('CLAUDE_CONFIG_DIR')) ?? (await $.env.get('USERPROFILE')) ?? (await $.env.get('HOME'))
140 try {
141 const j = JSON.parse((await $.fs.read(`${home}/.claude.json`)) as string)
142 const cu = j.cachedUsageUtilization, uuid = j.oauthAccount?.accountUuid ?? ''
143 if (!cu || (cu.accountUuid ?? uuid) !== uuid || (await $.clock.now()) -(cu.fetchedAtMs ?? 0) >= 864e5) return []
144 return (cu.utilization?.limits ?? [])
145 .filter((r: any) => r?.kind === 'weekly_scoped' && typeof r.scope?.model?.display_name === 'string' && typeof r.percent === 'number')
146 .map((r: any) => ({ name: r.scope.model.display_name as string, pct: r.percent as number }))
147 } catch {
148 return []
149 }
150}
151
152// 프롬프트 캐시 적중률 = 이 대화의 메인 스레드 요청 누적 cache_read / 전체 입력 토큰.
153// 리로드 뒤에도 남게 state 에 두고, 대화가 바뀌면(/clear 는 session.start 없이 세션 id만 바뀐다) 0부터 센다.
154// 새 프로세스(resume 포함)에서는 state 가 비어 있어 0부터 센다.
155const cacheAcc = atom({ plugin: 'usage-line', key: 'cache' } as const, null as Cache | null)
156
157export function addUsage(prev: Cache | null, sessionId: string, u: { input_tokens: number; cache_read_input_tokens: number; cache_creation_input_tokens: number }): Cache {
158 const base = prev?.sessionId === sessionId ? prev : { sessionId, read: 0, total: 0 }
159 return {
160 sessionId,
161 read: base.read + u.cache_read_input_tokens,
162 total: base.total + u.input_tokens + u.cache_read_input_tokens + u.cache_creation_input_tokens,
163 }
164}
165
166export const hitRatio = (c: Cache | null, sessionId: string) =>
167 c?.sessionId === sessionId && c.total > 0 ? Math.round((c.read / c.total) * 100) : undefined
168
169// effort 는 모델 요청(turn.step)이 있어야 알 수 있다. 그 값은 리로드 뒤에도 남게 state 에 두고,
170// 아직 요청이 없으면 settings 의 modelSettings[모델 ID].effortLevel(없으면 effortLevel)로 채운다.
171const lastEffort = atom({ plugin: 'usage-line', key: 'effort' } as const, null as string | null)
172
173async function settingsEffort($: EngineInterface, modelId: string) {
174 const s = (await $.settings.read({})) as {
175 modelSettings?: Record<string, { effortLevel?: unknown } | undefined>
176 effortLevel?: unknown
177 }
178 const v = s.modelSettings?.[modelId]?.effortLevel ?? s.effortLevel
179 return typeof v === 'string' ? v : undefined
180}
181
182async function refresh($: EngineInterface) {
183 const cwd = await $.session.cwd()
184 const [usage, model, git, scoped] = await Promise.all([
185 $.session.usage(),
186 $.session.model(),
187 $.process
188 .run(['git', '--no-optional-locks', 'status', '--porcelain=v2', '--branch', '-uall'], { cwd })
189 .then(r => (r.exitCode === 0 ? parseGit(r.stdout) : undefined), () => undefined),
190 readScoped($),
191 ])
192 const effort = (await read($, lastEffort)) ?? (await settingsEffort($, model))
193 const cacheHit = hitRatio(await read($, cacheAcc), await $.session.id())
194 const rate = (k: string) => usage.rateLimits.find(r => r.kind === k)
195 const five = rate('five_hour'), seven = rate('seven_day')
196 const d: Line = {
197 dir: cwd.split(/[\\/]/).filter(Boolean).pop() ?? cwd,
198 git,
199 ctxPct: usage.context.percent,
200 ctxTokens: usage.context.tokens,
201 ctxWindow: usage.context.window,
202 cost: usage.cost?.usd,
203 cacheHit,
204 five: five && { pct: five.percentUsed, reset: hhmm(five.resetsAt) },
205 seven: seven?.percentUsed,
206 scoped,
207 model: modelName(model),
208 effort,
209 }
210 await update($, band, () => format(d))
211 await update($, extra, () => extraButtons(d))
212}
213
214const REFRESH_MS = 10_000
215// 버튼을 [ label ] 로 그려도 첫 줄 폴더·모델에 남겨 둘 최소 칸 수.
216const MIN_TOP = 40
217// [compact] 버튼이 /compact 뒤에 붙이는 요약 지시.
218export const COMPACT_PROMPT =
219 '현재 상황을 유지하고 이어서 작업할 수 있게 요약해 줘. 사용자의 요청 원문과 목표, 확정된 결정과 그 이유, ' +
220 '수정한 파일 경로와 핵심 변경, 실행한 검증과 결과(실패 메시지 포함), 진행 중인 작업과 남은 할 일, ' +
221 '사용자가 준 제약과 피드백은 빠짐없이 남기고, 끝난 탐색 과정과 긴 도구 출력은 결론만 남겨.'
222
223export const register: Register = on => {
224 on('session.start', async ($, e, next) => {
225 const r = await next(e)
226 await refresh($)
227 // 이벤트가 없어도(대기 중, Claude 밖의 git 변경) 10초마다 다시 읽는다. 앞선 갱신이 안 끝났으면 건너뛴다.
228 // 리로드하면 엔진이 이전 타이머를 버리므로 따로 cancel 하지 않는다.
229 let busy = false
230 $.clock.every(REFRESH_MS, () => {
231 if (busy) return
232 busy = true
233 refresh($).finally(() => (busy = false))
234 })
235 return r
236 })
237
238 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
239 const rows = await read($, band)
240 if (!rows.length) return next(e)
241 const buttons = await read($, extra)
242 // 띠에 두 줄 자리가 없으면 한 줄로 합친다.
243 const [top = [], bottom = []] = e.props.maxRows >= 2 ? rows : [join(rows, BAR)]
244 const { Box, Button, Text } = $.ui.resolve(e)
245 const draw = (segs: Seg[]) => (
246 <Text wrap="truncate-end">
247 {segs.map(g => (
248 <Text color={g.color} bold={g.bold} dimColor={g.dim}>
249 {g.text}
250 </Text>
251 ))}
252 </Text>
253 )
254 // 버튼은 첫 줄 끝에 줄지 않게(flexShrink 0) 두고, 폭이 모자라면 폴더·모델 쪽이 …로 잘린다.
255 // 버튼까지 같이 줄면 "[ status ]"가 세로로 접힌다. [ label ] 로 그리면 폴더·모델에 MIN_TOP 칸이
256 // 안 남는 좁은 폭에서는 compact 말고는 테두리 없이 흐린 라벨로 그린다(포인터·포커스 아래선 진하게).
257 // 응답 중에 누르면 응답이 끝난 뒤 열린다.
258 const run = (command: string, args?: string) =>
259 $.command.run({ command, args }).catch(err => $.ui.toast(`/${command} 실행 실패: ${err}`))
260 const labels = ['status', 'context', ...buttons]
261 const framed = [...labels, 'compact'].reduce((n, l) => n + l.length + 5, 1)
262 const slim = e.props.bodyColumns - framed < MIN_TOP ? ({ plain: true, dimColor: true } as const) : {}
263 return (
264 // 위 대화와 붙어 보이지 않게 한 줄 띄운다. 자리가 모자라면 띄우지 않는다.
265 <Box flexDirection="column" marginTop={e.props.maxRows > (bottom.length ? 2 : 1) ? 1 : 0}>
266 <Box>
267 <Box flexShrink={1} minWidth={0}>{draw(top)}</Box>
268 <Box flexShrink={0}>
269 <Text> </Text>
270 {labels.map(c => (
271 <Box key={c} paddingLeft={1}>
272 <Button key={c} label={c} {...slim} onPress={() => void run(c)} />
273 </Box>
274 ))}
275 <Text> </Text>
276 <Button key="compact" label="compact" onPress={() => void run('compact', COMPACT_PROMPT)} />
277 </Box>
278 </Box>
279 {bottom.length ? <Box paddingLeft={2}>{draw(bottom)}</Box> : null}
280 </Box>
281 )
282 })
283
284 // 메인 스레드 요청만 본다: effort 와 프롬프트 캐시 누적값.
285 on('turn.step', async function* ($, e, next) {
286 const r = yield* next(e)
287 if (e.agentId === undefined) {
288 if (typeof e.effort === 'string') await update($, lastEffort, () => e.effort as string)
289 if (r.usage) {
290 const usage = r.usage, id = await $.session.id()
291 await update($, cacheAcc, c => addUsage(c, id, usage))
292 }
293 }
294 return r
295 })
296
297 on('session.measure', async ($, e, next) => {
298 const r = await next(e)
299 await refresh($)
300 return r
301 })
302
303 // 턴 중 파일이 바뀌면 git 구간을 갱신한다.
304 on('turn.complete', async ($, e, next) => {
305 const r = await next(e)
306 if (e.agentId === undefined) await refresh($)
307 return r
308 })
309}
310types/index.d.ts 9 lines1export type Seg = { text: string; color?: string; bold?: boolean; dim?: boolean }
2export type Cache = { sessionId: string; read: number; total: number }
3
4declare module 'claude-code' {
5 interface PluginState {
6 'usage-line': { rows: Seg[][]; buttons: string[]; effort: string | null; cache: Cache | null }
7 }
8}
9