Claude の使用量(5時間枠・週間枠)をデスクトップに常駐表示する Windows ウィジェット「ClaudeUsageTray」を作りました。前回の NoToDo に続くデスクトップウィジェット第2弾です。

ソースコードは GitHub で公開しています。
https://github.com/wadokon/claude-usage-tray
なぜ作ったのか
「あと何回使えるか」が見えないまま作業していた
Claude Code を使っていると、5時間ごとの利用枠と週間の利用枠があります。困るのは、作業中に「いま残りどれくらいか」をすぐ確かめる手段が意外と乏しいことでした。
作業に集中しているときほど確認を忘れ、いざ大きめのタスクを投げようとしたら枠を使い切っていた、ということが何度かありました。「残りどれくらいか」は、わざわざ見にいく情報ではなく常に視界にあってほしい情報です。
この「開くのが面倒だから常駐させる」という発想は、前作の TODO ウィジェットとまったく同じです。自分の場合、ツールを作る動機はだいたいここに行き着くようです。
ステータスラインでは足りなかった
Claude Code にはステータスラインがあり、設定すれば使用率を表示できます。実際しばらくこれで運用していたのですが、自分の使い方には合いませんでした。
ひとつは、プロンプトの入力待ちのあいだは表示が更新されないこと。次に何を頼むか考えている時間こそ「この残量で大丈夫か」を判断したいのに、その瞬間の数字は少し前のままです。リアルタイムに確認する用途には向いていませんでした。
もうひとつは、Fable の使用率がわからないことです。モデルごとに枠が分かれている以上、全体の残量だけ見えても判断材料としては足りません。実際に効いてくるのはモデル別の残量のほうでした。
そこで、Claude Code の中ではなくデスクトップそのものに置くことにしました。ターミナルを見ていなくても、入力待ちで手が止まっていても、いつでも同じ場所に最新の残量がある。この「常にそこにある」状態が欲しかったものでした。
数字ではなく「色」で判断したかった
常駐させるうえでこだわったのは、視界の端に置いても意味がわかることです。残量に応じて緑(余裕)・橙(注意)・赤(残りわずか)と色が変わるようにしたので、数字を読まなくても状況が伝わります。

タスクトレイのアイコン自体も、5時間枠の残量を数字で描き、下のバーで週間枠の残量を示すようにしました。ウィジェットを隠していても、トレイを見れば残量がわかります。
大変だったこと
Windows 版は C#(WinForms)の1ファイルで約690行です。その中で手間がかかったのは次の2か所でした。
使用量レスポンスの構造を読み解く
いちばん手間取ったのは、返ってくるデータの構造を理解する部分でした。5時間枠は単純に利用率が入っているのですが、週間枠は配列で返ってきて、しかも「全体の週間枠」と「モデル別の週間枠」が同じ配列に混在しています。
見分ける手がかりは、その要素がモデルの情報を持っているかどうかだけ。持っていればモデル別、持っていなければ全体、という判定を入れて、ウィジェットの行を組み立てるようにしました。アカウントの種類によって返ってくる中身が変わるため、要素が1つも無かった場合は別のフィールドを見にいくフォールバックも用意しています。

