Skip to main content
Мод может рисовать свой собственный интерфейс в Claude Code и изменять части интерфейса, которые уже рисует Claude Code. Каждое место, где мод может рисовать, называется сайтом рендеринга, например панель, полоса над приглашением или спиннер. Claude Code вызывает событие ui.render каждый раз, когда собирается рисовать сайт рендеринга, и ваш хук для этого события возвращает то, что нужно рисовать там. На этой карте показано, где мод может рисовать в сеансе терминала: 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. 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. В более узком терминале панель находится над приглашением вместо того, чтобы находиться рядом с расшифровкой. Создайте свой первый мод перед тем, как начать здесь. Начните с рабочего примера, который создает панель с двумя вкладками и счетчиком, затем прочитайте раздел для каждой части, которую вы хотите изменить.
Чтобы найти одно свойство или ограничение, см. справку.

Создание панели с вкладками

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

Создание плагина

Мод — это плагин с манифестом, hooks.json, который указывает на ваш код, и файл кода. Создание мода объясняет каждый из них. Создайте каталог с именем hello-tabs с каталогами .claude-plugin и hooks внутри него, затем сохраните первые два файла.Сохраните манифест как hello-tabs/.claude-plugin/plugin.json:
hello-tabs/.claude-plugin/plugin.json
Назовите точку входа в hello-tabs/hooks/hooks.json:
hello-tabs/hooks/hooks.json
2

Написание кода

Код выполняет три задачи, по одной в каждом хуке:
  • Добавляет команду /hello-tabs
  • Открывает панель при запуске этой команды
  • Рисует содержимое панели: ряд вкладок и тело открытой вкладки
Две переменные уровня модуля, tab и count, содержат состояние панели.Сохраните это как hello-tabs/hooks/register.js:
hello-tabs/hooks/register.js
Каждый хук также делает что-то, что код не делает явным:
  • session.start также читает сохраненный счет из $.store, хранилища ключ-значение, которое сохраняется между сеансами.
  • command.run только сообщает Claude Code, что панель существует. Открытие панели ничего не рисует само по себе: Claude Code затем вызывает ui.render, чтобы спросить, что в ней находится.
  • ui.render возвращает дерево элементов, Box, который содержит другие боксы, текст и кнопки, и строит его снова из tab и count каждый раз, когда он запускается.
Нажатие кнопки запускает ее обратный вызов onPress, который изменяет переменную и вызывает redraw. Claude Code затем запускает хук ui.render снова, и хук строит новое дерево из новых значений. Каждое интерактивное рисование использует этот цикл рендеринга: обратный вызов изменяет состояние, и хук рисует снова из нового состояния.
3

Открытие панели

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

Проверка того, что счет был сохранен

Нажмите Esc, чтобы закрыть панель, затем выйдите из сеанса. В вашей оболочке запустите Claude Code снова с той же командой claude --plugin-dir ./hello-tabs, и в приглашении Claude Code запустите /hello-tabs. Счет находится там, где вы его оставили.Чтобы очистить счет, попросите мод вызвать $.store.delete('count'). Сохранение состояния охватывает, как долго длится каждый вид значения.

Выбор места для рисования

Хук ui.render запускается для каждого сайта рендеринга, если вы не сузите его до того, который вы хотите рисовать. Чтобы выбрать сайт рендеринга, передайте фильтр, называемый matcher, в качестве второго аргумента для on. { component: 'Pane' } запускает хук только для панелей. В хуке e.component называет сайт, e.surface говорит, какое приложение рисует, и e.props содержит собственные данные сайта. Для панели e.requestId — это id, с которым вы ее открыли. Два сайта пусты, пока мод их не заполнит, панель и полоса. Выберите вкладку, чтобы увидеть, что это такое и как рисовать в них:
Панель — это боковая панель рядом с расшифровкой в широком полноэкранном терминале или обрамленная область над приглашением в противном случае. При открытии нескольких панелей каждая получает вкладку, которая показывает ее название.Панель появляется, когда ваш мод вызывает $.ui.open с id, который вы выбираете, как в $.ui.open({ id: 'hello-tabs' }). Открытие панели в нужное время охватывает другие поля и когда панель ждет более широкого терминала.Чтобы рисовать в вашей панели, отфильтруйте по { component: 'Pane' } и проверьте, что e.requestId — это ваш id.

Изменение того, что уже рисует Claude Code

