ui.render 事件,而您對該事件的鉤子會返回要在那裡繪製的內容。
此地圖顯示 mod 可以在終端工作階段中的繪製位置:
若要查詢一個屬性或限制,請參閱參考。
建立具有標籤的窗格
在本部分中,您將建立一個 mod,該 mod 新增/hello-tabs 命令,該命令會開啟一個窗格。窗格是在寬全螢幕終端中文字記錄旁邊的側邊欄,或在其他情況下是提示上方的框架區域。此窗格顯示兩個標籤,第二個標籤有一個按鈕,可將計數器加一。重新啟動 Claude Code 後,計數仍然存在。
完成的 mod 看起來像這樣。錄製會開啟窗格、切換到第二個標籤、按幾次按鈕,然後返回第一個標籤:
1
建立外掛程式
mod 是一個具有清單、指向您的程式碼的 在
hooks.json 和程式碼檔案的外掛程式。建立 mod 說明了每一個。建立一個名為 hello-tabs 的目錄,其中包含 .claude-plugin 和 hooks 目錄,然後儲存前兩個檔案。將清單儲存為 hello-tabs/.claude-plugin/plugin.json:hello-tabs/.claude-plugin/plugin.json
hello-tabs/hooks/hooks.json 中命名您的進入點:hello-tabs/hooks/hooks.json
2
編寫程式碼
程式碼執行三項工作,每個鉤子一項:每個鉤子也執行程式碼沒有明確說明的事情:
- 新增
/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
開啟窗格
在您的 shell 中,使用
claude --plugin-dir ./hello-tabs 啟動 Claude Code。在 Claude Code 提示處,執行 /hello-tabs。一個窗格會開啟,頂部有 1: One 和 2: Two。按 2,然後按 a(Add one 的快捷鍵)幾次。計數上升。4
檢查計數是否已儲存
按 Esc 關閉窗格,然後退出工作階段。在您的 shell 中,使用相同的
claude --plugin-dir ./hello-tabs 命令再次啟動 Claude Code,並在 Claude Code 提示處執行 /hello-tabs。計數在您留下的地方。若要清除計數,請讓 mod 呼叫 $.store.delete('count')。保持狀態涵蓋每種值持續多長時間。選擇繪製位置
ui.render 鉤子為每個渲染位置執行,除非您將其縮小到您想要繪製的位置。若要選擇渲染位置,請傳遞一個稱為匹配器的篩選器作為 on 的第二個引數。{ component: 'Pane' } 只為窗格執行鉤子。在鉤子中,e.component 命名位置,e.surface 說明哪個應用程式在繪製,e.props 保持位置自己的資料。對於窗格,e.requestId 是您用來開啟它的 id。
兩個位置在 mod 填充它們之前是空的,窗格和帶狀區域。選擇一個標籤以查看每個位置是什麼以及如何在其中繪製:
- Pane
- Band above the prompt
窗格是在寬全螢幕終端中文字記錄旁邊的側邊欄,或在其他情況下是提示上方的框架區域。開啟多個窗格時,每個窗格都會獲得一個顯示其標題的標籤。當您的 mod 使用您選擇的
id 呼叫 $.ui.open 時,窗格會出現,如 $.ui.open({ id: 'hello-tabs' })。在正確的時間開啟窗格涵蓋其他欄位以及窗格何時等待更寬的終端。若要在您的窗格中繪製,請篩選 { component: 'Pane' } 並檢查 e.requestId 是否為您的 id。更改 Claude Code 已經繪製的內容
Claude Code 自己繪製大部分介面:訊息、工具呼叫列、微調器等。這些部分中的每一個也是一個渲染位置,因此 mod 可以重新設定樣式或替換它。若要更改一個,請在ui.render 鉤子上篩選此表中的其名稱:
在 Claude Code 已經繪製的位置,您的鉤子有三個選擇:更改詳細資訊、替換繪製或不理會。選擇一個標籤以查看每個應用於微調器的選項。範例讀取另一個鉤子計數的
calls 變數,如教學 mod 中所示。
- Change a detail
- Replace the drawing
- Leave it alone
若要保持 Claude Code 的繪製並更改其一部分,請將 微調器保持其動畫和單詞,您的文字跟隨單詞:
next 傳遞事件的副本,其中 props 已更改。此鉤子更改微調器單詞後的文字:AskUserQuestion 是一個,因此 mod 可以更改它。
終端和桌面應用程式不會引發所有相同的位置。Pane、AbovePrompt、Spinner 和文字記錄位置在兩者中都有效。其他一些狀態行僅在終端中引發。渲染位置表列出每個位置的引發位置。
在正確的時間開啟窗格
窗格只在您的 mod 開啟它時出現。您如何以及何時開啟它決定了它是否獲得鍵盤焦點、它要求多少空間,以及它是否在狹窄的終端中顯示。 若要開啟窗格,請使用您選擇的id 呼叫 $.ui.open。id 是窗格的名稱:您的 ui.render 鉤子檢查它,您再次傳遞它以關閉窗格。
id 呼叫 $.ui.close:
id,$.ui.open 還採用這些可選欄位:
若要讓命令在 Claude 工作時開啟窗格,請在註冊命令時新增
immediate: true。沒有它,在輪次期間輸入的命令會等待輪次結束。
當窗格等待更寬的終端時
您的 mod 開啟的窗格(未被要求)不會在狹窄的終端中出現,因此它無法接管小螢幕。它是否出現取決於開啟它的內容:- 由使用者執行的操作開啟,例如他們執行的命令或他們按下的按鈕,窗格在任何寬度出現
- 由您的 mod 自行開啟,例如從計時器或
turn.start鉤子,窗格只在至少 144 列寬的終端中出現。使用者自己開啟該窗格一次後,110 列就足夠了。
$.ui.open 解析為 { isPlaced: true }。當窗格在等待時,isPlaced 是 false,reason 是說明原因的字串。當使用者開啟窗格或加寬終端時,等待的窗格會出現。若要說明某些內容可用而不開啟窗格,請呼叫 $.ui.toast('Your message'),它會顯示在幾秒後消失的小通知。
從元素建立樹
ui.render 鉤子返回的是元素樹:對要繪製的內容的描述,由相互嵌套的框、文字和控制項組成。您描述繪製,Claude Code 在終端或桌面應用程式中呈現它。
若要取得元素,請在您的鉤子中呼叫 $.ui.resolve(e),如 const { Box, Text, Button } = $.ui.resolve(e)。每個元素都是一個函式。您傳遞它屬性,並將應該在其中的元素和字串放在 children 中。
大多數繪製使用四個元素。選擇一個標籤以查看每個元素以及終端如何繪製它:
- Text
- Box
- Input
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 並提供相同的原因。工作階段中沒有其他內容出現,因此當繪製不顯示時,請檢查該行或日誌。
繪製彩色儲存格網格
對於熱力圖、迷你圖或終端中的遊戲板,繪製一個Raster 而不是每個儲存格的 Box。Raster 採用 key、其大小(以 columns 和 rows 為單位)以及 cells,它將每個儲存格打包到一個字串中。每個儲存格是三個數字:字元的程式碼點、其顏色和其背景顏色。顏色是十六進位數字,紅色、綠色和藍色各有兩位數字,例如 0xc62828 表示紅色,或 0x01000000 表示終端的預設值。
桌面應用程式沒有 Raster,因此請檢查 e.surface 並在那裡繪製文字。此窗格主體繪製一個三乘二的熱力圖:
rows 陣列是您要更改的部分,cellsOf 將其轉換為打包的字串。鉤子只在 id 為 heat 的窗格中繪製,因此從命令開啟一個,如 hello-tabs 範例開啟其窗格。
每個字元必須是一個儲存格寬。若要動畫已在螢幕上的 Raster,請使用窗格的 id 作為 requestId、Raster 的 key、相同的大小和新儲存格呼叫 $.ui.blit。對於此範例,這是 $.ui.blit({ requestId: 'heat', key: 'grid', columns: 3, rows: 2, cells: cellsOf(newRows) })。它重新繪製該一個元素,而不再次執行您的 ui.render 鉤子。
回應按下和輸入
當使用者按下按鈕、輸入欄位或從您的 mod 繪製的清單中選擇時,Claude Code 會呼叫您給該控制項的函式,並在您的模組中執行。每個控制項採用其自己的回呼:Button:採用onPress(e),其中e.surface是按下來自的應用程式Input:採用onSubmit(value)和onInput(value)Select:採用onSelect(value)及其options中的選擇,至少一個具有唯一值的選擇清單,例如[{ value: 'sm', label: 'Small' }, { value: 'lg', label: 'Large' }]
key 按下或輸入到控制項,因此給每個控制項一個。控制項的每次使用也會引發 ui.press、ui.input 或 ui.select,其中 key 在 e.element 中,另一個 mod 可以鉤住這些事件。其鉤子在您的回呼之前執行,因此它會看到使用者輸入到您的 Input 中的內容,並可以更改它或代替您的回呼回答。mod API 沒有按下另一個 mod 按鈕的方法。
鍵盤焦點和快捷鍵
您的 mod 永遠不會自己讀取鍵盤。使用者按下一個鍵,Claude Code 決定它是為您的哪個控制項,該控制項的回呼執行。除了帶狀區域上的數字快捷鍵外,這只在您的窗格或帶狀區域具有鍵盤焦點時發生。其餘時間,鍵進入提示。窗格如何獲得鍵盤焦點
窗格通過以下三種方式之一獲得鍵盤焦點:- 您的 mod 使用
focus: true從命令或按下開啟它 - 使用者按 Ctrl+X 然後 Tab
- 使用者點擊它
focus: true。在使用者輸入時開啟的窗格不會接收他們的按鍵。
每個鍵執行的操作
此表列出當您的窗格或帶狀區域具有鍵盤焦點時鍵執行的操作:
mod 無法將 Tab 或箭頭鍵綁定到其他任何內容,因此遊戲使用
w、a、s 和 d 進行轉向。
設定快捷鍵和第一個焦點
控制項上的兩個屬性決定鍵盤如何到達它:hotkey:若要讓使用者使用一個鍵按下Button,請給它一個hotkey的一位數字或一個小寫字母,如hotkey: 'a'autoFocus:若要選擇窗格開啟時哪個控制項具有焦點,請將autoFocus: true新增到它。在其他項上省略屬性,因為 Claude Code 拒絕autoFocus: false。
在終端中,在括號按鈕的標籤中命名鍵,或使用
plain: true,以便使用者可以看到要按什麼。元素參考有其他 Button 規則:action、帶狀區域上的數字快捷鍵,以及一個快捷鍵上的兩個按鈕。
取得輸入的文字並為每個項目繪製一列
許多窗格是一個文字欄位,下面有一個清單。本部分中的範例是一個筆記窗格:您輸入一個筆記並按 Enter 新增它,每個筆記都有一個x 按鈕來刪除它。新增兩個筆記後,終端會以這種方式繪製窗格:
- 取得輸入的文字:
Input在使用者按 Enter 時使用欄位的文字呼叫onSubmit(value),並在每次更改時呼叫onInput(value) - 繪製清單:將您的資料對應到每個一列,並給每列的按鈕其自己的
key
- 新增筆記:輸入一行並按 Enter。該行作為新列出現,欄位清空。
- 刪除筆記:按 Tab 直到筆記的
x按鈕具有焦點,然後按 Enter。x是按鈕的標籤,而不是快捷鍵,因此輸入字母不會按下它。
hello-tabs 相同的渲染週期:回呼更改 notes、呼叫 redraw 並將清單儲存到 $.store。
欄位在每次提交後清空,因為其 value 屬性。value 是繪製欄位時欄位保持的文字,使用者的輸入替換它,直到您的鉤子再次繪製欄位。範例始終使用 '' 繪製欄位。
範例儲存筆記但不載入它們。若要在下一個工作階段中將它們帶回,請在 session.start 鉤子中讀取它們,就像 hello-tabs 讀取 count 的方式一樣。
三個屬性組成欄位的行,Note: Type a note and press Enter ⏎ add:
提交
Input 不會啟動輪次,除非您的回呼呼叫 $.prompt.submit。
重新繪製位置
繪製是快照:它顯示您的ui.render 鉤子上次執行時返回的內容。若要顯示新內容,鉤子必須再次執行。Claude Code 為某些更改再次執行它,您的 mod 要求其餘的。
當 Claude Code 在未被要求時重新繪製
當位置的屬性更改或終端的寬度更改時,Claude Code 會再次執行您的ui.render 鉤子。它不會在計時器上執行鉤子,也無法判斷您的模組中的變數何時更改。
當您的資料更改時重新繪製
若要在您自己的資料更改後重新繪製您的位置,請呼叫$.ui.invalidate('ui.render')。此窗格計數按下。按鈕的回呼更改 count,然後要求重新繪製:
hello-tabs 範例將相同的呼叫包裝在其 redraw 函式中。
您在 $.state 中保持的值不需要呼叫,因為寫入值會重新繪製讀取它的位置。
在計時器上重新繪製
若要保持時鐘、倒計時或來自工作階段外部的值為最新,請按計劃重新繪製。在模組的session.start 鉤子中啟動計時器。如果模組已經有一個,如 hello-tabs 所做的,請將 $.clock.every 行新增到它:
ui.render 鉤子一次。計時器在模組重新載入時停止,新副本啟動自己的。
位置可以重新繪製的頻率
Claude Code 限制重新繪製的頻率,因此您的 mod 可以在其資料更改時經常呼叫$.ui.invalidate。可見窗格和帶狀區域的限制比其他位置更高,限制表有數字。
比限制更快到達的呼叫會合併為一次重新繪製。該重新繪製執行您的鉤子一次,鉤子在該時刻讀取您的資料,因此最新值顯示,介於兩者之間的值不顯示。動畫無法比限制更快執行。
保持狀態
mod 有三個地方可以保持值,它們在值持續多長時間方面有所不同:直到模組重新載入、直到工作階段結束或從一個工作階段到下一個工作階段。根據值必須持續多長時間選擇:$.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 寫入它。三個幫助程式為您呼叫 $.state.get 和 $.state.set:
ui.render 鉤子讀取 count,Claude Code 每次按鈕寫入它時都會再次執行鉤子。
三個規則適用於程式碼:
- 將
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替換為從多個工作階段儲存中的按鈕,該按鈕儲存計數以及寫入它 - 在
session.start鉤子中:將讀取saved的兩行替換為在/clear後再次載入儲存的值中的loadCount呼叫
redraw 用於標籤按鈕,因為 tab 仍然是變數。
在 /clear 後再次載入儲存的值
如果您的 mod 在 session.start 時將儲存的值從 $.store 複製到 $.state,則必須在 /clear、/resume 或 /branch 後再次複製它。這些命令將每個 $.state 值放回其預設值,session.start 不會再次引發。classic.SessionStart 在每個之後引發,e.source 設定為 clear、resume 或 fork,因此在鉤子上再次複製值。否則您的繪製顯示預設值,儲存 $.state 值的回呼會將預設值寫入您儲存的內容。
此程式碼從兩個鉤子載入 count。它建立在 hello-tabs 的 $.state 版本上,其中 count 是原子,update 被匯入。將 loadCount 放在 register 上方,並將 loadCount 呼叫新增到您已經擁有的 session.start 鉤子。classic.SessionStart 也在啟動和壓縮後引發,這不會重設 $.state,因此 source 上的篩選將鉤子保持在三個重設:
/clear 後顯示儲存的計數,而不是 0,下一次按 Add one 會新增到儲存的計數。
loadCount 將儲存的值寫入 $.state 中的值,session.start 每次模組重新載入時都會再次引發。若要保持存放區不落後,請在每次更改時儲存,如 Add one 按鈕所做的。
若要在不工作階段的情況下檢查重新載入,請在 /clear 後測試繪製。
從多個工作階段儲存
您機器上執行您的 mod 的每個工作階段都共享一個$.store。get 後跟 set 不是原子的。當兩個工作階段各自讀取值、更改它並寫回時,它們會競爭,第二次寫入會替換第一次。
兩個選擇使這種情況不太可能:
- 給每個項目其自己的鍵:
set只更改其自己的鍵,因此寫入不同鍵的工作階段不會相互覆蓋 - 在寫入之前再次讀取:對於多個工作階段更改的值,在回呼中
get鍵並從該值建立新值,而不是從您在session.start時載入的副本。如果另一個工作階段的寫入落在您的get和set之間,仍然會丟失。
後續步驟
- 回應事件:從工具呼叫和輪次提供您的繪製
- 使用 mod API:從計時器和模型呼叫提供您的繪製
- 測試繪製:從測試按下您的按鈕,在多個表面上
- 渲染位置和元素:每個位置的屬性和每個元素的屬性