Follow Discord
Sweep 23 Oct 2026 · 21:48Z Build v2.1.310 529 read Stable v2.1.308 Latest v2.1.310 Next v2.1.310 Feeds RSS JSON llms.txt llms-full.txt Unofficial
One change · claude-code

Draw in the interface with a mod changedplugins/mods/interface

Nearest release: v2.1.309, published 3 hours before upstream edited the page. Shown because the two are within 24 hours of each other. Nothing here says the release caused the edit.

Upstream edited this page at 23 Oct 2026 00:33 UTC, give or take a minute or two: the time comes from Anthropic’s own sitemap rather than from a commit. This site recorded the change at 23 Oct 2026 19:07 UTC.

Upstream edited
Recorded here
Lines+40added
Lines−40removed
From line 2 where the diff opens
First seen 1 Oct 2026 this site's first read of the page
Recorded edits24to this page, all time

The whole hunk

from line 2, old and new numbered
/
lines
from line 2
22 
33> Draw panes, a band above the prompt, buttons, and text fields from a Claude Code mod, handle presses and input, and keep state between redraws and sessions.
44 
5A mod can draw its own interface in Claude Code and change parts of the interface Claude Code already draws. Each place a mod can draw is called a [render site](/docs/en/plugins/mods/reference.md#render-sites), such as a pane, the band above the prompt, or the spinner. Claude Code fires the [`ui.render`](/docs/en/plugins/mods/reference#interface) event each time it's about to draw a render site, and your hook for that event returns what to draw there.
5A mod can draw its own interface in Claude Code and change parts of the interface Claude Code already draws. Each place a mod can draw is called a [render site](https://code.claude.com/docs/en/plugins/mods/reference.md#render-sites), such as a pane, the band above the prompt, or the spinner. Claude Code fires the [`ui.render`](https://code.claude.com/docs/en/plugins/mods/reference#interface) event each time it's about to draw a render site, and your hook for that event returns what to draw there.
66 
77This map shows where a mod can draw in a terminal session:
88 
from line 10
1010 
1111In a narrower terminal, the pane sits above the prompt instead of beside the transcript.
1212 
13Build your [first mod](/docs/en/plugins/mods/create.md) 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.
13Build your [first mod](https://code.claude.com/docs/en/plugins/mods/create.md) 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.
1414 
1515> [!NOTE]
1616>
17> To look up one prop or limit, see the [reference](/docs/en/plugins/mods/reference.md#render-sites).
17> To look up one prop or limit, see the [reference](https://code.claude.com/docs/en/plugins/mods/reference.md#render-sites).
1818 
1919## Build a pane with tabs
2020 
from line 28
2828 
2929**1. Create the plugin**
3030 
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.md#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.
31A mod is a plugin with a manifest, a `hooks.json` that points to your code, and the code file. [Create a mod](https://code.claude.com/docs/en/plugins/mods/create.md#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.
3232 
3333Save the manifest as `hello-tabs/.claude-plugin/plugin.json`:
3434 
from line 155
155155 
156156Each hook also does something the code doesn't make plain:
157157 
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.
158* **[`session.start`](https://code.claude.com/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`](https://code.claude.com/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.
160160* **`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.
161161 
162162Pressing 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.
from line 173
173173 
174174## Pick where to draw
175175 
176A `ui.render` hook runs for every render site unless you narrow it to the one you want to draw in. To choose the render site, pass a filter, called a [matcher](/docs/en/plugins/mods/events.md#filter-which-events-a-hook-handles), as the second argument to `on`. `{ component: 'Pane' }` runs the hook only for panes. In the hook, `e.component` names the site, `e.surface` says which app is drawing, and `e.props` holds the site's own data. For a pane, `e.requestId` is the `id` you opened it with.
176A `ui.render` hook runs for every render site unless you narrow it to the one you want to draw in. To choose the render site, pass a filter, called a [matcher](https://code.claude.com/docs/en/plugins/mods/events.md#filter-which-events-a-hook-handles), as the second argument to `on`. `{ component: 'Pane' }` runs the hook only for panes. In the hook, `e.component` names the site, `e.surface` says which app is drawing, and `e.props` holds the site's own data. For a pane, `e.requestId` is the `id` you opened it with.
177177 
178178The 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:
179179 
from line 189
189189 
190190The band is a strip directly above the prompt input. It's always there, and every mod shares it.
191191 
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.md#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.
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](https://code.claude.com/docs/en/plugins/mods/events.md#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.
193193 
194194To draw in the band, filter on `{ component: 'AbovePrompt' }`.
195195 
from line 206
206206| `Spinner`, `ToolProgress`, `TurnDuration` | Status lines for a turn: the line that animates while Claude works, a running tool's live progress line, and the line that closes a turn |
207207| `InfoNotice`, `SessionMode`, `PromptHint` | Status lines under the logo, the mode labels in the footer, and the hint line under the prompt |
208208 
209`Pane`, `AskUserQuestion`, `Spinner`, `UserMessage`, `AssistantMessage`, and the tool call sites work in the terminal, the Desktop app, and the VS Code extension. The [render sites table](/docs/en/plugins/mods/reference.md#render-sites) lists where each of the others is raised.
209`Pane`, `AskUserQuestion`, `Spinner`, `UserMessage`, `AssistantMessage`, and the tool call sites work in the terminal, the Desktop app, and the VS Code extension. The [render sites table](https://code.claude.com/docs/en/plugins/mods/reference.md#render-sites) lists where each of the others is raised.
210210 
211At 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.md#write-a-mod-yourself).
211At 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](https://code.claude.com/docs/en/plugins/mods/create.md#write-a-mod-yourself).
212212 
213213**Change a detail**
214214 
from line 281
281281 
282282A pane appears only when your mod opens it. How and when you open it decides whether it takes keyboard focus, how much room it asks for, and whether it shows at all in a narrow terminal.
283283 
284To open a pane, call [`$.ui.open`](/docs/en/plugins/mods/reference#mods-api-methods) with an `id` you choose. The `id` is the pane's name: your `ui.render` hook checks for it, and you pass it again to close the pane.
284To open a pane, call [`$.ui.open`](https://code.claude.com/docs/en/plugins/mods/reference#mods-api-methods) with an `id` you choose. The `id` is the pane's name: your `ui.render` hook checks for it, and you pass it again to close the pane.
285285 
286286```javascript theme={null}
287287await $.ui.open({ id: 'hello-tabs', title: 'Hello tabs', focus: true })
from line 313
313313await $.ui.open(items.length > 0 ? { ...pane, focus: true } : pane)
314314```
315315 
316To let a command open the pane while Claude is working, add `immediate: true` when you [register the command](/docs/en/plugins/mods/api.md#add-a-command). Without it, a command typed during a turn waits for the turn to end.
316To let a command open the pane while Claude is working, add `immediate: true` when you [register the command](https://code.claude.com/docs/en/plugins/mods/api.md#add-a-command). Without it, a command typed during a turn waits for the turn to end.
317317 
318318#### Hold toasts behind a dialog
319319 
320320Pass `holdToasts: true` to `$.ui.open` when the pane is a dialog the user answers and leaves, so toasts don't appear while they decide. In the terminal, the hold lasts while that pane is the one showing, and a toast raised in that time waits until the hold ends.
321321 
322Claude Code holds other mods' toasts and its own short-lived notifications as well as the ones your mod raises with [`$.ui.toast`](/docs/en/plugins/mods/api#show-something-without-starting-a-turn). Leave the field off a pane that stays open, so the user keeps seeing them.
322Claude Code holds other mods' toasts and its own short-lived notifications as well as the ones your mod raises with [`$.ui.toast`](https://code.claude.com/docs/en/plugins/mods/api#show-something-without-starting-a-turn). Leave the field off a pane that stays open, so the user keeps seeing them.
323323 
324324#### When a pane waits for a wider terminal
325325 
from line 326
326326A pane your mod opens without being asked doesn't appear in a narrow terminal, so it can't take over a small screen. Whether it appears depends on what opened it:
327327 
328328* **Opened by something the user did**, such as a command they ran or a button they pressed, the pane appears at any width
329* **Opened by your mod acting by itself**, such as from a timer or a [`turn.start`](/docs/en/plugins/mods/events#follow-a-turn) hook, the pane appears only in a terminal at least 144 columns wide. After the user has opened that pane once themselves, 110 columns is enough.
329* **Opened by your mod acting by itself**, such as from a timer or a [`turn.start`](https://code.claude.com/docs/en/plugins/mods/events#follow-a-turn) hook, the pane appears only in a terminal at least 144 columns wide. After the user has opened that pane once themselves, 110 columns is enough.
330330 
331331When the pane appears, `$.ui.open` resolves to `{ isPlaced: true }`. When the pane is waiting, `isPlaced` is `false` and `reason` is a string that says why. A waiting pane appears when the user opens it or widens the terminal. To say something is available without opening a pane, call `$.ui.toast('Your message')`, which shows a toast notification.
332332 
333333### Draw in the VS Code extension
334334 
335In the VS Code extension, what a mod draws appears in the chat panel with extension version 2.1.299 or later. A session that [doesn't fetch feature flags](/docs/en/env-vars.md#features-that-need-feature-flag-fetching), such as one on Amazon Bedrock, needs 2.1.304 or later. In a `ui.render` hook for the panel, `e.surface` is `vscode`. To check which sites and elements the panel draws, and which props it ignores, see the [render sites](/docs/en/plugins/mods/reference.md#render-sites) and [elements](/docs/en/plugins/mods/reference.md#elements) tables.
335In the VS Code extension, what a mod draws appears in the chat panel with extension version 2.1.299 or later. A session that [doesn't fetch feature flags](https://code.claude.com/docs/en/env-vars.md#features-that-need-feature-flag-fetching), such as one on Amazon Bedrock, needs 2.1.304 or later. In a `ui.render` hook for the panel, `e.surface` is `vscode`. To check which sites and elements the panel draws, and which props it ignores, see the [render sites](https://code.claude.com/docs/en/plugins/mods/reference.md#render-sites) and [elements](https://code.claude.com/docs/en/plugins/mods/reference.md#elements) tables.
336336 
337337To make a mod work in the panel as well as the terminal, account for these differences:
338338 
from line 417
417417Note: Type a note and press Enter
418418```
419419 
420The [interface gallery](/docs/en/plugins/mods/gallery.md) has samples and screenshots of most elements. This table lists every element:
420The [interface gallery](https://code.claude.com/docs/en/plugins/mods/gallery.md) has samples and screenshots of most elements. This table lists every element:
421421 
422422| Element | What it draws | Where |
423423| :- | :- | :- |
424| `Box` | A flex container that takes layout props such as `flexDirection`, [`borderStyle`](/docs/en/plugins/mods/reference#box-border-styles), and `width`. Spacing props such as `columnGap` and `padding` take a number or a [step name](/docs/en/plugins/mods/reference.md#box-spacing-steps) from `'xs'` to `'xl'`. | Everywhere |
425| `Text` | Styled text. Takes `color`, `bold`, `dimColor`, `italic`, `wrap`, and others such as [`weight`](/docs/en/plugins/mods/reference#text-typography). A `color` is a theme key or a color such as `'red'`. A `wrap` is `'wrap'`, `'truncate'`, `'truncate-start'`, `'truncate-middle'`, or `'truncate-end'`. | Everywhere |
424| `Box` | A flex container that takes layout props such as `flexDirection`, [`borderStyle`](https://code.claude.com/docs/en/plugins/mods/reference#box-border-styles), and `width`. Spacing props such as `columnGap` and `padding` take a number or a [step name](https://code.claude.com/docs/en/plugins/mods/reference.md#box-spacing-steps) from `'xs'` to `'xl'`. | Everywhere |
425| `Text` | Styled text. Takes `color`, `bold`, `dimColor`, `italic`, `wrap`, and others such as [`weight`](https://code.claude.com/docs/en/plugins/mods/reference#text-typography). A `color` is a theme key or a color such as `'red'`. A `wrap` is `'wrap'`, `'truncate'`, `'truncate-start'`, `'truncate-middle'`, or `'truncate-end'`. | Everywhere |
426426| `Button` | A control that calls `onPress` | Everywhere |
427427| `Link`, `Code`, `Markdown` | A link with `href` and an optional `label`, a code block, and text formatted the way Claude's replies are. `Markdown` takes its content in a `text` prop, not in `children`, and needs a `key` when you pass `onLinkPress`. | Everywhere |
428428| `Input`, `Select` | A text field and a dropdown | Terminal, Desktop, VS Code |
429429| `Svg` | An SVG document | Desktop, VS Code |
430| `Client` | A region drawn by a second file of yours, for animation and pointer input. That file gets no mods API. It reaches your hooks by posting data, which arrives as a `ui.message` event. If it fails to load, draw, or run, your hooks receive a [`ui.fault`](/docs/en/plugins/mods/reference#interface) event. | Terminal, Desktop |
430| `Client` | A region drawn by a second file of yours, for animation and pointer input. That file gets no mods API. It reaches your hooks by posting data, which arrives as a `ui.message` event. If it fails to load, draw, or run, your hooks receive a [`ui.fault`](https://code.claude.com/docs/en/plugins/mods/reference#interface) event. | Terminal, Desktop |
431431| `Raster`, `Image` | A [grid of colored cells](#draw-a-grid-of-colored-cells), and a picture | Terminal |
432432 
433433If your module is a `.tsx` or `.jsx` file, you can write the tree as JSX. Destructure the elements from `$.ui.resolve(e)` first.
from line 434
434434 
435435If a tree uses an element the app doesn't have, a prop an element doesn't take, or a child where none goes, Claude Code draws its own version of the site.
436436 
437In 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.md#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.
437In 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](https://code.claude.com/docs/en/plugins/mods/troubleshoot.md#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.
438438 
439439### `Link` in the Desktop app and VS Code
440440 
from line 450
450450 
451451In 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.
452452 
453If your mod handles [`ui.fault`](/docs/en/plugins/mods/reference#interface), Claude Code then [draws the site again](#when-claude-code-redraws-without-being-asked).
453If your mod handles [`ui.fault`](https://code.claude.com/docs/en/plugins/mods/reference#interface), Claude Code then [draws the site again](#when-claude-code-redraws-without-being-asked).
454454 
455455### Draw a grid of colored cells
456456 
from line 504
504504* **`Input`**: takes `onSubmit(value)` and `onInput(value)`
505505* **`Select`**: takes `onSelect(value)` with its choices in `options`, a list of at least one choice with unique values, such as `[{ value: 'sm', label: 'Small' }, { value: 'lg', label: 'Large' }]`
506506 
507A 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.
507A 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`](https://code.claude.com/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.
508508 
509509### Keyboard focus and hotkeys
510510 
511Your 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 or an `action` binding](/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.
511Your 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 or an `action` binding](https://code.claude.com/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.
512512 
513513In the [VS Code extension](#draw-in-the-vs-code-extension), buttons have no hotkeys: the user clicks a button or presses Enter on it, and types into fields.
514514 
from line 518
518518 
519519* Your mod opens it with `focus: true` from a command or a press
520520* The user presses Ctrl+X then Tab
521* The user clicks it, which in the terminal needs [fullscreen rendering](/docs/en/fullscreen.md)
521* The user clicks it, which in the terminal needs [fullscreen rendering](https://code.claude.com/docs/en/fullscreen.md)
522522 
523523Claude Code grants `focus: true` only while the prompt is empty and nothing else has keyboard focus. A pane that opens while the user is typing doesn't take their keystrokes.
524524 
from line 552
552552| With brackets, the default | `[ Add one ]`, with no hotkey shown | The label with a small key beside it | The label, and the hotkey does nothing |
553553| With `plain: true` | `1: One` | The label with a small key beside it | The label, and the hotkey does nothing |
554554 
555In the terminal, name the key in a bracketed button's label, or use `plain: true`, so the user can see what to press. The [elements reference](/docs/en/plugins/mods/reference.md#elements) has the other `Button` rules: `action`, digit hotkeys on the band, and two buttons on one hotkey.
555In the terminal, name the key in a bracketed button's label, or use `plain: true`, so the user can see what to press. The [elements reference](https://code.claude.com/docs/en/plugins/mods/reference.md#elements) has the other `Button` rules: `action`, digit hotkeys on the band, and two buttons on one hotkey.
556556 
557557#### When the user collapses the band
558558 
559559In the terminal, while a mod draws in the band, the user can collapse the band to one row to get it out of the way:
560560 
561* **Click `[-]`**: while the band is expanded, Claude Code draws this mark at the right end of its first row. In [fullscreen rendering](/docs/en/fullscreen.md), the user clicks it to collapse the band.
562* **Press Ctrl+X then Ctrl+A**: this works whether the prompt, the band, or a pane has keyboard focus. The user can [bind other keys](/docs/en/keybindings.md#above-prompt-actions) to it through the `abovePrompt:toggle` action.
561* **Click `[-]`**: while the band is expanded, Claude Code draws this mark at the right end of its first row. In [fullscreen rendering](https://code.claude.com/docs/en/fullscreen.md), the user clicks it to collapse the band.
562* **Press Ctrl+X then Ctrl+A**: this works whether the prompt, the band, or a pane has keyboard focus. The user can [bind other keys](https://code.claude.com/docs/en/keybindings.md#above-prompt-actions) to it through the `abovePrompt:toggle` action.
563563 
564564A collapsed band hides everything the mods draw there and shows one dim row instead, which names the keys that expand it as the user has them bound:
565565 
from line 567
567567▸ plugin panel hidden · ctrl+x ctrl+a or click to show
568568```
569569 
570The same keys, or in fullscreen rendering a click on the row, expand the band again. Until then, nothing presses a button in it: no hotkey, no digit typed into an empty prompt, and no keybinding for a button's [`action`](/docs/en/plugins/mods/reference#elements). If the band has keyboard focus when the user collapses it, the focus returns to the prompt, and Ctrl+X then Tab skips the band while it's collapsed.
570The same keys, or in fullscreen rendering a click on the row, expand the band again. Until then, nothing presses a button in it: no hotkey, no digit typed into an empty prompt, and no keybinding for a button's [`action`](https://code.claude.com/docs/en/plugins/mods/reference#elements). If the band has keyboard focus when the user collapses it, the focus returns to the prompt, and Ctrl+X then Tab skips the band while it's collapsed.
571571 
572572The band stays collapsed until the user expands it, even after `/clear`, and is expanded again the next time Claude Code starts. Your mod can't expand it or tell that it's collapsed: the `ui.render` hook keeps running, and nothing in `e.props` says so. Don't make the band the only place your mod shows something the user has to answer.
573573 
from line 666
666666| `value` | `''` | The text the field starts with. It's empty here, so the placeholder shows. |
667667| `submitLabel` | `add` | The word after `⏎` that says what Enter does |
668668 
669Submitting 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).
669Submitting an `Input` doesn't start a turn unless your callback calls [`$.prompt.submit`](https://code.claude.com/docs/en/plugins/mods/api#start-a-turn-from-a-background-job).
670670 
671671## Redraw a site
672672 
from line 674
674674 
675675### When Claude Code redraws without being asked
676676 
677Claude Code runs your `ui.render` hook again when the site's props change or the terminal's width changes. When a `Client` in the site fails and your mod handles [`ui.fault`](/docs/en/plugins/mods/reference#interface), Claude Code runs the hook once more after your `ui.fault` hooks return, so your `ui.render` hook can leave the `Client` out. It doesn't run the hook on a timer, and it can't tell when a variable in your module changes.
677Claude Code runs your `ui.render` hook again when the site's props change or the terminal's width changes. When a `Client` in the site fails and your mod handles [`ui.fault`](https://code.claude.com/docs/en/plugins/mods/reference#interface), Claude Code runs the hook once more after your `ui.fault` hooks return, so your `ui.render` hook can leave the `Client` out. It doesn't run the hook on a timer, and it can't tell when a variable in your module changes.
678678 
679679### Redraw when your data changes
680680 
from line 711
711711 
712712### Redraw on a timer
713713 
714To keep a clock, a countdown, or a value from outside the session current, redraw on a schedule. Start a timer in the module's `session.start` hook. If the module already has one, as `hello-tabs` does, add the [`$.clock.every`](/docs/en/plugins/mods/api#run-work-in-the-background) line to it:
714To keep a clock, a countdown, or a value from outside the session current, redraw on a schedule. Start a timer in the module's `session.start` hook. If the module already has one, as `hello-tabs` does, add the [`$.clock.every`](https://code.claude.com/docs/en/plugins/mods/api#run-work-in-the-background) line to it:
715715 
716716```javascript theme={null}
717717on('session.start', async ($, e, next) => {
from line 725
725725 
726726### How often a site can redraw
727727 
728Claude Code throttles redraws of a site, so your mod can call `$.ui.invalidate` as often as its data changes. For how often each site can redraw, see the [limits table](/docs/en/plugins/mods/reference.md#limits).
728Claude Code throttles redraws of a site, so your mod can call `$.ui.invalidate` as often as its data changes. For how often each site can redraw, see the [limits table](https://code.claude.com/docs/en/plugins/mods/reference.md#limits).
729729 
730730Calls that come faster than the limit are coalesced into one redraw. That redraw runs your hook once, and the hook reads your data as it is at that moment, so the latest value shows and the values in between don't. An animation can't run faster than the limit.
731731 
from line 737
737737| :- | :- | :- |
738738| A module-level variable | The module reloads, which happens every time you save a file during development | Values you can lose, as `tab` is in `hello-tabs` |
739739| `$.state` | The session ends, or the user runs `/clear`, `/resume`, or `/branch` | Values a drawing depends on that should survive a reload |
740| `$.store` | Your mod deletes it, or no session reads or writes the store for [`cleanupPeriodDays`](/docs/en/settings-reference#cleanupperioddays). The store is a key-value store, saved as a JSON file of your plugin's own under `~/.claude/plugins/store/`. | Settings, history, anything the user expects to find next time |
740| `$.store` | Your mod deletes it, or no session reads or writes the store for [`cleanupPeriodDays`](https://code.claude.com/docs/en/settings-reference#cleanupperioddays). The store is a key-value store, saved as a JSON file of your plugin's own under `~/.claude/plugins/store/`. | Settings, history, anything the user expects to find next time |
741741 
742742`$.store.get(key)` resolves to the value or `undefined`, and `$.store.set(key, value)` takes any JSON value.
743743 
from line 815
815815 
816816### Load a saved value again after `/clear`
817817 
818If 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.
818If 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`](https://code.claude.com/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.
819819 
820820This code loads `count` from both hooks. It builds on the `$.state` version of `hello-tabs`, where `count` is an atom and `update` is imported. Put `loadCount` above `register`, and add the `loadCount` call to the `session.start` hook you already have. `classic.SessionStart` also fires at startup and after compaction, which doesn't reset `$.state`, so the filter on `source` keeps the hook to the three resets:
821821 
from line 843
843843 
844844`loadCount` writes the stored value over the one in `$.state`, and `session.start` fires again each time the module reloads. To keep the store from falling behind, save on every change, as the **Add one** button does.
845845 
846To check the reload without a session, [test the drawing after `/clear`](/docs/en/plugins/mods/test#test-a-drawing-after-clear).
846To check the reload without a session, [test the drawing after `/clear`](https://code.claude.com/docs/en/plugins/mods/test#test-a-drawing-after-clear).
847847 
848848### Save from more than one session
849849 
from line 870
870870 
871871## Next steps
872872 
873* [React to events](/docs/en/plugins/mods/events.md): feed your drawing from tool calls and turns
874* [Use the mods API](/docs/en/plugins/mods/api.md): feed your drawing from timers and model calls
875* [Test a drawing](/docs/en/plugins/mods/test.md#test-a-drawing): press your buttons from a test, on more than one surface
876* [Render sites](/docs/en/plugins/mods/reference.md#render-sites) and [elements](/docs/en/plugins/mods/reference.md#elements): each site's props and each element's props
873* [React to events](https://code.claude.com/docs/en/plugins/mods/events.md): feed your drawing from tool calls and turns
874* [Use the mods API](https://code.claude.com/docs/en/plugins/mods/api.md): feed your drawing from timers and model calls
875* [Test a drawing](https://code.claude.com/docs/en/plugins/mods/test.md#test-a-drawing): press your buttons from a test, on more than one surface
876* [Render sites](https://code.claude.com/docs/en/plugins/mods/reference.md#render-sites) and [elements](https://code.claude.com/docs/en/plugins/mods/reference.md#elements): each site's props and each element's props
877877 
Feedback