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 |
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
319`focus`, `closeOnEscape`, dan `holdToasts` bersifat opsional dan hanya menerima `true`. Untuk menghilangkan satu, hilangkan saja. Melewatkan `false` melempar kesalahan seperti `ui.open: focus is true or left out`. Untuk menetapkan salah satu dari mereka secara kondisional, tambahkan bidang hanya ketika kondisi berlaku. Panggilan ini meminta fokus keyboard hanya ketika `items` tidak kosong:
320
321```javascript theme={null}
322const pane = { id: 'hello-tabs', title: 'Hello tabs' }
323await $.ui.open(items.length > 0 ? { ...pane, focus: true } : pane)
324```
325
326Untuk 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.
327
328<h4 id="when-a-pane-waits-for-a-wider-terminal">
329 Ketika panel menunggu terminal yang lebih lebar
330</h4>
331
332Panel 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:
333
334* **Dibuka oleh sesuatu yang dilakukan pengguna**, seperti perintah yang mereka jalankan atau tombol yang mereka tekan, panel muncul pada lebar apa pun
335* **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.
336
337Ketika 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.
338
339<h2 id="build-a-tree-from-elements">
340 Bangun pohon dari elemen
341</h2>
342
343Apa 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.
344
345Untuk 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`.
346
347Sebagian besar gambar menggunakan empat elemen. Pilih tab untuk melihat masing-masing dan cara terminal menggambarnya:
348
349<Tabs>
350 <Tab title="Text">
351 `Text` menggambar string, dengan gaya opsional seperti `bold` dan `color`:
352
353 ```javascript theme={null}
354 Text({ children: ['This is the first tab.'] })
355 ```
356
357 ```text theme={null}
358 This is the first tab.
359 ```
360 </Tab>
361
362 <Tab title="Box">
363 `Box` mengatur apa yang ada di dalamnya, dalam baris atau kolom. Ini menempatkan tombol dan baris teks berdampingan, dua kolom terpisah:
364
365 ```javascript theme={null}
366 Box({
367 flexDirection: 'row',
368 columnGap: 2,
369 children: [
370 Button({ key: 'more', label: 'Add one', onPress: addOne }),
371 Text({ children: ['Count: 0'] }),
372 ],
373 })
374 ```
375
376 ```text theme={null}
377 [ Add one ] Count: 0
378 ```
379 </Tab>
380
381 <Tab title="Button">
382 `Button` adalah kontrol yang dapat ditekan pengguna. Ini menjalankan callback `onPress` Anda. Dengan `plain: true` tidak memiliki tanda kurung dan menampilkan pintasan keyboard:
383
384 ```javascript theme={null}
385 Button({ key: 'more', label: 'Add one', onPress: addOne })
386 Button({ key: 'tab-one', label: 'One', hotkey: '1', plain: true, onPress: showTabOne })
387 ```
388
389 ```text theme={null}
390 [ Add one ]
391 1: One
392 ```
393 </Tab>
394
395 <Tab title="Input">
396 `Input` adalah bidang teks. Ini menjalankan callback `onSubmit` Anda dengan teks saat pengguna menekan Enter:
397
398 ```javascript theme={null}
399 Input({
400 key: 'new-note',
401 label: 'Note',
402 placeholder: 'Type a note and press Enter',
403 value: '',
404 submitLabel: 'add',
405 onSubmit: addNote,
406 })
407 ```
408
409 ```text theme={null}
410 Note: Type a note and press Enter ⏎ add
411 ```
412 </Tab>
413</Tabs>
414
415Tabel ini mencantumkan setiap elemen:
416
417| Elemen | Apa yang digambarnya | Di mana |
418| :- | :- | :- |
419| `Box` | Kontainer flex. Mengambil prop tata letak seperti `flexDirection`, `columnGap`, `padding`, `borderStyle`, dan `width`. | Di mana-mana |
420| `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 |
421| `Button` | Kontrol yang memanggil `onPress` | Di mana-mana |
422| `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 |
423| `Input`, `Select` | Bidang teks dan pemilih | Terminal, Desktop |
424| `Svg` | Dokumen SVG | Desktop |
425| `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 |
426| `Raster`, `Image` | [Grid sel berwarna](#draw-a-grid-of-colored-cells), dan gambar | Terminal |
427
428Jika 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.
429
430Jika 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.
431
432Dalam 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.
433
434<h3 id="draw-a-grid-of-colored-cells">
435 Gambar grid sel berwarna
436</h3>
437
438Untuk 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.
439
440Aplikasi Desktop tidak memiliki `Raster`, jadi periksa `e.surface` dan gambar teks di sana. Badan panel ini menggambar peta panas tiga kali dua:
441
442```javascript theme={null}
443// Nilai yang berarti "gunakan warna default terminal"
444const DEFAULT_COLOR = 0x01000000
445
446// Kemasan baris pasangan [karakter, warna] ke dalam satu string yang diambil Raster
447// Satu sel adalah tiga angka: titik kode karakter, warnanya, dan warna latar belakangnya
448function cellsOf(rows) {
449 const numbers = rows.flat().flatMap(([char, color]) => [char.codePointAt(0), color, DEFAULT_COLOR])
450 return new Uint8Array(Uint32Array.from(numbers).buffer).toBase64()
451}
452
453on('ui.render', { component: 'Pane' }, async ($, e, next) => {
454 // Gambar hanya di panel yang dibuka dengan id 'heat'
455 if (e.requestId !== 'heat') return next(e)
456 const { Box, Text, Raster } = $.ui.resolve(e)
457 // Dua baris tiga sel, masing-masing karakter blok dan warnanya
458 const rows = [
459 [['█', 0x2e7d32], ['█', 0xf9a825], ['█', 0xc62828]],
460 [['█', 0x2e7d32], ['█', 0x2e7d32], ['█', 0xf9a825]],
461 ]
462 if (e.surface !== 'terminal') {
463 return Text({ children: ['The heat map needs the terminal.'] })
464 }
465 return Box({
466 flexDirection: 'column',
467 children: [Raster({ key: 'grid', columns: 3, rows: 2, cells: cellsOf(rows) })],
468 })
469})
470```
471
472Di terminal, panel menampilkan grid:
473
474<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" />
475
476Array `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.
477
478Setiap 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.
479
480<h2 id="respond-to-presses-and-typing">
481 Merespons penekanan dan pengetikan
482</h2>
483
484Ketika 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:
485
486* **`Button`**: mengambil `onPress(e)`, di mana `e.surface` adalah aplikasi tempat penekanan berasal
487* **`Input`**: mengambil `onSubmit(value)` dan `onInput(value)`
488* **`Select`**: mengambil `onSelect(value)` dengan pilihan dalam `options`, daftar setidaknya satu pilihan dengan nilai unik, seperti `[{ value: 'sm', label: 'Small' }, { value: 'lg', label: 'Large' }]`
489
490Tes 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.
491
492<h3 id="know-which-keys-your-mod-can-receive">
493 Fokus keyboard dan pintasan keyboard
494</h3>
495
496Mod 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.
497
498<h4 id="how-a-pane-gets-keyboard-focus">
499 Bagaimana panel mendapat fokus keyboard
500</h4>
501
502Panel mendapat fokus keyboard dalam salah satu dari tiga cara:
503
504* Mod Anda membukanya dengan `focus: true` dari perintah atau penekanan
505* Pengguna menekan Ctrl+X kemudian Tab
506* Pengguna mengkliknya
507
508Claude 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.
509
510<h4 id="what-each-key-does">
511 Apa yang dilakukan setiap kunci
512</h4>
513
514Tabel ini mencantumkan apa yang dilakukan kunci saat panel atau pita Anda memiliki fokus keyboard:
515
516| Kunci | Apa yang dilakukannya |
517| :- | :- |
518| Tab | Bergerak ke kontrol berikutnya |
519| Atas dan Bawah | Bergerak antar kontrol saat gambar Anda pas. Ketika panel atau pita memiliki lebih banyak baris daripada yang dapat ditampilkan, mereka menggulirnya. |
520| Enter | Menekan `Button` yang fokus, mengirimkan `Input` yang fokus, atau memilih di `Select` |
521| Pintasan keyboard tombol | Menekan tombol itu. Saat `Input` memiliki fokus, setiap kunci yang dapat dicetak pergi ke bidang. |
522| Esc | Mengembalikan fokus keyboard ke prompt. Dengan `closeOnEscape: true`, itu juga menutup panel. |
523
524Mod tidak dapat mengikat Tab atau tombol panah ke apa pun yang lain, jadi permainan mengarahkan dengan `w`, `a`, `s`, dan `d`.
525
526<h4 id="set-a-hotkey-and-the-first-focus">
527 Atur pintasan keyboard dan fokus pertama
528</h4>
529
530Dua prop pada kontrol memutuskan bagaimana keyboard mencapainya:
531
532* **`hotkey`**: untuk membiarkan pengguna menekan `Button` dengan satu kunci, berikan `hotkey` dari satu digit atau satu huruf kecil, seperti dalam `hotkey: 'a'`
533* **`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`.
534
535Bagaimana pintasan keyboard ditampilkan tergantung pada tombol dan aplikasi:
536
537| Tombol | Di terminal | Di aplikasi Desktop |
538| :- | :- | :- |
539| Dengan tanda kurung, default | `[ Add one ]`, tanpa pintasan keyboard ditampilkan | Label dengan kunci kecil di sebelahnya |
540| Dengan `plain: true` | `1: One` | Label dengan kunci kecil di sebelahnya |
541
542Di 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.
543
544<h3 id="take-typed-input-and-draw-a-row-for-each-item">
545 Ambil input yang diketik dan gambar baris untuk setiap item
546</h3>
547
548Banyak 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:
549
550```text theme={null}
551╭──────────────────────────────────────────────────────────╮
552│ Note: Type a note and press Enter ⏎ add ✕ │
553│ x buy milk │
554│ x call bob │
555╰──────────────────────────────────────────────────────────╯
556```
557
558Contoh menggunakan dua teknik:
559
560* **Ambil input yang diketik**: `Input` memanggil `onSubmit(value)` dengan teks bidang saat pengguna menekan Enter, dan `onInput(value)` pada setiap perubahan
561* **Gambar daftar**: petakan data Anda ke satu baris masing-masing, dan berikan setiap tombol baris `key`-nya sendiri
562
563Hook ini menggambar konten panel:
564
565```javascript theme={null}
566// Daftar yang digambar panel
567let notes = []
568
569on('ui.render', { component: 'Pane' }, async ($, e, next) => {
570 // Gambar hanya di panel yang dibuka dengan id 'notes'
571 if (e.requestId !== 'notes') return next(e)
572 const { Box, Text, Button, Input } = $.ui.resolve(e)
573 const redraw = () => $.ui.invalidate('ui.render')
574
575 return Box({
576 flexDirection: 'column',
577 children: [
578 Input({
579 key: 'new-note',
580 label: 'Note',
581 placeholder: 'Type a note and press Enter',
582 // Gambar bidang kosong setiap kali, yang menghapusnya setelah submit
583 value: '',
584 submitLabel: 'add',
585 autoFocus: true,
586 // Berjalan saat Anda menekan Enter di bidang
587 onSubmit: async (value) => {
588 // Abaikan baris kosong
589 if (!value.trim()) return
590 notes = [...notes, value.trim()]
591 redraw()
592 await $.store.set('notes', notes)
593 },
594 }),
595 // Satu baris untuk setiap catatan: tombol hapus, kemudian teks catatan
596 ...notes.map((note, i) =>
597 Box({
598 flexDirection: 'row',
599 columnGap: 1,
600 children: [
601 Button({
602 // Kunci miliknya sendiri, jadi setiap tombol baris dapat dibedakan
603 key: 'delete-' + i,
604 label: 'x',
605 plain: true,
606 onPress: async () => {
607 notes = notes.filter((_, j) => j !== i)
608 redraw()
609 await $.store.set('notes', notes)
610 },
611 }),
612 Text({ children: [note] }),
613 ],
614 }),
615 ),
616 ],
617 })
618})
619```
620
621Untuk mencoba panel:
622
623* **Tambahkan catatan**: ketik baris dan tekan Enter. Baris muncul sebagai baris baru, dan bidang kosong.
624* **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.
625
626Setiap perubahan mengikuti siklus render yang sama seperti `hello-tabs`: callback mengubah `notes`, memanggil `redraw`, dan menyimpan daftar ke `$.store`.
627
628Bidang 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 `''`.
629
630Contoh menyimpan catatan dan tidak memuatnya. Untuk membawanya kembali di sesi berikutnya, bacalah dalam hook `session.start`, cara `hello-tabs` membaca `count`.
631
632Tiga prop membuat baris bidang, `Note: Type a note and press Enter ⏎ add`:
633
634| Prop | Dalam contoh | Apa itu |
635| :- | :- | :- |
636| `label` | `Note` | Teks sebelum bidang. Terminal menggambar `: ` setelahnya. |
637| `placeholder` | `Type a note and press Enter` | Teks redup yang ditampilkan saat bidang kosong |
638| `submitLabel` | `add` | Kata setelah `⏎` yang mengatakan apa yang dilakukan Enter |
639
640Mengirimkan `Input` tidak memulai giliran kecuali callback Anda memanggil [`$.prompt.submit`](/docs/id/plugins/mods/api#start-a-turn-from-a-background-job).
641
642<h2 id="redraw-when-something-changes">
643 Gambar ulang situs
644</h2>
645
646Gambar 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.
647
648<h3 id="when-claude-code-redraws-without-being-asked">
649 Ketika Claude Code menggambar ulang tanpa diminta
650</h3>
651
652Claude 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.
653
654<h3 id="redraw-when-your-data-changes">
655 Gambar ulang saat data Anda berubah
656</h3>
657
658Untuk 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:
659
660```javascript theme={null}
661let count = 0
662
663on('ui.render', { component: 'Pane' }, async ($, e, next) => {
664 if (e.requestId !== 'counter') return next(e)
665 const { Box, Text, Button } = $.ui.resolve(e)
666 return Box({
667 flexDirection: 'row',
668 columnGap: 2,
669 children: [
670 Button({
671 key: 'more',
672 label: 'Add one',
673 onPress: () => {
674 count += 1
675 // Data berubah, jadi minta Claude Code menggambar panel lagi
676 $.ui.invalidate('ui.render')
677 },
678 }),
679 Text({ children: ['Count: ' + count] }),
680 ],
681 })
682})
683```
684
685Setiap penekanan menaikkan angka di panel. Contoh [`hello-tabs`](#build-a-pane-with-tabs) membungkus panggilan yang sama dalam fungsi `redraw`-nya.
686
687Nilai yang Anda simpan dalam [`$.state`](#keep-a-value-in-\$-state) tidak memerlukan panggilan, karena menulis nilai menggambar ulang situs yang membacanya.
688
689<h3 id="redraw-on-a-timer">
690 Gambar ulang pada timer
691</h3>
692
693Untuk 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:
694
695```javascript theme={null}
696on('session.start', async ($, e, next) => {
697 // Setiap 1000 milidetik, minta Claude Code menggambar situs Anda lagi
698 $.clock.every(1000, () => $.ui.invalidate('ui.render'))
699 return next(e)
700})
701```
702
703Claude Code sekarang menjalankan hook `ui.render` Anda sekali per detik. Timer berhenti saat modul dimuat ulang, dan salinan baru modul memulai miliknya sendiri.
704
705<h3 id="how-often-a-site-can-redraw">
706 Seberapa sering situs dapat digambar ulang
707</h3>
708
709Claude 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.
710
711Panggilan 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.
712
713<h2 id="keep-state">
714 Jaga status
715</h2>
716
717Mod 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:
718
719| Simpan di | Bertahan sampai | Gunakan untuk |
720| :- | :- | :- |
721| 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` |
722| `$.state` | Sesi berakhir, atau pengguna menjalankan `/clear`, `/resume`, atau `/branch` | Nilai yang gambar bergantung pada yang harus bertahan reload |
723| `$.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 |
724
725`$.store.get(key)` diselesaikan ke nilai atau `undefined`, dan `$.store.set(key, value)` mengambil nilai JSON apa pun.
726
727<h3 id="keep-a-value-in-state">
728 Simpan nilai dalam `$.state`
729</h3>
730
731`$.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.
732
733Untuk mengaturnya, deklarasikan nilai Anda, arahkan manifest Anda ke deklarasi, kemudian tentukan dan gunakan setiap nilai. Contoh memindahkan `count` dari `hello-tabs` ke dalam `$.state`.
734
735<h4 id="declare-the-values">
736 Deklarasikan nilai
737</h4>
738
739Deklarasikan 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`:
740
741```typescript hello-tabs/types/index.d.ts theme={null}
742declare module 'claude-code' {
743 interface PluginState {
744 'hello-tabs': {
745 tab: 'one' | 'two'
746 count: number
747 }
748 }
749}
750```
751
752<h4 id="point-the-manifest-at-the-declaration">
753 Arahkan manifest ke deklarasi
754</h4>
755
756Untuk membiarkan `claude plugin validate` memeriksa kode Anda terhadap file itu, tambahkan bidang `types` ke manifest dengan jalurnya:
757
758```json hello-tabs/.claude-plugin/plugin.json theme={null}
759{
760 "name": "hello-tabs",
761 "version": "0.1.0",
762 "description": "Opens a pane with two tabs and a counter",
763 "author": { "name": "Your Name" },
764 "types": "./types/index.d.ts"
765}
766```
767
768<h4 id="define-read-and-write-a-value">
769 Tentukan, baca, dan tulis nilai
770</h4>
771
772Dalam 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:
773
774```javascript theme={null}
775import { atom, read, update } from 'claude-code'
776
777// Di atas modul: beri nama nilai dan berikan defaultnya
778const count = atom({ plugin: 'hello-tabs', key: 'count' }, 0)
779
780// Dalam hook ui.render: baca nilai untuk menggambarnya
781const n = await read($, count)
782
783// Dalam Tombol: tulis nilai baru dari yang lama
784onPress: () => update($, count, (value) => value + 1)
785```
786
787Karena hook `ui.render` membaca `count`, Claude Code menjalankan hook lagi setiap kali tombol menulisnya.
788
789Tiga aturan berlaku untuk kode:
790
791* **Tulis `plugin` dan `key` sebagai string literal**: `claude plugin validate` membacanya dari sumber Anda
792* **Deklarasikan setiap nilai dalam file tipe**: jika tidak, validasi gagal dengan `hello-tabs.count is not declared`
793* **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
794
795<h4 id="change-hello-tabs-to-use-state">
796 Ubah `hello-tabs` untuk menggunakan `$.state`
797</h4>
798
799Untuk memindahkan `count` dalam `hello-tabs` ke dalam `$.state`, ubah setiap baris yang menggunakannya:
800
801* **Di atas modul**: tambahkan baris `import`, dan ganti `let count = 0` dengan baris `atom`
802* **Dalam hook `ui.render`**: tambahkan baris `read` sebelum `tabButton`, dan gambar `'Count: ' + n` dalam `Text`
803* **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
804* **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)
805
806Simpan `redraw` untuk tombol tab, karena `tab` masih variabel.
807
808<h3 id="load-a-saved-value-again-after-clear">
809 Muat nilai yang disimpan lagi setelah `/clear`
810</h3>
811
812Jika 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.
813
814Kode 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:
815
816```javascript theme={null}
817// Salin hitungan yang disimpan dari $.store ke dalam $.state, atau 0 jika tidak ada yang disimpan
818async function loadCount($) {
819 const saved = Number((await $.store.get('count')) ?? 0)
820 await update($, count, () => saved)
821}
822
823// Berjalan sebelum prompt pertama Anda, dan lagi setelah reload
824on('session.start', async ($, e, next) => {
825 await loadCount($)
826 return next(e)
827})
828
829// Berjalan lagi setelah /clear, /resume, dan /branch, yang melaporkan fork
830on('classic.SessionStart', { source: ['clear', 'resume', 'fork'] }, async ($, e, next) => {
831 await loadCount($)
832 return next(e)
833})
834```
835
836Dengan 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.
837
838`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**.
839
840Untuk memeriksa reload tanpa sesi, [uji gambar setelah `/clear`](/docs/id/plugins/mods/test#test-a-drawing-after-clear).
841
842<h3 id="save-from-more-than-one-session">
843 Simpan dari lebih dari satu sesi
844</h3>
845
846Setiap 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.
847
848Dua pilihan membuat itu kurang mungkin:
849
850* **Berikan setiap item kuncinya sendiri**: `set` mengubah hanya kuncinya sendiri, jadi sesi yang menulis kunci berbeda tidak menimpa satu sama lain
851* **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.
852
853Tombol ini menambahkan satu ke apa pun yang disimpan sekarang, kemudian memperbarui gambar:
854
855```javascript theme={null}
856onPress: async () => {
857 // Baca apa yang disimpan sekarang, yang sesi lain mungkin telah ubah
858 const saved = Number((await $.store.get('count')) ?? 0)
859 // Simpan hitungan baru, kemudian tampilkan
860 await $.store.set('count', saved + 1)
861 await update($, count, () => saved + 1)
862}
863```
864
865Jika sesi kedua telah menekan tombolnya sendiri tiga kali sejak sesi ini dimulai, penekanan ini menampilkan dan menyimpan hitungan yang mencakup ketiga itu.
866
867<h2 id="next-steps">
868 Langkah berikutnya
869</h2>
870
871* [Bereaksi terhadap event](/docs/id/plugins/mods/events): umpan gambar Anda dari panggilan alat dan giliran
872* [Gunakan API mods](/docs/id/plugins/mods/api): umpan gambar Anda dari timer dan panggilan model
873* [Uji gambar](/docs/id/plugins/mods/test#test-a-drawing): tekan tombol Anda dari tes, di lebih dari satu permukaan
874* [Render sites](/docs/id/plugins/mods/reference#render-sites) dan [elemen](/docs/id/plugins/mods/reference#elements): prop setiap situs dan prop setiap elemen