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# Menggambar di antarmuka dengan mod
6
7> Menggambar panel, pita di atas prompt, tombol, dan bidang teks dari mod Claude Code, menangani penekanan dan input, serta menjaga status antara redraw dan sesi.
8
9Mod dapat menggambar antarmukanya sendiri di Claude Code dan mengubah bagian antarmuka yang sudah digambar Claude Code. Setiap tempat mod dapat menggambar disebut [render site](/docs/id/plugins/mods/reference#render-sites), seperti panel, pita di atas prompt, atau spinner. Claude Code menaikkan event [`ui.render`](/docs/id/plugins/mods/reference#interface) setiap kali akan menggambar render site, dan hook Anda untuk event tersebut mengembalikan apa yang akan digambar di sana.
10
11Peta ini menunjukkan di mana mod dapat menggambar dalam sesi terminal:
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="Peta sesi terminal Claude Code. Mod dapat menambahkan panel sebagai sidebar di sebelah kanan, toast di sudut kanan atas transkrip, baris log dalam transkrip, pita di atas prompt, dan baris status di bawah prompt. Mod dapat menggambar ulang pesan, baris panggilan alat, dan spinner. Prompt adalah milik Claude Code sendiri." 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="Peta sesi terminal Claude Code. Mod dapat menambahkan panel sebagai sidebar di sebelah kanan, toast di sudut kanan atas transkrip, baris log dalam transkrip, pita di atas prompt, dan baris status di bawah prompt. Mod dapat menggambar ulang pesan, baris panggilan alat, dan spinner. Prompt adalah milik Claude Code sendiri." width="600" height="336" data-path="images/mods-screen-map-dark.svg" />
16
17Di terminal yang lebih sempit, panel duduk di atas prompt daripada di samping transkrip.
18
19Bangun [mod pertama Anda](/docs/id/plugins/mods/create) sebelum Anda mulai di sini. Mulai dengan contoh yang dikerjakan, yang membangun panel dengan dua tab dan penghitung, kemudian baca bagian untuk setiap bagian yang ingin Anda ubah.
20
21<Note>
22 Untuk mencari satu prop atau batas, lihat [referensi](/docs/id/plugins/mods/reference#render-sites).
23</Note>
24
25<h2 id="build-a-pane-with-tabs">
26 Bangun panel dengan tab
27</h2>
28
29Di bagian ini Anda membangun mod yang menambahkan perintah `/hello-tabs`, dan perintah membuka panel. Panel adalah sidebar di samping transkrip dalam terminal fullscreen yang lebar, atau wilayah berbingkai di atas prompt sebaliknya. Panel ini menampilkan dua tab, dan tab kedua memiliki tombol yang menambah satu ke penghitung. Hitungan masih ada setelah Anda memulai ulang Claude Code.
30
31Mod yang selesai terlihat seperti ini. Rekaman membuka panel, beralih ke tab kedua, menekan tombol beberapa kali, dan kembali ke tab pertama:
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="Perintah /hello-tabs diketik di prompt Claude Code dan panel berbingkai terbuka di atasnya, dengan '1: One' dan '2: Two' di seluruh bagian atas dan teks 'This is the first tab.' Tab kedua menampilkan tombol 'Add one' di samping 'Count: 1', dan hitungan naik menjadi 3. Panel kemudian kembali ke tab pertama." 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="Perintah /hello-tabs diketik di prompt Claude Code dan panel berbingkai terbuka di atasnya, dengan '1: One' dan '2: Two' di seluruh bagian atas dan teks 'This is the first tab.' Tab kedua menampilkan tombol 'Add one' di samping 'Count: 1', dan hitungan naik menjadi 3. Panel kemudian kembali ke tab pertama." data-path="images/mods-hello-tabs-dark.mp4" />
37</Frame>
38
39Claude Code tidak memiliki elemen tab bawaan, jadi tab adalah dua tombol dalam satu baris. Mod melacak mana yang aktif dan menggambar konten tab tersebut di bawah baris.
40
41<Steps>
42 <Step title="Buat plugin">
43 Mod adalah plugin dengan manifest, `hooks.json` yang menunjuk ke kode Anda, dan file kode. [Buat mod](/docs/id/plugins/mods/create#write-a-mod-yourself) menjelaskan masing-masing. Buat direktori bernama `hello-tabs` dengan direktori `.claude-plugin` dan `hooks` di dalamnya, kemudian simpan dua file pertama.
44
45 Simpan manifest sebagai `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 Beri nama titik masuk Anda di `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="Tulis kodenya">
66 Kode melakukan tiga pekerjaan, satu di setiap hook:
67
68 * Menambahkan perintah `/hello-tabs`
69 * Membuka panel saat Anda menjalankan perintah tersebut
70 * Menggambar konten panel: baris tab dan badan tab yang terbuka
71
72 Dua variabel tingkat modul, `tab` dan `count`, menyimpan status panel.
73
74 Simpan ini sebagai `hello-tabs/hooks/register.js`:
75
76 ```javascript hello-tabs/hooks/register.js theme={null}
77 // ID panel, digunakan untuk membuka panel dan mengenalinya saat menggambar
78 const PANE = 'hello-tabs'
79
80 // Apa yang ditampilkan panel: tab mana yang terbuka, dan nilai penghitung
81 let tab = 'one'
82 let count = 0
83
84 export function register(on) {
85 // Berjalan sebelum prompt pertama Anda, dan lagi setelah reload
86 on('session.start', async ($, e, next) => {
87 await $.command.register({ name: 'hello-tabs', description: 'Open the hello-tabs pane' })
88 // Muat hitungan yang disimpan sesi sebelumnya, jika ada
89 const saved = await $.store.get('count')
90 if (typeof saved === 'number') count = saved
91 return next(e)
92 })
93
94 // Berjalan saat Anda mengetik /hello-tabs
95 on('command.run', { command: 'hello-tabs' }, async ($) => {
96 // Buka panel, berikan keyboard, dan biarkan Esc menutupnya
97 await $.ui.open({ id: PANE, title: 'Hello tabs', focus: true, closeOnEscape: true })
98 // Cetak tidak ada dalam transkrip
99 return {}
100 })
101
102 // Berjalan setiap kali Claude Code menggambar panel
103 on('ui.render', { component: 'Pane' }, async ($, e, next) => {
104 // Biarkan panel mod lain saja
105 if (e.requestId !== PANE) return next(e)
106 // Dapatkan elemen yang dapat digambar aplikasi ini
107 const { Box, Text, Button } = $.ui.resolve(e)
108 // Minta Claude Code menjalankan hook ini lagi
109 const redraw = () => $.ui.invalidate('ui.render')
110
111 // Satu tab: tombol yang beralih ke tabnya saat ditekan
112 const tabButton = (name, label, hotkey) =>
113 Button({
114 key: 'tab-' + name,
115 label,
116 hotkey,
117 plain: true,
118 // Redup tab yang tidak terbuka
119 dimColor: tab !== name,
120 onPress: () => {
121 tab = name
122 redraw()
123 },
124 })
125
126 // Apa yang ada di bawah tab, tergantung mana yang terbuka
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 // Simpan hitungan sehingga ada setelah restart
143 await $.store.set('count', count)
144 },
145 }),
146 Text({ children: ['Count: ' + count] }),
147 ],
148 }),
149 ]
150
151 // Seluruh panel: baris tab, baris kosong, kemudian badan
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 Setiap hook juga melakukan sesuatu yang tidak jelas dari kode:
169
170 * **[`session.start`](/docs/id/plugins/mods/reference#session)** juga membaca hitungan yang disimpan dari [`$.store`](#keep-state), penyimpanan kunci-nilai yang bertahan antar sesi.
171 * **[`command.run`](/docs/id/plugins/mods/api#add-a-command)** hanya memberi tahu Claude Code bahwa panel ada. Membuka panel tidak menggambar apa pun dengan sendirinya: Claude Code kemudian menaikkan `ui.render` untuk menanyakan apa yang ada di dalamnya.
172 * **`ui.render`** mengembalikan pohon elemen, `Box` yang menyimpan kotak lain, teks, dan tombol, dan membangunnya lagi dari `tab` dan `count` setiap kali berjalan.
173
174 Menekan tombol menjalankan callback `onPress`-nya, yang mengubah variabel dan memanggil `redraw`. Claude Code kemudian menjalankan hook `ui.render` lagi, dan hook membangun pohon baru dari nilai baru. Setiap gambar interaktif menggunakan siklus render itu: callback mengubah status, dan hook merender lagi dari status baru.
175 </Step>
176
177 <Step title="Buka panel">
178 Di shell Anda, mulai Claude Code dengan `claude --plugin-dir ./hello-tabs`. Di prompt Claude Code, jalankan `/hello-tabs`. Panel terbuka dengan `1: One` dan `2: Two` di seluruh bagian atas. Tekan `2`, kemudian tekan `a`, pintasan keyboard untuk **Add one**, beberapa kali. Hitungan naik.
179 </Step>
180
181 <Step title="Periksa bahwa hitungan disimpan">
182 Tekan Esc untuk menutup panel, kemudian keluar dari sesi. Di shell Anda, mulai Claude Code lagi dengan perintah `claude --plugin-dir ./hello-tabs` yang sama, dan di prompt Claude Code jalankan `/hello-tabs`. Hitungan ada di mana Anda meninggalkannya.
183
184 Untuk menghapus hitungan, buat mod memanggil `$.store.delete('count')`. [Jaga status](#keep-state) mencakup berapa lama setiap jenis nilai bertahan.
185 </Step>
186</Steps>
187
188<h2 id="pick-where-to-draw">
189 Pilih di mana menggambar
190</h2>
191
192Hook `ui.render` berjalan untuk setiap render site kecuali Anda mempersempit ke yang Anda inginkan. Untuk memilih render site, teruskan filter, disebut [matcher](/docs/id/plugins/mods/events#filter-which-events-a-hook-handles), sebagai argumen kedua ke `on`. `{ component: 'Pane' }` menjalankan hook hanya untuk panel. Dalam hook, `e.component` menamai situs, `e.surface` mengatakan aplikasi mana yang menggambar, dan `e.props` menyimpan data situs sendiri. Untuk panel, `e.requestId` adalah `id` yang Anda buka dengannya.
193
194Dua situs kosong sampai mod mengisinya, panel dan pita. Pilih tab untuk melihat apa itu masing-masing dan cara menggambar di dalamnya:
195
196<Tabs>
197 <Tab title="Pane">
198 Panel adalah sidebar di samping transkrip dalam terminal fullscreen yang lebar, atau wilayah berbingkai di atas prompt sebaliknya. Dengan beberapa panel terbuka, masing-masing mendapat tab yang menampilkan judulnya.
199
200 Panel muncul saat mod Anda memanggil `$.ui.open` dengan `id` yang Anda pilih, seperti dalam `$.ui.open({ id: 'hello-tabs' })`. [Buka panel pada waktu yang tepat](#open-a-pane-at-the-right-time) mencakup bidang lain dan kapan panel menunggu terminal yang lebih lebar.
201
202 Untuk menggambar di panel Anda, filter pada `{ component: 'Pane' }` dan periksa bahwa `e.requestId` adalah `id` Anda.
203 </Tab>
204
205 <Tab title="Band above the prompt">
206 Pita adalah strip langsung di atas input prompt. Selalu ada, dan setiap mod berbaginya.
207
208 Hook Anda mengembalikan pohon untuk menampilkan sesuatu di pita, atau `next(e)` untuk menampilkan tidak ada. Pohon menggantikan apa yang mod [setelah Anda](/docs/id/plugins/mods/events#the-order-mods-run-in) gambar di sana. Untuk menjaga milik mereka, letakkan hasil `await next(e)` di antara anak-anak [`Box`](#build-a-tree-from-elements) dalam pohon Anda.
209
210 Untuk menggambar di pita, filter pada `{ component: 'AbovePrompt' }`.
211 </Tab>
212</Tabs>
213
214<h3 id="change-what-claude-code-already-draws">
215 Ubah apa yang sudah digambar Claude Code
216</h3>
217
218Claude Code menggambar sebagian besar antarmukanya sendiri: pesan, baris panggilan alat, spinner, dan lainnya. Masing-masing bagian itu adalah render site juga, jadi mod dapat mengubah gaya atau menggantinya. Untuk mengubah satu, filter hook `ui.render` Anda pada namanya dari tabel ini:
219
220| Situs | Apa itu |
221| :- | :- |
222| `UserMessage`, `AssistantMessage` | Pesan dalam transkrip |
223| `ToolUse`, `ToolResult`, `ToolGroup` | Baris panggilan alat, hasilnya, dan run panggilan yang dilipat |
224| `CommandOutput` | Baris yang dicetak perintah |
225| `AskUserQuestion` | Dialog yang dibuka Claude untuk menanyakan pertanyaan kepada Anda |
226| `Spinner`, `ToolProgress`, `TurnDuration` | Baris status untuk giliran: baris yang beranimasi saat Claude bekerja, baris kemajuan langsung alat yang berjalan, dan baris yang menutup giliran |
227| `InfoNotice`, `SessionMode`, `PromptHint` | Baris status di bawah logo, label mode di footer, dan baris petunjuk di bawah prompt |
228
229Di situs yang sudah digambar Claude Code, hook Anda memiliki tiga pilihan: ubah detail, ganti gambar, atau biarkan saja. Pilih tab untuk melihat masing-masing diterapkan pada spinner. Contoh membaca variabel `calls` yang hook lain hitung, seperti dalam [mod tutorial](/docs/id/plugins/mods/create#write-a-mod-yourself).
230
231<Tabs>
232 <Tab title="Change a detail">
233 Untuk menjaga gambar Claude Code dan mengubah satu bagian darinya, teruskan `next` salinan event dengan `props` yang diubah. Hook ini mengubah teks setelah kata spinner:
234
235 ```javascript theme={null}
236 on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
237 // Jaga spinner Claude Code, dan ubah teks setelah katanya
238 return next({ ...e, props: { ...e.props, suffix: ' · tool calls: ' + calls + '…' } })
239 })
240 ```
241
242 Spinner menjaga animasi dan katanya, dan teks Anda mengikuti kata:
243
244 ```text theme={null}
245 Thinking · tool calls: 2…
246 ```
247 </Tab>
248
249 <Tab title="Replace the drawing">
250 Untuk menggambar sesuatu milik Anda sendiri di tempat situs, kembalikan pohon dan jangan panggil `next`. Hook ini menggambar satu baris teks di mana spinner akan berada:
251
252 ```javascript theme={null}
253 on('ui.render', { component: 'Spinner' }, async ($, e) => {
254 const { Text } = $.ui.resolve(e)
255 // Tidak ada panggilan ke next, jadi baris ini digambar di tempat spinner
256 return Text({ children: ['Claude has made ' + calls + ' tool calls'] })
257 })
258 ```
259
260 Saat Claude bekerja, baris Anda menampilkan dan spinner Claude Code tidak:
261
262 ```text theme={null}
263 Claude has made 2 tool calls
264 ```
265 </Tab>
266
267 <Tab title="Leave it alone">
268 Untuk meninggalkan situs seperti yang digambar Claude Code, kembalikan `next(e)`. Hook sering melakukan itu untuk beberapa event dan tidak untuk yang lain. Hook ini meninggalkan spinner saja sampai ada panggilan untuk dihitung:
269
270 ```javascript theme={null}
271 on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
272 // Tidak ada yang ditampilkan dulu, jadi teruskan event tanpa perubahan
273 if (calls === 0) return next(e)
274 return next({ ...e, props: { ...e.props, suffix: ' · tool calls: ' + calls + '…' } })
275 })
276 ```
277
278 Sebelum panggilan alat pertama, spinner terlihat seperti tanpa mod:
279
280 ```text theme={null}
281 Thinking…
282 ```
283 </Tab>
284</Tabs>
285
286Prompt izin bukan render site, jadi mod tidak dapat mengubah apa yang ditampilkannya. Dialog pertanyaan, `AskUserQuestion`, adalah satu, jadi mod dapat mengubah itu.
287
288Terminal dan aplikasi Desktop tidak menaikkan semua situs yang sama. `Pane`, `AbovePrompt`, `Spinner`, dan situs transkrip bekerja di keduanya. Beberapa baris status lainnya hanya diangkat di terminal. Tabel [render sites](/docs/id/plugins/mods/reference#render-sites) mencantumkan di mana masing-masing diangkat.
289
290<h3 id="open-a-pane-at-the-right-time">
291 Buka panel pada waktu yang tepat
292</h3>
293
294Panel hanya muncul saat mod Anda membukanya. Bagaimana dan kapan Anda membukanya menentukan apakah itu mengambil fokus keyboard, berapa banyak ruang yang dimintanya, dan apakah itu menampilkan sama sekali di terminal yang sempit.
295
296Untuk membuka panel, panggil [`$.ui.open`](/docs/id/plugins/mods/reference#mods-api-methods) dengan `id` yang Anda pilih. `id` adalah nama panel: hook `ui.render` Anda memeriksanya, dan Anda meneruskannya lagi untuk menutup panel.
297
298```javascript theme={null}
299await $.ui.open({ id: 'hello-tabs', title: 'Hello tabs', focus: true })
300```
301
302Untuk menutup panel, panggil `$.ui.close` dengan `id` yang Anda buka dengannya:
303
304```javascript theme={null}
305await $.ui.close({ id: 'hello-tabs' })
306```
307
308Selain `id`, `$.ui.open` mengambil bidang opsional ini:
309
310| Bidang | Apa yang dilakukannya |
311| :- | :- |
312| `title` | Label tab panel saat lebih dari satu panel terbuka |
313| `focus` | Permintaan [fokus keyboard](#know-which-keys-your-mod-can-receive) |
314| `closeOnEscape` | Membuat Esc menutup panel. Teruskan `true` atau tinggalkan bidang, karena Claude Code menolak `false`. |
315| `holdToasts` | Menahan toast, pemberitahuan kecil dari [`$.ui.toast`](/docs/id/plugins/mods/api#show-something-without-starting-a-turn), sampai panel ditutup |
316| `rows` | Tinggi untuk diminta saat panel duduk di atas prompt. Default adalah sepertiga dari ruang. |
317| `columns` | Lebar untuk diminta saat panel duduk di samping transkrip |
318
319Untuk membiarkan perintah membuka panel saat Claude bekerja, tambahkan `immediate: true` saat Anda [mendaftarkan perintah](/docs/id/plugins/mods/api#add-a-command). Tanpanya, perintah yang diketik selama giliran menunggu giliran berakhir.
320
321<h4 id="when-a-pane-waits-for-a-wider-terminal">
322 Ketika panel menunggu terminal yang lebih lebar
323</h4>
324
325Panel yang dibuka mod Anda tanpa diminta tidak muncul di terminal sempit, jadi tidak dapat mengambil alih layar kecil. Apakah itu muncul tergantung pada apa yang membukanya:
326
327* **Dibuka oleh sesuatu yang dilakukan pengguna**, seperti perintah yang mereka jalankan atau tombol yang mereka tekan, panel muncul pada lebar apa pun
328* **Dibuka oleh mod Anda bertindak sendiri**, seperti dari timer atau hook [`turn.start`](/docs/id/plugins/mods/events#follow-a-turn), panel hanya muncul di terminal setidaknya 144 kolom lebar. Setelah pengguna telah membuka panel itu sendiri sekali, 110 kolom cukup.
329
330Ketika panel muncul, `$.ui.open` diselesaikan ke `{ isPlaced: true }`. Ketika panel menunggu, `isPlaced` adalah `false` dan `reason` adalah string yang mengatakan mengapa. Panel yang menunggu muncul saat pengguna membukanya atau memperlebar terminal. Untuk mengatakan sesuatu tersedia tanpa membuka panel, panggil `$.ui.toast('Your message')`, yang menampilkan pemberitahuan kecil yang hilang setelah beberapa detik.
331
332<h2 id="build-a-tree-from-elements">
333 Bangun pohon dari elemen
334</h2>
335
336Apa yang dikembalikan hook `ui.render` adalah pohon elemen: deskripsi apa yang akan digambar, terbuat dari kotak, teks, dan kontrol bersarang di dalam satu sama lain. Anda mendeskripsikan gambar, dan Claude Code merender di terminal atau aplikasi Desktop.
337
338Untuk mendapatkan elemen, panggil `$.ui.resolve(e)` dalam hook Anda, seperti dalam `const { Box, Text, Button } = $.ui.resolve(e)`. Setiap elemen adalah fungsi. Anda meneruskan prop, dan Anda menempatkan elemen dan string yang ada di dalamnya dalam `children`.
339
340Sebagian besar gambar menggunakan empat elemen. Pilih tab untuk melihat masing-masing dan cara terminal menggambarnya:
341
342<Tabs>
343 <Tab title="Text">
344 `Text` menggambar string, dengan gaya opsional seperti `bold` dan `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` mengatur apa yang ada di dalamnya, dalam baris atau kolom. Ini menempatkan tombol dan baris teks berdampingan, dua kolom terpisah:
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` adalah kontrol yang dapat ditekan pengguna. Ini menjalankan callback `onPress` Anda. Dengan `plain: true` tidak memiliki tanda kurung dan menampilkan pintasan keyboard:
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` adalah bidang teks. Ini menjalankan callback `onSubmit` Anda dengan teks saat pengguna menekan Enter:
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
408Tabel ini mencantumkan setiap elemen:
409
410| Elemen | Apa yang digambarnya | Di mana |
411| :- | :- | :- |
412| `Box` | Kontainer flex. Mengambil prop tata letak seperti `flexDirection`, `columnGap`, `padding`, `borderStyle`, dan `width`. | Di mana-mana |
413| `Text` | Teks bergaya. Mengambil `color`, `bold`, `dimColor`, `italic`, dan `wrap`. `color` adalah kunci tema atau warna seperti `'red'`. `wrap` adalah `'wrap'`, `'truncate'`, `'truncate-start'`, `'truncate-middle'`, atau `'truncate-end'`. | Di mana-mana |
414| `Button` | Kontrol yang memanggil `onPress` | Di mana-mana |
415| `Link`, `Code`, `Markdown` | Tautan dengan `href` dan `label` opsional, blok kode, dan teks yang diformat seperti balasan Claude. `Markdown` mengambil kontennya dalam prop `text`, bukan dalam `children`, dan membutuhkan `key` saat Anda meneruskan `onLinkPress`. | Di mana-mana |
416| `Input`, `Select` | Bidang teks dan pemilih | Terminal, Desktop |
417| `Svg` | Dokumen SVG | Desktop |
418| `Client` | Wilayah yang digambar oleh file kedua Anda, untuk animasi dan input pointer. File itu tidak mendapat API mods. Itu mencapai hook Anda hanya dengan memposting data, yang tiba sebagai event `ui.message`. | Terminal, Desktop |
419| `Raster`, `Image` | [Grid sel berwarna](#draw-a-grid-of-colored-cells), dan gambar | Terminal |
420
421Jika modul Anda adalah file `.tsx` atau `.jsx`, Anda dapat menulis pohon sebagai JSX. Dekonstruksi elemen dari `$.ui.resolve(e)` terlebih dahulu, karena modul hooks tidak memiliki global elemen.
422
423Jika pohon menggunakan elemen yang tidak dimiliki aplikasi, prop yang tidak diambil elemen, atau anak di mana tidak ada, Claude Code menggambar versinya sendiri dari situs.
424
425Dalam sesi yang dimulai dengan `--plugin-dir`, baris transkrip mengatakan demikian, seperti `ui.render (Pane) refused: Text prop "bogusProp" is not allowed; the engine drew its own`. [Debug log](/docs/id/plugins/mods/troubleshoot#read-the-debug-log) mencatatnya sebagai `ui.render (Pane): a hook returned a tree that does not validate` dengan alasan yang sama. Tidak ada yang lain muncul dalam sesi, jadi ketika gambar tidak muncul, periksa baris itu atau log.
426
427<h3 id="draw-a-grid-of-colored-cells">
428 Gambar grid sel berwarna
429</h3>
430
431Untuk peta panas, sparkline, atau papan permainan di terminal, gambar satu `Raster` dan bukan `Box` untuk setiap sel. `Raster` mengambil `key`, ukurannya dalam `columns` dan `rows`, dan `cells`, yang mengemas setiap sel menjadi satu string. Setiap sel adalah tiga angka: titik kode karakter, warnanya, dan warna latar belakangnya. Warna adalah angka heksadesimal dengan dua digit masing-masing untuk merah, hijau, dan biru, seperti `0xc62828` untuk merah, atau `0x01000000` untuk default terminal.
432
433Aplikasi Desktop tidak memiliki `Raster`, jadi periksa `e.surface` dan gambar teks di sana. Badan panel ini menggambar peta panas tiga kali dua:
434
435```javascript theme={null}
436// Nilai yang berarti "gunakan warna default terminal"
437const DEFAULT_COLOR = 0x01000000
438
439// Kemasan baris pasangan [karakter, warna] ke dalam satu string yang diambil Raster
440// Satu sel adalah tiga angka: titik kode karakter, warnanya, dan warna latar belakangnya
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 // Gambar hanya di panel yang dibuka dengan id 'heat'
448 if (e.requestId !== 'heat') return next(e)
449 const { Box, Text, Raster } = $.ui.resolve(e)
450 // Dua baris tiga sel, masing-masing karakter blok dan warnanya
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
465Di terminal, panel menampilkan grid:
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="Panel di terminal yang menyimpan grid kecil blok berwarna, dua baris tiga. Baris atas adalah hijau, amber, dan merah. Baris bawah adalah hijau, hijau, dan amber." width="360" height="132" data-path="images/mods-heat-map.svg" />
468
469Array `rows` adalah bagian yang akan Anda ubah, dan `cellsOf` mengubahnya menjadi string yang dikemas. Hook menggambar hanya di panel yang `id`-nya adalah `heat`, jadi buka satu dengan `$.ui.open({ id: 'heat' })` dari perintah, seperti contoh [`hello-tabs`](#build-a-pane-with-tabs) membuka panelnya.
470
471Setiap karakter harus lebar satu sel. Untuk menganimasikan `Raster` yang sudah di layar, panggil `$.ui.blit` dengan `id` panel sebagai `requestId`, `key` `Raster`, ukuran yang sama, dan sel baru. Untuk contoh ini, itu adalah `$.ui.blit({ requestId: 'heat', key: 'grid', columns: 3, rows: 2, cells: cellsOf(newRows) })`. Itu melukis ulang elemen itu saja tanpa menjalankan hook `ui.render` Anda lagi.
472
473<h2 id="respond-to-presses-and-typing">
474 Merespons penekanan dan pengetikan
475</h2>
476
477Ketika pengguna menekan tombol, mengetik ke bidang, atau memilih dari daftar yang digambar mod Anda, Claude Code memanggil fungsi yang Anda berikan kontrol itu, dan itu berjalan dalam modul Anda. Setiap kontrol mengambil callback-nya sendiri:
478
479* **`Button`**: mengambil `onPress(e)`, di mana `e.surface` adalah aplikasi tempat penekanan berasal
480* **`Input`**: mengambil `onSubmit(value)` dan `onInput(value)`
481* **`Select`**: mengambil `onSelect(value)` dengan pilihan dalam `options`, daftar setidaknya satu pilihan dengan nilai unik, seperti `[{ value: 'sm', label: 'Small' }, { value: 'lg', label: 'Large' }]`
482
483Tes menekan atau mengetik ke kontrol dengan `key`-nya, jadi berikan masing-masing satu. Setiap penggunaan kontrol juga menembakkan [`ui.press`, `ui.input`, atau `ui.select`](/docs/id/plugins/mods/reference#interface) dengan `key` dalam `e.element`, dan mod lain dapat menghubungkan event tersebut. Hook-nya berjalan sebelum callback Anda, jadi itu melihat apa yang pengguna ketik ke `Input` Anda dan dapat mengubahnya atau menjawab sebagai pengganti callback Anda. API mods tidak memiliki metode yang menekan tombol mod lain.
484
485<h3 id="know-which-keys-your-mod-can-receive">
486 Fokus keyboard dan pintasan keyboard
487</h3>
488
489Mod Anda tidak pernah membaca keyboard itu sendiri. Pengguna menekan kunci, Claude Code memutuskan kontrol mana yang dimaksudkan, dan callback kontrol itu berjalan. Terlepas dari [pintasan keyboard digit di pita](/docs/id/plugins/mods/reference#elements), itu hanya terjadi saat panel atau pita Anda memiliki fokus keyboard. Sisa waktu, kunci pergi ke prompt.
490
491<h4 id="how-a-pane-gets-keyboard-focus">
492 Bagaimana panel mendapat fokus keyboard
493</h4>
494
495Panel mendapat fokus keyboard dalam salah satu dari tiga cara:
496
497* Mod Anda membukanya dengan `focus: true` dari perintah atau penekanan
498* Pengguna menekan Ctrl+X kemudian Tab
499* Pengguna mengkliknya
500
501Claude Code memberikan `focus: true` hanya saat prompt kosong dan tidak ada yang lain memiliki fokus keyboard. Panel yang terbuka saat pengguna mengetik tidak mengambil keystroke mereka.
502
503<h4 id="what-each-key-does">
504 Apa yang dilakukan setiap kunci
505</h4>
506
507Tabel ini mencantumkan apa yang dilakukan kunci saat panel atau pita Anda memiliki fokus keyboard:
508
509| Kunci | Apa yang dilakukannya |
510| :- | :- |
511| Tab | Bergerak ke kontrol berikutnya |
512| Atas dan Bawah | Bergerak antar kontrol saat gambar Anda pas. Ketika panel atau pita memiliki lebih banyak baris daripada yang dapat ditampilkan, mereka menggulirnya. |
513| Enter | Menekan `Button` yang fokus, mengirimkan `Input` yang fokus, atau memilih di `Select` |
514| Pintasan keyboard tombol | Menekan tombol itu. Saat `Input` memiliki fokus, setiap kunci yang dapat dicetak pergi ke bidang. |
515| Esc | Mengembalikan fokus keyboard ke prompt. Dengan `closeOnEscape: true`, itu juga menutup panel. |
516
517Mod tidak dapat mengikat Tab atau tombol panah ke apa pun yang lain, jadi permainan mengarahkan dengan `w`, `a`, `s`, dan `d`.
518
519<h4 id="set-a-hotkey-and-the-first-focus">
520 Atur pintasan keyboard dan fokus pertama
521</h4>
522
523Dua prop pada kontrol memutuskan bagaimana keyboard mencapainya:
524
525* **`hotkey`**: untuk membiarkan pengguna menekan `Button` dengan satu kunci, berikan `hotkey` dari satu digit atau satu huruf kecil, seperti dalam `hotkey: 'a'`
526* **`autoFocus`**: untuk memilih kontrol mana yang memiliki fokus saat panel terbuka, tambahkan `autoFocus: true` ke dalamnya. Tinggalkan prop dari yang lain, karena Claude Code menolak `autoFocus: false`.
527
528Bagaimana pintasan keyboard ditampilkan tergantung pada tombol dan aplikasi:
529
530| Tombol | Di terminal | Di aplikasi Desktop |
531| :- | :- | :- |
532| Dengan tanda kurung, default | `[ Add one ]`, tanpa pintasan keyboard ditampilkan | Label dengan kunci kecil di sebelahnya |
533| Dengan `plain: true` | `1: One` | Label dengan kunci kecil di sebelahnya |
534
535Di terminal, beri nama kunci dalam label tombol berbingkai, atau gunakan `plain: true`, sehingga pengguna dapat melihat apa yang harus ditekan. [Referensi elemen](/docs/id/plugins/mods/reference#elements) memiliki aturan `Button` lainnya: `action`, pintasan keyboard digit di pita, dan dua tombol pada satu pintasan keyboard.
536
537<h3 id="take-typed-input-and-draw-a-row-for-each-item">
538 Ambil input yang diketik dan gambar baris untuk setiap item
539</h3>
540
541Banyak panel adalah bidang teks dengan daftar di bawahnya. Contoh di bagian ini adalah panel catatan: Anda mengetik catatan dan menekan Enter untuk menambahkannya, dan setiap catatan memiliki tombol `x` yang menghapusnya. Dengan dua catatan ditambahkan, terminal menggambar panel dengan cara ini:
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
551Contoh menggunakan dua teknik:
552
553* **Ambil input yang diketik**: `Input` memanggil `onSubmit(value)` dengan teks bidang saat pengguna menekan Enter, dan `onInput(value)` pada setiap perubahan
554* **Gambar daftar**: petakan data Anda ke satu baris masing-masing, dan berikan setiap tombol baris `key`-nya sendiri
555
556Hook ini menggambar konten panel:
557
558```javascript theme={null}
559// Daftar yang digambar panel
560let notes = []
561
562on('ui.render', { component: 'Pane' }, async ($, e, next) => {
563 // Gambar hanya di panel yang dibuka dengan 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 // Gambar bidang kosong setiap kali, yang menghapusnya setelah submit
576 value: '',
577 submitLabel: 'add',
578 autoFocus: true,
579 // Berjalan saat Anda menekan Enter di bidang
580 onSubmit: async (value) => {
581 // Abaikan baris kosong
582 if (!value.trim()) return
583 notes = [...notes, value.trim()]
584 redraw()
585 await $.store.set('notes', notes)
586 },
587 }),
588 // Satu baris untuk setiap catatan: tombol hapus, kemudian teks catatan
589 ...notes.map((note, i) =>
590 Box({
591 flexDirection: 'row',
592 columnGap: 1,
593 children: [
594 Button({
595 // Kunci miliknya sendiri, jadi setiap tombol baris dapat dibedakan
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
614Untuk mencoba panel:
615
616* **Tambahkan catatan**: ketik baris dan tekan Enter. Baris muncul sebagai baris baru, dan bidang kosong.
617* **Hapus catatan**: tekan Tab sampai tombol `x` catatan memiliki fokus, kemudian tekan Enter. `x` adalah label tombol dan bukan pintasan keyboard, jadi mengetik huruf tidak menekan itu.
618
619Setiap perubahan mengikuti siklus render yang sama seperti `hello-tabs`: callback mengubah `notes`, memanggil `redraw`, dan menyimpan daftar ke `$.store`.
620
621Bidang kosong setelah setiap submit karena prop `value`-nya. `value` adalah teks yang dipegang bidang saat digambar, dan pengetikan pengguna menggantinya sampai hook Anda menggambar bidang lagi. Contoh selalu menggambar bidang dengan `''`.
622
623Contoh menyimpan catatan dan tidak memuatnya. Untuk membawanya kembali di sesi berikutnya, bacalah dalam hook `session.start`, cara `hello-tabs` membaca `count`.
624
625Tiga prop membuat baris bidang, `Note: Type a note and press Enter ⏎ add`:
626
627| Prop | Dalam contoh | Apa itu |
628| :- | :- | :- |
629| `label` | `Note` | Teks sebelum bidang. Terminal menggambar `: ` setelahnya. |
630| `placeholder` | `Type a note and press Enter` | Teks redup yang ditampilkan saat bidang kosong |
631| `submitLabel` | `add` | Kata setelah `⏎` yang mengatakan apa yang dilakukan Enter |
632
633Mengirimkan `Input` tidak memulai giliran kecuali callback Anda memanggil [`$.prompt.submit`](/docs/id/plugins/mods/api#start-a-turn-from-a-background-job).
634
635<h2 id="redraw-when-something-changes">
636 Gambar ulang situs
637</h2>
638
639Gambar adalah snapshot: itu menunjukkan apa yang dikembalikan hook `ui.render` Anda terakhir kali hook berjalan. Untuk menampilkan sesuatu yang baru, hook harus berjalan lagi. Claude Code menjalankannya lagi untuk beberapa perubahan, dan mod Anda meminta sisanya.
640
641<h3 id="when-claude-code-redraws-without-being-asked">
642 Ketika Claude Code menggambar ulang tanpa diminta
643</h3>
644
645Claude Code menjalankan hook `ui.render` Anda lagi ketika prop situs berubah atau lebar terminal berubah. Itu tidak menjalankan hook pada timer, dan itu tidak dapat mengatakan ketika variabel dalam modul Anda berubah.
646
647<h3 id="redraw-when-your-data-changes">
648 Gambar ulang saat data Anda berubah
649</h3>
650
651Untuk memiliki situs Anda digambar lagi setelah data Anda sendiri berubah, panggil `$.ui.invalidate('ui.render')`. Panel ini menghitung penekanan. Callback tombol mengubah `count`, kemudian meminta redraw:
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 // Data berubah, jadi minta Claude Code menggambar panel lagi
669 $.ui.invalidate('ui.render')
670 },
671 }),
672 Text({ children: ['Count: ' + count] }),
673 ],
674 })
675})
676```
677
678Setiap penekanan menaikkan angka di panel. Contoh [`hello-tabs`](#build-a-pane-with-tabs) membungkus panggilan yang sama dalam fungsi `redraw`-nya.
679
680Nilai yang Anda simpan dalam [`$.state`](#keep-a-value-in-\$-state) tidak memerlukan panggilan, karena menulis nilai menggambar ulang situs yang membacanya.
681
682<h3 id="redraw-on-a-timer">
683 Gambar ulang pada timer
684</h3>
685
686Untuk menjaga jam, hitung mundur, atau nilai dari luar sesi saat ini, gambar ulang sesuai jadwal. Mulai timer dalam hook `session.start` modul. Jika modul sudah memiliki satu, seperti `hello-tabs`, tambahkan baris [`$.clock.every`](/docs/id/plugins/mods/api#run-work-in-the-background) ke dalamnya:
687
688```javascript theme={null}
689on('session.start', async ($, e, next) => {
690 // Setiap 1000 milidetik, minta Claude Code menggambar situs Anda lagi
691 $.clock.every(1000, () => $.ui.invalidate('ui.render'))
692 return next(e)
693})
694```
695
696Claude Code sekarang menjalankan hook `ui.render` Anda sekali per detik. Timer berhenti saat modul dimuat ulang, dan salinan baru modul memulai miliknya sendiri.
697
698<h3 id="how-often-a-site-can-redraw">
699 Seberapa sering situs dapat digambar ulang
700</h3>
701
702Claude Code membatasi seberapa sering itu menggambar ulang situs, jadi mod Anda dapat memanggil `$.ui.invalidate` sesering data berubah. Panel yang terlihat dan pita memiliki batas yang lebih tinggi daripada situs lainnya, dan [tabel batas](/docs/id/plugins/mods/reference#limits) memiliki angkanya.
703
704Panggilan yang datang lebih cepat dari batas digabungkan menjadi satu redraw. Redraw itu menjalankan hook Anda sekali, dan hook membaca data Anda seperti adanya saat itu, jadi nilai terbaru ditampilkan dan nilai di antaranya tidak. Animasi tidak dapat berjalan lebih cepat dari batas.
705
706<h2 id="keep-state">
707 Jaga status
708</h2>
709
710Mod memiliki tiga tempat untuk menyimpan nilai, dan mereka berbeda dalam berapa lama nilai bertahan: sampai modul dimuat ulang, sampai sesi berakhir, atau dari satu sesi ke sesi berikutnya. Pilih berdasarkan berapa lama nilai harus bertahan:
711
712| Simpan di | Bertahan sampai | Gunakan untuk |
713| :- | :- | :- |
714| Variabel tingkat modul | Modul dimuat ulang, yang terjadi setiap kali Anda menyimpan file selama pengembangan | Nilai yang dapat Anda hilangkan, seperti `tab` dalam `hello-tabs` |
715| `$.state` | Sesi berakhir, atau pengguna menjalankan `/clear`, `/resume`, atau `/branch` | Nilai yang gambar bergantung pada yang harus bertahan reload |
716| `$.store` | Mod Anda menghapusnya, atau tidak ada sesi yang membaca atau menulis penyimpanan selama [`cleanupPeriodDays`](/docs/id/settings-reference#cleanupperioddays). Penyimpanan adalah penyimpanan kunci-nilai, disimpan sebagai file JSON milik plugin Anda sendiri di bawah `~/.claude/plugins/store/`. | Pengaturan, riwayat, apa pun yang diharapkan pengguna untuk ditemukan lagi |
717
718`$.store.get(key)` diselesaikan ke nilai atau `undefined`, dan `$.store.set(key, value)` mengambil nilai JSON apa pun.
719
720<h3 id="keep-a-value-in-state">
721 Simpan nilai dalam `$.state`
722</h3>
723
724`$.state` menyimpan nilai untuk panjang sesi, dan itu menggambar ulang untuk Anda. Ini adalah status reaktif: hook `ui.render` yang membaca nilai berlangganan ke dalamnya, jadi Claude Code menggambar ulang situs itu setiap kali Anda menulis nilai, dan Anda tidak memanggil `$.ui.invalidate`. Nilai dalam `$.state` juga bertahan reload modul, yang variabel tidak.
725
726Untuk mengaturnya, deklarasikan nilai Anda, arahkan manifest Anda ke deklarasi, kemudian tentukan dan gunakan setiap nilai. Contoh memindahkan `count` dari `hello-tabs` ke dalam `$.state`.
727
728<h4 id="declare-the-values">
729 Deklarasikan nilai
730</h4>
731
732Deklarasikan nilai dalam file tipe. Kunci luar adalah nama plugin Anda, dan setiap entri di bawahnya adalah nilai dan tipenya. Simpan ini sebagai `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 Arahkan manifest ke deklarasi
747</h4>
748
749Untuk membiarkan `claude plugin validate` memeriksa kode Anda terhadap file itu, tambahkan bidang `types` ke manifest dengan jalurnya:
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 Tentukan, baca, dan tulis nilai
763</h4>
764
765Dalam modul Anda, tentukan setiap nilai dengan default, bacalah saat menggambar, dan tulislah dari callback. `atom` menamai nilai dan defaultnya, `read` mengembalikannya, dan `update` menulisnya. Tiga pembantu memanggil `$.state.get` dan `$.state.set` untuk Anda:
766
767```javascript theme={null}
768import { atom, read, update } from 'claude-code'
769
770// Di atas modul: beri nama nilai dan berikan defaultnya
771const count = atom({ plugin: 'hello-tabs', key: 'count' }, 0)
772
773// Dalam hook ui.render: baca nilai untuk menggambarnya
774const n = await read($, count)
775
776// Dalam Tombol: tulis nilai baru dari yang lama
777onPress: () => update($, count, (value) => value + 1)
778```
779
780Karena hook `ui.render` membaca `count`, Claude Code menjalankan hook lagi setiap kali tombol menulisnya.
781
782Tiga aturan berlaku untuk kode:
783
784* **Tulis `plugin` dan `key` sebagai string literal**: `claude plugin validate` membacanya dari sumber Anda
785* **Deklarasikan setiap nilai dalam file tipe**: jika tidak, validasi gagal dengan `hello-tabs.count is not declared`
786* **Tulis dari callback atau hook event lain**: hook `ui.render` dapat membaca status dan tidak dapat menulisnya, jadi tulis dari `onPress`, `onSubmit`, atau hook untuk event lain
787
788<h4 id="change-hello-tabs-to-use-state">
789 Ubah `hello-tabs` untuk menggunakan `$.state`
790</h4>
791
792Untuk memindahkan `count` dalam `hello-tabs` ke dalam `$.state`, ubah setiap baris yang menggunakannya:
793
794* **Di atas modul**: tambahkan baris `import`, dan ganti `let count = 0` dengan baris `atom`
795* **Dalam hook `ui.render`**: tambahkan baris `read` sebelum `tabButton`, dan gambar `'Count: ' + n` dalam `Text`
796* **Dalam tombol Add one**: ganti `onPress` dengan yang dalam [Simpan dari lebih dari satu sesi](#save-from-more-than-one-session), yang menyimpan hitungan serta menulisnya
797* **Dalam hook `session.start`**: ganti dua baris yang membaca `saved` dengan panggilan `loadCount` dari [Muat nilai yang disimpan lagi setelah `/clear`](#load-a-saved-value-again-after-clear)
798
799Simpan `redraw` untuk tombol tab, karena `tab` masih variabel.
800
801<h3 id="load-a-saved-value-again-after-clear">
802 Muat nilai yang disimpan lagi setelah `/clear`
803</h3>
804
805Jika mod Anda menyalin nilai yang disimpan dari `$.store` ke dalam `$.state` pada `session.start`, itu harus menyalinnya lagi setelah `/clear`, `/resume`, atau `/branch`. Perintah tersebut mengembalikan setiap nilai `$.state` ke defaultnya, dan `session.start` tidak dipecat lagi. [`classic.SessionStart`](/docs/id/plugins/mods/events#hook-the-settings-hook-events) dipecat setelah masing-masing, dengan `e.source` diatur ke `clear`, `resume`, atau `fork`, jadi salin nilai lagi dalam hook di atasnya. Jika tidak, gambar Anda menampilkan default, dan callback yang menyimpan nilai `$.state` menulis default di atas apa yang Anda simpan.
806
807Kode ini memuat `count` dari kedua hook. Itu dibangun di atas versi `$.state` dari `hello-tabs`, di mana `count` adalah atom dan `update` diimpor. Letakkan `loadCount` di atas `register`, dan tambahkan panggilan `loadCount` ke hook `session.start` yang sudah Anda miliki. `classic.SessionStart` juga dipecat saat startup dan setelah pemadatan, yang tidak mengatur ulang `$.state`, jadi filter pada `source` menjaga hook ke tiga reset:
808
809```javascript theme={null}
810// Salin hitungan yang disimpan dari $.store ke dalam $.state, atau 0 jika tidak ada yang disimpan
811async function loadCount($) {
812 const saved = Number((await $.store.get('count')) ?? 0)
813 await update($, count, () => saved)
814}
815
816// Berjalan sebelum prompt pertama Anda, dan lagi setelah reload
817on('session.start', async ($, e, next) => {
818 await loadCount($)
819 return next(e)
820})
821
822// Berjalan lagi setelah /clear, /resume, dan /branch, yang melaporkan fork
823on('classic.SessionStart', { source: ['clear', 'resume', 'fork'] }, async ($, e, next) => {
824 await loadCount($)
825 return next(e)
826})
827```
828
829Dengan kedua hook di tempat, panel menampilkan hitungan yang disimpan setelah `/clear` dan bukan `0`, dan penekanan berikutnya dari **Add one** menambah ke hitungan yang disimpan.
830
831`loadCount` menulis nilai yang disimpan di atas yang ada dalam `$.state`, dan `session.start` dipecat lagi setiap kali modul dimuat ulang. Untuk menjaga penyimpanan agar tidak tertinggal, simpan pada setiap perubahan, seperti yang dilakukan tombol **Add one**.
832
833Untuk memeriksa reload tanpa sesi, [uji gambar setelah `/clear`](/docs/id/plugins/mods/test#test-a-drawing-after-clear).
834
835<h3 id="save-from-more-than-one-session">
836 Simpan dari lebih dari satu sesi
837</h3>
838
839Setiap sesi di mesin Anda yang menjalankan mod Anda berbagi satu `$.store`. `get` diikuti oleh `set` bukan atomik. Ketika dua sesi masing-masing membaca nilai, mengubahnya, dan menulisnya kembali, mereka bersaing, dan penulisan kedua menggantikan yang pertama.
840
841Dua pilihan membuat itu kurang mungkin:
842
843* **Berikan setiap item kuncinya sendiri**: `set` mengubah hanya kuncinya sendiri, jadi sesi yang menulis kunci berbeda tidak menimpa satu sama lain
844* **Baca lagi tepat sebelum Anda menulis**: untuk nilai yang beberapa sesi ubah, `get` kunci dalam callback dan bangun nilai baru dari itu, bukan dari salinan yang Anda muat pada `session.start`. Penulisan sesi lain masih hilang jika mendarat antara `get` dan `set` Anda.
845
846Tombol ini menambahkan satu ke apa pun yang disimpan sekarang, kemudian memperbarui gambar:
847
848```javascript theme={null}
849onPress: async () => {
850 // Baca apa yang disimpan sekarang, yang sesi lain mungkin telah ubah
851 const saved = Number((await $.store.get('count')) ?? 0)
852 // Simpan hitungan baru, kemudian tampilkan
853 await $.store.set('count', saved + 1)
854 await update($, count, () => saved + 1)
855}
856```
857
858Jika sesi kedua telah menekan tombolnya sendiri tiga kali sejak sesi ini dimulai, penekanan ini menampilkan dan menyimpan hitungan yang mencakup ketiga itu.
859
860<h2 id="next-steps">
861 Langkah berikutnya
862</h2>
863
864* [Bereaksi terhadap event](/docs/id/plugins/mods/events): umpan gambar Anda dari panggilan alat dan giliran
865* [Gunakan API mods](/docs/id/plugins/mods/api): umpan gambar Anda dari timer dan panggilan model
866* [Uji gambar](/docs/id/plugins/mods/test#test-a-drawing): tekan tombol Anda dari tes, di lebih dari satu permukaan
867* [Render sites](/docs/id/plugins/mods/reference#render-sites) dan [elemen](/docs/id/plugins/mods/reference#elements): prop setiap situs dan prop setiap elemen