Skip to main content

Surfaces, Elements, and Render Sites

An element is a building block of a drawing, such as Box, Text, or Button. A render site is a part of Claude Code that a mod may redraw, such as the spinner or the prompt hint. A ui.render hook gets the site in e.component and returns a tree of elements. The 15 render sites are AskUserQuestion, UserMessage, AssistantMessage, ToolUse, ToolResult, ToolGroup, ToolProgress, CommandOutput, Spinner, TurnDuration, InfoNotice, SessionMode, PromptHint, AbovePrompt, and Pane. The engine alone draws the permission dialog. A mod adds context to it with $.ui.notice.

Return the engine's tree (call next(e)) to leave a site as it is. JSX needs a .tsx or .jsx file.

Official pages: Draw in the Interface, Mods Overview, Mods Reference. Source: the program (types-2.1.304.d.ts).

Example​

This hook adds a tail to the hint line under the prompt.

on('ui.render', { component: 'PromptHint' }, async ($, e, next) =>
next({ ...e, props: { ...e.props, tail: ' (mod)' } }))

There are no string tags in JSX. Every tag is an element from $.ui.resolve(e), and JSX compiles a tag to h(Box, props, ...children).

on('ui.render', { component: 'Pane' }, async ($, e) => {
const { Box, Text } = $.ui.resolve(e)
return <Box><Text>Hello</Text></Box>
})

Surfaces​

ItemStatusWhat it doesElementsWhere it can be used
terminalDocumentedInk. Draws the whole tree of the hook.Box, Text, Button, Input, Select, Link, Code, Markdown, Client, Raster, Image. No Svg.e.surface === 'terminal'
desktopDocumentedClaude Code Desktop. Asks over the wire.Box, Text, Button, Input, Select, Svg, Link, Code, Markdown, Client.e.surface === 'desktop'
mobileDocumentedThe mobile app. Asks over the wire.Box, Text, Button, Svg, Link, Code, Markdown. No Input, Select, or Client yet. The docs name no mobile surface.e.surface === 'mobile'
vscodeDiffersThe editor webview. The same table as desktop without Client.Box, Text, Button, Input, Select, Svg, Link, Code, Markdown.e.surface === 'vscode'

Render Sites and Props​

"Read-only" means a rewrite must carry the prop on as received. A change is refused. Many sites have onScreen (type OnScreen \| null, optional, read-only): which of its rows the viewport shows now. It is null while drawn outside the viewport, and absent where the surface does not say.

AskUserQuestion​

The dialog the AskUserQuestion tool opens. A tree for it holds the engine's node once. At most 12 rows on the terminal and 48 on other surfaces. Markdown works on other surfaces only.

ItemStatusWhat it is forShape and limits
toolDocumentedThe name of the tool whose call opened the dialog.string.
questionsDocumentedThe tool's questions input, as the dialog draws them. A rewrite must still fit the tool's schema, or the original is drawn.unknown[].
metadataSourceDocumentedThe call's metadata.source (who asked), when the model gave one. Analytics only, never drawn.string, optional.

UserMessage​

A user-role transcript row: the person's prompt, a background task notification, or a message from another agent, teammate, or session. A rewrite of text changes only what is drawn. The stored message stays as it was.

ItemStatusWhat it is forShape and limits
textDocumentedWhat the row shows.string. A rewrite is drawn up to 100000 characters. A teammate block with several frames keeps the engine's drawing.
originDocumentedWhere the stored message came from, as prompt.submit named it: composer, task notification, peer, channel, or plugin.PromptOrigin. Read-only. unclassified for a teammate's message and for a message stored before stamps.
isExpandedDocumentedWhether the view draws the row in full: the ctrl+o transcript, --verbose, or a surface with no ctrl+o.boolean. Read-only. When false, a message row is one dim line that names the sender.
taskDocumentedWhat a notification row (origin.kind task-notification) reports on: its background task.UserMessageTask, optional. Read-only.
fromDocumentedWho sent the message: another agent of this session, a teammate, another session, or a channel.UserMessageFrom, optional. Read-only.
onScreenDocumentedWhich rows are in the viewport.See above.

AssistantMessage​

One block of an assistant reply: its text, or a summary of the text the model wrote between two tool calls. A rewrite changes the drawing and leaves the stored message alone. It is raised on every attached surface.

ItemStatusWhat it is forShape and limits
textDocumentedThe block's text in markdown, as the surface will draw it.string.
isFirstOfReplyDocumentedTrue on the block that draws the bullet that opens a reply.boolean.
isSummaryDocumentedTrue on a block that holds a summary of the model's text.true, optional, read-only. A rewrite that changes it is refused.
onScreenDocumentedWhich rows are in the viewport.See above.

ToolUse​

A tool call's row in the transcript, such as Bash(ls -la) and its result. The call was decided by tool.call, so a rewrite here changes the row alone. Raised on every attached surface.

