Sessions

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 / reasoning Deltas to the current text Part and tool_input Deltas to the current Tool Part's input_text.
  • Replace complete Part state by stable part_id.
  • Replace complete Message state by message_id + revision.
  • Treat the top-level error Message as the single user-visible error for one failure.
  • Render controls for a pending Interaction and a waiting state for its waiting-user Tool.

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:

  1. unsubscribe() from the old Session.
  2. Clear old messages, Turn state, and approval UI state.
  3. After obtaining the new Session, run initialization.
  4. 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.