Claude Code рисует большую часть своего интерфейса сам: сообщения, строки вызовов инструментов, спиннер и многое другое. Каждая из этих частей также является сайтом рендеринга, поэтому мод может переделать стиль или заменить его. Чтобы изменить один, отфильтруйте ваш хук ui.render по его имени из этой таблицы: На сайте, который Claude Code уже рисует, ваш хук имеет три варианта: изменить деталь, заменить рисование или оставить его в покое. Выберите вкладку, чтобы увидеть каждый из них, применяемый к спиннеру. Примеры читают переменную calls, которую другой хук считает, как в учебном моде.
Чтобы сохранить рисование Claude Code и изменить одну его часть, передайте next копию события с измененными props. Этот хук изменяет текст после слова спиннера:
Спиннер сохраняет свою анимацию и свое слово, и ваш текст следует за словом:
Приглашение разрешения не является сайтом рендеринга, поэтому мод не может изменить то, что оно показывает. Диалог вопроса, AskUserQuestion, является одним, поэтому мод может изменить это. Терминал и приложение Desktop не вызывают все одни и те же сайты. Pane, AbovePrompt, Spinner и сайты расшифровки работают в обоих. Несколько других строк состояния вызываются только в терминале. Таблица сайтов рендеринга указывает, где каждый из них вызывается.

Открытие панели в нужное время

Панель появляется только когда ваш мод ее открывает. То, как и когда вы ее открываете, определяет, получает ли она фокус клавиатуры, сколько места она запрашивает и появляется ли она вообще в узком терминале. Чтобы открыть панель, вызовите $.ui.open с id, который вы выбираете. id — это имя панели: ваш хук ui.render проверяет его, и вы передаете его снова, чтобы закрыть панель.
Чтобы закрыть панель, вызовите $.ui.close с id, с которым вы ее открыли:
Помимо id, $.ui.open принимает эти необязательные поля: Чтобы позволить команде открыть панель, пока Claude работает, добавьте immediate: true при регистрации команды. Без этого команда, введенная во время хода, ждет конца хода.

Когда панель ждет более широкого терминала

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

Построение дерева из элементов

То, что возвращает хук ui.render, — это дерево элементов: описание того, что рисовать, состоящее из боксов, текста и элементов управления, вложенных друг в друга. Вы описываете рисование, и Claude Code рисует его в терминале или приложении Desktop. Чтобы получить элементы, вызовите $.ui.resolve(e) в вашем хуке, как в const { Box, Text, Button } = $.ui.resolve(e). Каждый элемент — это функция. Вы передаете ей свойства, и вы помещаете элементы и строки, которые идут внутри него, в children. Большинство рисунков используют четыре элемента. Выберите вкладку, чтобы увидеть каждый из них и как терминал его рисует:
Text рисует строку с необязательным стилем, таким как bold и color:
Эта таблица перечисляет каждый элемент: Если ваш модуль — это файл .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. Журнал отладки записывает это как ui.render (Pane): a hook returned a tree that does not validate с той же причиной. Ничего больше не появляется в сеансе, поэтому когда рисование не показывается, проверьте эту строку или журнал.

Рисование сетки цветных ячеек

Для тепловой карты, спарклайна или игровой доски в терминале нарисуйте один Raster, а не Box для каждой ячейки. Raster принимает key, его размер в columns и rows, и cells, который упаковывает каждую ячейку в одну строку. Каждая ячейка — это три числа: кодовая точка символа, его цвет и цвет фона. Цвет — это шестнадцатеричное число с двумя цифрами каждого для красного, зеленого и синего, например 0xc62828 для красного или 0x01000000 для терминала по умолчанию. Приложение Desktop не имеет Raster, поэтому проверьте e.surface и нарисуйте текст там. Это тело панели рисует тепловую карту три на два:
В терминале панель показывает сетку: 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. Массив rows — это часть, которую вы бы изменили, и cellsOf превращает его в упакованную строку. Хук рисует только в панели, чей id — это heat, поэтому откройте один с $.ui.open({ id: 'heat' }) из команды, как пример hello-tabs открывает свою панель. Каждый символ должен быть шириной в одну ячейку. Чтобы анимировать Raster, который уже на экране, вызовите $.ui.blit с id панели как requestId, key Raster, тем же размером и новыми ячейками. Для этого примера это $.ui.blit({ requestId: 'heat', key: 'grid', columns: 3, rows: 2, cells: cellsOf(newRows) }). Он перерисовывает только этот элемент без повторного запуска вашего хука ui.render.

Ответ на нажатия и ввод

Когда пользователь нажимает кнопку, вводит текст в поле или выбирает из списка, который нарисовал ваш мод, 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 с key в e.element, и другой мод может подключить эти события. Его хук запускается перед вашим обратным вызовом, поэтому он видит, что пользователь вводит в ваш Input, и может изменить это или ответить вместо вашего обратного вызова. API модов не имеет метода, который нажимает кнопку другого мода.

Фокус клавиатуры и горячие клавиши

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

Как панель получает фокус клавиатуры

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

Что делает каждая клавиша

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

Установка горячей клавиши и первого фокуса

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

Получение введенного текста и рисование строки для каждого элемента

Многие панели — это текстовое поле со списком под ним. Пример в этом разделе — панель заметок: вы вводите заметку и нажимаете Enter, чтобы добавить ее, и каждая заметка имеет кнопку x, которая удаляет ее. С двумя добавленными заметками терминал рисует панель таким образом:
Пример использует две техники:
  • Получение введенного текста: Input вызывает onSubmit(value) с текстом поля, когда пользователь нажимает Enter, и onInput(value) при каждом изменении
  • Рисование списка: отобразите ваши данные в одну строку каждый, и дайте каждой кнопке строки свой собственный key
