Customizing¶
Elements are protocol-independent: they render the chat model (ToolPart.kind / source, data
parts, messages), whichever backend produced it. Customize how things render app-wide, without
forking Conversation.
Renderers¶
LocalAiElementsRenderers replaces how parts render:
CompositionLocalProvider(
LocalAiElementsRenderers provides AiElementsRenderers(
tools = mapOf("get_weather" to ToolRenderer { part, _ -> WeatherCard(part.output) }),
data = mapOf("chart" to DataRenderer { part -> MyChart(part.data) }), // `data-chart` parts
),
LocalFileLoader provides FileLoader { url -> myHttp.bytes(url) }, // attachment previews
) {
Chat(controller)
}
| Key | Replaces |
|---|---|
tools |
a tool call, by tool name |
tool |
any tool call you choose (return null to keep the built-in rendering) |
data |
a data part, by name (data-chart → "chart"), including keys of the agent's shared state |
codeBlocks |
a fenced code block, by language |
Tool renderers receive the full ToolDecision callback, including reason, edited arguments and
remember choice. A null callback means read-only rendering. Use ToolCall inside a renderer to
keep the standard approval controls; do not call ToolPartView there, which would select the same
renderer recursively.
@Composable
fun CustomToolDecisions(controller: ChatController) {
CompositionLocalProvider(
LocalAiElementsRenderers provides AiElementsRenderers(
tools = mapOf("write_file" to ToolRenderer { part, respond ->
ToolCall(part, onDecision = respond?.let { cb -> { decision -> cb(part.id, decision) } })
}),
),
) { Chat(controller) }
}
LocalFileLoader loads attachment previews with your HTTP stack, cache or authentication.
Theme¶
AiElementsTheme(colorScheme = brandColors, typography = brandType, shapes = brandShapes) {
Chat(controller)
}
Links¶
Web links in content (citations, sources, Markdown links, A2UI and MCP Apps links) open in a
Custom Tab: the user's browser inside the
app, with the theme's colours and a way back to the chat. mailto:, tel: and other schemes go to
the app that handles them. Every element opens links through Compose's LocalUriHandler, which
AiElementsTheme sets to a CustomTabsUriHandler:
AiElementsTheme(openLinksInCustomTabs = false)keeps the platform handler (the default browser);CustomTabsUriHandler(context, preferNativeApp = true)opens a link in an installed app that handles it (a video in its app, for example) before the Custom Tab;- provide your own
LocalUriHandlerinside the theme to route links in the app.
Tools say what they are¶
A tool's meaning is a model field, never guessed from its name: ToolKind.Function,
ToolKind.Delegation (renders as a Subagent) or ToolKind.Skill (a skill badge), and source
(for example an MCP server's name, shown as a chip). On-device tools declare them with
AgentTool.kindFor and AgentTool.source, so custom tools render like built-in ones. Protocol
mappings in ai-elements-core set them for server tools.
If an element needs more information, extend the model rather than matching a tool name in the UI.
Native Mermaid¶
The default Mermaid renderer is bundled and offline. To select the optional native renderer,
add ai-elements-mermaid-native and provide NativeMermaidRenderer through
LocalMermaidRenderer. Annotate the composable that references it with
@OptIn(dev.ai.elements.mermaid.ExperimentalNativeMermaidApi::class).
This opt-in is required because native layout and its interface remain experimental;
the stable default renderer needs no such opt-in.