The two sides of this change are more than 400 edits apart, too far apart to line up, so this is the differ's own diff of it and the words inside a line are not marked.
from line 6
66
77This map shows where a mod can draw in a terminal session:
88
9<img src="https://mintcdn.com/claude-code/dgiVO_Od1X1faduV/images/mods-screen-map.svg?fit=max&auto=format&n=dgiVO_Od1X1faduV&q=85&s=5fda26b6609c62b68c6f9e528c1590ea" className="dark:hidden" alt="Map of a Claude Code terminal session in fullscreen rendering. A mod can add a pane as a sidebar on the right, a toast at the top right of the transcript, a log line in the transcript, a band above the prompt, and a status line under the prompt. A mod can redraw messages, tool call rows, and the spinner. The prompt is Claude Code's own." width="600" height="336" data-path="images/mods-screen-map.svg" />
10
11<img src="https://mintcdn.com/claude-code/dgiVO_Od1X1faduV/images/mods-screen-map-dark.svg?fit=max&auto=format&n=dgiVO_Od1X1faduV&q=85&s=5b4161581a1bd2c0450b0c8b57bc1225" className="hidden dark:block" alt="Map of a Claude Code terminal session in fullscreen rendering. A mod can add a pane as a sidebar on the right, a toast at the top right of the transcript, a log line in the transcript, a band above the prompt, and a status line under the prompt. A mod can redraw messages, tool call rows, and the spinner. The prompt is Claude Code's own." width="600" height="336" data-path="images/mods-screen-map-dark.svg" />
9
1210
1311In a narrower terminal, the pane sits above the prompt instead of beside the transcript.
1412
1513Build your [first mod](/docs/en/plugins/mods/create) before you start here. Begin with the worked example, which builds a pane with two tabs and a counter, then read the section for each piece you want to change.
1614
17<Note>
18 To look up one prop or limit, see the [reference](/docs/en/plugins/mods/reference#render-sites).
19</Note>
15> [!NOTE]
16>
17> To look up one prop or limit, see the [reference](/docs/en/plugins/mods/reference#render-sites).
2018
2119## Build a pane with tabs
2220
from line 22
2422
2523The finished mod looks like this. The recording opens the pane, switches to the second tab, presses the button a few times, and returns to the first tab:
2624
27<Frame>
28 <video autoPlay muted loop playsInline controls className="w-full dark:hidden" src="https://mintcdn.com/claude-code/dgiVO_Od1X1faduV/images/mods-hello-tabs-light.mp4?fit=max&auto=format&n=dgiVO_Od1X1faduV&q=85&s=49d520094d87b5b44bfe50fa49677f06" aria-label="The /hello-tabs command is typed at the Claude Code prompt and a framed pane opens above it, with '1: One' and '2: Two' across the top and the text 'This is the first tab.' The second tab shows an 'Add one' button beside 'Count: 1', and the count rises to 3. The pane then returns to the first tab." data-path="images/mods-hello-tabs-light.mp4" />
29
30 <video autoPlay muted loop playsInline controls className="w-full hidden dark:block" src="https://mintcdn.com/claude-code/dgiVO_Od1X1faduV/images/mods-hello-tabs-dark.mp4?fit=max&auto=format&n=dgiVO_Od1X1faduV&q=85&s=ff7a14d713d6e5d3b0000efa8522ea4b" aria-label="The /hello-tabs command is typed at the Claude Code prompt and a framed pane opens above it, with '1: One' and '2: Two' across the top and the text 'This is the first tab.' The second tab shows an 'Add one' button beside 'Count: 1', and the count rises to 3. The pane then returns to the first tab." data-path="images/mods-hello-tabs-dark.mp4" />
31</Frame>
25<video autoPlay muted loop playsInline controls src="https://mintcdn.com/claude-code/dgiVO_Od1X1faduV/images/mods-hello-tabs-light.mp4?fit=max&auto=format&n=dgiVO_Od1X1faduV&q=85&s=49d520094d87b5b44bfe50fa49677f06" aria-label="The /hello-tabs command is typed at the Claude Code prompt and a framed pane opens above it, with '1: One' and '2: Two' across the top and the text 'This is the first tab.' The second tab shows an 'Add one' button beside 'Count: 1', and the count rises to 3. The pane then returns to the first tab." />
3226
3327The tabs are two buttons in a row. The mod keeps track of which one is active and draws that tab's content under the row.
3428
35<Steps>
36 <Step title="Create the plugin">
37 A mod is a plugin with a manifest, a `hooks.json` that points to your code, and the code file. [Create a mod](/docs/en/plugins/mods/create#write-a-mod-yourself) explains each one. Create a directory named `hello-tabs` with `.claude-plugin` and `hooks` directories inside it, then save the first two files.
38
39 Save the manifest as `hello-tabs/.claude-plugin/plugin.json`:
40
41 ```json hello-tabs/.claude-plugin/plugin.json theme={null}
42 {
43 "name": "hello-tabs",
44 "version": "0.1.0",
45 "description": "Opens a pane with two tabs and a counter",
46 "author": { "name": "Your Name" }
47 }
48 ```
49
50 Name your entry point in `hello-tabs/hooks/hooks.json`:
51
52 ```json hello-tabs/hooks/hooks.json theme={null}
53 {
54 "modules": ["./register.js"]
55 }
56 ```
57 </Step>
58
59 <Step title="Write the code">
60 This list says what each hook does, in the order they appear in the code:
61
62 * Adds the `/hello-tabs` command, and loads the count an earlier session saved
63 * Opens the pane when you run that command
64 * Draws the pane's content: the row of tabs and the open tab's body
65
66 Two module-level variables, `tab` and `count`, hold the pane's state.
67
68 Save this as `hello-tabs/hooks/register.js`:
69
70 ```javascript hello-tabs/hooks/register.js theme={null}
71 // The pane's id, used to open the pane and to recognize it when drawing
72 const PANE = 'hello-tabs'
73
74 // What the pane shows: which tab is open, and the counter's value
75 let tab = 'one'
76 let count = 0
77
78 export function register(on) {
79 // Runs before your first prompt, and again after a reload
80 on('session.start', async ($, e, next) => {
81 await $.command.register({ name: 'hello-tabs', description: 'Open the hello-tabs pane' })
82 // Load the count an earlier session saved, if there is one
83 const saved = await $.store.get('count')
84 if (typeof saved === 'number') count = saved
85 return next(e)
29**1. Create the plugin**
30
31A mod is a plugin with a manifest, a `hooks.json` that points to your code, and the code file. [Create a mod](/docs/en/plugins/mods/create#write-a-mod-yourself) explains each one. Create a directory named `hello-tabs` with `.claude-plugin` and `hooks` directories inside it, then save the first two files.
32
33Save the manifest as `hello-tabs/.claude-plugin/plugin.json`:
34
35```json hello-tabs/.claude-plugin/plugin.json theme={null}
36{
37 "name": "hello-tabs",
38 "version": "0.1.0",
39 "description": "Opens a pane with two tabs and a counter",
40 "author": { "name": "Your Name" }
41}
42```
43
44Name your entry point in `hello-tabs/hooks/hooks.json`:
45
46```json hello-tabs/hooks/hooks.json theme={null}
47{
48 "modules": ["./register.js"]
49}
50```
51
52**2. Write the code**
53
54This list says what each hook does, in the order they appear in the code:
55
56* Adds the `/hello-tabs` command, and loads the count an earlier session saved
57* Opens the pane when you run that command
58* Draws the pane's content: the row of tabs and the open tab's body
59
60Two module-level variables, `tab` and `count`, hold the pane's state.
61
62Save this as `hello-tabs/hooks/register.js`:
63
64```javascript hello-tabs/hooks/register.js theme={null}
65// The pane's id, used to open the pane and to recognize it when drawing
66const PANE = 'hello-tabs'
67
68// What the pane shows: which tab is open, and the counter's value
69let tab = 'one'
70let count = 0
71
72export function register(on) {
73 // Runs before your first prompt, and again after a reload
74 on('session.start', async ($, e, next) => {
75 await $.command.register({ name: 'hello-tabs', description: 'Open the hello-tabs pane' })
76 // Load the count an earlier session saved, if there is one
77 const saved = await $.store.get('count')
78 if (typeof saved === 'number') count = saved
79 return next(e)
80 })
81
82 // Runs when you type /hello-tabs
83 on('command.run', { command: 'hello-tabs' }, async ($) => {
84 // Open the pane, give it the keyboard, and let Esc close it
85 await $.ui.open({ id: PANE, title: 'Hello tabs', focus: true, closeOnEscape: true })
86 // Print nothing in the transcript
87 return {}
88 })
89
90 // Runs each time Claude Code draws a pane
91 on('ui.render', { component: 'Pane' }, async ($, e, next) => {
92 // Leave other mods' panes alone
93 if (e.requestId !== PANE) return next(e)
94 // Get the elements this app can draw
95 const { Box, Text, Button } = $.ui.resolve(e)
96 // Ask Claude Code to run this hook again
97 const redraw = () => $.ui.invalidate('ui.render')
98
99 // One tab: a button that switches to its tab when pressed
100 const tabButton = (name, label, hotkey) =>
101 Button({
102 key: 'tab-' + name,
103 label,
104 hotkey,
105 plain: true,
106 // Dim the tab that isn't open
107 dimColor: tab !== name,
108 onPress: () => {
109 tab = name
110 redraw()
111 },
86112 })
87113
88 // Runs when you type /hello-tabs
89 on('command.run', { command: 'hello-tabs' }, async ($) => {
90 // Open the pane, give it the keyboard, and let Esc close it
91 await $.ui.open({ id: PANE, title: 'Hello tabs', focus: true, closeOnEscape: true })
92 // Print nothing in the transcript
93 return {}
94 })
95
96 // Runs each time Claude Code draws a pane
97 on('ui.render', { component: 'Pane' }, async ($, e, next) => {
98 // Leave other mods' panes alone
99 if (e.requestId !== PANE) return next(e)
100 // Get the elements this app can draw
101 const { Box, Text, Button } = $.ui.resolve(e)
102 // Ask Claude Code to run this hook again
103 const redraw = () => $.ui.invalidate('ui.render')
104
105 // One tab: a button that switches to its tab when pressed
106 const tabButton = (name, label, hotkey) =>
107 Button({
108 key: 'tab-' + name,
109 label,
110 hotkey,
111 plain: true,
112 // Dim the tab that isn't open
113 dimColor: tab !== name,
114 onPress: () => {
115 tab = name
116 redraw()
117 },
118 })
119
120 // What goes under the tabs, depending on which one is open
121 const body =
122 tab === 'one'
123 ? [Text({ children: ['This is the first tab.'] })]
124 : [
125 Box({
126 flexDirection: 'row',
127 columnGap: 2,
128 children: [
129 Button({
130 key: 'more',
131 label: 'Add one',
132 hotkey: 'a',
133 onPress: async () => {
134 count += 1
135 redraw()
136 // Save the count so it's there after a restart
137 await $.store.set('count', count)
138 },
139 }),
140 Text({ children: ['Count: ' + count] }),
141 ],
142 }),
143 ]
144
145 // The whole pane: the row of tabs, a blank line, then the body
146 return Box({
147 flexDirection: 'column',
148 children: [
114 // What goes under the tabs, depending on which one is open
115 const body =
116 tab === 'one'
117 ? [Text({ children: ['This is the first tab.'] })]
118 : [
149119 Box({
150120 flexDirection: 'row',
151 columnGap: 3,
152 children: [tabButton('one', 'One', '1'), tabButton('two', 'Two', '2')],
121 columnGap: 2,
122 children: [
123 Button({
124 key: 'more',
125 label: 'Add one',
126 hotkey: 'a',
127 onPress: async () => {
128 count += 1
129 redraw()
130 // Save the count so it's there after a restart
131 await $.store.set('count', count)
132 },
133 }),
134 Text({ children: ['Count: ' + count] }),
135 ],
153136 }),
154 Text({ children: [' '] }),
155 ...body,
156 ],
157 })
158 })
159 }
160 ```
161
162 Each hook also does something the code doesn't make plain:
163
164 * **[`session.start`](/docs/en/plugins/mods/reference#session)** also reads the saved count from [`$.store`](#keep-state), a key-value store that persists between sessions.
165 * **[`command.run`](/docs/en/plugins/mods/api#add-a-command)** only tells Claude Code the pane exists. Opening a pane draws nothing by itself: Claude Code then fires `ui.render` to ask what goes in it.
166 * **`ui.render`** returns the element tree, a `Box` that holds other boxes, text, and buttons, and builds it again from `tab` and `count` each time it runs.
167
168 Pressing a button runs its `onPress` callback, which changes a variable and calls `redraw`. Claude Code then runs the `ui.render` hook again, and the hook builds a new tree from the new values. Every interactive drawing uses that render cycle: a callback changes state, and the hook renders again from the new state.
169 </Step>
170
171 <Step title="Open the pane">
172 In your shell, start Claude Code with `claude --plugin-dir ./hello-tabs`. At the Claude Code prompt, run `/hello-tabs`. A pane opens with `1: One` and `2: Two` across the top. Press `2`, then press `a`, the hotkey for **Add one**, a few times. The count rises.
173 </Step>
174
175 <Step title="Check that the count was saved">
176 Press Esc to close the pane, then exit the session. In your shell, start Claude Code again with the same `claude --plugin-dir ./hello-tabs` command, and at the Claude Code prompt run `/hello-tabs`. The count is where you left it.
177
178 To clear the count, have the mod call `$.store.delete('count')`. [Keep state](#keep-state) covers how long each kind of value lasts.
179 </Step>
180</Steps>
137 ]
138
139 // The whole pane: the row of tabs, a blank line, then the body
140 return Box({
141 flexDirection: 'column',
142 children: [
143 Box({
144 flexDirection: 'row',
145 columnGap: 3,
146 children: [tabButton('one', 'One', '1'), tabButton('two', 'Two', '2')],
147 }),
148 Text({ children: [' '] }),
149 ...body,
150 ],
151 })
152 })
153}
154```
155
156Each hook also does something the code doesn't make plain:
157
158* **[`session.start`](/docs/en/plugins/mods/reference#session)** also reads the saved count from [`$.store`](#keep-state), a key-value store that persists between sessions.
159* **[`command.run`](/docs/en/plugins/mods/api#add-a-command)** only tells Claude Code the pane exists. Opening a pane draws nothing by itself: Claude Code then fires `ui.render` to ask what goes in it.
160* **`ui.render`** returns the element tree, a `Box` that holds other boxes, text, and buttons, and builds it again from `tab` and `count` each time it runs.
161
162Pressing a button runs its `onPress` callback, which changes a variable and calls `redraw`. Claude Code then runs the `ui.render` hook again, and the hook builds a new tree from the new values. Every interactive drawing uses that render cycle: a callback changes state, and the hook renders again from the new state.
163
164**3. Open the pane**
165
166In your shell, start Claude Code with `claude --plugin-dir ./hello-tabs`. At the Claude Code prompt, run `/hello-tabs`. A pane opens with `1: One` and `2: Two` across the top. Press `2`, then press `a`, the hotkey for **Add one**, a few times. The count rises.
167
168**4. Check that the count was saved**
169
170Press Esc to close the pane, then exit the session. In your shell, start Claude Code again with the same `claude --plugin-dir ./hello-tabs` command, and at the Claude Code prompt run `/hello-tabs`. The count is where you left it.
171
172To clear the count, have the mod call `$.store.delete('count')`. [Keep state](#keep-state) covers how long each kind of value lasts.
181173
182174## Pick where to draw
183175
from line 177
185177
186178The pane and the band are empty until a mod fills them. Select a tab to see what each one is and how to draw in it:
187179
188<Tabs>
189 <Tab title="Pane">
190 A pane is a sidebar beside the transcript in a wide fullscreen terminal, or a framed region above the prompt otherwise. With several panes open, each gets a tab that shows its title.
191
192 A pane appears when your mod calls `$.ui.open` with an `id` you choose, as in `$.ui.open({ id: 'hello-tabs' })`. [Open a pane at the right time](#open-a-pane-at-the-right-time) covers the other fields and when a pane waits for a wider terminal.
193
194 To draw in your pane, filter on `{ component: 'Pane' }` and check that `e.requestId` is your `id`.
195 </Tab>
196
197 <Tab title="Band above the prompt">
198 The band is a strip directly above the prompt input. It's always there, and every mod shares it.
199
200 Your hook returns a tree to show something in the band, or `next(e)` to show nothing. A tree replaces what the mods [after yours](/docs/en/plugins/mods/events#the-order-mods-run-in) draw there. To keep theirs, put the result of `await next(e)` among the children of a [`Box`](#build-a-tree-from-elements) in your tree.
201
202 To draw in the band, filter on `{ component: 'AbovePrompt' }`.
203 </Tab>
204</Tabs>
180**Pane**
181
182A pane is a sidebar beside the transcript in a wide fullscreen terminal, or a framed region above the prompt otherwise. With several panes open, each gets a tab that shows its title.
183
184A pane appears when your mod calls `$.ui.open` with an `id` you choose, as in `$.ui.open({ id: 'hello-tabs' })`. [Open a pane at the right time](#open-a-pane-at-the-right-time) covers the other fields and when a pane waits for a wider terminal.
185
186To draw in your pane, filter on `{ component: 'Pane' }` and check that `e.requestId` is your `id`.
187
188**Band above the prompt**
189
190The band is a strip directly above the prompt input. It's always there, and every mod shares it.
191
192Your hook returns a tree to show something in the band, or `next(e)` to show nothing. A tree replaces what the mods [after yours](/docs/en/plugins/mods/events#the-order-mods-run-in) draw there. To keep theirs, put the result of `await next(e)` among the children of a [`Box`](#build-a-tree-from-elements) in your tree.
193
194To draw in the band, filter on `{ component: 'AbovePrompt' }`.
205195
206196### Change what Claude Code already draws
207197
from line 210
220210
221211At a site Claude Code already draws, your hook can change a detail, replace the drawing, or leave it alone. Select a tab to see each one applied to the spinner. The examples read a `calls` variable that another hook counts, as in the [tutorial mod](/docs/en/plugins/mods/create#write-a-mod-yourself).
222212
223<Tabs>
224 <Tab title="Change a detail">
225 To keep Claude Code's drawing and change one part of it, pass `next` a copy of the event with changed `props`. This hook changes the text after the spinner's word:
226
227 ```javascript theme={null}
228 on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
229 // Keep Claude Code's spinner, and change the text after its word
230 return next({ ...e, props: { ...e.props, suffix: ' · tool calls: ' + calls + '…' } })
231 })
232 ```
233
234 The spinner keeps its animation and its word, and your text follows the word:
235
236 ```text theme={null}
237 Thinking · tool calls: 2…
238 ```
239 </Tab>
240
241 <Tab title="Replace the drawing">
242 To draw something of your own in the site's place, return a tree and don't call `next`. This hook draws one line of text where the spinner would be:
243
244 ```javascript theme={null}
245 on('ui.render', { component: 'Spinner' }, async ($, e) => {
246 const { Text } = $.ui.resolve(e)
247 // No call to next, so this line is drawn in the spinner's place
248 return Text({ children: ['Claude has made ' + calls + ' tool calls'] })
249 })
250 ```
251
252 While Claude works, your line shows and Claude Code's spinner doesn't:
253
254 ```text theme={null}
255 Claude has made 2 tool calls
256 ```
257 </Tab>
258
259 <Tab title="Leave it alone">
260 To leave the site as Claude Code draws it, return `next(e)`. A hook often does that for some events and not others. This hook leaves the spinner alone until there's a call to count:
261
262 ```javascript theme={null}
263 on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
264 // Nothing to show yet, so pass the event on unchanged
265 if (calls === 0) return next(e)
266 return next({ ...e, props: { ...e.props, suffix: ' · tool calls: ' + calls + '…' } })
267 })
268 ```
269
270 Before the first tool call, the spinner looks the way it does without the mod:
271
272 ```text theme={null}
273 Thinking…
274 ```
275 </Tab>
276</Tabs>
213**Change a detail**
214
215To keep Claude Code's drawing and change one part of it, pass `next` a copy of the event with changed `props`. This hook changes the text after the spinner's word:
216
217```javascript theme={null}
218on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
219 // Keep Claude Code's spinner, and change the text after its word
220 return next({ ...e, props: { ...e.props, suffix: ' · tool calls: ' + calls + '…' } })
221})
222```
223
224The spinner keeps its animation and its word, and your text follows the word:
225
226```text theme={null}
227Thinking · tool calls: 2…
228```
229
230**Replace the drawing**
231
232To draw something of your own in the site's place, return a tree and don't call `next`. This hook draws one line of text where the spinner would be:
233
234```javascript theme={null}
235on('ui.render', { component: 'Spinner' }, async ($, e) => {
236 const { Text } = $.ui.resolve(e)
237 // No call to next, so this line is drawn in the spinner's place
238 return Text({ children: ['Claude has made ' + calls + ' tool calls'] })
239})
240```
241
242While Claude works, your line shows and Claude Code's spinner doesn't:
243
244```text theme={null}
245Claude has made 2 tool calls
246```
247
248**Leave it alone**
249
250To leave the site as Claude Code draws it, return `next(e)`. A hook often does that for some events and not others. This hook leaves the spinner alone until there's a call to count:
251
252```javascript theme={null}
253on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
254 // Nothing to show yet, so pass the event on unchanged
255 if (calls === 0) return next(e)
256 return next({ ...e, props: { ...e.props, suffix: ' · tool calls: ' + calls + '…' } })
257})
258```
259
260Before the first tool call, the spinner looks the way it does without the mod:
261
262```text theme={null}
263Thinking…
264```
277265
278266At these sites, `next(e)` returns a reference to Claude Code's drawing, `{ type: 'engine', ref }`, unless a mod that runs after yours returned a tree of its own. To change what's in that drawing, pass `next` a copy of the event with different props, as the **Change a detail** tab does. You can return the reference as it is, or place it in a `Box` beside elements of your own:
279267
from line 353
365353
366354Select a tab to see each of the most common elements and how the terminal draws it:
367355
368<Tabs>
369 <Tab title="Text">
370 `Text` draws a string, with optional styling such as `bold` and `color`:
371
372 ```javascript theme={null}
373 Text({ children: ['This is the first tab.'] })
374 ```
375
376 ```text theme={null}
377 This is the first tab.
378 ```
379 </Tab>
380
381 <Tab title="Box">
382 `Box` arranges what's inside it, in a row or a column. This one puts a button and a line of text side by side, two columns apart:
383
384 ```javascript theme={null}
385 Box({
386 flexDirection: 'row',
387 columnGap: 2,
388 children: [
389 Button({ key: 'more', label: 'Add one', onPress: addOne }),
390 Text({ children: ['Count: 0'] }),
391 ],
392 })
393 ```
394
395 ```text theme={null}
396 [ Add one ] Count: 0
397 ```
398 </Tab>
399
400 <Tab title="Button">
401 `Button` is a control the user can press. It runs your `onPress` callback. With `plain: true` it has no brackets and shows its hotkey:
402
403 ```javascript theme={null}
404 Button({ key: 'more', label: 'Add one', onPress: addOne })
405 Button({ key: 'tab-one', label: 'One', hotkey: '1', plain: true, onPress: showTabOne })
406 ```
407
408 ```text theme={null}
409 [ Add one ]
410 1: One
411 ```
412 </Tab>
413
414 <Tab title="Input">
415 `Input` is a text field. It runs your `onSubmit` callback with the text when the user presses Enter:
416
417 ```javascript theme={null}
418 Input({
419 key: 'new-note',
420 label: 'Note',
421 placeholder: 'Type a note and press Enter',
422 value: '',
423 submitLabel: 'add',
424 onSubmit: addNote,
425 })
426 ```
427
428 ```text theme={null}
429 Note: Type a note and press Enter
430 ```
431 </Tab>
432</Tabs>
356**Text**
357
358`Text` draws a string, with optional styling such as `bold` and `color`:
359
360```javascript theme={null}
361Text({ children: ['This is the first tab.'] })
362```
363
364```text theme={null}
365This is the first tab.
366```
367
368**Box**
369
370`Box` arranges what's inside it, in a row or a column. This one puts a button and a line of text side by side, two columns apart:
371
372```javascript theme={null}
373Box({
374 flexDirection: 'row',
375 columnGap: 2,
376 children: [
377 Button({ key: 'more', label: 'Add one', onPress: addOne }),
378 Text({ children: ['Count: 0'] }),
379 ],
380})
381```
382
383```text theme={null}
384[ Add one ] Count: 0
385```
386
387**Button**
388
389`Button` is a control the user can press. It runs your `onPress` callback. With `plain: true` it has no brackets and shows its hotkey:
390
391```javascript theme={null}
392Button({ key: 'more', label: 'Add one', onPress: addOne })
393Button({ key: 'tab-one', label: 'One', hotkey: '1', plain: true, onPress: showTabOne })
394```
395
396```text theme={null}
397[ Add one ]
3981: One
399```
400
401**Input**
402
403`Input` is a text field. It runs your `onSubmit` callback with the text when the user presses Enter:
404
405```javascript theme={null}
406Input({
407 key: 'new-note',
408 label: 'Note',
409 placeholder: 'Type a note and press Enter',
410 value: '',
411 submitLabel: 'add',
412 onSubmit: addNote,
413})
414```
415
416```text theme={null}
417Note: Type a note and press Enter
418```
433419
434420The [interface gallery](/docs/en/plugins/mods/gallery) has samples and screenshots of most elements. This table lists every element:
435421
from line 436
450436
451437In a session started with `--plugin-dir`, a transcript line says so, such as `ui.render (Pane) refused: Text prop "bogusProp" is not allowed; the engine drew its own`. The [debug log](/docs/en/plugins/mods/troubleshoot#read-the-debug-log) records it as `ui.render (Pane): a hook returned a tree that does not validate` with the same reason. Nothing else appears in the session, so when a drawing doesn't show up, check that line or the log.
452438
453<h3 id="link-in-the-desktop-app">
454 `Link` in the Desktop app and VS Code
455</h3>
439### `Link` in the Desktop app and VS Code
456440
457441In the Desktop app and the VS Code extension, a `Link` draws as plain text unless its `href` meets these requirements:
458442
from line 446
462446
463447In the terminal, these requirements don't apply.
464448
465<h3 id="when-a-client-fails">
466 When a `Client` fails
467</h3>
449### When a `Client` fails
468450
469451In the terminal, when the file a `Client` runs fails, a dimmed line such as `my-mod: Client client/spinner.js: boom` takes the `Client`'s place, and the rest of your drawing still shows.
470452
from line 490
508490
509491In the terminal, the pane shows the grid:
510492
511<img src="https://mintcdn.com/claude-code/dgiVO_Od1X1faduV/images/mods-heat-map.svg?fit=max&auto=format&n=dgiVO_Od1X1faduV&q=85&s=b91bcce3bad74bc851149133d4acc5d5" alt="A pane in the terminal that holds a small grid of colored blocks, two rows of three. The top row is green, amber, and red. The bottom row is green, green, and amber." width="360" height="132" data-path="images/mods-heat-map.svg" />
493
512494
513495The `rows` array is the part you'd change, and `cellsOf` turns it into the packed string. The hook draws only in a pane whose `id` is `heat`, so open one with `$.ui.open({ id: 'heat' })` from a command, as the [`hello-tabs` example](#build-a-pane-with-tabs) opens its pane.
514496
from line 506
524506
525507A test presses or types into a control by its `key`, so give each control one. Each use of a control also fires [`ui.press`, `ui.input`, or `ui.select`](/docs/en/plugins/mods/reference#interface) with the `key` in `e.element`, and another mod can handle those events. Its hook runs before your callback, so it sees what the user types into your `Input` and can change it or answer in place of your callback. The mods API has no method that presses another mod's button.
526508
527<h3 id="know-which-keys-your-mod-can-receive">
528 Keyboard focus and hotkeys
529</h3>
509### Keyboard focus and hotkeys
530510
531511Your mod never reads the keyboard itself. The user presses a key, Claude Code decides which of your controls it's for, and that control's callback runs. Apart from a [digit hotkey on the band](/docs/en/plugins/mods/reference#elements), that happens only while your pane or band has keyboard focus. The rest of the time, keys go to the prompt.
532512
from line 651
671651
672652Submitting an `Input` doesn't start a turn unless your callback calls [`$.prompt.submit`](/docs/en/plugins/mods/api#start-a-turn-from-a-background-job).
673653
674<h2 id="redraw-when-something-changes">
675 Redraw a site
676</h2>
654## Redraw a site
677655
678656A drawing is a snapshot: it shows what your `ui.render` hook returned the last time the hook ran. To show something new, the hook has to run again. Claude Code runs it again for some changes, and your mod asks for the rest.
679657
from line 796
818796
819797Keep `redraw` for the tab buttons, because `tab` is still a variable.
820798
821<h3 id="load-a-saved-value-again-after-clear">
822 Load a saved value again after `/clear`
823</h3>
799### Load a saved value again after `/clear`
824800
825801If your mod copies a saved value from `$.store` into `$.state` at `session.start`, it has to copy it again after `/clear`, `/resume`, or `/branch`. Those commands reset every `$.state` value to its default, and `session.start` doesn't fire again. [`classic.SessionStart`](/docs/en/plugins/mods/events#hook-the-settings-hook-events) does fire after each of them, with `e.source` set to `clear`, `resume`, or `fork`, so copy the value again in a hook on it. Otherwise your drawing shows the default, and a callback that saves the `$.state` value writes the default over what you stored.
826802
827803
No line in this hunk matches that.