Claude Code を長時間使っていると、「今どのブランチか」「コンテキストをどれだけ使ったか」「ツールを何回呼んだか」を、画面のどこかに出しておきたくなります。Mod を使えば、Claude Code が描く画面の決まった場所に、自分の文字を足せます。この記事では、場所ごとの書き方を、動作を確かめたコードで紹介します。コードは Claude Code 2.1.289 の claude plugin validate と claude plugin test で確認しました。
Mod の基本(plugin.json、hooks.json、register.js の 3 ファイル)は 初めての Mod の作り方 で説明しています。この記事では、各 Mod の register.js と、必要なら hooks.json だけを示します。plugin.json の name と description は、それぞれの Mod に合わせて書き換えてください。
先に結論:出す場所は 4 つ
画面に何かを出す方法は、「どれだけ目立たせたいか」「いつまで残すか」で選びます。
| 場所 | 使うもの | 向いている内容 |
|---|---|---|
| プロンプトの下の 1 行 | $.ui.status(text) | 変えるまで残る、ブランチ名や接続先など |
| スピナーの横 | ui.render の Spinner | Claude が作業している間だけ見たい数値 |
| プロンプトの上の帯 | ui.render の AbovePrompt | 常に見えていてほしい、使用率やタイマー |
| 通知 | $.ui.toast(text) | 一度だけ知らせたい出来事(4 秒で消える) |
迷ったら、まず $.ui.status を使ってください。1 行の関数呼び出しで済み、描画の仕組みを知らなくても動きます。
1. プロンプトの下に、1 行を出す
$.ui.status(text) は、プロンプトの下に 1 行を出し、次に呼び直すまで残します。文字の先頭には ⚠ と Mod の名前が付くので、⚠ branch-line: branch: main のように表示されます。この前置きは Claude Code が付けるので、どの Mod が出した行かが一目で分かります。
次の Mod は、今のブランチ名をこの行に出します。セッションの開始時と、Claude の返答が終わるたびに調べ直します。Claude が git switch でブランチを切り替えることがあるためです。
// 今のブランチを調べて、プロンプトの下の 1 行に出す
async function showBranch($) {
try {
// 引数は配列で渡す。シェルを通さないので、引用符の扱いを気にしなくてよい
const git = await $.process.run(['git', 'branch', '--show-current'])
const branch = git.exitCode === 0 ? git.stdout.trim() : ''
// git のリポジトリでないときは何も出さない
$.ui.status(branch ? 'branch: ' + branch : '')
} catch {
// git が無いなど、起動できなかったとき。表示を足せないだけなので黙って戻る
}
}
export function register(on) {
// セッション開始時に 1 回
on('session.start', async ($, e, next) => {
await showBranch($)
return next(e)
})
// Claude の返答が終わるたびに調べ直す
on('turn.complete', async ($, e, next) => {
await showBranch($)
return next(e)
})
}ポイントは 3 つです。
- 外部コマンドは
$.process.runに配列で渡します。文字列を連結してシェルに渡す形にしないので、ブランチ名に妙な文字が入っていても影響を受けません。 - どのフックも、最後は
next(e)を返します。呼ばずに返すと、同じイベントを受け取る後ろの Mod に届きません。 - 表示を足すだけの Mod は、失敗しても作業を止めてはいけません。上のコードは、
gitがなくても例外を握りつぶして戻ります。
テストは次のとおりです。外部コマンドと画面の代役を on で用意するので、セッションを起動せずに確かめられます。
import { expect, test } from 'claude-code/testing'
test('セッション開始でブランチ名を 1 行に出す', async ($, on) => {
const lines: string[] = []
on('process.run', () => ({ value: { exitCode: 0, stdout: 'feature/login\n', stderr: '' } }))
on('ui.status', ($, e) => {
lines.push(e.text)
return { value: undefined }
})
on('session.start', () => ({ cwd: '/work' }))
await $.session.start({ surface: 'terminal', isInteractive: true, cwd: '/work' })
expect(lines).toEqual(['branch: feature/login'])
})
test('git のリポジトリでなければ空の行にする', async ($, on) => {
const lines: string[] = []
on('process.run', () => ({ value: { exitCode: 128, stdout: '', stderr: 'fatal: not a git repository' } }))
on('ui.status', ($, e) => {
lines.push(e.text)
return { value: undefined }
})
on('session.start', () => ({ cwd: '/work' }))
await $.session.start({ surface: 'terminal', isInteractive: true, cwd: '/work' })
expect(lines).toEqual([''])
})私たちが確かめたテストは、この 2 件で、どちらも通りました。テストの書き方は Mod のテストの書き方 で詳しく説明します。
2. スピナーの横に、数値を足す
Claude が作業している間は、「Thinking…」のようなスピナーが回ります。この行は Claude Code が描いていますが、Mod は描画の一部だけを書き換えることができます。ui.render を { component: 'Spinner' } で絞り、next に props.suffix を変えたイベントを渡すと、スピナーの動きはそのままで、言葉の後ろに文字が付きます。
// ツールごとの回数。下の複数のフックが同じ変数を使う
const counts = {}
function total() {
return Object.values(counts).reduce((sum, n) => sum + n, 0)
}
export function register(on) {
// Claude がツールを使う直前に呼ばれる。数えるだけで、ツールはそのまま動かす
on('tool.call', async ($, e, next) => {
counts[e.tool] = (counts[e.tool] ?? 0) + 1
// 画面を描き直すよう頼む
$.ui.invalidate('ui.render')
return next(e)
})
// スピナーの横に合計を足す。スピナー自体は Claude Code のものを使う
on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
if (total() === 0) return next(e)
return next({ ...e, props: { ...e.props, suffix: ' · ツール ' + total() + ' 回…' } })
})
}この抜粋は、初めての Mod の作り方 で作る Mod から、スピナーに関係する部分だけを取り出したものです。
こう表示されます。
Thinking · ツール 2 回…覚えておきたいのは $.ui.invalidate('ui.render') です。ui.render は、Claude Code が画面を描くたびに呼ばれますが、数値が変わった瞬間に勝手には呼ばれません。値を更新したら、invalidate で描き直しを頼んでください。これを忘れると、次に何かの理由で再描画されるまで、古い数字が出続けます。
Spinner は端末版もデスクトップ版も描く場所です。一方、ToolProgress や TurnDuration のように端末版だけが描く場所もあります。どちらの画面で動かしたいかは、Mod はどこで動くか で整理しています。
3. プロンプトの上の帯に、使用率を出す
常に目に入る場所に出したいなら、プロンプトのすぐ上の「帯」が向いています。帯は全部の Mod で共有する場所です。自分の木(描画の指示)を返すと、自分より後ろの Mod が描いたものを置き換えてしまうので、ほかの Mod の表示を残したいときは await next(e) の結果を自分の Box の子に入れます。
次の Mod は、コンテキストの使用率を帯に 1 行出し、利用枠(レート制限)が 90% を超えたら通知します。値は session.measure から受け取ります。このイベントは、返答のたびと、利用枠の使用率が変わったときに呼ばれます。
// 直近の計測値。session.measure で更新し、ui.render で表示する
let percent = null
// すでに知らせた利用枠の種類。同じ枠で何度も通知しないため
const notified = new Set()
export function register(on) {
// 返答のたびと、利用枠の割合が変わったときに呼ばれる
on('session.measure', async ($, e, next) => {
percent = e.context.percent ?? null
for (const limit of e.rateLimits) {
if (limit.percentUsed >= 90 && !notified.has(limit.kind)) {
notified.add(limit.kind)
$.ui.toast(limit.kind + ' の枠を ' + limit.percentUsed + '% 使いました')
}
}
// 画面を描き直すよう頼む
$.ui.invalidate('ui.render')
return next(e)
})
// プロンプトの上の帯に、コンテキストの使用率を 1 行足す
on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
// まだ計測値がなければ、何も足さない
if (percent === null) return next(e)
const { Box, Text } = $.ui.resolve(e)
// 他の Mod が帯に描いたものを残して、その下に足す
const theirs = await next(e)
return Box({
flexDirection: 'column',
children: [theirs, Text({ bold: percent >= 80, dimColor: percent < 80, children: ['コンテキスト ' + percent + '% 使用'] })],
})
})
}hooks.json は、モジュールを 1 つ指すだけです。
{
"description": "The usage-note hooks module",
"modules": ["./register.js"]
}この Mod には、設計上の工夫が 3 つあります。
- 計測がまだなら、何も足さない。
percentがnullのあいだはnext(e)をそのまま返すので、帯は Claude Code の元の状態のままです。 - 使用率が 80% を超えたら太字にする。 80% 未満は薄い色(
dimColor)で目立たせません。「普段は邪魔をしないが、危なくなったら目に入る」形です。 - 通知は 1 種類の枠につき 1 回だけ。
notifiedに覚えておかないと、使用率が変わるたびに同じ通知が出て、うるさくなります。
画面の木が正しくないと、どうなるか
ui.render が返す木に、使えない属性が入っていたり、形が合っていなかったりすると、Claude Code は自分の描画に戻します。画面にエラーは出ません。--plugin-dir で起動したセッションなら、ui.render (Pane) refused: ... のような 1 行が会話に出ます。「出るはずの表示が出ない」ときは、まずこの行を探してください。原因の探し方は Mod が動かないときの確認手順 にまとめています。
帯のテスト
画面の描画は、$.ui.mount で確かめられます。Claude Code の描画の代役を on('ui.render', ...) で用意し、自分の Mod が足した文字を find で探します。
import { expect, test } from 'claude-code/testing'
const BAND = {
plugin: 'usage-note',
component: 'AbovePrompt',
requestId: 'band',
viewport: { columns: 100, rows: 30 },
props: { hasSurvey: false, isWorking: false, maxRows: 6, bodyColumns: 80, scroll: { offset: 0, bodyRows: 6 }, view: {} },
surface: 'terminal',
} as const
test('計測値が届くと、帯にコンテキストの使用率が出る', async ($, on) => {
on('session.measure', ($, e) => ({ changed: e.changed }))
on('ui.render', () => ({ type: 'Text', props: {}, children: ['drawn by Claude Code'] }))
await $.session.measure({ context: { window: 200000, tokens: 90000, percent: 45 }, rateLimits: [], changed: ['context'] })
const ui = await $.ui.mount(BAND)
expect(await ui.find({ type: 'Text', text: 'コンテキスト 45% 使用' })).toBeDefined()
})
test('利用枠が 90% を超えたら、1 回だけ通知する', async ($, on) => {
const toasts: string[] = []
on('ui.toast', ($, e) => {
toasts.push(e.text)
return { value: undefined }
})
on('session.measure', ($, e) => ({ changed: e.changed }))
const reading = { context: { window: 200000 }, rateLimits: [{ kind: 'five_hour', percentUsed: 91 }], changed: ['rateLimits'] }
await $.session.measure(reading)
await $.session.measure(reading)
expect(toasts).toEqual(['five_hour の枠を 91% 使いました'])
})2 件とも通りました。2 件目では同じ計測値を 2 回送り、通知が 1 回だけだと確かめています。
4. 通知(トースト)を出す
一度だけ知らせたいことは、$.ui.toast(text) が向いています。何も指定しなければ 4 秒で消えます。全画面表示では右上の箱、従来の表示ではプロンプトの下の右側に出ます。上の usage-note のように、session.measure の中で呼べば、利用枠が危なくなった瞬間に知らせられます。
通知は消えるので、あとから見返したい情報には向きません。見返したいなら、$.ui.status の行に残すか、帯に出してください。
5. 使い分けの目安
- 1 つの値を、変えるまで見せたい →
$.ui.status。最も簡単で、失敗しにくい。 - 作業中だけ見せたい → スピナーの
suffix。余計な場所を取らない。 - 常に見せたい、書式も変えたい → 帯。色や太字を使えるが、木を正しく組む手間がある。
- 一度きりの知らせ →
$.ui.toast。 - 複数の画面やボタンが要る → ペイン(
$.ui.open)。この記事の範囲を超えるので、公式ドキュメントの「Draw in the interface with a mod」を参照してください。
Mod を作ったら、他人に渡す前に Mod の安全チェックリスト で、使っている API が何を読むかを確かめてください。画面に出すだけの Mod でも、$.process.run で外部コマンドを動かすなら、確認の対象になります。
まとめ
- 画面に出す場所は、プロンプトの下の 1 行、スピナーの横、上の帯、通知の 4 つ。迷ったら
$.ui.statusから始める。 - 値を変えたら
$.ui.invalidate('ui.render')で描き直しを頼む。 - 帯は共有の場所なので、
await next(e)の結果を子に入れて、ほかの Mod の表示を残す。 - 表示だけの Mod は、失敗しても作業を止めないように、例外を握る。
claude plugin testの$.ui.mountで、描画も確かめられる。
ほかの人の書き方と見比べたいときは、ModsCode の MCP サーバー の find_code_examples に status や band を渡すと、最小の動く形と、公開されている Mod の実際のコードが返ります。