- 5時間枠はトップレベルに1つだけ
使用率の項目名はutilization - 週間枠は
limits配列に入り、複数返ってくる
使用率の項目名はpercent - 週間枠のうち、
scope.modelを持つものはモデル別の枠
持たないものが全体の枠
同じ「使用率」なのに utilization と percent で名前が違うので、片方だけ見て書くと必ず片方が動きません。
// 5時間枠:トップレベルに1つだけ。項目名は utilization
var five = (Dictionary<string, object>)json["five_hour"];
int fiveRemain = Math.Max(0, 100 - (int)Math.Round(Convert.ToDouble(five["utilization"])));
// 週間枠:limits 配列に複数入る。項目名は percent
var overall = new List<Dictionary<string, object>>();
var perModel = new List<Dictionary<string, object>>();
var limits = json.ContainsKey("limits") ? json["limits"] as object[] : null;
if (limits != null)
{
foreach (var o in limits)
{
var lim = o as Dictionary<string, object>;
if (lim == null || (string)lim["group"] != "weekly") continue;
// scope.model があればモデル別、無ければ全体の枠
var scope = lim.ContainsKey("scope") ? lim["scope"] as Dictionary<string, object> : null;
var model = (scope != null && scope.ContainsKey("model"))
? scope["model"] as Dictionary<string, object> : null;
if (model != null) perModel.Add(lim); else overall.Add(lim);
}
}
要素が1つも無いときに見にいく「別のフィールド」は seven_day です。コード上は limits を持たない旧レスポンス向けのフォールバックとして残しています。
else
{
// limits が無い旧レスポンス向けのフォールバック
var seven = (Dictionary<string, object>)json["seven_day"];
int remain = Math.Max(0, 100 - (int)Math.Round(Convert.ToDouble(seven["utilization"])));
rows.Add(new UsageRow { Label = "週", Remain = remain, ... });
}
公式のドキュメントがあるわけではないので、実際のレスポンスを見ながら「この形なら何を意味しているか」を推測して組み立てる作業でした。表示は数行のバーだけなのに、そこに至るまでの判定がいちばん厚いという、よくある構成になっています。
複数ある週間枠を、1つの数字にどうまとめるか
全体の枠とモデル別の枠が同時に返ってくるので、トレイアイコンに出す「週の残り」を1つ選ぶ必要があります。悩んだ末、いちばん残りが少ないものを採用しました。
// 週の枠が複数あるとき、表示するのは「いちばん残りが少ないもの」
weekWorst = Math.Min(weekWorst, remain);
理由は単純で、先に止まるのはいちばん残りが少ない枠だからです。平均を出すと「全体はまだ余裕がある」と見えてしまい、実際にはモデル別の枠で止まる、ということが起きます。複数の上限があるとき、見るべきは平均ではなく最小値でした。
取得に失敗したときにどう振る舞うか
常駐ツールは、失敗したときの振る舞いが使い心地を決めます。API から取得できなかったときに画面が「エラー」だけになってしまうと、直前まで見えていた情報まで失われて不便です。
そこで、一度でも取得できていれば最後に取れた値を表示し続け、右上に警告マークと最終取得時刻を出すだけにしました。加えて、失敗するたびにポーリング間隔を倍にしていく(最大5分)ようにして、API を叩き続けないようにしています。
実際、開発中にレート制限に当たったのですが、この作りのおかげで「少し前の残量」を見ながら復帰を待てました。エラー処理は地味ですが、常駐ツールでは本体機能と同じくらい重要だと感じます。
コードでは、失敗の種類ごとに次の3つの振る舞いを分けています。
一度でも取れていれば、前回値を残す
最初は失敗したら空欄にしていたのですが、空欄と「残り0%」が見分けられません。 常駐ツールでこれは致命的なので、前回値を残したうえで「いつの値か」を添える形に変えました。
if (widget.Rows.Count > 0)
{
// 一度でも取得できていれば、前回値を残して警告表示にとどめる
widget.IsStale = true;
widget.StaleSince = lastSuccess.ToString("HH:mm");
lastDetail = "[" + msg + "]\n最終取得 "
+ lastSuccess.ToString("HH:mm:ss") + "\n" + lastGoodDetail;
}
else
{
// 一度も成功していないなら、見せられる過去値がない
widget.IsError = true;
widget.ErrorMessage = msg;
}
画面には最終取得時刻を小さく出しています。古い値でも、古いと分かっていれば判断材料になります。
429 は自己判断で待たず、サーバーの指定に従う
レート制限を食らったときに適当な秒数で再試行すると、制限を延ばすだけになります。サーバーが Retry-After を返してくるので、それに従います。
// 429 のときサーバーが返す Retry-After(秒)を待ち時間に変換する
static int RetryAfterMs(WebException web)
{
var res = web != null ? web.Response as HttpWebResponse : null;
if (res != null)
{
var v = res.Headers["Retry-After"];
int sec;
if (!string.IsNullOrEmpty(v) && int.TryParse(v.Trim(), out sec) && sec > 0)
return Math.Min(sec, 86400) * 1000 + 5000; // 少し余裕を足す
}
return MaxBackoffMs;
}
返ってきた秒数には5秒の余裕を足しています。境界ぴったりで叩いて、もう一度弾かれるのを避けるためです。ただし待つのは最長65分までにしていて、ヘッダが無いときは5分待ちます。
それ以外は間隔を倍にしていく
ネットワークが切れているときに元の間隔(2分)で叩き続けても意味がないので、失敗するたびに間隔を倍にし、5分で頭打ちにしています。
if (status == 429)
{
// サーバーが待ち時間を指定してきたら、それに従う
int wait = RetryAfterMs(web);
timer.Interval = Math.Max(PollMs, Math.Min(wait, MaxRateLimitWaitMs));
}
else
{
// 失敗中は間隔を倍にして、API を叩きすぎないようにする
timer.Interval = Math.Min(timer.Interval * 2, MaxBackoffMs);
}
ただし 401 / 403 だけはメッセージを変えています。 認証エラーは待っても直らず、再ログインという人の操作が必要だからです。「自動で直るもの」と「人がやらないと直らないもの」を同じ表示にすると、ユーザーは永遠に待ってしまいます。
公開するにあたって考えたこと
このツールは、使用量の取得に公開ドキュメントに載っていないエンドポイントを使っています。認証も Claude Code のログイン情報を読み取る形です。自分専用ならまったく気にならなかった部分ですが、公開して人に使ってもらうとなると話は変わります。
そのため README の冒頭に、非公式であること・予告なく動かなくなる可能性があること・認証情報をどう扱っているか(読み取るのはローカルのファイルのみで、Anthropic の API 以外へは一切送信しない)を明記しました。ソースを公開していれば、その主張を読み手が自分で検証できます。これは公開する側の責任として最低限やるべきことだと思っています。
便利なツールほど「動けばいい」で済ませたくなりますが、他人の環境で動くものを配る以上、何をしているか説明できる状態にしておきたいところです。
このツールで学んだこと
- 外部APIのレスポンスは、同じ意味の値でも場所ごとに名前が違うことがある
実物を1回全部ダンプしてから書いたほうが早い - 上限が複数あるときは最小値を見る
平均は「まだ余裕がある」という誤った安心を与える - 常駐ツールは、失敗したときの表示のほうが設計が難しい
消す・古い値を出す・エラーを出すのどれにするかを、状況ごとに決めておく
3つ目は、このツールを作るまで意識していませんでした。普段使いのツールは動いている時間より、うまく動かない時間のほうが記憶に残ります。
おわりに
デスクトップウィジェットは、Web アプリと違ってサーバーもデプロイも要らず、思いついたその日に形にできるのが魅力です。今回のように「ちょっと不便」を潰すだけの小さなツールでも、常駐させると体感がはっきり変わります。
Windows 用です。実行ファイルは今は Releases に置いていないので、README の「ビルド」の手順で作成してください(Windows に入っているコンパイラだけで作れて、Visual Studio は不要です)。Claude Code を使っている方はぜひ試してみてください。
https://github.com/wadokon/claude-usage-tray
次に読む記事



