Status rows under the Claude Code prompt: model and effort pills, and context, 5-hour, 7-day and Fable usage bars

在 Claude Code 輸入框下方加兩列狀態列(終端機比較窄時是三列),任何專案都能用:

⏵⏵ auto mode on (shift+tab to cycle)
Opus 5.5 ▾ ctx ▰▰▱▱▱▱▱▱▱▱ 24% ↓ → × ⌫ 5h ▰▱▱▱▱▱▱▱ 9% ↻19:00
xhigh ▾ 7d ▰▰▰▰▱▱▱▱▱▱ 35% ↻Sun 05:00 Fable ▰▱▱▱▱▱▱▱ 17% ↻Sun 05:00
/model、/effort,直接打開切換選單;切換後膠囊立刻跟著變。/clear 或 compact 之後、模型第一次回覆之前,Claude Code 還沒有這個數字,這時顯示本機估算值(系統提示、工具、記憶檔與對話已經佔掉的量;不發請求、不呼叫模型),回覆後換成實際值。/resume 接回的對話顯示它最後一次回覆時的實際值。百分比右邊空一格是四顆鈕。滑鼠移到鈕上,旁邊會浮出反白的名稱標籤,暫時蓋住 5h 那一欄,移開就消失:↓ 等於在輸入框送出 /compact;模型正在回覆時,會排隊等這一輪結束再壓縮。→ 等於在輸入框打「繼續工作」再按 Enter,按一次就送出;模型正在回覆時,會等這一輪結束再送出。× 等於送出 /clear。⌫ 清空輸入框。Claude 工作中按 Esc 會中斷這一輪,不會清掉打到一半的字;這顆鈕只清輸入框,工作照常進行。↓、× 會改掉整段對話,⌫ 會丟掉打好的字,都要按兩次,免得誤觸:第一次按下變成 ↓ compact?、× clear? 或 ⌫ clear input?,3 秒內再按同一顆才執行,否則恢復原狀。▾ 是打開選單;→ 是請模型繼續;↓(往下壓)和 ×(清掉)是對這段對話動手;⌫(往左刪除)只動輸入框。? for shortcuts、auto mode on)照常顯示在最上面,狀態列在它下面。終端機不到 85 欄時,改成三列兩欄、不畫進度條的窄版。模型和 effort 分別在兩欄的最上面,用量的標籤放進百分比膠囊,所以每一欄的膠囊都對齊。用量的配對和寬版一樣:

