Build a Chat UI
Combine history, streaming text, tools, approvals, stop, and reconnect into one Session client
Build a Chat UI
A usable Session chat UI does not need a second Timeline. It maintains one linear state of SessionMessage values and merges Mutations into that state.
Initialize
Subscribe and buffer first, then load snapshots so startup cannot lose a Delta:
const buffered: SessionMutation[] = [];
let ready = false;
const unsubscribe = session.subscribe(async (mutation) => {
if (!ready) {
buffered.push(mutation);
return;
}
await apply_live_mutation(mutation);
});
const [page, info] = await Promise.all([
session.messages(),
session.get_info(),
]);
set_messages(page.items);
set_session_info(info);
for (const mutation of buffered) {
await apply_live_mutation(mutation);
}
ready = true;See Reconnect and sync for full race handling.
Submit and stop
async function submit(query: string): Promise<void> {
const turn = await session.prompt({ query });
set_active_turn(turn.id);
const result = await turn.finished;
clear_active_turn(turn.id);
set_turn_status(turn.id, result.success ? "completed" : "failed");
}
async function stop(): Promise<void> {
const result = await session.stop();
if (result.cancelled_queued_prompts > 0) {
show_notice("Input that had not started was cancelled");
}
}Disable Send for empty input. Drive the Stop button from Turn Mutations or get_info().executing.
Rendering rules
- Render the conversation by top-level Message
sequence. - Render text, tool, then text inside an Assistant by Part
sequence. - Render one role heading per Assistant Message. Parts from Tool Loops and Provider continuations stay in that same role container.
- Append
text/reasoningDeltas to the current text Part andtool_inputDeltas to the current Tool Part'sinput_text. - Replace complete Part state by stable
part_id. - Replace complete Message state by
message_id + revision. - Treat the top-level
errorMessage as the single user-visible error for one failure. - Render controls for a
pendingInteraction and a waiting state for itswaiting-userTool.
Do not create Transcript Message DTOs, flatten tool calls into another persisted list, or overwrite an Assistant already rendered by Parts with final turn.text. Use turn.finished.error only for control flow and logging; when the UI already renders an error Message, do not append another error notice.
Approval card
Approval UI gets the same identity from an Interaction Part or session.interactions():
await session.respond({
interaction_id,
response: { kind: "approval", decision: "approved" },
});After submission, wait for the next Part Mutation. Change tool display state only when the SDK publishes running, completed, or failed.
Session switch and unmount
Switch Sessions in this order:
unsubscribe()from the old Session.- Clear old messages, Turn state, and approval UI state.
- After obtaining the new Session, run initialization.
- Never reuse the old Session's
interaction_id,part_id, or Turn loading state.
This boundary prevents approval decisions from targeting the wrong Session and prevents old Deltas from appearing in the new history.