Skip to content

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)
}

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 LocalUriHandler inside 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.