窄版大約要 49 欄。再窄的話,5h、Fable 那欄會從尾巴切掉,最先切掉的是重置時間。
▾、↓、→、×、⌫)需要 Claude Code 的全螢幕版面(settings.json 的 "tui": "fullscreen")。沒開全螢幕時,按鈕點了沒有反應。在 Claude Code 的輸入框輸入:
/plugin install statusbar --marketplace MomoChenisMe/claude-code-mods
第一次會問要不要加入 github:MomoChenisMe/claude-code-mods 這個 marketplace,回答 y;範圍選 user,每個專案都會載入。
它和 settings.json 的 statusLine 是兩樣東西,可以同時存在。如果不想看到兩行,就把 statusLine 拿掉。
/clear 或 compact 之後的 ctx 是估算值,和第一次回覆後的實際值大約差 1 個百分點。→ 送出的「繼續工作」是固定的中文,不能改,也不跟著語言設定變。compact? 約 6 格、clear input? 約 10 格);按下或逾時後回到原位。claude -p /usage 讀出來:開 session 時一次,之後最多每 5 分鐘一次,每次約 2~4 秒、不呼叫模型。Fable 數字因此最多落後 5 分鐘。/model 後,模型名稱立刻變;但 /model 的輸出不含 effort,所以 effort 要等下一次送出訊息才更新。/effort 的英文輸出 Set effort level to … 讀出來的。Claude Code 改了這句話時,effort 一樣會在下一次送出訊息時更新。claude plugin validate statusbar
claude plugin test statusbar
本機開發方式見 repo 的 README。
hooks/register.tsx 455 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Armed, RateLimit, Usage } from '../types'
5
6const EMPTY: Usage = { model: '', effort: null, contextPercent: null, rateLimits: [], fable: null }
7const usage = atom({ plugin: 'statusbar', key: 'usage' } as const, EMPTY)
8// 等第二次按下的 compact/clear 鈕;null 是兩顆都沒按。
9const armed = atom({ plugin: 'statusbar', key: 'armed' } as const, null as Armed | null)
10
11const ORANGE = '#ff8700'
12const DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
13const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
14
15const EFFORT_COLOR: Record<string, string> = {
16 low: 'success',
17 medium: 'warning',
18 high: ORANGE,
19 xhigh: 'error',
20 max: 'error',
21}
22
23// 百分比膠囊固定白字,底色由藍到紅,任何主題都讀得清楚。
24const WHITE = '#ffffff'
25const BLUE = '#3a7cc0'
26const ROSE = '#c9605f'
27const RED = '#b03a3a'
28
29// 寬版(兩列三欄、有進度條)要的寬度;終端機比這窄就改用三列兩欄、標籤放進百分比膠囊的窄版。
30const WIDE_COLUMNS = 85
31
32// claude-opus-5-5 → Opus 5.5
33export const modelName = (id: string) => {
34 const [family = '', ...version] = id
35 .replace(/^claude-/, '')
36 .replace(/-\d{8}$/, '')
37 .split('-')
38 return `${family.charAt(0).toUpperCase()}${family.slice(1)} ${version.join('.')}`.trim()
39}
40
41// <50 綠、<75 黃、<90 橘、其餘紅。
42export const colorFor = (pct: number) =>
43 pct < 50 ? 'success' : pct < 75 ? 'warning' : pct < 90 ? ORANGE : 'error'
44
45// 百分比膠囊的底色:<50 藍、<75 玫瑰紅、其餘紅。
46export const pillColor = (pct: number) => (pct < 50 ? BLUE : pct < 75 ? ROSE : RED)
47
48const percent = (pct: number | null) => (pct === null ? '--%' : `${Math.round(pct)}%`)
49
50export const bar = (pct: number, width: number) => {
51 const filled = Math.max(0, Math.min(width, Math.round((pct / 100) * width)))
52 return '▰'.repeat(filled) + '▱'.repeat(width - filled)
53}
54
55const pad = (n: number) => String(n).padStart(2, '0')
56const hhmm = (d: Date) => `${pad(d.getHours())}:${pad(d.getMinutes())}`
57const dayHhmm = (d: Date) => `${DAYS[d.getDay()]} ${hhmm(d)}`
58
59const rowText = (content: readonly { type: string; [field: string]: unknown }[]) =>
60 content.map(b => (b.type === 'text' && typeof b.text === 'string' ? b.text : '')).join('\n')
61
62// `/effort`、`/model` 輸出列帶來的變動;其他列回 null。
63export const pickerChange = (text: string) => {
64 const effort = text.match(/Set effort level to (\w+)/)?.[1]
65 return effort !== undefined || text.includes('Set model to') ? { effort } : null
66}
67
68// `claude -p /usage` 裡的「Current week (Fable): 17% used · resets Oct 11 at 4:59am (Asia/Taipei)」;
69// 重置時間是本機時區。沒有這一列(帳號沒有 Fable 額度)回 null。
70export const fableUsage = (text: string, now: Date): RateLimit | null => {
71 const m = text.match(
72 /^Current week \(Fable\): ([\d.]+)% used(?: · resets (\w{3}) (\d{1,2}) at (\d{1,2})(?::(\d{2}))?(am|pm))?/m,
73 )
74 if (m === null) {
75 return null
76 }
77 const [, pct, month, day, hour, minute, half] = m
78 if (month === undefined) {
79 return { kind: 'fable', percentUsed: Number(pct) }
80 }
81 const at = new Date(
82 now.getFullYear(),
83 MONTHS.indexOf(month),
84 Number(day),
85 (Number(hour) % 12) + (half === 'pm' ? 12 : 0),
86 Number(minute ?? 0),
87 )
88 // 十二月看到一月的重置日:跨年。
89 if (now.getTime() - at.getTime() > 86_400_000) {
90 at.setFullYear(at.getFullYear() + 1)
91 }
92 return { kind: 'fable', percentUsed: Number(pct), resetsAt: at.toISOString() }
93}
94
95// 和在輸入框打 `/<command>` 一樣:記進對話,模型回覆中則排隊等這一輪結束。
96// `/model`、`/effort` 打開選單,選完的變動由 session.append 讀回來。
97const run = ($: EngineInterface, command: string) => $.command.run({ command })
98
99// 和自己在輸入框打「繼續工作」再按 Enter 一樣;模型回覆中時,等這一輪結束才送出。
100const CONTINUE_TEXT = '繼續工作'
101const continueWork = ($: EngineInterface) => $.prompt.submit({ text: CONTINUE_TEXT, asUser: true })
102
103// 清空輸入框。Claude 工作中按 Esc 會中斷這一輪而不是清掉打到一半的字,這顆鈕只清輸入框。
104const clearInput = ($: EngineInterface) => $.prompt.fill({ text: '' })
105
106// `/compact`、`/clear` 都會改掉整段對話,清空輸入框會丟掉打好的字,所以都要按兩次:第一次只把
107// 鈕換成 `↓ compact?`、`× clear?`、`⌫ clear input?`,3 秒內再按同一顆才執行,否則恢復原狀;
108// 改按另一顆就換成等那一顆。比對指令與按下的時間,免得上一次的計時把新的一次提早收掉。
109const CONFIRM_MS = 3000
110
111const pressTwice = async ($: EngineInterface, command: Armed['command']) => {
112 if ((await read($, armed))?.command === command) {
113 await update($, armed, () => null)
114 await (command === 'erase' ? clearInput($) : run($, command))
115 return
116 }
117 const at = await $.clock.now()
118 await update($, armed, () => ({ command, at }))
119 try {
120 await $.clock.sleep(CONFIRM_MS)
121 } catch {
122 // 計時中模組被卸下(/reload-plugins、session 結束)會中止等待;那時沒有東西要恢復。
123 return
124 }
125 await update($, armed, a => (a?.command === command && a.at === at ? null : a))
126}
127
128// 不等第一輪對話,先把模型、設定的 effort 與用量讀進來。`/resume` 接回的對話帶著它最後一次
129// 回覆的 token 數(contextTokens),ctx 照它算;這時引擎的值還是換過去之前那段對話的。
130const refresh = async ($: EngineInterface, contextTokens?: number) => {
131 const [model, measured, settings] = await Promise.all([
132 $.session.model(),
133 $.session.usage(),
134 $.settings.read(),
135 ])
136 const configured = typeof settings.effortLevel === 'string' ? settings.effortLevel : null
137 const window = measured.context.window
138 const percent = contextTokens === undefined ? measured.context.percent : Math.round((contextTokens / window) * 100)
139 await update($, usage, u => ({
140 ...u,
141 model,
142 effort: u.effort ?? configured,
143 contextPercent: percent ?? null,
144 rateLimits: measured.rateLimits,
145 }))
146 if (percent === undefined) {
147 await estimateContext($, window)
148 }
149}
150
151// compact 換掉對話後,引擎要等下一次回覆才有新的 ctx,在那之前還是 compact 前的數字。
152// 新的對話在 session.compact 的 hook 都回去之後才換上,所以等一下再讀,改用本機估算。
153const refreshAfterCompact = async ($: EngineInterface) => {
154 try {
155 await $.clock.sleep(0)
156 } catch {
157 return
158 }
159 await refresh($)
160}
161
162// 引擎要等第一次回覆才有 ctx(開 session、/clear、compact 之後)。這之前先用本機估算:系統提示、
163// 工具、記憶檔與對話已經佔掉的量,不發請求、不呼叫模型;回覆後由 session.measure 換成實際值。
164// 估不出來就維持空的,畫成 0%。
165const estimateContext = async ($: EngineInterface, window: number) => {
166 const { context } = await $.session.usage({ breakdown: 'summary' })
167 const tokens = context.breakdown?.totalTokens
168 if (tokens !== undefined) {
169 const estimate = Math.round((tokens / window) * 100)
170 await update($, usage, u => (u.contextPercent === null ? { ...u, contextPercent: estimate } : u))
171 }
172}
173
174// 引擎只回報 five_hour、seven_day;Fable 的週用量只在 `/usage`,借 `claude -p /usage` 讀。
175// 一次約 2~4 秒、不呼叫模型,所以背景跑、最多每 5 分鐘一次。`claude -p` 也會載入這個
176// mod,只在互動 session 讀,免得一路生下去。
177const FABLE_EVERY_MS = 5 * 60 * 1000
178let interactive = false
179let fableReadAt = 0
180
181const refreshFable = async ($: EngineInterface) => {
182 if (!interactive || Date.now() - fableReadAt < FABLE_EVERY_MS) {
183 return
184 }
185 fableReadAt = Date.now()
186 try {
187 const { stdout } = await $.process.run(
188 ['claude', '-p', '/usage', '--no-session-persistence', '--strict-mcp-config'],
189 { timeoutMs: 20_000 },
190 )
191 const fable = fableUsage(stdout, new Date())
192 await update($, usage, u => ({ ...u, fable }))
193 } catch {
194 // 找不到 claude 或逾時:Fable 維持上一次的值,5 分鐘後再試。
195 }
196}
197
198export const register: Register = on => {
199 on('session.start', async ($, e, next) => {
200 interactive = e.isInteractive
201 await refresh($)
202 void refreshFable($)
203
204 return next(e)
205 })
206
207 // `/clear`、`/resume` 換成另一個 session,卻不發 session.start;新 session 的值是空的,
208 // 模型、effort 與用量會空白到第一輪對話結束。
209 on('classic.SessionStart', async ($, e, next) => {
210 if (e.source === 'clear' || e.source === 'resume') {
211 await refresh($, e.context_tokens)
212 fableReadAt = 0
213 void refreshFable($)
214 }
215
216 return next(e)
217 })
218
219 // 主對話的 compact(`/compact`、自動 compact、ctx 旁的 ↓);precompute 只是先算好摘要,對話不變。
220 on('session.compact', async ($, e, next) => {
221 const done = await next(e)
222 if (e.agentId === undefined && e.trigger !== 'precompute' && done.skip === undefined) {
223 void refreshAfterCompact($)
224 }
225
226 return done
227 })
228
229 // 開 session 後不久也會量一次,那時還沒有回覆、沒有 ctx;保留目前的值(本機估算),
230 // 不要蓋成空的。
231 on('session.measure', async ($, e, next) => {
232 await update($, usage, u => ({
233 ...u,
234 contextPercent: e.context.percent ?? u.contextPercent,
235 rateLimits: e.rateLimits,
236 }))
237 void refreshFable($)
238
239 return next(e)
240 })
241
242 // 主對話送出請求時的 effort;subagent 的請求帶 agentId,不算。
243 on('turn.step', async function* ($, e, next) {
244 if (e.agentId === undefined) {
245 const effort = e.effort === undefined ? null : String(e.effort)
246 await update($, usage, u => ({ ...u, model: e.model, effort }))
247 }
248
249 return yield* next(e)
250 })
251
252 // `/effort`、`/model` 不送模型請求,turn.step 看不到;它們的 command.run 又在
253 // 選單一打開就結束。選單關掉後留下的輸出列(`Set effort level to max (this
254 // session only): ...`、`Set model to ...`)會記進對話,從這裡讀。只看 door
255 // `command`,模型與使用者自己打的同一句話不算。
256 on('session.append', { door: 'command' }, async ($, e, next) => {
257 const kept = await next(e)
258 const change = pickerChange(rowText(e.message.content))
259 if (change !== null) {
260 const model = await $.session.model()
261 await update($, usage, u => ({ ...u, model, effort: change.effort ?? u.effort }))
262 }
263
264 return kept
265 })
266
267 // 畫在輸入框下方的提示列位置:引擎自己的那一列(`⏵⏵ auto mode on`、`? for shortcuts`)
268 // 永遠在最上面,狀態列排在它下面。
269 on('ui.render', { component: 'PromptHint' }, async ($, e, next) => {
270 const hint = await next(e)
271 const { Box, Button, Text } = $.ui.resolve(e)
272 const u = await read($, usage)
273
274 // 模型、effort 的膠囊字色跟著主題反白;百分比膠囊固定白字。
275 const pill = (text: string, background: string, color: string) => (
276 <Text backgroundColor={background} color={color} bold>
277 {` ${text} `}
278 </Text>
279 )
280
281 // 膠囊旁的 ▾:按下送出同名指令,滑鼠移上去會反白。Button 的字不能上色,所以白字
282 // 膠囊只能是文字。不等指令結束:選單開著或 compact 跑超過 10 秒,按鈕會被引擎判逾時。
283 const control = (command: string) => (
284 <Button key={command} label="▾" plain dimColor onPress={() => void run($, command)} />
285 )
286
287 // ctx 旁的 ↓ compact(把對話壓下去)、→ 送出「繼續工作」、× clear(清掉對話)、⌫ 清空輸入框;
288 // ↓、×、⌫ 要按兩次,等第二次時鈕上寫著它的名稱。
289 const waiting = (await read($, armed))?.command ?? null
290 const confirmButton = (command: Armed['command'], glyph: string, name: string = command) => (
291 <Button
292 key={command}
293 label={waiting === command ? `${glyph} ${name}?` : glyph}
294 plain
295 dimColor={waiting !== command}
296 hover={{ scope: `hint:${command}` }}
297 onPress={() => void pressTwice($, command)}
298 />
299 )
300 // 和膠囊之間空一格,跟 7d、5h 那幾列的重置時間一樣。
301 const contextActions = (
302 <Box flexShrink={0} marginLeft={1}>
303 {confirmButton('compact', '↓')}
304 <Text> </Text>
305 <Button
306 key="continue"
307 label="→"
308 plain
309 dimColor
310 hover={{ scope: 'hint:continue' }}
311 onPress={() => void continueWork($)}
312 />
313 <Text> </Text>
314 {confirmButton('clear', '×')}
315 <Text> </Text>
316 {confirmButton('erase', '⌫', 'clear input')}
317 </Box>
318 )
319
320 // 寬版 `ctx ▰▰▰▰▱▱▱▱▱▱ [38%]`,進度條依用量上色。窄版沒有進度條,標籤放進膠囊 `[ctx 38%]`,
321 // 百分比靠右補到 3 格,同一欄的膠囊一樣寬。標籤都補到同欄一樣寬。
322 const isNarrow = (e.viewport?.columns ?? WIDE_COLUMNS) < WIDE_COLUMNS
323 const narrowText = (label: string, pct: number | null) => `${label} ${percent(pct).padStart(3)}`
324 const gauge = (label: string, pct: number | null, width: number) =>
325 isNarrow ? (
326 pct === null ? (
327 <Text dimColor>{` ${narrowText(label, pct)} `}</Text>
328 ) : (
329 pill(narrowText(label, pct), pillColor(pct), WHITE)
330 )
331 ) : (
332 <Text>
333 <Text dimColor>{`${label} `}</Text>
334 <Text color={pct === null ? undefined : colorFor(pct)} dimColor={pct === null}>
335 {`${bar(pct ?? 0, width)} `}
336 </Text>
337 {pct === null ? <Text dimColor>{` ${percent(pct)} `}</Text> : pill(percent(pct), pillColor(pct), WHITE)}
338 </Text>
339 )
340
341 const resets = (r: RateLimit | null | undefined, when: (d: Date) => string) =>
342 r?.resetsAt !== undefined && <Text dimColor>{` ↻${when(new Date(r.resetsAt))}`}</Text>
343
344 const fiveHour = u.rateLimits.find(one => one.kind === 'five_hour')
345 const sevenDay = u.rateLimits.find(one => one.kind === 'seven_day')
346
347 const modelPill = (
348 <Box>
349 {pill(modelName(u.model), 'gray', 'inverseText')}
350 {control('model')}
351 </Box>
352 )
353 const effortPill = u.effort !== null && (
354 <Box>
355 {pill(u.effort, EFFORT_COLOR[u.effort] ?? 'gray', 'inverseText')}
356 {control('effort')}
357 </Box>
358 )
359
360 // 滑鼠移到四顆鈕上,鈕的右邊浮出反白的名稱標籤,像工具提示一樣暫時蓋在 5h 那一欄上,
361 // 不推擠版面。後畫的元素蓋在先畫的上面,所以標籤放在整列最後、位置用前面的寬度算:
362 // 寬版先是模型/effort 那一欄與 3 格間隔,ctx 列是「ctx 」、進度條、百分比膠囊;窄版的 ctx
363 // 列在模型下面那一列,只有放進標籤的膠囊。再空一格才是鈕。四個標籤補到一樣寬,每個都整個
364 // 蓋住「5h」,不會露出半個字。有一顆在等確認時不浮出,免得和 `compact?` 疊在一起。
365 const ctxPercent = u.contextPercent ?? 0
366 const modelColumn = Math.max(modelName(u.model).length, u.effort?.length ?? 0) + 3 + 3
367 const buttonsAt = isNarrow
368 ? narrowText('ctx', ctxPercent).length + 2 + 1
369 : modelColumn + 4 + 11 + percent(ctxPercent).length + 2 + 1
370 const hoverLabel = (scope: string, text: string) => (
371 <Box
372 position="absolute"
373 top={isNarrow ? 1 : 0}
374 left={buttonsAt + 8}
375 display="none"
376 hover={{ scope, display: 'flex' }}
377 >
378 <Text inverse>{` ${text.padEnd(11)} `}</Text>
379 </Box>
380 )
381 const hoverLabels = waiting === null && [
382 hoverLabel('hint:compact', 'compact'),
383 hoverLabel('hint:continue', 'continue'),
384 hoverLabel('hint:clear', 'clear'),
385 hoverLabel('hint:erase', 'clear input'),
386 ]
387
388 // 用量的配對:短期的 ctx/5h 一列,每週的 7d/Fable 一列。終端機太窄時,5h、Fable
389 // 那欄從尾巴截斷。
390 const ctxRow = (
391 <Box>
392 {gauge('ctx', ctxPercent, 10)}
393 {contextActions}
394 </Box>
395 )
396 const sevenDayRow = (
397 <Text>
398 {gauge('7d ', sevenDay?.percentUsed ?? null, 10)}
399 {resets(sevenDay, dayHhmm)}
400 </Text>
401 )
402 const fiveHourRow = (
403 <Text wrap="truncate-end">
404 {gauge(u.fable === null ? '5h' : '5h ', fiveHour?.percentUsed ?? null, 8)}
405 {resets(fiveHour, hhmm)}
406 </Text>
407 )
408 const fableRow = u.fable !== null && (
409 <Text wrap="truncate-end">
410 {gauge('Fable', u.fable.percentUsed, 8)}
411 {resets(u.fable, dayHhmm)}
412 </Text>
413 )
414
415 // 寬版:模型/effort 疊成最左一欄,兩列三欄。窄版:三列兩欄,模型、effort 各在一欄的最上面,
416 // 底下是那一欄的用量,所以每個膠囊都從兩條直線開始;沒有 effort 時那一格空著。
417 return (
418 <Box flexDirection="column">
419 {hint}
420 {isNarrow ? (
421 <Box>
422 <Box flexDirection="column" flexShrink={0} marginRight={3}>
423 {modelPill}
424 {ctxRow}
425 {sevenDayRow}
426 </Box>
427 <Box flexDirection="column">
428 {effortPill || <Text> </Text>}
429 {fiveHourRow}
430 {fableRow}
431 </Box>
432 {hoverLabels}
433 </Box>
434 ) : (
435 <Box>
436 <Box flexDirection="column" flexShrink={0} marginRight={3}>
437 {modelPill}
438 {effortPill}
439 </Box>
440 <Box flexDirection="column" flexShrink={0} marginRight={3}>
441 {ctxRow}
442 {sevenDayRow}
443 </Box>
444 <Box flexDirection="column">
445 {fiveHourRow}
446 {fableRow}
447 </Box>
448 {hoverLabels}
449 </Box>
450 )}
451 </Box>
452 )
453 })
454}
455types/index.d.ts 20 lines1export type RateLimit = { kind: string; percentUsed: number; resetsAt?: string }
2
3export type Usage = {
4 model: string
5 effort: string | null
6 contextPercent: number | null
7 rateLimits: RateLimit[]
8 // Fable 的週用量,讀自 `claude -p /usage`;帳號沒有這一列時是 null。
9 fable: RateLimit | null
10}
11
12// 按了一次、等第二次按下確認的鈕,與第一次按下的時間。
13export type Armed = { command: 'compact' | 'clear' | 'erase'; at: number }
14
15declare module 'claude-code' {
16 interface PluginState {
17 statusbar: { usage: Usage; armed: Armed | null }
18 }
19}
20