Skip to main content
mod 可以在 Claude Code 中繪製自己的介面,並更改 Claude Code 已經繪製的介面部分。mod 可以繪製的每個位置稱為渲染位置,例如窗格、提示上方的帶狀區域或微調器。Claude Code 在即將繪製渲染位置時會引發 ui.render 事件,而您對該事件的鉤子會返回要在那裡繪製的內容。 此地圖顯示 mod 可以在終端工作階段中的繪製位置: Claude Code 終端工作階段的地圖。mod 可以在右側新增窗格作為側邊欄、在文字記錄右上角新增快顯通知、在文字記錄中新增日誌行、在提示上方新增帶狀區域,以及在提示下方新增狀態行。mod 可以重新繪製訊息、工具呼叫列和微調器。提示是 Claude Code 自己的。 Claude Code 終端工作階段的地圖。mod 可以在右側新增窗格作為側邊欄、在文字記錄右上角新增快顯通知、在文字記錄中新增日誌行、在提示上方新增帶狀區域,以及在提示下方新增狀態行。mod 可以重新繪製訊息、工具呼叫列和微調器。提示是 Claude Code 自己的。 在較窄的終端中,窗格位於提示上方而不是文字記錄旁邊。 在開始之前,請先建立您的第一個 mod。從已完成的範例開始,該範例建立一個具有兩個標籤和計數器的窗格,然後閱讀您想要更改的每個部分的部分。
若要查詢一個屬性或限制,請參閱參考。

建立具有標籤的窗格

在本部分中,您將建立一個 mod,該 mod 新增 /hello-tabs 命令,該命令會開啟一個窗格。窗格是在寬全螢幕終端中文字記錄旁邊的側邊欄,或在其他情況下是提示上方的框架區域。此窗格顯示兩個標籤,第二個標籤有一個按鈕,可將計數器加一。重新啟動 Claude Code 後,計數仍然存在。 完成的 mod 看起來像這樣。錄製會開啟窗格、切換到第二個標籤、按幾次按鈕,然後返回第一個標籤:
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 填充它們之前是空的,窗格和帶狀區域。選擇一個標籤以查看每個位置是什麼以及如何在其中繪製:
窗格是在寬全螢幕終端中文字記錄旁邊的側邊欄,或在其他情況下是提示上方的框架區域。開啟多個窗格時,每個窗格都會獲得一個顯示其標題的標籤。當您的 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 中所示。
若要保持 Claude Code 的繪製並更改其一部分,請將 next 傳遞事件的副本,其中 props 已更改。此鉤子更改微調器單詞後的文字:
微調器保持其動畫和單詞,您的文字跟隨單詞:
權限提示不是渲染位置,因此 mod 無法更改其顯示的內容。問題對話框 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 繪製一個字串,具有可選的樣式,例如 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
  • 使用者點擊它
Claude Code 只在提示為空且沒有其他內容具有鍵盤焦點時授予 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 行新增到它:
Claude Code 現在每秒執行您的 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 之間,仍然會丟失。
此按鈕現在新增一個到存放區保持的任何內容,然後更新繪製:
如果第二個工作階段自此工作階段啟動以來按下了其自己的按鈕三次,此按下會顯示並儲存包含這三個的計數。

後續步驟