ui.renderイベントを発生させ、そのイベントのフックはそこに何を描画するかを返します。
このマップは、モッドがターミナルセッションのどこに描画できるかを示しています。
1 つのプロップまたは制限を調べるには、リファレンスを参照してください。
タブ付きペインを構築する
このセクションでは、/hello-tabs コマンドを追加するモッドを構築します。このコマンドはペインを開きます。ペインは、広いフルスクリーンターミナルではトランスクリプトの横にあるサイドバー、またはそれ以外の場合はプロンプト上部のフレーム領域です。このペインは 2 つのタブを表示し、2 番目のタブにはカウンターに 1 を加えるボタンがあります。カウントは Claude Code を再起動した後も残ります。
完成したモッドは次のようになります。記録はペインを開き、2 番目のタブに切り替え、ボタンを数回押し、最初のタブに戻ります。
1
プラグインを作成する
モッドはマニフェスト、
hooks.json がコードを指す、およびコードファイルを持つプラグインです。モッドを作成するは各ファイルについて説明しています。hello-tabs という名前のディレクトリを作成し、その中に .claude-plugin と hooks ディレクトリを作成してから、最初の 2 つのファイルを保存します。マニフェストを hello-tabs/.claude-plugin/plugin.json として保存します。hello-tabs/.claude-plugin/plugin.json
hello-tabs/hooks/hooks.json でエントリーポイントに名前を付けます。hello-tabs/hooks/hooks.json
2
コードを書く
コードは 3 つのジョブを実行し、各フックで 1 つずつ実行します。各フックはコードが明確にしていないことも実行します。
/hello-tabsコマンドを追加する- そのコマンドを実行するときペインを開く
- ペインのコンテンツを描画する。タブの行とオープンタブのボディ
tab と count がペインの状態を保持します。これを hello-tabs/hooks/register.js として保存します。hello-tabs/hooks/register.js
- **
session.start**は$.storeから保存されたカウントも読み込みます。これはセッション間で永続化するキー値ストアです。 - **
command.run**は Claude Code にペインが存在することを伝えるだけです。ペインを開くこと自体は何も描画しません。Claude Code はui.renderを発生させてそこに何が入るかを尋ねます。 - **
ui.render**は要素ツリーを返します。これは他のボックス、テキスト、ボタンを保持するBoxであり、実行されるたびにtabとcountから再度構築されます。
onPress コールバックが実行され、変数が変更され、redraw が呼ばれます。Claude Code は ui.render フックを再度実行し、フックは新しい値から新しいツリーを構築します。すべてのインタラクティブな描画はそのレンダーサイクルを使用します。コールバックが状態を変更し、フックが新しい状態から再度レンダリングします。3
ペインを開く
シェルで
claude --plugin-dir ./hello-tabs で Claude Code を起動します。Claude Code プロンプトで /hello-tabs を実行します。ペインが開き、上部に 1: One と 2: Two が表示されます。2 を押してから、Add one のホットキーである a を数回押します。カウントが上がります。4
カウントが保存されたことを確認する
Esc を押してペインを閉じ、セッションを終了します。シェルで同じ
claude --plugin-dir ./hello-tabs コマンドで Claude Code を再度起動し、Claude Code プロンプトで /hello-tabs を実行します。カウントは残したままです。カウントをクリアするには、モッドに $.store.delete('count') を呼び出させます。状態を保持するは各種類の値がどのくらい続くかをカバーしています。描画する場所を選ぶ
ui.render フックは、希望するレンダーサイトに絞り込まない限り、すべてのレンダーサイトに対して実行されます。レンダーサイトを選択するには、on の 2 番目の引数としてマッチャーと呼ばれるフィルターを渡します。{ component: 'Pane' } はペインに対してのみフックを実行します。フック内では、e.component がサイトに名前を付け、e.surface はどのアプリが描画しているかを示し、e.props はサイト独自のデータを保持します。ペインの場合、e.requestId はそれを開いた id です。
2 つのサイトはモッドがそれらを埋めるまで空です。ペインとバンドです。タブを選択して、各サイトが何であり、どのように描画するかを確認してください。
- ペイン
- プロンプト上部のバンド
ペインは、広いフルスクリーンターミナルではトランスクリプトの横にあるサイドバー、またはそれ以外の場合はプロンプト上部のフレーム領域です。複数のペインがオープンされている場合、各ペインはそのタイトルを表示するタブを取得します。ペインは、モッドが
$.ui.open を id で呼び出すときに表示されます。例えば $.ui.open({ id: 'hello-tabs' }) のように。適切なタイミングでペインを開くは他のフィールドと、ペインがより広いターミナルを待つときをカバーしています。ペインに描画するには、{ component: 'Pane' } でフィルターし、e.requestId が id であることを確認します。Claude Code が既に描画しているものを変更する
Claude Code はそのインターフェースのほとんどを自分で描画します。メッセージ、ツール呼び出し行、スピナーなど。これらの各部分もレンダーサイトであるため、モッドはそれをリスタイルまたは置き換えることができます。1 つを変更するには、ui.render フックをこのテーブルの名前でフィルターします。
Claude Code が既に描画しているサイトでは、フックには 3 つの選択肢があります。詳細を変更する、描画を置き換える、またはそのままにする。タブを選択して、スピナーに適用された各ものを確認してください。例は、チュートリアルモッドのように別のフックがカウントする
calls 変数を読みます。
- 詳細を変更する
- 描画を置き換える
- そのままにする
Claude Code の描画を保持し、その一部を変更するには、変更された スピナーはそのアニメーションと単語を保持し、テキストが単語に続きます。
props を持つイベントのコピーを next に渡します。このフックはスピナーの単語の後のテキストを変更します。AskUserQuestion は 1 つであるため、モッドはそれを変更できます。
ターミナルと Desktop アプリはすべての同じサイトを発生させません。Pane、AbovePrompt、Spinner、およびトランスクリプトサイトは両方で機能します。他のいくつかのステータス行はターミナルでのみ発生します。レンダーサイトテーブルは各サイトが発生する場所をリストしています。
適切なタイミングでペインを開く
ペインはモッドがそれを開くときにのみ表示されます。どのように、いつ開くかは、キーボードフォーカスを取得するかどうか、どのくらいのスペースを要求するか、および狭いターミナルで表示されるかどうかを決定します。 ペインを開くには、選択したid で$.ui.openを呼び出します。id はペインの名前です。ui.render フックはそれをチェックし、ペインを閉じるときに再度渡します。
id で $.ui.close を呼び出します。
id の他に、$.ui.open はこれらのオプションフィールドを取ります。
Claude が作業している間にコマンドがペインを開くようにするには、コマンドを登録するときに
immediate: true を追加します。それなしでは、ターン中に入力されたコマンドはターンが終わるまで待ちます。
ペインがより広いターミナルを待つとき
モッドが要求されずに開くペインは狭いターミナルに表示されないため、小さな画面を引き継ぐことはできません。表示されるかどうかは、それを開いたものによって異なります。- ユーザーが実行したコマンドやボタンを押すなど、ユーザーが実行したものによって開かれた場合、ペインは任意の幅で表示されます
- タイマーまたは
turn.startフックなど、モッドが自分で実行したものによって開かれた場合、ペインは少なくとも 144 列幅のターミナルでのみ表示されます。ユーザーが一度そのペインを自分で開いた後は、110 列で十分です。
$.ui.open は { isPlaced: true } に解決されます。ペインが待機しているとき、isPlaced は false で、reason は理由を説明する文字列です。待機中のペインはユーザーがそれを開くか、ターミナルを広げるときに表示されます。ペインを開かずに何かが利用可能であることを言うには、$.ui.toast('Your message') を呼び出します。これは数秒後に消える小さな通知を表示します。
要素からツリーを構築する
ui.render フックが返すものは要素ツリーです。これは描画する内容の説明であり、ボックス、テキスト、および制御が互いにネストされています。描画を説明し、Claude Code はターミナルまたは Desktop アプリでそれをレンダリングします。
要素を取得するには、フック内で $.ui.resolve(e) を呼び出します。例えば const { Box, Text, Button } = $.ui.resolve(e) のように。各要素は関数です。プロップを渡し、その中に入るべき要素と文字列を children に配置します。
ほとんどの描画は 4 つの要素を使用します。タブを選択して、各要素とターミナルがそれをどのように描画するかを確認してください。
- テキスト
- ボックス
- ボタン
- 入力
Text は文字列を描画し、bold や color などのオプションのスタイリングを使用します。
モジュールが
.tsx または .jsx ファイルの場合、ツリーを JSX として記述できます。$.ui.resolve(e) から要素を分割代入してください。フックモジュールには要素グローバルがないためです。
ツリーがアプリが持たない要素、要素が取らないプロップ、または子が入らない場所を使用する場合、Claude Code はサイトの独自のバージョンを描画します。
--plugin-dir で開始されたセッションでは、トランスクリプト行がそう言います。例えば ui.render (Pane) refused: Text prop "bogusProp" is not allowed; the engine drew its own。デバッグログは ui.render (Pane): a hook returned a tree that does not validate として同じ理由で記録します。他に何も表示されないため、描画が表示されない場合は、その行またはログを確認してください。
色付きセルのグリッドを描画する
ヒートマップ、スパークライン、またはターミナルのゲームボードの場合、各セルに対して 1 つのRaster を描画し、Box ではありません。Raster は key、columns と rows のサイズ、および cells を取ります。これはすべてのセルを 1 つの文字列にパックします。各セルは 3 つの数字です。文字のコードポイント、その色、背景色。色は 0xc62828 のような赤、または 0x01000000 のようなターミナルのデフォルトの 16 進数です。
Desktop アプリには Raster がないため、e.surface をチェックしてそこにテキストを描画します。このペインボディは 3 x 2 のヒートマップを描画します。
rows 配列は変更する部分であり、cellsOf はそれをパックされた文字列に変換します。フックは id が heat のペインでのみ描画するため、$.ui.open({ id: 'heat' }) をコマンドから開きます。hello-tabs の例がそのペインを開く方法のように。
各文字は 1 セル幅である必要があります。既に画面上にある Raster をアニメーション化するには、ペインの id を requestId として、Raster の key、同じサイズ、および新しいセルで $.ui.blit を呼び出します。この例では、$.ui.blit({ requestId: 'heat', key: 'grid', columns: 3, rows: 2, cells: cellsOf(newRows) }) です。ui.render フックを再度実行せずにその 1 つの要素を再描画します。
押下とタイピングに応答する
ユーザーがボタンを押す、フィールドに入力する、またはモッドが描画したリストから選択するとき、Claude Code はそのコントロールに与えた関数を呼び出し、モジュール内で実行されます。各コントロールは独自のコールバックを取ります。Button:onPress(e)を取ります。ここでe.surfaceは押下が来たアプリですInput:onSubmit(value)とonInput(value)を取りますSelect:onSelect(value)を取ります。選択肢はoptionsにあります。少なくとも 1 つの選択肢を持つリスト。例えば[{ value: 'sm', label: 'Small' }, { value: 'lg', label: 'Large' }]
key でコントロールを押すか入力するため、各コントロールに 1 つを与えます。コントロールの各使用はui.press、ui.input、または ui.selectも発生させます。e.element に key があり、別のモッドはそれらのイベントをフックできます。そのフックはコールバックの前に実行されるため、ユーザーが Input に入力するものを見て、それを変更するか、コールバックの代わりに答えることができます。モッド API には別のモッドのボタンを押すメソッドがありません。
キーボードフォーカスとホットキー
モッドはキーボード自体を読むことはありません。ユーザーがキーを押し、Claude Code はそれがコントロールのどれであるかを決定し、そのコントロールのコールバックが実行されます。バンド上の数字ホットキーを除いて、これはペインまたはバンドがキーボードフォーカスを持っている間にのみ発生します。それ以外の場合、キーはプロンプトに移動します。ペインがキーボードフォーカスを取得する方法
ペインは 3 つの方法のいずれかでキーボードフォーカスを取得します。- モッドがコマンドまたは押下から
focus: trueで開く - ユーザーが Ctrl+X を押してから Tab を押す
- ユーザーがそれをクリックする
focus: true をプロンプトが空で、他に何もキーボードフォーカスを持たない間にのみ付与します。ユーザーが入力している間に開くペインはそのキーストロークを取得しません。
各キーが実行すること
このテーブルは、ペインまたはバンドがキーボードフォーカスを持っている間、キーが実行することをリストしています。
モッドは Tab またはアロー キーを他のものにバインドできないため、ゲームは
w、a、s、d で操舵します。
ホットキーと最初のフォーカスを設定する
コントロール上の 2 つのプロップがキーボードがそれに到達する方法を決定します。hotkey: ユーザーがButtonを 1 つのキーで押すことを許可するには、hotkey: 'a'のように 1 つの数字または 1 つの小文字のhotkeyを与えますautoFocus: ペインが開くときどのコントロールがフォーカスを持つかを選択するには、autoFocus: trueを追加します。他のコントロールからプロップを省略してください。Claude Code はautoFocus: falseを拒否します。
ターミナルでは、括弧付きボタンのラベルにキーに名前を付けるか、
plain: true を使用して、ユーザーが何を押すかを見ることができます。要素リファレンスには他の Button ルールがあります。action、バンド上の数字ホットキー、および 1 つのホットキー上の 2 つのボタン。
入力を取得し、各アイテムの行を描画する
多くのペインはテキストフィールドとその下のリストです。このセクションの例はノートペインです。ノートを入力して Enter を押して追加し、各ノートには削除するx ボタンがあります。2 つのノートが追加されたとき、ターミナルはペインをこのように描画します。
- 入力を取得:
Inputはユーザーが Enter を押すとフィールドのテキストでonSubmit(value)を呼び出し、すべての変更でonInput(value)を呼び出します - リストを描画: データを各行にマップし、すべての行のボタンに独自の
keyを与えます
- ノートを追加: 行を入力して Enter を押します。行は新しい行として表示され、フィールドは空になります。
- ノートを削除: Tab を押してノートの
xボタンがフォーカスを持つまで、その後 Enter を押します。xはボタンのラベルであり、ホットキーではないため、文字を入力してもそれを押しません。
hello-tabs と同じレンダーサイクルに従います。コールバックが notes を変更し、redraw を呼び出し、リストを $.store に保存します。
フィールドは各送信後に空になります。これは value プロップのためです。value はフィールドが描画されるときに保持するテキストであり、ユーザーのタイピングはフックが再度フィールドを描画するまでそれを置き換えます。例は常にフィールドを '' で描画します。
例はノートを保存し、それらを読み込みません。次のセッションでそれらを戻すには、hello-tabs が count を読み込む方法で session.start フックでそれらを読み込みます。
3 つのプロップはフィールドの行を構成します。Note: Type a note and press Enter ⏎ add。
Input を送信してもターンを開始しません。コールバックが $.prompt.submit を呼び出さない限り。
サイトを再描画する
描画はスナップショットです。ui.render フックが最後に実行したときに返したものを表示します。何か新しいものを表示するには、フックを再度実行する必要があります。Claude Code はいくつかの変更に対して再度実行し、モッドは残りを要求します。
Claude Code が要求なしで再描画するとき
Claude Code はサイトのプロップが変更されるか、ターミナルの幅が変更されるときにui.render フックを再度実行します。タイマーでフックを実行しません。モジュール内の変数が変更されたときは判断できません。
データが変更されたときに再描画する
データが変更された後にサイトを再度描画するには、$.ui.invalidate('ui.render') を呼び出します。このペインは押下をカウントします。ボタンのコールバックは count を変更し、再描画を要求します。
hello-tabs の例は同じ呼び出しを redraw 関数にラップします。
$.stateに保持する値は呼び出しを必要としません。値を書くことはそれを読むサイトを再描画するためです。
タイマーで再描画する
時計、カウントダウン、またはセッション外の値を最新に保つには、スケジュールで再描画します。モジュールのsession.start フックでタイマーを開始します。モジュールが既に 1 つを持っている場合、hello-tabs のように、$.clock.every行をそれに追加します。
ui.render フックを実行します。タイマーはモジュールがリロードされるときに停止し、新しいコピーは独自のものを開始します。
サイトが再描画できる頻度
Claude Code は再描画の頻度を制限するため、モッドはデータが変更されるたびに$.ui.invalidate を呼び出すことができます。表示されているペインとバンドは他のサイトより高い制限を持ち、制限テーブルに数字があります。
制限より速く来る呼び出しは 1 つの再描画に結合されます。その再描画はフックを 1 回実行し、フックはその時点でのデータを読むため、最新の値が表示され、その間の値は表示されません。アニメーションは制限より速く実行できません。
状態を保持する
モジュールには値を保持する 3 つの場所があり、値がどのくらい続くかが異なります。モジュールがリロードされるまで、セッションが終了するまで、または次のセッションまで続きます。値がどのくらい続く必要があるかで選択してください。$.store.get(key) は値または undefined に解決され、$.store.set(key, value) は任意の JSON 値を受け取ります。
$.state に値を保持する
$.state はセッションの長さの間値を保持し、自動的に再描画します。これはリアクティブな状態です。値を読む ui.render フックはそれにサブスクライブするため、値を書くたびに Claude Code はそのサイトを再描画し、$.ui.invalidate を呼び出す必要はありません。$.state の値は、変数とは異なり、モジュールのリロードも生き残ります。
これを設定するには、値を宣言し、マニフェストを宣言に指定し、各値を定義して使用します。例は hello-tabs の count を $.state に移動します。
値を宣言する
型ファイルで値を宣言します。外側のキーはプラグインの名前で、その下の各エントリは値とその型です。これをhello-tabs/types/index.d.ts として保存します。
hello-tabs/types/index.d.ts
マニフェストを宣言に指定する
claude plugin validate がコードをそのファイルに対して検証できるようにするには、マニフェストに types フィールドをそのパスで追加します。
hello-tabs/.claude-plugin/plugin.json
値を定義、読み取り、書き込みする
モジュールで、各値をデフォルトで定義し、描画中に読み取り、コールバックから書き込みます。atom は値とそのデフォルトに名前を付け、read はそれを返し、update はそれを書き込みます。3 つのヘルパーは $.state.get と $.state.set をあなたのために呼び出します。
ui.render フックが count を読み取ったため、ボタンがそれを書き込むたびに Claude Code はフックを再度実行します。
コードに 3 つのルールが適用されます。
pluginとkeyをリテラル文字列として書き込みます:claude plugin validateはソースからそれらを読み取ります- 型ファイルですべての値を宣言します:そうしないと、検証は
hello-tabs.count is not declaredで失敗します - コールバックまたは別のイベントのフックから書き込みます:
ui.renderフックは状態を読み取ることができ、それを書き込むことはできないため、onPress、onSubmit、または別のイベントのフックから書き込みます
hello-tabs を $.state を使用するように変更する
hello-tabs の count を $.state に移動するには、それを使用するすべての行を変更します。
- モジュールの最上部:
import行を追加し、let count = 0をatom行に置き換えます ui.renderフック内:tabButtonの前にread行を追加し、Textで'Count: ' + nを描画します- Add one ボタン内:
onPressを Save from more than one session のものに置き換えます。これはカウントを保存し、それを書き込みます session.startフック内:savedを読み取る 2 行を Load a saved value again after/clearのloadCount呼び出しに置き換えます
tab はまだ変数であるため、タブボタンの redraw を保持します。
/clear の後に保存された値を再度読み込む
モッドが session.start で $.store から保存された値を $.state にコピーする場合、/clear、/resume、または /branch の後に再度コピーする必要があります。これらのコマンドはすべての $.state 値をデフォルトに戻し、session.start は再度発火しません。classic.SessionStart は各値の後に発火し、e.source は clear、resume、または fork に設定されるため、値を再度コピーします。そうしないと、描画はデフォルトを表示し、$.state 値を保存するコールバックは保存したものをデフォルトで上書きします。
このコードは両方のフックから count を読み込みます。これは count がアトムで update がインポートされている hello-tabs の $.state バージョンに基づいています。loadCount を register の上に配置し、loadCount 呼び出しを既に持っている session.start フックに追加します。classic.SessionStart はスタートアップと圧縮後にも発火します。圧縮は $.state をリセットしないため、source のフィルターはフックを 3 つのリセットに保ちます。
/clear の後に保存されたカウントを表示し、0 ではなく、Add one の次のプレスは保存されたカウントに追加されます。
loadCount は保存された値を $.state のものに上書きし、session.start はモジュールがリロードされるたびに再度発火します。ストアが遅れないようにするには、Add one ボタンが行うように、すべての変更で保存します。
セッションなしでリロードを確認するには、/clear の後の描画をテストします。
複数のセッションから保存する
マシン上のモッドを実行するすべてのセッションは 1 つの$.store を共有します。get の後に set が続くことはアトミックではありません。2 つのセッションが各値を読み取り、変更し、書き戻すと、競合が発生し、2 番目の書き込みが最初の書き込みを置き換えます。
2 つの選択肢がそれをより可能性が低くします。
- 各アイテムに独自のキーを付与します:
setは独自のキーのみを変更するため、異なるキーを書き込むセッションは互いに上書きしません - 書き込む直前に再度読み取ります:複数のセッションが変更する値の場合、コールバックでキーを
getし、session.startで読み込んだコピーからではなく、その値から新しい値を構築します。別のセッションの書き込みは、getとsetの間に着地した場合でも失われます。
次のステップ
- イベントに反応する。ツール呼び出しとターンから描画をフィード
- モッド API を使用する。タイマーとモデル呼び出しから描画をフィード
- 描画をテストする。複数のサーフェスでボタンをテストから押す
- レンダーサイトと要素。各サイトのプロップと各要素のプロップ