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.
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:
Claude Code tidak memiliki elemen tab bawaan, jadi tab adalah dua tombol dalam satu baris. Mod melacak mana yang aktif dan menggambar konten tab tersebut di bawah baris.
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"]
}
Tulis kodenya
Kode melakukan tiga pekerjaan, satu di setiap hook:
- Menambahkan perintah
/hello-tabs - Membuka panel saat Anda menjalankan perintah tersebut
- Menggambar konten panel: baris tab dan badan tab yang terbuka
Dua variabel tingkat modul, tab dan count, menyimpan status panel.
Simpan ini sebagai hello-tabs/hooks/register.js:
// 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.
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.
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 di mana menggambar
Hook ui.render berjalan untuk setiap render site kecuali Anda mempersempit ke yang Anda inginkan. Untuk memilih render site, teruskan filter, disebut matcher, sebagai argumen kedua ke on. { component: 'Pane' } menjalankan hook hanya untuk panel. Dalam hook, e.component menamai situs, e.surface mengatakan aplikasi mana yang menggambar, dan e.props menyimpan data situs sendiri. Untuk panel, e.requestId adalah id yang Anda buka dengannya.
Dua situs kosong sampai mod mengisinya, panel dan pita. Pilih tab untuk melihat apa itu masing-masing dan cara menggambar di dalamnya:
Panel adalah sidebar di samping transkrip dalam terminal fullscreen yang lebar, atau wilayah berbingkai di atas prompt sebaliknya. Dengan beberapa panel terbuka, masing-masing mendapat tab yang menampilkan judulnya.
Panel muncul saat mod Anda memanggil $.ui.open dengan id yang Anda pilih, seperti dalam $.ui.open({ id: 'hello-tabs' }). Buka panel pada waktu yang tepat mencakup bidang lain dan kapan panel menunggu terminal yang lebih lebar.
Untuk menggambar di panel Anda, filter pada { component: 'Pane' } dan periksa bahwa e.requestId adalah id Anda.
Pita adalah strip langsung di atas input prompt. Selalu ada, dan setiap mod berbaginya.
Hook Anda mengembalikan pohon untuk menampilkan sesuatu di pita, atau next(e) untuk menampilkan tidak ada. Pohon menggantikan apa yang mod setelah Anda gambar di sana. Untuk menjaga milik mereka, letakkan hasil await next(e) di antara anak-anak Box dalam pohon Anda.
Untuk menggambar di pita, filter pada { component: 'AbovePrompt' }.
Ubah apa yang sudah digambar Claude Code
Claude Code menggambar sebagian besar antarmukanya sendiri: pesan, baris panggilan alat, spinner, dan lainnya. Masing-masing bagian itu adalah render site juga, jadi mod dapat mengubah gaya atau menggantinya. Untuk mengubah satu, filter hook ui.render Anda pada namanya dari tabel ini:
| Situs | Apa itu |
|---|---|
UserMessage, AssistantMessage |
Pesan dalam transkrip |
ToolUse, ToolResult, ToolGroup |
Baris panggilan alat, hasilnya, dan run panggilan yang dilipat |
CommandOutput |
Baris yang dicetak perintah |
AskUserQuestion |
Dialog yang dibuka Claude untuk menanyakan pertanyaan kepada Anda |
Spinner, ToolProgress, TurnDuration |
Baris status untuk giliran: baris yang beranimasi saat Claude bekerja, baris kemajuan langsung alat yang berjalan, dan baris yang menutup giliran |
InfoNotice, SessionMode, PromptHint |
Baris status di bawah logo, label mode di footer, dan baris petunjuk di bawah prompt |
Di situs yang sudah digambar Claude Code, hook Anda memiliki tiga pilihan: ubah detail, ganti gambar, atau biarkan saja. Pilih tab untuk melihat masing-masing diterapkan pada spinner. Contoh membaca variabel calls yang hook lain hitung, seperti dalam mod tutorial.
Untuk menjaga gambar Claude Code dan mengubah satu bagian darinya, teruskan next salinan event dengan props yang diubah. Hook ini mengubah teks setelah kata spinner:
on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
// Jaga spinner Claude Code, dan ubah teks setelah katanya
return next({ ...e, props: { ...e.props, suffix: ' · tool calls: ' + calls + '…' } })
})
Spinner menjaga animasi dan katanya, dan teks Anda mengikuti kata:
Thinking · tool calls: 2…
Untuk menggambar sesuatu milik Anda sendiri di tempat situs, kembalikan pohon dan jangan panggil next. Hook ini menggambar satu baris teks di mana spinner akan berada:
on('ui.render', { component: 'Spinner' }, async ($, e) => {
const { Text } = $.ui.resolve(e)
// Tidak ada panggilan ke next, jadi baris ini digambar di tempat spinner
return Text({ children: ['Claude has made ' + calls + ' tool calls'] })
})
Saat Claude bekerja, baris Anda menampilkan dan spinner Claude Code tidak:
Claude has made 2 tool calls
Untuk meninggalkan situs seperti yang digambar Claude Code, kembalikan next(e). Hook sering melakukan itu untuk beberapa event dan tidak untuk yang lain. Hook ini meninggalkan spinner saja sampai ada panggilan untuk dihitung:
on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
// Tidak ada yang ditampilkan dulu, jadi teruskan event tanpa perubahan
if (calls === 0) return next(e)
return next({ ...e, props: { ...e.props, suffix: ' · tool calls: ' + calls + '…' } })
})
Sebelum panggilan alat pertama, spinner terlihat seperti tanpa mod:
Thinking…
Prompt izin bukan render site, jadi mod tidak dapat mengubah apa yang ditampilkannya. Dialog pertanyaan, AskUserQuestion, adalah satu, jadi mod dapat mengubah itu.
Terminal dan aplikasi Desktop tidak menaikkan semua situs yang sama. Pane, AbovePrompt, Spinner, dan situs transkrip bekerja di keduanya. Beberapa baris status lainnya hanya diangkat di terminal. Tabel render sites mencantumkan di mana masing-masing diangkat.
Buka panel pada waktu yang tepat
Panel hanya muncul saat mod Anda membukanya. Bagaimana dan kapan Anda membukanya menentukan apakah itu mengambil fokus keyboard, berapa banyak ruang yang dimintanya, dan apakah itu menampilkan sama sekali di terminal yang sempit.
Untuk membuka panel, panggil $.ui.open dengan id yang Anda pilih. id adalah nama panel: hook ui.render Anda memeriksanya, dan Anda meneruskannya 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 buka dengannya:
await $.ui.close({ id: 'hello-tabs' })
Selain id, $.ui.open mengambil bidang opsional ini:
| Bidang | Apa yang dilakukannya |
|---|---|
title |
Label tab panel saat lebih dari satu panel terbuka |
focus |
Permintaan fokus keyboard |
closeOnEscape |
Membuat Esc menutup panel. Teruskan true atau tinggalkan bidang, karena Claude Code menolak false. |
holdToasts |
Menahan toast, pemberitahuan kecil dari $.ui.toast, sampai panel ditutup |
rows |
Tinggi untuk diminta saat panel duduk di atas prompt. Default adalah sepertiga dari ruang. |
columns |
Lebar untuk diminta saat panel duduk di samping transkrip |
Untuk membiarkan perintah membuka panel saat Claude bekerja, tambahkan immediate: true saat Anda mendaftarkan perintah. Tanpanya, perintah yang diketik selama giliran menunggu giliran berakhir.
Ketika panel menunggu terminal yang lebih lebar
Panel yang dibuka mod Anda tanpa diminta tidak muncul di terminal sempit, jadi tidak dapat mengambil alih layar kecil. Apakah itu muncul tergantung pada apa yang membukanya:
- Dibuka oleh sesuatu yang dilakukan pengguna, seperti perintah yang mereka jalankan atau tombol yang mereka tekan, panel muncul pada lebar apa pun
- Dibuka oleh mod Anda bertindak sendiri, seperti dari timer atau hook
turn.start, panel hanya muncul di terminal setidaknya 144 kolom lebar. Setelah pengguna telah membuka panel itu sendiri sekali, 110 kolom cukup.
Ketika panel muncul, $.ui.open diselesaikan ke { isPlaced: true }. Ketika panel menunggu, isPlaced adalah false dan reason adalah string yang mengatakan mengapa. Panel yang menunggu muncul saat pengguna membukanya atau memperlebar terminal. Untuk mengatakan sesuatu tersedia tanpa membuka panel, panggil $.ui.toast('Your message'), yang menampilkan pemberitahuan kecil yang hilang setelah beberapa detik.
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 merender di terminal atau aplikasi Desktop.
Untuk mendapatkan elemen, panggil $.ui.resolve(e) dalam hook Anda, seperti dalam const { Box, Text, Button } = $.ui.resolve(e). Setiap elemen adalah fungsi. Anda meneruskan prop, dan Anda menempatkan elemen dan string yang ada di dalamnya dalam children.
Sebagian besar gambar menggunakan empat elemen. Pilih tab untuk melihat masing-masing dan cara terminal menggambarnya:
Text menggambar string, dengan gaya opsional seperti bold dan color:
Text({ children: ['This is the first tab.'] })
This is the first tab.
Box mengatur 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
Button adalah kontrol yang dapat ditekan pengguna. Ini menjalankan callback onPress Anda. Dengan plain: true tidak 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
Input adalah bidang teks. Ini menjalankan callback onSubmit Anda 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 Enter ⏎ add
Tabel ini mencantumkan setiap elemen:
| Elemen | Apa yang digambarnya | Di mana |
|---|---|---|
Box |
Kontainer flex. Mengambil prop tata letak seperti flexDirection, columnGap, padding, borderStyle, dan width. |
Di mana-mana |
Text |
Teks bergaya. Mengambil color, bold, dimColor, italic, dan wrap. color adalah kunci tema atau warna seperti 'red'. wrap adalah 'wrap', 'truncate', 'truncate-start', 'truncate-middle', atau 'truncate-end'. |
Di mana-mana |
Button |
Kontrol yang memanggil onPress |
Di mana-mana |
Link, Code, Markdown |
Tautan dengan href dan label opsional, blok kode, dan teks yang diformat seperti balasan Claude. Markdown mengambil kontennya dalam prop text, bukan dalam children, dan membutuhkan key saat Anda meneruskan onLinkPress. |
Di mana-mana |
Input, Select |
Bidang teks dan pemilih | Terminal, Desktop |
Svg |
Dokumen SVG | Desktop |
Client |
Wilayah yang digambar oleh file kedua Anda, untuk animasi dan input pointer. File itu tidak mendapat API mods. Itu mencapai hook Anda hanya dengan memposting data, yang tiba sebagai event ui.message. |
Terminal, Desktop |
Raster, Image |
Grid sel berwarna, dan gambar | Terminal |
Jika modul Anda adalah file .tsx atau .jsx, Anda dapat menulis pohon sebagai JSX. Dekonstruksi elemen dari $.ui.resolve(e) terlebih dahulu, karena modul hooks tidak memiliki global elemen.
Jika pohon menggunakan elemen yang tidak dimiliki aplikasi, prop yang tidak diambil elemen, atau anak di mana tidak ada, Claude Code menggambar versinya sendiri dari situs.
Dalam sesi yang dimulai dengan --plugin-dir, baris transkrip mengatakan demikian, seperti ui.render (Pane) refused: Text prop "bogusProp" is not allowed; the engine drew its own. Debug log mencatatnya sebagai ui.render (Pane): a hook returned a tree that does not validate dengan alasan yang sama. Tidak ada yang lain muncul dalam sesi, jadi ketika gambar tidak muncul, periksa baris itu atau log.
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, yang mengemas setiap sel menjadi satu string. Setiap sel adalah tiga angka: titik kode karakter, warnanya, dan warna latar belakangnya. Warna adalah angka heksadesimal dengan dua digit masing-masing untuk merah, hijau, dan biru, seperti 0xc62828 untuk merah, atau 0x01000000 untuk default terminal.
Aplikasi Desktop tidak memiliki Raster, jadi periksa e.surface dan gambar teks di sana. Badan panel ini menggambar peta panas tiga kali dua:
// Nilai yang berarti "gunakan warna default terminal"
const DEFAULT_COLOR = 0x01000000
// Kemasan baris pasangan [karakter, warna] ke dalam satu string yang diambil Raster
// Satu sel adalah tiga angka: titik kode karakter, warnanya, dan warna latar belakangnya
function cellsOf(rows) {
const numbers = rows.flat().flatMap(([char, color]) => [char.codePointAt(0), color, DEFAULT_COLOR])
return new Uint8Array(Uint32Array.from(numbers).buffer).toBase64()
}
on('ui.render', { component: 'Pane' }, async ($, e, next) => {
// Gambar hanya di panel yang dibuka dengan id 'heat'
if (e.requestId !== 'heat') return next(e)
const { Box, Text, Raster } = $.ui.resolve(e)
// Dua baris tiga sel, masing-masing karakter blok dan warnanya
const rows = [
[['█', 0x2e7d32], ['█', 0xf9a825], ['█', 0xc62828]],
[['█', 0x2e7d32], ['█', 0x2e7d32], ['█', 0xf9a825]],
]
if (e.surface !== 'terminal') {
return Text({ children: ['The heat map needs the terminal.'] })
}
return Box({
flexDirection: 'column',
children: [Raster({ key: 'grid', columns: 3, rows: 2, cells: cellsOf(rows) })],
})
})
Di terminal, panel menampilkan grid:
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 fungsi yang Anda berikan kontrol itu, dan itu 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' }]
Tes menekan atau mengetik ke kontrol dengan key-nya, jadi berikan masing-masing satu. Setiap penggunaan kontrol juga menembakkan ui.press, ui.input, atau ui.select dengan key dalam e.element, dan mod lain dapat menghubungkan event tersebut. Hook-nya berjalan sebelum callback Anda, jadi itu melihat apa yang pengguna ketik ke Input Anda dan dapat mengubahnya atau menjawab sebagai pengganti callback Anda. API mods tidak memiliki metode yang menekan tombol mod lain.
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. Terlepas dari pintasan keyboard digit di pita, itu hanya terjadi saat panel atau pita Anda memiliki fokus keyboard. Sisa waktu, kunci pergi ke prompt.
Bagaimana panel mendapat fokus keyboard
Panel mendapat fokus keyboard dalam salah satu dari tiga cara:
- Mod Anda membukanya dengan
focus: truedari perintah atau penekanan - Pengguna menekan Ctrl+X kemudian Tab
- Pengguna mengkliknya
Claude Code memberikan focus: true hanya saat prompt kosong dan tidak ada yang lain memiliki fokus keyboard. Panel yang terbuka saat pengguna mengetik tidak mengambil keystroke mereka.
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. |
| Enter | Menekan Button yang fokus, mengirimkan Input yang fokus, atau memilih di Select |
| Pintasan keyboard tombol | Menekan tombol itu. Saat Input memiliki fokus, setiap kunci yang dapat dicetak pergi ke bidang. |
| Esc | Mengembalikan fokus keyboard ke prompt. Dengan closeOnEscape: true, itu juga menutup panel. |
Mod tidak dapat mengikat Tab atau tombol panah ke apa pun yang lain, jadi permainan mengarahkan dengan w, a, s, dan d.
Atur pintasan keyboard dan fokus pertama
Dua prop pada kontrol memutuskan bagaimana keyboard mencapainya:
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. Tinggalkan prop dari yang lain, karena Claude Code menolakautoFocus: false.
Bagaimana pintasan keyboard ditampilkan tergantung pada tombol dan aplikasi:
| Tombol | Di terminal | Di aplikasi Desktop |
|---|---|---|
| Dengan tanda kurung, default | [ Add one ], tanpa pintasan keyboard ditampilkan |
Label dengan kunci kecil di sebelahnya |
Dengan plain: true |
1: One |
Label dengan kunci kecil di sebelahnya |
Di terminal, beri nama kunci dalam label tombol berbingkai, atau gunakan plain: true, sehingga pengguna dapat melihat apa yang harus ditekan. Referensi elemen memiliki aturan Button lainnya: action, pintasan keyboard digit di pita, dan dua tombol pada satu pintasan keyboard.
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 │
╰──────────────────────────────────────────────────────────╯
Contoh menggunakan dua teknik:
- 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',
// Gambar bidang kosong setiap kali, yang menghapusnya setelah submit
value: '',
submitLabel: 'add',
autoFocus: true,
// Berjalan saat Anda menekan Enter di bidang
onSubmit: async (value) => {
// Abaikan baris kosong
if (!value.trim()) return
notes = [...notes, value.trim()]
redraw()
await $.store.set('notes', notes)
},
}),
// Satu baris untuk setiap catatan: tombol hapus, kemudian teks catatan
...notes.map((note, i) =>
Box({
flexDirection: 'row',
columnGap: 1,
children: [
Button({
// Kunci miliknya sendiri, jadi setiap tombol baris dapat dibedakan
key: 'delete-' + i,
label: 'x',
plain: true,
onPress: async () => {
notes = notes.filter((_, j) => j !== i)
redraw()
await $.store.set('notes', notes)
},
}),
Text({ children: [note] }),
],
}),
),
],
})
})
Untuk mencoba panel:
- Tambahkan catatan: ketik baris dan tekan Enter. Baris muncul sebagai baris baru, dan bidang kosong.
- Hapus catatan: tekan Tab sampai tombol
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.
Bidang kosong setelah setiap submit karena prop value-nya. value adalah teks yang dipegang bidang saat digambar, dan pengetikan pengguna menggantinya sampai hook Anda menggambar bidang lagi. Contoh selalu menggambar bidang dengan ''.
Contoh menyimpan catatan dan tidak memuatnya. Untuk membawanya kembali di sesi berikutnya, bacalah dalam hook session.start, cara hello-tabs membaca count.
Tiga prop membuat baris bidang, Note: Type a note and press Enter ⏎ add:
| Prop | Dalam contoh | Apa itu |
|---|---|---|
label |
Note |
Teks sebelum bidang. Terminal menggambar : setelahnya. |
placeholder |
Type a note and press Enter |
Teks redup yang ditampilkan saat bidang kosong |
submitLabel |
add |
Kata setelah ⏎ yang mengatakan apa yang dilakukan Enter |
Mengirimkan Input tidak memulai giliran kecuali callback Anda memanggil $.prompt.submit.
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. 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 sekarang menjalankan hook ui.render Anda sekali per detik. Timer berhenti saat modul dimuat ulang, dan salinan baru modul memulai miliknya sendiri.
Seberapa sering situs dapat digambar ulang
Claude Code membatasi seberapa sering itu menggambar ulang situs, jadi mod Anda dapat memanggil $.ui.invalidate sesering data berubah. Panel yang terlihat dan pita memiliki batas yang lebih tinggi daripada situs lainnya, dan tabel batas memiliki angkanya.
Panggilan yang datang lebih cepat dari batas digabungkan menjadi satu redraw. Redraw itu menjalankan hook Anda sekali, dan hook membaca data Anda seperti adanya saat itu, jadi nilai terbaru ditampilkan dan nilai di antaranya tidak. Animasi tidak dapat berjalan lebih cepat dari batas.
Jaga status
Mod memiliki tiga tempat untuk menyimpan nilai, dan mereka berbeda dalam berapa lama nilai bertahan: sampai modul dimuat ulang, sampai sesi berakhir, atau dari satu sesi ke sesi berikutnya. Pilih berdasarkan berapa lama nilai harus bertahan:
| Simpan di | Bertahan sampai | Gunakan untuk |
|---|---|---|
| Variabel tingkat modul | Modul dimuat ulang, yang terjadi setiap kali Anda menyimpan file selama pengembangan | Nilai yang dapat Anda hilangkan, seperti tab dalam hello-tabs |
$.state |
Sesi berakhir, atau pengguna menjalankan /clear, /resume, atau /branch |
Nilai yang gambar bergantung pada yang harus bertahan reload |
$.store |
Mod Anda menghapusnya, atau tidak ada sesi yang membaca atau menulis penyimpanan selama cleanupPeriodDays. Penyimpanan adalah penyimpanan kunci-nilai, disimpan sebagai file JSON milik plugin Anda sendiri di bawah ~/.claude/plugins/store/. |
Pengaturan, riwayat, apa pun yang diharapkan pengguna untuk ditemukan lagi |
$.store.get(key) diselesaikan ke nilai atau undefined, dan $.store.set(key, value) mengambil nilai JSON apa pun.
Simpan nilai dalam `$.state`
$.state menyimpan nilai untuk panjang sesi, dan itu menggambar ulang untuk Anda. Ini adalah status reaktif: hook ui.render yang membaca nilai berlangganan ke dalamnya, jadi Claude Code menggambar ulang situs itu setiap kali Anda menulis nilai, dan Anda tidak memanggil $.ui.invalidate. Nilai dalam $.state juga bertahan reload modul, yang variabel tidak.
Untuk mengaturnya, deklarasikan nilai Anda, arahkan manifest Anda ke deklarasi, kemudian tentukan dan gunakan setiap nilai. Contoh memindahkan count dari hello-tabs ke dalam $.state.
Deklarasikan nilai
Deklarasikan nilai dalam file tipe. Kunci luar adalah nama plugin Anda, dan setiap entri di bawahnya adalah nilai dan tipenya. Simpan ini sebagai hello-tabs/types/index.d.ts:
declare module 'claude-code' {
interface PluginState {
'hello-tabs': {
tab: 'one' | 'two'
count: number
}
}
}
Arahkan manifest ke deklarasi
Untuk membiarkan claude plugin validate memeriksa kode Anda terhadap file itu, tambahkan bidang types ke manifest dengan jalurnya:
{
"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"
}
Tentukan, baca, dan tulis nilai
Dalam modul Anda, tentukan setiap nilai dengan default, bacalah saat menggambar, dan tulislah dari callback. atom menamai nilai dan defaultnya, read mengembalikannya, dan update menulisnya. Tiga pembantu memanggil $.state.get dan $.state.set untuk Anda:
import { atom, read, update } from 'claude-code'
// Di atas modul: beri nama nilai dan berikan defaultnya
const count = atom({ plugin: 'hello-tabs', key: 'count' }, 0)
// Dalam hook ui.render: baca nilai untuk menggambarnya
const n = await read($, count)
// Dalam Tombol: tulis nilai baru dari yang lama
onPress: () => update($, count, (value) => value + 1)
Karena hook ui.render membaca count, Claude Code menjalankan hook lagi setiap kali tombol menulisnya.
Tiga aturan berlaku untuk kode:
- Tulis
plugindankeysebagai string literal:claude plugin validatemembacanya dari sumber Anda - Deklarasikan setiap nilai dalam file tipe: jika tidak, validasi gagal dengan
hello-tabs.count is not declared - Tulis dari callback atau hook event lain: hook
ui.renderdapat membaca status dan tidak dapat menulisnya, jadi tulis darionPress,onSubmit, atau hook untuk event lain
Ubah `hello-tabs` untuk menggunakan `$.state`
Untuk memindahkan count dalam hello-tabs ke dalam $.state, ubah setiap baris yang menggunakannya:
- Di atas modul: tambahkan baris
import, dan gantilet count = 0dengan barisatom - Dalam hook
ui.render: tambahkan barisreadsebelumtabButton, dan gambar'Count: ' + ndalamText - Dalam tombol Add one: ganti
onPressdengan yang dalam Simpan dari lebih dari satu sesi, yang menyimpan hitungan serta menulisnya - Dalam hook
session.start: ganti dua baris yang membacasaveddengan panggilanloadCountdari Muat nilai yang disimpan lagi setelah/clear
Simpan redraw untuk tombol tab, karena tab masih variabel.
Muat nilai yang disimpan lagi setelah `/clear`
Jika mod Anda menyalin nilai yang disimpan dari $.store ke dalam $.state pada session.start, itu harus menyalinnya lagi setelah /clear, /resume, atau /branch. Perintah tersebut mengembalikan setiap nilai $.state ke defaultnya, dan session.start tidak dipecat lagi. classic.SessionStart dipecat setelah masing-masing, dengan e.source diatur ke clear, resume, atau fork, jadi salin nilai lagi dalam hook di atasnya. Jika tidak, gambar Anda menampilkan default, dan callback yang menyimpan nilai $.state menulis default di atas apa yang Anda simpan.
Kode ini memuat count dari kedua hook. Itu dibangun di atas versi $.state dari hello-tabs, di mana count adalah atom dan update diimpor. Letakkan loadCount di atas register, dan tambahkan panggilan loadCount ke hook session.start yang sudah Anda miliki. classic.SessionStart juga dipecat saat startup dan setelah pemadatan, yang tidak mengatur ulang $.state, jadi filter pada source menjaga hook ke tiga reset:
// Salin hitungan yang disimpan dari $.store ke dalam $.state, atau 0 jika tidak ada yang disimpan
async function loadCount($) {
const saved = Number((await $.store.get('count')) ?? 0)
await update($, count, () => saved)
}
// Berjalan sebelum prompt pertama Anda, dan lagi setelah reload
on('session.start', async ($, e, next) => {
await loadCount($)
return next(e)
})
// Berjalan lagi setelah /clear, /resume, dan /branch, yang melaporkan fork
on('classic.SessionStart', { source: ['clear', 'resume', 'fork'] }, async ($, e, next) => {
await loadCount($)
return next(e)
})
Dengan kedua hook di tempat, panel menampilkan hitungan yang disimpan setelah /clear dan bukan 0, dan penekanan berikutnya dari Add one menambah ke hitungan yang disimpan.
loadCount menulis nilai yang disimpan di atas yang ada dalam $.state, dan session.start dipecat lagi setiap kali modul dimuat ulang. Untuk menjaga penyimpanan agar tidak tertinggal, simpan pada setiap perubahan, seperti yang dilakukan tombol Add one.
Untuk memeriksa reload tanpa sesi, uji gambar setelah /clear.
Simpan dari lebih dari satu sesi
Setiap sesi di mesin Anda yang menjalankan mod Anda berbagi satu $.store. get diikuti oleh set bukan atomik. Ketika dua sesi masing-masing membaca nilai, mengubahnya, dan menulisnya kembali, mereka bersaing, dan penulisan kedua menggantikan yang pertama.
Dua pilihan membuat itu kurang mungkin:
- Berikan setiap item kuncinya sendiri:
setmengubah hanya kuncinya sendiri, jadi sesi yang menulis kunci berbeda tidak menimpa satu sama lain - Baca lagi tepat sebelum Anda menulis: untuk nilai yang beberapa sesi ubah,
getkunci dalam callback dan bangun nilai baru dari itu, bukan dari salinan yang Anda muat padasession.start. Penulisan sesi lain masih hilang jika mendarat antaragetdansetAnda.
Tombol ini menambahkan satu ke apa pun yang disimpan sekarang, kemudian memperbarui gambar:
onPress: async () => {
// Baca apa yang disimpan sekarang, yang sesi lain mungkin telah ubah
const saved = Number((await $.store.get('count')) ?? 0)
// Simpan hitungan baru, kemudian tampilkan
await $.store.set('count', saved + 1)
await update($, count, () => saved + 1)
}
Jika sesi kedua telah menekan tombolnya sendiri tiga kali sejak sesi ini dimulai, penekanan ini menampilkan dan menyimpan hitungan yang mencakup ketiga itu.
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