Why Use These?
Building a chat list by hand means handling placeholder bubbles, tool cards, empty slots between tool calls, paging through history, and error recovery. The SDK ships two classes that do all of that for you:
Both update on the main thread and work with
@Observable, so SwiftUI redraws when they change. Keep them in @State, in a view model, or in the environment, whichever your app already does.
ConversationState
@MainActor @Observable final class ConversationState
init
ChatbaseClient
required
The client to send through. Register your tools on it first.
String?
Reopen an existing conversation. Leave it out for a new one.
A complete chat screen
Showing a message
What you can read
You cannot set these directly. Use the methods below.
sendMessage
error, and the failed bubble is marked with isError.
isSending is true, so you do not have to guard the button yourself (though disabling it is nicer).
The conversation ID is taken from the first reply and reused after that.
retry
messages, then the new reply streams in.
UiMessage.messageId, not UiMessage.id. The call is ignored if a message is already being sent, if there is no conversation yet, or if that ID is not in the list.
loadHistory
messages with the newest page of an existing conversation and switches to it. Use it when opening a conversation from a list:
loadMoreHistory
hasMoreHistory is false or a load is already running.
clear
client.logout(), or when starting a new chat:
clearError and setConversationId
clearError() dismisses the last error. setConversationId(_:) points the state at a conversation without loading its messages, which is useful if you already have them from somewhere else.
UiMessage
struct ConversationState.UiMessage: Identifiable, Sendable
One row on screen. A single message from the server can become several UiMessage values, one per part, so text and tool cards appear in the order the agent made them.
String
required
A stable ID for
ForEach. Made locally for new rows, and from the server’s message for older ones.String?
The server’s message ID, filled in once the reply finishes. Pass this to
retry(messageId:).Kind
required
.text(String) for a bubble, .toolCall(ToolCallCard) for a tool card.MessageSender
required
.user or .agent. Tool cards are always .agent.Date
required
When the row was made, or when the message was sent.
Bool
required
true while text is still arriving in this bubble. Use it to show a typing dot or a cursor.Bool
required
true when the reply failed. Show a retry button.MessageFeedback?
.positive, .negative, or nil. Set for messages loaded from history.ToolCallCard
struct ConversationState.ToolCallCard: Sendable
.executing as soon as the agent asks for the tool, then becomes .success or .failure when the result arrives.
The status follows the SDK’s usual rule: a result that is an object with an
error key counts as .failure, and anything else counts as .success. This is the same rule your handlers use to report a problem. See Client-Side Tools.If the whole reply fails, every card still showing .executing is switched to .failure, so no card spins forever.ConversationListState
@MainActor @Observable final class ConversationListState
A list of the user’s past conversations, loaded a page at a time.
load
conversations. Use it for the first load and for pull to refresh.
loadMore
hasMore is false or a page is already loading.
clearError
Using UIKit
Neither class is tied to SwiftUI. From UIKit you can watch them withwithObservationTracking, or skip them and call client.send(_:conversationId:configure:) yourself, switching to the main thread inside the callbacks:
Related
Streaming
The send and callback API underneath
Client-Side Tools
What fills in the tool cards
Conversations & History
The paging behind these classes
Error Handling
Turning
state.error into a good message