Mit einem Mod in der Benutzeroberfläche zeichnen
Zeichnen Sie Panes, ein Band über der Eingabeaufforderung, Schaltflächen und Textfelder aus einem Claude Code Mod, verarbeiten Sie Drücke und Eingaben, und behalten Sie den Status zwischen Neuzeichnungen und Sitzungen bei.
Ein Mod kann seine eigene Benutzeroberfläche in Claude Code zeichnen und Teile der Benutzeroberfläche ändern, die Claude Code bereits zeichnet. Jeder Ort, an dem ein Mod zeichnen kann, wird als Render-Site bezeichnet, z. B. ein Pane, das Band über der Eingabeaufforderung oder der Spinner. Claude Code löst das ui.render Ereignis jedes Mal aus, wenn es eine Render-Site zeichnen möchte, und Ihr Hook für dieses Ereignis gibt zurück, was dort gezeichnet werden soll.
Diese Karte zeigt, wo ein Mod in einer Terminal-Sitzung zeichnen kann:
In einem schmaleren Terminal sitzt das Pane über der Eingabeaufforderung statt neben dem Transkript.
Erstellen Sie Ihren ersten Mod, bevor Sie hier beginnen. Beginnen Sie mit dem durchgearbeiteten Beispiel, das ein Pane mit zwei Registerkarten und einem Zähler erstellt, lesen Sie dann den Abschnitt für jeden Teil, den Sie ändern möchten.
Um eine Eigenschaft oder ein Limit nachzuschlagen, siehe die Referenz.
Erstellen Sie ein Pane mit Registerkarten
In diesem Abschnitt erstellen Sie einen Mod, der einen /hello-tabs Befehl hinzufügt, und der Befehl öffnet ein Pane. Ein Pane ist eine Seitenleiste neben dem Transkript in einem breiten Vollbildterminal oder eine gerahmte Region über der Eingabeaufforderung. Dieses Pane zeigt zwei Registerkarten, und die zweite Registerkarte hat eine Schaltfläche, die eins zu einem Zähler addiert. Die Anzahl ist immer noch da, nachdem Sie Claude Code neu starten.
Der fertige Mod sieht so aus. Die Aufzeichnung öffnet das Pane, wechselt zur zweiten Registerkarte, drückt die Schaltfläche ein paar Mal und kehrt zur ersten Registerkarte zurück:
Claude Code hat kein integriertes Registerkarten-Element, daher sind die Registerkarten zwei Schaltflächen in einer Reihe. Der Mod verfolgt, welche aktiv ist, und zeichnet den Inhalt dieser Registerkarte unter der Reihe.
Erstellen Sie das Plugin
Ein Mod ist ein Plugin mit einem Manifest, einer hooks.json, die auf Ihren Code verweist, und der Codedatei. Erstellen Sie einen Mod erklärt jeden. Erstellen Sie ein Verzeichnis namens hello-tabs mit .claude-plugin und hooks Verzeichnissen darin, speichern Sie dann die ersten zwei Dateien.
Speichern Sie das Manifest als 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" }
}
Benennen Sie Ihren Einstiegspunkt in hello-tabs/hooks/hooks.json:
{
"modules": ["./register.js"]
}
Schreiben Sie den Code
Der Code führt drei Aufgaben aus, eine in jedem Hook:
- Fügt den
/hello-tabsBefehl hinzu - Öffnet das Pane, wenn Sie diesen Befehl ausführen
- Zeichnet den Inhalt des Pane: die Reihe von Registerkarten und den Hauptteil der offenen Registerkarte
Zwei Variablen auf Modulebene, tab und count, halten den Status des Pane.
Speichern Sie dies als hello-tabs/hooks/register.js:
// The pane's id, used to open the pane and to recognize it when drawing
const PANE = 'hello-tabs'
// What the pane shows: which tab is open, and the counter's value
let tab = 'one'
let count = 0
export function register(on) {
// Runs before your first prompt, and again after a reload
on('session.start', async ($, e, next) => {
await $.command.register({ name: 'hello-tabs', description: 'Open the hello-tabs pane' })
// Load the count an earlier session saved, if there is one
const saved = await $.store.get('count')
if (typeof saved === 'number') count = saved
return next(e)
})
// Runs when you type /hello-tabs
on('command.run', { command: 'hello-tabs' }, async ($) => {
// Open the pane, give it the keyboard, and let Esc close it
await $.ui.open({ id: PANE, title: 'Hello tabs', focus: true, closeOnEscape: true })
// Print nothing in the transcript
return {}
})
// Runs each time Claude Code draws a pane
on('ui.render', { component: 'Pane' }, async ($, e, next) => {
// Leave other mods' panes alone
if (e.requestId !== PANE) return next(e)
// Get the elements this app can draw
const { Box, Text, Button } = $.ui.resolve(e)
// Ask Claude Code to run this hook again
const redraw = () => $.ui.invalidate('ui.render')
// One tab: a button that switches to its tab when pressed
const tabButton = (name, label, hotkey) =>
Button({
key: 'tab-' + name,
label,
hotkey,
plain: true,
// Dim the tab that isn't open
dimColor: tab !== name,
onPress: () => {
tab = name
redraw()
},
})
// What goes under the tabs, depending on which one is open
const body =
tab === 'one'
? [Text({ children: ['This is the first tab.'] })]
: [
Box({
flexDirection: 'row',
columnGap: 2,
children: [
Button({
key: 'more',
label: 'Add one',
hotkey: 'a',
onPress: async () => {
count += 1
redraw()
// Save the count so it's there after a restart
await $.store.set('count', count)
},
}),
Text({ children: ['Count: ' + count] }),
],
}),
]
// The whole pane: the row of tabs, a blank line, then the body
return Box({
flexDirection: 'column',
children: [
Box({
flexDirection: 'row',
columnGap: 3,
children: [tabButton('one', 'One', '1'), tabButton('two', 'Two', '2')],
}),
Text({ children: [' '] }),
...body,
],
})
})
}
Jeder Hook führt auch etwas aus, das der Code nicht deutlich macht:
session.startliest auch die gespeicherte Anzahl aus$.store, einem Schlüssel-Wert-Speicher, der zwischen Sitzungen bestehen bleibt.command.runteilt Claude Code nur mit, dass das Pane existiert. Das Öffnen eines Pane zeichnet nichts von selbst: Claude Code löst dannui.renderaus, um zu fragen, was darin geht.ui.rendergibt den Element-Baum zurück, einBox, das andere Boxen, Text und Schaltflächen enthält, und erstellt ihn jedes Mal austabundcountneu.
Das Drücken einer Schaltfläche führt ihren onPress Callback aus, der eine Variable ändert und redraw aufruft. Claude Code führt dann den ui.render Hook erneut aus, und der Hook erstellt einen neuen Baum aus den neuen Werten. Jede interaktive Zeichnung verwendet diesen Render-Zyklus: Ein Callback ändert den Status, und der Hook zeichnet aus dem neuen Status neu.
Öffnen Sie das Pane
Starten Sie Claude Code in Ihrer Shell mit claude --plugin-dir ./hello-tabs. Führen Sie an der Claude Code Eingabeaufforderung /hello-tabs aus. Ein Pane öffnet sich mit 1: One und 2: Two oben. Drücken Sie 2, dann drücken Sie a, die Hotkey für Add one, ein paar Mal. Die Anzahl steigt.
Überprüfen Sie, dass die Anzahl gespeichert wurde
Drücken Sie Esc, um das Pane zu schließen, und beenden Sie dann die Sitzung. Starten Sie Claude Code in Ihrer Shell erneut mit dem gleichen claude --plugin-dir ./hello-tabs Befehl, und führen Sie an der Claude Code Eingabeaufforderung /hello-tabs aus. Die Anzahl ist dort, wo Sie sie gelassen haben.
Um die Anzahl zu löschen, lassen Sie den Mod $.store.delete('count') aufrufen. Behalten Sie den Status behandelt, wie lange jede Art von Wert dauert.
Wählen Sie, wo Sie zeichnen möchten
Ein ui.render Hook läuft für jede Render-Site, es sei denn, Sie grenzen ihn auf die gewünschte ein. Um die Render-Site auszuwählen, übergeben Sie einen Filter, genannt Matcher, als zweites Argument an on. { component: 'Pane' } führt den Hook nur für Panes aus. Im Hook benennt e.component die Site, e.surface sagt, welche App zeichnet, und e.props enthält die eigenen Daten der Site. Für ein Pane ist e.requestId die id, mit der Sie es geöffnet haben.
Zwei Sites sind leer, bis ein Mod sie ausfüllt, das Pane und das Band. Wählen Sie eine Registerkarte, um zu sehen, was jede ist und wie man darin zeichnet:
Ein Pane ist eine Seitenleiste neben dem Transkript in einem breiten Vollbildterminal oder eine gerahmte Region über der Eingabeaufforderung. Mit mehreren offenen Panes erhält jedes eine Registerkarte, die seinen Titel anzeigt.
Ein Pane erscheint, wenn Ihr Mod $.ui.open mit einer id aufruft, die Sie wählen, wie in $.ui.open({ id: 'hello-tabs' }). Öffnen Sie ein Pane zum richtigen Zeitpunkt behandelt die anderen Felder und wann ein Pane auf ein breiteres Terminal wartet.
Um in Ihrem Pane zu zeichnen, filtern Sie auf { component: 'Pane' } und überprüfen Sie, dass e.requestId Ihre id ist.
Das Band ist ein Streifen direkt über der Eingabeaufforderung. Es ist immer da, und jeder Mod teilt es sich.
Ihr Hook gibt einen Baum zurück, um etwas im Band zu zeigen, oder next(e), um nichts zu zeigen. Ein Baum ersetzt das, was die Mods nach Ihrem dort zeichnen. Um das Ihre zu behalten, setzen Sie das Ergebnis von await next(e) unter die Kinder eines Box in Ihrem Baum.
Um im Band zu zeichnen, filtern Sie auf { component: 'AbovePrompt' }.
Ändern Sie, was Claude Code bereits zeichnet
Claude Code zeichnet den Großteil seiner Benutzeroberfläche selbst: Nachrichten, Tool-Call-Zeilen, den Spinner und mehr. Jeder dieser Teile ist auch eine Render-Site, daher kann ein Mod ihn umgestalten oder ersetzen. Um einen zu ändern, filtern Sie Ihren ui.render Hook auf seinen Namen aus dieser Tabelle:
| Site | Was es ist |
|---|---|
UserMessage, AssistantMessage |
Eine Nachricht im Transkript |
ToolUse, ToolResult, ToolGroup |
Die Zeile eines Tool-Calls, sein Ergebnis und eine gefaltete Reihe von Calls |
CommandOutput |
Die Zeile, die ein Befehl gedruckt hat |
AskUserQuestion |
Der Dialog, den Claude öffnet, um Sie eine Frage zu stellen |
Spinner, ToolProgress, TurnDuration |
Statuszeilen für einen Turn: die Zeile, die animiert wird, während Claude arbeitet, eine laufende Tool-Fortschrittszeile und die Zeile, die einen Turn schließt |
InfoNotice, SessionMode, PromptHint |
Statuszeilen unter dem Logo, die Modusbezeichnungen in der Fußzeile und die Hinweiszeile unter der Eingabeaufforderung |
An einer Site, die Claude Code bereits zeichnet, hat Ihr Hook drei Möglichkeiten: ein Detail ändern, die Zeichnung ersetzen oder sie allein lassen. Wählen Sie eine Registerkarte, um jede auf den Spinner angewendet zu sehen. Die Beispiele lesen eine calls Variable, die ein anderer Hook zählt, wie im Tutorial-Mod.
Um Claude Codes Zeichnung zu behalten und einen Teil davon zu ändern, übergeben Sie next eine Kopie des Ereignisses mit geänderten props. Dieser Hook ändert den Text nach dem Wort des Spinners:
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 + '…' } })
})
Der Spinner behält seine Animation und sein Wort, und Ihr Text folgt dem Wort:
Thinking · tool calls: 2…
Um etwas Eigenes an der Stelle des Spinners zu zeichnen, geben Sie einen Baum zurück und rufen Sie next nicht auf. Dieser Hook zeichnet eine Textzeile, wo der Spinner wäre:
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'] })
})
Während Claude arbeitet, zeigt Ihre Zeile und Claude Codes Spinner nicht:
Claude has made 2 tool calls
Um die Site so zu lassen, wie Claude Code sie zeichnet, geben Sie next(e) zurück. Ein Hook tut das oft für einige Ereignisse und nicht für andere. Dieser Hook lässt den Spinner allein, bis es einen Call zu zählen gibt:
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 + '…' } })
})
Vor dem ersten Tool-Call sieht der Spinner so aus, wie er ohne den Mod aussieht:
Thinking…
Die Berechtigungsaufforderung ist keine Render-Site, daher kann ein Mod nicht ändern, was sie zeigt. Der Frage-Dialog, AskUserQuestion, ist einer, daher kann ein Mod diesen ändern.
Das Terminal und die Desktop-App lösen nicht alle gleichen Sites aus. Pane, AbovePrompt, Spinner und die Transkript-Sites funktionieren in beiden. Ein paar andere Statuszeilen werden nur im Terminal ausgelöst. Die Render-Sites-Tabelle listet auf, wo jede ausgelöst wird.
Öffnen Sie ein Pane zum richtigen Zeitpunkt
Ein Pane erscheint nur, wenn Ihr Mod es öffnet. Wie und wann Sie es öffnen, entscheidet, ob es den Tastaturfokus erhält, wie viel Platz es anfordert und ob es überhaupt in einem schmalen Terminal angezeigt wird.
Um ein Pane zu öffnen, rufen Sie $.ui.open mit einer id auf, die Sie wählen. Die id ist der Name des Pane: Ihr ui.render Hook überprüft sie, und Sie übergeben sie erneut, um das Pane zu schließen.
await $.ui.open({ id: 'hello-tabs', title: 'Hello tabs', focus: true })
Um das Pane zu schließen, rufen Sie $.ui.close mit der id auf, mit der Sie es geöffnet haben:
await $.ui.close({ id: 'hello-tabs' })
Neben id nimmt $.ui.open diese optionalen Felder:
| Feld | Was es tut |
|---|---|
title |
Die Registerkarte des Pane, wenn mehr als ein Pane offen ist |
focus |
Fordert Tastaturfokus an |
closeOnEscape |
Macht Esc das Pane schließen. Übergeben Sie true oder lassen Sie das Feld weg, da Claude Code false ablehnt. |
holdToasts |
Hält Toasts, die kleinen Mitteilungen von $.ui.toast, bis das Pane schließt |
rows |
Die Höhe, die angefordert wird, wenn das Pane über der Eingabeaufforderung sitzt. Der Standard ist ein Drittel des Platzes. |
columns |
Die Breite, die angefordert wird, wenn das Pane neben dem Transkript sitzt |
Um einem Befehl zu ermöglichen, das Pane zu öffnen, während Claude arbeitet, fügen Sie immediate: true hinzu, wenn Sie den Befehl registrieren. Ohne ihn wartet ein Befehl, der während eines Turns eingegeben wird, bis der Turn endet.
Wenn ein Pane auf ein breiteres Terminal wartet
Ein Pane, das Ihr Mod öffnet, ohne gefragt zu werden, erscheint nicht in einem schmalen Terminal, daher kann es einen kleinen Bildschirm nicht übernehmen. Ob es erscheint, hängt davon ab, was es geöffnet hat:
- Geöffnet durch etwas, das der Benutzer getan hat, z. B. ein Befehl, den er ausgeführt hat, oder eine Schaltfläche, die er gedrückt hat, das Pane erscheint bei jeder Breite
- Geöffnet durch Ihren Mod, der von selbst handelt, z. B. von einem Timer oder einem
turn.startHook, das Pane erscheint nur in einem Terminal mit mindestens 144 Spalten Breite. Nachdem der Benutzer dieses Pane einmal selbst geöffnet hat, reichen 110 Spalten aus.
Wenn das Pane erscheint, wird $.ui.open zu { isPlaced: true } aufgelöst. Wenn das Pane wartet, ist isPlaced false und reason ist ein String, der sagt, warum. Ein wartendes Pane erscheint, wenn der Benutzer es öffnet oder das Terminal verbreitert. Um zu sagen, dass etwas verfügbar ist, ohne ein Pane zu öffnen, rufen Sie $.ui.toast('Your message') auf, das eine kleine Mitteilung zeigt, die nach ein paar Sekunden verschwindet.
Erstellen Sie einen Baum aus Elementen
Was ein ui.render Hook zurückgibt, ist ein Element-Baum: eine Beschreibung dessen, was zu zeichnen ist, bestehend aus Boxen, Text und Steuerelementen, die ineinander verschachtelt sind. Sie beschreiben die Zeichnung, und Claude Code rendert sie im Terminal oder der Desktop-App.
Um die Elemente zu erhalten, rufen Sie $.ui.resolve(e) in Ihrem Hook auf, wie in const { Box, Text, Button } = $.ui.resolve(e). Jedes Element ist eine Funktion. Sie übergeben ihr Props, und Sie setzen die Elemente und Strings, die darin gehen, in children.
Die meisten Zeichnungen verwenden vier Elemente. Wählen Sie eine Registerkarte, um jedes zu sehen und wie das Terminal es zeichnet:
Text zeichnet einen String mit optionalem Styling wie bold und color:
Text({ children: ['This is the first tab.'] })
This is the first tab.
Box ordnet an, was darin ist, in einer Reihe oder einer Spalte. Diese setzt eine Schaltfläche und eine Textzeile nebeneinander, zwei Spalten auseinander:
Box({
flexDirection: 'row',
columnGap: 2,
children: [
Button({ key: 'more', label: 'Add one', onPress: addOne }),
Text({ children: ['Count: 0'] }),
],
})
[ Add one ] Count: 0
Button ist ein Steuerelement, das der Benutzer drücken kann. Es führt Ihren onPress Callback aus. Mit plain: true hat es keine Klammern und zeigt seine Hotkey:
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 ist ein Textfeld. Es führt Ihren onSubmit Callback mit dem Text aus, wenn der Benutzer Enter drückt:
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
Diese Tabelle listet jedes Element auf:
| Element | Was es zeichnet | Wo |
|---|---|---|
Box |
Ein Flex-Container. Nimmt Layout-Props wie flexDirection, columnGap, padding, borderStyle und width. |
Überall |
Text |
Gestylter Text. Nimmt color, bold, dimColor, italic und wrap. Ein color ist ein Theme-Schlüssel oder eine Farbe wie 'red'. Ein wrap ist 'wrap', 'truncate', 'truncate-start', 'truncate-middle' oder 'truncate-end'. |
Überall |
Button |
Ein Steuerelement, das onPress aufruft |
Überall |
Link, Code, Markdown |
Ein Link mit href und einem optionalen label, ein Codeblock und Text, der so formatiert ist wie Claude's Antworten. Markdown nimmt seinen Inhalt in einem text Prop, nicht in children, und braucht einen key, wenn Sie onLinkPress übergeben. |
Überall |
Input, Select |
Ein Textfeld und ein Picker | Terminal, Desktop |
Svg |
Ein SVG-Dokument | Desktop |
Client |
Eine Region, die von einer zweiten Datei von Ihnen gezeichnet wird, für Animation und Zeigereingabe. Diese Datei erhält keine Mods API. Sie erreicht Ihre Hooks nur durch das Posten von Daten, die als ui.message Ereignis ankommen. |
Terminal, Desktop |
Raster, Image |
Ein Gitter von farbigen Zellen und ein Bild | Terminal |
Wenn Ihr Modul eine .tsx oder .jsx Datei ist, können Sie den Baum als JSX schreiben. Destrukturieren Sie die Elemente zuerst aus $.ui.resolve(e), da ein Hooks-Modul keine Element-Globals hat.
Wenn ein Baum ein Element verwendet, das die App nicht hat, ein Prop, das ein Element nicht nimmt, oder ein Kind, wo keines geht, zeichnet Claude Code seine eigene Version der Site.
In einer Sitzung, die mit --plugin-dir gestartet wurde, sagt eine Transkriptzeile so, wie ui.render (Pane) refused: Text prop "bogusProp" is not allowed; the engine drew its own. Das Debug-Protokoll zeichnet es als ui.render (Pane): a hook returned a tree that does not validate mit dem gleichen Grund auf. Nichts anderes erscheint in der Sitzung, also wenn eine Zeichnung nicht angezeigt wird, überprüfen Sie diese Zeile oder das Protokoll.
Zeichnen Sie ein Gitter von farbigen Zellen
Für eine Wärmekarte, eine Sparkline oder ein Spielbrett im Terminal zeichnen Sie ein Raster und nicht ein Box für jede Zelle. Ein Raster nimmt einen key, seine Größe in columns und rows und cells, die jede Zelle in einen String packt. Jede Zelle sind drei Zahlen: der Code-Punkt des Zeichens, seine Farbe und seine Hintergrundfarbe. Eine Farbe ist eine Hexadezimalzahl mit zwei Ziffern jeweils für Rot, Grün und Blau, wie 0xc62828 für ein Rot oder 0x01000000 für das Standard des Terminals.
Die Desktop-App hat kein Raster, also überprüfen Sie e.surface und zeichnen Sie dort Text. Dieser Pane-Hauptteil zeichnet eine drei mal zwei Wärmekarte:
// 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) })],
})
})
Im Terminal zeigt das Pane das Gitter:
Das rows Array ist der Teil, den Sie ändern würden, und cellsOf verwandelt es in den gepackten String. Der Hook zeichnet nur in einem Pane, dessen id heat ist, also öffnen Sie eines mit $.ui.open({ id: 'heat' }) von einem Befehl, wie das hello-tabs Beispiel sein Pane öffnet.
Jedes Zeichen muss eine Zelle breit sein. Um ein Raster, das bereits auf dem Bildschirm ist, zu animieren, rufen Sie $.ui.blit mit der id des Pane als requestId, dem key des Raster, der gleichen Größe und neuen Zellen auf. Für dieses Beispiel ist das $.ui.blit({ requestId: 'heat', key: 'grid', columns: 3, rows: 2, cells: cellsOf(newRows) }). Es malt dieses eine Element neu, ohne Ihren ui.render Hook erneut auszuführen.
Reagieren Sie auf Drücke und Eingaben
Wenn der Benutzer eine Schaltfläche drückt, in ein Feld tippt oder aus einer Liste auswählt, die Ihr Mod gezeichnet hat, ruft Claude Code die Funktion auf, die Sie diesem Steuerelement gegeben haben, und sie läuft in Ihrem Modul. Jedes Steuerelement nimmt seine eigenen Callbacks:
Button: nimmtonPress(e), wobeie.surfacedie App ist, von der der Druck kamInput: nimmtonSubmit(value)undonInput(value)Select: nimmtonSelect(value)mit seinen Auswahlmöglichkeiten inoptions, eine Liste von mindestens einer Auswahlmöglichkeit mit eindeutigen Werten, wie[{ value: 'sm', label: 'Small' }, { value: 'lg', label: 'Large' }]
Ein Test drückt oder tippt in ein Steuerelement nach seinem key, also geben Sie jedem einen. Jede Verwendung eines Steuerelements löst auch ui.press, ui.input oder ui.select mit dem key in e.element aus, und ein anderer Mod kann diese Ereignisse hooken. Sein Hook läuft vor Ihrem Callback, daher sieht er, was der Benutzer in Ihren Input tippt und kann es ändern oder an Stelle Ihres Callbacks antworten. Die Mods API hat keine Methode, die die Schaltfläche eines anderen Mods drückt.
Tastaturfokus und Hotkeys
Ihr Mod liest die Tastatur nie selbst. Der Benutzer drückt eine Taste, Claude Code entscheidet, welches Ihrer Steuerelemente es ist, und der Callback dieses Steuerelements läuft. Abgesehen von einer Ziffern-Hotkey auf dem Band passiert das nur, während Ihr Pane oder Band den Tastaturfokus hat. Der Rest der Zeit gehen Tasten zur Eingabeaufforderung.
Wie ein Pane den Tastaturfokus erhält
Ein Pane erhält den Tastaturfokus auf eine von drei Arten:
- Ihr Mod öffnet es mit
focus: truevon einem Befehl oder einem Druck - Der Benutzer drückt Ctrl+X dann Tab
- Der Benutzer klickt darauf
Claude Code gewährt focus: true nur, während die Eingabeaufforderung leer ist und nichts anderes den Tastaturfokus hat. Ein Pane, das sich öffnet, während der Benutzer tippt, nimmt seine Tastenanschläge nicht.
Was jede Taste tut
Diese Tabelle listet auf, was eine Taste tut, während Ihr Pane oder Band den Tastaturfokus hat:
| Taste | Was es tut |
|---|---|
| Tab | Bewegt sich zum nächsten Steuerelement |
| Auf und Ab | Bewegen Sie sich zwischen Steuerelementen, während Ihre Zeichnung passt. Wenn das Pane oder Band mehr Reihen hat, als es anzeigen kann, scrollen sie es. |
| Enter | Drückt die fokussierte Button, sendet die fokussierte Input oder wählt in einem Select |
| Die Hotkey einer Schaltfläche | Drückt diese Schaltfläche. Während ein Input den Fokus hat, geht jede druckbare Taste zum Feld. |
| Esc | Gibt den Tastaturfokus zur Eingabeaufforderung zurück. Mit closeOnEscape: true schließt es auch das Pane. |
Ein Mod kann Tab oder die Pfeiltasten nicht an etwas anderes binden, daher steuert ein Spiel mit w, a, s und d.
Setzen Sie eine Hotkey und den ersten Fokus
Zwei Props auf einem Steuerelement entscheiden, wie die Tastatur es erreicht:
hotkey: um dem Benutzer zu ermöglichen, eineButtonmit einer Taste zu drücken, geben Sie ihr einehotkeyvon einer Ziffer oder einem Kleinbuchstaben, wie inhotkey: 'a'autoFocus: um zu wählen, welches Steuerelement den Fokus hat, wenn das Pane sich öffnet, fügen SieautoFocus: truehinzu. Lassen Sie das Prop bei den anderen weg, da Claude CodeautoFocus: falseablehnt.
Wie eine Hotkey angezeigt wird, hängt von der Schaltfläche und der App ab:
| Schaltfläche | Im Terminal | In der Desktop-App |
|---|---|---|
| Mit Klammern, der Standard | [ Add one ], ohne Hotkey angezeigt |
Das Label mit einem kleinen Schlüssel daneben |
Mit plain: true |
1: One |
Das Label mit einem kleinen Schlüssel daneben |
Im Terminal benennen Sie die Taste im Label einer geklammerten Schaltfläche oder verwenden Sie plain: true, damit der Benutzer sehen kann, was zu drücken ist. Die Elements-Referenz hat die anderen Button Regeln: action, Ziffern-Hotkeys auf dem Band und zwei Schaltflächen auf einer Hotkey.
Nehmen Sie eingegebenen Text und zeichnen Sie eine Reihe für jedes Element
Viele Panes sind ein Textfeld mit einer Liste darunter. Das Beispiel in diesem Abschnitt ist ein Notizen-Pane: Sie tippen eine Notiz und drücken Enter, um sie hinzuzufügen, und jede Notiz hat eine x Schaltfläche, die sie löscht. Mit zwei hinzugefügten Notizen zeichnet das Terminal das Pane so:
╭──────────────────────────────────────────────────────────╮
│ Note: Type a note and press Enter ⏎ add ✕ │
│ x buy milk │
│ x call bob │
╰──────────────────────────────────────────────────────────╯
Das Beispiel verwendet zwei Techniken:
- Nehmen Sie eingegebenen Text: ein
InputruftonSubmit(value)mit dem Text des Feldes auf, wenn der Benutzer Enter drückt, undonInput(value)bei jeder Änderung - Zeichnen Sie eine Liste: ordnen Sie Ihre Daten einer Reihe jeweils zu, und geben Sie jedem
keyder Reihe seine eigene
Dieser Hook zeichnet den Inhalt des Pane:
// The list the pane draws
let notes = []
on('ui.render', { component: 'Pane' }, async ($, e, next) => {
// Draw only in the pane opened with the id 'notes'
if (e.requestId !== 'notes') return next(e)
const { Box, Text, Button, Input } = $.ui.resolve(e)
const redraw = () => $.ui.invalidate('ui.render')
return Box({
flexDirection: 'column',
children: [
Input({
key: 'new-note',
label: 'Note',
placeholder: 'Type a note and press Enter',
// Draw the field empty each time, which clears it after a submit
value: '',
submitLabel: 'add',
autoFocus: true,
// Runs when you press Enter in the field
onSubmit: async (value) => {
// Ignore an empty line
if (!value.trim()) return
notes = [...notes, value.trim()]
redraw()
await $.store.set('notes', notes)
},
}),
// One row for each note: a delete button, then the note's text
...notes.map((note, i) =>
Box({
flexDirection: 'row',
columnGap: 1,
children: [
Button({
// A key of its own, so each row's button can be told apart
key: 'delete-' + i,
label: 'x',
plain: true,
onPress: async () => {
notes = notes.filter((_, j) => j !== i)
redraw()
await $.store.set('notes', notes)
},
}),
Text({ children: [note] }),
],
}),
),
],
})
})
Um das Pane zu versuchen:
- Fügen Sie eine Notiz hinzu: tippen Sie eine Zeile und drücken Sie Enter. Die Zeile erscheint als neue Reihe, und das Feld leert sich.
- Löschen Sie eine Notiz: drücken Sie Tab, bis die
xSchaltfläche der Notiz den Fokus hat, dann drücken Sie Enter. Dasxist das Label der Schaltfläche und keine Hotkey, daher drückt das Tippen des Buchstabens es nicht.
Jede Änderung folgt dem gleichen Render-Zyklus wie hello-tabs: Der Callback ändert notes, ruft redraw auf und speichert die Liste in $.store.
Das Feld leert sich nach jedem Submit wegen seines value Props. value ist der Text, den das Feld hält, wenn es gezeichnet wird, und das Tippen des Benutzers ersetzt ihn, bis Ihr Hook das Feld erneut zeichnet. Das Beispiel zeichnet das Feld immer mit ''.
Das Beispiel speichert die Notizen und lädt sie nicht. Um sie in der nächsten Sitzung zurückzubringen, lesen Sie sie in einem session.start Hook, wie hello-tabs count liest.
Drei Props machen die Zeile des Feldes aus, Note: Type a note and press Enter ⏎ add:
| Prop | Im Beispiel | Was es ist |
|---|---|---|
label |
Note |
Der Text vor dem Feld. Das Terminal zeichnet : danach. |
placeholder |
Type a note and press Enter |
Schwacher Text, der angezeigt wird, während das Feld leer ist |
submitLabel |
add |
Das Wort nach ⏎, das sagt, was Enter tut |
Das Absenden eines Input startet keinen Turn, es sei denn, Ihr Callback ruft $.prompt.submit auf.
Zeichnen Sie eine Site neu
Eine Zeichnung ist ein Schnappschuss: Sie zeigt, was Ihr ui.render Hook das letzte Mal zurückgegeben hat, als der Hook lief. Um etwas Neues zu zeigen, muss der Hook erneut laufen. Claude Code führt ihn für einige Änderungen erneut aus, und Ihr Mod fragt nach dem Rest.
Wenn Claude Code ohne Aufforderung neu zeichnet
Claude Code führt Ihren ui.render Hook erneut aus, wenn sich die Props der Site ändern oder die Breite des Terminals ändert. Es führt den Hook nicht auf einem Timer aus, und es kann nicht sagen, wenn sich eine Variable in Ihrem Modul ändert.
Zeichnen Sie neu, wenn sich Ihre Daten ändern
Um Ihre Sites nach Ihren eigenen Datenänderungen erneut zu zeichnen, rufen Sie $.ui.invalidate('ui.render') auf. Dieses Pane zählt Drücke. Der Callback der Schaltfläche ändert count und fragt dann nach einem Neuzeichnen:
let count = 0
on('ui.render', { component: 'Pane' }, async ($, e, next) => {
if (e.requestId !== 'counter') return next(e)
const { Box, Text, Button } = $.ui.resolve(e)
return Box({
flexDirection: 'row',
columnGap: 2,
children: [
Button({
key: 'more',
label: 'Add one',
onPress: () => {
count += 1
// The data changed, so ask Claude Code to draw the pane again
$.ui.invalidate('ui.render')
},
}),
Text({ children: ['Count: ' + count] }),
],
})
})
Jeder Druck erhöht die Zahl im Pane. Das hello-tabs Beispiel wickelt den gleichen Aufruf in seine redraw Funktion.
Ein Wert, den Sie in $.state halten, braucht den Aufruf nicht, da das Schreiben des Wertes die Sites neu zeichnet, die ihn lesen.
Zeichnen Sie auf einem Timer neu
Um eine Uhr, einen Countdown oder einen Wert von außerhalb der Sitzung aktuell zu halten, zeichnen Sie nach einem Zeitplan neu. Starten Sie einen Timer im session.start Hook des Moduls. Wenn das Modul bereits einen hat, wie hello-tabs, fügen Sie die $.clock.every Zeile hinzu:
on('session.start', async ($, e, next) => {
// Every 1000 milliseconds, ask Claude Code to draw your sites again
$.clock.every(1000, () => $.ui.invalidate('ui.render'))
return next(e)
})
Claude Code führt jetzt Ihren ui.render Hook einmal pro Sekunde aus. Der Timer stoppt, wenn das Modul neu geladen wird, und die neue Kopie des Moduls startet ihren eigenen.
Wie oft eine Site neu gezeichnet werden kann
Claude Code begrenzt, wie oft es neu zeichnet, daher kann Ihr Mod $.ui.invalidate so oft aufrufen, wie sich seine Daten ändern. Das sichtbare Pane und das Band haben ein höheres Limit als andere Sites, und die Limits-Tabelle hat die Zahlen.
Aufrufe, die schneller als das Limit kommen, werden in einem Neuzeichnen kombiniert. Dieses Neuzeichnen führt Ihren Hook einmal aus, und der Hook liest Ihre Daten, wie sie in diesem Moment sind, daher zeigt der neueste Wert und die Werte dazwischen nicht. Eine Animation kann nicht schneller als das Limit laufen.
Behalten Sie den Status
Ein Mod hat drei Orte, um einen Wert zu halten, und sie unterscheiden sich darin, wie lange der Wert dauert: bis das Modul neu geladen wird, bis die Sitzung endet oder von einer Sitzung zur nächsten. Wählen Sie danach, wie lange der Wert dauern muss:
| Halten Sie es in | Es dauert bis | Verwenden Sie es für |
|---|---|---|
| Eine Modulebenen-Variable | Das Modul wird neu geladen, was jedes Mal passiert, wenn Sie eine Datei während der Entwicklung speichern | Werte, die Sie verlieren können, wie tab in hello-tabs |
$.state |
Die Sitzung endet oder der Benutzer führt /clear, /resume oder /branch aus |
Werte, die eine Zeichnung abhängt, die einen Reload überleben sollten |
$.store |
Ihr Mod löscht es oder keine Sitzung liest oder schreibt den Store für cleanupPeriodDays. Der Store ist ein Schlüssel-Wert-Speicher, gespeichert als JSON-Datei Ihres Plugins unter ~/.claude/plugins/store/. |
Einstellungen, Verlauf, alles, das der Benutzer das nächste Mal erwartet zu finden |
$.store.get(key) wird zu dem Wert oder undefined aufgelöst, und $.store.set(key, value) nimmt jeden JSON-Wert.
Behalten Sie einen Wert in `$.state`
$.state hält Werte für die Länge einer Sitzung, und es zeichnet für Sie neu. Es ist reaktiver Status: ein ui.render Hook, der einen Wert liest, abonniert ihn, daher zeichnet Claude Code diese Site jedes Mal neu, wenn Sie den Wert schreiben, und Sie rufen $.ui.invalidate nicht auf. Ein Wert in $.state überlebt auch ein Reload des Moduls, das eine Variable nicht tut.
Um es einzurichten, deklarieren Sie Ihre Werte, zeigen Sie Ihr Manifest auf die Deklaration, dann definieren und verwenden Sie jeden Wert. Die Beispiele verschieben den count aus hello-tabs in $.state.
Deklarieren Sie die Werte
Deklarieren Sie die Werte in einer Typendatei. Der äußere Schlüssel ist der Name Ihres Plugins, und jeder Eintrag darunter ist ein Wert und sein Typ. Speichern Sie dies als hello-tabs/types/index.d.ts:
declare module 'claude-code' {
interface PluginState {
'hello-tabs': {
tab: 'one' | 'two'
count: number
}
}
}
Zeigen Sie das Manifest auf die Deklaration
Um claude plugin validate zu lassen, Ihren Code gegen diese Datei zu überprüfen, fügen Sie ein types Feld zum Manifest mit seinem Pfad hinzu:
{
"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"
}
Definieren, lesen und schreiben Sie einen Wert
In Ihrem Modul definieren Sie jeden Wert mit einem Standard, lesen ihn beim Zeichnen und schreiben ihn von einem Callback. atom benennt einen Wert und seinen Standard, read gibt ihn zurück, und update schreibt ihn. Die drei Helfer rufen $.state.get und $.state.set für Sie auf:
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)
Da der ui.render Hook count gelesen hat, führt Claude Code den Hook jedes Mal erneut aus, wenn die Schaltfläche ihn schreibt.
Drei Regeln gelten für den Code:
- Schreiben Sie
pluginundkeyals Literal-Strings:claude plugin validateliest sie aus Ihrer Quelle - Deklarieren Sie jeden Wert in der Typendatei: sonst schlägt die Validierung mit
hello-tabs.count is not declaredfehl - Schreiben Sie von einem Callback oder einem anderen Ereignis-Hook: ein
ui.renderHook kann Status lesen und kann ihn nicht schreiben, daher schreiben Sie vononPress,onSubmitoder einem Hook für ein anderes Ereignis
Ändern Sie `hello-tabs`, um `$.state` zu verwenden
Um count in hello-tabs in $.state zu verschieben, ändern Sie jede Zeile, die ihn verwendet:
- Am Anfang des Moduls: fügen Sie die
importZeile hinzu, und ersetzen Sielet count = 0mit deratomZeile - Im
ui.renderHook: fügen Sie diereadZeile vortabButtonhinzu, und zeichnen Sie'Count: ' + nimText - Im Add one Button: ersetzen Sie
onPressmit dem aus Speichern Sie aus mehr als einer Sitzung, das die Anzahl speichert sowie schreibt - Im
session.startHook: ersetzen Sie die zwei Zeilen, diesavedlesen, mit demloadCountAufruf aus Laden Sie einen gespeicherten Wert erneut nach/clear
Behalten Sie redraw für die Tab-Schaltflächen, da tab immer noch eine Variable ist.
Laden Sie einen gespeicherten Wert erneut nach `/clear`
Wenn Ihr Mod einen gespeicherten Wert aus $.store in $.state bei session.start kopiert, muss er ihn nach /clear, /resume oder /branch erneut kopieren. Diese Befehle setzen jeden $.state Wert auf seinen Standard zurück, und session.start wird nicht erneut ausgelöst. classic.SessionStart wird nach jedem ausgelöst, mit e.source auf clear, resume oder fork gesetzt, daher kopieren Sie den Wert erneut in einem Hook darauf. Sonst zeigt Ihre Zeichnung den Standard, und ein Callback, der den $.state Wert speichert, schreibt den Standard über das, was Sie gespeichert haben.
Dieser Code lädt count aus beiden Hooks. Es baut auf der $.state Version von hello-tabs auf, wo count ein Atom ist und update importiert wird. Setzen Sie loadCount über register, und fügen Sie den loadCount Aufruf zum session.start Hook hinzu, den Sie bereits haben. classic.SessionStart wird auch beim Start und nach Verdichtung ausgelöst, was $.state nicht zurückgesetzt, daher hält der Filter auf source den Hook zu den drei Resets:
// 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)
})
Mit beiden Hooks an Ort und Stelle zeigt das Pane die gespeicherte Anzahl nach /clear und nicht 0, und der nächste Druck von Add one addiert zur gespeicherten Anzahl.
loadCount schreibt den gespeicherten Wert über den in $.state, und session.start wird jedes Mal ausgelöst, wenn das Modul neu geladen wird. Um den Store nicht hinter sich zu lassen, speichern Sie bei jeder Änderung, wie die Add one Schaltfläche tut.
Um den Reload ohne eine Sitzung zu überprüfen, testen Sie die Zeichnung nach /clear.
Speichern Sie aus mehr als einer Sitzung
Jede Sitzung auf Ihrem Computer, die Ihren Mod ausführt, teilt einen $.store. Ein get gefolgt von einem set ist nicht atomar. Wenn zwei Sitzungen jeweils einen Wert lesen, ihn ändern und zurückschreiben, rennen sie, und der zweite Schreib ersetzt den ersten.
Zwei Auswahlmöglichkeiten machen das weniger wahrscheinlich:
- Geben Sie jedem Element seinen eigenen Schlüssel: ein
setändert nur seinen eigenen Schlüssel, daher überschreiben Sitzungen, die verschiedene Schlüssel schreiben, sich nicht gegenseitig - Lesen Sie erneut direkt vor dem Schreiben: für einen Wert, den mehrere Sitzungen ändern,
getden Schlüssel im Callback und erstellen Sie den neuen Wert daraus, nicht aus einer Kopie, die Sie beisession.startgeladen haben. Ein Schreib einer anderen Sitzung geht immer noch verloren, wenn er zwischen Ihremgetund Ihremsetlandet.
Diese Schaltfläche addiert eins zu dem, was der Store jetzt hält, dann aktualisiert die Zeichnung:
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)
}
Wenn eine zweite Sitzung ihre eigene Schaltfläche drei Mal gedrückt hat, seit diese Sitzung gestartet wurde, zeigt dieser Druck und speichert eine Anzahl, die diese drei enthält.
Nächste Schritte
- Reagieren Sie auf Ereignisse: speisen Sie Ihre Zeichnung aus Tool-Calls und Turns
- Verwenden Sie die Mods API: speisen Sie Ihre Zeichnung aus Timern und Modell-Aufrufen
- Testen Sie eine Zeichnung: drücken Sie Ihre Schaltflächen aus einem Test auf mehr als einer Oberfläche
- Render-Sites und Elemente: jede Site's Props und jedes Element's Props