SLOPSHOPPER

statusbar

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

newspinnerprocess
v0.7.1no licenseupdated 2026-10-08MomoChenisMe/claude-code-mods/statusbar
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · statusbar
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ⟨Claude Code's own drawing⟩ Opus 5.5 ▾ ctx ▰▰▰▰▰▱▱▱▱▱ 49% ↓ → × ⌫ 5h ▰▰▱▱▱▱▱▱ 31% ↻09:53 7d ▱▱▱▱▱▱▱▱▱▱ --%

Draws

Prompt hint
⟨Claude Code's own drawing⟩ Opus 5.5 ▾ ctx ▰▰▰▰▰▱▱▱▱▱ 49% ↓ → × ⌫ 5h ▰▰▱▱▱▱▱▱ 31% ↻09:53 7d ▱▱▱▱▱▱▱▱▱▱ --%
README

statusbar:Claude Code 的用量狀態列

English

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

statusbar 截圖:auto mode 下方兩列,模型/effort、ctx/7d、5h/Fable 三欄

⏵⏵ 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
  • 模型、effort:膠囊樣式,模型是灰底。effort 依等級上色:low 綠、medium 黃、high 橘、xhigh 和 max 紅。按膠囊右邊的 ▾ 等於送出 /model、/effort,直接打開切換選單;切換後膠囊立刻跟著變。
  • ctx:這段對話用掉多少上下文視窗。開 session、/clear 或 compact 之後、模型第一次回覆之前,Claude Code 還沒有這個數字,這時顯示本機估算值(系統提示、工具、記憶檔與對話已經佔掉的量;不發請求、不呼叫模型),回覆後換成實際值。/resume 接回的對話顯示它最後一次回覆時的實際值。百分比右邊空一格是四顆鈕。滑鼠移到鈕上,旁邊會浮出反白的名稱標籤,暫時蓋住 5h 那一欄,移開就消失:
  • ↓ 等於在輸入框送出 /compact;模型正在回覆時,會排隊等這一輪結束再壓縮。
  • → 等於在輸入框打「繼續工作」再按 Enter,按一次就送出;模型正在回覆時,會等這一輪結束再送出。
  • × 等於送出 /clear。
  • ⌫ 清空輸入框。Claude 工作中按 Esc 會中斷這一輪,不會清掉打到一半的字;這顆鈕只清輸入框,工作照常進行。
  • ↓、× 會改掉整段對話,⌫ 會丟掉打好的字,都要按兩次,免得誤觸:第一次按下變成 ↓ compact?、× clear? 或 ⌫ clear input?,3 秒內再按同一顆才執行,否則恢復原狀。
  • 符號的分工:▾ 是打開選單;→ 是請模型繼續;↓(往下壓)和 ×(清掉)是對這段對話動手;⌫(往左刪除)只動輸入框。
  • 5h、7d:5 小時與每週的用量額度,後面是重置時間。訂閱帳號才有這兩項。
  • Fable:Fable 的每週用量,後面是重置時間。帳號有 Fable 額度才顯示。
  • 進度條依用量上色:未滿 50% 綠、未滿 75% 黃、未滿 90% 橘、其餘紅。百分比膠囊的底色:未滿 50% 藍、未滿 75% 玫瑰紅、其餘紅。
  • 百分比膠囊固定白字、深色底,不跟著主題變。
  • Claude Code 自己的提示列(? for shortcuts、auto mode on)照常顯示在最上面,狀態列在它下面。

終端機不到 85 欄時,改成三列兩欄、不畫進度條的窄版。模型和 effort 分別在兩欄的最上面,用量的標籤放進百分比膠囊,所以每一欄的膠囊都對齊。用量的配對和寬版一樣:

statusbar 窄版截圖:72 欄的終端機,auto mode 下方三列兩欄,模型/ctx/7d 一欄、effort/5h/Fable 一欄,標籤都在膠囊裡

窄版大約要 49 欄。再窄的話,5h、Fable 那欄會從尾巴切掉,最先切掉的是重置時間。

需求

  • Claude Code 2.1.290 以上(以這一版測試)。mod 的 API 目前是 early access,Claude Code 改版時,這個 mod 可能要跟著更新。
  • 用滑鼠點按鈕(▾、↓、→、×、⌫)需要 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 拿掉。

已知限制

  • 開 session、/clear 或 compact 之後的 ctx 是估算值,和第一次回覆後的實際值大約差 1 個百分點。
  • → 送出的「繼續工作」是固定的中文,不能改,也不跟著語言設定變。
  • 等第二次按下的 3 秒內,鈕旁邊多出的名稱會把右邊那一欄往右推(compact? 約 6 格、clear input? 約 10 格);按下或逾時後回到原位。
  • ▾ 是膠囊旁的小符號,不是整顆膠囊:mod 的按鈕不能設定字的顏色,白字膠囊只能是文字。
  • Claude Code 沒有把 Fable 的用量交給 mod,所以 statusbar 在背景跑 claude -p /usage 讀出來:開 session 時一次,之後最多每 5 分鐘一次,每次約 2~4 秒、不呼叫模型。Fable 數字因此最多落後 5 分鐘。
  • 切換 /model 後,模型名稱立刻變;但 /model 的輸出不含 effort,所以 effort 要等下一次送出訊息才更新。
  • effort 是從 /effort 的英文輸出 Set effort level to … 讀出來的。Claude Code 改了這句話時,effort 一樣會在下一次送出訊息時更新。

開發

claude plugin validate statusbar
claude plugin test statusbar

本機開發方式見 repo 的 README。

Source 2 files
hooks/register.tsx 455 lines
1import { 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}
455
types/index.d.ts 20 lines
1export 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