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

# Рисование в интерфейсе с помощью мода

> Рисуйте панели, полосу над приглашением, кнопки и текстовые поля из мода Claude Code, обрабатывайте нажатия и ввод, и сохраняйте состояние между перерисовками и сеансами.

Мод может рисовать свой собственный интерфейс в Claude Code и изменять части интерфейса, которые уже рисует Claude Code. Каждое место, где мод может рисовать, называется [сайтом рендеринга](/docs/ru/plugins/mods/reference#render-sites), например панель, полоса над приглашением или спиннер. Claude Code вызывает событие [`ui.render`](/docs/ru/plugins/mods/reference#interface) каждый раз, когда собирается рисовать сайт рендеринга, и ваш хук для этого события возвращает то, что нужно рисовать там.

На этой карте показано, где мод может рисовать в сеансе терминала:

<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="Map of a Claude Code terminal session. A mod can add a pane as a sidebar on the right, a toast at the top right of the transcript, a log line in the transcript, a band above the prompt, and a status line under the prompt. A mod can redraw messages, tool call rows, and the spinner. The prompt is Claude Code's own." 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="Map of a Claude Code terminal session. A mod can add a pane as a sidebar on the right, a toast at the top right of the transcript, a log line in the transcript, a band above the prompt, and a status line under the prompt. A mod can redraw messages, tool call rows, and the spinner. The prompt is Claude Code's own." width="600" height="336" data-path="images/mods-screen-map-dark.svg" />

В более узком терминале панель находится над приглашением вместо того, чтобы находиться рядом с расшифровкой.

Создайте свой [первый мод](/docs/ru/plugins/mods/create) перед тем, как начать здесь. Начните с рабочего примера, который создает панель с двумя вкладками и счетчиком, затем прочитайте раздел для каждой части, которую вы хотите изменить.

<Note>
  Чтобы найти одно свойство или ограничение, см. [справку](/docs/ru/plugins/mods/reference#render-sites).
</Note>

<h2 id="build-a-pane-with-tabs">
  Создание панели с вкладками
</h2>

В этом разделе вы создаете мод, который добавляет команду `/hello-tabs`, и команда открывает панель. Панель — это боковая панель рядом с расшифровкой в широком полноэкранном терминале или обрамленная область над приглашением в противном случае. Эта панель показывает две вкладки, и вторая вкладка имеет кнопку, которая добавляет единицу к счетчику. Счет остается там после перезагрузки Claude Code.

Готовый мод выглядит так. Запись открывает панель, переключается на вторую вкладку, нажимает кнопку несколько раз и возвращается на первую вкладку:

<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="The /hello-tabs command is typed at the Claude Code prompt and a framed pane opens above it, with '1: One' and '2: Two' across the top and the text 'This is the first tab.' The second tab shows an 'Add one' button beside 'Count: 1', and the count rises to 3. The pane then returns to the first tab." 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="The /hello-tabs command is typed at the Claude Code prompt and a framed pane opens above it, with '1: One' and '2: Two' across the top and the text 'This is the first tab.' The second tab shows an 'Add one' button beside 'Count: 1', and the count rises to 3. The pane then returns to the first tab." data-path="images/mods-hello-tabs-dark.mp4" />
</Frame>

Claude Code не имеет встроенного элемента вкладок, поэтому вкладки — это две кнопки в ряду. Мод отслеживает, какая из них активна, и рисует содержимое этой вкладки под рядом.

<Steps>
  <Step title="Создание плагина">
    Мод — это плагин с манифестом, `hooks.json`, который указывает на ваш код, и файл кода. [Создание мода](/docs/ru/plugins/mods/create#write-a-mod-yourself) объясняет каждый из них. Создайте каталог с именем `hello-tabs` с каталогами `.claude-plugin` и `hooks` внутри него, затем сохраните первые два файла.

    Сохраните манифест как `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" }
    }
    ```

    Назовите точку входа в `hello-tabs/hooks/hooks.json`:

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

  <Step title="Написание кода">
    Код выполняет три задачи, по одной в каждом хуке:

    * Добавляет команду `/hello-tabs`
    * Открывает панель при запуске этой команды
    * Рисует содержимое панели: ряд вкладок и тело открытой вкладки

    Две переменные уровня модуля, `tab` и `count`, содержат состояние панели.

    Сохраните это как `hello-tabs/hooks/register.js`:

    ```javascript hello-tabs/hooks/register.js theme={null}
    // The pane's id, used to open the pane and to recognize it when drawing
    const PANE = 'hello-tabs'

    // What the pane shows: which tab is open, and the counter's value
    let tab = 'one'
    let count = 0

    export function register(on) {
      // Runs before your first prompt, and again after a reload
      on('session.start', async ($, e, next) => {
        await $.command.register({ name: 'hello-tabs', description: 'Open the hello-tabs pane' })
        // Load the count an earlier session saved, if there is one
        const saved = await $.store.get('count')
        if (typeof saved === 'number') count = saved
        return next(e)
      })

      // Runs when you type /hello-tabs
      on('command.run', { command: 'hello-tabs' }, async ($) => {
        // Open the pane, give it the keyboard, and let Esc close it
        await $.ui.open({ id: PANE, title: 'Hello tabs', focus: true, closeOnEscape: true })
        // Print nothing in the transcript
        return {}
      })

      // Runs each time Claude Code draws a pane
      on('ui.render', { component: 'Pane' }, async ($, e, next) => {
        // Leave other mods' panes alone
        if (e.requestId !== PANE) return next(e)
        // Get the elements this app can draw
        const { Box, Text, Button } = $.ui.resolve(e)
        // Ask Claude Code to run this hook again
        const redraw = () => $.ui.invalidate('ui.render')

        // One tab: a button that switches to its tab when pressed
        const tabButton = (name, label, hotkey) =>
          Button({
            key: 'tab-' + name,
            label,
            hotkey,
            plain: true,
            // Dim the tab that isn't open
            dimColor: tab !== name,
            onPress: () => {
              tab = name
              redraw()
            },
          })

        // What goes under the tabs, depending on which one is open
        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()
                        // Save the count so it's there after a restart
                        await $.store.set('count', count)
                      },
                    }),
                    Text({ children: ['Count: ' + count] }),
                  ],
                }),
              ]

        // The whole pane: the row of tabs, a blank line, then the body
        return Box({
          flexDirection: 'column',
          children: [
            Box({
              flexDirection: 'row',
              columnGap: 3,
              children: [tabButton('one', 'One', '1'), tabButton('two', 'Two', '2')],
            }),
            Text({ children: [' '] }),
            ...body,
          ],
        })
      })
    }
    ```

    Каждый хук также делает что-то, что код не делает явным:

    * **[`session.start`](/docs/ru/plugins/mods/reference#session)** также читает сохраненный счет из [`$.store`](#keep-state), хранилища ключ-значение, которое сохраняется между сеансами.
    * **[`command.run`](/docs/ru/plugins/mods/api#add-a-command)** только сообщает Claude Code, что панель существует. Открытие панели ничего не рисует само по себе: Claude Code затем вызывает `ui.render`, чтобы спросить, что в ней находится.
    * **`ui.render`** возвращает дерево элементов, `Box`, который содержит другие боксы, текст и кнопки, и строит его снова из `tab` и `count` каждый раз, когда он запускается.

    Нажатие кнопки запускает ее обратный вызов `onPress`, который изменяет переменную и вызывает `redraw`. Claude Code затем запускает хук `ui.render` снова, и хук строит новое дерево из новых значений. Каждое интерактивное рисование использует этот цикл рендеринга: обратный вызов изменяет состояние, и хук рисует снова из нового состояния.
  </Step>

  <Step title="Открытие панели">
    В вашей оболочке запустите Claude Code с помощью `claude --plugin-dir ./hello-tabs`. В приглашении Claude Code запустите `/hello-tabs`. Панель открывается с `1: One` и `2: Two` в верхней части. Нажмите `2`, затем нажмите `a`, горячую клавишу для **Add one**, несколько раз. Счет растет.
  </Step>

  <Step title="Проверка того, что счет был сохранен">
    Нажмите Esc, чтобы закрыть панель, затем выйдите из сеанса. В вашей оболочке запустите Claude Code снова с той же командой `claude --plugin-dir ./hello-tabs`, и в приглашении Claude Code запустите `/hello-tabs`. Счет находится там, где вы его оставили.

    Чтобы очистить счет, попросите мод вызвать `$.store.delete('count')`. [Сохранение состояния](#keep-state) охватывает, как долго длится каждый вид значения.
  </Step>
</Steps>

<h2 id="pick-where-to-draw">
  Выбор места для рисования
</h2>

Хук `ui.render` запускается для каждого сайта рендеринга, если вы не сузите его до того, который вы хотите рисовать. Чтобы выбрать сайт рендеринга, передайте фильтр, называемый [matcher](/docs/ru/plugins/mods/events#filter-which-events-a-hook-handles), в качестве второго аргумента для `on`. `{ component: 'Pane' }` запускает хук только для панелей. В хуке `e.component` называет сайт, `e.surface` говорит, какое приложение рисует, и `e.props` содержит собственные данные сайта. Для панели `e.requestId` — это `id`, с которым вы ее открыли.

Два сайта пусты, пока мод их не заполнит, панель и полоса. Выберите вкладку, чтобы увидеть, что это такое и как рисовать в них:

<Tabs>
  <Tab title="Pane">
    Панель — это боковая панель рядом с расшифровкой в широком полноэкранном терминале или обрамленная область над приглашением в противном случае. При открытии нескольких панелей каждая получает вкладку, которая показывает ее название.

    Панель появляется, когда ваш мод вызывает `$.ui.open` с `id`, который вы выбираете, как в `$.ui.open({ id: 'hello-tabs' })`. [Открытие панели в нужное время](#open-a-pane-at-the-right-time) охватывает другие поля и когда панель ждет более широкого терминала.

    Чтобы рисовать в вашей панели, отфильтруйте по `{ component: 'Pane' }` и проверьте, что `e.requestId` — это ваш `id`.
  </Tab>

  <Tab title="Band above the prompt">
    Полоса — это полоса прямо над вводом приглашения. Она всегда там, и каждый мод делит ее.

    Ваш хук возвращает дерево, чтобы показать что-то в полосе, или `next(e)`, чтобы показать ничего. Дерево заменяет то, что модов [после вашего](/docs/ru/plugins/mods/events#the-order-mods-run-in) рисуют там. Чтобы сохранить их, поместите результат `await next(e)` среди дочерних элементов [`Box`](#build-a-tree-from-elements) в вашем дереве.

    Чтобы рисовать в полосе, отфильтруйте по `{ component: 'AbovePrompt' }`.
  </Tab>
</Tabs>

<h3 id="change-what-claude-code-already-draws">
  Изменение того, что уже рисует Claude Code
</h3>

Claude Code рисует большую часть своего интерфейса сам: сообщения, строки вызовов инструментов, спиннер и многое другое. Каждая из этих частей также является сайтом рендеринга, поэтому мод может переделать стиль или заменить его. Чтобы изменить один, отфильтруйте ваш хук `ui.render` по его имени из этой таблицы:

| Сайт | Что это такое |
| :- | :- |
| `UserMessage`, `AssistantMessage` | Сообщение в расшифровке |
| `ToolUse`, `ToolResult`, `ToolGroup` | Строка вызова инструмента, его результат и свернутый запуск вызовов |
| `CommandOutput` | Строка, которую напечатала команда |
| `AskUserQuestion` | Диалог, который Claude открывает, чтобы задать вам вопрос |
| `Spinner`, `ToolProgress`, `TurnDuration` | Строки состояния для хода: строка, которая анимируется, пока Claude работает, строка прямого прогресса работающего инструмента и строка, которая закрывает ход |
| `InfoNotice`, `SessionMode`, `PromptHint` | Строки состояния под логотипом, метки режима в нижнем колонтитуле и строка подсказки под приглашением |

На сайте, который Claude Code уже рисует, ваш хук имеет три варианта: изменить деталь, заменить рисование или оставить его в покое. Выберите вкладку, чтобы увидеть каждый из них, применяемый к спиннеру. Примеры читают переменную `calls`, которую другой хук считает, как в [учебном моде](/docs/ru/plugins/mods/create#write-a-mod-yourself).

<Tabs>
  <Tab title="Change a detail">
    Чтобы сохранить рисование Claude Code и изменить одну его часть, передайте `next` копию события с измененными `props`. Этот хук изменяет текст после слова спиннера:

    ```javascript theme={null}
    on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
      // Keep Claude Code's spinner, and change the text after its word
      return next({ ...e, props: { ...e.props, suffix: ' · tool calls: ' + calls + '…' } })
    })
    ```

    Спиннер сохраняет свою анимацию и свое слово, и ваш текст следует за словом:

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

  <Tab title="Replace the drawing">
    Чтобы нарисовать что-то свое на месте сайта, верните дерево и не вызывайте `next`. Этот хук рисует одну строку текста там, где был бы спиннер:

    ```javascript theme={null}
    on('ui.render', { component: 'Spinner' }, async ($, e) => {
      const { Text } = $.ui.resolve(e)
      // No call to next, so this line is drawn in the spinner's place
      return Text({ children: ['Claude has made ' + calls + ' tool calls'] })
    })
    ```

    Пока Claude работает, ваша строка показывается и спиннер Claude Code не показывается:

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

  <Tab title="Leave it alone">
    Чтобы оставить сайт так, как его рисует Claude Code, верните `next(e)`. Хук часто делает это для некоторых событий и не для других. Этот хук оставляет спиннер в покое, пока нечего считать:

    ```javascript theme={null}
    on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
      // Nothing to show yet, so pass the event on unchanged
      if (calls === 0) return next(e)
      return next({ ...e, props: { ...e.props, suffix: ' · tool calls: ' + calls + '…' } })
    })
    ```

    До первого вызова инструмента спиннер выглядит так, как он выглядит без мода:

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

Приглашение разрешения не является сайтом рендеринга, поэтому мод не может изменить то, что оно показывает. Диалог вопроса, `AskUserQuestion`, является одним, поэтому мод может изменить это.

Терминал и приложение Desktop не вызывают все одни и те же сайты. `Pane`, `AbovePrompt`, `Spinner` и сайты расшифровки работают в обоих. Несколько других строк состояния вызываются только в терминале. Таблица [сайтов рендеринга](/docs/ru/plugins/mods/reference#render-sites) указывает, где каждый из них вызывается.

<h3 id="open-a-pane-at-the-right-time">
  Открытие панели в нужное время
</h3>

Панель появляется только когда ваш мод ее открывает. То, как и когда вы ее открываете, определяет, получает ли она фокус клавиатуры, сколько места она запрашивает и появляется ли она вообще в узком терминале.

Чтобы открыть панель, вызовите [`$.ui.open`](/docs/ru/plugins/mods/reference#mods-api-methods) с `id`, который вы выбираете. `id` — это имя панели: ваш хук `ui.render` проверяет его, и вы передаете его снова, чтобы закрыть панель.

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

Чтобы закрыть панель, вызовите `$.ui.close` с `id`, с которым вы ее открыли:

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

Помимо `id`, `$.ui.open` принимает эти необязательные поля:

| Поле | Что оно делает |
| :- | :- |
| `title` | Метка вкладки панели, когда открыто несколько панелей |
| `focus` | Запрашивает [фокус клавиатуры](#know-which-keys-your-mod-can-receive) |
| `closeOnEscape` | Делает Esc закрытие панели. Передайте `true` или оставьте поле, потому что Claude Code отказывает `false`. |
| `holdToasts` | Удерживает всплывающие уведомления, небольшие уведомления от [`$.ui.toast`](/docs/ru/plugins/mods/api#show-something-without-starting-a-turn), пока панель не закроется |
| `rows` | Высота, которую нужно запросить, когда панель находится над приглашением. По умолчанию треть пространства. |
| `columns` | Ширина, которую нужно запросить, когда панель находится рядом с расшифровкой |

Чтобы позволить команде открыть панель, пока Claude работает, добавьте `immediate: true` при [регистрации команды](/docs/ru/plugins/mods/api#add-a-command). Без этого команда, введенная во время хода, ждет конца хода.

<h4 id="when-a-pane-waits-for-a-wider-terminal">
  Когда панель ждет более широкого терминала
</h4>

Панель, которую ваш мод открывает без запроса, не появляется в узком терминале, поэтому она не может захватить маленький экран. Появляется ли она, зависит от того, что ее открыло:

* **Открыто чем-то, что сделал пользователь**, например командой, которую он запустил, или кнопкой, которую он нажал, панель появляется при любой ширине
* **Открыто вашим модом, действующим самостоятельно**, например из таймера или хука [`turn.start`](/docs/ru/plugins/mods/events#follow-a-turn), панель появляется только в терминале шириной не менее 144 столбцов. После того, как пользователь открыл эту панель один раз сам, достаточно 110 столбцов.

Когда панель появляется, `$.ui.open` разрешается в `{ isPlaced: true }`. Когда панель ждет, `isPlaced` — это `false` и `reason` — это строка, которая говорит почему. Ожидающая панель появляется, когда пользователь ее открывает или расширяет терминал. Чтобы сказать, что что-то доступно без открытия панели, вызовите `$.ui.toast('Your message')`, которая показывает небольшое уведомление, которое исчезает через несколько секунд.

<h2 id="build-a-tree-from-elements">
  Построение дерева из элементов
</h2>

То, что возвращает хук `ui.render`, — это дерево элементов: описание того, что рисовать, состоящее из боксов, текста и элементов управления, вложенных друг в друга. Вы описываете рисование, и Claude Code рисует его в терминале или приложении Desktop.

Чтобы получить элементы, вызовите `$.ui.resolve(e)` в вашем хуке, как в `const { Box, Text, Button } = $.ui.resolve(e)`. Каждый элемент — это функция. Вы передаете ей свойства, и вы помещаете элементы и строки, которые идут внутри него, в `children`.

Большинство рисунков используют четыре элемента. Выберите вкладку, чтобы увидеть каждый из них и как терминал его рисует:

<Tabs>
  <Tab title="Text">
    `Text` рисует строку с необязательным стилем, таким как `bold` и `color`:

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

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

  <Tab title="Box">
    `Box` расставляет то, что находится внутри него, в ряд или столбец. Этот помещает кнопку и строку текста рядом, два столбца отдельно:

    ```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` — это элемент управления, который пользователь может нажать. Он запускает ваш обратный вызов `onPress`. С `plain: true` он не имеет скобок и показывает его горячую клавишу:

    ```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` — это текстовое поле. Он запускает ваш обратный вызов `onSubmit` с текстом, когда пользователь нажимает 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>

Эта таблица перечисляет каждый элемент:

| Элемент | Что он рисует | Где |
| :- | :- | :- |
| `Box` | Контейнер flex. Принимает свойства макета, такие как `flexDirection`, `columnGap`, `padding`, `borderStyle` и `width`. | Везде |
| `Text` | Стилизованный текст. Принимает `color`, `bold`, `dimColor`, `italic` и `wrap`. `color` — это ключ темы или цвет, такой как `'red'`. `wrap` — это `'wrap'`, `'truncate'`, `'truncate-start'`, `'truncate-middle'` или `'truncate-end'`. | Везде |
| `Button` | Элемент управления, который вызывает `onPress` | Везде |
| `Link`, `Code`, `Markdown` | Ссылка с `href` и необязательной `label`, блок кода и текст, отформатированный так, как ответы Claude. `Markdown` принимает свое содержимое в свойстве `text`, а не в `children`, и нуждается в `key`, когда вы передаете `onLinkPress`. | Везде |
| `Input`, `Select` | Текстовое поле и средство выбора | Терминал, Desktop |
| `Svg` | Документ SVG | Desktop |
| `Client` | Область, нарисованная вторым файлом вашего, для анимации и ввода указателя. Этот файл не получает API модов. Он достигает ваших хуков только путем отправки данных, которые поступают как событие `ui.message`. | Терминал, Desktop |
| `Raster`, `Image` | [Сетка цветных ячеек](#draw-a-grid-of-colored-cells) и изображение | Терминал |

Если ваш модуль — это файл `.tsx` или `.jsx`, вы можете написать дерево как JSX. Сначала деструктурируйте элементы из `$.ui.resolve(e)`, потому что модуль хуков не имеет глобальных элементов.

Если дерево использует элемент, который приложение не имеет, свойство, которое элемент не принимает, или дочерний элемент, где его нет, Claude Code рисует свою собственную версию сайта.

В сеансе, запущенном с `--plugin-dir`, строка расшифровки говорит об этом, например `ui.render (Pane) refused: Text prop "bogusProp" is not allowed; the engine drew its own`. [Журнал отладки](/docs/ru/plugins/mods/troubleshoot#read-the-debug-log) записывает это как `ui.render (Pane): a hook returned a tree that does not validate` с той же причиной. Ничего больше не появляется в сеансе, поэтому когда рисование не показывается, проверьте эту строку или журнал.

<h3 id="draw-a-grid-of-colored-cells">
  Рисование сетки цветных ячеек
</h3>

Для тепловой карты, спарклайна или игровой доски в терминале нарисуйте один `Raster`, а не `Box` для каждой ячейки. `Raster` принимает `key`, его размер в `columns` и `rows`, и `cells`, который упаковывает каждую ячейку в одну строку. Каждая ячейка — это три числа: кодовая точка символа, его цвет и цвет фона. Цвет — это шестнадцатеричное число с двумя цифрами каждого для красного, зеленого и синего, например `0xc62828` для красного или `0x01000000` для терминала по умолчанию.

Приложение Desktop не имеет `Raster`, поэтому проверьте `e.surface` и нарисуйте текст там. Это тело панели рисует тепловую карту три на два:

```javascript theme={null}
// The value that means "use the terminal's default color"
const DEFAULT_COLOR = 0x01000000

// Pack rows of [character, color] pairs into the one string a Raster takes
// One cell is three numbers: the character's code point, its color, and its background
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) => {
  // Draw only in the pane opened with the id 'heat'
  if (e.requestId !== 'heat') return next(e)
  const { Box, Text, Raster } = $.ui.resolve(e)
  // Two rows of three cells, each a block character and its color
  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) })],
  })
})
```

В терминале панель показывает сетку:

<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="A pane in the terminal that holds a small grid of colored blocks, two rows of three. The top row is green, amber, and red. The bottom row is green, green, and amber." width="360" height="132" data-path="images/mods-heat-map.svg" />

Массив `rows` — это часть, которую вы бы изменили, и `cellsOf` превращает его в упакованную строку. Хук рисует только в панели, чей `id` — это `heat`, поэтому откройте один с `$.ui.open({ id: 'heat' })` из команды, как пример [`hello-tabs`](#build-a-pane-with-tabs) открывает свою панель.

Каждый символ должен быть шириной в одну ячейку. Чтобы анимировать `Raster`, который уже на экране, вызовите `$.ui.blit` с `id` панели как `requestId`, `key` `Raster`, тем же размером и новыми ячейками. Для этого примера это `$.ui.blit({ requestId: 'heat', key: 'grid', columns: 3, rows: 2, cells: cellsOf(newRows) })`. Он перерисовывает только этот элемент без повторного запуска вашего хука `ui.render`.

<h2 id="respond-to-presses-and-typing">
  Ответ на нажатия и ввод
</h2>

Когда пользователь нажимает кнопку, вводит текст в поле или выбирает из списка, который нарисовал ваш мод, Claude Code вызывает функцию, которую вы дали этому элементу управления, и она запускается в вашем модуле. Каждый элемент управления принимает свои собственные обратные вызовы:

* **`Button`**: принимает `onPress(e)`, где `e.surface` — это приложение, из которого пришло нажатие
* **`Input`**: принимает `onSubmit(value)` и `onInput(value)`
* **`Select`**: принимает `onSelect(value)` с его выборами в `options`, список по крайней мере одного выбора с уникальными значениями, такой как `[{ value: 'sm', label: 'Small' }, { value: 'lg', label: 'Large' }]`

Тест нажимает или вводит текст в элемент управления по его `key`, поэтому дайте каждому элементу управления один. Каждое использование элемента управления также запускает [`ui.press`, `ui.input` или `ui.select`](/docs/ru/plugins/mods/reference#interface) с `key` в `e.element`, и другой мод может подключить эти события. Его хук запускается перед вашим обратным вызовом, поэтому он видит, что пользователь вводит в ваш `Input`, и может изменить это или ответить вместо вашего обратного вызова. API модов не имеет метода, который нажимает кнопку другого мода.

<h3 id="know-which-keys-your-mod-can-receive">
  Фокус клавиатуры и горячие клавиши
</h3>

Ваш мод никогда не читает клавиатуру сам. Пользователь нажимает клавишу, Claude Code решает, для какого из ваших элементов управления она предназначена, и запускается обратный вызов этого элемента управления. Кроме [горячей клавиши цифры на полосе](/docs/ru/plugins/mods/reference#elements), это происходит только пока ваша панель или полоса имеет фокус клавиатуры. Остальное время клавиши идут к приглашению.

<h4 id="how-a-pane-gets-keyboard-focus">
  Как панель получает фокус клавиатуры
</h4>

Панель получает фокус клавиатуры одним из трех способов:

* Ваш мод открывает его с `focus: true` из команды или нажатия
* Пользователь нажимает Ctrl+X, затем Tab
* Пользователь нажимает на него

Claude Code предоставляет `focus: true` только пока приглашение пусто и ничто другое не имеет фокус клавиатуры. Панель, которая открывается, пока пользователь печатает, не берет его нажатия клавиш.

<h4 id="what-each-key-does">
  Что делает каждая клавиша
</h4>

Эта таблица перечисляет, что делает клавиша, пока ваша панель или полоса имеет фокус клавиатуры:

| Клавиша | Что она делает |
| :- | :- |
| Tab | Переходит к следующему элементу управления |
| Up и Down | Перемещаются между элементами управления, пока ваше рисование подходит. Когда панель или полоса имеет больше строк, чем может показать, они прокручивают ее. |
| Enter | Нажимает сфокусированный `Button`, отправляет сфокусированный `Input` или выбирает в `Select` |
| Горячая клавиша кнопки | Нажимает эту кнопку. Пока `Input` имеет фокус, каждая печатаемая клавиша идет в поле. |
| Esc | Возвращает фокус клавиатуры к приглашению. С `closeOnEscape: true`, он также закрывает панель. |

Мод не может привязать Tab или клавиши со стрелками к чему-либо еще, поэтому игра управляется с помощью `w`, `a`, `s` и `d`.

<h4 id="set-a-hotkey-and-the-first-focus">
  Установка горячей клавиши и первого фокуса
</h4>

Два свойства элемента управления решают, как клавиатура его достигает:

* **`hotkey`**: чтобы позволить пользователю нажать `Button` одной клавишей, дайте ему `hotkey` одной цифры или одной строчной буквы, как в `hotkey: 'a'`
* **`autoFocus`**: чтобы выбрать, какой элемент управления имеет фокус при открытии панели, добавьте `autoFocus: true` к нему. Оставьте свойство на других, потому что Claude Code отказывает `autoFocus: false`.

То, как горячая клавиша показывается, зависит от кнопки и приложения:

| Кнопка | В терминале | В приложении Desktop |
| :- | :- | :- |
| С скобками, по умолчанию | `[ Add one ]`, без показанной горячей клавиши | Метка с маленькой клавишей рядом |
| С `plain: true` | `1: One` | Метка с маленькой клавишей рядом |

В терминале назовите клавишу в метке кнопки в скобках или используйте `plain: true`, чтобы пользователь мог видеть, что нажать. [Справка элементов](/docs/ru/plugins/mods/reference#elements) имеет другие правила `Button`: `action`, горячие клавиши цифр на полосе и две кнопки на одной горячей клавише.

<h3 id="take-typed-input-and-draw-a-row-for-each-item">
  Получение введенного текста и рисование строки для каждого элемента
</h3>

Многие панели — это текстовое поле со списком под ним. Пример в этом разделе — панель заметок: вы вводите заметку и нажимаете Enter, чтобы добавить ее, и каждая заметка имеет кнопку `x`, которая удаляет ее. С двумя добавленными заметками терминал рисует панель таким образом:

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

Пример использует две техники:

* **Получение введенного текста**: `Input` вызывает `onSubmit(value)` с текстом поля, когда пользователь нажимает Enter, и `onInput(value)` при каждом изменении
* **Рисование списка**: отобразите ваши данные в одну строку каждый, и дайте каждой кнопке строки свой собственный `key`

Этот хук рисует содержимое панели:

```javascript theme={null}
// The list the pane draws
let notes = []

on('ui.render', { component: 'Pane' }, async ($, e, next) => {
  // Draw only in the pane opened with the 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',
        // Draw the field empty each time, which clears it after a submit
        value: '',
        submitLabel: 'add',
        autoFocus: true,
        // Runs when you press Enter in the field
        onSubmit: async (value) => {
          // Ignore an empty line
          if (!value.trim()) return
          notes = [...notes, value.trim()]
          redraw()
          await $.store.set('notes', notes)
        },
      }),
      // One row for each note: a delete button, then the note's text
      ...notes.map((note, i) =>
        Box({
          flexDirection: 'row',
          columnGap: 1,
          children: [
            Button({
              // A key of its own, so each row's button can be told apart
              key: 'delete-' + i,
              label: 'x',
              plain: true,
              onPress: async () => {
                notes = notes.filter((_, j) => j !== i)
                redraw()
                await $.store.set('notes', notes)
              },
            }),
            Text({ children: [note] }),
          ],
        }),
      ),
    ],
  })
})
```

Чтобы попробовать панель:

* **Добавить заметку**: введите строку и нажмите Enter. Строка появляется как новая строка, и поле очищается.
* **Удалить заметку**: нажимайте Tab, пока кнопка `x` заметки не получит фокус, затем нажмите Enter. `x` — это метка кнопки, а не горячая клавиша, поэтому ввод буквы не нажимает ее.

Каждое изменение следует тому же циклу рендеринга, что и `hello-tabs`: обратный вызов изменяет `notes`, вызывает `redraw` и сохраняет список в `$.store`.

Поле очищается после каждой отправки из-за его свойства `value`. `value` — это текст, который поле содержит при его рисовании, и ввод пользователя заменяет его до тех пор, пока ваш хук не нарисует поле снова. Пример всегда рисует поле с `''`.

Пример сохраняет заметки и не загружает их. Чтобы вернуть их в следующем сеансе, прочитайте их в хуке `session.start`, как `hello-tabs` читает `count`.

Три свойства составляют строку поля, `Note: Type a note and press Enter ⏎ add`:

| Свойство | В примере | Что это такое |
| :- | :- | :- |
| `label` | `Note` | Текст перед полем. Терминал рисует `: ` после него. |
| `placeholder` | `Type a note and press Enter` | Тусклый текст, который показывается, пока поле пусто |
| `submitLabel` | `add` | Слово после `⏎`, которое говорит, что делает Enter |

Отправка `Input` не запускает ход, если ваш обратный вызов не вызывает [`$.prompt.submit`](/docs/ru/plugins/mods/api#start-a-turn-from-a-background-job).

<h2 id="redraw-when-something-changes">
  Перерисовка сайта
</h2>

Рисование — это снимок: оно показывает то, что ваш хук `ui.render` вернул в последний раз, когда хук запустился. Чтобы показать что-то новое, хук должен запуститься снова. Claude Code запускает его снова для некоторых изменений, и ваш мод просит остальное.

<h3 id="when-claude-code-redraws-without-being-asked">
  Когда Claude Code перерисовывает без запроса
</h3>

Claude Code запускает ваш хук `ui.render` снова, когда свойства сайта изменяются или ширина терминала изменяется. Он не запускает хук на таймере и не может сказать, когда переменная в вашем модуле изменяется.

<h3 id="redraw-when-your-data-changes">
  Перерисовка при изменении ваших данных
</h3>

Чтобы ваши сайты были нарисованы снова после изменения ваших собственных данных, вызовите `$.ui.invalidate('ui.render')`. Эта панель считает нажатия. Обратный вызов кнопки изменяет `count`, затем просит перерисовку:

```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
          // The data changed, so ask Claude Code to draw the pane again
          $.ui.invalidate('ui.render')
        },
      }),
      Text({ children: ['Count: ' + count] }),
    ],
  })
})
```

Каждое нажатие поднимает число в панели. Пример [`hello-tabs`](#build-a-pane-with-tabs) оборачивает тот же вызов в свою функцию `redraw`.

Значение, которое вы сохраняете в [`$.state`](#keep-a-value-in-\$-state), не нуждается в вызове, потому что написание значения перерисовывает сайты, которые его читают.

<h3 id="redraw-on-a-timer">
  Перерисовка на таймере
</h3>

Чтобы сохранить часы, обратный отсчет или значение извне сеанса в актуальном состоянии, перерисовывайте по расписанию. Запустите таймер в хуке `session.start` модуля. Если модуль уже имеет один, как `hello-tabs`, добавьте строку [`$.clock.every`](/docs/ru/plugins/mods/api#run-work-in-the-background) к нему:

```javascript theme={null}
on('session.start', async ($, e, next) => {
  // Every 1000 milliseconds, ask Claude Code to draw your sites again
  $.clock.every(1000, () => $.ui.invalidate('ui.render'))
  return next(e)
})
```

Claude Code теперь запускает ваш хук `ui.render` один раз в секунду. Таймер останавливается при перезагрузке модуля, и новая копия модуля запускает свой собственный.

<h3 id="how-often-a-site-can-redraw">
  Как часто сайт может перерисовываться
</h3>

Claude Code ограничивает, как часто он перерисовывает сайт, поэтому ваш мод может вызывать `$.ui.invalidate` так часто, как его данные изменяются. Видимая панель и полоса имеют более высокий лимит, чем другие сайты, и [таблица лимитов](/docs/ru/plugins/mods/reference#limits) содержит цифры.

Вызовы, которые приходят быстрее, чем лимит, объединяются в одну перерисовку. Эта перерисовка запускает ваш хук один раз, и хук читает ваши данные такими, какие они есть в этот момент, поэтому показывается последнее значение и значения между ними не показываются. Анимация не может работать быстрее, чем лимит.

<h2 id="keep-state">
  Сохранение состояния
</h2>

Мод имеет три места для сохранения значения, и они отличаются тем, как долго значение длится: пока модуль не перезагрузится, пока сеанс не закончится или от одного сеанса к другому. Выбирайте по тому, как долго значение должно длиться:

| Сохраняйте это в | Это длится до | Используйте это для |
| :- | :- | :- |
| Переменная уровня модуля | Модуль перезагружается, что происходит каждый раз, когда вы сохраняете файл во время разработки | Значения, которые вы можете потерять, как `tab` в `hello-tabs` |
| `$.state` | Сеанс заканчивается, или пользователь запускает `/clear`, `/resume` или `/branch` | Значения, от которых зависит рисование, которые должны пережить перезагрузку |
| `$.store` | Ваш мод удаляет его, или ни один сеанс не читает и не пишет в хранилище в течение [`cleanupPeriodDays`](/docs/ru/settings-reference#cleanupperioddays). Хранилище — это хранилище ключ-значение, сохраненное как файл JSON вашего плагина под `~/.claude/plugins/store/`. | Настройки, история, все, что пользователь ожидает найти в следующий раз |

`$.store.get(key)` разрешается в значение или `undefined`, и `$.store.set(key, value)` принимает любое значение JSON.

<h3 id="keep-a-value-in-state">
  Сохранение значения в `$.state`
</h3>

`$.state` содержит значения на протяжении сеанса, и он перерисовывает для вас. Это реактивное состояние: хук `ui.render`, который читает значение, подписывается на него, поэтому Claude Code перерисовывает этот сайт каждый раз, когда вы пишете значение, и вам не нужно вызывать `$.ui.invalidate`. Значение в `$.state` также пережит перезагрузку модуля, которую переменная не пережит.

Чтобы установить его, объявите ваши значения, укажите ваш манифест на объявление, затем определите и используйте каждое значение. Примеры перемещают `count` из `hello-tabs` в `$.state`.

<h4 id="declare-the-values">
  Объявление значений
</h4>

Объявите значения в файле типов. Внешний ключ — это имя вашего плагина, и каждая запись под ним — это значение и его тип. Сохраните это как `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">
  Указание манифеста на объявление
</h4>

Чтобы позволить `claude plugin validate` проверить ваш код против этого файла, добавьте поле `types` в манифест с его путем:

```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">
  Определение, чтение и запись значения
</h4>

В вашем модуле определите каждое значение с по умолчанию, прочитайте его при рисовании и напишите его из обратного вызова. `atom` называет значение и его по умолчанию, `read` возвращает его, и `update` пишет его. Три помощника вызывают `$.state.get` и `$.state.set` для вас:

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

// At the top of the module: name the value and give its default
const count = atom({ plugin: 'hello-tabs', key: 'count' }, 0)

// In the ui.render hook: read the value to draw it
const n = await read($, count)

// In a Button: write a new value from the old one
onPress: () => update($, count, (value) => value + 1)
```

Потому что хук `ui.render` прочитал `count`, Claude Code запускает хук снова каждый раз, когда кнопка пишет его.

Три правила применяются к коду:

* **Напишите `plugin` и `key` как буквальные строки**: `claude plugin validate` читает их из вашего источника
* **Объявите каждое значение в файле типов**: в противном случае валидация не пройдет с `hello-tabs.count is not declared`
* **Напишите из обратного вызова или хука другого события**: хук `ui.render` может читать состояние и не может писать его, поэтому пишите из `onPress`, `onSubmit` или хука для другого события

<h4 id="change-hello-tabs-to-use-state">
  Изменение `hello-tabs` для использования `$.state`
</h4>

Чтобы переместить `count` в `hello-tabs` в `$.state`, измените каждую строку, которая его использует:

* **В верхней части модуля**: добавьте строку `import` и замените `let count = 0` на строку `atom`
* **В хуке `ui.render`**: добавьте строку `read` перед `tabButton` и нарисуйте `'Count: ' + n` в `Text`
* **В кнопке Add one**: замените `onPress` на тот, что в [Сохранение из более чем одного сеанса](#save-from-more-than-one-session), который сохраняет счет, а также пишет его
* **В хуке `session.start`**: замените две строки, которые читают `saved`, на вызов `loadCount` из [Загрузка сохраненного значения снова после `/clear`](#load-a-saved-value-again-after-clear)

Сохраняйте `redraw` для кнопок вкладок, потому что `tab` все еще переменная.

<h3 id="load-a-saved-value-again-after-clear">
  Загрузка сохраненного значения снова после `/clear`
</h3>

Если ваш мод копирует сохраненное значение из `$.store` в `$.state` при `session.start`, он должен скопировать его снова после `/clear`, `/resume` или `/branch`. Эти команды возвращают каждое значение `$.state` к его по умолчанию, и `session.start` не запускается снова. [`classic.SessionStart`](/docs/ru/plugins/mods/events#hook-the-settings-hook-events) запускается после каждого из них, с `e.source`, установленным на `clear`, `resume` или `fork`, поэтому скопируйте значение снова в хук на нем. В противном случае ваше рисование показывает по умолчанию, и обратный вызов, который сохраняет значение `$.state`, пишет по умолчанию над тем, что вы сохранили.

Этот код загружает `count` из обоих хуков. Он строится на версии `$.state` `hello-tabs`, где `count` — это атом и `update` импортируется. Поместите `loadCount` выше `register` и добавьте вызов `loadCount` к хуку `session.start`, который у вас уже есть. `classic.SessionStart` также запускается при запуске и после компактирования, которое не сбрасывает `$.state`, поэтому фильтр на `source` сохраняет хук к трем сбросам:

```javascript theme={null}
// Copy the saved count from $.store into $.state, or 0 if nothing is saved
async function loadCount($) {
  const saved = Number((await $.store.get('count')) ?? 0)
  await update($, count, () => saved)
}

// Runs before your first prompt, and again after a reload
on('session.start', async ($, e, next) => {
  await loadCount($)
  return next(e)
})

// Runs again after /clear, /resume, and /branch, which reports fork
on('classic.SessionStart', { source: ['clear', 'resume', 'fork'] }, async ($, e, next) => {
  await loadCount($)
  return next(e)
})
```

С обоими хуками на месте, панель показывает сохраненный счет после `/clear` и не `0`, и следующее нажатие **Add one** добавляет к сохраненному счету.

`loadCount` пишет сохраненное значение над тем, что в `$.state`, и `session.start` запускается снова каждый раз, когда модуль перезагружается. Чтобы хранилище не отставало, сохраняйте при каждом изменении, как кнопка **Add one** делает.

Чтобы проверить перезагрузку без сеанса, [протестируйте рисование после `/clear`](/docs/ru/plugins/mods/test#test-a-drawing-after-clear).

<h3 id="save-from-more-than-one-session">
  Сохранение из более чем одного сеанса
</h3>

Каждый сеанс на вашей машине, который запускает ваш мод, делит одно `$.store`. `get`, за которым следует `set`, не является атомарным. Когда два сеанса каждый читают значение, изменяют его и пишут его обратно, они гонятся, и второе написание заменяет первое.

Два выбора делают это менее вероятным:

* **Дайте каждому элементу свой собственный ключ**: `set` изменяет только свой собственный ключ, поэтому сеансы, которые пишут разные ключи, не перезаписывают друг друга
* **Прочитайте снова прямо перед тем, как вы напишете**: для значения, которое несколько сеансов изменяют, `get` ключ в обратном вызове и постройте новое значение из этого, а не из копии, которую вы загрузили при `session.start`. Написание другого сеанса все еще теряется, если оно приземляется между вашим `get` и вашим `set`.

Эта кнопка добавляет один к тому, что хранилище содержит сейчас, затем обновляет рисование:

```javascript theme={null}
onPress: async () => {
  // Read what the store holds now, which another session may have changed
  const saved = Number((await $.store.get('count')) ?? 0)
  // Save the new count, then show it
  await $.store.set('count', saved + 1)
  await update($, count, () => saved + 1)
}
```

Если второй сеанс нажал свою собственную кнопку три раза с тех пор, как этот сеанс начался, это нажатие показывает и сохраняет счет, который включает эти три.

<h2 id="next-steps">
  Следующие шаги
</h2>

* [Реагирование на события](/docs/ru/plugins/mods/events): питайте ваше рисование из вызовов инструментов и ходов
* [Использование API модов](/docs/ru/plugins/mods/api): питайте ваше рисование из таймеров и вызовов модели
* [Тестирование рисования](/docs/ru/plugins/mods/test#test-a-drawing): нажимайте ваши кнопки из теста, на более чем одной поверхности
* [Сайты рендеринга](/docs/ru/plugins/mods/reference#render-sites) и [элементы](/docs/ru/plugins/mods/reference#elements): свойства каждого сайта и свойства каждого элемента
