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, seperti panel, pita di atas prompt, atau spinner. Claude Code menaikkan event ui.render 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:
Di terminal yang lebih sempit, panel duduk di atas prompt daripada di samping transkrip.
Bangun mod pertama Anda 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.
Bangun panel dengan tab
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:
Tab-tab tersebut adalah dua tombol dalam satu baris. Mod melacak mana yang aktif dan menggambar konten tab tersebut di bawah baris.
1. Buat plugin
Mod adalah plugin dengan manifest, hooks.json yang menunjuk ke kode Anda, dan file kode. Buat mod 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:
{
"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:
{
"modules": ["./register.js"]
}
2. Tulis kodenya
Daftar ini menjelaskan apa yang dilakukan setiap hook, sesuai urutan kemunculannya dalam kode:
- Menambahkan perintah
/hello-tabs, dan memuat hitungan yang disimpan sesi sebelumnya - 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:
// 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.startjuga membaca hitungan yang disimpan dari$.store, penyimpanan kunci-nilai yang bertahan antar sesi.command.runhanya memberi tahu Claude Code bahwa panel ada. Membuka panel tidak menggambar apa pun dengan sendirinya: Claude Code kemudian menaikkanui.renderuntuk menanyakan apa yang ada di dalamnya.ui.rendermengembalikan pohon elemen,Boxyang menyimpan kotak lain, teks, dan tombol, dan membangunnya lagi daritabdancountsetiap 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.
3. 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.
4. 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 mencakup berapa lama setiap jenis nilai bertahan.
Pilih tempat untuk menggambar
Hook ui.render berjalan untuk setiap titik render kecuali Anda mempersempitnya ke titik yang ingin Anda gambar. Untuk memilih titik render, berikan filter, yang disebut matcher, sebagai argumen kedua untuk on. { component: 'Pane' } menjalankan hook hanya untuk panel. Di dalam hook, e.component menyebutkan nama titik render, e.surface menyatakan aplikasi mana yang sedang menggambar, dan e.props menyimpan data milik titik render tersebut. Untuk panel, e.requestId adalah id yang Anda gunakan saat membukanya.
Panel dan pita kosong hingga sebuah mod mengisinya. Pilih tab untuk melihat apa masing-masing dan cara menggambar di dalamnya:
[tab] Panel
Panel adalah bilah samping di sebelah transkrip pada terminal layar penuh yang lebar, atau area berbingkai di atas prompt jika tidak demikian. Ketika beberapa panel terbuka, masing-masing mendapatkan tab yang menampilkan judulnya.
Panel muncul ketika mod Anda memanggil
$.ui.opendenganidyang Anda pilih, seperti dalam$.ui.open({ id: 'hello-tabs' }). Buka panel pada waktu yang tepat membahas field lainnya dan kapan panel menunggu terminal yang lebih lebar.Untuk menggambar di panel Anda, filter dengan
{ component: 'Pane' }dan periksa bahwae.requestIdadalahidAnda.
[tab] Pita di atas prompt
Pita adalah jalur tepat di atas input prompt. Pita ini selalu ada, dan setiap mod berbagi pita tersebut.
Hook Anda mengembalikan sebuah tree untuk menampilkan sesuatu di pita, atau
next(e)untuk tidak menampilkan apa pun. Sebuah tree menggantikan apa yang digambar di sana oleh mod setelah milik Anda. Untuk mempertahankan milik mereka, letakkan hasilawait next(e)di antara children dari sebuahBoxdalam tree Anda.Untuk menggambar di pita, filter dengan
{ component: 'AbovePrompt' }.
Sebuah mod juga dapat menempatkan barisnya sendiri di transkrip.
Mengubah apa yang sudah digambar Claude Code
Claude Code menggambar sebagian besar antarmukanya sendiri: pesan, baris panggilan tool, spinner, dan lainnya. Masing-masing bagian tersebut juga merupakan titik render, sehingga sebuah mod dapat mengubah gayanya atau menggantinya. Untuk mengubah salah satunya, filter hook ui.render Anda dengan namanya dari tabel ini:
| Titik | Apa itu |
|---|---|
UserMessage, AssistantMessage |
Sebuah pesan di transkrip |
ToolUse, ToolResult, ToolGroup |
Baris panggilan tool, hasilnya, dan grup panggilan yang diciutkan |
CommandOutput |
Baris yang dicetak oleh sebuah perintah |
AskUserQuestion |
Dialog yang dibuka Claude untuk mengajukan pertanyaan kepada Anda |
Spinner, ToolProgress, TurnDuration |
Baris status untuk sebuah giliran: baris yang beranimasi saat Claude bekerja, baris progres langsung dari tool yang sedang berjalan, dan baris yang menutup sebuah giliran |
InfoNotice, SessionMode, PromptHint |
Baris status di bawah logo, label mode di footer, dan baris petunjuk di bawah prompt |
Pane, AskUserQuestion, Spinner, UserMessage, AssistantMessage, dan titik panggilan tool berfungsi di terminal, aplikasi Desktop, dan ekstensi VS Code. Tabel titik render mencantumkan di mana masing-masing titik lainnya dimunculkan.
Pada titik yang sudah digambar Claude Code, hook Anda dapat mengubah detail, mengganti gambarnya, atau membiarkannya. Pilih tab untuk melihat masing-masing diterapkan pada spinner. Contoh-contoh ini membaca variabel calls yang dihitung oleh hook lain, seperti dalam mod tutorial.
[tab] Mengubah detail
Untuk mempertahankan gambar Claude Code dan mengubah satu bagiannya, berikan kepada
nextsalinan event denganpropsyang diubah. Hook ini mengubah teks setelah kata spinner: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 + '…' } }) })Spinner mempertahankan animasi dan katanya, dan teks Anda mengikuti kata tersebut:
Thinking · tool calls: 2…
[tab] Mengganti gambar
Untuk menggambar sesuatu milik Anda sendiri di tempat titik tersebut, kembalikan sebuah tree dan jangan panggil
next. Hook ini menggambar satu baris teks di tempat spinner seharusnya berada: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'] }) })Saat Claude bekerja, baris Anda ditampilkan dan spinner Claude Code tidak:
Claude has made 2 tool calls
[tab] Membiarkannya
Untuk membiarkan titik tersebut seperti yang digambar Claude Code, kembalikan
next(e). Sebuah hook sering melakukan hal itu untuk beberapa event dan tidak untuk yang lain. Hook ini membiarkan spinner hingga ada panggilan yang perlu dihitung: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 + '…' } }) })Sebelum panggilan tool pertama, spinner terlihat seperti tanpa mod:
Thinking…
Pada titik-titik ini, next(e) mengembalikan referensi ke gambar Claude Code, { type: 'engine', ref }, kecuali jika sebuah mod yang berjalan setelah milik Anda mengembalikan tree-nya sendiri. Untuk mengubah isi gambar tersebut, berikan kepada next salinan event dengan props yang berbeda, seperti yang dilakukan tab Mengubah detail. Anda dapat mengembalikan referensi apa adanya, atau menempatkannya di dalam Box di samping elemen Anda sendiri:
on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
const { Box, Text } = $.ui.resolve(e)
const theirs = await next(e)
return Box({ flexDirection: 'column', children: [theirs, Text({ children: ['under the spinner'] })] })
})
Saat Claude bekerja, spinner beranimasi seperti sebelumnya, dan under the spinner muncul di bawahnya.
Dialog izin bukan titik render, sehingga sebuah mod tidak dapat mengubah apa yang ditampilkannya. Dialog pertanyaan, AskUserQuestion, adalah titik render, sehingga sebuah mod dapat mengubahnya. Tree untuk dialog tersebut harus memuat referensi tepat satu kali, dengan elemen Anda di atasnya. Jika tidak, Claude Code menggambar dialognya sendiri.
Buka panel pada waktu yang tepat
Panel hanya muncul ketika mod Anda membukanya. Cara dan waktu Anda membukanya menentukan apakah panel tersebut mengambil fokus keyboard, berapa banyak ruang yang dimintanya, dan apakah panel tersebut ditampilkan sama sekali di terminal yang sempit.
Untuk membuka panel, panggil $.ui.open dengan id yang Anda pilih. id adalah nama panel: hook ui.render Anda memeriksanya, dan Anda memberikannya lagi untuk menutup panel.
await $.ui.open({ id: 'hello-tabs', title: 'Hello tabs', focus: true })
Untuk menutup panel, panggil $.ui.close dengan id yang Anda gunakan saat membukanya:
await $.ui.close({ id: 'hello-tabs' })
Selain id, $.ui.open menerima field opsional berikut:
| Field | Fungsinya |
|---|---|
title |
Label tab panel ketika lebih dari satu panel terbuka |
focus |
Meminta fokus keyboard |
closeOnEscape |
Membuat Esc menutup panel |
holdToasts |
Di terminal, menahan toast selama panel ini yang sedang ditampilkan. Lihat Menahan toast di balik dialog. |
rows |
Tinggi yang diminta ketika panel berada di atas prompt. Default-nya adalah sepertiga dari ruang yang tersedia. |
columns |
Lebar yang diminta ketika panel berada di samping transkrip, paling banyak 45% dari lebar terminal. Setelah pengguna mengubah ukuran panel di samping transkrip, dalam sesi ini atau sesi sebelumnya, panel dibuka di sana dengan lebar yang mereka tetapkan. Batas 45% yang sama berlaku untuk lebar tersebut. |
Ekstensi VS Code mengabaikan rows dan columns dan menentukan ukuran panel sendiri.
focus, closeOnEscape, dan holdToasts bersifat opsional dan hanya menerima true. Untuk tidak menggunakannya, hilangkan field tersebut. Memberikan false akan memunculkan error seperti ui.open: focus is true or left out. Untuk menetapkan salah satunya secara kondisional, tambahkan field tersebut hanya ketika kondisinya terpenuhi. Panggilan ini meminta fokus keyboard hanya ketika items tidak kosong:
const pane = { id: 'hello-tabs', title: 'Hello tabs' }
await $.ui.open(items.length > 0 ? { ...pane, focus: true } : pane)
Agar sebuah perintah dapat membuka panel saat Claude sedang bekerja, tambahkan immediate: true saat Anda mendaftarkan perintah. Tanpanya, perintah yang diketik selama giliran berlangsung akan menunggu hingga giliran berakhir.
Menahan toast di balik dialog
Berikan holdToasts: true ke $.ui.open ketika panel adalah dialog yang dijawab lalu ditinggalkan pengguna, sehingga toast tidak muncul saat mereka memutuskan. Di terminal, penahanan berlangsung selama panel tersebut yang sedang ditampilkan, dan toast yang dimunculkan dalam waktu tersebut menunggu hingga penahanan berakhir.
Claude Code menahan toast dari mod lain dan notifikasi singkatnya sendiri, selain toast yang dimunculkan mod Anda dengan $.ui.toast. Jangan gunakan field ini pada panel yang tetap terbuka, agar pengguna tetap dapat melihatnya.
Ketika panel menunggu terminal yang lebih lebar
Panel yang dibuka mod Anda tanpa diminta tidak muncul di terminal yang sempit, sehingga panel tersebut tidak dapat mengambil alih layar kecil. Apakah panel muncul bergantung pada apa yang membukanya:
- Dibuka oleh sesuatu yang dilakukan pengguna, seperti perintah yang mereka jalankan atau tombol yang mereka tekan, panel muncul pada lebar berapa pun
- Dibuka oleh mod Anda yang bertindak sendiri, seperti dari timer atau hook
turn.start, panel hanya muncul di terminal dengan lebar minimal 144 kolom. Setelah pengguna membuka panel tersebut sendiri sekali, 110 kolom sudah cukup.
Ketika panel muncul, $.ui.open di-resolve menjadi { isPlaced: true }. Ketika panel sedang menunggu, isPlaced bernilai false dan reason adalah string yang menjelaskan alasannya. Panel yang menunggu muncul ketika pengguna membukanya atau melebarkan terminal. Untuk memberi tahu bahwa sesuatu tersedia tanpa membuka panel, panggil $.ui.toast('Your message'), yang menampilkan notifikasi toast.
Menggambar di ekstensi VS Code
Di ekstensi VS Code, apa yang digambar sebuah mod muncul di panel chat dengan ekstensi versi 2.1.299 atau lebih baru. Sesi yang tidak mengambil feature flag, seperti sesi di Amazon Bedrock, memerlukan versi 2.1.304 atau lebih baru. Dalam hook ui.render untuk panel tersebut, e.surface bernilai vscode. Untuk memeriksa titik render dan elemen mana yang digambar panel, serta props mana yang diabaikannya, lihat tabel titik render dan elemen.
Agar sebuah mod berfungsi di panel sekaligus di terminal, perhitungkan perbedaan berikut:
- Panel: pada panel chat dengan lebar 800 piksel atau lebih, panel-panel berlabuh di samping percakapan,
e.props.placementbernilai'dock', dane.viewport.isFullscreenbernilaitrue. Pada panel chat yang lebih sempit, panel-panel digambar di atas kotak prompt,placementbernilai'inline', dan sebuah panel menampilkan 12 baris sebelum menggulir. - Toast dan baris status: semuanya berada dalam satu tumpukan tepat di atas kotak prompt, bersama dengan panel-panel ketika panel chat sempit, dengan satu baris status per mod. Tumpukan tersebut disembunyikan saat permintaan izin atau dialog ditampilkan.
- Label: panel chat membingkai apa yang digambar atau ditulis ulang oleh sebuah mod dan memberinya label dengan nama plugin, seperti From the token-chart plugin atau Edited by the token-chart plugin.
- Keyboard: tombol tidak memiliki hotkey, sehingga pengguna mengklik tombol atau menekan Enter pada tombol tersebut setelah tombol mendapatkan fokus.
- Fokus keyboard: panel chat mengabulkan permintaan
focus: truedari sebuah panel hanya ketika panel chat memiliki fokus, Claude sedang idle tanpa pertanyaan yang menunggu, dan kotak prompt kosong. - Dialog pertanyaan: pada
AskUserQuestion, panel chat mempertahankan kartu pertanyaannya sendiri dan menggambar elemen Anda di atasnya. Props yang Anda berikan kenexttidak mengubah kartu tersebut. - Saran prompt:
$.prompt.suggestdi-resolve denganisShown: falsedan tidak menampilkan apa pun, karena panel chat menyimpan teks saran redup di kotak prompt untuk saran Claude sendiri. - Batas teks: panel chat memotong baris status pada 200 karakter atau dua baris, dan toast pada 500 karakter.
Menempatkan baris di transkrip
Baris adalah satu baris yang ditambahkan mod Anda ke transkrip di terminal, setelah pesan terbaru. Baris tersebut tetap berada di posisi itu seiring berjalannya percakapan, dan Claude tidak melihatnya. $.ui.place memerlukan Claude Code v2.1.314 atau lebih baru.
1. Tempatkan baris
Panggil $.ui.place dengan sebuah id. id adalah nama yang Anda buat untuk baris tersebut, seperti build-status. Nama ini dapat berisi 1 hingga 64 huruf, digit, _, atau -, dan setiap baris yang dimiliki mod Anda di transkrip memerlukan id-nya sendiri.
2. Gambar baris
Gambar baris tersebut pada titik render Placed.
Contoh ini menambahkan perintah /build-status. Ketika pengguna menjalankan perintah tersebut, mod menambahkan baris yang menampilkan Build: passing. Teksnya tetap agar contoh ini singkat, sehingga mod sungguhan akan menggambar hasilnya sendiri:
export function register(on) {
on('session.start', async ($, e, next) => {
await $.command.register({ name: 'build-status', description: 'Show the build status' })
return next(e)
})
on('command.run', { command: 'build-status' }, async ($) => {
const placed = await $.ui.place({ id: 'build-status' })
// Where nothing draws the row, reply with text instead
return placed.isPlaced ? {} : { text: 'Build: passing' }
})
on('ui.render', { component: 'Placed' }, async ($, e, next) => {
// This hook runs for every mod's rows, and another mod can use the same id
if (e.props.plugin !== $.plugin.name || e.props.id !== 'build-status') return next(e)
const { Text } = $.ui.resolve(e)
return Text({ children: ['Build: passing'] })
})
}
Untuk mencoba contoh ini, letakkan kode ini di sebuah mod. Dalam sesi terminal, jalankan /build-status di prompt Claude Code. Build: passing muncul sebagai baris di akhir transkrip.
Panggilan tersebut tidak selalu menampilkan baris, dan sebuah baris dapat hilang kemudian, jadi tangani kasus-kasus berikut di mod Anda:
- Hasilnya:
{ isPlaced: true, requestId }ketika terminal menggambar baris tersebut. Jika tidak,isPlacedbernilaifalsedanreasonmenjelaskan alasannya, sepertino attached surface draws placed rows yet. Salah satu alasannya adalah batas jumlah baris yang dapat dimiliki sebuah mod pada satu waktu. - Panggilan kedua dengan
idyang sama: selama baris pertama masih ada di transkrip, panggilan tersebut tidak menambahkan baris dan di-resolve denganrequestIddari baris tersebut. Untuk mengubah apa yang ditampilkan baris, gambar ulang baris tersebut. - Ketika baris hilang: ketika percakapan berakhir, ketika pengguna melakukan rewind melewati baris tersebut, atau, di luar rendering layar penuh, ketika percakapan dipadatkan.
--resumetidak mengembalikan baris tersebut. Untuk menampilkan baris lagi, panggil$.ui.placelagi.
Memasang latar belakang di balik percakapan
[!NOTE]
$.theme.setdan$.theme.clearmenggantikan$.ui.backdrop, yang telah dihapus di Claude Code v2.1.319. Ubah$.ui.backdrop({ id: 'your-backdrop' })menjadi$.theme.set({ backdrop: { id: 'your-backdrop' } }), dan$.ui.backdrop(undefined)menjadi$.theme.clear(), yang menghapus latar belakang dari mod mana pun.
Untuk memasang latar belakang di balik percakapan di aplikasi Desktop atau aplikasi seluler, panggil $.theme.set dengan backdrop, lalu gambar latar belakang tersebut pada titik render Backdrop. Untuk melepas latar belakang, panggil $.theme.clear(). Terminal tidak menggambar latar belakang. $.theme.set dan $.theme.clear memerlukan Claude Code v2.1.319 atau lebih baru.
Mod ini memasang latar belakang saat sesi dimulai, menggambar latar belakang tersebut, dan menambahkan perintah /plain yang melepas latar belakang:
export function register(on) {
// Neither a reload of this mod nor --resume keeps the backdrop, so set it here
on('session.start', async ($, e, next) => {
await $.theme.set({ backdrop: { id: 'night-sky', colorScheme: 'dark' } })
await $.command.register({ name: 'plain', description: 'Take the backdrop off' })
return next(e)
})
on('ui.render', { component: 'Backdrop' }, async ($, e, next) => {
// This hook runs for every mod's backdrop, and another mod can use the same id
if (e.props.plugin !== $.plugin.name || e.props.id !== 'night-sky') return next(e)
const { Box, Text } = $.ui.resolve(e)
return Box({ backgroundColor: 'blue', children: [Text({ children: ['Night sky'] })] })
})
on('command.run', { command: 'plain' }, async ($) => {
const cleared = await $.theme.clear()
return { text: cleared.reason }
})
}
Untuk mencoba contoh ini, letakkan kode ini di sebuah mod dan mulai sesi di aplikasi Desktop atau aplikasi seluler. Untuk melepas latar belakang, jalankan /plain di prompt Claude Code. Transkrip menampilkan the theme is cleared setelah nama mod, seperti night-sky: the theme is cleared untuk mod bernama night-sky.
Periksa apa yang diterima backdrop, apa yang diberitahukan hasil panggilan kepada Anda, dan apa yang dapat diubah oleh mod lain:
- Field
backdrop:id, sebuah nama yang Anda pilih, wajib diisi, dan field lainnya bersifat opsional.colorSchemebernilai'light'atau'dark'dan menyatakan apakah gambar Anda terang atau gelap. Untukglass, lihat tipe untuk build Anda. - Apakah latar belakang digambar:
$.theme.setdi-resolve denganisDrawn: trueketika aplikasi Desktop atau seluler yang terhubung ke sesi menyatakan bahwa aplikasi tersebut menggambar latar belakang. Jika tidak,isDrawnbernilaifalsedanreasonmenjelaskan alasannya. - Satu latar belakang untuk setiap sesi: panggilan
$.theme.setmenggantikan latar belakang sesi dan$.theme.clear()menghapus latar belakang tersebut, mod mana pun yang memasangnya. Panggil$.theme.clear()hanya ketika pengguna memintanya.
Bangun pohon dari elemen
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 merendernya di aplikasi mana pun yang dibuka pengguna: terminal, aplikasi Desktop, atau ekstensi VS Code.
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.
Pilih tab untuk melihat setiap elemen yang paling umum dan cara terminal menggambarnya:
[tab] Text
Textmenggambar string, dengan gaya opsional sepertibolddancolor:Text({ children: ['This is the first tab.'] })This is the first tab.
[tab] Box
Boxmengatur apa yang ada di dalamnya, dalam baris atau kolom. Ini menempatkan tombol dan baris teks berdampingan, dua kolom terpisah:Box({ flexDirection: 'row', columnGap: 2, children: [ Button({ key: 'more', label: 'Add one', onPress: addOne }), Text({ children: ['Count: 0'] }), ], })[ Add one ] Count: 0
[tab] Button
Buttonadalah kontrol yang dapat ditekan pengguna. Ini menjalankan callbackonPressAnda. Denganplain: truetidak memiliki tanda kurung dan menampilkan pintasan keyboard:Button({ key: 'more', label: 'Add one', onPress: addOne }) Button({ key: 'tab-one', label: 'One', hotkey: '1', plain: true, onPress: showTabOne })[ Add one ] 1: One
[tab] Input
Inputadalah bidang teks. Ini menjalankan callbackonSubmitAnda dengan teks saat pengguna menekan Enter:Input({ key: 'new-note', label: 'Note', placeholder: 'Type a note and press Enter', value: '', submitLabel: 'add', onSubmit: addNote, })Note: Type a note and press EnterAtur
rowske 2 atau lebih untuk bidang yang menampung beberapa baris.
Galeri antarmuka memiliki contoh dan tangkapan layar dari sebagian besar elemen. Tabel ini mencantumkan setiap elemen:
| Elemen | Apa yang digambarnya | Di mana |
|---|---|---|
Box |
Kontainer flex yang mengambil prop tata letak seperti flexDirection, borderStyle, dan width. Prop spasi seperti columnGap dan padding mengambil angka atau nama langkah dari 'xs' hingga 'xl'. |
Di mana-mana |
Text |
Teks bergaya. Mengambil color, bold, dimColor, italic, wrap, dan lainnya seperti weight. 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 menu tarik-turun | Terminal, Desktop, VS Code |
Svg |
Dokumen SVG | Desktop, VS Code |
Client |
Wilayah yang digambar oleh file kedua Anda, untuk animasi dan input pointer. File itu tidak mendapat API mod. File itu mencapai hook Anda dengan memposting data, yang tiba sebagai event ui.message. Jika file itu gagal dimuat, digambar, atau dijalankan, hook Anda menerima event ui.fault. |
Terminal, Desktop |
Raster, Image |
Grid sel berwarna, 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.
Jika gambar Anda tidak muncul, periksa pohonnya untuk prop yang tidak diambil oleh elemen atau anak di tempat yang tidak seharusnya ada. Hook yang mengembalikan pohon seperti itu gagal, dan baris hook skipped menyebutkan mod Anda dan aturan yang dilanggar pohon tersebut. Untuk Text di panel dengan prop bernama bogusProp, baris itu berbunyi:
my-mod: ui.render hook skipped: returned the wrong shape (a tree Pane refuses: Text prop "bogusProp" is not allowed)
Link di aplikasi Desktop dan VS Code
Di aplikasi Desktop dan ekstensi VS Code, Link digambar sebagai teks biasa kecuali href-nya memenuhi persyaratan berikut:
- Skema dan host: URL
https:, atau URLhttp://localhostsepertihttp://localhost:3000 - Tanpa
@: tulis@di path atau query sebagai%40 - Ejaan: apa yang dikembalikan
new URL(href).href, selain/yang tidak ada setelah host. Hal itu mengecualikan host dengan huruf besar, spasi, dan:443pada URLhttps:.
Di terminal, persyaratan ini tidak berlaku.
Saat Client gagal
Di terminal, saat file yang dijalankan Client gagal, baris redup seperti my-mod: Client client/spinner.js: boom menggantikan tempat Client, dan sisa gambar Anda tetap ditampilkan.
Jika mod Anda menangani ui.fault, Claude Code kemudian menggambar situs lagi.
Gambar grid sel berwarna
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, string base64 yang mengemas setiap sel. Setiap sel adalah tiga angka: titik kode karakter, warnanya, dan warna latar belakangnya. Warna adalah nilai RGB 24-bit dalam heksadesimal, seperti 0xc62828 untuk merah. Nilai 0x01000000, satu di atas rentang tersebut, berarti default terminal.
Aplikasi selain terminal tidak memiliki Raster, jadi periksa e.surface dan gambar teks di sana. Badan panel ini menggambar peta panas tiga kali dua:
// 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) })],
})
})
Di terminal, panel menampilkan grid:
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 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.
Merespons penekanan dan pengetikan
Ketika pengguna menekan tombol, mengetik ke bidang, atau memilih dari daftar yang digambar mod Anda, Claude Code memanggil callback kontrol itu, yang berjalan dalam modul Anda. Setiap kontrol mengambil callback-nya sendiri:
Button: mengambilonPress(e), di manae.surfaceadalah aplikasi tempat penekanan berasalInput: mengambilonSubmit(value)danonInput(value)Select: mengambilonSelect(value)dengan pilihan dalamoptions, daftar setidaknya satu pilihan dengan nilai unik, seperti[{ value: 'sm', label: 'Small' }, { value: 'lg', label: 'Large' }]
Kontrol mana yang memerlukan key
Berikan prop key pada setiap Input dan Select, seperti key: 'size'. Button boleh tidak memiliki key ketika memiliki prop label atau satu anak berupa string. Key-nya kemudian adalah nilai label atau string tersebut.
Tes dan mod lain mengidentifikasi kontrol berdasarkan key-nya:
- Tes: sebuah tes menekan atau mengetik ke kontrol berdasarkan key-nya.
- Mod lain: Claude Code menembakkan
ui.press,ui.input, atauui.selectdengan key kontrol dalame.elementsebelum menjalankan callback kontrol tersebut. Hook mod lain untuk event tersebut melihat apa yang pengguna ketik keInputAnda dan dapat mengubah teks itu atau mencegah callback Anda berjalan. API mods tidak memberi mod tersebut metode untuk menekan tombol Anda.
Jika sebuah pesan mengatakan needs a key atau is drawn twice, temukan penyebabnya berdasarkan pesan tersebut.
Fokus keyboard dan pintasan keyboard
Mod Anda tidak pernah membaca keyboard itu sendiri. Pengguna menekan kunci, Claude Code memutuskan kontrol mana yang dimaksudkan, dan callback kontrol itu berjalan. Kecuali untuk pintasan keyboard digit di pita atau pengikatan action, itu hanya terjadi saat panel atau pita Anda memiliki fokus keyboard. Sisa waktu, kunci pergi ke prompt.
Di ekstensi VS Code, tombol tidak memiliki pintasan keyboard: pengguna mengklik tombol atau menekan Enter padanya, dan mengetik ke bidang.
Bagaimana panel mendapat fokus keyboard
Panel mendapat fokus keyboard ketika:
- Mod Anda membukanya dengan
focus: truedari perintah atau penekanan - Pengguna menekan Ctrl+X kemudian Tab
- Pengguna mengkliknya, yang di terminal memerlukan rendering layar penuh
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.
Apa yang dilakukan setiap kunci
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. Di dalam Input di terminal, kunci ini berpindah antar baris bidang, dan dari baris pertama atau terakhirnya, kunci ini berpindah ke kontrol sebelum atau sesudahnya. |
| 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. |
| Page Up, Page Down, Home, dan End | Menggulir panel atau pita Anda ketika memiliki lebih banyak baris daripada yang dapat ditampilkan |
| Ctrl+X kemudian tombol panah | Mengubah ukuran panel Anda. Kiri atau Atas memberinya lebih banyak ruang, dan Kanan atau Bawah mengembalikan ruang tersebut. Panel di samping transkrip berhenti melebar pada 45% lebar terminal. |
| Ctrl+X kemudian X | Menutup panel Anda, bahkan saat salah satu bidangnya memiliki fokus |
| Ctrl+X kemudian Ctrl+A | Menciutkan pita menjadi satu baris saat mod menggambar di sana, atau membentangkannya kembali. Kunci ini juga berfungsi saat prompt memiliki fokus. |
| 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.
Atur pintasan keyboard dan fokus pertama
hotkey: untuk membiarkan pengguna menekanButtondengan satu kunci, berikanhotkeydari satu digit atau satu huruf kecil, seperti dalamhotkey: 'a'autoFocus: untuk memilih kontrol mana yang memiliki fokus saat panel terbuka, tambahkanautoFocus: trueke dalamnya. Di ekstensi VS Code,autoFocustidak melakukan apa-apa: kontrol digambar seolah-olah Anda tidak menyertakannya, dan Anda tidak perlu mengubah apa pun. Prop ini hanya menerimatrue, jadi hilangkan pada kontrol lainnya.
Bagaimana pintasan keyboard ditampilkan tergantung pada tombol dan aplikasi:
| Tombol | Di terminal | Di aplikasi Desktop | Di ekstensi VS Code |
|---|---|---|---|
| Dengan tanda kurung, default | [ Add one ], tanpa pintasan keyboard ditampilkan |
Label dengan kunci kecil di sebelahnya | Label, dan pintasan keyboard tidak melakukan apa-apa |
Dengan plain: true |
1: One |
Label dengan kunci kecil di sebelahnya | Label, dan pintasan keyboard tidak melakukan apa-apa |
Di terminal, beri nama kunci dalam label tombol berbingkai, atau gunakan plain: true, sehingga pengguna dapat melihat apa yang harus ditekan. Referensi elemen memiliki aturan Button lainnya: action, pintasan keyboard digit di pita, dan dua tombol pada satu pintasan keyboard.
Saat pengguna menciutkan pita
Di terminal, saat mod menggambar di pita, pengguna dapat menciutkan pita menjadi satu baris agar tidak menghalangi:
- Klik
[-]: saat pita dibentangkan, Claude Code menggambar tanda ini di ujung kanan baris pertamanya. Dalam rendering layar penuh, pengguna mengkliknya untuk menciutkan pita. - Tekan Ctrl+X kemudian Ctrl+A: ini berfungsi baik saat prompt, pita, atau panel yang memiliki fokus keyboard. Pengguna dapat mengikat kunci lain ke sini melalui aksi
abovePrompt:toggle.
Pita yang diciutkan menyembunyikan semua yang digambar mod di sana dan sebagai gantinya menampilkan satu baris redup, yang menyebutkan kunci untuk membentangkannya sesuai pengikatan yang dimiliki pengguna:
▸ plugin panel hidden · ctrl+x ctrl+a or click to show
Kunci yang sama, atau dalam rendering layar penuh sebuah klik pada baris tersebut, membentangkan pita kembali. Sampai saat itu, tidak ada yang menekan tombol di dalamnya: tidak ada pintasan keyboard, tidak ada digit yang diketik ke prompt kosong, dan tidak ada pintasan keyboard untuk action sebuah tombol. Jika pita memiliki fokus keyboard saat pengguna menciutkannya, fokus kembali ke prompt, dan Ctrl+X kemudian Tab melewati pita selama pita diciutkan.
Pita tetap diciutkan sampai pengguna membentangkannya, bahkan setelah /clear, dan dibentangkan kembali saat Claude Code dimulai berikutnya. Mod Anda tidak dapat membentangkannya atau mengetahui bahwa pita sedang diciutkan: hook ui.render tetap berjalan, dan tidak ada apa pun dalam e.props yang menunjukkannya. Jangan jadikan pita sebagai satu-satunya tempat mod Anda menampilkan sesuatu yang harus dijawab pengguna.
Ambil input yang diketik dan gambar baris untuk setiap item
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:
╭────────────────────────────────────────────────────────✕─╮
│ Note: Type a note and press Enter ⏎ add │
│ x buy milk │
│ x call bob │
╰──────────────────────────────────────────────────────────╯
✕ di batas atas adalah tanda milik Claude Code sendiri untuk menutup panel.
Contoh menggunakan teknik berikut:
- Ambil input yang diketik:
InputmemanggilonSubmit(value)dengan teks bidang saat pengguna menekan Enter, danonInput(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:
// 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',
// Teks awal bidang
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
xcatatan memiliki fokus, kemudian tekan Enter.xadalah 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. Jika hook Anda menggambar panel lagi saat pengguna sedang mengetik catatan, bidang mempertahankan teks mereka selama hook meneruskan value yang sama setiap kali. Untuk mengganti teks mereka dengan teks Anda sendiri, teruskan value yang berbeda.
Contoh menyimpan catatan dan tidak memuatnya. Untuk membawanya kembali di sesi berikutnya, bacalah dalam hook session.start, cara hello-tabs membaca count.
Prop berikut membentuk 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 |
value |
'' |
Teks awal bidang. Di sini kosong, sehingga placeholder ditampilkan. |
submitLabel |
add |
Kata setelah ⏎ yang mengatakan apa yang dilakukan Enter |
Mengirimkan Input tidak memulai giliran kecuali callback Anda memanggil $.prompt.submit.
Gambar ulang situs
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.
Ketika Claude Code menggambar ulang tanpa diminta
Claude Code menjalankan hook ui.render Anda lagi ketika prop situs berubah atau lebar terminal berubah. Ketika sebuah Client di situs gagal dan mod Anda menangani ui.fault, Claude Code menjalankan hook sekali lagi setelah hook ui.fault Anda kembali, sehingga hook ui.render Anda dapat menghilangkan Client tersebut. Itu tidak menjalankan hook pada timer, dan itu tidak dapat mengatakan ketika variabel dalam modul Anda berubah.
Gambar ulang saat data Anda berubah
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:
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 membungkus panggilan yang sama dalam fungsi redraw-nya.
Nilai yang Anda simpan dalam $.state tidak memerlukan panggilan, karena menulis nilai menggambar ulang situs yang membacanya.
Gambar ulang pada timer
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 ke dalamnya:
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 kini menjalankan hook ui.render Anda sekali setiap detik. Timer berhenti setelah modul dimuat ulang, dan instance baru dari modul tersebut memulai timer-nya sendiri.
Seberapa sering situs dapat digambar ulang
Claude Code membatasi frekuensi penggambaran ulang situs, jadi mod Anda dapat memanggil $.ui.invalidate sesering data berubah. Untuk seberapa sering setiap situs dapat digambar ulang, lihat tabel batas.
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.
Menyimpan state
Tempat sebuah mod menyimpan nilai menentukan berapa lama nilai itu bertahan: hingga modul dimuat ulang, hingga sesi berakhir, atau dari satu sesi ke sesi berikutnya. Pilihlah berdasarkan berapa lama nilai tersebut harus bertahan:
| Simpan di | Bertahan hingga | Gunakan untuk |
|---|---|---|
| Variabel tingkat modul | Modul dimuat ulang, yang terjadi setiap kali Anda menyimpan file selama pengembangan | Nilai yang boleh hilang, seperti tab di hello-tabs |
$.state |
Sesi berakhir, atau pengguna menjalankan /clear, /resume, atau /branch |
Nilai yang menjadi dasar sebuah gambar dan harus bertahan setelah pemuatan ulang |
$.store |
Mod Anda menghapusnya, atau tidak ada sesi yang membaca atau menulis store selama cleanupPeriodDays. Store adalah penyimpanan key-value, yang disimpan sebagai file JSON milik plugin Anda sendiri di bawah ~/.claude/plugins/store/. |
Pengaturan, riwayat, apa pun yang diharapkan pengguna untuk ditemukan lagi di lain waktu |
$.store.get(key) di-resolve ke nilai tersebut atau undefined, dan $.store.set(key, value) menerima nilai JSON apa pun.
Menyimpan nilai di $.state
$.state menyimpan nilai selama satu sesi, dan menggambar ulang untuk Anda. Ini adalah state reaktif: hook ui.render yang membaca sebuah nilai akan berlangganan ke nilai tersebut, sehingga Claude Code menggambar ulang lokasi itu setiap kali Anda menulis nilainya, dan Anda tidak perlu memanggil $.ui.invalidate. Nilai di $.state juga bertahan setelah modul dimuat ulang, sedangkan variabel tidak.
Untuk menyiapkannya, deklarasikan nilai-nilai Anda, arahkan manifest Anda ke deklarasi tersebut, lalu definisikan dan gunakan setiap nilai. Contoh-contoh berikut memindahkan count dari hello-tabs ke $.state.
Mendeklarasikan nilai
Deklarasikan nilai-nilai dalam file deklarasi tipe. Key terluar adalah nama plugin Anda, dan setiap entri di bawahnya adalah sebuah nilai beserta tipenya. Simpan ini sebagai hello-tabs/types/index.d.ts:
declare module 'claude-code' {
interface PluginState {
'hello-tabs': {
tab: 'one' | 'two'
count: number
}
}
}
Mengarahkan manifest ke deklarasi
Agar claude plugin validate dapat memeriksa kode Anda terhadap file tersebut, tambahkan field types ke manifest dengan path-nya:
{
"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"
}
Mendefinisikan, membaca, dan menulis nilai
Di modul Anda, definisikan setiap nilai dengan nilai default, baca saat menggambar, dan tulis dari sebuah callback. atom memberi nama sebuah nilai dan default-nya, read mengembalikannya, dan update menulisnya. Ketiga helper tersebut memanggil $.state.get dan $.state.set untuk Anda:
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)
Karena hook ui.render membaca count, Claude Code menjalankan hook tersebut lagi setiap kali tombol menulisnya.
Aturan berikut berlaku untuk kode tersebut:
- Tulis
plugindankeysebagai string literal:claude plugin validatemembacanya dari source Anda - Simpan hasil setiap panggilan
atomdalamconst: jika Anda mendeklarasikancountdenganlet, validasi gagal dengantakes a source the scan can read - Deklarasikan setiap nilai dalam file deklarasi tipe: jika tidak, validasi gagal dengan
hello-tabs.count is not declared - Tulis dari callback atau hook event lain: hook
ui.renderdapat membaca state tetapi tidak dapat menulisnya, jadi tulislah darionPress,onSubmit, atau hook untuk event lain
Mengubah hello-tabs agar menggunakan $.state
Untuk memindahkan count di hello-tabs ke $.state, ubah setiap baris yang menggunakannya:
- Di bagian atas modul: tambahkan baris
import, dan gantilet count = 0dengan barisatom - Di hook
ui.render: tambahkan barisreadsebelumtabButton, dan gambar'Count: ' + ndi dalamText - Di tombol Add one: ganti
onPressdengan yang ada di Menyimpan dari lebih dari satu sesi, yang menyimpan hitungan sekaligus menulisnya - Di hook
session.start: ganti dua baris yang membacasaveddengan panggilanloadCountdari Memuat ulang nilai yang tersimpan setelah/clear
Pertahankan redraw untuk tombol-tombol tab, karena tab masih berupa variabel.
Memuat ulang nilai yang tersimpan setelah /clear
Jika mod Anda menyalin nilai tersimpan dari $.store ke $.state pada session.start, mod tersebut harus menyalinnya lagi setelah /clear, /resume, atau /branch. Perintah-perintah tersebut mengatur ulang setiap nilai $.state ke default-nya, dan session.start tidak dipicu lagi. classic.SessionStart memang dipicu setelah masing-masing perintah tersebut, dengan e.source bernilai clear, resume, atau fork, jadi salin nilai tersebut lagi dalam hook padanya. Jika tidak, gambar Anda menampilkan nilai default, dan callback yang menyimpan nilai $.state akan menulis nilai default di atas apa yang telah Anda simpan.
Kode ini memuat count dari kedua hook. Kode ini dibangun di atas versi $.state dari hello-tabs, di mana count adalah sebuah atom dan update diimpor. Letakkan loadCount di atas register, dan tambahkan panggilan loadCount ke hook session.start yang sudah Anda miliki. classic.SessionStart juga dipicu saat startup dan setelah compaction, yang tidak mengatur ulang $.state, sehingga filter pada source membatasi hook tersebut pada tiga pengaturan ulang itu:
// 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)
})
Dengan kedua hook terpasang, panel menampilkan hitungan yang tersimpan setelah /clear, bukan 0, dan penekanan Add one berikutnya menambah hitungan yang tersimpan.
loadCount menulis nilai yang tersimpan di atas nilai di $.state, dan session.start dipicu lagi setiap kali modul dimuat ulang. Agar store tidak tertinggal, simpan pada setiap perubahan, seperti yang dilakukan tombol Add one.
Untuk memeriksa pemuatan ulang tanpa sesi, uji gambar setelah /clear.
Menyimpan dari lebih dari satu sesi
Setiap sesi di mesin Anda yang menjalankan mod Anda berbagi satu $.store. get yang diikuti set tidak bersifat atomik. Ketika dua sesi masing-masing membaca sebuah nilai, mengubahnya, dan menulisnya kembali, keduanya berlomba, dan penulisan kedua menggantikan yang pertama.
Untuk memperkecil kemungkinan tersebut:
- Berikan setiap item key-nya sendiri:
sethanya mengubah key-nya sendiri, sehingga sesi-sesi yang menulis key berbeda tidak saling menimpa - Baca lagi tepat sebelum Anda menulis: untuk nilai yang diubah oleh beberapa sesi, lakukan
getpada key di dalam callback dan bangun nilai baru dari situ, bukan dari salinan yang Anda muat padasession.start. Penulisan dari sesi lain tetap hilang jika terjadi di antaragetdansetAnda.
Tombol ini menambahkan satu ke apa pun yang saat ini disimpan oleh store, lalu memperbarui gambar:
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)
}
Jika sesi kedua telah menekan tombolnya sendiri tiga kali sejak sesi ini dimulai, penekanan ini menampilkan dan menyimpan hitungan yang mencakup ketiga penekanan tersebut.
Langkah berikutnya
- Bereaksi terhadap event: umpan gambar Anda dari panggilan alat dan giliran
- Gunakan API mods: umpan gambar Anda dari timer dan panggilan model
- Uji gambar: tekan tombol Anda dari tes, di lebih dari satu permukaan
- Render sites dan elemen: prop setiap situs dan prop setiap elemen