> ## Documentation Index
> Fetch the complete documentation index at: https://code.claude.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Menggambar di antarmuka dengan mod

> 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.

Mod 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.

Peta ini menunjukkan di mana mod dapat menggambar dalam sesi terminal:

<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" />

<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" />

Di terminal yang lebih sempit, panel duduk di atas prompt daripada di samping transkrip.

Bangun [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.

<Note>
  Untuk mencari satu prop atau batas, lihat [referensi](/docs/id/plugins/mods/reference#render-sites).
</Note>

<h2 id="build-a-pane-with-tabs">
  Bangun panel dengan tab
</h2>

Di 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.

Mod yang selesai terlihat seperti ini. Rekaman membuka panel, beralih ke tab kedua, menekan tombol beberapa kali, dan kembali ke tab pertama:

<Frame>
  <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" />

  <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" />
</Frame>

Claude 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.

<Steps>
  <Step title="Buat plugin">
    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.

    Simpan manifest sebagai `hello-tabs/.claude-plugin/plugin.json`:

    ```json hello-tabs/.claude-plugin/plugin.json theme={null}
    {
      "name": "hello-tabs",
      "version": "0.1.0",
      "description": "Opens a pane with two tabs and a counter",
      "author": { "name": "Your Name" }
    }
    ```

    Beri nama titik masuk Anda di `hello-tabs/hooks/hooks.json`:

    ```json hello-tabs/hooks/hooks.json theme={null}
    {
      "modules": ["./register.js"]
    }
    ```
  </Step>

  <Step title="Tulis kodenya">
    Kode melakukan tiga pekerjaan, satu di setiap hook:

    * Menambahkan perintah `/hello-tabs`
    * Membuka panel saat Anda menjalankan perintah tersebut
    * Menggambar konten panel: baris tab dan badan tab yang terbuka

    Dua variabel tingkat modul, `tab` dan `count`, menyimpan status panel.

    Simpan ini sebagai `hello-tabs/hooks/register.js`:

    ```javascript hello-tabs/hooks/register.js theme={null}
    // ID panel, digunakan untuk membuka panel dan mengenalinya saat menggambar
    const PANE = 'hello-tabs'

    // Apa yang ditampilkan panel: tab mana yang terbuka, dan nilai penghitung
    let tab = 'one'
    let count = 0

    export function register(on) {
      // Berjalan sebelum prompt pertama Anda, dan lagi setelah reload
      on('session.start', async ($, e, next) => {
        await $.command.register({ name: 'hello-tabs', description: 'Open the hello-tabs pane' })
        // Muat hitungan yang disimpan sesi sebelumnya, jika ada
        const saved = await $.store.get('count')
        if (typeof saved === 'number') count = saved
        return next(e)
      })

      // Berjalan saat Anda mengetik /hello-tabs
      on('command.run', { command: 'hello-tabs' }, async ($) => {
        // Buka panel, berikan keyboard, dan biarkan Esc menutupnya
        await $.ui.open({ id: PANE, title: 'Hello tabs', focus: true, closeOnEscape: true })
        // Cetak tidak ada dalam transkrip
        return {}
      })

      // Berjalan setiap kali Claude Code menggambar panel
      on('ui.render', { component: 'Pane' }, async ($, e, next) => {
        // Biarkan panel mod lain saja
        if (e.requestId !== PANE) return next(e)
        // Dapatkan elemen yang dapat digambar aplikasi ini
        const { Box, Text, Button } = $.ui.resolve(e)
        // Minta Claude Code menjalankan hook ini lagi
        const redraw = () => $.ui.invalidate('ui.render')

        // Satu tab: tombol yang beralih ke tabnya saat ditekan
        const tabButton = (name, label, hotkey) =>
          Button({
            key: 'tab-' + name,
            label,
            hotkey,
            plain: true,
            // Redup tab yang tidak terbuka
            dimColor: tab !== name,
            onPress: () => {
              tab = name
              redraw()
            },
          })

        // Apa yang ada di bawah tab, tergantung mana yang terbuka
        const body =
          tab === 'one'
            ? [Text({ children: ['This is the first tab.'] })]
            : [
                Box({
                  flexDirection: 'row',
                  columnGap: 2,
                  children: [
                    Button({
                      key: 'more',
                      label: 'Add one',
                      hotkey: 'a',
                      onPress: async () => {
                        count += 1
                        redraw()
                        // Simpan hitungan sehingga ada setelah restart
                        await $.store.set('count', count)
                      },
                    }),
                    Text({ children: ['Count: ' + count] }),
                  ],
                }),
              ]

        // Seluruh panel: baris tab, baris kosong, kemudian badan
        return Box({
          flexDirection: 'column',
          children: [
            Box({
              flexDirection: 'row',
              columnGap: 3,
              children: [tabButton('one', 'One', '1'), tabButton('two', 'Two', '2')],
            }),
            Text({ children: [' '] }),
            ...body,
          ],
        })
      })
    }
    ```

    Setiap hook juga melakukan sesuatu yang tidak jelas dari kode:

    * **[`session.start`](/docs/id/plugins/mods/reference#session)** juga membaca hitungan yang disimpan dari [`$.store`](#keep-state), penyimpanan kunci-nilai yang bertahan antar sesi.
    * **[`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.
    * **`ui.render`** mengembalikan pohon elemen, `Box` yang menyimpan kotak lain, teks, dan tombol, dan membangunnya lagi dari `tab` dan `count` setiap kali berjalan.

    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.
  </Step>

  <Step title="Buka panel">
    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.
  </Step>

  <Step title="Periksa bahwa hitungan disimpan">
    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.

    Untuk menghapus hitungan, buat mod memanggil `$.store.delete('count')`. [Jaga status](#keep-state) mencakup berapa lama setiap jenis nilai bertahan.
  </Step>
</Steps>

<h2 id="pick-where-to-draw">
  Pilih di mana menggambar
</h2>

Hook `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.

Dua situs kosong sampai mod mengisinya, panel dan pita. Pilih tab untuk melihat apa itu masing-masing dan cara menggambar di dalamnya:

<Tabs>
  <Tab title="Pane">
    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.

    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.

    Untuk menggambar di panel Anda, filter pada `{ component: 'Pane' }` dan periksa bahwa `e.requestId` adalah `id` Anda.
  </Tab>

  <Tab title="Band above the prompt">
    Pita adalah strip langsung di atas input prompt. Selalu ada, dan setiap mod berbaginya.

    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.

    Untuk menggambar di pita, filter pada `{ component: 'AbovePrompt' }`.
  </Tab>
</Tabs>

<h3 id="change-what-claude-code-already-draws">
  Ubah apa yang sudah digambar Claude Code
</h3>

Claude 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:

| Situs | Apa itu |
| :- | :- |
| `UserMessage`, `AssistantMessage` | Pesan dalam transkrip |
| `ToolUse`, `ToolResult`, `ToolGroup` | Baris panggilan alat, hasilnya, dan run panggilan yang dilipat |
| `CommandOutput` | Baris yang dicetak perintah |
| `AskUserQuestion` | Dialog yang dibuka Claude untuk menanyakan pertanyaan kepada Anda |
| `Spinner`, `ToolProgress`, `TurnDuration` | Baris status untuk giliran: baris yang beranimasi saat Claude bekerja, baris kemajuan langsung alat yang berjalan, dan baris yang menutup giliran |
| `InfoNotice`, `SessionMode`, `PromptHint` | Baris status di bawah logo, label mode di footer, dan baris petunjuk di bawah prompt |

Di 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).

<Tabs>
  <Tab title="Change a detail">
    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:

    ```javascript theme={null}
    on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
      // Jaga spinner Claude Code, dan ubah teks setelah katanya
      return next({ ...e, props: { ...e.props, suffix: ' · tool calls: ' + calls + '…' } })
    })
    ```

    Spinner menjaga animasi dan katanya, dan teks Anda mengikuti kata:

    ```text theme={null}
    Thinking · tool calls: 2…
    ```
  </Tab>

  <Tab title="Replace the drawing">
    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:

    ```javascript theme={null}
    on('ui.render', { component: 'Spinner' }, async ($, e) => {
      const { Text } = $.ui.resolve(e)
      // Tidak ada panggilan ke next, jadi baris ini digambar di tempat spinner
      return Text({ children: ['Claude has made ' + calls + ' tool calls'] })
    })
    ```

    Saat Claude bekerja, baris Anda menampilkan dan spinner Claude Code tidak:

    ```text theme={null}
    Claude has made 2 tool calls
    ```
  </Tab>

  <Tab title="Leave it alone">
    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:

    ```javascript theme={null}
    on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
      // Tidak ada yang ditampilkan dulu, jadi teruskan event tanpa perubahan
      if (calls === 0) return next(e)
      return next({ ...e, props: { ...e.props, suffix: ' · tool calls: ' + calls + '…' } })
    })
    ```

    Sebelum panggilan alat pertama, spinner terlihat seperti tanpa mod:

    ```text theme={null}
    Thinking…
    ```
  </Tab>
</Tabs>

Prompt izin bukan render site, jadi mod tidak dapat mengubah apa yang ditampilkannya. Dialog pertanyaan, `AskUserQuestion`, adalah satu, jadi mod dapat mengubah itu.

Terminal 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.

<h3 id="open-a-pane-at-the-right-time">
  Buka panel pada waktu yang tepat
</h3>

Panel 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.

Untuk 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.

```javascript theme={null}
await $.ui.open({ id: 'hello-tabs', title: 'Hello tabs', focus: true })
```

Untuk menutup panel, panggil `$.ui.close` dengan `id` yang Anda buka dengannya:

```javascript theme={null}
await $.ui.close({ id: 'hello-tabs' })
```

Selain `id`, `$.ui.open` mengambil bidang opsional ini:

| Bidang | Apa yang dilakukannya |
| :- | :- |
| `title` | Label tab panel saat lebih dari satu panel terbuka |
| `focus` | Permintaan [fokus keyboard](#know-which-keys-your-mod-can-receive) |
| `closeOnEscape` | Membuat Esc menutup panel. Teruskan `true` atau tinggalkan bidang, karena Claude Code menolak `false`. |
| `holdToasts` | Menahan toast, pemberitahuan kecil dari [`$.ui.toast`](/docs/id/plugins/mods/api#show-something-without-starting-a-turn), sampai panel ditutup |
| `rows` | Tinggi untuk diminta saat panel duduk di atas prompt. Default adalah sepertiga dari ruang. |
| `columns` | Lebar untuk diminta saat panel duduk di samping transkrip |

Untuk 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.

<h4 id="when-a-pane-waits-for-a-wider-terminal">
  Ketika panel menunggu terminal yang lebih lebar
</h4>

Panel 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:

* **Dibuka oleh sesuatu yang dilakukan pengguna**, seperti perintah yang mereka jalankan atau tombol yang mereka tekan, panel muncul pada lebar apa pun
* **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.

Ketika 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.

<h2 id="build-a-tree-from-elements">
  Bangun pohon dari elemen
</h2>

Apa 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.

Untuk 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`.

Sebagian besar gambar menggunakan empat elemen. Pilih tab untuk melihat masing-masing dan cara terminal menggambarnya:

<Tabs>
  <Tab title="Text">
    `Text` menggambar string, dengan gaya opsional seperti `bold` dan `color`:

    ```javascript theme={null}
    Text({ children: ['This is the first tab.'] })
    ```

    ```text theme={null}
    This is the first tab.
    ```
  </Tab>

  <Tab title="Box">
    `Box` mengatur apa yang ada di dalamnya, dalam baris atau kolom. Ini menempatkan tombol dan baris teks berdampingan, dua kolom terpisah:

    ```javascript theme={null}
    Box({
      flexDirection: 'row',
      columnGap: 2,
      children: [
        Button({ key: 'more', label: 'Add one', onPress: addOne }),
        Text({ children: ['Count: 0'] }),
      ],
    })
    ```

    ```text theme={null}
    [ Add one ]  Count: 0
    ```
  </Tab>

  <Tab title="Button">
    `Button` adalah kontrol yang dapat ditekan pengguna. Ini menjalankan callback `onPress` Anda. Dengan `plain: true` tidak memiliki tanda kurung dan menampilkan pintasan keyboard:

    ```javascript theme={null}
    Button({ key: 'more', label: 'Add one', onPress: addOne })
    Button({ key: 'tab-one', label: 'One', hotkey: '1', plain: true, onPress: showTabOne })
    ```

    ```text theme={null}
    [ Add one ]
    1: One
    ```
  </Tab>

  <Tab title="Input">
    `Input` adalah bidang teks. Ini menjalankan callback `onSubmit` Anda dengan teks saat pengguna menekan Enter:

    ```javascript theme={null}
    Input({
      key: 'new-note',
      label: 'Note',
      placeholder: 'Type a note and press Enter',
      value: '',
      submitLabel: 'add',
      onSubmit: addNote,
    })
    ```

    ```text theme={null}
    Note: Type a note and press Enter ⏎ add
    ```
  </Tab>
</Tabs>

Tabel ini mencantumkan setiap elemen:

| Elemen | Apa yang digambarnya | Di mana |
| :- | :- | :- |
| `Box` | Kontainer flex. Mengambil prop tata letak seperti `flexDirection`, `columnGap`, `padding`, `borderStyle`, dan `width`. | Di mana-mana |
| `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 |
| `Button` | Kontrol yang memanggil `onPress` | Di mana-mana |
| `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 |
| `Input`, `Select` | Bidang teks dan pemilih | Terminal, Desktop |
| `Svg` | Dokumen SVG | Desktop |
| `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 |
| `Raster`, `Image` | [Grid sel berwarna](#draw-a-grid-of-colored-cells), dan gambar | Terminal |

Jika 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.

Jika 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.

Dalam 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.

<h3 id="draw-a-grid-of-colored-cells">
  Gambar grid sel berwarna
</h3>

Untuk 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.

Aplikasi Desktop tidak memiliki `Raster`, jadi periksa `e.surface` dan gambar teks di sana. Badan panel ini menggambar peta panas tiga kali dua:

```javascript theme={null}
// Nilai yang berarti "gunakan warna default terminal"
const DEFAULT_COLOR = 0x01000000

// Kemasan baris pasangan [karakter, warna] ke dalam satu string yang diambil Raster
// Satu sel adalah tiga angka: titik kode karakter, warnanya, dan warna latar belakangnya
function cellsOf(rows) {
  const numbers = rows.flat().flatMap(([char, color]) => [char.codePointAt(0), color, DEFAULT_COLOR])
  return new Uint8Array(Uint32Array.from(numbers).buffer).toBase64()
}

on('ui.render', { component: 'Pane' }, async ($, e, next) => {
  // Gambar hanya di panel yang dibuka dengan id 'heat'
  if (e.requestId !== 'heat') return next(e)
  const { Box, Text, Raster } = $.ui.resolve(e)
  // Dua baris tiga sel, masing-masing karakter blok dan warnanya
  const rows = [
    [['█', 0x2e7d32], ['█', 0xf9a825], ['█', 0xc62828]],
    [['█', 0x2e7d32], ['█', 0x2e7d32], ['█', 0xf9a825]],
  ]
  if (e.surface !== 'terminal') {
    return Text({ children: ['The heat map needs the terminal.'] })
  }
  return Box({
    flexDirection: 'column',
    children: [Raster({ key: 'grid', columns: 3, rows: 2, cells: cellsOf(rows) })],
  })
})
```

Di terminal, panel menampilkan grid:

<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" />

Array `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.

Setiap 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.

<h2 id="respond-to-presses-and-typing">
  Merespons penekanan dan pengetikan
</h2>

Ketika 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:

* **`Button`**: mengambil `onPress(e)`, di mana `e.surface` adalah aplikasi tempat penekanan berasal
* **`Input`**: mengambil `onSubmit(value)` dan `onInput(value)`
* **`Select`**: mengambil `onSelect(value)` dengan pilihan dalam `options`, daftar setidaknya satu pilihan dengan nilai unik, seperti `[{ value: 'sm', label: 'Small' }, { value: 'lg', label: 'Large' }]`

Tes 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.

<h3 id="know-which-keys-your-mod-can-receive">
  Fokus keyboard dan pintasan keyboard
</h3>

Mod 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.

<h4 id="how-a-pane-gets-keyboard-focus">
  Bagaimana panel mendapat fokus keyboard
</h4>

Panel mendapat fokus keyboard dalam salah satu dari tiga cara:

* Mod Anda membukanya dengan `focus: true` dari perintah atau penekanan
* Pengguna menekan Ctrl+X kemudian Tab
* Pengguna mengkliknya

Claude 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.

<h4 id="what-each-key-does">
  Apa yang dilakukan setiap kunci
</h4>

Tabel ini mencantumkan apa yang dilakukan kunci saat panel atau pita Anda memiliki fokus keyboard:

| Kunci | Apa yang dilakukannya |
| :- | :- |
| Tab | Bergerak ke kontrol berikutnya |
| Atas dan Bawah | Bergerak antar kontrol saat gambar Anda pas. Ketika panel atau pita memiliki lebih banyak baris daripada yang dapat ditampilkan, mereka menggulirnya. |
| Enter | Menekan `Button` yang fokus, mengirimkan `Input` yang fokus, atau memilih di `Select` |
| Pintasan keyboard tombol | Menekan tombol itu. Saat `Input` memiliki fokus, setiap kunci yang dapat dicetak pergi ke bidang. |
| Esc | Mengembalikan fokus keyboard ke prompt. Dengan `closeOnEscape: true`, itu juga menutup panel. |

Mod tidak dapat mengikat Tab atau tombol panah ke apa pun yang lain, jadi permainan mengarahkan dengan `w`, `a`, `s`, dan `d`.

<h4 id="set-a-hotkey-and-the-first-focus">
  Atur pintasan keyboard dan fokus pertama
</h4>

Dua prop pada kontrol memutuskan bagaimana keyboard mencapainya:

* **`hotkey`**: untuk membiarkan pengguna menekan `Button` dengan satu kunci, berikan `hotkey` dari satu digit atau satu huruf kecil, seperti dalam `hotkey: 'a'`
* **`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`.

Bagaimana pintasan keyboard ditampilkan tergantung pada tombol dan aplikasi:

| Tombol | Di terminal | Di aplikasi Desktop |
| :- | :- | :- |
| Dengan tanda kurung, default | `[ Add one ]`, tanpa pintasan keyboard ditampilkan | Label dengan kunci kecil di sebelahnya |
| Dengan `plain: true` | `1: One` | Label dengan kunci kecil di sebelahnya |

Di 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.

<h3 id="take-typed-input-and-draw-a-row-for-each-item">
  Ambil input yang diketik dan gambar baris untuk setiap item
</h3>

Banyak 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:

```text theme={null}
╭──────────────────────────────────────────────────────────╮
│ Note: Type a note and press Enter ⏎ add                ✕ │
│ x buy milk │
│ x call bob │
╰──────────────────────────────────────────────────────────╯
```

Contoh menggunakan dua teknik:

* **Ambil input yang diketik**: `Input` memanggil `onSubmit(value)` dengan teks bidang saat pengguna menekan Enter, dan `onInput(value)` pada setiap perubahan
* **Gambar daftar**: petakan data Anda ke satu baris masing-masing, dan berikan setiap tombol baris `key`-nya sendiri

Hook ini menggambar konten panel:

```javascript theme={null}
// Daftar yang digambar panel
let notes = []

on('ui.render', { component: 'Pane' }, async ($, e, next) => {
  // Gambar hanya di panel yang dibuka dengan id 'notes'
  if (e.requestId !== 'notes') return next(e)
  const { Box, Text, Button, Input } = $.ui.resolve(e)
  const redraw = () => $.ui.invalidate('ui.render')

  return Box({
    flexDirection: 'column',
    children: [
      Input({
        key: 'new-note',
        label: 'Note',
        placeholder: 'Type a note and press Enter',
        // Gambar bidang kosong setiap kali, yang menghapusnya setelah submit
        value: '',
        submitLabel: 'add',
        autoFocus: true,
        // Berjalan saat Anda menekan Enter di bidang
        onSubmit: async (value) => {
          // Abaikan baris kosong
          if (!value.trim()) return
          notes = [...notes, value.trim()]
          redraw()
          await $.store.set('notes', notes)
        },
      }),
      // Satu baris untuk setiap catatan: tombol hapus, kemudian teks catatan
      ...notes.map((note, i) =>
        Box({
          flexDirection: 'row',
          columnGap: 1,
          children: [
            Button({
              // Kunci miliknya sendiri, jadi setiap tombol baris dapat dibedakan
              key: 'delete-' + i,
              label: 'x',
              plain: true,
              onPress: async () => {
                notes = notes.filter((_, j) => j !== i)
                redraw()
                await $.store.set('notes', notes)
              },
            }),
            Text({ children: [note] }),
          ],
        }),
      ),
    ],
  })
})
```

Untuk mencoba panel:

* **Tambahkan catatan**: ketik baris dan tekan Enter. Baris muncul sebagai baris baru, dan bidang kosong.
* **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.

Setiap perubahan mengikuti siklus render yang sama seperti `hello-tabs`: callback mengubah `notes`, memanggil `redraw`, dan menyimpan daftar ke `$.store`.

Bidang 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 `''`.

Contoh menyimpan catatan dan tidak memuatnya. Untuk membawanya kembali di sesi berikutnya, bacalah dalam hook `session.start`, cara `hello-tabs` membaca `count`.

Tiga prop membuat baris bidang, `Note: Type a note and press Enter ⏎ add`:

| Prop | Dalam contoh | Apa itu |
| :- | :- | :- |
| `label` | `Note` | Teks sebelum bidang. Terminal menggambar `: ` setelahnya. |
| `placeholder` | `Type a note and press Enter` | Teks redup yang ditampilkan saat bidang kosong |
| `submitLabel` | `add` | Kata setelah `⏎` yang mengatakan apa yang dilakukan Enter |

Mengirimkan `Input` tidak memulai giliran kecuali callback Anda memanggil [`$.prompt.submit`](/docs/id/plugins/mods/api#start-a-turn-from-a-background-job).

<h2 id="redraw-when-something-changes">
  Gambar ulang situs
</h2>

Gambar 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.

<h3 id="when-claude-code-redraws-without-being-asked">
  Ketika Claude Code menggambar ulang tanpa diminta
</h3>

Claude 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.

<h3 id="redraw-when-your-data-changes">
  Gambar ulang saat data Anda berubah
</h3>

Untuk 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:

```javascript theme={null}
let count = 0

on('ui.render', { component: 'Pane' }, async ($, e, next) => {
  if (e.requestId !== 'counter') return next(e)
  const { Box, Text, Button } = $.ui.resolve(e)
  return Box({
    flexDirection: 'row',
    columnGap: 2,
    children: [
      Button({
        key: 'more',
        label: 'Add one',
        onPress: () => {
          count += 1
          // Data berubah, jadi minta Claude Code menggambar panel lagi
          $.ui.invalidate('ui.render')
        },
      }),
      Text({ children: ['Count: ' + count] }),
    ],
  })
})
```

Setiap penekanan menaikkan angka di panel. Contoh [`hello-tabs`](#build-a-pane-with-tabs) membungkus panggilan yang sama dalam fungsi `redraw`-nya.

Nilai yang Anda simpan dalam [`$.state`](#keep-a-value-in-\$-state) tidak memerlukan panggilan, karena menulis nilai menggambar ulang situs yang membacanya.

<h3 id="redraw-on-a-timer">
  Gambar ulang pada timer
</h3>

Untuk 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:

```javascript theme={null}
on('session.start', async ($, e, next) => {
  // Setiap 1000 milidetik, minta Claude Code menggambar situs Anda lagi
  $.clock.every(1000, () => $.ui.invalidate('ui.render'))
  return next(e)
})
```

Claude Code sekarang menjalankan hook `ui.render` Anda sekali per detik. Timer berhenti saat modul dimuat ulang, dan salinan baru modul memulai miliknya sendiri.

<h3 id="how-often-a-site-can-redraw">
  Seberapa sering situs dapat digambar ulang
</h3>

Claude 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.

Panggilan 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.

<h2 id="keep-state">
  Jaga status
</h2>

Mod 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:

| Simpan di | Bertahan sampai | Gunakan untuk |
| :- | :- | :- |
| 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` |
| `$.state` | Sesi berakhir, atau pengguna menjalankan `/clear`, `/resume`, atau `/branch` | Nilai yang gambar bergantung pada yang harus bertahan reload |
| `$.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 |

`$.store.get(key)` diselesaikan ke nilai atau `undefined`, dan `$.store.set(key, value)` mengambil nilai JSON apa pun.

<h3 id="keep-a-value-in-state">
  Simpan nilai dalam `$.state`
</h3>

`$.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.

Untuk mengaturnya, deklarasikan nilai Anda, arahkan manifest Anda ke deklarasi, kemudian tentukan dan gunakan setiap nilai. Contoh memindahkan `count` dari `hello-tabs` ke dalam `$.state`.

<h4 id="declare-the-values">
  Deklarasikan nilai
</h4>

Deklarasikan 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`:

```typescript hello-tabs/types/index.d.ts theme={null}
declare module 'claude-code' {
  interface PluginState {
    'hello-tabs': {
      tab: 'one' | 'two'
      count: number
    }
  }
}
```

<h4 id="point-the-manifest-at-the-declaration">
  Arahkan manifest ke deklarasi
</h4>

Untuk membiarkan `claude plugin validate` memeriksa kode Anda terhadap file itu, tambahkan bidang `types` ke manifest dengan jalurnya:

```json hello-tabs/.claude-plugin/plugin.json theme={null}
{
  "name": "hello-tabs",
  "version": "0.1.0",
  "description": "Opens a pane with two tabs and a counter",
  "author": { "name": "Your Name" },
  "types": "./types/index.d.ts"
}
```

<h4 id="define-read-and-write-a-value">
  Tentukan, baca, dan tulis nilai
</h4>

Dalam 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:

```javascript theme={null}
import { atom, read, update } from 'claude-code'

// Di atas modul: beri nama nilai dan berikan defaultnya
const count = atom({ plugin: 'hello-tabs', key: 'count' }, 0)

// Dalam hook ui.render: baca nilai untuk menggambarnya
const n = await read($, count)

// Dalam Tombol: tulis nilai baru dari yang lama
onPress: () => update($, count, (value) => value + 1)
```

Karena hook `ui.render` membaca `count`, Claude Code menjalankan hook lagi setiap kali tombol menulisnya.

Tiga aturan berlaku untuk kode:

* **Tulis `plugin` dan `key` sebagai string literal**: `claude plugin validate` membacanya dari sumber Anda
* **Deklarasikan setiap nilai dalam file tipe**: jika tidak, validasi gagal dengan `hello-tabs.count is not declared`
* **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

<h4 id="change-hello-tabs-to-use-state">
  Ubah `hello-tabs` untuk menggunakan `$.state`
</h4>

Untuk memindahkan `count` dalam `hello-tabs` ke dalam `$.state`, ubah setiap baris yang menggunakannya:

* **Di atas modul**: tambahkan baris `import`, dan ganti `let count = 0` dengan baris `atom`
* **Dalam hook `ui.render`**: tambahkan baris `read` sebelum `tabButton`, dan gambar `'Count: ' + n` dalam `Text`
* **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
* **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)

Simpan `redraw` untuk tombol tab, karena `tab` masih variabel.

<h3 id="load-a-saved-value-again-after-clear">
  Muat nilai yang disimpan lagi setelah `/clear`
</h3>

Jika 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.

Kode 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:

```javascript theme={null}
// Salin hitungan yang disimpan dari $.store ke dalam $.state, atau 0 jika tidak ada yang disimpan
async function loadCount($) {
  const saved = Number((await $.store.get('count')) ?? 0)
  await update($, count, () => saved)
}

// Berjalan sebelum prompt pertama Anda, dan lagi setelah reload
on('session.start', async ($, e, next) => {
  await loadCount($)
  return next(e)
})

// Berjalan lagi setelah /clear, /resume, dan /branch, yang melaporkan fork
on('classic.SessionStart', { source: ['clear', 'resume', 'fork'] }, async ($, e, next) => {
  await loadCount($)
  return next(e)
})
```

Dengan 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.

`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**.

Untuk memeriksa reload tanpa sesi, [uji gambar setelah `/clear`](/docs/id/plugins/mods/test#test-a-drawing-after-clear).

<h3 id="save-from-more-than-one-session">
  Simpan dari lebih dari satu sesi
</h3>

Setiap 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.

Dua pilihan membuat itu kurang mungkin:

* **Berikan setiap item kuncinya sendiri**: `set` mengubah hanya kuncinya sendiri, jadi sesi yang menulis kunci berbeda tidak menimpa satu sama lain
* **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.

Tombol ini menambahkan satu ke apa pun yang disimpan sekarang, kemudian memperbarui gambar:

```javascript theme={null}
onPress: async () => {
  // Baca apa yang disimpan sekarang, yang sesi lain mungkin telah ubah
  const saved = Number((await $.store.get('count')) ?? 0)
  // Simpan hitungan baru, kemudian tampilkan
  await $.store.set('count', saved + 1)
  await update($, count, () => saved + 1)
}
```

Jika sesi kedua telah menekan tombolnya sendiri tiga kali sejak sesi ini dimulai, penekanan ini menampilkan dan menyimpan hitungan yang mencakup ketiga itu.

<h2 id="next-steps">
  Langkah berikutnya
</h2>

* [Bereaksi terhadap event](/docs/id/plugins/mods/events): umpan gambar Anda dari panggilan alat dan giliran
* [Gunakan API mods](/docs/id/plugins/mods/api): umpan gambar Anda dari timer dan panggilan model
* [Uji gambar](/docs/id/plugins/mods/test#test-a-drawing): tekan tombol Anda dari tes, di lebih dari satu permukaan
* [Render sites](/docs/id/plugins/mods/reference#render-sites) dan [elemen](/docs/id/plugins/mods/reference#elements): prop setiap situs dan prop setiap elemen