Этот хук рисует содержимое панели:
Чтобы попробовать панель:
  • Добавить заметку: введите строку и нажмите 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: Отправка Input не запускает ход, если ваш обратный вызов не вызывает $.prompt.submit.

Перерисовка сайта

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

Когда Claude Code перерисовывает без запроса

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

Перерисовка при изменении ваших данных

Чтобы ваши сайты были нарисованы снова после изменения ваших собственных данных, вызовите $.ui.invalidate('ui.render'). Эта панель считает нажатия. Обратный вызов кнопки изменяет count, затем просит перерисовку:
Каждое нажатие поднимает число в панели. Пример hello-tabs оборачивает тот же вызов в свою функцию redraw. Значение, которое вы сохраняете в $.state, не нуждается в вызове, потому что написание значения перерисовывает сайты, которые его читают.

Перерисовка на таймере

Чтобы сохранить часы, обратный отсчет или значение извне сеанса в актуальном состоянии, перерисовывайте по расписанию. Запустите таймер в хуке session.start модуля. Если модуль уже имеет один, как hello-tabs, добавьте строку $.clock.every к нему:
Claude Code теперь запускает ваш хук ui.render один раз в секунду. Таймер останавливается при перезагрузке модуля, и новая копия модуля запускает свой собственный.

Как часто сайт может перерисовываться

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

Сохранение состояния

Мод имеет три места для сохранения значения, и они отличаются тем, как долго значение длится: пока модуль не перезагрузится, пока сеанс не закончится или от одного сеанса к другому. Выбирайте по тому, как долго значение должно длиться: $.store.get(key) разрешается в значение или undefined, и $.store.set(key, value) принимает любое значение JSON.

Сохранение значения в $.state

$.state содержит значения на протяжении сеанса, и он перерисовывает для вас. Это реактивное состояние: хук ui.render, который читает значение, подписывается на него, поэтому Claude Code перерисовывает этот сайт каждый раз, когда вы пишете значение, и вам не нужно вызывать $.ui.invalidate. Значение в $.state также пережит перезагрузку модуля, которую переменная не пережит. Чтобы установить его, объявите ваши значения, укажите ваш манифест на объявление, затем определите и используйте каждое значение. Примеры перемещают count из hello-tabs в $.state.

Объявление значений

Объявите значения в файле типов. Внешний ключ — это имя вашего плагина, и каждая запись под ним — это значение и его тип. Сохраните это как hello-tabs/types/index.d.ts:
hello-tabs/types/index.d.ts

Указание манифеста на объявление

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

Определение, чтение и запись значения

В вашем модуле определите каждое значение с по умолчанию, прочитайте его при рисовании и напишите его из обратного вызова. atom называет значение и его по умолчанию, read возвращает его, и update пишет его. Три помощника вызывают $.state.get и $.state.set для вас:
Потому что хук ui.render прочитал count, Claude Code запускает хук снова каждый раз, когда кнопка пишет его. Три правила применяются к коду:
  • Напишите plugin и key как буквальные строки: claude plugin validate читает их из вашего источника
  • Объявите каждое значение в файле типов: в противном случае валидация не пройдет с hello-tabs.count is not declared
  • Напишите из обратного вызова или хука другого события: хук ui.render может читать состояние и не может писать его, поэтому пишите из onPress, onSubmit или хука для другого события

Изменение hello-tabs для использования $.state

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

Загрузка сохраненного значения снова после /clear

Если ваш мод копирует сохраненное значение из $.store в $.state при session.start, он должен скопировать его снова после /clear, /resume или /branch. Эти команды возвращают каждое значение $.state к его по умолчанию, и session.start не запускается снова. classic.SessionStart запускается после каждого из них, с e.source, установленным на clear, resume или fork, поэтому скопируйте значение снова в хук на нем. В противном случае ваше рисование показывает по умолчанию, и обратный вызов, который сохраняет значение $.state, пишет по умолчанию над тем, что вы сохранили. Этот код загружает count из обоих хуков. Он строится на версии $.state hello-tabs, где count — это атом и update импортируется. Поместите loadCount выше register и добавьте вызов loadCount к хуку session.start, который у вас уже есть. classic.SessionStart также запускается при запуске и после компактирования, которое не сбрасывает $.state, поэтому фильтр на source сохраняет хук к трем сбросам:
С обоими хуками на месте, панель показывает сохраненный счет после /clear и не 0, и следующее нажатие Add one добавляет к сохраненному счету. loadCount пишет сохраненное значение над тем, что в $.state, и session.start запускается снова каждый раз, когда модуль перезагружается. Чтобы хранилище не отставало, сохраняйте при каждом изменении, как кнопка Add one делает. Чтобы проверить перезагрузку без сеанса, протестируйте рисование после /clear.

Сохранение из более чем одного сеанса

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

Следующие шаги