1> ## Documentation Index
2> Fetch the complete documentation index at: https://code.claude.com/docs/llms.txt
3> Use this file to discover all available pages before exploring further.
4
5# 使用 mod 在介面中繪製
6
7> 從 Claude Code mod 繪製窗格、提示上方的帶狀區域、按鈕和文字欄位,處理按下和輸入,並在重新繪製和工作階段之間保持狀態。
8
9mod 可以在 Claude Code 中繪製自己的介面,並更改 Claude Code 已經繪製的介面部分。mod 可以繪製的每個位置稱為[渲染位置](/docs/zh-TW/plugins/mods/reference#render-sites),例如窗格、提示上方的帶狀區域或微調器。Claude Code 在即將繪製渲染位置時會引發 [`ui.render`](/docs/zh-TW/plugins/mods/reference#interface) 事件,而您對該事件的鉤子會返回要在那裡繪製的內容。
10
11此地圖顯示 mod 可以在終端工作階段中的繪製位置:
12
13<img src="https://mintcdn.com/claude-code/dgiVO_Od1X1faduV/images/mods-screen-map.svg?fit=max&auto=format&n=dgiVO_Od1X1faduV&q=85&s=5fda26b6609c62b68c6f9e528c1590ea" className="dark:hidden" alt="Claude Code 終端工作階段的地圖。mod 可以在右側新增窗格作為側邊欄、在文字記錄右上角新增快顯通知、在文字記錄中新增日誌行、在提示上方新增帶狀區域,以及在提示下方新增狀態行。mod 可以重新繪製訊息、工具呼叫列和微調器。提示是 Claude Code 自己的。" width="600" height="336" data-path="images/mods-screen-map.svg" />
14
15<img src="https://mintcdn.com/claude-code/dgiVO_Od1X1faduV/images/mods-screen-map-dark.svg?fit=max&auto=format&n=dgiVO_Od1X1faduV&q=85&s=5b4161581a1bd2c0450b0c8b57bc1225" className="hidden dark:block" alt="Claude Code 終端工作階段的地圖。mod 可以在右側新增窗格作為側邊欄、在文字記錄右上角新增快顯通知、在文字記錄中新增日誌行、在提示上方新增帶狀區域,以及在提示下方新增狀態行。mod 可以重新繪製訊息、工具呼叫列和微調器。提示是 Claude Code 自己的。" width="600" height="336" data-path="images/mods-screen-map-dark.svg" />
16
17在較窄的終端中,窗格位於提示上方而不是文字記錄旁邊。
18
19在開始之前,請先建立您的[第一個 mod](/docs/zh-TW/plugins/mods/create)。從已完成的範例開始,該範例建立一個具有兩個標籤和計數器的窗格,然後閱讀您想要更改的每個部分的部分。
20
21<Note>
22 若要查詢一個屬性或限制,請參閱[參考](/docs/zh-TW/plugins/mods/reference#render-sites)。
23</Note>
24
25<h2 id="build-a-pane-with-tabs">
26 建立具有標籤的窗格
27</h2>
28
29在本部分中,您將建立一個 mod,該 mod 新增 `/hello-tabs` 命令,該命令會開啟一個窗格。窗格是在寬全螢幕終端中文字記錄旁邊的側邊欄,或在其他情況下是提示上方的框架區域。此窗格顯示兩個標籤,第二個標籤有一個按鈕,可將計數器加一。重新啟動 Claude Code 後,計數仍然存在。
30
31完成的 mod 看起來像這樣。錄製會開啟窗格、切換到第二個標籤、按幾次按鈕,然後返回第一個標籤:
32
33<Frame>
34 <video autoPlay muted loop playsInline controls className="w-full dark:hidden" src="https://mintcdn.com/claude-code/dgiVO_Od1X1faduV/images/mods-hello-tabs-light.mp4?fit=max&auto=format&n=dgiVO_Od1X1faduV&q=85&s=49d520094d87b5b44bfe50fa49677f06" aria-label="在 Claude Code 提示處輸入 /hello-tabs 命令,一個框架窗格在其上方開啟,頂部有「1: One」和「2: Two」,以及文字「This is the first tab.」。第二個標籤顯示「Add one」按鈕,旁邊是「Count: 1」,計數上升到 3。窗格然後返回第一個標籤。" data-path="images/mods-hello-tabs-light.mp4" />
35
36 <video autoPlay muted loop playsInline controls className="w-full hidden dark:block" src="https://mintcdn.com/claude-code/dgiVO_Od1X1faduV/images/mods-hello-tabs-dark.mp4?fit=max&auto=format&n=dgiVO_Od1X1faduV&q=85&s=ff7a14d713d6e5d3b0000efa8522ea4b" aria-label="在 Claude Code 提示處輸入 /hello-tabs 命令,一個框架窗格在其上方開啟,頂部有「1: One」和「2: Two」,以及文字「This is the first tab.」。第二個標籤顯示「Add one」按鈕,旁邊是「Count: 1」,計數上升到 3。窗格然後返回第一個標籤。" data-path="images/mods-hello-tabs-dark.mp4" />
37</Frame>
38
39Claude Code 沒有內建的標籤元素,因此標籤是一列中的兩個按鈕。mod 會追蹤哪一個是活動的,並在列下方繪製該標籤的內容。
40
41<Steps>
42 <Step title="建立外掛程式">
43 mod 是一個具有清單、指向您的程式碼的 `hooks.json` 和程式碼檔案的外掛程式。[建立 mod](/docs/zh-TW/plugins/mods/create#write-a-mod-yourself) 說明了每一個。建立一個名為 `hello-tabs` 的目錄,其中包含 `.claude-plugin` 和 `hooks` 目錄,然後儲存前兩個檔案。
44
45 將清單儲存為 `hello-tabs/.claude-plugin/plugin.json`:
46
47 ```json hello-tabs/.claude-plugin/plugin.json theme={null}
48 {
49 "name": "hello-tabs",
50 "version": "0.1.0",
51 "description": "Opens a pane with two tabs and a counter",
52 "author": { "name": "Your Name" }
53 }
54 ```
55
56 在 `hello-tabs/hooks/hooks.json` 中命名您的進入點:
57
58 ```json hello-tabs/hooks/hooks.json theme={null}
59 {
60 "modules": ["./register.js"]
61 }
62 ```
63 </Step>
64
65 <Step title="編寫程式碼">
66 程式碼執行三項工作,每個鉤子一項:
67
68 * 新增 `/hello-tabs` 命令
69 * 執行該命令時開啟窗格
70 * 繪製窗格的內容:標籤列和開啟的標籤的主體
71
72 兩個模組級變數 `tab` 和 `count` 保持窗格的狀態。
73
74 將此儲存為 `hello-tabs/hooks/register.js`:
75
76 ```javascript hello-tabs/hooks/register.js theme={null}
77 // 窗格的 id,用於開啟窗格並在繪製時識別它
78 const PANE = 'hello-tabs'
79
80 // 窗格顯示的內容:哪個標籤是開啟的,以及計數器的值
81 let tab = 'one'
82 let count = 0
83
84 export function register(on) {
85 // 在您的第一個提示之前執行,並在重新載入後再次執行
86 on('session.start', async ($, e, next) => {
87 await $.command.register({ name: 'hello-tabs', description: 'Open the hello-tabs pane' })
88 // 載入較早的工作階段儲存的計數(如果有的話)
89 const saved = await $.store.get('count')
90 if (typeof saved === 'number') count = saved
91 return next(e)
92 })
93
94 // 當您輸入 /hello-tabs 時執行
95 on('command.run', { command: 'hello-tabs' }, async ($) => {
96 // 開啟窗格,給它鍵盤焦點,並讓 Esc 關閉它
97 await $.ui.open({ id: PANE, title: 'Hello tabs', focus: true, closeOnEscape: true })
98 // 在文字記錄中不列印任何內容
99 return {}
100 })
101
102 // 每次 Claude Code 繪製窗格時執行
103 on('ui.render', { component: 'Pane' }, async ($, e, next) => {
104 // 不理會其他 mod 的窗格
105 if (e.requestId !== PANE) return next(e)
106 // 取得此應用程式可以繪製的元素
107 const { Box, Text, Button } = $.ui.resolve(e)
108 // 要求 Claude Code 再次執行此鉤子
109 const redraw = () => $.ui.invalidate('ui.render')
110
111 // 一個標籤:一個按鈕,按下時切換到其標籤
112 const tabButton = (name, label, hotkey) =>
113 Button({
114 key: 'tab-' + name,
115 label,
116 hotkey,
117 plain: true,
118 // 調暗不是開啟的標籤
119 dimColor: tab !== name,
120 onPress: () => {
121 tab = name
122 redraw()
123 },
124 })
125
126 // 根據開啟的標籤,在標籤下方顯示的內容
127 const body =
128 tab === 'one'
129 ? [Text({ children: ['This is the first tab.'] })]
130 : [
131 Box({
132 flexDirection: 'row',
133 columnGap: 2,
134 children: [
135 Button({
136 key: 'more',
137 label: 'Add one',
138 hotkey: 'a',
139 onPress: async () => {
140 count += 1
141 redraw()
142 // 儲存計數,以便在重新啟動後仍然存在
143 await $.store.set('count', count)
144 },
145 }),
146 Text({ children: ['Count: ' + count] }),
147 ],
148 }),
149 ]
150
151 // 整個窗格:標籤列、空白行,然後是主體
152 return Box({
153 flexDirection: 'column',
154 children: [
155 Box({
156 flexDirection: 'row',
157 columnGap: 3,
158 children: [tabButton('one', 'One', '1'), tabButton('two', 'Two', '2')],
159 }),
160 Text({ children: [' '] }),
161 ...body,
162 ],
163 })
164 })
165 }
166 ```
167
168 每個鉤子也執行程式碼沒有明確說明的事情:
169
170 * **[`session.start`](/docs/zh-TW/plugins/mods/reference#session)** 也從 [`$.store`](#keep-state) 讀取儲存的計數,這是一個在工作階段之間持續的鍵值存放區。
171 * **[`command.run`](/docs/zh-TW/plugins/mods/api#add-a-command)** 只告訴 Claude Code 窗格存在。開啟窗格本身不會繪製任何內容:Claude Code 然後引發 `ui.render` 以詢問其中應該放什麼。
172 * **`ui.render`** 返回元素樹,一個包含其他框、文字和按鈕的 `Box`,並每次從 `tab` 和 `count` 重新建立它。
173
174 按下按鈕會執行其 `onPress` 回呼,該回呼會更改變數並呼叫 `redraw`。Claude Code 然後再次執行 `ui.render` 鉤子,該鉤子從新值建立新樹。每個互動式繪製都使用該渲染週期:回呼更改狀態,鉤子從新狀態重新渲染。
175 </Step>
176
177 <Step title="開啟窗格">
178 在您的 shell 中,使用 `claude --plugin-dir ./hello-tabs` 啟動 Claude Code。在 Claude Code 提示處,執行 `/hello-tabs`。一個窗格會開啟,頂部有 `1: One` 和 `2: Two`。按 `2`,然後按 `a`(**Add one** 的快捷鍵)幾次。計數上升。
179 </Step>
180
181 <Step title="檢查計數是否已儲存">
182 按 Esc 關閉窗格,然後退出工作階段。在您的 shell 中,使用相同的 `claude --plugin-dir ./hello-tabs` 命令再次啟動 Claude Code,並在 Claude Code 提示處執行 `/hello-tabs`。計數在您留下的地方。
183
184 若要清除計數,請讓 mod 呼叫 `$.store.delete('count')`。[保持狀態](#keep-state)涵蓋每種值持續多長時間。
185 </Step>
186</Steps>
187
188<h2 id="pick-where-to-draw">
189 選擇繪製位置
190</h2>
191
192`ui.render` 鉤子為每個渲染位置執行,除非您將其縮小到您想要繪製的位置。若要選擇渲染位置,請傳遞一個稱為[匹配器](/docs/zh-TW/plugins/mods/events#filter-which-events-a-hook-handles)的篩選器作為 `on` 的第二個引數。`{ component: 'Pane' }` 只為窗格執行鉤子。在鉤子中,`e.component` 命名位置,`e.surface` 說明哪個應用程式在繪製,`e.props` 保持位置自己的資料。對於窗格,`e.requestId` 是您用來開啟它的 `id`。
193
194兩個位置在 mod 填充它們之前是空的,窗格和帶狀區域。選擇一個標籤以查看每個位置是什麼以及如何在其中繪製:
195
196<Tabs>
197 <Tab title="Pane">
198 窗格是在寬全螢幕終端中文字記錄旁邊的側邊欄,或在其他情況下是提示上方的框架區域。開啟多個窗格時,每個窗格都會獲得一個顯示其標題的標籤。
199
200 當您的 mod 使用您選擇的 `id` 呼叫 `$.ui.open` 時,窗格會出現,如 `$.ui.open({ id: 'hello-tabs' })`。[在正確的時間開啟窗格](#open-a-pane-at-the-right-time)涵蓋其他欄位以及窗格何時等待更寬的終端。
201
202 若要在您的窗格中繪製,請篩選 `{ component: 'Pane' }` 並檢查 `e.requestId` 是否為您的 `id`。
203 </Tab>
204
205 <Tab title="Band above the prompt">
206 帶狀區域是直接在提示輸入上方的條帶。它始終存在,每個 mod 都共享它。
207
208 您的鉤子返回一棵樹以在帶狀區域中顯示某些內容,或返回 `next(e)` 以不顯示任何內容。樹會替換 mod [在您之後](/docs/zh-TW/plugins/mods/events#the-order-mods-run-in)在那裡繪製的內容。若要保持他們的內容,請將 `await next(e)` 的結果放在您樹中 [`Box`](#build-a-tree-from-elements) 的子項中。
209
210 若要在帶狀區域中繪製,請篩選 `{ component: 'AbovePrompt' }`。
211 </Tab>
212</Tabs>
213
214<h3 id="change-what-claude-code-already-draws">
215 更改 Claude Code 已經繪製的內容
216</h3>
217
218Claude Code 自己繪製大部分介面:訊息、工具呼叫列、微調器等。這些部分中的每一個也是一個渲染位置,因此 mod 可以重新設定樣式或替換它。若要更改一個,請在 `ui.render` 鉤子上篩選此表中的其名稱:
219
220| 位置 | 它是什麼 |
221| :- | :- |
222| `UserMessage`, `AssistantMessage` | 文字記錄中的訊息 |
223| `ToolUse`, `ToolResult`, `ToolGroup` | 工具呼叫的列、其結果和折疊的呼叫執行 |
224| `CommandOutput` | 命令列印的列 |
225| `AskUserQuestion` | Claude 開啟以詢問您問題的對話框 |
226| `Spinner`, `ToolProgress`, `TurnDuration` | 輪次的狀態行:在 Claude 工作時動畫的行、執行中工具的即時進度行,以及關閉輪次的行 |
227| `InfoNotice`, `SessionMode`, `PromptHint` | 標誌下的狀態行、頁尾中的模式標籤,以及提示下的提示行 |
228
229在 Claude Code 已經繪製的位置,您的鉤子有三個選擇:更改詳細資訊、替換繪製或不理會。選擇一個標籤以查看每個應用於微調器的選項。範例讀取另一個鉤子計數的 `calls` 變數,如[教學 mod](/docs/zh-TW/plugins/mods/create#write-a-mod-yourself) 中所示。
230
231<Tabs>
232 <Tab title="Change a detail">
233 若要保持 Claude Code 的繪製並更改其一部分,請將 `next` 傳遞事件的副本,其中 `props` 已更改。此鉤子更改微調器單詞後的文字:
234
235 ```javascript theme={null}
236 on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
237 // 保持 Claude Code 的微調器,並更改其單詞後的文字
238 return next({ ...e, props: { ...e.props, suffix: ' · tool calls: ' + calls + '…' } })
239 })
240 ```
241
242 微調器保持其動畫和單詞,您的文字跟隨單詞:
243
244 ```text theme={null}
245 Thinking · tool calls: 2…
246 ```
247 </Tab>
248
249 <Tab title="Replace the drawing">
250 若要在位置的位置繪製您自己的內容,請返回樹並不呼叫 `next`。此鉤子在微調器所在的位置繪製一行文字:
251
252 ```javascript theme={null}
253 on('ui.render', { component: 'Spinner' }, async ($, e) => {
254 const { Text } = $.ui.resolve(e)
255 // 沒有呼叫 next,所以此行在微調器的位置繪製
256 return Text({ children: ['Claude has made ' + calls + ' tool calls'] })
257 })
258 ```
259
260 當 Claude 工作時,您的行會顯示,Claude Code 的微調器不會:
261
262 ```text theme={null}
263 Claude has made 2 tool calls
264 ```
265 </Tab>
266
267 <Tab title="Leave it alone">
268 若要將位置保留為 Claude Code 繪製的方式,請返回 `next(e)`。鉤子通常對某些事件執行此操作,對其他事件則不執行。此鉤子在沒有要計數的呼叫之前保留微調器:
269
270 ```javascript theme={null}
271 on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
272 // 還沒有要顯示的內容,所以不變地傳遞事件
273 if (calls === 0) return next(e)
274 return next({ ...e, props: { ...e.props, suffix: ' · tool calls: ' + calls + '…' } })
275 })
276 ```
277
278 在第一個工具呼叫之前,微調器看起來就像沒有 mod 的方式:
279
280 ```text theme={null}
281 Thinking…
282 ```
283 </Tab>
284</Tabs>
285
286權限提示不是渲染位置,因此 mod 無法更改其顯示的內容。問題對話框 `AskUserQuestion` 是一個,因此 mod 可以更改它。
287
288終端和桌面應用程式不會引發所有相同的位置。`Pane`、`AbovePrompt`、`Spinner` 和文字記錄位置在兩者中都有效。其他一些狀態行僅在終端中引發。[渲染位置表](/docs/zh-TW/plugins/mods/reference#render-sites)列出每個位置的引發位置。
289
290<h3 id="open-a-pane-at-the-right-time">
291 在正確的時間開啟窗格
292</h3>
293
294窗格只在您的 mod 開啟它時出現。您如何以及何時開啟它決定了它是否獲得鍵盤焦點、它要求多少空間,以及它是否在狹窄的終端中顯示。
295
296若要開啟窗格,請使用您選擇的 `id` 呼叫 [`$.ui.open`](/docs/zh-TW/plugins/mods/reference#mods-api-methods)。`id` 是窗格的名稱:您的 `ui.render` 鉤子檢查它,您再次傳遞它以關閉窗格。
297
298```javascript theme={null}
299await $.ui.open({ id: 'hello-tabs', title: 'Hello tabs', focus: true })
300```
301
302若要關閉窗格,請使用您用來開啟它的 `id` 呼叫 `$.ui.close`:
303
304```javascript theme={null}
305await $.ui.close({ id: 'hello-tabs' })
306```
307
308除了 `id`,`$.ui.open` 還採用這些可選欄位:
309
310| 欄位 | 它執行的操作 |
311| :- | :- |
312| `title` | 開啟多個窗格時窗格的標籤標籤 |
313| `focus` | 要求[鍵盤焦點](#know-which-keys-your-mod-can-receive) |
314| `closeOnEscape` | 使 Esc 關閉窗格。傳遞 `true` 或省略欄位,因為 Claude Code 拒絕 `false`。 |
315| `holdToasts` | 保持快顯通知,來自 [`$.ui.toast`](/docs/zh-TW/plugins/mods/api#show-something-without-starting-a-turn) 的小通知,直到窗格關閉 |
316| `rows` | 當窗格位於提示上方時要求的高度。預設值是空間的三分之一。 |
317| `columns` | 當窗格位於文字記錄旁邊時要求的寬度 |
318
319若要讓命令在 Claude 工作時開啟窗格,請在[註冊命令](/docs/zh-TW/plugins/mods/api#add-a-command)時新增 `immediate: true`。沒有它,在輪次期間輸入的命令會等待輪次結束。
320
321<h4 id="when-a-pane-waits-for-a-wider-terminal">
322 當窗格等待更寬的終端時
323</h4>
324
325您的 mod 開啟的窗格(未被要求)不會在狹窄的終端中出現,因此它無法接管小螢幕。它是否出現取決於開啟它的內容:
326
327* **由使用者執行的操作開啟**,例如他們執行的命令或他們按下的按鈕,窗格在任何寬度出現
328* **由您的 mod 自行開啟**,例如從計時器或 [`turn.start`](/docs/zh-TW/plugins/mods/events#follow-a-turn) 鉤子,窗格只在至少 144 列寬的終端中出現。使用者自己開啟該窗格一次後,110 列就足夠了。
329
330當窗格出現時,`$.ui.open` 解析為 `{ isPlaced: true }`。當窗格在等待時,`isPlaced` 是 `false`,`reason` 是說明原因的字串。當使用者開啟窗格或加寬終端時,等待的窗格會出現。若要說明某些內容可用而不開啟窗格,請呼叫 `$.ui.toast('Your message')`,它會顯示在幾秒後消失的小通知。
331
332<h2 id="build-a-tree-from-elements">
333 從元素建立樹
334</h2>
335
336`ui.render` 鉤子返回的是元素樹:對要繪製的內容的描述,由相互嵌套的框、文字和控制項組成。您描述繪製,Claude Code 在終端或桌面應用程式中呈現它。
337
338若要取得元素,請在您的鉤子中呼叫 `$.ui.resolve(e)`,如 `const { Box, Text, Button } = $.ui.resolve(e)`。每個元素都是一個函式。您傳遞它屬性,並將應該在其中的元素和字串放在 `children` 中。
339
340大多數繪製使用四個元素。選擇一個標籤以查看每個元素以及終端如何繪製它:
341
342<Tabs>
343 <Tab title="Text">
344 `Text` 繪製一個字串,具有可選的樣式,例如 `bold` 和 `color`:
345
346 ```javascript theme={null}
347 Text({ children: ['This is the first tab.'] })
348 ```
349
350 ```text theme={null}
351 This is the first tab.
352 ```
353 </Tab>
354
355 <Tab title="Box">
356 `Box` 排列其中的內容,在列或行中。此項將按鈕和文字行並排放置,相隔兩列:
357
358 ```javascript theme={null}
359 Box({
360 flexDirection: 'row',
361 columnGap: 2,
362 children: [
363 Button({ key: 'more', label: 'Add one', onPress: addOne }),
364 Text({ children: ['Count: 0'] }),
365 ],
366 })
367 ```
368
369 ```text theme={null}
370 [ Add one ] Count: 0
371 ```
372 </Tab>
373
374 <Tab title="Button">
375 `Button` 是使用者可以按下的控制項。它執行您的 `onPress` 回呼。使用 `plain: true` 時,它沒有括號並顯示其快捷鍵:
376
377 ```javascript theme={null}
378 Button({ key: 'more', label: 'Add one', onPress: addOne })
379 Button({ key: 'tab-one', label: 'One', hotkey: '1', plain: true, onPress: showTabOne })
380 ```
381
382 ```text theme={null}
383 [ Add one ]
384 1: One
385 ```
386 </Tab>
387
388 <Tab title="Input">
389 `Input` 是一個文字欄位。當使用者按 Enter 時,它使用文字執行您的 `onSubmit` 回呼:
390
391 ```javascript theme={null}
392 Input({
393 key: 'new-note',
394 label: 'Note',
395 placeholder: 'Type a note and press Enter',
396 value: '',
397 submitLabel: 'add',
398 onSubmit: addNote,
399 })
400 ```
401
402 ```text theme={null}
403 Note: Type a note and press Enter ⏎ add
404 ```
405 </Tab>
406</Tabs>
407
408此表列出每個元素:
409
410| 元素 | 它繪製的內容 | 位置 |
411| :- | :- | :- |
412| `Box` | 彈性容器。採用佈局屬性,例如 `flexDirection`、`columnGap`、`padding`、`borderStyle` 和 `width`。 | 到處 |
413| `Text` | 樣式文字。採用 `color`、`bold`、`dimColor`、`italic` 和 `wrap`。`color` 是主題鍵或顏色,例如 `'red'`。`wrap` 是 `'wrap'`、`'truncate'`、`'truncate-start'`、`'truncate-middle'` 或 `'truncate-end'`。 | 到處 |
414| `Button` | 呼叫 `onPress` 的控制項 | 到處 |
415| `Link`, `Code`, `Markdown` | 具有 `href` 和可選 `label` 的連結、程式碼區塊和格式化為 Claude 回覆方式的文字。`Markdown` 在 `text` 屬性中而不是在 `children` 中採用其內容,並在您傳遞 `onLinkPress` 時需要 `key`。 | 到處 |
416| `Input`, `Select` | 文字欄位和選擇器 | 終端、桌面 |
417| `Svg` | SVG 文件 | 桌面 |
418| `Client` | 由您的第二個檔案繪製的區域,用於動畫和指標輸入。該檔案不取得 mod API。它只能通過發佈資料到達您的鉤子,該資料作為 `ui.message` 事件到達。 | 終端、桌面 |
419| `Raster`, `Image` | [彩色儲存格網格](#draw-a-grid-of-colored-cells)和圖片 | 終端 |
420
421如果您的模組是 `.tsx` 或 `.jsx` 檔案,您可以將樹寫成 JSX。首先從 `$.ui.resolve(e)` 解構元素,因為鉤子模組沒有元素全域。
422
423如果樹使用應用程式沒有的元素、元素不採用的屬性或沒有子項的位置,Claude Code 會繪製其自己的位置版本。
424
425在使用 `--plugin-dir` 啟動的工作階段中,文字記錄行會說明這一點,例如 `ui.render (Pane) refused: Text prop "bogusProp" is not allowed; the engine drew its own`。[偵錯日誌](/docs/zh-TW/plugins/mods/troubleshoot#read-the-debug-log)將其記錄為 `ui.render (Pane): a hook returned a tree that does not validate` 並提供相同的原因。工作階段中沒有其他內容出現,因此當繪製不顯示時,請檢查該行或日誌。
426
427<h3 id="draw-a-grid-of-colored-cells">
428 繪製彩色儲存格網格
429</h3>
430
431對於熱力圖、迷你圖或終端中的遊戲板,繪製一個 `Raster` 而不是每個儲存格的 `Box`。`Raster` 採用 `key`、其大小(以 `columns` 和 `rows` 為單位)以及 `cells`,它將每個儲存格打包到一個字串中。每個儲存格是三個數字:字元的程式碼點、其顏色和其背景顏色。顏色是十六進位數字,紅色、綠色和藍色各有兩位數字,例如 `0xc62828` 表示紅色,或 `0x01000000` 表示終端的預設值。
432
433桌面應用程式沒有 `Raster`,因此請檢查 `e.surface` 並在那裡繪製文字。此窗格主體繪製一個三乘二的熱力圖:
434
435```javascript theme={null}
436// 表示「使用終端的預設顏色」的值
437const DEFAULT_COLOR = 0x01000000
438
439// 將 [character, color] 對的列打包到 Raster 採用的一個字串中
440// 一個儲存格是三個數字:字元的程式碼點、其顏色和其背景
441function cellsOf(rows) {
442 const numbers = rows.flat().flatMap(([char, color]) => [char.codePointAt(0), color, DEFAULT_COLOR])
443 return new Uint8Array(Uint32Array.from(numbers).buffer).toBase64()
444}
445
446on('ui.render', { component: 'Pane' }, async ($, e, next) => {
447 // 只在使用 id 'heat' 開啟的窗格中繪製
448 if (e.requestId !== 'heat') return next(e)
449 const { Box, Text, Raster } = $.ui.resolve(e)
450 // 三個儲存格的兩列,每個都是一個區塊字元及其顏色
451 const rows = [
452 [['█', 0x2e7d32], ['█', 0xf9a825], ['█', 0xc62828]],
453 [['█', 0x2e7d32], ['█', 0x2e7d32], ['█', 0xf9a825]],
454 ]
455 if (e.surface !== 'terminal') {
456 return Text({ children: ['The heat map needs the terminal.'] })
457 }
458 return Box({
459 flexDirection: 'column',
460 children: [Raster({ key: 'grid', columns: 3, rows: 2, cells: cellsOf(rows) })],
461 })
462})
463```
464
465在終端中,窗格顯示網格:
466
467<img src="https://mintcdn.com/claude-code/dgiVO_Od1X1faduV/images/mods-heat-map.svg?fit=max&auto=format&n=dgiVO_Od1X1faduV&q=85&s=b91bcce3bad74bc851149133d4acc5d5" alt="終端中的窗格,其中包含一個小的彩色區塊網格,兩列三個。頂列是綠色、琥珀色和紅色。底列是綠色、綠色和琥珀色。" width="360" height="132" data-path="images/mods-heat-map.svg" />
468
469`rows` 陣列是您要更改的部分,`cellsOf` 將其轉換為打包的字串。鉤子只在 `id` 為 `heat` 的窗格中繪製,因此從命令開啟一個,如 [`hello-tabs` 範例](#build-a-pane-with-tabs)開啟其窗格。
470
471每個字元必須是一個儲存格寬。若要動畫已在螢幕上的 `Raster`,請使用窗格的 `id` 作為 `requestId`、`Raster` 的 `key`、相同的大小和新儲存格呼叫 `$.ui.blit`。對於此範例,這是 `$.ui.blit({ requestId: 'heat', key: 'grid', columns: 3, rows: 2, cells: cellsOf(newRows) })`。它重新繪製該一個元素,而不再次執行您的 `ui.render` 鉤子。
472
473<h2 id="respond-to-presses-and-typing">
474 回應按下和輸入
475</h2>
476
477當使用者按下按鈕、輸入欄位或從您的 mod 繪製的清單中選擇時,Claude Code 會呼叫您給該控制項的函式,並在您的模組中執行。每個控制項採用其自己的回呼:
478
479* **`Button`**:採用 `onPress(e)`,其中 `e.surface` 是按下來自的應用程式
480* **`Input`**:採用 `onSubmit(value)` 和 `onInput(value)`
481* **`Select`**:採用 `onSelect(value)` 及其 `options` 中的選擇,至少一個具有唯一值的選擇清單,例如 `[{ value: 'sm', label: 'Small' }, { value: 'lg', label: 'Large' }]`
482
483測試通過其 `key` 按下或輸入到控制項,因此給每個控制項一個。控制項的每次使用也會引發 [`ui.press`、`ui.input` 或 `ui.select`](/docs/zh-TW/plugins/mods/reference#interface),其中 `key` 在 `e.element` 中,另一個 mod 可以鉤住這些事件。其鉤子在您的回呼之前執行,因此它會看到使用者輸入到您的 `Input` 中的內容,並可以更改它或代替您的回呼回答。mod API 沒有按下另一個 mod 按鈕的方法。
484
485<h3 id="know-which-keys-your-mod-can-receive">
486 鍵盤焦點和快捷鍵
487</h3>
488
489您的 mod 永遠不會自己讀取鍵盤。使用者按下一個鍵,Claude Code 決定它是為您的哪個控制項,該控制項的回呼執行。除了[帶狀區域上的數字快捷鍵](/docs/zh-TW/plugins/mods/reference#elements)外,這只在您的窗格或帶狀區域具有鍵盤焦點時發生。其餘時間,鍵進入提示。
490
491<h4 id="how-a-pane-gets-keyboard-focus">
492 窗格如何獲得鍵盤焦點
493</h4>
494
495窗格通過以下三種方式之一獲得鍵盤焦點:
496
497* 您的 mod 使用 `focus: true` 從命令或按下開啟它
498* 使用者按 Ctrl+X 然後 Tab
499* 使用者點擊它
500
501Claude Code 只在提示為空且沒有其他內容具有鍵盤焦點時授予 `focus: true`。在使用者輸入時開啟的窗格不會接收他們的按鍵。
502
503<h4 id="what-each-key-does">
504 每個鍵執行的操作
505</h4>
506
507此表列出當您的窗格或帶狀區域具有鍵盤焦點時鍵執行的操作:
508
509| 鍵 | 它執行的操作 |
510| :- | :- |
511| Tab | 移動到下一個控制項 |
512| 向上和向下 | 在您的繪製適合時在控制項之間移動。當窗格或帶狀區域的列數超過它可以顯示的列數時,它們會滾動它。 |
513| Enter | 按下焦點 `Button`、提交焦點 `Input` 或在 `Select` 中選擇 |
514| 按鈕的快捷鍵 | 按下該按鈕。當 `Input` 具有焦點時,每個可列印鍵都進入欄位。 |
515| Esc | 將鍵盤焦點返回到提示。使用 `closeOnEscape: true` 時,它也會關閉窗格。 |
516
517mod 無法將 Tab 或箭頭鍵綁定到其他任何內容,因此遊戲使用 `w`、`a`、`s` 和 `d` 進行轉向。
518
519<h4 id="set-a-hotkey-and-the-first-focus">
520 設定快捷鍵和第一個焦點
521</h4>
522
523控制項上的兩個屬性決定鍵盤如何到達它:
524
525* **`hotkey`**:若要讓使用者使用一個鍵按下 `Button`,請給它一個 `hotkey` 的一位數字或一個小寫字母,如 `hotkey: 'a'`
526* **`autoFocus`**:若要選擇窗格開啟時哪個控制項具有焦點,請將 `autoFocus: true` 新增到它。在其他項上省略屬性,因為 Claude Code 拒絕 `autoFocus: false`。
527
528快捷鍵的顯示方式取決於按鈕和應用程式:
529
530| 按鈕 | 在終端中 | 在桌面應用程式中 |
531| :- | :- | :- |
532| 帶括號,預設值 | `[ Add one ]`,沒有顯示快捷鍵 | 標籤旁邊有一個小鍵 |
533| 使用 `plain: true` | `1: One` | 標籤旁邊有一個小鍵 |
534
535在終端中,在括號按鈕的標籤中命名鍵,或使用 `plain: true`,以便使用者可以看到要按什麼。[元素參考](/docs/zh-TW/plugins/mods/reference#elements)有其他 `Button` 規則:`action`、帶狀區域上的數字快捷鍵,以及一個快捷鍵上的兩個按鈕。
536
537<h3 id="take-typed-input-and-draw-a-row-for-each-item">
538 取得輸入的文字並為每個項目繪製一列
539</h3>
540
541許多窗格是一個文字欄位,下面有一個清單。本部分中的範例是一個筆記窗格:您輸入一個筆記並按 Enter 新增它,每個筆記都有一個 `x` 按鈕來刪除它。新增兩個筆記後,終端會以這種方式繪製窗格:
542
543```text theme={null}
544╭──────────────────────────────────────────────────────────╮
545│ Note: Type a note and press Enter ⏎ add ✕ │
546│ x buy milk │
547│ x call bob │
548╰──────────────────────────────────────────────────────────╯
549```
550
551範例使用兩種技術:
552
553* **取得輸入的文字**:`Input` 在使用者按 Enter 時使用欄位的文字呼叫 `onSubmit(value)`,並在每次更改時呼叫 `onInput(value)`
554* **繪製清單**:將您的資料對應到每個一列,並給每列的按鈕其自己的 `key`
555
556此鉤子繪製窗格的內容:
557
558```javascript theme={null}
559// 窗格繪製的清單
560let notes = []
561
562on('ui.render', { component: 'Pane' }, async ($, e, next) => {
563 // 只在使用 id 'notes' 開啟的窗格中繪製
564 if (e.requestId !== 'notes') return next(e)
565 const { Box, Text, Button, Input } = $.ui.resolve(e)
566 const redraw = () => $.ui.invalidate('ui.render')
567
568 return Box({
569 flexDirection: 'column',
570 children: [
571 Input({
572 key: 'new-note',
573 label: 'Note',
574 placeholder: 'Type a note and press Enter',
575 // 每次繪製欄位為空,這在提交後清除它
576 value: '',
577 submitLabel: 'add',
578 autoFocus: true,
579 // 當您在欄位中按 Enter 時執行
580 onSubmit: async (value) => {
581 // 忽略空行
582 if (!value.trim()) return
583 notes = [...notes, value.trim()]
584 redraw()
585 await $.store.set('notes', notes)
586 },
587 }),
588 // 每個筆記一列:刪除按鈕,然後是筆記的文字
589 ...notes.map((note, i) =>
590 Box({
591 flexDirection: 'row',
592 columnGap: 1,
593 children: [
594 Button({
595 // 它自己的鍵,所以每列的按鈕可以區分
596 key: 'delete-' + i,
597 label: 'x',
598 plain: true,
599 onPress: async () => {
600 notes = notes.filter((_, j) => j !== i)
601 redraw()
602 await $.store.set('notes', notes)
603 },
604 }),
605 Text({ children: [note] }),
606 ],
607 }),
608 ),
609 ],
610 })
611})
612```
613
614若要嘗試窗格:
615
616* **新增筆記**:輸入一行並按 Enter。該行作為新列出現,欄位清空。
617* **刪除筆記**:按 Tab 直到筆記的 `x` 按鈕具有焦點,然後按 Enter。`x` 是按鈕的標籤,而不是快捷鍵,因此輸入字母不會按下它。
618
619每個更改都遵循與 `hello-tabs` 相同的渲染週期:回呼更改 `notes`、呼叫 `redraw` 並將清單儲存到 `$.store`。
620
621欄位在每次提交後清空,因為其 `value` 屬性。`value` 是繪製欄位時欄位保持的文字,使用者的輸入替換它,直到您的鉤子再次繪製欄位。範例始終使用 `''` 繪製欄位。
622
623範例儲存筆記但不載入它們。若要在下一個工作階段中將它們帶回,請在 `session.start` 鉤子中讀取它們,就像 `hello-tabs` 讀取 `count` 的方式一樣。
624
625三個屬性組成欄位的行,`Note: Type a note and press Enter ⏎ add`:
626
627| 屬性 | 在範例中 | 它是什麼 |
628| :- | :- | :- |
629| `label` | `Note` | 欄位前的文字。終端在其後繪製 `: `。 |
630| `placeholder` | `Type a note and press Enter` | 欄位為空時顯示的暗文字 |
631| `submitLabel` | `add` | `⏎` 後的單詞,說明 Enter 執行的操作 |
632
633提交 `Input` 不會啟動輪次,除非您的回呼呼叫 [`$.prompt.submit`](/docs/zh-TW/plugins/mods/api#start-a-turn-from-a-background-job)。
634
635<h2 id="redraw-when-something-changes">
636 重新繪製位置
637</h2>
638
639繪製是快照:它顯示您的 `ui.render` 鉤子上次執行時返回的內容。若要顯示新內容,鉤子必須再次執行。Claude Code 為某些更改再次執行它,您的 mod 要求其餘的。
640
641<h3 id="when-claude-code-redraws-without-being-asked">
642 當 Claude Code 在未被要求時重新繪製
643</h3>
644
645當位置的屬性更改或終端的寬度更改時,Claude Code 會再次執行您的 `ui.render` 鉤子。它不會在計時器上執行鉤子,也無法判斷您的模組中的變數何時更改。
646
647<h3 id="redraw-when-your-data-changes">
648 當您的資料更改時重新繪製
649</h3>
650
651若要在您自己的資料更改後重新繪製您的位置,請呼叫 `$.ui.invalidate('ui.render')`。此窗格計數按下。按鈕的回呼更改 `count`,然後要求重新繪製:
652
653```javascript theme={null}
654let count = 0
655
656on('ui.render', { component: 'Pane' }, async ($, e, next) => {
657 if (e.requestId !== 'counter') return next(e)
658 const { Box, Text, Button } = $.ui.resolve(e)
659 return Box({
660 flexDirection: 'row',
661 columnGap: 2,
662 children: [
663 Button({
664 key: 'more',
665 label: 'Add one',
666 onPress: () => {
667 count += 1
668 // 資料已更改,因此要求 Claude Code 再次繪製窗格
669 $.ui.invalidate('ui.render')
670 },
671 }),
672 Text({ children: ['Count: ' + count] }),
673 ],
674 })
675})
676```
677
678每次按下都會提高窗格中的數字。[`hello-tabs` 範例](#build-a-pane-with-tabs)將相同的呼叫包裝在其 `redraw` 函式中。
679
680您在 [`$.state`](#keep-a-value-in-\$-state) 中保持的值不需要呼叫,因為寫入值會重新繪製讀取它的位置。
681
682<h3 id="redraw-on-a-timer">
683 在計時器上重新繪製
684</h3>
685
686若要保持時鐘、倒計時或來自工作階段外部的值為最新,請按計劃重新繪製。在模組的 `session.start` 鉤子中啟動計時器。如果模組已經有一個,如 `hello-tabs` 所做的,請將 [`$.clock.every`](/docs/zh-TW/plugins/mods/api#run-work-in-the-background) 行新增到它:
687
688```javascript theme={null}
689on('session.start', async ($, e, next) => {
690 // 每 1000 毫秒,要求 Claude Code 再次繪製您的位置
691 $.clock.every(1000, () => $.ui.invalidate('ui.render'))
692 return next(e)
693})
694```
695
696Claude Code 現在每秒執行您的 `ui.render` 鉤子一次。計時器在模組重新載入時停止,新副本啟動自己的。
697
698<h3 id="how-often-a-site-can-redraw">
699 位置可以重新繪製的頻率
700</h3>
701
702Claude Code 限制重新繪製的頻率,因此您的 mod 可以在其資料更改時經常呼叫 `$.ui.invalidate`。可見窗格和帶狀區域的限制比其他位置更高,[限制表](/docs/zh-TW/plugins/mods/reference#limits)有數字。
703
704比限制更快到達的呼叫會合併為一次重新繪製。該重新繪製執行您的鉤子一次,鉤子在該時刻讀取您的資料,因此最新值顯示,介於兩者之間的值不顯示。動畫無法比限制更快執行。
705
706<h2 id="keep-state">
707 保持狀態
708</h2>
709
710mod 有三個地方可以保持值,它們在值持續多長時間方面有所不同:直到模組重新載入、直到工作階段結束或從一個工作階段到下一個工作階段。根據值必須持續多長時間選擇:
711
712| 將其保持在 | 它持續到 | 用於 |
713| :- | :- | :- |
714| 模組級變數 | 模組重新載入,這在開發期間每次儲存檔案時發生 | 您可以丟失的值,如 `hello-tabs` 中的 `tab` |
715| `$.state` | 工作階段結束,或使用者執行 `/clear`、`/resume` 或 `/branch` | 繪製依賴的值,應該在重新載入後存活 |
716| `$.store` | 您的 mod 刪除它,或沒有工作階段在 [`cleanupPeriodDays`](/docs/zh-TW/settings-reference#cleanupperioddays) 內讀取或寫入存放區。存放區是一個鍵值存放區,儲存為您外掛程式自己的 JSON 檔案,位於 `~/.claude/plugins/store/` 下。 | 設定、歷史記錄、使用者期望下次找到的任何內容 |
717
718`$.store.get(key)` 解析為值或 `undefined`,`$.store.set(key, value)` 採用任何 JSON 值。
719
720<h3 id="keep-a-value-in-state">
721 在 `$.state` 中保持值
722</h3>
723
724`$.state` 為工作階段的長度保持值,並為您重新繪製。它是反應式狀態:讀取值的 `ui.render` 鉤子訂閱它,因此 Claude Code 每次您寫入值時都會重新繪製該位置,您不呼叫 `$.ui.invalidate`。`$.state` 中的值也在模組重新載入後存活,變數不會。
725
726若要設定它,請宣告您的值、將您的清單指向宣告,然後定義並使用每個值。範例將 `hello-tabs` 中的 `count` 移動到 `$.state`。
727
728<h4 id="declare-the-values">
729 宣告值
730</h4>
731
732在類型檔案中宣告值。外部鍵是您的外掛程式的名稱,其下的每個項目是一個值及其類型。將此儲存為 `hello-tabs/types/index.d.ts`:
733
734```typescript hello-tabs/types/index.d.ts theme={null}
735declare module 'claude-code' {
736 interface PluginState {
737 'hello-tabs': {
738 tab: 'one' | 'two'
739 count: number
740 }
741 }
742}
743```
744
745<h4 id="point-the-manifest-at-the-declaration">
746 將清單指向宣告
747</h4>
748
749若要讓 `claude plugin validate` 根據該檔案檢查您的程式碼,請將 `types` 欄位新增到清單及其路徑:
750
751```json hello-tabs/.claude-plugin/plugin.json theme={null}
752{
753 "name": "hello-tabs",
754 "version": "0.1.0",
755 "description": "Opens a pane with two tabs and a counter",
756 "author": { "name": "Your Name" },
757 "types": "./types/index.d.ts"
758}
759```
760
761<h4 id="define-read-and-write-a-value">
762 定義、讀取和寫入值
763</h4>
764
765在您的模組中,使用預設值定義每個值,在繪製時讀取它,並從回呼寫入它。`atom` 命名值及其預設值,`read` 返回它,`update` 寫入它。三個幫助程式為您呼叫 `$.state.get` 和 `$.state.set`:
766
767```javascript theme={null}
768import { atom, read, update } from 'claude-code'
769
770// 在模組的頂部:命名值並給出其預設值
771const count = atom({ plugin: 'hello-tabs', key: 'count' }, 0)
772
773// 在 ui.render 鉤子中:讀取值以繪製它
774const n = await read($, count)
775
776// 在按鈕中:從舊值寫入新值
777onPress: () => update($, count, (value) => value + 1)
778```
779
780因為 `ui.render` 鉤子讀取 `count`,Claude Code 每次按鈕寫入它時都會再次執行鉤子。
781
782三個規則適用於程式碼:
783
784* **將 `plugin` 和 `key` 寫成文字字串**:`claude plugin validate` 從您的來源讀取它們
785* **在類型檔案中宣告每個值**:否則驗證失敗,出現 `hello-tabs.count is not declared`
786* **從回呼或另一個事件的鉤子寫入**:`ui.render` 鉤子可以讀取狀態,無法寫入它,因此從 `onPress`、`onSubmit` 或另一個事件的鉤子寫入
787
788<h4 id="change-hello-tabs-to-use-state">
789 將 `hello-tabs` 更改為使用 `$.state`
790</h4>
791
792若要將 `hello-tabs` 中的 `count` 移動到 `$.state`,請更改使用它的每一行:
793
794* **在模組的頂部**:新增 `import` 行,並將 `let count = 0` 替換為 `atom` 行
795* **在 `ui.render` 鉤子中**:在 `tabButton` 之前新增 `read` 行,並在 `Text` 中繪製 `'Count: ' + n`
796* **在 Add one 按鈕中**:將 `onPress` 替換為[從多個工作階段儲存](#save-from-more-than-one-session)中的按鈕,該按鈕儲存計數以及寫入它
797* **在 `session.start` 鉤子中**:將讀取 `saved` 的兩行替換為[在 `/clear` 後再次載入儲存的值](#load-a-saved-value-again-after-clear)中的 `loadCount` 呼叫
798
799保持 `redraw` 用於標籤按鈕,因為 `tab` 仍然是變數。
800
801<h3 id="load-a-saved-value-again-after-clear">
802 在 `/clear` 後再次載入儲存的值
803</h3>
804
805如果您的 mod 在 `session.start` 時將儲存的值從 `$.store` 複製到 `$.state`,則必須在 `/clear`、`/resume` 或 `/branch` 後再次複製它。這些命令將每個 `$.state` 值放回其預設值,`session.start` 不會再次引發。[`classic.SessionStart`](/docs/zh-TW/plugins/mods/events#hook-the-settings-hook-events) 在每個之後引發,`e.source` 設定為 `clear`、`resume` 或 `fork`,因此在鉤子上再次複製值。否則您的繪製顯示預設值,儲存 `$.state` 值的回呼會將預設值寫入您儲存的內容。
806
807此程式碼從兩個鉤子載入 `count`。它建立在 `hello-tabs` 的 `$.state` 版本上,其中 `count` 是原子,`update` 被匯入。將 `loadCount` 放在 `register` 上方,並將 `loadCount` 呼叫新增到您已經擁有的 `session.start` 鉤子。`classic.SessionStart` 也在啟動和壓縮後引發,這不會重設 `$.state`,因此 `source` 上的篩選將鉤子保持在三個重設:
808
809```javascript theme={null}
810// 將儲存的計數從 $.store 複製到 $.state,如果沒有儲存任何內容,則為 0
811async function loadCount($) {
812 const saved = Number((await $.store.get('count')) ?? 0)
813 await update($, count, () => saved)
814}
815
816// 在您的第一個提示之前執行,並在重新載入後再次執行
817on('session.start', async ($, e, next) => {
818 await loadCount($)
819 return next(e)
820})
821
822// 在 /clear、/resume 和 /branch 後再次執行,報告 fork
823on('classic.SessionStart', { source: ['clear', 'resume', 'fork'] }, async ($, e, next) => {
824 await loadCount($)
825 return next(e)
826})
827```
828
829兩個鉤子就位後,窗格在 `/clear` 後顯示儲存的計數,而不是 `0`,下一次按 **Add one** 會新增到儲存的計數。
830
831`loadCount` 將儲存的值寫入 `$.state` 中的值,`session.start` 每次模組重新載入時都會再次引發。若要保持存放區不落後,請在每次更改時儲存,如 **Add one** 按鈕所做的。
832
833若要在不工作階段的情況下檢查重新載入,請[在 `/clear` 後測試繪製](/docs/zh-TW/plugins/mods/test#test-a-drawing-after-clear)。
834
835<h3 id="save-from-more-than-one-session">
836 從多個工作階段儲存
837</h3>
838
839您機器上執行您的 mod 的每個工作階段都共享一個 `$.store`。`get` 後跟 `set` 不是原子的。當兩個工作階段各自讀取值、更改它並寫回時,它們會競爭,第二次寫入會替換第一次。
840
841兩個選擇使這種情況不太可能:
842
843* **給每個項目其自己的鍵**:`set` 只更改其自己的鍵,因此寫入不同鍵的工作階段不會相互覆蓋
844* **在寫入之前再次讀取**:對於多個工作階段更改的值,在回呼中 `get` 鍵並從該值建立新值,而不是從您在 `session.start` 時載入的副本。如果另一個工作階段的寫入落在您的 `get` 和 `set` 之間,仍然會丟失。
845
846此按鈕現在新增一個到存放區保持的任何內容,然後更新繪製:
847
848```javascript theme={null}
849onPress: async () => {
850 // 讀取存放區現在保持的內容,另一個工作階段可能已更改
851 const saved = Number((await $.store.get('count')) ?? 0)
852 // 儲存新計數,然後顯示它
853 await $.store.set('count', saved + 1)
854 await update($, count, () => saved + 1)
855}
856```
857
858如果第二個工作階段自此工作階段啟動以來按下了其自己的按鈕三次,此按下會顯示並儲存包含這三個的計數。
859
860<h2 id="next-steps">
861 後續步驟
862</h2>
863
864* [回應事件](/docs/zh-TW/plugins/mods/events):從工具呼叫和輪次提供您的繪製
865* [使用 mod API](/docs/zh-TW/plugins/mods/api):從計時器和模型呼叫提供您的繪製
866* [測試繪製](/docs/zh-TW/plugins/mods/test#test-a-drawing):從測試按下您的按鈕,在多個表面上
867* [渲染位置](/docs/zh-TW/plugins/mods/reference#render-sites)和[元素](/docs/zh-TW/plugins/mods/reference#elements):每個位置的屬性和每個元素的屬性