ItemStatusWhat it is forShape and limits
tool_use_idDocumentedThe id tool.call carried for this call. Same value as the row's requestId.string. Read-only.
toolDocumentedWhich tool the row draws (Bash, Read, a plugin's tool).string.
inputDocumentedThe call's input, as the model sent it.unknown.
isRunningDocumentedTrue while the call is still running.boolean.
isErroredDocumentedTrue when the call ended in an error. A refusal at the dialog counts.boolean.
isInterruptedDocumentedTrue when an abort ended the call: the user's Esc or a plugin's $.turn.abort.boolean. The row draws Interrupted.
outputDocumentedThe stored result once the call has resolved. Undefined while it runs.unknown, optional. For a failed, refused, or cut call it is the text the model read.
onScreenDocumentedWhich rows are in the viewport.See above.

ToolResult​

The result block under a standalone tool row, which the tool's own result renderer draws from output. A rewrite of output is checked against the tool's output schema. One that does not fit draws nothing. The stored result is untouched.

ItemStatusWhat it is forShape and limits
tool_use_idDocumentedThe id of the call this result belongs to.string. Read-only.
toolDocumentedWhich tool the result belongs to.string. Read-only.
outputDocumentedThe tool's own result object. The same one ToolUse.output carries. A rewrite is drawn.unknown.
isErroredDocumentedTrue when the call ended in an error. The row then draws the error text, not output.boolean. Read-only.
onScreenDocumentedWhich rows are in the viewport.See above.

ToolGroup​

A run of tool calls that the transcript folds into one count line, such as Read 3 files, ran 2 shell commands. A hook that sets isExpanded unfolds the group. Each unfolded row is a ToolUse. The fullscreen ctrl+o view draws no ToolGroup.

ItemStatusWhat it is forShape and limits
callsDocumentedThe calls of the group, in the order the model made them.ReadonlyArray<ToolGroupCall>.
isActiveDocumentedTrue while the group is the live one. A call in it may still run, and the model's next call may join it.boolean.
isExpandedDocumentedWhether each call draws as its own ToolUse row, or the group draws one line.boolean. True under --verbose and in the non-fullscreen ctrl+o transcript. The one prop a rewrite changes on screen.
onScreenDocumentedWhich rows are in the viewport.See above.

CommandOutput​

The output row a slash command printed in the transcript, under its echo. A rewrite of text changes only what is drawn. The model still reads the stored row.

ItemStatusWhat it is forShape and limits
commandDocumentedWhich command printed the row, as command.run named it (no slash).string. Read-only. A change or a drop is refused, and the engine draws its own row.
argsDocumentedThe arguments of the run, as the echo shows them. *** for a command that marks its arguments sensitive.string. Read-only.
textDocumentedThe text of the row, in markdown.string.
isErroredDocumentedTrue when the row is the error line of a command that threw.boolean. Read-only.
onScreenDocumentedWhich rows are in the viewport.See above.

ToolProgress​

One row of the live tool-progress region under a running tool call. Today it is the run-in-background pill. A hook matches { props: { kind: "background_hint" } }.

ItemStatusWhat it is forShape and limits
tool_use_idDocumentedThe tool call the row belongs to.string. Read-only. While several foreground calls share one pill, the first of them.
kindDocumentedWhich progress row.'background_hint'. Read-only.
hintDocumentedThe text of the pill as the engine draws it, dim. For example (ctrl+b to run in background).string. A rewrite is drawn in its place. "" draws nothing.

Spinner​

The line that animates while a turn runs, such as Sauteing... (12s, 300 tokens). On the desktop it is the row that carries the mark of the turn.

ItemStatusWhat it is forShape and limits
wordDocumentedThe animated word (Sauteing), as sampled for this turn.string. On the desktop it is what the step is doing (Creating notes.md), or Working.
messageDocumentedThe text drawn instead of the word while a state overrides it. Otherwise null.string | null.
suffixDocumentedWhat the engine draws after the word to say the turn is still going: one ellipsis character.string. Not added to a text that already ends in an ellipsis.
modeDocumentedWhat the turn is doing.'requesting' | 'responding' | 'thinking' | 'tool-input' | 'tool-use'. The desktop never says tool-input.

TurnDuration​

The line that closes a turn in the transcript, such as Baked for 3s. Only the terminal surface raises it. A remote surface draws its own footer.

ItemStatusWhat it is forShape and limits
wordDocumentedThe past-tense word the line drew (Baked).string.
durationMsDocumentedThe duration of the turn in milliseconds.number.
onScreenDocumentedWhich rows are in the viewport.See above.

InfoNotice​

One dim status line under the logo (the model source, an experiment enrollment, a settings hint), with a trailing /command. Terminal only.

ItemStatusWhat it is forShape and limits
textDocumentedThe text of the notice, flattened to one string.string.
commandDocumentedThe slash command appended after the text.string | null.
onScreenDocumentedWhich rows are in the viewport.See above.

SessionMode​

The dim mode labels at the right of the prompt footer (focus, memory paused), joined by &. One instance. Terminal and desktop only. A hook adds a mode by rewriting modes, removes one by filtering, or draws its own tree.

ItemStatusWhat it is forShape and limits
modesDocumentedThe labels the footer shows, in order. Empty when there are none.readonly string[].

PromptHint​

The dim hint line under the prompt (? for shortcuts, esc to interrupt, and the pills beside them). One instance.

ItemStatusWhat it is forShape and limits
isDraftDocumentedTrue while the prompt holds typed text.boolean. Read-only.
isWorkingDocumentedTrue while a model turn is running.boolean. Read-only.
hintDocumentedThe text of the line as the engine draws it, one string. A rewrite replaces the line.string.
tailDocumentedText a hook adds after the line.string, optional. The terminal draws it dim. It is left out when fewer than four columns would show.

AbovePrompt​

The band directly above the prompt input, where surveys draw. The engine draws a [-] beside the tree and an n more row under it. A hook draws one tree or passes. The person collapses it (ctrl+x ctrl+a) or focuses it (a click, ctrl+x tab).

ItemStatusWhat it is forShape and limits
hasSurveyDocumentedTrue while a survey holds the band. A hook yields to it.boolean. Read-only.
isWorkingDocumentedTrue while a model turn is running.boolean. Read-only.
maxRowsDocumentedRows the band may take. In fullscreen, what the bottom slot has left above the prompt. Otherwise the terminal's height.number. Read-only. The slot is capped at half the terminal's rows. A taller tree scrolls in a window of scroll.bodyRows.
bodyColumnsDocumentedCells the tree is laid out in.number. Read-only. Size a table to this value, not to viewport.columns.
scrollDocumentedThe window over a tree taller than maxRows. Engine-owned. Moved by the wheel and by the person's keys while the band is focused.SiteScroll. bodyRows is maxRows less the n more row.
viewDocumentedWhich transcript is on screen above the band: the main conversation or one agent's.SiteView. Read-only. A switch of view re-runs the hook.

Pane​

The framed region a plugin opened with $.ui.open({ id }). There is one instance for each id (requestId). The body is the hook's tree. One pane shows, the rest are tabs. The surface places it (docked in fullscreen, otherwise above the prompt). The person closes it, which raises ui.close with origin person, and a hook may refuse.

ItemStatusWhat it is forShape and limits
titleDocumentedThe label of its tab while more than one pane is open.string. Read-only here. Another $.ui.open, or a hook on ui.open, retitles it.
isFocusedDocumentedTrue while the person has given the pane the keyboard. Tab walks its elements, a Button's hotkey presses it, and the arrows scroll.boolean. Read-only.
bodyColumnsDocumentedCells across the body, inside the frame.number. Read-only.
placementDocumentedWhere the surface seated the pane: dock beside the transcript (the terminal in fullscreen from 110 columns), or inline above the prompt.'dock' | 'inline'. Read-only. A change is refused.
scrollDocumentedThe window of the body over the tree. Engine-owned.SiteScroll. bodyRows is the room the frame may use. Inline, the frame fits the tree.
viewDocumentedWhich transcript is on screen beside the pane.SiteView. The pane stays open across a switch of view.

JSX and Globals​

ItemStatusWhat it doesHow it worksWhere it can be used
h(tag, props, ...children)DocumentedThe JSX factory (classic runtime). Returns a plain-data element from a string tag or a component.JSX compiles to bare h(...) calls. A module must not declare, import, or take as a parameter anything named h or Fragment where it writes JSX. The engine adds the pragma.Hooks modules and surface modules.
FragmentDocumented<>...</>. A column Box around the children.The same naming rule as h.Hooks modules and surface modules.
JSX namespaceDocumentedElement is RenderElement. IntrinsicElements is empty, so there are no string tags. IntrinsicAttributes allows key.Every tag is a constructor from $.ui.resolve(e), as in const { Box, Text } = $.ui.resolve(e).Files named .tsx or .jsx.
Web globalsDocumentedURL, URLSearchParams, TextEncoder, TextDecoder, AbortController, AbortSignal, atob, btoa, structuredClone, crypto.subtle.digest, crypto.randomUUID, crypto.getRandomValues, and performance.now.There is no DOM, no Node, no require, and no import(). eval and new Function over a string throw. There is no WebAssembly. A surface module also has console and no timers.Hooks modules and surface modules.
Module rulesDocumentedA file ends in .ts, .tsx, .jsx, .js, .mjs, .cjs, .mts, or .cts. It is an ES module whatever its suffix.A file of the plugin is imported with an import declaration. A module with import() does not load. on, $, $.env names, and $.state references must be spelled literally.Hooks modules.