> ## Documentation Index
> Fetch the complete documentation index at: https://docs.metabind.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom host UI

> Replace the default Assistant SDK chat surface with your own UI

On iOS, the Assistant SDK ships a default chat surface — `MetabindAssistantView` — that handles most cases. When you need full control over the UI (a different layout, a non-chat interaction model, custom branding beyond what theming covers), use the SDK's lower-level API to drive your own surface. On Android there's no default surface — you always build the chat UI yourself; see the [Android SDK](/guides/assistant-sdk/android-sdk).

## Why go custom

Most teams ship the default UI for the first release and customize later. Cases where custom is right from day one:

* Your assistant lives inside a non-chat surface — a sidebar, an inline panel, a voice-first interaction.
* You want UI primitives that don't fit a chat metaphor — e.g., a multi-pane workspace where the assistant is one component.
* Your design system mandates components that diverge significantly from the SDK's chat default.
* You're building an agent UI (open-ended task execution) rather than a chat UI.

If your case is "the chat looks fine but I want a different color scheme," start with theming the default — it covers far more than it appears to.

## What the lower-level API gives you

The default chat surface is built on a small public API on the `MetabindAssistant` object:

| Member                   | Purpose                                                            |
| ------------------------ | ------------------------------------------------------------------ |
| `assistant.send(text)`   | Submit a user message; returns when the response stream completes. |
| `assistant.conversation` | Observable conversation state. `messages` holds the running turns. |
| `assistant.cancel()`     | Cancel the in-flight turn.                                         |
| `assistant.isProcessing` | Observable boolean — true while a turn is streaming.               |

Tool result UI is rendered through the BindJS native renderer — `BindJSView` on iOS, and `BindJSView` from `bindjs-android` on Android. Each message's tool results carry the BindJS spec; you hand it to the renderer.

## iOS example

```swift theme={null}
import SwiftUI
import MetabindAssistant
import BindJS

struct CustomAssistant: View {
  @ObservedObject var assistant: MetabindAssistant

  @State private var input: String = ""

  var body: some View {
    VStack {
      ScrollView {
        ForEach(assistant.conversation.messages) { message in
          MessageView(message: message)
        }
      }

      HStack {
        TextField("Ask something…", text: $input)

        if assistant.isProcessing {
          Button("Stop") { assistant.cancel() }
        } else {
          Button("Send") {
            let text = input
            input = ""
            Task { await assistant.send(text) }
          }
        }
      }
      .padding()
    }
  }
}

struct MessageView: View {
  let message: ConversationMessage

  var body: some View {
    VStack(alignment: .leading) {
      if let text = message.text { Text(text) }

      ForEach(message.toolResults) { result in
        BindJSView(spec: result.ui)
      }
    }
  }
}
```

`MetabindAssistant` is an `ObservableObject` — bind it with `@ObservedObject` or `@StateObject`. Tool results render through `BindJSView`, the SwiftUI renderer for BindJS specs.

## Android

The Android SDK doesn't expose a `MetabindAssistant` object — on Android you build the chat UI directly on the streaming API (`MetabindAgentProvider.streamMessage(...)`) and render tool results with `bindjs-android`. That flow, and the demo app that implements it end to end, are covered in the [Android SDK](/guides/assistant-sdk/android-sdk).

## What the default UI does that you'll need to handle

If you replace the default UI, replicate (or skip) these as needed:

| Feature                        | Why                                                                     |
| ------------------------------ | ----------------------------------------------------------------------- |
| Streaming token rendering      | Show partial responses as they arrive — read `message.text` reactively. |
| Tool call status               | "Calling product\_search…" while the tool runs.                         |
| Error states                   | Tool failures, network errors, cancellations.                           |
| Cancel button                  | Let users abort a long response — call `assistant.cancel()`.            |
| Auto-scroll                    | Keep the latest content in view.                                        |
| Accessibility                  | Dynamic type, screen reader support — VoiceOver / TalkBack.             |
| Empty / loading / error states | Cover the conversation lifecycle.                                       |

## Conversation state observability

The conversation state is the source of truth. Patterns:

* **Read-only views.** Render the conversation in a different surface (e.g., a summary sidebar) by subscribing to the same state.
* **Multi-pane layouts.** A chat pane on one side, a tool result detail pane on the other — both subscribed to the same state, both displaying different slices.
* **Server-side replay.** Persist messages to your backend (the SDK does not ship a persistence adapter today); rehydrate on next session by re-creating the assistant and replaying messages.

## Persistence

Conversation state is held in memory by the `Conversation` observable on the assistant. If your app needs persistence across launches or reloads, serialize `assistant.conversation.messages` to your platform's storage (Core Data, SwiftData, Room, IndexedDB, your backend) and rehydrate on next launch.

## Related

<CardGroup cols={2}>
  <Card title="iOS SDK" icon="apple" href="/guides/assistant-sdk/ios-sdk">
    Default chat surface and configuration.
  </Card>

  <Card title="Android SDK" icon="android" href="/guides/assistant-sdk/android-sdk">
    Default chat surface and configuration.
  </Card>

  <Card title="LLM provider configuration" icon="brain" href="/guides/assistant-sdk/llm-provider-configuration">
    Agent proxy vs. BYOK for the LLM call.
  </Card>

  <Card title="Assistant SDK overview" icon="rocket" href="/guides/getting-started/embed-an-assistant">
    Conceptual: when to embed and what's in the box.
  </Card>
</CardGroup>
